@@ -3,7 +3,10 @@ package cliui
3
3
import (
4
4
"flag"
5
5
"fmt"
6
+ "os"
7
+ "os/signal"
6
8
"strings"
9
+ "syscall"
7
10
8
11
"github.com/charmbracelet/bubbles/textinput"
9
12
tea "github.com/charmbracelet/bubbletea"
@@ -16,6 +19,36 @@ import (
16
19
17
20
const defaultSelectModelHeight = 7
18
21
22
+ type terminateMsg struct {}
23
+
24
+ func installSignalHandler (p * tea.Program ) func () {
25
+ ch := make (chan struct {})
26
+
27
+ go func () {
28
+ sig := make (chan os.Signal , 1 )
29
+ signal .Notify (sig , os .Interrupt , syscall .SIGTERM )
30
+
31
+ defer func () {
32
+ signal .Stop (sig )
33
+ close (ch )
34
+ }()
35
+
36
+ for {
37
+ select {
38
+ case <- ch :
39
+ return
40
+
41
+ case <- sig :
42
+ p .Send (terminateMsg {})
43
+ }
44
+ }
45
+ }()
46
+
47
+ return func () {
48
+ ch <- struct {}{}
49
+ }
50
+ }
51
+
19
52
type SelectOptions struct {
20
53
Options []string
21
54
// Default will be highlighted first if it's a valid option.
@@ -92,12 +125,18 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
92
125
initialModel .search .Prompt = ""
93
126
initialModel .search .Focus ()
94
127
95
- m , err := tea .NewProgram (
128
+ p := tea .NewProgram (
96
129
initialModel ,
130
+ tea .WithoutSignalHandler (),
97
131
tea .WithContext (inv .Context ()),
98
132
tea .WithInput (inv .Stdin ),
99
133
tea .WithOutput (inv .Stdout ),
100
- ).Run ()
134
+ )
135
+
136
+ closeSignalHandler := installSignalHandler (p )
137
+ defer closeSignalHandler ()
138
+
139
+ m , err := p .Run ()
101
140
if err != nil {
102
141
return "" , err
103
142
}
@@ -126,14 +165,19 @@ type selectModel struct {
126
165
}
127
166
128
167
func (selectModel ) Init () tea.Cmd {
129
- return textinput . Blink
168
+ return nil
130
169
}
131
170
132
171
//nolint:revive // The linter complains about modifying 'm' but this is typical practice for bubbletea
133
172
func (m selectModel ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
134
173
var cmd tea.Cmd
135
174
136
- if msg , ok := msg .(tea.KeyMsg ); ok {
175
+ switch msg := msg .(type ) {
176
+ case terminateMsg :
177
+ m .canceled = true
178
+ return m , tea .Quit
179
+
180
+ case tea.KeyMsg :
137
181
switch msg .Type {
138
182
case tea .KeyCtrlC :
139
183
m .canceled = true
@@ -292,12 +336,18 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er
292
336
initialModel .search .Prompt = ""
293
337
initialModel .search .Focus ()
294
338
295
- m , err := tea .NewProgram (
339
+ p := tea .NewProgram (
296
340
initialModel ,
341
+ tea .WithoutSignalHandler (),
297
342
tea .WithContext (inv .Context ()),
298
343
tea .WithInput (inv .Stdin ),
299
344
tea .WithOutput (inv .Stdout ),
300
- ).Run ()
345
+ )
346
+
347
+ closeSignalHandler := installSignalHandler (p )
348
+ defer closeSignalHandler ()
349
+
350
+ m , err := p .Run ()
301
351
if err != nil {
302
352
return nil , err
303
353
}
@@ -336,7 +386,12 @@ func (multiSelectModel) Init() tea.Cmd {
336
386
func (m multiSelectModel ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
337
387
var cmd tea.Cmd
338
388
339
- if msg , ok := msg .(tea.KeyMsg ); ok {
389
+ switch msg := msg .(type ) {
390
+ case terminateMsg :
391
+ m .canceled = true
392
+ return m , tea .Quit
393
+
394
+ case tea.KeyMsg :
340
395
switch msg .Type {
341
396
case tea .KeyCtrlC :
342
397
m .canceled = true
@@ -353,6 +408,9 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
353
408
if len (options ) != 0 {
354
409
options [m .cursor ].chosen = ! options [m .cursor ].chosen
355
410
}
411
+ // We back out early here otherwise a space will be inserted
412
+ // into the search field.
413
+ return m , nil
356
414
357
415
case tea .KeyUp :
358
416
options := m .filteredOptions ()
0 commit comments