chore: jotts-go tui improvements d0c90703
Steve · 2026-05-17 11:41 3 file(s) · +84 −15
apps/jotts-go/tui/render_md.go +72 −2
4 4
	"fmt"
5 5
6 6
	"github.com/charmbracelet/glamour"
7 +
	"github.com/charmbracelet/glamour/ansi"
7 8
)
8 9
9 10
type mdRenderer struct {
12 13
	cache map[string]string
13 14
}
14 15
16 +
func sp(s string) *string { return &s }
17 +
func bp(b bool) *bool     { return &b }
18 +
func up(u uint) *uint     { return &u }
19 +
20 +
// ansiStyle uses only base ANSI palette indexes (0-7 for normal, 8-15 bright)
21 +
// so colors follow the terminal theme instead of hardcoded hex/256 values.
22 +
func ansiStyle() ansi.StyleConfig {
23 +
	return ansi.StyleConfig{
24 +
		Document: ansi.StyleBlock{
25 +
			StylePrimitive: ansi.StylePrimitive{BlockPrefix: "\n", BlockSuffix: "\n"},
26 +
			Margin:         up(2),
27 +
		},
28 +
		BlockQuote: ansi.StyleBlock{
29 +
			Indent:      up(1),
30 +
			IndentToken: sp("│ "),
31 +
		},
32 +
		Paragraph: ansi.StyleBlock{},
33 +
		List: ansi.StyleList{
34 +
			LevelIndent: 2,
35 +
		},
36 +
		Heading: ansi.StyleBlock{
37 +
			StylePrimitive: ansi.StylePrimitive{BlockSuffix: "\n", Color: sp("4"), Bold: bp(true)},
38 +
		},
39 +
		H1: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Prefix: "# ", Color: sp("4"), Bold: bp(true)}},
40 +
		H2: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Prefix: "## ", Color: sp("4"), Bold: bp(true)}},
41 +
		H3: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Prefix: "### ", Color: sp("6"), Bold: bp(true)}},
42 +
		H4: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Prefix: "#### ", Color: sp("6")}},
43 +
		H5: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Prefix: "##### ", Color: sp("6")}},
44 +
		H6: ansi.StyleBlock{StylePrimitive: ansi.StylePrimitive{Prefix: "###### ", Color: sp("6")}},
45 +
		Strikethrough: ansi.StylePrimitive{CrossedOut: bp(true)},
46 +
		Emph:          ansi.StylePrimitive{Italic: bp(true)},
47 +
		Strong:        ansi.StylePrimitive{Bold: bp(true)},
48 +
		HorizontalRule: ansi.StylePrimitive{
49 +
			Color:  sp("8"),
50 +
			Format: "\n--------\n",
51 +
		},
52 +
		Item:        ansi.StylePrimitive{BlockPrefix: "• "},
53 +
		Enumeration: ansi.StylePrimitive{BlockPrefix: ". "},
54 +
		Task: ansi.StyleTask{
55 +
			Ticked:   "[✓] ",
56 +
			Unticked: "[ ] ",
57 +
		},
58 +
		Link:     ansi.StylePrimitive{Color: sp("6"), Underline: bp(true)},
59 +
		LinkText: ansi.StylePrimitive{Color: sp("2"), Bold: bp(true)},
60 +
		Image:    ansi.StylePrimitive{Color: sp("5"), Underline: bp(true)},
61 +
		ImageText: ansi.StylePrimitive{
62 +
			Color:  sp("8"),
63 +
			Format: "Image: {{.text}} →",
64 +
		},
65 +
		Code: ansi.StyleBlock{
66 +
			StylePrimitive: ansi.StylePrimitive{Color: sp("1"), Prefix: "`", Suffix: "`"},
67 +
		},
68 +
		CodeBlock: ansi.StyleCodeBlock{
69 +
			StyleBlock: ansi.StyleBlock{
70 +
				StylePrimitive: ansi.StylePrimitive{Color: sp("7")},
71 +
				Margin:         up(2),
72 +
			},
73 +
		},
74 +
		Table: ansi.StyleTable{
75 +
			CenterSeparator: sp("┼"),
76 +
			ColumnSeparator: sp("│"),
77 +
			RowSeparator:    sp("─"),
78 +
		},
