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 * `:boosts` = Lists users that have boosted the toot
* `:bookmarks` = List all your bookmarks * `:bookmarks` = List all your bookmarks
* `:clear-notifications` = Remove all of your notifications * `: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 * `:compose` = Compose a new toot
* `:edit` = Edit one of your toots * `:edit` = Edit one of your toots
* `:favorited` = Lists toots you've favorited * `: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 * `:history` = Show edits of a toot
* `:lists` = Show a list of your lists * `:lists` = Show a list of your lists
* `:list-placement top|right|bottom|left` = Place the list in choosen placement * `: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 * `:list-split row|column` = Split the timelines by row or column
* `:move-window left|right|up|down|home|end` = Moves the window in choosen direction * `:login` = Login to one more account
* `:mv l|r|u|d|h|e` = Shorter form of former command * `: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 * `:muting` = Lists users that you've muted
* `:newer` = Force load newer toots in current timeline * `: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 * `: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 * `: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. * `:refetch` = Refetches the current item that you're viewing. Can be used to update poll results.
* `:saved` = Alias for bookmarks * `:saved` = Alias for bookmarks
* `:stick-to-top` = Toggle the stick-to-top setting that always shows the latest toot in all timelines * `: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 * `:tags` = List of tags that you're following
* `:unfollow-tag <tag>` = Unfollow the hashtag named &lt;tag&gt;, e.g. :unfollow-tag tut * `: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 * `: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 Keys without description in tut
* `c` = Compose a new toot * `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 what image viewer to use and some more. Check out the configuration file to see
all the options. 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). 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` 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. 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 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 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)`. 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 ## Flags and commands
``` ```
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: Flags:
-h --help prints this message -h --help prints this message
-v --version prints the version -v --version prints the version
-n --new-user add one more user to tut -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> -d --config-dir <path> load all config from <path>
-u --user <name> login directly to user named <name> -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 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 }} {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:clear-notifications{{ Flags "-" }}{{ Color .Style.Text }}
Remove all of your notifications Remove all of your notifications
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:close-window{{ Flags "-" }}{{ Color .Style.Text }} {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:clear-temp{{ Flags "-" }}{{ Color .Style.Text }}
Closes the current window, including all the timelines in said window 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 }} {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:compose{{ Flags "-" }}{{ Color .Style.Text }}
Compose a new toot Compose a new toot
@ -90,12 +93,15 @@ Here's a list of supported commands.
Place the list in choosen placement Place the list in choosen placement
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:list-split{{ Flags "-" }}{{ Color .Style.Text }} row|column {{ 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 {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:move-pane{{ Flags "-" }}{{ Color .Style.Text }} left|right|up|down|home|end
Moves the window in choosen direction 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 Shorter form of former command
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:muting{{ Flags "-" }}{{ Color .Style.Text }} {{ 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 }} {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:newer{{ Flags "-" }}{{ Color .Style.Text }}
Force load newer toots in current timeline 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 }} {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:preferences{{ Flags "-" }}{{ Color .Style.Text }}
Update your profile and some other settings 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 }} {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:profile{{ Flags "-" }}{{ Color .Style.Text }}
Go to your profile Go to your profile
{{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:proportions{{ Flags "-" }}{{ Color .Style.Text }} [int] [int] {{ 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 }} {{ 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. 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> {{ 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 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> {{ Color .Style.TextSpecial2 }}{{ Flags "b" }}:pane{{ Flags "-" }}{{ Color .Style.Text }} <int>
Switch window by index (zero indexed) e.g. :window 0 for the left/top window Switch pane by index (zero indexed) e.g. :pane 0 for the left/top pane
{{ Color .Style.Text }}{{ Flags "b" }}Configuration{{ Flags "-" }} {{ Color .Style.Text }}{{ Flags "b" }}Configuration{{ Flags "-" }}
tut searches for a config file in the following locations on Linux: tut searches for a config file in the following locations on Linux:
1. $XDG_CONFIG_HOME/tut/config.ini 1. $XDG_CONFIG_HOME/tut/config.toml
2. $HOME/.config/tut/config.ini 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 "-" }}. 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 "-" }} 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 return text
} }
func ColorMark(color tcell.Color) string {
return fmt.Sprintf("[#%06x]", color.Hex())
}
func TextFlags(s string) string { func TextFlags(s string) string {
return fmt.Sprintf("[::%s]", s) return fmt.Sprintf("[::%s]", s)
} }
@ -44,3 +40,7 @@ func SublteText(c *Config, text string) string {
subtle := ColorMark(c.Style.Subtle) subtle := ColorMark(c.Style.Subtle)
return fmt.Sprintf("%s%s", subtle, text) 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) fmt.Printf("Couldn't create or access the configuration dir. Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
path, exists, err := checkConfig("config.ini", cnfPath, cnfDir) path, exists, err := checkConfig("config.toml", cnfPath, cnfDir)
if err != nil { 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) os.Exit(1)
} }
if !exists { 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.TextSpecial1 }} {{ .Toot.Boosts }}
{{- Color .Style.Subtle }} Favorites {{- Color .Style.Subtle }} Favorites
{{- Color .Style.TextSpecial1 }} {{ .Toot.Favorites }} {{- Color .Style.TextSpecial1 }} {{ .Toot.Favorites }}
{{- Color .Style.TextSpecial2 }} {{ .Toot.Lang }}

14
docs/man/tut.1

@ -14,7 +14,7 @@
. ftr VB CB . ftr VB CB
. ftr VBI CBI . ftr VBI CBI
.\} .\}
.TH "tut" "1" "2023-01-01" "tut 1.0.34" "" .TH "tut" "1" "2023-01-23" "tut 2.0.0" ""
.hy .hy
.SH NAME .SH NAME
.PP .PP
@ -39,13 +39,17 @@ Show the version number
Add one more user to tut Add one more user to tut
.TP .TP
\f[B]-c\f[R], \f[B]--config\f[R] <path> \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 .TP
\f[B]-d\f[R], \f[B]--config-dir\f[R] <path> \f[B]-d\f[R], \f[B]--config-dir\f[R] <path>
Load all config from \f[I]<path>\f[R] Load all config from \f[I]<path>\f[R]
.TP .TP
\f[B]-u\f[R], \f[B]--user\f[R] <name> \f[B]-u\f[R], \f[B]--user\f[R] <name>
Login directly to user named \f[I]<name>\f[R]. 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 If two users are named the same, use full name like
\f[I]tut\[at]fosstodon.org\f[R] \f[I]tut\[at]fosstodon.org\f[R]
.SH COMMANDS .SH COMMANDS
@ -55,15 +59,15 @@ Runs the TUI
.TP .TP
\f[B]example-config\f[R] \f[B]example-config\f[R]
Generates the default configuration file in the current directory and Generates the default configuration file in the current directory and
names it ./config.example.ini names it ./config.example.toml
.SH CONFIGURATION .SH CONFIGURATION
.PP .PP
Tut is configurable, so you can change things like the colors, the Tut is configurable, so you can change things like the colors, the
default timeline, what image viewer to use and some more. default timeline, what image viewer to use and some more.
Check out tut(5) or the configuration file to see all the options. Check out tut(5) or the configuration file to see all the options.
.PP .PP
You find it in \f[I]$XDG_CONFIG_HOME/tut/config.ini\f[R] on Linux which 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.ini\f[R]. 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 If you don\[cq]t run Linux it will use the path of the Go funcdtion
os.UserConfigDir(). os.UserConfigDir().
But if you move the tut folder to \f[I]XDG_CONFIG_HOME/tut/\f[R] and 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 % Rasmus Lindroth
% 2023-01-01 % 2023-01-23
# NAME # NAME
tut - a Mastodon TUI 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 : Add one more user to tut
**-c**, **\--config** \<path\> **-c**, **\--config** \<path\>
: Load config.ini from *\<path\>* : Load config.toml from *\<path\>*
**-d**, **\--config-dir** \<path\> **-d**, **\--config-dir** \<path\>
: Load all config from *\<path\>* : Load all config from *\<path\>*
**-u**, **\--user** \<name\> **-u**, **\--user** \<name\>
: Login directly to user named *\<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* : If two users are named the same, use full name like *tut@fosstodon.org*
# COMMANDS # COMMANDS
@ -39,12 +40,12 @@ To see keys and commands you can use inside of tut check tut(7).
: Runs the TUI : Runs the TUI
**example-config** **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 # 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. 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(). 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* 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. 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 VB CB
. ftr VBI CBI . ftr VBI CBI
.\} .\}
.TH "tut" "7" "2023-01-01" "tut 1.0.34" "" .TH "tut" "7" "2023-01-23" "tut 2.0.0" ""
.hy .hy
.SH NAME .SH NAME
.PP .PP
@ -108,8 +108,13 @@ List all your bookmarks
\f[B]:clear-notifications\f[R] \f[B]:clear-notifications\f[R]
Remove all of your notifications Remove all of your notifications
.TP .TP
\f[B]:close-window\f[R] \f[B]:clear-temp\f[R]
Closes the current window, including all the timelines in said window 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 .TP
\f[B]:compose\f[R] \f[B]:compose\f[R]
Compose a new toot Compose a new toot
@ -150,12 +155,15 @@ Show a list of your lists
Place the list in choosen placement Place the list in choosen placement
.TP .TP
\f[B]:list-split\f[R] \f[I]row|column\f[R] \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 .TP
\f[B]:move-window\f[R] \f[I]left|right|up|down|home|end\f[R] \f[B]:move-pane\f[R] \f[I]left|right|up|down|home|end\f[R]
Moves the window in choosen direction Moves the pane in choosen direction
.TP .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 Shorter form of former command
.TP .TP
\f[B]:muting\f[R] \f[B]:muting\f[R]
@ -164,15 +172,21 @@ Lists users that you\[aq]ve muted
\f[B]:newer\f[R] \f[B]:newer\f[R]
Force load newer toots in current timeline Force load newer toots in current timeline
.TP .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] \f[B]:preferences\f[R]
Update your profile and some other settings Update your profile and some other settings
.TP .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] \f[B]:profile\f[R]
Go to your profile Go to your profile
.TP .TP
\f[B]:proportions\f[R] \f[I][int] [int]\f[R] \f[B]:proportions\f[R] \f[I][int] [int]\f[R]
Sets the proportions of the windows and the content. Sets the proportions of the panes and the content.
The first integer is your windows and the other for content, The first integer is your panes and the other for content,
e.g.\ :proportions 1 3 e.g.\ :proportions 1 3
.TP .TP
\f[B]:refetch\f[R] \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 To narrow a search include the instance like this :user
rasmus\[at]mastodon.acc.sunet.se rasmus\[at]mastodon.acc.sunet.se
.TP .TP
\f[B]:window\f[R] \f[I]<int>\f[R] \f[B]:pane\f[R] \f[I]<int>\f[R]
Switch window by index (zero indexed) e.g.\ :window 0 for the left/top Switch pane by index (zero indexed) e.g.\ :pane 0 for the left/top pane
window
.SH SEE ALSO .SH SEE ALSO
.IP .IP
.nf .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 % Rasmus Lindroth
% 2023-01-01 % 2023-01-23
# NAME # NAME
tut - keys and commands inside of tut(1) 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** **:clear-notifications**
: Remove all of your notifications : Remove all of your notifications
**:close-window** **:clear-temp**
: Closes the current window, including all the timelines in said window : 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**
: Compose a new toot : 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 : Place the list in choosen placement
**:list-split** *row|column* **: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* **:move-pane** *left|right|up|down|home|end*
: Moves the window in choosen direction : 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 : Shorter form of former command
**:muting** **:muting**
@ -107,14 +113,20 @@ To change the keys look at tut(5) under the *INPUT* section.
**:newer** **:newer**
: Force load newer toots in current timeline : Force load newer toots in current timeline
**:next-acct**
: Go to the next account if you\'re logged in to multiple
**:preferences** **:preferences**
: Update your profile and some other settings : Update your profile and some other settings
**:prev-acct**
: Go to the prev account if you\'re logged in to multiple
**:profile** **:profile**
: Go to your profile : Go to your profile
**:proportions** *[int] [int]* **: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 : 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** **:refetch**
: Refetches the current item that you\'re viewing. Can be used to update poll results. : 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\>* **: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 : 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\>* **:pane** *\<int\>*
: Switch window by index (zero indexed) e.g. :window 0 for the left/top window : Switch pane by index (zero indexed) e.g. :pane 0 for the left/top pane
# SEE ALSO # SEE ALSO
tut(1) - flags and commands tut(1) - flags and commands

80
feed/feed.go

@ -64,8 +64,8 @@ type Feed struct {
streams []*api.Receiver streams []*api.Receiver
name string name string
close func() close func()
showBoosts bool hideBoosts bool
showReplies bool hideReplies bool
} }
func (f *Feed) Type() config.FeedType { 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 { if f.Type() == config.TimelineHomeSpecial && x.Reblog == nil && x.InReplyToID == nil {
continue continue
} }
if x.Reblog != nil && !f.showBoosts { if x.Reblog != nil && f.hideBoosts {
continue continue
} }
if x.InReplyToID != nil && !f.showReplies { if x.InReplyToID != nil && f.hideReplies {
continue 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{ return &Feed{
accountClient: ac, accountClient: ac,
config: cnf, config: cnf,
@ -849,13 +849,13 @@ func newFeed(ac *api.AccountClient, ft config.FeedType, cnf *config.Config, show
Update: make(chan DesktopNotificationHolder, 1), Update: make(chan DesktopNotificationHolder, 1),
loadingNewer: &LoadingLock{}, loadingNewer: &LoadingLock{},
loadingOlder: &LoadingLock{}, loadingOlder: &LoadingLock{},
showBoosts: showBoosts, hideBoosts: hideBoosts,
showReplies: showReplies, hideReplies: hideReplies,
} }
} }
func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineHome, cnf, showBoosts, showReplies) feed := newFeed(ac, config.TimelineHome, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) } feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) }
feed.startStream(feed.accountClient.NewHomeStream()) feed.startStream(feed.accountClient.NewHomeStream())
@ -868,8 +868,8 @@ func NewTimelineHome(ac *api.AccountClient, cnf *config.Config, showBoosts bool,
return feed return feed
} }
func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineHomeSpecial, cnf, showBoosts, showReplies) feed := newFeed(ac, config.TimelineHomeSpecial, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) } feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimeline) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimeline) }
feed.startStream(feed.accountClient.NewHomeStream()) feed.startStream(feed.accountClient.NewHomeStream())
@ -882,8 +882,8 @@ func NewTimelineHomeSpecial(ac *api.AccountClient, cnf *config.Config, showBoost
return feed return feed
} }
func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineFederated, cnf, showBoosts, showReplies) feed := newFeed(ac, config.TimelineFederated, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineFederated) } feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineFederated) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineFederated) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineFederated) }
feed.startStream(feed.accountClient.NewFederatedStream()) feed.startStream(feed.accountClient.NewFederatedStream())
@ -896,8 +896,8 @@ func NewTimelineFederated(ac *api.AccountClient, cnf *config.Config, showBoosts
return feed return feed
} }
func NewTimelineLocal(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { func NewTimelineLocal(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.TimelineLocal, cnf, showBoosts, showReplies) feed := newFeed(ac, config.TimelineLocal, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineLocal) } feed.loadNewer = func() { feed.normalNewer(feed.accountClient.GetTimelineLocal) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineLocal) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetTimelineLocal) }
feed.startStream(feed.accountClient.NewLocalStream()) 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 { 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.loadNewer = func() { feed.normalNewer(feed.accountClient.GetConversations) }
feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetConversations) } feed.loadOlder = func() { feed.normalOlder(feed.accountClient.GetConversations) }
feed.startStream(feed.accountClient.NewDirectStream()) feed.startStream(feed.accountClient.NewDirectStream())
@ -923,8 +923,8 @@ func NewConversations(ac *api.AccountClient, cnf *config.Config) *Feed {
return feed return feed
} }
func NewNotifications(ac *api.AccountClient, cnf *config.Config, showBoosts bool, showReplies bool) *Feed { func NewNotifications(ac *api.AccountClient, cnf *config.Config, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.Notifications, cnf, showBoosts, showReplies) feed := newFeed(ac, config.Notifications, cnf, hideBoosts, hideReplies)
feed.loadNewer = func() { feed.loadNewer = func() {
feed.normalNewerNotification(feed.accountClient.GetNotifications, cnf.General.NotificationsToHide) 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 { 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} hide := []config.NotificationToHide{config.HideStatus, config.HideBoost, config.HideFollow, config.HideFollowRequest, config.HideFavorite, config.HidePoll, config.HideEdited}
feed.loadNewer = func() { feed.loadNewer = func() {
feed.normalNewerNotification(feed.accountClient.GetNotifications, hide) 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 { 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.loadNewer = func() { feed.linkNewer(feed.accountClient.GetFavorites) }
feed.loadOlder = func() { feed.linkOlder(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 { 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.loadNewer = func() { feed.linkNewer(feed.accountClient.GetBookmarks) }
feed.loadOlder = func() { feed.linkOlder(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 { 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.name = search
feed.loadNewer = func() { feed.singleNewerSearch(feed.accountClient.GetUsers, 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 { 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.name = user.Data.Acct
feed.sticky = append(feed.sticky, api.NewUserItem(user, true)) feed.sticky = append(feed.sticky, api.NewUserItem(user, true))
pinned, err := ac.GetUserPinned(user.Data.ID) 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { if once {
@ -1023,8 +1023,8 @@ func NewHistory(ac *api.AccountClient, cnf *config.Config, status *mastodon.Stat
return feed return feed
} }
func NewTag(ac *api.AccountClient, cnf *config.Config, search string, showBoosts bool, showReplies bool) *Feed { func NewTag(ac *api.AccountClient, cnf *config.Config, search string, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.Tag, cnf, showBoosts, showReplies) feed := newFeed(ac, config.Tag, cnf, hideBoosts, hideReplies)
parts := strings.Split(search, " ") parts := strings.Split(search, " ")
var tparts []string var tparts []string
for _, p := range parts { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { if once {
@ -1064,7 +1064,7 @@ func NewTags(ac *api.AccountClient, cnf *config.Config) *Feed {
} }
func NewListList(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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { if once {
@ -1076,8 +1076,8 @@ func NewListList(ac *api.AccountClient, cnf *config.Config) *Feed {
return feed return feed
} }
func NewList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List, showBoosts bool, showReplies bool) *Feed { func NewList(ac *api.AccountClient, cnf *config.Config, list *mastodon.List, hideBoosts bool, hideReplies bool) *Feed {
feed := newFeed(ac, config.List, cnf, showBoosts, showReplies) feed := newFeed(ac, config.List, cnf, hideBoosts, hideReplies)
feed.name = list.Title feed.name = list.Title
feed.loadNewer = func() { feed.normalNewerID(feed.accountClient.GetListStatuses, list.ID) } feed.loadNewer = func() { feed.normalNewerID(feed.accountClient.GetListStatuses, list.ID) }
feed.loadOlder = func() { feed.normalOlderID(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 { 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 feed.name = list.Title
once := true once := true
feed.loadNewer = func() { 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 { 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 feed.name = list.Title
once := true once := true
feed.loadNewer = func() { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { 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 { 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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { if once {
@ -1190,7 +1190,7 @@ func NewBlocking(ac *api.AccountClient, cnf *config.Config) *Feed {
} }
func NewMuting(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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { if once {
@ -1204,7 +1204,7 @@ func NewMuting(ac *api.AccountClient, cnf *config.Config) *Feed {
} }
func NewFollowRequests(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 once := true
feed.loadNewer = func() { feed.loadNewer = func() {
if once { if once {

17
go.mod

@ -6,18 +6,17 @@ require (
github.com/RasmusLindroth/go-mastodon v0.0.21 github.com/RasmusLindroth/go-mastodon v0.0.21
github.com/adrg/xdg v0.4.0 github.com/adrg/xdg v0.4.0
github.com/atotto/clipboard v0.1.4 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/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6
github.com/gobwas/glob v0.2.3 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/microcosm-cc/bluemonday v1.0.21
github.com/pelletier/go-toml/v2 v2.0.6 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/rivo/uniseg v0.4.3
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 golang.org/x/exp v0.0.0-20230118134722-a68e582fa157
golang.org/x/net v0.4.0 golang.org/x/net v0.5.0
gopkg.in/ini.v1 v1.67.0
mvdan.cc/xurls/v2 v2.4.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/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sys v0.4.0 // indirect
golang.org/x/term v0.3.0 // indirect golang.org/x/term v0.4.0 // indirect
golang.org/x/text v0.5.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/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 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 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.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= 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 h1:jFEK/SA/7E8lg9T33+y8D4Z0I782+bbiEjmyyklRzRQ=
github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw= 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= 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/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 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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-20230117093757-93f961aa2755 h1:CdxhyIdXDB8Ilp3ogohR8g1omzjKzDsVh5kd71M7Pvc=
github.com/icza/gox v0.0.0-20221026131554-a08a8cdc726a/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk= 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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-20230104153304-892d1a2eb0da h1:3Mh+tcC2KqetuHpWMurDeF+yOgyt4w4qtLIpwSQ3uqo=
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/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/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 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 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-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-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-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.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 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.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.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.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-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 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/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/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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

51
main.go

@ -1,6 +1,8 @@
package main package main
import ( import (
"strings"
"github.com/RasmusLindroth/tut/auth" "github.com/RasmusLindroth/tut/auth"
"github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/ui" "github.com/RasmusLindroth/tut/ui"
@ -8,7 +10,9 @@ import (
"github.com/rivo/tview" "github.com/rivo/tview"
) )
const version = "1.0.34" const version = "2.0.0"
var tutViews []*ui.TutView
func main() { func main() {
util.SetTerminalTitle("tut") util.SetTerminalTitle("tut")
@ -17,32 +21,35 @@ func main() {
accs := auth.StartAuth(newUser) accs := auth.StartAuth(newUser)
app := tview.NewApplication() app := tview.NewApplication()
t := &ui.Tut{ cnf := config.Load(cnfPath, cnfDir)
App: app,
Config: config.Load(cnfPath, cnfDir), if cnf.General.MouseSupport {
}
if t.Config.General.MouseSupport {
app.EnableMouse(true) app.EnableMouse(true)
} }
tview.Styles = tview.Theme{ tview.Styles = tview.Theme{
PrimitiveBackgroundColor: t.Config.Style.Background, // background PrimitiveBackgroundColor: cnf.Style.Background, // background
ContrastBackgroundColor: t.Config.Style.Text, //background for button, checkbox, form, modal ContrastBackgroundColor: cnf.Style.Text, //background for button, checkbox, form, modal
MoreContrastBackgroundColor: t.Config.Style.Text, //background for dropdown MoreContrastBackgroundColor: cnf.Style.Text, //background for dropdown
BorderColor: t.Config.Style.Background, //border BorderColor: cnf.Style.Background, //border
TitleColor: t.Config.Style.Text, //titles TitleColor: cnf.Style.Text, //titles
GraphicsColor: t.Config.Style.Text, //borders GraphicsColor: cnf.Style.Text, //borders
PrimaryTextColor: t.Config.Style.StatusBarViewBackground, //backround color selected PrimaryTextColor: cnf.Style.StatusBarViewBackground, //backround color selected
SecondaryTextColor: t.Config.Style.Text, //text SecondaryTextColor: cnf.Style.Text, //text
TertiaryTextColor: t.Config.Style.Text, //list secondary TertiaryTextColor: cnf.Style.Text, //list secondary
InverseTextColor: t.Config.Style.Text, //label activated InverseTextColor: cnf.Style.Text, //label activated
ContrastSecondaryTextColor: t.Config.Style.Text, //foreground on input and prefix on dropdown ContrastSecondaryTextColor: cnf.Style.Text, //foreground on input and prefix on dropdown
} }
main := ui.NewTutView(t, accs, selectedUser) ui.SetVars(cnf, app, accs)
app.SetInputCapture(main.Input) users := strings.Fields(selectedUser)
if t.Config.General.MouseSupport { if len(users) > 0 {
app.SetMouseCapture(main.MouseInput) 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) 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") showVersion := pflag.BoolP("version", "v", false, "config path")
nu := pflag.BoolP("new-user", "n", false, "add one more user to tut") nu := pflag.BoolP("new-user", "n", false, "add one more user to tut")
user := pflag.StringP("user", "u", "", "login directly to user named `<name>`") 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>`") cnfDir := pflag.StringP("config-dir", "d", "", "load all config from `<path>`")
pflag.Parse() pflag.Parse()
if len(os.Args) > 1 { if len(os.Args) > 1 {
switch os.Args[1] { switch os.Args[1] {
case "example-config": case "example-config":
config.CreateDefaultConfig("./config.example.ini") config.CreateDefaultConfig("./config.example.toml")
os.Exit(0) 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("\tTo run the program you just have to write tut\n\n")
fmt.Print("Commands:\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("Flags:\n")
fmt.Print("\t-h --help prints this message\n") fmt.Print("\t-h --help prints this message\n")
fmt.Print("\t-v --version prints the version\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-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-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("\t\tIf two users are named the same. Use full name like tut@fosstodon.org\n\n")
fmt.Print("Configuration:\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.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") 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), View: NewInputField(tv.tut.Config),
} }
c.View.SetAutocompleteFunc(c.Autocomplete) c.View.SetAutocompleteFunc(c.Autocomplete)
//c.View.SetAutocompletedFunc(c.Autocompleted) c.View.SetAutocompletedFunc(c.Autocompleted)
c.View.SetDoneFunc(c.DoneFunc) c.View.SetDoneFunc(c.DoneFunc)
return c return c
@ -100,28 +100,40 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
case ":newer": case ":newer":
c.tutView.LoadNewerCommand() c.tutView.LoadNewerCommand()
c.Back() 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": case ":clear-notifications":
c.tutView.ClearNotificationsCommand() c.tutView.ClearNotificationsCommand()
c.Back() c.Back()
case ":close-window": case ":clear-temp":
c.tutView.CloseWindowCommand() c.tutView.ClearTemp()
c.Back() c.Back()
case ":move-window", ":mv": case ":close-pane":
c.tutView.ClosePaneCommand()
c.Back()
case ":move-pane", ":mp":
if len(parts) < 2 { if len(parts) < 2 {
break break
} }
switch parts[1] { switch parts[1] {
case "left", "up", "l", "u": case "left", "up", "l", "u":
c.tutView.MoveWindowLeft() c.tutView.MovePaneLeft()
c.Back() c.Back()
case "right", "down", "r", "d": case "right", "down", "r", "d":
c.tutView.MoveWindowRight() c.tutView.MovePaneRight()
c.Back() c.Back()
case "home", "h": case "home", "h":
c.tutView.MoveWindowHome() c.tutView.MovePaneHome()
c.Back() c.Back()
case "end", "e": case "end", "e":
c.tutView.MoveWindowEnd() c.tutView.MovePaneEnd()
c.Back() c.Back()
} }
case ":list-placement": case ":list-placement":
@ -185,13 +197,13 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
c.tutView.FederatedCommand() c.tutView.FederatedCommand()
c.Back() c.Back()
case "special-all", "sa": case "special-all", "sa":
c.tutView.SpecialCommand(true, true) c.tutView.SpecialCommand(false, false)
c.Back() c.Back()
case "special-boosts", "sb": case "special-boosts", "sb":
c.tutView.SpecialCommand(true, false) c.tutView.SpecialCommand(false, true)
c.Back() c.Back()
case "special-replies", "sr": case "special-replies", "sr":
c.tutView.SpecialCommand(false, true) c.tutView.SpecialCommand(true, false)
c.Back() c.Back()
case "direct", "d": case "direct", "d":
c.tutView.DirectCommand() c.tutView.DirectCommand()
@ -222,11 +234,11 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
case ":tags": case ":tags":
c.tutView.TagsCommand() c.tutView.TagsCommand()
c.Back() c.Back()
case ":window": case ":pane":
if len(parts) < 2 { if len(parts) < 2 {
break break
} }
c.tutView.WindowCommand(parts[1]) c.tutView.PaneCommand(parts[1])
c.Back() c.Back()
case ":user": case ":user":
if len(parts) < 2 { if len(parts) < 2 {
@ -237,7 +249,10 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
break break
} }
c.tutView.Timeline.AddFeed( 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() c.Back()
case ":refetch": case ":refetch":
@ -279,13 +294,13 @@ func (c *CmdBar) DoneFunc(key tcell.Key) {
func (c *CmdBar) Autocomplete(curr string) []string { func (c *CmdBar) Autocomplete(curr string) []string {
var entries []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 == "" { if curr == "" {
return entries return entries
} }
if len(curr) > 2 && curr[:3] == ":tl" { 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" { 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", ",") 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", ",") words = strings.Split(":list-split row,:list-split column", ",")
} }
if len(curr) > 11 && curr[:12] == ":move-window" { if len(curr) > 11 && curr[:12] == ":move-pane" {
words = strings.Split(":move-window left,:move-window right,:move-window up,:move-window down,:move-window home,:move-window end", ",") 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" { if len(curr) > 2 && curr[:3] == ":mv" {
words = strings.Split(":mv left,:mv right,:mv up,:mv down,:mv home,:mv end", ",") 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 return entries
} }
/*
func (c *CmdBar) Autocompleted(text string, index, source int) bool { func (c *CmdBar) Autocompleted(text string, index, source int) bool {
if source != tview.AutocompletedNavigate { if source != tview.AutocompletedNavigate {
c.View.SetText(text) c.View.SetText(text)
} }
return false
return source == tview.AutocompletedEnter || source == tview.AutocompletedClick
} }
*/

271
ui/commands.go

@ -4,13 +4,11 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/api"
"github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/util" "github.com/RasmusLindroth/tut/util"
"golang.org/x/exp/slices"
) )
func (tv *TutView) ComposeCommand() { func (tv *TutView) ComposeCommand() {
@ -35,91 +33,124 @@ func (tv *TutView) EditCommand() {
func (tv *TutView) BlockingCommand() { func (tv *TutView) BlockingCommand() {
tv.Timeline.AddFeed( tv.Timeline.AddFeed(
NewBlocking(tv), NewBlocking(tv, config.NewTimeline(config.Timeline{
FeedType: config.Blocking,
})), tv.tut.Config.General.CommandsInNewPane,
) )
} }
func (tv *TutView) BookmarksCommand() { func (tv *TutView) BookmarksCommand() {
tv.Timeline.AddFeed( tv.Timeline.AddFeed(
NewBookmarksFeed(tv), NewBookmarksFeed(tv, config.NewTimeline(config.Timeline{
) FeedType: config.Saved,
})),
tv.tut.Config.General.CommandsInNewPane)
} }
func (tv *TutView) FavoritedCommand() { func (tv *TutView) FavoritedCommand() {
tv.Timeline.AddFeed( tv.Timeline.AddFeed(
NewFavoritedFeed(tv), NewFavoritedFeed(tv, config.NewTimeline(config.Timeline{
) FeedType: config.Favorited,
})),
tv.tut.Config.General.CommandsInNewPane)
} }
func (tv *TutView) MutingCommand() { func (tv *TutView) MutingCommand() {
tv.Timeline.AddFeed( tv.Timeline.AddFeed(
NewMuting(tv), NewMuting(tv, config.NewTimeline(config.Timeline{
) FeedType: config.Muting,
})),
tv.tut.Config.General.CommandsInNewPane)
} }
func (tv *TutView) FollowRequestsCommand() { func (tv *TutView) FollowRequestsCommand() {
tv.Timeline.AddFeed( tv.Timeline.AddFeed(
NewFollowRequests(tv), NewFollowRequests(tv, config.NewTimeline(config.Timeline{
) FeedType: config.FollowRequests,
})),
tv.tut.Config.General.CommandsInNewPane)
} }
func (tv *TutView) LocalCommand() { func (tv *TutView) LocalCommand() {
tv.Timeline.AddFeed( 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() { func (tv *TutView) FederatedCommand() {
tv.Timeline.AddFeed( 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( 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() { func (tv *TutView) DirectCommand() {
tv.Timeline.AddFeed( tv.Timeline.AddFeed(
NewConversationsFeed(tv), NewConversationsFeed(tv, config.NewTimeline(config.Timeline{
) FeedType: config.Conversations,
})),
tv.tut.Config.General.CommandsInNewPane)
} }
func (tv *TutView) HomeCommand() { func (tv *TutView) HomeCommand() {
tv.Timeline.AddFeed( 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() { func (tv *TutView) NotificationsCommand() {
tv.Timeline.AddFeed( 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() { func (tv *TutView) MentionsCommand() {
tv.Timeline.AddFeed( 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() { func (tv *TutView) ListsCommand() {
tv.Timeline.AddFeed( 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) { func (tv *TutView) TagCommand(tag string) {
tv.Timeline.AddFeed( 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() { func (tv *TutView) TagsCommand() {
tv.Timeline.AddFeed( 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) { 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) i, err := strconv.Atoi(index)
if err != nil { if err != nil {
tv.ShowError( tv.ShowError(
@ -146,119 +177,27 @@ func (tv *TutView) WindowCommand(index string) {
) )
return return
} }
tv.FocusFeed(i) tv.FocusFeed(i, nil)
} }
func (tv *TutView) MoveWindowLeft() { func (tv *TutView) MovePaneLeft() {
tv.Timeline.MoveCurrentWindowLeft() tv.Timeline.MoveCurrentPaneLeft()
} }
func (tv *TutView) MoveWindowRight() { func (tv *TutView) MovePaneRight() {
tv.Timeline.MoveCurrentWindowRight() tv.Timeline.MoveCurrentPaneRight()
} }
func (tv *TutView) MoveWindowHome() { func (tv *TutView) MovePaneHome() {
tv.Timeline.MoveCurrentWindowHome() tv.Timeline.MoveCurrentPaneHome()
} }
func (tv *TutView) MoveWindowEnd() { func (tv *TutView) MovePaneEnd() {
tv.Timeline.MoveCurrentWindowEnd() tv.Timeline.MoveCurrentPaneEnd()
} }
func (tv *TutView) SwitchCommand(s string) { func (tv *TutView) ClosePaneCommand() {
ft := config.InvalidFeed tv.Timeline.CloseCurrentPane()
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) BoostsCommand() { func (tv *TutView) BoostsCommand() {
@ -272,8 +211,12 @@ func (tv *TutView) BoostsCommand() {
s := item.Raw().(*mastodon.Status) s := item.Raw().(*mastodon.Status)
s = util.StatusOrReblog(s) s = util.StatusOrReblog(s)
tv.Timeline.AddFeed( 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() { func (tv *TutView) FavoritesCommand() {
@ -287,8 +230,10 @@ func (tv *TutView) FavoritesCommand() {
s := item.Raw().(*mastodon.Status) s := item.Raw().(*mastodon.Status)
s = util.StatusOrReblog(s) s = util.StatusOrReblog(s)
tv.Timeline.AddFeed( 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() { func (tv *TutView) FollowingCommand() {
@ -301,8 +246,10 @@ func (tv *TutView) FollowingCommand() {
} }
s := item.Raw().(*api.User) s := item.Raw().(*api.User)
tv.Timeline.AddFeed( 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() { func (tv *TutView) FollowersCommand() {
@ -315,8 +262,10 @@ func (tv *TutView) FollowersCommand() {
} }
s := item.Raw().(*api.User) s := item.Raw().(*api.User)
tv.Timeline.AddFeed( 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() { func (tv *TutView) HistoryCommand() {
@ -328,8 +277,10 @@ func (tv *TutView) HistoryCommand() {
return return
} }
tv.Timeline.AddFeed( 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() { func (tv *TutView) ProfileCommand() {
@ -339,8 +290,10 @@ func (tv *TutView) ProfileCommand() {
return return
} }
tv.Timeline.AddFeed( 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() { func (tv *TutView) PreferencesCommand() {
@ -378,6 +331,17 @@ func (tv *TutView) LoadNewerCommand() {
f.LoadNewer(true) f.LoadNewer(true)
} }
func (tv *TutView) LoginCommand() {
NewTutView("")
}
func (tv *TutView) NextAcct() {
TutViews.Next()
}
func (tv *TutView) PrevAcct() {
TutViews.Prev()
}
func (tv *TutView) ClearNotificationsCommand() { func (tv *TutView) ClearNotificationsCommand() {
err := tv.tut.Client.ClearNotifications() err := tv.tut.Client.ClearNotifications()
if err != nil { 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() { func (tv *TutView) ToggleStickToTop() {
tv.tut.Config.General.StickToTop = !tv.tut.Config.General.StickToTop tv.tut.Config.General.StickToTop = !tv.tut.Config.General.StickToTop
} }

216
ui/composeview.go

@ -33,17 +33,19 @@ type msgToot struct {
} }
type ComposeView struct { type ComposeView struct {
tutView *TutView tutView *TutView
shared *Shared shared *Shared
View *tview.Flex View *tview.Flex
content *tview.TextView content *tview.TextView
input *MediaInput textAreaMain *tview.TextArea
info *tview.TextView textAreaCW *tview.TextArea
controls *tview.Flex input *MediaInput
visibility *tview.DropDown info *tview.TextView
lang *tview.DropDown controls *tview.Flex
media *MediaList visibility *tview.DropDown
msg *msgToot lang *tview.DropDown
media *MediaList
msg *msgToot
} }
var visibilities = map[string]int{ var visibilities = map[string]int{
@ -61,15 +63,17 @@ var visibilitiesStr = []string{
func NewComposeView(tv *TutView) *ComposeView { func NewComposeView(tv *TutView) *ComposeView {
cv := &ComposeView{ cv := &ComposeView{
tutView: tv, tutView: tv,
shared: tv.Shared, shared: tv.Shared,
content: NewTextView(tv.tut.Config), content: NewTextView(tv.tut.Config),
input: NewMediaInput(tv), textAreaMain: NewTextArea(tv.tut.Config),
controls: NewControlView(tv.tut.Config), textAreaCW: NewTextArea(tv.tut.Config),
info: NewTextView(tv.tut.Config), input: NewMediaInput(tv),
visibility: NewDropDown(tv.tut.Config), controls: NewControlView(tv.tut.Config),
lang: NewDropDown(tv.tut.Config), info: NewTextView(tv.tut.Config),
media: NewMediaList(tv), visibility: NewDropDown(tv.tut.Config),
lang: NewDropDown(tv.tut.Config),
media: NewMediaList(tv),
} }
cv.content.SetDynamicColors(true) cv.content.SetDynamicColors(true)
cv.View = newComposeUI(cv) cv.View = newComposeUI(cv)
@ -81,18 +85,41 @@ func newComposeUI(cv *ComposeView) *tview.Flex {
if cv.tutView.tut.Config.General.TerminalTitle < 2 { if cv.tutView.tut.Config.General.TerminalTitle < 2 {
r.AddItem(cv.tutView.Shared.Top.View, 1, 0, false) r.AddItem(cv.tutView.Shared.Top.View, 1, 0, false)
} }
r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn). if !cv.tutView.tut.Config.General.UseInternalEditor {
AddItem(tview.NewFlex().SetDirection(tview.FlexRow). r.AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(cv.content, 0, 2, false), 0, 2, false). AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(tview.NewBox(), 2, 0, false). AddItem(cv.content, 0, 2, false), 0, 2, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow). AddItem(tview.NewBox(), 2, 0, false).
AddItem(cv.visibility, 1, 0, false). AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(cv.lang, 1, 0, false). AddItem(cv.visibility, 1, 0, false).
AddItem(cv.info, 5, 0, false). AddItem(cv.lang, 1, 0, false).
AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false). AddItem(cv.info, 5, 0, false).
AddItem(cv.input.View, 1, 0, false). AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false).
AddItem(cv.controls, 1, 0, false). AddItem(cv.input.View, 1, 0, false).
AddItem(cv.tutView.Shared.Bottom.View, 2, 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 return r
} }
@ -167,6 +194,8 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) {
func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) error { func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) error {
cv.tutView.PollView.Reset() cv.tutView.PollView.Reset()
cv.media.Reset() cv.media.Reset()
cv.textAreaMain.SetText("", false)
cv.textAreaCW.SetText("", false)
msg := &msgToot{} msg := &msgToot{}
me := cv.tutView.tut.Client.Me me := cv.tutView.tut.Client.Me
visibility := mastodon.VisibilityPublic 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.SetOptions(langStrs, cv.langSelected)
cv.lang.SetCurrentOption(index) 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.UpdateContent()
cv.SetControls(ComposeNormal) cv.SetControls(ComposeNormal)
return nil return nil
@ -275,27 +308,35 @@ func (cv *ComposeView) getAccs() string {
} }
func (cv *ComposeView) EditText() { func (cv *ComposeView) EditText() {
text, err := OpenEditor(cv.tutView, cv.msg.Text) if !cv.tutView.tut.Config.General.UseInternalEditor {
if err != nil { text, err := OpenEditor(cv.tutView, cv.msg.Text)
cv.tutView.ShowError( if err != nil {
fmt.Sprintf("Couldn't open editor. Error: %v", err), cv.tutView.ShowError(
) fmt.Sprintf("Couldn't open editor. Error: %v", err),
return )
return
}
cv.msg.Text = text
cv.UpdateContent()
} else {
cv.textAreaMainFocus()
} }
cv.msg.Text = text
cv.UpdateContent()
} }
func (cv *ComposeView) EditSpoiler() { func (cv *ComposeView) EditSpoiler() {
text, err := OpenEditor(cv.tutView, cv.msg.CWText) if !cv.tutView.tut.Config.General.UseInternalEditor {
if err != nil { text, err := OpenEditor(cv.tutView, cv.msg.CWText)
cv.tutView.ShowError( if err != nil {
fmt.Sprintf("Couldn't open editor. Error: %v", err), cv.tutView.ShowError(
) fmt.Sprintf("Couldn't open editor. Error: %v", err),
return )
return
}
cv.msg.CWText = text
cv.UpdateContent()
} else {
cv.textAreaCWFocus()
} }
cv.msg.CWText = text
cv.UpdateContent()
} }
func (cv *ComposeView) ToggleCW() { func (cv *ComposeView) ToggleCW() {
@ -315,26 +356,29 @@ func (cv *ComposeView) UpdateContent() {
if cv.msg.Reply != nil { if cv.msg.Reply != nil {
var acct string var acct string
if cv.msg.Reply.Account.DisplayName != "" { 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 { } 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 outputHead += subtleColor + "Replying to " + tview.Escape(acct) + "\n" + normal
} }
if cv.msg.CWText != "" && !cv.msg.Sensitive { 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 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 == "" { 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 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 != "" { if !cv.tutView.tut.Config.General.UseInternalEditor {
outputHead += subtleColor + "Content warning\n\n" + normal if cv.msg.Sensitive && cv.msg.CWText != "" {
outputHead += tview.Escape(cv.msg.CWText) outputHead += subtleColor + "Content warning\n\n" + normal
outputHead += "\n\n" + subtleColor + "---hidden content below---\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) cv.content.SetText(output)
} }
@ -354,12 +398,51 @@ func (cv *ComposeView) IncludeQuote() {
for _, line := range strings.Split(tootText, "\n") { for _, line := range strings.Split(tootText, "\n") {
t += "> " + line + "\n" t += "> " + line + "\n"
} }
t += "\n"
cv.msg.Text = t cv.msg.Text = t
if cv.tutView.tut.Config.General.UseInternalEditor {
cv.textAreaMain.SetText(cv.msg.Text, false)
}
cv.msg.QuoteIncluded = true cv.msg.QuoteIncluded = true
cv.UpdateContent() 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 { func (cv *ComposeView) HasMedia() bool {
return len(cv.media.Files) > 0 return len(cv.media.Files) > 0
} }
@ -650,14 +733,29 @@ func (m *MediaList) EditDesc() {
) )
return 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 { if err != nil {
m.tutView.ShowError( m.tutView.ShowError(
fmt.Sprintf("Couldn't edit description. Error: %v\n", err), fmt.Sprintf("Couldn't edit description. Error: %v\n", err),
) )
return return
} }
file.Description = desc file.Description = text
m.Files[index] = file m.Files[index] = file
m.Draw() 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 { type Feed struct {
tutView *TutView tutView *TutView
Data *feed.Feed Data *feed.Feed
List *FeedList List *FeedList
Content *FeedContent Content *FeedContent
ShowBoosts bool Timeline *config.Timeline
ShowReplies bool
} }
func (f *Feed) ListInFocus() { func (f *Feed) ListInFocus() {
@ -141,113 +140,106 @@ func (f *Feed) update() {
} }
} }
func NewHomeFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { func NewHomeFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineHome(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) f := feed.NewTimelineHome(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd return fd
} }
func NewHomeSpecialFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { func NewHomeSpecialFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) f := feed.NewTimelineHomeSpecial(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd return fd
} }
func NewFederatedFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { func NewFederatedFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineFederated(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) f := feed.NewTimelineFederated(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd return fd
} }
func NewLocalFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { func NewLocalFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTimelineLocal(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) f := feed.NewTimelineLocal(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd return fd
} }
func NewNotificationFeed(tv *TutView, showBoosts bool, showReplies bool) *Feed { func NewNotificationFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewNotifications(tv.tut.Client, tv.tut.Config, showBoosts, showReplies) f := feed.NewNotifications(tv.tut.Client, tv.tut.Config, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewNotificationsMentions(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd 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)) status := util.StatusOrReblog(item.Raw().(*mastodon.Status))
f := feed.NewThread(tv.tut.Client, tv.tut.Config, status) f := feed.NewThread(tv.tut.Client, tv.tut.Config, status)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
for i, s := range f.List() { for i, s := range f.List() {
main, symbol := DrawListItem(tv.tut.Config, s) main, symbol := DrawListItem(tv.tut.Config, s)
@ -261,17 +253,16 @@ func NewThreadFeed(tv *TutView, item api.Item) *Feed {
return fd 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)) status := util.StatusOrReblog(item.Raw().(*mastodon.Status))
f := feed.NewHistory(tv.tut.Client, tv.tut.Config, status) f := feed.NewHistory(tv.tut.Client, tv.tut.Config, status)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
for _, s := range f.List() { for _, s := range f.List() {
main, symbol := DrawListItem(tv.tut.Config, s) main, symbol := DrawListItem(tv.tut.Config, s)
@ -283,23 +274,22 @@ func NewHistoryFeed(tv *TutView, item api.Item) *Feed {
return fd 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 := feed.NewConversations(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 { if item.Type() != api.UserType && item.Type() != api.ProfileType {
panic("Can't open user. Wrong type.\n") 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 := feed.NewUserProfile(tv.tut.Client, tv.tut.Config, u)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd return fd
} }
func NewUserSearchFeed(tv *TutView, search string) *Feed { func NewUserSearchFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewUserSearch(tv.tut.Client, tv.tut.Config, search) f := feed.NewUserSearch(tv.tut.Client, tv.tut.Config, tl.Subaction)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
for _, s := range f.List() { for _, s := range f.List() {
main, symbol := DrawListItem(tv.tut.Config, s) main, symbol := DrawListItem(tv.tut.Config, s)
@ -339,240 +327,225 @@ func NewUserSearchFeed(tv *TutView, search string) *Feed {
return fd return fd
} }
func NewTagFeed(tv *TutView, search string, showBoosts bool, showReplies bool) *Feed { func NewTagFeed(tv *TutView, tl *config.Timeline) *Feed {
f := feed.NewTag(tv.tut.Client, tv.tut.Config, search, showBoosts, showReplies) f := feed.NewTag(tv.tut.Client, tv.tut.Config, tl.Subaction, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewTags(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewListList(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd return fd
} }
func NewListFeed(tv *TutView, l *mastodon.List, showBoosts bool, showReplies bool) *Feed { func NewListFeed(tv *TutView, l *mastodon.List, tl *config.Timeline) *Feed {
f := feed.NewList(tv.tut.Client, tv.tut.Config, l, showBoosts, showReplies) f := feed.NewList(tv.tut.Client, tv.tut.Config, l, tl.HideBoosts, tl.HideReplies)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: showBoosts, Timeline: tl,
ShowReplies: showReplies,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewUsersInList(tv.tut.Client, tv.tut.Config, l)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewUsersAddList(tv.tut.Client, tv.tut.Config, l)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewFavorites(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewBookmarks(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewFavoritesStatus(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewBoosts(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewFollowers(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewFollowing(tv.tut.Client, tv.tut.Config, id)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewBlocking(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewMuting(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()
return fd 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 := feed.NewFollowRequests(tv.tut.Client, tv.tut.Config)
f.LoadNewer() f.LoadNewer()
fd := &Feed{ fd := &Feed{
tutView: tv, tutView: tv,
Data: f, Data: f,
List: NewFeedList(tv.tut, f.StickyCount()), List: NewFeedList(tv.tut, f.StickyCount()),
Content: NewFeedContent(tv.tut), Content: NewFeedContent(tv.tut),
ShowBoosts: true, Timeline: tl,
ShowReplies: true,
} }
go fd.update() go fd.update()

206
ui/input.go

@ -2,7 +2,6 @@ package ui
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -30,6 +29,16 @@ func (tv *TutView) Input(event *tcell.EventKey) *tcell.EventKey {
return nil 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 { switch tv.PageFocus {
case LoginFocus: case LoginFocus:
return tv.InputLoginView(event) return tv.InputLoginView(event)
@ -55,6 +64,8 @@ func (tv *TutView) Input(event *tcell.EventKey) *tcell.EventKey {
return tv.InputHelp(event) return tv.InputHelp(event)
case PreferenceFocus: case PreferenceFocus:
return tv.InputPreference(event) return tv.InputPreference(event)
case EditorFocus:
return tv.InputEditorView(event)
default: default:
return event return event
} }
@ -95,6 +106,33 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
action := config.LeaderNone action := config.LeaderNone
var subaction string var subaction string
content := tv.Leader.Content() 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 { for _, la := range tv.tut.Config.General.LeaderActions {
if la.Shortcut == content { if la.Shortcut == content {
action = la.Command action = la.Command
@ -106,20 +144,6 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
switch action { 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: case config.LeaderClearNotifications:
tv.ClearNotificationsCommand() tv.ClearNotificationsCommand()
case config.LeaderCompose: case config.LeaderCompose:
@ -128,8 +152,6 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
tv.EditCommand() tv.EditCommand()
case config.LeaderBlocking: case config.LeaderBlocking:
tv.BlockingCommand() tv.BlockingCommand()
case config.LeaderBookmarks, config.LeaderSaved:
tv.BookmarksCommand()
case config.LeaderFavorited: case config.LeaderFavorited:
tv.FavoritedCommand() tv.FavoritedCommand()
case config.LeaderHistory: case config.LeaderHistory:
@ -148,36 +170,26 @@ func (tv *TutView) InputLeaderKey(event *tcell.EventKey) *tcell.EventKey {
tv.PreferencesCommand() tv.PreferencesCommand()
case config.LeaderProfile: case config.LeaderProfile:
tv.ProfileCommand() tv.ProfileCommand()
case config.LeaderNotifications:
tv.NotificationsCommand()
case config.LeaderMentions:
tv.MentionsCommand()
case config.LeaderLoadNewer: case config.LeaderLoadNewer:
tv.LoadNewerCommand() tv.LoadNewerCommand()
case config.LeaderLists:
tv.ListsCommand()
case config.LeaderStickToTop: case config.LeaderStickToTop:
tv.ToggleStickToTop() tv.ToggleStickToTop()
case config.LeaderRefetch: case config.LeaderRefetch:
tv.RefetchCommand() tv.RefetchCommand()
case config.LeaderTag:
tv.TagCommand(subaction)
case config.LeaderTags: case config.LeaderTags:
tv.TagsCommand() tv.TagsCommand()
case config.LeaderWindow: case config.LeaderPane:
tv.WindowCommand(subaction) tv.PaneCommand(subaction)
case config.LeaderCloseWindow: case config.LeaderClosePane:
tv.CloseWindowCommand() tv.ClosePaneCommand()
case config.LeaderMoveWindowLeft: case config.LeaderMovePaneLeft:
tv.MoveWindowLeft() tv.MovePaneLeft()
case config.LeaderMoveWindowRight: case config.LeaderMovePaneRight:
tv.MoveWindowRight() tv.MovePaneRight()
case config.LeaderMoveWindowHome: case config.LeaderMovePaneHome:
tv.MoveWindowHome() tv.MovePaneHome()
case config.LeaderMoveWindowEnd: case config.LeaderMovePaneEnd:
tv.MoveWindowEnd() tv.MovePaneEnd()
case config.LeaderSwitch:
tv.SwitchCommand(subaction)
case config.LeaderListPlacement: case config.LeaderListPlacement:
switch subaction { switch subaction {
case "top": 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 { func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey {
if tv.tut.Config.Input.MainHome.Match(event.Key(), event.Rune()) { if tv.tut.Config.Input.MainHome.Match(event.Key(), event.Rune()) {
tv.Timeline.HomeItemFeed() tv.Timeline.HomeItemFeed()
@ -244,16 +266,12 @@ func (tv *TutView) InputMainViewFeed(event *tcell.EventKey) *tcell.EventKey {
tv.Timeline.PrevItemFeed() tv.Timeline.PrevItemFeed()
return nil return nil
} }
if tv.tut.Config.Input.MainPrevWindow.Match(event.Key(), event.Rune()) { if tv.tut.Config.Input.MainPrevPane.Match(event.Key(), event.Rune()) {
if tv.tut.Config.General.NotificationFeed { tv.PrevFeed()
tv.PrevFeed()
}
return nil return nil
} }
if tv.tut.Config.Input.MainNextWindow.Match(event.Key(), event.Rune()) { if tv.tut.Config.Input.MainNextPane.Match(event.Key(), event.Rune()) {
if tv.tut.Config.General.NotificationFeed { tv.NextFeed()
tv.NextFeed()
}
return nil return nil
} }
if tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) { 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 return nil
} else if exiting && tv.Timeline.FeedFocusIndex != 0 { } else if exiting && tv.Timeline.FeedFocusIndex != 0 {
tv.FocusFeed(0) tv.FocusFeed(0, nil)
} }
return nil return nil
} }
for i, tl := range tv.tut.Config.General.Timelines {
if tl.Key.Match(event.Key(), event.Rune()) { foundFeed := false
tv.FocusFeed(i) 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()) { if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) {
exiting := tv.Timeline.RemoveCurrent(false) exiting := tv.Timeline.RemoveCurrent(false)
if exiting && tv.Timeline.FeedFocusIndex != 0 { if exiting && tv.Timeline.FeedFocusIndex != 0 {
tv.FocusFeed(0) tv.FocusFeed(0, nil)
} }
return nil return nil
} }
@ -511,7 +549,9 @@ func (tv *TutView) InputStatus(event *tcell.EventKey, item api.Item, status *mas
return nil return nil
} }
if tv.tut.Config.Input.StatusThread.Match(event.Key(), event.Rune()) { 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 return nil
} }
if tv.tut.Config.Input.StatusUser.Match(event.Key(), event.Rune()) { 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 { if err != nil {
return 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 return nil
} }
if tv.tut.Config.Input.StatusViewFocus.Match(event.Key(), event.Rune()) { 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 return nil
} }
if tv.tut.Config.Input.UserUser.Match(event.Key(), event.Rune()) { 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 return nil
} }
if tv.tut.Config.Input.UserViewFocus.Match(event.Key(), event.Rune()) { 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 return nil
} }
if tv.tut.Config.Input.GlobalEnter.Match(event.Key(), event.Rune()) { 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 nil
} }
return event 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 { func (tv *TutView) InputList(event *tcell.EventKey, list *mastodon.List) *tcell.EventKey {
if tv.tut.Config.Input.ListOpenFeed.Match(event.Key(), event.Rune()) || if tv.tut.Config.Input.ListOpenFeed.Match(event.Key(), event.Rune()) ||
tv.tut.Config.Input.GlobalEnter.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 return nil
} }
if tv.tut.Config.Input.ListUserList.Match(event.Key(), event.Rune()) { 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 return nil
} }
if tv.tut.Config.Input.ListUserAdd.Match(event.Key(), event.Rune()) { 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 nil
} }
return event 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 { func (tv *TutView) InputTag(event *tcell.EventKey, tag *mastodon.Tag) *tcell.EventKey {
if tv.tut.Config.Input.TagOpenFeed.Match(event.Key(), event.Rune()) || if tv.tut.Config.Input.TagOpenFeed.Match(event.Key(), event.Rune()) ||
tv.tut.Config.Input.GlobalEnter.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 return nil
} }
if tv.tut.Config.Input.TagFollow.Match(event.Key(), event.Rune()) { 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) tv.SetPage(MainFocus)
return nil return nil
} }
if event.Key() == tcell.KeyRune { for _, oc := range tv.tut.Config.OpenCustom.OpenCustoms {
switch event.Rune() { if oc.Key.Match(event.Key(), event.Rune()) {
case '1', '2', '3', '4', '5': tv.LinkView.OpenCustom(oc)
s := string(event.Rune())
i, _ := strconv.Atoi(s)
tv.LinkView.OpenCustom(i)
return nil return nil
} }
} }
@ -1099,6 +1151,20 @@ func (tv *TutView) InputCmdView(event *tcell.EventKey) *tcell.EventKey {
return event 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) { func (tv *TutView) MouseInput(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) {
if event == nil { if event == nil {
return nil, action return nil, action
@ -1108,6 +1174,12 @@ func (tv *TutView) MouseInput(event *tcell.EventMouse, action tview.MouseAction)
return event, action return event, action
} }
/* Switch accounts */
x, y := event.Position()
if tv.MainView.accView.InRect(x, y) {
return event, action
}
switch tv.PageFocus { switch tv.PageFocus {
case ViewFocus, MainFocus: case ViewFocus, MainFocus:
return tv.MouseInputMainView(event, action) 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) { func (tv *TutView) feedListMouse(list *tview.List, i int, event *tcell.EventMouse, action tview.MouseAction) {
tv.SetPage(MainFocus) tv.SetPage(MainFocus)
tv.FocusFeed(i) tv.FocusFeed(i, nil)
mh := list.MouseHandler() mh := list.MouseHandler()
if mh == nil { if mh == nil {
return return

13
ui/item_status.go

@ -36,6 +36,7 @@ type Toot struct {
Boosts int Boosts int
Favorites int Favorites int
Edited bool Edited bool
Lang string
Controls string Controls string
} }
@ -104,8 +105,7 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
status = status.Reblog status = status.Reblog
} }
strippedContent, _ = util.CleanHTML(status.Content) strippedContent, _ = util.CleanHTMLStyled(status.Content)
strippedContent = tview.Escape(strippedContent)
width := 0 width := 0
if main != nil { if main != nil {
@ -121,6 +121,12 @@ func drawStatus(tv *TutView, item api.Item, status *mastodon.Status, main *tview
ShowSpoiler: showSensitive, ShowSpoiler: showSensitive,
CWlabel: cwToggle.Label, 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.AccountDisplayName = tview.Escape(status.Account.DisplayName)
toot.Account = tview.Escape(status.Account.Acct) 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 { if status.Sensitive {
strippedSpoiler, _ = util.CleanHTML(status.SpoilerText) strippedSpoiler, _ = util.CleanHTMLStyled(status.SpoilerText)
strippedSpoiler = tview.Escape(strippedSpoiler)
} }
toot.CWText = strippedSpoiler 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), NewControl(lv.tutView.tut.Config, lv.tutView.tut.Config.Input.LinkYank, true),
} }
for _, cust := range lv.tutView.tut.Config.OpenCustom.OpenCustoms { for _, cust := range lv.tutView.tut.Config.OpenCustom.OpenCustoms {
key := config.Key{ items = append(items, NewControl(lv.tutView.tut.Config, cust.Key, true))
Hint: [][]string{{"", fmt.Sprintf("%d", cust.Index), cust.Name}},
}
items = append(items, NewControl(lv.tutView.tut.Config, key, true))
} }
lv.controls.Clear() lv.controls.Clear()
for i, item := range items { for i, item := range items {
@ -114,16 +111,20 @@ func (lv *LinkView) Open() {
return return
} }
lv.tutView.Timeline.AddFeed( lv.tutView.Timeline.AddFeed(
NewUserFeed(lv.tutView, u), NewUserFeed(lv.tutView, u, config.NewTimeline(config.Timeline{
) FeedType: config.User,
}),
), false)
lv.tutView.FocusMainNoHistory() lv.tutView.FocusMainNoHistory()
return return
} }
tIndex := index - len(mentions) - len(urls) tIndex := index - len(mentions) - len(urls)
if tIndex < len(tags) { if tIndex < len(tags) {
lv.tutView.Timeline.AddFeed( 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() lv.tutView.FocusMainNoHistory()
return return
} }
@ -162,17 +163,10 @@ func (lv *LinkView) Yank() {
copyToClipboard(url) copyToClipboard(url)
} }
func (lv *LinkView) OpenCustom(index int) { func (lv *LinkView) OpenCustom(c config.Custom) {
url := lv.getURL() url := lv.getURL()
if url == "" { if url == "" {
return return
} }
customs := lv.tutView.tut.Config.OpenCustom.OpenCustoms openCustom(lv.tutView, c.Program, c.Args, c.Terminal, url)
for _, c := range customs {
if c.Index != index {
continue
}
openCustom(lv.tutView, c.Program, c.Args, c.Terminal, url)
return
}
} }

97
ui/mainview.go

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

14
ui/media.go

@ -155,18 +155,22 @@ func openMediaType(tv *TutView, filenames []string, mediaType string) {
go func() { go func() {
for _, ext := range external { for _, ext := range external {
exec.Command(ext.Name, ext.Args...).Run() exec.Command(ext.Name, ext.Args...).Run()
deleteFiles(ext.Filenames) deleteFiles(tv, ext.Filenames)
} }
}() }()
for _, term := range terminal { for _, term := range terminal {
openInTerminal(tv, term.Name, term.Args...) openInTerminal(tv, term.Name, term.Args...)
deleteFiles(term.Filenames) deleteFiles(tv, term.Filenames)
} }
} }
func deleteFiles(filenames []string) { func deleteFiles(tv *TutView, filenames []string) {
for _, filename := range filenames { if tv.tut.Config.Media.DeleteTmpFiles {
os.Remove(filename) 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) text, err := OpenEditor(tv, s)
if err != nil { if err != nil {
return text, false, err return text, err
} }
s = strings.TrimSpace(text) s = strings.TrimSpace(text)
if len(s) == 0 {
return "", false, nil
}
if utf8.RuneCountInString(s) > limit { if utf8.RuneCountInString(s) > limit {
ns := "" ns := ""
i := 0 i := 0
@ -73,13 +71,19 @@ func OpenEditorLengthLimit(tv *TutView, s string, limit int) (string, bool, erro
} }
s = ns s = ns
} }
return s, true, nil return s, nil
} }
func OpenEditor(tv *TutView, content string) (string, error) { func OpenEditor(tv *TutView, content string) (string, error) {
editor, exists := os.LookupEnv("EDITOR") var editor string
if !exists || editor == "" { var exists bool
editor = "vi" 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{} args := []string{}
parts := strings.Split(editor, " ") 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)) p.tutView.ShowError(fmt.Sprintf("You can only have a maximum of %d options.", p.numOptions))
return 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err), fmt.Sprintf("Couldn't open editor. Error: %v", err),
) )
return return
} }
if !valid { if len(text) == 0 {
return return
} }
p.list.AddItem(text, "", 0, nil) p.list.AddItem(text, "", 0, nil)
@ -187,14 +197,24 @@ func (p *PollView) Edit() {
return return
} }
text, _ := p.list.GetItemText(p.list.GetCurrentItem()) 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't open editor. Error: %v", err), fmt.Sprintf("Couldn't open editor. Error: %v", err),
) )
return return
} }
if !valid { if len(text) == 0 {
return return
} }
p.list.SetItemText(p.list.GetCurrentItem(), text, "") 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.") p.tutView.ShowError("You can have a maximum of four fields.")
return 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't add name. Error: %v\n", err), fmt.Sprintf("Couldn't add name. Error: %v\n", err),
) )
return return
} }
if !valid { if len(name) == 0 {
p.tutView.ShowError("Name can't be empty.") p.tutView.ShowError("Name can't be empty.")
return 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't add value. Error: %v\n", err), fmt.Sprintf("Couldn't add value. Error: %v\n", err),
) )
return return
} }
if !valid { if len(value) == 0 {
p.tutView.ShowError("Value can't be empty.") p.tutView.ShowError("Value can't be empty.")
return return
} }
@ -223,25 +243,45 @@ func (p *PreferenceView) EditField() {
return return
} }
curr := p.preferences.fields[index] 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't edit name. Error: %v\n", err), fmt.Sprintf("Couldn't edit name. Error: %v\n", err),
) )
return return
} }
if !valid { if len(name) == 0 {
p.tutView.ShowError("Name can't be empty.") p.tutView.ShowError("Name can't be empty.")
return 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't edit value. Error: %v\n", err), fmt.Sprintf("Couldn't edit value. Error: %v\n", err),
) )
return return
} }
if !valid { if len(value) == 0 {
p.tutView.ShowError("Value can't be empty.") p.tutView.ShowError("Value can't be empty.")
return return
} }
@ -268,7 +308,17 @@ func (p *PreferenceView) DeleteField() {
func (p *PreferenceView) EditBio() { func (p *PreferenceView) EditBio() {
bio := p.preferences.bio 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't edit bio. Error: %v\n", err), fmt.Sprintf("Couldn't edit bio. Error: %v\n", err),
@ -281,7 +331,17 @@ func (p *PreferenceView) EditBio() {
func (p *PreferenceView) EditDisplayname() { func (p *PreferenceView) EditDisplayname() {
dn := p.preferences.displayname 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 { if err != nil {
p.tutView.ShowError( p.tutView.ShowError(
fmt.Sprintf("Couldn't edit display name. Error: %v\n", err), fmt.Sprintf("Couldn't edit display name. Error: %v\n", err),

3
ui/statusbar.go

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

38
ui/styled_elements.go

@ -26,6 +26,19 @@ func NewTextView(cnf *config.Config) *tview.TextView {
return tw 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 { func NewControlView(cnf *config.Config) *tview.Flex {
f := tview.NewFlex().SetDirection(tview.FlexColumn) f := tview.NewFlex().SetDirection(tview.FlexColumn)
f.SetBackgroundColor(cnf.Style.Background) f.SetBackgroundColor(cnf.Style.Background)
@ -127,3 +140,28 @@ func NewHorizontalLine(cnf *config.Config) *tview.Box {
}) })
return horizontalLine 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 { type FeedHolder struct {
Name string
Feeds []*Feed Feeds []*Feed
FeedIndex int FeedIndex int
} }
@ -21,31 +20,47 @@ type Timeline struct {
scrollSleep *scrollSleep 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 var nf *Feed
switch ft { switch f.FeedType {
case config.TimelineHome: case config.TimelineHome:
nf = NewHomeFeed(tv, showBoosts, showReplies) nf = NewHomeFeed(tv, f)
case config.TimelineHomeSpecial: case config.TimelineHomeSpecial:
nf = NewHomeSpecialFeed(tv, showBoosts, showReplies) nf = NewHomeSpecialFeed(tv, f)
case config.Conversations: case config.Conversations:
nf = NewConversationsFeed(tv) nf = NewConversationsFeed(tv, f)
case config.TimelineLocal: case config.TimelineLocal:
nf = NewLocalFeed(tv, showBoosts, showReplies) nf = NewLocalFeed(tv, f)
case config.TimelineFederated: case config.TimelineFederated:
nf = NewFederatedFeed(tv, showBoosts, showReplies) nf = NewFederatedFeed(tv, f)
case config.Saved: case config.Saved:
nf = NewBookmarksFeed(tv) nf = NewBookmarksFeed(tv, f)
case config.Favorited: case config.Favorited:
nf = NewFavoritedFeed(tv) nf = NewFavoritedFeed(tv, f)
case config.Notifications: case config.Notifications:
nf = NewNotificationFeed(tv, showBoosts, showReplies) nf = NewNotificationFeed(tv, f)
case config.Mentions: case config.Mentions:
nf = NewNotificatioMentionsFeed(tv, showBoosts, showReplies) nf = NewNotificatioMentionsFeed(tv, f)
case config.Lists: case config.Lists:
nf = NewListsFeed(tv) nf = NewListsFeed(tv, f)
case config.Tag: case config.Tag:
nf = NewTagFeed(tv, data, showBoosts, showReplies) nf = NewTagFeed(tv, f)
default: default:
fmt.Println("Invalid feed") fmt.Println("Invalid feed")
tv.CleanExit(1) tv.CleanExit(1)
@ -61,10 +76,12 @@ func NewTimeline(tv *TutView, update chan bool) *Timeline {
} }
tl.scrollSleep = NewScrollSleep(tl.NextItemFeed, tl.PrevItemFeed) tl.scrollSleep = NewScrollSleep(tl.NextItemFeed, tl.PrevItemFeed)
for _, f := range tv.tut.Config.General.Timelines { 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{ tl.Feeds = append(tl.Feeds, &FeedHolder{
Feeds: []*Feed{nf}, Feeds: []*Feed{nf},
Name: f.Name,
}) })
} }
for i := 1; i < len(tl.Feeds); i++ { for i := 1; i < len(tl.Feeds); i++ {
@ -76,10 +93,75 @@ func NewTimeline(tv *TutView, update chan bool) *Timeline {
return tl return tl
} }
func (tl *Timeline) AddFeed(f *Feed) { func (tl *Timeline) AddFeed(f *Feed, newPane bool) {
fh := tl.Feeds[tl.FeedFocusIndex] if f.Timeline.Name == "" && tl.tutView.tut.Config.General.DynamicTimelineName {
fh.Feeds = append(fh.Feeds, f) name := f.Data.Name()
fh.FeedIndex = fh.FeedIndex + 1 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.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true tl.update <- true
} }
@ -106,7 +188,7 @@ func (tl *Timeline) RemoveCurrent(quit bool) bool {
return false return false
} }
func (tl *Timeline) MoveCurrentWindowLeft() { func (tl *Timeline) MoveCurrentPaneLeft() {
length := len(tl.Feeds) length := len(tl.Feeds)
if length < 2 { if length < 2 {
return return
@ -116,10 +198,10 @@ func (tl *Timeline) MoveCurrentWindowLeft() {
return return
} }
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex] 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) length := len(tl.Feeds)
if length < 2 { if length < 2 {
return return
@ -129,31 +211,31 @@ func (tl *Timeline) MoveCurrentWindowRight() {
return return
} }
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex] 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) length := len(tl.Feeds)
if length < 2 { if length < 2 {
return return
} }
ni := 0 ni := 0
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex] 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) length := len(tl.Feeds)
if length < 2 { if length < 2 {
return return
} }
ni := len(tl.Feeds) - 1 ni := len(tl.Feeds) - 1
tl.Feeds[tl.FeedFocusIndex], tl.Feeds[ni] = tl.Feeds[ni], tl.Feeds[tl.FeedFocusIndex] 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() { func (tl *Timeline) CloseCurrentPane() {
if len(tl.Feeds) == 0 { if len(tl.Feeds) < 2 {
return return
} }
feeds := tl.Feeds[tl.FeedFocusIndex] feeds := tl.Feeds[tl.FeedFocusIndex]
@ -165,7 +247,7 @@ func (tl *Timeline) CloseCurrentWindow() {
if ni < 0 { if ni < 0 {
ni = 0 ni = 0
} }
tl.tutView.FocusFeed(ni) tl.tutView.FocusFeed(ni, nil)
} }
func (tl *Timeline) NextFeed() { func (tl *Timeline) NextFeed() {
@ -191,14 +273,14 @@ func (tl *Timeline) PrevFeed() {
tl.update <- true 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 i, fh := range tl.Feeds {
for j, f := range fh.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 { if ft == config.Tag && f.Data.Name() != data {
continue continue
} }
tl.tutView.FocusFeed(i) tl.tutView.FocusFeed(i, nil)
fh.FeedIndex = j fh.FeedIndex = j
tl.tutView.Shared.Top.SetText(tl.GetTitle()) tl.tutView.Shared.Top.SetText(tl.GetTitle())
tl.update <- true tl.update <- true
@ -244,11 +326,11 @@ func (tl *Timeline) GetTitle() string {
for i, p := range parts { for i, p := range parts {
parts[i] = fmt.Sprintf("#%s", p) parts[i] = fmt.Sprintf("#%s", p)
} }
ct = fmt.Sprintf("tag %s", strings.Join(parts, " ")) ct = strings.Join(parts, " ")
case config.Thread: case config.Thread:
ct = "thread feed" ct = "thread"
case config.History: case config.History:
ct = "history feed" ct = "history"
case config.TimelineFederated: case config.TimelineFederated:
ct = "federated" ct = "federated"
case config.TimelineHome: case config.TimelineHome:
@ -258,7 +340,7 @@ func (tl *Timeline) GetTitle() string {
case config.TimelineLocal: case config.TimelineLocal:
ct = "local" ct = "local"
case config.Saved: case config.Saved:
ct = "saved/bookmarked toots" ct = "bookmarked"
case config.User: case config.User:
ct = fmt.Sprintf("user %s", name) ct = fmt.Sprintf("user %s", name)
case config.UserList: case config.UserList:
@ -268,7 +350,7 @@ func (tl *Timeline) GetTitle() string {
case config.Lists: case config.Lists:
ct = "lists" ct = "lists"
case config.List: case config.List:
ct = fmt.Sprintf("list named %s", name) ct = fmt.Sprintf("list %s", name)
case config.Boosts: case config.Boosts:
ct = "boosts" ct = "boosts"
case config.Favorites: case config.Favorites:

4
ui/top.go

@ -25,7 +25,7 @@ func NewTop(tv *TutView) *Top {
} }
func (t *Top) SetText(s string) { 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 acct := t.TutView.tut.Client.Me
us := acct.Acct us := acct.Acct
u, err := url.Parse(acct.URL) u, err := url.Parse(acct.URL)
@ -48,7 +48,7 @@ func (t *Top) SetText(s string) {
func (t *Top) setText(s string) { func (t *Top) setText(s string) {
t.View.SetText(s) 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) util.SetTerminalTitle(s)
} }
} }

96
ui/tutview.go

@ -35,6 +35,22 @@ type Tut struct {
Config *config.Config 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 { type TutView struct {
tut *Tut tut *Tut
Timeline *Timeline Timeline *Timeline
@ -53,10 +69,14 @@ type TutView struct {
PollView *PollView PollView *PollView
PreferenceView *PreferenceView PreferenceView *PreferenceView
HelpView *HelpView HelpView *HelpView
EditorView *EditorView
ModalView *ModalView ModalView *ModalView
FileList []string
} }
func (tv *TutView) CleanExit(code int) { func (tv *TutView) CleanExit(code int) {
tv.ClearTemp()
os.Exit(code) os.Exit(code)
} }
@ -95,10 +115,19 @@ func (l *Leader) Content() string {
return l.content 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{ tv := &TutView{
tut: t, tut: &Tut{
View: tview.NewPages(), Client: &api.AccountClient{},
App: App,
Config: Config,
},
View: tview.NewPages(),
FileList: []string{},
} }
tv.Leader = NewLeader(tv) tv.Leader = NewLeader(tv)
tv.Shared = NewShared(tv) tv.Shared = NewShared(tv)
@ -131,7 +160,52 @@ func NewTutView(t *Tut, accs *auth.AccountData, selectedUser string) *TutView {
} else { } else {
tv.loggedIn(accs.Accounts[0]) 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) { func (tv *TutView) loggedIn(acc auth.Account) {
@ -181,6 +255,7 @@ func (tv *TutView) loggedIn(acc auth.Account) {
tv.PollView = NewPollView(tv) tv.PollView = NewPollView(tv)
tv.PreferenceView = NewPreferenceView(tv) tv.PreferenceView = NewPreferenceView(tv)
tv.HelpView = NewHelpView(tv) tv.HelpView = NewHelpView(tv)
tv.EditorView = NewEditorView(tv)
tv.ModalView = NewModalView(tv) tv.ModalView = NewModalView(tv)
tv.View.AddPage("main", tv.MainView.View, true, false) 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("compose", tv.ComposeView.View, true, false)
tv.View.AddPage("vote", tv.VoteView.View, true, false) tv.View.AddPage("vote", tv.VoteView.View, true, false)
tv.View.AddPage("help", tv.HelpView.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("poll", tv.PollView.View, true, false)
tv.View.AddPage("preference", tv.PreferenceView.View, true, false) tv.View.AddPage("preference", tv.PreferenceView.View, true, false)
tv.View.AddPage("modal", tv.ModalView.View, true, false) tv.View.AddPage("modal", tv.ModalView.View, true, false)
tv.SetPage(MainFocus) 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) { if index < 0 || index >= len(tv.Timeline.Feeds) {
return 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.Shared.Top.SetText(tv.Timeline.GetTitle())
tv.Timeline.update <- true tv.Timeline.update <- true
} }
@ -219,7 +301,7 @@ func (tv *TutView) NextFeed() {
if index >= len(tv.Timeline.Feeds) { if index >= len(tv.Timeline.Feeds) {
index = 0 index = 0
} }
tv.FocusFeed(index) tv.FocusFeed(index, nil)
} }
func (tv *TutView) PrevFeed() { func (tv *TutView) PrevFeed() {
@ -227,5 +309,5 @@ func (tv *TutView) PrevFeed() {
if index < 0 { if index < 0 {
index = len(tv.Timeline.Feeds) - 1 index = len(tv.Timeline.Feeds) - 1
} }
tv.FocusFeed(index) tv.FocusFeed(index, nil)
} }

9
ui/view.go

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

39
util/util.go

@ -11,6 +11,7 @@ import (
"github.com/RasmusLindroth/go-mastodon" "github.com/RasmusLindroth/go-mastodon"
"github.com/adrg/xdg" "github.com/adrg/xdg"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/rivo/tview"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@ -53,18 +54,48 @@ type URL struct {
} }
func CleanHTML(content string) (string, []URL) { 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) 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, "<br/>", "\n") stripped = strings.ReplaceAll(stripped, "<br/>", "\n")
stripped = strings.ReplaceAll(stripped, "<p>", "") stripped = strings.ReplaceAll(stripped, "<p>", "")
stripped = strings.ReplaceAll(stripped, "</p>", "\n\n") 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 = strings.TrimSpace(stripped)
stripped = html.UnescapeString(stripped) stripped = html.UnescapeString(stripped)
return stripped, urls 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 { func getURLs(text string) []URL {
doc := html.NewTokenizer(strings.NewReader(text)) doc := html.NewTokenizer(strings.NewReader(text))
var urls []URL var urls []URL
@ -209,3 +240,7 @@ func StatusOrReblog(s *mastodon.Status) *mastodon.Status {
func SetTerminalTitle(s string) { func SetTerminalTitle(s string) {
fmt.Printf("\033]0;%s\a", s) 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 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) { 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