diff --git a/build.cake b/build.cake index 281d18ff..9cc07291 100644 --- a/build.cake +++ b/build.cake @@ -3,7 +3,7 @@ var configuration = Argument("configuration", "Release"); var output = Argument("output", "artifacts"); -var version = Argument("version", "v0.10"); +var version = Argument("version", "v0.11"); var sln = "windows-terminal-quake.sln"; var bin = "./windows-terminal-quake/bin"; diff --git a/windows-terminal-quake/Native/TerminalProcess.cs b/windows-terminal-quake/Native/TerminalProcess.cs index 7326f473..8bfb2673 100644 --- a/windows-terminal-quake/Native/TerminalProcess.cs +++ b/windows-terminal-quake/Native/TerminalProcess.cs @@ -97,6 +97,10 @@ private static Process GetOrCreate() // Make sure the process name equals "WindowsTerminal", otherwise WT might still be starting if (process.ProcessName != "WindowsTerminal") throw new Exception("Process name is not 'WindowsTerminal' yet."); + // We need a proper window title before we can continue + if (process.MainWindowTitle == "") + throw new Exception($"Process still has temporary '' window title."); + // This is a way-too-specific check to further ensure the WT process is ready if (process.MainWindowTitle == "DesktopWindowXamlSource") throw new Exception($"Process still has temporary 'DesktopWindowXamlSource' window title."); diff --git a/windows-terminal-quake/Native/TopMostWindow.cs b/windows-terminal-quake/Native/TopMostWindow.cs new file mode 100644 index 00000000..103b93b6 --- /dev/null +++ b/windows-terminal-quake/Native/TopMostWindow.cs @@ -0,0 +1,26 @@ +using Polly; +using Polly.Retry; +using System; +using System.Diagnostics; + +namespace WindowsTerminalQuake.Native +{ + public static class TopMostWindow + { + private static readonly RetryPolicy Retry = Policy + .Handle() + .WaitAndRetry(new[] { TimeSpan.FromMilliseconds(250), TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1) }) + ; + + public static void SetTopMost(Process process) + { + Retry.Execute(() => + { + if (process.MainWindowHandle == IntPtr.Zero) throw new Exception("Process handle zero"); + + var isSet = User32.SetWindowPos(process.MainWindowHandle, User32.HWND_TOPMOST, 0, 0, 0, 0, User32.TOPMOST_FLAGS); + if (!isSet) throw new Exception("Could not set window top most"); + }); + } + } +} \ No newline at end of file diff --git a/windows-terminal-quake/Native/User32.cs b/windows-terminal-quake/Native/User32.cs index 966dc752..4f033668 100644 --- a/windows-terminal-quake/Native/User32.cs +++ b/windows-terminal-quake/Native/User32.cs @@ -29,6 +29,10 @@ public static class User32 [DllImport("user32.dll")] public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + public struct Rect { public int Left { get; set; } @@ -45,5 +49,10 @@ public struct Rect public const int WS_EX_APPWINDOW = 0x00040000; public const int WS_EX_LAYERED = 0x80000; public const int WS_EX_TOOLWINDOW = 0x00000080; + + public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + public const UInt32 SWP_NOSIZE = 0x0001; + public const UInt32 SWP_NOMOVE = 0x0002; + public const UInt32 TOPMOST_FLAGS = SWP_NOMOVE | SWP_NOSIZE; } } \ No newline at end of file diff --git a/windows-terminal-quake/Settings.cs b/windows-terminal-quake/Settings.cs index eb4f0bdf..d738fd82 100644 --- a/windows-terminal-quake/Settings.cs +++ b/windows-terminal-quake/Settings.cs @@ -46,19 +46,29 @@ static Settings() _fsWatchers = PathsToSettings .Select(path => { - Log.Information($"Watching settings file '{path}' for changes"); - var fsWatcher = new FileSystemWatcher(Path.GetDirectoryName(path), Path.GetFileName(path)); - - fsWatcher.Changed += (s, a) => + try { - Log.Information($"Settings file '{a.FullPath}' changed"); - Reload(true); - }; + Log.Information($"Watching settings file '{path}' for changes"); + var fsWatcher = new FileSystemWatcher(Path.GetDirectoryName(path), Path.GetFileName(path)); - fsWatcher.EnableRaisingEvents = true; + fsWatcher.Changed += (s, a) => + { + Log.Information($"Settings file '{a.FullPath}' changed"); + Reload(true); + }; - return fsWatcher; + fsWatcher.EnableRaisingEvents = true; + + return fsWatcher; + } + catch (Exception ex) + { + Log.Error(ex, $"Could not load settings file at location '{path}': {ex.Message}"); + return null; + } }) + .Where(s => s != null) + .Select(s => s!) .ToList() ; @@ -130,6 +140,8 @@ public class SettingsDto public bool Logging { get; set; } = false; public bool HideOnFocusLost { get; set; } = true; + + public bool AlwaysOnTop { get; set; } } public class Hotkey diff --git a/windows-terminal-quake/Toggler.cs b/windows-terminal-quake/Toggler.cs index 803de3ef..02e91004 100644 --- a/windows-terminal-quake/Toggler.cs +++ b/windows-terminal-quake/Toggler.cs @@ -18,6 +18,9 @@ public class Toggler : IDisposable public Toggler() { + // Always on top + if (Settings.Instance.AlwaysOnTop) TopMostWindow.SetTopMost(_process); + // Hide from taskbar User32.SetWindowLong(_process.MainWindowHandle, User32.GWL_EX_STYLE, (User32.GetWindowLong(_process.MainWindowHandle, User32.GWL_EX_STYLE) | User32.WS_EX_TOOLWINDOW) & ~User32.WS_EX_APPWINDOW); diff --git a/windows-terminal-quake/windows-terminal-quake.json b/windows-terminal-quake/windows-terminal-quake.json index d610399e..e9941095 100644 --- a/windows-terminal-quake/windows-terminal-quake.json +++ b/windows-terminal-quake/windows-terminal-quake.json @@ -22,7 +22,7 @@ // How long the toggle up/down takes in milliseconds. "ToggleDurationMs": 250, - // How much room to leave between the top of the terminal and the top of the screen + // How much room to leave between the top of the terminal and the top of the screen. "VerticalOffset": 0, // How far the terminal should come down, in percentage (eg. 50 = half way, 100 = full screen). @@ -38,7 +38,10 @@ "HorizontalScreenCoverage": 100, // When clicking or alt-tabbing away to another app, the terminal will automatically (and instantly) hide. - "HideOnFocusLost": true + "HideOnFocusLost": true, + + // Whether to keep the terminal window always on top (requires restart). + "AlwaysOnTop": false // # HotKeys // ## Modifiers