@zeluizr/lattice 1.0.0
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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/MANIFESTO.md +37 -0
- package/README.md +139 -0
- package/dist/app.js +196 -0
- package/dist/cli.js +116 -0
- package/dist/collectors/disks.js +96 -0
- package/dist/collectors/git.js +89 -0
- package/dist/collectors/gpu.js +28 -0
- package/dist/collectors/power.js +105 -0
- package/dist/collectors/sensors.js +80 -0
- package/dist/collectors/system.js +71 -0
- package/dist/collectors/tokens.js +185 -0
- package/dist/collectors/types.js +2 -0
- package/dist/collectors/vtex.js +37 -0
- package/dist/components/LanguageSelect.js +25 -0
- package/dist/components/Panel.js +6 -0
- package/dist/config.js +42 -0
- package/dist/format.js +56 -0
- package/dist/i18n/en.js +76 -0
- package/dist/i18n/es.js +75 -0
- package/dist/i18n/index.js +38 -0
- package/dist/i18n/pt-BR.js +75 -0
- package/dist/icons.js +50 -0
- package/dist/theme.js +34 -0
- package/native/build.sh +23 -0
- package/native/smc.c +151 -0
- package/package.json +74 -0
- package/prebuilds/darwin-arm64/lattice-smc +0 -0
- package/scripts/setup-sudoers.sh +29 -0
package/dist/format.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Formatting helpers ported from the original Python dashboard. */
|
|
2
|
+
const BLOCKS = " ▁▂▃▄▅▆▇█";
|
|
3
|
+
export function fmtTok(n) {
|
|
4
|
+
const v = Number(n || 0);
|
|
5
|
+
if (v >= 1e6)
|
|
6
|
+
return `${(v / 1e6).toFixed(2)}M`;
|
|
7
|
+
if (v >= 1e3)
|
|
8
|
+
return `${(v / 1e3).toFixed(1)}k`;
|
|
9
|
+
return `${v.toFixed(0)}`;
|
|
10
|
+
}
|
|
11
|
+
export function humanBytes(n) {
|
|
12
|
+
if (n === null || n === undefined)
|
|
13
|
+
return "—";
|
|
14
|
+
let v = Number(n);
|
|
15
|
+
for (const unit of ["B", "KB", "MB", "GB", "TB"]) {
|
|
16
|
+
if (v < 1024 || unit === "TB") {
|
|
17
|
+
return unit === "B" ? `${v.toFixed(0)}${unit}` : `${v.toFixed(1)}${unit}`;
|
|
18
|
+
}
|
|
19
|
+
v /= 1024;
|
|
20
|
+
}
|
|
21
|
+
return `${v.toFixed(1)}TB`;
|
|
22
|
+
}
|
|
23
|
+
export function humanRate(bps) {
|
|
24
|
+
return `${humanBytes(bps)}/s`;
|
|
25
|
+
}
|
|
26
|
+
/** One block glyph (0–100%) for the per-core CPU strip. */
|
|
27
|
+
export function coreCell(pct) {
|
|
28
|
+
const clamped = Math.max(0, Math.min(100, Number(pct || 0)));
|
|
29
|
+
const idx = Math.floor((clamped / 100) * (BLOCKS.length - 1));
|
|
30
|
+
return BLOCKS[idx];
|
|
31
|
+
}
|
|
32
|
+
/** Build a sparkline string from a numeric history. */
|
|
33
|
+
export function sparkline(history, width = 0) {
|
|
34
|
+
const data = history.filter((x) => x !== null && x !== undefined && !Number.isNaN(x));
|
|
35
|
+
if (data.length === 0)
|
|
36
|
+
return "";
|
|
37
|
+
const slice = width > 0 ? data.slice(-width) : data;
|
|
38
|
+
const lo = Math.min(...slice);
|
|
39
|
+
const hi = Math.max(...slice);
|
|
40
|
+
const span = hi - lo || 1;
|
|
41
|
+
return slice
|
|
42
|
+
.map((v) => {
|
|
43
|
+
const idx = Math.round(((v - lo) / span) * (BLOCKS.length - 1));
|
|
44
|
+
return BLOCKS[Math.max(1, idx)];
|
|
45
|
+
})
|
|
46
|
+
.join("");
|
|
47
|
+
}
|
|
48
|
+
/** Pick a status level by thresholds (mirrors the Python status() helper). */
|
|
49
|
+
export function statusLevel(value, warn, crit) {
|
|
50
|
+
const v = Number(value || 0);
|
|
51
|
+
if (v < warn)
|
|
52
|
+
return "ok";
|
|
53
|
+
if (v < crit)
|
|
54
|
+
return "warn";
|
|
55
|
+
return "crit";
|
|
56
|
+
}
|
package/dist/i18n/en.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** English (base locale). Every other locale must implement these exact keys. */
|
|
2
|
+
export const en = {
|
|
3
|
+
"subtitle": "system · gpu · power",
|
|
4
|
+
"paused": "PAUSED",
|
|
5
|
+
"panel.cpu": "CPU",
|
|
6
|
+
"panel.mem": "MEMORY",
|
|
7
|
+
"panel.temp": "TEMPERATURE",
|
|
8
|
+
"panel.net": "NETWORK",
|
|
9
|
+
"panel.disks": "DISKS",
|
|
10
|
+
"panel.git": "GIT · REPOS",
|
|
11
|
+
"panel.gpu": "GPU",
|
|
12
|
+
"panel.tokens": "AI · TOKENS TODAY",
|
|
13
|
+
"panel.vtex": "VTEX",
|
|
14
|
+
"panel.procs": "PROCESSES",
|
|
15
|
+
"status.cpu.ok": "calm",
|
|
16
|
+
"status.cpu.warn": "busy",
|
|
17
|
+
"status.cpu.crit": "overload",
|
|
18
|
+
"status.mem.ok": "ok",
|
|
19
|
+
"status.mem.warn": "watch",
|
|
20
|
+
"status.mem.crit": "full",
|
|
21
|
+
"status.disk.ok": "ok",
|
|
22
|
+
"status.disk.warn": "filling",
|
|
23
|
+
"status.disk.crit": "full",
|
|
24
|
+
"status.gpu.ok": "calm",
|
|
25
|
+
"status.gpu.warn": "active",
|
|
26
|
+
"status.gpu.crit": "high",
|
|
27
|
+
"status.temp.ok": "normal",
|
|
28
|
+
"status.temp.warn": "warm",
|
|
29
|
+
"status.temp.crit": "hot",
|
|
30
|
+
"cpu.usage": "Usage",
|
|
31
|
+
"cpu.cores": "Cores",
|
|
32
|
+
"cpu.perCore": "per core",
|
|
33
|
+
"mem.ram": "RAM",
|
|
34
|
+
"mem.used": "{used} of {total} used · swap {swap}%",
|
|
35
|
+
"disks.mount": "MOUNT",
|
|
36
|
+
"disks.read": "READ",
|
|
37
|
+
"disks.write": "WRITE",
|
|
38
|
+
"disks.usage": "USAGE",
|
|
39
|
+
"git.repo": "REPO",
|
|
40
|
+
"git.branch": "BRANCH",
|
|
41
|
+
"git.state": "STATE",
|
|
42
|
+
"git.clean": "clean",
|
|
43
|
+
"git.dirty": "dirty",
|
|
44
|
+
"gpu.usage": "Usage",
|
|
45
|
+
"gpu.mem": "mem {used}/{alloc}",
|
|
46
|
+
"temp.unavailable": "sensors unavailable",
|
|
47
|
+
"temp.waitingSudo": "waiting for sudo",
|
|
48
|
+
"temp.needsSudo": "power: requires sudo",
|
|
49
|
+
"spark.collecting": "collecting…",
|
|
50
|
+
"spark.lastMin": "last min: {range}",
|
|
51
|
+
"tokens.spent": "Spent today: ${cost} · {messages} messages",
|
|
52
|
+
"tokens.tokens": "Tokens: {total} · in {input} · out {output}",
|
|
53
|
+
"tokens.cache": "Cache: write {cw} · read {cr}",
|
|
54
|
+
"tokens.byModel": "By model: {models}",
|
|
55
|
+
"tokens.none": "—",
|
|
56
|
+
"vtex.status": "Status",
|
|
57
|
+
"vtex.notInstalled": "CLI not installed",
|
|
58
|
+
"vtex.install": "Install with: brew install vtex/cli/vtex",
|
|
59
|
+
"vtex.connected": "connected ✓",
|
|
60
|
+
"vtex.account": "Account",
|
|
61
|
+
"vtex.user": "User",
|
|
62
|
+
"vtex.workspace": "Workspace",
|
|
63
|
+
"vtex.notConnected": "not connected",
|
|
64
|
+
"vtex.signin": "Sign in with: vtex login <account>",
|
|
65
|
+
"proc.cpu": "CPU%",
|
|
66
|
+
"proc.mem": "MEM",
|
|
67
|
+
"proc.pid": "PID",
|
|
68
|
+
"proc.name": "Process",
|
|
69
|
+
"key.quit": "Quit",
|
|
70
|
+
"key.pause": "Pause",
|
|
71
|
+
"key.faster": "Faster",
|
|
72
|
+
"key.slower": "Slower",
|
|
73
|
+
"cli.sudoNeed": "lattice needs sudo to read power (powermetrics).",
|
|
74
|
+
"cli.sudoFail": "sudo unavailable — continuing without power data.",
|
|
75
|
+
"cli.sudoCancel": "cancelled.",
|
|
76
|
+
};
|
package/dist/i18n/es.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const es = {
|
|
2
|
+
"subtitle": "sistema · gpu · energía",
|
|
3
|
+
"paused": "PAUSADO",
|
|
4
|
+
"panel.cpu": "CPU",
|
|
5
|
+
"panel.mem": "MEMORIA",
|
|
6
|
+
"panel.temp": "TEMPERATURA",
|
|
7
|
+
"panel.net": "RED",
|
|
8
|
+
"panel.disks": "DISCOS",
|
|
9
|
+
"panel.git": "GIT · REPOS",
|
|
10
|
+
"panel.gpu": "GPU",
|
|
11
|
+
"panel.tokens": "IA · TOKENS HOY",
|
|
12
|
+
"panel.vtex": "VTEX",
|
|
13
|
+
"panel.procs": "PROCESOS",
|
|
14
|
+
"status.cpu.ok": "tranquilo",
|
|
15
|
+
"status.cpu.warn": "ocupado",
|
|
16
|
+
"status.cpu.crit": "sobrecarga",
|
|
17
|
+
"status.mem.ok": "ok",
|
|
18
|
+
"status.mem.warn": "atención",
|
|
19
|
+
"status.mem.crit": "llena",
|
|
20
|
+
"status.disk.ok": "ok",
|
|
21
|
+
"status.disk.warn": "llenándose",
|
|
22
|
+
"status.disk.crit": "lleno",
|
|
23
|
+
"status.gpu.ok": "tranquila",
|
|
24
|
+
"status.gpu.warn": "activa",
|
|
25
|
+
"status.gpu.crit": "alta",
|
|
26
|
+
"status.temp.ok": "normal",
|
|
27
|
+
"status.temp.warn": "tibia",
|
|
28
|
+
"status.temp.crit": "caliente",
|
|
29
|
+
"cpu.usage": "Uso",
|
|
30
|
+
"cpu.cores": "Núcleos",
|
|
31
|
+
"cpu.perCore": "por núcleo",
|
|
32
|
+
"mem.ram": "RAM",
|
|
33
|
+
"mem.used": "{used} de {total} usados · swap {swap}%",
|
|
34
|
+
"disks.mount": "MONTAJE",
|
|
35
|
+
"disks.read": "LECTURA",
|
|
36
|
+
"disks.write": "ESCRITURA",
|
|
37
|
+
"disks.usage": "USO",
|
|
38
|
+
"git.repo": "REPO",
|
|
39
|
+
"git.branch": "BRANCH",
|
|
40
|
+
"git.state": "ESTADO",
|
|
41
|
+
"git.clean": "limpio",
|
|
42
|
+
"git.dirty": "sucio",
|
|
43
|
+
"gpu.usage": "Uso",
|
|
44
|
+
"gpu.mem": "mem {used}/{alloc}",
|
|
45
|
+
"temp.unavailable": "sensores no disponibles",
|
|
46
|
+
"temp.waitingSudo": "esperando sudo",
|
|
47
|
+
"temp.needsSudo": "energía: requiere sudo",
|
|
48
|
+
"spark.collecting": "recolectando…",
|
|
49
|
+
"spark.lastMin": "último min: {range}",
|
|
50
|
+
"tokens.spent": "Gastado hoy: ${cost} · {messages} mensajes",
|
|
51
|
+
"tokens.tokens": "Tokens: {total} · entrada {input} · salida {output}",
|
|
52
|
+
"tokens.cache": "Caché: escribe {cw} · lee {cr}",
|
|
53
|
+
"tokens.byModel": "Por modelo: {models}",
|
|
54
|
+
"tokens.none": "—",
|
|
55
|
+
"vtex.status": "Estado",
|
|
56
|
+
"vtex.notInstalled": "CLI no instalada",
|
|
57
|
+
"vtex.install": "Instala con: brew install vtex/cli/vtex",
|
|
58
|
+
"vtex.connected": "conectado ✓",
|
|
59
|
+
"vtex.account": "Cuenta",
|
|
60
|
+
"vtex.user": "Usuario",
|
|
61
|
+
"vtex.workspace": "Workspace",
|
|
62
|
+
"vtex.notConnected": "no conectado",
|
|
63
|
+
"vtex.signin": "Entra con: vtex login <cuenta>",
|
|
64
|
+
"proc.cpu": "CPU%",
|
|
65
|
+
"proc.mem": "MEM",
|
|
66
|
+
"proc.pid": "PID",
|
|
67
|
+
"proc.name": "Proceso",
|
|
68
|
+
"key.quit": "Salir",
|
|
69
|
+
"key.pause": "Pausar",
|
|
70
|
+
"key.faster": "Más rápido",
|
|
71
|
+
"key.slower": "Más lento",
|
|
72
|
+
"cli.sudoNeed": "lattice necesita sudo para leer la energía (powermetrics).",
|
|
73
|
+
"cli.sudoFail": "sudo no disponible — siguiendo sin datos de energía.",
|
|
74
|
+
"cli.sudoCancel": "cancelado.",
|
|
75
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { en } from "./en.js";
|
|
2
|
+
import { es } from "./es.js";
|
|
3
|
+
import { ptBR } from "./pt-BR.js";
|
|
4
|
+
export const LANGS = [
|
|
5
|
+
{ code: "en", label: "English" },
|
|
6
|
+
{ code: "es", label: "Español" },
|
|
7
|
+
{ code: "pt-BR", label: "Português (Brasil)" },
|
|
8
|
+
];
|
|
9
|
+
const TABLES = {
|
|
10
|
+
"en": en,
|
|
11
|
+
"es": es,
|
|
12
|
+
"pt-BR": ptBR,
|
|
13
|
+
};
|
|
14
|
+
export function isLang(value) {
|
|
15
|
+
return value === "en" || value === "es" || value === "pt-BR";
|
|
16
|
+
}
|
|
17
|
+
/** Best-effort detection from the environment (LANG / LC_ALL). Falls back to en. */
|
|
18
|
+
export function detectLang() {
|
|
19
|
+
const raw = (process.env.LANG || process.env.LC_ALL || process.env.LANGUAGE || "").toLowerCase();
|
|
20
|
+
if (raw.startsWith("pt"))
|
|
21
|
+
return "pt-BR";
|
|
22
|
+
if (raw.startsWith("es"))
|
|
23
|
+
return "es";
|
|
24
|
+
return "en";
|
|
25
|
+
}
|
|
26
|
+
/** Build a translator bound to a language, with {var} interpolation and en fallback. */
|
|
27
|
+
export function makeT(lang) {
|
|
28
|
+
const table = TABLES[lang] ?? en;
|
|
29
|
+
return (key, vars) => {
|
|
30
|
+
let s = table[key] ?? en[key] ?? key;
|
|
31
|
+
if (vars) {
|
|
32
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
33
|
+
s = s.replaceAll(`{${k}}`, String(v));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return s;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const ptBR = {
|
|
2
|
+
"subtitle": "sistema · gpu · energia",
|
|
3
|
+
"paused": "PAUSADO",
|
|
4
|
+
"panel.cpu": "CPU",
|
|
5
|
+
"panel.mem": "MEMÓRIA",
|
|
6
|
+
"panel.temp": "TEMPERATURA",
|
|
7
|
+
"panel.net": "REDE",
|
|
8
|
+
"panel.disks": "DISCOS",
|
|
9
|
+
"panel.git": "GIT · REPOS",
|
|
10
|
+
"panel.gpu": "GPU",
|
|
11
|
+
"panel.tokens": "IA · TOKENS HOJE",
|
|
12
|
+
"panel.vtex": "VTEX",
|
|
13
|
+
"panel.procs": "PROCESSOS",
|
|
14
|
+
"status.cpu.ok": "tranquilo",
|
|
15
|
+
"status.cpu.warn": "ocupado",
|
|
16
|
+
"status.cpu.crit": "sobrecarga",
|
|
17
|
+
"status.mem.ok": "ok",
|
|
18
|
+
"status.mem.warn": "atento",
|
|
19
|
+
"status.mem.crit": "cheia",
|
|
20
|
+
"status.disk.ok": "ok",
|
|
21
|
+
"status.disk.warn": "enchendo",
|
|
22
|
+
"status.disk.crit": "cheio",
|
|
23
|
+
"status.gpu.ok": "tranquila",
|
|
24
|
+
"status.gpu.warn": "ativa",
|
|
25
|
+
"status.gpu.crit": "alta",
|
|
26
|
+
"status.temp.ok": "normal",
|
|
27
|
+
"status.temp.warn": "morna",
|
|
28
|
+
"status.temp.crit": "quente",
|
|
29
|
+
"cpu.usage": "Uso",
|
|
30
|
+
"cpu.cores": "Núcleos",
|
|
31
|
+
"cpu.perCore": "por núcleo",
|
|
32
|
+
"mem.ram": "RAM",
|
|
33
|
+
"mem.used": "{used} de {total} usados · swap {swap}%",
|
|
34
|
+
"disks.mount": "MONTAGEM",
|
|
35
|
+
"disks.read": "LEITURA",
|
|
36
|
+
"disks.write": "ESCRITA",
|
|
37
|
+
"disks.usage": "USO",
|
|
38
|
+
"git.repo": "REPO",
|
|
39
|
+
"git.branch": "BRANCH",
|
|
40
|
+
"git.state": "ESTADO",
|
|
41
|
+
"git.clean": "limpo",
|
|
42
|
+
"git.dirty": "sujo",
|
|
43
|
+
"gpu.usage": "Uso",
|
|
44
|
+
"gpu.mem": "mem {used}/{alloc}",
|
|
45
|
+
"temp.unavailable": "sensores indisponíveis",
|
|
46
|
+
"temp.waitingSudo": "aguardando sudo",
|
|
47
|
+
"temp.needsSudo": "energia: requer sudo",
|
|
48
|
+
"spark.collecting": "coletando…",
|
|
49
|
+
"spark.lastMin": "último min: {range}",
|
|
50
|
+
"tokens.spent": "Gasto hoje: ${cost} · {messages} mensagens",
|
|
51
|
+
"tokens.tokens": "Tokens: {total} · entrada {input} · saída {output}",
|
|
52
|
+
"tokens.cache": "Cache: grava {cw} · lê {cr}",
|
|
53
|
+
"tokens.byModel": "Por modelo: {models}",
|
|
54
|
+
"tokens.none": "—",
|
|
55
|
+
"vtex.status": "Status",
|
|
56
|
+
"vtex.notInstalled": "CLI não instalada",
|
|
57
|
+
"vtex.install": "Instale com: brew install vtex/cli/vtex",
|
|
58
|
+
"vtex.connected": "conectado ✓",
|
|
59
|
+
"vtex.account": "Conta",
|
|
60
|
+
"vtex.user": "Usuário",
|
|
61
|
+
"vtex.workspace": "Workspace",
|
|
62
|
+
"vtex.notConnected": "não conectado",
|
|
63
|
+
"vtex.signin": "Entre com: vtex login <conta>",
|
|
64
|
+
"proc.cpu": "CPU%",
|
|
65
|
+
"proc.mem": "MEM",
|
|
66
|
+
"proc.pid": "PID",
|
|
67
|
+
"proc.name": "Processo",
|
|
68
|
+
"key.quit": "Sair",
|
|
69
|
+
"key.pause": "Pausar",
|
|
70
|
+
"key.faster": "Mais rápido",
|
|
71
|
+
"key.slower": "Mais lento",
|
|
72
|
+
"cli.sudoNeed": "lattice precisa de sudo para ler energia (powermetrics).",
|
|
73
|
+
"cli.sudoFail": "sudo indisponível — seguindo sem dados de energia.",
|
|
74
|
+
"cli.sudoCancel": "cancelado.",
|
|
75
|
+
};
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel/metric icons with three modes: nerd (Nerd Font glyphs), emoji, none.
|
|
3
|
+
*
|
|
4
|
+
* The Nerd Font glyphs are from the Font Awesome 4 subset (U+F000–U+F2E0),
|
|
5
|
+
* present in any Nerd Font patch (e.g. MesloLGS NF). Select a Nerd Font in your
|
|
6
|
+
* terminal, or pass --icons emoji / --icons none.
|
|
7
|
+
*/
|
|
8
|
+
const NERD = {
|
|
9
|
+
cpu: "", // microchip
|
|
10
|
+
gpu: "", // desktop
|
|
11
|
+
mem: "", // database
|
|
12
|
+
swap: "", // exchange
|
|
13
|
+
net: "", // globe
|
|
14
|
+
disk: "", // hdd
|
|
15
|
+
temp: "", // fire
|
|
16
|
+
fan: "", // rotate
|
|
17
|
+
battery: "", // battery-full
|
|
18
|
+
power: "", // bolt
|
|
19
|
+
tokens: "", // money
|
|
20
|
+
vtex: "", // shopping-cart
|
|
21
|
+
git: "\uF126", // code-fork
|
|
22
|
+
proc: "", // tasks
|
|
23
|
+
clock: "", // clock
|
|
24
|
+
};
|
|
25
|
+
const EMOJI = {
|
|
26
|
+
cpu: "🖥️",
|
|
27
|
+
gpu: "🎮",
|
|
28
|
+
mem: "🧠",
|
|
29
|
+
swap: "🔁",
|
|
30
|
+
net: "🌐",
|
|
31
|
+
disk: "💽",
|
|
32
|
+
temp: "🌡️",
|
|
33
|
+
fan: "🌀",
|
|
34
|
+
battery: "🔋",
|
|
35
|
+
power: "⚡",
|
|
36
|
+
tokens: "🪙",
|
|
37
|
+
vtex: "🛒",
|
|
38
|
+
git: "🌿",
|
|
39
|
+
proc: "📋",
|
|
40
|
+
clock: "🕐",
|
|
41
|
+
};
|
|
42
|
+
export function isIconMode(mode) {
|
|
43
|
+
return mode === "nerd" || mode === "emoji" || mode === "none";
|
|
44
|
+
}
|
|
45
|
+
export function makeIcons(mode) {
|
|
46
|
+
if (mode === "none")
|
|
47
|
+
return () => "";
|
|
48
|
+
const set = mode === "emoji" ? EMOJI : NERD;
|
|
49
|
+
return (name) => set[name] ?? "";
|
|
50
|
+
}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dracula Pro theme (official palette) for lattice.
|
|
3
|
+
*
|
|
4
|
+
* The accents (foreground + 7 colors) are shared by every dark variant; only
|
|
5
|
+
* Background / Comment / Selection change per variant. Switch the active
|
|
6
|
+
* variant with --theme or the persisted config.
|
|
7
|
+
*/
|
|
8
|
+
// Dracula PRO accents (identical across every dark variant).
|
|
9
|
+
const ACCENTS = {
|
|
10
|
+
foreground: "#F8F8F2",
|
|
11
|
+
cyan: "#80FFEA",
|
|
12
|
+
green: "#8AFF80",
|
|
13
|
+
orange: "#FFCA80",
|
|
14
|
+
pink: "#FF80BF",
|
|
15
|
+
purple: "#9580FF",
|
|
16
|
+
red: "#FF9580",
|
|
17
|
+
yellow: "#FFFF80",
|
|
18
|
+
};
|
|
19
|
+
// Background / Comment / Selection per variant.
|
|
20
|
+
const VARIANTS = {
|
|
21
|
+
pro: { background: "#22212C", comment: "#7970A9", selection: "#454158" },
|
|
22
|
+
blade: { background: "#212C2A", comment: "#70A99F", selection: "#415854" },
|
|
23
|
+
buffy: { background: "#2A212C", comment: "#9F70A9", selection: "#544158" },
|
|
24
|
+
lincoln: { background: "#2C2A21", comment: "#A99F70", selection: "#585441" },
|
|
25
|
+
morbius: { background: "#2C2122", comment: "#A97079", selection: "#584145" },
|
|
26
|
+
"van-helsing": { background: "#0B0D0F", comment: "#708CA9", selection: "#414D58" },
|
|
27
|
+
};
|
|
28
|
+
export const VARIANT_NAMES = Object.keys(VARIANTS);
|
|
29
|
+
export function isVariant(name) {
|
|
30
|
+
return name in VARIANTS;
|
|
31
|
+
}
|
|
32
|
+
export function palette(variant = "pro") {
|
|
33
|
+
return { ...ACCENTS, ...VARIANTS[variant] };
|
|
34
|
+
}
|
package/native/build.sh
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the lattice-smc helper and place it in prebuilds/<platform>-<arch>/.
|
|
3
|
+
# The prebuilt binary is shipped in the npm package so `npx @zeluizr/lattice`
|
|
4
|
+
# works without a compiler on the user's machine.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
here="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
root="$(cd "$here/.." && pwd)"
|
|
9
|
+
|
|
10
|
+
arch="$(uname -m)" # arm64
|
|
11
|
+
plat="$(uname -s | tr '[:upper:]' '[:lower:]')" # darwin
|
|
12
|
+
outdir="$root/prebuilds/${plat}-${arch}"
|
|
13
|
+
mkdir -p "$outdir"
|
|
14
|
+
|
|
15
|
+
if [[ "$plat" != "darwin" ]]; then
|
|
16
|
+
echo "lattice-smc only builds on macOS (got $plat) — skipping." >&2
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
clang -O2 -Wall -framework IOKit -framework CoreFoundation \
|
|
21
|
+
-o "$outdir/lattice-smc" "$here/smc.c"
|
|
22
|
+
chmod +x "$outdir/lattice-smc"
|
|
23
|
+
echo "built: $outdir/lattice-smc"
|
package/native/smc.c
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* lattice-smc — reads Apple Silicon CPU/GPU temperatures and fan speeds from the
|
|
3
|
+
* AppleSMC via IOKit (no sudo) and prints them as JSON. Ported from the original
|
|
4
|
+
* Python ctypes implementation.
|
|
5
|
+
*
|
|
6
|
+
* Build: clang -O2 -framework IOKit -framework CoreFoundation -o lattice-smc smc.c
|
|
7
|
+
* Output: {"ok":true,"cpu_temp":48.2,"gpu_temp":41.0,"fans":[{"rpm":0,"min":0,"max":0,"pct":0}]}
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
#include <stdint.h>
|
|
13
|
+
#include <stdbool.h>
|
|
14
|
+
#include <mach/mach.h>
|
|
15
|
+
#include <IOKit/IOKitLib.h>
|
|
16
|
+
|
|
17
|
+
#define KERNEL_INDEX_SMC 2
|
|
18
|
+
#define SMC_CMD_READ_BYTES 5
|
|
19
|
+
#define SMC_CMD_READ_KEYINFO 9
|
|
20
|
+
|
|
21
|
+
typedef struct { uint8_t major, minor, build, reserved; uint16_t release; } SMCVers;
|
|
22
|
+
typedef struct { uint16_t version, length; uint32_t cpuPLimit, gpuPLimit, memPLimit; } SMCPLimit;
|
|
23
|
+
typedef struct { uint32_t dataSize, dataType; uint8_t dataAttributes; } SMCKeyInfo;
|
|
24
|
+
typedef struct {
|
|
25
|
+
uint32_t key;
|
|
26
|
+
SMCVers vers;
|
|
27
|
+
SMCPLimit pLimit;
|
|
28
|
+
SMCKeyInfo keyInfo;
|
|
29
|
+
uint8_t result, status, data8;
|
|
30
|
+
uint32_t data32;
|
|
31
|
+
uint8_t bytes[32];
|
|
32
|
+
} SMCParam;
|
|
33
|
+
|
|
34
|
+
static io_connect_t g_conn = 0;
|
|
35
|
+
|
|
36
|
+
/* Candidate sensor keys; we keep the ones that return a plausible value. */
|
|
37
|
+
static const char *CPU_KEYS[] = {
|
|
38
|
+
"Tp01","Tp02","Tp05","Tp09","Tp0D","Tp0H","Tp0L","Tp0P","Tp0T",
|
|
39
|
+
"Tp0X","Tp0b","Tp0f","Tp0j","Tp0n","Tp0r","Tp0v",
|
|
40
|
+
"Te05","Te0L","Te0P","Te0S", NULL
|
|
41
|
+
};
|
|
42
|
+
static const char *GPU_KEYS[] = { "Tg05","Tg09","Tg0D","Tg0L","Tg0T","Tg0X", NULL };
|
|
43
|
+
|
|
44
|
+
static uint32_t k2i(const char *s) {
|
|
45
|
+
return ((uint32_t)(uint8_t)s[0] << 24) | ((uint32_t)(uint8_t)s[1] << 16) |
|
|
46
|
+
((uint32_t)(uint8_t)s[2] << 8) | (uint32_t)(uint8_t)s[3];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static kern_return_t smc_call(SMCParam *in, SMCParam *out) {
|
|
50
|
+
size_t outSize = sizeof(SMCParam);
|
|
51
|
+
return IOConnectCallStructMethod(g_conn, KERNEL_INDEX_SMC, in, sizeof(SMCParam), out, &outSize);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static bool read_value(const char *key, double *out_val) {
|
|
55
|
+
SMCParam in, out;
|
|
56
|
+
memset(&in, 0, sizeof(in));
|
|
57
|
+
memset(&out, 0, sizeof(out));
|
|
58
|
+
in.key = k2i(key);
|
|
59
|
+
in.data8 = SMC_CMD_READ_KEYINFO;
|
|
60
|
+
if (smc_call(&in, &out) != kIOReturnSuccess) return false;
|
|
61
|
+
uint32_t size = out.keyInfo.dataSize;
|
|
62
|
+
uint32_t type = out.keyInfo.dataType;
|
|
63
|
+
if (size == 0 || size > 32) return false;
|
|
64
|
+
|
|
65
|
+
SMCParam in2, out2;
|
|
66
|
+
memset(&in2, 0, sizeof(in2));
|
|
67
|
+
memset(&out2, 0, sizeof(out2));
|
|
68
|
+
in2.key = k2i(key);
|
|
69
|
+
in2.keyInfo.dataSize = size;
|
|
70
|
+
in2.keyInfo.dataType = type;
|
|
71
|
+
in2.data8 = SMC_CMD_READ_BYTES;
|
|
72
|
+
if (smc_call(&in2, &out2) != kIOReturnSuccess) return false;
|
|
73
|
+
|
|
74
|
+
char t[5];
|
|
75
|
+
t[0] = (char)((type >> 24) & 0xff);
|
|
76
|
+
t[1] = (char)((type >> 16) & 0xff);
|
|
77
|
+
t[2] = (char)((type >> 8) & 0xff);
|
|
78
|
+
t[3] = (char)(type & 0xff);
|
|
79
|
+
t[4] = 0;
|
|
80
|
+
uint8_t *b = out2.bytes;
|
|
81
|
+
|
|
82
|
+
if (memcmp(t, "flt ", 4) == 0) { float f; memcpy(&f, b, 4); *out_val = (double)f; return true; }
|
|
83
|
+
if (strncmp(t, "ui8", 3) == 0) { *out_val = (double)b[0]; return true; }
|
|
84
|
+
if (memcmp(t, "ui16", 4) == 0) { *out_val = (double)((b[0] << 8) | b[1]); return true; }
|
|
85
|
+
if (memcmp(t, "fpe2", 4) == 0) { *out_val = (double)((b[0] << 8) | b[1]) / 4.0; return true; }
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static bool read_temp(const char *key, double *out_val) {
|
|
90
|
+
double v;
|
|
91
|
+
if (read_value(key, &v) && v > 5.0 && v < 130.0) { *out_val = v; return true; }
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static bool avg_temp(const char **keys, double *out_val) {
|
|
96
|
+
double sum = 0.0;
|
|
97
|
+
int n = 0;
|
|
98
|
+
for (int i = 0; keys[i]; i++) {
|
|
99
|
+
double v;
|
|
100
|
+
if (read_temp(keys[i], &v)) { sum += v; n++; }
|
|
101
|
+
}
|
|
102
|
+
if (n == 0) return false;
|
|
103
|
+
*out_val = sum / n;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
int main(void) {
|
|
108
|
+
io_service_t svc = IOServiceGetMatchingService(0, IOServiceMatching("AppleSMC"));
|
|
109
|
+
if (!svc) { printf("{\"ok\":false,\"error\":\"AppleSMC not found\"}\n"); return 0; }
|
|
110
|
+
if (IOServiceOpen(svc, mach_task_self(), 0, &g_conn) != kIOReturnSuccess) {
|
|
111
|
+
IOObjectRelease(svc);
|
|
112
|
+
printf("{\"ok\":false,\"error\":\"IOServiceOpen failed\"}\n");
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
IOObjectRelease(svc);
|
|
116
|
+
|
|
117
|
+
double cpu, gpu;
|
|
118
|
+
bool hasCpu = avg_temp(CPU_KEYS, &cpu);
|
|
119
|
+
bool hasGpu = avg_temp(GPU_KEYS, &gpu);
|
|
120
|
+
|
|
121
|
+
printf("{\"ok\":true,");
|
|
122
|
+
if (hasCpu) printf("\"cpu_temp\":%.2f,", cpu); else printf("\"cpu_temp\":null,");
|
|
123
|
+
if (hasGpu) printf("\"gpu_temp\":%.2f,", gpu); else printf("\"gpu_temp\":null,");
|
|
124
|
+
|
|
125
|
+
printf("\"fans\":[");
|
|
126
|
+
double fn;
|
|
127
|
+
int nfans = read_value("FNum", &fn) ? (int)fn : 0;
|
|
128
|
+
int printed = 0;
|
|
129
|
+
for (int i = 0; i < nfans; i++) {
|
|
130
|
+
char ka[8], kn[8], kx[8];
|
|
131
|
+
snprintf(ka, sizeof(ka), "F%dAc", i);
|
|
132
|
+
snprintf(kn, sizeof(kn), "F%dMn", i);
|
|
133
|
+
snprintf(kx, sizeof(kx), "F%dMx", i);
|
|
134
|
+
double rpm, mn = 0.0, mx = 0.0;
|
|
135
|
+
if (!read_value(ka, &rpm)) continue;
|
|
136
|
+
read_value(kn, &mn);
|
|
137
|
+
read_value(kx, &mx);
|
|
138
|
+
double pct = 0.0;
|
|
139
|
+
if (mx > mn) {
|
|
140
|
+
pct = (rpm - mn) / (mx - mn) * 100.0;
|
|
141
|
+
if (pct < 0) pct = 0;
|
|
142
|
+
if (pct > 100) pct = 100;
|
|
143
|
+
}
|
|
144
|
+
if (printed++) printf(",");
|
|
145
|
+
printf("{\"rpm\":%.0f,\"min\":%.0f,\"max\":%.0f,\"pct\":%.0f}", rpm, mn, mx, pct);
|
|
146
|
+
}
|
|
147
|
+
printf("]}\n");
|
|
148
|
+
|
|
149
|
+
IOServiceClose(g_conn);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zeluizr/lattice",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Real-time terminal dashboard for macOS Apple Silicon — GPU, power, temperatures/fans, per-disk I/O (incl. /Volumes), network, memory, processes, and AI token cost.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lattice": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"prebuilds",
|
|
12
|
+
"native/smc.c",
|
|
13
|
+
"native/build.sh",
|
|
14
|
+
"scripts/setup-sudoers.sh",
|
|
15
|
+
"README.md",
|
|
16
|
+
"MANIFESTO.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"os": [
|
|
24
|
+
"darwin"
|
|
25
|
+
],
|
|
26
|
+
"cpu": [
|
|
27
|
+
"arm64"
|
|
28
|
+
],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"macos",
|
|
31
|
+
"apple-silicon",
|
|
32
|
+
"system-monitor",
|
|
33
|
+
"tui",
|
|
34
|
+
"terminal",
|
|
35
|
+
"dashboard",
|
|
36
|
+
"gpu",
|
|
37
|
+
"powermetrics",
|
|
38
|
+
"htop",
|
|
39
|
+
"ink",
|
|
40
|
+
"cli"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "Jose Luiz Rodrigues",
|
|
44
|
+
"homepage": "https://github.com/zeluizr/lattice#readme",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/zeluizr/lattice.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/zeluizr/lattice/issues"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc && node scripts/postbuild.mjs",
|
|
54
|
+
"build:native": "bash native/build.sh",
|
|
55
|
+
"dev": "npm run build && node dist/cli.js",
|
|
56
|
+
"start": "node dist/cli.js",
|
|
57
|
+
"prepublishOnly": "npm run build:native && npm run build"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"bplist-parser": "^0.3.2",
|
|
61
|
+
"execa": "^9.6.1",
|
|
62
|
+
"ink": "^7.1.0",
|
|
63
|
+
"meow": "^14.1.0",
|
|
64
|
+
"plist": "^5.0.0",
|
|
65
|
+
"react": "^19.2.7",
|
|
66
|
+
"systeminformation": "^5.31.9"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^26.0.0",
|
|
70
|
+
"@types/react": "^19.2.17",
|
|
71
|
+
"ink-testing-library": "^4.0.0",
|
|
72
|
+
"typescript": "^6.0.3"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
Binary file
|