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.
This commit is contained in:
Hayden Hargreaves 2026-04-15 23:06:55 -07:00
parent d26d812d3c
commit b43cab8b0a
8 changed files with 204 additions and 55 deletions

View File

@ -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)
}

20
go.mod
View File

@ -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
)

25
go.sum
View File

@ -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=

View File

@ -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(),
}
}

View File

@ -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

View File

@ -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))

90
internal/tui/style.go Normal file
View File

@ -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, "...")
}

View File

@ -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 {