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

kdash-rs / kdash / 24052360545

06 Apr 2026 09:34PM UTC coverage: 63.284% (+0.4%) from 62.923%
24052360545

push

github

deepu105
fix: FIx troubleshoot view

34 of 67 new or added lines in 2 files covered. (50.75%)

768 existing lines in 22 files now uncovered.

7158 of 11311 relevant lines covered (63.28%)

215.15 hits per line

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

66.38
/src/ui/utils.rs
1
use std::{borrow::Cow, collections::BTreeMap, io::Cursor, rc::Rc, sync::OnceLock};
2

3
use glob_match::glob_match;
4
use ratatui::{
5
  layout::{Constraint, Direction, Layout, Position, 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
  key_binding::DEFAULT_KEYBINDING,
17
  models::{KubeResource, StatefulTable},
18
  ActiveBlock, App,
19
};
20
use crate::event::Key;
21
use crate::ui::theme::override_color;
22
// Utils
23

24
#[derive(Clone, Debug, PartialEq, Eq)]
25
pub enum LinePart<'a> {
26
  Default(Cow<'a, str>),
27
  Help(Cow<'a, str>),
28
}
29

30
// Catppuccin Macchiato (dark)
31
pub const MACCHIATO_BASE: Color = Color::Rgb(36, 39, 58);
32
pub const MACCHIATO_BLUE: Color = Color::Rgb(138, 173, 244);
33
pub const MACCHIATO_GREEN: Color = Color::Rgb(166, 218, 149);
34
pub const MACCHIATO_RED: Color = Color::Rgb(237, 135, 150);
35
pub const MACCHIATO_YELLOW: Color = Color::Rgb(238, 212, 159);
36
pub const MACCHIATO_PEACH: Color = Color::Rgb(245, 169, 127);
37
pub const MACCHIATO_TEXT: Color = Color::Rgb(202, 211, 245);
38
pub const MACCHIATO_MAUVE: Color = Color::Rgb(198, 160, 246);
39
// Catppuccin Latte (light)
40
pub const LATTE_MAUVE: Color = Color::Rgb(136, 57, 239);
41
pub const LATTE_TEXT: Color = Color::Rgb(76, 79, 105);
42
pub const LATTE_BLUE: Color = Color::Rgb(30, 102, 245);
43
pub const LATTE_MAROON: Color = Color::Rgb(230, 69, 83);
44
pub const LATTE_GREEN: Color = Color::Rgb(64, 160, 43);
45
pub const LATTE_RED: Color = Color::Rgb(210, 15, 57);
46
pub const LATTE_PEACH: Color = Color::Rgb(254, 100, 11);
47
pub const LATTE_BASE: Color = Color::Rgb(239, 241, 245);
48
const CATPPUCCIN_MACCHIATO_THEME: &[u8] =
49
  include_bytes!("../../assets/themes/CatppuccinMacchiato.tmTheme");
50
const CATPPUCCIN_LATTE_THEME: &[u8] = include_bytes!("../../assets/themes/CatppuccinLatte.tmTheme");
51

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

96
fn get_syntax_set() -> &'static syntect::parsing::SyntaxSet {
×
97
  static SYNTAX_SET: OnceLock<syntect::parsing::SyntaxSet> = OnceLock::new();
98
  SYNTAX_SET.get_or_init(syntect::parsing::SyntaxSet::load_defaults_newlines)
×
UNCOV
99
}
×
100

101
fn get_yaml_syntax_reference() -> &'static syntect::parsing::SyntaxReference {
×
102
  static YAML_SYNTAX_REFERENCE: OnceLock<syntect::parsing::SyntaxReference> = OnceLock::new();
103
  YAML_SYNTAX_REFERENCE.get_or_init(|| {
×
104
    get_syntax_set()
×
105
      .find_syntax_by_extension("yaml")
×
UNCOV
106
      .unwrap()
×
107
      .clone()
×
UNCOV
108
  })
×
109
}
×
110

111
struct YamlThemes {
112
  dark: syntect::highlighting::Theme,
113
  light: syntect::highlighting::Theme,
114
}
115

116
fn get_yaml_themes() -> &'static YamlThemes {
×
117
  static YAML_THEMES: OnceLock<YamlThemes> = OnceLock::new();
118
  YAML_THEMES.get_or_init(|| {
×
119
    let dark = load_embedded_theme(CATPPUCCIN_MACCHIATO_THEME);
×
120
    let light = load_embedded_theme(CATPPUCCIN_LATTE_THEME);
×
UNCOV
121
    YamlThemes { dark, light }
×
UNCOV
122
  })
×
UNCOV
123
}
×
124

UNCOV
125
fn load_embedded_theme(theme_bytes: &[u8]) -> syntect::highlighting::Theme {
×
UNCOV
126
  syntect::highlighting::ThemeSet::load_from_reader(&mut Cursor::new(theme_bytes))
×
127
    .expect("embedded theme should load")
×
UNCOV
128
}
×
129

130
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
131
pub enum Styles {
132
  Text,
133
  Failure,
134
  Warning,
135
  Success,
136
  Primary,
137
  Secondary,
138
  Help,
139
  Background,
140
}
141

142
pub fn theme_styles(light: bool) -> BTreeMap<Styles, Style> {
97✔
143
  let mut styles = if light {
97✔
UNCOV
144
    BTreeMap::from([
×
UNCOV
145
      (Styles::Text, Style::default().fg(LATTE_TEXT)),
×
UNCOV
146
      (Styles::Failure, Style::default().fg(LATTE_RED)),
×
UNCOV
147
      (Styles::Warning, Style::default().fg(LATTE_PEACH)),
×
UNCOV
148
      (Styles::Success, Style::default().fg(LATTE_GREEN)),
×
UNCOV
149
      (Styles::Primary, Style::default().fg(LATTE_MAUVE)),
×
UNCOV
150
      (Styles::Secondary, Style::default().fg(LATTE_MAROON)),
×
UNCOV
151
      (Styles::Help, Style::default().fg(LATTE_BLUE)),
×
UNCOV
152
      (
×
UNCOV
153
        Styles::Background,
×
UNCOV
154
        Style::default().bg(LATTE_BASE).fg(LATTE_TEXT),
×
155
      ),
×
156
    ])
×
157
  } else {
158
    BTreeMap::from([
97✔
159
      (Styles::Text, Style::default().fg(MACCHIATO_TEXT)),
97✔
160
      (Styles::Failure, Style::default().fg(MACCHIATO_RED)),
97✔
161
      (Styles::Warning, Style::default().fg(MACCHIATO_PEACH)),
97✔
162
      (Styles::Success, Style::default().fg(MACCHIATO_GREEN)),
97✔
163
      (Styles::Primary, Style::default().fg(MACCHIATO_MAUVE)),
97✔
164
      (Styles::Secondary, Style::default().fg(MACCHIATO_YELLOW)),
97✔
165
      (Styles::Help, Style::default().fg(MACCHIATO_BLUE)),
97✔
166
      (
97✔
167
        Styles::Background,
97✔
168
        Style::default().bg(MACCHIATO_BASE).fg(MACCHIATO_TEXT),
97✔
169
      ),
97✔
170
    ])
97✔
171
  };
172

173
  apply_theme_override(&mut styles, Styles::Text, "text", false, light);
97✔
174
  apply_theme_override(&mut styles, Styles::Failure, "failure", false, light);
97✔
175
  apply_theme_override(&mut styles, Styles::Warning, "warning", false, light);
97✔
176
  apply_theme_override(&mut styles, Styles::Success, "success", false, light);
97✔
177
  apply_theme_override(&mut styles, Styles::Primary, "primary", false, light);
97✔
178
  apply_theme_override(&mut styles, Styles::Secondary, "secondary", false, light);
97✔
179
  apply_theme_override(&mut styles, Styles::Help, "help", false, light);
97✔
180
  apply_theme_override(&mut styles, Styles::Background, "background", true, light);
97✔
181

182
  styles
97✔
183
}
97✔
184

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

