gitstack

grindlemire/go-tui code browser

3.3 KB 123 lines 2026-04-18 · 15a2ce7 raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package main

import (
	"fmt"
	tui "github.com/grindlemire/go-tui"
)

type fileList struct {
	files         []string
	selected      *tui.State[int]
	scrollY       *tui.State[int]
	hideScrollbar *tui.State[bool]
	content       *tui.Ref
}

func FileList() *fileList {
	files := []string{
		"main.go", "app.go", "app_loop.go", "app_render.go",
		"buffer.go", "cell.go", "color.go", "component.go",
		"dirty.go", "element.go", "element_options.go",
		"element_render.go", "element_scroll.go", "escape.go",
		"event.go", "focus.go", "key.go", "keymap.go",
		"layout.go", "mount.go", "ref.go", "render.go",
		"state.go", "style.go", "terminal.go", "watcher.go",
	}
	return &fileList{
		files:         files,
		selected:      tui.NewState(0),
		scrollY:       tui.NewState(0),
		hideScrollbar: tui.NewState(false),
		content:       tui.NewRef(),
	}
}

func (f *fileList) scrollBy(delta int) {
	el := f.content.El()
	if el == nil {
		return
	}
	_, maxY := el.MaxScroll()
	newY := f.scrollY.Get() + delta
	if newY < 0 {
		newY = 0
	}
	if newY > maxY {
		newY = maxY
	}
	f.scrollY.Set(newY)
}

func (f *fileList) moveTo(idx int) {
	if idx < 0 {
		idx = len(f.files) - 1
	}
	if idx >= len(f.files) {
		idx = 0
	}
	f.selected.Set(idx)

	// Keep selected item visible by adjusting scroll
	el := f.content.El()
	if el == nil {
		return
	}
	_, vpH := el.ViewportSize()
	y := f.scrollY.Get()
	if idx < y {
		f.scrollY.Set(idx)
	} else if idx >= y+vpH {
		f.scrollY.Set(idx - vpH + 1)
	}
}

func (f *fileList) KeyMap() tui.KeyMap {
	return tui.KeyMap{
		tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }),
		tui.On(tui.Rune('j'), func(ke tui.KeyEvent) { f.moveTo(f.selected.Get() + 1) }),
		tui.On(tui.Rune('k'), func(ke tui.KeyEvent) { f.moveTo(f.selected.Get() - 1) }),
		tui.On(tui.KeyDown, func(ke tui.KeyEvent) { f.moveTo(f.selected.Get() + 1) }),
		tui.On(tui.KeyUp, func(ke tui.KeyEvent) { f.moveTo(f.selected.Get() - 1) }),
		tui.On(tui.KeyPageDown, func(ke tui.KeyEvent) { f.moveTo(f.selected.Get() + 10) }),
		tui.On(tui.KeyPageUp, func(ke tui.KeyEvent) { f.moveTo(f.selected.Get() - 10) }),
		tui.On(tui.KeyHome, func(ke tui.KeyEvent) { f.moveTo(0) }),
		tui.On(tui.KeyEnd, func(ke tui.KeyEvent) { f.moveTo(len(f.files) - 1) }),
		tui.On(tui.Rune('h'), func(ke tui.KeyEvent) { f.hideScrollbar.Set(!f.hideScrollbar.Get()) }),
	}
}

func (f *fileList) HandleMouse(me tui.MouseEvent) bool {
	switch me.Button {
	case tui.MouseWheelUp:
		f.scrollBy(-3)
		return true
	case tui.MouseWheelDown:
		f.scrollBy(3)
		return true
	}
	return false
}

templ (f *fileList) Render() {
	<div class="flex-col p-1 border-rounded border-cyan">
		<span class="text-gradient-cyan-magenta font-bold">Files</span>
		<div
			ref={f.content}
			class="overflow-y-scroll scrollbar-cyan scrollbar-thumb-bright-cyan"
			height={12}
			hideScrollbar={f.hideScrollbar.Get()}
			scrollOffset={0, f.scrollY.Get()}>
			for i, name := range f.files {
				if i == f.selected.Get() {
					<span class="text-cyan font-bold bg-bright-black">{fmt.Sprintf(" > %s ", name)}</span>
				} else {
					<span>{fmt.Sprintf("   %s", name)}</span>
				}
			}
		</div>
		<div class="flex justify-between">
			<span class="font-dim">j/k navigate | h toggle scrollbar | esc quit</span>
			<span class="font-dim">{fmt.Sprintf("%d/%d", f.selected.Get()+1, len(f.files))}</span>
		</div>
	</div>
}