CLAUDE.md 9.4 KB

Project Context

What This File Is

This is the persistent context file for Claude Code. Keep it concise and useful.

CLAUDE.md Maintenance Rules

  • Keep this file under 160 lines
  • Only record decisions, not explanations of common knowledge
  • When adding something, check if anything existing is now outdated and remove it
  • Use short bullet points, not paragraphs
  • No boilerplate or filler text
  • If a section grows past 10 items, consolidate or prune the least relevant ones
  • Format: what we chose + why in one line (e.g., "imv over feh — WebP/Wayland support")

Overview

  • Fork of ayn2op/discordo — a Discord TUI client
  • Upstream: https://github.com/ayn2op/discordo.git
  • Our repo: https://gogs.altsol.dev/claude/discordo-plus.git (branch: master)
  • Binary name: discordo-plus (installed to /usr/local/bin/)

Stack

  • Go (module: github.com/ayn2op/discordo)
  • TUI: github.com/ayn2op/tview (custom tview fork) + tcell/v3
  • Discord API: arikawa/v3 + ningen/v3 (state management + Discord markdown)
  • Markdown: goldmark parser + chroma/v2 syntax highlighting
  • Config: TOML via BurntSushi/toml, file at ~/.config/discordo/config.toml
  • Cache: ~/.cache/discordo/ (attachments, logs, state)

Architecture

  • main.gocmd/root.go (app init) → internal/ui/ (TUI layers)
  • internal/ui/chat/ — core chat UI: messages_list.go (rendering/keybinds), attachment_handler.go, embed_renderer.go, url_extractor.go, message_input.go, guilds_tree.go, attachments_picker.go
  • internal/markdown/renderer.go — AST→styled lines, handles Discord markdown flavors
  • internal/config/config.go (struct + loader), config.toml (defaults, embedded), keybinds.go, theme.go, editor.go
  • internal/consts/ — app name, cache dir
  • internal/ui/chat/guildstate.go — persists guild + channel expand/collapse state to ~/.cache/discordo/state.json
  • Rendering pipeline: messages → tview.LineBuilder[]tview.Line (segments with tcell.Style)
  • URLs get style.Url(rawURL) metadata for OSC 8 terminal hyperlinks
  • External commands (editor, image viewer) use app.Suspend() pattern — suspends TUI, runs command, resumes

Key Patterns

  • Keybinds: defined in config/keybinds.go structs, matched in HandleEvent() via keybind.Matches()
  • Help tooltips: ShortHelp() (bottom bar, contextual) and FullHelp() (full overlay via ?)
  • Attachments: downloaded to ~/.cache/discordo/attachments/, opened via configured viewer or openDefault()
  • Embeds: rendered with bar prefix, wrapped to viewport width, markdown in descriptions
  • Config defaults embedded via //go:embed config.toml

Our Changes (vs upstream)

  • Image viewer: image_viewer / image_viewer_args config, save_image keybind (S), mpv geometry auto-detection via xdotool, supported types: jpeg/png/webp/gif
  • Attachments: URL fix (proper NewLine() + .Url() style), o in ShortHelp when attachments/URLs present
  • Help/Config: ? keybind (was ctrl+.), E edits config in $EDITOR/vim from help overlay
  • State persistence: guild + channel expand/collapse state in ~/.cache/discordo/state.json (guildstate.go)
  • Focus/Navigation: Always focus messages + select latest on channel enter; ESC respects hidden guilds panel (falls back to input); arrow keys: up/down same as k/j, left/right cycle between panels (configurable keybinds); vim-style h/l panel nav, i focus input, H toggle guilds, c channels picker, I attach file; input isolation (single-char keybinds blocked while typing)
  • Config auto-create: config path ~/.config/discordo-plus/, auto-creates dir + default config on first run
  • Dynamic input height: message input grows from 3 to 8 lines based on content
  • Security hardening: path traversal prevention, HTTPS-only downloads, bounded downloads (100MB), exec.LookPath validation, atomic writes, restrictive perms, direct exec.Command (no sh -c)
  • Bug fixes: Brotli body leak, cache type assertion panic, MarkRead uses newest fetched message ID
  • Code structure: extracted url_extractor.go, embed_renderer.go, attachment_handler.go from messages_list.go; replaced open-golang with stdlib
  • Reactions display: renders below content (drawReactions), bold for own, real-time gateway updates; E opens emoji picker to add reactions (emoji_picker.go)
  • Search picker: / opens fuzzy search over current channel messages (search_picker.go)
  • Thread indicators: "Thread: name" display, T navigates to thread (was t)
  • User info popup: w shows author info overlay (user_info.go)
  • Command mode: : opens vim-style input, supports :q/:quit/:logout (command_input.go)
  • LRU cache cap: itemByID evicts stale entries past 500 items
  • Reply quote italic: dim + italic style for reply lines; Z toggles reply collapse (shows > marker only)
  • Timestamp toggle: t toggles timestamps on/off at runtime
  • Wrap indentation: continuation lines get 2-space indent for visual clarity
  • Picker browse mode: ESC in overlay pickers enters browse mode (j/k/g/G/i) via pickerBrowseHandleKey
  • Link display compression: ui.LinkDisplayText() shows human-friendly labels instead of raw URLs in chat and embeds; ui.CdnDisplayName() cleans attachment filenames (encoded URLs → image.ext, UUIDs → image.ext); link preview embeds suppressed when URL already in message content (isLinkPreviewEmbed); removed show_attachment_links config (attachments always show as OSC 8 clickable filenames)

