Files
osui/src/lib.rs
T

551 lines
15 KiB
Rust

//! # 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 events.
//!
//! ## Example Usage
//!
//! ```rust
//! use osui::prelude::*;
//!
//! #[component]
//! pub fn App() -> Element {
//! let count = State::new(0);
//!
//! rsx! {
//! button {
//! class: "btn",
//!
//! on_click: fn(_, _, _) @count {
//! count += 1;
//! },
//!
//! "The current count is: {count}"
//! }
//! }
//! }
//! fn main() {
//! launch!(App);
//! }
//! ```
//!
//! ## Modules
//! - `ui` - Contains all user interface components, enabling rich CLI experiences.
//! - `utils` - Utility functions for common TUI tasks such as clearing the screen.
use std::{
collections::HashMap,
fmt::Debug,
ops::{AddAssign, SubAssign},
sync::{Arc, Mutex},
};
use crossterm::event::Event;
use ui::{Style, StyleCore};
pub mod css;
pub mod examples;
pub mod macros;
pub mod rsx;
pub mod ui;
pub mod utils;
pub mod prelude {
pub use crate::ui::Color::*;
pub use crate::ui::Font::*;
pub use crate::ui::Number::{Auto, Center};
pub use crate::Instruction::*;
pub use crate::{self as osui, css, ersx, launch, rsx, ui::*, Handler};
pub use crate::{style, Component, Document, Instruction, State};
pub use crate::{Children, Element, ElementCore, ElementWidget};
pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
pub use osui_element::component;
pub fn sleep(ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(ms));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// Type aliases
//////////////////////////////////////////////////////////////////////////////////////////////////
pub type Element = Box<dyn ElementWidget>;
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Traits
//////////////////////////////////////////////////////////////////////////////////////////////////
pub trait ElementCore {
fn get_element_by_id(&mut self, id: &str) -> Option<&mut Element>;
fn get_id(&self) -> String;
fn get_class(&self) -> String;
fn get_style(&self) -> &Style;
}
pub trait ElementWidget: ElementCore + std::fmt::Debug {
fn render(&self, writer: &mut Writer);
fn event(&mut self, event: crossterm::event::Event, document: &mut Document) {
_ = (event, document)
}
fn initialize(&mut self, document: &mut Document) {
_ = document
}
}
pub trait Component: std::fmt::Debug {
type Element;
fn create_element(self) -> Self::Element;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Structs
//////////////////////////////////////////////////////////////////////////////////////////////////
pub struct Handler<T>(pub Arc<Mutex<dyn FnMut(&mut T, crossterm::event::Event, &mut Document)>>);
pub struct Document {
element: *mut Element,
running: Option<bool>,
pub css: ui::Css,
}
#[derive(Debug)]
pub struct Writer {
css: *const ui::Css,
style: ui::StyleElement,
focused: bool,
size: (u16, u16),
pos: (u16, u16),
written: (u16, u16),
parent: Option<*mut Writer>,
}
pub struct State<T>(Arc<Mutex<T>>);
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Enums
//////////////////////////////////////////////////////////////////////////////////////////////////
pub enum Children {
None,
Text(Arc<dyn Fn() -> String>),
StaticText(String),
Children(Vec<Element>, usize),
}
//////////////////////////////////////////////////////////////////////////////////////////////////
/// Implementations
//////////////////////////////////////////////////////////////////////////////////////////////////
impl Document {
pub fn with_elem(element: &mut Element) -> Document {
Document {
element,
running: None,
css: HashMap::new(),
}
}
pub fn exit(&mut self) {
self.running = Some(false);
}
pub fn restart(&mut self) {
self.running = Some(true);
}
pub fn get_root<T>(&self) -> &mut Box<T> {
unsafe { &mut *(self.element as *mut Element as *mut Box<T>) }
}
pub fn get_element_by_id<T>(&self, id: &str) -> Option<&mut Box<T>> {
if let Some(e) = self.get_element_by_id_raw(id) {
Some(unsafe { &mut *(e as *mut Element as *mut Box<T>) })
} else {
None
}
}
pub fn get_element_by_id_raw(&self, id: &str) -> Option<&mut Element> {
if !self.element.is_null() {
unsafe {
if (*self.element).get_id() == id {
Some(&mut *self.element)
} else {
(*self.element).get_element_by_id(id)
}
}
} else {
None
}
}
pub fn render(&self) {
if !self.element.is_null() {
let (width, height) = crossterm::terminal::size().unwrap();
let mut writer = Writer {
css: &self.css,
focused: true,
style: ui::StyleElement {
// outline: (true, true),
..Default::default()
},
size: (width, height),
pos: (1, 1),
written: (0, 0),
parent: None,
};
let mut frame = writer.new_frame();
utils::clear();
frame.render(true, unsafe { &*self.element });
writer.after_render(width, height);
utils::flush();
}
}
pub fn set_css(&mut self, css: ui::Css) {
self.css = css;
}
pub fn clear_css(&mut self) {
self.css.clear();
}
pub fn draw(&mut self, element: Element) {
unsafe {
*self.element = element;
(*self.element).initialize(self);
}
}
pub fn run(&mut self) -> bool {
// Set up the screen
utils::hide_cursor();
crossterm::terminal::enable_raw_mode().unwrap();
utils::clear();
self.running = None;
let element = unsafe { &mut *self.element };
self.render();
element.initialize(self);
// Send initial event
element.event(crossterm::event::Event::FocusGained, self);
while self.running == None {
self.render();
element.event(crossterm::event::read().unwrap(), self);
}
utils::show_cursor();
crossterm::terminal::disable_raw_mode().unwrap();
utils::clear();
self.running.unwrap()
}
}
impl Default for Children {
fn default() -> Self {
Self::None
}
}
impl std::fmt::Debug for Children {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&match self {
Self::None => String::new(),
Self::Children(a, b) => format!("({b}, {a:?})"),
Self::Text(a) => format!("{:?}", a()),
Self::StaticText(a) => format!("static({a:?})"),
})
}
}
impl Children {
pub fn is_none(&self) -> bool {
match self {
Children::None => true,
_ => false,
}
}
pub fn get_text(&self) -> String {
match self {
Children::Text(text) => text(),
Children::StaticText(text) => text.clone(),
_ => String::new(),
}
}
pub fn set_text(&mut self, text: Arc<dyn Fn() -> String>) {
match self {
Children::Text(t) => {
*t = text;
}
_ => {}
}
}
pub fn set_text_force(&mut self, text: Arc<dyn Fn() -> String>) {
match self {
Children::Text(t) => {
*t = text;
}
_ => {
*self = Children::Text(text);
}
}
}
pub fn set_static_force(&mut self, text: String) {
*self = Children::StaticText(text);
}
pub fn add_child(&mut self, element: Element) {
if let Children::Children(children, _) = self {
children.push(element);
}
}
pub fn get_child(&mut self) -> (Option<&mut Element>, usize) {
if let Children::Children(children, child) = self {
let l = children.len();
(children.get_mut(*child), l)
} else {
(None, 0)
}
}
pub fn get_child_idx(&mut self) -> (Option<(&mut Element, &mut usize)>, usize) {
if let Children::Children(children, child) = self {
let l = children.len();
if let Some(d) = children.get_mut(*child) {
(Some((d, child)), l)
} else {
(None, 0)
}
} else {
(None, 0)
}
}
pub fn set_index(&mut self, index: usize) {
if let Children::Children(_, child) = self {
*child = index;
}
}
pub fn len(&self) -> usize {
if let Children::Children(children, _) = self {
children.len()
} else {
0
}
}
pub fn index(&self) -> usize {
if let Children::Children(_, c) = self {
*c
} else {
0
}
}
pub fn insert(&mut self, index: usize, element: Element) {
if let Children::Children(children, _) = self {
children.insert(index, element);
}
}
}
impl<T> Handler<T> {
pub fn new<F>(handler_fn: F) -> Handler<T>
where
F: FnMut(&mut T, crossterm::event::Event, &mut Document) + 'static,
{
Handler(Arc::new(Mutex::new(handler_fn)))
}
pub fn call(&self, s: &mut T, event: Event, document: &mut Document) {
let mut o = self.0.lock().unwrap();
o(s, event, document);
}
pub fn clone(&self) -> Handler<T> {
Handler(Arc::clone(&self.0))
}
}
impl Writer {
pub fn after_render(&self, width: u16, height: u16) {
if self.style.outline.1 {
let ol = self.style.write_outline("│");
let hl = "─".repeat(width as usize - 2);
self.write_abs_outline(&self.style.write_outline(&format!("╭{}╮", hl)), (0, 0));
for i in 1..height {
self.write_abs_outline(&ol, (0, i));
self.write_abs_outline(&ol, (width - 1, i));
}
self.write_abs_outline(&self.style.write_outline(&format!("╰{}╯", hl)), (0, height));
}
}
pub fn write(&mut self, s: &str) {
// let width = self.get_size().0 as usize;
// let s = s
// .lines()
// .map(|l| {
// if l.len() > width {
// l.chars().take(width).collect::<String>()
// } else {
// l.to_string()
// }
// })
// .collect::<Vec<String>>()
// .join("\n");
self.write_abs(&self.style.write(&s), (0, 0));
}
pub fn write_abs_outline(&self, s: &str, pos: (u16, u16)) {
if let Some(parent) = self.parent {
let parent = unsafe { &mut *parent };
parent.write_abs_outline(s, (self.pos.0 + pos.0, self.pos.1 + pos.1));
} else {
print!(
"\x1b[{};{}H{}",
self.pos.1 + pos.1 - 1,
self.pos.0 + pos.0 - 1,
self.style.write_outline(s)
);
}
}
pub fn write_abs(&mut self, s: &str, pos: (u16, u16)) {
let width = self.get_size().0 as usize;
let mut max_written = self.written.0;
let mut lines_written = 0;
let truncated_lines: Vec<_> = s
.lines()
.map(|l| {
let truncated = if l.len() > width {
l.chars().take(width).collect::<String>()
} else {
l.to_string()
};
max_written = max_written.max(truncated.len() as u16);
lines_written += 1;
truncated
})
.collect();
self.written.0 = max_written;
self.written.1 += lines_written;
if let Some(parent) = self.parent {
let parent = unsafe { &mut *parent };
let combined_pos = (self.pos.0 + pos.0, self.pos.1 + pos.1);
let s_combined = truncated_lines.join("\n");
parent.write_abs(&s_combined, combined_pos);
} else {
for (i, line) in truncated_lines.iter().enumerate() {
print!(
"\x1b[{};{}H{}",
self.pos.1 + pos.1 + i as u16,
self.pos.0 + pos.0,
line
);
}
}
}
pub fn get_focused(&self) -> bool {
self.focused
}
pub fn get_size(&self) -> (u16, u16) {
(
self.style
.width
.1
.as_size(self.size.0, self.size.0, self.style.outline.1),
self.style
.height
.1
.as_size(self.size.1, self.size.1, self.style.outline.1),
)
}
pub fn get_size_root(&self) -> (u16, u16) {
(
self.style
.width
.1
.as_size(self.written.0, self.size.0, self.style.outline.1),
self.style
.height
.1
.as_size(self.written.1, self.size.1, self.style.outline.1),
)
}
pub fn new_frame(&mut self) -> crate::utils::Frame {
crate::utils::Frame::new(self)
}
pub fn caret(&self) -> String {
format!(
"{}{}\x1b[0m",
self.style.cursor.1.ansi(),
self.style.caret.1
)
}
}
impl<T> Default for Handler<T> {
fn default() -> Self {
Handler(Arc::new(Mutex::new(
|_: &mut T, _: crossterm::event::Event, _: &mut Document| {},
)))
}
}
impl<T> std::fmt::Debug for Handler<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Handler()")
}
}
impl<T> State<T> {
pub fn new<'a>(v: T) -> State<T> {
State(Arc::new(Mutex::new(v)))
}
pub fn use_state<'a>(&'a self) -> std::sync::MutexGuard<'a, T> {
self.0.lock().unwrap()
}
pub fn clone<'a>(&'a self) -> State<T> {
State(Arc::clone(&self.0))
}
}
impl<T: std::fmt::Display> std::fmt::Display for State<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Acquire the lock, handle potential poisoning
match self.0.lock() {
Ok(guard) => write!(f, "{}", *guard),
Err(poisoned) => write!(f, "<Poisoned: {}>", *poisoned.into_inner()),
}
}
}
impl<T: AddAssign> AddAssign<T> for State<T> {
fn add_assign(&mut self, rhs: T) {
*self.0.lock().unwrap() += rhs;
}
}
impl<T: SubAssign> SubAssign<T> for State<T> {
fn sub_assign(&mut self, rhs: T) {
*self.0.lock().unwrap() -= rhs;
}
}
impl<T: PartialEq> PartialEq<T> for State<T> {
fn eq(&self, other: &T) -> bool {
self.use_state().eq(other)
}
fn ne(&self, other: &T) -> bool {
self.use_state().ne(other)
}
}
#[derive(Debug)]
pub enum Instruction<'a> {
SetStyle(ui::Css),
SetChild(usize),
Ghost(usize),
Load(Handler<ui::Div<'a>>),
}