forked from fyne-io/terminal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
term.go
189 lines (158 loc) · 4.13 KB
/
term.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package terminal
import (
"image/color"
"math"
"os"
"os/exec"
"sync"
"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/widget"
"github.com/creack/pty"
)
// Config is the state of a terminal, updated upon certain actions or commands.
// Use Terminal.OnConfigure hook to register for changes.
type Config struct {
Title string
Rows, Columns uint
}
// Terminal is a terminal widget that loads a shell and handles input/output.
type Terminal struct {
widget.BaseWidget
content *widget.TextGrid
config Config
listenerLock sync.Mutex
listeners []chan Config
pty *os.File
focused, bell bool
cursorRow, cursorCol int
cursorMoved func()
}
// AddListener registers a new outgoing channel that will have our Config sent each time it changes.
func (t *Terminal) AddListener(listener chan Config) {
t.listenerLock.Lock()
defer t.listenerLock.Unlock()
t.listeners = append(t.listeners, listener)
}
// RemoveListener de-registers a Config channel and closes it
func (t *Terminal) RemoveListener(listener chan Config) {
t.listenerLock.Lock()
defer t.listenerLock.Unlock()
for i, l := range t.listeners {
if l == listener {
if i < len(t.listeners)-1 {
t.listeners = append(t.listeners[:i], t.listeners[i+1:]...)
} else {
t.listeners = t.listeners[:i]
}
close(l)
return
}
}
}
// Resize is called when this terminal widget has been resized.
// It ensures that the virtual terminal is within the bounds of the widget.
func (t *Terminal) Resize(s fyne.Size) {
if s.Width == t.Size().Width && s.Height == t.Size().Height {
return
}
if s.Width < 20 { // not sure why we get tiny sizes
return
}
t.BaseWidget.Resize(s)
t.content.Resize(s)
cellSize := t.guessCellSize()
t.config.Columns = uint(math.Floor(float64(s.Width) / float64(cellSize.Width)))
t.config.Rows = uint(math.Floor(float64(s.Height) / float64(cellSize.Height)))
t.onConfigure()
t.updatePTYSize()
}
func (t *Terminal) updatePTYSize() {
scale := float32(1.0)
c := fyne.CurrentApp().Driver().CanvasForObject(t)
if c != nil {
scale = c.Scale()
}
_ = pty.Setsize(t.pty, &pty.Winsize{
Rows: uint16(t.config.Rows), Cols: uint16(t.config.Columns),
X: uint16(float32(t.Size().Width) * scale), Y: uint16(float32(t.Size().Height) * scale)})
}
func (t *Terminal) onConfigure() {
t.listenerLock.Lock()
for _, l := range t.listeners {
select {
case l <- t.config:
default:
// channel blocked, might be closed
}
}
t.listenerLock.Unlock()
}
func (t *Terminal) open() error {
shell := os.Getenv("SHELL")
if shell == "" {
shell = "bash"
}
env := os.Environ()
env = append(env, "TERM=xterm-256color")
c := exec.Command(shell)
c.Env = env
// Start the command with a pty.
handle, err := pty.Start(c)
if err != nil {
return err
}
t.pty = handle
t.updatePTYSize()
return nil
}
// Exit requests that this terminal exits.
// If there are embedded shells it will exit the child one only.
func (t *Terminal) Exit() {
_, _ = t.pty.Write([]byte("exit\n"))
}
func (t *Terminal) close() error {
return t.pty.Close()
}
// don't call often - should we cache?
func (t *Terminal) guessCellSize() fyne.Size {
cell := canvas.NewText("M", color.White)
cell.TextStyle.Monospace = true
return cell.MinSize()
}
func (t *Terminal) run() {
bufLen := 4069
buf := make([]byte, bufLen)
for {
num, err := t.pty.Read(buf)
if err != nil {
// this is the pre-go 1.13 way to check for the read failing (terminal closed)
if err.Error() == "EOF" {
break // term exit on macOS
} else if err, ok := err.(*os.PathError); ok && err.Err.Error() == "input/output error" {
break // broken pipe, terminal exit
}
fyne.LogError("pty read error", err)
}
t.handleOutput(buf[:num])
if num < bufLen {
t.Refresh()
}
}
}
// Run starts the terminal by loading a shell and starting to process the input/output
func (t *Terminal) Run() error {
err := t.open()
if err != nil {
return err
}
t.run()
return t.close()
}
// NewTerminal sets up a new terminal instance with the bash shell
func NewTerminal() *Terminal {
t := &Terminal{}
t.ExtendBaseWidget(t)
t.content = widget.NewTextGrid()
return t
}