Skip to content
Closed
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
16 changes: 16 additions & 0 deletions docs/LOCAL_WEBKIT_PREVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Local WebKit preview

Pasted can run against a locally built WebKit on macOS without changing its ordinary build, release artifacts, or dependency graph. This path is intended for WebKit development and before/after demonstrations; it is not a distributable Pasted build.

Build WebKit, install Pasted's locked dependencies, then pass the WebKit build-products directory to the preview launcher:

```sh
npm ci
npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release
```

The launcher builds Pasted's normal debug executable and CLI, starts Vite on loopback, and launches the executable directly with framework and XPC loader paths pointed at the supplied build. It uses `vmmap` to verify that the Pasted process actually loaded that `WebKit.framework`; a successful application launch without that verification is treated as a failure.

For recording safety, the launcher creates and seeds a temporary demonstration database, starts the preview with clipboard capture paused, and deletes that temporary database when the preview exits. It never opens or modifies the ordinary Pasted library. Quit Pasted to stop the preview and its Vite server. Set `PASTED_LOCAL_WEBKIT_SKIP_BUILD=1` to reuse the existing debug executables during repeated recording sessions.

The launcher does not bundle WebKit, modify Pasted's release configuration, replace the system framework, or alter the release build's database selection. The database override is compiled only into debug builds.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:local-webkit": "bash scripts/run-with-local-webkit.sh",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "cargo test --manifest-path src-tauri/Cargo.toml",
Expand All @@ -29,7 +30,7 @@
"test:transformations": "node --experimental-strip-types scripts/test-manual-transform-step-model.ts && node scripts/audit-transformations.js",
"test:activity": "node scripts/audit-activity-events.js",
"test:backup": "node --experimental-strip-types scripts/test-backup-client-state.js && node --experimental-strip-types scripts/test-app-ui-state.ts && node scripts/audit-full-backup.js",
"test:platform": "node scripts/audit-platform-chrome.js",
"test:platform": "node scripts/test-local-webkit-preview.js && node scripts/audit-platform-chrome.js",
"test:feedback": "node scripts/audit-capture-feedback.js",
"test:app-lock": "node --experimental-strip-types scripts/test-app-lock-idle.ts",
"bench:analysis": "cargo run --release --manifest-path src-tauri/Cargo.toml --example analysis_baseline --",
Expand Down
156 changes: 156 additions & 0 deletions scripts/run-with-local-webkit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
cat <<'EOF'
Usage: npm run dev:local-webkit -- /absolute/path/to/WebKitBuild/Release

Builds the Pasted debug application, starts its Vite frontend, and launches the
application directly with a local WebKit build selected for both the app and its
XPC services. The script exits unless vmmap confirms that Pasted loaded that
WebKit.framework instead of the system framework. It uses a temporary, seeded
database and pauses clipboard capture so private Pasted history cannot appear.

Set PASTED_LOCAL_WEBKIT_SKIP_BUILD=1 to reuse an existing debug application.
EOF
}

if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi

if [[ "$(uname -s)" != "Darwin" ]]; then
echo "A local WebKit preview can only run on macOS." >&2
exit 1
fi

if [[ $# -ne 1 ]]; then
usage >&2
exit 1
fi

webkit_build="$1"
if [[ "$webkit_build" != /* ]]; then
echo "Pass an absolute WebKit build directory." >&2
exit 1
fi

webkit_binary="$webkit_build/WebKit.framework/Versions/A/WebKit"
if [[ ! -f "$webkit_binary" ]]; then
echo "WebKit.framework was not found at $webkit_build." >&2
exit 1
fi

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
app_binary="$repo_root/src-tauri/target/debug/pasted-app"
cli_binary="$repo_root/src-tauri/target/debug/pasted"
preview_parent="${TMPDIR:-/tmp}"
preview_parent="${preview_parent%/}"
vite_pid=""
app_pid=""
preview_root=""

cleanup() {
local status=$?
trap - EXIT INT TERM HUP
if [[ -n "$app_pid" ]] && kill -0 "$app_pid" 2>/dev/null; then
kill "$app_pid" 2>/dev/null || true
wait "$app_pid" 2>/dev/null || true
fi
if [[ -n "$vite_pid" ]] && kill -0 "$vite_pid" 2>/dev/null; then
kill "$vite_pid" 2>/dev/null || true
wait "$vite_pid" 2>/dev/null || true
fi
if [[ -n "$preview_root" && "$preview_root" == "$preview_parent/pasted-local-webkit."* ]]; then
rm -rf -- "$preview_root"
fi
exit "$status"
}
trap cleanup EXIT INT TERM HUP

cd "$repo_root"

if [[ "${PASTED_LOCAL_WEBKIT_SKIP_BUILD:-0}" != "1" ]]; then
if [[ ! -d node_modules ]]; then
echo "Install the locked frontend dependencies with npm ci first." >&2
exit 1
fi
cargo build --locked --manifest-path src-tauri/Cargo.toml --bin pasted-app
cargo build --locked --manifest-path src-tauri/Cargo.toml --no-default-features --features cli --bin pasted
fi
Comment on lines +75 to +82

if [[ ! -x "$app_binary" || ! -x "$cli_binary" ]]; then
echo "Pasted's debug application and CLI were not found in src-tauri/target/debug." >&2
exit 1
fi

preview_root="$(mktemp -d "$preview_parent/pasted-local-webkit.XXXXXX")"
preview_database="$preview_root/pasted.db"
preview_seed="$preview_root/demo-clips.csv"
printf '%s\n' \
'id,content_type,source,is_pinned,created_at,name,text_content' \
'1,"text","Pasted Demo",true,"2026-09-01T18:00:00Z","Release engineering","Ship it, but make the pixels behave first."' \
'2,"text","Pasted Demo",false,"2026-09-01T18:01:00Z","WebKit field notes","Yellow and cyan have agreed to remain perfectly still."' \
'3,"text","Pasted Demo",false,"2026-09-01T18:02:00Z","Important research","Can a window corner outrun a layout viewport? Not anymore."' \
'4,"text","Pasted Demo",false,"2026-09-01T18:03:00Z","Extremely official memo","This clipboard contains zero secrets and several excellent rectangles."' \
> "$preview_seed"
PASTED_DATABASE_PATH="$preview_database" \
"$cli_binary" clip import "$preview_seed" --format csv >/dev/null

npm run dev -- --host 127.0.0.1 &
vite_pid=$!

vite_ready=false
for _ in {1..100}; do
if curl --silent --fail --output /dev/null http://127.0.0.1:1420/; then
vite_ready=true
break
fi
if ! kill -0 "$vite_pid" 2>/dev/null; then
wait "$vite_pid"
fi
sleep 0.1
done

if [[ "$vite_ready" != "true" ]]; then
echo "Vite did not become ready at http://127.0.0.1:1420/." >&2
exit 1
fi

env \
DYLD_FRAMEWORK_PATH="$webkit_build" \
__XPC_DYLD_FRAMEWORK_PATH="$webkit_build" \
DYLD_LIBRARY_PATH="$webkit_build" \
__XPC_DYLD_LIBRARY_PATH="$webkit_build" \
PASTED_PREVIEW_DATABASE_PATH="$preview_database" \
"$app_binary" --skip-welcome &
app_pid=$!

verified_path=""
alternate_build="${webkit_build#/private}"
for _ in {1..100}; do
if ! kill -0 "$app_pid" 2>/dev/null; then
wait "$app_pid"
fi
loaded_webkit="$(vmmap "$app_pid" 2>/dev/null | sed -n '/\/WebKit\.framework\/.*\/WebKit$/p' || true)"
if [[ "$loaded_webkit" == *"$webkit_build/WebKit.framework/"* \
|| "$loaded_webkit" == *"$alternate_build/WebKit.framework/"* ]]; then
verified_path="$loaded_webkit"
break
fi
sleep 0.1
done

if [[ -z "$verified_path" ]]; then
echo "Pasted launched, but vmmap did not confirm the requested WebKit.framework." >&2
exit 1
fi

echo "Verified local WebKit.framework:"
echo "$verified_path"
echo "Quit Pasted to stop the preview and its Vite server."

wait "$app_pid"
app_pid=""
37 changes: 37 additions & 0 deletions scripts/test-local-webkit-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';

const scriptPath = 'scripts/run-with-local-webkit.sh';
const source = fs.readFileSync(scriptPath, 'utf8');

const syntax = spawnSync('bash', ['-n', scriptPath], { encoding: 'utf8' });
assert.equal(syntax.status, 0, syntax.stderr);

const help = spawnSync('bash', [scriptPath, '--help'], { encoding: 'utf8' });
assert.equal(help.status, 0, help.stderr);
assert.match(help.stdout, /WebKitBuild\/Release/);
assert.match(help.stdout, /vmmap confirms/);
assert.match(help.stdout, /temporary, seeded/);

for (const variable of [
'DYLD_FRAMEWORK_PATH',
'__XPC_DYLD_FRAMEWORK_PATH',
'DYLD_LIBRARY_PATH',
'__XPC_DYLD_LIBRARY_PATH',
]) {
assert.match(source, new RegExp(`\\b${variable}=`), `${variable} must remain in the preview process environment`);
}

assert.match(source, /cargo build --locked/, 'The preview must build from the reviewed Rust lockfile');
assert.match(source, /--no-default-features --features cli --bin pasted/, 'The preview must build the CLI used to seed its isolated database');
assert.match(source, /npm run dev -- --host 127\.0\.0\.1/, 'The preview must bind Vite to loopback');
assert.match(source, /vmmap "\$app_pid"/, 'The preview must inspect the launched Pasted process');
assert.match(source, /mktemp -d/, 'The preview must isolate its demonstration database in a temporary directory');
assert.match(source, /PASTED_PREVIEW_DATABASE_PATH="\$preview_database"/, 'The GUI must use the isolated preview database');
assert.match(source, /PASTED_DATABASE_PATH="\$preview_database"/, 'The CLI must seed the same isolated preview database');
assert.match(source, /rm -rf -- "\$preview_root"/, 'The temporary preview database must be removed during cleanup');
assert.match(source, /kill "\$app_pid"/, 'Interrupted previews must stop the launched application');
assert.match(source, /kill "\$vite_pid"/, 'Interrupted previews must stop the Vite server');

console.log('Local WebKit preview launcher checks passed.');
16 changes: 15 additions & 1 deletion src-tauri/src/app_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ use tauri::{Emitter, Manager};

static EXIT_REQUESTED: AtomicBool = AtomicBool::new(false);

#[cfg(debug_assertions)]
fn preview_database_path() -> Option<std::path::PathBuf> {
std::env::var_os("PASTED_PREVIEW_DATABASE_PATH").map(Into::into)
}

#[cfg(not(debug_assertions))]
fn preview_database_path() -> Option<std::path::PathBuf> {
None
}

pub(crate) fn exit_requested() -> bool {
EXIT_REQUESTED.load(Ordering::SeqCst)
}
Expand Down Expand Up @@ -68,7 +78,10 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Erro
.path()
.app_data_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("./pasted_data"));
let db_path = crate::library_storage::resolve_database_path(&app_dir);
let preview_database_path = preview_database_path();
let db_path = preview_database_path
.clone()
.unwrap_or_else(|| crate::library_storage::resolve_database_path(&app_dir));
let db_state =
Arc::new(crate::db::DbState::new(db_path).expect("Failed to initialize SQLite database"));
if startup_args
Expand Down Expand Up @@ -118,6 +131,7 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Erro
db_state.clone(),
queue_state,
ocr_service,
preview_database_path.is_some(),
);
app.manage(Arc::new(crate::clipboard_monitor::ClipboardMonitorState {
is_manually_paused: monitor_handle.is_manually_paused.clone(),
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/clipboard_monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ pub fn start_clipboard_monitor(
db_state: Arc<DbState>,
seq_state: Arc<SequentialQueueState>,
ocr_service: Arc<crate::ocr::OcrService>,
initially_paused: bool,
) -> MonitorHandle {
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();

let is_manually_paused = Arc::new(AtomicBool::new(false));
let is_manually_paused = Arc::new(AtomicBool::new(initially_paused));
let is_auto_paused = Arc::new(AtomicBool::new(false));

let is_manually_paused_clone = is_manually_paused.clone();
Expand Down
Loading