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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ Here are the key development loops:

```sh
pnpm install
pnpm dev:website # http://localhost:5173/playground
pnpm storybook # http://localhost:6006
pnpm test # runs all tests
pnpm dev:website # vite hotreload at http://localhost:5173/playground
pnpm dev:standalone # tauri hotreload

pnpm dogfood:vscode # builds the VSCode extension and installs it into your local VSCode
pnpm dogfood:standalone # builds and runs the standalone app
pnpm dogfood:standalone --install # installs your local build overtop of your existing system installation

pnpm storybook # http://localhost:6006
pnpm test # runs all tests
```

### Folder structure
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"build:standalone": "pnpm --filter mouseterm-standalone tauri build",
"build:website": "pnpm --filter mouseterm-website build",
"dogfood:vscode": "pnpm run build:vscode && pnpm --filter mouseterm dogfood",
"dogfood:standalone": "bash standalone/scripts/dogfood.sh",
"storybook": "pnpm --filter mouseterm-lib storybook"
},
"pnpm": {
Expand Down
77 changes: 77 additions & 0 deletions standalone/scripts/dogfood.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
#
# Builds the standalone app and either launches or installs it.
#
# Usage:
# pnpm dogfood:standalone Build and launch from the build directory.
# pnpm dogfood:standalone --install Build and copy into the system install location.
#
# Launch mode (default):
# Runs the built binary directly from target/release. Works on Windows, macOS,
# and Linux with no prior setup. This is the fastest way to test changes.
#
# Install mode (--install):
# Copies the built files over the system-installed copy, bypassing the slow
# bundling/installer step. Requires a one-time install via the NSIS installer
# so that registry entries, shortcuts, etc. are in place. Currently Windows only.
#
set -euo pipefail

# Skip past "--" that pnpm injects when forwarding arguments
[[ "${1:-}" == "--" ]] && shift

RELEASE_DIR="standalone/src-tauri/target/release"

if [[ "${1:-}" == "--install" ]]; then
# Full build with bundling, but disable updater artifact signing
pnpm --filter mouseterm-standalone tauri build \
-c '{"bundle":{"createUpdaterArtifacts":false}}'
else
# Fast build: skip bundling entirely since we just need the exe
pnpm --filter mouseterm-standalone tauri build --no-bundle
fi

if [[ "${1:-}" == "--install" ]]; then
# --- Install mode ---
# Platform-specific: copy built files to system install location
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*|Windows_NT)
INSTALL_DIR="$LOCALAPPDATA/MouseTerm"
if [[ ! -f "$INSTALL_DIR/uninstall.exe" ]]; then
echo "MouseTerm is not installed yet."
echo "Run the installer once first:"
echo " $RELEASE_DIR/bundle/nsis/MouseTerm_*-setup.exe"
echo ""
echo "After that, 'dogfood:standalone --install' will work from then on."
exit 1
fi
# Wipe everything except uninstall.exe (managed by NSIS), then copy
TMP_UNINSTALL="$(mktemp)"
cp "$INSTALL_DIR/uninstall.exe" "$TMP_UNINSTALL"
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
mv "$TMP_UNINSTALL" "$INSTALL_DIR/uninstall.exe"
cp "$RELEASE_DIR/mouseterm.exe" "$INSTALL_DIR/"
cp "$RELEASE_DIR/node.exe" "$INSTALL_DIR/"
cp -r "$RELEASE_DIR/_up_/" "$INSTALL_DIR/_up_/"
echo "✦ Installed to $INSTALL_DIR"
;;
*)
echo "--install is not yet implemented for this platform."
exit 1
;;
esac
else
# --- Launch mode (default) ---
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*|Windows_NT)
"$RELEASE_DIR/mouseterm.exe" ;;
Darwin)
"$RELEASE_DIR/mouseterm" ;;
Linux)
"$RELEASE_DIR/mouseterm" ;;
*)
echo "Unsupported platform: $(uname -s)"
exit 1 ;;
esac
fi
1 change: 1 addition & 0 deletions standalone/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions standalone/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[profile.release]
strip = true
lto = true
Expand Down
42 changes: 38 additions & 4 deletions standalone/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
sync::{Arc, Mutex},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tauri::{AppHandle, Emitter, Manager};
use tauri::{AppHandle, Emitter, Manager, RunEvent};
use tauri_plugin_shell::{process::CommandEvent, ShellExt};

enum SidecarMsg {
Expand All @@ -26,6 +26,7 @@ struct SidecarState {
tx: SidecarSender,
pending_requests: PendingRequests,
next_request_id: AtomicU64,
child_pid: u32,
}

const LOG_FILE_ENV: &str = "MOUSETERM_LOG_FILE";
Expand Down Expand Up @@ -215,6 +216,28 @@ fn pty_get_scrollback(
#[tauri::command]
fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) {
let _ = state.tx.send(SidecarMsg::Shutdown);
kill_process_tree(state.child_pid);
}

/// Kill the sidecar process. On Windows, `taskkill /T` kills the entire
/// process tree so that child shell processes don't outlive the sidecar.
/// On Unix, a single SIGTERM to the sidecar is sufficient because node-pty
/// manages its own child processes and cleans them up on exit.
fn kill_process_tree(pid: u32) {
append_log(format!("[sidecar] killing process tree (pid={pid})"));
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let _ = std::process::Command::new("taskkill")
.args(["/F", "/T", "/PID", &pid.to_string()])
.creation_flags(CREATE_NO_WINDOW)
.output();
}
#[cfg(unix)]
{
unsafe { libc::kill(pid as i32, libc::SIGTERM); }
}
}

#[tauri::command]
Expand Down Expand Up @@ -298,7 +321,8 @@ fn start_sidecar(app: &AppHandle) -> Result<SidecarState, String> {
.set_raw_out(false)
.spawn()
.map_err(|err| format!("failed to start Node.js sidecar: {err}"))?;
append_log("[sidecar] spawned Node.js runtime");
let child_pid = child.pid();
append_log(format!("[sidecar] spawned Node.js runtime (pid={child_pid})"));

let handle = app.clone();
let pending_requests: PendingRequests = Arc::new(Mutex::new(HashMap::new()));
Expand Down Expand Up @@ -392,6 +416,7 @@ fn start_sidecar(app: &AppHandle) -> Result<SidecarState, String> {
tx,
pending_requests,
next_request_id: AtomicU64::new(0),
child_pid,
})
}

Expand Down Expand Up @@ -437,8 +462,17 @@ pub fn run() {
get_project_dir,
get_available_shells,
])
.run(tauri::generate_context!())
.expect("error while running MouseTerm");
.build(tauri::generate_context!())
.expect("error while building MouseTerm")
.run(|app, event| {
if let RunEvent::Exit = event {
if let Some(state) = app.try_state::<SidecarState>() {
append_log("[app] exit — killing sidecar");
let _ = state.tx.send(SidecarMsg::Shutdown);
kill_process_tree(state.child_pid);
}
}
});
}

#[cfg(test)]
Expand Down
Loading