turbo-editors/turbo-corepublic Fork 0
b1c5e36e1d1a12e805a39c391e69028b649e6a87
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

view.go · 191 lines · 6.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g yesterday1package acp
2
3import (
4 "strings"
5 "time"
6
📦 Turbo Core f3ade8d k33g yesterday7 "rickub.com/turbo-editors/turbo-core/syntax"
8 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g yesterday9)
10
11// InputHeight is how many rows the box you type into takes, not counting the
12// rule above it. Three is enough for a sentence that wrapped once, and leaves
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago13// the conversation the rest; a longer prompt scrolls inside the box.
🛟 Updated. 28d5985 k33g yesterday14const InputHeight = 3
15
16// View is the inside of an agent window: the conversation above, a box to type
17// in below.
18//
19// It draws from whatever the session's transcript says at that moment and
20// keeps no copy, so a reply arriving on the connection's goroutine shows up on
21// the next turn of the event loop with nothing to keep in step.
22//
23// view := acp.NewView(session)
24// window := ui.NewWindow(session.Agent().Name, view)
25type View struct {
26 ui.Box
27
28 session *Session
29
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago30 // input is what has been typed but not sent, as lines — the ones Alt-Enter
31 // makes. How they wrap to the pane is worked out when drawing (input.go).
🛟 Updated. 28d5985 k33g yesterday32 input []string
33 cursor int // which line the cursor is on
34 column int // and where in it, in runes
35
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago36 // pastes is the text kept aside for each token in the box (paste.go), and
37 // pasteCount numbers them for the life of the window, so that "#1" in an
38 // earlier exchange is not the same thing as "#1" in this one.
39 pastes []Paste
40 pasteCount int
41
🛟 Updated. 28d5985 k33g yesterday42 // scroll is how many laid-out lines are hidden above the top of the
43 // conversation, and follow says to keep the last line in sight as the
44 // agent writes.
45 scroll int
46 follow bool
47
48 // onTranscript has the focus when false. Two panes and one Tab between
49 // them is enough of a focus model for a window with two things in it.
50 onInput bool
51 focused bool
52
53 // laidOut is the conversation as it was last drawn, kept so that a click
54 // and a scroll agree with what is on the screen.
55 laidOut int
56
57 // caret is the line of the conversation the cursor is on, and anchor the
58 // other end of the selection, or noSelection. Both are indices into the
59 // laid-out lines, which are re-wrapped at the current width every frame —
60 // so a resize moves them, which is the same bargain the scroll position
61 // already makes.
62 caret int
63 anchor int
64
65 // dragging says a selection is being made with the mouse held down.
66 dragging bool
67
68 // OnCopy is given the text to put on the clipboard. It is a field rather
69 // than a parameter because nothing here starts a goroutine that reads it,
70 // and because what a clipboard *is* belongs to the editor around this
71 // window rather than to the window.
72 OnCopy func(text string)
73
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago74 // OnPaste returns what the editor's clipboard holds, for Ctrl-V and
75 // Shift-Ins. Same shape as OnCopy, for the same reasons; nil means those
76 // keys paste nothing. A paste from outside the editor does not come this
77 // way: the terminal brackets it and the editor hands it to Paste whole.
78 OnPaste func() string
79
🛟 Updated. 28d5985 k33g yesterday80 // Files returns the project's files, for the picker "@" opens and for
81 // telling a mention from an email address when the prompt is sent. It is
82 // the editor's to supply because the editor knows where the project is;
83 // nil means "@" is only ever a character.
84 Files func() []Mention
85
86 // picker is the popup that opens over the conversation while a "/" or
87 // "@" word is being typed.
88 picker picker
89
90 // now is the clock, so that the spinner can be tested without waiting for
91 // it. Same reason editor.View and app.App have one.
92 now func() time.Time
93}
94
95// Span is a run of one colour on a line, re-exported so that a window drawing
96// a conversation need not import the syntax package for one type.
97type Span = syntax.Span
98
99// NewView returns the inside of a window onto a session.
100func NewView(session *Session) *View {
101 return &View{
102 session: session,
103 input: []string{""},
104 follow: true,
105 onInput: true,
106 anchor: noSelection,
107 now: time.Now,
108 }
109}
110
111// SetClock replaces the view's clock, so a test can say what the spinner shows
112// without waiting for a frame to come round.
113func (v *View) SetClock(now func() time.Time) { v.now = now }
114
115// LinesForTest returns the conversation as it is currently laid out.
116//
117// Exported for the same reason app.Tick and app.Render are: the laid-out lines
118// are what a caret index means, and a test that wanted to put the cursor on a
119// particular line would otherwise have to count them by hand and go stale the
120// moment the layout changed.
121func (v *View) LinesForTest() []Line { return v.lines() }
122
123// SetCaretForTest puts the transcript's cursor on a line, as clicking it would.
124func (v *View) SetCaretForTest(at int) { v.moveCaret(at, false) }
125
126// Session returns the conversation this view is showing.
127func (v *View) Session() *Session { return v.session }
128
129// SetFocused gives or takes the keyboard focus, which is what stops every open
130// window drawing a cursor.
131func (v *View) SetFocused(focused bool) { v.focused = focused }
132
133// Focused reports whether this view has the keyboard focus.
134func (v *View) Focused() bool { return v.focused }
135
136// CanFocus reports that this widget takes the keyboard.
137func (v *View) CanFocus() bool { return true }
138
139// Input returns what has been typed and not yet sent.
140func (v *View) Input() string { return strings.Join(v.input, "\n") }
141
142// SetInput replaces what has been typed, putting the cursor at the end.
143func (v *View) SetInput(text string) {
144 v.input = strings.Split(text, "\n")
145 v.cursor = len(v.input) - 1
146 v.column = len([]rune(v.input[v.cursor]))
147}
148
149// Send hands the input to the agent and clears the box.
150//
151// It does nothing with an empty box: Enter on a blank line is somebody
152// thinking, not a turn worth spending. A file named with "@" goes along as a
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago153// mention, so the agent is given the file and not merely its name; a paste
154// standing in the box as a token goes along as its text.
🛟 Updated. 28d5985 k33g yesterday155func (v *View) Send() {
156 text := strings.TrimSpace(v.Input())
157 if text == "" {
158 return
159 }
160
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago161 v.session.PromptWith(text, v.pastesIn(text), v.mentions(text)...)
🛟 Updated. 28d5985 k33g yesterday162 v.SetInput("")
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 11h ago163 v.pastes = nil
🛟 Updated. 28d5985 k33g yesterday164 v.follow = true
165}
166
167// transcriptHeight is how many rows the conversation gets, leaving room for
168// the rule and the box below it.
169func (v *View) transcriptHeight() int {
170 return max(v.Bounds().H-InputHeight-1, 0)
171}
172
173// transcriptRect is where the conversation is, in absolute coordinates, for
174// hit-testing a click.
175func (v *View) transcriptRect() ui.Rect {
176 bounds := v.Bounds()
177 return ui.Rect{X: bounds.X, Y: bounds.Y, W: bounds.W, H: v.transcriptHeight()}
178}
179
180// inputRect is where what you type is, in absolute coordinates.
181func (v *View) inputRect() ui.Rect {
182 bounds := v.Bounds()
183 top := bounds.Y + v.transcriptHeight() + 1
184 return ui.Rect{X: bounds.X, Y: top, W: bounds.W, H: max(bounds.Bottom()-top, 0)}
185}
186
187// lines lays the conversation out at the current width.
188func (v *View) lines() []Line {
189 width := max(v.Bounds().W-3, 1) // the margin, the scroll bar, and a space
190 return Lines(v.session.Entries(), v.session.AgentName(), width)
191}