-
-
Notifications
You must be signed in to change notification settings - Fork 5
feat(ui): Flyout tray panel #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
080f8e7
3cb11a9
84091b1
2c3842a
b1fe40a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Misleading security description: Prompt for AI agents |
||
| "identifier": "tray-panel", | ||
| "description": "Minimal capabilities for the tray flyout panel. Grants only window management, store access, and the invoke channel needed for flyout commands (show_window, get_global_stat, tell_active, tell_waiting, tell_stopped, pause/unpause/remove task, add_uri, open_path, reveal_in_folder). No filesystem, shell, process, OS, or dialog access.", | ||
| "windows": ["tray-panel"], | ||
| "permissions": [ | ||
| "core:default", | ||
| "core:window:allow-show", | ||
| "core:window:allow-hide", | ||
| "core:window:allow-set-focus", | ||
| "core:window:allow-start-dragging", | ||
| "store:default" | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| //! Tray "Quick Panel" flyout window | ||
|
|
||
| #[cfg(not(target_os = "android"))] | ||
| use tauri::{ | ||
| AppHandle, Emitter, Manager, PhysicalPosition, WebviewUrl, WebviewWindow, WebviewWindowBuilder, | ||
| }; | ||
|
|
||
| #[cfg(target_os = "android")] | ||
| use tauri::AppHandle; | ||
|
|
||
| #[cfg(not(target_os = "android"))] | ||
| pub const FLYOUT_LABEL: &str = "tray-panel"; | ||
|
|
||
| /// Logical size of the flyout window | ||
| #[cfg(not(target_os = "android"))] | ||
| const FLYOUT_WIDTH: f64 = 396.0; | ||
| #[cfg(not(target_os = "android"))] | ||
| const FLYOUT_HEIGHT: f64 = 540.0; | ||
|
|
||
| /// Gap in physical pixels between the tray icon and the flyout edge | ||
| #[cfg(not(target_os = "android"))] | ||
| const FLYOUT_GAP: i32 = 0; | ||
|
|
||
| /// Create the flyout window | ||
| #[cfg(not(target_os = "android"))] | ||
| pub fn setup_flyout(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||
| let handle = app.handle(); | ||
| // Skip if it somehow already exists | ||
| if handle.get_webview_window(FLYOUT_LABEL).is_some() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let mut builder = | ||
| WebviewWindowBuilder::new(app, FLYOUT_LABEL, WebviewUrl::App("tray.html".into())) | ||
| .title("Risuko Quick Panel") | ||
| .inner_size(FLYOUT_WIDTH, FLYOUT_HEIGHT) | ||
| .decorations(false) | ||
| .always_on_top(true) | ||
| .skip_taskbar(true) | ||
| .resizable(false) | ||
| .visible(false) | ||
| .focused(false) | ||
| .transparent(true) | ||
| .shadow(false) | ||
| .accept_first_mouse(true); | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| { | ||
| builder = builder.visible_on_all_workspaces(true); | ||
| } | ||
|
|
||
| builder.build()?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(target_os = "android")] | ||
| pub fn setup_flyout(_app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> { | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Cache the tray icon rect | ||
| #[cfg(not(target_os = "android"))] | ||
| pub fn cache_tray_rect(app: &AppHandle, rect: &tauri::Rect, scale_factor: f64) { | ||
| let pos = rect.position.to_physical::<f64>(scale_factor); | ||
| let size = rect.size.to_physical::<f64>(scale_factor); | ||
| if let Some(state) = app.try_state::<crate::state::AppState>() { | ||
| if let Ok(mut anchor) = state.tray_anchor.lock() { | ||
| *anchor = Some((pos.x, pos.y, size.width, size.height)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Toggle the flyout | ||
| #[cfg(not(target_os = "android"))] | ||
| pub fn toggle_flyout(app: &AppHandle) { | ||
| let Some(window) = app.get_webview_window(FLYOUT_LABEL) else { | ||
| log::warn!("[Risuko] flyout window not found"); | ||
| return; | ||
| }; | ||
|
|
||
| if window.is_visible().unwrap_or(false) { | ||
| #[cfg(target_os = "macos")] | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory); | ||
| let _ = window.hide(); | ||
| return; | ||
| } | ||
|
|
||
| position_flyout(app, &window); | ||
| let _ = window.show(); | ||
| let _ = window.set_focus(); | ||
| // Wake the webview's polling loop so the panel is fresh on open. | ||
| let _ = window.emit("flyout:show", ()); | ||
| } | ||
|
|
||
| #[cfg(target_os = "android")] | ||
| pub fn toggle_flyout(_app: &AppHandle) {} | ||
|
|
||
| /// Position the flyout near the cached tray anchor | ||
| #[cfg(not(target_os = "android"))] | ||
| fn position_flyout(app: &AppHandle, window: &WebviewWindow) { | ||
| // Resolve the anchor point (icon center) in physical pixels | ||
| let anchor = app | ||
| .try_state::<crate::state::AppState>() | ||
| .and_then(|state| state.tray_anchor.lock().ok().and_then(|guard| *guard)); | ||
|
|
||
| let (icon_x, icon_y, icon_w, icon_h) = match anchor { | ||
| Some(rect) => rect, | ||
| None => { | ||
| // No tray rect was ever delivered | ||
| match app.cursor_position() { | ||
| Ok(pos) => (pos.x, pos.y, 0.0, 0.0), | ||
| Err(_) => (0.0, 0.0, 0.0, 0.0), | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| let icon_center_x = icon_x + icon_w / 2.0; | ||
| let icon_center_y = icon_y + icon_h / 2.0; | ||
|
|
||
| // Find the monitor under the icon; fall back to primary | ||
| let monitor = app | ||
| .monitor_from_point(icon_center_x, icon_center_y) | ||
| .ok() | ||
| .flatten() | ||
| .or_else(|| app.primary_monitor().ok().flatten()); | ||
|
|
||
| let Some(monitor) = monitor else { | ||
| // Last resort: drop it at the anchor with no clamping | ||
| let _ = window.set_position(PhysicalPosition::new(icon_x as i32, icon_y as i32)); | ||
| return; | ||
| }; | ||
|
|
||
| let scale = monitor.scale_factor(); | ||
| let work = monitor.work_area(); | ||
| let wa_x = work.position.x as f64; | ||
| let wa_y = work.position.y as f64; | ||
| let wa_w = work.size.width as f64; | ||
| let wa_h = work.size.height as f64; | ||
|
|
||
| // Window size in physical pixels | ||
| let win_w = FLYOUT_WIDTH * scale; | ||
| let win_h = FLYOUT_HEIGHT * scale; | ||
| let gap = FLYOUT_GAP as f64 * scale; | ||
|
|
||
| // Decide above vs below using the icon center relative to the work area | ||
| let in_top_half = icon_center_y < wa_y + wa_h / 2.0; | ||
| let mut y = if in_top_half { | ||
| // Place below the icon | ||
| icon_y + icon_h + gap | ||
| } else { | ||
| // Place above the icon | ||
| icon_y - win_h - gap | ||
| }; | ||
|
|
||
| // Horizontally center on the icon | ||
| let mut x = icon_center_x - win_w / 2.0; | ||
|
|
||
| // Clamp into the work area | ||
| let max_x = wa_x + wa_w - win_w; | ||
| let max_y = wa_y + wa_h - win_h; | ||
| if x < wa_x { | ||
| x = wa_x; | ||
| } else if x > max_x { | ||
| x = max_x.max(wa_x); | ||
| } | ||
| if y < wa_y { | ||
| y = wa_y; | ||
| } else if y > max_y { | ||
| y = max_y.max(wa_y); | ||
| } | ||
|
|
||
| let _ = window.set_position(PhysicalPosition::new(x.round() as i32, y.round() as i32)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| pub mod flyout; | ||
| pub mod menu; | ||
| pub mod tray; | ||
| pub mod vault; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 2430
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 5773
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 6698
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 686
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 10200
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 734
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 44
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 42
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 1902
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 967
🏁 Script executed:
Repository: YueMiyuki/Risuko
Length of output: 20797
Scope
tray-panelto a minimal capability set.Line 4 adds
tray-panelto the default capability, which grants it all 80+ exposed commands and broad permissions including shell execution, file access, process control, and OS operations. However, the flyout only calls 4 read-only backend commands:show_window,get_global_stat,tell_active,tell_waiting, andtell_stopped. If the tray panel webview is compromised, an attacker gains access to file operations, process management, and shell execution across the entire application. Create a dedicated minimal capability profile fortray-panelwith only the window show/hide operations and the specific read-only query commands it uses.🤖 Prompt for AI Agents