Large structural changes
This commit is contained in:
+129
-145
@@ -1,163 +1,147 @@
|
||||
use crossterm::ExecutableCommand;
|
||||
//! # OSUI
|
||||
//!
|
||||
//! A terminal user interface (TUI) library providing customizable components
|
||||
//! to build command-line interfaces in Rust. OSUI enables users to create
|
||||
//! interactive CLI applications with various UI elements and handle keyboard
|
||||
//! input for real-time updates.
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! use osui::{parse_rsx_param, rsx, ui::*};
|
||||
//!
|
||||
//! osui::app::run(&mut rsx! {
|
||||
//! text { "Hello, World!" }
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! ## Modules
|
||||
//! - `element` - Defines base elements for constructing UI components.
|
||||
//! - `key` - Handles keyboard input, providing key event management.
|
||||
//! - `utils` - Utility functions for common TUI tasks such as clearing the screen.
|
||||
//! - `ui` - Contains all user interface components, enabling rich CLI experiences.
|
||||
|
||||
pub mod element;
|
||||
pub mod key;
|
||||
pub mod macros;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
pub trait Element: std::fmt::Debug {
|
||||
fn get_child(&mut self) -> Option<&mut Box<dyn Element>>;
|
||||
fn get_data(&self) -> ElementData;
|
||||
fn set_data(&mut self, _: ElementData);
|
||||
fn clear_ticks(&mut self);
|
||||
fn render(&mut self, _tick: usize) -> String {
|
||||
String::new()
|
||||
}
|
||||
fn update(&mut self, _ctx: &mut UpdateContext) {}
|
||||
}
|
||||
pub use element::*;
|
||||
pub use utils::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ElementData {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub style: ui::styles::Style,
|
||||
}
|
||||
pub mod app {
|
||||
//! Application entry point and main event loop for OSUI.
|
||||
//!
|
||||
//! Provides functions to render and update UI elements based on keyboard
|
||||
//! input. Manages cursor visibility, terminal size, and controls UI behavior
|
||||
//! using custom commands such as rendering, updating, or exiting.
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum UpdateResponse {
|
||||
Exit,
|
||||
Done,
|
||||
None,
|
||||
}
|
||||
use crate::{
|
||||
clear, create_frame, flush, get_term_size, hide_cursor,
|
||||
key::{read_key, Key},
|
||||
render_to_frame, show_cursor, Command, Element, ElementSize, UpdateResponse,
|
||||
};
|
||||
|
||||
pub struct UpdateContext {
|
||||
pub key: key::Key,
|
||||
pub tick: usize,
|
||||
pub response: UpdateResponse,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
element: Box<dyn Element>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Creates a new screen to render components
|
||||
pub fn new() -> App {
|
||||
App {
|
||||
element: ui::text(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new screen to render components with a pre-existing component
|
||||
pub fn from(elem: Box<dyn Element>) -> App {
|
||||
let mut app = App::new();
|
||||
app.set_component(elem);
|
||||
app
|
||||
}
|
||||
|
||||
/// Sets a component
|
||||
pub fn set_component(&mut self, element: Box<dyn Element>) {
|
||||
let (width, height) = crossterm::terminal::size().unwrap();
|
||||
self.element = element;
|
||||
let mut data = self.element.get_data();
|
||||
data.style.is_active = true;
|
||||
if data.width == 0 {
|
||||
data.width = width as usize;
|
||||
}
|
||||
if data.height == 0 {
|
||||
data.height = height as usize;
|
||||
}
|
||||
self.element.set_data(data);
|
||||
}
|
||||
|
||||
/// Render to the screen
|
||||
fn render(&mut self, tick: usize) {
|
||||
let (width, height) = crossterm::terminal::size().unwrap();
|
||||
let mut data = self.element.get_data();
|
||||
if data.width == 0 {
|
||||
data.width = width as usize;
|
||||
}
|
||||
if data.height == 0 {
|
||||
data.height = height as usize;
|
||||
}
|
||||
self.element.set_data(data);
|
||||
let mut frame: Vec<String> = create_frame!(width as usize, height as usize);
|
||||
utils::render_to_frame(tick, &mut frame, &mut self.element);
|
||||
utils::clear();
|
||||
/// Renders a single frame of the UI to the terminal.
|
||||
///
|
||||
/// Sets up a new frame based on the terminal's current size, updates
|
||||
/// the element dimensions, and renders the UI element to the frame.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `elem` - A mutable reference to a boxed UI element that implements the `Element` trait.
|
||||
/// * `state` - Current state of the element, typically used to track the UI's state in the app loop.
|
||||
fn render(elem: &mut Box<dyn Element>, state: usize) {
|
||||
let (width, height) = get_term_size();
|
||||
elem.update_data(width, height);
|
||||
let mut frame: Vec<String> =
|
||||
create_frame(ElementSize::Custom(width), ElementSize::Custom(height));
|
||||
render_to_frame(state, &mut frame, elem);
|
||||
clear();
|
||||
print!("{}", frame.join(""));
|
||||
utils::flush();
|
||||
let mut data = self.element.get_data();
|
||||
if data.width == width as usize {
|
||||
data.width = 0;
|
||||
}
|
||||
if data.height == height as usize {
|
||||
data.height = 0;
|
||||
}
|
||||
self.element.set_data(data);
|
||||
flush();
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &mut UpdateContext) {
|
||||
self.element.update(ctx);
|
||||
match ctx.response {
|
||||
UpdateResponse::Exit => {
|
||||
crossterm::terminal::disable_raw_mode().unwrap();
|
||||
utils::clear();
|
||||
utils::show_cursor();
|
||||
println!("");
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the screen
|
||||
pub fn run(&mut self) {
|
||||
// Initialize
|
||||
utils::hide_cursor();
|
||||
utils::clear();
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout
|
||||
.execute(crossterm::terminal::EnterAlternateScreen)
|
||||
.unwrap();
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
|
||||
// Start the update thread
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || loop {
|
||||
tx.send(key::read_key()).unwrap();
|
||||
});
|
||||
|
||||
// Start the render loop
|
||||
let mut tick: usize = 0;
|
||||
let tick_duration = std::time::Duration::from_millis(1000/30);
|
||||
let mut last_tick = std::time::Instant::now();
|
||||
loop {
|
||||
let now = std::time::Instant::now();
|
||||
let elapsed = now.duration_since(last_tick);
|
||||
|
||||
if elapsed >= tick_duration {
|
||||
last_tick = now;
|
||||
if tick > 99 {
|
||||
tick = 0;
|
||||
}
|
||||
self.render(tick);
|
||||
tick += 1;
|
||||
match rx.try_recv() {
|
||||
Ok(k) => self.update(&mut UpdateContext {
|
||||
key: k,
|
||||
tick,
|
||||
response: UpdateResponse::None,
|
||||
}),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => {}
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
panic!("disconnected")
|
||||
/// Updates the UI element based on keyboard input and issues any commands in response.
|
||||
///
|
||||
/// Processes the result of `Element::update` to handle commands like rendering,
|
||||
/// updating, and exiting. Commands can be a single action or a list of actions.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `elem` - A mutable reference to a boxed UI element.
|
||||
/// * `state` - The current UI state, used for conditional updates.
|
||||
/// * `k` - A `Key` input, typically read from the user’s keyboard input.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if an `Exit` command is issued, signaling the application to terminate.
|
||||
fn update(elem: &mut Box<dyn Element>, state: usize, k: Key) -> bool {
|
||||
match elem.event(state, k.clone()) {
|
||||
UpdateResponse::Command(command) => run_command(command, elem, k.clone()),
|
||||
UpdateResponse::CommandList(commands) => {
|
||||
for command in commands {
|
||||
if run_command(command, elem, k.clone()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
if let Some(remaining) = tick_duration.checked_sub(elapsed) {
|
||||
std::thread::sleep(remaining);
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a command, performing actions such as rendering, updating,
|
||||
/// sleeping, or exiting the application.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `command` - The command to execute, such as rendering or updating the UI.
|
||||
/// * `elem` - A mutable reference to the UI element.
|
||||
/// * `k` - A `Key` input passed along for further processing.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the command is `Exit`, ending the application loop.
|
||||
fn run_command(command: Command, elem: &mut Box<dyn Element>, k: Key) -> bool {
|
||||
match command {
|
||||
Command::Render(state) => {
|
||||
render(elem, state);
|
||||
false
|
||||
}
|
||||
Command::Update(state) => update(elem, state, k),
|
||||
Command::Sleep(duration) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(duration));
|
||||
false
|
||||
}
|
||||
Command::Exit => {
|
||||
show_cursor();
|
||||
crossterm::terminal::disable_raw_mode().unwrap();
|
||||
clear();
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the main event loop for the application.
|
||||
///
|
||||
/// Enables raw mode, hides the cursor, and continuously renders and updates
|
||||
/// the UI based on user input. The loop will break if the `Exit` command is triggered.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `elem` - A mutable reference to the main UI element to be rendered and updated.
|
||||
pub fn run(elem: &mut Box<dyn Element>) {
|
||||
// Initialize terminal settings
|
||||
hide_cursor();
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
clear();
|
||||
loop {
|
||||
render(elem, 1);
|
||||
let k = read_key();
|
||||
if update(elem, 1, k) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user