tinybox

Owner: IIIlllIIIllI URL: git@github.com:nyangkosense/tinybox.git

tinyfm

Commit 5c5a293e4d70308040318c2185c5088b6010a0ca by SM <seb.michalk@gmail.com> on 2025-09-24 11:09:36 +0200
diff --git a/demo/tinyfm.go b/demo/tinyfm.go
new file mode 100644
index 0000000..df7dfa5
--- /dev/null
+++ b/demo/tinyfm.go
@@ -0,0 +1,195 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	tb "tinybox-example/tinybox"
+)
+
+type entry struct {
+	name string
+	dir  bool
+}
+
+type state struct {
+	path   string
+	items  []entry
+	sel    int
+	scroll int
+	msg    string
+}
+
+func main() {
+	st := &state{path: startPath()}
+	st.reload()
+	if err := tb.Init(); err != nil {
+		fmt.Fprintln(os.Stderr, "tinyfm:", err)
+		return
+	}
+	defer tb.Close()
+	tb.SetCursorVisible(false)
+	for {
+		draw(st)
+		evt, err := tb.PollEvent()
+		if err != nil {
+			st.msg = err.Error()
+			continue
+		}
+		if evt.Type != tb.EventKey {
+			continue
+		}
+		switch evt.Key {
+		case tb.KeyCtrlC, tb.KeyEscape:
+			return
+		case tb.KeyArrowUp:
+			st.move(-1)
+		case tb.KeyArrowDown:
+			st.move(1)
+		case tb.KeyEnter:
+			st.openSelection()
+		case tb.KeyBackspace:
+			st.up()
+		default:
+			if evt.Ch == 'q' || evt.Ch == 'Q' {
+				return
+			}
+		}
+	}
+}
+
+func startPath() string {
+	if len(os.Args) > 1 {
+		if abs, err := filepath.Abs(os.Args[1]); err == nil {
+			return abs
+		}
+	}
+	if pwd, err := os.Getwd(); err == nil {
+		return pwd
+	}
+	return "."
+}
+
+func (s *state) reload() {
+	entries, err := os.ReadDir(s.path)
+	if err != nil {
+		s.items = nil
+		s.msg = err.Error()
+		return
+	}
+	list := make([]entry, 0, len(entries))
+	for _, e := range entries {
+		list = append(list, entry{name: e.Name(), dir: e.IsDir()})
+	}
+	sort.Slice(list, func(i, j int) bool {
+		if list[i].dir == list[j].dir {
+			return list[i].name < list[j].name
+		}
+		return list[i].dir
+	})
+	s.items = list
+	if s.sel >= len(list) {
+		s.sel = len(list) - 1
+	}
+	if s.sel < 0 {
+		s.sel = 0
+	}
+	if len(list) == 0 {
+		s.sel = 0
+	}
+	s.msg = ""
+}
+
+func (s *state) move(step int) {
+	if len(s.items) == 0 {
+		s.sel = 0
+		return
+	}
+	s.sel += step
+	if s.sel < 0 {
+		s.sel = 0
+	} else if s.sel >= len(s.items) {
+		s.sel = len(s.items) - 1
+	}
+}
+
+func (s *state) openSelection() {
+	if len(s.items) == 0 {
+		return
+	}
+	it := s.items[s.sel]
+	next := filepath.Join(s.path, it.name)
+	if it.dir {
+		s.path = next
+		s.sel, s.scroll = 0, 0
+		s.reload()
+		return
+	}
+	s.msg = next
+}
+
+func (s *state) up() {
+	parent := filepath.Dir(s.path)
+	if parent == s.path {
+		return
+	}
+	s.path = parent
+	s.sel, s.scroll = 0, 0
+	s.reload()
+}
+
+func draw(s *state) {
+	tb.Clear()
+	w, h := tb.Size()
+	if w <= 0 || h <= 2 {
+		tb.Flush()
+		return
+	}
+	tb.SetColor(14, 0)
+	tb.PrintAt(0, 0, " "+s.path)
+	tb.SetColor(8, 0)
+	tb.PrintAt(0, 1, " arrows navigate · enter open · backspace up · q quit")
+	viewTop := 2
+	view := h - viewTop - 1
+	if view < 1 {
+		view = 1
+	}
+	if s.sel < s.scroll {
+		s.scroll = s.sel
+	}
+	if s.sel >= s.scroll+view {
+		s.scroll = s.sel - view + 1
+	}
+	if max := len(s.items) - view; max >= 0 && s.scroll > max {
+		s.scroll = max
+	}
+	for i := 0; i < view && s.scroll+i < len(s.items); i++ {
+		idx := s.scroll + i
+		it := s.items[idx]
+		y := viewTop + i
+		if idx == s.sel {
+			tb.SetColor(0, 12)
+		} else if it.dir {
+			tb.SetColor(11, 0)
+		} else {
+			tb.SetColor(15, 0)
+		}
+		tb.PrintAt(0, y, formatEntry(it))
+	}
+	tb.SetColor(8, 0)
+	status := s.msg
+	if status == "" {
+		status = fmt.Sprintf("%d item(s)", len(s.items))
+	}
+	tb.PrintAt(0, h-1, " "+status)
+	tb.Flush()
+}
+
+func formatEntry(e entry) string {
+	name := e.name
+	if e.dir {
+		name += "/"
+	}
+	return " " + name
+}