package main import ( "flag" "fmt" "io/ioutil" "log" "os" "strings" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/mattn/go-isatty" "github.com/muesli/reflow/indent" "samhofi.us/x/keybase/v2" "samhofi.us/x/keybase/v2/types/chat1" ) var ( helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render titleStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() b.Right = "├" return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) }() infoStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() b.Left = "┤" return titleStyle.Copy().BorderStyle(b) }() k = keybase.NewKeybase() mainModel *model useHighPerformanceRenderer = false ) func (m model) headerView() string { title := titleStyle.Render("convo-name") line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title))) return lipgloss.JoinHorizontal(lipgloss.Center, title, line) } func (m model) footerView() string { info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) return lipgloss.JoinHorizontal(lipgloss.Center, line, info) } func max(a, b int) int { if a > b { return a } return b } func main() { var ( daemonMode bool showHelp bool opts []tea.ProgramOption ) flag.BoolVar(&daemonMode, "d", false, "run as a daemon") flag.BoolVar(&showHelp, "h", false, "show help") flag.Parse() if showHelp { flag.Usage() os.Exit(0) } if daemonMode || !isatty.IsTerminal(os.Stdout.Fd()) { // If we're in daemon mode don't render the TUI opts = []tea.ProgramOption{tea.WithoutRenderer()} } else { // If we're in TUI mode, discard log output log.SetOutput(ioutil.Discard) } m1 := newModel() mainModel = &m1 chatHandler := handleChat handlers := keybase.Handlers{ ChatHandler: &chatHandler, } go k.Run(handlers, &keybase.RunOptions{}) p := tea.NewProgram(mainModel, opts...) if err := p.Start(); err != nil { fmt.Println("Error starting Bubble Tea program:", err) os.Exit(1) } } func newModel() model { sp := spinner.New() sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("206")) return model{ spinner: sp, } } func (m model) Init() tea.Cmd { log.Println("Starting work...") return tea.Batch( spinner.Tick, ) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "ctrl+c" { m.quitting = true return m, tea.Quit } else { return m, nil } case spinner.TickMsg: m.spinner, cmd = m.spinner.Update(msg) return m, cmd case chat1.MsgSummary: log.Println("chat1.MsgSummary passed to m.Update()") return m, cmd case tea.WindowSizeMsg: headerHeight := lipgloss.Height(m.headerView()) footerHeight := lipgloss.Height(m.footerView()) verticalMarginHeight := headerHeight + footerHeight if !m.ready { // Since this program is using the full size of the viewport we // need to wait until we've received the window dimensions before // we can initialize the viewport. The initial dimensions come in // quickly, though asynchronously, which is why we wait for them // here. m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) m.viewport.YPosition = headerHeight m.viewport.HighPerformanceRendering = useHighPerformanceRenderer m.viewport.SetContent(m.PopulateChat()) m.ready = true // This is only necessary for high performance rendering, which in // most cases you won't need. // Render the viewport one line below the header. m.viewport.YPosition = headerHeight + 1 } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - verticalMarginHeight } if useHighPerformanceRenderer { // Render (or re-render) the whole viewport. Necessary both to // initialize the viewport and when the window is resized. // This is needed for high-performance rendering only. cmds = append(cmds, viewport.Sync(m.viewport)) } // Handle keyboard and mouse events in the viewport m.viewport, cmd = m.viewport.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) default: return m, nil } } func (m model) PopulateChat() string { if m.currentConversation.Name == "" { return "" } else { ret := "" for _, chatmsg := range m.chat { var content string if chatmsg.Content.TypeName == "text" { content = chatmsg.Content.Text.Body } else { content = "Unrendered." } ret += fmt.Sprintf("%+v: %+v", chatmsg.Sender.Username, content) } return ret } } func (m model) View() string { s := "\n" s += m.viewport.View() s += helpStyle("\nCtrl+C to exit\n") if m.quitting { s += "\n" } return indent.String(s, 1) }