term: replace go-libvterm with tcell-term
Replace go-libvterm package with tcell-term. go-libvterm provides the embedded terminal for aerc. It uses a statically linked C library, requiring CGO. tcell-term is written in pure go and is written to be portable with tcell applications by implementing the tcell Widget interface. This allows the terminal to take a view (which aerc already supplies) and draw directly to it, as well as issue tcell Events to a Watcher. Enable setting cursor shapes in embedded terminals. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Signed-off-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
17c4781911
commit
518f3e962c
5 changed files with 84 additions and 383 deletions
|
@ -11,12 +11,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Zoxide support with `:z`.
|
- Zoxide support with `:z`.
|
||||||
- Hide local timezone with `send-as-utc = true` in `accounts.conf`.
|
- Hide local timezone with `send-as-utc = true` in `accounts.conf`.
|
||||||
- Persistent command history in `~/.cache/aerc/history`.
|
- Persistent command history in `~/.cache/aerc/history`.
|
||||||
|
- Cursor shape support in embedded terminals.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `:open-link` now supports link types other than HTTP(S)
|
- `:open-link` now supports link types other than HTTP(S)
|
||||||
- Running the same command multiple times only adds one entry to the command
|
- Running the same command multiple times only adds one entry to the command
|
||||||
history.
|
history.
|
||||||
|
- Embedded terminal backend (libvterm was replaced by a pure go implementation).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -3,12 +3,12 @@ module git.sr.ht/~rjarry/aerc
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.sr.ht/~rockorager/tcell-term v0.1.0
|
||||||
git.sr.ht/~sircmpwn/getopt v1.0.0
|
git.sr.ht/~sircmpwn/getopt v1.0.0
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab
|
github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab
|
||||||
github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182
|
github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182
|
||||||
github.com/creack/pty v1.1.18
|
github.com/creack/pty v1.1.18 // indirect
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964
|
||||||
github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810
|
|
||||||
github.com/emersion/go-imap v1.2.0
|
github.com/emersion/go-imap v1.2.0
|
||||||
github.com/emersion/go-imap-sortthread v1.2.0
|
github.com/emersion/go-imap-sortthread v1.2.0
|
||||||
github.com/emersion/go-maildir v0.2.0
|
github.com/emersion/go-maildir v0.2.0
|
||||||
|
@ -28,7 +28,6 @@ require (
|
||||||
github.com/kyoh86/xdg v1.2.0
|
github.com/kyoh86/xdg v1.2.0
|
||||||
github.com/lithammer/fuzzysearch v1.1.3
|
github.com/lithammer/fuzzysearch v1.1.3
|
||||||
github.com/mattn/go-isatty v0.0.16
|
github.com/mattn/go-isatty v0.0.16
|
||||||
github.com/mattn/go-pointer v0.0.1 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.13
|
github.com/mattn/go-runewidth v0.0.13
|
||||||
github.com/miolini/datacounter v1.0.2
|
github.com/miolini/datacounter v1.0.2
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
@ -39,9 +38,6 @@ require (
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||||
github.com/zenhack/go.notmuch v0.0.0-20211022191430-4d57e8ad2a8b
|
github.com/zenhack/go.notmuch v0.0.0-20211022191430-4d57e8ad2a8b
|
||||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||||
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect
|
|
||||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200420072808-71bec3603bf3
|
replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200420072808-71bec3603bf3
|
||||||
|
|
9
go.sum
9
go.sum
|
@ -62,6 +62,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
|
contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
git.sr.ht/~rockorager/tcell-term v0.1.0 h1:5tRUEW6y5O5xFr4SvVL+hBO56fllgbYBr7AYGFe6O6I=
|
||||||
|
git.sr.ht/~rockorager/tcell-term v0.1.0/go.mod h1:0CEvSJrV0ItwHlPHSmkxd5egiuUtpvGnnCsqzV02BN4=
|
||||||
git.sr.ht/~sircmpwn/getopt v1.0.0 h1:/pRHjO6/OCbBF4puqD98n6xtPEgE//oq5U8NXjP7ROc=
|
git.sr.ht/~sircmpwn/getopt v1.0.0 h1:/pRHjO6/OCbBF4puqD98n6xtPEgE//oq5U8NXjP7ROc=
|
||||||
git.sr.ht/~sircmpwn/getopt v1.0.0/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
|
git.sr.ht/~sircmpwn/getopt v1.0.0/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
|
||||||
github.com/Antonboom/errname v0.1.7 h1:mBBDKvEYwPl4WFFNwec1CZO096G6vzK9vvDQzAwkako=
|
github.com/Antonboom/errname v0.1.7 h1:mBBDKvEYwPl4WFFNwec1CZO096G6vzK9vvDQzAwkako=
|
||||||
|
@ -186,6 +188,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/cristalhq/acmd v0.7.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
|
github.com/cristalhq/acmd v0.7.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
|
||||||
|
@ -199,8 +202,6 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810 h1:VlHKuIrEvuGlED53TkovT4AVUjrqTyeCt3wiqw1OsFc=
|
|
||||||
github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810/go.mod h1:Ow1oE1Hr4xE7eWY2/Ih2kbcOyyXDH7G0XKv/I4yiCYs=
|
|
||||||
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
|
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
|
||||||
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
|
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
@ -648,9 +649,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
|
|
||||||
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
|
|
||||||
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
@ -1037,7 +1035,6 @@ golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl
|
||||||
golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d h1:+W8Qf4iJtMGKkyAygcKohjxTk4JPsL9DpzApJ22m5Ic=
|
golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d h1:+W8Qf4iJtMGKkyAygcKohjxTk4JPsL9DpzApJ22m5Ic=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
|
|
@ -758,6 +758,10 @@ func (pv *PartViewer) copyFilterOutToPager() {
|
||||||
logging.Warnf("failed to wait for the filter process: %v", err)
|
logging.Warnf("failed to wait for the filter process: %v", err)
|
||||||
}
|
}
|
||||||
pv.pagerin.Close()
|
pv.pagerin.Close()
|
||||||
|
// If the pager command doesn't keep the terminal running, we
|
||||||
|
// risk not drawing the screen until user input unless we
|
||||||
|
// invalidate after writing
|
||||||
|
pv.Invalidate()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,113 +1,27 @@
|
||||||
package widgets
|
package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.sr.ht/~rjarry/aerc/lib/ui"
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
||||||
"git.sr.ht/~rjarry/aerc/logging"
|
"git.sr.ht/~rjarry/aerc/logging"
|
||||||
|
tcellterm "git.sr.ht/~rockorager/tcell-term"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
vterm "github.com/ddevault/go-libvterm"
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/gdamore/tcell/v2/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
type vtermKey struct {
|
|
||||||
Key vterm.Key
|
|
||||||
Rune rune
|
|
||||||
Mod vterm.Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyMap map[tcell.Key]vtermKey
|
|
||||||
|
|
||||||
func directKey(key vterm.Key) vtermKey {
|
|
||||||
return vtermKey{key, 0, vterm.ModNone}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runeMod(r rune, mod vterm.Modifier) vtermKey {
|
|
||||||
return vtermKey{vterm.KeyNone, r, mod}
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyMod(key vterm.Key, mod vterm.Modifier) vtermKey {
|
|
||||||
return vtermKey{key, 0, mod}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
keyMap = make(map[tcell.Key]vtermKey)
|
|
||||||
keyMap[tcell.KeyCtrlSpace] = runeMod(' ', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlA] = runeMod('a', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlB] = runeMod('b', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlC] = runeMod('c', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlD] = runeMod('d', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlE] = runeMod('e', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlF] = runeMod('f', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlG] = runeMod('g', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlH] = runeMod('h', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlI] = runeMod('i', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlJ] = runeMod('j', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlK] = runeMod('k', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlL] = runeMod('l', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlM] = runeMod('m', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlN] = runeMod('n', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlO] = runeMod('o', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlP] = runeMod('p', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlQ] = runeMod('q', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlR] = runeMod('r', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlS] = runeMod('s', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlT] = runeMod('t', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlU] = runeMod('u', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlV] = runeMod('v', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlW] = runeMod('w', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlX] = runeMod('x', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlY] = runeMod('y', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlZ] = runeMod('z', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlBackslash] = runeMod('\\', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlCarat] = runeMod('^', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyCtrlUnderscore] = runeMod('_', vterm.ModCtrl)
|
|
||||||
keyMap[tcell.KeyEnter] = directKey(vterm.KeyEnter)
|
|
||||||
keyMap[tcell.KeyTab] = directKey(vterm.KeyTab)
|
|
||||||
keyMap[tcell.KeyBacktab] = keyMod(vterm.KeyTab, vterm.ModShift)
|
|
||||||
keyMap[tcell.KeyBackspace] = directKey(vterm.KeyBackspace)
|
|
||||||
keyMap[tcell.KeyEscape] = directKey(vterm.KeyEscape)
|
|
||||||
keyMap[tcell.KeyUp] = directKey(vterm.KeyUp)
|
|
||||||
keyMap[tcell.KeyDown] = directKey(vterm.KeyDown)
|
|
||||||
keyMap[tcell.KeyLeft] = directKey(vterm.KeyLeft)
|
|
||||||
keyMap[tcell.KeyRight] = directKey(vterm.KeyRight)
|
|
||||||
keyMap[tcell.KeyInsert] = directKey(vterm.KeyIns)
|
|
||||||
keyMap[tcell.KeyDelete] = directKey(vterm.KeyDel)
|
|
||||||
keyMap[tcell.KeyHome] = directKey(vterm.KeyHome)
|
|
||||||
keyMap[tcell.KeyEnd] = directKey(vterm.KeyEnd)
|
|
||||||
keyMap[tcell.KeyPgUp] = directKey(vterm.KeyPageUp)
|
|
||||||
keyMap[tcell.KeyPgDn] = directKey(vterm.KeyPageDown)
|
|
||||||
for i := 0; i < 64; i++ {
|
|
||||||
keyMap[tcell.Key(int(tcell.KeyF1)+i)] = directKey(vterm.Key(int(vterm.KeyFunction0) + i + 1))
|
|
||||||
}
|
|
||||||
keyMap[tcell.KeyTAB] = directKey(vterm.KeyTab)
|
|
||||||
keyMap[tcell.KeyESC] = directKey(vterm.KeyEscape)
|
|
||||||
keyMap[tcell.KeyDEL] = directKey(vterm.KeyBackspace)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
closed bool
|
closed bool
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
ctx *ui.Context
|
ctx *ui.Context
|
||||||
cursorPos vterm.Pos
|
|
||||||
cursorShown bool
|
cursorShown bool
|
||||||
destroyed bool
|
destroyed bool
|
||||||
err error
|
|
||||||
focus bool
|
focus bool
|
||||||
pty *os.File
|
vterm *tcellterm.Terminal
|
||||||
start chan interface{}
|
running bool
|
||||||
vterm *vterm.VTerm
|
|
||||||
|
|
||||||
damage []vterm.Rect // protected by damageMutex
|
|
||||||
damageMutex sync.Mutex
|
|
||||||
writeMutex sync.Mutex
|
|
||||||
readMutex sync.Mutex
|
|
||||||
closeMutex sync.Mutex
|
|
||||||
|
|
||||||
OnClose func(err error)
|
OnClose func(err error)
|
||||||
OnEvent func(event tcell.Event) bool
|
OnEvent func(event tcell.Event) bool
|
||||||
|
@ -120,115 +34,56 @@ func NewTerminal(cmd *exec.Cmd) (*Terminal, error) {
|
||||||
cursorShown: true,
|
cursorShown: true,
|
||||||
}
|
}
|
||||||
term.cmd = cmd
|
term.cmd = cmd
|
||||||
term.vterm = vterm.New(24, 80)
|
term.vterm = tcellterm.New()
|
||||||
term.vterm.SetUTF8(true)
|
|
||||||
term.start = make(chan interface{})
|
|
||||||
screen := term.vterm.ObtainScreen()
|
|
||||||
go func() {
|
|
||||||
defer logging.PanicHandler()
|
|
||||||
|
|
||||||
<-term.start
|
|
||||||
buf := make([]byte, 4096)
|
|
||||||
for {
|
|
||||||
n, err := term.pty.Read(buf)
|
|
||||||
if err != nil || term.closed {
|
|
||||||
// These are generally benine errors when the process exits
|
|
||||||
term.Close(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
term.writeMutex.Lock()
|
|
||||||
_, err = term.vterm.Write(buf[:n])
|
|
||||||
term.writeMutex.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
term.Close(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
screen.Flush()
|
|
||||||
term.flushTerminal()
|
|
||||||
term.invalidate()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
screen.OnDamage = term.onDamage
|
|
||||||
screen.OnMoveCursor = term.onMoveCursor
|
|
||||||
screen.OnSetTermProp = term.onSetTermProp
|
|
||||||
screen.EnableAltScreen(true)
|
|
||||||
screen.Reset(true)
|
|
||||||
return term, nil
|
return term, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (term *Terminal) flushTerminal() {
|
|
||||||
buf := make([]byte, 4096)
|
|
||||||
for {
|
|
||||||
term.readMutex.Lock()
|
|
||||||
n, err := term.vterm.Read(buf)
|
|
||||||
term.readMutex.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
term.Close(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
_, err = term.pty.Write(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
term.Close(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (term *Terminal) Close(err error) {
|
func (term *Terminal) Close(err error) {
|
||||||
term.closeMutex.Lock()
|
|
||||||
defer term.closeMutex.Unlock()
|
|
||||||
|
|
||||||
if term.closed {
|
if term.closed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
term.err = err
|
// Stop receiving events
|
||||||
if term.pty != nil {
|
term.vterm.Unwatch(term)
|
||||||
term.pty.Close()
|
|
||||||
term.pty = nil
|
|
||||||
}
|
|
||||||
if term.cmd != nil && term.cmd.Process != nil {
|
if term.cmd != nil && term.cmd.Process != nil {
|
||||||
err := term.cmd.Process.Kill()
|
err := term.cmd.Process.Kill()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Warnf("failed to kill process: %v", err)
|
logging.Warnf("failed to kill process: %v", err)
|
||||||
}
|
}
|
||||||
err = term.cmd.Wait()
|
// Race condition here, check if cmd exists. If process exits
|
||||||
|
// fast, this could by nil and panic
|
||||||
|
if term.cmd != nil {
|
||||||
|
err = term.cmd.Wait()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Warnf("failed for wait for process to terminate: %v", err)
|
logging.Warnf("failed for wait for process to terminate: %v", err)
|
||||||
}
|
}
|
||||||
term.cmd = nil
|
term.cmd = nil
|
||||||
}
|
}
|
||||||
|
if term.vterm != nil {
|
||||||
|
term.vterm.Close()
|
||||||
|
}
|
||||||
if !term.closed && term.OnClose != nil {
|
if !term.closed && term.OnClose != nil {
|
||||||
term.OnClose(err)
|
term.OnClose(err)
|
||||||
}
|
}
|
||||||
term.closed = true
|
|
||||||
term.ctx.HideCursor()
|
term.ctx.HideCursor()
|
||||||
|
term.closed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (term *Terminal) Destroy() {
|
func (term *Terminal) Destroy() {
|
||||||
if term.destroyed {
|
if term.destroyed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if term.vterm != nil {
|
|
||||||
term.vterm.Close()
|
|
||||||
term.vterm = nil
|
|
||||||
}
|
|
||||||
if term.ctx != nil {
|
if term.ctx != nil {
|
||||||
term.ctx.HideCursor()
|
term.ctx.HideCursor()
|
||||||
}
|
}
|
||||||
|
// If we destroy, we don't want to call the OnClose callback
|
||||||
|
term.OnClose = nil
|
||||||
|
term.Close(nil)
|
||||||
|
term.vterm = nil
|
||||||
term.destroyed = true
|
term.destroyed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (term *Terminal) Invalidate() {
|
func (term *Terminal) Invalidate() {
|
||||||
if term.vterm != nil {
|
|
||||||
width, height := term.vterm.Size()
|
|
||||||
rect := vterm.NewRect(0, width, 0, height)
|
|
||||||
term.damageMutex.Lock()
|
|
||||||
term.damage = append(term.damage, *rect)
|
|
||||||
term.damageMutex.Unlock()
|
|
||||||
}
|
|
||||||
term.invalidate()
|
term.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,104 +95,44 @@ func (term *Terminal) Draw(ctx *ui.Context) {
|
||||||
if term.destroyed {
|
if term.destroyed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
term.ctx = ctx // gross
|
term.ctx = ctx // gross
|
||||||
|
term.vterm.SetView(ctx.View())
|
||||||
if !term.closed {
|
if !term.running && !term.closed && term.cmd != nil {
|
||||||
winsize := pty.Winsize{
|
go func() {
|
||||||
Cols: uint16(ctx.Width()),
|
defer logging.PanicHandler()
|
||||||
Rows: uint16(ctx.Height()),
|
term.vterm.Watch(term)
|
||||||
}
|
attr := &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 1}
|
||||||
if winsize.Cols == 0 || winsize.Rows == 0 || term.cmd == nil {
|
if err := term.vterm.RunWithAttrs(term.cmd, attr); err != nil {
|
||||||
return
|
logging.Errorf("error running terminal: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
if term.pty == nil {
|
|
||||||
term.vterm.SetSize(ctx.Height(), ctx.Width())
|
|
||||||
|
|
||||||
term.closeMutex.Lock()
|
|
||||||
if term.cmd == nil {
|
|
||||||
term.closeMutex.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tty, err := pty.StartWithAttrs(term.cmd, &winsize, &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 1})
|
|
||||||
term.closeMutex.Unlock()
|
|
||||||
|
|
||||||
term.pty = tty
|
|
||||||
if err != nil {
|
|
||||||
term.Close(err)
|
term.Close(err)
|
||||||
|
term.running = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
term.start <- nil
|
term.running = false
|
||||||
if term.OnStart != nil {
|
term.Close(nil)
|
||||||
term.OnStart()
|
}()
|
||||||
|
for {
|
||||||
|
if term.cmd.Process != nil {
|
||||||
|
term.running = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if term.OnStart != nil {
|
||||||
ws, err := pty.GetsizeFull(term.pty)
|
term.OnStart()
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rows := int(ws.Rows)
|
|
||||||
cols := int(ws.Cols)
|
|
||||||
|
|
||||||
if ctx.Width() != cols || ctx.Height() != rows {
|
|
||||||
term.writeMutex.Lock()
|
|
||||||
err := pty.Setsize(term.pty, &winsize)
|
|
||||||
if err != nil {
|
|
||||||
logging.Warnf("failed to set terminal size: %v", err)
|
|
||||||
}
|
|
||||||
term.vterm.SetSize(ctx.Height(), ctx.Width())
|
|
||||||
term.writeMutex.Unlock()
|
|
||||||
rect := vterm.NewRect(0, ctx.Width(), 0, ctx.Height())
|
|
||||||
term.damageMutex.Lock()
|
|
||||||
term.damage = append(term.damage, *rect)
|
|
||||||
term.damageMutex.Unlock()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
term.draw()
|
||||||
|
}
|
||||||
|
|
||||||
screen := term.vterm.ObtainScreen()
|
func (term *Terminal) draw() {
|
||||||
|
term.vterm.Draw()
|
||||||
type coords struct {
|
|
||||||
x int
|
|
||||||
y int
|
|
||||||
}
|
|
||||||
|
|
||||||
// naive optimization
|
|
||||||
visited := make(map[coords]interface{})
|
|
||||||
|
|
||||||
term.damageMutex.Lock()
|
|
||||||
for _, rect := range term.damage {
|
|
||||||
for x := rect.StartCol(); x < rect.EndCol() && x < ctx.Width(); x += 1 {
|
|
||||||
for y := rect.StartRow(); y < rect.EndRow() && y < ctx.Height(); y += 1 {
|
|
||||||
|
|
||||||
coords := coords{x, y}
|
|
||||||
if _, ok := visited[coords]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
visited[coords] = nil
|
|
||||||
|
|
||||||
cell, err := screen.GetCellAt(y, x)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
style := term.styleFromCell(cell)
|
|
||||||
ctx.Printf(x, y, style, "%s", string(cell.Chars()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
term.damage = nil
|
|
||||||
term.damageMutex.Unlock()
|
|
||||||
|
|
||||||
if term.focus && !term.closed {
|
if term.focus && !term.closed {
|
||||||
if !term.cursorShown {
|
if !term.cursorShown {
|
||||||
ctx.HideCursor()
|
term.ctx.HideCursor()
|
||||||
} else {
|
} else {
|
||||||
state := term.vterm.ObtainState()
|
_, x, y, style := term.vterm.GetCursor()
|
||||||
row, col := state.GetCursorPos()
|
term.ctx.SetCursor(x, y)
|
||||||
ctx.SetCursor(col, row)
|
term.ctx.SetCursorStyle(style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -364,29 +159,38 @@ func (term *Terminal) Focus(focus bool) {
|
||||||
if !term.focus {
|
if !term.focus {
|
||||||
term.ctx.HideCursor()
|
term.ctx.HideCursor()
|
||||||
} else {
|
} else {
|
||||||
state := term.vterm.ObtainState()
|
_, x, y, style := term.vterm.GetCursor()
|
||||||
row, col := state.GetCursorPos()
|
term.ctx.SetCursor(x, y)
|
||||||
term.ctx.SetCursor(col, row)
|
term.ctx.SetCursorStyle(style)
|
||||||
term.Invalidate()
|
term.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertMods(mods tcell.ModMask) vterm.Modifier {
|
// HandleEvent is used to watch the underlying terminal events
|
||||||
var (
|
func (term *Terminal) HandleEvent(ev tcell.Event) bool {
|
||||||
ret uint = 0
|
if term.closed || term.destroyed {
|
||||||
mask uint = uint(mods)
|
return false
|
||||||
)
|
|
||||||
if mask&uint(tcell.ModShift) > 0 {
|
|
||||||
ret |= uint(vterm.ModShift)
|
|
||||||
}
|
}
|
||||||
if mask&uint(tcell.ModCtrl) > 0 {
|
switch ev := ev.(type) {
|
||||||
ret |= uint(vterm.ModCtrl)
|
case *views.EventWidgetContent:
|
||||||
|
// Draw here for performance improvement. We call draw again in
|
||||||
|
// the main Draw, but tcell-term only draws dirty cells, so it
|
||||||
|
// won't be too much extra CPU there. Drawing there is needed
|
||||||
|
// for certain msgviews, particularly if the pager command
|
||||||
|
// exits.
|
||||||
|
term.draw()
|
||||||
|
// Perform a tcell screen.Show() to show our updates
|
||||||
|
// immediately
|
||||||
|
term.ctx.Show()
|
||||||
|
term.invalidate()
|
||||||
|
return true
|
||||||
|
case *tcellterm.EventTitle:
|
||||||
|
if term.OnTitle != nil {
|
||||||
|
term.OnTitle(ev.Title())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if mask&uint(tcell.ModAlt) > 0 {
|
return false
|
||||||
ret |= uint(vterm.ModAlt)
|
|
||||||
}
|
|
||||||
return vterm.Modifier(ret)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (term *Terminal) Event(event tcell.Event) bool {
|
func (term *Terminal) Event(event tcell.Event) bool {
|
||||||
|
@ -398,107 +202,5 @@ func (term *Terminal) Event(event tcell.Event) bool {
|
||||||
if term.closed {
|
if term.closed {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if event, ok := event.(*tcell.EventKey); ok {
|
return term.vterm.HandleEvent(event)
|
||||||
if event.Key() == tcell.KeyRune {
|
|
||||||
term.vterm.KeyboardUnichar(
|
|
||||||
event.Rune(), convertMods(event.Modifiers()))
|
|
||||||
} else if key, ok := keyMap[event.Key()]; ok {
|
|
||||||
switch {
|
|
||||||
case key.Key == vterm.KeyNone:
|
|
||||||
term.vterm.KeyboardUnichar(
|
|
||||||
key.Rune, key.Mod)
|
|
||||||
case key.Mod == vterm.ModNone:
|
|
||||||
term.vterm.KeyboardKey(key.Key,
|
|
||||||
convertMods(event.Modifiers()))
|
|
||||||
default:
|
|
||||||
term.vterm.KeyboardKey(key.Key, key.Mod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
term.flushTerminal()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (term *Terminal) styleFromCell(cell *vterm.ScreenCell) tcell.Style {
|
|
||||||
style := tcell.StyleDefault
|
|
||||||
|
|
||||||
background := cell.Bg()
|
|
||||||
foreground := cell.Fg()
|
|
||||||
|
|
||||||
var (
|
|
||||||
bg tcell.Color
|
|
||||||
fg tcell.Color
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
case background.IsDefaultBg():
|
|
||||||
bg = tcell.ColorDefault
|
|
||||||
case background.IsIndexed():
|
|
||||||
bg = tcell.Color(tcell.PaletteColor(int(background.GetIndex())))
|
|
||||||
case background.IsRgb():
|
|
||||||
r, g, b := background.GetRGB()
|
|
||||||
bg = tcell.NewRGBColor(int32(r), int32(g), int32(b))
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case foreground.IsDefaultFg():
|
|
||||||
fg = tcell.ColorDefault
|
|
||||||
case foreground.IsIndexed():
|
|
||||||
fg = tcell.Color(tcell.PaletteColor(int(foreground.GetIndex())))
|
|
||||||
case foreground.IsRgb():
|
|
||||||
r, g, b := foreground.GetRGB()
|
|
||||||
fg = tcell.NewRGBColor(int32(r), int32(g), int32(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
style = style.Background(bg).Foreground(fg)
|
|
||||||
attrs := cell.Attrs()
|
|
||||||
|
|
||||||
if attrs.Bold != 0 {
|
|
||||||
style = style.Bold(true)
|
|
||||||
}
|
|
||||||
if attrs.Italic != 0 {
|
|
||||||
style = style.Italic(true)
|
|
||||||
}
|
|
||||||
if attrs.Underline != 0 {
|
|
||||||
style = style.Underline(true)
|
|
||||||
}
|
|
||||||
if attrs.Blink != 0 {
|
|
||||||
style = style.Blink(true)
|
|
||||||
}
|
|
||||||
if attrs.Reverse != 0 {
|
|
||||||
style = style.Reverse(true)
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
func (term *Terminal) onDamage(rect *vterm.Rect) int {
|
|
||||||
term.damageMutex.Lock()
|
|
||||||
term.damage = append(term.damage, *rect)
|
|
||||||
term.damageMutex.Unlock()
|
|
||||||
term.invalidate()
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (term *Terminal) onMoveCursor(old *vterm.Pos,
|
|
||||||
pos *vterm.Pos, visible bool,
|
|
||||||
) int {
|
|
||||||
rows, cols, _ := pty.Getsize(term.pty)
|
|
||||||
if pos.Row() >= rows || pos.Col() >= cols {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
term.cursorPos = *pos
|
|
||||||
term.invalidate()
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (term *Terminal) onSetTermProp(prop int, val *vterm.VTermValue) int {
|
|
||||||
switch prop {
|
|
||||||
case vterm.VTERM_PROP_TITLE:
|
|
||||||
if term.OnTitle != nil {
|
|
||||||
term.OnTitle(val.String)
|
|
||||||
}
|
|
||||||
case vterm.VTERM_PROP_CURSORVISIBLE:
|
|
||||||
term.cursorShown = val.Boolean
|
|
||||||
term.invalidate()
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue