Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ objc2 = "0.6.3"
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.61.2", features = ["Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse"] }

[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"
x11rb = "0.13"

[dev-dependencies]
tempfile = "3.20"

Expand Down
2 changes: 2 additions & 0 deletions src/config/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ impl Default for Settings {
activation_key: "control+shift+d".to_string(),
#[cfg(target_os = "windows")]
activation_key: "ctrl+shift+d".to_string(),
#[cfg(target_os = "linux")]
activation_key: "ctrl+shift+d".to_string(),
},
storage: StorageSettings {
max_history_records: 100,
Expand Down
69 changes: 66 additions & 3 deletions src/gui/app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::clipboard::{self, ClipboardEvent, LastCopyState};
use crate::config::{AppTheme, AutoStartManager, Settings};
use crate::gui::board::RopyBoard;
use crate::gui::tray::start_tray_handler;
use crate::gui::tray::start_tray_handler_inner;
use crate::gui::x11::X11;
use crate::repository::{ClipboardRecord, ClipboardRepository};
use gpui::{
App, AppContext, Application, AssetSource, AsyncApp, Bounds, KeyBinding, WindowBounds,
Expand All @@ -11,7 +12,13 @@ use gpui_component::theme::Theme;
use gpui_component::{Root, ThemeMode};
use rust_embed::RustEmbed;
use std::borrow::Cow;
use std::sync::{Arc, Mutex, RwLock};
#[cfg(target_os = "linux")]
use std::env;
use std::sync::{Arc, Mutex, OnceLock, RwLock, mpsc};
use std::thread;
use std::time::Duration;

pub static X11: OnceLock<X11> = OnceLock::new();

#[derive(RustEmbed)]
#[folder = "assets"]
Expand All @@ -32,6 +39,8 @@ impl AssetSource for Assets {
#[cfg(target_os = "macos")]
use objc2::{class, msg_send, runtime::AnyObject};

use super::tray::TrayEvent;

#[cfg(target_os = "macos")]
fn set_activation_policy_accessory() {
unsafe {
Expand Down Expand Up @@ -243,12 +252,64 @@ pub fn launch_app() {
board.set_hotkey_tx(hotkey_tx);
});
});
start_tray_handler(window_handle, async_app.clone(), settings.clone());

start_tray_handler(settings, async_app, window_handle);

if !is_silent {
cx.activate(true);
}

// Initialize X11 control
#[cfg(target_os = "linux")]
if env::var("DISPLAY").is_ok() {
let x11 = X11.get_or_init(|| X11::new().expect("Failed to connect x11rb"));
let _ = x11.active_window();
}
});
}

fn start_tray_handler(
settings: Arc<RwLock<Settings>>,
async_app: AsyncApp,
window_handle: WindowHandle<Root>,
) {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
#[cfg(target_os = "linux")]
gtk::init().expect("Failed to init gtk modules");

start_tray_handler_inner(settings, tx);

#[cfg(target_os = "linux")]
gtk::main();
});

let fg_executor = async_app.foreground_executor().clone();
let bg_executor = async_app.background_executor().clone();

fg_executor
.spawn(async move {
loop {
while let Ok(event) = rx.try_recv() {
match event {
TrayEvent::Show => {
let _ = async_app.update(move |cx| {
crate::gui::tray::send_active_action(window_handle, cx);
});
}
TrayEvent::Quit => {
let _ = async_app.update(move |cx| {
cx.quit();
});
}
}
}

bg_executor.timer(Duration::from_millis(100)).await;
}
})
.detach();
}

