| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- package ui
- import (
- "cmp"
- "net/url"
- "path"
- "slices"
- "strings"
- "github.com/ayn2op/discordo/internal/config"
- "github.com/ayn2op/tview"
- "github.com/diamondburned/arikawa/v3/discord"
- "github.com/diamondburned/ningen/v3"
- "github.com/gdamore/tcell/v3"
- )
- // ConfigureBox configures the provided box according to the provided theme.
- func ConfigureBox(box *tview.Box, cfg *config.Theme) *tview.Box {
- 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
- 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)
- })
- if border.Enabled {
- box.SetBorders(tview.BordersAll)
- }
- return 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().
- SetColumns(0, width, 0).
- SetRows(0, height, 0).
- AddItem(m, 1, 1, 1, 1, 0, 0, true)
- }
- func ChannelToString(channel discord.Channel, icons config.Icons, state *ningen.State) string {
- var icon string
- switch channel.Type {
- case discord.DirectMessage, discord.GroupDM:
- if channel.Name != "" {
- return channel.Name
- }
- recipients := make([]string, len(channel.DMRecipients))
- for i, r := range channel.DMRecipients {
- if state != nil && channel.Type == discord.DirectMessage {
- if rel, ok := state.RelationshipState.FullRelationship(r.ID); ok && rel.Type == discord.FriendRelationship {
- if rel.Nickname != nil && *rel.Nickname != "" {
- recipients[i] = *rel.Nickname
- continue
- }
- }
- }
- recipients[i] = r.DisplayOrUsername()
- }
- return strings.Join(recipients, ", ")
- case discord.GuildCategory:
- icon = icons.GuildCategory
- case discord.GuildText:
- icon = icons.GuildText
- case discord.GuildVoice:
- icon = icons.GuildVoice
- case discord.GuildStageVoice:
- icon = icons.GuildStageVoice
- case discord.GuildAnnouncementThread:
- icon = icons.GuildAnnouncementThread
- case discord.GuildPublicThread:
- icon = icons.GuildPublicThread
- case discord.GuildPrivateThread:
- icon = icons.GuildPrivateThread
- case discord.GuildAnnouncement:
- icon = icons.GuildAnnouncement
- case discord.GuildForum:
- icon = icons.GuildForum
- case discord.GuildStore:
- icon = icons.GuildStore
- }
- return icon + channel.Name
- }
- func SortGuildChannels(channels []discord.Channel) {
- slices.SortFunc(channels, func(a, b discord.Channel) int {
- return cmp.Compare(a.Position, b.Position)
- })
- }
- func SortPrivateChannels(channels []discord.Channel) {
- slices.SortFunc(channels, func(a, b discord.Channel) int {
- // Descending order
- return cmp.Compare(getMessageIDFromChannel(b), getMessageIDFromChannel(a))
- })
- }
- func getMessageIDFromChannel(channel discord.Channel) discord.MessageID {
- if channel.LastMessageID.IsValid() {
- return channel.LastMessageID
- }
- return discord.MessageID(channel.ID)
- }
- // LinkDisplayText returns a short, human-friendly label for a URL.
- // Known hosts get special treatment; everything else shows host + truncated path.
- func LinkDisplayText(raw string) string {
- parsed, err := url.Parse(raw)
- if err != nil || parsed.Host == "" {
- return raw
- }
- host := strings.ToLower(parsed.Host)
- p := strings.TrimRight(parsed.EscapedPath(), "/")
- segments := strings.Split(strings.TrimLeft(p, "/"), "/")
- // Discord CDN / media — show cleaned filename
- if host == "cdn.discordapp.com" || host == "media.discordapp.net" {
- if base := path.Base(p); base != "" && base != "." && base != "/" {
- return CdnDisplayName(base)
- }
- }
- // Tenor GIFs
- if host == "tenor.com" || strings.HasSuffix(host, ".tenor.com") {
- return "Tenor GIF"
- }
- // Substack: open.substack.com/pub/{author} or {author}.substack.com
- switch {
- case host == "open.substack.com" || host == "substack.com":
- if len(segments) >= 2 && segments[0] == "pub" && segments[1] != "" {
- return "Substack - " + segments[1]
- }
- return "Substack"
- case strings.HasSuffix(host, ".substack.com"):
- return "Substack - " + strings.TrimSuffix(host, ".substack.com")
- }
- // YouTube
- if host == "youtube.com" || host == "www.youtube.com" || host == "m.youtube.com" || host == "youtu.be" {
- return "YouTube"
- }
- // Twitter / X
- if host == "twitter.com" || host == "www.twitter.com" || host == "x.com" || host == "www.x.com" {
- return "X (Twitter)"
- }
- // Reddit
- if host == "reddit.com" || host == "www.reddit.com" || host == "old.reddit.com" {
- if len(segments) >= 2 && segments[0] == "r" {
- return "Reddit - r/" + segments[1]
- }
- return "Reddit"
- }
- // GitHub
- if host == "github.com" || host == "www.github.com" {
- if len(segments) >= 2 && segments[0] != "" && segments[1] != "" {
- return "GitHub - " + segments[0] + "/" + segments[1]
- }
- return "GitHub"
- }
- // Generic fallback: host + truncated path
- switch {
- case p == "", p == "/":
- return parsed.Host
- case len(p) > 48:
- return parsed.Host + p[:45] + "..."
- default:
- return parsed.Host + p
- }
- }
- // CdnDisplayName cleans up Discord CDN filenames for display.
- // Handles URL-encoded URLs used as filenames and UUID filenames.
- func CdnDisplayName(name string) string {
- ext := path.Ext(name)
- stem := strings.TrimSuffix(name, ext)
- // URL-encoded URL as filename (e.g., "https3A2F2F...512x512.png")
- if strings.HasPrefix(stem, "http") && strings.Contains(stem, "2F") {
- return "image" + ext
- }
- // UUID filename (e.g., "c02b97b3-813f-4c03-904a-0bd4240f2c10.jpg")
- if isUUID(stem) {
- return "image" + ext
- }
- // Truncate long filenames
- if len(name) > 40 {
- return name[:37] + "..." + ext
- }
- return name
- }
- func isUUID(s string) bool {
- if len(s) != 36 {
- return false
- }
- for i, c := range s {
- if i == 8 || i == 13 || i == 18 || i == 23 {
- if c != '-' {
- return false
- }
- } else if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
- return false
- }
- }
- return true
- }
- func MergeStyle(base, overlay tcell.Style) tcell.Style {
- fg := overlay.GetForeground()
- if fg == tcell.ColorDefault {
- fg = base.GetForeground()
- }
- bg := overlay.GetBackground()
- if bg == tcell.ColorDefault {
- bg = base.GetBackground()
- }
- style := base.Foreground(fg).Background(bg)
- style = style.Bold(base.HasBold() || overlay.HasBold())
- style = style.Dim(base.HasDim() || overlay.HasDim())
- style = style.Italic(base.HasItalic() || overlay.HasItalic())
- style = style.Blink(base.HasBlink() || overlay.HasBlink())
- style = style.Reverse(base.HasReverse() || overlay.HasReverse())
- style = style.StrikeThrough(base.HasStrikeThrough() || overlay.HasStrikeThrough())
- if base.HasUnderline() || overlay.HasUnderline() {
- style = style.Underline(true)
- }
- return style
- }
|