From 1c8f184994971b67eaae2733c8f4206e53f628ed Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Tue, 24 Feb 2026 17:15:42 +1100 Subject: [PATCH] Initial portable Caffeine implementation --- README.md | 119 +++ caffeine.ps1 | 1976 ++++++++++++++++++++++++++++++++++++++++++++++ run-caffeine.bat | 13 + 3 files changed, 2108 insertions(+) create mode 100644 README.md create mode 100644 caffeine.ps1 create mode 100644 run-caffeine.bat diff --git a/README.md b/README.md new file mode 100644 index 0000000..667701c --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Portable Caffeine (PowerShell 5.1 + Embedded C#) + +Portable Windows implementation of the Zhorn Software Caffeine utility, built as a single `caffeine.ps1` script with embedded C# (`WinForms` + Win32 API calls). + +## What It Does + +Prevents sleep/idle by either: + +- Sending periodic key pulses (default `F15`) +- Using `SetThreadExecutionState` (`-stes`) + +It also includes a tray icon app, status dialog, timers, rule-based activation, and single-instance app control commands. + +## Files + +- `caffeine.ps1`: main portable app +- `run-caffeine.bat`: launcher that runs with `-ExecutionPolicy Bypass -STA` + +## Requirements + +- Windows +- Windows PowerShell 5.1 +- Desktop session (uses `System.Windows.Forms` + tray icon) + +## Quick Start + +Run with the batch launcher: + +```bat +run-caffeine.bat -showdlg -notify +``` + +Or directly: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File .\caffeine.ps1 -showdlg -notify +``` + +## Execution Policy Note + +If PowerShell blocks script execution, use the launcher (`run-caffeine.bat`) or run: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File .\caffeine.ps1 +``` + +Optional: + +```powershell +Unblock-File .\caffeine.ps1 +``` + +## Supported Features / Switches + +Core: + +- `-on` / `-off` +- `-activefor:N` +- `-inactivefor:N` +- `-exitafter:N` +- `-lock` +- `-notify` +- `-showdlg` +- `-ontaskbar` +- `-replace` + +Single-instance app commands: + +- `-appexit` +- `-appon` +- `-appoff` +- `-apptoggle` +- `-apptoggleshowdlg` + +Activity methods: + +- Default F15 key pulse +- `-useshift` +- `-leftshift` +- `-key:NN` / `-keypress:NN` +- `-keyshift[:NN]` +- `-stes` +- `-allowss` + +Conditional activation: + +- `-watchwindow:TEXT` +- `-activeperiods:RANGES` +- `-activehours:RANGES` (alias) +- `-onac` +- `-cpu:N` + +Tray / icon behavior: + +- Custom coffee-cup tray icon (active/inactive variants) +- `-nohicon` keeps the same tray icon in both states +- Double-click tray icon toggles active/inactive + +Compatibility: + +- `-allowlocal` is recognized (compatibility flag) + +## Examples + +```bat +run-caffeine.bat -showdlg -notify +run-caffeine.bat -activefor:30 +run-caffeine.bat -watchwindow:Notepad -on +run-caffeine.bat -activeperiods:08:00-12:00,13:00-17:00 +run-caffeine.bat -stes -allowss +run-caffeine.bat -apptoggle +run-caffeine.bat -replace -off +``` + +## Notes + +- This project is intended to be portable (no install required). +- The implementation targets Windows PowerShell 5.1 and Windows desktop APIs. +- Exact visual/behavioral parity with the original Zhorn Caffeine may differ slightly in some edge cases. diff --git a/caffeine.ps1 b/caffeine.ps1 new file mode 100644 index 0000000..eff3775 --- /dev/null +++ b/caffeine.ps1 @@ -0,0 +1,1976 @@ +$ErrorActionPreference = 'Stop' + +$csharp = @' +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows.Forms; + +namespace PortableCaffeine +{ + internal enum RemoteCommandKind + { + None, + AppExit, + AppOn, + AppOff, + AppToggle, + AppToggleShowDialog, + ShowDialog, + ExitForReplace + } + + internal enum ManualOverrideKind + { + None, + ForceActive, + ForceInactive + } + + internal enum EffectiveMode + { + Inactive, + ActiveKeyPulse, + ActiveStesSystemOnly, + ActiveStesSystemAndDisplay + } + + internal sealed class TimeRange + { + public TimeSpan Start; + public TimeSpan End; + + public bool Contains(TimeSpan time) + { + if (Start == End) + { + return true; + } + + if (Start < End) + { + return time >= Start && time < End; + } + + return time >= Start || time < End; + } + + public override string ToString() + { + return Start.ToString(@"hh\:mm") + "-" + End.ToString(@"hh\:mm"); + } + } + + internal sealed class Options + { + public bool ShowHelp; + public bool StartupEnabled = true; + public bool StartupModeExplicit; + public bool LockWorkstation; + public bool UseStes; + public bool AllowScreenSaver; + public bool Notify; + public bool ShowDialog; + public bool OnTaskbar; + public bool NoHIcon; + public bool ReplaceExisting; + public bool OnAcOnly; + public bool AllowLocalCompat; + public bool KeyWithShift; + public string WatchWindowContains; + public int? CpuThresholdPercent; + public double? ExitAfterMinutes; + public double? ActiveForMinutes; + public double? InactiveForMinutes; + public ushort VirtualKey = 0x7E; // F15 default + public List ActivePeriods = new List(); + public RemoteCommandKind AppCommand = RemoteCommandKind.None; + public List UnknownSwitches = new List(); + public List RawArgs = new List(); + + public bool HasAnyAppCommand + { + get { return AppCommand != RemoteCommandKind.None; } + } + } + + internal static class CommandLine + { + public static Options Parse(string[] args) + { + Options options = new Options(); + if (args == null) + { + return options; + } + + for (int i = 0; i < args.Length; i++) + { + string token = args[i]; + if (token == null) + { + continue; + } + + options.RawArgs.Add(token); + + if (!token.StartsWith("-") && !token.StartsWith("/")) + { + options.UnknownSwitches.Add(token); + continue; + } + + string body = token.Substring(1); + string name = body; + string value = null; + + int colon = body.IndexOf(':'); + int equals = body.IndexOf('='); + int sep = -1; + if (colon >= 0 && equals >= 0) + { + sep = Math.Min(colon, equals); + } + else if (colon >= 0) + { + sep = colon; + } + else if (equals >= 0) + { + sep = equals; + } + + if (sep >= 0) + { + name = body.Substring(0, sep); + value = body.Substring(sep + 1); + } + + name = name.Trim().ToLowerInvariant(); + if (value != null) + { + value = value.Trim(); + } + + switch (name) + { + case "?": + case "h": + case "help": + options.ShowHelp = true; + break; + case "on": + options.StartupEnabled = true; + options.StartupModeExplicit = true; + break; + case "off": + options.StartupEnabled = false; + options.StartupModeExplicit = true; + break; + case "lock": + options.LockWorkstation = true; + break; + case "showdlg": + options.ShowDialog = true; + break; + case "ontaskbar": + options.OnTaskbar = true; + break; + case "notify": + options.Notify = true; + break; + case "nohicon": + options.NoHIcon = true; + break; + case "replace": + options.ReplaceExisting = true; + break; + case "onac": + options.OnAcOnly = true; + break; + case "allowlocal": + options.AllowLocalCompat = true; + break; + case "stes": + options.UseStes = true; + break; + case "allowss": + options.AllowScreenSaver = true; + break; + case "watchwindow": + if (!string.IsNullOrEmpty(value)) + { + options.WatchWindowContains = value; + } + break; + case "cpu": + { + int cpu; + if (TryParseInt(value, out cpu)) + { + if (cpu < 0) cpu = 0; + if (cpu > 100) cpu = 100; + options.CpuThresholdPercent = cpu; + } + } + break; + case "exitafter": + options.ExitAfterMinutes = ParseMinutes(value, options.ExitAfterMinutes); + break; + case "activefor": + options.ActiveForMinutes = ParseMinutes(value, options.ActiveForMinutes); + break; + case "inactivefor": + options.InactiveForMinutes = ParseMinutes(value, options.InactiveForMinutes); + break; + case "activehours": + case "activeperiods": + if (!string.IsNullOrEmpty(value)) + { + List parsed = ParseTimeRanges(value); + if (parsed.Count > 0) + { + options.ActivePeriods = parsed; + } + } + break; + case "useshift": + options.VirtualKey = 0x10; // VK_SHIFT + break; + case "leftshift": + options.VirtualKey = 0xA0; // VK_LSHIFT + break; + case "key": + case "keypress": + { + ushort vk; + if (TryParseVirtualKey(value, out vk)) + { + options.VirtualKey = vk; + } + } + break; + case "keyshift": + options.KeyWithShift = true; + if (!string.IsNullOrEmpty(value)) + { + ushort vk; + if (TryParseVirtualKey(value, out vk)) + { + options.VirtualKey = vk; + } + } + break; + case "appexit": + options.AppCommand = RemoteCommandKind.AppExit; + break; + case "appon": + options.AppCommand = RemoteCommandKind.AppOn; + break; + case "appoff": + options.AppCommand = RemoteCommandKind.AppOff; + break; + case "apptoggle": + options.AppCommand = RemoteCommandKind.AppToggle; + break; + case "apptoggleshowdlg": + options.AppCommand = RemoteCommandKind.AppToggleShowDialog; + break; + default: + options.UnknownSwitches.Add(token); + break; + } + } + + return options; + } + + private static double? ParseMinutes(string value, double? fallback) + { + if (string.IsNullOrEmpty(value)) + { + return fallback; + } + + double minutes; + if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out minutes)) + { + return minutes; + } + + return fallback; + } + + private static bool TryParseInt(string value, out int number) + { + number = 0; + if (string.IsNullOrEmpty(value)) + { + return false; + } + + return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number); + } + + private static bool TryParseVirtualKey(string value, out ushort vk) + { + vk = 0; + if (string.IsNullOrEmpty(value)) + { + return false; + } + + string v = value.Trim(); + try + { + if (v.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + vk = Convert.ToUInt16(v.Substring(2), 16); + return true; + } + + vk = Convert.ToUInt16(v, CultureInfo.InvariantCulture); + return true; + } + catch + { + return false; + } + } + + private static List ParseTimeRanges(string value) + { + List list = new List(); + string[] parts = value.Split(new char[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < parts.Length; i++) + { + string p = parts[i].Trim(); + if (p.Length == 0) + { + continue; + } + + int dash = p.IndexOf('-'); + if (dash <= 0 || dash >= p.Length - 1) + { + continue; + } + + string left = p.Substring(0, dash).Trim(); + string right = p.Substring(dash + 1).Trim(); + + TimeSpan start; + TimeSpan end; + if (!TryParseTime(left, out start) || !TryParseTime(right, out end)) + { + continue; + } + + TimeRange range = new TimeRange(); + range.Start = start; + range.End = end; + list.Add(range); + } + + return list; + } + + private static bool TryParseTime(string text, out TimeSpan time) + { + time = TimeSpan.Zero; + if (string.IsNullOrEmpty(text)) + { + return false; + } + + DateTime dt; + string[] formats = new string[] + { + "H", + "HH", + "H:mm", + "HH:mm", + "H.mm", + "HH.mm", + "Hmm", + "HHmm" + }; + + if (DateTime.TryParseExact(text, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) + { + time = dt.TimeOfDay; + return true; + } + + int hour; + if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out hour) && hour >= 0 && hour <= 23) + { + time = new TimeSpan(hour, 0, 0); + return true; + } + + return false; + } + } + + internal static class NativeMethods + { + public const int WM_COPYDATA = 0x004A; + public const uint ES_SYSTEM_REQUIRED = 0x00000001; + public const uint ES_DISPLAY_REQUIRED = 0x00000002; + public const uint ES_CONTINUOUS = 0x80000000; + public const uint KEYEVENTF_KEYUP = 0x0002; + + [StructLayout(LayoutKind.Sequential)] + public struct COPYDATASTRUCT + { + public IntPtr dwData; + public int cbData; + public IntPtr lpData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_POWER_STATUS + { + public byte ACLineStatus; + public byte BatteryFlag; + public byte BatteryLifePercent; + public byte SystemStatusFlag; + public int BatteryLifeTime; + public int BatteryFullLifeTime; + } + + [DllImport("kernel32.dll")] + public static extern uint SetThreadExecutionState(uint esFlags); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref COPYDATASTRUCT lParam); + + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + public static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool LockWorkStation(); + + [DllImport("kernel32.dll")] + public static extern bool GetSystemPowerStatus(out SYSTEM_POWER_STATUS sps); + + [DllImport("user32.dll")] + public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DestroyIcon(IntPtr hIcon); + } + + internal static class TrayIconFactory + { + public static Icon CreateCaffeineTrayIcon(bool active) + { + Bitmap bmp = new Bitmap(16, 16); + try + { + using (Graphics g = Graphics.FromImage(bmp)) + { + g.SmoothingMode = SmoothingMode.AntiAlias; + g.Clear(Color.Transparent); + + Color cupFill = active ? Color.FromArgb(68, 118, 184) : Color.FromArgb(140, 145, 152); + Color cupOutline = active ? Color.FromArgb(35, 62, 102) : Color.FromArgb(84, 88, 94); + Color coffeeFill = active ? Color.FromArgb(111, 72, 41) : Color.FromArgb(108, 96, 88); + Color steamColor = active ? Color.FromArgb(245, 248, 252) : Color.FromArgb(188, 191, 196); + Color saucerColor = active ? Color.FromArgb(54, 77, 101) : Color.FromArgb(106, 110, 116); + + using (SolidBrush cupBrush = new SolidBrush(cupFill)) + using (SolidBrush coffeeBrush = new SolidBrush(coffeeFill)) + using (Pen outlinePen = new Pen(cupOutline, 1f)) + using (Pen steamPen = new Pen(steamColor, 1.1f)) + using (Pen saucerPen = new Pen(saucerColor, 1f)) + { + // Mug body + g.FillRectangle(cupBrush, 3, 7, 7, 5); + g.DrawRectangle(outlinePen, 3, 7, 7, 5); + + // Coffee surface + g.FillRectangle(coffeeBrush, 4, 8, 5, 2); + + // Mug handle + g.DrawArc(outlinePen, 8, 7, 4, 4, 285, 255); + + // Saucer + g.DrawLine(saucerPen, 2, 13, 12, 13); + + // Steam + g.DrawBezier(steamPen, 4, 7, 3, 5, 5, 4, 4, 2); + g.DrawBezier(steamPen, 7, 7, 6, 5, 8, 4, 7, 2); + } + + if (!active) + { + using (Pen slashPen = new Pen(Color.FromArgb(210, 186, 53, 53), 1.8f)) + { + g.DrawLine(slashPen, 3, 13, 13, 3); + } + } + } + + IntPtr hIcon = bmp.GetHicon(); + try + { + using (Icon tmp = Icon.FromHandle(hIcon)) + { + return (Icon)tmp.Clone(); + } + } + finally + { + NativeMethods.DestroyIcon(hIcon); + } + } + catch + { + return null; + } + finally + { + bmp.Dispose(); + } + } + } + + internal static class IpcTransport + { + public const string WindowTitle = "PortableCaffeine.HiddenIpcWindow"; + + public static bool Send(string payload) + { + IntPtr hwnd = NativeMethods.FindWindow(null, WindowTitle); + if (hwnd == IntPtr.Zero) + { + return false; + } + + IntPtr hGlobal = IntPtr.Zero; + try + { + string data = payload ?? string.Empty; + hGlobal = Marshal.StringToHGlobalUni(data); + NativeMethods.COPYDATASTRUCT cds = new NativeMethods.COPYDATASTRUCT(); + cds.dwData = IntPtr.Zero; + cds.cbData = (data.Length + 1) * 2; + cds.lpData = hGlobal; + NativeMethods.SendMessage(hwnd, NativeMethods.WM_COPYDATA, IntPtr.Zero, ref cds); + return true; + } + catch + { + return false; + } + finally + { + if (hGlobal != IntPtr.Zero) + { + Marshal.FreeHGlobal(hGlobal); + } + } + } + } + + internal sealed class IpcForm : Form + { + public delegate void PayloadReceivedHandler(object sender, string payload); + public event PayloadReceivedHandler PayloadReceived; + + public IpcForm() + { + this.Text = IpcTransport.WindowTitle; + this.ShowInTaskbar = false; + this.FormBorderStyle = FormBorderStyle.FixedToolWindow; + this.StartPosition = FormStartPosition.Manual; + this.Size = new Size(1, 1); + this.Location = new Point(-32000, -32000); + this.Opacity = 0; + } + + protected override void SetVisibleCore(bool value) + { + base.SetVisibleCore(false); + } + + protected override void OnShown(EventArgs e) + { + this.Hide(); + base.OnShown(e); + } + + protected override void WndProc(ref Message m) + { + if (m.Msg == NativeMethods.WM_COPYDATA) + { + try + { + NativeMethods.COPYDATASTRUCT cds = (NativeMethods.COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam, typeof(NativeMethods.COPYDATASTRUCT)); + if (cds.lpData != IntPtr.Zero && cds.cbData > 0) + { + string payload = Marshal.PtrToStringUni(cds.lpData); + if (PayloadReceived != null) + { + PayloadReceived(this, payload); + } + } + } + catch + { + // Ignore malformed IPC messages. + } + } + + base.WndProc(ref m); + } + } + + internal sealed class StatusForm : Form + { + private Label _statusLabel; + private Label _detailLabel; + private Label _conditionLabel; + private Button _toggleButton; + private Button _revertButton; + private Button _closeButton; + + public event EventHandler ToggleRequested; + public event EventHandler RevertRequested; + + public StatusForm(bool showInTaskbar) + { + this.Text = "Portable Caffeine"; + this.FormBorderStyle = FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.StartPosition = FormStartPosition.CenterScreen; + this.ClientSize = new Size(470, 190); + this.ShowInTaskbar = showInTaskbar; + + _statusLabel = new Label(); + _statusLabel.AutoSize = false; + _statusLabel.Location = new Point(12, 12); + _statusLabel.Size = new Size(446, 28); + _statusLabel.Font = new Font(SystemFonts.MessageBoxFont.FontFamily, 11, FontStyle.Bold); + + _detailLabel = new Label(); + _detailLabel.AutoSize = false; + _detailLabel.Location = new Point(12, 48); + _detailLabel.Size = new Size(446, 44); + + _conditionLabel = new Label(); + _conditionLabel.AutoSize = false; + _conditionLabel.Location = new Point(12, 96); + _conditionLabel.Size = new Size(446, 42); + + _toggleButton = new Button(); + _toggleButton.Text = "Toggle"; + _toggleButton.Location = new Point(12, 148); + _toggleButton.Size = new Size(90, 28); + _toggleButton.Click += delegate(object sender, EventArgs e) + { + if (ToggleRequested != null) + { + ToggleRequested(this, EventArgs.Empty); + } + }; + + _revertButton = new Button(); + _revertButton.Text = "Revert"; + _revertButton.Location = new Point(108, 148); + _revertButton.Size = new Size(90, 28); + _revertButton.Click += delegate(object sender, EventArgs e) + { + if (RevertRequested != null) + { + RevertRequested(this, EventArgs.Empty); + } + }; + + _closeButton = new Button(); + _closeButton.Text = "Close"; + _closeButton.Location = new Point(368, 148); + _closeButton.Size = new Size(90, 28); + _closeButton.Click += delegate(object sender, EventArgs e) + { + this.Hide(); + }; + + this.Controls.Add(_statusLabel); + this.Controls.Add(_detailLabel); + this.Controls.Add(_conditionLabel); + this.Controls.Add(_toggleButton); + this.Controls.Add(_revertButton); + this.Controls.Add(_closeButton); + } + + public void UpdateView(string status, string detail, string condition) + { + _statusLabel.Text = status ?? string.Empty; + _detailLabel.Text = detail ?? string.Empty; + _conditionLabel.Text = condition ?? string.Empty; + } + } + + internal sealed class CaffeineAppContext : ApplicationContext + { + private readonly Options _options; + private readonly Mutex _instanceMutex; + private readonly NotifyIcon _tray; + private readonly Icon _trayIconActive; + private readonly Icon _trayIconInactive; + private readonly ContextMenuStrip _menu; + private readonly IpcForm _ipcForm; + private readonly System.Windows.Forms.Timer _timer; + private readonly DateTime _startedLocal; + private DateTime? _exitAtLocal; + private DateTime? _startupActiveUntilLocal; + private DateTime? _startupInactiveUntilLocal; + private DateTime? _manualOverrideUntilLocal; + private ManualOverrideKind _manualOverrideKind; + private bool _lastEffectiveActive; + private bool _hasLastEffectiveActive; + private bool _lastConditionsMatch; + private string _lastReason = "Initializing"; + private string _lastConditionSummary = "Initializing"; + private string _lastCpuSummary = "CPU condition not configured."; + private DateTime _lastPulseUtc = DateTime.MinValue; + private DateTime _lastStesRefreshUtc = DateTime.MinValue; + private PerformanceCounter _cpuCounter; + private bool _cpuCounterFailed; + private bool _cpuCounterPrimed; + private float _lastCpuPercent; + private StatusForm _statusForm; + private bool _lockPending; + private bool _disposed; + + // Menu references for state updates + private ToolStripMenuItem _miStatus; + private ToolStripMenuItem _miActiveNow; + private ToolStripMenuItem _miInactiveNow; + private ToolStripMenuItem _miRevert; + private ToolStripMenuItem _miShowHide; + private ToolStripMenuItem _miTimed5; + private ToolStripMenuItem _miTimed15; + private ToolStripMenuItem _miTimed30; + private ToolStripMenuItem _miTimed60; + private ToolStripMenuItem _miTimed120; + private ToolStripMenuItem _miTimedInactive5; + private ToolStripMenuItem _miTimedInactive15; + private ToolStripMenuItem _miTimedInactive30; + + public CaffeineAppContext(Options options, Mutex instanceMutex) + { + _options = options; + _instanceMutex = instanceMutex; + _startedLocal = DateTime.Now; + _lockPending = options.LockWorkstation; + + if (options.ExitAfterMinutes.HasValue) + { + _exitAtLocal = _startedLocal.AddMinutes(options.ExitAfterMinutes.Value); + } + if (options.ActiveForMinutes.HasValue) + { + _startupActiveUntilLocal = _startedLocal.AddMinutes(options.ActiveForMinutes.Value); + } + if (options.InactiveForMinutes.HasValue) + { + _startupInactiveUntilLocal = _startedLocal.AddMinutes(options.InactiveForMinutes.Value); + } + + _tray = new NotifyIcon(); + _trayIconActive = TrayIconFactory.CreateCaffeineTrayIcon(true); + _trayIconInactive = TrayIconFactory.CreateCaffeineTrayIcon(false); + _tray.Visible = true; + _tray.Text = "Portable Caffeine"; + _tray.DoubleClick += delegate(object sender, EventArgs e) + { + ToggleManual(); + }; + + _menu = new ContextMenuStrip(); + _tray.ContextMenuStrip = _menu; + BuildMenu(); + SetTrayIcon(false); + + _ipcForm = new IpcForm(); + _ipcForm.PayloadReceived += OnIpcPayload; + IntPtr dummyHandle = _ipcForm.Handle; + _ipcForm.Show(); + _ipcForm.Hide(); + + if (_options.CpuThresholdPercent.HasValue) + { + TryInitCpuCounter(); + } + + _timer = new System.Windows.Forms.Timer(); + _timer.Interval = 1000; + _timer.Tick += OnTick; + _timer.Start(); + + if (_options.ShowDialog) + { + EnsureStatusForm(); + ShowStatusForm(true); + } + + // Run one evaluation immediately so state is correct on launch. + TickCore(); + } + + private void BuildMenu() + { + _menu.Items.Clear(); + + _miStatus = new ToolStripMenuItem("Status: Initializing"); + _miStatus.Enabled = false; + + _miActiveNow = new ToolStripMenuItem("Active (indefinite)"); + _miActiveNow.Click += delegate { SetManualOverride(ManualOverrideKind.ForceActive, null); }; + + _miInactiveNow = new ToolStripMenuItem("Inactive (indefinite)"); + _miInactiveNow.Click += delegate { SetManualOverride(ManualOverrideKind.ForceInactive, null); }; + + _miTimed5 = new ToolStripMenuItem("Active for 5 minutes"); + _miTimed5.Click += delegate { SetManualOverride(ManualOverrideKind.ForceActive, 5); }; + _miTimed15 = new ToolStripMenuItem("Active for 15 minutes"); + _miTimed15.Click += delegate { SetManualOverride(ManualOverrideKind.ForceActive, 15); }; + _miTimed30 = new ToolStripMenuItem("Active for 30 minutes"); + _miTimed30.Click += delegate { SetManualOverride(ManualOverrideKind.ForceActive, 30); }; + _miTimed60 = new ToolStripMenuItem("Active for 60 minutes"); + _miTimed60.Click += delegate { SetManualOverride(ManualOverrideKind.ForceActive, 60); }; + _miTimed120 = new ToolStripMenuItem("Active for 120 minutes"); + _miTimed120.Click += delegate { SetManualOverride(ManualOverrideKind.ForceActive, 120); }; + + _miTimedInactive5 = new ToolStripMenuItem("Inactive for 5 minutes"); + _miTimedInactive5.Click += delegate { SetManualOverride(ManualOverrideKind.ForceInactive, 5); }; + _miTimedInactive15 = new ToolStripMenuItem("Inactive for 15 minutes"); + _miTimedInactive15.Click += delegate { SetManualOverride(ManualOverrideKind.ForceInactive, 15); }; + _miTimedInactive30 = new ToolStripMenuItem("Inactive for 30 minutes"); + _miTimedInactive30.Click += delegate { SetManualOverride(ManualOverrideKind.ForceInactive, 30); }; + + _miRevert = new ToolStripMenuItem("Revert To Parameters"); + _miRevert.Click += delegate { RevertToParameters(); }; + + _miShowHide = new ToolStripMenuItem("Show Status"); + _miShowHide.Click += delegate { ToggleStatusForm(); }; + + ToolStripMenuItem about = new ToolStripMenuItem("About"); + about.Click += delegate { ShowAbout(); }; + + ToolStripMenuItem exit = new ToolStripMenuItem("Exit"); + exit.Click += delegate { ExitApplication(); }; + + _menu.Items.Add(_miStatus); + _menu.Items.Add(new ToolStripSeparator()); + _menu.Items.Add(_miActiveNow); + _menu.Items.Add(_miInactiveNow); + _menu.Items.Add(new ToolStripSeparator()); + _menu.Items.Add(_miTimed5); + _menu.Items.Add(_miTimed15); + _menu.Items.Add(_miTimed30); + _menu.Items.Add(_miTimed60); + _menu.Items.Add(_miTimed120); + _menu.Items.Add(new ToolStripSeparator()); + _menu.Items.Add(_miTimedInactive5); + _menu.Items.Add(_miTimedInactive15); + _menu.Items.Add(_miTimedInactive30); + _menu.Items.Add(new ToolStripSeparator()); + _menu.Items.Add(_miRevert); + _menu.Items.Add(_miShowHide); + _menu.Items.Add(new ToolStripSeparator()); + _menu.Items.Add(about); + _menu.Items.Add(exit); + } + + private void EnsureStatusForm() + { + if (_statusForm != null && !_statusForm.IsDisposed) + { + return; + } + + _statusForm = new StatusForm(_options.OnTaskbar); + _statusForm.ToggleRequested += delegate { ToggleManual(); }; + _statusForm.RevertRequested += delegate { RevertToParameters(); }; + _statusForm.FormClosing += delegate(object sender, FormClosingEventArgs e) + { + if (e.CloseReason == CloseReason.UserClosing) + { + e.Cancel = true; + _statusForm.Hide(); + UpdateMenuState(); + } + }; + } + + private void ToggleStatusForm() + { + EnsureStatusForm(); + if (_statusForm.Visible) + { + _statusForm.Hide(); + } + else + { + ShowStatusForm(true); + } + + UpdateMenuState(); + } + + private void ShowStatusForm(bool activate) + { + EnsureStatusForm(); + if (!_statusForm.Visible) + { + _statusForm.Show(); + } + if (activate) + { + _statusForm.WindowState = FormWindowState.Normal; + _statusForm.BringToFront(); + _statusForm.Activate(); + } + UpdateMenuState(); + } + + private void ShowAbout() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Portable Caffeine (PowerShell + embedded C#)"); + sb.AppendLine("Zhorn Caffeine-compatible portable implementation."); + sb.AppendLine(); + sb.AppendLine("Method: " + GetMethodSummary()); + sb.AppendLine("Key: " + FormatVirtualKey(_options.VirtualKey) + (_options.KeyWithShift ? " (with Shift)" : string.Empty)); + if (_options.AllowLocalCompat) + { + sb.AppendLine("Compatibility flag: -allowlocal recognized."); + } + if (_options.UnknownSwitches.Count > 0) + { + sb.AppendLine("Unknown switches ignored: " + string.Join(", ", _options.UnknownSwitches.ToArray())); + } + MessageBox.Show(sb.ToString(), "About Portable Caffeine", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void OnTick(object sender, EventArgs e) + { + TickCore(); + } + + private void TickCore() + { + if (_disposed) + { + return; + } + + DateTime nowLocal = DateTime.Now; + DateTime nowUtc = DateTime.UtcNow; + + if (_lockPending) + { + _lockPending = false; + try + { + NativeMethods.LockWorkStation(); + } + catch + { + // Ignore lock failures. + } + } + + if (_exitAtLocal.HasValue && nowLocal >= _exitAtLocal.Value) + { + ExitApplication(); + return; + } + + ExpireManualOverrideIfNeeded(nowLocal); + + bool conditionsMatch; + string conditionSummary; + bool baseShouldBeActive = EvaluateBaseRules(nowLocal, out conditionsMatch, out conditionSummary); + + bool effectiveActive; + string reason; + EvaluateEffectiveState(nowLocal, baseShouldBeActive, out effectiveActive, out reason); + + _lastConditionsMatch = conditionsMatch; + _lastReason = reason; + _lastConditionSummary = conditionSummary; + + EffectiveMode mode = effectiveActive ? GetActiveMode() : EffectiveMode.Inactive; + ApplyActiveMode(mode, nowUtc); + + if (!_hasLastEffectiveActive || effectiveActive != _lastEffectiveActive) + { + _hasLastEffectiveActive = true; + _lastEffectiveActive = effectiveActive; + SetTrayIcon(effectiveActive); + if (_options.Notify) + { + ShowNotify(effectiveActive ? "Caffeine active" : "Caffeine inactive", reason); + } + } + + UpdateMenuState(); + UpdateStatusForm(); + UpdateTrayTooltip(effectiveActive, reason); + } + + private void ExpireManualOverrideIfNeeded(DateTime nowLocal) + { + if (_manualOverrideUntilLocal.HasValue && nowLocal >= _manualOverrideUntilLocal.Value) + { + _manualOverrideKind = ManualOverrideKind.None; + _manualOverrideUntilLocal = null; + } + } + + private bool EvaluateBaseRules(DateTime nowLocal, out bool conditionsMatch, out string conditionSummary) + { + List parts = new List(); + + bool baseMode = _options.StartupEnabled; + parts.Add("Base: " + (baseMode ? "On" : "Off")); + + if (_options.ActivePeriods != null && _options.ActivePeriods.Count > 0) + { + bool inPeriod = false; + TimeSpan current = nowLocal.TimeOfDay; + for (int i = 0; i < _options.ActivePeriods.Count; i++) + { + if (_options.ActivePeriods[i].Contains(current)) + { + inPeriod = true; + break; + } + } + parts.Add("Period: " + (inPeriod ? "matched" : "outside")); + if (!inPeriod) + { + conditionsMatch = false; + conditionSummary = string.Join(" | ", parts.ToArray()); + return false; + } + } + + if (!string.IsNullOrEmpty(_options.WatchWindowContains)) + { + bool match = ForegroundWindowMatches(_options.WatchWindowContains); + parts.Add("WatchWindow: " + (match ? "matched" : "not matched")); + if (!match) + { + conditionsMatch = false; + conditionSummary = string.Join(" | ", parts.ToArray()); + return false; + } + } + + if (_options.OnAcOnly) + { + bool onAc; + string powerSummary; + onAc = IsOnAc(out powerSummary); + parts.Add(powerSummary); + if (!onAc) + { + conditionsMatch = false; + conditionSummary = string.Join(" | ", parts.ToArray()); + return false; + } + } + + if (_options.CpuThresholdPercent.HasValue) + { + bool cpuOk; + string cpuSummary; + cpuOk = IsCpuAtOrAboveThreshold(_options.CpuThresholdPercent.Value, out cpuSummary); + _lastCpuSummary = cpuSummary; + parts.Add(cpuSummary); + if (!cpuOk) + { + conditionsMatch = false; + conditionSummary = string.Join(" | ", parts.ToArray()); + return false; + } + } + else + { + _lastCpuSummary = "CPU condition not configured."; + } + + conditionsMatch = true; + conditionSummary = string.Join(" | ", parts.ToArray()); + return baseMode; + } + + private void EvaluateEffectiveState(DateTime nowLocal, bool baseShouldBeActive, out bool effectiveActive, out string reason) + { + string timedSource; + ManualOverrideKind timedStartupOverride = GetStartupTimedOverride(nowLocal, out timedSource); + + if (_manualOverrideKind != ManualOverrideKind.None) + { + effectiveActive = _manualOverrideKind == ManualOverrideKind.ForceActive; + if (_manualOverrideUntilLocal.HasValue) + { + reason = "Manual " + (effectiveActive ? "active" : "inactive") + " until " + _manualOverrideUntilLocal.Value.ToString("HH:mm:ss"); + } + else + { + reason = "Manual " + (effectiveActive ? "active" : "inactive"); + } + return; + } + + if (timedStartupOverride != ManualOverrideKind.None) + { + effectiveActive = timedStartupOverride == ManualOverrideKind.ForceActive; + reason = timedSource; + return; + } + + effectiveActive = baseShouldBeActive; + reason = baseShouldBeActive ? "Parameters/rules allow active" : "Parameters/rules inactive"; + } + + private ManualOverrideKind GetStartupTimedOverride(DateTime nowLocal, out string source) + { + source = null; + + bool activeLive = _startupActiveUntilLocal.HasValue && nowLocal < _startupActiveUntilLocal.Value; + bool inactiveLive = _startupInactiveUntilLocal.HasValue && nowLocal < _startupInactiveUntilLocal.Value; + + if (!activeLive && !inactiveLive) + { + return ManualOverrideKind.None; + } + + // If both exist, later end time wins because it was most recently specified in typical use. + if (activeLive && inactiveLive) + { + if (_startupInactiveUntilLocal.Value >= _startupActiveUntilLocal.Value) + { + source = "Startup inactivefor until " + _startupInactiveUntilLocal.Value.ToString("HH:mm:ss"); + return ManualOverrideKind.ForceInactive; + } + source = "Startup activefor until " + _startupActiveUntilLocal.Value.ToString("HH:mm:ss"); + return ManualOverrideKind.ForceActive; + } + + if (activeLive) + { + source = "Startup activefor until " + _startupActiveUntilLocal.Value.ToString("HH:mm:ss"); + return ManualOverrideKind.ForceActive; + } + + source = "Startup inactivefor until " + _startupInactiveUntilLocal.Value.ToString("HH:mm:ss"); + return ManualOverrideKind.ForceInactive; + } + + private EffectiveMode GetActiveMode() + { + if (_options.AllowScreenSaver) + { + return EffectiveMode.ActiveStesSystemOnly; + } + + if (_options.UseStes) + { + return EffectiveMode.ActiveStesSystemAndDisplay; + } + + return EffectiveMode.ActiveKeyPulse; + } + + private void ApplyActiveMode(EffectiveMode mode, DateTime nowUtc) + { + switch (mode) + { + case EffectiveMode.Inactive: + ClearExecutionState(); + return; + case EffectiveMode.ActiveKeyPulse: + PulseKeyIfDue(nowUtc); + return; + case EffectiveMode.ActiveStesSystemOnly: + RefreshStesIfDue(nowUtc, NativeMethods.ES_CONTINUOUS | NativeMethods.ES_SYSTEM_REQUIRED); + return; + case EffectiveMode.ActiveStesSystemAndDisplay: + RefreshStesIfDue(nowUtc, NativeMethods.ES_CONTINUOUS | NativeMethods.ES_SYSTEM_REQUIRED | NativeMethods.ES_DISPLAY_REQUIRED); + return; + } + } + + private void ClearExecutionState() + { + try + { + NativeMethods.SetThreadExecutionState(NativeMethods.ES_CONTINUOUS); + } + catch + { + // Ignore. + } + } + + private void RefreshStesIfDue(DateTime nowUtc, uint flags) + { + if ((nowUtc - _lastStesRefreshUtc).TotalSeconds < 15) + { + return; + } + + _lastStesRefreshUtc = nowUtc; + try + { + NativeMethods.SetThreadExecutionState(flags); + } + catch + { + // Ignore. + } + } + + private void PulseKeyIfDue(DateTime nowUtc) + { + if ((nowUtc - _lastPulseUtc).TotalSeconds < 59) + { + return; + } + + _lastPulseUtc = nowUtc; + SendConfiguredKeyPulse(); + } + + private void SendConfiguredKeyPulse() + { + byte vk = (byte)_options.VirtualKey; + bool pressShiftModifier = _options.KeyWithShift; + + try + { + if (pressShiftModifier && vk != 0x10 && vk != 0xA0) + { + NativeMethods.keybd_event(0x10, 0, 0, UIntPtr.Zero); + } + + NativeMethods.keybd_event(vk, 0, 0, UIntPtr.Zero); + NativeMethods.keybd_event(vk, 0, NativeMethods.KEYEVENTF_KEYUP, UIntPtr.Zero); + + if (pressShiftModifier && vk != 0x10 && vk != 0xA0) + { + NativeMethods.keybd_event(0x10, 0, NativeMethods.KEYEVENTF_KEYUP, UIntPtr.Zero); + } + } + catch + { + // Ignore. + } + } + + private void SetManualOverride(ManualOverrideKind kind, double? minutes) + { + _manualOverrideKind = kind; + if (minutes.HasValue) + { + _manualOverrideUntilLocal = DateTime.Now.AddMinutes(minutes.Value); + } + else + { + _manualOverrideUntilLocal = null; + } + + TickCore(); + } + + private void ToggleManual() + { + bool currentEffective = _hasLastEffectiveActive && _lastEffectiveActive; + SetManualOverride(currentEffective ? ManualOverrideKind.ForceInactive : ManualOverrideKind.ForceActive, null); + } + + private void RevertToParameters() + { + _manualOverrideKind = ManualOverrideKind.None; + _manualOverrideUntilLocal = null; + TickCore(); + } + + private void UpdateMenuState() + { + bool manualActive = _manualOverrideKind == ManualOverrideKind.ForceActive; + bool manualInactive = _manualOverrideKind == ManualOverrideKind.ForceInactive; + bool dialogVisible = _statusForm != null && !_statusForm.IsDisposed && _statusForm.Visible; + + _miActiveNow.Checked = manualActive && !_manualOverrideUntilLocal.HasValue; + _miInactiveNow.Checked = manualInactive && !_manualOverrideUntilLocal.HasValue; + _miTimed5.Checked = manualActive && IsMinutesApproximately(5); + _miTimed15.Checked = manualActive && IsMinutesApproximately(15); + _miTimed30.Checked = manualActive && IsMinutesApproximately(30); + _miTimed60.Checked = manualActive && IsMinutesApproximately(60); + _miTimed120.Checked = manualActive && IsMinutesApproximately(120); + _miTimedInactive5.Checked = manualInactive && IsMinutesApproximately(5); + _miTimedInactive15.Checked = manualInactive && IsMinutesApproximately(15); + _miTimedInactive30.Checked = manualInactive && IsMinutesApproximately(30); + + _miRevert.Enabled = _manualOverrideKind != ManualOverrideKind.None; + _miShowHide.Text = dialogVisible ? "Hide Status" : "Show Status"; + + string stateText = (_hasLastEffectiveActive && _lastEffectiveActive) ? "Active" : "Inactive"; + _miStatus.Text = "Status: " + stateText + " | " + GetMethodSummary(); + } + + private bool IsMinutesApproximately(int minutes) + { + if (!_manualOverrideUntilLocal.HasValue) + { + return false; + } + + double diff = (_manualOverrideUntilLocal.Value - DateTime.Now).TotalMinutes; + return diff > minutes - 1.1 && diff <= minutes + 0.1; + } + + private void UpdateStatusForm() + { + if (_statusForm == null || _statusForm.IsDisposed) + { + return; + } + + string state = (_hasLastEffectiveActive && _lastEffectiveActive) ? "ACTIVE" : "INACTIVE"; + string detail = "Reason: " + _lastReason + Environment.NewLine + + "Mode: " + GetMethodSummary() + + " | Key: " + FormatVirtualKey(_options.VirtualKey) + (_options.KeyWithShift ? " + Shift" : string.Empty); + + if (_manualOverrideKind != ManualOverrideKind.None) + { + detail += Environment.NewLine + "Manual override: " + _manualOverrideKind.ToString() + + (_manualOverrideUntilLocal.HasValue ? (" until " + _manualOverrideUntilLocal.Value.ToString("HH:mm:ss")) : string.Empty); + } + + string condition = _lastConditionSummary; + if (_options.CpuThresholdPercent.HasValue) + { + condition += Environment.NewLine + _lastCpuSummary; + } + if (_options.UnknownSwitches.Count > 0) + { + condition += Environment.NewLine + "Unknown switches ignored: " + string.Join(", ", _options.UnknownSwitches.ToArray()); + } + if (_options.AllowLocalCompat) + { + condition += Environment.NewLine + "Compatibility flag -allowlocal is recognized (no special handling required for local IPC)."; + } + + _statusForm.UpdateView(state, detail, condition); + } + + private void UpdateTrayTooltip(bool active, string reason) + { + string text = "Portable Caffeine: " + (active ? "Active" : "Inactive"); + if (!string.IsNullOrEmpty(reason)) + { + text += " | " + reason; + } + if (text.Length > 63) + { + text = text.Substring(0, 63); + } + _tray.Text = text; + } + + private void SetTrayIcon(bool active) + { + if (active) + { + _tray.Icon = _trayIconActive ?? SystemIcons.Information; + } + else + { + _tray.Icon = _options.NoHIcon + ? (_trayIconActive ?? SystemIcons.Information) + : (_trayIconInactive ?? SystemIcons.Warning); + } + } + + private void ShowNotify(string title, string text) + { + try + { + _tray.BalloonTipTitle = title; + _tray.BalloonTipText = string.IsNullOrEmpty(text) ? string.Empty : text; + _tray.BalloonTipIcon = ToolTipIcon.Info; + _tray.ShowBalloonTip(1500); + } + catch + { + // Ignore notification failures. + } + } + + private void OnIpcPayload(object sender, string payload) + { + if (string.IsNullOrEmpty(payload)) + { + return; + } + + string cmd = payload.Trim(); + if (cmd.Length == 0) + { + return; + } + + if (string.Equals(cmd, "EXIT_REPLACE", StringComparison.Ordinal)) + { + ExitApplication(); + return; + } + + if (cmd.StartsWith("APPCMD:", StringComparison.Ordinal)) + { + string name = cmd.Substring("APPCMD:".Length); + ApplyRemoteCommand(name); + return; + } + + if (string.Equals(cmd, "SHOW", StringComparison.Ordinal)) + { + ShowStatusForm(true); + } + } + + private void ApplyRemoteCommand(string name) + { + if (string.IsNullOrEmpty(name)) + { + return; + } + + if (string.Equals(name, "AppExit", StringComparison.OrdinalIgnoreCase)) + { + ExitApplication(); + return; + } + + if (string.Equals(name, "AppOn", StringComparison.OrdinalIgnoreCase)) + { + SetManualOverride(ManualOverrideKind.ForceActive, null); + return; + } + + if (string.Equals(name, "AppOff", StringComparison.OrdinalIgnoreCase)) + { + SetManualOverride(ManualOverrideKind.ForceInactive, null); + return; + } + + if (string.Equals(name, "AppToggle", StringComparison.OrdinalIgnoreCase)) + { + ToggleManual(); + return; + } + + if (string.Equals(name, "AppToggleShowDialog", StringComparison.OrdinalIgnoreCase)) + { + ToggleManual(); + ShowStatusForm(true); + return; + } + + if (string.Equals(name, "ShowDialog", StringComparison.OrdinalIgnoreCase)) + { + ShowStatusForm(true); + } + } + + private void ExitApplication() + { + if (_disposed) + { + return; + } + + ExitThread(); + } + + protected override void ExitThreadCore() + { + if (_disposed) + { + base.ExitThreadCore(); + return; + } + + _disposed = true; + + try + { + ClearExecutionState(); + } + catch + { + } + + try + { + if (_timer != null) + { + _timer.Stop(); + _timer.Dispose(); + } + } + catch + { + } + + try + { + if (_tray != null) + { + _tray.Visible = false; + _tray.Dispose(); + } + } + catch + { + } + + try + { + if (_trayIconActive != null) + { + _trayIconActive.Dispose(); + } + if (_trayIconInactive != null) + { + _trayIconInactive.Dispose(); + } + } + catch + { + } + + try + { + if (_ipcForm != null && !_ipcForm.IsDisposed) + { + _ipcForm.Close(); + _ipcForm.Dispose(); + } + } + catch + { + } + + try + { + if (_statusForm != null && !_statusForm.IsDisposed) + { + _statusForm.Close(); + _statusForm.Dispose(); + } + } + catch + { + } + + try + { + if (_cpuCounter != null) + { + _cpuCounter.Dispose(); + } + } + catch + { + } + + try + { + if (_instanceMutex != null) + { + _instanceMutex.ReleaseMutex(); + _instanceMutex.Dispose(); + } + } + catch + { + } + + base.ExitThreadCore(); + } + + private void TryInitCpuCounter() + { + try + { + _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); + _cpuCounter.NextValue(); + _cpuCounterPrimed = false; + _cpuCounterFailed = false; + } + catch + { + _cpuCounterFailed = true; + _cpuCounter = null; + } + } + + private bool IsCpuAtOrAboveThreshold(int threshold, out string summary) + { + summary = "CPU >= " + threshold.ToString(CultureInfo.InvariantCulture) + "% required"; + + if (_cpuCounterFailed) + { + summary = "CPU counter unavailable (condition not met)"; + return false; + } + + if (_cpuCounter == null) + { + TryInitCpuCounter(); + if (_cpuCounter == null) + { + summary = "CPU counter unavailable (condition not met)"; + return false; + } + } + + try + { + float next = _cpuCounter.NextValue(); + if (!_cpuCounterPrimed) + { + _cpuCounterPrimed = true; + summary = "CPU warmup sample..."; + return false; + } + _lastCpuPercent = next; + bool ok = next >= threshold; + summary = "CPU " + next.ToString("0.0", CultureInfo.InvariantCulture) + "% vs threshold " + threshold.ToString(CultureInfo.InvariantCulture) + "% (" + (ok ? "matched" : "below") + ")"; + return ok; + } + catch + { + _cpuCounterFailed = true; + summary = "CPU counter failed (condition not met)"; + return false; + } + } + + private bool ForegroundWindowMatches(string needle) + { + try + { + IntPtr hwnd = NativeMethods.GetForegroundWindow(); + if (hwnd == IntPtr.Zero) + { + return false; + } + + int length = NativeMethods.GetWindowTextLength(hwnd); + if (length <= 0) + { + return false; + } + + StringBuilder sb = new StringBuilder(length + 1); + NativeMethods.GetWindowText(hwnd, sb, sb.Capacity); + string title = sb.ToString(); + if (string.IsNullOrEmpty(title)) + { + return false; + } + + return title.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; + } + catch + { + return false; + } + } + + private bool IsOnAc(out string summary) + { + summary = "Power: unknown"; + try + { + NativeMethods.SYSTEM_POWER_STATUS sps; + if (!NativeMethods.GetSystemPowerStatus(out sps)) + { + summary = "Power status unavailable"; + return false; + } + + if (sps.ACLineStatus == 1) + { + summary = "Power: on AC"; + return true; + } + + if (sps.ACLineStatus == 0) + { + summary = "Power: on battery"; + return false; + } + + summary = "Power: AC unknown (allowing)"; + return true; + } + catch + { + summary = "Power status error"; + return false; + } + } + + private string GetMethodSummary() + { + EffectiveMode mode = GetActiveMode(); + switch (mode) + { + case EffectiveMode.ActiveKeyPulse: + return "KeyPulse"; + case EffectiveMode.ActiveStesSystemOnly: + return "STES(System)"; + case EffectiveMode.ActiveStesSystemAndDisplay: + return "STES(System+Display)"; + default: + return "Inactive"; + } + } + + private static string FormatVirtualKey(ushort vk) + { + return "0x" + vk.ToString("X2", CultureInfo.InvariantCulture); + } + } + + public static class Program + { + private const string MutexName = @"Local\PortableCaffeine.ZhornCompat"; + + public static int Run(string[] args) + { + Options options = CommandLine.Parse(args); + + if (options.ShowHelp) + { + Console.WriteLine(BuildUsageText()); + return 0; + } + + bool createdNew; + Mutex instanceMutex = null; + + try + { + instanceMutex = new Mutex(true, MutexName, out createdNew); + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to acquire single-instance mutex: " + ex.Message); + return 2; + } + + if (!createdNew) + { + try + { + if (options.ReplaceExisting) + { + IpcTransport.Send("EXIT_REPLACE"); + instanceMutex.Dispose(); + instanceMutex = null; + + DateTime deadline = DateTime.UtcNow.AddSeconds(8); + while (DateTime.UtcNow < deadline) + { + Thread.Sleep(200); + try + { + instanceMutex = new Mutex(true, MutexName, out createdNew); + if (createdNew) + { + break; + } + instanceMutex.Dispose(); + instanceMutex = null; + } + catch + { + // Keep retrying. + } + } + + if (!createdNew) + { + Console.Error.WriteLine("Could not replace existing instance (timeout)."); + return 3; + } + } + else + { + if (options.HasAnyAppCommand) + { + bool sent = IpcTransport.Send("APPCMD:" + options.AppCommand.ToString()); + return sent ? 0 : 1; + } + + bool showSent = IpcTransport.Send("SHOW"); + return showSent ? 0 : 1; + } + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to communicate with running instance: " + ex.Message); + return 1; + } + } + + if (createdNew && options.HasAnyAppCommand && !options.ReplaceExisting) + { + // App commands target an existing instance only; no-op if nothing is running. + try + { + instanceMutex.ReleaseMutex(); + } + catch + { + } + instanceMutex.Dispose(); + return 0; + } + + try + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + { + Console.Error.WriteLine("This script must run in an STA PowerShell host (Windows PowerShell 5.1 console is typically STA)."); + return 4; + } + + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + CaffeineAppContext ctx = null; + try + { + ctx = new CaffeineAppContext(options, instanceMutex); + instanceMutex = null; // ownership transferred + Application.Run(ctx); + } + finally + { + if (ctx != null) + { + ctx.Dispose(); + } + } + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.ToString()); + return 5; + } + finally + { + if (instanceMutex != null) + { + try + { + instanceMutex.ReleaseMutex(); + } + catch + { + } + + try + { + instanceMutex.Dispose(); + } + catch + { + } + } + } + } + + private static string BuildUsageText() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Portable Caffeine (Zhorn-compatible, PowerShell + embedded C#)"); + sb.AppendLine(); + sb.AppendLine("Common switches:"); + sb.AppendLine(" -on / -off Start active or inactive (default active)"); + sb.AppendLine(" -activefor:N Force active for N minutes"); + sb.AppendLine(" -inactivefor:N Force inactive for N minutes"); + sb.AppendLine(" -exitafter:N Exit app after N minutes"); + sb.AppendLine(" -showdlg Show status dialog"); + sb.AppendLine(" -ontaskbar Show status dialog in taskbar"); + sb.AppendLine(" -notify Balloon notifications on state changes"); + sb.AppendLine(" -lock Lock workstation after startup"); + sb.AppendLine(" -replace Replace a running instance"); + sb.AppendLine(" -watchwindow:TEXT Only active when foreground window title contains TEXT"); + sb.AppendLine(" -activeperiods:RANGES Active only within local time ranges (e.g. 08:00-12:00,13:00-17:00)"); + sb.AppendLine(" -activehours:RANGES Alias of -activeperiods"); + sb.AppendLine(" -onac Only active when on AC power"); + sb.AppendLine(" -cpu:N Only active when total CPU >= N percent"); + sb.AppendLine(" -stes Use SetThreadExecutionState instead of key pulse"); + sb.AppendLine(" -allowss Allow screensaver (uses STES system-only mode)"); + sb.AppendLine(" -useshift / -leftshift Use Shift / Left Shift instead of F15"); + sb.AppendLine(" -key:NN or -keypress:NN Use custom virtual key (decimal or hex, e.g. 0x7E)"); + sb.AppendLine(" -keyshift[:NN] Press key with Shift modifier; optional custom key"); + sb.AppendLine(" -appexit Ask running instance to exit"); + sb.AppendLine(" -appon / -appoff Ask running instance to force active/inactive"); + sb.AppendLine(" -apptoggle Ask running instance to toggle"); + sb.AppendLine(" -apptoggleshowdlg Toggle and show running instance status dialog"); + sb.AppendLine(" -nohicon Use same tray icon while inactive"); + sb.AppendLine(" -allowlocal Compatibility flag (recognized)"); + return sb.ToString(); + } + } +} +'@ + +if (-not ('PortableCaffeine.Program' -as [type])) { + Add-Type -AssemblyName System.Windows.Forms + Add-Type -AssemblyName System.Drawing + Add-Type -TypeDefinition $csharp -ReferencedAssemblies @( + 'System.dll', + 'System.Windows.Forms.dll', + 'System.Drawing.dll' + ) +} + +$exitCode = [PortableCaffeine.Program]::Run([string[]]$args) +exit $exitCode diff --git a/run-caffeine.bat b/run-caffeine.bat new file mode 100644 index 0000000..51d3579 --- /dev/null +++ b/run-caffeine.bat @@ -0,0 +1,13 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +set "PS_SCRIPT=%SCRIPT_DIR%caffeine.ps1" + +if not exist "%PS_SCRIPT%" ( + echo Could not find "%PS_SCRIPT%". + exit /b 1 +) + +powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File "%PS_SCRIPT%" %* +exit /b %ERRORLEVEL%