diff --git a/cmd/context.go b/cmd/context.go new file mode 100644 index 0000000..51e50bb --- /dev/null +++ b/cmd/context.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/isabelroses/izrss/lib" +) + +type context struct { + prev string + curr string + keys keyMap + 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 +} diff --git a/cmd/keys.go b/cmd/keys.go index 23832b6..af17631 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -40,31 +40,7 @@ func (k keyMap) FullHelp() [][]key.Binding { } } -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"), - ), +var allKeys = keyMap{ Refresh: key.NewBinding( key.WithKeys("r"), key.WithHelp("r", "refresh"), @@ -89,7 +65,7 @@ var keys = keyMap{ // TODO: refator this so its per page and not global func (m Model) handleKeys(msg tea.KeyMsg) (Model, tea.Cmd) { - if m.context == "search" { + if m.context.curr == "search" { switch msg.String() { case "enter": m = m.loadSearchValues() @@ -104,71 +80,71 @@ func (m Model) handleKeys(msg tea.KeyMsg) (Model, tea.Cmd) { } switch { - case key.Matches(msg, m.keys.Help): + case key.Matches(msg, m.context.keys.Help): m.help.ShowAll = !m.help.ShowAll - m.table.SetHeight(m.viewport.Height - lipgloss.Height(m.help.View(m.keys)) - lib.MainStyle.GetBorderBottomSize()) + m.table.SetHeight(m.viewport.Height - lipgloss.Height(m.help.View(m.context.keys)) - lib.MainStyle.GetBorderBottomSize()) - case key.Matches(msg, m.keys.Quit): - err := m.feeds.WriteTracking() + case key.Matches(msg, m.context.keys.Quit): + err := m.context.feeds.WriteTracking() if err != nil { log.Fatalf("Could not write tracking data: %s", err) } return m, tea.Quit - case key.Matches(msg, m.keys.Refresh): - switch m.context { + case key.Matches(msg, m.context.keys.Refresh): + switch m.context.curr { case "home": 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() case "content": - feed := &m.feed + 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.loadContent(m.feed.ID) + m = m.loadContent(m.context.feed.ID) default: return m, nil } - case key.Matches(msg, m.keys.RefreshAll): - if m.context == "home" { - m.feeds = lib.GetAllContent(lib.UserConfig.Urls, false) + case key.Matches(msg, m.context.keys.RefreshAll): + if m.context.curr == "home" { + 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.loadHome() } - case key.Matches(msg, m.keys.Back): - switch m.context { + case key.Matches(msg, m.context.keys.Back): + switch m.context.curr { case "reader": - m = m.loadContent(m.feed.ID) - m.table.SetCursor(m.post.ID) + m = m.loadContent(m.context.feed.ID) + m.table.SetCursor(m.context.post.ID) case "content": m = m.loadHome() - m.table.SetCursor(m.feed.ID) + m.table.SetCursor(m.context.feed.ID) } m.viewport.SetYOffset(0) - case key.Matches(msg, m.keys.Open): - switch m.context { + case key.Matches(msg, m.context.keys.Open): + switch m.context.curr { case "reader": - err := lib.OpenURL(m.post.Link) + err := lib.OpenURL(m.context.post.Link) if err != nil { log.Panic(err) } @@ -182,39 +158,39 @@ func (m Model) handleKeys(msg tea.KeyMsg) (Model, tea.Cmd) { m.viewport.SetYOffset(0) } - case key.Matches(msg, m.keys.Search): - if m.context != "search" { + case key.Matches(msg, m.context.keys.Search): + if m.context.curr != "search" { m = m.loadSearch() } - case key.Matches(msg, m.keys.ToggleRead): - switch m.context { + case key.Matches(msg, m.context.keys.ToggleRead): + switch m.context.curr { case "reader": - lib.ToggleRead(m.feeds, m.feed.ID, m.post.ID) - m = m.loadContent(m.feed.ID) + lib.ToggleRead(m.context.feeds, m.context.feed.ID, m.context.post.ID) + m = m.loadContent(m.context.feed.ID) case "content": - lib.ToggleRead(m.feeds, m.feed.ID, m.table.Cursor()) - m = m.loadContent(m.feed.ID) + lib.ToggleRead(m.context.feeds, m.context.feed.ID, m.table.Cursor()) + m = m.loadContent(m.context.feed.ID) } - err := m.feeds.WriteTracking() + err := m.context.feeds.WriteTracking() if err != nil { log.Fatalf("Could not write tracking data: %s", err) } - case key.Matches(msg, m.keys.ReadAll): - switch m.context { + case key.Matches(msg, m.context.keys.ReadAll): + switch m.context.curr { 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) + lib.ToggleRead(m.context.feeds, m.context.feed.ID, m.context.post.ID) case "content": - lib.ReadAll(m.feeds, m.feed.ID) - m = m.loadContent(m.feed.ID) + lib.ReadAll(m.context.feeds, m.context.feed.ID) + m = m.loadContent(m.context.feed.ID) case "home": - lib.ReadAll(m.feeds, m.table.Cursor()) + lib.ReadAll(m.context.feeds, m.table.Cursor()) m = m.loadHome() } - err := m.feeds.WriteTracking() + err := m.context.feeds.WriteTracking() if err != nil { log.Fatalf("Could not write tracking data: %s", err) } @@ -222,3 +198,34 @@ func (m Model) handleKeys(msg tea.KeyMsg) (Model, tea.Cmd) { return m, nil } + +func defaultKeyMap(overrides ...keyMap) keyMap { + base := 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"), + ), + } + + return keys +} diff --git a/cmd/load.go b/cmd/load.go index 75a72ff..602468a 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -18,46 +18,69 @@ func (m Model) loadHome() Model { } 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.context.curr = "home" m = m.loadNewTable(columns, rows) return m } +func (m Model) loadMixed() Model { + columns := []table.Column{ + {Title: "Date", Width: 15}, + {Title: "Read", Width: 10}, + {Title: "Title", Width: m.table.Width() - 25}, + } + + posts := []lib.Post{} + for _, feed := range m.context.feeds { + posts = append(posts, feed.Posts...) + } + + rows := []table.Row{} + for _, post := range posts { + read := lib.ReadSymbol(post.Read) + rows = append(rows, table.Row{post.Date, read, post.Title}) + } + + m.context.feed.Posts = posts + + m = m.loadNewTable(columns, rows) + m.swapPage("mixed") + + return m +} + func (m Model) loadContent(id int) Model { - feed := m.feeds[id] + feed := m.context.feeds[id] feed.ID = id columns := []table.Column{ + {Title: "", Width: 10}, {Title: "Date", Width: 15}, - {Title: "Read", Width: 10}, {Title: "Title", Width: m.table.Width() - 25}, } 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 + m.swapPage("content") + m.context.feed = feed return m } func (m Model) loadSearch() Model { - m.context = "search" + m.swapPage("search") m.table.Blur() @@ -73,7 +96,7 @@ func (m Model) loadSearchValues() Model { 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) @@ -88,8 +111,8 @@ func (m Model) loadSearchValues() Model { } m = m.loadNewTable(columns, rows) - m.context = "content" - m.feed.Posts = filteredPosts + m.swapPage("content") + m.context.feed.Posts = filteredPosts m.table.Focus() m.filter.Blur() m.table.SetCursor(0) diff --git a/cmd/main.go b/cmd/main.go index 1308d79..0a1ac1e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -39,14 +39,14 @@ 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.context.keys)) - lib.MainStyle.GetBorderBottomSize()) 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) } @@ -74,7 +74,11 @@ func (m Model) handleWindowSize(msg tea.WindowSizeMsg) Model { glamWidth, ) - m = m.loadHome() + if lib.UserConfig.Home == "mixed" { + m = m.loadMixed() + } else { + m = m.loadHome() + } m.ready = true } else { @@ -96,14 +100,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.context.keys), ) 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 +115,14 @@ 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.context.keys), ) 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) + if m.context.curr == "reader" && 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..2c78448 100644 --- a/cmd/modal.go +++ b/cmd/modal.go @@ -15,16 +15,12 @@ import ( // Model is the main model for the application type Model struct { help help.Model - context string - 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 @@ -56,15 +52,11 @@ func NewModel() Model { h.Styles.ShortSeparator = lib.HelpStyle return Model{ - context: "", - feeds: lib.Feeds{}, - feed: lib.Feed{}, + context: context{}, viewport: viewport.Model{}, table: t, ready: false, - keys: keys, help: h, - post: lib.Post{}, filter: f, } } diff --git a/cmd/render.go b/cmd/render.go index 07c350e..9790b4e 100644 --- a/cmd/render.go +++ b/cmd/render.go @@ -10,11 +10,11 @@ var htom = tomd.NewConverter("", true, nil) func (m Model) loadReader() Model { 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 diff --git a/cmd/view.go b/cmd/view.go index 17c2619..cfd01e9 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -9,7 +9,7 @@ import ( ) func (m Model) headerView() string { - title := lib.ReaderStyle.Render(m.post.Title) + title := lib.ReaderStyle.Render(m.context.post.Title) line := strings.Repeat("─", lib.Max(0, m.viewport.Width-lipgloss.Width(title))) return lipgloss.JoinHorizontal(lipgloss.Center, title, line) } @@ -26,7 +26,7 @@ func (m Model) View() string { if !m.ready { out = "Initializing..." - } else if m.context == "reader" { + } else if m.context.curr == "reader" { out = lipgloss.JoinVertical( lipgloss.Top, m.headerView(), diff --git a/lib/config.go b/lib/config.go index 3eb1ea9..8ae44ab 100644 --- a/lib/config.go +++ b/lib/config.go @@ -32,6 +32,7 @@ func LoadConfig(config string) { // UserConfig is the global user configuration var UserConfig = config{ + Home: "home", DateFormat: "02/01/2006", Urls: []string{}, Reader: reader{ @@ -49,6 +50,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"` diff --git a/lib/helpers.go b/lib/helpers.go index 4739ad7..a006bdf 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -103,3 +103,10 @@ func Max(a, b int) int { } return b } + +func ReadSymbol(read bool) string { + if read { + return " " + } + return "•" +}