First working version of the shortcuts table. Fixes
MODIFY
internal/app.go
internal/app.go
@@ -7,6 +7,8 @@ import (
"os"
"runtime"
+ "github.com/Bios-Marcel/cordless/internal/shortcuts"
+
"github.com/princebot/getpass"
"github.com/Bios-Marcel/cordless/internal/commands/commandimpls"
@@ -52,6 +54,11 @@ func Run() {
log.Fatalf("Error loading configuration file (%s).\n", configLoadError.Error())
}
+ shortcutsLoadError := shortcuts.Load()
+ if shortcutsLoadError != nil {
+ panic(shortcutsLoadError)
+ }
+
discord := attemptLogin(defaultLoginMessage, app, configuration)
config.GetConfig().Token = discord.Token

ADD
internal/shortcuts/shortcuts.go
+package shortcuts
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/Bios-Marcel/cordless/internal/config"
+ "github.com/gdamore/tcell"
+)
+
+var (
+ globalScope = &Scope{
+ Parent: nil,
+ Identifier: "global",
+ Name: "Application wide",
+ }
+
+ scopes = []*Scope{
+ globalScope,
+ }
+
+ FocusChannelContainer = &Shortcut{
+ Name: "Focus channel container",
+ Identifier: "focus_channel_container",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModAlt),
+ }
+
+ FocusUserContainer = &Shortcut{
+ Name: "Focus user container",
+ Identifier: "focus_user_container",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 'u', tcell.ModAlt),
+ }
+
+ ToggleUserContainer = &Shortcut{
+ Name: "Toggle user container",
+ Identifier: "toggle_user_container",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 'U', tcell.ModAlt),
+ }
+
+ FocusGuildContainer = &Shortcut{
+ Name: "Focus guild container",
+ Identifier: "focus_guild_container",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModAlt),
+ }
+
+ FocusPrivateChatPage = &Shortcut{
+ Name: "Focus private chat page",
+ Identifier: "focus_private_chat_page",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModAlt),
+ }
+
+ FocusMessageInput = &Shortcut{
+ Name: "Focus message input",
+ Identifier: "focus_message_input",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModAlt),
+ }
+
+ FocusMessageContainer = &Shortcut{
+ Name: "Focus message container",
+ Identifier: "focus_message_container",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, 't', tcell.ModAlt),
+ }
+
+ FocusCommandInput = &Shortcut{
+ Name: "Focus command input",
+ Identifier: "focus_command_input",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModNone),
+ }
+
+ FocusCommandOutput = &Shortcut{
+ Name: "Focus command output",
+ Identifier: "focus_command_output",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyCtrlO, rune(tcell.KeyCtrlO), tcell.ModCtrl),
+ }
+
+ ToggleCommandView = &Shortcut{
+ Name: "Toggle command view",
+ Identifier: "toggle_command_view",
+ scope: globalScope,
+ Event: tcell.NewEventKey(tcell.KeyRune, '.', tcell.ModAlt),
+ }
+
+ Shortcuts = []*Shortcut{
+ FocusChannelContainer,
+ FocusUserContainer,
+ ToggleUserContainer,
+ FocusGuildContainer,
+ FocusPrivateChatPage,
+ FocusMessageInput,
+ FocusMessageContainer,
+ FocusCommandInput,
+ FocusCommandOutput,
+ ToggleCommandView,
+ }
+)
+
+// 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
+}
+
+// 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 on the users harddrive.
+// This prevents redudancy of scopes.
+type ShortcutDataRepresentation struct {
+ Identifier string
+ ScopeIdentifier string
+ EventKey tcell.Key
+ EventRune rune
+ EventMod tcell.ModMask
+}
+
+// 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) {
+ 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
+ }
+
+ 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))
+}
+
+func getShortcutsPath() (string, error) {
+ configDirecotry, configError := config.GetConfigDirectory()
+ if configError != nil {
+ return "", configError
+ }
+
+ return filepath.Join(configDirecotry, "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 held in memory held shortcuts
+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
+}

ADD
internal/shortcuts/table.go
+package shortcuts
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/Bios-Marcel/tview"
+ "github.com/gdamore/tcell"
+)
+
+const (
+ actionCellIndex int = iota
+ scopeCellIndex
+ shortcutCellIndex
+)
+
+// ShortcutTable is a component that displays shortcuts and allows changing
+// them.
+type ShortcutTable struct {
+ table *tview.Table
+ shortcuts []*Shortcut
+ onClose func()
+ selection int
+}
+
+// NewShortcutTable creates a new shortcut table that doesn't contain any data.
+func NewShortcutTable() *ShortcutTable {
+ table := tview.NewTable()
+ shortcutsTable := &ShortcutTable{
+ table: table,
+ selection: -1,
+ }
+
+ table.SetSelectable(true, false)
+ table.SetBorder(true)
+
+ //Header + emptyrow
+ table.SetFixed(2, 3)
+
+ table.SetCell(0, actionCellIndex, createHeaderCell("Action"))
+ table.SetCell(0, scopeCellIndex, createHeaderCell("Scope"))
+ table.SetCell(0, shortcutCellIndex, createHeaderCell("Shortcut"))
+
+ table.SetInputCapture(shortcutsTable.handleInput)
+
+ return shortcutsTable
+}
+
+// GetPrimitive returns the primitive to be put onto a layout.
+func (shortcutTable *ShortcutTable) GetPrimitive() tview.Primitive {
+ return shortcutTable.table
+}
+
+func createHeaderCell(text string) *tview.TableCell {
+ return tview.NewTableCell(text).
+ SetSelectable(false).
+ SetAlign(tview.AlignCenter).
+ SetExpansion(1).
+ SetMaxWidth(1)
+}
+
+// SetShortcuts sets the shortcut data and changes the UI accordingly.
+func (shortcutTable *ShortcutTable) SetShortcuts(shortcuts []*Shortcut) {
+ shortcutTable.shortcuts = shortcuts
+
+ row, _ := shortcutTable.table.GetFixed()
+
+ //Using clear will remove the content of the fixed rows, therefore we
+ // manually remove starting from the first non-fixed row.
+ for index := row; index < shortcutTable.table.GetRowCount(); index++ {
+ shortcutTable.table.RemoveRow(index)
+ }
+
+ for _, shortcut := range shortcuts {
+ nameCell := tview.NewTableCell(shortcut.Name).
+ SetExpansion(1).
+ SetMaxWidth(1)
+ shortcutTable.table.SetCell(row, actionCellIndex, nameCell)
+
+ scopeCell := tview.NewTableCell(shortcut.scope.Name).
+ SetExpansion(1).
+ SetMaxWidth(1)
+ shortcutTable.table.SetCell(row, scopeCellIndex, scopeCell)
+
+ eventCell := tview.NewTableCell(EventToString(shortcut.Event)).
+ SetExpansion(1).
+ SetMaxWidth(1)
+ shortcutTable.table.SetCell(row, shortcutCellIndex, eventCell)
+
+ row++
+ }
+}
+
+// GetShortcuts returns the array containing the currently displayed shortcuts.
+func (shortcutTable *ShortcutTable) GetShortcuts() []*Shortcut {
+ return shortcutTable.shortcuts
+}
+
+func (shortcutTable *ShortcutTable) handleInput(event *tcell.EventKey) *tcell.EventKey {
+ if shortcutTable.selection == -1 {
+ if event.Key() == tcell.KeyESC {
+ if shortcutTable.onClose != nil {
+ shortcutTable.onClose()
+ }
+ return nil
+ }
+
+ if event.Key() == tcell.KeyUp || event.Key() == tcell.KeyDown {
+ return event
+ }
+ }
+
+ firstNonFixedRow, _ := shortcutTable.table.GetFixed()
+ selectedRow, _ := shortcutTable.table.GetSelection()
+ //The first row of the table isn't the first row containing data
+ dataIndex := selectedRow - firstNonFixedRow
+ if shortcutTable.selection == -1 && selectedRow >= firstNonFixedRow {
+ if event.Key() == tcell.KeyEnter {
+ if selectedRow != -1 {
+ shortcutTable.table.GetCell(selectedRow, shortcutCellIndex).SetText("[blue][::ub]Hit the desired keycombination")
+ shortcutTable.selection = selectedRow
+ }
+ } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
+ shortcutTable.table.GetCell(selectedRow, shortcutCellIndex).SetText("")
+ shortcutTable.shortcuts[dataIndex].Event = nil
+ }
+ } else if selectedRow >= firstNonFixedRow && shortcutTable.selection != -1 {
+ shortcutTable.table.GetCell(selectedRow, shortcutCellIndex).SetText(EventToString(event))
+
+ shortcutTable.shortcuts[dataIndex].Event = tcell.NewEventKey(
+ event.Key(),
+ event.Rune(),
+ event.Modifiers())
+
+ persistError := Persist()
+ if persistError != nil {
+ panic(persistError)
+ }
+
+ shortcutTable.selection = -1
+ }
+
+ return nil
+}
+
+// SetOnClose sets the handler that will be run when someone attempts closing
+// the shortcuts table.
+func (shortcutTable *ShortcutTable) SetOnClose(onClose func()) {
+ shortcutTable.onClose = onClose
+}
+
+// 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()
+}
+
+// EventToString renders a tcell.EventKey as a human readable string
+func EventToString(event *tcell.EventKey) string {
+ s := ""
+ m := []string{}
+ if event.Modifiers()&tcell.ModCtrl != 0 {
+ m = append(m, "Ctrl")
+ }
+ if event.Modifiers()&tcell.ModShift != 0 {
+ m = append(m, "Shift")
+ }
+ if event.Modifiers()&tcell.ModAlt != 0 {
+ m = append(m, "Alt")
+ }
+ if event.Modifiers()&tcell.ModMeta != 0 {
+ m = append(m, "Meta")
+ }
+
+ ok := false
+ if s, ok = tcell.KeyNames[event.Key()]; !ok {
+ if event.Key() == tcell.KeyRune {
+ if event.Rune() >= 'A' && event.Rune() <= 'Z' {
+ s = "Shift+" + string(event.Rune())
+ } else {
+ s = strings.ToUpper(string(event.Rune()))
+ }
+ } else {
+ s = fmt.Sprintf("Key[%d,%d]", event.Key(), int(event.Rune()))
+ }
+ }
+ if len(m) != 0 {
+ if event.Modifiers()&tcell.ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") {
+ s = s[5:]
+ }
+ return fmt.Sprintf("%s+%s", strings.Join(m, "+"), s)
+ }
+
+ return tview.Escape(s)
+}

MODIFY
internal/ui/window.go
internal/ui/window.go
@@ -17,6 +17,7 @@ import (
"github.com/Bios-Marcel/cordless/internal/maths"
"github.com/Bios-Marcel/cordless/internal/scripting"
"github.com/Bios-Marcel/cordless/internal/scripting/js"
+ "github.com/Bios-Marcel/cordless/internal/shortcuts"
"github.com/Bios-Marcel/cordless/internal/times"
"github.com/Bios-Marcel/cordless/internal/ui/tview/treeview"
"github.com/Bios-Marcel/discordemojimap"
@@ -42,6 +43,7 @@ var (
type Window struct {
app *tview.Application
rootContainer *tview.Flex
+ currentContainer tview.Primitive
leftArea *tview.Pages
guildList *tview.TreeView
@@ -544,6 +546,7 @@ func NewWindow(doRestart chan bool, app *tview.Application, discord *discordgo.S
window.rootContainer.SetTitleAlign(tview.AlignCenter)
app.SetRoot(window.rootContainer, true)
+ window.currentContainer = window.rootContainer
app.SetInputCapture(window.handleGlobalShortcuts)
conf := config.GetConfig()
@@ -1007,6 +1010,7 @@ func (window *Window) askForMessageDeletion(messageID string, usedWithSelection
window.exitMessageEditMode()
window.app.SetRoot(window.rootContainer, true)
+ window.currentContainer = window.rootContainer
if usedWithSelection {
window.app.SetFocus(previousFocus)
window.chatView.SignalSelectionDeleted()
@@ -1017,6 +1021,7 @@ func (window *Window) askForMessageDeletion(messageID string, usedWithSelection
})
window.app.SetRoot(dialog, false)
+ window.currentContainer = dialog
}
// SetCommandModeEnabled hides or shows the command ui elements and toggles
@@ -1034,12 +1039,27 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
return event
}
+ // Maybe compare directly to table?
+ if window.currentContainer != window.rootContainer {
+ return event
+ }
+
window.userActive = true
window.userActiveTimer.Reset(10 * time.Second)
- if event.Rune() == '.' &&
- (event.Modifiers()&tcell.ModAlt) == tcell.ModAlt {
+ if event.Modifiers()&tcell.ModAlt == tcell.ModAlt && event.Rune() == 'S' {
+ table := shortcuts.NewShortcutTable()
+ table.SetShortcuts(shortcuts.Shortcuts)
+ table.SetOnClose(func() {
+ window.app.SetRoot(window.rootContainer, true)
+ window.currentContainer = window.rootContainer
+ })
+ window.app.SetRoot(table.GetPrimitive(), true)
+ window.currentContainer = table.GetPrimitive()
+ }
+
+ if shortcuts.EventsEqual(event, shortcuts.ToggleCommandView.Event) {
window.SetCommandModeEnabled(!window.commandMode)
if window.commandMode {
@@ -1051,7 +1071,7 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
return nil
}
- if window.commandMode && event.Key() == tcell.KeyCtrlO {
+ if shortcuts.EventsEqual(event, shortcuts.FocusCommandOutput.Event) {
if !window.commandMode {
window.SetCommandModeEnabled(true)
}
@@ -1059,7 +1079,7 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
window.app.SetFocus(window.commandView.commandOutput)
}
- if window.commandMode && event.Key() == tcell.KeyCtrlI {
+ if shortcuts.EventsEqual(event, shortcuts.FocusCommandInput.Event) {
if !window.commandMode {
window.SetCommandModeEnabled(true)
}
@@ -1067,8 +1087,7 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
window.app.SetFocus(window.commandView.commandInput.internalTextView)
}
- if event.Rune() == 'U' &&
- (event.Modifiers()&tcell.ModAlt) == tcell.ModAlt {
+ if shortcuts.EventsEqual(event, shortcuts.ToggleUserContainer.Event) {
conf := config.GetConfig()
conf.ShowUserContainer = !conf.ShowUserContainer
@@ -1081,42 +1100,40 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
return nil
}
- if event.Modifiers()&tcell.ModAlt == tcell.ModAlt {
- if event.Rune() == 'p' {
- window.SwitchToFriendsPage()
- window.app.SetFocus(window.privateList.GetComponent())
+ if shortcuts.EventsEqual(event, shortcuts.FocusChannelContainer.Event) {
+ window.SwitchToGuildsPage()
+ window.app.SetFocus(window.channelTree.internalTreeView)
return nil
}
- if event.Rune() == 'c' {
- window.SwitchToGuildsPage()
- window.app.SetFocus(window.channelTree.internalTreeView)
+ if shortcuts.EventsEqual(event, shortcuts.FocusPrivateChatPage.Event) {
+ window.SwitchToFriendsPage()
+ window.app.SetFocus(window.privateList.GetComponent())
return nil
}
- if event.Rune() == 's' {
+ if shortcuts.EventsEqual(event, shortcuts.FocusGuildContainer.Event) {
window.SwitchToGuildsPage()
window.app.SetFocus(window.guildList)
return nil
}
- if event.Rune() == 't' {
+ if shortcuts.EventsEqual(event, shortcuts.FocusMessageContainer.Event) {
window.app.SetFocus(window.chatView.internalTextView)
return nil
}
- if event.Rune() == 'u' {
+ if shortcuts.EventsEqual(event, shortcuts.FocusUserContainer.Event) {
if window.leftArea.GetCurrentPage() == guildPageName && window.userList.internalTreeView.IsVisible() {
window.app.SetFocus(window.userList.internalTreeView)
}
return nil
}
- if event.Rune() == 'm' {
+ if shortcuts.EventsEqual(event, shortcuts.FocusMessageInput.Event) {
window.app.SetFocus(window.messageInput.GetPrimitive())
return nil
}
- }
return event
}
@@ -1165,10 +1182,12 @@ func (window *Window) ShowErrorDialog(text string) {
dialog.SetDoneFunc(func(index int, label string) {
window.app.SetRoot(window.rootContainer, true)
+ window.currentContainer = window.rootContainer
window.app.SetFocus(previousFocus)
})
window.app.SetRoot(dialog, false)
+ window.currentContainer = dialog
}
func (window *Window) editMessage(channelID, messageID, messageEdited string) {

First working version of the shortcuts table. Fixes #58