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

kdash-rs / kdash / 18585133834

17 Oct 2025 07:02AM UTC coverage: 57.358% (-1.0%) from 58.34%
18585133834

push

github

web-flow
Merge pull request #502 from mloskot/ml/feat/configurable-install-dir

feat: Allow getLatest.sh to deploy binary to custom location

3925 of 6843 relevant lines covered (57.36%)

7.77 hits per line

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

70.0
/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_ESC_HINT: &str = "| describe <d> | yaml <y> | back to menu <esc> ";
24
pub static DESCRIBE_YAML_DECODE_AND_ESC_HINT: &str =
25
  "| describe <d> | yaml <y> | decode <x> | back to menu <esc> ";
26

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

54
fn get_syntax_set() -> &'static syntect::parsing::SyntaxSet {
×
55
  static SYNTAX_SET: OnceLock<syntect::parsing::SyntaxSet> = OnceLock::new();
56
  SYNTAX_SET.get_or_init(syntect::parsing::SyntaxSet::load_defaults_newlines)
×
57
}
×
58

59
fn get_yaml_syntax_reference() -> &'static syntect::parsing::SyntaxReference {
×
60
  static YAML_SYNTAX_REFERENCE: OnceLock<syntect::parsing::SyntaxReference> = OnceLock::new();
61
  YAML_SYNTAX_REFERENCE.get_or_init(|| {
×
62
    get_syntax_set()
×
63
      .find_syntax_by_extension("yaml")
×
64
      .unwrap()
×
65
      .clone()
×
66
  })
×
67
}
×
68

69
struct YamlThemes {
70
  dark: syntect::highlighting::Theme,
71
  light: syntect::highlighting::Theme,
72
}
73

74
fn get_yaml_themes() -> &'static YamlThemes {
×
75
  static YAML_THEMES: OnceLock<YamlThemes> = OnceLock::new();
76
  YAML_THEMES.get_or_init(|| {
×
77
    let ts = syntect::highlighting::ThemeSet::load_defaults();
×
78
    let mut dark = ts.themes["Solarized (dark)"].clone();
×
79
    dark.settings.background = Some(YAML_BACKGROUND_DARK);
×
80
    let mut light = ts.themes["Solarized (light)"].clone();
×
81
    light.settings.background = Some(YAML_BACKGROUND_LIGHT);
×
82
    YamlThemes { dark, light }
×
83
  })
×
84
}
×
85

86
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
87
pub enum Styles {
88
  Default,
89
  Header,
90
  Logo,
91
  Failure,
92
  Warning,
93
  Success,
94
  Primary,
95
  Secondary,
96
  Help,
97
  Background,
98
}
99

100
pub fn theme_styles(light: bool) -> BTreeMap<Styles, Style> {
77✔
101
  if light {
77✔
102
    BTreeMap::from([
×
103
      (Styles::Default, Style::default().fg(COLOR_GRAY)),
×
104
      (Styles::Header, Style::default().fg(COLOR_DARK_GRAY)),
×
105
      (Styles::Logo, Style::default().fg(COLOR_GREEN_DARK)),
×
106
      (Styles::Failure, Style::default().fg(COLOR_RED_DARK)),
×
107
      (Styles::Warning, Style::default().fg(COLOR_ORANGE_DARK)),
×
108
      (Styles::Success, Style::default().fg(COLOR_GREEN_DARK)),
×
109
      (Styles::Primary, Style::default().fg(COLOR_BLUE)),
×
110
      (Styles::Secondary, Style::default().fg(COLOR_MAGENTA_DARK)),
×
111
      (Styles::Help, Style::default().fg(COLOR_BLUE)),
×
112
      (
×
113
        Styles::Background,
×
114
        Style::default().bg(COLOR_WHITE).fg(COLOR_GRAY),
×
115
      ),
×
116
    ])
×
117
  } else {
118
    BTreeMap::from([
77✔
119
      (Styles::Default, Style::default().fg(COLOR_WHITE)),
77✔
120
      (Styles::Header, Style::default().fg(COLOR_DARK_GRAY)),
77✔
121
      (Styles::Logo, Style::default().fg(COLOR_GREEN)),
77✔
122
      (Styles::Failure, Style::default().fg(COLOR_RED)),
77✔
123
      (Styles::Warning, Style::default().fg(COLOR_ORANGE)),
77✔
124
      (Styles::Success, Style::default().fg(COLOR_GREEN)),
77✔
125
      (Styles::Primary, Style::default().fg(COLOR_CYAN)),
77✔
126
      (Styles::Secondary, Style::default().fg(COLOR_YELLOW)),
77✔
127
      (Styles::Help, Style::default().fg(COLOR_LIGHT_BLUE)),
77✔
128
      (
77✔
129
        Styles::Background,
77✔
130
        Style::default().bg(COLOR_TEAL).fg(COLOR_WHITE),
77✔
131
      ),
77✔
132
    ])
77✔
133
  }
134
}
77✔
135

