package root import ( "os" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/discordo/internal/ui/chat" "github.com/ayn2op/discordo/internal/ui/login" "github.com/ayn2op/discordo/internal/ui/login/password" "github.com/ayn2op/discordo/internal/ui/login/qr" "github.com/ayn2op/discordo/internal/ui/login/token" "github.com/ayn2op/tview" "github.com/ayn2op/tview/backdrop" "github.com/ayn2op/tview/box" "github.com/ayn2op/tview/column" "github.com/ayn2op/tview/dialog" "github.com/ayn2op/tview/help" "github.com/ayn2op/tview/keybind" "github.com/ayn2op/tview/stack" "github.com/gdamore/tcell/v3" ) const tokenEnvVarKey = "DISCORDO_TOKEN" type Model struct { inner tview.Model // helpVisible reports whether help is shown below the inner model, and helpShowAll whether it shows every keybind. helpVisible, helpShowAll bool // modal is the request shown in a dialog, or nil when none is open, and dialogFocus its focused button. modal *ui.ModalMsg dialogFocus int cfg *config.Config } func NewModel(cfg *config.Config) *Model { return &Model{cfg: cfg, helpVisible: cfg.Help.Enabled} } var _ tview.Model = (*Model)(nil) func (m *Model) Init() tview.Cmd { var cmd tview.Cmd if token := os.Getenv(tokenEnvVarKey); token != "" { cmd = useToken(token) } else { cmd = getToken() } return tview.Batch( tview.SetTitle(consts.Name), initClipboard(), cmd, ) } func (m *Model) Update(msg tview.Msg) tview.Cmd { switch msg := msg.(type) { case loginMsg: return m.showLogin() case tokenMsg: return m.showChat(string(msg)) case password.TokenMsg: return tview.Batch(m.showChat(string(msg)), setToken(string(msg))) case qr.TokenMsg: return tview.Batch(m.showChat(string(msg)), setToken(string(msg))) case token.TokenMsg: return tview.Batch(m.showChat(string(msg)), setToken(string(msg))) case chat.LogoutMsg: return tview.Batch( m.showLogin(), deleteToken(), ) case ui.ModalMsg: return m.showModal(msg) case dialogFocusMsg: m.dialogFocus = int(msg) return nil case dialogDoneMsg: return m.finishModal(int(msg)) case tview.KeyMsg: // The dialog takes all input while it is open, so nothing behind it reacts. if m.modal != nil { return nil } switch { case keybind.Matches(msg, m.cfg.Keybinds.ToggleHelp.Keybind): m.toggleHelp() return nil case keybind.Matches(msg, m.cfg.Keybinds.ToggleFullHelp.Keybind): if m.cfg.Help.Enabled { m.helpShowAll = !m.helpShowAll } return nil case keybind.Matches(msg, m.cfg.Keybinds.Suspend.Keybind): return suspend() case keybind.Matches(msg, m.cfg.Keybinds.Quit.Keybind): var innerCmd tview.Cmd if m.inner != nil { innerCmd = m.inner.Update(chat.QuitMsg{}) } return tview.Batch(innerCmd, tview.Quit()) } case tview.MouseMsg, tview.PasteMsg: if m.modal != nil { return nil } } if m.inner != nil { return m.inner.Update(msg) } return nil } // View shows the inner model above help, with the modal dialog on top of both when one is open. func (m *Model) View() tview.Element { var innerView, helpView tview.Element if m.inner != nil { innerView = m.inner.View() } if m.helpVisible { padded := box.New(m.helpView()).Padding(0, 0, m.cfg.Help.Padding[0], m.cfg.Help.Padding[1]) helpView = column.New(padded).Height(tview.Fixed(max(m.helpView().Rows(0), 1))) } content := column.New(innerView, helpView) if m.modal == nil { return content } return stack.New(inert{content}, backdrop.New().Style(m.cfg.Theme.Dialog.BackgroundStyle.Style), m.dialogView()) } func (m *Model) showModal(request ui.ModalMsg) tview.Cmd { if m.modal != nil { return nil } m.modal, m.dialogFocus = &request, 0 return nil } func (m *Model) showLogin() tview.Cmd { return m.show(login.NewModel(m.cfg)) } func (m *Model) showChat(token string) tview.Cmd { return m.show(chat.NewModel(m.cfg, token)) } // show replaces the inner model, closing any open modal. func (m *Model) show(inner tview.Model) tview.Cmd { m.inner = inner m.modal = nil return tview.Batch(tview.SetTitle(consts.Name), m.inner.Init()) } // helpView returns the help for the keybinds that currently apply. func (m *Model) helpView() help.Widget { cfg := m.cfg styles := help.DefaultStyles() styles.ShortKey = cfg.Theme.Help.ShortKeyStyle.Style styles.ShortDesc = cfg.Theme.Help.ShortDescStyle.Style styles.FullKey = cfg.Theme.Help.FullKeyStyle.Style styles.FullDesc = cfg.Theme.Help.FullDescStyle.Style return help.New(m). Styles(styles). CompactModifiers(cfg.Help.CompactModifiers). ShortSeparator(cfg.Help.Separator). ShowAll(m.helpShowAll) } type ( dialogFocusMsg int dialogDoneMsg int ) // dialogView returns the dialog for the open modal request. func (m *Model) dialogView() tview.Element { labels := make([]string, len(m.modal.Buttons)) for i, button := range m.modal.Buttons { labels[i] = button.Label } style := m.cfg.Theme.Dialog.Style.Style d := dialog.New(). Text(m.modal.Text). Buttons(labels...). Focus(m.dialogFocus). ButtonStyle(style). ActivatedStyle(style.Reverse(true)). OnFocus(func(i int) tview.Msg { return dialogFocusMsg(i) }). OnDone(func(i int, _ string) tview.Msg { return dialogDoneMsg(i) }) if bg := style.GetBackground(); bg != tcell.ColorDefault { d = d.Background(bg) } if fg := style.GetForeground(); fg != tcell.ColorDefault { d = d.TextColor(fg) } return d } func (m *Model) toggleHelp() { if m.cfg.Help.Enabled { m.helpVisible = !m.helpVisible } } func (m *Model) finishModal(index int) tview.Cmd { state := m.modal if state == nil { return nil } var result tview.Msg if index >= 0 && index < len(state.Buttons) { button := state.Buttons[index] if button.KeepOpen { return func() tview.Msg { return button.Result } } result = button.Result } m.modal = nil if result == nil { return nil } return func() tview.Msg { return result } }