Files

104 lines
2.8 KiB
Go

// fv — file voyager: an lf / midnight-commander / xxd hybrid.
// Demo application for the terminal + tui + color packages.
//
// Layout: [parent dir] [current dir list] [preview: info + text/hex/dir table]
// Enter on a file opens a fullscreen hex viewer with byte cursor and search.
//
// Keys (browse): j/k/arrows move · h/l or ←/→ parent/enter · Enter open
//
// gg/G top/bottom · PgUp/PgDn, Ctrl+D/U page · ~ home · . hidden · s sort
// / live filter (Esc clears) · Space mark · a mark-all · u unmark
// y yank paths · r rename · m mkdir · D delete (confirm) · T theme
// ? help · Ctrl+L redraw · q quit · Q quit + exec $SHELL in last dir
//
// Keys (hex): hjkl/arrows · 0/$ row ends · g/G · PgUp/PgDn · Ctrl+D/U
//
// / search (hex bytes like "de ad" / "0xdead", or literal text) · n/N cycle
// Esc/q back
package main
import (
"fmt"
"os"
"path/filepath"
"syscall"
"time"
"github.com/lixenwraith/terminal"
)
const tickInterval = 100 * time.Millisecond // 10 fps animation clock
func main() {
term := terminal.New()
// Raw-mode safety net: restore the terminal before re-panicking.
defer func() {
if r := recover(); r != nil {
terminal.EmergencyReset(os.Stdout)
panic(r)
}
}()
if err := term.Init(); err != nil {
fmt.Fprintln(os.Stderr, "terminal init:", err)
os.Exit(1)
}
_ = term.SetMouseMode(terminal.MouseModeClick) // wheel + click select
start, err := os.Getwd()
if err != nil {
start = "/"
}
app := newApp(term, start)
if err := app.loadDir(start, ""); err != nil {
term.Fini()
fmt.Fprintln(os.Stderr, "read dir:", err)
os.Exit(1)
}
// Animation clock: the input reader never emits {EventKey, KeyNone}
// (unknown sequences are swallowed), so it is a collision-free
// synthetic tick marker through the MPSC event channel.
tickDone := make(chan struct{})
go func() {
t := time.NewTicker(tickInterval)
defer t.Stop()
for {
select {
case <-tickDone:
return
case <-t.C:
term.PostEvent(terminal.Event{Type: terminal.EventKey, Key: terminal.KeyNone})
}
}
}()
for !app.quit {
app.render()
app.handleEvent(term.PollEvent())
}
close(tickDone)
term.Fini()
// --- cd-on-exit -----------------------------------------------------
// A child cannot change the parent shell's cwd. Best scriptless
// approximation: chdir + exec a fresh $SHELL (Q). Plain quit (q)
// records the dir for optional shell-function integration.
last := app.br.cwd
_ = os.Chdir(last)
if dir, err := os.UserCacheDir(); err == nil {
_ = os.WriteFile(filepath.Join(dir, "fv_lastdir"), []byte(last+"\n"), 0o600)
}
if app.execShell {
sh := os.Getenv("SHELL")
if sh == "" {
sh = "/bin/sh"
}
if _, err := os.Stat(sh); err == nil {
_ = syscall.Exec(sh, []string{sh}, os.Environ()) // no return on success
}
}
fmt.Fprintln(os.Stderr, last)
}