Compare commits
27 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57aa8c5930 | |||
| e3af509358 | |||
| 89b4595ed9 | |||
| 96b2e13c4a | |||
| 712c1537ad | |||
| d28ce90ec7 | |||
| 423dd100c1 | |||
| be7eff0979 | |||
| 77a02116a6 | |||
| 1618264cb7 | |||
| 73a0268dfd | |||
| 65f31a2697 | |||
| 3a5ce3832b | |||
| b1d7221bae | |||
| 0f7505ebb4 | |||
| ef6d75c23a | |||
| 8556fd8176 | |||
| 94052c5a65 | |||
| 761e8baeba | |||
| 2d604d606c | |||
| b01ee297d5 | |||
| 44512f1088 | |||
| b757f1be03 | |||
| 2714deeafb | |||
| 77b4f825c9 | |||
| 2c7888fa41 | |||
| f6cbba5231 |
11 changed files with 304 additions and 28 deletions
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -13,6 +13,58 @@ Procedure when bumping the version number:
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## v0.1.0 - 2023-05.14
|
## 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
|
||||||
|
- `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
|
||||||
|
|
|
||||||
12
Cargo.toml
12
Cargo.toml
|
|
@ -1,11 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "toss"
|
name = "toss"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.68"
|
async-trait = "0.1.83"
|
||||||
crossterm = "0.26.1"
|
crossterm = "0.28.1"
|
||||||
unicode-linebreak = "0.1.4"
|
unicode-linebreak = "0.1.5"
|
||||||
unicode-segmentation = "1.10.1"
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width = "0.1.10"
|
unicode-width = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/frame.rs
11
src/frame.rs
|
|
@ -7,6 +7,8 @@ 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 {
|
||||||
|
|
@ -24,6 +26,7 @@ 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> {
|
||||||
|
|
@ -42,6 +45,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@ use crossterm::event::{
|
||||||
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
|
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
|
||||||
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||||
};
|
};
|
||||||
use crossterm::style::{PrintStyledContent, StyledContent};
|
use crossterm::style::{Print, PrintStyledContent, StyledContent};
|
||||||
use crossterm::terminal::{
|
use crossterm::terminal::{
|
||||||
BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen,
|
BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen,
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen, SetTitle,
|
||||||
};
|
};
|
||||||
use crossterm::{ExecutableCommand, QueueableCommand};
|
use crossterm::{ExecutableCommand, QueueableCommand};
|
||||||
|
|
||||||
use crate::buffer::Buffer;
|
use crate::buffer::Buffer;
|
||||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod};
|
||||||
|
|
||||||
/// 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(DisableBracketedPaste)?;
|
|
||||||
self.out.execute(PopKeyboardEnhancementFlags)?;
|
self.out.execute(PopKeyboardEnhancementFlags)?;
|
||||||
|
self.out.execute(DisableBracketedPaste)?;
|
||||||
}
|
}
|
||||||
|
self.out.execute(LeaveAlternateScreen)?;
|
||||||
self.out.execute(Show)?;
|
self.out.execute(Show)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -112,11 +112,25 @@ 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.active = active;
|
self.frame.widthdb.measure = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether grapheme widths should be measured or estimated.
|
/// 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
|
/// 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.active
|
self.frame.widthdb.measure
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether any unmeasured graphemes were seen since the last call to
|
/// Whether any unmeasured graphemes were seen since the last call to
|
||||||
|
|
@ -193,6 +207,12 @@ 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
|
||||||
|
|
@ -253,6 +273,8 @@ impl Terminal {
|
||||||
|
|
||||||
self.draw_differences()?;
|
self.draw_differences()?;
|
||||||
self.update_cursor()?;
|
self.update_cursor()?;
|
||||||
|
self.update_title()?;
|
||||||
|
self.ring_bell()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -287,4 +309,19 @@ 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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
JoinSegment, Layer2, Padding, Resize, Title,
|
||||||
};
|
};
|
||||||
use crate::{Frame, Size, WidthDb};
|
use crate::{Frame, Size, WidthDb};
|
||||||
|
|
||||||
|
|
@ -60,6 +60,7 @@ pub trait WidgetExt: Sized {
|
||||||
{
|
{
|
||||||
BoxedAsync::new(self)
|
BoxedAsync::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desync(self) -> Desync<Self> {
|
fn desync(self) -> Desync<Self> {
|
||||||
Desync(self)
|
Desync(self)
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +108,10 @@ 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.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -13,8 +14,10 @@ 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::*;
|
||||||
|
|
@ -29,3 +32,4 @@ pub use padding::*;
|
||||||
pub use predrawn::*;
|
pub use predrawn::*;
|
||||||
pub use resize::*;
|
pub use resize::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
|
pub use title::*;
|
||||||
|
|
|
||||||
55
src/widgets/bell.rs
Normal file
55
src/widgets/bell.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -465,7 +465,7 @@ impl<E> Widget<E> for Editor<'_> {
|
||||||
Ok(Size::new(width, height))
|
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 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);
|
||||||
|
|
|
||||||
59
src/widgets/title.rs
Normal file
59
src/widgets/title.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,31 @@ 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::UnicodeWidthStr;
|
use unicode_width::{UnicodeWidthChar, 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) active: bool,
|
pub(crate) estimate: WidthEstimationMethod,
|
||||||
|
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>,
|
||||||
|
|
@ -22,7 +39,8 @@ pub struct WidthDb {
|
||||||
impl Default for WidthDb {
|
impl Default for WidthDb {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
active: false,
|
estimate: WidthEstimationMethod::default(),
|
||||||
|
measure: false,
|
||||||
tab_width: 8,
|
tab_width: 8,
|
||||||
known: Default::default(),
|
known: Default::default(),
|
||||||
requested: Default::default(),
|
requested: Default::default(),
|
||||||
|
|
@ -47,14 +65,37 @@ 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 {
|
|
||||||
return grapheme.width() as u8;
|
if self.measure {
|
||||||
}
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +128,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.active && !self.requested.is_empty()
|
self.measure && !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
|
||||||
|
|
@ -97,10 +138,19 @@ 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.active {
|
if !self.measure {
|
||||||
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))?;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue