all repos

smutok @ 47b0d33

yet another tui rss reader (not abandoned, just paused development)

smutok/internal/tui/tui.go(view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package tui

import (
	"context"
	"log/slog"

	"github.com/charmbracelet/bubbles/table"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/glamour"
	"olexsmir.xyz/smutok/internal/store"
)

type Syncer interface {
	Sync(ctx context.Context) error
}

type AppState int

const (
	ArticlesView AppState = iota
	FeedsView
	ReadingView
)

type Model struct {
	ctx context.Context

	state     AppState
	isQutting bool
	showErr   bool
	err       error

	syncer Syncer
	store  *store.Sqlite

	articles []store.Article

	glamur   *glamour.TermRenderer
	table    table.Model    // feeds, articles feed
	viewport viewport.Model // article content reader
}

func NewModel(
	ctx context.Context,
	syncer Syncer,
	store *store.Sqlite,
) *Model {
	tbl := table.New(table.WithFocused(true))
	vp := viewport.New(0, 0)

	return &Model{
		ctx:       ctx,
		syncer:    syncer,
		store:     store,
		glamur:    &glamour.TermRenderer{},
		table:     tbl,
		viewport:  vp,
		state:     ArticlesView,
		isQutting: false,
		showErr:   false,
		err:       nil,
		articles:  nil,
	}
}

func (m *Model) Init() tea.Cmd {
	return tea.Batch(
		tea.SetWindowTitle("smutok"),
		m.fetchArticles(),
	)
}

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd
	switch msg := msg.(type) {
	case errMsg:
		m.err = msg
		m.showErr = true
		return m, nil

	case tea.WindowSizeMsg:
		m.table.SetHeight(msg.Height)
		m.table.SetWidth(msg.Width)
		m.viewport.Height = msg.Height
		m.viewport.Width = msg.Width
		return m, nil

	case fetchedArticles:
		m.articles = msg

		columns := []table.Column{
			// {Title: "read", Width: 4},
			// {Title: "stared", Width: 6},
			{Title: "author", Width: 14},
			{Title: "title", Width: m.table.Width() - 14},
		}

		rows := make([]table.Row, len(msg))
		for i, article := range msg {
			rows[i] = table.Row{article.Author, article.Title}
		}

		m.table.SetColumns(columns)
		m.table.SetRows(rows)

		slog.Debug("got articles")
		return m, nil

	case tea.KeyMsg:
		// page specific keys
		switch m.state {
		case ArticlesView, FeedsView:
			m.table, cmd = m.table.Update(msg)
		case ReadingView:
			m.viewport, cmd = m.viewport.Update(msg)
		}

		// global keys
		switch msg.String() {
		case "q":
			m.isQutting = true
			return m, tea.Quit
		}
	}
	return m, cmd
}

func (m *Model) View() string {
	if m.isQutting {
		return ""
	}

	var out string
	switch m.state {
	case ArticlesView, FeedsView:
		out = m.table.View()
	case ReadingView:
		out = m.viewport.View()
	}
	return out
}