# Image Viewing & Save Feature — Implementation Plan ## Overview Add lightweight image viewing and saving for image attachments (jpg, jpeg, png, webp, gif) in discordo. Uses an external viewer (`imv` by default, configurable) that can be dismissed with `q`. Adds a new keybind to save the highlighted message's image attachment to a user-configured directory. --- ## Bug Fix (prerequisite) ### 1. Fix broken attachment link rendering **File:** `internal/ui/chat/messages_list.go` ~line 500 **Problem:** Attachment URLs are rendered with a literal `\n` inside a single `builder.Write()` call: ```go builder.Write(a.Filename+":\n"+a.URL, attachmentStyle) ``` The `\n` is embedded in the segment text but `tview.LineBuilder` does not split on `\n` within a `Write()` call — the newline character ends up treated as inline content, corrupting the URL display and breaking the link. **Fix:** Split into two writes separated by `builder.NewLine()`: ```go builder.Write(a.Filename+":", attachmentStyle) builder.NewLine() builder.Write(a.URL, attachmentStyle.Url(a.URL)) ``` This also applies the `.Url()` style metadata so the link is properly clickable in terminals that support OSC 8 hyperlinks. --- ## Feature Changes ### 2. Add config fields for image viewer and save directory **File:** `internal/config/config.go` Add two new fields to the `Config` struct: ```go ImageViewer string `toml:"image_viewer"` // e.g. "imv", "feh", "sxiv" ImageSaveDir string `toml:"image_save_dir"` // e.g. "~/Pictures/discordo" ``` **File:** `internal/config/config.toml` Add defaults: ```toml # Program used to view image attachments. Must accept a file path as its # first argument. Set to "default" to use xdg-open / open. image_viewer = "imv" # Directory where images are saved with the save_image keybind. # Supports ~ for home directory. Leave empty to use the current directory. image_save_dir = "" ``` **File:** `internal/config/config.go` — `applyDefaults()` - Expand `~` in `ImageSaveDir` to the user's home directory. - If `ImageViewer` is `"default"` or empty, fall back to `open.Start()` behavior (system default). ### 3. Add `save_image` keybind **File:** `internal/config/keybinds.go` Add to `MessagesListKeybinds`: ```go SaveImage Keybind `toml:"save_image"` ``` Default key: `S` (capital S — currently unused in messages list). **File:** `internal/config/config.toml` ```toml # Save the selected message's image attachment to image_save_dir. save_image = "S" ``` ### 4. Change `openAttachment()` to use configured image viewer **File:** `internal/ui/chat/messages_list.go` Modify `openAttachment()` (line 1183): - After downloading the attachment to cache (existing logic), instead of calling `open.Start(path)`, check `ml.cfg.ImageViewer`: - If set and not `"default"`: exec the viewer with the cached file path as arg, e.g. `exec.Command(ml.cfg.ImageViewer, path).Run()`. - If `"default"` or empty: fall back to `open.Start(path)` (current behavior). - The viewer command runs in a goroutine. While the viewer is open, discordo's TUI is suspended (same pattern as the external editor: `ml.chatView.app.Suspend()` + resume after the command exits). This lets terminal-based viewers like `imv` take over the terminal cleanly. Affected types: jpg, jpeg, png, webp, gif — filter on `attachment.ContentType` matching `image/jpeg`, `image/png`, `image/webp`, `image/gif`. For non-image attachments, keep existing `open.Start(url)` behavior. ### 5. Implement `saveImage()` function and wire up keybind **File:** `internal/ui/chat/messages_list.go` New function `saveImage()`: ``` func (ml *messagesList) saveImage() ``` Logic: 1. Get the selected message via `ml.selectedMessage()`. 2. Filter `msg.Attachments` to only image types (jpg, jpeg, png, webp, gif) by checking `attachment.ContentType`. 3. If no image attachments, return early (no-op). 4. If one image: download and save directly. 5. If multiple images: show the attachments picker (reuse existing `showAttachmentsList` pattern but with save actions instead of open actions). 6. Download the image via HTTP (reuse the download logic from `openAttachment` — extract into a shared helper `downloadAttachment()` that returns the local file path). 7. Copy the file from cache to `ml.cfg.ImageSaveDir/filename`. If `ImageSaveDir` is empty, save to the current working directory. 8. Log success/failure via `slog`. **File:** `internal/ui/chat/messages_list.go` — keybind registration In the function that registers messages list keybinds (look for where `Open` keybind is registered), add the `SaveImage` keybind pointing to `ml.saveImage`. ### 6. Extract shared download helper **File:** `internal/ui/chat/messages_list.go` Refactor the HTTP download + cache write logic currently in `openAttachment()` into: ```go func (ml *messagesList) downloadToCache(attachment discord.Attachment) (string, error) ``` Returns the local cached file path. Used by both `openAttachment()` and `saveImage()`. --- ## File Change Summary | File | Changes | |------|---------| | `internal/ui/chat/messages_list.go` | Fix attachment link rendering; refactor download logic; modify `openAttachment()` to use configured viewer with app suspend/resume; add `saveImage()`; register new keybind | | `internal/config/config.go` | Add `ImageViewer` and `ImageSaveDir` fields; expand `~` in defaults | | `internal/config/config.toml` | Add `image_viewer`, `image_save_dir`, and `save_image` keybind defaults | | `internal/config/keybinds.go` | Add `SaveImage` keybind to `MessagesListKeybinds` and default | --- ## Implementation Order 1. Fix the `\n` attachment link bug (quick, independent) 2. Add config fields + defaults (`config.go`, `config.toml`) 3. Add `SaveImage` keybind (`keybinds.go`, `config.toml`) 4. Extract `downloadToCache()` helper from `openAttachment()` 5. Modify `openAttachment()` to use configured viewer + suspend/resume 6. Implement `saveImage()` and wire up keybind --- ## Notes - `imv` is chosen as the default viewer because it's lightweight, supports Wayland and X11, handles all target formats (jpg, png, webp, gif) out of the box via FreeImage/libnsgif, and exits on `q` by default. - Users who prefer X11-only can set `image_viewer = "feh"` (note: `feh` lacks WebP support on many distros). - Users on macOS can set `image_viewer = "open"` or `image_viewer = "default"`. - The suspend/resume pattern for the viewer follows the same approach already used for the external editor (see `message_input.go` `openEditor` flow). - No new Go dependencies are needed — `os/exec` is in the stdlib and HTTP download logic already exists.