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

kdash-rs / kdash / 18585133834

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

push

github

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

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

3925 of 6843 relevant lines covered (57.36%)

7.77 hits per line

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

92.59
/src/app/models.rs
1
use std::collections::VecDeque;
2

3
use async_trait::async_trait;
4
use ratatui::{
5
  layout::Rect,
6
  style::{Modifier, Style},
7
  text::Span,
8
  widgets::{Block, List, ListItem, ListState, TableState},
9
  Frame,
10
};
11
use serde::Serialize;
12

13
use super::{ActiveBlock, App, Route};
14
use crate::network::Network;
15

16
#[async_trait]
17
pub trait AppResource {
18
  fn render(block: ActiveBlock, f: &mut Frame<'_>, app: &mut App, area: Rect);
19

20
  async fn get_resource(network: &Network<'_>);
21
}
22
pub trait KubeResource<T: Serialize> {
23
  fn get_name(&self) -> &String;
24

25
  fn get_k8s_obj(&self) -> &T;
26

27
  /// generate YAML from the original kubernetes resource
28
  fn resource_to_yaml(&self) -> String {
2✔
29
    match serde_yaml::to_string(&self.get_k8s_obj()) {
2✔
30
      Ok(yaml) => yaml,
2✔
31
      Err(_) => "".into(),
×
32
    }
33
  }
2✔
34
}
35

36
pub trait Scrollable {
37
  fn handle_scroll(&mut self, up: bool, page: bool) {
9✔
38
    // support page up/down
39
    let inc_or_dec = if page { 10 } else { 1 };
9✔
40
    if up {
9✔
41
      self.scroll_up(inc_or_dec);
4✔
42
    } else {
5✔
43
      self.scroll_down(inc_or_dec);
5✔
44
    }
5✔
45
  }
9✔
46
  fn scroll_down(&mut self, inc_or_dec: usize);
47
  fn scroll_up(&mut self, inc_or_dec: usize);
48
}
49

50
pub struct StatefulList<T> {
51
  pub state: ListState,
52
  pub items: Vec<T>,
53
}
54

55
impl<T> StatefulList<T> {
56
  pub fn new() -> StatefulList<T> {
15✔
57
    StatefulList {
15✔
58
      state: ListState::default(),
15✔
59
      items: Vec::new(),
15✔
60
    }
15✔
61
  }
15✔
62
  pub fn with_items(items: Vec<T>) -> StatefulList<T> {
15✔
63
    let mut state = ListState::default();
15✔
64
    if !items.is_empty() {
15✔
65
      state.select(Some(0));
15✔
66
    }
15✔
67
    StatefulList { state, items }
15✔
68
  }
15✔
69
}
70

71
impl<T> Scrollable for StatefulList<T> {
72
  // for lists we cycle back to the beginning when we reach the end
73
  fn scroll_down(&mut self, increment: usize) {
×
74
    let i = match self.state.selected() {
×
75
      Some(i) => {
×
76
        if i >= self.items.len().saturating_sub(increment) {
×
77
          0
×
78
        } else {
79
          i + increment
×
80
        }
81
      }
82
      None => 0,
×
83
    };
84
    self.state.select(Some(i));
×
85
  }
×
86
  // for lists we cycle back to the end when we reach the beginning
87
  fn scroll_up(&mut self, decrement: usize) {
×
88
    let i = match self.state.selected() {
×
89
      Some(i) => {
×
90
        if i == 0 {
×
91
          self.items.len().saturating_sub(decrement)
×
92
        } else {
93
          i.saturating_sub(decrement)
×
94
        }
95
      }
96
      None => 0,
×
97
    };
98
    self.state.select(Some(i));
×
99
  }
×
100
}
101

102
#[derive(Clone, Debug)]
103
pub struct StatefulTable<T> {
104
  pub state: TableState,
105
  pub items: Vec<T>,
106
}
107

108
impl<T> StatefulTable<T> {
109
  pub fn new() -> StatefulTable<T> {
426✔
110
    StatefulTable {
426✔
111
      state: TableState::default(),
426✔
112
      items: Vec::new(),
426✔
113
    }
426✔
114
  }
426✔
115

116
  pub fn with_items(items: Vec<T>) -> StatefulTable<T> {
16✔
117
    let mut table = StatefulTable::new();
16✔
118
    if !items.is_empty() {
16✔
119
      table.state.select(Some(0));
16✔
120
    }
16✔
121
    table.set_items(items);
16✔
122
    table
16✔
123
  }
16✔
124

125
  pub fn set_items(&mut self, items: Vec<T>) {
26✔
126
    let item_len = items.len();
26✔
127
    self.items = items;
26✔
128
    if !self.items.is_empty() {
26✔
129
      let i = self.state.selected().map_or(0, |i| {
26✔
130
        if i > 0 && i < item_len {
18✔
131
          i
1✔
132
        } else if i >= item_len {
17✔
133
          item_len - 1
1✔
134
        } else {
135
          0
16✔
136
        }
137
      });
18✔
138
      self.state.select(Some(i));
26✔
139
    }
×
140
  }
26✔
141
}
142

143
impl<T> Scrollable for StatefulTable<T> {
144
  fn scroll_down(&mut self, increment: usize) {
8✔
145
    if let Some(i) = self.state.selected() {
8✔
146
      if (i + increment) < self.items.len() {
8✔
147
        self.state.select(Some(i + increment));
4✔
148
      } else {
4✔
149
        self.state.select(Some(self.items.len().saturating_sub(1)));
4✔
150
      }
4✔
151
    }
×
152
  }
8✔
153

154
  fn scroll_up(&mut self, decrement: usize) {
5✔
155
    if let Some(i) = self.state.selected() {
5✔
156
      if i != 0 {
5✔
157
        self.state.select(Some(i.saturating_sub(decrement)));
4✔
158
      }
4✔
159
    }
×
160
  }
5✔
161
}
162

163
impl<T: Clone> StatefulTable<T> {
164
  /// a clone of the currently selected item.
165
  /// for mutable ref use state.selected() and fetch from items when needed
166
  pub fn get_selected_item_copy(&self) -> Option<T> {
1✔
167
    if !self.items.is_empty() {
1✔
168
      self.state.selected().map(|i| self.items[i].clone())
1✔
169
    } else {
170
      None
×
171
    }
172
  }
1✔
173
}
174

175
#[derive(Clone)]
176
pub struct TabRoute {
177
  pub title: String,
178
  pub route: Route,
179
}
180

181
pub struct TabsState {
182
  pub items: Vec<TabRoute>,
183
  pub index: usize,
184
}
185

186
impl TabsState {
187
  pub fn new(items: Vec<TabRoute>) -> TabsState {
31✔
188
    TabsState { items, index: 0 }
31✔
189
  }
31✔
190
  pub fn set_index(&mut self, index: usize) -> &TabRoute {
5✔
191
    self.index = index;
5✔
192
    &self.items[self.index]
5✔
193
  }
5✔
194
  pub fn get_active_route(&self) -> &Route {
5✔
195
    &self.items[self.index].route
5✔
196
  }
5✔
197

198
  pub fn next(&mut self) {
2✔
199
    self.index = (self.index + 1) % self.items.len();
2✔
200
  }
2✔
201
  pub fn previous(&mut self) {
2✔
202
    if self.index > 0 {
2✔
203
      self.index -= 1;
1✔
204
    } else {
1✔
205
      self.index = self.items.len() - 1;
1✔
206
    }
1✔
207
  }
2✔
208
}
209

210
#[derive(Debug, Eq, PartialEq)]
211
pub struct ScrollableTxt {
212
  items: Vec<String>,
213
  pub offset: u16,
214
}
215

216
impl ScrollableTxt {
217
  pub fn new() -> ScrollableTxt {
16✔
218
    ScrollableTxt {
16✔
219
      items: vec![],
16✔
220
      offset: 0,
16✔
221
    }
16✔
222
  }
16✔
223

224
  pub fn with_string(item: String) -> ScrollableTxt {
4✔
225
    let items: Vec<&str> = item.split('\n').collect();
4✔
226
    let items: Vec<String> = items.iter().map(|it| it.to_string()).collect();
26✔
227
    ScrollableTxt { items, offset: 0 }
4✔
228
  }
4✔
229

230
  pub fn get_txt(&self) -> String {
5✔
231
    self.items.join("\n")
5✔
232
  }
5✔
233
}
234

235
impl Scrollable for ScrollableTxt {
236
  fn scroll_down(&mut self, increment: usize) {
5✔
237
    // scroll only if offset is less than total lines in text
238
    // we subtract increment + 2 to keep the text in view. Its just an arbitrary number that works
239
    if self.offset < self.items.len().saturating_sub(increment + 2) as u16 {
5✔
240
      self.offset += increment as u16;
3✔
241
    }
3✔
242
  }
5✔
243
  fn scroll_up(&mut self, decrement: usize) {
3✔
244
    // scroll up and avoid going negative
245
    if self.offset > 0 {
3✔
246
      self.offset = self.offset.saturating_sub(decrement as u16);
2✔
247
    }
2✔
248
  }
3✔
249
}
250

251
// TODO implement line buffer to avoid gathering too much data in memory
252
#[derive(Debug, Clone)]
253
pub struct LogsState {
254
  /// Stores the log messages to be displayed
255
  ///
256
  /// (original_message, (wrapped_message, wrapped_at_width))
257
  #[allow(clippy::type_complexity)]
258
  records: VecDeque<(String, Option<(Vec<ListItem<'static>>, u16)>)>,
259
  wrapped_length: usize,
260
  pub state: ListState,
261
  pub id: String,
262
}
263

264
impl LogsState {
265
  pub fn new(id: String) -> LogsState {
16✔
266
    LogsState {
16✔
267
      records: VecDeque::with_capacity(512),
16✔
268
      state: ListState::default(),
16✔
269
      wrapped_length: 0,
16✔
270
      id,
16✔
271
    }
16✔
272
  }
16✔
273

274
  /// get a plain text version of the logs
275
  pub fn get_plain_text(&self) -> String {
1✔
276
    self.records.iter().fold(String::new(), |mut acc, v| {
2✔
277
      acc.push('\n');
2✔
278
      acc.push_str(v.0.as_str());
2✔
279
      acc
2✔
280
    })
2✔
281
  }
1✔
282

283
  /// Render the current state as a list widget
284
  pub fn render_list(
5✔
285
    &mut self,
5✔
286
    f: &mut Frame<'_>,
5✔
287
    logs_area: Rect,
5✔
288
    block: Block<'_>,
5✔
289
    style: Style,
5✔
290
    follow: bool,
5✔
291
  ) {
5✔
292
    let available_lines = logs_area.height as usize;
5✔
293
    let logs_area_width = logs_area.width as usize;
5✔
294

295
    let num_records = self.records.len();
5✔
296
    // Keep track of the number of lines after wrapping so we can skip lines as
297
    // needed below
298
    let mut wrapped_lines_len = 0;
5✔
299

300
    let mut items = Vec::with_capacity(logs_area.height as usize);
5✔
301

302
    let lines_to_skip = if follow {
5✔
303
      self.unselect();
2✔
304
      num_records.saturating_sub(available_lines)
2✔
305
    } else {
306
      0
3✔
307
    };
308

309
    items.extend(
5✔
310
      self
5✔
311
        .records
5✔
312
        .iter_mut()
5✔
313
        // Only wrap the records we could potentially be displaying
314
        .skip(lines_to_skip)
5✔
315
        .flat_map(|r| {
41✔
316
          // See if we can use a cached wrapped line
317
          if let Some(wrapped) = &r.1 {
41✔
318
            if wrapped.1 as usize == logs_area_width {
31✔
319
              wrapped_lines_len += wrapped.0.len();
31✔
320
              return wrapped.0.clone();
31✔
321
            }
×
322
          }
10✔
323

324
          // If not, wrap the line and cache it
325
          r.1 = Some((
10✔
326
            textwrap::wrap(r.0.as_ref(), logs_area_width)
10✔
327
              .into_iter()
10✔
328
              .map(|s| s.to_string())
15✔
329
              .map(|c| Span::styled(c, style))
15✔
330
              .map(ListItem::new)
10✔
331
              .collect::<Vec<ListItem<'_>>>(),
10✔
332
            logs_area.width,
10✔
333
          ));
334

335
          wrapped_lines_len += r.1.as_ref().unwrap().0.len();
10✔
336
          r.1.as_ref().unwrap().0.clone()
10✔
337
        }),
41✔
338
    );
339

340
    let wrapped_lines_to_skip = if follow {
5✔
341
      wrapped_lines_len.saturating_sub(available_lines)
2✔
342
    } else {
343
      0
3✔
344
    };
345

346
    let items = items
5✔
347
      .into_iter()
5✔
348
      // Wrapping could have created more lines than what we can display;
349
      // skip them
350
      .skip(wrapped_lines_to_skip)
5✔
351
      .collect::<Vec<_>>();
5✔
352

353
    self.wrapped_length = items.len();
5✔
354

355
    // TODO: All this is a workaround. we should be wrapping text with paragraph, but it currently
356
    // doesn't support wrapping and staying scrolled to the bottom
357
    //
358
    // see https://github.com/fdehau/tui-rs/issues/89
359
    let list = List::new(items)
5✔
360
      .block(block)
5✔
361
      .highlight_style(Style::default().add_modifier(Modifier::BOLD));
5✔
362

363
    f.render_stateful_widget(list, logs_area, &mut self.state);
5✔
364
  }
5✔
365
  /// Add a record to be displayed
366
  pub fn add_record(&mut self, record: String) {
13✔
367
    self.records.push_back((record, None));
13✔
368
  }
13✔
369

370
  fn unselect(&mut self) {
2✔
371
    self.state.select(None);
2✔
372
  }
2✔
373
}
374

375
impl Scrollable for LogsState {
376
  fn scroll_down(&mut self, increment: usize) {
1✔
377
    let i = self.state.selected().map_or(0, |i| {
1✔
378
      if i >= self.wrapped_length.saturating_sub(increment) {
1✔
379
        i
×
380
      } else {
381
        i + increment
1✔
382
      }
383
    });
1✔
384
    self.state.select(Some(i));
1✔
385
  }
1✔
386

387
  fn scroll_up(&mut self, decrement: usize) {
2✔
388
    let i = self.state.selected().map_or(0, |i| {
2✔
389
      if i != 0 {
×
390
        i.saturating_sub(decrement)
×
391
      } else {
392
        0
×
393
      }
394
    });
×
395
    self.state.select(Some(i));
2✔
396
  }
2✔
397
}
398

399
#[cfg(test)]
400
mod tests {
401
  use k8s_openapi::api::core::v1::Namespace;
402
  use kube::api::ObjectMeta;
403
  use ratatui::{backend::TestBackend, buffer::Buffer, layout::Position, Terminal};
404

405
  use super::*;
406
  use crate::app::{ns::KubeNs, ActiveBlock, RouteId};
407

408
  #[test]
409
  fn test_kube_resource() {
1✔
410
    struct TestStruct {
411
      name: String,
412
      k8s_obj: Namespace,
413
    }
414
    impl KubeResource<Namespace> for TestStruct {
415
      fn get_name(&self) -> &String {
×
416
        &self.name
×
417
      }
×
418
      fn get_k8s_obj(&self) -> &Namespace {
1✔
419
        &self.k8s_obj
1✔
420
      }
1✔
421
    }
422
    let ts = TestStruct {
1✔
423
      name: "test".into(),
1✔
424
      k8s_obj: Namespace {
1✔
425
        metadata: ObjectMeta {
1✔
426
          name: Some("test".into()),
1✔
427
          namespace: Some("test".into()),
1✔
428
          ..ObjectMeta::default()
1✔
429
        },
1✔
430
        ..Namespace::default()
1✔
431
      },
1✔
432
    };
1✔
433
    assert_eq!(
1✔
434
      ts.resource_to_yaml(),
1✔
435
      "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: test\n  namespace: test\n"
436
    )
437
  }
1✔
438

439
  #[test]
440
  fn test_stateful_table() {
1✔
441
    let mut sft: StatefulTable<KubeNs> = StatefulTable::new();
1✔
442

443
    assert_eq!(sft.items.len(), 0);
1✔
444
    assert_eq!(sft.state.selected(), None);
1✔
445
    // check default selection on set
446
    sft.set_items(vec![KubeNs::default(), KubeNs::default()]);
1✔
447
    assert_eq!(sft.items.len(), 2);
1✔
448
    assert_eq!(sft.state.selected(), Some(0));
1✔
449
    // check selection retain on set
450
    sft.state.select(Some(1));
1✔
451
    sft.set_items(vec![
1✔
452
      KubeNs::default(),
1✔
453
      KubeNs::default(),
1✔
454
      KubeNs::default(),
1✔
455
    ]);
456
    assert_eq!(sft.items.len(), 3);
1✔
457
    assert_eq!(sft.state.selected(), Some(1));
1✔
458
    // check selection overflow prevention
459
    sft.state.select(Some(2));
1✔
460
    sft.set_items(vec![KubeNs::default(), KubeNs::default()]);
1✔
461
    assert_eq!(sft.items.len(), 2);
1✔
462
    assert_eq!(sft.state.selected(), Some(1));
1✔
463
    // check scroll down
464
    sft.state.select(Some(0));
1✔
465
    assert_eq!(sft.state.selected(), Some(0));
1✔
466
    sft.scroll_down(1);
1✔
467
    assert_eq!(sft.state.selected(), Some(1));
1✔
468
    // check scroll overflow
469
    sft.scroll_down(1);
1✔
470
    assert_eq!(sft.state.selected(), Some(1));
1✔
471
    sft.scroll_up(1);
1✔
472
    assert_eq!(sft.state.selected(), Some(0));
1✔
473
    // check scroll overflow
474
    sft.scroll_up(1);
1✔
475
    assert_eq!(sft.state.selected(), Some(0));
1✔
476
    // check increment
477
    sft.scroll_down(10);
1✔
478
    assert_eq!(sft.state.selected(), Some(1));
1✔
479

480
    let sft2 = StatefulTable::with_items(vec![KubeNs::default(), KubeNs::default()]);
1✔
481
    assert_eq!(sft2.state.selected(), Some(0));
1✔
482
  }
1✔
483

484
  #[test]
485
  fn test_handle_table_scroll() {
1✔
486
    let mut item: StatefulTable<&str> = StatefulTable::new();
1✔
487
    item.set_items(vec!["A", "B", "C"]);
1✔
488

489
    assert_eq!(item.state.selected(), Some(0));
1✔
490

491
    item.handle_scroll(false, false);
1✔
492
    assert_eq!(item.state.selected(), Some(1));
1✔
493

494
    item.handle_scroll(false, false);
1✔
495
    assert_eq!(item.state.selected(), Some(2));
1✔
496

497
    item.handle_scroll(false, false);
1✔
498
    assert_eq!(item.state.selected(), Some(2));
1✔
499
    // previous
500
    item.handle_scroll(true, false);
1✔
501
    assert_eq!(item.state.selected(), Some(1));
1✔
502
    // page down
503
    item.handle_scroll(false, true);
1✔
504
    assert_eq!(item.state.selected(), Some(2));
1✔
505
    // page up
506
    item.handle_scroll(true, true);
1✔
507
    assert_eq!(item.state.selected(), Some(0));
1✔
508
  }
1✔
509

510
  #[test]
511
  fn test_stateful_tab() {
1✔
512
    let mut tab = TabsState::new(vec![
1✔
513
      TabRoute {
1✔
514
        title: "Hello".into(),
1✔
515
        route: Route {
1✔
516
          active_block: ActiveBlock::Pods,
1✔
517
          id: RouteId::Home,
1✔
518
        },
1✔
519
      },
1✔
520
      TabRoute {
1✔
521
        title: "Test".into(),
1✔
522
        route: Route {
1✔
523
          active_block: ActiveBlock::Nodes,
1✔
524
          id: RouteId::Home,
1✔
525
        },
1✔
526
      },
1✔
527
    ]);
528

529
    assert_eq!(tab.index, 0);
1✔
530
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Pods);
1✔
531
    tab.next();
1✔
532
    assert_eq!(tab.index, 1);
1✔
533
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Nodes);
1✔
534
    tab.next();
1✔
535
    assert_eq!(tab.index, 0);
1✔
536
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Pods);
1✔
537
    tab.previous();
1✔
538
    assert_eq!(tab.index, 1);
1✔
539
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Nodes);
1✔
540
    tab.previous();
1✔
541
    assert_eq!(tab.index, 0);
1✔
542
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Pods);
1✔
543
  }
