summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--MACOS.md71
-rw-r--r--README.md15
-rw-r--r--internal/collector/collector.go19
-rw-r--r--internal/collector/loadbars-remote-darwin.sh77
-rw-r--r--internal/collector/loadbars-remote.sh35
-rw-r--r--internal/collector/script.go13
-rw-r--r--internal/display/activate.go8
-rw-r--r--internal/display/activate_darwin.go24
-rw-r--r--internal/display/display.go3
10 files changed, 263 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index e9293f1..aa902ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.serena/
/loadbars
+.claude/
diff --git a/MACOS.md b/MACOS.md
new file mode 100644
index 0000000..ba6efda
--- /dev/null
+++ b/MACOS.md
@@ -0,0 +1,71 @@
+# macOS Support for Loadbars
+
+## What was implemented
+
+Loadbars now fully supports macOS with automatic window activation built into the binary.
+
+### Changes made:
+
+1. **Script embedding** - Both Linux and Darwin monitoring scripts are embedded in the binary
+2. **OS detection** - Automatically uses the correct script based on the host:
+ - `localhost` on macOS → Darwin script (sysctl, vm_stat, netstat, iostat)
+ - `localhost` on Linux → Linux script (/proc filesystem)
+ - All remote hosts → Linux script (assumes remote servers are Linux)
+3. **Window activation** - macOS-specific code automatically brings SDL window to foreground
+ - Uses build tags (`activate_darwin.go` and `activate.go`)
+ - No external helper script needed
+
+## Usage on macOS
+
+Simply run the binary directly:
+
+```bash
+# Monitor localhost
+./loadbars --showcores --showmem --shownet
+
+# Monitor remote Linux servers
+./loadbars server1.example.com server2.example.com --showcores
+
+# Monitor both localhost and remotes
+./loadbars localhost server1.example.com server2.example.com --showcores
+```
+
+The window will automatically appear in the foreground.
+
+## Building on macOS
+
+Requirements:
+```bash
+brew install sdl2
+```
+
+Build:
+```bash
+mage build
+# or
+go build -o loadbars ./cmd/loadbars
+```
+
+## Technical details
+
+### Script selection logic
+- `internal/collector/script.go` - Embeds both scripts
+- `internal/collector/collector.go` - Selects script based on host:
+ - For localhost: checks if `/proc` exists to determine Linux vs macOS
+ - For remote hosts: always uses Linux script
+
+### Window activation
+- `internal/display/activate_darwin.go` - macOS-specific activation using `open -a`
+- `internal/display/activate.go` - No-op for other platforms
+- Called automatically after SDL window creation
+
+### Darwin monitoring script
+- Uses macOS native tools: `sysctl`, `vm_stat`, `netstat -ibn`, `iostat`
+- Outputs same protocol format as Linux script (M LOADAVG, M MEMSTATS, etc.)
+- Limitations: No per-core CPU stats on macOS (iostat limitation)
+
+## Known limitations
+
+- Remote macOS hosts are not supported (assumes all remote hosts are Linux)
+- macOS script doesn't provide per-core CPU statistics (iostat limitation)
+- Swap usage on macOS always shows 0 (macOS uses compressed memory differently)
diff --git a/README.md b/README.md
index 5f3f938..d06873a 100644
--- a/README.md
+++ b/README.md
@@ -10,8 +10,11 @@ loadbars [LIST OF HOSTNAMES] [OPTIONS]
### Tested platforms
-This version of loadbars has been tested on Fedora Linux 43 and should work on
-most modern Linux distributions (RHEL, CentOS, Ubuntu, Debian, etc.).
+This version of loadbars has been tested on:
+- Fedora Linux 43 and most modern Linux distributions (RHEL, CentOS, Ubuntu, Debian, etc.)
+- macOS (Darwin) - localhost monitoring uses native macOS tools (sysctl, vm_stat, netstat, iostat)
+
+**Note:** Remote hosts are assumed to be Linux (using /proc filesystem). When running on macOS, localhost monitoring uses macOS-specific commands, while remote hosts use the Linux script.
### I like flying elephants
@@ -102,6 +105,14 @@ On Ubuntu/Debian:
sudo apt install libsdl2-dev
```
+On macOS:
+
+```bash
+brew install sdl2
+```
+
+**macOS Note:** The window will automatically come to the foreground when launched. No additional scripts needed.
+
### Running from Source
To run loadbars directly from the source directory:
diff --git a/internal/collector/collector.go b/internal/collector/collector.go
index dea88c7..cab9df0 100644
--- a/internal/collector/collector.go
+++ b/internal/collector/collector.go
@@ -25,7 +25,14 @@ type StatsStore interface {
// The script is embedded in the binary; no external script file is required.
func Run(ctx context.Context, host string, cfg *config.Config, store StatsStore) error {
hostKey, user := splitHostUser(host)
- script := bytes.NewReader(RemoteScript)
+
+ // Select script: Darwin for localhost on macOS, Linux for everything else (all remotes are Linux)
+ scriptBytes := LinuxScript
+ if isLocal(hostKey) {
+ scriptBytes = getLocalScript()
+ }
+
+ script := bytes.NewReader(scriptBytes)
var scanner *bufio.Scanner
if isLocal(hostKey) {
cmd := exec.CommandContext(ctx, "bash", "-s")
@@ -121,3 +128,13 @@ func splitHostUser(host string) (h, u string) {
func isLocal(h string) bool {
return h == "localhost" || h == "127.0.0.1"
}
+
+// getLocalScript returns the appropriate script for the local OS
+func getLocalScript() []byte {
+ // Check if /proc exists (Linux/Unix)
+ if _, err := exec.Command("test", "-d", "/proc").CombinedOutput(); err == nil {
+ return LinuxScript
+ }
+ // Otherwise assume macOS
+ return DarwinScript
+}
diff --git a/internal/collector/loadbars-remote-darwin.sh b/internal/collector/loadbars-remote-darwin.sh
new file mode 100644
index 0000000..f82c802
--- /dev/null
+++ b/internal/collector/loadbars-remote-darwin.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+# loadbars-remote-darwin.sh - macOS version using sysctl, vm_stat, netstat, and iostat
+# Emits loadbars protocol (M LOADAVG, M MEMSTATS, M NETSTATS, M CPUSTATS)
+# Interval for CPU sampling (seconds)
+INTERVAL=0.14
+
+# Get number of CPUs
+NCPU=$(sysctl -n hw.ncpu)
+
+while true; do
+ # Load average: from sysctl
+ echo "M LOADAVG"
+ sysctl -n vm.loadavg 2>/dev/null | awk '{print $2";"$3";"$4}' || echo "0;0;0"
+
+ # Memory: convert vm_stat output to /proc/meminfo-like format
+ echo "M MEMSTATS"
+ vm_stat 2>/dev/null | awk '
+ BEGIN { pagesize = 4096 }
+ /page size of ([0-9]+)/ { pagesize = $8 }
+ /Pages free:/ { free = $3 * pagesize / 1024 }
+ /Pages active:/ { active = $3 * pagesize / 1024 }
+ /Pages inactive:/ { inactive = $3 * pagesize / 1024 }
+ /Pages speculative:/ { speculative = $3 * pagesize / 1024 }
+ /Pages wired down:/ { wired = $4 * pagesize / 1024 }
+ /Pages occupied by compressor:/ { compressed = $5 * pagesize / 1024 }
+ END {
+ total = free + active + inactive + speculative + wired + compressed
+ printf "MemTotal: %d kB\n", total
+ printf "MemFree: %d kB\n", free
+ printf "MemAvailable: %d kB\n", free + inactive + speculative
+ printf "SwapTotal: 0 kB\n"
+ printf "SwapFree: 0 kB\n"
+ }
+ '
+
+ # Network: use netstat -ibn for interface stats
+ echo "M NETSTATS"
+ netstat -ibn 2>/dev/null | awk '
+ NR > 1 && $1 !~ /^Name/ && $3 ~ /^<Link/ {
+ # netstat -ibn output on macOS:
+ # Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll
+ iface = $1
+ ipkts = $5
+ ierrs = $6
+ ibytes = $7
+ opkts = $8
+ oerrs = $9
+ obytes = $10
+
+ if (ibytes ~ /^[0-9]+$/ && obytes ~ /^[0-9]+$/) {
+ printf "%s:b=%s;tb=%s;p=%s;tp=%s e=%s;te=%s;d=0;td=0\n",
+ iface, ibytes, obytes, ipkts, opkts, ierrs, oerrs
+ }
+ }
+ '
+
+ # CPU: macOS doesn't have /proc/stat, use iostat for CPU percentages
+ echo "M CPUSTATS"
+ for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
+ # iostat output on macOS: user sys idle
+ # Convert to /proc/stat format: cpu user nice system idle iowait irq softirq steal guest guest_nice
+ # We'll simulate values since macOS doesn't provide all fields
+ iostat -c 1 2>/dev/null | tail -1 | awk -v ncpu="$NCPU" '
+ {
+ # iostat columns: %user %nice %sys %idle (approximately)
+ # Multiply by ncpu to get total ticks (simulated)
+ user = int($3 * ncpu * 10)
+ sys = int($4 * ncpu * 10)
+ idle = int($5 * ncpu * 10)
+
+ # Output in /proc/stat format
+ printf "cpu %d 0 %d %d 0 0 0 0 0 0\n", user, sys, idle
+ }
+ '
+ sleep "$INTERVAL" 2>/dev/null || true
+ done
+done
diff --git a/internal/collector/loadbars-remote.sh b/internal/collector/loadbars-remote.sh
new file mode 100644
index 0000000..9037ad8
--- /dev/null
+++ b/internal/collector/loadbars-remote.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# loadbars-remote.sh - Emits loadbars protocol (M LOADAVG, M MEMSTATS, M NETSTATS, M CPUSTATS)
+# for local or remote execution. No Perl required.
+# Usage: bash loadbars-remote.sh
+# Interval for CPU sampling (seconds)
+INTERVAL=0.14
+
+while true; do
+ # Load average: first 3 fields of /proc/loadavg joined by ;
+ echo "M LOADAVG"
+ read -r l1 l5 l15 _ < /proc/loadavg 2>/dev/null || true
+ echo "${l1:-0};${l5:-0};${l15:-0}"
+
+ # Memory: full /proc/meminfo
+ echo "M MEMSTATS"
+ cat /proc/meminfo 2>/dev/null || true
+
+ # Network: /proc/net/dev, skip 2 header lines, then "iface: rx... tx..."
+ echo "M NETSTATS"
+ while IFS= read -r line; do
+ line="${line/:/ }"
+ set -- $line
+ # $1=iface, $2=rx_bytes $3=rx_packets $4=rx_errs $5=rx_drop ... $10=tx_bytes $11=tx_packets $12=tx_errs $13=tx_drop
+ if [ -n "$2" ] || [ -n "${10:-}" ]; then
+ echo "$1:b=${2:-0};tb=${10:-0};p=${3:-0};tp=${11:-0} e=${4:-0};te=${12:-0};d=${5:-0};td=${13:-0}"
+ fi
+ done < <(tail -n +3 /proc/net/dev 2>/dev/null)
+
+ # CPU: /proc/stat, 20 times with INTERVAL sleep
+ echo "M CPUSTATS"
+ for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
+ cat /proc/stat 2>/dev/null || true
+ sleep "$INTERVAL" 2>/dev/null || true
+ done
+done
diff --git a/internal/collector/script.go b/internal/collector/script.go
new file mode 100644
index 0000000..3be2190
--- /dev/null
+++ b/internal/collector/script.go
@@ -0,0 +1,13 @@
+package collector
+
+import _ "embed"
+
+// LinuxScript contains the embedded loadbars-remote.sh script for Linux hosts
+//
+//go:embed loadbars-remote.sh
+var LinuxScript []byte
+
+// DarwinScript contains the embedded loadbars-remote-darwin.sh script for macOS hosts
+//
+//go:embed loadbars-remote-darwin.sh
+var DarwinScript []byte
diff --git a/internal/display/activate.go b/internal/display/activate.go
new file mode 100644
index 0000000..b9040d7
--- /dev/null
+++ b/internal/display/activate.go
@@ -0,0 +1,8 @@
+// +build !darwin
+
+package display
+
+// activateWindow is a no-op on non-macOS platforms
+func activateWindow() {
+ // Nothing needed on Linux/other platforms
+}
diff --git a/internal/display/activate_darwin.go b/internal/display/activate_darwin.go
new file mode 100644
index 0000000..54c6b94
--- /dev/null
+++ b/internal/display/activate_darwin.go
@@ -0,0 +1,24 @@
+// +build darwin
+
+package display
+
+import (
+ "os"
+ "os/exec"
+ "time"
+)
+
+// activateWindow brings the SDL window to the foreground on macOS
+func activateWindow() {
+ // Give SDL a moment to create the window
+ go func() {
+ time.Sleep(500 * time.Millisecond)
+ // Get the executable path
+ execPath, err := os.Executable()
+ if err != nil {
+ return
+ }
+ // Use open -a to bring the window to foreground
+ exec.Command("open", "-a", execPath).Run()
+ }()
+}
diff --git a/internal/display/display.go b/internal/display/display.go
index 231b8c6..10a0b04 100644
--- a/internal/display/display.go
+++ b/internal/display/display.go
@@ -87,6 +87,9 @@ func Run(ctx context.Context, cfg *config.Config, src stats.Source) error {
defer renderer.Destroy()
window.SetTitle(title)
+ // On macOS, bring the window to the foreground
+ activateWindow()
+
state := newRunState(cfg, int32(width), int32(height))
ticker := time.NewTicker(time.Duration(constants.IntervalSDL * float64(time.Second)))
defer ticker.Stop()