x-1/tui.go

178 lines
4.2 KiB
Go

package main
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
func runTUI(state *BrowserState, args []string) {
model := NewMainModel(state)
state.Printer = (*TUIPrinter)(model)
if len(args) > 0 {
if err := Go(state, args[0]); err != nil {
state.Printer.PrintError(err.Error())
}
}
p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
state.Printer.PrintError(err.Error())
os.Exit(1)
}
}
var (
hdrStyle = lipgloss.NewStyle().Background(lipgloss.Color("56")).Bold(true)
ftrStyle = lipgloss.NewStyle().Background(lipgloss.Color("56"))
errStyle = lipgloss.NewStyle().Background(lipgloss.Color("196")).Bold(true)
)
type MainModel struct {
State *BrowserState
Viewport viewport.Model
Prompt *textinput.Model
ErrorMsg string
}
func NewMainModel(state *BrowserState) *MainModel {
return &MainModel{
State: state,
Viewport: viewport.Model{
HighPerformanceRendering: true,
YPosition: 2,
},
}
}
func (model *MainModel) Init() tea.Cmd {
return nil
}
func (model *MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if model.Prompt != nil {
return model.updatePrompt(msg)
}
m := model
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
model.ErrorMsg = ""
switch msg.String() {
case "ctrl+c", "q":
cmds = append(cmds, tea.Quit)
case "ctrl+l":
cmds = append(cmds, viewport.Sync(model.Viewport))
case "g":
lines := model.Viewport.GotoTop()
cmds = append(cmds, viewport.ViewUp(model.Viewport, lines))
case "G":
lines := model.Viewport.GotoBottom()
cmds = append(cmds, viewport.ViewDown(model.Viewport, lines))
case ":":
p := textinput.New()
model.Prompt = &p
cmds = append(cmds, p.Focus())
}
case tea.WindowSizeMsg:
model.Viewport.Width = msg.Width
model.Viewport.Height = msg.Height - 2
hdrStyle = hdrStyle.Width(msg.Width)
cmds = append(cmds, viewport.Sync(model.Viewport))
}
var cmd tea.Cmd
vp, cmd := model.Viewport.Update(msg)
model.Viewport = vp
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (model *MainModel) updatePrompt(msg tea.Msg) (tea.Model, tea.Cmd) {
if keymsg, ok := msg.(tea.KeyMsg); ok {
model.ErrorMsg = ""
switch keymsg.String() {
case "enter":
cmd, err := ParseCommand(model.Prompt.Value())
model.Prompt = nil
if err != nil {
model.State.Printer.PrintError(err.Error())
return model, nil
}
if err := RunCommand(cmd, model.State); err != nil {
model.State.Printer.PrintError(err.Error())
return model, nil
}
return model, viewport.Sync(model.Viewport)
}
}
p, cmd := model.Prompt.Update(msg)
model.Prompt = &p
return model, cmd
}
func (model *MainModel) View() string {
return model.viewHeader() + "\n" + model.Viewport.View() + "\n" + model.viewFooter()
}
func (model *MainModel) viewHeader() string {
hdrLine := " Welcome to X-1"
if model.State.Url != nil {
hdrLine = " " + model.State.Url.String()
}
return hdrStyle.Render(hdrLine)
}
func (model *MainModel) viewFooter() string {
if model.ErrorMsg != "" {
return errStyle.Render(model.ErrorMsg)
}
var footerLine string
if model.Prompt != nil {
footerLine = ftrStyle.Width(hdrStyle.GetWidth()).Render(model.Prompt.View())
} else {
footerLine = ftrStyle.Render(" ") + ftrStyle.Copy().Italic(true).Render(model.State.DocType)
pct := ftrStyle.Render(fmt.Sprintf("%3.f%% ", model.Viewport.ScrollPercent()*100))
footerLine += ftrStyle.Render(strings.Repeat(" ", max(0, model.Viewport.Width-lipgloss.Width(footerLine)-lipgloss.Width(pct))))
footerLine += pct
}
return footerLine
}
func max(a, b int) int {
if a < b {
return b
}
return a
}
type TUIPrinter MainModel
func (p *TUIPrinter) PrintModal(state *BrowserState, contents []byte) error {
(*MainModel)(p).Viewport.SetContent(strings.TrimSuffix(string(contents), "\n"))
return nil
}
func (p *TUIPrinter) PrintPage(state *BrowserState, body string) error {
(*MainModel)(p).Viewport.SetContent(strings.TrimSuffix(body, "\n"))
return nil
}
func (p *TUIPrinter) PrintError(msg string) error {
(*MainModel)(p).ErrorMsg = msg
return nil
}