Skip to content

Commit

Permalink
feat: add --summary command
Browse files Browse the repository at this point in the history
  • Loading branch information
radulucut committed Aug 19, 2024
1 parent d582fab commit 75002eb
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 6 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ cleed list mylist
cleed config

# Disable styling
cleed config --styling=false
cleed config --styling=2

# Map color 0 to 230 and color 1 to 213
cleed config --map-colors=0:230,1:213
Expand All @@ -106,6 +106,9 @@ cleed config --map-colors=

# Display color range. Useful for finding colors to map
cleed config --color-range

# Enable run summary
cleed config --summary=1
```

> **Color mapping**
Expand Down
13 changes: 12 additions & 1 deletion cmd/cleed/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Examples:
cleed config
# Disable styling
cleed config --styling=false
cleed config --styling=2
# Map color 0 to 230 and color 1 to 213
cleed config --map-colors=0:230,1:213
Expand All @@ -28,12 +28,16 @@ Examples:
# Display color range. Useful for finding colors to map
cleed config --color-range
# Enable run summary
cleed config --summary=1
`,
RunE: r.RunConfig,
}

flags := cmd.Flags()
flags.Uint8("styling", 0, "disable or enable styling (0: default, 1: enable, 2: disable)")
flags.Uint8("summary", 0, "disable or enable summary (0: disable, 1: enable)")
flags.String("map-colors", "", "map colors to other colors, e.g. 0:230,1:213. Use --color-range to check available colors")
flags.Bool("color-range", false, "display color range. Useful for finding colors to map")

Expand All @@ -48,6 +52,13 @@ func (r *Root) RunConfig(cmd *cobra.Command, args []string) error {
}
return r.feed.SetStyling(styling)
}
if cmd.Flag("summary").Changed {
summary, err := cmd.Flags().GetUint8("summary")
if err != nil {
return err
}
return r.feed.SetSummary(summary)
}
if cmd.Flag("map-colors").Changed {
return r.feed.UpdateColorMap(cmd.Flag("map-colors").Value.String())
}
Expand Down
37 changes: 37 additions & 0 deletions cmd/cleed/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func Test_Config(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, `Styling: enabled
Color map:
Summary: disabled
`, out.String())

config, err := storage.LoadConfig()
Expand Down Expand Up @@ -86,6 +87,42 @@ func Test_Config_Styling(t *testing.T) {
assert.Equal(t, expectedConfig, config)
}

func Test_Config_Summary(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

timeMock := mocks.NewMockTime(ctrl)
timeMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

out := new(bytes.Buffer)
printer := internal.NewPrinter(nil, out, out)
storage := _storage.NewLocalStorage("cleed_test", timeMock)
defer localStorageCleanup(t, storage)

feed := internal.NewTerminalFeed(timeMock, printer, storage)
feed.SetAgent("cleed/test")

root, err := NewRoot("0.1.0", timeMock, printer, storage, feed)
assert.NoError(t, err)

os.Args = []string{"cleed", "config", "--summary", "1"}

err = root.Cmd.Execute()
assert.NoError(t, err)
assert.Equal(t, "summary was updated\n", out.String())

config, err := storage.LoadConfig()
assert.NoError(t, err)
expectedConfig := &_storage.Config{
Version: "0.1.0",
LastRun: time.Time{},
Styling: 0,
Summary: 1,
ColorMap: make(map[uint8]uint8),
}
assert.Equal(t, expectedConfig, config)
}

func Test_Config_MapColors(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
127 changes: 127 additions & 0 deletions cmd/cleed/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,133 @@ RSS Feed • Item 1
assert.Equal(t, atom, string(b))
}

func Test_Feed_With_Summary(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

timeMock := mocks.NewMockTime(ctrl)
timeMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

out := new(bytes.Buffer)
printer := internal.NewPrinter(nil, out, out)
storage := _storage.NewLocalStorage("cleed_test", timeMock)
defer localStorageCleanup(t, storage)
storage.Init("0.1.0")

configDir, err := os.UserConfigDir()
if err != nil {
t.Fatal(err)
}
listsDir := path.Join(configDir, "cleed_test", "lists")
err = os.MkdirAll(listsDir, 0700)
if err != nil {
t.Fatal(err)
}

config, err := storage.LoadConfig()
if err != nil {
t.Fatal(err)
}
config.Summary = 1
err = storage.SaveConfig()
if err != nil {
t.Fatal(err)
}

rss := createDefaultRSS()
atom := createDefaultAtom()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/rss" {
w.Header().Set("ETag", "123")
w.Write([]byte(rss))
} else if r.URL.Path == "/atom" {
w.Write([]byte(atom))
}
}))
defer server.Close()

err = os.WriteFile(path.Join(listsDir, "default"),
[]byte(fmt.Sprintf("%d %s\n",
defaultCurrentTime.Unix(), server.URL+"/rss",
),
), 0600)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(path.Join(listsDir, "test"),
[]byte(fmt.Sprintf("%d %s\n",
defaultCurrentTime.Unix(), server.URL+"/atom",
),
), 0600)
if err != nil {
t.Fatal(err)
}

feed := internal.NewTerminalFeed(timeMock, printer, storage)
feed.SetAgent("cleed/test")

root, err := NewRoot("0.1.0", timeMock, printer, storage, feed)
assert.NoError(t, err)

os.Args = []string{"cleed"}

err = root.Cmd.Execute()
assert.NoError(t, err)
assert.Equal(t, `RSS Feed • Item 2
1688 days ago https://rss-feed.com/item-2/
Atom Feed • Item 2
1594 days ago https://atom-feed.com/item-2/
Atom Feed • Item 1
18 hours ago https://atom-feed.com/item-1/
RSS Feed • Item 1
15 minutes ago https://rss-feed.com/item-1/
Displayed 4 items from 2 feeds (0 cached, 2 fetched) with 4 items in 0.00s
`, out.String())

userCacheDir, err := os.UserCacheDir()
if err != nil {
t.Fatal(err)
}
cacheDir := path.Join(userCacheDir, "cleed_test")
files, err := os.ReadDir(cacheDir)
if err != nil {
t.Fatal(err)
}
assert.Len(t, files, 3)

cacheInfo, err := storage.LoadCacheInfo()
assert.NoError(t, err)
assert.Equal(t, 2, len(cacheInfo))
assert.Equal(t, &_storage.CacheInfoItem{
URL: server.URL + "/rss",
LastCheck: time.Unix(defaultCurrentTime.Unix(), 0),
ETag: "123",
FetchAfter: time.Unix(defaultCurrentTime.Unix()+60, 0),
}, cacheInfo[server.URL+"/rss"])
assert.Equal(t, &_storage.CacheInfoItem{
URL: server.URL + "/atom",
LastCheck: time.Unix(defaultCurrentTime.Unix(), 0),
ETag: "",
FetchAfter: time.Unix(defaultCurrentTime.Unix()+60, 0),
}, cacheInfo[server.URL+"/atom"])

b, err := os.ReadFile(path.Join(cacheDir, "feed_"+url.QueryEscape(server.URL+"/rss")))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, rss, string(b))

b, err = os.ReadFile(path.Join(cacheDir, "feed_"+url.QueryEscape(server.URL+"/atom")))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, atom, string(b))
}

func Test_Feed_Specific_List(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
61 changes: 57 additions & 4 deletions internal/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ func (f *TerminalFeed) DisplayConfig() error {
f.printer.Printf(" %d:%d", k, v)
}
f.printer.Println()
summary := "disabled"
if config.Summary == 1 {
summary = "enabled"
}
f.printer.Println("Summary:", summary)
return nil
}

Expand All @@ -86,6 +91,23 @@ func (f *TerminalFeed) SetStyling(v uint8) error {
return nil
}

func (f *TerminalFeed) SetSummary(v uint8) error {
config, err := f.storage.LoadConfig()
if err != nil {
return utils.NewInternalError("failed to load config: " + err.Error())
}
if v > 1 {
return utils.NewInternalError("invalid value for summary")
}
config.Summary = v
err = f.storage.SaveConfig()
if err != nil {
return utils.NewInternalError("failed to save config: " + err.Error())
}
f.printer.Println("summary was updated")
return nil
}

func (f *TerminalFeed) UpdateColorMap(mappings string) error {
config, err := f.storage.LoadConfig()
if err != nil {
Expand Down Expand Up @@ -209,12 +231,24 @@ type FeedOptions struct {
Since time.Time
}

type RunSummary struct {
Start time.Time
FeedsCount int
FeedsCached int
FeedsFetched int
ItemsCount int
ItemsShown int
}

func (f *TerminalFeed) Feed(opts *FeedOptions) error {
summary := &RunSummary{
Start: f.time.Now(),
}
config, err := f.storage.LoadConfig()
if err != nil {
return utils.NewInternalError("failed to load config: " + err.Error())
}
items, err := f.processFeeds(opts, config)
items, err := f.processFeeds(opts, config, summary)
if err != nil {
return err
}
Expand Down Expand Up @@ -266,10 +300,25 @@ func (f *TerminalFeed) Feed(opts *FeedOptions) error {
}
config.LastRun = f.time.Now()
f.storage.SaveConfig()
if config.Summary == 1 {
summary.ItemsShown = l
f.printSummary(summary)
}
return nil
}

func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config) ([]*FeedItem, error) {
func (f *TerminalFeed) printSummary(s *RunSummary) {
f.printer.Printf("Displayed %s from %s (%d cached, %d fetched) with %s in %.2fs\n",
utils.Pluralize(int64(s.ItemsShown), "item"),
utils.Pluralize(int64(s.FeedsCount), "feed"),
s.FeedsCached,
s.FeedsFetched,
utils.Pluralize(int64(s.ItemsCount), "item"),
f.time.Now().Sub(s.Start).Seconds(),
)
}

func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config, summary *RunSummary) ([]*FeedItem, error) {
var err error
lists := make([]string, 0)
if opts.List != "" {
Expand All @@ -287,6 +336,7 @@ func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config) (
for i := range lists {
f.storage.LoadFeedsFromList(feeds, lists[i])
}
summary.FeedsCount = len(feeds)
cacheInfo, err := f.storage.LoadCacheInfo()
if err != nil {
return nil, utils.NewInternalError("failed to load cache info: " + err.Error())
Expand Down Expand Up @@ -342,6 +392,9 @@ func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config) (
if res.Changed {
ci.ETag = res.ETag
ci.LastCheck = f.time.Now()
summary.FeedsFetched++
} else {
summary.FeedsCached++
}
if res.FetchAfter.After(ci.FetchAfter) {
ci.FetchAfter = res.FetchAfter
Expand All @@ -353,6 +406,7 @@ func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config) (
if err != nil {
f.printer.ErrPrintln("failed to save cache informaton:", err)
}
summary.ItemsCount = len(items)
return items, nil
}

Expand Down Expand Up @@ -397,7 +451,6 @@ func (f *TerminalFeed) fetchFeed(feed *storage.CacheInfoItem) (*FetchResult, err
return nil, err
}
defer res.Body.Close()

if res.StatusCode == http.StatusNotModified {
return &FetchResult{
Changed: false,
Expand Down Expand Up @@ -456,7 +509,7 @@ func parseMaxAge(cacheControl string) time.Duration {
if strings.HasPrefix(part, "max-age=") {
seconds, err := strconv.ParseInt(part[8:], 10, 64)
if err == nil {
return time.Duration(seconds) * time.Second
return time.Duration(max(seconds, 60)) * time.Second
}
break
}
Expand Down
1 change: 1 addition & 0 deletions internal/storage/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
Version string `json:"version"`
LastRun time.Time `json:"lastRun"`
Styling uint8 `json:"styling"` // 0: default, 1: enabled, 2: disabled
Summary uint8 `json:"summary"` // 0: disabled, 1: enabled
ColorMap map[uint8]uint8 `json:"colorMap"`
}

Expand Down

0 comments on commit 75002eb

Please sign in to comment.