diff --git a/config.example.ini b/config.example.ini index 8f996d8..fb75160 100644 --- a/config.example.ini +++ b/config.example.ini @@ -96,6 +96,39 @@ show-help=true # default=true redraw-ui=true +# The leader is used as a shortcut to run commands as you can do in Vim. By +# default this is disabled and you enable it by setting a leader-key. It can +# only consist of one char and I like to use comma as leader key. So to set it +# you write leader-key=, +# default= +leader-key= + +# Number of milliseconds before the leader command resets. So if you tap the +# leader-key by mistake or are to slow it empties all the input after X +# milliseconds. +# default=1000 +leader-timeout=1000 + +# You set actions for the leader-key with one or more leader-action. It consists +# of two parts first the action then the shortcut. And they're seperated by a +# comma. +# +# Available commands: home, direct, local, federated, compose, blocking, +# bookmarks, saved, favorited, boosts, favorites, following, followers, muting, +# profile, notifications, lists +# +# 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 +# "fav", the one with "f" will always run and "fav" will never run. +# +# Some examples: +# leader-action=local,lo +# leader-action=lists,li +# leader-action=federated,fed +# leader-action=direct,d +# + + [media] # Your image viewer. # default=xdg-open @@ -349,7 +382,6 @@ list-selected-text=xrdb:background # look for "var KeyNames = map[Key]string{" # # https://github.com/gdamore/tcell/blob/master/key.go -# # Keys for moving down # default="",'j','J',"Down" diff --git a/config/config.go b/config/config.go index 60d2c6e..35e25fe 100644 --- a/config/config.go +++ b/config/config.go @@ -41,6 +41,34 @@ type Config struct { Input Input } +type LeaderAction struct { + Command LeaderCommand + Shortcut string +} + +type LeaderCommand uint + +const ( + LeaderNone LeaderCommand = iota + LeaderHome + LeaderDirect + LeaderLocal + LeaderFederated + LeaderCompose + LeaderBlocking + LeaderBookmarks + LeaderSaved + LeaderFavorited + LeaderBoosts + LeaderFavorites + LeaderFollowing + LeaderFollowers + LeaderMuting + LeaderProfile + LeaderNotifications + LeaderLists +) + type General struct { Confirmation bool DateTodayFormat string @@ -60,6 +88,9 @@ type General struct { ShowIcons bool ShowHelp bool RedrawUI bool + LeaderKey rune + LeaderTimeout int64 + LeaderActions []LeaderAction } type Style struct { @@ -545,6 +576,75 @@ func parseGeneral(cfg *ini.File) General { general.ListProportion = listProp general.ContentProportion = contentProp + leaderString := cfg.Section("general").Key("leader-key").MustString("") + leaderRunes := []rune(leaderString) + if len(leaderRunes) > 1 { + leaderRunes = []rune(strings.TrimSpace(leaderString)) + } + if len(leaderRunes) > 1 { + fmt.Println("error parsing leader-key. Error: leader-key can only be one char long") + os.Exit(1) + } + if len(leaderRunes) == 1 { + general.LeaderKey = leaderRunes[0] + } + if general.LeaderKey != rune(0) { + general.LeaderTimeout = cfg.Section("general").Key("leader-timeout").MustInt64(1000) + lactions := cfg.Section("general").Key("leader-action").ValueWithShadows() + var las []LeaderAction + for _, l := range lactions { + 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, ",")) + } + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + la := LeaderAction{} + switch parts[0] { + case "home": + la.Command = LeaderHome + case "direct": + la.Command = LeaderDirect + case "local": + la.Command = LeaderLocal + case "federated": + la.Command = LeaderFederated + case "compose": + la.Command = LeaderCompose + case "blocking": + la.Command = LeaderBlocking + case "bookmarks": + la.Command = LeaderBookmarks + case "saved": + la.Command = LeaderSaved + case "favorited": + la.Command = LeaderFavorited + case "boosts": + la.Command = LeaderBoosts + case "favorites": + la.Command = LeaderFavorites + case "following": + la.Command = LeaderFollowing + case "followers": + la.Command = LeaderFollowers + case "muting": + la.Command = LeaderMuting + case "profile": + la.Command = LeaderProfile + case "notifications": + la.Command = LeaderNotifications + case "lists": + la.Command = LeaderLists + default: + fmt.Printf("leader-action %s is invalid\n", parts[0]) + os.Exit(1) + } + la.Shortcut = parts[1] + las = append(las, la) + } + general.LeaderActions = las + } return general } @@ -893,6 +993,7 @@ func parseInput(cfg *ini.File) Input { func parseConfig(filepath string) (Config, error) { cfg, err := ini.LoadSources(ini.LoadOptions{ SpaceBeforeInlineComment: true, + AllowShadows: true, }, filepath) conf := Config{} if err != nil { diff --git a/config/default_config.go b/config/default_config.go index e6f847a..d9742c7 100644 --- a/config/default_config.go +++ b/config/default_config.go @@ -98,6 +98,39 @@ show-help=true # default=true redraw-ui=true +# The leader is used as a shortcut to run commands as you can do in Vim. By +# default this is disabled and you enable it by setting a leader-key. It can +# only consist of one char and I like to use comma as leader key. So to set it +# you write leader-key=, +# default= +leader-key= + +# Number of milliseconds before the leader command resets. So if you tap the +# leader-key by mistake or are to slow it empties all the input after X +# milliseconds. +# default=1000 +leader-timeout=1000 + +# You set actions for the leader-key with one or more leader-action. It consists +# of two parts first the action then the shortcut. And they're seperated by a +# comma. +# +# Available commands: home, direct, local, federated, compose, blocking, +# bookmarks, saved, favorited, boosts, favorites, following, followers, muting, +# profile, notifications, lists +# +# 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 +# "fav", the one with "f" will always run and "fav" will never run. +# +# Some examples: +# leader-action=local,lo +# leader-action=lists,li +# leader-action=federated,fed +# leader-action=direct,d +# + + [media] # Your image viewer. # default=xdg-open @@ -351,7 +384,6 @@ list-selected-text=xrdb:background # look for "var KeyNames = map[Key]string{" # # https://github.com/gdamore/tcell/blob/master/key.go -# # Keys for moving down # default="",'j','J',"Down" diff --git a/main.go b/main.go index dafb997..e9c43a1 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( "github.com/rivo/tview" ) -const version = "1.0.4" +const version = "1.0.5" func main() { util.MakeDirs() diff --git a/ui/cmdbar.go b/ui/cmdbar.go index 20bfaa9..7ce58be 100644 --- a/ui/cmdbar.go +++ b/ui/cmdbar.go @@ -1,12 +1,8 @@ package ui import ( - "fmt" "strings" - "github.com/RasmusLindroth/go-mastodon" - "github.com/RasmusLindroth/tut/api" - "github.com/RasmusLindroth/tut/util" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -62,7 +58,6 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { } input := c.GetInput() parts := strings.Split(input, " ") - item, itemErr := c.tutView.GetCurrentItem() if len(parts) == 0 { return } @@ -72,96 +67,35 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { case ":quit": c.tutView.tut.App.Stop() case ":compose": - c.tutView.InitPost(nil) + c.tutView.ComposeCommand() c.ClearInput() c.View.Autocomplete() case ":blocking": - c.tutView.Timeline.AddFeed( - NewBlocking(c.tutView), - ) + c.tutView.BlockingCommand() c.Back() case ":bookmarks", ":saved": - c.tutView.Timeline.AddFeed( - NewBookmarksFeed(c.tutView), - ) + c.tutView.BookmarksCommand() c.Back() case ":favorited": - c.tutView.Timeline.AddFeed( - NewFavoritedFeed(c.tutView), - ) + c.tutView.FavoritedCommand() c.Back() case ":boosts": - if itemErr != nil { - c.Back() - return - } - if item.Type() != api.StatusType { - c.Back() - return - } - s := item.Raw().(*mastodon.Status) - s = util.StatusOrReblog(s) - c.tutView.Timeline.AddFeed( - NewBoosts(c.tutView, s.ID), - ) + c.tutView.BoostsCommand() c.Back() case ":favorites": - if itemErr != nil { - c.Back() - return - } - if item.Type() != api.StatusType { - c.Back() - return - } - s := item.Raw().(*mastodon.Status) - s = util.StatusOrReblog(s) - c.tutView.Timeline.AddFeed( - NewFavoritesStatus(c.tutView, s.ID), - ) + c.tutView.FavoritesCommand() c.Back() case ":following": - if itemErr != nil { - c.Back() - return - } - if item.Type() != api.UserType && item.Type() != api.ProfileType { - c.Back() - return - } - s := item.Raw().(*api.User) - c.tutView.Timeline.AddFeed( - NewFollowing(c.tutView, s.Data.ID), - ) + c.tutView.FollowingCommand() c.Back() case ":followers": - if itemErr != nil { - c.Back() - return - } - if item.Type() != api.UserType && item.Type() != api.ProfileType { - c.Back() - return - } - s := item.Raw().(*api.User) - c.tutView.Timeline.AddFeed( - NewFollowers(c.tutView, s.Data.ID), - ) + c.tutView.FollowersCommand() c.Back() case ":muting": - c.tutView.Timeline.AddFeed( - NewMuting(c.tutView), - ) + c.tutView.MutingCommand() c.Back() case ":profile": - item, err := c.tutView.tut.Client.GetUserByID(c.tutView.tut.Client.Me.ID) - if err != nil { - c.ShowError(fmt.Sprintf("Couldn't load user. Error: %v\n", err)) - c.Back() - } - c.tutView.Timeline.AddFeed( - NewUserFeed(c.tutView, item), - ) + c.tutView.ProfileCommand() c.Back() case ":timeline", ":tl": if len(parts) < 2 { @@ -169,34 +103,22 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { } switch parts[1] { case "local", "l": - c.tutView.Timeline.AddFeed( - NewLocalFeed(c.tutView), - ) + c.tutView.LocalCommand() c.Back() case "federated", "f": - c.tutView.Timeline.AddFeed( - NewFederatedFeed(c.tutView), - ) + c.tutView.FederatedCommand() c.Back() case "direct", "d": - c.tutView.Timeline.AddFeed( - NewConversationsFeed(c.tutView), - ) + c.tutView.DirectCommand() c.Back() case "home", "h": - c.tutView.Timeline.AddFeed( - NewHomeFeed(c.tutView), - ) + c.tutView.HomeCommand() c.Back() case "notifications", "n": - c.tutView.Timeline.AddFeed( - NewNotificationFeed(c.tutView), - ) + c.tutView.NotificationsCommand() c.Back() case "favorited", "fav": - c.tutView.Timeline.AddFeed( - NewFavoritedFeed(c.tutView), - ) + c.tutView.FavoritedCommand() c.Back() } c.ClearInput() @@ -225,9 +147,7 @@ func (c *CmdBar) DoneFunc(key tcell.Key) { ) c.Back() case ":lists": - c.tutView.Timeline.AddFeed( - NewListsFeed(c.tutView), - ) + c.tutView.ListsCommand() c.Back() case ":help", ":h": c.tutView.PageFocus = c.tutView.PrevPageFocus diff --git a/ui/commands.go b/ui/commands.go new file mode 100644 index 0000000..12fe1f3 --- /dev/null +++ b/ui/commands.go @@ -0,0 +1,141 @@ +package ui + +import ( + "fmt" + + "github.com/RasmusLindroth/go-mastodon" + "github.com/RasmusLindroth/tut/api" + "github.com/RasmusLindroth/tut/util" +) + +func (tv *TutView) ComposeCommand() { + tv.InitPost(nil) +} + +func (tv *TutView) BlockingCommand() { + tv.Timeline.AddFeed( + NewBlocking(tv), + ) +} + +func (tv *TutView) BookmarksCommand() { + tv.Timeline.AddFeed( + NewBookmarksFeed(tv), + ) +} +func (tv *TutView) FavoritedCommand() { + tv.Timeline.AddFeed( + NewFavoritedFeed(tv), + ) +} + +func (tv *TutView) MutingCommand() { + tv.Timeline.AddFeed( + NewMuting(tv), + ) +} + +func (tv *TutView) LocalCommand() { + tv.Timeline.AddFeed( + NewLocalFeed(tv), + ) +} + +func (tv *TutView) FederatedCommand() { + tv.Timeline.AddFeed( + NewFederatedFeed(tv), + ) +} + +func (tv *TutView) DirectCommand() { + tv.Timeline.AddFeed( + NewConversationsFeed(tv), + ) +} + +func (tv *TutView) HomeCommand() { + tv.Timeline.AddFeed( + NewHomeFeed(tv), + ) +} + +func (tv *TutView) NotificationsCommand() { + tv.Timeline.AddFeed( + NewNotificationFeed(tv), + ) +} + +func (tv *TutView) ListsCommand() { + tv.Timeline.AddFeed( + NewListsFeed(tv), + ) +} + +func (tv *TutView) BoostsCommand() { + item, itemErr := tv.GetCurrentItem() + if itemErr != nil { + return + } + if item.Type() != api.StatusType { + return + } + s := item.Raw().(*mastodon.Status) + s = util.StatusOrReblog(s) + tv.Timeline.AddFeed( + NewBoosts(tv, s.ID), + ) +} + +func (tv *TutView) FavoritesCommand() { + item, itemErr := tv.GetCurrentItem() + if itemErr != nil { + return + } + if item.Type() != api.StatusType { + return + } + s := item.Raw().(*mastodon.Status) + s = util.StatusOrReblog(s) + tv.Timeline.AddFeed( + NewFavoritesStatus(tv, s.ID), + ) +} + +func (tv *TutView) FollowingCommand() { + item, itemErr := tv.GetCurrentItem() + if itemErr != nil { + return + } + if item.Type() != api.UserType && item.Type() != api.ProfileType { + return + } + s := item.Raw().(*api.User) + tv.Timeline.AddFeed( + NewFollowing(tv, s.Data.ID), + ) +} + +func (tv *TutView) FollowersCommand() { + item, itemErr := tv.GetCurrentItem() + if itemErr != nil { + return + } + if item.Type() != api.UserType && item.Type() != api.ProfileType { + return + } + s := item.Raw().(*api.User) + tv.Timeline.AddFeed( + NewFollowers(tv, s.Data.ID), + ) +} + +func (tv *TutView) ProfileCommand() { + item, err := tv.tut.Client.GetUserByID(tv.tut.Client.Me.ID) + if err != nil { + tv.ShowError(fmt.Sprintf("Couldn't load user. Error: %v\n", err)) + return + } + tv.Timeline.AddFeed( + NewUserFeed(tv, item), + ) +} diff --git a/ui/input.go b/ui/input.go index 0c56563..5a859f9 100644 --- a/ui/input.go +++ b/ui/input.go @@ -6,6 +6,7 @@ import ( "github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/tut/api" + "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" "github.com/gdamore/tcell/v2" ) @@ -19,6 +20,12 @@ func (tv *TutView) Input(event *tcell.EventKey) *tcell.EventKey { tv.SetPage(HelpFocus) } } + if tv.PageFocus != LoginFocus && tv.PageFocus != CmdFocus { + event = tv.InputLeaderKey(event) + if event == nil { + return nil + } + } switch tv.PageFocus { case LoginFocus: return tv.InputLoginView(event) @@ -61,6 +68,68 @@ func (tv *TutView) InputLoginView(event *tcell.EventKey) *tcell.EventKey { return event } +func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey { + if tv.tut.Config.General.LeaderKey == rune(0) { + return event + } + if event.Rune() == tv.tut.Config.General.LeaderKey { + tv.Leader.Reset() + return nil + } else if tv.Leader.IsActive() { + if event.Rune() != rune(0) { + tv.Leader.AddRune(event.Rune()) + } + action := config.LeaderNone + content := tv.Leader.Content() + for _, la := range tv.tut.Config.General.LeaderActions { + if la.Shortcut == content { + action = la.Command + break + } + } + if action == config.LeaderNone { + return nil + } + switch action { + case config.LeaderHome: + tv.HomeCommand() + case config.LeaderDirect: + tv.DirectCommand() + case config.LeaderLocal: + tv.LocalCommand() + case config.LeaderFederated: + tv.FederatedCommand() + case config.LeaderCompose: + tv.ComposeCommand() + case config.LeaderBlocking: + tv.BlockingCommand() + case config.LeaderBookmarks, config.LeaderSaved: + tv.BookmarksCommand() + case config.LeaderFavorited: + tv.FavoritedCommand() + case config.LeaderBoosts: + tv.BoostsCommand() + case config.LeaderFavorites: + tv.FavoritesCommand() + case config.LeaderFollowing: + tv.FollowingCommand() + case config.LeaderFollowers: + tv.FollowersCommand() + case config.LeaderMuting: + tv.MutingCommand() + case config.LeaderProfile: + tv.ProfileCommand() + case config.LeaderNotifications: + tv.NotificationsCommand() + case config.LeaderLists: + tv.ListsCommand() + } + tv.Leader.ResetInactive() + return nil + } + return event +} + func (tv *TutView) InputMainView(event *tcell.EventKey) *tcell.EventKey { switch tv.SubFocus { case ListFocus: diff --git a/ui/tutview.go b/ui/tutview.go index dacce65..73a2fe6 100644 --- a/ui/tutview.go +++ b/ui/tutview.go @@ -6,6 +6,7 @@ import ( "log" "os" "strings" + "time" "github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/tut/api" @@ -41,6 +42,7 @@ type TutView struct { PrevPageFocus PageFocusAt TimelineFocus TimelineFocusAt SubFocus SubFocusAt + Leader *Leader Shared *Shared View *tview.Pages @@ -55,12 +57,48 @@ type TutView struct { FileList []string } +func NewLeader(tv *TutView) *Leader { + return &Leader{ + tv: tv, + } +} + +type Leader struct { + tv *TutView + timeStart time.Time + content string +} + +func (l *Leader) IsActive() bool { + td := time.Duration(l.tv.tut.Config.General.LeaderTimeout) + return time.Since(l.timeStart) < td*time.Millisecond +} + +func (l *Leader) Reset() { + l.timeStart = time.Now() + l.content = "" +} + +func (l *Leader) ResetInactive() { + l.timeStart = time.Now().Add(-1 * time.Hour) + l.content = "" +} + +func (l *Leader) AddRune(r rune) { + l.content += string(r) +} + +func (l *Leader) Content() string { + return l.content +} + func NewTutView(t *Tut, accs *auth.AccountData, selectedUser string) *TutView { tv := &TutView{ tut: t, View: tview.NewPages(), FileList: []string{}, } + tv.Leader = NewLeader(tv) tv.Shared = NewShared(tv) if selectedUser != "" { useHost := false