diff --git a/.github/assets/demo.gif b/.github/assets/demo.gif index c4191cd..2866df0 100644 Binary files a/.github/assets/demo.gif and b/.github/assets/demo.gif differ diff --git a/.github/assets/demo.tape b/.github/assets/demo.tape index e27cde5..2d51754 100644 --- a/.github/assets/demo.tape +++ b/.github/assets/demo.tape @@ -2,7 +2,7 @@ Output demo.gif Require echo -Set Shell "bash" +Set Shell "fish" Set Width 1920 Set Height 1080 Set CursorBlink false @@ -17,11 +17,15 @@ Enter Sleep 500ms Type "?" Sleep 500ms -Down +Down 1 +Sleep 500ms Enter Sleep 500ms Down 4 +Type "x" +Sleep 1s Enter Sleep 1.5s -Down 10 -Sleep 2s +Down 15 +Type "q" +Sleep 5s diff --git a/cmd/keys.go b/cmd/keys.go index ded12d8..6cca200 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -19,6 +19,8 @@ type keyMap struct { Refresh key.Binding RefreshAll key.Binding Search key.Binding + ToggleRead key.Binding + ReadAll key.Binding } func (k keyMap) ShortHelp() []key.Binding { @@ -31,6 +33,7 @@ func (k keyMap) FullHelp() [][]key.Binding { {k.Help, k.Quit}, {k.Refresh, k.RefreshAll}, {k.Open, k.Search}, + {k.ToggleRead, k.ReadAll}, } } @@ -67,6 +70,14 @@ var keys = keyMap{ key.WithKeys("/"), key.WithHelp("/", "search"), ), + ToggleRead: key.NewBinding( + key.WithKeys("x"), + key.WithHelp("x", "toggle read"), + ), + ReadAll: key.NewBinding( + key.WithKeys("X"), + key.WithHelp("X", "toggle read"), + ), } func (m model) handleKeys(msg tea.KeyMsg) (model, tea.Cmd) { @@ -78,17 +89,20 @@ func (m model) handleKeys(msg tea.KeyMsg) (model, tea.Cmd) { case key.Matches(msg, m.keys.Quit): switch m.context { case "reader": - m.context = "content" - m.viewport.SetYOffset(0) + m = m.loadContent(m.feed.ID) m.table.SetCursor(m.post.ID) case "content": m = m.loadHome() m.table.SetCursor(m.feed.ID) case "search": - m = m.loadContent() + m = m.loadContent(m.table.Cursor()) m.table.Focus() m.filter.Blur() default: + err := m.feeds.WriteTracking() + if err != nil { + log.Fatalf("Could not write tracking data: %s", err) + } return m, tea.Quit } @@ -107,7 +121,7 @@ func (m model) handleKeys(msg tea.KeyMsg) (model, tea.Cmd) { case "content": feed := &m.feed feed.Posts = lib.GetPosts(feed.URL) - m = m.loadContent() + m = m.loadContent(m.feed.ID) } case key.Matches(msg, m.keys.RefreshAll): @@ -134,7 +148,7 @@ func (m model) handleKeys(msg tea.KeyMsg) (model, tea.Cmd) { } default: - m = m.loadContent() + m = m.loadContent(m.table.Cursor()) m.table.SetCursor(0) } @@ -142,6 +156,29 @@ func (m model) handleKeys(msg tea.KeyMsg) (model, tea.Cmd) { if m.context != "search" { m = m.loadSearch() } + + case key.Matches(msg, m.keys.ToggleRead): + switch m.context { + case "reader": + lib.ToggleRead(m.feeds, m.feed.ID, m.post.ID) + m = m.loadContent(m.feed.ID) + case "content": + lib.ToggleRead(m.feeds, m.feed.ID, m.table.Cursor()) + m = m.loadContent(m.feed.ID) + } + + case key.Matches(msg, m.keys.ReadAll): + switch m.context { + case "reader": + // if we are in the reader view, fall back to the normal mark all as read + lib.ToggleRead(m.feeds, m.feed.ID, m.post.ID) + case "content": + lib.ReadAll(m.feeds, m.feed.ID) + m = m.loadContent(m.table.Cursor()) + case "home": + lib.ReadAll(m.feeds, m.table.Cursor()) + m = m.loadHome() + } } return m, nil diff --git a/cmd/load.go b/cmd/load.go index 0675901..c43a406 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -1,6 +1,7 @@ package cmd import ( + "strconv" "strings" "github.com/charmbracelet/bubbles/table" @@ -11,12 +12,14 @@ import ( // load the home view, this conists of the list of feeds func (m model) loadHome() model { columns := []table.Column{ - {Title: "Title", Width: m.table.Width()}, + {Title: "Unread", Width: 7}, + {Title: "Title", Width: m.table.Width() - 7}, } rows := []table.Row{} - for _, Feeds := range m.feeds { - rows = append(rows, table.Row{Feeds.Title}) + for _, Feed := range m.feeds { + totalUnread := strconv.Itoa(Feed.GetTotalUnreads()) + rows = append(rows, table.Row{totalUnread, Feed.Title}) } m = m.loadNewTable(columns, rows) @@ -25,19 +28,23 @@ func (m model) loadHome() model { return m } -func (m model) loadContent() model { - id := m.table.Cursor() +func (m model) loadContent(id int) model { feed := m.feeds[id] feed.ID = id columns := []table.Column{ - {Title: "Date", Width: 13}, - {Title: "Title", Width: m.table.Width() - 15}, + {Title: "Date", Width: 11}, + {Title: "Unread", Width: 7}, + {Title: "Title", Width: m.table.Width() - 27}, } rows := []table.Row{} for _, post := range feed.Posts { - rows = append(rows, table.Row{post.Date, post.Title}) + unread := "x" + if !post.Read { + unread = "✓" + } + rows = append(rows, table.Row{post.Date, unread, post.Title}) } m = m.loadNewTable(columns, rows) diff --git a/cmd/main.go b/cmd/main.go index ccafadc..217e6eb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,6 +1,8 @@ package cmd import ( + "log" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -40,7 +42,12 @@ func (m model) handleWindowSize(msg tea.WindowSizeMsg) model { if !m.ready { m.feeds = lib.GetAllContent(true) m.viewport = viewport.New(width, height) + err := error(nil) + m.feeds, err = m.feeds.ReadTracking() m = m.loadHome() + if err != nil { + log.Fatal(err) + } m.ready = true } else { m.viewport.Width = width @@ -82,6 +89,10 @@ func (m model) updateViewport(msg tea.Msg) (model, tea.Cmd) { m.viewport.SetContent(view) } + if m.context == "reader" && m.viewport.ScrollPercent() >= 0.8 { + lib.MarkRead(m.feeds, m.feed.ID, m.post.ID) + } + m.viewport, cmd = m.viewport.Update(msg) cmds = append(cmds, cmd) diff --git a/lib/feeds.go b/lib/feeds.go index 7336149..f67eae2 100644 --- a/lib/feeds.go +++ b/lib/feeds.go @@ -3,18 +3,20 @@ package lib import "sort" type Post struct { - Title string - Content string - Link string - Date string - ID int + UUID string `json:"uuid"` + Title string `json:"-"` + Content string `json:"-"` + Link string `json:"-"` + Date string `json:"-"` + ID int `json:"-"` + Read bool `json:"read"` } type Feed struct { - Title string - URL string - Posts []Post - ID int + Title string `json:"-"` + URL string `json:"URL"` + Posts []Post `json:"posts"` + ID int `json:"-"` } type Feeds []Feed @@ -33,3 +35,13 @@ func (f Feeds) sort(urls []string) Feeds { return f } + +func (f Feed) GetTotalUnreads() int { + total := 0 + for _, post := range f.Posts { + if !post.Read { + total++ + } + } + return total +} diff --git a/lib/fetch.go b/lib/fetch.go index 60ba88f..6754ccc 100644 --- a/lib/fetch.go +++ b/lib/fetch.go @@ -100,6 +100,7 @@ func createPost(item *gofeed.Item) Post { Content: content, Link: item.Link, Date: ConvertDate(item.Published), + UUID: item.GUID, } return post diff --git a/lib/state.go b/lib/state.go new file mode 100644 index 0000000..4ae9ef7 --- /dev/null +++ b/lib/state.go @@ -0,0 +1,65 @@ +package lib + +import ( + "encoding/json" + "log" + "os" + + "github.com/adrg/xdg" +) + +func ToggleRead(feeds Feeds, feedID int, postID int) Feeds { + postr := &feeds[feedID].Posts[postID] + postr.Read = !postr.Read + return feeds +} + +func ReadAll(feeds Feeds, feedID int) Feeds { + for i := range feeds[feedID].Posts { + feeds[feedID].Posts[i].Read = true + } + return feeds +} + +func MarkRead(feeds Feeds, feedID int, postID int) Feeds { + postr := &feeds[feedID].Posts[postID] + postr.Read = true + return feeds +} + +func (feeds Feeds) WriteTracking() error { + json, err := json.Marshal(feeds) + if err != nil { + return err + } + return os.WriteFile(getSateFile(), json, 0644) +} + +// Read from JSON file +func (feeds Feeds) ReadTracking() (Feeds, error) { + fileStr := getSateFile() + if _, err := os.Stat(fileStr); os.IsNotExist(err) { + err := feeds.WriteTracking() + if err != nil { + log.Fatalf("could not write tracking file: %v", err) + } + } + + file, err := os.ReadFile(fileStr) + if err != nil { + return nil, err + } + err = json.Unmarshal(file, &feeds) + if err != nil { + return nil, err + } + return feeds, nil +} + +func getSateFile() string { + stateFile, err := xdg.StateFile("izrss/tracking.json") + if err != nil { + log.Fatalf("could not find state file: %v", err) + } + return stateFile +} diff --git a/nix/default.nix b/nix/default.nix index fcff0ab..59c2edc 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -9,7 +9,7 @@ buildGoModule { src = ../.; - vendorHash = "sha256-u50qWuZH2VjnHWjCMeEYFKsVxQarNbx7ixZ+aJ8xOFw="; + vendorHash = "sha256-gH5AFroreBD0tQmT99Bmo2pAdPkiPWUNGsmKX4p3/JA="; ldflags = [ "-s"