diff --git a/.agents/skills/contribution-flow/SKILL.md b/.agents/skills/contribution-flow/SKILL.md index e5d90af..6833f61 100644 --- a/.agents/skills/contribution-flow/SKILL.md +++ b/.agents/skills/contribution-flow/SKILL.md @@ -20,7 +20,7 @@ Shared vocabulary used throughout the flow below: `` and `` placeho ### 1. Open the Issue -If the project has issue templates (`.github/ISSUE_TEMPLATE/`), pick the appropriate one. Otherwise use a concise description with acceptance criteria. Fill it into `/tmp/issue-body.md`, then: +Pick the appropriate issue template from `.github/ISSUE_TEMPLATE/` — using a template is **mandatory**. Fill it into `/tmp/issue-body.md`, then: ```bash gh issue create --title ": ..." --label "" --body-file /tmp/issue-body.md @@ -61,7 +61,7 @@ Multiple commits are fine — they are squashed on merge. ### 6. Push & Open the PR -If the project has a PR template (`.github/PULL_REQUEST_TEMPLATE.md`), fill it into `/tmp/pr-body.md`. Otherwise write a concise summary with a test plan. Then: +Fill in the PR template from `.github/PULL_REQUEST_TEMPLATE.md` into `/tmp/pr-body.md` — using the template is **mandatory**. Then: ```bash git push -u origin HEAD diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 428b788..3b680f0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,6 +31,5 @@ Closes # - [ ] PR title follows Conventional Commits - [ ] No hardcoded user-facing strings — i18n keys added to all locale files -- [ ] New UI components use `gpui-component` - [ ] Errors defined with `thiserror` (no manual `impl Display/Error`) - [ ] No unrelated changes mixed in diff --git a/assets/locales/en.toml b/assets/locales/en.toml index 03ec336..69007fb 100644 --- a/assets/locales/en.toml +++ b/assets/locales/en.toml @@ -4,7 +4,8 @@ language_name = "English" # Application # Tray menu -tray_show = "Show" +tray_settings = "Settings" +tray_about = "About" tray_quit = "Quit" # Main window diff --git a/assets/locales/ja.toml b/assets/locales/ja.toml index ce23e44..3b343f6 100644 --- a/assets/locales/ja.toml +++ b/assets/locales/ja.toml @@ -5,7 +5,8 @@ language_name = "日本語" # Application # Tray menu -tray_show = "表示" +tray_settings = "設定" +tray_about = "について" tray_quit = "終了" # Main window diff --git a/assets/locales/zh-CN.toml b/assets/locales/zh-CN.toml index d7ae4e7..88e2b35 100644 --- a/assets/locales/zh-CN.toml +++ b/assets/locales/zh-CN.toml @@ -4,7 +4,8 @@ language_name = "简体中文" # 应用程序 # 托盘菜单 -tray_show = "显示" +tray_settings = "设置" +tray_about = "关于" tray_quit = "退出" # 主窗口 diff --git a/src/gui/board.rs b/src/gui/board.rs index 17e5cf7..4c515d5 100644 --- a/src/gui/board.rs +++ b/src/gui/board.rs @@ -173,6 +173,7 @@ pub(crate) struct RopyBoard { pub(crate) confirm_mode: ConfirmMode, pub(crate) layout_mode: LayoutMode, pub(crate) pinned: bool, + pub(crate) activated: bool, pub(crate) hotkey_tx: Option>, pub(crate) update_manager: UpdateManager, /// Which clear action is currently awaiting confirmation @@ -479,6 +480,7 @@ impl RopyBoard { confirm_mode, layout_mode, pinned: false, + activated: false, hotkey_tx: None, update_manager: UpdateManager::new(), clear_confirm_action: ClearConfirmAction::AllHistory, diff --git a/src/gui/board/actions.rs b/src/gui/board/actions.rs index 3df4bf4..ec7b985 100644 --- a/src/gui/board/actions.rs +++ b/src/gui/board/actions.rs @@ -257,6 +257,7 @@ impl RopyBoard { self.reveal_selected_record(); self.active_panel = ActivePanel::ClipboardList; self.ui_state.clear_confirm = crate::gui::board::ClearConfirmState::Hidden; + self.activated = true; reset_window_geometry_for_activation(window, default_window_size()); active_window(window, cx); } diff --git a/src/gui/board/render.rs b/src/gui/board/render.rs index edb154a..69bff45 100644 --- a/src/gui/board/render.rs +++ b/src/gui/board/render.rs @@ -23,6 +23,13 @@ impl Render for RopyBoard { .px_4() .pb_4(); + if !self.activated { + return gpui::div() + .size_full() + .child(base.bg(self.main_panel_surface(cx.theme().background))) + .into_any_element(); + } + let body: AnyElement = match self.active_panel { ActivePanel::Settings => base .bg(self.main_panel_surface(cx.theme().background)) @@ -80,5 +87,6 @@ impl Render for RopyBoard { .children(notifs), ) }) + .into_any_element() } } diff --git a/src/gui/tray.rs b/src/gui/tray.rs index 1ada9e6..c7e36dc 100644 --- a/src/gui/tray.rs +++ b/src/gui/tray.rs @@ -12,7 +12,8 @@ use tray_icon::{ use crate::{constants::APP_NAME, i18n::I18n}; /// Fixed menu IDs so event handlers remain valid after menu rebuilds. -const TRAY_SHOW_ID: &str = "tray_show"; +const TRAY_SETTINGS_ID: &str = "tray_settings"; +const TRAY_ABOUT_ID: &str = "tray_about"; const TRAY_QUIT_ID: &str = "tray_quit"; const TRAY_ICON_SIZE: u32 = 32; @@ -62,20 +63,23 @@ impl TrayState { } pub(crate) fn build_tray_menu(i18n: &I18n) -> Result> { - let show_item = MenuItem::with_id(TRAY_SHOW_ID, i18n.t("tray_show"), true, None); + let settings_item = MenuItem::with_id(TRAY_SETTINGS_ID, i18n.t("tray_settings"), true, None); + let about_item = MenuItem::with_id(TRAY_ABOUT_ID, i18n.t("tray_about"), true, None); let quit_item = MenuItem::with_id(TRAY_QUIT_ID, i18n.t("tray_quit"), true, None); let tray_menu = Menu::new(); - tray_menu.append(&show_item)?; + tray_menu.append(&settings_item)?; + tray_menu.append(&about_item)?; tray_menu.append(&quit_item)?; Ok(tray_menu) } pub(crate) fn init_tray( i18n: &I18n, -) -> Result<(TrayIcon, MenuId, MenuId), Box> { +) -> Result<(TrayIcon, MenuId, MenuId, MenuId), Box> { let tray_menu = build_tray_menu(i18n)?; - let show_id = MenuId::new(TRAY_SHOW_ID); + let settings_id = MenuId::new(TRAY_SETTINGS_ID); + let about_id = MenuId::new(TRAY_ABOUT_ID); let quit_id = MenuId::new(TRAY_QUIT_ID); let icon = create_icon()?; @@ -87,7 +91,7 @@ pub(crate) fn init_tray( .with_menu_on_left_click(false) .build()?; - Ok((tray, show_id, quit_id)) + Ok((tray, settings_id, about_id, quit_id)) } fn create_icon() -> Result> { @@ -122,7 +126,8 @@ fn load_tray_icon_rgba(bytes: &[u8]) -> Result<(Vec, u32, u32), image::Image #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TrayEvent { - Show, + Settings, + About, Quit, } @@ -178,8 +183,11 @@ fn spawn_tray_event_loop( cx.spawn(async move |async_app| { while let Ok(event) = rx.recv().await { match event { - TrayEvent::Show => { - let _ = async_app.update(|cx| send_active_action(window_handle, cx)); + TrayEvent::Settings => { + let _ = async_app.update(|cx| send_open_settings(window_handle, cx)); + } + TrayEvent::About => { + let _ = async_app.update(|cx| send_open_about(window_handle, cx)); } TrayEvent::Quit => { let _ = async_app.update(|cx| cx.quit()); @@ -195,12 +203,12 @@ pub(crate) fn start_tray_handler_inner( tx: async_channel::Sender, ) -> Option { match init_tray(i18n) { - Ok((tray, show_id, quit_id)) => { + Ok((tray, settings_id, about_id, quit_id)) => { tracing::info!("tray icon initialized successfully"); #[cfg(not(target_os = "linux"))] spawn_tray_icon_event_forwarder(tx.clone()); - spawn_tray_menu_event_forwarder(tx, show_id, quit_id); + spawn_tray_menu_event_forwarder(tx, settings_id, about_id, quit_id); Some(tray) } @@ -211,23 +219,64 @@ pub(crate) fn start_tray_handler_inner( } } -pub(crate) fn send_active_action(window_handle: WindowHandle, cx: &mut App) { +pub(crate) fn send_open_settings(window_handle: WindowHandle, cx: &mut App) { + window_handle + .update(cx, |root, window: &mut gpui::Window, cx| { + if let Ok(board) = root + .view() + .clone() + .downcast::() + { + board.update(cx, |board, cx| { + board.active_panel = crate::gui::board::ActivePanel::Settings; + board + .settings_editor + .panel_state + .window_opacity_slider_visible = true; + board.activated = true; + window.focus(&board.focus_handle); + cx.notify(); + }); + } + super::active_window(window, cx); + }) + .ok(); +} + +pub(crate) fn send_open_about(window_handle: WindowHandle, cx: &mut App) { window_handle - .update(cx, |_, window: &mut gpui::Window, cx| { - window.dispatch_action(Box::new(crate::gui::board::Active), cx); + .update(cx, |root, window: &mut gpui::Window, cx| { + if let Ok(board) = root + .view() + .clone() + .downcast::() + { + board.update(cx, |board, cx| { + board.active_panel = crate::gui::board::ActivePanel::About; + board.activated = true; + cx.notify(); + }); + } + super::active_window(window, cx); }) .ok(); } fn spawn_tray_menu_event_forwarder( tx: async_channel::Sender, - show_id: MenuId, + settings_id: MenuId, + about_id: MenuId, quit_id: MenuId, ) { let receiver = tray_icon::menu::MenuEvent::receiver().clone(); super::utils::spawn_event_forwarder("tray-menu-event-forwarder", tx, move |forward| { while let Ok(event) = receiver.recv() { - if !forward(tray_event_from_menu_event(&event, &show_id, &quit_id)) { + if !forward(tray_event_from_menu_event( + &event, + &settings_id, + &about_id, + &quit_id, + )) { break; } } @@ -248,11 +297,14 @@ fn spawn_tray_icon_event_forwarder(tx: async_channel::Sender) { fn tray_event_from_menu_event( event: &tray_icon::menu::MenuEvent, - show_id: &MenuId, + settings_id: &MenuId, + about_id: &MenuId, quit_id: &MenuId, ) -> Option { - if event.id == *show_id { - Some(TrayEvent::Show) + if event.id == *settings_id { + Some(TrayEvent::Settings) + } else if event.id == *about_id { + Some(TrayEvent::About) } else if event.id == *quit_id { Some(TrayEvent::Quit) } else { @@ -265,7 +317,7 @@ fn tray_event_from_icon_event(event: &TrayIconEvent) -> Option { if let TrayIconEvent::Click { button, .. } = event && *button == tray_icon::MouseButton::Left { - Some(TrayEvent::Show) + Some(TrayEvent::Settings) } else { None } @@ -321,27 +373,24 @@ mod tests { } #[rstest] - #[case(TRAY_SHOW_ID, Some(TrayEvent::Show))] + #[case(TRAY_SETTINGS_ID, Some(TrayEvent::Settings))] + #[case(TRAY_ABOUT_ID, Some(TrayEvent::About))] #[case(TRAY_QUIT_ID, Some(TrayEvent::Quit))] #[case("other", None)] fn test_tray_event_from_menu_event_matches_menu_id( #[case] event_id: &str, #[case] expected: Option, ) { - let show_id = MenuId::new(TRAY_SHOW_ID); + let settings_id = MenuId::new(TRAY_SETTINGS_ID); + let about_id = MenuId::new(TRAY_ABOUT_ID); let quit_id = MenuId::new(TRAY_QUIT_ID); let event = tray_icon::menu::MenuEvent { id: MenuId::new(event_id), }; - let result = tray_event_from_menu_event(&event, &show_id, &quit_id); + let result = tray_event_from_menu_event(&event, &settings_id, &about_id, &quit_id); - assert!(matches!( - (result, expected), - (Some(TrayEvent::Show), Some(TrayEvent::Show)) - | (Some(TrayEvent::Quit), Some(TrayEvent::Quit)) - | (None, None) - )); + assert_eq!(result, expected); } #[cfg(not(target_os = "linux"))] @@ -354,7 +403,7 @@ mod tests { button: tray_icon::MouseButton::Left, button_state: tray_icon::MouseButtonState::Up, }, - Some(TrayEvent::Show) + Some(TrayEvent::Settings) )] #[case( TrayIconEvent::Click { @@ -380,9 +429,6 @@ mod tests { ) { let result = tray_event_from_icon_event(&event); - assert!(matches!( - (result, expected), - (Some(TrayEvent::Show), Some(TrayEvent::Show)) | (None, None) - )); + assert_eq!(result, expected); } } diff --git a/src/i18n.rs b/src/i18n.rs index 8fc69e0..d877ddb 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -149,19 +149,19 @@ mod tests { let i18n = I18n::new(Language::new("en")); assert!(i18n.is_ok()); let i18n = i18n.unwrap(); - assert_eq!(i18n.t("tray_show"), "Show"); + assert_eq!(i18n.t("tray_settings"), "Settings"); } #[test] #[expect(clippy::unwrap_used)] fn test_i18n_language_switch() { let mut i18n = I18n::new(Language::new("en")).unwrap(); - assert_eq!(i18n.t("tray_show"), "Show"); + assert_eq!(i18n.t("tray_settings"), "Settings"); // Switch to Chinese let result = i18n.set_language(Language::new("zh-CN")); assert!(result.is_ok()); - assert_eq!(i18n.t("tray_show"), "显示"); + assert_eq!(i18n.t("tray_settings"), "设置"); } #[test]