# 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](https://github.com/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.go` → `cmd/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`); picker defaults to browse/scroll mode; `f` toggles favorites (★, up to 10, persisted to `~/.cache/discordo/emoji_favorites.json`) - **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`; global single-char keybinds suppressed while picker is open (fixes `i` going to message input) - **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 - CDN filenames also cleaned via `ui.CdnDisplayName()` — encoded URLs and UUIDs become `image.ext`, long names truncated - Link preview embeds (no description/fields/footer) are skipped when the URL is already in message content - Currently handled: Discord CDN (filename), Tenor (GIF), Substack (author), YouTube, X/Twitter, Reddit (subreddit), GitHub (owner/repo) - 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 ` - 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)