all repos

smutok @ 64bec18

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
package tui

import (
	"context"

	"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
	isSyncing bool
	showErr   bool
	err       error

	syncer Syncer
	store  *store.Sqlite

	articlesKind store.ArticleKind
	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,
	sqlite *store.Sqlite,
) *Model {
	tbl := table.New(table.WithFocused(true))
	vp := viewport.New(0, 0)

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

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

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 - 2)
		m.viewport.Height = msg.Height
		m.viewport.Width = msg.Width
		return m, nil

	case fetchedArticles:
		m.articles = msg
		m.setupTableWithArticles()
		return m, nil

	case triggerSync:
		m.isSyncing = true
		return m, m.triggerSync()

	case finishedSync:
		m.isSyncing = false
		return m, m.fetchArticles(m.articlesKind)

	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
		case "s":
			return m, m.triggerSync()
		}
	}
	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
}