ソースを参照

Rename configuration fields and UI widgets

ayn2op 3 年 前
コミット
433da8cd68
11 ファイル変更262 行追加310 行削除
  1. 81 73
      config/config.go
  2. 2 2
      main.go
  3. 0 0
      out.log
  4. 51 51
      ui/actions_list.go
  5. 13 13
      ui/application.go
  6. 16 16
      ui/channels_tree.go
  7. 13 13
      ui/guilds_tree.go
  8. 14 14
      ui/login_form.go
  9. 27 27
      ui/message_input.go
  10. 32 32
      ui/messages_text.go
  11. 13 69
      ui/view.go

+ 81 - 73
config/config.go

@@ -11,25 +11,28 @@ import (
 
 const Name = "discordo"
 
-type MessagesViewKeysConfig struct {
-	OpenActionsView string `yaml:"open_actions_view"`
+type (
+	MessagesTextKeysConfig struct {
+		LaunchActions string `yaml:"launch_actions"`
+
+		SelectPrevious string `yaml:"select_previous"`
+		SelectNext     string `yaml:"select_next"`
+		SelectFirst    string `yaml:"select_first"`
+		SelectLast     string `yaml:"select_last"`
+	}
 
-	SelectPreviousMessage string `yaml:"select_previous_message"`
-	SelectNextMessage     string `yaml:"select_next_message"`
-	SelectFirstMessage    string `yaml:"select_first_message"`
-	SelectLastMessage     string `yaml:"select_last_message"`
-}
+	MessageInputKeysConfig struct {
+		Send  string `yaml:"send"`
+		Paste string `yaml:"paste"`
 
-type InputViewKeysConfig struct {
-	SendMessage        string `yaml:"send_message"`
-	OpenExternalEditor string `yaml:"open_external_editor"`
-	PasteClipboard     string `yaml:"paste_clipboard"`
-}
+		LaunchEditor string `yaml:"launch_editor"`
+	}
 
-type KeysConfig struct {
-	MessagesView MessagesViewKeysConfig `yaml:"messages_view"`
-	InputView    InputViewKeysConfig    `yaml:"input_view"`
-}
+	KeysConfig struct {
+		MessagesText MessagesTextKeysConfig `yaml:"messages_text"`
+		MessageInput MessageInputKeysConfig `yaml:"message_input"`
+	}
+)
 
 type ThemeConfig struct {
 	Background string `yaml:"background"`
@@ -48,34 +51,77 @@ type Config struct {
 	Timezone string `yaml:"timezone"`
 	// A textual representation of the time value formatted according to the layout defined by its value. Learn more: https://pkg.go.dev/time#Layout
 	TimeFormat string `yaml:"time_format"`
-	// Keybindings
-	Keys KeysConfig `yaml:"keys"`
-	// Theme
+
+	Keys  KeysConfig  `yaml:"keys"`
 	Theme ThemeConfig `yaml:"theme"`
 }
 
-func New() *Config {
-	return &Config{
+func New() (*Config, error) {
+	path, err := os.UserConfigDir()
+	if err != nil {
+		return nil, err
+	}
+
+	// Create the configuration directory if it does not exist already.
+	path = filepath.Join(path, Name)
+	err = os.MkdirAll(path, os.ModePerm)
+	if err != nil {
+		return nil, err
+	}
+
+	c := def()
+	path = filepath.Join(path, "config.yml")
+	_, err = os.Stat(path)
+	// If the configuration file does not exist, create a new one and write the default configuration to it.
+	if os.IsNotExist(err) {
+		f, err := os.Create(path)
+		if err != nil {
+			return nil, err
+		}
+		defer f.Close()
+
+		err = yaml.NewEncoder(f).Encode(c)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		f, err := os.Open(path)
+		if err != nil {
+			return nil, err
+		}
+		defer f.Close()
+
+		err = yaml.NewDecoder(f).Decode(&c)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &c, nil
+}
+
+func def() Config {
+	return Config{
 		Mouse:         true,
 		MessagesLimit: 50,
-
-		Timestamps: false,
-		Timezone:   "Local",
-		TimeFormat: time.Kitchen,
+		Timestamps:    false,
+		Timezone:      "Local",
+		TimeFormat:    time.Kitchen,
 
 		Keys: KeysConfig{
-			MessagesView: MessagesViewKeysConfig{
-				OpenActionsView: "Rune[a]",
+			MessagesText: MessagesTextKeysConfig{
+				LaunchActions: "Rune[a]",
 
-				SelectPreviousMessage: "Up",
-				SelectNextMessage:     "Down",
-				SelectFirstMessage:    "Home",
-				SelectLastMessage:     "End",
+				SelectPrevious: "Up",
+				SelectNext:     "Down",
+				SelectFirst:    "Home",
+				SelectLast:     "End",
 			},
-			InputView: InputViewKeysConfig{
-				SendMessage:        "Enter",
-				OpenExternalEditor: "Ctrl+E",
-				PasteClipboard:     "Ctrl+V",
+			MessageInput: MessageInputKeysConfig{
+				Send:  "Enter",
+				Paste: "Ctrl+V",
+
+				LaunchEditor: "Ctrl+E",
 			},
 		},
 		Theme: ThemeConfig{
@@ -86,44 +132,6 @@ func New() *Config {
 	}
 }
 
-func (cfg *Config) Load() error {
-	path := DirPath()
-	// Create the configuration directory if it does not exist already.
-	err := os.MkdirAll(path, os.ModePerm)
-	if err != nil {
-		return err
-	}
-
-	// Open the configuration file with create and read-write flag.
-	f, err := os.OpenFile(
-		filepath.Join(path, "config.yml"),
-		os.O_CREATE|os.O_RDWR,
-		os.ModePerm,
-	)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	fi, err := f.Stat()
-	if err != nil {
-		return err
-	}
-
-	// If the configuration file is empty (the size of the file is zero; a new configuration file was created), write the default configuration to the file.
-	if fi.Size() == 0 {
-		return yaml.NewEncoder(f).Encode(cfg)
-	}
-
-	return yaml.NewDecoder(f).Decode(&cfg)
-}
-
-// DirPath returns the path of the configuration directory.
-func DirPath() string {
-	path, _ := os.UserConfigDir()
-	return filepath.Join(path, Name)
-}
-
 // LogDirPath returns the path of the log directory.
 func LogDirPath() string {
 	path, _ := os.UserCacheDir()

+ 2 - 2
main.go

@@ -38,8 +38,8 @@ func init() {
 func main() {
 	flag.Parse()
 
-	cfg := config.New()
-	if err := cfg.Load(); err != nil {
+	cfg, err := config.New()
+	if err != nil {
 		log.Fatal(err)
 	}
 

+ 0 - 0
out.log


+ 51 - 51
ui/actions_view.go → ui/actions_list.go

@@ -15,14 +15,14 @@ import (
 
 var linkRegex = regexp.MustCompile("https?://.+")
 
-type ActionsView struct {
+type ActionsList struct {
 	*tview.List
 	app     *Application
 	message *discord.Message
 }
 
-func newActionsView(app *Application, m *discord.Message) *ActionsView {
-	v := &ActionsView{
+func newActionsList(app *Application, m *discord.Message) *ActionsList {
+	v := &ActionsList{
 		List:    tview.NewList(),
 		app:     app,
 		message: m,
@@ -31,13 +31,13 @@ func newActionsView(app *Application, m *discord.Message) *ActionsView {
 	v.ShowSecondaryText(false)
 	v.SetDoneFunc(func() {
 		app.SetRoot(app.view, true)
-		app.SetFocus(app.view.MessagesView)
+		app.SetFocus(app.view.MessagesText)
 	})
 
-	isDM := channelIsInDMCategory(app.view.ChannelsView.selected)
+	isDM := channelIsInDMCategory(app.view.ChannelsTree.selected)
 
 	// If the client user has the `SEND_MESSAGES` permission, add "Reply" and "Mention Reply" actions.
-	if isDM || !isDM && hasPermission(app.state, app.view.ChannelsView.selected.ID, discord.PermissionSendMessages) {
+	if isDM || !isDM && hasPermission(app.state, app.view.ChannelsTree.selected.ID, discord.PermissionSendMessages) {
 		v.AddItem("Reply", "", 'r', v.replyAction)
 		v.AddItem("Mention Reply", "", 'R', v.mentionReplyAction)
 	}
@@ -56,7 +56,7 @@ func newActionsView(app *Application, m *discord.Message) *ActionsView {
 			}
 
 			app.SetRoot(app.view, true)
-			app.SetFocus(app.view.MessagesView)
+			app.SetFocus(app.view.MessagesText)
 		})
 	}
 
@@ -69,7 +69,7 @@ func newActionsView(app *Application, m *discord.Message) *ActionsView {
 	me, _ := app.state.MeStore.Me()
 
 	// If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message.
-	if (isDM && m.Author.ID == me.ID) || (!isDM && hasPermission(app.state, app.view.ChannelsView.selected.ID, discord.PermissionManageMessages)) {
+	if (isDM && m.Author.ID == me.ID) || (!isDM && hasPermission(app.state, app.view.ChannelsTree.selected.ID, discord.PermissionManageMessages)) {
 		v.AddItem("Delete", "", 'd', v.deleteAction)
 	}
 
@@ -85,37 +85,37 @@ func newActionsView(app *Application, m *discord.Message) *ActionsView {
 	return v
 }
 
-func (v *ActionsView) replyAction() {
-	v.app.view.InputView.SetTitle("Replying to " + v.message.Author.Tag())
+func (al *ActionsList) replyAction() {
+	al.app.view.MessageInput.SetTitle("Replying to " + al.message.Author.Tag())
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.InputView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessageInput)
 }
 
-func (v *ActionsView) mentionReplyAction() {
-	v.app.view.InputView.SetTitle("[@] Replying to " + v.message.Author.Tag())
+func (al *ActionsList) mentionReplyAction() {
+	al.app.view.MessageInput.SetTitle("[@] Replying to " + al.message.Author.Tag())
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.InputView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessageInput)
 }
 
-func (v *ActionsView) selectReplyAction() {
-	ms, err := v.app.state.Cabinet.Messages(v.message.ChannelID)
+func (al *ActionsList) selectReplyAction() {
+	ms, err := al.app.state.Cabinet.Messages(al.message.ChannelID)
 	if err != nil {
 		return
 	}
 
-	v.app.view.MessagesView.selected, _ = findMessageByID(ms, v.message.ReferencedMessage.ID)
-	v.app.view.MessagesView.
-		Highlight(v.message.ReferencedMessage.ID.String()).
+	al.app.view.MessagesText.selected, _ = findMessageByID(ms, al.message.ReferencedMessage.ID)
+	al.app.view.MessagesText.
+		Highlight(al.message.ReferencedMessage.ID.String()).
 		ScrollToHighlight()
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }
 
-func (v *ActionsView) openAttachmentAction() {
-	for _, a := range v.message.Attachments {
+func (al *ActionsList) openAttachmentAction() {
+	for _, a := range al.message.Attachments {
 		cacheDirPath, _ := os.UserCacheDir()
 		f, err := os.Create(filepath.Join(cacheDirPath, a.Filename))
 		if err != nil {
@@ -137,12 +137,12 @@ func (v *ActionsView) openAttachmentAction() {
 		go open.Run(f.Name())
 	}
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }
 
-func (v *ActionsView) downloadAttachmentAction() {
-	for _, a := range v.message.Attachments {
+func (al *ActionsList) downloadAttachmentAction() {
+	for _, a := range al.message.Attachments {
 		path, err := os.UserHomeDir()
 		if err != nil {
 			path = os.TempDir()
@@ -168,66 +168,66 @@ func (v *ActionsView) downloadAttachmentAction() {
 		f.Write(d)
 	}
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }
 
-func (v *ActionsView) deleteAction() {
-	v.app.view.MessagesView.Clear()
+func (al *ActionsList) deleteAction() {
+	al.app.view.MessagesText.Clear()
 
-	err := v.app.state.MessageRemove(v.message.ChannelID, v.message.ID)
+	err := al.app.state.MessageRemove(al.message.ChannelID, al.message.ID)
 	if err != nil {
 		return
 	}
 
-	err = v.app.state.DeleteMessage(v.message.ChannelID, v.message.ID, "Unknown")
+	err = al.app.state.DeleteMessage(al.message.ChannelID, al.message.ID, "Unknown")
 	if err != nil {
 		return
 	}
 
 	// The returned slice will be sorted from latest to oldest.
-	ms, err := v.app.state.Cabinet.Messages(v.message.ChannelID)
+	ms, err := al.app.state.Cabinet.Messages(al.message.ChannelID)
 	if err != nil {
 		return
 	}
 
 	for i := len(ms) - 1; i >= 0; i-- {
-		_, err = v.app.view.MessagesView.Write(buildMessage(v.app, ms[i]))
+		_, err = al.app.view.MessagesText.Write(buildMessage(al.app, ms[i]))
 		if err != nil {
 			return
 		}
 	}
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }
 
-func (v *ActionsView) copyContentAction() {
-	err := clipboard.WriteAll(v.message.Content)
+func (al *ActionsList) copyContentAction() {
+	err := clipboard.WriteAll(al.message.Content)
 	if err != nil {
 		return
 	}
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }
 
-func (v *ActionsView) copyIDAction() {
-	err := clipboard.WriteAll(v.message.ID.String())
+func (al *ActionsList) copyIDAction() {
+	err := clipboard.WriteAll(al.message.ID.String())
 	if err != nil {
 		return
 	}
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }
 
-func (v *ActionsView) copyLinkAction() {
-	err := clipboard.WriteAll(v.message.URL())
+func (al *ActionsList) copyLinkAction() {
+	err := clipboard.WriteAll(al.message.URL())
 	if err != nil {
 		return
 	}
 
-	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.MessagesView)
+	al.app.SetRoot(al.app.view, true)
+	al.app.SetFocus(al.app.view.MessagesText)
 }

+ 13 - 13
ui/application.go

@@ -73,9 +73,9 @@ func (app *Application) Run(token string) {
 		}
 
 		app.SetRoot(app.view, true)
-		app.SetFocus(app.view.GuildsView)
+		app.SetFocus(app.view.GuildsTree)
 	} else {
-		loginView := newLoginView(app)
+		loginView := newLoginForm(app)
 		app.SetRoot(loginView, true)
 	}
 
@@ -103,7 +103,7 @@ func (app *Application) onBeforeDraw(screen tcell.Screen) bool {
 }
 
 func (c *Application) onReady(r *gateway.ReadyEvent) {
-	root := c.view.GuildsView.GetRoot()
+	root := c.view.GuildsTree.GetRoot()
 	for _, gf := range r.UserSettings.GuildFolders {
 		if gf.ID == 0 {
 			for _, gID := range gf.GuildIDs {
@@ -154,24 +154,24 @@ func (c *Application) onReady(r *gateway.ReadyEvent) {
 
 	}
 
-	c.view.GuildsView.SetCurrentNode(root)
-	c.SetFocus(c.view.GuildsView)
+	c.view.GuildsTree.SetCurrentNode(root)
+	c.SetFocus(c.view.GuildsTree)
 }
 
 func (c *Application) onGuildCreate(g *gateway.GuildCreateEvent) {
 	guildNode := tview.NewTreeNode(g.Name)
 	guildNode.SetReference(g.ID)
 
-	rootNode := c.view.GuildsView.GetRoot()
+	rootNode := c.view.GuildsTree.GetRoot()
 	rootNode.AddChild(guildNode)
 
-	c.view.GuildsView.SetCurrentNode(rootNode)
-	c.SetFocus(c.view.GuildsView)
+	c.view.GuildsTree.SetCurrentNode(rootNode)
+	c.SetFocus(c.view.GuildsTree)
 	c.Draw()
 }
 
 func (c *Application) onGuildDelete(g *gateway.GuildDeleteEvent) {
-	rootNode := c.view.GuildsView.GetRoot()
+	rootNode := c.view.GuildsTree.GetRoot()
 	var parentNode *tview.TreeNode
 	rootNode.Walk(func(node, _ *tview.TreeNode) bool {
 		if node.GetReference() == g.ID {
@@ -190,14 +190,14 @@ func (c *Application) onGuildDelete(g *gateway.GuildDeleteEvent) {
 }
 
 func (c *Application) onMessageCreate(m *gateway.MessageCreateEvent) {
-	if c.view.ChannelsView.selected != nil && m.ChannelID == c.view.ChannelsView.selected.ID {
-		_, err := c.view.MessagesView.Write(buildMessage(c, m.Message))
+	if c.view.ChannelsTree.selected != nil && m.ChannelID == c.view.ChannelsTree.selected.ID {
+		_, err := c.view.MessagesText.Write(buildMessage(c, m.Message))
 		if err != nil {
 			return
 		}
 
-		if len(c.view.MessagesView.GetHighlights()) == 0 {
-			c.view.MessagesView.ScrollToEnd()
+		if len(c.view.MessagesText.GetHighlights()) == 0 {
+			c.view.MessagesText.ScrollToEnd()
 		}
 	}
 }

+ 16 - 16
ui/channels_view.go → ui/channels_tree.go

@@ -8,15 +8,15 @@ import (
 	"github.com/rivo/tview"
 )
 
-type ChannelsView struct {
+type ChannelsTree struct {
 	*tview.TreeView
 
 	app      *Application
 	selected *discord.Channel
 }
 
-func newChannelsView(app *Application) *ChannelsView {
-	v := &ChannelsView{
+func newChannelsTree(app *Application) *ChannelsTree {
+	v := &ChannelsTree{
 		TreeView: tview.NewTreeView(),
 
 		app: app,
@@ -34,14 +34,14 @@ func newChannelsView(app *Application) *ChannelsView {
 	return v
 }
 
-func (v *ChannelsView) onSelected(node *tview.TreeNode) {
+func (v *ChannelsTree) onSelected(node *tview.TreeNode) {
 	v.selected = nil
-	v.app.view.MessagesView.selected = -1
-	v.app.view.MessagesView.
+	v.app.view.MessagesText.selected = -1
+	v.app.view.MessagesText.
 		Highlight().
 		Clear().
 		SetTitle("")
-	v.app.view.InputView.SetText("")
+	v.app.view.MessageInput.SetText("")
 
 	ref := node.GetReference()
 	if ref == nil {
@@ -64,21 +64,21 @@ func (v *ChannelsView) onSelected(node *tview.TreeNode) {
 	default:
 		v.selected = c
 
-		v.app.view.MessagesView.setTitle(c)
-		v.app.SetFocus(v.app.view.InputView)
+		v.app.view.MessagesText.setTitle(c)
+		v.app.SetFocus(v.app.view.MessageInput)
 
-		go v.app.view.MessagesView.loadMessages(c)
+		go v.app.view.MessagesText.loadMessages(c)
 	}
 }
 
-func (v *ChannelsView) createChannelNode(c discord.Channel) *tview.TreeNode {
+func (v *ChannelsTree) createChannelNode(c discord.Channel) *tview.TreeNode {
 	channelNode := tview.NewTreeNode(channelToString(c))
 	channelNode.SetReference(c.ID)
 
 	return channelNode
 }
 
-func (v *ChannelsView) createPrivateChannelNodes(root *tview.TreeNode) {
+func (v *ChannelsTree) createPrivateChannelNodes(root *tview.TreeNode) {
 	cs, err := v.app.state.Cabinet.PrivateChannels()
 	if err != nil {
 		log.Println(err)
@@ -102,7 +102,7 @@ func (v *ChannelsView) createPrivateChannelNodes(root *tview.TreeNode) {
 	}
 }
 
-func (v *ChannelsView) createGuildChannelNodes(root *tview.TreeNode, gID discord.GuildID) {
+func (v *ChannelsTree) createGuildChannelNodes(root *tview.TreeNode, gID discord.GuildID) {
 	cs, err := v.app.state.Cabinet.Channels(gID)
 	if err != nil {
 		log.Println(err)
@@ -118,7 +118,7 @@ func (v *ChannelsView) createGuildChannelNodes(root *tview.TreeNode, gID discord
 	v.createChildrenChannelNodes(root, cs)
 }
 
-func (v *ChannelsView) createOrphanChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
+func (v *ChannelsTree) createOrphanChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
 	for _, c := range cs {
 		if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (!c.ParentID.IsValid()) {
 			root.AddChild(v.createChannelNode(c))
@@ -126,7 +126,7 @@ func (v *ChannelsView) createOrphanChannelNodes(root *tview.TreeNode, cs []disco
 	}
 }
 
-func (v *ChannelsView) createCategoryChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
+func (v *ChannelsTree) createCategoryChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
 CATEGORY:
 	for _, c := range cs {
 		if c.Type == discord.GuildCategory {
@@ -142,7 +142,7 @@ CATEGORY:
 	}
 }
 
-func (v *ChannelsView) createChildrenChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
+func (v *ChannelsTree) createChildrenChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
 	for _, c := range cs {
 		if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID.IsValid()) {
 			var parentNode *tview.TreeNode

+ 13 - 13
ui/guilds_view.go → ui/guilds_tree.go

@@ -5,14 +5,14 @@ import (
 	"github.com/rivo/tview"
 )
 
-type GuildsView struct {
+type GuildsTree struct {
 	*tview.TreeView
 
 	app *Application
 }
 
-func newGuildsView(app *Application) *GuildsView {
-	v := &GuildsView{
+func newGuildsTree(app *Application) *GuildsTree {
+	v := &GuildsTree{
 		TreeView: tview.NewTreeView(),
 
 		app: app,
@@ -33,16 +33,16 @@ func newGuildsView(app *Application) *GuildsView {
 	return v
 }
 
-func (v *GuildsView) onSelected(node *tview.TreeNode) {
-	v.app.view.ChannelsView.selected = nil
-	v.app.view.MessagesView.selected = -1
-	rootNode := v.app.view.ChannelsView.GetRoot()
+func (v *GuildsTree) onSelected(node *tview.TreeNode) {
+	v.app.view.ChannelsTree.selected = nil
+	v.app.view.MessagesText.selected = -1
+	rootNode := v.app.view.ChannelsTree.GetRoot()
 	rootNode.ClearChildren()
-	v.app.view.MessagesView.
+	v.app.view.MessagesText.
 		Highlight().
 		Clear().
 		SetTitle("")
-	v.app.view.InputView.SetText("")
+	v.app.view.MessageInput.SetText("")
 
 	// If the selected node has children (guild folder), expand the selected node if it is collapsed, otherwise collapse.
 	if len(node.GetChildren()) != 0 {
@@ -53,11 +53,11 @@ func (v *GuildsView) onSelected(node *tview.TreeNode) {
 	ref := node.GetReference()
 	// If the reference of the selected node is nil, it must be the direct messages node.
 	if ref == nil {
-		v.app.view.ChannelsView.createPrivateChannelNodes(rootNode)
+		v.app.view.ChannelsTree.createPrivateChannelNodes(rootNode)
 	} else { // Guild
-		v.app.view.ChannelsView.createGuildChannelNodes(rootNode, ref.(discord.GuildID))
+		v.app.view.ChannelsTree.createGuildChannelNodes(rootNode, ref.(discord.GuildID))
 	}
 
-	v.app.view.ChannelsView.SetCurrentNode(rootNode)
-	v.app.SetFocus(v.app.view.ChannelsView)
+	v.app.view.ChannelsTree.SetCurrentNode(rootNode)
+	v.app.SetFocus(v.app.view.ChannelsTree)
 }

+ 14 - 14
ui/login_view.go → ui/login_form.go

@@ -10,31 +10,31 @@ import (
 	"github.com/zalando/go-keyring"
 )
 
-type LoginView struct {
+type LoginForm struct {
 	*tview.Form
 	app *Application
 }
 
-func newLoginView(app *Application) *LoginView {
-	v := &LoginView{
+func newLoginForm(app *Application) *LoginForm {
+	lf := &LoginForm{
 		Form: tview.NewForm(),
 		app:  app,
 	}
 
-	v.AddInputField("Email", "", 0, nil, nil)
-	v.AddPasswordField("Password", "", 0, 0, nil)
-	v.AddPasswordField("Code (optional)", "", 0, 0, nil)
-	v.AddButton("Login", v.onLoginButtonSelected)
+	lf.AddInputField("Email", "", 0, nil, nil)
+	lf.AddPasswordField("Password", "", 0, 0, nil)
+	lf.AddPasswordField("Code (optional)", "", 0, 0, nil)
+	lf.AddButton("Login", lf.onLoginButtonSelected)
 
-	v.SetTitle("Login")
-	v.SetTitleAlign(tview.AlignLeft)
-	v.SetBorder(true)
-	v.SetBorderPadding(1, 1, 1, 1)
+	lf.SetTitle("Login")
+	lf.SetTitleAlign(tview.AlignLeft)
+	lf.SetBorder(true)
+	lf.SetBorderPadding(1, 1, 1, 1)
 
-	return v
+	return lf
 }
 
-func (v *LoginView) onLoginButtonSelected() {
+func (v *LoginForm) onLoginButtonSelected() {
 	email := v.GetFormItem(0).(*tview.InputField).GetText()
 	password := v.GetFormItem(1).(*tview.InputField).GetText()
 	if email == "" || password == "" {
@@ -69,7 +69,7 @@ func (v *LoginView) onLoginButtonSelected() {
 	}
 
 	v.app.SetRoot(v.app.view, true)
-	v.app.SetFocus(v.app.view.GuildsView)
+	v.app.SetFocus(v.app.view.GuildsTree)
 
 	go keyring.Set(config.Name, "token", l.Token)
 }

+ 27 - 27
ui/input_view.go → ui/message_input.go

@@ -15,53 +15,53 @@ import (
 	"github.com/rivo/tview"
 )
 
-type InputView struct {
+type MessageInput struct {
 	*tview.InputField
 
 	app *Application
 }
 
-func newInputView(app *Application) *InputView {
-	v := &InputView{
+func newMessageInput(app *Application) *MessageInput {
+	mi := &MessageInput{
 		InputField: tview.NewInputField(),
 
 		app: app,
 	}
 
-	v.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
-	v.SetPlaceholder("Message...")
-	v.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor))
-	v.SetInputCapture(v.inputCapture)
+	mi.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
+	mi.SetPlaceholder("Message...")
+	mi.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor))
+	mi.SetInputCapture(mi.inputCapture)
 
-	v.SetTitleAlign(tview.AlignLeft)
-	v.SetBorder(true)
-	v.SetBorderPadding(0, 0, 1, 1)
+	mi.SetTitleAlign(tview.AlignLeft)
+	mi.SetBorder(true)
+	mi.SetBorderPadding(0, 0, 1, 1)
 
-	return v
+	return mi
 }
 
-func (v *InputView) inputCapture(event *tcell.EventKey) *tcell.EventKey {
+func (v *MessageInput) inputCapture(event *tcell.EventKey) *tcell.EventKey {
 	switch event.Name() {
-	case v.app.config.Keys.InputView.SendMessage:
+	case v.app.config.Keys.MessageInput.Send:
 		return v.sendMessage()
-	case v.app.config.Keys.InputView.OpenExternalEditor:
+	case v.app.config.Keys.MessageInput.LaunchEditor:
 		return v.openExternalEditor()
-	case v.app.config.Keys.InputView.PasteClipboard:
+	case v.app.config.Keys.MessageInput.Paste:
 		return v.pasteClipboard()
 	case "Esc":
 		v.
 			SetText("").
 			SetTitle("")
-		v.app.view.MessagesView.selected = -1
-		v.app.view.MessagesView.Highlight()
+		v.app.view.MessagesText.selected = -1
+		v.app.view.MessagesText.Highlight()
 		return nil
 	}
 
 	return event
 }
 
-func (v *InputView) sendMessage() *tcell.EventKey {
-	if v.app.view.ChannelsView.selected == nil {
+func (v *MessageInput) sendMessage() *tcell.EventKey {
+	if v.app.view.ChannelsTree.selected == nil {
 		return nil
 	}
 
@@ -70,14 +70,14 @@ func (v *InputView) sendMessage() *tcell.EventKey {
 		return nil
 	}
 
-	ms, err := v.app.state.Messages(v.app.view.ChannelsView.selected.ID, v.app.config.MessagesLimit)
+	ms, err := v.app.state.Messages(v.app.view.ChannelsTree.selected.ID, v.app.config.MessagesLimit)
 	if err != nil {
 		log.Println(err)
 		return nil
 	}
 
-	if len(v.app.view.MessagesView.GetHighlights()) != 0 {
-		mID, err := discord.ParseSnowflake(v.app.view.MessagesView.GetHighlights()[0])
+	if len(v.app.view.MessagesText.GetHighlights()) != 0 {
+		mID, err := discord.ParseSnowflake(v.app.view.MessagesText.GetHighlights()[0])
 		if err != nil {
 			log.Println(err)
 			return nil
@@ -99,19 +99,19 @@ func (v *InputView) sendMessage() *tcell.EventKey {
 
 		go v.app.state.SendMessageComplex(m.ChannelID, d)
 
-		v.app.view.MessagesView.selected = -1
-		v.app.view.MessagesView.Highlight()
+		v.app.view.MessagesText.selected = -1
+		v.app.view.MessagesText.Highlight()
 
 		v.SetTitle("")
 	} else {
-		go v.app.state.SendMessage(v.app.view.ChannelsView.selected.ID, t)
+		go v.app.state.SendMessage(v.app.view.ChannelsTree.selected.ID, t)
 	}
 
 	v.SetText("")
 	return nil
 }
 
-func (v *InputView) pasteClipboard() *tcell.EventKey {
+func (v *MessageInput) pasteClipboard() *tcell.EventKey {
 	text, err := clipboard.ReadAll()
 	if err != nil {
 		log.Println(err)
@@ -123,7 +123,7 @@ func (v *InputView) pasteClipboard() *tcell.EventKey {
 	return nil
 }
 
-func (v *InputView) openExternalEditor() *tcell.EventKey {
+func (v *MessageInput) openExternalEditor() *tcell.EventKey {
 	e := os.Getenv("EDITOR")
 	if e == "" {
 		log.Println("environment variable EDITOR is empty")

+ 32 - 32
ui/messages_view.go → ui/messages_text.go

@@ -9,7 +9,7 @@ import (
 	"github.com/rivo/tview"
 )
 
-type MessagesView struct {
+type MessagesText struct {
 	*tview.TextView
 
 	// The index of the currently selected message. A negative index indicates that there is no currently selected message.
@@ -17,31 +17,31 @@ type MessagesView struct {
 	app      *Application
 }
 
-func newMessagesView(app *Application) *MessagesView {
-	v := &MessagesView{
+func newMessagesText(app *Application) *MessagesText {
+	mt := &MessagesText{
 		TextView: tview.NewTextView(),
 
 		selected: -1,
 		app:      app,
 	}
 
-	v.SetDynamicColors(true)
-	v.SetRegions(true)
-	v.SetWordWrap(true)
-	v.SetInputCapture(v.onInputCapture)
-	v.SetChangedFunc(func() {
-		v.app.Draw()
+	mt.SetDynamicColors(true)
+	mt.SetRegions(true)
+	mt.SetWordWrap(true)
+	mt.SetInputCapture(mt.onInputCapture)
+	mt.SetChangedFunc(func() {
+		mt.app.Draw()
 	})
 
-	v.SetTitle("Messages")
-	v.SetTitleAlign(tview.AlignLeft)
-	v.SetBorder(true)
-	v.SetBorderPadding(0, 0, 1, 1)
+	mt.SetTitle("Messages")
+	mt.SetTitleAlign(tview.AlignLeft)
+	mt.SetBorder(true)
+	mt.SetBorderPadding(0, 0, 1, 1)
 
-	return v
+	return mt
 }
 
-func (v *MessagesView) setTitle(c *discord.Channel) {
+func (v *MessagesText) setTitle(c *discord.Channel) {
 	title := channelToString(*c)
 	if c.Topic != "" {
 		title += " - " + discordmd.Parse(c.Topic)
@@ -50,7 +50,7 @@ func (v *MessagesView) setTitle(c *discord.Channel) {
 	v.SetTitle(title)
 }
 
-func (v *MessagesView) loadMessages(c *discord.Channel) {
+func (v *MessagesText) loadMessages(c *discord.Channel) {
 	// The returned slice will be sorted from latest to oldest.
 	ms, err := v.app.state.Messages(c.ID, v.app.config.MessagesLimit)
 	if err != nil {
@@ -59,7 +59,7 @@ func (v *MessagesView) loadMessages(c *discord.Channel) {
 	}
 
 	for i := len(ms) - 1; i >= 0; i-- {
-		_, err = v.app.view.MessagesView.Write(buildMessage(v.app, ms[i]))
+		_, err = v.app.view.MessagesText.Write(buildMessage(v.app, ms[i]))
 		if err != nil {
 			log.Println(err)
 			continue
@@ -69,32 +69,32 @@ func (v *MessagesView) loadMessages(c *discord.Channel) {
 	v.ScrollToEnd()
 }
 
-func (v *MessagesView) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
-	if v.app.view.ChannelsView.selected == nil {
+func (v *MessagesText) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
+	if v.app.view.ChannelsTree.selected == nil {
 		return nil
 	}
 
 	// Messages should return messages ordered from latest to earliest.
-	ms, err := v.app.state.Cabinet.Messages(v.app.view.ChannelsView.selected.ID)
+	ms, err := v.app.state.Cabinet.Messages(v.app.view.ChannelsTree.selected.ID)
 	if err != nil || len(ms) == 0 {
 		return nil
 	}
 
 	switch e.Name() {
-	case v.app.config.Keys.MessagesView.OpenActionsView:
+	case v.app.config.Keys.MessagesText.LaunchActions:
 		return v.openActionsView(ms)
 
-	case v.app.config.Keys.MessagesView.SelectPreviousMessage:
+	case v.app.config.Keys.MessagesText.SelectPrevious:
 		return v.selectPreviousMessage(ms)
-	case v.app.config.Keys.MessagesView.SelectNextMessage:
+	case v.app.config.Keys.MessagesText.SelectNext:
 		return v.selectNextMessage(ms)
-	case v.app.config.Keys.MessagesView.SelectFirstMessage:
+	case v.app.config.Keys.MessagesText.SelectFirst:
 		return v.selectFirstMessage(ms)
-	case v.app.config.Keys.MessagesView.SelectLastMessage:
+	case v.app.config.Keys.MessagesText.SelectLast:
 		return v.selectLastMessage(ms)
 	case "Esc":
 		v.selected = -1
-		v.app.view.ChannelsView.selected = nil
+		v.app.view.ChannelsTree.selected = nil
 
 		v.app.SetFocus(v.app.view)
 		v.
@@ -107,7 +107,7 @@ func (v *MessagesView) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
 	return e
 }
 
-func (v *MessagesView) selectPreviousMessage(ms []discord.Message) *tcell.EventKey {
+func (v *MessagesText) selectPreviousMessage(ms []discord.Message) *tcell.EventKey {
 	// If there are no highlighted regions, select the latest (last) message.
 	if len(v.GetHighlights()) == 0 {
 		v.selected = 0
@@ -125,7 +125,7 @@ func (v *MessagesView) selectPreviousMessage(ms []discord.Message) *tcell.EventK
 	return nil
 }
 
-func (v *MessagesView) selectNextMessage(ms []discord.Message) *tcell.EventKey {
+func (v *MessagesText) selectNextMessage(ms []discord.Message) *tcell.EventKey {
 	// If there are no highlighted regions, select the latest (last) message.
 	if len(v.GetHighlights()) == 0 {
 		v.selected = 0
@@ -144,7 +144,7 @@ func (v *MessagesView) selectNextMessage(ms []discord.Message) *tcell.EventKey {
 	return nil
 }
 
-func (v *MessagesView) selectFirstMessage(ms []discord.Message) *tcell.EventKey {
+func (v *MessagesText) selectFirstMessage(ms []discord.Message) *tcell.EventKey {
 	v.selected = len(ms) - 1
 	v.
 		Highlight(ms[v.selected].ID.String()).
@@ -152,7 +152,7 @@ func (v *MessagesView) selectFirstMessage(ms []discord.Message) *tcell.EventKey
 	return nil
 }
 
-func (v *MessagesView) selectLastMessage(ms []discord.Message) *tcell.EventKey {
+func (v *MessagesText) selectLastMessage(ms []discord.Message) *tcell.EventKey {
 	v.selected = 0
 	v.
 		Highlight(ms[v.selected].ID.String()).
@@ -160,7 +160,7 @@ func (v *MessagesView) selectLastMessage(ms []discord.Message) *tcell.EventKey {
 	return nil
 }
 
-func (v *MessagesView) openActionsView(ms []discord.Message) *tcell.EventKey {
+func (v *MessagesText) openActionsView(ms []discord.Message) *tcell.EventKey {
 	hs := v.GetHighlights()
 	if len(hs) == 0 {
 		return nil
@@ -176,7 +176,7 @@ func (v *MessagesView) openActionsView(ms []discord.Message) *tcell.EventKey {
 		return nil
 	}
 
-	actionsView := newActionsView(v.app, m)
+	actionsView := newActionsList(v.app, m)
 	v.app.SetRoot(actionsView, true)
 	return nil
 }

+ 13 - 69
ui/view.go

@@ -1,98 +1,42 @@
 package ui
 
 import (
-	"github.com/gdamore/tcell/v2"
 	"github.com/rivo/tview"
 )
 
-type focusedId int
-
-const (
-	focusedIdGuildsView focusedId = iota
-	focusedIdChannelsView
-	focusedIdMessagesView
-	focusedIdInputView
-)
-
 type View struct {
 	*tview.Flex
 
-	GuildsView   *GuildsView
-	ChannelsView *ChannelsView
-	MessagesView *MessagesView
-	InputView    *InputView
+	GuildsTree   *GuildsTree
+	ChannelsTree *ChannelsTree
+	MessagesText *MessagesText
+	MessageInput *MessageInput
 
-	app     *Application
-	focused focusedId
+	app *Application
 }
 
 func newView(app *Application) *View {
 	v := &View{
 		Flex:         tview.NewFlex(),
-		GuildsView:   newGuildsView(app),
-		ChannelsView: newChannelsView(app),
-		MessagesView: newMessagesView(app),
-		InputView:    newInputView(app),
+		GuildsTree:   newGuildsTree(app),
+		ChannelsTree: newChannelsTree(app),
+		MessagesText: newMessagesText(app),
+		MessageInput: newMessageInput(app),
 
 		app: app,
 	}
 
 	left := tview.NewFlex().
 		SetDirection(tview.FlexRow).
-		AddItem(v.GuildsView, 10, 1, false).
-		AddItem(v.ChannelsView, 0, 1, false)
+		AddItem(v.GuildsTree, 10, 1, false).
+		AddItem(v.ChannelsTree, 0, 1, false)
 	right := tview.NewFlex().
 		SetDirection(tview.FlexRow).
-		AddItem(v.MessagesView, 0, 1, false).
-		AddItem(v.InputView, 3, 1, false)
+		AddItem(v.MessagesText, 0, 1, false).
+		AddItem(v.MessageInput, 3, 1, false)
 
 	v.AddItem(left, 0, 1, false)
 	v.AddItem(right, 0, 4, false)
 
-	v.SetInputCapture(v.onInputCapture)
-
 	return v
 }
-
-func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
-	switch event.Key() {
-	case tcell.KeyEsc:
-		v.focused = 0
-	case tcell.KeyBacktab:
-		// If the currently focused view is the guilds view (first), then focus the input view (last)
-		if v.focused == 0 {
-			v.focused = focusedIdInputView
-		} else {
-			v.focused--
-		}
-
-		v.setFocus()
-	case tcell.KeyTab:
-		// If the currently focused view is the input view (last), then focus the guilds view (first)
-		if v.focused == focusedIdInputView {
-			v.focused = focusedIdGuildsView
-		} else {
-			v.focused++
-		}
-
-		v.setFocus()
-	}
-
-	return event
-}
-
-func (v *View) setFocus() {
-	var p tview.Primitive
-	switch v.focused {
-	case focusedIdGuildsView:
-		p = v.GuildsView
-	case focusedIdChannelsView:
-		p = v.ChannelsView
-	case focusedIdMessagesView:
-		p = v.MessagesView
-	case focusedIdInputView:
-		p = v.InputView
-	}
-
-	v.app.SetFocus(p)
-}