136
pub fn title_style(txt: &str) -> Span<'_> {
1✔
137
  Span::styled(txt, style_bold())
1✔
138
}
1✔
139

140
pub fn style_header_text(light: bool) -> Style {
×
141
  *theme_styles(light).get(&Styles::Header).unwrap()
×
142
}
×
143

144
pub fn style_header() -> Style {
×
145
  Style::default().bg(COLOR_MAGENTA)
×
146
}
×
147

148
pub fn style_bold() -> Style {
1✔
149
  Style::default().add_modifier(Modifier::BOLD)
1✔
150
}
1✔
151

152
pub fn style_default(light: bool) -> Style {
20✔
153
  *theme_styles(light).get(&Styles::Default).unwrap()
20✔
154
}
20✔
155
pub fn style_logo(light: bool) -> Style {
×
156
  *theme_styles(light).get(&Styles::Logo).unwrap()
×
157
}
×
158
pub fn style_failure(light: bool) -> Style {
1✔
159
  *theme_styles(light).get(&Styles::Failure).unwrap()
1✔
160
}
1✔
161
pub fn style_warning(light: bool) -> Style {
×
162
  *theme_styles(light).get(&Styles::Warning).unwrap()
×
163
}
×
164
pub fn style_success(light: bool) -> Style {
×
165
  *theme_styles(light).get(&Styles::Success).unwrap()
×
166
}
×
167
pub fn style_primary(light: bool) -> Style {
47✔
168
  *theme_styles(light).get(&Styles::Primary).unwrap()
47✔
169
}
47✔
170
pub fn style_help(light: bool) -> Style {
×
171
  *theme_styles(light).get(&Styles::Help).unwrap()
×
172
}
×
173

174
pub fn style_secondary(light: bool) -> Style {
9✔
175
  *theme_styles(light).get(&Styles::Secondary).unwrap()
9✔
176
}
9✔
177

178
pub fn style_main_background(light: bool) -> Style {
×
179
  *theme_styles(light).get(&Styles::Background).unwrap()
×
180
}
×
181

182
pub fn style_highlight() -> Style {
5✔
183
  Style::default().add_modifier(Modifier::REVERSED)
5✔
184
}
5✔
185

186
pub fn get_gauge_style(enhanced_graphics: bool) -> symbols::line::Set {
×
187
  if enhanced_graphics {
×
188
    symbols::line::THICK
×
189
  } else {
190
    symbols::line::NORMAL
×
191
  }
192
}
×
193

194
pub fn table_header_style(cells: Vec<&str>, light: bool) -> Row<'_> {
4✔
195
  Row::new(cells).style(style_default(light)).bottom_margin(0)
4✔
196
}
4✔
197

198
pub fn horizontal_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
×
199
  Layout::default()
×
200
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
201
      &constraints,
×
202
    ))
×
203
    .direction(Direction::Horizontal)
×
204
    .split(size)
×
205
}
×
206

207
pub fn horizontal_chunks_with_margin(
×
208
  constraints: Vec<Constraint>,
×
209
  size: Rect,
×
210
  margin: u16,
×
211
) -> Rc<[Rect]> {
×
212
  Layout::default()
×
213
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
214
      &constraints,
×
215
    ))
×
216
    .direction(Direction::Horizontal)
×
217
    .margin(margin)
×
218
    .split(size)
