pax_global_header00006660000000000000000000000064137556476530014537gustar00rootroot0000000000000052 comment=d842d4b7c2481b390a603949f936d1f3209f1fe8 liner-1.2.1/000077500000000000000000000000001375564765300126515ustar00rootroot00000000000000liner-1.2.1/.github/000077500000000000000000000000001375564765300142115ustar00rootroot00000000000000liner-1.2.1/.github/CONTRIBUTING.md000066400000000000000000000012521375564765300164420ustar00rootroot00000000000000#### Liner is a scratch-your-own-itch project While I try my best to fix any bugs encountered in liner, I do not have sufficient time to implement feature requests on your behalf. If you are opening a feature request, you are implicitly volunteering to implement that feature. Obvious feature requests should be made via a pull request. Complex feature requests will be interpreted as a request-for-comments, and will be closed once comments are given. #### Liner must remain backwards compatible The API of Liner must not change in an incompatible way. When making changes to liner, please use the [Go 1 Compatibility Promise](https://golang.org/doc/go1compat) as a guideline. liner-1.2.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000005711375564765300167210ustar00rootroot00000000000000If you have a feature request, please see the Contribution Guidelines before proceeding. If you have a bug report, please supply the following information: - Operating System (eg. Windows, Linux, Mac) - Terminal Emulator (eg. xterm, gnome-terminal, konsole, ConEmu, Terminal.app, Command Prompt) - Bug behaviour - Expected behaviour - Complete sample that reproduces the bug liner-1.2.1/COPYING000066400000000000000000000021001375564765300136750ustar00rootroot00000000000000Copyright © 2012 Peter Harris Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. liner-1.2.1/README.md000066400000000000000000000052501375564765300141320ustar00rootroot00000000000000Liner ===== Liner is a command line editor with history. It was inspired by linenoise; everything Unix-like is a VT100 (or is trying very hard to be). If your terminal is not pretending to be a VT100, change it. Liner also support Windows. Liner is released under the X11 license (which is similar to the new BSD license). Line Editing ------------ The following line editing commands are supported on platforms and terminals that Liner supports: Keystroke | Action --------- | ------ Ctrl-A, Home | Move cursor to beginning of line Ctrl-E, End | Move cursor to end of line Ctrl-B, Left | Move cursor one character left Ctrl-F, Right| Move cursor one character right Ctrl-Left, Alt-B | Move cursor to previous word Ctrl-Right, Alt-F | Move cursor to next word Ctrl-D, Del | (if line is *not* empty) Delete character under cursor Ctrl-D | (if line *is* empty) End of File - usually quits application Ctrl-C | Reset input (create new empty prompt) Ctrl-L | Clear screen (line is unmodified) Ctrl-T | Transpose previous character with current character Ctrl-H, BackSpace | Delete character before cursor Ctrl-W, Alt-BackSpace | Delete word leading up to cursor Alt-D | Delete word following cursor Ctrl-K | Delete from cursor to end of line Ctrl-U | Delete from start of line to cursor Ctrl-P, Up | Previous match from history Ctrl-N, Down | Next match from history Ctrl-R | Reverse Search history (Ctrl-S forward, Ctrl-G cancel) Ctrl-Y | Paste from Yank buffer (Alt-Y to paste next yank instead) Tab | Next completion Shift-Tab | (after Tab) Previous completion Getting started ----------------- ```go package main import ( "log" "os" "path/filepath" "strings" "github.com/peterh/liner" ) var ( history_fn = filepath.Join(os.TempDir(), ".liner_example_history") names = []string{"john", "james", "mary", "nancy"} ) func main() { line := liner.NewLiner() defer line.Close() line.SetCtrlCAborts(true) line.SetCompleter(func(line string) (c []string) { for _, n := range names { if strings.HasPrefix(n, strings.ToLower(line)) { c = append(c, n) } } return }) if f, err := os.Open(history_fn); err == nil { line.ReadHistory(f) f.Close() } if name, err := line.Prompt("What is your name? "); err == nil { log.Print("Got: ", name) line.AppendHistory(name) } else if err == liner.ErrPromptAborted { log.Print("Aborted") } else { log.Print("Error reading line: ", err) } if f, err := os.Create(history_fn); err != nil { log.Print("Error writing history file: ", err) } else { line.WriteHistory(f) f.Close() } } ``` For documentation, see http://godoc.org/github.com/peterh/liner liner-1.2.1/bsdinput.go000066400000000000000000000010041375564765300150230ustar00rootroot00000000000000// +build openbsd freebsd netbsd package liner import "syscall" const ( getTermios = syscall.TIOCGETA setTermios = syscall.TIOCSETA ) const ( // Input flags inpck = 0x010 istrip = 0x020 icrnl = 0x100 ixon = 0x200 // Output flags opost = 0x1 // Control flags cs8 = 0x300 // Local flags isig = 0x080 icanon = 0x100 iexten = 0x400 ) type termios struct { Iflag uint32 Oflag uint32 Cflag uint32 Lflag uint32 Cc [20]byte Ispeed int32 Ospeed int32 } const cursorColumn = false liner-1.2.1/common.go000066400000000000000000000177311375564765300145010ustar00rootroot00000000000000/* Package liner implements a simple command line editor, inspired by linenoise (https://github.com/antirez/linenoise/). This package supports WIN32 in addition to the xterm codes supported by everything else. */ package liner import ( "bufio" "container/ring" "errors" "fmt" "io" "strings" "sync" "unicode/utf8" ) type commonState struct { terminalSupported bool outputRedirected bool inputRedirected bool history []string historyMutex sync.RWMutex completer WordCompleter columns int killRing *ring.Ring ctrlCAborts bool r *bufio.Reader tabStyle TabStyle multiLineMode bool cursorRows int maxRows int shouldRestart ShouldRestart noBeep bool needRefresh bool } // TabStyle is used to select how tab completions are displayed. type TabStyle int // Two tab styles are currently available: // // TabCircular cycles through each completion item and displays it directly on // the prompt // // TabPrints prints the list of completion items to the screen after a second // tab key is pressed. This behaves similar to GNU readline and BASH (which // uses readline) const ( TabCircular TabStyle = iota TabPrints ) // ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C // if SetCtrlCAborts(true) has been called on the State var ErrPromptAborted = errors.New("prompt aborted") // ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the // platform is normally supported, but stdout has been redirected var ErrNotTerminalOutput = errors.New("standard output is not a terminal") // ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the // prompt contains any unprintable runes (including substrings that could // be colour codes on some platforms). var ErrInvalidPrompt = errors.New("invalid prompt") // ErrInternal is returned when liner experiences an error that it cannot // handle. For example, if the number of colums becomes zero during an // active call to Prompt var ErrInternal = errors.New("liner: internal error") // KillRingMax is the max number of elements to save on the killring. const KillRingMax = 60 // HistoryLimit is the maximum number of entries saved in the scrollback history. const HistoryLimit = 1000 // ReadHistory reads scrollback history from r. Returns the number of lines // read, and any read error (except io.EOF). func (s *State) ReadHistory(r io.Reader) (num int, err error) { s.historyMutex.Lock() defer s.historyMutex.Unlock() in := bufio.NewReader(r) num = 0 for { line, part, err := in.ReadLine() if err == io.EOF { break } if err != nil { return num, err } if part { return num, fmt.Errorf("line %d is too long", num+1) } if !utf8.Valid(line) { return num, fmt.Errorf("invalid string at line %d", num+1) } num++ s.history = append(s.history, string(line)) if len(s.history) > HistoryLimit { s.history = s.history[1:] } } return num, nil } // WriteHistory writes scrollback history to w. Returns the number of lines // successfully written, and any write error. // // Unlike the rest of liner's API, WriteHistory is safe to call // from another goroutine while Prompt is in progress. // This exception is to facilitate the saving of the history buffer // during an unexpected exit (for example, due to Ctrl-C being invoked) func (s *State) WriteHistory(w io.Writer) (num int, err error) { s.historyMutex.RLock() defer s.historyMutex.RUnlock() for _, item := range s.history { _, err := fmt.Fprintln(w, item) if err != nil { return num, err } num++ } return num, nil } // AppendHistory appends an entry to the scrollback history. AppendHistory // should be called iff Prompt returns a valid command. func (s *State) AppendHistory(item string) { s.historyMutex.Lock() defer s.historyMutex.Unlock() if len(s.history) > 0 { if item == s.history[len(s.history)-1] { return } } s.history = append(s.history, item) if len(s.history) > HistoryLimit { s.history = s.history[1:] } } // ClearHistory clears the scrollback history. func (s *State) ClearHistory() { s.historyMutex.Lock() defer s.historyMutex.Unlock() s.history = nil } // Returns the history lines starting with prefix func (s *State) getHistoryByPrefix(prefix string) (ph []string) { for _, h := range s.history { if strings.HasPrefix(h, prefix) { ph = append(ph, h) } } return } // Returns the history lines matching the intelligent search func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) { if pattern == "" { return } for _, h := range s.history { if i := strings.Index(h, pattern); i >= 0 { ph = append(ph, h) pos = append(pos, i) } } return } // Completer takes the currently edited line content at the left of the cursor // and returns a list of completion candidates. // If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed // to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!". type Completer func(line string) []string // WordCompleter takes the currently edited line with the cursor position and // returns the completion candidates for the partial word to be completed. // If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed // to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!". type WordCompleter func(line string, pos int) (head string, completions []string, tail string) // SetCompleter sets the completion function that Liner will call to // fetch completion candidates when the user presses tab. func (s *State) SetCompleter(f Completer) { if f == nil { s.completer = nil return } s.completer = func(line string, pos int) (string, []string, string) { return "", f(string([]rune(line)[:pos])), string([]rune(line)[pos:]) } } // SetWordCompleter sets the completion function that Liner will call to // fetch completion candidates when the user presses tab. func (s *State) SetWordCompleter(f WordCompleter) { s.completer = f } // SetTabCompletionStyle sets the behvavior when the Tab key is pressed // for auto-completion. TabCircular is the default behavior and cycles // through the list of candidates at the prompt. TabPrints will print // the available completion candidates to the screen similar to BASH // and GNU Readline func (s *State) SetTabCompletionStyle(tabStyle TabStyle) { s.tabStyle = tabStyle } // ModeApplier is the interface that wraps a representation of the terminal // mode. ApplyMode sets the terminal to this mode. type ModeApplier interface { ApplyMode() error } // SetCtrlCAborts sets whether Prompt on a supported terminal will return an // ErrPromptAborted when Ctrl-C is pressed. The default is false (will not // return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT // (and Prompt does not return) regardless of the value passed to SetCtrlCAborts. func (s *State) SetCtrlCAborts(aborts bool) { s.ctrlCAborts = aborts } // SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line). func (s *State) SetMultiLineMode(mlmode bool) { s.multiLineMode = mlmode } // ShouldRestart is passed the error generated by readNext and returns true if // the the read should be restarted or false if the error should be returned. type ShouldRestart func(err error) bool // SetShouldRestart sets the restart function that Liner will call to determine // whether to retry the call to, or return the error returned by, readNext. func (s *State) SetShouldRestart(f ShouldRestart) { s.shouldRestart = f } // SetBeep sets whether liner should beep the terminal at various times (output // ASCII BEL, 0x07). Default is true (will beep). func (s *State) SetBeep(beep bool) { s.noBeep = !beep } func (s *State) promptUnsupported(p string) (string, error) { if !s.inputRedirected || !s.terminalSupported { fmt.Print(p) } linebuf, _, err := s.r.ReadLine() if err != nil { return "", err } return string(linebuf), nil } liner-1.2.1/fallbackinput.go000066400000000000000000000023721375564765300160230ustar00rootroot00000000000000// +build !windows,!linux,!darwin,!openbsd,!freebsd,!netbsd package liner import ( "bufio" "errors" "os" ) // State represents an open terminal type State struct { commonState } // Prompt displays p, and then waits for user input. Prompt does not support // line editing on this operating system. func (s *State) Prompt(p string) (string, error) { return s.promptUnsupported(p) } // PasswordPrompt is not supported in this OS. func (s *State) PasswordPrompt(p string) (string, error) { return "", errors.New("liner: function not supported in this terminal") } // NewLiner initializes a new *State // // Note that this operating system uses a fallback mode without line // editing. Patches welcome. func NewLiner() *State { var s State s.r = bufio.NewReader(os.Stdin) return &s } // Close returns the terminal to its previous mode func (s *State) Close() error { return nil } // TerminalSupported returns false because line editing is not // supported on this platform. func TerminalSupported() bool { return false } type noopMode struct{} func (n noopMode) ApplyMode() error { return nil } // TerminalMode returns a noop InputModeSetter on this platform. func TerminalMode() (ModeApplier, error) { return noopMode{}, nil } const cursorColumn = true liner-1.2.1/go.mod000066400000000000000000000001151375564765300137540ustar00rootroot00000000000000module github.com/peterh/liner require github.com/mattn/go-runewidth v0.0.3 liner-1.2.1/go.sum000066400000000000000000000002611375564765300140030ustar00rootroot00000000000000github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= liner-1.2.1/input.go000066400000000000000000000175151375564765300143500ustar00rootroot00000000000000// +build linux darwin openbsd freebsd netbsd package liner import ( "bufio" "errors" "os" "os/signal" "strconv" "strings" "syscall" "time" ) type nexter struct { r rune err error } // State represents an open terminal type State struct { commonState origMode termios defaultMode termios next <-chan nexter winch chan os.Signal pending []rune useCHA bool } // NewLiner initializes a new *State, and sets the terminal into raw mode. To // restore the terminal to its previous state, call State.Close(). func NewLiner() *State { var s State s.r = bufio.NewReader(os.Stdin) s.terminalSupported = TerminalSupported() if m, err := TerminalMode(); err == nil { s.origMode = *m.(*termios) } else { s.inputRedirected = true } if _, err := getMode(syscall.Stdout); err != 0 { s.outputRedirected = true } if s.inputRedirected && s.outputRedirected { s.terminalSupported = false } if s.terminalSupported && !s.inputRedirected && !s.outputRedirected { mode := s.origMode mode.Iflag &^= icrnl | inpck | istrip | ixon mode.Cflag |= cs8 mode.Lflag &^= syscall.ECHO | icanon | iexten mode.ApplyMode() winch := make(chan os.Signal, 1) signal.Notify(winch, syscall.SIGWINCH) s.winch = winch s.checkOutput() } if !s.outputRedirected { s.outputRedirected = !s.getColumns() } return &s } var errTimedOut = errors.New("timeout") func (s *State) startPrompt() { if s.terminalSupported { if m, err := TerminalMode(); err == nil { s.defaultMode = *m.(*termios) mode := s.defaultMode mode.Lflag &^= isig mode.ApplyMode() } } s.restartPrompt() } func (s *State) inputWaiting() bool { return len(s.next) > 0 } func (s *State) restartPrompt() { next := make(chan nexter, 200) go func() { for { var n nexter n.r, _, n.err = s.r.ReadRune() next <- n // Shut down nexter loop when an end condition has been reached if n.err != nil || n.r == '\n' || n.r == '\r' || n.r == ctrlC || n.r == ctrlD { close(next) return } } }() s.next = next } func (s *State) stopPrompt() { if s.terminalSupported { s.defaultMode.ApplyMode() } } func (s *State) nextPending(timeout <-chan time.Time) (rune, error) { select { case thing, ok := <-s.next: if !ok { return 0, ErrInternal } if thing.err != nil { return 0, thing.err } s.pending = append(s.pending, thing.r) return thing.r, nil case <-timeout: rv := s.pending[0] s.pending = s.pending[1:] return rv, errTimedOut } } func (s *State) readNext() (interface{}, error) { if len(s.pending) > 0 { rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } var r rune select { case thing, ok := <-s.next: if !ok { return 0, ErrInternal } if thing.err != nil { return nil, thing.err } r = thing.r case <-s.winch: s.getColumns() return winch, nil } if r != esc { return r, nil } s.pending = append(s.pending, r) // Wait at most 50 ms for the rest of the escape sequence // If nothing else arrives, it was an actual press of the esc key timeout := time.After(50 * time.Millisecond) flag, err := s.nextPending(timeout) if err != nil { if err == errTimedOut { return flag, nil } return unknown, err } switch flag { case '[': code, err := s.nextPending(timeout) if err != nil { if err == errTimedOut { return code, nil } return unknown, err } switch code { case 'A': s.pending = s.pending[:0] // escape code complete return up, nil case 'B': s.pending = s.pending[:0] // escape code complete return down, nil case 'C': s.pending = s.pending[:0] // escape code complete return right, nil case 'D': s.pending = s.pending[:0] // escape code complete return left, nil case 'F': s.pending = s.pending[:0] // escape code complete return end, nil case 'H': s.pending = s.pending[:0] // escape code complete return home, nil case 'Z': s.pending = s.pending[:0] // escape code complete return shiftTab, nil case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': num := []rune{code} for { code, err := s.nextPending(timeout) if err != nil { if err == errTimedOut { return code, nil } return nil, err } switch code { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': num = append(num, code) case ';': // Modifier code to follow // This only supports Ctrl-left and Ctrl-right for now x, _ := strconv.ParseInt(string(num), 10, 32) if x != 1 { // Can't be left or right rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } num = num[:0] for { code, err = s.nextPending(timeout) if err != nil { if err == errTimedOut { rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } return nil, err } switch code { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': num = append(num, code) case 'C', 'D': // right, left mod, _ := strconv.ParseInt(string(num), 10, 32) if mod != 5 { // Not bare Ctrl rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } s.pending = s.pending[:0] // escape code complete if code == 'C' { return wordRight, nil } return wordLeft, nil default: // Not left or right rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } } case '~': s.pending = s.pending[:0] // escape code complete x, _ := strconv.ParseInt(string(num), 10, 32) switch x { case 2: return insert, nil case 3: return del, nil case 5: return pageUp, nil case 6: return pageDown, nil case 1, 7: return home, nil case 4, 8: return end, nil case 15: return f5, nil case 17: return f6, nil case 18: return f7, nil case 19: return f8, nil case 20: return f9, nil case 21: return f10, nil case 23: return f11, nil case 24: return f12, nil default: return unknown, nil } default: // unrecognized escape code rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } } } case 'O': code, err := s.nextPending(timeout) if err != nil { if err == errTimedOut { return code, nil } return nil, err } s.pending = s.pending[:0] // escape code complete switch code { case 'c': return wordRight, nil case 'd': return wordLeft, nil case 'H': return home, nil case 'F': return end, nil case 'P': return f1, nil case 'Q': return f2, nil case 'R': return f3, nil case 'S': return f4, nil default: return unknown, nil } case 'b': s.pending = s.pending[:0] // escape code complete return altB, nil case 'd': s.pending = s.pending[:0] // escape code complete return altD, nil case bs: s.pending = s.pending[:0] // escape code complete return altBs, nil case 'f': s.pending = s.pending[:0] // escape code complete return altF, nil case 'y': s.pending = s.pending[:0] // escape code complete return altY, nil default: rv := s.pending[0] s.pending = s.pending[1:] return rv, nil } // not reached return r, nil } // Close returns the terminal to its previous mode func (s *State) Close() error { signal.Stop(s.winch) if !s.inputRedirected { s.origMode.ApplyMode() } return nil } // TerminalSupported returns true if the current terminal supports // line editing features, and false if liner will use the 'dumb' // fallback for input. // Note that TerminalSupported does not check all factors that may // cause liner to not fully support the terminal (such as stdin redirection) func TerminalSupported() bool { bad := map[string]bool{"": true, "dumb": true, "cons25": true} return !bad[strings.ToLower(os.Getenv("TERM"))] } liner-1.2.1/input_darwin.go000066400000000000000000000011401375564765300156770ustar00rootroot00000000000000// +build darwin package liner import "syscall" const ( getTermios = syscall.TIOCGETA setTermios = syscall.TIOCSETA ) const ( // Input flags inpck = 0x010 istrip = 0x020 icrnl = 0x100 ixon = 0x200 // Output flags opost = 0x1 // Control flags cs8 = 0x300 // Local flags isig = 0x080 icanon = 0x100 iexten = 0x400 ) type termios struct { Iflag uintptr Oflag uintptr Cflag uintptr Lflag uintptr Cc [20]byte Ispeed uintptr Ospeed uintptr } // Terminal.app needs a column for the cursor when the input line is at the // bottom of the window. const cursorColumn = true liner-1.2.1/input_linux.go000066400000000000000000000006361375564765300155630ustar00rootroot00000000000000// +build linux package liner import "syscall" const ( getTermios = syscall.TCGETS setTermios = syscall.TCSETS ) const ( icrnl = syscall.ICRNL inpck = syscall.INPCK istrip = syscall.ISTRIP ixon = syscall.IXON opost = syscall.OPOST cs8 = syscall.CS8 isig = syscall.ISIG icanon = syscall.ICANON iexten = syscall.IEXTEN ) type termios struct { syscall.Termios } const cursorColumn = false liner-1.2.1/input_test.go000066400000000000000000000022651375564765300154030ustar00rootroot00000000000000// +build !windows package liner import ( "bufio" "bytes" "testing" ) func (s *State) expectRune(t *testing.T, r rune) { item, err := s.readNext() if err != nil { t.Fatalf("Expected rune '%c', got error %s\n", r, err) } if v, ok := item.(rune); !ok { t.Fatalf("Expected rune '%c', got non-rune %v\n", r, v) } else { if v != r { t.Fatalf("Expected rune '%c', got rune '%c'\n", r, v) } } } func (s *State) expectAction(t *testing.T, a action) { item, err := s.readNext() if err != nil { t.Fatalf("Expected Action %d, got error %s\n", a, err) } if v, ok := item.(action); !ok { t.Fatalf("Expected Action %d, got non-Action %v\n", a, v) } else { if v != a { t.Fatalf("Expected Action %d, got Action %d\n", a, v) } } } func TestTypes(t *testing.T) { input := []byte{'A', 27, 'B', 27, 91, 68, 27, '[', '1', ';', '5', 'D', 'e'} var s State s.r = bufio.NewReader(bytes.NewBuffer(input)) next := make(chan nexter) go func() { for { var n nexter n.r, _, n.err = s.r.ReadRune() next <- n } }() s.next = next s.expectRune(t, 'A') s.expectRune(t, 27) s.expectRune(t, 'B') s.expectAction(t, left) s.expectAction(t, wordLeft) s.expectRune(t, 'e') } liner-1.2.1/input_windows.go000066400000000000000000000216331375564765300161160ustar00rootroot00000000000000package liner import ( "bufio" "os" "syscall" "unicode/utf16" "unsafe" ) var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") procGetStdHandle = kernel32.NewProc("GetStdHandle") procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW") procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents") procGetConsoleMode = kernel32.NewProc("GetConsoleMode") procSetConsoleMode = kernel32.NewProc("SetConsoleMode") procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") ) // These names are from the Win32 api, so they use underscores (contrary to // what golint suggests) const ( std_input_handle = uint32(-10 & 0xFFFFFFFF) std_output_handle = uint32(-11 & 0xFFFFFFFF) std_error_handle = uint32(-12 & 0xFFFFFFFF) invalid_handle_value = ^uintptr(0) ) type inputMode uint32 // State represents an open terminal type State struct { commonState handle syscall.Handle hOut syscall.Handle origMode inputMode defaultMode inputMode key interface{} repeat uint16 } const ( enableEchoInput = 0x4 enableInsertMode = 0x20 enableLineInput = 0x2 enableMouseInput = 0x10 enableProcessedInput = 0x1 enableQuickEditMode = 0x40 enableWindowInput = 0x8 ) // NewLiner initializes a new *State, and sets the terminal into raw mode. To // restore the terminal to its previous state, call State.Close(). func NewLiner() *State { var s State hIn, _, _ := procGetStdHandle.Call(uintptr(std_input_handle)) s.handle = syscall.Handle(hIn) hOut, _, _ := procGetStdHandle.Call(uintptr(std_output_handle)) s.hOut = syscall.Handle(hOut) s.terminalSupported = true if m, err := TerminalMode(); err == nil { s.origMode = m.(inputMode) mode := s.origMode mode &^= enableEchoInput mode &^= enableInsertMode mode &^= enableLineInput mode &^= enableMouseInput mode |= enableWindowInput mode.ApplyMode() } else { s.inputRedirected = true s.r = bufio.NewReader(os.Stdin) } s.getColumns() s.outputRedirected = s.columns <= 0 return &s } // These names are from the Win32 api, so they use underscores (contrary to // what golint suggests) const ( focus_event = 0x0010 key_event = 0x0001 menu_event = 0x0008 mouse_event = 0x0002 window_buffer_size_event = 0x0004 ) type input_record struct { eventType uint16 pad uint16 blob [16]byte } type key_event_record struct { KeyDown int32 RepeatCount uint16 VirtualKeyCode uint16 VirtualScanCode uint16 Char uint16 ControlKeyState uint32 } // These names are from the Win32 api, so they use underscores (contrary to // what golint suggests) const ( vk_back = 0x08 vk_tab = 0x09 vk_menu = 0x12 // ALT key vk_prior = 0x21 vk_next = 0x22 vk_end = 0x23 vk_home = 0x24 vk_left = 0x25 vk_up = 0x26 vk_right = 0x27 vk_down = 0x28 vk_insert = 0x2d vk_delete = 0x2e vk_f1 = 0x70 vk_f2 = 0x71 vk_f3 = 0x72 vk_f4 = 0x73 vk_f5 = 0x74 vk_f6 = 0x75 vk_f7 = 0x76 vk_f8 = 0x77 vk_f9 = 0x78 vk_f10 = 0x79 vk_f11 = 0x7a vk_f12 = 0x7b bKey = 0x42 dKey = 0x44 fKey = 0x46 yKey = 0x59 ) const ( shiftPressed = 0x0010 leftAltPressed = 0x0002 leftCtrlPressed = 0x0008 rightAltPressed = 0x0001 rightCtrlPressed = 0x0004 modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed ) // inputWaiting only returns true if the next call to readNext will return immediately. func (s *State) inputWaiting() bool { var num uint32 ok, _, _ := procGetNumberOfConsoleInputEvents.Call(uintptr(s.handle), uintptr(unsafe.Pointer(&num))) if ok == 0 { // call failed, so we cannot guarantee a non-blocking readNext return false } // during a "paste" input events are always an odd number, and // the last one results in a blocking readNext, so return false // when num is 1 or 0. return num > 1 } func (s *State) readNext() (interface{}, error) { if s.repeat > 0 { s.repeat-- return s.key, nil } var input input_record pbuf := uintptr(unsafe.Pointer(&input)) var rv uint32 prv := uintptr(unsafe.Pointer(&rv)) var surrogate uint16 for { ok, _, err := procReadConsoleInput.Call(uintptr(s.handle), pbuf, 1, prv) if ok == 0 { return nil, err } if input.eventType == window_buffer_size_event { xy := (*coord)(unsafe.Pointer(&input.blob[0])) s.columns = int(xy.x) return winch, nil } if input.eventType != key_event { continue } ke := (*key_event_record)(unsafe.Pointer(&input.blob[0])) if ke.KeyDown == 0 { if ke.VirtualKeyCode == vk_menu && ke.Char > 0 { // paste of unicode (eg. via ALT-numpad) if surrogate > 0 { return utf16.DecodeRune(rune(surrogate), rune(ke.Char)), nil } else if utf16.IsSurrogate(rune(ke.Char)) { surrogate = ke.Char continue } else { return rune(ke.Char), nil } } continue } if ke.VirtualKeyCode == vk_tab && ke.ControlKeyState&modKeys == shiftPressed { s.key = shiftTab } else if ke.VirtualKeyCode == vk_back && (ke.ControlKeyState&modKeys == leftAltPressed || ke.ControlKeyState&modKeys == rightAltPressed) { s.key = altBs } else if ke.VirtualKeyCode == bKey && (ke.ControlKeyState&modKeys == leftAltPressed || ke.ControlKeyState&modKeys == rightAltPressed) { s.key = altB } else if ke.VirtualKeyCode == dKey && (ke.ControlKeyState&modKeys == leftAltPressed || ke.ControlKeyState&modKeys == rightAltPressed) { s.key = altD } else if ke.VirtualKeyCode == fKey && (ke.ControlKeyState&modKeys == leftAltPressed || ke.ControlKeyState&modKeys == rightAltPressed) { s.key = altF } else if ke.VirtualKeyCode == yKey && (ke.ControlKeyState&modKeys == leftAltPressed || ke.ControlKeyState&modKeys == rightAltPressed) { s.key = altY } else if ke.Char > 0 { if surrogate > 0 { s.key = utf16.DecodeRune(rune(surrogate), rune(ke.Char)) } else if utf16.IsSurrogate(rune(ke.Char)) { surrogate = ke.Char continue } else { s.key = rune(ke.Char) } } else { switch ke.VirtualKeyCode { case vk_prior: s.key = pageUp case vk_next: s.key = pageDown case vk_end: s.key = end case vk_home: s.key = home case vk_left: s.key = left if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 { if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) { s.key = wordLeft } } case vk_right: s.key = right if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 { if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) { s.key = wordRight } } case vk_up: s.key = up case vk_down: s.key = down case vk_insert: s.key = insert case vk_delete: s.key = del case vk_f1: s.key = f1 case vk_f2: s.key = f2 case vk_f3: s.key = f3 case vk_f4: s.key = f4 case vk_f5: s.key = f5 case vk_f6: s.key = f6 case vk_f7: s.key = f7 case vk_f8: s.key = f8 case vk_f9: s.key = f9 case vk_f10: s.key = f10 case vk_f11: s.key = f11 case vk_f12: s.key = f12 default: // Eat modifier keys // TODO: return Action(Unknown) if the key isn't a // modifier. continue } } if ke.RepeatCount > 1 { s.repeat = ke.RepeatCount - 1 } return s.key, nil } } // Close returns the terminal to its previous mode func (s *State) Close() error { s.origMode.ApplyMode() return nil } func (s *State) startPrompt() { if m, err := TerminalMode(); err == nil { s.defaultMode = m.(inputMode) mode := s.defaultMode mode &^= enableProcessedInput mode.ApplyMode() } } func (s *State) restartPrompt() { } func (s *State) stopPrompt() { s.defaultMode.ApplyMode() } // TerminalSupported returns true because line editing is always // supported on Windows. func TerminalSupported() bool { return true } func (mode inputMode) ApplyMode() error { hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle)) if hIn == invalid_handle_value || hIn == 0 { return err } ok, _, err := procSetConsoleMode.Call(hIn, uintptr(mode)) if ok != 0 { err = nil } return err } // TerminalMode returns the current terminal input mode as an InputModeSetter. // // This function is provided for convenience, and should // not be necessary for most users of liner. func TerminalMode() (ModeApplier, error) { var mode inputMode hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle)) if hIn == invalid_handle_value || hIn == 0 { return nil, err } ok, _, err := procGetConsoleMode.Call(hIn, uintptr(unsafe.Pointer(&mode))) if ok != 0 { err = nil } return mode, err } const cursorColumn = true liner-1.2.1/line.go000066400000000000000000000652761375564765300141470ustar00rootroot00000000000000// +build windows linux darwin openbsd freebsd netbsd package liner import ( "bufio" "container/ring" "errors" "fmt" "io" "os" "strings" "unicode" "unicode/utf8" ) type action int const ( left action = iota right up down home end insert del pageUp pageDown f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 altB altBs // Alt+Backspace altD altF altY shiftTab wordLeft wordRight winch unknown ) const ( ctrlA = 1 ctrlB = 2 ctrlC = 3 ctrlD = 4 ctrlE = 5 ctrlF = 6 ctrlG = 7 ctrlH = 8 tab = 9 lf = 10 ctrlK = 11 ctrlL = 12 cr = 13 ctrlN = 14 ctrlO = 15 ctrlP = 16 ctrlQ = 17 ctrlR = 18 ctrlS = 19 ctrlT = 20 ctrlU = 21 ctrlV = 22 ctrlW = 23 ctrlX = 24 ctrlY = 25 ctrlZ = 26 esc = 27 bs = 127 ) const ( beep = "\a" ) type tabDirection int const ( tabForward tabDirection = iota tabReverse ) func (s *State) refresh(prompt []rune, buf []rune, pos int) error { if s.columns == 0 { return ErrInternal } s.needRefresh = false if s.multiLineMode { return s.refreshMultiLine(prompt, buf, pos) } return s.refreshSingleLine(prompt, buf, pos) } func (s *State) refreshSingleLine(prompt []rune, buf []rune, pos int) error { s.cursorPos(0) _, err := fmt.Print(string(prompt)) if err != nil { return err } pLen := countGlyphs(prompt) bLen := countGlyphs(buf) // on some OS / terminals extra column is needed to place the cursor char if cursorColumn { bLen++ } pos = countGlyphs(buf[:pos]) if pLen+bLen < s.columns { _, err = fmt.Print(string(buf)) s.eraseLine() s.cursorPos(pLen + pos) } else { // Find space available space := s.columns - pLen space-- // space for cursor start := pos - space/2 end := start + space if end > bLen { end = bLen start = end - space } if start < 0 { start = 0 end = space } pos -= start // Leave space for markers if start > 0 { start++ } if end < bLen { end-- } startRune := len(getPrefixGlyphs(buf, start)) line := getPrefixGlyphs(buf[startRune:], end-start) // Output if start > 0 { fmt.Print("{") } fmt.Print(string(line)) if end < bLen { fmt.Print("}") } // Set cursor position s.eraseLine() s.cursorPos(pLen + pos) } return err } func (s *State) refreshMultiLine(prompt []rune, buf []rune, pos int) error { promptColumns := countMultiLineGlyphs(prompt, s.columns, 0) totalColumns := countMultiLineGlyphs(buf, s.columns, promptColumns) // on some OS / terminals extra column is needed to place the cursor char // if cursorColumn { // totalColumns++ // } // it looks like Multiline mode always assume that a cursor need an extra column, // and always emit a newline if we are at the screen end, so no worarounds needed there totalRows := (totalColumns + s.columns - 1) / s.columns maxRows := s.maxRows if totalRows > s.maxRows { s.maxRows = totalRows } cursorRows := s.cursorRows if cursorRows == 0 { cursorRows = 1 } /* First step: clear all the lines used before. To do so start by * going to the last row. */ if maxRows-cursorRows > 0 { s.moveDown(maxRows - cursorRows) } /* Now for every row clear it, go up. */ for i := 0; i < maxRows-1; i++ { s.cursorPos(0) s.eraseLine() s.moveUp(1) } /* Clean the top line. */ s.cursorPos(0) s.eraseLine() /* Write the prompt and the current buffer content */ if _, err := fmt.Print(string(prompt)); err != nil { return err } if _, err := fmt.Print(string(buf)); err != nil { return err } /* If we are at the very end of the screen with our prompt, we need to * emit a newline and move the prompt to the first column. */ cursorColumns := countMultiLineGlyphs(buf[:pos], s.columns, promptColumns) if cursorColumns == totalColumns && totalColumns%s.columns == 0 { s.emitNewLine() s.cursorPos(0) totalRows++ if totalRows > s.maxRows { s.maxRows = totalRows } } /* Move cursor to right position. */ cursorRows = (cursorColumns + s.columns) / s.columns if s.cursorRows > 0 && totalRows-cursorRows > 0 { s.moveUp(totalRows - cursorRows) } /* Set column. */ s.cursorPos(cursorColumns % s.columns) s.cursorRows = cursorRows return nil } func (s *State) resetMultiLine(prompt []rune, buf []rune, pos int) { columns := countMultiLineGlyphs(prompt, s.columns, 0) columns = countMultiLineGlyphs(buf[:pos], s.columns, columns) columns += 2 // ^C cursorRows := (columns + s.columns) / s.columns if s.maxRows-cursorRows > 0 { for i := 0; i < s.maxRows-cursorRows; i++ { fmt.Println() // always moves the cursor down or scrolls the window up as needed } } s.maxRows = 1 s.cursorRows = 0 } func longestCommonPrefix(strs []string) string { if len(strs) == 0 { return "" } longest := strs[0] for _, str := range strs[1:] { for !strings.HasPrefix(str, longest) { longest = longest[:len(longest)-1] } } // Remove trailing partial runes longest = strings.TrimRight(longest, "\uFFFD") return longest } func (s *State) circularTabs(items []string) func(tabDirection) (string, error) { item := -1 return func(direction tabDirection) (string, error) { if direction == tabForward { if item < len(items)-1 { item++ } else { item = 0 } } else if direction == tabReverse { if item > 0 { item-- } else { item = len(items) - 1 } } return items[item], nil } } func calculateColumns(screenWidth int, items []string) (numColumns, numRows, maxWidth int) { for _, item := range items { if len(item) >= screenWidth { return 1, len(items), screenWidth - 1 } if len(item) >= maxWidth { maxWidth = len(item) + 1 } } numColumns = screenWidth / maxWidth numRows = len(items) / numColumns if len(items)%numColumns > 0 { numRows++ } if len(items) <= numColumns { maxWidth = 0 } return } func (s *State) printedTabs(items []string) func(tabDirection) (string, error) { numTabs := 1 prefix := longestCommonPrefix(items) return func(direction tabDirection) (string, error) { if len(items) == 1 { return items[0], nil } if numTabs == 2 { if len(items) > 100 { fmt.Printf("\nDisplay all %d possibilities? (y or n) ", len(items)) prompt: for { next, err := s.readNext() if err != nil { return prefix, err } if key, ok := next.(rune); ok { switch key { case 'n', 'N': return prefix, nil case 'y', 'Y': break prompt case ctrlC, ctrlD, cr, lf: s.restartPrompt() } } } } fmt.Println("") numColumns, numRows, maxWidth := calculateColumns(s.columns, items) for i := 0; i < numRows; i++ { for j := 0; j < numColumns*numRows; j += numRows { if i+j < len(items) { if maxWidth > 0 { fmt.Printf("%-*.[1]*s", maxWidth, items[i+j]) } else { fmt.Printf("%v ", items[i+j]) } } } fmt.Println("") } } else { numTabs++ } return prefix, nil } } func (s *State) tabComplete(p []rune, line []rune, pos int) ([]rune, int, interface{}, error) { if s.completer == nil { return line, pos, rune(esc), nil } head, list, tail := s.completer(string(line), pos) if len(list) <= 0 { return line, pos, rune(esc), nil } hl := utf8.RuneCountInString(head) if len(list) == 1 { err := s.refresh(p, []rune(head+list[0]+tail), hl+utf8.RuneCountInString(list[0])) return []rune(head + list[0] + tail), hl + utf8.RuneCountInString(list[0]), rune(esc), err } direction := tabForward tabPrinter := s.circularTabs(list) if s.tabStyle == TabPrints { tabPrinter = s.printedTabs(list) } for { pick, err := tabPrinter(direction) if err != nil { return line, pos, rune(esc), err } err = s.refresh(p, []rune(head+pick+tail), hl+utf8.RuneCountInString(pick)) if err != nil { return line, pos, rune(esc), err } next, err := s.readNext() if err != nil { return line, pos, rune(esc), err } if key, ok := next.(rune); ok { if key == tab { direction = tabForward continue } if key == esc { return line, pos, rune(esc), nil } } if a, ok := next.(action); ok && a == shiftTab { direction = tabReverse continue } return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil } } // reverse intelligent search, implements a bash-like history search. func (s *State) reverseISearch(origLine []rune, origPos int) ([]rune, int, interface{}, error) { p := "(reverse-i-search)`': " err := s.refresh([]rune(p), origLine, origPos) if err != nil { return origLine, origPos, rune(esc), err } line := []rune{} pos := 0 foundLine := string(origLine) foundPos := origPos getLine := func() ([]rune, []rune, int) { search := string(line) prompt := "(reverse-i-search)`%s': " return []rune(fmt.Sprintf(prompt, search)), []rune(foundLine), foundPos } history, positions := s.getHistoryByPattern(string(line)) historyPos := len(history) - 1 for { next, err := s.readNext() if err != nil { return []rune(foundLine), foundPos, rune(esc), err } switch v := next.(type) { case rune: switch v { case ctrlR: // Search backwards if historyPos > 0 && historyPos < len(history) { historyPos-- foundLine = history[historyPos] foundPos = positions[historyPos] } else { s.doBeep() } case ctrlS: // Search forward if historyPos < len(history)-1 && historyPos >= 0 { historyPos++ foundLine = history[historyPos] foundPos = positions[historyPos] } else { s.doBeep() } case ctrlH, bs: // Backspace if pos <= 0 { s.doBeep() } else { n := len(getSuffixGlyphs(line[:pos], 1)) line = append(line[:pos-n], line[pos:]...) pos -= n // For each char deleted, display the last matching line of history history, positions := s.getHistoryByPattern(string(line)) historyPos = len(history) - 1 if len(history) > 0 { foundLine = history[historyPos] foundPos = positions[historyPos] } else { foundLine = "" foundPos = 0 } } case ctrlG: // Cancel return origLine, origPos, rune(esc), err case tab, cr, lf, ctrlA, ctrlB, ctrlD, ctrlE, ctrlF, ctrlK, ctrlL, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: fallthrough case 0, ctrlC, esc, 28, 29, 30, 31: return []rune(foundLine), foundPos, next, err default: line = append(line[:pos], append([]rune{v}, line[pos:]...)...) pos++ // For each keystroke typed, display the last matching line of history history, positions = s.getHistoryByPattern(string(line)) historyPos = len(history) - 1 if len(history) > 0 { foundLine = history[historyPos] foundPos = positions[historyPos] } else { foundLine = "" foundPos = 0 } } case action: return []rune(foundLine), foundPos, next, err } err = s.refresh(getLine()) if err != nil { return []rune(foundLine), foundPos, rune(esc), err } } } // addToKillRing adds some text to the kill ring. If mode is 0 it adds it to a // new node in the end of the kill ring, and move the current pointer to the new // node. If mode is 1 or 2 it appends or prepends the text to the current entry // of the killRing. func (s *State) addToKillRing(text []rune, mode int) { // Don't use the same underlying array as text killLine := make([]rune, len(text)) copy(killLine, text) // Point killRing to a newNode, procedure depends on the killring state and // append mode. if mode == 0 { // Add new node to killRing if s.killRing == nil { // if killring is empty, create a new one s.killRing = ring.New(1) } else if s.killRing.Len() >= KillRingMax { // if killring is "full" s.killRing = s.killRing.Next() } else { // Normal case s.killRing.Link(ring.New(1)) s.killRing = s.killRing.Next() } } else { if s.killRing == nil { // if killring is empty, create a new one s.killRing = ring.New(1) s.killRing.Value = []rune{} } if mode == 1 { // Append to last entry killLine = append(s.killRing.Value.([]rune), killLine...) } else if mode == 2 { // Prepend to last entry killLine = append(killLine, s.killRing.Value.([]rune)...) } } // Save text in the current killring node s.killRing.Value = killLine } func (s *State) yank(p []rune, text []rune, pos int) ([]rune, int, interface{}, error) { if s.killRing == nil { return text, pos, rune(esc), nil } lineStart := text[:pos] lineEnd := text[pos:] var line []rune for { value := s.killRing.Value.([]rune) line = make([]rune, 0) line = append(line, lineStart...) line = append(line, value...) line = append(line, lineEnd...) pos = len(lineStart) + len(value) err := s.refresh(p, line, pos) if err != nil { return line, pos, 0, err } next, err := s.readNext() if err != nil { return line, pos, next, err } switch v := next.(type) { case rune: return line, pos, next, nil case action: switch v { case altY: s.killRing = s.killRing.Prev() default: return line, pos, next, nil } } } } // Prompt displays p and returns a line of user input, not including a trailing // newline character. An io.EOF error is returned if the user signals end-of-file // by pressing Ctrl-D. Prompt allows line editing if the terminal supports it. func (s *State) Prompt(prompt string) (string, error) { return s.PromptWithSuggestion(prompt, "", 0) } // PromptWithSuggestion displays prompt and an editable text with cursor at // given position. The cursor will be set to the end of the line if given position // is negative or greater than length of text (in runes). Returns a line of user input, not // including a trailing newline character. An io.EOF error is returned if the user // signals end-of-file by pressing Ctrl-D. func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (string, error) { for _, r := range prompt { if unicode.Is(unicode.C, r) { return "", ErrInvalidPrompt } } if s.inputRedirected || !s.terminalSupported { return s.promptUnsupported(prompt) } p := []rune(prompt) const minWorkingSpace = 10 if s.columns < countGlyphs(p)+minWorkingSpace { return s.tooNarrow(prompt) } if s.outputRedirected { return "", ErrNotTerminalOutput } s.historyMutex.RLock() defer s.historyMutex.RUnlock() fmt.Print(prompt) var line = []rune(text) historyEnd := "" var historyPrefix []string historyPos := 0 historyStale := true historyAction := false // used to mark history related actions killAction := 0 // used to mark kill related actions defer s.stopPrompt() if pos < 0 || len(line) < pos { pos = len(line) } if len(line) > 0 { err := s.refresh(p, line, pos) if err != nil { return "", err } } restart: s.startPrompt() s.getColumns() mainLoop: for { next, err := s.readNext() haveNext: if err != nil { if s.shouldRestart != nil && s.shouldRestart(err) { goto restart } return "", err } historyAction = false switch v := next.(type) { case rune: switch v { case cr, lf: if s.needRefresh { err := s.refresh(p, line, pos) if err != nil { return "", err } } if s.multiLineMode { s.resetMultiLine(p, line, pos) } fmt.Println() break mainLoop case ctrlA: // Start of line pos = 0 s.needRefresh = true case ctrlE: // End of line pos = len(line) s.needRefresh = true case ctrlB: // left if pos > 0 { pos -= len(getSuffixGlyphs(line[:pos], 1)) s.needRefresh = true } else { s.doBeep() } case ctrlF: // right if pos < len(line) { pos += len(getPrefixGlyphs(line[pos:], 1)) s.needRefresh = true } else { s.doBeep() } case ctrlD: // del if pos == 0 && len(line) == 0 { // exit return "", io.EOF } // ctrlD is a potential EOF, so the rune reader shuts down. // Therefore, if it isn't actually an EOF, we must re-startPrompt. s.restartPrompt() if pos >= len(line) { s.doBeep() } else { n := len(getPrefixGlyphs(line[pos:], 1)) line = append(line[:pos], line[pos+n:]...) s.needRefresh = true } case ctrlK: // delete remainder of line if pos >= len(line) { s.doBeep() } else { if killAction > 0 { s.addToKillRing(line[pos:], 1) // Add in apend mode } else { s.addToKillRing(line[pos:], 0) // Add in normal mode } killAction = 2 // Mark that there was a kill action line = line[:pos] s.needRefresh = true } case ctrlP: // up historyAction = true if historyStale { historyPrefix = s.getHistoryByPrefix(string(line)) historyPos = len(historyPrefix) historyStale = false } if historyPos > 0 { if historyPos == len(historyPrefix) { historyEnd = string(line) } historyPos-- line = []rune(historyPrefix[historyPos]) pos = len(line) s.needRefresh = true } else { s.doBeep() } case ctrlN: // down historyAction = true if historyStale { historyPrefix = s.getHistoryByPrefix(string(line)) historyPos = len(historyPrefix) historyStale = false } if historyPos < len(historyPrefix) { historyPos++ if historyPos == len(historyPrefix) { line = []rune(historyEnd) } else { line = []rune(historyPrefix[historyPos]) } pos = len(line) s.needRefresh = true } else { s.doBeep() } case ctrlT: // transpose prev glyph with glyph under cursor if len(line) < 2 || pos < 1 { s.doBeep() } else { if pos == len(line) { pos -= len(getSuffixGlyphs(line, 1)) } prev := getSuffixGlyphs(line[:pos], 1) next := getPrefixGlyphs(line[pos:], 1) scratch := make([]rune, len(prev)) copy(scratch, prev) copy(line[pos-len(prev):], next) copy(line[pos-len(prev)+len(next):], scratch) pos += len(next) s.needRefresh = true } case ctrlL: // clear screen s.eraseScreen() s.needRefresh = true case ctrlC: // reset fmt.Println("^C") if s.multiLineMode { s.resetMultiLine(p, line, pos) } if s.ctrlCAborts { return "", ErrPromptAborted } line = line[:0] pos = 0 fmt.Print(prompt) s.restartPrompt() case ctrlH, bs: // Backspace if pos <= 0 { s.doBeep() } else { n := len(getSuffixGlyphs(line[:pos], 1)) line = append(line[:pos-n], line[pos:]...) pos -= n s.needRefresh = true } case ctrlU: // Erase line before cursor if killAction > 0 { s.addToKillRing(line[:pos], 2) // Add in prepend mode } else { s.addToKillRing(line[:pos], 0) // Add in normal mode } killAction = 2 // Mark that there was some killing line = line[pos:] pos = 0 s.needRefresh = true case ctrlW: // Erase word pos, line, killAction = s.eraseWord(pos, line, killAction) case ctrlY: // Paste from Yank buffer line, pos, next, err = s.yank(p, line, pos) goto haveNext case ctrlR: // Reverse Search line, pos, next, err = s.reverseISearch(line, pos) s.needRefresh = true goto haveNext case tab: // Tab completion line, pos, next, err = s.tabComplete(p, line, pos) goto haveNext // Catch keys that do nothing, but you don't want them to beep case esc: // DO NOTHING // Unused keys case ctrlG, ctrlO, ctrlQ, ctrlS, ctrlV, ctrlX, ctrlZ: fallthrough // Catch unhandled control codes (anything <= 31) case 0, 28, 29, 30, 31: s.doBeep() default: if pos == len(line) && !s.multiLineMode && len(p)+len(line) < s.columns*4 && // Avoid countGlyphs on large lines countGlyphs(p)+countGlyphs(line) < s.columns-1 { line = append(line, v) fmt.Printf("%c", v) pos++ } else { line = append(line[:pos], append([]rune{v}, line[pos:]...)...) pos++ s.needRefresh = true } } case action: switch v { case del: if pos >= len(line) { s.doBeep() } else { n := len(getPrefixGlyphs(line[pos:], 1)) line = append(line[:pos], line[pos+n:]...) } case left: if pos > 0 { pos -= len(getSuffixGlyphs(line[:pos], 1)) } else { s.doBeep() } case wordLeft, altB: if pos > 0 { var spaceHere, spaceLeft, leftKnown bool for { pos-- if pos == 0 { break } if leftKnown { spaceHere = spaceLeft } else { spaceHere = unicode.IsSpace(line[pos]) } spaceLeft, leftKnown = unicode.IsSpace(line[pos-1]), true if !spaceHere && spaceLeft { break } } } else { s.doBeep() } case right: if pos < len(line) { pos += len(getPrefixGlyphs(line[pos:], 1)) } else { s.doBeep() } case wordRight, altF: if pos < len(line) { var spaceHere, spaceLeft, hereKnown bool for { pos++ if pos == len(line) { break } if hereKnown { spaceLeft = spaceHere } else { spaceLeft = unicode.IsSpace(line[pos-1]) } spaceHere, hereKnown = unicode.IsSpace(line[pos]), true if spaceHere && !spaceLeft { break } } } else { s.doBeep() } case up: historyAction = true if historyStale { historyPrefix = s.getHistoryByPrefix(string(line)) historyPos = len(historyPrefix) historyStale = false } if historyPos > 0 { if historyPos == len(historyPrefix) { historyEnd = string(line) } historyPos-- line = []rune(historyPrefix[historyPos]) pos = len(line) } else { s.doBeep() } case down: historyAction = true if historyStale { historyPrefix = s.getHistoryByPrefix(string(line)) historyPos = len(historyPrefix) historyStale = false } if historyPos < len(historyPrefix) { historyPos++ if historyPos == len(historyPrefix) { line = []rune(historyEnd) } else { line = []rune(historyPrefix[historyPos]) } pos = len(line) } else { s.doBeep() } case home: // Start of line pos = 0 case end: // End of line pos = len(line) case altD: // Delete next word if pos == len(line) { s.doBeep() break } // Remove whitespace to the right var buf []rune // Store the deleted chars in a buffer for { if pos == len(line) || !unicode.IsSpace(line[pos]) { break } buf = append(buf, line[pos]) line = append(line[:pos], line[pos+1:]...) } // Remove non-whitespace to the right for { if pos == len(line) || unicode.IsSpace(line[pos]) { break } buf = append(buf, line[pos]) line = append(line[:pos], line[pos+1:]...) } // Save the result on the killRing if killAction > 0 { s.addToKillRing(buf, 2) // Add in prepend mode } else { s.addToKillRing(buf, 0) // Add in normal mode } killAction = 2 // Mark that there was some killing case altBs: // Erase word pos, line, killAction = s.eraseWord(pos, line, killAction) case winch: // Window change if s.multiLineMode { if s.maxRows-s.cursorRows > 0 { s.moveDown(s.maxRows - s.cursorRows) } for i := 0; i < s.maxRows-1; i++ { s.cursorPos(0) s.eraseLine() s.moveUp(1) } s.maxRows = 1 s.cursorRows = 1 } } s.needRefresh = true } if s.needRefresh && !s.inputWaiting() { err := s.refresh(p, line, pos) if err != nil { return "", err } } if !historyAction { historyStale = true } if killAction > 0 { killAction-- } } return string(line), nil } // PasswordPrompt displays p, and then waits for user input. The input typed by // the user is not displayed in the terminal. func (s *State) PasswordPrompt(prompt string) (string, error) { for _, r := range prompt { if unicode.Is(unicode.C, r) { return "", ErrInvalidPrompt } } if !s.terminalSupported || s.columns == 0 { return "", errors.New("liner: function not supported in this terminal") } if s.inputRedirected { return s.promptUnsupported(prompt) } if s.outputRedirected { return "", ErrNotTerminalOutput } p := []rune(prompt) defer s.stopPrompt() restart: s.startPrompt() s.getColumns() fmt.Print(prompt) var line []rune pos := 0 mainLoop: for { next, err := s.readNext() if err != nil { if s.shouldRestart != nil && s.shouldRestart(err) { goto restart } return "", err } switch v := next.(type) { case rune: switch v { case cr, lf: fmt.Println() break mainLoop case ctrlD: // del if pos == 0 && len(line) == 0 { // exit return "", io.EOF } // ctrlD is a potential EOF, so the rune reader shuts down. // Therefore, if it isn't actually an EOF, we must re-startPrompt. s.restartPrompt() case ctrlL: // clear screen s.eraseScreen() err := s.refresh(p, []rune{}, 0) if err != nil { return "", err } case ctrlH, bs: // Backspace if pos <= 0 { s.doBeep() } else { n := len(getSuffixGlyphs(line[:pos], 1)) line = append(line[:pos-n], line[pos:]...) pos -= n } case ctrlC: fmt.Println("^C") if s.ctrlCAborts { return "", ErrPromptAborted } line = line[:0] pos = 0 fmt.Print(prompt) s.restartPrompt() // Unused keys case esc, tab, ctrlA, ctrlB, ctrlE, ctrlF, ctrlG, ctrlK, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlR, ctrlS, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: fallthrough // Catch unhandled control codes (anything <= 31) case 0, 28, 29, 30, 31: s.doBeep() default: line = append(line[:pos], append([]rune{v}, line[pos:]...)...) pos++ } } } return string(line), nil } func (s *State) tooNarrow(prompt string) (string, error) { // Docker and OpenWRT and etc sometimes return 0 column width // Reset mode temporarily. Restore baked mode in case the terminal // is wide enough for the next Prompt attempt. m, merr := TerminalMode() s.origMode.ApplyMode() if merr == nil { defer m.ApplyMode() } if s.r == nil { // Windows does not always set s.r s.r = bufio.NewReader(os.Stdin) defer func() { s.r = nil }() } return s.promptUnsupported(prompt) } func (s *State) eraseWord(pos int, line []rune, killAction int) (int, []rune, int) { if pos == 0 { s.doBeep() return pos, line, killAction } // Remove whitespace to the left var buf []rune // Store the deleted chars in a buffer for { if pos == 0 || !unicode.IsSpace(line[pos-1]) { break } buf = append(buf, line[pos-1]) line = append(line[:pos-1], line[pos:]...) pos-- } // Remove non-whitespace to the left for { if pos == 0 || unicode.IsSpace(line[pos-1]) { break } buf = append(buf, line[pos-1]) line = append(line[:pos-1], line[pos:]...) pos-- } // Invert the buffer and save the result on the killRing var newBuf []rune for i := len(buf) - 1; i >= 0; i-- { newBuf = append(newBuf, buf[i]) } if killAction > 0 { s.addToKillRing(newBuf, 2) // Add in prepend mode } else { s.addToKillRing(newBuf, 0) // Add in normal mode } killAction = 2 // Mark that there was some killing s.needRefresh = true return pos, line, killAction } func (s *State) doBeep() { if !s.noBeep { fmt.Print(beep) } } liner-1.2.1/line_test.go000066400000000000000000000063541375564765300151760ustar00rootroot00000000000000package liner import ( "bytes" "fmt" "strings" "testing" ) func TestAppend(t *testing.T) { var s State s.AppendHistory("foo") s.AppendHistory("bar") var out bytes.Buffer num, err := s.WriteHistory(&out) if err != nil { t.Fatal("Unexpected error writing history", err) } if num != 2 { t.Fatalf("Expected 2 history entries, got %d", num) } s.AppendHistory("baz") num, err = s.WriteHistory(&out) if err != nil { t.Fatal("Unexpected error writing history", err) } if num != 3 { t.Fatalf("Expected 3 history entries, got %d", num) } s.AppendHistory("baz") num, err = s.WriteHistory(&out) if err != nil { t.Fatal("Unexpected error writing history", err) } if num != 3 { t.Fatalf("Expected 3 history entries after duplicate append, got %d", num) } s.AppendHistory("baz") } func TestHistory(t *testing.T) { input := `foo bar baz quux dingle` var s State num, err := s.ReadHistory(strings.NewReader(input)) if err != nil { t.Fatal("Unexpected error reading history", err) } if num != 5 { t.Fatal("Wrong number of history entries read") } var out bytes.Buffer num, err = s.WriteHistory(&out) if err != nil { t.Fatal("Unexpected error writing history", err) } if num != 5 { t.Fatal("Wrong number of history entries written") } if strings.TrimSpace(out.String()) != input { t.Fatal("Round-trip failure") } // clear the history and re-write s.ClearHistory() num, err = s.WriteHistory(&out) if err != nil { t.Fatal("Unexpected error writing history", err) } if num != 0 { t.Fatal("Wrong number of history entries written, expected none") } // Test reading with a trailing newline present var s2 State num, err = s2.ReadHistory(&out) if err != nil { t.Fatal("Unexpected error reading history the 2nd time", err) } if num != 5 { t.Fatal("Wrong number of history entries read the 2nd time") } num, err = s.ReadHistory(strings.NewReader(input + "\n\xff")) if err == nil { t.Fatal("Unexpected success reading corrupted history", err) } if num != 5 { t.Fatal("Wrong number of history entries read the 3rd time") } } func TestColumns(t *testing.T) { list := []string{"foo", "food", "This entry is quite a bit longer than the typical entry"} output := []struct { width, columns, rows, maxWidth int }{ {80, 1, 3, len(list[2]) + 1}, {120, 2, 2, len(list[2]) + 1}, {800, 14, 1, 0}, {8, 1, 3, 7}, } for i, o := range output { col, row, max := calculateColumns(o.width, list) if col != o.columns { t.Fatalf("Wrong number of columns, %d != %d, in TestColumns %d\n", col, o.columns, i) } if row != o.rows { t.Fatalf("Wrong number of rows, %d != %d, in TestColumns %d\n", row, o.rows, i) } if max != o.maxWidth { t.Fatalf("Wrong column width, %d != %d, in TestColumns %d\n", max, o.maxWidth, i) } } } // This example demonstrates a way to retrieve the current // history buffer without using a file. func ExampleState_WriteHistory() { var s State s.AppendHistory("foo") s.AppendHistory("bar") buf := new(bytes.Buffer) _, err := s.WriteHistory(buf) if err == nil { history := strings.Split(strings.TrimSpace(buf.String()), "\n") for i, line := range history { fmt.Println("History entry", i, ":", line) } } // Output: // History entry 0 : foo // History entry 1 : bar } liner-1.2.1/output.go000066400000000000000000000027221375564765300145430ustar00rootroot00000000000000// +build linux darwin openbsd freebsd netbsd package liner import ( "fmt" "os" "strings" "syscall" "unsafe" ) func (s *State) cursorPos(x int) { if s.useCHA { // 'G' is "Cursor Character Absolute (CHA)" fmt.Printf("\x1b[%dG", x+1) } else { // 'C' is "Cursor Forward (CUF)" fmt.Print("\r") if x > 0 { fmt.Printf("\x1b[%dC", x) } } } func (s *State) eraseLine() { fmt.Print("\x1b[0K") } func (s *State) eraseScreen() { fmt.Print("\x1b[H\x1b[2J") } func (s *State) moveUp(lines int) { fmt.Printf("\x1b[%dA", lines) } func (s *State) moveDown(lines int) { fmt.Printf("\x1b[%dB", lines) } func (s *State) emitNewLine() { fmt.Print("\n") } type winSize struct { row, col uint16 xpixel, ypixel uint16 } func (s *State) getColumns() bool { var ws winSize ok, _, _ := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout), syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws))) if int(ok) < 0 { return false } s.columns = int(ws.col) return true } func (s *State) checkOutput() { // xterm is known to support CHA if strings.Contains(strings.ToLower(os.Getenv("TERM")), "xterm") { s.useCHA = true return } // The test for functional ANSI CHA is unreliable (eg the Windows // telnet command does not support reading the cursor position with // an ANSI DSR request, despite setting TERM=ansi) // Assume CHA isn't supported (which should be safe, although it // does result in occasional visible cursor jitter) s.useCHA = false } liner-1.2.1/output_windows.go000066400000000000000000000042671375564765300163230ustar00rootroot00000000000000package liner import ( "unsafe" ) type coord struct { x, y int16 } type smallRect struct { left, top, right, bottom int16 } type consoleScreenBufferInfo struct { dwSize coord dwCursorPosition coord wAttributes int16 srWindow smallRect dwMaximumWindowSize coord } func (s *State) cursorPos(x int) { var sbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) procSetConsoleCursorPosition.Call(uintptr(s.hOut), uintptr(int(x)&0xFFFF|int(sbi.dwCursorPosition.y)<<16)) } func (s *State) eraseLine() { var sbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) var numWritten uint32 procFillConsoleOutputCharacter.Call(uintptr(s.hOut), uintptr(' '), uintptr(sbi.dwSize.x-sbi.dwCursorPosition.x), uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|int(sbi.dwCursorPosition.y)<<16), uintptr(unsafe.Pointer(&numWritten))) } func (s *State) eraseScreen() { var sbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) var numWritten uint32 procFillConsoleOutputCharacter.Call(uintptr(s.hOut), uintptr(' '), uintptr(sbi.dwSize.x)*uintptr(sbi.dwSize.y), 0, uintptr(unsafe.Pointer(&numWritten))) procSetConsoleCursorPosition.Call(uintptr(s.hOut), 0) } func (s *State) moveUp(lines int) { var sbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) procSetConsoleCursorPosition.Call(uintptr(s.hOut), uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|(int(sbi.dwCursorPosition.y)-lines)<<16)) } func (s *State) moveDown(lines int) { var sbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) procSetConsoleCursorPosition.Call(uintptr(s.hOut), uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|(int(sbi.dwCursorPosition.y)+lines)<<16)) } func (s *State) emitNewLine() { // windows doesn't need to omit a new line } func (s *State) getColumns() { var sbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) s.columns = int(sbi.dwSize.x) } liner-1.2.1/prefix_test.go000066400000000000000000000020371375564765300155360ustar00rootroot00000000000000// +build windows linux darwin openbsd freebsd netbsd package liner import "testing" type testItem struct { list []string prefix string } func TestPrefix(t *testing.T) { list := []testItem{ {[]string{"food", "foot"}, "foo"}, {[]string{"foo", "foot"}, "foo"}, {[]string{"food", "foo"}, "foo"}, {[]string{"food", "foe", "foot"}, "fo"}, {[]string{"food", "foot", "barbeque"}, ""}, {[]string{"cafeteria", "café"}, "caf"}, {[]string{"cafe", "café"}, "caf"}, {[]string{"cafè", "café"}, "caf"}, {[]string{"cafés", "café"}, "café"}, {[]string{"áéíóú", "áéíóú"}, "áéíóú"}, {[]string{"éclairs", "éclairs"}, "éclairs"}, {[]string{"éclairs are the best", "éclairs are great", "éclairs"}, "éclairs"}, {[]string{"éclair", "éclairs"}, "éclair"}, {[]string{"éclairs", "éclair"}, "éclair"}, {[]string{"éclair", "élan"}, "é"}, } for _, test := range list { lcp := longestCommonPrefix(test.list) if lcp != test.prefix { t.Errorf("%s != %s for %+v", lcp, test.prefix, test.list) } } } liner-1.2.1/race_test.go000066400000000000000000000012221375564765300151460ustar00rootroot00000000000000// +build race package liner import ( "io/ioutil" "os" "sync" "testing" ) func TestWriteHistory(t *testing.T) { oldout := os.Stdout defer func() { os.Stdout = oldout }() oldin := os.Stdout defer func() { os.Stdin = oldin }() newinr, newinw, err := os.Pipe() if err != nil { t.Fatal(err) } os.Stdin = newinr newoutr, newoutw, err := os.Pipe() if err != nil { t.Fatal(err) } defer newoutr.Close() os.Stdout = newoutw var wait sync.WaitGroup wait.Add(1) s := NewLiner() go func() { s.AppendHistory("foo") s.AppendHistory("bar") s.Prompt("") wait.Done() }() s.WriteHistory(ioutil.Discard) newinw.Close() wait.Wait() } liner-1.2.1/unixmode.go000066400000000000000000000015101375564765300150250ustar00rootroot00000000000000// +build linux darwin freebsd openbsd netbsd package liner import ( "syscall" "unsafe" ) func (mode *termios) ApplyMode() error { _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), setTermios, uintptr(unsafe.Pointer(mode))) if errno != 0 { return errno } return nil } // TerminalMode returns the current terminal input mode as an InputModeSetter. // // This function is provided for convenience, and should // not be necessary for most users of liner. func TerminalMode() (ModeApplier, error) { mode, errno := getMode(syscall.Stdin) if errno != 0 { return nil, errno } return mode, nil } func getMode(handle int) (*termios, syscall.Errno) { var mode termios _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(handle), getTermios, uintptr(unsafe.Pointer(&mode))) return &mode, errno } liner-1.2.1/width.go000066400000000000000000000031411375564765300143160ustar00rootroot00000000000000package liner import ( "unicode" "github.com/mattn/go-runewidth" ) // These character classes are mostly zero width (when combined). // A few might not be, depending on the user's font. Fixing this // is non-trivial, given that some terminals don't support // ANSI DSR/CPR var zeroWidth = []*unicode.RangeTable{ unicode.Mn, unicode.Me, unicode.Cc, unicode.Cf, } // countGlyphs considers zero-width characters to be zero glyphs wide, // and members of Chinese, Japanese, and Korean scripts to be 2 glyphs wide. func countGlyphs(s []rune) int { n := 0 for _, r := range s { // speed up the common case if r < 127 { n++ continue } n += runewidth.RuneWidth(r) } return n } func countMultiLineGlyphs(s []rune, columns int, start int) int { n := start for _, r := range s { if r < 127 { n++ continue } switch runewidth.RuneWidth(r) { case 0: case 1: n++ case 2: n += 2 // no room for a 2-glyphs-wide char in the ending // so skip a column and display it at the beginning if n%columns == 1 { n++ } } } return n } func getPrefixGlyphs(s []rune, num int) []rune { p := 0 for n := 0; n < num && p < len(s); p++ { // speed up the common case if s[p] < 127 { n++ continue } if !unicode.IsOneOf(zeroWidth, s[p]) { n++ } } for p < len(s) && unicode.IsOneOf(zeroWidth, s[p]) { p++ } return s[:p] } func getSuffixGlyphs(s []rune, num int) []rune { p := len(s) for n := 0; n < num && p > 0; p-- { // speed up the common case if s[p-1] < 127 { n++ continue } if !unicode.IsOneOf(zeroWidth, s[p-1]) { n++ } } return s[p:] } liner-1.2.1/width_test.go000066400000000000000000000050651375564765300153640ustar00rootroot00000000000000package liner import ( "strconv" "testing" ) func accent(in []rune) []rune { var out []rune for _, r := range in { out = append(out, r) out = append(out, '\u0301') } return out } type testCase struct { s []rune glyphs int } var testCases = []testCase{ {[]rune("query"), 5}, {[]rune("私"), 2}, {[]rune("hello『世界』"), 13}, } func TestCountGlyphs(t *testing.T) { for _, testCase := range testCases { count := countGlyphs(testCase.s) if count != testCase.glyphs { t.Errorf("ASCII count incorrect. %d != %d", count, testCase.glyphs) } count = countGlyphs(accent(testCase.s)) if count != testCase.glyphs { t.Errorf("Accent count incorrect. %d != %d", count, testCase.glyphs) } } } func compare(a, b []rune, name string, t *testing.T) { if len(a) != len(b) { t.Errorf(`"%s" != "%s" in %s"`, string(a), string(b), name) return } for i := range a { if a[i] != b[i] { t.Errorf(`"%s" != "%s" in %s"`, string(a), string(b), name) return } } } func TestPrefixGlyphs(t *testing.T) { for _, testCase := range testCases { for i := 0; i <= len(testCase.s); i++ { iter := strconv.Itoa(i) out := getPrefixGlyphs(testCase.s, i) compare(out, testCase.s[:i], "ascii prefix "+iter, t) out = getPrefixGlyphs(accent(testCase.s), i) compare(out, accent(testCase.s[:i]), "accent prefix "+iter, t) } out := getPrefixGlyphs(testCase.s, 999) compare(out, testCase.s, "ascii prefix overflow", t) out = getPrefixGlyphs(accent(testCase.s), 999) compare(out, accent(testCase.s), "accent prefix overflow", t) out = getPrefixGlyphs(testCase.s, -3) if len(out) != 0 { t.Error("ascii prefix negative") } out = getPrefixGlyphs(accent(testCase.s), -3) if len(out) != 0 { t.Error("accent prefix negative") } } } func TestSuffixGlyphs(t *testing.T) { for _, testCase := range testCases { for i := 0; i <= len(testCase.s); i++ { iter := strconv.Itoa(i) out := getSuffixGlyphs(testCase.s, i) compare(out, testCase.s[len(testCase.s)-i:], "ascii suffix "+iter, t) out = getSuffixGlyphs(accent(testCase.s), i) compare(out, accent(testCase.s[len(testCase.s)-i:]), "accent suffix "+iter, t) } out := getSuffixGlyphs(testCase.s, 999) compare(out, testCase.s, "ascii suffix overflow", t) out = getSuffixGlyphs(accent(testCase.s), 999) compare(out, accent(testCase.s), "accent suffix overflow", t) out = getSuffixGlyphs(testCase.s, -3) if len(out) != 0 { t.Error("ascii suffix negative") } out = getSuffixGlyphs(accent(testCase.s), -3) if len(out) != 0 { t.Error("accent suffix negative") } } }