Compare commits

...

10 commits

Author SHA1 Message Date
57aa8c5930 Bump version to 0.3.4 2025-03-08 19:30:44 +01:00
e3af509358 Add bell widget 2025-03-08 19:10:29 +01:00
89b4595ed9 Print bell character 2025-03-08 19:05:38 +01:00
96b2e13c4a Bump version to 0.3.3 2025-02-28 14:30:45 +01:00
712c1537ad Fix incorrect width estimation of ascii control characters 2025-02-28 14:29:53 +01:00
d28ce90ec7 Bump version to 0.3.2 2025-02-23 23:31:25 +01:00
423dd100c1 Add unicode-based grapheme width estimation method 2025-02-23 18:36:42 +01:00
be7eff0979 Bump version to 0.3.1 2025-02-21 00:36:39 +01:00
77a02116a6 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.
2025-02-20 22:10:11 +01:00
1618264cb7 Fix newlines causing bad rendering artifacts
The unicode-width crate has started to consider newlines to have a width
of 1 instead of 0.
2025-02-20 22:08:37 +01:00
7 changed files with 165 additions and 18 deletions

View file

@ -13,6 +13,27 @@ 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`
## 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

View file

@ -1,6 +1,6 @@
[package]
name = "toss"
version = "0.3.0"
version = "0.3.4"
edition = "2021"
[dependencies]

View file

@ -8,6 +8,7 @@ pub struct Frame {
pub(crate) widthdb: WidthDb,
pub(crate) buffer: Buffer,
pub(crate) title: Option<String>,
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
}

View file

@ -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,
@ -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
@ -260,6 +274,7 @@ impl Terminal {
self.draw_differences()?;
self.update_cursor()?;
self.update_title()?;
self.ring_bell()?;
Ok(())
}
@ -301,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(())
}
}

View file

@ -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::*;

55
src/widgets/bell.rs Normal file
View file

@ -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<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

@ -6,14 +6,31 @@ use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType};
use crossterm::QueueableCommand;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
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<String, u8>,
requested: HashSet<String>,
@ -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(),
@ -47,14 +65,37 @@ impl WidthDb {
if grapheme == "\t" {
return self.tab_width_at_column(col);
}
if !self.active {
return grapheme.width() as u8;
}
if self.measure {
if let Some(width) = self.known.get(grapheme) {
*width
} else {
return *width;
}
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),
}
}
@ -87,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
@ -97,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() {