×
219
}
×
220

221
pub fn vertical_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
1✔
222
  Layout::default()
1✔
223
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
1✔
224
      &constraints,
1✔
225
    ))
1✔
226
    .direction(Direction::Vertical)
1✔
227
    .split(size)
1✔
228
}
1✔
229

230
pub fn vertical_chunks_with_margin(
1✔
231
  constraints: Vec<Constraint>,
1✔
232
  size: Rect,
1✔
233
  margin: u16,
1✔
234
) -> Rc<[Rect]> {
1✔
235
  Layout::default()
1✔
236
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
1✔
237
      &constraints,
1✔
238
    ))
1✔
239
    .direction(Direction::Vertical)
1✔
240
    .margin(margin)
1✔
241
    .split(size)
1✔
242
}
1✔
243

244
pub fn layout_block(title: Span<'_>) -> Block<'_> {
1✔
245
  Block::default().borders(Borders::ALL).title(title)
1✔
246
}
1✔
247

248
pub fn layout_block_default(title: &str) -> Block<'_> {
1✔
249
  layout_block(title_style(title))
1✔
250
}
1✔
251

252
pub fn layout_block_active(title: &str, light: bool) -> Block<'_> {
×
253
  layout_block(title_style(title)).style(style_secondary(light))
×
254
}
×
255

256
pub fn layout_block_active_span(title: Line<'_>, light: bool) -> Block<'_> {
1✔
257
  Block::default()
1✔
258
    .borders(Borders::ALL)
1✔
259
    .title(title)
1✔
260
    .style(style_secondary(light))
1✔
261
}
1✔
262

263
pub fn layout_block_top_border(title: Line<'_>) -> Block<'_> {
4✔
264
  Block::default().borders(Borders::TOP).title(title)
4✔
265
}
4✔
266

267
pub fn title_with_dual_style<'a>(part_1: String, part_2: String, light: bool) -> Line<'a> {
5✔
268
  Line::from(vec![
5✔
269
    Span::styled(part_1, style_secondary(light).add_modifier(Modifier::BOLD)),
5✔
270
    Span::styled(part_2, style_default(light).add_modifier(Modifier::BOLD)),
5✔
271
  ])
272
}
5✔
273

274
/// helper function to create a centered rect using up
275
/// certain percentage of the available rect `r`
276
pub fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
×
277
  let Rect {
278
    width: grid_width,
×
279
    height: grid_height,
×
280
    ..
281
  } = r;
×
282
  let outer_height = (grid_height / 2).saturating_sub(height / 2);
×
283

284
  let popup_layout = Layout::default()
×
285
    .direction(Direction::Vertical)
×
286
    .constraints(
×
287
      [
×
288
        Constraint::Length(outer_height),
×
289
        Constraint::Length(height),
×
290
        Constraint::Length(outer_height),
×
291
      ]
×
292
      .as_ref(),
×
293
    )
×
294
    .split(r);
×
295

296
  let outer_width = (grid_width / 2).saturating_sub(width / 2);
×
297

298
  Layout::default()
×
299
    .direction(Direction::Horizontal)
×
300
    .constraints(
×
301
      [
×
302
        Constraint::Length(outer_width),
×
303
        Constraint::Length(width),
×
304
        Constraint::Length(outer_width),
×
305
      ]
×
306
      .as_ref(),
×
307
    )
×
308
    .split(popup_layout[1])[1]
×
309
}
×
310

311
pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool, light: bool) {
×
312
  if is_loading {
×
313
    let text = "\n\n Loading ...\n\n".to_owned();
×
314
    let text = Text::from(text);
×
315
    let text = text.patch_style(style_secondary(light));
×
316

×
317
    // Contains the text
×
318
    let paragraph = Paragraph::new(text)
×
319
      .style(style_secondary(light))
×
320
      .block(block);
×
321
    f.render_widget(paragraph, area);
×
322
  } else {
×
323
    f.render_widget(block, area)
×
324
  }
325
}
×
326

