From 45b9d70c901ac7c8761bf4c80a33da98b7724e7d Mon Sep 17 00:00:00 2001 From: whitlocktech Date: Sun, 26 Oct 2025 10:40:21 -0500 Subject: [PATCH] initial --- .gitignore | 125 +++++++++++++++++++++++++ App.xaml | 8 ++ App.xaml.cs | 55 +++++++++++ AutoStart.cs | 85 +++++++++++++++++ Bedtimer.csproj | 12 +++ Bedtimer.sln | 31 +++++++ Config.cs | 121 ++++++++++++++++++++++++ MainWindow.xaml | 43 +++++++++ MainWindow.xaml.cs | 64 +++++++++++++ OverlayManager.cs | 73 +++++++++++++++ OverlayWindow.xaml | 15 +++ OverlayWindow.xaml.cs | 155 +++++++++++++++++++++++++++++++ State.cs | 49 ++++++++++ TrayHost.cs | 207 ++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1043 insertions(+) create mode 100644 .gitignore create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 AutoStart.cs create mode 100644 Bedtimer.csproj create mode 100644 Bedtimer.sln create mode 100644 Config.cs create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs create mode 100644 OverlayManager.cs create mode 100644 OverlayWindow.xaml create mode 100644 OverlayWindow.xaml.cs create mode 100644 State.cs create mode 100644 TrayHost.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45acc48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,125 @@ +## Build results +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +TestResults/ +artifacts/ +*.VisualState.xml +*.userprefs + +## Rider / VS Code +.idea/ +.vscode/ +*.suo +*.user +*.userosscache +*.sln.docstates + +## VS Working / Cache directories +.vs/ +_ReSharper*/ +_ReSharper.Caches/ +*.DotSettings.user + +## Build Artifacts +*.dll +*.exe +*.appx +*.appxbundle +*.msix +*.msixbundle +*.log +*.cache +*.pdb +*.mdb + +## NuGet +*.nupkg +*.snupkg +*.nuspec +.nuget/ +packages/ +*.nuget.props +*.nuget.targets + +## Others +*.dbmdl +*.bak +*.swp +*.tmp +*.temp +*.pidb +*.svclog +*.scc +*.orig +*.rej +*.dif +*.patch + +## ASP.NET and Blazor +node_modules/ +wwwroot/dist/ +wwwroot/lib/ +wwwroot/**/*.map + +## Generated code +Generated_Code/ +ServiceFabricBackup/ +ClientBin/ + +## Publish artifacts +publish/ +dist/ +out/ +*.publishsettings +app.publish/ + +## Azure +*.azurePubxml +PublishProfiles/ +App_Data/ +*.Publish.xml + +## Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.stackdump + +## macOS +.DS_Store +.AppleDouble +.LSOverride + +## Logs +*.log +*.tlog + +## Coverage +*.coverage +*.coveragexml +*.opencover.xml +coverage*.json +coverage*.xml +coverage*.info + +## Temporary files +~$* +*.tmp +*.bak +*.swp +*.swo +*.DS_Store + +## Git +*.orig +*.rej +*.diff +*.patch + +## Environment files +.env +.env.local +.env.*.local diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..9a7bd8f --- /dev/null +++ b/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..c7d5ede --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Threading; +using System.Windows; + +namespace Bedtimer +{ + public partial class App : Application + { + private Mutex? _single; + private TrayHost? _tray; + + private void OnStartup(object sender, StartupEventArgs e) + { + var args = Environment.GetCommandLineArgs(); + + // 1) Elevated branches (exit immediately after doing the thing) + if (args.Length > 1 && args[1] == "--save-admin") + { + Config.SaveFromArgs(args); + Shutdown(); + return; + } + + // Handle elevated autostart toggle: Bedtimer.exe --autostart-admin on|off + AutoStart.HandleAdminArgs(args); // returns normally when not matching + + // 2) Single-instance guard + _single = new Mutex(true, @"Global\Bedtimer_Singleton", out bool isNew); + if (!isNew) + { + Shutdown(); + return; + } + + // 3) Optional bootstrap: enable autostart only if current process is admin + // (Non-admin users won't be prompted on first run; they can toggle from UI later.) + if (!AutoStart.IsEnabled() && Config.IsProcessElevated()) + { + // Will set HKLM\...\Run directly since we’re already elevated + AutoStart.SetEnabled(true); + } + + // 4) Start tray + overlay + _tray = new TrayHost(); + _tray.Init(); + } + + private void OnExit(object sender, ExitEventArgs e) + { + _tray?.Dispose(); + _single?.Dispose(); + } + } +} diff --git a/AutoStart.cs b/AutoStart.cs new file mode 100644 index 0000000..f020c32 --- /dev/null +++ b/AutoStart.cs @@ -0,0 +1,85 @@ +using Microsoft.Win32; +using System; +using System.Diagnostics; + +namespace Bedtimer +{ + public static class AutoStart + { + private const string RunKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + private const string ValueName = "Bedtimer"; + + // Public API used by UI + public static bool IsEnabled() => ExistsHKLM(); + + public static void SetEnabled(bool enabled) + { + try + { + if (enabled) EnsureHKLM(); + else DisableHKLM(); + } + catch (UnauthorizedAccessException) + { + // Relaunch self elevated to write HKLM + var exe = Environment.ProcessPath!; + var args = enabled ? "--autostart-admin on" : "--autostart-admin off"; + Process.Start(new ProcessStartInfo(exe, args) + { + UseShellExecute = true, + Verb = "runas" + }); + } + } + + // Branch the app handles on startup (see App.xaml.cs patch below) + public static void HandleAdminArgs(string[] args) + { + // args: ["--autostart-admin", "on"|"off"] + if (args.Length >= 3 && args[1] == "--autostart-admin") + { + if (string.Equals(args[2], "on", StringComparison.OrdinalIgnoreCase)) EnsureHKLM(); + else DisableHKLM(); + Environment.Exit(0); + } + } + + // -------- Internal HKLM helpers (explicit registry view) -------- + private static RegistryKey OpenHKLM() + { + var view = Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32; + return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); + } + + private static bool ExistsHKLM() + { + using var root = OpenHKLM(); + using var key = root.OpenSubKey(RunKey, false); + return key?.GetValue(ValueName) is string; + } + + private static void EnsureHKLM() + { + using var root = OpenHKLM(); + using var key = root.CreateSubKey(RunKey, true); + var exe = Environment.ProcessPath!; + var target = $"\"{exe}\""; // no args; single EXE handles all + key.SetValue(ValueName, target, RegistryValueKind.String); + + // (Optional) Clean up any old per-user Run entries so we don't double-start + try + { + using var hkcu = Registry.CurrentUser.CreateSubKey(RunKey, true); + hkcu.DeleteValue(ValueName, false); + } + catch { /* ignore */ } + } + + private static void DisableHKLM() + { + using var root = OpenHKLM(); + using var key = root.CreateSubKey(RunKey, true); + key.DeleteValue(ValueName, false); + } + } +} diff --git a/Bedtimer.csproj b/Bedtimer.csproj new file mode 100644 index 0000000..a51d84b --- /dev/null +++ b/Bedtimer.csproj @@ -0,0 +1,12 @@ + + + + WinExe + net8.0-windows + enable + enable + true + AnyCPU;x64 + + + diff --git a/Bedtimer.sln b/Bedtimer.sln new file mode 100644 index 0000000..d57fa51 --- /dev/null +++ b/Bedtimer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36518.9 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bedtimer", "Bedtimer.csproj", "{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|x64.ActiveCfg = Debug|x64 + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|x64.Build.0 = Debug|x64 + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|Any CPU.Build.0 = Release|Any CPU + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|x64.ActiveCfg = Release|x64 + {B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F77F2FC4-235E-4350-9B69-EA23D2113489} + EndGlobalSection +EndGlobal diff --git a/Config.cs b/Config.cs new file mode 100644 index 0000000..4732d73 --- /dev/null +++ b/Config.cs @@ -0,0 +1,121 @@ +using Microsoft.Win32; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Threading; + +namespace Bedtimer +{ + public sealed class Config + { + public int BedMinutes { get; set; } = 22 * 60 + 30; // 10:30 PM + public int WarnLead { get; set; } = 60; + public int FlashLead { get; set; } = 20; + public int LockDelay { get; set; } = 5; + + private const string RegPath = @"SOFTWARE\Whitlocktech\Bedtimer"; + + // --- Public API ------------------------------------------------------- + + public static Config LoadOrCreate() + { + // Read only; do NOT write defaults here. + using var k = OpenHKLM().OpenSubKey(RegPath, writable: false); + if (k == null) return new Config(); // defaults in memory only + + int Get(string n, int d) => (k.GetValue(n) is int v) ? v : d; + + return new Config + { + BedMinutes = Get(nameof(BedMinutes), 22 * 60 + 30), + WarnLead = Get(nameof(WarnLead), 60), + FlashLead = Get(nameof(FlashLead), 20), + LockDelay = Get(nameof(LockDelay), 5), + }; + } + + public void Save() + { + try + { + using var root = OpenHKLM(); + using var k = root.CreateSubKey(RegPath, writable: true); + k.SetValue(nameof(BedMinutes), BedMinutes, RegistryValueKind.DWord); + k.SetValue(nameof(WarnLead), WarnLead, RegistryValueKind.DWord); + k.SetValue(nameof(FlashLead), FlashLead, RegistryValueKind.DWord); + k.SetValue(nameof(LockDelay), LockDelay, RegistryValueKind.DWord); + Signals.RaiseConfigChanged(); + } + catch (UnauthorizedAccessException) + { + // Relaunch self elevated to write HKLM in the SAME registry view + var exe = Environment.ProcessPath!; + var args = $"--save-admin {BedMinutes} {WarnLead} {FlashLead} {LockDelay}"; + Process.Start(new ProcessStartInfo(exe, args) + { + UseShellExecute = true, + Verb = "runas" + }); + } + } + + public static void SaveFromArgs(string[] args) + { + // Accept: Bedtimer.exe --save-admin + // Find the flag and read the 4 numbers after it. + int i = Array.FindIndex(args, a => string.Equals(a, "--save-admin", StringComparison.OrdinalIgnoreCase)); + if (i < 0 || args.Length < i + 5) return; + + var cfg = new Config + { + BedMinutes = int.Parse(args[i + 1]), + WarnLead = int.Parse(args[i + 2]), + FlashLead = int.Parse(args[i + 3]), + LockDelay = int.Parse(args[i + 4]), + }; + + using var root = OpenHKLM(); + using var k = root.CreateSubKey(RegPath, writable: true); + k.SetValue(nameof(BedMinutes), cfg.BedMinutes, RegistryValueKind.DWord); + k.SetValue(nameof(WarnLead), cfg.WarnLead, RegistryValueKind.DWord); + k.SetValue(nameof(FlashLead), cfg.FlashLead, RegistryValueKind.DWord); + k.SetValue(nameof(LockDelay), cfg.LockDelay, RegistryValueKind.DWord); + Signals.RaiseConfigChanged(); + } + + + // Keep %AppData% for state/logs + public static string Dir => System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Whitlocktech", "Bedtimer"); + + // --- Helpers ---------------------------------------------------------- + + private static RegistryKey OpenHKLM() + { + // Always open the correct view explicitly to avoid WOW6432 redirection issues + var view = Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32; + return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); + } + + public static bool IsProcessElevated() + { + using var id = WindowsIdentity.GetCurrent(); + var p = new WindowsPrincipal(id); + return p.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + public static class Signals + { + public const string ConfigChangedName = @"Local\Bedtimer_Config_Changed"; + public static void RaiseConfigChanged() + { + using var ev = new EventWaitHandle(false, EventResetMode.AutoReset, ConfigChangedName); + ev.Set(); + } + public static EventWaitHandle SubscribeConfigChanged() + => new EventWaitHandle(false, EventResetMode.AutoReset, ConfigChangedName); + } +} diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..4e596b6 --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +