Skip to content

Commit f598836

Browse files
chore: handle signals ourself instead of bubbletea
1 parent 7784110 commit f598836

File tree

1 file changed

+65
-7
lines changed

1 file changed

+65
-7
lines changed

cli/cliui/select.go

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package cliui
33
import (
44
"flag"
55
"fmt"
6+
"os"
7+
"os/signal"
68
"strings"
9+
"syscall"
710

811
"github.com/charmbracelet/bubbles/textinput"
912
tea "github.com/charmbracelet/bubbletea"
@@ -16,6 +19,36 @@ import (
1619

1720
const defaultSelectModelHeight = 7
1821

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+
1952
type SelectOptions struct {
2053
Options []string
2154
// Default will be highlighted first if it's a valid option.
@@ -92,12 +125,18 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
92125
initialModel.search.Prompt = ""
93126
initialModel.search.Focus()
94127

95-
m, err := tea.NewProgram(
128+
p := tea.NewProgram(
96129
initialModel,
130+
tea.WithoutSignalHandler(),
97131
tea.WithContext(inv.Context()),
98132
tea.WithInput(inv.Stdin),
99133
tea.WithOutput(inv.Stdout),
100-
).Run()
134+
)
135+
136+
closeSignalHandler := installSignalHandler(p)
137+
defer closeSignalHandler()
138+
139+
m, err := p.Run()
101140
if err != nil {
102141
return "", err
103142
}
@@ -126,14 +165,19 @@ type selectModel struct {
126165
}
127166

128167
func (selectModel) Init() tea.Cmd {
129-
return textinput.Blink
168+
return nil
130169
}
131170

132171
//nolint:revive // The linter complains about modifying 'm' but this is typical practice for bubbletea
133172
func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134173
var cmd tea.Cmd
135174

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:
137181
switch msg.Type {
138182
case tea.KeyCtrlC:
139183
m.canceled = true
@@ -292,12 +336,18 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er
292336
initialModel.search.Prompt = ""
293337
initialModel.search.Focus()
294338

295-
m, err := tea.NewProgram(
339+
p := tea.NewProgram(
296340
initialModel,
341+
tea.WithoutSignalHandler(),
297342
tea.WithContext(inv.Context()),
298343
tea.WithInput(inv.Stdin),
299344
tea.WithOutput(inv.Stdout),
300-
).Run()
345+
)
346+
347+
closeSignalHandler := installSignalHandler(p)
348+
defer closeSignalHandler()
349+
350+
m, err := p.Run()
301351
if err != nil {
302352
return nil, err
303353
}
@@ -336,7 +386,12 @@ func (multiSelectModel) Init() tea.Cmd {
336386
func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
337387
var cmd tea.Cmd
338388

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:
340395
switch msg.Type {
341396
case tea.KeyCtrlC:
342397
m.canceled = true
@@ -353,6 +408,9 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
353408
if len(options) != 0 {
354409
options[m.cursor].chosen = !options[m.cursor].chosen
355410
}
411+
// We back out early here otherwise a space will be inserted
412+
// into the search field.
413+
return m, nil
356414

357415
case tea.KeyUp:
358416
options := m.filteredOptions()

0 commit comments

Comments
 (0)