gitstack

grindlemire/go-tui code browser

39.7 KB markdown 940 lines 2026-06-27 · 4be96e4 raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
# go-tui Project Guidelines

A declarative terminal UI framework for Go with templ-like syntax and flexbox layout.

## Git Commits IMPORTANT

Use `gcommit -m ""` for all commits to ensure proper signing.

ONLY EVER COMMIT USING THIS APPROACH

All commit messages MUST use conventional commit format. This is required for
automated releases via release-please.

Format: `<type>: <description>` or `<type>(<scope>): <description>`

| Prefix | Version Bump | Use When |
|--------|-------------|----------|
| `feat:` | minor (0.1.0 → 0.2.0) | Adding new functionality |
| `fix:` | patch (0.1.0 → 0.1.1) | Fixing a bug |
| `perf:` | patch | Performance improvements |
| `refactor:` | patch | Code changes that don't add features or fix bugs |
| `docs:` | patch | Documentation only |
| `test:` | patch | Adding or updating tests |
| `chore:` | patch | Maintenance, dependencies, tooling |
| `ci:` | patch | CI/CD changes |
| `build:` | patch | Build system changes |
| `revert:` | patch | Reverting a previous commit |

For BREAKING CHANGES (major bump, e.g. 0.1.0 → 1.0.0), add `!` after the type:
`feat!: remove deprecated API` or include `BREAKING CHANGE:` in the commit body.

## Pull Requests IMPORTANT

PR titles MUST pass the GitHub Actions checks before merging. There are two CI
workflows that run on every PR:

1. **PR Title** (`pr-title.yml`): Enforces conventional commit format on the PR
   title using `action-semantic-pull-request`. The title must start with one of
   the allowed types (`feat`, `fix`, `docs`, `chore`, `ci`, `test`, `refactor`,
   `perf`, `build`, `revert`). This is the same format as commit messages.
2. **CI** (`ci.yml`): Runs `go test ./...` on ubuntu-latest.

Both checks must pass. Always verify your PR title matches the conventional
commit format and that tests pass before considering a PR ready.

Examples:
```
gcommit -m "feat: add table element support"
gcommit -m "fix(layout): correct flexbox gap calculation"
gcommit -m "feat!: change State API to require type parameter"
gcommit -m "chore: update golang.org/x dependencies"
```

## Code Style & Linting

We enforce strict formatting, linting, and language modernization standards for all Go files:

- **Formatting**: Format all Go files strictly using `golangci-lint fmt`:
  ```bash
  golangci-lint fmt
  ```
- **Modernization**: Use `go fix` to apply and keep modern Go library usages and constructs updated:
  ```bash
  go fix ./...
  ```
- **Linting**: Verify code quality and static analysis warnings locally using `golangci-lint`:
  ```bash
  golangci-lint run
  ```
- **Template Formatting**: Format custom `.gsx` template files using:
  ```bash
  tui fmt [path...]
  ```

## Project Overview

go-tui allows defining UIs in `.gsx` files that compile to type-safe Go code. The framework provides:

- Declarative component syntax (similar to templ/JSX)
- Pure Go flexbox layout engine (no CGO)
- Minimal external dependencies (golang.org/x/{mod,sync,sys,tools})
- Composable widget system
- Reactive state management with generic `State[T]`
- Language server, formatter, and tree-sitter grammar for editor support

## Architecture

```
.gsx files (declarative syntax)
        │ tui generate (internal/tuigen)

Generated Go code (*_gsx.go)
        │ imports tui "github.com/grindlemire/go-tui"

Widget Tree + Layout Engine (internal/layout)


Character Buffer (double-buffered 2D grid)


Terminal (ANSI escape sequences)
```

All public API types live in the root `tui` package. Internal packages (`internal/layout`,
`internal/tuigen`, `internal/formatter`, `internal/lsp`, `internal/debug`) are not importable
by external consumers.

## Where to Look (By Task)

Use this section to quickly find the right files for a given change.

### Changing the public API (Element options, App config, types)

- `element.go` — Element struct definition, TextAlign/ScrollMode/OverflowMode enums
- `element_options.go` — Option funcs: WithWidth, WithHeight, WithFlexGrow, WithDirection, WithBorder, WithScrollable, WithTruncate, WithHidden, WithOverflow, WithTextGradient, WithBackgroundGradient, WithBorderGradient, WithBorderTitleAlign, WithBorderTitleStyle, WithFocusBorderStyle, WithCursorSource, etc.
- `element_options_auto.go` — WithWidthAuto(), WithHeightAuto()
- `element_accessors.go` — Getters/setters: SetText, SetBorder, SetStyle, Background, SetWidth, SetHeight, SetBorderTitleAlign, SetBorderTitleStyle, SetFocusBorderStyle, etc.
- `element_tree.go` — Tree manipulation: AddChild, RemoveChild, RemoveAllChildren
- `element_scroll.go` — Scroll methods: ScrollTo, ScrollOffset, MaxScroll, ViewportSize
- `app_options.go` — AppOption funcs: WithFrameRate, WithMouseEnabled, WithInlineHeight, WithGlobalKeyHandler, WithInputLatency, WithEventQueueSize, WithPreRenderHook, WithPostRenderHook, WithManualCursor, etc.
- `layout.go` — Re-exports from internal/layout (Direction, Justify, Align, Value, etc.)
- `color.go` — Color type, ANSIColor(), RGBColor(), HexColor(), Gradient, GradientDirection
- `style.go` — Style type with chainable methods (Bold, Dim, Italic, Foreground, etc.)
- `ref.go` — Ref, RefList, RefMap[K] for element references
- `click.go` — Click(), HandleClicks() for ref-based mouse hit testing
- `keymap.go` — KeyMap, KeyBinding, KeyPattern, On(), OnStop(), Rune(), etc.
- `component.go` — Component, KeyListener, MouseListener, Initializer, WatcherProvider interfaces
- `events.go` — Events[T] generic event bus for cross-component communication
- `mount.go` — Component caching and lifecycle (Mount, PropsUpdater)
- `modal.go` -- Modal struct, Render, KeyMap (Escape/Tab/Enter/catch-all), HandleMouse (backdrop + onActivate click)
- `modal_options.go` -- ModalOption funcs: WithModalOpen, WithModalBackdrop, WithModalCloseOnEscape, WithModalCloseOnBackdropClick, WithModalTrapFocus, WithModalElementOptions
- `app_overlay.go` -- Overlay registration: overlayEntry, registerOverlay, clearOverlays

