1
0
mirror of https://github.com/mrusme/neonmodem.git synced 2024-06-09 06:20:43 +00:00

Implemented first draft structure

This commit is contained in:
マリウス 2022-12-28 22:22:36 -05:00
parent 9036561a8c
commit 770eb43e52
No known key found for this signature in database
GPG Key ID: 272ED814BF63261F
11 changed files with 776 additions and 0 deletions

25
gobbs.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/mrusme/gobbs/system"
"github.com/mrusme/gobbs/ui"
"github.com/mrusme/gobbs/ui/ctx"
)
func main() {
c := ctx.New()
discourse, err := system.New("discourse", nil)
if err != nil {
panic(err)
}
c.AddSystem(&discourse)
tui := tea.NewProgram(ui.NewModel(&c), tea.WithAltScreen())
err = tui.Start()
if err != nil {
panic(err)
}
}

19
models/post/post.go Normal file
View File

@ -0,0 +1,19 @@
package post
type Post struct {
ID string
Subject string
}
func (post Post) FilterValue() string {
return post.Subject
}
func (post Post) Title() string {
return post.Subject
}
func (post Post) Description() string {
return post.ID
}

View File

@ -0,0 +1,6 @@
package adapter
type Capability struct {
ID string
Name string
}

View File

@ -0,0 +1,36 @@
package discourse
import (
"github.com/mrusme/gobbs/models/post"
"github.com/mrusme/gobbs/system/adapter"
)
type System struct {
}
func (sys *System) Load() error {
return nil
}
func (sys *System) ListPosts() ([]post.Post, error) {
return []post.Post{}, nil
}
func (sys *System) GetCapabilities() []adapter.Capability {
var caps []adapter.Capability
caps = append(caps, adapter.Capability{
ID: "posts",
Name: "Posts",
})
caps = append(caps, adapter.Capability{
ID: "groups",
Name: "Groups",
})
caps = append(caps, adapter.Capability{
ID: "search",
Name: "Search",
})
return caps
}

36
system/lemmy/lemmy.go Normal file
View File

@ -0,0 +1,36 @@
package lemmy
import (
"github.com/mrusme/gobbs/models/post"
"github.com/mrusme/gobbs/system/adapter"
)
type System struct {
}
func (sys *System) Load() error {
return nil
}
func (sys *System) ListPosts() ([]post.Post, error) {
return []post.Post{}, nil
}
func (sys *System) GetCapabilities() []adapter.Capability {
var caps []adapter.Capability
caps = append(caps, adapter.Capability{
ID: "posts",
Name: "Posts",
})
caps = append(caps, adapter.Capability{
ID: "groups",
Name: "Groups",
})
caps = append(caps, adapter.Capability{
ID: "search",
Name: "Search",
})
return caps
}

38
system/system.go Normal file
View File

@ -0,0 +1,38 @@
package system
import (
"errors"
"github.com/mrusme/gobbs/models/post"
"github.com/mrusme/gobbs/system/adapter"
"github.com/mrusme/gobbs/system/discourse"
"github.com/mrusme/gobbs/system/lemmy"
)
type System interface {
GetCapabilities() []adapter.Capability
Load() error
ListPosts() ([]post.Post, error)
}
func New(sysType string, sysConfig *map[string]interface{}) (System, error) {
var sys System
switch sysType {
case "discourse":
sys = new(discourse.System)
case "lemmy":
sys = new(lemmy.System)
default:
return nil, errors.New("No such system")
}
err := sys.Load()
if err != nil {
return nil, err
}
return sys, nil
}

23
ui/ctx/ctx.go Normal file
View File

@ -0,0 +1,23 @@
package ctx
import "github.com/mrusme/gobbs/system"
type Ctx struct {
Screen [2]int
Content [2]int
Systems []*system.System
Loading bool
}
func New() Ctx {
return Ctx{
Screen: [2]int{0, 0},
Content: [2]int{0, 0},
Loading: false,
}
}
func (c *Ctx) AddSystem(sys *system.System) error {
c.Systems = append(c.Systems, sys)
return nil
}

151
ui/navigation/navigation.go Normal file
View File

