v0.1.0 initial commit

This commit is contained in:
2026-07-12 18:41:05 -04:00
commit aa22225c61
69 changed files with 10971 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
package tui
import "github.com/lixenwraith/terminal"
// ToastPosition specifies where toast renders
type ToastPosition uint8
const (
ToastBottom ToastPosition = iota // Full-width bar at bottom
ToastTop // Full-width bar at top
ToastBottomRight // Floating box bottom-right
ToastBottomLeft // Floating box bottom-left
ToastTopRight // Floating box top-right
ToastTopLeft // Floating box top-left
ToastCenter // Centered floating box
)
// ToastSeverity defines message type for styling
type ToastSeverity uint8
const (
ToastInfo ToastSeverity = iota // Default, neutral
ToastSuccess // Green, positive
ToastWarning // Yellow, caution
ToastError // Red, failure
)
// ToastStyle defines visual appearance
type ToastStyle uint8
const (
ToastStyleMinimal ToastStyle = iota // No border, just text
ToastStyleBar // Full-width background bar
ToastStyleBox // Bordered box
ToastStyleRounded // Rounded border box
ToastStyleDouble // Double-line border
ToastStyleShadow // Box with shadow effect
)
// ToastIcons for severity levels
var ToastIcons = map[ToastSeverity]rune{
ToastInfo: '',
ToastSuccess: '✓',
ToastWarning: '⚠',
ToastError: '✗',
}
// ToastColors default colors per severity
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon terminal.RGB }{
ToastInfo: {
Fg: terminal.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 40, G: 40, B: 50},
Icon: terminal.RGB{R: 100, G: 150, B: 255},
},
ToastSuccess: {
Fg: terminal.RGB{R: 220, G: 255, B: 220},
Bg: terminal.RGB{R: 30, G: 60, B: 30},
Icon: terminal.RGB{R: 80, G: 220, B: 80},
},
ToastWarning: {
Fg: terminal.RGB{R: 255, G: 240, B: 200},
Bg: terminal.RGB{R: 60, G: 50, B: 20},
Icon: terminal.RGB{R: 255, G: 200, B: 60},
},
ToastError: {
Fg: terminal.RGB{R: 255, G: 220, B: 220},
Bg: terminal.RGB{R: 60, G: 25, B: 25},
Icon: terminal.RGB{R: 255, G: 80, B: 80},
},
}
// ToastOpts configures toast rendering
type ToastOpts struct {
Message string
Severity ToastSeverity
Position ToastPosition
Style ToastStyle
ShowIcon bool
MinWidth int // Minimum width for floating toasts, 0 = auto
MaxWidth int // Maximum width, 0 = region width
Padding int // Horizontal padding, default 1
MarginX int // Margin from edge for floating positions
MarginY int // Margin from edge for floating positions
CustomFg terminal.RGB
CustomBg terminal.RGB
CustomIcon terminal.RGB
}
// DefaultToastOpts returns sensible defaults
func DefaultToastOpts(message string, severity ToastSeverity) ToastOpts {
return ToastOpts{
Message: message,
Severity: severity,
Position: ToastBottom,
Style: ToastStyleBar,
ShowIcon: true,
Padding: 1,
MarginX: 2,
MarginY: 1,
}
}
// Toast renders a toast message overlay
// Returns the region occupied by the toast for hit testing
func (r Region) Toast(opts ToastOpts) Region {
if r.W < 5 || r.H < 1 || opts.Message == "" {
return Region{}
}
// Resolve colors
fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon
if fg == (terminal.RGB{}) {
fg = ToastColors[opts.Severity].Fg
}
if bg == (terminal.RGB{}) {
bg = ToastColors[opts.Severity].Bg
}
if iconFg == (terminal.RGB{}) {
iconFg = ToastColors[opts.Severity].Icon
}
padding := opts.Padding
if padding == 0 {
padding = 1
}
// Calculate content width
iconW := 0
if opts.ShowIcon {
iconW = 2 // icon + space
}
msgLen := RuneLen(opts.Message)
contentW := iconW + msgLen + padding*2
// Determine toast dimensions based on style
borderW := 0
if opts.Style >= ToastStyleBox {
borderW = 2
}
toastW := contentW + borderW
toastH := 1 + borderW
// Apply width constraints
maxW := opts.MaxWidth
if maxW == 0 || maxW > r.W {
maxW = r.W
}
if toastW > maxW {
toastW = maxW
}
if opts.MinWidth > 0 && toastW < opts.MinWidth {
toastW = opts.MinWidth
}
// Calculate position
var toastX, toastY int
marginX := opts.MarginX
marginY := opts.MarginY
switch opts.Position {
case ToastBottom:
toastX = 0
toastY = r.H - toastH
toastW = r.W // Full width for bar positions
case ToastTop:
toastX = 0
toastY = 0
toastW = r.W
case ToastBottomRight:
toastX = r.W - toastW - marginX
toastY = r.H - toastH - marginY
case ToastBottomLeft:
toastX = marginX
toastY = r.H - toastH - marginY
case ToastTopRight:
toastX = r.W - toastW - marginX
toastY = marginY
case ToastTopLeft:
toastX = marginX
toastY = marginY
case ToastCenter:
toastX = (r.W - toastW) / 2
toastY = (r.H - toastH) / 2
}
// Clamp position
if toastX < 0 {
toastX = 0
}
if toastY < 0 {
toastY = 0
}
toastRegion := r.Sub(toastX, toastY, toastW, toastH)
// Render based on style
switch opts.Style {
case ToastStyleMinimal:
r.renderToastContent(toastRegion, opts, fg, bg, iconFg, 0)
case ToastStyleBar:
toastRegion.Fill(bg)
r.renderToastContent(toastRegion, opts, fg, bg, iconFg, 0)
case ToastStyleBox:
toastRegion.BoxFilled(LineSingle, fg, bg)
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
case ToastStyleRounded:
toastRegion.BoxFilled(LineRounded, fg, bg)
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
case ToastStyleDouble:
toastRegion.BoxFilled(LineDouble, fg, bg)
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
case ToastStyleShadow:
// Shadow offset
shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH)
shadowRegion.Fill(terminal.RGB{R: 10, G: 10, B: 15})
toastRegion.BoxFilled(LineSingle, fg, bg)
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
}
return toastRegion
}
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg terminal.RGB, _ int) {
if content.W < 1 || content.H < 1 {
return
}
x := opts.Padding
y := 0
// Icon
if opts.ShowIcon {
icon := ToastIcons[opts.Severity]
if x < content.W {
content.Cell(x, y, icon, iconFg, bg, terminal.AttrBold)
}
x += 2
}
// Message
msg := opts.Message
availW := content.W - x - opts.Padding
if availW < 1 {
return
}
if RuneLen(msg) > availW {
msg = Truncate(msg, availW)
}
content.Text(x, y, msg, fg, bg, terminal.AttrNone)
}
// ToastState manages toast lifecycle
type ToastState struct {
Visible bool
Opts ToastOpts
FramesLeft int // Countdown to auto-dismiss, -1 = persistent
}
// NewToastState creates a toast that auto-dismisses after frames
// Use frames=-1 for persistent toast
func NewToastState(opts ToastOpts, frames int) *ToastState {
return &ToastState{
Visible: true,
Opts: opts,
FramesLeft: frames,
}
}
// Tick decrements frame counter, returns true if toast should dismiss
func (t *ToastState) Tick() bool {
if !t.Visible {
return false
}
if t.FramesLeft < 0 {
return false // Persistent
}
t.FramesLeft--
if t.FramesLeft <= 0 {
t.Visible = false
return true
}
return false
}
// Dismiss hides the toast
func (t *ToastState) Dismiss() {
t.Visible = false
}
// Show displays a new toast
func (t *ToastState) Show(opts ToastOpts, frames int) {
t.Opts = opts
t.FramesLeft = frames
t.Visible = true
}