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 }