@ -0,0 +1,151 @@
package navigation
import (
"strings"
"github.com/mrusme/gobbs/ui/ctx"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
activeTabBorder = lipgloss.Border{
Top: "─",
Bottom: " ",
Left: "│",
Right: "│",
TopLeft: "╭",
TopRight: "╮",
BottomLeft: "┘",
BottomRight: "└",
}
tabBorder = lipgloss.Border{
Top: "─",
Bottom: "─",
Left: "│",
Right: "│",
TopLeft: "╭",
TopRight: "╮",
BottomLeft: "┴",
BottomRight: "┴",
}
tab = lipgloss.NewStyle().
Border(tabBorder, true).
BorderForeground(highlight).
Padding(0, 1)
activeTab = tab.Copy().Border(activeTabBorder, true)
tabGap = tab.Copy().
BorderTop(false).
BorderLeft(false).
BorderRight(false)
)
var Navigation = []string{}
type Model struct {
CurrentId int
ctx *ctx.Ctx
spinner spinner.Model
}
func NewModel(c *ctx.Ctx) Model {
m := Model{
CurrentId: 0,
ctx: c,
}
m.spinner = spinner.New()
m.spinner.Spinner = spinner.Dot
m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return m
}
func (m Model) Init() tea.Cmd {
return m.spinner.Tick
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var cmds []tea.Cmd
if m.ctx.Loading == true {
cmds = append(cmds, m.spinner.Tick)
}
switch msg := msg.(type) {
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
var items []string
for i, nav := range Navigation {
if m.CurrentId == i {
items = append(items, activeTab.Render(nav))
} else {
items = append(items, tab.Render(nav))
}
}
row := lipgloss.JoinHorizontal(
lipgloss.Top,
items...,
)
if m.ctx.Loading == false {
gap := tabGap.Render(strings.Repeat(" ", max(0, m.ctx.Screen[0]-lipgloss.Width(row)-2)))
row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
} else {
gap := tabGap.Render(strings.Repeat(" ", max(0, m.ctx.Screen[0]-lipgloss.Width(row)-4)))
row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap, " ", m.spinner.View())
}
return lipgloss.JoinHorizontal(lipgloss.Top, row, "\n\n")
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (m *Model) NthTab(nth int) {
if nth > len(Navigation) {
nth = len(Navigation)
} else if nth < 1 {
nth = 1
}
m.CurrentId = nth - 1
}
func (m *Model) PrevTab() {
m.CurrentId--
if m.CurrentId < 0 {
m.CurrentId = len(Navigation) - 1
}
}
func (m *Model) NextTab() {
m.CurrentId++
if m.CurrentId >= len(Navigation) {
m.CurrentId = 0
}
}

237
ui/ui.go Normal file
View File

@ -0,0 +1,237 @@
package ui
import (
// "fmt"
"strings"
"github.com/mrusme/gobbs/ui/ctx"
"github.com/mrusme/gobbs/ui/navigation"
"github.com/mrusme/gobbs/ui/views"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)
type KeyMap struct {
FirstTab key.Binding
SecondTab key.Binding
ThirdTab key.Binding
FourthTab key.Binding
FifthTab key.Binding
SixthTab key.Binding
SeventhTab key.Binding
EightTab key.Binding
NinthTab key.Binding
TenthTab key.Binding
EleventhTab key.Binding
TwelfthTab key.Binding
ThirteenthTab key.Binding
PrevTab key.Binding
NextTab key.Binding
Up key.Binding
Down key.Binding
Quit key.Binding
}
var DefaultKeyMap = KeyMap{
FirstTab: key.NewBinding(
key.WithKeys("f1"),
key.WithHelp("f1", "first tab"),
),
SecondTab: key.NewBinding(
key.WithKeys("f2"),
key.WithHelp("f2", "second tab"),
),
ThirdTab: key.NewBinding(
key.WithKeys("f3"),
key.WithHelp("f3", "third tab"),
),
FourthTab: key.NewBinding(
key.WithKeys("f4"),
key.WithHelp("f4", "fourth tab"),
),
FifthTab: key.NewBinding(
key.WithKeys("f5"),
key.WithHelp("f5", "fifth tab"),
),
SixthTab: key.NewBinding(
key.WithKeys("f6"),
key.WithHelp("f6", "sixth tab"),
),
SeventhTab: key.NewBinding(
key.WithKeys("f7"),
key.WithHelp("f7", "seventh tab"),
),
EightTab: key.NewBinding(
key.WithKeys("f8"),
key.WithHelp("f8", "eight tab"),
),
NinthTab: key.NewBinding(
key.WithKeys("f9"),
key.WithHelp("f9", "ninth tab"),
),
TenthTab: key.NewBinding(
key.WithKeys("f10"),
key.WithHelp("f10", "tenth tab"),
),
EleventhTab: key.NewBinding(
key.WithKeys("f11"),
key.WithHelp("f11", "eleventh tab"),
),
TwelfthTab: key.NewBinding(
key.WithKeys("f12"),
key.WithHelp("f12", "twelfth tab"),
),
ThirteenthTab: key.NewBinding(
key.WithKeys("f13"),
key.WithHelp("f13", "thirteenth tab"),
),
PrevTab: key.NewBinding(
key.WithKeys("ctrl+p"),
key.WithHelp("ctrl+p", "previous tab"),
),
NextTab: key.NewBinding(
key.WithKeys("ctrl+n"),
key.WithHelp("ctrl+n", "next tab"),
),
Up: key.NewBinding(
key.WithKeys("k", "up"),
key.WithHelp("↑/k", "move up"),
),
Down: key.NewBinding(
key.WithKeys("j", "down"),
key.WithHelp("↓/j", "move down"),
),
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+q"),
key.WithHelp("q/Q", "quit"),
),
}
type Model struct {
keymap KeyMap
nav navigation.Model
views []views.View
ctx *ctx.Ctx
}
func NewModel(c *ctx.Ctx) Model {
m := Model{
keymap: DefaultKeyMap,
ctx: c,
}
m.nav = navigation.NewModel(m.ctx)
return m
}
func (m Model) Init() tea.Cmd {
return tea.Batch(tea.EnterAltScreen)
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keymap.Quit):
return m, tea.Quit
case key.Matches(msg, m.keymap.FirstTab):
m.nav.NthTab(1)
return m, nil
case key.Matches(msg, m.keymap.SecondTab):
m.nav.NthTab(2)
return m, nil
case key.Matches(msg, m.keymap.ThirdTab):
m.nav.NthTab(3)
return m, nil
case key.Matches(msg, m.keymap.FourthTab):
m.nav.NthTab(4)
return m, nil
case key.Matches(msg, m.keymap.FifthTab):
m.nav.NthTab(5)
return m, nil
case key.Matches(msg, m.keymap.SixthTab):
m.nav.NthTab(6)
return m, nil
case key.Matches(msg, m.keymap.SeventhTab):
m.nav.NthTab(7)
return m, nil
case key.Matches(msg, m.keymap.EightTab):
m.nav.NthTab(8)
return m, nil
case key.Matches(msg, m.keymap.NinthTab):
m.nav.NthTab(9)
return m, nil
case key.Matches(msg, m.keymap.TenthTab):
m.nav.NthTab(10)
return m, nil
case key.Matches(msg, m.keymap.EleventhTab):
m.nav.NthTab(11)
return m, nil
case key.Matches(msg, m.keymap.TwelfthTab):
m.nav.NthTab(12)
return m, nil
case key.Matches(msg, m.keymap.ThirteenthTab):
m.nav.NthTab(13)
return m, nil
case key.Matches(msg, m.keymap.PrevTab):
m.nav.PrevTab()
return m, nil
case key.Matches(msg, m.keymap.NextTab):
m.nav.NextTab()
return m, nil
}
case tea.WindowSizeMsg:
m.setSizes(msg.Width, msg.Height)
for i := range m.views {
v, cmd := m.views[i].Update(msg)
m.views[i] = v
cmds = append(cmds, cmd)
}
}
v, cmd := m.views[m.nav.CurrentId].Update(msg)
m.views[m.nav.CurrentId] = v
cmds = append(cmds, cmd)
nav, cmd := m.nav.Update(msg)
m.nav = nav
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
s := strings.Builder{}
s.WriteString(m.nav.View() + "\n\n")
s.WriteString(m.views[m.nav.CurrentId].View())
return s.String()
}
func (m Model) setSizes(winWidth int, winHeight int) {
(*m.ctx).Screen[0] = winWidth
(*m.ctx).Screen[1] = winHeight
m.ctx.Content[0] = m.ctx.Screen[0]
m.ctx.Content[1] = m.ctx.Screen[1] - 5
}

195
ui/views/posts/posts.go Normal file
View File

@ -0,0 +1,195 @@
package posts
import (
"fmt"
"math"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mrusme/gobbs/models/post"
"github.com/mrusme/gobbs/ui/ctx"
)
var (
listStyle = lipgloss.NewStyle().
Margin(0, 0, 0, 0).
Padding(1, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#874BFD")).
BorderTop(true).
BorderLeft(true).
BorderRight(true).
BorderBottom(true)
viewportStyle = lipgloss.NewStyle().
Margin(0, 0, 0, 0).
Padding(1, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#874BFD")).
BorderTop(true).
BorderLeft(true).
BorderRight(true).
BorderBottom(true)
)
type KeyMap struct {
Refresh key.Binding
Select key.Binding
SwitchFocus key.Binding
}
var DefaultKeyMap = KeyMap{
Refresh: key.NewBinding(
key.WithKeys("r", "R"),
key.WithHelp("r/R", "refresh"),
),
Select: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
SwitchFocus: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "switch focus"),
),
}
type Model struct {
keymap KeyMap
list list.Model
items []list.Item
viewport viewport.Model
ctx *ctx.Ctx
focused int
focusables [2]tea.Model
}
func (m Model) Init() tea.Cmd {
return nil
}
func NewModel(c *ctx.Ctx) Model {
m := Model{
keymap: DefaultKeyMap,
focused: 0,
}
// m.focusables = append(m.focusables, m.list)
// m.focusables = append(m.focusables, m.viewport)
m.list = list.New(m.items, list.NewDefaultDelegate(), 0, 0)
m.list.Title = "Posts"
m.ctx = c
return m
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keymap.Refresh):
m.ctx.Loading = true
cmds = append(cmds, m.refresh())
case key.Matches(msg, m.keymap.SwitchFocus):
m.focused++
if m.focused >= len(m.focusables) {
m.focused = 0
}
// return m, nil
case key.Matches(msg, m.keymap.Select):
i, ok := m.list.SelectedItem().(post.Post)
if ok {
m.viewport.SetContent(m.renderViewport(&i))
return m, nil
}
}
case tea.WindowSizeMsg:
listWidth := int(math.Floor(float64(m.ctx.Content[0]) / 4.0))
listHeight := m.ctx.Content[1] - 1
viewportWidth := m.ctx.Content[0] - listWidth - 4
viewportHeight := m.ctx.Content[1] - 1
listStyle.Width(listWidth)
listStyle.Height(listHeight)
m.list.SetSize(
listWidth-2,
listHeight-2,
)
viewportStyle.Width(viewportWidth)
viewportStyle.Height(viewportHeight)
m.viewport = viewport.New(viewportWidth-4, viewportHeight-4)
m.viewport.Width = viewportWidth - 4
m.viewport.Height = viewportHeight - 4
// cmds = append(cmds, viewport.Sync(m.viewport))
case []list.Item:
m.items = msg
m.list.SetItems(m.items)
m.ctx.Loading = false
}
var cmd tea.Cmd
if m.focused == 0 {
listStyle.BorderForeground(lipgloss.Color("#FFFFFF"))
viewportStyle.BorderForeground(lipgloss.Color("#874BFD"))
m.list, cmd = m.list.Update(msg)
cmds = append(cmds, cmd)
} else if m.focused == 1 {
listStyle.BorderForeground(lipgloss.Color("#874BFD"))
viewportStyle.BorderForeground(lipgloss.Color("#FFFFFF"))
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
var view string
view = lipgloss.JoinHorizontal(
lipgloss.Top,
listStyle.Render(m.list.View()),
viewportStyle.Render(m.viewport.View()),
)
return view
}
func (m *Model) refresh() tea.Cmd {
return func() tea.Msg {
var items []list.Item
posts, err := (*m.ctx.Systems[0]).ListPosts()
if err != nil {
fmt.Printf("%s", err) // TODO: Implement error message
}
for _, post := range posts {
items = append(items, post)
}
return items
}
}
func (m *Model) renderViewport(post *post.Post) string {
var vp string = ""
vp = fmt.Sprintf(
"%s\n",
post.Subject,
)
return vp
}

10
ui/views/views.go Normal file
View File

@ -0,0 +1,10 @@
package views
import (
tea "github.com/charmbracelet/bubbletea"
)
type View interface {
View() string
Update(msg tea.Msg) (tea.Model, tea.Cmd)
}