From f6cbba5231f240f87ed56210f681a69bce42564e Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 31 Aug 2023 13:20:54 +0200 Subject: [PATCH 01/27] Update dependencies --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3de8599..fcb3d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -async-trait = "0.1.68" -crossterm = "0.26.1" -unicode-linebreak = "0.1.4" +async-trait = "0.1.73" +crossterm = "0.27.0" +unicode-linebreak = "0.1.5" unicode-segmentation = "1.10.1" unicode-width = "0.1.10" From 2c7888fa413c9b12bec7d55a73051aa96d59386f Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 31 Aug 2023 13:23:00 +0200 Subject: [PATCH 02/27] Bump version to 0.2.0 --- CHANGELOG.md | 7 ++++++- Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bef79..9083adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ Procedure when bumping the version number: ## Unreleased -## v0.1.0 - 2023-05.14 +## v0.2.0 - 2023-08-31 + +### Changed +- **(breaking)** Updated dependencies + +## v0.1.0 - 2023-05-14 Initial release diff --git a/Cargo.toml b/Cargo.toml index fcb3d86..6832db1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] From 77b4f825c98b84f56351bd2423ae0166d9341992 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:23:18 +0100 Subject: [PATCH 03/27] Fix clippy warning --- src/widgets/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/editor.rs b/src/widgets/editor.rs index 3417ebe..aa36e8e 100644 --- a/src/widgets/editor.rs +++ b/src/widgets/editor.rs @@ -465,7 +465,7 @@ impl Widget for Editor<'_> { Ok(Size::new(width, height)) } - fn draw(mut self, frame: &mut Frame) -> Result<(), E> { + fn draw(self, frame: &mut Frame) -> Result<(), E> { let size = frame.size(); let indices = self.indices(frame.widthdb(), Some(size.width)); let rows = self.rows(&indices); From 2714deeafbc744edcd96462af9655f25d6fe81ed Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:33:08 +0100 Subject: [PATCH 04/27] Add support for setting window title --- CHANGELOG.md | 3 +++ src/frame.rs | 6 ++++++ src/terminal.rs | 10 +++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9083adf..d029ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- `Frame::set_title` + ## v0.2.0 - 2023-08-31 ### Changed diff --git a/src/frame.rs b/src/frame.rs index 2e9bad1..03fbb04 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -7,6 +7,7 @@ use crate::{Pos, Size, Styled, WidthDb}; pub struct Frame { pub(crate) widthdb: WidthDb, pub(crate) buffer: Buffer, + pub(crate) title: Option, } impl Frame { @@ -24,6 +25,7 @@ impl Frame { pub fn reset(&mut self) { self.buffer.reset(); + self.title = None; } pub fn cursor(&self) -> Option { @@ -42,6 +44,10 @@ impl Frame { self.set_cursor(None); } + pub fn set_title(&mut self, title: Option) { + self.title = title; + } + pub fn widthdb(&mut self) -> &mut WidthDb { &mut self.widthdb } diff --git a/src/terminal.rs b/src/terminal.rs index db7dcdd..545a701 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -11,7 +11,7 @@ use crossterm::event::{ use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::terminal::{ BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, - LeaveAlternateScreen, + LeaveAlternateScreen, SetTitle, }; use crossterm::{ExecutableCommand, QueueableCommand}; @@ -253,6 +253,7 @@ impl Terminal { self.draw_differences()?; self.update_cursor()?; + self.update_title()?; Ok(()) } @@ -287,4 +288,11 @@ impl Terminal { self.out.queue(Hide)?; Ok(()) } + + fn update_title(&mut self) -> io::Result<()> { + if let Some(title) = &self.frame.title { + self.out.queue(SetTitle(title.clone()))?; + } + Ok(()) + } } From b757f1be03e9aa66be1c13e2794f104ddf49139a Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:33:51 +0100 Subject: [PATCH 05/27] Add Title widget --- CHANGELOG.md | 2 ++ src/widget.rs | 6 ++++- src/widgets.rs | 2 ++ src/widgets/title.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/widgets/title.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d029ed5..0b8b087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Procedure when bumping the version number: ### Added - `Frame::set_title` +- `WidgetExt::title` +- `widgets::title` ## v0.2.0 - 2023-08-31 diff --git a/src/widget.rs b/src/widget.rs index 58ce562..8aec3d2 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use crate::widgets::{ Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float, - JoinSegment, Layer2, Padding, Resize, + JoinSegment, Layer2, Padding, Resize, Title, }; use crate::{Frame, Size, WidthDb}; @@ -107,6 +107,10 @@ pub trait WidgetExt: Sized { fn resize(self) -> Resize { Resize::new(self) } + + fn title(self, title: S) -> Title { + Title::new(self, title) + } } // It would be nice if this could be restricted to types implementing Widget. diff --git a/src/widgets.rs b/src/widgets.rs index 7c3fc55..28b44d8 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -13,6 +13,7 @@ pub mod padding; pub mod predrawn; pub mod resize; pub mod text; +pub mod title; pub use background::*; pub use border::*; @@ -29,3 +30,4 @@ pub use padding::*; pub use predrawn::*; pub use resize::*; pub use text::*; +pub use title::*; diff --git a/src/widgets/title.rs b/src/widgets/title.rs new file mode 100644 index 0000000..c0dc0d4 --- /dev/null +++ b/src/widgets/title.rs @@ -0,0 +1,59 @@ +use async_trait::async_trait; + +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; + +#[derive(Debug, Clone)] +pub struct Title { + pub inner: I, + pub title: String, +} + +impl Title { + pub fn new(inner: I, title: S) -> Self { + Self { + inner, + title: title.to_string(), + } + } +} + +impl Widget for Title +where + I: Widget, +{ + fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + 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 AsyncWidget for Title +where + I: AsyncWidget + Send + Sync, +{ + async fn size( + &self, + widthdb: &mut WidthDb, + max_width: Option, + max_height: Option, + ) -> Result { + 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(()) + } +} From 44512f1088246052c1b305d9ba644a8f82f50084 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:53:13 +0100 Subject: [PATCH 06/27] Update dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6832db1..e1c4124 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.2.0" edition = "2021" [dependencies] -async-trait = "0.1.73" +async-trait = "0.1.77" crossterm = "0.27.0" unicode-linebreak = "0.1.5" unicode-segmentation = "1.10.1" -unicode-width = "0.1.10" +unicode-width = "0.1.11" From b01ee297d5bdbb3b28cafe2b5b130c2767667974 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 5 Jan 2024 13:54:37 +0100 Subject: [PATCH 07/27] Bump version to 0.2.1 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8b087..e6acbb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.2.1 - 2024-01-05 + ### Added - `Frame::set_title` - `WidgetExt::title` diff --git a/Cargo.toml b/Cargo.toml index e1c4124..1c63cad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.0" +version = "0.2.1" edition = "2021" [dependencies] From 2d604d606cc07a411a61d5a040d3414ba0437aa4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Jan 2024 12:32:10 +0100 Subject: [PATCH 08/27] Fix crash when drawing Predrawn with width 0 --- CHANGELOG.md | 3 +++ src/buffer.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6acbb9..d8276ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Crash when drawing `widgets::Predrawn` with width 0 + ## v0.2.1 - 2024-01-05 ### Added diff --git a/src/buffer.rs b/src/buffer.rs index 094a143..022145b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -332,6 +332,9 @@ impl<'a> Iterator for Cells<'a> { type Item = (u16, u16, &'a Cell); fn next(&mut self) -> Option { + if self.x >= self.buffer.size.width { + return None; + } if self.y >= self.buffer.size.height { return None; } From 761e8baeba09b923e2a409ea7df7bb363fc77fd5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Jan 2024 12:38:33 +0100 Subject: [PATCH 09/27] Bump version to 0.2.2 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8276ab..2e012aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.2.2 - 2024-01-14 + ### Fixed - Crash when drawing `widgets::Predrawn` with width 0 diff --git a/Cargo.toml b/Cargo.toml index 1c63cad..ec48349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.1" +version = "0.2.2" edition = "2021" [dependencies] From 94052c5a65b07ab068aa064cd5fe65b874b43b56 Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 4 Mar 2024 00:08:27 +0100 Subject: [PATCH 10/27] Fix formatting --- src/widget.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget.rs b/src/widget.rs index 8aec3d2..356a047 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -60,6 +60,7 @@ pub trait WidgetExt: Sized { { BoxedAsync::new(self) } + fn desync(self) -> Desync { Desync(self) } From 8556fd8176d234df6910037c51561203c19ff149 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 19:45:35 +0200 Subject: [PATCH 11/27] Fix control character width measurement --- CHANGELOG.md | 3 +++ src/widthdb.rs | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e012aa..a01f614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Width measurements of ASCII control characters + ## v0.2.2 - 2024-01-14 ### Fixed diff --git a/src/widthdb.rs b/src/widthdb.rs index 7d18570..200765f 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -101,6 +101,15 @@ impl WidthDb { return Ok(()); } 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))? .queue(MoveTo(0, 0))? .queue(Print(&grapheme))?; From ef6d75c23a229a1a7fec5d695cdbc502eea81236 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 19:56:09 +0200 Subject: [PATCH 12/27] Fix suspend sequence In my kitty-based setup, I observed the following bug: 1. Run cove[1], a toss-based application, in a kitty tab 2. Exit cove 3. Start lazygit[2] 4. Stage some files and enter a commit message 5. Try to press enter and observe garbage appearing in the text box The bug occurred reliably after running cove, but never occurred if cove was not run in that tab. This commit fixes the bug by making the suspend sequence undo the unsuspend sequence's steps in reverse order. --- CHANGELOG.md | 1 + src/terminal.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a01f614..a466408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Procedure when bumping the version number: ### Fixed - Width measurements of ASCII control characters +- Toss messing up the terminal state ## v0.2.2 - 2024-01-14 diff --git a/src/terminal.rs b/src/terminal.rs index 545a701..33c37fa 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -68,12 +68,12 @@ impl Terminal { /// presenting the next frame. pub fn suspend(&mut self) -> io::Result<()> { crossterm::terminal::disable_raw_mode()?; - self.out.execute(LeaveAlternateScreen)?; #[cfg(not(windows))] { - self.out.execute(DisableBracketedPaste)?; self.out.execute(PopKeyboardEnhancementFlags)?; + self.out.execute(DisableBracketedPaste)?; } + self.out.execute(LeaveAlternateScreen)?; self.out.execute(Show)?; Ok(()) } From 0f7505ebb4dbca778b6f24f496dd6a18fce067db Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 20:13:42 +0200 Subject: [PATCH 13/27] Update dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ec48349..a54a858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.2.2" edition = "2021" [dependencies] -async-trait = "0.1.77" +async-trait = "0.1.80" crossterm = "0.27.0" unicode-linebreak = "0.1.5" -unicode-segmentation = "1.10.1" +unicode-segmentation = "1.11.0" unicode-width = "0.1.11" From b1d7221bae9e1bb57d8e5b49c315dc3ca56e947a Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 25 Apr 2024 20:14:26 +0200 Subject: [PATCH 14/27] Bump version to 0.2.3 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a466408..7728464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.2.3 - 2024-04-25 + ### Fixed - Width measurements of ASCII control characters - Toss messing up the terminal state diff --git a/Cargo.toml b/Cargo.toml index a54a858..618da80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.2" +version = "0.2.3" edition = "2021" [dependencies] From 3a5ce3832beb159d657f9dd74b827c53be13d8d2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Nov 2024 22:16:47 +0100 Subject: [PATCH 15/27] Add Terminal::mark_dirty --- CHANGELOG.md | 3 +++ src/terminal.rs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7728464..352a2df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- `Terminal::mark_dirty` + ## v0.2.3 - 2024-04-25 ### Fixed diff --git a/src/terminal.rs b/src/terminal.rs index 33c37fa..439ce4e 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -193,6 +193,12 @@ impl Terminal { &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. /// /// Before drawing and presenting a frame, [`Self::measure_widths`] and From 65f31a2697396f80c796fe485c6d7473e318826a Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Nov 2024 22:13:11 +0100 Subject: [PATCH 16/27] Update dependencies --- CHANGELOG.md | 3 +++ Cargo.toml | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 352a2df..7c2eb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Procedure when bumping the version number: ### Added - `Terminal::mark_dirty` +### Changed +- **(breaking)** Updated dependencies + ## v0.2.3 - 2024-04-25 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 618da80..894e169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.2.3" edition = "2021" [dependencies] -async-trait = "0.1.80" -crossterm = "0.27.0" +async-trait = "0.1.83" +crossterm = "0.28.1" unicode-linebreak = "0.1.5" -unicode-segmentation = "1.11.0" -unicode-width = "0.1.11" +unicode-segmentation = "1.12.0" +unicode-width = "0.2.0" From 73a0268dfd1f90496a92ef2486363f2f5e83770a Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 6 Nov 2024 22:21:20 +0100 Subject: [PATCH 17/27] Bump version to 0.3.0 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2eb3f..13edb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.0 - 2024-11-06 + ### Added - `Terminal::mark_dirty` diff --git a/Cargo.toml b/Cargo.toml index 894e169..4083c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.2.3" +version = "0.3.0" edition = "2021" [dependencies] From 1618264cb701917e90353b70bc6edd534bfdecd8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 20 Feb 2025 21:24:07 +0100 Subject: [PATCH 18/27] Fix newlines causing bad rendering artifacts The unicode-width crate has started to consider newlines to have a width of 1 instead of 0. --- src/widthdb.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/widthdb.rs b/src/widthdb.rs index 200765f..9815190 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -47,9 +47,13 @@ impl WidthDb { if grapheme == "\t" { return self.tab_width_at_column(col); } + if grapheme.chars().any(|c| c.is_ascii_control()) { + return 0; // See measure_widths function + } if !self.active { return grapheme.width() as u8; } + if let Some(width) = self.known.get(grapheme) { *width } else { @@ -101,7 +105,7 @@ impl WidthDb { return Ok(()); } for grapheme in self.requested.drain() { - if grapheme.chars().any(|c|c.is_ascii_control()){ + 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 From 77a02116a64d300264277bfade5553a1a8d9f01d Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 20 Feb 2025 22:08:10 +0100 Subject: [PATCH 19/27] Fix grapheme width estimation I'm pretty sure it still breaks in lots of terminal emulators, but it works far better than what recent versions of the unicode_width crate were doing. --- CHANGELOG.md | 3 +++ src/widthdb.rs | 30 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13edb2a..e18e88a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Rendering glitches, mainly related to emoji + ## v0.3.0 - 2024-11-06 ### Added diff --git a/src/widthdb.rs b/src/widthdb.rs index 9815190..53f20ec 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -6,7 +6,7 @@ use crossterm::style::Print; use crossterm::terminal::{Clear, ClearType}; use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use crate::wrap; @@ -36,6 +36,26 @@ impl WidthDb { self.tab_width - (col % self.tab_width as usize) as u8 } + /// Estimate what our terminal emulator thinks the width of a grapheme is. + /// + /// Different terminal emulators are all broken in different ways, so this + /// method will never be able to give a correct solution. For that, the only + /// possible method is actually measuring. + /// + /// Instead, it implements a character-wise width calculation. The hope is + /// that dumb terminal emulators do something roughly like this, and smart + /// terminal emulators try to emulate dumb ones for compatibility. In + /// practice, this counting approach seems to be fairly robust. + fn grapheme_width_estimate(grapheme: &str) -> u8 { + grapheme + .chars() + .filter(|c| !c.is_ascii_control()) + .flat_map(|c| c.width()) + .sum::() + .try_into() + .unwrap_or(u8::MAX) + } + /// Determine the width of a grapheme. /// /// If the grapheme is a tab, the column is used to determine its width. @@ -47,18 +67,14 @@ impl WidthDb { if grapheme == "\t" { return self.tab_width_at_column(col); } - if grapheme.chars().any(|c| c.is_ascii_control()) { - return 0; // See measure_widths function - } if !self.active { - return grapheme.width() as u8; + return Self::grapheme_width_estimate(grapheme); } - if let Some(width) = self.known.get(grapheme) { *width } else { self.requested.insert(grapheme.to_string()); - grapheme.width() as u8 + Self::grapheme_width_estimate(grapheme) } } From be7eff0979e0e95d070e7c9cea42c328ffd04cc4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 21 Feb 2025 00:36:39 +0100 Subject: [PATCH 20/27] Bump version to 0.3.1 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e18e88a..d84e1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.1 - 2025-02-21 + ### Fixed - Rendering glitches, mainly related to emoji diff --git a/Cargo.toml b/Cargo.toml index 4083c28..5adf243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] From 423dd100c1360decffc5107ea4757d751ac0f4db Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 23 Feb 2025 17:19:59 +0100 Subject: [PATCH 21/27] Add unicode-based grapheme width estimation method --- CHANGELOG.md | 3 ++ src/terminal.rs | 20 ++++++++++-- src/widthdb.rs | 85 ++++++++++++++++++++++++++++++------------------- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d84e1fe..6292746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- Unicode-based grapheme width estimation method + ## v0.3.1 - 2025-02-21 ### Fixed diff --git a/src/terminal.rs b/src/terminal.rs index 439ce4e..c26b0fc 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -16,7 +16,7 @@ use crossterm::terminal::{ use crossterm::{ExecutableCommand, QueueableCommand}; use crate::buffer::Buffer; -use crate::{AsyncWidget, Frame, Size, Widget, WidthDb}; +use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod}; /// Wrapper that manages terminal output. /// @@ -112,11 +112,25 @@ impl Terminal { 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. /// /// For more details, see [`Self::measuring`]. pub fn set_measuring(&mut self, active: bool) { - self.frame.widthdb.active = active; + self.frame.widthdb.measure = active; } /// Whether grapheme widths should be measured or estimated. @@ -135,7 +149,7 @@ impl Terminal { /// Standard Annex #11. This usually works fine, but may break on some emoji /// or other less commonly used character sequences. pub fn measuring(&self) -> bool { - self.frame.widthdb.active + self.frame.widthdb.measure } /// Whether any unmeasured graphemes were seen since the last call to diff --git a/src/widthdb.rs b/src/widthdb.rs index 53f20ec..bb21ef6 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -6,14 +6,31 @@ use crossterm::style::Print; use crossterm::terminal::{Clear, ClearType}; use crossterm::QueueableCommand; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthChar; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; 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. #[derive(Debug)] pub struct WidthDb { - pub(crate) active: bool, + pub(crate) estimate: WidthEstimationMethod, + pub(crate) measure: bool, pub(crate) tab_width: u8, known: HashMap, requested: HashSet, @@ -22,7 +39,8 @@ pub struct WidthDb { impl Default for WidthDb { fn default() -> Self { Self { - active: false, + estimate: WidthEstimationMethod::default(), + measure: false, tab_width: 8, known: Default::default(), requested: Default::default(), @@ -36,26 +54,6 @@ impl WidthDb { self.tab_width - (col % self.tab_width as usize) as u8 } - /// Estimate what our terminal emulator thinks the width of a grapheme is. - /// - /// Different terminal emulators are all broken in different ways, so this - /// method will never be able to give a correct solution. For that, the only - /// possible method is actually measuring. - /// - /// Instead, it implements a character-wise width calculation. The hope is - /// that dumb terminal emulators do something roughly like this, and smart - /// terminal emulators try to emulate dumb ones for compatibility. In - /// practice, this counting approach seems to be fairly robust. - fn grapheme_width_estimate(grapheme: &str) -> u8 { - grapheme - .chars() - .filter(|c| !c.is_ascii_control()) - .flat_map(|c| c.width()) - .sum::() - .try_into() - .unwrap_or(u8::MAX) - } - /// Determine the width of a grapheme. /// /// If the grapheme is a tab, the column is used to determine its width. @@ -67,14 +65,37 @@ impl WidthDb { if grapheme == "\t" { return self.tab_width_at_column(col); } - if !self.active { - return Self::grapheme_width_estimate(grapheme); - } - if let Some(width) = self.known.get(grapheme) { - *width - } else { + + if self.measure { + if let Some(width) = self.known.get(grapheme) { + return *width; + } self.requested.insert(grapheme.to_string()); - Self::grapheme_width_estimate(grapheme) + } + + 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::() + .try_into() + .unwrap_or(u8::MAX), + + // The unicode width crate considers newlines to have a width of 1 + // while the rendering code expects it to have a width of 0. + WidthEstimationMethod::Unicode => grapheme + .split('\n') + .map(|s| s.width()) + .sum::() + .try_into() + .unwrap_or(u8::MAX), } } @@ -107,7 +128,7 @@ impl WidthDb { /// Whether any new graphemes have been seen since the last time /// [`Self::measure_widths`] was called. pub(crate) fn measuring_required(&self) -> bool { - self.active && !self.requested.is_empty() + self.measure && !self.requested.is_empty() } /// Measure the width of all new graphemes that have been seen since the @@ -117,7 +138,7 @@ impl WidthDb { /// the terminal. After it finishes, the terminal's contents should be /// assumed to be garbage and a full redraw should be performed. pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { - if !self.active { + if !self.measure { return Ok(()); } for grapheme in self.requested.drain() { From d28ce90ec7590778e6035a7b00b1d85064f03dbf Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 23 Feb 2025 23:31:25 +0100 Subject: [PATCH 22/27] Bump version to 0.3.2 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6292746..0305201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.2 - 2025-02-23 + ### Added - Unicode-based grapheme width estimation method diff --git a/Cargo.toml b/Cargo.toml index 5adf243..af2b654 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.1" +version = "0.3.2" edition = "2021" [dependencies] From 712c1537adbef0db607d39c2320a4d8d0b2a5b36 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Feb 2025 14:29:53 +0100 Subject: [PATCH 23/27] Fix incorrect width estimation of ascii control characters --- CHANGELOG.md | 3 +++ src/widthdb.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0305201..b70bde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Fixed +- Rendering glitches in unicode-based with estimation + ## v0.3.2 - 2025-02-23 ### Added diff --git a/src/widthdb.rs b/src/widthdb.rs index bb21ef6..fe5a26e 100644 --- a/src/widthdb.rs +++ b/src/widthdb.rs @@ -88,10 +88,10 @@ impl WidthDb { .try_into() .unwrap_or(u8::MAX), - // The unicode width crate considers newlines to have a width of 1 - // while the rendering code expects it to have a width of 0. + // 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('\n') + .split(|c: char| c.is_ascii_control()) .map(|s| s.width()) .sum::() .try_into() From 96b2e13c4a4b0174601d90246d92d148c4230eeb Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Feb 2025 14:30:45 +0100 Subject: [PATCH 24/27] Bump version to 0.3.3 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70bde6..48e4615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.3 - 2025-02-28 + ### Fixed - Rendering glitches in unicode-based with estimation diff --git a/Cargo.toml b/Cargo.toml index af2b654..6868881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.2" +version = "0.3.3" edition = "2021" [dependencies] From 89b4595ed9228df2ce976e4b0630f5dba1208474 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Mar 2025 19:05:38 +0100 Subject: [PATCH 25/27] Print bell character --- CHANGELOG.md | 3 +++ src/frame.rs | 5 +++++ src/terminal.rs | 11 ++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48e4615..c43f3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Procedure when bumping the version number: ## Unreleased +### Added +- `Frame::set_bell` to print a bell character when the frame is displayed + ## v0.3.3 - 2025-02-28 ### Fixed diff --git a/src/frame.rs b/src/frame.rs index 03fbb04..e42ba6b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -8,6 +8,7 @@ pub struct Frame { pub(crate) widthdb: WidthDb, pub(crate) buffer: Buffer, pub(crate) title: Option, + pub(crate) bell: bool, } impl Frame { @@ -48,6 +49,10 @@ impl Frame { self.title = title; } + pub fn set_bell(&mut self, bell: bool) { + self.bell = bell; + } + pub fn widthdb(&mut self) -> &mut WidthDb { &mut self.widthdb } diff --git a/src/terminal.rs b/src/terminal.rs index c26b0fc..07fe686 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -8,7 +8,7 @@ use crossterm::event::{ DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; -use crossterm::style::{PrintStyledContent, StyledContent}; +use crossterm::style::{Print, PrintStyledContent, StyledContent}; use crossterm::terminal::{ BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, @@ -274,6 +274,7 @@ impl Terminal { self.draw_differences()?; self.update_cursor()?; self.update_title()?; + self.ring_bell()?; Ok(()) } @@ -315,4 +316,12 @@ impl Terminal { } Ok(()) } + + fn ring_bell(&mut self) -> io::Result<()> { + if self.frame.bell { + self.out.queue(Print('\x07'))?; + } + self.frame.bell = false; + Ok(()) + } } From e3af509358dd57e24e3073de8a32640d6a3832d5 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Mar 2025 19:10:29 +0100 Subject: [PATCH 26/27] Add bell widget --- CHANGELOG.md | 1 + src/widgets.rs | 2 ++ src/widgets/bell.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/widgets/bell.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c43f3dc..c30abdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Procedure when bumping the version number: ### Added - `Frame::set_bell` to print a bell character when the frame is displayed +- `widgets::bell` ## v0.3.3 - 2025-02-28 diff --git a/src/widgets.rs b/src/widgets.rs index 28b44d8..cbbff7c 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,4 +1,5 @@ pub mod background; +pub mod bell; pub mod border; pub mod boxed; pub mod cursor; @@ -16,6 +17,7 @@ pub mod text; pub mod title; pub use background::*; +pub use bell::*; pub use border::*; pub use boxed::*; pub use cursor::*; diff --git a/src/widgets/bell.rs b/src/widgets/bell.rs new file mode 100644 index 0000000..b37fb67 --- /dev/null +++ b/src/widgets/bell.rs @@ -0,0 +1,55 @@ +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 Widget for Bell<'_> { + fn size( + &self, + _widthdb: &mut WidthDb, + _max_width: Option, + _max_height: Option, + ) -> Result { + Ok(Size::ZERO) + } + + fn draw(self, frame: &mut Frame) -> Result<(), E> { + if self.state.ring { + frame.set_bell(true); + self.state.ring = false + } + Ok(()) + } +} From 57aa8c59308f6f0aa82bde415a42b56c3d6f7c4d Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 8 Mar 2025 19:30:44 +0100 Subject: [PATCH 27/27] Bump version to 0.3.4 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30abdf..bef60bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Procedure when bumping the version number: ## Unreleased +## v0.3.4 - 2025-03-8 + ### Added - `Frame::set_bell` to print a bell character when the frame is displayed - `widgets::bell` diff --git a/Cargo.toml b/Cargo.toml index 6868881..22967fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toss" -version = "0.3.3" +version = "0.3.4" edition = "2021" [dependencies]