diff --git a/cmd/root.go b/cmd/root.go index b0aafab..0f5a5db 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -64,7 +64,5 @@ func Run() error { screen.EnablePaste() tview.Styles = tview.Theme{} - app := tview.NewApplication(tview.WithScreen(screen)) - app.SetRoot(root.NewModel(cfg, app)) - return app.Run() + return tview.NewApplication(root.NewModel(cfg), tview.WithScreen(screen)).Run() } diff --git a/go.mod b/go.mod index 4356a14..be2d889 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/andybalholm/brotli v1.2.2 github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6 - github.com/ayn2op/tview v0.0.0-20260808011018-71a6ffd9ad39 + github.com/ayn2op/tview v0.0.0-20260810031733-e2c6c70cbb87 github.com/bogdanfinn/fhttp v0.6.8 github.com/bogdanfinn/tls-client v1.15.1 github.com/gdamore/tcell/v3 v3.4.1 @@ -39,7 +39,7 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/dchest/jsmin v1.0.0 // indirect - github.com/dlclark/regexp2/v2 v2.5.2 // indirect + github.com/dlclark/regexp2/v2 v2.6.0 // indirect github.com/ebitengine/purego v0.10.2 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect diff --git a/go.sum b/go.sum index d09299d..3af449e 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e h1:rsMifRXwxkbha github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e/go.mod h1:gqklpHK7WHYTzpd1ZY6Ho/p9GURBghzBCzRiBYpaUP8= github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6 h1:/khHm6G+GWwXpmwA8bJoaXoUhubqMEUG6spjZsnklIw= github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6/go.mod h1:vqLIvETLkjc2vIgQzVEBqpGNCrgNe7YJqS9TUAJsXRI= -github.com/ayn2op/tview v0.0.0-20260808011018-71a6ffd9ad39 h1:SOzvhkwbIQtlIkRUUFTBKdnECPuFjUBqptO8Pe7aP9Q= -github.com/ayn2op/tview v0.0.0-20260808011018-71a6ffd9ad39/go.mod h1:bJZdl/FfspPLEW3nuX6ubt7YbmDoSj7f2IoWH37w1NE= +github.com/ayn2op/tview v0.0.0-20260810031733-e2c6c70cbb87 h1:GsOC8upE5wja+ncj6M3M0oIIvoRVkRyd3LztSa/YchE= +github.com/ayn2op/tview v0.0.0-20260810031733-e2c6c70cbb87/go.mod h1:GQq8n9VWVgRcEGjVaPBu53kXhPeNC13LCIcq0btKLqQ= github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= @@ -40,8 +40,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/jsmin v1.0.0 h1:Y2hWXmGZiRxtl+VcTksyucgTlYxnhPzTozCwx9gy9zI= github.com/dchest/jsmin v1.0.0/go.mod h1:AVBIund7Mr7lKXT70hKT2YgL3XEXUaUk5iw9DZ8b0Uc= -github.com/dlclark/regexp2/v2 v2.5.2 h1:HAsucWRhsqcDzl6Ua9aR8JwYOTzrZyPrF0/FNxJVAI0= -github.com/dlclark/regexp2/v2 v2.5.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dlclark/regexp2/v2 v2.6.0 h1:KugbSrpXcRpziHIcqqFEXwKwi09LbPkTzvLRItf2mo8= +github.com/dlclark/regexp2/v2 v2.6.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= diff --git a/internal/ui/chat/composer.go b/internal/ui/chat/composer.go index 4acd072..31f0ae2 100644 --- a/internal/ui/chat/composer.go +++ b/internal/ui/chat/composer.go @@ -62,6 +62,7 @@ type composer struct { } type tabSuggestMsg struct{} +type editorMsg string var _ help.KeyMap = (*composer)(nil) @@ -92,35 +93,26 @@ func (c *composer) forwardToTextArea(ev *tcell.EventKey) tview.Cmd { return cmd } -// resizeForContent fits the input to its newline count, capped at Composer.MaxHeight and at the parent flex's height (leaving at least 1 row for the messages list), then pins the scroll offset to the new visible window. -// Safe to call after every edit; ResizeItem and SetOffset are both idempotent. func (c *composer) resizeForContent() { - if c.chat == nil || c.chat.rightFlex == nil { - return - } _, _, _, outerH := c.Rect() _, _, _, innerH := c.InnerRect() - frame := outerH - innerH // border + title/footer + padding rows + frame := outerH - innerH _, _, _, parentH := c.chat.rightFlex.InnerRect() visible := min( - strings.Count(c.Text(), "\n")+1, // newline-driven height - max(c.cfg.Composer.MaxHeight, 1), // user-configured cap - max(parentH-frame-1, 1), // available room, reserving 1 row for messages + strings.Count(c.Text(), "\n")+1, + max(c.cfg.Composer.MaxHeight, 1), + max(parentH-frame-1, 1), ) - c.chat.rightFlex.ResizeItem(c, visible+frame, 1) // outer height = inner content + frame - - // Sync the textarea's inner height so the next keystroke's cursor clamping uses the new size. + c.chat.rightFlex.ResizeItem(c, visible+frame, 1) c.SetVisibleSize(0, visible) - // Clamp scroll: when content overflows, pin the bottom row to the end of the text so backspacing the last newline doesn't leave a blank trailing row; once everything fits, reset to (0,0). total := c.LineCount(0) row, col := c.Offset() - maxOff := max(total-visible, 0) // last valid rowOffset that still shows real content on the bottom row - if row > maxOff { - c.SetOffset(maxOff, col) - } else if total <= visible && (row != 0 || col != 0) { + if total <= visible { c.SetOffset(0, 0) + } else if offset := total - visible; row > offset { + c.SetOffset(offset, col) } } @@ -132,8 +124,6 @@ func (c *composer) reset() { c.SetText("", true) } -// The following overrides wrap the embedded TextArea/Box methods to auto-resize when callers change text or chrome. - func (c *composer) SetText(text string, cursorAtTheEnd bool) *tview.TextArea { defer c.resizeForContent() return c.TextArea.SetText(text, cursorAtTheEnd) @@ -164,7 +154,10 @@ func (c *composer) stopTypingTimer() { } func (c *composer) Update(msg tview.Msg) tview.Cmd { + ui.UpdateBoxFocus(c.Box, &c.cfg.Theme, msg) switch msg := msg.(type) { + case tview.FocusMsg: + return tview.Sequence(c.TextArea.Update(msg), focused(c)) case tabSuggestMsg: return c.tabSuggest() case imagePastedMsg: @@ -182,6 +175,9 @@ func (c *composer) Update(msg tview.Msg) tview.Cmd { c.attach(file.Name, file.Reader) } return nil + case editorMsg: + c.SetText(string(msg), true) + return nil case tview.KeyMsg: switch { @@ -195,9 +191,7 @@ func (c *composer) Update(msg tview.Msg) tview.Cmd { } return c.send() case keybind.Matches(msg, c.cfg.Keybinds.Composer.OpenEditor.Keybind): - cmd := c.stopTabCompletion() - c.editor() - return cmd + return tview.Sequence(c.stopTabCompletion(), c.editor()) case keybind.Matches(msg, c.cfg.Keybinds.Composer.OpenFilePicker.Keybind): return tview.Sequence(c.stopTabCompletion(), c.pickFiles()) case keybind.Matches(msg, c.cfg.Keybinds.Composer.Cancel.Keybind): @@ -228,7 +222,6 @@ func (c *composer) Update(msg tview.Msg) tview.Cmd { } } - // Apply key edits first, then recompute autocomplete through Msg/Cmd. return tview.Batch(typingCmd, tview.Sequence(c.forwardToTextArea(msg), c.tabSuggest())) } return tview.Batch(typingCmd, c.forwardToTextArea(msg)) @@ -757,7 +750,6 @@ func (c *composer) addMentionUser(user *discord.User) { } func (c *composer) removeMentionsList() { - // Make sure that the layer is visible before hiding it to avoid a refocus in the parent. if c.chat.GetVisible(mentionsListLayerName) { c.chat.HideLayer(mentionsListLayerName) } @@ -772,46 +764,44 @@ func (c *composer) stopTabCompletion() tview.Cmd { return nil } -func (c *composer) editor() { - file, err := os.CreateTemp("", tmpFilePattern) - if err != nil { - slog.Error("failed to create tmp file", "err", err) - return - } - defer file.Close() - defer os.Remove(file.Name()) - - file.WriteString(c.Text()) - +func (c *composer) editor() tview.Cmd { if c.cfg.Editor == "" { - slog.Warn("Attempt to open file with editor, but no editor is set") - return - } - - cmd := c.cfg.CreateEditorCommand(file.Name()) - if cmd == nil { - return - } - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - c.chat.app.Suspend(func() { - err := cmd.Run() - if err != nil { - slog.Error("failed to run command", "args", cmd.Args, "err", err) - return + return func() tview.Msg { + slog.Warn("Attempt to open file with editor, but no editor is set") + return nil } - }) - - msg, err := os.ReadFile(file.Name()) - if err != nil { - slog.Error("failed to read tmp file", "name", file.Name(), "err", err) - return } + text := c.Text() + cfg := c.cfg + return tview.Suspend(func() tview.Msg { + file, err := os.CreateTemp("", tmpFilePattern) + if err != nil { + slog.Error("failed to create tmp file", "err", err) + return nil + } + name := file.Name() + defer os.Remove(name) + _, _ = file.WriteString(text) + _ = file.Close() - c.SetText(strings.TrimSpace(string(msg)), true) + cmd := cfg.CreateEditorCommand(name) + if cmd == nil { + return nil + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + slog.Error("failed to run command", "args", cmd.Args, "err", err) + return nil + } + msg, err := os.ReadFile(name) + if err != nil { + slog.Error("failed to read tmp file", "name", name, "err", err) + return nil + } + return editorMsg(strings.TrimSpace(string(msg))) + }) } func (c *composer) attach(name string, reader io.Reader) { diff --git a/internal/ui/chat/guilds_tree.go b/internal/ui/chat/guilds_tree.go index 8997390..4e86560 100644 --- a/internal/ui/chat/guilds_tree.go +++ b/internal/ui/chat/guilds_tree.go @@ -336,7 +336,10 @@ func (gt *guildsTree) collapseParentNode(node *tree.Node) { } func (gt *guildsTree) Update(msg tview.Msg) tview.Cmd { + ui.UpdateBoxFocus(gt.Box, >.cfg.Theme, msg) switch msg := msg.(type) { + case tview.FocusMsg: + return tview.Sequence(gt.Model.Update(msg), focused(gt)) case tree.SelectedMsg: return gt.onSelected(msg.Node) case tview.KeyMsg: diff --git a/internal/ui/chat/keybinds.go b/internal/ui/chat/keybinds.go index 1e240be..812b87d 100644 --- a/internal/ui/chat/keybinds.go +++ b/internal/ui/chat/keybinds.go @@ -33,7 +33,7 @@ func (m *Model) activeKeyMap() help.KeyMap { return m.messagesList.attachmentsPicker } - switch m.app.Focused() { + switch m.focused { case m.guildsTree: return m.guildsTree case m.messagesList: @@ -64,7 +64,7 @@ func (m *Model) baseFullHelp() [][]keybind.Keybind { func (m *Model) focusHelp() []keybind.Keybind { kbs := m.cfg.Keybinds - focused := m.app.Focused() + focused := m.focused focusKbs := make([]keybind.Keybind, 0, 3) if focused != m.guildsTree { diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index 19bf1c0..448ccbe 100644 --- a/internal/ui/chat/messages_list.go +++ b/internal/ui/chat/messages_list.go @@ -847,7 +847,10 @@ func (ml *messagesList) selectedMessage() (*discord.Message, bool) { } func (ml *messagesList) Update(msg tview.Msg) tview.Cmd { + ui.UpdateBoxFocus(ml.Box, &ml.cfg.Theme, msg) switch msg := msg.(type) { + case tview.FocusMsg: + return tview.Sequence(ml.Model.Update(msg), focused(ml)) case tview.KeyMsg: switch { case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Cancel.Keybind): diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index f4d0ced..b41629b 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -51,6 +51,7 @@ type Model struct { messagesList *messagesList composer *composer channelsPicker *channelspicker.Model + focused tview.Model selectedChannel *discord.Channel selectedChannelMu sync.RWMutex @@ -61,11 +62,10 @@ type Model struct { typersMu sync.RWMutex typers map[discord.UserID]*time.Timer - app *tview.Application cfg *config.Config } -func NewModel(app *tview.Application, cfg *config.Config, token string) *Model { +func NewModel(cfg *config.Config, token string) *Model { m := &Model{ Layers: layers.New(), @@ -74,7 +74,6 @@ func NewModel(app *tview.Application, cfg *config.Config, token string) *Model { typers: make(map[discord.UserID]*time.Timer), - app: app, cfg: cfg, } @@ -147,15 +146,14 @@ func (m *Model) buildLayout() { ) } -func (m *Model) togglePicker() { +func (m *Model) togglePicker() tview.Cmd { if m.HasLayer(channelsPickerLayerName) { - m.closePicker() - } else { - m.openPicker() + return m.closePicker() } + return m.openPicker() } -func (m *Model) openPicker() { +func (m *Model) openPicker() tview.Cmd { m.AddLayer( ui.Centered(m.channelsPicker, m.cfg.Picker.Width, m.cfg.Picker.Height), layers.WithName(channelsPickerLayerName), @@ -164,11 +162,13 @@ func (m *Model) openPicker() { layers.WithOverlay(), ).SendToFront(channelsPickerLayerName) m.channelsPicker.RefreshChannels(m.state) + return tview.SetFocus(m.channelsPicker) } -func (m *Model) closePicker() { +func (m *Model) closePicker() tview.Cmd { m.RemoveLayer(channelsPickerLayerName) m.channelsPicker.Refresh() + return tview.SetFocus(m.mainFlex) } func (m *Model) closeAttachmentsPicker() tview.Cmd { @@ -191,15 +191,14 @@ func (m *Model) navigateToChannel(channelID discord.ChannelID) tview.Cmd { m.guildsTree.expandPathToNode(node) m.guildsTree.SetCurrentNode(node) - m.closePicker() + focus := m.closePicker() if channel.Type != discord.GuildCategory { - return m.guildsTree.onSelected(node) + return tview.Sequence(focus, m.guildsTree.onSelected(node)) } - return nil + return focus } func (m *Model) toggleGuildsTree() tview.Cmd { - // The guilds tree is visible if the number of items is two. if m.mainFlex.GetItemCount() == 2 { m.mainFlex.RemoveItem(m.guildsTree) if m.guildsTree.HasFocus() { @@ -213,7 +212,6 @@ func (m *Model) toggleGuildsTree() tview.Cmd { } func (m *Model) focusGuildsTree() tview.Cmd { - // The guilds tree is not hidden if the number of items is two. if m.mainFlex.GetItemCount() == 2 { return tview.SetFocus(m.guildsTree) } @@ -227,38 +225,32 @@ func (m *Model) focusComposer() tview.Cmd { return nil } -func (m *Model) focusMessagesList() tview.Cmd { - return tview.SetFocus(m.messagesList) -} - func (m *Model) focusPrevious() tview.Cmd { - switch m.app.Focused() { + switch m.focused { case m.guildsTree: if cmd := m.focusComposer(); cmd != nil { return cmd } - return m.focusMessagesList() + return tview.SetFocus(m.messagesList) case m.messagesList: - // Fallback when guilds/input are unavailable. if cmd := m.focusGuildsTree(); cmd != nil { return cmd } if cmd := m.focusComposer(); cmd != nil { return cmd } - return m.focusMessagesList() + return tview.SetFocus(m.messagesList) case m.composer: - return m.focusMessagesList() + return tview.SetFocus(m.messagesList) } return nil } func (m *Model) focusNext() tview.Cmd { - switch m.app.Focused() { + switch m.focused { case m.guildsTree: - return m.focusMessagesList() + return tview.SetFocus(m.messagesList) case m.messagesList: - // Fallback when input/guilds are unavailable. if cmd := m.focusComposer(); cmd != nil { return cmd } @@ -269,13 +261,16 @@ func (m *Model) focusNext() tview.Cmd { if cmd := m.focusGuildsTree(); cmd != nil { return cmd } - return m.focusMessagesList() + return tview.SetFocus(m.messagesList) } return nil } func (m *Model) Update(msg tview.Msg) tview.Cmd { switch msg := msg.(type) { + case FocusedMsg: + m.focused = msg.Model + return nil case tview.InitMsg: return tview.Batch(m.openState(), m.listen()) case gateway.Event: @@ -341,7 +336,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { m.composer.SetDisabled(hasNoPerm) text := "Message..." - focusCmd := tview.Cmd(nil) + var focusCmd tview.Cmd if hasNoPerm { text = "You do not have permission to send messages in this channel." @@ -358,8 +353,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { case channelspicker.SelectedMsg: return m.navigateToChannel(msg.ChannelID) case channelspicker.CancelMsg: - m.closePicker() - return nil + return m.closePicker() case attachmentspicker.SelectedMsg: return tview.Sequence(msg.Open, m.closeAttachmentsPicker()) case attachmentspicker.CancelMsg: @@ -373,7 +367,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { return m.focusGuildsTree() case keybind.Matches(msg, m.cfg.Keybinds.FocusMessagesList.Keybind): m.composer.removeMentionsList() - return m.focusMessagesList() + return tview.SetFocus(m.messagesList) case keybind.Matches(msg, m.cfg.Keybinds.FocusComposer.Keybind): return m.focusComposer() @@ -385,15 +379,12 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { case keybind.Matches(msg, m.cfg.Keybinds.ToggleGuildsTree.Keybind): return m.toggleGuildsTree() case keybind.Matches(msg, m.cfg.Keybinds.ToggleChannelsPicker.Keybind): - m.togglePicker() - return nil + return m.togglePicker() case keybind.Matches(msg, m.cfg.Keybinds.Logout.Keybind): return tview.Sequence(m.closeState(), m.logout()) } case tabSuggestMsg: - // Member search completes in a command goroutine; resume suggestion - // generation on the update loop to keep UI mutations serialized. return m.composer.Update(msg) } return m.Layers.Update(msg) diff --git a/internal/ui/chat/msg.go b/internal/ui/chat/msg.go index 716bd1c..ab216bd 100644 --- a/internal/ui/chat/msg.go +++ b/internal/ui/chat/msg.go @@ -57,3 +57,7 @@ func (m *Model) logout() tview.Cmd { } type QuitMsg struct{} + +type FocusedMsg struct{ Model tview.Model } + +func focused(model tview.Model) tview.Cmd { return func() tview.Msg { return FocusedMsg{model} } } diff --git a/internal/ui/root/model.go b/internal/ui/root/model.go index d72c7f4..2cd2f57 100644 --- a/internal/ui/root/model.go +++ b/internal/ui/root/model.go @@ -29,7 +29,6 @@ const ( type Model struct { *layers.Layers - app *tview.Application rootFlex *flex.Model // inner + help inner tview.Model help *help.Model @@ -37,13 +36,13 @@ type Model struct { modalRequest *ui.ModalMsg modalDialog *modal.Model modalPreviousFocus tview.Model + focused tview.Model cfg *config.Config } -func NewModel(cfg *config.Config, app *tview.Application) *Model { +func NewModel(cfg *config.Config) *Model { m := &Model{ Layers: layers.New(), - app: app, rootFlex: flex.NewModel(), help: help.NewModel(), @@ -75,7 +74,7 @@ func (m *Model) showLogin() tview.Cmd { } func (m *Model) showChat(token string) tview.Cmd { - m.inner = chat.NewModel(m.app, m.cfg, token) + m.inner = chat.NewModel(m.cfg, token) m.buildLayout() return tview.Batch(m.inner.Update(tview.InitMsg{}), tview.SetFocus(m)) } @@ -85,6 +84,7 @@ func (m *Model) buildLayout() { m.modalRequest = nil m.modalDialog = nil m.modalPreviousFocus = nil + m.focused = nil m.rootFlex.Clear() if m.inner != nil { m.rootFlex.AddItem(m.inner, 0, 1, true) @@ -98,6 +98,8 @@ var _ tview.Model = (*Model)(nil) func (m *Model) Update(msg tview.Msg) tview.Cmd { switch msg := msg.(type) { + case chat.FocusedMsg: + m.focused = msg.Model case tview.InitMsg: var cmd tview.Cmd if token := os.Getenv(tokenEnvVarKey); token != "" { @@ -146,8 +148,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { m.updateHelpHeight() return nil case keybind.Matches(msg, m.cfg.Keybinds.Suspend.Keybind): - m.suspend() - return nil + return suspend() case keybind.Matches(msg, m.cfg.Keybinds.Quit.Keybind): var innerCmd tview.Cmd if m.inner != nil { @@ -188,14 +189,17 @@ func (m *Model) showModal(request ui.ModalMsg) tview.Cmd { m.modalRequest = &request m.modalDialog = dialog - m.modalPreviousFocus = m.app.Focused() + m.modalPreviousFocus = m.focused + if m.modalPreviousFocus == nil { + m.modalPreviousFocus = m.inner + } m.AddLayer(dialog, layers.WithName(modalLayerName), layers.WithResize(true), layers.WithVisible(true), layers.WithOverlay(), ) - return nil + return tview.SetFocus(dialog) } func (m *Model) finishModal(done modal.DoneMsg) tview.Cmd { @@ -214,7 +218,7 @@ func (m *Model) finishModal(done modal.DoneMsg) tview.Cmd { } m.RemoveLayer(modalLayerName) - focus := tview.Cmd(nil) + var focus tview.Cmd if m.modalPreviousFocus != nil { focus = tview.SetFocus(m.modalPreviousFocus) } diff --git a/internal/ui/root/suspend_default.go b/internal/ui/root/suspend_default.go index 6f89647..79c2672 100644 --- a/internal/ui/root/suspend_default.go +++ b/internal/ui/root/suspend_default.go @@ -2,4 +2,6 @@ package root -func (m *Model) suspend() {} +import "github.com/ayn2op/tview" + +func suspend() tview.Cmd { return nil } diff --git a/internal/ui/root/suspend_unix.go b/internal/ui/root/suspend_unix.go index 39b7011..7802746 100644 --- a/internal/ui/root/suspend_unix.go +++ b/internal/ui/root/suspend_unix.go @@ -6,15 +6,18 @@ import ( "os" "os/signal" "syscall" + + "github.com/ayn2op/tview" ) -func (m *Model) suspend() { - m.app.Suspend(func() { +func suspend() tview.Cmd { + return tview.Suspend(func() tview.Msg { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGCONT) defer signal.Stop(c) _ = syscall.Kill(0, syscall.SIGTSTP) <-c + return nil }) } diff --git a/internal/ui/util.go b/internal/ui/util.go index 077c460..ffa54fd 100644 --- a/internal/ui/util.go +++ b/internal/ui/util.go @@ -16,57 +16,37 @@ import ( // ConfigureBox configures the provided box according to the provided theme. func ConfigureBox(box *tview.Box, cfg *config.Theme) { - border := cfg.Border - normalBorderStyle, activeBorderStyle := border.NormalStyle.Style, border.ActiveStyle.Style - normalBorderSet, activeBorderSet := border.NormalSet.BorderSet, border.ActiveSet.BorderSet - - title := cfg.Title - normalTitleStyle, activeTitleStyle := title.NormalStyle.Style, title.ActiveStyle.Style - - footer := cfg.Footer - normalFooterStyle, activeFooterStyle := footer.NormalStyle.Style, footer.ActiveStyle.Style - - padding := border.Padding - + padding := cfg.Border.Padding + UpdateBoxFocus(box, cfg, tview.BlurMsg{}) box. - SetBorderStyle(normalBorderStyle). - SetBorderSet(normalBorderSet). SetBorderPadding(padding[0], padding[1], padding[2], padding[3]). - SetTitleStyle(normalTitleStyle). - SetTitleAlignment(title.Alignment.Alignment). - SetFooterStyle(normalFooterStyle). - SetFooterAlignment(footer.Alignment.Alignment). - SetBlurFunc(func() { - box. - SetBorderStyle(normalBorderStyle). - SetBorderSet(normalBorderSet) - box.SetTitleStyle(normalTitleStyle).SetFooterStyle(normalFooterStyle) - }). - SetFocusFunc(func() { - box. - SetBorderStyle(activeBorderStyle). - SetBorderSet(activeBorderSet) - box.SetTitleStyle(activeTitleStyle).SetFooterStyle(activeFooterStyle) - }) + SetTitleAlignment(cfg.Title.Alignment.Alignment). + SetFooterAlignment(cfg.Footer.Alignment.Alignment) - if border.Enabled { + if cfg.Border.Enabled { box.SetBorders(tview.BordersAll) } } +func UpdateBoxFocus(box *tview.Box, cfg *config.Theme, msg tview.Msg) { + switch msg.(type) { + case tview.FocusMsg: + box.SetBorderStyle(cfg.Border.ActiveStyle.Style). + SetBorderSet(cfg.Border.ActiveSet.BorderSet). + SetTitleStyle(cfg.Title.ActiveStyle.Style). + SetFooterStyle(cfg.Footer.ActiveStyle.Style) + case tview.BlurMsg: + box.SetBorderStyle(cfg.Border.NormalStyle.Style). + SetBorderSet(cfg.Border.NormalSet.BorderSet). + SetTitleStyle(cfg.Title.NormalStyle.Style). + SetFooterStyle(cfg.Footer.NormalStyle.Style) + } +} + func ConfigurePicker(model *picker.Model, cfg *config.Config, title string) { model.Box = tview.NewBox() ConfigureBox(model.Box, &cfg.Theme) - // When a child of the parent flex is focused, the parent layout itself is not reported as focused. - // Instead, the focused child (picker) is considered focused. - // Therefore, we manually set the active border style on the picker to ensure it displays the correct focused appearance. - model. - SetBlurFunc(nil). - SetFocusFunc(nil). - SetBorderSet(cfg.Theme.Border.ActiveSet.BorderSet). - SetBorderStyle(cfg.Theme.Border.ActiveStyle.Style). - SetTitleStyle(cfg.Theme.Title.ActiveStyle.Style). - SetFooterStyle(cfg.Theme.Footer.ActiveStyle.Style) + UpdateBoxFocus(model.Box, &cfg.Theme, tview.FocusMsg{}) model.SetTitle(title) model.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility)