Browse Source

1.0.7 (#147)

* fix so links don't get messed up by notifications

* bump version

* truncate accounts and fix login bug

* update packages

* add follow requests

* confirm quit

* fix top text

* fix multiple feed windows
pull/148/head 1.0.7
Rasmus Lindroth 4 years ago committed by GitHub
parent
commit
d7f485c8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      README.md
  2. 7
      api/feed.go
  3. 8
      api/user.go
  4. 2
      auth/add.go
  5. 2
      auth/file.go
  6. 6
      auth/load.go
  7. 62
      config.example.ini
  8. 147
      config/config.go
  9. 62
      config/default_config.go
  10. 3
      config/help.tmpl
  11. 38
      feed/feed.go
  12. 6
      go.sum
  13. 2
      main.go
  14. 14
      ui/cliview.go
  15. 5
      ui/cmdbar.go
  16. 6
      ui/commands.go
  17. 23
      ui/feed.go
  18. 96
      ui/input.go
  19. 17
      ui/item.go
  20. 5
      ui/item_notification.go
  21. 5
      ui/item_user.go
  22. 8
      ui/linkview.go
  23. 1
      ui/loginview.go
  24. 82
      ui/mainview.go
  25. 13
      ui/modalview.go
  26. 12
      ui/open.go
  27. 214
      ui/timeline.go
  28. 21
      ui/top.go
  29. 43
      ui/tutview.go
  30. 16
      ui/view.go

1
README.md

@ -39,6 +39,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
* `:lists` show a list of your lists
* `:muting` lists users that you have muted
* `:profile` go to your profile
* `:requests` see following requests
* `:saved` alias for bookmarks
* `:tag` followed by the hashtag e.g. `:tag linux`
* `:user` followed by a username e.g. `:user rasmus` to narrow a search include

7
api/feed.go

@ -223,6 +223,13 @@ func (ac *AccountClient) GetMuting(pg *mastodon.Pagination) ([]Item, error) {
return ac.getUserSimilar(fn)
}
func (ac *AccountClient) GetFollowRequests(pg *mastodon.Pagination) ([]Item, error) {
fn := func() ([]*mastodon.Account, error) {
return ac.Client.GetFollowRequests(context.Background(), pg)
}
return ac.getUserSimilar(fn)
}
func (ac *AccountClient) getUserSimilar(fn func() ([]*mastodon.Account, error)) ([]Item, error) {
var items []Item
users, err := fn()

8
api/user.go

@ -71,3 +71,11 @@ func (ac *AccountClient) MuteUser(u *mastodon.Account) (*mastodon.Relationship,
func (ac *AccountClient) UnmuteUser(u *mastodon.Account) (*mastodon.Relationship, error) {
return ac.Client.AccountUnmute(context.Background(), u.ID)
}
func (ac *AccountClient) FollowRequestAccept(u *mastodon.Account) error {
return ac.Client.FollowRequestAuthorize(context.Background(), u.ID)
}
func (ac *AccountClient) FollowRequestDeny(u *mastodon.Account) error {
return ac.Client.FollowRequestReject(context.Background(), u.ID)
}

2
auth/add.go

@ -82,7 +82,7 @@ func AddAccount(ad *AccountData) *mastodon.Client {
}
me, err := client.GetAccountCurrentUser(context.Background())
if err != nil {
fmt.Printf("\nCouldnät get user. Error: %v\nExiting...\n", err)
fmt.Printf("\nCouldn't get user. Error: %v\nExiting...\n", err)
os.Exit(1)
}
acc := Account{

2
auth/file.go

@ -48,7 +48,7 @@ func (ad *AccountData) Save(filepath string) error {
if err != nil {
return err
}
f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE, 0600)
f, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}

6
auth/load.go

@ -16,7 +16,11 @@ func StartAuth(newUser bool) *AccountData {
accs, err = GetAccounts(path)
}
if err != nil || accs == nil || len(accs.Accounts) == 0 || newUser {
AddAccount(nil)
if err == nil && accs != nil {
AddAccount(accs)
} else {
AddAccount(nil)
}
return StartAuth(false)
}
return accs

62
config.example.ini

@ -6,6 +6,42 @@
# default=true
confirmation=true
# Timelines adds windows of feeds. You can customize the number of feeds, what
# they should show and the key to activate them.
#
# Available timelines: home, direct, local, federated, bookmarks, saved,
# favorited, notifications, lists, tag
#
# Tag is special as you need to add the tag after, see the example below.
#
# The syntax is:
# timelines=feed,[name],[keys...]
#
# Tha values in brackets are optional. You can see the syntax for keys under the
# [input] section.
#
# Some examples:
#
# home timeline with the name Home
# timelines=home,Home
#
# local timeline with the name Local and it gets focus when you press 2
# timelines=local,Local,'2'
#
# notification timeline with the name [N]otifications and it gets focus when you
# press n or N
# timelines=notifications,[N]otifications,'n','N'
#
# tag timeline for #linux with the name Linux and it gets focus when you press
# timelines=tag linux,Linux,"F2"
#
#
# If you don't set any timelines it will default to this:
# timelines=home
# timelines=notifications,[N]otifications,'n','N'
#
# The date format to be used. See https://godoc.org/time#Time.Format
# default=2006-01-02 15:04
date-format=2006-01-02 15:04
@ -27,20 +63,11 @@ date-today-format=15:04
# default=-1
date-relative=-1
# The timeline that opens up when you start tut.
# Valid values: home, direct, local, federated
# default=home
timeline=home
# The max width of text before it wraps when displaying toots.
# 0 = no restriction.
# default=0
max-width=0
# If you want to display a list of notifications under your timeline feed.
# default=true
notification-feed=true
# Where do you want the list of toots to be placed?
# Valid values: left, right, top, bottom.
# default=left
@ -52,11 +79,6 @@ list-placement=left
# default=row
list-split=row
# Hide notification text above list in column split. It's displayed as
# [N]otifications.
# default=false
hide-notification-text=false
# You can change the proportions of the list view in relation to the content
# view list-proportion=1 and content-proportoin=3 will result in the content
# taking up 3 times more space.
@ -421,6 +443,14 @@ main-prev-feed="",'h','H',"Left"
# default="",'l','L',"Right"
main-next-feed="",'l','L',"Right"
# Focus on the previous feed window
# default="","Backtab"
main-prev-window="","Backtab"
# Focus on the next feed window
# default="","Tab"
main-next-window="","Tab"
# Focus on the notification list
# default="[N]otifications",'n','N'
main-notification-focus="[N]otifications",'n','N'
@ -497,6 +527,10 @@ user-block="[B]lock","Un[B]lock",'b','B'
# default="[F]ollow","Un[F]ollow",'f','F'
user-follow="[F]ollow","Un[F]ollow",'f','F'
# Follow user
# default="Follow [R]equest","Follow [R]equest",'r','R'
user-follow-request-decide="Follow [R]equest","Follow [R]equest",'r','R'
# Mute user
# default="[M]ute","Un[M]ute",'m','M'
user-mute="[M]ute","Un[M]ute",'m','M'

147
config/config.go

@ -72,6 +72,13 @@ const (
LeaderUser
)
type Timeline struct {
FeedType feed.FeedType
Subaction string
Name string
Key Key
}
type General struct {
Confirmation bool
DateTodayFormat string
@ -94,6 +101,8 @@ type General struct {
LeaderKey rune
LeaderTimeout int64
LeaderActions []LeaderAction
TimelineName bool
Timelines []Timeline
}
type Style struct {
@ -306,12 +315,13 @@ type Input struct {
GlobalBack Key
GlobalExit Key
MainHome Key
MainEnd Key
MainPrevFeed Key
MainNextFeed Key
MainNotificationFocus Key
MainCompose Key
MainHome Key
MainEnd Key
MainPrevFeed Key
MainNextFeed Key
MainPrevWindow Key
MainNextWindow Key
MainCompose Key
StatusAvatar Key
StatusBoost Key
@ -328,14 +338,15 @@ type Input struct {
StatusYank Key
StatusToggleSpoiler Key
UserAvatar Key
UserBlock Key
UserFollow Key
UserMute Key
UserLinks Key
UserUser Key
UserViewFocus Key
UserYank Key
UserAvatar Key
UserBlock Key
UserFollow Key
UserFollowRequestDecide Key
UserMute Key
UserLinks Key
UserUser Key
UserViewFocus Key
UserYank Key
ListOpenFeed Key
@ -599,6 +610,7 @@ func parseGeneral(cfg *ini.File) General {
parts := strings.Split(l, ",")
if len(parts) != 2 {
fmt.Printf("leader-action must consist of two parts seperated by a comma. Your value is: %s\n", strings.Join(parts, ","))
os.Exit(1)
}
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
@ -658,6 +670,81 @@ func parseGeneral(cfg *ini.File) General {
}
general.LeaderActions = las
}
general.TimelineName = cfg.Section("general").Key("timeline-show-name").MustBool(true)
var tls []Timeline
timelines := cfg.Section("general").Key("timelines").ValueWithShadows()
for _, l := range timelines {
parts := strings.Split(l, ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
if len(parts) == 0 {
fmt.Printf("timelines must consist of atleast one part seperated by a comma. Your value is: %s\n", strings.Join(parts, ","))
os.Exit(1)
}
if len(parts) == 1 {
parts = append(parts, "")
}
cmd := parts[0]
var subaction string
if strings.Contains(parts[0], " ") {
p := strings.Split(cmd, " ")
cmd = p[0]
subaction = strings.Join(p[1:], " ")
}
tl := Timeline{}
switch cmd {
case "home":
tl.FeedType = feed.TimelineHome
case "direct":
tl.FeedType = feed.Conversations
case "local":
tl.FeedType = feed.TimelineLocal
case "federated":
tl.FeedType = feed.TimelineFederated
case "bookmarks":
tl.FeedType = feed.Saved
case "saved":
tl.FeedType = feed.Saved
case "favorited":
tl.FeedType = feed.Favorited
case "notifications":
tl.FeedType = feed.Notification
case "lists":
tl.FeedType = feed.Lists
case "tag":
tl.FeedType = feed.Tag
tl.Subaction = subaction
default:
fmt.Printf("timeline %s is invalid\n", parts[0])
os.Exit(1)
}
tl.Name = parts[1]
if len(parts) > 2 {
vals := []string{""}
vals = append(vals, parts[2:]...)
tl.Key = inputStrOrErr(vals, false)
}
tls = append(tls, tl)
}
if len(tls) == 0 {
tls = append(tls,
Timeline{
FeedType: feed.TimelineHome,
Name: "",
},
)
tls = append(tls,
Timeline{
FeedType: feed.Notification,
Name: "[N]otifications",
Key: inputStrOrErr([]string{"", "'n'", "'N'"}, false),
},
)
}
general.Timelines = tls
return general
}
@ -893,12 +980,13 @@ func parseInput(cfg *ini.File) Input {
GlobalBack: inputStrOrErr([]string{"\"[Esc]\"", "\"Esc\""}, false),
GlobalExit: inputStrOrErr([]string{"\"[Q]uit\"", "'q'", "'Q'"}, false),
MainHome: inputStrOrErr([]string{"\"\"", "'g'", "\"Home\""}, false),
MainEnd: inputStrOrErr([]string{"\"\"", "'G'", "\"End\""}, false),
MainPrevFeed: inputStrOrErr([]string{"\"\"", "'h'", "'H'", "\"Left\""}, false),
MainNextFeed: inputStrOrErr([]string{"\"\"", "'l'", "'L'", "\"Right\""}, false),
MainNotificationFocus: inputStrOrErr([]string{"\"[N]otifications\"", "'n'", "'N'"}, false),
MainCompose: inputStrOrErr([]string{"\"\"", "'c'", "'C'"}, false),
MainHome: inputStrOrErr([]string{"\"\"", "'g'", "\"Home\""}, false),
MainEnd: inputStrOrErr([]string{"\"\"", "'G'", "\"End\""}, false),
MainPrevFeed: inputStrOrErr([]string{"\"\"", "'h'", "'H'", "\"Left\""}, false),
MainNextFeed: inputStrOrErr([]string{"\"\"", "'l'", "'L'", "\"Right\""}, false),
MainPrevWindow: inputStrOrErr([]string{"\"\"", "\"Backtab\""}, false),
MainNextWindow: inputStrOrErr([]string{"\"\"", "\"Tab\""}, false),
MainCompose: inputStrOrErr([]string{"\"\"", "'c'", "'C'"}, false),
StatusAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false),
StatusBoost: inputStrOrErr([]string{"\"[B]oost\"", "\"Un[B]oost\"", "'b'", "'B'"}, true),
@ -915,14 +1003,15 @@ func parseInput(cfg *ini.File) Input {
StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false),
StatusToggleSpoiler: inputStrOrErr([]string{"\"Press [Z] to toggle spoiler\"", "'z'", "'Z'"}, false),
UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false),
UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true),
UserFollow: inputStrOrErr([]string{"\"[F]ollow\"", "\"Un[F]ollow\"", "'f'", "'F'"}, true),
UserMute: inputStrOrErr([]string{"\"[M]ute\"", "\"Un[M]ute\"", "'m'", "'M'"}, true),
UserLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false),
UserUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false),
UserViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false),
UserYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false),
UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false),
UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true),
UserFollow: inputStrOrErr([]string{"\"[F]ollow\"", "\"Un[F]ollow\"", "'f'", "'F'"}, true),
UserFollowRequestDecide: inputStrOrErr([]string{"\"Follow [R]equest\"", "\"Follow [R]equest\"", "'r'", "'R'"}, true),
UserMute: inputStrOrErr([]string{"\"[M]ute\"", "\"Un[M]ute\"", "'m'", "'M'"}, true),
UserLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false),
UserUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false),
UserViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false),
UserYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false),
ListOpenFeed: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false),
@ -954,7 +1043,6 @@ func parseInput(cfg *ini.File) Input {
ic.MainEnd = inputOrErr(cfg, "main-end", false, ic.MainEnd)
ic.MainPrevFeed = inputOrErr(cfg, "main-prev-feed", false, ic.MainPrevFeed)
ic.MainNextFeed = inputOrErr(cfg, "main-next-feed", false, ic.MainNextFeed)
ic.MainNotificationFocus = inputOrErr(cfg, "main-notification-focus", false, ic.MainNotificationFocus)
ic.MainCompose = inputOrErr(cfg, "main-compose", false, ic.MainCompose)
ic.StatusAvatar = inputOrErr(cfg, "status-avatar", false, ic.StatusAvatar)
@ -975,6 +1063,7 @@ func parseInput(cfg *ini.File) Input {
ic.UserAvatar = inputOrErr(cfg, "user-avatar", false, ic.UserAvatar)
ic.UserBlock = inputOrErr(cfg, "user-block", true, ic.UserBlock)
ic.UserFollow = inputOrErr(cfg, "user-follow", true, ic.UserFollow)
ic.UserFollowRequestDecide = inputOrErr(cfg, "user-follow-request-decide", true, ic.UserFollowRequestDecide)
ic.UserMute = inputOrErr(cfg, "user-mute", true, ic.UserMute)
ic.UserLinks = inputOrErr(cfg, "user-links", false, ic.UserLinks)
ic.UserUser = inputOrErr(cfg, "user-user", false, ic.UserUser)

62
config/default_config.go

@ -8,6 +8,42 @@ var conftext = `# Configuration file for tut
# default=true
confirmation=true
# Timelines adds windows of feeds. You can customize the number of feeds, what
# they should show and the key to activate them.
#
# Available timelines: home, direct, local, federated, bookmarks, saved,
# favorited, notifications, lists, tag
#
# Tag is special as you need to add the tag after, see the example below.
#
# The syntax is:
# timelines=feed,[name],[keys...]
#
# Tha values in brackets are optional. You can see the syntax for keys under the
# [input] section.
#
# Some examples:
#
# home timeline with the name Home
# timelines=home,Home
#
# local timeline with the name Local and it gets focus when you press 2
# timelines=local,Local,'2'
#
# notification timeline with the name [N]otifications and it gets focus when you
# press n or N
# timelines=notifications,[N]otifications,'n','N'
#
# tag timeline for #linux with the name Linux and it gets focus when you press
# timelines=tag linux,Linux,"F2"
#
#
# If you don't set any timelines it will default to this:
# timelines=home
# timelines=notifications,[N]otifications,'n','N'
#
# The date format to be used. See https://godoc.org/time#Time.Format
# default=2006-01-02 15:04
date-format=2006-01-02 15:04
@ -29,20 +65,11 @@ date-today-format=15:04
# default=-1
date-relative=-1
# The timeline that opens up when you start tut.
# Valid values: home, direct, local, federated
# default=home
timeline=home
# The max width of text before it wraps when displaying toots.
# 0 = no restriction.
# default=0
max-width=0
# If you want to display a list of notifications under your timeline feed.
# default=true
notification-feed=true
# Where do you want the list of toots to be placed?
# Valid values: left, right, top, bottom.
# default=left
@ -54,11 +81,6 @@ list-placement=left
# default=row
list-split=row
# Hide notification text above list in column split. It's displayed as
# [N]otifications.
# default=false
hide-notification-text=false
# You can change the proportions of the list view in relation to the content
# view list-proportion=1 and content-proportoin=3 will result in the content
# taking up 3 times more space.
@ -423,6 +445,14 @@ main-prev-feed="",'h','H',"Left"
# default="",'l','L',"Right"
main-next-feed="",'l','L',"Right"
# Focus on the previous feed window
# default="","Backtab"
main-prev-window="","Backtab"
# Focus on the next feed window
# default="","Tab"
main-next-window="","Tab"
# Focus on the notification list
# default="[N]otifications",'n','N'
main-notification-focus="[N]otifications",'n','N'
@ -499,6 +529,10 @@ user-block="[B]lock","Un[B]lock",'b','B'
# default="[F]ollow","Un[F]ollow",'f','F'
user-follow="[F]ollow","Un[F]ollow",'f','F'
# Follow user
# default="Follow [R]equest","Follow [R]equest",'r','R'
user-follow-request-decide="Follow [R]equest","Follow [R]equest",'r','R'
# Mute user
# default="[M]ute","Un[M]ute",'m','M'
user-mute="[M]ute","Un[M]ute",'m','M'

3
config/help.tmpl

@ -67,6 +67,9 @@ Here's a list of supported commands.
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:profile{{ Flags "-" }}{{ Color .Style.Text }}
Go to your own profile
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:requests{{ Flags "-" }}{{ Color .Style.Text }}
See following requests
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:saved{{ Flags "-" }}{{ Color .Style.Text }}
Alias for :bookmarks

38
feed/feed.go

@ -26,6 +26,7 @@ const (
Boosts
Followers
Following
FollowRequests
Blocking
Muting
InvalidFeed
@ -87,6 +88,19 @@ func (f *Feed) List() []api.Item {
return f.items
}
func (f *Feed) Delete(id uint) {
f.itemsMux.Lock()
defer f.itemsMux.Unlock()
var items []api.Item
for _, item := range f.items {
if item.ID() != id {
items = append(items, item)
}
}
f.items = items
f.Updated(DekstopNotificationNone)
}
func (f *Feed) Item(index int) (api.Item, error) {
f.itemsMux.RLock()
defer f.itemsMux.RUnlock()
@ -744,6 +758,7 @@ func NewUserProfile(ac *api.AccountClient, user *api.User) *Feed {
loadOlder: func() {},
apiData: &api.RequestData{},
Update: make(chan DesktopNotificationType, 1),
name: user.Data.Acct,
loadingNewer: &LoadingLock{},
loadingOlder: &LoadingLock{},
}
@ -974,3 +989,26 @@ func NewMuting(ac *api.AccountClient) *Feed {
return feed
}
func NewFollowRequests(ac *api.AccountClient) *Feed {
feed := &Feed{
accountClient: ac,
feedType: FollowRequests,
items: make([]api.Item, 0),
apiData: &api.RequestData{},
Update: make(chan DesktopNotificationType, 1),
loadingNewer: &LoadingLock{},
loadingOlder: &LoadingLock{},
}
once := true
feed.loadNewer = func() {
if once {
feed.linkNewer(feed.accountClient.GetFollowRequests)
}
once = false
}
feed.loadOlder = func() { feed.linkOlder(feed.accountClient.GetFollowRequests) }
return feed
}

6
go.sum

@ -8,7 +8,6 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
@ -38,8 +37,6 @@ github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDS
github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/tview v0.0.0-20211129142845-821b2667c414 h1:8pLxYvjWizid9rNUDyWv9D4gti+/w+TK7P10eXnh+xA=
github.com/rivo/tview v0.0.0-20211129142845-821b2667c414/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@ -58,15 +55,12 @@ golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

2
main.go

@ -10,7 +10,7 @@ import (
"github.com/rivo/tview"
)
const version = "1.0.6"
const version = "1.0.7"
func main() {
util.MakeDirs()

14
ui/cliview.go

@ -26,13 +26,13 @@ func CliView(version string) (newUser bool, selectedUser string) {
}
case "--help", "-h":
fmt.Print("tut - a TUI for Mastodon with vim inspired keys.\n\n")
fmt.Print("Usage:\n\n")
fmt.Print("Usage:\n")
fmt.Print("\tTo run the program you just have to write tut\n\n")
fmt.Print("Commands:\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\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")
@ -40,16 +40,16 @@ func CliView(version string) (newUser bool, selectedUser string) {
fmt.Print("\t\tDon't use a = between --user and the <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\n")
fmt.Print("Configuration:\n")
fmt.Printf("\tThe config is located in XDG_CONFIG_HOME/tut/config.ini which usally 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\n")
fmt.Printf("\t@rasmus@mastodon.acc.sunet.se\n\trasmus@lindroth.xyz\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\n", version)
fmt.Printf("tut version %s\n", version)
fmt.Printf("https://github.com/RasmusLindroth/tut\n")
os.Exit(0)
}

5
ui/cmdbar.go

@ -94,6 +94,9 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
case ":muting":
c.tutView.MutingCommand()
c.Back()
case ":requests":
c.tutView.FollowRequestsCommand()
c.Back()
case ":profile":
c.tutView.ProfileCommand()
c.Back()
@ -159,7 +162,7 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
func (c *CmdBar) Autocomplete(curr string) []string {
var entries []string
words := strings.Split(":blocking,:boosts,:bookmarks,:compose,:favorites,:favorited,:followers,:following,:help,:h,:lists,:muting,:profile,:saved,:tag,:timeline,:tl,:user,:quit,:q", ",")
words := strings.Split(":blocking,:boosts,:bookmarks,:compose,:favorites,:favorited,:followers,:following,:help,:h,:lists,:muting,:profile,:requests,:saved,:tag,:timeline,:tl,:user,:quit,:q", ",")
if curr == "" {
return entries
}

6
ui/commands.go

@ -35,6 +35,12 @@ func (tv *TutView) MutingCommand() {
)
}
func (tv *TutView) FollowRequestsCommand() {
tv.Timeline.AddFeed(
NewFollowRequests(tv),
)
}
func (tv *TutView) LocalCommand() {
tv.Timeline.AddFeed(
NewLocalFeed(tv),

23
ui/feed.go

@ -69,14 +69,18 @@ func (f *Feed) LoadNewer() {
f.Data.LoadNewer()
}
func (f *Feed) Delete() {
id := f.List.GetCurrentID()
f.Data.Delete(id)
}
func (f *Feed) DrawContent() {
id := f.List.GetCurrentID()
for _, item := range f.Data.List() {
if id != item.ID() {
continue
}
DrawItem(f.tutView.tut, item, f.Content.Main, f.Content.Controls)
f.tutView.LinkView.SetLinks(item)
DrawItem(f.tutView.tut, item, f.Content.Main, f.Content.Controls, f.Data.Type())
f.tutView.ShouldSync()
}
}
@ -424,6 +428,21 @@ func NewMuting(tv *TutView) *Feed {
return fd
}
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),
Content: NewFeedContent(tv.tut),
}
go fd.update()
return fd
}
func NewFeedList(t *Tut) *FeedList {
fl := &FeedList{
Text: NewList(t.Config),

96
ui/input.go

@ -7,6 +7,7 @@ import (
"github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/api"
"github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/feed"
"github.com/RasmusLindroth/tut/util"
"github.com/gdamore/tcell/v2"
)
@ -147,55 +148,64 @@ func (tv *TutView) InputMainView(event *tcell.EventKey) *tcell.EventKey {
}
func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey {
mainFocus := tv.TimelineFocus == FeedFocus
if tv.tut.Config.Input.MainHome.Match(event.Key(), event.Rune()) {
tv.Timeline.HomeItemFeed(mainFocus)
tv.Timeline.HomeItemFeed()
return nil
}
if tv.tut.Config.Input.MainEnd.Match(event.Key(), event.Rune()) {
tv.Timeline.EndItemFeed(mainFocus)
tv.Timeline.EndItemFeed()
return nil
}
if tv.tut.Config.Input.MainPrevFeed.Match(event.Key(), event.Rune()) {
if mainFocus {
tv.Timeline.PrevFeed()
}
tv.Timeline.PrevFeed()
return nil
}
if tv.tut.Config.Input.MainNextFeed.Match(event.Key(), event.Rune()) {
if mainFocus {
tv.Timeline.NextFeed()
}
tv.Timeline.NextFeed()
return nil
}
if tv.tut.Config.Input.GlobalDown.Match(event.Key(), event.Rune()) {
tv.Timeline.NextItemFeed(mainFocus)
tv.Timeline.NextItemFeed()
return nil
}
if tv.tut.Config.Input.GlobalUp.Match(event.Key(), event.Rune()) {
tv.Timeline.PrevItemFeed(mainFocus)
tv.Timeline.PrevItemFeed()
return nil
}
if tv.tut.Config.Input.MainPrevWindow.Match(event.Key(), event.Rune()) {
if tv.tut.Config.General.NotificationFeed {
tv.PrevFeed()
}
return nil
}
if tv.tut.Config.Input.MainNotificationFocus.Match(event.Key(), event.Rune()) {
if tv.tut.Config.Input.MainNextWindow.Match(event.Key(), event.Rune()) {
if tv.tut.Config.General.NotificationFeed {
tv.FocusNotification()
tv.NextFeed()
}
return nil
}
if tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) {
if mainFocus {
tv.Timeline.RemoveCurrent(true)
} else {
tv.FocusFeed()
exiting := tv.Timeline.RemoveCurrent(false)
if exiting && tv.Timeline.FeedFocusIndex == 0 {
tv.ModalView.Run("Do you want to exit tut?",
func() {
tv.Timeline.RemoveCurrent(true)
})
return nil
} else if exiting && tv.Timeline.FeedFocusIndex != 0 {
tv.FocusFeed(0)
}
return nil
}
for i, tl := range tv.tut.Config.General.Timelines {
if tl.Key.Match(event.Key(), event.Rune()) {
tv.FocusFeed(i)
}
}
if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) {
if mainFocus {
tv.Timeline.RemoveCurrent(false)
} else {
tv.FocusFeed()
exiting := tv.Timeline.RemoveCurrent(false)
if exiting && tv.Timeline.FeedFocusIndex != 0 {
tv.FocusFeed(0)
}
return nil
}
@ -233,6 +243,8 @@ func (tv *TutView) InputViewItem(event *tcell.EventKey) *tcell.EventKey {
}
func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey {
fd := tv.GetCurrentFeed()
ft := fd.Data.Type()
item, err := tv.GetCurrentItem()
if err != nil {
return event
@ -245,12 +257,16 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey {
case api.StatusType:
return tv.InputStatus(event, item, item.Raw().(*mastodon.Status))
case api.UserType, api.ProfileType:
return tv.InputUser(event, item.Raw().(*api.User))
if ft == feed.FollowRequests {
return tv.InputUser(event, item.Raw().(*api.User), true)
} else {
return tv.InputUser(event, item.Raw().(*api.User), false)
}
case api.NotificationType:
nd := item.Raw().(*api.NotificationData)
switch nd.Item.Type {
case "follow":
return tv.InputUser(event, nd.User.Raw().(*api.User))
return tv.InputUser(event, nd.User.Raw().(*api.User), false)
case "favourite":
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status))
case "reblog":
@ -262,7 +278,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey {
case "poll":
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status))
case "follow_request":
return tv.InputUser(event, nd.User.Raw().(*api.User))
return tv.InputUser(event, nd.User.Raw().(*api.User), true)
}
case api.ListsType:
ld := item.Raw().(*mastodon.List)
@ -423,11 +439,39 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas
return event
}
func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User) *tcell.EventKey {
func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, fr bool) *tcell.EventKey {
blocking := user.Relation.Blocking
muting := user.Relation.Muting
following := user.Relation.Following
if tv.tut.Config.Input.UserFollowRequestDecide.Match(event.Key(), event.Rune()) {
tv.ModalView.RunDecide("Do you want accept the follow request?",
func() {
err := tv.tut.Client.FollowRequestAccept(user.Data)
if err != nil {
tv.ShowError(
fmt.Sprintf("Couldn't accept follow request. Error: %v\n", err),
)
return
}
f := tv.GetCurrentFeed()
f.Delete()
tv.RedrawContent()
},
func() {
err := tv.tut.Client.FollowRequestDeny(user.Data)
if err != nil {
tv.ShowError(
fmt.Sprintf("Couldn't deny follow request. Error: %v\n", err),
)
return
}
f := tv.GetCurrentFeed()
f.Delete()
tv.RedrawContent()
})
return nil
}
if tv.tut.Config.Input.UserAvatar.Match(event.Key(), event.Rune()) {
openAvatar(tv, *user.Data)
return nil

17
ui/item.go

@ -8,6 +8,7 @@ import (
"github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/api"
"github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/feed"
"github.com/icza/gox/timex"
"github.com/rivo/tview"
)
@ -58,12 +59,16 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) {
}
}
func DrawItem(tut *Tut, item api.Item, main *tview.TextView, controls *tview.TextView) {
func DrawItem(tut *Tut, item api.Item, main *tview.TextView, controls *tview.TextView, ft feed.FeedType) {
switch item.Type() {
case api.StatusType:
drawStatus(tut, item, item.Raw().(*mastodon.Status), main, controls, "")
case api.UserType, api.ProfileType:
drawUser(tut, item.Raw().(*api.User), main, controls, "")
if ft == feed.FollowRequests {
drawUser(tut, item.Raw().(*api.User), main, controls, "", true)
} else {
drawUser(tut, item.Raw().(*api.User), main, controls, "", false)
}
case api.NotificationType:
drawNotification(tut, item, item.Raw().(*api.NotificationData), main, controls)
case api.ListsType:
@ -71,12 +76,16 @@ func DrawItem(tut *Tut, item api.Item, main *tview.TextView, controls *tview.Tex
}
}
func DrawItemControls(tut *Tut, item api.Item, controls *tview.TextView) {
func DrawItemControls(tut *Tut, item api.Item, controls *tview.TextView, ft feed.FeedType) {
switch item.Type() {
case api.StatusType:
drawStatus(tut, item, item.Raw().(*mastodon.Status), nil, controls, "")
case api.UserType, api.ProfileType:
drawUser(tut, item.Raw().(*api.User), nil, controls, "")
if ft == feed.FollowRequests {
drawUser(tut, item.Raw().(*api.User), nil, controls, "", true)
} else {
drawUser(tut, item.Raw().(*api.User), nil, controls, "", false)
}
case api.NotificationType:
drawNotification(tut, item, item.Raw().(*api.NotificationData), nil, controls)
}

5
ui/item_notification.go

@ -12,7 +12,7 @@ func drawNotification(tut *Tut, item api.Item, notification *api.NotificationDat
switch notification.Item.Type {
case "follow":
drawUser(tut, notification.User.Raw().(*api.User), main, controls,
fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)),
fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), false,
)
case "favourite":
drawStatus(tut, notification.Status, notification.Item.Status, main, controls,
@ -36,7 +36,8 @@ func drawNotification(tut *Tut, item api.Item, notification *api.NotificationDat
)
case "follow_request":
drawUser(tut, notification.User.Raw().(*api.User), main, controls,
fmt.Sprintf("%s wants to follow you. This is currently not implemented, so use another app to accept or reject the request.", util.FormatUsername(notification.Item.Account)),
fmt.Sprintf("%s wants to follow you.", util.FormatUsername(notification.Item.Account)),
true,
)
}
}

5
ui/item_user.go

@ -44,7 +44,7 @@ type DisplayUserData struct {
Style config.Style
}
func drawUser(tut *Tut, data *api.User, main *tview.TextView, controls *tview.TextView, additional string) {
func drawUser(tut *Tut, data *api.User, main *tview.TextView, controls *tview.TextView, additional string, fr bool) {
user := data.Data
relation := data.Relation
showUserControl := true
@ -82,6 +82,9 @@ func drawUser(tut *Tut, data *api.User, main *tview.TextView, controls *tview.Te
u.Fields = fields
var controlItems []string
if fr {
controlItems = append(controlItems, config.ColorFromKey(tut.Config, tut.Config.Input.UserFollowRequestDecide, false))
}
if tut.Client.Me.ID != user.ID {
if relation.Following {
controlItems = append(controlItems, config.ColorFromKey(tut.Config, tut.Config.Input.UserFollow, false))

8
ui/linkview.go

@ -4,7 +4,6 @@ import (
"fmt"
"strings"
"github.com/RasmusLindroth/tut/api"
"github.com/RasmusLindroth/tut/config"
"github.com/rivo/tview"
)
@ -49,7 +48,12 @@ func linkViewUI(lv *LinkView) *tview.Flex {
AddItem(lv.shared.Bottom.View, 2, 0, false)
}
func (lv *LinkView) SetLinks(item api.Item) {
func (lv *LinkView) SetLinks() {
item, err := lv.tutView.GetCurrentItem()
if err != nil {
lv.list.Clear()
return
}
lv.list.Clear()
urls, mentions, tags, _ := item.URLs()

1
ui/loginview.go

@ -15,6 +15,7 @@ type LoginView struct {
}
func NewLoginView(tv *TutView, accs *auth.AccountData) *LoginView {
tv.Shared.Top.SetText("select account")
list := NewList(tv.tut.Config)
for _, a := range accs.Accounts {
list.AddItem(fmt.Sprintf("%s - %s", a.Name, a.Server), "", 0, nil)

82
ui/mainview.go

@ -1,8 +1,6 @@
package ui
import (
"fmt"
"github.com/RasmusLindroth/tut/config"
"github.com/rivo/tview"
)
@ -19,48 +17,28 @@ func NewMainView(tv *TutView, update chan bool) *MainView {
for range update {
tv.tut.App.QueueUpdateDraw(func() {
*tv.MainView.View = *mainViewUI(tv)
tv.ShouldSync()
})
}
}()
return mv
}
func feedList(mv *TutView) *tview.Flex {
iw := 3
if !mv.tut.Config.General.ShowIcons {
iw = 0
}
return tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(mv.Timeline.GetFeedList().Text, 0, 1, false).
AddItem(mv.Timeline.GetFeedList().Symbol, iw, 0, false) //fix so you can hide
}
func notificationList(mv *TutView) *tview.Flex {
func feedList(mv *TutView, fh *FeedHolder) *tview.Flex {
iw := 3
if !mv.tut.Config.General.ShowIcons {
iw = 0
}
return tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(mv.Timeline.Notifications.List.Text, 0, 1, false).
AddItem(mv.Timeline.Notifications.List.Symbol, iw, 0, false) //fix so you can hide
AddItem(fh.GetFeedList().Text, 0, 1, false).
AddItem(fh.GetFeedList().Symbol, iw, 0, false)
}
func mainViewUI(mv *TutView) *tview.Flex {
showMain := mv.TimelineFocus == FeedFocus
vl := NewVerticalLine(mv.tut.Config)
hl := NewHorizontalLine(mv.tut.Config)
nt := NewTextView(mv.tut.Config)
lp := mv.tut.Config.General.ListProportion
cp := mv.tut.Config.General.ContentProportion
nt.SetTextColor(mv.tut.Config.Style.Subtle)
parts := mv.tut.Config.Input.MainNotificationFocus.Hint
start, middle, end := "", "", ""
if len(parts) > 0 && len(parts[0]) == 3 {
start = parts[0][0]
middle = parts[0][1]
end = parts[0][2]
}
nt.SetText(tview.Escape(fmt.Sprintf("%s[%s]%s", start, middle, end)))
var list *tview.Flex
if mv.tut.Config.General.ListSplit == config.ListColumn {
list = tview.NewFlex().SetDirection(tview.FlexColumn)
@ -68,33 +46,37 @@ func mainViewUI(mv *TutView) *tview.Flex {
list = tview.NewFlex().SetDirection(tview.FlexRow)
}
if mv.tut.Config.General.NotificationFeed && !mv.tut.Config.General.HideNotificationText {
if mv.tut.Config.General.ListSplit == config.ListColumn {
list.AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(tview.NewBox(), 1, 0, false).
AddItem(feedList(mv), 0, 1, false), 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(nt, 1, 0, false).
AddItem(notificationList(mv), 0, 1, false), 0, 1, false)
} else {
list.AddItem(feedList(mv), 0, 1, false).
AddItem(nt, 1, 0, false).
AddItem(notificationList(mv), 0, 1, false)
if mv.tut.Config.General.ListSplit == config.ListColumn {
feeds := tview.NewFlex()
for _, fh := range mv.Timeline.Feeds {
if mv.tut.Config.General.TimelineName && len(fh.Name) > 0 {
txt := NewTextView(mv.tut.Config)
txt.SetText(tview.Escape(fh.Name))
txt.SetTextColor(mv.tut.Config.Style.Subtle)
feeds.AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(txt, 1, 0, false).
AddItem(feedList(mv, fh), 0, 1, false), 0, 1, false)
} else {
feeds.AddItem(feedList(mv, fh), 0, 1, false)
}
}
} else if mv.tut.Config.General.NotificationFeed && mv.tut.Config.General.HideNotificationText {
if mv.tut.Config.General.ListSplit == config.ListColumn {
list.AddItem(feedList(mv), 0, 1, false).
AddItem(notificationList(mv), 0, 1, false)
} else {
list.AddItem(feedList(mv), 0, 1, false).
AddItem(notificationList(mv), 0, 1, false)
list.AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(feeds, 0, 1, false), 0, 1, false)
} else {
feeds := tview.NewFlex().SetDirection(tview.FlexRow)
for _, fh := range mv.Timeline.Feeds {
if mv.tut.Config.General.TimelineName && len(fh.Name) > 0 {
txt := NewTextView(mv.tut.Config)
txt.SetText(tview.Escape(fh.Name))
txt.SetTextColor(mv.tut.Config.Style.Subtle)
feeds.AddItem(txt, 1, 0, false)
}
feeds.AddItem(feedList(mv, fh), 0, 1, false)
}
} else if !mv.tut.Config.General.NotificationFeed {
list.AddItem(feedList(mv), 0, 1, false)
list.AddItem(feeds, 0, 1, false)
}
fc := mv.Timeline.GetFeedContent(showMain)
fc := mv.Timeline.GetFeedContent()
content := fc.Main
controls := fc.Controls
r := tview.NewFlex().SetDirection(tview.FlexRow).

13
ui/modalview.go

@ -29,6 +29,7 @@ func NewModalView(tv *TutView) *ModalView {
}
func (mv *ModalView) run(text string) (chan bool, func()) {
mv.View.SetFocus(0)
mv.View.SetText(text)
mv.tutView.SetPage(ModalFocus)
return mv.res, func() {
@ -54,3 +55,15 @@ func (mv *ModalView) Run(text string, fn func()) {
func (mv *ModalView) Stop(fn func()) {
fn()
}
func (mv *ModalView) RunDecide(text string, fnYes func(), fnNo func()) {
r, f := mv.run(text)
go func() {
if <-r {
fnYes()
} else {
fnNo()
}
f()
}()
}

12
ui/open.go

@ -1,7 +1,6 @@
package ui
import (
"io/ioutil"
"log"
"os"
"os/exec"
@ -63,7 +62,7 @@ func OpenEditor(tv *TutView, content string) (string, error) {
args = append(args, parts[1:]...)
editor = parts[0]
}
f, err := ioutil.TempFile("", "tut")
f, err := os.CreateTemp("", "tut")
if err != nil {
return "", err
}
@ -73,7 +72,9 @@ func OpenEditor(tv *TutView, content string) (string, error) {
return "", err
}
}
args = append(args, f.Name())
fname := f.Name()
args = append(args, fname)
f.Close()
cmd := exec.Command(editor, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@ -84,10 +85,9 @@ func OpenEditor(tv *TutView, content string) (string, error) {
if err != nil {
log.Fatalln(err)
}
f.Seek(0, 0)
text, err = ioutil.ReadAll(f)
text, err = os.ReadFile(fname)
})
f.Close()
os.Remove(fname)
if err != nil {
return "", err
}

214
ui/timeline.go

@ -7,115 +7,139 @@ import (
"github.com/RasmusLindroth/tut/feed"
)
type FeedHolder struct {
Name string
Feeds []*Feed
FeedIndex int
}
type Timeline struct {
tutView *TutView
Feeds []*Feed
FeedIndex int
Notifications *Feed
update chan bool
tutView *TutView
Feeds []*FeedHolder
FeedFocusIndex int
update chan bool
}
func NewTimeline(tv *TutView, update chan bool) *Timeline {
tl := &Timeline{
tutView: tv,
Feeds: []*Feed{},
FeedIndex: 0,
Notifications: nil,
update: update,
tutView: tv,
Feeds: []*FeedHolder{},
update: update,
}
var nf *Feed
switch tv.tut.Config.General.StartTimeline {
case feed.TimelineFederated:
nf = NewFederatedFeed(tv)
case feed.TimelineLocal:
nf = NewLocalFeed(tv)
case feed.Conversations:
nf = NewConversationsFeed(tv)
default:
nf = NewHomeFeed(tv)
for _, f := range tv.tut.Config.General.Timelines {
switch f.FeedType {
case feed.TimelineHome:
nf = NewHomeFeed(tv)
case feed.Conversations:
nf = NewConversationsFeed(tv)
case feed.TimelineLocal:
nf = NewLocalFeed(tv)
case feed.TimelineFederated:
nf = NewFederatedFeed(tv)
case feed.Saved:
nf = NewBookmarksFeed(tv)
case feed.Favorited:
nf = NewFavoritedFeed(tv)
case feed.Notification:
nf = NewNotificationFeed(tv)
case feed.Lists:
nf = NewListsFeed(tv)
case feed.Tag:
nf = NewTagFeed(tv, f.Subaction)
default:
fmt.Println("Invalid feed")
os.Exit(1)
}
tl.Feeds = append(tl.Feeds, &FeedHolder{
Feeds: []*Feed{nf},
Name: f.Name,
})
}
for i := 1; i < len(tl.Feeds); i++ {
for _, f := range tl.Feeds[i].Feeds {
f.ListOutFocus()
}
}
tl.Feeds = append(tl.Feeds, nf)
tl.Notifications = NewNotificationFeed(tv)
tl.Notifications.ListOutFocus()
return tl
}
func (tl *Timeline) AddFeed(f *Feed) {
tl.tutView.FocusFeed()
tl.Feeds = append(tl.Feeds, f)
tl.FeedIndex = tl.FeedIndex + 1
fh := tl.Feeds[tl.FeedFocusIndex]
fh.Feeds = append(fh.Feeds, f)
fh.FeedIndex = fh.FeedIndex + 1
tl.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true
}
func (tl *Timeline) RemoveCurrent(quit bool) {
if len(tl.Feeds) == 1 && !quit {
return
func (tl *Timeline) RemoveCurrent(quit bool) bool {
if len(tl.Feeds[tl.FeedFocusIndex].Feeds) == 1 && !quit {
return true
}
if len(tl.Feeds) == 1 && quit {
if len(tl.Feeds[tl.FeedFocusIndex].Feeds) == 1 && quit {
tl.tutView.tut.App.Stop()
os.Exit(0)
}
tl.Feeds[tl.FeedIndex].Data.Close()
tl.Feeds = append(tl.Feeds[:tl.FeedIndex], tl.Feeds[tl.FeedIndex+1:]...)
ni := tl.FeedIndex - 1
f := tl.Feeds[tl.FeedFocusIndex]
f.Feeds[f.FeedIndex].Data.Close()
f.Feeds = append(f.Feeds[:f.FeedIndex], f.Feeds[f.FeedIndex+1:]...)
ni := f.FeedIndex - 1
if ni < 0 {
ni = 0
}
tl.FeedIndex = ni
f.FeedIndex = ni
tl.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true
return false
}
func (tl *Timeline) NextFeed() {
l := len(tl.Feeds)
ni := tl.FeedIndex + 1
f := tl.Feeds[tl.FeedFocusIndex]
l := len(f.Feeds)
ni := f.FeedIndex + 1
if ni >= l {
ni = l - 1
}
tl.FeedIndex = ni
f.FeedIndex = ni
tl.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true
}
func (tl *Timeline) PrevFeed() {
ni := tl.FeedIndex - 1
f := tl.Feeds[tl.FeedFocusIndex]
ni := f.FeedIndex - 1
if ni < 0 {
ni = 0
}
tl.FeedIndex = ni
f.FeedIndex = ni
tl.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true
}
func (tl *Timeline) DrawContent(main bool) {
var f *Feed
if main {
f = tl.Feeds[tl.FeedIndex]
} else {
f = tl.Notifications
}
func (tl *Timeline) DrawContent() {
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
f.DrawContent()
}
func (tl *Timeline) GetFeedList() *FeedList {
return tl.Feeds[tl.FeedIndex].List
func (fh *FeedHolder) GetFeedList() *FeedList {
return fh.Feeds[fh.FeedIndex].List
}
func (tl *Timeline) GetFeedContent(main bool) *FeedContent {
if main {
return tl.Feeds[tl.FeedIndex].Content
} else {
return tl.Notifications.Content
}
func (tl *Timeline) GetFeedContent() *FeedContent {
fh := tl.Feeds[tl.FeedFocusIndex]
return fh.Feeds[fh.FeedIndex].Content
}
func (tl *Timeline) GetTitle() string {
index := tl.FeedIndex
total := len(tl.Feeds)
current := tl.Feeds[index].Data.Type()
name := tl.Feeds[index].Data.Name()
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
index := fh.FeedIndex
total := len(fh.Feeds)
current := f.Data.Type()
name := f.Data.Name()
ct := ""
switch current {
case feed.Favorited:
@ -127,19 +151,19 @@ func (tl *Timeline) GetTitle() string {
case feed.Thread:
ct = "thread feed"
case feed.TimelineFederated:
ct = "timeline federated"
ct = "federated"
case feed.TimelineHome:
ct = "timeline home"
ct = "home"
case feed.TimelineLocal:
ct = "timeline local"
ct = "local"
case feed.Saved:
ct = "saved/bookmarked toots"
case feed.User:
ct = "timeline user"
ct = fmt.Sprintf("user %s", name)
case feed.UserList:
ct = fmt.Sprintf("user search %s", name)
case feed.Conversations:
ct = "timeline direct"
ct = "direct"
case feed.Lists:
ct = "lists"
case feed.List:
@ -152,6 +176,8 @@ func (tl *Timeline) GetTitle() string {
ct = "followers"
case feed.Following:
ct = "following"
case feed.FollowRequests:
ct = "follow requests"
case feed.Blocking:
ct = "blocking"
case feed.Muting:
@ -161,7 +187,8 @@ func (tl *Timeline) GetTitle() string {
}
func (tl *Timeline) ScrollUp() {
f := tl.Feeds[tl.FeedIndex]
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
row, _ := f.Content.Main.GetScrollOffset()
if row > 0 {
row = row - 1
@ -170,62 +197,55 @@ func (tl *Timeline) ScrollUp() {
}
func (tl *Timeline) ScrollDown() {
f := tl.Feeds[tl.FeedIndex]
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
row, _ := f.Content.Main.GetScrollOffset()
f.Content.Main.ScrollTo(row+1, 0)
}
func (tl *Timeline) NextItemFeed(mainFocus bool) {
var f *Feed
if mainFocus {
f = tl.Feeds[tl.FeedIndex]
} else {
f = tl.Notifications
}
func (tl *Timeline) NextItemFeed() {
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
loadMore := f.List.Next()
if loadMore {
f.LoadOlder()
}
tl.DrawContent(mainFocus)
}
func (tl *Timeline) PrevItemFeed(mainFocus bool) {
var f *Feed
if mainFocus {
f = tl.Feeds[tl.FeedIndex]
} else {
f = tl.Notifications
}
tl.DrawContent()
}
func (tl *Timeline) PrevItemFeed() {
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
loadMore := f.List.Prev()
if loadMore {
f.LoadNewer()
}
tl.DrawContent(mainFocus)
tl.DrawContent()
}
func (tl *Timeline) HomeItemFeed(mainFocus bool) {
var f *Feed
if mainFocus {
f = tl.Feeds[tl.FeedIndex]
} else {
f = tl.Notifications
}
func (tl *Timeline) HomeItemFeed() {
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
f.List.SetCurrentItem(0)
f.LoadNewer()
tl.DrawContent(mainFocus)
tl.DrawContent()
}
func (tl *Timeline) EndItemFeed(mainFocus bool) {
var f *Feed
if mainFocus {
f = tl.Feeds[tl.FeedIndex]
} else {
f = tl.Notifications
}
func (tl *Timeline) DeleteItemFeed() {
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
f.List.GetCurrentID()
tl.DrawContent()
}
func (tl *Timeline) EndItemFeed() {
fh := tl.Feeds[tl.FeedFocusIndex]
f := fh.Feeds[fh.FeedIndex]
ni := f.List.GetItemCount() - 1
if ni < 0 {
return
}
f.List.SetCurrentItem(ni)
f.LoadOlder()
tl.DrawContent(mainFocus)
tl.DrawContent()
}

21
ui/top.go

@ -2,6 +2,7 @@ package ui
import (
"fmt"
"net/url"
"github.com/rivo/tview"
)
@ -23,9 +24,23 @@ func NewTop(tv *TutView) *Top {
}
func (t *Top) SetText(s string) {
if s == "" {
t.View.SetText("tut")
if t.TutView.tut.Client != nil {
acct := t.TutView.tut.Client.Me
us := acct.Acct
u, err := url.Parse(acct.URL)
if err == nil {
us = fmt.Sprintf("%s@%s", us, u.Host)
}
if s == "" {
t.View.SetText(fmt.Sprintf("tut - %s", us))
} else {
t.View.SetText(fmt.Sprintf("tut - %s - %s", s, us))
}
} else {
t.View.SetText(fmt.Sprintf("tut - %s", s))
if s == "" {
t.View.SetText("tut")
} else {
t.View.SetText(fmt.Sprintf("tut - %s", s))
}
}
}

43
ui/tutview.go

@ -40,7 +40,6 @@ type TutView struct {
Timeline *Timeline
PageFocus PageFocusAt
PrevPageFocus PageFocusAt
TimelineFocus TimelineFocusAt
SubFocus SubFocusAt
Leader *Leader
Shared *Shared
@ -116,6 +115,7 @@ func NewTutView(t *Tut, accs *auth.AccountData, selectedUser string) *TutView {
if accName == selectedUser {
tv.loggedIn(acc)
found = true
break
}
}
if !found {
@ -155,7 +155,6 @@ func (tv *TutView) loggedIn(acc auth.Account) {
tv.tut.Client = ac
update := make(chan bool, 1)
tv.TimelineFocus = FeedFocus
tv.SubFocus = ListFocus
tv.LinkView = NewLinkView(tv)
tv.Timeline = NewTimeline(tv, update)
@ -174,20 +173,38 @@ func (tv *TutView) loggedIn(acc auth.Account) {
tv.SetPage(MainFocus)
}
func (tv *TutView) FocusNotification() {
tv.TimelineFocus = NotificationFocus
for _, f := range tv.Timeline.Feeds {
f.ListOutFocus()
func (tv *TutView) FocusFeed(index int) {
if index < 0 || index >= len(tv.Timeline.Feeds) {
return
}
tv.Timeline.Notifications.ListInFocus()
tv.Timeline.FeedFocusIndex = index
for i := 0; i < len(tv.Timeline.Feeds); i++ {
if i == index {
for _, f := range tv.Timeline.Feeds[i].Feeds {
f.ListInFocus()
}
} else {
for _, f := range tv.Timeline.Feeds[i].Feeds {
f.ListOutFocus()
}
}
}
tv.Shared.Top.SetText(tv.Timeline.GetTitle())
tv.Timeline.update <- true
}
func (tv *TutView) FocusFeed() {
tv.TimelineFocus = FeedFocus
for _, f := range tv.Timeline.Feeds {
f.ListInFocus()
func (tv *TutView) NextFeed() {
index := tv.Timeline.FeedFocusIndex + 1
if index >= len(tv.Timeline.Feeds) {
index = 0
}
tv.Timeline.Notifications.ListOutFocus()
tv.Timeline.update <- true
tv.FocusFeed(index)
}
func (tv *TutView) PrevFeed() {
index := tv.Timeline.FeedFocusIndex - 1
if index < 0 {
index = len(tv.Timeline.Feeds) - 1
}
tv.FocusFeed(index)
}

16
ui/view.go

@ -22,11 +22,8 @@ const (
)
func (tv *TutView) GetCurrentFeed() *Feed {
foc := tv.TimelineFocus
if foc == FeedFocus {
return tv.Timeline.Feeds[tv.Timeline.FeedIndex]
}
return tv.Timeline.Notifications
fh := tv.Timeline.Feeds[tv.Timeline.FeedFocusIndex]
return fh.Feeds[fh.FeedIndex]
}
func (tv *TutView) GetCurrentItem() (api.Item, error) {
@ -38,9 +35,11 @@ func (tv *TutView) RedrawContent() {
f := tv.GetCurrentFeed()
item, err := f.Data.Item(f.List.Text.GetCurrentItem())
if err != nil {
f.Content.Main.SetText("")
f.Content.Controls.SetText("")
return
}
DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls)
DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls, f.Data.Type())
}
func (tv *TutView) RedrawPoll(poll *mastodon.Poll) {
f := tv.GetCurrentFeed()
@ -58,7 +57,7 @@ func (tv *TutView) RedrawPoll(poll *mastodon.Poll) {
} else {
so.Poll = poll
}
DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls)
DrawItem(tv.tut, item, f.Content.Main, f.Content.Controls, f.Data.Type())
}
func (tv *TutView) RedrawControls() {
f := tv.GetCurrentFeed()
@ -66,7 +65,7 @@ func (tv *TutView) RedrawControls() {
if err != nil {
return
}
DrawItemControls(tv.tut, item, f.Content.Controls)
DrawItemControls(tv.tut, item, f.Content.Controls, f.Data.Type())
}
func (tv *TutView) SetPage(f PageFocusAt) {
@ -96,6 +95,7 @@ func (tv *TutView) SetPage(f PageFocusAt) {
tv.Shared.Bottom.StatusBar.SetMode(ScrollMode)
tv.tut.App.SetFocus(f.Content.Main)
case LinkFocus:
tv.LinkView.SetLinks()
tv.PageFocus = LinkFocus
tv.View.SwitchToPage("link")
tv.Shared.Bottom.StatusBar.SetMode(ListMode)

Loading…
Cancel
Save