feat: v0.1.6 - auto-refresh file list, separate reset key

- Auto-refresh: file list now checks directories every 2 seconds for new files
- r key: resets done/error/deleted file back to idle for reconversion
- f key: manually refresh the file list
- d key: removes file from list only (unchanged)
- Auto-refresh works for directories that files were loaded from
- Updated help menu and bottom bar with new keybindings
This commit is contained in:
noah
2026-03-11 10:46:31 +01:00
parent 82a4123f84
commit 31cfd03e42
6 changed files with 99 additions and 41 deletions
+23 -18
View File
@@ -4,21 +4,22 @@ import "github.com/charmbracelet/bubbles/key"
// KeyMap defines key bindings for the TUI.
type KeyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Enter key.Binding
Space key.Binding
Tab key.Binding
Delete key.Binding
SelectAll key.Binding
Convert key.Binding
Quit key.Binding
Help key.Binding
Back key.Binding
Preview key.Binding
DeleteOutput key.Binding
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Enter key.Binding
Space key.Binding
Tab key.Binding
Delete key.Binding
SelectAll key.Binding
Convert key.Binding
Quit key.Binding
Help key.Binding
Back key.Binding
Preview key.Binding
Reset key.Binding
Refresh key.Binding
}
// DefaultKeyMap returns the default key bindings.
@@ -80,9 +81,13 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("p"),
key.WithHelp("p", "preview file"),
),
DeleteOutput: key.NewBinding(
key.WithKeys("x"),
key.WithHelp("x", "delete output"),
Reset: key.NewBinding(
key.WithKeys("r"),
key.WithHelp("r", "reset to idle"),
),
Refresh: key.NewBinding(
key.WithKeys("f"),
key.WithHelp("f", "refresh files"),
),
}
}
+67 -15
View File
@@ -54,6 +54,8 @@ type conversionStartMsg struct {
type tickMsg time.Time
type refreshFilesMsg struct{}
// ─── Model ───────────────────────────────────────────────────
type Model struct {
@@ -72,6 +74,10 @@ type Model struct {
converted int
totalToConv int
startTime time.Time
// File watching
watchDirs map[string]int64 // dir path -> last known mod time
lastRefresh time.Time
}
// New creates a new TUI model from a list of file paths and an output directory.
@@ -168,7 +174,10 @@ func makeFileEntry(path string, info os.FileInfo) *fileEntry {
// ─── Init ────────────────────────────────────────────────────
func (m Model) Init() tea.Cmd {
return nil
// Start a ticker to watch for file changes every 2 seconds
return tea.Tick(2*time.Second, func(t time.Time) tea.Msg {
return refreshFilesMsg{}
})
}
// ─── Update ──────────────────────────────────────────────────
@@ -198,6 +207,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
return m.handleKey(msg)
case refreshFilesMsg:
// Check directories for new files
m = m.checkForNewFiles()
// Continue watching
return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg {
return refreshFilesMsg{}
})
}
return m, nil
@@ -291,20 +308,8 @@ func (m Model) handleFileListKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
openFile(path)
}
case key.Matches(msg, m.keys.DeleteOutput):
// Delete the converted output file from disk
if len(m.files) > 0 && !isConverting {
f := &m.files[m.cursor]
if f.status == "done" && f.outputPath != "" {
if err := os.Remove(f.outputPath); err == nil {
f.status = "idle"
f.outputPath = ""
}
}
}
case key.Matches(msg, m.keys.Back):
// Reset done/error files to idle for reconversion
case key.Matches(msg, m.keys.Reset):
// Reset done/error/deleted file back to idle for reconversion
if len(m.files) > 0 && !isConverting {
f := &m.files[m.cursor]
if f.status == "done" || f.status == "error" || f.status == "deleted" {
@@ -313,6 +318,10 @@ func (m Model) handleFileListKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
f.outputPath = ""
}
}
case key.Matches(msg, m.keys.Refresh):
// Manually refresh the file list
m = m.checkForNewFiles()
}
return m, nil
@@ -414,3 +423,46 @@ func formatSize(bytes int64) string {
return fmt.Sprintf("%d B", bytes)
}
}
// checkForNewFiles scans directories for new files and adds them to the list.
func (m Model) checkForNewFiles() Model {
dirs := make(map[string]bool)
for _, f := range m.files {
dir := filepath.Dir(f.path)
dirs[dir] = true
}
for dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() || strings.HasPrefix(e.Name(), ".") {
continue
}
path := filepath.Join(dir, e.Name())
// Check if already in list
exists := false
for _, f := range m.files {
if f.path == path {
exists = true
break
}
}
if !exists {
info, err := e.Info()
if err != nil {
continue
}
entry := makeFileEntry(path, info)
if entry != nil {
m.files = append(m.files, *entry)
}
}
}
}
return m
}
+4 -4
View File
@@ -113,15 +113,15 @@ func TestDeleteOutput(t *testing.T) {
t.Fatalf("expected done, got %s", m.files[0].status)
}
// Test pressing 'x' to delete the output and reset to idle
newModel, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}})
// Test pressing 'r' to reset the done file back to idle
newModel, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}})
m = newModel.(Model)
if m.files[0].status != "idle" {
t.Fatalf("expected 'idle' status after pressing x, got '%s'", m.files[0].status)
t.Fatalf("expected 'idle' status after pressing r, got '%s'", m.files[0].status)
}
t.Log("Delete output works: file removed from disk, status reset to 'idle'")
t.Log("Reset works: status reset from done back to 'idle'")
}
func TestViewRendersDuringConversion(t *testing.T) {
+4 -3
View File
@@ -317,8 +317,8 @@ func (m Model) renderBottomBar() string {
// Adaptive keybindings: full or compact based on available space
leftW := lipgloss.Width(left)
fullHelp := "up/down navigate left/right format space select p preview a all q quit"
shortHelp := "↑↓ nav ←→ fmt spc sel p preview q quit"
fullHelp := "↑↓ nav ←→ fmt space sel p preview r reset d delete f refresh c convert q quit"
shortHelp := "↑↓ nav ←→ fmt spc sel p prev r rst d del q quit"
helpText := fullHelp
rightW := len(helpText) + 4 // 2 padding each side
@@ -379,8 +379,9 @@ func (m Model) renderHelp() string {
{"space", "Toggle selection"},
{"a", "Select / deselect all"},
{"p", "Preview / open file"},
{"r", "Reset to idle"},
{"d", "Remove from list"},
{"x", "Delete output"},
{"f", "Refresh files"},
{"c or enter", "Convert selected"},
{"? or q", "Close / quit"},
}