about-system 0.0.40 → 0.0.45

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.
@@ -6,7 +6,7 @@
6
6
  import os from "os";
7
7
  import fs from "fs";
8
8
  import type { InfoContext } from "../types/internal-types";
9
- import { IS_LINUX } from "../utils/platform";
9
+ import { IS_LINUX, IS_MAC } from "../utils/platform";
10
10
  import { execCommand } from "../utils/command";
11
11
  import { getCachedValue, setCachedValue } from "../cache/cache";
12
12
 
@@ -56,56 +56,101 @@ export function ram_used(context: InfoContext): string {
56
56
 
57
57
  /**
58
58
  * Gets available memory in gigabytes
59
- * Reads MemAvailable from /proc/meminfo (Linux only)
59
+ * Reads MemAvailable from /proc/meminfo (Linux) or derives it from
60
+ * vm_stat's free+inactive pages (macOS)
60
61
  * @returns Available memory with "GB available" suffix or empty string
61
62
  * @example "12GB available", "4GB available"
62
63
  */
63
64
  export function memory_available(): string {
64
- if (!IS_LINUX) return "";
65
+ if (IS_LINUX) {
66
+ try {
67
+ const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
68
+ const availableMatch = meminfo.match(/MemAvailable:\s+(\d+) kB/);
69
+ if (availableMatch) {
70
+ const availableGB = Math.round(parseInt(availableMatch[1]) / 1024 / 1024);
71
+ return `${availableGB}GB available`;
72
+ }
73
+ } catch {}
74
+ return "";
75
+ }
76
+
77
+ if (IS_MAC) {
78
+ try {
79
+ const vmStat = execCommand("vm_stat");
80
+ const pageSizeMatch = vmStat.match(/page size of (\d+) bytes/);
81
+ const freeMatch = vmStat.match(/Pages free:\s+(\d+)\./);
82
+ const inactiveMatch = vmStat.match(/Pages inactive:\s+(\d+)\./);
83
+
84
+ if (pageSizeMatch && freeMatch && inactiveMatch) {
85
+ const pageSize = parseInt(pageSizeMatch[1]);
86
+ const availablePages = parseInt(freeMatch[1]) + parseInt(inactiveMatch[1]);
87
+ const availableGB = Math.round((availablePages * pageSize) / (1024 * 1024 * 1024));
88
+ return `${availableGB}GB available`;
89
+ }
90
+ } catch {}
91
+ return "";
92
+ }
65
93
 
66
- try {
67
- const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
68
- const availableMatch = meminfo.match(/MemAvailable:\s+(\d+) kB/);
69
- if (availableMatch) {
70
- const availableGB = Math.round(parseInt(availableMatch[1]) / 1024 / 1024);
71
- return `${availableGB}GB available`;
72
- }
73
- } catch {}
74
94
  return "";
75
95
  }
76
96
 
77
97
  /**
78
98
  * Gets swap memory usage
79
- * Calculates swap usage from /proc/meminfo (Linux only)
99
+ * Calculates swap usage from /proc/meminfo (Linux) or `sysctl vm.swapusage`
100
+ * (macOS)
80
101
  * @returns Swap usage as "percentage (size MB) swap" or empty string
81
102
  * @example "15% (512MB) swap", "0% (0MB) swap"
82
103
  */
83
104
  export function swap_used(): string {
84
- if (!IS_LINUX) return "";
105
+ if (IS_LINUX) {
106
+ try {
107
+ const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
108
+ const swapTotalMatch = meminfo.match(/SwapTotal:\s+(\d+) kB/);
109
+ const swapFreeMatch = meminfo.match(/SwapFree:\s+(\d+) kB/);
85
110
 
86
- try {
87
- const meminfo = fs.readFileSync("/proc/meminfo", "utf8");
88
- const swapTotalMatch = meminfo.match(/SwapTotal:\s+(\d+) kB/);
89
- const swapFreeMatch = meminfo.match(/SwapFree:\s+(\d+) kB/);
90
-
91
- if (swapTotalMatch && swapFreeMatch) {
92
- const swapTotal = parseInt(swapTotalMatch[1]);
93
- const swapFree = parseInt(swapFreeMatch[1]);
94
- const swapUsed = swapTotal - swapFree;
95
-
96
- if (swapTotal > 0) {
97
- const swapUsedPercent = Math.round((swapUsed / swapTotal) * 100);
98
- const swapUsedMB = Math.round(swapUsed / 1024);
99
- return `${swapUsedPercent}% (${swapUsedMB}MB) swap`;
111
+ if (swapTotalMatch && swapFreeMatch) {
112
+ const swapTotal = parseInt(swapTotalMatch[1]);
113
+ const swapFree = parseInt(swapFreeMatch[1]);
114
+ const swapUsed = swapTotal - swapFree;
115
+
116
+ if (swapTotal > 0) {
117
+ const swapUsedPercent = Math.round((swapUsed / swapTotal) * 100);
118
+ const swapUsedMB = Math.round(swapUsed / 1024);
119
+ return `${swapUsedPercent}% (${swapUsedMB}MB) swap`;
120
+ }
100
121
  }
101
- }
102
- } catch {}
122
+ } catch {}
123
+ return "";
124
+ }
125
+
126
+ if (IS_MAC) {
127
+ try {
128
+ const swapusage = execCommand("sysctl -n vm.swapusage");
129
+ const match = swapusage.match(
130
+ /total\s*=\s*([\d.]+)M\s+used\s*=\s*([\d.]+)M\s+free\s*=\s*([\d.]+)M/
131
+ );
132
+
133
+ if (match) {
134
+ const swapTotal = parseFloat(match[1]);
135
+ const swapUsedMB = parseFloat(match[2]);
136
+
137
+ if (swapTotal > 0) {
138
+ const swapUsedPercent = Math.round((swapUsedMB / swapTotal) * 100);
139
+ return `${swapUsedPercent}% (${Math.round(swapUsedMB)}MB) swap`;
140
+ }
141
+
142
+ return "0% (0MB) swap";
143
+ }
144
+ } catch {}
145
+ return "";
146
+ }
147
+
103
148
  return "";
104
149
  }
105
150
 
106
151
  /**
107
152
  * Gets root filesystem disk usage percentage
108
- * Uses df command on Linux/Android
153
+ * Uses df command on Linux/Android/macOS
109
154
  * @param context - Info context with cache
110
155
  * @returns Percentage string or empty string
111
156
  * @example "45%", "78%"
@@ -114,7 +159,7 @@ export function disk_used(context: InfoContext): string {
114
159
  const cached = getCachedValue(context.cache, "disk_used");
115
160
  if (cached !== null) return cached;
116
161
 
117
- if (IS_LINUX) {
162
+ if (IS_LINUX || IS_MAC) {
118
163
  try {
119
164
  const df = execCommand("df -h");
120
165
  let diskUsage = "";
@@ -151,35 +196,108 @@ export function disk_used(context: InfoContext): string {
151
196
  }
152
197
 
153
198
  /**
154
- * Gets system load averages
155
- * Reads 1, 5, and 15 minute load averages from /proc/loadavg (Linux only)
156
- * @returns Space-separated load averages (1m 5m 15m) or empty string
157
- * @example "0.52 0.58 0.59", "2.10 1.95 1.88"
199
+ * Minimum total size (in KB) for a partition to be considered a real disk
200
+ * rather than a small system/boot partition
158
201
  */
159
- export function load_average(): string {
160
- if (!IS_LINUX) return "";
202
+ const MIN_REAL_DISK_KB = 10 * 1024 * 1024; // 10GB
203
+
204
+ /**
205
+ * Gets disk size (used/total) for each real disk
206
+ * Uses `df -k` on Linux/macOS, filtering out pseudo filesystems, system/boot
207
+ * partitions (e.g. /boot, /System/Volumes/*), and anything under 10GB, since
208
+ * those aren't disks a user would care about. When exactly one real disk is
209
+ * found (the common case) the result omits the mount point; when multiple
210
+ * real disks are found (e.g. a data drive or external volume), each is
211
+ * labeled by its mount point.
212
+ * @param context - Info context with cache
213
+ * @returns Disk size as "used/total GB" or "mount(used/total GB) ..." or empty string
214
+ * @example "256/512GB", "/(100/500GB) /Volumes/Backup(900/2000GB)"
215
+ */
216
+ export function disk_size(context: InfoContext): string {
217
+ const cached = getCachedValue(context.cache, "disk_size");
218
+ if (cached !== null) return cached;
219
+
220
+ if (!IS_LINUX && !IS_MAC) {
221
+ setCachedValue(context.cache, "disk_size", "");
222
+ return "";
223
+ }
161
224
 
162
225
  try {
163
- const loadavg = fs.readFileSync("/proc/loadavg", "utf8");
164
- const loads = loadavg.split(" ").slice(0, 3);
165
- return loads.join(" ");
226
+ const df = execCommand("df -k");
227
+ const lines = df.trim().split("\n").slice(1);
228
+ const disks: { mount: string; usedGB: number; totalGB: number }[] = [];
229
+
230
+ for (const line of lines) {
231
+ const parts = line.trim().split(/\s+/);
232
+ if (parts.length < 6) continue;
233
+
234
+ const filesystem = parts[0];
235
+ const totalKB = parseInt(parts[1]);
236
+ const usedKB = parseInt(parts[2]);
237
+ const mount = parts[parts.length - 1];
238
+
239
+ if (isNaN(totalKB) || isNaN(usedKB) || totalKB < MIN_REAL_DISK_KB) continue;
240
+
241
+ if (IS_MAC) {
242
+ // Only the boot disk ("/") and externally mounted real disks count.
243
+ // APFS internal system volumes (Data, VM, Preboot, Update, ...) all
244
+ // live under /System/Volumes and aren't separate physical disks.
245
+ if (mount !== "/" && !mount.startsWith("/Volumes/")) continue;
246
+ } else {
247
+ if (
248
+ filesystem.startsWith("devtmpfs") ||
249
+ filesystem.startsWith("tmpfs") ||
250
+ filesystem === "overlay" ||
251
+ mount.startsWith("/boot") ||
252
+ mount.startsWith("/dev") ||
253
+ mount.startsWith("/proc") ||
254
+ mount.startsWith("/sys") ||
255
+ mount.startsWith("/run")
256
+ ) {
257
+ continue;
258
+ }
259
+ }
260
+
261
+ disks.push({
262
+ mount,
263
+ usedGB: Math.round(usedKB / (1024 * 1024)),
264
+ totalGB: Math.round(totalKB / (1024 * 1024)),
265
+ });
266
+ }
267
+
268
+ let result = "";
269
+ if (disks.length === 1) {
270
+ result = `${disks[0].usedGB}/${disks[0].totalGB}GB`;
271
+ } else if (disks.length > 1) {
272
+ result = disks
273
+ .map((d) => `${d.mount}(${d.usedGB}/${d.totalGB}GB)`)
274
+ .join(" ");
275
+ }
276
+
277
+ setCachedValue(context.cache, "disk_size", result);
278
+ return result;
166
279
  } catch {}
280
+
281
+ setCachedValue(context.cache, "disk_size", "");
167
282
  return "";
168
283
  }
169
284
 
170
285
  /**
171
286
  * Gets mounted filesystem information
172
- * Lists non-system mount points with usage from df (Linux only)
173
- * Excludes /, /dev, /proc, and /sys mounts
287
+ * Lists non-system mount points with usage from df (Linux/macOS)
288
+ * On Linux, excludes /, /dev, /proc, /sys and internal /System/Volumes mounts.
289
+ * On macOS, APFS internal volumes (Data, VM, Preboot, Update, ...) all live
290
+ * under /System/Volumes and are not user-relevant, so only /Volumes (external
291
+ * disks, network shares, disk images) are reported.
174
292
  * @param context - Info context with cache
175
293
  * @returns Space-separated mount points with usage or empty string
176
- * @example "/home(45%) /mnt/data(78%)", "/media/usb(12%)"
294
+ * @example "/home(45%) /mnt/data(78%)", "/Volumes/Backup(78%)"
177
295
  */
178
296
  export function mount_points(context: InfoContext): string {
179
297
  const cached = getCachedValue(context.cache, "mount_points");
180
298
  if (cached !== null) return cached;
181
299
 
182
- if (!IS_LINUX) {
300
+ if (!IS_LINUX && !IS_MAC) {
183
301
  setCachedValue(context.cache, "mount_points", "");
184
302
  return "";
185
303
  }
@@ -191,17 +309,29 @@ export function mount_points(context: InfoContext): string {
191
309
 
192
310
  lines.forEach((line) => {
193
311
  const parts = line.trim().split(/\s+/);
194
- if (parts.length >= 6) {
195
- const mountPoint = parts[5];
196
- const usage = parts[4];
197
- if (
198
- !mountPoint.startsWith("/dev") &&
199
- !mountPoint.startsWith("/proc") &&
200
- !mountPoint.startsWith("/sys") &&
201
- mountPoint !== "/"
202
- ) {
312
+ if (parts.length < 2) return;
313
+
314
+ const mountPoint = parts[parts.length - 1];
315
+ const usage = parts.find((part) => /^\d+%$/.test(part)) || "";
316
+
317
+ if (!usage) return;
318
+
319
+ if (IS_MAC) {
320
+ // Skip the "Macintosh HD" self-symlink and any other symlink back to /
321
+ if (mountPoint.startsWith("/Volumes/") && mountPoint !== "/") {
203
322
  mountPoints.push(`${mountPoint}(${usage})`);
204
323
  }
324
+ return;
325
+ }
326
+
327
+ if (
328
+ !mountPoint.startsWith("/dev") &&
329
+ !mountPoint.startsWith("/proc") &&
330
+ !mountPoint.startsWith("/sys") &&
331
+ !mountPoint.startsWith("/System/Volumes") &&
332
+ mountPoint !== "/"
333
+ ) {
334
+ mountPoints.push(`${mountPoint}(${usage})`);
205
335
  }
206
336
  });
207
337
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import os from "os";
7
7
  import type { InfoContext } from "../types/internal-types";
8
- import { IS_LINUX } from "../utils/platform";
8
+ import { IS_LINUX, IS_MAC } from "../utils/platform";
9
9
  import { execCommand } from "../utils/command";
10
10
  import { getCachedValue, setCachedValue } from "../cache/cache";
11
11
 
@@ -135,16 +135,16 @@ export async function isp(context: InfoContext): Promise<string> {
135
135
 
136
136
  /**
137
137
  * Gets active network interface names
138
- * Lists non-loopback interfaces with IPv4 addresses (Linux only)
138
+ * Lists non-loopback interfaces with IPv4 addresses (Linux/macOS only)
139
139
  * @param context - Info context with cache
140
140
  * @returns Space-separated interface names or empty string
141
- * @example "eth0 wlan0", "enp0s3"
141
+ * @example "eth0 wlan0", "en0 en1"
142
142
  */
143
143
  export function network_interfaces(context: InfoContext): string {
144
144
  const cached = getCachedValue(context.cache, "network_interfaces");
145
145
  if (cached !== null) return cached;
146
146
 
147
- if (!IS_LINUX) {
147
+ if (!IS_LINUX && !IS_MAC) {
148
148
  setCachedValue(context.cache, "network_interfaces", "");
149
149
  return "";
150
150
  }
@@ -174,7 +174,7 @@ export function network_interfaces(context: InfoContext): string {
174
174
  }
175
175
  /**
176
176
  * Gets open TCP ports with service names
177
- * Uses lsof to find listening TCP ports (Linux only)
177
+ * Uses lsof to find listening TCP ports (Linux/macOS only)
178
178
  * @param context - Info context with cache
179
179
  * @returns Space-separated port+process pairs or empty string
180
180
  * @example "80http 443http 22ssh", "3000node 5432post"
@@ -183,7 +183,7 @@ export function ports(context: InfoContext): string {
183
183
  const cached = getCachedValue(context.cache, "ports");
184
184
  if (cached !== null) return cached;
185
185
 
186
- if (IS_LINUX) {
186
+ if (IS_LINUX || IS_MAC) {
187
187
  try {
188
188
  const lsof = execCommand("lsof -nP -iTCP -sTCP:LISTEN");
189
189
  const lines = lsof.split("\n").slice(1);
@@ -50,7 +50,12 @@ export function os_info(context: InfoContext): string {
50
50
  osName = `Windows ${release}`;
51
51
  }
52
52
  } else if (IS_MAC) {
53
- osName = `macOS ${release}`;
53
+ const productName = execCommand("sw_vers -productName");
54
+ const productVersion = execCommand("sw_vers -productVersion");
55
+ osName =
56
+ productName && productVersion
57
+ ? `${productName} ${productVersion}`
58
+ : `macOS ${release}`;
54
59
  } else if (IS_LINUX) {
55
60
  try {
56
61
  const osRelease = fs.readFileSync("/etc/os-release", "utf8");
@@ -119,6 +124,14 @@ export function device(context: InfoContext): string {
119
124
  return device;
120
125
  }
121
126
  } catch {}
127
+ } else if (IS_MAC) {
128
+ const profile = execCommand("system_profiler SPHardwareDataType");
129
+ const nameMatch = profile.match(/Model Name:\s*(.+)/);
130
+ if (nameMatch) {
131
+ const device = nameMatch[1].trim();
132
+ setCachedValue(context.cache, "device", device);
133
+ return device;
134
+ }
122
135
  } else if (IS_LINUX) {
123
136
  try {
124
137
  if (commandExists("getprop")) {
@@ -5,13 +5,13 @@
5
5
 
6
6
  import os from "os";
7
7
  import type { InfoContext } from "../types/internal-types";
8
- import { IS_LINUX } from "../utils/platform";
8
+ import { IS_LINUX, IS_MAC } from "../utils/platform";
9
9
  import { execCommand } from "../utils/command";
10
10
  import { getCachedValue, setCachedValue } from "../cache/cache";
11
11
 
12
12
  /**
13
13
  * Gets the highest CPU-consuming process
14
- * Uses ps command to find top process by CPU usage (Linux only)
14
+ * Uses ps command to find top process by CPU usage (Linux/macOS only)
15
15
  * @param context - Info context with cache
16
16
  * @returns Process info as "percentage processname" or empty string
17
17
  * @example "8% firefox", "15% chrome", "3% systemd"
@@ -33,6 +33,22 @@ export function top_process(context: InfoContext): string {
33
33
  return result;
34
34
  }
35
35
  } catch {}
36
+ } else if (IS_MAC) {
37
+ try {
38
+ // -c gives the short command name, -r sorts by %CPU descending
39
+ const ps = execCommand("ps -Aceo pcpu,comm -r");
40
+ const lines = ps.split("\n").filter((line) => line.trim());
41
+ if (lines.length > 1) {
42
+ const topProcess = lines[1].trim().split(/\s+/);
43
+ const cpuValue = Math.round(parseFloat(topProcess[0]));
44
+ const process = topProcess.slice(1).join(" ");
45
+ if (!isNaN(cpuValue) && process) {
46
+ const result = `${cpuValue}% ${process}`;
47
+ setCachedValue(context.cache, "top_process", result);
48
+ return result;
49
+ }
50
+ }
51
+ } catch {}
36
52
  }
37
53
 
38
54
  setCachedValue(context.cache, "top_process", "");
@@ -54,12 +70,12 @@ export function uptime(): string {
54
70
 
55
71
  /**
56
72
  * Gets number of logged in users
57
- * Uses who command to count active user sessions (Linux only)
73
+ * Uses who command to count active user sessions (Linux/macOS only)
58
74
  * @returns User count with "users" suffix or empty string
59
75
  * @example "3 users", "1 users"
60
76
  */
61
77
  export function users_logged_in(): string {
62
- if (!IS_LINUX) return "";
78
+ if (!IS_LINUX && !IS_MAC) return "";
63
79
 
64
80
  try {
65
81
  const who = execCommand("who");
@@ -37,6 +37,7 @@ export const DEFAULT_SETTINGS = {
37
37
  ["user", "hostname", "os", "device", "kernel", "cpu", "gpu", "bench"],
38
38
  [
39
39
  "disk_used",
40
+ "disk_size",
40
41
  "ram_used",
41
42
  "top_process",
42
43
  "uptime",
@@ -44,13 +45,14 @@ export const DEFAULT_SETTINGS = {
44
45
  "battery",
45
46
  "load_average",
46
47
  ],
47
- ["ip", "iplocal", "city", "domain", "isp"],
48
+ ["ip", "iplocal", "city"],
48
49
  ["shell", "services_running", "pacman", "containers"],
49
50
  ],
50
51
  colors: {
51
52
  user: "red",
52
53
  hostname: "orange",
53
54
  disk_used: "purple",
55
+ disk_size: "purple",
54
56
  ram_used: "yellow",
55
57
  top_process: "magenta",
56
58
  uptime: "cyan",
@@ -97,6 +99,7 @@ export const DEFAULT_SETTINGS = {
97
99
  shell: "🐚 ",
98
100
  pacman: "🚀 ",
99
101
  disk_used: "📁 ",
102
+ disk_size: "💽 ",
100
103
  ram_used: "💾 ",
101
104
  top_process: "🔝 ",
102
105
  uptime: "⏱️ ",
@@ -131,6 +134,7 @@ export const DEFAULT_SETTINGS = {
131
134
  shell: "Shell",
132
135
  pacman: "Packages",
133
136
  disk_used: "Disk",
137
+ disk_size: "Disk Size",
134
138
  ram_used: "RAM",
135
139
  top_process: "Top",
136
140
  uptime: "Uptime",
@@ -150,7 +154,7 @@ export const DEFAULT_SETTINGS = {
150
154
  },
151
155
  display: {
152
156
  show_emojis: true,
153
- single_line: false,
157
+ single_line: true,
154
158
  line_wrap_length: process?.stdout?.columns || 100,
155
159
  },
156
160
  network: {
@@ -168,6 +172,7 @@ export interface Settings {
168
172
  labels: Record<string, string>;
169
173
  display: {
170
174
  show_emojis: boolean;
175
+ /** true: print one continuous line and let the terminal soft-wrap it; false: hard-wrap at line_wrap_length */
171
176
  single_line: boolean;
172
177
  line_wrap_length: number;
173
178
  };
@@ -3,15 +3,17 @@
3
3
  * @module info/software
4
4
  */
5
5
 
6
+ import os from "os";
6
7
  import process from "process";
7
8
  import type { InfoContext } from "../types/internal-types";
8
- import { IS_LINUX } from "../utils/platform";
9
+ import { IS_WINDOWS } from "../utils/platform";
9
10
  import { execCommand, commandExists } from "../utils/command";
10
11
  import { getCachedValue, setCachedValue } from "../cache/cache";
11
12
 
12
13
  /**
13
14
  * Gets the current shell name
14
- * Uses ps command to find parent process shell (Linux/Unix only)
15
+ * Uses the user's login shell (macOS/Linux), falls back to ps for the
16
+ * parent process shell
15
17
  * @returns Shell name or empty string on Windows
16
18
  * @example "bash", "zsh", "fish", "nu"
17
19
  */
@@ -19,7 +21,16 @@ export function shell(context: InfoContext): string {
19
21
  const cached = getCachedValue(context.cache, "shell");
20
22
  if (cached !== null) return cached;
21
23
 
22
- if (IS_LINUX) {
24
+ if (!IS_WINDOWS) {
25
+ try {
26
+ const loginShell = os.userInfo().shell;
27
+ if (loginShell) {
28
+ const shellName = loginShell.split("/").pop() as string;
29
+ setCachedValue(context.cache, "shell", shellName);
30
+ return shellName;
31
+ }
32
+ } catch {}
33
+
23
34
  try {
24
35
  const ppid = process.ppid;
25
36
  const shellName = execCommand(`ps -p ${ppid} -o comm=`).split("/").pop();
@@ -47,6 +58,7 @@ export function packages(context: InfoContext): string {
47
58
 
48
59
  const commands = [
49
60
  "apt",
61
+ "brew",
50
62
  "npm",
51
63
  "uv",
52
64
  "docker",
@@ -6,17 +6,25 @@
6
6
  import fs from "fs";
7
7
  import os from "os";
8
8
  import type { InfoContext } from "../types/internal-types";
9
- import { IS_LINUX } from "../utils/platform";
9
+ import { IS_LINUX, IS_MAC } from "../utils/platform";
10
10
  import { execCommand, commandExists } from "../utils/command";
11
11
  import { getCachedValue, setCachedValue } from "../cache/cache";
12
12
 
13
13
  /**
14
14
  * Gets system load averages
15
- * Reads 1, 5, and 15 minute load averages from /proc/loadavg (Linux only)
15
+ * Reads 1, 5, and 15 minute load averages from /proc/loadavg (Linux) or
16
+ * os.loadavg() (macOS)
16
17
  * @returns Space-separated load averages (1m 5m 15m) or empty string
17
18
  * @example "0.52 0.58 0.59", "2.10 1.95 1.88"
18
19
  */
19
20
  export function load_average(context: InfoContext): string {
21
+ if (IS_MAC) {
22
+ return os
23
+ .loadavg()
24
+ .map((n) => n.toFixed(2))
25
+ .join(" ");
26
+ }
27
+
20
28
  if (!IS_LINUX) return "";
21
29
 
22
30
  try {
@@ -38,6 +46,22 @@ export function battery(context: InfoContext): string {
38
46
  const cached = getCachedValue(context.cache, "battery");
39
47
  if (cached !== null) return cached;
40
48
 
49
+ if (IS_MAC) {
50
+ try {
51
+ const batt = execCommand("pmset -g batt");
52
+ const percentMatch = batt.match(/(\d+)%/);
53
+ if (percentMatch) {
54
+ const isCharging = /;\s*(charging|charged);/i.test(batt);
55
+ const result = `${percentMatch[1]}%${isCharging ? "+" : ""}`;
56
+ setCachedValue(context.cache, "battery", result);
57
+ return result;
58
+ }
59
+ } catch {}
60
+
61
+ setCachedValue(context.cache, "battery", "");
62
+ return "";
63
+ }
64
+
41
65
  if (!IS_LINUX) {
42
66
  setCachedValue(context.cache, "battery", "");
43
67
  return "";
@@ -68,7 +92,9 @@ export function battery(context: InfoContext): string {
68
92
 
69
93
  /**
70
94
  * Gets system temperature in Celsius
71
- * Reads from thermal zone or hwmon sensors (Linux only)
95
+ * Reads from thermal zone or hwmon sensors (Linux), or from the
96
+ * `osx-cpu-temp` CLI (macOS, if installed via `brew install osx-cpu-temp`) -
97
+ * macOS has no unprivileged API for this, so nothing is reported without it
72
98
  * @param context - Info context with cache
73
99
  * @returns Temperature with °C suffix or empty string
74
100
  * @example "45°C", "62°C"
@@ -77,6 +103,26 @@ export function temperature(context: InfoContext): string {
77
103
  const cached = getCachedValue(context.cache, "temperature");
78
104
  if (cached !== null) return cached;
79
105
 
106
+ if (IS_MAC) {
107
+ if (commandExists("osx-cpu-temp")) {
108
+ try {
109
+ const output = execCommand("osx-cpu-temp");
110
+ const match = output.match(/([\d.]+)\s*°?C/);
111
+ if (match) {
112
+ const tempC = Math.round(parseFloat(match[1]));
113
+ if (tempC > 0 && tempC < 150) {
114
+ const result = `${tempC}°C`;
115
+ setCachedValue(context.cache, "temperature", result);
116
+ return result;
117
+ }
118
+ }
119
+ } catch {}
120
+ }
121
+
122
+ setCachedValue(context.cache, "temperature", "");
123
+ return "";
124
+ }
125
+
80
126
  if (!IS_LINUX) {
81
127
  setCachedValue(context.cache, "temperature", "");
82
128
  return "";
@@ -118,6 +164,24 @@ export function services_running(context: InfoContext): string {
118
164
  const cached = getCachedValue(context.cache, "services_running");
119
165
  if (cached !== null) return cached;
120
166
 
167
+ if (IS_MAC) {
168
+ try {
169
+ const services = execCommand("launchctl list");
170
+ const serviceCount = services
171
+ .split("\n")
172
+ .filter((line) => line.trim() && !line.startsWith("PID")).length;
173
+
174
+ if (serviceCount > 0) {
175
+ const result = `${serviceCount} services`;
176
+ setCachedValue(context.cache, "services_running", result);
177
+ return result;
178
+ }
179
+ } catch {}
180
+
181
+ setCachedValue(context.cache, "services_running", "");
182
+ return "";
183
+ }
184
+
121
185
  if (!IS_LINUX) {
122
186
  setCachedValue(context.cache, "services_running", "");
123
187
  return "";