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

kdash-rs / kdash / 24897180135

24 Apr 2026 03:19PM UTC coverage: 73.276% (+0.1%) from 73.15%
24897180135

Pull #515

github

web-flow
Merge 042cab6d9 into 6a1cbe609
Pull Request #515: feat(ui): more efficient redraw

181 of 252 new or added lines in 3 files covered. (71.83%)

5 existing lines in 2 files now uncovered.

10340 of 14111 relevant lines covered (73.28%)

148.91 hits per line

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

79.37
/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

13
use super::HIGHLIGHT;
14
use crate::app::{
15
  key_binding::DEFAULT_KEYBINDING,
16
  models::{Named, StatefulTable},
17
  ActiveBlock, App,
18
};
19
use crate::event::Key;
20
use crate::ui::theme::override_color;
21
// Viewport width thresholds for responsive column display
22
pub const COMPACT_WIDTH_THRESHOLD: u16 = 120;
23
pub const WIDE_WIDTH_THRESHOLD: u16 = 180;
24

25
/// Which responsive tier the current view is in.
26
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
27
pub enum ViewTier {
28
  Compact,
29
  Standard,
30
  Wide,
31
}
32

33
impl ViewTier {
34
  pub fn from_width(area_width: u16, force_wide: bool) -> Self {
5✔
35
    if force_wide || area_width >= WIDE_WIDTH_THRESHOLD {
5✔
36
      Self::Wide
×
37
    } else if area_width >= COMPACT_WIDTH_THRESHOLD {
5✔
38
      Self::Standard
1✔
39
    } else {
40
      Self::Compact
4✔
41
    }
42
  }
5✔
43
}
44

45
/// Declarative column definition with per-tier width percentages.
46
/// A `None` width means the column is hidden at that tier.
47
pub struct ColumnDef {
48
  pub label: &'static str,
49
  pub compact: Option<u16>,
50
  pub standard: Option<u16>,
51
  pub wide: Option<u16>,
52
}
53

54
impl ColumnDef {
55
  /// Column visible at all tiers with different widths.
56
  pub const fn all(label: &'static str, compact: u16, standard: u16, wide: u16) -> Self {
×
57
    Self {
×
58
      label,
×
59
      compact: Some(compact),
×
60
      standard: Some(standard),
×
61
      wide: Some(wide),
×
62
    }
×
63
  }
×
64

65
  /// Column visible only at Standard and Wide tiers.
66
  pub const fn standard(label: &'static str, standard: u16, wide: u16) -> Self {
×
67
    Self {
×
68
      label,
×
69
      compact: None,
×
70
      standard: Some(standard),
×
71
      wide: Some(wide),
×
72
    }
×
73
  }
×
74

75
  /// Column visible only at Wide tier.
76
  pub const fn wide(label: &'static str, wide: u16) -> Self {
×
77
    Self {
×
78
      label,
×
79
      compact: None,
×
80
      standard: None,
×
81
      wide: Some(wide),
×
82
    }
×
83
  }
×
84
}
85

86
/// Given column definitions and a view tier, return the visible headers and widths.
87
pub fn responsive_columns(columns: &[ColumnDef], tier: ViewTier) -> (Vec<&str>, Vec<Constraint>) {
7✔
88
  columns
7✔
89
    .iter()
7✔
90
    .filter_map(|col| {
55✔
91
      let w = match tier {
55✔
92
        ViewTier::Wide => col.wide,
×
93
        ViewTier::Standard => col.standard,
8✔
94
        ViewTier::Compact => col.compact,
47✔
95
      };
96
      w.map(|w| (col.label, Constraint::Percentage(w)))
55✔
97
    })
55✔
98
    .unzip()
7✔
99
}
7✔
100

101
// Utils
102

103
#[derive(Clone, Debug, PartialEq, Eq)]
104
pub enum LinePart<'a> {
105
  Default(Cow<'a, str>),
106
  Help(Cow<'a, str>),
107
}
108

109
// Catppuccin Macchiato (dark)
110
pub const MACCHIATO_BASE: Color = Color::Rgb(36, 39, 58);
111
pub const MACCHIATO_BLUE: Color = Color::Rgb(138, 173, 244);
112
pub const MACCHIATO_GREEN: Color = Color::Rgb(166, 218, 149);
113
pub const MACCHIATO_RED: Color = Color::Rgb(237, 135, 150);
114
pub const MACCHIATO_YELLOW: Color = Color::Rgb(238, 212, 159);
115
pub const MACCHIATO_PEACH: Color = Color::Rgb(245, 169, 127);
116
pub const MACCHIATO_TEXT: Color = Color::Rgb(202, 211, 245);
117
pub const MACCHIATO_MAUVE: Color = Color::Rgb(198, 160, 246);
118
// Catppuccin Latte (light)
119
pub const LATTE_MAUVE: Color = Color::Rgb(136, 57, 239);
120
pub const LATTE_TEXT: Color = Color::Rgb(76, 79, 105);
121
pub const LATTE_BLUE: Color = Color::Rgb(30, 102, 245);
122
pub const LATTE_MAROON: Color = Color::Rgb(230, 69, 83);
123
pub const LATTE_GREEN: Color = Color::Rgb(64, 160, 43);
124
pub const LATTE_RED: Color = Color::Rgb(210, 15, 57);
125
pub const LATTE_PEACH: Color = Color::Rgb(254, 100, 11);
126
pub const LATTE_BASE: Color = Color::Rgb(239, 241, 245);
127
const CATPPUCCIN_MACCHIATO_THEME: &[u8] =
128
  include_bytes!("../../assets/themes/CatppuccinMacchiato.tmTheme");
129
const CATPPUCCIN_LATTE_THEME: &[u8] = include_bytes!("../../assets/themes/CatppuccinLatte.tmTheme");
130

131
/// Convert a syntect highlight segment into an owned ratatui Span.
132
fn syntect_to_ratatui_span_owned(
×
133
  (style, content): (syntect::highlighting::Style, &str),
×
134
) -> Option<Span<'static>> {
×
135
  use syntect::highlighting::FontStyle;
136
  let fg = if style.foreground.a > 0 {
×
137
    Some(Color::Rgb(
×
138
      style.foreground.r,
×
139
      style.foreground.g,
×
140
      style.foreground.b,
×
141
    ))
×
142
  } else {
143
    None
×
144
  };
145
  let bg = if style.background.a > 0 {
×
146
    Some(Color::Rgb(
×
147
      style.background.r,
×
148
      style.background.g,
×
149
      style.background.b,
×
150
    ))
×
151
  } else {
152
    None
×
153
  };
154
  let modifier = {
×
155
    let fs = style.font_style;
×
156
    let mut m = Modifier::empty();
×
157
    if fs.contains(FontStyle::BOLD) {
×
158
      m |= Modifier::BOLD;
×
159
    }
×
160
    if fs.contains(FontStyle::ITALIC) {
×
161
      m |= Modifier::ITALIC;
×
162
    }
×
163
    if fs.contains(FontStyle::UNDERLINE) {
×
164
      m |= Modifier::UNDERLINED;
×
165
    }
×
166
    m
×
167
  };
168
  let ratatui_style = Style::default()
×
169
    .fg(fg.unwrap_or_default())
×
170
    .bg(bg.unwrap_or_default())
×
171
    .add_modifier(modifier);
×
172
  Some(Span::styled(content.to_owned(), ratatui_style))
×
173
}
×
174

175
fn get_syntax_set() -> &'static syntect::parsing::SyntaxSet {
×
176
  static SYNTAX_SET: OnceLock<syntect::parsing::SyntaxSet> = OnceLock::new();
177
  SYNTAX_SET.get_or_init(syntect::parsing::SyntaxSet::load_defaults_newlines)
×
178
}
×
179

180
fn get_yaml_syntax_reference() -> &'static syntect::parsing::SyntaxReference {
×
181
  static YAML_SYNTAX_REFERENCE: OnceLock<syntect::parsing::SyntaxReference> = OnceLock::new();
182
  YAML_SYNTAX_REFERENCE.get_or_init(|| {
×
183
    get_syntax_set()
×
184
      .find_syntax_by_extension("yaml")
×
185
      .unwrap()
×
186
      .clone()
×
187
  })
×
188
}
×
189

190
struct YamlThemes {
191
  dark: syntect::highlighting::Theme,
192
  light: syntect::highlighting::Theme,
193
}
194

195
fn get_yaml_themes() -> &'static YamlThemes {
×
196
  static YAML_THEMES: OnceLock<YamlThemes> = OnceLock::new();
197
  YAML_THEMES.get_or_init(|| {
×
198
    let dark = load_embedded_theme(CATPPUCCIN_MACCHIATO_THEME);
×
199
    let light = load_embedded_theme(CATPPUCCIN_LATTE_THEME);
×
200
    YamlThemes { dark, light }
×
201
  })
×
202
}
×
203

204
fn load_embedded_theme(theme_bytes: &[u8]) -> syntect::highlighting::Theme {
×
205
  syntect::highlighting::ThemeSet::load_from_reader(&mut Cursor::new(theme_bytes))
×
206
    .expect("embedded theme should load")
×
207
}
×
208

209
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
210
pub enum Styles {
211
  Text,
212
  Failure,
213
  Warning,
214
  Success,
215
  Primary,
216
  Secondary,
217
  Help,
218
  Background,
219
}
220

221
pub fn theme_styles(light: bool) -> BTreeMap<Styles, Style> {
435✔
222
  let mut styles = if light {
435✔
223
    BTreeMap::from([
×
224
      (Styles::Text, Style::default().fg(LATTE_TEXT)),
×
225
      (Styles::Failure, Style::default().fg(LATTE_RED)),
×
226
      (Styles::Warning, Style::default().fg(LATTE_PEACH)),
×
227
      (Styles::Success, Style::default().fg(LATTE_GREEN)),
×
228
      (Styles::Primary, Style::default().fg(LATTE_MAUVE)),
×
229
      (Styles::Secondary, Style::default().fg(LATTE_MAROON)),
×
230
      (Styles::Help, Style::default().fg(LATTE_BLUE)),
×
231
      (
×
232
        Styles::Background,
×
233
        Style::default().bg(LATTE_BASE).fg(LATTE_TEXT),
×
234
      ),
×
235
    ])
×
236
  } else {
237
    BTreeMap::from([
435✔
238
      (Styles::Text, Style::default().fg(MACCHIATO_TEXT)),
435✔
239
      (Styles::Failure, Style::default().fg(MACCHIATO_RED)),
435✔
240
      (Styles::Warning, Style::default().fg(MACCHIATO_PEACH)),
435✔
241
      (Styles::Success, Style::default().fg(MACCHIATO_GREEN)),
435✔
242
      (Styles::Primary, Style::default().fg(MACCHIATO_MAUVE)),
435✔
243
      (Styles::Secondary, Style::default().fg(MACCHIATO_YELLOW)),
435✔
244
      (Styles::Help, Style::default().fg(MACCHIATO_BLUE)),
435✔
245
      (
435✔
246
        Styles::Background,
435✔
247
        Style::default().bg(MACCHIATO_BASE).fg(MACCHIATO_TEXT),
435✔
248
      ),
435✔
249
    ])
435✔
250
  };
251

252
  apply_theme_override(&mut styles, Styles::Text, "text", false, light);
435✔
253
  apply_theme_override(&mut styles, Styles::Failure, "failure", false, light);
435✔
254
  apply_theme_override(&mut styles, Styles::Warning, "warning", false, light);
435✔
255
  apply_theme_override(&mut styles, Styles::Success, "success", false, light);
435✔
256
  apply_theme_override(&mut styles, Styles::Primary, "primary", false, light);
435✔
257
  apply_theme_override(&mut styles, Styles::Secondary, "secondary", false, light);
435✔
258
  apply_theme_override(&mut styles, Styles::Help, "help", false, light);
435✔
259
  apply_theme_override(&mut styles, Styles::Background, "background", true, light);
435✔
260

261
  styles
435✔
262
}
435✔
263

264
pub fn title_style(txt: &str) -> Span<'_> {
8✔
265
  Span::styled(txt, style_bold())
8✔
266
}
8✔
267

268
pub fn default_part<'a, S: Into<Cow<'a, str>>>(text: S) -> LinePart<'a> {
104✔
269
  LinePart::Default(text.into())
104✔
270
}
104✔
271

272
pub fn help_part<'a, S: Into<Cow<'a, str>>>(text: S) -> LinePart<'a> {
166✔
273
  LinePart::Help(text.into())
166✔
274
}
166✔
275

276
pub fn style_header(light: bool) -> Style {
14✔
277
  style_primary(light).add_modifier(Modifier::REVERSED)
14✔
278
}
14✔
279

280
pub fn style_bold() -> Style {
8✔
281
  Style::default().add_modifier(Modifier::BOLD)
8✔
282
}
8✔
283

284
pub fn style_text(light: bool) -> Style {
112✔
285
  *theme_styles(light).get(&Styles::Text).unwrap()
112✔
286
}
112✔
287
pub fn style_logo(light: bool) -> Style {
3✔
288
  style_primary(light)
3✔
289
}
3✔
290
pub fn style_failure(light: bool) -> Style {
10✔
291
  *theme_styles(light).get(&Styles::Failure).unwrap()
10✔
292
}
10✔
293
pub fn style_warning(light: bool) -> Style {
2✔
294
  *theme_styles(light).get(&Styles::Warning).unwrap()
2✔
295
}
2✔
296
pub fn style_caution(light: bool) -> Style {
2✔
297
  style_warning(light)
2✔
298
}
2✔
299
pub fn style_success(light: bool) -> Style {
4✔
300
  *theme_styles(light).get(&Styles::Success).unwrap()
4✔
301
}
4✔
302
pub fn style_primary(light: bool) -> Style {
105✔
303
  *theme_styles(light).get(&Styles::Primary).unwrap()
105✔
304
}
105✔
305
pub fn style_help(light: bool) -> Style {
152✔
306
  *theme_styles(light).get(&Styles::Help).unwrap()
152✔
307
}
152✔
308

309
pub fn style_secondary(light: bool) -> Style {
43✔
310
  *theme_styles(light).get(&Styles::Secondary).unwrap()
43✔
311
}
43✔
312

313
pub fn style_main_background(light: bool) -> Style {
7✔
314
  *theme_styles(light).get(&Styles::Background).unwrap()
7✔
315
}
7✔
316

317
pub fn style_highlight() -> Style {
16✔
318
  Style::default().add_modifier(Modifier::REVERSED)
16✔
319
}
16✔
320

321
fn line_part_style(part: &LinePart<'_>, light: bool, bold: bool) -> Style {
251✔
322
  let style = match part {
251✔
323
    LinePart::Default(_) => style_text(light),
99✔
324
    LinePart::Help(_) => style_help(light),
152✔
325
  };
326
  if bold {
251✔
327
    style.add_modifier(Modifier::BOLD)
102✔
328
  } else {
329
    style
149✔
330
  }
331
}
251✔
332

333
fn apply_theme_override(
3,480✔
334
  styles: &mut BTreeMap<Styles, Style>,
3,480✔
335
  slot: Styles,
3,480✔
336
  config_key: &str,
3,480✔
337
  background: bool,
3,480✔
338
  light: bool,
3,480✔
339
) {
3,480✔
340
  if let Some(color) = override_color(config_key, light) {
3,480✔
341
    let style = styles.entry(slot).or_default();
×
342
    *style = if background {
×
343
      style.bg(color)
×
344
    } else {
345
      style.fg(color)
×
346
    };
347
  }
3,480✔
348
}
3,480✔
349

350
pub fn mixed_line<'a, I>(parts: I, light: bool) -> Line<'a>
78✔
351
where
78✔
352
  I: IntoIterator<Item = LinePart<'a>>,
78✔
353
{
354
  styled_line(parts, light, false)
78✔
355
}
78✔
356

357
pub fn mixed_bold_line<'a, I>(parts: I, light: bool) -> Line<'a>
47✔
358
where
47✔
359
  I: IntoIterator<Item = LinePart<'a>>,
47✔
360
{
361
  styled_line(parts, light, true)
47✔
362
}
47✔
363

364
pub fn help_bold_line<'a, S: Into<Cow<'a, str>>>(text: S, light: bool) -> Line<'a> {
11✔
365
  mixed_bold_line([help_part(text)], light)
11✔
366
}
11✔
367

368
pub fn key_hints(keys: &[Key]) -> String {
10✔
369
  keys
10✔
370
    .iter()
10✔
371
    .map(ToString::to_string)
10✔
372
    .collect::<Vec<_>>()
10✔
373
    .join("/")
10✔
374
}
10✔
375

376
pub fn action_hint(action: &str, key: Key) -> String {
82✔
377
  format!("{} {}", action, key)
82✔
378
}
82✔
379

380
pub fn describe_and_yaml_hint() -> String {
6✔
381
  format!(
6✔
382
    "{} | {} ",
383
    action_hint("describe", DEFAULT_KEYBINDING.describe_resource.key),
6✔
384
    action_hint("yaml", DEFAULT_KEYBINDING.resource_yaml.key)
6✔
385
  )
386
}
6✔
387

388
pub fn describe_yaml_and_logs_hint() -> String {
5✔
389
  format!(
5✔
390
    "{} | {} ",
391
    describe_and_yaml_hint().trim_end(),
5✔
392
    action_hint("logs", DEFAULT_KEYBINDING.aggregate_logs.key)
5✔
393
  )
394
}
5✔
395

396
pub fn describe_yaml_logs_and_esc_hint() -> String {
×
397
  format!(
×
398
    "{} | back {} ",
399
    describe_yaml_and_logs_hint().trim_end(),
×
400
    DEFAULT_KEYBINDING.esc.key
×
401
  )
402
}
×
403

404
pub fn describe_yaml_and_esc_hint() -> String {
×
405
  format!(
×
406
    "{} | back {} ",
407
    describe_and_yaml_hint().trim_end(),
×
408
    DEFAULT_KEYBINDING.esc.key
×
409
  )
410
}
×
411

412
pub fn describe_yaml_decode_and_esc_hint() -> String {
×
413
  format!(
×
414
    "{} | {} | back {} ",
415
    describe_and_yaml_hint().trim_end(),
×
416
    action_hint("decode", DEFAULT_KEYBINDING.decode_secret.key),
×
417
    DEFAULT_KEYBINDING.esc.key
×
418
  )
419
}
×
420

421
pub fn wide_hint() -> String {
5✔
422
  format!("wide {}", DEFAULT_KEYBINDING.toggle_wide_columns.key)
5✔
423
}
5✔
424

425
pub fn filter_cursor_position(area: Rect, prefix_width: usize, filter: &str) -> Position {
7✔
426
  Position {
7✔
427
    x: area.x
7✔
428
      + (prefix_width as u16 + 1 + filter.chars().count() as u16).min(area.width.saturating_sub(2)),
7✔
429
    y: area.y,
7✔
430
  }
7✔
431
}
7✔
432

433
fn styled_line<'a, I>(parts: I, light: bool, bold: bool) -> Line<'a>
125✔
434
where
125✔
435
  I: IntoIterator<Item = LinePart<'a>>,
125✔
436
{
437
  Line::from(
125✔
438
    parts
125✔
439
      .into_iter()
125✔
440
      .map(|part| {
251✔
441
        let style = line_part_style(&part, light, bold);
251✔
442
        match part {
251✔
443
          LinePart::Default(text) | LinePart::Help(text) => Span::styled(text, style),
251✔
444
        }
445
      })
251✔
446
      .collect::<Vec<_>>(),
125✔
447
  )
448
}
125✔
449

450
pub fn get_gauge_symbol(enhanced_graphics: bool) -> &'static str {
12✔
451
  if enhanced_graphics {
12✔
452
    symbols::line::THICK_HORIZONTAL
4✔
453
  } else {
454
    symbols::line::HORIZONTAL
8✔
455
  }
456
}
12✔
457

458
pub fn table_header_style(cells: Vec<&str>, light: bool) -> Row<'_> {
10✔
459
  Row::new(cells).style(style_text(light)).bottom_margin(0)
10✔
460
}
10✔
461

462
pub fn horizontal_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
3✔
463
  Layout::default()
3✔
464
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
3✔
465
      &constraints,
3✔
466
    ))
3✔
467
    .direction(Direction::Horizontal)
3✔
468
    .split(size)
3✔
469
}
3✔
470

471
pub fn horizontal_chunks_with_margin(
7✔
472
  constraints: Vec<Constraint>,
7✔
473
  size: Rect,
7✔
474
  margin: u16,
7✔
475
) -> Rc<[Rect]> {
7✔
476
  Layout::default()
7✔
477
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
7✔
478
      &constraints,
7✔
479
    ))
7✔
480
    .direction(Direction::Horizontal)
7✔
481
    .margin(margin)
7✔
482
    .split(size)
7✔
483
}
7✔
484

485
pub fn vertical_chunks(constraints: Vec<Constraint>, size: Rect) -> Rc<[Rect]> {
12✔
486
  Layout::default()
12✔
487
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
12✔
488
      &constraints,
12✔
489
    ))
12✔
490
    .direction(Direction::Vertical)
12✔
491
    .split(size)
12✔
492
}
12✔
493

494
pub fn vertical_chunks_with_margin(
8✔
495
  constraints: Vec<Constraint>,
8✔
496
  size: Rect,
8✔
497
  margin: u16,
8✔
498
) -> Rc<[Rect]> {
8✔
499
  Layout::default()
8✔
500
    .constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
8✔
501
      &constraints,
8✔
502
    ))
8✔
503
    .direction(Direction::Vertical)
8✔
504
    .margin(margin)
8✔
505
    .split(size)
8✔
506
}
8✔
507

508
pub fn layout_block(title: Span<'_>) -> Block<'_> {
8✔
509
  Block::default().borders(Borders::ALL).title(title)
8✔
510
}
8✔
511

512
pub fn layout_block_default(title: &str) -> Block<'_> {
8✔
513
  layout_block(title_style(title))
8✔
514
}
8✔
515

516
pub fn layout_block_default_line(title: Line<'_>) -> Block<'_> {
11✔
517
  Block::default().borders(Borders::ALL).title(title)
11✔
518
}
11✔
519

520
pub fn layout_block_active_line(title: Line<'_>, light: bool) -> Block<'_> {
5✔
521
  Block::default()
5✔
522
    .borders(Borders::ALL)
5✔
523
    .title(title)
5✔
524
    .style(style_secondary(light))
5✔
525
}
5✔
526

527
pub fn layout_block_active_span(title: Line<'_>, light: bool) -> Block<'_> {
2✔
528
  layout_block_active_line(title, light)
2✔
529
}
2✔
530

531
pub fn layout_block_top_border(title: Line<'_>) -> Block<'_> {
10✔
532
  Block::default().borders(Borders::TOP).title(title)
10✔
533
}
10✔
534

535
enum FilterDisplayState<'a> {
536
  Inactive,
537
  EditingEmpty,
538
  Value { filter: &'a str, active: bool },
539
}
540

541
fn filter_display_state(filter: &str, active: bool) -> FilterDisplayState<'_> {
20✔
542
  if active && filter.is_empty() {
20✔
543
    FilterDisplayState::EditingEmpty
×
544
  } else if !filter.is_empty() {
20✔
545
    FilterDisplayState::Value { filter, active }
7✔
546
  } else {
547
    FilterDisplayState::Inactive
13✔
548
  }
549
}
20✔
550

551
fn filter_display_parts(filter: &str, active: bool) -> Vec<LinePart<'_>> {
20✔
552
  let state = filter_display_state(filter, active);
20✔
553
  let inactive_text = action_hint("filter", DEFAULT_KEYBINDING.filter.key);
20✔
554
  let clear_suffix = format!(" | clear {} ", DEFAULT_KEYBINDING.esc.key);
20✔
555
  let edit_suffix = format!(" | edit {} ", DEFAULT_KEYBINDING.filter.key);
20✔
556

557
  match state {
20✔
558
    FilterDisplayState::Inactive => vec![help_part(inactive_text)],
13✔
559
    FilterDisplayState::EditingEmpty => {
560
      vec![help_part("[type to filter]"), help_part(clear_suffix)]
×
561
    }
562
    FilterDisplayState::Value {
563
      filter,
4✔
564
      active: true,
565
    } => vec![
4✔
566
      default_part(format!("[{}]", filter)),
4✔
567
      help_part(clear_suffix),
4✔
568
    ],
569
    FilterDisplayState::Value {
570
      filter,
3✔
571
      active: false,
572
    } => vec![
3✔
573
      default_part(format!("[{}]", filter)),
3✔
574
      help_part(edit_suffix),
3✔
575
    ],
576
  }
577
}
20✔
578

579
pub fn filter_status_parts(filter: &str, active: bool) -> Vec<LinePart<'_>> {
6✔
580
  filter_display_parts(filter, active)
6✔
581
}
6✔
582

583
pub fn owned_filter_status_parts(filter: &str, active: bool) -> Vec<LinePart<'static>> {
14✔
584
  filter_display_parts(filter, active)
14✔
585
    .into_iter()
14✔
586
    .map(|part| match part {
19✔
587
      LinePart::Default(text) => default_part(text.into_owned()),
5✔
588
      LinePart::Help(text) => help_part(text.into_owned()),
14✔
589
    })
19✔
590
    .collect()
14✔
591
}
14✔
592

593
pub fn title_with_dual_style<'a>(part_1: String, part_2: Line<'a>, light: bool) -> Line<'a> {
12✔
594
  let mut spans = vec![Span::styled(
12✔
595
    part_1,
12✔
596
    style_secondary(light).add_modifier(Modifier::BOLD),
12✔
597
  )];
598
  spans.extend(part_2.spans);
12✔
599
  Line::from(spans)
12✔
600
}
12✔
601

602
pub fn copy_and_escape_title_line<'a, S: Into<Cow<'a, str>>>(_target: S, light: bool) -> Line<'a> {
×
603
  mixed_bold_line(
×
604
    [
×
605
      help_part(format!(
×
606
        "{} | ",
×
607
        action_hint("copy", DEFAULT_KEYBINDING.copy_to_clipboard.key)
×
608
      )),
×
609
      help_part(format!("back {} ", DEFAULT_KEYBINDING.esc.key)),
×
610
    ],
×
611
    light,
×
612
  )
613
}
×
614

615
pub fn copy_scroll_and_escape_title_line<'a, S: Into<Cow<'a, str>>>(
×
616
  _target: S,
×
617
  auto_scroll: bool,
×
618
  light: bool,
×
619
) -> Line<'a> {
×
620
  let auto_scroll_action = if auto_scroll {
×
621
    "pause scroll"
×
622
  } else {
623
    "resume scroll"
×
624
  };
625
  mixed_bold_line(
×
626
    [
×
627
      help_part(format!(
×
628
        "{} | {} | ",
×
629
        action_hint("copy", DEFAULT_KEYBINDING.copy_to_clipboard.key),
×
630
        action_hint(auto_scroll_action, DEFAULT_KEYBINDING.log_auto_scroll.key)
×
631
      )),
×
632
      help_part(format!("back {} ", DEFAULT_KEYBINDING.esc.key)),
×
633
    ],
×
634
    light,
×
635
  )
636
}
×
637

638
pub fn split_hint_suffix(text: &str) -> (&str, Option<&str>) {
83✔
639
  if let Some(pos) = text.rfind(" <") {
83✔
640
    (&text[..pos], Some(&text[(pos + 1)..]))
83✔
641
  } else {
642
    (text, None)
×
643
  }
644
}
83✔
645

646
/// helper function to create a centered rect using up
647
/// certain percentage of the available rect `r`
648
pub fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
4✔
649
  let Rect {
650
    width: grid_width,
4✔
651
    height: grid_height,
4✔
652
    ..
653
  } = r;
4✔
654
  let outer_height = (grid_height / 2).saturating_sub(height / 2);
4✔
655

656
  let popup_layout = Layout::default()
4✔
657
    .direction(Direction::Vertical)
4✔
658
    .constraints(
4✔
659
      [
4✔
660
        Constraint::Length(outer_height),
4✔
661
        Constraint::Length(height),
4✔
662
        Constraint::Length(outer_height),
4✔
663
      ]
4✔
664
      .as_ref(),
4✔
665
    )
4✔
666
    .split(r);
4✔
667

668
  let outer_width = (grid_width / 2).saturating_sub(width / 2);
4✔
669

670
  Layout::default()
4✔
671
    .direction(Direction::Horizontal)
4✔
672
    .constraints(
4✔
673
      [
4✔
674
        Constraint::Length(outer_width),
4✔
675
        Constraint::Length(width),
4✔
676
        Constraint::Length(outer_width),
4✔
677
      ]
4✔
678
      .as_ref(),
4✔
679
    )
4✔
680
    .split(popup_layout[1])[1]
4✔
681
}
4✔
682

683
pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool, light: bool) {
9✔
684
  if is_loading {
9✔
685
    let text = "\n\n Loading ...\n\n".to_owned();
×
686
    let text = Text::from(text);
×
687
    let text = text.patch_style(style_secondary(light));
×
688

×
689
    // Contains the text
×
690
    let paragraph = Paragraph::new(text)
×
691
      .style(style_secondary(light))
×
692
      .block(block);
×
693
    f.render_widget(paragraph, area);
×
694
  } else {
×
695
    f.render_widget(block, area)
9✔
696
  }
697
}
9✔
698

699
// using a macro to reuse code as generics will make handling lifetimes a PITA
700
#[macro_export]
701
macro_rules! draw_resource_tab {
702
  ($title:expr, $block:expr, $f:expr, $app:expr, $area:expr, $fn1:expr, $fn2:expr, $res:expr) => {
703
    match $block {
704
      ActiveBlock::Describe => draw_describe_block(
705
        $f,
706
        $app,
707
        $area,
708
        title_with_dual_style(
709
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
710
          $crate::ui::utils::copy_and_escape_title_line($title, $app.light_theme),
711
          $app.light_theme,
712
        ),
713
      ),
714
      ActiveBlock::Yaml => draw_yaml_block(
715
        $f,
716
        $app,
717
        $area,
718
        title_with_dual_style(
719
          get_resource_title($app, $title, get_describe_active($block), $res.items.len()),
720
          $crate::ui::utils::copy_and_escape_title_line($title, $app.light_theme),
721
          $app.light_theme,
722
        ),
723
      ),
724
      ActiveBlock::Pods => $crate::app::pods::draw_block_as_sub($f, $app, $area),
725
      ActiveBlock::Containers => $crate::app::pods::draw_containers_block($f, $app, $area),
726
      ActiveBlock::Logs => $crate::app::pods::draw_logs_block($f, $app, $area),
727
      ActiveBlock::Namespaces => $fn1($app.get_prev_route().active_block, $f, $app, $area),
728
      _ => $fn2($f, $app, $area),
729
    };
730
  };
731
}
732

733
pub struct ResourceTableProps<'a, T> {
734
  pub title: String,
735
  pub inline_help: Line<'a>,
736
  pub resource: &'a mut StatefulTable<T>,
737
  pub table_headers: Vec<&'a str>,
738
  pub column_widths: Vec<Constraint>,
739
}
740
/// common for all resources
741
pub fn draw_describe_block(f: &mut Frame<'_>, app: &mut App, area: Rect, title: Line<'_>) {
×
742
  draw_yaml_block(f, app, area, title);
×
743
}
×
744

745
/// Refreshes the syntax-highlight cache when empty or the theme changed.
746
/// Returns `false` when there is no content to highlight.
NEW
747
fn ensure_highlight_cache(app: &mut App) -> bool {
×
NEW
748
  if app.data.describe_out.get_txt().is_empty() {
×
NEW
749
    return false;
×
NEW
750
  }
×
NEW
751
  if app.data.describe_out.highlighted_lines.is_empty()
×
NEW
752
    || app.data.describe_out.highlight_light_theme != app.light_theme
×
753
  {
NEW
754
    let ss = get_syntax_set();
×
NEW
755
    let syntax = get_yaml_syntax_reference();
×
NEW
756
    let theme = if app.light_theme {
×
NEW
757
      &get_yaml_themes().light
×
758
    } else {
NEW
759
      &get_yaml_themes().dark
×
760
    };
NEW
761
    let mut h = syntect::easy::HighlightLines::new(syntax, theme);
×
NEW
762
    let txt = app.data.describe_out.get_txt();
×
NEW
763
    let lines: Vec<_> = syntect::util::LinesWithEndings::from(txt)
×
NEW
764
      .filter_map(|line| match h.highlight_line(line, ss) {
×
NEW
765
        Ok(segments) => {
×
NEW
766
          let line_spans: Vec<_> = segments
×
NEW
767
            .into_iter()
×
NEW
768
            .filter_map(syntect_to_ratatui_span_owned)
×
NEW
769
            .collect();
×
NEW
770
          Some(ratatui::text::Line::from(line_spans))
×
771
        }
NEW
772
        Err(_) => None,
×
NEW
773
      })
×
NEW
774
      .collect();
×
NEW
775
    app.data.describe_out.highlighted_lines = lines;
×
NEW
776
    app.data.describe_out.highlight_light_theme = app.light_theme;
×
NEW
777
  }
×
NEW
778
  true
×
NEW
779
}
×
780

781
/// Compute the (start, end, scroll-within-slice) window into a buffer of
782
/// `total` highlighted lines for the given `offset` and visible row count.
783
/// Clamps `offset` to a valid index and ensures `start <= end <= total`.
784
fn highlight_window(offset: usize, total: usize, view_h: usize) -> (usize, usize, u16) {
5✔
785
  // Caller guarantees total > 0; clamp here defensively too.
786
  let view_h = view_h.max(1);
5✔
787
  let effective_offset = offset.min(total.saturating_sub(1));
5✔
788
  let slice_start = effective_offset.saturating_sub(view_h);
5✔
789
  let slice_end = total.min(effective_offset + view_h * 3);
5✔
790
  let adjusted_offset = (effective_offset - slice_start).min(u16::MAX as usize) as u16;
5✔
791
  (slice_start, slice_end, adjusted_offset)
5✔
792
}
5✔
793

794
/// common for all resources
795
pub fn draw_yaml_block(f: &mut Frame<'_>, app: &mut App, area: Rect, title: Line<'_>) {
×
796
  let block = layout_block_top_border(title);
×
NEW
797
  if ensure_highlight_cache(app) {
×
NEW
798
    let total = app.data.describe_out.highlighted_lines.len();
×
NEW
799
    if total == 0 {
×
NEW
800
      loading(f, block, area, app.is_loading(), app.light_theme);
×
NEW
801
      return;
×
UNCOV
802
    }
×
NEW
803
    let offset = app.data.describe_out.offset;
×
804
    // Subtract 2 for the top-border of the block; clamp to >=1 so a tiny
805
    // terminal doesn't degenerate into an empty slice.
NEW
806
    let view_h = (area.height.saturating_sub(2) as usize).max(1);
×
NEW
807
    let (slice_start, slice_end, adjusted_offset) = highlight_window(offset, total, view_h);
×
NEW
808
    let visible_lines = app.data.describe_out.highlighted_lines[slice_start..slice_end].to_vec();
×
NEW
809
    let paragraph = Paragraph::new(visible_lines)
×
810
      .block(block)
×
811
      .wrap(Wrap { trim: false })
×
NEW
812
      .scroll((adjusted_offset, 0));
×
813
    f.render_widget(paragraph, area);
×
814
  } else {
×
815
    loading(f, block, area, app.is_loading(), app.light_theme);
×
816
  }
×
817
}
×
818

819
fn draw_resource_table<'a, T: Named, F>(
11✔
820
  f: &mut Frame<'_>,
11✔
821
  area: Rect,
11✔
822
  table_props: ResourceTableProps<'a, T>,
11✔
823
  row_cell_mapper: F,
11✔
824
  light_theme: bool,
11✔
825
  is_loading: bool,
11✔
826
  block: Block<'a>,
11✔
827
) where
11✔
828
  F: Fn(&T) -> Row<'a>,
