diff --git a/.editorconfig b/.editorconfig index 4283ada28..b775e8c2f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ indent_style = space end_of_line = crlf # Code files -[*.{cs,yaml,xaml,json,csproj,sln}] +[*.{cs,yaml,xaml,json,csproj}] indent_size = 2 insert_final_newline = true charset = utf-8 diff --git a/.vscode/launch.json b/.vscode/launch.json index 97135d553..391805849 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,9 +10,9 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/GlazeWM.Bootstrapper/bin/Debug/net7-windows10.0.17763.0/GlazeWM.dll", + "program": "${workspaceFolder}/GlazeWM.App/bin/Debug/net7-windows10.0.17763.0/GlazeWM.dll", "args": [], - "cwd": "${workspaceFolder}/GlazeWM.Bootstrapper", + "cwd": "${workspaceFolder}/GlazeWM.App", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ec4f7dd73..0079abd47 100755 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj", + "${workspaceFolder}/GlazeWM.App/GlazeWM.App.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -19,7 +19,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj", + "${workspaceFolder}/GlazeWM.App/GlazeWM.App.csproj", "--configuration=Release", "--runtime=win-x64", "--self-contained", @@ -36,7 +36,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj", + "${workspaceFolder}/GlazeWM.App/GlazeWM.App.csproj", "--configuration=Release", "--runtime=win-x86", "--self-contained", @@ -55,7 +55,7 @@ "watch", "run", "--project", - "${workspaceFolder}/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj" + "${workspaceFolder}/GlazeWM.App/GlazeWM.App.csproj" ], "problemMatcher": "$msCompile" }, @@ -65,7 +65,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj", + "${workspaceFolder}/GlazeWM.App/GlazeWM.App.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], diff --git a/GlazeWM.App.Cli/CliStartup.cs b/GlazeWM.App.Cli/CliStartup.cs new file mode 100644 index 000000000..9ff57ee70 --- /dev/null +++ b/GlazeWM.App.Cli/CliStartup.cs @@ -0,0 +1,83 @@ +using System.Reactive.Linq; +using System.Text.Json; +using GlazeWM.Infrastructure.Common; +using GlazeWM.Infrastructure.Utils; + +namespace GlazeWM.App.Cli +{ + public sealed class CliStartup + { + public static async Task Run( + string[] args, + int ipcServerPort, + bool isSubscribeMessage) + { + var client = new WebsocketClient(ipcServerPort); + + try + { + var isConnected = client.Connect(); + + if (!isConnected) + throw new Exception("Unable to connect to IPC server."); + + client.ReceiveAsync(); + + var message = string.Join(" ", args); + var sendSuccess = client.SendTextAsync(message); + + if (!sendSuccess) + throw new Exception("Failed to send message to IPC server."); + + var serverMessages = GetMessagesObservable(client); + + // Wait for server to respond with a message. + var firstMessage = await serverMessages + .Timeout(TimeSpan.FromSeconds(5)) + .FirstAsync(); + + // Exit on first message received when not subscribing to an event. + if (!isSubscribeMessage) + { + Console.WriteLine(firstMessage); + client.Disconnect(); + return ExitCode.Success; + } + + // Special handling is needed for event subscriptions. + serverMessages.Subscribe( + onNext: Console.WriteLine, + onError: Console.Error.WriteLine + ); + + var _ = Console.ReadLine(); + + client.Disconnect(); + return ExitCode.Success; + } + catch (Exception exception) + { + Console.Error.WriteLine(exception.Message); + client.Disconnect(); + return ExitCode.Error; + } + } + + /// + /// Get `IObservable` of parsed server messages. + /// + private static IObservable GetMessagesObservable(WebsocketClient client) + { + return client.Messages.Select(message => + { + var parsedMessage = JsonDocument.Parse(message).RootElement; + var error = parsedMessage.GetProperty("error").GetString(); + + if (error is not null) + throw new Exception(error); + + return parsedMessage.GetProperty("data").ToString(); + }); + } + } +} diff --git a/GlazeWM.App.Cli/GlazeWM.App.Cli.csproj b/GlazeWM.App.Cli/GlazeWM.App.Cli.csproj new file mode 100644 index 000000000..18154ae3c --- /dev/null +++ b/GlazeWM.App.Cli/GlazeWM.App.Cli.csproj @@ -0,0 +1,12 @@ + + + net7-windows10.0.17763 + enable + enable + + + + + + + diff --git a/GlazeWM.App.IpcServer/DependencyInjection.cs b/GlazeWM.App.IpcServer/DependencyInjection.cs new file mode 100644 index 000000000..718b27150 --- /dev/null +++ b/GlazeWM.App.IpcServer/DependencyInjection.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace GlazeWM.App.IpcServer +{ + public static class DependencyInjection + { + public static IServiceCollection AddIpcServerServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + } +} diff --git a/GlazeWM.App.IpcServer/GlazeWM.App.IpcServer.csproj b/GlazeWM.App.IpcServer/GlazeWM.App.IpcServer.csproj new file mode 100644 index 000000000..838f54dab --- /dev/null +++ b/GlazeWM.App.IpcServer/GlazeWM.App.IpcServer.csproj @@ -0,0 +1,12 @@ + + + net7-windows10.0.17763.0 + embedded + enable + + + + + + + diff --git a/GlazeWM.App.IpcServer/IpcMessageHandler.cs b/GlazeWM.App.IpcServer/IpcMessageHandler.cs new file mode 100644 index 000000000..cf31878f7 --- /dev/null +++ b/GlazeWM.App.IpcServer/IpcMessageHandler.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using CommandLine; +using GlazeWM.App.IpcServer.Messages; +using GlazeWM.App.IpcServer.Server; +using GlazeWM.Domain.Containers; +using GlazeWM.Domain.Monitors; +using GlazeWM.Domain.UserConfigs; +using GlazeWM.Domain.Windows; +using GlazeWM.Domain.Workspaces; +using GlazeWM.Infrastructure.Bussing; +using GlazeWM.Infrastructure.Serialization; +using GlazeWM.Infrastructure.Utils; +using Microsoft.Extensions.Logging; + +namespace GlazeWM.App.IpcServer +{ + public sealed class IpcMessageHandler + { + private readonly Bus _bus; + private readonly CommandParsingService _commandParsingService; + private readonly ContainerService _containerService; + private readonly ILogger _logger; + private readonly MonitorService _monitorService; + private readonly WorkspaceService _workspaceService; + private readonly WindowService _windowService; + + private readonly JsonSerializerOptions _serializeOptions = + JsonParser.OptionsFactory((options) => + { + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.Converters.Add(new JsonContainerConverter()); + }); + + /// + /// Dictionary of event names and session IDs subscribed to that event. + /// + internal Dictionary> SubscribedSessions = new(); + + /// + /// Matches words separated by spaces when not surrounded by double quotes. + /// Example: "a \"b c\" d" -> ["a", "\"b c\"", "d"] + /// + private static readonly Regex _messagePartsRegex = new("(\".*?\"|\\S+)"); + + public IpcMessageHandler( + Bus bus, + CommandParsingService commandParsingService, + ContainerService containerService, + ILogger logger, + MonitorService monitorService, + WorkspaceService workspaceService, + WindowService windowService) + { + _bus = bus; + _commandParsingService = commandParsingService; + _containerService = containerService; + _logger = logger; + _monitorService = monitorService; + _workspaceService = workspaceService; + _windowService = windowService; + } + + internal string GetResponseMessage(ClientMessage message) + { + var (sessionId, messageString) = message; + + _logger.LogDebug( + "IPC message from session {Session}: {Message}.", + sessionId, + messageString + ); + + try + { + var messageParts = _messagePartsRegex.Matches(messageString) + .Select(match => match.Value) + .Where(match => match is not null); + + var parsedArgs = Parser.Default.ParseArguments< + InvokeCommandMessage, + SubscribeMessage, + GetMonitorsMessage, + GetWorkspacesMessage, + GetWindowsMessage + >(messageParts); + + object? data = parsedArgs.Value switch + { + InvokeCommandMessage commandMsg => HandleInvokeCommandMessage(commandMsg), + SubscribeMessage subscribeMsg => HandleSubscribeMessage(subscribeMsg, sessionId), + GetMonitorsMessage => _monitorService.GetMonitors(), + GetWorkspacesMessage => _workspaceService.GetActiveWorkspaces(), + GetWindowsMessage => _windowService.GetWindows(), + _ => throw new Exception($"Invalid message '{messageString}'") + }; + + return ToResponseMessage( + success: true, + data: data, + clientMessage: messageString + ); + } + catch (Exception exception) + { + return ToResponseMessage( + success: false, + data: null, + clientMessage: messageString, + error: exception.Message + ); + } + } + + private bool? HandleInvokeCommandMessage(InvokeCommandMessage message) + { + var contextContainer = + _containerService.GetContainerById(message.ContextContainerId) ?? + _containerService.FocusedContainer; + + var commandString = CommandParsingService.FormatCommand(message.Command); + + var command = _commandParsingService.ParseCommand( + commandString, + contextContainer + ); + + _bus.Invoke((dynamic)command); + return null; + } + + private bool? HandleSubscribeMessage(SubscribeMessage message, Guid sessionId) + { + foreach (var eventName in message.Events.Split(',')) + { + if (SubscribedSessions.ContainsKey(eventName)) + { + var sessionIds = SubscribedSessions.GetValueOrThrow(eventName); + sessionIds.Add(sessionId); + continue; + } + + SubscribedSessions.Add(eventName, new() { sessionId }); + } + + return null; + } + + private string ToResponseMessage( + bool success, + T? data, + string clientMessage, + string? error = null) + { + var responseMessage = new ServerMessage( + Success: success, + MessageType: ServerMessageType.ClientResponse, + Data: data, + Error: error, + ClientMessage: clientMessage + ); + + return JsonParser.ToString((dynamic)responseMessage, _serializeOptions); + } + + internal string ToEventMessage(Event @event) + { + var eventMessage = new ServerMessage( + Success: true, + MessageType: ServerMessageType.SubscribedEvent, + Data: @event, + Error: null, + ClientMessage: null + ); + + return JsonParser.ToString((dynamic)eventMessage, _serializeOptions); + } + } +} diff --git a/GlazeWM.App.IpcServer/IpcServerManager.cs b/GlazeWM.App.IpcServer/IpcServerManager.cs new file mode 100644 index 000000000..0a612d717 --- /dev/null +++ b/GlazeWM.App.IpcServer/IpcServerManager.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using GlazeWM.App.IpcServer.Server; +using GlazeWM.Infrastructure.Bussing; +using Microsoft.Extensions.Logging; + +namespace GlazeWM.App.IpcServer +{ + public sealed class IpcServerManager : IDisposable + { + private readonly Bus _bus; + private readonly IpcMessageHandler _ipcMessageHandler; + private readonly ILogger _logger; + + /// + /// The websocket server instance. + /// + private Server.IpcServer? _server { get; set; } + + private readonly Subject _serverKill = new(); + + public IpcServerManager( + Bus bus, + IpcMessageHandler ipcMessageHandler, + ILogger logger) + { + _bus = bus; + _ipcMessageHandler = ipcMessageHandler; + _logger = logger; + } + + /// + /// Start the IPC server on specified port. + /// + public void StartServer(int port) + { + _server = new(port); + _server.Start(); + + // Start listening for messages. + _server.Messages + .TakeUntil(_serverKill) + .Subscribe(clientMessage => + { + var responseMessage = _ipcMessageHandler.GetResponseMessage(clientMessage); + SendToSession(clientMessage.SessionId, responseMessage); + }); + + // Broadcast events to subscribed sessions. + _bus.Events + .TakeUntil(_serverKill) + .Subscribe(@event => + { + var subscribedSessionIds = + _ipcMessageHandler.SubscribedSessions.GetValueOrDefault( + @event.FriendlyName, + new List() + ); + + foreach (var sessionId in subscribedSessionIds) + { + var responseMessage = _ipcMessageHandler.ToEventMessage(@event); + SendToSession(sessionId, responseMessage); + } + }); + + _logger.LogDebug("Started IPC server on port {Port}.", port); + } + + /// + /// Kill the IPC server. + /// + public void StopServer() + { + if (_server is null) + return; + + _serverKill.OnNext(true); + _server.Stop(); + _logger.LogDebug("Stopped IPC server on port {Port}.", _server.Port); + } + + /// + /// Send text message to given session ID. + /// + private void SendToSession(Guid sessionId, string text) + { + var session = _server?.FindSession(sessionId) as IpcSession; + session?.SendTextAsync(text); + } + + public void Dispose() + { + if (_server?.IsDisposed != true) + _server?.Dispose(); + } + } +} diff --git a/GlazeWM.App.IpcServer/Messages/GetMonitorsMessage.cs b/GlazeWM.App.IpcServer/Messages/GetMonitorsMessage.cs new file mode 100644 index 000000000..a75768992 --- /dev/null +++ b/GlazeWM.App.IpcServer/Messages/GetMonitorsMessage.cs @@ -0,0 +1,9 @@ +using CommandLine; + +namespace GlazeWM.App.IpcServer.Messages +{ + [Verb("monitors", HelpText = "Get all monitors.")] + public class GetMonitorsMessage + { + } +} diff --git a/GlazeWM.App.IpcServer/Messages/GetWindowsMessage.cs b/GlazeWM.App.IpcServer/Messages/GetWindowsMessage.cs new file mode 100644 index 000000000..66c860a66 --- /dev/null +++ b/GlazeWM.App.IpcServer/Messages/GetWindowsMessage.cs @@ -0,0 +1,9 @@ +using CommandLine; + +namespace GlazeWM.App.IpcServer.Messages +{ + [Verb("windows", HelpText = "Get all windows.")] + public class GetWindowsMessage + { + } +} diff --git a/GlazeWM.App.IpcServer/Messages/GetWorkspacesMessage.cs b/GlazeWM.App.IpcServer/Messages/GetWorkspacesMessage.cs new file mode 100644 index 000000000..720d61cd2 --- /dev/null +++ b/GlazeWM.App.IpcServer/Messages/GetWorkspacesMessage.cs @@ -0,0 +1,9 @@ +using CommandLine; + +namespace GlazeWM.App.IpcServer.Messages +{ + [Verb("workspaces", HelpText = "Get all workspaces.")] + public class GetWorkspacesMessage + { + } +} diff --git a/GlazeWM.App.IpcServer/Messages/InvokeCommandMessage.cs b/GlazeWM.App.IpcServer/Messages/InvokeCommandMessage.cs new file mode 100644 index 000000000..8c3cddfec --- /dev/null +++ b/GlazeWM.App.IpcServer/Messages/InvokeCommandMessage.cs @@ -0,0 +1,26 @@ +using CommandLine; + +namespace GlazeWM.App.IpcServer.Messages +{ + [Verb( + "command", + HelpText = "Invoke a WM command (eg. `command \"focus workspace 1\"`)." + )] + public class InvokeCommandMessage + { + [Value( + 0, + Required = true, + HelpText = "WM command to run (eg. \"focus workspace 1\")" + )] + public string Command { get; set; } + + [Option( + 'c', + "context-container-id", + Required = false, + HelpText = "ID of container to use as context." + )] + public string ContextContainerId { get; set; } + } +} diff --git a/GlazeWM.App.IpcServer/Messages/SubscribeMessage.cs b/GlazeWM.App.IpcServer/Messages/SubscribeMessage.cs new file mode 100644 index 000000000..01a12b7de --- /dev/null +++ b/GlazeWM.App.IpcServer/Messages/SubscribeMessage.cs @@ -0,0 +1,19 @@ +using CommandLine; + +namespace GlazeWM.App.IpcServer.Messages +{ + [Verb( + "subscribe", + HelpText = "Subscribe to a WM event (eg. `subscribe -e window_focus,window_close`)" + )] + public class SubscribeMessage + { + [Option( + 'e', + "events", + Required = true, + HelpText = "WM events to subscribe to." + )] + public string Events { get; set; } + } +} diff --git a/GlazeWM.App.IpcServer/Server/ClientMessage.cs b/GlazeWM.App.IpcServer/Server/ClientMessage.cs new file mode 100644 index 000000000..bdf823702 --- /dev/null +++ b/GlazeWM.App.IpcServer/Server/ClientMessage.cs @@ -0,0 +1,6 @@ +using System; + +namespace GlazeWM.App.IpcServer.Server +{ + internal sealed record ClientMessage(Guid SessionId, string Message); +} diff --git a/GlazeWM.App.IpcServer/Server/IpcServer.cs b/GlazeWM.App.IpcServer/Server/IpcServer.cs new file mode 100644 index 000000000..437bbee61 --- /dev/null +++ b/GlazeWM.App.IpcServer/Server/IpcServer.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Reactive.Subjects; +using NetCoreServer; + +namespace GlazeWM.App.IpcServer.Server +{ + internal sealed class IpcServer : WsServer + { + /// + /// Messages received from all websocket sessions. + /// + public readonly Subject Messages = new(); + + public IpcServer(int port) : base(IPAddress.Any, port) + { + } + + protected override TcpSession CreateSession() + { + return new IpcSession(this); + } + + protected override void Dispose(bool disposingManagedResources) + { + if (disposingManagedResources) + Messages.Dispose(); + + base.Dispose(disposingManagedResources); + } + } +} diff --git a/GlazeWM.App.IpcServer/Server/IpcSession.cs b/GlazeWM.App.IpcServer/Server/IpcSession.cs new file mode 100644 index 000000000..a0bf38822 --- /dev/null +++ b/GlazeWM.App.IpcServer/Server/IpcSession.cs @@ -0,0 +1,23 @@ +using System.Text; +using NetCoreServer; + +namespace GlazeWM.App.IpcServer.Server +{ + internal sealed class IpcSession : WsSession + { + public IpcSession(IpcServer server) : base(server) + { + } + + /// + /// Emit received text buffers to `Messages` subject of the server. + /// + public override void OnWsReceived(byte[] buffer, long offset, long size) + { + var text = Encoding.UTF8.GetString(buffer, (int)offset, (int)size); + + if (Server is IpcServer server) + server.Messages.OnNext(new ClientMessage(Id, text)); + } + } +} diff --git a/GlazeWM.App.IpcServer/Server/ServerMessage.cs b/GlazeWM.App.IpcServer/Server/ServerMessage.cs new file mode 100644 index 000000000..04457c36b --- /dev/null +++ b/GlazeWM.App.IpcServer/Server/ServerMessage.cs @@ -0,0 +1,28 @@ +namespace GlazeWM.App.IpcServer.Server +{ + internal sealed record ServerMessage( + bool Success, + + /// + /// The type of server message. + /// + ServerMessageType MessageType, + + /// + /// The response or event data. This property is only present for messages where + /// 'Success' is true. + /// + T? Data, + + /// + /// The error message. This property is only present for messages where 'Success' + /// is false. + /// + string? Error, + + /// + /// The client message that this is in response to. This property is only present for + /// 'ClientResponse' message types. + /// + string? ClientMessage); +} diff --git a/GlazeWM.App.IpcServer/Server/ServerMessageType.cs b/GlazeWM.App.IpcServer/Server/ServerMessageType.cs new file mode 100644 index 000000000..7e6ef73e4 --- /dev/null +++ b/GlazeWM.App.IpcServer/Server/ServerMessageType.cs @@ -0,0 +1,8 @@ +namespace GlazeWM.App.IpcServer.Server +{ + internal enum ServerMessageType + { + ClientResponse, + SubscribedEvent + } +} diff --git a/GlazeWM.App.WindowManager/GlazeWM.App.WindowManager.csproj b/GlazeWM.App.WindowManager/GlazeWM.App.WindowManager.csproj new file mode 100644 index 000000000..18154ae3c --- /dev/null +++ b/GlazeWM.App.WindowManager/GlazeWM.App.WindowManager.csproj @@ -0,0 +1,12 @@ + + + net7-windows10.0.17763 + enable + enable + + + + + + + diff --git a/GlazeWM.Bootstrapper/Startup.cs b/GlazeWM.App.WindowManager/WmStartup.cs similarity index 81% rename from GlazeWM.Bootstrapper/Startup.cs rename to GlazeWM.App.WindowManager/WmStartup.cs index 837e20933..a3afc16ec 100644 --- a/GlazeWM.Bootstrapper/Startup.cs +++ b/GlazeWM.App.WindowManager/WmStartup.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Reactive.Linq; -using System.Windows.Forms; -using GlazeWM.Bar; using GlazeWM.Domain.Common.Commands; using GlazeWM.Domain.Containers.Commands; using GlazeWM.Domain.Containers.Events; @@ -11,38 +7,36 @@ using GlazeWM.Domain.Windows; using GlazeWM.Domain.Windows.Commands; using GlazeWM.Infrastructure.Bussing; +using GlazeWM.Infrastructure.Common; using GlazeWM.Infrastructure.Common.Commands; using GlazeWM.Infrastructure.Common.Events; using GlazeWM.Infrastructure.WindowsApi; using static GlazeWM.Infrastructure.WindowsApi.WindowsApiService; -namespace GlazeWM.Bootstrapper +namespace GlazeWM.App.WindowManager { - internal sealed class Startup + public sealed class WmStartup { - private readonly BarService _barService; private readonly Bus _bus; private readonly KeybindingService _keybindingService; private readonly WindowEventService _windowEventService; private readonly UserConfigService _userConfigService; - private SystemTrayIcon _systemTrayIcon { get; set; } + private SystemTrayIcon? _systemTrayIcon { get; set; } - public Startup( - BarService barService, + public WmStartup( Bus bus, KeybindingService keybindingService, WindowEventService windowEventService, UserConfigService userConfigService) { - _barService = barService; _bus = bus; _keybindingService = keybindingService; _windowEventService = windowEventService; _userConfigService = userConfigService; } - public void Run() + public ExitCode Run() { try { @@ -54,10 +48,6 @@ public void Run() _bus.Events.OfType().Subscribe((@event) => _bus.InvokeAsync(new SetActiveWindowBorderCommand(@event.FocusedContainer as Window))); - // Launch bar WPF application. Spawns bar window when monitors are added, so the service needs - // to be initialized before populating initial state. - _barService.StartApp(); - // Populate initial monitors, windows, workspaces and user config. _bus.Invoke(new PopulateInitialStateCommand()); @@ -74,7 +64,7 @@ public void Run() var systemTrayIconConfig = new SystemTrayIconConfig { HoverText = "GlazeWM", - IconResourceName = "GlazeWM.Bootstrapper.Resources.icon.ico", + IconResourceName = "GlazeWM.App.Resources.icon.ico", Actions = new Dictionary { { "Reload config", () => _bus.Invoke(new ReloadUserConfigCommand()) }, @@ -94,11 +84,13 @@ public void Run() _bus.InvokeAsync(new FocusContainerUnderCursorCommand(@event.Point)); }); - Application.Run(); + System.Windows.Forms.Application.Run(); + return ExitCode.Success; } catch (Exception exception) { _bus.Invoke(new HandleFatalExceptionCommand(exception)); + return ExitCode.Error; } } @@ -106,9 +98,8 @@ private void OnApplicationExit() { _bus.Invoke(new ShowAllWindowsCommand()); _bus.Invoke(new SetActiveWindowBorderCommand(null)); - _barService.ExitApp(); _systemTrayIcon?.Remove(); - Application.Exit(); + System.Windows.Forms.Application.Exit(); } } } diff --git a/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj b/GlazeWM.App/GlazeWM.App.csproj similarity index 77% rename from GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj rename to GlazeWM.App/GlazeWM.App.csproj index 0bd75ccf0..9f11a87b1 100644 --- a/GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj +++ b/GlazeWM.App/GlazeWM.App.csproj @@ -1,7 +1,7 @@ - WinExe + Exe net7-windows10.0.17763.0 embedded Resources\icon.ico @@ -9,6 +9,9 @@ + + + diff --git a/GlazeWM.App/Program.cs b/GlazeWM.App/Program.cs new file mode 100644 index 000000000..5d6b36063 --- /dev/null +++ b/GlazeWM.App/Program.cs @@ -0,0 +1,178 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommandLine; +using GlazeWM.App.Cli; +using GlazeWM.App.IpcServer; +using GlazeWM.App.IpcServer.Messages; +using GlazeWM.App.WindowManager; +using GlazeWM.Bar; +using GlazeWM.Domain; +using GlazeWM.Domain.Common; +using GlazeWM.Domain.Containers; +using GlazeWM.Infrastructure; +using GlazeWM.Infrastructure.Bussing; +using GlazeWM.Infrastructure.Common; +using GlazeWM.Infrastructure.Exceptions; +using GlazeWM.Infrastructure.Logging; +using GlazeWM.Infrastructure.Serialization; +using GlazeWM.Infrastructure.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +//// TODO: Handle circular reference for that one workspace event. + +namespace GlazeWM.App +{ + internal static class Program + { + private const string AppGuid = "325d0ed7-7f60-4925-8d1b-aa287b26b218"; + private const int IpcServerPort = 6123; + + /// + /// The main entry point for the application. The thread must be an STA + /// thread in order to run a message loop. + /// + [STAThread] + public static async Task Main(string[] args) + { + bool isSingleInstance; + using var _ = new Mutex(false, "Global\\" + AppGuid, out isSingleInstance); + + var parsedArgs = Parser.Default.ParseArguments< + WmStartupOptions, + InvokeCommandMessage, + SubscribeMessage, + GetMonitorsMessage, + GetWorkspacesMessage, + GetWindowsMessage + >(args); + + var exitCode = parsedArgs.Value switch + { + WmStartupOptions options => StartWm(options, isSingleInstance), + InvokeCommandMessage or + GetMonitorsMessage or + GetWorkspacesMessage or + GetWindowsMessage => await StartCli(args, isSingleInstance, false), + SubscribeMessage => await StartCli(args, isSingleInstance, true), + _ => ExitWithError(parsedArgs.Errors.First()) + }; + + return (int)exitCode; + } + + private static ExitCode StartWm(WmStartupOptions options, bool isSingleInstance) + { + if (!isSingleInstance) + { + Console.Error.WriteLine("Application is already running."); + return ExitCode.Error; + } + + ServiceLocator.Provider = BuildWmServiceProvider(options); + + var (barService, ipcServerManager, wmStartup) = + ServiceLocator.GetRequiredServices< + BarService, + IpcServerManager, + WmStartup + >(); + + ThreadUtils.CreateSTA("GlazeWMBar", barService.StartApp); + ThreadUtils.Create("GlazeWMIPC", () => ipcServerManager.StartServer(IpcServerPort)); + + // Run the window manager on the main thread. + return wmStartup.Run(); + } + + private static async Task StartCli( + string[] args, + bool isSingleInstance, + bool isSubscribeMessage) + { + if (isSingleInstance) + { + Console.Error.WriteLine("No running instance found. Cannot run CLI command."); + return ExitCode.Error; + } + + return await CliStartup.Run(args, IpcServerPort, isSubscribeMessage); + } + + private static ExitCode ExitWithError(Error error) + { + Console.Error.WriteLine($"Failed to parse startup arguments: {error}."); + return ExitCode.Error; + } + + private static ServiceProvider BuildWmServiceProvider(WmStartupOptions options) + { + var services = new ServiceCollection() + .AddLoggingService() + .AddExceptionHandler() + .AddInfrastructureServices() + .AddDomainServices() + .AddBarServices() + .AddIpcServerServices() + .AddSingleton() + .AddSingleton(options); + + return services.BuildServiceProvider(); + } + } + + public static class DependencyInjection + { + public static IServiceCollection AddLoggingService(this IServiceCollection services) + { + return services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddConsoleFormatter(); + builder.AddConsole(options => options.FormatterName = LogFormatter.Name); + builder.SetMinimumLevel(LogLevel.Debug); + }); + } + + public static IServiceCollection AddExceptionHandler(this IServiceCollection services) + { + services + .AddOptions() + .Configure((options, bus, containerService) => + { + options.ErrorLogPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "./.glaze-wm/errors.log" + ); + + options.ErrorLogMessageDelegate = (Exception exception) => + { + var serializeOptions = JsonParser.OptionsFactory( + options => options.Converters.Add(new JsonContainerConverter()) + ); + + var stateDump = JsonParser.ToString( + containerService.ContainerTree, + serializeOptions + ); + + // History of latest command invocations. Most recent is first. + var commandHistory = bus.CommandHistory + .Select(command => command.Name) + .Reverse(); + + return $"{DateTime.Now}\n" + + $"{exception}\n" + + $"Command history: {string.Join(", ", commandHistory)} \n" + + $"State dump: {stateDump}\n\n"; + }; + }); + + return services; + } + } +} diff --git a/GlazeWM.Bootstrapper/Resources/MaterialIcons-Regular.ttf b/GlazeWM.App/Resources/MaterialIcons-Regular.ttf similarity index 100% rename from GlazeWM.Bootstrapper/Resources/MaterialIcons-Regular.ttf rename to GlazeWM.App/Resources/MaterialIcons-Regular.ttf diff --git a/GlazeWM.Bootstrapper/Resources/icon.ico b/GlazeWM.App/Resources/icon.ico similarity index 100% rename from GlazeWM.Bootstrapper/Resources/icon.ico rename to GlazeWM.App/Resources/icon.ico diff --git a/GlazeWM.Bootstrapper/Resources/sample-config.yaml b/GlazeWM.App/Resources/sample-config.yaml similarity index 100% rename from GlazeWM.Bootstrapper/Resources/sample-config.yaml rename to GlazeWM.App/Resources/sample-config.yaml diff --git a/GlazeWM.Bar/Components/ComponentPortal.xaml b/GlazeWM.Bar/Components/ComponentPortal.xaml index 484348515..9a6eeae41 100644 --- a/GlazeWM.Bar/Components/ComponentPortal.xaml +++ b/GlazeWM.Bar/Components/ComponentPortal.xaml @@ -65,6 +65,7 @@ + - /// The main entry point for the application. - /// - [STAThread] - private static void Main(string[] args) - { - Debug.WriteLine("Application started."); - - // Prevent multiple app instances using a global UUID mutex. - using var mutex = new Mutex(false, "Global\\" + APP_GUID); - if (!mutex.WaitOne(0, false)) - { - Debug.Write( - "Application is already running. Only one instance of this application is allowed." - ); - return; - } - - var host = CreateHost(args); - ServiceLocator.Provider = host.Services; - - var startup = ServiceLocator.GetRequiredService(); - startup.Run(); - } - - private static IHost CreateHost(string[] args) - { - return Host.CreateDefaultBuilder() - .ConfigureAppConfiguration(appConfig => - { - appConfig.AddCommandLine(args, new Dictionary - { - // Map CLI argument `--config` to `UserConfigPath` configuration key. - {"--config", "UserConfigPath"} - }); - }) - .ConfigureServices((_, services) => - { - services.AddInfrastructureServices(); - services.AddDomainServices(); - services.AddBarServices(); - services.AddSingleton(); - - // Configure exception handler. - services - .AddOptions() - .Configure((options, bus, containerService, jsonService) => - { - options.ErrorLogPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "./.glaze-wm/errors.log" - ); - - options.ErrorLogMessageDelegate = (Exception exception) => - { - var stateDump = jsonService.Serialize( - containerService.ContainerTree, - new List { new JsonContainerConverter() } - ); - - // History of latest command invocations. Most recent is first. - var commandHistory = bus.CommandHistory - .Select(command => command.Name) - .Reverse(); - - return $"{DateTime.Now}\n" - + $"{exception}\n" - + $"Command history: {string.Join(", ", commandHistory)} \n" - + $"State dump: {stateDump}\n\n"; - }; - }); - }) - .ConfigureLogging(builder => - { - builder.ClearProviders(); - builder.AddConsole(options => options.FormatterName = "customFormatter") - .AddConsoleFormatter(); - }) - .Build(); - } - } -} diff --git a/GlazeWM.Bootstrapper/appsettings.json b/GlazeWM.Bootstrapper/appsettings.json deleted file mode 100644 index 12a52ad97..000000000 --- a/GlazeWM.Bootstrapper/appsettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug" - }, - "Console": { - "FormatterOptions": { - "SingleLine": true, - "TimestampFormat": "HH:mm:ss " - } - } - } -} diff --git a/GlazeWM.Domain/Common/WmStartupOptions.cs b/GlazeWM.Domain/Common/WmStartupOptions.cs new file mode 100644 index 000000000..821795be2 --- /dev/null +++ b/GlazeWM.Domain/Common/WmStartupOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace GlazeWM.Domain.Common +{ + [Verb("start", isDefault: true)] + public class WmStartupOptions + { + [Option( + 'c', + "config", + Required = false, + HelpText = "Custom path to user config file." + )] + public string ConfigPath { get; set; } + } +} diff --git a/GlazeWM.Domain/Containers/Container.cs b/GlazeWM.Domain/Containers/Container.cs index 18cf5220a..4a9fa4dd9 100644 --- a/GlazeWM.Domain/Containers/Container.cs +++ b/GlazeWM.Domain/Containers/Container.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using GlazeWM.Infrastructure.Utils; using GlazeWM.Infrastructure.WindowsApi; namespace GlazeWM.Domain.Containers @@ -32,7 +33,7 @@ public abstract class Container /// /// Index of this container in parent's child focus order. /// - public int FocusIndex => Parent.ChildFocusOrder.IndexOf(this); + public int FocusIndex => this is RootContainer ? 0 : Parent.ChildFocusOrder.IndexOf(this); public List SelfAndSiblings => Parent.Children; @@ -43,6 +44,12 @@ public abstract class Container /// public int Index => Parent.Children.IndexOf(this); + /// + /// Friendly name of the derived container type (eg. `workspace`). For CLI and IPC + /// usage. + /// + public string Type => CasingUtil.PascalToSnake(GetType().Name); + /// /// Get the last focused descendant by traversing downwards. /// diff --git a/GlazeWM.Domain/Containers/JsonContainerConverter.cs b/GlazeWM.Domain/Containers/JsonContainerConverter.cs index e1eb09920..91b8a8639 100644 --- a/GlazeWM.Domain/Containers/JsonContainerConverter.cs +++ b/GlazeWM.Domain/Containers/JsonContainerConverter.cs @@ -1,13 +1,10 @@ using System; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -using GlazeWM.Domain.Common.Enums; using GlazeWM.Domain.Monitors; using GlazeWM.Domain.Windows; using GlazeWM.Domain.Workspaces; using GlazeWM.Infrastructure.Serialization; -using GlazeWM.Infrastructure.WindowsApi; namespace GlazeWM.Domain.Containers { @@ -23,163 +20,140 @@ public override Container Read( Type typeToConvert, JsonSerializerOptions options) { - using var jsonDocument = JsonDocument.ParseValue(ref reader); - - return DeserializeContainerJson(jsonDocument.RootElement, options); + throw new NotSupportedException( + $"Deserializing {typeToConvert.Name} from JSON is not supported." + ); } - private static Container DeserializeContainerJson( - JsonElement jsonObject, - JsonSerializerOptions options, - Container parent = null) + public override void Write( + Utf8JsonWriter writer, + Container value, + JsonSerializerOptions options) { - // Get the type of container (eg. "Workspace", "MinimizedWindow"). - var typeDiscriminator = jsonObject.GetProperty("__type").ToString(); + writer.WriteStartObject(); - Container newContainer = typeDiscriminator switch + switch (value) { - "RootContainer" => new RootContainer(), - "Monitor" => new Monitor( - jsonObject.GetProperty("DeviceName").GetString(), - jsonObject.GetProperty("Width").GetInt32(), - jsonObject.GetProperty("Height").GetInt32(), - jsonObject.GetProperty("X").GetInt32(), - jsonObject.GetProperty("Y").GetInt32() - ), - "Workspace" => new Workspace( - jsonObject.GetProperty("Name").GetString(), - jsonObject.GetProperty("Layout").Deserialize() - ), - "SplitContainer" => new SplitContainer - { - Layout = jsonObject.GetProperty("Layout").Deserialize(), - SizePercentage = jsonObject.GetProperty("SizePercentage").GetDouble() - }, - "MinimizedWindow" => new MinimizedWindow( - // TODO: Handle `IntPtr` for 32-bit processes. - new IntPtr(Convert.ToInt64(jsonObject.GetProperty("Handle").GetString(), 16)), - jsonObject.GetProperty("FloatingPlacement").Deserialize(), - jsonObject.GetProperty("BorderDelta").Deserialize(), - jsonObject.GetEnumProperty("PreviousState", options) - ), - "FloatingWindow" => new FloatingWindow( - // TODO: Handle `IntPtr` for 32-bit processes. - new IntPtr(Convert.ToInt64(jsonObject.GetProperty("Handle").GetString(), 16)), - jsonObject.GetProperty("FloatingPlacement").Deserialize(), - jsonObject.GetProperty("BorderDelta").Deserialize() - ), - "TilingWindow" => new TilingWindow( - // TODO: Handle `IntPtr` for 32-bit processes. - new IntPtr(Convert.ToInt64(jsonObject.GetProperty("Handle").GetString(), 16)), - jsonObject.GetProperty("FloatingPlacement").Deserialize(), - jsonObject.GetProperty("BorderDelta").Deserialize(), - jsonObject.GetProperty("SizePercentage").GetDouble() - ), - _ => throw new ArgumentException(null, nameof(jsonObject)), - }; - - newContainer.Parent = parent; - - var children = jsonObject.GetProperty("Children").EnumerateArray(); - newContainer.Children = children - .Select((child) => DeserializeContainerJson(child, options, newContainer)) - .ToList(); - - var focusIndices = - children.Select(child => child.GetProperty("FocusIndex").GetInt32()); - - // Map focus index to the corresponding child container. - newContainer.ChildFocusOrder = focusIndices - .Select(focusIndex => newContainer.Children[focusIndex]) - .ToList(); - - return newContainer; + case Monitor monitor: + WriteMonitorProperties(writer, monitor, options); + break; + case Workspace workspace: + WriteSplitContainerProperties(writer, workspace, options); + WriteWorkspaceProperties(writer, workspace, options); + break; + case SplitContainer splitContainer: + WriteSplitContainerProperties(writer, splitContainer, options); + break; + case MinimizedWindow minimizedWindow: + WriteWindowProperties(writer, minimizedWindow, options); + WriteMinimizedWindowProperties(writer, minimizedWindow, options); + break; + case FloatingWindow floatingWindow: + WriteWindowProperties(writer, floatingWindow, options); + break; + case TilingWindow tilingWindow: + WriteWindowProperties(writer, tilingWindow, options); + WriteTilingWindowProperties(writer, tilingWindow, options); + break; + } + + // The following properties are required for all container types. + WriteCommonProperties(writer, value, options); + + writer.WriteEndObject(); } - public override void Write( + private void WriteCommonProperties( Utf8JsonWriter writer, Container value, JsonSerializerOptions options) { - writer.WriteStartObject(); - writer.WriteNumber("X", value.X); - writer.WriteNumber("Y", value.Y); - writer.WriteNumber("Width", value.Width); - writer.WriteNumber("Height", value.Height); - writer.WriteString("__type", value.GetType().Name); - - // Handle focus index for root container. - var focusIndex = value is RootContainer ? 0 : value.FocusIndex; - writer.WriteNumber("FocusIndex", focusIndex); - - WriteContainerValue(writer, value); + writer.WriteString(JsonParser.ChangeCasing("Id", options), value.Id); + writer.WriteNumber(JsonParser.ChangeCasing("X", options), value.X); + writer.WriteNumber(JsonParser.ChangeCasing("Y", options), value.Y); + writer.WriteNumber(JsonParser.ChangeCasing("Width", options), value.Width); + writer.WriteNumber(JsonParser.ChangeCasing("Height", options), value.Height); + writer.WriteString(JsonParser.ChangeCasing("Type", options), value.Type); + writer.WriteNumber( + JsonParser.ChangeCasing("FocusIndex", options), + value.FocusIndex + ); // Recursively serialize child containers. - writer.WriteStartArray("Children"); + writer.WriteStartArray(JsonParser.ChangeCasing("Children", options)); foreach (var child in value.Children) Write(writer, child, options); writer.WriteEndArray(); - writer.WriteEndObject(); } - private static void WriteContainerValue( + private static void WriteMonitorProperties( Utf8JsonWriter writer, - Container value) + Monitor monitor, + JsonSerializerOptions options) { - switch (value) - { - case Monitor: - var monitor = value as Monitor; - writer.WriteString("DeviceName", monitor.DeviceName); - return; - case Workspace: - var workspace = value as Workspace; - writer.WriteString("Name", workspace.Name); - return; - case SplitContainer: - var splitContainer = value as SplitContainer; - writer.WriteString("Layout", splitContainer.Layout.ToString()); - writer.WriteNumber("SizePercentage", splitContainer.SizePercentage); - return; - case MinimizedWindow: - var minimizedWindow = value as MinimizedWindow; - writer.WriteNumber( - "Handle", - minimizedWindow.Handle.ToInt64() - ); - writer.WritePropertyName("FloatingPlacement"); - JsonSerializer.Serialize(writer, minimizedWindow.FloatingPlacement); - writer.WritePropertyName("BorderDelta"); - JsonSerializer.Serialize(writer, minimizedWindow.BorderDelta); - writer.WriteString("PreviousState", minimizedWindow.PreviousState.ToString()); - return; - case FloatingWindow: - var floatingWindow = value as FloatingWindow; - writer.WriteNumber( - "Handle", - floatingWindow.Handle.ToInt64() - ); - writer.WritePropertyName("FloatingPlacement"); - JsonSerializer.Serialize(writer, floatingWindow.FloatingPlacement); - writer.WritePropertyName("BorderDelta"); - JsonSerializer.Serialize(writer, floatingWindow.BorderDelta); - return; - case TilingWindow: - var tilingWindow = value as TilingWindow; - writer.WriteNumber( - "Handle", - tilingWindow.Handle.ToInt64() - ); - writer.WritePropertyName("FloatingPlacement"); - JsonSerializer.Serialize(writer, tilingWindow.FloatingPlacement); - writer.WritePropertyName("BorderDelta"); - JsonSerializer.Serialize(writer, tilingWindow.BorderDelta); - writer.WriteNumber("SizePercentage", tilingWindow.SizePercentage); - return; - default: - return; - } + writer.WriteString( + JsonParser.ChangeCasing("DeviceName", options), + monitor.DeviceName + ); + } + + private static void WriteWorkspaceProperties( + Utf8JsonWriter writer, + Workspace workspace, + JsonSerializerOptions options) + { + writer.WriteString(JsonParser.ChangeCasing("Name", options), workspace.Name); + } + + private static void WriteSplitContainerProperties( + Utf8JsonWriter writer, + SplitContainer splitContainer, + JsonSerializerOptions options) + { + writer.WritePropertyName(JsonParser.ChangeCasing("Layout", options)); + JsonSerializer.Serialize(writer, splitContainer.Layout, options); + + writer.WriteNumber( + JsonParser.ChangeCasing("SizePercentage", options), + splitContainer.SizePercentage + ); + } + + private static void WriteWindowProperties( + Utf8JsonWriter writer, + Window window, + JsonSerializerOptions options) + { + writer.WritePropertyName(JsonParser.ChangeCasing("FloatingPlacement", options)); + JsonSerializer.Serialize(writer, window.FloatingPlacement, options); + writer.WritePropertyName(JsonParser.ChangeCasing("BorderDelta", options)); + JsonSerializer.Serialize(writer, window.BorderDelta, options); + writer.WriteNumber( + JsonParser.ChangeCasing("Handle", options), + window.Handle.ToInt64() + ); + } + + private static void WriteMinimizedWindowProperties( + Utf8JsonWriter writer, + MinimizedWindow minimizedWindow, + JsonSerializerOptions options) + { + + writer.WritePropertyName(JsonParser.ChangeCasing("PreviousState", options)); + JsonSerializer.Serialize(writer, minimizedWindow.PreviousState, options); + } + + private static void WriteTilingWindowProperties( + Utf8JsonWriter writer, + TilingWindow tilingWindow, + JsonSerializerOptions options) + { + writer.WriteNumber( + JsonParser.ChangeCasing("SizePercentage", options), + tilingWindow.SizePercentage + ); } } } diff --git a/GlazeWM.Domain/Containers/RootContainer.cs b/GlazeWM.Domain/Containers/RootContainer.cs index 10b21bd2e..1076dc0c1 100644 --- a/GlazeWM.Domain/Containers/RootContainer.cs +++ b/GlazeWM.Domain/Containers/RootContainer.cs @@ -4,6 +4,6 @@ namespace GlazeWM.Domain.Containers { public sealed class RootContainer : Container { - public override string Id { get; init; } = $"ROOT/{new Guid()}"; + public override string Id { get; init; } = $"root_container/{new Guid()}"; } } diff --git a/GlazeWM.Domain/Containers/SplitContainer.cs b/GlazeWM.Domain/Containers/SplitContainer.cs index b7a86bb40..b08d43922 100644 --- a/GlazeWM.Domain/Containers/SplitContainer.cs +++ b/GlazeWM.Domain/Containers/SplitContainer.cs @@ -6,7 +6,7 @@ namespace GlazeWM.Domain.Containers { public class SplitContainer : Container, IResizable { - public override string Id { get; init; } = $"SPLIT/{new Guid()}"; + public override string Id { get; init; } = $"split_container/{new Guid()}"; public Layout Layout { get; set; } = Layout.Horizontal; public double SizePercentage { get; set; } = 1; diff --git a/GlazeWM.Domain/Monitors/Monitor.cs b/GlazeWM.Domain/Monitors/Monitor.cs index 243b1e126..b0398d28f 100644 --- a/GlazeWM.Domain/Monitors/Monitor.cs +++ b/GlazeWM.Domain/Monitors/Monitor.cs @@ -23,7 +23,7 @@ public Monitor( int x, int y) { - Id = $"MONITOR/{deviceName}"; + Id = $"monitor/{deviceName}"; DeviceName = deviceName; Width = width; Height = height; diff --git a/GlazeWM.Domain/UserConfigs/CommandHandlers/EvaluateUserConfigHandler.cs b/GlazeWM.Domain/UserConfigs/CommandHandlers/EvaluateUserConfigHandler.cs index 67ac13152..0276c69f1 100644 --- a/GlazeWM.Domain/UserConfigs/CommandHandlers/EvaluateUserConfigHandler.cs +++ b/GlazeWM.Domain/UserConfigs/CommandHandlers/EvaluateUserConfigHandler.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Reflection; using System.Text.Json; -using System.Text.Json.Serialization; using System.Windows.Forms; using GlazeWM.Domain.UserConfigs.Commands; using GlazeWM.Infrastructure.Bussing; @@ -18,7 +17,6 @@ internal sealed class EvaluateUserConfigHandler : ICommandHandler @@ -34,12 +32,10 @@ internal sealed class EvaluateUserConfigHandler : ICommandHandler( - input, - new List() { new BarComponentConfigConverter() } + var deserializeOptions = JsonParser.OptionsFactory((options) => + options.Converters.Add(new BarComponentConfigConverter()) ); + + return YamlParser.ToInstance(input, deserializeOptions); } catch (Exception exception) { diff --git a/GlazeWM.Domain/UserConfigs/CommandParsingService.cs b/GlazeWM.Domain/UserConfigs/CommandParsingService.cs index eb5694144..7793da329 100644 --- a/GlazeWM.Domain/UserConfigs/CommandParsingService.cs +++ b/GlazeWM.Domain/UserConfigs/CommandParsingService.cs @@ -29,29 +29,35 @@ public CommandParsingService(UserConfigService userConfigService) public static string FormatCommand(string commandString) { - var trimmedCommandString = commandString.Trim().ToLowerInvariant(); + // Remove leading/trailing whitespace (if present). + commandString = commandString.Trim(); - var multipleSpacesRegex = new Regex(@"\s+"); - var formattedCommandString = multipleSpacesRegex.Replace(trimmedCommandString, " "); + // Remove leading/trailing single quotes. + if (commandString.StartsWith("'") && commandString.EndsWith("'")) + commandString = commandString.Trim('\''); + + // Remove leading/trailing double quotes. + if (commandString.StartsWith("\"") && commandString.EndsWith("\"")) + commandString = commandString.Trim('"'); var caseSensitiveCommandRegex = new List { new Regex("^(exec).*", RegexOptions.IgnoreCase), }; - // Some commands are partially case-sensitive (eg. `exec ...`). To handle such cases, only - // format part of the command string to be lowercase. + // Some commands are partially case-sensitive (eg. `exec ...`). To handle such + // cases, only format part of the command string to be lowercase. foreach (var regex in caseSensitiveCommandRegex) { - if (regex.IsMatch(formattedCommandString)) + if (regex.IsMatch(commandString)) { - return regex.Replace(formattedCommandString, (Match match) => + return regex.Replace(commandString, (Match match) => match.Value.ToLowerInvariant() ); } } - return formattedCommandString.ToLowerInvariant(); + return commandString.ToLowerInvariant(); } public void ValidateCommand(string commandString) diff --git a/GlazeWM.Domain/UserConfigs/UserConfigService.cs b/GlazeWM.Domain/UserConfigs/UserConfigService.cs index f1db8b014..82dbd58f1 100644 --- a/GlazeWM.Domain/UserConfigs/UserConfigService.cs +++ b/GlazeWM.Domain/UserConfigs/UserConfigService.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using GlazeWM.Domain.Common; using GlazeWM.Domain.Monitors; using GlazeWM.Domain.Windows; -using Microsoft.Extensions.Configuration; namespace GlazeWM.Domain.UserConfigs { @@ -27,18 +27,18 @@ public class UserConfigService /// Path to the user's config file. /// public string UserConfigPath => - _configuration.GetValue("UserConfigPath") ?? _defaultUserConfigPath; + _startupOptions.ConfigPath ?? _defaultUserConfigPath; private readonly string _defaultUserConfigPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "./.glaze-wm/config.yaml" ); - private readonly IConfiguration _configuration; + private readonly WmStartupOptions _startupOptions; - public UserConfigService(IConfiguration configuration) + public UserConfigService(WmStartupOptions startupOptions) { - _configuration = configuration; + _startupOptions = startupOptions; } public readonly List DefaultWindowRules = GetDefaultWindowRules(); diff --git a/GlazeWM.Domain/Windows/Window.cs b/GlazeWM.Domain/Windows/Window.cs index ddbc9a133..b70245046 100644 --- a/GlazeWM.Domain/Windows/Window.cs +++ b/GlazeWM.Domain/Windows/Window.cs @@ -31,7 +31,7 @@ public abstract class Window : Container protected Window(IntPtr handle, Rect floatingPlacement, RectDelta borderDelta) { - Id = $"WINDOW/{handle:x}"; + Id = $"window/{handle}"; Handle = handle; FloatingPlacement = floatingPlacement; BorderDelta = borderDelta; diff --git a/GlazeWM.Domain/Workspaces/Workspace.cs b/GlazeWM.Domain/Workspaces/Workspace.cs index 9913aad3b..376fe240d 100644 --- a/GlazeWM.Domain/Workspaces/Workspace.cs +++ b/GlazeWM.Domain/Workspaces/Workspace.cs @@ -93,7 +93,7 @@ public override int Y public Workspace(string name, Layout layout) { Layout = layout; - Id = $"WORKSPACE/{name}"; + Id = $"workspace/{name}"; Name = name; } } diff --git a/GlazeWM.Infrastructure/Bussing/Event.cs b/GlazeWM.Infrastructure/Bussing/Event.cs index 6bbe6af43..225a23613 100644 --- a/GlazeWM.Infrastructure/Bussing/Event.cs +++ b/GlazeWM.Infrastructure/Bussing/Event.cs @@ -1,7 +1,21 @@ +using GlazeWM.Infrastructure.Utils; + namespace GlazeWM.Infrastructure.Bussing { public class Event { public string Name => GetType().Name; + + /// + /// Name used to subscribe to for IPC and CLI usage. + /// + public string FriendlyName + { + get + { + var shortName = Name.Replace("Event", ""); + return CasingUtil.PascalToSnake(shortName); + } + } } } diff --git a/GlazeWM.Infrastructure/Common/ExitCode.cs b/GlazeWM.Infrastructure/Common/ExitCode.cs new file mode 100644 index 000000000..44f4ff5e9 --- /dev/null +++ b/GlazeWM.Infrastructure/Common/ExitCode.cs @@ -0,0 +1,8 @@ +namespace GlazeWM.Infrastructure.Common +{ + public enum ExitCode + { + Success = 0, + Error = 1, + } +} diff --git a/GlazeWM.Infrastructure/DependencyInjection.cs b/GlazeWM.Infrastructure/DependencyInjection.cs index d46cd3bc3..26f09407b 100644 --- a/GlazeWM.Infrastructure/DependencyInjection.cs +++ b/GlazeWM.Infrastructure/DependencyInjection.cs @@ -1,7 +1,6 @@ using GlazeWM.Infrastructure.Bussing; using GlazeWM.Infrastructure.Common.CommandHandlers; using GlazeWM.Infrastructure.Common.Commands; -using GlazeWM.Infrastructure.Serialization; using GlazeWM.Infrastructure.WindowsApi; using Microsoft.Extensions.DependencyInjection; @@ -14,8 +13,6 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/GlazeWM.Infrastructure/GlazeWM.Infrastructure.csproj b/GlazeWM.Infrastructure/GlazeWM.Infrastructure.csproj index b33fcb3d0..a0ec5012a 100644 --- a/GlazeWM.Infrastructure/GlazeWM.Infrastructure.csproj +++ b/GlazeWM.Infrastructure/GlazeWM.Infrastructure.csproj @@ -8,8 +8,8 @@ + Include="CommandLineParser" + Version="2.9.1" /> @@ -49,8 +49,14 @@ + + diff --git a/GlazeWM.Infrastructure/Logging/LogFormatter.cs b/GlazeWM.Infrastructure/Logging/LogFormatter.cs index 3579c9f6a..20777bae4 100644 --- a/GlazeWM.Infrastructure/Logging/LogFormatter.cs +++ b/GlazeWM.Infrastructure/Logging/LogFormatter.cs @@ -10,10 +10,12 @@ namespace GlazeWM.Infrastructure.Logging { public sealed class LogFormatter : ConsoleFormatter, IDisposable { + public new const string Name = "customFormatter"; + private readonly IDisposable _optionsReloadToken; private ConsoleFormatterOptions _formatterOptions; - public LogFormatter(IOptionsMonitor options) : base("customFormatter") + public LogFormatter(IOptionsMonitor options) : base(Name) { (_optionsReloadToken, _formatterOptions) = (options.OnChange(ReloadLoggerOptions), options.CurrentValue); diff --git a/GlazeWM.Infrastructure/Serialization/JsonElementExtensions.cs b/GlazeWM.Infrastructure/Serialization/JsonElementExtensions.cs deleted file mode 100644 index 0642828ed..000000000 --- a/GlazeWM.Infrastructure/Serialization/JsonElementExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Text.Json; - -namespace GlazeWM.Infrastructure.Serialization -{ - public static class JsonElementExtensions - { - /// - /// Get JSON property where property name has been converted according to naming policy. - /// - private static JsonElement GetConvertedProperty( - this JsonElement element, - string propertyName, - JsonSerializerOptions options) - { - // Convert name according to given naming policy. - var convertedName = - options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName; - - return element.GetProperty(convertedName); - } - - public static string GetStringProperty( - this JsonElement element, - string propertyName, - JsonSerializerOptions options) - { - return element.GetConvertedProperty(propertyName, options).GetString(); - } - - public static int GetInt64Property( - this JsonElement element, - string propertyName, - JsonSerializerOptions options) - { - return element.GetConvertedProperty(propertyName, options).GetInt32(); - } - - public static double GetDoubleProperty( - this JsonElement element, - string propertyName, - JsonSerializerOptions options) - { - return element.GetConvertedProperty(propertyName, options).GetDouble(); - } - - public static T GetEnumProperty( - this JsonElement element, - string propertyName, - JsonSerializerOptions _) where T : struct, Enum - { - var isEnum = Enum.TryParse(element.GetProperty(propertyName).GetString(), out T value); - - if (!isEnum) - throw new Exception(); - - return value; - } - } -} diff --git a/GlazeWM.Infrastructure/Serialization/JsonParser.cs b/GlazeWM.Infrastructure/Serialization/JsonParser.cs new file mode 100644 index 000000000..4d5e79ad0 --- /dev/null +++ b/GlazeWM.Infrastructure/Serialization/JsonParser.cs @@ -0,0 +1,62 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GlazeWM.Infrastructure.Serialization +{ + public static class JsonParser + { + private static readonly JsonSerializerOptions _defaultOptions = new() + { + // TODO: Use built-in snake case policy once support is added. + // Ref: https://github.com/dotnet/runtime/issues/782 + PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + IncludeFields = true, + Converters = { + // Enable strings to be mapped to a C# enum (eg. `BarPosition` enum). + new JsonStringEnumConverter(SnakeCaseNamingPolicy.Instance), + // Enable boolean strings to be mapped to a C# bool (eg. `"true"` -> `true`). + new JsonStringBoolConverter(), + } + }; + + public static string ToString(T value) + { + return JsonSerializer.Serialize(value, _defaultOptions); + } + + public static string ToString(T value, JsonSerializerOptions options) + { + return JsonSerializer.Serialize(value, options); + } + + public static T ToInstance(string json) + { + return JsonSerializer.Deserialize(json, _defaultOptions); + } + + public static T ToInstance(string json, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(json, options); + } + + public static JsonSerializerOptions OptionsFactory( + Action callback) + { + var options = new JsonSerializerOptions(_defaultOptions); + callback(options); + return options; + } + + /// + /// Convert property name according to naming policy. + /// + public static string ChangeCasing( + string propertyName, + JsonSerializerOptions options) + { + return options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName; + } + } +} diff --git a/GlazeWM.Infrastructure/Serialization/JsonService.cs b/GlazeWM.Infrastructure/Serialization/JsonService.cs deleted file mode 100644 index b576f5bc6..000000000 --- a/GlazeWM.Infrastructure/Serialization/JsonService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace GlazeWM.Infrastructure.Serialization -{ - public class JsonService - { - private readonly JsonSerializerOptions _jsonSerializerOptions = new() - { - // TODO: Use built-in snake case policy once support is added. - // Ref: https://github.com/dotnet/runtime/issues/782 - PropertyNamingPolicy = new SnakeCaseNamingPolicy(), - NumberHandling = JsonNumberHandling.AllowReadingFromString, - IncludeFields = true, - Converters = { - // Enable strings to be mapped to a C# enum (eg. `BarPosition` enum). - new JsonStringEnumConverter(), - // Enable boolean strings to be mapped to a C# bool (eg. `"true"` -> `true`). - new JsonStringBoolConverter(), - } - }; - - public string Serialize(T value, List converters) - { - var jsonSerializerOptions = GetJsonSerializerOptions(converters); - return JsonSerializer.Serialize(value, jsonSerializerOptions); - } - - public T Deserialize(string json, List converters) - { - var jsonDeserializerOptions = GetJsonSerializerOptions(converters); - return JsonSerializer.Deserialize(json, jsonDeserializerOptions); - } - - private JsonSerializerOptions GetJsonSerializerOptions(List converters) - { - var jsonSerializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - - foreach (var converter in converters) - jsonSerializerOptions.Converters.Add(converter); - - return jsonSerializerOptions; - } - } -} diff --git a/GlazeWM.Infrastructure/Serialization/YamlService.cs b/GlazeWM.Infrastructure/Serialization/YamlParser.cs similarity index 51% rename from GlazeWM.Infrastructure/Serialization/YamlService.cs rename to GlazeWM.Infrastructure/Serialization/YamlParser.cs index 0edc4c4de..83962c86e 100644 --- a/GlazeWM.Infrastructure/Serialization/YamlService.cs +++ b/GlazeWM.Infrastructure/Serialization/YamlParser.cs @@ -1,36 +1,27 @@ -using System.Collections.Generic; using System.IO; -using System.Text.Json.Serialization; +using System.Text.Json; using YamlDotNet.Core; using YamlDotNet.Serialization; namespace GlazeWM.Infrastructure.Serialization { - public class YamlService + public static class YamlParser { - private readonly JsonService _jsonService; - private readonly IDeserializer _yamlDeserializer = new DeserializerBuilder().Build(); - - public YamlService(JsonService jsonService) - { - _jsonService = jsonService; - } - /// /// The YAML deserialization library doesn't have support for polymorphic objects. Because of /// this, the YAML is first converted into JSON and then deserialized via `System.Text.Json`. /// - public T Deserialize(string input, List converters) + public static T ToInstance(string input, JsonSerializerOptions deserializeOptions) { // Deserializes YAML into key-value pairs (ie. not an object of type `T`). Merging parser is // used to enable the use of merge keys. var reader = new MergingParser(new Parser(new StringReader(input))); - var yamlObject = _yamlDeserializer.Deserialize(reader); + var yamlObject = new DeserializerBuilder().Build().Deserialize(reader); // Convert key-value pairs into a JSON string. - var jsonString = _jsonService.Serialize(yamlObject, new List()); + var jsonString = JsonParser.ToString(yamlObject); - return _jsonService.Deserialize(jsonString, converters); + return JsonParser.ToInstance(jsonString, deserializeOptions); } } } diff --git a/GlazeWM.Infrastructure/ServiceLocator.cs b/GlazeWM.Infrastructure/ServiceLocator.cs index 9530bb240..a845ab154 100644 --- a/GlazeWM.Infrastructure/ServiceLocator.cs +++ b/GlazeWM.Infrastructure/ServiceLocator.cs @@ -22,5 +22,20 @@ public static IEnumerable GetServices(Type type) { return Provider.GetServices(type); } + + public static (T1, T2) GetRequiredServices() + { + var service1 = Provider.GetRequiredService(); + var service2 = Provider.GetRequiredService(); + return (service1, service2); + } + + public static (T1, T2, T3) GetRequiredServices() + { + var service1 = Provider.GetRequiredService(); + var service2 = Provider.GetRequiredService(); + var service3 = Provider.GetRequiredService(); + return (service1, service2, service3); + } } } diff --git a/GlazeWM.Infrastructure/Utils/CasingUtil.cs b/GlazeWM.Infrastructure/Utils/CasingUtil.cs new file mode 100644 index 000000000..6b659f63d --- /dev/null +++ b/GlazeWM.Infrastructure/Utils/CasingUtil.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace GlazeWM.Infrastructure.Utils +{ + public static class CasingUtil + { + private static readonly Regex _regex = new("([a-z])([A-Z])"); + + /// + /// Converts pascal case to snake case (eg. `FirstName` -> `first_name`). + /// + public static string PascalToSnake(string input) + { + return _regex.Replace(input, "$1_$2").ToLower( + CultureInfo.InvariantCulture + ); + } + } +} diff --git a/GlazeWM.Infrastructure/Utils/DictionaryExtensions.cs b/GlazeWM.Infrastructure/Utils/DictionaryExtensions.cs new file mode 100644 index 000000000..4610309fa --- /dev/null +++ b/GlazeWM.Infrastructure/Utils/DictionaryExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace GlazeWM.Infrastructure.Utils +{ + public static class DictionaryExtensions + { + public static TValue GetValueOrDefault( + this Dictionary dictionary, + TKey key, + TValue defaultValue) + { + TValue val; + + if (dictionary.TryGetValue(key, out val)) + return val ?? defaultValue; + + return defaultValue; + } + + public static TValue GetValueOrThrow( + this Dictionary dictionary, + TKey key) + { + TValue val; + + if (!dictionary.TryGetValue(key, out val)) + throw new Exception($"Dictionary value does not exist at key '{key}'."); + + return val; + } + } +} diff --git a/GlazeWM.Infrastructure/Utils/ThreadUtils.cs b/GlazeWM.Infrastructure/Utils/ThreadUtils.cs new file mode 100644 index 000000000..bb5d01221 --- /dev/null +++ b/GlazeWM.Infrastructure/Utils/ThreadUtils.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; + +namespace GlazeWM.Infrastructure.Utils +{ + public static class ThreadUtils + { + public static void CreateSTA(string threadName, Action threadAction) + { + var thread = new Thread(() => threadAction()) + { + Name = threadName + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + public static void Create(string threadName, Action threadAction) + { + var thread = new Thread(() => threadAction()) + { + Name = threadName + }; + + thread.Start(); + } + } +} diff --git a/GlazeWM.Infrastructure/Utils/WebsocketClient.cs b/GlazeWM.Infrastructure/Utils/WebsocketClient.cs new file mode 100644 index 000000000..f41b249b9 --- /dev/null +++ b/GlazeWM.Infrastructure/Utils/WebsocketClient.cs @@ -0,0 +1,38 @@ +using System; +using System.Reactive.Subjects; +using System.Text; +using NetCoreServer; + +namespace GlazeWM.Infrastructure.Utils +{ + public class WebsocketClient : WsClient + { + /// + /// Messages received from websocket server. + /// + public readonly ReplaySubject Messages = new(); + + public WebsocketClient(int port) : base("127.0.0.1", port) { } + + public WebsocketClient(string address, int port) : base(address, port) { } + + public override void OnWsConnecting(HttpRequest request) + { + request.SetBegin("GET", "/") + .SetHeader("Host", "localhost") + .SetHeader("Origin", "http://localhost") + .SetHeader("Upgrade", "websocket") + .SetHeader("Connection", "Upgrade") + .SetHeader("Sec-WebSocket-Key", Convert.ToBase64String(WsNonce)) + .SetHeader("Sec-WebSocket-Protocol", "chat, superchat") + .SetHeader("Sec-WebSocket-Version", "13") + .SetBody(); + } + + public override void OnWsReceived(byte[] buffer, long offset, long size) + { + var message = Encoding.UTF8.GetString(buffer, (int)offset, (int)size); + Messages.OnNext(message); + } + } +} diff --git a/GlazeWM.sln b/GlazeWM.sln index 19d1e3927..0c5a557a7 100644 --- a/GlazeWM.sln +++ b/GlazeWM.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32922.545 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlazeWM.Domain", "GlazeWM.Domain\GlazeWM.Domain.csproj", "{D7741551-9A1D-4357-9571-991C6FDB1906}" EndProject @@ -9,40 +8,58 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlazeWM.Bar", "GlazeWM.Bar\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlazeWM.Infrastructure", "GlazeWM.Infrastructure\GlazeWM.Infrastructure.csproj", "{9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlazeWM.Bootstrapper", "GlazeWM.Bootstrapper\GlazeWM.Bootstrapper.csproj", "{86A11BD4-B17A-4422-9D3A-D2516A730BFE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlazeWM.App", "GlazeWM.App\GlazeWM.App.csproj", "{86A11BD4-B17A-4422-9D3A-D2516A730BFE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlazeWM.App.IpcServer", "GlazeWM.App.IpcServer\GlazeWM.App.IpcServer.csproj", "{BBD11CB5-FDF2-4BD5-B3F6-8B75BC3AFD79}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{877CCE5C-94A9-4CE4-8A29-164179ADE9D3}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GlazeWM.App.WindowManager", "GlazeWM.App.WindowManager\GlazeWM.App.WindowManager.csproj", "{D160D151-C030-4A0E-A805-6E28B533F03F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GlazeWM.App.Cli", "GlazeWM.App.Cli\GlazeWM.App.Cli.csproj", "{BC6D71E7-8D01-4627-A3C9-2B6B9186A36E}" EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D7741551-9A1D-4357-9571-991C6FDB1906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D7741551-9A1D-4357-9571-991C6FDB1906}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D7741551-9A1D-4357-9571-991C6FDB1906}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D7741551-9A1D-4357-9571-991C6FDB1906}.Release|Any CPU.Build.0 = Release|Any CPU - {F038C06D-2085-40EA-AEE9-A517231656FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F038C06D-2085-40EA-AEE9-A517231656FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F038C06D-2085-40EA-AEE9-A517231656FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F038C06D-2085-40EA-AEE9-A517231656FC}.Release|Any CPU.Build.0 = Release|Any CPU - {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Release|Any CPU.Build.0 = Release|Any CPU - {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {90AFACCD-B199-4A1E-B14E-5DA783E09D17} - EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D7741551-9A1D-4357-9571-991C6FDB1906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7741551-9A1D-4357-9571-991C6FDB1906}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7741551-9A1D-4357-9571-991C6FDB1906}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7741551-9A1D-4357-9571-991C6FDB1906}.Release|Any CPU.Build.0 = Release|Any CPU + {F038C06D-2085-40EA-AEE9-A517231656FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F038C06D-2085-40EA-AEE9-A517231656FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F038C06D-2085-40EA-AEE9-A517231656FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F038C06D-2085-40EA-AEE9-A517231656FC}.Release|Any CPU.Build.0 = Release|Any CPU + {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FA1F058-7ED2-4BC3-B3BC-C8C43B3C62C5}.Release|Any CPU.Build.0 = Release|Any CPU + {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86A11BD4-B17A-4422-9D3A-D2516A730BFE}.Release|Any CPU.Build.0 = Release|Any CPU + {BBD11CB5-FDF2-4BD5-B3F6-8B75BC3AFD79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBD11CB5-FDF2-4BD5-B3F6-8B75BC3AFD79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBD11CB5-FDF2-4BD5-B3F6-8B75BC3AFD79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBD11CB5-FDF2-4BD5-B3F6-8B75BC3AFD79}.Release|Any CPU.Build.0 = Release|Any CPU + {D160D151-C030-4A0E-A805-6E28B533F03F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D160D151-C030-4A0E-A805-6E28B533F03F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D160D151-C030-4A0E-A805-6E28B533F03F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D160D151-C030-4A0E-A805-6E28B533F03F}.Release|Any CPU.Build.0 = Release|Any CPU + {BC6D71E7-8D01-4627-A3C9-2B6B9186A36E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC6D71E7-8D01-4627-A3C9-2B6B9186A36E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC6D71E7-8D01-4627-A3C9-2B6B9186A36E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC6D71E7-8D01-4627-A3C9-2B6B9186A36E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {90AFACCD-B199-4A1E-B14E-5DA783E09D17} + EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 032e8f682..cdc8a1ac2 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ scoop install glazewm Alternatively, to build from source, use the following .NET CLI command: ``` -dotnet publish ./GlazeWM.Bootstrapper/GlazeWM.Bootstrapper.csproj --configuration=Release --runtime=win-x64 --output=. --self-contained -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true +dotnet publish ./GlazeWM.App/GlazeWM.App.csproj --configuration=Release --runtime=win-x64 --output=. --self-contained -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true ``` To build for other runtimes than Windows x64, see [here](https://docs.microsoft.com/en-us/dotnet/core/rid-catalog#windows-rids).