tlc: status bar clock + dialog scrollbars on help/jobs/find

Status bar now shows wall clock (HH:MM UTC) and spinner character
right-aligned. Native ratatui scrollbars added to Help, Jobs, and Find
dialogs when content overflows visible area.
This commit is contained in:
2026-06-20 00:07:30 +03:00
parent 135439d763
commit b4a0d68f66
4 changed files with 101 additions and 27 deletions
@@ -27,7 +27,7 @@ use std::sync::Arc;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{List, ListItem, Paragraph, Wrap};
use ratatui::widgets::{List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap};
use ratatui::Frame;
use walkdir::WalkDir;
@@ -344,6 +344,19 @@ impl FindDialog {
let list = List::new(items);
frame.render_widget(list, chunks[2]);
if self.results.len() > visible_h {
let pos = self.cursor.min(self.results.len().saturating_sub(visible_h));
let mut sb_state = ScrollbarState::new(self.results.len())
.viewport_content_length(visible_h)
.position(pos);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(Style::default().fg(theme.border)),
chunks[2],
&mut sb_state,
);
}
// Hint + status.
let status: &str = match self.last_error.as_deref() {
Some(e) => e,
@@ -17,7 +17,7 @@
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{List, ListItem, Paragraph};
use ratatui::widgets::{List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState};
use ratatui::Frame;
use crate::key::Key;
@@ -380,6 +380,17 @@ impl HelpDialog {
.bg(body_bg),
);
frame.render_widget(list, chunks[0]);
let total = self.bindings.len();
let mut sb_state = ScrollbarState::new(total)
.viewport_content_length(visible)
.position(offset);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(Style::default().fg(self.theme.border)),
chunks[0],
&mut sb_state,
);
}
// Hint line.
@@ -42,7 +42,7 @@ use std::thread;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Wrap};
use ratatui::widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap};
use ratatui::Frame;
use crate::key::Key;
@@ -743,6 +743,18 @@ impl JobsDialog {
})
.collect();
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), chunks[1]);
if n > visible {
let mut sb_state = ScrollbarState::new(n)
.viewport_content_length(visible)
.position(start);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(Style::default().fg(theme.border)),
chunks[1],
&mut sb_state,
);
}
}
let hint = Line::from(vec![
@@ -9,7 +9,7 @@ use std::time::{Duration, Instant};
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Wrap};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use super::color::Theme;
@@ -53,6 +53,8 @@ pub struct StatusLine {
message: Option<Message>,
/// Hint text for the active command (F-key tooltip).
hint: String,
/// Spinner character shown at the right edge when jobs are active.
spinner_char: String,
}
impl StatusLine {
@@ -86,35 +88,71 @@ impl StatusLine {
self.hint = hint.into();
}
/// Set the spinner character shown at the right edge of the status bar.
pub fn set_spinner_char(&mut self, ch: String) {
self.spinner_char = ch;
}
/// Render the status line into a frame.
pub fn render(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let status_pair = mc_skin::color_pair(theme.name, "statusbar", "_default_");
let status_fg = status_pair.map(|p| p.fg).unwrap_or(theme.status_fg);
let status_bg = status_pair.map(|p| p.bg).unwrap_or(theme.status_bg);
let now_message = self.message.as_ref().filter(|m| !m.expired());
let line = match now_message {
Some(m) => Line::from(vec![
Span::styled(
m.text(),
Style::default()
.fg(status_fg)
.bg(status_bg)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
&self.hint,
Style::default().fg(status_fg).bg(status_bg),
),
]),
None => Line::from(Span::styled(
&self.hint,
Style::default().fg(status_fg).bg(status_bg),
)),
};
let para = Paragraph::new(line)
.style(Style::default().fg(status_fg).bg(status_bg))
.wrap(Wrap { trim: false });
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let clock = format!(
"{:02}:{:02}",
(secs / 3600) % 24,
(secs / 60) % 60
);
let mut left_spans: Vec<Span> = Vec::new();
if let Some(m) = now_message {
left_spans.push(Span::styled(
m.text(),
Style::default()
.fg(status_fg)
.bg(status_bg)
.add_modifier(Modifier::BOLD),
));
left_spans.push(Span::raw(" "));
}
left_spans.push(Span::styled(
&self.hint,
Style::default().fg(status_fg).bg(status_bg),
));
let mut right_spans: Vec<Span> = Vec::new();
if !self.spinner_char.is_empty() {
right_spans.push(Span::styled(
self.spinner_char.clone(),
Style::default().fg(theme.info).bg(status_bg),
));
right_spans.push(Span::raw(" "));
}
right_spans.push(Span::styled(
clock,
Style::default().fg(status_fg).bg(status_bg),
));
let left_w: usize = left_spans.iter().map(|s| s.width()).sum();
let right_w: usize = right_spans.iter().map(|s| s.width()).sum();
let avail = area.width as usize;
let pad = avail.saturating_sub(left_w + right_w);
let mut all_spans = left_spans;
all_spans.push(Span::styled(
" ".repeat(pad),
Style::default().bg(status_bg),
));
all_spans.extend(right_spans);
let para = Paragraph::new(Line::from(all_spans))
.style(Style::default().fg(status_fg).bg(status_bg));
frame.render_widget(para, area);
}
}