use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use scrin::animation::{Animation, AnimationClock};
use scrin::command_palette::{Command, CommandPalette};
use scrin::core::buffer::Buffer;
use scrin::core::color::Color;
use scrin::core::rect::Rect;
use scrin::effects::easing::EasingFn;
use scrin::effects::{EffectPlayer, LoaderPlayer};
use scrin::input::{AppAction, EventRouter};
use scrin::layout::{Constraint, Layout};
use scrin::overlays::toast::{self, ToastKind};
use scrin::overlays::{Modal, Overlay};
use scrin::status_bar::{StatusBar, StatusBarPosition};
use scrin::widgets::barchart::{Bar, BarChart};
use scrin::widgets::block::{Block, BorderStyle};
use scrin::widgets::gauge::Gauge;
use scrin::widgets::list::{List, ListItem};
use scrin::widgets::scrollbar::{ScrollBar, ScrollBarOrientation};
use scrin::widgets::sparkline::Sparkline;
use scrin::widgets::tabs::Tabs;
use scrin::widgets::Widget;
use std::io::{self, Write};
use std::time::{Duration, Instant};
fn main() -> io::Result<()> {
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
let result = run(&mut stdout);
execute!(stdout, cursor::Show, LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
result
}
fn run(stdout: &mut io::Stdout) -> io::Result<()> {
let mut router = EventRouter::new();
router.bind(
KeyModifiers::CONTROL,
KeyCode::Char('n'),
AppAction::TabNext,
"Next Tab",
);
let mut palette = CommandPalette::new().with_commands(vec![
Command {
name: "New File".into(),
shortcut: Some("Ctrl+N".into()),
description: "Create a new file".into(),
action_id: "new_file".into(),
},
Command {
name: "Open File".into(),
shortcut: Some("Ctrl+O".into()),
description: "Open an existing file".into(),
action_id: "open_file".into(),
},
Command {
name: "Save".into(),
shortcut: Some("Ctrl+S".into()),
description: "Save current file".into(),
action_id: "save".into(),
},
Command {
name: "Search".into(),
shortcut: Some("Ctrl+F".into()),
description: "Search in files".into(),
action_id: "search".into(),
},
Command {
name: "Settings".into(),
shortcut: Some("Ctrl+,".into()),
description: "Open settings".into(),
action_id: "settings".into(),
},
Command {
name: "Quit".into(),
shortcut: Some("Ctrl+Q".into()),
description: "Exit the application".into(),
action_id: "quit".into(),
},
]);
let mut status = StatusBar::new().with_position(StatusBarPosition::Bottom);
let tab_names = &["Dashboard", "Effects", "Loaders", "Widgets"];
let mut tab_index: usize = 0;
let mut modal = Modal::new("Welcome to scrin", "A production-grade TUI library")
.with_options(vec!["Explore".into(), "Settings".into(), "Dismiss".into()]);
modal.show();
let kinds = EffectPlayer::all_kinds();
let mut effect_index = 0;
let effect_text = "scrin";
let mut effect_tick: usize = 0;
let all_loader_kinds = LoaderPlayer::all_kinds();
let mut loader_progress: f32 = 0.0;
let mut loader_progress_dir: f32 = 0.015;
let mut loader_tick: usize = 0;
let mut progress: f32 = 0.0;
let mut progress_dir: f32 = 0.004;
let mut spark_data: Vec<u64> = vec![3, 5, 7, 4, 6, 8, 5, 7, 9, 6, 8, 10, 7, 9, 11, 8];
let mut clock = AnimationClock::new(60.0);
let mut glow_anim = Animation::new(0.0, 1.0, Duration::from_secs(2))
.with_easing(EasingFn::InOutSine)
.with_loop()
.with_alternate();
let mut last_tick = Instant::now();
let mut frame_count: u64 = 0;
let mut fps: f64 = 0.0;
let mut fps_timer = Instant::now();
let mut fps_counter: u64 = 0;
let mut toasts: Vec<toast::Toast> = Vec::new();
loop {
let now = Instant::now();
let delta = now.duration_since(last_tick);
last_tick = now;
frame_count += 1;
fps_counter += 1;
clock.tick();
glow_anim.update(delta);
if fps_timer.elapsed() >= Duration::from_secs(1) {
fps = fps_counter as f64 / fps_timer.elapsed().as_secs_f64();
fps_counter = 0;
fps_timer = Instant::now();
}
for t in &mut toasts {
t.update(delta);
}
toasts.retain(|t| !t.is_expired());
progress += progress_dir;
if progress >= 1.0 {
progress = 1.0;
progress_dir = -0.003;
} else if progress <= 0.0 {
progress = 0.0;
progress_dir = 0.004;
}
loader_progress += loader_progress_dir;
if loader_progress >= 1.0 {
loader_progress = 1.0;
loader_progress_dir = -0.01;
} else if loader_progress <= 0.0 {
loader_progress = 0.0;
loader_progress_dir = 0.015;
}
spark_data.push((progress * 15.0) as u64);
if spark_data.len() > 32 {
spark_data.remove(0);
}
effect_tick = effect_tick.wrapping_add(1)
% EffectPlayer::new(kinds[effect_index], effect_text)
.total_frames()
.max(1);
loader_tick = loader_tick.wrapping_add(1);
let (cols, rows) = terminal::size()?;
let mut buffer = Buffer::new(cols as usize, rows as usize);
let glow_intensity = glow_anim.current;
let accent = Color::rgb(88, 166, 255).brighten(glow_intensity * 0.3);
let main_layout = Layout::vertical(vec![
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let main_rects = main_layout.split(Rect::new(0, 0, cols, rows));
{
let tabs = Tabs::new(tab_names).with_selected(tab_index);
tabs.render(&mut buffer, main_rects[0]);
match tab_index {
0 => render_dashboard(
&mut buffer,
main_rects[1],
progress,
&spark_data,
fps,
frame_count,
glow_intensity,
),
1 => render_effects_tab(
&mut buffer,
main_rects[1],
kinds[effect_index],
effect_text,
effect_tick,
accent,
),
2 => render_loaders_tab(
&mut buffer,
main_rects[1],
all_loader_kinds,
loader_tick,
loader_progress,
),
3 => render_widgets_tab(&mut buffer, main_rects[1], progress),
_ => {}
}
status.clear();
status.set_left(&format!("scrin // {}", tab_names[tab_index]), accent);
status.set_center(&format!("FPS: {:.0}", fps), Color::rgb(110, 118, 129));
status.set_right(
&format!(
"Frame: {} | Glow: {:.0}%",
frame_count,
glow_intensity * 100.0
),
Color::rgb(139, 148, 158),
);
status.render(&mut buffer, main_rects[2]);
for t in &toasts {
t.render(&mut buffer, Rect::new(0, 0, cols, rows));
}
if modal.is_visible() {
modal.render(&mut buffer, Rect::new(0, 0, cols, rows));
}
palette.render(&mut buffer, Rect::new(0, 0, cols, rows));
}
write!(stdout, "\x1b[H{}", buffer.to_ansi_string())?;
stdout.flush()?;
if event::poll(Duration::from_millis(16))? {
let ev = event::read()?;
if let Some(action) = router.handle_event(&ev) {
match action {
AppAction::Quit => break,
AppAction::TabNext => {
tab_index = (tab_index + 1) % tab_names.len();
if tab_index == 1 {
effect_index = (effect_index + 1) % kinds.len();
}
}
AppAction::TabPrev => {
tab_index = if tab_index == 0 {
tab_names.len() - 1
} else {
tab_index - 1
};
}
AppAction::OpenCommandPalette => {
palette.open();
}
AppAction::Back => {
if palette.is_open() {
palette.close();
} else if modal.is_visible() {
modal.hide();
}
}
AppAction::Select => {
if modal.is_visible() {
if let Some(sel) = modal.selected_option() {
toasts.push(toast::Toast::new(
&format!("Selected: {}", sel),
ToastKind::Success,
));
}
modal.hide();
} else if palette.is_open() {
if let Some(aid) = palette.execute_selected() {
match aid {
"quit" => break,
"settings" => {
modal = Modal::new("Settings", "Configure your app")
.with_options(vec![
"Theme".into(),
"Keybindings".into(),
"Close".into(),
]);
modal.show();
}
other => {
toasts.push(toast::Toast::new(
&format!("Action: {}", other),
ToastKind::Copy,
));
}
}
palette.close();
}
} else {
modal = Modal::new("Confirm", "Do you want to proceed?")
.with_options(vec!["Yes".into(), "No".into()]);
modal.show();
}
}
_ => {}
}
}
if let Event::Key(key) = ev {
if !palette.is_open() {
match (key.modifiers, key.code) {
(KeyModifiers::CONTROL, KeyCode::Char('c'))
| (KeyModifiers::CONTROL, KeyCode::Char('q')) => break,
(_, KeyCode::Char('m')) => modal.toggle(),
(_, KeyCode::Char('t')) => {
toasts.push(toast::Toast::new("Notification", ToastKind::Info));
}
(_, KeyCode::Char('s')) => {
toasts.push(toast::Toast::new("Saved!", ToastKind::Success));
}
(_, KeyCode::Char('e')) => {
toasts.push(toast::Toast::new("Error!", ToastKind::Error));
}
(_, KeyCode::Char('c')) => {
toasts.push(toast::Toast::new("Copied to clipboard", ToastKind::Copy));
}
(_, KeyCode::Up) | (_, KeyCode::Char('k')) => {
modal.select_prev();
}
(_, KeyCode::Down) | (_, KeyCode::Char('j')) => {
modal.select_next();
}
_ => {}
}
} else {
match key.code {
KeyCode::Esc => palette.close(),
KeyCode::Up => palette.select_prev(),
KeyCode::Down => palette.select_next(),
KeyCode::Enter => {
if let Some(aid) = palette.execute_selected() {
match aid {
"quit" => break,
"settings" => {
modal = Modal::new("Settings", "Configure your app")
.with_options(vec![
"Theme".into(),
"Keybindings".into(),
"Close".into(),
]);
modal.show();
}
other => {
toasts.push(toast::Toast::new(
&format!("Action: {}", other),
ToastKind::Copy,
));
}
}
palette.close();
}
}
KeyCode::Char(c) => palette.input_char(c),
KeyCode::Backspace => palette.backspace(),
_ => {}
}
}
}
}
}
Ok(())
}
fn render_dashboard(
buf: &mut Buffer,
area: Rect,
progress: f32,
spark_data: &[u64],
fps: f64,
frame_count: u64,
glow: f32,
) {
let layout = Layout::vertical(vec![
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(8),
Constraint::Min(0),
]);
let rects = layout.split(area);
let gauge = Gauge::new()
.with_ratio(progress as f64)
.with_label("System Load");
gauge.render(buf, rects[0]);
let spark = Sparkline::new()
.with_data(spark_data.to_vec())
.with_max(12)
.with_color(Color::rgb(88, 166, 255));
spark.render(buf, rects[1]);
let info_block = Block::new("System Status")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(48, 54, 61));
info_block.render(buf, rects[2]);
let inner = info_block.inner(rects[2]);
let lines = [
&format!("FPS: {:.0} Frames: {}", fps, frame_count),
&format!(
"Progress: {:.1}% Glow: {:.0}%",
progress * 100.0,
glow * 100.0
),
"All subsystems nominal",
"Terminal effects active",
"Animation clock running",
];
for (i, line) in lines.iter().enumerate() {
if i as u16 >= inner.height {
break;
}
buf.set_str(
inner.x as usize,
inner.y as usize + i,
line,
Color::rgb(139, 148, 158),
None,
);
}
let help_block = Block::new("Controls")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(48, 54, 61));
help_block.render(buf, rects[3]);
let help_inner = help_block.inner(rects[3]);
let help_lines = [
"Tab/Shift+Tab : Switch tabs",
"Ctrl+K : Command Palette",
"m : Toggle modal",
"t/s/e/c : Toast types",
"Ctrl+Q : Quit",
];
for (i, line) in help_lines.iter().enumerate() {
if i as u16 >= help_inner.height {
break;
}
buf.set_str(
help_inner.x as usize,
help_inner.y as usize + i,
line,
Color::rgb(110, 118, 129),
None,
);
}
}
fn render_effects_tab(
buf: &mut Buffer,
area: Rect,
kind: aisling::effects::EffectKind,
text: &str,
tick: usize,
accent: Color,
) {
let block = Block::new("Live Effects")
.with_borders(BorderStyle::Rounded)
.with_border_color(accent);
block.render(buf, area);
let inner = block.inner(area);
let layout = Layout::vertical(vec![Constraint::Length(1), Constraint::Min(0)]);
let rects = layout.split(inner);
let mut player = EffectPlayer::new(kind, text).with_accent(accent);
buf.set_str(
rects[0].x as usize,
rects[0].y as usize,
&format!("{} ({} frames)", player.name(), player.total_frames()),
Color::rgb(255, 178, 72),
None,
);
player.set_frame(tick);
player.render_to_buffer(buf, rects[1]);
}
fn render_loaders_tab(
buf: &mut Buffer,
area: Rect,
kinds: &[aisling::loaders::LoaderKind],
tick: usize,
progress: f32,
) {
let block = Block::new("Live Loaders")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(88, 166, 255));
block.render(buf, area);
let inner = block.inner(area);
let grid = 3.min(kinds.len() as u16);
let col_constraints: Vec<Constraint> = (0..grid)
.map(|_| Constraint::Percentage(100 / grid))
.collect();
let row_constraints: Vec<Constraint> = (0..grid)
.map(|_| Constraint::Percentage(100 / grid))
.collect();
let row_layout = Layout::vertical(row_constraints);
let row_rects = row_layout.split(inner);
for (row_idx, &row_rect) in row_rects.iter().enumerate() {
let col_layout = Layout::horizontal(col_constraints.clone());
let col_rects = col_layout.split(row_rect);
for (col_idx, &col_rect) in col_rects.iter().enumerate() {
let item_idx = row_idx * grid as usize + col_idx;
if item_idx >= kinds.len() {
break;
}
let loader = LoaderPlayer::new(kinds[item_idx]);
let name = loader.name();
let mini_block = Block::new(name)
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(48, 54, 61));
mini_block.render(buf, col_rect);
let inner_area = mini_block.inner(col_rect);
let prog = LoaderPlayer::progress_from_fraction(progress);
loader.render(tick, prog, buf, inner_area);
}
}
}
fn render_widgets_tab(buf: &mut Buffer, area: Rect, progress: f32) {
let layout = Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]);
let rects = layout.split(area);
let left_layout = Layout::vertical(vec![Constraint::Length(4), Constraint::Min(0)]);
let left_rects = left_layout.split(rects[0]);
let gauge = Gauge::new()
.with_ratio(progress as f64)
.with_label("Widget Demo");
gauge.render(buf, left_rects[0]);
let list_block = Block::new("List")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(48, 54, 61));
list_block.render(buf, left_rects[1]);
let list_inner = list_block.inner(left_rects[1]);
let items = vec![
ListItem::new("Gauges"),
ListItem::new("Sparklines"),
ListItem::new("BarCharts"),
ListItem::new("ScrollBars"),
ListItem::new("Tabs"),
ListItem::new("Tables"),
];
let list = List::new(&items).with_selected((progress * 5.0) as usize % 6);
list.render(buf, list_inner);
let right_layout = Layout::vertical(vec![Constraint::Length(8), Constraint::Min(0)]);
let right_rects = right_layout.split(rects[1]);
let bar_chart = BarChart::new().with_bars(vec![
Bar::new("CPU", ((progress * 80.0) as u64).max(5)),
Bar::new("MEM", (progress * 60.0 + 20.0) as u64),
Bar::new("DISK", 45),
Bar::new("NET", (progress * 50.0 + 10.0) as u64),
]);
bar_chart.render(buf, right_rects[0]);
let scroll_block = Block::new("Scroll")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(48, 54, 61));
scroll_block.render(buf, right_rects[1]);
let scroll_inner = scroll_block.inner(right_rects[1]);
let scrollbar = ScrollBar::new(ScrollBarOrientation::Vertical)
.with_position((progress * 30.0) as usize)
.with_total(40)
.with_viewport(scroll_inner.height as usize);
scrollbar.render(buf, scroll_inner);
}