Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ once_cell = "1.21.3"
rand = "0.9.2"
rayon = "1.11.0"
rfd = "0.17.2"
rusqlite = { version = "0.39.0", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tokio = { version = "1.48.0", features = ["full"] }
Expand Down
77 changes: 77 additions & 0 deletions src/app/pages/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,83 @@ fn viewport_content(content: &ClipBoardContentType, theme: &Theme) -> Element<'s
.width(Length::Fill)
.into()
}
ClipBoardContentType::Files(files, img_opt) => {
let is_single_image = files.len() == 1 && {
let p = std::path::Path::new(&files[0]);
if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
matches!(
ext.to_lowercase().as_str(),
"png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" | "ico" | "tiff"
)
} else {
false
}
};

if is_single_image {
container(
Viewer::new(Handle::from_path(&files[0]))
.content_fit(ContentFit::ScaleDown)
.scale_step(0.)
.max_scale(1.)
.min_scale(1.),
)
.padding(10)
.style(|_| container::Style {
border: iced::Border {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

move this styling to styles.rs

color: iced::Color::WHITE,
width: 1.,
radius: Radius::new(0.),
},
..Default::default()
})
.width(Length::Fill)
.into()
} else if let Some(data) = img_opt {
let bytes = data.to_owned_img().into_owned_bytes();
container(
Viewer::new(
Handle::from_rgba(data.width as u32, data.height as u32, bytes.to_vec())
.clone(),
)
.content_fit(ContentFit::ScaleDown)
.scale_step(0.)
.max_scale(1.)
.min_scale(1.),
)
.padding(10)
.style(|_| container::Style {
border: iced::Border {
color: iced::Color::WHITE,
width: 1.,
radius: Radius::new(0.),
},
..Default::default()
})
.width(Length::Fill)
.into()
} else {
Scrollable::with_direction(
container(
Text::new(files.join("\n"))
.height(Length::Fill)
.width(Length::Fill)
.align_x(Alignment::Start)
.font(theme.font())
.size(16),
)
.width(Length::Fill)
.height(Length::Fill),
Direction::Both {
vertical: Scrollbar::hidden(),
horizontal: Scrollbar::hidden(),
},
)
.height(Length::Fill)
.width(Length::Fill)
.into()
}
}
};

let theme_clone = theme.clone();
Expand Down
11 changes: 9 additions & 2 deletions src/app/tile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use tray_icon::TrayIcon;
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

/// This is a wrapper around the sender to disable dropping
Expand Down Expand Up @@ -182,6 +183,7 @@ pub struct Tile {
page: Page,
pub height: f32,
pub file_search_sender: Option<tokio::sync::watch::Sender<(String, Vec<String>)>>,
pub db: Arc<crate::database::Database>,
debouncer: Debouncer,
}

Expand Down Expand Up @@ -360,8 +362,13 @@ fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
let mut prev_byte_rep: Option<ClipBoardContentType> = None;

