host: the tab bridge ships from waybetter-host; create-waybetter uses it

tab-bridge.js (moved from wayBetter for X) is served by bridge::SOURCE,
or by bridge::script(BridgeOptions) with shortcut and link-drag
forwarding switched off for a shell that does not handle them (a
forwarded shortcut is taken from the page). Tests: one options line,
only tab_report/tab_shortcut/tab_link_drag, every forwarded shortcut is
one tabs::valid_shortcut accepts. Generated apps now inject
bridge::script(BridgeOptions::REPORT_ONLY) instead of writing their own
duplicated src-tauri/runtime/tab-bridge.js.
This commit is contained in:
CRTkafa 2026-09-29 11:22:54 +07:00
parent 3e6cbe0773
commit d3ed72ea4d
4 changed files with 209 additions and 37 deletions

View file

@ -0,0 +1,67 @@
//! The tab bridge: the script every tab webview gets as a main-frame initialization script
//! ([`script`]). It reports the tab's own URL, title, favicon and focus to the shell (`tab_report`)
//! and, when the app enables them, forwards browser shortcuts pressed in the page (`tab_shortcut`,
//! [`crate::tabs::valid_shortcut`]) and link drags (`tab_link_drag`, [`crate::tabs::link_drag`]).
//! The app's `tab-chrome` capability grants exactly the commands it calls, about the tab itself.
/// The bridge source (`tab-bridge.js` next to this crate's `Cargo.toml`), all features on.
pub const SOURCE: &str = include_str!("../tab-bridge.js");
const DEFAULT_OPTIONS: &str = "const options = { shortcuts: true, linkDrags: true };";
/// What the bridge forwards besides the tab report. Turn a feature off when the app's shell does
/// not handle it: a forwarded shortcut is taken from the page (`preventDefault`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BridgeOptions {
/// Browser shortcuts (new/close/reopen tab, reload, back/forward, split, palette, deck keys).
pub shortcuts: bool,
/// Link drags out of (or into) the page, for the shell's split/tab drop zones.
pub link_drags: bool,
}
impl BridgeOptions {
pub const ALL: Self = Self { shortcuts: true, link_drags: true };
/// Only `tab_report` (URL, title, favicon, focus).
pub const REPORT_ONLY: Self = Self { shortcuts: false, link_drags: false };
}
/// The bridge script with `options`.
pub fn script(options: BridgeOptions) -> String {
let set = format!("const options = {{ shortcuts: {}, linkDrags: {} }};", options.shortcuts, options.link_drags);
SOURCE.replacen(DEFAULT_OPTIONS, &set, 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn options_are_set_in_the_one_options_line() {
assert_eq!(SOURCE.matches(DEFAULT_OPTIONS).count(), 1);
assert_eq!(script(BridgeOptions::ALL), SOURCE);
let report = script(BridgeOptions::REPORT_ONLY);
assert!(report.contains("const options = { shortcuts: false, linkDrags: false };"));
assert!(!report.contains(DEFAULT_OPTIONS));
assert!(report.contains("if (options.shortcuts) addEventListener('keydown'"));
assert!(report.contains("if (options.linkDrags) {"));
}
#[test]
fn bridge_calls_only_the_tab_chrome_commands() {
let mut commands: Vec<&str> = SOURCE.match_indices("invoke('").map(|(at, _)| SOURCE[at + 8..].split('\'').next().unwrap()).collect();
commands.sort();
commands.dedup();
assert_eq!(commands, ["tab_link_drag", "tab_report", "tab_shortcut"]);
assert!(SOURCE.contains("if (window.top !== window || window.__wbTabBridge) return;"), "top frame only, once");
}
#[test]
fn forwarded_shortcuts_are_ones_the_host_accepts() {
for name in ["deck-toggle", "deck-compose", "column-page-prev", "column-page-next", "column-prev", "column-next", "reopen-tab", "new-tab",
"open-panel", "close-tab", "focus-address", "command-palette", "prev-tab", "next-tab", "hard-reload", "reload", "split-down",
"split-right", "back", "forward"] {
assert!(SOURCE.contains(&format!("'{name}'")), "{name} is not in the bridge");
assert!(crate::tabs::valid_shortcut(name), "{name} would be refused");
}
assert!(crate::tabs::valid_shortcut("column-3"));
}
}