327
// using a macro to reuse code as generics will make handling lifetimes a PITA
328
#[macro_export]
329
macro_rules! draw_resource_tab {
330
  ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => {
331
    match $block {
332
      ActiveBlock::Describe => draw_describe_block(
333
        $f,
334
        $app,
335
        $area,
336
        title_with_dual_style(
337
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
338
          format!("{} | {} <esc> ", COPY_HINT, $title),
339
          $app.light_theme,
340
        ),
341
      ),
342
      ActiveBlock::Yaml => draw_yaml_block(
343
        $f,
344
        $app,
345
        $area,
346
        title_with_dual_style(
347
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
348
          format!("{} | {} <esc> ", COPY_HINT, $title),
349
          $app.light_theme,
350
        ),
351
      ),
352
      ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area),
353
      _ => $fn2($f, $app, $area),
354
    };
355
  };
356
}
357

358
pub struct ResourceTableProps<'a, T> {
359
  pub title: String,
360
  pub inline_help: String,
361
  pub resource: &'a mut StatefulTable<T>,
362
  pub table_headers: Vec<&'a str>,
363
  pub column_widths: Vec<Constraint>,
364
}
365
/// common for all resources
366
pub fn draw_describe_block(f: &mut Frame<'_>, app: &App, area: Rect, title: Line<'_>) {
×
367
  draw_yaml_block(f, app, area, title);
×
368
}
×
369

370
/// common for all resources
371
pub fn draw_yaml_block(f: &mut Frame<'_>, app: &App, area: Rect, title: Line<'_>) {
×
372
  let block = layout_block_top_border(title);
×
373

374
  let txt = &app.data.describe_out.get_txt();
×
375
  if !txt.is_empty() {
×
376
    let ss = get_syntax_set();
×
377
    let syntax = get_yaml_syntax_reference();
×
378
    let theme = if app.light_theme {
×
379
      &get_yaml_themes().light
×
380
    } else {
381
      &get_yaml_themes().dark
×
382
    };
383
    let mut h = syntect::easy::HighlightLines::new(syntax, theme);
×
384
    let lines: Vec<_> = syntect::util::LinesWithEndings::from(txt)
×
385
      .filter_map(|line| {
×
386
        match h.highlight_line(line, ss) {
×
387
          Ok(segments) => {
×
388
            let line_spans: Vec<_> = segments
×
389
              .into_iter()
×
390
              .filter_map(|segment| syntect_tui::into_span(segment).ok())
×
391
              .collect();
×
392
            Some(ratatui::text::Line::from(
×
393
              line_spans.into_iter().collect::<Vec<_>>(),
×
394
            ))
×
395
          }
396
          Err(_) => None, // Handle the error gracefully
×
397
        }
398
      })
×
399
      .collect();
×
400

401
    let paragraph = Paragraph::new(lines)
×
402
      .block(block)
×
403
      .wrap(Wrap { trim: false })
×
404
      .scroll((app.data.describe_out.offset, 0));
×
405
    f.render_widget(paragraph, area);
×
406
  } else {
×
407
    loading(f, block, area, app.is_loading, app.light_theme);
×
408
  }
×
409
}
×
410

411
/// Draw a kubernetes resource overview tab
412
pub fn draw_resource_block<'a, T: KubeResource<U>, F, U: Serialize>(
4✔
413
  f: &mut Frame<'_>,
4✔
414
  area: Rect,
4✔
415
  table_props: ResourceTableProps<'a, T>,
4✔
416
  row_cell_mapper: F,
4✔
417
  light_theme: bool,
4✔
418
  is_loading: bool,
4✔
419
  filter: Option<String>,
4✔
420
) where
4✔
421
  F: Fn(&T) -> Row<'a>,
4✔
422
{
423
  let title = title_with_dual_style(table_props.title, table_props.inline_help, light_theme);
4✔
424
  let block = layout_block_top_border(title);
4✔
425

426
  if !table_props.resource.items.is_empty() {
4✔
427
    let rows = table_props.resource.items.iter().filter_map(|c| {
10✔
428
      let mapper = row_cell_mapper(c);
10✔
429
      // return only rows that match filter if filter is set
430
      match filter.as_ref() {
10✔
431
        None => Some(mapper),
4✔
432
        Some(ft) if filter_by_name(ft, c) => Some(mapper),
6✔
433
        _ => None,
2✔
434
      }
435
    });
10✔
436

437
    let table = Table::new(rows, &table_props.column_widths)
4✔
438
      .header(table_header_style(table_props.table_headers, light_theme))
4✔
439
      .block(block)
4✔
440
      .row_highlight_style(style_highlight())
4✔
441
      .highlight_symbol(HIGHLIGHT);
4✔
442

443
    f.render_stateful_widget(table, area, &mut table_props.resource.state);
4✔
444
  } else {
×
445
    loading(f, block, area, is_loading, light_theme);
×
446
  }
×
447
}
4✔
448

