diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b76622a..5671077 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,38 +16,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- - name: Configure Git Line Endings
- run: git config --global core.autocrlf input
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
with:
- dotnet-version: 8.0.x
+ targets: x86_64-pc-windows-msvc
- - name: Restore dependencies
- run: dotnet restore
-
- # 1. Build Framework-Dependent FIRST (The tiny one)
- - name: Build Framework-Dependent (Requires .NET 8)
- run: >
- dotnet publish SshHandler/SshHandler.csproj --configuration Release
- --self-contained false
- /p:PublishSingleFile=true
- -o ./publish-dotnet8
-
- # 2. Clean to prevent artifact leakage
- - name: Clean intermediate files
- run: dotnet clean SshHandler/SshHandler.csproj
-
- # 3. Build Standalone SECOND (The heavy one)
- - name: Build Standalone (Includes Runtime)
- run: >
- dotnet publish SshHandler/SshHandler.csproj --configuration Release
- --runtime win-x64
- --self-contained true
- /p:PublishSingleFile=true
- /p:IncludeNativeLibrariesForSelfExtract=true
- -o ./publish-standalone
+ - name: Build ssh-handler (Rust)
+ working-directory: SshHandlerRs
+ run: cargo build --release --target x86_64-pc-windows-msvc
- name: Ensure Unix Line Endings for Shell Script
shell: pwsh
@@ -64,11 +40,10 @@ jobs:
run: |
$tag = "${{ github.ref_name }}"
if (-not $tag -or $tag -eq "main") { $tag = "manual" }
-
- # Rename Executables
- Move-Item ./publish-standalone/SshHandler.exe "./SshHandler-$tag-Standalone.exe"
- Move-Item ./publish-dotnet8/SshHandler.exe "./SshHandler-$tag-dotnet8.exe"
-
+
+ # Rename Rust executable
+ Move-Item SshHandlerRs/target/x86_64-pc-windows-msvc/release/ssh-handler.exe "./ssh-handler-rs-$tag.exe"
+
# Rename Scripts
Copy-Item "ssh-handler.sh" "./ssh-handler-$tag.sh"
Copy-Item "openssh_protocol_handler.bat" "./openssh_protocol_handler-$tag.bat"
@@ -78,8 +53,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
- SshHandler-*-Standalone.exe
- SshHandler-*-dotnet8.exe
+ ssh-handler-rs-*.exe
ssh-handler-*.sh
openssh_protocol_handler-*.bat
generate_release_notes: true
diff --git a/.gitignore b/.gitignore
index 9a49c78..6c8c89a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,4 @@
# .gitignore
-# User-specific files
-*.rsuser
-*.suo
-*.user
-*.userosscache
-*.sln.docstates
-
-# Build results
-[Dd]ebug/
-[Dd]ebugPublic/
-[Rr]elease/
-[Rr]eleases/
-x64/
-x86/
-[Aa][Rr][Mm]/
-[Aa][Rr][Mm]64/
-bld/
-[Bb]in/
-[Oo]bj/
-
# Rust
/SshHandlerRs/target/
diff --git a/SshHandler.sln b/SshHandler.sln
deleted file mode 100644
index deffb31..0000000
--- a/SshHandler.sln
+++ /dev/null
@@ -1,21 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{50924D75-2903-46D4-86DD-02269EFDBCA9}") = "SshHandler", "SshHandler\SshHandler.csproj", "{960BAB18-309B-42D0-B67E-B4EF8E7F7490}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {960BAB18-309B-42D0-B67E-B4EF8E7F7490}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/SshHandler/Form1.Designer.cs b/SshHandler/Form1.Designer.cs
deleted file mode 100644
index b1e5ad7..0000000
--- a/SshHandler/Form1.Designer.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-partial class Form1
-{
- ///
- /// Required designer variable.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region Windows Form Designer generated code
-
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- this.label1 = new System.Windows.Forms.Label();
- this.targetTextBox = new System.Windows.Forms.TextBox();
- this.legacyCheckBox = new System.Windows.Forms.CheckBox();
- this.connectButton = new System.Windows.Forms.Button();
- this.cancelButton = new System.Windows.Forms.Button();
- this.SuspendLayout();
- //
- // label1
- //
- this.label1.AutoSize = true;
- this.label1.Location = new System.Drawing.Point(12, 15);
- this.label1.Name = "label1";
- this.label1.Size = new System.Drawing.Size(73, 15);
- this.label1.TabIndex = 0;
- this.label1.Text = "Connect to:";
- //
- // targetTextBox
- //
- this.targetTextBox.Location = new System.Drawing.Point(15, 33);
- this.targetTextBox.Name = "targetTextBox";
- this.targetTextBox.Size = new System.Drawing.Size(357, 23);
- this.targetTextBox.TabIndex = 1;
- //
- // legacyCheckBox
- //
- this.legacyCheckBox.AutoSize = true;
- this.legacyCheckBox.Location = new System.Drawing.Point(15, 71);
- this.legacyCheckBox.Name = "legacyCheckBox";
- this.legacyCheckBox.Size = new System.Drawing.Size(206, 19);
- this.legacyCheckBox.TabIndex = 2;
- this.legacyCheckBox.Text = "Enable Legacy Mode (for old devices)";
- this.legacyCheckBox.UseVisualStyleBackColor = true;
- //
- // connectButton
- //
- this.connectButton.Location = new System.Drawing.Point(297, 106);
- this.connectButton.Name = "connectButton";
- this.connectButton.Size = new System.Drawing.Size(75, 23);
- this.connectButton.TabIndex = 3;
- this.connectButton.Text = "Connect";
- this.connectButton.UseVisualStyleBackColor = true;
- this.connectButton.Click += new System.EventHandler(this.connectButton_Click);
- //
- // cancelButton
- //
- this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
- this.cancelButton.Location = new System.Drawing.Point(216, 106);
- this.cancelButton.Name = "cancelButton";
- this.cancelButton.Size = new System.Drawing.Size(75, 23);
- this.cancelButton.TabIndex = 4;
- this.cancelButton.Text = "Cancel";
- this.cancelButton.UseVisualStyleBackColor = true;
- this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click);
- //
- // Form1
- //
- this.AcceptButton = this.connectButton;
- this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.CancelButton = this.cancelButton;
- this.ClientSize = new System.Drawing.Size(384, 141);
- this.Controls.Add(this.cancelButton);
- this.Controls.Add(this.connectButton);
- this.Controls.Add(this.legacyCheckBox);
- this.Controls.Add(this.targetTextBox);
- this.Controls.Add(this.label1);
- this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
- this.MaximizeBox = false;
- this.MinimizeBox = false;
- this.Name = "Form1";
- this.ShowIcon = false;
- this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
- this.Text = "SSH Connection";
- this.ResumeLayout(false);
- this.PerformLayout();
- }
-
- #endregion
-
- private System.Windows.Forms.Label label1;
- private System.Windows.Forms.TextBox targetTextBox;
- private System.Windows.Forms.CheckBox legacyCheckBox;
- private System.Windows.Forms.Button connectButton;
- private System.Windows.Forms.Button cancelButton;
-}
diff --git a/SshHandler/Form1.cs b/SshHandler/Form1.cs
deleted file mode 100644
index 90cc8c5..0000000
--- a/SshHandler/Form1.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-using System.Windows.Forms;
-
-public partial class Form1 : Form
-{
- // This property will store the result for the main program
- public string FinalSshCommand { get; private set; }
-
- public Form1(string initialTarget)
- {
- InitializeComponent();
- targetTextBox.Text = initialTarget;
- // Select the text for easy editing
- targetTextBox.SelectAll();
- targetTextBox.Focus();
- }
-
- private void connectButton_Click(object sender, EventArgs e)
- {
- string target = targetTextBox.Text;
- bool useLegacy = legacyCheckBox.Checked;
-
- string legacyArgs = "-o KexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 " +
- "-o HostKeyAlgorithms=+ssh-rsa " +
- "-o MACs=+hmac-sha1,hmac-sha1-96";
-
- // Build the arguments for ssh.exe
- string arguments = $"-A -C {(useLegacy ? legacyArgs : "")} {target}";
-
- FinalSshCommand = arguments;
- this.DialogResult = DialogResult.OK; // Set dialog result to OK
- this.Close(); // Close the form
- }
-
- private void cancelButton_Click(object sender, EventArgs e)
- {
- this.DialogResult = DialogResult.Cancel; // Set dialog result to Cancel
- this.Close();
- }
-}
diff --git a/SshHandler/Form1.resx b/SshHandler/Form1.resx
deleted file mode 100644
index 5aada26..0000000
--- a/SshHandler/Form1.resx
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- 384, 141
-
-
- SSH Connection
-
-
diff --git a/SshHandler/Program.cs b/SshHandler/Program.cs
deleted file mode 100644
index ff98f3c..0000000
--- a/SshHandler/Program.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using Microsoft.Win32;
-using System;
-using System.Diagnostics;
-using System.Security.Principal;
-using System.Windows.Forms;
-
-static class Program
-{
- private const string ProtocolName = "ssh";
-
- [STAThread]
- static void Main(string[] args)
- {
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
-
- // If no arguments, run setup mode.
- if (args.Length == 0)
- {
- SetupHandler();
- }
- // Otherwise, run handler mode.
- else
- {
- HandleConnection(args[0]);
- }
- }
-
- static void HandleConnection(string url)
- {
- string target = url.Replace("ssh://", "").TrimEnd('/');
- if (!target.Contains("@"))
- {
- target = $"{Environment.UserName}@{target}";
- }
-
- // Show the form as a dialog
- using (var form = new Form1(target))
- {
- if (form.ShowDialog() == DialogResult.OK)
- {
- // If user clicked "Connect", run the command
- ProcessStartInfo psi = new ProcessStartInfo
- {
- FileName = "ssh.exe",
- Arguments = form.FinalSshCommand,
- UseShellExecute = true,
- CreateNoWindow = false
- };
- Process.Start(psi);
- }
- }
- }
-
- static void SetupHandler()
- {
- if (!IsAdmin())
- {
- MessageBox.Show("Please run this application as an administrator to register the URL handler.", "Admin Privileges Required", MessageBoxButtons.OK, MessageBoxIcon.Error);
- return;
- }
-
- // The path is simple and correct because it's a native app
- string exePath = Application.ExecutablePath;
- string commandStr = $"\"{exePath}\" \"%1\"";
-
- var confirmResult = MessageBox.Show(
- "This will register this application to handle all ssh:// links.\n\n" +
- $"Command:\n{commandStr}\n\n" +
- "Do you want to proceed?",
- "Registry Setup",
- MessageBoxButtons.YesNo,
- MessageBoxIcon.Information);
-
- if (confirmResult == DialogResult.Yes)
- {
- try
- {
- // Write the keys to HKEY_CLASSES_ROOT
- using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(ProtocolName))
- {
- key.SetValue("", $"URL:{ProtocolName} Protocol");
- key.SetValue("URL Protocol", "");
- }
- using (RegistryKey key = Registry.ClassesRoot.CreateSubKey($"{ProtocolName}\\shell\\open\\command"))
- {
- key.SetValue("", commandStr);
- }
- MessageBox.Show("Successfully registered ssh:// protocol handler!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Failed to write to registry:\n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
- }
-
- private static bool IsAdmin()
- {
- using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
- {
- WindowsPrincipal principal = new WindowsPrincipal(identity);
- return principal.IsInRole(WindowsBuiltInRole.Administrator);
- }
- }
-}
diff --git a/SshHandler/Properties/AssemblyInfo.cs b/SshHandler/Properties/AssemblyInfo.cs
deleted file mode 100644
index e69de29..0000000
diff --git a/SshHandler/SshHandler.csproj b/SshHandler/SshHandler.csproj
deleted file mode 100644
index b695334..0000000
--- a/SshHandler/SshHandler.csproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- WinExe
- net8.0-windows
- enable
- true
- enable
- ssh_icon.ico
-
-
-
diff --git a/SshHandler/ssh_icon.ico b/SshHandler/ssh_icon.ico
deleted file mode 100644
index 3334a3d..0000000
Binary files a/SshHandler/ssh_icon.ico and /dev/null differ
diff --git a/SshHandlerRs/README.md b/SshHandlerRs/README.md
index fb8a823..287f83c 100644
--- a/SshHandlerRs/README.md
+++ b/SshHandlerRs/README.md
@@ -1,18 +1,22 @@
# ssh-handler (Rust)
-A from-scratch rewrite of `SshHandler` (the .NET/WinForms `ssh://` protocol
-handler) in Rust, targeting raw Win32 (via `windows-rs`) instead of a managed
-GUI framework. Same behavior, same registry keys, same dialog layout — just
-without the .NET runtime.
-
-Why: the WinForms build's self-contained publish is ~150MB and pays CLR
-startup cost on every single `ssh://` link click. This version has no
-runtime, no GC, no JIT — cold start is just process creation plus a native
-dialog box. Release build is ~250KB.
+The Windows `ssh://` protocol handler, in Rust — raw Win32 (via `windows-rs`)
+instead of a managed GUI framework. This started as a rewrite of an earlier
+.NET/WinForms version (since removed) whose self-contained publish was
+~150MB and paid CLR startup cost on every single `ssh://` link click. This
+version has no runtime, no GC, no JIT — cold start is just process creation
+plus a native dialog box. Release build is ~250KB.
## Building
-Cross-compiling from Linux (what CI does):
+CI (`.github/workflows/build.yml`) builds natively on `windows-latest` with
+the MSVC toolchain:
+
+```sh
+cargo build --release --target x86_64-pc-windows-msvc
+```
+
+Cross-compiling from Linux also works, e.g. for local iteration:
```sh
rustup target add x86_64-pc-windows-gnu
@@ -20,19 +24,21 @@ sudo apt install mingw-w64
cargo build --release --target x86_64-pc-windows-gnu
```
-Or natively on Windows with the MSVC toolchain:
-
-```sh
-cargo build --release
-```
-
The dialog is a native Win32 resource (`resources/ssh_handler.rc`), compiled
via `embed-resource` in `build.rs` — no GUI toolkit dependency.
## Behavior
- No arguments: setup mode. Checks for admin rights, confirms with the user,
- then writes the `ssh://` protocol registration under `HKEY_CLASSES_ROOT`.
+ then registers `ssh_handler_rs` as an `ssh://` handler using the "Default
+ Programs" capability model (a dedicated ProgID with
+ `Capabilities\UrlAssociations`, plus a
+ `HKLM\SOFTWARE\RegisteredApplications` entry) — the same pattern
+ `openssh_protocol_handler.bat` uses, and the one Windows 10/11 actually
+ honors in Settings > Apps > Default Apps > "Choose default apps by link
+ type". A bare `HKCR\ssh\shell\open\command` write (what the old .NET
+ version did) gets silently ignored once a `UserChoice` already exists for
+ the protocol, which is the common case.
- `ssh://user@host` argument: handler mode. Parses the target (defaulting the
username to the current Windows user if omitted), shows a small dialog to
confirm/edit the target and optionally enable "legacy mode" (older