diff --git a/cmd/application.go b/cmd/application.go index 5c7f2dc..96efc7c 100644 --- a/cmd/application.go +++ b/cmd/application.go @@ -2,6 +2,8 @@ package cmd import ( "log/slog" + "strings" + "fmt" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/keyring" @@ -9,13 +11,14 @@ import ( "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/tview" "github.com/gdamore/tcell/v2" + "golang.design/x/clipboard" ) const ( flexPageName = "flex" mentionsListPageName = "mentionsList" attachmentsListPageName = "attachmentsList" - confirmModalPageName = "confirmModal" + modalPageName = "modalPageName" ) type application struct { @@ -131,7 +134,7 @@ func (a *application) onPagesInputCapture(event *tcell.EventKey) *tcell.EventKey a.quit() if err := keyring.DeleteToken(); err != nil { - slog.Error("failed to delete token from keyring", "err", err) + a.onError("Failed to delete token from keyring", err) return nil } @@ -202,22 +205,60 @@ func (a *application) focusNext() { } } -func (a *application) showConfirmModal(prompt string, buttons []string, onDone func(label string)) { +func (a *application) onError(msg string, err error, info ...any) { + slog.Error(msg, append(info, "err", err)...) + a.showErrorModal(msg, err.Error(), info...) +} + +func (a *application) showModal(title, prompt string, buttons []string, onDone func(label string)) { previousFocus := a.GetFocus() - modal := tview.NewModal(). + modal := tview.NewModal() + modal.Box = ui.ConfigureBox(modal.Box, &a.cfg.Theme) + modal. SetText(prompt). AddButtons(buttons). SetDoneFunc(func(_ int, buttonLabel string) { - a.pages.RemovePage(confirmModalPageName).SwitchToPage(flexPageName) + a.pages. + RemovePage(modalPageName). + SwitchToPage(flexPageName) a.SetFocus(previousFocus) if onDone != nil { onDone(buttonLabel) } - }) + }). + SetTitle(title). + SetTitleAlignment(tview.AlignmentCenter) a.pages. - AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true). + AddAndSwitchToPage(modalPageName, ui.Centered(modal, 0, 0), true). ShowPage(flexPageName) + a.SetFocus(modal) +} + +func (a *application) showConfirmModal(prompt string, buttons []string, onDone func(label string)) { + a.showModal("", prompt, buttons, onDone) +} + +func (a *application) showErrorModal(msg, err string, info ...any) { + a.showModal("[ ERROR ]", msg + "\nReason: " + err, []string{"Copy", "OK"}, func(label string) { + if label != "Copy" { + return + } + res := &strings.Builder{} + fmt.Fprintf(res, "%s\nReason: %s", msg, err) + for i := 0; i < len(info)-1; i += 2 { + fmt.Fprintf(res, "\n%v: %#v", info[i], info[i+1]) + } + // Do as log/slog does. "!BADKEY" for last odd argument. + if (len(info) % 2) == 1 { + fmt.Fprintf(res, "\n!BADKEY: %#v", info[len(info)-1]) + } + go clipboard.Write(clipboard.FmtText, []byte(res.String())) + }) +} + +func (a *application) showInfoModal(text string) { + a.showModal("[ INFO ]", text, []string{"OK"}, nil) } diff --git a/cmd/guilds_tree.go b/cmd/guilds_tree.go index a127525..6a22d8a 100644 --- a/cmd/guilds_tree.go +++ b/cmd/guilds_tree.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "log/slog" "sort" "github.com/ayn2op/discordo/internal/config" @@ -53,7 +52,7 @@ func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder) { for _, gID := range folder.GuildIDs { guild, err := discordState.Cabinet.Guild(gID) if err != nil { - slog.Error("failed to get guild from state", "guild_id", gID, "err", err) + app.onError("Failed to get guild from state", err, "guild_id", gID) continue } @@ -156,7 +155,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) { channels, err := discordState.Cabinet.Channels(ref) if err != nil { - slog.Error("failed to get channels", "err", err, "guild_id", ref) + app.onError("Failed to get channels", err, "guild_id", ref) return } @@ -168,7 +167,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) { case discord.ChannelID: channel, err := discordState.Cabinet.Channel(ref) if err != nil { - slog.Error("failed to get channel", "channel_id", ref) + app.onError("Failed to get channel", err, "channel_id", ref) return } @@ -176,7 +175,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) { messages, err := discordState.Messages(channel.ID, uint(gt.cfg.MessagesLimit)) if err != nil { - slog.Error("failed to get messages", "err", err, "channel_id", channel.ID, "limit", gt.cfg.MessagesLimit) + app.onError("Failed to get messages", err, "channel_id", channel.ID, "limit", gt.cfg.MessagesLimit) return } @@ -203,7 +202,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) { case nil: // Direct messages channels, err := discordState.PrivateChannels() if err != nil { - slog.Error("failed to get private channels", "err", err) + app.onError("Failed to get private channels", err) return } diff --git a/cmd/message_input.go b/cmd/message_input.go index 5529eff..4385dbe 100644 --- a/cmd/message_input.go +++ b/cmd/message_input.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "log/slog" "os" "os/exec" "path/filepath" @@ -60,7 +59,7 @@ func newMessageInput(cfg *config.Config) *messageInput { } if err := clipboard.Init(); err != nil { - slog.Warn("failed to init clipboard", "err", err) + app.onError("Failed to init clipboard", err) } else { mi. SetClipboard(func(s string) { @@ -176,13 +175,13 @@ func (mi *messageInput) send() { if mi.edit { m, err := app.messagesList.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } data := api.EditMessageData{Content: option.NewNullableString(text)} if _, err := discordState.EditMessageComplex(m.ChannelID, m.ID, data); err != nil { - slog.Error("failed to edit message", "err", err) + app.onError("Failed to edit message", err) } mi.edit = false @@ -190,7 +189,7 @@ func (mi *messageInput) send() { data := mi.sendMessageData data.Content = text if _, err := discordState.SendMessageComplex(app.guildsTree.selectedChannelID, *data); err != nil { - slog.Error("failed to send message in channel", "channel_id", app.guildsTree.selectedChannelID, "err", err) + app.onError("Failed to send message in channel", err, "channel_id", app.guildsTree.selectedChannelID) } } @@ -272,7 +271,7 @@ func (mi *messageInput) tabComplete(isAuto bool) { members, err := discordState.Cabinet.Members(gID) if err != nil { - slog.Error("failed to get members from state", "guild_id", gID, "err", err) + app.onError("Failed to get members from state", err, "guild_id", gID) return } @@ -319,7 +318,7 @@ func (mi *messageInput) tabComplete(isAuto bool) { mi.mentionsList.Clear() mems, err := discordState.Cabinet.Members(gID) if err != nil { - slog.Error("fetching members failed", "err", err) + app.onError("Fetching members failed", err) return } res := fuzzy.FindFrom(name, memberList(mems)) @@ -349,7 +348,7 @@ func (m memberList) Len() int { return len(m) } func channelHasUser(cID discord.ChannelID, id discord.UserID) bool { perms, err := discordState.Permissions(cID, id) if err != nil { - slog.Error("can't get permissions", "channel", cID, "user", id) + app.onError("Can't get permissions", err, "channel", cID, "user", id) return false } return perms.Has(discord.PermissionViewChannel) @@ -440,7 +439,7 @@ func (mi *messageInput) addMentionItem(gID discord.GuildID, m *discord.Member) b presence, err := discordState.Cabinet.Presence(gID, m.User.ID) if err != nil { - slog.Info("failed to get presence from state", "guild_id", gID, "user_id", m.User.ID, "err", err) + app.onError("Failed to get presence from state", err, "guild_id", gID, "user_id", m.User.ID) } if presence != nil && presence.Status == discord.OfflineStatus { @@ -468,7 +467,7 @@ func (mi *messageInput) stopTabCompletion() { func (mi *messageInput) editor() { file, err := os.CreateTemp("", tmpFilePattern) if err != nil { - slog.Error("failed to create tmp file", "err", err) + app.onError("Failed to create tmp file", err) return } defer file.Close() @@ -484,14 +483,14 @@ func (mi *messageInput) editor() { app.Suspend(func() { err := cmd.Run() if err != nil { - slog.Error("failed to run command", "args", cmd.Args, "err", err) + app.onError("Failed to run command", err, "args", cmd.Args) return } }) msg, err := os.ReadFile(file.Name()) if err != nil { - slog.Error("failed to read tmp file", "name", file.Name(), "err", err) + app.onError("Failed to read tmp file", err, "name", file.Name()) return } @@ -514,14 +513,14 @@ func (mi *messageInput) openFilePicker() { paths, err := zenity.SelectFileMultiple() if err != nil { - slog.Error("failed to open file dialog", "err", err) + app.onError("Failed to open file dialog", err) return } for _, path := range paths { file, err := os.Open(path) if err != nil { - slog.Error("failed to open file", "path", path, "err", err) + app.onError("Failed to open file", err, "path", path) continue } diff --git a/cmd/messages_list.go b/cmd/messages_list.go index 0b45a85..02530d2 100644 --- a/cmd/messages_list.go +++ b/cmd/messages_list.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "log/slog" "net/http" "os" "path/filepath" @@ -145,7 +144,7 @@ func (ml *messagesList) drawAuthor(message discord.Message) { if message.GuildID.IsValid() { member, err := discordState.Cabinet.Member(message.GuildID, message.Author.ID) if err != nil { - slog.Error("failed to get member from state", "guild_id", message.GuildID, "member_id", message.Author.ID, "err", err) + app.onError("Failed to get member from state", err, "guild_id", message.GuildID, "user", message.Author) } else { if member.Nick != "" { name = member.Nick @@ -294,13 +293,13 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey { func (ml *messagesList) _select(name string) { ms, err := discordState.Cabinet.Messages(app.guildsTree.selectedChannelID) if err != nil { - slog.Error("failed to get messages", "err", err, "channel_id", app.guildsTree.selectedChannelID) + app.onError("Failed to get messages", err, "channel_id", app.guildsTree.selectedChannelID) return } msgIdx, err := ml.selectedMessageIndex() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } @@ -349,7 +348,7 @@ func (ml *messagesList) onHighlighted(added, removed, remaining []string) { if len(added) > 0 { id, err := discord.ParseSnowflake(added[0]) if err != nil { - slog.Error("Failed to parse region id as int to use as message id.", "err", err) + app.onError("Failed to parse region id as int to use as message id", err) return } @@ -360,7 +359,7 @@ func (ml *messagesList) onHighlighted(added, removed, remaining []string) { func (ml *messagesList) yankID() { msg, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } @@ -370,7 +369,7 @@ func (ml *messagesList) yankID() { func (ml *messagesList) yankContent() { msg, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } @@ -380,7 +379,7 @@ func (ml *messagesList) yankContent() { func (ml *messagesList) yankURL() { msg, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } @@ -390,7 +389,7 @@ func (ml *messagesList) yankURL() { func (ml *messagesList) open() { msg, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } @@ -492,45 +491,45 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord func (ml *messagesList) openAttachment(attachment discord.Attachment) { resp, err := http.Get(attachment.URL) if err != nil { - slog.Error("failed to fetch the attachment", "err", err, "url", attachment.URL) + app.onError("Failed to fetch the attachment", err, "url", attachment.URL) } defer resp.Body.Close() path := filepath.Join(consts.CacheDir(), "attachments") if err := os.MkdirAll(path, os.ModePerm); err != nil { - slog.Error("failed to create attachments dir", "err", err, "path", path) + app.onError("Failed to create attachments dir", err, "path", path) return } path = filepath.Join(path, attachment.Filename) file, err := os.Create(path) if err != nil { - slog.Error("failed to create attachment file", "err", err, "path", path) + app.onError("Failed to create attachment file", err, "path", path) return } defer file.Close() if _, err := io.Copy(file, resp.Body); err != nil { - slog.Error("failed to copy attachment to file", "err", err) + app.onError("Failed to copy attachment to file", err) return } if err := open.Start(path); err != nil { - slog.Error("failed to open attachment file", "err", err, "path", path) + app.onError("Failed to open attachment file", err, "path", path) return } } func (ml *messagesList) openURL(url string) { if err := open.Start(url); err != nil { - slog.Error("failed to open URL", "err", err, "url", url) + app.onError("Failed to open URL", err, "url", url) } } func (ml *messagesList) reply(mention bool) { msg, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } @@ -538,7 +537,7 @@ func (ml *messagesList) reply(mention bool) { if msg.GuildID.IsValid() { member, err := discordState.Cabinet.Member(msg.GuildID, msg.Author.ID) if err != nil { - slog.Error("failed to get member from state", "guild_id", msg.GuildID, "member_id", msg.Author.ID, "err", err) + app.onError("Failed to get member from state", err, "guild_id", msg.GuildID, "member_id", msg.Author.ID) } else { if member.Nick != "" { name = member.Nick @@ -564,18 +563,18 @@ func (ml *messagesList) reply(mention bool) { func (ml *messagesList) edit() { message, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } me, err := discordState.Cabinet.Me() if err != nil { - slog.Error("failed to get client user (me)", "err", err) + app.onError("Failed to get client user (me)", err) return } if message.Author.ID != me.ID { - slog.Error("failed to edit message; not the author", "channel_id", message.ChannelID, "message_id", message.ID) + app.onError("Failed to edit message", errors.New("You are not the author"), "channel_id", message.ChannelID, "message_id", message.ID) return } @@ -602,25 +601,25 @@ func (ml *messagesList) confirmDelete() { func (ml *messagesList) delete() { msg, err := ml.selectedMessage() if err != nil { - slog.Error("failed to get selected message", "err", err) + app.onError("Failed to get selected message", err) return } if msg.GuildID.IsValid() { me, err := discordState.Cabinet.Me() if err != nil { - slog.Error("failed to get client user (me)", "err", err) + app.onError("Failed to get client user (me)", err) return } if msg.Author.ID != me.ID && !discordState.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) { - slog.Error("failed to delete message; missing relevant permissions", "channel_id", msg.ChannelID, "message_id", msg.ID) + app.onError("Failed to delete message", errors.New("Permission denied."), "channel_id", msg.ChannelID, "message_id", msg.ID) return } } if err := discordState.DeleteMessage(app.guildsTree.selectedChannelID, msg.ID, ""); err != nil { - slog.Error("failed to delete message", "channel_id", app.guildsTree.selectedChannelID, "message_id", msg.ID, "err", err) + app.onError("Failed to delete message", err, "channel_id", app.guildsTree.selectedChannelID, "message_id", msg.ID) return } @@ -628,7 +627,7 @@ func (ml *messagesList) delete() { ml.Highlight() if err := discordState.MessageRemove(app.guildsTree.selectedChannelID, msg.ID); err != nil { - slog.Error("failed to delete message", "channel_id", app.guildsTree.selectedChannelID, "message_id", msg.ID, "err", err) + app.onError("Failed to delete message", err, "channel_id", app.guildsTree.selectedChannelID, "message_id", msg.ID) return } @@ -650,7 +649,7 @@ func (ml *messagesList) requestGuildMembers(gID discord.GuildID, ms []discord.Me UserIDs: slices.Compact(usersToFetch), }) if err != nil { - slog.Error("failed to request guild members", "guild_id", gID, "err", err) + app.onError("Failed to request guild members", err, "guild_id", gID) return } diff --git a/cmd/state.go b/cmd/state.go index bcf083a..faced45 100644 --- a/cmd/state.go +++ b/cmd/state.go @@ -152,7 +152,7 @@ func onMessageCreate(message *gateway.MessageCreateEvent) { } if err := notifications.Notify(discordState, message, app.cfg); err != nil { - slog.Error("Notification failed", "err", err) + app.onError("Notification failed", err) } } @@ -166,7 +166,7 @@ func onMessageDelete(message *gateway.MessageDeleteEvent) { if app.guildsTree.selectedChannelID == message.ChannelID { messages, err := discordState.Cabinet.Messages(message.ChannelID) if err != nil { - slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID) + app.onError("Failed to get messages from state", err, "channel_id", message.ChannelID) return }