mirror of
				https://github.com/quitesimpleorg/hs9001.git
				synced 2025-10-31 15:29:30 +01:00 
			
		
		
		
	Implement bash CTRL-R reverse history
Fork customized liner into own repo bash. Rename -being and -end options to -after and -before.
This commit is contained in:
		
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,7 +3,7 @@ module hs9001 | |||||||
| go 1.16 | go 1.16 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/peterh/liner v1.2.1 | 	github.com/mattn/go-runewidth v0.0.13 | ||||||
| 	github.com/tj/go-naturaldate v1.3.0 | 	github.com/tj/go-naturaldate v1.3.0 | ||||||
| 	modernc.org/sqlite v1.10.0 | 	modernc.org/sqlite v1.10.0 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @@ -8,16 +8,16 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU | |||||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | ||||||
| github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | ||||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||||
| github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= | ||||||
| github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||||
| github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= | ||||||
| github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||||
| github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg= |  | ||||||
| github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= |  | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= | ||||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||||
|  | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | ||||||
|  | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								history.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								history.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"database/sql" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type history struct { | ||||||
|  | 	conn *sql.DB | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *history) GetHistoryByPrefix(prefix string) (ph []string) { | ||||||
|  | 	opts := searchopts{} | ||||||
|  | 	opts.order = "DESC" | ||||||
|  | 	cmdqry := prefix + "%" | ||||||
|  | 	opts.command = &cmdqry | ||||||
|  | 	results := search(h.conn, opts) | ||||||
|  | 	for e := results.Front(); e != nil; e = e.Next() { | ||||||
|  | 		entry, ok := e.Value.(*HistoryEntry) | ||||||
|  | 		if !ok { | ||||||
|  | 			log.Panic("Failed to retrieve entries") | ||||||
|  | 		} | ||||||
|  | 		ph = append(ph, entry.cmd) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | func (h *history) GetHistoryByPattern(pattern string) (ph []string, pos []int) { | ||||||
|  | 	opts := searchopts{} | ||||||
|  | 	opts.order = "DESC" | ||||||
|  | 	cmdqry := "%" + pattern + "%" | ||||||
|  | 	opts.command = &cmdqry | ||||||
|  | 	results := search(h.conn, opts) | ||||||
|  | 	for e := results.Front(); e != nil; e = e.Next() { | ||||||
|  | 		entry, ok := e.Value.(*HistoryEntry) | ||||||
|  | 		if !ok { | ||||||
|  | 			log.Panic("Failed to retrieve entries") | ||||||
|  | 		} | ||||||
|  | 		ph = append(ph, entry.cmd) | ||||||
|  | 		pos = append(pos, strings.Index(strings.ToLower(entry.cmd), strings.ToLower(pattern))) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *history) ReadHistory(r io.Reader) (num int, err error) { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | func (h *history) WriteHistory(w io.Writer) (num int, err error) { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | func (h *history) AppendHistory(item string) { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | func (h *history) ClearHistory() { | ||||||
|  | 	panic("not implemented") | ||||||
|  | } | ||||||
|  | func (h *history) RLock() { | ||||||
|  | 	//noop | ||||||
|  | } | ||||||
|  | func (h *history) RUnlock() { | ||||||
|  | 	//noop | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								liner/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								liner/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | Copyright © 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. | ||||||
							
								
								
									
										196
									
								
								liner/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								liner/common.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type commonState struct { | ||||||
|  | 	terminalSupported bool | ||||||
|  | 	outputRedirected  bool | ||||||
|  | 	inputRedirected   bool | ||||||
|  | 	historyProvider   HistoryProvider | ||||||
|  | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type HistoryProvider interface { | ||||||
|  | 	ReadHistory(r io.Reader) (num int, err error) | ||||||
|  | 	WriteHistory(w io.Writer) (num int, err error) | ||||||
|  | 	AppendHistory(item string) | ||||||
|  | 	ClearHistory() | ||||||
|  | 	GetHistoryByPrefix(prefix string) (ph []string) | ||||||
|  | 	GetHistoryByPattern(pattern string) (ph []string, pos []int) | ||||||
|  | 	RLock() | ||||||
|  | 	RUnlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  |  | ||||||
|  | func (s *State) ReadHistory(r io.Reader) (num int, err error) { | ||||||
|  | 	return s.historyProvider.ReadHistory(r) | ||||||
|  | } | ||||||
|  | func (s *State) WriteHistory(w io.Writer) (num int, err error) { | ||||||
|  | 	return s.historyProvider.WriteHistory(w) | ||||||
|  | } | ||||||
|  | func (s *State) AppendHistory(item string) { | ||||||
|  | 	s.historyProvider.AppendHistory(item) | ||||||
|  | } | ||||||
|  | func (s *State) ClearHistory() { | ||||||
|  | 	s.historyProvider.ClearHistory() | ||||||
|  | } | ||||||
|  | func (s *State) getHistoryByPrefix(prefix string) (ph []string) { | ||||||
|  | 	return s.historyProvider.GetHistoryByPrefix(prefix) | ||||||
|  | } | ||||||
|  | func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) { | ||||||
|  | 	return s.historyProvider.GetHistoryByPattern(pattern) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetHistoryProvider allows you to set a custom provider | ||||||
|  | // for reading, writing and searching history. | ||||||
|  | // By implementing the HistoryProvider interface | ||||||
|  | // and injecting your custom implementation here. | ||||||
|  | func (s *State) SetHistoryProvider(f HistoryProvider) { | ||||||
|  | 	s.historyProvider = f | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | ||||||
|  | } | ||||||
							
								
								
									
										391
									
								
								liner/input.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								liner/input.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,391 @@ | |||||||
|  | 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"))] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
							
								
								
									
										1075
									
								
								liner/line.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1075
									
								
								liner/line.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										74
									
								
								liner/output.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								liner/output.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								liner/unixmode.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								liner/unixmode.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								liner/width.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								liner/width.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | package 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:] | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										137
									
								
								main.go
									
									
									
									
									
								
							| @@ -10,10 +10,12 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/peterh/liner" | 	"hs9001/liner" | ||||||
|  |  | ||||||
| 	"github.com/tj/go-naturaldate" | 	"github.com/tj/go-naturaldate" | ||||||
| 	_ "modernc.org/sqlite" | 	_ "modernc.org/sqlite" | ||||||
| ) | ) | ||||||
| @@ -149,28 +151,47 @@ func importFromStdin(conn *sql.DB) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func search(conn *sql.DB, q string, workdir string, beginTime time.Time, endTime time.Time, retval int) list.List { | type searchopts struct { | ||||||
|  | 	command *string | ||||||
|  | 	workdir *string | ||||||
|  | 	after   *time.Time | ||||||
|  | 	before  *time.Time | ||||||
|  | 	retval  *int | ||||||
|  | 	order   string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func search(conn *sql.DB, opts searchopts) list.List { | ||||||
|  | 	args := make([]interface{}, 0) | ||||||
| 	var sb strings.Builder | 	var sb strings.Builder | ||||||
| 	sb.WriteString("SELECT id, command, workdir, user, hostname, retval ") | 	sb.WriteString("SELECT id, command, workdir, user, hostname, retval ") | ||||||
| 	sb.WriteString("FROM history ") | 	sb.WriteString("FROM history ") | ||||||
| 	sb.WriteString("WHERE timestamp BETWEEN datetime(?, 'unixepoch') ") | 	sb.WriteString("WHERE 1=1 ") //1=1 so we can append as many AND foo as we want, or none | ||||||
| 	sb.WriteString("AND datetime(?, 'unixepoch') ") |  | ||||||
| 	sb.WriteString("AND command LIKE ? ") | 	if opts.command != nil { | ||||||
| 	sb.WriteString("AND workdir LIKE ? ") | 		sb.WriteString("AND command LIKE ? ") | ||||||
| 	if retval != -9001 { | 		args = append(args, opts.command) | ||||||
| 		sb.WriteString("AND retval = ? ") |  | ||||||
| 	} | 	} | ||||||
| 	sb.WriteString("ORDER BY timestamp ASC ") | 	if opts.workdir != nil { | ||||||
|  | 		sb.WriteString("AND workdir LIKE ? ") | ||||||
|  | 		args = append(args, opts.workdir) | ||||||
|  | 	} | ||||||
|  | 	if opts.after != nil { | ||||||
|  | 		sb.WriteString("AND timestamp > datetime(?, 'unixepoch') ") | ||||||
|  | 		args = append(args, opts.after.Unix()) | ||||||
|  | 	} | ||||||
|  | 	if opts.before != nil { | ||||||
|  | 		sb.WriteString("AND timestamp < datetime(?, 'unixepoch') ") | ||||||
|  | 		args = append(args, opts.before.Unix()) | ||||||
|  | 	} | ||||||
|  | 	if opts.retval != nil { | ||||||
|  | 		sb.WriteString("AND retval = ? ") | ||||||
|  | 		args = append(args, opts.retval) | ||||||
|  | 	} | ||||||
|  | 	sb.WriteString("ORDER BY timestamp ") | ||||||
|  | 	sb.WriteString("ASC ") | ||||||
|  |  | ||||||
| 	queryStmt := sb.String() | 	queryStmt := sb.String() | ||||||
| 	args := make([]interface{}, 0) |  | ||||||
| 	args = append(args, beginTime.Unix()) |  | ||||||
| 	args = append(args, endTime.Unix()) |  | ||||||
| 	args = append(args, q) |  | ||||||
| 	args = append(args, workdir) |  | ||||||
| 	if retval != -9001 { |  | ||||||
| 		args = append(args, retval) |  | ||||||
| 	} |  | ||||||
| 	rows, err := conn.Query(queryStmt, args...) | 	rows, err := conn.Query(queryStmt, args...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Panic(err) | 		log.Panic(err) | ||||||
| @@ -271,34 +292,22 @@ func main() { | |||||||
| 		line := liner.NewLiner() | 		line := liner.NewLiner() | ||||||
| 		defer line.Close() | 		defer line.Close() | ||||||
| 		line.SetCtrlCAborts(true) | 		line.SetCtrlCAborts(true) | ||||||
| 		line.SetCompleter(func(line string) (c []string) { | 		line.SetHistoryProvider(&history{conn: conn}) | ||||||
| 			beginTimestamp, err := naturaldate.Parse("50 years ago", time.Now()) | 		line.SetMultiLineMode(true) | ||||||
| 			if err != nil { |  | ||||||
| 				fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) |  | ||||||
| 			} |  | ||||||
| 			endTimeStamp, err := naturaldate.Parse("now", time.Now()) |  | ||||||
| 			if err != nil { |  | ||||||
| 				fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) |  | ||||||
| 			} |  | ||||||
| 			results := search(conn, "%"+line+"%", "%", beginTimestamp, endTimeStamp, -9001) |  | ||||||
| 			for e := results.Front(); e != nil; e = e.Next() { |  | ||||||
| 				entry, ok := e.Value.(*HistoryEntry) |  | ||||||
| 				if !ok { |  | ||||||
| 					log.Panic("Failed to retrieve entries") |  | ||||||
| 				} |  | ||||||
| 				c = append(c, entry.cmd) |  | ||||||
| 			} |  | ||||||
| 			return |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		if name, err := line.Prompt("What is your command? "); err == nil { | 		rdlineline := os.Getenv("READLINE_LINE") | ||||||
| 			log.Print("Got: ", name) | 		rdlinepos := os.Getenv("READLINE_POS") | ||||||
|  | 		rdlineposint, _ := strconv.Atoi(rdlinepos) | ||||||
|  |  | ||||||
|  | 		if name, err := line.PromptWithSuggestionReverse("", rdlineline, rdlineposint); err == nil { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "%s\n", name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	case "bash-enable": | 	case "bash-enable": | ||||||
| 		fmt.Printf(` | 		fmt.Printf(` | ||||||
| 			if [ -n "$PS1" ] ; then | 			if [ -n "$PS1" ] ; then | ||||||
| 				PROMPT_COMMAND='hs9001 add -ret $? "$(history 1)"' | 				PROMPT_COMMAND='hs9001 add -ret $? "$(history 1)"' | ||||||
|  | 				bind -x '"\C-r": " READLINE_LINE=$(hs9001 bash-ctrlr 3>&1 1>&2 2>&3) READLINE_POINT=0"' | ||||||
| 			fi | 			fi | ||||||
| 		`) | 		`) | ||||||
| 	case "bash-disable": | 	case "bash-disable": | ||||||
| @@ -326,13 +335,13 @@ func main() { | |||||||
| 		fallthrough | 		fallthrough | ||||||
| 	case "delete": | 	case "delete": | ||||||
| 		var workDir string | 		var workDir string | ||||||
| 		var beginTime string | 		var afterTime string | ||||||
| 		var endTime string | 		var beforeTime string | ||||||
| 		var distinct bool = true | 		var distinct bool = true | ||||||
| 		var retVal int | 		var retVal int | ||||||
| 		searchCmd.StringVar(&workDir, "cwd", "%", "Search only within this workdir") | 		searchCmd.StringVar(&workDir, "cwd", "", "Search only within this workdir") | ||||||
| 		searchCmd.StringVar(&beginTime, "begin", "50 years ago", "Start searching from this timeframe") | 		searchCmd.StringVar(&afterTime, "after", "", "Start searching from this timeframe") | ||||||
| 		searchCmd.StringVar(&endTime, "end", "now", "End searching from this timeframe") | 		searchCmd.StringVar(&beforeTime, "before", "", "End searching from this timeframe") | ||||||
| 		searchCmd.BoolVar(&distinct, "distinct", true, "Remove consecutive duplicate commands from output") | 		searchCmd.BoolVar(&distinct, "distinct", true, "Remove consecutive duplicate commands from output") | ||||||
| 		searchCmd.IntVar(&retVal, "ret", -9001, "Only query commands that returned with this exit code. -9001=all (default)") | 		searchCmd.IntVar(&retVal, "ret", -9001, "Only query commands that returned with this exit code. -9001=all (default)") | ||||||
|  |  | ||||||
| @@ -340,25 +349,39 @@ func main() { | |||||||
|  |  | ||||||
| 		args := searchCmd.Args() | 		args := searchCmd.Args() | ||||||
|  |  | ||||||
| 		beginTimestamp, err := naturaldate.Parse(beginTime, time.Now()) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		endTimeStamp, err := naturaldate.Parse(endTime, time.Now()) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		q := strings.Join(args, " ") | 		q := strings.Join(args, " ") | ||||||
| 		if workDir != "%" { |  | ||||||
| 			workDir, err = filepath.Abs(workDir) | 		opts := searchopts{} | ||||||
|  | 		opts.order = "ASC" | ||||||
|  | 		if q != "" { | ||||||
|  | 			cmd := "%" + q + "%" | ||||||
|  | 			opts.command = &cmd | ||||||
|  | 		} | ||||||
|  | 		if workDir != "" { | ||||||
|  | 			wd, err := filepath.Abs(workDir) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				fmt.Fprintf(os.Stderr, "Failed parse working directory path: %s\n", err.Error()) | 				fmt.Fprintf(os.Stderr, "Failed parse working directory path: %s\n", err.Error()) | ||||||
| 			} | 			} | ||||||
|  | 			opts.workdir = &wd | ||||||
| 		} | 		} | ||||||
|  | 		if afterTime != "" { | ||||||
| 		results := search(conn, "%"+q+"%", workDir, beginTimestamp, endTimeStamp, retVal) | 			afterTimestamp, err := naturaldate.Parse(afterTime, time.Now()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) | ||||||
|  | 			} | ||||||
|  | 			opts.after = &afterTimestamp | ||||||
|  | 		} | ||||||
|  | 		if beforeTime != "" { | ||||||
|  | 			beforeTimestamp, err := naturaldate.Parse(beforeTime, time.Now()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				fmt.Fprintf(os.Stderr, "Failed to convert time string: %s\n", err.Error()) | ||||||
|  | 			} | ||||||
|  | 			opts.before = &beforeTimestamp | ||||||
|  | 		} | ||||||
|  | 		if retVal != -9001 { | ||||||
|  | 			opts.retval = &retVal | ||||||
|  | 		} | ||||||
|  | 		results := search(conn, opts) | ||||||
|  |  | ||||||
| 		previousCmd := "" | 		previousCmd := "" | ||||||
| 		for e := results.Front(); e != nil; e = e.Next() { | 		for e := results.Front(); e != nil; e = e.Next() { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 lawl
					lawl