feat: add CLI with TUI, self-update, install script, and terminal section on landing page

- Full-screen Bubble Tea TUI with cream background fill using PadLine/FillBlankLines
- Self-update command (--update) pulling from GitHub releases
- install.sh for curl one-liner installation
- Terminal Lovers section on web landing page with install command and CLI features
- All 7 format categories, glob/directory batch support, auto-download ffmpeg
This commit is contained in:
noah
2026-03-09 22:53:10 +01:00
parent b9aac61ab8
commit 04a1f33cb1
23 changed files with 4064 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
tea "github.com/charmbracelet/bubbletea"
"github.com/noauf/transmute-cli/internal/ffmpeg"
"github.com/noauf/transmute-cli/internal/tui"
"github.com/noauf/transmute-cli/internal/update"
)
// Execute runs the CLI.
func Execute() {
args := os.Args[1:]
// Handle flags
var outputDir string
var installFFmpeg bool
var doUpdate bool
var paths []string
for i := 0; i < len(args); i++ {
switch args[i] {
case "-d", "--output-dir":
if i+1 < len(args) {
outputDir = args[i+1]
i++
} else {
fmt.Fprintln(os.Stderr, "Error: -d requires a directory argument")
os.Exit(1)
}
case "--install-ffmpeg":
installFFmpeg = true
case "--update":
doUpdate = true
case "-h", "--help":
printUsage()
os.Exit(0)
case "-v", "--version":
fmt.Printf("transmute v%s\n", update.CurrentVersion)
os.Exit(0)
default:
if args[i][0] == '-' {
fmt.Fprintf(os.Stderr, "Unknown flag: %s\n", args[i])
printUsage()
os.Exit(1)
}
paths = append(paths, args[i])
}
}
// Handle --install-ffmpeg
if installFFmpeg {
handleInstallFFmpeg()
return
}
// Handle --update
if doUpdate {
handleUpdate()
return
}
// If output dir specified, ensure it exists
if outputDir != "" {
if err := os.MkdirAll(outputDir, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
os.Exit(1)
}
}
// Expand glob patterns
expandedPaths := expandGlobs(paths)
// If no paths given, use current directory
if len(expandedPaths) == 0 {
// Launch TUI with empty state — user can see instructions
expandedPaths = []string{"."}
}
// Create and run TUI
model := tui.New(expandedPaths, outputDir)
p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func expandGlobs(patterns []string) []string {
var result []string
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err != nil {
// Not a glob, treat as literal path
result = append(result, pattern)
continue
}
if len(matches) == 0 {
// No matches, keep original (might be a direct path)
result = append(result, pattern)
} else {
result = append(result, matches...)
}
}
return result
}
func handleInstallFFmpeg() {
if ffmpeg.IsAvailable() {
path, _ := ffmpeg.Resolve()
fmt.Printf("ffmpeg is already available at: %s\n", path)
return
}
fmt.Println("Downloading ffmpeg...")
err := ffmpeg.Download(func(downloaded int64) {
fmt.Printf("\r Downloaded %.1f MB", float64(downloaded)/1024/1024)
})
if err != nil {
fmt.Fprintf(os.Stderr, "\nError downloading ffmpeg: %v\n", err)
os.Exit(1)
}
fmt.Println("\n ffmpeg installed successfully!")
}
func handleUpdate() {
err := update.Run(func(msg string) {
fmt.Println(msg)
})
if err != nil {
fmt.Fprintf(os.Stderr, "Update failed: %v\n", err)
os.Exit(1)
}
}
func printUsage() {
fmt.Println(`transmute - Universal file converter
Usage:
transmute [files...] Convert files interactively
transmute *.png Convert all PNG files
transmute ./photos/ Convert all files in directory
transmute file.csv -d ./output/ Output to specific directory
Flags:
-d, --output-dir <dir> Output converted files to this directory
--install-ffmpeg Download and install ffmpeg for audio/video
--update Update transmute to the latest version
-h, --help Show this help
-v, --version Show version
Interactive Controls:
↑/↓ or j/k Navigate files
←/→ or h/l Change target format
space Toggle file selection
a Select / deselect all
c or enter Start conversion
d Remove file from list
? Toggle help
q or ctrl+c Quit`)
}