214 lines
4.7 KiB
Go
214 lines
4.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"github.com/lixenwraith/color"
|
|
"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 color.RGB
|
|
Align BarAlign
|
|
Padding int // Left/right padding, default 1
|
|
}
|
|
|
|
// DefaultBarOpts returns sensible defaults
|
|
func DefaultBarOpts() BarOpts {
|
|
return BarOpts{
|
|
Separator: " │ ",
|
|
SepStyle: Style{Fg: color.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, ' ', color.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 color.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,
|
|
})
|
|
}
|