feat: add mention support (#555)
Co-authored-by: ayn2op <ayn2op@gmail.com>
This commit is contained in:
parent
b1e30d2ef6
commit
b7e095a50b
15 changed files with 514 additions and 92 deletions
|
|
@ -22,6 +22,9 @@ type application struct {
|
|||
guildsTree *guildsTree
|
||||
messagesText *messagesText
|
||||
messageInput *messageInput
|
||||
|
||||
flexPage tview.Page
|
||||
autocompletePage tview.Page
|
||||
}
|
||||
|
||||
func newApplication(cfg *config.Config) *application {
|
||||
|
|
@ -77,14 +80,8 @@ func (a *application) run(token string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *application) clearPages() {
|
||||
for _, name := range a.pages.GetPageNames(false) {
|
||||
a.pages.RemovePage(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *application) init() {
|
||||
a.clearPages()
|
||||
a.pages.Clear()
|
||||
a.flex.Clear()
|
||||
|
||||
right := tview.NewFlex()
|
||||
|
|
@ -94,7 +91,8 @@ func (a *application) init() {
|
|||
// The guilds tree is always focused first at start-up.
|
||||
a.flex.AddItem(a.guildsTree, 0, 1, true)
|
||||
a.flex.AddItem(right, 0, 4, false)
|
||||
a.pages.AddAndSwitchToPage("flex", a.flex, true)
|
||||
a.flexPage = a.pages.AddAndSwitchToPage(a.flex, true)
|
||||
a.autocompletePage = a.pages.AddPage(a.messageInput.autocomplete, false, false)
|
||||
}
|
||||
|
||||
func (a *application) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
|
|
@ -118,13 +116,15 @@ func (a *application) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
func (a *application) onFlexInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Name() {
|
||||
case a.cfg.Keys.FocusGuildsTree:
|
||||
a.SetFocus(a.guildsTree)
|
||||
a.pages.HidePage(a.autocompletePage)
|
||||
a.SetFocus(app.guildsTree)
|
||||
return nil
|
||||
case a.cfg.Keys.FocusMessagesText:
|
||||
a.SetFocus(a.messagesText)
|
||||
a.pages.HidePage(a.autocompletePage)
|
||||
a.SetFocus(app.messagesText)
|
||||
return nil
|
||||
case a.cfg.Keys.FocusMessageInput:
|
||||
a.SetFocus(a.messageInput)
|
||||
a.SetFocus(app.messageInput)
|
||||
return nil
|
||||
case a.cfg.Keys.Logout:
|
||||
a.Stop()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type guildsTree struct {
|
|||
*tview.TreeView
|
||||
cfg *config.Config
|
||||
selectedChannelID discord.ChannelID
|
||||
selectedGuildID discord.GuildID
|
||||
}
|
||||
|
||||
func newGuildsTree(cfg *config.Config) *guildsTree {
|
||||
|
|
@ -201,6 +202,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
app.messagesText.SetTitle(gt.channelToString(*channel))
|
||||
|
||||
gt.selectedChannelID = channel.ID
|
||||
gt.selectedGuildID = channel.GuildID
|
||||
app.SetFocus(app.messageInput)
|
||||
case nil: // Direct messages
|
||||
channels, err := discordState.PrivateChannels()
|
||||
|
|
|
|||
|
|
@ -5,30 +5,46 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/sahilm/fuzzy"
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/consts"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/discordo/internal/cache"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/ningen/v3/discordmd"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
const tmpFilePattern = consts.Name + "_*.md"
|
||||
var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+")
|
||||
|
||||
type messageInput struct {
|
||||
*tview.TextArea
|
||||
cfg *config.Config
|
||||
replyMessageID discord.MessageID
|
||||
cfg *config.Config
|
||||
cache *cache.Cache
|
||||
autocomplete *tview.List
|
||||
replyMessageID discord.MessageID
|
||||
lastSearch time.Time
|
||||
}
|
||||
|
||||
type memberList []discord.Member
|
||||
|
||||
func newMessageInput(cfg *config.Config) *messageInput {
|
||||
mi := &messageInput{
|
||||
TextArea: tview.NewTextArea(),
|
||||
cfg: cfg,
|
||||
TextArea: tview.NewTextArea(),
|
||||
cfg: cfg,
|
||||
cache: cache.NewCache(),
|
||||
autocomplete: tview.NewList(),
|
||||
}
|
||||
|
||||
mi.Box = ui.NewConfiguredBox(mi.Box, &cfg.Theme)
|
||||
|
|
@ -43,6 +59,18 @@ func newMessageInput(cfg *config.Config) *messageInput {
|
|||
}).
|
||||
SetInputCapture(mi.onInputCapture)
|
||||
|
||||
mi.autocomplete.Box = ui.NewConfiguredBox(mi.autocomplete.Box, &mi.cfg.Theme)
|
||||
mi.autocomplete.SetTitle("Mention")
|
||||
mi.autocomplete.
|
||||
ShowSecondaryText(false).
|
||||
SetSelectedStyle(tcell.StyleDefault.
|
||||
Background(tcell.ColorWhite).
|
||||
Foreground(tcell.ColorBlack))
|
||||
mi.autocomplete.SetRect(0, 0, 0, 0)
|
||||
b := mi.autocomplete.GetBorderSet()
|
||||
b.BottomLeft = b.BottomT
|
||||
b.BottomRight = b.BottomT
|
||||
mi.autocomplete.SetBorderSet(b)
|
||||
return mi
|
||||
}
|
||||
|
||||
|
|
@ -55,16 +83,55 @@ func (mi *messageInput) reset() {
|
|||
func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Name() {
|
||||
case mi.cfg.Keys.MessageInput.Send:
|
||||
if app.pages.GetVisible(app.autocompletePage) {
|
||||
mi.tabComplete(false)
|
||||
return nil
|
||||
}
|
||||
mi.send()
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.Editor:
|
||||
mi.stopTabCompletion()
|
||||
mi.editor()
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.Cancel:
|
||||
mi.reset()
|
||||
if app.pages.GetVisible(app.autocompletePage) {
|
||||
mi.stopTabCompletion()
|
||||
} else {
|
||||
mi.reset()
|
||||
}
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.TabComplete:
|
||||
go app.QueueUpdateDraw(func(){ mi.tabComplete(false) })
|
||||
return nil
|
||||
}
|
||||
|
||||
if app.pages.GetVisible(app.autocompletePage) && mi.cfg.AutocompleteLimit > 0 {
|
||||
count := mi.autocomplete.GetItemCount()
|
||||
cur := mi.autocomplete.GetCurrentItem()
|
||||
n := event.Name()
|
||||
switch n {
|
||||
case mi.cfg.Keys.Autocomplete.Down:
|
||||
if cur == count-1 {
|
||||
mi.autocomplete.SetCurrentItem(0)
|
||||
} else {
|
||||
mi.autocomplete.SetCurrentItem(cur+1)
|
||||
}
|
||||
return nil
|
||||
case mi.cfg.Keys.Autocomplete.Up:
|
||||
if cur == 0 {
|
||||
mi.autocomplete.SetCurrentItem(count-1)
|
||||
} else {
|
||||
mi.autocomplete.SetCurrentItem(cur-1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
go app.QueueUpdateDraw(func(){ mi.tabComplete(true) })
|
||||
} else {
|
||||
go app.QueueUpdate(func(){ mi.tabComplete(true) })
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +145,10 @@ func (mi *messageInput) send() {
|
|||
return
|
||||
}
|
||||
|
||||
// Process mentions (there's no shortcut, just parse the entire message
|
||||
// as markdown and then expand non-code mentions)
|
||||
data := api.SendMessageData{
|
||||
Content: text,
|
||||
Content: processText(app.guildsTree.selectedChannelID, []byte(text)),
|
||||
}
|
||||
if mi.replyMessageID != 0 {
|
||||
data.Reference = &discord.MessageReference{MessageID: mi.replyMessageID}
|
||||
|
|
@ -103,12 +172,252 @@ func (mi *messageInput) send() {
|
|||
app.messagesText.ScrollToEnd()
|
||||
}
|
||||
|
||||
func (mi *messageInput) editor() {
|
||||
editor := mi.cfg.Editor
|
||||
if editor == "" {
|
||||
func processText(cID discord.ChannelID, src []byte) string {
|
||||
// ranges we can expandMentions in them
|
||||
var rngs [][2]int
|
||||
canMention := true
|
||||
n := discordmd.Parse(src)
|
||||
ast.Walk(n, func(n ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
switch n := n.(type) {
|
||||
case *ast.CodeBlock:
|
||||
canMention = !enter
|
||||
case *discordmd.Inline:
|
||||
if (n.Attr & discordmd.AttrMonospace) != 0 {
|
||||
canMention = !enter
|
||||
}
|
||||
case *ast.Text:
|
||||
if canMention {
|
||||
rngs = append(rngs, [2]int{ n.Segment.Start,
|
||||
n.Segment.Stop })
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
for _, rng := range rngs {
|
||||
src = slices.Replace(src, rng[0], rng[1],
|
||||
expandMentions(cID, src[rng[0]:rng[1]])...)
|
||||
}
|
||||
return string(src)
|
||||
}
|
||||
|
||||
func expandMentions(cID discord.ChannelID, src []byte) []byte {
|
||||
return mentionRegex.ReplaceAllFunc(src, func(in []byte) (out []byte) {
|
||||
out = in
|
||||
name := strings.ToLower(string(in[1:]))
|
||||
discordState.MemberStore.Each(app.guildsTree.selectedGuildID, func (m *discord.Member) bool {
|
||||
if strings.ToLower(m.User.Username) == name {
|
||||
if channelHasUser(cID , m.User.ID) {
|
||||
out = []byte(m.User.ID.Mention())
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (mi *messageInput) tabComplete(isAuto bool) {
|
||||
posEnd, name, r := mi.GetWordUnderCursor(isValidUserRune)
|
||||
if r != '@' {
|
||||
mi.stopTabCompletion()
|
||||
return
|
||||
}
|
||||
pos := posEnd - (len(name)+1)
|
||||
|
||||
if !isAuto && mi.autocomplete.GetItemCount() != 0 {
|
||||
_, name = mi.autocomplete.GetItemText(mi.autocomplete.GetCurrentItem())
|
||||
mi.Replace(pos, posEnd, "@" + name + " ")
|
||||
mi.stopTabCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
gID := app.guildsTree.selectedGuildID
|
||||
cID := app.guildsTree.selectedChannelID
|
||||
|
||||
// Special case, show recent messages' authors
|
||||
if name == "" {
|
||||
msgs, err := discordState.Cabinet.Messages(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
shown := make(map[string]bool)
|
||||
mi.autocomplete.Clear()
|
||||
for _, m := range msgs {
|
||||
if shown[m.Author.Username] {
|
||||
continue
|
||||
}
|
||||
shown[m.Author.Username] = true
|
||||
discordState.MemberState.RequestMember(gID, m.Author.ID)
|
||||
if mem, err := discordState.Cabinet.Member(gID, m.Author.ID); err == nil {
|
||||
if mi.addAutocompleteItem(gID, mem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mi.searchMember(gID, name)
|
||||
mi.autocomplete.Clear()
|
||||
mems, err := discordState.Cabinet.Members(gID)
|
||||
if err != nil {
|
||||
slog.Error("fetching members failed", "err", err)
|
||||
return
|
||||
}
|
||||
res := fuzzy.FindFrom(name, memberList(mems))
|
||||
if mi.cfg.AutocompleteLimit != 0 &&
|
||||
len(res) > int(mi.cfg.AutocompleteLimit) {
|
||||
res = res[:int(mi.cfg.AutocompleteLimit)]
|
||||
}
|
||||
for _, r := range res {
|
||||
if channelHasUser(cID, mems[r.Index].User.ID) &&
|
||||
mi.addAutocompleteItem(gID, &mems[r.Index]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mi.autocomplete.GetItemCount() == 0 {
|
||||
mi.stopTabCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
if mi.cfg.AutocompleteLimit == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, col, _, _ := mi.GetCursor()
|
||||
mi.showMentionList(col-1)
|
||||
}
|
||||
|
||||
func (m memberList) String(i int) string { return m[i].Nick + m[i].User.DisplayName + m[i].User.Tag() }
|
||||
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)
|
||||
return false
|
||||
}
|
||||
return perms.Has(discord.PermissionViewChannel)
|
||||
}
|
||||
|
||||
func (mi *messageInput) searchMember(gID discord.GuildID, name string) {
|
||||
key := gID.String() + " " + name
|
||||
if mi.cache.Exists(key) {
|
||||
return
|
||||
}
|
||||
// If searching for "ab" returns less than SearchLimit,
|
||||
// then "abc" would not return anything new because we already searched
|
||||
// everything starting with "ab". This will still be true even if a new
|
||||
// member joins because arikawa loads new members into the state.
|
||||
if k := key[:len(key)-1]; mi.cache.Exists(k) {
|
||||
if c := mi.cache.Get(k);
|
||||
c < discordState.MemberState.SearchLimit {
|
||||
mi.cache.Create(key, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Rate limit on our side because we can't distinguish between a
|
||||
// successful search and SearchMember not doing anything becuase of its
|
||||
// internal rate limit that we can't detect
|
||||
if mi.lastSearch.Add(discordState.MemberState.SearchFrequency).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
mi.lastSearch = time.Now()
|
||||
app.messagesText.waitForChunkEvent()
|
||||
app.messagesText.setFetchingChunk(true, 0)
|
||||
discordState.MemberState.SearchMember(gID, name)
|
||||
mi.cache.Create(key, app.messagesText.waitForChunkEvent())
|
||||
}
|
||||
|
||||
|
||||
func isValidUserRune(x rune) bool {
|
||||
return (x >= 'a' && x <= 'z') ||
|
||||
(x >= 'A' && x <= 'Z') ||
|
||||
(x >= '0' && x <= '9') ||
|
||||
x == '_' || x == '.'
|
||||
}
|
||||
|
||||
func (mi *messageInput) showMentionList(col int) {
|
||||
borders := 0
|
||||
if mi.cfg.Theme.Border.Enabled {
|
||||
borders = 1
|
||||
}
|
||||
l := mi.autocomplete
|
||||
x, _, _, _ := mi.GetInnerRect()
|
||||
_, y, _, _ := mi.GetRect()
|
||||
_, _, maxW, maxH := app.messagesText.GetInnerRect()
|
||||
if t := int(mi.cfg.Theme.Autocomplete.MaxHeight); t != 0 {
|
||||
maxH = min(maxH, t)
|
||||
}
|
||||
count := l.GetItemCount() + borders
|
||||
h := min(count, maxH) + borders + mi.cfg.Theme.Border.Padding[1]
|
||||
y -= h
|
||||
w := int(mi.cfg.Theme.Autocomplete.MinWidth)
|
||||
if w == 0 {
|
||||
w = maxW
|
||||
} else {
|
||||
for i := 0; i < count-1; i++ {
|
||||
t, _ := mi.autocomplete.GetItemText(i)
|
||||
w = max(w, tview.TaggedStringWidth(t))
|
||||
}
|
||||
w = min(w + borders*2, maxW)
|
||||
x += min(col, maxW - w)
|
||||
}
|
||||
l.SetRect(x, y, w, h)
|
||||
app.pages.ShowPage(app.autocompletePage)
|
||||
app.SetFocus(mi)
|
||||
}
|
||||
|
||||
func (mi *messageInput) addAutocompleteItem(gID discord.GuildID, m *discord.Member) bool {
|
||||
username := m.User.Username
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
var dname string
|
||||
if mi.cfg.Theme.Autocomplete.ShowNicknames && m.Nick != "" {
|
||||
dname = m.Nick
|
||||
} else {
|
||||
dname = m.User.DisplayName
|
||||
}
|
||||
if dname != "" {
|
||||
dname = tview.Escape(dname)
|
||||
}
|
||||
// this is WAY faster than discordState.MemberColor
|
||||
if mi.cfg.Theme.Autocomplete.ShowUsernameColors {
|
||||
if c, ok := state.MemberColor(m, func(id discord.RoleID) *discord.Role {
|
||||
r, _ := discordState.Cabinet.Role(gID, id)
|
||||
return r
|
||||
}); ok {
|
||||
if dname != "" {
|
||||
dname = "[" + c.String() + "]" + dname + "[-]"
|
||||
} else {
|
||||
username = "[" + c.String() + "]" + username + "[-]"
|
||||
}
|
||||
}
|
||||
}
|
||||
// The username overwrite in the case of dname == "" is intended
|
||||
if presence, _ := discordState.Cabinet.Presence(gID, m.User.ID);
|
||||
presence == nil || presence.Status == discord.OfflineStatus {
|
||||
username = "[::d]" + username + "[::D]"
|
||||
}
|
||||
if dname != "" {
|
||||
mi.autocomplete.AddItem(dname + " (" + username + ")", m.User.Username, 0, nil)
|
||||
} else {
|
||||
mi.autocomplete.AddItem(username, m.User.Username, 0, nil)
|
||||
}
|
||||
return mi.autocomplete.GetItemCount() > int(mi.cfg.AutocompleteLimit)
|
||||
}
|
||||
|
||||
func (mi *messageInput) stopTabCompletion() {
|
||||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
app.pages.HidePage(app.autocompletePage)
|
||||
mi.autocomplete.Clear()
|
||||
app.SetFocus(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (mi *messageInput) editor() {
|
||||
file, err := os.CreateTemp("", tmpFilePattern)
|
||||
if err != nil {
|
||||
slog.Error("failed to create tmp file", "err", err)
|
||||
|
|
@ -119,7 +428,7 @@ func (mi *messageInput) editor() {
|
|||
|
||||
_, _ = file.WriteString(mi.GetText())
|
||||
|
||||
cmd := exec.Command(editor, file.Name())
|
||||
cmd := exec.Command(mi.cfg.Editor, file.Name())
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ type messagesText struct {
|
|||
fetchingMembers struct {
|
||||
mu sync.Mutex
|
||||
value bool
|
||||
count uint
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
urlListPage tview.Page
|
||||
}
|
||||
|
||||
func newMessagesText(cfg *config.Config) *messagesText {
|
||||
|
|
@ -76,8 +79,10 @@ func (mt *messagesText) drawMsgs(cID discord.ChannelID) {
|
|||
}
|
||||
}
|
||||
|
||||
mt.Clear()
|
||||
|
||||
for _, m := range slices.Backward(msgs) {
|
||||
app.messagesText.createMsg(m)
|
||||
mt.createMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,8 +450,8 @@ func extractURLs(content string) []string {
|
|||
|
||||
func (mt *messagesText) showUrlSelector(urls []string, attachments []discord.Attachment) {
|
||||
done := func() {
|
||||
app.pages.RemovePage("list").SwitchToPage("flex")
|
||||
app.SetFocus(app.messagesText)
|
||||
app.pages.RemovePage(mt.urlListPage).SwitchToPage(app.flexPage)
|
||||
app.SetFocus(mt)
|
||||
}
|
||||
|
||||
list := tview.NewList().
|
||||
|
|
@ -473,24 +478,19 @@ func (mt *messagesText) showUrlSelector(urls []string, attachments []discord.Att
|
|||
})
|
||||
|
||||
for i, a := range attachments {
|
||||
attachment := a
|
||||
list.AddItem(a.Filename, "", rune('a'+i), func() {
|
||||
go openURL(attachment.URL)
|
||||
done()
|
||||
go openURL(a.URL)
|
||||
})
|
||||
}
|
||||
|
||||
for i, u := range urls {
|
||||
url := u
|
||||
list.AddItem(u, "", rune('1'+i), func() {
|
||||
go openURL(url)
|
||||
done()
|
||||
go openURL(u)
|
||||
})
|
||||
}
|
||||
|
||||
app.pages.
|
||||
AddAndSwitchToPage("list", ui.Centered(list, 0, 0), true).
|
||||
ShowPage("flex")
|
||||
mt.urlListPage = app.pages.AddAndSwitchToPage(ui.Centered(list, 0, 0), true)
|
||||
app.pages.ShowPage(app.flexPage)
|
||||
}
|
||||
|
||||
func openURL(url string) {
|
||||
|
|
@ -547,22 +547,17 @@ func (mt *messagesText) delete() {
|
|||
return
|
||||
}
|
||||
|
||||
mt.selectedMessageID = 0
|
||||
app.messageInput.replyMessageID = 0
|
||||
mt.Highlight()
|
||||
|
||||
if err := discordState.MessageRemove(app.guildsTree.selectedChannelID, msg.ID); err != nil {
|
||||
slog.Error("failed to delete message", "err", err, "channel_id", app.guildsTree.selectedChannelID, "message_id", msg.ID)
|
||||
return
|
||||
}
|
||||
|
||||
ms, err := discordState.Cabinet.Messages(app.guildsTree.selectedChannelID)
|
||||
if err != nil {
|
||||
slog.Error("failed to delete message", "err", err, "channel_id", app.guildsTree.selectedChannelID)
|
||||
return
|
||||
}
|
||||
|
||||
mt.Clear()
|
||||
|
||||
for _, m := range slices.Backward(ms) {
|
||||
app.messagesText.createMsg(m)
|
||||
}
|
||||
// No need to redraw messages after deletion, onMessageDelete will do
|
||||
// its work after the event returns
|
||||
}
|
||||
|
||||
func (mt *messagesText) requestGuildMembers(gID discord.GuildID, ms []discord.Message) {
|
||||
|
|
@ -583,12 +578,12 @@ func (mt *messagesText) requestGuildMembers(gID discord.GuildID, ms []discord.Me
|
|||
return
|
||||
}
|
||||
|
||||
mt.setFetchingChunk(true)
|
||||
mt.setFetchingChunk(true, 0)
|
||||
mt.waitForChunkEvent()
|
||||
}
|
||||
}
|
||||
|
||||
func (mt *messagesText) setFetchingChunk(value bool) {
|
||||
func (mt *messagesText) setFetchingChunk(value bool, count uint) {
|
||||
mt.fetchingMembers.mu.Lock()
|
||||
defer mt.fetchingMembers.mu.Unlock()
|
||||
|
||||
|
|
@ -601,21 +596,19 @@ func (mt *messagesText) setFetchingChunk(value bool) {
|
|||
if value {
|
||||
mt.fetchingMembers.done = make(chan struct{})
|
||||
} else {
|
||||
mt.fetchingMembers.count = count
|
||||
close(mt.fetchingMembers.done)
|
||||
}
|
||||
}
|
||||
|
||||
func (mt *messagesText) waitForChunkEvent() {
|
||||
func (mt *messagesText) waitForChunkEvent() uint {
|
||||
mt.fetchingMembers.mu.Lock()
|
||||
if !mt.fetchingMembers.value {
|
||||
mt.fetchingMembers.mu.Unlock()
|
||||
return
|
||||
return 0
|
||||
}
|
||||
mt.fetchingMembers.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-mt.fetchingMembers.done:
|
||||
default:
|
||||
<-mt.fetchingMembers.done
|
||||
}
|
||||
<-mt.fetchingMembers.done
|
||||
return mt.fetchingMembers.count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ import (
|
|||
"github.com/ayn2op/discordo/internal/logger"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/diamondburned/arikawa/v3/utils/ws"
|
||||
"github.com/diamondburned/ningen/v3"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
var (
|
||||
discordState *state
|
||||
discordState *ningen.State
|
||||
app *application
|
||||
)
|
||||
|
||||
|
|
|
|||
45
cmd/state.go
45
cmd/state.go
|
|
@ -15,10 +15,6 @@ import (
|
|||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
*ningen.State
|
||||
}
|
||||
|
||||
func openState(token string) error {
|
||||
api.UserAgent = app.cfg.Identify.UserAgent
|
||||
gateway.DefaultIdentity = gateway.IdentifyProperties{
|
||||
|
|
@ -34,17 +30,19 @@ func openState(token string) error {
|
|||
Status: app.cfg.Identify.Status,
|
||||
}
|
||||
|
||||
discordState = &state{
|
||||
State: ningen.New(token),
|
||||
}
|
||||
discordState = ningen.New(token)
|
||||
|
||||
// Handlers
|
||||
discordState.AddHandler(discordState.onReady)
|
||||
discordState.AddHandler(discordState.onMessageCreate)
|
||||
discordState.AddHandler(discordState.onMessageDelete)
|
||||
discordState.AddHandler(onReady)
|
||||
discordState.AddHandler(onMessageCreate)
|
||||
discordState.AddHandler(onMessageDelete)
|
||||
|
||||
discordState.AddHandler(func(_ *gateway.GuildMembersChunkEvent) {
|
||||
app.messagesText.setFetchingChunk(false)
|
||||
discordState.AddHandler(func(event *gateway.GuildMembersChunkEvent) {
|
||||
app.messagesText.setFetchingChunk(false, uint(len(event.Members)))
|
||||
})
|
||||
|
||||
discordState.AddHandler(func(event *gateway.GuildMemberRemoveEvent) {
|
||||
app.messageInput.cache.Invalidate(event.GuildID.String() + " " + event.User.Username, discordState.MemberState.SearchLimit)
|
||||
})
|
||||
|
||||
discordState.AddHandler(func(event *ws.RawEvent) {
|
||||
|
|
@ -63,12 +61,12 @@ func openState(token string) error {
|
|||
slog.Error("state log", "err", err)
|
||||
}
|
||||
|
||||
discordState.OnRequest = append(discordState.OnRequest, discordState.onRequest)
|
||||
discordState.OnRequest = append(discordState.OnRequest, onRequest)
|
||||
|
||||
return discordState.Open(context.TODO())
|
||||
}
|
||||
|
||||
func (s *state) onRequest(r httpdriver.Request) error {
|
||||
func onRequest(r httpdriver.Request) error {
|
||||
req, ok := r.(*httpdriver.DefaultRequest)
|
||||
if ok {
|
||||
slog.Debug("new HTTP request", "method", req.Method, "url", req.URL)
|
||||
|
|
@ -77,7 +75,7 @@ func (s *state) onRequest(r httpdriver.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *state) onReady(ready *gateway.ReadyEvent) {
|
||||
func onReady(r *gateway.ReadyEvent) {
|
||||
root := app.guildsTree.GetRoot()
|
||||
root.ClearChildren()
|
||||
|
||||
|
|
@ -85,7 +83,7 @@ func (s *state) onReady(ready *gateway.ReadyEvent) {
|
|||
dmNode.SetColor(tcell.GetColor(app.cfg.Theme.GuildsTree.PrivateChannelColor))
|
||||
root.AddChild(dmNode)
|
||||
|
||||
for _, folder := range ready.UserSettings.GuildFolders {
|
||||
for _, folder := range r.UserSettings.GuildFolders {
|
||||
if folder.ID == 0 && len(folder.GuildIDs) == 1 {
|
||||
guild, err := discordState.Cabinet.Guild(folder.GuildIDs[0])
|
||||
if err != nil {
|
||||
|
|
@ -110,23 +108,22 @@ func (s *state) onReady(ready *gateway.ReadyEvent) {
|
|||
app.SetFocus(app.guildsTree)
|
||||
}
|
||||
|
||||
func (s *state) onMessageCreate(msg *gateway.MessageCreateEvent) {
|
||||
func onMessageCreate(m *gateway.MessageCreateEvent) {
|
||||
if app.guildsTree.selectedChannelID.IsValid() &&
|
||||
app.guildsTree.selectedChannelID == msg.ChannelID {
|
||||
app.messagesText.createMsg(msg.Message)
|
||||
app.guildsTree.selectedChannelID == m.ChannelID {
|
||||
app.messagesText.createMsg(m.Message)
|
||||
}
|
||||
|
||||
if err := notifications.HandleIncomingMessage(s.State, msg, app.cfg); err != nil {
|
||||
if err := notifications.HandleIncomingMessage(discordState, m, app.cfg); err != nil {
|
||||
slog.Error("Notification failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) onMessageDelete(msg *gateway.MessageDeleteEvent) {
|
||||
if app.guildsTree.selectedChannelID == msg.ChannelID {
|
||||
func onMessageDelete(m *gateway.MessageDeleteEvent) {
|
||||
if app.guildsTree.selectedChannelID == m.ChannelID {
|
||||
app.messagesText.selectedMessageID = 0
|
||||
app.messagesText.Highlight()
|
||||
app.messagesText.Clear()
|
||||
|
||||
app.messagesText.drawMsgs(msg.ChannelID)
|
||||
app.messagesText.drawMsgs(m.ChannelID)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
go.mod
7
go.mod
|
|
@ -5,12 +5,13 @@ go 1.24.3
|
|||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/ayn2op/tview v0.0.0-20250604214506-7f855bbbeea6
|
||||
github.com/ayn2op/tview v0.0.0-20250615210231-7feb9ae5f9b1
|
||||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
|
||||
github.com/diamondburned/arikawa/v3 v3.5.0
|
||||
github.com/diamondburned/ningen/v3 v3.0.1-0.20250607192146-d6b46a4689a5
|
||||
github.com/gdamore/tcell/v2 v2.8.1
|
||||
github.com/gen2brain/beeep v0.10.0
|
||||
github.com/gen2brain/beeep v0.11.0
|
||||
github.com/sahilm/fuzzy v0.1.1
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/yuin/goldmark v1.7.12
|
||||
github.com/zalando/go-keyring v0.2.6
|
||||
|
|
@ -26,8 +27,10 @@ require (
|
|||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -27,6 +27,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/ayn2op/tview v0.0.0-20250604214506-7f855bbbeea6 h1:W+W8iOKrt1mwNk1NJKg8D2y7kG9c+aA+RjrCiWytNaQ=
|
||||
github.com/ayn2op/tview v0.0.0-20250604214506-7f855bbbeea6/go.mod h1:PuMMP3J7SfW0jgAc1fdABOHJkoGbQIw3jejRAkl0API=
|
||||
github.com/ayn2op/tview v0.0.0-20250615210231-7feb9ae5f9b1 h1:yJcq6PmnNKMZTr7y5mWylp3+XY20vNTNHwxRIPGL9YU=
|
||||
github.com/ayn2op/tview v0.0.0-20250615210231-7feb9ae5f9b1/go.mod h1:PuMMP3J7SfW0jgAc1fdABOHJkoGbQIw3jejRAkl0API=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
|
|
@ -53,6 +55,8 @@ github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997N
|
|||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||
github.com/gen2brain/beeep v0.10.0 h1:sR/rgmJjHVOVABgpbuICvw7SVtI13RRNnQPv+wiaoMg=
|
||||
github.com/gen2brain/beeep v0.10.0/go.mod h1:UzRwrHPeN99aobEPCjiuBossVv32YViFiytGwaA1EO0=
|
||||
github.com/gen2brain/beeep v0.11.0 h1:ScADjlllAwqUIdEgosHXRmS0OEBTDtfGvIuLj2G/mlU=
|
||||
github.com/gen2brain/beeep v0.11.0/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
|
|
@ -94,6 +98,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
|
||||
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
|
|
@ -106,6 +112,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
|||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
|
@ -117,6 +125,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
|
||||
|
|
|
|||
46
internal/cache/cache.go
vendored
Normal file
46
internal/cache/cache.go
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Used by MessageInput.searchMember to not overflow the gateway with redundent
|
||||
// search requests.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
items sync.Map
|
||||
}
|
||||
|
||||
func NewCache() *Cache {
|
||||
return &Cache{ items: sync.Map{} }
|
||||
}
|
||||
|
||||
func (c *Cache) Create(query string, value uint) {
|
||||
c.items.Store(query, value)
|
||||
}
|
||||
|
||||
func (c *Cache) Exists(query string) (ok bool) {
|
||||
_, ok = c.items.Load(query)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache) Get(query string) uint {
|
||||
i, _ := c.items.Load(query)
|
||||
return i.(uint)
|
||||
}
|
||||
|
||||
// Invalidate is only needed when a member leaves and the search query reaches
|
||||
// the search limit.
|
||||
// "aa", "ab", "ac", ..., "ay" // where length is longer than the limit
|
||||
// if "ay" leaves, then "az" would not be loaded becaue it would not be
|
||||
// returned by the search results because of the search limit
|
||||
func (c *Cache) Invalidate(name string, limit uint) {
|
||||
for name != "" {
|
||||
if c.Exists(name) && c.Get(name) >= limit {
|
||||
for name != "" {
|
||||
c.items.Delete(name)
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
}
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,9 @@ type (
|
|||
Markdown bool `toml:"markdown"`
|
||||
HideBlockedUsers bool `toml:"hide_blocked_users"`
|
||||
ShowAttachmentLinks bool `toml:"show_attachment_links"`
|
||||
|
||||
// Use 0 to disable
|
||||
AutocompleteLimit uint8 `toml:"autocomplete_limit"`
|
||||
MessagesLimit uint8 `toml:"messages_limit"`
|
||||
|
||||
Timestamps Timestamps `toml:"timestamps"`
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ editor = "default"
|
|||
markdown = true
|
||||
hide_blocked_users = true
|
||||
show_attachment_links = true
|
||||
|
||||
# Use autocomplete_limit = 0 to disable autocompleting mentions
|
||||
# Note: tab completion will still work, but it won't show any list.
|
||||
autocomplete_limit = 20
|
||||
|
||||
# The number of messages to fetch when a text-based channel is selected from guilds tree. The minimum and maximum value is 0 and 100, respectively.
|
||||
messages_limit = 50
|
||||
|
||||
|
|
@ -82,12 +87,19 @@ yank_id = "Rune[i]"
|
|||
# Only while typing a message
|
||||
# Alt+Enter: Insert a new line to the current text.
|
||||
[keys.message_input]
|
||||
# Send the message.
|
||||
send = "Enter"
|
||||
# Open message input in your editor.
|
||||
editor = "Ctrl+E"
|
||||
# Remove existing text or cancel reply.
|
||||
cancel = "Esc"
|
||||
# Complete usernames when mentioning
|
||||
tab_complete = "Tab"
|
||||
|
||||
# When typing and there's an autocomplete list visible,
|
||||
# pressing these keys will focus on the list instead of the message input.
|
||||
[keys.autocomplete]
|
||||
up = "Up"
|
||||
down = "Down"
|
||||
|
||||
# Applies to all
|
||||
[theme]
|
||||
|
|
@ -122,10 +134,25 @@ channel_color = "white"
|
|||
# Set to false to show messages with usernames instead of nicknames
|
||||
show_user_nicks = true
|
||||
show_user_colors = true
|
||||
|
||||
reply_indicator = ">"
|
||||
forwarded_indicator = "<"
|
||||
|
||||
author_style = { foreground = "aqua" }
|
||||
mention_style = { foreground = "blue" }
|
||||
emoji_style = { foreground = "green" }
|
||||
url_style = { foreground = "blue" }
|
||||
attachment_style = { foreground = "yellow" }
|
||||
|
||||
[theme.autocomplete]
|
||||
show_user_nicks = true
|
||||
show_user_colors = true
|
||||
|
||||
# Note: width and height are capped to the avaliable space
|
||||
# Minimum width
|
||||
# 0 = make the list as wide as possible
|
||||
min_width = 20
|
||||
|
||||
# Maximum height
|
||||
# 0 = make the list as tall as needed
|
||||
max_height = 0
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type (
|
|||
GuildsTree GuildsTreeKeys `toml:"guilds_tree"`
|
||||
MessagesText MessagesTextKeys `toml:"messages_text"`
|
||||
MessageInput MessageInputKeys `toml:"message_input"`
|
||||
Autocomplete AutocompleteKeys `toml:"autocomplete"`
|
||||
|
||||
Logout string `toml:"logout"`
|
||||
Quit string `toml:"quit"`
|
||||
|
|
@ -47,8 +48,14 @@ type (
|
|||
}
|
||||
|
||||
MessageInputKeys struct {
|
||||
Send string `toml:"send"`
|
||||
Editor string `toml:"editor"`
|
||||
Cancel string `toml:"cancel"`
|
||||
Send string `toml:"send"`
|
||||
Editor string `toml:"editor"`
|
||||
Cancel string `toml:"cancel"`
|
||||
TabComplete string `toml:"tab_complete"`
|
||||
}
|
||||
|
||||
AutocompleteKeys struct {
|
||||
Up string `toml:"up"`
|
||||
Down string `toml:"down"`
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ type (
|
|||
Border BorderTheme `toml:"border"`
|
||||
GuildsTree GuildsTreeTheme `toml:"guilds_tree"`
|
||||
MessagesText MessagesTextTheme `toml:"messages_text"`
|
||||
Autocomplete AutocompleteTheme `toml:"autocomplete"`
|
||||
}
|
||||
|
||||
GuildsTreeTheme struct {
|
||||
|
|
@ -113,4 +114,12 @@ type (
|
|||
URLStyle StyleWrapper `toml:"url_style"`
|
||||
AttachmentStyle StyleWrapper `toml:"attachment_style"`
|
||||
}
|
||||
|
||||
AutocompleteTheme struct {
|
||||
ShowNicknames bool `toml:"show_user_nicks"`
|
||||
ShowUsernameColors bool `toml:"show_user_colors"`
|
||||
|
||||
MinWidth uint `toml:"min_width"`
|
||||
MaxHeight uint `toml:"max_height"`
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ type Form struct {
|
|||
cfg *config.Config
|
||||
form *tview.Form
|
||||
done DoneFn
|
||||
|
||||
formPage tview.Page
|
||||
errPage tview.Page
|
||||
}
|
||||
|
||||
func NewForm(cfg *config.Config, done DoneFn) *Form {
|
||||
|
|
@ -34,7 +37,7 @@ func NewForm(cfg *config.Config, done DoneFn) *Form {
|
|||
AddPasswordField("Password", "", 0, 0, nil).
|
||||
AddPasswordField("Code (optional)", "", 0, 0, nil).
|
||||
AddButton("Login", f.login)
|
||||
f.AddAndSwitchToPage("form", f.form, true)
|
||||
f.formPage = f.AddAndSwitchToPage(f.form, true)
|
||||
return f
|
||||
}
|
||||
|
||||
|
|
@ -90,9 +93,8 @@ func (f *Form) onError(err error) {
|
|||
SetText(err.Error()).
|
||||
AddButtons([]string{"Close"}).
|
||||
SetDoneFunc(func(_ int, _ string) {
|
||||
f.RemovePage("modal").SwitchToPage("form")
|
||||
f.RemovePage(f.errPage).SwitchToPage(f.formPage)
|
||||
})
|
||||
f.
|
||||
AddAndSwitchToPage("modal", ui.Centered(modal, 0, 0), true).
|
||||
ShowPage("form")
|
||||
f.errPage = f.AddAndSwitchToPage(ui.Centered(modal, 0, 0), true)
|
||||
f.ShowPage(f.formPage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package markdown
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/diamondburned/ningen/v3/discordmd"
|
||||
|
|
@ -34,7 +35,7 @@ func (r *renderer) Render(w io.Writer, source []byte, n ast.Node) error {
|
|||
case *ast.Document:
|
||||
// noop
|
||||
case *ast.Heading:
|
||||
r.renderHeading(w)
|
||||
r.renderHeading(w, n, entering)
|
||||
case *ast.Text:
|
||||
r.renderText(w, n, entering, source)
|
||||
case *ast.FencedCodeBlock:
|
||||
|
|
@ -56,14 +57,26 @@ func (r *renderer) Render(w io.Writer, source []byte, n ast.Node) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (r *renderer) renderHeading(w io.Writer) {
|
||||
io.WriteString(w, "\n")
|
||||
func (r *renderer) renderHeading(w io.Writer, n *ast.Heading, entering bool) {
|
||||
if entering {
|
||||
io.WriteString(w, strings.Repeat("#", n.Level))
|
||||
io.WriteString(w, " ")
|
||||
} else {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renderer) renderFencedCodeBlock(w io.Writer, n *ast.FencedCodeBlock, entering bool, source []byte) {
|
||||
io.WriteString(w, "\n")
|
||||
|
||||
if entering {
|
||||
// language
|
||||
if l := n.Language(source); l != nil {
|
||||
io.WriteString(w, "|=> ")
|
||||
w.Write(l)
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
|
||||
// body
|
||||
lines := n.Lines()
|
||||
for i := range lines.Len() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue