feat: add preview/delete-output to TUI, replace how-it-works with tutorial gif

CLI:
- p key opens file with system viewer (input in file list, output in results)
- x key deletes converted output file from disk in results state
- New 'deleted' status shown in red in the status column
- Updated help overlay and bottom bar keybindings

Web:
- Replace three-step timeline in 'How it works' with Tuturial.gif
- GIF shown in browser-style window frame matching site design
This commit is contained in:
noah
2026-03-10 10:35:41 +01:00
parent 062af2630f
commit cf489c4f02
6 changed files with 152 additions and 133 deletions
+48 -2
View File
@@ -3,7 +3,9 @@ package tui
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
@@ -35,7 +37,7 @@ type fileEntry struct {
targetFormat string
formats []string
formatIdx int
status string // "idle", "converting", "done", "error"
status string // "idle", "converting", "done", "error", "deleted"
error string
outputPath string
}
@@ -290,6 +292,11 @@ func (m Model) handleFileListKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if !isConverting {
return m.startConversion()
}
case key.Matches(msg, m.keys.Preview):
if len(m.files) > 0 {
openFile(m.files[m.cursor].path)
}
}
return m, nil
@@ -303,7 +310,7 @@ func (m Model) handleResultsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Go back to file list to convert more
m.state = stateFileList
for i := range m.files {
if m.files[i].status == "done" || m.files[i].status == "error" {
if m.files[i].status == "done" || m.files[i].status == "error" || m.files[i].status == "deleted" {
m.files[i].status = "idle"
m.files[i].error = ""
m.files[i].outputPath = ""
@@ -315,10 +322,33 @@ func (m Model) handleResultsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keys.Up):
if m.cursor > 0 {
m.cursor--
m.ensureVisible()
}
case key.Matches(msg, m.keys.Down):
if m.cursor < len(m.files)-1 {
m.cursor++
m.ensureVisible()
}
case key.Matches(msg, m.keys.Preview):
// Preview: open output file if done, otherwise open input file
if len(m.files) > 0 {
f := m.files[m.cursor]
if f.status == "done" && f.outputPath != "" {
openFile(f.outputPath)
} else {
openFile(f.path)
}
}
case key.Matches(msg, m.keys.DeleteOutput):
// Delete the converted output file from disk
if len(m.files) > 0 {
f := &m.files[m.cursor]
if f.status == "done" && f.outputPath != "" {
if err := os.Remove(f.outputPath); err == nil {
f.status = "deleted"
f.outputPath = ""
}
}
}
}
return m, nil
@@ -387,6 +417,22 @@ func (m Model) maxVisibleFiles() int {
// ─── Helpers ─────────────────────────────────────────────────
// openFile opens a file with the system default viewer.
func openFile(path string) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", path)
case "linux":
cmd = exec.Command("xdg-open", path)
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", path)
default:
return
}
cmd.Start() //nolint:errcheck
}
func formatSize(bytes int64) string {
const (
KB = 1024