Skip to content

Commit 53dd949

Browse files
feat(extensions): restyle remaining extension surfaces
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 7ec4704 commit 53dd949

9 files changed

Lines changed: 1741 additions & 741 deletions

File tree

crates/photoncast-clipboard/src/ui/clipboard_item.rs

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use gpui::{
1010

1111
use crate::models::{ClipboardContentType, ClipboardItem};
1212

13+
/// Maximum detail preview text length.
14+
pub(crate) const DETAIL_PREVIEW_LENGTH: usize = 420;
15+
1316
/// Catppuccin Mocha color palette.
1417
mod colors {
1518
pub const BASE: u32 = 0x1E_1E2E;
@@ -158,7 +161,7 @@ impl Render for ClipboardItemView {
158161
rgb(colors::BASE)
159162
};
160163

161-
let preview = self.item.preview();
164+
let preview = privacy_safe_preview(&self.item.content_type, MAX_PREVIEW_LENGTH);
162165
let time = self.relative_time();
163166
let pinned = self.item.is_pinned;
164167

@@ -301,13 +304,101 @@ fn render_metadata(pinned: bool, time: String) -> impl IntoElement {
301304
)
302305
}
303306

304-
/// Truncates text to a maximum length.
307+
/// Returns a display label for a clipboard content type.
308+
pub(crate) const fn content_type_label(content_type: &ClipboardContentType) -> &'static str {
309+
match content_type {
310+
ClipboardContentType::Text { .. } => "Text",
311+
ClipboardContentType::RichText { .. } => "Rich text",
312+
ClipboardContentType::Image { .. } => "Image",
313+
ClipboardContentType::File { .. } => "File",
314+
ClipboardContentType::Link { .. } => "Link",
315+
ClipboardContentType::Color { .. } => "Color",
316+
}
317+
}
318+
319+
/// Returns a truthful preview without rendering image pixels or full filesystem paths.
320+
pub(crate) fn privacy_safe_preview(content_type: &ClipboardContentType, max_len: usize) -> String {
321+
match content_type {
322+
ClipboardContentType::Text { content, .. } => truncate_text(content, max_len),
323+
ClipboardContentType::RichText { plain, .. } => truncate_text(plain, max_len),
324+
ClipboardContentType::Image {
325+
dimensions,
326+
size_bytes,
327+
..
328+
} => format!(
329+
"Image preview hidden • {}x{} • {}",
330+
dimensions.0,
331+
dimensions.1,
332+
format_bytes(*size_bytes)
333+
),
334+
ClipboardContentType::File {
335+
paths, total_size, ..
336+
} => format!(
337+
"{} • {}",
338+
file_names_summary(paths, 3),
339+
format_bytes(*total_size)
340+
),
341+
ClipboardContentType::Link { url, title, .. } => {
342+
let label = title.as_deref().unwrap_or(url);
343+
truncate_text(label, max_len)
344+
},
345+
ClipboardContentType::Color {
346+
hex, display_name, ..
347+
} => truncate_text(display_name.as_deref().unwrap_or(hex), max_len),
348+
}
349+
}
350+
351+
/// Summarizes file names without exposing full directory paths.
352+
pub(crate) fn file_names_summary(paths: &[std::path::PathBuf], max_names: usize) -> String {
353+
let names: Vec<_> = paths
354+
.iter()
355+
.filter_map(|path| path.file_name())
356+
.filter_map(|name| name.to_str())
357+
.take(max_names)
358+
.collect();
359+
360+
let base = if names.is_empty() {
361+
format!(
362+
"{} file{}",
363+
paths.len(),
364+
if paths.len() == 1 { "" } else { "s" }
365+
)
366+
} else {
367+
names.join(", ")
368+
};
369+
370+
if paths.len() > max_names {
371+
format!("{} +{} more", base, paths.len() - max_names)
372+
} else {
373+
base
374+
}
375+
}
376+
377+
/// Formats byte counts for UI metadata.
378+
pub(crate) fn format_bytes(size_bytes: u64) -> String {
379+
const KB: u64 = 1024;
380+
const MB: u64 = 1024 * 1024;
381+
382+
if size_bytes < KB {
383+
format!("{} B", size_bytes)
384+
} else if size_bytes < MB {
385+
let whole = size_bytes / KB;
386+
let decimal = (size_bytes % KB) * 10 / KB;
387+
format!("{whole}.{decimal} KB")
388+
} else {
389+
let whole = size_bytes / MB;
390+
let decimal = (size_bytes % MB) * 10 / MB;
391+
format!("{whole}.{decimal} MB")
392+
}
393+
}
394+
395+
/// Truncates text to a maximum character length.
305396
fn truncate_text(text: &str, max_len: usize) -> String {
306-
// Normalize whitespace
307397
let normalized: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
308398

309-
if normalized.len() > max_len {
310-
format!("{}...", &normalized[..max_len])
399+
if normalized.chars().count() > max_len {
400+
let truncated: String = normalized.chars().take(max_len).collect();
401+
format!("{}...", truncated)
311402
} else {
312403
normalized
313404
}
@@ -388,6 +479,46 @@ mod tests {
388479
assert!(!no_match.has_highlights());
389480
}
390481

482+
#[test]
483+
fn test_privacy_safe_preview_does_not_expose_image_path() {
484+
let content = ClipboardContentType::image(
485+
std::path::PathBuf::from("/private/source/secret.png"),
486+
std::path::PathBuf::from("/private/thumb/secret.png"),
487+
2048,
488+
(640, 480),
489+
);
490+
491+
let preview = privacy_safe_preview(&content, DETAIL_PREVIEW_LENGTH);
492+
493+
assert_eq!(preview, "Image preview hidden • 640x480 • 2.0 KB");
494+
assert!(!preview.contains("/private"));
495+
}
496+
497+
#[test]
498+
fn test_file_preview_uses_names_not_paths() {
499+
let content = ClipboardContentType::file(
500+
vec![
501+
std::path::PathBuf::from("/tmp/photoncast-task-8-safe-alpha.txt"),
502+
std::path::PathBuf::from("/Users/example/private/beta.txt"),
503+
],
504+
Vec::new(),
505+
1536,
506+
);
507+
508+
let preview = privacy_safe_preview(&content, DETAIL_PREVIEW_LENGTH);
509+
510+
assert_eq!(
511+
preview,
512+
"photoncast-task-8-safe-alpha.txt, beta.txt • 1.5 KB"
513+
);
514+
assert!(!preview.contains("/Users/example/private"));
515+
}
516+
517+
#[test]
518+
fn test_text_preview_truncates_on_char_boundaries() {
519+
assert_eq!(truncate_text("αβγδε", 3), "αβγ...");
520+
}
521+
391522
#[test]
392523
fn test_highlighted_text_empty_query() {
393524
let ht = HighlightedText::from_search("Hello World", "");

0 commit comments

Comments
 (0)