Compare commits
2 Commits
7bcf8e8301
...
4c4a3f6bb0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c4a3f6bb0 | ||
|
|
d51d1fea56 |
11
internal/tui/messages.go
Normal file
11
internal/tui/messages.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "termtap.dev/internal/model"
|
||||||
|
|
||||||
|
type EventMsg struct {
|
||||||
|
value model.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrMsg struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
@ -4,58 +4,55 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/google/uuid"
|
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: How big can we actually make this?
|
||||||
const (
|
const (
|
||||||
maxEvents = 200
|
maxEvents = 256
|
||||||
maxRequests = 200
|
maxRequests = 256
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventMsg struct {
|
|
||||||
value model.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrMsg struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
msgCh <-chan model.Event
|
channel <-chan model.Event
|
||||||
|
|
||||||
events []model.Event
|
events []model.Event
|
||||||
requestOrder []uuid.UUID
|
requests []model.Request
|
||||||
requests map[uuid.UUID]model.Request
|
|
||||||
|
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
|
showEvents bool
|
||||||
|
showStd bool
|
||||||
|
showSearch bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(msgCh <-chan model.Event) Model {
|
func NewModel(ch <-chan model.Event) Model {
|
||||||
return Model{
|
return Model{
|
||||||
msgCh: msgCh,
|
channel: ch,
|
||||||
events: make([]model.Event, 0, maxEvents),
|
events: make([]model.Event, 0, maxEvents),
|
||||||
requestOrder: make([]uuid.UUID, 0, maxRequests),
|
requests: make([]model.Request, 0, maxRequests),
|
||||||
requests: map[uuid.UUID]model.Request{},
|
width: 0,
|
||||||
width: 100,
|
height: 0,
|
||||||
height: 28,
|
showEvents: false,
|
||||||
|
showStd: false,
|
||||||
|
showSearch: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(msgCh <-chan model.Event) error {
|
func Run(ch <-chan model.Event) error {
|
||||||
p := tea.NewProgram(NewModel(msgCh), tea.WithAltScreen())
|
p := tea.NewProgram(NewModel(ch), tea.WithAltScreen())
|
||||||
_, err := p.Run()
|
_, err := p.Run()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return waitForAppMessage(m.msgCh)
|
return waitForEvent(m.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForAppMessage(msgCh <-chan model.Event) tea.Cmd {
|
func waitForEvent(ch <-chan model.Event) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
msg, ok := <-msgCh
|
msg, ok := <-ch
|
||||||
if !ok {
|
if !ok {
|
||||||
return ErrMsg{err: fmt.Errorf("event channel closed")}
|
return ErrMsg{err: fmt.Errorf("event channel closed")}
|
||||||
}
|
}
|
||||||
|
|||||||
110
internal/tui/split.go
Normal file
110
internal/tui/split.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func (m Model) renderAppPane() string {
|
||||||
|
var (
|
||||||
|
searchW int = m.width
|
||||||
|
searchH int = 1
|
||||||
|
|
||||||
|
reqW int = int(float64(m.width) * 0.6)
|
||||||
|
reqH int = m.height
|
||||||
|
|
||||||
|
detW int = int(float64(m.width) * 0.4)
|
||||||
|
detH int = m.height
|
||||||
|
|
||||||
|
eventW int = m.width
|
||||||
|
eventH int = int(float64(m.height) * 0.15)
|
||||||
|
|
||||||
|
stdW int = m.width
|
||||||
|
stdH int = int(float64(m.height) * 0.2)
|
||||||
|
)
|
||||||
|
|
||||||
|
if m.showSearch {
|
||||||
|
reqH -= searchH
|
||||||
|
detH -= searchH
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.showEvents {
|
||||||
|
reqH -= eventH
|
||||||
|
detH -= eventH
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.showStd {
|
||||||
|
reqH -= stdH
|
||||||
|
detH -= stdH
|
||||||
|
}
|
||||||
|
|
||||||
|
reqPane := m.renderRequestPane(reqW, reqH)
|
||||||
|
detPane := m.renderDetailsPane(detW, detH)
|
||||||
|
|
||||||
|
if len(reqPane) != len(detPane) {
|
||||||
|
return "height of request and details did not match"
|
||||||
|
}
|
||||||
|
|
||||||
|
var screen []string
|
||||||
|
if m.showSearch {
|
||||||
|
searchPane := m.renderSearchPane(searchW, searchH)
|
||||||
|
screen = append(screen, searchPane...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range reqPane {
|
||||||
|
screen = append(screen, reqPane[i]+detPane[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.showEvents {
|
||||||
|
eventPane := m.renderEventsPane(eventW, eventH)
|
||||||
|
screen = append(screen, eventPane...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.showStd {
|
||||||
|
stdPane := m.renderStdPane(stdW, stdH)
|
||||||
|
screen = append(screen, stdPane...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(screen) != m.height {
|
||||||
|
return "height of screen does not match terminal height"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(screen, ("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderSearchPane(w, h int) []string {
|
||||||
|
lines := make([]string, h)
|
||||||
|
for y := range lines {
|
||||||
|
lines[y] = strings.Repeat(" ", w)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderRequestPane(w, h int) []string {
|
||||||
|
lines := make([]string, h)
|
||||||
|
for y := range lines {
|
||||||
|
lines[y] = strings.Repeat(".", w)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderDetailsPane(w, h int) []string {
|
||||||
|
lines := make([]string, h)
|
||||||
|
for y := range lines {
|
||||||
|
lines[y] = strings.Repeat("^", w)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderEventsPane(w, h int) []string {
|
||||||
|
lines := make([]string, h)
|
||||||
|
for y := range lines {
|
||||||
|
lines[y] = strings.Repeat("~", w)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) renderStdPane(w, h int) []string {
|
||||||
|
lines := make([]string, h)
|
||||||
|
for y := range lines {
|
||||||
|
lines[y] = strings.Repeat(" ", w)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/google/uuid"
|
|
||||||
"termtap.dev/internal/model"
|
"termtap.dev/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,10 +14,19 @@ 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
|
||||||
|
|
||||||
|
// TODO: Abstract the keymaps
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q", "esc":
|
case "ctrl+c", "q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
case "e":
|
||||||
|
m.showEvents = !m.showEvents
|
||||||
|
case "o":
|
||||||
|
m.showStd = !m.showStd
|
||||||
|
case "/":
|
||||||
|
m.showSearch = true
|
||||||
|
case "esc":
|
||||||
|
m.showSearch = false
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
@ -32,7 +40,7 @@ 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)
|
||||||
return m, waitForAppMessage(m.msgCh)
|
return m, waitForEvent(m.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@ -48,30 +56,29 @@ func (m *Model) pushEvent(msg model.Event) {
|
|||||||
func (m *Model) applyMessage(msg model.Event) {
|
func (m *Model) applyMessage(msg model.Event) {
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case model.EventTypeRequestStarted:
|
case model.EventTypeRequestStarted:
|
||||||
m.upsertRequest(msg.Request, true)
|
m.createRequest(msg.Request)
|
||||||
case model.EventTypeRequestFinished, model.EventTypeRequestFailed:
|
case model.EventTypeRequestFinished, model.EventTypeRequestFailed:
|
||||||
m.upsertRequest(msg.Request, false)
|
m.updateRequest(msg.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) upsertRequest(req model.Request, addIfMissing bool) {
|
func (m *Model) createRequest(req model.Request) {
|
||||||
if req.ID == uuid.Nil {
|
m.requests = append(m.requests, req)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, exists := m.requests[req.ID]
|
// If we passed the max, delete the first one
|
||||||
if !exists && !addIfMissing {
|
// Maybe we should notify the user?
|
||||||
return
|
if len(m.requests) > maxRequests {
|
||||||
|
m.requests = m.requests[1:]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !exists {
|
func (m *Model) updateRequest(req model.Request) {
|
||||||
m.requestOrder = append(m.requestOrder, req.ID)
|
// Traverse backward, since the newest one is at the end, and its likely we will be
|
||||||
if len(m.requestOrder) > maxRequests {
|
// updated a new request.
|
||||||
drop := m.requestOrder[0]
|
for i := len(m.requests) - 1; i >= 0; i-- {
|
||||||
delete(m.requests, drop)
|
if m.requests[i].ID == req.ID {
|
||||||
m.requestOrder = m.requestOrder[1:]
|
m.requests[i] = req
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.requests[req.ID] = req
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,20 +9,23 @@ import (
|
|||||||
|
|
||||||
// TODO: This is all temporary
|
// TODO: This is all temporary
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
eventLines := m.renderEvents(8)
|
return m.renderAppPane()
|
||||||
requestLines := m.renderRequests(12)
|
|
||||||
|
|
||||||
return strings.Join([]string{
|
// eventLines := m.renderEvents(8)
|
||||||
"termtap - live session",
|
// requestLines := m.renderRequests(12)
|
||||||
fmt.Sprintf("events=%d requests=%d", len(m.events), len(m.requestOrder)),
|
//
|
||||||
"keys: q/esc/ctrl+c quit",
|
// return strings.Join([]string{
|
||||||
"",
|
// "termtap - live session",
|
||||||
"Recent events:",
|
// fmt.Sprintf("events=%d requests=%d", len(m.events), len(m.requests)),
|
||||||
eventLines,
|
// fmt.Sprintf("%dx%d", m.height, m.width),
|
||||||
"",
|
// "keys: q/esc/ctrl+c quit",
|
||||||
"Recent requests:",
|
// "",
|
||||||
requestLines,
|
// "Recent events:",
|
||||||
}, "\n")
|
// eventLines,
|
||||||
|
// "",
|
||||||
|
// "Recent requests:",
|
||||||
|
// requestLines,
|
||||||
|
// }, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderEvents(limit int) string {
|
func (m Model) renderEvents(limit int) string {
|
||||||
@ -42,22 +45,16 @@ func (m Model) renderEvents(limit int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderRequests(limit int) string {
|
func (m Model) renderRequests(limit int) string {
|
||||||
if len(m.requestOrder) == 0 {
|
if len(m.requests) == 0 {
|
||||||
return " (none yet)"
|
return " (none yet)"
|
||||||
}
|
}
|
||||||
|
|
||||||
start := len(m.requestOrder) - limit
|
start := max(0, len(m.requests)-limit)
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]string, 0, len(m.requestOrder)-start)
|
// Traverse backwards since we don't have a stack
|
||||||
for i := start; i < len(m.requestOrder); i++ {
|
rows := make([]string, 0, len(m.requests)-start)
|
||||||
id := m.requestOrder[i]
|
for i := len(m.requests) - 1; i >= start; i-- {
|
||||||
req, ok := m.requests[id]
|
req := m.requests[i]
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
state := "done"
|
state := "done"
|
||||||
if req.Pending {
|
if req.Pending {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user