mirror of
https://github.com/sst/opencode.git
synced 2025-07-08 00:25:00 +00:00
initial
This commit is contained in:
commit
4b0ea68d7a
28 changed files with 2229 additions and 0 deletions
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
debug.log
|
||||
|
||||
# Binary output directory
|
||||
/bin/
|
||||
/dist/
|
||||
|
||||
# Local environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
.termai
|
59
cmd/termai/main.go
Normal file
59
cmd/termai/main.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/termai/internal/logging"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui"
|
||||
)
|
||||
|
||||
var log = logging.Get()
|
||||
|
||||
func main() {
|
||||
log.Info("Starting termai...")
|
||||
ctx := context.Background()
|
||||
|
||||
app := tea.NewProgram(
|
||||
tui.New(),
|
||||
tea.WithAltScreen(),
|
||||
)
|
||||
log.Info("Setting up subscriptions...")
|
||||
ch, unsub := setupSubscriptions(ctx)
|
||||
defer unsub()
|
||||
|
||||
go func() {
|
||||
for msg := range ch {
|
||||
app.Send(msg)
|
||||
}
|
||||
}()
|
||||
if _, err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupSubscriptions(ctx context.Context) (chan tea.Msg, func()) {
|
||||
ch := make(chan tea.Msg)
|
||||
wg := sync.WaitGroup{}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
{
|
||||
sub := log.Subscribe(ctx)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
for ev := range sub {
|
||||
ch <- ev
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
// cleanup function to be invoked when program is terminated.
|
||||
return ch, func() {
|
||||
cancel()
|
||||
// Wait for relays to finish before closing channel, to avoid sends
|
||||
// to a closed channel, which would result in a panic.
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}
|
||||
}
|
43
go.mod
Normal file
43
go.mod
Normal file
|
@ -0,0 +1,43 @@
|
|||
module github.com/kujtimiihoxha/termai
|
||||
|
||||
go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/catppuccin/go v0.3.0
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.3.4
|
||||
github.com/charmbracelet/glamour v0.9.1
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.15.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.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/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // 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
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
84
go.sum
Normal file
84
go.sum
Normal file
|
@ -0,0 +1,84 @@
|
|||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
|
||||
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
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/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
|
||||
github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
|
||||
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.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
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/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
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/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
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/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
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=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
12
internal/logging/default.go
Normal file
12
internal/logging/default.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package logging
|
||||
|
||||
var defaultLogger Interface
|
||||
|
||||
func Get() Interface {
|
||||
if defaultLogger == nil {
|
||||
defaultLogger = NewLogger(Options{
|
||||
Level: "info",
|
||||
})
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
100
internal/logging/logger.go
Normal file
100
internal/logging/logger.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
const DefaultLevel = "info"
|
||||
|
||||
var levels = map[string]slog.Level{
|
||||
"debug": slog.LevelDebug,
|
||||
DefaultLevel: slog.LevelInfo,
|
||||
"warn": slog.LevelWarn,
|
||||
"error": slog.LevelError,
|
||||
}
|
||||
|
||||
func ValidLevels() []string {
|
||||
keys := maps.Keys(levels)
|
||||
slices.SortFunc(keys, func(a, b string) int {
|
||||
if a == DefaultLevel {
|
||||
return -1
|
||||
}
|
||||
if b == DefaultLevel {
|
||||
return 1
|
||||
}
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
func NewLogger(opts Options) *Logger {
|
||||
logger := &Logger{}
|
||||
broker := pubsub.NewBroker[Message]()
|
||||
writer := &writer{
|
||||
messages: []Message{},
|
||||
Broker: broker,
|
||||
}
|
||||
|
||||
handler := slog.NewTextHandler(
|
||||
io.MultiWriter(append(opts.AdditionalWriters, writer)...),
|
||||
&slog.HandlerOptions{
|
||||
Level: slog.Level(levels[opts.Level]),
|
||||
},
|
||||
)
|
||||
logger.logger = slog.New(handler)
|
||||
logger.writer = writer
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Level string
|
||||
AdditionalWriters []io.Writer
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
logger *slog.Logger
|
||||
writer *writer
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(msg string, args ...any) {
|
||||
l.logger.Debug(msg, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Info(msg string, args ...any) {
|
||||
l.logger.Info(msg, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(msg string, args ...any) {
|
||||
l.logger.Warn(msg, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Error(msg string, args ...any) {
|
||||
l.logger.Error(msg, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) List() []Message {
|
||||
return l.writer.messages
|
||||
}
|
||||
|
||||
func (l *Logger) Get(id string) (Message, error) {
|
||||
for _, msg := range l.writer.messages {
|
||||
if msg.ID == id {
|
||||
return msg, nil
|
||||
}
|
||||
}
|
||||
return Message{}, io.EOF
|
||||
}
|
||||
|
||||
func (l *Logger) Subscribe(ctx context.Context) <-chan pubsub.Event[Message] {
|
||||
return l.writer.Subscribe(ctx)
|
||||
}
|
17
internal/logging/logging.go
Normal file
17
internal/logging/logging.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Debug(msg string, args ...any)
|
||||
Info(msg string, args ...any)
|
||||
Warn(msg string, args ...any)
|
||||
Error(msg string, args ...any)
|
||||
Subscribe(ctx context.Context) <-chan pubsub.Event[Message]
|
||||
|
||||
List() []Message
|
||||
}
|
19
internal/logging/message.go
Normal file
19
internal/logging/message.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message is the event payload for a log message
|
||||
type Message struct {
|
||||
ID string
|
||||
Time time.Time
|
||||
Level string
|
||||
Message string `json:"msg"`
|
||||
Attributes []Attr
|
||||
}
|
||||
|
||||
type Attr struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
49
internal/logging/writer.go
Normal file
49
internal/logging/writer.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-logfmt/logfmt"
|
||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||
)
|
||||
|
||||
type writer struct {
|
||||
messages []Message
|
||||
*pubsub.Broker[Message]
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (int, error) {
|
||||
d := logfmt.NewDecoder(bytes.NewReader(p))
|
||||
for d.ScanRecord() {
|
||||
msg := Message{
|
||||
ID: time.Now().Format(time.RFC3339Nano),
|
||||
}
|
||||
for d.ScanKeyval() {
|
||||
switch string(d.Key()) {
|
||||
case "time":
|
||||
parsed, err := time.Parse(time.RFC3339, string(d.Value()))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing time: %w", err)
|
||||
}
|
||||
msg.Time = parsed
|
||||
case "level":
|
||||
msg.Level = string(d.Value())
|
||||
case "msg":
|
||||
msg.Message = string(d.Value())
|
||||
default:
|
||||
msg.Attributes = append(msg.Attributes, Attr{
|
||||
Key: string(d.Key()),
|
||||
Value: string(d.Value()),
|
||||
})
|
||||
}
|
||||
}
|
||||
w.messages = append(w.messages, msg)
|
||||
w.Publish(pubsub.CreatedEvent, msg)
|
||||
}
|
||||
if d.Err() != nil {
|
||||
return 0, d.Err()
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
101
internal/pubsub/broker.go
Normal file
101
internal/pubsub/broker.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const bufferSize = 1024
|
||||
|
||||
type Logger interface {
|
||||
Debug(msg string, args ...any)
|
||||
Info(msg string, args ...any)
|
||||
Warn(msg string, args ...any)
|
||||
Error(msg string, args ...any)
|
||||
}
|
||||
|
||||
// Broker allows clients to publish events and subscribe to events
|
||||
type Broker[T any] struct {
|
||||
subs map[chan Event[T]]struct{} // subscriptions
|
||||
mu sync.Mutex // sync access to map
|
||||
done chan struct{} // close when broker is shutting down
|
||||
}
|
||||
|
||||
// NewBroker constructs a pub/sub broker.
|
||||
func NewBroker[T any]() *Broker[T] {
|
||||
b := &Broker[T]{
|
||||
subs: make(map[chan Event[T]]struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Shutdown the broker, terminating any subscriptions.
|
||||
func (b *Broker[T]) Shutdown() {
|
||||
close(b.done)
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Remove each subscriber entry, so Publish() cannot send any further
|
||||
// messages, and close each subscriber's channel, so the subscriber cannot
|
||||
// consume any more messages.
|
||||
for ch := range b.subs {
|
||||
delete(b.subs, ch)
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe subscribes the caller to a stream of events. The returned channel
|
||||
// is closed when the broker is shutdown.
|
||||
func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Check if broker has shutdown and if so return closed channel
|
||||
select {
|
||||
case <-b.done:
|
||||
ch := make(chan Event[T])
|
||||
close(ch)
|
||||
return ch
|
||||
default:
|
||||
}
|
||||
|
||||
// Subscribe
|
||||
sub := make(chan Event[T], bufferSize)
|
||||
b.subs[sub] = struct{}{}
|
||||
|
||||
// Unsubscribe when context is done.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Check if broker has shutdown and if so do nothing
|
||||
select {
|
||||
case <-b.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
delete(b.subs, sub)
|
||||
close(sub)
|
||||
}()
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
// Publish an event to subscribers.
|
||||
func (b *Broker[T]) Publish(t EventType, payload T) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
for sub := range b.subs {
|
||||
select {
|
||||
case sub <- Event[T]{Type: t, Payload: payload}:
|
||||
case <-b.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
22
internal/pubsub/events.go
Normal file
22
internal/pubsub/events.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package pubsub
|
||||
|
||||
const (
|
||||
CreatedEvent EventType = "created"
|
||||
UpdatedEvent EventType = "updated"
|
||||
DeletedEvent EventType = "deleted"
|
||||
)
|
||||
|
||||
type (
|
||||
// EventType identifies the type of event
|
||||
EventType string
|
||||
|
||||
// Event represents an event in the lifecycle of a resource
|
||||
Event[T any] struct {
|
||||
Type EventType
|
||||
Payload T
|
||||
}
|
||||
|
||||
Publisher[T any] interface {
|
||||
Publish(EventType, T)
|
||||
}
|
||||
)
|
0
internal/tui/components/core/status.go
Normal file
0
internal/tui/components/core/status.go
Normal file
131
internal/tui/components/logs/table.go
Normal file
131
internal/tui/components/logs/table.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package logs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/termai/internal/logging"
|
||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||
)
|
||||
|
||||
type TableComponent interface {
|
||||
tea.Model
|
||||
layout.Focusable
|
||||
layout.Sizeable
|
||||
layout.Bindings
|
||||
}
|
||||
|
||||
var logger = logging.Get()
|
||||
|
||||
type tableCmp struct {
|
||||
table table.Model
|
||||
}
|
||||
|
||||
func (i *tableCmp) Init() tea.Cmd {
|
||||
i.setRows()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if i.table.Focused() {
|
||||
switch msg := msg.(type) {
|
||||
case pubsub.Event[logging.Message]:
|
||||
i.setRows()
|
||||
return i, nil
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "ctrl+s" {
|
||||
logger.Info("Saving logs...",
|
||||
"rows", len(i.table.Rows()),
|
||||
)
|
||||
}
|
||||
}
|
||||
t, cmd := i.table.Update(msg)
|
||||
i.table = t
|
||||
return i, cmd
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *tableCmp) View() string {
|
||||
return i.table.View()
|
||||
}
|
||||
|
||||
func (i *tableCmp) Blur() tea.Cmd {
|
||||
i.table.Blur()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *tableCmp) Focus() tea.Cmd {
|
||||
i.table.Focus()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *tableCmp) IsFocused() bool {
|
||||
return i.table.Focused()
|
||||
}
|
||||
|
||||
func (i *tableCmp) GetSize() (int, int) {
|
||||
return i.table.Width(), i.table.Height()
|
||||
}
|
||||
|
||||
func (i *tableCmp) SetSize(width int, height int) {
|
||||
i.table.SetWidth(width)
|
||||
i.table.SetHeight(height)
|
||||
cloumns := i.table.Columns()
|
||||
for i, col := range cloumns {
|
||||
col.Width = (width / len(cloumns)) - 2
|
||||
cloumns[i] = col
|
||||
}
|
||||
i.table.SetColumns(cloumns)
|
||||
}
|
||||
|
||||
func (i *tableCmp) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(i.table.KeyMap)
|
||||
}
|
||||
|
||||
func (i *tableCmp) setRows() {
|
||||
rows := []table.Row{}
|
||||
|
||||
logs := logger.List()
|
||||
slices.SortFunc(logs, func(a, b logging.Message) int {
|
||||
if a.Time.Before(b.Time) {
|
||||
return 1
|
||||
}
|
||||
if a.Time.After(b.Time) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for _, log := range logs {
|
||||
bm, _ := json.Marshal(log.Attributes)
|
||||
|
||||
row := table.Row{
|
||||
log.Time.Format("15:04:05"),
|
||||
log.Level,
|
||||
log.Message,
|
||||
string(bm),
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
i.table.SetRows(rows)
|
||||
}
|
||||
|
||||
func NewLogsTable() TableComponent {
|
||||
columns := []table.Column{
|
||||
{Title: "Time", Width: 4},
|
||||
{Title: "Level", Width: 10},
|
||||
{Title: "Message", Width: 10},
|
||||
{Title: "Attributes", Width: 10},
|
||||
}
|
||||
tableModel := table.New(
|
||||
table.WithColumns(columns),
|
||||
)
|
||||
return &tableCmp{
|
||||
table: tableModel,
|
||||
}
|
||||
}
|
21
internal/tui/components/repl/editor.go
Normal file
21
internal/tui/components/repl/editor.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package repl
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type editorCmp struct{}
|
||||
|
||||
func (i *editorCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *editorCmp) Update(_ tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *editorCmp) View() string {
|
||||
return "Editor"
|
||||
}
|
||||
|
||||
func NewEditorCmp() tea.Model {
|
||||
return &editorCmp{}
|
||||
}
|
21
internal/tui/components/repl/messages.go
Normal file
21
internal/tui/components/repl/messages.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package repl
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type messagesCmp struct{}
|
||||
|
||||
func (i *messagesCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *messagesCmp) Update(_ tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *messagesCmp) View() string {
|
||||
return "Messages"
|
||||
}
|
||||
|
||||
func NewMessagesCmp() tea.Model {
|
||||
return &messagesCmp{}
|
||||
}
|
21
internal/tui/components/repl/threads.go
Normal file
21
internal/tui/components/repl/threads.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package repl
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type threadsCmp struct{}
|
||||
|
||||
func (i *threadsCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *threadsCmp) Update(_ tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *threadsCmp) View() string {
|
||||
return "Threads"
|
||||
}
|
||||
|
||||
func NewThreadsCmp() tea.Model {
|
||||
return &threadsCmp{}
|
||||
}
|
361
internal/tui/layout/bento.go
Normal file
361
internal/tui/layout/bento.go
Normal file
|
@ -0,0 +1,361 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type paneID string
|
||||
|
||||
const (
|
||||
BentoLeftPane paneID = "left"
|
||||
BentoRightTopPane paneID = "right-top"
|
||||
BentoRightBottomPane paneID = "right-bottom"
|
||||
)
|
||||
|
||||
type BentoPanes map[paneID]tea.Model
|
||||
|
||||
const (
|
||||
defaultLeftWidthRatio = 0.2
|
||||
defaultRightTopHeightRatio = 0.85
|
||||
|
||||
minLeftWidth = 10
|
||||
minRightBottomHeight = 10
|
||||
)
|
||||
|
||||
type BentoLayout interface {
|
||||
tea.Model
|
||||
Sizeable
|
||||
Bindings
|
||||
}
|
||||
|
||||
type BentoKeyBindings struct {
|
||||
SwitchPane key.Binding
|
||||
SwitchPaneBack key.Binding
|
||||
HideCurrentPane key.Binding
|
||||
ShowAllPanes key.Binding
|
||||
}
|
||||
|
||||
var defaultBentoKeyBindings = BentoKeyBindings{
|
||||
SwitchPane: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "switch pane"),
|
||||
),
|
||||
SwitchPaneBack: key.NewBinding(
|
||||
key.WithKeys("shift+tab"),
|
||||
key.WithHelp("shift+tab", "switch pane back"),
|
||||
),
|
||||
HideCurrentPane: key.NewBinding(
|
||||
key.WithKeys("X"),
|
||||
key.WithHelp("X", "hide current pane"),
|
||||
),
|
||||
ShowAllPanes: key.NewBinding(
|
||||
key.WithKeys("R"),
|
||||
key.WithHelp("R", "show all panes"),
|
||||
),
|
||||
}
|
||||
|
||||
type bentoLayout struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
leftWidthRatio float64
|
||||
rightTopHeightRatio float64
|
||||
|
||||
currentPane paneID
|
||||
panes map[paneID]SinglePaneLayout
|
||||
hiddenPanes map[paneID]bool
|
||||
}
|
||||
|
||||
func (b *bentoLayout) GetSize() (int, int) {
|
||||
return b.width, b.height
|
||||
}
|
||||
|
||||
func (b bentoLayout) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b bentoLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
b.SetSize(msg.Width, msg.Height)
|
||||
return b, nil
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, defaultBentoKeyBindings.SwitchPane):
|
||||
return b, b.SwitchPane(false)
|
||||
case key.Matches(msg, defaultBentoKeyBindings.SwitchPaneBack):
|
||||
return b, b.SwitchPane(true)
|
||||
case key.Matches(msg, defaultBentoKeyBindings.HideCurrentPane):
|
||||
return b, b.HidePane(b.currentPane)
|
||||
case key.Matches(msg, defaultBentoKeyBindings.ShowAllPanes):
|
||||
for id := range b.hiddenPanes {
|
||||
delete(b.hiddenPanes, id)
|
||||
}
|
||||
b.SetSize(b.width, b.height)
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
|
||||
if pane, ok := b.panes[b.currentPane]; ok {
|
||||
u, cmd := pane.Update(msg)
|
||||
b.panes[b.currentPane] = u.(SinglePaneLayout)
|
||||
return b, cmd
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b bentoLayout) View() string {
|
||||
if b.width <= 0 || b.height <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for id, pane := range b.panes {
|
||||
if b.currentPane == id {
|
||||
pane.Focus()
|
||||
} else {
|
||||
pane.Blur()
|
||||
}
|
||||
}
|
||||
|
||||
leftVisible := false
|
||||
rightTopVisible := false
|
||||
rightBottomVisible := false
|
||||
|
||||
var leftPane, rightTopPane, rightBottomPane string
|
||||
|
||||
if pane, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] {
|
||||
leftPane = pane.View()
|
||||
leftVisible = true
|
||||
}
|
||||
|
||||
if pane, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] {
|
||||
rightTopPane = pane.View()
|
||||
rightTopVisible = true
|
||||
}
|
||||
|
||||
if pane, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] {
|
||||
rightBottomPane = pane.View()
|
||||
rightBottomVisible = true
|
||||
}
|
||||
|
||||
if leftVisible {
|
||||
if rightTopVisible || rightBottomVisible {
|
||||
rightSection := ""
|
||||
if rightTopVisible && rightBottomVisible {
|
||||
rightSection = lipgloss.JoinVertical(lipgloss.Top, rightTopPane, rightBottomPane)
|
||||
} else if rightTopVisible {
|
||||
rightSection = rightTopPane
|
||||
} else {
|
||||
rightSection = rightBottomPane
|
||||
}
|
||||
return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, leftPane, rightSection),
|
||||
)
|
||||
} else {
|
||||
return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(leftPane)
|
||||
}
|
||||
} else if rightTopVisible || rightBottomVisible {
|
||||
if rightTopVisible && rightBottomVisible {
|
||||
return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(
|
||||
lipgloss.JoinVertical(lipgloss.Top, rightTopPane, rightBottomPane),
|
||||
)
|
||||
} else if rightTopVisible {
|
||||
return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(rightTopPane)
|
||||
} else {
|
||||
return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(rightBottomPane)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *bentoLayout) SetSize(width int, height int) {
|
||||
if width < 0 || height < 0 {
|
||||
return
|
||||
}
|
||||
b.width = width
|
||||
b.height = height
|
||||
|
||||
// Check which panes are available
|
||||
leftExists := false
|
||||
rightTopExists := false
|
||||
rightBottomExists := false
|
||||
|
||||
if _, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] {
|
||||
leftExists = true
|
||||
}
|
||||
if _, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] {
|
||||
rightTopExists = true
|
||||
}
|
||||
if _, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] {
|
||||
rightBottomExists = true
|
||||
}
|
||||
|
||||
leftWidth := 0
|
||||
rightWidth := 0
|
||||
rightTopHeight := 0
|
||||
rightBottomHeight := 0
|
||||
|
||||
if leftExists && (rightTopExists || rightBottomExists) {
|
||||
leftWidth = int(float64(width) * b.leftWidthRatio)
|
||||
if leftWidth < minLeftWidth && width >= minLeftWidth {
|
||||
leftWidth = minLeftWidth
|
||||
}
|
||||
rightWidth = width - leftWidth
|
||||
|
||||
if rightTopExists && rightBottomExists {
|
||||
rightTopHeight = int(float64(height) * b.rightTopHeightRatio)
|
||||
rightBottomHeight = height - rightTopHeight
|
||||
|
||||
// Ensure minimum height for bottom pane
|
||||
if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight {
|
||||
rightBottomHeight = minRightBottomHeight
|
||||
rightTopHeight = height - rightBottomHeight
|
||||
}
|
||||
} else if rightTopExists {
|
||||
rightTopHeight = height
|
||||
} else if rightBottomExists {
|
||||
rightBottomHeight = height
|
||||
}
|
||||
} else if leftExists {
|
||||
leftWidth = width
|
||||
} else if rightTopExists || rightBottomExists {
|
||||
rightWidth = width
|
||||
|
||||
if rightTopExists && rightBottomExists {
|
||||
rightTopHeight = int(float64(height) * b.rightTopHeightRatio)
|
||||
rightBottomHeight = height - rightTopHeight
|
||||
|
||||
if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight {
|
||||
rightBottomHeight = minRightBottomHeight
|
||||
rightTopHeight = height - rightBottomHeight
|
||||
}
|
||||
} else if rightTopExists {
|
||||
rightTopHeight = height
|
||||
} else if rightBottomExists {
|
||||
rightBottomHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
if pane, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] {
|
||||
pane.SetSize(leftWidth, height)
|
||||
}
|
||||
if pane, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] {
|
||||
pane.SetSize(rightWidth, rightTopHeight)
|
||||
}
|
||||
if pane, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] {
|
||||
pane.SetSize(rightWidth, rightBottomHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bentoLayout) HidePane(pane paneID) tea.Cmd {
|
||||
if len(b.panes)-len(b.hiddenPanes) == 1 {
|
||||
return nil
|
||||
}
|
||||
if _, ok := b.panes[pane]; ok {
|
||||
b.hiddenPanes[pane] = true
|
||||
}
|
||||
b.SetSize(b.width, b.height)
|
||||
return b.SwitchPane(false)
|
||||
}
|
||||
|
||||
func (b *bentoLayout) SwitchPane(back bool) tea.Cmd {
|
||||
if back {
|
||||
switch b.currentPane {
|
||||
case BentoLeftPane:
|
||||
b.currentPane = BentoRightBottomPane
|
||||
case BentoRightTopPane:
|
||||
b.currentPane = BentoLeftPane
|
||||
case BentoRightBottomPane:
|
||||
b.currentPane = BentoRightTopPane
|
||||
}
|
||||
} else {
|
||||
switch b.currentPane {
|
||||
case BentoLeftPane:
|
||||
b.currentPane = BentoRightTopPane
|
||||
case BentoRightTopPane:
|
||||
b.currentPane = BentoRightBottomPane
|
||||
case BentoRightBottomPane:
|
||||
b.currentPane = BentoLeftPane
|
||||
}
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
for id, pane := range b.panes {
|
||||
if _, ok := b.hiddenPanes[id]; ok {
|
||||
continue
|
||||
}
|
||||
if id == b.currentPane {
|
||||
cmds = append(cmds, pane.Focus())
|
||||
} else {
|
||||
cmds = append(cmds, pane.Blur())
|
||||
}
|
||||
}
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (s *bentoLayout) BindingKeys() []key.Binding {
|
||||
bindings := KeyMapToSlice(defaultBentoKeyBindings)
|
||||
if b, ok := s.panes[s.currentPane].(Bindings); ok {
|
||||
bindings = append(bindings, b.BindingKeys()...)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
type BentoLayoutOption func(*bentoLayout)
|
||||
|
||||
func NewBentoLayout(panes BentoPanes, opts ...BentoLayoutOption) BentoLayout {
|
||||
p := make(map[paneID]SinglePaneLayout, len(panes))
|
||||
for id, pane := range panes {
|
||||
// Wrap any pane that is not a SinglePaneLayout in a SinglePaneLayout
|
||||
if _, ok := pane.(SinglePaneLayout); !ok {
|
||||
p[id] = NewSinglePane(
|
||||
pane,
|
||||
WithSinglePaneFocusable(true),
|
||||
WithSinglePaneBordered(true),
|
||||
)
|
||||
} else {
|
||||
p[id] = pane.(SinglePaneLayout)
|
||||
}
|
||||
}
|
||||
if len(p) == 0 {
|
||||
panic("no panes provided for BentoLayout")
|
||||
}
|
||||
layout := &bentoLayout{
|
||||
panes: p,
|
||||
hiddenPanes: make(map[paneID]bool),
|
||||
currentPane: BentoLeftPane,
|
||||
leftWidthRatio: defaultLeftWidthRatio,
|
||||
rightTopHeightRatio: defaultRightTopHeightRatio,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(layout)
|
||||
}
|
||||
|
||||
return layout
|
||||
}
|
||||
|
||||
func WithBentoLayoutLeftWidthRatio(ratio float64) BentoLayoutOption {
|
||||
return func(b *bentoLayout) {
|
||||
if ratio > 0 && ratio < 1 {
|
||||
b.leftWidthRatio = ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithBentoLayoutRightTopHeightRatio(ratio float64) BentoLayoutOption {
|
||||
return func(b *bentoLayout) {
|
||||
if ratio > 0 && ratio < 1 {
|
||||
b.rightTopHeightRatio = ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithBentoLayoutCurrentPane(pane paneID) BentoLayoutOption {
|
||||
return func(b *bentoLayout) {
|
||||
b.currentPane = pane
|
||||
}
|
||||
}
|
99
internal/tui/layout/border.go
Normal file
99
internal/tui/layout/border.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/styles"
|
||||
)
|
||||
|
||||
type BorderPosition int
|
||||
|
||||
const (
|
||||
TopLeftBorder BorderPosition = iota
|
||||
TopMiddleBorder
|
||||
TopRightBorder
|
||||
BottomLeftBorder
|
||||
BottomMiddleBorder
|
||||
BottomRightBorder
|
||||
)
|
||||
|
||||
var (
|
||||
ActiveBorder = styles.Blue
|
||||
InactivePreviewBorder = styles.Grey
|
||||
)
|
||||
|
||||
func Borderize(content string, active bool, embeddedText map[BorderPosition]string) string {
|
||||
if embeddedText == nil {
|
||||
embeddedText = make(map[BorderPosition]string)
|
||||
}
|
||||
var (
|
||||
thickness = map[bool]lipgloss.Border{
|
||||
true: lipgloss.Border(lipgloss.ThickBorder()),
|
||||
false: lipgloss.Border(lipgloss.NormalBorder()),
|
||||
}
|
||||
color = map[bool]lipgloss.TerminalColor{
|
||||
true: ActiveBorder,
|
||||
false: InactivePreviewBorder,
|
||||
}
|
||||
border = thickness[active]
|
||||
style = lipgloss.NewStyle().Foreground(color[active])
|
||||
width = lipgloss.Width(content)
|
||||
)
|
||||
|
||||
encloseInSquareBrackets := func(text string) string {
|
||||
if text != "" {
|
||||
return fmt.Sprintf("%s%s%s",
|
||||
style.Render(border.TopRight),
|
||||
text,
|
||||
style.Render(border.TopLeft),
|
||||
)
|
||||
}
|
||||
return text
|
||||
}
|
||||
buildHorizontalBorder := func(leftText, middleText, rightText, leftCorner, inbetween, rightCorner string) string {
|
||||
leftText = encloseInSquareBrackets(leftText)
|
||||
middleText = encloseInSquareBrackets(middleText)
|
||||
rightText = encloseInSquareBrackets(rightText)
|
||||
// Calculate length of border between embedded texts
|
||||
remaining := max(0, width-lipgloss.Width(leftText)-lipgloss.Width(middleText)-lipgloss.Width(rightText))
|
||||
leftBorderLen := max(0, (width/2)-lipgloss.Width(leftText)-(lipgloss.Width(middleText)/2))
|
||||
rightBorderLen := max(0, remaining-leftBorderLen)
|
||||
// Then construct border string
|
||||
s := leftText +
|
||||
style.Render(strings.Repeat(inbetween, leftBorderLen)) +
|
||||
middleText +
|
||||
style.Render(strings.Repeat(inbetween, rightBorderLen)) +
|
||||
rightText
|
||||
// Make it fit in the space available between the two corners.
|
||||
s = lipgloss.NewStyle().
|
||||
Inline(true).
|
||||
MaxWidth(width).
|
||||
Render(s)
|
||||
// Add the corners
|
||||
return style.Render(leftCorner) + s + style.Render(rightCorner)
|
||||
}
|
||||
// Stack top border, content and horizontal borders, and bottom border.
|
||||
return strings.Join([]string{
|
||||
buildHorizontalBorder(
|
||||
embeddedText[TopLeftBorder],
|
||||
embeddedText[TopMiddleBorder],
|
||||
embeddedText[TopRightBorder],
|
||||
border.TopLeft,
|
||||
border.Top,
|
||||
border.TopRight,
|
||||
),
|
||||
lipgloss.NewStyle().
|
||||
BorderForeground(color[active]).
|
||||
Border(border, false, true, false, true).Render(content),
|
||||
buildHorizontalBorder(
|
||||
embeddedText[BottomLeftBorder],
|
||||
embeddedText[BottomMiddleBorder],
|
||||
embeddedText[BottomRightBorder],
|
||||
border.BottomLeft,
|
||||
border.Bottom,
|
||||
border.BottomRight,
|
||||
),
|
||||
}, "\n")
|
||||
}
|
39
internal/tui/layout/layout.go
Normal file
39
internal/tui/layout/layout.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type Focusable interface {
|
||||
Focus() tea.Cmd
|
||||
Blur() tea.Cmd
|
||||
IsFocused() bool
|
||||
}
|
||||
|
||||
type Bordered interface {
|
||||
BorderText() map[BorderPosition]string
|
||||
}
|
||||
|
||||
type Sizeable interface {
|
||||
SetSize(width, height int)
|
||||
GetSize() (int, int)
|
||||
}
|
||||
|
||||
type Bindings interface {
|
||||
BindingKeys() []key.Binding
|
||||
}
|
||||
|
||||
func KeyMapToSlice(t any) (bindings []key.Binding) {
|
||||
typ := reflect.TypeOf(t)
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
for i := range typ.NumField() {
|
||||
v := reflect.ValueOf(t).Field(i)
|
||||
bindings = append(bindings, v.Interface().(key.Binding))
|
||||
}
|
||||
return
|
||||
}
|
172
internal/tui/layout/single.go
Normal file
172
internal/tui/layout/single.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type SinglePaneLayout interface {
|
||||
tea.Model
|
||||
Focusable
|
||||
Sizeable
|
||||
Bindings
|
||||
}
|
||||
|
||||
type singlePaneLayout struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
focusable bool
|
||||
focused bool
|
||||
|
||||
bordered bool
|
||||
borderText map[BorderPosition]string
|
||||
|
||||
content tea.Model
|
||||
|
||||
padding []int
|
||||
}
|
||||
|
||||
type SinglePaneOption func(*singlePaneLayout)
|
||||
|
||||
func (s singlePaneLayout) Init() tea.Cmd {
|
||||
return s.content.Init()
|
||||
}
|
||||
|
||||
func (s singlePaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
s.SetSize(msg.Width, msg.Height)
|
||||
return s, nil
|
||||
}
|
||||
u, cmd := s.content.Update(msg)
|
||||
s.content = u
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
func (s singlePaneLayout) View() string {
|
||||
style := lipgloss.NewStyle().Width(s.width).Height(s.height)
|
||||
if s.bordered {
|
||||
style = style.Width(s.width).Height(s.height)
|
||||
}
|
||||
if s.padding != nil {
|
||||
style = style.Padding(s.padding...)
|
||||
}
|
||||
content := style.Render(s.content.View())
|
||||
if s.bordered {
|
||||
if s.borderText == nil {
|
||||
s.borderText = map[BorderPosition]string{}
|
||||
}
|
||||
if bordered, ok := s.content.(Bordered); ok {
|
||||
s.borderText = bordered.BorderText()
|
||||
}
|
||||
return Borderize(content, s.focused, s.borderText)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (s *singlePaneLayout) Blur() tea.Cmd {
|
||||
if s.focusable {
|
||||
s.focused = false
|
||||
}
|
||||
if blurable, ok := s.content.(Focusable); ok {
|
||||
return blurable.Blur()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *singlePaneLayout) Focus() tea.Cmd {
|
||||
if s.focusable {
|
||||
s.focused = true
|
||||
}
|
||||
if focusable, ok := s.content.(Focusable); ok {
|
||||
return focusable.Focus()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *singlePaneLayout) SetSize(width, height int) {
|
||||
s.width = width
|
||||
s.height = height
|
||||
if s.bordered {
|
||||
s.width -= 2
|
||||
s.height -= 2
|
||||
}
|
||||
if s.padding != nil {
|
||||
if len(s.padding) == 1 {
|
||||
s.width -= s.padding[0] * 2
|
||||
s.height -= s.padding[0] * 2
|
||||
} else if len(s.padding) == 2 {
|
||||
s.width -= s.padding[0] * 2
|
||||
s.height -= s.padding[1] * 2
|
||||
} else if len(s.padding) == 3 {
|
||||
s.width -= s.padding[0] * 2
|
||||
s.height -= s.padding[1] + s.padding[2]
|
||||
} else if len(s.padding) == 4 {
|
||||
s.width -= s.padding[0] + s.padding[2]
|
||||
s.height -= s.padding[1] + s.padding[3]
|
||||
}
|
||||
}
|
||||
if s.content != nil {
|
||||
if c, ok := s.content.(Sizeable); ok {
|
||||
c.SetSize(s.width, s.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *singlePaneLayout) IsFocused() bool {
|
||||
return s.focused
|
||||
}
|
||||
|
||||
func (s *singlePaneLayout) GetSize() (int, int) {
|
||||
return s.width, s.height
|
||||
}
|
||||
|
||||
func (s *singlePaneLayout) BindingKeys() []key.Binding {
|
||||
if b, ok := s.content.(Bindings); ok {
|
||||
return b.BindingKeys()
|
||||
}
|
||||
return []key.Binding{}
|
||||
}
|
||||
|
||||
func NewSinglePane(content tea.Model, opts ...SinglePaneOption) SinglePaneLayout {
|
||||
layout := &singlePaneLayout{
|
||||
content: content,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(layout)
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
func WithSignlePaneSize(width, height int) SinglePaneOption {
|
||||
return func(opts *singlePaneLayout) {
|
||||
opts.width = width
|
||||
opts.height = height
|
||||
}
|
||||
}
|
||||
|
||||
func WithSinglePaneFocusable(focusable bool) SinglePaneOption {
|
||||
return func(opts *singlePaneLayout) {
|
||||
opts.focusable = focusable
|
||||
}
|
||||
}
|
||||
|
||||
func WithSinglePaneBordered(bordered bool) SinglePaneOption {
|
||||
return func(opts *singlePaneLayout) {
|
||||
opts.bordered = bordered
|
||||
}
|
||||
}
|
||||
|
||||
func WithSignlePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption {
|
||||
return func(opts *singlePaneLayout) {
|
||||
opts.borderText = borderText
|
||||
}
|
||||
}
|
||||
|
||||
func WithSinglePanePadding(padding ...int) SinglePaneOption {
|
||||
return func(opts *singlePaneLayout) {
|
||||
opts.padding = padding
|
||||
}
|
||||
}
|
37
internal/tui/page/init.go
Normal file
37
internal/tui/page/init.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||
)
|
||||
|
||||
var InitPage PageID = "init"
|
||||
|
||||
type initPage struct {
|
||||
layout layout.SinglePaneLayout
|
||||
}
|
||||
|
||||
func (i initPage) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i initPage) Update(_ tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i initPage) View() string {
|
||||
return "Initializing..."
|
||||
}
|
||||
|
||||
func NewInitPage() tea.Model {
|
||||
return layout.NewSinglePane(
|
||||
&initPage{},
|
||||
layout.WithSinglePaneFocusable(true),
|
||||
layout.WithSinglePaneBordered(true),
|
||||
layout.WithSignlePaneBorderText(
|
||||
map[layout.BorderPosition]string{
|
||||
layout.TopMiddleBorder: "Welcome to termai",
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
25
internal/tui/page/logs.go
Normal file
25
internal/tui/page/logs.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/components/logs"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||
)
|
||||
|
||||
var LogsPage PageID = "logs"
|
||||
|
||||
func NewLogsPage() tea.Model {
|
||||
p := layout.NewSinglePane(
|
||||
logs.NewLogsTable(),
|
||||
layout.WithSinglePaneFocusable(true),
|
||||
layout.WithSinglePaneBordered(true),
|
||||
layout.WithSignlePaneBorderText(
|
||||
map[layout.BorderPosition]string{
|
||||
layout.TopMiddleBorder: "Logs",
|
||||
},
|
||||
),
|
||||
layout.WithSinglePanePadding(1),
|
||||
)
|
||||
p.Focus()
|
||||
return p
|
||||
}
|
3
internal/tui/page/page.go
Normal file
3
internal/tui/page/page.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package page
|
||||
|
||||
type PageID string
|
19
internal/tui/page/repl.go
Normal file
19
internal/tui/page/repl.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/components/repl"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||
)
|
||||
|
||||
var ReplPage PageID = "repl"
|
||||
|
||||
func NewReplPage() tea.Model {
|
||||
return layout.NewBentoLayout(
|
||||
layout.BentoPanes{
|
||||
layout.BentoLeftPane: repl.NewThreadsCmp(),
|
||||
layout.BentoRightTopPane: repl.NewMessagesCmp(),
|
||||
layout.BentoRightBottomPane: repl.NewEditorCmp(),
|
||||
},
|
||||
)
|
||||
}
|
12
internal/tui/styles/icons.go
Normal file
12
internal/tui/styles/icons.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package styles
|
||||
|
||||
const (
|
||||
SessionsIcon string = ""
|
||||
ChatIcon string = ""
|
||||
|
||||
BotIcon string = ""
|
||||
ToolIcon string = ""
|
||||
UserIcon string = ""
|
||||
|
||||
SleepIcon string = ""
|
||||
)
|
498
internal/tui/styles/markdown.go
Normal file
498
internal/tui/styles/markdown.go
Normal file
|
@ -0,0 +1,498 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/glamour/ansi"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const defaultMargin = 2
|
||||
|
||||
// Helper functions for style pointers
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func stringPtr(s string) *string { return &s }
|
||||
func uintPtr(u uint) *uint { return &u }
|
||||
|
||||
// CatppuccinMarkdownStyle is the Catppuccin Mocha style for Glamour markdown rendering.
|
||||
func CatppuccinMarkdownStyle() ansi.StyleConfig {
|
||||
isDark := lipgloss.HasDarkBackground()
|
||||
if isDark {
|
||||
return catppuccinDark
|
||||
}
|
||||
return catppuccinLight
|
||||
}
|
||||
|
||||
var catppuccinDark = ansi.StyleConfig{
|
||||
Document: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n",
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(dark.Text().Hex),
|
||||
},
|
||||
Margin: uintPtr(defaultMargin),
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Yellow().Hex),
|
||||
Italic: boolPtr(true),
|
||||
Prefix: "┃ ",
|
||||
},
|
||||
Indent: uintPtr(1),
|
||||
Margin: uintPtr(defaultMargin),
|
||||
},
|
||||
List: ansi.StyleList{
|
||||
LevelIndent: defaultMargin,
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Text().Hex),
|
||||
},
|
||||
},
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(dark.Mauve().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "# ",
|
||||
Color: stringPtr(dark.Lavender().Hex),
|
||||
Bold: boolPtr(true),
|
||||
BlockPrefix: "\n",
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: stringPtr(dark.Mauve().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: stringPtr(dark.Flamingo().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: stringPtr(dark.Rosewater().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: stringPtr(dark.Rosewater().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: boolPtr(true),
|
||||
Color: stringPtr(dark.Overlay1().Hex),
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Yellow().Hex),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
Color: stringPtr(dark.Peach().Hex),
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Overlay0().Hex),
|
||||
Format: "\n─────────────────────────────────────────\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
Color: stringPtr(dark.Blue().Hex),
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
Color: stringPtr(dark.Sky().Hex),
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
StylePrimitive: ansi.StylePrimitive{},
|
||||
Ticked: "[✓] ",
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Sky().Hex),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Sapphire().Hex),
|
||||
Underline: boolPtr(true),
|
||||
Format: "🖼 {{.text}}",
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
Format: "{{.text}}",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Green().Hex),
|
||||
Prefix: " ",
|
||||
Suffix: " ",
|
||||
},
|
||||
},
|
||||
CodeBlock: ansi.StyleCodeBlock{
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: " ",
|
||||
Color: stringPtr(dark.Text().Hex),
|
||||
},
|
||||
|
||||
Margin: uintPtr(defaultMargin),
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Text().Hex),
|
||||
},
|
||||
Error: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Text().Hex),
|
||||
},
|
||||
Comment: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Overlay1().Hex),
|
||||
},
|
||||
CommentPreproc: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
Keyword: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Sky().Hex),
|
||||
},
|
||||
Operator: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
Punctuation: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Text().Hex),
|
||||
},
|
||||
Name: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Sky().Hex),
|
||||
},
|
||||
NameBuiltin: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Sky().Hex),
|
||||
},
|
||||
NameTag: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
NameAttribute: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Green().Hex),
|
||||
},
|
||||
NameClass: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Sky().Hex),
|
||||
},
|
||||
NameConstant: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Mauve().Hex),
|
||||
},
|
||||
NameDecorator: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Green().Hex),
|
||||
},
|
||||
NameFunction: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Green().Hex),
|
||||
},
|
||||
LiteralNumber: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Teal().Hex),
|
||||
},
|
||||
LiteralString: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Yellow().Hex),
|
||||
},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Pink().Hex),
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Red().Hex),
|
||||
},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Yellow().Hex),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Green().Hex),
|
||||
},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Peach().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
Color: stringPtr(dark.Mauve().Hex),
|
||||
},
|
||||
},
|
||||
},
|
||||
Table: ansi.StyleTable{
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n",
|
||||
BlockSuffix: "\n",
|
||||
},
|
||||
},
|
||||
CenterSeparator: stringPtr("┼"),
|
||||
ColumnSeparator: stringPtr("│"),
|
||||
RowSeparator: stringPtr("─"),
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n ❯ ",
|
||||
Color: stringPtr(dark.Sapphire().Hex),
|
||||
},
|
||||
}
|
||||
|
||||
var catppuccinLight = ansi.StyleConfig{
|
||||
Document: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n",
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(light.Text().Hex),
|
||||
},
|
||||
Margin: uintPtr(defaultMargin),
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Yellow().Hex),
|
||||
Italic: boolPtr(true),
|
||||
Prefix: "┃ ",
|
||||
},
|
||||
Indent: uintPtr(1),
|
||||
Margin: uintPtr(defaultMargin),
|
||||
},
|
||||
List: ansi.StyleList{
|
||||
LevelIndent: defaultMargin,
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Text().Hex),
|
||||
},
|
||||
},
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(light.Mauve().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "# ",
|
||||
Color: stringPtr(light.Lavender().Hex),
|
||||
Bold: boolPtr(true),
|
||||
BlockPrefix: "\n",
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: stringPtr(light.Mauve().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: stringPtr(light.Flamingo().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: stringPtr(light.Rosewater().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: stringPtr(light.Rosewater().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: boolPtr(true),
|
||||
Color: stringPtr(light.Overlay1().Hex),
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Yellow().Hex),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
Color: stringPtr(light.Peach().Hex),
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Overlay0().Hex),
|
||||
Format: "\n─────────────────────────────────────────\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
Color: stringPtr(light.Blue().Hex),
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
Color: stringPtr(light.Sky().Hex),
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
StylePrimitive: ansi.StylePrimitive{},
|
||||
Ticked: "[✓] ",
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Sky().Hex),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Sapphire().Hex),
|
||||
Underline: boolPtr(true),
|
||||
Format: "🖼 {{.text}}",
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
Format: "{{.text}}",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Green().Hex),
|
||||
Prefix: " ",
|
||||
Suffix: " ",
|
||||
},
|
||||
},
|
||||
CodeBlock: ansi.StyleCodeBlock{
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: " ",
|
||||
Color: stringPtr(light.Text().Hex),
|
||||
},
|
||||
|
||||
Margin: uintPtr(defaultMargin),
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Text().Hex),
|
||||
},
|
||||
Error: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Text().Hex),
|
||||
},
|
||||
Comment: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Overlay1().Hex),
|
||||
},
|
||||
CommentPreproc: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
Keyword: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Sky().Hex),
|
||||
},
|
||||
Operator: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
Punctuation: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Text().Hex),
|
||||
},
|
||||
Name: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Sky().Hex),
|
||||
},
|
||||
NameBuiltin: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Sky().Hex),
|
||||
},
|
||||
NameTag: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
NameAttribute: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Green().Hex),
|
||||
},
|
||||
NameClass: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Sky().Hex),
|
||||
},
|
||||
NameConstant: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Mauve().Hex),
|
||||
},
|
||||
NameDecorator: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Green().Hex),
|
||||
},
|
||||
NameFunction: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Green().Hex),
|
||||
},
|
||||
LiteralNumber: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Teal().Hex),
|
||||
},
|
||||
LiteralString: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Yellow().Hex),
|
||||
},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Pink().Hex),
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Red().Hex),
|
||||
},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Yellow().Hex),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Green().Hex),
|
||||
},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Peach().Hex),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
Color: stringPtr(light.Mauve().Hex),
|
||||
},
|
||||
},
|
||||
},
|
||||
Table: ansi.StyleTable{
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n",
|
||||
BlockSuffix: "\n",
|
||||
},
|
||||
},
|
||||
CenterSeparator: stringPtr("┼"),
|
||||
ColumnSeparator: stringPtr("│"),
|
||||
RowSeparator: stringPtr("─"),
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n ❯ ",
|
||||
Color: stringPtr(light.Sapphire().Hex),
|
||||
},
|
||||
}
|
121
internal/tui/styles/styles.go
Normal file
121
internal/tui/styles/styles.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
catppuccin "github.com/catppuccin/go"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
light = catppuccin.Latte
|
||||
dark = catppuccin.Mocha
|
||||
)
|
||||
|
||||
var (
|
||||
Regular = lipgloss.NewStyle()
|
||||
Bold = Regular.Bold(true)
|
||||
Padded = Regular.Padding(0, 1)
|
||||
|
||||
Border = Regular.Border(lipgloss.NormalBorder())
|
||||
ThickBorder = Regular.Border(lipgloss.ThickBorder())
|
||||
DoubleBorder = Regular.Border(lipgloss.DoubleBorder())
|
||||
// Colors
|
||||
|
||||
Surface0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface0().Hex,
|
||||
Light: light.Surface0().Hex,
|
||||
}
|
||||
|
||||
Overlay0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Overlay0().Hex,
|
||||
Light: light.Overlay0().Hex,
|
||||
}
|
||||
|
||||
Ovelay1 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Overlay1().Hex,
|
||||
Light: light.Overlay1().Hex,
|
||||
}
|
||||
|
||||
Text = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Text().Hex,
|
||||
Light: light.Text().Hex,
|
||||
}
|
||||
|
||||
SubText0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Subtext0().Hex,
|
||||
Light: light.Subtext0().Hex,
|
||||
}
|
||||
|
||||
SubText1 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Subtext1().Hex,
|
||||
Light: light.Subtext1().Hex,
|
||||
}
|
||||
|
||||
LightGrey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface0().Hex,
|
||||
Light: light.Surface0().Hex,
|
||||
}
|
||||
Grey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface1().Hex,
|
||||
Light: light.Surface1().Hex,
|
||||
}
|
||||
|
||||
DarkGrey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface2().Hex,
|
||||
Light: light.Surface2().Hex,
|
||||
}
|
||||
|
||||
Base = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Base().Hex,
|
||||
Light: light.Base().Hex,
|
||||
}
|
||||
|
||||
Crust = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Crust().Hex,
|
||||
Light: light.Crust().Hex,
|
||||
}
|
||||
|
||||
Blue = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Blue().Hex,
|
||||
Light: light.Blue().Hex,
|
||||
}
|
||||
|
||||
Red = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Red().Hex,
|
||||
Light: light.Red().Hex,
|
||||
}
|
||||
|
||||
Green = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Green().Hex,
|
||||
Light: light.Green().Hex,
|
||||
}
|
||||
|
||||
Mauve = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Mauve().Hex,
|
||||
Light: light.Mauve().Hex,
|
||||
}
|
||||
|
||||
Teal = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Teal().Hex,
|
||||
Light: light.Teal().Hex,
|
||||
}
|
||||
|
||||
Rosewater = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Rosewater().Hex,
|
||||
Light: light.Rosewater().Hex,
|
||||
}
|
||||
|
||||
Flamingo = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Flamingo().Hex,
|
||||
Light: light.Flamingo().Hex,
|
||||
}
|
||||
|
||||
Lavender = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Lavender().Hex,
|
||||
Light: light.Lavender().Hex,
|
||||
}
|
||||
|
||||
Peach = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Peach().Hex,
|
||||
Light: light.Peach().Hex,
|
||||
}
|
||||
)
|
99
internal/tui/tui.go
Normal file
99
internal/tui/tui.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package tui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/termai/internal/tui/page"
|
||||
)
|
||||
|
||||
type keyMap struct {
|
||||
Logs key.Binding
|
||||
Back key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
Logs: key.NewBinding(
|
||||
key.WithKeys("L"),
|
||||
key.WithHelp("L", "logs"),
|
||||
),
|
||||
Back: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "back"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("ctrl+c", "q"),
|
||||
key.WithHelp("ctrl+c/q", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
type appModel struct {
|
||||
width, height int
|
||||
currentPage page.PageID
|
||||
previousPage page.PageID
|
||||
pages map[page.PageID]tea.Model
|
||||
loadedPages map[page.PageID]bool
|
||||
}
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
cmd := a.pages[a.currentPage].Init()
|
||||
a.loadedPages[a.currentPage] = true
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
a.width, a.height = msg.Width, msg.Height
|
||||
case tea.KeyMsg:
|
||||
if key.Matches(msg, keys.Quit) {
|
||||
return a, tea.Quit
|
||||
}
|
||||
if key.Matches(msg, keys.Back) {
|
||||
if a.previousPage != "" {
|
||||
return a, a.moveToPage(a.previousPage)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
if key.Matches(msg, keys.Logs) {
|
||||
return a, a.moveToPage(page.LogsPage)
|
||||
}
|
||||
}
|
||||
p, cmd := a.pages[a.currentPage].Update(msg)
|
||||
if p != nil {
|
||||
a.pages[a.currentPage] = p
|
||||
}
|
||||
return a, cmd
|
||||
}
|
||||
|
||||
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
if _, ok := a.loadedPages[pageID]; !ok {
|
||||
cmd = a.pages[pageID].Init()
|
||||
a.loadedPages[pageID] = true
|
||||
}
|
||||
a.previousPage = a.currentPage
|
||||
a.currentPage = pageID
|
||||
if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
|
||||
sizable.SetSize(a.width, a.height)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (a appModel) View() string {
|
||||
return a.pages[a.currentPage].View()
|
||||
}
|
||||
|
||||
func New() tea.Model {
|
||||
return &appModel{
|
||||
currentPage: page.ReplPage,
|
||||
loadedPages: make(map[page.PageID]bool),
|
||||
pages: map[page.PageID]tea.Model{
|
||||
page.LogsPage: page.NewLogsPage(),
|
||||
page.InitPage: page.NewInitPage(),
|
||||
page.ReplPage: page.NewReplPage(),
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue