Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/tabTransfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,5 @@ test('source removal waits for destination completion', () => {
assert.match(broker, /pub fn complete_detached_tab/);
const claim = broker.slice(broker.indexOf('pub fn claim_detached_tab'), broker.indexOf('pub fn complete_detached_tab'));
assert.doesNotMatch(claim, /tab-transfer-claimed/);
assert.match(session, /invoke\('complete_detached_tab', \{ token: claimToken \}\)/);
assert.match(session, /invoke\('complete_detached_tab', \{ token \}\)/);
});
35 changes: 35 additions & 0 deletions scripts/windowOrganization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

const runtime = readFileSync('src-tauri/src/window_runtime.rs', 'utf8');
const session = readFileSync('src/lib/sessions/windowSession.svelte.ts', 'utf8');
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const tab = readFileSync('src/lib/components/Tab.svelte', 'utf8');
const titleBar = readFileSync('src/lib/components/TitleBar.svelte', 'utf8');

test('the runtime registers live viewer windows in stable creation order', () => {
assert.match(runtime, /window_registry/);
assert.match(runtime, /window_counter/);
assert.match(runtime, /pub fn set_window_meta/);
assert.match(runtime, /pub fn list_viewer_windows/);
assert.match(runtime, /list\.sort_by_key\(\|entry\| entry\.meta\.number\)/);
assert.match(runtime, /window_registry[\s\S]*?remove\(window\.label\(\)\)/);
});

test('moving to an existing window uses the acknowledged transfer protocol', () => {
assert.match(session, /async function transfer\(/);
assert.match(session, /invoke\('complete_detached_tab', \{ token \}\)/);
assert.match(session, /onTransferClaimed\(tabId\)/);
assert.match(session, /async function acceptOfferedTransfer/);
assert.match(viewer, /invoke\('offer_tab_to_window', \{ targetLabel, token \}\)/);
});

test('window organization exposes move, merge, and carry actions', () => {
assert.match(tab, /list_viewer_windows/);
assert.match(tab, /menu-tab-move/);
assert.match(viewer, /async function mergeAllWindowsHere/);
assert.match(viewer, /async function carryActiveTabToNextWindow/);
assert.match(viewer, /cmdOrCtrl && e\.shiftKey && key === 'm'/);
assert.match(titleBar, /onmergeAllWindows/);
});
29 changes: 29 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,31 @@ fn clear_window_state(app: AppHandle) -> Result<(), String> {
window_runtime::clear_window_state(app)
}

#[tauri::command]
fn set_window_meta(
window: tauri::Window,
state: State<'_, AppState>,
active_tab_title: String,
tab_count: usize,
) {
window_runtime::set_window_meta(window, state, active_tab_title, tab_count)
}

#[tauri::command]
fn list_viewer_windows(state: State<'_, AppState>) -> Vec<window_runtime::WindowListEntry> {
window_runtime::list_viewer_windows(state)
}

#[tauri::command]
fn offer_tab_to_window(app: AppHandle, target_label: String, token: String) -> Result<(), String> {
window_runtime::offer_tab_to_window(app, target_label, token)
}

#[tauri::command]
fn focus_window(app: AppHandle, label: String) -> Result<(), String> {
window_runtime::focus_window(app, label)
}

/// Byte ranges of code regions — fenced code blocks and inline code spans —
/// paired with CommonMark's rules. The regex alternation previously used for
/// protection (```` ```.*?```|`.*?` ````) cannot express them: a fence closes
Expand Down Expand Up @@ -1487,6 +1512,10 @@ pub fn run() {
tab_transfer::complete_detached_tab,
tab_transfer::cancel_detached_tab,
create_transfer_window,
set_window_meta,
list_viewer_windows,
offer_tab_to_window,
focus_window,
save_window_state,
load_window_state,
clear_window_state
Expand Down
82 changes: 81 additions & 1 deletion src-tauri/src/window_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::Mutex;
use std::sync::{
atomic::{AtomicU64, Ordering},
Mutex,
};
use tauri::{AppHandle, Emitter, Manager, State};

pub struct WatcherState {
Expand All @@ -20,17 +23,88 @@ impl WatcherState {
pub struct AppState {
pub(crate) startup_file: Mutex<Option<String>>,
pub(crate) last_focused_viewer: Mutex<Option<String>>,
window_registry: Mutex<HashMap<String, WindowMeta>>,
window_counter: AtomicU64,
}

impl AppState {
pub fn new() -> Self {
Self {
startup_file: Mutex::new(None),
last_focused_viewer: Mutex::new(None),
window_registry: Mutex::new(HashMap::new()),
window_counter: AtomicU64::new(0),
}
}
}

#[derive(Clone, serde::Serialize)]
struct WindowMeta {
number: u64,
active_tab_title: String,
tab_count: usize,
}

#[derive(Clone, serde::Serialize)]
pub struct WindowListEntry {
label: String,
#[serde(flatten)]
meta: WindowMeta,
}

pub fn set_window_meta(
window: tauri::Window,
state: State<'_, AppState>,
active_tab_title: String,
tab_count: usize,
) {
let label = window.label().to_string();
if label != "main" && !label.starts_with("window-") {
return;
}
let mut registry = state.window_registry.lock().unwrap();
let entry = registry.entry(label).or_insert_with(|| WindowMeta {
number: state.window_counter.fetch_add(1, Ordering::SeqCst) + 1,
active_tab_title: String::new(),
tab_count: 0,
});
entry.active_tab_title = active_tab_title;
entry.tab_count = tab_count;
}

pub fn list_viewer_windows(state: State<'_, AppState>) -> Vec<WindowListEntry> {
let registry = state.window_registry.lock().unwrap();
let mut list: Vec<WindowListEntry> = registry
.iter()
.map(|(label, meta)| WindowListEntry {
label: label.clone(),
meta: meta.clone(),
})
.collect();
list.sort_by_key(|entry| entry.meta.number);
list
}

pub fn offer_tab_to_window(
app: AppHandle,
target_label: String,
token: String,
) -> Result<(), String> {
if app.get_webview_window(&target_label).is_none() {
return Err(format!("no such window: {target_label}"));
}
app.emit_to(target_label.as_str(), "tab-transfer-offer", token)
.map_err(|error| error.to_string())
}

pub fn focus_window(app: AppHandle, label: String) -> Result<(), String> {
let window = app
.get_webview_window(&label)
.ok_or_else(|| format!("no such window: {label}"))?;
bring_to_front(&window);
Ok(())
}

pub async fn show_window(window: tauri::Window) {
let _ = window.show();
let _ = window.unminimize();
Expand Down Expand Up @@ -200,6 +274,12 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) {
tauri::WindowEvent::Destroyed => {
let state = window.state::<WatcherState>();
state.watchers.lock().unwrap().remove(window.label());
let app_state = window.state::<AppState>();
app_state
.window_registry
.lock()
.unwrap()
.remove(window.label());
}
_ => {}
}
Expand Down
Loading
Loading