brightctrl 0.0.0 → 0.0.2
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 +85 -0
- package/dist/index.js +535 -0
- package/package.json +45 -9
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# BrightCtrl
|
|
2
|
+
|
|
3
|
+
A lightweight DDC/CI monitor brightness controller for Linux — now a terminal UI built with [React](https://react.dev) / [Ink](https://github.com/vadimdemedes/ink) on [Bun](https://bun.sh).
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Auto-detects all DDC/CI-capable monitors
|
|
10
|
+
- Per-monitor brightness bars with live percentage
|
|
11
|
+
- Arrow key / vim (`h`/`l`) brightness adjustment (5% steps)
|
|
12
|
+
- **Sync mode** (`s`) — control all monitors at once
|
|
13
|
+
- Direct input dialog (`i`) — type a specific brightness value
|
|
14
|
+
- 500ms debounce on DDC writes (no flooding the i2c bus)
|
|
15
|
+
- Brightness-dependent color coding (red ≤20%, cyan 21-89%, yellow ≥90%)
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
### System
|
|
20
|
+
|
|
21
|
+
| Distro | Command |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Arch / Manjaro | `sudo pacman -S ddcutil` |
|
|
24
|
+
| Debian / Ubuntu | `sudo apt install ddcutil` |
|
|
25
|
+
| Fedora / RHEL | `sudo dnf install ddcutil` |
|
|
26
|
+
|
|
27
|
+
Then set up the i2c kernel module and add yourself to the i2c group:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
sudo usermod -aG i2c $USER
|
|
31
|
+
sudo modprobe i2c-dev
|
|
32
|
+
|
|
33
|
+
# Persist across reboots:
|
|
34
|
+
echo 'i2c-dev' | sudo tee /etc/modules-load.d/i2c.conf
|
|
35
|
+
|
|
36
|
+
# Log out and back in for the group change to take effect.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Verify everything works:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ddcutil detect
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Runtime
|
|
46
|
+
|
|
47
|
+
- [Node.js](https://nodejs.org) >= 18
|
|
48
|
+
- Or [Bun](https://bun.sh) 1.x (for development)
|
|
49
|
+
|
|
50
|
+
## Install & Run
|
|
51
|
+
|
|
52
|
+
### Quick — no install needed
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx brightctrl
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Global install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install -g brightctrl
|
|
62
|
+
brightctrl
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### From source
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/shahriyardx/brightctrl.git
|
|
69
|
+
cd brightctrl
|
|
70
|
+
bun install
|
|
71
|
+
bun src/index.tsx
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Controls
|
|
75
|
+
|
|
76
|
+
| Key | Action |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `↑` / `↓` | Select monitor |
|
|
79
|
+
| `←` / `h` | Decrease brightness 5% |
|
|
80
|
+
| `→` / `l` | Increase brightness 5% |
|
|
81
|
+
| `i` | Open input dialog (type 0–100, Enter to confirm) |
|
|
82
|
+
| `s` | Toggle sync mode (all monitors) |
|
|
83
|
+
| `r` | Refresh monitor detection |
|
|
84
|
+
| `q` | Quit |
|
|
85
|
+
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/index.tsx
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
|
|
7
|
+
// src/app.tsx
|
|
8
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
10
|
+
|
|
11
|
+
// src/ddcutil.ts
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
var execFileAsync = promisify(execFile);
|
|
15
|
+
var isWindows = process.platform === "win32";
|
|
16
|
+
var isMac = process.platform === "darwin";
|
|
17
|
+
function ddcutil(args, timeout = 8000) {
|
|
18
|
+
return execFileAsync("ddcutil", args, {
|
|
19
|
+
timeout,
|
|
20
|
+
encoding: "utf-8"
|
|
21
|
+
}).then((r) => r.stdout);
|
|
22
|
+
}
|
|
23
|
+
function powerShell(script, timeout = 8000) {
|
|
24
|
+
return execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout, encoding: "utf-8" }).then((r) => r.stdout.trim());
|
|
25
|
+
}
|
|
26
|
+
async function checkDdcutil() {
|
|
27
|
+
if (isWindows) {
|
|
28
|
+
try {
|
|
29
|
+
const out = await powerShell("Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightness | ConvertTo-Json -Depth 3", 5000);
|
|
30
|
+
return out.length > 0 && out !== "null";
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (isMac)
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
await execFileAsync("which", ["ddcutil"]);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function detectMonitorsLinux() {
|
|
45
|
+
const stdout = await ddcutil(["detect", "--brief"], 15000);
|
|
46
|
+
const monitors = [];
|
|
47
|
+
let current = null;
|
|
48
|
+
for (const line of stdout.split(`
|
|
49
|
+
`)) {
|
|
50
|
+
const t = line.trim();
|
|
51
|
+
if (t.startsWith("Display ")) {
|
|
52
|
+
if (current?.index != null)
|
|
53
|
+
monitors.push(current);
|
|
54
|
+
const m = t.match(/Display\s+(\d+)/);
|
|
55
|
+
const idxStr = m?.[1];
|
|
56
|
+
current = {
|
|
57
|
+
index: idxStr ? Number.parseInt(idxStr, 10) : monitors.length + 1,
|
|
58
|
+
name: "Unknown Monitor",
|
|
59
|
+
bus: ""
|
|
60
|
+
};
|
|
61
|
+
} else if (t.includes("I2C bus:")) {
|
|
62
|
+
if (current)
|
|
63
|
+
current.bus = t.replace("I2C bus:", "").trim();
|
|
64
|
+
} else if (t.startsWith("Monitor:")) {
|
|
65
|
+
if (current) {
|
|
66
|
+
const p = t.replace("Monitor:", "").trim();
|
|
67
|
+
current.name = p.replace(/\s+unspecified/i, "").trim() || "Monitor";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (current?.index != null)
|
|
72
|
+
monitors.push(current);
|
|
73
|
+
return monitors;
|
|
74
|
+
}
|
|
75
|
+
async function detectMonitorsWindows() {
|
|
76
|
+
const ps = `$b=Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightness; $r=@(); $i=0; foreach($m in $b){$i++; $r+=[PSCustomObject]@{index=$i;name=[string]($m.InstanceName -replace '.*\\\\([^\\\\]+)\\\\.*','$1');bus=$m.InstanceName}}; $r | ConvertTo-Json -Depth 3`;
|
|
77
|
+
const stdout = await powerShell(ps, 1e4);
|
|
78
|
+
if (!stdout || stdout === "null")
|
|
79
|
+
return [];
|
|
80
|
+
const data = JSON.parse(stdout);
|
|
81
|
+
const arr = Array.isArray(data) ? data : [data];
|
|
82
|
+
return arr.map((d, i) => ({
|
|
83
|
+
index: i + 1,
|
|
84
|
+
name: typeof d.name === "string" && d.name ? d.name : "Monitor",
|
|
85
|
+
bus: typeof d.bus === "string" ? d.bus : ""
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
async function detectMonitorsMac() {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
async function getBrightnessMac(_displayIndex) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
async function setBrightnessMac(_displayIndex, _value) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
async function detectMonitors() {
|
|
98
|
+
if (isWindows)
|
|
99
|
+
return detectMonitorsWindows();
|
|
100
|
+
if (isMac)
|
|
101
|
+
return detectMonitorsMac();
|
|
102
|
+
return detectMonitorsLinux();
|
|
103
|
+
}
|
|
104
|
+
async function getBrightnessLinux(displayIndex) {
|
|
105
|
+
try {
|
|
106
|
+
const stdout = await ddcutil(["getvcp", "10", `--display=${displayIndex}`], 5000);
|
|
107
|
+
const m = stdout.match(/current value\s*=\s*(\d+)/);
|
|
108
|
+
const val = m?.[1];
|
|
109
|
+
return val ? Number.parseInt(val, 10) : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function getBrightnessWindows(displayIndex) {
|
|
115
|
+
try {
|
|
116
|
+
const idx = Math.max(0, displayIndex - 1);
|
|
117
|
+
const ps = `$b=Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightness; if($b -and $b.Count -ge ${idx + 1}){$b[${idx}].CurrentBrightness}else{-1}`;
|
|
118
|
+
const stdout = await powerShell(ps, 5000);
|
|
119
|
+
const v = Number.parseInt(stdout, 10);
|
|
120
|
+
return Number.isNaN(v) || v < 0 ? null : v;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function getBrightness(displayIndex) {
|
|
126
|
+
if (isWindows)
|
|
127
|
+
return getBrightnessWindows(displayIndex);
|
|
128
|
+
if (isMac)
|
|
129
|
+
return getBrightnessMac(displayIndex);
|
|
130
|
+
return getBrightnessLinux(displayIndex);
|
|
131
|
+
}
|
|
132
|
+
async function setBrightnessLinux(displayIndex, value) {
|
|
133
|
+
try {
|
|
134
|
+
await ddcutil(["setvcp", "10", String(Math.round(value)), `--display=${displayIndex}`], 5000);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function setBrightnessWindows(displayIndex, value) {
|
|
141
|
+
try {
|
|
142
|
+
const idx = Math.max(0, displayIndex - 1);
|
|
143
|
+
const ps = `$m=Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods; if($m -and $m.Count -ge ${idx + 1}){$m[${idx}].WmiSetBrightness(1,${Math.round(value)})}`;
|
|
144
|
+
await powerShell(ps, 5000);
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function setBrightness(displayIndex, value) {
|
|
151
|
+
if (isWindows)
|
|
152
|
+
return setBrightnessWindows(displayIndex, value);
|
|
153
|
+
if (isMac)
|
|
154
|
+
return setBrightnessMac(displayIndex, value);
|
|
155
|
+
return setBrightnessLinux(displayIndex, value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/app.tsx
|
|
159
|
+
import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
|
|
160
|
+
function BrightnessBar({
|
|
161
|
+
value,
|
|
162
|
+
width = 25
|
|
163
|
+
}) {
|
|
164
|
+
const filled = Math.round(value / 100 * width);
|
|
165
|
+
const empty = width - filled;
|
|
166
|
+
const color = value <= 20 ? "red" : value >= 90 ? "yellow" : "cyan";
|
|
167
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
170
|
+
color,
|
|
171
|
+
children: filled > 0 ? "█".repeat(filled) : ""
|
|
172
|
+
}, undefined, false, undefined, this),
|
|
173
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
174
|
+
color: "#444",
|
|
175
|
+
children: empty > 0 ? "░".repeat(empty) : ""
|
|
176
|
+
}, undefined, false, undefined, this),
|
|
177
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
178
|
+
children: " "
|
|
179
|
+
}, undefined, false, undefined, this),
|
|
180
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
181
|
+
color,
|
|
182
|
+
bold: true,
|
|
183
|
+
children: [
|
|
184
|
+
Math.round(value),
|
|
185
|
+
"%"
|
|
186
|
+
]
|
|
187
|
+
}, undefined, true, undefined, this)
|
|
188
|
+
]
|
|
189
|
+
}, undefined, true, undefined, this);
|
|
190
|
+
}
|
|
191
|
+
function MonitorCard({
|
|
192
|
+
monitor,
|
|
193
|
+
selected
|
|
194
|
+
}) {
|
|
195
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
196
|
+
borderStyle: selected ? "round" : "single",
|
|
197
|
+
borderColor: selected ? "cyan" : "gray",
|
|
198
|
+
flexDirection: "column",
|
|
199
|
+
paddingX: 1,
|
|
200
|
+
paddingY: 0,
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
203
|
+
gap: 1,
|
|
204
|
+
children: [
|
|
205
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
206
|
+
bold: true,
|
|
207
|
+
color: selected ? "cyan" : "white",
|
|
208
|
+
children: [
|
|
209
|
+
"Display ",
|
|
210
|
+
monitor.index
|
|
211
|
+
]
|
|
212
|
+
}, undefined, true, undefined, this),
|
|
213
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
214
|
+
color: "white",
|
|
215
|
+
children: monitor.name
|
|
216
|
+
}, undefined, false, undefined, this),
|
|
217
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
218
|
+
color: "gray",
|
|
219
|
+
children: monitor.bus
|
|
220
|
+
}, undefined, false, undefined, this),
|
|
221
|
+
selected ? /* @__PURE__ */ jsxDEV(Text, {
|
|
222
|
+
color: "cyan",
|
|
223
|
+
children: "◄"
|
|
224
|
+
}, undefined, false, undefined, this) : null
|
|
225
|
+
]
|
|
226
|
+
}, undefined, true, undefined, this),
|
|
227
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
228
|
+
children: /* @__PURE__ */ jsxDEV(BrightnessBar, {
|
|
229
|
+
value: monitor.brightness
|
|
230
|
+
}, undefined, false, undefined, this)
|
|
231
|
+
}, undefined, false, undefined, this)
|
|
232
|
+
]
|
|
233
|
+
}, undefined, true, undefined, this);
|
|
234
|
+
}
|
|
235
|
+
function ErrorPanel() {
|
|
236
|
+
const plat = process.platform;
|
|
237
|
+
if (plat === "darwin")
|
|
238
|
+
return null;
|
|
239
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
240
|
+
flexDirection: "column",
|
|
241
|
+
marginTop: 1,
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
244
|
+
color: "yellow",
|
|
245
|
+
children: "Troubleshooting:"
|
|
246
|
+
}, undefined, false, undefined, this),
|
|
247
|
+
plat === "win32" ? /* @__PURE__ */ jsxDEV(Fragment, {
|
|
248
|
+
children: [
|
|
249
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
250
|
+
color: "gray",
|
|
251
|
+
children: "Some GPUs/monitors don't expose brightness via WMI."
|
|
252
|
+
}, undefined, false, undefined, this),
|
|
253
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
254
|
+
color: "gray",
|
|
255
|
+
children: "Try installing MonitorController from Windows Store."
|
|
256
|
+
}, undefined, false, undefined, this)
|
|
257
|
+
]
|
|
258
|
+
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV(Fragment, {
|
|
259
|
+
children: [
|
|
260
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
261
|
+
color: "gray",
|
|
262
|
+
children: " sudo pacman -S ddcutil"
|
|
263
|
+
}, undefined, false, undefined, this),
|
|
264
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
265
|
+
color: "gray",
|
|
266
|
+
children: " sudo usermod -aG i2c $USER"
|
|
267
|
+
}, undefined, false, undefined, this),
|
|
268
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
269
|
+
color: "gray",
|
|
270
|
+
children: " sudo modprobe i2c-dev"
|
|
271
|
+
}, undefined, false, undefined, this),
|
|
272
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
273
|
+
color: "gray",
|
|
274
|
+
children: "echo 'i2c-dev' | sudo tee /etc/modules-load.d/i2c.conf"
|
|
275
|
+
}, undefined, false, undefined, this),
|
|
276
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
277
|
+
color: "gray",
|
|
278
|
+
children: " (log out and in for group change)"
|
|
279
|
+
}, undefined, false, undefined, this)
|
|
280
|
+
]
|
|
281
|
+
}, undefined, true, undefined, this)
|
|
282
|
+
]
|
|
283
|
+
}, undefined, true, undefined, this);
|
|
284
|
+
}
|
|
285
|
+
function App() {
|
|
286
|
+
const { exit } = useApp();
|
|
287
|
+
const [monitors, setMonitors] = useState([]);
|
|
288
|
+
const [selected, setSelected] = useState(0);
|
|
289
|
+
const [syncMode, setSyncMode] = useState(false);
|
|
290
|
+
const [status, setStatus] = useState("Starting...");
|
|
291
|
+
const [error, setError] = useState(null);
|
|
292
|
+
const [loading, setLoading] = useState(true);
|
|
293
|
+
const [typing, setTyping] = useState(false);
|
|
294
|
+
const [typed, setTyped] = useState("");
|
|
295
|
+
const debounceTimer = useRef(null);
|
|
296
|
+
const pendingRef = useRef(new Map);
|
|
297
|
+
function scheduleBrightness(index, value) {
|
|
298
|
+
pendingRef.current.set(index, value);
|
|
299
|
+
if (debounceTimer.current)
|
|
300
|
+
clearTimeout(debounceTimer.current);
|
|
301
|
+
debounceTimer.current = setTimeout(() => {
|
|
302
|
+
const vals = new Map(pendingRef.current);
|
|
303
|
+
pendingRef.current.clear();
|
|
304
|
+
for (const [idx, v] of vals) {
|
|
305
|
+
setBrightness(idx, v);
|
|
306
|
+
}
|
|
307
|
+
}, 500);
|
|
308
|
+
}
|
|
309
|
+
const load = useCallback(async () => {
|
|
310
|
+
setLoading(true);
|
|
311
|
+
setError(null);
|
|
312
|
+
setStatus("Checking ddcutil...");
|
|
313
|
+
const hasBackend = await checkDdcutil();
|
|
314
|
+
if (!hasBackend) {
|
|
315
|
+
if (process.platform === "win32") {
|
|
316
|
+
setError("No monitors with WMI brightness control found");
|
|
317
|
+
} else if (process.platform === "darwin") {
|
|
318
|
+
setError("macOS is not supported at this moment");
|
|
319
|
+
} else {
|
|
320
|
+
setError("ddcutil not found. Install: sudo pacman -S ddcutil");
|
|
321
|
+
}
|
|
322
|
+
setStatus("backend unavailable");
|
|
323
|
+
setLoading(false);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
setStatus("Detecting monitors...");
|
|
327
|
+
const infoList = await detectMonitors();
|
|
328
|
+
if (infoList.length === 0) {
|
|
329
|
+
setError("No DDC/CI monitors detected");
|
|
330
|
+
setStatus("No monitors found");
|
|
331
|
+
setLoading(false);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
setStatus(`Reading brightness for ${infoList.length} monitor(s)...`);
|
|
335
|
+
const loaded = [];
|
|
336
|
+
for (const info of infoList) {
|
|
337
|
+
const b = await getBrightness(info.index);
|
|
338
|
+
loaded.push({ ...info, brightness: b ?? 50 });
|
|
339
|
+
}
|
|
340
|
+
setMonitors(loaded);
|
|
341
|
+
setSelected((s) => Math.min(s, loaded.length - 1));
|
|
342
|
+
setStatus(`${loaded.length} monitor(s) — DDC/CI via ddcutil`);
|
|
343
|
+
setLoading(false);
|
|
344
|
+
}, []);
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
load();
|
|
347
|
+
}, [load]);
|
|
348
|
+
function adjustBrightness(delta) {
|
|
349
|
+
if (monitors.length === 0)
|
|
350
|
+
return;
|
|
351
|
+
const idx = selected;
|
|
352
|
+
setMonitors((prev) => prev.map((m, i) => {
|
|
353
|
+
if (!syncMode && i !== idx)
|
|
354
|
+
return m;
|
|
355
|
+
const v = Math.max(0, Math.min(100, m.brightness + delta));
|
|
356
|
+
scheduleBrightness(m.index, v);
|
|
357
|
+
return { ...m, brightness: v };
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
useInput((input, key) => {
|
|
361
|
+
if (typing) {
|
|
362
|
+
if (key.escape) {
|
|
363
|
+
setTyping(false);
|
|
364
|
+
setTyped("");
|
|
365
|
+
} else if (key.return) {
|
|
366
|
+
const v = Number.parseInt(typed, 10);
|
|
367
|
+
if (!Number.isNaN(v) && v >= 0 && v <= 100) {
|
|
368
|
+
setMonitors((prev) => prev.map((m, i) => {
|
|
369
|
+
if (!syncMode && i !== selected)
|
|
370
|
+
return m;
|
|
371
|
+
scheduleBrightness(m.index, v);
|
|
372
|
+
return { ...m, brightness: v };
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
setTyping(false);
|
|
376
|
+
setTyped("");
|
|
377
|
+
} else if (key.backspace) {
|
|
378
|
+
setTyped((p) => p.slice(0, -1));
|
|
379
|
+
} else if (/^[0-9]$/.test(input)) {
|
|
380
|
+
setTyped((p) => (p + input).slice(0, 3));
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (input === "q")
|
|
385
|
+
exit();
|
|
386
|
+
if (key.upArrow) {
|
|
387
|
+
setSelected((s) => Math.max(0, s - 1));
|
|
388
|
+
} else if (key.downArrow) {
|
|
389
|
+
setSelected((s) => Math.min(monitors.length - 1, s + 1));
|
|
390
|
+
} else if (input === "h" || key.leftArrow) {
|
|
391
|
+
adjustBrightness(-5);
|
|
392
|
+
} else if (input === "l" || key.rightArrow) {
|
|
393
|
+
adjustBrightness(5);
|
|
394
|
+
} else if (input === "s") {
|
|
395
|
+
setSyncMode((s) => !s);
|
|
396
|
+
} else if (input === "r") {
|
|
397
|
+
load();
|
|
398
|
+
} else if (input === "i") {
|
|
399
|
+
setTyping(true);
|
|
400
|
+
setTyped("");
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
404
|
+
flexDirection: "column",
|
|
405
|
+
padding: 1,
|
|
406
|
+
children: [
|
|
407
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
408
|
+
gap: 2,
|
|
409
|
+
children: [
|
|
410
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
411
|
+
bold: true,
|
|
412
|
+
color: "cyan",
|
|
413
|
+
children: "BrightCtrl"
|
|
414
|
+
}, undefined, false, undefined, this),
|
|
415
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
416
|
+
color: "gray",
|
|
417
|
+
children: "DDC/CI Brightness Controller"
|
|
418
|
+
}, undefined, false, undefined, this)
|
|
419
|
+
]
|
|
420
|
+
}, undefined, true, undefined, this),
|
|
421
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
422
|
+
color: "#333",
|
|
423
|
+
children: "─".repeat(56)
|
|
424
|
+
}, undefined, false, undefined, this),
|
|
425
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
426
|
+
marginBottom: 1,
|
|
427
|
+
gap: 3,
|
|
428
|
+
children: [
|
|
429
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
430
|
+
color: syncMode ? "green" : "gray",
|
|
431
|
+
children: [
|
|
432
|
+
"[",
|
|
433
|
+
syncMode ? "✓" : " ",
|
|
434
|
+
"] Sync All ",
|
|
435
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
436
|
+
color: "gray",
|
|
437
|
+
children: "(s)"
|
|
438
|
+
}, undefined, false, undefined, this)
|
|
439
|
+
]
|
|
440
|
+
}, undefined, true, undefined, this),
|
|
441
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
442
|
+
children: [
|
|
443
|
+
"⟳ Refresh ",
|
|
444
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
445
|
+
color: "gray",
|
|
446
|
+
children: "(r)"
|
|
447
|
+
}, undefined, false, undefined, this)
|
|
448
|
+
]
|
|
449
|
+
}, undefined, true, undefined, this),
|
|
450
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
451
|
+
children: [
|
|
452
|
+
"✕ Quit ",
|
|
453
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
454
|
+
color: "gray",
|
|
455
|
+
children: "(q)"
|
|
456
|
+
}, undefined, false, undefined, this)
|
|
457
|
+
]
|
|
458
|
+
}, undefined, true, undefined, this)
|
|
459
|
+
]
|
|
460
|
+
}, undefined, true, undefined, this),
|
|
461
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
462
|
+
marginBottom: 1,
|
|
463
|
+
children: loading ? /* @__PURE__ */ jsxDEV(Text, {
|
|
464
|
+
color: "gray",
|
|
465
|
+
children: [
|
|
466
|
+
status,
|
|
467
|
+
"..."
|
|
468
|
+
]
|
|
469
|
+
}, undefined, true, undefined, this) : error ? /* @__PURE__ */ jsxDEV(Text, {
|
|
470
|
+
color: "red",
|
|
471
|
+
children: error
|
|
472
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Text, {
|
|
473
|
+
color: "gray",
|
|
474
|
+
children: status
|
|
475
|
+
}, undefined, false, undefined, this)
|
|
476
|
+
}, undefined, false, undefined, this),
|
|
477
|
+
!loading && !error && monitors.length > 0 ? /* @__PURE__ */ jsxDEV(Box, {
|
|
478
|
+
flexDirection: "column",
|
|
479
|
+
gap: 0,
|
|
480
|
+
children: monitors.map((m, i) => /* @__PURE__ */ jsxDEV(MonitorCard, {
|
|
481
|
+
monitor: m,
|
|
482
|
+
selected: i === selected
|
|
483
|
+
}, m.index, false, undefined, this))
|
|
484
|
+
}, undefined, false, undefined, this) : null,
|
|
485
|
+
!loading && error ? /* @__PURE__ */ jsxDEV(ErrorPanel, {}, undefined, false, undefined, this) : null,
|
|
486
|
+
typing ? /* @__PURE__ */ jsxDEV(Box, {
|
|
487
|
+
flexDirection: "column",
|
|
488
|
+
marginTop: 1,
|
|
489
|
+
marginBottom: 1,
|
|
490
|
+
children: /* @__PURE__ */ jsxDEV(Box, {
|
|
491
|
+
borderStyle: "round",
|
|
492
|
+
borderColor: "cyan",
|
|
493
|
+
paddingX: 1,
|
|
494
|
+
flexDirection: "column",
|
|
495
|
+
children: [
|
|
496
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
497
|
+
gap: 1,
|
|
498
|
+
children: [
|
|
499
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
500
|
+
bold: true,
|
|
501
|
+
color: "cyan",
|
|
502
|
+
children: "Brightness:"
|
|
503
|
+
}, undefined, false, undefined, this),
|
|
504
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
505
|
+
bold: true,
|
|
506
|
+
color: "white",
|
|
507
|
+
children: [
|
|
508
|
+
typed || "0",
|
|
509
|
+
"_"
|
|
510
|
+
]
|
|
511
|
+
}, undefined, true, undefined, this)
|
|
512
|
+
]
|
|
513
|
+
}, undefined, true, undefined, this),
|
|
514
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
515
|
+
color: "gray",
|
|
516
|
+
children: "Enter 0-100 ↵ confirm ⎋ cancel"
|
|
517
|
+
}, undefined, false, undefined, this)
|
|
518
|
+
]
|
|
519
|
+
}, undefined, true, undefined, this)
|
|
520
|
+
}, undefined, false, undefined, this) : null,
|
|
521
|
+
!loading ? /* @__PURE__ */ jsxDEV(Box, {
|
|
522
|
+
marginTop: 1,
|
|
523
|
+
children: error ? null : /* @__PURE__ */ jsxDEV(Text, {
|
|
524
|
+
color: "gray",
|
|
525
|
+
children: "↑↓ select h/l ←→ brightness i input s sync r refresh q quit"
|
|
526
|
+
}, undefined, false, undefined, this)
|
|
527
|
+
}, undefined, false, undefined, this) : null
|
|
528
|
+
]
|
|
529
|
+
}, undefined, true, undefined, this);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/index.tsx
|
|
533
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
534
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsxDEV2(App, {}, undefined, false, undefined, this));
|
|
535
|
+
await waitUntilExit();
|
package/package.json
CHANGED
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightctrl",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Terminal UI for DDC/CI monitor brightness control",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"brightctrl": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "bun build src/index.tsx --outdir dist --target node --format esm --external ink --external react --external react-reconciler --external scheduler --external '@types/react' && sed -i 's_^#!/usr/bin/env bun_#!/usr/bin/env node_' dist/index.js",
|
|
18
|
+
"prepublishOnly": "bun run build",
|
|
19
|
+
"lint": "biome check src/",
|
|
20
|
+
"format": "biome check --write src/",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ink": "^5.2.0",
|
|
25
|
+
"react": "^18.3.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "^2.4.15",
|
|
29
|
+
"@types/react": "^18.3.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/shahriyardx/brightctrl.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/shahriyardx/brightctrl",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"brightness",
|
|
39
|
+
"ddc-ci",
|
|
40
|
+
"ddcutil",
|
|
41
|
+
"monitor",
|
|
42
|
+
"tui",
|
|
43
|
+
"ink",
|
|
44
|
+
"waybar"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// Placeholder
|