79 +
		DefinitionDescription: ansi.StylePrimitive{BlockPrefix: "\n* "},
80 +
	}
81 +
}
82 +
15 83
func newRenderer(width int) *mdRenderer {
16 84
	if width < 20 {
17 85
		width = 80
18 86
	}
87 +
	style := ansiStyle()
19 88
	r, _ := glamour.NewTermRenderer(
20 -
		glamour.WithAutoStyle(),
89 +
		glamour.WithStyles(style),
21 90
		glamour.WithWordWrap(width-2),
22 91
	)
23 92
	return &mdRenderer{r: r, width: width, cache: map[string]string{}}
27 96
	if width == m.width || width < 20 {
28 97
		return
29 98
	}
99 +
	style := ansiStyle()
30 100
	r, _ := glamour.NewTermRenderer(
31 -
		glamour.WithAutoStyle(),
101 +
		glamour.WithStyles(style),
32 102
		glamour.WithWordWrap(width-2),
33 103
	)
34 104
	m.r = r
apps/jotts-go/tui/update.go +3 −3
125 125
	m.contentVP.Width = maxInt(contentInnerW, 1)
126 126
	m.contentVP.Height = maxInt(contentInnerH-1, 1)
127 127
128 -
	m.titleInput.Width = maxInt(contentInnerW-2, 1)
129 -
	m.contentArea.SetWidth(maxInt(contentInnerW, 1))
130 -
	m.contentArea.SetHeight(maxInt(contentInnerH-4, 1))
128 +
	m.titleInput.Width = maxInt(contentInnerW-4, 1)
129 +
	m.contentArea.SetWidth(maxInt(contentInnerW-2, 1))
130 +
	m.contentArea.SetHeight(maxInt(contentInnerH-6, 1))
131 131
132 132
	listOuterW, _ := splitWidths(m.width)
133 133
	listInnerW := maxInt(listOuterW-paneFrameWidth(), 1)
apps/jotts-go/tui/view.go +9 −10
10 10
var (
11 11
	borderStyle = lipgloss.NewStyle().
12 12
			Border(lipgloss.NormalBorder()).
13 -
			BorderForeground(lipgloss.Color("240"))
13 +
			BorderForeground(lipgloss.Color("8"))
14 14
	borderActive = lipgloss.NewStyle().
15 15
			Border(lipgloss.NormalBorder()).
16 -
			BorderForeground(lipgloss.Color("214"))
16 +
			BorderForeground(lipgloss.Color("3"))
17 17
	titleStyle = lipgloss.NewStyle().
18 18
			Bold(true).
19 -
			Foreground(lipgloss.Color("214")).
19 +
			Foreground(lipgloss.Color("3")).
20 20
			Padding(0, 1)
21 21
	itemStyle    = lipgloss.NewStyle().Padding(0, 1)
22 22
	itemSelected = lipgloss.NewStyle().
23 23
			Padding(0, 1).
24 24
			Bold(true).
25 -
			Foreground(lipgloss.Color("214"))
25 +
			Foreground(lipgloss.Color("3"))
26 26
	statusOK = lipgloss.NewStyle().
27 -
			Foreground(lipgloss.Color("82")).
27 +
			Foreground(lipgloss.Color("2")).
28 28
			Bold(true)
29 29
	statusErr = lipgloss.NewStyle().
30 -
			Foreground(lipgloss.Color("196")).
30 +
			Foreground(lipgloss.Color("1")).
31 31
			Bold(true)
32 32
	hintStyle = lipgloss.NewStyle().
33 -
			Foreground(lipgloss.Color("244"))
33 +
			Foreground(lipgloss.Color("8"))
34 34
	modalStyle = lipgloss.NewStyle().
35 35
			Border(lipgloss.RoundedBorder()).
36 -
			BorderForeground(lipgloss.Color("214")).
37 -
			Padding(1, 2).
38 -
			Background(lipgloss.Color("236"))
36 +
			BorderForeground(lipgloss.Color("3")).
37 +
			Padding(1, 2)
39 38
)
40 39
41 40
func (m Model) View() string {