189
pub fn default_part<'a, S: Into<Cow<'a, str>>>(text: S) -> LinePart<'a> {
13✔
190
  LinePart::Default(text.into())
13✔
191
}
13✔
192

193
pub fn help_part<'a, S: Into<Cow<'a, str>>>(text: S) -> LinePart<'a> {
20✔
194
  LinePart::Help(text.into())
20✔
195
}
20✔
196

UNCOV
197
pub fn style_header(light: bool) -> Style {
×
UNCOV
198
  style_primary(light).add_modifier(Modifier::REVERSED)
×
UNCOV
199
}
×
200

201
pub fn style_bold() -> Style {
1✔
202
  Style::default().add_modifier(Modifier::BOLD)
1✔
203
}
1✔
204

205
pub fn style_text(light: bool) -> Style {
17✔
206
  *theme_styles(light).get(&Styles::Text).unwrap()
17✔
207
}
17✔
UNCOV
208
pub fn style_logo(light: bool) -> Style {
×
UNCOV
209
  style_primary(light)
×
UNCOV
210
}
×
211
pub fn style_failure(light: bool) -> Style {
1✔
212
  *theme_styles(light).get(&Styles::Failure).unwrap()
1✔
213
}
1✔
UNCOV
214
pub fn style_warning(light: bool) -> Style {
×
UNCOV
215
  *theme_styles(light).get(&Styles::Warning).unwrap()
×
216
}
×
217
pub fn style_caution(light: bool) -> Style {
×
218
  style_warning(light)
×
UNCOV
219
}
×
UNCOV
220
pub fn style_success(light: bool) -> Style {
×
UNCOV
221
  *theme_styles(light).get(&Styles::Success).unwrap()
×
222
}
×
223
pub fn style_primary(light: bool) -> Style {
49✔
224
  *theme_styles(light).get(&Styles::Primary).unwrap()
49✔
225
}
49✔
226
pub fn style_help(light: bool) -> Style {
20✔
227
  *theme_styles(light).get(&Styles::Help).unwrap()
20✔
228
}
20✔
229

230
pub fn style_secondary(light: bool) -> Style {
10✔
231
  *theme_styles(light).get(&Styles::Secondary).unwrap()
10✔
232
}
10✔
233

UNCOV
234
pub fn style_main_background(light: bool) -> Style {
×
UNCOV
235
  *theme_styles(light).get(&Styles::Background).unwrap()
×
UNCOV
236
}
×
237

238
pub fn style_highlight() -> Style {
6✔
239
  Style::default().add_modifier(Modifier::REVERSED)
6✔
240
}
6✔
241

242
fn line_part_style(part: &LinePart<'_>, light: bool, bold: bool) -> Style {
33✔
243
  let style = match part {
33✔
244
    LinePart::Default(_) => style_text(light),
13✔
245
    LinePart::Help(_) => style_help(light),
20✔
246
  };
247
  if bold {
33✔
248
    style.add_modifier(Modifier::BOLD)
10✔
249
  } else {
250
    style
23✔
251
  }
252
}
33✔
253

254
fn apply_theme_override(
776✔
255
  styles: &mut BTreeMap<Styles, Style>,
776✔
256
  slot: Styles,
776✔
257
  config_key: &str,
776✔
258
  background: bool,
776✔
259
  light: bool,
776✔
260
) {
776✔
261
  if let Some(color) = override_color(config_key, light) {
776✔
UNCOV
262
    let style = styles.entry(slot).or_default();
×
UNCOV
263
    *style = if background {
×
UNCOV
264
      style.bg(color)
×
265
    } else {
UNCOV
266
      style.fg(color)
×
267
    };
268
  }
776✔
269
}
776✔
270

271
pub fn mixed_line<'a, I>(parts: I, light: bool) -> Line<'a>
11✔
272
where
11✔
273
  I: IntoIterator<Item = LinePart<'a>>,
11✔
274
{
275
  styled_line(parts, light, false)
11✔
276
}
11✔
277

278
pub fn mixed_bold_line<'a, I>(parts: I, light: bool) -> Line<'a>
6✔
279
where
6✔
280
  I: IntoIterator<Item = LinePart<'a>>,
6✔
281
{
282
  styled_line(parts, light, true)
6✔
283
}
6✔
284

285
pub fn help_bold_line<'a, S: Into<Cow<'a, str>>>(text: S, light: bool) -> Line<'a> {
4✔
286
  mixed_bold_line([help_part(text)], light)
4✔
287
}
4✔
288

UNCOV
289
pub fn key_hints(keys: &[Key]) -> String {
×
UNCOV
290
  keys
×
UNCOV
291
    .iter()
×
UNCOV
292
    .map(ToString::to_string)
×
UNCOV
293
    .collect::<Vec<_>>()
×
UNCOV
294
    .join("/")
×
UNCOV
295
}
×
296

297
pub fn action_hint(action: &str, key: Key) -> String {
7✔
298
  format!("{} {}", action, key)
7✔
299
}
7✔
300

301
pub fn describe_and_yaml_hint() -> String {
1✔
302
  format!(
1✔
303
    "{} | {} ",
304
    action_hint("describe", DEFAULT_KEYBINDING.describe_resource.key),
1✔
305
    action_hint("yaml", DEFAULT_KEYBINDING.resource_yaml.key)
1✔
306
  )
307
}
1✔
308

309
pub fn describe_yaml_and_logs_hint() -> String {
1✔
310
  format!(
1✔
311
    "{} | {} ",
312
    describe_and_yaml_hint().trim_end(),
1✔
313
    action_hint("logs", DEFAULT_KEYBINDING.aggregate_logs.key)
1✔
314
  )
315
}
1✔
316

