Skip to content

Commit ff2a005

Browse files
chore: try to reduce flickering
1 parent ecddbcf commit ff2a005

File tree

5 files changed

+110
-27
lines changed

5 files changed

+110
-27
lines changed

area.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package tui
2+
3+
import (
4+
"os"
5+
"strings"
6+
7+
"atomicgo.dev/cursor"
8+
)
9+
10+
type area struct {
11+
content string
12+
cursor *cursor.Area
13+
ignore bool
14+
}
15+
16+
func (a *area) Write(p []byte) (n int, err error) {
17+
if a.ignore {
18+
return len(p), nil
19+
}
20+
return os.Stdout.Write(p)
21+
}
22+
23+
func (a *area) Fd() uintptr {
24+
return os.Stdout.Fd()
25+
}
26+
27+
func (a *area) Finish(text string) {
28+
a.Update(text)
29+
cursor.Show()
30+
}
31+
32+
func (a *area) Update(text string) {
33+
if a.cursor == nil {
34+
c := cursor.NewArea().WithWriter(a)
35+
a.cursor = &c
36+
cursor.Hide()
37+
}
38+
39+
if a.content == text {
40+
return
41+
}
42+
43+
defer func() {
44+
a.ignore = false
45+
a.content = text
46+
}()
47+
48+
if rest, ok := strings.CutPrefix(text, a.content); ok {
49+
// Just write suffix
50+
_, _ = os.Stdout.Write([]byte(rest))
51+
a.ignore = true
52+
a.cursor.Update(text)
53+
} else {
54+
a.cursor.Update(text)
55+
}
56+
}

