From afbf315e97fa70b27cc3b0a85b55fcc37865a5a7 Mon Sep 17 00:00:00 2001 From: Blomios Date: Mon, 13 Jul 2026 17:59:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(view-windows):=20exposer=20list=5Fopen=5Fv?= =?UTF-8?q?iew=5Fwindows=20pour=20l'=C3=A9tat=20du=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute la commande Tauri `list_open_view_windows` qui retourne un `ViewWindowSnapshot` (panel, label, visible) par fenêtre de panneau détachée, avec le parseur `view_panel_from_window_label` (labels stables et legacy suffixés du project id). Enregistre la commande dans le handler. Backend seul pour le ticket #50 ; pas encore de consommateur frontend. Co-Authored-By: Claude Opus 4.8 --- crates/app-tauri/src/commands.rs | 85 ++++++++++++++++++++++++++++++++ crates/app-tauri/src/lib.rs | 1 + 2 files changed, 86 insertions(+) diff --git a/crates/app-tauri/src/commands.rs b/crates/app-tauri/src/commands.rs index 6875ca0..7944d75 100644 --- a/crates/app-tauri/src/commands.rs +++ b/crates/app-tauri/src/commands.rs @@ -2336,6 +2336,18 @@ pub struct CloseViewWindowResponseDto { pub closed: bool, } +/// Snapshot returned by `list_open_view_windows`. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ViewWindowSnapshot { + /// Panel id rendered by the detached window. + pub panel: String, + /// Stable Tauri window label. + pub label: String, + /// Whether Tauri currently reports the OS window as visible. + pub visible: bool, +} + /// Payload emitted on `view-window://lifecycle`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -2429,6 +2441,19 @@ pub(crate) fn view_window_url(panel: ViewPanel) -> String { format!("index.html?panel={}", panel.as_str()) } +fn view_panel_from_window_label(label: &str) -> Option { + let rest = label.strip_prefix("view-")?; + if let Ok(panel) = ViewPanel::parse(rest) { + return Some(panel); + } + + let (panel, project_id) = rest.rsplit_once('-')?; + if project_id.len() != 32 || uuid::Uuid::parse_str(project_id).is_err() { + return None; + } + ViewPanel::parse(panel).ok() +} + pub(crate) fn emit_view_window_lifecycle( app: &AppHandle, kind: &'static str, @@ -2468,6 +2493,30 @@ pub async fn get_focused_project( Ok(state.get_focused_project()) } +/// `list_open_view_windows` — return the detached panel windows currently +/// registered by Tauri. +#[tauri::command] +pub fn list_open_view_windows(app: AppHandle) -> Vec { + let mut windows = app + .webview_windows() + .into_iter() + .filter_map(|(label, window)| { + let panel = view_panel_from_window_label(&label)?; + Some(ViewWindowSnapshot { + panel: panel.as_str().to_owned(), + label, + visible: window.is_visible().unwrap_or(true), + }) + }) + .collect::>(); + windows.sort_by(|left, right| { + left.panel + .cmp(&right.panel) + .then(left.label.cmp(&right.label)) + }); + windows +} + /// `open_view_window` — open or focus a detached OS window for one panel. /// /// The window is a normal decorated, resizable system window. Its webview loads @@ -2578,6 +2627,42 @@ mod view_window_tests { assert!(err.message.contains("invalid view panel: terminal")); } + #[test] + fn view_window_label_parser_accepts_stable_and_legacy_labels() { + assert_eq!( + view_panel_from_window_label("view-tickets") + .unwrap() + .as_str(), + "tickets" + ); + assert_eq!( + view_panel_from_window_label("view-tickets-0000000000000000000000000000002a") + .unwrap() + .as_str(), + "tickets" + ); + assert!(view_panel_from_window_label("main").is_none()); + assert!(view_panel_from_window_label("view-unknown").is_none()); + assert!(view_panel_from_window_label("view-tickets-not-a-project").is_none()); + } + + #[test] + fn view_window_snapshot_payload_is_camel_case() { + let payload = ViewWindowSnapshot { + panel: "tickets".to_owned(), + label: "view-tickets".to_owned(), + visible: true, + }; + + let json = serde_json::to_string(&payload).unwrap(); + assert!(json.contains("\"panel\":\"tickets\""), "json was {json}"); + assert!( + json.contains("\"label\":\"view-tickets\""), + "json was {json}" + ); + assert!(json.contains("\"visible\":true"), "json was {json}"); + } + #[test] fn view_window_lifecycle_payload_is_camel_case() { let payload = ViewWindowLifecycleEventDto { diff --git a/crates/app-tauri/src/lib.rs b/crates/app-tauri/src/lib.rs index 03dcd11..d349d82 100644 --- a/crates/app-tauri/src/lib.rs +++ b/crates/app-tauri/src/lib.rs @@ -264,6 +264,7 @@ pub fn run() { commands::resolve_memory_links, commands::set_focused_project, commands::get_focused_project, + commands::list_open_view_windows, commands::open_view_window, commands::close_view_window, commands::move_tab_to_new_window,