commit 45b9d70c901ac7c8761bf4c80a33da98b7724e7d Author: whitlocktech Date: Sun Oct 26 10:40:21 2025 -0500 initial 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +