-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Turned the very much proof-of-concept KWin hotkey code into something closer to production code. Supports most keys and modifiers, and includes decent auto-cleanup of KWin shortcuts.
- Loading branch information
Showing
4 changed files
with
175 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,95 @@ | ||
#pragma warning disable // PoC | ||
|
||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Options; | ||
using Wtq.Configuration; | ||
using Wtq.Events; | ||
using Wtq.Services.KWin.DBus; | ||
|
||
namespace Wtq.Services.KWin; | ||
|
||
internal class KWinHotkeyService : IHostedService | ||
/// <summary> | ||
/// TODO: DBus-only shortcut registration (we did get listening to key press events working, actual registration proved more difficult). | ||
/// TODO: Fetch known WTQ shortcut names, instead of the fixed index-based names. | ||
/// </summary> | ||
internal sealed class KWinHotkeyService | ||
: IDisposable, IHostedService | ||
{ | ||
private readonly IOptionsMonitor<WtqOptions> _opts; | ||
private readonly IKWinClient _kwinClient; | ||
/// <summary> | ||
/// To de-register any left-over shortcuts on WTQ start, we need to know their names.<br/> | ||
/// However, we don't have a nice way of finding out what those names are.<br/> | ||
/// | ||
/// So instead, we use an index-based naming scheme, so we get deterministic names. | ||
/// </summary> | ||
private const int MaxShortcutCount = 50; | ||
|
||
private readonly ILogger _log = Log.For<KWinHotkeyService>(); | ||
private readonly InitLock _init = new(); | ||
|
||
private readonly IDBusConnection _dbus; | ||
|
||
private int _shortcutIndex; | ||
|
||
public KWinHotkeyService( | ||
IOptionsMonitor<WtqOptions> opts, | ||
IKWinClient kwinClient, | ||
IDBusConnection dbus) | ||
IDBusConnection dbus, | ||
IWtqBus bus) | ||
{ | ||
_opts = opts; | ||
_kwinClient = kwinClient; | ||
_dbus = dbus; | ||
_dbus = Guard.Against.Null(dbus); | ||
_ = Guard.Against.Null(bus); | ||
|
||
bus.OnEvent<WtqHotkeyDefinedEvent>( | ||
async e => | ||
{ | ||
await InitAsync().NoCtx(); | ||
|
||
var name = GetShortcutName(_shortcutIndex++); | ||
|
||
_log.LogInformation("Registering shortcut with name '{Name}', modifiers '{Modifiers}' and key '{Key}'", name, e.Modifiers, e.Key); | ||
|
||
await kwinClient.RegisterHotkeyAsync(name, e.AppOptions?.Name ?? string.Empty, e.Modifiers, e.Key, CancellationToken.None).NoCtx(); | ||
}); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_init.Dispose(); | ||
} | ||
|
||
public async Task StartAsync(CancellationToken cancellationToken) | ||
{ | ||
var kwinx = await _dbus.GetKWinServiceAsync(); | ||
await InitAsync().NoCtx(); | ||
} | ||
|
||
var gl = kwinx.CreateKGlobalAccel("/kglobalaccel"); | ||
var comp = kwinx.CreateComponent("/component/kwin"); | ||
// var kwin = kwinx.CreateKWin("/org/kde/KWin"); | ||
public async Task StopAsync(CancellationToken cancellationToken) | ||
{ | ||
await ResetShortcutsAsync().NoCtx(); | ||
} | ||
|
||
// Clear. | ||
for (var i = 0; i < 50; i++) | ||
{ | ||
var resx1 = await gl.UnregisterAsync("kwin", $"wtq_hk1_{i:000}_scr_text"); | ||
} | ||
private static string GetShortcutName(int index) => $"wtq_hotkey_{index:000}"; | ||
|
||
private async Task InitAsync() | ||
{ | ||
await _init.InitAsync(ResetShortcutsAsync).NoCtx(); | ||
} | ||
|
||
await comp.CleanUpAsync(); | ||
private async Task ResetShortcutsAsync() | ||
{ | ||
_log.LogDebug("Removing shortcuts"); | ||
|
||
// TODO: Although we haven't gotten shortcut registration to work reliably through direct DBus calls, | ||
// we _can_ catch when shortcuts are being pressed/released. | ||
// So dial down the JS part to just registration, remove the callback to WTQ part. | ||
var kwin = await _dbus.GetKWinServiceAsync().NoCtx(); | ||
|
||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_000_scr", KeyModifiers.Control, Keys.Q, cancellationToken); | ||
var gl = kwin.CreateKGlobalAccel("/kglobalaccel"); | ||
var comp = kwin.CreateComponent("/component/kwin"); | ||
|
||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_001_scr", KeyModifiers.Control, Keys.D1, cancellationToken); | ||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_002_scr", KeyModifiers.Control, Keys.D2, cancellationToken); | ||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_003_scr", KeyModifiers.Control, Keys.D3, cancellationToken); | ||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_004_scr", KeyModifiers.Control, Keys.D4, cancellationToken); | ||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_005_scr", KeyModifiers.Control, Keys.D5, cancellationToken); | ||
await _kwinClient.RegisterHotkeyAsync("wtq_hk1_006_scr", KeyModifiers.Control, Keys.D6, cancellationToken); | ||
} | ||
// Remove individual shortcut registrations. | ||
for (var i = 0; i < MaxShortcutCount; i++) | ||
{ | ||
var name = GetShortcutName(i); | ||
|
||
public Task StopAsync(CancellationToken cancellationToken) | ||
{ | ||
// TODO: Cleanup. | ||
return Task.CompletedTask; | ||
if (await gl.UnregisterAsync("kwin", name).NoCtx()) | ||
{ | ||
_log.LogDebug("Unregistered {Name}", name); | ||
} | ||
} | ||
|
||
// Some GC-like flush. | ||
await comp.CleanUpAsync().NoCtx(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
using Wtq.Configuration; | ||
using static Wtq.Configuration.Keys; | ||
|
||
namespace Wtq.Services.KWin; | ||
|
||
public static class Mapping | ||
{ | ||
public static string Sequence(KeyModifiers modifiers, Keys key) | ||
{ | ||
var kwinMod = Modifier(modifiers); | ||
|
||
var kwinKey = Key(key); | ||
|
||
var kwinSequence = $"{kwinMod}+{kwinKey}"; | ||
|
||
return kwinSequence; | ||
} | ||
|
||
private static string Key(Keys key) => | ||
key switch | ||
{ | ||
Oemtilde => "`", | ||
|
||
// F-keys. | ||
F1 => "F1", F2 => "F2", F3 => "F3", F4 => "F4", F5 => "F5", F6 => "F6", F7 => "F7", F8 => "F8", F9 => "F9", F10 => "F10", F11 => "F11", F12 => "F12", F13 => "F13", F14 => "F14", F15 => "F15", F16 => "F16", F17 => "F17", F18 => "F18", F19 => "F19", F20 => "F20", F21 => "F21", F22 => "F22", F23 => "F23", F24 => "F24", | ||
|
||
// Keys above A-Z keys, under F-keys. | ||
D0 => "0", D1 => "1", D2 => "2", D3 => "3", D4 => "4", D5 => "5", D6 => "6", D7 => "7", D8 => "8", D9 => "9", | ||
|
||
// Letters. | ||
A => "A", B => "B", C => "C", D => "D", E => "E", F => "F", G => "G", H => "H", I => "I", J => "J", K => "K", L => "L", M => "M", N => "N", O => "O", P => "P", Q => "Q", R => "R", S => "S", T => "T", U => "U", V => "V", W => "W", X => "X", Y => "Y", Z => "Z", | ||
|
||
// Numpad (can't seem to differentiate from regular numbers?). | ||
NumPad0 => "0", NumPad1 => "1", NumPad2 => "2", NumPad3 => "3", NumPad4 => "4", NumPad5 => "5", NumPad6 => "6", NumPad7 => "7", NumPad8 => "8", NumPad9 => "9", | ||
|
||
Left => "Left", | ||
Up => "Up", | ||
Right => "Right", | ||
Down => "Down", | ||
|
||
Add => "+", | ||
Back => "Backspace", | ||
Delete => "Del", | ||
Divide => "/", | ||
End => "End", | ||
Escape => "Esc", | ||
Home => "Home", | ||
Insert => "Ins", | ||
Keys.Decimal => ".", | ||
Multiply => "*", | ||
NumLock => "NumLock", | ||
Pause => "Pause", | ||
Print => "Print", | ||
Return => "Return", | ||
Separator => ".", | ||
Space => " ", | ||
Subtract => "-", | ||
Tab => "Tab", | ||
|
||
OemBackslash => "\\", | ||
OemCloseBrackets => "[", | ||
OemMinus => "-", | ||
OemOpenBrackets => "[", | ||
OemPeriod => ".", | ||
OemPipe => "|", | ||
OemQuestion => "?", | ||
OemQuotes => "\"", | ||
OemSemicolon => ";", | ||
Oemcomma => ",", | ||
Oemplus => "+", | ||
|
||
VolumeMute => "Volume Mute", | ||
VolumeDown => "Volume Down", | ||
VolumeUp => "Volume Up", | ||
|
||
_ => throw new WtqException($"Unsupported key '{key}'."), | ||
}; | ||
|
||
private static string Modifier(KeyModifiers modifiers) => | ||
modifiers switch | ||
{ | ||
KeyModifiers.Control | ||
=> "Ctrl", | ||
KeyModifiers.Alt | ||
=> "Alt", | ||
KeyModifiers.Shift | ||
=> "Shift", | ||
KeyModifiers.Super | ||
=> "Meta", | ||
KeyModifiers.None | ||
=> throw new WtqException($"Unsupported modifier '{modifiers}'."), | ||
KeyModifiers.NoRepeat | ||
=> throw new WtqException($"Unsupported modifier '{modifiers}'."), | ||
_ | ||
=> throw new WtqException($"Unsupported modifier '{modifiers}'."), | ||
}; | ||
} |