#![allow(unexpected_cfgs)]
#![allow(unsafe_op_in_unsafe_fn)]
use cocoa::appkit::{
NSApp, NSApplication, NSApplicationActivationPolicyProhibited, NSMenu, NSMenuItem, NSStatusBar,
NSStatusItem, NSVariableStatusItemLength,
};
use cocoa::base::{YES, id, nil};
use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::OnceLock;
use sysinfo::{Disks, System};
const THEME_NAMES: [&str; 3] = ["Classic", "Mono", "Transparent"];
const WARN_THRESHOLD_OPTIONS: [f64; 9] = [50.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0, 90.0];
const CRIT_THRESHOLD_OPTIONS: [f64; 7] = [70.0, 75.0, 80.0, 85.0, 90.0, 95.0, 99.0];
const LAUNCH_AGENT_LABEL: &str = "com.pnkjsng.mstat";
struct StatsSnapshot {
cpu_used_pct: f32,
mem_used: u64,
mem_total: u64,
mem_used_pct: f32,
storage_used: u64,
storage_total: u64,
storage_used_pct: f32,
swap_used: u64,
swap_total: u64,
swap_used_pct: f32,
load_1m: f32,
load_used_pct: f32,
}
fn format_gib(bytes: u64) -> String {
format!("{:.1}G", bytes as f64 / 1024.0 / 1024.0 / 1024.0)
}
fn stats_snapshot(system: &mut System, disks: &mut Disks) -> StatsSnapshot {
system.refresh_cpu_usage();
system.refresh_memory();
disks.refresh(false);
let cpu_used_pct = system.global_cpu_usage();
let mem_total = system.total_memory();
let mem_used = system.used_memory();
let mem_used_pct = if mem_total == 0 {
0.0
} else {
mem_used as f32 * 100.0 / mem_total as f32
};
let mut storage_used = 0_u64;
let mut storage_total = 0_u64;
if let Some(root_disk) = disks
.iter()
.find(|d| d.mount_point().to_string_lossy() == "/")
.or_else(|| disks.iter().next())
{
storage_total = root_disk.total_space();
storage_used = storage_total.saturating_sub(root_disk.available_space());
}
let storage_used_pct = if storage_total == 0 {
0.0
} else {
storage_used as f32 * 100.0 / storage_total as f32
};
let swap_total = system.total_swap();
let swap_used = system.used_swap();
let swap_used_pct = if swap_total == 0 {
0.0
} else {
swap_used as f32 * 100.0 / swap_total as f32
};
let load_1m = System::load_average().one as f32;
let cores = system.cpus().len().max(1) as f32;
let load_used_pct = (load_1m / cores * 100.0).clamp(0.0, 100.0);
StatsSnapshot {
cpu_used_pct,
mem_used,
mem_total,
mem_used_pct,
storage_used,
storage_total,
storage_used_pct,
swap_used,
swap_total,
swap_used_pct,
load_1m,
load_used_pct,
}
}
unsafe fn ns_string(text: &str) -> id {
unsafe { NSString::alloc(nil).init_str(text) }
}
fn theme_color(theme: usize, level: usize) -> (f64, f64, f64, f64) {
match (theme % THEME_NAMES.len(), level) {
(0, 0) => (0.23, 0.61, 0.37, 0.88),
(0, 1) => (0.80, 0.55, 0.23, 0.88),
(0, _) => (0.74, 0.28, 0.28, 0.88),
(1, 0) => (0.38, 0.38, 0.38, 0.88),
(1, 1) => (0.55, 0.55, 0.55, 0.88),
(1, _) => (0.72, 0.72, 0.72, 0.90),
(2, _) => (0.0, 0.0, 0.0, 0.0),
(_, _) => (0.72, 0.72, 0.72, 0.90),
}
}
fn usage_level(used_pct: f32, warn: f64, crit: f64) -> usize {
if used_pct as f64 >= crit {
2
} else if used_pct as f64 >= warn {
1
} else {
0
}
}
unsafe fn usage_color(level: usize, theme: usize) -> id {
let (r, g, b, a) = theme_color(theme, level);
unsafe {
msg_send![
class!(NSColor),
colorWithCalibratedRed: r
green: g
blue: b
alpha: a
]
}
}
unsafe fn level_text_color(level: usize) -> id {
match level {
2 => {
msg_send![class!(NSColor), colorWithCalibratedRed: 1.0_f64 green: 0.10_f64 blue: 0.10_f64 alpha: 1.0_f64]
}
1 => {
msg_send![class!(NSColor), colorWithCalibratedRed: 1.0_f64 green: 0.72_f64 blue: 0.06_f64 alpha: 1.0_f64]
}
_ => {
msg_send![class!(NSColor), colorWithCalibratedRed: 0.20_f64 green: 0.98_f64 blue: 0.36_f64 alpha: 1.0_f64]
}
}
}
unsafe fn create_value_label() -> id {
let label: id = unsafe { msg_send![class!(NSTextField), labelWithString: ns_string("")] };
let white: id = unsafe { msg_send![class!(NSColor), whiteColor] };
let font: id = unsafe { msg_send![class!(NSFont), systemFontOfSize: 12.0_f64] };
let _: () = unsafe { msg_send![label, setTextColor: white] };
let _: () = unsafe { msg_send![label, setFont: font] };
let _: () = unsafe { msg_send![label, setAlignment: 0_i64] };
label
}
unsafe fn create_icon_view(symbol_name: &str) -> id {
let icon_view: id = unsafe { msg_send![class!(NSImageView), alloc] };
let icon_view: id = unsafe {
msg_send![
icon_view,
initWithFrame: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(16.0, 16.0))
]
};
let image: id = unsafe {
msg_send![
class!(NSImage),
imageWithSystemSymbolName: ns_string(symbol_name)
accessibilityDescription: nil
]
};
if image != nil {
let _: () = unsafe { msg_send![image, setSize: NSSize::new(16.0, 16.0)] };
let _: () = unsafe { msg_send![icon_view, setImage: image] };
}
let white: id = unsafe { msg_send![class!(NSColor), whiteColor] };
let _: () = unsafe { msg_send![icon_view, setContentTintColor: white] };
icon_view
}
unsafe fn create_chip(symbol_name: &str) -> (id, id, id) {
let chip: id = unsafe { msg_send![class!(NSView), alloc] };
let chip: id = unsafe {
msg_send![
chip,
initWithFrame: NSRect::new(NSPoint::new(0.0, 2.0), NSSize::new(100.0, 18.0))
]
};
let _: () = unsafe { msg_send![chip, setWantsLayer: YES] };
let layer: id = unsafe { msg_send![chip, layer] };
let _: () = unsafe { msg_send![layer, setCornerRadius: 5.0_f64] };
let _: () = unsafe { msg_send![layer, setMasksToBounds: YES] };
let icon = unsafe { create_icon_view(symbol_name) };
let value = unsafe { create_value_label() };
let _: () = unsafe { msg_send![chip, addSubview: icon] };
let _: () = unsafe { msg_send![chip, addSubview: value] };
(chip, icon, value)
}
unsafe fn create_wrapper_box() -> id {
let wrapper: id = unsafe { msg_send![class!(NSView), alloc] };
let wrapper: id = unsafe {
msg_send![
wrapper,
initWithFrame: NSRect::new(NSPoint::new(2.0, 1.0), NSSize::new(260.0, 22.0))
]
};
let _: () = unsafe { msg_send![wrapper, setWantsLayer: YES] };
let layer: id = unsafe { msg_send![wrapper, layer] };
let _: () = unsafe { msg_send![layer, setCornerRadius: 7.0_f64] };
let _: () = unsafe { msg_send![layer, setMasksToBounds: YES] };
let _: () = unsafe { msg_send![layer, setBorderWidth: 0.8_f64] };
let border: id = unsafe { msg_send![class!(NSColor), colorWithWhite: 1.0_f64 alpha: 0.24_f64] };
let border_cg: id = unsafe { msg_send![border, CGColor] };
let _: () = unsafe { msg_send![layer, setBorderColor: border_cg] };
let bg: id = unsafe { msg_send![class!(NSColor), colorWithWhite: 0.0_f64 alpha: 0.18_f64] };
let bg_cg: id = unsafe { msg_send![bg, CGColor] };
let _: () = unsafe { msg_send![layer, setBackgroundColor: bg_cg] };
wrapper
}
unsafe fn update_chip(
chip: id,
icon: id,
value: id,
value_text: &str,
used_pct: f32,
warn: f64,
crit: f64,
theme: usize,
) {
let _: () = unsafe { msg_send![value, setStringValue: ns_string(value_text)] };
let _: () = unsafe { msg_send![value, sizeToFit] };
let value_frame: NSRect = unsafe { msg_send![value, frame] };
let value_w = value_frame.size.width;
let icon_x = 5.0;
let icon_size = 16.0;
let gap = 2.0;
let chip_h = 20.0;
let chip_w = icon_x + icon_size + gap + value_w + 5.0;
let _: () = unsafe {
msg_send![
icon,
setFrame: NSRect::new(NSPoint::new(icon_x, 2.0), NSSize::new(icon_size, icon_size))
]
};
let _: () = unsafe {
msg_send![
value,
setFrame: NSRect::new(
NSPoint::new(icon_x + icon_size + gap, 2.0),
NSSize::new(value_w, 15.0)
)
]
};
let chip_frame: NSRect = unsafe { msg_send![chip, frame] };
let _: () = unsafe {
msg_send![
chip,
setFrame: NSRect::new(chip_frame.origin, NSSize::new(chip_w, chip_h))
]
};
let level = usage_level(used_pct, warn, crit);
let color = unsafe { usage_color(level, theme) };
let cg_color: id = unsafe { msg_send![color, CGColor] };
let layer: id = unsafe { msg_send![chip, layer] };
let _: () = unsafe { msg_send![layer, setBackgroundColor: cg_color] };
let text_color: id = if theme == 2 {
unsafe { level_text_color(level) }
} else {
unsafe { msg_send![class!(NSColor), whiteColor] }
};
let _: () = unsafe { msg_send![value, setTextColor: text_color] };
let _: () = unsafe { msg_send![icon, setContentTintColor: text_color] };
}
unsafe fn layout_chip(chip: id, x: f64) -> f64 {
let frame: NSRect = unsafe { msg_send![chip, frame] };
let width = frame.size.width;
let _: () = unsafe {
msg_send![
chip,
setFrame: NSRect::new(NSPoint::new(x, 1.0), NSSize::new(width, 20.0))
]
};
x + width + 3.0
}
fn enabled_count(this: &Object) -> u8 {
let keys = ["show_cpu", "show_mem", "show_ssd", "show_swap", "show_load"];
keys.iter()
.map(|k| {
if unsafe { *this.get_ivar::<u8>(k) } == 0 {
0
} else {
1
}
})
.sum()
}
unsafe fn update_menu_titles(this: &Object) {
let warn: f64 = *this.get_ivar("warn_threshold");
let crit: f64 = *this.get_ivar("crit_threshold");
let warn_item = *this.get_ivar::<usize>("menu_warn_ptr") as id;
let crit_item = *this.get_ivar::<usize>("menu_crit_ptr") as id;
let _: () = msg_send![
warn_item,
setTitle: ns_string(&format!("Set warn threshold… ({:.0}%)", warn))
];
let _: () = msg_send![crit_item, setTitle: ns_string(&format!("Set critical threshold… ({:.0}%)", crit))];
}
unsafe fn update_threshold_checks(this: &Object) {
let warn = *this.get_ivar::<f64>("warn_threshold");
let crit = *this.get_ivar::<f64>("crit_threshold");
let warn_menu = *this.get_ivar::<usize>("menu_warn_menu_ptr") as id;
let crit_menu = *this.get_ivar::<usize>("menu_crit_menu_ptr") as id;
for (idx, value) in WARN_THRESHOLD_OPTIONS.iter().enumerate() {
let item: id = msg_send![warn_menu, itemAtIndex: idx as i64];
let _: () =
msg_send![item, setState: if (*value - warn).abs() < 0.5 { 1_i64 } else { 0_i64 }];
let _: () = msg_send![item, setEnabled: *value < crit];
}
for (idx, value) in CRIT_THRESHOLD_OPTIONS.iter().enumerate() {
let item: id = msg_send![crit_menu, itemAtIndex: idx as i64];
let _: () =
msg_send![item, setState: if (*value - crit).abs() < 0.5 { 1_i64 } else { 0_i64 }];
let _: () = msg_send![item, setEnabled: *value > warn];
}
}
unsafe fn update_theme_checks(this: &Object) {
let theme: usize = *this.get_ivar("theme_index");
let items = [
*this.get_ivar::<usize>("menu_theme_0_ptr") as id,
*this.get_ivar::<usize>("menu_theme_1_ptr") as id,
*this.get_ivar::<usize>("menu_theme_2_ptr") as id,
];
for (idx, item) in items.iter().enumerate() {
let _: () = msg_send![*item, setState: if idx == theme { 1_i64 } else { 0_i64 }];
}
}
unsafe fn toggle_flag(this: &mut Object, key: &str, sender: id) {
let current = *this.get_ivar::<u8>(key);
if current == 1 && enabled_count(this) == 1 {
let _: () = msg_send![sender, setState: 1_i64];
return;
}
let next: u8 = if current == 0 { 1 } else { 0 };
this.set_ivar(key, next);
let _: () = msg_send![sender, setState: if next == 1 { 1_i64 } else { 0_i64 }];
}
unsafe fn set_theme(this: &mut Object, idx: usize) {
this.set_ivar("theme_index", idx % THEME_NAMES.len());
update_theme_checks(this);
}
unsafe fn set_warn(this: &mut Object, value: f64) {
let crit = *this.get_ivar::<f64>("crit_threshold");
if value < crit {
this.set_ivar("warn_threshold", value);
}
update_menu_titles(this);
update_threshold_checks(this);
}
unsafe fn set_crit(this: &mut Object, value: f64) {
let warn = *this.get_ivar::<f64>("warn_threshold");
if value > warn {
this.set_ivar("crit_threshold", value);
}
update_menu_titles(this);
update_threshold_checks(this);
}
unsafe fn set_warn_from_sender(this: &mut Object, sender: id) {
let idx: i64 = msg_send![sender, tag];
if idx >= 0 && (idx as usize) < WARN_THRESHOLD_OPTIONS.len() {
set_warn(this, WARN_THRESHOLD_OPTIONS[idx as usize]);
}
}
unsafe fn set_crit_from_sender(this: &mut Object, sender: id) {
let idx: i64 = msg_send![sender, tag];
if idx >= 0 && (idx as usize) < CRIT_THRESHOLD_OPTIONS.len() {
set_crit(this, CRIT_THRESHOLD_OPTIONS[idx as usize]);
}
}
extern "C" fn toggle_cpu(this: &mut Object, _: Sel, sender: id) {
unsafe { toggle_flag(this, "show_cpu", sender) }
}
extern "C" fn toggle_mem(this: &mut Object, _: Sel, sender: id) {
unsafe { toggle_flag(this, "show_mem", sender) }
}
extern "C" fn toggle_ssd(this: &mut Object, _: Sel, sender: id) {
unsafe { toggle_flag(this, "show_ssd", sender) }
}
extern "C" fn toggle_swap(this: &mut Object, _: Sel, sender: id) {
unsafe { toggle_flag(this, "show_swap", sender) }
}
extern "C" fn toggle_load(this: &mut Object, _: Sel, sender: id) {
unsafe { toggle_flag(this, "show_load", sender) }
}
extern "C" fn toggle_percent(this: &mut Object, _: Sel, sender: id) {
unsafe { toggle_flag(this, "show_percent_only", sender) }
}
extern "C" fn noop(_: &mut Object, _: Sel, _: id) {}
extern "C" fn set_warn_from_menu(this: &mut Object, _: Sel, sender: id) {
unsafe { set_warn_from_sender(this, sender) }
}
extern "C" fn set_crit_from_menu(this: &mut Object, _: Sel, sender: id) {
unsafe { set_crit_from_sender(this, sender) }
}
extern "C" fn set_theme_0(this: &mut Object, _: Sel, _: id) {
unsafe { set_theme(this, 0) }
}
extern "C" fn set_theme_1(this: &mut Object, _: Sel, _: id) {
unsafe { set_theme(this, 1) }
}
extern "C" fn set_theme_2(this: &mut Object, _: Sel, _: id) {
unsafe { set_theme(this, 2) }
}
extern "C" fn tick(this: &Object, _: Sel, _: id) {
unsafe {
let status_item = *this.get_ivar::<usize>("status_item_ptr") as id;
let wrapper = *this.get_ivar::<usize>("wrapper_ptr") as id;
let system = &mut *(*this.get_ivar::<usize>("system_ptr") as *mut System);
let disks = &mut *(*this.get_ivar::<usize>("disks_ptr") as *mut Disks);
let cpu_chip = *this.get_ivar::<usize>("cpu_chip_ptr") as id;
let cpu_icon = *this.get_ivar::<usize>("cpu_icon_ptr") as id;
let cpu_value = *this.get_ivar::<usize>("cpu_value_ptr") as id;
let mem_chip = *this.get_ivar::<usize>("mem_chip_ptr") as id;
let mem_icon = *this.get_ivar::<usize>("mem_icon_ptr") as id;
let mem_value = *this.get_ivar::<usize>("mem_value_ptr") as id;
let ssd_chip = *this.get_ivar::<usize>("ssd_chip_ptr") as id;
let ssd_icon = *this.get_ivar::<usize>("ssd_icon_ptr") as id;
let ssd_value = *this.get_ivar::<usize>("ssd_value_ptr") as id;
let swap_chip = *this.get_ivar::<usize>("swap_chip_ptr") as id;
let swap_icon = *this.get_ivar::<usize>("swap_icon_ptr") as id;
let swap_value = *this.get_ivar::<usize>("swap_value_ptr") as id;
let load_chip = *this.get_ivar::<usize>("load_chip_ptr") as id;
let load_icon = *this.get_ivar::<usize>("load_icon_ptr") as id;
let load_value = *this.get_ivar::<usize>("load_value_ptr") as id;
let show_cpu = *this.get_ivar::<u8>("show_cpu") == 1;
let show_mem = *this.get_ivar::<u8>("show_mem") == 1;
let show_ssd = *this.get_ivar::<u8>("show_ssd") == 1;
let show_swap = *this.get_ivar::<u8>("show_swap") == 1;
let show_load = *this.get_ivar::<u8>("show_load") == 1;
let show_percent_only = *this.get_ivar::<u8>("show_percent_only") == 1;
let warn = *this.get_ivar::<f64>("warn_threshold");
let crit = *this.get_ivar::<f64>("crit_threshold");
let theme = *this.get_ivar::<usize>("theme_index");
let stats = stats_snapshot(system, disks);
let mut x = 5.0;
if show_cpu {
update_chip(
cpu_chip,
cpu_icon,
cpu_value,
&format!("{:.0}%", stats.cpu_used_pct),
stats.cpu_used_pct,
warn,
crit,
theme,
);
x = layout_chip(cpu_chip, x);
} else {
let _: () = msg_send![cpu_chip, setFrame: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))];
}
if show_mem {
let mem_text = if show_percent_only {
format!("{:.0}%", stats.mem_used_pct)
} else {
format!(
"{}/{}",
format_gib(stats.mem_used),
format_gib(stats.mem_total)
)
};
update_chip(
mem_chip,
mem_icon,
mem_value,
&mem_text,
stats.mem_used_pct,
warn,
crit,
theme,
);
x = layout_chip(mem_chip, x);
} else {
let _: () = msg_send![mem_chip, setFrame: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))];
}
if show_ssd {
let ssd_text = if show_percent_only {
format!("{:.0}%", stats.storage_used_pct)
} else {
format!(
"{}/{}",
format_gib(stats.storage_used),
format_gib(stats.storage_total)
)
};
update_chip(
ssd_chip,
ssd_icon,
ssd_value,
&ssd_text,
stats.storage_used_pct,
warn,
crit,
theme,
);
x = layout_chip(ssd_chip, x);
} else {
let _: () = msg_send![ssd_chip, setFrame: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))];
}
if show_swap {
update_chip(
swap_chip,
swap_icon,
swap_value,
&format!(
"{}/{}",
format_gib(stats.swap_used),
format_gib(stats.swap_total)
),
stats.swap_used_pct,
warn,
crit,
theme,
);
x = layout_chip(swap_chip, x);
} else {
let _: () = msg_send![swap_chip, setFrame: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))];
}
if show_load {
update_chip(
load_chip,
load_icon,
load_value,
&format!("{:.2}", stats.load_1m),
stats.load_used_pct,
warn,
crit,
theme,
);
x = layout_chip(load_chip, x);
} else {
let _: () = msg_send![load_chip, setFrame: NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))];
}
let wrapper_w = x + 2.0;
let _: () = msg_send![
wrapper,
setFrame: NSRect::new(NSPoint::new(2.0, 1.0), NSSize::new(wrapper_w, 22.0))
];
let _: () = msg_send![status_item, setLength: wrapper_w + 3.0];
}
}
unsafe fn add_menu_item(menu: id, title: &str, action: Sel, target: id, state: Option<i64>) -> id {
let item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(ns_string(title), action, ns_string(""))
.autorelease();
let _: () = msg_send![item, setTarget: target];
if let Some(s) = state {
let _: () = msg_send![item, setState: s];
}
menu.addItem_(item);
item
}
fn updater_class() -> &'static Class {
static CLASS: OnceLock<&'static Class> = OnceLock::new();
CLASS.get_or_init(|| {
let superclass = class!(NSObject);
let mut decl = ClassDecl::new("RustStatsUpdater", superclass).expect("class declaration");
decl.add_ivar::<usize>("status_item_ptr");
decl.add_ivar::<usize>("wrapper_ptr");
decl.add_ivar::<usize>("system_ptr");
decl.add_ivar::<usize>("disks_ptr");
decl.add_ivar::<usize>("cpu_chip_ptr");
decl.add_ivar::<usize>("cpu_icon_ptr");
decl.add_ivar::<usize>("cpu_value_ptr");
decl.add_ivar::<usize>("mem_chip_ptr");
decl.add_ivar::<usize>("mem_icon_ptr");
decl.add_ivar::<usize>("mem_value_ptr");
decl.add_ivar::<usize>("ssd_chip_ptr");
decl.add_ivar::<usize>("ssd_icon_ptr");
decl.add_ivar::<usize>("ssd_value_ptr");
decl.add_ivar::<usize>("swap_chip_ptr");
decl.add_ivar::<usize>("swap_icon_ptr");
decl.add_ivar::<usize>("swap_value_ptr");
decl.add_ivar::<usize>("load_chip_ptr");
decl.add_ivar::<usize>("load_icon_ptr");
decl.add_ivar::<usize>("load_value_ptr");
decl.add_ivar::<u8>("show_cpu");
decl.add_ivar::<u8>("show_mem");
decl.add_ivar::<u8>("show_ssd");
decl.add_ivar::<u8>("show_swap");
decl.add_ivar::<u8>("show_load");
decl.add_ivar::<u8>("show_percent_only");
decl.add_ivar::<f64>("warn_threshold");
decl.add_ivar::<f64>("crit_threshold");
decl.add_ivar::<usize>("theme_index");
decl.add_ivar::<usize>("menu_warn_ptr");
decl.add_ivar::<usize>("menu_crit_ptr");
decl.add_ivar::<usize>("menu_warn_menu_ptr");
decl.add_ivar::<usize>("menu_crit_menu_ptr");
decl.add_ivar::<usize>("menu_theme_0_ptr");
decl.add_ivar::<usize>("menu_theme_1_ptr");
decl.add_ivar::<usize>("menu_theme_2_ptr");
unsafe {
decl.add_method(sel!(tick:), tick as extern "C" fn(&Object, Sel, id));
decl.add_method(
sel!(toggleCpu:),
toggle_cpu as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(toggleMem:),
toggle_mem as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(toggleSsd:),
toggle_ssd as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(toggleSwap:),
toggle_swap as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(toggleLoad:),
toggle_load as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(togglePercent:),
toggle_percent as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(setWarnFromMenu:),
set_warn_from_menu as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(setCritFromMenu:),
set_crit_from_menu as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(sel!(noop:), noop as extern "C" fn(&mut Object, Sel, id));
decl.add_method(
sel!(setTheme0:),
set_theme_0 as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(setTheme1:),
set_theme_1 as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(setTheme2:),
set_theme_2 as extern "C" fn(&mut Object, Sel, id),
);
}
decl.register()
})
}
fn xml_escape(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn launch_agent_path() -> Result<PathBuf, String> {
let home = env::var_os("HOME").ok_or("HOME is not set".to_string())?;
Ok(PathBuf::from(home).join("Library/LaunchAgents/com.pnkjsng.mstat.plist"))
}
fn user_id() -> Result<String, String> {
let output = Command::new("id")
.arg("-u")
.output()
.map_err(|e| format!("failed to run id -u: {e}"))?;
if !output.status.success() {
return Err("failed to determine user id".to_string());
}
let uid = String::from_utf8_lossy(&output.stdout).trim().to_string();
if uid.is_empty() {
return Err("empty user id".to_string());
}
Ok(uid)
}
fn launch_agent_plist(exe_path: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>ProcessType</key>
<string>Interactive</string>
</dict>
</plist>
"#,
label = LAUNCH_AGENT_LABEL,
exe = xml_escape(exe_path)
)
}
fn install_startup() -> Result<(), String> {
let plist_path = launch_agent_path()?;
if let Some(parent) = plist_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("failed to create LaunchAgents dir: {e}"))?;
}
let exe = env::current_exe().map_err(|e| format!("failed to resolve executable path: {e}"))?;
let plist = launch_agent_plist(&exe.to_string_lossy());
fs::write(&plist_path, plist).map_err(|e| format!("failed to write launch agent: {e}"))?;
let uid = user_id()?;
let gui = format!("gui/{uid}");
let target = format!("{gui}/{LAUNCH_AGENT_LABEL}");
let _ = Command::new("launchctl")
.arg("bootout")
.arg(&gui)
.arg(&plist_path)
.status();
let bootstrap = Command::new("launchctl")
.arg("bootstrap")
.arg(&gui)
.arg(&plist_path)
.status()
.map_err(|e| format!("failed to run launchctl bootstrap: {e}"))?;
if !bootstrap.success() {
return Err("launchctl bootstrap failed".to_string());
}
let enable = Command::new("launchctl")
.arg("enable")
.arg(&target)
.status()
.map_err(|e| format!("failed to run launchctl enable: {e}"))?;
if !enable.success() {
return Err("launchctl enable failed".to_string());
}
let _ = Command::new("launchctl")
.arg("kickstart")
.arg("-k")
.arg(&target)
.status();
Ok(())
}
fn uninstall_startup() -> Result<(), String> {
let plist_path = launch_agent_path()?;
let uid = user_id()?;
let gui = format!("gui/{uid}");
let _ = Command::new("launchctl")
.arg("bootout")
.arg(&gui)
.arg(&plist_path)
.status();
if plist_path.exists() {
fs::remove_file(&plist_path).map_err(|e| format!("failed to remove launch agent: {e}"))?;
}
Ok(())
}
fn startup_status() -> Result<bool, String> {
let plist_path = launch_agent_path()?;
let uid = user_id()?;
let loaded = Command::new("launchctl")
.arg("print")
.arg(format!("gui/{uid}/{LAUNCH_AGENT_LABEL}"))
.output()
.map_err(|e| format!("failed to run launchctl print: {e}"))?
.status
.success();
Ok(plist_path.exists() && loaded)
}
fn main() {
let mut args = env::args().skip(1);
if let Some(arg) = args.next() {
match arg.as_str() {
"--install-startup" => {
match install_startup() {
Ok(()) => println!("Startup enabled for MStat."),
Err(e) => {
eprintln!("Failed to enable startup: {e}");
std::process::exit(1);
}
}
return;
}
"--uninstall-startup" => {
match uninstall_startup() {
Ok(()) => println!("Startup disabled for MStat."),
Err(e) => {
eprintln!("Failed to disable startup: {e}");
std::process::exit(1);
}
}
return;
}
"--startup-status" => {
match startup_status() {
Ok(true) => println!("Startup is enabled."),
Ok(false) => println!("Startup is disabled."),
Err(e) => {
eprintln!("Failed to read startup status: {e}");
std::process::exit(1);
}
}
return;
}
"--help" | "-h" => {
println!("mstat - macOS menu bar system monitor");
println!("Usage:");
println!(" mstat Run menu bar app");
println!(" mstat --install-startup Enable launch at login");
println!(" mstat --uninstall-startup Disable launch at login");
println!(" mstat --startup-status Show startup status");
return;
}
_ => {}
}
}
unsafe {
let app = NSApp();
app.setActivationPolicy_(NSApplicationActivationPolicyProhibited);
let status_bar = NSStatusBar::systemStatusBar(nil);
let status_item = status_bar.statusItemWithLength_(NSVariableStatusItemLength);
let button: id = msg_send![status_item, button];
let _: () = msg_send![button, setTitle: ns_string("")];
let wrapper = create_wrapper_box();
let (cpu_chip, cpu_icon, cpu_value) = create_chip("cpu.fill");
let (mem_chip, mem_icon, mem_value) = create_chip("memorychip.fill");
let (ssd_chip, ssd_icon, ssd_value) = create_chip("internaldrive.fill");
let (swap_chip, swap_icon, swap_value) = create_chip("arrow.left.arrow.right");
let (load_chip, load_icon, load_value) = create_chip("speedometer");
let _: () = msg_send![wrapper, addSubview: cpu_chip];
let _: () = msg_send![wrapper, addSubview: mem_chip];
let _: () = msg_send![wrapper, addSubview: ssd_chip];
let _: () = msg_send![wrapper, addSubview: swap_chip];
let _: () = msg_send![wrapper, addSubview: load_chip];
let _: () = msg_send![button, addSubview: wrapper];
let mut system = System::new();
system.refresh_cpu_usage();
system.refresh_memory();
let mut disks = Disks::new_with_refreshed_list();
disks.refresh(false);
let updater: id = msg_send![updater_class(), new];
(*updater).set_ivar("status_item_ptr", status_item as usize);
(*updater).set_ivar("wrapper_ptr", wrapper as usize);
(*updater).set_ivar("system_ptr", Box::into_raw(Box::new(system)) as usize);
(*updater).set_ivar("disks_ptr", Box::into_raw(Box::new(disks)) as usize);
(*updater).set_ivar("cpu_chip_ptr", cpu_chip as usize);
(*updater).set_ivar("cpu_icon_ptr", cpu_icon as usize);
(*updater).set_ivar("cpu_value_ptr", cpu_value as usize);
(*updater).set_ivar("mem_chip_ptr", mem_chip as usize);
(*updater).set_ivar("mem_icon_ptr", mem_icon as usize);
(*updater).set_ivar("mem_value_ptr", mem_value as usize);
(*updater).set_ivar("ssd_chip_ptr", ssd_chip as usize);
(*updater).set_ivar("ssd_icon_ptr", ssd_icon as usize);
(*updater).set_ivar("ssd_value_ptr", ssd_value as usize);
(*updater).set_ivar("swap_chip_ptr", swap_chip as usize);
(*updater).set_ivar("swap_icon_ptr", swap_icon as usize);
(*updater).set_ivar("swap_value_ptr", swap_value as usize);
(*updater).set_ivar("load_chip_ptr", load_chip as usize);
(*updater).set_ivar("load_icon_ptr", load_icon as usize);
(*updater).set_ivar("load_value_ptr", load_value as usize);
(*updater).set_ivar("show_cpu", 1_u8);
(*updater).set_ivar("show_mem", 1_u8);
(*updater).set_ivar("show_ssd", 1_u8);
(*updater).set_ivar("show_swap", 0_u8);
(*updater).set_ivar("show_load", 0_u8);
(*updater).set_ivar("show_percent_only", 0_u8);
(*updater).set_ivar("warn_threshold", 70.0_f64);
(*updater).set_ivar("crit_threshold", 90.0_f64);
(*updater).set_ivar("theme_index", 0_usize);
let menu = NSMenu::new(nil).autorelease();
add_menu_item(menu, "Show CPU", sel!(toggleCpu:), updater, Some(1));
add_menu_item(menu, "Show Memory", sel!(toggleMem:), updater, Some(1));
add_menu_item(menu, "Show Storage", sel!(toggleSsd:), updater, Some(1));
add_menu_item(menu, "Show Swap", sel!(toggleSwap:), updater, Some(0));
add_menu_item(menu, "Show Load (1m)", sel!(toggleLoad:), updater, Some(0));
add_menu_item(
menu,
"Use % only for Memory/Storage",
sel!(togglePercent:),
updater,
Some(0),
);
menu.addItem_(NSMenuItem::separatorItem(nil));
let warn_item = add_menu_item(
menu,
"Set warn threshold… (70%)",
sel!(noop:),
updater,
None,
);
let crit_item = add_menu_item(
menu,
"Set critical threshold… (90%)",
sel!(noop:),
updater,
None,
);
let warn_menu = NSMenu::new(nil).autorelease();
for (idx, value) in WARN_THRESHOLD_OPTIONS.iter().enumerate() {
let item = add_menu_item(
warn_menu,
&format!("{:.0}%", value),
sel!(setWarnFromMenu:),
updater,
Some(if (*value - 70.0).abs() < 0.5 { 1 } else { 0 }),
);
let _: () = msg_send![item, setTag: idx as i64];
}
let _: () = msg_send![warn_item, setSubmenu: warn_menu];
let crit_menu = NSMenu::new(nil).autorelease();
for (idx, value) in CRIT_THRESHOLD_OPTIONS.iter().enumerate() {
let item = add_menu_item(
crit_menu,
&format!("{:.0}%", value),
sel!(setCritFromMenu:),
updater,
Some(if (*value - 90.0).abs() < 0.5 { 1 } else { 0 }),
);
let _: () = msg_send![item, setTag: idx as i64];
}
let _: () = msg_send![crit_item, setSubmenu: crit_menu];
menu.addItem_(NSMenuItem::separatorItem(nil));
let theme_item_0 =
add_menu_item(menu, "Theme: Classic", sel!(setTheme0:), updater, Some(1));
let theme_item_1 = add_menu_item(menu, "Theme: Mono", sel!(setTheme1:), updater, Some(0));
let theme_item_2 = add_menu_item(
menu,
"Theme: Transparent",
sel!(setTheme2:),
updater,
Some(0),
);
(*updater).set_ivar("menu_warn_ptr", warn_item as usize);
(*updater).set_ivar("menu_crit_ptr", crit_item as usize);
(*updater).set_ivar("menu_warn_menu_ptr", warn_menu as usize);
(*updater).set_ivar("menu_crit_menu_ptr", crit_menu as usize);
(*updater).set_ivar("menu_theme_0_ptr", theme_item_0 as usize);
(*updater).set_ivar("menu_theme_1_ptr", theme_item_1 as usize);
(*updater).set_ivar("menu_theme_2_ptr", theme_item_2 as usize);
update_menu_titles(&*updater);
update_threshold_checks(&*updater);
update_theme_checks(&*updater);
menu.addItem_(NSMenuItem::separatorItem(nil));
let quit_item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string("Quit"),
sel!(terminate:),
ns_string("q"),
)
.autorelease();
menu.addItem_(quit_item);
status_item.setMenu_(menu);
let _: id = msg_send![
class!(NSTimer),
scheduledTimerWithTimeInterval: 2.0_f64
target: updater
selector: sel!(tick:)
userInfo: nil
repeats: YES
];
let _: () = msg_send![updater, tick: nil];
app.run();
}
}