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:
parent
d26d812d3c
commit
b43cab8b0a
@ -8,10 +8,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -21,7 +21,8 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
upstreamHost, err := findNonLoopbackIPv4()
|
upstreamHost, err := findNonLoopbackIPv4()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
fmt.Printf("error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -30,14 +31,16 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := startUpstream(upstreamHost); err != nil {
|
if err := startUpstream(upstreamHost); err != nil {
|
||||||
log.Fatal(err)
|
fmt.Printf("error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := startFrontend(upstreamHost); err != nil {
|
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) {
|
mux.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) {
|
||||||
client := &http.Client{Timeout: parseTimeout(req.URL.Query().Get("timeoutMs"))}
|
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 {
|
switch req.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
@ -66,18 +79,24 @@ func startFrontend(upstreamHost string) error {
|
|||||||
"http://%s:3001/echo?message=%s&code=%s&fail=%s&sleepMs=%s",
|
"http://%s:3001/echo?message=%s&code=%s&fail=%s&sleepMs=%s",
|
||||||
upstreamHost,
|
upstreamHost,
|
||||||
url.QueryEscape(message),
|
url.QueryEscape(message),
|
||||||
url.QueryEscape(req.URL.Query().Get("code")),
|
url.QueryEscape(codeParam),
|
||||||
url.QueryEscape(req.URL.Query().Get("fail")),
|
url.QueryEscape(failParam),
|
||||||
url.QueryEscape(req.URL.Query().Get("sleepMs")),
|
url.QueryEscape(req.URL.Query().Get("sleepMs")),
|
||||||
)
|
)
|
||||||
|
fmt.Printf("frontend -> upstream GET %s\n", upstreamURL)
|
||||||
|
|
||||||
resp, err := client.Get(upstreamURL)
|
resp, err := client.Get(upstreamURL)
|
||||||
if err != nil {
|
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)
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
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)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
@ -102,10 +121,11 @@ func startFrontend(upstreamHost string) error {
|
|||||||
upstreamURL := fmt.Sprintf(
|
upstreamURL := fmt.Sprintf(
|
||||||
"http://%s:3001/echo?code=%s&fail=%s&sleepMs=%s",
|
"http://%s:3001/echo?code=%s&fail=%s&sleepMs=%s",
|
||||||
upstreamHost,
|
upstreamHost,
|
||||||
url.QueryEscape(req.URL.Query().Get("code")),
|
url.QueryEscape(codeParam),
|
||||||
url.QueryEscape(req.URL.Query().Get("fail")),
|
url.QueryEscape(failParam),
|
||||||
url.QueryEscape(req.URL.Query().Get("sleepMs")),
|
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))
|
upstreamReq, err := http.NewRequest(http.MethodPost, upstreamURL, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -116,11 +136,16 @@ func startFrontend(upstreamHost string) error {
|
|||||||
|
|
||||||
resp, err := client.Do(upstreamReq)
|
resp, err := client.Do(upstreamReq)
|
||||||
if err != nil {
|
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)
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
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)
|
upstreamBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
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")
|
fmt.Println("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")
|
fmt.Println("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\"}'")
|
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\"}'")
|
||||||
log.Printf("frontend timeout example: http://127.0.0.1:3000/echo?message=late&sleepMs=4000&timeoutMs=1000")
|
fmt.Println("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")
|
fmt.Println("frontend failure examples: fail=true, fail=drop, fail=timeout, fail=status")
|
||||||
log.Printf("frontend calls upstream at http://%s:3001/echo", upstreamHost)
|
fmt.Printf("frontend calls upstream at http://%s:3001/echo\n", upstreamHost)
|
||||||
return http.ListenAndServe("127.0.0.1:3000", mux)
|
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)
|
fmt.Printf("upstream listening on http://%s:3001/echo?message=hello&code=201\n", 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 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)
|
return http.ListenAndServe(":3001", mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
go.mod
20
go.mod
@ -2,26 +2,30 @@ module termtap.dev
|
|||||||
|
|
||||||
go 1.26.1
|
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 (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // 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/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/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-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // 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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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
|
golang.org/x/text v0.3.8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
25
go.sum
25
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/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 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
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.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
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 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
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 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
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 h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
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.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
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 h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
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.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
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 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
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 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
|||||||
@ -23,6 +23,7 @@ type Model struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
|
theme Theme
|
||||||
showEvents bool
|
showEvents bool
|
||||||
showStd bool
|
showStd bool
|
||||||
showSearch bool
|
showSearch bool
|
||||||
@ -40,6 +41,7 @@ func NewModel(ch <-chan model.Event) Model {
|
|||||||
showEvents: false,
|
showEvents: false,
|
||||||
showStd: false,
|
showStd: false,
|
||||||
showSearch: false,
|
showSearch: false,
|
||||||
|
theme: newTheme(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,24 +5,30 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: LOTS OF THIS SUCKS BUT IT WORKS
|
||||||
|
|
||||||
func (m Model) renderStatusBar(w int) string {
|
func (m Model) renderStatusBar(w int) string {
|
||||||
var errCount int
|
var errCount int
|
||||||
|
var msSum int64
|
||||||
for _, req := range m.requests {
|
for _, req := range m.requests {
|
||||||
if req.Failed || (req.Status >= 400 && req.Status < 600) {
|
if req.Failed || (req.Status >= 400 && req.Status < 600) {
|
||||||
errCount++
|
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 "
|
right := "j/k nav / search tab panel e events o output r replay q quit "
|
||||||
|
|
||||||
spaceSize := max(w-(len(left)+len(right)), 0)
|
spaceSize := max(w-(len(left)+len(right)), 0)
|
||||||
space := strings.Repeat(" ", spaceSize)
|
space := strings.Repeat(" ", spaceSize)
|
||||||
|
|
||||||
return left + space + right
|
return m.theme.Header.Render(left + space + right)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderSearchPane(w, h int) []string {
|
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 {
|
func (m Model) renderDetailsPane(w, h int) []string {
|
||||||
lines := make([]string, h)
|
lines := make([]string, h)
|
||||||
for y := range lines {
|
for y := range lines {
|
||||||
lines[y] = strings.Repeat("^", w)
|
lines[y] = m.theme.Text.Render(strings.Repeat(" ", w))
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
@ -113,7 +119,7 @@ func (m Model) renderEventsPane(w, h int) []string {
|
|||||||
|
|
||||||
left := fmt.Sprintf("EVENT LOG - %d EVENTS", len(events))
|
left := fmt.Sprintf("EVENT LOG - %d EVENTS", len(events))
|
||||||
right := "E: TOGGLE"
|
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}
|
lines := []string{status}
|
||||||
|
|
||||||
for _, event := range events {
|
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))
|
left := fmt.Sprintf("STDOUT/STDERR LOG - %d LINES", len(logs))
|
||||||
right := "O: TOGGLE"
|
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}
|
lines := []string{status}
|
||||||
|
|
||||||
for _, log := range logs {
|
for _, log := range logs {
|
||||||
var t string
|
var (
|
||||||
|
tag string
|
||||||
|
body string
|
||||||
|
timePart string
|
||||||
|
)
|
||||||
if log.Type == model.EventTypeProcessStderr {
|
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 {
|
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(
|
line := clampRendered(timePart+tag+body, w)
|
||||||
"%s %6s %s",
|
lines = append(lines, line)
|
||||||
log.Time.Format("15:04:05"),
|
|
||||||
t,
|
|
||||||
log.Body,
|
|
||||||
)
|
|
||||||
lines = append(lines, truncate(line, w))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
|
|||||||
@ -11,13 +11,13 @@ func (m Model) renderAppPane() string {
|
|||||||
searchH int = 1
|
searchH int = 1
|
||||||
|
|
||||||
reqW int = max(0, int(float64(m.width)*0.55))
|
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)
|
detH int = max(0, m.height-constHeightOffset)
|
||||||
|
|
||||||
eventW int = max(0, m.width)
|
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)
|
stdW int = max(0, m.width)
|
||||||
stdH int = max(0, int(float64(m.height)*0.2))
|
stdH int = max(0, int(float64(m.height)*0.2))
|
||||||
|
|||||||
90
internal/tui/style.go
Normal file
90
internal/tui/style.go
Normal 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, "...")
|
||||||
|
}
|
||||||
@ -5,9 +5,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: This is all temporary
|
|
||||||
func (m Model) View() string {
|
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 {
|
func formatDuration(d time.Duration) string {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user