317
pub fn describe_yaml_logs_and_esc_hint() -> String {
×
318
  format!(
×
319
    "{} | back to menu {} ",
320
    describe_yaml_and_logs_hint().trim_end(),
×
321
    DEFAULT_KEYBINDING.esc.key
×
322
  )
323
}
×
324

325
pub fn describe_yaml_and_esc_hint() -> String {
×
326
  format!(
×
327
    "{} | back to menu {} ",
328
    describe_and_yaml_hint().trim_end(),
×
329
    DEFAULT_KEYBINDING.esc.key
×
330
  )
331
}
×
332

333
pub fn describe_yaml_decode_and_esc_hint() -> String {
×
334
  format!(
×
335
    "{} | {} | back to menu {} ",
UNCOV
336
    describe_and_yaml_hint().trim_end(),
×
UNCOV
337
    action_hint("decode", DEFAULT_KEYBINDING.decode_secret.key),
×
UNCOV
338
    DEFAULT_KEYBINDING.esc.key
×
339
  )
UNCOV
340
}
×
341

342
pub fn filter_cursor_position(area: Rect, prefix_width: usize, filter: &str) -> Position {
1✔
343
  Position {
1✔
344
    x: area.x
1✔
345
      + (prefix_width as u16 + 1 + filter.chars().count() as u16).min(area.width.saturating_sub(2)),
1✔
346
    y: area.y,
1✔
347
  }
1✔
348
}
1✔
349

350
fn styled_line<'a, I>(parts: I, light: bool, bold: bool) -> Line<'a>
17✔
351
where
17✔
352
  I: IntoIterator<Item = LinePart<'a>>,
17✔
353
{
354
  Line::from(
17✔
355
    parts
17✔
356
      .into_iter()
17✔
357
      .map(|part| {
33✔
358
        let style = line_part_style(&part, light, bold);
33✔
359
        match part {
33✔
360
          LinePart::Default(text) | LinePart::Help(text) => Span::styled(text, style),
33✔
361
        }
362
      })
33✔
363
      .collect::<Vec<_>>(),
17✔
364
  )
365
}
17✔
366

UNCOV
367
pub fn get_gauge_symbol(enhanced_graphics: bool) -> &'static str {
×
368
  if enhanced_graphics {
×
369
    symbols::line::THICK_HORIZONTAL
×
370
  } else {
UNCOV
371
    symbols::line::HORIZONTAL
×
372
  }
UNCOV
373
}
×
374

375
pub fn table_header_style(cells: Vec<&str>, light: bool) -> Row<'_> {
4✔
376
  Row::new(cells).style(style_text(light)).bottom_margin(0)
4✔
377
}
4✔
378

379
pub fn horizontal_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
×
380
  Layout::default()
×
381
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
UNCOV
382
      &constraints,
×
UNCOV
383
    ))
×
UNCOV
384
    .direction(Direction::Horizontal)
×
UNCOV
385
    .split(size)
×
UNCOV
386
}
×
387

UNCOV
388
pub fn horizontal_chunks_with_margin(
×
UNCOV
389
  constraints: Vec<Constraint>,
×
UNCOV
390
  size: Rect,
×
UNCOV
391
  margin: u16,
×
UNCOV
392
) -> Rc<[Rect]> {
×
UNCOV
393
  Layout::default()
×
UNCOV
394
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
×
395
      &constraints,
×
UNCOV
396
    ))
×
UNCOV
397
    .direction(Direction::Horizontal)
×
UNCOV
398
    .margin(margin)
×
UNCOV
399
    .split(size)
×
UNCOV
400
}
×
401

402
pub fn vertical_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
3✔
403
  Layout::default()
3✔
404
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
3✔
405
      &constraints,
3✔
406
    ))
3✔
407
    .direction(Direction::Vertical)
3✔
408
    .split(size)
3✔
409
}
3✔
410

411
pub fn vertical_chunks_with_margin(
1✔
412
  constraints: Vec<Constraint>,
1✔
413
  size: Rect,
1✔
414
  margin: u16,
1✔
415
) -> Rc<[Rect]> {
1✔
416
  Layout::default()
1✔
417
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
1✔
418
      &constraints,
1✔
419
    ))
1✔
420
    .direction(Direction::Vertical)
1✔
421
    .margin(margin)
1✔
422
    .split(size)
1✔
423
}
1✔
424

425
pub fn layout_block(title: Span<'_>) -> Block<'_> {
1✔
426
  Block::default().borders(Borders::ALL).title(title)
1✔
427
}
1✔
428

429
pub fn layout_block_default(title: &str) -> Block<'_> {
1✔
430
  layout_block(title_style(title))
1✔
431
}
1✔
432

UNCOV
433
pub fn layout_block_default_line(title: Line<'_>) -> Block<'_> {
×
UNCOV
434
  Block::default().borders(Borders::ALL).title(title)
×
UNCOV
435
}
×
436

437
pub fn layout_block_active_line(title: Line<'_>, light: bool) -> Block<'_> {
2✔
438
  Block::default()
2✔
439
    .borders(Borders::ALL)
2✔
440
    .title(title)
2✔
441
    .style(style_secondary(light))
2✔
442
}
2✔
443

UNCOV
444
pub fn layout_block_active_span(title: Line<'_>, light: bool) -> Block<'_> {
×
UNCOV
445
  layout_block_active_line(title, light)
×
UNCOV
446
}
×
447

448
pub fn layout_block_top_border(title: Line<'_>) -> Block<'_> {
5✔
449
  Block::default().borders(Borders::TOP).title(title)
5✔
450
}
5✔
451

452
enum FilterDisplayState<'a> {
453
  Inactive,
454
  EditingEmpty,
455
  Value { filter: &'a str, active: bool },
456
}
457

458
fn filter_display_state(filter: &str, active: bool) -> FilterDisplayState<'_> {
3✔
459
  if active && filter.is_empty() {
3✔
460
    FilterDisplayState::EditingEmpty
×
461
  } else if !filter.is_empty() {
3✔
462
    FilterDisplayState::Value { filter, active }
1✔
463
  } else {
464
    FilterDisplayState::Inactive
2✔
465
  }
466
}
3✔
467

468
fn filter_display_parts(filter: &str, active: bool) -> Vec<LinePart<'_>> {
3✔
469
  let state = filter_display_state(filter, active);
3✔
470
  let inactive_text = action_hint("filter", DEFAULT_KEYBINDING.filter.key);
3✔
471
  let clear_suffix = format!(" | clear {} ", DEFAULT_KEYBINDING.esc.key);
3✔
472
  let edit_suffix = format!(" | edit {} ", DEFAULT_KEYBINDING.filter.key);
3✔
473

474
  match state {
3✔
475
    FilterDisplayState::Inactive => vec![help_part(inactive_text)],
2✔
476
    FilterDisplayState::EditingEmpty => {
477
      vec![help_part("[type to filter]"), help_part(clear_suffix)]
×
478
    }
479
    FilterDisplayState::Value {
480
      filter,
1✔
481
      active: true,
482
    } => vec![
1✔
483
      default_part(format!("[{}]", filter)),
1✔
484
      help_part(clear_suffix),
1✔
485
    ],
486
    FilterDisplayState::Value {
487
      filter,
×
488
      active: false,
489
    } => vec![
×
490
      default_part(format!("[{}]", filter)),
×
UNCOV
491
      help_part(edit_suffix),
×
492
    ],
493
  }
494
}
3✔
495

496
pub fn filter_status_parts(filter: &str, active: bool) -> Vec<LinePart<'_>> {
2✔
497
  filter_display_parts(filter, active)
2✔
498
}
2✔
499

500
pub fn filter_bar_title<'a>(filter: &'a str, active: bool, light: bool) -> Line<'a> {
1✔
501
  let mut parts = vec![help_part(" ")];
