feat(config): generate docs/config.md from the config structs (#842)
This commit is contained in:
parent
534c4e3c64
commit
84835018c9
12 changed files with 1743 additions and 721 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -26,6 +26,12 @@ jobs:
|
|||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Check that docs/config.md is up to date
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
run: |
|
||||
go generate ./internal/config
|
||||
git diff --exit-code docs/config.md
|
||||
|
||||
- name: Test
|
||||
if: ${{ matrix.os != 'windows-11-arm' }}
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ The configuration file allows you to configure and customize the behavior, keybi
|
|||
- Darwin: `$HOME/Library/Application Support/discordo/config.toml`
|
||||
- Windows: `%AppData%/discordo/config.toml`
|
||||
|
||||
Discordo uses the default configuration if a configuration file is not found in the aforementioned path; however, the default configuration file is not written to the path. [The default configuration can be found here](./internal/config/config.toml).
|
||||
Discordo uses the default configuration if a configuration file is not found in the aforementioned path; however, the default configuration file is not written to the path. [The configuration reference can be found here](./docs/config.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
1037
docs/config.md
Normal file
1037
docs/config.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"mime"
|
||||
|
|
@ -58,30 +57,41 @@ func (types MIMETypes) Has(mediaType string) bool {
|
|||
|
||||
type (
|
||||
Timestamps struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Format string `toml:"format"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
// https://pkg.go.dev/time#Layout
|
||||
Format string `toml:"format"`
|
||||
}
|
||||
|
||||
AttachmentsConfig struct {
|
||||
ShowLinks bool `toml:"show_links"`
|
||||
ShowLinks bool `toml:"show_links"`
|
||||
// Attachment MIME types that can be opened or downloaded without confirmation.
|
||||
// Exact types and wildcards such as "image/*" are supported.
|
||||
AllowedMIMETypes MIMETypes `toml:"allowed_mime_types"`
|
||||
Preview bool `toml:"preview"`
|
||||
Protocol string `toml:"protocol"`
|
||||
// Whether to draw image attachments and embedded images below their messages.
|
||||
Preview bool `toml:"preview"`
|
||||
// How previews are drawn: "auto", "kitty" or "halfblocks".
|
||||
Protocol string `toml:"protocol"`
|
||||
}
|
||||
|
||||
DateSeparator struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Format string `toml:"format"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
// https://pkg.go.dev/time#Layout
|
||||
Format string `toml:"format"`
|
||||
// The fill character used on both sides of the date label.
|
||||
Character string `toml:"character"`
|
||||
}
|
||||
|
||||
Notifications struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
// Whether to also notify about messages in the open channel while the terminal window is unfocused.
|
||||
// Requires a terminal that reports focus.
|
||||
WhenUnfocused bool `toml:"when_unfocused"`
|
||||
}
|
||||
|
||||
TypingIndicator struct {
|
||||
Send bool `toml:"send"`
|
||||
// Whether to send typing status.
|
||||
Send bool `toml:"send"`
|
||||
// Whether to receive typing status.
|
||||
Receive bool `toml:"receive"`
|
||||
}
|
||||
|
||||
|
|
@ -106,21 +116,27 @@ type (
|
|||
}
|
||||
|
||||
MarkdownConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MaskSpoilers bool `toml:"mask_spoilers"`
|
||||
Theme string `toml:"theme"`
|
||||
// Whether to parse and render markdown in messages.
|
||||
Enabled bool `toml:"enabled"`
|
||||
// Replace spoiler contents with a placeholder.
|
||||
MaskSpoilers bool `toml:"mask_spoilers"`
|
||||
// Theme for fenced code blocks. Available themes: https://xyproto.github.io/splash/docs
|
||||
Theme string `toml:"theme"`
|
||||
}
|
||||
|
||||
HelpConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
CompactModifiers bool `toml:"compact_modifiers"`
|
||||
Padding [2]int `toml:"padding"`
|
||||
Separator string `toml:"separator"`
|
||||
// Whether to show keybind help.
|
||||
Enabled bool `toml:"enabled"`
|
||||
// Show compact key modifiers in help, e.g. "^x" instead of "ctrl+x".
|
||||
CompactModifiers bool `toml:"compact_modifiers"`
|
||||
// [left, right]
|
||||
Padding [2]int `toml:"padding"`
|
||||
Separator string `toml:"separator"`
|
||||
}
|
||||
|
||||
ComposerConfig struct {
|
||||
// MaxHeight caps how tall (in newline-separated rows) the input grows before it starts scrolling internally.
|
||||
// Must be >= 1; values <= 0 fall back to the default.
|
||||
// Maximum height (in newline-separated rows) the composer grows to before it starts scrolling internally.
|
||||
// Set to 1 for a fixed single-line input. Values below 1 fall back to 10.
|
||||
MaxHeight int `toml:"max_height"`
|
||||
}
|
||||
|
||||
|
|
@ -141,29 +157,37 @@ type (
|
|||
|
||||
// PaneConfig is a side pane that can be toggled at runtime.
|
||||
PaneConfig struct {
|
||||
// Visible controls whether the pane is shown by default at start-up.
|
||||
// Whether the pane is visible at start-up. It can still be toggled at runtime with its toggle keybind.
|
||||
Visible bool `toml:"visible"`
|
||||
// WidthPercent is the percentage of the total window width that the pane occupies.
|
||||
// Percentage (%) of the available width used by the pane.
|
||||
// Valid values are 1-99. Invalid values fall back to 20.
|
||||
WidthPercent int `toml:"width_percent"`
|
||||
}
|
||||
|
||||
SidebarConfig struct {
|
||||
PaneConfig
|
||||
Markers SidebarMarkersConfig `toml:"markers"`
|
||||
// Indentation for different item types.
|
||||
Indents SidebarIndentsConfig `toml:"indents"`
|
||||
}
|
||||
|
||||
Config struct {
|
||||
AutoFocus bool `toml:"auto_focus"`
|
||||
Mouse bool `toml:"mouse"`
|
||||
Editor string `toml:"editor"`
|
||||
// Whether to focus the composer automatically when a channel is selected.
|
||||
// Set to false to preview channels without moving focus.
|
||||
AutoFocus bool `toml:"auto_focus"`
|
||||
// Whether to enable the mouse.
|
||||
Mouse bool `toml:"mouse"`
|
||||
// The program opened by the `composer.open_editor` keybind. "default" uses `$EDITOR`.
|
||||
Editor string `toml:"editor"`
|
||||
|
||||
// "default" (unknown), "online", "dnd", "idle", "invisible" or "offline".
|
||||
Status discord.Status `toml:"status"`
|
||||
HideBlockedUsers bool `toml:"hide_blocked_users"`
|
||||
|
||||
// Use 0 to disable
|
||||
// The maximum number of members in the mention autocomplete list. 0 disables it.
|
||||
AutocompleteLimit uint8 `toml:"autocomplete_limit"`
|
||||
MessagesLimit uint8 `toml:"messages_limit"`
|
||||
// The number of messages to fetch when a text-based channel is selected, from 1 to 100.
|
||||
MessagesLimit uint8 `toml:"messages_limit"`
|
||||
|
||||
Attachments AttachmentsConfig `toml:"attachments"`
|
||||
|
||||
|
|
@ -174,20 +198,81 @@ type (
|
|||
DateSeparator DateSeparator `toml:"date_separator"`
|
||||
Notifications Notifications `toml:"notifications"`
|
||||
TypingIndicator TypingIndicator `toml:"typing_indicator"`
|
||||
Sidebar SidebarConfig `toml:"sidebar"`
|
||||
MembersTree PaneConfig `toml:"members_tree"`
|
||||
Composer ComposerConfig `toml:"composer"`
|
||||
// The guilds tree pane.
|
||||
Sidebar SidebarConfig `toml:"sidebar"`
|
||||
// The members tree pane, sized from the width right of the guilds tree.
|
||||
MembersTree PaneConfig `toml:"members_tree"`
|
||||
Composer ComposerConfig `toml:"composer"`
|
||||
|
||||
// Layout and behavior of the widgets.
|
||||
UI UIConfig `toml:"ui"`
|
||||
Icons Icons `toml:"icons"`
|
||||
|
||||
// Global shortcuts. A keybind is a key such as "ctrl+k", or a list of keys.
|
||||
Keybinds Keybinds `toml:"keybinds"`
|
||||
Theme Theme `toml:"theme"`
|
||||
// Styles of the widgets, each written as
|
||||
// `{ foreground = "", background = "", attributes = "" or ["", ""], underline = "", underline_color = "" }`.
|
||||
Theme ThemeConfig `toml:"theme"`
|
||||
}
|
||||
)
|
||||
|
||||
//go:embed config.toml
|
||||
var defaultCfg []byte
|
||||
//go:generate go run gen.go
|
||||
|
||||
// Default returns the default configuration.
|
||||
func Default() Config {
|
||||
pane := PaneConfig{Visible: true, WidthPercent: 20}
|
||||
return Config{
|
||||
AutoFocus: true,
|
||||
Mouse: true,
|
||||
Editor: "default",
|
||||
|
||||
Status: "default",
|
||||
HideBlockedUsers: true,
|
||||
|
||||
AutocompleteLimit: 20,
|
||||
MessagesLimit: 50,
|
||||
|
||||
Attachments: AttachmentsConfig{
|
||||
ShowLinks: true,
|
||||
AllowedMIMETypes: MIMETypes{"image/*", "video/*"},
|
||||
Preview: true,
|
||||
Protocol: "auto",
|
||||
},
|
||||
|
||||
Markdown: MarkdownConfig{Enabled: true, Theme: "monokai"},
|
||||
Help: HelpConfig{Enabled: true, CompactModifiers: true, Padding: [2]int{1, 1}, Separator: " • "},
|
||||
Picker: PickerConfig{Width: 80, Height: 25},
|
||||
Timestamps: Timestamps{Enabled: true, Format: "3:04PM"},
|
||||
DateSeparator: DateSeparator{Enabled: true, Format: "January 2, 2006", Character: "─"},
|
||||
Notifications: Notifications{Enabled: true, WhenUnfocused: true},
|
||||
TypingIndicator: TypingIndicator{Send: true, Receive: true},
|
||||
Sidebar: SidebarConfig{
|
||||
PaneConfig: pane,
|
||||
Markers: SidebarMarkersConfig{Expanded: "▾ ", Collapsed: "▸ "},
|
||||
Indents: SidebarIndentsConfig{Guild: 2, Category: 1, Channel: 2, Forum: 2, GroupDM: 1, DM: 2},
|
||||
},
|
||||
MembersTree: pane,
|
||||
Composer: ComposerConfig{MaxHeight: 10},
|
||||
|
||||
UI: defaultUI(),
|
||||
Icons: Icons{
|
||||
GuildText: "#",
|
||||
GuildVoice: "♪ ",
|
||||
GuildStageVoice: "♪ ",
|
||||
|
||||
GuildAnnouncementThread: "a-",
|
||||
GuildPublicThread: "› ",
|
||||
GuildPrivateThread: "› ",
|
||||
|
||||
GuildAnnouncement: "a-",
|
||||
GuildForum: "≡ ",
|
||||
GuildStore: "s-",
|
||||
},
|
||||
|
||||
Keybinds: defaultKeybinds(),
|
||||
Theme: defaultTheme(),
|
||||
}
|
||||
}
|
||||
|
||||
var dir = sync.OnceValue(func() string {
|
||||
path, err := os.UserConfigDir()
|
||||
|
|
@ -204,13 +289,7 @@ func DefaultPath() string {
|
|||
|
||||
// Load reads the configuration file and parses it.
|
||||
func Load(path string) (*Config, error) {
|
||||
cfg := Config{
|
||||
Keybinds: defaultKeybinds(),
|
||||
}
|
||||
if err := toml.Unmarshal(defaultCfg, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal default config: %w", err)
|
||||
}
|
||||
|
||||
cfg := Default()
|
||||
file, err := os.Open(path)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
|
|
|
|||
|
|
@ -1,370 +0,0 @@
|
|||
# Whether to focus the composer automatically when a channel is selected.
|
||||
# Set to false to preview channels without moving focus.
|
||||
auto_focus = true
|
||||
|
||||
# Whether to enable mouse or not.
|
||||
mouse = true
|
||||
# The program to open when the `composer.open_editor` keybind is pressed. Set the value to `"default"` to use `$EDITOR`.
|
||||
editor = "default"
|
||||
|
||||
# "default" (unknown), "online", "dnd", "idle", "invisible", "offline"
|
||||
status = "default"
|
||||
|
||||
hide_blocked_users = true
|
||||
|
||||
# Max members to be in the mention autocomplete suggestions list
|
||||
# Note: Use autocomplete_limit = 0 to disable.
|
||||
autocomplete_limit = 20
|
||||
|
||||
# The number of messages to fetch when a text-based channel is selected from guilds tree. The minimum and maximum value is 1 and 100, respectively.
|
||||
messages_limit = 50
|
||||
|
||||
[attachments]
|
||||
show_links = true
|
||||
# Attachment MIME types that can be opened or downloaded without confirmation.
|
||||
# Exact types and wildcards such as "image/*" are supported.
|
||||
allowed_mime_types = ["image/*", "video/*"]
|
||||
# Whether to draw image attachments and embedded images below their messages.
|
||||
preview = true
|
||||
# How previews are drawn: "auto", "kitty" or "halfblocks".
|
||||
protocol = "auto"
|
||||
|
||||
[composer]
|
||||
# Maximum height (in newline-separated rows) the composer grows to before it starts scrolling internally.
|
||||
# Set to 1 for a fixed single-line input.
|
||||
max_height = 10
|
||||
|
||||
[markdown]
|
||||
# Whether to parse and render markdown in messages or not.
|
||||
enabled = true
|
||||
# Replace spoiler contents with a placeholder.
|
||||
mask_spoilers = false
|
||||
# Theme for fenced code blocks. Available themes: https://xyproto.github.io/splash/docs
|
||||
theme = "monokai"
|
||||
|
||||
[help]
|
||||
# Whether to show keybind help.
|
||||
enabled = true
|
||||
# Show compact key modifiers in help, e.g. "^x" instead of "ctrl+x".
|
||||
compact_modifiers = true
|
||||
# [left, right]
|
||||
padding = [1, 1]
|
||||
separator = " • "
|
||||
|
||||
[picker]
|
||||
width = 80
|
||||
height = 25
|
||||
|
||||
[ui.tabs]
|
||||
# Where tabs that fit are placed: "left", "center" or "right".
|
||||
alignment = "center"
|
||||
# Whether moving past the last tab selects the first, and past the first selects the last.
|
||||
wrap = false
|
||||
# Text between tabs.
|
||||
separator = "│"
|
||||
# Text on either side of each tab: [left, right]
|
||||
padding = [" ", " "]
|
||||
# Arrows shown while tabs are hidden past an end: [start, end]. An empty string turns that arrow off.
|
||||
arrows = ["◀", "▶"]
|
||||
# Whether clicking an arrow selects the tab next to the active one.
|
||||
clickable_arrows = true
|
||||
|
||||
[ui.title]
|
||||
# "left", "center" or "right".
|
||||
alignment = "left"
|
||||
|
||||
[ui.footer]
|
||||
alignment = "left"
|
||||
|
||||
[ui.border]
|
||||
enabled = true
|
||||
padding = [0, 0, 1, 1] # [top, bottom, left, right]
|
||||
# "hidden", "plain", "round", "thick" or "double".
|
||||
normal_set = "round"
|
||||
active_set = "round"
|
||||
|
||||
[ui.guilds_tree]
|
||||
auto_expand_folders = true
|
||||
# Give tree-like shape
|
||||
graphics = true
|
||||
|
||||
[ui.members_tree]
|
||||
# Give tree-like shape
|
||||
graphics = true
|
||||
|
||||
[ui.scroll_bar]
|
||||
visibility = "auto"
|
||||
# "minimal", "box_drawing", "vertical" or "double_vertical"
|
||||
symbol_set = "box_drawing"
|
||||
|
||||
[ui.messages_list]
|
||||
reply_indicator = ">"
|
||||
forwarded_indicator = "<"
|
||||
|
||||
[ui.mentions_list]
|
||||
# Note: width and height are capped to the available space
|
||||
# Minimum width
|
||||
# 0 = make the list as wide as possible
|
||||
min_width = 20
|
||||
# Maximum height
|
||||
# 0 = make the list as tall as needed
|
||||
max_height = 0
|
||||
|
||||
[timestamps]
|
||||
enabled = true
|
||||
# https://pkg.go.dev/time#Layout
|
||||
format = "3:04PM"
|
||||
|
||||
[date_separator]
|
||||
enabled = true
|
||||
# https://pkg.go.dev/time#Layout
|
||||
format = "January 2, 2006"
|
||||
# The fill character used on both sides of the date label.
|
||||
character = "─"
|
||||
|
||||
[notifications]
|
||||
enabled = true
|
||||
# Whether to also notify about messages in the open channel while the terminal window is unfocused. Requires a terminal that reports focus.
|
||||
when_unfocused = true
|
||||
|
||||
[typing_indicator]
|
||||
# Whether to send typing status or not.
|
||||
send = true
|
||||
# Whether to receive typing status or not.
|
||||
receive = true
|
||||
|
||||
[sidebar]
|
||||
# Whether the guilds tree sidebar is visible by default at start-up.
|
||||
# It can still be toggled at runtime with the `toggle_guilds_tree` keybind.
|
||||
visible = true
|
||||
# Percentage (%) of the available width used by the guilds tree sidebar.
|
||||
# Valid values are 1-99. Invalid values fall back to 20.
|
||||
width_percent = 20
|
||||
|
||||
[sidebar.markers]
|
||||
expanded = "▾ "
|
||||
collapsed = "▸ "
|
||||
leaf = ""
|
||||
|
||||
# Indentation for different item types.
|
||||
[sidebar.indents]
|
||||
dm = 2
|
||||
group_dm = 1
|
||||
guild = 2
|
||||
category = 1
|
||||
channel = 2
|
||||
forum = 2
|
||||
|
||||
[members_tree]
|
||||
# Whether the members tree is visible by default at start-up.
|
||||
# It can still be toggled at runtime with the `toggle_members_tree` keybind.
|
||||
visible = true
|
||||
# Percentage (%) of the width right of the guilds tree used by the members tree.
|
||||
# Valid values are 1-99. Invalid values fall back to 20.
|
||||
width_percent = 20
|
||||
|
||||
[icons]
|
||||
guild_category = ""
|
||||
guild_text = "#"
|
||||
guild_voice = "♪ "
|
||||
guild_stage_voice = "♪ "
|
||||
guild_announcement_thread = "a-"
|
||||
guild_public_thread = "› "
|
||||
guild_private_thread = "› "
|
||||
guild_announcement = "a-"
|
||||
guild_forum = "≡ "
|
||||
guild_store = "s-"
|
||||
|
||||
# Global shortcuts
|
||||
# Esc: Reset message selection or close the channel selection popup.
|
||||
[keybinds]
|
||||
# Hide/show the guilds tree.
|
||||
toggle_guilds_tree = "alt+g"
|
||||
# Hide/show the members tree.
|
||||
toggle_members_tree = "alt+m"
|
||||
toggle_channels_picker = "ctrl+k"
|
||||
# Hide/show keybind help.
|
||||
toggle_help = "alt+."
|
||||
# Switch between compact and full keybind help.
|
||||
toggle_full_help = "ctrl+."
|
||||
focus_guilds_tree = "ctrl+g"
|
||||
focus_members_tree = "ctrl+y"
|
||||
focus_messages_list = "ctrl+t"
|
||||
focus_composer = "ctrl+i"
|
||||
# Cycle focus between the widgets.
|
||||
focus_previous = "ctrl+h"
|
||||
focus_next = "ctrl+l"
|
||||
# Log out and remove the authentication token from keyring.
|
||||
# Requires re-login upon restart.
|
||||
logout = "ctrl+d"
|
||||
suspend = "ctrl+z"
|
||||
quit = "ctrl+c"
|
||||
|
||||
[keybinds.picker]
|
||||
cancel = "esc"
|
||||
select_up = "ctrl+p"
|
||||
select_down = "ctrl+n"
|
||||
select_top = "home"
|
||||
select_bottom = "end"
|
||||
select = "enter"
|
||||
|
||||
# Only while focusing on the guilds tree
|
||||
[keybinds.guilds_tree]
|
||||
select_up = "k"
|
||||
select_down = "j"
|
||||
select_top = "g"
|
||||
select_bottom = "G"
|
||||
# Select the currently highlighted text-based channel or expand a guild or channel.
|
||||
select_current = "enter"
|
||||
# Highlight the previous or next unread guild or channel, wrapping around.
|
||||
select_previous_unread = "K"
|
||||
select_next_unread = "J"
|
||||
yank_id = "i"
|
||||
collapse_all = "_"
|
||||
collapse_parent_node = "-"
|
||||
move_to_parent_node = "p"
|
||||
|
||||
# Only while focusing on the members tree
|
||||
[keybinds.members_tree]
|
||||
select_up = "k"
|
||||
select_down = "j"
|
||||
select_top = "g"
|
||||
select_bottom = "G"
|
||||
# Expand or collapse the highlighted group.
|
||||
select_current = "enter"
|
||||
yank_id = "i"
|
||||
collapse_all = "_"
|
||||
collapse_parent_node = "-"
|
||||
move_to_parent_node = "p"
|
||||
|
||||
# Only while focusing on sent messages
|
||||
[keybinds.messages_list]
|
||||
select_up = "k"
|
||||
select_down = "j"
|
||||
select_top = "g"
|
||||
select_bottom = "G"
|
||||
# Scroll the messages list without changing the selection.
|
||||
scroll_up = "K"
|
||||
scroll_down = "J"
|
||||
scroll_top = "home"
|
||||
scroll_bottom = "end"
|
||||
# Select the message reference (reply) of the selected channel.
|
||||
select_reply = "s"
|
||||
# Reply to the selected message.
|
||||
reply = "R"
|
||||
# Reply (with mention) to the selected message.
|
||||
reply_mention = "r"
|
||||
cancel = "esc"
|
||||
edit = "e"
|
||||
# Delete without confirmation.
|
||||
# delete = "D"
|
||||
delete_confirm = "d"
|
||||
# Open attachments with their associated application. MIME types outside the
|
||||
# allowlist require confirmation. Hyperlinks still use the browser.
|
||||
open = "o"
|
||||
# Open attachments or hyperlinks in the browser.
|
||||
open_in_browser = "O"
|
||||
# Download one of the selected message's attachments.
|
||||
download = "D"
|
||||
# Yank (copy) the selected message's content/url/id.
|
||||
yank_content = "y"
|
||||
yank_url = "u"
|
||||
yank_id = "i"
|
||||
|
||||
# Only while typing a message
|
||||
[keybinds.composer]
|
||||
# Edit your newest editable message loaded in the current channel, with no age limit.
|
||||
# Only when the composer is empty, with no reply, attachments, or edit in progress.
|
||||
edit_last = "up"
|
||||
# Paste from clipboard (supports both text and images)
|
||||
paste = "ctrl+v"
|
||||
send = "enter"
|
||||
# Insert a new line at the cursor.
|
||||
newline = "shift+enter"
|
||||
# Remove existing text or cancel reply.
|
||||
cancel = "esc"
|
||||
# Complete usernames when mentioning
|
||||
tab_complete = "tab"
|
||||
# Toggle whether a reply mentions its author.
|
||||
toggle_reply_mention = "shift+tab"
|
||||
undo = "ctrl+u"
|
||||
|
||||
open_editor = "ctrl+e"
|
||||
open_file_picker = "ctrl+\\"
|
||||
|
||||
[keybinds.mentions_list]
|
||||
select_up = "ctrl+p"
|
||||
select_down = "ctrl+n"
|
||||
select_top = "home"
|
||||
select_bottom = "end"
|
||||
|
||||
# style = { foreground = "", background = "", attributes = "" or ["",""], underline = "", underline_color = "" }
|
||||
[theme.title]
|
||||
normal_style = { attributes = "dim" }
|
||||
active_style = { foreground = "green", attributes = "bold" }
|
||||
|
||||
[theme.footer]
|
||||
normal_style = { attributes = "dim" }
|
||||
active_style = { foreground = "green", attributes = "bold" }
|
||||
|
||||
[theme.border]
|
||||
normal_style = { attributes = "dim" }
|
||||
active_style = { foreground = "green", attributes = "bold" }
|
||||
|
||||
[theme.guilds_tree]
|
||||
graphics_color = "default"
|
||||
|
||||
# Direct-message recipient presence styles.
|
||||
online_style = { foreground = "green" }
|
||||
idle_style = { foreground = "yellow" }
|
||||
dnd_style = { foreground = "red" }
|
||||
offline_style = { foreground = "gray" }
|
||||
|
||||
[theme.members_tree]
|
||||
graphics_color = "default"
|
||||
|
||||
# Styles of the status dot before each member.
|
||||
online_style = { foreground = "green" }
|
||||
idle_style = { foreground = "yellow" }
|
||||
dnd_style = { foreground = "red" }
|
||||
offline_style = { foreground = "gray" }
|
||||
|
||||
[theme.scroll_bar]
|
||||
track_style = { attributes = "dim" }
|
||||
thumb_style = {}
|
||||
|
||||
[theme.messages_list]
|
||||
mention_style = { foreground = "blue", attributes = "bold" }
|
||||
emoji_style = { foreground = "green" }
|
||||
url_style = { foreground = "blue" }
|
||||
attachment_style = { foreground = "yellow" }
|
||||
|
||||
reaction_style = { attributes = "dim" }
|
||||
own_reaction_style = { attributes = "bold" }
|
||||
|
||||
message_style = {}
|
||||
selected_message_style = { attributes = "reverse" }
|
||||
|
||||
[theme.messages_list.embeds]
|
||||
provider_style = { attributes = ["dim", "italic"] }
|
||||
author_style = { attributes = "italic" }
|
||||
title_style = { foreground = "blue", attributes = "bold" }
|
||||
description_style = { attributes = "dim" }
|
||||
field_name_style = { attributes = ["bold", "underline"] }
|
||||
field_value_style = {}
|
||||
footer_style = { attributes = ["dim", "italic"] }
|
||||
url_style = { foreground = "blue", underline = "solid" }
|
||||
|
||||
[theme.mentions_list]
|
||||
selected_style = { attributes = "reverse" }
|
||||
|
||||
[theme.dialog]
|
||||
style = {}
|
||||
# Background style: everything else behind the dialog
|
||||
background_style = { attributes = "dim" }
|
||||
|
||||
[theme.help]
|
||||
short_key_style = { attributes = "dim" }
|
||||
short_desc_style = {}
|
||||
full_key_style = { attributes = "dim" }
|
||||
full_desc_style = {}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/consts"
|
||||
)
|
||||
|
||||
|
|
@ -27,15 +28,6 @@ func TestDefaultPath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
t.Run("invalid default config returns error", func(t *testing.T) {
|
||||
orig := defaultCfg
|
||||
defaultCfg = []byte("invalid =")
|
||||
t.Cleanup(func() { defaultCfg = orig })
|
||||
if _, err := Load("does-not-matter.toml"); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid config returns error", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bad.toml")
|
||||
if err := os.WriteFile(path, []byte("invalid ="), os.ModePerm); err != nil {
|
||||
|
|
@ -77,41 +69,6 @@ func TestLoad(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
// Keys live in config.toml, descriptions in defaultKeybinds(); fail if either is missing.
|
||||
t.Run("default keybinds are fully populated", func(t *testing.T) {
|
||||
cfg, err := Load(filepath.Join(t.TempDir(), "missing.toml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var walk func(path string, v reflect.Value)
|
||||
walk = func(path string, v reflect.Value) {
|
||||
rt := v.Type()
|
||||
for i := range rt.NumField() {
|
||||
fv, name := v.Field(i), path+rt.Field(i).Name
|
||||
if kb, ok := fv.Interface().(Keybind); ok {
|
||||
if len(kb.Keys()) == 0 {
|
||||
if rt.Field(i).Tag.Get("toml") != "delete" {
|
||||
t.Errorf("%s: no keys (missing from config.toml?)", name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if kb.Help().Desc == "" {
|
||||
t.Errorf("%s: no help description (missing from defaultKeybinds?)", name)
|
||||
}
|
||||
if kb.Help().Key == "" {
|
||||
t.Errorf("%s: help key not populated from config.toml", name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if fv.Kind() == reflect.Struct {
|
||||
walk(name+".", fv)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk("Keybinds.", reflect.ValueOf(cfg.Keybinds))
|
||||
})
|
||||
|
||||
t.Run("open with bad path returns error (!= ErrNotExist)", func(t *testing.T) {
|
||||
if _, err := Load("bad\x00path"); err == nil {
|
||||
t.Fatal("expected error")
|
||||
|
|
@ -125,10 +82,7 @@ func TestLoad(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defCfg := Config{Keybinds: defaultKeybinds()}
|
||||
if err := toml.Unmarshal(defaultCfg, &defCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defCfg := Default()
|
||||
defCfg.applyDefaults()
|
||||
|
||||
if !reflect.DeepEqual(defCfg, *cfg) {
|
||||
|
|
@ -137,6 +91,22 @@ func TestLoad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// The generated docs show defaults as they are marshaled, so those must parse back to the same defaults.
|
||||
func TestDefaultRoundTrip(t *testing.T) {
|
||||
data, err := toml.Marshal(Default())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Help descriptions are not part of the TOML.
|
||||
got := Config{Keybinds: defaultKeybinds()}
|
||||
if err := toml.Unmarshal(data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := Default(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got = %+v, want = %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMETypesHas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
110
internal/config/gen.go
Normal file
110
internal/config/gen.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//go:build ignore
|
||||
|
||||
// Command gen writes docs/config.md from the config structs: a heading per table and option, named by its toml key, with its doc comment and default value.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
)
|
||||
|
||||
const header = "<!-- GENERATED BY go generate; DO NOT EDIT. -->\n\n"
|
||||
|
||||
// docs maps "Type.Field" to the doc comment of that struct field.
|
||||
var docs = map[string]string{}
|
||||
|
||||
type field struct {
|
||||
key, doc string
|
||||
value reflect.Value
|
||||
}
|
||||
|
||||
// fields returns the fields of the struct v, with those of embedded structs in place.
|
||||
func fields(v reflect.Value) []field {
|
||||
var fs []field
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
f := t.Field(i)
|
||||
if f.Anonymous {
|
||||
fs = append(fs, fields(v.Field(i))...)
|
||||
continue
|
||||
}
|
||||
fs = append(fs, field{f.Tag.Get("toml"), docs[t.Name()+"."+f.Name], v.Field(i)})
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// isTable reports whether v is written as a table rather than as a value.
|
||||
func isTable(v reflect.Value) bool {
|
||||
switch v.Interface().(type) {
|
||||
case toml.Marshaler, encoding.TextMarshaler:
|
||||
return false
|
||||
}
|
||||
return v.Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// write writes a section for each option of the table v at path: its doc comment, then its default value or, for a table, its own options.
|
||||
func write(w *bytes.Buffer, path string, v reflect.Value) {
|
||||
if path != "" {
|
||||
path += "."
|
||||
}
|
||||
for _, f := range fields(v) {
|
||||
name := path + f.key
|
||||
fmt.Fprintf(w, "%s %s\n\n", strings.Repeat("#", strings.Count(name, ".")+1), name)
|
||||
if f.doc != "" {
|
||||
fmt.Fprintf(w, "%s\n", f.doc)
|
||||
}
|
||||
if isTable(f.value) {
|
||||
write(w, name, f.value)
|
||||
continue
|
||||
}
|
||||
// The encoder only writes whole documents, so the value is what follows the key.
|
||||
var line bytes.Buffer
|
||||
if err := toml.NewEncoder(&line).Encode(map[string]any{f.key: f.value.Interface()}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, value, _ := strings.Cut(line.String(), " = ")
|
||||
fmt.Fprintf(w, "Default: `%s`\n\n", strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
names, _ := filepath.Glob("*.go")
|
||||
for _, name := range names {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), name, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
spec, ok := n.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if s, ok := spec.Type.(*ast.StructType); ok {
|
||||
for _, f := range s.Fields.List {
|
||||
for _, name := range f.Names {
|
||||
docs[spec.Name.Name+"."+name.Name] = f.Doc.Text()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
w := bytes.NewBufferString(header)
|
||||
write(w, "", reflect.ValueOf(config.Default()))
|
||||
if err := os.WriteFile("../../docs/config.md", append(bytes.TrimSpace(w.Bytes()), '\n'), 0o644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
)
|
||||
|
|
@ -35,14 +38,23 @@ func (k *Keybind) UnmarshalTOML(value any) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// desc builds a Keybind with only a help description; keys come from config.toml.
|
||||
func desc(s string) Keybind {
|
||||
return Keybind{
|
||||
Keybind: keybind.New().WithHelp("", s),
|
||||
// MarshalTOML writes the keys as UnmarshalTOML reads them: one key as a string, an unbound keybind as an empty one, and several keys as a list.
|
||||
func (k Keybind) MarshalTOML() ([]byte, error) {
|
||||
if keys := k.Keys(); len(keys) > 1 {
|
||||
return json.Marshal(keys)
|
||||
}
|
||||
return json.Marshal(strings.Join(k.Keys(), ""))
|
||||
}
|
||||
|
||||
// key builds a Keybind bound to key, or unbound if key is empty, with desc as its help description.
|
||||
func key(key, desc string) Keybind {
|
||||
k := Keybind{keybind.New().WithHelp("", desc)}
|
||||
k.UnmarshalTOML(key)
|
||||
return k
|
||||
}
|
||||
|
||||
type ScrollKeybinds struct {
|
||||
// Scroll the messages list without changing the selection.
|
||||
ScrollUp Keybind `toml:"scroll_up"`
|
||||
ScrollDown Keybind `toml:"scroll_down"`
|
||||
ScrollTop Keybind `toml:"scroll_top"`
|
||||
|
|
@ -64,6 +76,7 @@ type PickerKeybinds struct {
|
|||
|
||||
type TreeKeybinds struct {
|
||||
SelectionKeybinds
|
||||
// Select the highlighted text-based channel, or expand or collapse the highlighted node.
|
||||
SelectCurrent Keybind `toml:"select_current"`
|
||||
YankID Keybind `toml:"yank_id"`
|
||||
|
||||
|
|
@ -74,6 +87,7 @@ type TreeKeybinds struct {
|
|||
|
||||
type GuildsTreeKeybinds struct {
|
||||
TreeKeybinds
|
||||
// Highlight the previous or next unread guild or channel, wrapping around.
|
||||
SelectPreviousUnread Keybind `toml:"select_previous_unread"`
|
||||
SelectNextUnread Keybind `toml:"select_next_unread"`
|
||||
}
|
||||
|
|
@ -82,32 +96,48 @@ type MessagesListKeybinds struct {
|
|||
SelectionKeybinds
|
||||
ScrollKeybinds
|
||||
|
||||
SelectReply Keybind `toml:"select_reply"`
|
||||
Reply Keybind `toml:"reply"`
|
||||
// Select the message that the selected message replies to.
|
||||
SelectReply Keybind `toml:"select_reply"`
|
||||
// Reply to the selected message.
|
||||
Reply Keybind `toml:"reply"`
|
||||
// Reply (with mention) to the selected message.
|
||||
ReplyMention Keybind `toml:"reply_mention"`
|
||||
|
||||
Cancel Keybind `toml:"cancel"`
|
||||
Edit Keybind `toml:"edit"`
|
||||
|
||||
// Delete without confirmation. Unbound by default.
|
||||
Delete Keybind `toml:"delete"`
|
||||
DeleteConfirm Keybind `toml:"delete_confirm"`
|
||||
|
||||
Open Keybind `toml:"open"`
|
||||
// Open attachments with their associated application. MIME types outside the
|
||||
// allowlist require confirmation. Hyperlinks still use the browser.
|
||||
Open Keybind `toml:"open"`
|
||||
// Open attachments or hyperlinks in the browser.
|
||||
OpenInBrowser Keybind `toml:"open_in_browser"`
|
||||
Download Keybind `toml:"download"`
|
||||
// Download one of the selected message's attachments.
|
||||
Download Keybind `toml:"download"`
|
||||
|
||||
// Yank (copy) the selected message's content/url/id.
|
||||
YankContent Keybind `toml:"yank_content"`
|
||||
YankURL Keybind `toml:"yank_url"`
|
||||
YankID Keybind `toml:"yank_id"`
|
||||
}
|
||||
|
||||
type ComposerKeybinds struct {
|
||||
EditLast Keybind `toml:"edit_last"`
|
||||
Paste Keybind `toml:"paste"`
|
||||
Send Keybind `toml:"send"`
|
||||
Newline Keybind `toml:"newline"`
|
||||
Cancel Keybind `toml:"cancel"`
|
||||
TabComplete Keybind `toml:"tab_complete"`
|
||||
// Edit your newest editable message loaded in the current channel, with no age limit.
|
||||
// Only when the composer is empty, with no reply, attachments, or edit in progress.
|
||||
EditLast Keybind `toml:"edit_last"`
|
||||
// Paste from clipboard (supports both text and images).
|
||||
Paste Keybind `toml:"paste"`
|
||||
Send Keybind `toml:"send"`
|
||||
// Insert a new line at the cursor.
|
||||
Newline Keybind `toml:"newline"`
|
||||
// Remove existing text or cancel reply.
|
||||
Cancel Keybind `toml:"cancel"`
|
||||
// Complete usernames when mentioning.
|
||||
TabComplete Keybind `toml:"tab_complete"`
|
||||
// Toggle whether a reply mentions its author.
|
||||
ToggleReplyMention Keybind `toml:"toggle_reply_mention"`
|
||||
Undo Keybind `toml:"undo"`
|
||||
|
||||
|
|
@ -120,130 +150,135 @@ type MentionsListKeybinds struct {
|
|||
}
|
||||
|
||||
type Keybinds struct {
|
||||
ToggleGuildsTree Keybind `toml:"toggle_guilds_tree"`
|
||||
// Hide/show the guilds tree.
|
||||
ToggleGuildsTree Keybind `toml:"toggle_guilds_tree"`
|
||||
// Hide/show the members tree.
|
||||
ToggleMembersTree Keybind `toml:"toggle_members_tree"`
|
||||
ToggleChannelsPicker Keybind `toml:"toggle_channels_picker"`
|
||||
ToggleHelp Keybind `toml:"toggle_help"`
|
||||
ToggleFullHelp Keybind `toml:"toggle_full_help"`
|
||||
Suspend Keybind `toml:"suspend"`
|
||||
// Hide/show keybind help.
|
||||
ToggleHelp Keybind `toml:"toggle_help"`
|
||||
// Switch between compact and full keybind help.
|
||||
ToggleFullHelp Keybind `toml:"toggle_full_help"`
|
||||
Suspend Keybind `toml:"suspend"`
|
||||
|
||||
FocusGuildsTree Keybind `toml:"focus_guilds_tree"`
|
||||
FocusMembersTree Keybind `toml:"focus_members_tree"`
|
||||
FocusMessagesList Keybind `toml:"focus_messages_list"`
|
||||
FocusComposer Keybind `toml:"focus_composer"`
|
||||
|
||||
// Cycle focus between the widgets.
|
||||
FocusPrevious Keybind `toml:"focus_previous"`
|
||||
FocusNext Keybind `toml:"focus_next"`
|
||||
|
||||
Picker PickerKeybinds `toml:"picker"`
|
||||
GuildsTree GuildsTreeKeybinds `toml:"guilds_tree"`
|
||||
MembersTree TreeKeybinds `toml:"members_tree"`
|
||||
Picker PickerKeybinds `toml:"picker"`
|
||||
// Only while focusing on the guilds tree.
|
||||
GuildsTree GuildsTreeKeybinds `toml:"guilds_tree"`
|
||||
// Only while focusing on the members tree.
|
||||
MembersTree TreeKeybinds `toml:"members_tree"`
|
||||
// Only while focusing on sent messages.
|
||||
MessagesList MessagesListKeybinds `toml:"messages_list"`
|
||||
Composer ComposerKeybinds `toml:"composer"`
|
||||
// Only while typing a message.
|
||||
Composer ComposerKeybinds `toml:"composer"`
|
||||
// Only while the mention suggestions are shown.
|
||||
MentionsList MentionsListKeybinds `toml:"mentions_list"`
|
||||
|
||||
// Log out and remove the authentication token from keyring.
|
||||
// Requires re-login upon restart.
|
||||
Logout Keybind `toml:"logout"`
|
||||
Quit Keybind `toml:"quit"`
|
||||
}
|
||||
|
||||
func defaultSelectionKeybinds() SelectionKeybinds {
|
||||
func selectionKeybinds(up, down, top, bottom string) SelectionKeybinds {
|
||||
return SelectionKeybinds{
|
||||
SelectUp: desc("up"),
|
||||
SelectDown: desc("down"),
|
||||
SelectTop: desc("top"),
|
||||
SelectBottom: desc("btm"),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPickerKeybinds() PickerKeybinds {
|
||||
return PickerKeybinds{
|
||||
SelectionKeybinds: defaultSelectionKeybinds(),
|
||||
Cancel: desc("cancel"),
|
||||
Select: desc("sel"),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultTreeKeybinds() TreeKeybinds {
|
||||
return TreeKeybinds{
|
||||
SelectionKeybinds: defaultSelectionKeybinds(),
|
||||
SelectCurrent: desc("select"),
|
||||
YankID: desc("copy id"),
|
||||
|
||||
CollapseAll: desc("collapse all"),
|
||||
CollapseParentNode: desc("collapse parent"),
|
||||
MoveToParentNode: desc("parent"),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultMessagesListKeybinds() MessagesListKeybinds {
|
||||
return MessagesListKeybinds{
|
||||
SelectionKeybinds: defaultSelectionKeybinds(),
|
||||
ScrollUp: desc("scr up"),
|
||||
ScrollDown: desc("scr down"),
|
||||
ScrollTop: desc("scr top"),
|
||||
ScrollBottom: desc("scr btm"),
|
||||
SelectReply: desc("sel reply"),
|
||||
Reply: desc("reply"),
|
||||
ReplyMention: desc("@reply"),
|
||||
Cancel: desc("cancel"),
|
||||
Edit: desc("edit"),
|
||||
Delete: desc("force delete"),
|
||||
DeleteConfirm: desc("delete"),
|
||||
Open: desc("open"),
|
||||
OpenInBrowser: desc("open browser"),
|
||||
Download: desc("download"),
|
||||
YankContent: desc("copy text"),
|
||||
YankURL: desc("copy url"),
|
||||
YankID: desc("copy id"),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultComposerKeybinds() ComposerKeybinds {
|
||||
return ComposerKeybinds{
|
||||
EditLast: desc("edit last"),
|
||||
Paste: desc("paste"),
|
||||
Send: desc("send"),
|
||||
Newline: desc("nl"),
|
||||
Cancel: desc("cancel"),
|
||||
TabComplete: desc("complete"),
|
||||
ToggleReplyMention: desc("toggle @reply"),
|
||||
Undo: desc("undo"),
|
||||
OpenEditor: desc("editor"),
|
||||
OpenFilePicker: desc("attach"),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultMentionsListKeybinds() MentionsListKeybinds {
|
||||
return MentionsListKeybinds{
|
||||
SelectionKeybinds: defaultSelectionKeybinds(),
|
||||
SelectUp: key(up, "up"),
|
||||
SelectDown: key(down, "down"),
|
||||
SelectTop: key(top, "top"),
|
||||
SelectBottom: key(bottom, "btm"),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultKeybinds() Keybinds {
|
||||
vi := selectionKeybinds("k", "j", "g", "G")
|
||||
emacs := selectionKeybinds("ctrl+p", "ctrl+n", "home", "end")
|
||||
tree := TreeKeybinds{
|
||||
SelectionKeybinds: vi,
|
||||
SelectCurrent: key("enter", "select"),
|
||||
YankID: key("i", "copy id"),
|
||||
|
||||
CollapseAll: key("_", "collapse all"),
|
||||
CollapseParentNode: key("-", "collapse parent"),
|
||||
MoveToParentNode: key("p", "parent"),
|
||||
}
|
||||
return Keybinds{
|
||||
ToggleGuildsTree: desc("toggle guilds"),
|
||||
ToggleMembersTree: desc("toggle members"),
|
||||
ToggleChannelsPicker: desc("channels picker"),
|
||||
ToggleHelp: desc("toggle help"),
|
||||
ToggleFullHelp: desc("full help"),
|
||||
Suspend: desc("suspend"),
|
||||
ToggleGuildsTree: key("alt+g", "toggle guilds"),
|
||||
ToggleMembersTree: key("alt+m", "toggle members"),
|
||||
ToggleChannelsPicker: key("ctrl+k", "channels picker"),
|
||||
ToggleHelp: key("alt+.", "toggle help"),
|
||||
ToggleFullHelp: key("ctrl+.", "full help"),
|
||||
Suspend: key("ctrl+z", "suspend"),
|
||||
|
||||
FocusGuildsTree: desc("guilds"),
|
||||
FocusMembersTree: desc("members"),
|
||||
FocusMessagesList: desc("messages"),
|
||||
FocusComposer: desc("composer"),
|
||||
FocusGuildsTree: key("ctrl+g", "guilds"),
|
||||
FocusMembersTree: key("ctrl+y", "members"),
|
||||
FocusMessagesList: key("ctrl+t", "messages"),
|
||||
FocusComposer: key("ctrl+i", "composer"),
|
||||
|
||||
FocusPrevious: desc("focus prev"),
|
||||
FocusNext: desc("focus next"),
|
||||
FocusPrevious: key("ctrl+h", "focus prev"),
|
||||
FocusNext: key("ctrl+l", "focus next"),
|
||||
|
||||
Logout: desc("logout"),
|
||||
Quit: desc("quit"),
|
||||
Picker: PickerKeybinds{
|
||||
SelectionKeybinds: emacs,
|
||||
Select: key("enter", "sel"),
|
||||
Cancel: key("esc", "cancel"),
|
||||
},
|
||||
GuildsTree: GuildsTreeKeybinds{
|
||||
TreeKeybinds: tree,
|
||||
SelectPreviousUnread: key("K", "prev unread"),
|
||||
SelectNextUnread: key("J", "next unread"),
|
||||
},
|
||||
MembersTree: tree,
|
||||
MessagesList: MessagesListKeybinds{
|
||||
SelectionKeybinds: vi,
|
||||
ScrollKeybinds: ScrollKeybinds{
|
||||
ScrollUp: key("K", "scr up"),
|
||||
ScrollDown: key("J", "scr down"),
|
||||
ScrollTop: key("home", "scr top"),
|
||||
ScrollBottom: key("end", "scr btm"),
|
||||
},
|
||||
|
||||
Picker: defaultPickerKeybinds(),
|
||||
GuildsTree: GuildsTreeKeybinds{TreeKeybinds: defaultTreeKeybinds(), SelectPreviousUnread: desc("prev unread"), SelectNextUnread: desc("next unread")},
|
||||
MembersTree: defaultTreeKeybinds(),
|
||||
MessagesList: defaultMessagesListKeybinds(),
|
||||
Composer: defaultComposerKeybinds(),
|
||||
MentionsList: defaultMentionsListKeybinds(),
|
||||
SelectReply: key("s", "sel reply"),
|
||||
Reply: key("R", "reply"),
|
||||
ReplyMention: key("r", "@reply"),
|
||||
|
||||
Cancel: key("esc", "cancel"),
|
||||
Edit: key("e", "edit"),
|
||||
|
||||
Delete: key("", "force delete"),
|
||||
DeleteConfirm: key("d", "delete"),
|
||||
|
||||
Open: key("o", "open"),
|
||||
OpenInBrowser: key("O", "open browser"),
|
||||
Download: key("D", "download"),
|
||||
|
||||
YankContent: key("y", "copy text"),
|
||||
YankURL: key("u", "copy url"),
|
||||
YankID: key("i", "copy id"),
|
||||
},
|
||||
Composer: ComposerKeybinds{
|
||||
EditLast: key("up", "edit last"),
|
||||
Paste: key("ctrl+v", "paste"),
|
||||
Send: key("enter", "send"),
|
||||
Newline: key("shift+enter", "nl"),
|
||||
Cancel: key("esc", "cancel"),
|
||||
TabComplete: key("tab", "complete"),
|
||||
ToggleReplyMention: key("shift+tab", "toggle @reply"),
|
||||
Undo: key("ctrl+u", "undo"),
|
||||
|
||||
OpenEditor: key("ctrl+e", "editor"),
|
||||
OpenFilePicker: key("ctrl+\\", "attach"),
|
||||
},
|
||||
MentionsList: MentionsListKeybinds{SelectionKeybinds: emacs},
|
||||
|
||||
Logout: key("ctrl+d", "logout"),
|
||||
Quit: key("ctrl+c", "quit"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/tview"
|
||||
|
|
@ -11,6 +13,25 @@ import (
|
|||
|
||||
var errInvalidType = errors.New("invalid type")
|
||||
|
||||
var (
|
||||
attributes = enum[tcell.AttrMask]{
|
||||
{"bold", tcell.AttrBold},
|
||||
{"dim", tcell.AttrDim},
|
||||
{"italic", tcell.AttrItalic},
|
||||
{"blink", tcell.AttrBlink},
|
||||
{"reverse", tcell.AttrReverse},
|
||||
{"strikethrough", tcell.AttrStrikeThrough},
|
||||
}
|
||||
underlines = enum[tcell.UnderlineStyle]{
|
||||
{"", tcell.UnderlineStyleNone},
|
||||
{"solid", tcell.UnderlineStyleSolid},
|
||||
{"double", tcell.UnderlineStyleDouble},
|
||||
{"curly", tcell.UnderlineStyleCurly},
|
||||
{"dotted", tcell.UnderlineStyleDotted},
|
||||
{"dashed", tcell.UnderlineStyleDashed},
|
||||
}
|
||||
)
|
||||
|
||||
type StyleWrapper struct{ tview.Style }
|
||||
|
||||
func (sw *StyleWrapper) UnmarshalTOML(v any) error {
|
||||
|
|
@ -45,22 +66,11 @@ func (sw *StyleWrapper) UnmarshalTOML(v any) error {
|
|||
}
|
||||
case "underline":
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleNone)
|
||||
case "solid":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleSolid)
|
||||
case "double":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleDouble)
|
||||
case "curly":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleCurly)
|
||||
case "dotted":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleDotted)
|
||||
case "dashed":
|
||||
sw.Style = sw.Underline(tcell.UnderlineStyleDashed)
|
||||
default:
|
||||
return fmt.Errorf("unknown value: %q", s)
|
||||
underline, err := underlines.parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sw.Style = sw.Underline(underline)
|
||||
}
|
||||
case "underline_color":
|
||||
if s, ok := val.(string); ok {
|
||||
|
|
@ -73,38 +83,83 @@ func (sw *StyleWrapper) UnmarshalTOML(v any) error {
|
|||
}
|
||||
|
||||
func (sw *StyleWrapper) parseAttr(s string) {
|
||||
switch s {
|
||||
case "underline":
|
||||
if s == "underline" {
|
||||
sw.Style = sw.Underline(true)
|
||||
case "bold":
|
||||
sw.Style = sw.Bold(true)
|
||||
case "blink":
|
||||
sw.Style = sw.Blink(true)
|
||||
case "reverse":
|
||||
sw.Style = sw.Reverse(true)
|
||||
case "dim":
|
||||
sw.Style = sw.Dim(true)
|
||||
case "italic":
|
||||
sw.Style = sw.Italic(true)
|
||||
case "strikethrough":
|
||||
sw.Style = sw.StrikeThrough(true)
|
||||
return
|
||||
}
|
||||
// An unknown attribute parses to no attribute.
|
||||
attr, _ := attributes.parse(s)
|
||||
sw.Style = sw.Attributes(sw.GetAttributes() | attr)
|
||||
}
|
||||
|
||||
// MarshalTOML writes the style as the inline table UnmarshalTOML reads.
|
||||
func (sw StyleWrapper) MarshalTOML() ([]byte, error) {
|
||||
var fields []string
|
||||
add := func(key string, value any) {
|
||||
b, _ := json.Marshal(value)
|
||||
fields = append(fields, key+" = "+string(b))
|
||||
}
|
||||
if c := sw.GetForeground(); c != tcell.ColorDefault {
|
||||
add("foreground", colorName(c))
|
||||
}
|
||||
if c := sw.GetBackground(); c != tcell.ColorDefault {
|
||||
add("background", colorName(c))
|
||||
}
|
||||
var attrs []string
|
||||
for _, a := range attributes {
|
||||
if sw.GetAttributes()&a.value != 0 {
|
||||
attrs = append(attrs, a.name)
|
||||
}
|
||||
}
|
||||
switch len(attrs) {
|
||||
case 0:
|
||||
case 1:
|
||||
add("attributes", attrs[0])
|
||||
default:
|
||||
add("attributes", attrs)
|
||||
}
|
||||
if u := sw.GetUnderlineStyle(); u != tcell.UnderlineStyleNone {
|
||||
name, _ := underlines.text(u)
|
||||
add("underline", string(name))
|
||||
}
|
||||
if c := sw.GetUnderlineColor(); c != tcell.ColorDefault {
|
||||
add("underline_color", colorName(c))
|
||||
}
|
||||
if fields == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
return []byte("{ " + strings.Join(fields, ", ") + " }"), nil
|
||||
}
|
||||
|
||||
// colorName returns the first of the names of c, or its hex code if it has none.
|
||||
func colorName(c tcell.Color) string {
|
||||
var names []string
|
||||
for name, color := range tcell.ColorNames {
|
||||
if color == c {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
if names == nil {
|
||||
return c.CSS()
|
||||
}
|
||||
return slices.Min(names)
|
||||
}
|
||||
|
||||
type (
|
||||
HelpTheme struct {
|
||||
HelpThemeConfig struct {
|
||||
ShortKeyStyle StyleWrapper `toml:"short_key_style"`
|
||||
ShortDescStyle StyleWrapper `toml:"short_desc_style"`
|
||||
FullKeyStyle StyleWrapper `toml:"full_key_style"`
|
||||
FullDescStyle StyleWrapper `toml:"full_desc_style"`
|
||||
}
|
||||
|
||||
ThemeStyle struct {
|
||||
StyleThemeConfig struct {
|
||||
NormalStyle StyleWrapper `toml:"normal_style"`
|
||||
// The style used while the widget is focused.
|
||||
ActiveStyle StyleWrapper `toml:"active_style"`
|
||||
}
|
||||
|
||||
MessagesListTheme struct {
|
||||
MessagesListThemeConfig struct {
|
||||
AuthorStyle StyleWrapper `toml:"author_style"`
|
||||
MentionStyle StyleWrapper `toml:"mention_style"`
|
||||
EmojiStyle StyleWrapper `toml:"emoji_style"`
|
||||
|
|
@ -116,10 +171,10 @@ type (
|
|||
MessageStyle StyleWrapper `toml:"message_style"`
|
||||
SelectedMessageStyle StyleWrapper `toml:"selected_message_style"`
|
||||
|
||||
Embeds MessagesListEmbedsTheme `toml:"embeds"`
|
||||
Embeds MessagesListEmbedsThemeConfig `toml:"embeds"`
|
||||
}
|
||||
|
||||
MessagesListEmbedsTheme struct {
|
||||
MessagesListEmbedsThemeConfig struct {
|
||||
ProviderStyle StyleWrapper `toml:"provider_style"`
|
||||
AuthorStyle StyleWrapper `toml:"author_style"`
|
||||
TitleStyle StyleWrapper `toml:"title_style"`
|
||||
|
|
@ -130,35 +185,39 @@ type (
|
|||
URLStyle StyleWrapper `toml:"url_style"`
|
||||
}
|
||||
|
||||
MentionsListTheme struct {
|
||||
MentionsListThemeConfig struct {
|
||||
SelectedStyle StyleWrapper `toml:"selected_style"`
|
||||
}
|
||||
|
||||
DialogTheme struct {
|
||||
Style StyleWrapper `toml:"style"`
|
||||
DialogThemeConfig struct {
|
||||
Style StyleWrapper `toml:"style"`
|
||||
// The style of everything behind the dialog.
|
||||
BackgroundStyle StyleWrapper `toml:"background_style"`
|
||||
}
|
||||
|
||||
ScrollBarTheme struct {
|
||||
ScrollBarThemeConfig struct {
|
||||
TrackStyle StyleWrapper `toml:"track_style"`
|
||||
ThumbStyle StyleWrapper `toml:"thumb_style"`
|
||||
}
|
||||
|
||||
Theme struct {
|
||||
Title ThemeStyle `toml:"title"`
|
||||
Footer ThemeStyle `toml:"footer"`
|
||||
Border ThemeStyle `toml:"border"`
|
||||
GuildsTree CommonTreeTheme `toml:"guilds_tree"`
|
||||
MembersTree CommonTreeTheme `toml:"members_tree"`
|
||||
ScrollBar ScrollBarTheme `toml:"scroll_bar"`
|
||||
MessagesList MessagesListTheme `toml:"messages_list"`
|
||||
MentionsList MentionsListTheme `toml:"mentions_list"`
|
||||
Dialog DialogTheme `toml:"dialog"`
|
||||
Help HelpTheme `toml:"help"`
|
||||
ThemeConfig struct {
|
||||
Title StyleThemeConfig `toml:"title"`
|
||||
Footer StyleThemeConfig `toml:"footer"`
|
||||
Border StyleThemeConfig `toml:"border"`
|
||||
// The status styles are those of direct-message recipients.
|
||||
GuildsTree TreeThemeConfig `toml:"guilds_tree"`
|
||||
// The status styles are those of the dot before each member.
|
||||
MembersTree TreeThemeConfig `toml:"members_tree"`
|
||||
ScrollBar ScrollBarThemeConfig `toml:"scroll_bar"`
|
||||
MessagesList MessagesListThemeConfig `toml:"messages_list"`
|
||||
MentionsList MentionsListThemeConfig `toml:"mentions_list"`
|
||||
Dialog DialogThemeConfig `toml:"dialog"`
|
||||
Help HelpThemeConfig `toml:"help"`
|
||||
}
|
||||
)
|
||||
|
||||
type CommonTreeTheme struct {
|
||||
type TreeThemeConfig struct {
|
||||
// The color of the lines that give the tree its shape.
|
||||
GraphicsColor string `toml:"graphics_color"`
|
||||
|
||||
OnlineStyle StyleWrapper `toml:"online_style"`
|
||||
|
|
@ -168,7 +227,7 @@ type CommonTreeTheme struct {
|
|||
}
|
||||
|
||||
// StatusStyle returns the style for a user with status.
|
||||
func (ctt CommonTreeTheme) StatusStyle(status discord.Status) tview.Style {
|
||||
func (ctt TreeThemeConfig) StatusStyle(status discord.Status) tview.Style {
|
||||
switch status {
|
||||
case discord.OnlineStatus:
|
||||
return ctt.OnlineStyle.Style
|
||||
|
|
@ -180,3 +239,50 @@ func (ctt CommonTreeTheme) StatusStyle(status discord.Status) tview.Style {
|
|||
return ctt.OfflineStyle.Style
|
||||
}
|
||||
}
|
||||
|
||||
func defaultTheme() ThemeConfig {
|
||||
var (
|
||||
plain = tcell.StyleDefault
|
||||
dim = StyleWrapper{plain.Dim(true)}
|
||||
bold = plain.Bold(true)
|
||||
italic = plain.Italic(true)
|
||||
blue = plain.Foreground(tcell.ColorBlue)
|
||||
focus = StyleThemeConfig{dim, StyleWrapper{bold.Foreground(tcell.ColorGreen)}}
|
||||
tree = TreeThemeConfig{
|
||||
GraphicsColor: "default",
|
||||
OnlineStyle: StyleWrapper{plain.Foreground(tcell.ColorGreen)},
|
||||
IdleStyle: StyleWrapper{plain.Foreground(tcell.ColorYellow)},
|
||||
DNDStyle: StyleWrapper{plain.Foreground(tcell.ColorRed)},
|
||||
OfflineStyle: StyleWrapper{plain.Foreground(tcell.ColorGray)},
|
||||
}
|
||||
)
|
||||
return ThemeConfig{
|
||||
Title: focus,
|
||||
Footer: focus,
|
||||
Border: focus,
|
||||
GuildsTree: tree,
|
||||
MembersTree: tree,
|
||||
ScrollBar: ScrollBarThemeConfig{TrackStyle: dim},
|
||||
MessagesList: MessagesListThemeConfig{
|
||||
MentionStyle: StyleWrapper{blue.Bold(true)},
|
||||
EmojiStyle: StyleWrapper{plain.Foreground(tcell.ColorGreen)},
|
||||
URLStyle: StyleWrapper{blue},
|
||||
AttachmentStyle: StyleWrapper{plain.Foreground(tcell.ColorYellow)},
|
||||
ReactionStyle: dim,
|
||||
OwnReactionStyle: StyleWrapper{bold},
|
||||
SelectedMessageStyle: StyleWrapper{plain.Reverse(true)},
|
||||
Embeds: MessagesListEmbedsThemeConfig{
|
||||
ProviderStyle: StyleWrapper{italic.Dim(true)},
|
||||
AuthorStyle: StyleWrapper{italic},
|
||||
TitleStyle: StyleWrapper{blue.Bold(true)},
|
||||
DescriptionStyle: dim,
|
||||
FieldNameStyle: StyleWrapper{bold.Underline(true)},
|
||||
FooterStyle: StyleWrapper{italic.Dim(true)},
|
||||
URLStyle: StyleWrapper{blue.Underline(true)},
|
||||
},
|
||||
},
|
||||
MentionsList: MentionsListThemeConfig{SelectedStyle: StyleWrapper{plain.Reverse(true)}},
|
||||
Dialog: DialogThemeConfig{BackgroundStyle: dim},
|
||||
Help: HelpThemeConfig{ShortKeyStyle: dim, FullKeyStyle: dim},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,146 +8,195 @@ import (
|
|||
"github.com/ayn2op/tview/scrollbar"
|
||||
)
|
||||
|
||||
// enum pairs the names an option accepts with their values; a value is written back as the first of its names.
|
||||
type enum[T comparable] []struct {
|
||||
name string
|
||||
value T
|
||||
}
|
||||
|
||||
func (e enum[T]) parse(name string) (T, error) {
|
||||
for _, p := range e {
|
||||
if p.name == name {
|
||||
return p.value, nil
|
||||
}
|
||||
}
|
||||
var zero T
|
||||
return zero, fmt.Errorf("unknown value: %q", name)
|
||||
}
|
||||
|
||||
func (e enum[T]) text(value T) ([]byte, error) {
|
||||
for _, p := range e {
|
||||
if p.value == value {
|
||||
return []byte(p.name), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unknown value: %v", value)
|
||||
}
|
||||
|
||||
var (
|
||||
boxDrawing = scrollbar.SymbolSet{Track: "│", Thumb: "█", Begin: "▲", End: "▼"}
|
||||
|
||||
alignments = enum[tview.Alignment]{
|
||||
{"left", tview.AlignmentLeft},
|
||||
{"center", tview.AlignmentCenter},
|
||||
{"right", tview.AlignmentRight},
|
||||
}
|
||||
borderSets = enum[tview.BorderSet]{
|
||||
{"hidden", tview.BorderSetHidden()},
|
||||
{"plain", tview.BorderSetPlain()},
|
||||
{"round", tview.BorderSetRound()},
|
||||
{"thick", tview.BorderSetThick()},
|
||||
{"double", tview.BorderSetDouble()},
|
||||
}
|
||||
symbolSets = enum[scrollbar.SymbolSet]{
|
||||
{"minimal", scrollbar.SymbolSet{Thumb: "█", Begin: "▲", End: "▼"}},
|
||||
{"box_drawing", boxDrawing},
|
||||
{"boxdrawing", boxDrawing},
|
||||
{"box", boxDrawing},
|
||||
{"vertical", scrollbar.SymbolSetVertical()},
|
||||
{"double_vertical", scrollbar.SymbolSetDoubleVertical()},
|
||||
}
|
||||
scrollBarVisibilities = enum[list.ScrollBarVisibility]{
|
||||
{"auto", list.ScrollBarVisibilityAutomatic},
|
||||
{"automatic", list.ScrollBarVisibilityAutomatic},
|
||||
{"always", list.ScrollBarVisibilityAlways},
|
||||
{"never", list.ScrollBarVisibilityNever},
|
||||
{"hidden", list.ScrollBarVisibilityNever},
|
||||
{"off", list.ScrollBarVisibilityNever},
|
||||
}
|
||||
)
|
||||
|
||||
type AlignmentWrapper struct{ tview.Alignment }
|
||||
|
||||
func (aw *AlignmentWrapper) UnmarshalTOML(v any) error {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return errInvalidType
|
||||
}
|
||||
switch s {
|
||||
case "left":
|
||||
aw.Alignment = tview.AlignmentLeft
|
||||
case "center":
|
||||
aw.Alignment = tview.AlignmentCenter
|
||||
case "right":
|
||||
aw.Alignment = tview.AlignmentRight
|
||||
default:
|
||||
return fmt.Errorf("unknown value: %q", s)
|
||||
}
|
||||
return nil
|
||||
func (aw *AlignmentWrapper) UnmarshalText(text []byte) (err error) {
|
||||
aw.Alignment, err = alignments.parse(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (aw AlignmentWrapper) MarshalText() ([]byte, error) { return alignments.text(aw.Alignment) }
|
||||
|
||||
type BorderSetWrapper struct{ tview.BorderSet }
|
||||
|
||||
func (bw *BorderSetWrapper) UnmarshalTOML(val any) error {
|
||||
s, ok := val.(string)
|
||||
if !ok {
|
||||
return errInvalidType
|
||||
}
|
||||
switch s {
|
||||
case "hidden":
|
||||
bw.BorderSet = tview.BorderSetHidden()
|
||||
case "plain":
|
||||
bw.BorderSet = tview.BorderSetPlain()
|
||||
case "round":
|
||||
bw.BorderSet = tview.BorderSetRound()
|
||||
case "thick":
|
||||
bw.BorderSet = tview.BorderSetThick()
|
||||
case "double":
|
||||
bw.BorderSet = tview.BorderSetDouble()
|
||||
default:
|
||||
return fmt.Errorf("unknown value: %q", s)
|
||||
}
|
||||
return nil
|
||||
func (bw *BorderSetWrapper) UnmarshalText(text []byte) (err error) {
|
||||
bw.BorderSet, err = borderSets.parse(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (bw BorderSetWrapper) MarshalText() ([]byte, error) { return borderSets.text(bw.BorderSet) }
|
||||
|
||||
type SymbolSetWrapper struct{ scrollbar.SymbolSet }
|
||||
|
||||
func (sw *SymbolSetWrapper) UnmarshalTOML(val any) error {
|
||||
s, ok := val.(string)
|
||||
if !ok {
|
||||
return errInvalidType
|
||||
}
|
||||
switch s {
|
||||
case "minimal":
|
||||
sw.SymbolSet = scrollbar.SymbolSet{Thumb: "█", Begin: "▲", End: "▼"}
|
||||
case "box_drawing", "boxdrawing", "box":
|
||||
sw.SymbolSet = scrollbar.SymbolSet{Track: "│", Thumb: "█", Begin: "▲", End: "▼"}
|
||||
case "vertical":
|
||||
sw.SymbolSet = scrollbar.SymbolSetVertical()
|
||||
case "double_vertical":
|
||||
sw.SymbolSet = scrollbar.SymbolSetDoubleVertical()
|
||||
default:
|
||||
return fmt.Errorf("unknown value: %q", s)
|
||||
}
|
||||
return nil
|
||||
func (sw *SymbolSetWrapper) UnmarshalText(text []byte) (err error) {
|
||||
sw.SymbolSet, err = symbolSets.parse(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (sw SymbolSetWrapper) MarshalText() ([]byte, error) { return symbolSets.text(sw.SymbolSet) }
|
||||
|
||||
type ScrollBarVisibilityWrapper struct{ list.ScrollBarVisibility }
|
||||
|
||||
func (vw *ScrollBarVisibilityWrapper) UnmarshalTOML(val any) error {
|
||||
s, ok := val.(string)
|
||||
if !ok {
|
||||
return errInvalidType
|
||||
}
|
||||
switch s {
|
||||
case "automatic", "auto":
|
||||
vw.ScrollBarVisibility = list.ScrollBarVisibilityAutomatic
|
||||
case "always":
|
||||
vw.ScrollBarVisibility = list.ScrollBarVisibilityAlways
|
||||
case "never", "hidden", "off":
|
||||
vw.ScrollBarVisibility = list.ScrollBarVisibilityNever
|
||||
default:
|
||||
return fmt.Errorf("unknown value: %q", s)
|
||||
}
|
||||
return nil
|
||||
func (vw *ScrollBarVisibilityWrapper) UnmarshalText(text []byte) (err error) {
|
||||
vw.ScrollBarVisibility, err = scrollBarVisibilities.parse(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
func (vw ScrollBarVisibilityWrapper) MarshalText() ([]byte, error) {
|
||||
return scrollBarVisibilities.text(vw.ScrollBarVisibility)
|
||||
}
|
||||
|
||||
type (
|
||||
UIConfig struct {
|
||||
Tabs TabsConfig `toml:"tabs"`
|
||||
Title TitleConfig `toml:"title"`
|
||||
Footer TitleConfig `toml:"footer"`
|
||||
Border BorderConfig `toml:"border"`
|
||||
GuildsTree GuildsTreeConfig `toml:"guilds_tree"`
|
||||
MembersTree TreeConfig `toml:"members_tree"`
|
||||
ScrollBar ScrollBarConfig `toml:"scroll_bar"`
|
||||
MessagesList MessagesListConfig `toml:"messages_list"`
|
||||
MentionsList MentionsListConfig `toml:"mentions_list"`
|
||||
// The tabs of the login screen.
|
||||
Tabs TabsUIConfig `toml:"tabs"`
|
||||
Title TitleUIConfig `toml:"title"`
|
||||
Footer TitleUIConfig `toml:"footer"`
|
||||
Border BorderUIConfig `toml:"border"`
|
||||
GuildsTree GuildsTreeUIConfig `toml:"guilds_tree"`
|
||||
MembersTree TreeUIConfig `toml:"members_tree"`
|
||||
ScrollBar ScrollBarUIConfig `toml:"scroll_bar"`
|
||||
MessagesList MessagesListUIConfig `toml:"messages_list"`
|
||||
// The list of mention suggestions above the composer. Its width and height are capped to the available space.
|
||||
MentionsList MentionsListUIConfig `toml:"mentions_list"`
|
||||
}
|
||||
|
||||
TabsConfig struct {
|
||||
Alignment AlignmentWrapper `toml:"alignment"`
|
||||
Wrap bool `toml:"wrap"`
|
||||
Separator string `toml:"separator"`
|
||||
Padding [2]string `toml:"padding"`
|
||||
Arrows [2]string `toml:"arrows"`
|
||||
ClickableArrows bool `toml:"clickable_arrows"`
|
||||
TabsUIConfig struct {
|
||||
// Where tabs that fit are placed: "left", "center" or "right".
|
||||
Alignment AlignmentWrapper `toml:"alignment"`
|
||||
// Whether moving past the last tab selects the first, and past the first selects the last.
|
||||
Wrap bool `toml:"wrap"`
|
||||
// Text between tabs.
|
||||
Separator string `toml:"separator"`
|
||||
// Text on either side of each tab: [left, right]
|
||||
Padding [2]string `toml:"padding"`
|
||||
// Arrows shown while tabs are hidden past an end: [start, end]. An empty string turns that arrow off.
|
||||
Arrows [2]string `toml:"arrows"`
|
||||
// Whether clicking an arrow selects the tab next to the active one.
|
||||
ClickableArrows bool `toml:"clickable_arrows"`
|
||||
}
|
||||
|
||||
TitleConfig struct {
|
||||
TitleUIConfig struct {
|
||||
// "left", "center" or "right".
|
||||
Alignment AlignmentWrapper `toml:"alignment"`
|
||||
}
|
||||
|
||||
BorderConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Padding [4]int `toml:"padding"`
|
||||
BorderUIConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
// [top, bottom, left, right]
|
||||
Padding [4]int `toml:"padding"`
|
||||
// "hidden", "plain", "round", "thick" or "double".
|
||||
NormalSet BorderSetWrapper `toml:"normal_set"`
|
||||
// The set used while the widget is focused.
|
||||
ActiveSet BorderSetWrapper `toml:"active_set"`
|
||||
}
|
||||
|
||||
TreeConfig struct {
|
||||
TreeUIConfig struct {
|
||||
// Whether to draw lines that give the tree its shape.
|
||||
Graphics bool `toml:"graphics"`
|
||||
}
|
||||
|
||||
GuildsTreeConfig struct {
|
||||
TreeConfig
|
||||
GuildsTreeUIConfig struct {
|
||||
TreeUIConfig
|
||||
AutoExpandFolders bool `toml:"auto_expand_folders"`
|
||||
}
|
||||
|
||||
ScrollBarConfig struct {
|
||||
ScrollBarUIConfig struct {
|
||||
// "auto", "always" or "never".
|
||||
Visibility ScrollBarVisibilityWrapper `toml:"visibility"`
|
||||
SymbolSet SymbolSetWrapper `toml:"symbol_set"`
|
||||
// "minimal", "box_drawing", "vertical" or "double_vertical".
|
||||
SymbolSet SymbolSetWrapper `toml:"symbol_set"`
|
||||
}
|
||||
|
||||
MessagesListConfig struct {
|
||||
MessagesListUIConfig struct {
|
||||
ReplyIndicator string `toml:"reply_indicator"`
|
||||
ForwardedIndicator string `toml:"forwarded_indicator"`
|
||||
}
|
||||
|
||||
MentionsListConfig struct {
|
||||
MinWidth uint `toml:"min_width"`
|
||||
MentionsListUIConfig struct {
|
||||
// Minimum width. 0 makes the list as wide as possible.
|
||||
MinWidth uint `toml:"min_width"`
|
||||
// Maximum height. 0 makes the list as tall as needed.
|
||||
MaxHeight uint `toml:"max_height"`
|
||||
}
|
||||
)
|
||||
|
||||
func defaultUI() UIConfig {
|
||||
left := TitleUIConfig{AlignmentWrapper{tview.AlignmentLeft}}
|
||||
round := BorderSetWrapper{tview.BorderSetRound()}
|
||||
return UIConfig{
|
||||
Tabs: TabsUIConfig{
|
||||
Alignment: AlignmentWrapper{tview.AlignmentCenter},
|
||||
Separator: "│",
|
||||
Padding: [2]string{" ", " "},
|
||||
Arrows: [2]string{"◀", "▶"},
|
||||
ClickableArrows: true,
|
||||
},
|
||||
Title: left,
|
||||
Footer: left,
|
||||
Border: BorderUIConfig{Enabled: true, Padding: [4]int{0, 0, 1, 1}, NormalSet: round, ActiveSet: round},
|
||||
GuildsTree: GuildsTreeUIConfig{TreeUIConfig{Graphics: true}, true},
|
||||
MembersTree: TreeUIConfig{Graphics: true},
|
||||
ScrollBar: ScrollBarUIConfig{SymbolSet: SymbolSetWrapper{boxDrawing}},
|
||||
MessagesList: MessagesListUIConfig{ReplyIndicator: ">", ForwardedIndicator: "<"},
|
||||
MentionsList: MentionsListUIConfig{MinWidth: 20},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -670,7 +670,7 @@ const (
|
|||
embedLineURL
|
||||
)
|
||||
|
||||
func embedLineStyles(baseStyle tview.Style, theme config.MessagesListEmbedsTheme) [8]tview.Style {
|
||||
func embedLineStyles(baseStyle tview.Style, theme config.MessagesListEmbedsThemeConfig) [8]tview.Style {
|
||||
styles := [8]tview.Style{}
|
||||
styles[embedLineProvider] = tview.MergeStyle(baseStyle, theme.ProviderStyle.Style)
|
||||
styles[embedLineAuthor] = tview.MergeStyle(baseStyle, theme.AuthorStyle.Style)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
// New returns a boxed tree of the nodes under root, drawn with theme and graphics and moved with kbs, that turns changes and selected nodes into the messages onChange and onSelect return.
|
||||
func New(root *tree.Node, selectionState tree.SelectionState, cfg *config.Config, theme config.CommonTreeTheme, graphics bool, kbs config.TreeKeybinds, focused bool, onChange func(tree.Change) tview.Msg, onSelect func(*tree.Node) tview.Msg) box.Widget {
|
||||
func New(root *tree.Node, selectionState tree.SelectionState, cfg *config.Config, theme config.TreeThemeConfig, graphics bool, kbs config.TreeKeybinds, focused bool, onChange func(tree.Change) tview.Msg, onSelect func(*tree.Node) tview.Msg) box.Widget {
|
||||
set := cfg.UI.Border.NormalSet.BorderSet
|
||||
if focused {
|
||||
set = cfg.UI.Border.ActiveSet.BorderSet
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue