Browse Source

2.0.0 (#253)

* bump version

* rename window to pane

* Add internal editor

* show chars left

* don't assume xdg-open, use open on iOS and start on Windows

* remove leaders and fix "switch"

* support multiple accounts

* update title on named feeds

* terminal-title=3, no top bar and no terminal title

* remove ini package

* add commands-in-new-pane and dynamic-timeline-name

* add delete-temp-files

* add lang to toots

* update docs

* update modules

* add clear-temp

* upgrade modules

* add gruvbox light
pull/256/head 2.0.0
Rasmus Lindroth 3 years ago committed by GitHub
parent
commit
fc200ddb95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      README.md
  2. 826
      config.example.ini
  3. 1268
      config.example.toml
  4. 1730
      config/config.go
  5. 1380
      config/default_config.go
  6. 34
      config/help.tmpl
  7. 8
      config/keys.go
  8. 4
      config/load.go
  9. 28
      config/themes/default.ini
  10. 28
      config/themes/default.toml
  11. 27
      config/themes/gruvbox-light.toml
  12. 14
      config/themes/nord.ini
  13. 14
      config/themes/nord.toml
  14. 22
      config/themes/papercolor-light.ini
  15. 22
      config/themes/papercolor-light.toml
  16. 27
      config/themes/snow-white.toml
  17. 258
      config/toml.go
  18. 446
      config/toml_default.go
  19. 1
      config/toot.tmpl
  20. 14
      docs/man/tut.1
  21. 11
      docs/man/tut.1.md
  22. 1926
      docs/man/tut.5
  23. 1416
      docs/man/tut.5.md
  24. 37
      docs/man/tut.7
  25. 36
      docs/man/tut.7.md
  26. 80
      feed/feed.go
  27. 17
      go.mod
  28. 55
      go.sum
  29. 51
      main.go
  30. 13
      ui/cliview.go
  31. 56
      ui/cmdbar.go
  32. 271
      ui/commands.go
  33. 216
      ui/composeview.go
  34. 94
      ui/editorview.go
  35. 365
      ui/feed.go
  36. 206
      ui/input.go
  37. 13
      ui/item_status.go
  38. 28
      ui/linkview.go
  39. 97
      ui/mainview.go
  40. 14
      ui/media.go
  41. 22
      ui/open.go
  42. 28
      ui/pollview.go
  43. 80
      ui/preferenceview.go
  44. 3
      ui/statusbar.go
  45. 38
      ui/styled_elements.go
  46. 160
      ui/timeline.go
  47. 4
      ui/top.go
  48. 96
      ui/tutview.go
  49. 9
      ui/view.go
  50. 39
      util/util.go
  51. 23
      util/xdg.go

27
README.md

@ -47,7 +47,8 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
* `:boosts` = Lists users that have boosted the toot
* `:bookmarks` = List all your bookmarks
* `:clear-notifications` = Remove all of your notifications
* `:close-window` = Closes the current window, including all the timelines in said window
* `:clear-temp` = Remove all of your media files that have been downloaded. Only needed if you have set delete-temp-files to false under [media] in your config.
* `:close-pane` = Closes the current pane, including all the timelines in said pane
* `:compose` = Compose a new toot
* `:edit` = Edit one of your toots
* `:favorited` = Lists toots you've favorited
@ -60,14 +61,17 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
* `:history` = Show edits of a toot
* `:lists` = Show a list of your lists
* `:list-placement top|right|bottom|left` = Place the list in choosen placement
* `:list-split row|column` = Split the timelines in window by row or column
* `:move-window left|right|up|down|home|end` = Moves the window in choosen direction
* `:mv l|r|u|d|h|e` = Shorter form of former command
* `:list-split row|column` = Split the timelines by row or column
* `:login` = Login to one more account
* `:move-pane left|right|up|down|home|end` = Moves the pane in choosen direction
* `:mp l|r|u|d|h|e` = Shorter form of former command
* `:muting` = Lists users that you've muted
* `:newer` = Force load newer toots in current timeline
* `:next-acct` = Go to the next account if you're logged in to multiple
* `:preferences` = Update your profile and some other settings
* `:prev-acct` = Go to the prev account if you're logged in to multiple
* `:profile` = Go to your profile
* `:proportions [int] [int]` = Sets the proportions of the windows and the content. The first integer is your windows and the other for content, e.g. :proportions 1 3
* `:proportions [int] [int]` = Sets the proportions of the panes and the content. The first integer is your panes and the other for content, e.g. :proportions 1 3
* `:refetch` = Refetches the current item that you're viewing. Can be used to update poll results.
* `:saved` = Alias for bookmarks
* `:stick-to-top` = Toggle the stick-to-top setting that always shows the latest toot in all timelines
@ -75,7 +79,7 @@ You can find Linux binaries under [releases](https://github.com/RasmusLindroth/t
* `:tags` = List of tags that you're following
* `:unfollow-tag <tag>` = Unfollow the hashtag named &lt;tag&gt;, e.g. :unfollow-tag tut
* `:user <username>` = Search for users named &lt;username&gt;, e.g. :user rasmus. To narrow a search include the instance like this :user rasmus@mastodon.acc.sunet.se
* `:window <int>` = Switch window by index (zero indexed) e.g. :window 0 for the left/top window
* `:pane <int>` = Switch pane by index (zero indexed) e.g. :pane 0 for the left/top pane
Keys without description in tut
* `c` = Compose a new toot
@ -99,12 +103,12 @@ Tut is configurable, so you can change things like the colors, the default timel
what image viewer to use and some more. Check out the configuration file to see
all the options.
You find it in `XDG_CONFIG_HOME/tut/config.ini` on Linux which usually equals to `~/.config/tut/config.ini`.
You find it in `XDG_CONFIG_HOME/tut/config.toml` on Linux which usually equals to `~/.config/tut/config.toml`.
If you don't run Linux it will use the path of [os#UserConfigDir](https://github.com/golang/go/blob/7dc9fcb13de7bb20b11f6a526865545cc9142c2c/src/os/file.go#L455-L461).
But if you move the tut folder to `XDG_CONFIG_HOME/tut/` and have set the environment variable `XDG_CONFIG_HOME`
it will look there instead of the standard place.
You can find an updated configuration file in this repo named `config.example.ini`.
You can find an updated configuration file in this repo named `config.example.toml`.
If there are any new configurations options you can copy them frome that file.
If you prefer a website you can read about all the options on [tut.anv.nu](https://tut.anv.nu/config) and
if man pages are your thing use `tut(5)`.
@ -210,16 +214,17 @@ you will have to add `go/bin` to your `$PATH`.
## Flags and commands
```
Commands:
example-config - creates the default configuration file in the current directory and names it ./config.example.ini
example-config - creates the default configuration file in the current directory and names it ./config.example.toml
Flags:
-h --help prints this message
-v --version prints the version
-n --new-user add one more user to tut
-c --config <path> load config.ini from <path>
-c --config <path> load config.toml from <path>
-d --config-dir <path> load all config from <path>
-u --user <name> login directly to user named <name>
If two users are named the same. Use full name like tut@fosstodon.org
If you want to login to multiple accounts seperate them with a space and use quotation marks. E.g. -u "acc_one acc_two"
If two users are named the same. Use full name like tut@fosstodon.org
```
If you don't want to set `--config` or `--config-dir` everytime you can set

826
config.example.ini

@ -1,826 +0,0 @@
# Configuration file for tut
[general]
# Shows a confirmation view before actions such as favorite, delete toot, boost
# etc.
# default=true
confirmation=true
# Enable support for using the mouse in tut to select items.
# default=false
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, special, bookmarks,
# saved, favorited, notifications, lists, mentions, tag
#
# The one named special are the home timeline with only boosts and/or replies.
#
# Tag is special as you need to add the tag after, see the example below.
#
# The syntax is:
# timelines=feed,[name],[keys...],[showBoosts],[showReplies]
#
# Tha values in brackets are optional. You can see the syntax for keys under the
# [input] section.
#
# showBoosts and showReplies must be formated as bools. So either true or false.
# They always defaults to true.
#
# 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. It will
# also hide boosts in the timeline, but show toots that are replies.
# timelines=local,Local,'2',false,true
#
# 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
# Format for dates the same day. See date-format for more info.
# default=15:04
date-today-format=15:04
# This displays relative dates instead for statuses that are one day or older
# the output is 1y2m1d (1 year 2 months and 1 day)
#
# The value is an integear
# -1 = don't use relative dates
# 0 = always use relative dates, except for dates < 1 day
# 1 - ∞ = number of days to use relative dates
#
# Example: date-relative=28 will display a relative date for toots that are
# between 1-28 days old. Otherwhise it will use the short or long format.
# default=-1
date-relative=-1
# The max width of text before it wraps when displaying toots.
# 0 = no restriction.
# default=0
max-width=0
# Where do you want the list of toots to be placed?
# Valid values: left, right, top, bottom.
# default=left
list-placement=left
# If you have notification-feed set to true you can display it under the main
# list of toots (row) or place it to the right of the main list of toots
# (column).
# default=row
list-split=row
# 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.
# Must be n > 0
# default=1
list-proportion=1
# See list-proportion
# default=2
content-proportion=2
# Hide notifications of this type. If you have multiple you separate them with a
# comma. Valid types: mention, status, boost, follow, follow_request, favorite,
# poll, edit.
# default=
notifications-to-hide=
# If you always want to quote original message when replying.
# default=false
quote-reply=false
# If you want to show icons in the list of toots.
# default=true
show-icons=true
# If you've learnt all the shortcut keys you can remove the help text and only
# show the key in tui. So it gets less cluttered.
# default=false
short-hints=false
# If you want to display the filter that filtered a toot.
# default=true
show-filter-phrase=true
# If you want to show a message in the cmdbar on how to access the help text.
# default=true
show-help=true
# If you always want tut to jump to the newest post. May ruin your reading
# experience.
# 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.
# default=0
terminal-title=0
# If you don't want the whole UI to update, and only the text content you can
# set this option to true. This will lead to some artifacts being left on the
# screen when emojis are present. But it will keep the UI from flashing on every
# single toot in some terminals.
# 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 separated by a
# comma.
#
# Available commands: blocking, bookmarks, boosts, clear-notifications,
# close-window, compose, direct, edit, favorited, favorites, federated,
# followers, following, history, home, list-placement, list-split, lists, local,
# mentions, move-window-left, move-window-right, move-window-up,
# move-window-down, move-window-home, move-window-end, muting, newer,
# notifications, preferences, profile, proportions, refetch, saved, special-all,
# special-boosts, special-replies, stick-to-top, switch, tag, tags, window
#
# 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
# "fav", the one with "f" will always run and "fav" will never run.
#
# Some special leaders:
# tag is special as you need to add the tag after, e.g. tag linux
# window is special as it's a shortcut for switching between the timelines
# you've set under general and they are zero indexed. window 0 = your first
# timeline, window 1 = your second and so on.
# list-placement as it takes the argument top, right, bottom or left
# list-split as it takes the argument column or row
# proportions takes the arguments [int] [int], where the first integer is the
# list and the other content, e.g. proportions 1 3. See list-proportion above
# for more information.
# switch let's you go to a timeline if it already exists, if it doesn't it will
# open the timeline in a new window. The syntax is almost the same as in
# timelines= and is displayed under the examples.
#
# Some examples:
# leader-action=local,lo
# leader-action=lists,li
# leader-action=federated,fed
# leader-action=direct,d
# leader-action=history,h
# leader-action=tag linux,tl
# leader-action=window 0,h
# leader-action=list-placement bottom,b
# leader-action=list-split column,c
# leader-action=proportions 1 3,3
#
# Syntax for switch:
# leader-action=switch feed,shortcut,[name],[showBoosts],[showReplies]
# showBoosts can be either true or false and they are both optional. Here are
# some examples:
#
# leader-action=switch home,h,false,true
# leader-action=switch tag tut,tt
#
[media]
# Your image viewer.
# default=xdg-open
image-viewer=xdg-open
# Open the image viewer in the same terminal as toot. Only for terminal based
# viewers.
# default=false
image-terminal=false
# If images should open one by one e.g. "imv image.png" multiple times. If set
# to false all images will open at the same time like this "imv image1.png
# image2.png image3.png". Not all image viewers support this, so try it first.
# default=true
image-single=true
# If you want to open the images in reverse order. In some image viewers this
# will display the images in the "right" order.
# default=false
image-reverse=false
# Your video viewer.
# default=xdg-open
video-viewer=xdg-open
# Open the video viewer in the same terminal as toot. Only for terminal based
# viewers.
# default=false
video-terminal=false
# If videos should open one by one. See image-single.
# default=true
video-single=true
# If you want your videos in reverse order. In some video apps this will play
# the files in the "right" order.
# default=false
video-reverse=false
# Your audio viewer.
# default=xdg-open
audio-viewer=xdg-open
# Open the audio viewer in the same terminal as toot. Only for terminal based
# viewers.
# default=false
audio-terminal=false
# If audio should open one by one. See image-single.
# default=true
audio-single=true
# If you want to play the audio files in reverse order. In some audio apps this
# will play the files in the "right" order.
# default=false
audio-reverse=false
# Your web browser.
# default=xdg-open
link-viewer=xdg-open
# Open the browser in the same terminal as toot. Only for terminal based
# browsers.
# default=false
link-terminal=false
[open-custom]
# This sections allows you to set up to five custom programs to open URLs with.
# If the url points to an image, you can set c1-name to img and c1-use to imv.
# If the program runs in a terminal and you want to run it in the same terminal
# as tut. Set cX-terminal to true. The name will show up in the UI, so keep it
# short so all five fits.
#
# c1-name=name
# c1-use=program
# c1-terminal=false
#
# c2-name=name
# c2-use=program
# c2-terminal=false
#
# c3-name=name
# c3-use=program
# c3-terminal=false
#
# c4-name=name
# c4-use=program
# c4-terminal=false
#
# c5-name=name
# c5-use=program
# c5-terminal=false
[open-pattern]
# Here you can set your own glob patterns for opening matching URLs in the
# program you want them to open up in. You could for example open Youtube videos
# in your video player instead of your default browser.
#
# You must name the keys foo-pattern, foo-use and foo-terminal, where use is the
# program that will open up the URL. To see the syntax for glob pattern you can
# follow this URL https://github.com/gobwas/glob#syntax. foo-terminal is if the
# program runs in the terminal and should open in the same terminal as tut
# itself.
#
# Example for youtube.com and youtu.be to open up in mpv instead of the browser.
#
# y1-pattern=*youtube.com/watch*
# y1-use=mpv
# y1-terminal=false
#
# y2-pattern=*youtu.be/*
# y2-use=mpv
# y2-terminal=false
[desktop-notification]
# Notification when someone follows you.
# default=false
followers=false
# Notification when someone favorites one of your toots.
# default=false
favorite=false
# Notification when someone mentions you.
# default=false
mention=false
# Notification when someone edits their toot.
# default=false
update=false
# Notification when someone boosts one of your toots.
# default=false
boost=false
# Notification of poll results.
# default=false
poll=false
# Notification when there is new posts in current timeline.
# default=false
posts=false
[style]
# All styles can be represented in their HEX value like #ffffff or with their
# name, so in this case white. The only special value is "default" which equals
# to transparent, so it will be the same color as your terminal.
#
# You can also use xrdb colors like this xrdb:color1 The program will use colors
# prefixed with an * first then look for URxvt or XTerm if it can't find any
# color prefixed with an asterisk. If you don't want tut to guess the prefix you
# can set the prefix yourself. If the xrdb color can't be found a preset color
# will be used. You'll have to set theme=none for this to work.
# The xrdb prefix used for colors in .Xresources.
# default=guess
xrdb-prefix=guess
# You can use some themes that comes bundled with tut. Check out the themes
# available on the URL below. If a theme is named "nord.ini" you just write
# theme=nord
#
# https://github.com/RasmusLindroth/tut/tree/master/config/themes
#
# You can also create a theme file in your config directory e.g.
# ~/.config/tut/themes/foo.ini and then set theme=foo.
#
# If you want to use your own theme but don't want to create a new file, set
# theme=none and then you can create your own theme below.
# default=default
theme=default
# The background color used on most elements.
# default=
background=
# The text color used on most of the text.
# default=
text=
# The color to display subtle elements or subtle text. Like lines and help text.
# default=
subtle=
# The color for errors or warnings
# default=
warning-text=
# This color is used to display username.
# default=
text-special-one=
# This color is used to display username and key hints.
# default=
text-special-two=
# The color of the bar at the top
# default=
top-bar-background=
# The color of the text in the bar at the top.
# default=
top-bar-text=
# The color of the bar at the bottom
# default=
status-bar-background=
# The color of the text in the bar at the bottom.
# default=
status-bar-text=
# The color of the bar at the bottom in view mode.
# default=
status-bar-view-background=
# The color of the text in the bar at the bottom in view mode.
# default=
status-bar-view-text=
# The color of the text in the command bar at the bottom.
# default=
command-text=
# Background of selected list items.
# default=
list-selected-background=
# The text color of selected list items.
# default=
list-selected-text=
# The background color of selected list items that are out of focus.
# default=
list-selected-inactive-background=
# The text color of selected list items that are out of focus.
# default=
list-selected-inactive-text=
# The main color of the text for key hints
# default=
controls-text=
# The highlight color of for key hints
# default=
controls-highlight=
# The background color in dropdowns and autocompletions
# default=
autocomplete-background=
# The text color in dropdowns at autocompletions
# default=
autocomplete-text=
# The background color for selected value in dropdowns and autocompletions
# default=
autocomplete-selected-background=
# The text color for selected value in dropdowns and autocompletions
# default=
autocomplete-selected-text=
# The background color on selected button and the text color of unselected
# buttons
# default=
button-color-one=
# The text color on selected button and the background color of unselected
# buttons
# default=
button-color-two=
# The background on named timelines.
# default=
timeline-name-background=
# The text color on named timelines
# default=
timeline-name-text=
[input]
# You can edit the keys for tut below.
#
# The syntax is a bit weird, but it works. And I'll try to explain it as well as
# I can.
#
# Example:
# status-favorite="[F]avorite","Un[F]avorite",'f','F'
# status-delete="[D]elete",'d','D'
#
# status-favorite and status-delete differs because favorite can be in two
# states, so you will have to add two key hints.
# Most keys will only have on key hint. Look at the default value for reference.
#
# Key hints must be in some of the following formats. Remember the quotation
# marks.
# "" = empty
# "[D]elete" = Delete with a highlighted D
# "Un[F]ollow" = UnFollow with a highlighted F
# "[Enter]" = Enter where everything is highlighted
# "Yan[K]" = YanK with a highlighted K
#
# After the hint (or hints) you must set the keys. You can do this in two ways,
# with single quotation marks or double ones.
#
# The single ones are for single chars like 'a', 'b', 'c' and double marks are
# for special keys like "Enter". Remember that they are case sensitive.
#
# To find the names of special keys you have to go to the following site and
# look for "var KeyNames = map[Key]string{"
#
# https://github.com/gdamore/tcell/blob/master/key.go
# Keys for moving down
# default="",'j','J',"Down"
global-down="",'j','J',"Down"
# Keys for moving up
# default="",'k','K',"Up"
global-up="",'k','K',"Up"
# To select items
# default="","Enter"
global-enter="","Enter"
# To go back
# default="[Esc]","Esc"
global-back="[Esc]","Esc"
# To go back and exit Tut
# default="[Q]uit",'q','Q'
global-exit="[Q]uit",'q','Q'
# Move to the top
# default="",'g',"Home"
main-home="",'g',"Home"
# Move to the bottom
# default="",'G',"End"
main-end="",'G',"End"
# Go to previous feed
# default="",'h','H',"Left"
main-prev-feed="",'h','H',"Left"
# Go to next feed
# 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'
# Compose a new toot
# default="",'c','C'
main-compose="",'c','C'
# Open avatar
# default="[A]vatar",'a','A'
status-avatar="[A]vatar",'a','A'
# Boost a toot
# default="[B]oost","Un[B]oost",'b','B'
status-boost="[B]oost","Un[B]oost",'b','B'
# Edit a toot
# default="[E]dit",'e','E'
status-edit="[E]dit",'e','E'
# Delete a toot
# default="[D]elete",'d','D'
status-delete="[D]elete",'d','D'
# Favorite a toot
# default="[F]avorite","Un[F]avorite",'f','F'
status-favorite="[F]avorite","Un[F]avorite",'f','F'
# Open toots media files
# default="[M]edia",'m','M'
status-media="[M]edia",'m','M'
# Open links
# default="[O]pen",'o','O'
status-links="[O]pen",'o','O'
# Open poll
# default="[P]oll",'p','P'
status-poll="[P]oll",'p','P'
# Reply to toot
# default="[R]eply",'r','R'
status-reply="[R]eply",'r','R'
# Save/bookmark a toot
# default="[S]ave","Un[S]ave",'s','S'
status-bookmark="[S]ave","Un[S]ave",'s','S'
# View thread
# default="[T]hread",'t','T'
status-thread="[T]hread",'t','T'
# Open user profile
# default="[U]ser",'u','U'
status-user="[U]ser",'u','U'
# Open the view mode
# default="[V]iew",'v','V'
status-view-focus="[V]iew",'v','V'
# Yank the url of the toot
# default="[Y]ank",'y','Y'
status-yank="[Y]ank",'y','Y'
# 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'
user-avatar="[A]vatar",'a','A'
# Block the user
# default="[B]lock","Un[B]lock",'b','B'
user-block="[B]lock","Un[B]lock",'b','B'
# Follow user
# 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'
# Open links
# default="[O]pen",'o','O'
user-links="[O]pen",'o','O'
# View user profile
# default="[U]ser",'u','U'
user-user="[U]ser",'u','U'
# Open view mode
# default="[V]iew",'v','V'
user-view-focus="[V]iew",'v','V'
# Yank the user URL
# default="[Y]ank",'y','Y'
user-yank="[Y]ank",'y','Y'
# Open list
# default="[O]pen",'o','O'
list-open-feed="[O]pen",'o','O'
# List all users in a list
# default="[U]sers",'u','U'
list-user-list="[U]sers",'u','U'
# Add user to list
# default="[A]dd",'a','A'
list-user-add="[A]dd",'a','A'
# Delete user from list
# default="[D]elete",'d','D'
list-user-delete="[D]elete",'d','D'
# Open URL
# default="[O]pen",'o','O'
link-open="[O]pen",'o','O'
# Yank the URL
# default="[Y]ank",'y','Y'
link-yank="[Y]ank",'y','Y'
# Open tag feed
# default="[O]pen",'o','O'
tag-open-feed="[O]pen",'o','O'
# Toggle follow on tag
# default="[F]ollow","Un[F]ollow",'f','F'
tag-follow="[F]ollow","Un[F]ollow",'f','F'
# Edit content warning text on new toot
# default="[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'
compose-edit-text="[E]dit text",'e','E'
# Include a quote when replying
# default="[I]nclude quote",'i','I'
compose-include-quote="[I]nclude quote",'i','I'
# Focus on adding media to toot
# default="[M]edia",'m','M'
compose-media-focus="[M]edia",'m','M'
# Post the new toot
# default="[P]ost",'p','P'
compose-post="[P]ost",'p','P'
# Toggle content warning on toot
# default="[T]oggle CW",'t','T'
compose-toggle-content-warning="[T]oggle CW",'t','T'
# Edit the visibility on new toot
# default="[V]isibility",'v','V'
compose-visibility="[V]isibility",'v','V'
# Edit the language of a toot
# default="[L]ang",'l','L'
compose-language="[L]ang",'l','L'
# Switch to creating a poll
# default="P[O]ll",'o','O'
compose-poll="P[O]ll",'o','O'
# Delete media file
# default="[D]elete",'d','D'
media-delete="[D]elete",'d','D'
# Edit the description on media file
# default="[E]dit desc",'e','E'
media-edit-desc="[E]dit desc",'e','E'
# Add a new media file
# default="[A]dd",'a','A'
media-add="[A]dd",'a','A'
# Vote on poll
# default="[V]ote",'v','V'
vote-vote="[V]ote",'v','V'
# Select item to vote on
# default="[Enter] to select",' ', "Enter"
vote-select="[Enter] to select",' ', "Enter"
# Add a new poll option
# default="[A]dd",'a','A'
poll-add="[A]dd",'a','A'
# Edit a poll option
# default="[E]dit",'e','E'
poll-edit="[E]dit",'e','E'
# Delete a poll option
# default="[D]elete",'d','D'
poll-delete="[D]elete",'d','D'
# Toggle voting on multiple options
# default="Toggle [M]ultiple",'m','M'
poll-multi-toggle="Toggle [M]ultiple",'m','M'
# Change the expiration of poll
# default="E[X]pires",'x','X'
poll-expiration="E[X]pires",'x','X'
# Change display name
# default="[N]ame",'n','N'
preference-name="[N]ame",'n','N'
# Change default visibility of toots
# default="[V]isibility",'v','V'
preference-visibility="[V]isibility",'v','V'
# Change bio in profile
# default="[B]io",'b','B'
preference-bio="[B]io",'b','B'
# Save your preferences
# default="[S]ave",'s','S'
preference-save="[S]ave",'s','S'
# Edit profile fields
# default="[F]ields",'f','F'
preference-fields="[F]ields",'f','F'
# Add new field
# default="[A]dd",'a','A'
preference-fields-add="[A]dd",'a','A'
# Edit current field
# default="[E]dit",'e','E'
preference-fields-edit="[E]dit",'e','E'
# Delete current field
# default="[D]elete",'d','D'
preference-fields-delete="[D]elete",'d','D'

1268
config.example.toml

File diff suppressed because it is too large Load Diff

1730
config/config.go

File diff suppressed because it is too large Load Diff

1380
config/default_config.go

File diff suppressed because it is too large Load Diff

34
config/help.tmpl

@ -50,8 +50,11 @@ Here's a list of supported commands.
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:clear-notifications{{ Flags "-" }}{{ Color .Style.Text }}
Remove all of your notifications
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:close-window{{ Flags "-" }}{{ Color .Style.Text }}
Closes the current window, including all the timelines in said window
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:clear-temp{{ Flags "-" }}{{ Color .Style.Text }}
Remove all of your media files that have been downloaded. Only needed if you have set delete-temp-files to false under [media[] in your config.
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:close-pane{{ Flags "-" }}{{ Color .Style.Text }}
Closes the current pane, including all the timelines in said pane
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:compose{{ Flags "-" }}{{ Color .Style.Text }}
Compose a new toot
@ -90,12 +93,15 @@ Here's a list of supported commands.
Place the list in choosen placement
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:list-split{{ Flags "-" }}{{ Color .Style.Text }} row|column
Split the timelines in window by row or column
Split the timelines by row or column
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:login{{ Flags "-" }}{{ Color .Style.Text }}
Login to one more account
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:move-window{{ Flags "-" }}{{ Color .Style.Text }} left|right|up|down|home|end
Moves the window in choosen direction
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:move-pane{{ Flags "-" }}{{ Color .Style.Text }} left|right|up|down|home|end
Moves the pane in choosen direction
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:mv{{ Flags "-" }}{{ Color .Style.Text }} l|r|u|d|h|e
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:mp{{ Flags "-" }}{{ Color .Style.Text }} l|r|u|d|h|e
Shorter form of former command
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:muting{{ Flags "-" }}{{ Color .Style.Text }}
@ -104,14 +110,20 @@ Here's a list of supported commands.
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:newer{{ Flags "-" }}{{ Color .Style.Text }}
Force load newer toots in current timeline
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:next-acct{{ Flags "-" }}{{ Color .Style.Text }}
Go to the next account if you're logged in to multiple
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:preferences{{ Flags "-" }}{{ Color .Style.Text }}
Update your profile and some other settings
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:prev-acct{{ Flags "-" }}{{ Color .Style.Text }}
Go to the prev account if you're logged in to multiple
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:profile{{ Flags "-" }}{{ Color .Style.Text }}
Go to your profile
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:proportions{{ Flags "-" }}{{ Color .Style.Text }} [int] [int]
Sets the proportions of the windows and the content. The first integer is your windows and the other for content, e.g. :proportions 1 3
Sets the proportions of the panes and the content. The first integer is your panes and the other for content, e.g. :proportions 1 3
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:refetch{{ Flags "-" }}{{ Color .Style.Text }}
Refetches the current item that you're viewing. Can be used to update poll results.
@ -134,15 +146,15 @@ Here's a list of supported commands.
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:user{{ Flags "-" }}{{ Color .Style.Text }} <username>
Search for users named <username>, e.g. :user rasmus. To narrow a search include the instance like this :user rasmus@mastodon.acc.sunet.se
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:window{{ Flags "-" }}{{ Color .Style.Text }} <int>
Switch window by index (zero indexed) e.g. :window 0 for the left/top window
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:pane{{ Flags "-" }}{{ Color .Style.Text }} <int>
Switch pane by index (zero indexed) e.g. :pane 0 for the left/top pane
{{ Color .Style.Text }}{{ Flags "b" }}Configuration{{ Flags "-" }}
tut searches for a config file in the following locations on Linux:
1. $XDG_CONFIG_HOME/tut/config.ini
2. $HOME/.config/tut/config.ini
1. $XDG_CONFIG_HOME/tut/config.toml
2. $HOME/.config/tut/config.toml
If you don't run Linux it will use the path of the Go function {{ Flags "b" }}os.UserConfigDir(){{ Flags "-" }}.
But if you move the tut folder to {{ Flags "b" }}XDG_CONFIG_HOME/tut/{{ Flags "-" }} and have set the environment variable {{ Flags "b" }}XDG_CONFIG_HOME{{ Flags "-" }}

8
config/keys.go

@ -32,10 +32,6 @@ func ColorKey(c *Config, pre, key, end string) string {
return text
}
func ColorMark(color tcell.Color) string {
return fmt.Sprintf("[#%06x]", color.Hex())
}
func TextFlags(s string) string {
return fmt.Sprintf("[::%s]", s)
}
@ -44,3 +40,7 @@ func SublteText(c *Config, text string) string {
subtle := ColorMark(c.Style.Subtle)
return fmt.Sprintf("%s%s", subtle, text)
}
func ColorMark(color tcell.Color) string {
return fmt.Sprintf("[#%06x]", color.Hex())
}

4
config/load.go

@ -11,9 +11,9 @@ func Load(cnfPath string, cnfDir string) *Config {
fmt.Printf("Couldn't create or access the configuration dir. Error: %v\n", err)
os.Exit(1)
}
path, exists, err := checkConfig("config.ini", cnfPath, cnfDir)
path, exists, err := checkConfig("config.toml", cnfPath, cnfDir)
if err != nil {
fmt.Printf("Couldn't access config.ini. Error: %v\n", err)
fmt.Printf("Couldn't access config.toml. Error: %v\n", err)
os.Exit(1)
}
if !exists {

28
config/themes/default.ini

@ -1,28 +0,0 @@
background=#272822
text=#f8f8f2
subtle=#808080
warning-text=#f92672
text-special-one=#ae81ff
text-special-two=#a6e22e
top-bar-background=#f92672
top-bar-text=#f8f8f2
status-bar-background=#f92672
status-bar-text=#f8f8f2
status-bar-view-background=#ae81ff
status-bar-view-text=#f8f8f2
command-text=#f8f8f2
list-selected-background=#f92672
list-selected-text=#f8f8f2
list-selected-inactive-background=#ae81ff
list-selected-inactive-text=#f8f8f2
controls-text=#f8f8f2
controls-highlight=#a6e22e
autocomplete-background=#272822
autocomplete-text=#f8f8f2
autocomplete-selected-background=#ae81ff
autocomplete-selected-text=#f8f8f2
button-color-one=#f92672
button-color-two=#272822
timeline-name-background=#272822
timeline-name-text=#808080

28
config/themes/default.toml

@ -0,0 +1,28 @@
background="#272822"
text="#f8f8f2"
subtle="#808080"
warning-text="#f92672"
text-special-one="#ae81ff"
text-special-two="#a6e22e"
top-bar-background="#f92672"
top-bar-text="#f8f8f2"
status-bar-background="#f92672"
status-bar-text="#f8f8f2"
status-bar-view-background="#ae81ff"
status-bar-view-text="#f8f8f2"
command-text="#f8f8f2"
list-selected-background="#f92672"
list-selected-text="#f8f8f2"
list-selected-inactive-background="#ae81ff"
list-selected-inactive-text="#f8f8f2"
controls-text="#f8f8f2"
controls-highlight="#a6e22e"
autocomplete-background="#272822"
autocomplete-text="#f8f8f2"
autocomplete-selected-background="#ae81ff"
autocomplete-selected-text="#f8f8f2"
button-color-one="#f92672"
button-color-two="#272822"
timeline-name-background="#272822"
timeline-name-text="#808080"

27
config/themes/gruvbox-light.toml

@ -0,0 +1,27 @@
background="#fbf1c7"
text="#654735"
subtle="#b47109"
warning-text="#c14a4a"
text-special-one="#45707a"
text-special-two="#945e80"
top-bar-background="#654735"
top-bar-text="#fbf1c7"
status-bar-background="#654735"
status-bar-text="#fbf1c7"
status-bar-view-background="#4c7a5d"
status-bar-view-text="#fbf1c7"
list-selected-background="#654735"
list-selected-text="#fbf1c7"
command-text="#654735"
list-selected-inactive-background="#945e80"
list-selected-inactive-text="#fbf1c7"
controls-text="#654735"
controls-highlight="#945e80"
autocomplete-background="#fbf1c7"
autocomplete-text="#654735"
autocomplete-selected-background="#4c7a5d"
autocomplete-selected-text="#fbf1c7"
button-color-one="#654735"
button-color-two="#fbf1c7"
timeline-name-background="#fbf1c7"
timeline-name-text="#654735"

14
config/themes/nord.ini

@ -1,14 +0,0 @@
background=#2E3440
text=#ECEFF4
subtle=#4C566A
warning-text=#BF616A
text-special-one=#8FBCBB
text-special-two=#81A1C1
top-bar-background=#5E81AC
top-bar-text=#ECEFF4
status-bar-background=#5E81AC
status-bar-text=#ECEFF4
status-bar-view-background=#8FBCBB
status-bar-view-text=#f8f8f2
list-selected-background=#5E81AC
list-selected-text=#ECEFF4

14
config/themes/nord.toml

@ -0,0 +1,14 @@
background="#2E3440"
text="#ECEFF4"
subtle="#4C566A"
warning-text="#BF616A"
text-special-one="#8FBCBB"
text-special-two="#81A1C1"
top-bar-background="#5E81AC"
top-bar-text="#ECEFF4"
status-bar-background="#5E81AC"
status-bar-text="#ECEFF4"
status-bar-view-background="#8FBCBB"
status-bar-view-text="#f8f8f2"
list-selected-background="#5E81AC"
list-selected-text="#ECEFF4"

22
config/themes/papercolor-light.ini

@ -1,22 +0,0 @@
; This is an adaption of NLKNguyen (https://github.com/NLKNguyen)'s
; PaperColor Light theme for tut (https://github.com/RasmusLindroth/tut).
;
; @author: stoerdebegga <stoerdebegga@mailbox.org>
; @source: https://codeberg.org/stoerdebegga/papercolor-light-contrib
; @source99: https://github.com/stoerdebegga/papercolor-light-contrib
;
background=#EEEEEE
text=#4D4D4C
subtle=#4271AE
warning-text=#D7005F
text-special-one=#969694
text-special-two=#D7005F
top-bar-background=#4271AE
top-bar-text=#EEEEEE
status-bar-background=#4271AE
status-bar-text=#EEEEEE
status-bar-view-background=#718C00
status-bar-view-text=#f5f5f5
command-text=#4D4D4C
list-selected-background=#D7005F
list-selected-text=#f5f5f5

22
config/themes/papercolor-light.toml

@ -0,0 +1,22 @@
# This is an adaption of NLKNguyen (https://github.com/NLKNguyen)'s
# PaperColor Light theme for tut (https://github.com/RasmusLindroth/tut).
#
# @author: stoerdebegga <stoerdebegga@mailbox.org>
# @source: https://codeberg.org/stoerdebegga/papercolor-light-contrib
# @source99: https://github.com/stoerdebegga/papercolor-light-contrib
#
background="#EEEEEE"
text="#4D4D4C"
subtle="#4271AE"
warning-text="#D7005F"
text-special-one="#969694"
text-special-two="#D7005F"
top-bar-background="#4271AE"
top-bar-text="#EEEEEE"
status-bar-background="#4271AE"
status-bar-text="#EEEEEE"
status-bar-view-background="#718C00"
status-bar-view-text="#f5f5f5"
command-text="#4D4D4C"
list-selected-background="#D7005F"
list-selected-text="#f5f5f5"

27
config/themes/snow-white.toml

@ -0,0 +1,27 @@
background="#ffffff"
text="#000000"
subtle="#000000"
warning-text="#d7005f"
text-special-one="#000000"
text-special-two="#d7005f"
top-bar-background="#4271ae"
top-bar-text="#ffffff"
status-bar-background="#f0f0f0"
status-bar-text="#000000"
status-bar-view-background="#505050"
status-bar-view-text="#ffffff"
list-selected-background="#4271ae"
list-selected-text="#ffffff"
command-text="#000000"
list-selected-inactive-background="#6d6d6d"
list-selected-inactive-text="#ffffff"
controls-text="#000000"
controls-highlight="#f92672"
autocomplete-background="#fafafa"
autocomplete-text="#000000"
autocomplete-selected-background="#4271ae"
autocomplete-selected-text="#ffffff"
button-color-one="#4271ae"
button-color-two="#fafafa"
timeline-name-background="#f0f0f0"
timeline-name-text="#000000"

258
config/toml.go

@ -0,0 +1,258 @@
package config
type ConfigTOML struct {
General GeneralTOML `toml:"general"`
Style StyleTOML `toml:"style"`
Media MediaTOML `toml:"media"`
OpenPattern OpenPatternTOML `toml:"open-pattern"`
OpenCustom OpenCustomTOML `toml:"open-custom"`
NotificationConfig NotificationsTOML `toml:"desktop-notification"`
Input InputTOML `toml:"input"`
}
type GeneralTOML struct {
Editor *string `toml:"editor"`
Confirmation *bool `toml:"confirmation"`
MouseSupport *bool `toml:"mouse-support"`
DateFormat *string `toml:"date-format"`
DateTodayFormat *string `toml:"date-today-format"`
DateRelative *int `toml:"date-relative"`
MaxWidth *int `toml:"max-width"`
QuoteReply *bool `toml:"quote-reply"`
ShortHints *bool `toml:"short-hints"`
ShowFilterPhrase *bool `toml:"show-filter-phrase"`
ListPlacement *string `toml:"list-placement"`
ListSplit *string `toml:"list-split"`
ListProportion *int `toml:"list-proportion"`
ContentProportion *int `toml:"content-proportion"`
TerminalTitle *int `toml:"terminal-title"`
ShowIcons *bool `toml:"show-icons"`
ShowHelp *bool `toml:"show-help"`
RedrawUI *bool `toml:"redraw-ui"`
LeaderKey *string `toml:"leader-key"`
LeaderTimeout *int64 `toml:"leader-timeout"`
Timelines *[]TimelineTOML `toml:"timelines"`
LeaderActions *[]LeaderActionTOML `toml:"leader-actions"`
StickToTop *bool `toml:"stick-to-top"`
NotificationsToHide *[]string `toml:"notifications-to-hide"`
ShowBoostedUser *bool `toml:"show-boosted-user"`
DynamicTimelineName *bool `toml:"dynamic-timeline-name"`
CommandsInNewPane *bool `toml:"commands-in-new-pane"`
}
type TimelineTOML struct {
Name *string `toml:"name"`
Type *string `toml:"type"`
Data *string `toml:"data"`
Keys *[]string `toml:"keys"`
SpecialKeys *[]string `toml:"special-keys"`
Shortcut *string `toml:"shortcut"`
HideBoosts *bool `toml:"hide-boosts"`
HideReplies *bool `toml:"hide-replies"`
Closed *bool `toml:"closed"`
OnCreationClosed *string `toml:"on-creation-closed"`
OnFocus *string `toml:"on-focus"`
}
type LeaderActionTOML struct {
Type *string `toml:"type"`
Data *string `toml:"data"`
Shortcut *string `toml:"shortcut"`
}
type StyleTOML struct {
Theme *string `toml:"theme"`
XrdbPrefix *string `toml:"xrdb-prefix"`
Background *string `toml:"background"`
Text *string `toml:"text"`
Subtle *string `toml:"subtle"`
WarningText *string `toml:"warning-text"`
TextSpecial1 *string `toml:"text-special-one"`
TextSpecial2 *string `toml:"text-special-two"`
TopBarBackground *string `toml:"top-bar-background"`
TopBarText *string `toml:"top-bar-text"`
StatusBarBackground *string `toml:"status-bar-background"`
StatusBarText *string `toml:"status-bar-text"`
StatusBarViewBackground *string `toml:"status-bar-view-background"`
StatusBarViewText *string `toml:"status-bar-view-text"`
ListSelectedBackground *string `toml:"list-selected-background"`
ListSelectedText *string `toml:"list-selected-text"`
ListSelectedInactiveBackground *string `toml:"list-selected-inactive-background"`
ListSelectedInactiveText *string `toml:"list-selected-inactive-text"`
ControlsText *string `toml:"controls-text"`
ControlsHighlight *string `toml:"controls-highlight"`
AutocompleteBackground *string `toml:"autocomplete-background"`
AutocompleteText *string `toml:"autocomplete-text"`
AutocompleteSelectedBackground *string `toml:"autocomplete-selected-background"`
AutocompleteSelectedText *string `toml:"autocomplete-selected-text"`
ButtonColorOne *string `toml:"button-color-one"`
ButtonColorTwo *string `toml:"button-color-two"`
TimelineNameBackground *string `toml:"timeline-name-background"`
TimelineNameText *string `toml:"timeline-name-text"`
IconColor *string `toml:"icon-color"`
CommandText *string `toml:"command-text"`
}
type ViewerTOML struct {
Program *string `toml:"program"`
Args *string `toml:"args"`
Terminal *bool `toml:"terminal"`
Single *bool `toml:"single"`
Reverse *bool `toml:"reverse"`
}
type MediaTOML struct {
DeleteTmpFiles *bool `toml:"delete-temp-files"`
Image *ViewerTOML `toml:"image"`
Video *ViewerTOML `toml:"video"`
Audio *ViewerTOML `toml:"audio"`
Link *ViewerTOML `toml:"link"`
}
type PatternTOML struct {
Matching *string `toml:"matching"`
Program *string `toml:"program"`
Args *string `toml:"args"`
Terminal *bool `toml:"terminal"`
}
type OpenPatternTOML struct {
Patterns *[]PatternTOML `toml:"patterns"`
}
type CustomTOML struct {
Program *string `toml:"program"`
Args *string `toml:"args"`
Terminal *bool `toml:"terminal"`
Hint *string `toml:"hint"`
Keys *[]string `toml:"keys"`
SpecialKeys *[]string `toml:"special-keys"`
}
type OpenCustomTOML struct {
Programs *[]CustomTOML `toml:"programs"`
}
type NotificationsTOML struct {
Followers *bool `toml:"followers"`
Favorite *bool `toml:"favorite"`
Mention *bool `toml:"mention"`
Update *bool `toml:"update"`
Boost *bool `toml:"boost"`
Poll *bool `toml:"poll"`
Posts *bool `toml:"posts"`
}
type KeyHintTOML struct {
Hint *string `toml:"hint"`
HintAlt *string `toml:"hint-alt"`
Keys *[]string `toml:"keys"`
SpecialKeys *[]string `toml:"special-keys"`
}
type InputTOML struct {
GlobalDown *KeyHintTOML `toml:"global-down"`
GlobalUp *KeyHintTOML `toml:"global-up"`
GlobalEnter *KeyHintTOML `toml:"global-enter"`
GlobalBack *KeyHintTOML `toml:"global-back"`
GlobalExit *KeyHintTOML `toml:"global-exit"`
MainHome *KeyHintTOML `toml:"main-home"`
MainEnd *KeyHintTOML `toml:"main-end"`
MainPrevFeed *KeyHintTOML `toml:"main-prev-feed"`
MainNextFeed *KeyHintTOML `toml:"main-next-feed"`
MainPrevPane *KeyHintTOML `toml:"main-prev-pane"`
MainNextPane *KeyHintTOML `toml:"main-next-pane"`
MainCompose *KeyHintTOML `toml:"main-compose"`
MainNextAccount *KeyHintTOML `toml:"main-next-account"`
MainPrevAccount *KeyHintTOML `toml:"main-prev-account"`
StatusAvatar *KeyHintTOML `toml:"status-avatar"`
StatusBoost *KeyHintTOML `toml:"status-boost"`
StatusDelete *KeyHintTOML `toml:"status-delete"`
StatusEdit *KeyHintTOML `toml:"status-edit"`
StatusFavorite *KeyHintTOML `toml:"status-favorite"`
StatusMedia *KeyHintTOML `toml:"status-media"`
StatusLinks *KeyHintTOML `toml:"status-links"`
StatusPoll *KeyHintTOML `toml:"status-poll"`
StatusReply *KeyHintTOML `toml:"status-reply"`
StatusBookmark *KeyHintTOML `toml:"status-bookmark"`
StatusThread *KeyHintTOML `toml:"status-thread"`
StatusUser *KeyHintTOML `toml:"status-user"`
StatusViewFocus *KeyHintTOML `toml:"status-view-focus"`
StatusYank *KeyHintTOML `toml:"status-yank"`
StatusToggleCW *KeyHintTOML `toml:"status-toggle-cw"`
StatusShowFiltered *KeyHintTOML `toml:"status-show-filtered"`
UserAvatar *KeyHintTOML `toml:"user-avatar"`
UserBlock *KeyHintTOML `toml:"user-block"`
UserFollow *KeyHintTOML `toml:"user-follow"`
UserFollowRequestDecide *KeyHintTOML `toml:"user-follow-request-decide"`
UserMute *KeyHintTOML `toml:"user-mute"`
UserLinks *KeyHintTOML `toml:"user-links"`
UserUser *KeyHintTOML `toml:"user-user"`
UserViewFocus *KeyHintTOML `toml:"user-view-focus"`
UserYank *KeyHintTOML `toml:"user-yank"`
ListOpenFeed *KeyHintTOML `toml:"list-open-feed"`
ListUserList *KeyHintTOML `toml:"list-user-list"`
ListUserAdd *KeyHintTOML `toml:"list-user-add"`
ListUserDelete *KeyHintTOML `toml:"list-user-delete"`
TagOpenFeed *KeyHintTOML `toml:"tag-open-feed"`
TagFollow *KeyHintTOML `toml:"tag-follow"`
LinkOpen *KeyHintTOML `toml:"link-open"`
LinkYank *KeyHintTOML `toml:"link-yank"`
ComposeEditCW *KeyHintTOML `toml:"compose-edit-cw"`
ComposeEditText *KeyHintTOML `toml:"compose-edit-text"`
ComposeIncludeQuote *KeyHintTOML `toml:"compose-include-quote"`
ComposeMediaFocus *KeyHintTOML `toml:"compose-media-focus"`
ComposePost *KeyHintTOML `toml:"compose-post"`
ComposeToggleContentWarning *KeyHintTOML `toml:"compose-toggle-content-warning"`
ComposeVisibility *KeyHintTOML `toml:"compose-visibility"`
ComposeLanguage *KeyHintTOML `toml:"compose-language"`
ComposePoll *KeyHintTOML `toml:"compose-poll"`
MediaDelete *KeyHintTOML `toml:"media-delete"`
MediaEditDesc *KeyHintTOML `toml:"media-edit-desc"`
MediaAdd *KeyHintTOML `toml:"media-add"`
VoteVote *KeyHintTOML `toml:"vote-vote"`
VoteSelect *KeyHintTOML `toml:"vote-select"`
PollAdd *KeyHintTOML `toml:"poll-add"`
PollEdit *KeyHintTOML `toml:"poll-edit"`
PollDelete *KeyHintTOML `toml:"poll-delete"`
PollMultiToggle *KeyHintTOML `toml:"poll-multi-toggle"`
PollExpiration *KeyHintTOML `toml:"poll-expiration"`
PreferenceName *KeyHintTOML `toml:"preference-name"`
PreferenceVisibility *KeyHintTOML `toml:"preference-visibility"`
PreferenceBio *KeyHintTOML `toml:"preference-bio"`
PreferenceSave *KeyHintTOML `toml:"preference-save"`
PreferenceFields *KeyHintTOML `toml:"preference-fields"`
PreferenceFieldsAdd *KeyHintTOML `toml:"preference-fields-add"`
PreferenceFieldsEdit *KeyHintTOML `toml:"preference-fields-edit"`
PreferenceFieldsDelete *KeyHintTOML `toml:"preference-fields-delete"`
EditorExit *KeyHintTOML `toml:"editor-exit"`
}

446
config/toml_default.go

@ -0,0 +1,446 @@
package config
var tvar = true
var fvar = false
var bt = &tvar
var bf = &fvar
func sp(s string) *string {
return &s
}
func ip(i int) *int {
return &i
}
func ip64(i int64) *int64 {
return &i
}
var ConfigDefault = ConfigTOML{
General: GeneralTOML{
Editor: sp("TUT_USE_INTERNAL"),
Confirmation: bt,
MouseSupport: bf,
DateFormat: sp("2006-01-02 15:04"),
DateTodayFormat: sp("15:04"),
DateRelative: ip(-1),
QuoteReply: bf,
MaxWidth: ip(0),
ShortHints: bf,
ShowFilterPhrase: bt,
ShowIcons: bt,
ShowHelp: bt,
RedrawUI: bt,
StickToTop: bf,
ShowBoostedUser: bf,
DynamicTimelineName: bt,
CommandsInNewPane: bt,
ListPlacement: sp("left"),
ListSplit: sp("row"),
ListProportion: ip(1),
ContentProportion: ip(2),
TerminalTitle: ip(0),
LeaderKey: sp(""),
LeaderTimeout: ip64(1000),
NotificationsToHide: &[]string{},
Timelines: &[]TimelineTOML{
{
Name: sp("Home"),
Type: sp("home"),
HideBoosts: bf,
HideReplies: bf,
},
{
Name: sp("Notifications"),
Type: sp("notifications"),
Keys: &[]string{"n", "N"},
},
},
},
Style: StyleTOML{
Theme: sp("none"),
XrdbPrefix: sp("guess"),
Background: sp("#272822"),
Text: sp("#f8f8f2"),
Subtle: sp("#808080"),
WarningText: sp("#f92672"),
TextSpecial1: sp("#ae81ff"),
TextSpecial2: sp("#a6e22e"),
TopBarBackground: sp("#f92672"),
TopBarText: sp("#f8f8f2"),
StatusBarBackground: sp("#f92672"),
StatusBarText: sp("#f8f8f2"),
StatusBarViewBackground: sp("#ae81ff"),
StatusBarViewText: sp("#f8f8f2"),
CommandText: sp("#f8f8f2"),
ListSelectedBackground: sp("#f92672"),
ListSelectedText: sp("#f8f8f2"),
ListSelectedInactiveBackground: sp("#ae81ff"),
ListSelectedInactiveText: sp("#f8f8f2"),
ControlsText: sp("#f8f8f2"),
ControlsHighlight: sp("#a6e22e"),
AutocompleteBackground: sp("#272822"),
AutocompleteText: sp("#f8f8f2"),
AutocompleteSelectedBackground: sp("#ae81ff"),
AutocompleteSelectedText: sp("#f8f8f2"),
ButtonColorOne: sp("#f92672"),
ButtonColorTwo: sp("#272822"),
TimelineNameBackground: sp("#272822"),
TimelineNameText: sp("#808080"),
},
Media: MediaTOML{
DeleteTmpFiles: bt,
Image: &ViewerTOML{
Program: sp("TUT_OS_DEFAULT"),
Args: sp(""),
Terminal: bf,
Single: bt,
Reverse: bf,
},
Video: &ViewerTOML{
Program: sp("TUT_OS_DEFAULT"),
Args: sp(""),
Terminal: bf,
Single: bt,
Reverse: bf,
},
Audio: &ViewerTOML{
Program: sp("TUT_OS_DEFAULT"),
Args: sp(""),
Terminal: bf,
Single: bt,
Reverse: bf,
},
Link: &ViewerTOML{
Program: sp("TUT_OS_DEFAULT"),
Args: sp(""),
Terminal: bf,
Single: bt,
Reverse: bf,
},
},
NotificationConfig: NotificationsTOML{
Followers: bf,
Favorite: bf,
Mention: bf,
Update: bf,
Boost: bf,
Poll: bf,
Posts: bf,
},
Input: InputTOML{
GlobalDown: &KeyHintTOML{
Keys: &[]string{"j", "J"},
SpecialKeys: &[]string{"Down"},
},
GlobalUp: &KeyHintTOML{
Keys: &[]string{"k", "K"},
SpecialKeys: &[]string{"Up"},
},
GlobalEnter: &KeyHintTOML{
SpecialKeys: &[]string{"Enter"},
},
GlobalBack: &KeyHintTOML{
Hint: sp("[Esc]"),
SpecialKeys: &[]string{"Esc"},
},
GlobalExit: &KeyHintTOML{
Hint: sp("[Q]uit"),
Keys: &[]string{"q", "Q"},
},
MainHome: &KeyHintTOML{
Hint: sp(""),
Keys: &[]string{"g"},
SpecialKeys: &[]string{"Home"},
},
MainEnd: &KeyHintTOML{
Hint: sp(""),
Keys: &[]string{"G"},
SpecialKeys: &[]string{"End"},
},
MainPrevFeed: &KeyHintTOML{
Hint: sp(""),
Keys: &[]string{"h", "H"},
SpecialKeys: &[]string{"Left"},
},
MainNextFeed: &KeyHintTOML{
Hint: sp(""),
Keys: &[]string{"l", "L"},
SpecialKeys: &[]string{"Right"},
},
MainPrevPane: &KeyHintTOML{
Hint: sp(""),
SpecialKeys: &[]string{"Backtab"},
},
MainNextPane: &KeyHintTOML{
Hint: sp(""),
SpecialKeys: &[]string{"Tab"},
},
MainCompose: &KeyHintTOML{
Hint: sp(""),
Keys: &[]string{"c", "C"},
},
MainNextAccount: &KeyHintTOML{
Hint: sp(""),
SpecialKeys: &[]string{"Ctrl-N"},
},
MainPrevAccount: &KeyHintTOML{
Hint: sp(""),
SpecialKeys: &[]string{"Ctrl-P"},
},
StatusAvatar: &KeyHintTOML{
Hint: sp("[A]vatar"),
Keys: &[]string{"a", "A"},
},
StatusBoost: &KeyHintTOML{
Hint: sp("[B]oost"),
HintAlt: sp("Un[B]oost"),
Keys: &[]string{"b", "B"},
},
StatusEdit: &KeyHintTOML{
Hint: sp("[E]dit"),
Keys: &[]string{"e", "E"},
},
StatusDelete: &KeyHintTOML{
Hint: sp("[D]elete"),
Keys: &[]string{"d", "D"},
},
StatusFavorite: &KeyHintTOML{
Hint: sp("[F]avorite"),
HintAlt: sp("Un[F]avorite"),
Keys: &[]string{"f", "F"},
},
StatusMedia: &KeyHintTOML{
Hint: sp("[M]edia"),
Keys: &[]string{"m", "M"},
},
StatusLinks: &KeyHintTOML{
Hint: sp("[O]pen"),
Keys: &[]string{"o", "O"},
},
StatusPoll: &KeyHintTOML{
Hint: sp("[P]oll"),
Keys: &[]string{"p", "P"},
},
StatusReply: &KeyHintTOML{
Hint: sp("[R]eply"),
Keys: &[]string{"r", "R"},
},
StatusBookmark: &KeyHintTOML{
Hint: sp("[S]ave"),
HintAlt: sp("Un[S]ave"),
Keys: &[]string{"s", "S"},
},
StatusThread: &KeyHintTOML{
Hint: sp("[T]hread"),
Keys: &[]string{"t", "T"},
},
StatusUser: &KeyHintTOML{
Hint: sp("[U]ser"),
Keys: &[]string{"u", "U"},
},
StatusViewFocus: &KeyHintTOML{
Hint: sp("[V]iew"),
Keys: &[]string{"v", "V"},
},
StatusYank: &KeyHintTOML{
Hint: sp("[Y]ank"),
Keys: &[]string{"y", "Y"},
},
StatusToggleCW: &KeyHintTOML{
Hint: sp("Press [Z] to toggle cw"),
Keys: &[]string{"z", "Z"},
},
StatusShowFiltered: &KeyHintTOML{
Hint: sp("Press [Z] to view filtered toot"),
Keys: &[]string{"z", "Z"},
},
UserAvatar: &KeyHintTOML{
Hint: sp("[A]vatar"),
Keys: &[]string{"a", "A"},
},
UserBlock: &KeyHintTOML{
Hint: sp("[B]lock"),
HintAlt: sp("Un[B]lock"),
Keys: &[]string{"b", "B"},
},
UserFollow: &KeyHintTOML{
Hint: sp("[F]ollow"),
HintAlt: sp("Un[F]ollow"),
Keys: &[]string{"f", "F"},
},
UserFollowRequestDecide: &KeyHintTOML{
Hint: sp("Follow [R]equest"),
HintAlt: sp("Follow [R]equest"),
Keys: &[]string{"r", "R"},
},
UserMute: &KeyHintTOML{
Hint: sp("[M]ute"),
HintAlt: sp("Un[M]ute"),
Keys: &[]string{"m", "M"},
},
UserLinks: &KeyHintTOML{
Hint: sp("[O]pen"),
Keys: &[]string{"o", "O"},
},
UserUser: &KeyHintTOML{
Hint: sp("[U]ser"),
Keys: &[]string{"u", "U"},
},
UserViewFocus: &KeyHintTOML{
Hint: sp("[V]iew"),
Keys: &[]string{"v", "V"},
},
UserYank: &KeyHintTOML{
Hint: sp("[Y]ank"),
Keys: &[]string{"y", "Y"},
},
ListOpenFeed: &KeyHintTOML{
Hint: sp("[O]pen"),
Keys: &[]string{"o", "O"},
},
ListUserList: &KeyHintTOML{
Hint: sp("[U]sers"),
Keys: &[]string{"u", "U"},
},
ListUserAdd: &KeyHintTOML{
Hint: sp("[A]dd"),
Keys: &[]string{"a", "A"},
},
ListUserDelete: &KeyHintTOML{
Hint: sp("[D]elete"),
Keys: &[]string{"d", "D"},
},
LinkOpen: &KeyHintTOML{
Hint: sp("[O]pen"),
Keys: &[]string{"o", "O"},
},
LinkYank: &KeyHintTOML{
Hint: sp("[Y]ank"),
Keys: &[]string{"y", "Y"},
},
TagOpenFeed: &KeyHintTOML{
Hint: sp("[O]pen"),
Keys: &[]string{"o", "O"},
},
TagFollow: &KeyHintTOML{
Hint: sp("[F]ollow"),
HintAlt: sp("Un[F]ollow"),
Keys: &[]string{"f", "F"},
},
ComposeEditCW: &KeyHintTOML{
Hint: sp("[C]W text"),
Keys: &[]string{"c", "C"},
},
ComposeEditText: &KeyHintTOML{
Hint: sp("[E]dit text"),
Keys: &[]string{"e", "E"},
},
ComposeIncludeQuote: &KeyHintTOML{
Hint: sp("[I]nclude quote"),
Keys: &[]string{"i", "I"},
},
ComposeMediaFocus: &KeyHintTOML{
Hint: sp("[M]edia"),
Keys: &[]string{"m", "M"},
},
ComposePost: &KeyHintTOML{
Hint: sp("[P]ost"),
Keys: &[]string{"p", "P"},
},
ComposeToggleContentWarning: &KeyHintTOML{
Hint: sp("[T]oggle CW"),
Keys: &[]string{"t", "T"},
},
ComposeVisibility: &KeyHintTOML{
Hint: sp("[V]isibility"),
Keys: &[]string{"v", "V"},
},
ComposeLanguage: &KeyHintTOML{
Hint: sp("[L]ang"),
Keys: &[]string{"l", "L"},
},
ComposePoll: &KeyHintTOML{
Hint: sp("P[O]ll"),
Keys: &[]string{"o", "O"},
},
MediaDelete: &KeyHintTOML{
Hint: sp("[D]elete"),
Keys: &[]string{"d", "D"},
},
MediaEditDesc: &KeyHintTOML{
Hint: sp("[E]dit desc"),
Keys: &[]string{"e", "E"},
},
MediaAdd: &KeyHintTOML{
Hint: sp("[A]dd"),
Keys: &[]string{"a", "A"},
},
VoteVote: &KeyHintTOML{
Hint: sp("[V]ote"),
Keys: &[]string{"v", "V"},
},
VoteSelect: &KeyHintTOML{
Hint: sp("[Enter] to select"),
Keys: &[]string{" "},
SpecialKeys: &[]string{"Enter"},
},
PollAdd: &KeyHintTOML{
Hint: sp("[A]dd"),
Keys: &[]string{"a", "A"},
},
PollEdit: &KeyHintTOML{
Hint: sp("[E]dit"),
Keys: &[]string{"e", "E"},
},
PollDelete: &KeyHintTOML{
Hint: sp("[D]elete"),
Keys: &[]string{"d", "D"},
},
PollMultiToggle: &KeyHintTOML{
Hint: sp("Toggle [M]ultiple"),
Keys: &[]string{"m", "M"},
},
PollExpiration: &KeyHintTOML{
Hint: sp("E[X]pires"),
Keys: &[]string{"x", "X"},
},
PreferenceName: &KeyHintTOML{
Hint: sp("[N]ame"),
Keys: &[]string{"n", "N"},
},
PreferenceVisibility: &KeyHintTOML{
Hint: sp("[V]isibility"),
Keys: &[]string{"v", "V"},
},
PreferenceBio: &KeyHintTOML{
Hint: sp("[B]io"),
Keys: &[]string{"b", "B"},
},
PreferenceSave: &KeyHintTOML{
Hint: sp("[S]ave"),
Keys: &[]string{"s", "S"},
},
PreferenceFields: &KeyHintTOML{
Hint: sp("[F]ields"),
Keys: &[]string{"f", "F"},
},
PreferenceFieldsAdd: &KeyHintTOML{
Hint: sp("[A]dd"),
Keys: &[]string{"a", "A"},
},
PreferenceFieldsEdit: &KeyHintTOML{
Hint: sp("[E]dit"),
Keys: &[]string{"e", "E"},
},
PreferenceFieldsDelete: &KeyHintTOML{
Hint: sp("[D]elete"),
Keys: &[]string{"d", "D"},
},
EditorExit: &KeyHintTOML{
Hint: sp("[Esc] when done"),
SpecialKeys: &[]string{"Esc"},
},
},
}

1
config/toot.tmpl

@ -71,3 +71,4 @@
{{- Color .Style.TextSpecial1 }} {{ .Toot.Boosts }}
{{- Color .Style.Subtle }} Favorites
{{- Color .Style.TextSpecial1 }} {{ .Toot.Favorites }}
{{- Color .Style.TextSpecial2 }} {{ .Toot.Lang }}

14
docs/man/tut.1

@ -14,7 +14,7 @@
. ftr VB CB
. ftr VBI CBI
.\}
.TH "tut" "1" "2023-01-01" "tut 1.0.34" ""
.TH "tut" "1" "2023-01-23" "tut 2.0.0" ""
.hy
.SH NAME
.PP
@ -39,13 +39,17 @@ Show the version number
Add one more user to tut
.TP
\f[B]-c\f[R], \f[B]--config\f[R] <path>
Load config.ini from \f[I]<path>\f[R]
Load config.toml from \f[I]<path>\f[R]
.TP
\f[B]-d\f[R], \f[B]--config-dir\f[R] <path>
Load all config from \f[I]<path>\f[R]
.TP
\f[B]-u\f[R], \f[B]--user\f[R] <name>
Login directly to user named \f[I]<name>\f[R].
If you want to login to multiple accounts seperate them with a space and
use quotation marks.
E.g.
-u \[lq]acc_one acc_two\[rq].
If two users are named the same, use full name like
\f[I]tut\[at]fosstodon.org\f[R]
.SH COMMANDS
@ -55,15 +59,15 @@ Runs the TUI
.TP
\f[B]example-config\f[R]
Generates the default configuration file in the current directory and
names it ./config.example.ini
names it ./config.example.toml
.SH CONFIGURATION
.PP
Tut is configurable, so you can change things like the colors, the
default timeline, what image viewer to use and some more.
Check out tut(5) or the configuration file to see all the options.
.PP
You find it in \f[I]$XDG_CONFIG_HOME/tut/config.ini\f[R] on Linux which
usually equals to \f[I]\[ti]/.config/tut/config.ini\f[R].
You find it in \f[I]$XDG_CONFIG_HOME/tut/config.toml\f[R] on Linux which
usually equals to \f[I]\[ti]/.config/tut/config.toml\f[R].
If you don\[cq]t run Linux it will use the path of the Go funcdtion
os.UserConfigDir().
But if you move the tut folder to \f[I]XDG_CONFIG_HOME/tut/\f[R] and

11
docs/man/tut.1.md

@ -1,6 +1,6 @@
% tut(1) tut 1.0.34
% tut(1) tut 2.0.0
% Rasmus Lindroth
% 2023-01-01
% 2023-01-23
# NAME
tut - a Mastodon TUI
@ -24,13 +24,14 @@ To see keys and commands you can use inside of tut check tut(7).
: Add one more user to tut
**-c**, **\--config** \<path\>
: Load config.ini from *\<path\>*
: Load config.toml from *\<path\>*
**-d**, **\--config-dir** \<path\>
: Load all config from *\<path\>*
**-u**, **\--user** \<name\>
: Login directly to user named *\<name\>*.
: If you want to login to multiple accounts seperate them with a space and use quotation marks. E.g. -u "acc_one acc_two".
: If two users are named the same, use full name like *tut@fosstodon.org*
# COMMANDS
@ -39,12 +40,12 @@ To see keys and commands you can use inside of tut check tut(7).
: Runs the TUI
**example-config**
: Generates the default configuration file in the current directory and names it ./config.example.ini
: Generates the default configuration file in the current directory and names it ./config.example.toml
# CONFIGURATION
Tut is configurable, so you can change things like the colors, the default timeline, what image viewer to use and some more. Check out tut(5) or the configuration file to see all the options.
You find it in *$XDG_CONFIG_HOME/tut/config.ini* on Linux which usually equals to *~/.config/tut/config.ini*.
You find it in *$XDG_CONFIG_HOME/tut/config.toml* on Linux which usually equals to *~/.config/tut/config.toml*.
If you don't run Linux it will use the path of the Go funcdtion os.UserConfigDir().
But if you move the tut folder to *XDG_CONFIG_HOME/tut/* and have set the environment variable *XDG_CONFIG_HOME*
it will look there instead of the standard place.

1926
docs/man/tut.5

File diff suppressed because it is too large Load Diff

1416
docs/man/tut.5.md

File diff suppressed because it is too large Load Diff

37
docs/man/tut.7

@ -14,7 +14,7 @@
. ftr VB CB
. ftr VBI CBI
.\}
.TH "tut" "7" "2023-01-01" "tut 1.0.34" ""
.TH "tut" "7" "2023-01-23" "tut 2.0.0" ""
.hy
.SH NAME
.PP
@ -108,8 +108,13 @@ List all your bookmarks
\f[B]:clear-notifications\f[R]
Remove all of your notifications
.TP
\f[B]:close-window\f[R]
Closes the current window, including all the timelines in said window
\f[B]:clear-temp\f[R]
Remove all of your media files that have been downloaded.
Only needed if you have set delete-temp-files to false under [media] in
your config.
.TP
\f[B]:close-pane\f[R]
Closes the current pane, including all the timelines in said pane
.TP
\f[B]:compose\f[R]
Compose a new toot
@ -150,12 +155,15 @@ Show a list of your lists
Place the list in choosen placement
.TP
\f[B]:list-split\f[R] \f[I]row|column\f[R]
Split the timelines in window by row or column
Split the timelines by row or column
.TP
\f[B]:login\f[R]
Login to one more account
.TP
\f[B]:move-window\f[R] \f[I]left|right|up|down|home|end\f[R]
Moves the window in choosen direction
\f[B]:move-pane\f[R] \f[I]left|right|up|down|home|end\f[R]
Moves the pane in choosen direction
.TP
\f[B]:mv\f[R] \f[I]l|r|u|d|h|e\f[R]
\f[B]:mp\f[R] \f[I]l|r|u|d|h|e\f[R]
Shorter form of former command
.TP
\f[B]:muting\f[R]
@ -164,15 +172,21 @@ Lists users that you\[aq]ve muted
\f[B]:newer\f[R]
Force load newer toots in current timeline
.TP
\f[B]:next-acct\f[R]
Go to the next account if you\[aq]re logged in to multiple
.TP
\f[B]:preferences\f[R]
Update your profile and some other settings
.TP
\f[B]:prev-acct\f[R]
Go to the prev account if you\[aq]re logged in to multiple
.TP
\f[B]:profile\f[R]
Go to your profile
.TP
\f[B]:proportions\f[R] \f[I][int] [int]\f[R]
Sets the proportions of the windows and the content.
The first integer is your windows and the other for content,
Sets the proportions of the panes and the content.
The first integer is your panes and the other for content,
e.g.\ :proportions 1 3
.TP
\f[B]:refetch\f[R]
@ -202,9 +216,8 @@ Search for users named <username>, e.g.\ :user rasmus.
To narrow a search include the instance like this :user
rasmus\[at]mastodon.acc.sunet.se
.TP
\f[B]:window\f[R] \f[I]<int>\f[R]
Switch window by index (zero indexed) e.g.\ :window 0 for the left/top
window
\f[B]:pane\f[R] \f[I]<int>\f[R]
Switch pane by index (zero indexed) e.g.\ :pane 0 for the left/top pane
.SH SEE ALSO
.IP
.nf

36
docs/man/tut.7.md

@ -1,6 +1,6 @@
% tut(7) tut 1.0.34
% tut(7) tut 2.0.0
% Rasmus Lindroth
% 2023-01-01
% 2023-01-23
# NAME
tut - keys and commands inside of tut(1)
@ -53,8 +53,11 @@ To change the keys look at tut(5) under the *INPUT* section.
**:clear-notifications**
: Remove all of your notifications
**:close-window**
: Closes the current window, including all the timelines in said window
**:clear-temp**
: Remove all of your media files that have been downloaded. Only needed if you have set delete-temp-files to false under \[media\] in your config.
**:close-pane**
: Closes the current pane, including all the timelines in said pane
**:compose**
: Compose a new toot
@ -93,12 +96,15 @@ To change the keys look at tut(5) under the *INPUT* section.
: Place the list in choosen placement
**:list-split** *row|column*
: Split the timelines in window by row or column
: Split the timelines by row or column
**:login**
: Login to one more account
**:move-window** *left|right|up|down|home|end*
: Moves the window in choosen direction
**:move-pane** *left|right|up|down|home|end*
: Moves the pane in choosen direction
**:mv** *l|r|u|d|h|e*
**:mp** *l|r|u|d|h|e*
: Shorter form of former command
**:muting**
@ -107,14 +113,20 @@ To change the keys look at tut(5) under the *INPUT* section.
**:newer**
: Force load newer toots in current timeline
**:next-acct**
: Go to the next account if you\'re logged in to multiple
**:preferences**
: Update your profile and some other settings
**:prev-acct**
: Go to the prev account if you\'re logged in to multiple
**:profile**
: Go to your profile
**:proportions** *[int] [int]*
: Sets the proportions of the windows and the content. The first integer is your windows and the other for content, e.g. :proportions 1 3
**:proportions** *\[int\] \[int\]*
: Sets the proportions of the panes and the content. The first integer is your panes and the other for content, e.g. :proportions 1 3
**:refetch**
: Refetches the current item that you\'re viewing. Can be used to update poll results.
@ -137,8 +149,8 @@ To change the keys look at tut(5) under the *INPUT* section.
**:user** *\<username\>*
: Search for users named \<username\>, e.g. :user rasmus. To narrow a search include the instance like this :user rasmus@mastodon.acc.sunet.se
**:window** *\<int\>*
: Switch window by index (zero indexed) e.g. :window 0 for the left/top window
**:pane** *\<int\>*
: Switch pane by index (zero indexed) e.g. :pane 0 for the left/top pane
# SEE ALSO
tut(1) - flags and commands

80
feed/feed.go

@ -64,8 +64,8 @@ type Feed struct {
streams []*api.Receiver
name string
close func()
showBoosts bool
showReplies bool
hideBoosts bool
hideReplies bool
}
func (f *Feed) Type() config.FeedType {
@ -82,10 +82,10 @@ func (f *Feed) filteredList() []api.Item {
if f.Type() == config.TimelineHomeSpecial && x.Reblog == nil && x.InReplyToID == nil {
continue
}
if x.Reblog != nil && !f.showBoosts {
if x.Reblog != nil && f.hideBoosts {
continue
}
if x.InReplyToID != nil && !f.showReplies {
if x.InReplyToID != nil && f.hideReplies {
continue
}
}
@ -836,7 +836,7 @@ func (f *Feed) startStreamNotification(rec *api.Receiver, timeline string, err e
}()
}
func newFeed(ac *api.AccountClient, ft config.FeedType, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
func newFeed(ac *api.AccountClient, ft config.FeedType, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
return &Feed{
accountClient: ac,
config: cnf,
@ -849,13 +849,13 @@ func newFeed(ac *api.AccountClient, ft config.FeedType, cnf *config.Config, show
Update: make(chan DesktopNotificationHolder, 1),
loadingNewer: &LoadingLock{},
loadingOlder: &LoadingLock{},
showBoosts: showBoosts,
showReplies: showReplies,
hideBoosts: hideBoosts,
hideReplies: hideReplies,
}
}
func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.TimelineHome, cnf, showBoosts, showReplies)
func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineHome, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) }
feed.startStream(feed.accountClient.NewHomeStream())
@ -868,8 +868,8 @@ 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)
func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineHomeSpecial, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) }
feed.startStream(feed.accountClient.NewHomeStream())
@ -882,8 +882,8 @@ func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, showBoost
return feed
}
func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.TimelineFederated, cnf, showBoosts, showReplies)
func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineFederated, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineFederated) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineFederated) }
feed.startStream(feed.accountClient.NewFederatedStream())
@ -896,8 +896,8 @@ func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, showBoosts
return feed
}
func NewTimelineLocal(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.TimelineLocal, cnf, showBoosts, showReplies)
func NewTimelineLocal(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineLocal, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineLocal) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineLocal) }
feed.startStream(feed.accountClient.NewLocalStream())
@ -910,7 +910,7 @@ func NewTimelineLocal(ac *api.AccountClient, cnf *config.Config, showBoosts bool
}
func NewConversations(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Conversations, cnf, true, true)
feed := newFeed(ac, config.Conversations, cnf, false, false)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetConversations) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetConversations) }
feed.startStream(feed.accountClient.NewDirectStream())
@ -923,8 +923,8 @@ func NewConversations(ac *api.AccountClient, cnf *config.Config) *Feed {
return feed
}
func NewNotifications(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.Notifications, cnf, showBoosts, showReplies)
func NewNotifications(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.Notifications, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() {
feed.normalNewerNotification(feed.accountClient.GetNotifications, cnf.General.NotificationsToHide)
}
@ -942,7 +942,7 @@ func NewNotifications(ac *api.AccountClient, cnf *config.Config, showBoosts bool
}
func NewNotificationsMentions(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Notifications, cnf, true, true)
feed := newFeed(ac, config.Notifications, cnf, false, false)
hide := []config.NotificationToHide{config.HideStatus, config.HideBoost, config.HideFollow, config.HideFollowRequest, config.HideFavorite, config.HidePoll, config.HideEdited}
feed.loadNewer = func() {
feed.normalNewerNotification(feed.accountClient.GetNotifications, hide)
@ -961,7 +961,7 @@ func NewNotificationsMentions(ac *api.AccountClient, cnf *config.Config) *Feed {
}
func NewFavorites(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Favorited, cnf, true, true)
feed := newFeed(ac, config.Favorited, cnf, false, false)
feed.loadNewer = func() { feed.linkNewer(feed.accountClient.GetFavorites) }
feed.loadOlder = func() { feed.linkOlder(feed.accountClient.GetFavorites) }
@ -969,7 +969,7 @@ func NewFavorites(ac *api.AccountClient, cnf *config.Config) *Feed {
}
func NewBookmarks(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Saved, cnf, true, true)
feed := newFeed(ac, config.Saved, cnf, false, false)
feed.loadNewer = func() { feed.linkNewer(feed.accountClient.GetBookmarks) }
feed.loadOlder = func() { feed.linkOlder(feed.accountClient.GetBookmarks) }
@ -977,7 +977,7 @@ func NewBookmarks(ac *api.AccountClient, cnf *config.Config) *Feed {
}
func NewUserSearch(ac *api.AccountClient, cnf *config.Config, search string) *Feed {
feed := newFeed(ac, config.UserList, cnf, true, true)
feed := newFeed(ac, config.UserList, cnf, false, false)
feed.name = search
feed.loadNewer = func() { feed.singleNewerSearch(feed.accountClient.GetUsers, search) }
@ -985,7 +985,7 @@ func NewUserSearch(ac *api.AccountClient, cnf *config.Config, search string) *Fe
}
func NewUserProfile(ac *api.AccountClient, cnf *config.Config, user *api.User) *Feed {
feed := newFeed(ac, config.User, cnf, true, true)
feed := newFeed(ac, config.User, cnf, false, false)
feed.name = user.Data.Acct
feed.sticky = append(feed.sticky, api.NewUserItem(user, true))
pinned, err := ac.GetUserPinned(user.Data.ID)
@ -999,7 +999,7 @@ func NewUserProfile(ac *api.AccountClient, cnf *config.Config, user *api.User) *
}
func NewThread(ac *api.AccountClient, cnf *config.Config, status *mastodon.Status) *Feed {
feed := newFeed(ac, config.Thread, cnf, true, true)
feed := newFeed(ac, config.Thread, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1012,7 +1012,7 @@ func NewThread(ac *api.AccountClient, cnf *config.Config, status *mastodon.Statu
}
func NewHistory(ac *api.AccountClient, cnf *config.Config, status *mastodon.Status) *Feed {
feed := newFeed(ac, config.History, cnf, true, true)
feed := newFeed(ac, config.History, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1023,8 +1023,8 @@ func NewHistory(ac *api.AccountClient, cnf *config.Config, status *mastodon.Stat
return feed
}
func NewTag(ac *api.AccountClient, cnf *config.Config, search string, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.Tag, cnf, showBoosts, showReplies)
func NewTag(ac *api.AccountClient, cnf *config.Config, search string, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.Tag, cnf, hideBoosts, hideReplies)
parts := strings.Split(search, " ")
var tparts []string
for _, p := range parts {
@ -1050,7 +1050,7 @@ func NewTag(ac *api.AccountClient, cnf *config.Config, search string, showBoosts
}
func NewTags(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Tags, cnf, true, true)
feed := newFeed(ac, config.Tags, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1064,7 +1064,7 @@ func NewTags(ac *api.AccountClient, cnf *config.Config) *Feed {
}
func NewListList(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Lists, cnf, true, true)
feed := newFeed(ac, config.Lists, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1076,8 +1076,8 @@ func NewListList(ac *api.AccountClient, cnf *config.Config) *Feed {
return feed
}
func NewList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List, showBoosts bool, showReplies bool) *Feed {
feed := newFeed(ac, config.List, cnf, showBoosts, showReplies)
func NewList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.List, cnf, hideBoosts, hideReplies)
feed.name = list.Title
feed.loadNewer = func() { feed.normalNewerID(feed.accountClient.GetListStatuses, list.ID) }
feed.loadOlder = func() { feed.normalOlderID(feed.accountClient.GetListStatuses, list.ID) }
@ -1092,7 +1092,7 @@ func NewList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List, sho
}
func NewUsersInList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List) *Feed {
feed := newFeed(ac, config.ListUsersIn, cnf, true, true)
feed := newFeed(ac, config.ListUsersIn, cnf, false, false)
feed.name = list.Title
once := true
feed.loadNewer = func() {
@ -1107,7 +1107,7 @@ func NewUsersInList(ac *api.AccountClient, cnf *config.Config, list *mastodon.Li
}
func NewUsersAddList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List) *Feed {
feed := newFeed(ac, config.ListUsersAdd, cnf, true, true)
feed := newFeed(ac, config.ListUsersAdd, cnf, false, false)
feed.name = list.Title
once := true
feed.loadNewer = func() {
@ -1122,7 +1122,7 @@ func NewUsersAddList(ac *api.AccountClient, cnf *config.Config, list *mastodon.L
}
func NewFavoritesStatus(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Feed {
feed := newFeed(ac, config.Favorites, cnf, true, true)
feed := newFeed(ac, config.Favorites, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1135,7 +1135,7 @@ func NewFavoritesStatus(ac *api.AccountClient, cnf *config.Config, id mastodon.I
}
func NewBoosts(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Feed {
feed := newFeed(ac, config.Boosts, cnf, true, true)
feed := newFeed(ac, config.Boosts, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1148,7 +1148,7 @@ func NewBoosts(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Feed
}
func NewFollowers(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Feed {
feed := newFeed(ac, config.Followers, cnf, true, true)
feed := newFeed(ac, config.Followers, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1162,7 +1162,7 @@ func NewFollowers(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Fe
}
func NewFollowing(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Feed {
feed := newFeed(ac, config.Following, cnf, true, true)
feed := newFeed(ac, config.Following, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1176,7 +1176,7 @@ func NewFollowing(ac *api.AccountClient, cnf *config.Config, id mastodon.ID) *Fe
}
func NewBlocking(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Blocking, cnf, true, true)
feed := newFeed(ac, config.Blocking, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1190,7 +1190,7 @@ func NewBlocking(ac *api.AccountClient, cnf *config.Config) *Feed {
}
func NewMuting(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.Muting, cnf, true, true)
feed := newFeed(ac, config.Muting, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {
@ -1204,7 +1204,7 @@ func NewMuting(ac *api.AccountClient, cnf *config.Config) *Feed {
}
func NewFollowRequests(ac *api.AccountClient, cnf *config.Config) *Feed {
feed := newFeed(ac, config.FollowRequests, cnf, true, true)
feed := newFeed(ac, config.FollowRequests, cnf, false, false)
once := true
feed.loadNewer = func() {
if once {

17
go.mod

@ -6,18 +6,17 @@ require (
github.com/RasmusLindroth/go-mastodon v0.0.21
github.com/adrg/xdg v0.4.0
github.com/atotto/clipboard v0.1.4
github.com/gdamore/tcell/v2 v2.5.3
github.com/gdamore/tcell/v2 v2.5.4
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6
github.com/gobwas/glob v0.2.3
github.com/icza/gox v0.0.0-20221026131554-a08a8cdc726a
github.com/icza/gox v0.0.0-20230117093757-93f961aa2755
github.com/microcosm-cc/bluemonday v1.0.21
github.com/pelletier/go-toml/v2 v2.0.6
github.com/rivo/tview v0.0.0-20221221172851-9c04916f4eaa
github.com/rivo/tview v0.0.0-20230104153304-892d1a2eb0da
github.com/rivo/uniseg v0.4.3
github.com/spf13/pflag v1.0.5
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30
golang.org/x/net v0.4.0
gopkg.in/ini.v1 v1.67.0
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157
golang.org/x/net v0.5.0
mvdan.cc/xurls/v2 v2.4.0
)
@ -33,7 +32,7 @@ require (
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/term v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
)

55
go.sum

@ -11,8 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/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.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 h1:jFEK/SA/7E8lg9T33+y8D4Z0I782+bbiEjmyyklRzRQ=
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
@ -25,14 +25,13 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/icza/gox v0.0.0-20221026131554-a08a8cdc726a h1:ctOSka++0Y+9xF7VLtZ8TOJjyXjOGYywzuhbzj3IEHw=
github.com/icza/gox v0.0.0-20221026131554-a08a8cdc726a/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk=
github.com/icza/gox v0.0.0-20230117093757-93f961aa2755 h1:CdxhyIdXDB8Ilp3ogohR8g1omzjKzDsVh5kd71M7Pvc=
github.com/icza/gox v0.0.0-20230117093757-93f961aa2755/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
@ -44,8 +43,8 @@ github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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-20221221172851-9c04916f4eaa h1:f5OVRPQnaO1dvCS5UdaSLsf+3RFG49UlQAUE/Jfstp0=
github.com/rivo/tview v0.0.0-20221221172851-9c04916f4eaa/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ=
github.com/rivo/tview v0.0.0-20230104153304-892d1a2eb0da h1:3Mh+tcC2KqetuHpWMurDeF+yOgyt4w4qtLIpwSQ3uqo=
github.com/rivo/tview v0.0.0-20230104153304-892d1a2eb0da/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ=
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=
@ -64,30 +63,46 @@ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG0
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY=
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20211025201205-69cdffdb9359/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

51
main.go

@ -1,6 +1,8 @@
package main
import (
"strings"
"github.com/RasmusLindroth/tut/auth"
"github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/ui"
@ -8,7 +10,9 @@ import (
"github.com/rivo/tview"
)
const version = "1.0.34"
const version = "2.0.0"
var tutViews []*ui.TutView
func main() {
util.SetTerminalTitle("tut")
@ -17,32 +21,35 @@ func main() {
accs := auth.StartAuth(newUser)
app := tview.NewApplication()
t := &ui.Tut{
App: app,
Config: config.Load(cnfPath, cnfDir),
}
if t.Config.General.MouseSupport {
cnf := config.Load(cnfPath, cnfDir)
if cnf.General.MouseSupport {
app.EnableMouse(true)
}
tview.Styles = tview.Theme{
PrimitiveBackgroundColor: t.Config.Style.Background, // background
ContrastBackgroundColor: t.Config.Style.Text, //background for button, checkbox, form, modal
MoreContrastBackgroundColor: t.Config.Style.Text, //background for dropdown
BorderColor: t.Config.Style.Background, //border
TitleColor: t.Config.Style.Text, //titles
GraphicsColor: t.Config.Style.Text, //borders
PrimaryTextColor: t.Config.Style.StatusBarViewBackground, //backround color selected
SecondaryTextColor: t.Config.Style.Text, //text
TertiaryTextColor: t.Config.Style.Text, //list secondary
InverseTextColor: t.Config.Style.Text, //label activated
ContrastSecondaryTextColor: t.Config.Style.Text, //foreground on input and prefix on dropdown
PrimitiveBackgroundColor: cnf.Style.Background, // background
ContrastBackgroundColor: cnf.Style.Text, //background for button, checkbox, form, modal
MoreContrastBackgroundColor: cnf.Style.Text, //background for dropdown
BorderColor: cnf.Style.Background, //border
TitleColor: cnf.Style.Text, //titles
GraphicsColor: cnf.Style.Text, //borders
PrimaryTextColor: cnf.Style.StatusBarViewBackground, //backround color selected
SecondaryTextColor: cnf.Style.Text, //text
TertiaryTextColor: cnf.Style.Text, //list secondary
InverseTextColor: cnf.Style.Text, //label activated
ContrastSecondaryTextColor: cnf.Style.Text, //foreground on input and prefix on dropdown
}
main := ui.NewTutView(t, accs, selectedUser)
app.SetInputCapture(main.Input)
if t.Config.General.MouseSupport {
app.SetMouseCapture(main.MouseInput)
ui.SetVars(cnf, app, accs)
users := strings.Fields(selectedUser)
if len(users) > 0 {
for _, user := range strings.Fields(selectedUser) {
ui.NewTutView(user)
}
} else {
ui.NewTutView(selectedUser)
}
if err := app.SetRoot(main.View, true).Run(); err != nil {
ui.DoneAdding()
if err := app.Run(); err != nil {
panic(err)
}
}

13
ui/cliview.go

@ -16,14 +16,14 @@ func CliView(version string) (newUser bool, selectedUser string, confPath string
showVersion := pflag.BoolP("version", "v", false, "config path")
nu := pflag.BoolP("new-user", "n", false, "add one more user to tut")
user := pflag.StringP("user", "u", "", "login directly to user named `<name>`")
cnf := pflag.StringP("config", "c", "", "load config.ini from `<path>`")
cnf := pflag.StringP("config", "c", "", "load config.toml from `<path>`")
cnfDir := pflag.StringP("config-dir", "d", "", "load all config from `<path>`")
pflag.Parse()
if len(os.Args) > 1 {
switch os.Args[1] {
case "example-config":
config.CreateDefaultConfig("./config.example.ini")
config.CreateDefaultConfig("./config.example.toml")
os.Exit(0)
}
}
@ -69,19 +69,20 @@ func CliView(version string) (newUser bool, selectedUser string, confPath string
fmt.Print("\tTo run the program you just have to write tut\n\n")
fmt.Print("Commands:\n")
fmt.Print("\texample-config - creates the default configuration file in the current directory and names it ./config.example.ini\n\n")
fmt.Print("\texample-config - creates the default configuration file in the current directory and names it ./config.example.toml\n\n")
fmt.Print("Flags:\n")
fmt.Print("\t-h --help prints this message\n")
fmt.Print("\t-v --version prints the version\n")
fmt.Print("\t-n --new-user add one more user to tut\n")
fmt.Print("\t-c --config <path> load config.ini from <path>\n")
fmt.Print("\t-c --config <path> load config.toml from <path>\n")
fmt.Print("\t-d --config-dir <path> load all config from <path>\n")
fmt.Print("\t-u --user <name> login directly to user named <name>\n")
fmt.Print("\t-u --user <name> login directly to user named <name>.\n")
fmt.Print("\t\tIf you want to login to multiple accounts seperate them with a space and use quotation marks. E.g. -u \"acc_one acc_two\"\n")
fmt.Print("\t\tIf two users are named the same. Use full name like tut@fosstodon.org\n\n")
fmt.Print("Configuration:\n")
fmt.Printf("\tThe config is located in XDG_CONFIG_HOME/tut/config.ini which usually equals to ~/.config/tut/config.ini.\n")
fmt.Printf("\tThe config is located in XDG_CONFIG_HOME/tut/config.toml which usually equals to ~/.config/tut/config.toml.\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")

56
ui/cmdbar.go

@ -19,7 +19,7 @@ func NewCmdBar(tv *TutView) *CmdBar {
View: NewInputField(tv.tut.Config),
}
c.View.SetAutocompleteFunc(c.Autocomplete)
//c.View.SetAutocompletedFunc(c.Autocompleted)
c.View.SetAutocompletedFunc(c.Autocompleted)
c.View.SetDoneFunc(c.DoneFunc)
return c
@ -100,28 +100,40 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
case ":newer":
c.tutView.LoadNewerCommand()
c.Back()
case ":login":
c.tutView.LoginCommand()
c.Back()
case ":next-acct":
c.tutView.NextAcct()
c.Back()
case ":prev-acct":
c.tutView.PrevAcct()
c.Back()
case ":clear-notifications":
c.tutView.ClearNotificationsCommand()
c.Back()
case ":close-window":
c.tutView.CloseWindowCommand()
case ":clear-temp":
c.tutView.ClearTemp()
c.Back()
case ":move-window", ":mv":
case ":close-pane":
c.tutView.ClosePaneCommand()
c.Back()
case ":move-pane", ":mp":
if len(parts) < 2 {
break
}
switch parts[1] {
case "left", "up", "l", "u":
c.tutView.MoveWindowLeft()
c.tutView.MovePaneLeft()
c.Back()
case "right", "down", "r", "d":
c.tutView.MoveWindowRight()
c.tutView.MovePaneRight()
c.Back()
case "home", "h":
c.tutView.MoveWindowHome()
c.tutView.MovePaneHome()
c.Back()
case "end", "e":
c.tutView.MoveWindowEnd()
c.tutView.MovePaneEnd()
c.Back()
}
case ":list-placement":
@ -185,13 +197,13 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
c.tutView.FederatedCommand()
c.Back()
case "special-all", "sa":
c.tutView.SpecialCommand(true, true)
c.tutView.SpecialCommand(false, false)
c.Back()
case "special-boosts", "sb":
c.tutView.SpecialCommand(true, false)
c.tutView.SpecialCommand(false, true)
c.Back()
case "special-replies", "sr":
c.tutView.SpecialCommand(false, true)
c.tutView.SpecialCommand(true, false)
c.Back()
case "direct", "d":
c.tutView.DirectCommand()
@ -222,11 +234,11 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
case ":tags":
c.tutView.TagsCommand()
c.Back()
case ":window":
case ":pane":
if len(parts) < 2 {
break
}
c.tutView.WindowCommand(parts[1])
c.tutView.PaneCommand(parts[1])
c.Back()
case ":user":
if len(parts) < 2 {
@ -237,7 +249,10 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
break
}
c.tutView.Timeline.AddFeed(
NewUserSearchFeed(c.tutView, user),
NewUserSearchFeed(c.tutView, config.NewTimeline(config.Timeline{
FeedType: config.UserList,
Subaction: user,
})), c.tutView.tut.Config.General.CommandsInNewPane,
)
c.Back()
case ":refetch":
@ -279,13 +294,13 @@ 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,:move-window,: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", ",")
words := strings.Split(":blocking,:boosts,:bookmarks,:clear-notifications,:clear-temp,:close-pane,:compose,:favorites,:favorited,:follow-tag,:followers,:following,:help,:h,:history,:move-pane,:next-acct,:lists,:list-placement,:list-split,:login,:muting,:newer,:preferences,:prev-acct,:profile,:proportions,:refetch,:requests,:saved,:stick-to-top,:tag,:timeline,:tl,:unfollow-tag,:user,:pane,: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 mentions,:tl favorited,:tl special-all,:tl special-boosts,:tl-special-replies", ",")
words = strings.Split(":tl home,:tl notifications,:tl local,:tl federated,:tl direct,:tl mentions,: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 mentions,:timeline favorited,:timeline special-all,:timeline special-boosts,:timeline special-replies", ",")
@ -297,8 +312,8 @@ func (c *CmdBar) Autocomplete(curr string) []string {
words = strings.Split(":list-split row,:list-split column", ",")
}
if len(curr) > 11 && curr[:12] == ":move-window" {
words = strings.Split(":move-window left,:move-window right,:move-window up,:move-window down,:move-window home,:move-window end", ",")
if len(curr) > 11 && curr[:12] == ":move-pane" {
words = strings.Split(":move-pane left,:move-pane right,:move-pane up,:move-pane down,:move-pane home,:move-pane end", ",")
}
if len(curr) > 2 && curr[:3] == ":mv" {
words = strings.Split(":mv left,:mv right,:mv up,:mv down,:mv home,:mv end", ",")
@ -315,12 +330,9 @@ func (c *CmdBar) Autocomplete(curr string) []string {
return entries
}
/*
func (c *CmdBar) Autocompleted(text string, index, source int) bool {
if source != tview.AutocompletedNavigate {
c.View.SetText(text)
}
return source == tview.AutocompletedEnter || source == tview.AutocompletedClick
return false
}
*/

271
ui/commands.go

@ -4,13 +4,11 @@ import (
"fmt"
"os"
"strconv"
"strings"
"github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/api"
"github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/util"
"golang.org/x/exp/slices"
)
func (tv *TutView) ComposeCommand() {
@ -35,91 +33,124 @@ func (tv *TutView) EditCommand() {
func (tv *TutView) BlockingCommand() {
tv.Timeline.AddFeed(
NewBlocking(tv),
NewBlocking(tv, config.NewTimeline(config.Timeline{
FeedType: config.Blocking,
})), tv.tut.Config.General.CommandsInNewPane,
)
}
func (tv *TutView) BookmarksCommand() {
tv.Timeline.AddFeed(
NewBookmarksFeed(tv),
)
NewBookmarksFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Saved,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) FavoritedCommand() {
tv.Timeline.AddFeed(
NewFavoritedFeed(tv),
)
NewFavoritedFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Favorited,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) MutingCommand() {
tv.Timeline.AddFeed(
NewMuting(tv),
)
NewMuting(tv, config.NewTimeline(config.Timeline{
FeedType: config.Muting,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) FollowRequestsCommand() {
tv.Timeline.AddFeed(
NewFollowRequests(tv),
)
NewFollowRequests(tv, config.NewTimeline(config.Timeline{
FeedType: config.FollowRequests,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) LocalCommand() {
tv.Timeline.AddFeed(
NewLocalFeed(tv, true, true),
)
NewLocalFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.TimelineLocal,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) FederatedCommand() {
tv.Timeline.AddFeed(
NewFederatedFeed(tv, true, true),
)
NewFederatedFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.TimelineFederated,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) SpecialCommand(boosts, replies bool) {
func (tv *TutView) SpecialCommand(hideBoosts, hideReplies bool) {
tv.Timeline.AddFeed(
NewHomeSpecialFeed(tv, boosts, replies),
)
NewHomeSpecialFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.TimelineHomeSpecial,
HideBoosts: hideBoosts,
HideReplies: hideReplies,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) DirectCommand() {
tv.Timeline.AddFeed(
NewConversationsFeed(tv),
)
NewConversationsFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Conversations,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) HomeCommand() {
tv.Timeline.AddFeed(
NewHomeFeed(tv, true, true),
)
NewHomeFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.TimelineHome,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) NotificationsCommand() {
tv.Timeline.AddFeed(
NewNotificationFeed(tv, true, true),
)
NewNotificationFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Notifications,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) MentionsCommand() {
tv.Timeline.AddFeed(
NewNotificatioMentionsFeed(tv, true, true),
)
NewNotificatioMentionsFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Mentions,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) ListsCommand() {
tv.Timeline.AddFeed(
NewListsFeed(tv),
)
NewListsFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Lists,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) TagCommand(tag string) {
tv.Timeline.AddFeed(
NewTagFeed(tv, tag, true, true),
)
NewTagFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Tag,
Subaction: tag,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) TagsCommand() {
tv.Timeline.AddFeed(
NewTagsFeed(tv),
)
NewTagsFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Tags,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) TagFollowCommand(tag string) {
@ -138,7 +169,7 @@ func (tv *TutView) TagUnfollowCommand(tag string) {
}
}
func (tv *TutView) WindowCommand(index string) {
func (tv *TutView) PaneCommand(index string) {
i, err := strconv.Atoi(index)
if err != nil {
tv.ShowError(
@ -146,119 +177,27 @@ func (tv *TutView) WindowCommand(index string) {
)
return
}
tv.FocusFeed(i)
tv.FocusFeed(i, nil)
}
func (tv *TutView) MoveWindowLeft() {
tv.Timeline.MoveCurrentWindowLeft()
func (tv *TutView) MovePaneLeft() {
tv.Timeline.MoveCurrentPaneLeft()
}
func (tv *TutView) MoveWindowRight() {
tv.Timeline.MoveCurrentWindowRight()
func (tv *TutView) MovePaneRight() {
tv.Timeline.MoveCurrentPaneRight()
}
func (tv *TutView) MoveWindowHome() {
tv.Timeline.MoveCurrentWindowHome()
func (tv *TutView) MovePaneHome() {
tv.Timeline.MoveCurrentPaneHome()
}
func (tv *TutView) MoveWindowEnd() {
tv.Timeline.MoveCurrentWindowEnd()
func (tv *TutView) MovePaneEnd() {
tv.Timeline.MoveCurrentPaneEnd()
}
func (tv *TutView) SwitchCommand(s string) {
ft := config.InvalidFeed
parts := strings.Split(s, ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
cmd := parts[0]
var subaction string
if strings.Contains(parts[0], " ") {
p := strings.Split(cmd, " ")
cmd = p[0]
subaction = strings.Join(p[1:], " ")
}
showBoosts := true
showReplies := true
name := ""
if len(parts) > 1 {
tfStr := []string{"true", "false"}
name = parts[1]
if slices.Contains(tfStr, name) {
name = ""
}
if len(parts) > 2 && slices.Contains(tfStr, parts[len(parts)-2]) &&
slices.Contains(tfStr, parts[len(parts)-1]) {
showBoosts = parts[len(parts)-2] == "true"
showReplies = parts[len(parts)-1] == "true"
} else if len(parts) > 1 && slices.Contains(tfStr, parts[len(parts)-1]) {
showBoosts = parts[len(parts)-1] == "true"
} else if name == "" {
fmt.Printf("switch is invalid . Check this for errors: switch %s\n", s)
os.Exit(1)
}
}
var data string
switch cmd {
case "home":
ft = config.TimelineHome
case "direct":
ft = config.Conversations
case "local":
ft = config.TimelineLocal
case "federated":
ft = config.TimelineFederated
case "special":
ft = config.TimelineHomeSpecial
case "special-all":
ft = config.TimelineHomeSpecial
showBoosts = true
showReplies = true
case "special-boosts":
ft = config.TimelineHomeSpecial
showBoosts = true
showReplies = false
case "special-replies":
ft = config.TimelineHomeSpecial
showBoosts = false
showReplies = true
case "bookmarks", "saved":
ft = config.Saved
case "favorited":
ft = config.Favorited
case "notifications":
ft = config.Notifications
case "lists":
ft = config.Lists
case "tag":
ft = config.Tag
data = subaction
case "blocking":
ft = config.Blocking
case "muting":
ft = config.Muting
case "tags":
ft = config.Tags
case "mentions":
ft = config.Mentions
}
found := tv.Timeline.FindAndGoTo(ft, data, showBoosts, showReplies)
if found {
return
}
nf := CreateFeed(tv, ft, data, showBoosts, showReplies)
tv.Timeline.Feeds = append(tv.Timeline.Feeds, &FeedHolder{
Feeds: []*Feed{nf},
Name: name,
})
tv.FocusFeed(len(tv.Timeline.Feeds) - 1)
tv.Shared.Top.SetText(tv.Timeline.GetTitle())
tv.Timeline.update <- true
}
func (tv *TutView) CloseWindowCommand() {
tv.Timeline.CloseCurrentWindow()
func (tv *TutView) ClosePaneCommand() {
tv.Timeline.CloseCurrentPane()
}
func (tv *TutView) BoostsCommand() {
@ -272,8 +211,12 @@ func (tv *TutView) BoostsCommand() {
s := item.Raw().(*mastodon.Status)
s = util.StatusOrReblog(s)
tv.Timeline.AddFeed(
NewBoosts(tv, s.ID),
)
NewBoosts(tv, s.ID, config.NewTimeline(config.Timeline{
FeedType: config.Boosts,
HideBoosts: false,
HideReplies: true,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) FavoritesCommand() {
@ -287,8 +230,10 @@ func (tv *TutView) FavoritesCommand() {
s := item.Raw().(*mastodon.Status)
s = util.StatusOrReblog(s)
tv.Timeline.AddFeed(
NewFavoritesStatus(tv, s.ID),
)
NewFavoritesStatus(tv, s.ID, config.NewTimeline(config.Timeline{
FeedType: config.Favorites,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) FollowingCommand() {
@ -301,8 +246,10 @@ func (tv *TutView) FollowingCommand() {
}
s := item.Raw().(*api.User)
tv.Timeline.AddFeed(
NewFollowing(tv, s.Data.ID),
)
NewFollowing(tv, s.Data.ID, config.NewTimeline(config.Timeline{
FeedType: config.Following,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) FollowersCommand() {
@ -315,8 +262,10 @@ func (tv *TutView) FollowersCommand() {
}
s := item.Raw().(*api.User)
tv.Timeline.AddFeed(
NewFollowers(tv, s.Data.ID),
)
NewFollowers(tv, s.Data.ID, config.NewTimeline(config.Timeline{
FeedType: config.Followers,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) HistoryCommand() {
@ -328,8 +277,10 @@ func (tv *TutView) HistoryCommand() {
return
}
tv.Timeline.AddFeed(
NewHistoryFeed(tv, item),
)
NewHistoryFeed(tv, item, config.NewTimeline(config.Timeline{
FeedType: config.History,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) ProfileCommand() {
@ -339,8 +290,10 @@ func (tv *TutView) ProfileCommand() {
return
}
tv.Timeline.AddFeed(
NewUserFeed(tv, item),
)
NewUserFeed(tv, item, config.NewTimeline(config.Timeline{
FeedType: config.User,
})),
tv.tut.Config.General.CommandsInNewPane)
}
func (tv *TutView) PreferencesCommand() {
@ -378,6 +331,17 @@ func (tv *TutView) LoadNewerCommand() {
f.LoadNewer(true)
}
func (tv *TutView) LoginCommand() {
NewTutView("")
}
func (tv *TutView) NextAcct() {
TutViews.Next()
}
func (tv *TutView) PrevAcct() {
TutViews.Prev()
}
func (tv *TutView) ClearNotificationsCommand() {
err := tv.tut.Client.ClearNotifications()
if err != nil {
@ -393,6 +357,17 @@ func (tv *TutView) ClearNotificationsCommand() {
}
}
func (tv *TutView) ClearTemp() {
if !tv.tut.Config.Media.DeleteTmpFiles {
for _, t := range TutViews.Views {
for _, f := range t.FileList {
os.Remove(f)
}
t.FileList = []string{}
}
}
}
func (tv *TutView) ToggleStickToTop() {
tv.tut.Config.General.StickToTop = !tv.tut.Config.General.StickToTop
}

216
ui/composeview.go

@ -33,17 +33,19 @@ type msgToot struct {
}
type ComposeView struct {
tutView *TutView
shared *Shared
View *tview.Flex
content *tview.TextView
input *MediaInput
info *tview.TextView
controls *tview.Flex
visibility *tview.DropDown
lang *tview.DropDown
media *MediaList
msg *msgToot
tutView *TutView
shared *Shared
View *tview.Flex
content *tview.TextView
textAreaMain *tview.TextArea
textAreaCW *tview.TextArea
input *MediaInput
info *tview.TextView
controls *tview.Flex
visibility *tview.DropDown
lang *tview.DropDown
media *MediaList
msg *msgToot
}
var visibilities = map[string]int{
@ -61,15 +63,17 @@ var visibilitiesStr = []string{
func NewComposeView(tv *TutView) *ComposeView {
cv := &ComposeView{
tutView: tv,
shared: tv.Shared,
content: NewTextView(tv.tut.Config),
input: NewMediaInput(tv),
controls: NewControlView(tv.tut.Config),
info: NewTextView(tv.tut.Config),
visibility: NewDropDown(tv.tut.Config),
lang: NewDropDown(tv.tut.Config),
media: NewMediaList(tv),
tutView: tv,
shared: tv.Shared,
content: NewTextView(tv.tut.Config),
textAreaMain: NewTextArea(tv.tut.Config),
textAreaCW: NewTextArea(tv.tut.Config),
input: NewMediaInput(tv),
controls: NewControlView(tv.tut.Config),
info: NewTextView(tv.tut.Config),
visibility: NewDropDown(tv.tut.Config),
lang: NewDropDown(tv.tut.Config),
media: NewMediaList(tv),
}
cv.content.SetDynamicColors(true)
cv.View = newComposeUI(cv)
@ -81,18 +85,41 @@ func newComposeUI(cv *ComposeView) *tview.Flex {
if cv.tutView.tut.Config.General.TerminalTitle < 2 {
r.AddItem(cv.tutView.Shared.Top.View, 1, 0, false)
}
r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.content, 0, 2, false), 0, 2, false).
AddItem(tview.NewBox(), 2, 0, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.visibility, 1, 0, false).
AddItem(cv.lang, 1, 0, false).
AddItem(cv.info, 5, 0, false).
AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false).
AddItem(cv.input.View, 1, 0, false).
AddItem(cv.controls, 1, 0, false).
AddItem(cv.tutView.Shared.Bottom.View, 2, 0, false)
if !cv.tutView.tut.Config.General.UseInternalEditor {
r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.content, 0, 2, false), 0, 2, false).
AddItem(tview.NewBox(), 2, 0, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.visibility, 1, 0, false).
AddItem(cv.lang, 1, 0, false).
AddItem(cv.info, 5, 0, false).
AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false).
AddItem(cv.input.View, 1, 0, false).
AddItem(cv.controls, 1, 0, false).
AddItem(cv.tutView.Shared.Bottom.View, 2, 0, false)
} else {
txtOne := NewTextView(cv.tutView.tut.Config)
txtOne.SetText("Content warning text:")
txtTwo := NewTextView(cv.tutView.tut.Config)
txtTwo.SetText("Main content:")
r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.content, 3, 0, false).
AddItem(txtOne, 1, 0, false).
AddItem(cv.textAreaCW, 0, 1, false).
AddItem(txtTwo, 1, 0, false).
AddItem(cv.textAreaMain, 0, 2, false), 0, 2, false).
AddItem(tview.NewBox(), 2, 0, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.visibility, 1, 0, false).
AddItem(cv.lang, 1, 0, false).
AddItem(cv.info, 5, 0, false).
AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false).
AddItem(cv.input.View, 1, 0, false).
AddItem(cv.controls, 1, 0, false).
AddItem(cv.tutView.Shared.Bottom.View, 2, 0, false)
}
return r
}
@ -167,6 +194,8 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) {
func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) error {
cv.tutView.PollView.Reset()
cv.media.Reset()
cv.textAreaMain.SetText("", false)
cv.textAreaCW.SetText("", false)
msg := &msgToot{}
me := cv.tutView.tut.Client.Me
visibility := mastodon.VisibilityPublic
@ -250,6 +279,10 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status)
cv.lang.SetOptions(langStrs, cv.langSelected)
cv.lang.SetCurrentOption(index)
if cv.tutView.tut.Config.General.UseInternalEditor {
cv.textAreaMain.SetText(cv.msg.Text, true)
cv.textAreaCW.SetText(cv.msg.CWText, true)
}
cv.UpdateContent()
cv.SetControls(ComposeNormal)
return nil
@ -275,27 +308,35 @@ func (cv *ComposeView) getAccs() string {
}
func (cv *ComposeView) EditText() {
text, err := OpenEditor(cv.tutView, cv.msg.Text)
if err != nil {
cv.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err),
)
return
if !cv.tutView.tut.Config.General.UseInternalEditor {
text, err := OpenEditor(cv.tutView, cv.msg.Text)
if err != nil {
cv.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err),
)
return
}
cv.msg.Text = text
cv.UpdateContent()
} else {
cv.textAreaMainFocus()
}
cv.msg.Text = text
cv.UpdateContent()
}
func (cv *ComposeView) EditSpoiler() {
text, err := OpenEditor(cv.tutView, cv.msg.CWText)
if err != nil {
cv.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err),
)
return
if !cv.tutView.tut.Config.General.UseInternalEditor {
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.CWText = text
cv.UpdateContent()
} else {
cv.textAreaCWFocus()
}
cv.msg.CWText = text
cv.UpdateContent()
}
func (cv *ComposeView) ToggleCW() {
@ -315,26 +356,29 @@ func (cv *ComposeView) UpdateContent() {
if cv.msg.Reply != nil {
var acct string
if cv.msg.Reply.Account.DisplayName != "" {
acct = fmt.Sprintf("%s (%s)\n", cv.msg.Reply.Account.DisplayName, cv.msg.Reply.Account.Acct)
acct = fmt.Sprintf("%s (%s)", cv.msg.Reply.Account.DisplayName, cv.msg.Reply.Account.Acct)
} else {
acct = fmt.Sprintf("%s\n", cv.msg.Reply.Account.Acct)
acct = cv.msg.Reply.Account.Acct
}
outputHead += subtleColor + "Replying to " + tview.Escape(acct) + "\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.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.CWText != "" {
outputHead += subtleColor + "Content warning\n\n" + normal
outputHead += tview.Escape(cv.msg.CWText)
outputHead += "\n\n" + subtleColor + "---hidden content below---\n\n" + normal
if !cv.tutView.tut.Config.General.UseInternalEditor {
if cv.msg.Sensitive && cv.msg.CWText != "" {
outputHead += subtleColor + "Content warning\n\n" + normal
outputHead += tview.Escape(cv.msg.CWText)
outputHead += "\n\n" + subtleColor + "---hidden content below---\n\n" + normal
}
output = outputHead + normal + tview.Escape(cv.msg.Text)
} else {
output = strings.TrimSpace(outputHead)
}
output = outputHead + normal + tview.Escape(cv.msg.Text)
cv.content.SetText(output)
}
@ -354,12 +398,51 @@ func (cv *ComposeView) IncludeQuote() {
for _, line := range strings.Split(tootText, "\n") {
t += "> " + line + "\n"
}
t += "\n"
cv.msg.Text = t
if cv.tutView.tut.Config.General.UseInternalEditor {
cv.textAreaMain.SetText(cv.msg.Text, false)
}
cv.msg.QuoteIncluded = true
cv.UpdateContent()
}
func (cv *ComposeView) textAreaInput(event *tcell.EventKey) *tcell.EventKey {
if cv.tutView.tut.Config.Input.GlobalBack.Match(event.Key(), rune(-1)) {
cv.exitTextAreaInput()
return nil
}
switch key := event.Key(); key {
case tcell.KeyCtrlQ:
return nil
case tcell.KeyCtrlC:
return tcell.NewEventKey(tcell.KeyCtrlQ, rune(0), tcell.ModNone)
}
return event
}
func (cv *ComposeView) exitTextAreaInput() {
cv.tutView.tut.App.SetInputCapture(cv.tutView.Input)
cv.tutView.tut.App.SetFocus(cv.content)
}
func (cv *ComposeView) textAreaMainFocus() {
cv.tutView.tut.App.SetInputCapture(cv.textAreaInput)
cv.textAreaMain.SetChangedFunc(func() {
cv.msg.Text = cv.textAreaMain.GetText()
cv.UpdateContent()
})
cv.tutView.tut.App.SetFocus(cv.textAreaMain)
}
func (cv *ComposeView) textAreaCWFocus() {
cv.tutView.tut.App.SetInputCapture(cv.textAreaInput)
cv.textAreaCW.SetChangedFunc(func() {
cv.msg.CWText = cv.textAreaCW.GetText()
cv.UpdateContent()
})
cv.tutView.tut.App.SetFocus(cv.textAreaCW)
}
func (cv *ComposeView) HasMedia() bool {
return len(cv.media.Files) > 0
}
@ -650,14 +733,29 @@ func (m *MediaList) EditDesc() {
)
return
}
desc, err := OpenEditor(m.tutView, file.Description)
var desc string
var err error
if m.tutView.tut.Config.General.UseInternalEditor {
m.tutView.EditorView.Init(file.Description, 0, true, func(input string) {
m.editDesc(input, nil)
})
return
} else {
desc, err = OpenEditor(m.tutView, file.Description)
m.editDesc(desc, err)
}
}
func (m *MediaList) editDesc(text string, err error) {
index := m.list.GetCurrentItem()
file := m.Files[index]
if err != nil {
m.tutView.ShowError(
fmt.Sprintf("Couldn't edit description. Error: %v\n", err),
)
return
}
file.Description = desc
file.Description = text
m.Files[index] = file
m.Draw()
}

94
ui/editorview.go

@ -0,0 +1,94 @@
package ui
import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/rivo/uniseg"
)
type EditorView struct {
tutView *TutView
shared *Shared
View *tview.Flex
editor *tview.TextArea
controls *tview.Flex
info *tview.TextView
limit int
prevPage string
prevFocus tview.Primitive
prevInput func(event *tcell.EventKey) *tcell.EventKey
exitFunc func(string)
}
func NewEditorView(tv *TutView) *EditorView {
e := &EditorView{
tutView: tv,
shared: tv.Shared,
editor: NewTextArea(tv.tut.Config),
controls: NewControlView(tv.tut.Config),
info: NewTextView(tv.tut.Config),
}
item := NewControl(tv.tut.Config, tv.tut.Config.Input.EditorExit, true)
e.controls.AddItem(
NewControlButton(tv, item), item.Len, 0, false)
e.View = editorViewUI(e)
return e
}
func editorViewUI(e *EditorView) *tview.Flex {
r := tview.NewFlex().SetDirection(tview.FlexRow)
if e.tutView.tut.Config.General.TerminalTitle < 2 {
r.AddItem(e.shared.Top.View, 1, 0, false)
}
if e.limit > 0 {
r.AddItem(e.info, 1, 0, false).
AddItem(e.editor, 0, 1, false).
AddItem(e.controls, 1, 0, false).
AddItem(e.shared.Bottom.View, 2, 0, false)
} else {
r.AddItem(e.editor, 0, 1, false).
AddItem(e.controls, 1, 0, false).
AddItem(e.shared.Bottom.View, 2, 0, false)
}
return r
}
func (e *EditorView) Init(text string, textLimit int, setReturn bool, exit func(string)) {
e.editor.SetText(text, true)
e.limit = textLimit
*e.View = *editorViewUI(e)
e.exitFunc = exit
if setReturn {
e.prevPage, _ = e.tutView.View.GetFrontPage()
e.prevFocus = e.tutView.tut.App.GetFocus()
e.prevInput = e.tutView.tut.App.GetInputCapture()
}
e.info.SetText("")
e.editor.SetChangedFunc(e.updateInfo)
e.updateInfo()
e.tutView.View.HidePage(e.prevPage)
e.tutView.View.ShowPage("editor")
e.tutView.tut.App.SetInputCapture(e.tutView.InputEditorView)
e.tutView.tut.App.SetFocus(e.editor)
}
func (e *EditorView) updateInfo() {
if e.limit > 0 {
content := e.editor.GetText()
charCount := uniseg.GraphemeClusterCount(content)
charsLeft := e.limit - charCount
e.info.SetText(
fmt.Sprintf("Chars left: %d", charsLeft),
)
}
}
func (e *EditorView) ExitTextAreaInput() {
e.tutView.View.HidePage("editor")
e.tutView.View.ShowPage(e.prevPage)
e.tutView.tut.App.SetInputCapture(e.prevInput)
e.tutView.tut.App.SetFocus(e.prevFocus)
e.exitFunc(e.editor.GetText())
}

365
ui/feed.go

@ -45,12 +45,11 @@ func outFocus(l *tview.List, style config.Style) {
}
type Feed struct {
tutView *TutView
Data *feed.Feed
List *FeedList
Content *FeedContent
ShowBoosts bool
ShowReplies bool
tutView *TutView
Data *feed.Feed
List *FeedList
Content *FeedContent
Timeline *config.Timeline
}
func (f *Feed) ListInFocus() {
@ -141,113 +140,106 @@ func (f *Feed) update() {
}
}
func NewHomeFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
f := feed.NewTimelineHome(tv.tut.Client, tv.tut.Config, showBoosts, showReplies)
func NewHomeFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineHome(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewHomeSpecialFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, showBoosts, showReplies)
func NewHomeSpecialFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
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)
func NewFederatedFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineFederated(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewLocalFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
f := feed.NewTimelineLocal(tv.tut.Client, tv.tut.Config, showBoosts, showReplies)
func NewLocalFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineLocal(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewNotificationFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
f := feed.NewNotifications(tv.tut.Client, tv.tut.Config, showBoosts, showReplies)
func NewNotificationFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewNotifications(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewNotificatioMentionsFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed {
func NewNotificatioMentionsFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewNotificationsMentions(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewThreadFeed(tv *TutView, item api.Item) *Feed {
func NewThreadFeed(tv *TutView, item api.Item, tl *config.Timeline) *Feed {
status := util.StatusOrReblog(item.Raw().(*mastodon.Status))
f := feed.NewThread(tv.tut.Client, tv.tut.Config, status)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
for i, s := range f.List() {
main, symbol := DrawListItem(tv.tut.Config, s)
@ -261,17 +253,16 @@ func NewThreadFeed(tv *TutView, item api.Item) *Feed {
return fd
}
func NewHistoryFeed(tv *TutView, item api.Item) *Feed {
func NewHistoryFeed(tv *TutView, item api.Item, tl *config.Timeline) *Feed {
status := util.StatusOrReblog(item.Raw().(*mastodon.Status))
f := feed.NewHistory(tv.tut.Client, tv.tut.Config, status)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
for _, s := range f.List() {
main, symbol := DrawListItem(tv.tut.Config, s)
@ -283,23 +274,22 @@ func NewHistoryFeed(tv *TutView, item api.Item) *Feed {
return fd
}
func NewConversationsFeed(tv *TutView) *Feed {
func NewConversationsFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewConversations(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewUserFeed(tv *TutView, item api.Item) *Feed {
func NewUserFeed(tv *TutView, item api.Item, tl *config.Timeline) *Feed {
if item.Type() != api.UserType && item.Type() != api.ProfileType {
panic("Can't open user. Wrong type.\n")
}
@ -307,28 +297,26 @@ func NewUserFeed(tv *TutView, item api.Item) *Feed {
f := feed.NewUserProfile(tv.tut.Client, tv.tut.Config, u)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewUserSearchFeed(tv *TutView, search string) *Feed {
f := feed.NewUserSearch(tv.tut.Client, tv.tut.Config, search)
func NewUserSearchFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewUserSearch(tv.tut.Client, tv.tut.Config, tl.Subaction)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
for _, s := range f.List() {
main, symbol := DrawListItem(tv.tut.Config, s)
@ -339,240 +327,225 @@ func NewUserSearchFeed(tv *TutView, search string) *Feed {
return fd
}
func NewTagFeed(tv *TutView, search string, showBoosts bool, showReplies bool) *Feed {
f := feed.NewTag(tv.tut.Client, tv.tut.Config, search, showBoosts, showReplies)
func NewTagFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTag(tv.tut.Client, tv.tut.Config, tl.Subaction, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewTagsFeed(tv *TutView) *Feed {
func NewTagsFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTags(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewListsFeed(tv *TutView) *Feed {
func NewListsFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewListList(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewListFeed(tv *TutView, l *mastodon.List, showBoosts bool, showReplies bool) *Feed {
f := feed.NewList(tv.tut.Client, tv.tut.Config, l, showBoosts, showReplies)
func NewListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed {
f := feed.NewList(tv.tut.Client, tv.tut.Config, l, tl.HideBoosts, tl.HideReplies)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts,
ShowReplies: showReplies,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewUsersInListFeed(tv *TutView, l *mastodon.List) *Feed {
func NewUsersInListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed {
f := feed.NewUsersInList(tv.tut.Client, tv.tut.Config, l)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewUsersAddListFeed(tv *TutView, l *mastodon.List) *Feed {
func NewUsersAddListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed {
f := feed.NewUsersAddList(tv.tut.Client, tv.tut.Config, l)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewFavoritedFeed(tv *TutView) *Feed {
func NewFavoritedFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewFavorites(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewBookmarksFeed(tv *TutView) *Feed {
func NewBookmarksFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewBookmarks(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewFavoritesStatus(tv *TutView, id mastodon.ID) *Feed {
func NewFavoritesStatus(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed {
f := feed.NewFavoritesStatus(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewBoosts(tv *TutView, id mastodon.ID) *Feed {
func NewBoosts(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed {
f := feed.NewBoosts(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewFollowers(tv *TutView, id mastodon.ID) *Feed {
func NewFollowers(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed {
f := feed.NewFollowers(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewFollowing(tv *TutView, id mastodon.ID) *Feed {
func NewFollowing(tv *TutView, id mastodon.ID, tl *config.Timeline) *Feed {
f := feed.NewFollowing(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewBlocking(tv *TutView) *Feed {
func NewBlocking(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewBlocking(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewMuting(tv *TutView) *Feed {
func NewMuting(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewMuting(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()
return fd
}
func NewFollowRequests(tv *TutView) *Feed {
func NewFollowRequests(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewFollowRequests(tv.tut.Client, tv.tut.Config)
f.LoadNewer()
fd := &Feed{
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
ShowBoosts: true,
ShowReplies: true,
tutView: tv,
Data: f,
List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut),
Timeline: tl,
}
go fd.update()

206
ui/input.go

@ -2,7 +2,6 @@ package ui
import (
"fmt"
"strconv"
"strings"
"sync"
"time"
@ -30,6 +29,16 @@ func (tv *TutView) Input(event *tcell.EventKey) *tcell.EventKey {
return nil
}
}
if tv.tut.Config.Input.MainNextAccount.Match(event.Key(), event.Rune()) {
tv.NextAcct()
return nil
}
if tv.tut.Config.Input.MainPrevAccount.Match(event.Key(), event.Rune()) {
tv.NextAcct()
return nil
}
switch tv.PageFocus {
case LoginFocus:
return tv.InputLoginView(event)
@ -55,6 +64,8 @@ func (tv *TutView) Input(event *tcell.EventKey) *tcell.EventKey {
return tv.InputHelp(event)
case PreferenceFocus:
return tv.InputPreference(event)
case EditorFocus:
return tv.InputEditorView(event)
default:
return event
}
@ -95,6 +106,33 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
action := config.LeaderNone
var subaction string
content := tv.Leader.Content()
foundFeed := false
for i, fh := range tv.Timeline.Feeds {
for _, f := range fh.Feeds {
if f.Timeline.Shortcut == "" || f.Timeline.Shortcut != content {
continue
}
switch f.Timeline.OnFocus {
case config.TimelineFocusPane:
tv.FocusFeed(i, nil)
case config.TimelineFocusTimeline:
tv.FocusFeed(i, f.Timeline)
}
foundFeed = true
tv.Leader.ResetInactive()
return nil
}
}
if !foundFeed {
for _, tl := range tv.tut.Config.General.Timelines {
if tl.Shortcut == "" || tl.Shortcut != content {
continue
}
tv.matchedTimeline(tl)
tv.Leader.ResetInactive()
return nil
}
}
for _, la := range tv.tut.Config.General.LeaderActions {
if la.Shortcut == content {
action = la.Command
@ -106,20 +144,6 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
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.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:
@ -128,8 +152,6 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
tv.EditCommand()
case config.LeaderBlocking:
tv.BlockingCommand()
case config.LeaderBookmarks, config.LeaderSaved:
tv.BookmarksCommand()
case config.LeaderFavorited:
tv.FavoritedCommand()
case config.LeaderHistory:
@ -148,36 +170,26 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
tv.PreferencesCommand()
case config.LeaderProfile:
tv.ProfileCommand()
case config.LeaderNotifications:
tv.NotificationsCommand()
case config.LeaderMentions:
tv.MentionsCommand()
case config.LeaderLoadNewer:
tv.LoadNewerCommand()
case config.LeaderLists:
tv.ListsCommand()
case config.LeaderStickToTop:
tv.ToggleStickToTop()
case config.LeaderRefetch:
tv.RefetchCommand()
case config.LeaderTag:
tv.TagCommand(subaction)
case config.LeaderTags:
tv.TagsCommand()
case config.LeaderWindow:
tv.WindowCommand(subaction)
case config.LeaderCloseWindow:
tv.CloseWindowCommand()
case config.LeaderMoveWindowLeft:
tv.MoveWindowLeft()
case config.LeaderMoveWindowRight:
tv.MoveWindowRight()
case config.LeaderMoveWindowHome:
tv.MoveWindowHome()
case config.LeaderMoveWindowEnd:
tv.MoveWindowEnd()
case config.LeaderSwitch:
tv.SwitchCommand(subaction)
case config.LeaderPane:
tv.PaneCommand(subaction)
case config.LeaderClosePane:
tv.ClosePaneCommand()
case config.LeaderMovePaneLeft:
tv.MovePaneLeft()
case config.LeaderMovePaneRight:
tv.MovePaneRight()
case config.LeaderMovePaneHome:
tv.MovePaneHome()
case config.LeaderMovePaneEnd:
tv.MovePaneEnd()
case config.LeaderListPlacement:
switch subaction {
case "top":
@ -219,6 +231,16 @@ func (tv *TutView) InputMainView(event *tcell.EventKey) *tcell.EventKey {
}
}
func (tv *TutView) matchedTimeline(tl *config.Timeline) {
nf := CreateFeed(tv, tl)
switch tl.OnCreationClosed {
case config.TimelineCreationClosedCurrentPane:
tv.Timeline.AddFeed(nf, false)
case config.TimelineCreationClosedNewPane:
tv.Timeline.AddFeed(nf, true)
}
}
func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey {
if tv.tut.Config.Input.MainHome.Match(event.Key(), event.Rune()) {
tv.Timeline.HomeItemFeed()
@ -244,16 +266,12 @@ func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey {
tv.Timeline.PrevItemFeed()
return nil
}
if tv.tut.Config.Input.MainPrevWindow.Match(event.Key(), event.Rune()) {
if tv.tut.Config.General.NotificationFeed {
tv.PrevFeed()
}
if tv.tut.Config.Input.MainPrevPane.Match(event.Key(), event.Rune()) {
tv.PrevFeed()
return nil
}
if tv.tut.Config.Input.MainNextWindow.Match(event.Key(), event.Rune()) {
if tv.tut.Config.General.NotificationFeed {
tv.NextFeed()
}
if tv.tut.Config.Input.MainNextPane.Match(event.Key(), event.Rune()) {
tv.NextFeed()
return nil
}
if tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) {
@ -265,19 +283,39 @@ func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey {
})
return nil
} else if exiting && tv.Timeline.FeedFocusIndex != 0 {
tv.FocusFeed(0)
tv.FocusFeed(0, nil)
}
return nil
}
for i, tl := range tv.tut.Config.General.Timelines {
if tl.Key.Match(event.Key(), event.Rune()) {
tv.FocusFeed(i)
foundFeed := false
for i, fh := range tv.Timeline.Feeds {
for _, f := range fh.Feeds {
if f.Timeline.Key.Match(event.Key(), event.Rune()) {
switch f.Timeline.OnFocus {
case config.TimelineFocusPane:
tv.FocusFeed(i, nil)
case config.TimelineFocusTimeline:
tv.FocusFeed(i, f.Timeline)
}
foundFeed = true
return nil
}
}
}
if !foundFeed {
for _, tl := range tv.tut.Config.General.Timelines {
if tl.Key.Match(event.Key(), event.Rune()) {
tv.matchedTimeline(tl)
return nil
}
}
}
if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) {
exiting := tv.Timeline.RemoveCurrent(false)
if exiting && tv.Timeline.FeedFocusIndex != 0 {
tv.FocusFeed(0)
tv.FocusFeed(0, nil)
}
return nil
}
@ -511,7 +549,9 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas
return nil
}
if tv.tut.Config.Input.StatusThread.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewThreadFeed(tv, item))
tv.Timeline.AddFeed(NewThreadFeed(tv, item, config.NewTimeline(config.Timeline{
FeedType: config.Thread,
})), false)
return nil
}
if tv.tut.Config.Input.StatusUser.Match(event.Key(), event.Rune()) {
@ -523,7 +563,9 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas
if err != nil {
return nil
}
tv.Timeline.AddFeed(NewUserFeed(tv, user))
tv.Timeline.AddFeed(NewUserFeed(tv, user, config.NewTimeline(config.Timeline{
FeedType: config.User,
})), false)
return nil
}
if tv.tut.Config.Input.StatusViewFocus.Match(event.Key(), event.Rune()) {
@ -751,7 +793,9 @@ func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, ut InputUser
return nil
}
if tv.tut.Config.Input.UserUser.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewUserFeed(tv, api.NewUserItem(user, true)))
tv.Timeline.AddFeed(NewUserFeed(tv, api.NewUserItem(user, true), config.NewTimeline(config.Timeline{
FeedType: config.User,
})), false)
return nil
}
if tv.tut.Config.Input.UserViewFocus.Match(event.Key(), event.Rune()) {
@ -763,7 +807,9 @@ func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, ut InputUser
return nil
}
if tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewUserFeed(tv, api.NewUserItem(user, true)))
tv.Timeline.AddFeed(NewUserFeed(tv, api.NewUserItem(user, true), config.NewTimeline(config.Timeline{
FeedType: config.User,
})), false)
return nil
}
return event
@ -772,15 +818,21 @@ func (tv *TutView) InputUser(event *tcell.EventKey, user *api.User, ut InputUser
func (tv *TutView) InputList(event *tcell.EventKey, list *mastodon.List) *tcell.EventKey {
if tv.tut.Config.Input.ListOpenFeed.Match(event.Key(), event.Rune()) ||
tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewListFeed(tv, list, true, true))
tv.Timeline.AddFeed(NewListFeed(tv, list, config.NewTimeline(config.Timeline{
FeedType: config.List,
})), false)
return nil
}
if tv.tut.Config.Input.ListUserList.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewUsersInListFeed(tv, list))
tv.Timeline.AddFeed(NewUsersInListFeed(tv, list, config.NewTimeline(config.Timeline{
FeedType: config.ListUsersIn,
})), false)
return nil
}
if tv.tut.Config.Input.ListUserAdd.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewUsersAddListFeed(tv, list))
tv.Timeline.AddFeed(NewUsersAddListFeed(tv, list, config.NewTimeline(config.Timeline{
FeedType: config.ListUsersAdd,
})), false)
return nil
}
return event
@ -789,7 +841,10 @@ func (tv *TutView) InputList(event *tcell.EventKey, list *mastodon.List) *tcell.
func (tv *TutView) InputTag(event *tcell.EventKey, tag *mastodon.Tag) *tcell.EventKey {
if tv.tut.Config.Input.TagOpenFeed.Match(event.Key(), event.Rune()) ||
tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) {
tv.Timeline.AddFeed(NewTagFeed(tv, tag.Name, true, true))
tv.Timeline.AddFeed(NewTagFeed(tv, config.NewTimeline(config.Timeline{
FeedType: config.Tag,
Subaction: tag.Name,
})), false)
return nil
}
if tv.tut.Config.Input.TagFollow.Match(event.Key(), event.Rune()) {
@ -837,12 +892,9 @@ func (tv *TutView) InputLinkView(event *tcell.EventKey) *tcell.EventKey {
tv.SetPage(MainFocus)
return nil
}
if event.Key() == tcell.KeyRune {
switch event.Rune() {
case '1', '2', '3', '4', '5':
s := string(event.Rune())
i, _ := strconv.Atoi(s)
tv.LinkView.OpenCustom(i)
for _, oc := range tv.tut.Config.OpenCustom.OpenCustoms {
if oc.Key.Match(event.Key(), event.Rune()) {
tv.LinkView.OpenCustom(oc)
return nil
}
}
@ -1099,6 +1151,20 @@ func (tv *TutView) InputCmdView(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (tv *TutView) InputEditorView(event *tcell.EventKey) *tcell.EventKey {
if tv.tut.Config.Input.GlobalBack.Match(event.Key(), rune(-1)) {
tv.EditorView.ExitTextAreaInput()
return nil
}
switch key := event.Key(); key {
case tcell.KeyCtrlQ:
return nil
case tcell.KeyCtrlC:
return tcell.NewEventKey(tcell.KeyCtrlQ, rune(0), tcell.ModNone)
}
return event
}
func (tv *TutView) MouseInput(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) {
if event == nil {
return nil, action
@ -1108,6 +1174,12 @@ func (tv *TutView) MouseInput(event *tcell.EventMouse, action tview.MouseAction)
return event, action
}
/* Switch accounts */
x, y := event.Position()
if tv.MainView.accView.InRect(x, y) {
return event, action
}
switch tv.PageFocus {
case ViewFocus, MainFocus:
return tv.MouseInputMainView(event, action)
@ -1136,7 +1208,7 @@ func (tv *TutView) MouseInput(event *tcell.EventMouse, action tview.MouseAction)
func (tv *TutView) feedListMouse(list *tview.List, i int, event *tcell.EventMouse, action tview.MouseAction) {
tv.SetPage(MainFocus)
tv.FocusFeed(i)
tv.FocusFeed(i, nil)
mh := list.MouseHandler()
if mh == nil {
return

13
ui/item_status.go

@ -36,6 +36,7 @@ type Toot struct {
Boosts int
Favorites int
Edited bool
Lang string
Controls string
}
@ -104,8 +105,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
status = status.Reblog
}
strippedContent, _ = util.CleanHTML(status.Content)
strippedContent = tview.Escape(strippedContent)
strippedContent, _ = util.CleanHTMLStyled(status.Content)
width := 0
if main != nil {
@ -121,6 +121,12 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
ShowSpoiler: showSensitive,
CWlabel: cwToggle.Label,
}
for _, lang := range util.Languages {
if status.Language == lang.Code {
toot.Lang = lang.English
break
}
}
toot.AccountDisplayName = tview.Escape(status.Account.DisplayName)
toot.Account = tview.Escape(status.Account.Acct)
@ -162,8 +168,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
}
if status.Sensitive {
strippedSpoiler, _ = util.CleanHTML(status.SpoilerText)
strippedSpoiler = tview.Escape(strippedSpoiler)
strippedSpoiler, _ = util.CleanHTMLStyled(status.SpoilerText)
}
toot.CWText = strippedSpoiler

28
ui/linkview.go

@ -37,10 +37,7 @@ func linkViewUI(lv *LinkView) *tview.Flex {
NewControl(lv.tutView.tut.Config, lv.tutView.tut.Config.Input.LinkYank, true),
}
for _, cust := range lv.tutView.tut.Config.OpenCustom.OpenCustoms {
key := config.Key{
Hint: [][]string{{"", fmt.Sprintf("%d", cust.Index), cust.Name}},
}
items = append(items, NewControl(lv.tutView.tut.Config, key, true))
items = append(items, NewControl(lv.tutView.tut.Config, cust.Key, true))
}
lv.controls.Clear()
for i, item := range items {
@ -114,16 +111,20 @@ func (lv *LinkView) Open() {
return
}
lv.tutView.Timeline.AddFeed(
NewUserFeed(lv.tutView, u),
)
NewUserFeed(lv.tutView, u, config.NewTimeline(config.Timeline{
FeedType: config.User,
}),
), false)
lv.tutView.FocusMainNoHistory()
return
}
tIndex := index - len(mentions) - len(urls)
if tIndex < len(tags) {
lv.tutView.Timeline.AddFeed(
NewTagFeed(lv.tutView, tags[tIndex].Name, true, true),
)
NewTagFeed(lv.tutView, config.NewTimeline(config.Timeline{
FeedType: config.Tag,
Subaction: tags[tIndex].Name,
})), false)
lv.tutView.FocusMainNoHistory()
return
}
@ -162,17 +163,10 @@ func (lv *LinkView) Yank() {
copyToClipboard(url)
}
func (lv *LinkView) OpenCustom(index int) {
func (lv *LinkView) OpenCustom(c config.Custom) {
url := lv.getURL()
if url == "" {
return
}
customs := lv.tutView.tut.Config.OpenCustom.OpenCustoms
for _, c := range customs {
if c.Index != index {
continue
}
openCustom(lv.tutView, c.Program, c.Args, c.Terminal, url)
return
}
openCustom(lv.tutView, c.Program, c.Args, c.Terminal, url)
}

97
ui/mainview.go

@ -1,24 +1,28 @@
package ui
import (
"fmt"
"github.com/RasmusLindroth/tut/config"
"github.com/rivo/tview"
)
type MainView struct {
View *tview.Flex
update chan bool
View *tview.Flex
accView *tview.Flex
update chan bool
}
func NewMainView(tv *TutView, update chan bool) *MainView {
mv := &MainView{
View: mainViewUI(tv),
update: update,
update: update,
accView: NewControlView(tv.tut.Config),
}
mv.View = mv.mainViewUI(tv)
go func() {
for range mv.update {
tv.tut.App.QueueUpdateDraw(func() {
*tv.MainView.View = *mainViewUI(tv)
*tv.MainView.View = *mv.mainViewUI(tv)
tv.ShouldSync()
})
}
@ -40,85 +44,102 @@ func feedList(mv *TutView, fh *FeedHolder) *tview.Flex {
AddItem(fh.GetFeedList().Symbol, iw, 0, false)
}
func mainViewUI(mv *TutView) *tview.Flex {
vl := NewVerticalLine(mv.tut.Config)
hl := NewHorizontalLine(mv.tut.Config)
lp := mv.tut.Config.General.ListProportion
cp := mv.tut.Config.General.ContentProportion
func (mv *MainView) mainViewUI(tv *TutView) *tview.Flex {
vl := NewVerticalLine(tv.tut.Config)
hl := NewHorizontalLine(tv.tut.Config)
lp := tv.tut.Config.General.ListProportion
cp := tv.tut.Config.General.ContentProportion
var list *tview.Flex
if mv.tut.Config.General.ListSplit == config.ListColumn {
if tv.tut.Config.General.ListSplit == config.ListColumn {
list = tview.NewFlex().SetDirection(tview.FlexColumn)
} else {
list = tview.NewFlex().SetDirection(tview.FlexRow)
}
if mv.tut.Config.General.ListSplit == config.ListColumn {
if tv.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.SetBackgroundColor(mv.tut.Config.Style.TimelineNameBackground)
txt.SetTextColor(mv.tut.Config.Style.TimelineNameText)
for _, fh := range tv.Timeline.Feeds {
fTitle := fh.GetTitle()
if len(fTitle) > 0 {
txt := NewTextView(tv.tut.Config)
txt.SetText(tview.Escape(fTitle))
txt.SetBackgroundColor(tv.tut.Config.Style.TimelineNameBackground)
txt.SetTextColor(tv.tut.Config.Style.TimelineNameText)
feeds.AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(txt, 1, 0, false).
AddItem(feedList(mv, fh), 0, 1, false), 0, 1, false)
AddItem(feedList(tv, fh), 0, 1, false), 0, 1, false)
} else {
feeds.AddItem(feedList(mv, fh), 0, 1, false)
feeds.AddItem(feedList(tv, fh), 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.SetBackgroundColor(mv.tut.Config.Style.TimelineNameBackground)
txt.SetTextColor(mv.tut.Config.Style.TimelineNameText)
for _, fh := range tv.Timeline.Feeds {
fTitle := fh.GetTitle()
if len(fTitle) > 0 {
txt := NewTextView(tv.tut.Config)
txt.SetText(tview.Escape(fTitle))
txt.SetBackgroundColor(tv.tut.Config.Style.TimelineNameBackground)
txt.SetTextColor(tv.tut.Config.Style.TimelineNameText)
feeds.AddItem(txt, 1, 0, false)
}
feeds.AddItem(feedList(mv, fh), 0, 1, false)
feeds.AddItem(feedList(tv, fh), 0, 1, false)
}
list.AddItem(feeds, 0, 1, false)
}
fc := mv.Timeline.GetFeedContent()
fc := tv.Timeline.GetFeedContent()
content := fc.Main
controls := fc.Controls
mv.accView.Clear()
for i, t := range TutViews.Views {
acct := t.tut.Client.Me.Acct
acct = fmt.Sprintf("%s ", acct)
if i > 0 {
acct = fmt.Sprintf(" %s", acct)
}
item := NewAccButton(tv, tv.tut.Config, acct, i, i == TutViews.Current)
mv.accView.AddItem(item, len(acct), 0, false)
}
r := tview.NewFlex().SetDirection(tview.FlexRow)
if mv.tut.Config.General.TerminalTitle < 2 {
r.AddItem(mv.Shared.Top.View, 1, 0, false)
if tv.tut.Config.General.TerminalTitle < 2 {
r.AddItem(tv.Shared.Top.View, 1, 0, false)
}
if mv.tut.Config.General.ListPlacement == config.ListPlacementTop {
if tv.tut.Config.General.ListPlacement == config.ListPlacementTop {
r.AddItem(list, 0, lp, false).
AddItem(hl, 1, 0, false).
AddItem(content, 0, cp, false).
AddItem(controls, 1, 0, false).
AddItem(mv.Shared.Bottom.View, 2, 0, false)
} else if mv.tut.Config.General.ListPlacement == config.ListPlacementBottom {
AddItem(tv.Shared.Bottom.View, 2, 0, false)
} else if tv.tut.Config.General.ListPlacement == config.ListPlacementBottom {
r.AddItem(content, 0, cp, false).
AddItem(controls, 1, 0, false).
AddItem(hl, 1, 0, false).
AddItem(list, 0, lp, false).
AddItem(mv.Shared.Bottom.View, 2, 0, false)
} else if mv.tut.Config.General.ListPlacement == config.ListPlacementLeft {
AddItem(tv.Shared.Bottom.View, 2, 0, false)
} else if tv.tut.Config.General.ListPlacement == config.ListPlacementLeft {
r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(list, 0, lp, false).
AddItem(vl, 1, 0, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(content, 0, 1, false).
AddItem(controls, 1, 0, false), 0, cp, false), 0, 1, false).
AddItem(mv.Shared.Bottom.View, 2, 0, false)
} else if mv.tut.Config.General.ListPlacement == config.ListPlacementRight {
AddItem(tv.Shared.Bottom.View, 2, 0, false)
} else if tv.tut.Config.General.ListPlacement == config.ListPlacementRight {
r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(content, 0, 1, false).
AddItem(controls, 1, 0, false), 0, cp, false).
AddItem(vl, 1, 0, false).
AddItem(list, 0, lp, false), 0, 1, false).
AddItem(mv.Shared.Bottom.View, 2, 0, false)
AddItem(tv.Shared.Bottom.View, 2, 0, false)
}
if len(TutViews.Views) > 1 {
r.AddItem(mv.accView, 1, 0, false)
}
return r
}

14
ui/media.go

@ -155,18 +155,22 @@ func openMediaType(tv *TutView, filenames []string, mediaType string) {
go func() {
for _, ext := range external {
exec.Command(ext.Name, ext.Args...).Run()
deleteFiles(ext.Filenames)
deleteFiles(tv, ext.Filenames)
}
}()
for _, term := range terminal {
openInTerminal(tv, term.Name, term.Args...)
deleteFiles(term.Filenames)
deleteFiles(tv, term.Filenames)
}
}
func deleteFiles(filenames []string) {
for _, filename := range filenames {
os.Remove(filename)
func deleteFiles(tv *TutView, filenames []string) {
if tv.tut.Config.Media.DeleteTmpFiles {
for _, filename := range filenames {
os.Remove(filename)
}
} else {
tv.FileList = append(tv.FileList, filenames...)
}
}

22
ui/open.go

@ -52,15 +52,13 @@ func openCustom(tv *TutView, program string, args []string, terminal bool, url s
}
}
func OpenEditorLengthLimit(tv *TutView, s string, limit int) (string, bool, error) {
func OpenEditorLengthLimit(tv *TutView, s string, limit int) (string, error) {
text, err := OpenEditor(tv, s)
if err != nil {
return text, false, err
return text, err
}
s = strings.TrimSpace(text)
if len(s) == 0 {
return "", false, nil
}
if utf8.RuneCountInString(s) > limit {
ns := ""
i := 0
@ -73,13 +71,19 @@ func OpenEditorLengthLimit(tv *TutView, s string, limit int) (string, bool, erro
}
s = ns
}
return s, true, nil
return s, nil
}
func OpenEditor(tv *TutView, content string) (string, error) {
editor, exists := os.LookupEnv("EDITOR")
if !exists || editor == "" {
editor = "vi"
var editor string
var exists bool
if tv.tut.Config.General.Editor == strings.TrimSpace("$EDITOR") {
editor, exists = os.LookupEnv("EDITOR")
if !exists || editor == "" {
editor = "vi"
}
} else {
editor = strings.TrimSpace(tv.tut.Config.General.Editor)
}
args := []string{}
parts := strings.Split(editor, " ")

28
ui/pollview.go

@ -166,14 +166,24 @@ func (p *PollView) Add() {
p.tutView.ShowError(fmt.Sprintf("You can only have a maximum of %d options.", p.numOptions))
return
}
text, valid, err := OpenEditorLengthLimit(p.tutView, "", p.numChars)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init("", p.numChars, true, func(input string) {
p.add(input, nil)
})
} else {
text, err := OpenEditorLengthLimit(p.tutView, "", p.numChars)
p.add(text, err)
}
}
func (p *PollView) add(text string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err),
)
return
}
if !valid {
if len(text) == 0 {
return
}
p.list.AddItem(text, "", 0, nil)
@ -187,14 +197,24 @@ func (p *PollView) Edit() {
return
}
text, _ := p.list.GetItemText(p.list.GetCurrentItem())
text, valid, err := OpenEditorLengthLimit(p.tutView, text, p.numChars)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init(text, p.numChars, true, func(input string) {
p.edit(input, nil)
})
} else {
text, err := OpenEditorLengthLimit(p.tutView, text, p.numChars)
p.edit(text, err)
}
}
func (p *PollView) edit(text string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err),
)
return
}
if !valid {
if len(text) == 0 {
return
}
p.list.SetItemText(p.list.GetCurrentItem(), text, "")

80
ui/preferenceview.go

@ -183,25 +183,45 @@ func (p *PreferenceView) AddField() {
p.tutView.ShowError("You can have a maximum of four fields.")
return
}
name, valid, err := OpenEditorLengthLimit(p.tutView, "name", 255)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init("name", 255, true, func(input string) {
p.addFieldOne(input, nil)
})
} else {
name, err := OpenEditorLengthLimit(p.tutView, "name", 255)
p.addFieldOne(name, err)
}
}
func (p *PreferenceView) addFieldOne(name string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't add name. Error: %v\n", err),
)
return
}
if !valid {
if len(name) == 0 {
p.tutView.ShowError("Name can't be empty.")
return
}
value, valid, err := OpenEditorLengthLimit(p.tutView, "value", 255)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init("value", 255, false, func(input string) {
p.addFieldTwo(name, input, nil)
})
} else {
value, err := OpenEditorLengthLimit(p.tutView, "value", 255)
p.addFieldTwo(name, value, err)
}
}
func (p *PreferenceView) addFieldTwo(name string, value string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't add value. Error: %v\n", err),
)
return
}
if !valid {
if len(value) == 0 {
p.tutView.ShowError("Value can't be empty.")
return
}
@ -223,25 +243,45 @@ func (p *PreferenceView) EditField() {
return
}
curr := p.preferences.fields[index]
name, valid, err := OpenEditorLengthLimit(p.tutView, curr.Name, 255)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init(curr.Name, 255, true, func(input string) {
p.editFieldOne(index, input, curr.Value, nil)
})
} else {
name, err := OpenEditorLengthLimit(p.tutView, curr.Name, 255)
p.editFieldOne(index, name, curr.Value, err)
}
}
func (p *PreferenceView) editFieldOne(index int, name string, value string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't edit name. Error: %v\n", err),
)
return
}
if !valid {
if len(name) == 0 {
p.tutView.ShowError("Name can't be empty.")
return
}
value, valid, err := OpenEditorLengthLimit(p.tutView, curr.Value, 255)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init(value, 255, false, func(input string) {
p.editFieldTwo(index, name, input, nil)
})
} else {
value, err := OpenEditorLengthLimit(p.tutView, value, 255)
p.editFieldTwo(index, name, value, err)
}
}
func (p *PreferenceView) editFieldTwo(index int, name string, value string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't edit value. Error: %v\n", err),
)
return
}
if !valid {
if len(value) == 0 {
p.tutView.ShowError("Value can't be empty.")
return
}
@ -268,7 +308,17 @@ func (p *PreferenceView) DeleteField() {
func (p *PreferenceView) EditBio() {
bio := p.preferences.bio
text, _, err := OpenEditorLengthLimit(p.tutView, bio, 500)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init(bio, 500, true, func(input string) {
p.editBio(input, nil)
})
} else {
text, err := OpenEditorLengthLimit(p.tutView, bio, 500)
p.editBio(text, err)
}
}
func (p *PreferenceView) editBio(text string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't edit bio. Error: %v\n", err),
@ -281,7 +331,17 @@ func (p *PreferenceView) EditBio() {
func (p *PreferenceView) EditDisplayname() {
dn := p.preferences.displayname
text, _, err := OpenEditorLengthLimit(p.tutView, dn, 30)
if p.tutView.tut.Config.General.UseInternalEditor {
p.tutView.EditorView.Init(dn, 30, true, func(input string) {
p.editDisplayname(input, nil)
})
} else {
text, err := OpenEditorLengthLimit(p.tutView, dn, 30)
p.editDisplayname(text, err)
}
}
func (p *PreferenceView) editDisplayname(text string, err error) {
if err != nil {
p.tutView.ShowError(
fmt.Sprintf("Couldn't edit display name. Error: %v\n", err),

3
ui/statusbar.go

@ -23,6 +23,7 @@ const (
CmdMode ViewMode = iota
ComposeMode
HelpMode
EditorMode
LinkMode
ListMode
MediaMode
@ -48,6 +49,8 @@ func (sb *StatusBar) SetMode(m ViewMode) {
sb.View.SetText("-- LINK --")
case ListMode:
sb.View.SetText("-- LIST --")
case EditorMode:
sb.View.SetText("-- EDITOR --")
case MediaMode:
sb.View.SetText("-- MEDIA --")
case NotificationsMode:

38
ui/styled_elements.go

@ -26,6 +26,19 @@ func NewTextView(cnf *config.Config) *tview.TextView {
return tw
}
func NewTextArea(cnf *config.Config) *tview.TextArea {
ta := tview.NewTextArea()
ta.SetBackgroundColor(cnf.Style.Background)
ta.SetWordWrap(true)
ta.SetTextStyle(tcell.StyleDefault.
Background(cnf.Style.Background).
Foreground(cnf.Style.Text),
)
//tw.SetTextColor(cnf.Style.Text)
//tw.SetDynamicColors(true)
return ta
}
func NewControlView(cnf *config.Config) *tview.Flex {
f := tview.NewFlex().SetDirection(tview.FlexColumn)
f.SetBackgroundColor(cnf.Style.Background)
@ -127,3 +140,28 @@ func NewHorizontalLine(cnf *config.Config) *tview.Box {
})
return horizontalLine
}
func NewAccButton(tv *TutView, cnf *config.Config, name string, index int, isActive bool) *tview.Button {
btn := tview.NewButton(name)
style := tcell.Style{}
if !isActive {
style = style.Foreground(cnf.Style.Text)
style = style.Background(cnf.Style.Background)
} else {
style = style.Foreground(cnf.Style.ListSelectedText)
style = style.Background(cnf.Style.ListSelectedBackground)
}
btn.SetActivatedStyle(style)
btn.SetStyle(style)
btn.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) {
if !btn.InRect(event.Position()) {
return action, event
}
if action != tview.MouseLeftClick {
return action, event
}
TutViews.SetFocusedTutView(index)
return action, nil
})
return btn
}

160
ui/timeline.go

@ -8,7 +8,6 @@ import (
)
type FeedHolder struct {
Name string
Feeds []*Feed
FeedIndex int
}
@ -21,31 +20,47 @@ type Timeline struct {
scrollSleep *scrollSleep
}
func CreateFeed(tv *TutView, ft config.FeedType, data string, showBoosts, showReplies bool) *Feed {
func (fh *FeedHolder) GetTitle() string {
if fh.FeedIndex >= len(fh.Feeds) {
return ""
}
current := fh.Feeds[fh.FeedIndex]
if len(current.Timeline.Name) > 0 {
return current.Timeline.Name
}
for _, f := range fh.Feeds {
if len(f.Timeline.Name) > 0 {
return f.Timeline.Name
}
}
return ""
}
func CreateFeed(tv *TutView, f *config.Timeline) *Feed {
var nf *Feed
switch ft {
switch f.FeedType {
case config.TimelineHome:
nf = NewHomeFeed(tv, showBoosts, showReplies)
nf = NewHomeFeed(tv, f)
case config.TimelineHomeSpecial:
nf = NewHomeSpecialFeed(tv, showBoosts, showReplies)
nf = NewHomeSpecialFeed(tv, f)
case config.Conversations:
nf = NewConversationsFeed(tv)
nf = NewConversationsFeed(tv, f)
case config.TimelineLocal:
nf = NewLocalFeed(tv, showBoosts, showReplies)
nf = NewLocalFeed(tv, f)
case config.TimelineFederated:
nf = NewFederatedFeed(tv, showBoosts, showReplies)
nf = NewFederatedFeed(tv, f)
case config.Saved:
nf = NewBookmarksFeed(tv)
nf = NewBookmarksFeed(tv, f)
case config.Favorited:
nf = NewFavoritedFeed(tv)
nf = NewFavoritedFeed(tv, f)
case config.Notifications:
nf = NewNotificationFeed(tv, showBoosts, showReplies)
nf = NewNotificationFeed(tv, f)
case config.Mentions:
nf = NewNotificatioMentionsFeed(tv, showBoosts, showReplies)
nf = NewNotificatioMentionsFeed(tv, f)
case config.Lists:
nf = NewListsFeed(tv)
nf = NewListsFeed(tv, f)
case config.Tag:
nf = NewTagFeed(tv, data, showBoosts, showReplies)
nf = NewTagFeed(tv, f)
default:
fmt.Println("Invalid feed")
tv.CleanExit(1)
@ -61,10 +76,12 @@ func NewTimeline(tv *TutView, update chan bool) *Timeline {
}
tl.scrollSleep = NewScrollSleep(tl.NextItemFeed, tl.PrevItemFeed)
for _, f := range tv.tut.Config.General.Timelines {
nf := CreateFeed(tv, f.FeedType, f.Subaction, f.ShowBoosts, f.ShowReplies)
if f.Closed {
continue
}
nf := CreateFeed(tv, f)
tl.Feeds = append(tl.Feeds, &FeedHolder{
Feeds: []*Feed{nf},
Name: f.Name,
})
}
for i := 1; i < len(tl.Feeds); i++ {
@ -76,10 +93,75 @@ func NewTimeline(tv *TutView, update chan bool) *Timeline {
return tl
}
func (tl *Timeline) AddFeed(f *Feed) {
fh := tl.Feeds[tl.FeedFocusIndex]
fh.Feeds = append(fh.Feeds, f)
fh.FeedIndex = fh.FeedIndex + 1
func (tl *Timeline) AddFeed(f *Feed, newPane bool) {
if f.Timeline.Name == "" && tl.tutView.tut.Config.General.DynamicTimelineName {
name := f.Data.Name()
switch f.Timeline.FeedType {
case config.Favorited:
f.Timeline.Name = "Favorited"
case config.Notifications:
f.Timeline.Name = "Notifications"
case config.Mentions:
f.Timeline.Name = "Mentions"
case config.Tag:
parts := strings.Split(name, " ")
for i, p := range parts {
parts[i] = fmt.Sprintf("#%s", p)
}
f.Timeline.Name = strings.Join(parts, " ")
case config.Thread:
f.Timeline.Name = "Thread"
case config.History:
f.Timeline.Name = "History"
case config.TimelineFederated:
f.Timeline.Name = "Federated"
case config.TimelineHome:
f.Timeline.Name = "Home"
case config.TimelineHomeSpecial:
f.Timeline.Name = "Special"
case config.TimelineLocal:
f.Timeline.Name = "Local"
case config.Saved:
f.Timeline.Name = "Bookmarked"
case config.User:
f.Timeline.Name = fmt.Sprintf("@%s", name)
case config.UserList:
f.Timeline.Name = fmt.Sprintf("Search %s", name)
case config.Conversations:
f.Timeline.Name = "Direct"
case config.Lists:
f.Timeline.Name = "Lists"
case config.List:
f.Timeline.Name = fmt.Sprintf("List %s", name)
case config.Boosts:
f.Timeline.Name = "Boosts"
case config.Favorites:
f.Timeline.Name = "Favorites"
case config.Followers:
f.Timeline.Name = "Followers"
case config.Following:
f.Timeline.Name = "Following"
case config.FollowRequests:
f.Timeline.Name = "Follow requests"
case config.Blocking:
f.Timeline.Name = "Blocking"
case config.ListUsersAdd:
f.Timeline.Name = fmt.Sprintf("Add users to %s", name)
case config.ListUsersIn:
f.Timeline.Name = fmt.Sprintf("Delete users from %s", name)
}
}
if newPane {
tl.Feeds = append(tl.Feeds, &FeedHolder{
Feeds: []*Feed{f},
})
tl.tutView.FocusFeed(len(tl.Feeds)-1, nil)
} else {
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
}
@ -106,7 +188,7 @@ func (tl *Timeline) RemoveCurrent(quit bool) bool {
return false
}
func (tl *Timeline) MoveCurrentWindowLeft() {
func (tl *Timeline) MoveCurrentPaneLeft() {
length := len(tl.Feeds)
if length < 2 {
return
@ -116,10 +198,10 @@ func (tl *Timeline) MoveCurrentWindowLeft() {
return
}
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex]
tl.tutView.FocusFeed(ni)
tl.tutView.FocusFeed(ni, nil)
}
func (tl *Timeline) MoveCurrentWindowRight() {
func (tl *Timeline) MoveCurrentPaneRight() {
length := len(tl.Feeds)
if length < 2 {
return
@ -129,31 +211,31 @@ func (tl *Timeline) MoveCurrentWindowRight() {
return
}
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex]
tl.tutView.FocusFeed(ni)
tl.tutView.FocusFeed(ni, nil)
}
func (tl *Timeline) MoveCurrentWindowHome() {
func (tl *Timeline) MoveCurrentPaneHome() {
length := len(tl.Feeds)
if length < 2 {
return
}
ni := 0
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex]
tl.tutView.FocusFeed(ni)
tl.tutView.FocusFeed(ni, nil)
}
func (tl *Timeline) MoveCurrentWindowEnd() {
func (tl *Timeline) MoveCurrentPaneEnd() {
length := len(tl.Feeds)
if length < 2 {
return
}
ni := len(tl.Feeds) - 1
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex]
tl.tutView.FocusFeed(ni)
tl.tutView.FocusFeed(ni, nil)
}
func (tl *Timeline) CloseCurrentWindow() {
if len(tl.Feeds) == 0 {
func (tl *Timeline) CloseCurrentPane() {
if len(tl.Feeds) < 2 {
return
}
feeds := tl.Feeds[tl.FeedFocusIndex]
@ -165,7 +247,7 @@ func (tl *Timeline) CloseCurrentWindow() {
if ni < 0 {
ni = 0
}
tl.tutView.FocusFeed(ni)
tl.tutView.FocusFeed(ni, nil)
}
func (tl *Timeline) NextFeed() {
@ -191,14 +273,14 @@ func (tl *Timeline) PrevFeed() {
tl.update <- true
}
func (tl *Timeline) FindAndGoTo(ft config.FeedType, data string, showBoosts, showReplies bool) bool {
func (tl *Timeline) FindAndGoTo(ft config.FeedType, data string, hideBoosts, hideReplies bool) bool {
for i, fh := range tl.Feeds {
for j, f := range fh.Feeds {
if f.Data.Type() == ft && f.ShowBoosts == showBoosts && f.ShowReplies == showReplies {
if f.Data.Type() == ft && f.Timeline.HideBoosts == hideBoosts && f.Timeline.HideReplies == hideReplies {
if ft == config.Tag && f.Data.Name() != data {
continue
}
tl.tutView.FocusFeed(i)
tl.tutView.FocusFeed(i, nil)
fh.FeedIndex = j
tl.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true
@ -244,11 +326,11 @@ func (tl *Timeline) GetTitle() string {
for i, p := range parts {
parts[i] = fmt.Sprintf("#%s", p)
}
ct = fmt.Sprintf("tag %s", strings.Join(parts, " "))
ct = strings.Join(parts, " ")
case config.Thread:
ct = "thread feed"
ct = "thread"
case config.History:
ct = "history feed"
ct = "history"
case config.TimelineFederated:
ct = "federated"
case config.TimelineHome:
@ -258,7 +340,7 @@ func (tl *Timeline) GetTitle() string {
case config.TimelineLocal:
ct = "local"
case config.Saved:
ct = "saved/bookmarked toots"
ct = "bookmarked"
case config.User:
ct = fmt.Sprintf("user %s", name)
case config.UserList:
@ -268,7 +350,7 @@ func (tl *Timeline) GetTitle() string {
case config.Lists:
ct = "lists"
case config.List:
ct = fmt.Sprintf("list named %s", name)
ct = fmt.Sprintf("list %s", name)
case config.Boosts:
ct = "boosts"
case config.Favorites:

4
ui/top.go

@ -25,7 +25,7 @@ func NewTop(tv *TutView) *Top {
}
func (t *Top) SetText(s string) {
if t.TutView.tut.Client != nil {
if t.TutView.tut.Client != nil && t.TutView.tut.Client.Me != nil {
acct := t.TutView.tut.Client.Me
us := acct.Acct
u, err := url.Parse(acct.URL)
@ -48,7 +48,7 @@ func (t *Top) SetText(s string) {
func (t *Top) setText(s string) {
t.View.SetText(s)
if t.TutView.tut.Config.General.TerminalTitle > 0 {
if t.TutView.tut.Config.General.TerminalTitle > 0 && t.TutView.tut.Config.General.TerminalTitle != 3 {
util.SetTerminalTitle(s)
}
}

96
ui/tutview.go

@ -35,6 +35,22 @@ type Tut struct {
Config *config.Config
}
var App *tview.Application
var Config *config.Config
var Accounts *auth.AccountData
var TutViews *TutViewsHolder
type TutViewsHolder struct {
Views []*TutView
Current int
}
func SetVars(config *config.Config, app *tview.Application, accounts *auth.AccountData) {
Config = config
App = app
Accounts = accounts
}
type TutView struct {
tut *Tut
Timeline *Timeline
@ -53,10 +69,14 @@ type TutView struct {
PollView *PollView
PreferenceView *PreferenceView
HelpView *HelpView
EditorView *EditorView
ModalView *ModalView
FileList []string
}
func (tv *TutView) CleanExit(code int) {
tv.ClearTemp()
os.Exit(code)
}
@ -95,10 +115,19 @@ func (l *Leader) Content() string {
return l.content
}
func NewTutView(t *Tut, accs *auth.AccountData, selectedUser string) *TutView {
func NewTutView(selectedUser string) {
if TutViews == nil {
TutViews = &TutViewsHolder{}
}
accs := Accounts
tv := &TutView{
tut: t,
View: tview.NewPages(),
tut: &Tut{
Client: &api.AccountClient{},
App: App,
Config: Config,
},
View: tview.NewPages(),
FileList: []string{},
}
tv.Leader = NewLeader(tv)
tv.Shared = NewShared(tv)
@ -131,7 +160,52 @@ func NewTutView(t *Tut, accs *auth.AccountData, selectedUser string) *TutView {
} else {
tv.loggedIn(accs.Accounts[0])
}
return tv
TutViews.Views = append(TutViews.Views, tv)
TutViews.SetFocusedTutView(len(TutViews.Views) - 1)
}
func (tvh *TutViewsHolder) SetFocusedTutView(index int) {
if index < 0 && index >= len(tvh.Views) {
return
}
tvh.Current = index
curr := tvh.Views[tvh.Current]
App.SetRoot(curr.View, true)
App.SetInputCapture(curr.Input)
if Config.General.MouseSupport {
App.SetMouseCapture(curr.MouseInput)
}
if curr.MainView != nil {
curr.MainView.ForceUpdate()
}
}
func (tvh *TutViewsHolder) Next() {
if len(tvh.Views) < 2 {
return
}
next := tvh.Current + 1
if next >= len(tvh.Views) {
next = 0
}
tvh.SetFocusedTutView(next)
}
func (tvh *TutViewsHolder) Prev() {
if len(tvh.Views) < 2 {
return
}
prev := tvh.Current - 1
if prev < 0 {
prev = len(tvh.Views) - 1
}
tvh.SetFocusedTutView(prev)
}
func DoneAdding() {
if len(TutViews.Views) > 0 {
TutViews.SetFocusedTutView(0)
}
}
func (tv *TutView) loggedIn(acc auth.Account) {
@ -181,6 +255,7 @@ func (tv *TutView) loggedIn(acc auth.Account) {
tv.PollView = NewPollView(tv)
tv.PreferenceView = NewPreferenceView(tv)
tv.HelpView = NewHelpView(tv)
tv.EditorView = NewEditorView(tv)
tv.ModalView = NewModalView(tv)
tv.View.AddPage("main", tv.MainView.View, true, false)
@ -188,13 +263,14 @@ func (tv *TutView) loggedIn(acc auth.Account) {
tv.View.AddPage("compose", tv.ComposeView.View, true, false)
tv.View.AddPage("vote", tv.VoteView.View, true, false)
tv.View.AddPage("help", tv.HelpView.View, true, false)
tv.View.AddPage("editor", tv.EditorView.View, true, false)
tv.View.AddPage("poll", tv.PollView.View, true, false)
tv.View.AddPage("preference", tv.PreferenceView.View, true, false)
tv.View.AddPage("modal", tv.ModalView.View, true, false)
tv.SetPage(MainFocus)
}
func (tv *TutView) FocusFeed(index int) {
func (tv *TutView) FocusFeed(index int, ct *config.Timeline) {
if index < 0 || index >= len(tv.Timeline.Feeds) {
return
}
@ -210,6 +286,12 @@ func (tv *TutView) FocusFeed(index int) {
}
}
}
for i, tl := range tv.Timeline.Feeds[index].Feeds {
if ct == tl.Timeline {
tv.Timeline.Feeds[index].FeedIndex = i
break
}
}
tv.Shared.Top.SetText(tv.Timeline.GetTitle())
tv.Timeline.update <- true
}
@ -219,7 +301,7 @@ func (tv *TutView) NextFeed() {
if index >= len(tv.Timeline.Feeds) {
index = 0
}
tv.FocusFeed(index)
tv.FocusFeed(index, nil)
}
func (tv *TutView) PrevFeed() {
@ -227,5 +309,5 @@ func (tv *TutView) PrevFeed() {
if index < 0 {
index = len(tv.Timeline.Feeds) - 1
}
tv.FocusFeed(index)
tv.FocusFeed(index, nil)
}

9
ui/view.go

@ -1,6 +1,8 @@
package ui
import (
"log"
"github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/api"
)
@ -19,6 +21,7 @@ const (
CmdFocus
VoteFocus
HelpFocus
EditorFocus
PollFocus
PreferenceFocus
)
@ -80,6 +83,7 @@ func (tv *TutView) SetPage(f PageFocusAt) {
}
switch f {
case LoginFocus:
log.Fatalln(tv.tut.Client.Me.Acct)
tv.PageFocus = LoginFocus
tv.View.SwitchToPage("login")
tv.Shared.Bottom.StatusBar.SetMode(UserMode)
@ -134,6 +138,11 @@ func (tv *TutView) SetPage(f PageFocusAt) {
tv.View.SwitchToPage("help")
tv.Shared.Bottom.StatusBar.SetMode(HelpMode)
tv.tut.App.SetFocus(tv.HelpView.content)
case EditorFocus:
tv.PageFocus = EditorFocus
tv.View.SwitchToPage("editor")
tv.Shared.Bottom.StatusBar.SetMode(EditorMode)
tv.tut.App.SetFocus(tv.EditorView.editor)
case ModalFocus:
tv.PageFocus = ModalFocus
tv.View.SwitchToPage("modal")

39
util/util.go

@ -11,6 +11,7 @@ import (
"github.com/RasmusLindroth/go-mastodon"
"github.com/adrg/xdg"
"github.com/microcosm-cc/bluemonday"
"github.com/rivo/tview"
"golang.org/x/net/html"
)
@ -53,18 +54,48 @@ type URL struct {
}
func CleanHTML(content string) (string, []URL) {
stripped := bluemonday.NewPolicy().AllowElements("p", "br").AllowAttrs("href", "class").OnElements("a").Sanitize(content)
stripped := bluemonday.NewPolicy().AllowElements("p", "br", "li", "ul", "ol").AllowAttrs("href", "class").OnElements("a").Sanitize(content)
urls := getURLs(stripped)
stripped = bluemonday.NewPolicy().AllowElements("p", "br").Sanitize(content)
stripped = bluemonday.NewPolicy().AllowElements("p", "br", "li", "ul", "ol").Sanitize(content)
stripped = strings.ReplaceAll(stripped, "<br>", "\n")
stripped = strings.ReplaceAll(stripped, "<br/>", "\n")
stripped = strings.ReplaceAll(stripped, "<p>", "")
stripped = strings.ReplaceAll(stripped, "</p>", "\n\n")
stripped = strings.ReplaceAll(stripped, "<li>", "* ")
stripped = strings.ReplaceAll(stripped, "</li>", "\n")
stripped = strings.ReplaceAll(stripped, "<ul>", "")
stripped = strings.ReplaceAll(stripped, "</ul>", "\n")
stripped = strings.ReplaceAll(stripped, "<ol>", "")
stripped = strings.ReplaceAll(stripped, "</ol>", "\n")
stripped = strings.TrimSpace(stripped)
stripped = html.UnescapeString(stripped)
return stripped, urls
}
func CleanHTMLStyled(content string) (string, []URL) {
stripped := bluemonday.NewPolicy().AllowElements("p", "br", "li", "ul", "ol", "strong", "em").AllowAttrs("href", "class").OnElements("a").Sanitize(content)
urls := getURLs(stripped)
stripped = bluemonday.NewPolicy().AllowElements("p", "br", "li", "ul", "ol", "strong", "em").Sanitize(content)
stripped = strings.ReplaceAll(stripped, "<br>", "\n")
stripped = strings.ReplaceAll(stripped, "<br/>", "\n")
stripped = strings.ReplaceAll(stripped, "<p>", "")
stripped = strings.ReplaceAll(stripped, "</p>", "\n\n")
stripped = strings.ReplaceAll(stripped, "<li>", "* ")
stripped = strings.ReplaceAll(stripped, "</li>", "\n")
stripped = strings.ReplaceAll(stripped, "<ul>", "")
stripped = strings.ReplaceAll(stripped, "</ul>", "\n")
stripped = strings.ReplaceAll(stripped, "<ol>", "")
stripped = strings.ReplaceAll(stripped, "</ol>", "\n")
stripped = html.UnescapeString(stripped)
stripped = tview.Escape(stripped)
stripped = strings.ReplaceAll(stripped, "<strong>", TextFlags("b"))
stripped = strings.ReplaceAll(stripped, "</strong>", TextFlags("-"))
stripped = strings.ReplaceAll(stripped, "<em>", TextFlags("i"))
stripped = strings.ReplaceAll(stripped, "</em>", TextFlags("-"))
stripped = strings.TrimSpace(stripped)
return stripped, urls
}
func getURLs(text string) []URL {
doc := html.NewTokenizer(strings.NewReader(text))
var urls []URL
@ -209,3 +240,7 @@ func StatusOrReblog(s *mastodon.Status) *mastodon.Status {
func SetTerminalTitle(s string) {
fmt.Printf("\033]0;%s\a", s)
}
func TextFlags(s string) string {
return fmt.Sprintf("[::%s]", s)
}

23
util/xdg.go

@ -1,7 +1,26 @@
package util
import "os/exec"
import (
"os/exec"
"runtime"
)
func GetDefaultForOS() (program string, args []string) {
switch runtime.GOOS {
case "windows":
program = "start"
args = []string{"/wait"}
case "darwin":
program = "open"
args = []string{"-W"}
default:
program = "xdg-open"
}
return program, args
}
func OpenURL(url string) {
exec.Command("xdg-open", url).Start()
program, args := GetDefaultForOS()
args = append(args, url)
exec.Command(program, args...).Start()
}

Loading…
Cancel
Save