Loading pager/pager.go +107 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,60 @@ import ( "golang.org/x/term" ) // readLine reads a line of input in raw mode, echoing characters. // Returns the entered string, or "" if ESC is pressed or input is empty. func readLine(prompt string) string { fmt.Print(prompt) var buf []byte for { key, err := readKey() if err != nil { return "" } switch key { case 0x1b: // ESC return "" case '\r', '\n': return string(buf) case 0x7f, 0x08: // backspace if len(buf) > 0 { buf = buf[:len(buf)-1] fmt.Print("\b \b") } default: if key >= 0x20 { buf = append(buf, key) fmt.Printf("%c", key) } } } } // highlightLine returns the line with all case-insensitive occurrences of term // wrapped in ANSI reverse video. func highlightLine(line, searchTerm string) string { if searchTerm == "" { return line } lower := strings.ToLower(line) lowerTerm := strings.ToLower(searchTerm) var b strings.Builder i := 0 for i < len(lower) { idx := strings.Index(lower[i:], lowerTerm) if idx < 0 { b.WriteString(line[i:]) break } b.WriteString(line[i : i+idx]) b.WriteString("\033[7m") b.WriteString(line[i+idx : i+idx+len(searchTerm)]) b.WriteString("\033[0m") i += idx + len(searchTerm) } return b.String() } func getTerminalHeight() (int, error) { ws, err := unix.IoctlGetWinsize(0, unix.TIOCGWINSZ) if err != nil { Loading @@ -34,6 +88,30 @@ func readKey() (byte, error) { return buf[0], nil } // searchForward finds the next line containing term (case-insensitive), // starting from the given line index. Returns -1 if not found. func searchForward(lines []string, term string, from int) int { lower := strings.ToLower(term) for i := max(from, 0); i < len(lines); i++ { if strings.Contains(strings.ToLower(lines[i]), lower) { return i } } return -1 } // searchBackward finds the previous line containing term (case-insensitive), // searching backward from the given line index. Returns -1 if not found. func searchBackward(lines []string, term string, from int) int { lower := strings.ToLower(term) for i := min(from, len(lines)-1); i >= 0; i-- { if strings.Contains(strings.ToLower(lines[i]), lower) { return i } } return -1 } // SanitizeText removes terminal escape sequences and dangerous control // characters from text, while preserving UTF-8 and normal whitespace. func SanitizeText(s string) string { Loading Loading @@ -76,6 +154,7 @@ func Pager(content string) bool { defer term.Restore(fd, oldState) // #nosec G115 start := 0 searchTerm := "" for { // State for where we are. if start < 0 { Loading @@ -85,7 +164,11 @@ func Pager(content string) bool { // Clear prompt and display the chunk. fmt.Print("\r \r") fmt.Print(strings.Join(lines[start:end], "\r\n")) visible := make([]string, end-start) for i, line := range lines[start:end] { visible[i] = highlightLine(line, searchTerm) } fmt.Print(strings.Join(visible, "\r\n")) if end == totalLines { fmt.Println("\r") // move to next line after paging ends Loading @@ -110,6 +193,29 @@ func Pager(content string) bool { case 'q', 'Q': // quit fmt.Println("\r") // move cursor to next line return false case '/': // search fmt.Print("\r \r") t := readLine("/") if t != "" { searchTerm = t if pos := searchForward(lines, searchTerm, start); pos >= 0 { start = pos } else { fmt.Print("\r\nNot found") } } case 'n': // next match if searchTerm != "" { if pos := searchForward(lines, searchTerm, start+1); pos >= 0 { start = pos } } case 'N': // previous match if searchTerm != "" { if pos := searchBackward(lines, searchTerm, start-1); pos >= 0 { start = pos } } } } } Loading
pager/pager.go +107 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,60 @@ import ( "golang.org/x/term" ) // readLine reads a line of input in raw mode, echoing characters. // Returns the entered string, or "" if ESC is pressed or input is empty. func readLine(prompt string) string { fmt.Print(prompt) var buf []byte for { key, err := readKey() if err != nil { return "" } switch key { case 0x1b: // ESC return "" case '\r', '\n': return string(buf) case 0x7f, 0x08: // backspace if len(buf) > 0 { buf = buf[:len(buf)-1] fmt.Print("\b \b") } default: if key >= 0x20 { buf = append(buf, key) fmt.Printf("%c", key) } } } } // highlightLine returns the line with all case-insensitive occurrences of term // wrapped in ANSI reverse video. func highlightLine(line, searchTerm string) string { if searchTerm == "" { return line } lower := strings.ToLower(line) lowerTerm := strings.ToLower(searchTerm) var b strings.Builder i := 0 for i < len(lower) { idx := strings.Index(lower[i:], lowerTerm) if idx < 0 { b.WriteString(line[i:]) break } b.WriteString(line[i : i+idx]) b.WriteString("\033[7m") b.WriteString(line[i+idx : i+idx+len(searchTerm)]) b.WriteString("\033[0m") i += idx + len(searchTerm) } return b.String() } func getTerminalHeight() (int, error) { ws, err := unix.IoctlGetWinsize(0, unix.TIOCGWINSZ) if err != nil { Loading @@ -34,6 +88,30 @@ func readKey() (byte, error) { return buf[0], nil } // searchForward finds the next line containing term (case-insensitive), // starting from the given line index. Returns -1 if not found. func searchForward(lines []string, term string, from int) int { lower := strings.ToLower(term) for i := max(from, 0); i < len(lines); i++ { if strings.Contains(strings.ToLower(lines[i]), lower) { return i } } return -1 } // searchBackward finds the previous line containing term (case-insensitive), // searching backward from the given line index. Returns -1 if not found. func searchBackward(lines []string, term string, from int) int { lower := strings.ToLower(term) for i := min(from, len(lines)-1); i >= 0; i-- { if strings.Contains(strings.ToLower(lines[i]), lower) { return i } } return -1 } // SanitizeText removes terminal escape sequences and dangerous control // characters from text, while preserving UTF-8 and normal whitespace. func SanitizeText(s string) string { Loading Loading @@ -76,6 +154,7 @@ func Pager(content string) bool { defer term.Restore(fd, oldState) // #nosec G115 start := 0 searchTerm := "" for { // State for where we are. if start < 0 { Loading @@ -85,7 +164,11 @@ func Pager(content string) bool { // Clear prompt and display the chunk. fmt.Print("\r \r") fmt.Print(strings.Join(lines[start:end], "\r\n")) visible := make([]string, end-start) for i, line := range lines[start:end] { visible[i] = highlightLine(line, searchTerm) } fmt.Print(strings.Join(visible, "\r\n")) if end == totalLines { fmt.Println("\r") // move to next line after paging ends Loading @@ -110,6 +193,29 @@ func Pager(content string) bool { case 'q', 'Q': // quit fmt.Println("\r") // move cursor to next line return false case '/': // search fmt.Print("\r \r") t := readLine("/") if t != "" { searchTerm = t if pos := searchForward(lines, searchTerm, start); pos >= 0 { start = pos } else { fmt.Print("\r\nNot found") } } case 'n': // next match if searchTerm != "" { if pos := searchForward(lines, searchTerm, start+1); pos >= 0 { start = pos } } case 'N': // previous match if searchTerm != "" { if pos := searchBackward(lines, searchTerm, start-1); pos >= 0 { start = pos } } } } }