diff --git a/Cargo.toml b/Cargo.toml index 34e715cc..336ae4c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,54 +1,41 @@ -[package] -name = "ratatui" -version = "0.28.1" # crate version -authors = ["Florian Dehau ", "The Ratatui Developers"] -description = "A library that's all about cooking up terminal user interfaces" -documentation = "https://docs.rs/ratatui/latest/ratatui/" -repository = "https://github.com/ratatui/ratatui" -homepage = "https://ratatui.rs" -keywords = ["tui", "terminal", "dashboard"] -categories = ["command-line-interface"] -readme = "README.md" -license = "MIT" -exclude = [ - "assets/*", - ".github", - "Makefile.toml", - "CONTRIBUTING.md", - "*.log", - "tags", -] -edition = "2021" -rust-version = "1.74.0" +[workspace] +resolver = "2" +members = ["ratatui*"] +# disabled for now because of the orphan rule on conversions +# +exclude = ["ratatui-termion", "ratatui-termwiz"] + +[workspace.dependencies] +ratatui = { path = "ratatui" } +ratatui-core = { path = "ratatui-core" } +ratatui-crossterm = { path = "ratatui-crossterm" } +ratatui-termion = { path = "ratatui-termion" } +ratatui-termwiz = { path = "ratatui-termwiz" } +ratatui-widgets = { path = "ratatui-widgets" } -[dependencies] bitflags = "2.3" cassowary = "0.3" compact_str = "0.8.0" -crossterm = { version = "0.28.1", optional = true } -document-features = { version = "0.2.7", optional = true } +crossterm = { version = "0.28.1" } +document-features = { version = "0.2.7" } instability = "0.3.1" itertools = "0.13" lru = "0.12.0" paste = "1.0.2" -palette = { version = "0.7.6", optional = true } -serde = { version = "1", optional = true, features = ["derive"] } +palette = { version = "0.7.6" } +serde = { version = "1", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] } -termwiz = { version = "0.22.0", optional = true } -time = { version = "0.3.11", optional = true, features = ["local-offset"] } +termion = { version = "4.0.0" } +termwiz = { version = "0.22.0" } +time = { version = "0.3.11", features = ["local-offset"] } unicode-segmentation = "1.10" unicode-truncate = "1" unicode-width = "=0.1.13" -[target.'cfg(not(windows))'.dependencies] -# termion is not supported on Windows -termion = { version = "4.0.0", optional = true } - -[dev-dependencies] +# dev-dependencies argh = "0.1.12" color-eyre = "0.6.2" criterion = { version = "0.5.1", features = ["html_reports"] } -crossterm = { version = "0.28.1", features = ["event-stream"] } fakeit = "1.1" font8x8 = "0.3.1" futures = "0.3.30" @@ -68,313 +55,3 @@ tokio = { version = "1.39.2", features = [ tracing = "0.1.40" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -cargo = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } -cast_possible_truncation = "allow" -cast_possible_wrap = "allow" -cast_precision_loss = "allow" -cast_sign_loss = "allow" -missing_errors_doc = "allow" -missing_panics_doc = "allow" -module_name_repetitions = "allow" -must_use_candidate = "allow" - -# we often split up a module into multiple files with the main type in a file named after the -# module, so we want to allow this pattern -module_inception = "allow" - -# nursery or restricted -as_underscore = "warn" -deref_by_slicing = "warn" -else_if_without_else = "warn" -empty_line_after_doc_comments = "warn" -equatable_if_let = "warn" -fn_to_numeric_cast_any = "warn" -format_push_string = "warn" -map_err_ignore = "warn" -missing_const_for_fn = "warn" -mixed_read_write_in_expression = "warn" -mod_module_files = "warn" -needless_pass_by_ref_mut = "warn" -needless_raw_strings = "warn" -or_fun_call = "warn" -redundant_type_annotations = "warn" -rest_pat_in_fully_bound_structs = "warn" -string_lit_chars_any = "warn" -string_slice = "warn" -string_to_string = "warn" -unnecessary_self_imports = "warn" -use_self = "warn" - -[features] -#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file. -#! -## By default, we enable the crossterm backend as this is a reasonable choice for most applications -## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature -## which allows you to set the underline color of text. -default = ["crossterm", "underline-color"] -#! Generally an application will only use one backend, so you should only enable one of the following features: -## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`]. -crossterm = ["dep:crossterm"] -## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`]. -termion = ["dep:termion"] -## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`]. -termwiz = ["dep:termwiz"] - -#! The following optional features are available for all backends: -## enables serialization and deserialization of style and color types using the [`serde`] crate. -## This is useful if you want to save themes to a file. -serde = ["dep:serde", "bitflags/serde", "compact_str/serde"] - -## enables the [`border!`] macro. -macros = [] - -## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color). -palette = ["dep:palette"] - -## enables all widgets. -all-widgets = ["widget-calendar"] - -#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive -#! dependencies. The available features are: -## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`]. -widget-calendar = ["dep:time"] - -#! The following optional features are only available for some backends: - -## enables the backend code that sets the underline color. -## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend, -## and is not supported on Windows 7. -underline-color = ["dep:crossterm"] - -#! The following features are unstable and may change in the future: - -## Enable all unstable features. -unstable = [ - "unstable-rendered-line-info", - "unstable-widget-ref", - "unstable-backend-writer", -] - -## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count) -## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods -## which are experimental and may change in the future. -## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details. -unstable-rendered-line-info = [] - -## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in -## the future. -unstable-widget-ref = [] - -## Enables getting access to backends' writers. -unstable-backend-writer = [] - -[package.metadata.docs.rs] -all-features = true -# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html -cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] -rustdoc-args = ["--cfg", "docsrs"] - -# Improve benchmark consistency -[profile.bench] -codegen-units = 1 -lto = true - -[lib] -bench = false - -[[bench]] -name = "main" -harness = false - -[[example]] -name = "async" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "barchart" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "barchart-grouped" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "block" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "calendar" -required-features = ["crossterm", "widget-calendar"] -doc-scrape-examples = true - -[[example]] -name = "canvas" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "chart" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "colors" -required-features = ["crossterm"] -# this example is a bit verbose, so we don't want to include it in the docs -doc-scrape-examples = false - -[[example]] -name = "colors_rgb" -required-features = ["crossterm", "palette"] -doc-scrape-examples = true - -[[example]] -name = "constraint-explorer" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "constraints" -required-features = ["crossterm"] -doc-scrape-examples = false - -[[example]] -name = "custom_widget" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "demo" -# this runs for all of the terminal backends, so it can't be built using --all-features or scraped -doc-scrape-examples = false - -[[example]] -name = "demo2" -required-features = ["crossterm", "palette", "widget-calendar"] -doc-scrape-examples = true - -[[example]] -name = "docsrs" -required-features = ["crossterm"] -doc-scrape-examples = false - -[[example]] -name = "flex" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "gauge" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "hello_world" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "inline" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "layout" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "line_gauge" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "hyperlink" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "list" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "minimal" -required-features = ["crossterm"] -# prefer to show the more featureful examples in the docs -doc-scrape-examples = false - -[[example]] -name = "modifiers" -required-features = ["crossterm"] -# this example is a bit verbose, so we don't want to include it in the docs -doc-scrape-examples = false - -[[example]] -name = "panic" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "paragraph" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "popup" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "ratatui-logo" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "scrollbar" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "sparkline" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "table" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "tabs" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "tracing" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "user_input" -required-features = ["crossterm"] -doc-scrape-examples = true - -[[example]] -name = "widget_impl" -required-features = ["crossterm", "unstable-widget-ref"] -doc-scrape-examples = true - -[[test]] -name = "state_serde" -required-features = ["serde"] diff --git a/ratatui-core/Cargo.toml b/ratatui-core/Cargo.toml new file mode 100644 index 00000000..aaf57b2c --- /dev/null +++ b/ratatui-core/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "ratatui-core" +version = "0.1.0" +edition = "2021" + +[features] +underline-color = [] +unstable-widget-ref = [] + +[dependencies] +bitflags.workspace = true +cassowary.workspace = true +compact_str.workspace = true +document-features.workspace = true +instability.workspace = true +itertools.workspace = true +lru.workspace = true +paste.workspace = true +palette = { workspace = true, optional = true } +serde = { workspace = true, optional = true, features = ["derive"] } +strum = { workspace = true, features = ["derive"] } +termwiz = { workspace = true, optional = true } +unicode-segmentation.workspace = true +unicode-truncate.workspace = true +unicode-width.workspace = true + +[dev-dependencies] +argh = "0.1.12" +color-eyre = "0.6.2" +criterion = { version = "0.5.1", features = ["html_reports"] } +crossterm = { version = "0.28.1", features = ["event-stream"] } +fakeit = "1.1" +font8x8 = "0.3.1" +futures = "0.3.30" +indoc = "2" +octocrab = "0.40.0" +pretty_assertions = "1.4.0" +rand = "0.8.5" +rand_chacha = "0.3.1" +rstest = "0.22.0" +serde_json = "1.0.109" +tokio = { version = "1.39.2", features = [ + "rt", + "macros", + "time", + "rt-multi-thread", +] } +tracing = "0.1.40" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/src/backend.rs b/ratatui-core/src/backend.rs similarity index 97% rename from src/backend.rs rename to ratatui-core/src/backend.rs index f8497b0b..2912905d 100644 --- a/src/backend.rs +++ b/ratatui-core/src/backend.rs @@ -109,21 +109,6 @@ use crate::{ layout::{Position, Size}, }; -#[cfg(all(not(windows), feature = "termion"))] -mod termion; -#[cfg(all(not(windows), feature = "termion"))] -pub use self::termion::TermionBackend; - -#[cfg(feature = "crossterm")] -mod crossterm; -#[cfg(feature = "crossterm")] -pub use self::crossterm::CrosstermBackend; - -#[cfg(feature = "termwiz")] -mod termwiz; -#[cfg(feature = "termwiz")] -pub use self::termwiz::TermwizBackend; - mod test; pub use self::test::TestBackend; diff --git a/src/backend/test.rs b/ratatui-core/src/backend/test.rs similarity index 100% rename from src/backend/test.rs rename to ratatui-core/src/backend/test.rs diff --git a/src/buffer.rs b/ratatui-core/src/buffer.rs similarity index 100% rename from src/buffer.rs rename to ratatui-core/src/buffer.rs diff --git a/src/buffer/assert.rs b/ratatui-core/src/buffer/assert.rs similarity index 100% rename from src/buffer/assert.rs rename to ratatui-core/src/buffer/assert.rs diff --git a/src/buffer/buffer.rs b/ratatui-core/src/buffer/buffer.rs similarity index 100% rename from src/buffer/buffer.rs rename to ratatui-core/src/buffer/buffer.rs diff --git a/src/buffer/cell.rs b/ratatui-core/src/buffer/cell.rs similarity index 100% rename from src/buffer/cell.rs rename to ratatui-core/src/buffer/cell.rs diff --git a/src/layout.rs b/ratatui-core/src/layout.rs similarity index 100% rename from src/layout.rs rename to ratatui-core/src/layout.rs diff --git a/src/layout/alignment.rs b/ratatui-core/src/layout/alignment.rs similarity index 100% rename from src/layout/alignment.rs rename to ratatui-core/src/layout/alignment.rs diff --git a/src/layout/constraint.rs b/ratatui-core/src/layout/constraint.rs similarity index 100% rename from src/layout/constraint.rs rename to ratatui-core/src/layout/constraint.rs diff --git a/src/layout/direction.rs b/ratatui-core/src/layout/direction.rs similarity index 100% rename from src/layout/direction.rs rename to ratatui-core/src/layout/direction.rs diff --git a/src/layout/flex.rs b/ratatui-core/src/layout/flex.rs similarity index 100% rename from src/layout/flex.rs rename to ratatui-core/src/layout/flex.rs diff --git a/src/layout/layout.rs b/ratatui-core/src/layout/layout.rs similarity index 99% rename from src/layout/layout.rs rename to ratatui-core/src/layout/layout.rs index ae44182a..22ed4f67 100644 --- a/src/layout/layout.rs +++ b/ratatui-core/src/layout/layout.rs @@ -1295,7 +1295,7 @@ mod tests { use crate::{ layout::flex::Flex, prelude::{Constraint::*, *}, - widgets::Paragraph, + // widgets::Paragraph, // TODO }; /// Test that the given constraints applied to the given area result in the expected layout. @@ -1317,7 +1317,7 @@ mod tests { let mut buffer = Buffer::empty(area); for (c, &area) in ('a'..='z').take(constraints.len()).zip(layout.iter()) { let s = c.to_string().repeat(area.width as usize); - Paragraph::new(s).render(area, &mut buffer); + // Paragraph::new(s).render(area, &mut buffer); // TODO } assert_eq!(buffer, Buffer::with_lines([expected])); } diff --git a/src/layout/margin.rs b/ratatui-core/src/layout/margin.rs similarity index 100% rename from src/layout/margin.rs rename to ratatui-core/src/layout/margin.rs diff --git a/src/layout/position.rs b/ratatui-core/src/layout/position.rs similarity index 100% rename from src/layout/position.rs rename to ratatui-core/src/layout/position.rs diff --git a/src/layout/rect.rs b/ratatui-core/src/layout/rect.rs similarity index 100% rename from src/layout/rect.rs rename to ratatui-core/src/layout/rect.rs diff --git a/src/layout/rect/iter.rs b/ratatui-core/src/layout/rect/iter.rs similarity index 100% rename from src/layout/rect/iter.rs rename to ratatui-core/src/layout/rect/iter.rs diff --git a/src/layout/size.rs b/ratatui-core/src/layout/size.rs similarity index 100% rename from src/layout/size.rs rename to ratatui-core/src/layout/size.rs diff --git a/src/lib.rs b/ratatui-core/src/lib.rs similarity index 100% rename from src/lib.rs rename to ratatui-core/src/lib.rs diff --git a/ratatui-core/src/prelude.rs b/ratatui-core/src/prelude.rs new file mode 100644 index 00000000..5d9d45a0 --- /dev/null +++ b/ratatui-core/src/prelude.rs @@ -0,0 +1,41 @@ +//! A prelude for conveniently writing applications using this library. +//! +//! ```rust,no_run +//! use ratatui::prelude::*; +//! ``` +//! +//! Aside from the main types that are used in the library, this prelude also re-exports several +//! modules to make it easy to qualify types that would otherwise collide. E.g.: +//! +//! ```rust +//! use ratatui::{prelude::*, widgets::*}; +//! +//! #[derive(Debug, Default, PartialEq, Eq)] +//! struct Line; +//! +//! assert_eq!(Line::default(), Line); +//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![])); +//! ``` + +// TODO: re-export the following modules: +// #[cfg(feature = "crossterm")] +// pub use crate::backend::CrosstermBackend; +// #[cfg(all(not(windows), feature = "termion"))] +// pub use crate::backend::TermionBackend; +// #[cfg(feature = "termwiz")] +// pub use crate::backend::TermwizBackend; +pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef}; +pub use crate::{ + backend::{self, Backend}, + buffer::{self, Buffer}, + layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size}, + style::{self, Color, Modifier, Style, Stylize}, + symbols::{self}, + text::{self, Line, Masked, Span, Text}, + widgets::{ + // block::BlockExt, // TODO + StatefulWidget, + Widget, + }, + Frame, Terminal, +}; diff --git a/src/style.rs b/ratatui-core/src/style.rs similarity index 100% rename from src/style.rs rename to ratatui-core/src/style.rs diff --git a/src/style/color.rs b/ratatui-core/src/style/color.rs similarity index 100% rename from src/style/color.rs rename to ratatui-core/src/style/color.rs diff --git a/src/style/palette.rs b/ratatui-core/src/style/palette.rs similarity index 100% rename from src/style/palette.rs rename to ratatui-core/src/style/palette.rs diff --git a/src/style/palette/material.rs b/ratatui-core/src/style/palette/material.rs similarity index 100% rename from src/style/palette/material.rs rename to ratatui-core/src/style/palette/material.rs diff --git a/src/style/palette/tailwind.rs b/ratatui-core/src/style/palette/tailwind.rs similarity index 100% rename from src/style/palette/tailwind.rs rename to ratatui-core/src/style/palette/tailwind.rs diff --git a/src/style/palette_conversion.rs b/ratatui-core/src/style/palette_conversion.rs similarity index 100% rename from src/style/palette_conversion.rs rename to ratatui-core/src/style/palette_conversion.rs diff --git a/src/style/stylize.rs b/ratatui-core/src/style/stylize.rs similarity index 100% rename from src/style/stylize.rs rename to ratatui-core/src/style/stylize.rs diff --git a/src/symbols.rs b/ratatui-core/src/symbols.rs similarity index 100% rename from src/symbols.rs rename to ratatui-core/src/symbols.rs diff --git a/src/symbols/border.rs b/ratatui-core/src/symbols/border.rs similarity index 100% rename from src/symbols/border.rs rename to ratatui-core/src/symbols/border.rs diff --git a/src/symbols/line.rs b/ratatui-core/src/symbols/line.rs similarity index 100% rename from src/symbols/line.rs rename to ratatui-core/src/symbols/line.rs diff --git a/src/terminal.rs b/ratatui-core/src/terminal.rs similarity index 87% rename from src/terminal.rs rename to ratatui-core/src/terminal.rs index d379f4a2..da9e6d38 100644 --- a/src/terminal.rs +++ b/ratatui-core/src/terminal.rs @@ -32,15 +32,9 @@ //! [`Buffer`]: crate::buffer::Buffer mod frame; -#[cfg(feature = "crossterm")] -mod init; mod terminal; mod viewport; pub use frame::{CompletedFrame, Frame}; -#[cfg(feature = "crossterm")] -pub use init::{ - init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal, -}; pub use terminal::{Options as TerminalOptions, Terminal}; pub use viewport::Viewport; diff --git a/src/terminal/frame.rs b/ratatui-core/src/terminal/frame.rs similarity index 100% rename from src/terminal/frame.rs rename to ratatui-core/src/terminal/frame.rs diff --git a/src/terminal/terminal.rs b/ratatui-core/src/terminal/terminal.rs similarity index 100% rename from src/terminal/terminal.rs rename to ratatui-core/src/terminal/terminal.rs diff --git a/src/terminal/viewport.rs b/ratatui-core/src/terminal/viewport.rs similarity index 100% rename from src/terminal/viewport.rs rename to ratatui-core/src/terminal/viewport.rs diff --git a/src/text.rs b/ratatui-core/src/text.rs similarity index 100% rename from src/text.rs rename to ratatui-core/src/text.rs diff --git a/src/text/grapheme.rs b/ratatui-core/src/text/grapheme.rs similarity index 97% rename from src/text/grapheme.rs rename to ratatui-core/src/text/grapheme.rs index 7a14ca6a..5e23e08f 100644 --- a/src/text/grapheme.rs +++ b/ratatui-core/src/text/grapheme.rs @@ -26,7 +26,7 @@ impl<'a> StyledGrapheme<'a> { } } - pub(crate) fn is_whitespace(&self) -> bool { + pub fn is_whitespace(&self) -> bool { let symbol = self.symbol; symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP } diff --git a/src/text/line.rs b/ratatui-core/src/text/line.rs similarity index 100% rename from src/text/line.rs rename to ratatui-core/src/text/line.rs diff --git a/src/text/masked.rs b/ratatui-core/src/text/masked.rs similarity index 100% rename from src/text/masked.rs rename to ratatui-core/src/text/masked.rs diff --git a/src/text/span.rs b/ratatui-core/src/text/span.rs similarity index 100% rename from src/text/span.rs rename to ratatui-core/src/text/span.rs diff --git a/src/text/text.rs b/ratatui-core/src/text/text.rs similarity index 100% rename from src/text/text.rs rename to ratatui-core/src/text/text.rs diff --git a/src/widgets.rs b/ratatui-core/src/widgets.rs similarity index 97% rename from src/widgets.rs rename to ratatui-core/src/widgets.rs index 99ebd124..40a1c414 100644 --- a/src/widgets.rs +++ b/ratatui-core/src/widgets.rs @@ -21,37 +21,7 @@ //! - [`Tabs`]: displays a tab bar and allows selection. //! //! [`Canvas`]: crate::widgets::canvas::Canvas -mod barchart; -pub mod block; -mod borders; -#[cfg(feature = "widget-calendar")] -pub mod calendar; -pub mod canvas; -mod chart; -mod clear; -mod gauge; -mod list; -mod paragraph; -mod reflow; -mod scrollbar; -mod sparkline; -mod table; -mod tabs; -pub use self::{ - barchart::{Bar, BarChart, BarGroup}, - block::{Block, BorderType, Padding}, - borders::*, - chart::{Axis, Chart, Dataset, GraphType, LegendPosition}, - clear::Clear, - gauge::{Gauge, LineGauge}, - list::{List, ListDirection, ListItem, ListState}, - paragraph::{Paragraph, Wrap}, - scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState}, - sparkline::{RenderDirection, Sparkline}, - table::{Cell, HighlightSpacing, Row, Table, TableState}, - tabs::Tabs, -}; use crate::{buffer::Buffer, layout::Rect, style::Style}; /// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`]. diff --git a/ratatui-crossterm/Cargo.toml b/ratatui-crossterm/Cargo.toml new file mode 100644 index 00000000..b2d040f5 --- /dev/null +++ b/ratatui-crossterm/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ratatui-crossterm" +version = "0.1.0" +edition = "2021" + +[features] +default = ["underline-color"] + +## enables the backend code that sets the underline color. +## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend, +## and is not supported on Windows 7. +underline-color = [] + +[dependencies] +ratatui-core = { workspace = true } +crossterm.workspace = true +instability.workspace = true + +[dev-dependencies] +rstest.workspace = true diff --git a/ratatui-crossterm/src/lib.rs b/ratatui-crossterm/src/lib.rs new file mode 100644 index 00000000..efef078f --- /dev/null +++ b/ratatui-crossterm/src/lib.rs @@ -0,0 +1,681 @@ +//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses +//! the [Crossterm] crate to interact with the terminal. +//! +//! [Crossterm]: https://crates.io/crates/crossterm +use std::io::{self, Write}; + +#[cfg(feature = "underline-color")] +use crossterm::style::SetUnderlineColor; +use crossterm::{ + cursor::{Hide, MoveTo, Show}, + execute, queue, + style::{ + Attribute as CrosstermAttribute, Attributes as CrosstermAttributes, + Color as CrosstermColor, Colors, ContentStyle, Print, SetAttribute, SetBackgroundColor, + SetColors, SetForegroundColor, + }, + terminal::{self, Clear}, +}; +use ratatui_core::{ + backend::{Backend, ClearType, WindowSize}, + buffer::Cell, + layout::{Position, Size}, + style::{Color, Modifier, Style}, +}; + +/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal. +/// +/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is +/// used to send commands to the terminal. It provides methods for drawing content, manipulating +/// the cursor, and clearing the terminal screen. +/// +/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead +/// use the [`Terminal`] struct, which provides a more ergonomic interface. +/// +/// Usually applications will enable raw mode and switch to alternate screen mode after creating +/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and +/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions +/// when the application exits). This is not done automatically by the backend because it is +/// possible that the application may want to use the terminal for other purposes (like showing +/// help text) before entering alternate screen mode. +/// +/// # Example +/// +/// ```rust,no_run +/// use std::io::{stderr, stdout}; +/// +/// use ratatui::{ +/// crossterm::{ +/// terminal::{ +/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +/// }, +/// ExecutableCommand, +/// }, +/// prelude::*, +/// }; +/// +/// let mut backend = CrosstermBackend::new(stdout()); +/// // or +/// let backend = CrosstermBackend::new(stderr()); +/// let mut terminal = Terminal::new(backend)?; +/// +/// enable_raw_mode()?; +/// stdout().execute(EnterAlternateScreen)?; +/// +/// terminal.clear()?; +/// terminal.draw(|frame| { +/// // -- snip -- +/// })?; +/// +/// stdout().execute(LeaveAlternateScreen)?; +/// disable_raw_mode()?; +/// +/// # std::io::Result::Ok(()) +/// ``` +/// +/// See the the [Examples] directory for more examples. See the [`backend`] module documentation +/// for more details on raw mode and alternate screen. +/// +/// [`Write`]: std::io::Write +/// [`Terminal`]: crate::terminal::Terminal +/// [`backend`]: crate::backend +/// [Crossterm]: https://crates.io/crates/crossterm +/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct CrosstermBackend { + /// The writer used to send commands to the terminal. + writer: W, +} + +impl CrosstermBackend +where + W: Write, +{ + /// Creates a new `CrosstermBackend` with the given writer. + /// + /// Most applications will use either [`stdout`](std::io::stdout) or + /// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use. + /// + /// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr + /// + /// # Example + /// + /// ```rust,no_run + /// # use std::io::stdout; + /// # use ratatui::prelude::*; + /// let backend = CrosstermBackend::new(stdout()); + /// ``` + pub const fn new(writer: W) -> Self { + Self { writer } + } + + /// Gets the writer. + #[instability::unstable( + feature = "backend-writer", + issue = "https://github.com/ratatui/ratatui/pull/991" + )] + pub const fn writer(&self) -> &W { + &self.writer + } + + /// Gets the writer as a mutable reference. + /// + /// Note: writing to the writer may cause incorrect output after the write. This is due to the + /// way that the Terminal implements diffing Buffers. + #[instability::unstable( + feature = "backend-writer", + issue = "https://github.com/ratatui/ratatui/pull/991" + )] + pub fn writer_mut(&mut self) -> &mut W { + &mut self.writer + } +} + +impl Write for CrosstermBackend +where + W: Write, +{ + /// Writes a buffer of bytes to the underlying buffer. + fn write(&mut self, buf: &[u8]) -> io::Result { + self.writer.write(buf) + } + + /// Flushes the underlying buffer. + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +impl Backend for CrosstermBackend +where + W: Write, +{ + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + let mut fg = Color::Reset; + let mut bg = Color::Reset; + #[cfg(feature = "underline-color")] + let mut underline_color = Color::Reset; + let mut modifier = Modifier::empty(); + let mut last_pos: Option = None; + for (x, y, cell) in content { + // Move the cursor if the previous location was not (x - 1, y) + if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) { + queue!(self.writer, MoveTo(x, y))?; + } + last_pos = Some(Position { x, y }); + if cell.modifier != modifier { + let diff = ModifierDiff { + from: modifier, + to: cell.modifier, + }; + diff.queue(&mut self.writer)?; + modifier = cell.modifier; + } + if cell.fg != fg || cell.bg != bg { + queue!( + self.writer, + SetColors(Colors::new( + from_ratatui_color(cell.fg), + from_ratatui_color(cell.bg) + )) + )?; + fg = cell.fg; + bg = cell.bg; + } + #[cfg(feature = "underline-color")] + if cell.underline_color != underline_color { + let color = from_ratatui_color(cell.underline_color); + queue!(self.writer, SetUnderlineColor(color))?; + underline_color = cell.underline_color; + } + + queue!(self.writer, Print(cell.symbol()))?; + } + + #[cfg(feature = "underline-color")] + return queue!( + self.writer, + SetForegroundColor(CrosstermColor::Reset), + SetBackgroundColor(CrosstermColor::Reset), + SetUnderlineColor(CrosstermColor::Reset), + SetAttribute(CrosstermAttribute::Reset), + ); + #[cfg(not(feature = "underline-color"))] + return queue!( + self.writer, + SetForegroundColor(CrosstermColor::Reset), + SetBackgroundColor(CrosstermColor::Reset), + SetAttribute(CrosstermAttribute::Reset), + ); + } + + fn hide_cursor(&mut self) -> io::Result<()> { + execute!(self.writer, Hide) + } + + fn show_cursor(&mut self) -> io::Result<()> { + execute!(self.writer, Show) + } + + fn get_cursor_position(&mut self) -> io::Result { + crossterm::cursor::position() + .map(|(x, y)| Position { x, y }) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) + } + + fn set_cursor_position>(&mut self, position: P) -> io::Result<()> { + let Position { x, y } = position.into(); + execute!(self.writer, MoveTo(x, y)) + } + + fn clear(&mut self) -> io::Result<()> { + self.clear_region(ClearType::All) + } + + fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> { + execute!( + self.writer, + Clear(match clear_type { + ClearType::All => crossterm::terminal::ClearType::All, + ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown, + ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp, + ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine, + ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine, + }) + ) + } + + fn append_lines(&mut self, n: u16) -> io::Result<()> { + for _ in 0..n { + queue!(self.writer, Print("\n"))?; + } + self.writer.flush() + } + + fn size(&self) -> io::Result { + let (width, height) = terminal::size()?; + Ok(Size { width, height }) + } + + fn window_size(&mut self) -> io::Result { + let crossterm::terminal::WindowSize { + columns, + rows, + width, + height, + } = terminal::window_size()?; + Ok(WindowSize { + columns_rows: Size { + width: columns, + height: rows, + }, + pixels: Size { width, height }, + }) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +fn from_ratatui_color(color: Color) -> CrosstermColor { + match color { + Color::Reset => CrosstermColor::Reset, + Color::Black => CrosstermColor::Black, + Color::Red => CrosstermColor::DarkRed, + Color::Green => CrosstermColor::DarkGreen, + Color::Yellow => CrosstermColor::DarkYellow, + Color::Blue => CrosstermColor::DarkBlue, + Color::Magenta => CrosstermColor::DarkMagenta, + Color::Cyan => CrosstermColor::DarkCyan, + Color::Gray => CrosstermColor::Grey, + Color::DarkGray => CrosstermColor::DarkGrey, + Color::LightRed => CrosstermColor::Red, + Color::LightGreen => CrosstermColor::Green, + Color::LightBlue => CrosstermColor::Blue, + Color::LightYellow => CrosstermColor::Yellow, + Color::LightMagenta => CrosstermColor::Magenta, + Color::LightCyan => CrosstermColor::Cyan, + Color::White => CrosstermColor::White, + Color::Indexed(i) => CrosstermColor::AnsiValue(i), + Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b }, + } +} + +fn from_crossterm_color(value: CrosstermColor) -> Color { + match value { + CrosstermColor::Reset => Color::Reset, + CrosstermColor::Black => Color::Black, + CrosstermColor::DarkRed => Color::Red, + CrosstermColor::DarkGreen => Color::Green, + CrosstermColor::DarkYellow => Color::Yellow, + CrosstermColor::DarkBlue => Color::Blue, + CrosstermColor::DarkMagenta => Color::Magenta, + CrosstermColor::DarkCyan => Color::Cyan, + CrosstermColor::Grey => Color::Gray, + CrosstermColor::DarkGrey => Color::DarkGray, + CrosstermColor::Red => Color::LightRed, + CrosstermColor::Green => Color::LightGreen, + CrosstermColor::Blue => Color::LightBlue, + CrosstermColor::Yellow => Color::LightYellow, + CrosstermColor::Magenta => Color::LightMagenta, + CrosstermColor::Cyan => Color::LightCyan, + CrosstermColor::White => Color::White, + CrosstermColor::Rgb { r, g, b } => Color::Rgb(r, g, b), + CrosstermColor::AnsiValue(v) => Color::Indexed(v), + } +} + +/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier` +/// values. This is useful when updating the terminal display, as it allows for more +/// efficient updates by only sending the necessary changes. +struct ModifierDiff { + pub from: Modifier, + pub to: Modifier, +} + +impl ModifierDiff { + fn queue(self, mut w: W) -> io::Result<()> + where + W: io::Write, + { + //use crossterm::Attribute; + let removed = self.from - self.to; + if removed.contains(Modifier::REVERSED) { + queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?; + } + if removed.contains(Modifier::BOLD) { + queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?; + if self.to.contains(Modifier::DIM) { + queue!(w, SetAttribute(CrosstermAttribute::Dim))?; + } + } + if removed.contains(Modifier::ITALIC) { + queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?; + } + if removed.contains(Modifier::UNDERLINED) { + queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?; + } + if removed.contains(Modifier::DIM) { + queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?; + } + if removed.contains(Modifier::CROSSED_OUT) { + queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?; + } + if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { + queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?; + } + + let added = self.to - self.from; + if added.contains(Modifier::REVERSED) { + queue!(w, SetAttribute(CrosstermAttribute::Reverse))?; + } + if added.contains(Modifier::BOLD) { + queue!(w, SetAttribute(CrosstermAttribute::Bold))?; + } + if added.contains(Modifier::ITALIC) { + queue!(w, SetAttribute(CrosstermAttribute::Italic))?; + } + if added.contains(Modifier::UNDERLINED) { + queue!(w, SetAttribute(CrosstermAttribute::Underlined))?; + } + if added.contains(Modifier::DIM) { + queue!(w, SetAttribute(CrosstermAttribute::Dim))?; + } + if added.contains(Modifier::CROSSED_OUT) { + queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?; + } + if added.contains(Modifier::SLOW_BLINK) { + queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?; + } + if added.contains(Modifier::RAPID_BLINK) { + queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?; + } + + Ok(()) + } +} + +fn from_crossterm_attribute(value: CrosstermAttribute) -> Modifier { + // `Attribute*s*` (note the *s*) contains multiple `Attribute` + // We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing + // the conversion again + from_crossterm_attributes(CrosstermAttributes::from(value)) +} + +fn from_crossterm_attributes(value: CrosstermAttributes) -> Modifier { + let mut res = Modifier::empty(); + + if value.has(CrosstermAttribute::Bold) { + res |= Modifier::BOLD; + } + if value.has(CrosstermAttribute::Dim) { + res |= Modifier::DIM; + } + if value.has(CrosstermAttribute::Italic) { + res |= Modifier::ITALIC; + } + if value.has(CrosstermAttribute::Underlined) + || value.has(CrosstermAttribute::DoubleUnderlined) + || value.has(CrosstermAttribute::Undercurled) + || value.has(CrosstermAttribute::Underdotted) + || value.has(CrosstermAttribute::Underdashed) + { + res |= Modifier::UNDERLINED; + } + if value.has(CrosstermAttribute::SlowBlink) { + res |= Modifier::SLOW_BLINK; + } + if value.has(CrosstermAttribute::RapidBlink) { + res |= Modifier::RAPID_BLINK; + } + if value.has(CrosstermAttribute::Reverse) { + res |= Modifier::REVERSED; + } + if value.has(CrosstermAttribute::Hidden) { + res |= Modifier::HIDDEN; + } + if value.has(CrosstermAttribute::CrossedOut) { + res |= Modifier::CROSSED_OUT; + } + + res +} + +fn from_crossterm_style(value: ContentStyle) -> Style { + let mut sub_modifier = Modifier::empty(); + + if value.attributes.has(CrosstermAttribute::NoBold) { + sub_modifier |= Modifier::BOLD; + } + if value.attributes.has(CrosstermAttribute::NoItalic) { + sub_modifier |= Modifier::ITALIC; + } + if value.attributes.has(CrosstermAttribute::NotCrossedOut) { + sub_modifier |= Modifier::CROSSED_OUT; + } + if value.attributes.has(CrosstermAttribute::NoUnderline) { + sub_modifier |= Modifier::UNDERLINED; + } + if value.attributes.has(CrosstermAttribute::NoHidden) { + sub_modifier |= Modifier::HIDDEN; + } + if value.attributes.has(CrosstermAttribute::NoBlink) { + sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK; + } + if value.attributes.has(CrosstermAttribute::NoReverse) { + sub_modifier |= Modifier::REVERSED; + } + + Style { + fg: value.foreground_color.map(from_crossterm_color), + bg: value.background_color.map(from_crossterm_color), + #[cfg(feature = "underline-color")] + underline_color: value.underline_color.map(from_crossterm_color), + add_modifier: from_crossterm_attributes(value.attributes), + sub_modifier, + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(CrosstermColor::Reset, Color::Reset)] + #[case(CrosstermColor::Black, Color::Black)] + #[case(CrosstermColor::DarkGrey, Color::DarkGray)] + #[case(CrosstermColor::Red, Color::LightRed)] + #[case(CrosstermColor::DarkRed, Color::Red)] + #[case(CrosstermColor::Green, Color::LightGreen)] + #[case(CrosstermColor::DarkGreen, Color::Green)] + #[case(CrosstermColor::Yellow, Color::LightYellow)] + #[case(CrosstermColor::DarkYellow, Color::Yellow)] + #[case(CrosstermColor::Blue, Color::LightBlue)] + #[case(CrosstermColor::DarkBlue, Color::Blue)] + #[case(CrosstermColor::Magenta, Color::LightMagenta)] + #[case(CrosstermColor::DarkMagenta, Color::Magenta)] + #[case(CrosstermColor::Cyan, Color::LightCyan)] + #[case(CrosstermColor::DarkCyan, Color::Cyan)] + #[case(CrosstermColor::White, Color::White)] + #[case(CrosstermColor::Grey, Color::Gray)] + #[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0))] + #[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30))] + #[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))] + #[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))] + fn convert_from_crossterm_color(#[case] value: CrosstermColor, #[case] expected: Color) { + assert_eq!(from_crossterm_color(value), expected); + } + + mod modifier { + use super::*; + + #[rstest] + #[rstest] + #[case(CrosstermAttribute::Reset, Modifier::empty())] + #[case(CrosstermAttribute::Bold, Modifier::BOLD)] + #[case(CrosstermAttribute::Italic, Modifier::ITALIC)] + #[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)] + #[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)] + #[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)] + #[case(CrosstermAttribute::Dim, Modifier::DIM)] + #[case(CrosstermAttribute::NormalIntensity, Modifier::empty())] + #[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)] + #[case(CrosstermAttribute::NoUnderline, Modifier::empty())] + #[case(CrosstermAttribute::OverLined, Modifier::empty())] + #[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)] + #[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)] + #[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)] + #[case(CrosstermAttribute::NoHidden, Modifier::empty())] + #[case(CrosstermAttribute::Reverse, Modifier::REVERSED)] + fn convert_from_crossterm_attribute( + #[case] value: CrosstermAttribute, + #[case] expected: Modifier, + ) { + assert_eq!(from_crossterm_attribute(value), expected); + } + + #[rstest] + #[case(&[CrosstermAttribute::Bold], Modifier::BOLD)] + #[case( + &[CrosstermAttribute::Bold, CrosstermAttribute::Italic], + Modifier::BOLD | Modifier::ITALIC + )] + #[case( + &[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut], + Modifier::BOLD + )] + #[case( + &[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted], + Modifier::DIM | Modifier::UNDERLINED + )] + #[case( + &[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic], + Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC + )] + #[case( + &[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut], + Modifier::HIDDEN + )] + #[case( + &[CrosstermAttribute::Reverse], + Modifier::REVERSED + )] + #[case( + &[CrosstermAttribute::Reset], + Modifier::empty() + )] + #[case( + &[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut], + Modifier::RAPID_BLINK | Modifier::CROSSED_OUT + )] + #[case( + &[CrosstermAttribute::DoubleUnderlined, CrosstermAttribute::OverLined], + Modifier::UNDERLINED + )] + #[case( + &[CrosstermAttribute::Undercurled, CrosstermAttribute::Underdashed], + Modifier::UNDERLINED + )] + #[case( + &[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic], + Modifier::empty() + )] + #[case( + &[CrosstermAttribute::NoBlink, CrosstermAttribute::NoReverse], + Modifier::empty() + )] + fn convert_from_crossterm_attributes( + #[case] value: &[CrosstermAttribute], + #[case] expected: Modifier, + ) { + assert_eq!( + from_crossterm_attributes(CrosstermAttributes::from(value)), + expected + ); + } + } + + #[rstest] + #[case(ContentStyle::default(), Style::default())] + #[case( + ContentStyle { + foreground_color: Some(CrosstermColor::DarkYellow), + ..Default::default() + }, + Style::default().fg(Color::Yellow) + )] + #[case( + ContentStyle { + background_color: Some(CrosstermColor::DarkYellow), + ..Default::default() + }, + Style::default().bg(Color::Yellow) + )] + #[case( + ContentStyle { + attributes: CrosstermAttributes::from(CrosstermAttribute::Bold), + ..Default::default() + }, + Style::default().add_modifier(Modifier::BOLD) + )] + #[case( + ContentStyle { + attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold), + ..Default::default() + }, + Style::default().remove_modifier(Modifier::BOLD) + )] + #[case( + ContentStyle { + attributes: CrosstermAttributes::from(CrosstermAttribute::Italic), + ..Default::default() + }, + Style::default().add_modifier(Modifier::ITALIC) + )] + #[case( + ContentStyle { + attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic), + ..Default::default() + }, + Style::default().remove_modifier(Modifier::ITALIC) + )] + #[case( + ContentStyle { + attributes: CrosstermAttributes::from( + [CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref() + ), + ..Default::default() + }, + Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::ITALIC) + )] + #[case( + ContentStyle { + attributes: CrosstermAttributes::from( + [CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref() + ), + ..Default::default() + }, + Style::default() + .remove_modifier(Modifier::BOLD) + .remove_modifier(Modifier::ITALIC) + )] + #[cfg(feature = "underline-color")] + #[case( + ContentStyle { + underline_color: Some(CrosstermColor::DarkRed), + ..Default::default() + }, + Style::default().underline_color(Color::Red) + )] + fn convert_from_crossterm_content_style(#[case] value: ContentStyle, #[case] expected: Style) { + assert_eq!(from_crossterm_style(value), expected); + } +} diff --git a/ratatui-termion/Cargo.toml b/ratatui-termion/Cargo.toml new file mode 100644 index 00000000..004ac3b6 --- /dev/null +++ b/ratatui-termion/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ratatui-termion" +version = "0.1.0" +edition = "2021" + +[dependencies] +instability = { workspace = true } +ratatui-core = { workspace = true } +termion.workspace = true diff --git a/src/backend/termion.rs b/ratatui-termion/src/lib.rs similarity index 99% rename from src/backend/termion.rs rename to ratatui-termion/src/lib.rs index bd7577e6..73e0b7b6 100644 --- a/src/backend/termion.rs +++ b/ratatui-termion/src/lib.rs @@ -9,13 +9,13 @@ use std::{ io::{self, Write}, }; -use crate::{ +use ratatui_core::{ backend::{Backend, ClearType, WindowSize}, buffer::Cell, layout::{Position, Size}, style::{Color, Modifier, Style}, - termion::{self, color as tcolor, color::Color as _, style as tstyle}, }; +use termion::{color as tcolor, color::Color as _, style as tstyle}; /// A [`Backend`] implementation that uses [Termion] to render to the terminal. /// diff --git a/ratatui-termwiz/Cargo.toml b/ratatui-termwiz/Cargo.toml new file mode 100644 index 00000000..a308c9fd --- /dev/null +++ b/ratatui-termwiz/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ratatui-termwiz" +version = "0.1.0" +edition = "2021" + +[dependencies] +ratatui-core = { workspace = true } diff --git a/src/backend/termwiz.rs b/ratatui-termwiz/src/lib.rs similarity index 100% rename from src/backend/termwiz.rs rename to ratatui-termwiz/src/lib.rs diff --git a/ratatui-widgets/Cargo.toml b/ratatui-widgets/Cargo.toml new file mode 100644 index 00000000..76014196 --- /dev/null +++ b/ratatui-widgets/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "ratatui-widgets" +version = "0.1.0" +edition = "2021" + +[features] +# TODO: remove unstable-widget-ref, consider whether to keep all-widgets +default = ["all-widgets", "unstable-widget-ref"] + +## enables serialization and deserialization of style and color types using the [`serde`] crate. +## This is useful if you want to save themes to a file. +serde = ["dep:serde", "bitflags/serde"] + + +## enables all widgets. +all-widgets = ["widget-calendar"] + +#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive +#! dependencies. The available features are: +## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`]. +widget-calendar = ["dep:time"] + +unstable-widget-ref = ["ratatui-core/unstable-widget-ref"] + +[dependencies] +bitflags.workspace = true +instability.workspace = true +itertools.workspace = true +ratatui-core.workspace = true +serde = { workspace = true, optional = true } +strum.workspace = true +time = { workspace = true, optional = true } +unicode-segmentation.workspace = true +unicode-width.workspace = true + +[dev-dependencies] +rstest.workspace = true +indoc.workspace = true +pretty_assertions.workspace = true +time.workspace = true diff --git a/src/widgets/barchart.rs b/ratatui-widgets/src/barchart.rs similarity index 99% rename from src/widgets/barchart.rs rename to ratatui-widgets/src/barchart.rs index 9cb68c94..4364dd27 100644 --- a/src/widgets/barchart.rs +++ b/ratatui-widgets/src/barchart.rs @@ -1,4 +1,8 @@ -use crate::{prelude::*, style::Styled, widgets::Block}; +use ratatui_core::{ + prelude::{symbols, Buffer, Direction, Line, Rect, Style, Stylize, Widget}, + style::Styled, + widgets::WidgetRef, +}; mod bar; mod bar_group; @@ -6,6 +10,8 @@ mod bar_group; pub use bar::Bar; pub use bar_group::BarGroup; +use crate::{block::BlockExt, Block}; + /// A chart showing values as [bars](Bar). /// /// Here is a possible `BarChart` output. @@ -613,9 +619,14 @@ impl<'a> Styled for BarChart<'a> { #[cfg(test)] mod tests { use itertools::iproduct; + use ratatui_core::{ + layout::Alignment, + style::{Color, Modifier}, + text::Span, + }; use super::*; - use crate::widgets::BorderType; + use crate::BorderType; #[test] fn default() { diff --git a/src/widgets/barchart/bar.rs b/ratatui-widgets/src/barchart/bar.rs similarity index 96% rename from src/widgets/barchart/bar.rs rename to ratatui-widgets/src/barchart/bar.rs index 499aac46..fd09713b 100644 --- a/src/widgets/barchart/bar.rs +++ b/ratatui-widgets/src/barchart/bar.rs @@ -1,8 +1,7 @@ +use ratatui_core::prelude::{Buffer, Line, Rect, Style, Widget}; use unicode_width::UnicodeWidthStr; -use crate::prelude::*; - -/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget. +/// A bar to be shown by the [`BarChart`](crate::BarChart) widget. /// /// Here is an explanation of a `Bar`'s components. /// ```plain @@ -61,7 +60,7 @@ impl<'a> Bar<'a> { /// display the label **under** the bar. /// For [`Horizontal`](crate::layout::Direction::Horizontal) bars, /// display the label **in** the bar. - /// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction. + /// See [`BarChart::direction`](crate::BarChart::direction) to set the direction. #[must_use = "method moves the value of self and returns the modified value"] pub fn label(mut self, label: Line<'a>) -> Self { self.label = Some(label); diff --git a/src/widgets/barchart/bar_group.rs b/ratatui-widgets/src/barchart/bar_group.rs similarity index 98% rename from src/widgets/barchart/bar_group.rs rename to ratatui-widgets/src/barchart/bar_group.rs index 6e934871..702b7042 100644 --- a/src/widgets/barchart/bar_group.rs +++ b/ratatui-widgets/src/barchart/bar_group.rs @@ -1,5 +1,6 @@ +use ratatui_core::prelude::*; + use super::Bar; -use crate::prelude::*; /// A group of bars to be shown by the Barchart. /// diff --git a/src/widgets/block.rs b/ratatui-widgets/src/block.rs similarity index 99% rename from src/widgets/block.rs rename to ratatui-widgets/src/block.rs index 1027a606..e10283d2 100644 --- a/src/widgets/block.rs +++ b/ratatui-widgets/src/block.rs @@ -6,15 +6,23 @@ //! [title](Block::title) and [padding](Block::padding). use itertools::Itertools; +use ratatui_core::{ + prelude::{Alignment, Buffer, Line, Rect, Style, Stylize, Widget}, + style::Styled, + symbols::border, + widgets::WidgetRef, +}; use strum::{Display, EnumString}; -use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders}; +use self::title::Position; mod padding; pub mod title; pub use padding::Padding; -pub use title::{Position, Title}; +pub use title::Title; + +use crate::Borders; /// Base widget to be used to display a box border around all [upper level ones](crate::widgets). /// @@ -495,7 +503,7 @@ impl<'a> Block<'a> { /// .style(Style::new().white().not_bold()); // will be white, and italic /// ``` /// - /// [`Paragraph`]: crate::widgets::Paragraph + /// [`Paragraph`]: crate::Paragraph #[must_use = "method moves the value of self and returns the modified value"] pub fn style>(mut self, style: S) -> Self { self.style = style.into(); @@ -986,6 +994,7 @@ impl<'a> Styled for Block<'a> { #[cfg(test)] mod tests { + use ratatui_core::style::{Color, Modifier}; use rstest::rstest; use strum::ParseError; @@ -1244,7 +1253,7 @@ mod tests { // .border_style(_DEFAULT_STYLE) // no longer const // .title_style(_DEFAULT_STYLE) // no longer const .title_alignment(Alignment::Left) - .title_position(Position::Top) + .title_position(crate::block::Position::Top) .padding(_DEFAULT_PADDING); } diff --git a/src/widgets/block/padding.rs b/ratatui-widgets/src/block/padding.rs similarity index 98% rename from src/widgets/block/padding.rs rename to ratatui-widgets/src/block/padding.rs index 2c5c8939..a87d4afe 100644 --- a/src/widgets/block/padding.rs +++ b/ratatui-widgets/src/block/padding.rs @@ -19,8 +19,8 @@ /// Padding::symmetric(5, 6); /// ``` /// -/// [`Block`]: crate::widgets::Block -/// [`padding`]: crate::widgets::Block::padding +/// [`Block`]: crate::Block +/// [`padding`]: crate::Block::padding /// [CSS padding]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Padding { diff --git a/src/widgets/block/title.rs b/ratatui-widgets/src/block/title.rs similarity index 88% rename from src/widgets/block/title.rs rename to ratatui-widgets/src/block/title.rs index 863aaad3..3ee4c6c8 100644 --- a/src/widgets/block/title.rs +++ b/ratatui-widgets/src/block/title.rs @@ -1,11 +1,10 @@ //! This module holds the [`Title`] element and its related configuration types. -//! A title is a piece of [`Block`](crate::widgets::Block) configuration. +//! A title is a piece of [`Block`](crate::Block) configuration. +use ratatui_core::{layout::Alignment, text::Line}; use strum::{Display, EnumString}; -use crate::{layout::Alignment, text::Line}; - -/// A [`Block`](crate::widgets::Block) title. +/// A [`Block`](crate::Block) title. /// /// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]). /// @@ -17,8 +16,8 @@ use crate::{layout::Alignment, text::Line}; /// . /// /// Use [`Line`] instead, when the position is not defined as part of the title. When a specific -/// position is needed, use [`Block::title_top`](crate::widgets::Block::title_top) or -/// [`Block::title_bottom`](crate::widgets::Block::title_bottom) instead. +/// position is needed, use [`Block::title_top`](crate::Block::title_top) or +/// [`Block::title_bottom`](crate::Block::title_bottom) instead. /// /// # Example /// @@ -64,19 +63,19 @@ pub struct Title<'a> { /// Title alignment /// /// If [`None`], defaults to the alignment defined with - /// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated - /// [`Block`](crate::widgets::Block). + /// [`Block::title_alignment`](crate::Block::title_alignment) in the associated + /// [`Block`](crate::Block). pub alignment: Option, /// Title position /// /// If [`None`], defaults to the position defined with - /// [`Block::title_position`](crate::widgets::Block::title_position) in the associated - /// [`Block`](crate::widgets::Block). + /// [`Block::title_position`](crate::Block::title_position) in the associated + /// [`Block`](crate::Block). pub position: Option, } -/// Defines the [title](crate::widgets::block::Title) position. +/// Defines the [title](crate::block::Title) position. /// /// The title can be positioned on top or at the bottom of the block. /// Defaults to [`Position::Top`]. diff --git a/src/widgets/borders.rs b/ratatui-widgets/src/borders.rs similarity index 98% rename from src/widgets/borders.rs rename to ratatui-widgets/src/borders.rs index a801ccf8..c079cd1a 100644 --- a/src/widgets/borders.rs +++ b/ratatui-widgets/src/borders.rs @@ -55,7 +55,7 @@ impl fmt::Debug for Borders { /// and RIGHT. /// /// When used with NONE you should consider omitting this completely. For ALL you should consider -/// [`Block::bordered()`](crate::widgets::Block::bordered) instead. +/// [`Block::bordered()`](crate::Block::bordered) instead. /// /// ## Examples /// diff --git a/src/widgets/calendar.rs b/ratatui-widgets/src/calendar.rs similarity index 98% rename from src/widgets/calendar.rs rename to ratatui-widgets/src/calendar.rs index 2e1e49e7..8e9423a5 100644 --- a/src/widgets/calendar.rs +++ b/ratatui-widgets/src/calendar.rs @@ -10,9 +10,13 @@ //! [`Monthly`] has several controls for what should be displayed use std::collections::HashMap; +use ratatui_core::{ + prelude::{Alignment, Buffer, Color, Constraint, Layout, Line, Rect, Span, Style, Widget}, + widgets::WidgetRef, +}; use time::{Date, Duration, OffsetDateTime}; -use crate::{prelude::*, widgets::Block}; +use crate::{block::BlockExt, Block}; /// Display a month calendar for the month containing `display_date` #[derive(Debug, Clone, Eq, PartialEq, Hash)] diff --git a/src/widgets/canvas.rs b/ratatui-widgets/src/canvas.rs similarity index 99% rename from src/widgets/canvas.rs rename to ratatui-widgets/src/canvas.rs index b8a903be..f4065bb8 100644 --- a/src/widgets/canvas.rs +++ b/ratatui-widgets/src/canvas.rs @@ -22,6 +22,12 @@ mod world; use std::{fmt, iter::zip}; use itertools::Itertools; +use ratatui_core::{ + prelude::{Buffer, Color, Rect, Style, Widget}, + symbols::{self, Marker}, + text::Line as TextLine, + widgets::WidgetRef, +}; pub use self::{ circle::Circle, @@ -30,7 +36,7 @@ pub use self::{ points::Points, rectangle::Rectangle, }; -use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block}; +use crate::block::{Block, BlockExt}; /// Something that can be drawn on a [`Canvas`]. /// @@ -425,7 +431,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> { /// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of /// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal. /// -/// [`Frame`]: crate::prelude::Frame +/// [`Frame`]: ratatui_core::prelude::Frame #[derive(Debug)] pub struct Context<'a> { x_bounds: [f64; 2], @@ -809,9 +815,9 @@ where #[cfg(test)] mod tests { use indoc::indoc; + use ratatui_core::buffer::Cell; use super::*; - use crate::buffer::Cell; // helper to test the canvas checks that drawing a vertical and horizontal line // results in the expected output diff --git a/src/widgets/canvas/circle.rs b/ratatui-widgets/src/canvas/circle.rs similarity index 85% rename from src/widgets/canvas/circle.rs rename to ratatui-widgets/src/canvas/circle.rs index 3f3570f1..144281a2 100644 --- a/src/widgets/canvas/circle.rs +++ b/ratatui-widgets/src/canvas/circle.rs @@ -1,7 +1,6 @@ -use crate::{ - style::Color, - widgets::canvas::{Painter, Shape}, -}; +use ratatui_core::style::Color; + +use crate::canvas::{Painter, Shape}; /// A circle with a given center and radius and with a given color #[derive(Debug, Default, Clone, PartialEq)] @@ -31,17 +30,12 @@ impl Shape for Circle { #[cfg(test)] mod tests { - use crate::{ - buffer::Buffer, - layout::Rect, - style::Color, - symbols::Marker, - widgets::{ - canvas::{Canvas, Circle}, - Widget, - }, + use ratatui_core::{ + buffer::Buffer, layout::Rect, style::Color, symbols::Marker, widgets::Widget, }; + use crate::canvas::{Canvas, Circle}; + #[test] fn test_it_draws_a_circle() { let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5)); diff --git a/src/widgets/canvas/line.rs b/ratatui-widgets/src/canvas/line.rs similarity index 96% rename from src/widgets/canvas/line.rs rename to ratatui-widgets/src/canvas/line.rs index 1ea9529d..f5f2f788 100644 --- a/src/widgets/canvas/line.rs +++ b/ratatui-widgets/src/canvas/line.rs @@ -1,7 +1,6 @@ -use crate::{ - style::Color, - widgets::canvas::{Painter, Shape}, -}; +use ratatui_core::style::Color; + +use crate::canvas::{Painter, Shape}; /// A line from `(x1, y1)` to `(x2, y2)` with the given color #[derive(Debug, Default, Clone, PartialEq)] @@ -112,16 +111,18 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us #[cfg(test)] mod tests { - use rstest::rstest; - - use super::*; - use crate::{ + use ratatui_core::{ buffer::Buffer, layout::Rect, style::{Style, Stylize}, symbols::Marker, - widgets::{canvas::Canvas, Widget}, + text, + widgets::Widget, }; + use rstest::rstest; + + use super::*; + use crate::canvas::Canvas; #[rstest] #[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])] @@ -207,7 +208,7 @@ mod tests { fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines) where ExpectedLines: IntoIterator, - ExpectedLines::Item: Into>, + ExpectedLines::Item: Into>, { let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10)); let canvas = Canvas::default() diff --git a/src/widgets/canvas/map.rs b/ratatui-widgets/src/canvas/map.rs similarity index 98% rename from src/widgets/canvas/map.rs rename to ratatui-widgets/src/canvas/map.rs index f990d61f..dabfc6c5 100644 --- a/src/widgets/canvas/map.rs +++ b/ratatui-widgets/src/canvas/map.rs @@ -1,13 +1,10 @@ +use ratatui_core::style::Color; use strum::{Display, EnumString}; -use crate::{ - style::Color, - widgets::canvas::{ - world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION}, - Painter, Shape, - }, +use crate::canvas::{ + world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION}, + Painter, Shape, }; - /// Defines how many points are going to be used to draw a [`Map`]. /// /// You generally want a [high](MapResolution::High) resolution map. @@ -62,10 +59,14 @@ impl Shape for Map { #[cfg(test)] mod tests { + use ratatui_core::{ + prelude::{Buffer, Color, Rect, Widget}, + symbols::Marker, + }; use strum::ParseError; use super::*; - use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas}; + use crate::canvas::Canvas; #[test] fn map_resolution_to_string() { diff --git a/src/widgets/canvas/points.rs b/ratatui-widgets/src/canvas/points.rs similarity index 86% rename from src/widgets/canvas/points.rs rename to ratatui-widgets/src/canvas/points.rs index 9a0874aa..794080ec 100644 --- a/src/widgets/canvas/points.rs +++ b/ratatui-widgets/src/canvas/points.rs @@ -1,7 +1,6 @@ -use crate::{ - style::Color, - widgets::canvas::{Painter, Shape}, -}; +use ratatui_core::style::Color; + +use crate::canvas::{Painter, Shape}; /// A group of points with a given color #[derive(Debug, Default, Clone, PartialEq)] diff --git a/src/widgets/canvas/rectangle.rs b/ratatui-widgets/src/canvas/rectangle.rs similarity index 97% rename from src/widgets/canvas/rectangle.rs rename to ratatui-widgets/src/canvas/rectangle.rs index f2e2ffdf..39109b02 100644 --- a/src/widgets/canvas/rectangle.rs +++ b/ratatui-widgets/src/canvas/rectangle.rs @@ -1,7 +1,6 @@ -use crate::{ - style::Color, - widgets::canvas::{Line, Painter, Shape}, -}; +use ratatui_core::style::Color; + +use crate::canvas::{Line, Painter, Shape}; /// A rectangle to draw on a [`Canvas`](super::Canvas) /// @@ -65,8 +64,10 @@ impl Shape for Rectangle { #[cfg(test)] mod tests { + use ratatui_core::{prelude::*, symbols::Marker}; + use super::*; - use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas}; + use crate::canvas::Canvas; #[test] fn draw_block_lines() { diff --git a/src/widgets/canvas/world.rs b/ratatui-widgets/src/canvas/world.rs similarity index 100% rename from src/widgets/canvas/world.rs rename to ratatui-widgets/src/canvas/world.rs diff --git a/src/widgets/chart.rs b/ratatui-widgets/src/chart.rs similarity index 99% rename from src/widgets/chart.rs rename to ratatui-widgets/src/chart.rs index 6993f5ec..1e3a817e 100644 --- a/src/widgets/chart.rs +++ b/ratatui-widgets/src/chart.rs @@ -1,17 +1,13 @@ use std::{cmp::max, ops::Not}; +use ratatui_core::{layout::Flex, prelude::*, style::Styled, widgets::WidgetRef}; use strum::{Display, EnumString}; use crate::{ - layout::Flex, - prelude::*, - style::Styled, - widgets::{ - canvas::{Canvas, Line as CanvasLine, Points}, - Block, - }, + block::BlockExt, + canvas::{Canvas, Line as CanvasLine, Points}, + Block, }; - /// An X or Y axis for the [`Chart`] widget /// /// An axis can have a [title](Axis::title) which will be displayed at the end of the axis. For an @@ -1074,7 +1070,7 @@ impl WidgetRef for Chart<'_> { for (i, (dataset_name, dataset_style)) in self .datasets .iter() - .filter_map(|ds| Some((ds.name.as_ref()?, ds.style()))) + .filter_map(|ds| Some((ds.name.as_ref()?, ds.style))) .enumerate() { let name = dataset_name.clone().patch_style(dataset_style); diff --git a/src/widgets/clear.rs b/ratatui-widgets/src/clear.rs similarity index 97% rename from src/widgets/clear.rs rename to ratatui-widgets/src/clear.rs index dd544816..ae6e1d51 100644 --- a/src/widgets/clear.rs +++ b/ratatui-widgets/src/clear.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use ratatui_core::{prelude::*, widgets::WidgetRef}; /// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups). /// diff --git a/src/widgets/gauge.rs b/ratatui-widgets/src/gauge.rs similarity index 99% rename from src/widgets/gauge.rs rename to ratatui-widgets/src/gauge.rs index 19144565..a0b01c83 100644 --- a/src/widgets/gauge.rs +++ b/ratatui-widgets/src/gauge.rs @@ -1,4 +1,6 @@ -use crate::{prelude::*, style::Styled, widgets::Block}; +use ratatui_core::{prelude::*, style::Styled, widgets::WidgetRef}; + +use crate::{block::BlockExt, Block}; /// A widget to display a progress bar. /// diff --git a/ratatui-widgets/src/lib.rs b/ratatui-widgets/src/lib.rs new file mode 100644 index 00000000..2d1eece9 --- /dev/null +++ b/ratatui-widgets/src/lib.rs @@ -0,0 +1,31 @@ +mod barchart; +pub mod block; +mod borders; +#[cfg(feature = "widget-calendar")] +pub mod calendar; +pub mod canvas; +mod chart; +mod clear; +mod gauge; +mod list; +mod paragraph; +mod reflow; +mod scrollbar; +mod sparkline; +mod table; +mod tabs; + +pub use self::{ + barchart::{Bar, BarChart, BarGroup}, + block::{Block, BorderType, Padding}, + borders::*, + chart::{Axis, Chart, Dataset, GraphType, LegendPosition}, + clear::Clear, + gauge::{Gauge, LineGauge}, + list::{List, ListDirection, ListItem, ListState}, + paragraph::{Paragraph, Wrap}, + scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState}, + sparkline::{RenderDirection, Sparkline}, + table::{Cell, HighlightSpacing, Row, Table, TableState}, + tabs::Tabs, +}; diff --git a/src/widgets/list.rs b/ratatui-widgets/src/list.rs similarity index 100% rename from src/widgets/list.rs rename to ratatui-widgets/src/list.rs diff --git a/src/widgets/list/item.rs b/ratatui-widgets/src/list/item.rs similarity index 97% rename from src/widgets/list/item.rs rename to ratatui-widgets/src/list/item.rs index d4923de3..3ff9a209 100644 --- a/src/widgets/list/item.rs +++ b/ratatui-widgets/src/list/item.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use ratatui_core::prelude::*; /// A single item in a [`List`] /// @@ -55,7 +55,7 @@ use crate::prelude::*; /// ListItem::new(Text::from("foo").alignment(Alignment::Right)); /// ``` /// -/// [`List`]: crate::widgets::List +/// [`List`]: crate::List /// [`Stylize`]: crate::style::Stylize #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct ListItem<'a> { @@ -94,8 +94,8 @@ impl<'a> ListItem<'a> { /// /// # See also /// - /// - [`List::new`](crate::widgets::List::new) to create a list of items that can be converted - /// to [`ListItem`] + /// - [`List::new`](crate::List::new) to create a list of items that can be converted to + /// [`ListItem`] pub fn new(content: T) -> Self where T: Into>, @@ -132,7 +132,7 @@ impl<'a> ListItem<'a> { /// ``` /// /// [`Styled`]: crate::style::Styled - /// [`ListState`]: crate::widgets::list::ListState + /// [`ListState`]: crate::list::ListState #[must_use = "method moves the value of self and returns the modified value"] pub fn style>(mut self, style: S) -> Self { self.style = style.into(); diff --git a/src/widgets/list/list.rs b/ratatui-widgets/src/list/list.rs similarity index 98% rename from src/widgets/list/list.rs rename to ratatui-widgets/src/list/list.rs index 30e4f53c..0d680dcb 100644 --- a/src/widgets/list/list.rs +++ b/ratatui-widgets/src/list/list.rs @@ -1,11 +1,8 @@ +use ratatui_core::{prelude::Style, style::Styled}; use strum::{Display, EnumString}; use super::ListItem; -use crate::{ - prelude::*, - style::Styled, - widgets::{Block, HighlightSpacing}, -}; +use crate::{Block, HighlightSpacing}; /// A widget to display several items among which one can be selected (optional) /// @@ -15,7 +12,7 @@ use crate::{ /// the item's height is automatically determined. A `List` can also be put in reverse order (i.e. /// *bottom to top*) whereas a [`Table`] cannot. /// -/// [`Table`]: crate::widgets::Table +/// [`Table`]: crate::Table /// /// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`]. /// @@ -85,9 +82,9 @@ use crate::{ /// (0..5).map(|i| format!("Item{i}")).collect::(); /// ``` /// -/// [`ListState`]: crate::widgets::list::ListState -/// [scroll]: crate::widgets::list::ListState::offset -/// [select]: crate::widgets::list::ListState::select +/// [`ListState`]: crate::list::ListState +/// [scroll]: crate::list::ListState::offset +/// [select]: crate::list::ListState::select #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub struct List<'a> { /// An optional block to wrap the widget in @@ -421,6 +418,7 @@ where #[cfg(test)] mod tests { use pretty_assertions::assert_eq; + use ratatui_core::style::{Color, Modifier, Stylize}; use super::*; diff --git a/src/widgets/list/rendering.rs b/ratatui-widgets/src/list/rendering.rs similarity index 99% rename from src/widgets/list/rendering.rs rename to ratatui-widgets/src/list/rendering.rs index 2f7ca9cf..d5776a58 100644 --- a/src/widgets/list/rendering.rs +++ b/ratatui-widgets/src/list/rendering.rs @@ -1,9 +1,10 @@ +use ratatui_core::{ + prelude::{Buffer, Rect, StatefulWidget, Widget}, + widgets::{StatefulWidgetRef, WidgetRef}, +}; use unicode_width::UnicodeWidthStr; -use crate::{ - prelude::{Buffer, Rect, StatefulWidget, StatefulWidgetRef, Widget, WidgetRef}, - widgets::{block::BlockExt, List, ListDirection, ListState}, -}; +use crate::{block::BlockExt, List, ListDirection, ListState}; impl Widget for List<'_> { fn render(self, area: Rect, buf: &mut Buffer) { @@ -269,13 +270,11 @@ impl List<'_> { #[cfg(test)] mod tests { use pretty_assertions::assert_eq; + use ratatui_core::prelude::*; use rstest::{fixture, rstest}; use super::*; - use crate::{ - prelude::*, - widgets::{Block, HighlightSpacing, ListItem}, - }; + use crate::{Block, HighlightSpacing, ListItem}; #[fixture] fn single_line_buf() -> Buffer { diff --git a/src/widgets/list/state.rs b/ratatui-widgets/src/list/state.rs similarity index 99% rename from src/widgets/list/state.rs rename to ratatui-widgets/src/list/state.rs index a0df742a..f43c7282 100644 --- a/src/widgets/list/state.rs +++ b/ratatui-widgets/src/list/state.rs @@ -36,7 +36,7 @@ /// # } /// ``` /// -/// [`List`]: crate::widgets::List +/// [`List`]: crate::List #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ListState { @@ -258,7 +258,7 @@ impl ListState { mod tests { use pretty_assertions::assert_eq; - use crate::widgets::ListState; + use crate::ListState; #[test] fn selected() { diff --git a/src/widgets/paragraph.rs b/ratatui-widgets/src/paragraph.rs similarity index 99% rename from src/widgets/paragraph.rs rename to ratatui-widgets/src/paragraph.rs index 6353fe09..d9df3b2b 100644 --- a/src/widgets/paragraph.rs +++ b/ratatui-widgets/src/paragraph.rs @@ -1,13 +1,10 @@ +use ratatui_core::{prelude::*, style::Styled, text::StyledGrapheme, widgets::WidgetRef}; use unicode_width::UnicodeWidthStr; use crate::{ - prelude::*, - style::Styled, - text::StyledGrapheme, - widgets::{ - reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine}, - Block, - }, + block::BlockExt, + reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine}, + Block, }; const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { @@ -470,11 +467,10 @@ impl<'a> Styled for Paragraph<'a> { #[cfg(test)] mod test { + use ratatui_core::backend::TestBackend; + use super::*; - use crate::{ - backend::TestBackend, - widgets::{block::Position, Borders}, - }; + use crate::{block::title::Position, Borders}; /// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal /// area and comparing the rendered and expected content. diff --git a/src/widgets/reflow.rs b/ratatui-widgets/src/reflow.rs similarity index 99% rename from src/widgets/reflow.rs rename to ratatui-widgets/src/reflow.rs index 856e239f..a15cb93b 100644 --- a/src/widgets/reflow.rs +++ b/ratatui-widgets/src/reflow.rs @@ -1,10 +1,9 @@ use std::{collections::VecDeque, mem}; +use ratatui_core::{layout::Alignment, text::StyledGrapheme}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use crate::{layout::Alignment, text::StyledGrapheme}; - /// A state machine to pack styled symbols into lines. /// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming /// iterators for that). @@ -344,12 +343,13 @@ fn trim_offset(src: &str, mut offset: usize) -> &str { #[cfg(test)] mod test { - use super::*; - use crate::{ + use ratatui_core::{ style::Style, text::{Line, Text}, }; + use super::*; + #[derive(Clone, Copy)] enum Composer { WordWrapper { trim: bool }, diff --git a/src/widgets/scrollbar.rs b/ratatui-widgets/src/scrollbar.rs similarity index 99% rename from src/widgets/scrollbar.rs rename to ratatui-widgets/src/scrollbar.rs index f1e704b1..06d30156 100644 --- a/src/widgets/scrollbar.rs +++ b/ratatui-widgets/src/scrollbar.rs @@ -8,13 +8,12 @@ use std::iter; -use strum::{Display, EnumString}; -use unicode_width::UnicodeWidthStr; - -use crate::{ +use ratatui_core::{ prelude::*, symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL}, }; +use strum::{Display, EnumString}; +use unicode_width::UnicodeWidthStr; /// A widget to display a scrollbar /// diff --git a/src/widgets/sparkline.rs b/ratatui-widgets/src/sparkline.rs similarity index 98% rename from src/widgets/sparkline.rs rename to ratatui-widgets/src/sparkline.rs index 760c6357..aa4d76ee 100644 --- a/src/widgets/sparkline.rs +++ b/ratatui-widgets/src/sparkline.rs @@ -1,8 +1,9 @@ use std::cmp::min; +use ratatui_core::{prelude::*, style::Styled, widgets::WidgetRef}; use strum::{Display, EnumString}; -use crate::{prelude::*, style::Styled, widgets::Block}; +use crate::{block::BlockExt, Block}; /// Widget to render a sparkline over one or more lines. /// @@ -208,10 +209,10 @@ impl Sparkline<'_> { #[cfg(test)] mod tests { + use ratatui_core::buffer::Cell; use strum::ParseError; use super::*; - use crate::buffer::Cell; #[test] fn render_direction_to_string() { diff --git a/src/widgets/table.rs b/ratatui-widgets/src/table.rs similarity index 100% rename from src/widgets/table.rs rename to ratatui-widgets/src/table.rs diff --git a/src/widgets/table/cell.rs b/ratatui-widgets/src/table/cell.rs similarity index 98% rename from src/widgets/table/cell.rs rename to ratatui-widgets/src/table/cell.rs index abeecb13..055379a0 100644 --- a/src/widgets/table/cell.rs +++ b/ratatui-widgets/src/table/cell.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, style::Styled}; +use ratatui_core::{prelude::*, style::Styled, widgets::WidgetRef}; /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// diff --git a/src/widgets/table/highlight_spacing.rs b/ratatui-widgets/src/table/highlight_spacing.rs similarity index 100% rename from src/widgets/table/highlight_spacing.rs rename to ratatui-widgets/src/table/highlight_spacing.rs diff --git a/src/widgets/table/row.rs b/ratatui-widgets/src/table/row.rs similarity index 99% rename from src/widgets/table/row.rs rename to ratatui-widgets/src/table/row.rs index 728b9d06..3b7ba53e 100644 --- a/src/widgets/table/row.rs +++ b/ratatui-widgets/src/table/row.rs @@ -1,5 +1,6 @@ +use ratatui_core::{prelude::*, style::Styled}; + use super::Cell; -use crate::{prelude::*, style::Styled}; /// A single row of data to be displayed in a [`Table`] widget. /// diff --git a/src/widgets/table/table.rs b/ratatui-widgets/src/table/table.rs similarity index 99% rename from src/widgets/table/table.rs rename to ratatui-widgets/src/table/table.rs index 831ed403..c3f72095 100644 --- a/src/widgets/table/table.rs +++ b/ratatui-widgets/src/table/table.rs @@ -1,9 +1,15 @@ use itertools::Itertools; +use ratatui_core::{ + layout::Flex, + prelude::*, + style::Styled, + widgets::{StatefulWidgetRef, WidgetRef}, +}; #[allow(unused_imports)] // `Cell` is used in the doc comment but not the code use super::Cell; use super::{HighlightSpacing, Row, TableState}; -use crate::{layout::Flex, prelude::*, style::Styled, widgets::Block}; +use crate::{block::BlockExt, Block}; /// A widget to display data in formatted columns. /// @@ -867,8 +873,10 @@ where mod tests { use std::vec; + use ratatui_core::{layout::Constraint::*, style::Style, text::Line}; + use super::*; - use crate::{layout::Constraint::*, style::Style, text::Line, widgets::Cell}; + use crate::table::Cell; #[test] fn new() { @@ -1031,14 +1039,15 @@ mod tests { #[cfg(test)] mod state { + use ratatui_core::{ + buffer::Buffer, + layout::{Constraint, Rect}, + widgets::StatefulWidget, + }; use rstest::{fixture, rstest}; use super::TableState; - use crate::{ - buffer::Buffer, - layout::{Constraint, Rect}, - widgets::{Row, StatefulWidget, Table}, - }; + use crate::{Row, Table}; #[fixture] fn table_buf() -> Buffer { diff --git a/src/widgets/table/table_state.rs b/ratatui-widgets/src/table/table_state.rs similarity index 99% rename from src/widgets/table/table_state.rs rename to ratatui-widgets/src/table/table_state.rs index c516f924..0cd26275 100644 --- a/src/widgets/table/table_state.rs +++ b/ratatui-widgets/src/table/table_state.rs @@ -41,8 +41,8 @@ /// Note that if [`Table::widths`] is not called before rendering, the rendered columns will have /// equal width. /// -/// [`Table`]: crate::widgets::Table -/// [`Table::widths`]: crate::widgets::Table::widths +/// [`Table`]: crate::Table +/// [`Table::widths`]: crate::Table::widths /// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/widgets/tabs.rs b/ratatui-widgets/src/tabs.rs similarity index 99% rename from src/widgets/tabs.rs rename to ratatui-widgets/src/tabs.rs index dd7758c6..51696f3d 100644 --- a/src/widgets/tabs.rs +++ b/ratatui-widgets/src/tabs.rs @@ -1,4 +1,6 @@ -use crate::{prelude::*, style::Styled, widgets::Block}; +use ratatui_core::{prelude::*, style::Styled, widgets::WidgetRef}; + +use crate::{block::BlockExt, Block}; const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED); diff --git a/ratatui/Cargo.toml b/ratatui/Cargo.toml new file mode 100644 index 00000000..492a6c94 --- /dev/null +++ b/ratatui/Cargo.toml @@ -0,0 +1,386 @@ +[package] +name = "ratatui" +version = "0.28.1" # crate version +authors = ["Florian Dehau ", "The Ratatui Developers"] +description = "A library that's all about cooking up terminal user interfaces" +documentation = "https://docs.rs/ratatui/latest/ratatui/" +repository = "https://github.com/ratatui/ratatui" +homepage = "https://ratatui.rs" +keywords = ["tui", "terminal", "dashboard"] +categories = ["command-line-interface"] +readme = "README.md" +license = "MIT" +exclude = [ + "assets/*", + ".github", + "Makefile.toml", + "CONTRIBUTING.md", + "*.log", + "tags", +] +edition = "2021" +rust-version = "1.74.0" + +[features] +#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file. +#! +## By default, we enable the crossterm backend as this is a reasonable choice for most applications +## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature +## which allows you to set the underline color of text. +default = ["crossterm", "underline-color"] +#! Generally an application will only use one backend, so you should only enable one of the following features: +## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`]. +crossterm = ["dep:ratatui-crossterm", "dep:crossterm"] +## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`]. +# termion = ["dep:ratatui-termion", "dep:termion"] +# ## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`]. +# termwiz = ["dep:ratatui-termwiz"] + +#! The following optional features are available for all backends: +## enables serialization and deserialization of style and color types using the [`serde`] crate. +## This is useful if you want to save themes to a file. +serde = ["dep:serde", "bitflags/serde", "compact_str/serde"] + +## enables the [`border!`] macro. +macros = [] + +## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color). +palette = ["dep:palette"] + +## enables all widgets. +all-widgets = ["widget-calendar"] + +#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive +#! dependencies. The available features are: +## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`]. +widget-calendar = ["ratatui-widgets/widget-calendar", "dep:time"] + +#! The following optional features are only available for some backends: + +## enables the backend code that sets the underline color. +## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend, +## and is not supported on Windows 7. +# underline-color = ["ratatui-crossterm/underline-color"] +underline-color = ["ratatui-core/underline-color"] + +#! The following features are unstable and may change in the future: + +## Enable all unstable features. +unstable = [ + "unstable-rendered-line-info", + "unstable-widget-ref", + "unstable-backend-writer", +] + +## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count) +## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods +## which are experimental and may change in the future. +## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details. +unstable-rendered-line-info = [] + +## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in +## the future. +unstable-widget-ref = [] + +## Enables getting access to backends' writers. +unstable-backend-writer = [] + +[dependencies] +ratatui-widgets = { workspace = true } +ratatui-core = { workspace = true } +ratatui-crossterm = { workspace = true, optional = true } +# ratatui-termwiz = { workspace = true, optional = true } + +bitflags = "2.3" +cassowary = "0.3" +crossterm = { workspace = true, optional = true } +compact_str = "0.8.0" +document-features = { version = "0.2.7", optional = true } +instability = "0.3.1" +itertools = "0.13" +lru = "0.12.0" +paste = "1.0.2" +palette = { version = "0.7.6", optional = true } +serde = { version = "1", optional = true, features = ["derive"] } +strum = { version = "0.26.3", features = ["derive"] } +unicode-segmentation = "1.10" +unicode-truncate = "1" +unicode-width = "=0.1.13" +time = { workspace = true, optional = true } + +# [target.'cfg(not(windows))'.dependencies] +# # termion is not supported on Windows +# ratatui-termion = { workspace = true, optional = true } +# termion = { workspace = true, optional = true } + +[dev-dependencies] +argh = "0.1.12" +color-eyre = "0.6.2" +criterion = { version = "0.5.1", features = ["html_reports"] } +fakeit = "1.1" +font8x8 = "0.3.1" +futures = "0.3.30" +indoc = "2" +octocrab = "0.40.0" +pretty_assertions = "1.4.0" +rand = "0.8.5" +rand_chacha = "0.3.1" +rstest = "0.22.0" +serde_json = "1.0.109" +tokio = { version = "1.39.2", features = [ + "rt", + "macros", + "time", + "rt-multi-thread", +] } +tracing = "0.1.40" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +cargo = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +cast_possible_truncation = "allow" +cast_possible_wrap = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +module_name_repetitions = "allow" +must_use_candidate = "allow" + +# we often split up a module into multiple files with the main type in a file named after the +# module, so we want to allow this pattern +module_inception = "allow" + +# nursery or restricted +as_underscore = "warn" +deref_by_slicing = "warn" +else_if_without_else = "warn" +empty_line_after_doc_comments = "warn" +equatable_if_let = "warn" +fn_to_numeric_cast_any = "warn" +format_push_string = "warn" +map_err_ignore = "warn" +missing_const_for_fn = "warn" +mixed_read_write_in_expression = "warn" +mod_module_files = "warn" +needless_pass_by_ref_mut = "warn" +needless_raw_strings = "warn" +or_fun_call = "warn" +redundant_type_annotations = "warn" +rest_pat_in_fully_bound_structs = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" +string_to_string = "warn" +unnecessary_self_imports = "warn" +use_self = "warn" + + +[package.metadata.docs.rs] +all-features = true +# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +rustdoc-args = ["--cfg", "docsrs"] + +# Improve benchmark consistency +[profile.bench] +codegen-units = 1 +lto = true + +[lib] +bench = false + +[[bench]] +name = "main" +harness = false + +[[example]] +name = "async" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "barchart" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "barchart-grouped" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "block" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "calendar" +required-features = ["crossterm", "widget-calendar"] +doc-scrape-examples = true + +[[example]] +name = "canvas" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "chart" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "colors" +required-features = ["crossterm"] +# this example is a bit verbose, so we don't want to include it in the docs +doc-scrape-examples = false + +[[example]] +name = "colors_rgb" +required-features = ["crossterm", "palette"] +doc-scrape-examples = true + +[[example]] +name = "constraint-explorer" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "constraints" +required-features = ["crossterm"] +doc-scrape-examples = false + +[[example]] +name = "custom_widget" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "demo" +# this runs for all of the terminal backends, so it can't be built using --all-features or scraped +doc-scrape-examples = false + +[[example]] +name = "demo2" +required-features = ["crossterm", "palette", "widget-calendar"] +doc-scrape-examples = true + +[[example]] +name = "docsrs" +required-features = ["crossterm"] +doc-scrape-examples = false + +[[example]] +name = "flex" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "gauge" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "hello_world" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "inline" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "layout" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "line_gauge" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "hyperlink" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "list" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "minimal" +required-features = ["crossterm"] +# prefer to show the more featureful examples in the docs +doc-scrape-examples = false + +[[example]] +name = "modifiers" +required-features = ["crossterm"] +# this example is a bit verbose, so we don't want to include it in the docs +doc-scrape-examples = false + +[[example]] +name = "panic" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "paragraph" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "popup" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "ratatui-logo" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "scrollbar" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "sparkline" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "table" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "tabs" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "tracing" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "user_input" +required-features = ["crossterm"] +doc-scrape-examples = true + +[[example]] +name = "widget_impl" +required-features = ["crossterm", "unstable-widget-ref"] +doc-scrape-examples = true + +[[test]] +name = "state_serde" +required-features = ["serde"] diff --git a/benches/main.rs b/ratatui/benches/main.rs similarity index 100% rename from benches/main.rs rename to ratatui/benches/main.rs diff --git a/benches/main/barchart.rs b/ratatui/benches/main/barchart.rs similarity index 100% rename from benches/main/barchart.rs rename to ratatui/benches/main/barchart.rs diff --git a/benches/main/block.rs b/ratatui/benches/main/block.rs similarity index 100% rename from benches/main/block.rs rename to ratatui/benches/main/block.rs diff --git a/benches/main/buffer.rs b/ratatui/benches/main/buffer.rs similarity index 100% rename from benches/main/buffer.rs rename to ratatui/benches/main/buffer.rs diff --git a/benches/main/line.rs b/ratatui/benches/main/line.rs similarity index 100% rename from benches/main/line.rs rename to ratatui/benches/main/line.rs diff --git a/benches/main/list.rs b/ratatui/benches/main/list.rs similarity index 100% rename from benches/main/list.rs rename to ratatui/benches/main/list.rs diff --git a/benches/main/paragraph.rs b/ratatui/benches/main/paragraph.rs similarity index 100% rename from benches/main/paragraph.rs rename to ratatui/benches/main/paragraph.rs diff --git a/benches/main/rect.rs b/ratatui/benches/main/rect.rs similarity index 100% rename from benches/main/rect.rs rename to ratatui/benches/main/rect.rs diff --git a/benches/main/sparkline.rs b/ratatui/benches/main/sparkline.rs similarity index 100% rename from benches/main/sparkline.rs rename to ratatui/benches/main/sparkline.rs diff --git a/examples/README.md b/ratatui/examples/README.md similarity index 100% rename from examples/README.md rename to ratatui/examples/README.md diff --git a/examples/async.rs b/ratatui/examples/async.rs similarity index 100% rename from examples/async.rs rename to ratatui/examples/async.rs diff --git a/examples/barchart-grouped.rs b/ratatui/examples/barchart-grouped.rs similarity index 100% rename from examples/barchart-grouped.rs rename to ratatui/examples/barchart-grouped.rs diff --git a/examples/barchart.rs b/ratatui/examples/barchart.rs similarity index 100% rename from examples/barchart.rs rename to ratatui/examples/barchart.rs diff --git a/examples/block.rs b/ratatui/examples/block.rs similarity index 100% rename from examples/block.rs rename to ratatui/examples/block.rs diff --git a/examples/calendar.rs b/ratatui/examples/calendar.rs similarity index 100% rename from examples/calendar.rs rename to ratatui/examples/calendar.rs diff --git a/examples/canvas.rs b/ratatui/examples/canvas.rs similarity index 100% rename from examples/canvas.rs rename to ratatui/examples/canvas.rs diff --git a/examples/chart.rs b/ratatui/examples/chart.rs similarity index 100% rename from examples/chart.rs rename to ratatui/examples/chart.rs diff --git a/examples/colors.rs b/ratatui/examples/colors.rs similarity index 100% rename from examples/colors.rs rename to ratatui/examples/colors.rs diff --git a/examples/colors_rgb.rs b/ratatui/examples/colors_rgb.rs similarity index 100% rename from examples/colors_rgb.rs rename to ratatui/examples/colors_rgb.rs diff --git a/examples/constraint-explorer.rs b/ratatui/examples/constraint-explorer.rs similarity index 100% rename from examples/constraint-explorer.rs rename to ratatui/examples/constraint-explorer.rs diff --git a/examples/constraints.rs b/ratatui/examples/constraints.rs similarity index 100% rename from examples/constraints.rs rename to ratatui/examples/constraints.rs diff --git a/examples/custom_widget.rs b/ratatui/examples/custom_widget.rs similarity index 100% rename from examples/custom_widget.rs rename to ratatui/examples/custom_widget.rs diff --git a/examples/demo/app.rs b/ratatui/examples/demo/app.rs similarity index 100% rename from examples/demo/app.rs rename to ratatui/examples/demo/app.rs diff --git a/examples/demo/crossterm.rs b/ratatui/examples/demo/crossterm.rs similarity index 100% rename from examples/demo/crossterm.rs rename to ratatui/examples/demo/crossterm.rs diff --git a/examples/demo/main.rs b/ratatui/examples/demo/main.rs similarity index 100% rename from examples/demo/main.rs rename to ratatui/examples/demo/main.rs diff --git a/examples/demo/termion.rs b/ratatui/examples/demo/termion.rs similarity index 100% rename from examples/demo/termion.rs rename to ratatui/examples/demo/termion.rs diff --git a/examples/demo/termwiz.rs b/ratatui/examples/demo/termwiz.rs similarity index 100% rename from examples/demo/termwiz.rs rename to ratatui/examples/demo/termwiz.rs diff --git a/examples/demo/ui.rs b/ratatui/examples/demo/ui.rs similarity index 100% rename from examples/demo/ui.rs rename to ratatui/examples/demo/ui.rs diff --git a/examples/demo2/app.rs b/ratatui/examples/demo2/app.rs similarity index 100% rename from examples/demo2/app.rs rename to ratatui/examples/demo2/app.rs diff --git a/examples/demo2/colors.rs b/ratatui/examples/demo2/colors.rs similarity index 100% rename from examples/demo2/colors.rs rename to ratatui/examples/demo2/colors.rs diff --git a/examples/demo2/destroy.rs b/ratatui/examples/demo2/destroy.rs similarity index 100% rename from examples/demo2/destroy.rs rename to ratatui/examples/demo2/destroy.rs diff --git a/examples/demo2/main.rs b/ratatui/examples/demo2/main.rs similarity index 100% rename from examples/demo2/main.rs rename to ratatui/examples/demo2/main.rs diff --git a/examples/demo2/tabs.rs b/ratatui/examples/demo2/tabs.rs similarity index 100% rename from examples/demo2/tabs.rs rename to ratatui/examples/demo2/tabs.rs diff --git a/examples/demo2/tabs/about.rs b/ratatui/examples/demo2/tabs/about.rs similarity index 100% rename from examples/demo2/tabs/about.rs rename to ratatui/examples/demo2/tabs/about.rs diff --git a/examples/demo2/tabs/email.rs b/ratatui/examples/demo2/tabs/email.rs similarity index 100% rename from examples/demo2/tabs/email.rs rename to ratatui/examples/demo2/tabs/email.rs diff --git a/examples/demo2/tabs/recipe.rs b/ratatui/examples/demo2/tabs/recipe.rs similarity index 100% rename from examples/demo2/tabs/recipe.rs rename to ratatui/examples/demo2/tabs/recipe.rs diff --git a/examples/demo2/tabs/traceroute.rs b/ratatui/examples/demo2/tabs/traceroute.rs similarity index 100% rename from examples/demo2/tabs/traceroute.rs rename to ratatui/examples/demo2/tabs/traceroute.rs diff --git a/examples/demo2/tabs/weather.rs b/ratatui/examples/demo2/tabs/weather.rs similarity index 100% rename from examples/demo2/tabs/weather.rs rename to ratatui/examples/demo2/tabs/weather.rs diff --git a/examples/demo2/theme.rs b/ratatui/examples/demo2/theme.rs similarity index 100% rename from examples/demo2/theme.rs rename to ratatui/examples/demo2/theme.rs diff --git a/examples/docsrs.rs b/ratatui/examples/docsrs.rs similarity index 100% rename from examples/docsrs.rs rename to ratatui/examples/docsrs.rs diff --git a/examples/flex.rs b/ratatui/examples/flex.rs similarity index 100% rename from examples/flex.rs rename to ratatui/examples/flex.rs diff --git a/examples/gauge.rs b/ratatui/examples/gauge.rs similarity index 100% rename from examples/gauge.rs rename to ratatui/examples/gauge.rs diff --git a/examples/hello_world.rs b/ratatui/examples/hello_world.rs similarity index 100% rename from examples/hello_world.rs rename to ratatui/examples/hello_world.rs diff --git a/examples/hyperlink.rs b/ratatui/examples/hyperlink.rs similarity index 100% rename from examples/hyperlink.rs rename to ratatui/examples/hyperlink.rs diff --git a/examples/inline.rs b/ratatui/examples/inline.rs similarity index 100% rename from examples/inline.rs rename to ratatui/examples/inline.rs diff --git a/examples/layout.rs b/ratatui/examples/layout.rs similarity index 100% rename from examples/layout.rs rename to ratatui/examples/layout.rs diff --git a/examples/line_gauge.rs b/ratatui/examples/line_gauge.rs similarity index 100% rename from examples/line_gauge.rs rename to ratatui/examples/line_gauge.rs diff --git a/examples/list.rs b/ratatui/examples/list.rs similarity index 100% rename from examples/list.rs rename to ratatui/examples/list.rs diff --git a/examples/minimal.rs b/ratatui/examples/minimal.rs similarity index 100% rename from examples/minimal.rs rename to ratatui/examples/minimal.rs diff --git a/examples/modifiers.rs b/ratatui/examples/modifiers.rs similarity index 100% rename from examples/modifiers.rs rename to ratatui/examples/modifiers.rs diff --git a/examples/panic.rs b/ratatui/examples/panic.rs similarity index 100% rename from examples/panic.rs rename to ratatui/examples/panic.rs diff --git a/examples/paragraph.rs b/ratatui/examples/paragraph.rs similarity index 100% rename from examples/paragraph.rs rename to ratatui/examples/paragraph.rs diff --git a/examples/popup.rs b/ratatui/examples/popup.rs similarity index 100% rename from examples/popup.rs rename to ratatui/examples/popup.rs diff --git a/examples/ratatui-logo.rs b/ratatui/examples/ratatui-logo.rs similarity index 100% rename from examples/ratatui-logo.rs rename to ratatui/examples/ratatui-logo.rs diff --git a/examples/scrollbar.rs b/ratatui/examples/scrollbar.rs similarity index 100% rename from examples/scrollbar.rs rename to ratatui/examples/scrollbar.rs diff --git a/examples/sparkline.rs b/ratatui/examples/sparkline.rs similarity index 100% rename from examples/sparkline.rs rename to ratatui/examples/sparkline.rs diff --git a/examples/table.rs b/ratatui/examples/table.rs similarity index 100% rename from examples/table.rs rename to ratatui/examples/table.rs diff --git a/examples/tabs.rs b/ratatui/examples/tabs.rs similarity index 100% rename from examples/tabs.rs rename to ratatui/examples/tabs.rs diff --git a/examples/tracing.rs b/ratatui/examples/tracing.rs similarity index 100% rename from examples/tracing.rs rename to ratatui/examples/tracing.rs diff --git a/examples/user_input.rs b/ratatui/examples/user_input.rs similarity index 100% rename from examples/user_input.rs rename to ratatui/examples/user_input.rs diff --git a/examples/vhs/barchart-grouped.tape b/ratatui/examples/vhs/barchart-grouped.tape similarity index 100% rename from examples/vhs/barchart-grouped.tape rename to ratatui/examples/vhs/barchart-grouped.tape diff --git a/examples/vhs/barchart.tape b/ratatui/examples/vhs/barchart.tape similarity index 100% rename from examples/vhs/barchart.tape rename to ratatui/examples/vhs/barchart.tape diff --git a/examples/vhs/block.tape b/ratatui/examples/vhs/block.tape similarity index 100% rename from examples/vhs/block.tape rename to ratatui/examples/vhs/block.tape diff --git a/examples/vhs/calendar.tape b/ratatui/examples/vhs/calendar.tape similarity index 100% rename from examples/vhs/calendar.tape rename to ratatui/examples/vhs/calendar.tape diff --git a/examples/vhs/canvas.tape b/ratatui/examples/vhs/canvas.tape similarity index 100% rename from examples/vhs/canvas.tape rename to ratatui/examples/vhs/canvas.tape diff --git a/examples/vhs/chart.tape b/ratatui/examples/vhs/chart.tape similarity index 100% rename from examples/vhs/chart.tape rename to ratatui/examples/vhs/chart.tape diff --git a/examples/vhs/colors.tape b/ratatui/examples/vhs/colors.tape similarity index 100% rename from examples/vhs/colors.tape rename to ratatui/examples/vhs/colors.tape diff --git a/examples/vhs/colors_rgb.tape b/ratatui/examples/vhs/colors_rgb.tape similarity index 100% rename from examples/vhs/colors_rgb.tape rename to ratatui/examples/vhs/colors_rgb.tape diff --git a/examples/vhs/constraint-explorer.tape b/ratatui/examples/vhs/constraint-explorer.tape similarity index 100% rename from examples/vhs/constraint-explorer.tape rename to ratatui/examples/vhs/constraint-explorer.tape diff --git a/examples/vhs/constraints.tape b/ratatui/examples/vhs/constraints.tape similarity index 100% rename from examples/vhs/constraints.tape rename to ratatui/examples/vhs/constraints.tape diff --git a/examples/vhs/custom_widget.tape b/ratatui/examples/vhs/custom_widget.tape similarity index 100% rename from examples/vhs/custom_widget.tape rename to ratatui/examples/vhs/custom_widget.tape diff --git a/examples/vhs/demo.tape b/ratatui/examples/vhs/demo.tape similarity index 100% rename from examples/vhs/demo.tape rename to ratatui/examples/vhs/demo.tape diff --git a/examples/vhs/demo2-destroy.tape b/ratatui/examples/vhs/demo2-destroy.tape similarity index 100% rename from examples/vhs/demo2-destroy.tape rename to ratatui/examples/vhs/demo2-destroy.tape diff --git a/examples/vhs/demo2-social.tape b/ratatui/examples/vhs/demo2-social.tape similarity index 100% rename from examples/vhs/demo2-social.tape rename to ratatui/examples/vhs/demo2-social.tape diff --git a/examples/vhs/demo2.tape b/ratatui/examples/vhs/demo2.tape similarity index 100% rename from examples/vhs/demo2.tape rename to ratatui/examples/vhs/demo2.tape diff --git a/examples/vhs/docsrs.tape b/ratatui/examples/vhs/docsrs.tape similarity index 100% rename from examples/vhs/docsrs.tape rename to ratatui/examples/vhs/docsrs.tape diff --git a/examples/vhs/flex.tape b/ratatui/examples/vhs/flex.tape similarity index 100% rename from examples/vhs/flex.tape rename to ratatui/examples/vhs/flex.tape diff --git a/examples/vhs/gauge.tape b/ratatui/examples/vhs/gauge.tape similarity index 100% rename from examples/vhs/gauge.tape rename to ratatui/examples/vhs/gauge.tape diff --git a/examples/vhs/generate.bash b/ratatui/examples/vhs/generate.bash similarity index 100% rename from examples/vhs/generate.bash rename to ratatui/examples/vhs/generate.bash diff --git a/examples/vhs/hello_world.tape b/ratatui/examples/vhs/hello_world.tape similarity index 100% rename from examples/vhs/hello_world.tape rename to ratatui/examples/vhs/hello_world.tape diff --git a/examples/vhs/hyperlink.tape b/ratatui/examples/vhs/hyperlink.tape similarity index 100% rename from examples/vhs/hyperlink.tape rename to ratatui/examples/vhs/hyperlink.tape diff --git a/examples/vhs/inline.tape b/ratatui/examples/vhs/inline.tape similarity index 100% rename from examples/vhs/inline.tape rename to ratatui/examples/vhs/inline.tape diff --git a/examples/vhs/layout.tape b/ratatui/examples/vhs/layout.tape similarity index 100% rename from examples/vhs/layout.tape rename to ratatui/examples/vhs/layout.tape diff --git a/examples/vhs/line_gauge.tape b/ratatui/examples/vhs/line_gauge.tape similarity index 100% rename from examples/vhs/line_gauge.tape rename to ratatui/examples/vhs/line_gauge.tape diff --git a/examples/vhs/list.tape b/ratatui/examples/vhs/list.tape similarity index 100% rename from examples/vhs/list.tape rename to ratatui/examples/vhs/list.tape diff --git a/examples/vhs/minimal.tape b/ratatui/examples/vhs/minimal.tape similarity index 100% rename from examples/vhs/minimal.tape rename to ratatui/examples/vhs/minimal.tape diff --git a/examples/vhs/modifiers.tape b/ratatui/examples/vhs/modifiers.tape similarity index 100% rename from examples/vhs/modifiers.tape rename to ratatui/examples/vhs/modifiers.tape diff --git a/examples/vhs/panic.tape b/ratatui/examples/vhs/panic.tape similarity index 100% rename from examples/vhs/panic.tape rename to ratatui/examples/vhs/panic.tape diff --git a/examples/vhs/paragraph.tape b/ratatui/examples/vhs/paragraph.tape similarity index 100% rename from examples/vhs/paragraph.tape rename to ratatui/examples/vhs/paragraph.tape diff --git a/examples/vhs/popup.tape b/ratatui/examples/vhs/popup.tape similarity index 100% rename from examples/vhs/popup.tape rename to ratatui/examples/vhs/popup.tape diff --git a/examples/vhs/ratatui-logo.tape b/ratatui/examples/vhs/ratatui-logo.tape similarity index 100% rename from examples/vhs/ratatui-logo.tape rename to ratatui/examples/vhs/ratatui-logo.tape diff --git a/examples/vhs/scrollbar.tape b/ratatui/examples/vhs/scrollbar.tape similarity index 100% rename from examples/vhs/scrollbar.tape rename to ratatui/examples/vhs/scrollbar.tape diff --git a/examples/vhs/sparkline.tape b/ratatui/examples/vhs/sparkline.tape similarity index 100% rename from examples/vhs/sparkline.tape rename to ratatui/examples/vhs/sparkline.tape diff --git a/examples/vhs/table.tape b/ratatui/examples/vhs/table.tape similarity index 100% rename from examples/vhs/table.tape rename to ratatui/examples/vhs/table.tape diff --git a/examples/vhs/tabs.tape b/ratatui/examples/vhs/tabs.tape similarity index 100% rename from examples/vhs/tabs.tape rename to ratatui/examples/vhs/tabs.tape diff --git a/examples/vhs/tracing.tape b/ratatui/examples/vhs/tracing.tape similarity index 100% rename from examples/vhs/tracing.tape rename to ratatui/examples/vhs/tracing.tape diff --git a/examples/vhs/user_input.tape b/ratatui/examples/vhs/user_input.tape similarity index 100% rename from examples/vhs/user_input.tape rename to ratatui/examples/vhs/user_input.tape diff --git a/examples/widget_impl.rs b/ratatui/examples/widget_impl.rs similarity index 100% rename from examples/widget_impl.rs rename to ratatui/examples/widget_impl.rs diff --git a/ratatui/src/backend.rs b/ratatui/src/backend.rs new file mode 100644 index 00000000..acf66269 --- /dev/null +++ b/ratatui/src/backend.rs @@ -0,0 +1,116 @@ +#![warn(missing_docs)] +//! This module provides the backend implementations for different terminal libraries. +//! +//! It defines the [`Backend`] trait which is used to abstract over the specific terminal library +//! being used. +//! +//! Supported terminal backends: +//! - [Crossterm]: enable the `crossterm` feature (enabled by default) and use [`CrosstermBackend`] +//! - [Termion]: enable the `termion` feature and use [`TermionBackend`] +//! - [Termwiz]: enable the `termwiz` feature and use [`TermwizBackend`] +//! +//! Additionally, a [`TestBackend`] is provided for testing purposes. +//! +//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different +//! backends. +//! +//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate +//! screen](#alternate-screen), and [mouse capture](#mouse-capture). These features are generally +//! not enabled by default, and must be enabled by the application before they can be used. See the +//! documentation for each backend for more details. +//! +//! Note: most applications should use the [`Terminal`] struct instead of directly calling methods +//! on the backend. +//! +//! # Example +//! +//! ```rust,no_run +//! use std::io::stdout; +//! +//! use ratatui::prelude::*; +//! +//! let backend = CrosstermBackend::new(stdout()); +//! let mut terminal = Terminal::new(backend)?; +//! terminal.clear()?; +//! terminal.draw(|frame| { +//! // -- snip -- +//! })?; +//! # std::io::Result::Ok(()) +//! ``` +//! +//! See the the [Examples] directory for more examples. +//! +//! # Raw Mode +//! +//! Raw mode is a mode where the terminal does not perform any processing or handling of the input +//! and output. This means that features such as echoing input characters, line buffering, and +//! special character processing (e.g., CTRL-C for SIGINT) are disabled. This is useful for +//! applications that want to have complete control over the terminal input and output, processing +//! each keystroke themselves. +//! +//! For example, in raw mode, the terminal will not perform line buffering on the input, so the +//! application will receive each key press as it is typed, instead of waiting for the user to +//! press enter. This makes it suitable for real-time applications like text editors, +//! terminal-based games, and more. +//! +//! Each backend handles raw mode differently, so the behavior may vary depending on the backend +//! being used. Be sure to consult the backend's specific documentation for exact details on how it +//! implements raw mode. + +//! # Alternate Screen +//! +//! The alternate screen is a separate buffer that some terminals provide, distinct from the main +//! screen. When activated, the terminal will display the alternate screen, hiding the current +//! content of the main screen. Applications can write to this screen as if it were the regular +//! terminal display, but when the application exits, the terminal will switch back to the main +//! screen, and the contents of the alternate screen will be cleared. This is useful for +//! applications like text editors or terminal games that want to use the full terminal window +//! without disrupting the command line or other terminal content. +//! +//! This creates a seamless transition between the application and the regular terminal session, as +//! the content displayed before launching the application will reappear after the application +//! exits. +//! +//! Note that not all terminal emulators support the alternate screen, and even those that do may +//! handle it differently. As a result, the behavior may vary depending on the backend being used. +//! Always consult the specific backend's documentation to understand how it implements the +//! alternate screen. +//! +//! # Mouse Capture +//! +//! Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and +//! movement, and sends them to the application as special sequences or events. This enables the +//! application to handle and respond to mouse actions, providing a more interactive and graphical +//! user experience within the terminal. It's particularly useful for applications like +//! terminal-based games, text editors, or other programs that require more direct interaction from +//! the user. +//! +//! Each backend handles mouse capture differently, with variations in the types of events that can +//! be captured and how they are represented. As such, the behavior may vary depending on the +//! backend being used, and developers should consult the specific backend's documentation to +//! understand how it implements mouse capture. +//! +//! [`TermionBackend`]: termion/struct.TermionBackend.html +//! [`Terminal`]: crate::terminal::Terminal +//! [`TermionBackend`]: termion/struct.TermionBackend.html +//! [Crossterm]: https://crates.io/crates/crossterm +//! [Termion]: https://crates.io/crates/termion +//! [Termwiz]: https://crates.io/crates/termwiz +//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md +//! [Backend Comparison]: +//! https://ratatui.rs/concepts/backends/comparison/ +//! [Ratatui Website]: https://ratatui.rs +use std::io; + +pub use ratatui_core::backend::{Backend, ClearType, TestBackend, WindowSize}; +use ratatui_core::{ + buffer::Cell, + layout::{Position, Size}, +}; +#[cfg(feature = "crossterm")] +pub use ratatui_crossterm::CrosstermBackend; +#[cfg(all(not(windows), feature = "termion"))] +pub use ratatui_termion::TermionBackend; +#[cfg(feature = "termwiz")] +pub use ratatui_termwiz::TermwizBackend; +use strum::{Display, EnumString}; diff --git a/src/terminal/init.rs b/ratatui/src/init.rs similarity index 100% rename from src/terminal/init.rs rename to ratatui/src/init.rs diff --git a/ratatui/src/lib.rs b/ratatui/src/lib.rs new file mode 100644 index 00000000..9ce92f8e --- /dev/null +++ b/ratatui/src/lib.rs @@ -0,0 +1,323 @@ +//! ![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true) +//! +//!
+//! +//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs +//! Badge]][Deps.rs]
[![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors +//! Badge]][GitHub Sponsors]
[![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix] +//! [![Forum Badge]][Forum]
+//! +//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]
+//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request] +//! +//!
+//! +//! # Ratatui +//! +//! [Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a +//! lightweight library that provides a set of widgets and utilities to build complex Rust TUIs. +//! Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development. +//! +//! ## Installation +//! +//! Add `ratatui` as a dependency to your cargo.toml: +//! +//! ```shell +//! cargo add ratatui +//! ``` +//! +//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation] +//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] / +//! [Termwiz]). +//! +//! ## Introduction +//! +//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means +//! that for each frame, your app must render all widgets that are supposed to be part of the UI. +//! This is in contrast to the retained mode style of rendering where widgets are updated and then +//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] +//! for more info. +//! +//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to +//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo. +//! +//! ## Other documentation +//! +//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials +//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library +//! - [API Docs] - the full API documentation for the library on docs.rs. +//! - [Examples] - a collection of examples that demonstrate how to use the library. +//! - [Contributing] - Please read this if you are interested in contributing to the project. +//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits]. +//! - [Breaking Changes] - a list of breaking changes in the library. +//! +//! ## Quickstart +//! +//! The following example demonstrates the minimal amount of code necessary to setup a terminal and +//! render "Hello World!". The full code for this example which contains a little more detail is in +//! the [Examples] directory. For more guidance on different ways to structure your application see +//! the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the +//! various [Examples]. There are also several starter templates available in the [templates] +//! repository. +//! +//! Every application built with `ratatui` needs to implement the following steps: +//! +//! - Initialize the terminal +//! - A main loop to: +//! - Handle input events +//! - Draw the UI +//! - Restore the terminal state +//! +//! The library contains a [`prelude`] module that re-exports the most commonly used traits and +//! types for convenience. Most examples in the documentation will use this instead of showing the +//! full path of each type. +//! +//! ### Initialize and restore the terminal +//! +//! The [`Terminal`] type is the main entry point for any Ratatui application. It is a light +//! abstraction over a choice of [`Backend`] implementations that provides functionality to draw +//! each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that +//! implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and +//! [Termwiz]. +//! +//! Most applications should enter the Alternate Screen when starting and leave it when exiting and +//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend` +//! module] and the [Backends] section of the [Ratatui Website] for more info. +//! +//! ### Drawing the UI +//! +//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The +//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`] +//! using the provided [`render_widget`] method. After this closure returns, a diff is performed and +//! only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website] +//! for more info. +//! +//! ### Handling events +//! +//! Ratatui does not include any input handling. Instead event handling can be implemented by +//! calling backend library methods directly. See the [Handling Events] section of the [Ratatui +//! Website] for more info. For example, if you are using [Crossterm], you can use the +//! [`crossterm::event`] module to handle events. +//! +//! ### Example +//! +//! ```rust,no_run +//! use ratatui::{ +//! crossterm::event::{self, Event, KeyCode, KeyEventKind}, +//! widgets::{Block, Paragraph}, +//! }; +//! +//! fn main() -> std::io::Result<()> { +//! let mut terminal = ratatui::init(); +//! loop { +//! terminal.draw(|frame| { +//! frame.render_widget( +//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")), +//! frame.area(), +//! ); +//! })?; +//! if let Event::Key(key) = event::read()? { +//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { +//! break; +//! } +//! } +//! } +//! ratatui::restore(); +//! Ok(()) +//! } +//! ``` +//! +//! Running this example produces the following output: +//! +//! ![docsrs-hello] +//! +//! ## Layout +//! +//! The library comes with a basic yet useful layout management object called [`Layout`] which +//! allows you to split the available space into multiple areas and then render widgets in each +//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout] +//! section of the [Ratatui Website] for more info. +//! +//! ```rust,no_run +//! use ratatui::{ +//! layout::{Constraint, Layout}, +//! widgets::Block, +//! Frame, +//! }; +//! +//! fn draw(frame: &mut Frame) { +//! let [title_area, main_area, status_area] = Layout::vertical([ +//! Constraint::Length(1), +//! Constraint::Min(0), +//! Constraint::Length(1), +//! ]) +//! .areas(frame.area()); +//! let [left_area, right_area] = +//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) +//! .areas(main_area); +//! +//! frame.render_widget(Block::bordered().title("Title Bar"), title_area); +//! frame.render_widget(Block::bordered().title("Status Bar"), status_area); +//! frame.render_widget(Block::bordered().title("Left"), left_area); +//! frame.render_widget(Block::bordered().title("Right"), right_area); +//! } +//! ``` +//! +//! Running this example produces the following output: +//! +//! ![docsrs-layout] +//! +//! ## Text and styling +//! +//! The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in +//! many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`] +//! is a string with a specific style. +//! +//! The [`style` module] provides types that represent the various styling options. The most +//! important one is [`Style`] which represents the foreground and background colors and the text +//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows +//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the +//! [Ratatui Website] for more info. +//! +//! ```rust,no_run +//! use ratatui::{ +//! layout::{Constraint, Layout}, +//! style::{Color, Modifier, Style, Stylize}, +//! text::{Line, Span}, +//! widgets::{Block, Paragraph}, +//! Frame, +//! }; +//! +//! fn draw(frame: &mut Frame) { +//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area()); +//! +//! let line = Line::from(vec![ +//! Span::raw("Hello "), +//! Span::styled( +//! "World", +//! Style::new() +//! .fg(Color::Green) +//! .bg(Color::White) +//! .add_modifier(Modifier::BOLD), +//! ), +//! "!".red().on_light_yellow().italic(), +//! ]); +//! frame.render_widget(line, areas[0]); +//! +//! // using the short-hand syntax and implicit conversions +//! let paragraph = Paragraph::new("Hello World!".red().on_white().bold()); +//! frame.render_widget(paragraph, areas[1]); +//! +//! // style the whole widget instead of just the text +//! let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white()); +//! frame.render_widget(paragraph, areas[2]); +//! +//! // use the simpler short-hand syntax +//! let paragraph = Paragraph::new("Hello World!").blue().on_yellow(); +//! frame.render_widget(paragraph, areas[3]); +//! } +//! ``` +//! +//! Running this example produces the following output: +//! +//! ![docsrs-styling] +#![cfg_attr(feature = "document-features", doc = "\n## Features")] +// #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] // +// TODO +//! +//! [Ratatui Website]: https://ratatui.rs/ +//! [Installation]: https://ratatui.rs/installation/ +//! [Rendering]: https://ratatui.rs/concepts/rendering/ +//! [Application Patterns]: https://ratatui.rs/concepts/application-patterns/ +//! [Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/ +//! [Backends]: https://ratatui.rs/concepts/backends/ +//! [Widgets]: https://ratatui.rs/how-to/widgets/ +//! [Handling Events]: https://ratatui.rs/concepts/event-handling/ +//! [Layout]: https://ratatui.rs/how-to/layout/ +//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/ +//! [templates]: https://github.com/ratatui/templates/ +//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md +//! [Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md +//! [Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md +//! [Create a Pull Request]: https://github.com/ratatui/ratatui/compare +//! [git-cliff]: https://git-cliff.org +//! [Conventional Commits]: https://www.conventionalcommits.org +//! [API Docs]: https://docs.rs/ratatui +//! [Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md +//! [Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md +//! [Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md +//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20 +//! [docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true +//! [docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true +//! [docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true +//! [`Frame`]: terminal::Frame +//! [`render_widget`]: terminal::Frame::render_widget +//! [`Widget`]: widgets::Widget +//! [`Layout`]: layout::Layout +//! [`Text`]: text::Text +//! [`Line`]: text::Line +//! [`Span`]: text::Span +//! [`Style`]: style::Style +//! [`style` module]: style +//! [`Stylize`]: style::Stylize +//! [`Backend`]: backend::Backend +//! [`backend` module]: backend +//! [`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html +//! [Crate]: https://crates.io/crates/ratatui +//! [Crossterm]: https://crates.io/crates/crossterm +//! [Termion]: https://crates.io/crates/termion +//! [Termwiz]: https://crates.io/crates/termwiz +//! [tui-rs]: https://crates.io/crates/tui +//! [GitHub Sponsors]: https://github.com/sponsors/ratatui +//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44 +//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3 +//! [CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github +//! [CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml +//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3 +//! [Codecov]: https://app.codecov.io/gh/ratatui/ratatui +//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square +//! [Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui +//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3 +//! [Discord Server]: https://discord.gg/pMCEU9hNEj +//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44 +//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3 +//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org +//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3 +//! [Forum]: https://forum.ratatui.rs +//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3 + +// show the feature flags in the generated documentation +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png", + html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico" +)] + +/// re-export the `crossterm` crate so that users don't have to add it as a dependency +#[cfg(feature = "crossterm")] +pub use crossterm; + +#[cfg(feature = "crossterm")] +mod init; +#[cfg(feature = "crossterm")] +pub use init::{ + init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal, +}; +/// re-export the `termion` crate so that users don't have to add it as a dependency +#[cfg(all(not(windows), feature = "termion"))] +pub use termion; +/// re-export the `termwiz` crate so that users don't have to add it as a dependency +#[cfg(feature = "termwiz")] +pub use termwiz; + +pub mod backend; +pub use ratatui_core::{ + buffer, layout, style, symbols, text, Frame, Terminal, TerminalOptions, Viewport, +}; + +pub mod widgets { + pub use ratatui_core::widgets::*; + pub use ratatui_widgets::*; +} + +pub mod prelude; diff --git a/src/prelude.rs b/ratatui/src/prelude.rs similarity index 76% rename from src/prelude.rs rename to ratatui/src/prelude.rs index b2ee58ca..7769702f 100644 --- a/src/prelude.rs +++ b/ratatui/src/prelude.rs @@ -17,12 +17,13 @@ //! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![])); //! ``` +// TODO: re-export the following modules: #[cfg(feature = "crossterm")] pub use crate::backend::CrosstermBackend; -#[cfg(all(not(windows), feature = "termion"))] -pub use crate::backend::TermionBackend; -#[cfg(feature = "termwiz")] -pub use crate::backend::TermwizBackend; +// #[cfg(all(not(windows), feature = "termion"))] +// pub use crate::backend::TermionBackend; +// #[cfg(feature = "termwiz")] +// pub use crate::backend::TermwizBackend; pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef}; pub use crate::{ backend::{self, Backend}, @@ -31,6 +32,10 @@ pub use crate::{ style::{self, Color, Modifier, Style, Stylize}, symbols::{self}, text::{self, Line, Masked, Span, Text}, - widgets::{block::BlockExt, StatefulWidget, Widget}, + widgets::{ + // block::BlockExt, // TODO + StatefulWidget, + Widget, + }, Frame, Terminal, }; diff --git a/tests/backend_termion.rs b/ratatui/tests/backend_termion.rs similarity index 100% rename from tests/backend_termion.rs rename to ratatui/tests/backend_termion.rs diff --git a/tests/state_serde.rs b/ratatui/tests/state_serde.rs similarity index 100% rename from tests/state_serde.rs rename to ratatui/tests/state_serde.rs diff --git a/tests/stylize.rs b/ratatui/tests/stylize.rs similarity index 100% rename from tests/stylize.rs rename to ratatui/tests/stylize.rs diff --git a/tests/terminal.rs b/ratatui/tests/terminal.rs similarity index 100% rename from tests/terminal.rs rename to ratatui/tests/terminal.rs diff --git a/tests/widgets_barchart.rs b/ratatui/tests/widgets_barchart.rs similarity index 100% rename from tests/widgets_barchart.rs rename to ratatui/tests/widgets_barchart.rs diff --git a/tests/widgets_block.rs b/ratatui/tests/widgets_block.rs similarity index 100% rename from tests/widgets_block.rs rename to ratatui/tests/widgets_block.rs diff --git a/tests/widgets_calendar.rs b/ratatui/tests/widgets_calendar.rs similarity index 100% rename from tests/widgets_calendar.rs rename to ratatui/tests/widgets_calendar.rs diff --git a/tests/widgets_canvas.rs b/ratatui/tests/widgets_canvas.rs similarity index 100% rename from tests/widgets_canvas.rs rename to ratatui/tests/widgets_canvas.rs diff --git a/tests/widgets_chart.rs b/ratatui/tests/widgets_chart.rs similarity index 100% rename from tests/widgets_chart.rs rename to ratatui/tests/widgets_chart.rs diff --git a/tests/widgets_gauge.rs b/ratatui/tests/widgets_gauge.rs similarity index 100% rename from tests/widgets_gauge.rs rename to ratatui/tests/widgets_gauge.rs diff --git a/tests/widgets_list.rs b/ratatui/tests/widgets_list.rs similarity index 100% rename from tests/widgets_list.rs rename to ratatui/tests/widgets_list.rs diff --git a/tests/widgets_paragraph.rs b/ratatui/tests/widgets_paragraph.rs similarity index 100% rename from tests/widgets_paragraph.rs rename to ratatui/tests/widgets_paragraph.rs diff --git a/tests/widgets_table.rs b/ratatui/tests/widgets_table.rs similarity index 100% rename from tests/widgets_table.rs rename to ratatui/tests/widgets_table.rs diff --git a/tests/widgets_tabs.rs b/ratatui/tests/widgets_tabs.rs similarity index 100% rename from tests/widgets_tabs.rs rename to ratatui/tests/widgets_tabs.rs diff --git a/src/backend/crossterm.rs b/src/backend/crossterm.rs deleted file mode 100644 index d374441e..00000000 --- a/src/backend/crossterm.rs +++ /dev/null @@ -1,701 +0,0 @@ -//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses -//! the [Crossterm] crate to interact with the terminal. -//! -//! [Crossterm]: https://crates.io/crates/crossterm -use std::io::{self, Write}; - -#[cfg(feature = "underline-color")] -use crossterm::style::SetUnderlineColor; - -use crate::{ - backend::{Backend, ClearType, WindowSize}, - buffer::Cell, - crossterm::{ - cursor::{Hide, MoveTo, Show}, - execute, queue, - style::{ - Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors, - ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor, - }, - terminal::{self, Clear}, - }, - layout::{Position, Size}, - style::{Color, Modifier, Style}, -}; - -/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal. -/// -/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is -/// used to send commands to the terminal. It provides methods for drawing content, manipulating -/// the cursor, and clearing the terminal screen. -/// -/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead -/// use the [`Terminal`] struct, which provides a more ergonomic interface. -/// -/// Usually applications will enable raw mode and switch to alternate screen mode after creating -/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and -/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions -/// when the application exits). This is not done automatically by the backend because it is -/// possible that the application may want to use the terminal for other purposes (like showing -/// help text) before entering alternate screen mode. -/// -/// # Example -/// -/// ```rust,no_run -/// use std::io::{stderr, stdout}; -/// -/// use ratatui::{ -/// crossterm::{ -/// terminal::{ -/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -/// }, -/// ExecutableCommand, -/// }, -/// prelude::*, -/// }; -/// -/// let mut backend = CrosstermBackend::new(stdout()); -/// // or -/// let backend = CrosstermBackend::new(stderr()); -/// let mut terminal = Terminal::new(backend)?; -/// -/// enable_raw_mode()?; -/// stdout().execute(EnterAlternateScreen)?; -/// -/// terminal.clear()?; -/// terminal.draw(|frame| { -/// // -- snip -- -/// })?; -/// -/// stdout().execute(LeaveAlternateScreen)?; -/// disable_raw_mode()?; -/// -/// # std::io::Result::Ok(()) -/// ``` -/// -/// See the the [Examples] directory for more examples. See the [`backend`] module documentation -/// for more details on raw mode and alternate screen. -/// -/// [`Write`]: std::io::Write -/// [`Terminal`]: crate::terminal::Terminal -/// [`backend`]: crate::backend -/// [Crossterm]: https://crates.io/crates/crossterm -/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct CrosstermBackend { - /// The writer used to send commands to the terminal. - writer: W, -} - -impl CrosstermBackend -where - W: Write, -{ - /// Creates a new `CrosstermBackend` with the given writer. - /// - /// Most applications will use either [`stdout`](std::io::stdout) or - /// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use. - /// - /// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr - /// - /// # Example - /// - /// ```rust,no_run - /// # use std::io::stdout; - /// # use ratatui::prelude::*; - /// let backend = CrosstermBackend::new(stdout()); - /// ``` - pub const fn new(writer: W) -> Self { - Self { writer } - } - - /// Gets the writer. - #[instability::unstable( - feature = "backend-writer", - issue = "https://github.com/ratatui/ratatui/pull/991" - )] - pub const fn writer(&self) -> &W { - &self.writer - } - - /// Gets the writer as a mutable reference. - /// - /// Note: writing to the writer may cause incorrect output after the write. This is due to the - /// way that the Terminal implements diffing Buffers. - #[instability::unstable( - feature = "backend-writer", - issue = "https://github.com/ratatui/ratatui/pull/991" - )] - pub fn writer_mut(&mut self) -> &mut W { - &mut self.writer - } -} - -impl Write for CrosstermBackend -where - W: Write, -{ - /// Writes a buffer of bytes to the underlying buffer. - fn write(&mut self, buf: &[u8]) -> io::Result { - self.writer.write(buf) - } - - /// Flushes the underlying buffer. - fn flush(&mut self) -> io::Result<()> { - self.writer.flush() - } -} - -impl Backend for CrosstermBackend -where - W: Write, -{ - fn draw<'a, I>(&mut self, content: I) -> io::Result<()> - where - I: Iterator, - { - let mut fg = Color::Reset; - let mut bg = Color::Reset; - #[cfg(feature = "underline-color")] - let mut underline_color = Color::Reset; - let mut modifier = Modifier::empty(); - let mut last_pos: Option = None; - for (x, y, cell) in content { - // Move the cursor if the previous location was not (x - 1, y) - if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) { - queue!(self.writer, MoveTo(x, y))?; - } - last_pos = Some(Position { x, y }); - if cell.modifier != modifier { - let diff = ModifierDiff { - from: modifier, - to: cell.modifier, - }; - diff.queue(&mut self.writer)?; - modifier = cell.modifier; - } - if cell.fg != fg || cell.bg != bg { - queue!( - self.writer, - SetColors(Colors::new(cell.fg.into(), cell.bg.into())) - )?; - fg = cell.fg; - bg = cell.bg; - } - #[cfg(feature = "underline-color")] - if cell.underline_color != underline_color { - let color = CColor::from(cell.underline_color); - queue!(self.writer, SetUnderlineColor(color))?; - underline_color = cell.underline_color; - } - - queue!(self.writer, Print(cell.symbol()))?; - } - - #[cfg(feature = "underline-color")] - return queue!( - self.writer, - SetForegroundColor(CColor::Reset), - SetBackgroundColor(CColor::Reset), - SetUnderlineColor(CColor::Reset), - SetAttribute(CAttribute::Reset), - ); - #[cfg(not(feature = "underline-color"))] - return queue!( - self.writer, - SetForegroundColor(CColor::Reset), - SetBackgroundColor(CColor::Reset), - SetAttribute(CAttribute::Reset), - ); - } - - fn hide_cursor(&mut self) -> io::Result<()> { - execute!(self.writer, Hide) - } - - fn show_cursor(&mut self) -> io::Result<()> { - execute!(self.writer, Show) - } - - fn get_cursor_position(&mut self) -> io::Result { - crossterm::cursor::position() - .map(|(x, y)| Position { x, y }) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) - } - - fn set_cursor_position>(&mut self, position: P) -> io::Result<()> { - let Position { x, y } = position.into(); - execute!(self.writer, MoveTo(x, y)) - } - - fn clear(&mut self) -> io::Result<()> { - self.clear_region(ClearType::All) - } - - fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> { - execute!( - self.writer, - Clear(match clear_type { - ClearType::All => crossterm::terminal::ClearType::All, - ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown, - ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp, - ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine, - ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine, - }) - ) - } - - fn append_lines(&mut self, n: u16) -> io::Result<()> { - for _ in 0..n { - queue!(self.writer, Print("\n"))?; - } - self.writer.flush() - } - - fn size(&self) -> io::Result { - let (width, height) = terminal::size()?; - Ok(Size { width, height }) - } - - fn window_size(&mut self) -> io::Result { - let crossterm::terminal::WindowSize { - columns, - rows, - width, - height, - } = terminal::window_size()?; - Ok(WindowSize { - columns_rows: Size { - width: columns, - height: rows, - }, - pixels: Size { width, height }, - }) - } - - fn flush(&mut self) -> io::Result<()> { - self.writer.flush() - } -} - -impl From for CColor { - fn from(color: Color) -> Self { - match color { - Color::Reset => Self::Reset, - Color::Black => Self::Black, - Color::Red => Self::DarkRed, - Color::Green => Self::DarkGreen, - Color::Yellow => Self::DarkYellow, - Color::Blue => Self::DarkBlue, - Color::Magenta => Self::DarkMagenta, - Color::Cyan => Self::DarkCyan, - Color::Gray => Self::Grey, - Color::DarkGray => Self::DarkGrey, - Color::LightRed => Self::Red, - Color::LightGreen => Self::Green, - Color::LightBlue => Self::Blue, - Color::LightYellow => Self::Yellow, - Color::LightMagenta => Self::Magenta, - Color::LightCyan => Self::Cyan, - Color::White => Self::White, - Color::Indexed(i) => Self::AnsiValue(i), - Color::Rgb(r, g, b) => Self::Rgb { r, g, b }, - } - } -} - -impl From for Color { - fn from(value: CColor) -> Self { - match value { - CColor::Reset => Self::Reset, - CColor::Black => Self::Black, - CColor::DarkRed => Self::Red, - CColor::DarkGreen => Self::Green, - CColor::DarkYellow => Self::Yellow, - CColor::DarkBlue => Self::Blue, - CColor::DarkMagenta => Self::Magenta, - CColor::DarkCyan => Self::Cyan, - CColor::Grey => Self::Gray, - CColor::DarkGrey => Self::DarkGray, - CColor::Red => Self::LightRed, - CColor::Green => Self::LightGreen, - CColor::Blue => Self::LightBlue, - CColor::Yellow => Self::LightYellow, - CColor::Magenta => Self::LightMagenta, - CColor::Cyan => Self::LightCyan, - CColor::White => Self::White, - CColor::Rgb { r, g, b } => Self::Rgb(r, g, b), - CColor::AnsiValue(v) => Self::Indexed(v), - } - } -} - -/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier` -/// values. This is useful when updating the terminal display, as it allows for more -/// efficient updates by only sending the necessary changes. -struct ModifierDiff { - pub from: Modifier, - pub to: Modifier, -} - -impl ModifierDiff { - fn queue(self, mut w: W) -> io::Result<()> - where - W: io::Write, - { - //use crossterm::Attribute; - let removed = self.from - self.to; - if removed.contains(Modifier::REVERSED) { - queue!(w, SetAttribute(CAttribute::NoReverse))?; - } - if removed.contains(Modifier::BOLD) { - queue!(w, SetAttribute(CAttribute::NormalIntensity))?; - if self.to.contains(Modifier::DIM) { - queue!(w, SetAttribute(CAttribute::Dim))?; - } - } - if removed.contains(Modifier::ITALIC) { - queue!(w, SetAttribute(CAttribute::NoItalic))?; - } - if removed.contains(Modifier::UNDERLINED) { - queue!(w, SetAttribute(CAttribute::NoUnderline))?; - } - if removed.contains(Modifier::DIM) { - queue!(w, SetAttribute(CAttribute::NormalIntensity))?; - } - if removed.contains(Modifier::CROSSED_OUT) { - queue!(w, SetAttribute(CAttribute::NotCrossedOut))?; - } - if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { - queue!(w, SetAttribute(CAttribute::NoBlink))?; - } - - let added = self.to - self.from; - if added.contains(Modifier::REVERSED) { - queue!(w, SetAttribute(CAttribute::Reverse))?; - } - if added.contains(Modifier::BOLD) { - queue!(w, SetAttribute(CAttribute::Bold))?; - } - if added.contains(Modifier::ITALIC) { - queue!(w, SetAttribute(CAttribute::Italic))?; - } - if added.contains(Modifier::UNDERLINED) { - queue!(w, SetAttribute(CAttribute::Underlined))?; - } - if added.contains(Modifier::DIM) { - queue!(w, SetAttribute(CAttribute::Dim))?; - } - if added.contains(Modifier::CROSSED_OUT) { - queue!(w, SetAttribute(CAttribute::CrossedOut))?; - } - if added.contains(Modifier::SLOW_BLINK) { - queue!(w, SetAttribute(CAttribute::SlowBlink))?; - } - if added.contains(Modifier::RAPID_BLINK) { - queue!(w, SetAttribute(CAttribute::RapidBlink))?; - } - - Ok(()) - } -} - -impl From for Modifier { - fn from(value: CAttribute) -> Self { - // `Attribute*s*` (note the *s*) contains multiple `Attribute` - // We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing - // the conversion again - Self::from(CAttributes::from(value)) - } -} - -impl From for Modifier { - fn from(value: CAttributes) -> Self { - let mut res = Self::empty(); - - if value.has(CAttribute::Bold) { - res |= Self::BOLD; - } - if value.has(CAttribute::Dim) { - res |= Self::DIM; - } - if value.has(CAttribute::Italic) { - res |= Self::ITALIC; - } - if value.has(CAttribute::Underlined) - || value.has(CAttribute::DoubleUnderlined) - || value.has(CAttribute::Undercurled) - || value.has(CAttribute::Underdotted) - || value.has(CAttribute::Underdashed) - { - res |= Self::UNDERLINED; - } - if value.has(CAttribute::SlowBlink) { - res |= Self::SLOW_BLINK; - } - if value.has(CAttribute::RapidBlink) { - res |= Self::RAPID_BLINK; - } - if value.has(CAttribute::Reverse) { - res |= Self::REVERSED; - } - if value.has(CAttribute::Hidden) { - res |= Self::HIDDEN; - } - if value.has(CAttribute::CrossedOut) { - res |= Self::CROSSED_OUT; - } - - res - } -} - -impl From for Style { - fn from(value: ContentStyle) -> Self { - let mut sub_modifier = Modifier::empty(); - - if value.attributes.has(CAttribute::NoBold) { - sub_modifier |= Modifier::BOLD; - } - if value.attributes.has(CAttribute::NoItalic) { - sub_modifier |= Modifier::ITALIC; - } - if value.attributes.has(CAttribute::NotCrossedOut) { - sub_modifier |= Modifier::CROSSED_OUT; - } - if value.attributes.has(CAttribute::NoUnderline) { - sub_modifier |= Modifier::UNDERLINED; - } - if value.attributes.has(CAttribute::NoHidden) { - sub_modifier |= Modifier::HIDDEN; - } - if value.attributes.has(CAttribute::NoBlink) { - sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK; - } - if value.attributes.has(CAttribute::NoReverse) { - sub_modifier |= Modifier::REVERSED; - } - - Self { - fg: value.foreground_color.map(Into::into), - bg: value.background_color.map(Into::into), - #[cfg(feature = "underline-color")] - underline_color: value.underline_color.map(Into::into), - add_modifier: value.attributes.into(), - sub_modifier, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn from_crossterm_color() { - assert_eq!(Color::from(CColor::Reset), Color::Reset); - assert_eq!(Color::from(CColor::Black), Color::Black); - assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray); - assert_eq!(Color::from(CColor::Red), Color::LightRed); - assert_eq!(Color::from(CColor::DarkRed), Color::Red); - assert_eq!(Color::from(CColor::Green), Color::LightGreen); - assert_eq!(Color::from(CColor::DarkGreen), Color::Green); - assert_eq!(Color::from(CColor::Yellow), Color::LightYellow); - assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow); - assert_eq!(Color::from(CColor::Blue), Color::LightBlue); - assert_eq!(Color::from(CColor::DarkBlue), Color::Blue); - assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta); - assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta); - assert_eq!(Color::from(CColor::Cyan), Color::LightCyan); - assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan); - assert_eq!(Color::from(CColor::White), Color::White); - assert_eq!(Color::from(CColor::Grey), Color::Gray); - assert_eq!( - Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }), - Color::Rgb(0, 0, 0) - ); - assert_eq!( - Color::from(CColor::Rgb { - r: 10, - g: 20, - b: 30 - }), - Color::Rgb(10, 20, 30) - ); - assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32)); - assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37)); - } - - mod modifier { - use super::*; - - #[test] - fn from_crossterm_attribute() { - assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty()); - assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD); - assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC); - assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED); - assert_eq!( - Modifier::from(CAttribute::DoubleUnderlined), - Modifier::UNDERLINED - ); - assert_eq!( - Modifier::from(CAttribute::Underdotted), - Modifier::UNDERLINED - ); - assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM); - assert_eq!( - Modifier::from(CAttribute::NormalIntensity), - Modifier::empty() - ); - assert_eq!( - Modifier::from(CAttribute::CrossedOut), - Modifier::CROSSED_OUT - ); - assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty()); - assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty()); - assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK); - assert_eq!( - Modifier::from(CAttribute::RapidBlink), - Modifier::RAPID_BLINK - ); - assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN); - assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty()); - assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED); - } - - #[test] - fn from_crossterm_attributes() { - assert_eq!( - Modifier::from(CAttributes::from(CAttribute::Bold)), - Modifier::BOLD - ); - assert_eq!( - Modifier::from(CAttributes::from( - [CAttribute::Bold, CAttribute::Italic].as_ref() - )), - Modifier::BOLD | Modifier::ITALIC - ); - assert_eq!( - Modifier::from(CAttributes::from( - [CAttribute::Bold, CAttribute::NotCrossedOut].as_ref() - )), - Modifier::BOLD - ); - assert_eq!( - Modifier::from(CAttributes::from( - [CAttribute::Dim, CAttribute::Underdotted].as_ref() - )), - Modifier::DIM | Modifier::UNDERLINED - ); - assert_eq!( - Modifier::from(CAttributes::from( - [CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref() - )), - Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC - ); - assert_eq!( - Modifier::from(CAttributes::from( - [ - CAttribute::Hidden, - CAttribute::NoUnderline, - CAttribute::NotCrossedOut - ] - .as_ref() - )), - Modifier::HIDDEN - ); - assert_eq!( - Modifier::from(CAttributes::from(CAttribute::Reverse)), - Modifier::REVERSED - ); - assert_eq!( - Modifier::from(CAttributes::from(CAttribute::Reset)), - Modifier::empty() - ); - assert_eq!( - Modifier::from(CAttributes::from( - [CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref() - )), - Modifier::RAPID_BLINK | Modifier::CROSSED_OUT - ); - } - } - - #[test] - fn from_crossterm_content_style() { - assert_eq!(Style::from(ContentStyle::default()), Style::default()); - assert_eq!( - Style::from(ContentStyle { - foreground_color: Some(CColor::DarkYellow), - ..Default::default() - }), - Style::default().fg(Color::Yellow) - ); - assert_eq!( - Style::from(ContentStyle { - background_color: Some(CColor::DarkYellow), - ..Default::default() - }), - Style::default().bg(Color::Yellow) - ); - assert_eq!( - Style::from(ContentStyle { - attributes: CAttributes::from(CAttribute::Bold), - ..Default::default() - }), - Style::default().add_modifier(Modifier::BOLD) - ); - assert_eq!( - Style::from(ContentStyle { - attributes: CAttributes::from(CAttribute::NoBold), - ..Default::default() - }), - Style::default().remove_modifier(Modifier::BOLD) - ); - assert_eq!( - Style::from(ContentStyle { - attributes: CAttributes::from(CAttribute::Italic), - ..Default::default() - }), - Style::default().add_modifier(Modifier::ITALIC) - ); - assert_eq!( - Style::from(ContentStyle { - attributes: CAttributes::from(CAttribute::NoItalic), - ..Default::default() - }), - Style::default().remove_modifier(Modifier::ITALIC) - ); - assert_eq!( - Style::from(ContentStyle { - attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()), - ..Default::default() - }), - Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::ITALIC) - ); - assert_eq!( - Style::from(ContentStyle { - attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()), - ..Default::default() - }), - Style::default() - .remove_modifier(Modifier::BOLD) - .remove_modifier(Modifier::ITALIC) - ); - } - - #[test] - #[cfg(feature = "underline-color")] - fn from_crossterm_content_style_underline() { - assert_eq!( - Style::from(ContentStyle { - underline_color: Some(CColor::DarkRed), - ..Default::default() - }), - Style::default().underline_color(Color::Red) - ); - } -}