pub const API_VERSION_MAJOR: u32 = 0;
pub const API_VERSION_MINOR: u32 = 1;
pub const API_VERSION_PATCH: u32 = 0;
#[macro_export]
macro_rules! require_api {
($major:literal, $minor:literal) => {
const _: () = {
if $crate::API_VERSION_MAJOR == 0 {
assert!(
$major == $crate::API_VERSION_MAJOR && $minor == $crate::API_VERSION_MINOR,
concat!(
"Telex API version mismatch: this code requires ", $major, ".", $minor,
" but the library is version ",
env!("CARGO_PKG_VERSION"),
". See https://docs.rs/telex for migration guides."
)
);
} else {
assert!(
$major == $crate::API_VERSION_MAJOR,
concat!(
"Telex API major version mismatch: this code requires major version ", $major,
" but the library is version ",
env!("CARGO_PKG_VERSION"),
". This is a breaking change - see https://docs.rs/telex for migration guides."
)
);
assert!(
$minor <= $crate::API_VERSION_MINOR,
concat!(
"Telex API minor version too new: this code requires ", $major, ".", $minor,
" but the library is version ",
env!("CARGO_PKG_VERSION"),
". Please upgrade the telex dependency in your Cargo.toml."
)
);
}
};
};
}
mod async_state;
mod buffer;
pub mod canvas;
mod command;
pub mod command_system;
mod component;
mod context;
mod focus;
pub mod form;
pub mod image;
pub mod markdown;
mod render;
mod scope;
mod state;
mod stream_state;
mod terminal;
mod terminal_state;
pub mod testing;
pub mod text;
pub mod theme;
pub mod toast;
mod view;
pub mod prelude;
pub use async_state::Async;
pub use command::KeyBinding;
pub use component::Component;
pub use scope::Scope;
pub use state::State;
pub use stream_state::{StreamHandle, StreamState, TextStreamHandle};
pub use telex_macro::{effect, effect_once, state, view, with};
pub use terminal::Terminal;
pub use terminal_state::{TerminalBuffer, TerminalHandle};
pub use view::{
Align, BoxBuilder, BoxNode, ButtonBuilder, ButtonNode, Callback, CanvasBuilder, CanvasNode,
ChangeCallback, CheckboxBuilder, CheckboxNode, ColumnWidth, CommandCallback,
CommandPaletteBuilder, CommandPaletteNode, FormBuilder, FormFieldBuilder, FormFieldNode,
FormNode, FormSubmitCallback, HStackBuilder, HStackNode, ImageBuilder, ImageNode, Justify,
LayoutMode, ListBuilder, ListNode, Menu, MenuBarBuilder, MenuBarNode, MenuItemNode,
ModalBuilder, ModalNode, Orientation, PaletteCommand, RadioGroupBuilder, RadioGroupNode,
SelectCallback, SpacerNode, SplitBuilder, SplitNode, TabPosition, TableBuilder, TableColumn,
TableNode, TabsBuilder, TabsNode, TextAlign, TextAreaBuilder, TextAreaNode, TextBuilder,
TextInputBuilder, TextInputNode, TextNode, TerminalBuilder, TerminalNode,
ToastContainerBuilder, ToastContainerNode, ToastItem, ToastLevelView, ToastPosition,
ToggleCallback, TreeActivateCallback, TreeBuilder, TreeItem, TreeNode, TreePath,
TreeSelectCallback, VStackBuilder, VStackNode, View,
};
pub use canvas::{animated_canvas, AnimatedCanvasBuilder, DrawContext, PixelBuffer};
pub use image::ImageSource;
pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
pub use crossterm::style::Color;
use command::CommandRegistry;
use context::ContextStorage;
use focus::FocusManager;
use scope::StateStorage;
use std::io::Result;
use std::panic;
use std::rc::Rc;
use theme::Theme;
fn has_visible_modal(view: &View) -> bool {
match view {
View::Modal(node) => node.visible,
View::VStack(node) => node.children.iter().any(has_visible_modal),
View::HStack(node) => node.children.iter().any(has_visible_modal),
View::Box(node) => node
.child
.as_ref()
.map(|c| has_visible_modal(c))
.unwrap_or(false),
View::Split(node) => has_visible_modal(&node.first) || has_visible_modal(&node.second),
View::Tabs(node) => node.children.iter().any(has_visible_modal),
_ => false,
}
}
fn has_visible_command_palette(view: &View) -> bool {
match view {
View::CommandPalette(node) => node.visible,
View::VStack(node) => node.children.iter().any(has_visible_command_palette),
View::HStack(node) => node.children.iter().any(has_visible_command_palette),
View::Box(node) => node
.child
.as_ref()
.map(|c| has_visible_command_palette(c))
.unwrap_or(false),
View::Split(node) => {
has_visible_command_palette(&node.first) || has_visible_command_palette(&node.second)
}
View::Tabs(node) => node.children.iter().any(has_visible_command_palette),
_ => false,
}
}
fn call_command_palette_dismiss(view: &View) {
match view {
View::CommandPalette(node) => {
if node.visible {
if let Some(callback) = &node.on_dismiss {
callback();
}
}
}
View::VStack(node) => {
for child in &node.children {
call_command_palette_dismiss(child);
}
}
View::HStack(node) => {
for child in &node.children {
call_command_palette_dismiss(child);
}
}
View::Box(node) => {
if let Some(child) = &node.child {
call_command_palette_dismiss(child);
}
}
View::Split(node) => {
call_command_palette_dismiss(&node.first);
call_command_palette_dismiss(&node.second);
}
View::Tabs(node) => {
for child in &node.children {
call_command_palette_dismiss(child);
}
}
_ => {}
}
}
fn call_modal_dismiss(view: &View) {
match view {
View::Modal(node) => {
if node.visible {
if let Some(callback) = &node.on_dismiss {
callback();
}
}
}
View::VStack(node) => {
for child in &node.children {
call_modal_dismiss(child);
}
}
View::HStack(node) => {
for child in &node.children {
call_modal_dismiss(child);
}
}
View::Box(node) => {
if let Some(child) = &node.child {
call_modal_dismiss(child);
}
}
View::Split(node) => {
call_modal_dismiss(&node.first);
call_modal_dismiss(&node.second);
}
View::Tabs(node) => {
for child in &node.children {
call_modal_dismiss(child);
}
}
_ => {}
}
}
pub fn is_debug_mode() -> bool {
std::env::var("TELEX_DEBUG")
.map(|v| v == "1" || v == "true")
.unwrap_or(false)
}
pub fn run_with_theme<C: Component>(root: C, theme: Theme) -> Result<()> {
theme::set_theme(theme);
run(root)
}
pub fn run<C: Component>(root: C) -> Result<()> {
let default_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!(
std::io::stdout(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::cursor::Show
);
eprintln!("\n┌─ Telex Panic ─────────────────────────────────────────────────┐");
eprintln!("│ │");
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
for line in message.lines() {
let chunks: Vec<&str> = line
.as_bytes()
.chunks(58)
.map(|c| std::str::from_utf8(c).unwrap_or(""))
.collect();
for chunk in chunks {
eprintln!("│ {:<58}│", chunk);
}
}
eprintln!("│ │");
if let Some(location) = panic_info.location() {
eprintln!(
"│ Location: {}:{}:{:<25}│",
location.file().split('/').next_back().unwrap_or(location.file()),
location.line(),
location.column()
);
}
eprintln!("│ │");
eprintln!("│ Tip: Check your hook order - hooks must be called │");
eprintln!("│ unconditionally in the same order every render. │");
eprintln!("│ │");
eprintln!("└──────────────────────────────────────────────────────────────┘\n");
default_hook(panic_info);
}));
let mut terminal = Terminal::new()?;
let mut focus = FocusManager::new();
let storage = Rc::new(StateStorage::new());
let commands = Rc::new(CommandRegistry::new());
let context = Rc::new(ContextStorage::new());
let debug_mode = is_debug_mode();
let mut frame_count = 0u64;
loop {
let render_start = std::time::Instant::now();
storage.decay_effect_counter();
focus.poll_terminals();
commands.clear();
let cx = Scope::with_all(
Rc::clone(&storage),
Rc::clone(&commands),
Rc::clone(&context),
);
let view = root.render(cx);
focus.collect_focusables(&view);
if let Ok((term_width, _)) = crossterm::terminal::size() {
focus.set_default_textarea_wrap_width(term_width.saturating_sub(2));
}
let render_time = render_start.elapsed();
frame_count += 1;
let scroll_offsets: Vec<(u16, u16)> = (0..focus.focus_index() + 10)
.map(|i| focus.scroll_offset(i))
.collect();
let cursor_offsets: Vec<usize> = (0..focus.focus_index() + 10)
.map(|i| focus.cursor_offset(i))
.collect();
let modal_visible = has_visible_modal(&view);
let clamped_offsets = terminal.draw(
&view,
focus.focus_index(),
focus.is_focus_visible(),
scroll_offsets,
cursor_offsets,
modal_visible,
)?;
focus.update_scroll_states(&clamped_offsets);
if debug_mode {
terminal.draw_debug(
frame_count,
render_time.as_micros() as u64,
focus.focus_index(),
focus.focusable_count(),
)?;
}
if storage.flush_effects() {
storage.reset_index();
let cx = Scope::with_all(
Rc::clone(&storage),
Rc::clone(&commands),
Rc::clone(&context),
);
let view = root.render(cx);
focus.collect_focusables(&view);
let scroll_offsets: Vec<(u16, u16)> = (0..focus.focus_index() + 10)
.map(|i| focus.scroll_offset(i))
.collect();
let cursor_offsets: Vec<usize> = (0..focus.focus_index() + 10)
.map(|i| focus.cursor_offset(i))
.collect();
let modal_visible = has_visible_modal(&view);
let clamped_offsets = terminal.draw(
&view,
focus.focus_index(),
focus.is_focus_visible(),
scroll_offsets,
cursor_offsets,
modal_visible,
)?;
focus.update_scroll_states(&clamped_offsets);
}
let max_scroll = 100u16;
let viewport_height = terminal.height().saturating_sub(6);
if let Some(event) = terminal.poll_event()? {
if let Event::Resize(_, _) = event {
continue;
}
if let Event::Key(key) = event {
let modal_visible = has_visible_modal(&view);
let palette_visible = has_visible_command_palette(&view);
if modal_visible && key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
{
call_modal_dismiss(&view);
continue;
}
if palette_visible {
match (key.modifiers, key.code) {
(KeyModifiers::NONE, KeyCode::Esc) => {
call_command_palette_dismiss(&view);
}
(KeyModifiers::NONE, KeyCode::Enter) => {
if focus.is_focused_command_palette() {
focus.command_palette_execute();
}
}
(KeyModifiers::NONE, KeyCode::Up) => {
}
(KeyModifiers::NONE, KeyCode::Down) => {
}
(KeyModifiers::NONE, KeyCode::Backspace) => {
if focus.is_focused_command_palette() {
focus.command_palette_backspace();
}
}
(KeyModifiers::NONE, KeyCode::Char(c)) => {
if focus.is_focused_command_palette() {
focus.command_palette_key(c);
}
}
(KeyModifiers::SHIFT, KeyCode::Char(c)) => {
if focus.is_focused_command_palette() {
focus.command_palette_key(c.to_ascii_uppercase());
}
}
_ => {}
}
continue;
}
if key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE
&& focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
focus.menu_bar_close();
continue;
}
if commands.execute(key.code, key.modifiers) {
continue;
}
match (key.modifiers, key.code) {
(m, KeyCode::Char('q')) if m.contains(KeyModifiers::CONTROL) => {
break;
}
(m, KeyCode::Char('['))
if m.contains(KeyModifiers::CONTROL)
&& m.contains(KeyModifiers::SHIFT) =>
{
if focus.is_focused_terminal() {
focus.focus_next();
}
}
_ if focus.is_focused_terminal() => {
if let Err(e) = focus.terminal_key(key) {
eprintln!("Terminal input error: {}", e);
}
}
(KeyModifiers::NONE, KeyCode::Tab) => {
focus.focus_next();
}
(KeyModifiers::SHIFT, KeyCode::BackTab) => {
focus.focus_prev();
}
(KeyModifiers::NONE, KeyCode::Enter | KeyCode::Char(' ')) => {
if focus.is_focused_text_area() {
if key.code == KeyCode::Enter {
focus.text_area_enter();
} else {
focus.text_area_key(' ');
}
} else if focus.is_focused_text_input() {
if key.code == KeyCode::Enter {
focus.text_input_submit();
} else {
focus.text_input_key(' ');
}
} else if focus.is_focused_tree() {
focus.tree_activate();
} else if focus.is_focused_table() {
focus.table_activate();
} else if focus.is_focused_menu_bar() {
if focus.menu_bar_has_open_menu() {
focus.menu_bar_execute();
} else {
focus.menu_bar_open();
}
} else {
focus.activate();
}
}
(KeyModifiers::NONE, KeyCode::Backspace) => {
if focus.is_focused_text_input() {
focus.text_input_backspace();
} else if focus.is_focused_text_area() {
focus.text_area_backspace();
} else if focus.is_focused_form_field() {
focus.form_field_backspace();
}
}
(KeyModifiers::NONE, KeyCode::Up) => {
if focus.is_focused_text_input() {
focus.text_input_key_up();
} else if focus.is_focused_text_area() {
focus.text_area_cursor_up();
} else if focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
focus.menu_bar_select_prev();
} else if focus.is_focused_scrollable() {
if focus.is_focused_auto_scroll_bottom() {
focus.scroll_down(1, max_scroll);
} else {
focus.scroll_up(1);
}
} else if focus.is_focused_list() {
focus.list_select_prev();
} else if focus.is_focused_tree() {
focus.tree_select_prev();
} else if focus.is_focused_table() {
focus.table_select_prev();
} else if focus.is_focused_radio_group() {
focus.radio_group_select_prev();
}
}
(KeyModifiers::NONE, KeyCode::Down) => {
if focus.is_focused_text_input() {
focus.text_input_key_down();
} else if focus.is_focused_text_area() {
focus.text_area_cursor_down();
} else if focus.is_focused_menu_bar() && focus.menu_bar_has_open_menu() {
focus.menu_bar_select_next();
} else if focus.is_focused_scrollable() {
if focus.is_focused_auto_scroll_bottom() {
focus.scroll_up(1);
} else {
focus.scroll_down(1, max_scroll);
}
} else if focus.is_focused_list() {
focus.list_select_next();
} else if focus.is_focused_tree() {
focus.tree_select_next();
} else if focus.is_focused_table() {
focus.table_select_next();
} else if focus.is_focused_radio_group() {
focus.radio_group_select_next();
}
}
(KeyModifiers::NONE, KeyCode::PageUp) => {
if focus.is_focused_scrollable() {
if focus.is_focused_auto_scroll_bottom() {
focus.scroll_down(viewport_height, max_scroll);
} else {
focus.scroll_up(viewport_height);
}
}
}
(KeyModifiers::NONE, KeyCode::PageDown) => {
if focus.is_focused_scrollable() {
if focus.is_focused_auto_scroll_bottom() {
focus.scroll_up(viewport_height);
} else {
focus.scroll_down(viewport_height, max_scroll);
}
}
}
(KeyModifiers::NONE, KeyCode::Home) => {
if focus.is_focused_scrollable() {
if focus.is_focused_auto_scroll_bottom() {
focus.scroll_end(max_scroll);
} else {
focus.scroll_home();
}
}
}
(KeyModifiers::NONE, KeyCode::End) => {
if focus.is_focused_scrollable() {
if focus.is_focused_auto_scroll_bottom() {
focus.scroll_home();
} else {
focus.scroll_end(max_scroll);
}
}
}
(KeyModifiers::NONE, KeyCode::Left) => {
if focus.is_focused_text_input() {
focus.text_input_cursor_left();
} else if focus.is_focused_text_area() {
focus.text_area_cursor_left();
} else if focus.is_focused_menu_bar() {
if focus.menu_bar_has_open_menu() {
focus.menu_bar_prev();
} else {
focus.menu_bar_highlight_prev();
}
} else if focus.is_focused_tabs() {
focus.tabs_select_prev();
} else if focus.is_focused_tree() {
focus.tree_activate();
}
}
(KeyModifiers::NONE, KeyCode::Right) => {
if focus.is_focused_text_input() {
focus.text_input_cursor_right();
} else if focus.is_focused_text_area() {
focus.text_area_cursor_right();
} else if focus.is_focused_menu_bar() {
if focus.menu_bar_has_open_menu() {
focus.menu_bar_next();
} else {
focus.menu_bar_highlight_next();
}
} else if focus.is_focused_tabs() {
focus.tabs_select_next();
} else if focus.is_focused_tree() {
focus.tree_activate();
}
}
(KeyModifiers::NONE, KeyCode::Char(c)) => {
if focus.is_focused_text_input() {
focus.text_input_key(c);
} else if focus.is_focused_text_area() {
focus.text_area_key(c);
} else if focus.is_focused_form_field() {
focus.form_field_key(c);
} else if focus.is_focused_tabs() {
match c {
'[' => focus.tabs_select_prev(),
']' => focus.tabs_select_next(),
'1'..='9' => {
let idx = (c as usize) - ('1' as usize);
focus.tabs_select(idx);
}
_ => {}
}
} else if focus.is_focused_tree() {
match c {
'j' => focus.tree_select_next(),
'k' => focus.tree_select_prev(),
' ' => focus.tree_activate(),
_ => {}
}
} else if focus.is_focused_table() {
match c {
'j' => focus.table_select_next(),
'k' => focus.table_select_prev(),
_ => {}
}
} else if focus.is_focused_radio_group() {
match c {
'j' => focus.radio_group_select_next(),
'k' => focus.radio_group_select_prev(),
_ => {}
}
}
}
(KeyModifiers::SHIFT, KeyCode::Char(c)) => {
if focus.is_focused_text_input() {
focus.text_input_key(c.to_ascii_uppercase());
} else if focus.is_focused_text_area() {
focus.text_area_key(c.to_ascii_uppercase());
} else if focus.is_focused_form_field() {
focus.form_field_key(c.to_ascii_uppercase());
}
}
_ => {}
}
}
}
}
storage.cleanup_all_effects();
terminal.cleanup()?;
Ok(())
}