### Changing layout behavior (flexbox algorithm)

- `internal/layout/calculate.go` — Main layout algorithm, `Calculate()` entry point
- `internal/layout/flex.go` — Core flexbox: `layoutChildren()` 6-phase algorithm, justify/align offsets
- `internal/layout/style.go` — LayoutStyle struct, Direction/Justify/Align constants
- `internal/layout/value.go` — Value type: Auto/Fixed/Percent dimension units
- `internal/layout/edges.go` — Edges struct (TRBL spacing), EdgeAll(), EdgeSymmetric()
- `internal/layout/layoutable.go` — Layoutable interface (LayoutStyle, LayoutChildren, SetLayout, etc.)
- `element_layout.go` — Element's implementation of Layoutable interface

### Changing rendering / terminal output

- `element_render.go` — RenderTree(), renderElement(): border drawing, text rendering, child recursion
- `render.go` — Render(), RenderFull(), RenderRegion(): diff computation, full redraw
- `buffer.go` — Double-buffered character grid: front/back, Diff(), Swap(), SetCell(), Fill()
- `cell.go` — Cell struct: Rune, Style, Width (CJK support)
- `render_element.go` — renderElementToBuffer, bufferRowToANSI: standalone element-to-ANSI rendering
- `escape.go` — escBuilder: ANSI escape sequence construction (cursor, colors, styles)
- `terminal.go` — Terminal interface definition
- `terminal_ansi.go` — ANSITerminal implementation (ANSI escape output, capabilities)
- `terminal_unix.go` — Unix raw mode via termios syscalls
- `terminal_windows.go` — Windows raw mode
- `caps.go` — DetectCapabilities(): TERM/COLORTERM env var detection
- `border.go` — BorderStyle type and BorderChars definitions
- `inline_ansi.go` — ANSI escape handling for inline mode
- `inline_session.go` — Inline session management
- `inline_wrap.go` — Inline content wrapping

### Changing event handling / input

- `event.go` — Event interface, KeyEvent, MouseEvent (MouseButton, MouseAction), ResizeEvent types
- `key.go` — Key type: special keys (Escape, Enter, Tab, arrows, Ctrl+A-Z, function keys)
- `keymap.go` — KeyMap, KeyBinding, KeyPattern; helpers: On(), OnStop(), OnPreemptStop(), OnFocused(), Rune(), AnyRune, AnyKey
- `dispatch.go` — Key dispatch table: priority-ordered key binding dispatch by tree position
- `parse.go` — parseInput(): CSI/SS3 sequence parsing, mouse SGR, UTF-8, modifiers
- `reader.go` — EventReader/InterruptibleReader interfaces, stdinReader, PollEvent()
- `reader_types.go` — EventReader and InterruptibleReader interface definitions
- `reader_unix.go` — Unix-specific: selectWithTimeout(), selectWithTimeoutAndInterrupt()
- `reader_windows.go` — Windows-specific terminal input
- `click.go` — ClickBinding, Click(), HandleClicks() for ref-based mouse hit testing
- `app_events.go` — App.Dispatch(): routes ResizeEvent, MouseEvent, key events via FocusManager
- `app_resize_unix.go` — SIGWINCH handler: registerResizeSignal(), dispatches ResizeEvent via event queue
- `app_resize_windows.go` — No-op (Windows has no SIGWINCH)

### Changing focus management

- `focus.go` — Focusable interface, FocusManager: Register(), Next(), Prev(), SetFocus(), Dispatch()
- `focus_group.go` — FocusGroup for Tab/Shift+Tab cycling between components
- `element_focus.go` — Element focus API: IsFocusable, Focus(), Blur(), HandleEvent(), SetOnEvent()
- `element_watchers.go` — Focus tree discovery: WalkFocusables(), SetOnFocusableAdded()

### Changing reactive state / watchers

- `state.go` — State[T] generic type: Bind(), Set(), Get(), Batch(), dirty marking
- `watcher.go` — Watcher interface, Watch() helper, TimerWatcher, TickerWatcher
- `element_watchers.go` — AddWatcher(), Watchers(), WalkWatchers(), SetOnUpdate()
- `dirty.go` — Global dirty flag: MarkDirty(), checkAndClearDirty()