11✔
829
{
830
  if !table_props.resource.items.is_empty() {
11✔
831
    let filter = table_props.resource.filter.to_lowercase();
8✔
832
    let has_filter = !filter.is_empty();
8✔
833
    let mut filtered_indices: Vec<usize> = Vec::new();
8✔
834

835
    let filtered_items: Vec<(usize, &T)> = table_props
8✔
836
      .resource
8✔
837
      .items
8✔
838
      .iter()
8✔
839
      .enumerate()
8✔
840
      .filter(|(_, c)| filter.is_empty() || filter_by_name(&filter, *c))
33✔
841
      .inspect(|(idx, _)| {
30✔
842
        if has_filter {
30✔
843
          filtered_indices.push(*idx);
5✔
844
        }
25✔
845
      })
30✔
846
      .collect();
8✔
847

848
    if has_filter {
8✔
849
      let max = filtered_items.len().saturating_sub(1);
4✔
850
      if let Some(sel) = table_props.resource.state.selected() {
4✔
851
        if sel > max {
4✔
852
          table_props.resource.state.select(Some(max));
×
853
        }
4✔
854
      }
×
855
    }
4✔
856
    table_props.resource.filtered_indices = filtered_indices;
8✔
857

858
    // Determine the visible row range to avoid expensive row_cell_mapper
859
    // calls for off-screen items. Subtract 3 for header + borders; clamp
860
    // to >=1 so a tiny terminal still renders at least one row of data
861
    // instead of degenerating into all-empty rows.
862
    let selected = table_props.resource.state.selected().unwrap_or(0);
8✔
863
    let view_h = (area.height.saturating_sub(3) as usize).max(1);
8✔
864
    let visible_start = selected.saturating_sub(view_h);
8✔
865
    let visible_end = (selected + view_h * 2).min(filtered_items.len());
8✔
866

867
    let rows: Vec<Row<'a>> = filtered_items
8✔
868
      .iter()
8✔
869
      .enumerate()
8✔
870
      .map(|(fi, (_orig_idx, item))| {
30✔
871
        if fi >= visible_start && fi < visible_end {
30✔
872
          row_cell_mapper(item)
30✔
873
        } else {
NEW
874
          Row::default()
×
875
        }
876
      })
30✔
877
      .collect();
8✔
878

879
    let table = Table::new(rows, &table_props.column_widths)
8✔
880
      .header(table_header_style(table_props.table_headers, light_theme))
8✔
881
      .block(block)
8✔
882
      .row_highlight_style(style_highlight())
8✔
883
      .highlight_symbol(HIGHLIGHT);
8✔
884

885
    f.render_stateful_widget(table, area, &mut table_props.resource.state);
8✔
886
  } else {
3✔
887
    loading(f, block, area, is_loading, light_theme);
3✔
888
  }
3✔
889
}
11✔
890

891
/// Builds the help `Line` for a resource block title, weaving filter status
892
/// into any existing inline help (placing it after a "containers" prefix when present).
893
fn build_resource_help_line(
12✔
894
  inline_help: Line<'_>,
12✔
895
  filter: &str,
12✔
896
  filter_active: bool,
12✔
897
  light_theme: bool,
12✔
898
) -> Line<'static> {
12✔
899
  let inline_help_text = inline_help
12✔
900
    .spans
12✔
901
    .iter()
12✔
902
    .map(|span| span.content.as_ref())
12✔
903
    .collect::<String>();
12✔
904
  let containers_prefix = format!(
12✔
905
    "{} | ",
906
    action_hint("containers", DEFAULT_KEYBINDING.submit.key)
12✔
907
  );
908
  let mut help_parts: Vec<LinePart<'static>> = Vec::new();
12✔
909
  if let Some(rest) = inline_help_text.strip_prefix(&containers_prefix) {
12✔
910
    help_parts.push(help_part(containers_prefix));
5✔
911
    help_parts.extend(owned_filter_status_parts(filter, filter_active));
5✔
912
    if !rest.is_empty() {
5✔
913
      help_parts.push(help_part(" | ".to_string()));
4✔
914
      help_parts.push(help_part(rest.to_string()));
4✔
915
    }
4✔
916
  } else {
917
    help_parts.extend(owned_filter_status_parts(filter, filter_active));
7✔
918
    if !inline_help_text.is_empty() {
7✔
919
      help_parts.push(help_part(" | ".to_string()));
5✔
920
      help_parts.push(help_part(inline_help_text));
5✔
921
    }
5✔
922
  }
923
  mixed_bold_line(help_parts, light_theme)
12✔
924
}
12✔
925

926
/// Draw a kubernetes resource overview tab
927
pub fn draw_resource_block<'a, T: Named, F>(
10✔
928
  f: &mut Frame<'_>,
10✔
929
  area: Rect,
10✔
930
  table_props: ResourceTableProps<'a, T>,
10✔
931
  row_cell_mapper: F,
10✔
932
  light_theme: bool,
10✔
933
  is_loading: bool,
10✔
934
) where
10✔
935
  F: Fn(&T) -> Row<'a>,
10✔
936
{
937
  let ResourceTableProps {
938
    title,
10✔
939
    inline_help,
10✔
940
    resource,
10✔
941
    table_headers,
10✔
942
    column_widths,
10✔
943
  } = table_props;
10✔
944
  let filter = resource.filter.clone();
10✔
945
  let filter_active = resource.filter_active;
10✔
946
  if filter_active {
10✔
947
    let title_width = title.chars().count();
2✔
948
    let title = title_with_dual_style(
2✔
949
      title,
2✔
950
      mixed_bold_line(owned_filter_status_parts(&filter, true), light_theme),
2✔
951
      light_theme,
2✔
952
    );
953
    let block = layout_block_top_border(title);
2✔
954
    draw_resource_table(
2✔
955
      f,
2✔
956
      area,
2✔
957
      ResourceTableProps {
2✔
958
        title: String::new(),
2✔
959
        inline_help: Line::default(),
2✔
960
        resource,
2✔
961
        table_headers,
2✔
962
        column_widths,
2✔
963
      },
2✔
964
      row_cell_mapper,
2✔
965
      light_theme,
2✔
966
      is_loading,
2✔
967
      block,
2✔
968
    );
969
    f.set_cursor_position(filter_cursor_position(area, title_width, &filter));
2✔
970
    return;
2✔
971
  }
8✔
972

973
  let help_line = build_resource_help_line(inline_help, &filter, filter_active, light_theme);
8✔
974
  let title = title_with_dual_style(title, help_line, light_theme);
8✔
975
  let block = layout_block_top_border(title);
8✔
976
  draw_resource_table(
8✔
977
    f,
8✔
978
    area,
8✔
979
    ResourceTableProps {
8✔
980
      title: String::new(),
8✔
981
      inline_help: Line::default(),
8✔
982
      resource,
8✔
983
      table_headers,
8✔
984
      column_widths,
8✔
985
    },
8✔
986
    row_cell_mapper,
8✔
987
    light_theme,
8✔
988
    is_loading,
8✔
989
    block,
8✔
990
  );
991
}
10✔
992

993
pub fn draw_route_resource_block<'a, T: Named, F>(
1✔
994
  f: &mut Frame<'_>,
1✔
995
  area: Rect,
1✔
996
  table_props: ResourceTableProps<'a, T>,
1✔
997
  row_cell_mapper: F,
1✔
998
  light_theme: bool,
1✔
999
  is_loading: bool,
1✔
1000
) where
1✔
1001
  F: Fn(&T) -> Row<'a>,
1✔
1002
{
1003
  let ResourceTableProps {
1004
    title,
1✔
1005
    inline_help,
1✔
1006
    resource,
1✔
1007
    table_headers,
1✔
1008
    column_widths,
1✔
1009
  } = table_props;
1✔
1010
  let filter = resource.filter.clone();
1✔
1011
  let filter_active = resource.filter_active;
1✔
1012
  if filter_active {
1✔
1013
    let title_width = title.chars().count();
×
1014
    let title = title_with_dual_style(
×
1015
      title,
×
1016
      mixed_bold_line(owned_filter_status_parts(&filter, true), light_theme),
×
1017
      light_theme,
×
1018
    );
1019
    let block = layout_block_active_span(title, light_theme);
×
1020
    draw_resource_table(
×
1021
      f,
×
1022
      area,
×
1023
      ResourceTableProps {
×
1024
        title: String::new(),
×
1025
        inline_help: Line::default(),
×
1026
        resource,
×
1027
        table_headers,
×
1028
        column_widths,
×
1029
      },
×
1030
      row_cell_mapper,
×
1031
      light_theme,
×
1032
      is_loading,
×
1033
      block,
×
1034
    );
1035
    f.set_cursor_position(filter_cursor_position(area, title_width, &filter));
×
1036
    return;
×
1037
  }
1✔
1038

1039
  let title = title_with_dual_style(title, inline_help, light_theme);
1✔
1040
  let block = layout_block_active_span(title, light_theme);
1✔
1041
  draw_resource_table(
1✔
1042
    f,
1✔
1043
    area,
1✔
1044
    ResourceTableProps {
1✔
1045
      title: String::new(),
1✔
1046
      inline_help: Line::default(),
1✔
1047
      resource,
1✔
1048
      table_headers,
1✔
1049
      column_widths,
1✔
1050
    },
1✔
1051
    row_cell_mapper,
1✔
1052
    light_theme,
1✔
1053
    is_loading,
1✔
1054
    block,
1✔
1055
  );
1056
}
1✔
1057

