Hello - The patches in this set make tcell-term into a proper tcell widget by conforming to the views.Widget interface. This makes it a more reusable widget in other libraries. Some performance improvements were made by only drawing changed cells. A fix was added for scrollable regions when inserting and deleting lines (very noticable when using neovim) Some of these patches revert parts of others. I was working on integrating within aerc and making simple, reusable examples while adjusting the API. Where it has ended up is pretty decent since it satisfies the tcell Widget interface. I removed the original example in favor of a more tcell-friendly one, and simplified by only providing one example. I will send a follow up patch that adjusts the readme to match the new API, if this is all accepted. Tim Culverhouse (22): cursor: externalize cursor placement api: simplify draw method and require views.View api: rename Event to HandleEvent api: change Resize arguments examples: ignore terminal: rename vp to view api: add size and Watch functions api: internalize redraw chan example_simple: fix chore: remove old comment cell: add dirty methods and prop cells: set dirty when cell changes terminal: don't require view and screen at New terminal: BREAKING: use a redraw poll instead of channel scrollable: fix delete and insert lines operations csi: use cell.setStyle instead of assigning directly chore: remove unused code perf: only redraw cell if dirty view: remove unused screen calls and prevent panics chore: gofumpt example: replace main example and remove _simple run: allow running with custom SysProcAttr example/example.go | 219 ++++++++++++++------------------------- example_simple/simple.go | 91 ---------------- terminal.go | 111 +++++++++++++++++--- termutil/buffer.go | 52 +++------- termutil/cell.go | 48 +++------ termutil/charsets.go | 1 - termutil/csi.go | 7 +- termutil/line.go | 9 +- termutil/osc.go | 1 - termutil/terminal.go | 50 ++++----- 10 files changed, 232 insertions(+), 357 deletions(-) delete mode 100644 example_simple/simple.go -- 2.37.3
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~ghost08/tcell-term/patches/35181/mbox | git am -3Learn more about email & git
Move cursor placement from within draw to the external app Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/terminal.go b/terminal.go index 11cd9c8fe746..2f74cc245654 100644 --- a/terminal.go +++ b/terminal.go @@ -12,6 +12,11 @@ import ( type Terminal struct { term *termutil.Terminal + + curX int + curY int + curStyle tcell.CursorStyle + curVis bool } func New(opts ...Option) *Terminal { @@ -67,10 +72,12 @@ func (t *Terminal) Draw(s tcell.Screen, X, Y uint16) { } } if buf.IsCursorVisible() { - s.ShowCursor(int(buf.CursorColumn()+X), int(buf.CursorLine()+Y)) - s.SetCursorStyle(tcell.CursorStyle(t.term.GetActiveBuffer().GetCursorShape())) + t.curVis = true + t.curX = int(buf.CursorColumn()) + t.curY = int(buf.CursorLine()) + t.curStyle = tcell.CursorStyle(t.term.GetActiveBuffer().GetCursorShape()) } else { - s.HideCursor() + t.curVis = false } for _, s := range buf.GetVisibleSixels() { fmt.Printf("\033[%d;%dH", s.Sixel.Y+uint64(Y), s.Sixel.X+X) @@ -82,6 +89,12 @@ func (t *Terminal) Draw(s tcell.Screen, X, Y uint16) { } } +// GetCursor returns if the cursor is visible, it's x and y position, and it's +// style. If the cursor is not visible, the coordinates will be -1,-1 +func (t *Terminal) GetCursor() (bool, int, int, tcell.CursorStyle) { + return t.curVis, t.curX, t.curY, t.curStyle +} + func convertColor(c color.Color, defaultColor tcell.Color) tcell.Color { if c == nil { return defaultColor -- 2.37.3
Use a tcell View interface instead of a screen + offsets for drawing. Require a screen and view at instantiation instead of at draw Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/terminal.go b/terminal.go index 2f74cc245654..4ffc03cd8809 100644 --- a/terminal.go +++ b/terminal.go @@ -8,6 +8,7 @@ import ( "git.sr.ht/~ghost08/tcell-term/termutil" "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" ) type Terminal struct { @@ -17,9 +18,22 @@ type Terminal struct { curY int curStyle tcell.CursorStyle curVis bool + + screen tcell.Screen + vp views.View } -func New(opts ...Option) *Terminal { +func New(screen tcell.Screen, vp views.View, opts ...Option) *Terminal { + var err error + if screen == nil { + screen, err = tcell.NewScreen() + if err != nil { + panic(err) + } + } + if vp == nil { + vp = views.NewViewPort(screen, 0, 0, -1, -1) + } t := &Terminal{ term: termutil.New(), } @@ -38,8 +52,9 @@ func WithWindowManipulator(wm termutil.WindowManipulator) Option { } } -func (t *Terminal) Run(cmd *exec.Cmd, redrawChan chan struct{}, width, height uint16) error { - return t.term.Run(cmd, redrawChan, height, width) +func (t *Terminal) Run(cmd *exec.Cmd, redrawChan chan struct{}) error { + w, h := t.vp.Size() + return t.term.Run(cmd, redrawChan, uint16(h), uint16(w)) } func (t *Terminal) Event(e tcell.Event) { @@ -58,8 +73,8 @@ func (t *Terminal) Event(e tcell.Event) { } } -func (t *Terminal) Draw(s tcell.Screen, X, Y uint16) { - s.Clear() +func (t *Terminal) Draw() { + t.vp.Clear() buf := t.term.GetActiveBuffer() for viewY := int(buf.ViewHeight()) - 1; viewY >= 0; viewY-- { for viewX := uint16(0); viewX < buf.ViewWidth(); viewX++ { @@ -68,7 +83,7 @@ func (t *Terminal) Draw(s tcell.Screen, X, Y uint16) { // s.SetContent(int(viewX+X), viewY+int(Y), ' ', nil, tcell.StyleDefault.Background(tcell.ColorBlack)) continue } - s.SetContent(int(viewX+X), viewY+int(Y), cell.Rune().Rune, nil, cell.Style()) + t.vp.SetContent(int(viewX), viewY, cell.Rune().Rune, nil, cell.Style()) } } if buf.IsCursorVisible() { @@ -80,7 +95,7 @@ func (t *Terminal) Draw(s tcell.Screen, X, Y uint16) { t.curVis = false } for _, s := range buf.GetVisibleSixels() { - fmt.Printf("\033[%d;%dH", s.Sixel.Y+uint64(Y), s.Sixel.X+X) + fmt.Printf("\033[%d;%dH", s.Sixel.Y, s.Sixel.X) // DECSIXEL Introducer(\033P0;0;8q) + DECGRA ("1;1): Set Raster Attributes os.Stdout.Write([]byte{0x1b, 0x50, 0x30, 0x3b, 0x30, 0x3b, 0x38, 0x71, 0x22, 0x31, 0x3b, 0x31}) os.Stdout.Write(s.Sixel.Data) -- 2.37.3
Rename Event to HandleEvent to be able to satisfy the tcell Widget interface Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/terminal.go b/terminal.go index 4ffc03cd8809..d41b06a8c0de 100644 --- a/terminal.go +++ b/terminal.go @@ -57,7 +57,7 @@ func (t *Terminal) Run(cmd *exec.Cmd, redrawChan chan struct{}) error { return t.term.Run(cmd, redrawChan, uint16(h), uint16(w)) } -func (t *Terminal) Event(e tcell.Event) { +func (t *Terminal) HandleEvent(e tcell.Event) bool { switch e := e.(type) { case *tcell.EventKey: var keycode string @@ -70,7 +70,9 @@ func (t *Terminal) Event(e tcell.Event) { keycode = getKeyCode(e) } t.term.WriteToPty([]byte(keycode)) + return true } + return false } func (t *Terminal) Draw() { -- 2.37.3
Change Resize() arguments to satisfy tcell Widget interface Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/terminal.go b/terminal.go index d41b06a8c0de..0b98e55bf79b 100644 --- a/terminal.go +++ b/terminal.go @@ -120,8 +120,9 @@ func convertColor(c color.Color, defaultColor tcell.Color) tcell.Color { return tcell.NewRGBColor(int32(r), int32(g), int32(b)) } -func (t *Terminal) Resize(width, height int) { - t.term.SetSize(uint16(height), uint16(width)) +func (t *Terminal) Resize() { + w, h := t.vp.Size() + t.term.SetSize(uint16(h), uint16(w)) } type windowManipulator struct{} -- 2.37.3
Ignore examples for build Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- example/example.go | 2 ++ example_simple/simple.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/example/example.go b/example/example.go index 3118826b3f28..f4715407afc3 100644 --- a/example/example.go +++ b/example/example.go @@ -1,3 +1,5 @@ +// +build ignore + package main import ( diff --git a/example_simple/simple.go b/example_simple/simple.go index 4f91ae226879..31456e4cc39a 100644 --- a/example_simple/simple.go +++ b/example_simple/simple.go @@ -1,3 +1,5 @@ +// +build ignore + package main import ( -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/terminal.go b/terminal.go index 0b98e55bf79b..152f83983bd2 100644 --- a/terminal.go +++ b/terminal.go @@ -20,10 +20,10 @@ type Terminal struct { curVis bool screen tcell.Screen - vp views.View + view views.View } -func New(screen tcell.Screen, vp views.View, opts ...Option) *Terminal { +func New(screen tcell.Screen, view views.View, opts ...Option) *Terminal { var err error if screen == nil { screen, err = tcell.NewScreen() @@ -31,11 +31,13 @@ func New(screen tcell.Screen, vp views.View, opts ...Option) *Terminal { panic(err) } } - if vp == nil { - vp = views.NewViewPort(screen, 0, 0, -1, -1) + if view == nil { + view = views.NewViewPort(screen, 0, 0, -1, -1) } t := &Terminal{ term: termutil.New(), + screen: screen, + view: view, } t.term.SetWindowManipulator(&windowManipulator{}) for _, opt := range opts { @@ -53,10 +55,14 @@ func WithWindowManipulator(wm termutil.WindowManipulator) Option { } func (t *Terminal) Run(cmd *exec.Cmd, redrawChan chan struct{}) error { - w, h := t.vp.Size() + w, h := t.view.Size() return t.term.Run(cmd, redrawChan, uint16(h), uint16(w)) } +func (t *Terminal) SetView(view views.View) { + t.view = view +} + func (t *Terminal) HandleEvent(e tcell.Event) bool { switch e := e.(type) { case *tcell.EventKey: @@ -76,7 +82,7 @@ func (t *Terminal) HandleEvent(e tcell.Event) bool { } func (t *Terminal) Draw() { - t.vp.Clear() + t.view.Clear() buf := t.term.GetActiveBuffer() for viewY := int(buf.ViewHeight()) - 1; viewY >= 0; viewY-- { for viewX := uint16(0); viewX < buf.ViewWidth(); viewX++ { @@ -85,7 +91,7 @@ func (t *Terminal) Draw() { // s.SetContent(int(viewX+X), viewY+int(Y), ' ', nil, tcell.StyleDefault.Background(tcell.ColorBlack)) continue } - t.vp.SetContent(int(viewX), viewY, cell.Rune().Rune, nil, cell.Style()) + t.view.SetContent(int(viewX), viewY, cell.Rune().Rune, nil, cell.Style()) } } if buf.IsCursorVisible() { @@ -121,7 +127,7 @@ func convertColor(c color.Color, defaultColor tcell.Color) tcell.Color { } func (t *Terminal) Resize() { - w, h := t.vp.Size() + w, h := t.view.Size() t.term.SetSize(uint16(h), uint16(w)) } -- 2.37.3
Add Size and Watch functions to finish satisfying Widget interface Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/terminal.go b/terminal.go index 152f83983bd2..e895fb648454 100644 --- a/terminal.go +++ b/terminal.go @@ -21,6 +21,8 @@ type Terminal struct { screen tcell.Screen view views.View + + views.WidgetWatchers } func New(screen tcell.Screen, view views.View, opts ...Option) *Terminal { @@ -63,6 +65,10 @@ func (t *Terminal) SetView(view views.View) { t.view = view } +func (t *Terminal) Size() (int, int) { + return t.view.Size() +} + func (t *Terminal) HandleEvent(e tcell.Event) bool { switch e := e.(type) { case *tcell.EventKey: -- 2.37.3
Internalize redraw chan and use it to post an event to the tcell screen. A new redraw request will issue a RedrawEvent to the screen. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/terminal.go b/terminal.go index e895fb648454..8cfdb1ab1c64 100644 --- a/terminal.go +++ b/terminal.go @@ -5,6 +5,7 @@ import ( "image/color" "os" "os/exec" + "time" "git.sr.ht/~ghost08/tcell-term/termutil" "github.com/gdamore/tcell/v2" @@ -20,7 +21,9 @@ type Terminal struct { curVis bool screen tcell.Screen - view views.View + view views.View + + redrawChan chan struct{} views.WidgetWatchers } @@ -37,9 +40,9 @@ func New(screen tcell.Screen, view views.View, opts ...Option) *Terminal { view = views.NewViewPort(screen, 0, 0, -1, -1) } t := &Terminal{ - term: termutil.New(), + term: termutil.New(), screen: screen, - view: view, + view: view, } t.term.SetWindowManipulator(&windowManipulator{}) for _, opt := range opts { @@ -56,11 +59,34 @@ func WithWindowManipulator(wm termutil.WindowManipulator) Option { } } -func (t *Terminal) Run(cmd *exec.Cmd, redrawChan chan struct{}) error { +func (t *Terminal) Run(cmd *exec.Cmd) error { w, h := t.view.Size() - return t.term.Run(cmd, redrawChan, uint16(h), uint16(w)) + t.redrawChan = make(chan struct{}, 10) + go func() { + for { + select { + case <-t.redrawChan: + t.screen.PostEvent(NewRedrawEvent()) + // term.Draw() + // s.Show() + } + } + }() + return t.term.Run(cmd, t.redrawChan, uint16(h), uint16(w)) +} + +type RedrawEvent struct { + when time.Time } +func NewRedrawEvent() *RedrawEvent { + return &RedrawEvent{ + when: time.Now(), + } +} + +func (e RedrawEvent) When() time.Time { return e.when } + func (t *Terminal) SetView(view views.View) { t.view = view } -- 2.37.3
Fix example_simple with latest updates Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- example_simple/simple.go | 84 ++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/example_simple/simple.go b/example_simple/simple.go index 31456e4cc39a..e530c3463bee 100644 --- a/example_simple/simple.go +++ b/example_simple/simple.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -34,60 +35,49 @@ func main() { s.Clear() quit := make(chan struct{}) - redraw := make(chan struct{}, 10) var term *tcellterm.Terminal - if term == nil { - term = tcellterm.New() + term = tcellterm.New(s, nil) - cmd := exec.Command("zsh") - go func() { - w, h := s.Size() - lh := h - lw := w - if err := term.Run(cmd, redraw, uint16(lw), uint16(lh)); err != nil { - log.Println(err) - } - s.HideCursor() - term = nil - close(quit) - }() - } + cmd := exec.Command("zsh") go func() { - for { - ev := s.PollEvent() - switch ev := ev.(type) { - case *tcell.EventKey: - switch ev.Key() { - case tcell.KeyCtrlC: - close(quit) - return - } - if term != nil { - term.Event(ev) - } - case *tcell.EventResize: - if term != nil { - w, h := s.Size() - lh := h - lw := w - term.Resize(lw, lh) - } - s.Sync() - } + if err := term.Run(cmd); err != nil { + log.Println(err) } + close(quit) + s.Fini() + os.Stdout.Write(logbuf.Bytes()) + os.Exit(0) }() - -loop: for { - select { - case <-quit: - break loop - case <-redraw: - term.Draw(s, 0, 0) + ev := s.PollEvent() + switch ev := ev.(type) { + case *tcell.EventKey: + switch ev.Key() { + case tcell.KeyCtrlC: + close(quit) + s.Fini() + os.Stdout.Write(logbuf.Bytes()) + return + } + if term != nil { + term.HandleEvent(ev) + } + case *tcell.EventResize: + if term != nil { + term.Resize() + } + s.Sync() + case *tcellterm.RedrawEvent: + term.Draw() + vis, x, y, style := term.GetCursor() + if vis { + s.ShowCursor(x, y) + s.SetCursorStyle(style) + } else { + s.HideCursor() + } s.Show() } - } - s.Fini() - os.Stdout.Write(logbuf.Bytes()) + } } -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/charsets.go | 1 - 1 file changed, 1 deletion(-) diff --git a/termutil/charsets.go b/termutil/charsets.go index 7e2d17a727dc..615b3227b1d4 100644 --- a/termutil/charsets.go +++ b/termutil/charsets.go @@ -54,7 +54,6 @@ func (t *Terminal) scsHandler(pty chan MeasuredRune, which int) bool { cs, ok := charSets[b.Rune] if ok { - //terminal.logger.Debugf("Selected charset %v into G%v", string(b), which) t.activeBuffer.charsets[which] = cs return false } -- 2.37.3
To enable preventing drawing every cell, even if it hasn't changed Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/cell.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/termutil/cell.go b/termutil/cell.go index d1a501ec8e65..a3a8cfbd1e9b 100644 --- a/termutil/cell.go +++ b/termutil/cell.go @@ -5,8 +5,9 @@ import ( ) type Cell struct { - r MeasuredRune - attr tcell.Style + r MeasuredRune + attr tcell.Style + dirty bool } func (cell *Cell) Rune() MeasuredRune { @@ -53,11 +54,26 @@ func (cell *Cell) Bg() tcell.Color { } */ +func (cell *Cell) Dirty() bool { + return cell.dirty +} + +func (cell *Cell) SetDirty(d bool) { + cell.dirty = d +} + func (cell *Cell) erase(bgColour tcell.Color) { cell.setRune(MeasuredRune{Rune: 0}) cell.attr = cell.attr.Background(bgColour) + cell.SetDirty(true) } func (cell *Cell) setRune(r MeasuredRune) { cell.r = r + cell.SetDirty(true) +} + +func (cell *Cell) setStyle(s tcell.Style) { + cell.attr = s + cell.SetDirty(true) } -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/buffer.go | 7 ++++++- termutil/line.go | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/termutil/buffer.go b/termutil/buffer.go index 0d24044b837c..836d9f633df1 100644 --- a/termutil/buffer.go +++ b/termutil/buffer.go @@ -130,6 +130,7 @@ func (buffer *Buffer) areaScrollDown(lines uint16) { i-- if i >= top+uint64(lines) { buffer.lines[i] = buffer.lines[i-uint64(lines)] + buffer.lines[i].setDirty(true) } else { buffer.lines[i] = newLine() } @@ -144,6 +145,7 @@ func (buffer *Buffer) areaScrollUp(lines uint16) { from := i + uint64(lines) if from < bottom { buffer.lines[i] = buffer.lines[from] + buffer.lines[i].setDirty(true) } else { buffer.lines[i] = newLine() } @@ -372,6 +374,9 @@ func (buffer *Buffer) index() { if uint64(len(buffer.lines)) > maxLines { copy(buffer.lines, buffer.lines[uint64(len(buffer.lines))-maxLines:]) buffer.lines = buffer.lines[:maxLines] + for _, line := range buffer.lines { + line.setDirty(true) + } } } buffer.cursorPosition.Line++ @@ -783,7 +788,7 @@ func (buffer *Buffer) defaultCell(applyEffects bool) Cell { attr = attr.Underline(false) attr = attr.Dim(false) } - return Cell{attr: attr} + return Cell{attr: attr, dirty: true} } func (buffer *Buffer) IsNewLineMode() bool { diff --git a/termutil/line.go b/termutil/line.go index 0b76251c4b5d..2553fa704cdd 100644 --- a/termutil/line.go +++ b/termutil/line.go @@ -30,6 +30,12 @@ func (line *Line) append(cells ...Cell) { line.cells = append(line.cells, cells...) } +func (line *Line) setDirty(d bool) { + for _, cell := range line.cells { + cell.SetDirty(d) + } +} + func (line *Line) shrink(width uint16) { if line.Len() <= width { return @@ -40,6 +46,7 @@ func (line *Line) shrink(width uint16) { if cell.r.Rune == 0 && remove > 0 { remove-- } else { + cell.SetDirty(true) cells = append(cells, cell) } } @@ -54,6 +61,7 @@ func (line *Line) wrap(width uint16) []Line { current.wrapped = line.wrapped for _, cell := range line.cells { + cell.SetDirty(true) if len(current.cells) == int(width) { output = append(output, current) current = newLine() -- 2.37.3
Reverts a previous commit Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/terminal.go b/terminal.go index 8cfdb1ab1c64..f1683c05447a 100644 --- a/terminal.go +++ b/terminal.go @@ -28,21 +28,9 @@ type Terminal struct { views.WidgetWatchers } -func New(screen tcell.Screen, view views.View, opts ...Option) *Terminal { - var err error - if screen == nil { - screen, err = tcell.NewScreen() - if err != nil { - panic(err) - } - } - if view == nil { - view = views.NewViewPort(screen, 0, 0, -1, -1) - } +func New(opts ...Option) *Terminal { t := &Terminal{ - term: termutil.New(), - screen: screen, - view: view, + term: termutil.New(), } t.term.SetWindowManipulator(&windowManipulator{}) for _, opt := range opts { -- 2.37.3
The processing of runes creates a very large amount of data sent to the redrawRequest channel. Use a bool and periodic check for if a render is required. Previously, a debounce had been added. The debounce functioned similarly to this implementation however there are fewer chances for error in this implementation. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 36 +++++++++++++++++++----------------- termutil/terminal.go | 41 ++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/terminal.go b/terminal.go index f1683c05447a..f59ee23fc6ef 100644 --- a/terminal.go +++ b/terminal.go @@ -23,7 +23,7 @@ type Terminal struct { screen tcell.Screen view views.View - redrawChan chan struct{} + close bool views.WidgetWatchers } @@ -49,31 +49,33 @@ func WithWindowManipulator(wm termutil.WindowManipulator) Option { func (t *Terminal) Run(cmd *exec.Cmd) error { w, h := t.view.Size() - t.redrawChan = make(chan struct{}, 10) + tmr := time.NewTicker(16 * time.Millisecond) go func() { for { select { - case <-t.redrawChan: - t.screen.PostEvent(NewRedrawEvent()) - // term.Draw() - // s.Show() + case <-tmr.C: + if t.close { + return + } + if t.term.ShouldRedraw() { + t.PostEventWidgetContent(t) + t.term.SetRedraw(false) + } } } }() - return t.term.Run(cmd, t.redrawChan, uint16(h), uint16(w)) -} - -type RedrawEvent struct { - when time.Time -} - -func NewRedrawEvent() *RedrawEvent { - return &RedrawEvent{ - when: time.Now(), + err := t.term.Run(cmd, uint16(h), uint16(w)) + if err != nil { + return err } + t.Close() + return nil } -func (e RedrawEvent) When() time.Time { return e.when } +func (t *Terminal) Close() { + t.close = true + t.term.Pty().Close() +} func (t *Terminal) SetView(view views.View) { t.view = view diff --git a/termutil/terminal.go b/termutil/terminal.go index 3e7dc7493d4c..9514eff3f382 100644 --- a/termutil/terminal.go +++ b/termutil/terminal.go @@ -23,22 +23,19 @@ const ( type Terminal struct { windowManipulator WindowManipulator pty *os.File - updateChan chan struct{} processChan chan MeasuredRune - closeChan chan struct{} buffers []*Buffer activeBuffer *Buffer mouseMode MouseMode mouseExtMode MouseExtMode theme *Theme - renderDebounce *time.Timer + redraw bool } // NewTerminal creates a new terminal instance func New(options ...Option) *Terminal { term := &Terminal{ processChan: make(chan MeasuredRune, 0xffff), - closeChan: make(chan struct{}), theme: &Theme{}, } for _, opt := range options { @@ -119,8 +116,7 @@ func (t *Terminal) SetSize(rows, cols uint16) error { } // Run starts the terminal/shell proxying process -func (t *Terminal) Run(c *exec.Cmd, updateChan chan struct{}, rows uint16, cols uint16) error { - t.updateChan = updateChan +func (t *Terminal) Run(c *exec.Cmd, rows uint16, cols uint16) error { c.Env = append(os.Environ(), "TERM=xterm-256color") // Start the command with a pty. @@ -148,34 +144,29 @@ func (t *Terminal) Run(c *exec.Cmd, updateChan chan struct{}, rows uint16, cols go t.process() _, _ = io.Copy(t, t.pty) - close(t.closeChan) return nil } -func (t *Terminal) requestRender() { - if t.renderDebounce != nil { - t.renderDebounce.Stop() - } - // 4 milliseconds = 250Hz. Probably don't need to render faster than - // that - t.renderDebounce = time.AfterFunc(4*time.Millisecond, func() { - t.updateChan <- struct{}{} - }) +func (t *Terminal) ShouldRedraw() bool { + return t.redraw +} + +func (t *Terminal) SetRedraw(b bool) { + t.redraw = b } func (t *Terminal) process() { for { - select { - case <-t.closeChan: + mr, ok := <-t.processChan + if !ok { return - case mr := <-t.processChan: - if mr.Rune == 0x1b { // ANSI escape char, which means this is a sequence - if t.handleANSI(t.processChan) { - t.requestRender() - } - } else if t.processRunes(mr) { // otherwise it's just an individual rune we need to process - t.requestRender() + } + if mr.Rune == 0x1b { // ANSI escape char, which means this is a sequence + if t.handleANSI(t.processChan) { + t.SetRedraw(true) } + } else if t.processRunes(mr) { // otherwise it's just an individual rune we need to process + t.SetRedraw(true) } } } -- 2.37.3
Delete lines and insert lines in a scrollable region has the same effect as scrolling up or down. Simplify the logic by calling these directly. See [0] for a reference implementation. [0]: https://github.com/james4k/terminal/blob/b4bcb6ee7c08ae4930eecdeb1ba90073c5f40d71/state.go#L682 Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/buffer.go | 45 ++++++--------------------------------------- 1 file changed, 6 insertions(+), 39 deletions(-) diff --git a/termutil/buffer.go b/termutil/buffer.go index 836d9f633df1..f7a397d025bf 100644 --- a/termutil/buffer.go +++ b/termutil/buffer.go @@ -273,48 +273,15 @@ func (buffer *Buffer) ViewHeight() uint16 { } func (buffer *Buffer) deleteLine() { - index := int(buffer.RawLine()) - buffer.lines = buffer.lines[:index+copy(buffer.lines[index:], buffer.lines[index+1:])] + // see + // https://github.com/james4k/terminal/blob/b4bcb6ee7c08ae4930eecdeb1ba90073c5f40d71/state.go#L682 + buffer.areaScrollUp(1) } func (buffer *Buffer) insertLine() { - if !buffer.InScrollableRegion() { - pos := buffer.RawLine() - maxLines := buffer.GetMaxLines() - newLineCount := uint64(len(buffer.lines) + 1) - if newLineCount > maxLines { - newLineCount = maxLines - } - - out := make([]Line, newLineCount) - copy( - out[:pos-(uint64(len(buffer.lines))+1-newLineCount)], - buffer.lines[uint64(len(buffer.lines))+1-newLineCount:pos]) - out[pos] = newLine() - copy(out[pos+1:], buffer.lines[pos:]) - buffer.lines = out - } else { - topIndex := buffer.convertViewLineToRawLine(uint16(buffer.topMargin)) - bottomIndex := buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin)) - before := buffer.lines[:topIndex] - after := buffer.lines[bottomIndex+1:] - out := make([]Line, len(buffer.lines)) - copy(out[0:], before) - - pos := buffer.RawLine() - for i := topIndex; i < bottomIndex; i++ { - if i < pos { - out[i] = buffer.lines[i] - } else { - out[i+1] = buffer.lines[i] - } - } - - copy(out[bottomIndex+1:], after) - - out[pos] = newLine() - buffer.lines = out - } + // see + // https://github.com/james4k/terminal/blob/b4bcb6ee7c08ae4930eecdeb1ba90073c5f40d71/state.go#L682 + buffer.areaScrollDown(1) } func (buffer *Buffer) insertBlankCharacters(count int) { -- 2.37.3
This permits easier setting of cell.dirty Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/csi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/termutil/csi.go b/termutil/csi.go index 377146dc3d6a..b959db50065b 100644 --- a/termutil/csi.go +++ b/termutil/csi.go @@ -972,7 +972,7 @@ func (t *Terminal) sgrSequenceHandler(params []string) bool { x := t.GetActiveBuffer().CursorColumn() y := t.GetActiveBuffer().CursorLine() if cell := t.GetActiveBuffer().GetCell(x, y); cell != nil { - cell.attr = t.GetActiveBuffer().cursorAttr + cell.setStyle(t.GetActiveBuffer().cursorAttr) } return false -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/cell.go | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/termutil/cell.go b/termutil/cell.go index a3a8cfbd1e9b..095c35a7bac4 100644 --- a/termutil/cell.go +++ b/termutil/cell.go @@ -18,42 +18,6 @@ func (cell *Cell) Style() tcell.Style { return cell.attr } -/* -func (cell *Cell) Fg() tcell.Color { - if cell.attr.inverse { - return cell.attr.bgColour - } - return cell.attr.fgColour -} - -func (cell *Cell) Bold() bool { - return cell.attr.bold -} - -func (cell *Cell) Dim() bool { - return cell.attr.dim -} - -func (cell *Cell) Italic() bool { - return cell.attr.italic -} - -func (cell *Cell) Underline() bool { - return cell.attr.underline -} - -func (cell *Cell) Strikethrough() bool { - return cell.attr.strikethrough -} - -func (cell *Cell) Bg() tcell.Color { - if cell.attr.inverse { - return cell.attr.fgColour - } - return cell.attr.bgColour -} -*/ - func (cell *Cell) Dirty() bool { return cell.dirty } -- 2.37.3
Use cell.Dirty to decide if redrawing cell or leaving as is. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/terminal.go b/terminal.go index f59ee23fc6ef..2b7e3941d8d8 100644 --- a/terminal.go +++ b/terminal.go @@ -104,16 +104,16 @@ func (t *Terminal) HandleEvent(e tcell.Event) bool { } func (t *Terminal) Draw() { - t.view.Clear() buf := t.term.GetActiveBuffer() - for viewY := int(buf.ViewHeight()) - 1; viewY >= 0; viewY-- { - for viewX := uint16(0); viewX < buf.ViewWidth(); viewX++ { + w, h := t.view.Size() + for viewY := 0; viewY < h; viewY++ { + for viewX := uint16(0); viewX < uint16(w); viewX++ { cell := buf.GetCell(viewX, uint16(viewY)) if cell == nil { - // s.SetContent(int(viewX+X), viewY+int(Y), ' ', nil, tcell.StyleDefault.Background(tcell.ColorBlack)) - continue + t.view.SetContent(int(viewX), viewY, ' ', nil, tcell.StyleDefault) + } else if cell.Dirty() { + t.view.SetContent(int(viewX), viewY, cell.Rune().Rune, nil, cell.Style()) } - t.view.SetContent(int(viewX), viewY, cell.Rune().Rune, nil, cell.Style()) } } if buf.IsCursorVisible() { -- 2.37.3
Prevent panics when calling references to view when it is nil. Also, remove logging in csi handler Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 15 +++++++++++++-- termutil/csi.go | 5 ++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/terminal.go b/terminal.go index 2b7e3941d8d8..1e0d661492df 100644 --- a/terminal.go +++ b/terminal.go @@ -20,8 +20,7 @@ type Terminal struct { curStyle tcell.CursorStyle curVis bool - screen tcell.Screen - view views.View + view views.View close bool @@ -77,11 +76,16 @@ func (t *Terminal) Close() { t.term.Pty().Close() } +// SetView sets the view for the terminal to draw to. This must be set before +// calling Draw func (t *Terminal) SetView(view views.View) { t.view = view } func (t *Terminal) Size() (int, int) { + if t.view == nil { + return 0, 0 + } return t.view.Size() } @@ -104,6 +108,9 @@ func (t *Terminal) HandleEvent(e tcell.Event) bool { } func (t *Terminal) Draw() { + if t.view == nil { + return + } buf := t.term.GetActiveBuffer() w, h := t.view.Size() for viewY := 0; viewY < h; viewY++ { @@ -148,7 +155,11 @@ func convertColor(c color.Color, defaultColor tcell.Color) tcell.Color { return tcell.NewRGBColor(int32(r), int32(g), int32(b)) } +// Resize resizes the terminal to the dimensions of the terminals view func (t *Terminal) Resize() { + if t.view == nil { + return + } w, h := t.view.Size() t.term.SetSize(uint16(h), uint16(w)) } diff --git a/termutil/csi.go b/termutil/csi.go index b959db50065b..8f4888367b2d 100644 --- a/termutil/csi.go +++ b/termutil/csi.go @@ -2,7 +2,6 @@ package termutil import ( "fmt" - "log" "strconv" "strings" @@ -128,7 +127,7 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) { // if this is an unknown CSI sequence, write it to stdout as we can't handle it? //_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...) _ = raw - log.Printf("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final) + // log.Printf("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final) return false } @@ -788,7 +787,7 @@ func (t *Terminal) csiSetMode(modes string, enabled bool) bool { case "?80": // t.activeBuffer.modes.SixelScrolling = enabled default: - log.Printf("Unsupported CSI mode %s = %t", modeStr, enabled) + // log.Printf("Unsupported CSI mode %s = %t", modeStr, enabled) } } return false -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- termutil/line.go | 1 - termutil/osc.go | 1 - 2 files changed, 2 deletions(-) diff --git a/termutil/line.go b/termutil/line.go index 2553fa704cdd..10f6a241f104 100644 --- a/termutil/line.go +++ b/termutil/line.go @@ -54,7 +54,6 @@ func (line *Line) shrink(width uint16) { } func (line *Line) wrap(width uint16) []Line { - var output []Line var current Line diff --git a/termutil/osc.go b/termutil/osc.go index af199d151be7..749cd12a4784 100644 --- a/termutil/osc.go +++ b/termutil/osc.go @@ -5,7 +5,6 @@ import ( ) func (t *Terminal) handleOSC(readChan chan MeasuredRune) (renderRequired bool) { - params := []string{} param := "" -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- example/example.go | 217 ++++++++++++++------------------------- example_simple/simple.go | 83 --------------- 2 files changed, 75 insertions(+), 225 deletions(-) delete mode 100644 example_simple/simple.go diff --git a/example/example.go b/example/example.go index f4715407afc3..332100de22b7 100644 --- a/example/example.go +++ b/example/example.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main @@ -7,181 +8,113 @@ import ( "fmt" "io" "log" - "math/rand" "os" "os/exec" - "time" tcellterm "git.sr.ht/~ghost08/tcell-term" "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" ) -var red = int32(rand.Int() % 256) -var grn = int32(rand.Int() % 256) -var blu = int32(rand.Int() % 256) -var inc = int32(8) // rate of color change -var redi = int32(inc) -var grni = int32(inc) -var blui = int32(inc) -var term *tcellterm.Terminal - -func makebox(s tcell.Screen) { - s.Clear() - w, h := s.Size() - - if w == 0 || h == 0 { - return - } - - glyphs := []rune{'@', '#', '&', '*', '=', '%', 'Z', 'A'} - - lh := h / 2 - lw := w / 2 - lx := w / 4 - ly := h / 4 - st := tcell.StyleDefault - gl := ' ' - - if s.Colors() == 0 { - st = st.Reverse(rand.Int()%2 == 0) - gl = glyphs[rand.Int()%len(glyphs)] - } else { +type model struct { + term *tcellterm.Terminal + s tcell.Screen + termView views.View + title *views.TextBar + titleView views.View +} - red += redi - if (red >= 256) || (red < 0) { - redi = -redi - red += redi +func (m *model) HandleEvent(ev tcell.Event) bool { + switch ev := ev.(type) { + case *tcell.EventKey: + switch ev.Key() { + case tcell.KeyCtrlC: + m.s.Clear() + m.s.Fini() + return true } - grn += grni - if (grn >= 256) || (grn < 0) { - grni = -grni - grn += grni + if m.term != nil { + return m.term.HandleEvent(ev) } - blu += blui - if (blu >= 256) || (blu < 0) { - blui = -blui - blu += blui - + case *tcell.EventResize: + if m.term != nil { + m.termView.Resize(0, 2, -1, -1) + m.term.Resize() } - st = st.Background(tcell.NewRGBColor(red, grn, blu)) - } - if term == nil { - for row := 0; row < lh; row++ { - for col := 0; col < lw; col++ { - s.SetCell(lx+col, ly+row, st, gl) - } + m.titleView.Resize(0, 0, -1, 2) + m.title.Resize() + m.s.Sync() + return true + case *views.EventWidgetContent: + m.term.Draw() + m.title.Draw() + + vis, x, y, style := m.term.GetCursor() + if vis { + m.s.ShowCursor(x, y+2) + m.s.SetCursorStyle(style) + } else { + m.s.HideCursor() } - } else { - term.Draw(s, uint16(lx), uint16(ly)) + m.s.Show() + return true } - s.Show() -} - -func flipcoin() bool { - if rand.Int()&1 == 0 { - return false - } - return true + return false } func main() { + var err error f, _ := os.Create("meh.log") defer f.Close() logbuf := bytes.NewBuffer(nil) log.SetOutput(io.MultiWriter(f, logbuf)) log.SetFlags(log.LstdFlags | log.Lshortfile) - rand.Seed(time.Now().UnixNano()) - tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) - s, e := tcell.NewScreen() - if e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) + m := &model{} + m.s, err = tcell.NewScreen() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } - if e = s.Init(); e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) + if err = m.s.Init(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } - s.SetStyle(tcell.StyleDefault. - Foreground(tcell.ColorWhite). - Background(tcell.ColorBlack)) - s.Clear() + m.title = views.NewTextBar() + m.title.SetCenter( + "Welcome to tcell-term", + tcell.StyleDefault.Foreground(tcell.ColorBlue). + Bold(true). + Underline(true), + ) + + m.titleView = views.NewViewPort(m.s, 0, 0, -1, 2) + m.title.Watch(m) + m.title.SetView(m.titleView) - quit := make(chan struct{}) - redraw := make(chan struct{}) + m.termView = views.NewViewPort(m.s, 0, 2, -1, -1) + m.term = tcellterm.New() + m.term.Watch(m) + m.term.SetView(m.termView) + + cmd := exec.Command(os.Getenv("SHELL")) go func() { - for { - ev := s.PollEvent() - switch ev := ev.(type) { - case *tcell.EventKey: - switch ev.Key() { - case tcell.KeyEscape: - if term == nil { - close(quit) - return - } - case tcell.KeyEnter: - if term == nil { - term = tcellterm.New() - cmd := exec.Command("less", "/etc/hosts") - go func() { - w, h := s.Size() - lh := h / 2 - lw := w / 2 - if err := term.Run(cmd, redraw, uint16(lw), uint16(lh)); err != nil { - log.Println(err) - } - s.HideCursor() - term = nil - }() - continue - } - } - if term != nil { - term.Event(ev) - } - case *tcell.EventResize: - if term != nil { - w, h := s.Size() - lh := h / 2 - lw := w / 2 - term.Resize(lw, lh) - } - s.Sync() - } + if err := m.term.Run(cmd); err != nil { + log.Println(err) } + m.s.Clear() + m.s.Fini() + os.Stdout.Write(logbuf.Bytes()) + return }() - - cnt := 0 -loop: for { - select { - case <-quit: - break loop - case <-time.After(time.Millisecond * 50): - case <-redraw: - } - makebox(s) - cnt++ - if cnt%(256/int(inc)) == 0 { - if flipcoin() { - redi = -redi - } - if flipcoin() { - grni = -grni - } - if flipcoin() { - blui = -blui - } + // s.Show() + ev := m.s.PollEvent() + if ev == nil { + break } + m.HandleEvent(ev) } - - s.Fini() os.Stdout.Write(logbuf.Bytes()) } - -func colorToString(c tcell.Color) string { - r, g, b := c.RGB() - return fmt.Sprintf("%02x%02x%02x", r, g, b) -} diff --git a/example_simple/simple.go b/example_simple/simple.go deleted file mode 100644 index e530c3463bee..000000000000 --- a/example_simple/simple.go @@ -1,83 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import ( - "bytes" - "fmt" - "io" - "log" - "os" - "os/exec" - - tcellterm "git.sr.ht/~ghost08/tcell-term" - "github.com/gdamore/tcell/v2" -) - -func main() { - f, _ := os.Create("meh.log") - defer f.Close() - logbuf := bytes.NewBuffer(nil) - log.SetOutput(io.MultiWriter(f, logbuf)) - log.SetFlags(log.LstdFlags | log.Lshortfile) - - s, err := tcell.NewScreen() - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } - if err = s.Init(); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } - - s.Clear() - - quit := make(chan struct{}) - var term *tcellterm.Terminal - term = tcellterm.New(s, nil) - - cmd := exec.Command("zsh") - go func() { - if err := term.Run(cmd); err != nil { - log.Println(err) - } - close(quit) - s.Fini() - os.Stdout.Write(logbuf.Bytes()) - os.Exit(0) - }() - for { - ev := s.PollEvent() - switch ev := ev.(type) { - case *tcell.EventKey: - switch ev.Key() { - case tcell.KeyCtrlC: - close(quit) - s.Fini() - os.Stdout.Write(logbuf.Bytes()) - return - } - if term != nil { - term.HandleEvent(ev) - } - case *tcell.EventResize: - if term != nil { - term.Resize() - } - s.Sync() - case *tcellterm.RedrawEvent: - term.Draw() - vis, x, y, style := term.GetCursor() - if vis { - s.ShowCursor(x, y) - s.SetCursorStyle(style) - } else { - s.HideCursor() - } - s.Show() - } - - } -} -- 2.37.3
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- terminal.go | 11 ++++++++++- termutil/terminal.go | 11 ++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/terminal.go b/terminal.go index 1e0d661492df..7212068ab52e 100644 --- a/terminal.go +++ b/terminal.go @@ -5,6 +5,7 @@ import ( "image/color" "os" "os/exec" + "syscall" "time" "git.sr.ht/~ghost08/tcell-term/termutil" @@ -47,6 +48,14 @@ func WithWindowManipulator(wm termutil.WindowManipulator) Option { } func (t *Terminal) Run(cmd *exec.Cmd) error { + return t.run(cmd, &syscall.SysProcAttr{}) +} + +func (t *Terminal) RunWithAttrs(cmd *exec.Cmd, attr *syscall.SysProcAttr) error { + return t.run(cmd, attr) +} + +func (t *Terminal) run(cmd *exec.Cmd, attr *syscall.SysProcAttr) error { w, h := t.view.Size() tmr := time.NewTicker(16 * time.Millisecond) go func() { @@ -63,7 +72,7 @@ func (t *Terminal) Run(cmd *exec.Cmd) error { } } }() - err := t.term.Run(cmd, uint16(h), uint16(w)) + err := t.term.Run(cmd, uint16(h), uint16(w), attr) if err != nil { return err } diff --git a/termutil/terminal.go b/termutil/terminal.go index 9514eff3f382..78ada1378973 100644 --- a/termutil/terminal.go +++ b/termutil/terminal.go @@ -7,7 +7,7 @@ import ( "io" "os" "os/exec" - "time" + "syscall" "github.com/creack/pty" "golang.org/x/term" @@ -116,12 +116,17 @@ func (t *Terminal) SetSize(rows, cols uint16) error { } // Run starts the terminal/shell proxying process -func (t *Terminal) Run(c *exec.Cmd, rows uint16, cols uint16) error { +func (t *Terminal) Run(c *exec.Cmd, rows uint16, cols uint16, attr *syscall.SysProcAttr) error { c.Env = append(os.Environ(), "TERM=xterm-256color") // Start the command with a pty. var err error - t.pty, err = pty.Start(c) + // t.pty, err = pty.Start(c) + winsize := pty.Winsize{ + Cols: cols, + Rows: rows, + } + t.pty, err = pty.StartWithAttrs(c, &winsize, &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 1}) if err != nil { return err } -- 2.37.3