5 Commits
Author SHA1 Message Date
Hadi 1bc3ee2821 add border layout config
Signed-off-by: Hadi <[email protected]>
2026-08-24 14:08:36 +02:00
Hadi 2fd1861339 Change overlay display to allow bubblezone to work
Signed-off-by: Hadi <[email protected]>
2026-08-23 17:42:33 +02:00
Hadi b22ce7cbf7 minimum window size package
Signed-off-by: Hadi <[email protected]>
2026-08-23 10:50:15 +02:00
Hadi 0648e7a761 app msgs
Signed-off-by: Hadi <[email protected]>
2026-08-20 20:23:15 +02:00
Hadi 55cb4be01d v0.2.1 2026-08-20 15:43:56 +02:00
12 changed files with 222 additions and 30 deletions
+33
View File
@@ -0,0 +1,33 @@
package app
import tea "charm.land/bubbletea/v2"
type QuitMsg struct{}
func Quit() tea.Cmd {
return func() tea.Msg { return QuitMsg{} }
}
type FocusMsg struct{}
func Focus() tea.Cmd {
return func() tea.Msg { return FocusMsg{} }
}
type BlurMsg struct{}
func Blur() tea.Cmd {
return func() tea.Msg { return BlurMsg{} }
}
type RequestFocusMsg struct{}
func RequestFocus() tea.Cmd {
return func() tea.Msg { return RequestFocusMsg{} }
}
type SetTitleMsg struct{ Title string }
func SetTitle(title string) tea.Cmd {
return func() tea.Msg { return SetTitleMsg{Title: title} }
}
+1 -5
View File
@@ -43,11 +43,7 @@ func (m Model) renderOne(d Drawer, background string, w, h int) string {
x = max(w-bw, 0) x = max(w-bw, 0)
} }
compositor := lipgloss.NewCompositor( return style.Overlay(dim(background, s.DimColor), box, x, 0)
lipgloss.NewLayer(dim(background, s.DimColor)),
lipgloss.NewLayer(box).X(x).Y(0).Z(1),
)
return compositor.Render()
} }
func dim(s string, c color.Color) string { func dim(s string, c color.Color) string {
+16 -2
View File
@@ -1,10 +1,13 @@
package helpbar package helpbar
import ( import (
"strings"
"charm.land/bubbles/v2/help" "charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key" "charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
"github.com/anotherhadi/ilovetui/bubbles" "github.com/anotherhadi/ilovetui/bubbles"
) )
@@ -59,10 +62,21 @@ func (m Model) View(contextual ...key.Binding) string {
if len(bindings) == 0 || m.width <= 0 { if len(bindings) == 0 || m.width <= 0 {
return "" return ""
} }
var view string
if !m.ShowAll { if !m.ShowAll {
return m.help.ShortHelpView(bindings) view = m.help.ShortHelpView(bindings)
} else {
view = m.help.FullHelpView(m.columns(bindings))
} }
return m.help.FullHelpView(m.columns(bindings)) return m.clampWidth(view)
}
func (m Model) clampWidth(view string) string {
lines := strings.Split(view, "\n")
for i, line := range lines {
lines[i] = ansi.Truncate(line, m.width, "")
}
return strings.Join(lines, "\n")
} }
func (m Model) Height(contextual ...key.Binding) int { func (m Model) Height(contextual ...key.Binding) int {
+60
View File
@@ -0,0 +1,60 @@
package minsize
import (
"fmt"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
type Model struct {
MinWidth int
MinHeight int
Style lipgloss.Style
}
type Option func(*Model)
func WithStyle(s lipgloss.Style) Option {
return func(m *Model) { m.Style = s }
}
func New(minWidth, minHeight int, opts ...Option) Model {
m := Model{
MinWidth: minWidth,
MinHeight: minHeight,
Style: lipgloss.NewStyle().Foreground(style.S.Muted),
}
for _, opt := range opts {
opt(&m)
}
return m
}
func (m Model) Fits(width, height int) bool {
return width >= m.MinWidth && height >= m.MinHeight
}
func (m Model) View(width, height int) string {
msg := m.Style.Render(m.message(width, height))
if width < 1 || height < 1 {
return msg
}
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, msg)
}
func (m Model) message(width, height int) string {
tooNarrow := width < m.MinWidth
tooShort := height < m.MinHeight
switch {
case tooNarrow && tooShort:
return fmt.Sprintf("minimum is %dx%d", m.MinWidth, m.MinHeight)
case tooNarrow:
return fmt.Sprintf("minimum width: %d", m.MinWidth)
case tooShort:
return fmt.Sprintf("minimum height: %d", m.MinHeight)
default:
return ""
}
}
+1 -5
View File
@@ -42,11 +42,7 @@ func (m Model) renderOne(mo Modal, background string, w, h int) string {
bw, bh := lipgloss.Width(box), lipgloss.Height(box) bw, bh := lipgloss.Width(box), lipgloss.Height(box)
x, y := max((w-bw)/2, 0), max((h-bh)/2, 0) x, y := max((w-bw)/2, 0), max((h-bh)/2, 0)
compositor := lipgloss.NewCompositor( return style.Overlay(dim(background, s.DimColor), box, x, y)
lipgloss.NewLayer(dim(background, s.DimColor)),
lipgloss.NewLayer(box).X(x).Y(y).Z(1),
)
return compositor.Render()
} }
func dim(s string, c color.Color) string { func dim(s string, c color.Color) string {
+1 -5
View File
@@ -24,11 +24,7 @@ func (m Model) Render(background string) string {
sw, sh := lipgloss.Width(stack), lipgloss.Height(stack) sw, sh := lipgloss.Width(stack), lipgloss.Height(stack)
x, y := placement(m.position, w, h, sw, sh) x, y := placement(m.position, w, h, sw, sh)
compositor := lipgloss.NewCompositor( return style.Overlay(background, stack, x, y)
lipgloss.NewLayer(background),
lipgloss.NewLayer(stack).X(x).Y(y).Z(1),
)
return compositor.Render()
} }
func (m Model) View(width, height int) string { func (m Model) View(width, height int) string {
+62 -2
View File
@@ -4,6 +4,7 @@ import (
"strings" "strings"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
) )
var borderTypes = map[string]lipgloss.Border{ var borderTypes = map[string]lipgloss.Border{
@@ -22,6 +23,25 @@ func resolveBorderType(name string) lipgloss.Border {
return lipgloss.RoundedBorder() return lipgloss.RoundedBorder()
} }
type LayoutBorderMode string
const (
LayoutBorderFull LayoutBorderMode = "full"
LayoutBorderSidebar LayoutBorderMode = "sidebar"
LayoutBorderSeparator LayoutBorderMode = "separator"
)
func resolveLayoutBorder(name string) LayoutBorderMode {
switch strings.ToLower(strings.TrimSpace(name)) {
case string(LayoutBorderSidebar):
return LayoutBorderSidebar
case string(LayoutBorderSeparator):
return LayoutBorderSeparator
default:
return LayoutBorderFull
}
}
func ContentHeight(totalH int) int { func ContentHeight(totalH int) int {
h := totalH - 2 h := totalH - 2
if h < 0 { if h < 0 {
@@ -41,12 +61,18 @@ func RenderWithTitle(border lipgloss.Style, title, content string, width, height
box := border.BorderTop(false).Width(width).Height(boxH).Render(content) box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0]) boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
titleW := lipgloss.Width(title)
b, _, _, _, _ := border.GetBorder() b, _, _, _, _ := border.GetBorder()
topLeft, top, topRight := b.TopLeft, b.Top, b.TopRight topLeft, top, topRight := b.TopLeft, b.Top, b.TopRight
overhead := lipgloss.Width(topLeft) + lipgloss.Width(topRight) + 2
fillW := boxWidth - titleW - lipgloss.Width(topLeft) - lipgloss.Width(topRight) - 2 maxTitleW := max(boxWidth-overhead, 0)
if titleW := lipgloss.Width(title); titleW > maxTitleW {
title = ansi.Truncate(title, maxTitleW, "")
}
titleW := lipgloss.Width(title)
fillW := boxWidth - titleW - overhead
if fillW < 0 { if fillW < 0 {
fillW = 0 fillW = 0
} }
@@ -55,3 +81,37 @@ func RenderWithTitle(border lipgloss.Style, title, content string, width, height
return lipgloss.JoinVertical(lipgloss.Left, topLine, box) return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
} }
func RenderPlain(border lipgloss.Style, title, content string, width, height int) string {
if titleW := lipgloss.Width(title); titleW > width {
title = ansi.Truncate(title, width, "")
}
titleLine := lipgloss.NewStyle().
Bold(true).
Foreground(border.GetBorderTopForeground()).
Width(width).
Render(title)
contentH := max(height-1, 0)
lines := strings.Split(content, "\n")
if len(lines) > contentH {
content = strings.Join(lines[:contentH], "\n")
}
box := lipgloss.NewStyle().Width(width).Height(contentH).Render(content)
return lipgloss.JoinVertical(lipgloss.Left, titleLine, box)
}
func VerticalDivider(border lipgloss.Style, height int) string {
b, _, _, _, _ := border.GetBorder()
ch := b.Left
if ch == "" {
ch = "│"
}
line := lipgloss.NewStyle().Foreground(border.GetBorderLeftForeground()).Render(ch)
rows := make([]string, max(height, 0))
for i := range rows {
rows[i] = line
}
return strings.Join(rows, "\n")
}
+7 -5
View File
@@ -20,9 +20,10 @@ type colorsYAML struct {
} }
type configYAML struct { type configYAML struct {
Colors colorsYAML `yaml:"colors"` Colors colorsYAML `yaml:"colors"`
NerdFonts bool `yaml:"nerd_fonts"` NerdFonts bool `yaml:"nerd_fonts"`
Border string `yaml:"border"` Border string `yaml:"border"`
LayoutBorder string `yaml:"layout_border"`
} }
func pickString(base, user string) string { func pickString(base, user string) string {
@@ -36,8 +37,9 @@ func mergeConfig(base, user configYAML) configYAML {
return configYAML{ return configYAML{
Colors: mergeColors(base.Colors, user.Colors), Colors: mergeColors(base.Colors, user.Colors),
NerdFonts: base.NerdFonts || user.NerdFonts, NerdFonts: base.NerdFonts || user.NerdFonts,
Border: pickString(base.Border, user.Border), Border: pickString(base.Border, user.Border),
LayoutBorder: pickString(base.LayoutBorder, user.LayoutBorder),
} }
} }
+6
View File
@@ -9,6 +9,12 @@ nerd_fonts: false
# One of: rounded, normal, thick, double, hidden, ascii. # One of: rounded, normal, thick, double, hidden, ascii.
border: rounded border: rounded
# How sidebar+content layouts draw their borders. One of:
# full: both the sidebar and content panes get a full titled border.
# sidebar: only the sidebar gets a full border
# separator: neither pane is bordered; a single vertical line divides them.
layout_border: full
colors: colors:
base00: "#110F12" # Background base00: "#110F12" # Background
base01: "#1C1920" # Lighter Background / Status Bars base01: "#1C1920" # Lighter Background / Status Bars
+26
View File
@@ -0,0 +1,26 @@
package style
import (
"strings"
"github.com/charmbracelet/x/ansi"
)
func Overlay(base, top string, x, y int) string {
baseLines := strings.Split(base, "\n")
topLines := strings.Split(top, "\n")
for i, tl := range topLines {
row := y + i
if row < 0 || row >= len(baseLines) {
continue
}
bl := baseLines[row]
tw := ansi.StringWidth(tl)
left := ansi.Cut(bl, 0, x)
right := ansi.Cut(bl, x+tw, ansi.StringWidth(bl))
baseLines[row] = left + tl + right
}
return strings.Join(baseLines, "\n")
}
+8 -5
View File
@@ -39,8 +39,9 @@ type Styles struct {
Bold lipgloss.Style Bold lipgloss.Style
Faint lipgloss.Style Faint lipgloss.Style
NerdFonts bool NerdFonts bool
BorderType lipgloss.Border BorderType lipgloss.Border
LayoutBorder LayoutBorderMode
Panel lipgloss.Style Panel lipgloss.Style
PanelFocused lipgloss.Style PanelFocused lipgloss.Style
@@ -49,7 +50,7 @@ type Styles struct {
PagerDotInactive string PagerDotInactive string
} }
func newStyles(c colorsYAML, nerdFonts bool, borderName string) Styles { func newStyles(c colorsYAML, nerdFonts bool, borderName, layoutBorderName string) Styles {
lc := func(s string) color.Color { lc := func(s string) color.Color {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if s != "" && s[0] != '#' { if s != "" && s[0] != '#' {
@@ -76,6 +77,7 @@ func newStyles(c colorsYAML, nerdFonts bool, borderName string) Styles {
b0F := lc(c.Base0F) b0F := lc(c.Base0F)
borderType := resolveBorderType(borderName) borderType := resolveBorderType(borderName)
layoutBorder := resolveLayoutBorder(layoutBorderName)
return Styles{ return Styles{
Base00: b00, Base01: b01, Base02: b02, Base03: b03, Base00: b00, Base01: b01, Base02: b02, Base03: b03,
@@ -97,8 +99,9 @@ func newStyles(c colorsYAML, nerdFonts bool, borderName string) Styles {
Bold: lipgloss.NewStyle().Bold(true), Bold: lipgloss.NewStyle().Bold(true),
Faint: lipgloss.NewStyle().Foreground(b03).Faint(true), Faint: lipgloss.NewStyle().Foreground(b03).Faint(true),
NerdFonts: nerdFonts, NerdFonts: nerdFonts,
BorderType: borderType, BorderType: borderType,
LayoutBorder: layoutBorder,
Panel: lipgloss.NewStyle(). Panel: lipgloss.NewStyle().
Border(borderType). Border(borderType).
+1 -1
View File
@@ -72,7 +72,7 @@ func stylesFromBytes(data []byte) (Styles, error) {
return Styles{}, fmt.Errorf("style: parse config: %w", err) return Styles{}, fmt.Errorf("style: parse config: %w", err)
} }
merged := mergeConfig(base, user) merged := mergeConfig(base, user)
return newStyles(merged.Colors, merged.NerdFonts, merged.Border), nil return newStyles(merged.Colors, merged.NerdFonts, merged.Border, merged.LayoutBorder), nil
} }
func configDir() string { func configDir() string {