• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kdash-rs / kdash / 24024608060

06 Apr 2026 08:20AM UTC coverage: 62.628% (+0.5%) from 62.154%
24024608060

push

github

deepu105
feat: Add inline filter to Context page

54 of 95 new or added lines in 3 files covered. (56.84%)

651 existing lines in 31 files now uncovered.

6363 of 10160 relevant lines covered (62.63%)

212.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.93
/src/ui/utils.rs
1
use std::{collections::BTreeMap, rc::Rc, sync::OnceLock};
2

3
use glob_match::glob_match;
4
use ratatui::{
5
  layout::{Constraint, Direction, Layout, Rect},
6
  style::{Color, Modifier, Style},
7
  symbols,
8
  text::{Line, Span, Text},
9
  widgets::{Block, Borders, Paragraph, Row, Table, Wrap},
10
  Frame,
11
};
12
use serde::Serialize;
13

14
use super::HIGHLIGHT;
15
use crate::app::{
16
  models::{KubeResource, StatefulTable},
17
  ActiveBlock, App,
18
};
19
// Utils
20

21
pub static COPY_HINT: &str = "| copy <c>";
22
pub static DESCRIBE_AND_YAML_HINT: &str = "| describe <d> | yaml <y> ";
23
pub static DESCRIBE_YAML_AND_LOGS_HINT: &str = "| describe <d> | yaml <y> | logs <o> ";
24
pub static DESCRIBE_YAML_LOGS_AND_ESC_HINT: &str =
25
  "| describe <d> | yaml <y> | logs <o> | back to menu <esc> ";
26
pub static DESCRIBE_YAML_AND_ESC_HINT: &str = "| describe <d> | yaml <y> | back to menu <esc> ";
27
pub static DESCRIBE_YAML_DECODE_AND_ESC_HINT: &str =
28
  "| describe <d> | yaml <y> | decode <x> | back to menu <esc> ";
29

30
// default colors
31
pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55);
32
pub const COLOR_CYAN: Color = Color::Rgb(0, 230, 230);
33
pub const COLOR_LIGHT_BLUE: Color = Color::Rgb(138, 196, 255);
34
pub const COLOR_YELLOW: Color = Color::Rgb(249, 229, 113);
35
pub const COLOR_GREEN: Color = Color::Rgb(72, 213, 150);
36
pub const COLOR_RED: Color = Color::Rgb(249, 167, 164);
37
pub const COLOR_ORANGE: Color = Color::Rgb(255, 170, 66);
38
pub const COLOR_WHITE: Color = Color::Rgb(255, 255, 255);
39
pub const COLOR_MAGENTA: Color = Color::Rgb(199, 146, 234);
40
pub const COLOR_DARK_GRAY: Color = Color::Rgb(50, 50, 50);
41
// light theme colors
42
pub const COLOR_MAGENTA_DARK: Color = Color::Rgb(153, 26, 237);
43
pub const COLOR_GRAY: Color = Color::Rgb(91, 87, 87);
44
pub const COLOR_BLUE: Color = Color::Rgb(0, 82, 163);
45
pub const COLOR_GREEN_DARK: Color = Color::Rgb(20, 97, 73);
46
pub const COLOR_RED_DARK: Color = Color::Rgb(173, 25, 20);
47
pub const COLOR_ORANGE_DARK: Color = Color::Rgb(184, 49, 15);
48
// YAML background colors
49
const YAML_BACKGROUND_LIGHT: syntect::highlighting::Color = syntect::highlighting::Color::WHITE;
50
const YAML_BACKGROUND_DARK: syntect::highlighting::Color = syntect::highlighting::Color {
51
  r: 35,
52
  g: 50,
53
  b: 55,
54
  a: 255,
55
}; // corresponds to TEAL
56

57
/// Convert a syntect highlight segment into an owned ratatui Span.
58
fn syntect_to_ratatui_span_owned(
×
59
  (style, content): (syntect::highlighting::Style, &str),
×
60
) -> Option<Span<'static>> {
×
61
  use syntect::highlighting::FontStyle;
62
  let fg = if style.foreground.a > 0 {
×
63
    Some(Color::Rgb(
×
64
      style.foreground.r,
×
65
      style.foreground.g,
×
66
      style.foreground.b,
×
67
    ))
×
68
  } else {
69
    None
×
70
  };
71
  let bg = if style.background.a > 0 {
×
72
    Some(Color::Rgb(
×
73
      style.background.r,
×
74
      style.background.g,
×
75
      style.background.b,
×
76
    ))
×
77
  } else {
78
    None
×
79
  };
80
  let modifier = {
×
81
    let fs = style.font_style;
×
82
    let mut m = Modifier::empty();
×
83
    if fs.contains(FontStyle::BOLD) {
×
84
      m |= Modifier::BOLD;
×
85
    }
×
86
    if fs.contains(FontStyle::ITALIC) {
×
87
      m |= Modifier::ITALIC;
×
88
    }
×
89
    if fs.contains(FontStyle::UNDERLINE) {
×
90
      m |= Modifier::UNDERLINED;
×
91
    }
×
92
    m
×
93
  };
94
  let ratatui_style = Style::default()
×
95
    .fg(fg.unwrap_or_default())
×
96
    .bg(bg.unwrap_or_default())
×
97
    .add_modifier(modifier);
×
98
  Some(Span::styled(content.to_owned(), ratatui_style))
×
99
}
×
100

101
fn get_syntax_set() -> &'static syntect::parsing::SyntaxSet {
×
102
  static SYNTAX_SET: OnceLock<syntect::parsing::SyntaxSet> = OnceLock::new();
103
  SYNTAX_SET.get_or_init(syntect::parsing::SyntaxSet::load_defaults_newlines)
