Browse Source

1.0.27 (#227)

* bump version

* filter

* show boosted instead of booster

* add :refetch

* add special-*

* update example config
pull/228/head
Rasmus Lindroth 3 years ago committed by GitHub
parent
commit
9f2d372ea0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      README.md
  2. 20
      api/feed.go
  3. 239
      api/item.go
  4. 4
      api/status.go
  5. 40
      config.example.ini
  6. 111
      config/config.go
  7. 40
      config/default_config.go
  8. 7
      config/help.tmpl
  9. 4
      config/toot.tmpl
  10. 25
      feed/feed.go
  11. 6
      go.mod
  12. 8
      go.sum
  13. 2
      main.go
  14. 18
      ui/cmdbar.go
  15. 18
      ui/commands.go
  16. 28
      ui/composeview.go
  17. 14
      ui/feed.go
  18. 44
      ui/input.go
  19. 11
      ui/item.go
  20. 12
      ui/item_notification.go
  21. 21
      ui/item_status.go
  22. 8
      ui/styled_elements.go
  23. 4
      ui/timeline.go

5
README.md

@ -40,8 +40,8 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
## Currently supported commands
* `:q` `:quit` exit
* `:timeline` home, local, federated, direct, notifications, favorited
* `:tl` h, l, f, d, n, fav (shorter form)
* `:timeline` home, local, federated, direct, notifications, favorited, special-all, special-boosts, special-replies
* `:tl` h, l, f, d, n, fav, sa, sb, sr (shorter form)
* `:blocking` lists users that you have blocked
* `:boosts` lists users that boosted the toot
* `:bookmarks` lists all your bookmarks
@ -63,6 +63,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
* `:preferences` update your profile and some other settings
* `:profile` go to your profile
* `:proportions` [int] [int], where the first integer is the list and the other content, e.g. `:proportions 1 3`
* `:refetch` refetches the current item
* `:requests` see following requests
* `:saved` alias for bookmarks
* `:stick-to-top` toggle the stick-to-top setting.

20
api/feed.go

@ -10,14 +10,14 @@ import (
type TimelineType uint
func (ac *AccountClient) getStatusSimilar(fn func() ([]*mastodon.Status, error), filter string) ([]Item, error) {
func (ac *AccountClient) getStatusSimilar(fn func() ([]*mastodon.Status, error), timeline string) ([]Item, error) {
var items []Item
statuses, err := fn()
if err != nil {
return items, err
}
for _, s := range statuses {
item := NewStatusItem(s, ac.Filters, filter, false)
item := NewStatusItem(s, false)
items = append(items, item)
}
return items, nil
@ -96,7 +96,7 @@ func (ac *AccountClient) GetNotifications(nth []config.NotificationToHide, pg *m
if n.Account.ID == r.ID {
item := NewNotificationItem(n, &User{
Data: &n.Account, Relation: r,
}, ac.Filters)
})
items = append(items, item)
break
}
@ -124,11 +124,11 @@ func (ac *AccountClient) GetThread(status *mastodon.Status) ([]Item, error) {
return items, err
}
for _, s := range statuses.Ancestors {
items = append(items, NewStatusItem(s, ac.Filters, "thread", false))
items = append(items, NewStatusItem(s, false))
}
items = append(items, NewStatusItem(status, ac.Filters, "thread", false))
items = append(items, NewStatusItem(status, false))
for _, s := range statuses.Descendants {
items = append(items, NewStatusItem(s, ac.Filters, "thread", false))
items = append(items, NewStatusItem(s, false))
}
return items, nil
}
@ -154,7 +154,7 @@ func (ac *AccountClient) GetConversations(pg *mastodon.Pagination) ([]Item, erro
return items, err
}
for _, c := range conversations {
item := NewStatusItem(c.LastStatus, ac.Filters, "thread", false)
item := NewStatusItem(c.LastStatus, false)
items = append(items, item)
}
return items, nil
@ -251,7 +251,7 @@ func (ac *AccountClient) GetUser(pg *mastodon.Pagination, id mastodon.ID) ([]Ite
return items, err
}
for _, s := range statuses {
item := NewStatusItem(s, ac.Filters, "account", false)
item := NewStatusItem(s, false)
items = append(items, item)
}
return items, nil
@ -264,7 +264,7 @@ func (ac *AccountClient) GetUserPinned(id mastodon.ID) ([]Item, error) {
return items, err
}
for _, s := range statuses {
item := NewStatusItem(s, ac.Filters, "account", true)
item := NewStatusItem(s, true)
items = append(items, item)
}
return items, nil
@ -301,7 +301,7 @@ func (ac *AccountClient) GetListStatuses(pg *mastodon.Pagination, id mastodon.ID
return items, err
}
for _, s := range statuses {
item := NewStatusItem(s, ac.Filters, "home", false)
item := NewStatusItem(s, false)
items = append(items, item)
}
return items, nil

239
api/item.go

@ -3,10 +3,11 @@ package api
import (
"strings"
"sync"
"unicode"
"github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/util"
"golang.org/x/exp/slices"
)
var id uint = 0
@ -22,17 +23,25 @@ func newID() uint {
type Item interface {
ID() uint
Type() MastodonType
ToggleSpoiler()
ShowSpoiler() bool
ToggleCW()
ShowCW() bool
Raw() interface{}
URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int)
Filtered() (bool, string)
Filtered(config.FeedType) (bool, string, string, bool)
ForceViewFilter()
Pinned() bool
Refetch(*AccountClient) bool
}
type filtered struct {
inUse bool
name string
InUse bool
Filters []filter
}
type filter struct {
Values []string
Where []string
Type string
}
func getUrlsStatus(status *mastodon.Status) ([]util.URL, []mastodon.Mention, []mastodon.Tag, int) {
@ -72,72 +81,36 @@ func getUrlsUser(user *mastodon.Account) ([]util.URL, []mastodon.Mention, []mast
return urls, []mastodon.Mention{}, []mastodon.Tag{}, len(urls)
}
func NewStatusItem(item *mastodon.Status, filters []*mastodon.Filter, timeline string, pinned bool) (sitem Item) {
filtered := filtered{inUse: false}
func NewStatusItem(item *mastodon.Status, pinned bool) (sitem Item) {
filtered := filtered{InUse: false}
if item == nil {
return &StatusItem{id: newID(), item: item, showSpoiler: false, filtered: filtered, pinned: pinned}
}
s := util.StatusOrReblog(item)
content := s.Content
if s.Sensitive {
content += "\n" + s.SpoilerText
}
content = strings.ToLower(content)
for _, f := range filters {
apply := false
for _, c := range f.Context {
if timeline == c {
apply = true
break
}
}
if !apply {
continue
}
if f.WholeWord {
lines := strings.Split(content, "\n")
var stripped []string
for _, l := range lines {
var words []string
words = append(words, strings.Split(l, " ")...)
for _, w := range words {
ns := strings.TrimSpace(w)
ns = strings.TrimFunc(ns, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
})
stripped = append(stripped, ns)
}
}
filter := strings.Split(strings.ToLower(f.Phrase), " ")
for i := 0; i+len(filter)-1 < len(stripped); i++ {
if strings.ToLower(f.Phrase) == strings.Join(stripped[i:i+len(filter)], " ") {
filtered.inUse = true
filtered.name = f.Phrase
break
}
}
} else {
if strings.Contains(s.Content, strings.ToLower(f.Phrase)) {
filtered.inUse = true
filtered.name = f.Phrase
}
if strings.Contains(s.SpoilerText, strings.ToLower(f.Phrase)) {
filtered.inUse = true
filtered.name = f.Phrase
}
}
if filtered.inUse {
break
}
for _, f := range s.Filtered {
filtered.InUse = true
filtered.Filters = append(filtered.Filters,
filter{
Type: f.Filter.FilterAction,
Values: f.KeywordMatches,
Where: f.Filter.Context,
})
}
sitem = &StatusItem{id: newID(), item: item, showSpoiler: false, filtered: filtered, pinned: pinned}
return sitem
}
func NewStatusItemID(item *mastodon.Status, pinned bool, id uint) (sitem Item) {
sitem = NewStatusItem(item, pinned)
sitem.(*StatusItem).id = id
return sitem
}
type StatusItem struct {
id uint
item *mastodon.Status
showSpoiler bool
forceView bool
filtered filtered
pinned bool
}
@ -150,11 +123,11 @@ func (s *StatusItem) Type() MastodonType {
return StatusType
}
func (s *StatusItem) ToggleSpoiler() {
func (s *StatusItem) ToggleCW() {
s.showSpoiler = !s.showSpoiler
}
func (s *StatusItem) ShowSpoiler() bool {
func (s *StatusItem) ShowCW() bool {
return s.showSpoiler
}
@ -166,14 +139,80 @@ func (s *StatusItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int
return getUrlsStatus(s.item)
}
func (s *StatusItem) Filtered() (bool, string) {
return s.filtered.inUse, s.filtered.name
func (s *StatusItem) Filtered(tl config.FeedType) (bool, string, string, bool) {
if !s.filtered.InUse || s.forceView {
return false, "", "", true
}
words := []string{}
t := ""
for _, f := range s.filtered.Filters {
used := false
for _, w := range f.Where {
switch w {
case "home", "special":
if tl == config.TimelineHome || tl == config.List {
used = true
}
case "thread":
if tl == config.Thread || tl == config.Conversations {
used = true
}
case "notifications":
if tl == config.Notifications {
used = true
}
case "account":
if tl == config.User {
used = true
}
case "public":
where := []config.FeedType{
config.Favorites,
config.Favorited,
config.Boosts,
config.Tag,
config.Notifications,
config.TimelineHome,
config.TimelineHomeSpecial,
config.Conversations,
config.User,
config.List,
}
if !slices.Contains(where, tl) {
used = true
}
}
if used {
words = append(words, f.Values...)
if t == "" || t == "warn" {
t = f.Type
}
break
}
}
}
return len(words) > 0, t, strings.Join(words, ", "), s.forceView
}
func (s *StatusItem) ForceViewFilter() {
s.forceView = true
}
func (s *StatusItem) Pinned() bool {
return s.pinned
}
func (s *StatusItem) Refetch(ac *AccountClient) bool {
ns, err := ac.GetStatus(s.item.ID)
if err != nil {
return false
}
nsi := NewStatusItemID(ns, s.pinned, s.id)
*s = *nsi.(*StatusItem)
return true
}
func NewStatusHistoryItem(item *mastodon.StatusHistory) (sitem Item) {
return &StatusHistoryItem{id: newID(), item: item, showSpoiler: false}
}
@ -192,11 +231,11 @@ func (s *StatusHistoryItem) Type() MastodonType {
return StatusHistoryType
}
func (s *StatusHistoryItem) ToggleSpoiler() {
func (s *StatusHistoryItem) ToggleCW() {
s.showSpoiler = !s.showSpoiler
}
func (s *StatusHistoryItem) ShowSpoiler() bool {
func (s *StatusHistoryItem) ShowCW() bool {
return s.showSpoiler
}
@ -217,14 +256,20 @@ func (s *StatusHistoryItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.T
return getUrlsStatus(&status)
}
func (s *StatusHistoryItem) Filtered() (bool, string) {
return false, ""
func (s *StatusHistoryItem) Filtered(config.FeedType) (bool, string, string, bool) {
return false, "", "", true
}
func (t *StatusHistoryItem) ForceViewFilter() {}
func (s *StatusHistoryItem) Pinned() bool {
return false
}
func (s *StatusHistoryItem) Refetch(ac *AccountClient) bool {
return false
}
func NewUserItem(item *User, profile bool) Item {
return &UserItem{id: newID(), item: item, profile: profile}
}
@ -246,10 +291,10 @@ func (u *UserItem) Type() MastodonType {
return UserType
}
func (u *UserItem) ToggleSpoiler() {
func (u *UserItem) ToggleCW() {
}
func (u *UserItem) ShowSpoiler() bool {
func (u *UserItem) ShowCW() bool {
return false
}
@ -261,16 +306,22 @@ func (u *UserItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int)
return getUrlsUser(u.item.Data)
}
func (s *UserItem) Filtered() (bool, string) {
return false, ""
func (u *UserItem) Filtered(config.FeedType) (bool, string, string, bool) {
return false, "", "", true
}
func (u *UserItem) ForceViewFilter() {}
func (u *UserItem) Pinned() bool {
return false
}
func NewNotificationItem(item *mastodon.Notification, user *User, filters []*mastodon.Filter) (nitem Item) {
status := NewStatusItem(item.Status, filters, "notifications", false)
func (u *UserItem) Refetch(ac *AccountClient) bool {
return false
}
func NewNotificationItem(item *mastodon.Notification, user *User) (nitem Item) {
status := NewStatusItem(item.Status, false)
nitem = &NotificationItem{
id: newID(),
item: item,
@ -304,11 +355,11 @@ func (n *NotificationItem) Type() MastodonType {
return NotificationType
}
func (n *NotificationItem) ToggleSpoiler() {
func (n *NotificationItem) ToggleCW() {
n.showSpoiler = !n.showSpoiler
}
func (n *NotificationItem) ShowSpoiler() bool {
func (n *NotificationItem) ShowCW() bool {
return n.showSpoiler
}
@ -344,14 +395,20 @@ func (n *NotificationItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Ta
}
}
func (n *NotificationItem) Filtered() (bool, string) {
return false, ""
func (n *NotificationItem) Filtered(config.FeedType) (bool, string, string, bool) {
return false, "", "", true
}
func (n *NotificationItem) ForceViewFilter() {}
func (n *NotificationItem) Pinned() bool {
return false
}
func (n *NotificationItem) Refetch(ac *AccountClient) bool {
return false
}
func NewListsItem(item *mastodon.List) Item {
return &ListItem{id: newID(), item: item, showSpoiler: true}
}
@ -370,10 +427,10 @@ func (s *ListItem) Type() MastodonType {
return ListsType
}
func (s *ListItem) ToggleSpoiler() {
func (s *ListItem) ToggleCW() {
}
func (s *ListItem) ShowSpoiler() bool {
func (s *ListItem) ShowCW() bool {
return true
}
@ -385,14 +442,20 @@ func (s *ListItem) URLs() ([]util.URL, []mastodon.Mention, []mastodon.Tag, int)
return nil, nil, nil, 0
}
func (s *ListItem) Filtered() (bool, string) {
return false, ""
func (s *ListItem) Filtered(config.FeedType) (bool, string, string, bool) {
return false, "", "", true
}
func (l *ListItem) ForceViewFilter() {}
func (n *ListItem) Pinned() bool {
return false
}
func (l *ListItem) Refetch(ac *AccountClient) bool {
return false
}
func NewTagItem(item *mastodon.Tag) Item {
return &TagItem{id: newID(), item: item, showSpoiler: true}
}
@ -411,10 +474,10 @@ func (t *TagItem) Type() MastodonType {
return TagType
}
func (t *TagItem) ToggleSpoiler() {
func (t *TagItem) ToggleCW() {
}
func (t *TagItem) ShowSpoiler() bool {
func (t *TagItem) ShowCW() bool {
return true
}
@ -426,10 +489,16 @@ 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) Filtered(config.FeedType) (bool, string, string, bool) {
return false, "", "", true
}
func (t *TagItem) ForceViewFilter() {}
func (t *TagItem) Pinned() bool {
return false
}
func (t *TagItem) Refetch(ac *AccountClient) bool {
return false
}

4
api/status.go

@ -90,3 +90,7 @@ func (ac *AccountClient) Unbookmark(s *mastodon.Status) (*mastodon.Status, error
func (ac *AccountClient) DeleteStatus(s *mastodon.Status) error {
return ac.Client.DeleteStatus(context.Background(), util.StatusOrReblog(s).ID)
}
func (ac *AccountClient) GetStatus(id mastodon.ID) (*mastodon.Status, error) {
return ac.Client.GetStatus(context.Background(), id)
}

40
config.example.ini

@ -13,8 +13,12 @@ mouse-support=false
# 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
# Available timelines: home, direct, local, federated, special-all,
# special-boosts, special-replies, bookmarks, saved, favorited, notifications,
# lists, tag
#
# The ones named special-* are the home timeline with only boosts and/or
# replies. All contains both, -boosts only boosts and -replies only replies.
#
# Tag is special as you need to add the tag after, see the example below.
#
@ -134,6 +138,11 @@ show-help=true
# default=false
stick-to-top=false
# If you want to display the username of the person being boosted instead of the
# person that boosted.
# default=false
show-boosted-user=false
# 0 = No terminal title
# 1 = Show title in terminal and top bar
# 2 = Only show terminal title, and no top bar in tut.
@ -164,11 +173,14 @@ leader-timeout=1000
# of two parts first the action then the shortcut. And they're separated by a
# comma.
#
# 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, stick-to-top, tag, tags, window, list-placement,
# list-split, proportions
# Available commands: home, direct, local, federated, special-all,
# special-boosts, special-replies, clear-notifications, compose, edit, history,
# blocking, bookmarks, refetch, saved, favorited, boosts, favorites, following,
# followers, muting, newer, preferences, profile, notifications, lists,
# stick-to-top, tag, tags, window, list-placement, list-split, proportions
#
# The ones named special-* are the home timeline with only boosts and/or
# replies. All contains both, -boosts only boosts and -replies only replies.
#
# 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
@ -622,9 +634,13 @@ status-view-focus="[V]iew",'v','V'
# default="[Y]ank",'y','Y'
status-yank="[Y]ank",'y','Y'
# Remove the spoiler
# default="Press [Z] to toggle spoiler",'z','Z'
status-toggle-spoiler="Press [Z] to toggle spoiler",'z','Z'
# Show the content in a content warning
# default="Press [Z] to toggle cw",'z','Z'
status-toggle-cw="Press [Z] to toggle cw",'z','Z'
# Show the content of a filtered toot
# default="Press [Z] to view filtered toot",'z','Z'
status-show-filtered="Press [Z] to view filtered toot",'z','Z'
# View avatar
# default="[A]vatar",'a','A'
@ -694,9 +710,9 @@ tag-open-feed="[O]pen",'o','O'
# default="[F]ollow","Un[F]ollow",'f','F'
tag-follow="[F]ollow","Un[F]ollow",'f','F'
# Edit spoiler text on new toot
# Edit content warning text on new toot
# default="[C]W text",'c','C'
compose-edit-spoiler="[C]W text",'c','C'
compose-edit-cw="[C]W text",'c','C'
# Edit the text on new toot
# default="[E]dit text",'e','E'

111
config/config.go

@ -55,6 +55,9 @@ const (
LeaderDirect
LeaderLocal
LeaderFederated
LeaderSpecialAll
LeaderSpecialBoosts
LeaderSpecialReplies
LeaderClearNotifications
LeaderCompose
LeaderEdit
@ -74,6 +77,7 @@ const (
LeaderProportions
LeaderNotifications
LeaderLists
LeaderRefetch
LeaderTag
LeaderTags
LeaderStickToTop
@ -103,6 +107,7 @@ const (
Thread
TimelineFederated
TimelineHome
TimelineHomeSpecial
TimelineLocal
Conversations
User
@ -142,7 +147,6 @@ type General struct {
DateFormat string
DateRelative int
MaxWidth int
StartTimeline FeedType
NotificationFeed bool
QuoteReply bool
CharLimit int
@ -163,6 +167,7 @@ type General struct {
Timelines []Timeline
StickToTop bool
NotificationsToHide []NotificationToHide
ShowBoostedUser bool
}
type Style struct {
@ -407,21 +412,22 @@ type Input struct {
MainNextWindow Key
MainCompose Key
StatusAvatar Key
StatusBoost Key
StatusDelete Key
StatusEdit Key
StatusFavorite Key
StatusMedia Key
StatusLinks Key
StatusPoll Key
StatusReply Key
StatusBookmark Key
StatusThread Key
StatusUser Key
StatusViewFocus Key
StatusYank Key
StatusToggleSpoiler Key
StatusAvatar Key
StatusBoost Key
StatusDelete Key
StatusEdit Key
StatusFavorite Key
StatusMedia Key
StatusLinks Key
StatusPoll Key
StatusReply Key
StatusBookmark Key
StatusThread Key
StatusUser Key
StatusViewFocus Key
StatusYank Key
StatusToggleCW Key
StatusShowFiltered Key
UserAvatar Key
UserBlock Key
@ -444,7 +450,7 @@ type Input struct {
LinkOpen Key
LinkYank Key
ComposeEditSpoiler Key
ComposeEditCW Key
ComposeEditText Key
ComposeIncludeQuote Key
ComposeMediaFocus Key
@ -831,18 +837,6 @@ func parseGeneral(cfg *ini.File) General {
}
general.DateRelative = dateRelative
tl := cfg.Section("general").Key("timeline").In("home", []string{"home", "direct", "local", "federated"})
switch tl {
case "direct":
general.StartTimeline = Conversations
case "local":
general.StartTimeline = TimelineLocal
case "federated":
general.StartTimeline = TimelineFederated
default:
general.StartTimeline = TimelineHome
}
general.NotificationFeed = cfg.Section("general").Key("notification-feed").MustBool(true)
general.QuoteReply = cfg.Section("general").Key("quote-reply").MustBool(false)
general.CharLimit = cfg.Section("general").Key("char-limit").MustInt(500)
@ -853,6 +847,7 @@ func parseGeneral(cfg *ini.File) General {
general.ShowHelp = cfg.Section("general").Key("show-help").MustBool(true)
general.RedrawUI = cfg.Section("general").Key("redraw-ui").MustBool(true)
general.StickToTop = cfg.Section("general").Key("stick-to-top").MustBool(false)
general.ShowBoostedUser = cfg.Section("general").Key("show-boosted-user").MustBool(false)
lp := cfg.Section("general").Key("list-placement").In("left", []string{"left", "right", "top", "bottom"})
switch lp {
@ -926,6 +921,12 @@ func parseGeneral(cfg *ini.File) General {
la.Command = LeaderLocal
case "federated":
la.Command = LeaderFederated
case "special-all":
la.Command = LeaderSpecialAll
case "special-boosts":
la.Command = LeaderSpecialBoosts
case "special-replies":
la.Command = LeaderSpecialReplies
case "clear-notifications":
la.Command = LeaderClearNotifications
case "compose":
@ -962,6 +963,8 @@ func parseGeneral(cfg *ini.File) General {
la.Command = LeaderLists
case "stick-to-top":
la.Command = LeaderStickToTop
case "refetch":
la.Command = LeaderRefetch
case "tag":
la.Command = LeaderTag
la.Subaction = subaction
@ -1017,6 +1020,8 @@ func parseGeneral(cfg *ini.File) General {
switch cmd {
case "home":
tl.FeedType = TimelineHome
case "special":
tl.FeedType = TimelineHomeSpecial
case "direct":
tl.FeedType = Conversations
case "local":
@ -1372,21 +1377,22 @@ func parseInput(cfg *ini.File) Input {
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),
StatusDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false),
StatusEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false),
StatusFavorite: inputStrOrErr([]string{"\"[F]avorite\"", "\"Un[F]avorite\"", "'f'", "'F'"}, true),
StatusMedia: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false),
StatusLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false),
StatusPoll: inputStrOrErr([]string{"\"[P]oll\"", "'p'", "'P'"}, false),
StatusReply: inputStrOrErr([]string{"\"[R]eply\"", "'r'", "'R'"}, false),
StatusBookmark: inputStrOrErr([]string{"\"[S]ave\"", "\"Un[S]ave\"", "'s'", "'S'"}, true),
StatusThread: inputStrOrErr([]string{"\"[T]hread\"", "'t'", "'T'"}, false),
StatusUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false),
StatusViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false),
StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false),
StatusToggleSpoiler: inputStrOrErr([]string{"\"Press [Z] to toggle spoiler\"", "'z'", "'Z'"}, false),
StatusAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false),
StatusBoost: inputStrOrErr([]string{"\"[B]oost\"", "\"Un[B]oost\"", "'b'", "'B'"}, true),
StatusDelete: inputStrOrErr([]string{"\"[D]elete\"", "'d'", "'D'"}, false),
StatusEdit: inputStrOrErr([]string{"\"[E]dit\"", "'e'", "'E'"}, false),
StatusFavorite: inputStrOrErr([]string{"\"[F]avorite\"", "\"Un[F]avorite\"", "'f'", "'F'"}, true),
StatusMedia: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false),
StatusLinks: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false),
StatusPoll: inputStrOrErr([]string{"\"[P]oll\"", "'p'", "'P'"}, false),
StatusReply: inputStrOrErr([]string{"\"[R]eply\"", "'r'", "'R'"}, false),
StatusBookmark: inputStrOrErr([]string{"\"[S]ave\"", "\"Un[S]ave\"", "'s'", "'S'"}, true),
StatusThread: inputStrOrErr([]string{"\"[T]hread\"", "'t'", "'T'"}, false),
StatusUser: inputStrOrErr([]string{"\"[U]ser\"", "'u'", "'U'"}, false),
StatusViewFocus: inputStrOrErr([]string{"\"[V]iew\"", "'v'", "'V'"}, false),
StatusYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false),
StatusToggleCW: inputStrOrErr([]string{"\"Press [Z] to toggle CW\"", "'z'", "'Z'"}, false),
StatusShowFiltered: inputStrOrErr([]string{"\"Press [Z] to view filtered toot\"", "'z'", "'Z'"}, false),
UserAvatar: inputStrOrErr([]string{"\"[A]vatar\"", "'a'", "'A'"}, false),
UserBlock: inputStrOrErr([]string{"\"[B]lock\"", "\"Un[B]lock\"", "'b'", "'B'"}, true),
@ -1409,7 +1415,7 @@ func parseInput(cfg *ini.File) Input {
LinkOpen: inputStrOrErr([]string{"\"[O]pen\"", "'o'", "'O'"}, false),
LinkYank: inputStrOrErr([]string{"\"[Y]ank\"", "'y'", "'Y'"}, false),
ComposeEditSpoiler: inputStrOrErr([]string{"\"[C]W Text\"", "'c'", "'C'"}, false),
ComposeEditCW: inputStrOrErr([]string{"\"[C]W Text\"", "'c'", "'C'"}, false),
ComposeEditText: inputStrOrErr([]string{"\"[E]dit text\"", "'e'", "'E'"}, false),
ComposeIncludeQuote: inputStrOrErr([]string{"\"[I]nclude quote\"", "'i'", "'I'"}, false),
ComposeMediaFocus: inputStrOrErr([]string{"\"[M]edia\"", "'m'", "'M'"}, false),
@ -1467,7 +1473,13 @@ func parseInput(cfg *ini.File) Input {
ic.StatusUser = inputOrErr(cfg, "status-user", false, ic.StatusUser)
ic.StatusViewFocus = inputOrErr(cfg, "status-view-focus", false, ic.StatusViewFocus)
ic.StatusYank = inputOrErr(cfg, "status-yank", false, ic.StatusYank)
ic.StatusToggleSpoiler = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleSpoiler)
ic.StatusToggleCW = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleCW)
ts := cfg.Section("input").Key("status-toggle-spoiler").MustString("")
if ts != "" {
ic.StatusToggleCW = inputOrErr(cfg, "status-toggle-spoiler", false, ic.StatusToggleCW)
} else {
ic.StatusToggleCW = inputOrErr(cfg, "status-toggle-cw", false, ic.StatusToggleCW)
}
ic.UserAvatar = inputOrErr(cfg, "user-avatar", false, ic.UserAvatar)
ic.UserBlock = inputOrErr(cfg, "user-block", true, ic.UserBlock)
@ -1490,7 +1502,12 @@ func parseInput(cfg *ini.File) Input {
ic.LinkOpen = inputOrErr(cfg, "link-open", false, ic.LinkOpen)
ic.LinkYank = inputOrErr(cfg, "link-yank", false, ic.LinkYank)
ic.ComposeEditSpoiler = inputOrErr(cfg, "compose-edit-spoiler", false, ic.ComposeEditSpoiler)
es := cfg.Section("input").Key("compose-edit-spoiler").MustString("")
if es != "" {
ic.ComposeEditCW = inputOrErr(cfg, "compose-edit-spoiler", false, ic.ComposeEditCW)
} else {
ic.ComposeEditCW = inputOrErr(cfg, "compose-edit-cw", false, ic.ComposeEditCW)
}
ic.ComposeEditText = inputOrErr(cfg, "compose-edit-text", false, ic.ComposeEditText)
ic.ComposeIncludeQuote = inputOrErr(cfg, "compose-include-quote", false, ic.ComposeIncludeQuote)
ic.ComposeMediaFocus = inputOrErr(cfg, "compose-media-focus", false, ic.ComposeMediaFocus)

40
config/default_config.go

@ -15,8 +15,12 @@ mouse-support=false
# 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
# Available timelines: home, direct, local, federated, special-all,
# special-boosts, special-replies, bookmarks, saved, favorited, notifications,
# lists, tag
#
# The ones named special-* are the home timeline with only boosts and/or
# replies. All contains both, -boosts only boosts and -replies only replies.
#
# Tag is special as you need to add the tag after, see the example below.
#
@ -136,6 +140,11 @@ show-help=true
# default=false
stick-to-top=false
# If you want to display the username of the person being boosted instead of the
# person that boosted.
# default=false
show-boosted-user=false
# 0 = No terminal title
# 1 = Show title in terminal and top bar
# 2 = Only show terminal title, and no top bar in tut.
@ -166,11 +175,14 @@ leader-timeout=1000
# of two parts first the action then the shortcut. And they're separated by a
# comma.
#
# 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, stick-to-top, tag, tags, window, list-placement,
# list-split, proportions
# Available commands: home, direct, local, federated, special-all,
# special-boosts, special-replies, clear-notifications, compose, edit, history,
# blocking, bookmarks, refetch, saved, favorited, boosts, favorites, following,
# followers, muting, newer, preferences, profile, notifications, lists,
# stick-to-top, tag, tags, window, list-placement, list-split, proportions
#
# The ones named special-* are the home timeline with only boosts and/or
# replies. All contains both, -boosts only boosts and -replies only replies.
#
# 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
@ -624,9 +636,13 @@ status-view-focus="[V]iew",'v','V'
# default="[Y]ank",'y','Y'
status-yank="[Y]ank",'y','Y'
# Remove the spoiler
# default="Press [Z] to toggle spoiler",'z','Z'
status-toggle-spoiler="Press [Z] to toggle spoiler",'z','Z'
# Show the content in a content warning
# default="Press [Z] to toggle cw",'z','Z'
status-toggle-cw="Press [Z] to toggle cw",'z','Z'
# Show the content of a filtered toot
# default="Press [Z] to view filtered toot",'z','Z'
status-show-filtered="Press [Z] to view filtered toot",'z','Z'
# View avatar
# default="[A]vatar",'a','A'
@ -696,9 +712,9 @@ tag-open-feed="[O]pen",'o','O'
# default="[F]ollow","Un[F]ollow",'f','F'
tag-follow="[F]ollow","Un[F]ollow",'f','F'
# Edit spoiler text on new toot
# Edit content warning text on new toot
# default="[C]W text",'c','C'
compose-edit-spoiler="[C]W text",'c','C'
compose-edit-cw="[C]W text",'c','C'
# Edit the text on new toot
# default="[E]dit text",'e','E'

7
config/help.tmpl

@ -30,10 +30,10 @@ Here's a list of supported commands.
{{- Color .Style.TextSpecial2 }}{{ Flags "b" }} :quit{{ Flags "-" }}{{ Color .Style.Text }}
Exit the program
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:timeline{{ Flags "-" }}{{ Color .Style.Text }} home|local|federated|direct|notifications|favorited
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:timeline{{ Flags "-" }}{{ Color .Style.Text }} home|local|federated|direct|notifications|favorited|special-all|special-boosts|special-replies
Open selected timeline
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tl{{ Flags "-" }}{{ Color .Style.Text }} h|l|f|d|n|fav
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:tl{{ Flags "-" }}{{ Color .Style.Text }} h|l|f|d|n|fav|sa|sb|sr
Shorter form of the former command *:timeline*
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:blocking{{ Flags "-" }}{{ Color .Style.Text }}
@ -97,6 +97,9 @@ Here's a list of supported commands.
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:proportions{{ Flags "-" }}{{ Color .Style.Text }} [int] [int]
Set proportions for list and content with an int. First list proportion then content proportion
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:refetch{{ Flags "-" }}{{ Color .Style.Text }}
Refetch the current item
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:requests{{ Flags "-" }}{{ Color .Style.Text }}
See following requests

4
config/toot.tmpl

@ -20,9 +20,9 @@
{{- end }} {{- if .Toot.Edited -}}{{ Color .Style.Subtle }} (edited toot){{ end }}
{{ if .Toot.Spoiler -}}
{{ Color .Style.Text }}{{ .Toot.SpoilerText }}
{{ Color .Style.Text }}{{ .Toot.CWText }}
{{ if not .Toot.ShowSpoiler }}
{{ Color .Style.Subtle }}Press [z[] to show hidden text{{ Color .Style.Text }}
{{ Color .Style.Subtle }}{{ .Toot.CWlabel }}{{ Color .Style.Text }}
{{ end }}
{{ end -}}
{{- if or (not .Toot.Spoiler) (.Toot.ShowSpoiler) -}}

25
feed/feed.go

@ -78,6 +78,9 @@ func (f *Feed) filteredList() []api.Item {
for _, fd := range f.items {
switch x := fd.Raw().(type) {
case *mastodon.Status:
if f.Type() == config.TimelineHomeSpecial && x.Reblog == nil && x.InReplyToID == nil {
continue
}
if x.Reblog != nil && !f.showBoosts {
continue
}
@ -85,6 +88,10 @@ func (f *Feed) filteredList() []api.Item {
continue
}
}
inUse, fType, _, _ := fd.Filtered(f.feedType)
if inUse && fType == "hide" {
continue
}
filtered = append(filtered, fd)
}
r := f.sticky
@ -684,7 +691,7 @@ func (f *Feed) startStream(rec *api.Receiver, timeline string, err error) {
for e := range rec.Ch {
switch t := e.(type) {
case *mastodon.UpdateEvent:
s := api.NewStatusItem(t.Status, f.accountClient.Filters, timeline, false)
s := api.NewStatusItem(t.Status, false)
f.itemsMux.Lock()
found := false
if len(f.streams) > 0 {
@ -766,7 +773,7 @@ func (f *Feed) startStreamNotification(rec *api.Receiver, timeline string, err e
&api.User{
Data: &t.Notification.Account,
Relation: rel[0],
}, f.accountClient.Filters)
})
f.itemsMux.Lock()
f.items = append([]api.Item{s}, f.items...)
nft := DesktopNotificationNone
@ -831,6 +838,20 @@ func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, showBoosts bool,
return feed
}
func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.TimelineHomeSpecial, cnf, showBoosts, showReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) }
feed.startStream(feed.accountClient.NewHomeStream())
feed.close = func() {
for _, s := range feed.streams {
feed.accountClient.RemoveHomeReceiver(s)
}
}
return feed
}
func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.TimelineFederated, cnf, showBoosts, showReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineFederated) }

6
go.mod

@ -3,7 +3,7 @@ module github.com/RasmusLindroth/tut
go 1.18
require (
github.com/RasmusLindroth/go-mastodon v0.0.16
github.com/RasmusLindroth/go-mastodon v0.0.17
github.com/atotto/clipboard v0.1.4
github.com/gdamore/tcell/v2 v2.5.3
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6
@ -11,10 +11,10 @@ require (
github.com/icza/gox v0.0.0-20221026131554-a08a8cdc726a
github.com/microcosm-cc/bluemonday v1.0.21
github.com/pelletier/go-toml/v2 v2.0.6
github.com/rivo/tview v0.0.0-20221212150847-19d943d59543
github.com/rivo/tview v0.0.0-20221217182043-ccce554c3803
github.com/rivo/uniseg v0.4.3
github.com/spf13/pflag v1.0.5
golang.org/x/exp v0.0.0-20221212164502-fae10dda9338
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
golang.org/x/net v0.4.0
gopkg.in/ini.v1 v1.67.0
)

8
go.sum

@ -1,5 +1,5 @@
github.com/RasmusLindroth/go-mastodon v0.0.16 h1:87lm4+TE6cNguL/gbfHDYqbOd1Jblnpy8J0l1O3/1wo=
github.com/RasmusLindroth/go-mastodon v0.0.16/go.mod h1:Lr6n8V1U2b+9P89YZKsICkNc+oNeJXkygY7raei9SXE=
github.com/RasmusLindroth/go-mastodon v0.0.17 h1:PUR4YS9ORe62ZSabvZVwxROZvrcMuNVC/8Y/D/d6dFQ=
github.com/RasmusLindroth/go-mastodon v0.0.17/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=
@ -40,6 +40,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/tview v0.0.0-20221212150847-19d943d59543 h1:qu4/1SXI23subKkH50FN7t6r0tPg7i7jI48M5kZ2qEE=
github.com/rivo/tview v0.0.0-20221212150847-19d943d59543/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
github.com/rivo/tview v0.0.0-20221217182043-ccce554c3803 h1:gaknGRzW4g4I+5sGu4X81BZbROJ0j96ap9xnEbcZhXA=
github.com/rivo/tview v0.0.0-20221217182043-ccce554c3803/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
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=
@ -58,6 +60,8 @@ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJ
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
golang.org/x/exp v0.0.0-20221212164502-fae10dda9338 h1:OvjRkcNHnf6/W5FZXSxODbxwD+X7fspczG7Jn/xQVD4=
golang.org/x/exp v0.0.0-20221212164502-fae10dda9338/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

2
main.go

@ -8,7 +8,7 @@ import (
"github.com/rivo/tview"
)
const version = "1.0.26"
const version = "1.0.27"
func main() {
util.SetTerminalTitle("tut")

18
ui/cmdbar.go

@ -162,6 +162,15 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
case "federated", "f":
c.tutView.FederatedCommand()
c.Back()
case "special-all", "sa":
c.tutView.SpecialCommand(true, true)
c.Back()
case "special-boosts", "sb":
c.tutView.SpecialCommand(true, false)
c.Back()
case "special-replies", "sr":
c.tutView.SpecialCommand(false, true)
c.Back()
case "direct", "d":
c.tutView.DirectCommand()
c.Back()
@ -206,6 +215,9 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
NewUserSearchFeed(c.tutView, user),
)
c.Back()
case ":refetch":
c.tutView.RefetchCommand()
c.Back()
case ":stick-to-top":
c.tutView.ToggleStickToTop()
c.Back()
@ -242,16 +254,16 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
func (c *CmdBar) Autocomplete(curr string) []string {
var entries []string
words := strings.Split(":blocking,:boosts,:bookmarks,:clear-notifications,:compose,:favorites,:favorited,:follow-tag,:followers,:following,:help,:h,:history,:lists,:list-placement,:list-split,:muting,:newer,:preferences,:profile,:proportions,:requests,:saved,:stick-to-top,:tag,:timeline,:tl,:unfollow-tag,:user,:window,:quit,:q", ",")
words := strings.Split(":blocking,:boosts,:bookmarks,:clear-notifications,:compose,:favorites,:favorited,:follow-tag,:followers,:following,:help,:h,:history,:lists,:list-placement,:list-split,:muting,:newer,:preferences,:profile,:proportions,:refetch,:requests,:saved,:stick-to-top,:tag,:timeline,:tl,:unfollow-tag,:user,:window,:quit,:q", ",")
if curr == "" {
return entries
}
if len(curr) > 2 && curr[:3] == ":tl" {
words = strings.Split(":tl home,:tl notifications,:tl local,:tl federated,:tl direct,:tl favorited", ",")
words = strings.Split(":tl home,:tl notifications,:tl local,:tl federated,:tl direct,:tl favorited,:tl special-all,:tl special-boosts,:tl-special-replies", ",")
}
if len(curr) > 8 && curr[:9] == ":timeline" {
words = strings.Split(":timeline home,:timeline notifications,:timeline local,:timeline federated,:timeline direct,:timeline favorited", ",")
words = strings.Split(":timeline home,:timeline notifications,:timeline local,:timeline federated,:timeline direct,:timeline favorited,:timeline special-all,:timeline special-boosts,:timeline special-replies", ",")
}
if len(curr) > 14 && curr[:15] == ":list-placement" {
words = strings.Split(":list-placement top,:list-placement right,:list-placement bottom,:list-placement left", ",")

18
ui/commands.go

@ -71,6 +71,12 @@ func (tv *TutView) FederatedCommand() {
)
}
func (tv *TutView) SpecialCommand(boosts, replies bool) {
tv.Timeline.AddFeed(
NewHomeSpecialFeed(tv, boosts, replies),
)
}
func (tv *TutView) DirectCommand() {
tv.Timeline.AddFeed(
NewConversationsFeed(tv),
@ -269,3 +275,15 @@ func (tv *TutView) ClearNotificationsCommand() {
func (tv *TutView) ToggleStickToTop() {
tv.tut.Config.General.StickToTop = !tv.tut.Config.General.StickToTop
}
func (tv *TutView) RefetchCommand() {
item, itemErr := tv.GetCurrentItem()
f := tv.GetCurrentFeed()
if itemErr != nil {
return
}
update := item.Refetch(tv.tut.Client)
if update {
f.DrawContent()
}
}

28
ui/composeview.go

@ -24,7 +24,7 @@ type msgToot struct {
Edit *mastodon.Status
MediaIDs []mastodon.ID
Sensitive bool
SpoilerText string
CWText string
ScheduledAt *time.Time
QuoteIncluded bool
Visibility string
@ -105,7 +105,7 @@ const (
func (cv *ComposeView) msgLength() int {
m := cv.msg
charCount := uniseg.GraphemeClusterCount(m.Text)
spoilerCount := uniseg.GraphemeClusterCount(m.SpoilerText)
spoilerCount := uniseg.GraphemeClusterCount(m.CWText)
totalCount := charCount
if m.Sensitive {
totalCount += spoilerCount
@ -122,7 +122,7 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) {
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeEditText, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeVisibility, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeToggleContentWarning, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeEditSpoiler, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeEditCW, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeMediaFocus, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposePoll, true))
items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeLanguage, true))
@ -165,7 +165,7 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status)
msg.Reply = reply
if reply.Sensitive {
msg.Sensitive = true
msg.SpoilerText = reply.SpoilerText
msg.CWText = reply.SpoilerText
}
if visibilities[reply.Visibility] > visibilities[visibility] {
visibility = reply.Visibility
@ -188,7 +188,7 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status)
msg.Edit = edit
msg.ID = source.ID
msg.Text = source.Text
msg.SpoilerText = source.SpoilerText
msg.CWText = source.SpoilerText
for _, mid := range edit.MediaAttachments {
msg.MediaIDs = append(msg.MediaIDs, mid.ID)
}
@ -268,14 +268,14 @@ func (cv *ComposeView) EditText() {
}
func (cv *ComposeView) EditSpoiler() {
text, err := OpenEditor(cv.tutView, cv.msg.SpoilerText)
text, err := OpenEditor(cv.tutView, cv.msg.CWText)
if err != nil {
cv.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err),
)
return
}
cv.msg.SpoilerText = text
cv.msg.CWText = text
cv.UpdateContent()
}
@ -285,7 +285,7 @@ func (cv *ComposeView) ToggleCW() {
}
func (cv *ComposeView) UpdateContent() {
cv.info.SetText(fmt.Sprintf("Chars left: %d\nSpoiler: %t\nHas poll: %t\n", cv.msgLength(), cv.msg.Sensitive, cv.tutView.PollView.HasPoll()))
cv.info.SetText(fmt.Sprintf("Chars left: %d\nCW: %t\nHas poll: %t\n", cv.msgLength(), cv.msg.Sensitive, cv.tutView.PollView.HasPoll()))
normal := config.ColorMark(cv.tutView.tut.Config.Style.Text)
subtleColor := config.ColorMark(cv.tutView.tut.Config.Style.Subtle)
warningColor := config.ColorMark(cv.tutView.tut.Config.Style.WarningText)
@ -302,17 +302,17 @@ func (cv *ComposeView) UpdateContent() {
}
outputHead += subtleColor + "Replying to " + tview.Escape(acct) + "\n" + normal
}
if cv.msg.SpoilerText != "" && !cv.msg.Sensitive {
outputHead += warningColor + "You have entered spoiler text, but haven't set an content warning. Do it by pressing " + tview.Escape("[T]") + "\n\n" + normal
if cv.msg.CWText != "" && !cv.msg.Sensitive {
outputHead += warningColor + "You have entered content warning text, but haven't set an content warning. Do it by pressing " + tview.Escape("[T]") + "\n\n" + normal
}
if cv.msg.Sensitive && cv.msg.SpoilerText == "" {
if cv.msg.Sensitive && cv.msg.CWText == "" {
outputHead += warningColor + "You have added an content warning, but haven't set any text above the hidden text. Do it by pressing " + tview.Escape("[C]") + "\n\n" + normal
}
if cv.msg.Sensitive && cv.msg.SpoilerText != "" {
if cv.msg.Sensitive && cv.msg.CWText != "" {
outputHead += subtleColor + "Content warning\n\n" + normal
outputHead += tview.Escape(cv.msg.SpoilerText)
outputHead += tview.Escape(cv.msg.CWText)
outputHead += "\n\n" + subtleColor + "---hidden content below---\n\n" + normal
}
output = outputHead + normal + tview.Escape(cv.msg.Text)
@ -425,7 +425,7 @@ func (cv *ComposeView) Post() {
}
if toot.Sensitive {
send.Sensitive = true
send.SpoilerText = toot.SpoilerText
send.SpoilerText = toot.CWText
}
if cv.HasMedia() {

14
ui/feed.go

@ -153,6 +153,20 @@ func NewHomeFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
return fd
}
func NewHomeSpecialFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, showBoosts, showReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
}
go fd.update()
return fd
}
func NewFederatedFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
f := feed.NewTimelineFederated(tv.tut.Client, tv.tut.Config, showBoosts, showReplies)
f.LoadNewer()

44
ui/input.go

@ -114,6 +114,12 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
tv.LocalCommand()
case config.LeaderFederated:
tv.FederatedCommand()
case config.LeaderSpecialAll:
tv.SpecialCommand(true, true)
case config.LeaderSpecialBoosts:
tv.SpecialCommand(true, false)
case config.LeaderSpecialReplies:
tv.SpecialCommand(false, true)
case config.LeaderClearNotifications:
tv.ClearNotificationsCommand()
case config.LeaderCompose:
@ -150,6 +156,8 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
tv.ListsCommand()
case config.LeaderStickToTop:
tv.ToggleStickToTop()
case config.LeaderRefetch:
tv.RefetchCommand()
case config.LeaderTag:
tv.TagCommand(subaction)
case config.LeaderTags:
@ -305,7 +313,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey {
}
switch item.Type() {
case api.StatusType:
return tv.InputStatus(event, item, item.Raw().(*mastodon.Status), nil)
return tv.InputStatus(event, item, item.Raw().(*mastodon.Status), nil, fd.Data.Type())
case api.StatusHistoryType:
return tv.InputStatusHistory(event, item, item.Raw().(*mastodon.StatusHistory), nil)
case api.UserType, api.ProfileType:
@ -326,18 +334,18 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey {
return tv.InputUser(event, nd.User.Raw().(*api.User), InputUserNormal)
case "favourite":
user := nd.User.Raw().(*api.User)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data, config.Notifications)
case "reblog":
user := nd.User.Raw().(*api.User)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), user.Data, config.Notifications)
case "mention":
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications)
case "update":
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications)
case "status":
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications)
case "poll":
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil)
return tv.InputStatus(event, nd.Status, nd.Status.Raw().(*mastodon.Status), nil, config.Notifications)
case "follow_request":
return tv.InputUser(event, nd.User.Raw().(*api.User), InputUserFollowRequest)
}
@ -351,7 +359,7 @@ func (tv *TutView) InputItem(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mastodon.Status, nAcc *mastodon.Account) *tcell.EventKey {
func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mastodon.Status, nAcc *mastodon.Account, fd config.FeedType) *tcell.EventKey {
sr := util.StatusOrReblog(status)
hasMedia := len(sr.MediaAttachments) > 0
@ -501,12 +509,18 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas
copyToClipboard(sr.URL)
return nil
}
if tv.tut.Config.Input.StatusToggleSpoiler.Match(event.Key(), event.Rune()) {
if tv.tut.Config.Input.StatusToggleCW.Match(event.Key(), event.Rune()) {
filtered, _, _, forceView := item.Filtered(fd)
if filtered && !forceView {
item.ForceViewFilter()
tv.RedrawContent()
return nil
}
if !hasSpoiler {
return nil
}
if !item.ShowSpoiler() {
item.ToggleSpoiler()
if !item.ShowCW() {
item.ToggleCW()
tv.RedrawContent()
}
return nil
@ -551,12 +565,12 @@ func (tv *TutView) InputStatusHistory(event *tcell.EventKey, item api.Item, sr *
tv.SetPage(ViewFocus)
return nil
}
if tv.tut.Config.Input.StatusToggleSpoiler.Match(event.Key(), event.Rune()) {
if tv.tut.Config.Input.StatusToggleCW.Match(event.Key(), event.Rune()) {
if !hasSpoiler {
return nil
}
if !item.ShowSpoiler() {
item.ToggleSpoiler()
if !item.ShowCW() {
item.ToggleCW()
tv.RedrawContent()
}
return nil
@ -811,7 +825,7 @@ func (tv *TutView) InputLinkView(event *tcell.EventKey) *tcell.EventKey {
}
func (tv *TutView) InputComposeView(event *tcell.EventKey) *tcell.EventKey {
if tv.tut.Config.Input.ComposeEditSpoiler.Match(event.Key(), event.Rune()) {
if tv.tut.Config.Input.ComposeEditCW.Match(event.Key(), event.Rune()) {
tv.ComposeView.EditSpoiler()
return nil
}

11
ui/item.go

@ -28,6 +28,9 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) {
symbol = " ! "
}
acc := strings.TrimSpace(s.Account.Acct)
if cfg.General.ShowBoostedUser && s.Reblog != nil {
acc = strings.TrimSpace(s.Reblog.Account.Acct)
}
if s.Reblog != nil && cfg.General.ShowIcons {
acc = fmt.Sprintf("♺ %s", acc)
}
@ -78,7 +81,7 @@ func DrawListItem(cfg *config.Config, item api.Item) (string, string) {
func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview.Flex, ft config.FeedType) {
switch item.Type() {
case api.StatusType:
drawStatus(tv, item, item.Raw().(*mastodon.Status), main, controls, false, "")
drawStatus(tv, item, item.Raw().(*mastodon.Status), main, controls, ft, false, "")
case api.StatusHistoryType:
s := item.Raw().(*mastodon.StatusHistory)
status := mastodon.Status{
@ -91,7 +94,7 @@ func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview.
MediaAttachments: s.MediaAttachments,
Visibility: mastodon.VisibilityPublic,
}
drawStatus(tv, item, &status, main, controls, true, "")
drawStatus(tv, item, &status, main, controls, ft, true, "")
case api.UserType, api.ProfileType:
switch ft {
case config.FollowRequests:
@ -115,7 +118,7 @@ func DrawItem(tv *TutView, item api.Item, main *tview.TextView, controls *tview.
func DrawItemControls(tv *TutView, item api.Item, controls *tview.Flex, ft config.FeedType) {
switch item.Type() {
case api.StatusType:
drawStatus(tv, item, item.Raw().(*mastodon.Status), nil, controls, false, "")
drawStatus(tv, item, item.Raw().(*mastodon.Status), nil, controls, ft, false, "")
case api.StatusHistoryType:
s := item.Raw().(*mastodon.StatusHistory)
status := mastodon.Status{
@ -128,7 +131,7 @@ func DrawItemControls(tv *TutView, item api.Item, controls *tview.Flex, ft confi
MediaAttachments: s.MediaAttachments,
Visibility: mastodon.VisibilityPublic,
}
drawStatus(tv, item, &status, nil, controls, true, "")
drawStatus(tv, item, &status, nil, controls, ft, true, "")
case api.UserType, api.ProfileType:
if ft == config.FollowRequests {
drawUser(tv, item.Raw().(*api.User), nil, controls, "", InputUserFollowRequest)

12
ui/item_notification.go

@ -16,27 +16,27 @@ func drawNotification(tv *TutView, item api.Item, notification *api.Notification
fmt.Sprintf("%s started following you", util.FormatUsername(notification.Item.Account)), InputUserNormal,
)
case "favourite":
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false,
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false,
fmt.Sprintf("%s favorited your toot", util.FormatUsername(notification.Item.Account)),
)
case "reblog":
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false,
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false,
fmt.Sprintf("%s boosted your toot", util.FormatUsername(notification.Item.Account)),
)
case "mention":
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false,
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false,
fmt.Sprintf("%s mentioned you", util.FormatUsername(notification.Item.Account)),
)
case "update":
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false,
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false,
fmt.Sprintf("%s updated their toot", util.FormatUsername(notification.Item.Account)),
)
case "status":
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false,
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false,
fmt.Sprintf("%s posted a new toot", util.FormatUsername(notification.Item.Account)),
)
case "poll":
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, false,
drawStatus(tv, notification.Status, notification.Item.Status, main, controls, config.Notifications, false,
"A poll of yours or one you participated in has ended",
)
case "follow_request":

21
ui/item_status.go

@ -22,8 +22,9 @@ type Toot struct {
AccountDisplayName string
Account string
Spoiler bool
SpoilerText string
CWText string
ShowSpoiler bool
CWlabel string
ContentText string
Width int
HasExtra bool
@ -71,15 +72,18 @@ type DisplayTootData struct {
Style config.Style
}
func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview.TextView, controls *tview.Flex, isHistory bool, additional string) {
filtered, phrase := item.Filtered()
func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview.TextView, controls *tview.Flex, ft config.FeedType, isHistory bool, additional string) {
controls.Clear()
filtered, _, phrase, _ := item.Filtered(ft)
if filtered {
var output string
if tv.tut.Config.General.ShowFilterPhrase {
output = fmt.Sprintf("Filtered by phrase: %s", tview.Escape(phrase))
output = fmt.Sprintf("Filtered by phrase: %s\n\n", tview.Escape(phrase))
} else {
output = "Filtered."
output = "Filtered.\n\n"
}
ctrl := NewControl(tv.tut.Config, tv.tut.Config.Input.StatusShowFiltered, true)
output += ctrl.Label
if main != nil {
if additional != "" {
additional = fmt.Sprintf("%s\n\n", config.SublteText(tv.tut.Config, additional))
@ -89,7 +93,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
return
}
showSensitive := item.ShowSpoiler()
showSensitive := item.ShowCW()
var strippedContent string
var strippedSpoiler string
@ -106,6 +110,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
if main != nil {
_, _, width, _ = main.GetInnerRect()
}
cwToggle := NewControl(tv.tut.Config, tv.tut.Config.Input.StatusToggleCW, true)
toot := Toot{
Width: width,
ContentText: strippedContent,
@ -113,6 +118,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
BoostedDisplayName: tview.Escape(so.Account.DisplayName),
BoostedAcct: tview.Escape(so.Account.Acct),
ShowSpoiler: showSensitive,
CWlabel: cwToggle.Label,
}
toot.AccountDisplayName = tview.Escape(status.Account.DisplayName)
@ -156,7 +162,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
strippedSpoiler = tview.Escape(strippedSpoiler)
}
toot.SpoilerText = strippedSpoiler
toot.CWText = strippedSpoiler
media := []Media{}
for _, att := range status.MediaAttachments {
@ -230,7 +236,6 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
info = append(info, NewControl(tv.tut.Config, tv.tut.Config.Input.StatusYank, true))
}
controls.Clear()
for i, item := range info {
if i < len(info)-1 {
controls.AddItem(NewControlButton(tv, item), item.Len+1, 0, false)

8
ui/styled_elements.go

@ -34,7 +34,15 @@ func NewControlView(cnf *config.Config) *tview.Flex {
func NewControlButton(tv *TutView, control Control) *tview.Button {
btn := tview.NewButton(control.Label)
style := tcell.Style{}
style = style.Foreground(tv.tut.Config.Style.Text)
style = style.Background(tv.tut.Config.Style.Background)
btn.SetActivatedStyle(style)
btn.SetStyle(style)
btn.SetBackgroundColor(tv.tut.Config.Style.Background)
btn.SetBackgroundColorActivated(tv.tut.Config.Style.Background)
btn.SetLabelColor(tv.tut.Config.Style.Background)
btn.SetLabelColorActivated(tv.tut.Config.Style.Background)
btn.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) {
if !btn.InRect(event.Position()) {
return action, event

4
ui/timeline.go

@ -33,6 +33,8 @@ func NewTimeline(tv *TutView, update chan bool) *Timeline {
switch f.FeedType {
case config.TimelineHome:
nf = NewHomeFeed(tv, f.ShowBoosts, f.ShowReplies)
case config.TimelineHomeSpecial:
nf = NewHomeSpecialFeed(tv, f.ShowBoosts, f.ShowReplies)
case config.Conversations:
nf = NewConversationsFeed(tv)
case config.TimelineLocal:
@ -162,6 +164,8 @@ func (tl *Timeline) GetTitle() string {
ct = "federated"
case config.TimelineHome:
ct = "home"
case config.TimelineHomeSpecial:
ct = "special"
case config.TimelineLocal:
ct = "local"
case config.Saved:

Loading…
Cancel
Save