### Changing the app lifecycle / main loop

- `app.go` — App struct, Viewable interface
- `app_loop.go` — Run() main event loop, frame timing, signal handling
- `app_lifecycle.go` — Close(), Stop(), PrintAbove(), PrintAboveElement(), StreamAbove() for inline mode
- `app_render.go` — App.Render(): buffer management, dirty checking, inline vs full-screen
- `app_screen.go` — Alternate screen mode API (EnterAlternateScreen, ExitAlternateScreen)
- `app_inline_startup.go` — Inline screen mode initialization and startup policy
- `mount.go` — Component caching with mark-and-sweep lifecycle (Mount, PropsUpdater)

### Changing the .gsx compiler (code generation)

The compiler pipeline is: **Lexer → Parser → Analyzer → Generator**

- `internal/tuigen/token.go` — TokenType enum, Token struct, Position
- **Lexer** (`internal/tuigen/`):
  - `lexer.go` — Main Lexer struct, NewLexer(), Next()
  - `lexer_utils.go` — Helper functions (readIdent, readString, isLetter)
  - `lexer_goexpr.go` — Go expression lexing inside `{...}`
  - `lexer_strings.go` — String literal parsing (double-quoted, backtick)
- **Parser** (`internal/tuigen/`):
  - `parser.go` — Parser struct, NewParser(), ParseFile(), error synchronization
  - `parser_component.go` — Component definition and parameter parsing
  - `parser_element.go` — Element parsing: attributes, children, self-closing tags
  - `parser_control.go` — Control flow: for, if, else, let/:= bindings
  - `parser_expr.go` — Go expression parsing within elements/attributes
- **AST** (`internal/tuigen/`):
  - `ast.go` — 20+ node types: File, Component, Element, Attribute, ForLoop, IfStmt, LetBinding, GoExpr, ComponentCall, ChildrenSlot, GoFunc, GoDecl, etc.
- **Analyzer** (`internal/tuigen/`):
  - `analyzer.go` — Semantic analysis: validates tags, attributes, imports
  - `analyzer_refs.go` — Named reference tracking and validation
  - `analyzer_state.go` — State variable and binding tracking
- **Generator** (`internal/tuigen/`):
  - `generator.go` — Generator struct, NewGenerator(), Generate(), imports processing
  - `generator_element.go` — Element code generation with option building, ref handling
  - `generator_children.go` — Child element generation, text handling
  - `generator_component.go` — Component function generation
  - `generator_control.go` — Control flow code generation (for, if, let/:= bindings)
- **Tailwind** (`internal/tuigen/`):
  - `tailwind.go` — ParseTailwindClasses(): converts class strings to element options
  - `tailwind_validation.go` — Class syntax validation
  - `tailwind_data.go` — Class definitions and mappings
  - `tailwind_autocomplete.go` — Autocomplete suggestions for classes
- `errors.go` — Error struct with position/message/hint, ErrorList

### Changing the formatter

- `internal/formatter/formatter.go` — Formatter struct, New(), Format(), FormatWithResult()
- `internal/formatter/printer.go` — printer struct: generates formatted .gsx from AST
- `internal/formatter/printer_comments.go` — Comment formatting (leading/trailing/orphan)
- `internal/formatter/printer_control.go` — Control structure formatting (if, for, let/:= bindings)
- `internal/formatter/printer_elements.go` — Element/attribute formatting, multi-line layout
- `internal/formatter/imports.go` — Import fixing via goimports integration

### Changing the LSP (language server)

- **Core** (`internal/lsp/`):
  - `server.go` — Server struct, JSON-RPC communication, document management
  - `router.go` — Request dispatch to handlers and providers
  - `handler.go` — LSP lifecycle: initialize, shutdown, exit
  - `document.go` — Document/DocumentManager for open .gsx file tracking
  - `context.go` — CursorContext: resolved AST info at cursor position
  - `context_resolve.go` — Cursor position resolution logic
  - `index.go` — ComponentIndex: workspace-level symbol indexing
  - `providers.go` — Provider interface definitions, Registry struct
  - `provider_adapters.go` — Adapters between lsp and provider package types
- **Feature files** (`internal/lsp/`):
  - `hover.go`, `completion.go`, `definition.go`, `references.go`
  - `diagnostics.go`, `formatting.go`, `semantic_tokens.go`, `symbols.go`
- **Provider implementations** (`internal/lsp/provider/`):
  - `provider.go` — Provider interface definitions, type aliases
  - `completion.go` / `completion_items.go` — Completion provider
  - `hover.go` — Hover documentation provider
  - `definition.go` / `definition_search.go` — Go-to-definition
  - `references.go` / `references_search.go` — Find references
  - `diagnostics.go` — Diagnostic reporting
  - `formatting.go` — Document formatting
  - `semantic.go` / `semantic_gocode.go` / `semantic_nodes.go` — Semantic tokens
  - `symbols.go` — Document and workspace symbols
- **gopls proxy** (`internal/lsp/gopls/`):
  - `proxy.go` — GoplsProxy: subprocess communication over JSON-RPC
  - `proxy_requests.go` — JSON-RPC request types
  - `mapping.go` — SourceMap: position translation between .gsx and generated .go
  - `generate.go` / `generate_state.go` — Generate virtual Go from .gsx AST for gopls
- **Schema** (`internal/lsp/schema/`):
  - `schema.go` — Element definitions (div, span, p, ul, li, button, input, table, progress, hr, br)
  - `keywords.go` — Keyword definitions (templ, for, if, else, :=)
  - `tailwind.go` — Tailwind class documentation and matching
- **Logging** (`internal/lsp/log/`):
  - `log.go` — Logging with component prefixes (Server, Gopls, Generate, Mapping)

### Changing the CLI tool

- `cmd/tui/main.go` — Entry point, version, CLI dispatcher
- `cmd/tui/generate.go` — `tui generate` command
- `cmd/tui/check.go` — `tui check` command
- `cmd/tui/fmt.go` — `tui fmt` command (supports --stdout, --check)
- `cmd/tui/lsp.go` — `tui lsp` command (supports --log FILE)

### Adding or modifying element types / attributes

- `internal/lsp/schema/schema.go` — Element definitions and attribute schemas
- `internal/tuigen/analyzer.go` — Attribute validation rules
- `internal/tuigen/tailwind.go` — Tailwind class → option mapping
- `element_options.go` — New Option funcs for generated code to call

### Changing modal/overlay behavior

- `modal.go` -- Modal struct, Render, KeyMap, HandleMouse
- `modal_options.go` -- ModalOption funcs
- `app_overlay.go` -- Overlay registration (overlayEntry, registerOverlay, clearOverlays); inline mode guard
- `app_render.go:67-106` -- Overlay render pass: focus scoping, backdrop, render
- `element.go:103-106` -- Overlay flag on Element
- `element_options.go` -- WithOverlay option
- `element_layout.go` -- Overlay children filtered from layout
- `element_render.go` -- Overlay children skipped in normal render
- `element_focus.go` -- Auto focus border highlighting, onActivate
- `buffer.go` -- ApplyDim, FillBlank for backdrop effects
- `focus.go` -- ScopeTo, ClearScope, isInScope for focus trapping
- `dispatch.go` -- Preemptive dispatch pass for modal key blocking
- `keymap.go` -- OnPreemptStop, AnyKey, Preempt flag on KeyBinding
- **Note:** Modals are not supported in inline mode. `registerOverlay` silently skips
  the overlay and logs via `debug.Log` when `inlineHeight > 0` and the app is not in
  alternate screen mode. Use `EnterAlternateScreen()` first.

### Changing markdown rendering

- `markdown.go` -- Markdown component: struct, NewMarkdown, BindApp, UpdateProps, Render, block-walk methods (renderHeading/renderParagraph/renderCodeFence/renderList/renderBlockquote/renderTable), inlineToSpans, single-entry parse cache
- `markdown_options.go` -- MarkdownOption funcs: WithMarkdownSource, WithMarkdownState, WithMarkdownWidth, WithMarkdownTheme
- `markdown_theme.go` -- MarkdownTheme struct and DefaultMarkdownTheme()
- `internal/markdown/` -- zero-dependency parser: Parse() returns a recursive []Block tree (no tui import)
- `richtext.go` -- TextSpan/WithRichText primitive that headings/paragraphs/table cells render through
- `highlight.go` -- CodeHighlighter interface, TokenKind, Palette, DefaultPalette,
  NewHighlighter (built-in), builtinHighlighter; maps internal/highlight tokens to colored TextSpans
- `internal/highlight/` -- zero-dependency syntax tokenizer (no tui import): Tokenize returns
  [][]Token per line; per-language lexers for Go, JSON, Bash, JS/TS; unknown languages fall back to plain
- **gsx integration:** `internal/tuigen/generator_element.go` (isComponentElement, componentConstructor, markdownAttributeToOption), `internal/tuigen/analyzer.go` (knownTags, voidElements, knownAttributes: source/state/theme), `internal/lsp/schema/schema.go` (markdownAttrs)
- **Note:** `<markdown>` is self-closing; content comes from `source`/`state` expression attributes (the generator cannot type-discriminate, so source and state are distinct attributes). The component owns no scroll/keys; wrap it in a scrollable container.
- **Note:** Fenced code blocks are syntax-highlighted by default for Go, JSON, Bash, and
  JS/TS via the theme's `CodeHighlighter`. Set `theme.CodeHighlighter = nil` to
  disable, or `tui.NewHighlighter(customPalette)` to recolor. Unknown languages
  render uncolored. Implement `CodeHighlighter` to plug in another engine (e.g. a
  chroma adapter using chroma's lexer, mapped to per-line `[]TextSpan`).

### Writing tests

- `mock_terminal.go` — MockTerminal: captures operations, maintains internal cell buffer
- `mock_reader.go` — MockEventReader: returns queued events in order
- `cmd/tui/testdata/` — .gsx fixtures and their expected generated .go output

## CLI Commands

```bash
tui generate [path...]       # Generate Go code from .gsx files
tui check [path...]          # Check .gsx files without generating
tui fmt [path...]            # Format .gsx files
tui fmt --check [path...]    # Check formatting without modifying
tui fmt --stdout [path...]   # Write formatted output to stdout
tui lsp                      # Start language server (stdio)
tui lsp --log FILE           # Start language server with logging
tui version                  # Print version
tui help                     # Show help
```

## .gsx File Syntax

```gsx
package mypackage

import (
    "fmt"
)

// Component definition (returns Element)
templ Header(title string) {
    <div class="border-single p-1">
        <span class="font-bold">{title}</span>
    </div>
}

// Conditionals
templ Conditional(show bool) {
    <div class="flex-col">
        if show {
            <span>Visible</span>
        } else {
            <span>Hidden</span>
        }
    </div>
}

// Loops
templ List(items []string) {
    <div class="flex-col gap-1">
        for i, item := range items {
            <span>{fmt.Sprintf("%d: %s", i, item)}</span>
        }
    </div>
}

// Local bindings (element references)
templ Counter(count int) {
    label := <span class="font-bold">{fmt.Sprintf("Count: %d", count)}</span>
    <div>{label}</div>
}

// Component calls
templ App() {
    <div class="flex-col">
        @Header("Hello")
        @Counter(0)
    </div>
}

// Helper functions (regular Go - no Element return type)
func helper(s string) string {
    return fmt.Sprintf("[%s]", s)
}
```

### Built-in Elements (HTML-style)

| Element | Description |
|---------|-------------|
| `<div>` | Block container with flexbox layout |
| `<span>` | Inline text/content container |
| `<p>` | Paragraph text |
| `<ul>` | Unordered list container |
| `<li>` | List item |
| `<button>` | Clickable button |
| `<input>` | Text input field |
| `<textarea>` | Multi-line text input (self-closing, mounted as Component) |
| `<table>` | Table container |
| `<tr>` | Table row |
| `<td>` | Table data cell |
| `<th>` | Table header cell (bold by default) |
| `<progress>` | Progress bar |
| `<hr>` | Horizontal rule (self-closing) |
| `<br>` | Line break (self-closing) |
| `<modal>` | Modal overlay dialog |
| `<markdown>` | Renders a markdown string into the widget tree (self-closing) |

### Common Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Unique identifier |
| `class` | `string` | Tailwind-style classes |
| `disabled` | `bool` | Disable interaction |
| `ref` | expression | Bind element to a `tui.Ref`/`RefList`/`RefMap` variable |
| `key` | expression | Stable identity for a loop item, unique among siblings of the innermost loop (like React keys): mount cache key for component elements, RefMap key with `ref`; a key on a container element keys the components mounted inside it |
| `deps` | expression | Explicit state dependencies for reactive bindings |

### Layout Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `width` | `int` | Fixed width in characters |
| `widthPercent` | `int` | Width as percentage |
| `height` | `int` | Fixed height in rows |
| `heightPercent` | `int` | Height as percentage |
| `minWidth` | `int` | Minimum width |
| `minHeight` | `int` | Minimum height |
| `maxWidth` | `int` | Maximum width |
| `maxHeight` | `int` | Maximum height |
| `direction` | `tui.Direction` | Flex direction |
| `justify` | `tui.Justify` | Main axis alignment |
| `align` | `tui.Align` | Cross axis alignment |
| `alignSelf` | `tui.Align` | Override parent's align for this item |
| `gap` | `int` | Gap between children |
| `flexGrow` | `float64` | Flex grow factor |
| `flexShrink` | `float64` | Flex shrink factor |
| `padding` | `int` | Padding on all sides |
| `margin` | `int` | Margin on all sides |

### Visual Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `border` | `tui.BorderStyle` | Border style |
| `borderStyle` | `string` | Border style name |
| `borderTitle` | `string` | Title text centered in the top border |
| `background` | `tui.Color` | Background color |
| `text` | `string` | Text content |
| `textStyle` | `tui.Style` | Text styling |
| `textAlign` | `string` | Text alignment |

### Event & Focus Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `focusable` | `bool` | Whether the element can receive focus |
| `onFocus` | `func()` | Called when the element gains focus |
| `onBlur` | `func()` | Called when the element loses focus |
| `onActivate` | `func()` | Called when Enter is pressed while focused |

### Scroll Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `scrollable` | `bool` | Enable scrolling for overflow content |
| `scrollbarStyle` | `tui.Style` | Style for the scrollbar track |
| `scrollbarThumbStyle` | `tui.Style` | Style for the scrollbar thumb |
| `hideScrollbar` | `bool` | Hide the scrollbar and reclaim its gutter width |

### Modal Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `open` | `expression` | Bind to a `*State[bool]` to control visibility (required) |
| `backdrop` | `string` | Backdrop style: `"dim"` (default), `"blank"`, or `"none"` |
| `closeOnEscape` | `bool` | Escape key closes the modal (default true) |
| `closeOnBackdropClick` | `bool` | Clicking backdrop closes the modal (default true) |
| `trapFocus` | `bool` | Tab/Shift+Tab restricted to modal children; also blocks unhandled keys from parents (default true) |
| `keyMap` | `expression` | Custom KeyMap bindings for the modal; fire before the catch-all when trapFocus is true |

### Input-specific Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `value` | `string` | Current input value |
| `placeholder` | `string` | Placeholder text when empty |

### Progress-specific Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `value` | `int` | Current progress value (0 to max) |
| `max` | `int` | Maximum progress value |

### Textarea-specific Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `placeholder` | `string` | Placeholder text when empty |
| `width` | `int` | Width in characters (default 40) |
| `maxHeight` | `int` | Maximum height in rows (0 = unlimited) |
| `border` | `tui.BorderStyle` | Border style |
| `textStyle` | `tui.Style` | Text styling |
| `placeholderStyle` | `tui.Style` | Placeholder styling (default: dim) |
| `cursor` | `rune` | Cursor character (default '▌') |
| `submitKey` | `tui.Key` | Submit trigger key (default KeyEnter) |
| `onSubmit` | `func(string)` | Called when submit key is pressed |

### Markdown-specific Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `source` | `string` | Static markdown content (string expression) |
| `state` | `*State[string]` | Reactive markdown source; re-renders on change |
| `width` | `int` | Fixed render width in characters (0 = fill available width) |
| `theme` | `tui.MarkdownTheme` | Override the default styling |

The `<markdown>` tag is self-closing; provide content through `source` or `state`
(literal markdown as children is not supported). `Markdown` is a pure content
renderer, so wrap it in a `scrollable` container to scroll long documents. It
renders headings, bold/italic, inline code, fenced code blocks, pipe tables,
ordered/unordered lists (with nesting), blockquotes, and links (OSC 8 hyperlinks
on capable terminals).

### Tailwind-style Classes

Use the `class` attribute for styling:

```gsx
<div class="flex-col gap-2 p-2 border-rounded">
    <span class="font-bold text-cyan">Title</span>
    <span class="font-dim">Subtitle</span>
</div>
```

**Layout Direction**

| Class | Description |
|-------|-------------|
| `flex` / `flex-row` | Display flex row |
| `flex-col` | Display flex column |

**Flex Sizing**

| Class | Description |
|-------|-------------|
| `grow` | Allow element to grow (flex-grow: 1) |
| `grow-0` | Prevent element from growing |
| `shrink` | Allow element to shrink (flex-shrink: 1) |
| `shrink-0` | Prevent element from shrinking |
| `flex-1` | Grow and shrink (takes available space) |
| `flex-auto` | Grow and shrink (respects content size) |
| `flex-initial` | Don't grow, can shrink |
| `flex-none` | Fixed size (no grow or shrink) |
| `flex-grow-N` | Flex grow factor of N |
| `flex-shrink-N` | Flex shrink factor of N |

**Width & Height**

| Class | Description |
|-------|-------------|
| `w-N` | Fixed width of N characters |
| `h-N` | Fixed height of N rows |
| `w-full` / `h-full` | Full width/height (100%) |
| `w-auto` / `h-auto` | Auto size to content |
| `w-N/M` / `h-N/M` | Fractional size (e.g., `w-1/2` for 50%) |
| `min-w-N` | Minimum width of N characters |
| `max-w-N` | Maximum width of N characters |
| `min-h-N` | Minimum height of N rows |
| `max-h-N` | Maximum height of N rows |

**Justify & Align**

| Class | Description |
|-------|-------------|
| `justify-start` | Justify content to start |
| `justify-center` | Justify content to center |
| `justify-end` | Justify content to end |
| `justify-between` | Justify space between |
| `justify-around` | Justify space around |
| `justify-evenly` | Justify space evenly |
| `items-start` | Align items to start |
| `items-center` | Align items to center |
| `items-end` | Align items to end |
| `items-stretch` | Stretch items to fill |
| `self-start` | Align self to start |
| `self-center` | Align self to center |
| `self-end` | Align self to end |
| `self-stretch` | Stretch self to fill |

**Spacing**

| Class | Description |
|-------|-------------|
| `gap-N` | Gap of N characters between children |
| `p-N` | Padding of N on all sides |
| `px-N` / `py-N` | Horizontal / vertical padding |
| `pt-N` / `pr-N` / `pb-N` / `pl-N` | Individual side padding |
| `m-N` | Margin of N on all sides |
| `mx-N` / `my-N` | Horizontal / vertical margin |
| `mt-N` / `mr-N` / `mb-N` / `ml-N` | Individual side margin |

**Borders**

| Class | Description |
|-------|-------------|
| `border` / `border-single` | Single line border |
| `border-double` | Double line border |
| `border-rounded` | Rounded border |
| `border-thick` | Thick border |
| `border-COLOR` | Border color (red, green, blue, cyan, etc.) |
| `border-[#hex]` | Border color from hex (e.g., `border-[#ff6600]`) |
| `border-gradient-C1-C2[-dir]` | Border gradient (directions: h, v, dd, du) |

**Text Styling**

| Class | Description |
|-------|-------------|
| `font-bold` | Bold text |
| `font-dim` / `text-dim` | Dim/faint text |
| `italic` | Italic text |
| `underline` | Underlined text |
| `strikethrough` | Strikethrough text |
| `blink` | Blinking text |
| `reverse` | Reverse video text |
| `text-left` / `text-center` / `text-right` | Text alignment |
| `truncate` | Truncate text with ellipsis on overflow |

**Colors**

| Class | Description |
|-------|-------------|
| `text-COLOR` | Text color (red, green, blue, cyan, magenta, yellow, white, black) |
| `text-bright-COLOR` | Bright text color variant |
| `text-[#hex]` | Text color from hex (e.g., `text-[#ff0000]`) |
| `bg-COLOR` | Background color |
| `bg-bright-COLOR` | Bright background color variant |
| `bg-[#hex]` | Background color from hex |

**Gradients**

| Class | Description |
|-------|-------------|
| `text-gradient-C1-C2[-dir]` | Text gradient between two colors |
| `bg-gradient-C1-C2[-dir]` | Background gradient between two colors |
| `border-gradient-C1-C2[-dir]` | Border gradient between two colors |

Gradient directions: `-h` (horizontal, default), `-v` (vertical), `-dd` (diagonal down), `-du` (diagonal up). Colors: any named color or bright- variant.

**Scroll & Overflow**

| Class | Description |
|-------|-------------|
| `overflow-scroll` | Enable scrolling in both directions |
| `overflow-y-scroll` | Enable vertical scrolling |
| `overflow-x-scroll` | Enable horizontal scrolling |
| `overflow-hidden` | Clip children without scrollbars |
| `scrollbar-hidden` | Hide scrollbar and reclaim its gutter width |
| `scrollbar-COLOR` | Scrollbar track color |
| `scrollbar-thumb-COLOR` | Scrollbar thumb color |
| `scrollbar-[#hex]` / `scrollbar-thumb-[#hex]` | Hex scrollbar colors |

**Other**

| Class | Description |
|-------|-------------|
| `focusable` | Make element focusable |
| `hidden` | Hide element from layout and rendering |

## Key Types

```go
// tui.Value - dimension specification
tui.Fixed(10)        // 10 characters
tui.Percent(50)      // 50% of available space
tui.Auto()           // Size to content

// tui.BorderStyle
tui.BorderNone
tui.BorderSingle     // ┌─┐│└─┘
tui.BorderDouble     // ╔═╗║╚═╝
tui.BorderRounded    // ╭─╮│╰─╯
tui.BorderThick      // ┏━┓┃┗━┛

// tui.Direction
tui.Row
tui.Column

// tui.Justify
tui.JustifyStart
tui.JustifyCenter
tui.JustifyEnd
tui.JustifySpaceBetween
tui.JustifySpaceAround
tui.JustifySpaceEvenly

// tui.Align
tui.AlignStart
tui.AlignCenter
tui.AlignEnd
tui.AlignStretch

// tui.TextAlign
tui.TextAlignLeft    // default
tui.TextAlignCenter
tui.TextAlignRight

// tui.ScrollMode
tui.ScrollNone       // default
tui.ScrollVertical
tui.ScrollHorizontal
tui.ScrollBoth

// tui.OverflowMode
tui.OverflowVisible  // default
tui.OverflowHidden

// tui.Color - terminal colors
tui.Black, tui.Red, tui.Green, tui.Yellow, tui.Blue, tui.Magenta, tui.Cyan, tui.White
tui.BrightBlack, tui.BrightRed, tui.BrightGreen, tui.BrightYellow, tui.BrightBlue, tui.BrightMagenta, tui.BrightCyan, tui.BrightWhite
tui.ANSIColor(index)        // ANSI 256 palette
tui.RGBColor(r, g, b)       // 24-bit true color
tui.HexColor("#ff6600")     // Hex string to Color
tui.DefaultColor()           // Terminal default

// tui.Gradient - color gradients
tui.NewGradient(tui.Red, tui.Blue)                                    // Horizontal gradient
tui.NewGradient(tui.Red, tui.Blue).WithDirection(tui.GradientVertical) // Vertical
// Directions: GradientHorizontal, GradientVertical, GradientDiagonalDown, GradientDiagonalUp

// tui.Style - text styling (chainable)
tui.NewStyle().Bold().Foreground(tui.Red)
tui.NewStyle().Dim().Italic().Underline().Background(tui.Blue)
// Methods: Bold(), Dim(), Italic(), Underline(), Blink(), Reverse(), Strikethrough(), Foreground(Color), Background(Color)

// tui.State[T] - reactive state
count := tui.NewState(0)
count.Set(count.Get() + 1)
count.Update(func(v int) int { return v + 1 })  // atomic read-modify-write
count.Bind(func(v int) { /* called on change */ })

// tui.Ref - element references for hit testing
ref := tui.NewRef()           // Single element ref
list := tui.NewRefList()      // Multiple elements from loops
m := tui.NewRefMap[string]()  // Keyed element refs

// tui.Events[T] - cross-component event bus
bus := tui.NewEvents[MyEvent]("topic-name")
bus.Emit(MyEvent{...})
unsub := bus.Subscribe(func(e MyEvent) { ... })

// tui.AnyKey - matches any key event (special or printable)
// Used by modal to block parent component key handlers
var AnyKey KeyMatcher

// Preemptive key bindings
tui.OnPreemptStop(matcher, handler)  // Fires before normal handlers, stops propagation
```

## Component Interfaces

Components are structs implementing `Component` (requires `Render(app *App) *Element`).
Additional optional interfaces add capabilities:

| Interface | Method | Description |
|-----------|--------|-------------|
| `Component` | `Render(app *App) *Element` | Required. Returns the element tree. |
| `KeyListener` | `KeyMap() KeyMap` | Keyboard input handling via key bindings |
| `MouseListener` | `HandleMouse(MouseEvent) bool` | Mouse input handling |
| `Initializer` | `Init() func()` | Setup on mount; returned func is cleanup on unmount |
| `WatcherProvider` | `Watchers() []Watcher` | Timers, tickers, channel watchers |
| `PropsUpdater` | `UpdateProps(fresh Component)` | Receive updated props when re-rendered from cache |
| `AppBinder` | `BindApp(app *App)` | Auto-called by mount system for State/Events fields |
| `AppUnbinder` | `UnbindApp()` | Detach app-bound resources on unmount |

## Event Handling

### Key Bindings (KeyMap)

Implement `KeyListener` to handle keyboard input:

```go
func (c *myComponent) KeyMap() tui.KeyMap {
    return tui.KeyMap{
        tui.On(tui.KeyEnter, c.onEnter),              // Specific key, broadcast
        tui.OnStop(tui.KeyEscape, c.onEscape),        // Specific key, stop propagation
        tui.On(tui.Rune('q'), func(ke tui.KeyEvent) { ... }), // Specific character
        tui.OnStop(tui.AnyRune, c.onTyping),          // All printable chars, exclusive
        tui.OnPreemptStop(tui.AnyKey, c.onBlock),     // Preemptive, stops propagation
    }
}
```

The `OnPreemptStop` variant fires in a preemptive pass before all normal handlers. Used by the Modal component to block parent key handlers when the modal is open.

When a hand-written component hosts more than one focusable widget (say a `TextArea` next to an `Input`), mount each with `app.Mount` instead of merging their `KeyMap()`s onto the host. `OnFocused` only gates on focus when the binding's owning component implements `IsFocused()` and is mounted, so merging keymaps onto a host without `IsFocused()` drops the gate and the first widget swallows every key; the dispatch builder now errors when it detects this.

### Mouse Click Handling

Implement `MouseListener` with ref-based hit testing:

```go
func (c *counter) HandleMouse(me tui.MouseEvent) bool {
    return tui.HandleClicks(me,
        tui.Click(c.incrementBtn, c.increment),
        tui.Click(c.decrementBtn, c.decrement),
    )
}
```

### Component Watchers

Implement `WatcherProvider` for component-level timers/channels:

```go
func (t *timer) Watchers() []tui.Watcher {
    return []tui.Watcher{
        tui.OnTimer(time.Second, t.tick),
        tui.NewChannelWatcher(t.dataChan, t.onData),
    }
}
```

### Cross-Component Communication (Events)

Use `Events[T]` for pub/sub between components:

```go
// In component struct
type myComponent struct {
    notifications *tui.Events[Notification]  // created with tui.NewEvents[Notification]("notifications")
}

// Emit from one component
c.notifications.Emit(Notification{Message: "hello"})

// Subscribe in another component
unsub := c.notifications.Subscribe(func(n Notification) { ... })
```

## Layout System

The layout engine (`internal/layout`) implements CSS flexbox with:

- `Row` and `Column` directions
- `JustifyContent`: Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenly
- `AlignItems`: Start, Center, End, Stretch
- Padding, margin, and gap
- Min/max width/height constraints
- Percentage, fixed, and auto values
- Float-precision positioning for jitter-free animation

## Design Patterns

- **Functional Options**: `Element` and `App` use `Option`/`AppOption` funcs for configuration
- **Double Buffering**: `Buffer` maintains front/back grids with diff-based rendering
- **Dirty Flags**: Global dirty flag triggers re-layout/re-render when state changes
- **Reactive State**: `State[T]` with `Bind()` callbacks and `Batch()` for coalescing
- **Interface-based**: `Viewable`, `Layoutable`, `Focusable`, `Watcher`, `Terminal`
- **Tree Walking**: BFS traversal for component dispatch (key/mouse events, watcher collection); DFS for focus discovery and rendering
- **Component Caching**: Mount system with mark-and-sweep cleanup — components are cached by (parent, index) key and reused across renders
- **Ref System**: Type-safe element references (`Ref`, `RefList`, `RefMap[K]`) for event handling and hit testing
- **Event Bus**: Generic `Events[T]` for topic-based pub/sub between components
- **Key Dispatch**: Priority-ordered key binding dispatch by tree position via `KeyListener`/`KeyMap`
- **Preemptive Dispatch**: Modal key bindings fire before normal tree-order dispatch, blocking parent handlers when the overlay is open

## Testing

Use table-driven tests for unit tests that cover multiple cases of the same
behavior, with the following pattern:

```go
type tc struct {
    // test case fields
}

tests := map[string]tc{
    "test name": {
        // test case values
    },
}

for name, tt := range tests {
    t.Run(name, func(t *testing.T) {
        // test logic
    })
}
```

Always define the `tc` struct separately before the test map.

Plain linear tests are fine when there is no case dimension: sequential
lifecycle or state-transition tests, goroutine-coordination tests, and
single-scenario assertion sets. Do not force these into a one-row table.

## Running Tests

```bash
go test ./...                        # Run all tests
go test -race ./...                  # Run all tests with race detector
go test ./internal/tuigen/...        # Run tuigen tests
go test ./internal/lsp/...           # Run LSP tests
go test ./internal/layout/...        # Run layout tests
go test ./internal/formatter/...     # Run formatter tests
go test -run TestParser ./...        # Run specific test
```

## Building

```bash
go build -o tui ./cmd/tui        # Build CLI
./tui generate ./examples/...    # Generate example code
```