From b43cab8b0a3653ed5bfe3e97bd427b4fc2f01f6a Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Wed, 15 Apr 2026 23:06:55 -0700 Subject: [PATCH] feat: lots of colors The output pane is pretty good, not perfect, but pretty good. None of this code is great, but if it works, it works. --- examples/echo/main.go | 57 +++++++++++++++++++-------- go.mod | 20 ++++++---- go.sum | 25 ++++++------ internal/tui/model.go | 2 + internal/tui/panes.go | 48 +++++++++++++++-------- internal/tui/split.go | 6 +-- internal/tui/style.go | 90 +++++++++++++++++++++++++++++++++++++++++++ internal/tui/view.go | 11 +++++- 8 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 internal/tui/style.go diff --git a/examples/echo/main.go b/examples/echo/main.go index 436712e..00f6a68 100644 --- a/examples/echo/main.go +++ b/examples/echo/main.go @@ -8,10 +8,10 @@ import ( "encoding/json" "fmt" "io" - "log" "net" "net/http" "net/url" + "os" "strconv" "strings" "sync" @@ -21,7 +21,8 @@ import ( func main() { upstreamHost, err := findNonLoopbackIPv4() if err != nil { - log.Fatal(err) + fmt.Printf("error: %v\n", err) + os.Exit(1) } var wg sync.WaitGroup @@ -30,14 +31,16 @@ func main() { go func() { defer wg.Done() if err := startUpstream(upstreamHost); err != nil { - log.Fatal(err) + fmt.Printf("error: %v\n", err) + os.Exit(1) } }() go func() { defer wg.Done() if err := startFrontend(upstreamHost); err != nil { - log.Fatal(err) + fmt.Printf("error: %v\n", err) + os.Exit(1) } }() @@ -58,6 +61,16 @@ func startFrontend(upstreamHost string) error { mux.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) { client := &http.Client{Timeout: parseTimeout(req.URL.Query().Get("timeoutMs"))} + codeParam := strings.TrimSpace(req.URL.Query().Get("code")) + failParam := strings.TrimSpace(req.URL.Query().Get("fail")) + + if failParam != "" && failParam != "false" && failParam != "0" && failParam != "no" { + fmt.Fprintf(os.Stderr, "frontend fail mode requested method=%s fail=%s\n", req.Method, failParam) + } + + if parsedCode := parseStatusCode(codeParam); parsedCode >= 400 { + fmt.Fprintf(os.Stderr, "frontend error status requested method=%s code=%d fail=%s\n", req.Method, parsedCode, failParam) + } switch req.Method { case http.MethodGet: @@ -66,18 +79,24 @@ func startFrontend(upstreamHost string) error { "http://%s:3001/echo?message=%s&code=%s&fail=%s&sleepMs=%s", upstreamHost, url.QueryEscape(message), - url.QueryEscape(req.URL.Query().Get("code")), - url.QueryEscape(req.URL.Query().Get("fail")), + url.QueryEscape(codeParam), + url.QueryEscape(failParam), url.QueryEscape(req.URL.Query().Get("sleepMs")), ) + fmt.Printf("frontend -> upstream GET %s\n", upstreamURL) resp, err := client.Get(upstreamURL) if err != nil { + fmt.Fprintf(os.Stderr, "frontend upstream GET failed url=%s err=%v\n", upstreamURL, err) http.Error(w, err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() + if resp.StatusCode >= 400 { + fmt.Fprintf(os.Stderr, "frontend upstream responded with error method=GET status=%d url=%s\n", resp.StatusCode, upstreamURL) + } + body, err := io.ReadAll(resp.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) @@ -102,10 +121,11 @@ func startFrontend(upstreamHost string) error { upstreamURL := fmt.Sprintf( "http://%s:3001/echo?code=%s&fail=%s&sleepMs=%s", upstreamHost, - url.QueryEscape(req.URL.Query().Get("code")), - url.QueryEscape(req.URL.Query().Get("fail")), + url.QueryEscape(codeParam), + url.QueryEscape(failParam), url.QueryEscape(req.URL.Query().Get("sleepMs")), ) + fmt.Printf("frontend -> upstream POST %s\n", upstreamURL) upstreamReq, err := http.NewRequest(http.MethodPost, upstreamURL, bytes.NewReader(body)) if err != nil { @@ -116,11 +136,16 @@ func startFrontend(upstreamHost string) error { resp, err := client.Do(upstreamReq) if err != nil { + fmt.Fprintf(os.Stderr, "frontend upstream POST failed url=%s err=%v\n", upstreamURL, err) http.Error(w, err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() + if resp.StatusCode >= 400 { + fmt.Fprintf(os.Stderr, "frontend upstream responded with error method=POST status=%d url=%s\n", resp.StatusCode, upstreamURL) + } + upstreamBody, err := io.ReadAll(resp.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) @@ -135,12 +160,12 @@ func startFrontend(upstreamHost string) error { } }) - log.Printf("frontend UI on http://127.0.0.1:3000") - log.Printf("frontend GET example: http://127.0.0.1:3000/echo?message=hello&code=201&sleepMs=200") - log.Printf("frontend POST example: curl -i -X POST 'http://127.0.0.1:3000/echo?code=202&sleepMs=200' -H 'content-type: application/json' -d '{\"message\":\"hello\"}'") - log.Printf("frontend timeout example: http://127.0.0.1:3000/echo?message=late&sleepMs=4000&timeoutMs=1000") - log.Printf("frontend failure examples: fail=true, fail=drop, fail=timeout, fail=status") - log.Printf("frontend calls upstream at http://%s:3001/echo", upstreamHost) + fmt.Println("frontend UI on http://127.0.0.1:3000") + fmt.Println("frontend GET example: http://127.0.0.1:3000/echo?message=hello&code=201&sleepMs=200") + fmt.Println("frontend POST example: curl -i -X POST 'http://127.0.0.1:3000/echo?code=202&sleepMs=200' -H 'content-type: application/json' -d '{\"message\":\"hello\"}'") + fmt.Println("frontend timeout example: http://127.0.0.1:3000/echo?message=late&sleepMs=4000&timeoutMs=1000") + fmt.Println("frontend failure examples: fail=true, fail=drop, fail=timeout, fail=status") + fmt.Printf("frontend calls upstream at http://%s:3001/echo\n", upstreamHost) return http.ListenAndServe("127.0.0.1:3000", mux) } @@ -174,8 +199,8 @@ func startUpstream(upstreamHost string) error { } }) - log.Printf("upstream listening on http://%s:3001/echo?message=hello&code=201", upstreamHost) - log.Printf("upstream POST example: curl -i -X POST 'http://%s:3001/echo?code=202&sleepMs=200' -H 'content-type: application/json' -d '{\"message\":\"hello\"}'", upstreamHost) + fmt.Printf("upstream listening on http://%s:3001/echo?message=hello&code=201\n", upstreamHost) + fmt.Printf("upstream POST example: curl -i -X POST 'http://%s:3001/echo?code=202&sleepMs=200' -H 'content-type: application/json' -d '{\"message\":\"hello\"}'\n", upstreamHost) return http.ListenAndServe(":3001", mux) } diff --git a/go.mod b/go.mod index c5e627a..7fb5167 100644 --- a/go.mod +++ b/go.mod @@ -2,26 +2,30 @@ module termtap.dev go 1.26.1 -require github.com/google/uuid v1.6.0 +require ( + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/google/uuid v1.6.0 +) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/colorprofile v0.3.2 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 314653d..4abd221 100644 --- a/go.sum +++ b/go.sum @@ -2,42 +2,45 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= +github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/internal/tui/model.go b/internal/tui/model.go index 0fd56a0..9330872 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -23,6 +23,7 @@ type Model struct { width int height int + theme Theme showEvents bool showStd bool showSearch bool @@ -40,6 +41,7 @@ func NewModel(ch <-chan model.Event) Model { showEvents: false, showStd: false, showSearch: false, + theme: newTheme(), } } diff --git a/internal/tui/panes.go b/internal/tui/panes.go index cd10c97..ae2b51f 100644 --- a/internal/tui/panes.go +++ b/internal/tui/panes.go @@ -5,24 +5,30 @@ import ( "strings" "time" + "github.com/charmbracelet/lipgloss" "termtap.dev/internal/model" ) +// TODO: LOTS OF THIS SUCKS BUT IT WORKS + func (m Model) renderStatusBar(w int) string { var errCount int + var msSum int64 for _, req := range m.requests { if req.Failed || (req.Status >= 400 && req.Status < 600) { errCount++ } + msSum += req.Duration.Milliseconds() } - left := fmt.Sprintf(" tap %3d reqs | %d err | avg 500ms", len(m.requests), errCount) + avg := int(msSum) / max(1, len(m.requests)) + left := fmt.Sprintf(" tap %3d reqs | %d err | avg %dms", len(m.requests), errCount, avg) right := "j/k nav / search tab panel e events o output r replay q quit " spaceSize := max(w-(len(left)+len(right)), 0) space := strings.Repeat(" ", spaceSize) - return left + space + right + return m.theme.Header.Render(left + space + right) } func (m Model) renderSearchPane(w, h int) []string { @@ -88,7 +94,7 @@ func (m Model) renderRequestPane(w, h int) []string { func (m Model) renderDetailsPane(w, h int) []string { lines := make([]string, h) for y := range lines { - lines[y] = strings.Repeat("^", w) + lines[y] = m.theme.Text.Render(strings.Repeat(" ", w)) } return lines } @@ -113,7 +119,7 @@ func (m Model) renderEventsPane(w, h int) []string { left := fmt.Sprintf("EVENT LOG - %d EVENTS", len(events)) right := "E: TOGGLE" - status := left + strings.Repeat(" ", w-len(left+right)) + right + status := m.theme.EventHeader.Render(left + strings.Repeat(" ", w-len(left+right)) + right) lines := []string{status} for _, event := range events { @@ -164,24 +170,36 @@ func (m Model) renderStdPane(w, h int) []string { left := fmt.Sprintf("STDOUT/STDERR LOG - %d LINES", len(logs)) right := "O: TOGGLE" - status := left + strings.Repeat(" ", w-len(left+right)) + right + status := m.theme.StdHeader.Render(left + strings.Repeat(" ", w-len(left+right)) + right) lines := []string{status} for _, log := range logs { - var t string + var ( + tag string + body string + timePart string + ) if log.Type == model.EventTypeProcessStderr { - t = "STDERR" + tag = m.theme.TextError.Render("ERR ") + timePart = m.theme.TextMutedError.Render(log.Time.Format("15:04:05") + " ") + + prefix := timePart + tag + avail := max(0, w-lipgloss.Width(prefix)) + body = clampRendered(m.theme.TextError.Render(log.Body), avail) + + pad := max(0, avail-lipgloss.Width(body)) + body += m.theme.TextMutedError.Render(strings.Repeat(" ", pad)) } if log.Type == model.EventTypeProcessStdout { - t = "STDOUT" + tag = m.theme.TextMuted.Render("OUT ") + timePart = m.theme.TextMuted.Render(log.Time.Format("15:04:05") + " ") + + prefix := timePart + tag + avail := max(0, w-lipgloss.Width(prefix)) + body = clampRendered(m.theme.Text.Render(log.Body), avail) } - line := fmt.Sprintf( - "%s %6s %s", - log.Time.Format("15:04:05"), - t, - log.Body, - ) - lines = append(lines, truncate(line, w)) + line := clampRendered(timePart+tag+body, w) + lines = append(lines, line) } // Cleanup diff --git a/internal/tui/split.go b/internal/tui/split.go index aa63f4a..f87c3a7 100644 --- a/internal/tui/split.go +++ b/internal/tui/split.go @@ -11,13 +11,13 @@ func (m Model) renderAppPane() string { searchH int = 1 reqW int = max(0, int(float64(m.width)*0.55)) - reqH int = max(0, m.height-constHeightOffset) + detW int = max(0, m.width-reqW) - detW int = max(0, int(float64(m.width)*0.45)) + reqH int = max(0, m.height-constHeightOffset) detH int = max(0, m.height-constHeightOffset) eventW int = max(0, m.width) - eventH int = max(0, int(float64(m.height)*0.15)) + eventH int = max(0, int(float64(m.height)*0.2)) stdW int = max(0, m.width) stdH int = max(0, int(float64(m.height)*0.2)) diff --git a/internal/tui/style.go b/internal/tui/style.go new file mode 100644 index 0000000..894c469 --- /dev/null +++ b/internal/tui/style.go @@ -0,0 +1,90 @@ +package tui + +import ( + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/ansi" +) + +type Theme struct { + Background lipgloss.Style + + Header lipgloss.Style + EventHeader lipgloss.Style + StdHeader lipgloss.Style + + Text lipgloss.Style + TextMuted lipgloss.Style + TextError lipgloss.Style + TextMutedError lipgloss.Style + + EventGreen lipgloss.Style + EventRed lipgloss.Style + EventBlue lipgloss.Style + EventOrange lipgloss.Style +} + +const background = lipgloss.Color("#010e1f") +const backgroundError = lipgloss.Color("#1f1118") +const text = lipgloss.Color("#dfe5ed") +const textMuted = lipgloss.Color("#7c7e80") + +const blue = lipgloss.Color("#2280f2") +const orange = lipgloss.Color("#f2a813") +const red = lipgloss.Color("#e6130b") +const green = lipgloss.Color("#10e31e") + +func newTheme() Theme { + return Theme{ + Background: lipgloss.NewStyle(). + Background(background), + + Header: lipgloss.NewStyle(). + Bold(true). + Foreground(background). + Background(blue), + EventHeader: lipgloss.NewStyle(). + Bold(true). + Foreground(background). + Background(blue), + StdHeader: lipgloss.NewStyle(). + Bold(true). + Foreground(background). + Background(orange), + + Text: lipgloss.NewStyle(). + Foreground(text). + Background(background), + TextMuted: lipgloss.NewStyle(). + Foreground(textMuted). + Background(background), + TextError: lipgloss.NewStyle(). + Foreground(red). + Background(backgroundError), + TextMutedError: lipgloss.NewStyle(). + Foreground(textMuted). + Background(backgroundError), + + EventGreen: lipgloss.NewStyle(). + Foreground(green). + Background(background), + EventBlue: lipgloss.NewStyle(). + Foreground(blue). + Background(background), + EventRed: lipgloss.NewStyle(). + Foreground(red). + Background(backgroundError), + EventOrange: lipgloss.NewStyle(). + Foreground(orange). + Background(background), + } +} + +func clampRendered(s string, maxCols int) string { + if maxCols <= 0 { + return "" + } + if lipgloss.Width(s) <= maxCols { + return s + } + return ansi.Truncate(s, maxCols, "...") +} diff --git a/internal/tui/view.go b/internal/tui/view.go index eb84e0f..799c1a5 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -5,9 +5,16 @@ import ( "time" ) -// TODO: This is all temporary func (m Model) View() string { - return m.renderAppPane() + view := m.renderAppPane() + if m.width <= 0 || m.height <= 0 { + return view + } + + return m.theme.Background. + Width(m.width). + Height(m.height). + Render(view) } func formatDuration(d time.Duration) string {