1058
pub fn filter_by_resource_name<T: Named>(
7✔
1059
  filter: &str,
7✔
1060
  res: &T,
7✔
1061
  row_cell_mapper: Row<'static>,
7✔
1062
) -> Option<Row<'static>> {
7✔
1063
  if filter.is_empty() || filter_by_name(filter, res) {
7✔
1064
    Some(row_cell_mapper)
7✔
1065
  } else {
1066
    None
×
1067
  }
1068
}
7✔
1069

1070
pub fn text_matches_filter(filter: &str, value: &str) -> bool {
202✔
1071
  let filter = filter.to_lowercase();
202✔
1072
  let value = value.to_lowercase();
202✔
1073
  filter.is_empty() || glob_match(&filter, &value) || value.contains(&filter)
202✔
1074
}
202✔
1075

1076
fn filter_by_name<T: Named>(ft: &str, res: &T) -> bool {
9✔
1077
  text_matches_filter(ft, res.get_name())
9✔
1078
}
9✔
1079

1080
pub fn get_cluster_wide_resource_title<S: AsRef<str>>(
2✔
1081
  title: S,
2✔
1082
  items_len: usize,
2✔
1083
  suffix: S,
2✔
1084
) -> String {
2✔
1085
  format!(" {} [{}] {}", title.as_ref(), items_len, suffix.as_ref())
2✔
1086
}
2✔
1087

1088
pub fn get_resource_title<S: AsRef<str>>(
10✔
1089
  app: &App,
10✔
1090
  title: S,
10✔
1091
  suffix: S,
10✔
1092
  items_len: usize,
10✔
1093
) -> String {
10✔
1094
  format!(
10✔
1095
    " {} {}",
1096
    title_with_ns(
10✔
1097
      title.as_ref(),
10✔
1098
      app
10✔
1099
        .data
10✔
1100
        .selected
10✔
1101
        .ns
10✔
1102
        .as_ref()
10✔
1103
        .unwrap_or(&String::from("all")),
10✔
1104
      items_len
10✔
1105
    ),
1106
    suffix.as_ref(),
10✔
1107
  )
1108
}
10✔
1109

1110
static DESCRIBE_ACTIVE: &str = "-> Describe ";
1111
static YAML_ACTIVE: &str = "-> YAML ";
1112

1113
pub fn get_describe_active<'a>(block: ActiveBlock) -> &'a str {
×
1114
  match block {
×
1115
    ActiveBlock::Describe => DESCRIBE_ACTIVE,
×
1116
    _ => YAML_ACTIVE,
×
1117
  }
1118
}
×
1119

1120
pub fn title_with_ns(title: &str, ns: &str, length: usize) -> String {
11✔
1121
  format!("{} (ns: {}) [{}]", title, ns, length)
11✔
1122
}
11✔
1123

1124
#[cfg(test)]
1125
mod tests {
1126
  use ratatui::{
1127
    backend::TestBackend, buffer::Buffer, layout::Position, style::Modifier, widgets::Cell,
1128
    Terminal,
1129
  };
1130

1131
  use super::*;
1132
  use crate::ui::utils::{MACCHIATO_BLUE, MACCHIATO_MAUVE, MACCHIATO_TEXT, MACCHIATO_YELLOW};
1133

1134
  #[test]
1135
  fn test_draw_resource_block() {
1✔
1136
    let backend = TestBackend::new(100, 6);
1✔
1137
    let mut terminal = Terminal::new(backend).unwrap();
1✔
1138

1139
    struct RenderTest {
1140
      pub name: String,
1141
      pub namespace: String,
1142
      pub data: i32,
1143
      pub age: String,
1144
    }
1145

1146
    impl Named for RenderTest {
1147
      fn get_name(&self) -> &String {
×
1148
        &self.name
×
1149
      }
×
1150
    }
1151
    terminal
1✔
1152
      .draw(|f| {
1✔
1153
        let size = f.area();
1✔
1154
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
1155
        resource.set_items(vec![
1✔
1156
          RenderTest {
1✔
1157
            name: "Test 1".into(),
1✔
1158
            namespace: "Test ns".into(),
1✔
1159
            age: "65h3m".into(),
1✔
1160
            data: 5,
1✔
1161
          },
1✔
1162
          RenderTest {
1✔
1163
            name: "Test long name that should be truncated from view".into(),
1✔
1164
            namespace: "Test ns".into(),
1✔
1165
            age: "65h3m".into(),
1✔
1166
            data: 3,
1✔
1167
          },
1✔
1168
          RenderTest {
1✔
1169
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
1170
            namespace: "Test ns long value check that should be truncated".into(),
1✔
1171
            age: "65h3m".into(),
1✔
1172
            data: 6,
1✔
1173
          },
1✔
1174
        ]);
1175
        draw_resource_block(
1✔
1176
          f,
1✔
1177
          size,
1✔
1178
          ResourceTableProps {
1✔
1179
            title: "Test".into(),
1✔
1180
            inline_help: help_bold_line("-> yaml <y>", false),
1✔
1181
            resource: &mut resource,
1✔
1182
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
1183
            column_widths: vec![
1✔
1184
              Constraint::Percentage(30),
1✔
1185
              Constraint::Percentage(40),
1✔
1186
              Constraint::Percentage(15),
1✔
1187
              Constraint::Percentage(15),
1✔
1188
            ],
1✔
1189
          },
1✔
1190
          |c| {
3✔
1191
            Row::new(vec![
3✔
1192
              Cell::from(c.namespace.to_owned()),
3✔
1193
              Cell::from(c.name.to_owned()),
3✔
1194
              Cell::from(c.data.to_string()),
3✔
1195
              Cell::from(c.age.to_owned()),
3✔
1196
            ])
1197
            .style(style_primary(false))
3✔
1198
          },
3✔
1199
          false,
1200
          false,
1201
        );
1202
      })
1✔
1203
      .unwrap();
1✔
1204

1205
    let mut expected = Buffer::with_lines(vec![
1✔
1206
        "Testfilter </> | -> yaml <y>────────────────────────────────────────────────────────────────────────",
1207
        "   Namespace                     Name                                 Data           Age            ",
1✔
1208
        "=> Test ns                       Test 1                               5              65h3m          ",
1✔
1209
        "   Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
1210
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
1211
        "                                                                                                    ",
1✔
1212
      ]);
1213
    // set row styles
1214
    // First row heading style
1215
    for col in 0..=99 {
100✔
1216
      match col {
100✔
1217
        0..=3 => {
100✔
1218
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
1219
            Style::default()
4✔
1220
              .fg(MACCHIATO_YELLOW)
4✔
1221
              .add_modifier(Modifier::BOLD),
4✔
1222
          );
4✔
1223
        }
4✔
1224
        4..=27 => {
96✔
1225
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
24✔
1226
            Style::default()
24✔
1227
              .fg(MACCHIATO_BLUE)
24✔
1228
              .add_modifier(Modifier::BOLD),
24✔
1229
          );
24✔
1230
        }
24✔
1231
        _ => {}
72✔
1232
      }
1233
    }
1234

1235
    // Second row table header style
1236
    for col in 0..=99 {
100✔
1237
      expected
100✔
1238
        .cell_mut(Position::new(col, 1))
100✔
1239
        .unwrap()
100✔
1240
        .set_style(Style::default().fg(MACCHIATO_TEXT));
100✔
1241
    }
100✔
1242
    // first table data row style
1243
    for col in 0..=99 {
100✔
1244
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1245
        Style::default()
100✔
1246
          .fg(MACCHIATO_MAUVE)
100✔
1247
          .add_modifier(Modifier::REVERSED),
100✔
1248
      );
100✔
1249
    }
100✔
1250
    // remaining table data row style
1251
    for row in 3..=4 {
2✔
1252
      for col in 0..=99 {
200✔
1253
        expected
200✔
1254
          .cell_mut(Position::new(col, row))
200✔
1255
          .unwrap()
200✔
1256
          .set_style(Style::default().fg(MACCHIATO_MAUVE));
200✔
1257
      }
200✔
1258
    }
1259

1260
    terminal.backend().assert_buffer(&expected);
1✔
1261
  }
1✔
1262

1263
  #[test]
