[mirror] yet another tui rss reader github.com/olexsmir/smutok

feat(tui): show list of articles

olexsmir.xyz e1ab4d0a 47b0d336

verified
Changed files
+66 -26
internal
+18 -4
internal/store/sqlite_articles.go
··· 56 return err 57 } 58 59 type Article struct { 60 ID string 61 Title string ··· 69 PublishedAt int64 70 } 71 72 - func (s *Sqlite) GetArticles(ctx context.Context) ([]Article, error) { 73 - query := `--sql 74 select a.id, a.title, a.href, a.content, a.author, s.is_read, s.is_starred, a.feed_id, f.title feed_name, a.published_at 75 from articles a 76 join article_statuses s on a.id = s.article_id 77 join feeds f on f.id = a.feed_id 78 - where s.is_read = false 79 - order by a.published_at desc` 80 81 rows, err := s.db.QueryContext(ctx, query) 82 if err != nil {
··· 56 return err 57 } 58 59 + type ArticleKind int 60 + 61 + const ( 62 + ArticleStarred ArticleKind = iota 63 + ArticleUnread 64 + ArticleAll 65 + ) 66 + 67 type Article struct { 68 ID string 69 Title string ··· 77 PublishedAt int64 78 } 79 80 + var getArticlesWhereClause = map[ArticleKind]string{ 81 + ArticleAll: "", 82 + ArticleStarred: "where s.is_starred = true", 83 + ArticleUnread: "where s.is_read = false", 84 + } 85 + 86 + func (s *Sqlite) GetArticles(ctx context.Context, kind ArticleKind) ([]Article, error) { 87 + query := fmt.Sprintf(`--sql 88 select a.id, a.title, a.href, a.content, a.author, s.is_read, s.is_starred, a.feed_id, f.title feed_name, a.published_at 89 from articles a 90 join article_statuses s on a.id = s.article_id 91 join feeds f on f.id = a.feed_id 92 + %s 93 + order by a.published_at desc`, getArticlesWhereClause[kind]) 94 95 rows, err := s.db.QueryContext(ctx, query) 96 if err != nil {
+45 -2
internal/tui/fetcher.go
··· 1 package tui 2 3 import ( 4 tea "github.com/charmbracelet/bubbletea" 5 "olexsmir.xyz/smutok/internal/store" 6 ) 7 8 type fetchedArticles []store.Article 9 10 - func (m *Model) fetchArticles() tea.Cmd { 11 return func() tea.Msg { 12 - articles, err := m.store.GetArticles(m.ctx) 13 if err != nil { 14 return sendErr(err) 15 } 16 return fetchedArticles(articles) 17 } 18 }
··· 1 package tui 2 3 import ( 4 + "time" 5 + 6 + "github.com/charmbracelet/bubbles/table" 7 tea "github.com/charmbracelet/bubbletea" 8 "olexsmir.xyz/smutok/internal/store" 9 ) 10 11 type fetchedArticles []store.Article 12 13 + func (m *Model) fetchArticles(kind store.ArticleKind) tea.Cmd { 14 return func() tea.Msg { 15 + articles, err := m.store.GetArticles(m.ctx, kind) 16 if err != nil { 17 return sendErr(err) 18 } 19 return fetchedArticles(articles) 20 } 21 } 22 + 23 + func (m *Model) setupTableWithArticles() { 24 + // clean up previous state 25 + m.table.SetRows([]table.Row{}) 26 + 27 + columns := []table.Column{ 28 + {Title: "date", Width: 10}, 29 + {Title: "status", Width: 6}, 30 + {Title: "author", Width: 14}, 31 + {Title: "title", Width: m.table.Width() - 30}, 32 + } 33 + 34 + rows := make([]table.Row, len(m.articles)) 35 + for i, a := range m.articles { 36 + rows[i] = table.Row{ 37 + m.toArticleDate(a.PublishedAt), 38 + m.toArticleStatus(a.IsRead, a.IsStarred), 39 + a.Author, 40 + a.Title, 41 + } 42 + } 43 + 44 + m.table.SetColumns(columns) 45 + m.table.SetRows(rows) 46 + } 47 + 48 + func (m *Model) toArticleStatus(isRead, isStarred bool) string { 49 + var out string 50 + if isRead { 51 + out += "✓ " 52 + } 53 + if isStarred { 54 + out += "★ " 55 + } 56 + return out 57 + } 58 + 59 + func (m *Model) toArticleDate(publishedAt int64) string { 60 + return time.Unix(publishedAt, 0).Format("2006/01/02") 61 + }
+3 -20
internal/tui/tui.go
··· 2 3 import ( 4 "context" 5 - "log/slog" 6 7 "github.com/charmbracelet/bubbles/table" 8 "github.com/charmbracelet/bubbles/viewport" ··· 67 func (m *Model) Init() tea.Cmd { 68 return tea.Batch( 69 tea.SetWindowTitle("smutok"), 70 - m.fetchArticles(), 71 ) 72 } 73 ··· 81 82 case tea.WindowSizeMsg: 83 m.table.SetHeight(msg.Height) 84 - m.table.SetWidth(msg.Width) 85 m.viewport.Height = msg.Height 86 m.viewport.Width = msg.Width 87 return m, nil 88 89 case fetchedArticles: 90 m.articles = msg 91 - 92 - columns := []table.Column{ 93 - // {Title: "read", Width: 4}, 94 - // {Title: "stared", Width: 6}, 95 - {Title: "author", Width: 14}, 96 - {Title: "title", Width: m.table.Width() - 14}, 97 - } 98 - 99 - rows := make([]table.Row, len(msg)) 100 - for i, article := range msg { 101 - rows[i] = table.Row{article.Author, article.Title} 102 - } 103 - 104 - m.table.SetColumns(columns) 105 - m.table.SetRows(rows) 106 - 107 - slog.Debug("got articles") 108 return m, nil 109 110 case tea.KeyMsg:
··· 2 3 import ( 4 "context" 5 6 "github.com/charmbracelet/bubbles/table" 7 "github.com/charmbracelet/bubbles/viewport" ··· 66 func (m *Model) Init() tea.Cmd { 67 return tea.Batch( 68 tea.SetWindowTitle("smutok"), 69 + m.fetchArticles(store.ArticleAll), 70 ) 71 } 72 ··· 80 81 case tea.WindowSizeMsg: 82 m.table.SetHeight(msg.Height) 83 + m.table.SetWidth(msg.Width - 2) 84 m.viewport.Height = msg.Height 85 m.viewport.Width = msg.Width 86 return m, nil 87 88 case fetchedArticles: 89 m.articles = msg 90 + m.setupTableWithArticles() 91 return m, nil 92 93 case tea.KeyMsg: