收口桌面窗口标题桥接边界

将 Tauri 窗口标题 HostBridge 处理收口到 title 模块

让桌面 dispatch 只委托窗口标题请求

同步桌面壳结构门禁、架构文档和共享记忆
This commit is contained in:
2026-06-19 22:45:02 +08:00
parent d53bd66e99
commit dfeea9e93c
8 changed files with 106 additions and 57 deletions
+32 -1
View File
@@ -118,6 +118,14 @@ const desktopHostBridgeShareSource = fs.readFileSync(
desktopHostBridgeSharePath,
'utf8',
);
const desktopHostBridgeTitlePath = new URL(
'../src-tauri/src/host_bridge/title.rs',
import.meta.url,
);
const desktopHostBridgeTitleSource = fs.readFileSync(
desktopHostBridgeTitlePath,
'utf8',
);
const desktopShellUrlPath = new URL(
'../src-tauri/src/shell/url.rs',
import.meta.url,
@@ -237,6 +245,7 @@ const expectedDesktopHostBridgeRustFiles = [
'notifications.rs',
'protocol.rs',
'share.rs',
'title.rs',
];
const expectedDesktopShellRustFiles = [
'deep_link.rs',
@@ -1323,7 +1332,7 @@ const desktopHostBridgePayloadLimits = {
'BADGE_COUNT_MAX',
),
HOST_BRIDGE_APP_TITLE_MAX_LENGTH: extractRustNumberConst(
rustHostSource,
desktopHostBridgeTitleSource,
'WINDOW_TITLE_MAX_LENGTH',
),
HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH: extractRustNumberConst(
@@ -1934,6 +1943,7 @@ const expectedRustHostBridgeFiles = [
'notifications.rs',
'protocol.rs',
'share.rs',
'title.rs',
];
const expectedRustShellFiles = [
'deep_link.rs',
@@ -2211,6 +2221,27 @@ for (const snippet of [
if (!desktopHostBridgeBadgeSource.match(/window\s*\.\s*set_badge_count\s*\(\s*count\s*\)/)) {
throw new Error('desktop shell badge module must call window.set_badge_count(count)');
}
if (
!desktopHostBridgeDispatchSource.includes(
'set_desktop_host_bridge_window_title(&app, &request)',
) ||
desktopHostBridgeDispatchSource.includes('WINDOW_TITLE_MAX_LENGTH') ||
desktopHostBridgeDispatchSource.includes('required_string_payload(&request, "title")') ||
desktopHostBridgeDispatchSource.includes('window.set_title')
) {
throw new Error('desktop shell title HostBridge method must delegate to title module');
}
for (const snippet of [
'WINDOW_TITLE_MAX_LENGTH',
'normalize_window_title',
'required_string_payload(request, "title")',
'window.set_title(&title)',
'"title is required"',
]) {
if (!desktopHostBridgeTitleSource.includes(snippet)) {
throw new Error(`desktop shell title module is missing ${snippet}`);
}
}
if (
!desktopHostBridgeDispatchSource.includes(
'show_desktop_local_notification(&app, &request)',
@@ -16,28 +16,17 @@ use crate::host_bridge::navigation::{
use crate::host_bridge::network::resolve_desktop_host_bridge_network_status;
use crate::host_bridge::notifications::show_desktop_local_notification;
use crate::host_bridge::protocol::{
failed, ok, required_string_payload, validate_request, HostBridgeRequest, HostBridgeResponse,
HostBridgeRuntime, HOST_BRIDGE_VERSION,
failed, ok, validate_request, HostBridgeRequest, HostBridgeResponse, HostBridgeRuntime,
HOST_BRIDGE_VERSION,
};
use crate::host_bridge::share::{
open_desktop_host_bridge_share, set_desktop_host_bridge_share_target,
};
use crate::host_bridge::title::set_desktop_host_bridge_window_title;
use crate::shell::webview::desktop_platform;
use serde_json::json;
use tauri::Manager;
use tauri_plugin_dialog::DialogExt;
const WINDOW_TITLE_MAX_LENGTH: usize = 80;
fn normalize_window_title(raw_title: &str) -> Option<String> {
let title = raw_title.trim();
if title.is_empty() || title.chars().any(char::is_control) {
return None;
}
Some(title.chars().take(WINDOW_TITLE_MAX_LENGTH).collect())
}
fn desktop_runtime() -> HostBridgeRuntime {
HostBridgeRuntime {
shell: "tauri_desktop",
@@ -274,23 +263,7 @@ pub(super) async fn execute_host_bridge_request(
}),
)
}
"app.setTitle" => {
let title = match required_string_payload(&request, "title")
.ok()
.and_then(normalize_window_title)
{
Some(title) => title,
None => return failed(request.id, "invalid_request", "title is required"),
};
match app.get_webview_window("main") {
Some(window) => match window.set_title(&title) {
Ok(()) => ok(request.id, json!(true)),
Err(error) => failed(request.id, "host_error", error.to_string()),
},
None => failed(request.id, "host_error", "main window not found"),
}
}
"app.setTitle" => set_desktop_host_bridge_window_title(&app, &request),
"app.setBadgeCount" => match set_desktop_app_badge_count(&app, &request) {
Ok(()) => ok(request.id, json!(true)),
Err(response) => response,
@@ -420,23 +393,4 @@ mod tests {
assert!(error.message.contains(method));
}
}
#[test]
fn window_title_normalization_requires_visible_text() {
assert_eq!(
normalize_window_title(" Genarrative "),
Some("Genarrative".to_string())
);
assert_eq!(normalize_window_title(""), None);
assert_eq!(normalize_window_title("Genarrative\nDev"), None);
let long_title = "甲".repeat(120);
assert_eq!(
normalize_window_title(&long_title)
.expect("truncated title")
.chars()
.count(),
80
);
}
}
@@ -9,6 +9,7 @@ mod network;
mod notifications;
pub(crate) mod protocol;
mod share;
mod title;
pub(crate) use protocol::HostBridgeReplayState;
pub(crate) use share::DesktopShareState;
@@ -0,0 +1,61 @@
use crate::host_bridge::protocol::{
failed, ok, required_string_payload, HostBridgeRequest, HostBridgeResponse,
};
use serde_json::json;
use tauri::Manager;
const WINDOW_TITLE_MAX_LENGTH: usize = 80;
fn normalize_window_title(raw_title: &str) -> Option<String> {
let title = raw_title.trim();
if title.is_empty() || title.chars().any(char::is_control) {
return None;
}
Some(title.chars().take(WINDOW_TITLE_MAX_LENGTH).collect())
}
pub(crate) fn set_desktop_host_bridge_window_title(
app: &tauri::AppHandle,
request: &HostBridgeRequest,
) -> HostBridgeResponse {
let title = match required_string_payload(request, "title")
.ok()
.and_then(normalize_window_title)
{
Some(title) => title,
None => return failed(request.id.clone(), "invalid_request", "title is required"),
};
match app.get_webview_window("main") {
Some(window) => match window.set_title(&title) {
Ok(()) => ok(request.id.clone(), json!(true)),
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
},
None => failed(request.id.clone(), "host_error", "main window not found"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn window_title_normalization_requires_visible_text() {
assert_eq!(
normalize_window_title(" Genarrative "),
Some("Genarrative".to_string())
);
assert_eq!(normalize_window_title(""), None);
assert_eq!(normalize_window_title("Genarrative\nDev"), None);
let long_title = "甲".repeat(120);
assert_eq!(
normalize_window_title(&long_title)
.expect("truncated title")
.chars()
.count(),
80
);
}
}