1✔
544

545
  #[test]
546
  fn test_scrollable_txt() {
1✔
547
    let mut stxt = ScrollableTxt::with_string("test\n multiline\n string".into());
1✔
548

549
    assert_eq!(stxt.offset, 0);
1✔
550
    assert_eq!(stxt.items.len(), 3);
1✔
551

552
    assert_eq!(stxt.get_txt(), "test\n multiline\n string");
1✔
553

554
    stxt.scroll_down(1);
1✔
555
    assert_eq!(stxt.offset, 0);
1✔
556

557
    let mut stxt2 = ScrollableTxt::with_string("te\nst\nmul\ntil\ni\nne\nstr\ni\nn\ng".into());
1✔
558
    assert_eq!(stxt2.items.len(), 10);
1✔
559
    stxt2.scroll_down(1);
1✔
560
    assert_eq!(stxt2.offset, 1);
1✔
561
    stxt2.scroll_down(1);
1✔
562
    assert_eq!(stxt2.offset, 2);
1✔
563
    stxt2.scroll_down(5);
1✔
564
    assert_eq!(stxt2.offset, 7);
1✔
565
    stxt2.scroll_down(1);
1✔
566
    // no overflow past (len - 2)
567
    assert_eq!(stxt2.offset, 7);
1✔
568
    stxt2.scroll_up(1);
1✔
569
    assert_eq!(stxt2.offset, 6);
1✔
570
    stxt2.scroll_up(6);
1✔
571
    assert_eq!(stxt2.offset, 0);
1✔
572
    stxt2.scroll_up(1);
1✔
573
    // no overflow past (0)
574
    assert_eq!(stxt2.offset, 0);
1✔
575
  }
