Browse Source

1.0.23 (#201)

* Removing directstream shouldn't crash tut

* update version

* Ignore tut binary (#194)

When I do 'go build .' it yields a 'tut' file in the current directory
that's not currently ignored.

* Print error when failing to connect (#193)

When setting up, network errors wouldn't print any useful information:

  Instance: bsd.network

  Couldn't connect to instance: https://bsd.network
  Try again or press ^C.

Now:

  Instance: bsd.network

  Couldn't connect to instance https://bsd.network:
  Get "https://bsd.network/api/v1/instance": x509: certificate signed
  by unknown authority
  Try again or press ^C.

(Turns out I didn't have root certificates installed.)

* set config with flag and env

* list tags that you follow

* fix help

* update readme

* remove unused field

* user search and multiple tags

Co-authored-by: Sijmen J. Mulder <ik@sjmulder.nl>
pull/202/head
Rasmus Lindroth 3 years ago committed by GitHub
parent
commit
44814f18e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 15
      README.md
  3. 41
      api/feed.go
  4. 41
      api/item.go
  5. 3
      api/stream.go
  6. 16
      api/tags.go
  7. 2
      auth/add.go
  8. 11
      config.example.ini
  9. 77
      config/config.go
  10. 11
      config/default_config.go
  11. 3
      config/help.tmpl
  12. 6
      config/load.go
  13. 104
      feed/feed.go
  14. 3
      go.mod
  15. 8
      go.sum
  16. 6
      main.go
  17. 77
      ui/cliview.go
  18. 9
      ui/cmdbar.go
  19. 6
      ui/commands.go
  20. 39
      ui/feed.go
  21. 33
      ui/input.go
  22. 7
      ui/item.go
  23. 2
      ui/item_list.go
  24. 47
      ui/item_tag.go
  25. 7
      ui/timeline.go
  26. 12
      util/util.go

1
.gitignore vendored

@ -2,3 +2,4 @@
Makefile
bin/
TODO.md
tut

15
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 <name> -u <name> - login directly to user named <name>
Don't use a = between --user and the <name>
-h --help prints this message
-v --version prints the version
-n --new-user add one more user to tut
-c --config <path> load config.ini from <path>
-d --config-dir <path> load all config from <path>
-u --user <name> login directly to user named <name>
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).

41
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")
}

41
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
}

3
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:

16
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
}

2
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

11
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'

77
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
}
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 := cd + "/tut/themes"
dir = filepath.Join(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()
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 := cd + "/tut/themes"
f, err = os.Open(fmt.Sprintf("%s/%s.ini", dir, strings.TrimSpace(fname)))
dir = filepath.Join(cd, "/tut/themes")
}
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)))
}

11
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'

3
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

6
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)

104
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()
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
}

3
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
)

8
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=

6
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)

77
ui/cliview.go

@ -7,24 +7,63 @@ 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 `<name>`")
cnf := pflag.StringP("config", "c", "", "load config.ini from `<path>`")
cnfDir := pflag.StringP("config-dir", "d", "", "load all config from `<path>`")
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":
}
}
if nu != nil && *nu {
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":
}
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")
@ -33,11 +72,12 @@ func CliView(version string) (newUser bool, selectedUser string) {
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 <name> -u <name> - login directly to user named <name>\n")
fmt.Print("\t\tDon't use a = between --user and the <name>\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 <path> load config.ini from <path>\n")
fmt.Print("\t-d --config-dir <path> load all config from <path>\n")
fmt.Print("\t-u --user <name> login directly to user named <name>\n")
fmt.Print("\t\tIf two users are named the same. Use full name like tut@fosstodon.org\n\n")
fmt.Print("Configuration:\n")
@ -48,11 +88,12 @@ func CliView(version string) (newUser bool, selectedUser string) {
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":
}
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
return newUser, selectedUser, confPath, confDir
}

9
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 {

6
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 {

39
ui/feed.go

@ -47,7 +47,6 @@ func outFocus(l *tview.List, style config.Style) {
type Feed struct {
tutView *TutView
Data *feed.Feed
ListIndex int
List *FeedList
Content *FeedContent
}
@ -141,7 +140,6 @@ func NewHomeFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -156,7 +154,6 @@ func NewFederatedFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -171,7 +168,6 @@ func NewLocalFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -186,7 +182,6 @@ func NewNotificationFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -202,7 +197,6 @@ func NewThreadFeed(tv *TutView, item api.Item) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -225,7 +219,6 @@ func NewHistoryFeed(tv *TutView, item api.Item) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -245,7 +238,6 @@ func NewConversationsFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -264,7 +256,6 @@ func NewUserFeed(tv *TutView, item api.Item) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -279,7 +270,6 @@ func NewUserSearchFeed(tv *TutView, search string) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -298,7 +288,6 @@ func NewTagFeed(tv *TutView, search string) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -306,13 +295,27 @@ func NewTagFeed(tv *TutView, search string) *Feed {
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),
}
@ -327,7 +330,6 @@ func NewListFeed(tv *TutView, l *mastodon.List) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -342,7 +344,6 @@ func NewUsersInListFeed(tv *TutView, l *mastodon.List) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -357,7 +358,6 @@ func NewUsersAddListFeed(tv *TutView, l *mastodon.List) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -372,7 +372,6 @@ func NewFavoritedFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -387,7 +386,6 @@ func NewBookmarksFeed(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -402,7 +400,6 @@ func NewFavoritesStatus(tv *TutView, id mastodon.ID) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -417,7 +414,6 @@ func NewBoosts(tv *TutView, id mastodon.ID) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -432,7 +428,6 @@ func NewFollowers(tv *TutView, id mastodon.ID) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -447,7 +442,6 @@ func NewFollowing(tv *TutView, id mastodon.ID) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -462,7 +456,6 @@ func NewBlocking(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -477,7 +470,6 @@ func NewMuting(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
@ -492,7 +484,6 @@ func NewFollowRequests(tv *TutView) *Feed {
fd := &Feed{
tutView: tv,
Data: f,
ListIndex: 0,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}

33
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()

7
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)
}
}

2
ui/item_list.go

@ -25,5 +25,7 @@ func drawList(tv *TutView, data *mastodon.List, main *tview.TextView, controls *
}
}
if main != nil {
main.SetText(fmt.Sprintf("List %s", tview.Escape(data.Title)))
}
}

47
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()
}
}

7
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:

12
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

Loading…
Cancel
Save