@@ -10,7 +10,7 @@ use ratatui::backend::CrosstermBackend;
1010use ratatui:: layout:: { Constraint , Direction , Layout , Rect } ;
1111use ratatui:: style:: { Modifier , Style } ;
1212use ratatui:: text:: { Line , Span } ;
13- use ratatui:: widgets:: { Block , Borders , Paragraph } ;
13+ use ratatui:: widgets:: { Block , Borders , Gauge , Paragraph , Wrap } ;
1414use ratatui:: Terminal ;
1515
1616use crate :: config:: { parse_color, BorderStyle , Config , ModuleTheme } ;
@@ -138,6 +138,7 @@ fn draw_ui(frame: &mut ratatui::Frame, app: &App) {
138138 . split ( area) ;
139139
140140 draw_header ( frame, chunks[ 0 ] , app, chrome_border, chrome_title, border_type) ;
141+ draw_modules ( frame, chunks[ 1 ] , app, border_type) ;
141142 draw_footer ( frame, chunks[ 2 ] , app, chrome_border, border_type) ;
142143}
143144
@@ -212,6 +213,138 @@ fn draw_footer(
212213 frame. render_widget ( help, area) ;
213214}
214215
216+ fn draw_modules (
217+ frame : & mut ratatui:: Frame ,
218+ area : Rect ,
219+ app : & App ,
220+ border_type : ratatui:: widgets:: BorderType ,
221+ ) {
222+ let snapshot = match & app. snapshot {
223+ Some ( s) => s,
224+ None => return ,
225+ } ;
226+
227+ if snapshot. modules . is_empty ( ) {
228+ return ;
229+ }
230+
231+ let module_constraints: Vec < Constraint > = snapshot
232+ . modules
233+ . iter ( )
234+ . map ( |_| Constraint :: Ratio ( 1 , snapshot. modules . len ( ) as u32 ) )
235+ . collect ( ) ;
236+
237+ let module_chunks = Layout :: default ( )
238+ . direction ( Direction :: Horizontal )
239+ . constraints ( module_constraints)
240+ . split ( area) ;
241+
242+ for ( i, ( name, data) ) in snapshot. modules . iter ( ) . enumerate ( ) {
243+ let fg = module_fg_color ( & app. config , name) ;
244+ let accent = module_accent_color ( & app. config , name) ;
245+ let border_color = if i == app. selected_tab {
246+ fg
247+ } else {
248+ parse_color ( & app. config . theme . chrome . border )
249+ } ;
250+
251+ let block = Block :: default ( )
252+ . borders ( Borders :: ALL )
253+ . border_type ( border_type)
254+ . border_style ( Style :: default ( ) . fg ( border_color) )
255+ . title ( Span :: styled (
256+ format ! ( " {} " , module_label( & app. config, name) ) ,
257+ Style :: default ( ) . fg ( fg) . add_modifier ( Modifier :: BOLD ) ,
258+ ) ) ;
259+
260+ let inner = block. inner ( module_chunks[ i] ) ;
261+ frame. render_widget ( block, module_chunks[ i] ) ;
262+
263+ let inner_chunks = Layout :: default ( )
264+ . direction ( Direction :: Vertical )
265+ . constraints ( [ Constraint :: Length ( 3 ) , Constraint :: Min ( 1 ) ] )
266+ . split ( inner) ;
267+
268+ if let Some ( gauge_data) = extract_gauge ( name, data) {
269+ let gauge = Gauge :: default ( )
270+ . gauge_style ( Style :: default ( ) . fg ( accent) )
271+ . ratio ( gauge_data. ratio . clamp ( 0.0 , 1.0 ) )
272+ . label ( format ! (
273+ "{}: {:.1}%" ,
274+ gauge_data. label,
275+ gauge_data. ratio * 100.0
276+ ) ) ;
277+ frame. render_widget ( gauge, inner_chunks[ 0 ] ) ;
278+ }
279+
280+ let mut lines: Vec < Line > = Vec :: new ( ) ;
281+ let mut entries: Vec < _ > = data. metrics . iter ( ) . collect ( ) ;
282+ entries. sort_by_key ( |( k, _) | k. clone ( ) ) ;
283+
284+ for ( key, value) in entries {
285+ lines. push ( Line :: from ( vec ! [
286+ Span :: styled(
287+ format!( "{}: " , key) ,
288+ Style :: default ( ) . fg( fg) . add_modifier( Modifier :: BOLD ) ,
289+ ) ,
290+ Span :: raw( metric_display( value) ) ,
291+ ] ) ) ;
292+ }
293+
294+ let detail = Paragraph :: new ( lines) . wrap ( Wrap { trim : true } ) ;
295+ frame. render_widget ( detail, inner_chunks[ 1 ] ) ;
296+ }
297+ }
298+
299+ struct GaugeData {
300+ label : String ,
301+ ratio : f64 ,
302+ }
303+
304+ fn extract_gauge ( module_name : & str , data : & crate :: core:: MetricData ) -> Option < GaugeData > {
305+ let key = match module_name {
306+ "cpu" => "cpu_usage_percent" ,
307+ "memory" => "memory_usage_percent" ,
308+ "disk" => "usage_percent" ,
309+ _ => return None ,
310+ } ;
311+
312+ data. metrics . get ( key) . and_then ( |v| match v {
313+ MetricValue :: Float ( f) => Some ( GaugeData {
314+ label : module_name. to_uppercase ( ) ,
315+ ratio : * f / 100.0 ,
316+ } ) ,
317+ _ => None ,
318+ } )
319+ }
320+
321+ fn metric_display ( value : & MetricValue ) -> String {
322+ match value {
323+ MetricValue :: Integer ( i) => format_bytes_smart ( * i) ,
324+ MetricValue :: Float ( f) => format ! ( "{:.2}" , f) ,
325+ MetricValue :: String ( s) => s. clone ( ) ,
326+ MetricValue :: Boolean ( b) => b. to_string ( ) ,
327+ MetricValue :: List ( items) => items
328+ . iter ( )
329+ . map ( metric_display)
330+ . collect :: < Vec < _ > > ( )
331+ . join ( ", " ) ,
332+ }
333+ }
334+
335+ fn format_bytes_smart ( value : i64 ) -> String {
336+ let abs = value. unsigned_abs ( ) ;
337+ if abs >= 1_073_741_824 {
338+ format ! ( "{:.2} GB" , abs as f64 / 1_073_741_824.0 )
339+ } else if abs >= 1_048_576 {
340+ format ! ( "{:.2} MB" , abs as f64 / 1_048_576.0 )
341+ } else if abs >= 1024 {
342+ format ! ( "{:.2} KB" , abs as f64 / 1024.0 )
343+ } else {
344+ value. to_string ( )
345+ }
346+ }
347+
215348fn module_theme < ' a > ( config : & ' a Config , name : & str ) -> & ' a ModuleTheme {
216349 match name {
217350 "cpu" => & config. theme . cpu ,
0 commit comments