fn bind_application_keys(cx: &mut App) {
Expand All @@ -258,6 +319,8 @@ fn bind_application_keys(cx: &mut App) {
KeyBinding::new("cmd-q", crate::gui::board::Quit, None),
#[cfg(target_os = "windows")]
KeyBinding::new("alt-f4", crate::gui::board::Quit, None),
#[cfg(target_os = "linux")]
KeyBinding::new("alt-f4", crate::gui::board::Quit, None),
KeyBinding::new("up", crate::gui::board::SelectPrev, None),
KeyBinding::new("down", crate::gui::board::SelectNext, None),
KeyBinding::new("enter", crate::gui::board::ConfirmSelection, None),
Expand Down
2 changes: 1 addition & 1 deletion src/gui/board/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ impl RopyBoard {

// Get current max_history_records from settings as fallback
let current_max_history = self.settings.read().unwrap().storage.max_history_records;

let max_history = self
.settings_max_history_input
.read(cx)
Expand Down
2 changes: 2 additions & 0 deletions src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ mod board;
mod hotkey;
mod tray;
mod utils;
#[cfg(target_os = "linux")]
mod x11;

pub use app::launch_app;
pub use utils::{active_window, hide_window};
67 changes: 32 additions & 35 deletions src/gui/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use crate::config::Settings;
use crate::i18n::I18n;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::mpsc::Sender;
use std::thread;
use std::time::Duration;

use gpui::{AsyncApp, WindowHandle};
use gpui::WindowHandle;
use gpui_component::Root;
use tray_icon::{
Icon, TrayIcon, TrayIconBuilder, TrayIconEvent,
Expand Down Expand Up @@ -51,48 +53,43 @@ fn create_icon() -> Result<Icon, Box<dyn std::error::Error>> {
Icon::from_rgba(rgba, width, height).map_err(|e| format!("Failed to create icon: {e:?}").into())
}

pub enum TrayEvent {
Show,
Quit,
}

/// Start the system tray handler
pub fn start_tray_handler(
window_handle: WindowHandle<Root>,
async_app: AsyncApp,
settings: Arc<RwLock<Settings>>,
) {
let fg_executor = async_app.foreground_executor().clone();
let bg_executor = async_app.background_executor().clone();
pub fn start_tray_handler_inner(settings: Arc<RwLock<Settings>>, tx: Sender<TrayEvent>) {
match init_tray(settings) {
Ok((tray, show_id, quit_id)) => {
println!("[ropy] Tray icon initialized successfully");
// Keep tray icon alive for the lifetime of the application
Box::leak(Box::new(tray));
fg_executor
.spawn(async move {
let menu_channel = tray_icon::menu::MenuEvent::receiver();
let tray_channel = TrayIconEvent::receiver();
loop {
while let Ok(event) = menu_channel.try_recv() {
if event.id == show_id {
let _ = async_app.update(move |cx| {
send_active_action(window_handle, cx);
});
} else if event.id == quit_id {
let _ = async_app.update(move |cx| {
cx.quit();
});
}

thread::spawn(move || {
Copy link
Owner

Choose a reason for hiding this comment

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

Why need to migrate this part? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The purpose of using another thread inside start_tray_handler_inner is to keep it from blocking gtk::main.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In Linux, tray-icon crate requires some additional operations to load the tray, see https://github.com/tauri-apps/tray-icon/blob/dev/examples/egui.rs#L17 for details, and gtk::main requires blocking the entire thread, so you need to put the tray operations in another thread

Copy link
Owner

Choose a reason for hiding this comment

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

Got it.

Copy link
Owner

Choose a reason for hiding this comment

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

Can we use a GPUI's background task to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we use a GPUI's background task to do this?

Done

let menu_channel = tray_icon::menu::MenuEvent::receiver();
let tray_channel = TrayIconEvent::receiver();

loop {
while let Ok(event) = menu_channel.try_recv() {
if event.id == show_id {
let _ = tx.send(TrayEvent::Show);
} else if event.id == quit_id {
let _ = tx.send(TrayEvent::Quit);
}
while let Ok(event) = tray_channel.try_recv() {
if let TrayIconEvent::Click { button, .. } = event
&& button == tray_icon::MouseButton::Left
{
let _ = async_app.update(move |cx| {
send_active_action(window_handle, cx);
});
}
}

while let Ok(event) = tray_channel.try_recv() {
if let TrayIconEvent::Click { button, .. } = event
&& button == tray_icon::MouseButton::Left
{
let _ = tx.send(TrayEvent::Show);
}
bg_executor.timer(Duration::from_millis(100)).await;
}
})
.detach();

thread::sleep(Duration::from_millis(100));
}
});
}
Err(e) => {
eprintln!("[ropy] Failed to initialize tray icon: {e}");
Expand All @@ -101,7 +98,7 @@ pub fn start_tray_handler(
}

/// Send the active action to the main window
fn send_active_action(window_handle: WindowHandle<Root>, cx: &mut gpui::App) {
pub fn send_active_action(window_handle: WindowHandle<Root>, cx: &mut gpui::App) {
window_handle
.update(cx, |_, window, cx| {
window.dispatch_action(Box::new(crate::gui::board::Active), cx)
Expand Down
31 changes: 28 additions & 3 deletions src/gui/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use gpui::{Context, Window};

#[cfg(not(target_os = "linux"))]
use raw_window_handle::{HasWindowHandle, RawWindowHandle};

#[cfg(target_os = "windows")]
Expand All @@ -24,6 +26,13 @@ pub fn hide_window<T>(_window: &mut Window, _cx: &mut Context<T>) {
}
#[cfg(target_os = "macos")]
_cx.hide();

#[cfg(target_os = "linux")]
if let Some(x11) = crate::gui::app::X11.get() {
if let Err(e) = x11.hide_window() {
eprintln!("[ropy] Failed to hide window: {e}")
}
}
}

/// Activate the window based on the platform
Expand All @@ -40,12 +49,20 @@ pub fn active_window<T>(_window: &mut Window, _cx: &mut Context<T>) {
}
#[cfg(target_os = "macos")]
_cx.activate(true);
#[cfg(target_os = "linux")]
{
if let Some(x11) = crate::gui::app::X11.get() {
if let Err(e) = x11.display_and_activate_window() {
eprintln!("[ropy] Failed to activate window: {e}")
}
}
}
}

/// Set the window to be always on top
pub fn set_always_on_top<T>(window: &mut Window, _cx: &mut Context<T>, always_on_top: bool) {
pub fn set_always_on_top<T>(_window: &mut Window, _cx: &mut Context<T>, always_on_top: bool) {
#[cfg(target_os = "windows")]
if let Ok(handle) = window.window_handle() {
if let Ok(handle) = _window.window_handle() {
if let RawWindowHandle::Win32(handle) = handle.as_raw() {
let hwnd = handle.hwnd.get() as *mut std::ffi::c_void;
unsafe {
Expand All @@ -62,7 +79,7 @@ pub fn set_always_on_top<T>(window: &mut Window, _cx: &mut Context<T>, always_on
}
}
#[cfg(target_os = "macos")]
if let Ok(handle) = window.window_handle()
if let Ok(handle) = _window.window_handle()
&& let RawWindowHandle::AppKit(handle) = handle.as_raw()
{
// NSFloatingWindowLevel = 3, NSNormalWindowLevel = 0
Expand All @@ -75,6 +92,14 @@ pub fn set_always_on_top<T>(window: &mut Window, _cx: &mut Context<T>, always_on
}
}
}
#[cfg(target_os = "linux")]
{
if let Some(x11) = crate::gui::app::X11.get() {
if let Err(e) = x11.set_always_on_top(always_on_top) {
eprintln!("[ropy] Failed to set always on top: {e}")
}
}
}
}

/// Start dragging the window
Expand Down
Loading