v0.2.2 tui update, reverse row bar

This commit is contained in:
lixen committed 2026-10-10 18:04:30 -04:00
1 parent 7d1c3b24a0
commit c657af606d
8 files changed
+183 -86

No files matched your search

+7 -5
View File
@@ -125,9 +125,11 @@ draws alike in each: `DefaultTheme` is dark true color, `Theme16` uses the
ANSI 16 on the terminal's own background, `MonoTheme` uses attributes
alone, and `ThemeFor(term.ColorMode())` picks among them. A selection
always changes a glyph (a radio mark, a pointer, the focus mark), so it reads
without color. `GlyphsUnicode`, `GlyphsCP437` (text consoles) and
`GlyphsASCII` (outside UTF-8) are the glyph sets; `Glyphs.Line` is the line
type of frames, rules and wires.
without color. A reverse-video `Selected` with no color of its own
(`Theme16`, `MonoTheme`) keeps the terminal's own foreground in every cell
drawn on it, so the cursor row is one bar. `GlyphsUnicode`, `GlyphsCP437`
(text consoles) and `GlyphsASCII` (outside UTF-8) are the glyph sets;
`Glyphs.Line` is the line type of frames, rules and wires.
```go
th := tui.DefaultTheme
@@ -257,8 +259,8 @@ Pure logic, no rendering — usable independently:
## Notes
- Width calculations count runes, not terminal columns; East Asian wide
characters and combining marks are not width-aware.
- Width calculations count runes, one column each, as `Flush` draws a wide or
joining rune as U+FFFD.
- Zero-value `color.RGB` in style fields generally means "inherit"
(widget default or row background) — check specific widget docs. A zero
background with no Bg color bits is transparent: the cell keeps the
+7 -1
View File
@@ -25,11 +25,17 @@ func (s Style) IsZero() bool {
}
// On returns s drawn on bg: s's foreground, bg's background (s's own where
// bg's is transparent), and the attributes of both
// bg's is transparent), and the attributes of both. On a reverse bg with no
// color of its own, s keeps only its background, with the terminal's own
// foreground and bg's styles: a reversed cell's background is its foreground
// (a console's keeps its intensity), so mixed styles would break the bar.
func (s Style) On(bg Style) Style {
out := Style{Fg: s.Fg, Bg: s.Bg, Attr: s.Attr | bg.Attr&terminal.AttrStyle}
if bg.Bg != (color.RGB{}) || bg.Attr&terminal.AttrBgColor != 0 {
out.Bg, out.Attr = bg.Bg, out.Attr&^terminal.AttrBgColor|bg.Attr&terminal.AttrBgColor
}
if bg.Attr&terminal.AttrReverse != 0 && bg.Bg == (color.RGB{}) && bg.Attr&terminal.AttrBg256 == 0 {
out.Fg, out.Attr = color.RGB{}, terminal.AttrFgDefault|out.Attr&terminal.AttrBgColor|bg.Attr&terminal.AttrStyle
}
return out
}
+2 -18
View File
@@ -5,16 +5,12 @@ import (
"unicode/utf8"
)
// --- Length calculation ---
// RuneLen returns rune count, used as display width under the package-wide assumption of 1 cell per rune
// Wide (CJK), emoji, and combining characters are not handled
// RuneLen returns s's width in cells, a rune each: Flush draws every rune in
// one column, a wide or joining one as U+FFFD
func RuneLen(s string) int {
return utf8.RuneCountInString(s)
}
// --- Truncation ---
// Truncate truncates string with … suffix if exceeds maxLen
func Truncate(s string, maxLen int) string {
if maxLen <= 0 {
@@ -63,16 +59,12 @@ func TruncateMiddle(s string, maxLen int) string {
return Truncate(s, maxLen)
}
// Split remaining space between start and end
// Favor start slightly: (maxLen-1)/2 for start, rest for end
startLen := (maxLen - 1) / 2
endLen := maxLen - 1 - startLen
return string(runes[:startLen]) + "…" + string(runes[len(runes)-endLen:])
}
// --- Padding ---
// PadRight pads string with spaces to width
func PadRight(s string, width int) string {
runes := []rune(s)
@@ -117,8 +109,6 @@ func PadCenter(s string, width int) string {
return string(result)
}
// --- Text wrapping ---
// WrapText wraps text to fit width: at each line break, at spaces, or, in a
// word longer than a line, after its last '.', ',', ':', '/', '[' or ']'
// that fits (a path or a key such as a[0].b); a word without one is cut.
@@ -146,17 +136,14 @@ func wrapLine(s string, width int) []string {
lastSpace, lastMark := -1, -1
for i := 0; i <= len(runes); i++ {
// Check if we need to wrap
if i-lineStart >= width || i == len(runes) {
if i == len(runes) {
// End of string
if lineStart < len(runes) {
lines = append(lines, string(runes[lineStart:]))
}
break
}
// Need to wrap
wrapAt := i
switch {
case lastSpace > lineStart:
@@ -167,7 +154,6 @@ func wrapLine(s string, width int) []string {
lines = append(lines, string(runes[lineStart:wrapAt]))
// Skip space at wrap point
if wrapAt < len(runes) && runes[wrapAt] == ' ' {
lineStart = wrapAt + 1
} else {
@@ -192,8 +178,6 @@ func wrapLine(s string, width int) []string {
return lines
}
// --- Repetition ---
// RepeatRune returns a string of n repeated runes, for string repeat use strings.Repeat
func RepeatRune(r rune, n int) string {
if n <= 0 {
+21 -1
View File
@@ -37,12 +37,32 @@ func TestTransparentBackgroundKeepsItsKind(t *testing.T) {
if c := r.Cells[0]; c.Bg != panel.Bg || c.Attrs != terminal.AttrBold|panel.Attr&terminal.AttrBgColor {
t.Errorf("on %+v: %+v", panel, c)
}
if on := panel.On(Style{Attr: terminal.AttrReverse}); on.Bg != panel.Bg || on.Attr != panel.Attr|terminal.AttrReverse {
if on := panel.On(Style{Attr: terminal.AttrReverse}); on.Bg != panel.Bg || on.Attr != panel.Attr&terminal.AttrBgColor|terminal.AttrFgDefault|terminal.AttrReverse {
t.Errorf("%+v on reverse: %+v", panel, on)
}
}
}
// A reverse cursor row is one bar: its pointer, key, name, hint and blanks
// take one style, whatever each draws in off the row, where they keep it
func TestReverseCursorRowIsOneStyle(t *testing.T) {
s := NewOptionListState([]Option{{Name: "file", Hint: "rotating files", Key: 'f'}, {Name: "tcp", Key: 't'}})
s.Menu = true
for name, th := range map[string]Theme{"16 colors": Theme16, "mono": MonoTheme} {
r := canvas(40, 2, th)
r.OptionList(s, th)
for x, c := range r.Cells[:r.W] {
if first := r.Cells[0]; c.Fg != first.Fg || c.Bg != first.Bg || c.Attrs != first.Attrs {
t.Errorf("%s %q: column %d %+v, column 0 %+v", name, row(r, 0), x, c, first)
break
}
}
if c, a := r.Cells[r.W+2], th.Accent; c.Rune != 't' || c.Fg != a.Fg || c.Attrs&^terminal.AttrBgColor != a.Attr {
t.Errorf("%s %q: the key off the cursor row %+v, want %+v", name, row(r, 1), c, a)
}
}
}
// Every widget marks its selection with a glyph, so it reads with no color
func TestSelectionChangesAGlyph(t *testing.T) {
for _, th := range []Theme{DefaultTheme, Theme16, MonoTheme} {