×
104
}
×
105

106
fn get_yaml_syntax_reference() -> &'static syntect::parsing::SyntaxReference {
×
107
  static YAML_SYNTAX_REFERENCE: OnceLock<syntect::parsing::SyntaxReference> = OnceLock::new();
108
  YAML_SYNTAX_REFERENCE.get_or_init(|| {
×
109
    get_syntax_set()
×
110
      .find_syntax_by_extension("yaml")
×
111
      .unwrap()
×
112
      .clone()
×
113
  })
×
114
}
×
115

116
struct YamlThemes {
117
  dark: syntect::highlighting::Theme,
118
  light: syntect::highlighting::Theme,
119
}
120

121
fn get_yaml_themes() -> &'static YamlThemes {
×
122
  static YAML_THEMES: OnceLock<YamlThemes> = OnceLock::new();
123
  YAML_THEMES.get_or_init(|| {
×
124
    let ts = syntect::highlighting::ThemeSet::load_defaults();
×
125
    let mut dark = ts.themes["Solarized (dark)"].clone();
×
126
    dark.settings.background = Some(YAML_BACKGROUND_DARK);
×
127
    let mut light = ts.themes["Solarized (light)"].clone();
×
128
    light.settings.background = Some(YAML_BACKGROUND_LIGHT);
×
129
    YamlThemes { dark, light }
×
130
  })
×
131
}
×
132

133
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
134
pub enum Styles {
135
  Default,
136
  Header,
137
  Logo,
138
  Failure,
139
  Warning,
140
  Success,
141
  Primary,
142
  Secondary,
143
  Help,
144
  Background,
145
}
146

147
pub fn theme_styles(light: bool) -> BTreeMap<Styles, Style> {
78✔
148
  if light {
78✔
149
    BTreeMap::from([
×
150
      (Styles::Default, Style::default().fg(COLOR_GRAY)),
×
151
      (Styles::Header, Style::default().fg(COLOR_DARK_GRAY)),
×
152
      (Styles::Logo, Style::default().fg(COLOR_GREEN_DARK)),
×
153
      (Styles::Failure, Style::default().fg(COLOR_RED_DARK)),
×
154
      (Styles::Warning, Style::default().fg(COLOR_ORANGE_DARK)),
×
155
      (Styles::Success, Style::default().fg(COLOR_GREEN_DARK)),
×
156
      (Styles::Primary, Style::default().fg(COLOR_BLUE)),
×
157
      (Styles::Secondary, Style::default().fg(COLOR_MAGENTA_DARK)),
×
158
      (Styles::Help, Style::default().fg(COLOR_BLUE)),
×
159
      (
×
160
        Styles::Background,
×
161
        Style::default().bg(COLOR_WHITE).fg(COLOR_GRAY),
×
162
      ),
×
163
    ])
×
164
  } else {
165
    BTreeMap::from([
78✔
166
      (Styles::Default, Style::default().fg(COLOR_WHITE)),
78✔
167
      (Styles::Header, Style::default().fg(COLOR_DARK_GRAY)),
78✔
168
      (Styles::Logo, Style::default().fg(COLOR_GREEN)),
78✔
169
      (Styles::Failure, Style::default().fg(COLOR_RED)),
78✔
170
      (Styles::Warning, Style::default().fg(COLOR_ORANGE)),
78✔
171
      (Styles::Success, Style::default().fg(COLOR_GREEN)),
78✔
172
      (Styles::Primary, Style::default().fg(COLOR_CYAN)),
78✔
173
      (Styles::Secondary, Style::default().fg(COLOR_YELLOW)),
78✔
174
      (Styles::Help, Style::default().fg(COLOR_LIGHT_BLUE)),
78✔
175
      (
78✔
176
        Styles::Background,
78✔
177
        Style::default().bg(COLOR_TEAL).fg(COLOR_WHITE),
78✔
178
      ),
78✔
179
    ])
78✔
180
  }
181
}
78✔
182

183
pub fn title_style(txt: &str) -> Span<'_> {
1✔
184
  Span::styled(txt, style_bold())
1✔
185
}
1✔
186

187
pub fn style_header_text(light: bool) -> Style {
×
188
  *theme_styles(light).get(&Styles::Header).unwrap()
×
189
}
×
190

191
pub fn style_header() -> Style {
×
192
  Style::default().bg(COLOR_MAGENTA)
×
193
}
×
194

195
pub fn style_bold() -> Style {
1✔
196
  Style::default().add_modifier(Modifier::BOLD)
1✔
197
}
1✔
198

199
pub fn style_default(light: bool) -> Style {
20✔
200
  *theme_styles(light).get(&Styles::Default).unwrap()
20✔
201
}
20✔
202
pub fn style_logo(light: bool) -> Style {
×
203
  *theme_styles(light).get(&Styles::Logo).unwrap()
×
204
}
×
205
pub fn style_failure(light: bool) -> Style {
1✔
206
  *theme_styles(light).get(&Styles::Failure).unwrap()
1✔
207
}
1✔
208
pub fn style_warning(light: bool) -> Style {
×
209
  *theme_styles(light).get(&Styles::Warning).unwrap()
×
210
}
×
211
pub fn style_success(light: bool) -> Style {
×
212
  *theme_styles(light).get(&Styles::Success).unwrap()
×
213
}
×
214
pub fn style_primary(light: bool) -> Style {
47✔
215
  *theme_styles(light).get(&Styles::Primary).unwrap()
47✔
216
}
47✔
217
pub fn style_help(light: bool) -> Style {
×
218
  *theme_styles(light).get(&Styles::Help).unwrap()
×
219
}
×
220

