diff --git a/strix/interface/tui/internal/app/model.go b/strix/interface/tui/internal/app/model.go index e7cc8975e..771b4165e 100644 --- a/strix/interface/tui/internal/app/model.go +++ b/strix/interface/tui/internal/app/model.go @@ -384,7 +384,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.vulnerabilityCopyError = msg.err.Error() } return m, nil + case tea.ResumeMsg: + // Suspend turns mouse tracking off with the rest of the terminal state, + // but the restore brings back only the alt screen, so turn it back on. + return m, tea.EnableMouseCellMotion case tea.KeyMsg: + // Raw mode clears ISIG, so ctrl+z arrives as a key instead of SIGTSTP. + // Suspend on every screen, the way a shell job would. + if msg.Type == tea.KeyCtrlZ { + return m, tea.Suspend + } if m.showSplash { switch msg.String() { case "ctrl+c", "ctrl+q", "q", "esc": diff --git a/strix/interface/tui/internal/app/model_test.go b/strix/interface/tui/internal/app/model_test.go index 92452e2ef..5b0d0cb53 100644 --- a/strix/interface/tui/internal/app/model_test.go +++ b/strix/interface/tui/internal/app/model_test.go @@ -1439,3 +1439,31 @@ func TestNarrowTerminalKeepsTheFrameIntact(t *testing.T) { } } } + +func TestCtrlZSuspendsFromEveryScreen(t *testing.T) { + for name, prepare := range map[string]func(*Model){ + "splash": func(m *Model) { m.showSplash = true }, + "modal": func(m *Model) { m.showSplash = false; m.openModal(modalHelp) }, + "main": func(m *Model) { m.showSplash = false }, + } { + model := New(nil) + prepare(&model) + _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyCtrlZ}) + if cmd == nil { + t.Fatalf("%s: ctrl+z returned no command", name) + } + if _, ok := cmd().(tea.SuspendMsg); !ok { + t.Fatalf("%s: ctrl+z did not suspend", name) + } + } +} + +func TestResumeReenablesMouse(t *testing.T) { + _, cmd := New(nil).Update(tea.ResumeMsg{}) + if cmd == nil { + t.Fatal("resume returned no command") + } + if msg := cmd(); msg != tea.EnableMouseCellMotion() { + t.Fatalf("resume did not re-enable mouse tracking: %#v", msg) + } +}