1✔
502
  parts.extend(filter_display_parts(filter, active));
1✔
503
  parts.push(help_part(" "));
1✔
504
  mixed_line(parts, light)
1✔
505
}
1✔
506

507
pub fn title_with_dual_style<'a>(part_1: String, part_2: Line<'a>, light: bool) -> Line<'a> {
4✔
508
  let mut spans = vec![Span::styled(
4✔
509
    part_1,
4✔
510
    style_secondary(light).add_modifier(Modifier::BOLD),
4✔
511
  )];
512
  spans.extend(part_2.spans);
4✔
513
  Line::from(spans)
4✔
514
}
4✔
515

516
pub fn copy_and_escape_title_line<'a, S: Into<Cow<'a, str>>>(target: S, light: bool) -> Line<'a> {
×
517
  mixed_bold_line(
×
518
    [
×
519
      help_part(format!(
×
UNCOV
520
        "{} | ",
×
521
        action_hint("copy", DEFAULT_KEYBINDING.copy_to_clipboard.key)
×
UNCOV
522
      )),
×
UNCOV
523
      help_part(target),
×
UNCOV
524
      help_part(format!(" {} ", DEFAULT_KEYBINDING.esc.key)),
×
UNCOV
525
    ],
×
UNCOV
526
    light,
×
527
  )
UNCOV
528
}
×
529

530
pub fn split_hint_suffix(text: &str) -> (&str, Option<&str>) {
11✔
531
  if let Some(pos) = text.rfind(" <") {
11✔
532
    (&text[..pos], Some(&text[(pos + 1)..]))
11✔
533
  } else {
UNCOV
534
    (text, None)
×
535
  }
536
}
11✔
537

538
/// helper function to create a centered rect using up
539
/// certain percentage of the available rect `r`
UNCOV
540
pub fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
×
541
  let Rect {
UNCOV
542
    width: grid_width,
×
UNCOV
543
    height: grid_height,
×
544
    ..
UNCOV
545
  } = r;
×
UNCOV
546
  let outer_height = (grid_height / 2).saturating_sub(height / 2);
×
547

UNCOV
548
  let popup_layout = Layout::default()
×
UNCOV
549
    .direction(Direction::Vertical)
×
UNCOV
550
    .constraints(
×
UNCOV
551
      [
×
UNCOV
552
        Constraint::Length(outer_height),
×
UNCOV
553
        Constraint::Length(height),
×
UNCOV
554
        Constraint::Length(outer_height),
×
UNCOV
555
      ]
×
UNCOV
556
      .as_ref(),
×
UNCOV
557
    )
×
UNCOV
558
    .split(r);
×
559

UNCOV
560
  let outer_width = (grid_width / 2).saturating_sub(width / 2);
×
561

UNCOV
562
  Layout::default()
×
UNCOV
563
    .direction(Direction::Horizontal)
×
UNCOV
564
    .constraints(
×
565
      [
×
566
        Constraint::Length(outer_width),
×
567
        Constraint::Length(width),
×
UNCOV
568
        Constraint::Length(outer_width),
×
UNCOV
569
      ]
×
570
      .as_ref(),
×
571
    )
×
UNCOV
572
    .split(popup_layout[1])[1]
×
573
}
×
574

UNCOV
575
pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool, light: bool) {
×
576
  if is_loading {
×
577
    let text = "\n\n Loading ...\n\n".to_owned();
×
UNCOV
578
    let text = Text::from(text);
×
579
    let text = text.patch_style(style_secondary(light));
×
580

×
581
    // Contains the text
×
582
    let paragraph = Paragraph::new(text)
×
UNCOV
583
      .style(style_secondary(light))
×
584
      .block(block);
×
UNCOV
585
    f.render_widget(paragraph, area);
×
586
  } else {
×
587
    f.render_widget(block, area)
×
588
  }
589
}
×
590

591
// using a macro to reuse code as generics will make handling lifetimes a PITA
592
#[macro_export]
593
macro_rules! draw_resource_tab {
594
  ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => {
595
    match $block {
596
      ActiveBlock::Describe => draw_describe_block(
597
        $f,
598
        $app,
599
        $area,
600
        title_with_dual_style(
601
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
602
          $crate::ui::utils::copy_and_escape_title_line($title, $app.light_theme),
603
          $app.light_theme,
604
        ),
605
      ),
606
      ActiveBlock::Yaml => draw_yaml_block(
607
        $f,
608
        $app,
609
        $area,
610
        title_with_dual_style(
611
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
612
          $crate::ui::utils::copy_and_escape_title_line($title, $app.light_theme),
613
          $app.light_theme,
614
        ),
615
      ),
616
      ActiveBlock::Pods => $crate::app::pods::draw_block_as_sub($f, $app, $area),
617
      ActiveBlock::Containers => $crate::app::pods::draw_containers_block($f, $app, $area),
618
      ActiveBlock::Logs => $crate::app::pods::draw_logs_block($f, $app, $area),
619
      ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area),
620
      _ => $fn2($f, $app, $area),
621
    };
622
  };
623
}
624

625
pub struct ResourceTableProps<'a, T> {
626
  pub title: String,
627
  pub inline_help: Line<'a>,
628
  pub resource: &'a mut StatefulTable<T>,
629
  pub table_headers: Vec<&'a str>,
630
  pub column_widths: Vec<Constraint>,
631
}
632
/// common for all resources
UNCOV
633
pub fn draw_describe_block(f: &mut Frame<'_>, app: &mut App, area: Rect, title: Line<'_>) {
×
UNCOV
634
  draw_yaml_block(f, app, area, title);
×
UNCOV
635
}
×
636

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