221
pub fn style_secondary(light: bool) -> Style {
10✔
222
  *theme_styles(light).get(&Styles::Secondary).unwrap()
10✔
223
}
10✔
224

225
pub fn style_main_background(light: bool) -> Style {
×
226
  *theme_styles(light).get(&Styles::Background).unwrap()
×
227
}
×
228

229
pub fn style_highlight() -> Style {
5✔
230
  Style::default().add_modifier(Modifier::REVERSED)
5✔
231
}
5✔
232

233
pub fn get_gauge_symbol(enhanced_graphics: bool) -> &'static str {
×
234
  if enhanced_graphics {
×
235
    symbols::line::THICK_HORIZONTAL
×
236
  } else {
237
    symbols::line::HORIZONTAL
×
238
  }
239
}
×
240

241
pub fn table_header_style(cells: Vec<&str>, light: bool) -> Row<'_> {
4✔
242
  Row::new(cells).style(style_default(light)).bottom_margin(0)
4✔
243
}
4✔
244

245
pub fn horizontal_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
×
246
  Layout::default()
×
247
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
248
      &constraints,
×
249
    ))
×
250
    .direction(Direction::Horizontal)
×
251
    .split(size)
×
252
}
×
253

254
pub fn horizontal_chunks_with_margin(
×
255
  constraints: Vec<Constraint>,
×
256
  size: Rect,
×
257
  margin: u16,
×
258
) -> Rc<[Rect]> {
×
259
  Layout::default()
×
260
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
261
      &constraints,
×
262
    ))
×
263
    .direction(Direction::Horizontal)
×
264
    .margin(margin)
×
265
    .split(size)
×
266
}
×
267

268
pub fn vertical_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
2✔
269
  Layout::default()
2✔
270
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
2✔
271
      &constraints,
2✔
272
    ))
2✔
273
    .direction(Direction::Vertical)
2✔
274
    .split(size)
2✔
275
}
2✔
276

277
pub fn vertical_chunks_with_margin(
1✔
278
  constraints: Vec<Constraint>,
1✔
279
  size: Rect,
1✔
280
  margin: u16,
1✔
281
) -> Rc<[Rect]> {
1✔
282
  Layout::default()
1✔
283
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
1✔
284
      &constraints,
1✔
285
    ))
1✔
286
    .direction(Direction::Vertical)
1✔
287
    .margin(margin)
1✔
288
    .split(size)
1✔
289
}
1✔
290

291
pub fn layout_block(title: Span<'_>) -> Block<'_> {
1✔
292
  Block::default().borders(Borders::ALL).title(title)
1✔
293
}
1✔
294

295
pub fn layout_block_default(title: &str) -> Block<'_> {
1✔
296
  layout_block(title_style(title))
1✔
297
}
1✔
298

299
pub fn layout_block_active(title: &str, light: bool) -> Block<'_> {
×
300
  layout_block(title_style(title)).style(style_secondary(light))
×
301
}
×
302

303
pub fn layout_block_active_span(title: Line<'_>, light: bool) -> Block<'_> {
1✔
304
  Block::default()
1✔
305
    .borders(Borders::ALL)
1✔
306
    .title(title)
1✔
307
    .style(style_secondary(light))
1✔
308
}
1✔
309

310
pub fn layout_block_top_border(title: Line<'_>) -> Block<'_> {
5✔
311
  Block::default().borders(Borders::TOP).title(title)
5✔
312
}
5✔
313

314
enum FilterDisplayState<'a> {
315
  Inactive,
316
  EditingEmpty,
317
  Value { filter: &'a str, active: bool },
318
}
319

320
fn filter_display_state(filter: &str, active: bool) -> FilterDisplayState<'_> {
1✔
321
  if active && filter.is_empty() {
1✔
UNCOV
322
    FilterDisplayState::EditingEmpty
×
323
  } else if !filter.is_empty() {
1✔
UNCOV
324
    FilterDisplayState::Value { filter, active }
×
325
  } else {
326
    FilterDisplayState::Inactive
1✔
327
  }
328
}
1✔
329

UNCOV
330
pub fn filter_status_hint(filter: &str, active: bool) -> String {
×
331
  match filter_display_state(filter, active) {
×
332
    FilterDisplayState::Inactive => "filter < / >".to_string(),
×
333
    FilterDisplayState::EditingEmpty => "type to filter | clear <esc>".to_string(),
×
334
    FilterDisplayState::Value {
335
      filter,
×
336
      active: true,
337
    } => format!("[{}] | clear <esc>", filter),
×
338
    FilterDisplayState::Value {
339
      filter,
×
340
      active: false,
341
    } => format!("[{}] | edit < / >", filter),
×
342
  }
343
}
×
344

345
pub fn filter_bar_title<'a>(filter: &'a str, active: bool, light: bool) -> Line<'a> {
1✔
346
  match filter_display_state(filter, active) {
1✔
347
    FilterDisplayState::Inactive => {
348
      Line::from(vec![Span::styled(" filter < / > ", style_secondary(light))])
1✔
349
    }
350
    FilterDisplayState::EditingEmpty => Line::from(vec![
×
351
      Span::styled(" / ", style_secondary(light)),
×
352
      Span::styled("type to filter", style_default(light)),
×
353
      Span::styled("  | clear <esc> ", style_secondary(light)),
×
354
    ]),
355
    FilterDisplayState::Value {
356
      filter,
×
357
      active: true,
358
    } => Line::from(vec![
×
359
      Span::styled(" / ", style_secondary(light)),
×
360
      Span::styled(filter, style_default(light)),
×
361
      Span::styled("  | clear <esc> ", style_secondary(light)),
×
362
    ]),
363
    FilterDisplayState::Value {
364
      filter,
×
365
      active: false,
366
    } => Line::from(vec![
×
367
      Span::styled(" / ", style_secondary(light)),
×
368
      Span::styled(filter, style_default(light)),
×
369
      Span::styled("  | edit < / > ", style_secondary(light)),
×
370
    ]),
371
  }