area_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package tui
2+
3+
import (
4+
"encoding/base64"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestControl(t *testing.T) {
13+
data := "ChtbMzg7NTsyNTJtG1swbRtbMzg7NTsyNTJtG1swbSAgG1szODs1OzI1Mm1XYWl0aW5nIGZvciBtb2RlbBtbMG0bWzM4OzU7MjUybSByZXNwb25zZS4uLhtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbRtbMzg7NTsyNTJtIBtbMG0bWzM4OzU7MjUybSAbWzBtG1szODs1OzI1Mm0gG1swbQoK"
14+
s, err := base64.StdEncoding.DecodeString(data)
15+
require.NoError(t, err)
16+
17+
str := string(s)
18+
x := stripControl.ReplaceAllString(str, "")
19+
assert.NotEqual(t, str, x)
20+
assert.True(t, strings.HasPrefix(str, x))
21+
22+
suffix, ok := strings.CutPrefix(str, x)
23+
assert.True(t, ok)
24+
assert.True(t, len(suffix) > 0)
25+
assert.Len(t, suffix, 2438)
26+
assert.Equal(t, "\n\x1b[38;5;252m\x1b[0m\x1b[38;5;252m\x1b[0m \x1b[38;5;252mWaiting for model\x1b[0m\x1b[38;5;252m response...", x)
27+
}

display.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,35 @@
11
package tui
22

33
import (
4+
"regexp"
45
"strings"
56
"time"
67

78
"github.com/pterm/pterm"
89
)
910

11+
var (
12+
stripControl = regexp.MustCompile("( ?\x1b\\[[0-9;]+m ?)+\n+$")
13+
defaultDuration = 200 * time.Millisecond
14+
)
15+
1016
type display struct {
11-
area *pterm.AreaPrinter
17+
area area
1218
prompter *prompter
1319
last time.Time
1420
lastDuration time.Duration
1521
stopped bool
1622
}
1723

1824
func newDisplay(tool string) (*display, error) {
19-
area, err := pterm.DefaultArea.Start()
20-
if err != nil {
21-
return nil, err
22-
}
23-
2425
prompter, err := newReadlinePrompter(tool)
2526
if err != nil {
2627
return nil, err
2728
}
2829

2930
return &display{
30-
area: area,
31-
prompter: prompter,
31+
prompter: prompter,
32+
lastDuration: defaultDuration,
3233
}, nil
3334
}
3435

@@ -44,7 +45,7 @@ func (a *display) setMultiLinePrompt(text string) {
4445
lines := strings.Split(text, "\n")
4546
a.prompter.SetPrompt(lines[len(lines)-1])
4647
if len(lines) > 1 {
47-
a.area.Update(a.area.GetContent() + "\n" + strings.Join(lines[:len(lines)-1], "\n") + "\n")
48+
a.area.Update(a.area.content + "\n" + strings.Join(lines[:len(lines)-1], "\n") + "\n")
4849
}
4950
}
5051

@@ -80,15 +81,17 @@ func (a *display) Prompt(text string) (string, bool, error) {
8081
}
8182

8283
func (a *display) Progress(text string) error {
84+
if text == "" {
85+
return nil
86+
}
87+
88+
text = stripControl.ReplaceAllString(text, "")
89+
8390
if a.stopped {
84-
area, err := pterm.DefaultArea.Start()
85-
if err != nil {
86-
return err
87-
}
88-
a.area = area
91+
a.area = area{}
8992
a.stopped = false
9093
a.last = time.Time{}
91-
a.lastDuration = 200 * time.Millisecond
94+
a.lastDuration = defaultDuration
9295
}
9396

9497
start := time.Now()
@@ -99,15 +102,13 @@ func (a *display) Progress(text string) error {
99102
lines = lines[len(lines)-height:]
100103
}
101104
newText := strings.Join(lines, "\n")
102-
if a.area.GetContent() != newText {
103-
a.area.Update(newText)
104-
}
105+
a.area.Update(newText)
105106
done := time.Now()
106107
delta := done.Sub(start)
107108
if delta > a.lastDuration {
108109
a.lastDuration = delta
109110
}
110-
a.last = start
111+
a.last = done
111112
}
112113

113114
return nil
@@ -117,11 +118,10 @@ func (a *display) Close() error {
117118
return a.prompter.Close()
118119
}
119120

120-
func (a *display) Finished(text string) error {
121+
func (a *display) Finished(text string) {
121122
if !strings.HasSuffix(text, "\n") {
122123
text += "\n"
123124
}
124125
a.stopped = true
125-
a.area.Update(text)
126-
return a.area.Stop()
126+
a.area.Finish(text)
127127
}

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/gptscript-ai/tui
33
go 1.22.3
44

55
require (
6+
atomicgo.dev/cursor v0.2.0
67
github.com/adrg/xdg v0.4.0
78
github.com/charmbracelet/glamour v0.7.0
89
github.com/charmbracelet/lipgloss v0.11.0
@@ -11,18 +12,19 @@ require (
1112
github.com/gptscript-ai/go-gptscript v0.9.3-0.20240715172623-8176fb20c5cb
1213
github.com/pterm/pterm v0.12.79
1314
github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e
15+
github.com/stretchr/testify v1.8.4
1416
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
1517
)
1618

1719
require (
18-
atomicgo.dev/cursor v0.2.0 // indirect
1920
atomicgo.dev/keyboard v0.2.9 // indirect
2021
atomicgo.dev/schedule v0.1.0 // indirect
2122
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
2223
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2324
github.com/aymerick/douceur v0.2.0 // indirect
2425
github.com/charmbracelet/x/ansi v0.1.1 // indirect
2526
github.com/containerd/console v1.0.4 // indirect
27+
github.com/davecgh/go-spew v1.1.1 // indirect
2628
github.com/dlclark/regexp2 v1.4.0 // indirect
2729
github.com/getkin/kin-openapi v0.124.0 // indirect
2830
github.com/go-openapi/jsonpointer v0.20.2 // indirect
@@ -43,6 +45,7 @@ require (
4345
github.com/muesli/termenv v0.15.2 // indirect
4446
github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 // indirect
4547
github.com/perimeterx/marshmallow v1.1.5 // indirect
48+
github.com/pmezard/go-difflib v1.0.0 // indirect
4649
github.com/rivo/uniseg v0.4.7 // indirect
4750
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4851
github.com/yuin/goldmark v1.5.4 // indirect

run.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,7 @@ func Run(ctx context.Context, tool string, opts ...RunOptions) error {
252252
text = "Interrupted\n\n"
253253
}
254254

255-
err = ui.Finished(text)
256-
if err != nil {
257-
return err
258-
}
255+
ui.Finished(text)
259256

260257
if opt.SaveChatStateFile != "" {
261258
if run.State() == gptscript.Finished {

0 commit comments

Comments
 (0)