diff --git a/src/10-Core/Wtq/WtqApp.cs b/src/10-Core/Wtq/WtqApp.cs index 3c6ae20..8ba6787 100644 --- a/src/10-Core/Wtq/WtqApp.cs +++ b/src/10-Core/Wtq/WtqApp.cs @@ -105,9 +105,6 @@ public async Task CloseAsync(ToggleModifiers mods = ToggleModifiers.None) // Move window off-screen. await _toggler.ToggleOffAsync(this, mods).NoCtx(); - // Hide window. - await Window.SetVisibleAsync(false).NoCtx(); - await UpdateWindowPropsAsync().NoCtx(); } @@ -206,8 +203,7 @@ public async Task OpenAsync(ToggleModifiers mods = ToggleModifiers.None) return false; } - // Make sure the app window is visible and has focus. - await Window.SetVisibleAsync(true).NoCtx(); + // Make sure the app has focus. await Window.BringToForegroundAsync().NoCtx(); // Move app onto screen. diff --git a/src/10-Core/Wtq/WtqWindow.cs b/src/10-Core/Wtq/WtqWindow.cs index 251e414..f61b2cd 100644 --- a/src/10-Core/Wtq/WtqWindow.cs +++ b/src/10-Core/Wtq/WtqWindow.cs @@ -40,8 +40,6 @@ public abstract class WtqWindow public abstract Task SetTransparencyAsync(int transparency); - public abstract Task SetVisibleAsync(bool isVisible); - public abstract Task SetWindowTitleAsync(string title); public override string ToString() => $"[{Id}] {Name}"; diff --git a/src/20-Services/Wtq.Services.KWin/DBus/DBusConnection.cs b/src/20-Services/Wtq.Services.KWin/DBus/DBusConnection.cs index c81ddb6..1e75d12 100644 --- a/src/20-Services/Wtq.Services.KWin/DBus/DBusConnection.cs +++ b/src/20-Services/Wtq.Services.KWin/DBus/DBusConnection.cs @@ -1,63 +1,82 @@ using Tmds.DBus; -using Wtq.Exceptions; using Address = Tmds.DBus.Protocol.Address; using Connection = Tmds.DBus.Protocol.Connection; namespace Wtq.Services.KWin.DBus; -/// -public class DBusConnection : IDBusConnection +/// +internal sealed class DBusConnection : IAsyncInitializable, IDBusConnection { private readonly ILogger _log = Log.For(); + /// + /// Client connection, used to send requests to DBus. + /// + private readonly Connection _clientConnection; + + /// + /// Server connection, used to register DBus objects. + /// + private readonly Tmds.DBus.Connection _serverConnection; + + private DBus.Generated.KWinService? _kwinService; + private DBus.Generated.KWin? _kwin; + private DBus.Generated.Scripting? _scripting; + public DBusConnection() + : this(Address.Session) { - var address = Address.Session; + } - _log.LogInformation("Setting up DBus using address {Address}", address); + public DBusConnection(string? address) + { + Guard.Against.NullOrWhiteSpace(address); - if (string.IsNullOrWhiteSpace(address)) - { - throw new WtqException("Could not determine address for session DBus."); - } + _log.LogInformation("Setting up DBus using address {Address}", address); - ClientConnection = new Connection(address); - ServerConnection = new Tmds.DBus.Connection(address); + _clientConnection = new Connection(address); + _serverConnection = new Tmds.DBus.Connection(address); } - /// - public Connection ClientConnection { get; } - - /// - public Tmds.DBus.Connection ServerConnection { get; } + public int InitializePriority => -20; - /// - /// Connects to DBus. - /// - public async Task StartAsync(CancellationToken cancellationToken) + public async Task InitializeAsync() { _log.LogInformation("Setting up DBus connections"); var sw = Stopwatch.StartNew(); - await ClientConnection.ConnectAsync().NoCtx(); + await _clientConnection.ConnectAsync().NoCtx(); _log.LogInformation("DBus client connection ready, took {Elapsed}", sw.Elapsed); sw.Restart(); - await ServerConnection.ConnectAsync().NoCtx(); + await _serverConnection.ConnectAsync().NoCtx(); _log.LogInformation("DBus server connection ready, took {Elapsed}", sw.Elapsed); } + public async Task GetKWinServiceAsync() + { + return _kwinService ??= new DBus.Generated.KWinService(_clientConnection, "org.kde.KWin"); + } + + public async Task GetKWinAsync() + { + return _kwin ??= (await GetKWinServiceAsync().NoCtx()).CreateKWin("/KWin"); + } + + public async Task GetScriptingAsync() + { + return _scripting ??= (await GetKWinServiceAsync().NoCtx()).CreateScripting("/Scripting"); + } + /// /// Cleans up connections to DBus. /// - public Task StopAsync(CancellationToken cancellationToken) + public void Dispose() { _log.LogInformation("Cleaning up DBus connections"); - ClientConnection.Dispose(); - ServerConnection.Dispose(); - - return Task.CompletedTask; + _clientConnection.Dispose(); + _serverConnection.Dispose(); } /// @@ -68,7 +87,7 @@ public async Task RegisterServiceAsync(string serviceName, IDBusObject serviceOb _log.LogInformation("Registering DBus service with name '{ServiceName}', and object '{ServiceObject}'", serviceName, serviceObject); - await ServerConnection.RegisterServiceAsync(serviceName).NoCtx(); - await ServerConnection.RegisterObjectAsync(serviceObject).NoCtx(); + await _serverConnection.RegisterServiceAsync(serviceName).NoCtx(); + await _serverConnection.RegisterObjectAsync(serviceObject).NoCtx(); } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/DBus/KWin.DBus.Generated.cs b/src/20-Services/Wtq.Services.KWin/DBus/Generated/KWin.DBus.Generated.cs similarity index 99% rename from src/20-Services/Wtq.Services.KWin/DBus/KWin.DBus.Generated.cs rename to src/20-Services/Wtq.Services.KWin/DBus/Generated/KWin.DBus.Generated.cs index fb4ce72..4b0bbe9 100644 --- a/src/20-Services/Wtq.Services.KWin/DBus/KWin.DBus.Generated.cs +++ b/src/20-Services/Wtq.Services.KWin/DBus/Generated/KWin.DBus.Generated.cs @@ -1,6 +1,6 @@ #pragma warning disable -namespace Wtq.Services.KWin.DBus; +namespace Wtq.Services.KWin.DBus.Generated; using System; using Tmds.DBus.Protocol; diff --git a/src/20-Services/Wtq.Services.KWin/DBus/IDBusConnection.cs b/src/20-Services/Wtq.Services.KWin/DBus/IDBusConnection.cs index 0e7fd35..1727f87 100644 --- a/src/20-Services/Wtq.Services.KWin/DBus/IDBusConnection.cs +++ b/src/20-Services/Wtq.Services.KWin/DBus/IDBusConnection.cs @@ -1,5 +1,4 @@ using Tmds.DBus; -using Connection = Tmds.DBus.Protocol.Connection; namespace Wtq.Services.KWin.DBus; @@ -7,17 +6,13 @@ namespace Wtq.Services.KWin.DBus; /// Wraps both a client- and a server connection to DBus.
/// The client connection is used for sending requests to DBus, the server one is used to register DBus object. /// -public interface IDBusConnection +internal interface IDBusConnection : IDisposable { - /// - /// Client connection, used to send requests to DBus. - /// - Connection ClientConnection { get; } + Task GetKWinAsync(); - /// - /// Server connection, used to register DBus objects. - /// - Tmds.DBus.Connection ServerConnection { get; } + Task GetKWinServiceAsync(); + + Task GetScriptingAsync(); /// /// Register an object that exposes methods that can be called by other processes. diff --git a/src/20-Services/Wtq.Services.KWin/DBus/IWtqDBusObject.cs b/src/20-Services/Wtq.Services.KWin/DBus/IWtqDBusObject.cs index 4e04aa8..cb07772 100644 --- a/src/20-Services/Wtq.Services.KWin/DBus/IWtqDBusObject.cs +++ b/src/20-Services/Wtq.Services.KWin/DBus/IWtqDBusObject.cs @@ -6,16 +6,23 @@ namespace Wtq.Services.KWin.DBus; /// Contains methods used to talk to, from KWin scripts. /// [DBusInterface("wtq.kwin")] -public interface IWtqDBusObject : IDBusObject +public interface IWtqDBusObject : IDBusObject, IDisposable { + Task LogAsync(string level, string msg); + /// - /// Called when a shortcut has been pressed. + /// Responses to commands (as passed through ), are dropped here. /// - Task OnPressShortcutAsync(string modStr, string keyStr); + Task SendResponseAsync(string respInfoStr); + + /// + /// Ask WTQ for the next command to execute in the KWin script. + /// + Task GetNextCommandAsync(); /// - /// Generic callback handler, responses are formatted as JSON. + /// Called when a shortcut has been pressed.
+ /// TODO: Would like to remove this, and do shortcuts through DBus (without KWin script). Didn't get that working just yet. ///
- /// TODO: Can we drop the request/response thing and move to pure events? - Task SendResponseAsync(string responderIdStr, string payloadJson); + Task OnPressShortcutAsync(string modStr, string keyStr); } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/DBus/WtqDBusObject.cs b/src/20-Services/Wtq.Services.KWin/DBus/WtqDBusObject.cs index da09f14..f28dbd1 100644 --- a/src/20-Services/Wtq.Services.KWin/DBus/WtqDBusObject.cs +++ b/src/20-Services/Wtq.Services.KWin/DBus/WtqDBusObject.cs @@ -1,49 +1,136 @@ +using Microsoft.VisualStudio.Threading; +using System.Text.Json; using Tmds.DBus; using Wtq.Configuration; using Wtq.Events; +using Wtq.Services.KWin.Dto; +using Wtq.Services.KWin.Exceptions; namespace Wtq.Services.KWin.DBus; -internal class WtqDBusObject( +internal sealed class WtqDBusObject( + IDBusConnection dbus, IWtqBus bus) - : IWtqDBusObject + : IAsyncInitializable, IWtqDBusObject { - public static readonly ObjectPath Path = new("/wtq/kwin"); + private static readonly ObjectPath _path = new("/wtq/kwin"); + private readonly CancellationTokenSource _cts = new(); + private readonly AsyncAutoResetEvent _res = new(false); + private readonly ConcurrentQueue _commandQueue = new(); private readonly ConcurrentDictionary _waiters = new(); private readonly ILogger _log = Log.For(); + private readonly IWtqBus _bus = Guard.Against.Null(bus); + private readonly IDBusConnection _dbus = Guard.Against.Null(dbus); + + public int InitializePriority => -10; - public ObjectPath ObjectPath => Path; + public ObjectPath ObjectPath => _path; - public Task SendResponseAsync(string responderIdStr, string payloadJson) + public async Task InitializeAsync() { - responderIdStr ??= string.Empty; - payloadJson ??= string.Empty; + await _dbus.RegisterServiceAsync("wtq.svc", this).NoCtx(); - _log.LogInformation( - "{MethodName}({ResponderId}, {PayloadJson})", - nameof(SendResponseAsync), - responderIdStr, - payloadJson.Length > 25 ? payloadJson[0..25] : payloadJson); + StartNoOpLoop(); + } + + public void Dispose() + { + _cts.Dispose(); + _dbus.Dispose(); + } + + public Task LogAsync(string level, string msg) + { + // TODO + _log.LogDebug("{Level} {Message}", level, msg); + + return Task.CompletedTask; + } + + public Task SendCommandAsync(string commandType, object? parameters = null) + => SendCommandAsync(new CommandInfo(commandType) { Params = parameters ?? new() }); + + public async Task SendCommandAsync(CommandInfo cmdInfo) + { + _log.LogDebug("{MethodName} command: {Command}", nameof(SendCommandAsync), cmdInfo); + + // Add response waiter. + var id = cmdInfo.ResponderId; + using var waiter = new KWinResponseWaiter(id, () => _waiters.TryRemove(id, out _)); + if (!_waiters.TryAdd(id, waiter)) + { + _log.LogError("Command with id '{Id}' already queued", id); + throw new InvalidOperationException($"Command with id '{id}' already queued."); + } + + // Queue command + _commandQueue.Enqueue(cmdInfo); + _res.Set(); + + // Wait for response + try + { + return await waiter.Task.TimeoutAfterAsync(TimeSpan.FromSeconds(1)).NoCtx(); + } + catch (TimeoutException ex) + { + throw new KWinException($"Timeout while attempting to send KWin command '{cmdInfo}': {ex.Message}", ex); + } + } + + /// + public async Task GetNextCommandAsync() + { + while (true) + { + // See if we have a command on the queue to send back. + if (_commandQueue.TryDequeue(out var cmdInfo)) + { + _log.LogTrace("Send command '{Command}' to KWin", cmdInfo); + return JsonSerializer.Serialize(cmdInfo); + } + + // If not, wait for one to be queued. + await _res.WaitAsync().NoCtx(); + } + } + + /// + public Task SendResponseAsync(string respInfoStr) + { + var respInfo = JsonSerializer.Deserialize(respInfoStr); + + _log.LogTrace("Got response {Response}", respInfo); + + var hasResponder = _waiters.TryGetValue(respInfo.ResponderId, out var responder); - if (!Guid.TryParse(responderIdStr, out var responderId)) + if (!hasResponder) { - _log.LogWarning("Could not parse responder id {ResponderId} as a guid", responderIdStr); + _log.LogWarning("Could not find response waiter with id {ResponderId}", respInfo.ResponderId); return Task.CompletedTask; } - if (!_waiters.TryRemove(responderId, out var waiter)) + if (!_waiters.TryRemove(respInfo.ResponderId, out var waiter)) { - _log.LogWarning("Could not find response waiter with id {ResponderId}", responderId); + _log.LogWarning("Could not find response waiter with id {ResponderId}", respInfo.ResponderId); return Task.CompletedTask; } - waiter.SetResult(payloadJson); + if (respInfo.Exception != null) + { + waiter.SetException(respInfo.Exception); + } + else + { + waiter.SetResult(respInfo); + } return Task.CompletedTask; } + /// public Task OnPressShortcutAsync(string modStr, string keyStr) { _log.LogInformation( @@ -65,12 +152,28 @@ public Task OnPressShortcutAsync(string modStr, string keyStr) return Task.CompletedTask; } - public KWinResponseWaiter CreateResponseWaiter(Guid id) + /// + /// The DBus calls from wtq.kwin need to get occasional commands, otherwise the request times out, + /// and the connections is dropped. + /// + private void StartNoOpLoop() { - var waiter = new KWinResponseWaiter(id, () => _waiters.TryRemove(id, out _)); - - _waiters.TryAdd(id, waiter); - - return waiter; + // TODO: Generalize loop. + _ = Task.Run(async () => + { + while (!_cts.IsCancellationRequested) + { + try + { + await SendCommandAsync("NOOP").NoCtx(); + } + catch (Exception ex) + { + _log.LogError(ex, "Error while sending NO_OP to wtq.kwin: {Message}", ex.Message); + } + + await Task.Delay(TimeSpan.FromSeconds(10)).NoCtx(); + } + }); } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/CommandInfo.cs b/src/20-Services/Wtq.Services.KWin/Dto/CommandInfo.cs new file mode 100644 index 0000000..54459d9 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Dto/CommandInfo.cs @@ -0,0 +1,29 @@ +namespace Wtq.Services.KWin.Dto; + +/// +/// Represents a set of parameters sent to the KWin script, like for requesting the list of windows, or moving one, or setting its opacity. +/// +public class CommandInfo( + string type) +{ + /// + /// The command we want to execute, like "get window list" and "set window opacity". + /// + [JsonPropertyName("type")] + public string Type { get; } = Guard.Against.NullOrWhiteSpace(type); + + /// + /// Any parameters that accompany the command, like where to move a window to, + /// or what opacity to set a window to. + /// + [JsonPropertyName("params")] + public object? Params { get; set; } + + /// + /// Used to correlate any responses coming back from the KWin script.
+ ///
+ [JsonPropertyName("responderId")] + public Guid ResponderId { get; } = Guid.NewGuid(); + + public override string ToString() => $"[{Type}]"; +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/KWinGetWindowListResponse.cs b/src/20-Services/Wtq.Services.KWin/Dto/KWinGetWindowListResponse.cs new file mode 100644 index 0000000..d27531a --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Dto/KWinGetWindowListResponse.cs @@ -0,0 +1,8 @@ +namespace Wtq.Services.KWin.Dto; + +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "MvdO: Used by deserializer.")] +public class KWinGetWindowListResponse +{ + [JsonPropertyName("windows")] + public ICollection Windows { get; set; } = []; +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/KWinPoint.cs b/src/20-Services/Wtq.Services.KWin/Dto/KWinPoint.cs index f77ddc2..b935092 100644 --- a/src/20-Services/Wtq.Services.KWin/Dto/KWinPoint.cs +++ b/src/20-Services/Wtq.Services.KWin/Dto/KWinPoint.cs @@ -1,8 +1,6 @@ -using System.Text.Json.Serialization; - namespace Wtq.Services.KWin.Dto; -public class KWinPoint +public sealed class KWinPoint { [JsonPropertyName("x")] public int? X { get; set; } @@ -10,5 +8,5 @@ public class KWinPoint [JsonPropertyName("y")] public int? Y { get; set; } - public Point ToPoint() => new Point(X.Value, Y.Value); + public Point ToPoint() => new(X ?? 0, Y ?? 0); } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/KWinRectangle.cs b/src/20-Services/Wtq.Services.KWin/Dto/KWinRectangle.cs new file mode 100644 index 0000000..13c9917 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Dto/KWinRectangle.cs @@ -0,0 +1,37 @@ +namespace Wtq.Services.KWin.Dto; + +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "MvdO: Used by deserializer.")] +public sealed class KWinRectangle +{ + [JsonPropertyName("x")] + public int? X { get; set; } + + [JsonPropertyName("y")] + public int? Y { get; set; } + + [JsonPropertyName("width")] + public int? Width { get; set; } + + [JsonPropertyName("height")] + public int? Height { get; set; } + + [JsonPropertyName("top")] + public int? Top { get; set; } + + [JsonPropertyName("bottom")] + public int? Bottom { get; set; } + + [JsonPropertyName("left")] + public int? Left { get; set; } + + [JsonPropertyName("right")] + public int? Right { get; set; } + + public Point ToPoint() => new(X ?? -1, Y ?? -1); + + public Size ToSize() => new(Width ?? -1, Height ?? -1); + + public Rectangle ToRect() => new(X ?? -1, Y ?? -1, Width ?? -1, Height ?? -1); + + public override string ToString() => $"(X:{X}, Y:{Y}, Width:{Width}, Height:{Height}) (Top:{Top} Bottom:{Bottom} Left:{Left} Right:{Right})"; +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/KWinScreenInfo.cs b/src/20-Services/Wtq.Services.KWin/Dto/KWinScreenInfo.cs index 66cd796..0d725f9 100644 --- a/src/20-Services/Wtq.Services.KWin/Dto/KWinScreenInfo.cs +++ b/src/20-Services/Wtq.Services.KWin/Dto/KWinScreenInfo.cs @@ -1,10 +1,9 @@ namespace Wtq.Services.KWin.Dto; +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "MvdO: Used by deserializer.")] public class KWinScreenInfo { public string? Name { get; set; } - public bool IsEnabled { get; set; } - public Rectangle Geometry { get; set; } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/KWinSupportInformation.cs b/src/20-Services/Wtq.Services.KWin/Dto/KWinSupportInformation.cs index 4b5d7de..6e373fa 100644 --- a/src/20-Services/Wtq.Services.KWin/Dto/KWinSupportInformation.cs +++ b/src/20-Services/Wtq.Services.KWin/Dto/KWinSupportInformation.cs @@ -2,8 +2,6 @@ namespace Wtq.Services.KWin.Dto; public class KWinSupportInformation { - private static readonly ILogger _log = Log.For(); - public required ICollection Screens { get; init; } public static KWinSupportInformation Parse(string supportInfoStr) @@ -14,7 +12,6 @@ public static KWinSupportInformation Parse(string supportInfoStr) var headerRegex = new Regex("^screen (?[0-9]+):$", RegexOptions.Compiled | RegexOptions.IgnoreCase); var nameRegex = new Regex("^name: (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - var enabledRegex = new Regex("^enabled: (?[0-9]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); var geometryRegex = new Regex("^geometry: (?[0-9]+),(?[0-9]+),(?[0-9]+)x(?[0-9]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); KWinScreenInfo? scr = null; diff --git a/src/20-Services/Wtq.Services.KWin/Dto/KWinWindow.cs b/src/20-Services/Wtq.Services.KWin/Dto/KWinWindow.cs index 2c4b01d..3749936 100644 --- a/src/20-Services/Wtq.Services.KWin/Dto/KWinWindow.cs +++ b/src/20-Services/Wtq.Services.KWin/Dto/KWinWindow.cs @@ -1,15 +1,37 @@ -using System.Text.Json.Serialization; - namespace Wtq.Services.KWin.Dto; public class KWinWindow { + [JsonPropertyName("frameGeometry")] + public KWinRectangle? FrameGeometry { get; set; } + + [JsonPropertyName("hidden")] + public bool Hidden { get; set; } + [JsonPropertyName("internalId")] public string? InternalId { get; set; } + [JsonPropertyName("keepAbove")] + public bool KeepAbove { get; set; } + + [JsonPropertyName("layer")] + public int Layer { get; set; } + + [JsonPropertyName("minimized")] + public bool Minimized { get; set; } + [JsonPropertyName("resourceClass")] public string? ResourceClass { get; set; } [JsonPropertyName("resourceName")] public string? ResourceName { get; set; } + + [JsonPropertyName("skipPager")] + public bool SkipPager { get; set; } + + [JsonPropertyName("skipSwitcher")] + public bool SkipSwitcher { get; set; } + + [JsonPropertyName("skipTaskbar")] + public bool SkipTaskbar { get; set; } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Dto/ResponseInfo.cs b/src/20-Services/Wtq.Services.KWin/Dto/ResponseInfo.cs new file mode 100644 index 0000000..b70c4d9 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Dto/ResponseInfo.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using Wtq.Services.KWin.Exceptions; + +namespace Wtq.Services.KWin.Dto; + +public class ResponseInfo +{ + private Exception? _exc; + + [JsonPropertyName("cmdType")] + public string? CommandType { get; set; } + + [JsonPropertyName("responderId")] + public Guid ResponderId { get; set; } + + [JsonPropertyName("params")] + public JsonElement Params { get; set; } + + [JsonPropertyName("exception_message")] + public string? ExceptionMessage { get; set; } + + public Exception? Exception + { + get + { + if (_exc == null && !string.IsNullOrWhiteSpace(ExceptionMessage)) + { + _exc = new KWinException(ExceptionMessage); + } + + return _exc; + } + } + + public T? GetParamsAs() + { + // TODO: Error handling, with print of json. + return Params.Deserialize(); + } + + public override string ToString() => $"[{CommandType}] exc:{ExceptionMessage}"; +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Exceptions/KWinException.cs b/src/20-Services/Wtq.Services.KWin/Exceptions/KWinException.cs new file mode 100644 index 0000000..713135f --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Exceptions/KWinException.cs @@ -0,0 +1,14 @@ +namespace Wtq.Services.KWin.Exceptions; + +public sealed class KWinException : Exception +{ + public KWinException(string message, Exception innerException) + : base(message, innerException) + { + } + + public KWinException(string message) + : base(message) + { + } +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/GlobalUsings.cs b/src/20-Services/Wtq.Services.KWin/GlobalUsings.cs index 7ac3d01..9492dca 100644 --- a/src/20-Services/Wtq.Services.KWin/GlobalUsings.cs +++ b/src/20-Services/Wtq.Services.KWin/GlobalUsings.cs @@ -8,7 +8,10 @@ global using System.Drawing; global using System.IO; global using System.Linq; +global using System.Text.Json.Serialization; global using System.Text.RegularExpressions; global using System.Threading; global using System.Threading.Tasks; -global using Wtq.Utils; \ No newline at end of file +global using Wtq.Exceptions; +global using Wtq.Utils; +global using Wtq.Utils.AsyncInit; \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/IKWinClient.cs b/src/20-Services/Wtq.Services.KWin/IKWinClient.cs index 8971390..e661d73 100644 --- a/src/20-Services/Wtq.Services.KWin/IKWinClient.cs +++ b/src/20-Services/Wtq.Services.KWin/IKWinClient.cs @@ -1,3 +1,4 @@ +using Wtq.Configuration; using Wtq.Services.KWin.Dto; namespace Wtq.Services.KWin; @@ -5,24 +6,39 @@ namespace Wtq.Services.KWin; /// /// High-level interface to the KWin compositor. /// -public interface IKWinClient +public interface IKWinClient : IAsyncDisposable { Task BringToForegroundAsync( KWinWindow window, CancellationToken cancellationToken); - Task> GetClientListAsync( + Task GetCursorPosAsync( CancellationToken cancellationToken); - Task GetCursorPosAsync( + Task GetSupportInformationAsync( + CancellationToken cancellationToken); + + Task GetForegroundWindowAsync(); + + Task GetWindowAsync( + KWinWindow window); + + Task> GetWindowListAsync( CancellationToken cancellationToken); - Task MoveClientAsync( + Task MoveWindowAsync( KWinWindow window, - Rectangle rect, + Point location, CancellationToken cancellationToken); - Task GetSupportInformationAsync( + Task RegisterHotkeyAsync( + string name, + KeyModifiers modifiers, + Keys key); + + Task ResizeWindowAsync( + KWinWindow window, + Size size, CancellationToken cancellationToken); Task SetTaskbarIconVisibleAsync( @@ -39,9 +55,4 @@ Task SetWindowOpacityAsync( KWinWindow window, float opacity, CancellationToken cancellationToken); - - Task SetWindowVisibleAsync( - KWinWindow window, - bool isVisible, - CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinClient.cs b/src/20-Services/Wtq.Services.KWin/KWinClient.cs deleted file mode 100644 index 7781309..0000000 --- a/src/20-Services/Wtq.Services.KWin/KWinClient.cs +++ /dev/null @@ -1,307 +0,0 @@ -using Wtq.Services.KWin.DBus; -using Wtq.Services.KWin.Dto; -using Wtq.Services.KWin.Utils; - -namespace Wtq.Services.KWin; - -/// -/// TODO(MvdO): Here's most of the work we would need to refactor, since each of these calls results in a JS file write.
-/// Now, they are currently written to a shared memory mount, so they shouldn't touch an actual drive, but it's still not great. -///
-public class KWinClient : IKWinClient -{ - private const string JsGetWindows = """ - let getWindows = () => { - - // KWin5 - if (typeof workspace.clientList === "function") { - return workspace.clientList(); - } - - // KWin6 - if (typeof workspace.windowList === "function") { - return workspace.windowList(); - } - - throw "Could not find function to fetch windows, unsupported version of KWin perhaps?"; - }; - """; - - private readonly KWinScriptExecutor _kwinScriptEx; - private readonly KWinService _kwinDbus; - - private KWinSupportInformation? _suppInf; - - internal KWinClient( - KWinScriptExecutor kwinScriptEx, - KWinService kwinDbus) - { - _kwinScriptEx = Guard.Against.Null(kwinScriptEx); - _kwinDbus = Guard.Against.Null(kwinDbus); - } - - public async Task BringToForegroundAsync( - KWinWindow window, - CancellationToken cancellationToken) - { - Guard.Against.Null(window); - - var js = $$""" - "use strict"; - - let isDone = false; - - {{JsGetWindows}} - - for (let client of getWindows()) - { - if (client.resourceClass !== "{{window.ResourceClass}}") { - continue; - } - - client.minimized = false; - client.desktop = workspace.currentDesktop; - - // KWin5 - if (typeof workspace.activeClient === "object") { - workspace.activeClient = client; - isDone = true; - break; - } - - // KWin6 - if (typeof workspace.activeWindow === "object") { - workspace.activeWindow = client; - isDone = true; - break; - } - - throw "Could not find property on workspace for active window, unsupported version of KWin perhaps?"; - } - - throw "[BringWindowToForeground] Did not find a window with resource class '{{window.ResourceClass}}'"; - """; - - await _kwinScriptEx.ExecuteAsync(js, cancellationToken).NoCtx(); - } - - public async Task> GetClientListAsync( - CancellationToken cancellationToken) - { - var id = Guid.NewGuid(); - - var js = - $$""" - {{JsGetWindows}} - - let clients = getWindows() - .map(client => { - return { - internalId: client.internalId, - resourceClass: client.resourceClass, - resourceName: client.resourceName - }; - }); - - callDBus("wtq.svc", "/wtq/kwin", "wtq.kwin", "SendResponse", "{{id}}", JSON.stringify(clients)); - """; - - return await _kwinScriptEx.ExecuteAsync>(id, js, cancellationToken).NoCtx(); - } - - public async Task GetCursorPosAsync( - CancellationToken cancellationToken) - { - var id = Guid.NewGuid(); - - var js = - $$""" - "use strict"; - - callDBus( - "wtq.svc", - "/wtq/kwin", - "wtq.kwin", - "SendResponse", - "{{id}}", - JSON.stringify(workspace.cursorPos)); - """; - - var point = await _kwinScriptEx.ExecuteAsync(id, js, cancellationToken).NoCtx(); - - return point.ToPoint(); - } - - public async Task GetSupportInformationAsync( - CancellationToken cancellationToken) - { - if (_suppInf == null) - { - var str = await _kwinDbus.CreateKWin("/KWin").SupportInformationAsync().NoCtx(); - - _suppInf = KWinSupportInformation.Parse(str); - } - - return _suppInf; - } - - public async Task MoveClientAsync( - KWinWindow window, - Rectangle rect, - CancellationToken cancellationToken) - { - Guard.Against.Null(window); - - var js = $$""" - "use strict"; - - let isDone = false; - - {{JsGetWindows}} - - for (let client of getWindows()) - { - if (client.resourceClass !== "{{window.ResourceClass}}") { - continue; - } - - console.log("Setting client '{{window.ResourceClass}}' to position ({{rect.X}}, {{rect.Y}}, {{rect.Width}}, {{rect.Height}})"); - - client.frameGeometry = { - x: {{rect.X}}, - y: {{rect.Y}}, - width: {{rect.Width}}, - height: {{rect.Height}} - }; - - client.frameGeometry.x = {{rect.X}}; - client.frameGeometry.y = {{rect.Y}}; - client.frameGeometry.width = {{rect.Width}}; - client.frameGeometry.height = {{rect.Height}}; - - isDone = true; - break; - } - - throw "[Move] Did not find a window with resource class '{{window.ResourceClass}}'"; - """; - - await _kwinScriptEx.ExecuteAsync(js, cancellationToken).NoCtx(); - } - - public async Task SetTaskbarIconVisibleAsync( - KWinWindow window, - bool isVisible, - CancellationToken cancellationToken) - { - Guard.Against.Null(window); - - var skip = JsUtils.ToJsBoolean(!isVisible); - - var js = $$""" - "use strict"; - - {{JsGetWindows}} - - for (let client of getWindows()) - { - if (client.resourceClass !== "{{window.ResourceClass}}") { - continue; - } - - client.skipPager = {{skip}}; - client.skipSwitcher = {{skip}}; - client.skipTaskbar = {{skip}}; - - break; - } - """; - - await _kwinScriptEx.ExecuteAsync(js, cancellationToken).NoCtx(); - } - - public async Task SetWindowAlwaysOnTopAsync( - KWinWindow window, - bool isAlwaysOnTop, - CancellationToken cancellationToken) - { - Guard.Against.Null(window); - - var keepAbove = JsUtils.ToJsBoolean(isAlwaysOnTop); - - var js = $$""" - "use strict"; - - {{JsGetWindows}} - - for (let client of getWindows()) - { - if (client.resourceClass !== "{{window.ResourceClass}}") { - continue; - } - - client.keepAbove = {{keepAbove}}; - - break; - } - """; - - await _kwinScriptEx.ExecuteAsync(js, cancellationToken).NoCtx(); - } - - public async Task SetWindowOpacityAsync( - KWinWindow window, - float opacity, - CancellationToken cancellationToken) - { - Guard.Against.Null(window); - - var js = $$""" - "use strict"; - - {{JsGetWindows}} - - for (let client of getWindows()) - { - if (client.resourceClass !== "{{window.ResourceClass}}") { - continue; - } - - client.opacity = {{opacity}}; - - break; - } - """; - - await _kwinScriptEx.ExecuteAsync(js, cancellationToken).NoCtx(); - } - - public async Task SetWindowVisibleAsync( - KWinWindow window, - bool isVisible, - CancellationToken cancellationToken) - { - Guard.Against.Null(window); - - var minimized = JsUtils.ToJsBoolean(!isVisible); - - var js = $$""" - "use strict"; - - {{JsGetWindows}} - - for (let client of getWindows()) - { - if (client.resourceClass !== "{{window.ResourceClass}}") { - continue; - } - - client.minimized = {{minimized}}; - - break; - } - """; - - await _kwinScriptEx.ExecuteAsync(js, cancellationToken).NoCtx(); - } -} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinClientV2.cs b/src/20-Services/Wtq.Services.KWin/KWinClientV2.cs new file mode 100644 index 0000000..6388845 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/KWinClientV2.cs @@ -0,0 +1,239 @@ +using Wtq.Configuration; +using Wtq.Services.KWin.DBus; +using Wtq.Services.KWin.Dto; +using Wtq.Services.KWin.Scripting; + +namespace Wtq.Services.KWin; + +/// +/// Wraps functions in the wtq.kwin script. +/// +internal sealed class KWinClientV2( + IDBusConnection dbus, + IKWinScriptService scriptService, + IWtqDBusObject wtqBusObj) + : IAsyncInitializable, IKWinClient +{ + private readonly ILogger _log = Log.For(); + + private readonly IDBusConnection _dbus = Guard.Against.Null(dbus); + private readonly WtqDBusObject _wtqBusObj = (WtqDBusObject)wtqBusObj; // TODO: Fix. + + private KWinScript? _script; + + public int InitializePriority => -5; + + public async Task InitializeAsync() + { + _script = await scriptService.LoadScriptAsync("wtq.kwin.js").NoCtx(); + } + + public async ValueTask DisposeAsync() + { + if (_script != null) + { + await _script.DisposeAsync().NoCtx(); + } + } + + public async Task BringToForegroundAsync(KWinWindow window, CancellationToken cancellationToken) + { + _ = await _wtqBusObj + .SendCommandAsync( + "BRING_WINDOW_TO_FOREGROUND", + new + { + internalId = window.InternalId, + }) + .NoCtx(); + } + + public async Task GetCursorPosAsync(CancellationToken cancellationToken) + { + return (await _wtqBusObj + .SendCommandAsync("GET_CURSOR_POS") + .NoCtx()) + .GetParamsAs() + .ToPoint(); + } + + public async Task GetForegroundWindowAsync() + { + return (await _wtqBusObj + .SendCommandAsync("GET_FOREGROUND_WINDOW") + .NoCtx()) + .GetParamsAs(); + } + + public async Task GetSupportInformationAsync( + CancellationToken cancellationToken) + { + var kwin = await _dbus.GetKWinAsync().NoCtx(); + + var supportInfStr = await kwin.SupportInformationAsync().NoCtx(); + + return KWinSupportInformation.Parse(supportInfStr); + } + + public async Task GetWindowAsync(KWinWindow window) + { + var resp = await _wtqBusObj + .SendCommandAsync( + "GET_WINDOW", + new + { + internalId = window.InternalId, + }) + .NoCtx(); + + return resp.GetParamsAs(); + } + + public async Task> GetWindowListAsync(CancellationToken cancellationToken) + { + var resp = await _wtqBusObj.SendCommandAsync("GET_WINDOW_LIST").NoCtx(); + + return resp + .GetParamsAs() + .Windows; + } + + public async Task MoveWindowAsync( + KWinWindow window, + Point location, + CancellationToken cancellationToken) + { + _ = await _wtqBusObj + .SendCommandAsync( + "MOVE_WINDOW", + new + { + internalId = window.InternalId, + x = location.X, + y = location.Y, + }) + .NoCtx(); + + // var w = await GetWindowAsync(window).NoCtx(); + // + // if (w?.FrameGeometry == null) + // { + // return; + // } + // + // var actualLocation = w.FrameGeometry.ToPoint(); + // + // if (actualLocation != location) + // { + // _log.LogWarning("Window '{Window}' did not have expected location '{ExpectedLocation}' after move (was '{ActualLocation}')", window, location, actualLocation); + // } + } + + public async Task RegisterHotkeyAsync(string name, KeyModifiers mod, Keys key) + { + var kwinMod = "Ctrl"; + var kwinKey = key switch + { + Keys.D1 => "1", + Keys.D2 => "2", + Keys.D3 => "3", + Keys.D4 => "4", + Keys.D5 => "5", + Keys.D6 => "6", + 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(), + key = key.ToString(), + }, + }) + .NoCtx(); + } + + public async Task ResizeWindowAsync( + KWinWindow window, + Size size, + CancellationToken cancellationToken) + { + _ = await _wtqBusObj + .SendCommandAsync( + "RESIZE_WINDOW", + new + { + internalId = window.InternalId, + width = size.Width, + height = size.Height, + }) + .NoCtx(); + + // var w = await GetWindowAsync(window).NoCtx(); + // + // if (w?.FrameGeometry == null) + // { + // return; + // } + // + // var actualSize = w.FrameGeometry.ToSize(); + // + // if (actualSize != size) + // { + // _log.LogWarning("Window '{Window}' did not have expected size '{ExpectedSize}' after resize (was '{ActualSize}')", window, size, actualSize); + // } + } + + public async Task SetTaskbarIconVisibleAsync(KWinWindow window, bool isVisible, CancellationToken cancellationToken) + { + _ = await _wtqBusObj + .SendCommandAsync( + "SET_WINDOW_TASKBAR_ICON_VISIBLE", + new + { + internalId = window.InternalId, + isVisible = JsUtils.ToJsBoolean(isVisible), + }) + .NoCtx(); + + await GetWindowAsync(window).NoCtx(); + } + + public async Task SetWindowAlwaysOnTopAsync(KWinWindow window, bool isAlwaysOnTop, CancellationToken cancellationToken) + { + _ = await _wtqBusObj + .SendCommandAsync( + "SET_WINDOW_ALWAYS_ON_TOP", + new + { + internalId = window.InternalId, + isAlwaysOnTop = JsUtils.ToJsBoolean(isAlwaysOnTop), + }) + .NoCtx(); + + await GetWindowAsync(window).NoCtx(); + } + + public async Task SetWindowOpacityAsync(KWinWindow window, float opacity, CancellationToken cancellationToken) + { + _ = await _wtqBusObj + .SendCommandAsync( + "SET_WINDOW_OPACITY", + new + { + internalId = window.InternalId, + opacity = opacity, + }) + .NoCtx(); + + await GetWindowAsync(window).NoCtx(); + } +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinHotkeyService.cs b/src/20-Services/Wtq.Services.KWin/KWinHotkeyService.cs index 827d36d..e42227a 100644 --- a/src/20-Services/Wtq.Services.KWin/KWinHotkeyService.cs +++ b/src/20-Services/Wtq.Services.KWin/KWinHotkeyService.cs @@ -1,42 +1,34 @@ #pragma warning disable // PoC -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using Tmds.DBus.Protocol; using Wtq.Configuration; using Wtq.Services.KWin.DBus; namespace Wtq.Services.KWin; -internal class KWinHotkeyService : IDisposable, IHostedService +internal class KWinHotkeyService : IAsyncInitializable { private readonly IOptionsMonitor _opts; - private readonly KWinScriptExecutor _scriptExecutor; - private readonly KWinService _kwinService; + private readonly IKWinClient _kwinClient; + private readonly IDBusConnection _dbus; public KWinHotkeyService( IOptionsMonitor opts, - KWinScriptExecutor scriptExecutor, - KWinService kwinService) + IKWinClient kwinClient, + IDBusConnection dbus) { _opts = opts; - _scriptExecutor = scriptExecutor; - _kwinService = kwinService; + _kwinClient = kwinClient; + _dbus = dbus; } - public void Dispose() + public async Task InitializeAsync() { - // Nothing to do. - } - - private IDisposable _disp1; - private IDisposable _disp2; + var kwinx = await _dbus.GetKWinServiceAsync(); - public async Task StartAsync(CancellationToken cancellationToken) - { - var gl = _kwinService.CreateKGlobalAccel("/kglobalaccel"); - var comp = _kwinService.CreateComponent("/component/kwin"); - var kwin = _kwinService.CreateKWin("/org/kde/KWin"); + var gl = kwinx.CreateKGlobalAccel("/kglobalaccel"); + var comp = kwinx.CreateComponent("/component/kwin"); + // var kwin = kwinx.CreateKWin("/org/kde/KWin"); // Clear. for (int i = 0; i < 5; i++) @@ -49,30 +41,23 @@ public async Task StartAsync(CancellationToken cancellationToken) // 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. - _disp1 = await comp.WatchGlobalShortcutPressedAsync((exception, tuple) => - { - var xx2 = 2; - Console.WriteLine($"PRESSED:{tuple.ComponentUnique} {tuple.ShortcutUnique} {tuple.Timestamp} {exception?.Message}"); - }); - - _disp2 = await comp.WatchGlobalShortcutReleasedAsync((exception, tuple) => - { - Console.WriteLine($"RELEASED:{tuple.ComponentUnique} {tuple.ShortcutUnique} {tuple.Timestamp} {exception?.Message}"); - }); - - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_005_scr", KeyModifiers.Control, Keys.Q); - - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_001_scr", KeyModifiers.Control, Keys.D1); - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_002_scr", KeyModifiers.Control, Keys.D2); - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_003_scr", KeyModifiers.Control, Keys.D3); - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_004_scr", KeyModifiers.Control, Keys.D4); - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_006_scr", KeyModifiers.Control, Keys.D5); - await _scriptExecutor.RegisterHotkeyAsync("wtq_hk1_007_scr", KeyModifiers.Control, Keys.D6); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - // Nothing to do. - return Task.CompletedTask; + // _disp1 = await comp.WatchGlobalShortcutPressedAsync((exception, tuple) => + // { + // // Console.WriteLine($"PRESSED:{tuple.ComponentUnique} {tuple.ShortcutUnique} {tuple.Timestamp} {exception?.Message}"); + // }); + + // _disp2 = await comp.WatchGlobalShortcutReleasedAsync((exception, tuple) => + // { + // // Console.WriteLine($"RELEASED:{tuple.ComponentUnique} {tuple.ShortcutUnique} {tuple.Timestamp} {exception?.Message}"); + // }); + + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_005_scr", KeyModifiers.Control, Keys.Q); + + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_001_scr", KeyModifiers.Control, Keys.D1); + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_002_scr", KeyModifiers.Control, Keys.D2); + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_003_scr", KeyModifiers.Control, Keys.D3); + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_004_scr", KeyModifiers.Control, Keys.D4); + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_006_scr", KeyModifiers.Control, Keys.D5); + await _kwinClient.RegisterHotkeyAsync("wtq_hk1_007_scr", KeyModifiers.Control, Keys.D6); } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinProcessService.cs b/src/20-Services/Wtq.Services.KWin/KWinProcessService.cs deleted file mode 100644 index e0cd111..0000000 --- a/src/20-Services/Wtq.Services.KWin/KWinProcessService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Wtq.Configuration; - -namespace Wtq.Services.KWin; - -public class KWinProcessService( - IKWinClient kwinClient) - : IWtqProcessService -{ - private readonly IKWinClient _kwinClient = Guard.Against.Null(kwinClient); - - public Task CreateAsync(WtqAppOptions opts) - { - // TODO - return Task.CompletedTask; - } - - public async Task FindWindowAsync(WtqAppOptions opts) - { - try - { - var clients = await GetWindowsAsync().NoCtx(); - - var x = clients.FirstOrDefault(c => c.Matches(opts)); - - Console.WriteLine($"GOT {opts.Name}=>{x?.Name}"); - return x; - } - catch (Exception ex) - { - var dbg = 2; - } - - return null; - } - - public WtqWindow? GetForegroundWindow() - { - return null; - } - - public async Task> GetWindowsAsync() - { - return (await _kwinClient.GetClientListAsync(CancellationToken.None).NoCtx()) - .Select(c => (WtqWindow)new KWinWtqWindow(_kwinClient, c)) - .ToList(); - } -} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinResponseWaiter.cs b/src/20-Services/Wtq.Services.KWin/KWinResponseWaiter.cs index 398c278..fda8db4 100644 --- a/src/20-Services/Wtq.Services.KWin/KWinResponseWaiter.cs +++ b/src/20-Services/Wtq.Services.KWin/KWinResponseWaiter.cs @@ -1,11 +1,11 @@ -using System.Text.Json; +using Wtq.Services.KWin.Dto; namespace Wtq.Services.KWin; public sealed class KWinResponseWaiter : IDisposable { private readonly Action _onDone; - private readonly TaskCompletionSource _tcs = new(); + private readonly TaskCompletionSource _tcs = new(); public KWinResponseWaiter(Guid id, Action onDone) { @@ -18,22 +18,20 @@ public KWinResponseWaiter(Guid id, Action onDone) public Guid Id { get; set; } - public Task Task => _tcs.Task; + public Task Task => _tcs.Task; public void Dispose() { _onDone(); } - public async Task GetResultAsync(CancellationToken cancellationToken) + public void SetException(Exception ex) { - var respStr = await _tcs.Task.WaitAsync(cancellationToken).NoCtx(); - - return JsonSerializer.Deserialize(respStr)!; + _tcs.TrySetException(ex); } - public void SetResult(string result) + public void SetResult(ResponseInfo respInfo) { - _tcs.TrySetResult(result); + _tcs.TrySetResult(respInfo); } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinScriptExecutor.cs b/src/20-Services/Wtq.Services.KWin/KWinScriptExecutor.cs deleted file mode 100644 index 81add54..0000000 --- a/src/20-Services/Wtq.Services.KWin/KWinScriptExecutor.cs +++ /dev/null @@ -1,148 +0,0 @@ -#pragma warning disable // PoC - -using Wtq.Configuration; -using Wtq.Services.KWin.DBus; - -namespace Wtq.Services.KWin; - -/// -/// TODO(MvdO): Here be dragons, this is the messiest part of the proof of concept. -/// -internal class KWinScriptExecutor -{ - private readonly ILogger _log = Log.For(); - private readonly Task _wtqDBusObj; - private readonly Scripting _scripting; - - public KWinScriptExecutor( - Task wtqDBusObj, - Scripting scripting) - { - _wtqDBusObj = Guard.Against.Null(wtqDBusObj); - _scripting = Guard.Against.Null(scripting); - } - - public async Task ExecuteAsync( - string script, - CancellationToken cancellationToken) - { - Guard.Against.NullOrWhiteSpace(script); - - var sw = Stopwatch.StartNew(); - - var scriptId = Guid.NewGuid().ToString(); - var path = $"/dev/shm/wtq-{scriptId}.js"; - - try - { - await File.WriteAllTextAsync(path, script, cancellationToken).NoCtx(); - - await _scripting.LoadScriptAsync(path, scriptId).NoCtx(); - await _scripting.StartAsync().NoCtx(); - await _scripting.UnloadScriptAsync(scriptId).NoCtx(); - } - finally - { - File.Delete(path); - - _log.LogInformation("Executed script in {ElapsedMs}ms", sw.ElapsedMilliseconds); - } - } - - public async Task ExecuteAsync( - Guid id, - string script, - CancellationToken cancellationToken) - { - Guard.Against.NullOrWhiteSpace(script); - - var sw = Stopwatch.StartNew(); - - var scriptId = id.ToString(); - var path = $"/dev/shm/wtq-{id}.js"; - - var dbus = (WtqDBusObject)await _wtqDBusObj.NoCtx(); - var waiter = dbus.CreateResponseWaiter(id); - - try - { - await File.WriteAllTextAsync(path, script, cancellationToken).NoCtx(); - - await _scripting.LoadScriptAsync(path, scriptId).NoCtx(); - await _scripting.StartAsync().NoCtx(); - await _scripting.UnloadScriptAsync(scriptId).NoCtx(); - - return await waiter.GetResultAsync(cancellationToken).NoCtx(); - } - catch (Exception ex) - { - _log.LogError(ex, "Error executing script with id '{ScriptId}' and content '{Script}': {Message}", scriptId, script, ex.Message); - throw; - } - finally - { - File.Delete(path); - - _log.LogInformation("Executed script in {ElapsedMs}ms", sw.ElapsedMilliseconds); - } - } - - public async Task RegisterHotkeyAsync(string name, KeyModifiers mod, Keys key) - { - var cancellationToken = CancellationToken.None; - - _log.LogInformation("Registering hotkey"); - - var sw = Stopwatch.StartNew(); - - var scriptId = name; - var unloaded = await _scripting.UnloadScriptAsync(scriptId).NoCtx(); - var xx2 = await _scripting.IsScriptLoadedAsync(scriptId).NoCtx(); - - var kwinMod = "Ctrl"; - var kwinKey = key switch - { - Keys.D1 => "1", - Keys.D2 => "2", - Keys.D3 => "3", - Keys.D4 => "4", - Keys.D5 => "5", - Keys.D6 => "6", - Keys.Q => "q", - _ => "1" - }; - - var kwinSequence = $"{kwinMod}+{kwinKey}"; - - var path = $"/dev/shm/{scriptId}.js"; - var script = $$""" - console.log("Registering shortcut"); - registerShortcut( - "{{name}}_text", - "{{name}}_title", - "{{kwinSequence}}", - () => { - console.log("BLEH! Fire shortcut '{{kwinSequence}}'"); - callDBus("wtq.svc", "/wtq/kwin", "wtq.kwin", "OnPressShortcut", "{{mod}}", "{{key}}"); - console.log("BLEH! /Fire shortcut '{{kwinSequence}}'"); - } - ); - console.log("/Registering shortcut"); - """; - - try - { - await File.WriteAllTextAsync(path, script, cancellationToken).NoCtx(); - - _log.LogInformation("Loading script '{ScriptId}'", scriptId); - await _scripting.LoadScriptAsync(path, scriptId).NoCtx(); - await _scripting.StartAsync().NoCtx(); - - _log.LogInformation("Loaded script '{ScriptId}'", scriptId); - } - finally - { - _log.LogInformation("Executed script in {ElapsedMs}ms", sw.ElapsedMilliseconds); - } - } -} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinWindowService.cs b/src/20-Services/Wtq.Services.KWin/KWinWindowService.cs new file mode 100644 index 0000000..44218ef --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/KWinWindowService.cs @@ -0,0 +1,63 @@ +using Wtq.Configuration; + +namespace Wtq.Services.KWin; + +public class KWinWindowService( + IKWinClient kwinClient) + : IWtqWindowService +{ + private readonly ILogger _log = Log.For(); + + private readonly IKWinClient _kwinClient = Guard.Against.Null(kwinClient); + + public Task CreateAsync(WtqAppOptions opts) + { + Guard.Against.Null(opts); + + using var process = new Process(); + + process.StartInfo = new ProcessStartInfo() + { + FileName = opts.FileName, + Arguments = opts.Arguments, + }; + + process.Start(); + + return Task.CompletedTask; + } + + public async Task FindWindowAsync(WtqAppOptions opts) + { + _ = Guard.Against.Null(opts); + + try + { + var clients = await GetWindowsAsync().NoCtx(); + + return clients.FirstOrDefault(c => c.Matches(opts)); + } + catch (Exception ex) + { + _log.LogError(ex, "Failed to look up list of windows: {Message}", ex.Message); + } + + return null; + } + + public async Task GetForegroundWindowAsync() + { + var w = await _kwinClient.GetForegroundWindowAsync().NoCtx(); + + return w != null + ? new KWinWtqWindow(_kwinClient, w) + : null; + } + + public async Task> GetWindowsAsync() + { + return (await _kwinClient.GetWindowListAsync(CancellationToken.None).NoCtx()) + .Select(c => (WtqWindow)new KWinWtqWindow(_kwinClient, c)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/KWinWtqWindow.cs b/src/20-Services/Wtq.Services.KWin/KWinWtqWindow.cs index 3036a22..d377d79 100644 --- a/src/20-Services/Wtq.Services.KWin/KWinWtqWindow.cs +++ b/src/20-Services/Wtq.Services.KWin/KWinWtqWindow.cs @@ -11,84 +11,73 @@ public class KWinWtqWindow( private readonly IKWinClient _kwinClient = Guard.Against.Null(kwinClient); private readonly KWinWindow _window = Guard.Against.Null(window); - private bool? _isAlwaysOnTop; - private bool? _isTaskbarIconVisible; - private int? _transparency; - private bool? _isVisible; - private Rectangle _rect = new(0, 0, 1024, 768); // TODO: Get actual window size. - - public override int Id { get; } - + public override string Id => _window.InternalId ?? ""; + + /// + /// TODO: Add proper window activity checking. + /// - Does the window still exist? + /// - Is the window still valid/movable/whatever? + /// - etc. + /// public override bool IsValid { get; } = true; public override string? Name => _window?.ResourceClass; - public override Rectangle WindowRect => _rect; - public override async Task BringToForegroundAsync() { await _kwinClient.BringToForegroundAsync(_window, CancellationToken.None).NoCtx(); } + public override async Task GetWindowRectAsync() + { + var w = await _kwinClient.GetWindowAsync(_window).NoCtx(); + + return w.FrameGeometry.ToRect(); // TODO: Handle null. + } + public override bool Matches(WtqAppOptions opts) { Guard.Against.Null(opts); - return _window.ResourceClass?.Equals(opts.FileName, StringComparison.OrdinalIgnoreCase) ?? false; + var searchTerm = opts.ProcessName; + if (string.IsNullOrWhiteSpace(searchTerm)) + { + searchTerm = Path.GetFileNameWithoutExtension(opts.FileName); + } + + return + searchTerm.Equals(_window.ResourceClass, StringComparison.OrdinalIgnoreCase) || + searchTerm.Equals(_window.ResourceName, StringComparison.OrdinalIgnoreCase); } - public override async Task MoveToAsync(Rectangle rect, bool repaint = true) + public override async Task MoveToAsync(Point location) { - _rect = rect; + await _kwinClient.MoveWindowAsync(_window, location, CancellationToken.None).NoCtx(); + } - await _kwinClient.MoveClientAsync(_window, rect, CancellationToken.None).NoCtx(); + public override async Task ResizeAsync(Size size) + { + await _kwinClient.ResizeWindowAsync(_window, size, CancellationToken.None).NoCtx(); } public override async Task SetAlwaysOnTopAsync(bool isAlwaysOnTop) { - if (_isAlwaysOnTop == isAlwaysOnTop) - { - return; - } - await _kwinClient.SetWindowAlwaysOnTopAsync(_window, isAlwaysOnTop, CancellationToken.None).NoCtx(); - - _isAlwaysOnTop = isAlwaysOnTop; } public override async Task SetTaskbarIconVisibleAsync(bool isVisible) { - if (_isTaskbarIconVisible == isVisible) - { - return; - } - await _kwinClient.SetTaskbarIconVisibleAsync(_window, isVisible, CancellationToken.None).NoCtx(); - - _isTaskbarIconVisible = isVisible; } public override async Task SetTransparencyAsync(int transparency) { - if (_transparency == transparency) - { - return; - } - await _kwinClient.SetWindowOpacityAsync(_window, transparency * .01f, CancellationToken.None).NoCtx(); - - _transparency = transparency; } - public override async Task SetVisibleAsync(bool isVisible) + public override Task SetWindowTitleAsync(string title) { - if (_isVisible == isVisible) - { - return; - } - - await _kwinClient.SetWindowVisibleAsync(_window, isVisible, CancellationToken.None).NoCtx(); - - _isVisible = isVisible; + // TODO + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Scripting/IKWinScriptService.cs b/src/20-Services/Wtq.Services.KWin/Scripting/IKWinScriptService.cs new file mode 100644 index 0000000..f7e6fc8 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Scripting/IKWinScriptService.cs @@ -0,0 +1,6 @@ +namespace Wtq.Services.KWin.Scripting; + +public interface IKWinScriptService +{ + Task LoadScriptAsync(string path); +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Utils/JsUtils.cs b/src/20-Services/Wtq.Services.KWin/Scripting/JsUtils.cs similarity index 73% rename from src/20-Services/Wtq.Services.KWin/Utils/JsUtils.cs rename to src/20-Services/Wtq.Services.KWin/Scripting/JsUtils.cs index 65e5b09..dbc3028 100644 --- a/src/20-Services/Wtq.Services.KWin/Utils/JsUtils.cs +++ b/src/20-Services/Wtq.Services.KWin/Scripting/JsUtils.cs @@ -1,10 +1,10 @@ -namespace Wtq.Services.KWin.Utils; +namespace Wtq.Services.KWin.Scripting; public static class JsUtils { [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "MvdO: Needs to be lower-case for JS runtime.")] public static string ToJsBoolean(bool b) { - return (!b).ToString().ToLowerInvariant(); + return b.ToString().ToLowerInvariant(); } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Scripting/KWinScript.cs b/src/20-Services/Wtq.Services.KWin/Scripting/KWinScript.cs new file mode 100644 index 0000000..003928c --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Scripting/KWinScript.cs @@ -0,0 +1,13 @@ +namespace Wtq.Services.KWin.Scripting; + +public sealed class KWinScript( + Func onDispose) + : IAsyncDisposable +{ + private readonly Func _onDispose = Guard.Against.Null(onDispose); + + public async ValueTask DisposeAsync() + { + await _onDispose().NoCtx(); + } +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Scripting/KWinScriptService.cs b/src/20-Services/Wtq.Services.KWin/Scripting/KWinScriptService.cs new file mode 100644 index 0000000..03cfae3 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/Scripting/KWinScriptService.cs @@ -0,0 +1,36 @@ +using Wtq.Services.KWin.DBus; + +namespace Wtq.Services.KWin.Scripting; + +internal sealed class KWinScriptService( + IDBusConnection dbus) + : IKWinScriptService +{ + private readonly IDBusConnection _dbus = Guard.Against.Null(dbus); + + public async Task LoadScriptAsync(string path) + { + Guard.Against.NullOrWhiteSpace(path); + + path = Path.GetFullPath(path); + + var id = Path.GetFileName(path); + + var scr = await _dbus.GetScriptingAsync().NoCtx(); + + if (await scr.IsScriptLoadedAsync(id).NoCtx()) + { + await scr.UnloadScriptAsync(id).NoCtx(); + } + + if (!File.Exists(path)) + { + throw new WtqException($"No such script file at path '{path}'."); + } + + await scr.LoadScriptAsync(path, id).NoCtx(); + await scr.StartAsync().NoCtx(); + + return new KWinScript(() => scr.UnloadScriptAsync(id)); + } +} \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/ServiceCollectionExtensions.cs b/src/20-Services/Wtq.Services.KWin/ServiceCollectionExtensions.cs index 3ff0aee..c68c8fa 100644 --- a/src/20-Services/Wtq.Services.KWin/ServiceCollectionExtensions.cs +++ b/src/20-Services/Wtq.Services.KWin/ServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using Wtq.Events; using Wtq.Services.KWin.DBus; +using Wtq.Services.KWin.Scripting; namespace Wtq.Services.KWin; @@ -8,54 +8,20 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddKWin(this IServiceCollection services) { + Guard.Against.Null(services); + return services - .AddSingleton() - .AddSingleton(p => new KWinClient( - p.GetRequiredService(), - p.GetRequiredService())) + // DBus. .AddSingleton() - .AddSingleton( - p => - { - var dbus = p.GetRequiredService(); - - return new KWinService(dbus.ClientConnection, "org.kde.KWin"); - }) - .AddSingleton( - p => - { - var kwinService = p.GetRequiredService(); - - return kwinService.CreateScripting("/Scripting"); - }) - .AddSingleton>( - async p => - { - var dbus = (DBusConnection)p.GetRequiredService(); - await dbus.StartAsync(CancellationToken.None).NoCtx(); + .AddSingleton() - var wtqDBusObj = new WtqDBusObject(p.GetRequiredService()); + .AddSingleton() + .AddSingleton() - await dbus.RegisterServiceAsync("wtq.svc", wtqDBusObj).ConfigureAwait(false); + .AddSingleton() + .AddSingleton() - return wtqDBusObj; - }) - .AddSingleton() - .AddKWinProcessService() - .AddKWinScreenCoordsProvider() - .AddHostedService(); - } - - public static IServiceCollection AddKWinProcessService(this IServiceCollection services) - { - return services - .AddSingleton(); - } - - public static IServiceCollection AddKWinScreenCoordsProvider(this IServiceCollection services) - { - return services - .AddSingleton(); + .AddSingleton(); } } \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/Wtq.Services.KWin.csproj b/src/20-Services/Wtq.Services.KWin/Wtq.Services.KWin.csproj index bc00503..adf7fef 100644 --- a/src/20-Services/Wtq.Services.KWin/Wtq.Services.KWin.csproj +++ b/src/20-Services/Wtq.Services.KWin/Wtq.Services.KWin.csproj @@ -7,11 +7,18 @@ - + - + + + + + + + - - + + Always + \ No newline at end of file diff --git a/src/20-Services/Wtq.Services.KWin/wtq.kwin.js b/src/20-Services/Wtq.Services.KWin/wtq.kwin.js new file mode 100644 index 0000000..2e743c4 --- /dev/null +++ b/src/20-Services/Wtq.Services.KWin/wtq.kwin.js @@ -0,0 +1,348 @@ +"use strict"; + +let cmds = {}; +let kwin = {}; +let log = {}; +let wtq = {}; + +// Logging ///////////////////////////////////////////////// + +log.log = (level, msg) => { + // console.log(`${new Date().toISOString()} [${level}] ${msg}`); + wtq.log(level, msg); +}; + +log.error = (msg) => log.log("ERR", msg); +log.info = (msg) => log.log("INF", msg); +log.warning = (msg) => log.log("WRN", msg); + +//////////////////////////////////////////////////////////// + +// Utils /////////////////////////////////////////////////// + +let utils = {}; +utils.mapRect = (rect) => { + return { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: Math.round(rect.height), + + top: Math.round(rect.top), + bottom: Math.round(rect.bottom), + left: Math.round(rect.left), + right: Math.round(rect.right), + }; +}; + +//////////////////////////////////////////////////////////// + +// KWin Helper Functions /////////////////////////////////// + +kwin.getWindows = () => { + + // KWin5 + if (typeof workspace.clientList === "function") { + log.info("Fetching window list using 'workspace.clientList()' (KWin5)"); + return workspace.clientList(); + } + + // KWin6 + if (typeof workspace.windowList === "function") { + log.info("Fetching window list using 'workspace.windowList()' (KWin6)"); + return workspace.windowList(); + } + + log.warning("Could not find function to fetch windows, unsupported version of KWin perhaps?"); +}; + +kwin.getWindowByInternalId = (internalId) => { + for (const w of kwin.getWindows()) { + // "internalId" is an object, so convert to string first. + // Looks like this: + // {ec94dfb2-f5fb-4485-bf9d-49658a68b365} + if (w.internalId.toString() === internalId) { + return w; + } + } +}; + +kwin.getWindowByInternalIdRequired = (internalId) => { + const w = kwin.getWindowByInternalId(internalId); + + if (!w) { + throw `No window found with internal id ${internalId}`; + } + + return w; +}; + +kwin.getActiveWindow = (window) => { + // KWin5 + if (typeof workspace.activeClient === "object") { + log.info("Using KWin5 interface 'workspace.activeClient'"); + return workspace.activeClient; + } + + // KWin6 + if (typeof workspace.activeWindow === "object") { + log.info("Using KWin6 interface 'workspace.activeWindow'"); + return workspace.activeWindow; + } + + throw "Could not find property for active client/window, unsupported version of KWin perhaps?"; +}; + +kwin.setActiveWindow = (window) => { + // KWin5 + if (typeof workspace.activeClient === "object") { + log.info("Using KWin5 interface 'workspace.activeClient'"); + workspace.activeClient = window; + return; + } + + // KWin6 + if (typeof workspace.activeWindow === "object") { + log.info("Using KWin6 interface 'workspace.activeWindow'"); + workspace.activeWindow = window; + return; + } + + throw "Could not find property for active client/window, unsupported version of KWin perhaps?"; +}; + +//////////////////////////////////////////////////////////// + +// WTQ ///////////////////////////////////////////////////// +wtq.DBUS_SERVICE = "wtq.svc"; +wtq.DBUS_PATH = "/wtq/kwin"; +wtq.DBUS_INTERFACE = "wtq.kwin"; + +// Send log to WTQ so we can see what's going on in the KWin script. +wtq.log = (level, msg) => { + callDBus( + wtq.DBUS_SERVICE, // Service + wtq.DBUS_PATH, // Path + wtq.DBUS_INTERFACE, // Interface + "Log", // Method + level, // Argument 1 + msg, // Argument 2 + ); +}; + +// Ask WTQ for the next command to execute. +wtq.getNextCommand = () => { + log.info(`GET_NEXT_COMMAND`); + + callDBus( + wtq.DBUS_SERVICE, // Service + wtq.DBUS_PATH, // Path + wtq.DBUS_INTERFACE, // Interface + "GetNextCommand", // Method + "arg 1", // Argument 1 + "arg 2", // Argument 2 + "arg 3", // Argument 3 + wtq.onGotCommand // Response callback + ); +}; + +// Called when a command has been received from WTQ. +wtq.onGotCommand = (cmdInfoStr) => { + // Deserialize command info from JSON. + const cmdInfo = JSON.parse(cmdInfoStr); + + try { + log.info(`COMMAND TYPE: ${cmdInfo.type}`); + + // TODO: Check if session ends. + const cmd = cmds[cmdInfo.type]; + + // See if we can map the received command to a function. + if (typeof cmd === "function") { + const respParams = cmd(cmdInfo, cmdInfo.params) ?? {}; + wtq.sendResponse(cmdInfo, respParams); + } else { + throw `Unknown command '${cmdInfo.type}'`; + } + } catch (ex) { + log.error(`OH NOES! Anyway ${ex}`); + wtq.sendResponse(cmdInfo, {}, ex.message); + } + + wtq.getNextCommand(); +}; + +(() => { wtq.getNextCommand(); })(); + +wtq.sendResponse = (cmdInfo, params, exception_message) => { + log.info(`SEND RESPONSE, a:${cmdInfo.type}`); + + callDBus( + wtq.DBUS_SERVICE, // Service + wtq.DBUS_PATH, // Path + wtq.DBUS_INTERFACE, // Interface + "SendResponse", + JSON.stringify({ + cmdType: cmdInfo.type, + responderId: cmdInfo.responderId, + params: params, + exception_message: exception_message, + })); +}; +//////////////////////////////////////////////////////////// + +// Commands //////////////////////////////////////////////// +cmds["BRING_WINDOW_TO_FOREGROUND"] = (cmdInfo) => { + const p = cmdInfo.params; + const w = kwin.getWindowByInternalIdRequired(p.internalId); + + log.info(`Bringing to foreground window with internal id '${p.internalId}'`); + + kwin.setActiveWindow(w); +}; + +cmds["GET_CURSOR_POS"] = (cmdInfo) => { + return workspace.cursorPos; +}; + +cmds["GET_FOREGROUND_WINDOW"] = (cmdInfo) => { + const w = kwin.getActiveWindow(); + + return { + frameGeometry: utils.mapRect(w.frameGeometry), + hidden: w.hidden, + internalId: w.internalId, + keepAbove: w.keepAbove, + layer: w.layer, + minimized: w.minimized, + resourceClass: w.resourceClass, + resourceName: w.resourceName, + skipPager: w.skipPager, + skipSwitcher: w.skipSwitcher, + skipTaskbar: w.skipTaskbar, + }; +}; + +cmds["GET_WINDOW"] = (cmdInfo) => { + const p = cmdInfo.params; + const w = kwin.getWindowByInternalIdRequired(p.internalId); + + return { + frameGeometry: utils.mapRect(w.frameGeometry), + hidden: w.hidden, + internalId: w.internalId, + keepAbove: w.keepAbove, + layer: w.layer, + minimized: w.minimized, + resourceClass: w.resourceClass, + resourceName: w.resourceName, + skipPager: w.skipPager, + skipSwitcher: w.skipSwitcher, + skipTaskbar: w.skipTaskbar, + }; +}; + +cmds["GET_WINDOW_LIST"] = (cmdInfo) => { + return { + windows: kwin + .getWindows() + .map(w => { + return { + internalId: w.internalId, + resourceClass: w.resourceClass, + resourceName: w.resourceName + }; + }), + }; +}; + +cmds["MOVE_WINDOW"] = (cmdInfo, p) => { + // We used to both move- and resize the window in here, but this caused some issues with multi-monitor setups. + // Not entirely sure why, apparently KWin doesn't like hammering so many widths and heights. + // So we split it off, and now do a single RESIZE_WINDOW before a bunch of MOVE_WINDOWs. + let w = kwin.getWindowByInternalIdRequired(p.internalId); + + log.info(`Moving win ${p.internalId} to x:${p.x}, y:${p.y}, width: ${p.width}, height:${p.height}`); + + // Note that it's important to set the entire "frameGeometry" object in one go, otherwise separate properties may become readonly, + // allowing us to eg. only set the width, and not the height, or vice versa. + // Not sure if this is a bug, but it took a bunch of time to figure out. + let q = Object.assign({}, w.frameGeometry); + + q.x = p.x; + q.y = p.y; + + w.frameGeometry = q; +}; + +cmds["RESIZE_WINDOW"] = (cmdInfo) => { + const p = cmdInfo.params; + let w = kwin.getWindowByInternalIdRequired(p.internalId); + + log.info(`Moving win ${p.internalId} to x:${p.x}, y:${p.y}, width: ${p.width}, height:${p.height}`); + + // Note that it's important to set the entire "frameGeometry" object in one go, otherwise separate properties may become readonly, + // allowing us to eg. only set the width, and not the height, or vice versa. + // Not sure if this is a bug, but it took a bunch of time to figure out. + + let q = Object.assign({}, w.frameGeometry); + + q.width = p.width; + q.height = p.height; + + w.frameGeometry = q; +}; + +cmds["NOOP"] = (cmdInfo) => { }; + +cmds["REGISTER_HOT_KEY"] = (cmdInfo, p) => { + log.info(`Registering hotkey with name:'${p.name}', sequence:'${p.sequence}', key:'${p.key}' and mod:'${p.mod}'`); + + registerShortcut( + p.name, + p.title, + p.sequence, + () => { + log.info(`Firing hotkey with name:'${p.name}', sequence:'${p.sequence}', key:'${p.key}' and mod:'${p.mod}'`); + + callDBus( + "wtq.svc", + "/wtq/kwin", + "wtq.kwin", + "OnPressShortcut", + p.mod, + p.key); + }); +} + +cmds["SET_WINDOW_ALWAYS_ON_TOP"] = (cmdInfo) => { + const p = cmdInfo.params; + const w = kwin.getWindowByInternalIdRequired(p.internalId); + + log.info(`Setting 'always on top'-state for window with internal id '${p.internalId}' to '${p.isAlwaysOnTop}'`); + + w.keepAbove = p.isAlwaysOnTop; +}; + +cmds["SET_WINDOW_OPACITY"] = (cmdInfo) => { + const p = cmdInfo.params; + const w = kwin.getWindowByInternalIdRequired(p.internalId); + + log.info(`Setting opacity for window with internal id '${p.internalId}' to '${p.opacity}'`); + + w.opacity = p.opacity; +}; + +cmds["SET_WINDOW_TASKBAR_ICON_VISIBLE"] = (cmdInfo) => { + const p = cmdInfo.params; + const w = kwin.getWindowByInternalIdRequired(p.internalId); + + const skip = !(p.isVisible == "true"); + + log.info(`Setting taskbar icon visible for window with internal id '${p.internalId}' to '${p.isVisible}' (skip: ${skip})`); + + w.skipPager = skip; + w.skipSwitcher = skip; + w.skipTaskbar = skip; +}; +//////////////////////////////////////////////////////////// diff --git a/src/20-Services/Wtq.Services.Win32/Win32WtqWindow.cs b/src/20-Services/Wtq.Services.Win32/Win32WtqWindow.cs index 9286abd..1c079f2 100644 --- a/src/20-Services/Wtq.Services.Win32/Win32WtqWindow.cs +++ b/src/20-Services/Wtq.Services.Win32/Win32WtqWindow.cs @@ -153,11 +153,6 @@ public override Task SetTransparencyAsync(int transparency) return Task.CompletedTask; } - public override Task SetVisibleAsync(bool isVisible) - { - return Task.CompletedTask; - } - public override Task SetWindowTitleAsync(string title) { User32.SetWindowText(_process.MainWindowHandle, title);