~ghost08/tcell-term

tcell-term: A bunch of patches v1 PROPOSED

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
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH tcell-term 01/22] cursor: externalize cursor placement Export this patch

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

[PATCH tcell-term 02/22] api: simplify draw method and require views.View Export this patch

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

[PATCH tcell-term 03/22] api: rename Event to HandleEvent Export this patch

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

[PATCH tcell-term 04/22] api: change Resize arguments Export this patch

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

[PATCH tcell-term 05/22] examples: ignore Export this patch

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

[PATCH tcell-term 06/22] terminal: rename vp to view Export this patch

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

[PATCH tcell-term 07/22] api: add size and Watch functions Export this patch

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

[PATCH tcell-term 08/22] api: internalize redraw chan Export this patch

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

[PATCH tcell-term 09/22] example_simple: fix Export this patch

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

[PATCH tcell-term 10/22] chore: remove old comment Export this patch

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

[PATCH tcell-term 11/22] cell: add dirty methods and prop Export this patch

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

[PATCH tcell-term 12/22] cells: set dirty when cell changes Export this patch

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

[PATCH tcell-term 13/22] terminal: don't require view and screen at New Export this patch

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

[PATCH tcell-term 14/22] terminal: BREAKING: use a redraw poll instead of channel Export this patch

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

[PATCH tcell-term 15/22] scrollable: fix delete and insert lines operations Export this patch

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

[PATCH tcell-term 16/22] csi: use cell.setStyle instead of assigning directly Export this patch

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

[PATCH tcell-term 17/22] chore: remove unused code Export this patch

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

[PATCH tcell-term 18/22] perf: only redraw cell if dirty Export this patch

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

[PATCH tcell-term 19/22] view: remove unused screen calls and prevent panics Export this patch

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

[PATCH tcell-term 20/22] chore: gofumpt Export this patch

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

[PATCH tcell-term 21/22] example: replace main example and remove _simple Export this patch

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

[PATCH tcell-term 22/22] run: allow running with custom SysProcAttr Export this patch

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