From 391f3b974cafaafe3beec5abe8f5c59a57c662b4 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Thu, 9 Jul 2026 04:12:33 -0400 Subject: [PATCH] refactor(ui): replace tview modal usage --- go.mod | 10 ++--- go.sum | 20 +++++----- internal/ui/chat/messages_list.go | 2 +- internal/ui/chat/model.go | 65 ++++++++++++++++++++----------- internal/ui/login/model.go | 47 +++++++++++++++------- internal/ui/util.go | 3 +- 6 files changed, 93 insertions(+), 54 deletions(-) diff --git a/go.mod b/go.mod index 96146f4..3e8fc80 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-20260708025551-16f85f97975e + github.com/ayn2op/tview v0.0.0-20260709081114-77dc5f1dd56c github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8 github.com/gen2brain/beeep v0.11.2 @@ -55,10 +55,10 @@ require ( go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.design/x/x11 v0.2.0 // indirect golang.org/x/exp/shiny v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/image v0.43.0 // indirect + golang.org/x/image v0.44.0 // indirect golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index e4d4dfe..610a05c 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,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-20260708025551-16f85f97975e h1:WVf0fqBMetR5F5VoA1lMvRpSX25YUyekaNwaWXieW7c= -github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e/go.mod h1:kBq8YDymQUJsGxUfhatCnwc1XU0z1tXjSLNpGpzcMp8= +github.com/ayn2op/tview v0.0.0-20260709081114-77dc5f1dd56c h1:hX1fxRak+RQnwWjeTVv7xuuOxqBxrT5ITM+reIq5eKs= +github.com/ayn2op/tview v0.0.0-20260709081114-77dc5f1dd56c/go.mod h1:kBq8YDymQUJsGxUfhatCnwc1XU0z1tXjSLNpGpzcMp8= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -123,8 +123,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp/shiny v0.0.0-20260611194520-c48552f49976 h1:xAKfctaZuXDxw/I98uYRl7TXMBy4a1BgPgl7NavbrdY= golang.org/x/exp/shiny v0.0.0-20260611194520-c48552f49976/go.mod h1:UXxgIHNj0uSRIk9ua62CRJcUpfHN9V+cRZRXHEo4GhE= -golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= -golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2 h1:zoM1gIKhVkcQNm43kad8OHLgPNoJ12xIqmxHtKr8Mug= golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2/go.mod h1:QGMqsqLn6orFQ/ksqYMf+Fa33Soa1vPoHEd0Pj7N+lQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -143,20 +143,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index 5e53c7c..8efcc38 100644 --- a/internal/ui/chat/messages_list.go +++ b/internal/ui/chat/messages_list.go @@ -1278,7 +1278,7 @@ func (ml *messagesList) confirmDelete() { } } - ml.chat.showConfirmModal( + ml.chat.showConfirmDialog( "Are you sure you want to delete this message?", []string{"Yes", "No"}, onChoice, diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index d4b89d7..b7d96a7 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -21,6 +21,7 @@ import ( "github.com/ayn2op/ningen/v3/states/read" "github.com/ayn2op/tview" "github.com/ayn2op/tview/flex" + "github.com/ayn2op/tview/frame" "github.com/ayn2op/tview/keybind" "github.com/ayn2op/tview/layers" "github.com/gdamore/tcell/v3" @@ -29,9 +30,9 @@ import ( const typingDuration = 10 * time.Second const ( - flexLayerName = "flex" - mentionsListLayerName = "mentionsList" - confirmModalLayerName = "confirmModal" + flexLayerName = "flex" + mentionsListLayerName = "mentionsList" + confirmDialogLayerName = "confirmDialog" channelsPickerLayerName = "channelsPicker" attachmentsPickerLayerName = "attachmentsPicker" @@ -53,8 +54,8 @@ type Model struct { selectedChannel *discord.Channel selectedChannelMu sync.RWMutex - confirmModalDone func(label string) - confirmModalPreviousFocus tview.Model + confirmDialogDone func(label string) + confirmDialogPreviousFocus tview.Model state *ningen.State events chan gateway.Event @@ -318,21 +319,36 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { return focusCmd case QuitMsg: return m.closeState() - case tview.ModalDoneMsg: - if m.HasLayer(confirmModalLayerName) { - m.RemoveLayer(confirmModalLayerName) + case tview.FormSubmitMsg: + if m.HasLayer(confirmDialogLayerName) { + m.RemoveLayer(confirmDialogLayerName) var focusCmd tview.Cmd - if m.confirmModalPreviousFocus != nil { - focusCmd = tview.SetFocus(m.confirmModalPreviousFocus) + if m.confirmDialogPreviousFocus != nil { + focusCmd = tview.SetFocus(m.confirmDialogPreviousFocus) } - onDone := m.confirmModalDone - m.confirmModalDone = nil - m.confirmModalPreviousFocus = nil + onDone := m.confirmDialogDone + m.confirmDialogDone = nil + m.confirmDialogPreviousFocus = nil if onDone != nil { onDone(msg.ButtonLabel) } return focusCmd } + case tview.FormCancelMsg: + if m.HasLayer(confirmDialogLayerName) { + m.RemoveLayer(confirmDialogLayerName) + var focusCmd tview.Cmd + if m.confirmDialogPreviousFocus != nil { + focusCmd = tview.SetFocus(m.confirmDialogPreviousFocus) + } + onDone := m.confirmDialogDone + m.confirmDialogDone = nil + m.confirmDialogPreviousFocus = nil + if onDone != nil { + onDone("") + } + return focusCmd + } case tview.KeyMsg: switch { case keybind.Matches(msg, m.cfg.Keybinds.FocusGuildsTree.Keybind): @@ -366,22 +382,27 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { return m.Layers.Update(msg) } -func (m *Model) showConfirmModal(prompt string, buttons []string, onDone func(label string)) { - m.confirmModalPreviousFocus = m.app.Focused() - m.confirmModalDone = onDone +func (m *Model) showConfirmDialog(prompt string, buttons []string, onDone func(label string)) { + m.confirmDialogPreviousFocus = m.app.Focused() + m.confirmDialogDone = onDone - modal := tview.NewModal(). - SetText(prompt). - AddButtons(buttons) + form := tview.NewForm().SetButtonsAlignment(tview.AlignmentCenter) + for _, button := range buttons { + form.AddButton(button) + } + dialog := frame.NewModel(form).SetBorders(0, 0, 1, 0, 0, 0) + for _, line := range tview.WordWrap(prompt, 80) { + dialog.AddText(line, true, tview.AlignmentCenter, tview.Styles.PrimaryTextColor) + } m. AddLayer( - ui.Centered(modal, 0, 0), - layers.WithName(confirmModalLayerName), + ui.Centered(dialog, 0, 0), + layers.WithName(confirmDialogLayerName), layers.WithResize(true), layers.WithVisible(true), layers.WithOverlay(), ). - SendToFront(confirmModalLayerName) + SendToFront(confirmDialogLayerName) } func (m *Model) clearTypers() { diff --git a/internal/ui/login/model.go b/internal/ui/login/model.go index a7f1635..d18564f 100644 --- a/internal/ui/login/model.go +++ b/internal/ui/login/model.go @@ -3,6 +3,7 @@ package login import ( "log/slog" + "github.com/ayn2op/tview/frame" "github.com/ayn2op/tview/layers" "github.com/ayn2op/tview/tabs" "github.com/gdamore/tcell/v3" @@ -23,8 +24,8 @@ type Model struct { *layers.Layers tabs *tabs.Model - cfg *config.Config - errorModalText string + cfg *config.Config + errorDialogText string } func NewModel(cfg *config.Config) *Model { @@ -48,15 +49,22 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { return nil } return m.showErrorDialog(msg.err) - case tview.ModalDoneMsg: + case tview.FormSubmitMsg: if !m.HasLayer(errorLayerName) { return nil } if msg.ButtonIndex == 0 { - return setClipboard(m.errorModalText) + return setClipboard(m.errorDialogText) } m.RemoveLayer(errorLayerName) - m.errorModalText = "" + m.errorDialogText = "" + return nil + case tview.FormCancelMsg: + if !m.HasLayer(errorLayerName) { + return nil + } + m.RemoveLayer(errorLayerName) + m.errorDialogText = "" return nil } return m.Layers.Update(msg) @@ -66,34 +74,43 @@ func (m *Model) showErrorDialog(err error) tview.Cmd { slog.Error("failed to login", "err", err) message := err.Error() - m.errorModalText = message - modal := tview.NewModal(). - SetText(message). - AddButtons([]string{"Copy", "Close"}) + m.errorDialogText = message + form := tview.NewForm(). + SetButtonsAlignment(tview.AlignmentCenter). + AddButton("Copy"). + AddButton("Close") + dialog := frame.NewModel(form).SetBorders(0, 0, 1, 0, 0, 0) + for _, line := range tview.WordWrap(message, 80) { + dialog.AddText(line, true, tview.AlignmentCenter, tview.Styles.PrimaryTextColor) + } { bg := m.cfg.Theme.Dialog.Style.GetBackground() buttonStyle := m.cfg.Theme.Dialog.Style.Style if bg != tcell.ColorDefault { - modal.SetBackgroundColor(bg) + form.SetBackgroundColor(bg) + dialog.SetBackgroundColor(bg) buttonStyle = buttonStyle.Background(bg) } fg := m.cfg.Theme.Dialog.Style.GetForeground() if fg != tcell.ColorDefault { - modal.SetTextColor(fg) + dialog.Clear() + for _, line := range tview.WordWrap(message, 80) { + dialog.AddText(line, true, tview.AlignmentCenter, fg) + } buttonStyle = buttonStyle.Foreground(fg) } // Keep button styles aligned with dialog content and still show focus. - modal.SetButtonStyle(buttonStyle) - modal.SetButtonActivatedStyle(buttonStyle.Reverse(true)) + form.SetButtonStyle(buttonStyle) + form.SetButtonActivatedStyle(buttonStyle.Reverse(true)) } m. AddLayer( - ui.Centered(modal, 0, 0), + ui.Centered(dialog, 0, 0), layers.WithName(errorLayerName), layers.WithResize(true), layers.WithVisible(true), layers.WithOverlay(), ). SendToFront(errorLayerName) - return tview.SetFocus(modal) + return tview.SetFocus(dialog) } diff --git a/internal/ui/util.go b/internal/ui/util.go index 29bd85c..8035a91 100644 --- a/internal/ui/util.go +++ b/internal/ui/util.go @@ -9,6 +9,7 @@ import ( "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/ningen/v3" "github.com/ayn2op/tview" + "github.com/ayn2op/tview/grid" ) // ConfigureBox configures the provided box according to the provided theme. @@ -55,7 +56,7 @@ func ConfigureBox(box *tview.Box, cfg *config.Theme) *tview.Box { // Centered creates a new grid with provided primitive aligned in the center. func Centered(m tview.Model, width, height int) tview.Model { - return tview.NewGrid(). + return grid.NewModel(). SetColumns(0, width, 0). SetRows(0, height, 0). AddItem(m, 1, 1, 1, 1, 0, 0, true)