v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
package tui
|
||||
|
||||
import "github.com/lixenwraith/terminal"
|
||||
|
||||
// BarSection represents one segment of a status bar
|
||||
type BarSection struct {
|
||||
Label string
|
||||
Value string
|
||||
LabelStyle Style
|
||||
ValueStyle Style
|
||||
Priority int // Higher = survives truncation
|
||||
}
|
||||
|
||||
// BarAlign specifies status bar alignment mode
|
||||
type BarAlign uint8
|
||||
|
||||
const (
|
||||
BarAlignRight BarAlign = iota // Pack sections from right
|
||||
BarAlignLeft // Pack sections from left
|
||||
BarAlignDistribute // Evenly space sections
|
||||
BarAlignCenter // Center all sections as a group
|
||||
)
|
||||
|
||||
// BarOpts configures status bar rendering
|
||||
type BarOpts struct {
|
||||
Separator string // Between sections, default " │ "
|
||||
SepStyle Style // Separator styling
|
||||
Bg terminal.RGB
|
||||
Align BarAlign
|
||||
Padding int // Left/right padding, default 1
|
||||
}
|
||||
|
||||
// DefaultBarOpts returns sensible defaults
|
||||
func DefaultBarOpts() BarOpts {
|
||||
return BarOpts{
|
||||
Separator: " │ ",
|
||||
SepStyle: Style{Fg: terminal.RGB{R: 80, G: 80, B: 100}},
|
||||
Padding: 1,
|
||||
Align: BarAlignRight,
|
||||
}
|
||||
}
|
||||
|
||||
// StatusBar renders horizontal status bar on row y
|
||||
func (r Region) StatusBar(y int, sections []BarSection, opts BarOpts) {
|
||||
if y < 0 || y >= r.H || len(sections) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if opts.Separator == "" {
|
||||
opts.Separator = " │ "
|
||||
}
|
||||
if opts.Padding == 0 {
|
||||
opts.Padding = 1
|
||||
}
|
||||
|
||||
// Fill background
|
||||
for x := 0; x < r.W; x++ {
|
||||
r.Cell(x, y, ' ', terminal.RGB{}, opts.Bg, terminal.AttrNone)
|
||||
}
|
||||
|
||||
sepLen := RuneLen(opts.Separator)
|
||||
|
||||
// Calculate total width needed
|
||||
totalW := 0
|
||||
sectionWidths := make([]int, len(sections))
|
||||
for i, sec := range sections {
|
||||
w := RuneLen(sec.Label) + RuneLen(sec.Value)
|
||||
sectionWidths[i] = w
|
||||
totalW += w
|
||||
if i < len(sections)-1 {
|
||||
totalW += sepLen
|
||||
}
|
||||
}
|
||||
|
||||
availW := r.W - opts.Padding*2
|
||||
|
||||
// Truncate low-priority sections if needed
|
||||
if totalW > availW {
|
||||
sections, sectionWidths = truncateSections(sections, sectionWidths, sepLen, availW)
|
||||
totalW = 0
|
||||
for i, w := range sectionWidths {
|
||||
totalW += w
|
||||
if i < len(sectionWidths)-1 {
|
||||
totalW += sepLen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate starting position based on alignment
|
||||
var x int
|
||||
switch opts.Align {
|
||||
case BarAlignLeft:
|
||||
x = opts.Padding
|
||||
case BarAlignRight:
|
||||
x = r.W - opts.Padding - totalW
|
||||
if x < opts.Padding {
|
||||
x = opts.Padding
|
||||
}
|
||||
case BarAlignDistribute:
|
||||
x = opts.Padding
|
||||
// Handled specially below
|
||||
case BarAlignCenter:
|
||||
x = (r.W - totalW) / 2
|
||||
if x < opts.Padding {
|
||||
x = opts.Padding
|
||||
}
|
||||
}
|
||||
|
||||
// Render sections
|
||||
if opts.Align == BarAlignDistribute && len(sections) > 1 {
|
||||
gap := (availW - totalW) / (len(sections) - 1)
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
for i, sec := range sections {
|
||||
x = r.renderBarSection(x, y, sec, opts)
|
||||
if i < len(sections)-1 {
|
||||
x += gap
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i, sec := range sections {
|
||||
x = r.renderBarSection(x, y, sec, opts)
|
||||
if i < len(sections)-1 {
|
||||
// Separator
|
||||
for j, ch := range opts.Separator {
|
||||
if x+j < r.W-opts.Padding {
|
||||
r.Cell(x+j, y, ch, opts.SepStyle.Fg, opts.Bg, opts.SepStyle.Attr)
|
||||
}
|
||||
}
|
||||
x += sepLen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r Region) renderBarSection(x, y int, sec BarSection, opts BarOpts) int {
|
||||
// Label
|
||||
for _, ch := range sec.Label {
|
||||
if x >= r.W-opts.Padding {
|
||||
break
|
||||
}
|
||||
r.Cell(x, y, ch, sec.LabelStyle.Fg, opts.Bg, sec.LabelStyle.Attr)
|
||||
x++
|
||||
}
|
||||
// Value
|
||||
for _, ch := range sec.Value {
|
||||
if x >= r.W-opts.Padding {
|
||||
break
|
||||
}
|
||||
r.Cell(x, y, ch, sec.ValueStyle.Fg, opts.Bg, sec.ValueStyle.Attr)
|
||||
x++
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// truncateSections removes lowest priority sections until fit
|
||||
func truncateSections(sections []BarSection, widths []int, sepLen, availW int) ([]BarSection, []int) {
|
||||
// Copy to avoid modifying original
|
||||
secs := make([]BarSection, len(sections))
|
||||
copy(secs, sections)
|
||||
ws := make([]int, len(widths))
|
||||
copy(ws, widths)
|
||||
|
||||
for {
|
||||
total := 0
|
||||
for i, w := range ws {
|
||||
total += w
|
||||
if i < len(ws)-1 {
|
||||
total += sepLen
|
||||
}
|
||||
}
|
||||
if total <= availW || len(secs) <= 1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Find lowest priority
|
||||
minIdx := 0
|
||||
minPrio := secs[0].Priority
|
||||
for i, sec := range secs {
|
||||
if sec.Priority < minPrio {
|
||||
minPrio = sec.Priority
|
||||
minIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveEntityAt it
|
||||
secs = append(secs[:minIdx], secs[minIdx+1:]...)
|
||||
ws = append(ws[:minIdx], ws[minIdx+1:]...)
|
||||
}
|
||||
|
||||
return secs, ws
|
||||
}
|
||||
|
||||
// QuickStatusBar renders simple label:value pairs right-aligned
|
||||
func (r Region) QuickStatusBar(y int, pairs [][2]string, labelFg, valueFg, bg terminal.RGB) {
|
||||
sections := make([]BarSection, len(pairs))
|
||||
for i, p := range pairs {
|
||||
sections[i] = BarSection{
|
||||
Label: p[0],
|
||||
Value: p[1],
|
||||
LabelStyle: Style{Fg: labelFg},
|
||||
ValueStyle: Style{Fg: valueFg},
|
||||
}
|
||||
}
|
||||
r.StatusBar(y, sections, BarOpts{
|
||||
Bg: bg,
|
||||
Align: BarAlignRight,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user