ai-battery 0.1.6 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/bin/ai-battery-hud.js +40 -29
- package/bin/ai-battery-hud.ps1 +250 -23
- package/bin/ai-battery-run-win.js +219 -25
- package/bin/ai-battery.js +410 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Codex 86% │ 5h 18:09 │ 7d 82% ┃ Claude 4% │ 5h 18:10 │ 7d 71%
|
|
|
51
51
|
| `ai-battery --watch` | 지원 | 지원 | 지원 | 지원 | 터미널 안에서 주기적으로 갱신합니다. |
|
|
52
52
|
| Claude statusLine | 지원 | 지원 | 지원 | 지원 | Claude Code `statusLine`에 `node <script>` 명령을 저장합니다. |
|
|
53
53
|
| Codex terminal row | 지원 | 지원 | 지원 | 지원 | Windows는 `node-pty`/ConPTY가 있으면 reserved row, 없으면 overlay fallback을 사용합니다. WSL/Linux/macOS는 POSIX PTY와 `python3`를 사용합니다. |
|
|
54
|
-
| `ai-battery setup codex` | 지원 | 지원 | 지원 | 지원 | Windows는 `codex.cmd` wrapper, WSL/Linux/macOS는 POSIX shell wrapper를 설치합니다. |
|
|
54
|
+
| `ai-battery setup codex` | 지원 | 지원 | 지원 | 지원 | Codex `[tui].status_line`을 맞추고, Windows는 `codex.cmd` wrapper, WSL/Linux/macOS는 POSIX shell wrapper를 설치합니다. |
|
|
55
55
|
| `ai-battery hud` | 지원 | 지원 | 미지원 | 지원 | Windows/WSL은 PowerShell/WinForms HUD, macOS는 menu bar status item입니다. |
|
|
56
56
|
|
|
57
57
|
실행 중 감지(흰색/회색 표시)는 Linux/WSL은 `/proc`, macOS는 `ps`, Windows는 PowerShell 프로세스 목록을 사용합니다.
|
|
@@ -141,7 +141,7 @@ ai-battery uninstall claude
|
|
|
141
141
|
ai-battery uninstall hud
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
이 명령은 AI Battery가 관리 마커를 넣은 Codex wrapper, Claude `statusLine`, HUD/menu bar autostart와 실행 중인 HUD를 정리합니다. 다른 도구가 만든 `codex` 파일이나 Claude `statusLine`은 건드리지 않습니다. 이전 버전이나 `--force`로 기존 파일을 백업한 경우에는 가능한 한 원래 파일/symlink를 복원합니다. 현재 이미 AI Battery wrapper 안에서 실행 중인 Codex 세션의 terminal row는 그 세션을 종료해야 사라집니다.
|
|
144
|
+
이 명령은 AI Battery가 관리 마커를 넣은 Codex wrapper, Codex `[tui].status_line`, Claude `statusLine`, HUD/menu bar autostart와 실행 중인 HUD를 정리합니다. 다른 도구가 만든 `codex` 파일이나 Claude `statusLine`은 건드리지 않습니다. Codex config가 setup 이후 사용자가 수정한 상태라면 안전하게 그대로 둡니다. 이전 버전이나 `--force`로 기존 파일을 백업한 경우에는 가능한 한 원래 파일/symlink를 복원합니다. 현재 이미 AI Battery wrapper 안에서 실행 중인 Codex 세션의 terminal row는 그 세션을 종료해야 사라집니다.
|
|
145
145
|
|
|
146
146
|
최신 npm은 패키지의 uninstall lifecycle을 실행하지 않기 때문에 `npm uninstall ai-battery` 또는 `npm uninstall -g ai-battery`만으로는 외부 통합 지점을 자동 정리할 수 없습니다. 완전히 제거하려면 npm 패키지를 지우기 전에 먼저 실행하세요.
|
|
147
147
|
|
|
@@ -150,11 +150,11 @@ ai-battery uninstall
|
|
|
150
150
|
npm uninstall -g ai-battery
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
이미 npm 패키지를 먼저 지운 뒤라면, 다시 설치한 다음 `ai-battery uninstall`을 실행하거나 아래 항목을 직접 확인해 제거해야 합니다: AI Battery가 만든 Codex wrapper, shell rc의 `# >>> ai-battery setup >>>` block, Claude `statusLine`, HUD/menu bar autostart.
|
|
153
|
+
이미 npm 패키지를 먼저 지운 뒤라면, 다시 설치한 다음 `ai-battery uninstall`을 실행하거나 아래 항목을 직접 확인해 제거해야 합니다: AI Battery가 만든 Codex wrapper, shell rc의 `# >>> ai-battery setup >>>` block, Codex `~/.codex/config.toml`의 `[tui].status_line`, Claude `statusLine`, HUD/menu bar autostart.
|
|
154
154
|
|
|
155
155
|
## Setup
|
|
156
156
|
|
|
157
|
-
`setup`은 한 번만 실행합니다. Claude Code에는 statusLine hook을 설치하고, Codex에는 platform wrapper를 설치해서 이후에는 추가 명령 없이 원래처럼 실행하게 합니다.
|
|
157
|
+
`setup`은 한 번만 실행합니다. Claude Code에는 statusLine hook을 설치하고, Codex에는 기본 status line 구성과 platform wrapper를 설치해서 이후에는 추가 명령 없이 원래처럼 실행하게 합니다.
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
160
|
ai-battery setup
|
|
@@ -167,7 +167,7 @@ ai-battery setup claude
|
|
|
167
167
|
ai-battery setup codex
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
Codex wrapper는 기존 `codex` 명령을 직접 덮어쓰지 않습니다. `~/.local/bin`이 이미 PATH에서 원본 `codex`보다 앞에 있고 `~/.local/bin/codex`가 비어 있거나 AI Battery 관리 파일이면 그 위치에 wrapper를 둬서 바로 잡히게 합니다. 그렇지 않으면 `~/.local/share/ai-battery/bin/codex`에 관리형 wrapper를 만들고, 필요한 경우 셸 설정에 이 디렉터리를 PATH 앞쪽으로 추가합니다. `~/.local/bin/codex` 같은 공용 위치에 이미 다른 파일이 있으면 덮어쓰지 않습니다. 새 터미널부터 `codex`가 자동으로 AI Battery 하단 행과 함께 실행됩니다. 같은 터미널에서 이미 `codex`를 실행한 적이 있으면 셸 캐시 때문에 `hash -r`이 한 번 필요할 수 있고, PATH 추가가 필요한 경우에는 `setup` 출력에 표시되는 `source ...` 명령을 실행하세요.
|
|
170
|
+
Codex setup은 `~/.codex/config.toml`의 `[tui]`에 `model-with-reasoning`, `current-dir`, `git-branch` status line을 설정합니다. 기존 값이 있으면 uninstall 복구용으로 백업합니다. Codex wrapper는 기존 `codex` 명령을 직접 덮어쓰지 않습니다. `~/.local/bin`이 이미 PATH에서 원본 `codex`보다 앞에 있고 `~/.local/bin/codex`가 비어 있거나 AI Battery 관리 파일이면 그 위치에 wrapper를 둬서 바로 잡히게 합니다. 그렇지 않으면 `~/.local/share/ai-battery/bin/codex`에 관리형 wrapper를 만들고, 필요한 경우 셸 설정에 이 디렉터리를 PATH 앞쪽으로 추가합니다. `~/.local/bin/codex` 같은 공용 위치에 이미 다른 파일이 있으면 덮어쓰지 않습니다. 새 터미널부터 `codex`가 자동으로 AI Battery 하단 행과 함께 실행됩니다. 같은 터미널에서 이미 `codex`를 실행한 적이 있으면 셸 캐시 때문에 `hash -r`이 한 번 필요할 수 있고, PATH 추가가 필요한 경우에는 `setup` 출력에 표시되는 `source ...` 명령을 실행하세요.
|
|
171
171
|
|
|
172
172
|
Windows native `cmd`/PowerShell에서는 `codex.cmd` wrapper가 Windows runner를 실행합니다. `node-pty`가 설치되어 있으면 ConPTY로 Codex 화면을 한 줄 짧게 띄워 하단 row를 예약하고, `node-pty`가 없거나 로드되지 않으면 같은 콘솔의 하단에 overlay row를 다시 그리는 fallback을 사용합니다. Claude statusLine은 일반 `cmd`/PowerShell 프롬프트가 아니라 Claude Code 안에서만 표시됩니다.
|
|
173
173
|
|
|
@@ -192,7 +192,7 @@ ai-battery on all
|
|
|
192
192
|
|
|
193
193
|
## Codex Terminal Row
|
|
194
194
|
|
|
195
|
-
Codex 자체 status line
|
|
195
|
+
`ai-battery setup`은 Codex 자체 status line을 `모델/추론강도 · 워크스페이스 · git branch` 구성으로 맞춥니다. 사용량 표시는 별도의 `codex` wrapper가 담당하므로, 사용자는 원래처럼 `codex`만 입력하면 `ai-battery-run`이 내부에서 Codex를 한 줄 짧은 PTY 안에서 실행합니다.
|
|
196
196
|
|
|
197
197
|
```bash
|
|
198
198
|
codex
|
package/bin/ai-battery-hud.js
CHANGED
|
@@ -74,6 +74,23 @@ function buildStartProcessCommand(filePath, args) {
|
|
|
74
74
|
return `Start-Process -WindowStyle Hidden -FilePath ${psSingleQuote(filePath)} -ArgumentList ${psSingleQuote(commandLine)}`;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function psInvocationArg(value) {
|
|
78
|
+
const text = String(value);
|
|
79
|
+
if (/^-[A-Za-z][A-Za-z0-9]*$/.test(text)) return text;
|
|
80
|
+
return psSingleQuote(text);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function powerShellCommandArgs(scriptPath, scriptArgs) {
|
|
84
|
+
const invocation = `& ${psSingleQuote(scriptPath)} ${scriptArgs.map(psInvocationArg).join(" ")}`;
|
|
85
|
+
return [
|
|
86
|
+
"-NoProfile",
|
|
87
|
+
"-ExecutionPolicy",
|
|
88
|
+
"Bypass",
|
|
89
|
+
"-Command",
|
|
90
|
+
invocation
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
function sleepSync(ms) {
|
|
78
95
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
79
96
|
}
|
|
@@ -413,6 +430,7 @@ const envPrefix = useWsl ? wslEnvPrefix() : "";
|
|
|
413
430
|
const batteryCommand = configuredCommand || (useWsl
|
|
414
431
|
? `${envPrefix ? `${envPrefix} ` : ""}HOME=${shellQuote(os.homedir())} ${shellQuote(nodePath)} ${shellQuote(batteryJs)} --json`
|
|
415
432
|
: `${winArgQuote(nodePath)} ${winArgQuote(batteryJs)} --json`);
|
|
433
|
+
const batteryCommandBase64 = Buffer.from(batteryCommand, "utf8").toString("base64");
|
|
416
434
|
|
|
417
435
|
const AUTOSTART_REG_PATH = "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
418
436
|
const AUTOSTART_REG_NAME = "AiBatteryHud";
|
|
@@ -430,7 +448,7 @@ function hudProcessStatus() {
|
|
|
430
448
|
+ "$_.Name -match '^(powershell|pwsh)' -and "
|
|
431
449
|
+ "$_.CommandLine -like '*ai-battery-hud.ps1*' -and "
|
|
432
450
|
+ "$_.CommandLine -notlike '*Start-Process*' }; "
|
|
433
|
-
+ "if ($hud) { 'running (PID ' +
|
|
451
|
+
+ "if ($hud) { $p = @($hud)[0]; $source = if ($p.CommandLine -like '*-UseWsl*') { 'WSL' } else { 'Windows' }; 'running (' + $source + ', PID ' + $p.ProcessId + ')' } else { 'stopped' }";
|
|
434
452
|
const result = runPowerShell(query);
|
|
435
453
|
return (result.stdout || "").trim() || "unknown";
|
|
436
454
|
}
|
|
@@ -448,26 +466,17 @@ function autostartEnable() {
|
|
|
448
466
|
// autostart.ps1 refreshes the local copy of the HUD script when the source
|
|
449
467
|
// is reachable (the WSL share is not mounted until the distro starts), then
|
|
450
468
|
// launches the copy so sign-in start never depends on WSL being up. The HUD
|
|
451
|
-
// must run as a separate
|
|
452
|
-
// stop/status and
|
|
453
|
-
const hudArgLiterals = [
|
|
454
|
-
"'-NoProfile'",
|
|
455
|
-
"'-ExecutionPolicy'",
|
|
456
|
-
"'Bypass'",
|
|
457
|
-
"'-File'",
|
|
458
|
-
"('\"' + $hud + '\"')",
|
|
459
|
-
"'-BatteryCommand'",
|
|
460
|
-
"('\"' + ($battery -replace '\"', '\\\"') + '\"')"
|
|
461
|
-
];
|
|
462
|
-
if (useWsl) hudArgLiterals.push("'-UseWsl'");
|
|
463
|
-
|
|
469
|
+
// must run as a separate PowerShell process whose command line contains
|
|
470
|
+
// ai-battery-hud.ps1: stop/status and single-instance cleanup match it.
|
|
464
471
|
const autostartScript = [
|
|
465
472
|
"# Generated by: ai-battery hud autostart on",
|
|
466
473
|
`$src = ${psSingleQuote(hudScript)}`,
|
|
467
474
|
"$hud = Join-Path $PSScriptRoot 'ai-battery-hud.ps1'",
|
|
468
475
|
"try { Copy-Item $src $hud -Force -ErrorAction Stop } catch { }",
|
|
469
476
|
`$battery = ${psSingleQuote(batteryCommand)}`,
|
|
470
|
-
|
|
477
|
+
"$batteryB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($battery))",
|
|
478
|
+
"$invoke = '& ' + \"'\" + ($hud -replace \"'\", \"''\") + \"'\" + ' -BatteryCommandBase64 ' + $batteryB64" + (useWsl ? " + ' -UseWsl'" : ""),
|
|
479
|
+
"$argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ('\"' + ($invoke -replace '\"', '\\\"') + '\"')) -join ' '",
|
|
471
480
|
"Start-Process -WindowStyle Hidden -FilePath 'powershell.exe' -ArgumentList $argList",
|
|
472
481
|
""
|
|
473
482
|
].join("\r\n");
|
|
@@ -527,29 +536,26 @@ const readyPath = (!useWsl && process.platform === "win32" && !foreground && !on
|
|
|
527
536
|
? path.join(os.tmpdir(), `ai-battery-hud-ready-${process.pid}-${Date.now()}.json`)
|
|
528
537
|
: null;
|
|
529
538
|
|
|
530
|
-
const
|
|
531
|
-
"-
|
|
532
|
-
|
|
533
|
-
"Bypass",
|
|
534
|
-
"-File",
|
|
535
|
-
hudScript,
|
|
536
|
-
"-BatteryCommand",
|
|
537
|
-
batteryCommand,
|
|
539
|
+
const hudScriptArgs = [
|
|
540
|
+
"-BatteryCommandBase64",
|
|
541
|
+
batteryCommandBase64,
|
|
538
542
|
...filteredArgs
|
|
539
543
|
];
|
|
540
544
|
|
|
541
545
|
if (readyPath) {
|
|
542
|
-
|
|
546
|
+
hudScriptArgs.push("-ReadyPath", readyPath);
|
|
543
547
|
}
|
|
544
548
|
|
|
545
549
|
if (initialJson) {
|
|
546
|
-
|
|
550
|
+
hudScriptArgs.push("-InitialJsonBase64", Buffer.from(initialJson, "utf8").toString("base64"));
|
|
547
551
|
}
|
|
548
552
|
|
|
549
553
|
if (useWsl) {
|
|
550
|
-
|
|
554
|
+
hudScriptArgs.push("-UseWsl");
|
|
551
555
|
}
|
|
552
556
|
|
|
557
|
+
const psArgs = powerShellCommandArgs(hudScript, hudScriptArgs);
|
|
558
|
+
|
|
553
559
|
if (foreground || once || stop) {
|
|
554
560
|
const result = spawnSync(powershell, psArgs, { stdio: "inherit", windowsHide: true });
|
|
555
561
|
if (stop && (result.status ?? 0) === 0) {
|
|
@@ -571,12 +577,17 @@ if (useWsl) {
|
|
|
571
577
|
});
|
|
572
578
|
if (start.status !== 0) process.exit(start.status ?? 1);
|
|
573
579
|
} else {
|
|
574
|
-
const
|
|
575
|
-
|
|
580
|
+
const start = spawnSync(powershell, [
|
|
581
|
+
"-NoProfile",
|
|
582
|
+
"-ExecutionPolicy",
|
|
583
|
+
"Bypass",
|
|
584
|
+
"-Command",
|
|
585
|
+
buildStartProcessCommand("powershell.exe", psArgs)
|
|
586
|
+
], {
|
|
576
587
|
stdio: "ignore",
|
|
577
588
|
windowsHide: true
|
|
578
589
|
});
|
|
579
|
-
|
|
590
|
+
if (start.status !== 0) process.exit(start.status ?? 1);
|
|
580
591
|
}
|
|
581
592
|
|
|
582
593
|
if (readyPath && !waitForFile(readyPath)) {
|
package/bin/ai-battery-hud.ps1
CHANGED
|
@@ -4,6 +4,7 @@ param(
|
|
|
4
4
|
[ValidateSet("tray", "statusline", "floating")]
|
|
5
5
|
[string]$Mode = $(if ($env:AI_BATTERY_HUD_MODE) { $env:AI_BATTERY_HUD_MODE } elseif ($env:CLAUDEX_BATTERY_HUD_MODE) { $env:CLAUDEX_BATTERY_HUD_MODE } else { "floating" }),
|
|
6
6
|
[string]$BatteryCommand = $(if ($env:AI_BATTERY_COMMAND) { $env:AI_BATTERY_COMMAND } elseif ($env:CLAUDEX_BATTERY_COMMAND) { $env:CLAUDEX_BATTERY_COMMAND } else { "ai-battery --json" }),
|
|
7
|
+
[string]$BatteryCommandBase64 = "",
|
|
7
8
|
[string]$InitialJsonBase64 = "",
|
|
8
9
|
[int]$Width = 282,
|
|
9
10
|
[double]$Opacity = $(if ($env:AI_BATTERY_HUD_OPACITY) { [double]$env:AI_BATTERY_HUD_OPACITY } elseif ($env:CLAUDEX_BATTERY_HUD_OPACITY) { [double]$env:CLAUDEX_BATTERY_HUD_OPACITY } else { 1.0 }),
|
|
@@ -20,6 +21,14 @@ $ErrorActionPreference = "Stop"
|
|
|
20
21
|
Add-Type -AssemblyName System.Drawing
|
|
21
22
|
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
|
|
22
23
|
|
|
24
|
+
if (-not [string]::IsNullOrWhiteSpace($BatteryCommandBase64)) {
|
|
25
|
+
try {
|
|
26
|
+
$BatteryCommand = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($BatteryCommandBase64))
|
|
27
|
+
} catch {
|
|
28
|
+
# Fall back to BatteryCommand/env defaults; the HUD can still show an error row.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
function Stop-ExistingHudProcesses {
|
|
24
33
|
$currentPid = $PID
|
|
25
34
|
Get-CimInstance Win32_Process |
|
|
@@ -89,6 +98,11 @@ function Get-HudStatePath {
|
|
|
89
98
|
return Join-Path $root "hud-position.json"
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
function Get-HudSourceId {
|
|
102
|
+
if ($UseWsl) { return "wsl" }
|
|
103
|
+
return "windows"
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
function Get-HudSnapshotPath {
|
|
93
107
|
$root = if ($env:LOCALAPPDATA) {
|
|
94
108
|
Join-Path $env:LOCALAPPDATA "ai-battery"
|
|
@@ -96,7 +110,7 @@ function Get-HudSnapshotPath {
|
|
|
96
110
|
Join-Path $env:TEMP "ai-battery"
|
|
97
111
|
}
|
|
98
112
|
New-Item -ItemType Directory -Force -Path $root | Out-Null
|
|
99
|
-
return Join-Path $root "hud-snapshot.json"
|
|
113
|
+
return Join-Path $root "hud-snapshot-$(Get-HudSourceId).json"
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
function Get-LegacyHudStatePath {
|
|
@@ -206,6 +220,7 @@ function Read-InitialHudSnapshot {
|
|
|
206
220
|
function Write-HudSnapshot($Snapshot) {
|
|
207
221
|
try {
|
|
208
222
|
if (-not $Snapshot) { return }
|
|
223
|
+
$Snapshot | Add-Member -NotePropertyName "hudSource" -NotePropertyValue (Get-HudSourceId) -Force
|
|
209
224
|
$Snapshot | ConvertTo-Json -Depth 20 -Compress | Set-Content -Encoding UTF8 -Path (Get-HudSnapshotPath)
|
|
210
225
|
} catch {
|
|
211
226
|
# The HUD can still run without a warm-start cache.
|
|
@@ -231,12 +246,36 @@ public struct AiBatteryMonitorInfo {
|
|
|
231
246
|
public AiBatteryRect Work;
|
|
232
247
|
public int Flags;
|
|
233
248
|
}
|
|
249
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
250
|
+
public struct AiBatteryPowerThrottlingState {
|
|
251
|
+
public uint Version;
|
|
252
|
+
public uint ControlMask;
|
|
253
|
+
public uint StateMask;
|
|
254
|
+
}
|
|
255
|
+
public delegate void AiBatteryWinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
|
234
256
|
public static class AiBatteryNative {
|
|
257
|
+
public const int ProcessPowerThrottling = 4;
|
|
258
|
+
public const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
|
|
259
|
+
public const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
|
|
260
|
+
public const int OBJID_WINDOW = 0;
|
|
235
261
|
public const int GWL_EXSTYLE = -20;
|
|
236
262
|
public const int WS_EX_TRANSPARENT = 0x20;
|
|
237
263
|
public const int WS_EX_TOOLWINDOW = 0x80;
|
|
238
264
|
public const int WS_EX_NOACTIVATE = 0x08000000;
|
|
265
|
+
public const int SW_HIDE = 0;
|
|
266
|
+
public const int SW_SHOWNOACTIVATE = 4;
|
|
239
267
|
public const UInt32 MONITOR_DEFAULTTONEAREST = 2;
|
|
268
|
+
public const UInt32 GW_HWNDNEXT = 2;
|
|
269
|
+
public const int DWMWA_CLOAKED = 14;
|
|
270
|
+
public const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
|
|
271
|
+
public const uint EVENT_OBJECT_SHOW = 0x8002;
|
|
272
|
+
public const uint EVENT_OBJECT_HIDE = 0x8003;
|
|
273
|
+
public const uint EVENT_OBJECT_REORDER = 0x8004;
|
|
274
|
+
public const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
|
|
275
|
+
public const uint EVENT_OBJECT_CLOAKED = 0x8017;
|
|
276
|
+
public const uint EVENT_OBJECT_UNCLOAKED = 0x8018;
|
|
277
|
+
public const uint WINEVENT_OUTOFCONTEXT = 0x0000;
|
|
278
|
+
public const uint WINEVENT_SKIPOWNPROCESS = 0x0002;
|
|
240
279
|
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
|
241
280
|
public const UInt32 SWP_NOSIZE = 0x0001;
|
|
242
281
|
public const UInt32 SWP_NOMOVE = 0x0002;
|
|
@@ -250,6 +289,26 @@ public static class AiBatteryNative {
|
|
|
250
289
|
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
|
251
290
|
[DllImport("user32.dll")]
|
|
252
291
|
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, UInt32 uFlags);
|
|
292
|
+
[DllImport("user32.dll")]
|
|
293
|
+
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
294
|
+
[DllImport("user32.dll")]
|
|
295
|
+
public static extern IntPtr GetWindow(IntPtr hWnd, UInt32 uCmd);
|
|
296
|
+
[DllImport("user32.dll")]
|
|
297
|
+
public static extern IntPtr GetTopWindow(IntPtr hWnd);
|
|
298
|
+
[DllImport("user32.dll")]
|
|
299
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
300
|
+
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
|
301
|
+
public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount);
|
|
302
|
+
[DllImport("dwmapi.dll")]
|
|
303
|
+
public static extern int DwmGetWindowAttribute(IntPtr hWnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
|
304
|
+
[DllImport("kernel32.dll")]
|
|
305
|
+
public static extern IntPtr GetCurrentProcess();
|
|
306
|
+
[DllImport("kernel32.dll", SetLastError=true)]
|
|
307
|
+
public static extern bool SetProcessInformation(IntPtr hProcess, int ProcessInformationClass, ref AiBatteryPowerThrottlingState ProcessInformation, int ProcessInformationSize);
|
|
308
|
+
[DllImport("user32.dll")]
|
|
309
|
+
public static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, AiBatteryWinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
|
310
|
+
[DllImport("user32.dll")]
|
|
311
|
+
public static extern bool UnhookWinEvent(IntPtr hWinEventHook);
|
|
253
312
|
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
|
254
313
|
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
|
255
314
|
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
|
@@ -266,6 +325,27 @@ public static class AiBatteryNative {
|
|
|
266
325
|
"@
|
|
267
326
|
Add-Type -TypeDefinition $nativeCode
|
|
268
327
|
|
|
328
|
+
function Disable-ProcessPowerThrottling {
|
|
329
|
+
# When a fullscreen app is in the foreground, Windows applies EcoQoS power
|
|
330
|
+
# throttling to background processes like this HUD, which stretches the
|
|
331
|
+
# WinForms timer from 250ms out to several seconds. That made the HUD linger
|
|
332
|
+
# over a fullscreen window for 3-5s before hiding (while unhide, which happens
|
|
333
|
+
# after throttling lifts, stayed instant). Opt the process out so the
|
|
334
|
+
# fullscreen check keeps firing on time even while backgrounded.
|
|
335
|
+
try {
|
|
336
|
+
$state = New-Object AiBatteryPowerThrottlingState
|
|
337
|
+
$state.Version = [AiBatteryNative]::PROCESS_POWER_THROTTLING_CURRENT_VERSION
|
|
338
|
+
$state.ControlMask = [AiBatteryNative]::PROCESS_POWER_THROTTLING_EXECUTION_SPEED
|
|
339
|
+
$state.StateMask = 0
|
|
340
|
+
$size = [System.Runtime.InteropServices.Marshal]::SizeOf([type][AiBatteryPowerThrottlingState])
|
|
341
|
+
return [AiBatteryNative]::SetProcessInformation([AiBatteryNative]::GetCurrentProcess(), [AiBatteryNative]::ProcessPowerThrottling, [ref]$state, $size)
|
|
342
|
+
} catch {
|
|
343
|
+
# Older Windows builds lack ProcessPowerThrottling; the HUD still works.
|
|
344
|
+
return $false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
$script:powerThrottlingDisabled = Disable-ProcessPowerThrottling
|
|
348
|
+
|
|
269
349
|
function Invoke-AiBatteryJson {
|
|
270
350
|
if ($UseWsl) {
|
|
271
351
|
$output = & wsl.exe bash -lc "$BatteryCommand 2>/dev/null"
|
|
@@ -1154,7 +1234,7 @@ function Show-HitForm {
|
|
|
1154
1234
|
$hitForm.Show()
|
|
1155
1235
|
}
|
|
1156
1236
|
$style = [AiBatteryNative]::GetWindowLong($hitForm.Handle, [AiBatteryNative]::GWL_EXSTYLE)
|
|
1157
|
-
$style = $style -bor [AiBatteryNative]::WS_EX_TOOLWINDOW
|
|
1237
|
+
$style = $style -bor [AiBatteryNative]::WS_EX_TOOLWINDOW -bor [AiBatteryNative]::WS_EX_NOACTIVATE
|
|
1158
1238
|
[AiBatteryNative]::SetWindowLong($hitForm.Handle, [AiBatteryNative]::GWL_EXSTYLE, $style) | Out-Null
|
|
1159
1239
|
}
|
|
1160
1240
|
|
|
@@ -1274,21 +1354,89 @@ function Test-RectCoversMonitor($WindowRect, $MonitorRect) {
|
|
|
1274
1354
|
)
|
|
1275
1355
|
}
|
|
1276
1356
|
|
|
1277
|
-
function Test-
|
|
1278
|
-
if (-not $
|
|
1279
|
-
$
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1357
|
+
function Test-RectIsSubstantialOnMonitor($WindowRect, $MonitorRect) {
|
|
1358
|
+
if (-not $WindowRect -or -not $MonitorRect) { return $false }
|
|
1359
|
+
$left = [math]::Max([int]$WindowRect.Left, [int]$MonitorRect.Left)
|
|
1360
|
+
$top = [math]::Max([int]$WindowRect.Top, [int]$MonitorRect.Top)
|
|
1361
|
+
$right = [math]::Min([int]$WindowRect.Right, [int]$MonitorRect.Right)
|
|
1362
|
+
$bottom = [math]::Min([int]$WindowRect.Bottom, [int]$MonitorRect.Bottom)
|
|
1363
|
+
$width = [math]::Max(0, $right - $left)
|
|
1364
|
+
$height = [math]::Max(0, $bottom - $top)
|
|
1365
|
+
$monitorArea = [math]::Max(1, [int]$MonitorRect.Width * [int]$MonitorRect.Height)
|
|
1366
|
+
$windowArea = $width * $height
|
|
1367
|
+
return (($windowArea / $monitorArea) -ge 0.20)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function Test-HudWindowHandle([IntPtr]$Handle) {
|
|
1371
|
+
if ($Handle -eq [IntPtr]::Zero) { return $false }
|
|
1372
|
+
if ($form -and -not $form.IsDisposed -and $Handle -eq $form.Handle) { return $true }
|
|
1373
|
+
if ($hitForm -and -not $hitForm.IsDisposed -and $Handle -eq $hitForm.Handle) { return $true }
|
|
1374
|
+
return $false
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function Get-WindowClassName([IntPtr]$Handle) {
|
|
1378
|
+
if ($Handle -eq [IntPtr]::Zero) { return "" }
|
|
1379
|
+
$builder = New-Object System.Text.StringBuilder 256
|
|
1380
|
+
[AiBatteryNative]::GetClassName($Handle, $builder, $builder.Capacity) | Out-Null
|
|
1381
|
+
return $builder.ToString()
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function Test-WindowCloaked([IntPtr]$Handle) {
|
|
1385
|
+
if ($Handle -eq [IntPtr]::Zero) { return $false }
|
|
1386
|
+
$value = 0
|
|
1387
|
+
try {
|
|
1388
|
+
$hr = [AiBatteryNative]::DwmGetWindowAttribute($Handle, [AiBatteryNative]::DWMWA_CLOAKED, [ref]$value, 4)
|
|
1389
|
+
} catch {
|
|
1390
|
+
return $false
|
|
1391
|
+
}
|
|
1392
|
+
if ($hr -ne 0) { return $false }
|
|
1393
|
+
return ($value -ne 0)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
# Desktop and shell surfaces span the monitor but must never hide the HUD.
|
|
1397
|
+
$script:hudSkipWindowClasses = @(
|
|
1398
|
+
"Shell_TrayWnd",
|
|
1399
|
+
"Shell_SecondaryTrayWnd",
|
|
1400
|
+
"NotifyIconOverflowWindow",
|
|
1401
|
+
"Progman",
|
|
1402
|
+
"WorkerW"
|
|
1403
|
+
)
|
|
1283
1404
|
|
|
1405
|
+
function Test-FullscreenOnHudMonitor {
|
|
1406
|
+
if (-not $form -or $form.IsDisposed) { return $false }
|
|
1284
1407
|
$hudMonitor = [AiBatteryNative]::MonitorFromWindow($form.Handle, [AiBatteryNative]::MONITOR_DEFAULTTONEAREST)
|
|
1285
|
-
$
|
|
1286
|
-
|
|
1287
|
-
if (
|
|
1408
|
+
if ($hudMonitor -eq [IntPtr]::Zero) { return $false }
|
|
1409
|
+
$monitorRect = Get-MonitorRectObject $hudMonitor
|
|
1410
|
+
if (-not $monitorRect) { return $false }
|
|
1411
|
+
|
|
1412
|
+
# Walk the Z-order from the top down. Small transient overlays (volume, IME,
|
|
1413
|
+
# Game Bar, browser controls) are ignored so we can still see the fullscreen
|
|
1414
|
+
# window beneath them. A substantial normal window stops the search: any
|
|
1415
|
+
# fullscreen window below it is not the visible fullscreen surface.
|
|
1416
|
+
$handle = [AiBatteryNative]::GetTopWindow([IntPtr]::Zero)
|
|
1417
|
+
for ($scanned = 0; $scanned -lt 400 -and $handle -ne [IntPtr]::Zero; $scanned += 1) {
|
|
1418
|
+
$next = [AiBatteryNative]::GetWindow($handle, [AiBatteryNative]::GW_HWNDNEXT)
|
|
1419
|
+
# Filter cheapest-first: most windows live on another monitor, so skip the
|
|
1420
|
+
# DWM cloak and class-name lookups until a window is on the HUD monitor.
|
|
1421
|
+
if ((-not (Test-HudWindowHandle $handle)) -and
|
|
1422
|
+
[AiBatteryNative]::IsWindowVisible($handle) -and
|
|
1423
|
+
([AiBatteryNative]::MonitorFromWindow($handle, [AiBatteryNative]::MONITOR_DEFAULTTONEAREST) -eq $hudMonitor) -and
|
|
1424
|
+
(-not (Test-WindowCloaked $handle)) -and
|
|
1425
|
+
($script:hudSkipWindowClasses -notcontains (Get-WindowClassName $handle))) {
|
|
1426
|
+
$windowRect = Get-WindowRectObject $handle
|
|
1427
|
+
if ($windowRect -and $windowRect.Width -gt 0 -and $windowRect.Height -gt 0) {
|
|
1428
|
+
if (Test-RectCoversMonitor $windowRect $monitorRect) {
|
|
1429
|
+
return $true
|
|
1430
|
+
}
|
|
1431
|
+
if (Test-RectIsSubstantialOnMonitor $windowRect $monitorRect) {
|
|
1432
|
+
return $false
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
$handle = $next
|
|
1437
|
+
}
|
|
1288
1438
|
|
|
1289
|
-
|
|
1290
|
-
$monitorRect = Get-MonitorRectObject $foregroundMonitor
|
|
1291
|
-
return Test-RectCoversMonitor $windowRect $monitorRect
|
|
1439
|
+
return $false
|
|
1292
1440
|
}
|
|
1293
1441
|
|
|
1294
1442
|
function Set-TaskbarPosition {
|
|
@@ -1418,9 +1566,11 @@ function Set-HudHiddenForFullscreen([bool]$Hidden) {
|
|
|
1418
1566
|
$script:hudHiddenForFullscreen = $Hidden
|
|
1419
1567
|
if ($Hidden) {
|
|
1420
1568
|
if ($hitForm -and -not $hitForm.IsDisposed -and $hitForm.Visible) {
|
|
1569
|
+
[AiBatteryNative]::ShowWindow($hitForm.Handle, [AiBatteryNative]::SW_HIDE) | Out-Null
|
|
1421
1570
|
$hitForm.Hide()
|
|
1422
1571
|
}
|
|
1423
1572
|
if ($form -and -not $form.IsDisposed -and $form.Visible) {
|
|
1573
|
+
[AiBatteryNative]::ShowWindow($form.Handle, [AiBatteryNative]::SW_HIDE) | Out-Null
|
|
1424
1574
|
$form.Hide()
|
|
1425
1575
|
}
|
|
1426
1576
|
return
|
|
@@ -1428,21 +1578,95 @@ function Set-HudHiddenForFullscreen([bool]$Hidden) {
|
|
|
1428
1578
|
|
|
1429
1579
|
if ($form -and -not $form.IsDisposed -and -not $form.Visible) {
|
|
1430
1580
|
$form.Show()
|
|
1581
|
+
[AiBatteryNative]::ShowWindow($form.Handle, [AiBatteryNative]::SW_SHOWNOACTIVATE) | Out-Null
|
|
1431
1582
|
}
|
|
1432
1583
|
Show-HitForm
|
|
1433
1584
|
Sync-HitFormBounds
|
|
1434
1585
|
}
|
|
1435
1586
|
|
|
1587
|
+
$script:fullscreenClearCount = 0
|
|
1588
|
+
|
|
1436
1589
|
function Update-HudVisibilityForFullscreen {
|
|
1437
1590
|
if (-not $form -or $form.IsDisposed) { return }
|
|
1438
|
-
if (Test-
|
|
1591
|
+
if (Test-FullscreenOnHudMonitor) {
|
|
1592
|
+
$script:fullscreenClearCount = 0
|
|
1439
1593
|
Set-HudHiddenForFullscreen $true
|
|
1440
1594
|
return
|
|
1441
1595
|
}
|
|
1596
|
+
if ($script:hudHiddenForFullscreen) {
|
|
1597
|
+
$script:fullscreenClearCount += 1
|
|
1598
|
+
if ($script:fullscreenClearCount -lt 2) { return }
|
|
1599
|
+
}
|
|
1600
|
+
$script:fullscreenClearCount = 0
|
|
1442
1601
|
Set-HudHiddenForFullscreen $false
|
|
1443
1602
|
Ensure-HudTopMost
|
|
1444
1603
|
}
|
|
1445
1604
|
|
|
1605
|
+
$script:fullscreenCheckPending = $false
|
|
1606
|
+
|
|
1607
|
+
function Request-FullscreenCheck {
|
|
1608
|
+
if (-not $form -or $form.IsDisposed) { return }
|
|
1609
|
+
if ($form.InvokeRequired) {
|
|
1610
|
+
if ($script:fullscreenCheckPending) { return }
|
|
1611
|
+
$script:fullscreenCheckPending = $true
|
|
1612
|
+
try {
|
|
1613
|
+
$form.BeginInvoke([Action]{
|
|
1614
|
+
$script:fullscreenCheckPending = $false
|
|
1615
|
+
Update-HudVisibilityForFullscreen
|
|
1616
|
+
}) | Out-Null
|
|
1617
|
+
} catch {
|
|
1618
|
+
$script:fullscreenCheckPending = $false
|
|
1619
|
+
}
|
|
1620
|
+
return
|
|
1621
|
+
}
|
|
1622
|
+
Update-HudVisibilityForFullscreen
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
$script:fullscreenEventCallback = $null
|
|
1626
|
+
$script:fullscreenEventHooks = @()
|
|
1627
|
+
|
|
1628
|
+
function Start-FullscreenEventHooks {
|
|
1629
|
+
if ($script:fullscreenEventCallback) { return }
|
|
1630
|
+
$script:fullscreenEventCallback = [AiBatteryWinEventDelegate]{
|
|
1631
|
+
param($hook, $eventType, $hwnd, $idObject, $idChild, $eventThread, $eventTime)
|
|
1632
|
+
if ($idObject -ne [AiBatteryNative]::OBJID_WINDOW) { return }
|
|
1633
|
+
Request-FullscreenCheck
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
$events = @(
|
|
1637
|
+
[AiBatteryNative]::EVENT_SYSTEM_FOREGROUND,
|
|
1638
|
+
[AiBatteryNative]::EVENT_OBJECT_SHOW,
|
|
1639
|
+
[AiBatteryNative]::EVENT_OBJECT_HIDE,
|
|
1640
|
+
[AiBatteryNative]::EVENT_OBJECT_REORDER,
|
|
1641
|
+
[AiBatteryNative]::EVENT_OBJECT_LOCATIONCHANGE,
|
|
1642
|
+
[AiBatteryNative]::EVENT_OBJECT_CLOAKED,
|
|
1643
|
+
[AiBatteryNative]::EVENT_OBJECT_UNCLOAKED
|
|
1644
|
+
)
|
|
1645
|
+
$flags = [AiBatteryNative]::WINEVENT_OUTOFCONTEXT -bor [AiBatteryNative]::WINEVENT_SKIPOWNPROCESS
|
|
1646
|
+
foreach ($eventId in $events) {
|
|
1647
|
+
try {
|
|
1648
|
+
$hook = [AiBatteryNative]::SetWinEventHook($eventId, $eventId, [IntPtr]::Zero, $script:fullscreenEventCallback, 0, 0, $flags)
|
|
1649
|
+
if ($hook -ne [IntPtr]::Zero) {
|
|
1650
|
+
$script:fullscreenEventHooks += $hook
|
|
1651
|
+
}
|
|
1652
|
+
} catch {
|
|
1653
|
+
# Polling still covers older or restricted Windows environments.
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function Stop-FullscreenEventHooks {
|
|
1659
|
+
foreach ($hook in $script:fullscreenEventHooks) {
|
|
1660
|
+
try {
|
|
1661
|
+
[AiBatteryNative]::UnhookWinEvent($hook) | Out-Null
|
|
1662
|
+
} catch {
|
|
1663
|
+
# The hook may already be gone during shutdown.
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
$script:fullscreenEventHooks = @()
|
|
1667
|
+
$script:fullscreenEventCallback = $null
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1446
1670
|
function Set-HudBatteryImage($Box, [Nullable[int]]$Percent, [bool]$Running) {
|
|
1447
1671
|
$oldImage = $Box.Image
|
|
1448
1672
|
$Box.Image = New-BatteryImage $Percent $Running
|
|
@@ -1789,8 +2013,11 @@ function Invoke-HudPump {
|
|
|
1789
2013
|
$timer = New-Object System.Windows.Forms.Timer
|
|
1790
2014
|
$timer.Interval = 1000
|
|
1791
2015
|
$timer.add_Tick({ Invoke-HudPump })
|
|
2016
|
+
# WinEvent hooks wake fullscreen checks immediately on foreground/Z-order/show
|
|
2017
|
+
# changes. The UI timer is only a fallback for Windows builds or shells that do
|
|
2018
|
+
# not deliver every event we subscribe to.
|
|
1792
2019
|
$topMostTimer = New-Object System.Windows.Forms.Timer
|
|
1793
|
-
$topMostTimer.Interval =
|
|
2020
|
+
$topMostTimer.Interval = 150
|
|
1794
2021
|
$topMostTimer.add_Tick({ Update-HudVisibilityForFullscreen })
|
|
1795
2022
|
$form.add_FormClosed({
|
|
1796
2023
|
if ($canMove) {
|
|
@@ -1800,6 +2027,7 @@ $form.add_FormClosed({
|
|
|
1800
2027
|
$timer.Dispose()
|
|
1801
2028
|
$topMostTimer.Stop()
|
|
1802
2029
|
$topMostTimer.Dispose()
|
|
2030
|
+
Stop-FullscreenEventHooks
|
|
1803
2031
|
Stop-SnapshotFetch
|
|
1804
2032
|
foreach ($box in @($codexIconLabel, $claudeIconLabel)) {
|
|
1805
2033
|
if ($box.Image) {
|
|
@@ -1841,14 +2069,13 @@ if ($script:initialHudSnapshot) {
|
|
|
1841
2069
|
}
|
|
1842
2070
|
Update-HudFromSnapshot $script:latestSnapshot
|
|
1843
2071
|
Start-SnapshotFetch
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
$style = $style -bor [AiBatteryNative]::WS_EX_TRANSPARENT
|
|
1849
|
-
}
|
|
1850
|
-
[AiBatteryNative]::SetWindowLong($form.Handle, [AiBatteryNative]::GWL_EXSTYLE, $style) | Out-Null
|
|
2072
|
+
$style = [AiBatteryNative]::GetWindowLong($form.Handle, [AiBatteryNative]::GWL_EXSTYLE)
|
|
2073
|
+
$style = $style -bor [AiBatteryNative]::WS_EX_TOOLWINDOW -bor [AiBatteryNative]::WS_EX_NOACTIVATE
|
|
2074
|
+
if ($ClickThrough) {
|
|
2075
|
+
$style = $style -bor [AiBatteryNative]::WS_EX_TRANSPARENT
|
|
1851
2076
|
}
|
|
2077
|
+
[AiBatteryNative]::SetWindowLong($form.Handle, [AiBatteryNative]::GWL_EXSTYLE, $style) | Out-Null
|
|
1852
2078
|
$timer.Start()
|
|
2079
|
+
Start-FullscreenEventHooks
|
|
1853
2080
|
$topMostTimer.Start()
|
|
1854
2081
|
[System.Windows.Forms.Application]::Run($form)
|