UNCOV
641
  let txt = app.data.describe_out.get_txt();
×
UNCOV
642
  if !txt.is_empty() {
×
643
    // Re-highlight only when the cache is empty or the theme changed.
UNCOV
644
    if app.data.describe_out.highlighted_lines.is_empty()
×
UNCOV
645
      || app.data.describe_out.highlight_light_theme != app.light_theme
×
646
    {
UNCOV
647
      let ss = get_syntax_set();
×
UNCOV
648
      let syntax = get_yaml_syntax_reference();
×
UNCOV
649
      let theme = if app.light_theme {
×
UNCOV
650
        &get_yaml_themes().light
×
651
      } else {
UNCOV
652
        &get_yaml_themes().dark
×
653
      };
UNCOV
654
      let mut h = syntect::easy::HighlightLines::new(syntax, theme);
×
UNCOV
655
      let lines: Vec<_> = syntect::util::LinesWithEndings::from(txt)
×
UNCOV
656
        .filter_map(|line| match h.highlight_line(line, ss) {
×
UNCOV
657
          Ok(segments) => {
×
UNCOV
658
            let line_spans: Vec<_> = segments
×
UNCOV
659
              .into_iter()
×
UNCOV
660
              .filter_map(syntect_to_ratatui_span_owned)
×
661
              .collect();
×
UNCOV
662
            Some(ratatui::text::Line::from(line_spans))
×
663
          }
UNCOV
664
          Err(_) => None,
×
UNCOV
665
        })
×
UNCOV
666
        .collect();
×
UNCOV
667
      app.data.describe_out.highlighted_lines = lines;
×
UNCOV
668
      app.data.describe_out.highlight_light_theme = app.light_theme;
×
UNCOV
669
    }
×
670

UNCOV
671
    let paragraph = Paragraph::new(app.data.describe_out.highlighted_lines.clone())
×
UNCOV
672
      .block(block)
×
UNCOV
673
      .wrap(Wrap { trim: false })
×
674
      .scroll((
×
675
        app.data.describe_out.offset.min(u16::MAX as usize) as u16,
×
676
        0,
×
UNCOV
677
      ));
×
UNCOV
678
    f.render_widget(paragraph, area);
×
679
  } else {
×
680
    loading(f, block, area, app.is_loading(), app.light_theme);
×
681
  }
×
682
}
×
683

684
fn draw_resource_table<'a, T: KubeResource<U>, F, U: Serialize>(
4✔
685
  f: &mut Frame<'_>,
4✔
686
  area: Rect,
4✔
687
  table_props: ResourceTableProps<'a, T>,
4✔
688
  row_cell_mapper: F,
4✔
689
  light_theme: bool,
4✔
690
  is_loading: bool,
4✔
691
  block: Block<'a>,
4✔
692
) where
4✔
693
  F: Fn(&T) -> Row<'a>,
4✔
694
{
695
  if !table_props.resource.items.is_empty() {
4✔
696
    let filter = table_props.resource.filter.to_lowercase();
4✔
697
    let has_filter = !filter.is_empty();
4✔
698
    let mut filtered_indices: Vec<usize> = Vec::new();
4✔
699
    let rows: Vec<Row<'a>> = table_props
4✔
700
      .resource
4✔
701
      .items
4✔
702
      .iter()
4✔
703
      .enumerate()
4✔
704
      .filter_map(|(idx, c)| {
10✔
705
        let mapper = row_cell_mapper(c);
10✔
706
        if filter.is_empty() || filter_by_name(&filter, c) {
10✔
707
          Some((idx, mapper))
8✔
708
        } else {
709
          None
2✔
710
        }
711
      })
10✔
712
      .map(|(idx, row)| {
8✔
713
        if has_filter {
8✔
714
          filtered_indices.push(idx);
4✔
715
        }
4✔
716
        row
8✔
717
      })
8✔
718
      .collect();
4✔
719

720
    if has_filter {
4✔
721
      let max = filtered_indices.len().saturating_sub(1);
2✔
722
      if let Some(sel) = table_props.resource.state.selected() {
2✔
723
        if sel > max {
2✔
UNCOV
724
          table_props.resource.state.select(Some(max));
×
725
        }
2✔
UNCOV
726
      }
×
727
    }
2✔
728
    table_props.resource.filtered_indices = filtered_indices;
4✔
729

730
    let table = Table::new(rows, &table_props.column_widths)
4✔
731
      .header(table_header_style(table_props.table_headers, light_theme))
4✔
732
      .block(block)
4✔
733
      .row_highlight_style(style_highlight())
4✔
734
      .highlight_symbol(HIGHLIGHT);
4✔
735

736
    f.render_stateful_widget(table, area, &mut table_props.resource.state);
4✔
UNCOV
737
  } else {
×
UNCOV
738
    loading(f, block, area, is_loading, light_theme);
×
UNCOV
739
  }
×
740
}
4✔
741

742
/// Draw a kubernetes resource overview tab
743
pub fn draw_resource_block<'a, T: KubeResource<U>, F, U: Serialize>(
4✔
744
  f: &mut Frame<'_>,
4✔
745
  area: Rect,
4✔
746
  table_props: ResourceTableProps<'a, T>,
4✔
747
  row_cell_mapper: F,
4✔
748
  light_theme: bool,
4✔
749
  is_loading: bool,
4✔
750
) where
4✔
751
  F: Fn(&T) -> Row<'a>,
4✔
752
{
753
  let ResourceTableProps {
754
    title,
4✔
755
    inline_help,
4✔
756
    resource,
4✔
757
    table_headers,
4✔
758
    column_widths,
4✔
759
  } = table_props;
4✔
760
  let title = title_with_dual_style(title, inline_help, light_theme);
4✔
761
  let block = layout_block_top_border(title);
4✔
762
  draw_resource_table(
4✔
763
    f,
4✔
764
    area,
4✔
765
    ResourceTableProps {
4✔
766
      title: String::new(),
4✔
767
      inline_help: Line::default(),
4✔
768
      resource,
4✔
769
      table_headers,
4✔
770
      column_widths,
4✔
771
    },
4✔
772
    row_cell_mapper,
4✔
773
    light_theme,
4✔
774
    is_loading,
4✔
775
    block,
4✔
776
  );
777
}
4✔
778

NEW
779
pub fn draw_route_resource_block<'a, T: KubeResource<U>, F, U: Serialize>(
×
NEW
780
  f: &mut Frame<'_>,
×
NEW
781
  area: Rect,
×
NEW
782
  table_props: ResourceTableProps<'a, T>,
×
NEW
783
  row_cell_mapper: F,
×
NEW
784
  light_theme: bool,
×
NEW
785
  is_loading: bool,
×
NEW
786
) where
×
NEW
787
  F: Fn(&T) -> Row<'a>,
×
788
{
789
  let ResourceTableProps {
NEW
790
    title,
×
NEW
791
    inline_help,
×
NEW
792
    resource,
×
NEW
793
    table_headers,
×
NEW
794
    column_widths,
×
NEW
795
  } = table_props;
×
NEW
796
  let title = title_with_dual_style(title, inline_help, light_theme);
×
NEW
797
  let block = layout_block_active_span(title, light_theme);
×
NEW
798
  draw_resource_table(
×
NEW
799
    f,
×
NEW
800
    area,
×
NEW
801
    ResourceTableProps {
×
NEW
802
      title: String::new(),
×
NEW
803
      inline_help: Line::default(),
×
NEW
804
      resource,
×
NEW
805
      table_headers,
×
NEW
806
      column_widths,
×
NEW
807
    },
×
NEW
808
    row_cell_mapper,
×
NEW
809
    light_theme,
×
NEW
810
    is_loading,
×
NEW
811
    block,
×
812
  );
NEW
813
}
×
814

UNCOV
815
pub fn filter_by_resource_name<T: KubeResource<U>, U: Serialize>(
×
UNCOV
816
  filter: &str,
×
UNCOV
817
  res: &T,
×
UNCOV
818
  row_cell_mapper: Row<'static>,
×
UNCOV
819
) -> Option<Row<'static>> {
×
UNCOV
820
  if filter.is_empty() || filter_by_name(filter, res) {
×
UNCOV
821
    Some(row_cell_mapper)
×
822
  } else {
UNCOV
823
    None
×
824
  }
UNCOV
825
}
×
826

827
pub fn text_matches_filter(filter: &str, value: &str) -> bool {
187✔
828
  let filter = filter.to_lowercase();
187✔
829
  let value = value.to_lowercase();
187✔
830
  filter.is_empty() || glob_match(&filter, &value) || value.contains(&filter)
187✔
831
}
187✔
832

833
fn filter_by_name<T: KubeResource<U>, U: Serialize>(ft: &str, res: &T) -> bool {
6✔
834
  text_matches_filter(ft, res.get_name())
6✔
835
}
6✔
836

837
pub fn get_cluster_wide_resource_title<S: AsRef<str>>(
2✔
838
  title: S,
2✔
839
  items_len: usize,
2✔
840
  suffix: S,
2✔
841
) -> String {
2✔
842
  format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref())
2✔
843
}
2✔
844

845
pub fn get_resource_title<S: AsRef<str>>(
5✔
846
  app: &App,
5✔
847
  title: S,
5✔
848
  suffix: S,
5✔
849
  items_len: usize,
5✔
850
) -> String {
5✔
851
  format!(
5✔
852
    " {} {}",
853
    title_with_ns(
5✔
854
      title.as_ref(),
5✔
855
      app
5✔
856
        .data
5✔
857
        .selected
5✔
858
        .ns
5✔
859
        .as_ref()
5✔
860
        .unwrap_or(&String::from("all")),
5✔
861
      items_len
5✔
862
    ),
863
    suffix.as_ref(),
5✔
864
  )
865
}
5✔
866

867
static DESCRIBE_ACTIVE: &str = "-> Describe ";
868
static YAML_ACTIVE: &str = "-> YAML ";
869

UNCOV
870
pub fn get_describe_active<'a>(block: ActiveBlock) -> &'a str {
×
UNCOV
871
  match block {
×
UNCOV
872
    ActiveBlock::Describe => DESCRIBE_ACTIVE,
×
UNCOV
873
    _ => YAML_ACTIVE,
×
874
  }
UNCOV
875
}
×
876

877
pub fn title_with_ns(title: &str, ns: &str, length: usize) -> String {
6✔
878
  format!("{} (ns: {}) [{}]", title, ns, length)
6✔
879
}
6✔
880

881
#[cfg(test)]
882
mod tests {
883
  use ratatui::{
884
    backend::TestBackend, buffer::Buffer, layout::Position, style::Modifier, widgets::Cell,
885
    Terminal,
886
  };
887

888
  use super::*;
889
  use crate::ui::utils::{MACCHIATO_BLUE, MACCHIATO_MAUVE, MACCHIATO_TEXT, MACCHIATO_YELLOW};
890

891
  #[test]
892
  fn test_draw_resource_block() {
1✔
893
    let backend = TestBackend::new(100, 6);
1✔
894
    let mut terminal = Terminal::new(backend).unwrap();
1✔
895

896
    struct RenderTest {
897
      pub name: String,
898
      pub namespace: String,
899
      pub data: i32,
900
      pub age: String,
901
    }
902

903
    impl KubeResource<Option<String>> for RenderTest {
UNCOV
904
      fn get_name(&self) -> &String {
×
UNCOV
905
        &self.name
×
UNCOV
906
      }
×
UNCOV
907
      fn get_k8s_obj(&self) -> &Option<String> {
×
UNCOV
908
        &None
×
UNCOV
909
      }
×
910
    }
911
    terminal
1✔
912
      .draw(|f| {
1✔
913
        let size = f.area();
1✔
914
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
915
        resource.set_items(vec![
1✔
916
          RenderTest {
1✔
917
            name: "Test 1".into(),
1✔
918
            namespace: "Test ns".into(),
1✔
919
            age: "65h3m".into(),
1✔
920
            data: 5,
1✔
921
          },
1✔
922
          RenderTest {
1✔
923
            name: "Test long name that should be truncated from view".into(),
1✔
924
            namespace: "Test ns".into(),
1✔
925
            age: "65h3m".into(),
1✔
926
            data: 3,
1✔
927
          },
1✔
928
          RenderTest {
1✔
929
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
930
            namespace: "Test ns long value check that should be truncated".into(),
1✔
931
            age: "65h3m".into(),
1✔
932
            data: 6,
1✔
933
          },
1✔
934
        ]);
935
        draw_resource_block(
1✔
936
          f,
1✔
937
          size,
1✔
938
          ResourceTableProps {
1✔
939
            title: "Test".into(),
1✔
940
            inline_help: help_bold_line("-> yaml <y>", false),
1✔
941
            resource: &mut resource,
1✔
942
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
943
            column_widths: vec![
1✔
944
              Constraint::Percentage(30),
1✔
945
              Constraint::Percentage(40),
1✔
946
              Constraint::Percentage(15),
1✔
947
              Constraint::Percentage(15),
1✔
948
            ],
1✔
949
          },
1✔
950
          |c| {
3✔
951
            Row::new(vec![
3✔
952
              Cell::from(c.namespace.to_owned()),
3✔
953
              Cell::from(c.name.to_owned()),
3✔
954
              Cell::from(c.data.to_string()),
3✔
955
              Cell::from(c.age.to_owned()),
3✔
956
            ])
957
            .style(style_primary(false))
3✔
958
          },
3✔
959
          false,
960
          false,
961
        );
962
      })
1✔
963
      .unwrap();
1✔
964

965
    let mut expected = Buffer::with_lines(vec![
1✔
966
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
967
        "   Namespace                     Name                                 Data           Age            ",
1✔
968
        "=> Test ns                       Test 1                               5              65h3m          ",
1✔
969
        "   Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
970
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
971
        "                                                                                                    ",
1✔
972
      ]);
973
    // set row styles
974
    // First row heading style
975
    for col in 0..=99 {
100✔
976
      match col {
100✔
977
        0..=3 => {
100✔
978
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
979
            Style::default()
4✔
980
              .fg(MACCHIATO_YELLOW)
4✔
981
              .add_modifier(Modifier::BOLD),
4✔
982
          );
4✔
983
        }
4✔
984
        4..=14 => {
96✔
985
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
986
            Style::default()
11✔
987
              .fg(MACCHIATO_BLUE)
11✔
988
              .add_modifier(Modifier::BOLD),
11✔
989
          );
11✔
990
        }
11✔
991
        _ => {}
85✔
992
      }
993
    }
