diff --git a/README.md b/README.md
index 41eb10e5..cddd2490 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ claws -l debug.log
| `P` | Select AWS profile(s) (multi-select supported) |
| `?` | Show help |
| `Esc` | Go back |
-| `Ctrl+c` | Quit |
+| `q` / `Ctrl+c` | Quit |
### Mouse Support
@@ -219,6 +219,7 @@ Selected profiles are queried in parallel; resources display with Profile and Ac
| Command | Action |
|---------|--------|
+| `:q` / `:quit` | Quit |
| `:login [name]` | AWS console login (default: `claws-login` profile) |
| `:ec2/instances` | Navigate to EC2 instances |
| `:sort
` | Sort by column |
diff --git a/internal/app/app.go b/internal/app/app.go
index 6c952996..d6c746ef 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -114,7 +114,7 @@ func (a *App) Init() tea.Cmd {
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.showWarnings && a.warningsReady {
if keyMsg, ok := msg.(tea.KeyPressMsg); ok {
- if keyMsg.Code == tea.KeyEnter || keyMsg.String() == " " {
+ if keyMsg.Code == tea.KeyEnter || keyMsg.String() == "space" || keyMsg.String() == "q" {
a.showWarnings = false
return a, nil
}
@@ -162,10 +162,9 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.commandInput.SetWidth(msg.Width)
// Update cached styles with new width
a.styles = newAppStyles(msg.Width)
- // Mark warnings as ready to be dismissed after first window size (terminal init complete)
- if a.showWarnings && !a.warningsReady {
- a.warningsReady = true
- }
+ // Mark warnings ready after first WindowSizeMsg (terminal initialized).
+ // Safe to set unconditionally - only affects dismissal when showWarnings is true.
+ a.warningsReady = true
if a.currentView != nil {
return a, a.currentView.SetSize(msg.Width, msg.Height-2)
}
@@ -203,6 +202,14 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, a.keys.Quit):
+ switch a.currentView.(type) {
+ case *view.DetailView, *view.DiffView:
+ if len(a.viewStack) > 0 {
+ a.currentView = a.viewStack[len(a.viewStack)-1]
+ a.viewStack = a.viewStack[:len(a.viewStack)-1]
+ return a, a.currentView.SetSize(a.width, a.height-2)
+ }
+ }
return a, tea.Quit
case key.Matches(msg, a.keys.Help):
@@ -420,7 +427,7 @@ func (a *App) renderWarnings() string {
content += s.warningItem.Render("• "+w) + "\n"
}
- content += "\n" + s.warningDim.Render("Press Enter or Space to continue...")
+ content += "\n" + s.warningDim.Render("Press Enter, Space, or q to continue...")
boxStyle := s.warningBox.Width(a.width - 10)
box := boxStyle.Render(content)
@@ -456,7 +463,7 @@ func (a *App) handleModalUpdate(msg tea.Msg) (tea.Model, tea.Cmd) {
)
case tea.KeyPressMsg:
- if view.IsEscKey(msg) || msg.Code == tea.KeyBackspace {
+ if view.IsEscKey(msg) || msg.Code == tea.KeyBackspace || msg.String() == "q" {
if ic, ok := a.modal.Content.(view.InputCapture); ok && ic.HasActiveInput() {
break
}
@@ -533,8 +540,8 @@ func defaultKeyMap() keyMap {
key.WithHelp("?", "help"),
),
Quit: key.NewBinding(
- key.WithKeys("ctrl+c"),
- key.WithHelp("ctrl+c", "quit"),
+ key.WithKeys("q", "ctrl+c"),
+ key.WithHelp("q", "quit"),
),
}
}
diff --git a/internal/app/app_test.go b/internal/app/app_test.go
index 2bb008c6..312909f2 100644
--- a/internal/app/app_test.go
+++ b/internal/app/app_test.go
@@ -301,3 +301,31 @@ func TestModalNavigateClosesModal(t *testing.T) {
t.Errorf("Expected viewStack length 1, got %d", len(app.viewStack))
}
}
+
+func TestWarningScreenDismissal(t *testing.T) {
+ tests := []struct {
+ name string
+ key tea.KeyPressMsg
+ }{
+ {"enter", tea.KeyPressMsg{Code: tea.KeyEnter}},
+ {"space", tea.KeyPressMsg{Code: tea.KeySpace}},
+ {"q", tea.KeyPressMsg{Code: 0, Text: "q"}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.Background()
+ reg := registry.New()
+
+ app := New(ctx, reg)
+ app.showWarnings = true
+ app.warningsReady = true
+
+ app.Update(tt.key)
+
+ if app.showWarnings {
+ t.Errorf("Expected showWarnings=false after %s key", tt.name)
+ }
+ })
+ }
+}
diff --git a/internal/view/command_input.go b/internal/view/command_input.go
index 714bdfbd..06db304f 100644
--- a/internal/view/command_input.go
+++ b/internal/view/command_input.go
@@ -217,6 +217,11 @@ func (c *CommandInput) executeCommand() (tea.Cmd, *NavigateMsg) {
return nil, &NavigateMsg{View: dashboard, ClearStack: true}
}
+ // Handle quit command
+ if input == "q" || input == "quit" {
+ return tea.Quit, nil
+ }
+
// Handle services/browse command - go to service browser
if input == "services" || input == "browse" {
browser := NewServiceBrowser(c.ctx, c.registry)
@@ -419,6 +424,9 @@ func (c *CommandInput) GetSuggestions() []string {
} else {
// Suggest services and special commands
// Add navigation commands
+ if strings.HasPrefix("quit", input) {
+ suggestions = append(suggestions, "quit")
+ }
if strings.HasPrefix("home", input) {
suggestions = append(suggestions, "home")
}
diff --git a/internal/view/command_input_test.go b/internal/view/command_input_test.go
index 9c6b89c7..197f952b 100644
--- a/internal/view/command_input_test.go
+++ b/internal/view/command_input_test.go
@@ -160,6 +160,36 @@ func TestCommandInput_Update_Enter_Service(t *testing.T) {
}
}
+func TestCommandInput_QuitCommand(t *testing.T) {
+ ctx := context.Background()
+ reg := registry.New()
+
+ tests := []struct {
+ input string
+ wantQuit bool
+ }{
+ {"q", true},
+ {"quit", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ ci := NewCommandInput(ctx, reg)
+ ci.Activate()
+ ci.textInput.SetValue(tt.input)
+
+ cmd, nav := ci.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+
+ if cmd == nil {
+ t.Error("Expected tea.Quit command")
+ }
+ if nav != nil {
+ t.Error("Expected nil NavigateMsg for quit")
+ }
+ })
+ }
+}
+
// mockDiffProvider for testing getDiffSuggestions
type mockDiffProvider struct {
names []string
diff --git a/internal/view/detail_view.go b/internal/view/detail_view.go
index 6795e791..d8a9c682 100644
--- a/internal/view/detail_view.go
+++ b/internal/view/detail_view.go
@@ -129,7 +129,7 @@ func (d *DetailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return d, nil
case tea.KeyPressMsg:
- // Let app handle back navigation
+ // Let app handle back navigation (esc/backspace/q handled by app.go)
if IsEscKey(msg) {
return d, nil
}
@@ -255,7 +255,7 @@ func (d *DetailView) StatusLine() string {
parts = append(parts, navInfo)
}
- parts = append(parts, "esc:back")
+ parts = append(parts, "q/esc:back")
return strings.Join(parts, " • ")
}
diff --git a/internal/view/diff_view.go b/internal/view/diff_view.go
index bceb23eb..7cc45e31 100644
--- a/internal/view/diff_view.go
+++ b/internal/view/diff_view.go
@@ -68,8 +68,9 @@ func (d *DiffView) Init() tea.Cmd {
func (d *DiffView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
- if IsEscKey(msg) || msg.String() == "q" {
- return d, nil // Let app handle back navigation
+ // Let app handle back navigation (esc/backspace/q handled by app.go)
+ if IsEscKey(msg) {
+ return d, nil
}
}
@@ -120,7 +121,7 @@ func (d *DiffView) SetSize(width, height int) tea.Cmd {
// StatusLine implements View
func (d *DiffView) StatusLine() string {
- return d.left.GetName() + " vs " + d.right.GetName() + " • ↑/↓:scroll • esc:back"
+ return d.left.GetName() + " vs " + d.right.GetName() + " • ↑/↓:scroll • q/esc:back"
}
// renderSideBySide generates the side-by-side view
diff --git a/internal/view/profile_detail_view.go b/internal/view/profile_detail_view.go
index 1edfbe18..a53fac6d 100644
--- a/internal/view/profile_detail_view.go
+++ b/internal/view/profile_detail_view.go
@@ -32,7 +32,7 @@ func (v *ProfileDetailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
- case "esc", "d", "q":
+ case "d":
return v, func() tea.Msg { return HideModalMsg{} }
}
}