Large structural changes

This commit is contained in:
2024-11-05 17:26:19 +01:00
parent 196c6094d2
commit 4f9a143008
10 changed files with 672 additions and 361 deletions
+129 -145
View File
@@ -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;
}
}
}