Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <col>` | Sort by column |
Expand Down
25 changes: 16 additions & 9 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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"),
),
}
}
Expand Down
28 changes: 28 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
8 changes: 8 additions & 0 deletions internal/view/command_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
Expand Down
30 changes: 30 additions & 0 deletions internal/view/command_input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/view/detail_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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, " • ")
}

Expand Down
7 changes: 4 additions & 3 deletions internal/view/diff_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/view/profile_detail_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{} }
}
}
Expand Down