diff --git a/README.md b/README.md index 45df7d6..3d803aa 100755 --- a/README.md +++ b/README.md @@ -195,6 +195,9 @@ Configuration values are applied in this order (later sources override earlier o | Key | Action | | ------------------ | -------------------------------- | | `r`, `Ctrl-r` | Reload (re-run command) | +| `R` | Reload & clear all lines | +| `Del` | Delete selected line | +| `Ctrl-Del` | Clear all lines (with confirm) | | `c` | Stop running command | | `q`, `Esc` | Quit | | `j`, `k` | Move down/up | diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 161a0fb..3d0028e 100755 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -75,6 +75,10 @@ type model struct { cmdPaletteFilter string // current filter text cmdPaletteCursor int // cursor position within filter string cmdPaletteSelected int // selected item index in filtered list + + confirmMode bool // whether a confirmation dialog is visible + confirmMessage string // message to display in confirmation dialog + confirmAction func(m *model) (tea.Model, tea.Cmd) } // messages @@ -114,6 +118,34 @@ func commands() []command { cmd := m.startStreaming() return m, tea.Batch(cmd, m.spinnerTickCmd()) }}, + {"Reload & clear lines", "R", func(m *model) (tea.Model, tea.Cmd) { + m.lines = nil + m.updateFiltered() + m.refreshGeneration++ + cmd := m.startStreaming() + return m, tea.Batch(cmd, m.spinnerTickCmd()) + }}, + {"Delete selected line", "Del", func(m *model) (tea.Model, tea.Cmd) { + if len(m.filtered) > 0 && m.cursor >= 0 && m.cursor < len(m.filtered) { + idx := m.filtered[m.cursor] + if idx < len(m.lines) { + m.lines = append(m.lines[:idx], m.lines[idx+1:]...) + m.updateFiltered() + } + } + return m, nil + }}, + {"Clear all lines", "Ctrl+Del", func(m *model) (tea.Model, tea.Cmd) { + m.confirmMode = true + m.confirmMessage = "Clear all lines? (y/N)" + m.confirmAction = func(m *model) (tea.Model, tea.Cmd) { + m.lines = nil + m.updateFiltered() + m.statusMsg = "All lines cleared" + return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { return clearStatusMsg{} }) + } + return m, nil + }}, {"Stop running command", "c", func(m *model) (tea.Model, tea.Cmd) { if m.streaming { m.cancel() @@ -478,6 +510,20 @@ func (m *model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } + // In confirmation mode, y confirms and any other key cancels + if m.confirmMode { + switch msg.String() { + case "y", "Y": + m.confirmMode = false + if m.confirmAction != nil { + return m.confirmAction(m) + } + default: + m.confirmMode = false + } + return m, nil + } + // In command palette mode if m.cmdPaletteMode { switch msg.Type { @@ -729,6 +775,33 @@ func (m *model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.refreshGeneration++ cmd := m.startStreaming() return m, tea.Batch(cmd, m.spinnerTickCmd()) + case "R": + // Refresh command and clear all lines + m.lines = nil + m.updateFiltered() + m.refreshGeneration++ + cmd := m.startStreaming() + return m, tea.Batch(cmd, m.spinnerTickCmd()) + case "delete": + // Delete the currently selected line + if len(m.filtered) > 0 && m.cursor >= 0 && m.cursor < len(m.filtered) { + idx := m.filtered[m.cursor] + if idx < len(m.lines) { + m.lines = append(m.lines[:idx], m.lines[idx+1:]...) + m.updateFiltered() + } + } + case "ctrl+delete": + // Clear all lines with confirmation + m.confirmMode = true + m.confirmMessage = "Clear all lines? (y/N)" + m.confirmAction = func(m *model) (tea.Model, tea.Cmd) { + m.lines = nil + m.updateFiltered() + m.statusMsg = "All lines cleared" + return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { return clearStatusMsg{} }) + } + return m, nil case "c": // Stop the running command if one is running if m.streaming { @@ -1165,6 +1238,9 @@ func (m model) renderHelpOverlay() (box string, boxWidth, boxHeight int) { {"Esc", "Exit filter / clear"}, {"", ""}, {"r / Ctrl+r", "Reload command"}, + {"R", "Reload & clear lines"}, + {"Del", "Delete selected line"}, + {"Ctrl+Del", "Clear all lines"}, {"c", "Stop running command"}, {"y", "Copy line to clipboard"}, {"Y", "Copy line (plain text)"}, @@ -1204,6 +1280,24 @@ func (m model) renderHelpOverlay() (box string, boxWidth, boxHeight int) { return box, boxWidth, boxHeight } +// renderConfirmOverlay creates a confirmation dialog overlay +func (m model) renderConfirmOverlay() (box string, boxWidth, boxHeight int) { + msgStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")) + + boxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("11")). + Padding(1, 2) + + content := msgStyle.Render(m.confirmMessage) + box = boxStyle.Render(content) + boxWidth = lipgloss.Width(box) + boxHeight = lipgloss.Height(box) + + return box, boxWidth, boxHeight +} + // splitAtVisualWidth splits a string at a visual width position, handling ANSI codes // Returns (left part, right part) where left has exactly targetWidth visual width func splitAtVisualWidth(s string, targetWidth int) (string, string) { @@ -1396,6 +1490,12 @@ func (m *model) View() string { return overlayBox(mainView, box, boxWidth, boxHeight, m.width, m.height) } + // Overlay confirmation dialog if active + if m.confirmMode { + box, boxWidth, boxHeight := m.renderConfirmOverlay() + return overlayBox(mainView, box, boxWidth, boxHeight, m.width, m.height) + } + return mainView } diff --git a/main.go b/main.go index d75c91c..36af542 100755 --- a/main.go +++ b/main.go @@ -47,6 +47,9 @@ func main() { flag.CommandLine.SetOutput(os.Stderr) _, _ = fmt.Fprintf(w, "\nKeybindings:\n") _, _ = fmt.Fprintf(w, " r, Ctrl-r Reload (re-run command)\n") + _, _ = fmt.Fprintf(w, " R Reload & clear all lines\n") + _, _ = fmt.Fprintf(w, " Del Delete selected line\n") + _, _ = fmt.Fprintf(w, " Ctrl-Del Clear all lines (with confirm)\n") _, _ = fmt.Fprintf(w, " c Stop running command\n") _, _ = fmt.Fprintf(w, " q, Esc Quit\n") _, _ = fmt.Fprintf(w, " j, k Move down/up\n")