@@ -274,37 +274,33 @@ fn render_mem_panel(f: &mut Frame, area: Rect, pv: &ProcView, theme: &Theme) {
274274}
275275
276276fn render_disk_panel ( f : & mut Frame , area : Rect , pv : & ProcView , theme : & Theme ) {
277- let title = format ! ( " Disk {}/s " , human_size( pv. disk_rate as u64 ) ) ;
277+ // ▲ writes (grow upward), ▼ reads (grow downward) from the centre line.
278+ let title = format ! (
279+ " Disk ▼{}/s ▲{}/s " ,
280+ human_size( pv. disk_read as u64 ) ,
281+ human_size( pv. disk_write as u64 )
282+ ) ;
278283 let inner = titled ( f, area, title, theme) ;
279- let samples: Vec < f64 > = pv. disk_history . iter ( ) . copied ( ) . collect ( ) ;
280- let max = peak ( & samples) ;
281- let color = theme. exec_fg ;
282- draw_sparkline ( f, inner, & samples, max, & |_| color, theme) ;
284+ let read: Vec < f64 > = pv. disk_read_history . iter ( ) . copied ( ) . collect ( ) ;
285+ let write: Vec < f64 > = pv. disk_write_history . iter ( ) . copied ( ) . collect ( ) ;
286+ // Shared scale so reads and writes are directly comparable.
287+ let max = peak ( & read) . max ( peak ( & write) ) ;
288+ draw_mirror_bars ( f, inner, ( & write, theme. header_fg ) , ( & read, theme. exec_fg ) , max, theme) ;
283289}
284290
285291fn render_net_panel ( f : & mut Frame , area : Rect , pv : & ProcView , theme : & Theme ) {
292+ // ▲ uploads (grow upward), ▼ downloads (grow downward) from the centre line.
286293 let title = format ! (
287294 " Net ▼{}/s ▲{}/s " ,
288295 human_size( pv. net_down as u64 ) ,
289296 human_size( pv. net_up as u64 )
290297 ) ;
291298 let inner = titled ( f, area, title, theme) ;
292- if inner. width == 0 || inner. height == 0 {
293- return ;
294- }
295299 let down: Vec < f64 > = pv. net_down_history . iter ( ) . copied ( ) . collect ( ) ;
296300 let up: Vec < f64 > = pv. net_up_history . iter ( ) . copied ( ) . collect ( ) ;
297- // Shared scale so the two halves are comparable; split interior in two .
301+ // Shared scale so the upload/download halves are directly comparable .
298302 let max = peak ( & down) . max ( peak ( & up) ) ;
299- if inner. height >= 2 {
300- let half = inner. height / 2 ;
301- let top = Rect { height : half, ..inner } ;
302- let bottom = Rect { y : inner. y + half, height : inner. height - half, ..inner } ;
303- draw_sparkline ( f, top, & down, max, & |_| theme. panel_border_active , theme) ;
304- draw_sparkline ( f, bottom, & up, max, & |_| theme. header_fg , theme) ;
305- } else {
306- draw_sparkline ( f, inner, & down, max, & |_| theme. panel_border_active , theme) ;
307- }
303+ draw_mirror_bars ( f, inner, ( & up, theme. header_fg ) , ( & down, theme. panel_border_active ) , max, theme) ;
308304}
309305
310306/// The peak of `samples`, floored at 1.0 so a flat/empty series doesn't divide
@@ -354,6 +350,83 @@ fn draw_sparkline(
354350 }
355351}
356352
353+ /// Draw a centre-line mirrored bar graph in `area`: `up` samples grow upward
354+ /// from the horizontal mid-line, `down` samples grow downward. Newest sample is
355+ /// at the right edge; both are scaled to the shared `max`. Used by the disk
356+ /// (write ▲ / read ▼) and network (upload ▲ / download ▼) panels.
357+ fn draw_mirror_bars (
358+ f : & mut Frame ,
359+ area : Rect ,
360+ up : ( & [ f64 ] , ratatui:: style:: Color ) ,
361+ down : ( & [ f64 ] , ratatui:: style:: Color ) ,
362+ max : f64 ,
363+ theme : & Theme ,
364+ ) {
365+ let ( up, up_color) = up;
366+ let ( down, down_color) = down;
367+ let ( w, h) = ( area. width as usize , area. height as usize ) ;
368+ if w == 0 || h == 0 {
369+ return ;
370+ }
371+ // One row is reserved for the horizontal centre axis; the remaining rows
372+ // split into an upper band (grows up) and a lower band (grows down). With an
373+ // odd leftover the spare row goes to the lower band.
374+ let up_h = ( h - 1 ) / 2 ;
375+ let down_h = h - 1 - up_h;
376+ let axis_y = area. y + up_h as u16 ;
377+ let up_levels = up_h * 8 ;
378+ let down_levels = down_h * 8 ;
379+ let ( nu, nd) = ( up. len ( ) , down. len ( ) ) ;
380+ let bg = theme. panel_bg ;
381+ let axis_style = Style :: default ( ) . fg ( theme. panel_border ) . bg ( bg) ;
382+ let frac = |v : f64 , levels : usize | -> usize {
383+ if max > 0.0 {
384+ ( ( v / max) . clamp ( 0.0 , 1.0 ) * levels as f64 ) . round ( ) as usize
385+ } else {
386+ 0
387+ }
388+ } ;
389+ let buf = f. buffer_mut ( ) ;
390+ for col in 0 ..w {
391+ // Right-align: the rightmost column is the newest sample.
392+ let from_right = w - 1 - col;
393+ let uv = if from_right < nu { up[ nu - 1 - from_right] } else { 0.0 } ;
394+ let dv = if from_right < nd { down[ nd - 1 - from_right] } else { 0.0 } ;
395+ let u_filled = frac ( uv, up_levels) ;
396+ let d_filled = frac ( dv, down_levels) ;
397+ let x = area. x + col as u16 ;
398+
399+ // Upper band: bottom-anchored cells (fill from the centre upward), using
400+ // the lower-block glyphs in the bar colour.
401+ for r in 0 ..up_h {
402+ let from_centre = up_h - 1 - r; // 0 = the row just above the axis
403+ let cell = u_filled. saturating_sub ( from_centre * 8 ) . min ( 8 ) ;
404+ let ( ch, style) = if cell == 0 {
405+ ( ' ' , Style :: default ( ) . fg ( theme. panel_border ) . bg ( bg) )
406+ } else {
407+ ( LEVELS [ cell] , Style :: default ( ) . fg ( up_color) . bg ( bg) )
408+ } ;
409+ buf. set_string ( x, area. y + r as u16 , ch. to_string ( ) , style) ;
410+ }
411+ // The centre axis line.
412+ buf. set_string ( x, axis_y, "─" , axis_style) ;
413+ // Lower band: top-anchored cells (fill from the centre downward). A cell's
414+ // top `cell`/8 is painted in the bar colour by using it as the cell
415+ // background and "erasing" the unfilled lower part with a lower-block
416+ // glyph drawn in the panel background colour.
417+ for r in 0 ..down_h {
418+ let from_centre = r; // 0 = the row just below the axis
419+ let cell = d_filled. saturating_sub ( from_centre * 8 ) . min ( 8 ) ;
420+ let ( ch, style) = if cell == 0 {
421+ ( ' ' , Style :: default ( ) . fg ( theme. panel_border ) . bg ( bg) )
422+ } else {
423+ ( LEVELS [ 8 - cell] , Style :: default ( ) . fg ( bg) . bg ( down_color) )
424+ } ;
425+ buf. set_string ( x, axis_y + 1 + r as u16 , ch. to_string ( ) , style) ;
426+ }
427+ }
428+ }
429+
357430/// A centered top-border title showing battery charge: "BAT[+] 86% ▆▆▆▆░░".
358431fn battery_title ( pct : u8 , charging : bool , theme : & Theme ) -> Line < ' static > {
359432 let header = Style :: default ( )
@@ -536,3 +609,40 @@ fn render_footer(f: &mut Frame, area: Rect, _pv: &ProcView, theme: &Theme) {
536609 area,
537610 ) ;
538611}
612+
613+ #[ cfg( test) ]
614+ mod tests {
615+ use super :: * ;
616+ use ratatui:: Terminal ;
617+ use ratatui:: backend:: TestBackend ;
618+ use ratatui:: style:: Color ;
619+
620+ /// `draw_mirror_bars` must grow the `up` series upward (top band, bar-colored
621+ /// foreground) and the `down` series downward (bottom band, bar-colored
622+ /// background) from the centre line, with the newest sample at the right.
623+ #[ test]
624+ fn mirror_bars_split_up_and_down ( ) {
625+ let theme = crate :: ui:: theme:: Theme :: mc ( ) ;
626+ let area = Rect { x : 0 , y : 0 , width : 3 , height : 4 } ;
627+ let ( up_c, down_c) = ( Color :: Red , Color :: Blue ) ;
628+ let cx = 2u16 ; // rightmost column = newest sample
629+
630+ // Upload-only: a full bar fills the TOP band; the bottom band stays clear.
631+ let mut t = Terminal :: new ( TestBackend :: new ( 3 , 4 ) ) . unwrap ( ) ;
632+ t. draw ( |f| draw_mirror_bars ( f, area, ( & [ 1.0 ] , up_c) , ( & [ ] , down_c) , 1.0 , & theme) )
633+ . unwrap ( ) ;
634+ let b = t. backend ( ) . buffer ( ) ;
635+ assert_eq ! ( b[ ( cx, 0 ) ] . fg, up_c, "top band carries the up colour" ) ;
636+ assert_ne ! ( b[ ( cx, 0 ) ] . symbol( ) , " " , "top band draws a bar glyph" ) ;
637+ assert_eq ! ( b[ ( cx, 1 ) ] . symbol( ) , "─" , "the centre axis line is drawn" ) ;
638+ assert_ne ! ( b[ ( cx, 3 ) ] . bg, down_c, "bottom band is clear with no downloads" ) ;
639+
640+ // Download-only: a full bar fills the BOTTOM band; the top band stays clear.
641+ let mut t = Terminal :: new ( TestBackend :: new ( 3 , 4 ) ) . unwrap ( ) ;
642+ t. draw ( |f| draw_mirror_bars ( f, area, ( & [ ] , up_c) , ( & [ 1.0 ] , down_c) , 1.0 , & theme) )
643+ . unwrap ( ) ;
644+ let b = t. backend ( ) . buffer ( ) ;
645+ assert_eq ! ( b[ ( cx, 3 ) ] . bg, down_c, "bottom band carries the down colour" ) ;
646+ assert_ne ! ( b[ ( cx, 0 ) ] . fg, up_c, "top band is clear with no uploads" ) ;
647+ }
648+ }
0 commit comments