收口桌面窗口标题桥接边界
将 Tauri 窗口标题 HostBridge 处理收口到 title 模块 让桌面 dispatch 只委托窗口标题请求 同步桌面壳结构门禁、架构文档和共享记忆
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user