diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79ad7de..49de299 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.1.0] - 2026-07-01
+
+### Added
+- Settings now has an **Enabled** checkbox for the global hotkey. Clearing it
+ turns the hotkey off immediately and disables the shortcut-capture box; the
+ choice persists across restarts, and the stored chord is kept so re-enabling
+ restores it. The Reset button turns the hotkey back on and restores the
+ Ctrl+Alt+S default.
+
## [1.0.4] - 2026-06-10
### Changed
diff --git a/Directory.Build.props b/Directory.Build.props
index a8a6205..d422903 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -22,4 +22,15 @@
+
+
+
+
+
diff --git a/README.md b/README.md
index 4bd1c95..d7b13b3 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ The v1 feature set:
colours, spinners, progress bars and interactive prompts all work — watch it live,
and keep a clean, searchable plain-text run history you can replay. Per-CLI shell,
executable path and working directory configure how runs launch.
-- Global hotkey (default **Ctrl+Alt+S**, rebindable), system tray with
+- Global hotkey (default **Ctrl+Alt+S**, rebindable, and switchable off), system tray with
configurable close-to-tray, Light/Dark/System theme, and per-write plus
pre-update store backups.
- Migrate from SnipCommand with the [`snipdeck-importer`](tools/Snipdeck.Importer)
diff --git a/src/Snipdeck.App/App.xaml.cs b/src/Snipdeck.App/App.xaml.cs
index bdffb2e..9696c8e 100644
--- a/src/Snipdeck.App/App.xaml.cs
+++ b/src/Snipdeck.App/App.xaml.cs
@@ -94,7 +94,10 @@ private void InitialiseHotkey(AppConfig config)
{
_hotkey = Services.GetRequiredService();
_hotkey.Pressed += OnHotkeyPressed;
- _ = _hotkey.TryRegister(config.Hotkey);
+ if (config.HotkeyEnabled)
+ {
+ _ = _hotkey.TryRegister(config.Hotkey);
+ }
}
private void OnTrayShowRequested(object? sender, EventArgs e) => BringToFront();
diff --git a/src/Snipdeck.App/Views/ShellPage.xaml b/src/Snipdeck.App/Views/ShellPage.xaml
index 32b4544..15ed934 100644
--- a/src/Snipdeck.App/Views/ShellPage.xaml
+++ b/src/Snipdeck.App/Views/ShellPage.xaml
@@ -310,12 +310,17 @@
TextWrapping="Wrap"
MaxWidth="200"
Visibility="{x:Bind HotkeyError, Mode=OneWay, Converter={StaticResource EmptyStringToVisibility}}" />
+
+ Command="{x:Bind RebindHotkeyCommand}"
+ IsEnabled="{x:Bind HotkeyEnabled, Mode=OneWay}" />
+ ToolTipService.ToolTip="Turn the hotkey on and reset to Ctrl+Alt+S" />
diff --git a/src/Snipdeck.Core/Models/AppConfig.cs b/src/Snipdeck.Core/Models/AppConfig.cs
index aeafff5..94c6878 100644
--- a/src/Snipdeck.Core/Models/AppConfig.cs
+++ b/src/Snipdeck.Core/Models/AppConfig.cs
@@ -24,6 +24,14 @@ public sealed class AppConfig
public HotkeyBinding Hotkey { get; set; } = HotkeyBinding.Default;
+ ///
+ /// Whether the global hotkey is registered. When false the app runs
+ /// without a system-wide shortcut; the stored chord
+ /// is retained so re-enabling restores it. Defaults to true so existing
+ /// settings files (written before this option existed) keep their hotkey.
+ ///
+ public bool HotkeyEnabled { get; set; } = true;
+
public CloseBehaviour CloseBehaviour { get; set; } = CloseBehaviour.HideToTray;
///
diff --git a/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs b/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs
index c40cd6a..c90a011 100644
--- a/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs
+++ b/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs
@@ -67,6 +67,7 @@ public SettingsViewModel(
CloseBehaviour = config.CloseBehaviour;
CloseBehaviourIndex = CloseBehaviourIndexFor(config.CloseBehaviour);
HotkeyDisplay = config.Hotkey.ToDisplayString();
+ HotkeyEnabled = config.HotkeyEnabled;
StorageDirectory = config.StoragePath ?? pathProvider.DefaultStorageDirectory;
BackupDirectory = config.BackupDirectory ?? pathProvider.DefaultBackupDirectory;
BackupRetention = config.BackupRetention;
@@ -130,6 +131,14 @@ public SettingsViewModel(
[ObservableProperty]
public partial string HotkeyError { get; set; } = string.Empty;
+ ///
+ /// Whether the global hotkey is on. Toggling this registers or
+ /// unregisters the hotkey immediately and persists the choice. When
+ /// false the hotkey-capture box is disabled in the UI.
+ ///
+ [ObservableProperty]
+ public partial bool HotkeyEnabled { get; set; }
+
[ObservableProperty]
public partial ThemePreference Theme { get; set; }
@@ -209,6 +218,33 @@ partial void OnCloseBehaviourIndexChanged(int value)
_ = PersistAsync();
}
+ partial void OnHotkeyEnabledChanged(bool value)
+ {
+ // During construction the hotkey is already in the state App set at
+ // startup; only react to genuine user toggles.
+ if (_suppressPersist)
+ {
+ return;
+ }
+ if (value)
+ {
+ if (_hotkeyService.TryRegister(_config.Hotkey))
+ {
+ HotkeyError = string.Empty;
+ }
+ else
+ {
+ HotkeyError = $"Couldn't set {_config.Hotkey.ToDisplayString()} — it may already be in use by another app.";
+ }
+ }
+ else
+ {
+ _hotkeyService.Unregister();
+ HotkeyError = string.Empty;
+ }
+ _ = PersistAsync();
+ }
+
[RelayCommand]
private async Task CheckForUpdatesAsync()
{
@@ -267,6 +303,11 @@ private void RebindHotkey(HotkeyBinding? binding)
[RelayCommand]
private void ResetHotkey()
{
+ // Reset also re-enables the hotkey if it was switched off. Setting the
+ // property first flips the UI and (when it was off) registers the
+ // stored chord; RebindHotkey then swaps in the default and persists,
+ // so the final state is enabled + Ctrl+Alt+S regardless of the start.
+ HotkeyEnabled = true;
RebindHotkey(HotkeyBinding.Default);
}
@@ -371,6 +412,7 @@ private async Task PersistAsync()
_config.BackupRetention = BackupRetention;
_config.HistoryRetentionPerSnip = HistoryRetentionPerSnip;
_config.MaxCapturedOutputBytes = (long)MaxCapturedOutputMb * 1024 * 1024;
+ _config.HotkeyEnabled = HotkeyEnabled;
// _config.Hotkey is set directly by RebindHotkey before this runs.
await _settingsStore.SaveAsync(_config).ConfigureAwait(true);
}
diff --git a/tests/Snipdeck.Core.Tests/ViewModels/SettingsViewModelTests.cs b/tests/Snipdeck.Core.Tests/ViewModels/SettingsViewModelTests.cs
index dfac0b1..43c9eaa 100644
--- a/tests/Snipdeck.Core.Tests/ViewModels/SettingsViewModelTests.cs
+++ b/tests/Snipdeck.Core.Tests/ViewModels/SettingsViewModelTests.cs
@@ -134,6 +134,82 @@ public void ResetHotkey_rebinds_to_the_default_chord()
Assert.Equal("S", hotkey.LastRegistered.Key);
}
+ [Fact]
+ public void Initial_hotkey_enabled_reflects_config()
+ {
+ var enabled = Build(out _, out _, new AppConfig { HotkeyEnabled = true });
+ Assert.True(enabled.HotkeyEnabled);
+
+ var disabled = Build(out _, out _, new AppConfig { HotkeyEnabled = false });
+ Assert.False(disabled.HotkeyEnabled);
+ }
+
+ [Fact]
+ public void Disabling_the_hotkey_unregisters_and_persists_the_off_state()
+ {
+ var vm = Build(out var hotkey, out var store, new AppConfig { HotkeyEnabled = true });
+
+ vm.HotkeyEnabled = false;
+
+ Assert.Equal(1, hotkey.UnregisterCount);
+ Assert.Equal(string.Empty, vm.HotkeyError);
+ Assert.Equal(1, store.SaveCount);
+ Assert.False(store.Current.HotkeyEnabled);
+ }
+
+ [Fact]
+ public void Enabling_the_hotkey_registers_the_stored_chord_and_persists()
+ {
+ var vm = Build(out var hotkey, out var store, new AppConfig
+ {
+ HotkeyEnabled = false,
+ Hotkey = HotkeyBinding.Default,
+ });
+
+ vm.HotkeyEnabled = true;
+
+ Assert.Equal(HotkeyBinding.Default.Key, hotkey.LastRegistered!.Key);
+ Assert.Equal(1, hotkey.RegisterCount);
+ Assert.Equal(string.Empty, vm.HotkeyError);
+ Assert.Equal(1, store.SaveCount);
+ Assert.True(store.Current.HotkeyEnabled);
+ }
+
+ [Fact]
+ public void Enabling_the_hotkey_reports_an_error_when_registration_fails()
+ {
+ var vm = Build(out var hotkey, out var store, new AppConfig
+ {
+ HotkeyEnabled = false,
+ Hotkey = HotkeyBinding.Default,
+ });
+ hotkey.NextRegisterResult = false;
+
+ vm.HotkeyEnabled = true;
+
+ Assert.Contains("in use", vm.HotkeyError);
+ // The choice is still persisted even though registration failed.
+ Assert.True(store.Current.HotkeyEnabled);
+ }
+
+ [Fact]
+ public void ResetHotkey_reenables_the_hotkey_when_it_was_switched_off()
+ {
+ var vm = Build(out var hotkey, out var store, new AppConfig
+ {
+ HotkeyEnabled = false,
+ Hotkey = new HotkeyBinding { Modifiers = HotkeyModifiers.Control | HotkeyModifiers.Shift, Key = "K" },
+ });
+
+ vm.ResetHotkeyCommand.Execute(null);
+
+ Assert.True(vm.HotkeyEnabled);
+ Assert.True(store.Current.HotkeyEnabled);
+ Assert.Equal("Ctrl+Alt+S", vm.HotkeyDisplay);
+ Assert.Equal(HotkeyBinding.Default.Modifiers, hotkey.LastRegistered!.Modifiers);
+ Assert.Equal("S", hotkey.LastRegistered.Key);
+ }
+
[Fact]
public async Task ChangeStoragePath_moves_the_store_to_an_empty_target_then_restarts()
{
diff --git a/version.json b/version.json
index bb497ce..ec73e3f 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
- "version": "1.0.4",
+ "version": "1.1.0",
"publicReleaseRefSpec": [
"^refs/tags/v\\d+\\.\\d+\\.\\d+"
]