449
pub fn filter_by_resource_name<T: KubeResource<U>, U: Serialize>(
×
450
  filter: Option<String>,
×
451
  res: &T,
×
452
  row_cell_mapper: Row<'static>,
×
453
) -> Option<Row<'static>> {
×
454
  match filter.as_ref() {
×
455
    None => Some(row_cell_mapper),
×
456
    Some(ft) if filter_by_name(ft, res) => Some(row_cell_mapper),
×
457
    _ => None,
×
458
  }
459
}
×
460

461
fn filter_by_name<T: KubeResource<U>, U: Serialize>(ft: &str, res: &T) -> bool {
6✔
462
  ft.is_empty()
6✔
463
    || glob_match(&ft.to_lowercase(), &res.get_name().to_lowercase())
6✔
464
    || res.get_name().to_lowercase().contains(&ft.to_lowercase())
4✔
465
}
6✔
466

467
pub fn get_cluster_wide_resource_title<S: AsRef<str>>(
2✔
468
  title: S,
2✔
469
  items_len: usize,
2✔
470
  suffix: S,
2✔
471
) -> String {
2✔
472
  format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref())
2✔
473
}
2✔
474

475
pub fn get_resource_title<S: AsRef<str>>(
3✔
476
  app: &App,
3✔
477
  title: S,
3✔
478
  suffix: S,
3✔
479
  items_len: usize,
3✔
480
) -> String {
3✔
481
  format!(
3✔
482
    " {} {}",
3✔
483
    title_with_ns(
3✔
484
      title.as_ref(),
3✔
485
      app
3✔
486
        .data
3✔
487
        .selected
3✔
488
        .ns
3✔
489
        .as_ref()
3✔
490
        .unwrap_or(&String::from("all")),
3✔
491
      items_len
3✔
492
    ),
493
    suffix.as_ref(),
3✔
494
  )
495
}
3✔
496

497
static DESCRIBE_ACTIVE: &str = "-> Describe ";
498
static YAML_ACTIVE: &str = "-> YAML ";
499

500
pub fn get_describe_active<'a>(block: ActiveBlock) -> &'a str {
×
501
  match block {
×
502
    ActiveBlock::Describe => DESCRIBE_ACTIVE,
×
503
    _ => YAML_ACTIVE,
×
504
  }
505
}
×
506

507
pub fn title_with_ns(title: &str, ns: &str, length: usize) -> String {
4✔
508
  format!("{} (ns: {}) [{}]", title, ns, length)
4✔
509
}
4✔
510