Adding Link Display Rules

To add a new site-specific URL label, edit ui.LinkDisplayText() in internal/ui/util.go:

  1. Add a host match (host == "example.com" or strings.HasSuffix(host, ".example.com")) after the existing site blocks
  2. Use segments (pre-split path segments) to extract meaningful parts (e.g., segments[1] for subreddit in /r/{sub})
  3. Return a short label string (e.g., "SiteName - " + segments[1])
  4. The raw URL is preserved as OSC 8 hyperlink metadata — only the visible text changes
  5. CDN filenames also cleaned via ui.CdnDisplayName() — encoded URLs and UUIDs become image.ext, long names truncated
  6. Link preview embeds (no description/fields/footer) are skipped when the URL is already in message content
  7. Currently handled: Discord CDN (filename), Tenor (GIF), Substack (author), YouTube, X/Twitter, Reddit (subreddit), GitHub (owner/repo)
  8. Fallback: host + truncated path (max 48 chars)

Config Fields We Added

  • image_viewer — external image viewer command (default: "mpv", "default" = system opener)
  • image_viewer_args — explicit viewer args list, overrides auto-detection (default: [], Wayland-friendly)
  • image_save_dir — directory for saved images (supports ~/, default: current dir)
  • keybinds.messages_list.save_image — save image keybind (default: S)
  • keybinds.edit_config — open config in editor from help overlay (default: E)
  • keybinds.toggle_help — changed default from ctrl+. to ?
  • keybinds.messages_list.search — fuzzy search messages (default: /)
  • keybinds.messages_list.open_thread — navigate to message's thread (default: T, was t)
  • keybinds.messages_list.user_info — show author info popup (default: w)
  • keybinds.command_mode — open vim-style command input (default: :)
  • keybinds.guilds_tree.arrow_* / keybinds.messages_list.arrow_* — arrow key navigation (default: up/down/left/right)
  • keybinds.focus_previous / keybinds.focus_next — vim-style panel nav (default: h/l)
  • keybinds.focus_message_input — focus input (default: i)
  • keybinds.toggle_guilds_tree — toggle sidebar (default: H)
  • keybinds.toggle_channels_picker — channels picker (default: c)
  • keybinds.attach_file — global attach file keybind (default: I)
  • keybinds.messages_list.reply / reply_mention — swapped: r = reply (no mention), R = @reply
  • keybinds.guilds_tree.yank_id / keybinds.messages_list.yank_id — copy ID (default: C, was i)
  • keybinds.messages_list.toggle_timestamps — runtime timestamp toggle (default: t)
  • keybinds.messages_list.toggle_replies — collapse/expand reply quotes (default: Z)
  • keybinds.messages_list.add_reaction — open emoji picker (default: E)

Build & Run

  • Build: go build -o discordo-plus .
  • Install: sudo mv discordo-plus /usr/local/bin/
  • Arch Linux: makepkg -si (uses PKGBUILD, installs via pacman)
  • Run: discordo-plus
  • Test: go test ./... (only config + keyring packages have tests)
  • Dependencies: mpv (or configured image viewer), xdotool (optional, X11 geometry detection), xdg-open (Linux, for default opener)

Git

  • Identity: claude <claude@altsol.dev>
  • Commit style: type(scope): description (e.g., feat(ui/chat): add image viewer)
  • Branch: master

Audit Status

  • Research audits in ./research/: SECFILE.md, COMPLIANCE.md, TECHFILE.md
  • Resolved all low-risk findings (marked ✅ FIXED in research files)
  • Remaining unfixed: SEC #7 (raw events in debug), COMP #19-20 (perf), COMP #22/24 (linter/tests), TECH #2-3 (unmaintained deps)

Known Issues

  • Discord ToS discourages third-party clients — use at own risk
  • xdotool geometry detection only works on X11; on Wayland use image_viewer_args or compositor window rules for mpv positioning
  • viewerArgs() only adds special flags for mpv; other viewers get plain viewer path invocation (use image_viewer_args to customize)