| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- # 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.
|