diff --git a/cmd/context.go b/cmd/context.go new file mode 100644 index 0000000..a31d144 --- /dev/null +++ b/cmd/context.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/isabelroses/izrss/lib" +) + +type context struct { + prev string + curr string + feeds lib.Feeds + post lib.Post + feed lib.Feed +} + +func (m *Model) swapPage(next string) { + m.context.prev = m.context.curr + m.context.curr = next + if m.context.prev == "reader" { + m.viewport.Height = m.viewport.Height + 2 + } +} diff --git a/cmd/help.go b/cmd/help.go new file mode 100644 index 0000000..a7bb967 --- /dev/null +++ b/cmd/help.go @@ -0,0 +1,187 @@ +package cmd + +// modified from https://github.com/charmbracelet/bubbles/blob/master/help/help.go +// I did this so it would be easier to pass context to the help model + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/isabelroses/izrss/lib" +) + +// KeyMap is a map of keybindings used to generate help. Since it's an +// interface it can be any type, though struct or a map[string][]key.Binding +// are likely candidates. +// +// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be +// rendered in the help view, so in theory generated help should self-manage. +type KeyMap interface { + // ShortHelp returns a slice of bindings to be displayed in the short + // version of the help. The help bubble will render help in the order in + // which the help items are returned here. + ShortHelp(m Model) []key.Binding + + // FullHelp returns an extended group of help items, grouped by columns. + // The help bubble will render the help in the order in which the help + // items are returned here. + FullHelp(m Model) [][]key.Binding +} + +// KeyModel contains the state of the help view. +type KeyModel struct { + Style lipgloss.Style + ShortSeparator string + FullSeparator string + Ellipsis string + Width int + ShowAll bool +} + +// New creates a new help view with some useful defaults. +func NewHelp() KeyModel { + return KeyModel{ + ShortSeparator: " • ", + FullSeparator: " • ", + Ellipsis: "…", + Style: lib.HelpStyle, + } +} + +// Update helps satisfy the Bubble Tea Model interface. It's a no-op. +func (m KeyModel) Update(_ tea.Msg) (KeyModel, tea.Cmd) { + return m, nil +} + +// View renders the help view's current state. +func (km KeyModel) View(k KeyMap, m Model) string { + if km.ShowAll { + return km.FullHelpView(k.FullHelp(m)) + } + return km.ShortHelpView(k.ShortHelp(m)) +} + +// ShortHelpView renders a single line help view from a slice of keybindings. +// If the line is longer than the maximum width it will be gracefully +// truncated, showing only as many help items as possible. +func (m KeyModel) ShortHelpView(bindings []key.Binding) string { + if len(bindings) == 0 { + return "" + } + + var b strings.Builder + var totalWidth int + separator := m.Style.Inline(true).Render(m.ShortSeparator) + + for i, kb := range bindings { + if !kb.Enabled() { + continue + } + + var sep string + if totalWidth > 0 && i < len(bindings) { + sep = separator + } + + str := sep + + m.Style.Inline(true).Render(kb.Help().Key) + " " + + m.Style.Inline(true).Render(kb.Help().Desc) + + w := lipgloss.Width(str) + + // If adding this help item would go over the available width, stop + // drawing. + if m.Width > 0 && totalWidth+w > m.Width { + // Although if there's room for an ellipsis, print that. + tail := " " + m.Style.Inline(true).Render(m.Ellipsis) + tailWidth := lipgloss.Width(tail) + + if totalWidth+tailWidth < m.Width { + b.WriteString(tail) + } + + break + } + + totalWidth += w + b.WriteString(str) + } + + return b.String() +} + +// FullHelpView renders help columns from a slice of key binding slices. Each +// top level slice entry renders into a column. +func (m KeyModel) FullHelpView(groups [][]key.Binding) string { + if len(groups) == 0 { + return "" + } + + // Linter note: at this time we don't think it's worth the additional + // code complexity involved in preallocating this slice. + //nolint:prealloc + var ( + out []string + + totalWidth int + sep = m.Style.Render(m.FullSeparator) + sepWidth = lipgloss.Width(sep) + ) + + // Iterate over groups to build columns + for i, group := range groups { + if group == nil || !shouldRenderColumn(group) { + continue + } + + var ( + keys []string + descriptions []string + ) + + // Separate keys and descriptions into different slices + for _, kb := range group { + if !kb.Enabled() { + continue + } + keys = append(keys, kb.Help().Key) + descriptions = append(descriptions, kb.Help().Desc) + } + + col := lipgloss.JoinHorizontal(lipgloss.Top, + m.Style.Render(strings.Join(keys, "\n")), + m.Style.Render(" "), + m.Style.Render(strings.Join(descriptions, "\n")), + ) + + // Column + totalWidth += lipgloss.Width(col) + if m.Width > 0 && totalWidth > m.Width { + break + } + + out = append(out, col) + + // Separator + if i < len(group)-1 { + totalWidth += sepWidth + if m.Width > 0 && totalWidth > m.Width { + break + } + out = append(out, sep) + } + } + + return lipgloss.JoinHorizontal(lipgloss.Top, out...) +} + +func shouldRenderColumn(b []key.Binding) (ok bool) { + for _, v := range b { + if v.Enabled() { + return true + } + } + return false +} diff --git a/cmd/keys.go b/cmd/keys.go index 23832b6..da4cfbf 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -14,6 +14,8 @@ import ( type keyMap struct { Up key.Binding Down key.Binding + JumpUp key.Binding + JumpDown key.Binding Back key.Binding Help key.Binding Quit key.Binding @@ -25,200 +27,273 @@ type keyMap struct { ReadAll key.Binding } -func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.Help, k.Quit} -} +func (k keyMap) ShortHelp(m Model) []key.Binding { + var help []key.Binding -func (k keyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.Up, k.Down}, - {k.Back, k.Open}, - {k.Search}, - {k.Refresh, k.RefreshAll}, - {k.ToggleRead, k.ReadAll}, - {k.Help, k.Quit}, + if m.context.curr == "reader" { + help = []key.Binding{k.Open, k.ToggleRead, k.Quit} + } else { + help = []key.Binding{k.Help, k.Quit} } -} -var keys = keyMap{ - Up: key.NewBinding( - key.WithKeys("up", "k"), - key.WithHelp("↑/k", "move up"), - ), - Down: key.NewBinding( - key.WithKeys("down", "j"), - key.WithHelp("↓/j", "move down"), - ), - Back: key.NewBinding( - key.WithKeys("left", "h", "shift+tab"), - key.WithHelp("←/h", "back"), - ), - Open: key.NewBinding( - key.WithKeys("enter", "o", "right", "l", "tab"), - key.WithHelp("o/enter", "open"), - ), - Help: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "toggle help"), - ), - Quit: key.NewBinding( - key.WithKeys("q", "esc", "ctrl+c"), - key.WithHelp("q/esc", "quit"), - ), - Refresh: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("r", "refresh"), - ), - RefreshAll: key.NewBinding( - key.WithKeys("R"), - key.WithHelp("R", "refresh all"), - ), - Search: key.NewBinding( - 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", "mark all as read"), - ), + return help } -// TODO: refator this so its per page and not global -func (m Model) handleKeys(msg tea.KeyMsg) (Model, tea.Cmd) { - if m.context == "search" { - switch msg.String() { - case "enter": - m = m.loadSearchValues() +func (k keyMap) FullHelp(m Model) [][]key.Binding { + var help [][]key.Binding - case "ctrl+c", "esc", "/": - m = m.loadContent(m.table.Cursor()) - m.table.Focus() - m.filter.Blur() + switch m.context.curr { + case "home": + help = [][]key.Binding{ + {k.Up, k.Down}, + {k.JumpUp, k.JumpDown}, + {k.Back, k.Open}, + {k.Search, k.ReadAll}, + {k.Refresh, k.RefreshAll}, + {k.Help, k.Quit}, } - - return m, nil + case "content": + help = [][]key.Binding{ + {k.Up, k.Down}, + {k.JumpUp, k.JumpDown}, + {k.Back, k.Open}, + {k.Search}, + {k.Refresh, k.RefreshAll}, + {k.ToggleRead, k.ReadAll}, + {k.Help, k.Quit}, + } + case "mixed": + help = [][]key.Binding{ + {k.Up, k.Down}, + {k.JumpUp, k.JumpDown}, + {k.Back, k.Open}, + {k.Search, k.ToggleRead}, + // {k.Refresh, k.RefreshAll}, + {k.Help, k.Quit}, + } + case "reader": + help = [][]key.Binding{} } - switch { - case key.Matches(msg, m.keys.Help): - m.help.ShowAll = !m.help.ShowAll - m.table.SetHeight(m.viewport.Height - lipgloss.Height(m.help.View(m.keys)) - lib.MainStyle.GetBorderBottomSize()) + return help +} - case key.Matches(msg, m.keys.Quit): - err := m.feeds.WriteTracking() - if err != nil { - log.Fatalf("Could not write tracking data: %s", err) - } - return m, tea.Quit +// TODO: refator this so its per page and not global +func (m Model) handleKeys(msg tea.KeyMsg) (Model, tea.Cmd) { + // handle page specific keys + switch m.context.curr { + case "home": + switch { + case key.Matches(msg, m.keys.Open): + m.loadContent(m.table.Cursor()) + m.table.SetCursor(0) + m.viewport.SetYOffset(0) - case key.Matches(msg, m.keys.Refresh): - switch m.context { - case "home": + case key.Matches(msg, m.keys.Refresh): id := m.table.Cursor() - feed := &m.feeds[id] + feed := &m.context.feeds[id] lib.FetchURL(feed.URL, false) feed.Posts = lib.GetPosts(feed.URL) err := error(nil) - m.feeds, err = m.feeds.ReadTracking() + m.context.feeds, err = m.context.feeds.ReadTracking() if err != nil { log.Fatal(err) } - m = m.loadHome() + m.loadHome() - case "content": - feed := &m.feed - feed.Posts = lib.GetPosts(feed.URL) + case key.Matches(msg, m.keys.RefreshAll): + m.context.feeds = lib.GetAllContent(lib.UserConfig.Urls, false) err := error(nil) - m.feeds, err = m.feeds.ReadTracking() + m.context.feeds, err = m.context.feeds.ReadTracking() if err != nil { log.Fatal(err) } - m = m.loadContent(m.feed.ID) + m.loadHome() - default: - return m, nil + case key.Matches(msg, m.keys.ReadAll): + lib.ReadAll(m.context.feeds, m.table.Cursor()) + m.loadHome() + err := m.context.feeds.WriteTracking() + if err != nil { + log.Fatalf("Could not write tracking data: %s", err) + } } - case key.Matches(msg, m.keys.RefreshAll): - if m.context == "home" { - m.feeds = lib.GetAllContent(lib.UserConfig.Urls, false) + case "content": + switch { + case key.Matches(msg, m.keys.Refresh): + feed := &m.context.feed + feed.Posts = lib.GetPosts(feed.URL) err := error(nil) - m.feeds, err = m.feeds.ReadTracking() + m.context.feeds, err = m.context.feeds.ReadTracking() if err != nil { log.Fatal(err) } - m = m.loadHome() - } + m.loadContent(m.context.feed.ID) + + case key.Matches(msg, m.keys.Back): + m.loadHome() + m.table.SetCursor(m.context.feed.ID) + m.viewport.SetYOffset(0) + + case key.Matches(msg, m.keys.Open): + m.loadReader() - case key.Matches(msg, m.keys.Back): - switch m.context { - case "reader": - m = m.loadContent(m.feed.ID) - m.table.SetCursor(m.post.ID) - case "content": - m = m.loadHome() - m.table.SetCursor(m.feed.ID) + case key.Matches(msg, m.keys.ToggleRead): + lib.ToggleRead(m.context.feeds, m.context.feed.ID, m.table.Cursor()) + m.loadContent(m.context.feed.ID) + err := m.context.feeds.WriteTracking() + if err != nil { + log.Fatalf("Could not write tracking data: %s", err) + } + + case key.Matches(msg, m.keys.ReadAll): + lib.ReadAll(m.context.feeds, m.context.feed.ID) + m.loadContent(m.context.feed.ID) + err := m.context.feeds.WriteTracking() + if err != nil { + log.Fatalf("Could not write tracking data: %s", err) + } } - m.viewport.SetYOffset(0) - case key.Matches(msg, m.keys.Open): - switch m.context { - case "reader": - err := lib.OpenURL(m.post.Link) + case "mixed": + switch { + case key.Matches(msg, m.keys.Open): + m.loadReader() + + case key.Matches(msg, m.keys.ToggleRead): + lib.ToggleRead(m.context.feeds, m.context.feed.ID, m.table.Cursor()) + m.loadMixed() + err := m.context.feeds.WriteTracking() if err != nil { - log.Panic(err) + log.Fatalf("Could not write tracking data: %s", err) } - case "content": - m = m.loadReader() + case key.Matches(msg, m.keys.ReadAll): + lib.ReadAll(m.context.feeds, m.context.feed.ID) + m.loadMixed() + err := m.context.feeds.WriteTracking() + if err != nil { + log.Fatalf("Could not write tracking data: %s", err) + } + } - default: - m = m.loadContent(m.table.Cursor()) - m.table.SetCursor(0) + case "reader": + switch { + case key.Matches(msg, m.keys.Back): + if m.context.prev == "mixed" { + m.loadMixed() + } else { + m.loadContent(m.context.feed.ID) + } + m.table.SetCursor(m.context.post.ID) m.viewport.SetYOffset(0) - } - case key.Matches(msg, m.keys.Search): - if m.context != "search" { - m = m.loadSearch() - } + case key.Matches(msg, m.keys.Open): + err := lib.OpenURL(m.context.post.Link) + if err != nil { + log.Panic(err) + } - 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.ToggleRead): + lib.ToggleRead(m.context.feeds, m.context.feed.ID, m.context.post.ID) + m.loadContent(m.context.feed.ID) + err := m.context.feeds.WriteTracking() + if err != nil { + log.Fatalf("Could not write tracking data: %s", err) + } } - err := m.feeds.WriteTracking() - if err != nil { - log.Fatalf("Could not write tracking data: %s", err) + + case "search": + switch msg.String() { + case "enter": + m.loadSearchValues() + + case "ctrl+c", "esc", "/": + m.loadContent(m.table.Cursor()) + m.table.Focus() + m.filter.Blur() } + } + + // handle global keys + switch { + case key.Matches(msg, m.keys.JumpUp): + m.table.MoveUp(5) + case key.Matches(msg, m.keys.JumpDown): + m.table.MoveDown(5) - 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.feed.ID) - case "home": - lib.ReadAll(m.feeds, m.table.Cursor()) - m = m.loadHome() + case key.Matches(msg, m.keys.Search): + if m.context.curr != "search" { + m.loadSearch() } - err := m.feeds.WriteTracking() + case key.Matches(msg, m.keys.Help): + m.help.ShowAll = !m.help.ShowAll + m.table.SetHeight(m.viewport.Height - lipgloss.Height(m.help.View(m.keys, m))) + + case key.Matches(msg, m.keys.Quit): + err := m.context.feeds.WriteTracking() if err != nil { log.Fatalf("Could not write tracking data: %s", err) } + return m, tea.Quit } return m, nil } + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "move up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "move down"), + ), + JumpUp: key.NewBinding( + key.WithKeys("shift+up", "K"), + key.WithHelp("↑/k", "jump move up"), + ), + JumpDown: key.NewBinding( + key.WithKeys("shift+down", "J"), + key.WithHelp("↓/j", "jump move down"), + ), + Back: key.NewBinding( + key.WithKeys("left", "h", "shift+tab"), + key.WithHelp("←/h", "back"), + ), + Open: key.NewBinding( + key.WithKeys("enter", "o", "right", "l", "tab"), + key.WithHelp("o/enter", "open"), + ), + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "toggle help"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c"), + key.WithHelp("q/esc", "quit"), + ), + Refresh: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "refresh"), + ), + RefreshAll: key.NewBinding( + key.WithKeys("R"), + key.WithHelp("R", "refresh all"), + ), + Search: key.NewBinding( + 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", "mark all as read"), + ), +} diff --git a/cmd/load.go b/cmd/load.go index 75a72ff..f53a988 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "log" "strconv" "strings" @@ -11,69 +12,89 @@ import ( ) // load the home view, this conists of the list of feeds -func (m Model) loadHome() Model { +func (m *Model) loadHome() { columns := []table.Column{ {Title: "Unread", Width: 10}, {Title: "Title", Width: m.table.Width() - 10}, } rows := []table.Row{} - for _, Feed := range m.feeds { + for _, Feed := range m.context.feeds { totalUnread := strconv.Itoa(Feed.GetTotalUnreads()) fraction := fmt.Sprintf("%s/%d", totalUnread, len(Feed.Posts)) rows = append(rows, table.Row{fraction, Feed.Title}) } - m.context = "home" - m = m.loadNewTable(columns, rows) + m.swapPage("home") + m.loadNewTable(columns, rows) +} + +func (m *Model) loadMixed() { + columns := []table.Column{ + {Title: "", Width: 2}, + {Title: "Date", Width: 15}, + {Title: "Title", Width: m.table.Width() - 17}, + } + + posts := []lib.Post{} + for _, feed := range m.context.feeds { + posts = append(posts, feed.Posts...) + } + + err := lib.SortPosts(posts) + if err != nil { + log.Printf("Failed to sort %s", err) + } - return m + rows := make([]table.Row, len(posts)) + for i, post := range posts { + read := lib.ReadSymbol(post.Read) + rows[i] = table.Row{read, post.Date, post.Title} + } + + m.context.feed = lib.Feed{Title: "Mixed", Posts: posts, ID: 0, URL: ""} + + m.loadNewTable(columns, rows) + m.swapPage("mixed") } -func (m Model) loadContent(id int) Model { - feed := m.feeds[id] +func (m *Model) loadContent(id int) { + feed := m.context.feeds[id] feed.ID = id columns := []table.Column{ + {Title: "", Width: 2}, {Title: "Date", Width: 15}, - {Title: "Read", Width: 10}, - {Title: "Title", Width: m.table.Width() - 25}, + {Title: "Title", Width: m.table.Width() - 17}, } rows := []table.Row{} for _, post := range feed.Posts { - read := "x" - if post.Read { - read = "✓" - } - rows = append(rows, table.Row{post.Date, read, post.Title}) + readsym := lib.ReadSymbol(post.Read) + rows = append(rows, table.Row{readsym, post.Date, post.Title}) } - m = m.loadNewTable(columns, rows) - m.context = "content" - m.feed = feed - - return m + m.loadNewTable(columns, rows) + m.swapPage("content") + m.context.feed = feed } -func (m Model) loadSearch() Model { - m.context = "search" +func (m *Model) loadSearch() { + m.swapPage("search") m.table.Blur() m.filter.Focus() m.filter.SetValue("") - - return m } -func (m Model) loadSearchValues() Model { +func (m Model) loadSearchValues() { search := m.filter.Value() var filteredPosts []lib.Post rows := []table.Row{} - for _, feed := range m.feeds { + for _, feed := range m.context.feeds { for _, post := range feed.Posts { if strings.Contains(strings.ToLower(post.Content), strings.ToLower(search)) { filteredPosts = append(filteredPosts, post) @@ -87,17 +108,15 @@ func (m Model) loadSearchValues() Model { {Title: "Title", Width: m.table.Width() - 15}, } - m = m.loadNewTable(columns, rows) - m.context = "content" - m.feed.Posts = filteredPosts + m.loadNewTable(columns, rows) + m.swapPage("content") + m.context.feed.Posts = filteredPosts m.table.Focus() m.filter.Blur() m.table.SetCursor(0) - - return m } -func (m Model) loadNewTable(columns []table.Column, rows []table.Row) Model { +func (m *Model) loadNewTable(columns []table.Column, rows []table.Row) { t := &m.table // NOTE: clear the rows first to prevent panic @@ -105,6 +124,4 @@ func (m Model) loadNewTable(columns []table.Column, rows []table.Row) Model { t.SetColumns(columns) t.SetRows(rows) - - return m } diff --git a/cmd/main.go b/cmd/main.go index 1308d79..0a5eeed 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package cmd import ( "log" + "sync" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -39,43 +40,64 @@ func (m Model) handleWindowSize(msg tea.WindowSizeMsg) Model { width := msg.Width - framew m.table.SetWidth(width) - m.table.SetHeight(height - lipgloss.Height(m.help.View(m.keys)) - lib.MainStyle.GetBorderBottomSize()) + m.table.SetHeight(height - lipgloss.Height(m.help.View(m.keys, m))) if !m.ready { - m.feeds = lib.GetAllContent(lib.UserConfig.Urls, lib.CheckCache()) + m.context.feeds = lib.GetAllContent(lib.UserConfig.Urls, lib.CheckCache()) m.viewport = viewport.New(width, height) err := error(nil) - m.feeds, err = m.feeds.ReadTracking() + m.context.feeds, err = m.context.feeds.ReadTracking() if err != nil { log.Fatalf("could not read tracking file: %v", err) } - var glamWidth glamour.TermRendererOption - switch lib.UserConfig.Reader.Size.(type) { - case string: - switch lib.UserConfig.Reader.Size { - case "full", "fullscreen": - glamWidth = glamour.WithWordWrap(width) - case "most": - glamWidth = glamour.WithWordWrap(int(float64(width) * 0.75)) - case "recomended": - glamWidth = glamour.WithWordWrap(80) + // we make this part mutli-threaded otherwise its really slow + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + var glamWidth glamour.TermRendererOption + switch lib.UserConfig.Reader.Size.(type) { + case string: + switch lib.UserConfig.Reader.Size { + case "full", "fullscreen": + glamWidth = glamour.WithWordWrap(width) + case "most": + glamWidth = glamour.WithWordWrap(int(float64(width) * 0.75)) + case "recomended": + glamWidth = glamour.WithWordWrap(80) + } + + case int64: + w := int(lib.UserConfig.Reader.Size.(int64)) + glamWidth = glamour.WithWordWrap(w) + default: + log.Fatalf("invalid reader size: %v", lib.UserConfig.Reader.Size) } - case int64: - w := int(lib.UserConfig.Reader.Size.(int64)) - glamWidth = glamour.WithWordWrap(w) - default: - log.Fatalf("invalid reader size: %v", lib.UserConfig.Reader.Size) - } - m.glam, _ = glamour.NewTermRenderer( - glamour.WithEnvironmentConfig(), - glamWidth, - ) + var glamTheme glamour.TermRendererOption + if lib.UserConfig.Reader.Theme == "environment" { + glamTheme = glamour.WithEnvironmentConfig() + } else if lib.UserConfig.Reader.Theme != "" { + glamTheme = glamour.WithStylePath(lib.UserConfig.Reader.Theme) + } else { + glamTheme = glamour.WithAutoStyle() + } - m = m.loadHome() + m.glam, _ = glamour.NewTermRenderer( + glamTheme, + glamWidth, + ) + }() + + if lib.UserConfig.Home == "mixed" { + m.loadMixed() + } else { + m.loadHome() + } + wg.Wait() m.ready = true } else { m.viewport.Width = width @@ -96,14 +118,14 @@ func (m Model) updateViewport(msg tea.Msg) (Model, tea.Cmd) { m.table, cmd = m.table.Update(msg) cmds = append(cmds, cmd) - if m.context != "reader" && m.context != "search" { + if m.context.curr != "reader" && m.context.curr != "search" { view := lipgloss.JoinVertical( lipgloss.Top, m.table.View(), - m.help.View(m.keys), + m.help.View(m.keys, m), ) m.viewport.SetContent(view) - } else if m.context == "search" { + } else if m.context.curr == "search" { m.filter, cmd = m.filter.Update(msg) cmds = append(cmds, cmd) @@ -111,14 +133,15 @@ func (m Model) updateViewport(msg tea.Msg) (Model, tea.Cmd) { lipgloss.Top, m.filter.View(), m.table.View(), - m.help.View(m.keys), + m.help.View(m.keys, m), ) m.viewport.SetContent(view) } - if m.context == "reader" && m.viewport.ScrollPercent() >= lib.UserConfig.Reader.ReadThreshold { - lib.MarkRead(m.feeds, m.feed.ID, m.post.ID) + // HACK: if the previous was mixed we never marked the post as read + if m.context.curr == "reader" && m.context.prev == "mixed" && m.viewport.ScrollPercent() >= lib.UserConfig.Reader.ReadThreshold { + lib.MarkRead(m.context.feeds, m.context.feed.ID, m.context.post.ID) } m.viewport, cmd = m.viewport.Update(msg) diff --git a/cmd/modal.go b/cmd/modal.go index 4146f19..319d8b8 100644 --- a/cmd/modal.go +++ b/cmd/modal.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -14,17 +13,14 @@ import ( // Model is the main model for the application type Model struct { - help help.Model - context string + help KeyModel keys keyMap + glam *glamour.TermRenderer + context context viewport viewport.Model - feeds lib.Feeds filter textinput.Model - post lib.Post - feed lib.Feed table table.Model ready bool - glam *glamour.TermRenderer } // Init sets the initial state of the model @@ -47,24 +43,13 @@ func NewModel() Model { Bold(true). Foreground(lipgloss.Color("229")) - h := help.New() - h.Styles.FullKey = lib.HelpStyle - h.Styles.FullDesc = lib.HelpStyle - h.Styles.FullSeparator = lib.HelpStyle - h.Styles.ShortKey = lib.HelpStyle - h.Styles.ShortDesc = lib.HelpStyle - h.Styles.ShortSeparator = lib.HelpStyle - return Model{ - context: "", - feeds: lib.Feeds{}, - feed: lib.Feed{}, + context: context{}, viewport: viewport.Model{}, table: t, ready: false, + help: NewHelp(), keys: keys, - help: h, - post: lib.Post{}, filter: f, } } diff --git a/cmd/render.go b/cmd/render.go index 07c350e..33131ec 100644 --- a/cmd/render.go +++ b/cmd/render.go @@ -8,13 +8,13 @@ import ( var htom = tomd.NewConverter("", true, nil) -func (m Model) loadReader() Model { +func (m *Model) loadReader() { id := m.table.Cursor() - post := m.feed.Posts[id] + post := m.context.feed.Posts[id] post.ID = id - m.context = "reader" - m.post = post + m.swapPage("reader") + m.context.post = post m.viewport.YPosition = 0 // reset the viewport position // render the post @@ -27,7 +27,7 @@ func (m Model) loadReader() Model { if err != nil { log.Fatalf("could not render markdown: %v", err) } - m.viewport.SetContent(out) - return m + m.viewport.SetContent(out) + m.viewport.Height = m.viewport.Height - 2 } diff --git a/cmd/view.go b/cmd/view.go index 17c2619..e12d423 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -2,36 +2,26 @@ package cmd import ( "fmt" - "strings" "github.com/charmbracelet/lipgloss" + "github.com/isabelroses/izrss/lib" ) -func (m Model) headerView() string { - title := lib.ReaderStyle.Render(m.post.Title) - line := strings.Repeat("─", lib.Max(0, m.viewport.Width-lipgloss.Width(title))) - return lipgloss.JoinHorizontal(lipgloss.Center, title, line) -} - -func (m Model) footerView() string { - info := lib.ReaderStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) - line := strings.Repeat("─", lib.Max(0, m.viewport.Width-lipgloss.Width(info))) - return lipgloss.JoinHorizontal(lipgloss.Center, line, info) -} - // View renders the model as a string func (m Model) View() string { out := "" if !m.ready { out = "Initializing..." - } else if m.context == "reader" { - out = lipgloss.JoinVertical( - lipgloss.Top, - m.headerView(), - m.viewport.View(), - m.footerView(), + } else if m.context.curr == "reader" { + out = lib.MainStyle.Render( + lipgloss.JoinVertical( + lipgloss.Top, + fmt.Sprintf("%s - %3.f%%", m.context.post.Title, m.viewport.ScrollPercent()*100), + m.viewport.View(), + m.help.View(m.keys, m), + ), ) } else { out = lib.MainStyle.Render(m.viewport.View()) diff --git a/flake.lock b/flake.lock index 994aa44..618e92e 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1720957393, - "narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=", + "lastModified": 1724224976, + "narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "693bc46d169f5af9c992095736e82c3488bf7dbb", + "rev": "c374d94f1536013ca8e92341b540eba4c22f9c62", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index ca3309a..d68931e 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.22.1 require ( github.com/JohannesKaufmann/html-to-markdown v1.6.0 - github.com/adrg/xdg v0.4.0 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.6 - github.com/charmbracelet/lipgloss v0.11.0 + github.com/adrg/xdg v0.5.0 + github.com/charmbracelet/bubbles v0.19.0 + github.com/charmbracelet/bubbletea v0.27.0 + github.com/charmbracelet/lipgloss v0.13.0 github.com/mmcdole/gofeed v1.3.0 github.com/pelletier/go-toml/v2 v2.2.2 - github.com/urfave/cli/v2 v2.27.2 + github.com/urfave/cli/v2 v2.27.4 ) require ( @@ -19,41 +19,38 @@ require ( github.com/andybalholm/cascadia v1.3.2 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.1.2 // indirect - github.com/charmbracelet/x/input v0.1.2 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/term v0.23.0 // indirect ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/glamour v0.7.0 + github.com/charmbracelet/glamour v0.8.0 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index 97d1ecc..4ded8af 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5 github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ= github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= -github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= -github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= +github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= @@ -16,39 +16,31 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= -github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= -github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= -github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= -github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= -github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= -github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= -github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= -github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= -github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4= -github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0= -github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= -github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= +github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= +github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= +github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -67,12 +59,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8= @@ -88,10 +79,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -114,26 +103,19 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= -github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -141,8 +123,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -153,20 +133,18 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -175,10 +153,9 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -187,16 +164,17 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/lib/config.go b/lib/config.go index 3eb1ea9..e176fb4 100644 --- a/lib/config.go +++ b/lib/config.go @@ -32,11 +32,13 @@ func LoadConfig(config string) { // UserConfig is the global user configuration var UserConfig = config{ + Home: "home", DateFormat: "02/01/2006", Urls: []string{}, Reader: reader{ Size: "recomended", ReadThreshold: 0.8, + Theme: "dark", }, Colors: colors{ Text: "#cdd6f4", @@ -49,6 +51,7 @@ var UserConfig = config{ // Config is the struct that holds the configuration type config struct { + Home string `toml:"home"` Colors colors `toml:"colors"` Reader reader `toml:"reader"` DateFormat string `toml:"dateformat"` @@ -65,5 +68,6 @@ type colors struct { type reader struct { Size interface{} `toml:"size"` + Theme string `toml:"theme"` ReadThreshold float64 `toml:"read_threshold"` } diff --git a/lib/feeds.go b/lib/feeds.go index e1e77ae..e680e6d 100644 --- a/lib/feeds.go +++ b/lib/feeds.go @@ -1,6 +1,9 @@ package lib -import "sort" +import ( + "sort" + "time" +) // Post represents a single post in a feed type Post struct { @@ -54,25 +57,44 @@ func (f Feed) GetTotalUnreads() int { // also a bit of nix inspired me to write this `foldl recursiveUpdate { } importedLibs` // okay maybe it was beacuse of the comments not actually the code, kinda fair. func mergeFeeds(feeds1, feeds2 Feeds) Feeds { - // Create a map to hold posts from feeds1 by their UUID for quick lookup - postMap := make(map[string]*Post) + // Create a map to hold posts' read state from feeds1 by their UUID for quick lookup + readStatusMap := make(map[string]bool) // Iterate through feeds1 and map their posts by UUID - for i := range feeds1 { - for j := range feeds1[i].Posts { - postMap[feeds1[i].Posts[j].UUID] = &feeds1[i].Posts[j] + for _, feed := range feeds1 { + for _, post := range feed.Posts { + readStatusMap[post.UUID] = post.Read } } // Iterate through feeds2 and merge their posts into feeds1 based on UUID for i := range feeds2 { for j := range feeds2[i].Posts { - if post1, exists := postMap[feeds2[i].Posts[j].UUID]; exists { - // Update the existing post in feeds1 with the one from feeds2 - post1.Read = feeds2[i].Posts[j].Read + if readStatus, exists := readStatusMap[feeds1[i].Posts[j].UUID]; exists { + feeds1[i].Posts[j].Read = readStatus } } } return feeds1 } + +// SortPostsByDate sorts an array of Post structs by the Date field. +func SortPosts(posts []Post) error { + dateFormat := UserConfig.DateFormat + + sort.Slice(posts, func(i, j int) bool { + // Parse the dates for the current comparison + dateI, errI := time.Parse(dateFormat, posts[i].Date) + dateJ, errJ := time.Parse(dateFormat, posts[j].Date) + + if errI != nil || errJ != nil { + return false + } + + // Compare the parsed dates + return dateI.After(dateJ) + }) + + return nil +} diff --git a/lib/helpers.go b/lib/helpers.go index 4739ad7..0aa674b 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -96,10 +96,9 @@ func URLToDir(url string) string { return url } -// Max returns the maximum of two integers -func Max(a, b int) int { - if a > b { - return a +func ReadSymbol(read bool) string { + if read { + return "" } - return b + return "•" } diff --git a/lib/style.go b/lib/style.go index d2024cc..0dd4947 100644 --- a/lib/style.go +++ b/lib/style.go @@ -14,9 +14,6 @@ var ( Padding(0, 1). Margin(0) - // ReaderStyle is the style for the reader - ReaderStyle = lipgloss.NewStyle() - // HelpStyle is the style for the help keybinds menu HelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(UserConfig.Colors.Subtext)) ) diff --git a/nix/default.nix b/nix/default.nix index df0d46e..a113b01 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -19,7 +19,7 @@ buildGoModule { ] ); }; - vendorHash = "sha256-XYJuXEGHGYTeprHp9oBiHCrKBVc46KpYY3qqlEknr+8="; + vendorHash = "sha256-K7rP6Cy4e2vzH1HLGfW0KZy/bLOlb2Thy5QQXDpijz8="; ldflags = [ "-s"