372
}
1✔
373

374
pub fn title_with_dual_style<'a>(part_1: String, part_2: String, light: bool) -> Line<'a> {
5✔
375
  Line::from(vec![
5✔
376
    Span::styled(part_1, style_secondary(light).add_modifier(Modifier::BOLD)),
5✔
377
    Span::styled(part_2, style_default(light).add_modifier(Modifier::BOLD)),
5✔
378
  ])
379
}
5✔
380

381
/// helper function to create a centered rect using up
382
/// certain percentage of the available rect `r`
UNCOV
383
pub fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
×
384
  let Rect {
UNCOV
385
    width: grid_width,
×
UNCOV
386
    height: grid_height,
×
387
    ..
UNCOV
388
  } = r;
×
UNCOV
389
  let outer_height = (grid_height / 2).saturating_sub(height / 2);
×
390

UNCOV
391
  let popup_layout = Layout::default()
×
UNCOV
392
    .direction(Direction::Vertical)
×
UNCOV
393
    .constraints(
×
UNCOV
394
      [
×
UNCOV
395
        Constraint::Length(outer_height),
×
UNCOV
396
        Constraint::Length(height),
×
UNCOV
397
        Constraint::Length(outer_height),
×
UNCOV
398
      ]
×
UNCOV
399
      .as_ref(),
×
UNCOV
400
    )
×
UNCOV
401
    .split(r);
×
402

UNCOV
403
  let outer_width = (grid_width / 2).saturating_sub(width / 2);
×
404

UNCOV
405
  Layout::default()
×
UNCOV
406
    .direction(Direction::Horizontal)
×
UNCOV
407
    .constraints(
×
UNCOV
408
      [
×
UNCOV
409
        Constraint::Length(outer_width),
×
UNCOV
410
        Constraint::Length(width),
×
UNCOV
411
        Constraint::Length(outer_width),
×
UNCOV
412
      ]
×
UNCOV
413
      .as_ref(),
×
UNCOV
414
    )
×
UNCOV
415
    .split(popup_layout[1])[1]
×
416
}
×
417

418
pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool, light: bool) {
×
UNCOV
419
  if is_loading {
×
UNCOV
420
    let text = "\n\n Loading ...\n\n".to_owned();
×
421
    let text = Text::from(text);
×
422
    let text = text.patch_style(style_secondary(light));
×
UNCOV
423

×
424
    // Contains the text
×
425
    let paragraph = Paragraph::new(text)
×
UNCOV
426
      .style(style_secondary(light))
×
427
      .block(block);
×
428
    f.render_widget(paragraph, area);
×
UNCOV
429
  } else {
×
430
    f.render_widget(block, area)
×
431
  }
432
}
×
433

434
// using a macro to reuse code as generics will make handling lifetimes a PITA
435
#[macro_export]
436
macro_rules! draw_resource_tab {
437
  ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => {
438
    match $block {
439
      ActiveBlock::Describe => draw_describe_block(
440
        $f,
441
        $app,
442
        $area,
443
        title_with_dual_style(
444
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
445
          format!("{} | {} <esc> ", COPY_HINT, $title),
446
          $app.light_theme,
447
        ),
448
      ),
449
      ActiveBlock::Yaml => draw_yaml_block(
450
        $f,
451
        $app,
452
        $area,
453
        title_with_dual_style(
454
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
455
          format!("{} | {} <esc> ", COPY_HINT, $title),
456
          $app.light_theme,
457
        ),
458
      ),
459
      ActiveBlock::Pods => $crate::app::pods::draw_block_as_sub($f, $app, $area),
460
      ActiveBlock::Containers => $crate::app::pods::draw_containers_block($f, $app, $area),
461
      ActiveBlock::Logs => $crate::app::pods::draw_logs_block($f, $app, $area),
462
      ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area),
463
      _ => $fn2($f, $app, $area),
464
    };
465
  };
466
}
467

468
pub struct ResourceTableProps<'a, T> {
469
  pub title: String,
470
  pub inline_help: String,
471
  pub resource: &'a mut StatefulTable<T>,
472
  pub table_headers: Vec<&'a str>,
473
  pub column_widths: Vec<Constraint>,
474
}
475
/// common for all resources
UNCOV
476
pub fn draw_describe_block(f: &mut Frame<'_>, app: &mut App, area: Rect, title: Line<'_>) {
×
UNCOV
477
  draw_yaml_block(f, app, area, title);
×
UNCOV
478
}
×
479

