cordless/shortcuts/shortcuts.go

391 lines
16 KiB
Go

package shortcuts
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
tcell "github.com/gdamore/tcell/v2"
"github.com/Bios-Marcel/cordless/config"
"github.com/Bios-Marcel/cordless/tview"
"github.com/Bios-Marcel/cordless/ui/tviewutil"
)
var (
globalScope = addScope("global", "Application wide", nil)
multilineTextInput = addScope("multiline_text_input", "Multiline text input", globalScope)
chatview = addScope("chatview", "Chatview", globalScope)
guildlist = addScope("guildlist", "Guildlist", globalScope)
channeltree = addScope("channeltree", "Channeltree", globalScope)
QuoteSelectedMessage = addShortcut("quote_selected_message", "Quote selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 'q', tcell.ModNone))
EditSelectedMessage = addShortcut("edit_selected_message", "Edit selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 'e', tcell.ModNone))
DownloadMessageFiles = addShortcut("dowbload_message_files", "Download all files in selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone))
ReplySelectedMessage = addShortcut("reply_selected_message", "Reply to author selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 'r', tcell.ModNone))
NewDirectMessage = addShortcut("new_direct_message", "Create a new direct message channel with this user",
chatview, tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModNone))
CopySelectedMessageLink = addShortcut("copy_selected_message_link", "Copy link to selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone))
CopySelectedMessage = addShortcut("copy_selected_message", "Copy content of selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModNone))
ToggleSelectedMessageSpoilers = addShortcut("toggle_selected_message_spoilers", "Toggle spoilers in selected message",
chatview, tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModNone))
DeleteSelectedMessage = addShortcut("delete_selected_message", "Delete the selected message",
chatview, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone))
ViewSelectedMessageImages = addShortcut("view_selected_message_images", "View selected message's attached files",
chatview, tcell.NewEventKey(tcell.KeyRune, 'o', tcell.ModNone))
ChatViewSelectionUp = addShortcut("selection_up", "Move selection up by one",
chatview, tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone))
ChatViewSelectionDown = addShortcut("selection_down", "Move selection down by one",
chatview, tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone))
ChatViewSelectionTop = addShortcut("selection_top", "Move selection to the upmost message",
chatview, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone))
ChatViewSelectionBottom = addShortcut("selection_bottom", "Move selection to the downmost message",
chatview, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone))
ExpandSelectionToLeft = addShortcut("expand_selection_word_to_left", "Expand selection word to left",
multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModShift))
ExpandSelectionToRight = addShortcut("expand_selection_word_to_right", "Expand selection word to right",
multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModShift))
SelectAll = addShortcut("select_all", "Select all",
multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl))
SelectWordLeft = addShortcut("select_word_to_left", "Select word to left",
multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModCtrl|tcell.ModShift))
SelectWordRight = addShortcut("select_word_to_right", "Select word to right",
multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModCtrl|tcell.ModShift))
SelectToStartOfLine = addShortcut("select_to_start_of_line", "Select to start of line",
multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModShift))
SelectToEndOfLine = addShortcut("select_to_end_of_line", "Select to end of line",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModShift))
SelectToStartOfText = addShortcut("select_to_start_of_text", "Select to start of text",
multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModCtrl|tcell.ModShift))
SelectToEndOfText = addShortcut("select_to_end_of_text", "Select to end of text",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModCtrl|tcell.ModShift))
MoveCursorLeft = addShortcut("move_cursor_to_left", "Move cursor to left",
multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone))
MoveCursorRight = addShortcut("move_cursor_to_right", "Move cursor to right",
multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone))
MoveCursorWordLeft = addShortcut("move_cursor_to_word_left", "Move cursor to word left",
multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModCtrl))
MoveCursorWordRight = addShortcut("move_cursor_to_word_right", "Move cursor to word right",
multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModCtrl))
MoveCursorStartOfLine = addShortcut("move_cursor_to_start_of_line", "Move cursor to start of line",
multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone))
MoveCursorEndOfLine = addShortcut("move_cursor_to_end_of_line", "Move cursor to end of line",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone))
MoveCursorStartOfText = addShortcut("move_cursor_to_start_of_text", "Move cursor to start of text",
multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModCtrl))
MoveCursorEndOfText = addShortcut("move_cursor_to_end_of_text", "Move cursor to end of text",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModCtrl))
DeleteLeft = addDeleteLeftShortcut()
DeleteRight = addShortcut("delete_right", "Delete right",
multilineTextInput, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone))
DeleteWordLeft = addShortcut("delete_word_left", "Delete word left",
multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlW, rune(tcell.KeyCtrlW), tcell.ModCtrl))
InputNewLine = addShortcut("add_new_line_character", "Add new line character",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModAlt))
CopySelection = addShortcut("copy_selection", "Copy selected text",
multilineTextInput, tcell.NewEventKey(tcell.KeyRune, 'C', tcell.ModAlt))
//Don't fix the typo, it'll break stuff ;)
PasteAtSelection = addShortcut("paste_at_selectiom", "Paste clipboard content",
multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlV, rune(tcell.KeyCtrlV), tcell.ModCtrl))
SendMessage = addShortcut("send_message", "Sends the typed message",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone))
AddNewLineInCodeBlock = addShortcut("add_new_line_in_code_block", "Adds a new line inside a code block",
multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone))
ExitApplication = addShortcut("exit_application", "Exit application",
globalScope, tcell.NewEventKey(tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl))
FocusUp = addShortcut("focus_up", "Focus the next widget above",
globalScope, tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModAlt))
FocusDown = addShortcut("focus_down", "Focus the next widget below",
globalScope, tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModAlt))
FocusLeft = addShortcut("focus_left", "Focus the next widget to the left",
globalScope, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModAlt))
FocusRight = addShortcut("focus_right", "Focus the next widget to the right",
globalScope, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModAlt))
FocusChannelContainer = addShortcut("focus_channel_container", "Focus channel container",
globalScope, tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModAlt))
FocusUserContainer = addShortcut("focus_user_container", "Focus user container",
globalScope, tcell.NewEventKey(tcell.KeyRune, 'u', tcell.ModAlt))
FocusGuildContainer = addShortcut("focus_guild_container", "Focus guild container",
globalScope, tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModAlt))
FocusPrivateChatPage = addShortcut("focus_private_chat_page", "Focus private chat page",
globalScope, tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModAlt))
SwitchToPreviousChannel = addShortcut("switch_to_previous_channel", "Switch to previous channel",
globalScope, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModAlt))
FocusMessageInput = addShortcut("focus_message_input", "Focus message input",
globalScope, tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModAlt))
FocusMessageContainer = addShortcut("focus_message_container", "Focus message container",
globalScope, tcell.NewEventKey(tcell.KeyRune, 't', tcell.ModAlt))
FocusCommandInput = addShortcut("focus_command_input", "Focus command input",
globalScope, nil)
FocusCommandOutput = addShortcut("focus_command_output", "Focus command output",
globalScope, nil)
ToggleUserContainer = addShortcut("toggle_user_container", "Toggle user container",
globalScope, tcell.NewEventKey(tcell.KeyRune, 'U', tcell.ModAlt))
ToggleCommandView = addShortcut("toggle_command_view", "Toggle command view",
globalScope, tcell.NewEventKey(tcell.KeyRune, '.', tcell.ModAlt))
ToggleBareChat = addShortcut("toggle_bare_chat", "Toggle bare chat",
globalScope, tcell.NewEventKey(tcell.KeyCtrlB, rune(tcell.KeyCtrlB), tcell.ModCtrl))
GuildListMarkRead = addShortcut("guild_mark_read", "Mark server as read",
guildlist, tcell.NewEventKey(tcell.KeyCtrlR, rune(tcell.KeyCtrlR), tcell.ModCtrl))
ChannelTreeMarkRead = addShortcut("channel_mark_read", "Mark channel as read",
channeltree, tcell.NewEventKey(tcell.KeyCtrlR, rune(tcell.KeyCtrlR), tcell.ModCtrl))
scopes []*Scope
Shortcuts []*Shortcut
)
func addScope(identifier, name string, parent *Scope) *Scope {
scope := &Scope{
Parent: parent,
Identifier: identifier,
Name: name,
}
scopes = append(scopes, scope)
return scope
}
func addShortcut(identifier, name string, scope *Scope, event *tcell.EventKey) *Shortcut {
shortcut := &Shortcut{
Identifier: identifier,
Name: name,
Scope: scope,
Event: event,
defaultEvent: event,
}
Shortcuts = append(Shortcuts, shortcut)
return shortcut
}
// Shortcut defines a shortcut within the application. The scope might for
// example be a widget or situation in which the user is.
type Shortcut struct {
// Identifier will be used for persistence and should never change
Identifier string
// Name will be shown on the UI
Name string
// The Scope will be omitted, as this needed be persisted anyway.
Scope *Scope
// Event is the shortcut expressed as it's resulting tcell Event.
Event *tcell.EventKey
//This shortcuts default, in order to be able to reset it.
defaultEvent *tcell.EventKey
}
// Equals compares the given EventKey with the Shortcuts Event.
func (shortcut *Shortcut) Equals(event *tcell.EventKey) bool {
return EventsEqual(shortcut.Event, event)
}
// EventsEqual compares the given events, respecting everything except for the
// When field.
func EventsEqual(eventOne, eventTwo *tcell.EventKey) bool {
if (eventOne == nil && eventTwo != nil) || (eventOne != nil && eventTwo == nil) {
return false
}
return eventOne.Rune() == eventTwo.Rune() &&
eventOne.Modifiers() == eventTwo.Modifiers() &&
eventOne.Key() == eventTwo.Key()
}
// Scope is what describes a shortcuts scope within the application. Usually
// a scope can only have a specific shortcut once and a children scope will
// overwrite that shortcut, since that lower scope has the upper hand.
type Scope struct {
// Parent is this scopes upper Scope, which may be null, in case this is a
// root scope.
Parent *Scope
// Identifier will be used for persistence and should never change
Identifier string
// Name will be shown on the UI
Name string
}
// ShortcutDataRepresentation represents a shortcut configured by the user.
// This prevents redundancy of scopes.
type ShortcutDataRepresentation struct {
Identifier string
ScopeIdentifier string
EventKey tcell.Key
EventMod tcell.ModMask
EventRune rune
}
// MarshalJSON marshals a Shortcut into a ShortcutDataRepresentation. This
// happens in order to prevent saving the scopes multiple times, therefore
// the scopes will be hardcoded.
func (shortcut *Shortcut) MarshalJSON() ([]byte, error) {
if shortcut.Event == nil {
return json.MarshalIndent(&ShortcutDataRepresentation{
Identifier: shortcut.Identifier,
ScopeIdentifier: shortcut.Scope.Identifier,
EventKey: -1,
EventRune: -1,
EventMod: -1,
}, "", " ")
}
return json.MarshalIndent(&ShortcutDataRepresentation{
Identifier: shortcut.Identifier,
ScopeIdentifier: shortcut.Scope.Identifier,
EventKey: shortcut.Event.Key(),
EventRune: shortcut.Event.Rune(),
EventMod: shortcut.Event.Modifiers(),
}, "", " ")
}
// UnmarshalJSON unmarshals ShortcutDataRepresentation JSON into a Shortcut.
// This happens in order to prevent saving the scopes multiple times,
// therefore the scopes will be hardcoded.
func (shortcut *Shortcut) UnmarshalJSON(data []byte) error {
var temp ShortcutDataRepresentation
err := json.Unmarshal(data, &temp)
if err != nil {
return err
}
if temp.EventKey == -1 && temp.EventMod == -1 && temp.EventRune == -1 {
shortcut.Event = nil
} else {
shortcut.Event = tcell.NewEventKey(temp.EventKey, temp.EventRune, temp.EventMod)
}
shortcut.Identifier = temp.Identifier
for _, scope := range scopes {
if scope.Identifier == temp.ScopeIdentifier {
shortcut.Scope = scope
return nil
}
}
return fmt.Errorf(fmt.Sprintf("error finding scope '%s'", temp.ScopeIdentifier))
}
// Reset resets this shortcuts event value to the default one.
func (shortcut *Shortcut) Reset() {
shortcut.Event = shortcut.defaultEvent
}
func getShortcutsPath() (string, error) {
configDirectory, configError := config.GetConfigDirectory()
if configError != nil {
return "", configError
}
return filepath.Join(configDirectory, "shortcuts.json"), nil
}
// Load loads the shortcuts and copies the events to the correct shortcuts
// that reside inside of the memory.
func Load() error {
shortcutsPath, pathError := getShortcutsPath()
if pathError != nil {
return pathError
}
shortcutsFile, openError := os.Open(shortcutsPath)
if os.IsNotExist(openError) {
return nil
}
if openError != nil {
return openError
}
defer shortcutsFile.Close()
decoder := json.NewDecoder(shortcutsFile)
tempShortcuts := make([]*Shortcut, 0)
shortcutsLoadError := decoder.Decode(&tempShortcuts)
//io.EOF would mean empty, therefore we use defaults.
if shortcutsLoadError != nil && shortcutsLoadError != io.EOF {
return nil
}
if shortcutsLoadError != nil {
return shortcutsLoadError
}
OUTER_LOOP:
for _, shortcut := range tempShortcuts {
for _, otherShortcut := range Shortcuts {
if otherShortcut.Identifier == shortcut.Identifier &&
otherShortcut.Scope.Identifier == shortcut.Scope.Identifier {
otherShortcut.Event = shortcut.Event
continue OUTER_LOOP
}
}
}
return nil
}
// Persist saves the currently shortcuts that are currently being held in
// memory.
func Persist() error {
filePath, pathError := getShortcutsPath()
if pathError != nil {
return pathError
}
shortcutsAsJSON, jsonError := json.MarshalIndent(&Shortcuts, "", " ")
if jsonError != nil {
return jsonError
}
writeError := ioutil.WriteFile(filePath, shortcutsAsJSON, 0666)
if writeError != nil {
return writeError
}
return nil
}
func DirectionalFocusHandling(event *tcell.EventKey, app *tview.Application) *tcell.EventKey {
focused := app.GetFocus()
if FocusUp.Equals(event) {
tviewutil.FocusNextIfPossible(tview.Up, app, focused)
} else if FocusDown.Equals(event) {
tviewutil.FocusNextIfPossible(tview.Down, app, focused)
} else if FocusLeft.Equals(event) {
tviewutil.FocusNextIfPossible(tview.Left, app, focused)
} else if FocusRight.Equals(event) {
tviewutil.FocusNextIfPossible(tview.Right, app, focused)
} else {
return event
}
return nil
}