511
#[cfg(test)]
512
mod tests {
513
  use ratatui::{
514
    backend::TestBackend, buffer::Buffer, layout::Position, style::Modifier, widgets::Cell,
515
    Terminal,
516
  };
517

518
  use super::*;
519
  use crate::ui::utils::{COLOR_CYAN, COLOR_WHITE, COLOR_YELLOW};
520

521
  #[test]
522
  fn test_draw_resource_block() {
1✔
523
    let backend = TestBackend::new(100, 6);
1✔
524
    let mut terminal = Terminal::new(backend).unwrap();
1✔
525

526
    struct RenderTest {
527
      pub name: String,
528
      pub namespace: String,
529
      pub data: i32,
530
      pub age: String,
531
    }
532

533
    impl KubeResource<Option<String>> for RenderTest {
534
      fn get_name(&self) -> &String {
×
535
        &self.name
×
536
      }
×
537
      fn get_k8s_obj(&self) -> &Option<String> {
×
538
        &None
×
539
      }
×
540
    }
541
    terminal
1✔
542
      .draw(|f| {
1✔
543
        let size = f.area();
1✔
544
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
545
        resource.set_items(vec![
1✔
546
          RenderTest {
1✔
547
            name: "Test 1".into(),
1✔
548
            namespace: "Test ns".into(),
1✔
549
            age: "65h3m".into(),
1✔
550
            data: 5,
1✔
551
          },
1✔
552
          RenderTest {
1✔
553
            name: "Test long name that should be truncated from view".into(),
1✔
554
            namespace: "Test ns".into(),
1✔
555
            age: "65h3m".into(),
1✔
556
            data: 3,
1✔
557
          },
1✔
558
          RenderTest {
1✔
559
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
560
            namespace: "Test ns long value check that should be truncated".into(),
1✔
561
            age: "65h3m".into(),
1✔
562
            data: 6,
1✔
563
          },
1✔
564
        ]);
565
        draw_resource_block(
1✔
566
          f,
1✔
567
          size,
1✔
568
          ResourceTableProps {
1✔
569
            title: "Test".into(),
1✔
570
            inline_help: "-> yaml <y>".into(),
1✔
571
            resource: &mut resource,
1✔
572
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
573
            column_widths: vec![
1✔
574
              Constraint::Percentage(30),
1✔
575
              Constraint::Percentage(40),
1✔
576
              Constraint::Percentage(15),
1✔
577
              Constraint::Percentage(15),
1✔
578
            ],
1✔
579
          },
1✔
580
          |c| {
3✔
581
            Row::new(vec![
3✔
582
              Cell::from(c.namespace.to_owned()),
3✔
583
              Cell::from(c.name.to_owned()),
3✔
584
              Cell::from(c.data.to_string()),
3✔
585
              Cell::from(c.age.to_owned()),
3✔
586
            ])
587
            .style(style_primary(false))
3✔
588
          },
3✔
589
          false,
590
          false,
591
          None,
1✔
592
        );
593
      })
1✔
594
      .unwrap();
1✔
595

596
    let mut expected = Buffer::with_lines(vec![
1✔
597
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
598
        "   Namespace                     Name                                 Data           Age            ",
1✔
599
        "=> Test ns                       Test 1                               5              65h3m          ",
1✔
600
        "   Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
601
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
602
        "                                                                                                    ",
1✔
603
      ]);
604
    // set row styles
605
    // First row heading style
606
    for col in 0..=99 {
101✔
607
      match col {
100✔
608
        0..=3 => {
100✔
609
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
610
            Style::default()
4✔
611
              .fg(COLOR_YELLOW)
4✔
612
              .add_modifier(Modifier::BOLD),
4✔
613
          );
4✔
614
        }
4✔
615
        4..=14 => {
96✔
616
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
617
            Style::default()
11✔
618
              .fg(COLOR_WHITE)
11✔
619
              .add_modifier(Modifier::BOLD),
11✔
620
          );
11✔
621
        }
11✔
622
        _ => {}
85✔
623
      }
624
    }
625

626
    // Second row table header style
627
    for col in 0..=99 {
101✔
628
      expected
100✔
629
        .cell_mut(Position::new(col, 1))
100✔
630
        .unwrap()
100✔
631
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
632
    }
100✔
633
    // first table data row style
634
    for col in 0..=99 {
101✔
635
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
636
        Style::default()
100✔
637
          .fg(COLOR_CYAN)
100✔
638
          .add_modifier(Modifier::REVERSED),
100✔
639
      );
100✔
640
    }
100✔
641
    // remaining table data row style
642
    for row in 3..=4 {
3✔
643
      for col in 0..=99 {
202✔
644
        expected
200✔
645
          .cell_mut(Position::new(col, row))
200✔
646
          .unwrap()
200✔
647
          .set_style(Style::default().fg(COLOR_CYAN));
200✔
648
      }
200✔
649
    }
650

651
    terminal.backend().assert_buffer(&expected);
1✔
652
  }
1✔
653

654
  #[test]
