main ece7682

terminal.go RawHistoryFolder

1// Package webterm connects the ordinary client terminal boundary to a browser.
2package webterm
3
4import (
5	"context"
6	"errors"
7	"io"
8	"sync"
9
10	vt "github.com/bnema/vev-vt"
11	renderer "github.com/bnema/vev-vt/core"
12	"github.com/bnema/vev/internal/adapters/term"
13	"github.com/bnema/vev/internal/domain"
14	"github.com/bnema/vev/internal/ports"
15)
16
17const (
18	MaxColumns      = 512
19	MaxRows         = 256
20	inputQueueDepth = 64
21)
22
23// Terminal owns a VT mirror of successfully flushed client output. Input and
24// terminal query replies share one ordered writer, just like a physical TTY.
25type Terminal struct {
26	mu       sync.Mutex
27	wheelAcc float64
28	screen   *vt.Screen
29	geometry domain.Geometry
30	latest   vt.ScreenSnapshot
31	changes  chan struct{}
32	resize   chan domain.Geometry
33	input    chan []byte
34	reader   *io.PipeReader
35	writer   *io.PipeWriter
36	cancel   context.CancelFunc
37	done     chan struct{}
38	closed   bool
39}
40
41var _ ports.Terminal = (*Terminal)(nil)
42
43func New(ctx context.Context, geometry domain.Geometry) (*Terminal, error) {
44	if geometry.Cols < 1 || geometry.Rows < 1 || geometry.Cols > MaxColumns || geometry.Rows > MaxRows {
45		return nil, errors.New("webterm: invalid geometry")
46	}
47	ctx, cancel := context.WithCancel(ctx)
48	reader, writer := io.Pipe()
49	t := &Terminal{screen: vt.NewScreen(geometry.Cols, geometry.Rows), geometry: geometry, changes: make(chan struct{}, 1), resize: make(chan domain.Geometry, 1), input: make(chan []byte, inputQueueDepth), reader: reader, writer: writer, cancel: cancel, done: make(chan struct{})}
50	t.screen.OnResponse = func(data []byte) {
51		select {
52		case t.input <- append([]byte(nil), data...):
53		default:
54			cancel()
55		}
56	}
57	// Match #terminal's default colors in app.css so the ordinary theme
58	// discovery enables native inactive-pane and modal-backdrop dimming.
59	t.screen.SetDefaultColors(renderer.RGB{R: 216, G: 216, B: 216}, renderer.RGB{R: 16, G: 16, B: 16}, true)
60	t.latest = t.screen.Snapshot()
61	go func() {
62		defer close(t.done)
63		stop := context.AfterFunc(ctx, func() { _ = writer.CloseWithError(ctx.Err()); _ = reader.CloseWithError(ctx.Err()) })
64		defer stop()
65		for {
66			select {
67			case <-ctx.Done():
68				return
69			case data := <-t.input:
70				if _, err := writer.Write(data); err != nil {
71					return
72				}
73			}
74		}
75	}()
76	return t, nil
77}
78
79// EnableKittyKeyboard is a no-op: a virtual terminal has no outer keyboard.
80func (t *Terminal) EnableKittyKeyboard(int) error { return nil }
81
82func (t *Terminal) EnterRaw() (func() error, error) {
83	if _, err := t.Write(term.VisualEnterSequence()); err != nil {
84		return nil, err
85	}
86	if err := t.Flush(); err != nil {
87		return nil, err
88	}
89	return func() error { return nil }, nil
90}
91func (t *Terminal) In() io.Reader                        { return t.reader }
92func (t *Terminal) Out() io.Writer                       { return t }
93func (t *Terminal) ResizeEvents() <-chan domain.Geometry { return t.resize }
94func (t *Terminal) Changes() <-chan struct{}             { return t.changes }
95func (t *Terminal) Geometry() (domain.Geometry, error) {
96	t.mu.Lock()
97	defer t.mu.Unlock()
98	if t.closed {
99		return domain.Geometry{}, io.ErrClosedPipe
100	}
101	return t.geometry, nil
102}
103func (t *Terminal) Write(data []byte) (int, error) {
104	t.mu.Lock()
105	defer t.mu.Unlock()
106	if t.closed {
107		return 0, io.ErrClosedPipe
108	}
109	t.screen.Write(data)
110	return len(data), nil
111}
112func (t *Terminal) Flush() error {
113	t.mu.Lock()
114	defer t.mu.Unlock()
115	if t.closed {
116		return io.ErrClosedPipe
117	}
118	t.latest = t.screen.Snapshot()
119	select {
120	case t.changes <- struct{}{}:
121	default:
122	}
123	return nil
124}
125func (t *Terminal) Snapshot() vt.ScreenSnapshot {
126	t.mu.Lock()
127	defer t.mu.Unlock()
128	return t.latest
129}
130func (t *Terminal) Resize(geometry domain.Geometry) error {
131	if geometry.Cols < 1 || geometry.Rows < 1 || geometry.Cols > MaxColumns || geometry.Rows > MaxRows {
132		return errors.New("webterm: invalid geometry")
133	}
134	t.mu.Lock()
135	defer t.mu.Unlock()
136	if t.closed {
137		return io.ErrClosedPipe
138	}
139	if t.geometry == geometry {
140		return nil
141	}
142	t.geometry = geometry
143	t.screen.SetGeometry(vt.Geometry{Cols: geometry.Cols, Rows: geometry.Rows, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
144	select {
145	case <-t.resize:
146	default:
147	}
148	t.resize <- geometry
149	return nil
150}
151func (t *Terminal) Send(ctx context.Context, data []byte) error {
152	if len(data) == 0 {
153		return nil
154	}
155	select {
156	case <-ctx.Done():
157		return ctx.Err()
158	case <-t.done:
159		return io.ErrClosedPipe
160	case t.input <- append([]byte(nil), data...):
161		return nil
162	default:
163		return errors.New("webterm: input queue full")
164	}
165}
166func (t *Terminal) Close() {
167	t.mu.Lock()
168	t.closed = true
169	t.mu.Unlock()
170	t.cancel()
171	_ = t.reader.Close()
172	_ = t.writer.Close()
173	<-t.done
174}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close