diff --git a/adlermon.conf b/adlermon.conf
index 56ee7df..03dfd9c 100644
--- a/adlermon.conf
+++ b/adlermon.conf
@@ -30,7 +30,8 @@ rail.in12.label = Fixed 1.14
rail.in13.hide = true
rail.in14.label = Fixed 1.5V
poll_ms = 250
-graph_secs = 3600
+graph_secs = 1800
+graph_scale_fixed = true
graphs = true
layout = auto
vcore_limit = 1.403
diff --git a/src/config.rs b/src/config.rs
index b5066f9..3de8226 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -26,8 +26,11 @@ pub struct Config {
/// Tick / sample period, ms.
pub poll_ms: u64,
/// Graph window (seconds of history shown). History kept is capped by
- /// the ring size (see plot::RING_CAP).
+ /// the ring size (see plot::Ring::new).
pub graph_secs: u64,
+ /// Graph vertical scale: auto (window peak + margin) or fixed (the
+ /// bar_max limits below — no rescaling mid-benchmark).
+ pub graph_scale_fixed: bool,
/// Path the config was loaded from (save-back target; None = never
/// loaded, defaults in effect).
pub loaded_path: Option<std::path::PathBuf>,
@@ -53,6 +56,7 @@ impl Default for Config {
graphs: true,
poll_ms: 250,
graph_secs: 120,
+ graph_scale_fixed: false,
loaded_path: None,
}
}
@@ -115,6 +119,7 @@ fn apply_line(cfg: &mut Config, line: &str) {
"graphs" => cfg.graphs = val == "true",
"poll_ms" => cfg.poll_ms = val.parse().unwrap_or(cfg.poll_ms).max(50),
"graph_secs" => cfg.graph_secs = val.parse().unwrap_or(cfg.graph_secs).max(5),
+ "graph_scale_fixed" => cfg.graph_scale_fixed = val == "true",
"layout" => {
cfg.layout = match val {
"single" => Layout::Single,
@@ -174,9 +179,10 @@ pub fn load() -> Config {
/// the loaded conf path (F10); rail map and limits stay as-is on disk.
pub fn settings_text(cfg: &Config) -> String {
format!(
- "poll_ms = {}\ngraph_secs = {}\ngraphs = {}\nlayout = {}\nvcore_limit = {:.3}\ntemp_warn = {:.1}\ntemp_crit = {:.1}\n",
+ "poll_ms = {}\ngraph_secs = {}\ngraph_scale_fixed = {}\ngraphs = {}\nlayout = {}\nvcore_limit = {:.3}\ntemp_warn = {:.1}\ntemp_crit = {:.1}\n",
cfg.poll_ms,
cfg.graph_secs,
+ cfg.graph_scale_fixed,
cfg.graphs,
match cfg.layout {
Layout::Auto => "auto",
@@ -313,4 +319,17 @@ mod tests {
assert_eq!(out.matches("poll_ms").count(), 1);
assert_eq!(out.matches("graphs").count(), 1);
}
+
+ #[test]
+ fn parses_scale_fixed() {
+ let mut cfg = Config::default();
+ apply_line(&mut cfg, "graph_scale_fixed = true");
+ assert!(cfg.graph_scale_fixed);
+ }
+
+ #[test]
+ fn settings_text_includes_scale() {
+ let cfg = Config::default();
+ assert!(settings_text(&cfg).contains("graph_scale_fixed = false"));
+ }
}
diff --git a/src/ui.rs b/src/ui.rs
index 71e68df..b2f10bf 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -59,15 +59,49 @@ pub struct App {
}
/// Settings-pane rows: (label, config field edited, unit).
-const SETTINGS_ROWS: [&str; 2] = ["Poll period", "Graph window"];
+const SETTINGS_ROWS: [&str; 3] = ["Poll period", "Graph window", "Graph scale"];
fn setting_value(app: &App, row: usize) -> String {
match row {
0 => format!("{} ms", app.cfg.poll_ms),
- _ => format!("{} s", app.cfg.graph_secs),
+ 1 => {
+ // Minutes when the window is a whole minute or more.
+ let s = app.cfg.graph_secs;
+ if s >= 60 && s % 60 == 0 {
+ format!("{} m", s / 60)
+ } else {
+ format!("{} s", s)
+ }
+ }
+ _ => {
+ if app.cfg.graph_scale_fixed {
+ "fixed".to_string()
+ } else {
+ "auto".to_string()
+ }
+ }
}
}
+/// Window input accepts "90" (seconds) or "5m"/"2m30s" (minutes).
+fn parse_window_secs(raw: &str) -> Result<u64, String> {
+ let raw = raw.trim().to_ascii_lowercase();
+ if let Some(mins) = raw.strip_suffix('m') {
+ return mins
+ .trim()
+ .parse::<u64>()
+ .map(|m| m * 60)
+ .map_err(|_| format!("'{raw}' is not a number"));
+ }
+ if let Some((m, s)) = raw.split_once('m') {
+ let s = s.strip_suffix('s').unwrap_or(s);
+ let m: u64 = m.trim().parse().map_err(|_| format!("'{raw}' is not a number"))?;
+ let s: u64 = s.trim().parse().map_err(|_| format!("'{raw}' is not a number"))?;
+ return Ok(m * 60 + s);
+ }
+ raw.parse::<u64>().map_err(|_| format!("'{raw}' is not a number"))
+}
+
fn setting_absorb(app: &mut App, row: usize, raw: &str) -> Result<(), String> {
match row {
0 => {
@@ -79,18 +113,26 @@ fn setting_absorb(app: &mut App, row: usize, raw: &str) -> Result<(), String> {
}
app.cfg.poll_ms = v;
}
- _ => {
- let v: u64 = raw
- .parse()
- .map_err(|_| format!("'{}' is not a number", raw))?;
+ 1 => {
+ let v = parse_window_secs(raw)?;
if !(5..=3600).contains(&v) {
- return Err("graph_secs must be 5..3600".into());
+ return Err("window must be 5s..60m".into());
}
app.cfg.graph_secs = v;
- app.vcore_graph = Ring::new(ring_capacity(&app.cfg));
+ app.vcore_graph = Ring::new(ring_capacity(&app.cfg));
app.watts_graph = Ring::new(ring_capacity(&app.cfg));
app.temp_graph = Ring::new(ring_capacity(&app.cfg));
}
+ _ => {
+ // Scale: toggle auto/fixed. Bare Enter toggles; also accept
+ // typed words for discoverability.
+ match raw.to_ascii_lowercase().as_str() {
+ "auto" => app.cfg.graph_scale_fixed = false,
+ "fixed" => app.cfg.graph_scale_fixed = true,
+ "" => app.cfg.graph_scale_fixed = !app.cfg.graph_scale_fixed,
+ _ => return Err("use auto|fixed (or Enter to toggle)".into()),
+ }
+ }
}
Ok(())
}
@@ -482,14 +524,23 @@ pub fn draw(f: &mut Frame, app: &App) {
let ring_max = |ring: &Ring, floor: f64| -> f64 {
(ring.max_window(ticks) * (1.0 + SCALE_MARGIN)).max(floor)
};
- let vc_graph_max = ring_max(&app.vcore_graph, 1.0);
- let watt_graph_max = ring_max(&app.watts_graph, 10.0);
+ // Fixed scale (settings pane / graph_scale_fixed): bar_max limits,
+ // never rescaled mid-benchmark — peaks clip at the top instead of
+ // the whole trace compacting. Auto: window peak + margin.
+ let (vc_graph_max, watt_graph_max, temp_graph_max) = if app.cfg.graph_scale_fixed {
+ (app.cfg.vcore_bar_max, app.cfg.power_bar_max, TEMP_BAR_MAX)
+ } else {
+ (
+ ring_max(&app.vcore_graph, 1.0),
+ ring_max(&app.watts_graph, 10.0),
+ ring_max(&app.temp_graph, 50.0),
+ )
+ };
for (i, (label, text, ring, max, color, marker)) in [
("vCore", hero_text.clone(), &app.vcore_graph, vc_graph_max, vc_color,
Some(((1.0 - (app.cfg.vcore_limit / vc_graph_max).clamp(0.0, 1.0)) * (graph_rows - 1) as f64).round() as u16)),
("Pkg power", fmt_opt_watts(app.package_watts), &app.watts_graph, watt_graph_max, Color::Blue, None),
- ("Pkg temp", fmt_opt_temp(app.package_temp), &app.temp_graph,
- ring_max(&app.temp_graph, 0.0).max(50.0),
+ ("Pkg temp", fmt_opt_temp(app.package_temp), &app.temp_graph, temp_graph_max,
app.package_temp.map_or(Color::DarkGray, |t| temp_color(t, app.cfg.temp_warn, app.cfg.temp_crit)), None),
].into_iter().enumerate() {
let span_w = if i == 2 { w - thirds * 2 } else { thirds };
@@ -587,9 +638,6 @@ fn settings_key(app: &mut App, key: KeyCode) -> Result<(), String> {
app.edit_buf.clear();
}
KeyCode::Enter => {
- if app.edit_buf.is_empty() {
- return Ok(());
- }
let raw = std::mem::take(&mut app.edit_buf);
setting_absorb(app, app.settings_row, &raw)?;
}
@@ -600,7 +648,9 @@ fn settings_key(app: &mut App, key: KeyCode) -> Result<(), String> {
app.settings_open = false;
app.edit_buf.clear();
}
- KeyCode::Char(c) if c.is_ascii_digit() => app.edit_buf.push(c),
+ KeyCode::Char(c) if c.is_ascii_digit() || c == 'm' || c == 's' => {
+ app.edit_buf.push(c)
+ }
_ => {}
}
Ok(())
Settings: graph window accepts minutes (30m/2m30s), graph scale auto/fixed (fixed pins bar_max, no mid-benchmark rescale); allow m/s in edit buffer; bare-Enter toggle fix
afeaffadbc7ac432f885cd7bd5164f1785b6eab9
josie <administrator@josie-c.com> · 2026-08-30T14:13 ·
browse files at this commit
parents:
c561d25