c7ea86e8d166896ed606bd1462feb394006ffa22
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/go.mod b/go.mod
2index ddbb7eb0163378b3fdf980b12747b28778fa2c7e..9eb7802f39776007ca032c68ceed94919cecde05 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -3,6 +3,7 @@ module japanese
6 go 1.27.0
7
8 require (
9+ github.com/charmbracelet/bubbles v1.0.0
10 github.com/charmbracelet/bubbletea v1.3.10
11 github.com/charmbracelet/lipgloss v1.1.0
12 github.com/ebitengine/oto/v3 v3.5.0
13@@ -10,8 +11,8 @@ require (
14 )
15
16 require (
17+ github.com/atotto/clipboard v0.1.4 // indirect
18 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19- github.com/charmbracelet/bubbles v1.0.0 // indirect
20 github.com/charmbracelet/colorprofile v0.4.3 // indirect
21 github.com/charmbracelet/x/ansi v0.11.8 // indirect
22 github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
23diff --git a/go.sum b/go.sum
24index c2dccee5f8a38d04788a04044f117b61c301b275..ef3a9cb93d0b42814a9f101776b34d57ec245b9b 100644
25--- a/go.sum
26+++ b/go.sum
27@@ -1,3 +1,5 @@
28+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
29+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
30 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
31 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
32 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
33diff --git a/internal/ui/app.go b/internal/ui/app.go
34index 91f2d8ddc0122eb2cb38b88d99eb7f5753e5aba8..65a720d9665f149f7456ac72d912df14f06fc5be 100644
35--- a/internal/ui/app.go
36+++ b/internal/ui/app.go
37@@ -6,6 +6,8 @@ import (
38 "strings"
39 "time"
40
41+ "github.com/charmbracelet/bubbles/key"
42+ "github.com/charmbracelet/bubbles/textinput"
43 "github.com/charmbracelet/bubbletea"
44 "github.com/charmbracelet/lipgloss"
45
46@@ -50,17 +52,22 @@ type model struct {
47 beginning bool
48 recSince time.Time
49
50- input string
51+ input textinput.Model
52 status string
53 height int
54 width int
55 }
56
57 func NewModel(state *game.State, orch *game.Orchestrator, recordCapSec int) *model {
58+ ti := textinput.New()
59+ // Ghostty encodes ctrl+backspace as 0x08, which bubbletea reports as "ctrl+h".
60+ ti.KeyMap.DeleteWordBackward = key.NewBinding(key.WithKeys("ctrl+w", "ctrl+h", "alt+backspace"))
61+ ti.Focus()
62 return &model{
63 state: state,
64 orch: orch,
65 recordCapSec: recordCapSec,
66+ input: ti,
67 height: 24,
68 }
69 }
70@@ -109,37 +116,40 @@ func (m *model) handleKey(k tea.KeyMsg) tea.Cmd {
71 m.state.RevealEnglish()
72 }
73 case tea.KeyEnter:
74- if m.phase == phaseIdle && m.input != "" {
75- if strings.HasPrefix(m.input, "?") {
76- question := strings.TrimSpace(strings.TrimPrefix(m.input, "?"))
77- if question == "" {
78- return nil // bare "?": keep the buffer
79- }
80- }
81- action := m.input
82- m.input = ""
83- m.phase = phaseProcessing
84- if strings.HasPrefix(action, "?") {
85- return askTurn(m.orch, action)
86- }
87- return actionTurn(m.orch, action)
88- }
89- case tea.KeyBackspace:
90- if m.phase != phaseRecording {
91- m.input = chopRune(m.input)
92- }
93- case tea.KeyRunes:
94- if m.phase != phaseRecording {
95- m.input += string(k.Runes)
96- }
97- case tea.KeySpace:
98+ return m.submit()
99+ default:
100 if m.phase != phaseRecording {
101- m.input += " "
102+ var cmd tea.Cmd
103+ m.input, cmd = m.input.Update(k)
104+ return cmd
105 }
106 }
107 return nil
108 }
109
110+// submit sends the input buffer as an action or scratch question.
111+func (m *model) submit() tea.Cmd {
112+ if m.phase != phaseIdle {
113+ return nil
114+ }
115+ action := m.input.Value()
116+ if action == "" {
117+ return nil
118+ }
119+ if strings.HasPrefix(action, "?") {
120+ question := strings.TrimSpace(strings.TrimPrefix(action, "?"))
121+ if question == "" {
122+ return nil // bare "?": keep the buffer
123+ }
124+ }
125+ m.input.SetValue("")
126+ m.phase = phaseProcessing
127+ if strings.HasPrefix(action, "?") {
128+ return askTurn(m.orch, action)
129+ }
130+ return actionTurn(m.orch, action)
131+}
132+
133 // talkToggle starts or stops push-to-talk.
134 func (m *model) talkToggle() tea.Cmd {
135 switch m.phase {
136@@ -196,7 +206,8 @@ func (m *model) View() string {
137diff --git a/internal/ui/render.go b/internal/ui/render.go
138index 48b9ad31c7e8da98654f480147ef0fb4e574764b..24015192b0fa598893665aabe6965c12044d0348 100644
139--- a/internal/ui/render.go
140+++ b/internal/ui/render.go
141@@ -16,6 +16,7 @@ const controlsHint = "F2 talk · F3 romaji · F4 english · ? ask · Esc quit"
142 type viewState struct {
143 logLines []string
144 input string
145+ cursor int
146 status string
147 phaseWord string
148 recElapsed int
149@@ -31,7 +32,7 @@ func render(vs viewState) string {
150 lines = append(lines, vs.logLines...)
151 }
152 lines = append(lines, "")
153- lines = append(lines, inputLine(vs.input))
154+ lines = append(lines, inputLine(vs.input, vs.cursor))
155 lines = append(lines, statusLine(vs.status, vs.phaseWord, vs.recElapsed, vs.recCap))
156 if len(lines) > vs.height && vs.height > 0 {
157 lines = lines[len(lines)-vs.height:]
158@@ -39,8 +40,12 @@ func render(vs viewState) string {
159 return strings.Join(lines, "\n")
160 }
161
162-func inputLine(buf string) string {
163- return "> " + sanitizeRomajiOnly(buf) + "▌"
164+func inputLine(buf string, cursor int) string {
165+ r := []rune(buf)
166+ if cursor > len(r) {
167+ cursor = len(r)
168+ }
169+ return "> " + sanitizeRomajiOnly(string(r[:cursor])) + "▌" + sanitizeRomajiOnly(string(r[cursor:]))
170 }
171
172 func statusLine(status, phaseWord string, recElapsed, recCap int) string {