v0.1.0 initial commit

This commit is contained in:
2026-07-12 17:01:37 -04:00
commit c3ca2ebb63
64 changed files with 10453 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
package tui
import "github.com/lixenwraith/terminal"
// FormField pairs a label with an editable text field
type FormField struct {
Label string
State *TextFieldState
}
// FormState holds state for a multi-field form with focus tracking
type FormState struct {
Fields []FormField
Focus int
}
// NewFormState creates a form with labeled fields initialized to empty values
func NewFormState(labels ...string) *FormState {
fields := make([]FormField, len(labels))
for i, label := range labels {
fields[i] = FormField{
Label: label,
State: NewTextFieldState(""),
}
}
return &FormState{Fields: fields}
}
// Value returns the text value of the field at idx
func (f *FormState) Value(idx int) string {
if idx >= 0 && idx < len(f.Fields) {
return f.Fields[idx].State.Value()
}
return ""
}
// SetValue replaces the text of the field at idx
func (f *FormState) SetValue(idx int, val string) {
if idx >= 0 && idx < len(f.Fields) {
f.Fields[idx].State.SetValue(val)
}
}
// Clear empties all fields in the form
func (f *FormState) Clear() {
for i := range f.Fields {
f.Fields[i].State.Clear()
}
}
// FocusNext moves focus to the next field, wrapping around
func (f *FormState) FocusNext() {
if len(f.Fields) > 0 {
f.Focus = (f.Focus + 1) % len(f.Fields)
}
}
// FocusPrev moves focus to the previous field, wrapping around
func (f *FormState) FocusPrev() {
if len(f.Fields) > 0 {
f.Focus = (f.Focus - 1 + len(f.Fields)) % len(f.Fields)
}
}
// CurrentField returns the TextFieldState of the focused field, or nil
func (f *FormState) CurrentField() *TextFieldState {
if f.Focus >= 0 && f.Focus < len(f.Fields) {
return f.Fields[f.Focus].State
}
return nil
}
// HandleKey processes keyboard input for form navigation and field editing, returns true if state changed
func (f *FormState) HandleKey(key terminal.Key, r rune, mod terminal.Modifier) bool {
switch key {
case terminal.KeyTab:
if mod&terminal.ModShift != 0 {
f.FocusPrev()
} else {
f.FocusNext()
}
return true
case terminal.KeyUp:
f.FocusPrev()
return true
case terminal.KeyDown:
f.FocusNext()
return true
default:
if field := f.CurrentField(); field != nil {
return field.HandleKey(key, r, mod)
}
}
return false
}
// FormOpts configures form rendering
type FormOpts struct {
LabelWidth int
Spacing int
Style FormStyle
}
// FormStyle defines form colors
type FormStyle struct {
LabelFg terminal.RGB
FieldFg terminal.RGB
FieldBg terminal.RGB
FocusBg terminal.RGB
CursorFg terminal.RGB
CursorBg terminal.RGB
Bg terminal.RGB
}
// DefaultFormStyle returns default form colors
func DefaultFormStyle() FormStyle {
return FormStyle{
LabelFg: terminal.RGB{R: 150, G: 150, B: 180},
FieldFg: terminal.RGB{R: 220, G: 220, B: 220},
FieldBg: terminal.RGB{R: 35, G: 35, B: 45},
FocusBg: terminal.RGB{R: 45, G: 45, B: 60},
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
Bg: terminal.RGB{R: 25, G: 25, B: 35},
}
}
// Form renders a multi-field form with labels and editable text fields, returns height used
func (r Region) Form(state *FormState, opts FormOpts) int {
if len(state.Fields) == 0 || r.H < 1 {
return 0
}
style := opts.Style
if style == (FormStyle{}) {
style = DefaultFormStyle()
}
labelW := opts.LabelWidth
if labelW <= 0 {
for _, f := range state.Fields {
if w := RuneLen(f.Label); w > labelW {
labelW = w
}
}
labelW += 2
}
spacing := opts.Spacing
if spacing < 1 {
spacing = 1
}
y := 0
for i, field := range state.Fields {
if y >= r.H {
break
}
isFocused := i == state.Focus
// Label
label := field.Label + ":"
for j, ch := range label {
if j >= labelW || j >= r.W {
break
}
r.Cell(j, y, ch, style.LabelFg, style.Bg, terminal.AttrNone)
}
// Field area
fieldX := labelW
fieldW := r.W - labelW
if fieldW < 1 {
y += spacing
continue
}
fieldBg := style.FieldBg
if isFocused {
fieldBg = style.FocusBg
}
for x := fieldX; x < fieldX+fieldW && x < r.W; x++ {
r.Cell(x, y, ' ', style.FieldFg, fieldBg, terminal.AttrNone)
}
// Render text using existing TextField logic
text := field.State.Text
scroll := field.State.Scroll
cursor := field.State.Cursor
field.State.AdjustScroll(fieldW)
scroll = field.State.Scroll
for x := 0; x < fieldW; x++ {
runeIdx := scroll + x
ch := ' '
if runeIdx < len(text) {
ch = text[runeIdx]
}
fg := style.FieldFg
bg := fieldBg
if isFocused && runeIdx == cursor {
fg = style.CursorFg
bg = style.CursorBg
}
if fieldX+x < r.W {
r.Cell(fieldX+x, y, ch, fg, bg, terminal.AttrNone)
}
}
// Cursor at end of text
if isFocused && cursor == len(text) {
cursorX := fieldX + cursor - scroll
if cursorX >= fieldX && cursorX < fieldX+fieldW && cursorX < r.W {
r.Cell(cursorX, y, ' ', style.CursorFg, style.CursorBg, terminal.AttrNone)
}
}
y += spacing
}
return y
}