480
/// common for all resources
UNCOV
481
pub fn draw_yaml_block(f: &mut Frame<'_>, app: &mut App, area: Rect, title: Line<'_>) {
×
UNCOV
482
  let block = layout_block_top_border(title);
×
483

UNCOV
484
  let txt = app.data.describe_out.get_txt();
×
UNCOV
485
  if !txt.is_empty() {
×
486
    // Re-highlight only when the cache is empty or the theme changed.
UNCOV
487
    if app.data.describe_out.highlighted_lines.is_empty()
×
UNCOV
488
      || app.data.describe_out.highlight_light_theme != app.light_theme
×
489
    {
UNCOV
490
      let ss = get_syntax_set();
×
UNCOV
491
      let syntax = get_yaml_syntax_reference();
×
UNCOV
492
      let theme = if app.light_theme {
×
UNCOV
493
        &get_yaml_themes().light
×
494
      } else {
UNCOV
495
        &get_yaml_themes().dark
×
496
      };
UNCOV
497
      let mut h = syntect::easy::HighlightLines::new(syntax, theme);
×
UNCOV
498
      let lines: Vec<_> = syntect::util::LinesWithEndings::from(txt)
×
UNCOV
499
        .filter_map(|line| match h.highlight_line(line, ss) {
×
UNCOV
500
          Ok(segments) => {
×
UNCOV
501
            let line_spans: Vec<_> = segments
×
UNCOV
502
              .into_iter()
×
UNCOV
503
              .filter_map(syntect_to_ratatui_span_owned)
×
UNCOV
504
              .collect();
×
UNCOV
505
            Some(ratatui::text::Line::from(line_spans))
×
506
          }
UNCOV
507
          Err(_) => None,
×
UNCOV
508
        })
×
UNCOV
509
        .collect();
×
UNCOV
510
      app.data.describe_out.highlighted_lines = lines;
×
UNCOV
511
      app.data.describe_out.highlight_light_theme = app.light_theme;
×
512
    }
×
513

514
    let paragraph = Paragraph::new(app.data.describe_out.highlighted_lines.clone())
×
UNCOV
515
      .block(block)
×
UNCOV
516
      .wrap(Wrap { trim: false })
×
UNCOV
517
      .scroll((
×
UNCOV
518
        app.data.describe_out.offset.min(u16::MAX as usize) as u16,
×
UNCOV
519
        0,
×
UNCOV
520
      ));
×
UNCOV
521
    f.render_widget(paragraph, area);
×
UNCOV
522
  } else {
×
UNCOV
523
    loading(f, block, area, app.is_loading(), app.light_theme);
×
UNCOV
524
  }
×
525
}
×
526

527
/// Draw a kubernetes resource overview tab
528
pub fn draw_resource_block<'a, T: KubeResource<U>, F, U: Serialize>(
4✔
529
  f: &mut Frame<'_>,
4✔
530
  area: Rect,
4✔
531
  table_props: ResourceTableProps<'a, T>,
4✔
532
  row_cell_mapper: F,
4✔
533
  light_theme: bool,
4✔
534
  is_loading: bool,
4✔
535
) where
4✔
536
  F: Fn(&T) -> Row<'a>,
4✔
537
{
538
  let title = title_with_dual_style(table_props.title, table_props.inline_help, light_theme);
4✔
539
  let block = layout_block_top_border(title);
4✔
540

541
  if !table_props.resource.items.is_empty() {
4✔
542
    let filter = table_props.resource.filter.to_lowercase();
4✔
543
    // Build filtered rows and track the mapping from visible index → items index.
544
    let has_filter = !filter.is_empty();
4✔
545
    let mut filtered_indices: Vec<usize> = Vec::new();
4✔
546
    let rows: Vec<Row<'a>> = table_props
4✔
547
      .resource
4✔
548
      .items
4✔
549
      .iter()
4✔
550
      .enumerate()
4✔
551
      .filter_map(|(idx, c)| {
10✔
552
        let mapper = row_cell_mapper(c);
10✔
553
        if filter.is_empty() || filter_by_name(&filter, c) {
10✔
554
          Some((idx, mapper))
8✔
555
        } else {
556
          None
2✔
557
        }
558
      })
10✔
559
      .map(|(idx, row)| {
8✔
560
        if has_filter {
8✔
561
          filtered_indices.push(idx);
4✔
562
        }
4✔
563
        row
8✔
564
      })
8✔
565
      .collect();
4✔
566

567
    // Clamp selection to the filtered list length.
568
    if has_filter {
4✔
569
      let max = filtered_indices.len().saturating_sub(1);
2✔
570
      if let Some(sel) = table_props.resource.state.selected() {
2✔
571
        if sel > max {
2✔
UNCOV
572
          table_props.resource.state.select(Some(max));
×
573
        }
2✔
UNCOV
574
      }
×
575
    }
2✔
576
    table_props.resource.filtered_indices = filtered_indices;
4✔
577

578
    let table = Table::new(rows, &table_props.column_widths)
4✔
579
      .header(table_header_style(table_props.table_headers, light_theme))
4✔
580
      .block(block)
4✔
581
      .row_highlight_style(style_highlight())
4✔
582
      .highlight_symbol(HIGHLIGHT);
4✔
583

584
    f.render_stateful_widget(table, area, &mut table_props.resource.state);
4✔
UNCOV
585
  } else {
×
586
    loading(f, block, area, is_loading, light_theme);
×
UNCOV
587
  }
×
588
}
4✔
589

UNCOV
590
pub fn filter_by_resource_name<T: KubeResource<U>, U: Serialize>(
×
UNCOV
591
  filter: &str,
×
UNCOV
592
  res: &T,
×
UNCOV
593
  row_cell_mapper: Row<'static>,
×
UNCOV
594
) -> Option<Row<'static>> {
×
UNCOV
595
  if filter.is_empty() || filter_by_name(filter, res) {
×
UNCOV
596
    Some(row_cell_mapper)
×
597
  } else {
UNCOV
598
    None
×
599
  }
UNCOV
600
}
×
601

602
pub fn text_matches_filter(filter: &str, value: &str) -> bool {
32✔
603
  let filter = filter.to_lowercase();
32✔
604
  let value = value.to_lowercase();
32✔
605
  filter.is_empty() || glob_match(&filter, &value) || value.contains(&filter)
32✔
606
}
32✔
607

608
fn filter_by_name<T: KubeResource<U>, U: Serialize>(ft: &str, res: &T) -> bool {
6✔
609
  text_matches_filter(ft, res.get_name())
6✔
610
}
6✔
611

