Compare commits
No commits in common. "master" and "v0.3.1" have entirely different histories.
7 changed files with 37 additions and 159 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -13,22 +13,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "toss"
|
||||
version = "0.3.4"
|
||||
version = "0.3.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ pub struct Frame {
|
|||
pub(crate) widthdb: WidthDb,
|
||||
pub(crate) buffer: Buffer,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) bell: bool,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
|
|
@ -49,10 +48,6 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crossterm::event::{
|
|||
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
|
||||
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::style::{Print, PrintStyledContent, StyledContent};
|
||||
use crossterm::style::{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, WidthEstimationMethod};
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||
|
||||
/// Wrapper that manages terminal output.
|
||||
///
|
||||
|
|
@ -112,25 +112,11 @@ 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.measure = active;
|
||||
self.frame.widthdb.active = active;
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// or other less commonly used character sequences.
|
||||
pub fn measuring(&self) -> bool {
|
||||
self.frame.widthdb.measure
|
||||
self.frame.widthdb.active
|
||||
}
|
||||
|
||||
/// Whether any unmeasured graphemes were seen since the last call to
|
||||
|
|
@ -274,7 +260,6 @@ impl Terminal {
|
|||
self.draw_differences()?;
|
||||
self.update_cursor()?;
|
||||
self.update_title()?;
|
||||
self.ring_bell()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -316,12 +301,4 @@ impl Terminal {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ring_bell(&mut self) -> io::Result<()> {
|
||||
if self.frame.bell {
|
||||
self.out.queue(Print('\x07'))?;
|
||||
}
|
||||
self.frame.bell = false;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
pub mod background;
|
||||
pub mod bell;
|
||||
pub mod border;
|
||||
pub mod boxed;
|
||||
pub mod cursor;
|
||||
|
|
@ -17,7 +16,6 @@ pub mod text;
|
|||
pub mod title;
|
||||
|
||||
pub use background::*;
|
||||
pub use bell::*;
|
||||
pub use border::*;
|
||||
pub use boxed::*;
|
||||
pub use cursor::*;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -6,31 +6,14 @@ use crossterm::style::Print;
|
|||
use crossterm::terminal::{Clear, ClearType};
|
||||
use crossterm::QueueableCommand;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
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) estimate: WidthEstimationMethod,
|
||||
pub(crate) measure: bool,
|
||||
pub(crate) active: bool,
|
||||
pub(crate) tab_width: u8,
|
||||
known: HashMap<String, u8>,
|
||||
requested: HashSet<String>,
|
||||
|
|
@ -39,8 +22,7 @@ pub struct WidthDb {
|
|||
impl Default for WidthDb {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
estimate: WidthEstimationMethod::default(),
|
||||
measure: false,
|
||||
active: false,
|
||||
tab_width: 8,
|
||||
known: Default::default(),
|
||||
requested: Default::default(),
|
||||
|
|
@ -54,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::<usize>()
|
||||
.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.
|
||||
|
|
@ -65,37 +67,14 @@ impl WidthDb {
|
|||
if grapheme == "\t" {
|
||||
return self.tab_width_at_column(col);
|
||||
}
|
||||
|
||||
if self.measure {
|
||||
if !self.active {
|
||||
return Self::grapheme_width_estimate(grapheme);
|
||||
}
|
||||
if let Some(width) = self.known.get(grapheme) {
|
||||
return *width;
|
||||
}
|
||||
*width
|
||||
} else {
|
||||
self.requested.insert(grapheme.to_string());
|
||||
}
|
||||
|
||||
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),
|
||||
Self::grapheme_width_estimate(grapheme)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +107,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.measure && !self.requested.is_empty()
|
||||
self.active && !self.requested.is_empty()
|
||||
}
|
||||
|
||||
/// Measure the width of all new graphemes that have been seen since the
|
||||
|
|
@ -138,7 +117,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.measure {
|
||||
if !self.active {
|
||||
return Ok(());
|
||||
}
|
||||
for grapheme in self.requested.drain() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue