use crate::{flow, panel};
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::Paragraph,
};
use wattop::telemetry::Sample;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
Left,
Right,
}
#[derive(Debug, PartialEq, Eq)]
pub struct PortPos {
pub name: &'static str,
pub short: &'static str,
pub side: Side,
pub slot: u8,
pub slots: u8,
}
const MAC16_1: &[PortPos] = &[
PortPos { name: "MagSafe", short: "MagSafe", side: Side::Left, slot: 1, slots: 4 },
PortPos { name: "USB-C port 2", short: "USB-C 2", side: Side::Left, slot: 2, slots: 4 },
PortPos { name: "USB-C port 1", short: "USB-C 1", side: Side::Left, slot: 3, slots: 4 },
PortPos { name: "audio", short: "audio", side: Side::Left, slot: 4, slots: 4 },
PortPos { name: "HDMI", short: "HDMI", side: Side::Right, slot: 1, slots: 3 },
PortPos { name: "USB-C port 4", short: "USB-C 4", side: Side::Right, slot: 2, slots: 3 },
PortPos { name: "SD card", short: "SD", side: Side::Right, slot: 3, slots: 3 },
];
pub fn layout(model: &str) -> Option<&'static [PortPos]> {
match model {
"Mac16,1" => Some(MAC16_1),
_ => None,
}
}
pub fn model() -> String {
std::process::Command::new("sysctl")
.args(["-n", "hw.model"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_owned())
.unwrap_or_default()
}
const BODY_W: i32 = 12;
const BODY_H: i32 = 7;
const LABEL_W: i32 = 36;
fn row(p: &PortPos) -> i32 {
1 + (p.slot as i32 - 1) * 4 / (p.slots as i32 - 1).max(1)
}
pub fn draw(f: &mut Frame, area: Rect, s: &Sample, ports: Option<&'static [PortPos]>) {
let watts = if s.plugged_in { format!(" in {:.1} W ", s.input_w) } else { " on battery ".into() };
let block = panel("ports").title_bottom(Span::styled(watts, Style::new().fg(Color::LightGreen).bold()));
let inner = block.inner(area);
f.render_widget(block, area);
let fits = inner.width as i32 >= BODY_W && inner.height as i32 >= BODY_H;
match ports {
Some(ports) if fits => outline(f, inner, s, ports),
_ => list(f, inner, s),
}
}
fn list(f: &mut Frame, inner: Rect, s: &Sample) {
let lines: Vec<Line> = if s.power_ports.is_empty() {
vec![Line::from("no power port".dark_gray())]
} else {
s.power_ports.iter().map(|p| Line::from(p.clone()).light_green().bold()).collect()
};
f.render_widget(Paragraph::new(lines), inner);
}
fn outline(f: &mut Frame, inner: Rect, s: &Sample, ports: &[PortPos]) {
let w = inner.width as i32;
let bx = (w - BODY_W) / 2; let rb = bx + BODY_W - 1; let labels = w >= LABEL_W;
let dim = Style::new().fg(Color::DarkGray);
let buf = f.buffer_mut();
let body = [
format!("╭{:─^10}╮", "back"),
"│▪▪▪▪▪▪▪▪▪▪│".to_string(),
"│▪▪▪▪▪▪▪▪▪▪│".to_string(),
"│ ┌────┐ │".to_string(),
"│ └────┘ │".to_string(),
"│ │".to_string(),
format!("╰{:─^10}╯", "front"),
];
for (y, line) in body.iter().enumerate() {
for (x, ch) in line.chars().enumerate() {
flow::put(buf, inner, bx + x as i32, y as i32, &ch.to_string(), dim);
}
}
for p in ports {
let y = row(p);
let on = s.power_ports.iter().any(|n| n == p.name);
let style = if on { Style::new().fg(Color::LightGreen).bold() } else { dim };
let notch = if p.side == Side::Left { "▐" } else { "▌" };
let edge = if p.side == Side::Left { bx } else { rb };
flow::put(buf, inner, edge, y, notch, style);
if !labels {
continue;
}
let text_style = if on { style } else { dim };
match p.side {
Side::Left => {
if on {
for x in (bx - 4)..bx {
flow::put(buf, inner, x, y, if x == bx - 1 { "▶" } else { "═" }, style);
}
}
let start = bx - 5 - p.short.chars().count() as i32;
flow::put_str(buf, inner, start, y, p.short, text_style);
}
Side::Right => {
if on {
for x in (rb + 1)..(rb + 5) {
flow::put(buf, inner, x, y, if x == rb + 1 { "◀" } else { "═" }, style);
}
}
flow::put_str(buf, inner, rb + 5, y, p.short, text_style);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usb_c_2_is_left_slot_2_of_4_on_mac16_1() {
let ports = layout("Mac16,1").unwrap();
let p = ports.iter().find(|p| p.name == "USB-C port 2").unwrap();
assert_eq!((p.side, p.slot, p.slots), (Side::Left, 2, 4));
}
#[test]
fn unknown_model_has_no_layout() {
assert!(layout("MacBookPro99,1").is_none());
}
}