feat: implemented the panels
This commit is contained in:
parent
ea201b4c91
commit
958fc7308a
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
func StartProcess(cmd model.Command, addr string, ch chan<- model.Event) (*model.Process, error) {
|
func StartProcess(cmd model.Command, addr string, ch chan<- model.Event) (*model.Process, error) {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeProcessStarting,
|
Type: model.EventTypeProcessStarting,
|
||||||
Body: fmt.Sprintf("spawning process '%s'", process.CommandString(cmd)),
|
Body: fmt.Sprintf("spawning process '%s'", process.CommandString(cmd)),
|
||||||
}
|
}
|
||||||
@ -35,6 +36,7 @@ func StopProcess(proc *model.Process, ch chan<- model.Event, sig syscall.Signal)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeProcessSignaled,
|
Type: model.EventTypeProcessSignaled,
|
||||||
Body: fmt.Sprintf("process with pid '%d' is being killed", proc.Exec.Process.Pid),
|
Body: fmt.Sprintf("process with pid '%d' is being killed", proc.Exec.Process.Pid),
|
||||||
PID: proc.Exec.Process.Pid,
|
PID: proc.Exec.Process.Pid,
|
||||||
@ -58,6 +60,7 @@ func waitForProcessExit(proc *model.Process, ch chan<- model.Event) {
|
|||||||
if err := proc.Exec.Wait(); err != nil {
|
if err := proc.Exec.Wait(); err != nil {
|
||||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeProcessExited,
|
Type: model.EventTypeProcessExited,
|
||||||
Body: fmt.Sprintf("process pid '%d' exited", proc.Exec.Process.Pid),
|
Body: fmt.Sprintf("process pid '%d' exited", proc.Exec.Process.Pid),
|
||||||
PID: proc.Exec.Process.Pid,
|
PID: proc.Exec.Process.Pid,
|
||||||
@ -68,6 +71,7 @@ func waitForProcessExit(proc *model.Process, ch chan<- model.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeFatal,
|
Type: model.EventTypeFatal,
|
||||||
Body: fmt.Sprintf("%q", err),
|
Body: fmt.Sprintf("%q", err),
|
||||||
}
|
}
|
||||||
@ -76,6 +80,7 @@ func waitForProcessExit(proc *model.Process, ch chan<- model.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeProcessExited,
|
Type: model.EventTypeProcessExited,
|
||||||
Body: fmt.Sprintf("process pid '%d' exited", proc.Exec.Process.Pid),
|
Body: fmt.Sprintf("process pid '%d' exited", proc.Exec.Process.Pid),
|
||||||
PID: proc.Exec.Process.Pid,
|
PID: proc.Exec.Process.Pid,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
)
|
)
|
||||||
@ -14,6 +15,7 @@ func StartProxy(ps *model.ProxyServer, ch chan<- model.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeProxyStarting,
|
Type: model.EventTypeProxyStarting,
|
||||||
Body: fmt.Sprintf("proxy server started on %s", (*ps.Listener).Addr().String()),
|
Body: fmt.Sprintf("proxy server started on %s", (*ps.Listener).Addr().String()),
|
||||||
}
|
}
|
||||||
@ -24,6 +26,7 @@ func StartProxy(ps *model.ProxyServer, ch chan<- model.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeFatal,
|
Type: model.EventTypeFatal,
|
||||||
Body: fmt.Sprintf("fatal error in proxy server: %q", err),
|
Body: fmt.Sprintf("fatal error in proxy server: %q", err),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type EventType string
|
type EventType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -26,6 +28,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
|
Time time.Time
|
||||||
Type EventType
|
Type EventType
|
||||||
Body string
|
Body string
|
||||||
PID int
|
PID int
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
)
|
)
|
||||||
@ -24,6 +25,7 @@ func NewProcess(cmd model.Command, addr string, ch chan<- model.Event) *model.Pr
|
|||||||
stdout, err := proc.StdoutPipe()
|
stdout, err := proc.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("could not open stdout pipe: %q", err),
|
Body: fmt.Sprintf("could not open stdout pipe: %q", err),
|
||||||
PID: proc.Process.Pid,
|
PID: proc.Process.Pid,
|
||||||
@ -35,6 +37,7 @@ func NewProcess(cmd model.Command, addr string, ch chan<- model.Event) *model.Pr
|
|||||||
stderr, err := proc.StderrPipe()
|
stderr, err := proc.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("could not open stderr pipe: %q", err),
|
Body: fmt.Sprintf("could not open stderr pipe: %q", err),
|
||||||
PID: proc.Process.Pid,
|
PID: proc.Process.Pid,
|
||||||
@ -70,6 +73,7 @@ func readPipe(pipe io.Reader, t model.EventType, ch chan<- model.Event) {
|
|||||||
scanner := bufio.NewScanner(pipe)
|
scanner := bufio.NewScanner(pipe)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: t,
|
Type: t,
|
||||||
Body: scanner.Text(),
|
Body: scanner.Text(),
|
||||||
}
|
}
|
||||||
@ -100,6 +104,7 @@ func UpdateStatus(proc *model.Process, running bool, ch chan<- model.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: t,
|
Type: t,
|
||||||
Body: fmt.Sprintf("Set process pid '%d' status to %s", proc.Exec.Process.Pid, status),
|
Body: fmt.Sprintf("Set process pid '%d' status to %s", proc.Exec.Process.Pid, status),
|
||||||
PID: proc.Exec.Process.Pid,
|
PID: proc.Exec.Process.Pid,
|
||||||
|
|||||||
@ -28,6 +28,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
if req.Method == http.MethodConnect {
|
if req.Method == http.MethodConnect {
|
||||||
http.Error(w, "CONNECT is not supported yet", http.StatusNotImplemented)
|
http.Error(w, "CONNECT is not supported yet", http.StatusNotImplemented)
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("CONNECT is not supported: %s", req.Host),
|
Body: fmt.Sprintf("CONNECT is not supported: %s", req.Host),
|
||||||
}
|
}
|
||||||
@ -37,6 +38,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
if req.URL.Scheme == "" || req.URL.Host == "" {
|
if req.URL.Scheme == "" || req.URL.Host == "" {
|
||||||
http.Error(w, "request must use absolute-form URLs through the proxy", http.StatusBadRequest)
|
http.Error(w, "request must use absolute-form URLs through the proxy", http.StatusBadRequest)
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("rejected non-proxy request %s %s", req.Method, req.URL.String()),
|
Body: fmt.Sprintf("rejected non-proxy request %s %s", req.Method, req.URL.String()),
|
||||||
}
|
}
|
||||||
@ -61,6 +63,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
requestPreview, err := readAndRestoreBody(&req.Body)
|
requestPreview, err := readAndRestoreBody(&req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("(%s) failed to read request body", request.ID),
|
Body: fmt.Sprintf("(%s) failed to read request body", request.ID),
|
||||||
Request: request,
|
Request: request,
|
||||||
@ -81,6 +84,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
request.RawURL = outReq.URL.String()
|
request.RawURL = outReq.URL.String()
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeRequestStarted,
|
Type: model.EventTypeRequestStarted,
|
||||||
Body: fmt.Sprintf("-> %+v", request),
|
Body: fmt.Sprintf("-> %+v", request),
|
||||||
Request: request,
|
Request: request,
|
||||||
@ -97,6 +101,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
request.Status = status
|
request.Status = status
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeRequestFailed,
|
Type: model.EventTypeRequestFailed,
|
||||||
Body: fmt.Sprintf("upstream error for %s %s: %v", outReq.Method, outReq.URL.String(), err),
|
Body: fmt.Sprintf("upstream error for %s %s: %v", outReq.Method, outReq.URL.String(), err),
|
||||||
Request: request,
|
Request: request,
|
||||||
@ -108,6 +113,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
responsePreview, err := readAndRestoreBody(&resp.Body)
|
responsePreview, err := readAndRestoreBody(&resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("(%s) failed to read response body", request.ID),
|
Body: fmt.Sprintf("(%s) failed to read response body", request.ID),
|
||||||
Request: request,
|
Request: request,
|
||||||
@ -125,6 +131,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
request.Status = resp.StatusCode
|
request.Status = resp.StatusCode
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeRequestFailed,
|
Type: model.EventTypeRequestFailed,
|
||||||
Body: fmt.Sprintf("write response body %s %s: %v", outReq.Method, outReq.URL.String(), err),
|
Body: fmt.Sprintf("write response body %s %s: %v", outReq.Method, outReq.URL.String(), err),
|
||||||
}
|
}
|
||||||
@ -137,6 +144,7 @@ func proxyHandler(ch chan<- model.Event) http.Handler {
|
|||||||
request.Pending = false
|
request.Pending = false
|
||||||
|
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeRequestFinished,
|
Type: model.EventTypeRequestFinished,
|
||||||
Body: fmt.Sprintf("<- %+v %s", request, formatHeaders(resp.Request.Header)),
|
Body: fmt.Sprintf("<- %+v %s", request, formatHeaders(resp.Request.Header)),
|
||||||
Request: request,
|
Request: request,
|
||||||
|
|||||||
@ -35,6 +35,7 @@ func Destroy(ps *model.ProxyServer, ch chan<- model.Event) {
|
|||||||
if ps != nil && ps.Server != nil {
|
if ps != nil && ps.Server != nil {
|
||||||
_ = ps.Server.Shutdown(ctx)
|
_ = ps.Server.Shutdown(ctx)
|
||||||
ch <- model.Event{
|
ch <- model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeProxyStarted,
|
Type: model.EventTypeProxyStarted,
|
||||||
Body: "proxy server was destroyed",
|
Body: "proxy server was destroyed",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "termtap.dev/internal/model"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"termtap.dev/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
type EventMsg struct {
|
type EventMsg struct {
|
||||||
value model.Event
|
value model.Event
|
||||||
@ -9,3 +14,15 @@ type EventMsg struct {
|
|||||||
type ErrMsg struct {
|
type ErrMsg struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TickMsg struct {
|
||||||
|
Now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = 20 * time.Millisecond
|
||||||
|
|
||||||
|
func tickCmd() tea.Cmd {
|
||||||
|
return tea.Tick(tick, func(t time.Time) tea.Msg {
|
||||||
|
return TickMsg{Now: t}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
@ -25,6 +26,8 @@ type Model struct {
|
|||||||
showEvents bool
|
showEvents bool
|
||||||
showStd bool
|
showStd bool
|
||||||
showSearch bool
|
showSearch bool
|
||||||
|
|
||||||
|
now time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(ch <-chan model.Event) Model {
|
func NewModel(ch <-chan model.Event) Model {
|
||||||
@ -47,7 +50,7 @@ func Run(ch <-chan model.Event) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return waitForEvent(m.channel)
|
return tea.Batch(waitForEvent(m.channel), tickCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForEvent(ch <-chan model.Event) tea.Cmd {
|
func waitForEvent(ch <-chan model.Event) tea.Cmd {
|
||||||
|
|||||||
@ -3,10 +3,12 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"termtap.dev/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) renderStatusBar(w int) string {
|
func (m Model) renderStatusBar(w int) string {
|
||||||
// TODO: Optimize somehow
|
|
||||||
var errCount int
|
var errCount int
|
||||||
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) {
|
||||||
@ -32,28 +34,58 @@ func (m Model) renderSearchPane(w, h int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderRequestPane(w, h int) []string {
|
func (m Model) renderRequestPane(w, h int) []string {
|
||||||
if w < 0 {
|
var lines []string
|
||||||
w = 0
|
|
||||||
}
|
// Render header
|
||||||
if h < 0 {
|
headerLeft := fmt.Sprintf(" %-7s %-24s %s", "METHOD", "HOST", "PATH")
|
||||||
h = 0
|
headerRight := fmt.Sprintf("%4s %8s ", "CODE", "TIME")
|
||||||
|
headerSpace := strings.Repeat(" ", max(0, w-len(headerLeft+headerRight)))
|
||||||
|
header := headerLeft + headerSpace + headerRight
|
||||||
|
lines = append(lines, header)
|
||||||
|
|
||||||
|
for i := len(m.requests) - 1; i >= 0; i-- {
|
||||||
|
req := m.requests[i]
|
||||||
|
|
||||||
|
// Some formatting magic here maybe
|
||||||
|
left := fmt.Sprintf(
|
||||||
|
" %-7s %-24s %s",
|
||||||
|
strings.ToUpper(req.Method),
|
||||||
|
req.Host,
|
||||||
|
req.URL,
|
||||||
|
)
|
||||||
|
right := fmt.Sprintf(
|
||||||
|
"%4d %8s ",
|
||||||
|
req.Status,
|
||||||
|
formatDuration(req.Duration),
|
||||||
|
)
|
||||||
|
if req.Pending && !req.StartTime.IsZero() {
|
||||||
|
right = fmt.Sprintf(
|
||||||
|
"%4s %8s ",
|
||||||
|
"",
|
||||||
|
formatDuration(time.Since(req.StartTime)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
space := strings.Repeat(" ", max(0, w-len(left+right)))
|
||||||
|
|
||||||
|
line := left + space + right
|
||||||
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := make([]string, h)
|
// Cleanup
|
||||||
for y := range lines {
|
if len(lines) < h {
|
||||||
lines[y] = strings.Repeat(".", w)
|
for i := len(lines); i < h; i++ {
|
||||||
|
lines = append(lines, strings.Repeat(" ", w))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(lines) > h {
|
||||||
|
lines = lines[:h]
|
||||||
|
}
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderDetailsPane(w, h int) []string {
|
func (m Model) renderDetailsPane(w, h int) []string {
|
||||||
if w < 0 {
|
|
||||||
w = 0
|
|
||||||
}
|
|
||||||
if h < 0 {
|
|
||||||
h = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make([]string, h)
|
lines := make([]string, h)
|
||||||
for y := range lines {
|
for y := range lines {
|
||||||
lines[y] = strings.Repeat("^", w)
|
lines[y] = strings.Repeat("^", w)
|
||||||
@ -61,32 +93,101 @@ func (m Model) renderDetailsPane(w, h int) []string {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This can be done better
|
||||||
|
// TODO: Should h be max or defined?
|
||||||
func (m Model) renderEventsPane(w, h int) []string {
|
func (m Model) renderEventsPane(w, h int) []string {
|
||||||
if w < 0 {
|
// Remove the stdout or stderr logs
|
||||||
w = 0
|
var events []model.Event
|
||||||
}
|
for _, ev := range m.events {
|
||||||
if h < 0 {
|
if ev.Type != model.EventTypeProcessStderr &&
|
||||||
h = 0
|
ev.Type != model.EventTypeProcessStdout {
|
||||||
|
events = append(events, ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := make([]string, h)
|
displayCount := max(h-1, 0)
|
||||||
for y := range lines {
|
|
||||||
lines[y] = strings.Repeat("~", w)
|
if displayCount < len(events) {
|
||||||
|
events = events[len(events)-displayCount:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lines := []string{
|
||||||
|
fmt.Sprintf("EVENT LOG - %d EVENTS", len(events)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
line := fmt.Sprintf(
|
||||||
|
"%s %-15s %s",
|
||||||
|
event.Time.Format("15:04:05"),
|
||||||
|
event.Type,
|
||||||
|
event.Body,
|
||||||
|
)
|
||||||
|
if event.PID > 0 {
|
||||||
|
line = fmt.Sprintf(
|
||||||
|
"%s %-15s %d %s",
|
||||||
|
event.Time.Format("15:04:05"),
|
||||||
|
event.Type,
|
||||||
|
event.PID,
|
||||||
|
event.Body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
lines = append(lines, truncate(line, w))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if len(lines) < h {
|
||||||
|
for i := len(lines); i < h; i++ {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Should h be max or defined?
|
||||||
func (m Model) renderStdPane(w, h int) []string {
|
func (m Model) renderStdPane(w, h int) []string {
|
||||||
if w < 0 {
|
// Only the stdout or stderr logs
|
||||||
w = 0
|
var logs []model.Event
|
||||||
}
|
for _, ev := range m.events {
|
||||||
if h < 0 {
|
if ev.Type == model.EventTypeProcessStderr ||
|
||||||
h = 0
|
ev.Type == model.EventTypeProcessStdout {
|
||||||
|
logs = append(logs, ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := make([]string, h)
|
displayCount := max(h-1, 0)
|
||||||
for y := range lines {
|
|
||||||
lines[y] = strings.Repeat(" ", w)
|
if displayCount < len(logs) {
|
||||||
|
logs = logs[len(logs)-displayCount:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lines := []string{
|
||||||
|
fmt.Sprintf("STDOUT/STDERR LOG - %d LINES", len(logs)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, log := range logs {
|
||||||
|
var t string
|
||||||
|
if log.Type == model.EventTypeProcessStderr {
|
||||||
|
t = "STDERR"
|
||||||
|
}
|
||||||
|
if log.Type == model.EventTypeProcessStdout {
|
||||||
|
t = "STDOUT"
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf(
|
||||||
|
"%s %6s %s",
|
||||||
|
log.Time.Format("15:04:05"),
|
||||||
|
t,
|
||||||
|
log.Body,
|
||||||
|
)
|
||||||
|
lines = append(lines, truncate(line, w))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if len(lines) < h {
|
||||||
|
for i := len(lines); i < h; i++ {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
@ -14,6 +15,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case TickMsg:
|
||||||
|
m.now = msg.Now
|
||||||
|
if m.hasPendingRequests() {
|
||||||
|
return m, tickCmd()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
// TODO: Abstract the keymaps
|
// TODO: Abstract the keymaps
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
@ -32,6 +40,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case ErrMsg:
|
case ErrMsg:
|
||||||
m.events = append(m.events, model.Event{
|
m.events = append(m.events, model.Event{
|
||||||
|
Time: time.Now().Local(),
|
||||||
Type: model.EventTypeWarn,
|
Type: model.EventTypeWarn,
|
||||||
Body: fmt.Sprintf("tui event stream closed: %v", msg.err),
|
Body: fmt.Sprintf("tui event stream closed: %v", msg.err),
|
||||||
})
|
})
|
||||||
@ -40,6 +49,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case EventMsg:
|
case EventMsg:
|
||||||
m.pushEvent(msg.value)
|
m.pushEvent(msg.value)
|
||||||
m.applyMessage(msg.value)
|
m.applyMessage(msg.value)
|
||||||
|
if m.hasPendingRequests() {
|
||||||
|
return m, tea.Batch(waitForEvent(m.channel), tickCmd())
|
||||||
|
}
|
||||||
return m, waitForEvent(m.channel)
|
return m, waitForEvent(m.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,3 +94,14 @@ func (m *Model) updateRequest(req model.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) hasPendingRequests() bool {
|
||||||
|
// Traverse backward to be a bit more efficient, the most recent requests are more
|
||||||
|
// like to be pending.
|
||||||
|
for i := len(m.requests) - 1; i >= 0; i-- {
|
||||||
|
if m.requests[i].Pending {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -2,95 +2,28 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
"termtap.dev/internal/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: This is all temporary
|
// TODO: This is all temporary
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
return m.renderAppPane()
|
return m.renderAppPane()
|
||||||
|
|
||||||
// eventLines := m.renderEvents(8)
|
|
||||||
// requestLines := m.renderRequests(12)
|
|
||||||
//
|
|
||||||
// return strings.Join([]string{
|
|
||||||
// "termtap - live session",
|
|
||||||
// fmt.Sprintf("events=%d requests=%d", len(m.events), len(m.requests)),
|
|
||||||
// fmt.Sprintf("%dx%d", m.height, m.width),
|
|
||||||
// "keys: q/esc/ctrl+c quit",
|
|
||||||
// "",
|
|
||||||
// "Recent events:",
|
|
||||||
// eventLines,
|
|
||||||
// "",
|
|
||||||
// "Recent requests:",
|
|
||||||
// requestLines,
|
|
||||||
// }, "\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderEvents(limit int) string {
|
func formatDuration(d time.Duration) string {
|
||||||
if len(m.events) == 0 {
|
if d == 0 {
|
||||||
return " (none yet)"
|
return "PENDING"
|
||||||
}
|
}
|
||||||
|
|
||||||
start := max(len(m.events)-limit, 0)
|
if d >= 10*time.Second {
|
||||||
|
return fmt.Sprintf("%.2fs", d.Seconds())
|
||||||
rows := make([]string, 0, len(m.events)-start)
|
|
||||||
for i := start; i < len(m.events); i++ {
|
|
||||||
e := m.events[i]
|
|
||||||
rows = append(rows, fmt.Sprintf(" [%s] %s", e.Type, truncate(e.Body, 100)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(rows, "\n")
|
if d >= time.Millisecond {
|
||||||
}
|
return fmt.Sprintf("%dms", d.Milliseconds())
|
||||||
|
|
||||||
func (m Model) renderRequests(limit int) string {
|
|
||||||
if len(m.requests) == 0 {
|
|
||||||
return " (none yet)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start := max(0, len(m.requests)-limit)
|
return fmt.Sprintf("%dus", d.Microseconds())
|
||||||
|
|
||||||
// Traverse backwards since we don't have a stack
|
|
||||||
rows := make([]string, 0, len(m.requests)-start)
|
|
||||||
for i := len(m.requests) - 1; i >= start; i-- {
|
|
||||||
req := m.requests[i]
|
|
||||||
|
|
||||||
state := "done"
|
|
||||||
if req.Pending {
|
|
||||||
state = "pending"
|
|
||||||
} else if req.Failed {
|
|
||||||
state = "failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = append(rows, fmt.Sprintf(
|
|
||||||
" %s %s status=%d duration=%s state=%s",
|
|
||||||
req.Method,
|
|
||||||
requestPath(req),
|
|
||||||
req.Status,
|
|
||||||
req.Duration,
|
|
||||||
state,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return " (none yet)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(rows, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestPath(req model.Request) string {
|
|
||||||
if req.URL != "" {
|
|
||||||
return truncate(req.URL, 80)
|
|
||||||
}
|
|
||||||
if req.RawURL != "" {
|
|
||||||
return truncate(req.RawURL, 80)
|
|
||||||
}
|
|
||||||
if req.Host != "" {
|
|
||||||
return truncate(req.Host, 80)
|
|
||||||
}
|
|
||||||
return "<unknown>"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func truncate(s string, max int) string {
|
func truncate(s string, max int) string {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user