diff --git a/main.go b/main.go new file mode 100644 index 0000000..bf4f630 --- /dev/null +++ b/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "os" + "log/slog" + + "sourcery.dny.nu/pana" + "github.com/alexflint/go-arg" + "github.com/cato-001/twink/tui" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + var args struct{ + Auth *struct{ + } `arg:"subcommand:auth"` + + Tui *tui.Args `arg:"subcommand:tui"` + } + + parser, err := arg.NewParser(arg.Config{}, &args) + if err != nil { + logger.Error("could not create cli argument parser", slog.Any("error", err)) + return + } + + parser.MustParse(os.Args[1:]) + + processor := pana.NewProcessor(logger) + _ = processor + + if args.Tui != nil { + err := tui.Start(*args.Tui) + if err != nil { + logger.Error("app exited unexpectedly", slog.Any("error", err)) + } + return + } +} diff --git a/tui/app.go b/tui/app.go new file mode 100644 index 0000000..74f7f2b --- /dev/null +++ b/tui/app.go @@ -0,0 +1,47 @@ +package tui + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" +) + +type ( + AppModel struct{ + Size Size + Header HeaderModel + Screen tea.Model + } +) + +func NewApp() AppModel { + return AppModel{ + Header: NewHeader(), + Screen: NewHomeScreen(), + } +} + +func (m AppModel) Init() tea.Cmd { + return nil +} + +func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd, headerCmd, screenCmd tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.Size = NewSizeFromWindow(msg) + } + + m.Header, headerCmd = m.Header.Update(msg) + m.Screen, screenCmd = m.Screen.Update(msg) + + return m, tea.Batch(cmd, headerCmd, screenCmd) +} + +func (m AppModel) View() string { + return strings.Join([]string{ + m.Header.View(), + m.Screen.View(), + }, "\n") +} diff --git a/tui/args.go b/tui/args.go new file mode 100644 index 0000000..845c8b1 --- /dev/null +++ b/tui/args.go @@ -0,0 +1,7 @@ +package tui + +type ( + Args struct{ + } +) + diff --git a/tui/header.go b/tui/header.go new file mode 100644 index 0000000..e9a4973 --- /dev/null +++ b/tui/header.go @@ -0,0 +1,114 @@ +package tui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type ( + HeaderModel struct{ + Tabs []HeaderTabModel + Width int + } + + HeaderTabModel struct{ + Current bool + Name string + Unread int + } +) + +func NewHeader() HeaderModel { + return HeaderModel{ + Tabs: []HeaderTabModel { + NewHeaderTab("Home", 10, true), + NewHeaderTab("Notifications", 3, false), + NewHeaderTab("Lists", 5, false), + NewHeaderTab("Private", 0, false), + }, + } +} + +func NewHeaderTab(name string, unread int, current bool) HeaderTabModel { + return HeaderTabModel{ Name: name, Unread: unread, Current: current } +} + +var ( + activeTabBorder = lipgloss.Border{ + Top: "─", + Bottom: " ", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "┘", + BottomRight: "└", + } + + tabBorder = lipgloss.Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "┴", + BottomRight: "┴", + } + + headerTabStyle = lipgloss.NewStyle(). + Border(tabBorder, true) + + activeHeaderTabStyle = headerTabStyle. + Border(activeTabBorder, true) +) + +func (m HeaderModel) Init() tea.Cmd { + return nil +} + +func (m HeaderModel) Update(msg tea.Msg) (HeaderModel, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case Size: + m.Width = msg.Width + } + + return m, cmd +} + +func (m HeaderModel) View() string { + header := lipgloss.NewStyle(). + Padding(1, 0). + Align(lipgloss.Center). + Width(m.Width) + + renderedTabs := make([]string, len(m.Tabs)) + for i, tab := range m.Tabs { + renderedTabs[i] = tab.View() + } + + return header.Render(renderedTabs...) +} + +func (m HeaderTabModel) Init() tea.Cmd { + return nil +} + +func (m HeaderTabModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + return m, cmd +} + +func (m HeaderTabModel) View() string { + content := lipgloss.JoinHorizontal(lipgloss.Center, m.Name, fmt.Sprintf("(%d)", m.Unread)) + + if m.Current { + return lipgloss.NewStyle().Underline(true).Render(content) + } + + return content +} diff --git a/tui/home.go b/tui/home.go new file mode 100644 index 0000000..41a8e78 --- /dev/null +++ b/tui/home.go @@ -0,0 +1,53 @@ +package tui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type ( + HomeScreenModel struct{ + Size Size + } +) + +func NewHomeScreen() HomeScreenModel { + return HomeScreenModel{} +} + +func (m HomeScreenModel) Init() tea.Cmd { + return nil +} + +func (m HomeScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl-c": + cmd = tea.Quit + } + case Size: + m.Size = msg + } + + return m, cmd +} + +func (m HomeScreenModel) View() string { + header := lipgloss.NewStyle(). + Align(lipgloss.Center). + Width(m.Size.Width). + SetString(fmt.Sprintf("Home\n%s", "@example@mastodon.social")) + + window := lipgloss.NewStyle(). + Width(m.Size.Width). + Height(m.Size.Height) + + return window.Render( + header.Render(), + ) +} diff --git a/tui/size.go b/tui/size.go new file mode 100644 index 0000000..f66bdc9 --- /dev/null +++ b/tui/size.go @@ -0,0 +1,15 @@ +package tui + +import tea "github.com/charmbracelet/bubbletea" + +type Size struct{ + Width, Height int +} + +func NewSize(width, height int) Size { + return Size { Width: width, Height: height } +} + +func NewSizeFromWindow(size tea.WindowSizeMsg) Size { + return Size { Width: size.Width, Height: size.Height } +} diff --git a/tui/start.go b/tui/start.go new file mode 100644 index 0000000..4396391 --- /dev/null +++ b/tui/start.go @@ -0,0 +1,10 @@ +package tui + +import tea "github.com/charmbracelet/bubbletea" + +func Start(args Args) error { + app := NewApp() + program := tea.NewProgram(app, tea.WithAltScreen()) + _, err := program.Run() + return err +}