612
pub fn get_cluster_wide_resource_title<S: AsRef<str>>(
2✔
613
  title: S,
2✔
614
  items_len: usize,
2✔
615
  suffix: S,
2✔
616
) -> String {
2✔
617
  format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref())
2✔
618
}
2✔
619

620
pub fn get_resource_title<S: AsRef<str>>(
5✔
621
  app: &App,
5✔
622
  title: S,
5✔
623
  suffix: S,
5✔
624
  items_len: usize,
5✔
625
) -> String {
5✔
626
  format!(
5✔
627
    " {} {}",
628
    title_with_ns(
5✔
629
      title.as_ref(),
5✔
630
      app
5✔
631
        .data
5✔
632
        .selected
5✔
633
        .ns
5✔
634
        .as_ref()
5✔
635
        .unwrap_or(&String::from("all")),
5✔
636
      items_len
5✔
637
    ),
638
    suffix.as_ref(),
5✔
639
  )
640
}
5✔
641

642
static DESCRIBE_ACTIVE: &str = "-> Describe ";
643
static YAML_ACTIVE: &str = "-> YAML ";
644

UNCOV
645
pub fn get_describe_active<'a>(block: ActiveBlock) -> &'a str {
×
UNCOV
646
  match block {
×
UNCOV
647
    ActiveBlock::Describe => DESCRIBE_ACTIVE,
×
UNCOV
648
    _ => YAML_ACTIVE,
×
649
  }
UNCOV
650
}
×
651

652
pub fn title_with_ns(title: &str, ns: &str, length: usize) -> String {
6✔
653
  format!("{} (ns: {}) [{}]", title, ns, length)
6✔
654
}
6✔
655

656
#[cfg(test)]
657
mod tests {
658
  use ratatui::{
659
    backend::TestBackend, buffer::Buffer, layout::Position, style::Modifier, widgets::Cell,
660
    Terminal,
661
  };
662

663
  use super::*;
664
  use crate::ui::utils::{COLOR_CYAN, COLOR_WHITE, COLOR_YELLOW};
665

666
  #[test]
667
  fn test_draw_resource_block() {
1✔
668
    let backend = TestBackend::new(100, 6);
1✔
669
    let mut terminal = Terminal::new(backend).unwrap();
1✔
670

671
    struct RenderTest {
672
      pub name: String,
673
      pub namespace: String,
674
      pub data: i32,
675
      pub age: String,
676
    }
677

678
    impl KubeResource<Option<String>> for RenderTest {
UNCOV
679
      fn get_name(&self) -> &String {
×
UNCOV
680
        &self.name
×
UNCOV
681
      }
×
UNCOV
682
      fn get_k8s_obj(&self) -> &Option<String> {
×
UNCOV
683
        &None
×
UNCOV
684
      }
×
685
    }
686
    terminal
1✔
687
      .draw(|f| {
1✔
688
        let size = f.area();
1✔
689
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
690
        resource.set_items(vec![
1✔
691
          RenderTest {
1✔
692
            name: "Test 1".into(),
1✔
693
            namespace: "Test ns".into(),
1✔
694
            age: "65h3m".into(),
1✔
695
            data: 5,
1✔
696
          },
1✔
697
          RenderTest {
1✔
698
            name: "Test long name that should be truncated from view".into(),
1✔
699
            namespace: "Test ns".into(),
1✔
700
            age: "65h3m".into(),
1✔
701
            data: 3,
1✔
702
          },
1✔
703
          RenderTest {
1✔
704
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
705
            namespace: "Test ns long value check that should be truncated".into(),
1✔
706
            age: "65h3m".into(),
1✔
707
            data: 6,
1✔
708
          },
1✔
709
        ]);
710
        draw_resource_block(
1✔
711
          f,
1✔
712
          size,
1✔
713
          ResourceTableProps {
1✔
714
            title: "Test".into(),
1✔
715
            inline_help: "-> yaml <y>".into(),
1✔
716
            resource: &mut resource,
1✔
717
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
718
            column_widths: vec![
1✔
719
              Constraint::Percentage(30),
1✔
720
              Constraint::Percentage(40),
1✔
721
              Constraint::Percentage(15),
1✔
722
              Constraint::Percentage(15),
1✔
723
            ],
1✔
724
          },
1✔
725
          |c| {
3✔
726
            Row::new(vec![
3✔
727
              Cell::from(c.namespace.to_owned()),
3✔
728
              Cell::from(c.name.to_owned()),
3✔
729
              Cell::from(c.data.to_string()),
3✔
730
              Cell::from(c.age.to_owned()),
3✔
731
            ])
732
            .style(style_primary(false))
3✔
733
          },
3✔
734
          false,
735
          false,
736
        );
737
      })
1✔
738
      .unwrap();
1✔
739

740
    let mut expected = Buffer::with_lines(vec![
1✔
741
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
742
        "   Namespace                     Name                                 Data           Age            ",
1✔
743
        "=> Test ns                       Test 1                               5              65h3m          ",
1✔
744
        "   Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
745
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
746
        "                                                                                                    ",
1✔
747
      ]);
748
    // set row styles
749
    // First row heading style
750
    for col in 0..=99 {
100✔
751
      match col {
100✔
752
        0..=3 => {
100✔
753
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
754
            Style::default()
4✔
755
              .fg(COLOR_YELLOW)
4✔
756
              .add_modifier(Modifier::BOLD),
4✔
757
          );
4✔
758
        }
4✔
759
        4..=14 => {
96✔
760
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
761
            Style::default()
11✔
762
              .fg(COLOR_WHITE)
11✔
763
              .add_modifier(Modifier::BOLD),
11✔
764
          );
11✔
765
        }
11✔
766
        _ => {}
85✔
767
      }
768
    }
769

770
    // Second row table header style
771
    for col in 0..=99 {
100✔
772
      expected
100✔
773
        .cell_mut(Position::new(col, 1))
100✔
774
        .unwrap()
100✔
775
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
776
    }
100✔
777
    // first table data row style
778
    for col in 0..=99 {
100✔
779
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
780
        Style::default()
100✔
781
          .fg(COLOR_CYAN)
100✔
782
          .add_modifier(Modifier::REVERSED),
100✔
783
      );
100✔
784
    }
100✔
785
    // remaining table data row style
786
    for row in 3..=4 {
2✔
787
      for col in 0..=99 {
200✔
788
        expected
200✔
789
          .cell_mut(Position::new(col, row))
200✔
790
          .unwrap()
200✔
791
          .set_style(Style::default().fg(COLOR_CYAN));
200✔
792
      }
200✔
793
    }
794

795
    terminal.backend().assert_buffer(&expected);
1✔
796
  }
1✔
797

798
  #[test]
799
  fn test_draw_resource_block_filter() {
1✔
800
    let backend = TestBackend::new(100, 6);
1✔
801
    let mut terminal = Terminal::new(backend).unwrap();
1✔
802

803
    struct RenderTest {
804
      pub name: String,
805
      pub namespace: String,
806
      pub data: i32,
807
      pub age: String,
808
    }
809
    impl KubeResource<Option<String>> for RenderTest {
810
      fn get_name(&self) -> &String {
3✔
811
        &self.name
3✔
812
      }
3✔
UNCOV
813
      fn get_k8s_obj(&self) -> &Option<String> {
×
UNCOV
814
        &None
×
UNCOV
815
      }
×
816
    }
817

818
    terminal
1✔
819
      .draw(|f| {
1✔
820
        let size = f.area();
1✔
821
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
822
        resource.set_items(vec![
1✔
823
          RenderTest {
1✔
824
            name: "Test 1".into(),
1✔
825
            namespace: "Test ns".into(),
1✔
826
            age: "65h3m".into(),
1✔
827
            data: 5,
1✔
828
          },
1✔
829
          RenderTest {
1✔
830
            name: "Test long name that should be truncated from view".into(),
1✔
831
            namespace: "Test ns".into(),
1✔
832
            age: "65h3m".into(),
1✔
833
            data: 3,
1✔
834
          },
1✔
835
          RenderTest {
1✔
836
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
837
            namespace: "Test ns long value check that should be truncated".into(),
1✔
838
            age: "65h3m".into(),
1✔
839
            data: 6,
1✔
840
          },
1✔
841
        ]);
842
        resource.filter = "truncated".to_string();
1✔
843
        draw_resource_block(
1✔
844
          f,
1✔
845
          size,
1✔
846
          ResourceTableProps {
1✔
847
            title: "Test".into(),
1✔
848
            inline_help: "-> yaml <y>".into(),
1✔
849
            resource: &mut resource,
1✔
850
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
851
            column_widths: vec![
1✔
852
              Constraint::Percentage(30),
1✔
853
              Constraint::Percentage(40),
1✔
854
              Constraint::Percentage(15),
1✔
855
              Constraint::Percentage(15),
1✔
856
            ],
1✔
857
          },
1✔
858
          |c| {
3✔
859
            Row::new(vec![
3✔
860
              Cell::from(c.namespace.to_owned()),
3✔
861
              Cell::from(c.name.to_owned()),
3✔
862
              Cell::from(c.data.to_string()),
3✔
863
              Cell::from(c.age.to_owned()),
3✔
864
            ])
865
            .style(style_primary(false))
3✔
866
          },
3✔
867
          false,
868
          false,
869
        );
870
      })
1✔
871
      .unwrap();
1✔
872

873
    let mut expected = Buffer::with_lines(vec![
1✔
874
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
875
        "   Namespace                     Name                                 Data           Age            ",
1✔
876
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
877
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
878
        "                                                                                                    ",
1✔
879
        "                                                                                                    ",
1✔
880
      ]);
881
    // set row styles
882
    // First row heading style
883
    for col in 0..=99 {
100✔
884
      match col {
100✔
885
        0..=3 => {
100✔
886
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
887
            Style::default()
4✔
888
              .fg(COLOR_YELLOW)
4✔
889
              .add_modifier(Modifier::BOLD),
4✔
890
          );
4✔
891
        }
4✔
892
        4..=14 => {
96✔
893
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
894
            Style::default()
11✔
895
              .fg(COLOR_WHITE)
11✔
896
              .add_modifier(Modifier::BOLD),
11✔
897
          );
11✔
898
        }
11✔
899
        _ => {}
85✔
900
      }
901
    }
902

903
    // Second row table header style
904
    for col in 0..=99 {
100✔
905
      expected
100✔
906
        .cell_mut(Position::new(col, 1))
100✔
907
        .unwrap()
100✔
908
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
909
    }
100✔
910
    // first table data row style
911
    for col in 0..=99 {
100✔
912
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
913
        Style::default()
100✔
914
          .fg(COLOR_CYAN)
100✔
915
          .add_modifier(Modifier::REVERSED),
100✔
916
      );
100✔
917
    }
100✔
918
    // remaining table data row style
919
    for row in 3..=3 {
1✔
920
      for col in 0..=99 {
100✔
921
        expected
100✔
922
          .cell_mut(Position::new(col, row))
100✔
923
          .unwrap()
100✔
924
          .set_style(Style::default().fg(COLOR_CYAN));
100✔
925
      }
100✔
926
    }
927

928
    terminal.backend().assert_buffer(&expected);
1✔
929
  }
1✔
930

931
  #[test]
