Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

11 changed files with 28 additions and 304 deletions

View file

@ -13,58 +13,6 @@ Procedure when bumping the version number:
## Unreleased ## Unreleased
## v0.3.4 - 2025-03-8 ## v0.1.0 - 2023-05.14
### Added
- `Frame::set_bell` to print a bell character when the frame is displayed
- `widgets::bell`
## v0.3.3 - 2025-02-28
### Fixed
- Rendering glitches in unicode-based with estimation
## v0.3.2 - 2025-02-23
### Added
- Unicode-based grapheme width estimation method
## v0.3.1 - 2025-02-21
### Fixed
- Rendering glitches, mainly related to emoji
## v0.3.0 - 2024-11-06
### Added
- `Terminal::mark_dirty`
### Changed
- **(breaking)** Updated dependencies
## v0.2.3 - 2024-04-25
### Fixed
- Width measurements of ASCII control characters
- Toss messing up the terminal state
## v0.2.2 - 2024-01-14
### Fixed
- Crash when drawing `widgets::Predrawn` with width 0
## v0.2.1 - 2024-01-05
### Added
- `Frame::set_title`
- `WidgetExt::title`
- `widgets::title`
## v0.2.0 - 2023-08-31
### Changed
- **(breaking)** Updated dependencies
## v0.1.0 - 2023-05-14
Initial release Initial release

View file

@ -1,11 +1,11 @@
[package] [package]
name = "toss" name = "toss"
version = "0.3.4" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.83" async-trait = "0.1.68"
crossterm = "0.28.1" crossterm = "0.26.1"
unicode-linebreak = "0.1.5" unicode-linebreak = "0.1.4"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.10.1"
unicode-width = "0.2.0" unicode-width = "0.1.10"

View file

