use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use scrin::core::buffer::Buffer;
use scrin::core::color::Color;
use scrin::core::rect::Rect;
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::panes::{Pane, PaneManager, ResizeBehavior};
use scrin::sanitize;
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::popup::{Popup, PopupPosition, PopupStyle};
use scrin::widgets::scrollbar::{ScrollBar, ScrollBarOrientation};
use scrin::widgets::sparkline::Sparkline;
use scrin::widgets::tabs::Tabs;
use scrin::widgets::toggle::{Toggle, ToggleStyle};
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",
);
router.bind(
KeyModifiers::CONTROL,
KeyCode::Char('p'),
AppAction::TabPrev,
"Prev Tab",
);
let mut status = StatusBar::new().with_position(StatusBarPosition::Bottom);
let tab_names = &[
"Dashboard",
"Panes",
"Popups",
"Toggles",
"Effects",
"Loaders",
"Widgets",
];
let mut tab_index: usize = 0;
let mut modal = Modal::new("scrin Ultimate Demo", "Production-grade TUI toolkit")
.with_options(vec!["Explore".into(), "Settings".into(), "Dismiss".into()]);
modal.show();
let mut toasts: Vec<toast::Toast> = Vec::new();
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 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 pane_mgr = PaneManager::new().with_direction(scrin::layout::Direction::Horizontal);
let pane_editor = Pane::new(0, "Editor")
.with_constraint(Constraint::Min(20))
.with_min_size(15, 5)
.with_resize_behavior(ResizeBehavior::AutoCollapse)
.with_toggle_key('1')
.with_border_color(Color::rgb(88, 166, 255));
let pane_explorer = Pane::new(1, "Explorer")
.with_constraint(Constraint::Percentage(25))
.with_min_size(10, 5)
.with_resize_behavior(ResizeBehavior::AutoMinimize)
.with_toggle_key('2')
.with_border_color(Color::rgb(63, 185, 80));
let pane_terminal = Pane::new(2, "Terminal")
.with_constraint(Constraint::Min(10))
.with_min_size(10, 3)
.with_resize_behavior(ResizeBehavior::AutoClose)
.with_toggle_key('3')
.with_border_color(Color::rgb(210, 153, 34));
let pane_problems = Pane::new(3, "Problems")
.with_constraint(Constraint::Length(8))
.with_min_size(10, 3)
.with_resize_behavior(ResizeBehavior::Fixed)
.with_toggle_key('4')
.with_border_color(Color::rgb(248, 81, 73));
pane_mgr.add_pane(pane_editor);
pane_mgr.add_pane(pane_explorer);
pane_mgr.add_pane(pane_terminal);
pane_mgr.add_pane(pane_problems);
let mut popup = Popup::new(
"Quick Info",
"Press keys to toggle panes.\n1-4: toggle panes\n5-8: popup styles\nt: toasts",
)
.with_style(PopupStyle::Floating)
.with_size(50, 8)
.with_position(PopupPosition::Center)
.with_border_color(Color::rgb(139, 148, 158));
let toggles = vec![
Toggle::new("Dark Mode", true).with_style(ToggleStyle::Slider),
Toggle::new("Auto Save", false).with_style(ToggleStyle::Checkbox),
Toggle::new("Notifications", true).with_style(ToggleStyle::Radio),
Toggle::new("Minimap", true).with_style(ToggleStyle::Text),
Toggle::new("Word Wrap", false).with_style(ToggleStyle::Block),
Toggle::new("Line Numbers", true)
.with_style(ToggleStyle::Slider)
.with_active_color(Color::rgb(255, 178, 72)),
Toggle::new("Bracket Pairs", false).with_style(ToggleStyle::Checkbox),
Toggle::new("Sticky Scroll", true).with_style(ToggleStyle::Radio),
];
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;
loop {
let now = Instant::now();
let delta = now.duration_since(last_tick);
last_tick = now;
frame_count += 1;
fps_counter += 1;
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());
popup.update(delta);
modal.update(delta);
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()?;
pane_mgr.handle_resize(cols, rows);
popup.adapt_to_terminal(cols, rows);
let mut buffer = Buffer::new(cols as usize, rows as usize);
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,
),
1 => render_panes_tab(&mut buffer, main_rects[1], &pane_mgr, cols, rows),
2 => render_popups_tab(&mut buffer, main_rects[1]),
3 => render_toggles_tab(&mut buffer, main_rects[1], &toggles),
4 => render_effects_tab(
&mut buffer,
main_rects[1],
kinds[effect_index],
effect_text,
effect_tick,
),
5 => render_loaders_tab(
&mut buffer,
main_rects[1],
&all_loader_kinds,
loader_tick,
loader_progress,
),
6 => render_widgets_tab(&mut buffer, main_rects[1], progress),
_ => {}
}
status.clear();
status.set_left(
&sanitize::truncate_str(
&format!("scrin // {}", tab_names[tab_index]),
cols as usize / 3,
),
Color::rgb(88, 166, 255),
);
status.set_center(&format!("FPS: {:.0}", fps), Color::rgb(110, 118, 129));
let size_info = format!("{}x{}", cols, rows);
let frame_info = format!("F:{}", frame_count);
status.set_right(
&sanitize::truncate_str(
&format!("{} | {}", size_info, frame_info),
cols as usize / 4,
),
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));
}
popup.render(&mut buffer, Rect::new(0, 0, cols, rows));
if modal.is_visible() {
modal.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(),
AppAction::TabPrev => {
tab_index = if tab_index == 0 {
tab_names.len() - 1
} else {
tab_index - 1
}
}
AppAction::OpenCommandPalette => {
popup.toggle();
}
AppAction::Back => {
if modal.is_visible() {
modal.hide();
} else if popup.is_visible() {
popup.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 {
modal = Modal::new("Confirm", "Do you want to proceed?")
.with_options(vec!["Yes".into(), "No".into()]);
modal.show();
}
}
AppAction::Resize => {
let (c, r) = terminal::size()?;
pane_mgr.handle_resize(c, r);
popup.adapt_to_terminal(c, r);
toasts.push(toast::Toast::new(
&format!("Resized: {}x{}", c, r),
ToastKind::Info,
));
}
_ => {}
}
}
if let Event::Key(key) = ev {
match (key.modifiers, key.code) {
(KeyModifiers::CONTROL, KeyCode::Char('c'))
| (KeyModifiers::CONTROL, KeyCode::Char('q')) => break,
_ => {}
}
if !modal.is_visible()
&& key.code == KeyCode::Char('p')
&& key.modifiers == KeyModifiers::NONE
{
popup.toggle();
continue;
}
if key.code == KeyCode::Esc {
if popup.is_visible() {
popup.hide();
continue;
}
if modal.is_visible() {
modal.hide();
continue;
}
}
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
if modal.is_visible() {
modal.select_prev();
} else {
pane_mgr.focus_prev();
}
}
KeyCode::Down | KeyCode::Char('j') => {
if modal.is_visible() {
modal.select_next();
} else {
pane_mgr.focus_next();
}
}
KeyCode::Char(c) => {
if let Some(pane_id) = pane_mgr.check_toggle(c) {
let pane_name = pane_mgr
.get_pane(pane_id)
.map(|p| p.title.clone())
.unwrap_or_default();
toasts.push(toast::Toast::new(
&format!("Pane '{}' toggled", pane_name),
ToastKind::Info,
));
}
match c {
't' => {
toasts.push(toast::Toast::new("Info notification", ToastKind::Info))
}
's' => toasts.push(toast::Toast::new("Saved!", ToastKind::Success)),
'e' => {
toasts.push(toast::Toast::new("Error occurred!", ToastKind::Error))
}
'c' => toasts
.push(toast::Toast::new("Copied to clipboard", ToastKind::Copy)),
'5' => {
popup.set_style(PopupStyle::Floating);
popup.show();
}
'6' => {
popup.set_style(PopupStyle::Drawer);
popup.show();
}
'7' => {
popup.set_style(PopupStyle::Tooltip);
popup.show();
}
'8' => {
popup.set_style(PopupStyle::Panel);
popup.show();
}
'0' => {
effect_index = (effect_index + 1) % kinds.len();
}
_ => {}
}
}
_ => {}
}
}
}
}
Ok(())
}
fn render_dashboard(
buf: &mut Buffer,
area: Rect,
progress: f32,
spark_data: &[u64],
fps: f64,
frame_count: u64,
) {
let layout = Layout::vertical(vec![
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(10),
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}%", progress * 100.0),
"All subsystems nominal",
"Terminal effects active",
"Dynamic panes loaded",
"Sanitization active",
"Popup system ready",
"Toggle system ready",
];
for (i, line) in lines.iter().enumerate() {
if i as u16 >= inner.height {
break;
}
let display = sanitize::truncate_str(line, inner.width as usize);
buf.set_str(
inner.x as usize,
inner.y as usize + i,
&display,
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",
"1-4 : Toggle panes",
"5-8 : Popup styles",
"p : Toggle popup",
"t/s/e/c : Toast types",
"k/j : Focus/Select",
"0 : Cycle effects",
"Ctrl+Q : Quit",
];
for (i, line) in help_lines.iter().enumerate() {
if i as u16 >= help_inner.height {
break;
}
let display = sanitize::truncate_str(line, help_inner.width as usize);
buf.set_str(
help_inner.x as usize,
help_inner.y as usize + i,
&display,
Color::rgb(110, 118, 129),
None,
);
}
}
fn render_panes_tab(buf: &mut Buffer, area: Rect, pane_mgr: &PaneManager, cols: u16, rows: u16) {
let layout = Layout::vertical(vec![Constraint::Length(3), Constraint::Min(0)]);
let rects = layout.split(area);
let info_block = Block::new("Dynamic Panes")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(88, 166, 255));
info_block.render(buf, rects[0]);
let inner = info_block.inner(rects[0]);
let status_lines = [
&format!("Terminal: {}x{}", cols, rows),
&format!(
"Open: {} | Focus: {:?}",
pane_mgr.panes.iter().filter(|p| p.is_open()).count(),
pane_mgr.active_focus
),
&format!(
"Visible: {}",
pane_mgr.panes.iter().filter(|p| p.is_visible()).count()
),
];
for (i, line) in status_lines.iter().enumerate() {
if i as u16 >= inner.height {
break;
}
let display = sanitize::truncate_str(line, inner.width as usize);
buf.set_str(
inner.x as usize,
inner.y as usize + i,
&display,
Color::rgb(255, 178, 72),
None,
);
}
pane_mgr.render(buf, rects[1]);
}
fn render_popups_tab(buf: &mut Buffer, area: Rect) {
let block = Block::new("Popup Windows")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(88, 166, 255));
block.render(buf, area);
let inner = block.inner(area);
let lines = [
"Popup Styles Available:",
"",
" 5 - Floating: Standard floating window",
" 6 - Drawer: Side panel style (double border)",
" 7 - Tooltip: Small info popup (plain border)",
" 8 - Panel: Heavy panel (thick border)",
"",
" p - Toggle popup on/off",
" Esc - Close popup/modal",
"",
"Popup Features:",
" - Auto-adapts to terminal size",
" - Auto-closes on small terminals",
" - 9 positions (corners, edges, center)",
" - Follow cursor mode",
" - Fade transitions",
];
for (i, line) in lines.iter().enumerate() {
if i as u16 >= inner.height {
break;
}
let display = sanitize::truncate_str(line, inner.width as usize);
let color = if line.starts_with(" ") {
Color::rgb(139, 148, 158)
} else if line.contains("Features") || line.contains("Styles") {
Color::rgb(255, 178, 72)
} else {
Color::rgb(201, 209, 217)
};
buf.set_str(
inner.x as usize,
inner.y as usize + i,
&display,
color,
None,
);
}
}
fn render_toggles_tab(buf: &mut Buffer, area: Rect, toggles: &[Toggle]) {
let block = Block::new("Toggle Widgets")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(88, 166, 255));
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 desc_lines = [
"5 toggle styles: Slider, Checkbox, Radio, Text, Block",
"Each with customizable colors",
];
for (i, line) in desc_lines.iter().enumerate() {
if i as u16 >= rects[0].height {
break;
}
let display = sanitize::truncate_str(line, rects[0].width as usize);
buf.set_str(
rects[0].x as usize,
rects[0].y as usize + i,
&display,
Color::rgb(110, 118, 129),
None,
);
}
let toggle_layout = Layout::vertical(toggles.iter().map(|_| Constraint::Length(1)).collect());
let toggle_rects = toggle_layout.split(rects[1]);
for (i, toggle) in toggles.iter().enumerate() {
if i >= toggle_rects.len() {
break;
}
toggle.render(buf, toggle_rects[i]);
}
}
fn render_effects_tab(
buf: &mut Buffer,
area: Rect,
kind: aisling::effects::EffectKind,
text: &str,
tick: usize,
) {
let block = Block::new("Live Effects")
.with_borders(BorderStyle::Rounded)
.with_border_color(Color::rgb(88, 166, 255));
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 player = EffectPlayer::new(kind, text);
let name_display = sanitize::truncate_str(
&format!(
"{} ({} frames) - Press 0 to cycle",
player.name(),
player.total_frames()
),
rects[0].width as usize,
);
buf.set_str(
rects[0].x as usize,
rects[0].y as usize,
&name_display,
Color::rgb(255, 178, 72),
None,
);
let mut player = player;
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 =
sanitize::truncate_str(loader.name(), col_rect.width.saturating_sub(4) as usize);
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"),
ListItem::new("Popups"),
ListItem::new("Toggles"),
ListItem::new("Panes"),
];
let list = List::new(&items).with_selected((progress * 8.0) as usize % 9);
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);
}