tui-react-0.20.0/.cargo_vcs_info.json0000644000000001470000000000100130120ustar { "git": { "sha1": "6cc634618e1549fcece860b7b43f5d7ee5d1d259" }, "path_in_vcs": "tui-react" }tui-react-0.20.0/.gitignore000064400000000000000000000000141046102023000135630ustar 00000000000000/Cargo.lock tui-react-0.20.0/CHANGELOG.md000064400000000000000000000074771046102023000134300ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 0.20.0 (2023-05-11) ### Chore (BREAKING) - switch to 'ratatui' in a fashion that leaves imports alone. However would still be a breaking change for anyone leaving `tui` in their dependency tree. ### Commit Statistics - 3 commits contributed to the release. - 241 days passed between releases. - 1 commit was understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' were seen in commit messages ### Commit Details
view details * **Uncategorized** - Merge branch 'ratatui-2' ([`948b539`](https://github.com/Byron/tui-crates/commit/948b5397c0b8317e857f79515165c04314b6838a)) - Further updates to `crossterm` and `termion` ([`3157b3b`](https://github.com/Byron/tui-crates/commit/3157b3bf392b18f0f6be7e2c04d0b8782d76314e)) - Switch to 'ratatui' in a fashion that leaves imports alone. ([`073005d`](https://github.com/Byron/tui-crates/commit/073005de8c9177248528a4cc9be58d6cee525394))
## 0.19.0 (2022-09-12) Upgrade to tui 0.19. ### Commit Statistics - 3 commits contributed to the release. - 231 days passed between releases. - 0 commits were understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' were seen in commit messages ### Commit Details
view details * **Uncategorized** - Release tui-react v0.19.0 ([`6296315`](https://github.com/Byron/tui-crates/commit/629631545c7137c4071df1d895dee6e547e4d149)) - Prepare changelog prior to release ([`2539f6e`](https://github.com/Byron/tui-crates/commit/2539f6e1a28d038523e342810b94770e57021cd2)) - Upgrade to tui 19 ([`1b2afad`](https://github.com/Byron/tui-crates/commit/1b2afadba7064febd0d2dffc68f82419f9a4510b))
## 0.17.0 (2022-01-23) ### New Features (BREAKING) - upgrade to tui 0.17 ### Commit Statistics - 3 commits contributed to the release. - 171 days passed between releases. - 1 commit was understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' were seen in commit messages ### Commit Details
view details * **Uncategorized** - Release tui-react v0.17.0 ([`4523678`](https://github.com/Byron/tui-crates/commit/4523678efbc9c876e46325682861c27ee5e7fb02)) - Add changelog ([`221474b`](https://github.com/Byron/tui-crates/commit/221474b379a2c3c2f0c45dff5471659413bb9548)) - Upgrade to tui 0.17 ([`b5dd733`](https://github.com/Byron/tui-crates/commit/b5dd73380c316d1722cc70ec4220d6ea9a7bf141))
## v0.16.0 (2021-08-04) ### Commit Statistics - 3 commits contributed to the release over the course of 93 calendar days. - 0 commits were understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' were seen in commit messages ### Commit Details
view details * **Uncategorized** - Upgrade to tui 0.16 ([`907a5e5`](https://github.com/Byron/tui-crates/commit/907a5e591688732c3f88821daebc7ff8c010730b)) - Changes repository paths in tui crates manifests ([`ac5c6e6`](https://github.com/Byron/tui-crates/commit/ac5c6e62c86189a72e2305d06da176821f88b180)) - Add tui crates ([`ccb6a24`](https://github.com/Byron/tui-crates/commit/ccb6a24315a7d881e50b24e98d4720406bff16d5))
tui-react-0.20.0/Cargo.toml0000644000000017660000000000100110200ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "tui-react" version = "0.20.0" authors = ["Sebastian Thiel "] description = "TUI widgets using a react-like paradigm, allowing mutable component state and render properties." readme = "README.md" license = "MIT" repository = "https://github.com/Byron/tui-crates" [dependencies.log] version = "0.4.6" [dependencies.tui] version = "0.20.1" default-features = false package = "ratatui" [dependencies.unicode-segmentation] version = "1.6.0" [dependencies.unicode-width] version = "0.1.7" tui-react-0.20.0/Cargo.toml.orig000064400000000000000000000007431046102023000144730ustar 00000000000000[package] name = "tui-react" version = "0.20.0" authors = ["Sebastian Thiel "] edition = "2018" repository = "https://github.com/Byron/tui-crates" description = "TUI widgets using a react-like paradigm, allowing mutable component state and render properties." readme = "README.md" license = "MIT" [dependencies] tui = { package = "ratatui", version = "0.20.1", default-features = false } log = "0.4.6" unicode-segmentation = "1.6.0" unicode-width = "0.1.7" tui-react-0.20.0/LICENSE000064400000000000000000000020601046102023000126030ustar 00000000000000MIT License Copyright (c) 2019 Sebastian Thiel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tui-react-0.20.0/README.md000064400000000000000000000040031046102023000130540ustar 00000000000000**tui-react** is a library to enable components with state, and with properties provided per render Please note that this crate is early in development and build for the needs of **dua**. ### How it works It uses the TUI infrastructure Terminal, but alters it to not enforce implementing the `Widget` trait. It provides only a single, optional, trait called `TopLevelComponent`, which makes it convenient to draw its implementors with `Terminal::render(..)`. However, since this enforces the absence of refernces in your state, it's probably not suitable for most. Instead, any struct can implement `render` methods or functions, and freely write into the terminal. That way, one can leverage everything Rust has to offer, which allows stateful components which work in your favor. Thus, this crate does away with 'one size fits all' render implementations, greatly adding to flexibility. State that one wants within the component for instance could be the scoll location. Alternatively, one can configure windows by altering their public state. ### What's the relation to TUI / Ratatui? This project couldn't exist without TUI, and is happy to provide an alternative set of components for use in command-line applications. Ratatui is the continuation of the tui project. ### Why `tui-react`? I kept having a terrible feeling when managing state with tui widgets when writing **dua**, and after trying many things, I realized what the problem actually was. It took me some time to realize it's not possible to have stateful components in with TUI, and I admire the smarts that went into the API design! After all, it effectively prohibited this! Amazing! That's why I implemented my own terminal and some key components, based on the ones provided by TUI, which can serve as standard building blocks in a stateful world. Thus far, the experience was fantastic, and it feels much better than before. Let's see what happens with it. ### Changelog #### v0.4.1 - Simplify `block_width(…)` function #### v0.2.1 - add license file to crate tui-react-0.20.0/src/lib.rs000064400000000000000000000105421046102023000135050ustar 00000000000000#![forbid(unsafe_code)] mod list; mod terminal; pub use list::*; pub use terminal::*; use std::iter::repeat; use tui::{self, buffer::Buffer, layout::Rect, style::Color, style::Style}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; pub fn fill_background_to_right(mut s: String, entire_width: u16) -> String { match (s.len(), entire_width as usize) { (x, y) if x >= y => s, (x, y) => { s.extend(repeat(' ').take(y - x)); s } } } /// Helper method to quickly set the background of all cells inside the specified area. pub fn fill_background(area: Rect, buf: &mut Buffer, color: Color) { for y in area.top()..area.bottom() { for x in area.left()..area.right() { buf.get_mut(x, y).set_bg(color); } } } pub fn draw_text_with_ellipsis_nowrap( bound: Rect, buf: &mut Buffer, text: impl AsRef, style: impl Into>, ) -> u16 { let s = style.into(); let t = text.as_ref(); let mut graphemes = t.graphemes(true); let mut total_width = 0; { let mut ellipsis_candidate_x = None; let mut x_offset = 0; for (g, mut x) in graphemes.by_ref().zip(bound.left()..bound.right()) { let width = g.width(); total_width += width; x += x_offset; let cell = buf.get_mut(x, bound.y); if x + 1 == bound.right() { ellipsis_candidate_x = Some(x); } cell.symbol = g.into(); if let Some(s) = s { cell.set_style(s); } x_offset += width.saturating_sub(1) as u16; if x + x_offset >= bound.right() { break; } let x = x as usize; for x in x + 1..x + width { let i = buf.index_of(x as u16, bound.y); buf.content[i].reset(); } } if let (Some(_), Some(x)) = (graphemes.next(), ellipsis_candidate_x) { buf.get_mut(x, bound.y).symbol = "…".into(); } } total_width as u16 } pub fn draw_text_nowrap_fn( bound: Rect, buf: &mut Buffer, t: impl AsRef, mut s: impl FnMut(&str, u16, u16) -> Style, ) { if bound.width == 0 { return; } for (g, x) in t.as_ref().graphemes(true).zip(bound.left()..bound.right()) { let cell = buf.get_mut(x, bound.y); cell.symbol = g.into(); cell.set_style(s(&cell.symbol, x, bound.y)); } } pub mod util { use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; pub fn sanitize_offset(offset: u16, num_items: usize, num_displayable_lines: u16) -> u16 { offset.min((num_items.saturating_sub(num_displayable_lines as usize)) as u16) } #[derive(Default)] pub struct GraphemeCountWriter(pub usize); impl std::io::Write for GraphemeCountWriter { fn write(&mut self, buf: &[u8]) -> Result { self.0 += String::from_utf8_lossy(buf).graphemes(true).count(); Ok(buf.len()) } fn flush(&mut self) -> Result<(), std::io::Error> { Ok(()) } } pub fn block_width(s: &str) -> u16 { s.width() as u16 } pub mod rect { use tui::layout::Rect; /// A safe version of Rect::intersection that doesn't suffer from underflows pub fn intersect(lhs: Rect, rhs: Rect) -> Rect { let x1 = lhs.x.max(rhs.x); let y1 = lhs.y.max(rhs.y); let x2 = lhs.right().min(rhs.right()); let y2 = lhs.bottom().min(rhs.bottom()); Rect { x: x1, y: y1, width: x2.saturating_sub(x1), height: y2.saturating_sub(y1), } } pub fn offset_x(r: Rect, offset: u16) -> Rect { Rect { x: r.x + offset, width: r.width.saturating_sub(offset), ..r } } pub fn snap_to_right(bound: Rect, new_width: u16) -> Rect { offset_x(bound, bound.width.saturating_sub(new_width)) } pub fn line_bound(bound: Rect, line: usize) -> Rect { Rect { y: bound.y + line as u16, height: 1, ..bound } } } } tui-react-0.20.0/src/list.rs000064400000000000000000000037001046102023000137100ustar 00000000000000use tui::{ buffer::Buffer, layout::Rect, text::{Span, Spans, Text}, widgets::{Block, Paragraph, Widget}, }; #[derive(Default)] pub struct List { /// The index at which the list last started. Used for scrolling pub offset: usize, } impl List { fn list_offset_for(&self, entry_in_view: Option, height: usize) -> usize { match entry_in_view { Some(pos) => match height as usize { h if self.offset + h - 1 < pos => pos - h + 1, _ if self.offset > pos => pos, _ => self.offset, }, None => 0, } } } #[derive(Default)] pub struct ListProps<'b> { pub block: Option>, pub entry_in_view: Option, } impl List { pub fn render<'a, 't>( &mut self, props: ListProps<'a>, items: impl IntoIterator>>, area: Rect, buf: &mut Buffer, ) { let ListProps { block, entry_in_view, } = props; let list_area = match block { Some(b) => { let inner_area = b.inner(area); b.render(area, buf); inner_area } None => area, }; self.offset = self.list_offset_for(entry_in_view, list_area.height as usize); if list_area.width < 1 || list_area.height < 1 { return; } for (i, vec_of_spans) in items .into_iter() .skip(self.offset) .enumerate() .take(list_area.height as usize) { let (x, y) = (list_area.left(), list_area.top() + i as u16); Paragraph::new(Text::from(Spans::from(vec_of_spans))).render( Rect { x, y, width: list_area.width, height: 1, }, buf, ); } } } tui-react-0.20.0/src/terminal.rs000064400000000000000000000116451046102023000145570ustar 00000000000000//! Derived from TUI-rs, license: MIT, Copyright (c) 2016 Florian Dehau use log::error; use std::{borrow::Borrow, io}; use tui::{backend::Backend, buffer::Buffer, layout::Rect}; /// A component meant to be rendered by `Terminal::render(...)`. /// All other components don't have to implement this trait, and instead /// provide a render method by convention, tuned towards their needs using whichever /// generic types or lifetimes they need. pub trait ToplevelComponent { type Props; fn render(&mut self, props: impl Borrow, area: Rect, buf: &mut Buffer); } #[derive(Debug)] pub struct Terminal where B: Backend, { pub backend: B, buffers: [Buffer; 2], current: usize, hidden_cursor: bool, known_size: Rect, } impl Drop for Terminal where B: Backend, { fn drop(&mut self) { // Attempt to restore the cursor state if self.hidden_cursor { if let Err(err) = self.show_cursor() { error!("Failed to show the cursor: {}", err); } } } } impl Terminal where B: Backend, { pub fn new(backend: B) -> io::Result> { let size = backend.size()?; Ok(Terminal { backend, buffers: [Buffer::empty(size), Buffer::empty(size)], current: 0, hidden_cursor: false, known_size: size, }) } pub fn current_buffer_mut(&mut self) -> &mut Buffer { &mut self.buffers[self.current] } pub fn reconcile_and_flush(&mut self) -> io::Result<()> { let previous_buffer = &self.buffers[1 - self.current]; let current_buffer = &self.buffers[self.current]; let updates = previous_buffer.diff(current_buffer); self.backend.draw(updates.into_iter()) } pub fn resize(&mut self, area: Rect) -> io::Result<()> { self.buffers[self.current].resize(area); self.buffers[1 - self.current].reset(); self.buffers[1 - self.current].resize(area); self.known_size = area; self.backend.clear() } pub fn autoresize(&mut self) -> io::Result<()> { let size = self.size()?; if self.known_size != size { self.resize(size)?; } Ok(()) } /// Get ready for rendering and return the maximum display size as `Rect` pub fn pre_render(&mut self) -> io::Result { // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets // and the terminal (if growing), which may OOB. self.autoresize()?; Ok(self.known_size) } pub fn post_render(&mut self) -> io::Result<()> { self.reconcile_and_flush()?; self.buffers[1 - self.current].reset(); self.current = 1 - self.current; self.backend.flush()?; Ok(()) } pub fn render(&mut self, component: &mut C, props: impl Borrow) -> io::Result<()> where C: ToplevelComponent, { self.pre_render()?; component.render(props, self.known_size, self.current_buffer_mut()); self.post_render() } pub fn hide_cursor(&mut self) -> io::Result<()> { self.backend.hide_cursor()?; self.hidden_cursor = true; Ok(()) } pub fn show_cursor(&mut self) -> io::Result<()> { self.backend.show_cursor()?; self.hidden_cursor = false; Ok(()) } pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> { self.backend.get_cursor() } pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { self.backend.set_cursor(x, y) } pub fn clear(&mut self) -> io::Result<()> { self.backend.clear() } pub fn size(&self) -> io::Result { self.backend.size() } } #[cfg(test)] mod tests { use super::*; use tui::backend::TestBackend; #[derive(Default, Clone)] struct ComplexProps { x: usize, y: String, } #[derive(Default)] struct StatefulComponent { x: usize, } #[derive(Default)] struct StatelessComponent; impl ToplevelComponent for StatefulComponent { type Props = usize; fn render(&mut self, props: impl Borrow, _area: Rect, _buf: &mut Buffer) { self.x += *props.borrow(); } } impl ToplevelComponent for StatelessComponent { type Props = ComplexProps; fn render(&mut self, _props: impl Borrow, _area: Rect, _buf: &mut Buffer) { // does not matter - we want to see it compiles essentially } } #[test] fn it_does_render_with_simple_and_complex_props() { let mut term = Terminal::new(TestBackend::new(20, 20)).unwrap(); let mut c = StatefulComponent::default(); term.render(&mut c, 3usize).ok(); assert_eq!(c.x, 3); let mut c = StatelessComponent::default(); term.render(&mut c, ComplexProps::default()).ok(); } }