655
  fn test_draw_resource_block_filter() {
1✔
656
    let backend = TestBackend::new(100, 6);
1✔
657
    let mut terminal = Terminal::new(backend).unwrap();
1✔
658

659
    struct RenderTest {
660
      pub name: String,
661
      pub namespace: String,
662
      pub data: i32,
663
      pub age: String,
664
    }
665
    impl KubeResource<Option<String>> for RenderTest {
666
      fn get_name(&self) -> &String {
6✔
667
        &self.name
6✔
668
      }
6✔
669
      fn get_k8s_obj(&self) -> &Option<String> {
×
670
        &None
×
671
      }
×
672
    }
673

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

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

759
    // Second row table header style
760
    for col in 0..=99 {
101✔
761
      expected
100✔
762
        .cell_mut(Position::new(col, 1))
100✔
763
        .unwrap()
100✔
764
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
765
    }
100✔
766
    // first table data row style
767
    for col in 0..=99 {
101✔
768
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
769
        Style::default()
100✔
770
          .fg(COLOR_CYAN)
100✔
771
          .add_modifier(Modifier::REVERSED),
100✔
772
      );
100✔
773
    }
100✔
774
    // remaining table data row style
775
    for row in 3..=3 {
2✔
776
      for col in 0..=99 {
101✔
777
        expected
100✔
778
          .cell_mut(Position::new(col, row))
100✔
779
          .unwrap()
100✔
780
          .set_style(Style::default().fg(COLOR_CYAN));
100✔
781
      }
100✔
782
    }
783

784
    terminal.backend().assert_buffer(&expected);
1✔
785
  }
1✔
786

787
  #[test]
788
  fn test_draw_resource_block_filter_glob() {
1✔
789
    let backend = TestBackend::new(100, 6);
1✔
790
    let mut terminal = Terminal::new(backend).unwrap();
1✔
791

792
    struct RenderTest {
793
      pub name: String,
794
      pub namespace: String,
795
      pub data: i32,
796
      pub age: String,
797
    }
798
    impl KubeResource<Option<String>> for RenderTest {
799
      fn get_name(&self) -> &String {
4✔
800
        &self.name
4✔
801
      }
4✔
802
      fn get_k8s_obj(&self) -> &Option<String> {
×
803
        &None
×
804
      }
×
805
    }
806

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

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

892
    // Second row table header style
893
    for col in 0..=99 {
101✔
894
      expected
100✔
895
        .cell_mut(Position::new(col, 1))
100✔
896
        .unwrap()
100✔
897
        .set_style(Style::default().fg(COLOR_WHITE));
100✔
898
    }
100✔
899
    // first table data row style
900
    for col in 0..=99 {
101✔
901
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
902
        Style::default()
100✔
903
          .fg(COLOR_CYAN)
100✔
904
          .add_modifier(Modifier::REVERSED),
100✔
905
      );
100✔
906
    }
100✔
907
    // remaining table data row style
908
    for row in 3..=3 {
2✔
909
      for col in 0..=99 {
101✔
910
        expected
100✔
911
          .cell_mut(Position::new(col, row))
100✔
912
          .unwrap()
100✔
913
          .set_style(Style::default().fg(COLOR_CYAN));
100✔
914
      }
100✔
915
    }
916

917
    terminal.backend().assert_buffer(&expected);
1✔
918
  }
1✔
919

920
  #[test]
921
  fn test_get_resource_title() {
1✔
922
    let app = App::default();
1✔
923
    assert_eq!(
1✔
924
      get_resource_title(&app, "Title", "-> hello", 5),
1✔
925
      " Title (ns: all) [5] -> hello"
926
    );
927
  }
1✔
928

929
  #[test]
930
  fn test_title_with_ns() {
1✔
931
    assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]");
1✔
932
  }
1✔
933

934
  #[test]
935
  fn test_get_cluster_wide_resource_title() {
1✔
936
    assert_eq!(
1✔
937
      get_cluster_wide_resource_title("Cluster Resource", 3, ""),
1✔
938
      " Cluster Resource [3] "
939
    );
940
    assert_eq!(
1✔
941
      get_cluster_wide_resource_title("Nodes", 10, "-> hello"),
1✔
942
      " Nodes [10] -> hello"
943
    );
944
  }
1✔
945
}
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

© 2025 Coveralls, Inc