loop {
let byte_rep = if let Ok(a) = clipboard.get_image() {
Some(ClipBoardContentType::Image(a))
let files_opt = crate::platform::get_copied_files();
let img_opt = clipboard.get_image().ok();

let byte_rep = if let Some(files) = files_opt {
Some(ClipBoardContentType::Files(files, img_opt))
} else if let Some(img) = img_opt {
Some(ClipBoardContentType::Image(img))
} else if let Ok(a) = clipboard.get_text()
&& !a.trim().is_empty()
{
Expand Down
15 changes: 7 additions & 8 deletions src/app/tile/elm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//! architecture. If the subscription function becomes too large, it should be moved to this file

use std::collections::HashMap;
use std::fs;

use global_hotkey::hotkey::HotKey;
use iced::border::Radius;
Expand Down Expand Up @@ -33,6 +32,7 @@ use crate::{
config::Config,
platform::transform_process_to_ui_element,
};
use std::sync::Arc;

/// Initialise the base window
pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
Expand Down Expand Up @@ -78,12 +78,10 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
shells: shells_map,
};

let home = std::env::var("HOME").unwrap_or("/".to_string());

let ranking = toml::from_str(
&fs::read_to_string(home + "/.config/rustcast/ranking.toml").unwrap_or("".to_string()),
)
.unwrap_or(HashMap::new());
let db =
Arc::new(crate::database::Database::new().expect("Failed to initialize SQLite database"));
let ranking = db.get_rankings().unwrap_or_default();
let clipboard_content = db.get_clipboard_history(100).unwrap_or_default();

(
Tile {
Expand All @@ -102,12 +100,13 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
config: config.clone(),
ranking,
theme: config.theme.to_owned().clone().into(),
clipboard_content: vec![],
clipboard_content,
tray_icon: None,
sender: None,
page: Page::Main,
height: DEFAULT_WINDOW_HEIGHT,
file_search_sender: None,
db,
debouncer: Debouncer::new(config.debounce_delay),
},
Task::batch([open.map(|_| Message::OpenWindow)]),
Expand Down
42 changes: 32 additions & 10 deletions src/app/tile/update.rs
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What about the Message::UpdateRankings and other ranking related messages?

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use iced::widget::operation::AbsoluteOffset;
use iced::window;
use iced::window::Id;
use log::info;
use rayon::iter::IntoParallelRefIterator;
use crate::clipboard::ClipBoardContentType;
use rayon::iter::ParallelIterator;
use rayon::slice::ParallelSliceMut;

Expand Down Expand Up @@ -239,10 +239,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {

Message::SaveRanking => {
tile.ranking = tile.options.get_rankings();
let string_rep = toml::to_string(&tile.ranking).unwrap_or("".to_string());
let ranking_file_path =
std::env::var("HOME").unwrap_or("/".to_string()) + "/.config/rustcast/ranking.toml";
fs::write(ranking_file_path, string_rep).ok();
for (name, rank) in &tile.ranking {
let _ = tile.db.save_ranking(name, *rank);
}
Task::none()
}

Expand Down Expand Up @@ -479,16 +478,31 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
Message::EditClipboardHistory(action) => {
match action {
Editable::Create(content) => {
if !tile.clipboard_content.contains(&content) {
tile.clipboard_content.insert(0, content);
let old_item = tile.clipboard_content.iter().find(|x| {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do you have to check for files seperately instead of just doing a normal == ?

if let (ClipBoardContentType::Files(f1, _), ClipBoardContentType::Files(f2, _)) = (x, &content) {
f1 == f2
} else {
*x == &content
}
}).cloned();

if old_item.is_none() {
tile.clipboard_content.insert(0, content.clone());
let _ = tile.db.save_clipboard_item(&content);
return Task::none();
}

let new_content_vec = tile
.clipboard_content
.par_iter()
.iter()
.filter_map(|x| {
if *x == content {
let is_match = if let (ClipBoardContentType::Files(f1, _), ClipBoardContentType::Files(f2, _)) = (x, &content) {
f1 == f2
} else {
x == &content
};

if is_match {
None
} else {
Some(x.to_owned())
Expand All @@ -497,7 +511,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
.collect();

tile.clipboard_content = new_content_vec;
tile.clipboard_content.insert(0, content);
tile.clipboard_content.insert(0, content.clone());
if let Some(old) = old_item {
let _ = tile.db.delete_clipboard_item(&old);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is there a need to delete the item if you update it when u save, since the SQL already does that..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Building on the point about the unnecessary delete — there are ~8 instances of let _ = silently discarding DB errors across this file. If a write fails (disk full, corruption), the in-memory state diverges from disk and items vanish on restart with no indication.

The codebase already uses the log crate. Swap these to:

if let Err(e) = tile.db.save_clipboard_item(&content) {
    log::error!("Failed to save clipboard item: {}", e);
}

}
let _ = tile.db.save_clipboard_item(&content);
}
Editable::Delete(content) => {
tile.clipboard_content = tile
Expand All @@ -511,13 +529,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
}
})
.collect();
let _ = tile.db.delete_clipboard_item(&content);
}
Editable::Update { old, new } => {
tile.clipboard_content = tile
.clipboard_content
.iter()
.map(|x| if x == &old { new.clone() } else { x.to_owned() })
.collect();
let _ = tile.db.delete_clipboard_item(&old);
let _ = tile.db.save_clipboard_item(&new);
}
}
Task::none()
Expand Down Expand Up @@ -765,6 +786,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {

Message::ClearClipboardHistory => {
tile.clipboard_content.clear();
let _ = tile.db.clear_clipboard();
Task::none()
}

Expand Down
44 changes: 40 additions & 4 deletions src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,40 @@ use crate::{
pub enum ClipBoardContentType {
Text(String),
Image(ImageData<'static>),
Files(Vec<String>, Option<ImageData<'static>>),
}

impl ToApp for ClipBoardContentType {
/// Returns the iced element for rendering the clipboard item, and the entire content since the
/// display name is only the first line
fn to_app(&self) -> App {
let mut display_name = match self {
ClipBoardContentType::Image(_) => "Image".to_string(),
ClipBoardContentType::Text(a) => a.get(0..25).unwrap_or(a).to_string(),
let (mut display_name, desc) = match self {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems like a weird pattern. Why do you do a tuple deconstruction for desc? just define it as a variable earlier / later when its needed?

ClipBoardContentType::Image(_) => ("Image".to_string(), "Clipboard Item".to_string()),
ClipBoardContentType::Text(a) => (
a.get(0..25).unwrap_or(a).to_string(),
"Clipboard Item".to_string(),
),
ClipBoardContentType::Files(f, _) => {
if f.len() == 1 {
let path = std::path::Path::new(&f[0]);
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
// Fall back to the raw path string if the file name was entirely empty
let mut final_name = name;
if final_name.is_empty() {
final_name = f[0].clone();
}
(final_name, f[0].clone())
} else {
(
format!("{} Files", f.len()),
"Multiple files copied".to_string(),
)
}
}
};

let self_clone = self.clone();
Expand All @@ -33,7 +58,7 @@ impl ToApp for ClipBoardContentType {
open_command: crate::app::apps::AppCommand::Function(Function::CopyToClipboard(
self_clone.to_owned(),
)),
desc: "Clipboard Item".to_string(),
desc,
icons: None,
display_name,
search_name,
Expand All @@ -52,6 +77,17 @@ impl PartialEq for ClipBoardContentType {
&& let Self::Image(other_image_data) = other
{
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not use a match statement for this entire thing rather than if else?

return image_data.bytes == other_image_data.bytes;
} else if let Self::Files(f1, img1) = self
&& let Self::Files(f2, img2) = other
{
if f1 != f2 {
return false;
}
return match (img1, img2) {
(Some(a), Some(b)) => a.bytes == b.bytes,
(None, None) => true,
_ => false,
};
}
false
}
Expand Down
3 changes: 3 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ impl Function {
ClipBoardContentType::Image(img) => {
Clipboard::new().unwrap().set_image(img.to_owned_img()).ok();
}
ClipBoardContentType::Files(files, _) => {
crate::platform::put_copied_files(files);
}
},

Function::Quit => std::process::exit(0),
Expand Down
Loading
Loading