diff --git a/.gitignore b/.gitignore index bf46d43..d491bb1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Makefile bin/ TODO.md +tut diff --git a/README.md b/README.md index 53c23be..3496dd0 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t * `:requests` see following requests * `:saved` alias for bookmarks * `:tag` followed by the hashtag e.g. `:tag linux` +* `:tags` list of followed tags * `:unfollow-tag` followed by the hashtag to unfollow e.g. `:unfollow-tag tut` * `:user` followed by a username e.g. `:user rasmus` to narrow a search include * `:window` switch window by index (zero indexed) e.g. `:window 0` for the first window. @@ -169,14 +170,18 @@ Commands: example-config - creates the default configuration file in the current directory and names it ./config.example.ini Flags: - --help -h - prints this message - --version -v - prints the version - --new-user -n - add one more user to tut - --user -u - login directly to user named - Don't use a = between --user and the - If two users are named the same. Use full name like tut@fosstodon.org + -h --help prints this message + -v --version prints the version + -n --new-user add one more user to tut + -c --config load config.ini from + -d --config-dir load all config from + -u --user login directly to user named + If two users are named the same. Use full name like tut@fosstodon.org ``` +If you don't want to set `--config` or `--config-dir` everytime you can set +the environment variables `TUT_CONF` and `TUT_CONF_DIR` instead. + ## Templates You can customise how toots and user profiles are displayed with a Go [text/template](https://pkg.go.dev/text/template). diff --git a/api/feed.go b/api/feed.go index b869aab..29a4b34 100644 --- a/api/feed.go +++ b/api/feed.go @@ -2,6 +2,7 @@ package api import ( "context" + "strings" "github.com/RasmusLindroth/go-mastodon" ) @@ -156,7 +157,14 @@ func (ac *AccountClient) GetConversations(pg *mastodon.Pagination) ([]Item, erro func (ac *AccountClient) GetUsers(search string) ([]Item, error) { var items []Item - users, err := ac.Client.AccountsSearch(context.Background(), search, 10) + var users []*mastodon.Account + var err error + if strings.HasPrefix(search, "@") && len(strings.Split(search, "@")) == 3 { + users, err = ac.Client.AccountsSearch(context.Background(), search, 10, true) + } + if len(users) == 0 || err != nil { + users, err = ac.Client.AccountsSearch(context.Background(), search, 10, false) + } if err != nil { return items, err } @@ -257,6 +265,18 @@ func (ac *AccountClient) GetUserPinned(id mastodon.ID) ([]Item, error) { return items, nil } +func (ac *AccountClient) GetTags(pg *mastodon.Pagination) ([]Item, error) { + var items []Item + tags, err := ac.Client.TagsFollowed(context.Background(), pg) + if err != nil { + return items, err + } + for _, t := range tags { + items = append(items, NewTagItem(t)) + } + return items, nil +} + func (ac *AccountClient) GetLists() ([]Item, error) { var items []Item lists, err := ac.Client.GetLists(context.Background()) @@ -302,3 +322,22 @@ func (ac *AccountClient) GetTag(pg *mastodon.Pagination, search string) ([]Item, } return ac.getStatusSimilar(fn, "public") } + +func (ac *AccountClient) GetTagMultiple(pg *mastodon.Pagination, search string) ([]Item, error) { + fn := func() ([]*mastodon.Status, error) { + var s string + td := mastodon.TagData{} + parts := strings.Split(search, " ") + for i, p := range parts { + if i == 0 { + s = p + continue + } + if len(p) > 0 { + td.Any = append(td.Any, p) + } + } + return ac.Client.GetTimelineHashtagMultiple(context.Background(), s, false, &td, pg) + } + return ac.getStatusSimilar(fn, "public") +} diff --git a/api/item.go b/api/item.go index 04c9722..9f92139 100644 --- a/api/item.go +++ b/api/item.go @@ -392,3 +392,44 @@ func (s *ListItem) Filtered() (bool, string) { func (n *ListItem) Pinned() bool { return false } + +func NewTagItem(item *mastodon.Tag) Item { + return &TagItem{id: newID(), item: item, showSpoiler: true} +} + +type TagItem struct { + id uint + item *mastodon.Tag + showSpoiler bool +} + +func (t *TagItem) ID() uint { + return t.id +} + +func (t *TagItem) Type() MastodonType { + return TagType +} + +func (t *TagItem) ToggleSpoiler() { +} + +func (t *TagItem) ShowSpoiler() bool { + return true +} + +func (t *TagItem) Raw() interface{} { + return t.item +} + +func (t *TagItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) { + return nil, nil, nil, 0 +} + +func (t *TagItem) Filtered() (bool, string) { + return false, "" +} + +func (t *TagItem) Pinned() bool { + return false +} diff --git a/api/stream.go b/api/stream.go index bb70645..a2d908c 100644 --- a/api/stream.go +++ b/api/stream.go @@ -16,6 +16,7 @@ const ( ProfileType NotificationType ListsType + TagType ) type StreamType uint @@ -193,6 +194,8 @@ func (ac *AccountClient) RemoveGenericReceiver(rec *Receiver, st StreamType, dat id = "LocalStream" case FederatedStream: id = "FederatedStream" + case DirectStream: + id = "DirectStream" case TagStream: id = "TagStream" + data case ListStream: diff --git a/api/tags.go b/api/tags.go index 348675c..b8fab99 100644 --- a/api/tags.go +++ b/api/tags.go @@ -3,6 +3,8 @@ package api import ( "context" "errors" + + "github.com/RasmusLindroth/go-mastodon" ) func (ac *AccountClient) FollowTag(tag string) error { @@ -33,3 +35,17 @@ func (ac *AccountClient) UnfollowTag(tag string) error { } return nil } + +func (ac *AccountClient) TagToggleFollow(tag *mastodon.Tag) (*mastodon.Tag, error) { + var t *mastodon.Tag + var err error + switch tag.Following { + case true: + t, err = ac.Client.TagUnfollow(context.Background(), tag.Name) + case false: + t, err = ac.Client.TagFollow(context.Background(), tag.Name) + default: + t, err = ac.Client.TagFollow(context.Background(), tag.Name) + } + return t, err +} diff --git a/auth/add.go b/auth/add.go index 44adca5..66ba46b 100644 --- a/auth/add.go +++ b/auth/add.go @@ -35,7 +35,7 @@ func AddAccount(ad *AccountData) *mastodon.Client { }) _, err = client.GetInstance(context.Background()) if err != nil { - fmt.Printf("\nCouldn't connect to instance: %s\nTry again or press ^C.\n", server) + fmt.Printf("\nCouldn't connect to instance %s:\n%s\nTry again or press ^C.\n", server, err) fmt.Println("--------------------------------------------------------------") } else { break diff --git a/config.example.ini b/config.example.ini index 946f019..46a20ca 100644 --- a/config.example.ini +++ b/config.example.ini @@ -152,7 +152,8 @@ leader-timeout=1000 # Available commands: home, direct, local, federated, clear-notifications, # compose, edit, history, blocking, bookmarks, saved, favorited, boosts, # favorites, following, followers, muting, newer, preferences, profile, -# notifications, lists, tag, window, list-placement, list-split, proportions +# notifications, lists, tag, tags, window, list-placement, list-split, +# proportions # # The shortcuts are up to you, but keep them quite short and make sure they # don't collide. If you have one shortcut that is "f" and an other one that is @@ -666,6 +667,14 @@ link-open="[O]pen",'o','O' # default="[Y]ank",'y','Y' link-yank="[Y]ank",'y','Y' +# Open tag feed +# default="[O]pen",'o','O' +tag-open-feed="[O]pen",'o','O' + +# Toggle follow on tag +# default="[F]ollow","Un[F]ollow",'f','F' +tag-follow="[F]ollow","Un[F]ollow",'f','F' + # Edit spoiler text on new toot # default="[C]W text",'c','C' compose-edit-spoiler="[C]W text",'c','C' diff --git a/config/config.go b/config/config.go index eb213ff..8b081ea 100644 --- a/config/config.go +++ b/config/config.go @@ -75,6 +75,7 @@ const ( LeaderNotifications LeaderLists LeaderTag + LeaderTags LeaderHistory LeaderUser LeaderWindow @@ -387,6 +388,9 @@ type Input struct { ListUserAdd Key ListUserDelete Key + TagOpenFeed Key + TagFollow Key + LinkOpen Key LinkYank Key @@ -439,7 +443,7 @@ func parseColor(input string, def string, xrdb map[string]string) tcell.Color { return tcell.GetColor(input) } -func parseStyle(cfg *ini.File) Style { +func parseStyle(cfg *ini.File, cnfPath string, cnfDir string) Style { var xrdbColors map[string]string xrdbMap, _ := GetXrdbColors() prefix := cfg.Section("style").Key("xrdb-prefix").String() @@ -464,7 +468,7 @@ func parseStyle(cfg *ini.File) Style { style := Style{} theme := cfg.Section("style").Key("theme").String() if theme != "none" && theme != "" { - bundled, local, err := getThemes() + bundled, local, err := getThemes(cnfPath, cnfDir) if err != nil { log.Fatalf("Couldn't load themes. Error: %s\n", err) } @@ -488,7 +492,7 @@ func parseStyle(cfg *ini.File) Style { if !found { log.Fatalf("Couldn't find theme %s\n", theme) } - tcfg, err := getTheme(theme, isLocal) + tcfg, err := getTheme(theme, isLocal, cnfDir) if err != nil { log.Fatalf("Couldn't load theme. Error: %s\n", err) } @@ -895,6 +899,8 @@ func parseGeneral(cfg *ini.File) General { case "tag": la.Command = LeaderTag la.Subaction = subaction + case "tags": + la.Command = LeaderTags case "list-placement": la.Command = LeaderListPlacement la.Subaction = subaction @@ -1150,9 +1156,9 @@ func parseNotifications(cfg *ini.File) Notification { return nc } -func parseTemplates(cfg *ini.File) Templates { +func parseTemplates(cfg *ini.File, cnfPath string, cnfDir string) Templates { var tootTmpl *template.Template - tootTmplPath, exists, err := checkConfig("toot.tmpl") + tootTmplPath, exists, err := checkConfig("toot.tmpl", cnfPath, cnfDir) if err != nil { log.Fatalf( fmt.Sprintf("Couldn't access toot.tmpl. Error: %v", err), @@ -1174,7 +1180,7 @@ func parseTemplates(cfg *ini.File) Templates { log.Fatalf("Couldn't parse toot.tmpl. Error: %v", err) } var userTmpl *template.Template - userTmplPath, exists, err := checkConfig("user.tmpl") + userTmplPath, exists, err := checkConfig("user.tmpl", cnfPath, cnfDir) if err != nil { log.Fatalf( fmt.Sprintf("Couldn't access user.tmpl. Error: %v", err), @@ -1278,6 +1284,9 @@ func parseInput(cfg *ini.File) Input { ListUserAdd: inputStrOrErr([]string{"\"[A]dd\"", "'a'", "'A'"}, false), ListUserDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false), + TagOpenFeed: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), + TagFollow: inputStrOrErr([]string{"\"[F]ollow\"", "\"Un[F]ollow\"", "'f'", "'F'"}, true), + LinkOpen: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false), LinkYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false), @@ -1356,6 +1365,9 @@ func parseInput(cfg *ini.File) Input { ic.ListUserAdd = inputOrErr(cfg, "list-user-add", false, ic.ListUserAdd) ic.ListUserDelete = inputOrErr(cfg, "list-user-delete", false, ic.ListUserDelete) + ic.TagOpenFeed = inputOrErr(cfg, "tag-open-feed", false, ic.TagOpenFeed) + ic.TagFollow = inputOrErr(cfg, "tag-follow", false, ic.TagFollow) + ic.LinkOpen = inputOrErr(cfg, "link-open", false, ic.LinkOpen) ic.LinkYank = inputOrErr(cfg, "link-yank", false, ic.LinkYank) @@ -1393,7 +1405,7 @@ func parseInput(cfg *ini.File) Input { return ic } -func parseConfig(filepath string) (Config, error) { +func parseConfig(filepath string, cnfPath string, cnfDir string) (Config, error) { cfg, err := ini.LoadSources(ini.LoadOptions{ SpaceBeforeInlineComment: true, AllowShadows: true, @@ -1404,11 +1416,11 @@ func parseConfig(filepath string) (Config, error) { } conf.General = parseGeneral(cfg) conf.Media = parseMedia(cfg) - conf.Style = parseStyle(cfg) + conf.Style = parseStyle(cfg, cnfPath, cnfDir) conf.OpenPattern = parseOpenPattern(cfg) conf.OpenCustom = parseCustom(cfg) conf.NotificationConfig = parseNotifications(cfg) - conf.Templates = parseTemplates(cfg) + conf.Templates = parseTemplates(cfg, cnfPath, cnfDir) conf.Input = parseInput(cfg) return conf, nil @@ -1423,7 +1435,25 @@ func createConfigDir() error { return os.MkdirAll(path, os.ModePerm) } -func checkConfig(filename string) (path string, exists bool, err error) { +func checkConfig(filename string, cnfPath string, cnfDir string) (path string, exists bool, err error) { + if cnfPath != "" && filename == "config.ini" { + _, err = os.Stat(cnfPath) + if os.IsNotExist(err) { + return cnfPath, false, nil + } else if err != nil { + return cnfPath, true, err + } + return cnfPath, true, err + } + if cnfDir != "" { + p := filepath.Join(cnfDir, filename) + if os.IsNotExist(err) { + return p, false, nil + } else if err != nil { + return p, true, err + } + return p, true, err + } cd, err := os.UserConfigDir() if err != nil { log.Fatalf("couldn't find config dir. Err %v", err) @@ -1452,7 +1482,7 @@ func CreateDefaultConfig(filepath string) error { return nil } -func getThemes() (bundled []string, local []string, err error) { +func getThemes(cnfPath string, cnfDir string) (bundled []string, local []string, err error) { entries, err := themesFS.ReadDir("themes") if err != nil { return bundled, local, err @@ -1464,18 +1494,23 @@ func getThemes() (bundled []string, local []string, err error) { fp := filepath.Join("themes/", entry.Name()) bundled = append(bundled, fp) } - _, exists, err := checkConfig("themes") + _, exists, err := checkConfig("themes", cnfPath, cnfDir) if err != nil { return bundled, local, err } if !exists { return bundled, local, err } - cd, err := os.UserConfigDir() - if err != nil { - log.Fatalf("couldn't find config dir. Err %v", err) + var dir string + if cnfDir != "" { + dir = filepath.Join(cnfDir, "themes") + } else { + cd, err := os.UserConfigDir() + if err != nil { + log.Fatalf("couldn't find config dir. Err %v", err) + } + dir = filepath.Join(cd, "/tut/themes") } - dir := cd + "/tut/themes" entries, err = os.ReadDir(dir) if err != nil { return bundled, local, err @@ -1490,17 +1525,23 @@ func getThemes() (bundled []string, local []string, err error) { return bundled, local, nil } -func getTheme(fname string, isLocal bool) (*ini.File, error) { +func getTheme(fname string, isLocal bool, cnfDir string) (*ini.File, error) { var f io.Reader var err error if isLocal { - var cd string - cd, err = os.UserConfigDir() - if err != nil { - log.Fatalf("couldn't find config dir. Err %v", err) + var dir string + if cnfDir != "" { + dir = filepath.Join(cnfDir, "themes") + } else { + cd, err := os.UserConfigDir() + if err != nil { + log.Fatalf("couldn't find config dir. Err %v", err) + } + dir = filepath.Join(cd, "/tut/themes") } - dir := cd + "/tut/themes" - f, err = os.Open(fmt.Sprintf("%s/%s.ini", dir, strings.TrimSpace(fname))) + f, err = os.Open( + filepath.Join(dir, fmt.Sprintf("%s.ini", strings.TrimSpace(fname))), + ) } else { f, err = themesFS.Open(fmt.Sprintf("themes/%s.ini", strings.TrimSpace(fname))) } diff --git a/config/default_config.go b/config/default_config.go index 0196e00..f59e641 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -154,7 +154,8 @@ leader-timeout=1000 # Available commands: home, direct, local, federated, clear-notifications, # compose, edit, history, blocking, bookmarks, saved, favorited, boosts, # favorites, following, followers, muting, newer, preferences, profile, -# notifications, lists, tag, window, list-placement, list-split, proportions +# notifications, lists, tag, tags, window, list-placement, list-split, +# proportions # # The shortcuts are up to you, but keep them quite short and make sure they # don't collide. If you have one shortcut that is "f" and an other one that is @@ -668,6 +669,14 @@ link-open="[O]pen",'o','O' # default="[Y]ank",'y','Y' link-yank="[Y]ank",'y','Y' +# Open tag feed +# default="[O]pen",'o','O' +tag-open-feed="[O]pen",'o','O' + +# Toggle follow on tag +# default="[F]ollow","Un[F]ollow",'f','F' +tag-follow="[F]ollow","Un[F]ollow",'f','F' + # Edit spoiler text on new toot # default="[C]W text",'c','C' compose-edit-spoiler="[C]W text",'c','C' diff --git a/config/help.tmpl b/config/help.tmpl index a2ac302..561cfb6 100644 --- a/config/help.tmpl +++ b/config/help.tmpl @@ -106,6 +106,9 @@ Here's a list of supported commands. {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tag{{ Flags "-" }}{{ Color .Style.Text }} tagname See toots for a tag e.g. :tag linux +{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tags{{ Flags "-" }}{{ Color .Style.Text }} tagname + List of followed tags + {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:unfollow-tag{{ Flags "-" }}{{ Color .Style.Text }} Followed by the hashtag to unfollow e.g. :unfollow-tag tut diff --git a/config/load.go b/config/load.go index 4e19083..3bb1882 100644 --- a/config/load.go +++ b/config/load.go @@ -5,13 +5,13 @@ import ( "os" ) -func Load() *Config { +func Load(cnfPath string, cnfDir string) *Config { err := createConfigDir() if err != nil { fmt.Printf("Couldn't create or access the configuration dir. Error: %v\n", err) os.Exit(1) } - path, exists, err := checkConfig("config.ini") + path, exists, err := checkConfig("config.ini", cnfPath, cnfDir) if err != nil { fmt.Printf("Couldn't access config.ini. Error: %v\n", err) os.Exit(1) @@ -23,7 +23,7 @@ func Load() *Config { os.Exit(1) } } - config, err := parseConfig(path) + config, err := parseConfig(path, cnfPath, cnfDir) if err != nil { fmt.Printf("Couldn't open or parse the config. Error: %v\n", err) os.Exit(1) diff --git a/feed/feed.go b/feed/feed.go index bd9c230..056b48b 100644 --- a/feed/feed.go +++ b/feed/feed.go @@ -4,6 +4,7 @@ import ( "context" "errors" "log" + "strings" "sync" "time" @@ -36,6 +37,7 @@ const ( Notification Saved Tag + Tags Thread TimelineFederated TimelineHome @@ -80,7 +82,7 @@ type Feed struct { Update chan DesktopNotificationType apiData *api.RequestData apiDataMux sync.Mutex - stream *api.Receiver + streams []*api.Receiver name string close func() } @@ -172,7 +174,7 @@ func (f *Feed) LoadOlder() { } func (f *Feed) HasStream() bool { - return f.stream != nil + return len(f.streams) > 0 } func (f *Feed) Close() { @@ -614,16 +616,30 @@ func (f *Feed) startStream(rec *api.Receiver, timeline string, err error) { if err != nil { log.Fatalln("Couldn't open stream") } - f.stream = rec + f.streams = append(f.streams, rec) go func() { - for e := range f.stream.Ch { + for e := range rec.Ch { switch t := e.(type) { case *mastodon.UpdateEvent: s := api.NewStatusItem(t.Status, f.accountClient.Filters, timeline, false) f.itemsMux.Lock() - f.items = append([]api.Item{s}, f.items...) - f.Updated(DesktopNotificationPost) - f.apiData.MinID = t.Status.ID + found := false + if len(f.streams) > 0 { + for _, item := range f.items { + switch v := item.Raw().(type) { + case *mastodon.Status: + if t.Status.ID == v.ID { + found = true + break + } + } + } + } + if !found { + f.items = append([]api.Item{s}, f.items...) + f.Updated(DesktopNotificationPost) + f.apiData.MinID = t.Status.ID + } f.itemsMux.Unlock() } } @@ -634,9 +650,9 @@ func (f *Feed) startStreamNotification(rec *api.Receiver, timeline string, err e if err != nil { log.Fatalln("Couldn't open stream") } - f.stream = rec + f.streams = append(f.streams, rec) go func() { - for e := range f.stream.Ch { + for e := range rec.Ch { switch t := e.(type) { case *mastodon.NotificationEvent: rel, err := f.accountClient.Client.GetAccountRelationships(context.Background(), []string{string(t.Notification.Account.ID)}) @@ -700,7 +716,11 @@ func NewTimelineHome(ac *api.AccountClient) *Feed { feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) } feed.startStream(feed.accountClient.NewHomeStream()) - feed.close = func() { feed.accountClient.RemoveHomeReceiver(feed.stream) } + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveHomeReceiver(s) + } + } return feed } @@ -710,7 +730,11 @@ func NewTimelineFederated(ac *api.AccountClient) *Feed { feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineFederated) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineFederated) } feed.startStream(feed.accountClient.NewFederatedStream()) - feed.close = func() { feed.accountClient.RemoveFederatedReceiver(feed.stream) } + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveFederatedReceiver(s) + } + } return feed } @@ -720,8 +744,11 @@ func NewTimelineLocal(ac *api.AccountClient) *Feed { feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineLocal) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineLocal) } feed.startStream(feed.accountClient.NewLocalStream()) - feed.close = func() { feed.accountClient.RemoveLocalReceiver(feed.stream) } - + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveLocalReceiver(s) + } + } return feed } @@ -730,7 +757,11 @@ func NewConversations(ac *api.AccountClient) *Feed { feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetConversations) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetConversations) } feed.startStream(feed.accountClient.NewDirectStream()) - feed.close = func() { feed.accountClient.RemoveConversationReceiver(feed.stream) } + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveConversationReceiver(s) + } + } return feed } @@ -740,7 +771,11 @@ func NewNotifications(ac *api.AccountClient) *Feed { feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetNotifications) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetNotifications) } feed.startStreamNotification(feed.accountClient.NewHomeStream()) - feed.close = func() { feed.accountClient.RemoveHomeReceiver(feed.stream) } + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveHomeReceiver(s) + } + } return feed } @@ -810,11 +845,40 @@ func NewHistory(ac *api.AccountClient, status *mastodon.Status) *Feed { func NewTag(ac *api.AccountClient, search string) *Feed { feed := newFeed(ac, Tag) - feed.name = search - feed.loadNewer = func() { feed.newerSearchPG(feed.accountClient.GetTag, search) } - feed.loadOlder = func() { feed.olderSearchPG(feed.accountClient.GetTag, search) } - feed.startStream(feed.accountClient.NewTagStream(search)) - feed.close = func() { feed.accountClient.RemoveTagReceiver(feed.stream, search) } + parts := strings.Split(search, " ") + var tparts []string + for _, p := range parts { + p = strings.TrimPrefix(p, "#") + if len(p) > 0 { + tparts = append(tparts, p) + } + } + joined := strings.Join(tparts, " ") + feed.name = joined + feed.loadNewer = func() { feed.newerSearchPG(feed.accountClient.GetTagMultiple, joined) } + feed.loadOlder = func() { feed.olderSearchPG(feed.accountClient.GetTagMultiple, joined) } + for _, t := range tparts { + feed.startStream(feed.accountClient.NewTagStream(t)) + } + feed.close = func() { + for i, s := range feed.streams { + feed.accountClient.RemoveTagReceiver(s, tparts[i]) + } + } + + return feed +} + +func NewTags(ac *api.AccountClient) *Feed { + feed := newFeed(ac, Tags) + once := true + feed.loadNewer = func() { + if once { + feed.normalNewer(feed.accountClient.GetTags) + } + once = false + } + feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTags) } return feed } @@ -838,7 +902,11 @@ func NewList(ac *api.AccountClient, list *mastodon.List) *Feed { feed.loadNewer = func() { feed.normalNewerID(feed.accountClient.GetListStatuses, list.ID) } feed.loadOlder = func() { feed.normalOlderID(feed.accountClient.GetListStatuses, list.ID) } feed.startStream(feed.accountClient.NewListStream(list.ID)) - feed.close = func() { feed.accountClient.RemoveListReceiver(feed.stream, list.ID) } + feed.close = func() { + for _, s := range feed.streams { + feed.accountClient.RemoveListReceiver(s, list.ID) + } + } return feed } diff --git a/go.mod b/go.mod index 6df0b6d..389019b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/RasmusLindroth/tut go 1.18 require ( - github.com/RasmusLindroth/go-mastodon v0.0.11 + github.com/RasmusLindroth/go-mastodon v0.0.14 github.com/atotto/clipboard v0.1.4 github.com/gdamore/tcell/v2 v2.5.3 github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 @@ -13,6 +13,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.6 github.com/rivo/tview v0.0.0-20221117065207-09f052e6ca98 github.com/rivo/uniseg v0.4.3 + github.com/spf13/pflag v1.0.5 golang.org/x/net v0.2.0 gopkg.in/ini.v1 v1.67.0 ) diff --git a/go.sum b/go.sum index 1135336..27c4e63 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -github.com/RasmusLindroth/go-mastodon v0.0.10 h1:huGNcPn5SASfJDhBL4drKL0PFJ29+hqjCroIrkf2R0E= -github.com/RasmusLindroth/go-mastodon v0.0.10/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE= -github.com/RasmusLindroth/go-mastodon v0.0.11 h1:Qcad+urrDVrboo13ayoHG3DcwsGK/07qR6IfOPPMilY= -github.com/RasmusLindroth/go-mastodon v0.0.11/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE= +github.com/RasmusLindroth/go-mastodon v0.0.14 h1:lmJEgXpYC7uS/8xEwg6yQ+blQ2iyXE4K0xWJitVmI+U= +github.com/RasmusLindroth/go-mastodon v0.0.14/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -45,6 +43,8 @@ github.com/rivo/tview v0.0.0-20221117065207-09f052e6ca98/go.mod h1:YX2wUZOcJGOIy github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/main.go b/main.go index 6b96a5b..b32978d 100644 --- a/main.go +++ b/main.go @@ -8,18 +8,18 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.22" +const version = "1.0.23" func main() { util.SetTerminalTitle("tut") util.MakeDirs() - newUser, selectedUser := ui.CliView(version) + newUser, selectedUser, cnfPath, cnfDir := ui.CliView(version) accs := auth.StartAuth(newUser) app := tview.NewApplication() t := &ui.Tut{ App: app, - Config: config.Load(), + Config: config.Load(cnfPath, cnfDir), } if t.Config.General.MouseSupport { app.EnableMouse(true) diff --git a/ui/cliview.go b/ui/cliview.go index 5e711f7..9fcf031 100644 --- a/ui/cliview.go +++ b/ui/cliview.go @@ -7,52 +7,93 @@ import ( "strings" "github.com/RasmusLindroth/tut/config" + "github.com/RasmusLindroth/tut/util" + "github.com/spf13/pflag" ) -func CliView(version string) (newUser bool, selectedUser string) { +func CliView(version string) (newUser bool, selectedUser string, confPath string, confDir string) { + showHelp := pflag.BoolP("help", "h", false, "config path") + showVersion := pflag.BoolP("version", "v", false, "config path") + nu := pflag.BoolP("new-user", "n", false, "add one more user to tut") + user := pflag.StringP("user", "u", "", "login directly to user named ``") + cnf := pflag.StringP("config", "c", "", "load config.ini from ``") + cnfDir := pflag.StringP("config-dir", "d", "", "load all config from ``") + pflag.Parse() + if len(os.Args) > 1 { switch os.Args[1] { case "example-config": config.CreateDefaultConfig("./config.example.ini") os.Exit(0) - case "--new-user", "-n": - newUser = true - case "--user", "-u": - if len(os.Args) > 2 { - name := os.Args[2] - selectedUser = strings.TrimSpace(name) - } else { - log.Fatalln("--user/-u must be followed by a user name. Like -u tut") - } - case "--help", "-h": - fmt.Print("tut - a TUI for Mastodon with vim inspired keys.\n\n") - fmt.Print("Usage:\n") - fmt.Print("\tTo run the program you just have to write tut\n\n") - - fmt.Print("Commands:\n") - fmt.Print("\texample-config - creates the default configuration file in the current directory and names it ./config.example.ini\n\n") - - fmt.Print("Flags:\n") - fmt.Print("\t--help -h - prints this message\n") - fmt.Print("\t--version -v - prints the version\n") - fmt.Print("\t--new-user -n - add one more user to tut\n") - fmt.Print("\t--user -u - login directly to user named \n") - fmt.Print("\t\tDon't use a = between --user and the \n") - fmt.Print("\t\tIf two users are named the same. Use full name like tut@fosstodon.org\n\n") - - fmt.Print("Configuration:\n") - fmt.Printf("\tThe config is located in XDG_CONFIG_HOME/tut/config.ini which usually equals to ~/.config/tut/config.ini.\n") - fmt.Printf("\tThe program will generate the file the first time you run tut. The file has comments which exmplains what each configuration option does.\n\n") - - fmt.Print("Contact info for issues or questions:\n") - fmt.Printf("\t@tut@fosstodon.org\n\t@rasmus@mastodon.acc.sunet.se\n\trasmus@lindroth.xyz\n") - fmt.Printf("\thttps://github.com/RasmusLindroth/tut\n") - os.Exit(0) - case "--version", "-v": - fmt.Printf("tut version %s\n", version) - fmt.Printf("https://github.com/RasmusLindroth/tut\n") - os.Exit(0) } } - return newUser, selectedUser + if nu != nil && *nu { + newUser = true + } + if user != nil && *user != "" { + selectedUser = strings.TrimSpace(*user) + } + if cnf != nil && *cnf != "" { + cp := strings.TrimSpace(*cnf) + abs, err := util.GetAbsPath(cp) + if err != nil { + log.Fatalln(err) + } + confPath = abs + } else if os.Getenv("TUT_CONF") != "" { + cp := os.Getenv("TUT_CONF") + abs, err := util.GetAbsPath(cp) + if err != nil { + log.Fatalln(err) + } + confPath = abs + } + if cnfDir != nil && *cnfDir != "" { + cd := strings.TrimSpace(*cnfDir) + abs, err := util.GetAbsPath(cd) + if err != nil { + log.Fatalln(err) + } + confDir = abs + } else if os.Getenv("TUT_CONF_DIR") != "" { + cd := os.Getenv("TUT_CONF_DIR") + abs, err := util.GetAbsPath(cd) + if err != nil { + log.Fatalln(err) + } + confDir = abs + } + if showHelp != nil && *showHelp { + fmt.Print("tut - a TUI for Mastodon with vim inspired keys.\n\n") + fmt.Print("Usage:\n") + fmt.Print("\tTo run the program you just have to write tut\n\n") + + fmt.Print("Commands:\n") + fmt.Print("\texample-config - creates the default configuration file in the current directory and names it ./config.example.ini\n\n") + + fmt.Print("Flags:\n") + fmt.Print("\t-h --help prints this message\n") + fmt.Print("\t-v --version prints the version\n") + fmt.Print("\t-n --new-user add one more user to tut\n") + fmt.Print("\t-c --config load config.ini from \n") + fmt.Print("\t-d --config-dir load all config from \n") + fmt.Print("\t-u --user login directly to user named \n") + fmt.Print("\t\tIf two users are named the same. Use full name like tut@fosstodon.org\n\n") + + fmt.Print("Configuration:\n") + fmt.Printf("\tThe config is located in XDG_CONFIG_HOME/tut/config.ini which usually equals to ~/.config/tut/config.ini.\n") + fmt.Printf("\tThe program will generate the file the first time you run tut. The file has comments which exmplains what each configuration option does.\n\n") + + fmt.Print("Contact info for issues or questions:\n") + fmt.Printf("\t@tut@fosstodon.org\n\t@rasmus@mastodon.acc.sunet.se\n\trasmus@lindroth.xyz\n") + fmt.Printf("\thttps://github.com/RasmusLindroth/tut\n") + os.Exit(0) + } + if showVersion != nil && *showVersion { + fmt.Printf("tut version %s\n", version) + fmt.Printf("https://github.com/RasmusLindroth/tut\n") + os.Exit(0) + + } + return newUser, selectedUser, confPath, confDir } diff --git a/ui/cmdbar.go b/ui/cmdbar.go index 214253f..a40c045 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -179,11 +179,14 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { if len(parts) < 2 { break } - tag := strings.TrimSpace(strings.TrimPrefix(parts[1], "#")) - if len(tag) == 0 { + tParts := strings.TrimSpace(strings.Join(parts[1:], " ")) + if len(tParts) == 0 { break } - c.tutView.TagCommand(tag) + c.tutView.TagCommand(tParts) + c.Back() + case ":tags": + c.tutView.TagsCommand() c.Back() case ":window": if len(parts) < 2 { diff --git a/ui/commands.go b/ui/commands.go index 116ade0..8f1b39f 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -102,6 +102,12 @@ func (tv *TutView) TagCommand(tag string) { ) } +func (tv *TutView) TagsCommand() { + tv.Timeline.AddFeed( + NewTagsFeed(tv), + ) +} + func (tv *TutView) TagFollowCommand(tag string) { err := tv.tut.Client.FollowTag(tag) if err != nil { diff --git a/ui/feed.go b/ui/feed.go index 6feb5e3..05775e0 100644 --- a/ui/feed.go +++ b/ui/feed.go @@ -45,11 +45,10 @@ func outFocus(l *tview.List, style config.Style) { } type Feed struct { - tutView *TutView - Data *feed.Feed - ListIndex int - List *FeedList - Content *FeedContent + tutView *TutView + Data *feed.Feed + List *FeedList + Content *FeedContent } func (f *Feed) ListInFocus() { @@ -139,11 +138,10 @@ func NewHomeFeed(tv *TutView) *Feed { f := feed.NewTimelineHome(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -154,11 +152,10 @@ func NewFederatedFeed(tv *TutView) *Feed { f := feed.NewTimelineFederated(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -169,11 +166,10 @@ func NewLocalFeed(tv *TutView) *Feed { f := feed.NewTimelineLocal(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -184,11 +180,10 @@ func NewNotificationFeed(tv *TutView) *Feed { f := feed.NewNotifications(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -200,11 +195,10 @@ func NewThreadFeed(tv *TutView, item api.Item) *Feed { f := feed.NewThread(tv.tut.Client, status) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } for i, s := range f.List() { main, symbol := DrawListItem(tv.tut.Config, s) @@ -223,11 +217,10 @@ func NewHistoryFeed(tv *TutView, item api.Item) *Feed { f := feed.NewHistory(tv.tut.Client, status) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } for _, s := range f.List() { main, symbol := DrawListItem(tv.tut.Config, s) @@ -243,11 +236,10 @@ func NewConversationsFeed(tv *TutView) *Feed { f := feed.NewConversations(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -262,11 +254,10 @@ func NewUserFeed(tv *TutView, item api.Item) *Feed { f := feed.NewUserProfile(tv.tut.Client, u) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -277,11 +268,10 @@ func NewUserSearchFeed(tv *TutView, search string) *Feed { f := feed.NewUserSearch(tv.tut.Client, search) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } for _, s := range f.List() { main, symbol := DrawListItem(tv.tut.Config, s) @@ -296,25 +286,38 @@ func NewTagFeed(tv *TutView, search string) *Feed { f := feed.NewTag(tv.tut.Client, search) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() return fd } + +func NewTagsFeed(tv *TutView) *Feed { + f := feed.NewTags(tv.tut.Client) + f.LoadNewer() + fd := &Feed{ + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), + } + go fd.update() + + return fd +} + func NewListsFeed(tv *TutView) *Feed { f := feed.NewListList(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -325,11 +328,10 @@ func NewListFeed(tv *TutView, l *mastodon.List) *Feed { f := feed.NewList(tv.tut.Client, l) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -340,11 +342,10 @@ func NewUsersInListFeed(tv *TutView, l *mastodon.List) *Feed { f := feed.NewUsersInList(tv.tut.Client, l) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -355,11 +356,10 @@ func NewUsersAddListFeed(tv *TutView, l *mastodon.List) *Feed { f := feed.NewUsersAddList(tv.tut.Client, l) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -370,11 +370,10 @@ func NewFavoritedFeed(tv *TutView) *Feed { f := feed.NewFavorites(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -385,11 +384,10 @@ func NewBookmarksFeed(tv *TutView) *Feed { f := feed.NewBookmarks(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -400,11 +398,10 @@ func NewFavoritesStatus(tv *TutView, id mastodon.ID) *Feed { f := feed.NewFavoritesStatus(tv.tut.Client, id) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -415,11 +412,10 @@ func NewBoosts(tv *TutView, id mastodon.ID) *Feed { f := feed.NewBoosts(tv.tut.Client, id) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -430,11 +426,10 @@ func NewFollowers(tv *TutView, id mastodon.ID) *Feed { f := feed.NewFollowers(tv.tut.Client, id) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -445,11 +440,10 @@ func NewFollowing(tv *TutView, id mastodon.ID) *Feed { f := feed.NewFollowing(tv.tut.Client, id) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -460,11 +454,10 @@ func NewBlocking(tv *TutView) *Feed { f := feed.NewBlocking(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -475,11 +468,10 @@ func NewMuting(tv *TutView) *Feed { f := feed.NewMuting(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() @@ -490,11 +482,10 @@ func NewFollowRequests(tv *TutView) *Feed { f := feed.NewFollowRequests(tv.tut.Client) f.LoadNewer() fd := &Feed{ - tutView: tv, - Data: f, - ListIndex: 0, - List: NewFeedList(tv.tut, f.StickyCount()), - Content: NewFeedContent(tv.tut), + tutView: tv, + Data: f, + List: NewFeedList(tv.tut, f.StickyCount()), + Content: NewFeedContent(tv.tut), } go fd.update() diff --git a/ui/input.go b/ui/input.go index 82655b6..f231eb3 100644 --- a/ui/input.go +++ b/ui/input.go @@ -151,6 +151,8 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey { tv.ListsCommand() case config.LeaderTag: tv.TagCommand(subaction) + case config.LeaderTags: + tv.TagsCommand() case config.LeaderWindow: tv.WindowCommand(subaction) case config.LeaderListPlacement: @@ -341,6 +343,9 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey { case api.ListsType: ld := item.Raw().(*mastodon.List) return tv.InputList(event, ld) + case api.TagType: + tag := item.Raw().(*mastodon.Tag) + return tv.InputTag(event, tag) } return event } @@ -741,6 +746,34 @@ func (tv *TutView) InputList(event *tcell.EventKey, list *mastodon.List) *tcell. return event } +func (tv *TutView) InputTag(event *tcell.EventKey, tag *mastodon.Tag) *tcell.EventKey { + if tv.tut.Config.Input.TagOpenFeed.Match(event.Key(), event.Rune()) || + tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) { + tv.Timeline.AddFeed(NewTagFeed(tv, tag.Name)) + return nil + } + if tv.tut.Config.Input.TagFollow.Match(event.Key(), event.Rune()) { + txt := "follow" + if tag.Following != nil && tag.Following == true { + txt = "unfollow" + } + tv.ModalView.Run(fmt.Sprintf("Do you want to %s #%s?", txt, tag.Name), + func() { + nt, err := tv.tut.Client.TagToggleFollow(tag) + if err != nil { + tv.ShowError( + fmt.Sprintf("Couldn't %s tag. Error: %v\n", txt, err), + ) + return + } + *tag = *nt + tv.RedrawControls() + }) + return nil + } + return event +} + func (tv *TutView) InputLinkView(event *tcell.EventKey) *tcell.EventKey { if tv.tut.Config.Input.GlobalDown.Match(event.Key(), event.Rune()) { tv.LinkView.Next() diff --git a/ui/item.go b/ui/item.go index 773c3b5..b8083f2 100644 --- a/ui/item.go +++ b/ui/item.go @@ -68,6 +68,9 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) { case api.ListsType: a := item.Raw().(*mastodon.List) return tview.Escape(a.Title), "" + case api.TagType: + a := item.Raw().(*mastodon.Tag) + return tview.Escape("#" + a.Name), "" default: return "", "" } @@ -105,6 +108,8 @@ func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview. drawNotification(tv, item, item.Raw().(*api.NotificationData), main, controls) case api.ListsType: drawList(tv, item.Raw().(*mastodon.List), main, controls) + case api.TagType: + drawTag(tv, item.Raw().(*mastodon.Tag), main, controls) } } @@ -135,6 +140,8 @@ func DrawItemControls(tv *TutView, item api.Item, controls *tview.Flex, ft feed. drawNotification(tv, item, item.Raw().(*api.NotificationData), nil, controls) case api.ListsType: drawList(tv, item.Raw().(*mastodon.List), nil, controls) + case api.TagType: + drawTag(tv, item.Raw().(*mastodon.Tag), nil, controls) } } diff --git a/ui/item_list.go b/ui/item_list.go index 2e1becf..c2a9911 100644 --- a/ui/item_list.go +++ b/ui/item_list.go @@ -25,5 +25,7 @@ func drawList(tv *TutView, data *mastodon.List, main *tview.TextView, controls * } } - main.SetText(fmt.Sprintf("List %s", tview.Escape(data.Title))) + if main != nil { + main.SetText(fmt.Sprintf("List %s", tview.Escape(data.Title))) + } } diff --git a/ui/item_tag.go b/ui/item_tag.go new file mode 100644 index 0000000..f0acde6 --- /dev/null +++ b/ui/item_tag.go @@ -0,0 +1,47 @@ +package ui + +import ( + "fmt" + "strconv" + "time" + + "github.com/RasmusLindroth/go-mastodon" + "github.com/rivo/tview" +) + +type Tag struct { +} + +func drawTag(tv *TutView, data *mastodon.Tag, main *tview.TextView, controls *tview.Flex) { + controls.Clear() + var items []Control + items = append(items, NewControl(tv.tut.Config, tv.tut.Config.Input.TagOpenFeed, true)) + if data.Following != nil && data.Following == true { + items = append(items, NewControl(tv.tut.Config, tv.tut.Config.Input.TagFollow, false)) + } else { + items = append(items, NewControl(tv.tut.Config, tv.tut.Config.Input.TagFollow, true)) + + } + controls.Clear() + for i, item := range items { + if i < len(items)-1 { + controls.AddItem(NewControlButton(tv, item), item.Len+1, 0, false) + } else { + controls.AddItem(NewControlButton(tv, item), item.Len, 0, false) + } + } + if main != nil { + out := fmt.Sprintf("#%s\n\n", tview.Escape(data.Name)) + for _, h := range data.History { + i, err := strconv.ParseInt(h.Day, 10, 64) + if err != nil { + continue + } + tm := time.Unix(i, 0) + out += fmt.Sprintf("%s: %s accounts and %s toots\n", + tm.Format("2006-01-02"), h.Accounts, h.Uses) + } + main.SetText(out) + main.ScrollToBeginning() + } +} diff --git a/ui/timeline.go b/ui/timeline.go index f18cb4e..4b6c094 100644 --- a/ui/timeline.go +++ b/ui/timeline.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "strings" "github.com/RasmusLindroth/tut/feed" ) @@ -148,7 +149,11 @@ func (tl *Timeline) GetTitle() string { case feed.Notification: ct = "notifications" case feed.Tag: - ct = fmt.Sprintf("tag #%s", name) + parts := strings.Split(name, " ") + for i, p := range parts { + parts[i] = fmt.Sprintf("#%s", p) + } + ct = fmt.Sprintf("tag %s", strings.Join(parts, " ")) case feed.Thread: ct = "thread feed" case feed.History: diff --git a/util/util.go b/util/util.go index 133c1c2..eec7249 100644 --- a/util/util.go +++ b/util/util.go @@ -13,6 +13,18 @@ import ( "golang.org/x/net/html" ) +func GetAbsPath(path string) (string, error) { + if filepath.IsAbs(path) { + return path, nil + } + curr, err := os.Getwd() + if err != nil { + return "", err + } + np := filepath.Join(curr, path) + return np, nil +} + type URL struct { Text string URL string