1264
  fn test_draw_resource_block_filter() {
1✔
1265
    let backend = TestBackend::new(100, 6);
1✔
1266
    let mut terminal = Terminal::new(backend).unwrap();
1✔
1267

1268
    struct RenderTest {
1269
      pub name: String,
1270
      pub namespace: String,
1271
      pub data: i32,
1272
      pub age: String,
1273
    }
1274
    impl Named for RenderTest {
1275
      fn get_name(&self) -> &String {
3✔
1276
        &self.name
3✔
1277
      }
3✔
1278
    }
1279

1280
    terminal
1✔
1281
      .draw(|f| {
1✔
1282
        let size = f.area();
1✔
1283
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
1284
        resource.set_items(vec![
1✔
1285
          RenderTest {
1✔
1286
            name: "Test 1".into(),
1✔
1287
            namespace: "Test ns".into(),
1✔
1288
            age: "65h3m".into(),
1✔
1289
            data: 5,
1✔
1290
          },
1✔
1291
          RenderTest {
1✔
1292
            name: "Test long name that should be truncated from view".into(),
1✔
1293
            namespace: "Test ns".into(),
1✔
1294
            age: "65h3m".into(),
1✔
1295
            data: 3,
1✔
1296
          },
1✔
1297
          RenderTest {
1✔
1298
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
1299
            namespace: "Test ns long value check that should be truncated".into(),
1✔
1300
            age: "65h3m".into(),
1✔
1301
            data: 6,
1✔
1302
          },
1✔
1303
        ]);
1304
        resource.filter = "truncated".to_string();
1✔
1305
        draw_resource_block(
1✔
1306
          f,
1✔
1307
          size,
1✔
1308
          ResourceTableProps {
1✔
1309
            title: "Test".into(),
1✔
1310
            inline_help: help_bold_line("-> yaml <y>", false),
1✔
1311
            resource: &mut resource,
1✔
1312
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
1313
            column_widths: vec![
1✔
1314
              Constraint::Percentage(30),
1✔
1315
              Constraint::Percentage(40),
1✔
1316
              Constraint::Percentage(15),
1✔
1317
              Constraint::Percentage(15),
1✔
1318
            ],
1✔
1319
          },
1✔
1320
          |c| {
2✔
1321
            Row::new(vec![
2✔
1322
              Cell::from(c.namespace.to_owned()),
2✔
1323
              Cell::from(c.name.to_owned()),
2✔
1324
              Cell::from(c.data.to_string()),
2✔
1325
              Cell::from(c.age.to_owned()),
2✔
1326
            ])
1327
            .style(style_primary(false))
2✔
1328
          },
2✔
1329
          false,
1330
          false,
1331
        );
1332
      })
1✔
1333
      .unwrap();
1✔
1334

1335
    let mut expected = Buffer::with_lines(vec![
1✔
1336
        "Test[truncated] | edit </>  | -> yaml <y>───────────────────────────────────────────────────────────",
1337
        "   Namespace                     Name                                 Data           Age            ",
1✔
1338
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
1339
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
1340
        "                                                                                                    ",
1✔
1341
        "                                                                                                    ",
1✔
1342
      ]);
1343
    // set row styles
1344
    // First row heading style
1345
    for col in 0..=99 {
100✔
1346
      match col {
100✔
1347
        0..=3 => {
100✔
1348
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
1349
            Style::default()
4✔
1350
              .fg(MACCHIATO_YELLOW)
4✔
1351
              .add_modifier(Modifier::BOLD),
4✔
1352
          );
4✔
1353
        }
4✔
1354
        4..=14 => {
96✔
1355
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
11✔
1356
            Style::default()
11✔
1357
              .fg(MACCHIATO_TEXT)
11✔
1358
              .add_modifier(Modifier::BOLD),
11✔
1359
          );
11✔
1360
        }
11✔
1361
        15..=40 => {
85✔
1362
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
26✔
1363
            Style::default()
26✔
1364
              .fg(MACCHIATO_BLUE)
26✔
1365
              .add_modifier(Modifier::BOLD),
26✔
1366
          );
26✔
1367
        }
26✔
1368
        _ => {}
59✔
1369
      }
1370
    }
1371

1372
    // Second row table header style
1373
    for col in 0..=99 {
100✔
1374
      expected
100✔
1375
        .cell_mut(Position::new(col, 1))
100✔
1376
        .unwrap()
100✔
1377
        .set_style(Style::default().fg(MACCHIATO_TEXT));
100✔
1378
    }
100✔
1379
    // first table data row style
1380
    for col in 0..=99 {
100✔
1381
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1382
        Style::default()
100✔
1383
          .fg(MACCHIATO_MAUVE)
100✔
1384
          .add_modifier(Modifier::REVERSED),
100✔
1385
      );
100✔
1386
    }
100✔
1387
    // remaining table data row style
1388
    for row in 3..=3 {
1✔
1389
      for col in 0..=99 {
100✔
1390
        expected
100✔
1391
          .cell_mut(Position::new(col, row))
100✔
1392
          .unwrap()
100✔
1393
          .set_style(Style::default().fg(MACCHIATO_MAUVE));
100✔
1394
      }
100✔
1395
    }
1396

1397
    terminal.backend().assert_buffer(&expected);
1✔
1398
  }
1✔
1399

1400
  #[test]
1401
  fn test_draw_resource_block_filter_glob() {
1✔
1402
    let backend = TestBackend::new(100, 6);
1✔
1403
    let mut terminal = Terminal::new(backend).unwrap();
1✔
1404

1405
    struct RenderTest {
1406
      pub name: String,
1407
      pub namespace: String,
1408
      pub data: i32,
1409
      pub age: String,
1410
    }
1411
    impl Named for RenderTest {
1412
      fn get_name(&self) -> &String {
3✔
1413
        &self.name
3✔
1414
      }
3✔
1415
    }
1416

1417
    terminal
1✔
1418
      .draw(|f| {
1✔
1419
        let size = f.area();
1✔
1420
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
1421
        resource.set_items(vec![
1✔
1422
          RenderTest {
1✔
1423
            name: "Test 1".into(),
1✔
1424
            namespace: "Test ns".into(),
1✔
1425
            age: "65h3m".into(),
1✔
1426
            data: 5,
1✔
1427
          },
1✔
1428
          RenderTest {
1✔
1429
            name: "Test long name that should be truncated from view".into(),
1✔
1430
            namespace: "Test ns".into(),
1✔
1431
            age: "65h3m".into(),
1✔
1432
            data: 3,
1✔
1433
          },
1✔
1434
          RenderTest {
1✔
1435
            name: "test_long_name_that_should_be_truncated_from_view".into(),
1✔
1436
            namespace: "Test ns long value check that should be truncated".into(),
1✔
1437
            age: "65h3m".into(),
1✔
1438
            data: 6,
1✔
1439
          },
1✔
1440
        ]);
1441
        resource.filter = "*long*truncated*".to_string();
1✔
1442
        draw_resource_block(
1✔
1443
          f,
1✔
1444
          size,
1✔
1445
          ResourceTableProps {
1✔
1446
            title: "Test".into(),
1✔
1447
            inline_help: help_bold_line("-> yaml <y>", false),
1✔
1448
            resource: &mut resource,
1✔
1449
            table_headers: vec!["Namespace", "Name", "Data", "Age"],
1✔
1450
            column_widths: vec![
1✔
1451
              Constraint::Percentage(30),
1✔
1452
              Constraint::Percentage(40),
1✔
1453
              Constraint::Percentage(15),
1✔
1454
              Constraint::Percentage(15),
1✔
1455
            ],
1✔
1456
          },
1✔
1457
          |c| {
2✔
1458
            Row::new(vec![
2✔
1459
              Cell::from(c.namespace.to_owned()),
2✔
1460
              Cell::from(c.name.to_owned()),
2✔
1461
              Cell::from(c.data.to_string()),
2✔
1462
              Cell::from(c.age.to_owned()),
2✔
1463
            ])
1464
            .style(style_primary(false))
2✔
1465
          },
2✔
1466
          false,
1467
          false,
1468
        );
1469
      })
1✔
1470
      .unwrap();
1✔
1471

1472
    let mut expected = Buffer::with_lines(vec![
1✔
1473
        "Test[*long*truncated*] | edit </>  | -> yaml <y>────────────────────────────────────────────────────",
1474
        "   Namespace                     Name                                 Data           Age            ",
1✔
1475
        "=> Test ns                       Test long name that should be trunca 3              65h3m          ",
1✔
1476
        "   Test ns long value check that test_long_name_that_should_be_trunca 6              65h3m          ",
1✔
1477
        "                                                                                                    ",
1✔
1478
        "                                                                                                    ",
1✔
1479
      ]);
1480
    // set row styles
1481
    // First row heading style
1482
    for col in 0..=99 {
100✔
1483
      match col {
100✔
1484
        0..=3 => {
100✔
1485
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
4✔
1486
            Style::default()
4✔
1487
              .fg(MACCHIATO_YELLOW)
4✔
1488
              .add_modifier(Modifier::BOLD),
4✔
1489
          );
4✔
1490
        }
4✔
1491
        4..=21 => {
96✔
1492
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
18✔
1493
            Style::default()
18✔
1494
              .fg(MACCHIATO_TEXT)
18✔
1495
              .add_modifier(Modifier::BOLD),
18✔
1496
          );
18✔
1497
        }
18✔
1498
        22..=47 => {
78✔
1499
          expected.cell_mut(Position::new(col, 0)).unwrap().set_style(
26✔
1500
            Style::default()
26✔
1501
              .fg(MACCHIATO_BLUE)
26✔
1502
              .add_modifier(Modifier::BOLD),
26✔
1503
          );
26✔
1504
        }
26✔
1505
        _ => {}
52✔
1506
      }
1507
    }
1508

1509
    // Second row table header style
