Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/instructions/Base.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ applyTo: "**"
---

- 仔细阅读整个项目以及 doc 文件夹中的文档。
- 遵循良好的 Rust 开发规范
- 遵循良好的 Rust 开发规范及最佳时间,满足 clippy 严格要求
- 编写可以运行的单元测试。
- 使用 MCP 工具 context7 查询库的文档。
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ tempfile = "3.24.0"
winres = "0.1"
image = "0.25"

[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
print_stdout = "warn"
print_stderr = "warn"
# Common allows for pedantic
module_name_repetitions = "allow"
cast_possible_truncation = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"

# The profile that 'dist' will build with
[profile.dist]
inherits = "release"
Expand Down
2 changes: 0 additions & 2 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# git-cliff configuration for generating CHANGELOG.md with GitHub usernames and contributors
# see: https://git-cliff.org/docs/integration/github

[remote.github]
# Project GitHub repo used for templating contributors/PR links
owner = "StudentWeis"
repo = "ropy"

Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[toolchain]
channel = "stable"
channel = "1.93"
37 changes: 27 additions & 10 deletions src/clipboard/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ impl ClipboardMonitor {
tx: Sender<ClipboardEvent>,
image_tx: Sender<DynamicImage>,
last_copy: Arc<Mutex<LastCopyState>>,
) -> Self {
let ctx = ClipboardContext::new().unwrap();
Self {
) -> Option<Self> {
let ctx = match ClipboardContext::new() {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("[ropy] Failed to initialize clipboard context: {e}");
return None;
}
};
Some(Self {
tx,
image_tx,
last_copy,
ctx,
}
last_copy,
})
}
}

Expand Down Expand Up @@ -76,17 +82,19 @@ impl ClipboardHandler for ClipboardMonitor {
/// Spawn a clipboard listener thread that watches for clipboard changes.
pub fn start_clipboard_monitor(
tx: Sender<ClipboardEvent>,
async_app: AsyncApp,
async_app: &AsyncApp,
last_copy: Arc<Mutex<LastCopyState>>,
) {
let (image_tx, image_rx) = async_channel::unbounded::<DynamicImage>();
let monitor = ClipboardMonitor::new(tx.clone(), image_tx, last_copy);
let Some(monitor) = ClipboardMonitor::new(tx.clone(), image_tx, last_copy) else {
return;
};
let executor = async_app.background_executor();

executor
.spawn(async move {
while let Ok(image) = image_rx.recv().await {
if let Some(path) = super::save_image(image) {
if let Some(path) = super::save_image(&image) {
let _ = tx.send_blocking(ClipboardEvent::Image(path));
}
}
Expand All @@ -95,7 +103,13 @@ pub fn start_clipboard_monitor(

executor
.spawn(async move {
let mut watcher = ClipboardWatcherContext::new().unwrap();
let mut watcher = match ClipboardWatcherContext::new() {
Ok(w) => w,
Err(e) => {
eprintln!("[ropy] Failed to create clipboard watcher: {e}");
return;
}
};
watcher.add_handler(monitor);
watcher.start_watch();
})
Expand Down Expand Up @@ -132,7 +146,10 @@ pub fn start_clipboard_listener(
};
guard.insert(0, record);
let max_history_records = {
let settings_guard = settings.read().unwrap();
let settings_guard = match settings.read() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
settings_guard.storage.max_history_records
};
if guard.len() > max_history_records {
Expand Down
2 changes: 1 addition & 1 deletion src/clipboard/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use chrono::Local;
use image::DynamicImage;

pub fn save_image(image: DynamicImage) -> Option<String> {
pub fn save_image(image: &DynamicImage) -> Option<String> {
let data_dir = dirs::data_local_dir()?.join("ropy").join("images");
if !data_dir.exists() {
std::fs::create_dir_all(&data_dir).ok()?;
Expand Down
22 changes: 14 additions & 8 deletions src/clipboard/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ use image::ImageReader;
use super::CopyRequest;

/// Start a background task to handle clipboard write requests.
/// This avoids creating a new ClipboardContext and spawning a new task for each write.
pub fn start_clipboard_writer(async_app: AsyncApp) -> async_channel::Sender<CopyRequest> {
/// This avoids creating a new `ClipboardContext` and spawning a new task for each write.
pub fn start_clipboard_writer(async_app: &AsyncApp) -> async_channel::Sender<CopyRequest> {
let (tx, rx) = async_channel::unbounded();
let executor = async_app.background_executor();

executor
.spawn(async move {
let ctx = ClipboardContext::new().unwrap();
let ctx = match ClipboardContext::new() {
Ok(ctx) => ctx,
Err(e) => {
eprintln!("[ropy] Failed to create clipboard output context: {e}");
return;
}
};
while let Ok(req) = rx.recv().await {
match req {
CopyRequest::Text(text) => {
set_text(&ctx, text);
}
CopyRequest::Image(path) => {
set_image(&ctx, path);
set_image(&ctx, &path);
}
}
}
Expand All @@ -35,10 +41,10 @@ fn set_text(ctx: &ClipboardContext, text: String) {

/// Set image to clipboard. The image is read from the given file path.
/// After setting the image, the original file and its thumbnail are deleted.
fn set_image(ctx: &ClipboardContext, path: String) {
let img_res = ImageReader::open(&path)
fn set_image(ctx: &ClipboardContext, path: &str) {
let img_res = ImageReader::open(path)
.map_err(image::ImageError::from)
.and_then(|r| r.decode());
.and_then(image::ImageReader::decode);
if let Ok(img) = img_res {
#[cfg(target_os = "macos")]
{
Expand All @@ -59,7 +65,7 @@ fn set_image(ctx: &ClipboardContext, path: String) {
.is_ok()
&& let Err(e) = ctx.set_buffer("public.png", bytes)
{
eprintln!("Failed to set image to clipboard: {}", e);
eprintln!("Failed to set image to clipboard: {e}");
}
}

Expand Down
29 changes: 15 additions & 14 deletions src/config/autostart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ pub struct AutoStartManager {
}

impl AutoStartManager {
/// Create a new AutoStartManager instance
/// Create a new `AutoStartManager` instance
///
/// # Arguments
/// * `app_name` - The name of the application
///
/// # Returns
/// Result containing AutoStartManager or AutoStartError
/// Result containing `AutoStartManager` or `AutoStartError`
pub fn new(app_name: &str) -> Result<Self, AutoStartError> {
let app_path = Self::get_app_path()?;

Expand All @@ -49,7 +49,7 @@ impl AutoStartManager {
/// Get the application executable path
///
/// For development builds, returns the debug executable path
/// For bundled macOS apps, returns the .app bundle path
/// For bundled `macOS` apps, returns the `.app` bundle path
fn get_app_path() -> Result<String, AutoStartError> {
// Get the current executable path
let exe_path =
Expand All @@ -67,9 +67,12 @@ impl AutoStartManager {
}

// For non-bundled or Windows builds, return the executable path
exe_path.to_str().map(|s| s.to_string()).ok_or_else(|| {
AutoStartError::ExecutablePath("Path contains invalid UTF-8".to_string())
})
exe_path
.to_str()
.map(std::string::ToString::to_string)
.ok_or_else(|| {
AutoStartError::ExecutablePath("Path contains invalid UTF-8".to_string())
})
}

/// Enable auto-start at system startup
Expand Down Expand Up @@ -99,14 +102,12 @@ impl AutoStartManager {
/// * `enabled` - Whether auto-start should be enabled
pub fn sync_state(&self, enabled: bool) -> Result<(), AutoStartError> {
let current_enabled = self.is_enabled().unwrap_or(false);
if current_enabled != enabled {
if enabled {
self.enable()
} else {
self.disable()
}
} else {
if current_enabled == enabled {
Ok(())
} else if enabled {
self.enable()
} else {
self.disable()
}
}
}
Expand Down Expand Up @@ -138,7 +139,7 @@ mod tests {

// Verify state if possible
if let Ok(enabled) = manager.is_enabled() {
assert!(!enabled)
assert!(!enabled);
}
}
}
24 changes: 14 additions & 10 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ pub enum AppTheme {
impl AppTheme {
pub fn get_theme(&self) -> Self {
match self {
AppTheme::System => match dark_light::detect().unwrap_or(dark_light::Mode::Light) {
dark_light::Mode::Dark => AppTheme::Dark,
dark_light::Mode::Light => AppTheme::Light,
_ => AppTheme::Light,
Self::System => match dark_light::detect().unwrap_or(dark_light::Mode::Light) {
dark_light::Mode::Dark => Self::Dark,
_ => Self::Light,
},
_ => self.clone(),
}
Expand All @@ -42,7 +41,7 @@ impl AppTheme {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HotkeySettings {
/// Global hotkey to activate clipboard manager (e.g., "cmd+shift+v")
/// Global hotkey to activate clipboard manager (e.g., "`cmd+shift+v`")
pub activation_key: String,
}

Expand Down Expand Up @@ -102,11 +101,16 @@ impl Settings {
std::fs::create_dir_all(&config_dir).map_err(|e| ConfigError::Foreign(Box::new(e)))?;
}

let builder = Config::builder()
let mut builder = Config::builder()
// Start with default values
.add_source(Config::try_from(&Settings::default())?)
// Add configuration from file (optional)
.add_source(File::with_name(config_file.to_str().unwrap()).required(false));
.add_source(Config::try_from(&Self::default())?);

// Add configuration from file (optional)
if let Some(path_str) = config_file.to_str() {
builder = builder.add_source(File::with_name(path_str).required(false));
} else {
eprintln!("[ropy] Warning: Config file path contains invalid UTF-8 characters");
}

let config = builder.build()?;
let mut settings: Self = config.try_deserialize()?;
Expand All @@ -115,7 +119,7 @@ impl Settings {
if settings.hotkey.activation_key.is_empty()
|| global_hotkey::hotkey::HotKey::from_str(&settings.hotkey.activation_key).is_err()
{
settings.hotkey.activation_key = Settings::default().hotkey.activation_key;
settings.hotkey.activation_key = Self::default().hotkey.activation_key;
}

Ok(settings)
Expand Down
Loading