aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 16:27:42 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 16:29:45 +0300
commit0956ad8c5883014ebf88664099fae41edd3fdc9e (patch)
tree8fb3ad282872547a9be2e9790b157d1bc90ca8c2 /src
parent745ce4254c646922d667ec13dd21963656405c12 (diff)
feat: add windows preview installer
Diffstat (limited to 'src')
-rw-r--r--src/protocol/wire.rs67
-rw-r--r--src/update.rs121
2 files changed, 164 insertions, 24 deletions
diff --git a/src/protocol/wire.rs b/src/protocol/wire.rs
index 04d1b1ae..357f414e 100644
--- a/src/protocol/wire.rs
+++ b/src/protocol/wire.rs
@@ -332,9 +332,6 @@ pub enum ClientMessage {
data: Vec<u8>,
},
- /// Structured input events from platform clients that do not expose Unix-style raw bytes.
- InputEvents { events: Vec<ClientInputEvent> },
-
/// Image bytes read from the client's local clipboard for remote paste bridging.
ClipboardImage {
/// Image file extension without a leading dot.
@@ -381,6 +378,9 @@ pub enum ClientMessage {
/// Crossterm-compatible modifier bits for forwarded mouse wheel events.
modifiers: u8,
},
+
+ /// Structured input events from platform clients that do not expose Unix-style raw bytes.
+ InputEvents { events: Vec<ClientInputEvent> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -929,6 +929,67 @@ mod tests {
}
#[test]
+ fn client_message_wire_tags_preserve_protocol_13_order() {
+ fn tag(msg: &ClientMessage) -> u8 {
+ *bincode::serde::encode_to_vec(msg, bincode::config::standard())
+ .unwrap()
+ .first()
+ .expect("encoded client message should include enum tag")
+ }
+
+ assert_eq!(
+ tag(&ClientMessage::Hello {
+ version: PROTOCOL_VERSION,
+ cols: 80,
+ rows: 24,
+ cell_width_px: 8,
+ cell_height_px: 16,
+ requested_encoding: RenderEncoding::SemanticFrame,
+ keybindings: ClientKeybindings::Server,
+ launch_mode: ClientLaunchMode::App,
+ }),
+ 0
+ );
+ assert_eq!(tag(&ClientMessage::Input { data: Vec::new() }), 1);
+ assert_eq!(
+ tag(&ClientMessage::ClipboardImage {
+ extension: "png".to_owned(),
+ data: Vec::new(),
+ }),
+ 2
+ );
+ assert_eq!(
+ tag(&ClientMessage::Resize {
+ cols: 80,
+ rows: 24,
+ cell_width_px: 8,
+ cell_height_px: 16,
+ }),
+ 3
+ );
+ assert_eq!(tag(&ClientMessage::Detach), 4);
+ assert_eq!(
+ tag(&ClientMessage::AttachTerminal {
+ terminal_id: "term".to_owned(),
+ takeover: false,
+ }),
+ 5
+ );
+ assert_eq!(
+ tag(&ClientMessage::AttachScroll {
+ source: AttachScrollSource::Wheel,
+ direction: AttachScrollDirection::Up,
+ lines: 1,
+ column: None,
+ row: None,
+ modifiers: 0,
+ }),
+ 6
+ );
+ assert_eq!(tag(&ClientMessage::InputEvents { events: Vec::new() }), 7);
+ }
+
+ #[test]
fn client_input_events_roundtrip() {
let msg = ClientMessage::InputEvents {
events: vec![
diff --git a/src/update.rs b/src/update.rs
index 4f192e3a..6b428b78 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -609,6 +609,42 @@ fn install_downloaded_update(mut update: DownloadedUpdate) -> Result<(), String>
Ok(())
}
+#[cfg(windows)]
+fn install_windows_update_with_installer(channel: UpdateChannel) -> Result<(), String> {
+ let status = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ "irm https://herdr.dev/install.ps1 | iex",
+ ])
+ .env("HERDR_CHANNEL", channel.as_str())
+ .status()
+ .map_err(|err| format!("failed to run Windows installer: {err}"))?;
+
+ if !status.success() {
+ return Err(format!("Windows installer failed with status {status}"));
+ }
+
+ Ok(())
+}
+
+#[cfg(windows)]
+fn windows_installed_herdr_exe_path() -> Result<PathBuf, String> {
+ if let Some(install_dir) = env::var_os("HERDR_INSTALL_DIR").filter(|value| !value.is_empty()) {
+ return Ok(PathBuf::from(install_dir).join("herdr.exe"));
+ }
+
+ let local_app_data = env::var_os("LOCALAPPDATA")
+ .ok_or("LOCALAPPDATA is not set; cannot locate Herdr install")?;
+ Ok(PathBuf::from(local_app_data)
+ .join("Programs")
+ .join("Herdr")
+ .join("bin")
+ .join("herdr.exe"))
+}
+
// ---------------------------------------------------------------------------
// Upgrade flow helpers
// ---------------------------------------------------------------------------
@@ -1901,32 +1937,73 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
if let Some(commit) = &release.commit {
tracing::info!(commit = %commit, build_id = ?release.build_id, "selected preview update build");
}
- eprintln!("downloading {}...", release.label());
if let Err(e) = crate::release_notes::save_pending(release.label(), &release.notes_body) {
tracing::warn!("failed to save pending release notes: {e}");
}
- let downloaded_update = download_update(&release)?;
- let updated_exe = downloaded_update.current_exe.clone();
- eprintln!("downloaded {}", release.label());
- if !options.live_handoff
- && !prompt_to_complete_plain_update(&server_update_decisions, &release)?
+
+ #[cfg(windows)]
{
- eprintln!("Herdr was not updated.");
- eprintln!("Stop running Herdr sessions when ready, then run `herdr update` again.");
- return Ok(current);
- }
- install_downloaded_update(downloaded_update)?;
- eprintln!("installed {}", release.label());
- let server_update_decisions = if options.live_handoff {
- server_update_decisions
- } else {
- mark_plain_update_stop_decisions(server_update_decisions)
- };
- let server_update_outcomes =
- apply_running_session_update_decisions(&release, &updated_exe, server_update_decisions)?;
- print_outdated_integration_notice_with_updated_binary(&updated_exe);
+ if !options.live_handoff
+ && !prompt_to_complete_plain_update(&server_update_decisions, &release)?
+ {
+ eprintln!("Herdr was not updated.");
+ eprintln!("Stop running Herdr sessions when ready, then run `herdr update` again.");
+ return Ok(current);
+ }
- print_running_session_update_outcomes(&server_update_outcomes, &release);
+ eprintln!(
+ "installing {} with the Windows installer...",
+ release.label()
+ );
+ install_windows_update_with_installer(channel)?;
+ let updated_exe = windows_installed_herdr_exe_path()?;
+ eprintln!("installed {}", release.label());
+ let server_update_decisions = if options.live_handoff {
+ server_update_decisions
+ } else {
+ mark_plain_update_stop_decisions(server_update_decisions)
+ };
+ let server_update_outcomes = apply_running_session_update_decisions(
+ &release,
+ &updated_exe,
+ server_update_decisions,
+ )?;
+ print_outdated_integration_notice_with_updated_binary(&updated_exe);
+
+ print_running_session_update_outcomes(&server_update_outcomes, &release);
+
+ return Ok(release.version);
+ }
+
+ #[cfg(not(windows))]
+ {
+ eprintln!("downloading {}...", release.label());
+ let downloaded_update = download_update(&release)?;
+ let updated_exe = downloaded_update.current_exe.clone();
+ eprintln!("downloaded {}", release.label());
+ if !options.live_handoff
+ && !prompt_to_complete_plain_update(&server_update_decisions, &release)?
+ {
+ eprintln!("Herdr was not updated.");
+ eprintln!("Stop running Herdr sessions when ready, then run `herdr update` again.");
+ return Ok(current);
+ }
+ install_downloaded_update(downloaded_update)?;
+ eprintln!("installed {}", release.label());
+ let server_update_decisions = if options.live_handoff {
+ server_update_decisions
+ } else {
+ mark_plain_update_stop_decisions(server_update_decisions)
+ };
+ let server_update_outcomes = apply_running_session_update_decisions(
+ &release,
+ &updated_exe,
+ server_update_decisions,
+ )?;
+ print_outdated_integration_notice_with_updated_binary(&updated_exe);
+
+ print_running_session_update_outcomes(&server_update_outcomes, &release);
+ }
Ok(release.version)
}
@@ -2077,6 +2154,8 @@ fn platform_target() -> (&'static str, &'static str) {
"linux"
} else if cfg!(target_os = "macos") {
"macos"
+ } else if cfg!(target_os = "windows") {
+ "windows"
} else {
"unknown"
};