Skip to content

Commit

Permalink
[KWin] Fixed shortcuts (#156)
Browse files Browse the repository at this point in the history
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
flyingpie authored Nov 25, 2024
1 parent 5ef4f3a commit ab611bc
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 63 deletions.
1 change: 1 addition & 0 deletions src/20-Services/Wtq.Services.KWin/IKWinClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Task MoveWindowAsync(

Task RegisterHotkeyAsync(
string name,
string description,
KeyModifiers modifiers,
Keys key,
CancellationToken cancellationToken);
Expand Down
32 changes: 6 additions & 26 deletions src/20-Services/Wtq.Services.KWin/KWinClientV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,43 +146,23 @@ public async Task MoveWindowAsync(

public async Task RegisterHotkeyAsync(
string name,
KeyModifiers mod,
string description,
KeyModifiers modifiers,
Keys key,
CancellationToken cancellationToken)
{
await InitAsync().NoCtx();

// TODO
var kwinMod = "Ctrl";
var kwinKey = key switch
{
Keys.Oemtilde => "`",
Keys.D0 => "0",
Keys.D1 => "1",
Keys.D2 => "2",
Keys.D3 => "3",
Keys.D4 => "4",
Keys.D5 => "5",
Keys.D6 => "6",
Keys.D7 => "7",
Keys.D8 => "8",
Keys.D9 => "9",
Keys.Q => "q",
_ => "1",
};

var kwinSequence = $"{kwinMod}+{kwinKey}";

_ = await _wtqBusObj
.SendCommandAsync(
new("REGISTER_HOT_KEY")
{
Params = new
{
name = $"{name}_name",
title = $"{name}_title",
sequence = kwinSequence,
mod = mod.ToString(),
name = name,
title = $"WTQ {(!string.IsNullOrWhiteSpace(description) ? $"- {description} - " : string.Empty)}(configured through WTQ settings)",
sequence = Mapping.Sequence(modifiers, key),
mod = modifiers.ToString(),
key = key.ToString(),
},
},
Expand Down
108 changes: 71 additions & 37 deletions src/20-Services/Wtq.Services.KWin/KWinHotkeyService.cs
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();
}
}
97 changes: 97 additions & 0 deletions src/20-Services/Wtq.Services.KWin/Mapping.cs
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}'."),
};
}

0 comments on commit ab611bc

Please sign in to comment.