Add multiaccount feature
MODIFY
internal/app.go
internal/app.go
@@ -42,6 +42,7 @@ func Run() {
splashScreen.SetTextAlign(tview.AlignCenter)
splashScreen.SetText(tview.Escape(splashText + "\n\nConfig lies at: " + configDir))
app.SetRoot(splashScreen, true)
+ runNext := make(chan bool, 1)
go func() {
configuration, configLoadError := config.LoadConfig()
@@ -75,7 +76,7 @@ func Run() {
<-readyChan
app.QueueUpdateDraw(func() {
- window, createError := ui.NewWindow(app, discord)
+ window, createError := ui.NewWindow(runNext, app, discord)
if createError != nil {
app.Stop()
@@ -83,6 +84,7 @@ func Run() {
}
window.RegisterCommand(commandimpls.NewStatusCommand(discord))
+ window.RegisterCommand(commandimpls.NewAccount(runNext, window))
window.RegisterCommand(commandimpls.NewHelpCommand(window))
window.RegisterCommand(commandimpls.NewManualCommand())
window.RegisterCommand(commandimpls.NewFixLayoutCommand(window))
@@ -94,6 +96,11 @@ func Run() {
if runError != nil {
log.Fatalf("Error launching View (%s).\n", runError.Error())
}
+
+ run := <-runNext
+ if run {
+ Run()
+ }
}
func attemptLogin(loginMessage string, app *tview.Application, configuration *config.Config) *discordgo.Session {

ADD
internal/commands/commandimpls/account.go
+package commandimpls
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/Bios-Marcel/cordless/internal/config"
+ "github.com/Bios-Marcel/cordless/internal/ui"
+)
+
+const accountDocumentation = `[orange][::u]# account command[white]
+
+The account command allows you to manage multiple discord accounts within
+cordless.
+
+Subcommands:
+ * add
+ * delete
+ * switch
+ * list
+ * current
+ * add-current
+`
+
+// Account manages the users account
+type Account struct {
+ window *ui.Window
+ runNext chan bool
+}
+
+// NewAccount creates a ready-to-use Account command.
+func NewAccount(runNext chan bool, window *ui.Window) *Account {
+ return &Account{window: window, runNext: runNext}
+}
+
+// Execute runs the command piping its output into the supplied writer.
+func (account *Account) Execute(writer io.Writer, parameters []string) {
+ if len(parameters) == 0 {
+ account.PrintHelp(writer)
+ } else {
+ switch parameters[0] {
+ case "add", "create", "new":
+ if len(parameters) != 3 {
+ account.printAccountAddHelp(writer)
+ } else {
+ account.addAcount(writer, parameters[1:])
+ }
+ case "delete", "remove":
+ if len(parameters) != 2 {
+ account.printAccountDeleteHelp(writer)
+ } else {
+ deleteAccount(writer, parameters[1])
+ }
+ case "switch", "change":
+ if len(parameters) != 2 {
+ account.printAccountSwitchHelp(writer)
+ } else {
+ account.switchAccount(writer, parameters[1])
+ }
+ case "list":
+ if len(parameters) != 1 {
+ account.printAccountListHelp(writer)
+ } else {
+ account.listAccounts(writer)
+ }
+ case "current":
+ if len(parameters) != 1 {
+ account.printAccountCurrentHelp(writer)
+ } else {
+ account.currentAccount(writer)
+ }
+ case "add-current":
+ if len(parameters) != 2 {
+ account.printAccountAddCurrentHelp(writer)
+ } else {
+ account.addCurrentAccount(writer, parameters[1])
+ }
+ }
+ }
+}
+
+func (account *Account) printAccountAddHelp(writer io.Writer) {
+ fmt.Fprintln(writer, "Usage: account add <Name> <Token>")
+}
+
+func (account *Account) addAcount(writer io.Writer, parameters []string) {
+ newName := strings.ToLower(parameters[0])
+ for _, acc := range config.GetConfig().Accounts {
+ if acc.Name == newName {
+ fmt.Fprintf(writer, "[red]The name '%s' is already in use.\n", acc.Name)
+ return
+ }
+ }
+
+ newAccount := &config.Account{
+ Name: newName,
+ Token: parameters[1],
+ }
+ config.GetConfig().Accounts = append(config.GetConfig().Accounts, newAccount)
+ config.PersistConfig()
+
+ fmt.Fprintf(writer, "The account '%s' has been created successfully.\n", newName)
+}
+
+func (account *Account) printAccountDeleteHelp(writer io.Writer) {
+ fmt.Fprintln(writer, "Usage `account delete <Name>`")
+}
+
+func deleteAccount(writer io.Writer, account string) {
+ var deletionSuccessful bool
+
+ newAccounts := make([]*config.Account, 0)
+ for _, acc := range config.GetConfig().Accounts {
+ if acc.Name != account {
+ newAccounts = append(newAccounts, acc)
+ } else {
+ deletionSuccessful = true
+ }
+ }
+
+ if deletionSuccessful {
+ fmt.Fprintf(writer, "Account '%s' has been deleted.\n", account)
+ config.GetConfig().Accounts = newAccounts
+ config.PersistConfig()
+ } else {
+ fmt.Fprintf(writer, "[red]Account '%s' could not be found.\n", account)
+ }
+
+}
+
+func (account *Account) printAccountSwitchHelp(writer io.Writer) {
+ fmt.Fprintln(writer, "Usage: account switch <Name>")
+}
+
+func (account *Account) switchAccount(writer io.Writer, accountName string) {
+ var newToken string
+ for _, acc := range config.GetConfig().Accounts {
+ if acc.Name == accountName {
+ newToken = acc.Token
+ break
+ }
+ }
+
+ if newToken == "" {
+ //TODO Not found. Throw error?
+ }
+
+ config.GetConfig().Token = newToken
+ config.PersistConfig()
+
+ //Using a go routine, so this instance doesn't stay alive and polutes the memory.
+ account.runNext <- true
+ account.window.Shutdown()
+}
+
+func (account *Account) printAccountListHelp(writer io.Writer) {
+ fmt.Fprintln(writer, "Usage: account list")
+}
+
+func (account *Account) listAccounts(writer io.Writer) {
+ fmt.Fprintln(writer, "Available accounts:")
+ for _, acc := range config.GetConfig().Accounts {
+ fmt.Fprintln(writer, " * "+acc.Name)
+ }
+}
+
+func (account *Account) printAccountCurrentHelp(writer io.Writer) {
+ fmt.Fprintln(writer, "Usage: account current")
+}
+
+func (account *Account) currentAccount(writer io.Writer) {
+ var currentAccount *config.Account
+ for _, acc := range config.GetConfig().Accounts {
+ if acc.Token == config.GetConfig().Token {
+ currentAccount = acc
+ break
+ }
+ }
+
+ if currentAccount != nil {
+ fmt.Fprintf(writer, "Current account '%s'.\n", currentAccount.Name)
+ } else {
+ fmt.Fprintln(writer, "Current token belongs to no saved account.")
+ }
+}
+
+func (account *Account) printAccountAddCurrentHelp(writer io.Writer) {
+ fmt.Fprintln(writer, "Usage: account add-current <Name>")
+}
+
+func (account *Account) addCurrentAccount(writer io.Writer, name string) {
+ account.addAcount(writer, []string{name, config.GetConfig().Token})
+}
+
+// Name represents this commands indentifier.
+func (account *Account) Name() string {
+ return "account"
+}
+
+// PrintHelp prints a static help page for this command
+func (account *Account) PrintHelp(writer io.Writer) {
+ fmt.Fprintln(writer, accountDocumentation)
+}

MODIFY
internal/config/config.go
internal/config/config.go
@@ -98,6 +98,17 @@ type Config struct {
// DesktopNotifications decides wether a popup will be shown in the users
// system when a notification needs to be sent.
DesktopNotifications bool
+
+ // Accounts contains all saved accounts, allowing the user to dynamicly
+ // switch between the accounts.
+ Accounts []*Account
+}
+
+// Account has a name and a token. The name is just for the users recognition.
+// The token is the actual token used to authenticate against the discord API.
+type Account struct {
+ Name string
+ Token string
}
var cachedConfigDir string

MODIFY
internal/scripting/js/javascript.go
internal/scripting/js/javascript.go
@@ -13,6 +13,7 @@ import (
"github.com/robertkrimen/otto"
)
+//FIXME Remove global state
var _ scripting.Engine = &JavaScriptEngine{}
// JavaScriptEngine stores scripting engine state

MODIFY
internal/ui/commandview.go
internal/ui/commandview.go
@@ -11,7 +11,7 @@ import (
// input. All commands are added to the history when confirmed via enter.
type CommandView struct {
commandOutput *tview.TextView
- commandInput *tview.InputField
+ commandInput *Editor
commandHistoryIndex int
commandHistory []string
@@ -28,8 +28,10 @@ func NewCommandView(onExecuteCommand func(command string)) *CommandView {
commandOutput.SetWordWrap(true)
commandOutput.SetWrap(true)
- commandInput := tview.NewInputField()
- commandInput.SetBorder(true)
+ commandInput := NewEditor()
+ commandInput.internalTextView.
+ SetWrap(false).
+ SetWordWrap(false)
cmdView := &CommandView{
commandOutput: commandOutput,
@@ -124,8 +126,8 @@ func (cmdView *CommandView) handleInput(event *tcell.EventKey) *tcell.EventKey {
// GetCommandInputWidget returns the component that can be added to the layout
// for the users command input.
-func (cmdView *CommandView) GetCommandInputWidget() *tview.InputField {
- return cmdView.commandInput
+func (cmdView *CommandView) GetCommandInputWidget() *tview.TextView {
+ return cmdView.commandInput.internalTextView
}
// GetCommandOutputWidget is the component that can be added to the layout
@@ -137,7 +139,7 @@ func (cmdView *CommandView) GetCommandOutputWidget() *tview.TextView {
// SetVisible sets the given visible state to both the input component and
// the output component
func (cmdView *CommandView) SetVisible(visible bool) {
- cmdView.commandInput.SetVisible(visible)
+ cmdView.commandInput.internalTextView.SetVisible(visible)
cmdView.commandOutput.SetVisible(visible)
}

MODIFY
internal/ui/editor.go
internal/ui/editor.go
@@ -318,12 +318,16 @@ func NewEditor() *Editor {
editor.currentMentionBeginIdx = atIndex + 1
editor.currentMentionEndIdx = endIndex + atIndex
+ if editor.mentionShowHandler != nil {
editor.mentionShowHandler(lookup[:endIndex])
+ }
} else {
editor.currentMentionBeginIdx = 0
editor.currentMentionEndIdx = 0
+ if editor.mentionHideHandler != nil {
editor.mentionHideHandler()
}
+ }
editor.triggerHeightRequestIfNeccessary()
editor.internalTextView.ScrollToHighlight()

MODIFY
internal/ui/window.go
internal/ui/window.go
@@ -77,13 +77,16 @@ type Window struct {
userActive bool
userActiveTimer *time.Timer
+
+ doRestart chan bool
}
//NewWindow constructs the whole application window and also registers all
//necessary handlers and functions. If this function returns an error, we can't
//start the application.
-func NewWindow(app *tview.Application, discord *discordgo.Session) (*Window, error) {
+func NewWindow(doRestart chan bool, app *tview.Application, discord *discordgo.Session) (*Window, error) {
window := Window{
+ doRestart: doRestart,
session: discord,
app: app,
commands: make(map[string]commands.Command, 1),
@@ -610,10 +613,10 @@ func NewWindow(app *tview.Application, discord *discordgo.Session) (*Window, err
window.chatArea.AddItem(window.messageInput.GetPrimitive(), window.messageInput.GetRequestedHeight(), 0, false)
window.commandView.commandOutput.SetVisible(false)
- window.commandView.commandInput.SetVisible(false)
+ window.commandView.commandInput.internalTextView.SetVisible(false)
window.chatArea.AddItem(window.commandView.commandOutput, 0, 1, false)
- window.chatArea.AddItem(window.commandView.commandInput, 3, 0, false)
+ window.chatArea.AddItem(window.commandView.commandInput.internalTextView, 3, 0, false)
if conf.ShowFrame {
window.rootContainer.SetTitle("Cordless")
@@ -933,6 +936,11 @@ func (window *Window) SetCommandModeEnabled(enabled bool) {
}
func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyCtrlC {
+ window.doRestart <- false
+ return event
+ }
+
window.userActive = true
window.userActiveTimer.Reset(10 * time.Second)
@@ -942,7 +950,7 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
window.SetCommandModeEnabled(!window.commandMode)
if window.commandMode {
- window.app.SetFocus(window.commandView.commandInput)
+ window.app.SetFocus(window.commandView.commandInput.internalTextView)
} else {
window.app.SetFocus(window.messageInput.GetPrimitive())
}
@@ -963,7 +971,7 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK
window.SetCommandModeEnabled(true)
}
- window.app.SetFocus(window.commandView.commandInput)
+ window.app.SetFocus(window.commandView.commandInput.internalTextView)
}
if event.Rune() == 'U' &&
@@ -1207,3 +1215,11 @@ func (window *Window) GetRegisteredCommands() map[string]commands.Command {
func (window *Window) Run() error {
return window.app.Run()
}
+
+// Shutdown disconnects from the discord API and stops the tview application.
+// TODO: Kill Linkshortener
+func (window *Window) Shutdown() {
+ window.chatView.shortener.Close()
+ window.session.Close()
+ window.app.Stop()
+}

Add multiaccount feature