feat: add reload+clear, delete line, delete all lines

This commit is contained in:
2026-03-25 01:16:16 +02:00
parent aa0467c07b
commit c4e4d5d0de
3 changed files with 106 additions and 0 deletions

View File

@@ -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 |

View File

@@ -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
}

View File

@@ -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")