Rebase Rust handler's protocol registration on the .bat's Default Apps model

The direct HKCR\ssh\shell\open\command write (what SshHandler and the
initial Rust port both did) doesn't actually get honored once a
UserChoice already exists for the ssh scheme, which is the normal
state on Windows 10/11 — so registration silently no-ops from the
user's perspective.

Switch to the same "Default Programs" capability registration
openssh_protocol_handler.bat uses: a dedicated ProgID with
Capabilities\UrlAssociations plus a HKLM\SOFTWARE\RegisteredApplications
entry, so the app actually shows up in and can be selected from
Settings > Apps > Default Apps > "Choose default apps by link type".
Uses its own identifier, ssh_handler_rs, distinct from the batch
script's ssh_custom_handler so the two don't collide.

Verified end-to-end under Wine: registry keys land exactly as
expected (checked via `wine reg query`), and both the setup and
connect dialogs still render correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYCT2RpUjFfBfp6CD4rREw
This commit is contained in:
Claude
2026-09-08 16:48:31 +00:00
parent a671325a34
commit 1bd676d2fe
+72 -19
View File
@@ -3,8 +3,8 @@
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HWND, LPARAM, WPARAM};
use windows::Win32::System::Registry::{
RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, KEY_WRITE, REG_OPTION_NON_VOLATILE,
REG_SZ,
RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, KEY_WRITE,
REG_OPTION_NON_VOLATILE, REG_SZ,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Controls::IsDlgButtonChecked;
@@ -26,6 +26,11 @@ const EM_SETSEL: u32 = 0x00B1;
const LEGACY_ARGS: &str = "-o KexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 \
-o HostKeyAlgorithms=+ssh-rsa -o MACs=+hmac-sha1,hmac-sha1-96 -o ciphers=+aes256-cbc";
/// Our ProgID / capability-provider identifier under HKCR and RegisteredApplications.
/// Distinct from the .bat's "ssh_custom_handler" so both can coexist/be told apart.
const PROG_ID: &str = "ssh_handler_rs";
const APP_DISPLAY_NAME: &str = "SSH Handler (Rust)";
fn to_wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
@@ -157,17 +162,26 @@ fn setup_handler() {
let command_str = format!("\"{exe_path}\" \"%1\"");
let prompt = format!(
"This will register this application to handle all ssh:// links.\n\nCommand:\n{command_str}\n\nDo you want to proceed?"
"This will register this application ({APP_DISPLAY_NAME}) as a candidate handler for ssh:// links.\n\n\
Command:\n{command_str}\n\n\
After this, you may need to open Settings > Apps > Default Apps > \"Choose default apps by link type\", \
find \"ssh\", and select {APP_DISPLAY_NAME} explicitly — Windows does not let an installer silently \
override an existing default protocol handler.\n\n\
Do you want to proceed?"
);
let result = message_box(&prompt, "Registry Setup", MB_YESNO | MB_ICONQUESTION);
if result != IDYES {
return;
}
match register_protocol(&command_str) {
match register_protocol(&exe_path, &command_str) {
Ok(()) => {
message_box(
"Successfully registered ssh:// protocol handler!",
&format!(
"Registered {APP_DISPLAY_NAME} as an ssh:// handler.\n\n\
If ssh:// links don't launch it, open Settings > Apps > Default Apps > \
\"Choose default apps by link type\", find \"ssh\", and pick it there."
),
"Success",
MB_OK | MB_ICONINFORMATION,
);
@@ -182,20 +196,27 @@ fn setup_handler() {
}
}
fn reg_set_sz(hkey: HKEY, value_name: PCWSTR, data: &str) -> windows::core::Result<()> {
let wide = to_wide(data);
let bytes = unsafe {
std::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2)
/// Sets a REG_SZ value. `value_name = None` targets the key's default (unnamed) value.
fn reg_set_sz(hkey: HKEY, value_name: Option<&str>, data: &str) -> windows::core::Result<()> {
let name_wide = value_name.map(to_wide);
let name_ptr = match &name_wide {
Some(w) => PCWSTR(w.as_ptr()),
None => PCWSTR::null(),
};
unsafe { RegSetValueExW(hkey, value_name, 0, REG_SZ, Some(bytes)) }.ok()
let data_wide = to_wide(data);
let bytes = unsafe {
std::slice::from_raw_parts(data_wide.as_ptr() as *const u8, data_wide.len() * 2)
};
unsafe { RegSetValueExW(hkey, name_ptr, 0, REG_SZ, Some(bytes)) }.ok()
}
fn reg_create_key(subkey: PCWSTR) -> windows::core::Result<HKEY> {
fn reg_create_key(root: HKEY, subkey: &str) -> windows::core::Result<HKEY> {
let subkey_wide = to_wide(subkey);
let mut key = HKEY::default();
unsafe {
RegCreateKeyExW(
HKEY_CLASSES_ROOT,
subkey,
root,
PCWSTR(subkey_wide.as_ptr()),
0,
None,
REG_OPTION_NON_VOLATILE,
@@ -209,13 +230,45 @@ fn reg_create_key(subkey: PCWSTR) -> windows::core::Result<HKEY> {
Ok(key)
}
fn register_protocol(command_str: &str) -> windows::core::Result<()> {
let ssh_key = reg_create_key(w!("ssh"))?;
reg_set_sz(ssh_key, PCWSTR::null(), "URL:ssh Protocol")?;
reg_set_sz(ssh_key, w!("URL Protocol"), "")?;
/// Registers via the "Default Programs" capability model (RegisteredApplications +
/// a distinct ProgID with Capabilities\UrlAssociations), the same pattern
/// `openssh_protocol_handler.bat` uses — this is what actually shows up in and gets
/// honored by Windows' "Choose default apps by link type" UI. A bare
/// HKCR\ssh\shell\open\command write is ignored once a UserChoice already exists for
/// the protocol, which is the common case on Windows 10/11.
fn register_protocol(exe_path: &str, command_str: &str) -> windows::core::Result<()> {
// Declare "ssh" as a URL protocol scheme (legacy marker some apps still check for).
let ssh_key = reg_create_key(HKEY_CLASSES_ROOT, "ssh")?;
reg_set_sz(ssh_key, None, "URL:ssh Protocol")?;
reg_set_sz(ssh_key, Some("URL Protocol"), "")?;
// Our ProgID: the actual open command plus Default-Apps display metadata.
let cmd_key = reg_create_key(HKEY_CLASSES_ROOT, &format!("{PROG_ID}\\shell\\open\\command"))?;
reg_set_sz(cmd_key, None, command_str)?;
let app_key = reg_create_key(HKEY_CLASSES_ROOT, &format!("{PROG_ID}\\Application"))?;
reg_set_sz(app_key, Some("ApplicationIcon"), &format!("\"{exe_path}\",0"))?;
reg_set_sz(app_key, Some("ApplicationName"), APP_DISPLAY_NAME)?;
reg_set_sz(
app_key,
Some("ApplicationDescription"),
"Handles ssh:// links via Windows OpenSSH",
)?;
let caps_key = reg_create_key(
HKEY_CLASSES_ROOT,
&format!("{PROG_ID}\\Capabilities\\UrlAssociations"),
)?;
reg_set_sz(caps_key, Some("ssh"), PROG_ID)?;
// Tell Windows' Default Apps UI this app exists as a capability provider.
let registered_key = reg_create_key(HKEY_LOCAL_MACHINE, "SOFTWARE\\RegisteredApplications")?;
reg_set_sz(
registered_key,
Some(APP_DISPLAY_NAME),
&format!("Software\\Classes\\{PROG_ID}\\Capabilities"),
)?;
let cmd_key = reg_create_key(w!("ssh\\shell\\open\\command"))?;
reg_set_sz(cmd_key, PCWSTR::null(), command_str)?;
Ok(())
}