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:
@@ -18,7 +18,8 @@ type KeyMap struct {
|
||||
Help key.Binding
|
||||
Back key.Binding
|
||||
Preview key.Binding
|
||||
DeleteOutput 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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
const (
|
||||
// CurrentVersion is the embedded build version. Updated at release time.
|
||||
CurrentVersion = "0.1.5"
|
||||
CurrentVersion = "0.1.6"
|
||||
|
||||
repoOwner = "noauf"
|
||||
repoName = "Transmute"
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user