diff --git a/app.go b/app.go index 87b85dc5..4065c953 100644 --- a/app.go +++ b/app.go @@ -268,7 +268,7 @@ func (app *app) loop() { serverChan = readExpr() } - app.ui.readExpr() + go app.ui.readEvents() if gConfigPath != "" { if _, err := os.Stat(gConfigPath); !os.IsNotExist(err) { diff --git a/client.go b/client.go index 08ba8c87..dfd77c89 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,7 @@ import ( "sync" "time" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) type State struct { diff --git a/colors.go b/colors.go index 4871de62..5d6100be 100644 --- a/colors.go +++ b/colors.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) type styleMap struct { diff --git a/colors_test.go b/colors_test.go index 9afc7536..1dddabc8 100644 --- a/colors_test.go +++ b/colors_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) func TestParseColor(t *testing.T) { diff --git a/doc.md b/doc.md index e6d86dfe..cb477621 100644 --- a/doc.md +++ b/doc.md @@ -236,7 +236,7 @@ The following Command-line mode commands are provided by lf: cmd-home (default '' and '') cmd-end (default '' and '') cmd-delete (default '' and '') - cmd-delete-back (default '' and '') + cmd-delete-back (default '') cmd-delete-home (default '') cmd-delete-end (default '') cmd-delete-unix-word (default '') @@ -246,7 +246,7 @@ The following Command-line mode commands are provided by lf: cmd-word (default '') cmd-word-back (default '') cmd-delete-word (default '') - cmd-delete-word-back (default '' and '') + cmd-delete-word-back (default '') cmd-capitalize-word (default '') cmd-uppercase-word (default '') cmd-lowercase-word (default '') @@ -798,7 +798,7 @@ Move the cursor to the beginning/end of the line. Delete the next character. -## cmd-delete-back (default `` and ``) +## cmd-delete-back (default ``) Delete the previous character. When at the beginning of a prompt, returns either to Normal mode or to `:` mode. @@ -827,7 +827,7 @@ Move the cursor by one word in the forward/backward direction. Delete the next word in the forward direction. -## cmd-delete-word-back (default `` and ``) +## cmd-delete-word-back (default ``) Delete the previous word in the backward direction. diff --git a/doc.txt b/doc.txt index 402582c4..154d06ac 100644 --- a/doc.txt +++ b/doc.txt @@ -248,7 +248,7 @@ The following Command-line mode commands are provided by lf: cmd-home (default '' and '') cmd-end (default '' and '') cmd-delete (default '' and '') - cmd-delete-back (default '' and '') + cmd-delete-back (default '') cmd-delete-home (default '') cmd-delete-end (default '') cmd-delete-unix-word (default '') @@ -258,7 +258,7 @@ The following Command-line mode commands are provided by lf: cmd-word (default '') cmd-word-back (default '') cmd-delete-word (default '') - cmd-delete-word-back (default '' and '') + cmd-delete-word-back (default '') cmd-capitalize-word (default '') cmd-uppercase-word (default '') cmd-lowercase-word (default '') @@ -848,7 +848,7 @@ cmd-delete (default and ) Delete the next character. -cmd-delete-back (default and ) +cmd-delete-back (default ) Delete the previous character. When at the beginning of a prompt, returns either to Normal mode or to : mode. @@ -877,7 +877,7 @@ cmd-delete-word (default ) Delete the next word in the forward direction. -cmd-delete-word-back (default and ) +cmd-delete-word-back (default ) Delete the previous word in the backward direction. diff --git a/eval.go b/eval.go index 24ec9b7b..74f632b7 100644 --- a/eval.go +++ b/eval.go @@ -14,7 +14,7 @@ import ( "unicode" "unicode/utf8" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) func applyBoolOpt(opt *bool, e *setExpr) error { @@ -1713,7 +1713,7 @@ func (e *callExpr) eval(app *app, _ []string) { } log.Println("pushing keys", e.args[0]) for _, val := range splitKeys(e.args[0]) { - app.ui.keyChan <- val + app.ui.evChan <- parseKey(val) } case "addcustominfo": var k, v string diff --git a/go.mod b/go.mod index 9b35d94d..56c4832c 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,17 @@ go 1.24.0 require ( github.com/djherbis/times v1.6.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/gdamore/tcell/v2 v2.9.0 + github.com/gdamore/tcell/v3 v3.0.2 github.com/mattn/go-runewidth v0.0.19 golang.org/x/sys v0.38.0 golang.org/x/term v0.37.0 ) require ( - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - golang.org/x/text v0.29.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/text v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index f5f848ab..d6a87bd3 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,25 @@ +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys= -github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo= +github.com/gdamore/tcell/v3 v3.0.0 h1:Ycu66eWmaUD/AFqV1pfE4sseUo0H9TlIcaaAcnMC968= +github.com/gdamore/tcell/v3 v3.0.0/go.mod h1:OoJxRXVKR7ROCFzAIUkHHy/7mWTkz9sNfWLhdEsNHVM= +github.com/gdamore/tcell/v3 v3.0.2 h1:1sM7T9uc/iqQiJCliTBuqGcWAWksI355ejAfkPYHmVs= +github.com/gdamore/tcell/v3 v3.0.2/go.mod h1:OoJxRXVKR7ROCFzAIUkHHy/7mWTkz9sNfWLhdEsNHVM= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -43,8 +51,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/icons.go b/icons.go index 77aadd74..525341c9 100644 --- a/icons.go +++ b/icons.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) type iconDef struct { diff --git a/key.go b/key.go new file mode 100644 index 00000000..cd5a3985 --- /dev/null +++ b/key.go @@ -0,0 +1,228 @@ +package main + +import ( + "fmt" + "regexp" + "strings" + + "github.com/gdamore/tcell/v3" +) + +var gKeyVal = map[tcell.Key]string{ + tcell.KeyEnter: "", + tcell.KeyBackspace: "", + tcell.KeyTab: "", + tcell.KeyBacktab: "", + tcell.KeyEsc: "", + tcell.KeyDelete: "", + tcell.KeyInsert: "", + tcell.KeyUp: "", + tcell.KeyDown: "", + tcell.KeyLeft: "", + tcell.KeyRight: "", + tcell.KeyHome: "", + tcell.KeyEnd: "", + tcell.KeyUpLeft: "", + tcell.KeyUpRight: "", + tcell.KeyDownLeft: "", + tcell.KeyDownRight: "", + tcell.KeyCenter: "
", + tcell.KeyPgDn: "", + tcell.KeyPgUp: "", + tcell.KeyClear: "", + tcell.KeyExit: "", + tcell.KeyCancel: "", + tcell.KeyPause: "", + tcell.KeyPrint: "", + tcell.KeyF1: "", + tcell.KeyF2: "", + tcell.KeyF3: "", + tcell.KeyF4: "", + tcell.KeyF5: "", + tcell.KeyF6: "", + tcell.KeyF7: "", + tcell.KeyF8: "", + tcell.KeyF9: "", + tcell.KeyF10: "", + tcell.KeyF11: "", + tcell.KeyF12: "", + tcell.KeyF13: "", + tcell.KeyF14: "", + tcell.KeyF15: "", + tcell.KeyF16: "", + tcell.KeyF17: "", + tcell.KeyF18: "", + tcell.KeyF19: "", + tcell.KeyF20: "", + tcell.KeyF21: "", + tcell.KeyF22: "", + tcell.KeyF23: "", + tcell.KeyF24: "", + tcell.KeyF25: "", + tcell.KeyF26: "", + tcell.KeyF27: "", + tcell.KeyF28: "", + tcell.KeyF29: "", + tcell.KeyF30: "", + tcell.KeyF31: "", + tcell.KeyF32: "", + tcell.KeyF33: "", + tcell.KeyF34: "", + tcell.KeyF35: "", + tcell.KeyF36: "", + tcell.KeyF37: "", + tcell.KeyF38: "", + tcell.KeyF39: "", + tcell.KeyF40: "", + tcell.KeyF41: "", + tcell.KeyF42: "", + tcell.KeyF43: "", + tcell.KeyF44: "", + tcell.KeyF45: "", + tcell.KeyF46: "", + tcell.KeyF47: "", + tcell.KeyF48: "", + tcell.KeyF49: "", + tcell.KeyF50: "", + tcell.KeyF51: "", + tcell.KeyF52: "", + tcell.KeyF53: "", + tcell.KeyF54: "", + tcell.KeyF55: "", + tcell.KeyF56: "", + tcell.KeyF57: "", + tcell.KeyF58: "", + tcell.KeyF59: "", + tcell.KeyF60: "", + tcell.KeyF61: "", + tcell.KeyF62: "", + tcell.KeyF63: "", + tcell.KeyF64: "", + tcell.KeyCtrlA: "", + tcell.KeyCtrlB: "", + tcell.KeyCtrlC: "", + tcell.KeyCtrlD: "", + tcell.KeyCtrlE: "", + tcell.KeyCtrlF: "", + tcell.KeyCtrlG: "", + tcell.KeyCtrlJ: "", + tcell.KeyCtrlK: "", + tcell.KeyCtrlL: "", + tcell.KeyCtrlN: "", + tcell.KeyCtrlO: "", + tcell.KeyCtrlP: "", + tcell.KeyCtrlQ: "", + tcell.KeyCtrlR: "", + tcell.KeyCtrlS: "", + tcell.KeyCtrlT: "", + tcell.KeyCtrlU: "", + tcell.KeyCtrlV: "", + tcell.KeyCtrlW: "", + tcell.KeyCtrlX: "", + tcell.KeyCtrlY: "", + tcell.KeyCtrlZ: "", +} + +var gValKey map[string]tcell.Key + +func init() { + gValKey = make(map[string]tcell.Key, len(gKeyVal)) + for k, v := range gKeyVal { + gValKey[v] = k + } +} + +// for simplicity, assume there will only be one modifier (ctrl, shift or alt) +var reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`) + +func wrapModifier(s string, mod string) string { + s = strings.TrimPrefix(s, "<") + s = strings.TrimSuffix(s, ">") + return fmt.Sprintf("<%s-%s>", mod, s) +} + +func addKeyModifier(s string, mod tcell.ModMask) string { + if reModKey.MatchString(s) { + return s + } + + switch { + case mod&tcell.ModCtrl != 0: + return wrapModifier(s, "c") + case mod&tcell.ModShift != 0: + return wrapModifier(s, "s") + case mod&tcell.ModAlt != 0: + return wrapModifier(s, "a") + default: + return s + } +} + +func readKey(ev *tcell.EventKey) string { + var s string + if ev.Key() == tcell.KeyRune { + switch ev.Str() { + case "<": + s = "" + case ">": + s = "" + case " ": + s = "" + default: + s = ev.Str() + } + } else { + s = gKeyVal[ev.Key()] + } + + return addKeyModifier(s, ev.Modifiers()) +} + +func parseKeyModifier(s string) (tcell.ModMask, string) { + matches := reModKey.FindStringSubmatch(s) + if matches == nil { + return tcell.ModNone, s + } + + mod := tcell.ModNone + switch matches[1] { + case "c": + mod = tcell.ModCtrl + case "s": + mod = tcell.ModShift + case "a": + mod = tcell.ModAlt + } + + s = matches[2] + if len(s) > 1 { + s = "<" + s + ">" + } + + return mod, s +} + +func parseKey(s string) *tcell.EventKey { + if key, ok := gValKey[s]; ok { + return tcell.NewEventKey(key, "", tcell.ModNone) + } + + mod, s := parseKeyModifier(s) + + k := tcell.KeyRune + if key, ok := gValKey[s]; ok { + k = key + s = "" + } else { + switch s { + case "": + s = "<" + case "": + s = ">" + case "": + s = " " + } + } + + return tcell.NewEventKey(k, s, mod) +} diff --git a/key_test.go b/key_test.go new file mode 100644 index 00000000..54bab0d4 --- /dev/null +++ b/key_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "testing" + + "github.com/gdamore/tcell/v3" +) + +var gKeyTests = []struct { + ev *tcell.EventKey + s string +}{ + {tcell.NewEventKey(tcell.KeyRune, "<", tcell.ModNone), ""}, + {tcell.NewEventKey(tcell.KeyRune, ">", tcell.ModNone), ""}, + {tcell.NewEventKey(tcell.KeyRune, " ", tcell.ModNone), ""}, + {tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModNone), "a"}, + {tcell.NewEventKey(tcell.KeyCtrlA, "", tcell.ModNone), ""}, + {tcell.NewEventKey(tcell.KeyRune, "A", tcell.ModNone), "A"}, + {tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModAlt), ""}, + {tcell.NewEventKey(tcell.KeyLeft, "", tcell.ModNone), ""}, + {tcell.NewEventKey(tcell.KeyLeft, "", tcell.ModCtrl), ""}, + {tcell.NewEventKey(tcell.KeyLeft, "", tcell.ModShift), ""}, + {tcell.NewEventKey(tcell.KeyLeft, "", tcell.ModAlt), ""}, + {tcell.NewEventKey(tcell.KeyEsc, "", tcell.ModNone), ""}, + {tcell.NewEventKey(tcell.KeyF1, "", tcell.ModNone), ""}, +} + +func TestReadKey(t *testing.T) { + for _, test := range gKeyTests { + if got := readKey(test.ev); got != test.s { + t.Errorf("at input '%#v' expected '%s' but got '%s'", test.ev, test.s, got) + } + } +} + +func TestParseKey(t *testing.T) { + keyEqual := func(ev1, ev2 *tcell.EventKey) bool { + return ev1.Key() == ev2.Key() && ev1.Modifiers() == ev2.Modifiers() && ev1.Str() == ev2.Str() + } + + for _, test := range gKeyTests { + if got := parseKey(test.s); !keyEqual(got, test.ev) { + t.Errorf("at input '%s' expected '%#v' but got '%#v'", test.s, test.ev, got) + } + } +} diff --git a/lf.1 b/lf.1 index 0baf8629..7a1d850d 100644 --- a/lf.1 +++ b/lf.1 @@ -257,7 +257,7 @@ cmd\-right (default \(aq\(aq and \(aq\(aq) cmd\-home (default \(aq\(aq and \(aq\(aq) cmd\-end (default \(aq\(aq and \(aq\(aq) cmd\-delete (default \(aq\(aq and \(aq\(aq) -cmd\-delete\-back (default \(aq\(aq and \(aq\(aq) +cmd\-delete\-back (default \(aq\(aq) cmd\-delete\-home (default \(aq\(aq) cmd\-delete\-end (default \(aq\(aq) cmd\-delete\-unix\-word (default \(aq\(aq) @@ -267,7 +267,7 @@ cmd\-transpose\-word (default \(aq\(aq) cmd\-word (default \(aq\(aq) cmd\-word\-back (default \(aq\(aq) cmd\-delete\-word (default \(aq\(aq) -cmd\-delete\-word\-back (default \(aq\(aq and \(aq\(aq) +cmd\-delete\-word\-back (default \(aq\(aq) cmd\-capitalize\-word (default \(aq\(aq) cmd\-uppercase\-word (default \(aq\(aq) cmd\-lowercase\-word (default \(aq\(aq) @@ -778,7 +778,7 @@ Move the cursor to the left/right. Move the cursor to the beginning/end of the line. .SS cmd\-delete (default \f[CR]\f[R] and \f[CR]\f[R]) Delete the next character. -.SS cmd\-delete\-back (default \f[CR]\f[R] and \f[CR]\f[R]) +.SS cmd\-delete\-back (default \f[CR]\f[R]) Delete the previous character. When at the beginning of a prompt, returns either to Normal mode or to \f[CR]:\f[R] mode. @@ -794,7 +794,7 @@ Transpose the positions of the last two characters/words. Move the cursor by one word in the forward/backward direction. .SS cmd\-delete\-word (default \f[CR]\f[R]) Delete the next word in the forward direction. -.SS cmd\-delete\-word\-back (default \f[CR]\f[R] and \f[CR]\f[R]) +.SS cmd\-delete\-word\-back (default \f[CR]\f[R]) Delete the previous word in the backward direction. .SS cmd\-capitalize\-word (default \f[CR]\f[R]), cmd\-uppercase\-word (default \f[CR]\f[R]), cmd\-lowercase\-word (default \f[CR]\f[R]) Capitalize/uppercase/lowercase the current word and jump to the next diff --git a/misc.go b/misc.go index 32fcd321..43aa103c 100644 --- a/misc.go +++ b/misc.go @@ -19,7 +19,6 @@ import ( ) var ( - reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`) reRulerSub = regexp.MustCompile(`%[apmcsvfithPd]|%\{[^}]+\}`) reSixelSize = regexp.MustCompile(`"1;1;(\d+);(\d+)`) ) diff --git a/opts.go b/opts.go index d6c8f878..0e1f2e2c 100644 --- a/opts.go +++ b/opts.go @@ -371,42 +371,40 @@ func init() { // Command-line mode bindings can be assigned directly gOpts.cmdkeys = map[string]expr{ - "": &callExpr{"cmd-insert", []string{" "}, 1}, - "": &callExpr{"cmd-escape", nil, 1}, - "": &callExpr{"cmd-complete", nil, 1}, - "": &callExpr{"cmd-enter", nil, 1}, - "": &callExpr{"cmd-enter", nil, 1}, - "": &callExpr{"cmd-history-next", nil, 1}, - "": &callExpr{"cmd-history-next", nil, 1}, - "": &callExpr{"cmd-history-prev", nil, 1}, - "": &callExpr{"cmd-history-prev", nil, 1}, - "": &callExpr{"cmd-delete", nil, 1}, - "": &callExpr{"cmd-delete", nil, 1}, - "": &callExpr{"cmd-delete-back", nil, 1}, - "": &callExpr{"cmd-delete-back", nil, 1}, - "": &callExpr{"cmd-left", nil, 1}, - "": &callExpr{"cmd-left", nil, 1}, - "": &callExpr{"cmd-right", nil, 1}, - "": &callExpr{"cmd-right", nil, 1}, - "": &callExpr{"cmd-home", nil, 1}, - "": &callExpr{"cmd-home", nil, 1}, - "": &callExpr{"cmd-end", nil, 1}, - "": &callExpr{"cmd-end", nil, 1}, - "": &callExpr{"cmd-delete-home", nil, 1}, - "": &callExpr{"cmd-delete-end", nil, 1}, - "": &callExpr{"cmd-delete-unix-word", nil, 1}, - "": &callExpr{"cmd-yank", nil, 1}, - "": &callExpr{"cmd-transpose", nil, 1}, - "": &callExpr{"cmd-interrupt", nil, 1}, - "": &callExpr{"cmd-word", nil, 1}, - "": &callExpr{"cmd-word-back", nil, 1}, - "": &callExpr{"cmd-capitalize-word", nil, 1}, - "": &callExpr{"cmd-delete-word", nil, 1}, - "": &callExpr{"cmd-delete-word-back", nil, 1}, - "": &callExpr{"cmd-delete-word-back", nil, 1}, - "": &callExpr{"cmd-uppercase-word", nil, 1}, - "": &callExpr{"cmd-lowercase-word", nil, 1}, - "": &callExpr{"cmd-transpose-word", nil, 1}, + "": &callExpr{"cmd-insert", []string{" "}, 1}, + "": &callExpr{"cmd-escape", nil, 1}, + "": &callExpr{"cmd-complete", nil, 1}, + "": &callExpr{"cmd-enter", nil, 1}, + "": &callExpr{"cmd-enter", nil, 1}, + "": &callExpr{"cmd-history-next", nil, 1}, + "": &callExpr{"cmd-history-next", nil, 1}, + "": &callExpr{"cmd-history-prev", nil, 1}, + "": &callExpr{"cmd-history-prev", nil, 1}, + "": &callExpr{"cmd-delete", nil, 1}, + "": &callExpr{"cmd-delete", nil, 1}, + "": &callExpr{"cmd-delete-back", nil, 1}, + "": &callExpr{"cmd-left", nil, 1}, + "": &callExpr{"cmd-left", nil, 1}, + "": &callExpr{"cmd-right", nil, 1}, + "": &callExpr{"cmd-right", nil, 1}, + "": &callExpr{"cmd-home", nil, 1}, + "": &callExpr{"cmd-home", nil, 1}, + "": &callExpr{"cmd-end", nil, 1}, + "": &callExpr{"cmd-end", nil, 1}, + "": &callExpr{"cmd-delete-home", nil, 1}, + "": &callExpr{"cmd-delete-end", nil, 1}, + "": &callExpr{"cmd-delete-unix-word", nil, 1}, + "": &callExpr{"cmd-yank", nil, 1}, + "": &callExpr{"cmd-transpose", nil, 1}, + "": &callExpr{"cmd-interrupt", nil, 1}, + "": &callExpr{"cmd-word", nil, 1}, + "": &callExpr{"cmd-word-back", nil, 1}, + "": &callExpr{"cmd-capitalize-word", nil, 1}, + "": &callExpr{"cmd-delete-word", nil, 1}, + "": &callExpr{"cmd-delete-word-back", nil, 1}, + "": &callExpr{"cmd-uppercase-word", nil, 1}, + "": &callExpr{"cmd-lowercase-word", nil, 1}, + "": &callExpr{"cmd-transpose-word", nil, 1}, } gOpts.cmds = make(map[string]expr) diff --git a/sixel.go b/sixel.go index ec04e28b..1b42980d 100644 --- a/sixel.go +++ b/sixel.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) type sixelScreen struct { diff --git a/termseq.go b/termseq.go index bd140170..efb3926a 100644 --- a/termseq.go +++ b/termseq.go @@ -8,7 +8,7 @@ import ( "strings" "unicode/utf8" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) const gEscapeCode byte = '\x1b' @@ -158,8 +158,7 @@ loop: st = st.Reverse(true) case "8": // TODO: tcell PR for proper conceal - _, bg, _ := st.Decompose() - st = st.Foreground(bg) + st = st.Foreground(st.GetBackground()) case "9": st = st.StrikeThrough(true) case "22": diff --git a/termseq_test.go b/termseq_test.go index 7d7dbb5e..14a2ed82 100644 --- a/termseq_test.go +++ b/termseq_test.go @@ -1,9 +1,10 @@ package main import ( + "reflect" "testing" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" ) func TestStripTermSequence(t *testing.T) { @@ -122,7 +123,7 @@ func TestApplyTermSequence(t *testing.T) { } for _, test := range tests { - if got := applyTermSequence(test.s, tcell.StyleDefault); got != test.exp { + if got := applyTermSequence(test.s, tcell.StyleDefault); !reflect.DeepEqual(got, test.exp) { t.Errorf("at input %q expected '%v' but got '%v'", test.s, test.exp, got) } } diff --git a/ui.go b/ui.go index c88c6840..5d307c1c 100644 --- a/ui.go +++ b/ui.go @@ -14,144 +14,13 @@ import ( "text/tabwriter" "text/template" "time" - "unicode" "unicode/utf8" - "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v3" "github.com/mattn/go-runewidth" "golang.org/x/term" ) -var gKeyVal = map[tcell.Key]string{ - tcell.KeyEnter: "", - tcell.KeyBackspace: "", - tcell.KeyTab: "", - tcell.KeyBacktab: "", - tcell.KeyEsc: "", - tcell.KeyBackspace2: "", - tcell.KeyDelete: "", - tcell.KeyInsert: "", - tcell.KeyUp: "", - tcell.KeyDown: "", - tcell.KeyLeft: "", - tcell.KeyRight: "", - tcell.KeyHome: "", - tcell.KeyEnd: "", - tcell.KeyUpLeft: "", - tcell.KeyUpRight: "", - tcell.KeyDownLeft: "", - tcell.KeyDownRight: "", - tcell.KeyCenter: "
", - tcell.KeyPgDn: "", - tcell.KeyPgUp: "", - tcell.KeyClear: "", - tcell.KeyExit: "", - tcell.KeyCancel: "", - tcell.KeyPause: "", - tcell.KeyPrint: "", - tcell.KeyF1: "", - tcell.KeyF2: "", - tcell.KeyF3: "", - tcell.KeyF4: "", - tcell.KeyF5: "", - tcell.KeyF6: "", - tcell.KeyF7: "", - tcell.KeyF8: "", - tcell.KeyF9: "", - tcell.KeyF10: "", - tcell.KeyF11: "", - tcell.KeyF12: "", - tcell.KeyF13: "", - tcell.KeyF14: "", - tcell.KeyF15: "", - tcell.KeyF16: "", - tcell.KeyF17: "", - tcell.KeyF18: "", - tcell.KeyF19: "", - tcell.KeyF20: "", - tcell.KeyF21: "", - tcell.KeyF22: "", - tcell.KeyF23: "", - tcell.KeyF24: "", - tcell.KeyF25: "", - tcell.KeyF26: "", - tcell.KeyF27: "", - tcell.KeyF28: "", - tcell.KeyF29: "", - tcell.KeyF30: "", - tcell.KeyF31: "", - tcell.KeyF32: "", - tcell.KeyF33: "", - tcell.KeyF34: "", - tcell.KeyF35: "", - tcell.KeyF36: "", - tcell.KeyF37: "", - tcell.KeyF38: "", - tcell.KeyF39: "", - tcell.KeyF40: "", - tcell.KeyF41: "", - tcell.KeyF42: "", - tcell.KeyF43: "", - tcell.KeyF44: "", - tcell.KeyF45: "", - tcell.KeyF46: "", - tcell.KeyF47: "", - tcell.KeyF48: "", - tcell.KeyF49: "", - tcell.KeyF50: "", - tcell.KeyF51: "", - tcell.KeyF52: "", - tcell.KeyF53: "", - tcell.KeyF54: "", - tcell.KeyF55: "", - tcell.KeyF56: "", - tcell.KeyF57: "", - tcell.KeyF58: "", - tcell.KeyF59: "", - tcell.KeyF60: "", - tcell.KeyF61: "", - tcell.KeyF62: "", - tcell.KeyF63: "", - tcell.KeyF64: "", - tcell.KeyCtrlA: "", - tcell.KeyCtrlB: "", - tcell.KeyCtrlC: "", - tcell.KeyCtrlD: "", - tcell.KeyCtrlE: "", - tcell.KeyCtrlF: "", - tcell.KeyCtrlG: "", - tcell.KeyCtrlJ: "", - tcell.KeyCtrlK: "", - tcell.KeyCtrlL: "", - tcell.KeyCtrlN: "", - tcell.KeyCtrlO: "", - tcell.KeyCtrlP: "", - tcell.KeyCtrlQ: "", - tcell.KeyCtrlR: "", - tcell.KeyCtrlS: "", - tcell.KeyCtrlT: "", - tcell.KeyCtrlU: "", - tcell.KeyCtrlV: "", - tcell.KeyCtrlW: "", - tcell.KeyCtrlX: "", - tcell.KeyCtrlY: "", - tcell.KeyCtrlZ: "", - tcell.KeyCtrlSpace: "", - tcell.KeyCtrlUnderscore: "", - tcell.KeyCtrlRightSq: "", - tcell.KeyCtrlBackslash: "", - tcell.KeyCtrlCarat: "", -} - -var gValKey map[string]tcell.Key - -func init() { - gValKey = make(map[string]tcell.Key, len(gKeyVal)) - for k, v := range gKeyVal { - gValKey[v] = k - } -} - type win struct { w, h, x, y int } @@ -189,45 +58,41 @@ func printLength(s string) int { } func (win *win) print(screen tcell.Screen, x, y int, st tcell.Style, s string) tcell.Style { - off := x - var comb []rune + buf := make([]rune, 0, len(s)) + off := 0 + put := func() { + if len(buf) > 0 { + screen.PutStrStyled(win.x+x+off, win.y+y, string(buf), st) + off += printLength(string(buf)) + buf = buf[:0] + } + } + + i := 0 slen := len(s) - for i := 0; i < slen; i++ { + for i < slen { seq := readTermSequence(s[i:]) if seq != "" { + put() st = applyTermSequence(seq, st) - i += len(seq) - 1 + i += len(seq) continue } - r, w := utf8.DecodeRuneInString(s[i:]) - for { - rc, wc := utf8.DecodeRuneInString(s[i+w:]) - if !unicode.Is(unicode.Mn, rc) { - break - } - comb = append(comb, rc) - i += wc - } - - if x < win.w { - screen.SetContent(win.x+x, win.y+y, r, comb, st) - comb = nil - } - - i += w - 1 - + r, n := utf8.DecodeRuneInString(s[i:]) if r == '\t' { - ind := gOpts.tabstop - (x-off)%gOpts.tabstop - for i := 0; i < ind && x+i < win.w; i++ { - screen.SetContent(win.x+x+i, win.y+y, ' ', nil, st) + w := gOpts.tabstop - (x+off+printLength(string(buf)))%gOpts.tabstop + for i := 0; i < w; i++ { + buf = append(buf, ' ') } - x += ind } else { - x += runewidth.RuneWidth(r) + buf = append(buf, r) } + + i += n } + put() return st } @@ -636,7 +501,6 @@ type menuSelect struct { type ui struct { screen tcell.Screen sxScreen sixelScreen - polling bool wins []*win promptWin *win msgWin *win @@ -645,8 +509,6 @@ type ui struct { regPrev *reg dirPrev *dir exprChan chan expr - keyChan chan string - tevChan chan tcell.Event evChan chan tcell.Event menu string menuSelect *menuSelect @@ -654,8 +516,8 @@ type ui struct { cmdAccLeft []rune cmdAccRight []rune cmdYankBuf []rune - keyAcc []rune - keyCount []rune + keyAcc string + keyCount string styles styleMap icons iconMap ruler *template.Template @@ -669,14 +531,11 @@ func newUI(screen tcell.Screen) *ui { ui := &ui{ screen: screen, - polling: true, wins: getWins(screen), promptWin: newWin(wtot, 1, 0, 0), msgWin: newWin(wtot, 1, 0, htot-1), menuWin: newWin(wtot, 1, 0, htot-2), exprChan: make(chan expr, 1000), - keyChan: make(chan string, 1000), - tevChan: make(chan tcell.Event, 1000), evChan: make(chan tcell.Event, 1000), styles: parseStyles(), icons: parseIcons(), @@ -685,8 +544,6 @@ func newUI(screen tcell.Screen) *ui { } ui.ruler, ui.rulerErr = parseRuler() - go ui.pollEvents() - return ui } @@ -700,18 +557,6 @@ func (ui *ui) winAt(x, y int) (int, *win) { return -1, nil } -func (ui *ui) pollEvents() { - var ev tcell.Event - for { - ev = ui.screen.PollEvent() - if ev == nil { - ui.polling = false - return - } - ui.tevChan <- ev - } -} - func (ui *ui) renew() { ui.wins = getWins(ui.screen) @@ -921,7 +766,7 @@ func (ui *ui) drawRuler(nav *nav) { tot := len(dir.files) ind := min(dir.ind+1, tot) hid := len(dir.allFiles) - tot - acc := string(ui.keyCount) + string(ui.keyAcc) + acc := ui.keyCount + ui.keyAcc var percentage string beg := max(dir.ind-dir.pos, 0) @@ -1117,7 +962,7 @@ func (ui *ui) drawRulerFile(nav *nav) { data := rulerData{ SPACER: "\x1f", Message: ui.msg, - Keys: string(ui.keyCount) + string(ui.keyAcc), + Keys: ui.keyCount + ui.keyAcc, Progress: progress, Copy: copiedPaths, Cut: cutPaths, @@ -1152,35 +997,35 @@ func (ui *ui) drawBox() { w, h := ui.screen.Size() for i := 1; i < w-1; i++ { - ui.screen.SetContent(i, 1, tcell.RuneHLine, nil, st) - ui.screen.SetContent(i, h-2, tcell.RuneHLine, nil, st) + ui.screen.PutStrStyled(i, 1, string(tcell.RuneHLine), st) + ui.screen.PutStrStyled(i, h-2, string(tcell.RuneHLine), st) } for i := 2; i < h-2; i++ { - ui.screen.SetContent(0, i, tcell.RuneVLine, nil, st) - ui.screen.SetContent(w-1, i, tcell.RuneVLine, nil, st) + ui.screen.PutStrStyled(0, i, string(tcell.RuneVLine), st) + ui.screen.PutStrStyled(w-1, i, string(tcell.RuneVLine), st) } if gOpts.roundbox { - ui.screen.SetContent(0, 1, '╭', nil, st) - ui.screen.SetContent(w-1, 1, '╮', nil, st) - ui.screen.SetContent(0, h-2, '╰', nil, st) - ui.screen.SetContent(w-1, h-2, '╯', nil, st) + ui.screen.PutStrStyled(0, 1, "╭", st) + ui.screen.PutStrStyled(w-1, 1, "╮", st) + ui.screen.PutStrStyled(0, h-2, "╰", st) + ui.screen.PutStrStyled(w-1, h-2, "╯", st) } else { - ui.screen.SetContent(0, 1, tcell.RuneULCorner, nil, st) - ui.screen.SetContent(w-1, 1, tcell.RuneURCorner, nil, st) - ui.screen.SetContent(0, h-2, tcell.RuneLLCorner, nil, st) - ui.screen.SetContent(w-1, h-2, tcell.RuneLRCorner, nil, st) + ui.screen.PutStrStyled(0, 1, string(tcell.RuneULCorner), st) + ui.screen.PutStrStyled(w-1, 1, string(tcell.RuneURCorner), st) + ui.screen.PutStrStyled(0, h-2, string(tcell.RuneLLCorner), st) + ui.screen.PutStrStyled(w-1, h-2, string(tcell.RuneLRCorner), st) } wacc := 0 for wind := range len(ui.wins) - 1 { wacc += ui.wins[wind].w + 1 - ui.screen.SetContent(wacc, 1, tcell.RuneTTee, nil, st) + ui.screen.PutStrStyled(wacc, 1, string(tcell.RuneTTee), st) for i := 2; i < h-2; i++ { - ui.screen.SetContent(wacc, i, tcell.RuneVLine, nil, st) + ui.screen.PutStrStyled(wacc, i, string(tcell.RuneVLine), st) } - ui.screen.SetContent(wacc, h-2, tcell.RuneBTee, nil, st) + ui.screen.PutStrStyled(wacc, h-2, string(tcell.RuneBTee), st) } } @@ -1478,71 +1323,6 @@ func listFilesInCurrDir(nav *nav) string { return b.String() } -func (ui *ui) pollEvent() tcell.Event { - select { - case val := <-ui.keyChan: - var ch rune - var mod tcell.ModMask - k := tcell.KeyRune - - if key, ok := gValKey[val]; ok { - return tcell.NewEventKey(key, ch, mod) - } - - switch { - case utf8.RuneCountInString(val) == 1: - ch, _ = utf8.DecodeRuneInString(val) - case val == "": - ch = '<' - case val == "": - ch = '>' - case val == "": - ch = ' ' - case reModKey.MatchString(val): - matches := reModKey.FindStringSubmatch(val) - switch matches[1] { - case "c": - mod = tcell.ModCtrl - case "s": - mod = tcell.ModShift - case "a": - mod = tcell.ModAlt - } - val = matches[2] - if utf8.RuneCountInString(val) == 1 { - ch, _ = utf8.DecodeRuneInString(val) - break - } else if key, ok := gValKey["<"+val+">"]; ok { - k = key - break - } - fallthrough - default: - k = tcell.KeyESC - ui.echoerrf("unknown key: %s", val) - } - - return tcell.NewEventKey(k, ch, mod) - case ev := <-ui.tevChan: - return ev - } -} - -func addSpecialKeyModifier(val string, mod tcell.ModMask) string { - switch { - case !strings.HasPrefix(val, "<"): - return val - case mod == tcell.ModCtrl && !strings.HasPrefix(val, "")...) - case tev.Rune() == '>': - ui.keyAcc = append(ui.keyAcc, []rune("")...) - case tev.Rune() == ' ': - ui.keyAcc = append(ui.keyAcc, []rune("")...) - case tev.Modifiers() == tcell.ModAlt: - ui.keyAcc = append(ui.keyAcc, '<', 'a', '-', tev.Rune(), '>') - case unicode.IsDigit(tev.Rune()) && len(ui.keyAcc) == 0: - ui.keyCount = append(ui.keyCount, tev.Rune()) - default: - ui.keyAcc = append(ui.keyAcc, tev.Rune()) - } - } else { - val := gKeyVal[tev.Key()] - val = addSpecialKeyModifier(val, tev.Modifiers()) - if val == "" && len(ui.keyAcc) != 0 { - ui.keyAcc = nil - ui.keyCount = nil - ui.menu = "" - return draw + isDigitKey := func(*tcell.EventKey) bool { + if tev.Key() != tcell.KeyRune || tev.Modifiers() != tcell.ModNone { + return false } - ui.keyAcc = append(ui.keyAcc, []rune(val)...) + + s := tev.Str() + return len(s) == 1 && s[0] >= '0' && s[0] <= '9' } - if len(ui.keyAcc) == 0 { + switch { + case tev.Key() == tcell.KeyEsc && ui.keyAcc != "": + ui.keyAcc = "" + ui.keyCount = "" + ui.menu = "" + return draw + case isDigitKey(tev) && ui.keyAcc == "": + ui.keyCount += tev.Str() return draw + default: + ui.keyAcc += readKey(tev) } - binds, ok := findBinds(keys, string(ui.keyAcc)) + binds, ok := findBinds(keys, ui.keyAcc) switch len(binds) { case 0: - ui.echoerrf("unknown mapping: %s", string(ui.keyAcc)) - ui.keyAcc = nil - ui.keyCount = nil + ui.echoerrf("unknown mapping: %s", ui.keyAcc) + ui.keyAcc = "" + ui.keyCount = "" ui.menu = "" return draw default: if ok { - if len(ui.keyCount) > 0 { - c, err := strconv.Atoi(string(ui.keyCount)) + if ui.keyCount != "" { + c, err := strconv.Atoi(ui.keyCount) if err != nil { log.Printf("converting command count: %s", err) } count = c } - expr := keys[string(ui.keyAcc)] + expr := keys[ui.keyAcc] if count != 0 { switch e := expr.(type) { @@ -1622,14 +1392,14 @@ func (ui *ui) readNormalEvent(ev tcell.Event, nav *nav) expr { } } - ui.keyAcc = nil - ui.keyCount = nil + ui.keyAcc = "" + ui.keyCount = "" ui.menu = "" return expr } if gOpts.showbinds { // mode and already typed keys are obvious here; no need to clutter the menu - ui.menu = listMatchingBinds(binds, string(ui.keyAcc)) + ui.menu = listMatchingBinds(binds, ui.keyAcc) } return draw } @@ -1676,8 +1446,8 @@ func (ui *ui) readNormalEvent(ev tcell.Event, nav *nav) expr { } if button != "" && button != "" { ui.echoerrf("unknown mapping: %s", button) - ui.keyAcc = nil - ui.keyCount = nil + ui.keyAcc = "" + ui.keyCount = "" ui.menu = "" return draw } @@ -1751,21 +1521,12 @@ func (ui *ui) readNormalEvent(ev tcell.Event, nav *nav) expr { func readCmdEvent(ev tcell.Event) expr { if tev, ok := ev.(*tcell.EventKey); ok { - if tev.Key() == tcell.KeyRune { - if tev.Modifiers() == tcell.ModAlt { - val := string([]rune{'<', 'a', '-', tev.Rune(), '>'}) - if expr, ok := gOpts.cmdkeys[val]; ok { - return expr - } - } else { - return &callExpr{"cmd-insert", []string{string(tev.Rune())}, 1} - } - } else { - val := gKeyVal[tev.Key()] - val = addSpecialKeyModifier(val, tev.Modifiers()) - if expr, ok := gOpts.cmdkeys[val]; ok { - return expr - } + if tev.Key() == tcell.KeyRune && tev.Modifiers()&tcell.ModAlt == 0 { + return &callExpr{"cmd-insert", []string{tev.Str()}, 1} + } + + if expr, ok := gOpts.cmdkeys[readKey(tev)]; ok { + return expr } } return nil @@ -1783,12 +1544,10 @@ func (ui *ui) readEvent(ev tcell.Event, nav *nav) expr { return ui.readNormalEvent(ev, nav) } -func (ui *ui) readExpr() { - go func() { - for { - ui.evChan <- ui.pollEvent() - } - }() +func (ui *ui) readEvents() { + for ev := range ui.screen.EventQ() { + ui.evChan <- ev + } } func (ui *ui) suspend() error { @@ -1797,12 +1556,7 @@ func (ui *ui) suspend() error { } func (ui *ui) resume() error { - err := ui.screen.Resume() - if !ui.polling { - go ui.pollEvents() - ui.polling = true - } - return err + return ui.screen.Resume() } func (ui *ui) exportSizes() {