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:
lawl 2021-06-05 19:59:47 +02:00 committed by Albert S
parent 33183dd7d3
commit 67050d32b9
11 changed files with 2029 additions and 62 deletions

2
go.mod
View File

@ -3,7 +3,7 @@ module hs9001
go 1.16
require (
github.com/peterh/liner v1.2.1
github.com/mattn/go-runewidth v0.0.13
github.com/tj/go-naturaldate v1.3.0
modernc.org/sqlite v1.10.0
)

8
go.sum
View File

@ -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/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
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.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
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/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/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/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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=

63
history.go Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

74
liner/output.go Normal file
View 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
View 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
View 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
View File

@ -10,10 +10,12 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/peterh/liner"
"hs9001/liner"
"github.com/tj/go-naturaldate"
_ "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
sb.WriteString("SELECT id, command, workdir, user, hostname, retval ")
sb.WriteString("FROM history ")
sb.WriteString("WHERE timestamp BETWEEN datetime(?, 'unixepoch') ")
sb.WriteString("AND datetime(?, 'unixepoch') ")
sb.WriteString("AND command LIKE ? ")
sb.WriteString("AND workdir LIKE ? ")
if retval != -9001 {
sb.WriteString("AND retval = ? ")
sb.WriteString("WHERE 1=1 ") //1=1 so we can append as many AND foo as we want, or none
if opts.command != nil {
sb.WriteString("AND command LIKE ? ")
args = append(args, opts.command)
}
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()
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...)
if err != nil {
log.Panic(err)
@ -271,34 +292,22 @@ func main() {
line := liner.NewLiner()
defer line.Close()
line.SetCtrlCAborts(true)
line.SetCompleter(func(line string) (c []string) {
beginTimestamp, err := naturaldate.Parse("50 years ago", time.Now())
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
})
line.SetHistoryProvider(&history{conn: conn})
line.SetMultiLineMode(true)
if name, err := line.Prompt("What is your command? "); err == nil {
log.Print("Got: ", name)
rdlineline := os.Getenv("READLINE_LINE")
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":
fmt.Printf(`
if [ -n "$PS1" ] ; then
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
`)
case "bash-disable":
@ -326,13 +335,13 @@ func main() {
fallthrough
case "delete":
var workDir string
var beginTime string
var endTime string
var afterTime string
var beforeTime string
var distinct bool = true
var retVal int
searchCmd.StringVar(&workDir, "cwd", "%", "Search only within this workdir")
searchCmd.StringVar(&beginTime, "begin", "50 years ago", "Start searching from this timeframe")
searchCmd.StringVar(&endTime, "end", "now", "End searching from this timeframe")
searchCmd.StringVar(&workDir, "cwd", "", "Search only within this workdir")
searchCmd.StringVar(&afterTime, "after", "", "Start 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.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()
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, " ")
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 {
fmt.Fprintf(os.Stderr, "Failed parse working directory path: %s\n", err.Error())
}
opts.workdir = &wd
}
results := search(conn, "%"+q+"%", workDir, beginTimestamp, endTimeStamp, retVal)
if afterTime != "" {
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 := ""
for e := results.Front(); e != nil; e = e.Next() {