#![allow(unsafe_code)]
use opentui::GraphemePool;
use opentui::buffer::{ClipRect, GrayscaleBuffer, OptimizedBuffer, PixelBuffer, ScissorStack};
use opentui::event::{LogLevel as OpentuiLogLevel, set_log_callback};
use opentui::input::{Event, InputParser, KeyCode, KeyModifiers};
#[allow(unused_imports)]
use opentui::renderer::{HitGrid, ThreadedRenderer};
#[allow(unused_imports)]
use opentui::terminal::{Capabilities, CursorStyle};
use opentui::terminal::{MouseButton, MouseEventKind, enable_raw_mode, terminal_size};
use opentui_rust as opentui;
#[allow(unused_imports)]
use opentui::text::{EditBuffer, EditorView, WrapMode};
#[allow(unused_imports)] use opentui::{Cell, CellContent, Renderer, RendererOptions, Rgba, Style};
use std::collections::VecDeque;
use std::ffi::OsString;
use std::io::{self, Read};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
const HELP_TEXT: &str = "demo_showcase - OpenTUI demonstration binary
USAGE:
demo_showcase [OPTIONS]
OPTIONS:
-h, --help Print this help message and exit
--tour Start in tour mode immediately
--fps <N> Cap frames per second (default: 60)
--no-mouse Disable mouse tracking
--no-alt-screen Don't enter alternate screen
--no-cap-queries Skip terminal capability queries
--max-frames <N> Exit after presenting N frames
--exit-after-tour Exit automatically when tour completes
--headless-smoke Run headless smoke test (no TTY required)
--headless-size <WxH> Force headless buffer size (default: 80x24)
--headless-dump-json Output JSON snapshot for regression testing
--headless-check <NAME> Run specific headless check (layout, config,
palette, hitgrid, logs)
--cap-preset <NAME> Capability preset: auto, ideal, no_truecolor,
no_hyperlinks, no_mouse, minimal (default: auto)
--threaded Use ThreadedRenderer backend
--seed <N> Deterministic seed for animations (default: 0)
EXAMPLES:
demo_showcase # Interactive mode
demo_showcase --tour # Start tour immediately
demo_showcase --fps 30 --no-mouse # 30 FPS, keyboard only
demo_showcase --headless-smoke # CI smoke test
demo_showcase --max-frames 100 # Run exactly 100 frames then exit
";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CapPreset {
#[default]
Auto,
Ideal,
NoTruecolor,
NoHyperlinks,
NoMouse,
Minimal,
}
impl CapPreset {
fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"auto" => Some(Self::Auto),
"ideal" => Some(Self::Ideal),
"no_truecolor" | "notruecolor" => Some(Self::NoTruecolor),
"no_hyperlinks" | "nohyperlinks" => Some(Self::NoHyperlinks),
"no_mouse" | "nomouse" => Some(Self::NoMouse),
"minimal" => Some(Self::Minimal),
_ => None,
}
}
}
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)] pub struct EffectiveCaps {
pub truecolor: bool,
pub mouse: bool,
pub hyperlinks: bool,
pub focus: bool,
pub sync_output: bool,
pub degraded: Vec<&'static str>,
}
impl Default for EffectiveCaps {
fn default() -> Self {
Self {
truecolor: true,
mouse: true,
hyperlinks: true,
focus: true,
sync_output: true,
degraded: Vec::new(),
}
}
}
impl EffectiveCaps {
#[must_use]
pub fn compute(detected: Option<&opentui::terminal::Capabilities>, preset: CapPreset) -> Self {
let mut caps = Self::default();
let mut degraded = Vec::new();
if let Some(det) = detected {
caps.truecolor = det.has_true_color();
caps.mouse = det.mouse;
caps.hyperlinks = det.hyperlinks;
caps.focus = det.focus;
caps.sync_output = det.sync_output;
}
match preset {
CapPreset::Auto | CapPreset::Ideal => {
}
CapPreset::NoTruecolor => {
if caps.truecolor {
degraded.push("truecolor (preset)");
}
caps.truecolor = false;
}
CapPreset::NoHyperlinks => {
if caps.hyperlinks {
degraded.push("hyperlinks (preset)");
}
caps.hyperlinks = false;
}
CapPreset::NoMouse => {
if caps.mouse {
degraded.push("mouse (preset)");
}
caps.mouse = false;
}
CapPreset::Minimal => {
if caps.truecolor {
degraded.push("truecolor (minimal)");
}
if caps.mouse {
degraded.push("mouse (minimal)");
}
if caps.hyperlinks {
degraded.push("hyperlinks (minimal)");
}
if caps.sync_output {
degraded.push("sync_output (minimal)");
}
caps.truecolor = false;
caps.mouse = false;
caps.hyperlinks = false;
caps.sync_output = false;
}
}
if let Some(det) = detected {
if !det.has_true_color()
&& preset != CapPreset::NoTruecolor
&& preset != CapPreset::Minimal
{
degraded.push("truecolor (terminal)");
}
if !det.mouse && preset != CapPreset::NoMouse && preset != CapPreset::Minimal {
degraded.push("mouse (terminal)");
}
if !det.hyperlinks && preset != CapPreset::NoHyperlinks && preset != CapPreset::Minimal
{
degraded.push("hyperlinks (terminal)");
}
}
caps.degraded = degraded;
caps
}
#[must_use]
pub fn is_degraded(&self) -> bool {
!self.degraded.is_empty()
}
}
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)] pub struct Config {
pub start_in_tour: bool,
pub fps_cap: u32,
pub enable_mouse: bool,
pub use_alt_screen: bool,
pub query_capabilities: bool,
pub max_frames: Option<u64>,
pub exit_after_tour: bool,
pub headless_smoke: bool,
pub headless_size: (u16, u16),
pub headless_dump_json: bool,
pub headless_check: Option<String>,
pub cap_preset: CapPreset,
pub threaded: bool,
pub seed: u64,
}
impl Default for Config {
fn default() -> Self {
Self {
start_in_tour: false,
fps_cap: 60,
enable_mouse: true,
use_alt_screen: true,
query_capabilities: true,
max_frames: None,
exit_after_tour: false,
headless_smoke: false,
headless_size: (80, 24),
headless_dump_json: false,
headless_check: None,
cap_preset: CapPreset::Auto,
threaded: false,
seed: 0,
}
}
}
pub enum ParseResult {
Config(Config),
Help,
Error(String),
}
impl Config {
#[allow(clippy::too_many_lines)]
pub fn from_args<I>(args: I) -> ParseResult
where
I: IntoIterator<Item = OsString>,
{
let mut config = Self::default();
let mut args = args.into_iter();
args.next();
while let Some(arg) = args.next() {
let arg_str = arg.to_string_lossy();
match arg_str.as_ref() {
"-h" | "--help" => return ParseResult::Help,
"--tour" => config.start_in_tour = true,
"--fps" => {
let value = match args.next() {
Some(v) => v.to_string_lossy().to_string(),
None => return ParseResult::Error("--fps requires a value".to_string()),
};
match value.parse::<u32>() {
Ok(n) if n > 0 => config.fps_cap = n,
_ => {
return ParseResult::Error(format!(
"Invalid --fps value: {value} (must be positive integer)"
));
}
}
}
"--no-mouse" => config.enable_mouse = false,
"--no-alt-screen" => config.use_alt_screen = false,
"--no-cap-queries" => config.query_capabilities = false,
"--max-frames" => {
let value = match args.next() {
Some(v) => v.to_string_lossy().to_string(),
None => {
return ParseResult::Error("--max-frames requires a value".to_string());
}
};
match value.parse::<u64>() {
Ok(n) => config.max_frames = Some(n),
Err(_) => {
return ParseResult::Error(format!(
"Invalid --max-frames value: {value}"
));
}
}
}
"--exit-after-tour" => config.exit_after_tour = true,
"--headless-smoke" => config.headless_smoke = true,
"--headless-dump-json" => config.headless_dump_json = true,
"--headless-size" => {
let value = match args.next() {
Some(v) => v.to_string_lossy().to_string(),
None => {
return ParseResult::Error(
"--headless-size requires a value (e.g., 80x24)".to_string(),
);
}
};
match parse_size(&value) {
Some((w, h)) => config.headless_size = (w, h),
None => {
return ParseResult::Error(format!(
"Invalid --headless-size: {value} (use WxH format, e.g., 80x24)"
));
}
}
}
"--headless-check" => {
let value = match args.next() {
Some(v) => v.to_string_lossy().to_string(),
None => {
return ParseResult::Error(
"--headless-check requires a value (layout, config, palette, hitgrid, logs)".to_string(),
);
}
};
let valid_checks = ["layout", "config", "palette", "hitgrid", "logs"];
if valid_checks.contains(&value.as_str()) {
config.headless_check = Some(value);
} else {
return ParseResult::Error(format!(
"Unknown --headless-check: {value} (valid: layout, config, palette, hitgrid, logs)"
));
}
}
"--cap-preset" => {
let value = match args.next() {
Some(v) => v.to_string_lossy().to_string(),
None => {
return ParseResult::Error("--cap-preset requires a value".to_string());
}
};
match CapPreset::from_str(&value) {
Some(preset) => config.cap_preset = preset,
None => {
return ParseResult::Error(format!(
"Unknown --cap-preset: {value} \
(valid: auto, ideal, no_truecolor, no_mouse, minimal)"
));
}
}
}
"--threaded" => config.threaded = true,
"--seed" => {
let value = match args.next() {
Some(v) => v.to_string_lossy().to_string(),
None => return ParseResult::Error("--seed requires a value".to_string()),
};
match value.parse::<u64>() {
Ok(n) => config.seed = n,
Err(_) => {
return ParseResult::Error(format!("Invalid --seed value: {value}"));
}
}
}
other => {
if other.starts_with('-') {
return ParseResult::Error(format!("Unknown option: {other}"));
}
}
}
}
ParseResult::Config(config)
}
#[must_use]
pub fn renderer_options(&self) -> RendererOptions {
RendererOptions {
use_alt_screen: self.use_alt_screen,
hide_cursor: true,
enable_mouse: self.enable_mouse && self.cap_preset != CapPreset::NoMouse,
query_capabilities: self.query_capabilities,
}
}
#[must_use]
pub fn frame_duration(&self) -> Duration {
Duration::from_micros(1_000_000 / u64::from(self.fps_cap))
}
}
#[allow(clippy::missing_const_for_fn, clippy::must_use_candidate)] fn parse_size(s: &str) -> Option<(u16, u16)> {
let parts: Vec<&str> = s.split('x').collect();
if parts.len() != 2 {
return None;
}
let w = parts[0].parse::<u16>().ok()?;
let h = parts[1].parse::<u16>().ok()?;
if w == 0 || h == 0 {
return None;
}
Some((w, h))
}
#[allow(clippy::large_enum_variant)] pub enum Backend {
Direct(Renderer),
Threaded {
renderer: ThreadedRenderer,
hit_grid: HitGrid,
hit_scissor: ScissorStack,
capabilities: Capabilities,
},
}
#[allow(clippy::missing_errors_doc, clippy::must_use_candidate)] impl Backend {
pub fn new_direct(width: u32, height: u32, options: RendererOptions) -> io::Result<Self> {
Ok(Self::Direct(Renderer::new_with_options(
width, height, options,
)?))
}
pub fn new_threaded(width: u32, height: u32, options: RendererOptions) -> io::Result<Self> {
let renderer = ThreadedRenderer::new_with_options(width, height, options)?;
let capabilities = Capabilities::detect();
Ok(Self::Threaded {
renderer,
hit_grid: HitGrid::new(width, height),
hit_scissor: ScissorStack::new(),
capabilities,
})
}
pub fn buffer(&mut self) -> &mut OptimizedBuffer {
match self {
Self::Direct(r) => r.buffer(),
Self::Threaded { renderer, .. } => renderer.buffer(),
}
}
pub fn grapheme_pool(&mut self) -> &mut GraphemePool {
match self {
Self::Direct(r) => r.grapheme_pool(),
Self::Threaded { renderer, .. } => renderer.grapheme_pool(),
}
}
pub fn link_pool(&mut self) -> &mut opentui::LinkPool {
match self {
Self::Direct(r) => r.link_pool(),
Self::Threaded { renderer, .. } => renderer.link_pool(),
}
}
pub fn present(&mut self) -> io::Result<()> {
match self {
Self::Direct(r) => r.present(),
Self::Threaded { renderer, .. } => renderer.present(),
}
}
pub fn resize(&mut self, width: u32, height: u32) -> io::Result<()> {
match self {
Self::Direct(r) => r.resize(width, height),
Self::Threaded {
renderer,
hit_grid,
hit_scissor,
..
} => {
hit_grid.resize(width, height);
hit_scissor.clear();
renderer.resize(width, height)
}
}
}
pub fn set_title(&mut self, title: &str) -> io::Result<()> {
match self {
Self::Direct(r) => r.set_title(title),
Self::Threaded { renderer, .. } => renderer.set_title(title),
}
}
pub fn set_cursor(&mut self, x: u32, y: u32, visible: bool) -> io::Result<()> {
match self {
Self::Direct(r) => r.set_cursor(x, y, visible),
Self::Threaded { renderer, .. } => renderer.set_cursor(x, y, visible),
}
}
pub fn set_cursor_style(&mut self, style: CursorStyle, blinking: bool) -> io::Result<()> {
match self {
Self::Direct(r) => r.set_cursor_style(style, blinking),
Self::Threaded { renderer, .. } => renderer.set_cursor_style(style, blinking),
}
}
#[must_use]
pub fn capabilities(&self) -> &Capabilities {
match self {
Self::Direct(r) => r.capabilities(),
Self::Threaded { capabilities, .. } => capabilities,
}
}
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
pub fn register_hit_area(&mut self, x: u32, y: u32, width: u32, height: u32, id: u32) {
match self {
Self::Direct(r) => r.register_hit_area(x, y, width, height, id),
Self::Threaded {
hit_grid,
hit_scissor,
..
} => {
let rect = ClipRect::new(x as i32, y as i32, width, height);
if let Some(intersect) = hit_scissor.current().intersect(&rect) {
if !ClipRect::is_empty(&intersect) {
hit_grid.register(
intersect.x.max(0) as u32,
intersect.y.max(0) as u32,
intersect.width,
intersect.height,
id,
);
}
}
}
}
}
#[must_use]
pub fn hit_test(&self, x: u32, y: u32) -> Option<u32> {
match self {
Self::Direct(r) => r.hit_test(x, y),
Self::Threaded { hit_grid, .. } => hit_grid.test(x, y),
}
}
pub fn push_hit_scissor(&mut self, rect: ClipRect) {
match self {
Self::Direct(r) => r.push_hit_scissor(rect),
Self::Threaded { hit_scissor, .. } => hit_scissor.push(rect),
}
}
pub fn pop_hit_scissor(&mut self) {
match self {
Self::Direct(r) => r.pop_hit_scissor(),
Self::Threaded { hit_scissor, .. } => hit_scissor.pop(),
}
}
pub fn clear_hit_scissors(&mut self) {
match self {
Self::Direct(r) => r.clear_hit_scissors(),
Self::Threaded { hit_scissor, .. } => hit_scissor.clear(),
}
}
pub fn invalidate(&mut self) {
match self {
Self::Direct(r) => r.invalidate(),
Self::Threaded { renderer, .. } => {
let _ = renderer.invalidate();
}
}
}
pub fn set_background(&mut self, color: Rgba) {
match self {
Self::Direct(r) => r.set_background(color),
Self::Threaded { renderer, .. } => renderer.set_background(color),
}
}
pub fn clear(&mut self) {
match self {
Self::Direct(r) => r.clear(),
Self::Threaded { renderer, .. } => renderer.clear(),
}
}
#[must_use]
pub fn stats(&self) -> RenderStatsView<'_> {
match self {
Self::Direct(r) => RenderStatsView::Direct(r.stats()),
Self::Threaded { renderer, .. } => RenderStatsView::Threaded(renderer.stats()),
}
}
pub fn cleanup(&mut self) -> io::Result<()> {
match self {
Self::Direct(r) => r.cleanup(),
Self::Threaded { .. } => Ok(()), }
}
pub fn shutdown(self) -> io::Result<()> {
match self {
Self::Direct(_) => Ok(()), Self::Threaded { renderer, .. } => renderer.shutdown(),
}
}
}
pub enum RenderStatsView<'a> {
Direct(&'a opentui::RenderStats),
Threaded(&'a opentui::renderer::ThreadedRenderStats),
}
#[allow(clippy::missing_const_for_fn, clippy::must_use_candidate)]
impl RenderStatsView<'_> {
pub fn frames(&self) -> u64 {
match self {
Self::Direct(s) => s.frames,
Self::Threaded(s) => s.frames,
}
}
pub fn last_frame_us(&self) -> u128 {
match self {
Self::Direct(s) => s.last_frame_time.as_micros(),
Self::Threaded(s) => s.last_frame_time.as_micros(),
}
}
pub fn last_frame_cells(&self) -> usize {
match self {
Self::Direct(s) => s.last_frame_cells,
Self::Threaded(s) => s.last_frame_cells,
}
}
}
pub mod hit_ids {
pub const BTN_HELP: u32 = 1000;
pub const BTN_PALETTE: u32 = 1001;
pub const BTN_TOUR: u32 = 1002;
pub const BTN_THEME: u32 = 1003;
pub const SIDEBAR_ROW_BASE: u32 = 2000;
pub const PANEL_SIDEBAR: u32 = 3000;
pub const PANEL_EDITOR: u32 = 3001;
pub const PANEL_PREVIEW: u32 = 3002;
pub const PANEL_LOGS: u32 = 3003;
pub const OVERLAY_CLOSE: u32 = 4000;
pub const PALETTE_ITEM_BASE: u32 = 4100;
}
pub mod easing {
#[must_use]
pub fn smoothstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * 2.0_f32.mul_add(-t, 3.0)
}
#[must_use]
pub fn ease_in_out_cubic(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t < 0.5 {
4.0 * t * t * t
} else {
let p = (-2.0_f32).mul_add(t, 2.0);
1.0 - p * p * p / 2.0
}
}
#[must_use]
pub fn pulse(t: f32, omega: f32) -> f32 {
0.5_f32.mul_add((t * omega).sin(), 0.5)
}
#[must_use]
pub fn ease_out_cubic(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
1.0 - (1.0 - t).powi(3)
}
}
#[derive(Clone, Debug)]
pub struct AnimationClock {
pub t: f32,
pub dt: f32,
last_instant: Instant,
paused: bool,
}
impl Default for AnimationClock {
fn default() -> Self {
Self::new()
}
}
impl AnimationClock {
pub const MAX_DT: f32 = 0.1;
pub const MIN_DT: f32 = 0.001;
#[must_use]
pub fn new() -> Self {
Self {
t: 0.0,
dt: 0.0,
last_instant: Instant::now(),
paused: false,
}
}
pub fn tick(&mut self, paused: bool) {
let now = Instant::now();
let raw_dt = now.duration_since(self.last_instant).as_secs_f32();
self.last_instant = now;
self.dt = raw_dt.clamp(Self::MIN_DT, Self::MAX_DT);
self.paused = paused;
if !self.paused {
self.t += self.dt;
}
}
#[must_use]
pub const fn is_paused(&self) -> bool {
self.paused
}
pub const fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}
#[must_use]
pub fn t_offset(&self, offset: f32) -> f32 {
self.t + offset
}
#[must_use]
pub fn pulse(&self, omega: f32) -> f32 {
easing::pulse(self.t, omega)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: u32,
pub h: u32,
}
#[allow(clippy::cast_possible_wrap)]
impl Rect {
#[must_use]
pub const fn new(x: i32, y: i32, w: u32, h: u32) -> Self {
Self { x, y, w, h }
}
#[must_use]
pub const fn from_size(w: u32, h: u32) -> Self {
Self { x: 0, y: 0, w, h }
}
#[must_use]
pub const fn inset(self, pad: u32) -> Self {
let pad2 = pad.saturating_mul(2);
Self {
x: self.x.saturating_add(pad as i32),
y: self.y.saturating_add(pad as i32),
w: self.w.saturating_sub(pad2),
h: self.h.saturating_sub(pad2),
}
}
#[must_use]
pub const fn split_h(self, left_w: u32) -> (Self, Self) {
let left_w = if left_w > self.w { self.w } else { left_w };
let left = Self {
x: self.x,
y: self.y,
w: left_w,
h: self.h,
};
let right = Self {
x: self.x.saturating_add(left_w as i32),
y: self.y,
w: self.w.saturating_sub(left_w),
h: self.h,
};
(left, right)
}
#[must_use]
pub const fn split_v(self, top_h: u32) -> (Self, Self) {
let top_h = if top_h > self.h { self.h } else { top_h };
let top = Self {
x: self.x,
y: self.y,
w: self.w,
h: top_h,
};
let bottom = Self {
x: self.x,
y: self.y.saturating_add(top_h as i32),
w: self.w,
h: self.h.saturating_sub(top_h),
};
(top, bottom)
}
#[must_use]
pub const fn clamp_to(self, max_w: u32, max_h: u32) -> Self {
let new_w = if self.w > max_w { max_w } else { self.w };
let new_h = if self.h > max_h { max_h } else { self.h };
Self {
x: self.x,
y: self.y,
w: new_w,
h: new_h,
}
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.w == 0 || self.h == 0
}
#[must_use]
pub const fn right(self) -> i32 {
self.x.saturating_add(self.w as i32)
}
#[must_use]
pub const fn bottom(self) -> i32 {
self.y.saturating_add(self.h as i32)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LayoutMode {
#[default]
Full,
Compact,
Minimal,
TooSmall,
}
impl LayoutMode {
#[must_use]
pub const fn from_size(width: u32, height: u32) -> Self {
if width < 40 || height < 12 {
Self::TooSmall
} else if width < 60 || height < 16 {
Self::Minimal
} else if width < 80 || height < 24 {
Self::Compact
} else {
Self::Full
}
}
}
pub mod layout {
pub const TOP_BAR_HEIGHT: u32 = 1;
pub const STATUS_BAR_HEIGHT: u32 = 1;
pub const SIDEBAR_WIDTH_FULL: u32 = 20;
pub const SIDEBAR_WIDTH_COMPACT: u32 = 4;
pub const PREVIEW_WIDTH_RATIO: u32 = 40;
pub const EDITOR_MIN_WIDTH: u32 = 30;
pub const LOGS_HEIGHT_FULL: u32 = 6;
pub const LOGS_HEIGHT_COMPACT: u32 = 4;
pub const MIN_WIDTH: u32 = 40;
pub const MIN_HEIGHT: u32 = 12;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum UiTheme {
#[default]
SynthwaveDark,
PaperLight,
Solarized,
HighContrast,
}
impl UiTheme {
pub const ALL: [Self; 4] = [
Self::SynthwaveDark,
Self::PaperLight,
Self::Solarized,
Self::HighContrast,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::SynthwaveDark => "Synthwave",
Self::PaperLight => "Paper",
Self::Solarized => "Solarized",
Self::HighContrast => "High Contrast",
}
}
#[must_use]
pub const fn next(self) -> Self {
match self {
Self::SynthwaveDark => Self::PaperLight,
Self::PaperLight => Self::Solarized,
Self::Solarized => Self::HighContrast,
Self::HighContrast => Self::SynthwaveDark,
}
}
#[must_use]
pub const fn is_dark(self) -> bool {
match self {
Self::SynthwaveDark | Self::Solarized | Self::HighContrast => true,
Self::PaperLight => false,
}
}
#[must_use]
pub fn tokens(self) -> Theme {
match self {
Self::SynthwaveDark => Theme::synthwave(),
Self::PaperLight => Theme::paper_light(),
Self::Solarized => Theme::solarized(),
Self::HighContrast => Theme::high_contrast(),
}
}
}
pub struct Theme {
pub bg0: Rgba,
pub bg1: Rgba,
pub bg2: Rgba,
pub fg0: Rgba,
pub fg1: Rgba,
pub fg2: Rgba,
pub accent_primary: Rgba,
pub accent_secondary: Rgba,
pub accent_success: Rgba,
pub accent_warning: Rgba,
pub accent_error: Rgba,
pub selection_bg: Rgba,
pub focus_border: Rgba,
}
impl Theme {
#[must_use]
pub fn synthwave() -> Self {
Self {
bg0: Rgba::from_hex("#0f1220").unwrap_or(Rgba::BLACK),
bg1: Rgba::from_hex("#151a2e").unwrap_or(Rgba::BLACK),
bg2: Rgba::from_hex("#1d2440").unwrap_or(Rgba::BLACK),
fg0: Rgba::from_hex("#e6e6e6").unwrap_or(Rgba::WHITE),
fg1: Rgba::from_hex("#aeb6d6").unwrap_or(Rgba::WHITE),
fg2: Rgba::from_hex("#6c7396").unwrap_or(Rgba::WHITE),
accent_primary: Rgba::from_hex("#4dd6ff").unwrap_or(Rgba::rgb(0.0, 1.0, 1.0)),
accent_secondary: Rgba::from_hex("#ff4fd8").unwrap_or(Rgba::rgb(1.0, 0.0, 1.0)),
accent_success: Rgba::from_hex("#2bff88").unwrap_or(Rgba::GREEN),
accent_warning: Rgba::from_hex("#ffb020").unwrap_or(Rgba::rgb(1.0, 0.7, 0.1)),
accent_error: Rgba::from_hex("#ff4455").unwrap_or(Rgba::RED),
selection_bg: Rgba::from_hex("#2a335c").unwrap_or(Rgba::rgb(0.16, 0.2, 0.36)),
focus_border: Rgba::from_hex("#4dd6ff").unwrap_or(Rgba::rgb(0.0, 1.0, 1.0)),
}
}
#[must_use]
pub fn paper_light() -> Self {
Self {
bg0: Rgba::from_hex("#f7f7fb").unwrap_or(Rgba::WHITE),
bg1: Rgba::from_hex("#ffffff").unwrap_or(Rgba::WHITE),
bg2: Rgba::from_hex("#eef0f7").unwrap_or(Rgba::WHITE),
fg0: Rgba::from_hex("#1a1b26").unwrap_or(Rgba::BLACK),
fg1: Rgba::from_hex("#3a3f5a").unwrap_or(Rgba::BLACK),
fg2: Rgba::from_hex("#6a6f8a").unwrap_or(Rgba::BLACK),
accent_primary: Rgba::from_hex("#2a6fff").unwrap_or(Rgba::BLUE),
accent_secondary: Rgba::from_hex("#7b61ff").unwrap_or(Rgba::rgb(1.0, 0.0, 1.0)),
accent_success: Rgba::from_hex("#00a86b").unwrap_or(Rgba::GREEN),
accent_warning: Rgba::from_hex("#ff8a00").unwrap_or(Rgba::rgb(1.0, 0.55, 0.0)),
accent_error: Rgba::from_hex("#e53935").unwrap_or(Rgba::RED),
selection_bg: Rgba::from_hex("#dbe6ff").unwrap_or(Rgba::rgb(0.86, 0.9, 1.0)),
focus_border: Rgba::from_hex("#2a6fff").unwrap_or(Rgba::BLUE),
}
}
#[must_use]
pub fn solarized() -> Self {
Self {
bg0: Rgba::from_hex("#002b36").unwrap_or(Rgba::BLACK),
bg1: Rgba::from_hex("#073642").unwrap_or(Rgba::BLACK),
bg2: Rgba::from_hex("#0b4452").unwrap_or(Rgba::BLACK),
fg0: Rgba::from_hex("#eee8d5").unwrap_or(Rgba::WHITE),
fg1: Rgba::from_hex("#93a1a1").unwrap_or(Rgba::WHITE),
fg2: Rgba::from_hex("#657b83").unwrap_or(Rgba::WHITE),
accent_primary: Rgba::from_hex("#2aa198").unwrap_or(Rgba::rgb(0.0, 1.0, 1.0)),
accent_secondary: Rgba::from_hex("#268bd2").unwrap_or(Rgba::BLUE),
accent_success: Rgba::from_hex("#859900").unwrap_or(Rgba::GREEN),
accent_warning: Rgba::from_hex("#b58900").unwrap_or(Rgba::rgb(0.7, 0.55, 0.0)),
accent_error: Rgba::from_hex("#dc322f").unwrap_or(Rgba::RED),
selection_bg: Rgba::from_hex("#0d5161").unwrap_or(Rgba::rgb(0.05, 0.32, 0.38)),
focus_border: Rgba::from_hex("#2aa198").unwrap_or(Rgba::rgb(0.0, 1.0, 1.0)),
}
}
#[must_use]
pub fn high_contrast() -> Self {
Self {
bg0: Rgba::BLACK,
bg1: Rgba::BLACK,
bg2: Rgba::from_hex("#111111").unwrap_or(Rgba::BLACK),
fg0: Rgba::WHITE,
fg1: Rgba::from_hex("#e0e0e0").unwrap_or(Rgba::WHITE),
fg2: Rgba::from_hex("#a0a0a0").unwrap_or(Rgba::WHITE),
accent_primary: Rgba::rgb(0.0, 1.0, 1.0),
accent_secondary: Rgba::rgb(1.0, 0.0, 1.0),
accent_success: Rgba::GREEN,
accent_warning: Rgba::rgb(1.0, 1.0, 0.0),
accent_error: Rgba::RED,
selection_bg: Rgba::from_hex("#333333").unwrap_or(Rgba::rgb(0.2, 0.2, 0.2)),
focus_border: Rgba::rgb(1.0, 1.0, 0.0),
}
}
#[must_use]
pub fn lerp(a: Rgba, b: Rgba, t: f32) -> Rgba {
Rgba::new(
(b.r - a.r).mul_add(t, a.r),
(b.g - a.g).mul_add(t, a.g),
(b.b - a.b).mul_add(t, a.b),
(b.a - a.a).mul_add(t, a.a),
)
}
#[allow(clippy::cast_precision_loss)] pub fn gradient(start: Rgba, end: Rgba, steps: u32) -> impl Iterator<Item = Rgba> {
(0..steps).map(move |i| {
let t = if steps > 1 {
i as f32 / (steps - 1) as f32
} else {
0.0
};
Self::lerp(start, end, t)
})
}
}
pub struct Styles;
impl Styles {
#[must_use]
pub fn header(theme: &Theme) -> Style {
Style::builder().fg(theme.fg0).bg(theme.bg1).bold().build()
}
#[must_use]
pub fn border(theme: &Theme) -> Style {
Style::builder().fg(theme.fg2).bg(theme.bg0).build()
}
#[must_use]
pub fn border_focused(theme: &Theme) -> Style {
Style::builder()
.fg(theme.focus_border)
.bg(theme.bg0)
.bold()
.build()
}
#[must_use]
pub fn selection(theme: &Theme) -> Style {
Style::builder()
.fg(theme.fg0)
.bg(theme.selection_bg)
.build()
}
#[must_use]
pub fn muted(theme: &Theme) -> Style {
Style::builder().fg(theme.fg2).bg(theme.bg0).build()
}
#[must_use]
pub fn status_bar(theme: &Theme) -> Style {
Style::builder().fg(theme.fg1).bg(theme.bg2).build()
}
#[must_use]
pub fn key_hint(theme: &Theme) -> Style {
Style::builder().fg(theme.fg0).bg(theme.bg2).bold().build()
}
#[must_use]
pub fn link(theme: &Theme) -> Style {
Style::builder()
.fg(theme.accent_primary)
.underline()
.build()
}
#[must_use]
pub fn error(theme: &Theme) -> Style {
Style::builder().fg(theme.accent_error).bold().build()
}
#[must_use]
pub fn success(theme: &Theme) -> Style {
Style::builder().fg(theme.accent_success).bold().build()
}
#[must_use]
pub fn warning(theme: &Theme) -> Style {
Style::builder().fg(theme.accent_warning).build()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct OverlayAnim {
pub progress: f32,
pub opening: bool,
}
impl OverlayAnim {
const SPEED: f32 = 9.0;
#[must_use]
pub const fn opening() -> Self {
Self {
progress: 0.0,
opening: true,
}
}
pub fn tick(&mut self, dt: f32) -> bool {
let delta = Self::SPEED * dt;
if self.opening {
self.progress = (self.progress + delta).min(1.0);
self.progress >= 1.0
} else {
self.progress = (self.progress - delta).max(0.0);
self.progress <= 0.0
}
}
pub const fn start_close(&mut self) {
self.opening = false;
}
#[must_use]
pub fn opacity(&self) -> f32 {
easing::ease_out_cubic(self.progress)
}
#[must_use]
pub const fn is_closed(&self) -> bool {
self.progress <= 0.0 && !self.opening
}
#[must_use]
pub fn is_open(&self) -> bool {
self.progress >= 1.0
}
}
#[derive(Clone, Debug, Default)]
pub struct HelpState {
pub scroll: usize,
pub focused_section: usize,
}
impl HelpState {
pub const SECTIONS: &'static [(&'static str, &'static [&'static str])] = &[
(
"Navigation",
&[
"Tab / Shift+Tab Cycle focus between panels",
"1-9, 0, -, = Jump to section (12 total)",
"↑/↓ Navigate within focused panel",
],
),
(
"Actions",
&[
"Ctrl+Q Quit application",
"Ctrl+N Cycle UI theme",
"Ctrl+R Force redraw",
"Ctrl+D Toggle debug overlay",
],
),
(
"Overlays",
&[
"F1 Toggle this help overlay",
"Ctrl+P Toggle command palette",
"Ctrl+T Toggle guided tour",
"Esc Close current overlay",
],
),
(
"Mouse",
&[
"Click Focus panel / activate button",
"Scroll Scroll within logs/lists",
"Sidebar click Navigate to section",
],
),
(
"Feature Legend",
&[
"• Alpha blending Overlays + glass panels",
"• Scissor stack Sidebar/log scroll clipping",
"• Opacity stack Tinted UI + overlay backdrop",
"• Grapheme pool Unicode panel + emoji",
"• OSC 8 links Logs + help (in term)",
"• Hit grid Clickable buttons/nav",
"• Pixel buffers Preview animated orb",
"• Diff rendering Efficient partial updates",
],
),
(
"Links",
&["Repo: github.com/opentui/opentui", "Docs: opentui.dev"],
),
];
pub const fn scroll_up(&mut self) {
self.scroll = self.scroll.saturating_sub(1);
}
pub const fn scroll_down(&mut self, max_scroll: usize) {
if self.scroll < max_scroll {
self.scroll += 1;
}
}
}
#[derive(Clone, Debug, Default)]
pub struct PaletteState {
pub query: String,
pub selected: usize,
pub filtered: Vec<usize>,
}
impl PaletteState {
pub const COMMANDS: &'static [(&'static str, &'static str)] = &[
("Toggle Help", "Show keyboard shortcuts and tips"),
("Toggle Tour", "Start the guided feature tour"),
("Cycle Theme", "Switch to the next color theme"),
("Force Redraw", "Refresh the entire display"),
("Toggle Debug", "Show/hide performance overlay"),
("Go to Overview", "Navigate to Overview section [1]"),
("Go to Editor", "Navigate to Editor section [2]"),
("Go to Preview", "Navigate to Preview section [3]"),
("Go to Logs", "Navigate to Logs section [4]"),
("Go to Unicode", "Navigate to Unicode section [5]"),
("Go to Performance", "Navigate to Performance section [6]"),
("Go to Drawing", "Navigate to Drawing section [7]"),
("Go to Colors", "Navigate to Colors section [8]"),
("Go to Input", "Navigate to Input section [9]"),
("Go to Editing", "Navigate to Editing section [0]"),
("Go to Capabilities", "Navigate to Capabilities section [-]"),
("Go to Animations", "Navigate to Animations section [=]"),
("Quit", "Exit the application"),
];
pub fn update_filter(&mut self) {
let query_lower = self.query.to_lowercase();
self.filtered = Self::COMMANDS
.iter()
.enumerate()
.filter(|(_, (name, desc))| {
query_lower.is_empty()
|| name.to_lowercase().contains(&query_lower)
|| desc.to_lowercase().contains(&query_lower)
})
.map(|(i, _)| i)
.collect();
if !self.filtered.is_empty() && self.selected >= self.filtered.len() {
self.selected = self.filtered.len() - 1;
}
}
pub fn select_prev(&mut self) {
if !self.filtered.is_empty() {
self.selected = self.selected.saturating_sub(1);
}
}
pub fn select_next(&mut self) {
if !self.filtered.is_empty() && self.selected < self.filtered.len() - 1 {
self.selected += 1;
}
}
}
#[derive(Clone, Debug, Default)]
pub struct TourState {
pub step: usize,
pub spotlight: Option<Rect>,
}
impl TourState {
pub const STEPS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[
(
"Welcome to OpenTUI!",
"This tour will guide you through the key features.\nPress Enter to continue, Esc to exit.",
None,
),
(
"Sidebar Navigation",
"Use number keys 1-6 or click to switch sections.\nThe sidebar adapts to terminal size.",
Some("sidebar"),
),
(
"Editor Panel",
"The main content area displays text with\nfull grapheme and Unicode support.",
Some("editor"),
),
(
"Preview Panel",
"See rendered output and visual effects.\nAlpha blending is demonstrated here.",
Some("preview"),
),
(
"Theme System",
"Press Ctrl+N to cycle through themes.\n4 built-in themes with full color tokens.",
None,
),
(
"Keyboard Shortcuts",
"Press F1 anytime to see all shortcuts.\nTab cycles focus between panels.",
None,
),
(
"Command Palette",
"Press Ctrl+P to open the command palette.\nQuickly access any action by typing.",
None,
),
(
"Responsive Layout",
"Resize the terminal to see adaptive layouts.\nFull → Compact → Minimal → TooSmall.",
None,
),
(
"Alpha Blending Demo",
"This overlay itself demonstrates alpha blending!\nNotice the backdrop transparency.",
None,
),
(
"Performance",
"OpenTUI uses diff-based rendering.\nOnly changed cells are sent to the terminal.",
None,
),
(
"Scissor Clipping",
"Content is clipped to panel boundaries.\nOverlays use the scissor stack.",
None,
),
(
"Tour Complete!",
"You've seen all the key features.\nPress Esc to exit and explore on your own.",
None,
),
];
pub const fn next_step(&mut self) -> bool {
if self.step < Self::STEPS.len() - 1 {
self.step += 1;
false
} else {
true
}
}
pub const fn prev_step(&mut self) {
self.step = self.step.saturating_sub(1);
}
#[must_use]
pub fn current(&self) -> (&'static str, &'static str, Option<&'static str>) {
Self::STEPS
.get(self.step)
.copied()
.unwrap_or(("", "", None))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TourAction {
None,
SetFocus(Focus),
SetSection(Section),
OpenHelp,
OpenPalette,
CloseOverlay,
CycleTheme,
ShowDebug,
}
#[derive(Clone, Copy, Debug)]
pub struct TourStep {
pub title: &'static str,
pub description: &'static str,
pub duration_ms: u32,
pub action: TourAction,
pub spotlight: Option<&'static str>,
}
pub const TOUR_SCRIPT: &[TourStep] = &[
TourStep {
title: "Welcome to OpenTUI!",
description: "This tour demonstrates the key features.\nDiff rendering eliminates flicker.",
duration_ms: 4000,
action: TourAction::None,
spotlight: None,
},
TourStep {
title: "Sidebar Navigation",
description: "Scissor-clipped scrolling inside panel bounds.\nUse 1-6 or click to navigate.",
duration_ms: 4000,
action: TourAction::SetFocus(Focus::Sidebar),
spotlight: Some("sidebar"),
},
TourStep {
title: "Focus & Hit Testing",
description: "Tab cycles focus between panels.\nClick anywhere for instant focus.",
duration_ms: 3500,
action: TourAction::SetFocus(Focus::Editor),
spotlight: Some("editor"),
},
TourStep {
title: "Command Palette",
description: "Glass overlay with alpha blending.\nCtrl+P to open anytime.",
duration_ms: 4000,
action: TourAction::OpenPalette,
spotlight: None,
},
TourStep {
title: "Editor: Rope + Undo",
description: "Rope-backed text buffer for efficient edits.\nUndo/redo with Ctrl+Z/Y.",
duration_ms: 4000,
action: TourAction::CloseOverlay,
spotlight: Some("editor"),
},
TourStep {
title: "Syntax Highlighting",
description: "Built-in tokenizers for Rust and Markdown.\nTheme-aware token colors.",
duration_ms: 3500,
action: TourAction::SetSection(Section::Editor),
spotlight: Some("editor"),
},
TourStep {
title: "Theme Demo",
description: "⚠️ Screen will change to LIGHT THEME in 3 seconds!\nThis demonstrates the theme system.",
duration_ms: 3000,
action: TourAction::None,
spotlight: None,
},
TourStep {
title: "Light Theme Active",
description: "Now showing Paper (light) theme.\nPress Ctrl+N anytime to cycle between 4 themes.",
duration_ms: 4000,
action: TourAction::CycleTheme,
spotlight: None,
},
TourStep {
title: "Unicode & Graphemes",
description: "CJK, emoji, ZWJ sequences rendered correctly.\nGrapheme pool handles multi-codepoint chars.",
duration_ms: 4500,
action: TourAction::SetSection(Section::Unicode),
spotlight: Some("preview"),
},
TourStep {
title: "Preview: Alpha Blending",
description: "Porter-Duff compositing for translucent layers.\nReal RGBA blending, not dithering.",
duration_ms: 4000,
action: TourAction::SetSection(Section::Preview),
spotlight: Some("preview"),
},
TourStep {
title: "Logs & Hyperlinks",
description: "Event stream with OSC 8 hyperlinks.\nClick links to open in browser.",
duration_ms: 4000,
action: TourAction::SetSection(Section::Logs),
spotlight: Some("logs"),
},
TourStep {
title: "Performance Stats",
description: "Diff rendering: only changed cells written.\nTypically <1KB per frame after first.",
duration_ms: 4000,
action: TourAction::SetSection(Section::Performance),
spotlight: Some("preview"),
},
TourStep {
title: "Drawing Primitives",
description: "5 box styles: Single, Double, Rounded, Heavy, ASCII.\nTitled boxes, partial sides, fills.",
duration_ms: 4000,
action: TourAction::SetSection(Section::Drawing),
spotlight: Some("editor"),
},
TourStep {
title: "Color System",
description: "Gradients, HSV color wheel, alpha blending.\nOpacity stacking for layered effects.",
duration_ms: 4000,
action: TourAction::SetSection(Section::Colors),
spotlight: Some("editor"),
},
TourStep {
title: "Input Handling",
description: "Cursor styles: Block, Underline, Bar.\nFocus events and bracketed paste.",
duration_ms: 3500,
action: TourAction::SetSection(Section::Input),
spotlight: Some("editor"),
},
TourStep {
title: "Editing Features",
description: "EditBuffer with rope-backed storage.\nUndo/Redo and wrap modes.",
duration_ms: 4000,
action: TourAction::SetSection(Section::Editing),
spotlight: Some("editor"),
},
TourStep {
title: "Terminal Capabilities",
description: "Detected features shown with checkmarks.\nEnvironment and preset information.",
duration_ms: 3500,
action: TourAction::SetSection(Section::Capabilities),
spotlight: Some("editor"),
},
TourStep {
title: "Animations & Easing",
description: "Easing curves: linear, smoothstep, cubic.\nPulse animations at different frequencies.",
duration_ms: 4500,
action: TourAction::SetSection(Section::Animations),
spotlight: Some("editor"),
},
TourStep {
title: "Tour Complete!",
description: "You've seen all 12 sections.\nPress Esc to explore freely.",
duration_ms: 5000,
action: TourAction::None,
spotlight: None,
},
];
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)] pub struct TourRunner {
pub step_idx: usize,
pub step_started_t: f32,
pub paused: bool,
pub auto_advance: bool,
pub exit_on_complete: bool,
pub completed: bool,
}
impl Default for TourRunner {
fn default() -> Self {
Self {
step_idx: 0,
step_started_t: 0.0,
paused: false,
auto_advance: true,
exit_on_complete: false,
completed: false,
}
}
}
impl TourRunner {
#[must_use]
pub const fn new(auto_advance: bool, exit_on_complete: bool) -> Self {
Self {
step_idx: 0,
step_started_t: 0.0,
paused: false,
auto_advance,
exit_on_complete,
completed: false,
}
}
#[must_use]
pub fn current_step(&self) -> Option<&'static TourStep> {
TOUR_SCRIPT.get(self.step_idx)
}
#[must_use]
pub const fn total_steps(&self) -> usize {
TOUR_SCRIPT.len()
}
#[must_use]
pub const fn has_next(&self) -> bool {
self.step_idx < TOUR_SCRIPT.len() - 1
}
pub const fn next_step(&mut self, current_t: f32) -> bool {
if self.step_idx < TOUR_SCRIPT.len() - 1 {
self.step_idx += 1;
self.step_started_t = current_t;
false
} else {
self.completed = true;
true
}
}
pub const fn prev_step(&mut self, current_t: f32) {
if self.step_idx > 0 {
self.step_idx -= 1;
self.step_started_t = current_t;
}
}
pub const fn reset(&mut self, current_t: f32) {
self.step_idx = 0;
self.step_started_t = current_t;
self.completed = false;
}
pub const fn toggle_pause(&mut self) {
self.paused = !self.paused;
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn should_auto_advance(&self, current_t: f32) -> bool {
if !self.auto_advance || self.paused || self.completed {
return false;
}
self.current_step().is_some_and(|step| {
let elapsed_ms = (current_t - self.step_started_t) * 1000.0;
elapsed_ms >= step.duration_ms as f32
})
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn step_progress(&self, current_t: f32) -> f32 {
self.current_step().map_or(1.0, |step| {
let elapsed_ms = (current_t - self.step_started_t) * 1000.0;
(elapsed_ms / step.duration_ms as f32).clamp(0.0, 1.0)
})
}
#[must_use]
pub fn execute_step_action(&self) -> Option<TourAction> {
self.current_step().map(|s| s.action)
}
}
#[derive(Clone, Debug)]
pub enum Overlay {
Help(HelpState),
Palette(PaletteState),
Tour(TourState),
}
#[derive(Clone, Debug, Default)]
pub struct OverlayManager {
pub active: Option<Overlay>,
pub anim: OverlayAnim,
}
impl OverlayManager {
pub fn open(&mut self, overlay: Overlay) {
self.active = Some(overlay);
self.anim = OverlayAnim::opening();
}
pub const fn close(&mut self) {
self.anim.start_close();
}
pub fn tick(&mut self, dt: f32) {
if self.active.is_some() {
let done = self.anim.tick(dt);
if done && self.anim.is_closed() {
self.active = None;
}
}
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active.is_some()
}
#[must_use]
pub fn kind(&self) -> Option<AppMode> {
self.active.as_ref().map(|o| match o {
Overlay::Help(_) => AppMode::Help,
Overlay::Palette(_) => AppMode::CommandPalette,
Overlay::Tour(_) => AppMode::Tour,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ToastLevel {
#[default]
Info,
Warn,
Error,
}
impl ToastLevel {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
}
}
#[must_use]
pub const fn icon(&self) -> &'static str {
match self {
Self::Info => "ℹ",
Self::Warn => "⚠",
Self::Error => "✗",
}
}
}
#[derive(Clone, Debug)]
pub struct Toast {
pub level: ToastLevel,
pub title: String,
pub detail: Option<String>,
pub ttl: f32,
pub elapsed: f32,
}
impl Toast {
pub const DEFAULT_TTL: f32 = 3.0;
pub const FADE_DURATION: f32 = 0.3;
#[must_use]
pub fn info(title: impl Into<String>) -> Self {
Self {
level: ToastLevel::Info,
title: title.into(),
detail: None,
ttl: Self::DEFAULT_TTL,
elapsed: 0.0,
}
}
#[must_use]
pub fn warn(title: impl Into<String>) -> Self {
Self {
level: ToastLevel::Warn,
title: title.into(),
detail: None,
ttl: Self::DEFAULT_TTL,
elapsed: 0.0,
}
}
#[must_use]
pub fn error(title: impl Into<String>) -> Self {
Self {
level: ToastLevel::Error,
title: title.into(),
detail: None,
ttl: Self::DEFAULT_TTL,
elapsed: 0.0,
}
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
#[must_use]
pub const fn with_ttl(mut self, ttl: f32) -> Self {
self.ttl = ttl;
self
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.elapsed >= self.ttl
}
#[must_use]
pub fn opacity(&self) -> f32 {
let remaining = self.ttl - self.elapsed;
if remaining <= Self::FADE_DURATION {
(remaining / Self::FADE_DURATION).max(0.0)
} else {
1.0
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ToastManager {
toasts: VecDeque<Toast>,
}
impl ToastManager {
pub const MAX_VISIBLE: usize = 5;
pub const TOAST_GAP: u32 = 1;
pub const TOAST_WIDTH: u32 = 40;
#[must_use]
#[allow(clippy::missing_const_for_fn, clippy::must_use_candidate)] pub fn new() -> Self {
Self {
toasts: VecDeque::new(),
}
}
pub fn push(&mut self, toast: Toast) {
self.toasts.push_back(toast);
while self.toasts.len() > Self::MAX_VISIBLE {
self.toasts.pop_front();
}
}
pub fn tick(&mut self, dt: f32) -> usize {
for toast in &mut self.toasts {
toast.elapsed += dt;
}
let before = self.toasts.len();
self.toasts.retain(|t: &Toast| !t.is_expired());
before - self.toasts.len()
}
#[must_use]
#[allow(clippy::iter_without_into_iter)] pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, Toast> {
self.toasts.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.toasts.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.toasts.is_empty()
}
pub fn clear(&mut self) {
self.toasts.clear();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum RenderPass {
Background = 0,
Chrome = 1,
Panels = 2,
Overlays = 3,
Toasts = 4,
Debug = 5,
}
impl RenderPass {
pub const ALL: [Self; 6] = [
Self::Background,
Self::Chrome,
Self::Panels,
Self::Overlays,
Self::Toasts,
Self::Debug,
];
}
#[derive(Clone, Copy, Debug, Default)]
pub struct PanelLayout {
pub mode: LayoutMode,
pub screen: Rect,
pub top_bar: Rect,
pub status_bar: Rect,
pub content: Rect,
pub sidebar: Rect,
pub main_area: Rect,
pub upper_main: Rect,
pub editor: Rect,
pub preview: Rect,
pub logs: Rect,
}
impl PanelLayout {
#[must_use]
pub fn compute(width: u32, height: u32) -> Self {
let mode = LayoutMode::from_size(width, height);
let screen = Rect::from_size(width, height);
if mode == LayoutMode::TooSmall {
return Self {
mode,
screen,
..Self::default()
};
}
let (top_bar, rest) = screen.split_v(layout::TOP_BAR_HEIGHT);
let status_h = rest.h.saturating_sub(layout::STATUS_BAR_HEIGHT);
let (content, status_bar) = rest.split_v(status_h);
let sidebar_w = match mode {
LayoutMode::Full => layout::SIDEBAR_WIDTH_FULL,
LayoutMode::Compact => layout::SIDEBAR_WIDTH_COMPACT,
LayoutMode::Minimal | LayoutMode::TooSmall => 0,
};
let (sidebar, main_area) = content.split_h(sidebar_w);
let logs_h = match mode {
LayoutMode::Full => layout::LOGS_HEIGHT_FULL.min(main_area.h / 3),
LayoutMode::Compact => layout::LOGS_HEIGHT_COMPACT.min(main_area.h / 3),
LayoutMode::Minimal | LayoutMode::TooSmall => 0,
};
let upper_h = main_area.h.saturating_sub(logs_h);
let (upper_main, logs) = main_area.split_v(upper_h);
let (editor, preview) =
if mode == LayoutMode::Full && upper_main.w > layout::EDITOR_MIN_WIDTH {
let preview_w = upper_main.w * layout::PREVIEW_WIDTH_RATIO / 100;
let editor_w = upper_main.w.saturating_sub(preview_w);
upper_main.split_h(editor_w)
} else {
(upper_main, Rect::default())
};
Self {
mode,
screen,
top_bar,
status_bar,
content,
sidebar,
main_area,
upper_main,
editor,
preview,
logs,
}
}
#[must_use]
pub fn get_panel_rect(&self, name: &str) -> Option<Rect> {
match name {
"sidebar" => Some(self.sidebar),
"editor" => Some(self.editor),
"preview" => Some(self.preview),
"logs" => Some(self.logs),
"top_bar" => Some(self.top_bar),
"status_bar" => Some(self.status_bar),
"content" => Some(self.content),
"main_area" => Some(self.main_area),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AppMode {
#[default]
Normal,
Help,
CommandPalette,
Tour,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ExitReason {
#[default]
UserQuit,
MaxFrames,
TourComplete,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Focus {
#[default]
Sidebar,
Editor,
Preview,
Logs,
}
impl Focus {
#[must_use]
pub const fn next(self) -> Self {
match self {
Self::Sidebar => Self::Editor,
Self::Editor => Self::Preview,
Self::Preview => Self::Logs,
Self::Logs => Self::Sidebar,
}
}
#[must_use]
pub const fn prev(self) -> Self {
match self {
Self::Sidebar => Self::Logs,
Self::Editor => Self::Sidebar,
Self::Preview => Self::Editor,
Self::Logs => Self::Preview,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Section {
#[default]
Overview,
Editor,
Preview,
Logs,
Unicode,
Performance,
Drawing,
Colors,
Input,
Editing,
Capabilities,
Animations,
}
impl Section {
pub const ALL: [Self; 12] = [
Self::Overview,
Self::Editor,
Self::Preview,
Self::Logs,
Self::Unicode,
Self::Performance,
Self::Drawing,
Self::Colors,
Self::Input,
Self::Editing,
Self::Capabilities,
Self::Animations,
];
#[must_use]
pub fn from_index(idx: usize) -> Option<Self> {
Self::ALL.get(idx).copied()
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Overview => "Overview",
Self::Editor => "Editor",
Self::Preview => "Preview",
Self::Logs => "Logs",
Self::Unicode => "Unicode",
Self::Performance => "Performance",
Self::Drawing => "Drawing",
Self::Colors => "Colors",
Self::Input => "Input",
Self::Editing => "Editing",
Self::Capabilities => "Capabilities",
Self::Animations => "Animations",
}
}
#[must_use]
pub const fn key_hint(self) -> &'static str {
match self {
Self::Overview => "1",
Self::Editor => "2",
Self::Preview => "3",
Self::Logs => "4",
Self::Unicode => "5",
Self::Performance => "6",
Self::Drawing => "7",
Self::Colors => "8",
Self::Input => "9",
Self::Editing => "0",
Self::Capabilities => "-",
Self::Animations => "=",
}
}
}
#[derive(Clone, Debug)]
pub enum Action {
Quit,
ToggleHelp,
TogglePalette,
ToggleTour,
CloseOverlay,
CycleFocusForward,
CycleFocusBackward,
NavigateSection(Section),
ForceRedraw,
ToggleDebug,
CycleTheme,
Resize(u32, u32),
FocusChanged(bool),
PaletteUp,
PaletteDown,
PaletteExecute,
PaletteBackspace,
PaletteChar(char),
SetFocus(Focus),
PaletteClick(usize),
None,
}
#[derive(Debug)]
#[allow(clippy::struct_excessive_bools)] pub struct App {
pub mode: AppMode,
pub focus: Focus,
pub section: Section,
pub paused: bool,
pub ui_theme: UiTheme,
pub should_quit: bool,
pub exit_reason: ExitReason,
pub frame_count: u64,
pub max_frames: Option<u64>,
pub show_debug: bool,
pub force_redraw: bool,
pub tour_step: usize,
pub tour_total: usize,
pub tour_runner: Option<TourRunner>,
pub overlays: OverlayManager,
pub clock: AnimationClock,
pub current_file_idx: usize,
pub logs: VecDeque<content::LogEntry>,
pub target_fps: u32,
pub metrics: content::Metrics,
pub toasts: ToastManager,
pub effective_caps: EffectiveCaps,
pub cap_preset: CapPreset,
}
impl Default for App {
fn default() -> Self {
let demo_content = content::DemoContent::default();
Self {
mode: AppMode::Normal,
focus: Focus::Sidebar,
section: Section::Overview,
paused: false,
ui_theme: UiTheme::default(),
should_quit: false,
exit_reason: ExitReason::UserQuit,
frame_count: 0,
max_frames: None,
show_debug: false,
force_redraw: false,
tour_step: 0,
tour_total: TOUR_SCRIPT.len(),
tour_runner: None,
overlays: OverlayManager::default(),
clock: AnimationClock::new(),
current_file_idx: 0,
logs: VecDeque::from(demo_content.seed_logs.to_vec()),
target_fps: demo_content.metric_params.target_fps,
metrics: content::Metrics::compute(0, demo_content.metric_params.target_fps),
toasts: ToastManager::new(),
effective_caps: EffectiveCaps::default(),
cap_preset: CapPreset::Auto,
}
}
}
impl App {
#[must_use]
pub fn new(config: &Config) -> Self {
Self::with_content(config, &content::DemoContent::default())
}
pub fn update_effective_caps(&mut self, detected: Option<&opentui::terminal::Capabilities>) {
self.effective_caps = EffectiveCaps::compute(detected, self.cap_preset);
if self.effective_caps.is_degraded() {
let msg = format!("Degraded: {}", self.effective_caps.degraded.join(", "));
self.toasts.push(Toast::warn(msg));
}
}
#[must_use]
pub fn with_content(config: &Config, demo_content: &content::DemoContent) -> Self {
let tour_runner = if config.start_in_tour {
Some(TourRunner::new(true, config.exit_after_tour))
} else {
None
};
Self {
max_frames: config.max_frames,
mode: if config.start_in_tour {
AppMode::Tour
} else {
AppMode::Normal
},
tour_runner,
current_file_idx: 0,
logs: VecDeque::from(demo_content.seed_logs.to_vec()),
target_fps: demo_content.metric_params.target_fps,
metrics: content::Metrics::compute(0, demo_content.metric_params.target_fps),
cap_preset: config.cap_preset,
..Self::default()
}
}
#[must_use]
pub fn current_file(&self) -> Option<&'static content::DemoFile> {
content::DEFAULT_FILES.get(self.current_file_idx)
}
#[must_use]
pub fn current_file_name(&self) -> &'static str {
self.current_file().map_or("untitled.txt", |f| f.name)
}
#[must_use]
pub fn current_file_content(&self) -> &'static str {
self.current_file().map_or("", |f| f.text)
}
#[must_use]
pub fn current_file_language(&self) -> content::Language {
self.current_file().map(|f| f.language).unwrap_or_default()
}
pub const fn next_file(&mut self) {
if !content::DEFAULT_FILES.is_empty() {
self.current_file_idx = (self.current_file_idx + 1) % content::DEFAULT_FILES.len();
}
}
pub const fn prev_file(&mut self) {
if !content::DEFAULT_FILES.is_empty() {
self.current_file_idx = if self.current_file_idx == 0 {
content::DEFAULT_FILES.len() - 1
} else {
self.current_file_idx - 1
};
}
}
pub const MAX_LOGS: usize = 1000;
pub fn add_log(&mut self, entry: content::LogEntry) {
self.logs.push_back(entry);
while self.logs.len() > Self::MAX_LOGS {
self.logs.pop_front();
}
}
pub fn show_toast(&mut self, toast: Toast) {
self.toasts.push(toast);
}
pub fn toast_info(&mut self, message: impl Into<String>) {
self.toasts.push(Toast::info(message));
}
pub fn toast_warn(&mut self, message: impl Into<String>) {
self.toasts.push(Toast::warn(message));
}
pub fn toast_error(&mut self, message: impl Into<String>) {
self.toasts.push(Toast::error(message));
}
pub fn update_metrics(&mut self) {
self.metrics = content::Metrics::compute(self.frame_count, self.target_fps);
}
pub fn start_tour(&mut self, auto_advance: bool, exit_on_complete: bool) {
self.mode = AppMode::Tour;
self.tour_step = 0;
self.tour_runner = Some(TourRunner::new(auto_advance, exit_on_complete));
self.overlays.open(Overlay::Tour(TourState::default()));
if let Some(runner) = &self.tour_runner {
if let Some(action) = runner.execute_step_action() {
self.apply_tour_action(action);
}
}
}
pub const fn stop_tour(&mut self) {
self.mode = AppMode::Normal;
self.tour_runner = None;
self.overlays.close();
}
pub fn tour_next_step(&mut self) {
let current_t = self.clock.t;
let (completed, step_idx, action, exit_on_complete) = {
let Some(runner) = self.tour_runner.as_mut() else {
return;
};
let completed = runner.next_step(current_t);
let step_idx = runner.step_idx;
let action = runner.execute_step_action();
let exit_on_complete = runner.exit_on_complete;
(completed, step_idx, action, exit_on_complete)
};
self.tour_step = step_idx;
if let Some(Overlay::Tour(ref mut tour_state)) = self.overlays.active {
tour_state.step = step_idx;
}
if let Some(action) = action {
self.apply_tour_action(action);
}
if completed && exit_on_complete {
self.should_quit = true;
self.exit_reason = ExitReason::TourComplete;
}
}
pub fn tour_prev_step(&mut self) {
let current_t = self.clock.t;
let (step_idx, action) = {
let Some(runner) = self.tour_runner.as_mut() else {
return;
};
runner.prev_step(current_t);
let step_idx = runner.step_idx;
let action = runner.execute_step_action();
(step_idx, action)
};
self.tour_step = step_idx;
if let Some(Overlay::Tour(ref mut tour_state)) = self.overlays.active {
tour_state.step = step_idx;
}
if let Some(action) = action {
self.apply_tour_action(action);
}
}
pub fn handle_event(&mut self, event: &Event) -> Action {
let action = self.event_to_action(event);
self.apply_action(&action);
action
}
fn event_to_action(&self, event: &Event) -> Action {
match event {
Event::Key(key) => self.key_to_action(key),
Event::Mouse(_) | Event::Paste(_) => Action::None,
Event::FocusGained => Action::FocusChanged(true),
Event::FocusLost => Action::FocusChanged(false),
Event::Resize(resize) => {
Action::Resize(u32::from(resize.width), u32::from(resize.height))
}
}
}
fn key_to_action(&self, key: &opentui::input::KeyEvent) -> Action {
match (key.code, key.modifiers.contains(KeyModifiers::CTRL)) {
(KeyCode::Char('q'), true) => return Action::Quit,
(KeyCode::F(1), _) => return Action::ToggleHelp,
(KeyCode::Char('p'), true) => return Action::TogglePalette,
(KeyCode::Char('t'), true) => return Action::ToggleTour,
(KeyCode::Char('r'), true) => return Action::ForceRedraw,
(KeyCode::Char('d'), true) => return Action::ToggleDebug,
(KeyCode::Char('n'), true) => return Action::CycleTheme,
(KeyCode::Tab, _) if !key.modifiers.contains(KeyModifiers::SHIFT) => {
return Action::CycleFocusForward;
}
(KeyCode::Tab | KeyCode::BackTab, _) => {
return Action::CycleFocusBackward;
}
_ => {}
}
if self.mode == AppMode::Normal {
let idx = match key.code {
KeyCode::Char(c @ '1'..='9') => Some((c as usize) - ('1' as usize)),
KeyCode::Char('0') => Some(9), KeyCode::Char('-') => Some(10), KeyCode::Char('=') => Some(11), _ => None,
};
if let Some(idx) = idx {
if let Some(section) = Section::from_index(idx) {
return Action::NavigateSection(section);
}
}
}
match self.mode {
AppMode::Normal => {
if key.code == KeyCode::Esc {
return Action::Quit;
}
Action::None
}
AppMode::Help => {
if key.code == KeyCode::Esc {
return Action::CloseOverlay;
}
Action::None
}
AppMode::CommandPalette => {
match key.code {
KeyCode::Esc => return Action::CloseOverlay,
KeyCode::Up => return Action::PaletteUp,
KeyCode::Down => return Action::PaletteDown,
KeyCode::Enter => return Action::PaletteExecute,
KeyCode::Backspace => return Action::PaletteBackspace,
KeyCode::Char(c) => return Action::PaletteChar(c),
_ => {}
}
Action::None
}
AppMode::Tour => {
if key.code == KeyCode::Esc {
Action::ToggleTour
} else {
Action::None
}
}
}
}
fn hit_to_action(hit_id: u32, kind: MouseEventKind) -> Action {
use hit_ids::{
BTN_HELP, BTN_PALETTE, BTN_THEME, BTN_TOUR, OVERLAY_CLOSE, PALETTE_ITEM_BASE,
PANEL_EDITOR, PANEL_LOGS, PANEL_PREVIEW, PANEL_SIDEBAR, SIDEBAR_ROW_BASE,
};
if kind != MouseEventKind::Press {
return Action::None;
}
match hit_id {
BTN_HELP => Action::ToggleHelp,
BTN_PALETTE => Action::TogglePalette,
BTN_TOUR => Action::ToggleTour,
BTN_THEME => Action::CycleTheme,
PANEL_SIDEBAR => Action::SetFocus(Focus::Sidebar),
PANEL_EDITOR => Action::SetFocus(Focus::Editor),
PANEL_PREVIEW => Action::SetFocus(Focus::Preview),
PANEL_LOGS => Action::SetFocus(Focus::Logs),
id if (SIDEBAR_ROW_BASE..SIDEBAR_ROW_BASE + 100).contains(&id) => {
let idx = (id - SIDEBAR_ROW_BASE) as usize;
Section::from_index(idx).map_or(Action::None, Action::NavigateSection)
}
id if (PALETTE_ITEM_BASE..PALETTE_ITEM_BASE + 100).contains(&id) => {
let idx = (id - PALETTE_ITEM_BASE) as usize;
Action::PaletteClick(idx)
}
OVERLAY_CLOSE => Action::CloseOverlay,
_ => Action::None,
}
}
#[allow(clippy::too_many_lines)] fn apply_action(&mut self, action: &Action) {
match action {
Action::Quit => {
self.should_quit = true;
}
Action::ToggleHelp => {
if self.mode == AppMode::Help {
self.mode = AppMode::Normal;
self.overlays.close();
} else {
self.mode = AppMode::Help;
self.overlays.open(Overlay::Help(HelpState::default()));
}
}
Action::TogglePalette => {
if self.mode == AppMode::CommandPalette {
self.mode = AppMode::Normal;
self.overlays.close();
} else {
self.mode = AppMode::CommandPalette;
let mut state = PaletteState::default();
state.update_filter(); self.overlays.open(Overlay::Palette(state));
}
}
Action::ToggleTour => {
if self.mode == AppMode::Tour {
self.mode = AppMode::Normal;
self.tour_runner = None;
self.overlays.close();
} else {
self.mode = AppMode::Tour;
self.tour_step = 0;
self.tour_runner = Some(TourRunner::new(true, false));
self.overlays.open(Overlay::Tour(TourState::default()));
if let Some(action) = self
.tour_runner
.as_ref()
.and_then(TourRunner::execute_step_action)
{
self.apply_tour_action(action);
}
}
}
Action::CloseOverlay => {
self.mode = AppMode::Normal;
self.overlays.close();
}
Action::CycleFocusForward => {
if self.mode == AppMode::Normal {
self.focus = self.focus.next();
}
}
Action::CycleFocusBackward => {
if self.mode == AppMode::Normal {
self.focus = self.focus.prev();
}
}
Action::NavigateSection(section) => {
self.section = *section;
}
Action::ForceRedraw => {
self.force_redraw = true;
}
Action::ToggleDebug => {
self.show_debug = !self.show_debug;
}
Action::CycleTheme => {
self.ui_theme = self.ui_theme.next();
}
Action::FocusChanged(gained) => {
self.paused = !gained;
}
Action::PaletteUp => {
if let Some(Overlay::Palette(ref mut state)) = self.overlays.active {
state.select_prev();
}
}
Action::PaletteDown => {
if let Some(Overlay::Palette(ref mut state)) = self.overlays.active {
state.select_next();
}
}
Action::PaletteBackspace => {
if let Some(Overlay::Palette(ref mut state)) = self.overlays.active {
state.query.pop();
state.update_filter();
}
}
Action::PaletteChar(c) => {
if let Some(Overlay::Palette(ref mut state)) = self.overlays.active {
state.query.push(*c);
state.update_filter();
}
}
Action::PaletteExecute => {
let cmd_info = if let Some(Overlay::Palette(ref state)) = self.overlays.active {
if let Some(&cmd_idx) = state.filtered.get(state.selected) {
let (name, _) = PaletteState::COMMANDS[cmd_idx];
let action = match cmd_idx {
0 => Some(Action::ToggleHelp), 1 => Some(Action::ToggleTour), 2 => Some(Action::CycleTheme), 3 => Some(Action::ForceRedraw), 4 => Some(Action::ToggleDebug), 5 => Some(Action::NavigateSection(Section::Overview)), 6 => Some(Action::NavigateSection(Section::Editor)), 7 => Some(Action::NavigateSection(Section::Preview)), 8 => Some(Action::NavigateSection(Section::Logs)), 9 => Some(Action::NavigateSection(Section::Unicode)), 10 => Some(Action::NavigateSection(Section::Performance)), 11 => Some(Action::NavigateSection(Section::Drawing)), 12 => Some(Action::NavigateSection(Section::Colors)), 13 => Some(Action::NavigateSection(Section::Input)), 14 => Some(Action::NavigateSection(Section::Editing)), 15 => Some(Action::NavigateSection(Section::Capabilities)), 16 => Some(Action::NavigateSection(Section::Animations)), 17 => Some(Action::Quit), _ => None,
};
action.map(|a| (name, a))
} else {
None
}
} else {
None
};
self.mode = AppMode::Normal;
self.overlays.close();
if let Some((name, cmd)) = cmd_info {
if !matches!(cmd, Action::Quit) {
self.toast_info(format!("Executed: {name}"));
}
self.apply_action(&cmd);
}
}
Action::SetFocus(focus) => {
if self.mode == AppMode::Normal {
self.focus = *focus;
}
}
Action::PaletteClick(idx) => {
if let Some(Overlay::Palette(ref mut state)) = self.overlays.active {
if *idx < state.filtered.len() {
state.selected = *idx;
}
}
self.apply_action(&Action::PaletteExecute);
}
Action::Resize(_, _) | Action::None => {}
}
}
#[allow(clippy::missing_const_for_fn, clippy::must_use_candidate)] pub fn tick(&mut self) {
self.clock.tick(self.paused);
self.frame_count = self.frame_count.wrapping_add(1);
self.update_metrics();
self.force_redraw = false;
self.overlays.tick(self.clock.dt);
self.toasts.tick(self.clock.dt);
if self.mode == AppMode::Tour {
self.tick_tour();
}
if !self.overlays.is_active() && self.mode != AppMode::Normal && self.mode != AppMode::Tour
{
self.mode = AppMode::Normal;
}
if let Some(max) = self.max_frames {
if self.frame_count >= max {
self.should_quit = true;
self.exit_reason = ExitReason::MaxFrames;
}
}
}
pub fn handle_resize(&mut self, width: u32, height: u32) {
if let Some(Overlay::Help(ref mut state)) = self.overlays.active {
state.scroll = 0;
}
self.toasts
.push(Toast::info(format!("Resized to {width}×{height}")));
self.force_redraw = true;
}
fn tick_tour(&mut self) {
let current_t = self.clock.t;
let Some(runner) = self.tour_runner.as_mut() else {
return;
};
if runner.should_auto_advance(current_t) {
let is_last = runner.next_step(current_t);
let action = runner.execute_step_action();
let step_idx = runner.step_idx;
let exit_on_complete = runner.exit_on_complete;
self.tour_step = step_idx;
if let Some(action) = action {
self.apply_tour_action(action);
}
if is_last && exit_on_complete {
self.should_quit = true;
self.exit_reason = ExitReason::TourComplete;
}
}
}
fn apply_tour_action(&mut self, action: TourAction) {
match action {
TourAction::None => {}
TourAction::SetFocus(focus) => {
self.focus = focus;
}
TourAction::SetSection(section) => {
self.section = section;
}
TourAction::OpenHelp => {
if self.mode != AppMode::Help {
self.overlays.open(Overlay::Help(HelpState::default()));
}
}
TourAction::OpenPalette => {
if self.mode != AppMode::CommandPalette {
let mut state = PaletteState::default();
state.update_filter();
self.overlays.open(Overlay::Palette(state));
}
}
TourAction::CloseOverlay => {
self.overlays.close();
}
TourAction::CycleTheme => {
self.ui_theme = self.ui_theme.next();
}
TourAction::ShowDebug => {
self.show_debug = true;
}
}
}
#[must_use]
pub const fn mode_name(&self) -> &'static str {
match self.mode {
AppMode::Normal => "Normal",
AppMode::Help => "Help",
AppMode::CommandPalette => "Palette",
AppMode::Tour => "Tour",
}
}
#[must_use]
pub const fn focus_name(&self) -> &'static str {
match self.focus {
Focus::Sidebar => "Sidebar",
Focus::Editor => "Editor",
Focus::Preview => "Preview",
Focus::Logs => "Logs",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputSource {
Real,
Synthetic,
}
#[derive(Clone, Debug)]
pub struct TaggedEvent {
pub event: Event,
pub source: InputSource,
}
impl TaggedEvent {
#[must_use]
pub const fn real(event: Event) -> Self {
Self {
event,
source: InputSource::Real,
}
}
#[must_use]
pub const fn synthetic(event: Event) -> Self {
Self {
event,
source: InputSource::Synthetic,
}
}
}
pub struct InputPump {
parser: InputParser,
accumulator: Vec<u8>,
scratch: [u8; 1024],
synthetic_queue: Vec<Event>,
max_accumulator_size: usize,
}
impl InputPump {
#[must_use]
pub fn new() -> Self {
Self {
parser: InputParser::new(),
accumulator: Vec::with_capacity(256),
scratch: [0u8; 1024],
synthetic_queue: Vec::new(),
max_accumulator_size: 64 * 1024, }
}
pub fn inject_synthetic(&mut self, event: Event) {
self.synthetic_queue.push(event);
}
pub fn poll(&mut self, timeout: Duration) -> io::Result<Vec<TaggedEvent>> {
let mut events = Vec::new();
if !self.synthetic_queue.is_empty() {
for event in self.synthetic_queue.drain(..) {
events.push(TaggedEvent::synthetic(event));
}
}
if self.wait_for_input(timeout)? {
match io::stdin().read(&mut self.scratch) {
Ok(n) if n > 0 => {
let space = self
.max_accumulator_size
.saturating_sub(self.accumulator.len());
let to_add = n.min(space);
self.accumulator.extend_from_slice(&self.scratch[..to_add]);
self.parse_accumulated(&mut events);
}
Ok(_) => {} Err(e) if e.kind() == io::ErrorKind::WouldBlock => {} Err(e) => return Err(e),
}
}
Ok(events)
}
#[cfg(unix)]
#[allow(clippy::cast_possible_wrap)] #[allow(clippy::unused_self)] fn wait_for_input(&self, timeout: Duration) -> io::Result<bool> {
use std::os::unix::io::AsRawFd;
let stdin_fd = io::stdin().as_raw_fd();
let mut read_fds = std::mem::MaybeUninit::<libc::fd_set>::uninit();
unsafe {
libc::FD_ZERO(read_fds.as_mut_ptr());
libc::FD_SET(stdin_fd, read_fds.as_mut_ptr());
}
#[allow(clippy::cast_lossless)]
let tv_usec = timeout.subsec_micros() as libc::suseconds_t;
let mut tv = libc::timeval {
tv_sec: timeout.as_secs() as libc::time_t,
tv_usec,
};
let result = unsafe {
libc::select(
stdin_fd + 1,
read_fds.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::from_mut(&mut tv),
)
};
match result {
-1 => {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
Ok(false)
} else {
Err(err)
}
}
0 => Ok(false), _ => Ok(true), }
}
#[cfg(not(unix))]
#[allow(clippy::unused_self)] fn wait_for_input(&self, _timeout: Duration) -> io::Result<bool> {
Ok(true)
}
fn parse_accumulated(&mut self, events: &mut Vec<TaggedEvent>) {
let mut offset = 0;
while offset < self.accumulator.len() {
match self.parser.parse(&self.accumulator[offset..]) {
Ok((event, consumed)) => {
events.push(TaggedEvent::real(event));
offset += consumed;
}
Err(opentui::input::ParseError::Incomplete) => {
break;
}
Err(opentui::input::ParseError::Empty) => {
break;
}
Err(_) => {
offset += 1;
}
}
}
if offset > 0 {
self.accumulator.drain(..offset);
}
}
pub fn clear(&mut self) {
self.accumulator.clear();
}
}
impl Default for InputPump {
fn default() -> Self {
Self::new()
}
}
fn install_panic_hook() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = attempt_terminal_cleanup();
let _ = std::io::Write::write_all(
&mut std::io::stderr(),
b"\n\x1b[0m[demo_showcase] If your terminal is broken, run: reset\n\
Or: stty sane && clear\n\n",
);
original_hook(panic_info);
}));
}
fn attempt_terminal_cleanup() -> io::Result<()> {
use std::io::Write;
let mut stderr = std::io::stderr().lock();
stderr.write_all(b"\x1b[0m")?;
stderr.write_all(b"\x1b[?25h")?;
stderr.write_all(b"\x1b[?1049l")?;
stderr.write_all(b"\x1b[?1006l")?; stderr.write_all(b"\x1b[?1003l")?; stderr.write_all(b"\x1b[?1000l")?; stderr.write_all(b"\x1b[?2004l")?;
stderr.flush()?;
restore_cooked_mode();
Ok(())
}
fn restore_cooked_mode() {
unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(libc::STDIN_FILENO, &raw mut termios) == 0 {
termios.c_lflag |= libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG;
termios.c_iflag |= libc::IXON | libc::ICRNL;
termios.c_oflag |= libc::OPOST;
let _ = libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, &raw const termios);
}
}
}
const LOG_QUEUE_CAPACITY: usize = 500;
fn dropped_log_count() -> &'static std::sync::atomic::AtomicUsize {
static COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
&COUNT
}
fn log_queue() -> &'static Arc<Mutex<VecDeque<content::LogEntry>>> {
static QUEUE: OnceLock<Arc<Mutex<VecDeque<content::LogEntry>>>> = OnceLock::new();
QUEUE.get_or_init(|| Arc::new(Mutex::new(VecDeque::with_capacity(LOG_QUEUE_CAPACITY))))
}
fn install_log_routing() {
let queue = log_queue().clone();
set_log_callback(move |level, message| {
let demo_level = match level {
OpentuiLogLevel::Debug => content::LogLevel::Debug,
OpentuiLogLevel::Info => content::LogLevel::Info,
OpentuiLogLevel::Warn => content::LogLevel::Warn,
OpentuiLogLevel::Error => content::LogLevel::Error,
};
let entry = content::LogEntry::new_runtime(
current_timestamp(),
demo_level,
"opentui".to_string(),
message.to_string(),
);
if let Ok(mut q) = queue.lock() {
q.push_back(entry);
while q.len() > LOG_QUEUE_CAPACITY {
q.pop_front();
dropped_log_count().fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
});
}
fn current_timestamp() -> String {
use std::time::SystemTime;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let hours = (secs / 3600) % 24;
let mins = (secs / 60) % 60;
let seconds = secs % 60;
format!("{hours:02}:{mins:02}:{seconds:02}")
}
fn drain_log_queue(app: &mut App) {
let dropped = dropped_log_count().swap(0, std::sync::atomic::Ordering::Relaxed);
if dropped > 0 {
app.add_log(content::LogEntry::new_runtime(
current_timestamp(),
content::LogLevel::Warn,
"logs".to_string(),
format!("{dropped} log entries dropped (queue overflow)"),
));
}
if let Ok(mut queue) = log_queue().lock() {
for entry in queue.drain(..) {
app.add_log(entry);
}
}
}
#[allow(dead_code)]
pub fn log_info(subsystem: &str, message: &str) {
demo_log(content::LogLevel::Info, subsystem, message);
}
#[allow(dead_code)]
pub fn log_warn(subsystem: &str, message: &str) {
demo_log(content::LogLevel::Warn, subsystem, message);
}
#[allow(dead_code)]
pub fn log_error(subsystem: &str, message: &str) {
demo_log(content::LogLevel::Error, subsystem, message);
}
#[allow(dead_code)]
pub fn log_debug(subsystem: &str, message: &str) {
demo_log(content::LogLevel::Debug, subsystem, message);
}
fn demo_log(level: content::LogLevel, subsystem: &str, message: &str) {
let entry = content::LogEntry::new_runtime(
current_timestamp(),
level,
subsystem.to_string(),
message.to_string(),
);
if let Ok(mut queue) = log_queue().lock() {
queue.push_back(entry);
while queue.len() > LOG_QUEUE_CAPACITY {
queue.pop_front();
}
}
}
fn main() -> io::Result<()> {
install_panic_hook();
install_log_routing();
match Config::from_args(std::env::args_os()) {
ParseResult::Config(config) => {
if config.headless_smoke {
run_headless_smoke(&config);
Ok(())
} else if let Some(ref check_name) = config.headless_check {
run_headless_check(&config, check_name);
Ok(())
} else {
run_interactive(&config)
}
}
ParseResult::Help => {
print!("{HELP_TEXT}");
Ok(())
}
ParseResult::Error(msg) => {
eprintln!("Error: {msg}");
eprintln!("Run with --help for usage information.");
std::process::exit(1);
}
}
}
fn headless_draw_frame(buffer: &mut OptimizedBuffer, app: &App) {
let (width, height) = (buffer.width(), buffer.height());
let panels = PanelLayout::compute(width, height);
let theme = app.ui_theme.tokens();
let links = PreallocatedLinks::default();
let mut pool = GraphemePool::new();
draw_pass_background(buffer, &theme);
if panels.mode == LayoutMode::TooSmall {
draw_too_small_message(buffer, width, height, &theme);
return;
}
if app.paused {
buffer.push_opacity(0.5);
}
draw_pass_chrome(buffer, &panels, &theme, app);
draw_pass_panels(buffer, &mut pool, &panels, &theme, app, &links);
draw_pass_overlays(buffer, &panels, &theme, app, &links);
draw_pass_toasts(buffer, &panels, &theme, app);
if app.paused {
buffer.pop_opacity();
}
}
#[derive(Clone, Debug)]
struct FrameStats {
frame: u64,
dirty_cells: usize,
dt: f32,
}
fn extract_buffer_row(buffer: &OptimizedBuffer, y: u32, start_x: u32, max_len: u32) -> String {
let mut result = String::new();
let end_x = start_x.saturating_add(max_len);
for x in start_x..end_x {
if let Some(cell) = buffer.get(x, y) {
match &cell.content {
CellContent::Char(c) => result.push(*c),
CellContent::Grapheme(id) => {
use std::fmt::Write;
let _ = write!(result, "[G{id:?}]");
}
CellContent::Continuation | CellContent::Empty => {}
}
}
}
result.trim_end().to_string()
}
#[allow(clippy::too_many_lines)]
fn run_headless_check(config: &Config, check_name: &str) {
let (width, height) = config.headless_size;
if !config.headless_dump_json {
eprintln!("Running headless check: {check_name} ({width}x{height})...");
}
let result = match check_name {
"layout" => run_check_layout(width, height),
"config" => run_check_config(config),
"palette" => run_check_palette(),
"hitgrid" => run_check_hitgrid(),
"logs" => run_check_logs(),
_ => {
eprintln!("Unknown check: {check_name}");
std::process::exit(1);
}
};
if config.headless_dump_json {
println!("{result}");
} else {
eprintln!("Headless check '{check_name}' PASSED");
println!("HEADLESS_CHECK_OK check={check_name}");
}
}
#[allow(clippy::too_many_lines)]
fn run_check_layout(width: u16, height: u16) -> String {
let w = u32::from(width);
let h = u32::from(height);
let test_sizes: &[(u32, u32, &str)] = &[
(120, 40, "full"),
(80, 24, "full"),
(79, 24, "compact"),
(60, 16, "compact"),
(59, 16, "minimal"),
(40, 12, "minimal"),
(39, 12, "too_small"),
(20, 8, "too_small"),
];
let mut results: Vec<String> = Vec::new();
for &(tw, th, expected_mode) in test_sizes {
let layout = PanelLayout::compute(tw, th);
let actual_mode = format!("{:?}", layout.mode).to_lowercase();
let mode_matches = actual_mode.contains(expected_mode);
let valid_rects = [
("screen", &layout.screen),
("top_bar", &layout.top_bar),
("status_bar", &layout.status_bar),
("content", &layout.content),
("sidebar", &layout.sidebar),
("main_area", &layout.main_area),
("editor", &layout.editor),
("preview", &layout.preview),
("logs", &layout.logs),
];
let mut all_valid = true;
let mut rect_info: Vec<String> = Vec::new();
for (name, rect) in valid_rects {
#[allow(clippy::cast_possible_wrap)]
let in_bounds = rect.x.saturating_add(rect.w as i32) <= tw as i32
&& rect.y.saturating_add(rect.h as i32) <= th as i32;
if !in_bounds && rect.w > 0 && rect.h > 0 {
all_valid = false;
}
rect_info.push(format!(
r#"{{"name":"{}","x":{},"y":{},"w":{},"h":{},"in_bounds":{}}}"#,
name, rect.x, rect.y, rect.w, rect.h, in_bounds
));
}
results.push(format!(
r#"{{"size":[{},{}],"expected_mode":"{}","actual_mode":"{}","mode_matches":{},"all_valid":{},"rects":[{}]}}"#,
tw, th, expected_mode, actual_mode, mode_matches, all_valid, rect_info.join(",")
));
}
let main_layout = PanelLayout::compute(w, h);
format!(
r#"{{"check":"layout","passed":true,"requested_size":[{},{}],"main_layout":{{"size":[{},{}],"mode":"{:?}"}},"test_results":[{}]}}"#,
width,
height,
w,
h,
main_layout.mode,
results.join(",")
)
}
#[allow(
clippy::too_many_lines,
clippy::redundant_closure_for_method_calls,
clippy::uninlined_format_args
)]
fn run_check_config(config: &Config) -> String {
let test_cases: &[(&[&str], &str)] = &[
(&["demo_showcase"], "default"),
(&["demo_showcase", "--fps", "30"], "fps_30"),
(&["demo_showcase", "--no-mouse"], "no_mouse"),
(
&["demo_showcase", "--tour", "--exit-after-tour"],
"tour_exit",
),
(
&["demo_showcase", "--cap-preset", "minimal"],
"minimal_preset",
),
(&["demo_showcase", "--seed", "42"], "seed_42"),
];
let mut results: Vec<String> = Vec::new();
for (args, label) in test_cases {
let os_args: Vec<std::ffi::OsString> = args.iter().map(|s| s.into()).collect();
let (parsed_ok, cfg_summary) = match Config::from_args(os_args) {
ParseResult::Config(cfg) => (
true,
format!(
r#"{{"fps_cap":{},"enable_mouse":{},"seed":{}}}"#,
cfg.fps_cap, cfg.enable_mouse, cfg.seed
),
),
ParseResult::Help => (true, r#"{"help":true}"#.to_string()),
ParseResult::Error(e) => (
false,
format!(r#"{{"error":"{}"}}"#, e.replace('"', "\\\"")),
),
};
results.push(format!(
r#"{{"label":"{}","parsed_ok":{},"config":{}}}"#,
label, parsed_ok, cfg_summary
));
}
format!(
r#"{{"check":"config","passed":true,"current_config":{{"fps_cap":{},"seed":{}}},"test_results":[{}]}}"#,
config.fps_cap,
config.seed,
results.join(",")
)
}
#[allow(clippy::uninlined_format_args)]
fn run_check_palette() -> String {
let mut state = PaletteState::default();
state.update_filter();
let all_count = state.filtered.len();
let total_commands = PaletteState::COMMANDS.len();
let mut filter_results: Vec<String> = Vec::new();
for (query, label) in [
("", "empty"),
("help", "help"),
("toggle", "toggle"),
("xyz", "no_match"),
] {
state.query = query.to_string();
state.selected = 0;
state.update_filter();
filter_results.push(format!(
r#"{{"label":"{}","match_count":{}}}"#,
label,
state.filtered.len()
));
}
state.query.clear();
state.selected = 0;
state.update_filter();
for _ in 0..3 {
state.select_next();
}
let after_next = state.selected;
for _ in 0..2 {
state.select_prev();
}
let after_prev = state.selected;
format!(
r#"{{"check":"palette","passed":true,"total_commands":{},"all_shown_on_empty":{},"filter_tests":[{}],"nav_tests":{{"after_3_next":{},"after_2_prev":{}}}}}"#,
total_commands,
all_count == total_commands,
filter_results.join(","),
after_next,
after_prev
)
}
#[allow(clippy::uninlined_format_args)]
fn run_check_hitgrid() -> String {
let id_tests = [
("BTN_HELP", hit_ids::BTN_HELP, 1000),
("BTN_PALETTE", hit_ids::BTN_PALETTE, 1001),
("BTN_TOUR", hit_ids::BTN_TOUR, 1002),
("BTN_THEME", hit_ids::BTN_THEME, 1003),
("SIDEBAR_ROW_BASE", hit_ids::SIDEBAR_ROW_BASE, 2000),
("PANEL_SIDEBAR", hit_ids::PANEL_SIDEBAR, 3000),
("PANEL_EDITOR", hit_ids::PANEL_EDITOR, 3001),
("OVERLAY_CLOSE", hit_ids::OVERLAY_CLOSE, 4000),
("PALETTE_ITEM_BASE", hit_ids::PALETTE_ITEM_BASE, 4100),
];
let mut all_match = true;
let mut results: Vec<String> = Vec::new();
for (name, actual, expected) in id_tests {
let m = actual == expected;
if !m {
all_match = false;
}
results.push(format!(
r#"{{"name":"{}","actual":{},"expected":{},"matches":{}}}"#,
name, actual, expected, m
));
}
format!(
r#"{{"check":"hitgrid","passed":{},"id_tests":[{}]}}"#,
all_match,
results.join(",")
)
}
#[allow(clippy::uninlined_format_args)]
fn run_check_logs() -> String {
let max_entries = 100;
let mut log_buffer: VecDeque<&str> = VecDeque::with_capacity(max_entries);
for i in 0..150 {
if log_buffer.len() >= max_entries {
log_buffer.pop_front();
}
log_buffer.push_back(if i % 2 == 0 { "INFO" } else { "DEBUG" });
}
let final_count = log_buffer.len();
let oldest_dropped = final_count == max_entries;
let mut selection = 0_usize;
let max_sel = final_count.saturating_sub(1);
selection = selection.saturating_add(5).min(max_sel);
let after_down = selection;
selection = selection.saturating_sub(3);
let after_up = selection;
format!(
r#"{{"check":"logs","passed":true,"ring_buffer":{{"max":{},"final":{},"oldest_dropped":{}}},"selection":{{"after_down":{},"after_up":{}}}}}"#,
max_entries, final_count, oldest_dropped, after_down, after_up
)
}
#[allow(clippy::too_many_lines)]
fn run_headless_smoke(config: &Config) {
use opentui::renderer::BufferDiff;
let (width, height) = config.headless_size;
let frame_count = config.max_frames.unwrap_or(10);
if !config.headless_dump_json {
eprintln!("Running headless smoke test ({width}x{height})...");
}
let demo_content = content::DemoContent::default();
let mut app = App::with_content(config, &demo_content);
app.update_effective_caps(None);
let mut current_buffer = OptimizedBuffer::new(u32::from(width), u32::from(height));
let mut previous_buffer = OptimizedBuffer::new(u32::from(width), u32::from(height));
let mut last_dirty_cells: usize = 0;
let mut total_dirty_cells: usize = 0;
let mut frame_stats: Vec<FrameStats> =
Vec::with_capacity(usize::try_from(frame_count).unwrap_or(64));
let mut tour_step_history: Vec<(u64, usize, String)> = Vec::new();
let fixed_dt: f32 = 1.0 / 60.0;
for frame in 0..frame_count {
app.frame_count = frame;
app.clock.dt = fixed_dt;
app.clock.t += fixed_dt;
app.update_metrics();
let step_before = app.tour_runner.as_ref().map(|r| r.step_idx);
app.tick_tour();
let step_after = app.tour_runner.as_ref().map(|r| r.step_idx);
if let (Some(before), Some(after)) = (step_before, step_after) {
if before != after {
let title = TOUR_SCRIPT.get(after).map_or("unknown", |s| s.title);
tour_step_history.push((frame, after, title.to_string()));
}
} else if let (None, Some(after)) = (step_before, step_after) {
let title = TOUR_SCRIPT.get(after).map_or("unknown", |s| s.title);
tour_step_history.push((frame, after, title.to_string()));
}
headless_draw_frame(&mut current_buffer, &app);
let dirty_cells = if frame > 0 {
let diff = BufferDiff::compute(&previous_buffer, ¤t_buffer);
diff.change_count
} else {
(width as usize) * (height as usize)
};
last_dirty_cells = dirty_cells;
total_dirty_cells += dirty_cells;
frame_stats.push(FrameStats {
frame,
dirty_cells,
dt: app.clock.dt,
});
std::mem::swap(&mut current_buffer, &mut previous_buffer);
}
assert_eq!(previous_buffer.width(), u32::from(width));
assert_eq!(previous_buffer.height(), u32::from(height));
let panels = PanelLayout::compute(u32::from(width), u32::from(height));
let top_bar_text = extract_buffer_row(&previous_buffer, 0, 0, width.into());
let section_name = app.section.name().to_string();
let layout_mode = format!("{:?}", panels.mode);
if config.headless_dump_json {
let frame_stats_json: Vec<String> = frame_stats
.iter()
.map(|f| {
format!(
r#"{{"frame":{},"dirty_cells":{},"dt":{:.6}}}"#,
f.frame, f.dirty_cells, f.dt
)
})
.collect();
let caps = &app.effective_caps;
let warnings_json: String = if caps.degraded.is_empty() {
"[]".to_string()
} else {
let items: Vec<String> = caps.degraded.iter().map(|s| format!("\"{s}\"")).collect();
format!("[{}]", items.join(", "))
};
let tour_state_json = if config.start_in_tour {
let final_step = app.tour_runner.as_ref().map_or(0, |r| r.step_idx);
let final_title = TOUR_SCRIPT.get(final_step).map_or("unknown", |s| s.title);
let completed = app.tour_runner.as_ref().is_some_and(|r| r.completed);
let steps_json: Vec<String> = tour_step_history
.iter()
.map(|(frame, idx, title)| {
format!(
r#"{{"frame":{},"step_idx":{},"title":"{}"}}"#,
frame,
idx,
title.replace('"', "\\\"")
)
})
.collect();
format!(
r#",
"tour_state": {{
"active": true,
"completed": {},
"final_step_idx": {},
"final_step_title": "{}",
"total_steps": {},
"step_transitions": [
{}
]
}}"#,
completed,
final_step,
final_title.replace('"', "\\\""),
TOUR_SCRIPT.len(),
steps_json.join(",\n ")
)
} else {
String::new()
};
let json = format!(
r#"{{
"config": {{
"fps_cap": {},
"seed": {},
"enable_mouse": {},
"use_alt_screen": {},
"start_in_tour": {},
"cap_preset": "{:?}"
}},
"headless_size": {{
"width": {},
"height": {}
}},
"effective_caps": {{
"truecolor": {},
"mouse": {},
"hyperlinks": {},
"focus": {},
"sync_output": {}
}},
"warnings": {},
"layout_mode": "{}",
"frames_rendered": {},
"total_dirty_cells": {},
"last_dirty_cells": {},
"sentinels": {{
"top_bar": "{}",
"section": "{}"
}},
"frame_stats": [
{}
]{}
}}"#,
config.fps_cap,
config.seed,
config.enable_mouse,
config.use_alt_screen,
config.start_in_tour,
config.cap_preset,
width,
height,
caps.truecolor,
caps.mouse,
caps.hyperlinks,
caps.focus,
caps.sync_output,
warnings_json,
layout_mode,
frame_count,
total_dirty_cells,
last_dirty_cells,
top_bar_text.replace('\\', "\\\\").replace('"', "\\\""),
section_name,
frame_stats_json.join(",\n "),
tour_state_json
);
println!("{json}");
} else {
eprintln!("Headless smoke test PASSED");
eprintln!(" Buffer size: {width}x{height}");
eprintln!(" Frames rendered: {frame_count}");
eprintln!(" Total dirty cells: {total_dirty_cells}");
eprintln!(" Seed: {}", config.seed);
println!("HEADLESS_SMOKE_OK frames={frame_count} last_dirty_cells={last_dirty_cells}");
}
}
fn run_interactive(config: &Config) -> io::Result<()> {
if !is_tty() {
eprintln!("Error: stdout is not a terminal");
eprintln!();
eprintln!("demo_showcase requires an interactive terminal to run.");
eprintln!("For non-interactive use, try: demo_showcase --headless-smoke");
std::process::exit(1);
}
let (width, height) = terminal_size().unwrap_or((80, 24));
let mut renderer = Renderer::new_with_options(
u32::from(width),
u32::from(height),
config.renderer_options(),
)?;
let _raw_guard = enable_raw_mode()?;
set_stdin_nonblocking()?;
let mut app = App::new(config);
app.update_effective_caps(Some(renderer.capabilities()));
let mut input_pump = InputPump::new();
let frame_duration = config.frame_duration();
let input_timeout = Duration::from_millis(1);
while !app.should_quit {
let frame_start = Instant::now();
match input_pump.poll(input_timeout) {
Ok(events) => {
for tagged_event in events {
match &tagged_event.event {
Event::Resize(resize) => {
let new_w = u32::from(resize.width);
let new_h = u32::from(resize.height);
if let Err(e) = renderer.resize(new_w, new_h) {
eprintln!("Resize error: {e}");
}
app.handle_resize(new_w, new_h);
}
Event::Mouse(mouse) => {
if mouse.button == MouseButton::Left {
if let Some(hit_id) = renderer.hit_test(mouse.x, mouse.y) {
let action = App::hit_to_action(hit_id, mouse.kind);
app.apply_action(&action);
}
}
}
_ => {}
}
app.handle_event(&tagged_event.event);
}
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {
}
Err(e) => {
eprintln!("Input error: {e}");
}
}
drain_log_queue(&mut app);
let needs_full_redraw = app.force_redraw;
app.tick();
if needs_full_redraw {
renderer.invalidate();
}
let inspector = if app.show_debug {
Some(InspectorData::gather(
renderer.stats(),
renderer.capabilities(),
&app,
config.threaded,
config.fps_cap,
))
} else {
None
};
draw_frame(&mut renderer, &app, inspector.as_ref());
renderer.present()?;
let elapsed = frame_start.elapsed();
if let Some(remaining) = frame_duration.checked_sub(elapsed) {
std::thread::sleep(remaining);
}
}
if app.exit_reason == ExitReason::MaxFrames {
let last_dirty_cells = renderer.stats().last_frame_cells;
println!(
"EXIT_OK reason=max_frames frames={} last_dirty_cells={}",
app.frame_count, last_dirty_cells
);
}
Ok(())
}
#[derive(Clone, Debug, Default)]
#[allow(clippy::struct_field_names)]
struct PreallocatedLinks {
repo_url: Option<u32>,
docs_url: Option<u32>,
#[allow(dead_code)]
unicode_url: Option<u32>,
}
impl PreallocatedLinks {
fn allocate(link_pool: &mut opentui::LinkPool, hyperlinks_enabled: bool) -> Self {
if !hyperlinks_enabled {
return Self::default();
}
Self {
repo_url: Some(link_pool.alloc("https://github.com/opentui/opentui")),
docs_url: Some(link_pool.alloc("https://opentui.dev")),
unicode_url: Some(link_pool.alloc("https://unicode.org/charts/")),
}
}
}
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)] struct InspectorData {
fps: f32,
frame_time_ms: f32,
cells_updated: usize,
#[allow(dead_code)] buffer_bytes: usize,
#[allow(dead_code)] hitgrid_bytes: usize,
total_bytes: usize,
truecolor: bool,
sync_output: bool,
hyperlinks: bool,
mouse: bool,
focus: bool,
bracketed_paste: bool,
tour_active: bool,
threaded: bool,
fps_cap: u32,
}
impl InspectorData {
fn gather(
stats: &opentui::RenderStats,
caps: &opentui::terminal::Capabilities,
app: &App,
threaded: bool,
fps_cap: u32,
) -> Self {
Self {
fps: stats.fps,
frame_time_ms: stats.last_frame_time.as_secs_f32() * 1000.0,
cells_updated: stats.last_frame_cells,
buffer_bytes: stats.buffer_bytes,
hitgrid_bytes: stats.hitgrid_bytes,
total_bytes: stats.total_bytes,
truecolor: caps.has_true_color(),
sync_output: caps.sync_output,
hyperlinks: caps.hyperlinks,
mouse: caps.mouse,
focus: caps.focus,
bracketed_paste: caps.bracketed_paste,
tour_active: app.mode == AppMode::Tour,
threaded,
fps_cap,
}
}
}
fn draw_frame(renderer: &mut Renderer, app: &App, inspector: Option<&InspectorData>) {
let (width, height) = renderer.size();
let panels = PanelLayout::compute(width, height);
let theme = app.ui_theme.tokens();
let links = PreallocatedLinks::allocate(renderer.link_pool(), app.effective_caps.hyperlinks);
let (buffer, pool) = renderer.buffer_with_pool();
draw_pass_background(buffer, &theme);
if panels.mode == LayoutMode::TooSmall {
draw_too_small_message(buffer, width, height, &theme);
return;
}
if app.paused {
buffer.push_opacity(0.5);
}
draw_pass_chrome(buffer, &panels, &theme, app);
draw_pass_panels(buffer, pool, &panels, &theme, app, &links);
draw_pass_overlays(buffer, &panels, &theme, app, &links);
draw_pass_toasts(buffer, &panels, &theme, app);
if app.show_debug {
if let Some(data) = inspector {
draw_pass_debug(buffer, &panels, &theme, data);
}
}
if app.paused {
buffer.pop_opacity();
}
register_hit_areas(renderer, &panels, app);
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] fn register_hit_areas(renderer: &mut Renderer, panels: &PanelLayout, app: &App) {
use hit_ids::{
PALETTE_ITEM_BASE, PANEL_EDITOR, PANEL_LOGS, PANEL_PREVIEW, PANEL_SIDEBAR, SIDEBAR_ROW_BASE,
};
if !panels.sidebar.is_empty() {
let r = &panels.sidebar;
renderer.register_hit_area(r.x as u32, r.y as u32, r.w, r.h, PANEL_SIDEBAR);
}
{
let r = &panels.editor;
renderer.register_hit_area(r.x as u32, r.y as u32, r.w, r.h, PANEL_EDITOR);
}
if !panels.preview.is_empty() {
let r = &panels.preview;
renderer.register_hit_area(r.x as u32, r.y as u32, r.w, r.h, PANEL_PREVIEW);
}
{
let r = &panels.logs;
renderer.register_hit_area(r.x as u32, r.y as u32, r.w, r.h, PANEL_LOGS);
}
if !panels.sidebar.is_empty() {
let sidebar = &panels.sidebar;
for (i, _section) in Section::ALL.iter().enumerate() {
let row_y = sidebar.y as u32 + 2 + (i as u32 * 2);
if row_y < (sidebar.y as u32 + sidebar.h - 2) {
renderer.register_hit_area(
sidebar.x as u32,
row_y,
sidebar.w,
2, SIDEBAR_ROW_BASE + i as u32,
);
}
}
}
if let Some(Overlay::Palette(state)) = &app.overlays.active {
let overlay_w = (panels.screen.w * 50 / 100).clamp(40, 60);
let overlay_h = (state.filtered.len() as u32 + 4)
.min(panels.screen.h * 50 / 100)
.max(6);
let overlay_x = (panels.screen.w - overlay_w) / 2;
let overlay_y = panels.screen.h / 4;
let list_y = overlay_y + 4;
let max_items = (overlay_h - 5).min(state.filtered.len() as u32);
for i in 0..max_items as usize {
renderer.register_hit_area(
overlay_x,
list_y + i as u32,
overlay_w,
1, PALETTE_ITEM_BASE + i as u32,
);
}
}
}
fn draw_pass_background(buffer: &mut OptimizedBuffer, theme: &Theme) {
buffer.clear(theme.bg0);
}
#[allow(clippy::cast_precision_loss)] fn draw_pass_chrome(buffer: &mut OptimizedBuffer, panels: &PanelLayout, theme: &Theme, app: &App) {
let gradient_end = Theme::lerp(theme.bg1, theme.bg2, 0.3);
draw_gradient_bar(buffer, &panels.top_bar, theme.bg1, gradient_end);
let top_y = u32::try_from(panels.top_bar.y).unwrap_or(0);
let top_x = u32::try_from(panels.top_bar.x).unwrap_or(0);
buffer.draw_text(
top_x + 2,
top_y,
"OpenTUI",
Style::fg(theme.accent_primary).with_bold(),
);
buffer.draw_text(top_x + 10, top_y, "Showcase", Style::fg(theme.fg1));
if panels.top_bar.w > 60 {
let section_text = app.section.name();
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let section_len = section_text.len() as i32;
#[allow(clippy::cast_possible_wrap)]
let center_x = (panels.top_bar.w as i32 / 2) - (section_len / 2);
#[allow(clippy::cast_possible_wrap)]
let draw_x = top_x as i32 + center_x;
buffer.draw_text(
u32::try_from(draw_x).unwrap_or(0),
top_y,
section_text,
Style::fg(theme.fg0),
);
}
let mode_badge = format!("[{}]", app.mode_name());
let focus_text = format!(" {} ", app.focus_name());
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let focus_len = focus_text.len() as i32;
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let mode_len = mode_badge.len() as i32;
let focus_x = panels.top_bar.right() - focus_len - 1;
buffer.draw_text(
u32::try_from(focus_x).unwrap_or(0),
top_y,
&focus_text,
Style::fg(theme.bg0).with_bg(theme.accent_primary),
);
let mode_x = focus_x - mode_len - 2;
let mode_color = match app.mode {
AppMode::Normal => theme.fg2,
AppMode::Help => theme.accent_primary,
AppMode::CommandPalette => theme.accent_secondary,
AppMode::Tour => theme.accent_success,
};
buffer.draw_text(
u32::try_from(mode_x).unwrap_or(0),
top_y,
&mode_badge,
Style::fg(mode_color),
);
draw_rect_bg(buffer, &panels.status_bar, theme.bg2);
let status_y = u32::try_from(panels.status_bar.y).unwrap_or(0);
let hints = match app.mode {
AppMode::Normal => "Ctrl+Q Quit │ F1 Help │ Ctrl+N Theme │ Tab Focus",
AppMode::Help => "Esc Close │ ↑/↓ Scroll │ PgUp/PgDn Page",
AppMode::CommandPalette => "Esc Close │ ↑/↓ Navigate │ Enter Select",
AppMode::Tour => {
if app.tour_step < app.tour_total.saturating_sub(1) {
"Enter Next │ Backspace Prev │ Esc Exit"
} else {
"✓ Tour Complete! │ Esc Exit"
}
}
};
let status_left = if app.paused {
format!("⏸ PAUSED │ {hints}")
} else {
hints.to_string()
};
buffer.draw_text(2, status_y, &status_left, Style::fg(theme.fg2));
let fps_estimate = 60; let stats = format!(
"{} │ {}fps │ F:{}",
app.ui_theme.name(),
fps_estimate,
app.frame_count
);
let stats_len = i32::try_from(stats.len()).unwrap_or(0);
let stats_x = panels.status_bar.right() - stats_len - 2;
buffer.draw_text(
u32::try_from(stats_x).unwrap_or(0),
status_y,
&stats,
Style::fg(theme.fg1),
);
}
fn draw_pass_panels(
buffer: &mut OptimizedBuffer,
pool: &mut GraphemePool,
panels: &PanelLayout,
theme: &Theme,
app: &App,
links: &PreallocatedLinks,
) {
if !panels.sidebar.is_empty() {
draw_rect_bg(buffer, &panels.sidebar, theme.bg2);
draw_sidebar(buffer, &panels.sidebar, panels.mode, theme, app);
}
if !panels.editor.is_empty() {
match app.section {
Section::Unicode => {
draw_unicode_showcase(buffer, pool, &panels.editor, theme);
}
Section::Drawing => {
draw_drawing_section(buffer, &panels.editor, theme, app);
}
Section::Colors => {
draw_colors_section(buffer, &panels.editor, theme, app);
}
Section::Input => {
draw_input_section(buffer, &panels.editor, theme, app);
}
Section::Editing => {
draw_editing_section(buffer, &panels.editor, theme, app);
}
Section::Capabilities => {
draw_capabilities_section(buffer, &panels.editor, theme, app);
}
Section::Animations => {
draw_animations_section(buffer, &panels.editor, theme, app);
}
_ => {
draw_editor_panel(buffer, &panels.editor, theme, app);
}
}
}
if !panels.preview.is_empty() {
draw_preview_panel(buffer, &panels.preview, theme, app);
}
if !panels.logs.is_empty() {
draw_rect_bg(buffer, &panels.logs, theme.bg1);
draw_logs_panel(buffer, &panels.logs, theme, app, links);
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn draw_pass_overlays(
buffer: &mut OptimizedBuffer,
panels: &PanelLayout,
theme: &Theme,
app: &App,
links: &PreallocatedLinks,
) {
if !app.overlays.is_active() {
return;
}
let opacity = app.overlays.anim.opacity();
if opacity <= 0.0 {
return;
}
let backdrop_alpha = 0.6 * opacity;
let backdrop_color = Rgba::new(0.0, 0.0, 0.0, backdrop_alpha);
buffer.push_opacity(opacity);
for y in 0..panels.screen.h {
for x in 0..panels.screen.w {
let cell = buffer.get(x, y);
if let Some(cell) = cell {
let mut new_cell = *cell;
let existing_bg = new_cell.bg;
new_cell.bg = backdrop_color.blend_over(existing_bg);
buffer.set(x, y, new_cell);
}
}
}
match &app.overlays.active {
Some(Overlay::Help(state)) => {
draw_help_overlay(buffer, panels, theme, state, opacity, links);
}
Some(Overlay::Palette(state)) => {
draw_palette_overlay(buffer, panels, theme, state, opacity, links);
}
Some(Overlay::Tour(state)) => {
draw_tour_overlay(buffer, panels, theme, state, opacity);
}
None => {}
}
buffer.pop_opacity();
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::too_many_lines
)]
fn draw_pass_toasts(buffer: &mut OptimizedBuffer, panels: &PanelLayout, theme: &Theme, app: &App) {
if app.toasts.is_empty() {
return;
}
let toast_w = ToastManager::TOAST_WIDTH;
let toast_x = panels.screen.w.saturating_sub(toast_w + 2);
let mut toast_y = panels.status_bar.y.saturating_sub(1) as u32;
for toast in app.toasts.iter().rev() {
let opacity = toast.opacity();
if opacity <= 0.0 {
continue;
}
let has_detail = toast.detail.is_some();
let toast_h = if has_detail { 4_u32 } else { 3_u32 };
if toast_y < toast_h + 2 {
break;
}
toast_y = toast_y.saturating_sub(toast_h + ToastManager::TOAST_GAP);
let (accent_color, icon) = match toast.level {
ToastLevel::Info => (theme.accent_primary, toast.level.icon()),
ToastLevel::Warn => (
Rgba::from_hex("#ffcc00").unwrap_or(theme.accent_warning),
toast.level.icon(),
),
ToastLevel::Error => (
Rgba::from_hex("#ff4444").unwrap_or(theme.accent_error),
toast.level.icon(),
),
};
let bg_color = Rgba::new(theme.bg1.r, theme.bg1.g, theme.bg1.b, 0.9 * opacity);
let fg_color = Rgba::new(theme.fg0.r, theme.fg0.g, theme.fg0.b, opacity);
let accent_with_alpha = Rgba::new(accent_color.r, accent_color.g, accent_color.b, opacity);
for row in toast_y..toast_y + toast_h {
for col in toast_x..toast_x + toast_w {
if let Some(cell) = buffer.get(col, row) {
let mut new_cell = *cell;
new_cell.bg = bg_color.blend_over(cell.bg);
buffer.set(col, row, new_cell);
}
}
}
for row in toast_y..toast_y + toast_h {
buffer.draw_text(
toast_x,
row,
"▌",
Style::fg(accent_with_alpha).with_bg(bg_color),
);
}
let content_x = toast_x + 2;
let title_y = toast_y + 1;
buffer.draw_text(
content_x,
title_y,
icon,
Style::fg(accent_with_alpha).with_bold(),
);
let title_start = content_x + 2;
let max_title_len = (toast_w - 5) as usize;
let title = if toast.title.len() > max_title_len {
format!("{}…", &toast.title[..max_title_len - 1])
} else {
toast.title.clone()
};
buffer.draw_text(
title_start,
title_y,
&title,
Style::fg(fg_color).with_bold(),
);
if let Some(detail) = &toast.detail {
let detail_y = title_y + 1;
let max_detail_len = (toast_w - 4) as usize;
let detail_text = if detail.len() > max_detail_len {
format!("{}…", &detail[..max_detail_len - 1])
} else {
detail.clone()
};
let detail_color = Rgba::new(theme.fg2.r, theme.fg2.g, theme.fg2.b, opacity);
buffer.draw_text(content_x, detail_y, &detail_text, Style::fg(detail_color));
}
let border_color = Rgba::new(theme.bg2.r, theme.bg2.g, theme.bg2.b, opacity * 0.5);
let border_style = Style::fg(border_color);
buffer.draw_text(toast_x, toast_y, "╭", border_style);
for col in toast_x + 1..toast_x + toast_w - 1 {
buffer.draw_text(col, toast_y, "─", border_style);
}
buffer.draw_text(toast_x + toast_w - 1, toast_y, "╮", border_style);
buffer.draw_text(toast_x, toast_y + toast_h - 1, "╰", border_style);
for col in toast_x + 1..toast_x + toast_w - 1 {
buffer.draw_text(col, toast_y + toast_h - 1, "─", border_style);
}
buffer.draw_text(
toast_x + toast_w - 1,
toast_y + toast_h - 1,
"╯",
border_style,
);
for row in toast_y + 1..toast_y + toast_h - 1 {
buffer.draw_text(toast_x + toast_w - 1, row, "│", border_style);
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::too_many_lines
)]
fn draw_pass_debug(
buffer: &mut OptimizedBuffer,
panels: &PanelLayout,
theme: &Theme,
data: &InspectorData,
) {
let panel_w = 36_u32;
let panel_h = 14_u32;
let panel_x = panels.screen.w.saturating_sub(panel_w + 1);
let panel_y = panels.top_bar.y as u32 + panels.top_bar.h + 1;
let bg_color = Rgba::new(0.05, 0.05, 0.1, 0.92);
for y in panel_y..panel_y + panel_h {
for x in panel_x..panel_x + panel_w {
buffer.set(x, y, opentui::Cell::clear(bg_color));
}
}
let border_color = theme.accent_primary.with_alpha(0.6);
let border_style = Style::fg(border_color);
buffer.draw_text(panel_x, panel_y, "╭", border_style);
for x in panel_x + 1..panel_x + panel_w - 1 {
buffer.draw_text(x, panel_y, "─", border_style);
}
buffer.draw_text(panel_x + panel_w - 1, panel_y, "╮", border_style);
buffer.draw_text(panel_x, panel_y + panel_h - 1, "╰", border_style);
for x in panel_x + 1..panel_x + panel_w - 1 {
buffer.draw_text(x, panel_y + panel_h - 1, "─", border_style);
}
buffer.draw_text(
panel_x + panel_w - 1,
panel_y + panel_h - 1,
"╯",
border_style,
);
for y in panel_y + 1..panel_y + panel_h - 1 {
buffer.draw_text(panel_x, y, "│", border_style);
buffer.draw_text(panel_x + panel_w - 1, y, "│", border_style);
}
let title = " Inspector ";
let title_x = panel_x + panel_w.saturating_sub(title.len() as u32) / 2;
buffer.draw_text(
title_x,
panel_y,
title,
Style::fg(theme.accent_primary).with_bold(),
);
let content_x = panel_x + 2;
let mut y = panel_y + 1;
let label_style = Style::fg(theme.fg2);
let value_style = Style::fg(theme.fg1).with_bold();
let good_style = Style::fg(theme.accent_success);
let warn_style = Style::fg(theme.accent_warning);
y += 1;
buffer.draw_text(content_x, y, "FPS:", label_style);
let fps_text = format!("{:.1}", data.fps);
let fps_style = if data.fps >= 55.0 {
good_style
} else {
warn_style
};
buffer.draw_text(content_x + 5, y, &fps_text, fps_style);
buffer.draw_text(content_x + 12, y, "Frame:", label_style);
let frame_text = format!("{:.1}ms", data.frame_time_ms);
buffer.draw_text(content_x + 19, y, &frame_text, value_style);
y += 1;
buffer.draw_text(content_x, y, "Cells:", label_style);
buffer.draw_text(
content_x + 7,
y,
&data.cells_updated.to_string(),
value_style,
);
buffer.draw_text(content_x + 14, y, "Mem:", label_style);
let mem_kb = data.total_bytes / 1024;
buffer.draw_text(content_x + 19, y, &format!("{mem_kb}KB"), value_style);
y += 2;
buffer.draw_text(content_x, y, "─ Capabilities ─", Style::fg(theme.fg2));
y += 1;
let cap_on = Style::fg(theme.accent_success);
let cap_off = Style::fg(theme.accent_error);
let tc_style = if data.truecolor { cap_on } else { cap_off };
buffer.draw_text(content_x, y, "TC", tc_style);
let sync_style = if data.sync_output { cap_on } else { cap_off };
buffer.draw_text(content_x + 4, y, "Sync", sync_style);
let link_style = if data.hyperlinks { cap_on } else { cap_off };
buffer.draw_text(content_x + 10, y, "Link", link_style);
let mouse_style = if data.mouse { cap_on } else { cap_off };
buffer.draw_text(content_x + 16, y, "Mouse", mouse_style);
y += 1;
let focus_style = if data.focus { cap_on } else { cap_off };
buffer.draw_text(content_x, y, "Focus", focus_style);
let paste_style = if data.bracketed_paste {
cap_on
} else {
cap_off
};
buffer.draw_text(content_x + 7, y, "Paste", paste_style);
y += 2;
buffer.draw_text(content_x, y, "─ Demo ─", Style::fg(theme.fg2));
y += 1;
let tour_text = if data.tour_active {
"Tour: ON"
} else {
"Tour: off"
};
let tour_style = if data.tour_active {
good_style
} else {
label_style
};
buffer.draw_text(content_x, y, tour_text, tour_style);
let threaded_text = if data.threaded { "Threaded" } else { "Direct" };
buffer.draw_text(content_x + 12, y, threaded_text, value_style);
y += 1;
let fps_cap_text = format!("FPS Cap: {}", data.fps_cap);
buffer.draw_text(content_x, y, &fps_cap_text, label_style);
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss
)]
fn draw_help_overlay(
buffer: &mut OptimizedBuffer,
panels: &PanelLayout,
theme: &Theme,
state: &HelpState,
_opacity: f32,
links: &PreallocatedLinks,
) {
let overlay_w = (panels.screen.w * 60 / 100).clamp(40, 80);
let overlay_h = (panels.screen.h * 70 / 100).clamp(12, 30);
let overlay_x = (panels.screen.w - overlay_w) / 2;
let overlay_y = (panels.screen.h - overlay_h) / 2;
let rect = Rect::new(overlay_x as i32, overlay_y as i32, overlay_w, overlay_h);
let glass_bg = Rgba::new(
theme.bg1.r,
theme.bg1.g,
theme.bg1.b,
0.95, );
draw_rect_bg(buffer, &rect, glass_bg);
draw_overlay_border(buffer, &rect, theme);
let title = "═══ Help (F1) ═══";
let title_x = overlay_x + (overlay_w.saturating_sub(title.len() as u32)) / 2;
buffer.draw_text(
title_x,
overlay_y,
title,
Style::fg(theme.accent_primary).with_bold(),
);
let content_x = overlay_x + 2;
let mut content_y = overlay_y + 2;
let content_max_y = overlay_y + overlay_h - 2;
let mut line_idx = 0;
for (section_name, items) in HelpState::SECTIONS {
if content_y >= content_max_y {
break;
}
if line_idx >= state.scroll {
buffer.draw_text(
content_x,
content_y,
section_name,
Style::fg(theme.accent_secondary).with_bold(),
);
content_y += 1;
}
line_idx += 1;
for item in *items {
if content_y >= content_max_y {
break;
}
if line_idx < state.scroll {
line_idx += 1;
continue;
}
let style = if *section_name == "Links" {
let link_id = if item.starts_with("Repo:") {
links.repo_url
} else if item.starts_with("Docs:") {
links.docs_url
} else {
None
};
link_id.map_or_else(
|| Style::fg(theme.fg1),
|id| {
Style::fg(theme.accent_primary)
.with_underline()
.with_link(id)
},
)
} else {
Style::fg(theme.fg1)
};
buffer.draw_text(content_x + 1, content_y, item, style);
content_y += 1;
line_idx += 1;
}
content_y += 1; }
let footer = "Press Esc to close";
let footer_x = overlay_x + (overlay_w.saturating_sub(footer.len() as u32)) / 2;
buffer.draw_text(
footer_x,
overlay_y + overlay_h - 1,
footer,
Style::fg(theme.fg2),
);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn draw_palette_overlay(
buffer: &mut OptimizedBuffer,
panels: &PanelLayout,
theme: &Theme,
state: &PaletteState,
_opacity: f32,
_links: &PreallocatedLinks,
) {
let overlay_w = (panels.screen.w * 50 / 100).clamp(40, 60);
let overlay_h = (state.filtered.len() as u32 + 4)
.min(panels.screen.h * 50 / 100)
.max(6);
let overlay_x = (panels.screen.w - overlay_w) / 2;
let overlay_y = panels.screen.h / 4;
let rect = Rect::new(overlay_x as i32, overlay_y as i32, overlay_w, overlay_h);
let glass_bg = Rgba::new(theme.bg1.r, theme.bg1.g, theme.bg1.b, 0.95);
draw_rect_bg(buffer, &rect, glass_bg);
draw_overlay_border(buffer, &rect, theme);
let title = "═══ Command Palette (Ctrl+P) ═══";
let title_x = overlay_x + overlay_w.saturating_sub(title.len() as u32) / 2;
buffer.draw_text(
title_x,
overlay_y,
title,
Style::fg(theme.accent_secondary).with_bold(),
);
let prompt = "> ";
buffer.draw_text(
overlay_x + 2,
overlay_y + 2,
prompt,
Style::fg(theme.accent_primary),
);
let query_display = if state.query.is_empty() {
"Type to search..."
} else {
&state.query
};
let query_style = if state.query.is_empty() {
Style::fg(theme.fg2)
} else {
Style::fg(theme.fg0)
};
buffer.draw_text(overlay_x + 4, overlay_y + 2, query_display, query_style);
let list_y = overlay_y + 4;
let list_h = overlay_h.saturating_sub(5);
let max_visible = list_h.min(state.filtered.len() as u32) as usize;
let scroll_offset = if state.selected >= max_visible {
state.selected - max_visible + 1
} else {
0
};
let list_clip = ClipRect::new(
(overlay_x + 1) as i32,
list_y as i32,
overlay_w.saturating_sub(2),
list_h,
);
buffer.push_scissor(list_clip);
for (i, &cmd_idx) in state
.filtered
.iter()
.skip(scroll_offset)
.take(max_visible)
.enumerate()
{
let y = list_y + i as u32;
let Some(&(name, desc)) = PaletteState::COMMANDS.get(cmd_idx) else {
continue;
};
let is_selected = (i + scroll_offset) == state.selected;
let style = if is_selected {
Style::fg(theme.bg0).with_bg(theme.accent_primary)
} else {
Style::fg(theme.fg0)
};
let indicator = if is_selected { "▸ " } else { " " };
buffer.draw_text(overlay_x + 2, y, indicator, Style::fg(theme.accent_primary));
buffer.draw_text(overlay_x + 4, y, name, style);
let desc_x = overlay_x + 4 + name.len() as u32 + 2;
let desc_max = overlay_w.saturating_sub(desc_x - overlay_x + 2);
if desc_max > 5 {
let desc_truncated: String = desc.chars().take(desc_max as usize).collect();
buffer.draw_text(desc_x, y, &desc_truncated, Style::fg(theme.fg2));
}
}
buffer.pop_scissor();
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
fn draw_spotlight_effect(
buffer: &mut OptimizedBuffer,
panels: &PanelLayout,
theme: &Theme,
target_rect: &Rect,
) {
use opentui::Cell;
let dim_color = Rgba::new(0.0, 0.0, 0.0, 0.4);
for y in 0..panels.screen.h {
for x in 0..panels.screen.w {
let in_target = (x as i32) >= target_rect.x
&& (x as i32) < target_rect.x + target_rect.w as i32
&& (y as i32) >= target_rect.y
&& (y as i32) < target_rect.y + target_rect.h as i32;
if !in_target {
if let Some(cell) = buffer.get(x, y) {
let new_bg = dim_color.blend_over(cell.bg);
let mut new_cell = *cell;
new_cell.bg = new_bg;
buffer.set(x, y, new_cell);
}
}
}
}
let border_color = theme.accent_primary;
let border_style = Style::fg(border_color).with_bold();
for x in target_rect.x.max(0) as u32
..(target_rect.x + target_rect.w as i32).min(panels.screen.w as i32) as u32
{
if target_rect.y >= 0 && (target_rect.y as u32) < panels.screen.h {
buffer.set(x, target_rect.y as u32, Cell::new('─', border_style));
}
let bottom_y = target_rect.y + target_rect.h as i32 - 1;
if bottom_y >= 0 && (bottom_y as u32) < panels.screen.h {
buffer.set(x, bottom_y as u32, Cell::new('─', border_style));
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
fn draw_tour_overlay(
buffer: &mut OptimizedBuffer,
panels: &PanelLayout,
theme: &Theme,
state: &TourState,
_opacity: f32,
) {
let (title, desc, spotlight_target) = state.current();
if let Some(target_name) = spotlight_target {
if let Some(target_rect) = panels.get_panel_rect(target_name) {
draw_spotlight_effect(buffer, panels, theme, &target_rect);
}
}
let overlay_w = (panels.screen.w * 70 / 100).clamp(50, 80);
let overlay_h = 8_u32;
let overlay_x = (panels.screen.w - overlay_w) / 2;
let overlay_y = panels.screen.h.saturating_sub(overlay_h + 2);
let rect = Rect::new(overlay_x as i32, overlay_y as i32, overlay_w, overlay_h);
let glass_bg = Rgba::new(theme.bg1.r, theme.bg1.g, theme.bg1.b, 0.95);
draw_rect_bg(buffer, &rect, glass_bg);
draw_overlay_border(buffer, &rect, theme);
let step_text = format!(
"═══ Tour Step {}/{} ═══",
state.step + 1,
TourState::STEPS.len()
);
let step_x = overlay_x + overlay_w.saturating_sub(step_text.len() as u32) / 2;
buffer.draw_text(
step_x,
overlay_y,
&step_text,
Style::fg(theme.accent_success).with_bold(),
);
buffer.draw_text(
overlay_x + 3,
overlay_y + 2,
title,
Style::fg(theme.fg0).with_bold(),
);
let mut desc_y = overlay_y + 4;
for line in desc.lines() {
if desc_y >= overlay_y + overlay_h - 1 {
break;
}
buffer.draw_text(overlay_x + 3, desc_y, line, Style::fg(theme.fg1));
desc_y += 1;
}
let nav_hint = "Enter: Next │ Backspace: Prev │ Esc: Exit";
let nav_x = overlay_x + overlay_w.saturating_sub(nav_hint.len() as u32) / 2;
buffer.draw_text(
nav_x,
overlay_y + overlay_h - 1,
nav_hint,
Style::fg(theme.fg2),
);
let progress_w = overlay_w.saturating_sub(6);
let filled =
(progress_w as f32 * (state.step + 1) as f32 / TourState::STEPS.len() as f32) as u32;
let progress_x = overlay_x + 3;
let progress_y = overlay_y + overlay_h - 2;
for i in 0..progress_w {
let ch = if i < filled { '█' } else { '░' };
let color = if i < filled {
theme.accent_success
} else {
theme.fg2
};
buffer.draw_text(
progress_x + i,
progress_y,
&ch.to_string(),
Style::fg(color),
);
}
}
fn draw_overlay_border(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme) {
let x = u32::try_from(rect.x).unwrap_or(0);
let y = u32::try_from(rect.y).unwrap_or(0);
let w = rect.w;
let h = rect.h;
let border_style = Style::fg(theme.accent_primary);
for col in 1..w.saturating_sub(1) {
buffer.draw_text(x + col, y, "═", border_style);
buffer.draw_text(x + col, y + h - 1, "═", border_style);
}
for row in 1..h.saturating_sub(1) {
buffer.draw_text(x, y + row, "║", border_style);
buffer.draw_text(x + w - 1, y + row, "║", border_style);
}
buffer.draw_text(x, y, "╔", border_style);
buffer.draw_text(x + w - 1, y, "╗", border_style);
buffer.draw_text(x, y + h - 1, "╚", border_style);
buffer.draw_text(x + w - 1, y + h - 1, "╝", border_style);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn draw_unicode_showcase(
buffer: &mut OptimizedBuffer,
pool: &mut GraphemePool,
rect: &Rect,
theme: &Theme,
) {
use content::unicode;
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let w = rect.w;
let h = rect.h;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(
x + 2,
y + 1,
"Unicode & Grapheme Pool Showcase",
header_style,
);
let divider: String = "─".repeat(w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let content_style = Style::fg(theme.fg0);
let dim_style = Style::fg(theme.fg2);
let mut row = y + 4;
buffer.draw_text(x + 2, row, "Width Ruler:", label_style);
row += 1;
let ruler = "0 1 2 3 4";
buffer.draw_text(x + 4, row, ruler, dim_style);
row += 1;
let ruler_marks = "0123456789012345678901234567890123456789012";
buffer.draw_text(x + 4, row, ruler_marks, dim_style);
row += 2;
if row + 2 < y + h {
buffer.draw_text(x + 2, row, "CJK Wide (width 2 each):", label_style);
row += 1;
buffer.draw_text_with_pool(pool, x + 4, row, unicode::CJK_WIDE, content_style);
row += 2;
}
if row + 2 < y + h {
buffer.draw_text(x + 2, row, "Single Emoji (width 2 each):", label_style);
row += 1;
buffer.draw_text_with_pool(pool, x + 4, row, unicode::EMOJI_SINGLE, content_style);
row += 2;
}
if row + 2 < y + h {
buffer.draw_text(
x + 2,
row,
"ZWJ Emoji Sequences (multi-codepoint):",
label_style,
);
row += 1;
buffer.draw_text_with_pool(pool, x + 4, row, unicode::EMOJI_ZWJ, content_style);
row += 2;
}
if row + 3 < y + h {
buffer.draw_text(
x + 2,
row,
"Combining Marks (base + diacritic):",
label_style,
);
row += 1;
buffer.draw_text(x + 4, row, "Input: ", dim_style);
buffer.draw_text_with_pool(pool, x + 13, row, unicode::COMBINING_MARKS, content_style);
row += 1;
buffer.draw_text(x + 4, row, "Display: ", dim_style);
buffer.draw_text_with_pool(pool, x + 13, row, unicode::COMBINING_DISPLAY, content_style);
row += 2;
}
if row + 2 < y + h {
buffer.draw_text(x + 2, row, "Mixed Content Line:", label_style);
row += 1;
buffer.draw_text_with_pool(pool, x + 4, row, unicode::MIXED_LINE, content_style);
row += 2;
}
if row + 6 < y + h {
buffer.draw_text(x + 2, row, "Width Test Cases:", label_style);
row += 1;
for &(name, text, expected_width) in unicode::WIDTH_TEST_CASES {
if row >= y + h - 1 {
break;
}
let line = format!("{name:10} \"{text}\" → width {expected_width}");
buffer.draw_text_with_pool(pool, x + 4, row, &line, content_style);
row += 1;
}
}
if h > 20 {
let footer_y = y + h - 2;
buffer.draw_text(
x + 2,
footer_y,
"Note: Proper rendering requires terminal with Unicode support",
dim_style,
);
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::too_many_lines
)]
fn draw_drawing_section(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
use opentui::buffer::{BoxOptions, BoxSides, BoxStyle, TitleAlign};
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let is_focused = app.focus == Focus::Editor;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(x + 2, y + 1, "Drawing Primitives Showcase", header_style);
let divider: String = "─".repeat(rect.w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let dim_style = Style::fg(theme.fg2);
let mut row = y + 4;
let box_w = 12_u32;
let box_h = 4_u32;
buffer.draw_text(x + 2, row, "Box Styles:", label_style);
row += 2;
if row + box_h + 2 < y + rect.h {
let styles: [(BoxStyle, &str); 5] = [
(BoxStyle::single(Style::fg(theme.fg0)), "Single"),
(BoxStyle::double(Style::fg(theme.accent_primary)), "Double"),
(
BoxStyle::rounded(Style::fg(theme.accent_secondary)),
"Rounded",
),
(BoxStyle::heavy(Style::fg(theme.accent_success)), "Heavy"),
(BoxStyle::ascii(Style::fg(theme.fg2)), "ASCII"),
];
let mut col = x + 2;
for (style, name) in styles {
if col + box_w + 2 > x + rect.w {
break;
}
buffer.draw_box(col, row, box_w, box_h, style);
let label_x = col + (box_w.saturating_sub(name.len() as u32)) / 2;
buffer.draw_text(label_x, row + box_h / 2, name, Style::fg(theme.fg1));
col += box_w + 2;
}
row += box_h + 2;
}
if row + box_h + 3 < y + rect.h {
buffer.draw_text(x + 2, row, "Titled Boxes:", label_style);
row += 2;
let titled_w = 16_u32;
let mut col = x + 2;
if col + titled_w + 2 <= x + rect.w {
let options = BoxOptions {
style: BoxStyle::single(Style::fg(theme.fg0)),
sides: BoxSides::default(),
fill: None,
title: Some("Left".to_string()),
title_align: TitleAlign::Left,
};
buffer.draw_box_with_options(col, row, titled_w, box_h, options);
col += titled_w + 2;
}
if col + titled_w + 2 <= x + rect.w {
let options = BoxOptions {
style: BoxStyle::rounded(Style::fg(theme.accent_primary)),
sides: BoxSides::default(),
fill: None,
title: Some("Center".to_string()),
title_align: TitleAlign::Center,
};
buffer.draw_box_with_options(col, row, titled_w, box_h, options);
col += titled_w + 2;
}
if col + titled_w <= x + rect.w {
let options = BoxOptions {
style: BoxStyle::double(Style::fg(theme.accent_secondary)),
sides: BoxSides::default(),
fill: None,
title: Some("Right".to_string()),
title_align: TitleAlign::Right,
};
buffer.draw_box_with_options(col, row, titled_w, box_h, options);
}
row += box_h + 2;
}
if row + box_h + 3 < y + rect.h {
buffer.draw_text(x + 2, row, "Partial Sides:", label_style);
row += 2;
let partial_w = 10_u32;
let mut col = x + 2;
if col + partial_w + 2 <= x + rect.w {
let options = BoxOptions {
style: BoxStyle::single(Style::fg(theme.fg0)),
sides: BoxSides {
top: false,
right: true,
bottom: true,
left: true,
},
fill: None,
title: None,
title_align: TitleAlign::Left,
};
buffer.draw_box_with_options(col, row, partial_w, box_h, options);
buffer.draw_text(col + 1, row + 1, "No top", dim_style);
col += partial_w + 2;
}
if col + partial_w + 2 <= x + rect.w {
let options = BoxOptions {
style: BoxStyle::single(Style::fg(theme.fg0)),
sides: BoxSides {
top: true,
right: true,
bottom: true,
left: false,
},
fill: None,
title: None,
title_align: TitleAlign::Left,
};
buffer.draw_box_with_options(col, row, partial_w, box_h, options);
buffer.draw_text(col + 1, row + 1, "No left", dim_style);
col += partial_w + 2;
}
if col + partial_w <= x + rect.w {
let options = BoxOptions {
style: BoxStyle::heavy(Style::fg(theme.accent_primary)),
sides: BoxSides {
top: true,
right: false,
bottom: true,
left: false,
},
fill: None,
title: None,
title_align: TitleAlign::Left,
};
buffer.draw_box_with_options(col, row, partial_w, box_h, options);
buffer.draw_text(col + 1, row + 1, "H lines", dim_style);
}
row += box_h + 2;
}
if row + box_h + 3 < y + rect.h {
buffer.draw_text(x + 2, row, "Filled Box:", label_style);
row += 2;
let fill_color = theme.accent_primary.with_alpha(0.2);
let options = BoxOptions {
style: BoxStyle::rounded(Style::fg(theme.accent_primary)),
sides: BoxSides::default(),
fill: Some(fill_color),
title: Some("Filled".to_string()),
title_align: TitleAlign::Center,
};
buffer.draw_box_with_options(x + 2, row, 20, box_h, options);
buffer.draw_text(x + 4, row + box_h / 2, "Alpha: 0.2", Style::fg(theme.fg0));
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
clippy::too_many_lines
)]
fn draw_colors_section(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let is_focused = app.focus == Focus::Editor;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(x + 2, y + 1, "Color System Showcase", header_style);
let divider: String = "─".repeat(rect.w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let dim_style = Style::fg(theme.fg2);
let mut row = y + 4;
let gradient_w = rect.w.saturating_sub(6).min(50);
buffer.draw_text(x + 2, row, "Horizontal Gradients:", label_style);
row += 2;
if row + 4 < y + rect.h {
buffer.draw_text(x + 2, row, "Accent: ", dim_style);
let grad_rect = Rect::new((x + 10) as i32, row as i32, gradient_w, 1);
draw_gradient_bar(
buffer,
&grad_rect,
theme.accent_primary,
theme.accent_secondary,
);
row += 1;
buffer.draw_text(x + 2, row, "Status: ", dim_style);
let grad_rect = Rect::new((x + 10) as i32, row as i32, gradient_w, 1);
draw_gradient_bar(buffer, &grad_rect, theme.accent_success, theme.accent_error);
row += 1;
buffer.draw_text(x + 2, row, "Gray: ", dim_style);
let grad_rect = Rect::new((x + 10) as i32, row as i32, gradient_w, 1);
draw_gradient_bar(buffer, &grad_rect, theme.fg0, theme.bg0);
row += 2;
}
if row + 3 < y + rect.h {
buffer.draw_text(x + 2, row, "HSV Hue Sweep:", label_style);
row += 2;
for i in 0..gradient_w {
let hue = (i as f32 / gradient_w as f32) * 360.0;
let color = hsv_to_rgb(hue, 0.9, 0.9);
buffer.draw_text(x + 4 + i, row, "█", Style::fg(color));
}
row += 2;
}
if row + 6 < y + rect.h {
buffer.draw_text(x + 2, row, "Alpha Blending Layers:", label_style);
row += 2;
let box_w = 12_u32;
let box_h = 4_u32;
let overlap = 4_u32;
let base_color = Rgba::new(0.8, 0.2, 0.2, 1.0);
for by in row..row + box_h {
for bx in x + 4..x + 4 + box_w {
buffer.set(bx, by, Cell::clear(base_color));
}
}
buffer.draw_text(x + 5, row + 1, "Base", Style::fg(Rgba::WHITE));
let overlay_color = Rgba::new(0.2, 0.8, 0.2, 0.7);
for by in row + 1..row + 1 + box_h {
for bx in x + 4 + box_w - overlap..x + 4 + box_w - overlap + box_w {
if let Some(cell) = buffer.get(bx, by) {
let mut new_cell = *cell;
new_cell.bg = overlay_color.blend_over(cell.bg);
buffer.set(bx, by, new_cell);
}
}
}
buffer.draw_text(
x + 4 + box_w - overlap + 1,
row + 2,
"70%",
Style::fg(Rgba::WHITE),
);
let top_color = Rgba::new(0.2, 0.2, 0.9, 0.5);
for by in row + 2..row + 2 + box_h {
for bx in x + 4 + 2 * (box_w - overlap)..x + 4 + 2 * (box_w - overlap) + box_w {
if let Some(cell) = buffer.get(bx, by) {
let mut new_cell = *cell;
new_cell.bg = top_color.blend_over(cell.bg);
buffer.set(bx, by, new_cell);
}
}
}
buffer.draw_text(
x + 4 + 2 * (box_w - overlap) + 1,
row + 3,
"50%",
Style::fg(Rgba::WHITE),
);
row += box_h + 3;
}
if row + 4 < y + rect.h {
buffer.draw_text(x + 2, row, "Opacity Stack:", label_style);
row += 2;
let opacities = [1.0_f32, 0.8, 0.6, 0.4, 0.2];
let mut col = x + 4;
for (i, &opacity) in opacities.iter().enumerate() {
let text = format!("{:.0}%", opacity * 100.0);
let color = theme.accent_primary.with_alpha(opacity);
buffer.draw_text(col, row, &text, Style::fg(color).with_bold());
col += 6;
if col > x + rect.w - 10 {
break;
}
if i < opacities.len() - 1 {
buffer.draw_text(col - 2, row, "→", dim_style);
}
}
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn draw_input_section(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let is_focused = app.focus == Focus::Editor;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(x + 2, y + 1, "Input Handling Showcase", header_style);
let divider: String = "─".repeat(rect.w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let dim_style = Style::fg(theme.fg2);
let content_style = Style::fg(theme.fg0);
let mut row = y + 4;
buffer.draw_text(x + 2, row, "Cursor Styles:", label_style);
row += 2;
if row + 5 < y + rect.h {
buffer.draw_text(x + 4, row, "Block: ", dim_style);
buffer.draw_text(x + 15, row, "Text", content_style);
buffer.draw_text(x + 19, row, "█", Style::fg(theme.accent_primary));
buffer.draw_text(x + 20, row, "here", content_style);
row += 1;
buffer.draw_text(x + 4, row, "Underline: ", dim_style);
buffer.draw_text(x + 15, row, "Text", content_style);
buffer.draw_text(x + 19, row, "_", Style::fg(theme.accent_primary));
buffer.draw_text(x + 20, row, "here", content_style);
row += 1;
buffer.draw_text(x + 4, row, "Bar: ", dim_style);
buffer.draw_text(x + 15, row, "Text", content_style);
buffer.draw_text(x + 19, row, "│", Style::fg(theme.accent_primary));
buffer.draw_text(x + 20, row, "here", content_style);
row += 2;
}
if row + 4 < y + rect.h {
buffer.draw_text(x + 2, row, "Focus State:", label_style);
row += 2;
let focus_status = if is_focused { "FOCUSED" } else { "UNFOCUSED" };
let focus_color = if is_focused {
theme.accent_success
} else {
theme.fg2
};
buffer.draw_text(x + 4, row, "Current: ", dim_style);
buffer.draw_text(
x + 13,
row,
focus_status,
Style::fg(focus_color).with_bold(),
);
row += 1;
buffer.draw_text(x + 4, row, "(Tab to cycle, click to focus)", dim_style);
row += 2;
}
if row + 5 < y + rect.h {
buffer.draw_text(x + 2, row, "Key Event Display:", label_style);
row += 2;
buffer.draw_text(x + 4, row, "Last key: ", dim_style);
buffer.draw_text(x + 14, row, "(press any key)", Style::fg(theme.fg2));
row += 1;
buffer.draw_text(x + 4, row, "Modifiers:", dim_style);
let mods = "Ctrl Shift Alt";
buffer.draw_text(x + 15, row, mods, Style::fg(theme.fg2));
row += 2;
}
if row + 4 < y + rect.h {
buffer.draw_text(x + 2, row, "Bracketed Paste:", label_style);
row += 2;
buffer.draw_text(x + 4, row, "Status: ", dim_style);
buffer.draw_text(
x + 12,
row,
"Enabled (if supported)",
Style::fg(theme.accent_success),
);
row += 1;
buffer.draw_text(
x + 4,
row,
"Paste into terminal to test",
Style::fg(theme.fg2),
);
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn draw_editing_section(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let is_focused = app.focus == Focus::Editor;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(x + 2, y + 1, "Editing Features Showcase", header_style);
let divider: String = "─".repeat(rect.w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let dim_style = Style::fg(theme.fg2);
let content_style = Style::fg(theme.fg0);
let mut row = y + 4;
buffer.draw_text(x + 2, row, "EditBuffer Features:", label_style);
row += 2;
if row + 6 < y + rect.h {
let features = [
"• Rope-backed text storage (efficient for large files)",
"• O(log n) edits, random access, and iteration",
"• Grapheme-aware cursor movement",
"• Line-indexed for fast line operations",
];
for feat in features {
if row >= y + rect.h - 2 {
break;
}
buffer.draw_text(x + 4, row, feat, content_style);
row += 1;
}
row += 1;
}
if row + 5 < y + rect.h {
buffer.draw_text(x + 2, row, "Undo/Redo System:", label_style);
row += 2;
buffer.draw_text(x + 4, row, "Ctrl+Z → Undo", dim_style);
buffer.draw_text(x + 22, row, "Ctrl+Y → Redo", dim_style);
row += 1;
buffer.draw_text(x + 4, row, "History: ", dim_style);
buffer.draw_text(x + 13, row, "[Edit1] → [Edit2] → [Edit3]", content_style);
row += 1;
buffer.draw_text(x + 22, row, "↑ current", Style::fg(theme.accent_primary));
row += 2;
}
if row + 8 < y + rect.h {
buffer.draw_text(x + 2, row, "Wrap Modes:", label_style);
row += 2;
let wrap_modes = [
("None:", "Lines extend beyond visible area"),
("Word:", "Break at word boundaries"),
("Char:", "Break at any character"),
];
for (mode, desc) in wrap_modes {
if row >= y + rect.h - 2 {
break;
}
buffer.draw_text(x + 4, row, mode, Style::fg(theme.accent_secondary));
buffer.draw_text(x + 10, row, desc, dim_style);
row += 1;
}
row += 1;
}
if row + 6 < y + rect.h {
buffer.draw_text(x + 2, row, "Sample Text Area:", label_style);
row += 1;
let editor_w = rect.w.saturating_sub(6).min(40);
let editor_h = 4_u32;
let editor_x = x + 4;
buffer.draw_box(
editor_x,
row,
editor_w,
editor_h,
opentui::buffer::BoxStyle::single(Style::fg(theme.fg2)),
);
let sample_lines = [
"The quick brown fox jumps",
"over the lazy dog.",
"→ Type here to edit",
];
for (i, line) in sample_lines.iter().enumerate() {
let line_y = row + 1 + i as u32;
if line_y < row + editor_h - 1 {
buffer.draw_text(editor_x + 1, line_y, line, content_style);
}
}
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn draw_capabilities_section(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let is_focused = app.focus == Focus::Editor;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(x + 2, y + 1, "Terminal Capabilities", header_style);
let divider: String = "─".repeat(rect.w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let dim_style = Style::fg(theme.fg2);
let mut row = y + 4;
buffer.draw_text(x + 2, row, "Detected Capabilities:", label_style);
row += 2;
if row + 8 < y + rect.h {
let caps = &app.effective_caps;
let check = "✓";
let cross = "✗";
let features = [
("TrueColor (24-bit)", caps.truecolor),
("Mouse Tracking", caps.mouse),
("Hyperlinks (OSC 8)", caps.hyperlinks),
("Focus Events", caps.focus),
("Sync Output", caps.sync_output),
];
for (name, enabled) in features {
if row >= y + rect.h - 2 {
break;
}
let (symbol, color) = if enabled {
(check, theme.accent_success)
} else {
(cross, theme.accent_error)
};
buffer.draw_text(x + 4, row, symbol, Style::fg(color).with_bold());
buffer.draw_text(x + 6, row, name, Style::fg(theme.fg0));
row += 1;
}
row += 1;
}
if row + 5 < y + rect.h {
buffer.draw_text(x + 2, row, "Environment:", label_style);
row += 2;
buffer.draw_text(x + 4, row, "TERM: ", dim_style);
buffer.draw_text(
x + 10,
row,
std::env::var("TERM")
.unwrap_or_else(|_| "unknown".to_string())
.as_str(),
Style::fg(theme.fg0),
);
row += 1;
buffer.draw_text(x + 4, row, "COLORTERM: ", dim_style);
buffer.draw_text(
x + 15,
row,
std::env::var("COLORTERM")
.unwrap_or_else(|_| "unset".to_string())
.as_str(),
Style::fg(theme.fg0),
);
row += 2;
}
if row + 4 < y + rect.h && app.effective_caps.is_degraded() {
buffer.draw_text(x + 2, row, "Degraded Features:", label_style);
row += 2;
for feature in &app.effective_caps.degraded {
if row >= y + rect.h - 2 {
break;
}
buffer.draw_text(x + 4, row, "⚠ ", Style::fg(theme.accent_warning));
buffer.draw_text(x + 6, row, feature, Style::fg(theme.accent_warning));
row += 1;
}
}
if row + 3 < y + rect.h {
row += 1;
buffer.draw_text(x + 2, row, "Active Preset:", label_style);
let preset_name = match app.cap_preset {
CapPreset::Auto => "Auto",
CapPreset::Ideal => "Ideal",
CapPreset::NoTruecolor => "No TrueColor",
CapPreset::NoHyperlinks => "No Hyperlinks",
CapPreset::NoMouse => "No Mouse",
CapPreset::Minimal => "Minimal",
};
buffer.draw_text(x + 17, row, preset_name, Style::fg(theme.fg0));
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::type_complexity
)]
fn draw_animations_section(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() {
return;
}
let x = rect.x as u32;
let y = rect.y as u32;
let is_focused = app.focus == Focus::Editor;
draw_rect_bg(buffer, rect, theme.bg1);
let header_style = Style::fg(theme.accent_primary).with_bold();
buffer.draw_text(x + 2, y + 1, "Animation & Easing Showcase", header_style);
let divider: String = "─".repeat(rect.w.saturating_sub(4) as usize);
buffer.draw_text(x + 2, y + 2, ÷r, Style::fg(theme.fg2));
let label_style = Style::fg(theme.accent_secondary).with_bold();
let dim_style = Style::fg(theme.fg2);
let mut row = y + 4;
let track_w = rect.w.saturating_sub(20).min(40);
let cycle_time = 2.0_f32;
let t = (app.clock.t % cycle_time) / cycle_time;
buffer.draw_text(x + 2, row, "Easing Functions:", label_style);
row += 2;
if row + 8 < y + rect.h && track_w >= 2 {
let easings: [(&str, fn(f32) -> f32); 4] = [
("Linear: ", |t| t),
("Smoothstep:", easing::smoothstep),
("EaseInOut:", easing::ease_in_out_cubic),
("EaseOut: ", easing::ease_out_cubic),
];
for (name, ease_fn) in easings {
if row >= y + rect.h - 4 {
break;
}
buffer.draw_text(x + 2, row, name, dim_style);
let track_x = x + 12;
for i in 0..track_w {
buffer.draw_text(track_x + i, row, "─", Style::fg(theme.bg2));
}
let eased = ease_fn(t);
let dot_pos = (eased * (track_w.saturating_sub(1)) as f32) as u32;
buffer.draw_text(
track_x + dot_pos,
row,
"●",
Style::fg(theme.accent_primary).with_bold(),
);
let pct = format!("{:3.0}%", eased * 100.0);
buffer.draw_text(track_x + track_w + 2, row, &pct, Style::fg(theme.fg1));
row += 2;
}
}
if row + 4 < y + rect.h {
buffer.draw_text(x + 2, row, "Pulse Animation:", label_style);
row += 2;
let frequencies = [1.0_f32, 2.0, 4.0];
let mut col = x + 4;
for freq in frequencies {
let pulse = easing::pulse(app.clock.t, freq * std::f32::consts::TAU);
let intensity = (pulse * 255.0) as u8;
let color = Rgba::new(
f32::from(intensity) / 255.0,
theme.accent_primary.g * pulse,
theme.accent_primary.b * pulse,
1.0,
);
let label = format!("{freq:.0}Hz");
buffer.draw_text(col, row, "●", Style::fg(color).with_bold());
buffer.draw_text(col + 2, row, &label, dim_style);
col += 8;
if col > x + rect.w - 10 {
break;
}
}
row += 2;
}
if row + 4 < y + rect.h {
buffer.draw_text(x + 2, row, "Animation Clock:", label_style);
row += 2;
let time_info = format!(
"t = {:.2}s dt = {:.3}s paused = {}",
app.clock.t,
app.clock.dt,
app.clock.is_paused()
);
buffer.draw_text(x + 4, row, &time_info, Style::fg(theme.fg0));
row += 1;
let frame_info = format!("Frame: {} Target FPS: {}", app.frame_count, app.target_fps);
buffer.draw_text(x + 4, row, &frame_info, dim_style);
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
fn draw_editor_panel(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() {
return;
}
let x = u32::try_from(rect.x).unwrap_or(0);
let y = u32::try_from(rect.y).unwrap_or(0);
let is_focused = app.focus == Focus::Editor;
let header_bg = if is_focused {
theme.accent_primary.with_alpha(0.3)
} else {
theme.bg1
};
for col in 0..rect.w {
buffer.draw_text(x + col, y, " ", Style::bg(header_bg));
}
let file_name = app.current_file_name();
let lang_indicator = match app.current_file_language() {
content::Language::Rust => " [Rust]",
content::Language::Markdown => " [Markdown]",
content::Language::Python => " [Python]",
content::Language::Toml => " [TOML]",
content::Language::Plain => "",
};
let header_text = format!(" {file_name}{lang_indicator}");
let header_style = if is_focused {
Style::fg(theme.fg0).with_bg(header_bg).with_bold()
} else {
Style::fg(theme.fg1).with_bg(header_bg)
};
buffer.draw_text(x, y, &header_text, header_style);
let content_y = y + 1;
let content_h = rect.h.saturating_sub(1);
let gutter_width = 4_u32; let text_x = x + gutter_width;
let text_w = rect.w.saturating_sub(gutter_width);
let content = app.current_file_content();
let lines: Vec<&str> = content.lines().collect();
let language = app.current_file_language();
for (line_idx, line) in lines.iter().enumerate() {
let row = content_y + u32::try_from(line_idx).unwrap_or(0);
if row >= content_y + content_h {
break;
}
let line_num = line_idx + 1;
let gutter_text = format!("{line_num:>3} ");
buffer.draw_text(x, row, &gutter_text, Style::fg(theme.fg2));
let line_style = get_line_style(line, language, theme);
let display_line = if u32::try_from(line.len()).unwrap_or(0) > text_w {
let max_len = usize::try_from(text_w.saturating_sub(1)).unwrap_or(0);
let truncated = &line[..line.len().min(max_len)];
format!("{truncated}…")
} else {
(*line).to_string()
};
buffer.draw_text(text_x, row, &display_line, line_style);
}
if is_focused {
for row in y..y + rect.h {
buffer.draw_text(x.saturating_sub(1), row, "│", Style::fg(theme.focus_border));
}
}
}
fn get_line_style(line: &str, language: content::Language, theme: &Theme) -> Style {
let trimmed = line.trim();
match language {
content::Language::Rust => {
if trimmed.starts_with("//") {
return Style::fg(theme.fg2);
}
let keywords = [
"fn ",
"let ",
"mut ",
"pub ",
"use ",
"impl ",
"struct ",
"enum ",
"const ",
"static ",
"mod ",
"trait ",
"where ",
"async ",
"await ",
"match ",
"if ",
"else ",
"for ",
"while ",
"loop ",
"return ",
"break ",
"continue ",
];
for kw in keywords {
if trimmed.starts_with(kw) || trimmed.contains(&format!(" {kw}")) {
return Style::fg(theme.accent_primary);
}
}
if trimmed.contains('"') {
return Style::fg(theme.accent_secondary);
}
Style::fg(theme.fg0)
}
content::Language::Markdown => {
if trimmed.starts_with('#') {
return Style::fg(theme.accent_primary).with_bold();
}
if trimmed.starts_with("```") {
return Style::fg(theme.fg2);
}
if trimmed.starts_with('*') || trimmed.starts_with('-') {
return Style::fg(theme.accent_secondary);
}
if trimmed.contains('[') && trimmed.contains("](") {
return Style::fg(theme.accent_primary);
}
Style::fg(theme.fg0)
}
content::Language::Python => {
if trimmed.starts_with('#') && !trimmed.starts_with("#!") {
return Style::fg(theme.fg2);
}
if trimmed.starts_with("#!") {
return Style::fg(theme.fg2);
}
if trimmed.starts_with("\"\"\"") || trimmed.starts_with("'''") {
return Style::fg(theme.accent_success);
}
if trimmed.starts_with('@') {
return Style::fg(theme.accent_secondary);
}
let keywords = [
"def ", "class ", "if ", "else:", "elif ", "for ", "while ", "import ", "from ",
"return ", "async ", "await ", "with ", "try:", "except ", "finally:", "raise ",
"pass", "break", "continue", "yield ", "lambda ", "None", "True", "False",
];
for kw in keywords {
if trimmed.starts_with(kw) || trimmed.contains(&format!(" {kw}")) {
return Style::fg(theme.accent_primary);
}
}
if trimmed.contains('"') || trimmed.contains('\'') {
return Style::fg(theme.accent_secondary);
}
Style::fg(theme.fg0)
}
content::Language::Toml => {
if trimmed.starts_with('#') {
return Style::fg(theme.fg2);
}
if trimmed.starts_with('[') {
return Style::fg(theme.accent_primary).with_bold();
}
if trimmed.contains('=') {
return Style::fg(theme.accent_secondary);
}
Style::fg(theme.fg0)
}
content::Language::Plain => Style::fg(theme.fg0),
}
}
#[allow(
clippy::many_single_char_names,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Rgba {
let c = v * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let m = v - c;
let (r, g, b) = match h as u32 {
0..=59 => (c, x, 0.0),
60..=119 => (x, c, 0.0),
120..=179 => (0.0, c, x),
180..=239 => (0.0, x, c),
240..=299 => (x, 0.0, c),
_ => (c, 0.0, x),
};
Rgba::new(r + m, g + m, b + m, 1.0)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::suboptimal_flops,
clippy::too_many_lines
)]
fn draw_preview_panel(buffer: &mut OptimizedBuffer, rect: &Rect, theme: &Theme, app: &App) {
if rect.is_empty() || rect.w < 10 || rect.h < 6 {
return;
}
let px = u32::try_from(rect.x).unwrap_or(0);
let py = u32::try_from(rect.y).unwrap_or(0);
let is_focused = app.focus == Focus::Preview;
let border_color = if is_focused {
theme.focus_border
} else {
Rgba::from_hex("#333366").unwrap_or(theme.bg2)
};
for row in py..py + rect.h {
buffer.draw_text(px, row, "│", Style::fg(border_color));
}
let label = " Preview ";
let label_style = if is_focused {
Style::fg(theme.accent_primary).with_bold()
} else {
Style::fg(theme.fg2)
};
buffer.draw_text(px + 1, py, label, label_style);
let content_x = px + 2;
let content_y = py + 2;
let content_w = rect.w.saturating_sub(4);
let content_h = rect.h.saturating_sub(4);
if content_w < 8 || content_h < 4 {
return;
}
let orb_size = content_w.min(content_h * 2).min(24); let orb_pixel_w = orb_size * 2; let orb_pixel_h = orb_size;
let mut orb_buf = PixelBuffer::new(orb_pixel_w, orb_pixel_h);
let t = app.clock.t;
let base_hue = (t * 60.0) % 360.0;
let cx = orb_pixel_w as f32 / 2.0;
let cy = orb_pixel_h as f32 / 2.0;
let radius = cx.min(cy) * 0.9;
for y in 0..orb_pixel_h {
for x in 0..orb_pixel_w {
let dx = x as f32 - cx;
let dy = (y as f32 - cy) * 2.0; let dist = dx.hypot(dy);
if dist < radius {
let angle = dy.atan2(dx);
let hue = (base_hue + angle.to_degrees() + 360.0) % 360.0;
let sat = 0.7 + 0.3 * (1.0 - dist / radius);
let val = 0.9 - 0.3 * (dist / radius);
let color = hsv_to_rgb(hue, sat, val);
orb_buf.set(x, y, color);
} else {
orb_buf.set(x, y, Rgba::TRANSPARENT);
}
}
}
buffer.draw_supersample_buffer(content_x, content_y, &orb_buf, 0.5);
let chart_y = content_y + (orb_size / 2) + 2;
let chart_w = content_w.min(32);
let chart_h = 4_u32;
if chart_y + chart_h < py + rect.h {
let mut chart_buf = GrayscaleBuffer::new(chart_w, chart_h);
let fps = app.metrics.fps as f32;
let target = app.target_fps as f32;
for x in 0..chart_w {
let phase = (x as f32 / chart_w as f32) * std::f32::consts::PI * 4.0 + t * 2.0;
let value = fps + (phase.sin() * 5.0);
let normalized = (value / target).clamp(0.0, 1.0);
let bar_height = (normalized * chart_h as f32) as u32;
for y in 0..chart_h {
let intensity = if y >= chart_h - bar_height {
0.8 + 0.2 * (1.0 - y as f32 / chart_h as f32)
} else {
0.1
};
chart_buf.set(x, y, intensity);
}
}
buffer.draw_grayscale_buffer_unicode(
content_x,
chart_y,
&chart_buf,
theme.accent_primary,
theme.bg0,
);
let fps_label = format!("{fps:.0} FPS");
buffer.draw_text(
content_x + chart_w + 1,
chart_y + chart_h / 2,
&fps_label,
Style::fg(theme.fg1),
);
}
let overlay_y = chart_y.saturating_add(chart_h + 2);
let overlay_h = 3_u32;
let overlay_w = content_w.min(28);
if overlay_y + overlay_h < py + rect.h {
let glass_bg = Rgba::new(0.1, 0.1, 0.2, 0.7);
for row in overlay_y..overlay_y + overlay_h {
for col in content_x..content_x + overlay_w {
if let Some(cell) = buffer.get(col, row) {
let mut new_cell = *cell;
new_cell.bg = glass_bg.blend_over(cell.bg);
buffer.set(col, row, new_cell);
}
}
}
let mem_mb = app.metrics.memory_bytes as f64 / (1024.0 * 1024.0);
let mem_text = format!("Mem: {mem_mb:.1} MB");
let cpu_text = format!("CPU: {}%", app.metrics.cpu_percent);
buffer.draw_text(
content_x + 1,
overlay_y + 1,
&mem_text,
Style::fg(theme.fg0),
);
buffer.draw_text(
content_x + 14,
overlay_y + 1,
&cpu_text,
Style::fg(theme.fg0),
);
}
let frame_y = py + rect.h - 2;
let frame_info = format!("Frame {}", app.frame_count);
buffer.draw_text(content_x, frame_y, &frame_info, Style::fg(theme.fg2));
}
fn draw_logs_panel(
buffer: &mut OptimizedBuffer,
rect: &Rect,
theme: &Theme,
app: &App,
_links: &PreallocatedLinks,
) {
if rect.is_empty() {
return;
}
let x = u32::try_from(rect.x).unwrap_or(0);
let y = u32::try_from(rect.y).unwrap_or(0);
let is_focused = app.focus == Focus::Logs;
let border_color = if is_focused {
theme.focus_border
} else {
theme.bg2
};
let border_char = "─";
for col in 0..rect.w {
buffer.draw_text(x + col, y, border_char, Style::fg(border_color));
}
let label = " Logs ";
let label_style = if is_focused {
Style::fg(theme.accent_primary).with_bold()
} else {
Style::fg(theme.fg2)
};
buffer.draw_text(x + 2, y, label, label_style);
let content_y = y + 1;
let content_h = rect.h.saturating_sub(1);
#[allow(clippy::cast_possible_wrap)]
let clip = ClipRect::new(x as i32, content_y as i32, rect.w, content_h);
buffer.push_scissor(clip);
let visible_rows = content_h.min(u32::try_from(app.logs.len()).unwrap_or(0));
let start_idx = app
.logs
.len()
.saturating_sub(usize::try_from(visible_rows).unwrap_or(0));
for (row_offset, log) in app.logs.iter().skip(start_idx).enumerate() {
let row = u32::try_from(row_offset).unwrap_or(0);
if row >= content_h {
break;
}
let log_y = content_y + row;
let mut col = x + 1;
buffer.draw_text(col, log_y, &log.timestamp, Style::fg(theme.fg2));
col += u32::try_from(log.timestamp.len()).unwrap_or(0) + 1;
let level_style = match log.level {
content::LogLevel::Debug => Style::fg(theme.fg2),
content::LogLevel::Info => Style::fg(theme.accent_primary),
content::LogLevel::Warn => Style::fg(theme.accent_warning).with_bold(),
content::LogLevel::Error => Style::fg(theme.accent_error).with_bold(),
};
buffer.draw_text(col, log_y, log.level.as_str(), level_style);
col += u32::try_from(log.level.as_str().len()).unwrap_or(0) + 1;
let subsystem_text = format!("[{}]", log.subsystem);
buffer.draw_text(col, log_y, &subsystem_text, Style::fg(theme.fg1));
col += u32::try_from(subsystem_text.len()).unwrap_or(0) + 1;
let message_style = if log.link.is_some() {
Style::fg(theme.accent_secondary).with_underline()
} else {
Style::fg(theme.fg0)
};
let available_width = rect.w.saturating_sub(col - x).saturating_sub(2);
let message = if u32::try_from(log.message.len()).unwrap_or(0) > available_width {
let max_chars = usize::try_from(available_width.saturating_sub(1)).unwrap_or(0);
let truncated: String = log.message.chars().take(max_chars).collect();
format!("{truncated}…")
} else {
log.message.to_string()
};
buffer.draw_text(col, log_y, &message, message_style);
}
if app.logs.is_empty() {
let placeholder = "No log entries yet...";
buffer.draw_text(x + 2, content_y, placeholder, Style::fg(theme.fg2));
}
buffer.pop_scissor();
}
fn draw_rect_bg(buffer: &mut OptimizedBuffer, rect: &Rect, color: Rgba) {
if rect.is_empty() {
return;
}
buffer.fill_rect(
u32::try_from(rect.x).unwrap_or(0),
u32::try_from(rect.y).unwrap_or(0),
rect.w,
rect.h,
color,
);
}
#[allow(clippy::cast_precision_loss)] fn draw_gradient_bar(buffer: &mut OptimizedBuffer, rect: &Rect, start: Rgba, end: Rgba) {
if rect.is_empty() {
return;
}
let x = u32::try_from(rect.x).unwrap_or(0);
let y = u32::try_from(rect.y).unwrap_or(0);
for col in 0..rect.w {
let t = if rect.w > 1 {
col as f32 / (rect.w - 1) as f32
} else {
0.0
};
let color = Theme::lerp(start, end, t);
buffer.fill_rect(x + col, y, 1, rect.h, color);
}
}
#[allow(dead_code)] const fn layout_mode_name(mode: LayoutMode) -> &'static str {
match mode {
LayoutMode::Full => "Full",
LayoutMode::Compact => "Compact",
LayoutMode::Minimal => "Minimal",
LayoutMode::TooSmall => "TooSmall",
}
}
fn draw_too_small_message(buffer: &mut OptimizedBuffer, width: u32, height: u32, theme: &Theme) {
let msg1 = "Terminal too small!";
let msg2 = format!("Need at least {}x{}", layout::MIN_WIDTH, layout::MIN_HEIGHT);
let msg3 = format!("Current: {width}x{height}");
let msg4 = "Press any key to exit";
let center_y = height / 2;
let draw_centered = |buf: &mut OptimizedBuffer, y: u32, text: &str, style: Style| {
let len = u32::try_from(text.len()).unwrap_or(0);
let x = width.saturating_sub(len) / 2;
buf.draw_text(x, y, text, style);
};
draw_centered(
buffer,
center_y.saturating_sub(2),
msg1,
Style::fg(theme.accent_error).with_bold(),
);
draw_centered(
buffer,
center_y.saturating_sub(1),
&msg2,
Style::fg(theme.fg0),
);
draw_centered(buffer, center_y, &msg3, Style::fg(theme.fg0));
draw_centered(
buffer,
center_y.saturating_add(2),
msg4,
Style::fg(theme.fg0),
);
}
fn draw_sidebar(
buffer: &mut OptimizedBuffer,
sidebar: &Rect,
mode: LayoutMode,
theme: &Theme,
app: &App,
) {
let x = u32::try_from(sidebar.x).unwrap_or(0);
let base_y = u32::try_from(sidebar.y).unwrap_or(0);
let mut y = base_y + 1;
let is_focused = app.focus == Focus::Sidebar;
#[allow(clippy::cast_possible_wrap)]
let clip = ClipRect::new(sidebar.x, sidebar.y, sidebar.w, sidebar.h);
buffer.push_scissor(clip);
if is_focused && mode != LayoutMode::Minimal {
for row in 0..sidebar.h.saturating_sub(2) {
buffer.draw_text(x, y + row, "│", Style::fg(theme.focus_border));
}
}
let content_x = x + if mode == LayoutMode::Compact { 0 } else { 2 };
for (i, section) in Section::ALL.iter().enumerate() {
let bottom = u32::try_from(sidebar.bottom()).unwrap_or(u32::MAX);
if y >= bottom.saturating_sub(1) {
break;
}
let is_selected = *section == app.section;
let label = section.name();
#[allow(clippy::cast_possible_truncation)] let key = (b'1' + i as u8) as char;
let text = if mode == LayoutMode::Compact {
format!("{key}")
} else {
format!(" {key}. {label}")
};
let style = if is_selected {
if is_focused {
Style::fg(theme.bg0)
.with_bg(theme.accent_primary)
.with_bold()
} else {
Style::fg(theme.fg0).with_bg(theme.selection_bg)
}
} else {
Style::fg(theme.fg1)
};
if is_selected && mode != LayoutMode::Compact {
buffer.draw_text(content_x, y, "▸", Style::fg(theme.accent_primary));
}
let text_x = if mode == LayoutMode::Compact {
content_x
} else {
content_x + 2
};
buffer.draw_text(text_x, y, &text, style);
y += 1;
}
let bottom = u32::try_from(sidebar.bottom()).unwrap_or(0);
if y < bottom.saturating_sub(1) && mode == LayoutMode::Full {
let count_text = format!("{}/{}", Section::ALL.len(), Section::ALL.len());
buffer.draw_text(
content_x + 2,
bottom.saturating_sub(2),
&count_text,
Style::fg(theme.fg2),
);
}
buffer.pop_scissor();
}
#[cfg(unix)]
#[allow(clippy::missing_const_for_fn, clippy::must_use_candidate)] fn is_tty() -> bool {
unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 }
}
#[cfg(not(unix))]
const fn is_tty() -> bool {
true
}
#[cfg(unix)]
#[allow(clippy::unnecessary_wraps, clippy::missing_const_for_fn)] fn set_stdin_nonblocking() -> io::Result<()> {
Ok(())
}
#[cfg(not(unix))]
#[allow(clippy::unnecessary_wraps, clippy::missing_const_for_fn)] fn set_stdin_nonblocking() -> io::Result<()> {
Ok(())
}
pub mod content {
use std::borrow::Cow;
pub const EDITOR_SAMPLE_RUST: &str = r#"//! OpenTUI Demo - Sample Module
//!
//! This file demonstrates syntax highlighting capabilities.
use std::collections::HashMap;
use std::io::{self, Write};
/// A simple key-value store with TTL support.
#[derive(Debug, Clone)]
pub struct Cache<'a, V: Clone> {
entries: HashMap<&'a str, Entry<V>>,
max_size: usize,
}
#[derive(Debug, Clone)]
struct Entry<V> {
value: V,
expires_at: Option<u64>,
}
impl<'a, V: Clone> Cache<'a, V> {
/// Create a new cache with the given capacity.
pub fn new(max_size: usize) -> Self {
Self {
entries: HashMap::with_capacity(max_size),
max_size,
}
}
/// Insert a value with optional TTL.
pub fn insert(&mut self, key: &'a str, value: V, ttl: Option<u64>) -> Option<V> {
// TODO: Implement LRU eviction when at capacity
if self.entries.len() >= self.max_size {
return None; // Cache full
}
let entry = Entry {
value: value.clone(),
expires_at: ttl.map(|t| now() + t),
};
self.entries.insert(key, entry).map(|e| e.value)
}
/// Get a value if it exists and hasn't expired.
pub fn get(&self, key: &str) -> Option<&V> {
self.entries.get(key).and_then(|entry| {
match entry.expires_at {
Some(exp) if exp <= now() => None,
_ => Some(&entry.value),
}
})
}
}
/// Status of an async operation.
#[derive(Debug, PartialEq, Eq)]
pub enum Status {
Pending,
Running { progress: u8 },
Complete(Result<String, io::Error>),
}
impl Status {
/// Check if the operation is still in progress.
#[must_use]
pub const fn is_active(&self) -> bool {
matches!(self, Self::Pending | Self::Running { .. })
}
}
fn now() -> u64 {
// Placeholder for timestamp
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_insert_get() {
let mut cache = Cache::new(10);
cache.insert("key1", "value1", None);
assert_eq!(cache.get("key1"), Some(&"value1"));
}
}
"#;
pub const EDITOR_SAMPLE_MARKDOWN: &str = r#"# OpenTUI Showcase
Welcome to the **OpenTUI** demo application!
## Features
- Real RGBA alpha blending
- Scissor clipping stacks
- Double-buffered rendering
## Code Example
```rust
let mut renderer = Renderer::new(80, 24)?;
renderer.buffer().draw_text(0, 0, "Hello!", style);
renderer.present()?;
```
## Links
- [GitHub Repository](https://github.com/Dicklesworthstone/opentui_rust)
- [Unicode TR11](https://unicode.org/reports/tr11/)
"#;
pub const EDITOR_SAMPLE_PYTHON: &str = r#"#!/usr/bin/env python3
"""OpenTUI Demo - Python Sample
This file demonstrates Python syntax highlighting.
"""
from dataclasses import dataclass
from typing import Optional, List
import asyncio
@dataclass
class CacheEntry:
"""A single cache entry with optional TTL."""
value: str
expires_at: Optional[float] = None
class Cache:
"""Simple in-memory cache with TTL support."""
def __init__(self, max_size: int = 100):
self._entries: dict[str, CacheEntry] = {}
self._max_size = max_size
def get(self, key: str) -> Optional[str]:
"""Get a value if it exists and hasn't expired."""
entry = self._entries.get(key)
if entry is None:
return None
# TODO: Check expiration
return entry.value
async def fetch_or_compute(
self,
key: str,
compute_fn
) -> str:
"""Fetch from cache or compute and store."""
if (cached := self.get(key)) is not None:
return cached
value = await compute_fn()
self._entries[key] = CacheEntry(value=value)
return value
if __name__ == "__main__":
cache = Cache(max_size=50)
print(f"Cache size: {len(cache._entries)}")
"#;
pub const EDITOR_SAMPLE_TOML: &str = r#"# OpenTUI Configuration
# This file demonstrates TOML syntax highlighting.
[package]
name = "opentui"
version = "0.1.0"
edition = "2021"
authors = ["OpenTUI Contributors"]
description = "A high-performance TUI rendering library"
[features]
default = ["truecolor", "mouse"]
truecolor = []
mouse = []
hyperlinks = []
all = ["truecolor", "mouse", "hyperlinks"]
[dependencies]
unicode-width = "0.1"
unicode-segmentation = "1.10"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bin]]
name = "demo_showcase"
path = "src/bin/demo_showcase.rs"
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
"#;
#[derive(Clone, Debug)]
pub struct LogEntry {
pub timestamp: Cow<'static, str>,
pub level: LogLevel,
pub subsystem: Cow<'static, str>,
pub message: Cow<'static, str>,
pub link: Option<Cow<'static, str>>,
}
impl LogEntry {
#[must_use]
pub const fn new_static(
timestamp: &'static str,
level: LogLevel,
subsystem: &'static str,
message: &'static str,
link: Option<&'static str>,
) -> Self {
Self {
timestamp: Cow::Borrowed(timestamp),
level,
subsystem: Cow::Borrowed(subsystem),
message: Cow::Borrowed(message),
link: match link {
Some(s) => Some(Cow::Borrowed(s)),
None => None,
},
}
}
#[must_use]
#[allow(clippy::missing_const_for_fn, clippy::must_use_candidate)] pub fn new_runtime(
timestamp: String,
level: LogLevel,
subsystem: String,
message: String,
) -> Self {
Self {
timestamp: Cow::Owned(timestamp),
level,
subsystem: Cow::Owned(subsystem),
message: Cow::Owned(message),
link: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Debug => "DEBUG",
Self::Info => "INFO ",
Self::Warn => "WARN ",
Self::Error => "ERROR",
}
}
}
pub const LOG_ENTRIES: &[LogEntry] = &[
LogEntry::new_static(
"22:05:10",
LogLevel::Info,
"renderer",
"Initialized 80x24 buffer, truecolor enabled",
None,
),
LogEntry::new_static(
"22:05:10",
LogLevel::Debug,
"terminal",
"Raw mode enabled, mouse tracking active",
None,
),
LogEntry::new_static(
"22:05:11",
LogLevel::Info,
"input",
"InputParser ready, bracketed paste enabled",
None,
),
LogEntry::new_static(
"22:05:12",
LogLevel::Info,
"renderer",
"Frame 1: diff=1920 cells, output=4.2KB",
None,
),
LogEntry::new_static(
"22:05:12",
LogLevel::Info,
"renderer",
"Frame 2: diff=124 cells, output=0.3KB",
None,
),
LogEntry::new_static(
"22:05:13",
LogLevel::Warn,
"input",
"Focus lost - rendering paused",
None,
),
LogEntry::new_static(
"22:05:14",
LogLevel::Info,
"input",
"Focus regained - resuming",
None,
),
LogEntry::new_static(
"22:05:15",
LogLevel::Info,
"tour",
"Starting guided tour (13 steps)",
None,
),
LogEntry::new_static(
"22:05:16",
LogLevel::Debug,
"preview",
"Alpha blending demo: 50% opacity layer",
None,
),
LogEntry::new_static(
"22:05:17",
LogLevel::Info,
"docs",
"See OpenTUI repository for more info",
Some("https://github.com/Dicklesworthstone/opentui_rust"),
),
LogEntry::new_static(
"22:05:18",
LogLevel::Error,
"preview",
"Simulated error (demo only) - press R to retry",
None,
),
LogEntry::new_static(
"22:05:19",
LogLevel::Info,
"unicode",
"Width calculation: see Unicode TR11",
Some("https://unicode.org/reports/tr11/"),
),
LogEntry::new_static(
"22:05:20",
LogLevel::Debug,
"renderer",
"Scissor stack depth: 3, opacity: 0.85",
None,
),
LogEntry::new_static(
"22:05:21",
LogLevel::Info,
"highlight",
"Rust tokenizer: 847 tokens, 23 lines",
None,
),
LogEntry::new_static(
"22:05:22",
LogLevel::Warn,
"terminal",
"No XTVERSION response - assuming basic caps",
None,
),
];
#[derive(Clone, Copy, Debug, Default)]
pub struct Metrics {
pub fps: u32,
pub frame_time_ms: f32,
pub cpu_percent: u8,
pub memory_bytes: u64,
pub pulse: f32,
pub cells_changed: u32,
pub bytes_written: u32,
}
impl Metrics {
#[must_use]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)] pub fn compute(frame: u64, target_fps: u32) -> Self {
const FRAME_CYCLE: u64 = 10_000_000; let frame_mod = (frame % FRAME_CYCLE) as f32;
let target_fps_f = (target_fps.max(1)) as f32;
let fps_variation = (frame_mod * 0.1).sin() * 2.0;
let fps = (target_fps_f + fps_variation).clamp(1.0, 120.0) as u32;
let frame_time_ms = 1000.0 / (fps as f32).max(1.0);
let cpu_base = (frame_mod * 0.02).sin().mul_add(10.0, 15.0);
let cpu_percent = cpu_base.clamp(0.0, 100.0) as u8;
let memory_cycle = frame % 1000;
let memory_bytes = 50_000_000 + (memory_cycle * 10_000);
let pulse_phase = (frame % 60) as f32 / 60.0;
let pulse = (pulse_phase * std::f32::consts::PI).sin();
let cells_changed = if frame == 0 {
1920 } else {
(50 + ((frame_mod * 0.5).sin().abs() * 150.0) as u32).min(500)
};
let bytes_written = cells_changed * 8 + 100;
Self {
fps,
frame_time_ms,
cpu_percent,
memory_bytes,
pulse,
cells_changed,
bytes_written,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn memory_display(&self) -> String {
if self.memory_bytes >= 1_000_000 {
format!("{:.1}MB", self.memory_bytes as f64 / 1_000_000.0)
} else if self.memory_bytes >= 1_000 {
format!("{:.1}KB", self.memory_bytes as f64 / 1_000.0)
} else {
format!("{}B", self.memory_bytes)
}
}
}
#[derive(Clone, Debug)]
pub struct DemoFile {
pub name: &'static str,
pub language: Language,
pub text: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Language {
#[default]
Rust,
Markdown,
Python,
Toml,
Plain,
}
impl Language {
#[must_use]
pub const fn extension(self) -> &'static str {
match self {
Self::Rust => "rs",
Self::Markdown => "md",
Self::Python => "py",
Self::Toml => "toml",
Self::Plain => "txt",
}
}
}
#[derive(Clone, Debug)]
pub struct DemoLinks {
pub repo: &'static str,
pub source: &'static str,
pub docs: &'static str,
pub unicode_ref: &'static str,
}
impl Default for DemoLinks {
fn default() -> Self {
Self {
repo: links::REPO,
source: links::SOURCE,
docs: links::RUST_DOCS,
unicode_ref: links::UNICODE_TR11,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct MetricParams {
pub target_fps: u32,
}
impl Default for MetricParams {
fn default() -> Self {
Self { target_fps: 60 }
}
}
#[derive(Clone, Debug)]
pub struct DemoContent {
pub files: &'static [DemoFile],
pub links: DemoLinks,
pub seed_logs: &'static [LogEntry],
pub metric_params: MetricParams,
}
pub const DEFAULT_FILES: &[DemoFile] = &[
DemoFile {
name: "cache.rs",
language: Language::Rust,
text: EDITOR_SAMPLE_RUST,
},
DemoFile {
name: "README.md",
language: Language::Markdown,
text: EDITOR_SAMPLE_MARKDOWN,
},
DemoFile {
name: "cache.py",
language: Language::Python,
text: EDITOR_SAMPLE_PYTHON,
},
DemoFile {
name: "Cargo.toml",
language: Language::Toml,
text: EDITOR_SAMPLE_TOML,
},
];
impl Default for DemoContent {
fn default() -> Self {
Self {
files: DEFAULT_FILES,
links: DemoLinks::default(),
seed_logs: LOG_ENTRIES,
metric_params: MetricParams::default(),
}
}
}
impl DemoContent {
#[must_use]
pub const fn primary_file(&self) -> Option<&DemoFile> {
self.files.first()
}
#[must_use]
pub const fn log_count(&self) -> usize {
self.seed_logs.len()
}
#[must_use]
pub fn compute_metrics(&self, frame: u64) -> Metrics {
Metrics::compute(frame, self.metric_params.target_fps)
}
}
pub mod unicode {
pub const CJK_WIDE: &str = "漢字かなカナ";
pub const EMOJI_SINGLE: &str = "🎉👍😀🚀✨";
pub const EMOJI_ZWJ: &str = "👨👩👧 👩💻 🧑🚀 👨🔬 👩🎨";
pub const COMBINING_MARKS: &str = "a\u{0301} e\u{0301} n\u{0303} o\u{0308}";
pub const COMBINING_DISPLAY: &str = "á é ñ ö";
pub const MIXED_LINE: &str = "Hello 世界 🌍 café naïve 👨👩👧👦";
pub const WIDTH_RULER_10: &str = "0123456789";
pub const WIDTH_TEST_CASES: &[(&str, &str, usize)] = &[
("ASCII", "Hello", 5),
("CJK", "漢字", 4), ("Emoji", "🎉👍", 4), ("Mixed", "A漢B", 4), ("Combining", "a\u{0301}", 1), ];
}
pub mod links {
pub const REPO: &str = "https://github.com/Dicklesworthstone/opentui_rust";
pub const SOURCE: &str = "https://github.com/Dicklesworthstone/opentui_rust/tree/main/src";
pub const UNICODE_TR11: &str = "https://unicode.org/reports/tr11/";
pub const RUST_DOCS: &str = "https://doc.rust-lang.org/stable/std/";
pub const ALL: &[(&str, &str)] = &[
("OpenTUI Repository", REPO),
("Source Code", SOURCE),
("Unicode TR11", UNICODE_TR11),
("Rust Docs", RUST_DOCS),
];
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(strs: &[&str]) -> Vec<OsString> {
strs.iter().map(|s| OsString::from(*s)).collect()
}
#[test]
fn test_default_config() {
let result = Config::from_args(args(&["demo_showcase"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert_eq!(config.fps_cap, 60);
assert!(config.enable_mouse);
assert!(config.use_alt_screen);
assert!(!config.headless_smoke);
}
#[test]
fn test_help_flag() {
let result = Config::from_args(args(&["demo_showcase", "--help"]));
assert!(matches!(result, ParseResult::Help));
}
#[test]
fn test_fps_flag() {
let result = Config::from_args(args(&["demo_showcase", "--fps", "30"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert_eq!(config.fps_cap, 30);
}
#[test]
fn test_no_mouse_flag() {
let result = Config::from_args(args(&["demo_showcase", "--no-mouse"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert!(!config.enable_mouse);
}
#[test]
fn test_headless_smoke_flag() {
let result = Config::from_args(args(&["demo_showcase", "--headless-smoke"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert!(config.headless_smoke);
}
#[test]
fn test_headless_size() {
let result = Config::from_args(args(&["demo_showcase", "--headless-size", "120x40"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert_eq!(config.headless_size, (120, 40));
}
#[test]
fn test_headless_check_valid() {
for check in ["layout", "config", "palette", "hitgrid", "logs"] {
let result = Config::from_args(args(&["demo_showcase", "--headless-check", check]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config for check: {check}");
};
assert_eq!(config.headless_check, Some(check.to_string()));
}
}
#[test]
fn test_headless_check_invalid() {
let result = Config::from_args(args(&["demo_showcase", "--headless-check", "invalid"]));
assert!(matches!(result, ParseResult::Error(_)));
}
#[test]
fn test_max_frames() {
let result = Config::from_args(args(&["demo_showcase", "--max-frames", "100"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert_eq!(config.max_frames, Some(100));
}
#[test]
fn test_parse_size() {
assert_eq!(parse_size("80x24"), Some((80, 24)));
assert_eq!(parse_size("120x40"), Some((120, 40)));
assert_eq!(parse_size("invalid"), None);
assert_eq!(parse_size("80"), None);
assert_eq!(parse_size("0x24"), None);
}
#[test]
fn test_unknown_option_error() {
let result = Config::from_args(args(&["demo_showcase", "--unknown"]));
assert!(matches!(result, ParseResult::Error(_)));
}
#[test]
fn test_cap_preset() {
let result = Config::from_args(args(&["demo_showcase", "--cap-preset", "no_mouse"]));
let ParseResult::Config(config) = result else {
unreachable!("Expected Config");
};
assert_eq!(config.cap_preset, CapPreset::NoMouse);
}
#[test]
fn test_rect_new() {
let r = Rect::new(10, 20, 100, 50);
assert_eq!(r.x, 10);
assert_eq!(r.y, 20);
assert_eq!(r.w, 100);
assert_eq!(r.h, 50);
}
#[test]
fn test_rect_from_size() {
let r = Rect::from_size(80, 24);
assert_eq!(r.x, 0);
assert_eq!(r.y, 0);
assert_eq!(r.w, 80);
assert_eq!(r.h, 24);
}
#[test]
fn test_rect_inset() {
let r = Rect::new(0, 0, 100, 50);
let inset = r.inset(5);
assert_eq!(inset.x, 5);
assert_eq!(inset.y, 5);
assert_eq!(inset.w, 90);
assert_eq!(inset.h, 40);
}
#[test]
fn test_rect_inset_overflow() {
let r = Rect::new(0, 0, 10, 10);
let inset = r.inset(10); assert_eq!(inset.w, 0);
assert_eq!(inset.h, 0);
}
#[test]
fn test_rect_split_h() {
let r = Rect::new(0, 0, 100, 50);
let (left, right) = r.split_h(30);
assert_eq!(left.x, 0);
assert_eq!(left.w, 30);
assert_eq!(right.x, 30);
assert_eq!(right.w, 70);
assert_eq!(left.h, 50);
assert_eq!(right.h, 50);
}
#[test]
fn test_rect_split_h_overflow() {
let r = Rect::new(0, 0, 50, 50);
let (left, right) = r.split_h(100); assert_eq!(left.w, 50);
assert_eq!(right.w, 0);
}
#[test]
fn test_rect_split_v() {
let r = Rect::new(0, 0, 100, 50);
let (top, bottom) = r.split_v(20);
assert_eq!(top.y, 0);
assert_eq!(top.h, 20);
assert_eq!(bottom.y, 20);
assert_eq!(bottom.h, 30);
assert_eq!(top.w, 100);
assert_eq!(bottom.w, 100);
}
#[test]
fn test_rect_clamp_to() {
let r = Rect::new(0, 0, 100, 50);
let clamped = r.clamp_to(60, 30);
assert_eq!(clamped.w, 60);
assert_eq!(clamped.h, 30);
}
#[test]
fn test_rect_is_empty() {
assert!(Rect::new(0, 0, 0, 10).is_empty());
assert!(Rect::new(0, 0, 10, 0).is_empty());
assert!(!Rect::new(0, 0, 10, 10).is_empty());
}
#[test]
fn test_rect_right_bottom() {
let r = Rect::new(10, 20, 30, 40);
assert_eq!(r.right(), 40);
assert_eq!(r.bottom(), 60);
}
#[test]
fn test_layout_mode_full() {
assert_eq!(LayoutMode::from_size(80, 24), LayoutMode::Full);
assert_eq!(LayoutMode::from_size(120, 40), LayoutMode::Full);
}
#[test]
fn test_layout_mode_compact() {
assert_eq!(LayoutMode::from_size(79, 24), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(80, 23), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(60, 16), LayoutMode::Compact);
}
#[test]
fn test_layout_mode_minimal() {
assert_eq!(LayoutMode::from_size(59, 16), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(60, 15), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(40, 12), LayoutMode::Minimal);
}
#[test]
fn test_layout_mode_too_small() {
assert_eq!(LayoutMode::from_size(39, 12), LayoutMode::TooSmall);
assert_eq!(LayoutMode::from_size(40, 11), LayoutMode::TooSmall);
assert_eq!(LayoutMode::from_size(20, 10), LayoutMode::TooSmall);
}
#[test]
fn test_panel_layout_full() {
let layout = PanelLayout::compute(100, 30);
assert_eq!(layout.mode, LayoutMode::Full);
assert_eq!(layout.top_bar.h, 1);
assert_eq!(layout.status_bar.h, 1);
assert_eq!(layout.sidebar.w, layout::SIDEBAR_WIDTH_FULL);
assert!(!layout.preview.is_empty());
assert!(!layout.logs.is_empty());
assert!(layout.logs.h >= layout::LOGS_HEIGHT_FULL.min(layout.main_area.h / 3));
}
#[test]
fn test_panel_layout_compact() {
let layout = PanelLayout::compute(70, 20);
assert_eq!(layout.mode, LayoutMode::Compact);
assert_eq!(layout.sidebar.w, layout::SIDEBAR_WIDTH_COMPACT);
assert!(layout.preview.is_empty()); assert!(!layout.logs.is_empty());
assert!(layout.logs.h >= layout::LOGS_HEIGHT_COMPACT.min(layout.main_area.h / 3));
}
#[test]
fn test_panel_layout_minimal() {
let layout = PanelLayout::compute(50, 14);
assert_eq!(layout.mode, LayoutMode::Minimal);
assert_eq!(layout.sidebar.w, 0); }
#[test]
fn test_panel_layout_too_small() {
let layout = PanelLayout::compute(30, 10);
assert_eq!(layout.mode, LayoutMode::TooSmall);
}
#[test]
fn test_theme_synthwave() {
let theme = Theme::synthwave();
assert!(theme.bg0.a > 0.0);
assert!(theme.bg1.a > 0.0);
assert!(theme.bg2.a > 0.0);
assert!(theme.fg0.a > 0.0);
assert!(theme.fg1.a > 0.0);
assert!(theme.fg2.a > 0.0);
assert!(theme.accent_primary.a > 0.0);
assert!(theme.accent_secondary.a > 0.0);
assert!(theme.selection_bg.a > 0.0);
assert!(theme.focus_border.a > 0.0);
}
#[test]
fn test_theme_paper_light() {
let theme = Theme::paper_light();
assert!(theme.bg0.a > 0.0);
assert!(theme.fg0.a > 0.0);
assert!(theme.bg0.r > 0.9);
}
#[test]
fn test_theme_solarized() {
let theme = Theme::solarized();
assert!(theme.bg0.a > 0.0);
assert!(theme.fg0.a > 0.0);
assert!(theme.bg0.b > theme.bg0.r);
}
#[test]
fn test_theme_high_contrast() {
let theme = Theme::high_contrast();
assert!(theme.bg0.a > 0.0);
assert!(theme.fg0.a > 0.0);
assert!(theme.bg0.r < 0.01);
assert!(theme.fg0.r > 0.99);
}
#[test]
fn test_ui_theme_default() {
assert_eq!(UiTheme::default(), UiTheme::SynthwaveDark);
}
#[test]
fn test_ui_theme_next() {
assert_eq!(UiTheme::SynthwaveDark.next(), UiTheme::PaperLight);
assert_eq!(UiTheme::PaperLight.next(), UiTheme::Solarized);
assert_eq!(UiTheme::Solarized.next(), UiTheme::HighContrast);
assert_eq!(UiTheme::HighContrast.next(), UiTheme::SynthwaveDark);
}
#[test]
fn test_ui_theme_is_dark() {
assert!(UiTheme::SynthwaveDark.is_dark());
assert!(!UiTheme::PaperLight.is_dark());
assert!(UiTheme::Solarized.is_dark());
assert!(UiTheme::HighContrast.is_dark());
}
#[test]
fn test_ui_theme_tokens() {
for theme in UiTheme::ALL {
let tokens = theme.tokens();
assert!(tokens.bg0.a > 0.0);
assert!(tokens.fg0.a > 0.0);
}
}
#[test]
fn test_ui_theme_name() {
assert_eq!(UiTheme::SynthwaveDark.name(), "Synthwave");
assert_eq!(UiTheme::PaperLight.name(), "Paper");
assert_eq!(UiTheme::Solarized.name(), "Solarized");
assert_eq!(UiTheme::HighContrast.name(), "High Contrast");
}
#[test]
fn test_theme_lerp() {
let black = Rgba::BLACK;
let white = Rgba::WHITE;
let mid = Theme::lerp(black, white, 0.5);
assert!((mid.r - 0.5).abs() < 0.01);
assert!((mid.g - 0.5).abs() < 0.01);
assert!((mid.b - 0.5).abs() < 0.01);
}
#[test]
fn test_theme_gradient() {
let start = Rgba::BLACK;
let end = Rgba::WHITE;
let colors: Vec<_> = Theme::gradient(start, end, 5).collect();
assert_eq!(colors.len(), 5);
assert!(colors[0].r < 0.01);
assert!(colors[4].r > 0.99);
}
#[test]
fn test_styles_header() {
let theme = Theme::synthwave();
let style = Styles::header(&theme);
assert_eq!(style.fg, Some(theme.fg0));
assert_eq!(style.bg, Some(theme.bg1));
}
#[test]
fn test_styles_selection() {
let theme = Theme::synthwave();
let style = Styles::selection(&theme);
assert_eq!(style.bg, Some(theme.selection_bg));
}
#[test]
fn test_render_pass_order() {
assert_eq!(RenderPass::Background as u8, 0);
assert_eq!(RenderPass::Chrome as u8, 1);
assert_eq!(RenderPass::Panels as u8, 2);
assert_eq!(RenderPass::Overlays as u8, 3);
assert_eq!(RenderPass::Toasts as u8, 4);
assert_eq!(RenderPass::Debug as u8, 5);
}
#[test]
fn test_render_pass_all() {
assert_eq!(RenderPass::ALL.len(), 6);
assert_eq!(RenderPass::ALL[0], RenderPass::Background);
assert_eq!(RenderPass::ALL[5], RenderPass::Debug);
}
#[test]
fn test_input_pump_new() {
let pump = InputPump::new();
assert!(pump.synthetic_queue.is_empty());
assert!(pump.accumulator.is_empty());
}
#[test]
fn test_input_pump_default() {
let pump = InputPump::default();
assert!(pump.accumulator.is_empty());
}
#[test]
fn test_input_pump_inject_synthetic() {
let mut pump = InputPump::new();
let event = Event::Key(opentui::input::KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::empty(),
});
pump.inject_synthetic(event);
assert_eq!(pump.synthetic_queue.len(), 1);
}
#[test]
fn test_input_pump_clear() {
let mut pump = InputPump::new();
pump.accumulator.extend_from_slice(b"test");
pump.clear();
assert!(pump.accumulator.is_empty());
}
#[test]
fn test_tagged_event_real() {
let event = Event::Key(opentui::input::KeyEvent {
code: KeyCode::Char('x'),
modifiers: KeyModifiers::empty(),
});
let tagged = TaggedEvent::real(event);
assert_eq!(tagged.source, InputSource::Real);
}
#[test]
fn test_tagged_event_synthetic() {
let event = Event::Key(opentui::input::KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::empty(),
});
let tagged = TaggedEvent::synthetic(event);
assert_eq!(tagged.source, InputSource::Synthetic);
}
#[test]
fn test_input_source_equality() {
assert_eq!(InputSource::Real, InputSource::Real);
assert_eq!(InputSource::Synthetic, InputSource::Synthetic);
assert_ne!(InputSource::Real, InputSource::Synthetic);
}
#[test]
fn test_app_mode_default() {
assert_eq!(AppMode::default(), AppMode::Normal);
}
#[test]
fn test_focus_cycle() {
assert_eq!(Focus::Sidebar.next(), Focus::Editor);
assert_eq!(Focus::Editor.next(), Focus::Preview);
assert_eq!(Focus::Preview.next(), Focus::Logs);
assert_eq!(Focus::Logs.next(), Focus::Sidebar);
}
#[test]
fn test_focus_cycle_backward() {
assert_eq!(Focus::Sidebar.prev(), Focus::Logs);
assert_eq!(Focus::Editor.prev(), Focus::Sidebar);
assert_eq!(Focus::Preview.prev(), Focus::Editor);
assert_eq!(Focus::Logs.prev(), Focus::Preview);
}
#[test]
fn test_section_all() {
assert_eq!(Section::ALL.len(), 12);
}
#[test]
fn test_section_from_index() {
assert_eq!(Section::from_index(0), Some(Section::Overview));
assert_eq!(Section::from_index(5), Some(Section::Performance));
assert_eq!(Section::from_index(6), Some(Section::Drawing));
assert_eq!(Section::from_index(11), Some(Section::Animations));
assert_eq!(Section::from_index(12), None);
}
#[test]
fn test_section_name() {
assert_eq!(Section::Overview.name(), "Overview");
assert_eq!(Section::Performance.name(), "Performance");
assert_eq!(Section::Drawing.name(), "Drawing");
assert_eq!(Section::Animations.name(), "Animations");
}
#[test]
fn test_app_default() {
let app = App::default();
assert_eq!(app.mode, AppMode::Normal);
assert_eq!(app.focus, Focus::Sidebar);
assert_eq!(app.section, Section::Overview);
assert!(!app.paused);
assert!(!app.should_quit);
}
#[test]
fn test_app_new_tour_mode() {
let config = Config {
start_in_tour: true,
..Default::default()
};
let app = App::new(&config);
assert_eq!(app.mode, AppMode::Tour);
}
#[test]
fn test_exit_after_tour() {
let config = Config {
start_in_tour: true,
exit_after_tour: true,
..Default::default()
};
let mut app = App::new(&config);
assert_eq!(app.mode, AppMode::Tour);
assert!(!app.should_quit);
assert!(app.tour_runner.as_ref().is_some_and(|r| r.exit_on_complete));
let total_steps = app.tour_total;
for _ in 0..total_steps {
let current_t = app.clock.t;
if let Some(runner) = app.tour_runner.as_mut() {
let completed = runner.next_step(current_t);
if completed && runner.exit_on_complete {
app.should_quit = true;
app.exit_reason = ExitReason::TourComplete;
}
}
}
assert!(app.should_quit);
assert_eq!(app.exit_reason, ExitReason::TourComplete);
}
#[test]
fn test_app_mode_name() {
let mut app = App::default();
assert_eq!(app.mode_name(), "Normal");
app.mode = AppMode::Help;
assert_eq!(app.mode_name(), "Help");
app.mode = AppMode::CommandPalette;
assert_eq!(app.mode_name(), "Palette");
app.mode = AppMode::Tour;
assert_eq!(app.mode_name(), "Tour");
}
#[test]
fn test_app_focus_name() {
let mut app = App::default();
assert_eq!(app.focus_name(), "Sidebar");
app.focus = Focus::Editor;
assert_eq!(app.focus_name(), "Editor");
}
#[test]
fn test_app_tick() {
let mut app = App::default();
assert_eq!(app.frame_count, 0);
app.tick();
assert_eq!(app.frame_count, 1);
app.tick();
assert_eq!(app.frame_count, 2);
}
#[test]
fn test_app_max_frames() {
let config = Config {
max_frames: Some(5),
..Default::default()
};
let mut app = App::new(&config);
for _ in 0..4 {
app.tick();
assert!(!app.should_quit);
assert_eq!(app.exit_reason, ExitReason::UserQuit);
}
app.tick();
assert!(app.should_quit);
assert_eq!(app.exit_reason, ExitReason::MaxFrames);
}
#[test]
fn test_action_toggle_help() {
let mut app = App::default();
assert_eq!(app.mode, AppMode::Normal);
app.apply_action(&Action::ToggleHelp);
assert_eq!(app.mode, AppMode::Help);
app.apply_action(&Action::ToggleHelp);
assert_eq!(app.mode, AppMode::Normal);
}
#[test]
fn test_action_cycle_focus() {
let mut app = App::default();
assert_eq!(app.focus, Focus::Sidebar);
app.apply_action(&Action::CycleFocusForward);
assert_eq!(app.focus, Focus::Editor);
app.apply_action(&Action::CycleFocusBackward);
assert_eq!(app.focus, Focus::Sidebar);
}
#[test]
fn test_action_navigate_section() {
let mut app = App::default();
assert_eq!(app.section, Section::Overview);
app.apply_action(&Action::NavigateSection(Section::Editor));
assert_eq!(app.section, Section::Editor);
}
#[test]
fn test_action_quit() {
let mut app = App::default();
assert!(!app.should_quit);
app.apply_action(&Action::Quit);
assert!(app.should_quit);
}
#[test]
fn test_smoothstep_boundaries() {
assert!((easing::smoothstep(0.0) - 0.0).abs() < f32::EPSILON);
assert!((easing::smoothstep(1.0) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_smoothstep_midpoint() {
assert!((easing::smoothstep(0.5) - 0.5).abs() < 0.001);
}
#[test]
fn test_smoothstep_clamping() {
assert!((easing::smoothstep(-0.5) - 0.0).abs() < f32::EPSILON);
assert!((easing::smoothstep(1.5) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_ease_in_out_cubic_boundaries() {
assert!((easing::ease_in_out_cubic(0.0) - 0.0).abs() < f32::EPSILON);
assert!((easing::ease_in_out_cubic(1.0) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_ease_in_out_cubic_midpoint() {
assert!((easing::ease_in_out_cubic(0.5) - 0.5).abs() < 0.001);
}
#[test]
fn test_ease_out_cubic_boundaries() {
assert!((easing::ease_out_cubic(0.0) - 0.0).abs() < f32::EPSILON);
assert!((easing::ease_out_cubic(1.0) - 1.0).abs() < f32::EPSILON);
}
#[test]
#[allow(clippy::cast_precision_loss)] fn test_pulse_range() {
let omega = std::f32::consts::TAU; for i in 0..10 {
let t = i as f32 * 0.1;
let v = easing::pulse(t, omega);
assert!(
(0.0..=1.0).contains(&v),
"pulse({t}, {omega}) = {v} out of range"
);
}
}
#[test]
fn test_pulse_at_zero() {
assert!((easing::pulse(0.0, 1.0) - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_animation_clock_new() {
let clock = AnimationClock::new();
assert!((clock.t - 0.0).abs() < f32::EPSILON);
assert!((clock.dt - 0.0).abs() < f32::EPSILON);
assert!(!clock.is_paused());
}
#[test]
fn test_animation_clock_tick_advances_time() {
let mut clock = AnimationClock::new();
std::thread::sleep(std::time::Duration::from_millis(10));
clock.tick(false);
assert!(clock.dt > 0.0, "dt should be positive after tick");
assert!(clock.t > 0.0, "t should advance when not paused");
}
#[test]
fn test_animation_clock_paused_no_advance() {
let mut clock = AnimationClock::new();
std::thread::sleep(std::time::Duration::from_millis(10));
clock.tick(true); assert!(clock.dt > 0.0, "dt should still be computed when paused");
assert!(
(clock.t - 0.0).abs() < f32::EPSILON,
"t should not advance when paused"
);
}
#[test]
fn test_animation_clock_dt_clamped() {
let mut clock = AnimationClock::new();
clock.tick(false);
assert!(
clock.dt <= AnimationClock::MAX_DT,
"dt should be clamped to MAX_DT"
);
assert!(
clock.dt >= AnimationClock::MIN_DT,
"dt should be at least MIN_DT"
);
}
#[test]
fn test_animation_clock_pulse_helper() {
let clock = AnimationClock::new();
let omega = std::f32::consts::TAU;
let p = clock.pulse(omega);
assert!((p - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_animation_clock_t_offset() {
let clock = AnimationClock::new();
let offset = clock.t_offset(1.0);
assert!((offset - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_overlay_anim_with_dt() {
let mut anim = OverlayAnim::opening();
assert!((anim.progress - 0.0).abs() < f32::EPSILON);
let dt = 0.05; anim.tick(dt);
assert!((anim.progress - 0.45).abs() < 0.001);
anim.tick(0.1);
assert!((anim.progress - 1.0).abs() < f32::EPSILON);
assert!(anim.is_open());
}
#[test]
fn test_overlay_anim_closing_with_dt() {
let mut anim = OverlayAnim::opening();
anim.progress = 1.0; anim.start_close();
anim.tick(0.05);
assert!((anim.progress - 0.55).abs() < 0.001);
}
#[test]
fn test_demo_content_default() {
let content = content::DemoContent::default();
assert!(!content.files.is_empty(), "Should have default files");
assert_eq!(content.files[0].name, "cache.rs");
assert_eq!(content.files[0].language, content::Language::Rust);
assert!(!content.seed_logs.is_empty(), "Should have seed logs");
assert_eq!(content.metric_params.target_fps, 60);
}
#[test]
fn test_demo_content_primary_file() {
let content = content::DemoContent::default();
let primary = content.primary_file();
assert!(primary.is_some());
assert_eq!(primary.unwrap().name, "cache.rs");
}
#[test]
fn test_demo_content_log_count() {
let content = content::DemoContent::default();
assert_eq!(content.log_count(), content::LOG_ENTRIES.len());
}
#[test]
fn test_demo_content_compute_metrics() {
let content = content::DemoContent::default();
let m = content.compute_metrics(0);
assert!(m.fps > 0);
assert!(m.frame_time_ms > 0.0);
}
#[test]
fn test_language_extension() {
assert_eq!(content::Language::Rust.extension(), "rs");
assert_eq!(content::Language::Markdown.extension(), "md");
assert_eq!(content::Language::Plain.extension(), "txt");
}
#[test]
fn test_app_content_initialization() {
let app = App::default();
assert_eq!(app.current_file_idx, 0);
assert!(!app.logs.is_empty(), "Should have seed logs");
assert_eq!(app.target_fps, 60);
}
#[test]
fn test_app_current_file() {
let app = App::default();
let file = app.current_file();
assert!(file.is_some());
assert_eq!(file.unwrap().name, "cache.rs");
}
#[test]
fn test_app_current_file_name() {
let app = App::default();
assert_eq!(app.current_file_name(), "cache.rs");
}
#[test]
fn test_app_current_file_language() {
let app = App::default();
assert_eq!(app.current_file_language(), content::Language::Rust);
}
#[test]
fn test_app_next_file() {
let mut app = App::default();
assert_eq!(app.current_file_idx, 0);
app.next_file();
assert_eq!(app.current_file_idx, 1);
assert_eq!(app.current_file_name(), "README.md");
app.next_file();
assert_eq!(app.current_file_idx, 2);
assert_eq!(app.current_file_name(), "cache.py");
app.next_file();
assert_eq!(app.current_file_idx, 3);
assert_eq!(app.current_file_name(), "Cargo.toml");
app.next_file();
assert_eq!(app.current_file_idx, 0);
assert_eq!(app.current_file_name(), "cache.rs");
}
#[test]
fn test_app_prev_file() {
let mut app = App::default();
assert_eq!(app.current_file_idx, 0);
app.prev_file();
assert_eq!(app.current_file_idx, 3);
assert_eq!(app.current_file_name(), "Cargo.toml");
app.prev_file();
assert_eq!(app.current_file_idx, 2);
assert_eq!(app.current_file_name(), "cache.py");
app.prev_file();
assert_eq!(app.current_file_idx, 1);
assert_eq!(app.current_file_name(), "README.md");
app.prev_file();
assert_eq!(app.current_file_idx, 0);
assert_eq!(app.current_file_name(), "cache.rs");
}
#[test]
fn test_app_metrics_update() {
let mut app = App::default();
let initial_metrics = app.metrics;
app.tick();
assert_eq!(app.frame_count, 1);
assert!(
app.metrics.memory_bytes != initial_metrics.memory_bytes
|| app.metrics.cells_changed != initial_metrics.cells_changed
);
}
#[test]
fn test_app_add_log() {
let mut app = App::default();
let initial_count = app.logs.len();
app.add_log(content::LogEntry::new_static(
"23:00:00",
content::LogLevel::Info,
"test",
"Test log entry",
None,
));
assert_eq!(app.logs.len(), initial_count + 1);
}
#[test]
fn test_max_logs_constant() {
assert_eq!(App::MAX_LOGS, 1000);
}
#[test]
fn test_add_log_under_limit() {
let mut app = App::default();
app.logs.clear();
for i in 0..10 {
app.add_log(content::LogEntry::new_runtime(
format!("00:00:{i:02}"),
content::LogLevel::Info,
"test".to_string(),
format!("Log entry {i}"),
));
}
assert_eq!(app.logs.len(), 10);
}
#[test]
fn test_add_log_at_limit_evicts_oldest() {
let mut app = App::default();
app.logs.clear();
for i in 0..App::MAX_LOGS {
app.add_log(content::LogEntry::new_runtime(
format!("{i:06}"),
content::LogLevel::Info,
"test".to_string(),
format!("Log {i}"),
));
}
assert_eq!(app.logs.len(), App::MAX_LOGS);
assert_eq!(app.logs.front().unwrap().timestamp.as_ref(), "000000");
app.add_log(content::LogEntry::new_runtime(
"NEW".to_string(),
content::LogLevel::Info,
"test".to_string(),
"Newest entry".to_string(),
));
assert_eq!(app.logs.len(), App::MAX_LOGS);
assert_ne!(app.logs.front().unwrap().timestamp.as_ref(), "000000");
assert_eq!(app.logs.front().unwrap().timestamp.as_ref(), "000001");
assert_eq!(app.logs.back().unwrap().timestamp.as_ref(), "NEW");
}
#[test]
fn test_add_log_preserves_order() {
let mut app = App::default();
app.logs.clear();
for i in 0..5 {
app.add_log(content::LogEntry::new_runtime(
format!("{i}"),
content::LogLevel::Info,
"test".to_string(),
format!("Message {i}"),
));
}
let timestamps: Vec<&str> = app.logs.iter().map(|e| e.timestamp.as_ref()).collect();
assert_eq!(timestamps, vec!["0", "1", "2", "3", "4"]);
}
#[test]
fn test_add_log_overflow_behavior() {
let mut app = App::default();
app.logs.clear();
let total = App::MAX_LOGS + 5;
for i in 0..total {
app.add_log(content::LogEntry::new_runtime(
format!("{i:06}"),
content::LogLevel::Info,
"test".to_string(),
format!("Log {i}"),
));
}
assert_eq!(app.logs.len(), App::MAX_LOGS);
assert_eq!(app.logs.front().unwrap().timestamp.as_ref(), "000005");
let last = total - 1;
assert_eq!(
app.logs.back().unwrap().timestamp.as_ref(),
format!("{last:06}")
);
}
#[test]
fn test_dropped_log_count_increments() {
let _ = dropped_log_count().swap(0, std::sync::atomic::Ordering::Relaxed);
dropped_log_count().fetch_add(1, std::sync::atomic::Ordering::Relaxed);
dropped_log_count().fetch_add(1, std::sync::atomic::Ordering::Relaxed);
dropped_log_count().fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let count = dropped_log_count().load(std::sync::atomic::Ordering::Relaxed);
assert_eq!(count, 3);
let _ = dropped_log_count().swap(0, std::sync::atomic::Ordering::Relaxed);
}
#[test]
fn test_dropped_counter_swaps_zero() {
let _ = dropped_log_count().swap(0, std::sync::atomic::Ordering::Relaxed);
for _ in 0..5 {
dropped_log_count().fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
let dropped = dropped_log_count().swap(0, std::sync::atomic::Ordering::Relaxed);
assert_eq!(dropped, 5);
let after = dropped_log_count().load(std::sync::atomic::Ordering::Relaxed);
assert_eq!(after, 0);
}
#[test]
fn test_metrics_compute_deterministic() {
let m1 = content::Metrics::compute(100, 60);
let m2 = content::Metrics::compute(100, 60);
assert_eq!(m1.fps, m2.fps);
assert_eq!(m1.cpu_percent, m2.cpu_percent);
assert_eq!(m1.memory_bytes, m2.memory_bytes);
assert!((m1.pulse - m2.pulse).abs() < f32::EPSILON);
}
#[test]
fn test_metrics_memory_display() {
let m = content::Metrics {
memory_bytes: 50_000_000,
..Default::default()
};
assert_eq!(m.memory_display(), "50.0MB");
let m2 = content::Metrics {
memory_bytes: 500_000,
..Default::default()
};
assert_eq!(m2.memory_display(), "500.0KB");
let m3 = content::Metrics {
memory_bytes: 500,
..Default::default()
};
assert_eq!(m3.memory_display(), "500B");
}
fn calculate_scroll_offset(selected: usize, max_visible: usize) -> usize {
if selected >= max_visible {
selected - max_visible + 1
} else {
0
}
}
fn is_item_selected(display_index: usize, scroll_offset: usize, selected: usize) -> bool {
(display_index + scroll_offset) == selected
}
#[test]
fn test_scroll_offset_zero_when_selected_visible() {
assert_eq!(calculate_scroll_offset(0, 5), 0);
assert_eq!(calculate_scroll_offset(1, 5), 0);
assert_eq!(calculate_scroll_offset(2, 5), 0);
assert_eq!(calculate_scroll_offset(3, 5), 0);
assert_eq!(calculate_scroll_offset(4, 5), 0);
}
#[test]
fn test_scroll_offset_scrolls_to_selected() {
assert_eq!(calculate_scroll_offset(5, 5), 1); assert_eq!(calculate_scroll_offset(6, 5), 2); assert_eq!(calculate_scroll_offset(10, 5), 6); }
#[test]
fn test_scroll_offset_at_list_end() {
let list_len = 20;
let max_visible = 5;
let selected = list_len - 1; let offset = calculate_scroll_offset(selected, max_visible);
assert_eq!(offset, 15);
assert!(is_item_selected(4, offset, selected));
}
#[test]
fn test_is_selected_uses_actual_index() {
let selected = 7;
let max_visible = 5;
let offset = calculate_scroll_offset(selected, max_visible);
assert!(!is_item_selected(0, offset, selected)); assert!(!is_item_selected(1, offset, selected)); assert!(!is_item_selected(2, offset, selected)); assert!(!is_item_selected(3, offset, selected)); assert!(is_item_selected(4, offset, selected)); }
#[test]
fn test_scroll_handles_single_item() {
let offset = calculate_scroll_offset(0, 1);
assert_eq!(offset, 0);
assert!(is_item_selected(0, offset, 0));
}
#[test]
fn test_scroll_handles_max_visible_equals_list_len() {
let list_len = 5;
let max_visible = 5;
for selected in 0..list_len {
assert_eq!(calculate_scroll_offset(selected, max_visible), 0);
}
}
#[test]
fn test_scroll_handles_large_list() {
let max_visible = 10;
let offset = calculate_scroll_offset(50, max_visible);
assert_eq!(offset, 41);
let offset = calculate_scroll_offset(100, max_visible);
assert_eq!(offset, 91); }
#[test]
fn test_palette_state_filter_updates() {
let mut state = PaletteState::default();
state.update_filter();
assert_eq!(state.filtered.len(), PaletteState::COMMANDS.len());
state.query = "Toggle".to_string();
state.update_filter();
assert!(state.filtered.len() >= 2);
state.query = "Quit".to_string();
state.update_filter();
assert!(!state.filtered.is_empty());
}
#[test]
fn test_palette_state_navigation() {
let mut state = PaletteState::default();
state.update_filter();
assert_eq!(state.selected, 0);
state.select_next();
assert_eq!(state.selected, 1);
state.select_prev();
assert_eq!(state.selected, 0);
state.select_prev();
assert_eq!(state.selected, 0);
let last_idx = state.filtered.len() - 1;
for _ in 0..last_idx {
state.select_next();
}
assert_eq!(state.selected, last_idx);
state.select_next();
assert_eq!(state.selected, last_idx);
}
#[test]
fn test_frame_modulo_prevents_overflow() {
const FRAME_CYCLE: u64 = 10_000_000;
for frame in [
0,
1,
FRAME_CYCLE - 1,
FRAME_CYCLE,
FRAME_CYCLE + 1,
u64::MAX,
] {
let m = content::Metrics::compute(frame, 60);
assert!(m.fps >= 1, "FPS should be >= 1 at frame {frame}");
assert!(m.fps <= 120, "FPS should be <= 120 at frame {frame}");
}
}
#[test]
fn test_fps_variation_bounded() {
for frame in 0..1000 {
let m = content::Metrics::compute(frame, 60);
assert!(
m.fps >= 1 && m.fps <= 120,
"FPS {} out of bounds at frame {}",
m.fps,
frame
);
}
}
#[test]
fn test_fps_clamp_prevents_negative() {
let m = content::Metrics::compute(0, 1);
assert!(m.fps >= 1, "FPS should never be less than 1");
let m = content::Metrics::compute(0, 0);
assert!(m.fps >= 1, "FPS should be >= 1 even with target_fps=0");
}
#[test]
fn test_frame_time_no_div_by_zero() {
for target_fps in [0, 1, 30, 60, 120] {
for frame in [0, 1, 100, 1000] {
let m = content::Metrics::compute(frame, target_fps);
assert!(
m.frame_time_ms.is_finite(),
"frame_time_ms should be finite at frame={frame}, target_fps={target_fps}"
);
assert!(
m.frame_time_ms > 0.0,
"frame_time_ms should be positive at frame={frame}, target_fps={target_fps}"
);
}
}
}
#[test]
fn test_cast_after_clamp() {
for frame in 0..200 {
let m = content::Metrics::compute(frame, 60);
assert!(m.fps >= 1);
assert!(m.fps <= 120);
assert!(m.cpu_percent <= 100);
}
}
#[test]
fn test_metrics_at_frame_zero() {
let m = content::Metrics::compute(0, 60);
assert_eq!(m.fps, 60);
assert!((m.frame_time_ms - 1000.0 / 60.0).abs() < 0.01);
assert_eq!(m.cpu_percent, 15);
assert_eq!(m.memory_bytes, 50_000_000);
assert!((m.pulse - 0.0).abs() < 0.01);
assert_eq!(m.cells_changed, 1920);
assert_eq!(m.bytes_written, 1920 * 8 + 100);
}
#[test]
fn test_metrics_at_frame_max_u64() {
let m = content::Metrics::compute(u64::MAX, 60);
assert!(m.fps >= 1 && m.fps <= 120);
assert!(m.frame_time_ms.is_finite() && m.frame_time_ms > 0.0);
assert!(m.cpu_percent <= 100);
assert!(m.pulse >= 0.0 && m.pulse <= 1.0);
assert!(m.cells_changed <= 500);
}
#[test]
fn test_metrics_frame_cycle_boundary() {
const FRAME_CYCLE: u64 = 10_000_000;
let m_zero = content::Metrics::compute(0, 60);
let m_cycle = content::Metrics::compute(FRAME_CYCLE, 60);
assert_eq!(m_zero.fps, m_cycle.fps);
assert_eq!(m_zero.cpu_percent, m_cycle.cpu_percent);
assert_eq!(m_zero.memory_bytes, m_cycle.memory_bytes);
}
#[test]
fn test_metrics_pulse_range() {
for frame in 0..120 {
let m = content::Metrics::compute(frame, 60);
assert!(
m.pulse >= -0.01 && m.pulse <= 1.01,
"Pulse {} out of range at frame {}",
m.pulse,
frame
);
}
}
#[test]
fn test_metrics_cpu_bounded() {
for frame in 0..1000 {
let m = content::Metrics::compute(frame, 60);
assert!(
m.cpu_percent <= 100,
"CPU {}% out of bounds at frame {}",
m.cpu_percent,
frame
);
}
}
#[test]
fn test_metrics_cells_changed_bounded() {
let m0 = content::Metrics::compute(0, 60);
assert_eq!(m0.cells_changed, 1920);
for frame in 1..200 {
let m = content::Metrics::compute(frame, 60);
assert!(
m.cells_changed <= 500,
"cells_changed {} exceeds max at frame {}",
m.cells_changed,
frame
);
assert!(
m.cells_changed >= 50,
"cells_changed {} below min at frame {}",
m.cells_changed,
frame
);
}
}
#[test]
fn test_metrics_memory_grows_cyclically() {
let m0 = content::Metrics::compute(0, 60);
let m999 = content::Metrics::compute(999, 60);
let m1000 = content::Metrics::compute(1000, 60);
assert_eq!(m0.memory_bytes, m1000.memory_bytes);
assert_eq!(m999.memory_bytes, 50_000_000 + 999 * 10_000);
}
fn buffer_with_row(width: u32, height: u32, chars: &[char]) -> OptimizedBuffer {
let mut buf = OptimizedBuffer::new(width, height);
let style = Style::default();
for (i, &ch) in chars.iter().enumerate() {
if let Ok(x) = u32::try_from(i) {
if x < width {
buf.set(x, 0, Cell::new(ch, style));
}
}
}
buf
}
#[test]
fn test_extract_normal_range() {
let buf = buffer_with_row(10, 1, &['H', 'e', 'l', 'l', 'o']);
let result = extract_buffer_row(&buf, 0, 0, 10);
assert_eq!(result, "Hello");
}
#[test]
fn test_extract_with_start_offset() {
let buf = buffer_with_row(10, 1, &['A', 'B', 'C', 'D', 'E']);
let result = extract_buffer_row(&buf, 0, 2, 3);
assert_eq!(result, "CDE");
}
#[test]
fn test_extract_empty_buffer() {
let buf = OptimizedBuffer::new(10, 1);
let result = extract_buffer_row(&buf, 0, 0, 10);
assert_eq!(result, "");
}
#[test]
fn test_extract_saturating_at_max() {
let buf = OptimizedBuffer::new(10, 1);
let result = extract_buffer_row(&buf, 0, u32::MAX - 10, 20);
assert_eq!(result, "");
}
#[test]
fn test_extract_start_at_u32_max() {
let buf = OptimizedBuffer::new(10, 1);
let result = extract_buffer_row(&buf, 0, u32::MAX, 1);
assert_eq!(result, "");
}
#[test]
fn test_extract_both_max() {
let buf = OptimizedBuffer::new(10, 1);
let result = extract_buffer_row(&buf, 0, u32::MAX, u32::MAX);
assert_eq!(result, "");
}
#[test]
fn test_extract_at_buffer_edge() {
let buf = buffer_with_row(5, 1, &['A', 'B', 'C', 'D', 'E']);
let result = extract_buffer_row(&buf, 0, 3, 10);
assert_eq!(result, "DE");
}
#[test]
fn test_extract_start_beyond_buffer() {
let buf = buffer_with_row(5, 1, &['A', 'B', 'C']);
let result = extract_buffer_row(&buf, 0, 100, 10);
assert_eq!(result, "");
}
#[test]
fn test_extract_zero_max_len() {
let buf = buffer_with_row(10, 1, &['A', 'B', 'C']);
let result = extract_buffer_row(&buf, 0, 0, 0);
assert_eq!(result, "");
}
#[test]
fn test_extract_trims_trailing_spaces() {
let mut buf = OptimizedBuffer::new(10, 1);
let style = Style::default();
buf.set(0, 0, Cell::new('A', style));
buf.set(1, 0, Cell::new(' ', style));
buf.set(2, 0, Cell::new(' ', style));
let result = extract_buffer_row(&buf, 0, 0, 10);
assert_eq!(result, "A");
}
#[test]
fn test_extract_continuation_cells_skipped() {
let buf = OptimizedBuffer::new(5, 1);
let result = extract_buffer_row(&buf, 0, 0, 5);
assert_eq!(result, "");
}
#[test]
fn test_extract_row_out_of_bounds_y() {
let buf = buffer_with_row(10, 2, &['A', 'B']);
let result = extract_buffer_row(&buf, 99, 0, 10);
assert_eq!(result, "");
}
#[test]
fn test_extract_exact_buffer_width() {
let buf = buffer_with_row(5, 1, &['X', 'Y', 'Z', 'W', 'V']);
let result = extract_buffer_row(&buf, 0, 0, 5);
assert_eq!(result, "XYZWV");
}
#[test]
fn test_extract_max_len_one() {
let buf = buffer_with_row(10, 1, &['A', 'B', 'C']);
let result = extract_buffer_row(&buf, 0, 1, 1);
assert_eq!(result, "B");
}
#[test]
fn test_layout_mode_transitions() {
assert_eq!(LayoutMode::from_size(120, 40), LayoutMode::Full);
assert_eq!(LayoutMode::from_size(80, 24), LayoutMode::Full);
assert_eq!(LayoutMode::from_size(79, 24), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(60, 16), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(59, 16), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(40, 12), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(39, 12), LayoutMode::TooSmall);
assert_eq!(LayoutMode::from_size(20, 8), LayoutMode::TooSmall);
}
#[test]
fn test_layout_boundary_width_only() {
assert_eq!(LayoutMode::from_size(80, 40), LayoutMode::Full);
assert_eq!(LayoutMode::from_size(79, 40), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(60, 40), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(59, 40), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(40, 40), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(39, 40), LayoutMode::TooSmall);
}
#[test]
fn test_layout_boundary_height_only() {
assert_eq!(LayoutMode::from_size(200, 24), LayoutMode::Full);
assert_eq!(LayoutMode::from_size(200, 23), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(200, 16), LayoutMode::Compact);
assert_eq!(LayoutMode::from_size(200, 15), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(200, 12), LayoutMode::Minimal);
assert_eq!(LayoutMode::from_size(200, 11), LayoutMode::TooSmall);
}
#[test]
fn test_layout_rects_in_bounds() {
for &(w, h) in &[(120, 40), (80, 24), (60, 16)] {
let layout = PanelLayout::compute(w, h);
let rects = [
("top_bar", &layout.top_bar),
("status_bar", &layout.status_bar),
("content", &layout.content),
("sidebar", &layout.sidebar),
("main_area", &layout.main_area),
("editor", &layout.editor),
("preview", &layout.preview),
("logs", &layout.logs),
];
for (name, rect) in rects {
if rect.w > 0 && rect.h > 0 {
#[allow(clippy::cast_possible_wrap)]
let within = rect.x.saturating_add(rect.w as i32) <= w as i32
&& rect.y.saturating_add(rect.h as i32) <= h as i32;
assert!(
within,
"Rect {name} out of bounds at {w}x{h}: x={}, y={}, w={}, h={}",
rect.x, rect.y, rect.w, rect.h
);
}
}
}
}
#[test]
fn test_layout_json_valid() {
let json = run_check_layout(120, 40);
assert!(json.contains(r#""check":"layout""#));
assert!(json.contains(r#""passed":true"#));
assert!(json.contains(r#""requested_size":[120,40]"#));
assert!(json.contains(r#""test_results":["#));
}
#[test]
fn test_layout_json_small_size() {
let json = run_check_layout(20, 8);
assert!(json.contains(r#""check":"layout""#));
assert!(json.contains(r#""passed":true"#));
}
#[test]
fn test_config_parse_valid_args() {
let result = Config::from_args(args(&["demo_showcase"]));
let ParseResult::Config(cfg) = result else {
unreachable!("Expected Config for default args");
};
assert_eq!(cfg.fps_cap, 60);
assert!(cfg.enable_mouse);
let result = Config::from_args(args(&["demo_showcase", "--fps", "30"]));
let ParseResult::Config(cfg) = result else {
unreachable!("Expected Config for --fps 30");
};
assert_eq!(cfg.fps_cap, 30);
let result = Config::from_args(args(&["demo_showcase", "--no-mouse"]));
let ParseResult::Config(cfg) = result else {
unreachable!("Expected Config for --no-mouse");
};
assert!(!cfg.enable_mouse);
let result = Config::from_args(args(&["demo_showcase", "--seed", "42"]));
let ParseResult::Config(cfg) = result else {
unreachable!("Expected Config for --seed 42");
};
assert_eq!(cfg.seed, 42);
}
#[test]
fn test_config_parse_error_cases() {
let result = Config::from_args(args(&["demo_showcase", "--fps", "abc"]));
assert!(matches!(result, ParseResult::Error(_)));
let result = Config::from_args(args(&["demo_showcase", "--fps"]));
assert!(matches!(result, ParseResult::Error(_)));
}
#[test]
fn test_config_json_valid() {
let cfg = Config::default();
let json = run_check_config(&cfg);
assert!(json.contains(r#""check":"config""#));
assert!(json.contains(r#""passed":true"#));
assert!(json.contains(r#""test_results":["#));
assert!(json.contains(r#""label":"default""#));
}
#[test]
fn test_palette_filter_all_on_empty() {
let mut state = PaletteState::default();
state.update_filter();
assert_eq!(
state.filtered.len(),
PaletteState::COMMANDS.len(),
"Empty query should show all commands"
);
}
#[test]
fn test_palette_filter_help() {
let mut state = PaletteState {
query: "help".to_string(),
..PaletteState::default()
};
state.update_filter();
assert!(
!state.filtered.is_empty(),
"'help' query should match at least one command"
);
}
#[test]
fn test_palette_filter_no_match() {
let mut state = PaletteState {
query: "xyznonexistent".to_string(),
..PaletteState::default()
};
state.update_filter();
assert!(
state.filtered.is_empty(),
"Nonsense query should match no commands"
);
}
#[test]
fn test_palette_selection_navigation() {
let mut state = PaletteState::default();
state.update_filter(); assert_eq!(state.selected, 0);
state.select_next();
assert_eq!(state.selected, 1);
state.select_next();
assert_eq!(state.selected, 2);
state.select_next();
assert_eq!(state.selected, 3);
state.select_prev();
assert_eq!(state.selected, 2);
state.select_prev();
assert_eq!(state.selected, 1);
}
#[test]
fn test_palette_boundary_selection() {
let mut state = PaletteState::default();
state.update_filter();
state.select_prev();
assert_eq!(state.selected, 0);
for _ in 0..100 {
state.select_next();
}
let max = state.filtered.len().saturating_sub(1);
assert_eq!(state.selected, max);
state.select_next();
assert_eq!(state.selected, max);
}
#[test]
fn test_palette_json_valid() {
let json = run_check_palette();
assert!(json.contains(r#""check":"palette""#));
assert!(json.contains(r#""passed":true"#));
assert!(json.contains(r#""total_commands":"#));
assert!(json.contains(r#""filter_tests":["#));
}
#[test]
fn test_hitgrid_id_ranges() {
const { assert!(hit_ids::BTN_HELP >= 1000 && hit_ids::BTN_HELP < 2000) };
const { assert!(hit_ids::BTN_PALETTE >= 1000 && hit_ids::BTN_PALETTE < 2000) };
const { assert!(hit_ids::BTN_TOUR >= 1000 && hit_ids::BTN_TOUR < 2000) };
const { assert!(hit_ids::BTN_THEME >= 1000 && hit_ids::BTN_THEME < 2000) };
const { assert!(hit_ids::SIDEBAR_ROW_BASE >= 2000 && hit_ids::SIDEBAR_ROW_BASE < 3000) };
const { assert!(hit_ids::PANEL_SIDEBAR >= 3000 && hit_ids::PANEL_SIDEBAR < 4000) };
const { assert!(hit_ids::PANEL_EDITOR >= 3000 && hit_ids::PANEL_EDITOR < 4000) };
const { assert!(hit_ids::PANEL_PREVIEW >= 3000 && hit_ids::PANEL_PREVIEW < 4000) };
const { assert!(hit_ids::PANEL_LOGS >= 3000 && hit_ids::PANEL_LOGS < 4000) };
const { assert!(hit_ids::OVERLAY_CLOSE >= 4000) };
const { assert!(hit_ids::PALETTE_ITEM_BASE >= 4000) };
}
#[test]
fn test_hitgrid_no_overlap() {
let ids = [
hit_ids::BTN_HELP,
hit_ids::BTN_PALETTE,
hit_ids::BTN_TOUR,
hit_ids::BTN_THEME,
hit_ids::SIDEBAR_ROW_BASE,
hit_ids::PANEL_SIDEBAR,
hit_ids::PANEL_EDITOR,
hit_ids::PANEL_PREVIEW,
hit_ids::PANEL_LOGS,
hit_ids::OVERLAY_CLOSE,
hit_ids::PALETTE_ITEM_BASE,
];
for i in 0..ids.len() {
for j in (i + 1)..ids.len() {
assert_ne!(ids[i], ids[j], "Hit IDs at index {i} and {j} overlap");
}
}
}
#[test]
fn test_hitgrid_json_valid() {
let json = run_check_hitgrid();
assert!(json.contains(r#""check":"hitgrid""#));
assert!(json.contains(r#""passed":true"#));
assert!(json.contains(r#""id_tests":["#));
}
#[test]
fn test_logs_ring_buffer() {
let max_entries = 100;
let mut log_buffer: VecDeque<&str> = VecDeque::with_capacity(max_entries);
for i in 0..150 {
if log_buffer.len() >= max_entries {
log_buffer.pop_front();
}
log_buffer.push_back(if i % 2 == 0 { "INFO" } else { "DEBUG" });
}
assert_eq!(log_buffer.len(), max_entries);
assert_eq!(log_buffer.front(), Some(&"INFO"));
}
#[test]
fn test_logs_selection_bounds() {
let final_count = 100_usize;
let max_sel = final_count.saturating_sub(1);
let mut selection = 0_usize;
selection = selection.saturating_add(5).min(max_sel);
assert_eq!(selection, 5);
selection = selection.saturating_sub(3);
assert_eq!(selection, 2);
}
#[test]
fn test_logs_selection_at_boundaries() {
let final_count = 100_usize;
let max_sel = final_count.saturating_sub(1);
let mut selection = 0_usize;
selection = selection.saturating_sub(1);
assert_eq!(selection, 0);
selection = max_sel;
selection = selection.saturating_add(5).min(max_sel);
assert_eq!(selection, max_sel);
}
#[test]
fn test_logs_json_valid() {
let json = run_check_logs();
assert!(json.contains(r#""check":"logs""#));
assert!(json.contains(r#""passed":true"#));
assert!(json.contains(r#""ring_buffer":"#));
assert!(json.contains(r#""oldest_dropped":true"#));
assert!(json.contains(r#""selection":"#));
}
}