1✔
576

577
  #[test]
578
  fn test_logs_state() {
1✔
579
    let mut log = LogsState::new("1".into());
1✔
580
    log.add_record("record 1".into());
1✔
581
    log.add_record("record 2".into());
1✔
582

583
    assert_eq!(log.get_plain_text(), "\nrecord 1\nrecord 2");
1✔
584

585
    let backend = TestBackend::new(20, 7);
1✔
586
    let mut terminal = Terminal::new(backend).unwrap();
1✔
587

588
    log.add_record("record 4 should be long enough to be wrapped".into());
1✔
589
    log.add_record("record 5".into());
1✔
590
    log.add_record("record 6".into());
1✔
591
    log.add_record("record 7".into());
1✔
592
    log.add_record("record 8".into());
1✔
593

594
    terminal
1✔
595
      .draw(|f| log.render_list(f, f.area(), Block::default(), Style::default(), true))
1✔
596
      .unwrap();
1✔
597

598
    let expected = Buffer::with_lines(vec![
1✔
599
      "record 4 should be  ",
600
      "long enough to be   ",
1✔
601
      "wrapped             ",
1✔
602
      "record 5            ",
1✔
603
      "record 6            ",
1✔
604
      "record 7            ",
1✔
605
      "record 8            ",
1✔
606
    ]);
607

608
    terminal.backend().assert_buffer(&expected);
1✔
609

610
    terminal
1✔
611
      .draw(|f| log.render_list(f, f.area(), Block::default(), Style::default(), false))
1✔
612
      .unwrap();
1✔
613

614
    let expected2 = Buffer::with_lines(vec![
1✔
615
      "record 1            ",
616
      "record 2            ",
1✔
617
      "record 4 should be  ",
1✔
618
      "long enough to be   ",
1✔
619
      "wrapped             ",
1✔
620
      "record 5            ",
1✔
621
      "record 6            ",
1✔
622
    ]);
623

624
    terminal.backend().assert_buffer(&expected2);
1✔
625

626
    log.add_record("record 9".into());
1✔
627
    log.add_record("record 10 which is again looooooooooooooooooooooooooooooonnnng".into());
1✔
628
    log.add_record("record 11".into());
1✔
629
    // enabling follow should scroll back to bottom
630
    terminal
1✔
631
      .draw(|f| log.render_list(f, f.area(), Block::default(), Style::default(), true))
1✔
632
      .unwrap();
1✔
633

634
    let expected3 = Buffer::with_lines(vec![
1✔
635
      "record 8            ",
636
      "record 9            ",
1✔
637
      "record 10           ",
1✔
638
      "which is again      ",
1✔
639
      "looooooooooooooooooo",
1✔
640
      "oooooooooooonnnng   ",
1✔
641
      "record 11           ",
1✔
642
    ]);
643

644
    terminal.backend().assert_buffer(&expected3);
1✔
645

646
    terminal
1✔
647
      .draw(|f| log.render_list(f, f.area(), Block::default(), Style::default(), false))
1✔
648
      .unwrap();
1✔
649

650
    let expected4 = Buffer::with_lines(vec![
1✔
651
      "record 1            ",
652
      "record 2            ",
1✔
653
      "record 4 should be  ",
1✔
654
      "long enough to be   ",
1✔
655
      "wrapped             ",
1✔
656
      "record 5            ",
1✔
657
      "record 6            ",
1✔
658
    ]);
659

660
    terminal.backend().assert_buffer(&expected4);
1✔
661

662
    log.scroll_up(1); // to reset select state
1✔
663
    log.scroll_down(11);
1✔
664

665
    terminal
1✔
666
      .draw(|f| log.render_list(f, f.area(), Block::default(), Style::default(), false))
1✔
667
      .unwrap();
1✔
668

669
    let mut expected5 = Buffer::with_lines(vec![
1✔
670
      "record 5            ",
671
      "record 6            ",
1✔
672
      "record 7            ",
1✔
673
      "record 8            ",
1✔
674
      "record 9            ",
1✔
675
      "record 10           ",
1✔
676
      "which is again      ",
1✔
677
    ]);
678

679
    // Second row table header style
680
    for col in 0..=19 {
21✔
681
      expected5
20✔
682
        .cell_mut(Position::new(col, 6))
20✔
683
        .unwrap()
20✔
684
        .set_style(Style::default().add_modifier(Modifier::BOLD));
20✔
685
    }
20✔
686

687
    terminal.backend().assert_buffer(&expected5);
1✔
688
  }
1✔
689
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc