Skip to content

Commit 5a650be

Browse files
feat: redesign TUI with big wordmark and rule-based layout
1 parent 07094ac commit 5a650be

1 file changed

Lines changed: 96 additions & 61 deletions

File tree

ui_tui.go

Lines changed: 96 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import (
99
"github.com/charmbracelet/lipgloss"
1010
)
1111

12-
const feedSize = 20
12+
const feedSize = 15
13+
14+
// figlet -f big HTTPSDEV
15+
const logo = ` _ _ _______ _______ _____ _____ _____ ________ __
16+
| | | |__ __|__ __| __ \ / ____| __ \| ____\ \ / /
17+
| |__| | | | | | | |__) | (___ | | | | |__ \ \ / /
18+
| __ | | | | | | ___/ \___ \| | | | __| \ \/ /
19+
| | | | | | | | | | ____) | |__| | |____ \ /
20+
|_| |_| |_| |_| |_| |_____/|_____/|______| \/ `
1321

1422
type tickMsg time.Time
1523
type eventMsg Event
@@ -23,35 +31,37 @@ type model struct {
2331
errors int
2432
totalDur time.Duration
2533
startedAt time.Time
26-
tab int // 0 = requests, 1 = cert info
34+
width int
2735
}
2836

2937
func initialModel(listenAddr, upstream string) model {
3038
return model{
3139
listenAddr: listenAddr,
3240
upstream: upstream,
3341
startedAt: time.Now(),
42+
width: 100,
3443
}
3544
}
3645

3746
func (m model) Init() tea.Cmd { return tea.Batch(tick(), tea.EnterAltScreen) }
3847

3948
func tick() tea.Cmd {
40-
return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { return tickMsg(t) })
49+
return tea.Tick(200*time.Millisecond, func(t time.Time) tea.Msg { return tickMsg(t) })
4150
}
4251

4352
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4453
switch msg := msg.(type) {
54+
case tea.WindowSizeMsg:
55+
m.width = msg.Width
4556
case tea.KeyMsg:
4657
switch msg.String() {
4758
case "q", "ctrl+c":
4859
return m, tea.Quit
4960
case "c":
5061
m.feed = nil
51-
case "1":
52-
m.tab = 0
53-
case "2":
54-
m.tab = 1
62+
m.total = 0
63+
m.errors = 0
64+
m.totalDur = 0
5565
}
5666
case tickMsg:
5767
return m, tick()
@@ -74,76 +84,101 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7484

7585
// Catppuccin Mocha palette.
7686
var (
77-
accent = lipgloss.Color("#cba6f7") // mauve — active/focus/title
78-
muted = lipgloss.Color("#6c7086") // overlay0 — dividers, labels, inactive
79-
ok = lipgloss.Color("#a6e3a1") // green — 2xx / good counts
80-
warn = lipgloss.Color("#fab387") // peach — 3xx
81-
bad = lipgloss.Color("#f38ba8") // red — 4xx/5xx / error counts
82-
83-
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(accent)
84-
labelStyle = lipgloss.NewStyle().Foreground(muted)
87+
accent = lipgloss.Color("#cba6f7") // mauve
88+
muted = lipgloss.Color("#6c7086") // overlay0
89+
ok = lipgloss.Color("#a6e3a1") // green
90+
warn = lipgloss.Color("#fab387") // peach
91+
bad = lipgloss.Color("#f38ba8") // red
92+
93+
logoStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
8594
dimStyle = lipgloss.NewStyle().Foreground(muted).Faint(true)
86-
sectionHdr = lipgloss.NewStyle().Foreground(accent).Bold(true)
87-
borderStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(muted).Padding(0, 1)
95+
mutedStyle = lipgloss.NewStyle().Foreground(muted)
96+
accentStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
8897
statOK = lipgloss.NewStyle().Foreground(ok)
8998
statBad = lipgloss.NewStyle().Foreground(bad)
9099
statWarn = lipgloss.NewStyle().Foreground(warn)
91-
tabActive = lipgloss.NewStyle().Foreground(accent).Bold(true)
92-
tabInactive = lipgloss.NewStyle().Foreground(muted)
93-
chev = lipgloss.NewStyle().Foreground(accent).Render("❯")
100+
arrowStyle = lipgloss.NewStyle().Foreground(muted)
101+
chevStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
94102
)
95103

96104
func (m model) View() string {
105+
w := m.width
106+
if w < 80 {
107+
w = 80
108+
}
109+
110+
// 1. Wordmark centered + tagline
111+
logoLines := strings.Split(logo, "\n")
112+
logoWidth := 0
113+
for _, l := range logoLines {
114+
if lipgloss.Width(l) > logoWidth {
115+
logoWidth = lipgloss.Width(l)
116+
}
117+
}
118+
pad := (w - logoWidth) / 2
119+
if pad < 0 {
120+
pad = 0
121+
}
122+
padStr := strings.Repeat(" ", pad)
123+
var logoBlock strings.Builder
124+
for _, l := range logoLines {
125+
logoBlock.WriteString(padStr + logoStyle.Render(l) + "\n")
126+
}
127+
tagline := "zero-config https for dev servers"
128+
taglinePad := (w - len(tagline)) / 2
129+
if taglinePad < 0 {
130+
taglinePad = 0
131+
}
132+
logoBlock.WriteString(strings.Repeat(" ", taglinePad) + dimStyle.Render(tagline) + "\n")
133+
134+
// 2. Connection strip + cert info
135+
connLine := " " + accentStyle.Render("▶") + " " +
136+
accentStyle.Render("https://"+m.listenAddr) + " " +
137+
arrowStyle.Render("⇢") + " " +
138+
mutedStyle.Render(m.upstream)
139+
certLine := " " + dimStyle.Render("mkcert · valid 30d")
140+
141+
// 3. Live section header with inline stats + trailing rule
97142
var avg time.Duration
98143
if m.total > 0 {
99144
avg = m.totalDur / time.Duration(m.total)
100145
}
101146
uptime := time.Since(m.startedAt).Round(time.Second)
102-
103-
header := chev + " " + titleStyle.Render("httpsdev v"+Version) + " " +
104-
labelStyle.Render(m.listenAddr+" → "+m.upstream)
105-
106-
statsHdr := sectionHdr.Render("▲ live")
107-
statsBox := borderStyle.Render(fmt.Sprintf(
108-
"total %d errors %s avg %s uptime %s",
147+
statsText := fmt.Sprintf("%d req · %s err · %s avg · %s",
109148
m.total,
110149
colorInt(m.errors, m.errors == 0),
111150
avg.Round(time.Millisecond),
112151
uptime,
113-
))
114-
115-
var mainHdr, mainBox string
116-
switch m.tab {
117-
case 0:
118-
mainHdr = sectionHdr.Render("▲ requests")
119-
mainBox = borderStyle.Render(m.renderFeed())
120-
case 1:
121-
mainHdr = sectionHdr.Render("▲ cert")
122-
mainBox = borderStyle.Render(strings.Join([]string{
123-
"cert " + labelStyle.Render("mkcert-signed"),
124-
"subject " + labelStyle.Render("localhost"),
125-
"valid " + labelStyle.Render("30 days (rolling)"),
126-
"",
127-
dimStyle.Render("more introspection in v0.2"),
128-
}, "\n"))
152+
)
153+
hdrPrefix := mutedStyle.Render(" ── ") + accentStyle.Render("live") + mutedStyle.Render(" ──────── ") + statsText + " "
154+
hdrFill := w - lipgloss.Width(hdrPrefix) - 2
155+
if hdrFill < 0 {
156+
hdrFill = 0
129157
}
158+
sectionHdr := hdrPrefix + mutedStyle.Render(strings.Repeat("─", hdrFill))
130159

131-
tabs := []string{"1 requests", "2 cert"}
132-
if m.tab == 0 {
133-
tabs[0] = tabActive.Render(tabs[0])
134-
tabs[1] = tabInactive.Render(tabs[1])
135-
} else {
136-
tabs[0] = tabInactive.Render(tabs[0])
137-
tabs[1] = tabActive.Render(tabs[1])
138-
}
139-
footer := strings.Join(tabs, " ") + labelStyle.Render(" q quit · c clear")
160+
// 4. Request feed
161+
feed := m.renderFeed()
162+
163+
// 5. Bottom rule
164+
bottomRule := " " + mutedStyle.Render(strings.Repeat("─", w-4))
165+
166+
// 6. Keybar
167+
keybar := " " + dimStyle.Render("[c]") + mutedStyle.Render(" clear ") +
168+
dimStyle.Render("[q]") + mutedStyle.Render(" quit")
140169

141-
return strings.Join([]string{"", header, "", statsHdr, statsBox, "", mainHdr, mainBox, "", footer}, "\n")
170+
return "\n" + logoBlock.String() + "\n" +
171+
connLine + "\n" +
172+
certLine + "\n\n" +
173+
sectionHdr + "\n\n" +
174+
feed + "\n\n" +
175+
bottomRule + "\n\n" +
176+
keybar
142177
}
143178

144179
func (m model) renderFeed() string {
145180
if len(m.feed) == 0 {
146-
return dimStyle.Render("waiting for requests…")
181+
return " " + dimStyle.Render("waiting for requests…")
147182
}
148183
var b strings.Builder
149184
lastIdx := len(m.feed) - 1
@@ -157,23 +192,23 @@ func (m model) renderFeed() string {
157192
case ev.Status >= 300:
158193
s = statWarn
159194
}
160-
marker := " "
195+
marker := " " + arrowStyle.Render("→") + " "
161196
if i == lastIdx {
162-
marker = chev + " "
197+
marker = " " + chevStyle.Render("❯") + " "
163198
}
164-
fmt.Fprintf(&b, "%s%-6s %-30s %s %s\n",
199+
fmt.Fprintf(&b, "%s%-6s %-36s %s %s\n",
165200
marker,
166201
ev.Method,
167-
truncate(ev.Path, 30),
202+
truncate(ev.Path, 36),
168203
s.Render(fmt.Sprintf("%d", ev.Status)),
169-
labelStyle.Render(ev.Duration.Round(time.Millisecond).String()),
204+
mutedStyle.Render(ev.Duration.Round(time.Millisecond).String()),
170205
)
171206
}
172207
return strings.TrimRight(b.String(), "\n")
173208
}
174209

175-
func colorInt(n int, ok bool) string {
176-
if ok {
210+
func colorInt(n int, isOK bool) string {
211+
if isOK {
177212
return statOK.Render(fmt.Sprintf("%d", n))
178213
}
179214
return statBad.Render(fmt.Sprintf("%d", n))

0 commit comments

Comments
 (0)