You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
4.9 KiB
197 lines
4.9 KiB
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) |
|
}
|
|
|