@ -332,9 +332,6 @@ impl<'a> Iterator for Cells<'a> {
type Item = (u16, u16, &'a Cell); type Item = (u16, u16, &'a Cell);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.x >= self.buffer.size.width {
return None;
}
if self.y >= self.buffer.size.height { if self.y >= self.buffer.size.height {
return None; return None;
} }

View file

@ -7,8 +7,6 @@ use crate::{Pos, Size, Styled, WidthDb};
pub struct Frame { pub struct Frame {
pub(crate) widthdb: WidthDb, pub(crate) widthdb: WidthDb,
pub(crate) buffer: Buffer, pub(crate) buffer: Buffer,
pub(crate) title: Option<String>,
pub(crate) bell: bool,
} }
impl Frame { impl Frame {
@ -26,7 +24,6 @@ impl Frame {
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.buffer.reset(); self.buffer.reset();
self.title = None;
} }
pub fn cursor(&self) -> Option<Pos> { pub fn cursor(&self) -> Option<Pos> {
@ -45,14 +42,6 @@ impl Frame {
self.set_cursor(None); self.set_cursor(None);
} }
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn set_bell(&mut self, bell: bool) {
self.bell = bell;
}
pub fn widthdb(&mut self) -> &mut WidthDb { pub fn widthdb(&mut self) -> &mut WidthDb {
&mut self.widthdb &mut self.widthdb
} }

View file

@ -8,15 +8,15 @@ use crossterm::event::{
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
}; };
use crossterm::style::{Print, PrintStyledContent, StyledContent}; use crossterm::style::{PrintStyledContent, StyledContent};
use crossterm::terminal::{ use crossterm::terminal::{
BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen,
LeaveAlternateScreen, SetTitle, LeaveAlternateScreen,
}; };
use crossterm::{ExecutableCommand, QueueableCommand}; use crossterm::{ExecutableCommand, QueueableCommand};
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod}; use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
/// Wrapper that manages terminal output. /// Wrapper that manages terminal output.
/// ///
@ -68,12 +68,12 @@ impl Terminal {
/// presenting the next frame. /// presenting the next frame.
pub fn suspend(&mut self) -> io::Result<()> { pub fn suspend(&mut self) -> io::Result<()> {
crossterm::terminal::disable_raw_mode()?; crossterm::terminal::disable_raw_mode()?;
self.out.execute(LeaveAlternateScreen)?;
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
self.out.execute(PopKeyboardEnhancementFlags)?;
self.out.execute(DisableBracketedPaste)?; self.out.execute(DisableBracketedPaste)?;
self.out.execute(PopKeyboardEnhancementFlags)?;
} }
self.out.execute(LeaveAlternateScreen)?;
self.out.execute(Show)?; self.out.execute(Show)?;
Ok(()) Ok(())
} }
@ -112,25 +112,11 @@ impl Terminal {
self.frame.widthdb.tab_width self.frame.widthdb.tab_width
} }
/// Set the grapheme width estimation method.
///
/// For more details, see [`WidthEstimationMethod`].
pub fn set_width_estimation_method(&mut self, method: WidthEstimationMethod) {
self.frame.widthdb.estimate = method;
}
/// The grapheme width estimation method.
///
/// For more details, see [`WidthEstimationMethod`].
pub fn width_estimation_method(&mut self) -> WidthEstimationMethod {
self.frame.widthdb.estimate
}
/// Enable or disable grapheme width measurements. /// Enable or disable grapheme width measurements.
/// ///
/// For more details, see [`Self::measuring`]. /// For more details, see [`Self::measuring`].
pub fn set_measuring(&mut self, active: bool) { pub fn set_measuring(&mut self, active: bool) {
self.frame.widthdb.measure = active; self.frame.widthdb.active = active;
} }
/// Whether grapheme widths should be measured or estimated. /// Whether grapheme widths should be measured or estimated.
@ -149,7 +135,7 @@ impl Terminal {
/// Standard Annex #11. This usually works fine, but may break on some emoji /// Standard Annex #11. This usually works fine, but may break on some emoji
/// or other less commonly used character sequences. /// or other less commonly used character sequences.
pub fn measuring(&self) -> bool { pub fn measuring(&self) -> bool {
self.frame.widthdb.measure self.frame.widthdb.active
} }
/// Whether any unmeasured graphemes were seen since the last call to /// Whether any unmeasured graphemes were seen since the last call to
@ -207,12 +193,6 @@ impl Terminal {
&mut self.frame.widthdb &mut self.frame.widthdb
} }
/// Mark the terminal as dirty, forcing a full redraw whenever any variant
/// of [`Self::present`] is called.
pub fn mark_dirty(&mut self) {
self.full_redraw = true;
}
/// Display the current frame on the screen and prepare the next frame. /// Display the current frame on the screen and prepare the next frame.
/// ///
/// Before drawing and presenting a frame, [`Self::measure_widths`] and /// Before drawing and presenting a frame, [`Self::measure_widths`] and
@ -273,8 +253,6 @@ impl Terminal {
self.draw_differences()?; self.draw_differences()?;
self.update_cursor()?; self.update_cursor()?;
self.update_title()?;
self.ring_bell()?;
Ok(()) Ok(())
} }
@ -309,19 +287,4 @@ impl Terminal {
self.out.queue(Hide)?; self.out.queue(Hide)?;
Ok(()) Ok(())
} }
fn update_title(&mut self) -> io::Result<()> {
if let Some(title) = &self.frame.title {
self.out.queue(SetTitle(title.clone()))?;
}
Ok(())
}
fn ring_bell(&mut self) -> io::Result<()> {
if self.frame.bell {
self.out.queue(Print('\x07'))?;
}
self.frame.bell = false;
Ok(())
}
} }

View file

@ -2,7 +2,7 @@ use async_trait::async_trait;
use crate::widgets::{ use crate::widgets::{
Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float, Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float,
JoinSegment, Layer2, Padding, Resize, Title, JoinSegment, Layer2, Padding, Resize,
}; };
use crate::{Frame, Size, WidthDb}; use crate::{Frame, Size, WidthDb};
@ -60,7 +60,6 @@ pub trait WidgetExt: Sized {
{ {
BoxedAsync::new(self) BoxedAsync::new(self)
} }
fn desync(self) -> Desync<Self> { fn desync(self) -> Desync<Self> {
Desync(self) Desync(self)
} }
@ -108,10 +107,6 @@ pub trait WidgetExt: Sized {
fn resize(self) -> Resize<Self> { fn resize(self) -> Resize<Self> {
Resize::new(self) Resize::new(self)
} }
fn title<S: ToString>(self, title: S) -> Title<Self> {
Title::new(self, title)
}
} }
// It would be nice if this could be restricted to types implementing Widget. // It would be nice if this could be restricted to types implementing Widget.

