Remove the node switcher UI, native title-bar integration, registry persistence, and dedicated dependencies

Hop-State: A_06FPH4TF27M92NB8GVKNGKG
Hop-Proposal: R_06FPH4T0F0KDZV6BB8BQ0J8
Hop-Task: T_06FPH2162KK06P10J2MY3SR
Hop-Attempt: AT_06FPH2162H2HW7STZZWQFJR
This commit is contained in:
2026-07-15 18:38:32 -07:00
committed by Hop
parent 016fcc6c94
commit d04b9633dc
5 changed files with 13 additions and 315 deletions
-25
View File
@@ -2267,17 +2267,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.13.1",
"block2",
"libc",
"objc2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-text",
"objc2-core-video",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
@@ -2297,7 +2289,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [
"bitflags 2.13.1",
"objc2",
"objc2-foundation",
]
@@ -2358,19 +2349,6 @@ dependencies = [
"objc2-core-graphics",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags 2.13.1",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
@@ -3699,9 +3677,6 @@ name = "synapsis-desktop"
version = "0.1.0"
dependencies = [
"http",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"reqwest 0.12.28",
"serde",
"serde_json",
-18
View File
@@ -24,23 +24,5 @@ serde_json = "1"
reqwest = { version = "0.12", features = ["cookies", "json", "rustls-tls"] }
tauri-plugin-notification = "2"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = [
"NSButton",
"NSCell",
"NSControl",
"NSLayoutConstraint",
"NSMenu",
"NSMenuItem",
"NSPopUpButton",
"NSResponder",
"NSTitlebarAccessoryViewController",
"NSView",
"NSViewController",
"NSWindow",
] }
objc2-foundation = { version = "0.3", features = ["NSArray", "NSString"] }
[dev-dependencies]
http = "1"
+2 -245
View File
@@ -1,7 +1,5 @@
use reqwest::{Client, Method, StatusCode, Url};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::Duration;
@@ -12,15 +10,6 @@ struct AppState {
client: Client,
notification_capability_sequence: AtomicU64,
entry_url: Mutex<Option<Url>>,
node_registry: Mutex<NodeRegistry>,
node_registry_path: Mutex<Option<PathBuf>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct NodeRegistry {
nodes: Vec<String>,
active: Option<String>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
@@ -109,56 +98,6 @@ fn endpoint(base: &Url, path: &str) -> Result<Url, String> {
.map_err(|_| "The selected node address is invalid.".into())
}
fn load_node_registry(path: &Path) -> NodeRegistry {
fs::read(path)
.ok()
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap_or_default()
}
fn persist_node_registry(state: &AppState, registry: &NodeRegistry) -> Result<(), String> {
let path = state
.node_registry_path
.lock()
.map_err(|_| "Couldnt access the saved node list.".to_string())?
.clone();
let Some(path) = path else {
return Ok(());
};
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("Couldnt save the node list: {error}"))?;
}
let bytes = serde_json::to_vec_pretty(registry)
.map_err(|error| format!("Couldnt encode the node list: {error}"))?;
fs::write(path, bytes).map_err(|error| format!("Couldnt save the node list: {error}"))
}
fn remember_node(state: &AppState, node: &Url) -> Result<NodeRegistry, String> {
let node_url = node.as_str().to_owned();
let snapshot = {
let mut registry = state
.node_registry
.lock()
.map_err(|_| "Couldnt access the saved node list.".to_string())?;
registry.nodes.retain(|saved| saved != &node_url);
registry.nodes.push(node_url.clone());
registry.active = Some(node_url);
registry.clone()
};
persist_node_registry(state, &snapshot)?;
Ok(snapshot)
}
#[tauri::command]
fn get_saved_nodes(state: State<'_, AppState>) -> Result<NodeRegistry, String> {
state
.node_registry
.lock()
.map(|registry| registry.clone())
.map_err(|_| "Couldnt access the saved node list.".to_string())
}
async fn decode_response<T: DeserializeOwned>(response: reqwest::Response) -> Result<T, String> {
let status = response.status();
let bytes = response.bytes().await.map_err(|error| error.to_string())?;
@@ -343,13 +282,6 @@ async fn open_web_app(
);
}
}
let registry = remember_node(&state, &base)?;
#[cfg(target_os = "macos")]
macos_titlebar::install(&webview, &registry)?;
navigate_to_node(&state, &webview, &base)
}
fn navigate_to_node(state: &AppState, webview: &WebviewWindow, base: &Url) -> Result<(), String> {
let origin = base.origin().ascii_serialization();
let capability_id = state
.notification_capability_sequence
@@ -359,7 +291,7 @@ fn navigate_to_node(state: &AppState, webview: &WebviewWindow, base: &Url) -> Re
.add_capability(capability)
.map_err(|error| format!("Couldnt enable native notifications for this node: {error}"))?;
webview
.navigate(base.clone())
.navigate(base)
.map_err(|error| format!("Couldnt open the Synapsis web app: {error}"))
}
@@ -377,145 +309,6 @@ fn remote_notification_capability(origin: &str, sequence: u64) -> String {
.to_string()
}
#[cfg(target_os = "macos")]
mod macos_titlebar {
use super::*;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{define_class, msg_send, sel, MainThreadOnly};
use objc2_app_kit::{
NSControlStateValueOn, NSLayoutAttribute, NSMenu, NSMenuItem, NSPopUpButton,
NSTitlebarAccessoryViewController, NSView, NSWindow,
};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString};
use std::sync::OnceLock;
static APP_HANDLE: OnceLock<tauri::AppHandle> = OnceLock::new();
define_class!(
#[unsafe(super = NSTitlebarAccessoryViewController)]
#[thread_kind = MainThreadOnly]
#[ivars = ()]
struct NodeTitlebarController;
impl NodeTitlebarController {
#[unsafe(method(selectNode:))]
fn select_node(&self, sender: &NSMenuItem) {
let Some(value) = sender
.representedObject()
.and_then(|object| object.downcast::<NSString>().ok())
.map(|string| string.to_string())
else {
return;
};
let Some(app) = APP_HANDLE.get() else {
return;
};
let Some(webview) = app.get_webview_window("main") else {
return;
};
let state = app.state::<AppState>();
if value == "__add_node__" {
let Ok(saved_entry_url) = state.entry_url.lock() else {
return;
};
let Some(mut entry_url) = saved_entry_url.clone() else {
return;
};
entry_url.set_query(Some("addNode=1"));
let _ = webview.navigate(entry_url);
return;
}
let Ok(base) = normalize_node(&value) else {
return;
};
let Ok(registry) = remember_node(&state, &base) else {
return;
};
let _ = install(&webview, &registry);
let _ = navigate_to_node(&state, &webview, &base);
}
}
unsafe impl NSObjectProtocol for NodeTitlebarController {}
);
impl NodeTitlebarController {
fn new(mtm: MainThreadMarker) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
pub fn initialize(app: &tauri::AppHandle) {
let _ = APP_HANDLE.set(app.clone());
}
pub fn install(webview: &WebviewWindow, registry: &NodeRegistry) -> Result<(), String> {
let mtm = MainThreadMarker::new()
.ok_or_else(|| "The node switcher must be created on the main thread.".to_string())?;
let controller = NodeTitlebarController::new(mtm);
let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str("Nodes"));
for node_url in &registry.nodes {
let Ok(url) = Url::parse(node_url) else {
continue;
};
let title = url.host_str().unwrap_or(node_url);
let item = unsafe {
menu.addItemWithTitle_action_keyEquivalent(
&NSString::from_str(title),
Some(sel!(selectNode:)),
&NSString::new(),
)
};
unsafe {
item.setTarget(Some(&*controller as &AnyObject));
item.setRepresentedObject(Some(&*NSString::from_str(node_url) as &AnyObject));
}
if registry.active.as_deref() == Some(node_url) {
item.setState(NSControlStateValueOn);
}
}
if !registry.nodes.is_empty() {
menu.addItem(&NSMenuItem::separatorItem(mtm));
}
let add_item = unsafe {
menu.addItemWithTitle_action_keyEquivalent(
&NSString::from_str("Add Node…"),
Some(sel!(selectNode:)),
&NSString::new(),
)
};
unsafe {
add_item.setTarget(Some(&*controller as &AnyObject));
add_item.setRepresentedObject(Some(&*NSString::from_str("__add_node__") as &AnyObject));
}
let button =
NSPopUpButton::pullDownButtonWithTitle_menu(&NSString::from_str("Nodes"), &menu);
button.setBordered(false);
button.setFrame(NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(86.0, 28.0)));
controller.setView(unsafe { &*(&*button as *const NSPopUpButton as *const NSView) });
controller.setLayoutAttribute(NSLayoutAttribute::Right);
controller.setAutomaticallyAdjustsSize(false);
let window = unsafe {
let pointer = webview
.ns_window()
.map_err(|error| format!("Couldnt access the desktop title bar: {error}"))?;
&*(pointer as *const NSWindow)
};
while window.titlebarAccessoryViewControllers().count() > 0 {
window.removeTitlebarAccessoryViewControllerAtIndex(0);
}
window.addTitlebarAccessoryViewController(&controller);
Ok(())
}
}
#[tauri::command]
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
let base = normalize_node(&node_url)?;
@@ -548,41 +341,6 @@ pub fn run() {
client,
notification_capability_sequence: AtomicU64::new(0),
entry_url: Mutex::new(None),
node_registry: Mutex::new(NodeRegistry::default()),
node_registry_path: Mutex::new(None),
})
.setup(|app| {
let webview = app
.get_webview_window("main")
.ok_or_else(|| "Couldnt find the main Synapsis window.".to_string())?;
let registry_path = app
.path()
.app_data_dir()
.map_err(|error| format!("Couldnt find the app data folder: {error}"))?
.join("nodes.json");
let registry = load_node_registry(&registry_path);
let state = app.state::<AppState>();
*state
.entry_url
.lock()
.map_err(|_| "Couldnt remember the app entry screen.".to_string())? =
Some(webview.url()?);
*state
.node_registry_path
.lock()
.map_err(|_| "Couldnt prepare the saved node list.".to_string())? =
Some(registry_path);
*state
.node_registry
.lock()
.map_err(|_| "Couldnt prepare the saved node list.".to_string())? =
registry.clone();
#[cfg(target_os = "macos")]
{
macos_titlebar::initialize(app.handle());
macos_titlebar::install(&webview, &registry)?;
}
Ok(())
})
.on_page_load(|webview, payload| {
if payload.event() != PageLoadEvent::Finished
@@ -632,8 +390,7 @@ pub fn run() {
register,
logout,
has_web_session,
open_web_app,
get_saved_nodes
open_web_app
])
.run(tauri::generate_context!())
.expect("error while running Synapsis");