932
  fn test_draw_resource_block_filter_glob() {
1✔
933
    let backend = TestBackend::new(100, 6);
1✔
934
    let mut terminal = Terminal::new(backend).unwrap();
1✔
935

936
    struct RenderTest {
937
      pub name: String,
938
      pub namespace: String,
939
      pub data: i32,
940
      pub age: String,
941
    }
942
    impl KubeResource<Option<String>> for RenderTest {
943
      fn get_name(&self) -> &String {
3✔
944
        &self.name
3✔
945
      }
3✔
UNCOV
946
      fn get_k8s_obj(&self) -> &Option<String> {
×
UNCOV
947
        &None
×
UNCOV
948
      }
×
949
    }
950

951
    terminal
1✔
952
      .draw(|f| {
1✔
953
        let size = f.area();
1✔
954
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
955
        resource.set_items(vec![
1✔
956
          RenderTest {
1✔
957
            name: "Test 1".into(),
1✔
958
            namespace: "Test ns".into(),
1✔
959
            age: "65h3m".into(),
1✔
960
            data: 5,
1✔
961
          },
1✔
962
          RenderTest {
1✔
963
            name: "Test long name that should be truncated from view".into(),
1✔
964
            namespace: "Test ns".into(),
1✔
965
            age: "65h3m".into(),
1✔
966
            data: 3,
1✔
967
          },
1✔
968
          RenderTest {
1✔
969
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
970
            namespace: "Test ns long value check that should be truncated".into(),
1✔
971
            age: "65h3m".into(),
1✔
972
            data: 6,
1✔
973
          },
1✔
974
        ]);
975
        resource.filter = "*long*truncated*".to_string();
1✔
976
        draw_resource_block(
1✔
977
          f,
1✔
978
          size,
1✔
979
          ResourceTableProps {
1✔
980
            title: "Test".into(),
1✔
981
            inline_help: "-> yaml <y>".into(),
1✔
982
            resource: &mut resource,
1✔
983
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
984
            column_widths: vec![
1✔
985
              Constraint::Percentage(30),
1✔
986
              Constraint::Percentage(40),
1✔
987
              Constraint::Percentage(15),
1✔
988
              Constraint::Percentage(15),
1✔
989
            ],
1✔
990
          },
1✔
991
          |c| {
3✔
992
            Row::new(vec![
3✔
993
              Cell::from(c.namespace.to_owned()),
3✔
994
              Cell::from(c.name.to_owned()),
3✔
995
              Cell::from(c.data.to_string()),
3✔
996
              Cell::from(c.age.to_owned()),
3✔
997
            ])
998
            .style(style_primary(false))
3✔
999
          },
3✔
1000
          false,
1001
          false,
1002
        );
1003
      })
1✔
1004
      .unwrap();
1✔
1005

1006
    let mut expected = Buffer::with_lines(vec![
1✔
1007
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
1008
        "   Namespace                     Name                                 Data           Age            ",
1✔
1009
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
1010
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
1011
        "                                                                                                    ",
1✔
1012
        "                                                                                                    ",
1✔
1013
      ]);
1014
    // set row styles
1015
    // First row heading style
1016
    for col in 0..=99 {
100✔
1017
      match col {
100✔
1018
        0..=3 => {
100✔
1019
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
1020
            Style::default()
4✔
1021
              .fg(COLOR_YELLOW)
4✔
1022
              .add_modifier(Modifier::BOLD),
4✔
1023
          );
4✔
1024
        }
4✔
1025
        4..=14 => {
96✔
1026
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
1027
            Style::default()
11✔
1028
              .fg(COLOR_WHITE)
11✔
1029
              .add_modifier(Modifier::BOLD),
11✔
1030
          );
11✔
1031
        }
11✔
1032
        _ => {}
85✔
1033
      }
1034
    }
1035

1036
    // Second row table header style
1037
    for col in 0..=99 {
100✔
1038
      expected
100✔
1039
        .cell_mut(Position::new(col, 1))
100✔
1040
        .unwrap()
100✔
1041
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
1042
    }
100✔
1043
    // first table data row style
1044
    for col in 0..=99 {
100✔
1045
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1046
        Style::default()
100✔
1047
          .fg(COLOR_CYAN)
100✔
1048
          .add_modifier(Modifier::REVERSED),
100✔
1049
      );
100✔
1050
    }
100✔
1051
    // remaining table data row style
1052
    for row in 3..=3 {
1✔
1053
      for col in 0..=99 {
100✔
1054
        expected
100✔
1055
          .cell_mut(Position::new(col, row))
100✔
1056
          .unwrap()
100✔
1057
          .set_style(Style::default().fg(COLOR_CYAN));
100✔
1058
      }
100✔
1059
    }
1060

1061
    terminal.backend().assert_buffer(&expected);
1✔
1062
  }
1✔
1063

1064
  #[test]
1065
  fn test_get_resource_title() {
1✔
1066
    let app = App::default();
1✔
1067
    assert_eq!(
1✔
1068
      get_resource_title(&app, "Title", "-> hello", 5),
1✔
1069
      " Title (ns: all) [5] -> hello"
1070
    );
1071
  }
1✔
1072

1073
  #[test]
1074
  fn test_title_with_ns() {
1✔
1075
    assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]");
1✔
1076
  }
1✔
1077

1078
  #[test]
1079
  fn test_get_cluster_wide_resource_title() {
1✔
1080
    assert_eq!(
1✔
1081
      get_cluster_wide_resource_title("Cluster Resource", 3, ""),
1✔
1082
      " Cluster Resource [3] "
1083
    );
1084
    assert_eq!(
1✔
1085
      get_cluster_wide_resource_title("Nodes", 10, "-> hello"),
1✔
1086
      " Nodes [10] -> hello"
1087
    );
1088
  }
1✔
1089
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc