@@ -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
1422type tickMsg time.Time
1523type 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
2937func initialModel (listenAddr , upstream string ) model {
3038 return model {
3139 listenAddr : listenAddr ,
3240 upstream : upstream ,
3341 startedAt : time .Now (),
42+ width : 100 ,
3443 }
3544}
3645
3746func (m model ) Init () tea.Cmd { return tea .Batch (tick (), tea .EnterAltScreen ) }
3847
3948func 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
4352func (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.
7686var (
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
96104func (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
144179func (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