diff --git a/Cargo.toml b/Cargo.toml index f7797f6..ca266b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ cc = "1.0" [dependencies] crossterm = "0.28.1" +downcast-rs = "2.0.1" [profile.dev] opt-level = 1 diff --git a/src/console.rs b/src/console.rs deleted file mode 100644 index 7979f85..0000000 --- a/src/console.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Console module -//! -//! This module provides utilities for managing terminal interactions, including event handling, -//! rendering, and terminal state management. - -use crate::{Element, Frame}; - -/// Represents the console state, containing a frame for rendering and a mouse capture flag. -pub struct Console(Frame, bool); - -/// Enum representing various events that can occur in the console. -#[derive(Debug, Clone)] -pub enum Event { - /// A keyboard event. - Key(crossterm::event::KeyEvent), - /// A terminal resize event with new dimensions (width, height). - Resize(u16, u16), - /// A mouse event. - Mouse(crossterm::event::MouseEvent), - /// A paste event with the pasted content. - Paste(String), - /// An event indicating the terminal gained focus. - FocusGained, - /// An event indicating the terminal lost focus. - FocusLost, -} - -/// Initializes the console with raw mode enabled and optionally mouse capture. -/// -/// # Arguments -/// * `mouse` - A boolean flag indicating whether to enable mouse capture. -/// -/// # Returns -/// A `Console` instance wrapped in a `Result`. -pub fn init(mouse: bool) -> crate::Result { - crossterm::terminal::enable_raw_mode()?; - crate::utils::clear()?; - crate::utils::hide_cursor()?; - if mouse { - crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)?; - } - Ok(Console(Frame::new(crossterm::terminal::size()?), mouse)) -} - -impl Console { - /// Renders a user interface element with an optional event. - /// - /// # Arguments - /// * `ui` - The UI element to render. - /// * `event` - An optional event to pass to the UI element. - /// - /// # Returns - /// A `Result` indicating success or failure. - pub fn draw(&mut self, ui: Element, event: Option) -> crate::Result<()> { - self.0.clear()?; - ui(&mut self.0, event) - } - - /// Runs the console loop, rendering the UI and handling events. - /// - /// - /// # Arguments - /// * `ui` - The UI element to render. - /// - /// # Returns - /// A `Result` indicating success or failure. - pub fn run(&mut self, ui: Element) -> crate::Result<()> { - self.draw(ui.clone(), None)?; - loop { - let event = read()?; - if let Event::Resize(w, h) = event { - self.0.width = w; - self.0.height = h; - } else if let Event::Mouse(crossterm::event::MouseEvent { - kind: crossterm::event::MouseEventKind::Moved, - row, - column, - .. - }) = event - { - self.0.mouse_pos = Some((column, row)); - - self.draw(ui.clone(), None)?; - } else { - self.draw(ui.clone(), Some(event))?; - } - } - } - - /// Ends the console session, restoring the terminal state. - /// - /// # Returns - /// A `Result` indicating success or failure. - pub fn end(&self) -> crate::Result<()> { - if self.1 { - crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture)?; - } - crossterm::terminal::disable_raw_mode()?; - crate::utils::clear()?; - crate::utils::show_cursor() - } - - /// Retrieves the current terminal size. - /// - /// # Returns - /// A tuple containing the width and height of the terminal. - pub fn size(&self) -> (u16, u16) { - (self.0.width, self.0.height) - } -} - -/// Reads an event from the terminal. -/// -/// # Returns -/// An `Event` wrapped in a `Result`. -pub fn read() -> crate::Result { - let event = crossterm::event::read()?; - - Ok(match event { - crossterm::event::Event::Key(k) => Event::Key(k), - crossterm::event::Event::Resize(w, h) => Event::Resize(w, h), - crossterm::event::Event::FocusGained => Event::FocusGained, - crossterm::event::Event::FocusLost => Event::FocusLost, - crossterm::event::Event::Mouse(m) => Event::Mouse(m), - crossterm::event::Event::Paste(p) => Event::Paste(p), - }) -} - -/// Attempts to read an event from the terminal without blocking. -/// -/// # Returns -/// An `Option` containing an `Event` if one is available. -pub fn try_read() -> Option { - if crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) { - read().ok() - } else { - None - } -} diff --git a/src/elements.rs b/src/elements.rs deleted file mode 100644 index 856749a..0000000 --- a/src/elements.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::prelude::*; - -impl Widget for &str { - fn render(&self) -> String { - self.to_string() - } -} - -impl Widget for String { - fn render(&self) -> String { - self.clone() - } -} - -pub struct Button { - pub on_click: Box, - children: String, - width: u16, - height: u16, - last_elem: (u16, u16), -} - -pub fn btn() -> Button { - Button { - on_click: Box::new(|| {}), - children: String::new(), - width: 0, - height: 0, - last_elem: (0, 0), - } -} - -impl Widget for Button { - fn render(&self) -> String { - self.children.clone() - } - fn event(&mut self, _: crate::console::Event) { - (self.on_click)() - } -} - -impl Button { - pub fn draw(&mut self, w: &W, props: crate::Props) -> crate::Result<()> - where - W: Widget, - { - let written = w.render(); - - let (ww, wh) = utils::str_size(&written); - - let (width, height) = ( - props.width.get(ww, self.width), - props.height.get(wh, self.height), - ); - - let (x, y) = ( - props.x.get(self.last_elem.0, width, self.width), - props.y.get(self.last_elem.1, height, self.height), - ); - - self.children += written - .lines() - .enumerate() - .filter(|(i, _)| -> bool { height > (*i as u16) }) - .map(|(_, s)| s.chars().take(width as usize).collect::()) - .collect::>() - .join("\n") - .as_str(); - - self.last_elem.0 = x + width; - self.last_elem.1 = y + height; - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 82603a4..2fa7fb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,205 +1,35 @@ -//! # Library Documentation -//! This library provides a framework for creating terminal-based user interfaces. -//! It includes support for widgets, layouts, and event handling using `crossterm`. +use std::{any::TypeId, thread, time::Duration}; -//! ## Modules -//! - `console`: Handles terminal input and events. -//! - `elements`: Provides pre-defined UI elements. Optional, enabled by default. -//! - `rsx`: Supports declarative UI definition using an XML-like syntax. Optional, enabled by default. -//! - `state`: Manages state for widgets and other components. -//! - `utils`: Utility functions for terminal rendering. -//! - `prelude`: Exports commonly used types and traits for easy access. +pub mod output; -pub mod console; -#[cfg(not(feature = "no_elem"))] -pub mod elements; -#[cfg(not(feature = "no_rsx"))] -pub mod rsx; -pub mod state; -pub mod utils; - -/// Commonly used imports for convenience. -pub mod prelude { - pub use crate::console::*; - pub use crate::elements::*; - pub use crate::rsx; - pub use crate::utils; - pub use crate::{Element, Frame, Pos, Props, Result, Size, Widget}; - pub use crossterm::event::{KeyCode, KeyEvent}; +pub trait Extension { + fn tick(&mut self, args: &mut Args); } -/// Type alias for simplifying error handling. -/// Represents an I/O operation result. -pub use std::io::Result; - -/// Type alias for a UI element. -/// An `Element` is a thread-safe function that renders onto a `Frame` -/// and optionally handles events. -pub type Element = std::sync::Arc) -> crate::Result<()>>; - -/// A trait representing a UI widget. -pub trait Widget { - /// Renders the widget as a `String`. - fn render(&self) -> String; - - /// Handles an event for the widget. Defaults to a no-op. - fn event(&mut self, event: console::Event) { - _ = event; - } +pub trait BaseElement { + fn init(&mut self); + fn get_components<'a>(&'a mut self) -> &'a mut Vec>; } -/// Struct representing a rectangular props in the terminal. -#[derive(Debug, Clone, Copy)] -pub struct Props { - pub width: Size, - pub height: Size, - pub x: Pos, - pub y: Pos, +pub trait ElementComponent { + fn get_type_id(&self) -> TypeId; } -/// Struct representing a rendering frame. -#[derive(Debug, Default, Clone, Copy)] -pub struct Frame { - pub width: u16, - pub height: u16, - mouse_pos: Option<(u16, u16)>, - last_elem: (u16, u16), +pub struct Args { + pub tick_rate: u16, + pub base_elements: Vec>, } -/// Enum representing positioning options for widgets. -#[derive(Debug, Clone, Copy)] -pub enum Pos { - /// Automatic positioning. - #[allow(non_camel_case_types)] - auto, - /// Centered positioning. - #[allow(non_camel_case_types)] - center, - /// Fixed position as a number of columns or rows. - Num(u16), -} - -/// Enum representing sizing options for widgets. -#[derive(Debug, Clone, Copy)] -pub enum Size { - /// Automatic sizing. - Auto, - /// Fixed size. - Num(u16), -} - -impl Pos { - /// Calculates the position based on the current frame dimensions. - pub fn get(self, auto: u16, width: u16, frame: u16) -> u16 { - match self { - Self::auto => auto, - Self::center => (frame - width) / 2, - Self::Num(n) => n, - } - } -} - -impl Size { - /// Internal method to compute size. - fn get_(self, written: u16) -> u16 { - match self { - Self::Auto => written, - Self::Num(n) => n, - } +pub fn run(mut args: Args, mut extensions: Vec>) { + for elem in &mut args.base_elements { + elem.init(); } - /// Computes the size based on written content or frame dimensions. - pub fn get(self, written: u16, _frame: u16) -> u16 { - self.get_(written) - } -} - -impl Props { - /// Creates a new `Props` with default values. - pub fn new() -> Self { - Self::default() - } -} - -impl Default for Props { - fn default() -> Self { - Self { - width: Size::Auto, - height: Size::Auto, - x: Pos::Num(0), - y: Pos::auto, - } - } -} - -impl Frame { - /// Draws a widget on the frame. - pub fn draw(&mut self, w: &W, props: Props) -> Result<()> - where - W: Widget, - { - let written = w.render(); - - let (ww, wh) = utils::str_size(&written); - - let (width, height) = ( - props.width.get(ww, self.width), - props.height.get(wh, self.height), - ); - - let (x, y) = ( - props.x.get(self.last_elem.0, width, self.width), - props.y.get(self.last_elem.1, height, self.height), - ); - - let ansi = if let Some((mouse_x, mouse_y)) = &self.mouse_pos { - if (x..x + width).contains(mouse_x) && (y..y + height).contains(mouse_y) { - "\x1b[32m" - } else { - "" - } - } else { - "" - }; - - for (i, line) in written.lines().enumerate() { - if i as u16 > height { - break; - } - - println!( - "\x1b[{};{}H{ansi}{}\x1b[0m", - y + (i as u16) + 1, - x + 1, - line.chars().take(width as usize).collect::() - ); + loop { + for extension in &mut extensions { + extension.tick(&mut args); } - self.last_elem.0 = x + width; - self.last_elem.1 = y + height; - - Ok(()) - } - - /// Clears the frame. - pub fn clear(&mut self) -> Result<()> { - self.last_elem.0 = 0; - self.last_elem.1 = 0; - utils::clear() - } - - /// Creates a new frame with the specified dimensions. - pub fn new((width, height): (u16, u16)) -> Self { - Self { - width, - height, - last_elem: (0, 0), - mouse_pos: None, - } + thread::sleep(Duration::from_millis(1000 / args.tick_rate as u64)); } } - -/// Creates a new state object. -pub fn use_state(v: T) -> state::State { - state::State(Box::into_raw(Box::new(v))) -} diff --git a/src/main.rs b/src/main.rs index b018ebf..06a0713 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,31 @@ -use osui::prelude::*; +use osui::{ + output::{OutputExtension, RenderAble}, + run, Args, BaseElement, +}; -fn main() -> Result<()> { - let mut con = init(true)?; +pub struct MyElem(*mut Vec>); +impl BaseElement for MyElem { + fn init(&mut self) { + unsafe { + (*self.0).push(RenderAble::new(|| String::from("value"))); + } + } - con.run(app())?; - - con.end() -} - -pub fn app() -> Element { - rsx! { - btn { "Click me" } (x-center y-center) + fn get_components<'a>(&'a mut self) -> &'a mut Vec> { + unsafe { &mut *self.0 } } } + +impl MyElem { + pub fn new(mut v: Vec>) -> Box { + Box::new(MyElem(&mut v)) + } +} + +fn main() { + let args = Args { + tick_rate: 40, + base_elements: vec![], + }; + run(args, vec![Box::new(OutputExtension)]); +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..13a37c4 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,31 @@ +use std::{any::TypeId, fmt::Debug}; + +use crate::{ElementComponent, Extension}; + +pub struct OutputExtension; + +impl Extension for OutputExtension { + fn tick(&mut self, args: &mut crate::Args) { + for elem in &mut args.base_elements { + for comp in elem.get_components() { + if comp.get_type_id() == TypeId::of::() { + comp + } + } + } + } +} + +pub struct RenderAble(pub Box String>); + +impl ElementComponent for RenderAble { + fn get_type_id(&self) -> std::any::TypeId { + TypeId::of::() + } +} + +impl RenderAble { + pub fn new String + 'static>(f: F) -> Box { + Box::new(RenderAble(Box::new(f))) + } +} diff --git a/src/rsx.rs b/src/rsx.rs deleted file mode 100644 index 694db95..0000000 --- a/src/rsx.rs +++ /dev/null @@ -1,186 +0,0 @@ -/// A brief summary of what the macro does. -/// -/// A detailed explanation about the macro's purpose, -/// how it expands, and when to use it. -/// -/// # Examples -/// -/// Simple -/// ```rust -/// rsx! { -/// "Hello, World!" -/// } -/// ``` -/// -/// Button example -/// ```rust -/// rsx! { -/// button { "{count}" } -/// } -/// ``` -#[macro_export] -macro_rules! rsx { - ($($inner:tt)*) => { - std::sync::Arc::new( - move |frame: &mut $crate::Frame, event: Option<$crate::console::Event>| -> $crate::Result<()> { - $crate::rsx_inner!(frame, event; $($inner)*); - Ok(()) - } - ) - }; -} - -/// # Warning -/// -/// **Don't use this macro manually** use `rsx!` instead -#[macro_export] -macro_rules! rsx_inner { - // Field - ($frame:expr, $event:expr; - $n:ident: $v:expr - $(, $($rest:tt)*)?) => { - $frame.$n = $v; - $crate::rsx_inner!($frame, $event; $($($rest)*)?); - }; - - // For loop - ($frame:expr, $event:expr; - for ($i:pat in $e:expr) $(%$s:ident),* {$($inner:tt)*} - $($rest:tt)*) => { - for $i in $e { - $(let $s = $s.copy_state();)* - $crate::rsx_inner!($frame, $event; $($inner)*); - } - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - - // If statement - ($frame:expr, $event:expr; - if ($e:expr) $(%$s:ident),* {$($inner:tt)*} - $(else if ($ee:expr) {$($elif_inner:tt)*})* - else {$($else_inner:tt)*} - $($rest:tt)*) => { - $(let $s = $s.copy_state();)* - if $e { - $crate::rsx_inner!($frame, $event; $($inner)*); - } $(else if $ee { - $crate::rsx_inner!($frame, $event; $($elif_inner)*); - })* else { - $crate::rsx_inner!($frame, $event; $($else_inner)*); - } - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - - ($frame:expr, $event:expr; - if ($e:expr) $(%$s:ident),* {$($inner:tt)*} - $($rest:tt)*) => { - $(let $s = $s.copy_state();)* - if $e { - $crate::rsx_inner!($frame, $event; $($inner)*); - } - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - - // Literal - ($frame:expr, $event:expr; - $elem:literal ($($inner:tt)*) - $($rest:tt)*) => { - let mut props = $crate::Props::new(); - $crate::tw_props!(props, $($inner)*); - $frame.draw(&format!($elem), props)?; - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - ($frame:expr, $event:expr; - $elem:literal - $($rest:tt)*) => { - $frame.draw(&format!($elem), Props::new())?; - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - - // Name { rsx } ( props ) - ($frame:expr, $event:expr; - $elem:path { - $($inner:tt)* - } ($($props_inner:tt)*) - $($rest:tt)*) => { - let mut props = $crate::Props::new(); - $crate::tw_props!(props, $($props_inner)*); - - let mut e = $elem(); - $crate::rsx_inner!(e, $event; $($inner)*); - - $frame.draw(&e, props)?; - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - - // Name { rsx } - ($frame:expr, $event:expr; - $elem:path { - $($inner:tt)* - } - $($rest:tt)*) => { - let mut e = $elem(); - $crate::rsx_inner!(e, $event; $($inner)*); - - $frame.draw(&e, $crate::Props::new())?; - $crate::rsx_inner!($frame, $event; $($rest)*); - }; - - ($frame:expr, $event:expr;) => {}; -} - -/// # Warning -/// -/// **Don't use this macro manually** use `rsx!` instead -#[macro_export] -macro_rules! tw_props { - ($a:expr, $n:ident) => { - $a.$n; - }; - - // X - ($a:expr, x-$v:ident $($rest:tt)*) => { - $a.x = $crate::Pos::$v; - $crate::tw_props!($a, $($rest)*); - }; - - ($a:expr, x-$v:literal $($rest:tt)*) => { - $a.x = $crate::Pos::Num($v); - $crate::tw_props!($a, $($rest)*); - }; - - // Y - ($a:expr, y-$v:ident $($rest:tt)*) => { - $a.y = $crate::Pos::$v; - $crate::tw_props!($a, $($rest)*); - }; - - ($a:expr, y-$v:literal $($rest:tt)*) => { - $a.y = $crate::Pos::Num($v); - $crate::tw_props!($a, $($rest)*); - }; - - // Width - ($a:expr, width-$v:ident $($rest:tt)*) => { - $a.width = $crate::Size::$v; - $crate::tw_props!($a, $($rest)*); - }; - - ($a:expr, width-$v:literal $($rest:tt)*) => { - $a.width = $crate::Size::Num($v); - $crate::tw_props!($a, $($rest)*); - }; - - // Height - ($a:expr, height-$v:ident $($rest:tt)*) => { - $a.height = $crate::Size::$v; - $crate::tw_props!($a, $($rest)*); - }; - - ($a:expr, height-$v:literal $($rest:tt)*) => { - $a.height = $crate::Size::Num($v); - $crate::tw_props!($a, $($rest)*); - }; - - ($a:expr,) => {}; -} diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index c9c7a2b..0000000 --- a/src/state.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! The `state` module provides the `State` type for managing mutable state across UI components -//! and operations. It wraps a raw pointer to enable shared mutability and integrates with common -//! traits for ease of use. - -/// A wrapper around a raw pointer to a mutable value, allowing shared state management. -/// -/// # Safety -/// This type uses unsafe code to dereference raw pointers. Proper care must be taken -/// to ensure the validity and lifetime of the referenced value. -/// -/// # Examples -/// ``` -/// let value = 42; -/// let state = State(Box::into_raw(Box::new(value))); -/// assert_eq!(*state, 42); -/// ``` -#[derive(Clone, Copy)] -pub struct State(pub(crate) *mut T); - -impl std::fmt::Display for State { - /// Formats the value of the state for display. - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", unsafe { &*self.0 }) - } -} - -impl std::fmt::Debug for State { - /// Formats the state for debugging purposes. - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "State({:?})", unsafe { &*self.0 }) - } -} - -//// State Operations //// - -impl + Clone> std::ops::Add for State { - type Output = T; - - /// Adds a value to the state, returning the result. - fn add(self, rhs: T) -> Self::Output { - unsafe { - let lhs = &mut *self.0; - lhs.clone() + rhs - } - } -} - -impl + Clone> std::ops::Sub for State { - type Output = T; - - /// Subtracts a value from the state, returning the result. - fn sub(self, rhs: T) -> Self::Output { - unsafe { - let lhs = &mut *self.0; - lhs.clone() - rhs - } - } -} - -impl + Clone> std::ops::Div for State { - type Output = T; - - /// Divides the state by a value, returning the result. - fn div(self, rhs: T) -> Self::Output { - unsafe { - let lhs = &mut *self.0; - lhs.clone() / rhs - } - } -} - -impl std::ops::AddAssign for State { - /// Adds a value to the state in place. - fn add_assign(&mut self, rhs: T) { - unsafe { *self.0 += rhs } - } -} - -impl std::ops::SubAssign for State { - /// Subtracts a value from the state in place. - fn sub_assign(&mut self, rhs: T) { - unsafe { *self.0 -= rhs } - } -} - -impl std::ops::Deref for State { - type Target = T; - - /// Dereferences the state to access the underlying value. - fn deref(&self) -> &Self::Target { - unsafe { &*self.0 } - } -} - -impl std::ops::DerefMut for State { - /// Dereferences the state to access the underlying mutable value. - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { &mut *self.0 } - } -} - -impl State { - /// Returns a copy of the current state. - pub fn copy_state(&self) -> Self { - Self(self.0) - } -} - -impl> PartialEq for State { - /// Compares the state with another value for equality. - fn eq(&self, other: &T) -> bool { - unsafe { &*self.0 == other } - } -} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 3158a5d..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! The `utils` module provides utility functions for terminal manipulation -//! and string operations, designed to enhance terminal-based applications. - -use std::io::Write; - -/// Clears the terminal screen and moves the cursor to the top-left corner. -/// -/// # Returns -/// A `crate::Result<()>` indicating whether the operation succeeded. -/// -/// # Example -/// ``` -/// utils::clear().unwrap(); -/// ``` -pub fn clear() -> crate::Result<()> { - print!("\x1B[2J\x1B[H"); - std::io::stdout().flush() -} - -/// Hides the terminal cursor. -/// -/// # Returns -/// A `crate::Result<()>` indicating whether the operation succeeded. -/// -/// # Example -/// ``` -/// utils::hide_cursor().unwrap(); -/// ``` -pub fn hide_cursor() -> crate::Result<()> { - print!("\x1b[?25l"); - std::io::stdout().flush() -} - -/// Shows the terminal cursor. -/// -/// # Returns -/// A `crate::Result<()>` indicating whether the operation succeeded. -/// -/// # Example -/// ``` -/// utils::show_cursor().unwrap(); -/// ``` -pub fn show_cursor() -> crate::Result<()> { - print!("\x1B[?25h"); - std::io::stdout().flush() -} - -/// Flushes the terminal's stdout buffer. -/// -/// # Returns -/// A `crate::Result<()>` indicating whether the operation succeeded. -/// -/// # Example -/// ``` -/// utils::flush().unwrap(); -/// ``` -pub fn flush() -> crate::Result<()> { - std::io::stdout().flush() -} - -/// Calculates the width and height of a string when rendered in a terminal. -/// -/// # Arguments -/// - `s`: The input string. -/// -/// # Returns -/// A tuple `(u16, u16)` where: -/// - The first value is the maximum width of the string in characters. -/// - The second value is the height of the string in lines. -/// -/// # Example -/// ``` -/// let (width, height) = utils::str_size("Hello\nWorld!"); -/// assert_eq!((5, 2), (width, height)); -/// ``` -pub fn str_size(s: &str) -> (u16, u16) { - let mut height = 1; - let mut max_width = 0; - let mut current_width = 0; - - for b in s.bytes() { - if b == b'\n' { - height += 1; - max_width = max_width.max(current_width); - current_width = 0; - } else { - current_width += 1; - } - } - - max_width = max_width.max(current_width); - - (max_width, height) -}