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
6 changes: 5 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ panic = "abort"

[dependencies]
risuko-engine = { workspace = true }
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri = { version = "2", features = [
"tray-icon",
"image-png",
"macos-private-api",
] }
tauri-plugin-store = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-dialog = "2"
Expand Down
13 changes: 13 additions & 0 deletions src-tauri/capabilities/tray-panel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Misleading security description: tray-panel.json claims "No filesystem, shell, process, OS, or dialog access", but default.json already includes "tray-panel" in its windows list and grants all of those permissions. In Tauri v2, capability permissions are unioned — so the restrictive intent is entirely defeated. Remove "tray-panel" from default.json's windows array if the intent is genuine restriction, or update the description to match reality.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src-tauri/capabilities/tray-panel.json:

<comment>Misleading security description: `tray-panel.json` claims "No filesystem, shell, process, OS, or dialog access", but `default.json` already includes `"tray-panel"` in its `windows` list and grants all of those permissions. In Tauri v2, capability permissions are unioned — so the restrictive intent is entirely defeated. Remove `"tray-panel"` from `default.json`'s windows array if the intent is genuine restriction, or update the description to match reality.</comment>

<file context>
@@ -0,0 +1,13 @@
+{
+	"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",
</file context>

"identifier": "tray-panel",
"description": "Minimal capabilities for the tray flyout panel.",
"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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
]
}
13 changes: 8 additions & 5 deletions src-tauri/risuko-bt/src/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1700,7 +1700,6 @@ fn maybe_clear_endgame(chunk_tracker: &mut ChunkTracker) {
}

/// Handle an inbound BEP-55 ut_holepunch message

fn handle_holepunch(
hp: HolepunchMsg,
from_addr: SocketAddr,
Expand Down Expand Up @@ -1791,10 +1790,14 @@ fn try_initiate_holepunch(
let Some(relay_hp) = relay.their_ut_holepunch_id else {
return; // relay doesn't support holepunch
};
if relay.cmd_tx.try_send(PeerCommand::Send(Message::Extended {
ext_id: relay_hp,
payload: build_holepunch(holepunch_type::RENDEZVOUS, target, 0),
})).is_ok() {
if relay
.cmd_tx
.try_send(PeerCommand::Send(Message::Extended {
ext_id: relay_hp,
payload: build_holepunch(holepunch_type::RENDEZVOUS, target, 0),
}))
.is_ok()
{
holepunch_attempted.insert(target);
log::debug!(
target: "diag",
Expand Down
35 changes: 2 additions & 33 deletions src-tauri/src/commands/app_cmds.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,10 @@
use std::sync::atomic::Ordering;
use tauri::{AppHandle, Manager};

const RUN_MODE_STANDARD: i64 = 1;
#[cfg(target_os = "macos")]
const RUN_MODE_TRAY: i64 = 2;
#[cfg(target_os = "macos")]
const RUN_MODE_HIDE_TRAY_LEGACY: i64 = 3;

fn current_run_mode(handle: &AppHandle) -> i64 {
handle
.state::<crate::state::AppState>()
.config
.lock()
.ok()
.and_then(|cfg| {
cfg.get_user_config()
.get("run-mode")
.and_then(|value| value.as_i64())
})
.unwrap_or(RUN_MODE_STANDARD)
}

#[cfg(target_os = "macos")]
fn update_macos_activation_policy_for_mode(handle: &AppHandle, run_mode: i64) {
let policy = if matches!(run_mode, RUN_MODE_TRAY | RUN_MODE_HIDE_TRAY_LEGACY) {
tauri::ActivationPolicy::Accessory
} else {
tauri::ActivationPolicy::Regular
};
let _ = handle.set_activation_policy(policy);
}

#[cfg(not(target_os = "macos"))]
fn update_macos_activation_policy_for_mode(_handle: &AppHandle, _run_mode: i64) {}
pub use crate::utils::run_mode::{current_run_mode, update_macos_activation_policy_for_mode};

pub fn show_main_window(handle: &AppHandle) -> Result<(), String> {
update_macos_activation_policy_for_mode(handle, RUN_MODE_STANDARD);
update_macos_activation_policy_for_mode(handle, crate::utils::run_mode::RUN_MODE_STANDARD);
if let Some(window) = handle.get_webview_window("main") {
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/src/commands/file_cmds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,10 @@ fn trash_generated_torrent_sidecars_in_dir(dir: &Path, normalized_info_hash: Opt
// dict and comparing to this task's info-hash identifies the sidecar
// precisely, and never touches an unrelated torrent (different hash).
let hex_stem = generated_torrent_hex_stem(file_name);
let matched_by_name = hex_stem.as_deref().map(|stem| stem == hash).unwrap_or(false);
let matched_by_name = hex_stem
.as_deref()
.map(|stem| stem == hash)
.unwrap_or(false);
// Only probe content when the filename has no hex-infohash stem.
// A hex-named .torrent whose stem differs from our hash is definitively
// a different torrent; content-matching it would be a false positive that
Expand Down
29 changes: 29 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod cli;
mod commands;
mod managers;
mod state;
mod utils;

// Re-export risuko_engine modules for use by commands and other app code
pub use risuko_engine::config;
Expand Down Expand Up @@ -338,6 +339,8 @@ pub fn run() {

managers::tray::setup_tray(app)?;

managers::flyout::setup_flyout(app)?;

// Start RSS background polling
if let Ok(guard) = app.state::<state::AppState>().rss.lock() {
if let Some(rss) = guard.clone() {
Expand All @@ -353,6 +356,15 @@ pub fn run() {
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
#[cfg(not(target_os = "android"))]
if window.label() == managers::flyout::FLYOUT_LABEL {
api.prevent_close();
let _ = window.hide();
return;
}
if window.label() != "main" {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let quitting = window
.app_handle()
.state::<state::AppState>()
Expand All @@ -364,6 +376,23 @@ pub fn run() {
api.prevent_close();
let _ = commands::app_cmds::hide_main_window(window.app_handle());
}

if let tauri::WindowEvent::Focused(false) = event {
#[cfg(not(target_os = "android"))]
if window.label() == managers::flyout::FLYOUT_LABEL {
#[cfg(target_os = "macos")]
{
// Only set Accessory policy when in tray mode
let run_mode = utils::run_mode::current_run_mode(window.app_handle());
if utils::run_mode::is_tray_mode(run_mode) {
let _ = window
.app_handle()
.set_activation_policy(tauri::ActivationPolicy::Accessory);
}
}
let _ = window.hide();
}
}
})
.invoke_handler(tauri::generate_handler![
commands::config_cmds::get_app_config,
Expand Down
179 changes: 179 additions & 0 deletions src-tauri/src/managers/flyout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//! 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")]
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{
// Only set Accessory policy when in tray mode
let run_mode = crate::utils::run_mode::current_run_mode(app);
if crate::utils::run_mode::is_tray_mode(run_mode) {
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));
}
1 change: 1 addition & 0 deletions src-tauri/src/managers/mod.rs
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;
Expand Down
Loading
Loading