View file

@ -1,5 +1,4 @@
pub mod background; pub mod background;
pub mod bell;
pub mod border; pub mod border;
pub mod boxed; pub mod boxed;
pub mod cursor; pub mod cursor;
@ -14,10 +13,8 @@ pub mod padding;
pub mod predrawn; pub mod predrawn;
pub mod resize; pub mod resize;
pub mod text; pub mod text;
pub mod title;
pub use background::*; pub use background::*;
pub use bell::*;
pub use border::*; pub use border::*;
pub use boxed::*; pub use boxed::*;
pub use cursor::*; pub use cursor::*;
@ -32,4 +29,3 @@ pub use padding::*;
pub use predrawn::*; pub use predrawn::*;
pub use resize::*; pub use resize::*;
pub use text::*; pub use text::*;
pub use title::*;

View file

@ -1,55 +0,0 @@
use crate::{Frame, Size, Widget, WidthDb};
///////////
// State //
///////////
#[derive(Debug, Default, Clone)]
pub struct BellState {
// Whether the bell should be rung the next time the widget is displayed.
pub ring: bool,
}
impl BellState {
pub fn new() -> Self {
Self::default()
}
pub fn widget(&mut self) -> Bell<'_> {
Bell { state: self }
}
}
////////////
// Widget //
////////////
#[derive(Debug)]
pub struct Bell<'a> {
state: &'a mut BellState,
}
impl Bell<'_> {
pub fn state(&mut self) -> &mut BellState {
self.state
}
}
impl<E> Widget<E> for Bell<'_> {
fn size(
&self,
_widthdb: &mut WidthDb,
_max_width: Option<u16>,
_max_height: Option<u16>,
) -> Result<Size, E> {
Ok(Size::ZERO)
}
fn draw(self, frame: &mut Frame) -> Result<(), E> {
if self.state.ring {
frame.set_bell(true);
self.state.ring = false
}
Ok(())
}
}

View file

@ -465,7 +465,7 @@ impl<E> Widget<E> for Editor<'_> {
Ok(Size::new(width, height)) Ok(Size::new(width, height))
} }
fn draw(self, frame: &mut Frame) -> Result<(), E> { fn draw(mut self, frame: &mut Frame) -> Result<(), E> {
let size = frame.size(); let size = frame.size();
let indices = self.indices(frame.widthdb(), Some(size.width)); let indices = self.indices(frame.widthdb(), Some(size.width));
let rows = self.rows(&indices); let rows = self.rows(&indices);

View file

@ -1,59 +0,0 @@
use async_trait::async_trait;
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
#[derive(Debug, Clone)]
pub struct Title<I> {
pub inner: I,
pub title: String,
}
impl<I> Title<I> {
pub fn new<S: ToString>(inner: I, title: S) -> Self {
Self {
inner,
title: title.to_string(),
}
}
}
impl<E, I> Widget<E> for Title<I>
where
I: Widget<E>,
{
fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Result<Size, E> {
self.inner.size(widthdb, max_width, max_height)
}
fn draw(self, frame: &mut Frame) -> Result<(), E> {
self.inner.draw(frame)?;
frame.set_title(Some(self.title));
Ok(())
}
}
#[async_trait]
impl<E, I> AsyncWidget<E> for Title<I>
where
I: AsyncWidget<E> + Send + Sync,
{
async fn size(
&self,
widthdb: &mut WidthDb,
max_width: Option<u16>,
max_height: Option<u16>,
) -> Result<Size, E> {
self.inner.size(widthdb, max_width, max_height).await
}
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
self.inner.draw(frame).await?;
frame.set_title(Some(self.title));
Ok(())
}
}

View file

@ -6,31 +6,14 @@ use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType}; use crossterm::terminal::{Clear, ClearType};
use crossterm::QueueableCommand; use crossterm::QueueableCommand;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::UnicodeWidthStr;
use crate::wrap; use crate::wrap;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum WidthEstimationMethod {
/// Estimate the width of a grapheme using legacy methods.
///
/// Different terminal emulators all use different approaches to determine
/// grapheme widths, so this method will never be able to give a fully
/// correct solution. For that, the only possible approach is measuring the
/// actual grapheme width.
#[default]
Legacy,
/// Estimate the width of a grapheme using the unicode standard in a
/// best-effort manner.
Unicode,
}
/// Measures and stores the with (in terminal coordinates) of graphemes. /// Measures and stores the with (in terminal coordinates) of graphemes.
#[derive(Debug)] #[derive(Debug)]
pub struct WidthDb { pub struct WidthDb {
pub(crate) estimate: WidthEstimationMethod, pub(crate) active: bool,
pub(crate) measure: bool,
pub(crate) tab_width: u8, pub(crate) tab_width: u8,
known: HashMap<String, u8>, known: HashMap<String, u8>,
requested: HashSet<String>, requested: HashSet<String>,
@ -39,8 +22,7 @@ pub struct WidthDb {
impl Default for WidthDb { impl Default for WidthDb {
fn default() -> Self { fn default() -> Self {
Self { Self {
estimate: WidthEstimationMethod::default(), active: false,
measure: false,
tab_width: 8, tab_width: 8,
known: Default::default(), known: Default::default(),
requested: Default::default(), requested: Default::default(),
@ -65,37 +47,14 @@ impl WidthDb {
if grapheme == "\t" { if grapheme == "\t" {
return self.tab_width_at_column(col); return self.tab_width_at_column(col);
} }
if !self.active {
if self.measure { return grapheme.width() as u8;
}
if let Some(width) = self.known.get(grapheme) { if let Some(width) = self.known.get(grapheme) {
return *width; *width
} } else {
self.requested.insert(grapheme.to_string()); self.requested.insert(grapheme.to_string());
} grapheme.width() as u8
match self.estimate {
// A character-wise width calculation is a simple and obvious
// approach to compute character widths. The idea is that dumb
// terminal emulators tend to do something roughly like this, and
// smart terminal emulators try to emulate dumb ones for
// compatibility. In practice, this approach seems to be fairly
// robust.
WidthEstimationMethod::Legacy => grapheme
.chars()
.filter(|c| !c.is_ascii_control())
.flat_map(|c| c.width())
.sum::<usize>()
.try_into()
.unwrap_or(u8::MAX),
// The unicode width crate considers control chars to have a width
// of 1 even though they usually have a width of 0 when displayed.
WidthEstimationMethod::Unicode => grapheme
.split(|c: char| c.is_ascii_control())
.map(|s| s.width())
.sum::<usize>()
.try_into()
.unwrap_or(u8::MAX),
} }
} }
@ -128,7 +87,7 @@ impl WidthDb {
/// Whether any new graphemes have been seen since the last time /// Whether any new graphemes have been seen since the last time
/// [`Self::measure_widths`] was called. /// [`Self::measure_widths`] was called.
pub(crate) fn measuring_required(&self) -> bool { pub(crate) fn measuring_required(&self) -> bool {
self.measure && !self.requested.is_empty() self.active && !self.requested.is_empty()
} }
/// Measure the width of all new graphemes that have been seen since the /// Measure the width of all new graphemes that have been seen since the
@ -138,19 +97,10 @@ impl WidthDb {
/// the terminal. After it finishes, the terminal's contents should be /// the terminal. After it finishes, the terminal's contents should be
/// assumed to be garbage and a full redraw should be performed. /// assumed to be garbage and a full redraw should be performed.
pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> {
if !self.measure { if !self.active {
return Ok(()); return Ok(());
} }
for grapheme in self.requested.drain() { for grapheme in self.requested.drain() {
if grapheme.chars().any(|c| c.is_ascii_control()) {
// ASCII control characters like the escape character or the
// bell character tend to be interpreted specially by terminals.
// This may break width measurements. To avoid this, we just
// assign each control character a with of 0.
self.known.insert(grapheme, 0);
continue;
}
out.queue(Clear(ClearType::All))? out.queue(Clear(ClearType::All))?
.queue(MoveTo(0, 0))? .queue(MoveTo(0, 0))?
.queue(Print(&grapheme))?; .queue(Print(&grapheme))?;