use std::{
io::{self, BufRead, BufReader, Write},
process,
sync::mpsc,
thread,
time::Duration,
};
use ratatui::{
Terminal,
backend::{Backend, TermionBackend},
};
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
};
use crate::{
Result,
app::App,
config::RunConfig,
error::Error as CrateError,
metrics,
modules::{powermetrics, soc::SocInfo, sysinfo},
ui,
};
pub fn run(args: RunConfig) -> Result<()> {
let soc_info = SocInfo::new()?;
let result = match args.json {
true => main_exporter_loop(soc_info, Duration::from_millis(args.sample_rate_ms as u64)),
false => {
let stdout = io::stdout().into_raw_mode()?.into_alternate_screen()?;
let stdout = MouseTerminal::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let app = App::new(soc_info, args.colors(), args.history_size);
let result = main_ui_loop(
&mut terminal,
app,
Duration::from_millis(args.sample_rate_ms as u64),
);
drop(terminal);
io::stdout().flush().ok();
result
}
};
if let Err(err) = result {
eprintln!("{err}");
if let CrateError::PowermetricsNonZeroExit(status, msg) = &err
&& status.code() == Some(1)
&& msg.contains("superuser")
{
eprintln!(
"macOS requires superuser privileges to access power metrics.\n\n sudo pumas run\n"
);
}
}
Ok(())
}
enum Event {
Input(Key),
Metrics(metrics::Metrics),
Error(CrateError),
}
fn main_ui_loop<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<()>
where
CrateError: From<B::Error>,
{
let events = start_event_threads(tick_rate);
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv() {
Ok(Event::Input(key)) => match key {
Key::Esc => app.on_key('q'),
Key::Left | Key::BackTab => app.on_left(),
Key::Right | Key::Char('\t') => app.on_right(),
Key::Char(c) => app.on_key(c),
Key::Ctrl(c) => app.on_ctrl(c),
_ => {}
},
Ok(Event::Metrics(metrics)) => app.on_metrics(metrics),
Ok(Event::Error(err)) => return Err(err),
Err(_) => break,
}
if app.should_quit {
return Ok(());
}
}
Ok(())
}
fn main_exporter_loop(soc_info: SocInfo, tick_rate: Duration) -> Result<()> {
let events = start_event_threads(tick_rate);
loop {
match events.recv() {
Ok(Event::Metrics(metrics)) => export(&soc_info, metrics),
Ok(Event::Error(err)) => return Err(err),
Ok(_) => {}
Err(_) => break,
}
}
Ok(())
}
fn export(soc_info: &SocInfo, metrics: metrics::Metrics) {
let json = serde_json::json!({
"soc": soc_info,
"metrics": metrics,
});
println!("{}", json);
}
fn start_event_threads(tick_rate: Duration) -> mpsc::Receiver<Event> {
let (tx, rx) = mpsc::channel();
let tx_keys = tx.clone();
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
if let Err(err) = tx_keys.send(Event::Input(key)) {
eprintln!("{}", err);
return;
}
}
});
thread::spawn(move || {
if let Err(err) = stream_metrics(tick_rate, tx.clone())
&& let Err(send_err) = tx.send(Event::Error(err))
{
eprintln!("failed to send error event: {send_err}");
}
});
rx
}
fn stream_metrics(tick_rate: Duration, tx: mpsc::Sender<Event>) -> Result<()> {
let sample_rate_ms = format!("{}", tick_rate.as_millis());
let binary = "/usr/bin/powermetrics";
let args = vec![
"--sample-rate",
sample_rate_ms.as_str(),
"--samplers",
"cpu_power,gpu_power,thermal",
"-f",
"plist",
];
let mut cmd = process::Command::new(binary)
.args(&args)
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.spawn()
.map_err(CrateError::PowermetricsSpawn)?;
let stdout = cmd.stdout.as_mut().ok_or(CrateError::PowermetricsStdout)?;
let stdout_reader = BufReader::new(stdout);
let stdout_lines = stdout_reader.lines();
let mut buffer = powermetrics::Buffer::new();
let mut system_state = sysinfo::SystemState::new();
for line in stdout_lines.map_while(std::result::Result::<String, std::io::Error>::ok) {
if line != "</plist>" {
buffer.append_line(line);
} else {
buffer.append_last_line(line);
let text = buffer.finalize();
let power_metrics = match metrics::Metrics::from_bytes(text.as_bytes()) {
Ok(metrics) => metrics,
Err(err) => {
eprintln!("{err}");
cmd.kill().map_err(CrateError::PowermetricsKill)?;
break;
}
};
let sysinfo_metrics = system_state.latest_metrics();
let metrics = match power_metrics.merge_sysinfo_metrics(sysinfo_metrics) {
Ok(metrics) => metrics,
Err(err) => {
eprintln!("{err}");
cmd.kill().map_err(CrateError::PowermetricsKill)?;
break;
}
};
if let Err(err) = tx.send(Event::Metrics(metrics)) {
eprintln!("{err}");
cmd.kill().map_err(CrateError::PowermetricsKill)?;
break;
}
}
}
let status = cmd.wait()?;
if !status.success() && status.code().is_some() {
let mut err_msg = String::new();
if let Some(mut stderr) = cmd.stderr.take() {
use std::io::Read;
stderr.read_to_string(&mut err_msg).ok();
}
return Err(CrateError::PowermetricsNonZeroExit(
status,
err_msg.trim().to_string(),
));
}
Ok(())
}