use std::ops::ControlFlow;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread;
use std::time::Duration;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use tuika::prelude::*;
const FOOTER_ROWS: u16 = 5;
struct Status {
published: u64,
paused: bool,
}
fn footer(status: &Status, frame: u64, theme: &Theme) -> Element {
let state = if status.paused { "paused" } else { "watching" };
let head = Line::from(vec![
Span::styled(
format!("{} ", Spinner::new(frame).glyph()),
Style::default().fg(theme.accent),
),
Span::styled(
format!("{state} "),
Style::default().add_modifier(Modifier::BOLD),
),
Span::styled(
format!("ยท {} lines published", status.published),
Style::default().fg(theme.muted),
),
]);
element(
Flex::column()
.child(
Dimension::Flex(1),
element(
Boxed::new(element(Text::new(vec![
head,
Line::from(Span::styled(
"output above this box is ordinary terminal scrollback",
Style::default().fg(theme.dim),
)),
])))
.title(" split footer "),
),
)
.child(
Dimension::Fixed(1),
element(KeyHints::new([
("space", "publish now"),
("p", "pause"),
("q", "quit"),
])),
),
)
}
fn publish(scrollback: &Scrollback, seq: u64, theme: &Theme) {
let accent = theme.accent;
let muted = theme.muted;
scrollback.write(move |_width| {
element(Text::new(vec![Line::from(vec![
Span::styled(format!("[{seq:04}] "), Style::default().fg(muted)),
Span::styled("build", Style::default().fg(accent)),
Span::raw(format!(" finished in {} ms", 40 + seq * 3 % 90)),
])]))
});
}
fn main() -> std::io::Result<()> {
let capture_mouse = std::env::args().any(|arg| arg == "--mouse");
let mode = ScreenMode::split_footer(FOOTER_ROWS);
let mode = if capture_mouse {
mode.with_mouse_capture()
} else {
mode
};
let theme = Theme::default();
let runner = Runner::new(RunnerConfig {
tick_rate: Duration::from_millis(80),
screen_mode: mode,
});
let scrollback = runner.scrollback();
let status = Live::with_redraw(
Status {
published: 0,
paused: false,
},
runner.redraw_handle(),
);
let stop = Arc::new(AtomicBool::new(false));
let seq = Arc::new(AtomicU64::new(1));
let worker = thread::spawn({
let (stop, seq) = (Arc::clone(&stop), Arc::clone(&seq));
let (scrollback, status) = (scrollback.clone(), status.clone());
move || {
while !stop.load(Ordering::Acquire) {
thread::sleep(Duration::from_millis(700));
if status.with(|status| status.paused) {
continue;
}
publish(&scrollback, seq.fetch_add(1, Ordering::AcqRel), &theme);
status.update(|status| status.published += 1);
}
}
});
let view_status = status.clone();
let result = runner.run(
&theme,
move |frame| {
element(LiveView::new(view_status.clone(), move |status| {
footer(status, frame, &theme)
}))
},
|event| match event {
Event::Key(key) if key.plain() => match key.code {
KeyCode::Char('q') | KeyCode::Esc => ControlFlow::Break(()),
KeyCode::Char('p') => {
status.update(|status| status.paused = !status.paused);
ControlFlow::Continue(())
}
KeyCode::Char(' ') | KeyCode::Enter => {
publish(&scrollback, seq.fetch_add(1, Ordering::AcqRel), &theme);
status.update(|status| status.published += 1);
ControlFlow::Continue(())
}
_ => ControlFlow::Continue(()),
},
_ => ControlFlow::Continue(()),
},
);
stop.store(true, Ordering::Release);
let _ = worker.join();
result
}