994

995
    // Second row table header style
996
    for col in 0..=99 {
100✔
997
      expected
100✔
998
        .cell_mut(Position::new(col, 1))
100✔
999
        .unwrap()
100✔
1000
        .set_style(Style::default().fg(MACCHIATO_TEXT));
100✔
1001
    }
100✔
1002
    // first table data row style
1003
    for col in 0..=99 {
100✔
1004
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1005
        Style::default()
100✔
1006
          .fg(MACCHIATO_MAUVE)
100✔
1007
          .add_modifier(Modifier::REVERSED),
100✔
1008
      );
100✔
1009
    }
100✔
1010
    // remaining table data row style
1011
    for row in 3..=4 {
2✔
1012
      for col in 0..=99 {
200✔
1013
        expected
200✔
1014
          .cell_mut(Position::new(col, row))
200✔
1015
          .unwrap()
200✔
1016
          .set_style(Style::default().fg(MACCHIATO_MAUVE));
200✔
1017
      }
200✔
1018
    }
1019

1020
    terminal.backend().assert_buffer(&expected);
1✔
1021
  }
1✔
1022

1023
  #[test]
1024
  fn test_draw_resource_block_filter() {
1✔
1025
    let backend = TestBackend::new(100, 6);
1✔
1026
    let mut terminal = Terminal::new(backend).unwrap();
1✔
1027

1028
    struct RenderTest {
1029
      pub name: String,
1030
      pub namespace: String,
1031
      pub data: i32,
1032
      pub age: String,
1033
    }
1034
    impl KubeResource<Option<String>> for RenderTest {
1035
      fn get_name(&self) -> &String {
3✔
1036
        &self.name
3✔
1037
      }
3✔
UNCOV
1038
      fn get_k8s_obj(&self) -> &Option<String> {
×
UNCOV
1039
        &None
×
UNCOV
1040
      }
×
1041
    }
1042

1043
    terminal
1✔
1044
      .draw(|f| {
1✔
1045
        let size = f.area();
1✔
1046
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
1047
        resource.set_items(vec![
1✔
1048
          RenderTest {
1✔
1049
            name: "Test 1".into(),
1✔
1050
            namespace: "Test ns".into(),
1✔
1051
            age: "65h3m".into(),
1✔
1052
            data: 5,
1✔
1053
          },
1✔
1054
          RenderTest {
1✔
1055
            name: "Test long name that should be truncated from view".into(),
1✔
1056
            namespace: "Test ns".into(),
1✔
1057
            age: "65h3m".into(),
1✔
1058
            data: 3,
1✔
1059
          },
1✔
1060
          RenderTest {
1✔
1061
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
1062
            namespace: "Test ns long value check that should be truncated".into(),
1✔
1063
            age: "65h3m".into(),
1✔
1064
            data: 6,
1✔
1065
          },
1✔
1066
        ]);
1067
        resource.filter = "truncated".to_string();
1✔
1068
        draw_resource_block(
1✔
1069
          f,
1✔
1070
          size,
1✔
1071
          ResourceTableProps {
1✔
1072
            title: "Test".into(),
1✔
1073
            inline_help: help_bold_line("-> yaml <y>", false),
1✔
1074
            resource: &mut resource,
1✔
1075
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
1076
            column_widths: vec![
1✔
1077
              Constraint::Percentage(30),
1✔
1078
              Constraint::Percentage(40),
1✔
1079
              Constraint::Percentage(15),
1✔
1080
              Constraint::Percentage(15),
1✔
1081
            ],
1✔
1082
          },
1✔
1083
          |c| {
3✔
1084
            Row::new(vec![
3✔
1085
              Cell::from(c.namespace.to_owned()),
3✔
1086
              Cell::from(c.name.to_owned()),
3✔
1087
              Cell::from(c.data.to_string()),
3✔
1088
              Cell::from(c.age.to_owned()),
3✔
1089
            ])
1090
            .style(style_primary(false))
3✔
1091
          },
3✔
1092
          false,
1093
          false,
1094
        );
1095
      })
1✔
1096
      .unwrap();
1✔
1097

1098
    let mut expected = Buffer::with_lines(vec![
1✔
1099
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
1100
        "   Namespace                     Name                                 Data           Age            ",
1✔
1101
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
1102
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
1103
        "                                                                                                    ",
1✔
1104
        "                                                                                                    ",
1✔
1105
      ]);
1106
    // set row styles
1107
    // First row heading style
1108
    for col in 0..=99 {
100✔
1109
      match col {
100✔
1110
        0..=3 => {
100✔
1111
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
1112
            Style::default()
4✔
1113
              .fg(MACCHIATO_YELLOW)
4✔
1114
              .add_modifier(Modifier::BOLD),
4✔
1115
          );
4✔
1116
        }
4✔
1117
        4..=14 => {
96✔
1118
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
1119
            Style::default()
11✔
1120
              .fg(MACCHIATO_BLUE)
11✔
1121
              .add_modifier(Modifier::BOLD),
11✔
1122
          );
11✔
1123
        }
11✔
1124
        _ => {}
85✔
1125
      }
1126
    }
1127

1128
    // Second row table header style
1129
    for col in 0..=99 {
100✔
1130
      expected
100✔
1131
        .cell_mut(Position::new(col, 1))
100✔
1132
        .unwrap()
100✔
1133
        .set_style(Style::default().fg(MACCHIATO_TEXT));
100✔
1134
    }
100✔
1135
    // first table data row style
1136
    for col in 0..=99 {
100✔
1137
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1138
        Style::default()
100✔
1139
          .fg(MACCHIATO_MAUVE)
100✔
1140
          .add_modifier(Modifier::REVERSED),
100✔
1141
      );
100✔
1142
    }
100✔
1143
    // remaining table data row style
1144
    for row in 3..=3 {
1✔
1145
      for col in 0..=99 {
100✔
1146
        expected
100✔
1147
          .cell_mut(Position::new(col, row))
100✔
1148
          .unwrap()
100✔
1149
          .set_style(Style::default().fg(MACCHIATO_MAUVE));
100✔
1150
      }
100✔
1151
    }
1152

1153
    terminal.backend().assert_buffer(&expected);
1✔
1154
  }
1✔
1155

1156
  #[test]
1157
  fn test_draw_resource_block_filter_glob() {
1✔
1158
    let backend = TestBackend::new(100, 6);
1✔
1159
    let mut terminal = Terminal::new(backend).unwrap();
1✔
1160

1161
    struct RenderTest {
1162
      pub name: String,
1163
      pub namespace: String,
1164
      pub data: i32,
1165
      pub age: String,
1166
    }
1167
    impl KubeResource<Option<String>> for RenderTest {
1168
      fn get_name(&self) -> &String {
3✔
1169
        &self.name
3✔
1170
      }
3✔
UNCOV
1171
      fn get_k8s_obj(&self) -> &Option<String> {
×
UNCOV
1172
        &None
×
UNCOV
1173
      }
×
1174
    }
1175

1176
    terminal
1✔
1177
      .draw(|f| {
1✔
1178
        let size = f.area();
1✔
1179
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
1180
        resource.set_items(vec![
1✔
1181
          RenderTest {
1✔
1182
            name: "Test 1".into(),
1✔
1183
            namespace: "Test ns".into(),
1✔
1184
            age: "65h3m".into(),
1✔
1185
            data: 5,
1✔
1186
          },
1✔
1187
          RenderTest {
1✔
1188
            name: "Test long name that should be truncated from view".into(),
1✔
1189
            namespace: "Test ns".into(),
1✔
1190
            age: "65h3m".into(),
1✔
1191
            data: 3,
1✔
1192
          },
1✔
1193
          RenderTest {
1✔
1194
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
1195
            namespace: "Test ns long value check that should be truncated".into(),
1✔
1196
            age: "65h3m".into(),
1✔
1197
            data: 6,
1✔
1198
          },
1✔
1199
        ]);
1200
        resource.filter = "*long*truncated*".to_string();
1✔
1201
        draw_resource_block(
1✔
1202
          f,
1✔
1203
          size,
1✔
1204
          ResourceTableProps {
1✔
1205
            title: "Test".into(),
1✔
1206
            inline_help: help_bold_line("-> yaml <y>", false),
1✔
1207
            resource: &mut resource,
1✔
1208
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
1209
            column_widths: vec![
1✔
1210
              Constraint::Percentage(30),
1✔
1211
              Constraint::Percentage(40),
1✔
1212
              Constraint::Percentage(15),
1✔
1213
              Constraint::Percentage(15),
1✔
1214
            ],
1✔
1215
          },
1✔
1216
          |c| {
3✔
1217
            Row::new(vec![
3✔
1218
              Cell::from(c.namespace.to_owned()),
3✔
1219
              Cell::from(c.name.to_owned()),
3✔
1220
              Cell::from(c.data.to_string()),
3✔
1221
              Cell::from(c.age.to_owned()),
3✔
1222
            ])
1223
            .style(style_primary(false))
3✔
1224
          },
3✔
1225
          false,
1226
          false,
1227
        );
1228
      })
1✔
1229
      .unwrap();
1✔
1230

1231
    let mut expected = Buffer::with_lines(vec![
1✔
1232
        "Test-> yaml <y>─────────────────────────────────────────────────────────────────────────────────────",
1233
        "   Namespace                     Name                                 Data           Age            ",
1✔
1234
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
1235
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
1236
        "                                                                                                    ",
1✔
1237
        "                                                                                                    ",
1✔
1238
      ]);
1239
    // set row styles
1240
    // First row heading style
1241
    for col in 0..=99 {
100✔
1242
      match col {
100✔
1243
        0..=3 => {
100✔
1244
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
1245
            Style::default()
4✔
1246
              .fg(MACCHIATO_YELLOW)
4✔
1247
              .add_modifier(Modifier::BOLD),
4✔
1248
          );
4✔
1249
        }
4✔
1250
        4..=14 => {
96✔
1251
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
1252
            Style::default()
11✔
1253
              .fg(MACCHIATO_BLUE)
11✔
1254
              .add_modifier(Modifier::BOLD),
11✔
1255
          );
11✔
1256
        }
11✔
1257
        _ => {}
85✔
1258
      }
1259
    }
1260

1261
    // Second row table header style
1262
    for col in 0..=99 {
100✔
1263
      expected
100✔
1264
        .cell_mut(Position::new(col, 1))
100✔
1265
        .unwrap()
100✔
1266
        .set_style(Style::default().fg(MACCHIATO_TEXT));
100✔
1267
    }
100✔
1268
    // first table data row style
1269
    for col in 0..=99 {
100✔
1270
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1271
        Style::default()
100✔
1272
          .fg(MACCHIATO_MAUVE)
100✔
1273
          .add_modifier(Modifier::REVERSED),
100✔
1274
      );
100✔
1275
    }
100✔
1276
    // remaining table data row style
1277
    for row in 3..=3 {
1✔
1278
      for col in 0..=99 {
100✔
1279
        expected
100✔
1280
          .cell_mut(Position::new(col, row))
100✔
1281
          .unwrap()
100✔
1282
          .set_style(Style::default().fg(MACCHIATO_MAUVE));
100✔
1283
      }
100✔
1284
    }
1285

1286
    terminal.backend().assert_buffer(&expected);
1✔
1287
  }
1✔
1288

1289
  #[test]
1290
  fn test_get_resource_title() {
1✔
1291
    let app = App::default();
1✔
1292
    assert_eq!(
1✔
1293
      get_resource_title(&app, "Title", "-> hello", 5),
1✔
1294
      " Title (ns: all) [5] -> hello"
1295
    );
1296
  }
1✔
1297

1298
  #[test]
1299
  fn test_title_with_ns() {
1✔
1300
    assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]");
1✔
1301
  }
1✔
1302

1303
  #[test]
1304
  fn test_get_cluster_wide_resource_title() {
1✔
1305
    assert_eq!(
1✔
1306
      get_cluster_wide_resource_title("Cluster Resource", 3, ""),
1✔
1307
      " Cluster Resource [3] "
1308
    );
1309
    assert_eq!(
1✔
1310
      get_cluster_wide_resource_title("Nodes", 10, "-> hello"),
1✔
1311
      " Nodes [10] -> hello"
1312
    );
1313
  }
1✔
1314
}
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