Browser pairing, notifications, element hiding; drop the top bar

Pairing decrypts a Chromium browser's cookie store (PBKDF2-HMAC-SHA1
against its Keychain key, then AES-128-CBC) and injects the result into
WKHTTPCookieStore. Only the configured apps' hosts and their sign-in
hosts survive the filter. Browsers are offered most-recently-used first,
since the first entry becomes the default and someone with four
Chromium browsers installed wants the one they actually browse in.

WKWebView defines window.Notification but it does nothing: constructing
one throws no error and shows no banner, so a page believes it notified
you. Measured on the machine as `api=function shim=no` before the shim
was made unconditional; `from page: Odoo - Test notification -> raised`
after.

Anything on a page can be right-clicked away. The rule is re-asserted on
every navigation, because the injected script only carries a snapshot
from when the view was built and a selector added since would otherwise
come back on reload.

The top bar is gone. Navigation lives beside the cog, the nav carries
the traffic lights, and two-finger swipe goes back and forward.

The seed is now the real app list, scoped to exact hosts so a Drive link
inside Gmail switches rather than being swallowed.
This commit is contained in:
2026-09-01 12:17:31 +02:00
parent e369c82774
commit ff4a0c6bc4
44 changed files with 2492 additions and 419 deletions
+31 -13
View File
@@ -29,6 +29,10 @@ pub struct App {
pub group_id: Option<String>,
#[serde(default)]
pub user_agent: Option<String>,
/// CSS selectors this app hides on every page. Chosen by right-clicking
/// the thing you never want to see again.
#[serde(default)]
pub hidden: Vec<String>,
#[serde(default)]
pub order: i32,
}
@@ -167,8 +171,11 @@ pub fn new_id() -> String {
uuid::Uuid::new_v4().to_string()
}
/// First-run contents: enough apps, across enough groups, that grouping and
/// cross-app link routing can both be seen working without typing anything.
/// First-run contents: the tools this was built for.
///
/// Each scope is the exact host, so a link from Gmail to Drive switches rather
/// than being swallowed by whichever Google app happens to be showing. The
/// `/u/N/` paths pin the second Google account, which is the one that matters.
pub fn seed() -> Config {
let mk = |name: &str, url: &str, group: &str, order: i32| App {
id: new_id(),
@@ -177,22 +184,20 @@ pub fn seed() -> Config {
scope: default_scope(url).into_iter().collect(),
group_id: Some(group.into()),
user_agent: None,
hidden: Vec::new(),
order,
};
Config {
version: 1,
groups: vec![
Group { id: "g-google".into(), name: "Google".into(), collapsed: false, order: 0 },
Group { id: "g-dev".into(), name: "Dev".into(), collapsed: false, order: 1 },
Group { id: "g-ref".into(), name: "Reference".into(), collapsed: false, order: 2 },
Group { id: "g-work".into(), name: "Work".into(), collapsed: false, order: 0 },
Group { id: "g-google".into(), name: "Google".into(), collapsed: false, order: 1 },
],
apps: vec![
mk("Google", "https://www.google.com", "g-google", 0),
mk("YouTube", "https://www.youtube.com", "g-google", 1),
mk("GitHub", "https://github.com", "g-dev", 0),
mk("Hacker News", "https://news.ycombinator.com", "g-dev", 1),
mk("Wikipedia", "https://en.wikipedia.org", "g-ref", 0),
mk("MDN", "https://developer.mozilla.org", "g-ref", 1),
mk("Odoo", "https://example.odoo.com/web", "g-work", 0),
mk("Gmail", "https://mail.google.com/mail/u/N/", "g-google", 0),
mk("Drive", "https://drive.google.com/drive/u/N/my-drive", "g-google", 1),
mk("Chat", "https://chat.google.com/u/N/app/home", "g-google", 2),
],
settings: Settings::default(),
}
@@ -240,10 +245,23 @@ mod tests {
assert_eq!(normalize_url("http://intranet.local"), "http://intranet.local");
}
#[test]
fn seeded_scopes_are_exact_hosts_that_cannot_swallow_each_other() {
// The whole point of exact hosts: a Drive link inside Gmail must match
// Drive, not Gmail. A shared `google.com` scope would break that.
let cfg = seed();
let scopes: Vec<String> = cfg.apps.iter().flat_map(|a| a.scopes()).collect();
assert!(scopes.contains(&"mail.google.com".to_string()));
assert!(scopes.contains(&"drive.google.com".to_string()));
assert!(scopes.contains(&"chat.google.com".to_string()));
assert!(scopes.contains(&"example.odoo.com".to_string()));
assert!(!scopes.contains(&"google.com".to_string()));
}
#[test]
fn seed_apps_all_carry_a_scope() {
let cfg = seed();
assert_eq!(cfg.apps.len(), 6);
assert_eq!(cfg.apps.len(), 4);
assert!(cfg.apps.iter().all(|a| !a.scopes().is_empty()));
}
@@ -251,7 +269,7 @@ mod tests {
fn ordering_follows_group_then_position() {
let cfg = seed();
let names: Vec<&str> = cfg.ordered().iter().map(|a| a.name.as_str()).collect();
assert_eq!(names, ["Google", "YouTube", "GitHub", "Hacker News", "Wikipedia", "MDN"]);
assert_eq!(names, ["Odoo", "Gmail", "Drive", "Chat"]);
}
#[test]