Skip to content

feat(cclip): integrate ratatui-image for native TUI image previews#24

Merged
Mjoyufull merged 17 commits intodevfrom
fix/cclip
Feb 23, 2026
Merged

feat(cclip): integrate ratatui-image for native TUI image previews#24
Mjoyufull merged 17 commits intodevfrom
fix/cclip

Conversation

@Mjoyufull
Copy link
Owner

Summary

Replace manual chafa subprocess logic with ratatui-image's widget-based rendering for clipboard image previews. Images are now rendered natively in the TUI using automatic terminal protocol detection (Kitty, Sixel, Halfblocks) instead of shelling out to chafa.

  • I did basic linting
  • I'm a clown who can't code 🤡

Changes

  • Added ratatui-image 10.0 and image 0.25 dependencies (without image-defaults to keep the dep tree lean)
  • Introduced ImageManager struct in ui/graphics.rs for centralized image loading and rendering via StatefulImage widget
  • Refactored cclip inline image preview to render within the ratatui draw loop
  • Implemented fullscreen image preview as a native ratatui modal loop (Alt+i)
  • Removed all chafa subprocess logic and check_chafa_available() from preview.rs
  • Removed base64 crate dependency (was only used for manual Kitty protocol encoding)
  • Removed duplicate get_cclip_rowid definition in dmenu_ui.rs
  • Replaced legacy image_hide() call with ImageManager::clear()
  • Fixed ratatui 0.30 constraints API compatibility (.as_ref() removal)
  • Streamlined clipboard copy to use cclip copy <rowid> directly

Testing

  1. Build with cargo build --release
  2. Run fsel --cclip in a Kitty/Foot/WezTerm terminal
  3. Navigate to an image entry and verify inline preview renders
  4. Press Alt+i to verify fullscreen image preview
  5. Press Enter on an image to verify clipboard copy works
  6. Press Escape/q to exit fullscreen preview
  7. Navigate away from image entries and verify cleanup (no ghost images)
  8. ngl you can just feel it tbh

Breaking Changes

  • chafa is no longer used or needed for image previews
  • base64 crate removed from dependencies

Related Issues

None

…age detection

get_cclip_rowid returns Some for ALL cclip entries (text and images),
causing chafa to behave strangly
Replace manual chafa subprocess logic with ratatui-image's automatic
protocol detection and widget-based rendering for clipboard image
previews (inline and fullscreen via Alt+i).

Changes:
- Add ratatui-image 10.0 and image 0.25 dependencies
- Introduce ImageManager in ui/graphics.rs for centralized image
  loading and rendering via StatefulImage widget
- Refactor cclip inline preview to render images within ratatui draw loop
- Implement fullscreen image preview as native ratatui modal (Alt+i)
- Remove all chafa subprocess logic and check_chafa_available()
- Remove base64 crate (was only used for manual Kitty encoding)
- Remove duplicate get_cclip_rowid definition in dmenu_ui.rs
- Replace legacy image_hide() with ImageManager::clear()
-Fix ratatui 0.30 constraints API (.as_ref() removal)
- Streamline clipboard copy to use cclip copy <rowid> directly
- Drop image-defaults feature to avoid pulling rav1e/ravif bloat
ngl i also think this is kinda useless but im lowkey too tired for all this bs fr
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces chafa/sixel checks with a ratatui-image-based ImageManager, adds async image loading/rendering (inline and fullscreen) with Loading/Image/Failed DisplayState, makes image helpers public in the UI, simplifies Wayland clipboard flow, and removes the chafa availability check.

Changes

Cohort / File(s) Summary
Dependency Management
Cargo.toml
Removed base64 = "0.22"; added ratatui-image = { version = "10.0", default-features = false, features = ["crossterm", "tokio"] } and image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "bmp", "webp"] }.
Graphics & Image Manager
src/ui/graphics.rs, src/ui/mod.rs
Introduced public ImageManager (including pub fn new(picker)), async load/render APIs, cache management, expanded DisplayState (Loading/Image/Failed), DISPLAY_STATE moved to tokio::sync::Mutex, and GraphicsAdapter::detect(picker: Option<&Picker>) signature. ImageManager re-exported.
Cclip runtime integration
src/modes/cclip/run.rs, src/modes/cclip/preview.rs
Initialized and used ImageManager in the cclip run loop for per-item async loading, tracked failed rowids, dynamic graphics capability detection, inline rendering via ImageManager::render, fullscreen preview loop, and removed pub fn check_chafa_available() from preview.rs.
Wayland clipboard simplification
src/modes/cclip/select.rs
Replaced two-process pipeline (cclip + wl-copy piping) with single cclip copy <rowid> invocation and simplified error handling.
Dmenu UI changes
src/ui/dmenu_ui.rs
Made is_cclip_image_item() and get_image_info() public; replaced legacy terminal-image code with a status-driven preview header using DISPLAY_STATE; simplified rowid extraction and removed terminal-fullscreen rendering path.
Picker/Detection & Layout tweaks
src/modes/dmenu/run.rs
Added optional Picker pre-detection and passed it into GraphicsAdapter::detect(picker.as_ref()); simplified certain layout constraint constructions.
Render integration across UI
src/modes/..., src/ui/...
Integrated image rendering into main draw loop, added per-iteration load coordination and UI temp messages for init/load errors, cleared image state on tag-mode transitions, and removed many Crossterm positioning/erasure paths in favor of ratatui-image flows.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as CclipRun
    participant Picker
    participant ImageMgr as ImageManager
    participant Sys as SystemCmd
    participant Renderer as RatatuiFrame

    User->>App: navigate/select item
    App->>Picker: optional detect graphics
    App->>ImageMgr: spawn load_cclip_image(rowid)
    ImageMgr->>Sys: run `cclip get <rowid>`
    Sys-->>ImageMgr: image bytes
    ImageMgr->>ImageMgr: decode & init StatefulProtocol
    ImageMgr-->>App: success/failure
    App->>DISPLAY_STATE: set Loading/Image/Failed
    App->>Renderer: draw UI
    Renderer->>ImageMgr: render(frame, image_area)
    ImageMgr-->>Renderer: draw StatefulImage
    Renderer-->>User: display inline image

    User->>App: press fullscreen key
    App->>Renderer: enter fullscreen loop
    Renderer->>ImageMgr: render(frame, full_area)
    ImageMgr-->>Renderer: draw fullscreen image
    Renderer-->>User: show fullscreen
    User->>App: press Esc/q/Ctrl-C
    App->>ImageMgr: clear()
    App-->>User: exit fullscreen
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I nudged chafa from the trail,
Brought async hops and a pixel veil.
Preview loads with Loading, Failed, or Glow,
Fullscreen leaps — the images show.
I twitch my whiskers — onward we go!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately describes the main change: integrating ratatui-image for native TUI image previews in cclip mode.
Description check ✅ Passed The description is comprehensive and clearly related to the changeset, explaining the motivation, changes made, testing steps, and breaking changes.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/cclip

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/modes/cclip/run.rs`:
- Around line 855-877: The loop that calls terminal.draw and checks input.next()
(inside the block using manager.render) can hang if input.next() keeps returning
Err; modify the loop to handle Err from input.next() by breaking or retrying
with a bounded backoff/timeout: detect Err from input.next() and either break
out to the cleanup block (so terminal.clear() and resetting DISPLAY_STATE still
run) or count consecutive Errs and break after a threshold; ensure
previous_was_image and crate::ui::DISPLAY_STATE::Empty are still set in the
cleanup path so the state is consistent.

In `@src/ui/dmenu_ui.rs`:
- Around line 697-705: The conditional parts.len() >= 1 is redundant because
splitn(2, '\t') on a non-empty original_line always yields at least one element;
simplify by removing that check and directly use parts[0] after collecting parts
from original_line.splitn(2, '\t'), keeping the existing trim and digit
validation for rowid (the variables and logic around original_line, parts, rowid
and the is_ascii_digit() check should remain unchanged).
🧹 Nitpick comments (5)
src/ui/graphics.rs (2)

64-89: Consider adding timeout or size limit for subprocess and image loading.

The load_cclip_image method spawns a subprocess and reads all output into memory without any size limit or timeout. For large images or a hung cclip process, this could cause issues.

💡 Suggested improvements
  1. Consider adding a timeout for the subprocess
  2. Consider limiting the maximum image size to prevent OOM on very large images
     pub fn load_cclip_image(&mut self, rowid: &str) -> Result<()> {
+        const MAX_IMAGE_SIZE: usize = 50 * 1024 * 1024; // 50MB limit
+        
         // Run cclip get to fetch image bytes
         let mut child = Command::new("cclip")
             .args(["get", rowid])
             .stdout(Stdio::piped())
             .stderr(Stdio::null())
             .spawn()?;
 
         let mut bytes = Vec::new();
         if let Some(mut stdout) = child.stdout.take() {
-            stdout.read_to_end(&mut bytes)?;
+            stdout.take(MAX_IMAGE_SIZE as u64).read_to_end(&mut bytes)?;
         }
         child.wait()?;

42-49: LGTM - Consider simplifying the match expression.

The logic correctly identifies high-resolution graphics support. Minor style suggestion:

✨ Simpler pattern
     pub fn supports_graphics(&self) -> bool {
         use ratatui_image::picker::ProtocolType;
-        match self.picker.protocol_type() {
-            ProtocolType::Halfblocks => false,
-            _ => true,
-        }
+        !matches!(self.picker.protocol_type(), ProtocolType::Halfblocks)
     }
src/modes/cclip/run.rs (3)

216-218: Consider preserving error context from ImageManager initialization.

Using .ok() silently discards the error. While the warning at lines 254-257 helps, the actual error details are lost. Consider logging the error for debugging.

💡 Preserve error details
     // Initialize ImageManager for ratatui-image
-    let mut image_manager = crate::ui::ImageManager::new().ok();
+    let mut image_manager = match crate::ui::ImageManager::new() {
+        Ok(manager) => Some(manager),
+        Err(e) => {
+            // Debug logging could help diagnose terminal detection issues
+            if cli.verbose.unwrap_or(0) >= 2 {
+                eprintln!("debug: ImageManager init failed: {}", e);
+            }
+            None
+        }
+    };

421-430: Silent failure on image load could leave UI in inconsistent state.

If load_cclip_image fails (line 425), the error is silently ignored but the DISPLAY_STATE is still updated to Image. This could show stale content or nothing.

💡 Handle load failure explicitly
             if current_is_image {
                 if let (Some(rowid), Some(manager)) = (&current_rowid_opt, &mut image_manager) {
                     if displayed_rowid_opt.as_deref() != Some(rowid) {
                         // Load the new image
-                        if manager.load_cclip_image(rowid).is_ok() {
-                            if let Ok(mut state) = crate::ui::DISPLAY_STATE.lock() {
-                                *state = crate::ui::DisplayState::Image(ratatui::layout::Rect::default(), rowid.clone());
-                            }
+                        match manager.load_cclip_image(rowid) {
+                            Ok(()) => {
+                                if let Ok(mut state) = crate::ui::DISPLAY_STATE.lock() {
+                                    *state = crate::ui::DisplayState::Image(ratatui::layout::Rect::default(), rowid.clone());
+                                }
+                            }
+                            Err(_) => {
+                                // Clear state on load failure to avoid showing stale image
+                                manager.clear();
+                                if let Ok(mut state) = crate::ui::DISPLAY_STATE.lock() {
+                                    *state = crate::ui::DisplayState::Empty;
+                                }
+                            }
                         }
                     }
                 }

316-318: Minor formatting artifact.

There appears to be trailing whitespace after the constraint array closing bracket. This is a minor formatting issue.

@Mjoyufull
Copy link
Owner Author

Pr is weird asf twin ngl to you, i don't think a 5x increase in the binary works mis @Mjoyufull maybe next time

@Mjoyufull Mjoyufull closed this Feb 17, 2026
@Mjoyufull Mjoyufull deleted the fix/cclip branch February 17, 2026 09:07
@Mjoyufull Mjoyufull restored the fix/cclip branch February 17, 2026 09:10
@Mjoyufull Mjoyufull reopened this Feb 17, 2026
@Mjoyufull Mjoyufull closed this Feb 18, 2026
@Mjoyufull Mjoyufull deleted the fix/cclip branch February 18, 2026 11:15
@Mjoyufull Mjoyufull restored the fix/cclip branch February 19, 2026 23:36
@Mjoyufull Mjoyufull reopened this Feb 19, 2026
@Mjoyufull
Copy link
Owner Author

idk how to feel about this tbh

- Add bounded error tolerance (3 consecutive) to fullscreen image
  preview loop to prevent hangs from persistent input.next() failures
- Log ImageManager initialization errors instead of silently discarding
- Clear stale image state on load_cclip_image failure
- Remove redundant parts.len() >= 1 check in get_cclip_rowid
- Simplify supports_graphics() match to !matches!() expression
- Fix trailing whitespace in layout constraint block
- cargo fmt
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/modes/cclip/run.rs (2)

216-223: eprintln! during alternate screen may flash briefly.

At this point the terminal backend is on stderr, so eprintln! writes directly to the alternate screen buffer. The next terminal.draw() will overwrite it, but it could flash for one frame. Consider using log::warn! or deferring the warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 216 - 223, Replace the direct stderr
write (eprintln!) in the ImageManager initialization with a logging call so the
warning doesn't flash on the alternate screen; specifically update the
ImageManager::new() match in run.rs (the initialization that sets image_manager)
to call log::warn! (or another configured logger) with the error details instead
of eprintln!, ensuring you keep the same message context and include the error
variable `e` so the warning is captured by the configured logger rather than
written to stderr.

1386-1403: Unify mouse-click copy operation with CclipItem::copy_to_clipboard.

The Enter/Ctrl+Y handler cleanly delegates to CclipItem::copy_to_clipboard, but the mouse-click handler still manually constructs the cclip getwl-copy/xclip piping. Refactor the mouse-click path to use CclipItem::from_line() and copy_to_clipboard() to eliminate duplication and improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 1386 - 1403, The mouse-click handler
currently duplicates the manual `cclip get` → `wl-copy`/`xclip` piping; refactor
it to parse the clicked line with
super::CclipItem::from_line(original_line.clone()) and call the existing
CclipItem::copy_to_clipboard() (same as the Enter/Ctrl+Y path), handling Err by
calling ui.set_temp_message(format!("Copy failed: {}", e)) and continuing; on
success perform the same terminal cleanup sequence
(terminal.show_cursor().wrap_err(...)?; drop(terminal); execute
DisableMouseCapture and LeaveAlternateScreen on stderr; disable_raw_mode();
return Ok(())). Use the same error handling and flow as in the Enter/Ctrl+Y
branch to eliminate duplication.
src/ui/dmenu_ui.rs (1)

678-690: Consider using splitn for consistency with other parsing methods.

get_image_info uses split('\t') while sibling methods (is_cclip_image_item, get_cclip_content_for_display, get_cclip_rowid) consistently use splitn. This works correctly but is a minor inconsistency.

♻️ Optional: align with existing parsing style
     pub fn get_image_info(&self, item: &crate::common::Item) -> String {
         if !self.is_cclip_image_item(item) {
             return String::new();
         }
 
-        let parts: Vec<&str> = item.original_line.split('\t').collect();
+        let parts: Vec<&str> = item.original_line.splitn(4, '\t').collect();
         if parts.len() >= 3 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/dmenu_ui.rs` around lines 678 - 690, get_image_info currently uses
item.original_line.split('\t') which is inconsistent with sibling parsers;
change it to use splitn(3, '\t') so it behaves like is_cclip_image_item,
get_cclip_content_for_display, and get_cclip_rowid, then collect/check the
resulting parts as before (ensure you still read the third part when parts.len()
>= 3 and return "Unknown Image" otherwise).
src/ui/graphics.rs (1)

105-127: Dual graphics detection: GraphicsAdapter::detect() vs ImageManager picker.

GraphicsAdapter::detect() uses env-var heuristics while ImageManager uses actual terminal protocol negotiation (Picker::from_query_stdio). They can disagree — e.g., a Sixel-capable terminal not matching the env-var check would get GraphicsAdapter::None, skipping needed Clear widget calls.

Currently GraphicsAdapter is only used in dmenu/run.rs (which has no image rendering), so this is low-risk, but worth noting for future maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 105 - 127, GraphicsAdapter::detect()
currently uses environment-variable heuristics and can disagree with the runtime
protocol negotiation used by ImageManager (Picker::from_query_stdio), causing
mismatches (e.g., Sixel capable terminals being classified as None and skipping
Clear widget calls); update GraphicsAdapter::detect() to prefer actual protocol
negotiation by calling the same detection logic as ImageManager (use
Picker::from_query_stdio or a shared helper) and map its result to
GraphicsAdapter variants, ensuring codepaths that rely on GraphicsAdapter (e.g.,
dmenu/run.rs) get the same capability decision as ImageManager so Clear and
other image-related widgets are executed consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ui/graphics.rs`:
- Around line 60-86: load_cclip_image currently spawns a blocking
std::process::Command and reads stdout synchronously which can hang the TUI;
change load_cclip_image to an async implementation (or run it on a dedicated
blocking thread) and use tokio timeouts to bound how long the subprocess can
block: replace std::process::Command with tokio::process::Command (or
spawn_blocking) and wrap the child execution + stdout read in
tokio::time::timeout(), ensuring you kill the child on timeout
(child.kill()/kill().await) and propagate a descriptive error; update callers
(e.g., the code in cclip/run.rs) to await the new async load_cclip_image
signature or spawn the blocking wrapper so the event loop is never blocked, and
keep the existing flow of decoding with image::load_from_memory and assigning
self.protocol via self.picker.new_resize_protocol(dyn_img).

---

Duplicate comments:
In `@src/ui/dmenu_ui.rs`:
- Around line 694-705: Update the get_cclip_rowid logic to rely on the existing
empty-string guard and safely use splitn(2, '\t') without the prior redundant
parts-length check: keep the early return if original_line.is_empty(), call
original_line.splitn(2, '\t') and use parts[0].trim(), then validate that the
trimmed rowid is all ASCII digits and return Some(rowid.to_string()) or None
otherwise (ensure the function name get_cclip_rowid and variables
original_line/parts/rowid are the points you modify).

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 216-223: Replace the direct stderr write (eprintln!) in the
ImageManager initialization with a logging call so the warning doesn't flash on
the alternate screen; specifically update the ImageManager::new() match in
run.rs (the initialization that sets image_manager) to call log::warn! (or
another configured logger) with the error details instead of eprintln!, ensuring
you keep the same message context and include the error variable `e` so the
warning is captured by the configured logger rather than written to stderr.
- Around line 1386-1403: The mouse-click handler currently duplicates the manual
`cclip get` → `wl-copy`/`xclip` piping; refactor it to parse the clicked line
with super::CclipItem::from_line(original_line.clone()) and call the existing
CclipItem::copy_to_clipboard() (same as the Enter/Ctrl+Y path), handling Err by
calling ui.set_temp_message(format!("Copy failed: {}", e)) and continuing; on
success perform the same terminal cleanup sequence
(terminal.show_cursor().wrap_err(...)?; drop(terminal); execute
DisableMouseCapture and LeaveAlternateScreen on stderr; disable_raw_mode();
return Ok(())). Use the same error handling and flow as in the Enter/Ctrl+Y
branch to eliminate duplication.

In `@src/ui/dmenu_ui.rs`:
- Around line 678-690: get_image_info currently uses
item.original_line.split('\t') which is inconsistent with sibling parsers;
change it to use splitn(3, '\t') so it behaves like is_cclip_image_item,
get_cclip_content_for_display, and get_cclip_rowid, then collect/check the
resulting parts as before (ensure you still read the third part when parts.len()
>= 3 and return "Unknown Image" otherwise).

In `@src/ui/graphics.rs`:
- Around line 105-127: GraphicsAdapter::detect() currently uses
environment-variable heuristics and can disagree with the runtime protocol
negotiation used by ImageManager (Picker::from_query_stdio), causing mismatches
(e.g., Sixel capable terminals being classified as None and skipping Clear
widget calls); update GraphicsAdapter::detect() to prefer actual protocol
negotiation by calling the same detection logic as ImageManager (use
Picker::from_query_stdio or a shared helper) and map its result to
GraphicsAdapter variants, ensuring codepaths that rely on GraphicsAdapter (e.g.,
dmenu/run.rs) get the same capability decision as ImageManager so Clear and
other image-related widgets are executed consistently.

- `src/ui/graphics.rs`: Refactor `load_cclip_image` to an async implementation
  using `tokio::process::Command` and `tokio::time::timeout` bounded to 1.5s to
  prevent the event loop from blocking indefinitely when cclip read hangs.
  Child processes are cleanly killed on timeout.
- `src/ui/graphics.rs`: Update `GraphicsAdapter::detect` to prioritize actual
  protocol capability negotiation via `ratatui_image::picker::Picker`, properly
  respecting genuine capabilities like Sixel correctly.
- `src/modes/cclip/run.rs`: Standardize the clipboard copy interaction on mouse
  clicks by using `CclipItem::from_line` instead of duplicate raw shell scripting.
- `src/ui/dmenu_ui.rs`: Simplify array length safety limits and checks in
  `get_cclip_rowid` and `get_image_info` parsers to consistently invoke
  `splitn`.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modes/cclip/run.rs (1)

929-955: ⚠️ Potential issue | 🟡 Minor

Missing bounds check before indexing ui.shown — potential panic.

The guard if !ui.shown.is_empty() only confirms the slice is non-empty, not that selected_idx < ui.shown.len(). If the selection index is ever inconsistent (e.g., set by one code path and not adjusted after a filter), ui.shown[selected_idx] will panic. The delete handler at the same level correctly uses if selected < ui.shown.len().

🔧 Proposed fix
     if let Some(selected_idx) = ui.selected {
-        if !ui.shown.is_empty() {
+        if selected_idx < ui.shown.len() {
             let item = &ui.shown[selected_idx];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 929 - 955, The code indexes ui.shown
with selected_idx without ensuring selected_idx < ui.shown.len(), which can
panic if the selection is stale; update the condition to check both
!ui.shown.is_empty() and selected_idx < ui.shown.len() (mirroring the delete
handler) before accessing ui.shown[selected_idx], and if out of bounds treat it
like no selection (set ui.tag_mode to RemovingTag with selected: None or skip
setting from CclipItem); use the same symbols shown (ui.selected, ui.shown,
selected_idx, TagMode::RemovingTag, super::CclipItem::from_line) to locate and
fix the guard.
🧹 Nitpick comments (4)
src/ui/graphics.rs (1)

42-58: ProtocolType import is repeated in every method — move to module level.

The use ratatui_image::picker::ProtocolType; statement is duplicated across supports_graphics, is_kitty, and is_sixel. A single top-level use would be cleaner.

♻️ Proposed refactor

Add at the top of the file alongside other imports:

+use ratatui_image::picker::ProtocolType;

Then remove the three inner use statements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 42 - 58, Move the repeated import into the
module scope: add a single top-level `use ratatui_image::picker::ProtocolType;`
alongside other imports, then remove the three inner `use` lines inside the
methods `supports_graphics`, `is_kitty`, and `is_sixel`; ensure the methods
continue to call `self.picker.protocol_type()` and pattern-match against
`ProtocolType::Halfblocks`, `ProtocolType::Kitty`, and `ProtocolType::Sixel`
respectively.
src/modes/cclip/run.rs (3)

313-343: Pre-loop layout calculation is stale after a terminal resize.

Lines 352–362 call ui.info_with_image_support using content_panel_width/height derived from the terminal size at startup. The draw closure (Lines 582–596) recalculates this correctly every frame, so the mismatch only affects the very first draw. If that initial sizing diverges (e.g., the user starts in a small window), the first-rendered text wrap width will be wrong until the next key/mouse event triggers a redraw.

Consider moving the initial ui.info_with_image_support call inside the loop (or removing it entirely, since the draw loop calls it unconditionally), rather than duplicating the layout math outside.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 313 - 343, The initial pre-loop layout
uses content_panel_width/content_panel_height (calculated before entering the
main draw loop) and calls ui.info_with_image_support with those stale sizes,
causing the first frame to render with incorrect wrapping after a terminal
resize; remove that pre-loop call or move the ui.info_with_image_support
invocation into the same per-frame draw closure (the closure around draw that
already recalculates layout every frame) so that ui.info_with_image_support
always receives the up-to-date content_panel_width/content_panel_height used by
the draw closure; locate references to ui.info_with_image_support and the
pre-loop content_panel_* calculations and eliminate the duplicate/out-of-date
call so only the per-frame draw path performs the layout and rendering.

404-424: DisplayState::Image(Rect, String) — the Rect field is unused.

The Rect is always set to Rect::default() (Line 422) and always destructured as _ when read (Line 406). It carries no runtime information. Simplify to DisplayState::Image(String) to reduce confusion.

♻️ Proposed refactor

In src/ui/graphics.rs:

 pub enum DisplayState {
     Empty,
-    Image(Rect, String),
+    Image(String),
 }

In src/modes/cclip/run.rs:

-    crate::ui::DisplayState::Image(_, displayed_rowid) => Some(displayed_rowid.clone()),
+    crate::ui::DisplayState::Image(displayed_rowid) => Some(displayed_rowid.clone()),
...
-    *state = crate::ui::DisplayState::Image(ratatui::layout::Rect::default(), rowid.to_string());
+    *state = crate::ui::DisplayState::Image(rowid.to_string());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 404 - 424, The DisplayState::Image
variant currently carries an unused Rect and should be simplified to hold only a
String: update the enum definition (in src/ui/graphics.rs) to change
DisplayState::Image(Rect, String) -> DisplayState::Image(String), then update
all places that construct or pattern-match this variant—e.g. in this file adjust
the lock/match on crate::ui::DISPLAY_STATE (replace destructuring that ignores
Rect with just the String), and update the site that sets the state after
loading an image (replace DisplayState::Image(ratatui::layout::Rect::default(),
rowid.to_string()) with DisplayState::Image(rowid.to_string())). Also search for
other uses of DisplayState::Image and adapt them to the new single-String
variant to keep types consistent (e.g., any code that previously expected a Rect
should be removed or refactored).

459-466: term_is_foot re-evaluated on every iteration — move outside the loop.

std::env::var("TERM") never changes at runtime. Computing it inside the loop is harmless but wasteful. The check at Lines 837-841 (synchronized-updates teardown) uses the same variable and also benefits from hoisting it.

♻️ Proposed refactor
+    let term_is_foot = std::env::var("TERM")
+        .unwrap_or_default()
+        .starts_with("foot");
+
     loop {
         ...
-        let term_is_foot = std::env::var("TERM")
-            .unwrap_or_default()
-            .starts_with("foot");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 459 - 466, Compute TERM once before
entering the loop and store it in the existing term_is_foot boolean, then use
that same term_is_foot value inside the loop where the escape sequence is
written (the std::io::stderr() / write_all/flush block) and also reuse it in the
synchronized-updates teardown codepath instead of re-calling
std::env::var("TERM") again; move the
std::env::var("TERM").unwrap_or_default().starts_with("foot") expression out of
the loop so the TERM check is hoisted and both the escape-write block and the
teardown reference the single term_is_foot variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 1383-1399: Handle parse failures from CclipItem::from_line by
reporting a temporary UI message and skipping the copy—i.e., where you currently
do "if let Ok(cclip_item) = super::CclipItem::from_line(...)" add the else
branch to call ui.set_temp_message(...) with a clear error (or the Err text) and
continue so malformed lines surface to the user; and make the terminal cleanup
consistent by guarding the DisableMouseCapture execute call with the same
disable_mouse check used in the mouse-click handler (use the disable_mouse
boolean before executing DisableMouseCapture) so you don’t unconditionally call
it.
- Around line 415-444: The loop retries failed image loads endlessly because
displayed_rowid_opt is reset to None on failure; add a HashSet (e.g.,
failed_rowids) scoped with the loop to track rowids that failed to load and skip
calling image_manager.load_cclip_image(rowid) when
failed_rowids.contains(rowid); on successful load remove rowid from
failed_rowids, on load failure insert rowid into failed_rowids, and also clear
the set entries for a rowid when transitioning away from images (where
image_manager.clear() and DISPLAY_STATE::Empty are used) so subsequent revisits
can retry. Ensure checks reference current_rowid_opt, displayed_rowid_opt,
image_manager.load_cclip_image, image_manager.clear, and
crate::ui::DISPLAY_STATE as the relevant symbols.

In `@src/ui/dmenu_ui.rs`:
- Around line 433-447: The preview always shows the static "Loading..." span
because there is no failed state; add a DisplayState::Failed(String) variant (or
a bool+message field) to the display state used by ImageManager and update
run.rs where load_cclip_image fails to set DISPLAY_STATE::Failed(error_msg)
instead of clearing to Empty; then modify the preview rendering code in
info_with_image_support / dmenu_ui.rs (the block that sets self.text and
currently emits Span::styled("- Loading...")) to pattern-match the display state
(DisplayState::Loading, DisplayState::Failed(msg), DisplayState::Ready) and
render a clear failure span using the failure message when Failed(msg) is
present; also ensure transitions back to Empty/Ready clear the failure message
appropriately.
- Around line 678-690: get_image_info currently uses
item.original_line.splitn(3, '\t') so parts[2] can include trailing tags (format
is rowid\tmime\tpreview[\ttags]); change splitn(3, '\t') to splitn(4, '\t') in
get_image_info so parts[2] contains only the preview field (consistent with
is_cclip_image_item), and return parts[2] (or "Unknown Image" when parts.len() <
3); optionally add a short comment noting the cclip field layout.

In `@src/ui/graphics.rs`:
- Around line 83-86: The code in load_cclip_image is propagating errors from
child.wait().await? which can return Err after bytes were successfully read;
change the handling of the child process exit so it does not use the ? operator
— e.g., await the exit without propagating (assign or match the result) or log a
non-fatal failure; update the block containing let _ = child.wait().await?; to
await the child without using ? (or handle the Result locally) so a successful
image read (res) is returned instead of being discarded by an Err from wait().
- Around line 97-101: The image decoding (image::load_from_memory) and protocol
creation (self.picker.new_resize_protocol) are blocking and must be moved off
the async executor; wrap both calls in tokio::task::spawn_blocking, run them
inside the blocking closure (decode the bytes to dyn_img and then call
picker.new_resize_protocol(dyn_img)), await the JoinHandle and propagate/map any
errors back into the async function, and only set self.protocol = Some(...) on
the async task's successful result to avoid blocking the tokio runtime thread.

---

Outside diff comments:
In `@src/modes/cclip/run.rs`:
- Around line 929-955: The code indexes ui.shown with selected_idx without
ensuring selected_idx < ui.shown.len(), which can panic if the selection is
stale; update the condition to check both !ui.shown.is_empty() and selected_idx
< ui.shown.len() (mirroring the delete handler) before accessing
ui.shown[selected_idx], and if out of bounds treat it like no selection (set
ui.tag_mode to RemovingTag with selected: None or skip setting from CclipItem);
use the same symbols shown (ui.selected, ui.shown, selected_idx,
TagMode::RemovingTag, super::CclipItem::from_line) to locate and fix the guard.

---

Duplicate comments:
In `@src/modes/cclip/run.rs`:
- Around line 1720-1736: The click handler currently swallows errors from
super::CclipItem::from_line(original_line.clone()) using if let Ok(...), so
change it to mirror the Enter handler: match or if let Err(e) branch and call
ui.set_temp_message(format!("Parse failed: {}", e)) (or similar message used by
Enter) when from_line returns Err, then continue; keep the existing
successful-path behavior (calling copy_to_clipboard, showing cursor, cleaning
terminal, disabling mouse/raw mode, and returning Ok(())) unchanged.

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 313-343: The initial pre-loop layout uses
content_panel_width/content_panel_height (calculated before entering the main
draw loop) and calls ui.info_with_image_support with those stale sizes, causing
the first frame to render with incorrect wrapping after a terminal resize;
remove that pre-loop call or move the ui.info_with_image_support invocation into
the same per-frame draw closure (the closure around draw that already
recalculates layout every frame) so that ui.info_with_image_support always
receives the up-to-date content_panel_width/content_panel_height used by the
draw closure; locate references to ui.info_with_image_support and the pre-loop
content_panel_* calculations and eliminate the duplicate/out-of-date call so
only the per-frame draw path performs the layout and rendering.
- Around line 404-424: The DisplayState::Image variant currently carries an
unused Rect and should be simplified to hold only a String: update the enum
definition (in src/ui/graphics.rs) to change DisplayState::Image(Rect, String)
-> DisplayState::Image(String), then update all places that construct or
pattern-match this variant—e.g. in this file adjust the lock/match on
crate::ui::DISPLAY_STATE (replace destructuring that ignores Rect with just the
String), and update the site that sets the state after loading an image (replace
DisplayState::Image(ratatui::layout::Rect::default(), rowid.to_string()) with
DisplayState::Image(rowid.to_string())). Also search for other uses of
DisplayState::Image and adapt them to the new single-String variant to keep
types consistent (e.g., any code that previously expected a Rect should be
removed or refactored).
- Around line 459-466: Compute TERM once before entering the loop and store it
in the existing term_is_foot boolean, then use that same term_is_foot value
inside the loop where the escape sequence is written (the std::io::stderr() /
write_all/flush block) and also reuse it in the synchronized-updates teardown
codepath instead of re-calling std::env::var("TERM") again; move the
std::env::var("TERM").unwrap_or_default().starts_with("foot") expression out of
the loop so the TERM check is hoisted and both the escape-write block and the
teardown reference the single term_is_foot variable.

In `@src/ui/graphics.rs`:
- Around line 42-58: Move the repeated import into the module scope: add a
single top-level `use ratatui_image::picker::ProtocolType;` alongside other
imports, then remove the three inner `use` lines inside the methods
`supports_graphics`, `is_kitty`, and `is_sixel`; ensure the methods continue to
call `self.picker.protocol_type()` and pattern-match against
`ProtocolType::Halfblocks`, `ProtocolType::Kitty`, and `ProtocolType::Sixel`
respectively.

- `src/ui/graphics.rs`: Add `tokio::task::spawn_blocking` to decouple heavy
  `image::load_from_memory` traits and correctly propagate success cases
  through `child.wait().await`.
- `src/modes/cclip/run.rs`: Introduce `HashSet` states for `failed_rowids`
  preventing cclip endless loop crashes during timeout issues, explicitly
  surface parse errors through both Enter + Mouse interaction handlers instead
  of dropping them quietly, and clean up unneeded sizing heuristics.
- `src/ui/dmenu_ui.rs`: Alter `get_image_info` string splitting limit matching
  sibling `splitn` boundaries dynamically scaling with `parts[2]` lengths. Replaced
  hardcoded `Span` loading variants inside `info_with_image_support` to map
  dynamically loaded states allowing Error failure components to render properly.
- Enforce unified type signature dropping unused `Rect` inside DisplayState Enum.
- `src/modes/cclip/run.rs`: Delete unused initializers for `terminal_area`,
  `content_height`, `input_panel_height`, and `content_panel_position` outside
  the main `terminal.draw` loop, resolving warnings generated by cargo clippy
  and `--release` optimizing passes, since these are correctly redefined internally
  per-frame anyway so its cool.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/modes/cclip/run.rs (1)

908-962: Significant code duplication in clipboard history reload logic.

The "reload items → recreate with formatter → preserve selection by rowid → restore scroll" pattern appears three times: delete handler (~Line 908), tag handler (~Line 1149), and untag handler (~Line 1238). Each copy is ~50 lines of near-identical code.

Consider extracting a helper method, e.g.:

fn reload_and_restore(
    ui: &mut DmenuUI,
    tag_filter: Option<&str>,
    formatter: &TagMetadataFormatter,
    show_line_numbers: bool,
    show_tag_color_names: bool,
) { /* shared reload logic */ }

This would reduce ~150 lines to ~50 + 3 call sites.

Also applies to: 1149-1208, 1238-1298

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 908 - 962, Duplicate clipboard-reload
logic (recreating Item list, preserving/restoring ui.selected and
ui.scroll_offset, filtering) appears in three places; extract it into a helper
like reload_and_restore(ui: &mut DmenuUI, updated_items: Vec<CclipItem>,
formatter: &TagMetadataFormatter, show_line_numbers: bool, show_tag_color_names:
bool) that builds Items (using
cclip_item.get_display_name_with_number_formatter_options /
get_display_name_with_formatter_options and Item::new_simple), sets ui.hidden,
clears ui.shown and calls ui.filter(), then restores selection/scroll with the
same bounds-check logic (ui.selected, ui.scroll_offset) currently duplicated;
replace the three inline blocks with calls to this helper to eliminate ~50-line
duplicates.
src/ui/graphics.rs (3)

45-58: supports_graphics() excludes Halfblocks — intentional?

Halfblocks is a valid rendering backend in ratatui-image (uses Unicode half-block characters). By returning false for it, image previews will be disabled on terminals without Kitty/Sixel support, even though Halfblocks provides a usable (if lower-fidelity) fallback. If this is intentional, a brief doc comment explaining the rationale would help future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 45 - 58, supports_graphics currently
excludes ProtocolType::Halfblocks which prevents using the half-block Unicode
fallback; update supports_graphics to treat Halfblocks as supported (e.g.,
return true for ProtocolType::Halfblocks or invert to check for a
non-none/unsupported variant instead of excluding Halfblocks) and add a short
doc comment explaining that Halfblocks is a lower-fidelity but valid fallback;
refer to the supports_graphics function, picker.protocol_type(), and
ProtocolType::Halfblocks when making the change.

80-91: child.wait().await on Line 84 runs outside the timeout boundary.

After read_future completes (within the 1.5 s timeout), the child.wait().await at Line 84 is unbounded. If the child process doesn't exit after stdout is consumed (e.g., it's stuck on stderr or a signal handler), this blocks the async executor indefinitely. This is unlikely in practice since stdout EOF typically triggers process exit, but a second timeout or try_wait loop would make it robust.

♻️ Suggested improvement
                 Ok(res) => {
-                    let _ = child.wait().await;
+                    // Best-effort wait with a short timeout; kill if stuck
+                    if tokio::time::timeout(
+                        std::time::Duration::from_millis(500),
+                        child.wait(),
+                    )
+                    .await
+                    .is_err()
+                    {
+                        let _ = child.kill().await;
+                    }
                     res?
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 80 - 91, The current code calls
child.wait().await after read_future completes which runs outside the
tokio::time::timeout and can block the executor; wrap the process wait in a
bounded operation — e.g., replace the direct child.wait().await with a
tokio::time::timeout(...) around child.wait(), or implement a non-blocking loop
using child.try_wait() with small sleeps — so that after Ok(res) you await the
child exit under a second timeout, and on timeout kill the child
(child.kill().await) and return an Err; update the match arm handling for
Ok(res) to use this additional timeout and propagate errors accordingly.

33-43: Picker::from_query_stdio is the correct ratatui-image 10.0 API and is properly used in the code.

Both ImageManager::new() and GraphicsAdapter::detect() call Picker::from_query_stdio() to query terminal capabilities. However, they are in separate execution paths: ImageManager is used only in cclip mode (line 214, src/modes/cclip/run.rs), while GraphicsAdapter::detect() is only invoked in dmenu mode (during rendering). In practice, the terminal is not queried twice in a single execution—the concern is mitigated by the architecture.

If the codebase ever evolves to use both in the same flow, consider refactoring GraphicsAdapter to accept a Picker instance rather than re-querying, or storing the picker result for reuse to avoid redundant terminal I/O.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 33 - 43, Both ImageManager::new() and
GraphicsAdapter::detect() call Picker::from_query_stdio(), which duplicates
terminal queries; to prevent redundant I/O if both paths are used together,
refactor GraphicsAdapter to accept a Picker (or a shared picker result) instead
of creating its own. Update GraphicsAdapter::detect to take a &Picker or change
the GraphicsAdapter constructor to accept a Picker instance; modify call sites
to pass the existing Picker from ImageManager::new (or a central place that
creates the Picker) and remove the direct Picker::from_query_stdio() call inside
GraphicsAdapter::detect so the picker is created once and reused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 213-220: The eprintln! used when initializing ImageManager
(crate::ui::ImageManager::new) writes to stderr while the alternate screen/TUI
is active and can corrupt the UI; replace these direct stderr prints by either
moving the ImageManager initialization and its warning log before entering the
alternate screen, or capture the error and pass a user-facing message into the
UI (e.g., set image_manager = None and call ui.set_temp_message(...) or
equivalent) so the warning is shown inside the TUI after initialization rather
than using eprintln!. Ensure both places where eprintln! is currently used
follow the same approach and reference the image_manager variable and
ImageManager::new for locating the changes.
- Around line 856-882: The untag handler can index out of bounds because it only
checks !ui.shown.is_empty() before using selected_idx; update the conditional
that guards indexing to the same safe form as the tag handler by requiring
ui.selected (selected_idx) to be Some and that selected_idx < ui.shown.len()
before accessing ui.shown[selected_idx]; apply this check where you build
selected_item and before calling super::CclipItem::from_line, keeping the
subsequent logic that constructs TagMode::RemovingTag (with input, tags,
selected, selected_item) unchanged.

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 908-962: Duplicate clipboard-reload logic (recreating Item list,
preserving/restoring ui.selected and ui.scroll_offset, filtering) appears in
three places; extract it into a helper like reload_and_restore(ui: &mut DmenuUI,
updated_items: Vec<CclipItem>, formatter: &TagMetadataFormatter,
show_line_numbers: bool, show_tag_color_names: bool) that builds Items (using
cclip_item.get_display_name_with_number_formatter_options /
get_display_name_with_formatter_options and Item::new_simple), sets ui.hidden,
clears ui.shown and calls ui.filter(), then restores selection/scroll with the
same bounds-check logic (ui.selected, ui.scroll_offset) currently duplicated;
replace the three inline blocks with calls to this helper to eliminate ~50-line
duplicates.

In `@src/ui/graphics.rs`:
- Around line 45-58: supports_graphics currently excludes
ProtocolType::Halfblocks which prevents using the half-block Unicode fallback;
update supports_graphics to treat Halfblocks as supported (e.g., return true for
ProtocolType::Halfblocks or invert to check for a non-none/unsupported variant
instead of excluding Halfblocks) and add a short doc comment explaining that
Halfblocks is a lower-fidelity but valid fallback; refer to the
supports_graphics function, picker.protocol_type(), and ProtocolType::Halfblocks
when making the change.
- Around line 80-91: The current code calls child.wait().await after read_future
completes which runs outside the tokio::time::timeout and can block the
executor; wrap the process wait in a bounded operation — e.g., replace the
direct child.wait().await with a tokio::time::timeout(...) around child.wait(),
or implement a non-blocking loop using child.try_wait() with small sleeps — so
that after Ok(res) you await the child exit under a second timeout, and on
timeout kill the child (child.kill().await) and return an Err; update the match
arm handling for Ok(res) to use this additional timeout and propagate errors
accordingly.
- Around line 33-43: Both ImageManager::new() and GraphicsAdapter::detect() call
Picker::from_query_stdio(), which duplicates terminal queries; to prevent
redundant I/O if both paths are used together, refactor GraphicsAdapter to
accept a Picker (or a shared picker result) instead of creating its own. Update
GraphicsAdapter::detect to take a &Picker or change the GraphicsAdapter
constructor to accept a Picker instance; modify call sites to pass the existing
Picker from ImageManager::new (or a central place that creates the Picker) and
remove the direct Picker::from_query_stdio() call inside GraphicsAdapter::detect
so the picker is created once and reused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/modes/cclip/run.rs (4)

797-841: Fullscreen preview: image is not re-loaded after returning from modal.

After the fullscreen loop exits (line 833), DISPLAY_STATE is reset to Empty and previous_was_image is set to false. On the next main-loop iteration, the image-loading block (line 356-378) will detect that displayed_rowid_opt doesn't match the current rowid and will re-trigger load_cclip_image. This means a redundant subprocess call + image decode just to restore the inline preview. The image data is already in manager.protocol — resetting DISPLAY_STATE without clearing manager.protocol would avoid the re-load. Alternatively, skip clearing self.protocol on the clear call and only reset DISPLAY_STATE to Image(rowid) after the modal.

This is a minor performance concern on the happy path, not a correctness bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 797 - 841, The fullscreen modal resets
DISPLAY_STATE to Empty and previous_was_image to false which forces
load_cclip_image to re-run on next loop; instead preserve the loaded image data
in image_manager by NOT clearing manager.protocol (or explicitly repopulating
DISPLAY_STATE to Image(current_rowid) after the modal) so the inline preview can
be restored from manager.protocol without spawning the subprocess again—locate
the modal loop handling around image_manager / manager.render and replace the
DISPLAY_STATE reset with either leaving manager.protocol intact or setting
crate::ui::DISPLAY_STATE to crate::ui::DisplayState::Image(current_rowid) (and
avoid clearing manager.protocol in any terminal.clear/clear-call path).

1170-1171: Duplicate comment on consecutive lines.

Line 1170 and 1171 both read // Reload clipboard history to get updated tags.

Proposed fix
                                         } else {
-                                            // Reload clipboard history to get updated tags
                                             // Reload clipboard history to get updated tags
                                             let updated_items_res = if let Some(ref tag_name) =
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 1170 - 1171, Remove the duplicated
comment line so the comment "// Reload clipboard history to get updated tags"
only appears once immediately above the let updated_items_res = if let Some(ref
tag_name) = ... expression; keep the single descriptive comment and delete the
consecutive duplicate to avoid redundancy around the updated_items_res /
tag_name block.

356-378: Image loading blocks the draw loop on the async executor.

manager.load_cclip_image(rowid).await is called synchronously within the main TUI loop iteration, before terminal.draw(...). While the subprocess I/O is async (with a 1.5s timeout), this still delays the frame draw by up to ~2s in the worst case (1.5s read + 0.5s wait timeout). During this time, the UI is unresponsive — no input events are processed and no frames are drawn.

For a "Chill" review, this is acceptable as a known trade-off (the timeout bounds the delay), but consider noting this limitation in a comment or TODO for future improvement (e.g., spawning the load as a background task and rendering a "Loading..." placeholder while in progress).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 356 - 378, The image load is awaited
inline (manager.load_cclip_image(rowid).await) which blocks the TUI draw/input
loop; instead, turn the synchronous await into an asynchronous background task:
when a new image rowid is detected in the current_is_image branch, immediately
set DISPLAY_STATE to DisplayState::Loading (or similar), clone/convert the
image_manager handle into a thread-safe shareable handle (e.g., Arc/Mutex or a
channel-friendly wrapper), spawn a tokio task (tokio::spawn or equivalent) to
call manager.load_cclip_image(rowid).await off the main loop, and on completion
update DISPLAY_STATE to Image(rowid)/Failed(error) and update failed_rowids via
a thread-safe mechanism or by sending back a message to the main loop; ensure
displayed_rowid_opt/failed_rowids logic is updated from the main loop when the
background task reports results so the main draw loop is not blocked.

471-473: Duplicate comment.

Lines 471 and 473 both say // Layout calculation. Remove the duplicate.

Proposed fix
             // Re-detect graphics adapter with the shared picker
             let graphics = crate::ui::GraphicsAdapter::detect(picker.as_ref());
 
-            // Layout calculation
-
             // Layout calculation
             let total_height = f.area().height;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 471 - 473, Remove the duplicated comment
line "// Layout calculation" so only a single instance remains; locate the two
consecutive "// Layout calculation" comments in run.rs (the ones inside the run
function / layout calculation block) and delete the redundant one to keep
comments concise.
src/ui/graphics.rs (1)

151-160: Env-var fallback heuristic: term.contains("xterm") may over-detect Sixel.

Many terminals set TERM=xterm-256color without actually supporting Sixel. This fallback only activates when Picker::from_query_stdio() failed (so protocol auto-detection wasn't possible), meaning the user is already in a degraded path. The consequence is that GraphicsAdapter::detect might return Sixel for terminals that don't support it, which could affect the clear/sync logic in the draw loop (Sixel-specific clearing paths).

This is low-risk since the actual image rendering goes through the picker's protocol (which would be halfblocks if detection failed), and GraphicsAdapter is only used for clear/sync heuristics. But worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 151 - 160, The current heuristic in
GraphicsAdapter::detect that treats term.contains("xterm") as Sixel is too
permissive; update the detection logic (the block using TERM and TERM_PROGRAM)
to be conservative: remove or restrict the generic term.contains("xterm") check
and instead check for explicit indicators of sixel support (e.g., specific TERM
values like "xterm-kitty", presence of "sixel" in TERM, or known TERM_PROGRAM
values such as "WezTerm" or "kitty"); keep Kitty detection as-is (Self::Kitty)
and ensure this fallback only applies when Picker::from_query_stdio()
auto-detection has failed by leaving that flow unchanged. Ensure you modify the
function where Self::Sixel is returned so only explicit sixel-capable term
strings or trusted TERM_PROGRAM names map to Self::Sixel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 213-228: The image initialization currently treats a missing
terminal picker as an error even though halfblocks is a valid fallback; update
the logic around picker, picker_fallback, image_manager and image_init_error so
the message reflects an informational "detection failed, using half-block
fallback" rather than an error, and change the UI code that reads
image_init_error (the warning display path) to show it as an informational
notice or lower-severity message when picker.is_none() but image_manager is
still Some(...); alternatively, remove the warning entirely and only surface an
error when image_manager is None (i.e., preview actually unavailable).

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 797-841: The fullscreen modal resets DISPLAY_STATE to Empty and
previous_was_image to false which forces load_cclip_image to re-run on next
loop; instead preserve the loaded image data in image_manager by NOT clearing
manager.protocol (or explicitly repopulating DISPLAY_STATE to
Image(current_rowid) after the modal) so the inline preview can be restored from
manager.protocol without spawning the subprocess again—locate the modal loop
handling around image_manager / manager.render and replace the DISPLAY_STATE
reset with either leaving manager.protocol intact or setting
crate::ui::DISPLAY_STATE to crate::ui::DisplayState::Image(current_rowid) (and
avoid clearing manager.protocol in any terminal.clear/clear-call path).
- Around line 1170-1171: Remove the duplicated comment line so the comment "//
Reload clipboard history to get updated tags" only appears once immediately
above the let updated_items_res = if let Some(ref tag_name) = ... expression;
keep the single descriptive comment and delete the consecutive duplicate to
avoid redundancy around the updated_items_res / tag_name block.
- Around line 356-378: The image load is awaited inline
(manager.load_cclip_image(rowid).await) which blocks the TUI draw/input loop;
instead, turn the synchronous await into an asynchronous background task: when a
new image rowid is detected in the current_is_image branch, immediately set
DISPLAY_STATE to DisplayState::Loading (or similar), clone/convert the
image_manager handle into a thread-safe shareable handle (e.g., Arc/Mutex or a
channel-friendly wrapper), spawn a tokio task (tokio::spawn or equivalent) to
call manager.load_cclip_image(rowid).await off the main loop, and on completion
update DISPLAY_STATE to Image(rowid)/Failed(error) and update failed_rowids via
a thread-safe mechanism or by sending back a message to the main loop; ensure
displayed_rowid_opt/failed_rowids logic is updated from the main loop when the
background task reports results so the main draw loop is not blocked.
- Around line 471-473: Remove the duplicated comment line "// Layout
calculation" so only a single instance remains; locate the two consecutive "//
Layout calculation" comments in run.rs (the ones inside the run function /
layout calculation block) and delete the redundant one to keep comments concise.

In `@src/ui/graphics.rs`:
- Around line 151-160: The current heuristic in GraphicsAdapter::detect that
treats term.contains("xterm") as Sixel is too permissive; update the detection
logic (the block using TERM and TERM_PROGRAM) to be conservative: remove or
restrict the generic term.contains("xterm") check and instead check for explicit
indicators of sixel support (e.g., specific TERM values like "xterm-kitty",
presence of "sixel" in TERM, or known TERM_PROGRAM values such as "WezTerm" or
"kitty"); keep Kitty detection as-is (Self::Kitty) and ensure this fallback only
applies when Picker::from_query_stdio() auto-detection has failed by leaving
that flow unchanged. Ensure you modify the function where Self::Sixel is
returned so only explicit sixel-capable term strings or trusted TERM_PROGRAM
names map to Self::Sixel.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 412-419: The code currently calls manager.lock().await.is_sixel()
each frame (inside the image_preview_enabled branch) which can block the UI;
change this to avoid awaiting the ImageManager lock per frame by caching the
is_sixel value once when the ImageManager is created or when
image_preview_enabled is toggled (store it in a local/struct field like
cached_is_sixel) and use that cached value in the frame loop, or perform a
non-blocking check via try_lock() and fall back to the cached value if the lock
is not available; update references to image_manager/is_sixel and remove the
per-frame await to ensure the UI loop never awaits the background loader lock.

---

Duplicate comments:
In `@src/ui/graphics.rs`:
- Around line 44-48: supports_graphics() currently always returns true (thus
treating Halfblocks as a valid high-fidelity protocol); change it so it only
reports true when the terminal supports at least one non-Halfblocks
high-fidelity protocol. Update the implementation of supports_graphics to
inspect the terminal/protocol capability list on the struct (referencing
supports_graphics and the Halfblocks variant) and return true only if there
exists a supported protocol != Halfblocks; alternatively, if intended behavior
is to include fallbacks, rename supports_graphics to something like
supports_graphics_or_fallback to reflect that semantics and adjust call sites
accordingly.

…ction

- Cache is_sixel capability once during init to avoid per-frame async lock contention
- Update supports_graphics to strictly report high-fidelity protocol availability
- Improve initialization messaging to clarify when Halfblock fallback is active
- Clean up dead code and redundant checks in the cclip run loop
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/modes/cclip/run.rs (1)

251-283: Simplify unreachable image-manager warning branch.

image_manager is always Some due to the halfblocks fallback, so the later warning if image_preview_enabled && picker.is_none() && image_manager.is_none() can never fire. Consider removing the dead branch or folding the logic into the earlier message to avoid confusion.

♻️ Suggested cleanup
-    // warn if image preview is enabled but terminal graphics detection failed (and no fallback)
-    if image_preview_enabled && picker.is_none() && image_manager.is_none() {
-        ui.set_temp_message(
-            "image_preview enabled but terminal graphics detection failed".to_string(),
-        );
-    }
+    // (optional) warning already surfaced above when picker is None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 251 - 283, Remove the unreachable
warning that checks image_manager.is_none() because image_manager is always Some
(due to the halfblocks fallback); delete the final if block that reads "if
image_preview_enabled && picker.is_none() && image_manager.is_none()" and, if
you still want to warn when image preview is enabled but terminal graphics
aren't available, fold that check into the earlier picker/is_none branch (use
image_preview_enabled and picker.is_none() there) so the warning is reachable
and uses the existing ui.set_temp_message call instead of a dead branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 901-925: The code currently ignores errors from
super::CclipItem::from_line; change the if let Ok(...) to match
super::CclipItem::from_line(item.original_line.clone()) and handle Err(e) by
surfacing the parse error to the user using the same UI/notification mechanism
used by the copy-paths flow (e.g., set the UI status/error message or call the
existing notify/display function), while preserving the existing fallback
TagMode::RemovingTag state when parsing fails; reference CclipItem::from_line,
TagMode::RemovingTag, ui.tag_mode, ui.selected, and ui.shown to locate where to
insert the match and error-reporting call.

In `@src/ui/graphics.rs`:
- Around line 141-165: The detect logic in GraphicsAdapter::detect currently
ignores ProtocolType::Iterm2 from the Picker and falls back to env heuristics;
inside the match on picker.protocol_type() add an explicit arm for
ProtocolType::Iterm2 and map it to the appropriate adapter (e.g., return
Self::Kitty) or alternatively add a new GraphicsAdapter::Iterm2 variant and
return that if iTerm2 needs distinct handling; update any downstream
rendering/clearing code if you choose the new variant to ensure behavior parity.

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 251-283: Remove the unreachable warning that checks
image_manager.is_none() because image_manager is always Some (due to the
halfblocks fallback); delete the final if block that reads "if
image_preview_enabled && picker.is_none() && image_manager.is_none()" and, if
you still want to warn when image preview is enabled but terminal graphics
aren't available, fold that check into the earlier picker/is_none branch (use
image_preview_enabled and picker.is_none() there) so the warning is reachable
and uses the existing ui.set_temp_message call instead of a dead branch.

- Change DISPLAY_STATE from std::sync::Mutex to tokio::sync::Mutex
- Use const_new for static initialization in src/ui/graphics.rs
- Update cclip run loop and background tasks to use .lock().await
- Switch to try_lock() in non-async DmenuUI draw methods
- Remove redundant poisoned mutex handling logic
- Ensure safe lock ordering by releasing failed_rowids before DISPLAY_STATE
- Also ran cargo update to update time byte to pass cargo audit
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/modes/cclip/run.rs (1)

1686-1698: Consider preserving scroll position relative to selection.

The current implementation sets ui.scroll_offset = pos (line 1694), which scrolls the selected item to the top of the visible area. If the item was previously in the middle of the viewport, this can be disorienting after tag operations.

A more seamless UX would preserve the item's relative position within the viewport when possible.

♻️ Optional: Preserve relative scroll position
     // Restore selection by finding the same rowid
     if let Some(ref rowid) = selected_rowid {
         if let Some(pos) = ui
             .shown
             .iter()
             .position(|item| item.original_line.split('\t').next() == Some(rowid.as_str()))
         {
             ui.selected = Some(pos);
-            // Restore scroll offset by scrolling selected item to top
-            ui.scroll_offset = pos;
+            // Keep item visible - only adjust scroll if item would be out of view
+            // (Scroll adjustment logic depends on knowing visible height, 
+            // which may require passing it as a parameter)
         } else if !ui.shown.is_empty() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 1686 - 1698, When restoring selection
for selected_rowid, preserve the item's relative position in the viewport
instead of forcing it to the top: capture the previous selected index
(ui.selected) and previous ui.scroll_offset, compute relative_offset =
previous_selected_index.saturating_sub(previous_scroll_offset) (use pos when
there was no previous selection), then set ui.selected = Some(pos) and set
ui.scroll_offset = pos.saturating_sub(relative_offset) clamped to valid bounds
(0..=shown.len().saturating_sub(1)). Update the block handling selected_rowid
(refs: selected_rowid, ui.shown, ui.selected, ui.scroll_offset, pos) to perform
that calculation instead of assigning ui.scroll_offset = pos.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 1686-1698: When restoring selection for selected_rowid, preserve
the item's relative position in the viewport instead of forcing it to the top:
capture the previous selected index (ui.selected) and previous ui.scroll_offset,
compute relative_offset =
previous_selected_index.saturating_sub(previous_scroll_offset) (use pos when
there was no previous selection), then set ui.selected = Some(pos) and set
ui.scroll_offset = pos.saturating_sub(relative_offset) clamped to valid bounds
(0..=shown.len().saturating_sub(1)). Update the block handling selected_rowid
(refs: selected_rowid, ui.shown, ui.selected, ui.scroll_offset, pos) to perform
that calculation instead of assigning ui.scroll_offset = pos.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/ui/graphics.rs (1)

7-7: Remove the commented-out import.

Dead commented-out code adds noise.

-// use std::sync::Mutex; (removed)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` at line 7, Remove the dead commented-out import line "use
std::sync::Mutex;" from src/ui/graphics.rs; simply delete that commented line so
the file contains only active imports and no commented-out dead code.
src/modes/cclip/run.rs (2)

494-495: GraphicsAdapter::detect() re-reads environment variables every frame inside the draw closure.

picker and the env vars (TERM, TERM_PROGRAM) don't change at runtime. Cache the result before the main loop to avoid per-frame overhead.

♻️ Suggested fix

Move detection before the loop:

+    let graphics = crate::ui::GraphicsAdapter::detect(picker.as_ref());
+
     // Main TUI loop
     loop {

Then inside the draw closure, remove the per-frame call:

-            // Re-detect graphics adapter with the shared picker
-            let graphics = crate::ui::GraphicsAdapter::detect(picker.as_ref());

And capture graphics in the closure instead (it's Copy).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 494 - 495, GraphicsAdapter::detect
currently gets called every frame inside the draw closure using picker.as_ref();
move the call to GraphicsAdapter::detect(picker.as_ref()) outside and before the
main event/render loop (cache the returned graphics value once), then remove the
per-frame call inside the draw closure and capture the cached graphics (it is
Copy) in the closure so the draw path uses the cached GraphicsAdapter instead of
re-reading environment variables (TERM/TERM_PROGRAM) each frame.

790-804: Image rendering overlays the status text — verify this is intentional.

When current_is_image is true, both the content paragraph (showing "IMAGE PREVIEW - Ready" header) and the image widget are rendered to overlapping areas. The image renders on top, which means the status text will be hidden when the image is ready. If the image doesn't fill the entire content area, status text may peek through at the edges. If this is intentional (status text is only visible during loading/failure), this is fine. Just confirming the UX intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 790 - 804, The image render currently
draws over the content paragraph when current_is_image is true, hiding the
"IMAGE PREVIEW - Ready" status; fix by either skipping the content paragraph
when an image is available or drawing the status after the image so it remains
visible. Concretely, in run.rs around the image_preview_enabled/current_is_image
block that uses image_manager and manager_lock.render, check whether the manager
has a ready image (via the same manager.try_lock() path) and if so either (A)
avoid rendering the content paragraph that prints the status, or (B) move the
status draw call to execute after manager_lock.render so the status is on top;
update the code paths that produce the content paragraph/status accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 216-230: The code sets image_manager to Some(...) via
picker.map(...) and then immediately assigns a fallback Some(...)
unconditionally, so the subsequent image_manager.is_none() check and the
`"image_manager initialization failed"` branch are dead; remove the redundant
fallback assignment and/or the unreachable is_none branch depending on intent:
either (A) if you want automatic fallback, drop the is_none() check and the
error path and keep the picker_fallback assignment to always produce
Some(Arc<Mutex<ImageManager::new(...)>>), or (B) if you want a fallback-free
path, only set image_manager = picker.map(...) and remove the fallback creation
(picker_fallback and its assignment), keeping the error branch to handle None;
update references to picker, picker_fallback, image_manager, and
crate::ui::ImageManager::new accordingly.

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 494-495: GraphicsAdapter::detect currently gets called every frame
inside the draw closure using picker.as_ref(); move the call to
GraphicsAdapter::detect(picker.as_ref()) outside and before the main
event/render loop (cache the returned graphics value once), then remove the
per-frame call inside the draw closure and capture the cached graphics (it is
Copy) in the closure so the draw path uses the cached GraphicsAdapter instead of
re-reading environment variables (TERM/TERM_PROGRAM) each frame.
- Around line 790-804: The image render currently draws over the content
paragraph when current_is_image is true, hiding the "IMAGE PREVIEW - Ready"
status; fix by either skipping the content paragraph when an image is available
or drawing the status after the image so it remains visible. Concretely, in
run.rs around the image_preview_enabled/current_is_image block that uses
image_manager and manager_lock.render, check whether the manager has a ready
image (via the same manager.try_lock() path) and if so either (A) avoid
rendering the content paragraph that prints the status, or (B) move the status
draw call to execute after manager_lock.render so the status is on top; update
the code paths that produce the content paragraph/status accordingly.

In `@src/ui/graphics.rs`:
- Line 7: Remove the dead commented-out import line "use std::sync::Mutex;" from
src/ui/graphics.rs; simply delete that commented line so the file contains only
active imports and no commented-out dead code.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/modes/cclip/run.rs (2)

265-276: Warning fires for all users when picker detection fails, even those who don't use image preview.

When picker.is_none() and image_preview_enabled is false, displaying "Terminal graphics detection failed (using half-block fallback)" is noisy for users who never use image preview. Consider suppressing the message entirely when image_preview_enabled is false.

Suggested fix
     // Show initialization warnings/errors
-    if picker.is_none() {
-        if image_preview_enabled {
-            ui.set_temp_message(
-                "image_preview enabled but terminal graphics detection failed (using half-block fallback)".to_string(),
-            );
-        } else {
-            ui.set_temp_message(
-                "Terminal graphics detection failed (using half-block fallback)".to_string(),
-            );
-        }
+    if picker.is_none() && image_preview_enabled {
+        ui.set_temp_message(
+            "image_preview enabled but terminal graphics detection failed (using half-block fallback)".to_string(),
+        );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 265 - 276, The warning is shown to all
users when picker.is_none(); update the condition so the UI temp message is only
set when image_preview_enabled is true. In the block referencing picker and
ui.set_temp_message (in run.rs), remove or skip the else branch and only call
ui.set_temp_message with the graphics-detection/fallback text when
image_preview_enabled is true, leaving silence when image_preview_enabled is
false.

1654-1716: reload_and_restore is a good extraction, but scroll_offset can become stale after reload.

When the restored item's position changes (e.g., after deletion or re-ordering), scroll_offset retains its previous value and isn't adjusted. This can cause the selected item to be off-screen until the user navigates. The comment on lines 1705-1707 acknowledges this.

A simple clamp would help:

Suggested fix
         if let Some(pos) = ui
             .shown
             .iter()
             .position(|item| item.original_line.split('\t').next() == Some(rowid.as_str()))
         {
             ui.selected = Some(pos);
-            // Keep item visible only adjust scroll if item would be out of view
-            // (Scroll adjustment logic depends on knowing visible height,
-            // which may require passing it as a parameter)
+            // Ensure scroll_offset doesn't exceed the new list bounds
+            if ui.scroll_offset > pos {
+                ui.scroll_offset = pos;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 1654 - 1716, reload_and_restore
currently doesn't adjust ui.scroll_offset so the restored selection can be
off-screen; update reload_and_restore to accept a visible_height parameter
(e.g., fn reload_and_restore(..., visible_height: usize)) and after you set
ui.selected compute and clamp ui.scroll_offset so the selected index is visible:
if selected < scroll_offset set scroll_offset = selected, else if selected >=
scroll_offset + visible_height set scroll_offset = selected + 1 -
visible_height; also ensure ui.scroll_offset is bounded between 0 and
ui.shown.len().saturating_sub(visible_height). Apply this logic in the branch
where you restore selection (the block referencing ui.selected and
ui.scroll_offset) and in the branch that selects the first item.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 379-394: The background task spawned with tokio::spawn currently
drops its JoinHandle so panics inside manager_lock.load_cclip_image (or
downstream image::load_from_memory) are swallowed; wrap the async task body in
std::panic::AssertUnwindSafe(...).catch_unwind().await (or equivalent) so you
can detect unwind (Err) and treat it like an error: on Ok(path) run the existing
success path (failed_lock.lock().await.remove(...); set DISPLAY_STATE::Image),
on Err(_) run the same failure path (failed_lock.lock().await.insert(...);
manager_lock.clear(); set DISPLAY_STATE::Failed with an error string). Apply
this change around the code using manager_clone, failed_lock,
manager_lock.load_cclip_image, and DISPLAY_STATE so panics update failed_rowids
and the UI instead of being silently dropped.

---

Nitpick comments:
In `@src/modes/cclip/run.rs`:
- Around line 265-276: The warning is shown to all users when picker.is_none();
update the condition so the UI temp message is only set when
image_preview_enabled is true. In the block referencing picker and
ui.set_temp_message (in run.rs), remove or skip the else branch and only call
ui.set_temp_message with the graphics-detection/fallback text when
image_preview_enabled is true, leaving silence when image_preview_enabled is
false.
- Around line 1654-1716: reload_and_restore currently doesn't adjust
ui.scroll_offset so the restored selection can be off-screen; update
reload_and_restore to accept a visible_height parameter (e.g., fn
reload_and_restore(..., visible_height: usize)) and after you set ui.selected
compute and clamp ui.scroll_offset so the selected index is visible: if selected
< scroll_offset set scroll_offset = selected, else if selected >= scroll_offset
+ visible_height set scroll_offset = selected + 1 - visible_height; also ensure
ui.scroll_offset is bounded between 0 and
ui.shown.len().saturating_sub(visible_height). Apply this logic in the branch
where you restore selection (the block referencing ui.selected and
ui.scroll_offset) and in the branch that selects the first item.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modes/cclip/run.rs (1)

1379-1381: ⚠️ Potential issue | 🟠 Major

Plain u16 subtraction in three navigation handlers panics in debug builds on small terminals.

total_height - content_height - input_panel_height (repeated at lines 1380–1381, 1443–1444, and 1511–1512) is unchecked u16 arithmetic. In a very small terminal — or any combination where content_height.max(3) + input_panel_height > total_height — this underflows: panic in debug builds, silent wrap-to-~65 k in release (causing max_visible to become absurdly large).

The outer-scope max_visible variable (declared at line 320, updated to the exact same geometry inside the draw closure at line 627) already holds the correct value and can be referenced directly, removing three ~10-line duplicate layout recalculations at once.

🐛 Proposed fix — use the cached `max_visible` in all three navigation handlers

Right-navigation handler (lines ~1374–1387):

-                            // Use same calculation as rendering code
-                            let content_height = (total_height as f32 * content_panel_height as f32
-                                / 100.0)
-                                .round() as u16;
-                            let content_height = content_height.max(3);
-                            let items_panel_height =
-                                total_height - content_height - input_panel_height;
-                            let max_visible = items_panel_height.saturating_sub(2) as usize;
-
-                            if max_visible > 0 && ui.shown.len() > max_visible {
+                            // Use max_visible kept in sync by the draw loop
+                            if max_visible > 0 && ui.shown.len() > max_visible {

Down-navigation handler (lines ~1438–1450):

-                                // Use same calculation as rendering code
-                                let content_height =
-                                    (total_height as f32 * content_panel_height as f32 / 100.0)
-                                        .round() as u16;
-                                let content_height = content_height.max(3);
-                                let items_panel_height =
-                                    total_height - content_height - input_panel_height;
-                                let max_visible = items_panel_height.saturating_sub(2) as usize; // -2 for borders
-
-                                // Scroll down if selection is below visible area
+                                // Scroll down if selection is below visible area
                                 if new_selected >= ui.scroll_offset + max_visible {
-                                    ui.scroll_offset = new_selected.saturating_sub(max_visible - 1);
+                                    ui.scroll_offset = new_selected.saturating_sub(max_visible.saturating_sub(1));

Apply the same pattern to the Up-navigation handler (lines ~1506–1521).

All three handlers also call terminal.size()? then derive layout from it, which can differ from f.area() used in the draw closure during a resize. Referencing the cached max_visible also eliminates that inconsistency.

Also applies to: 1443-1444, 1511-1513

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modes/cclip/run.rs` around lines 1379 - 1381, The three navigation
handlers (Right/Down/Up) recalculate items_panel_height using unchecked u16
subtraction which underflows on small terminals; instead of recomputing geometry
(including calls to terminal.size()? and the local total_height - content_height
- input_panel_height math), use the already-cached max_visible variable (the
outer-scope max_visible declared earlier and updated in the draw closure) when
computing visible limits and advancing selection; replace the duplicated layout
logic in the handlers with references to max_visible to avoid u16 underflow and
inconsistent geometry during resize.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modes/cclip/run.rs`:
- Around line 886-892: After calling terminal.clear() in the fullscreen exit
path, set the Sixel sync flag so ratatui emits Clear widgets for Sixel
terminals; locate the block where terminal.clear().wrap_err(...) is followed by
restoring DISPLAY_STATE (current_rowid_opt and crate::ui::DisplayState::Image),
and after setting *state = crate::ui::DisplayState::Image(rowid.clone()) also
set crate::ui::force_sixel_sync = true (or call the equivalent setter) so that
the next frame forces a Sixel clear and prevents ghost pixels.

---

Outside diff comments:
In `@src/modes/cclip/run.rs`:
- Around line 1379-1381: The three navigation handlers (Right/Down/Up)
recalculate items_panel_height using unchecked u16 subtraction which underflows
on small terminals; instead of recomputing geometry (including calls to
terminal.size()? and the local total_height - content_height -
input_panel_height math), use the already-cached max_visible variable (the
outer-scope max_visible declared earlier and updated in the draw closure) when
computing visible limits and advancing selection; replace the duplicated layout
logic in the handlers with references to max_visible to avoid u16 underflow and
inconsistent geometry during resize.

---

Duplicate comments:
In `@src/modes/cclip/run.rs`:
- Line 1549: The subtraction for items_panel_height (let items_panel_height =
total_height - content_height - input_panel_height) can underflow as u16; update
the calculation to use saturating_sub (or perform the subtraction in a larger
signed/unsigned type) to avoid underflow, or compute items_panel_height once per
loop and reuse it across the keyboard navigation handlers to ensure consistency;
locate the items_panel_height assignment in run.rs and replace the raw
subtraction with a safe saturating_sub sequence (or shared precomputed variable)
so all handlers use the same non-underflowing value.

…ng in ImageManager

- cargo fmt
- fixed the tokio::select! structure and correctly handled stuff
- used Hashmap based cache for imagemanager
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/ui/graphics.rs (4)

26-27: Split state between DISPLAY_STATE and ImageManager.current_rowid risks divergence.

DISPLAY_STATE (global tokio Mutex) and current_rowid (struct field) independently track overlapping concerns. Nothing in ImageManager updates DISPLAY_STATE, so callers must keep them in sync manually. A missed update leaves one stale — e.g. DISPLAY_STATE showing Image("5") while current_rowid is None after a clear().

Consider driving DISPLAY_STATE transitions from within ImageManager methods (load_cclip_image, set_image, clear) so state is always consistent, or consolidate everything into a single source of truth.

Also applies to: 30-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 26 - 27, DISPLAY_STATE and
ImageManager.current_rowid are two independent sources of truth and can diverge;
modify ImageManager so its methods drive DISPLAY_STATE transitions instead of
updating only current_rowid (or collapse to a single state field). Specifically,
update ImageManager::load_cclip_image, ImageManager::set_image, and
ImageManager::clear to acquire the tokio Mutex on DISPLAY_STATE and update it to
the correct DisplayState value whenever current_rowid changes (or remove
current_rowid and use DISPLAY_STATE exclusively), ensuring all reads/writes go
through the same symbol (DISPLAY_STATE or the single consolidated field) to
prevent stale state.

138-138: Redundant use ratatui_image::picker::ProtocolType — already imported at Line 9.

♻️ Proposed fix
     pub fn detect(picker: Option<&Picker>) -> Self {
         if let Some(picker) = picker {
-            use ratatui_image::picker::ProtocolType;
             match picker.protocol_type() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` at line 138, There is a duplicate import of ProtocolType
(use ratatui_image::picker::ProtocolType); remove the redundant import statement
so only the original import at the top of the module remains; ensure no other
code relies on the duplicate and run a build to confirm the single import
satisfies all references to ProtocolType.

33-33: Unbounded cache growth — StatefulProtocol objects accumulate without eviction.

self.cache only ever inserts entries (Line 101) and clear() doesn't purge it. The resizing and encoding is blocking by default, meaning each StatefulProtocol holds pre-encoded image data. For long clipboard sessions with many image items, memory grows without bound.

Consider capping the cache (e.g. a simple LRU with a fixed limit, or evicting on clear()):

♻️ Simple eviction option — flush cache on clear()
     pub fn clear(&mut self) {
         self.current_rowid = None;
+        self.cache.clear();
     }

For a more granular approach, use an LRU crate (e.g. lru) or cap insertions to a fixed N most-recently-used entries.

Also applies to: 100-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` at line 33, The cache field (cache: HashMap<String,
StatefulProtocol>) currently only ever inserts and never evicts, causing
unbounded memory growth because each StatefulProtocol holds pre-encoded image
data; update the cache to a bounded collection or flush it on clear(): either
replace the HashMap with an LRU (e.g., use the lru crate and a fixed capacity)
and update all cache.insert/cache.get usages accordingly, or modify the clear()
method to purge self.cache (and drop StatefulProtocol entries) and add a capped
insertion policy that evicts the least-recently-used entry when adding beyond N;
refer to the cache field, StatefulProtocol type, the clear() method, and the
places using cache.insert/cache.get to make these changes.

107-118: render() silently discards encoding/resize errors — change the signature to Result<()>.

The library explicitly recommends handling the encoding result by calling last_encoding_result() on the StatefulProtocol after each render. The current void signature makes it impossible to surface these errors to the caller (e.g. failed Sixel encoding, Kitty write failure).

♻️ Proposed fix
-    pub fn render(&mut self, f: &mut Frame, area: Rect) {
+    pub fn render(&mut self, f: &mut Frame, area: Rect) -> Result<()> {
         if let Some(rowid) = &self.current_rowid {
             if let Some(protocol) = self.cache.get_mut(rowid) {
                 f.render_stateful_widget(
                     StatefulImage::default().resize(Resize::Fit(None)),
                     area,
                     protocol,
                 );
+                protocol.last_encoding_result()?;
             }
         }
+        Ok(())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 107 - 118, Change render() to return a
Result and propagate encoding/resize errors: update the signature of render(&mut
self, f: &mut Frame, area: Rect) to return Result<(), E> (e.g. anyhow::Error or
Box<dyn std::error::Error>) and after f.render_stateful_widget(..., protocol)
call protocol.last_encoding_result() on the StatefulProtocol instance (the
variable protocol returned from cache.get_mut(rowid)); if last_encoding_result()
is Err, return that error to the caller instead of silently discarding it, and
update any call sites of render() accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ui/graphics.rs`:
- Around line 139-160: In detect(), when a Picker is present and
picker.protocol_type() yields ProtocolType::Halfblocks, short-circuit and return
Self::None (instead of falling through to env-var heuristics) so detect() stays
consistent with supports_graphics() for that Picker; update the match over
picker.protocol_type() to explicitly handle ProtocolType::Halfblocks => return
Self::None to prevent TERM/TERM_PROGRAM from overriding the picker's
authoritative result.

---

Nitpick comments:
In `@src/ui/graphics.rs`:
- Around line 26-27: DISPLAY_STATE and ImageManager.current_rowid are two
independent sources of truth and can diverge; modify ImageManager so its methods
drive DISPLAY_STATE transitions instead of updating only current_rowid (or
collapse to a single state field). Specifically, update
ImageManager::load_cclip_image, ImageManager::set_image, and ImageManager::clear
to acquire the tokio Mutex on DISPLAY_STATE and update it to the correct
DisplayState value whenever current_rowid changes (or remove current_rowid and
use DISPLAY_STATE exclusively), ensuring all reads/writes go through the same
symbol (DISPLAY_STATE or the single consolidated field) to prevent stale state.
- Line 138: There is a duplicate import of ProtocolType (use
ratatui_image::picker::ProtocolType); remove the redundant import statement so
only the original import at the top of the module remains; ensure no other code
relies on the duplicate and run a build to confirm the single import satisfies
all references to ProtocolType.
- Line 33: The cache field (cache: HashMap<String, StatefulProtocol>) currently
only ever inserts and never evicts, causing unbounded memory growth because each
StatefulProtocol holds pre-encoded image data; update the cache to a bounded
collection or flush it on clear(): either replace the HashMap with an LRU (e.g.,
use the lru crate and a fixed capacity) and update all cache.insert/cache.get
usages accordingly, or modify the clear() method to purge self.cache (and drop
StatefulProtocol entries) and add a capped insertion policy that evicts the
least-recently-used entry when adding beyond N; refer to the cache field,
StatefulProtocol type, the clear() method, and the places using
cache.insert/cache.get to make these changes.
- Around line 107-118: Change render() to return a Result and propagate
encoding/resize errors: update the signature of render(&mut self, f: &mut Frame,
area: Rect) to return Result<(), E> (e.g. anyhow::Error or Box<dyn
std::error::Error>) and after f.render_stateful_widget(..., protocol) call
protocol.last_encoding_result() on the StatefulProtocol instance (the variable
protocol returned from cache.get_mut(rowid)); if last_encoding_result() is Err,
return that error to the caller instead of silently discarding it, and update
any call sites of render() accordingly.

ui(graphics): consolidate imports and remove redundant state logic
cclip: handle render errors and remove redundant display state updates
- the code runs stfu coderabbit
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/ui/graphics.rs (3)

143-160: Render-time resize/encode is still blocking on the draw thread.

The resizing and encoding done by StatefulImage is blocking, and since it happens at render-time, it is a good idea to offload that to another thread or async task if the UI must be responsive. The spawn_blocking in load_cclip_image only covers image::load_from_memory; the first-render encode (and any re-encode after a terminal resize) still runs synchronously inside the ratatui draw callback here.

For large images or frequent resize events, consider the needs_resize + background resize_encode pattern from examples/async.rs in the ratatui-image repo to move this work off the draw thread.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 143 - 160, The render method is performing
blocking resize/encode work inside the draw thread via StatefulImage (see render
and StatefulImage::default().resize(...)), so refactor to offload that work:
detect when an image needs (re)encoding/resizing (use a needs_resize flag on the
cached protocol entry keyed by current_rowid), spawn a background task
(spawn_blocking or async task) to run the resize/encode logic (see
load_cclip_image which already spawn_blocking image::load_from_memory) and
expose a resize_encode function to update the cache entry when complete; then
make render only paint ready frames from cache (skip/queue when
protocol.last_encoding_result() is pending) so the heavy work no longer runs
synchronously in render.

162-167: clear() destroys the entire LRU cache — consider a separate hide() method.

clear() is the replacement for image_hide(), but it also wipes all 50 cached StatefulProtocol entries. Any previously loaded image must be fully re-fetched and re-decoded the next time it's previewed, defeating the purpose of the LRU cache.

Consider splitting the responsibility:

♻️ Proposed refactor
+    /// Hide the current image without evicting the cache.
+    pub fn hide(&mut self) {
+        self.current_rowid = None;
+        self.update_display_state(DisplayState::Empty);
+    }
+
     pub fn clear(&mut self) {
         self.current_rowid = None;
         self.cache.clear();
         self.cache_order.clear();
         self.update_display_state(DisplayState::Empty);
     }

Use hide() for dismiss/navigation and reserve clear() for full teardown (e.g., on mode exit).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 162 - 167, The clear() method currently
nukes the entire LRU cache (cache and cache_order) when used as an image_hide
replacement; instead add a new hide() method that only sets self.current_rowid =
None and calls self.update_display_state(DisplayState::Empty) without touching
self.cache or self.cache_order, and leave clear() as the full-teardown that
still clears cache and cache_order; update any callsites that intend to merely
hide/dismiss previews to call hide() (not clear()), and keep clear() for
mode-exit/full reset semantics.

101-115: No timeout on wait_with_output() — hung cclip get leaves Loading state permanently.

wait_with_output().await blocks the async task indefinitely if the subprocess stalls. The TUI itself stays responsive (other tasks continue), but DISPLAY_STATE never transitions out of Loading(rowid) and the task leaks. Wrapping with tokio::time::timeout bounds this:

♻️ Proposed fix
+        use tokio::time::{timeout, Duration};
+
-        let output = child.wait_with_output().await?;
+        let output = timeout(Duration::from_secs(5), child.wait_with_output())
+            .await
+            .map_err(|_| eyre!("cclip get timed out for rowid: {}", rowid))??;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/graphics.rs` around lines 101 - 115, The code calls
child.wait_with_output().await for the "cclip" process (spawned via
tokio::process::Command::new("cclip")...) which can block indefinitely; wrap the
await in tokio::time::timeout with a sensible duration, and on timeout ensure
you kill the child process (child.kill()/child.wait() or use Child::start_kill)
and return an error so DISPLAY_STATE doesn’t remain Loading(rowid); update the
logic around wait_with_output() to handle Ok(output), Err(timeout), and Err(io)
cases and include the timeout branch in the existing error handling that
currently returns eyre!("cclip get failed for rowid: {}", rowid).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ui/graphics.rs`:
- Around line 80-85: The sync update_display_state currently uses
DISPLAY_STATE.try_lock() and silently skips the update under async contention;
change it to perform a fire-and-forget async update so the write always occurs:
in update_display_state(state: DisplayState) spawn a detached tokio task (e.g.
tokio::spawn or tokio::task::spawn_local as appropriate) that awaits
DISPLAY_STATE.lock().await and sets *lock = state; this ensures the sync
clear()/DisplayState::Empty update is not lost when update_display_state_async
or load_cclip_image holds the tokio::sync::Mutex.

---

Nitpick comments:
In `@src/ui/graphics.rs`:
- Around line 143-160: The render method is performing blocking resize/encode
work inside the draw thread via StatefulImage (see render and
StatefulImage::default().resize(...)), so refactor to offload that work: detect
when an image needs (re)encoding/resizing (use a needs_resize flag on the cached
protocol entry keyed by current_rowid), spawn a background task (spawn_blocking
or async task) to run the resize/encode logic (see load_cclip_image which
already spawn_blocking image::load_from_memory) and expose a resize_encode
function to update the cache entry when complete; then make render only paint
ready frames from cache (skip/queue when protocol.last_encoding_result() is
pending) so the heavy work no longer runs synchronously in render.
- Around line 162-167: The clear() method currently nukes the entire LRU cache
(cache and cache_order) when used as an image_hide replacement; instead add a
new hide() method that only sets self.current_rowid = None and calls
self.update_display_state(DisplayState::Empty) without touching self.cache or
self.cache_order, and leave clear() as the full-teardown that still clears cache
and cache_order; update any callsites that intend to merely hide/dismiss
previews to call hide() (not clear()), and keep clear() for mode-exit/full reset
semantics.
- Around line 101-115: The code calls child.wait_with_output().await for the
"cclip" process (spawned via tokio::process::Command::new("cclip")...) which can
block indefinitely; wrap the await in tokio::time::timeout with a sensible
duration, and on timeout ensure you kill the child process
(child.kill()/child.wait() or use Child::start_kill) and return an error so
DISPLAY_STATE doesn’t remain Loading(rowid); update the logic around
wait_with_output() to handle Ok(output), Err(timeout), and Err(io) cases and
include the timeout branch in the existing error handling that currently returns
eyre!("cclip get failed for rowid: {}", rowid).

- Guard up/down nav against empty ui.shown to avoid underflow
- Set DISPLAY_STATE to Image(rowid) when load_cclip_image succeeds
- get_cclip_rowid: use trim().is_empty() and parse from trimmed
- update_display_state: log when try_lock fails
- Add 5s timeout for cclip get and return clear error on timeout
@Mjoyufull Mjoyufull self-assigned this Feb 23, 2026
@Mjoyufull
Copy link
Owner Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 23, 2026

@cubic-dev-ai

@Mjoyufull I have started the AI code review. It will take a few minutes to complete.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/ui/graphics.rs">

<violation number="1" location="src/ui/graphics.rs:85">
P2: `eprintln!` in a TUI context can corrupt the terminal display. More importantly, silently failing to update `DISPLAY_STATE` (while `current_rowid` is already updated by callers like `set_image`/`clear`) creates an inconsistency between local and global state. Consider either blocking (`.blocking_lock()`) or at least not printing to stderr. If `try_lock` fails, the state divergence should be handled more robustly (e.g., retry or mark state as dirty).</violation>
</file>

<file name="src/modes/dmenu/run.rs">

<violation number="1" location="src/modes/dmenu/run.rs:88">
P2: Unnecessary `Picker::from_query_stdio()` in dmenu mode adds startup latency. This method sends terminal escape sequences and waits for responses (with potential timeouts), but dmenu mode never renders images — the picker is only used for `GraphicsAdapter::detect()` to decide whether to apply Kitty-specific `Clear` rendering. The env-var–based fallback in `GraphicsAdapter::detect(None)` already handles Kitty detection and would be sufficient here.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…err noise

- dmenu: optimize startup by removing unnecessary picker query
- ui(graphics): fix mutex panic by switching to std::sync::Mutex
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/ui/graphics.rs">

<violation number="1" location="src/ui/graphics.rs:82">
P2: Silently ignoring a poisoned `Mutex` will cause display state updates to be lost without any diagnostic output. Other callers of `DISPLAY_STATE` in `run.rs` use `.lock().unwrap()` (which panics on poison), making the error-handling strategy inconsistent across the codebase. Consider recovering the inner value from the poison error to keep the state consistent:
```rust
let mut lock = DISPLAY_STATE.lock().unwrap_or_else(|e| e.into_inner());
*lock = state;

This ensures the state is always updated, even after a panic in another thread.

P1: Inside `tokio::spawn`, `.unwrap()` on a potentially poisoned mutex will silently kill the task. Since this code path uses `catch_unwind` (panics are expected), mutex poisoning is a realistic scenario here. Use `if let Ok(mut state) = ... { }` to match the pattern in `graphics.rs::update_display_state`. P1: Using `.unwrap()` on `std::sync::Mutex::lock()` will panic if the mutex is ever poisoned (e.g., a thread panicked while holding it). This is especially risky inside `tokio::spawn` error-handling paths where it could silently crash the task. The codebase already uses the graceful pattern `if let Ok(mut lock) = DISPLAY_STATE.lock()` in `graphics.rs` and `dmenu_ui.rs` — prefer that here too, or use `.lock().unwrap_or_else(|e| e.into_inner())` to recover from poison. ```

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@Mjoyufull
Copy link
Owner Author

LGTM

@Mjoyufull Mjoyufull merged commit 795236e into dev Feb 23, 2026
1 of 5 checks passed
@Mjoyufull Mjoyufull deleted the fix/cclip branch February 23, 2026 05:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant