1983 lines
67 KiB
PowerShell
1983 lines
67 KiB
PowerShell
$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<TimeRange> ActivePeriods = new List<TimeRange>();
|
|
public RemoteCommandKind AppCommand = RemoteCommandKind.None;
|
|
public List<string> UnknownSwitches = new List<string>();
|
|
public List<string> RawArgs = new List<string>();
|
|
|
|
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<TimeRange> 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<TimeRange> ParseTimeRanges(string value)
|
|
{
|
|
List<TimeRange> list = new List<TimeRange>();
|
|
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<string> parts = new List<string>();
|
|
|
|
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";
|
|
|
|
[STAThread]
|
|
public static int Main(string[] args)
|
|
{
|
|
return Run(args);
|
|
}
|
|
|
|
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
|