1510
    for col in 0..=99 {
100✔
1511
      expected
100✔
1512
        .cell_mut(Position::new(col, 1))
100✔
1513
        .unwrap()
100✔
1514
        .set_style(Style::default().fg(MACCHIATO_TEXT));
100✔
1515
    }
100✔
1516
    // first table data row style
1517
    for col in 0..=99 {
100✔
1518
      expected.cell_mut(Position::new(col, 2)).unwrap().set_style(
100✔
1519
        Style::default()
100✔
1520
          .fg(MACCHIATO_MAUVE)
100✔
1521
          .add_modifier(Modifier::REVERSED),
100✔
1522
      );
100✔
1523
    }
100✔
1524
    // remaining table data row style
1525
    for row in 3..=3 {
1✔
1526
      for col in 0..=99 {
100✔
1527
        expected
100✔
1528
          .cell_mut(Position::new(col, row))
100✔
1529
          .unwrap()
100✔
1530
          .set_style(Style::default().fg(MACCHIATO_MAUVE));
100✔
1531
      }
100✔
1532
    }
1533

1534
    terminal.backend().assert_buffer(&expected);
1✔
1535
  }
1✔
1536

1537
  #[test]
1538
  fn test_get_resource_title() {
1✔
1539
    let app = App::default();
1✔
1540
    assert_eq!(
1✔
1541
      get_resource_title(&app, "Title", "-> hello", 5),
1✔
1542
      " Title (ns: all) [5] -> hello"
1543
    );
1544
  }
1✔
1545

1546
  #[test]
1547
  fn test_draw_resource_block_filter_hides_other_hints_when_active() {
1✔
1548
    let backend = TestBackend::new(100, 4);
1✔
1549
    let mut terminal = Terminal::new(backend).unwrap();
1✔
1550

1551
    struct RenderTest {
1552
      pub name: String,
1553
    }
1554

1555
    impl Named for RenderTest {
1556
      fn get_name(&self) -> &String {
1✔
1557
        &self.name
1✔
1558
      }
1✔
1559
    }
1560

1561
    terminal
1✔
1562
      .draw(|f| {
1✔
1563
        let size = f.area();
1✔
1564
        let mut resource: StatefulTable<RenderTest> = StatefulTable::new();
1✔
1565
        resource.set_items(vec![RenderTest {
1✔
1566
          name: "test".into(),
1✔
1567
        }]);
1✔
1568
        resource.filter = "pod".into();
1✔
1569
        resource.filter_active = true;
1✔
1570
        draw_resource_block(
1✔
1571
          f,
1✔
1572
          size,
1✔
1573
          ResourceTableProps {
1✔
1574
            title: "Test".into(),
1✔
1575
            inline_help: help_bold_line("describe <d> | back <Esc>", false),
1✔
1576
            resource: &mut resource,
1✔
1577
            table_headers: vec!["Name"],
1✔
1578
            column_widths: vec![Constraint::Percentage(100)],
1✔
1579
          },
1✔
UNCOV
1580
          |c| Row::new(vec![Cell::from(c.name.to_owned())]).style(style_primary(false)),
×
1581
          false,
1582
          false,
1583
        );
1584
      })
1✔
1585
      .unwrap();
1✔
1586

1587
    let first_line = (0..terminal.backend().buffer().area.width)
1✔
1588
      .map(|col| terminal.backend().buffer()[(col, 0)].symbol())
100✔
1589
      .collect::<String>();
1✔
1590
    assert!(first_line.contains("[pod]"));
1✔
1591
    assert!(first_line.contains("clear <Esc>"));
1✔
1592
    assert!(!first_line.contains("describe <d>"));
1✔
1593
    assert!(!first_line.contains("back <Esc>"));
1✔
1594
  }
1✔
1595

1596
  #[test]
1597
  fn test_title_with_ns() {
1✔
1598
    assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]");
1✔
1599
  }
1✔
1600

1601
  #[test]
1602
  fn test_get_cluster_wide_resource_title() {
1✔
1603
    assert_eq!(
1✔
1604
      get_cluster_wide_resource_title("Cluster Resource", 3, ""),
1✔
1605
      " Cluster Resource [3] "
1606
    );
1607
    assert_eq!(
1✔
1608
      get_cluster_wide_resource_title("Nodes", 10, "-> hello"),
1✔
1609
      " Nodes [10] -> hello"
1610
    );
1611
  }
1✔
1612

1613
  #[test]
1614
  fn test_build_resource_help_line() {
1✔
1615
    // Case 1: Empty inline_help, empty filter, filter_active=false
1616
    // -> line text should contain the inactive "filter <key>" action hint
1617
    let line = build_resource_help_line(Line::default(), "", false, false);
1✔
1618
    let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
1✔
1619
    let expected_filter_hint = action_hint("filter", DEFAULT_KEYBINDING.filter.key);
1✔
1620
    assert!(
1✔
1621
      text.contains(&expected_filter_hint),
1✔
1622
      "Case 1: expected '{text}' to contain '{expected_filter_hint}'"
1623
    );
1624

1625
    // Case 2: Non-empty inline_help, empty filter, filter_active=false
1626
    // -> line text should contain the inline help hint after " | "
1627
    let line2 = build_resource_help_line(help_bold_line("-> yaml <y>", false), "", false, false);
1✔
1628
    let text2: String = line2.spans.iter().map(|s| s.content.as_ref()).collect();
3✔
1629
    assert!(
1✔
1630
      text2.contains("-> yaml <y>"),
1✔
1631
      "Case 2: expected '{text2}' to contain '-> yaml <y>'"
1632
    );
1633

1634
    // Case 3: inline_help starting with the containers prefix
1635
    // -> line text should start with the containers hint
1636
    let containers_prefix_str = format!(
1✔
1637
      "{} | ",
1638
      action_hint("containers", DEFAULT_KEYBINDING.submit.key)
1✔
1639
    );
1640
    let line3 = build_resource_help_line(
1✔
1641
      help_bold_line(containers_prefix_str.as_str(), false),
1✔
1642
      "",
1✔
1643
      false,
1644
      false,
1645
    );
1646
    let text3: String = line3.spans.iter().map(|s| s.content.as_ref()).collect();
2✔
1647
    let containers_hint = action_hint("containers", DEFAULT_KEYBINDING.submit.key);
1✔
1648
    assert!(
1✔
1649
      text3.starts_with(&containers_hint),
1✔
1650
      "Case 3: expected '{text3}' to start with '{containers_hint}'"
1651
    );
1652

1653
    // Case 4: Empty inline_help, filter="foo", filter_active=false
1654
    // -> line text should contain "[foo]"
1655
    let line4 = build_resource_help_line(Line::default(), "foo", false, false);
1✔
1656
    let text4: String = line4.spans.iter().map(|s| s.content.as_ref()).collect();
2✔
1657
    assert!(
1✔
1658
      text4.contains("[foo]"),
1✔
1659
      "Case 4: expected '{text4}' to contain '[foo]'"
1660
    );
1661
  }
1✔
1662

1663
  #[test]
1664
  fn test_highlight_window_offset_within_bounds() {
1✔
1665
    // total=100, view_h=10, offset=50 → window straddles the offset.
1666
    let (start, end, scroll) = highlight_window(50, 100, 10);
1✔
1667
    assert_eq!(start, 40);
1✔
1668
    assert_eq!(end, 80);
1✔
1669
    assert_eq!(scroll, 10);
1✔
1670
    assert!(start <= end && end <= 100);
1✔
1671
  }
1✔
1672

1673
  #[test]
1674
  fn test_highlight_window_offset_at_zero() {
1✔
1675
    let (start, end, scroll) = highlight_window(0, 100, 10);
1✔
1676
    assert_eq!(start, 0);
1✔
1677
    assert_eq!(end, 30);
1✔
1678
    assert_eq!(scroll, 0);
1✔
1679
  }
1✔
1680

1681
  #[test]
1682
  fn test_highlight_window_offset_exceeds_total_does_not_panic() {
1✔
1683
    // Regression: items.len() can exceed highlighted_lines.len() when
1684
    // some lines fail to highlight, leaving offset stale relative to total.
1685
    // The slice [start..end] must remain valid.
1686
    let (start, end, _) = highlight_window(50, 5, 10);
1✔
1687
    assert!(start <= end, "start {start} must not exceed end {end}");
1✔
1688
    assert!(end <= 5, "end {end} must not exceed total");
1✔
1689
  }
1✔
1690

1691
  #[test]
1692
  fn test_highlight_window_view_h_zero_clamps_to_one() {
1✔
1693
    // A view height of 0 should not collapse the window to empty.
1694
    let (start, end, _) = highlight_window(2, 10, 0);
1✔
1695
    assert!(start < end, "window must not be empty when content exists");
1✔
1696
  }
1✔
1697

1698
  #[test]
1699
  fn test_highlight_window_total_one() {
1✔
1700
    // Single-line buffer must produce a non-empty window.
1701
    let (start, end, scroll) = highlight_window(0, 1, 5);
1✔
1702
    assert_eq!(start, 0);
1✔
1703
    assert_eq!(end, 1);
1✔
1704
    assert_eq!(scroll, 0);
1✔
1705
  }
1✔
1706
}
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