v0.1.0 initial commit

This commit is contained in:
2026-08-06 10:18:22 -04:00
commit 28e42523e1
31 changed files with 5055 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
package ui
import (
"github.com/lixenwraith/terminal"
"github.com/lixenwraith/terminal/tui"
)
// Panel is a modal overlay with a scrollable body. It owns the frame, the
// scroll state and the scrollbar; the caller renders rows into the region
// Render returns, starting at Scroll.
type Panel struct {
Title string
Hint string
Status string // line inside the bottom of the frame, e.g. a full filename
W, H int // 0 = 80% of the screen
Cursor bool // track a selected row rather than scrolling freely
Sel int
Scroll int
Rows int // content rows, set by the caller before Render
View int // visible rows, set by Render
}
// Move advances the selection, or the scroll offset when Cursor is unset.
func (p *Panel) Move(d int) {
if p.Cursor {
p.Sel += d
} else {
p.Scroll += d
}
p.clamp()
}
// Page moves by n screens.
func (p *Panel) Page(n int) { p.Move(n * max(1, p.View)) }
// Half moves by n half screens.
func (p *Panel) Half(n int) { p.Move(n * max(1, p.View/2)) }
// First jumps to the top.
func (p *Panel) First() { p.Sel, p.Scroll = 0, 0 }
// Last jumps to the bottom.
func (p *Panel) Last() {
p.Sel, p.Scroll = p.Rows-1, p.Rows
p.clamp()
}
// Reset returns the panel to the top, for new content.
func (p *Panel) Reset() { p.Sel, p.Scroll = 0, 0 }
func (p *Panel) clamp() {
if p.Rows <= 0 {
p.Sel, p.Scroll = 0, 0
return
}
p.Sel = tui.ClampCursor(p.Sel, p.Rows)
if p.Cursor {
p.Scroll = tui.AdjustScroll(p.Sel, p.Scroll, p.View, p.Rows)
}
p.Scroll = tui.ClampScroll(p.Scroll, p.View, p.Rows)
}
// Render draws the frame and returns the body region, scrollbar excluded.
// A zero-width result means the screen is too small to show the panel.
func (p *Panel) Render(root tui.Region, th *Theme) tui.Region {
w, h := p.W, p.H
if w <= 0 {
w = root.W * 80 / 100
}
if h <= 0 {
h = root.H * 80 / 100
}
w, h = min(w, root.W-4), min(h, root.H-2)
if w < 24 || h < 6 {
return tui.Region{}
}
t, hint := p.Title, p.Hint
if t != "" {
t = " " + t + " "
}
if hint != "" {
hint = " " + hint + " "
}
content := tui.Center(root, w, h).Modal(tui.ModalOpts{
Title: t, Hint: hint, Border: tui.LineDouble,
BorderFg: th.Accent, TitleFg: th.HeaderFg, HintFg: th.HintFg, Bg: th.FocusBg,
})
body := content
if p.Status != "" && content.H > 3 {
body = content.Sub(0, 0, content.W, content.H-2)
content.HLine(content.H-2, tui.LineSingle, th.Border)
content.Text(0, content.H-1, tui.Truncate(p.Status, content.W),
th.HintFg, th.FocusBg, terminal.AttrDim)
}
p.View = body.H
p.clamp()
if p.Rows > body.H && body.W > 2 {
body.Sub(body.W-1, 0, 1, body.H).ScrollBar(0, p.Scroll, body.H, p.Rows, th.Border)
body = body.Sub(0, 0, body.W-1, body.H)
}
return body
}