Skip to content

Commit

Permalink
Support terminal-wg's BiDi draft proposal in alacritty_terminal
Browse files Browse the repository at this point in the history
https://terminal-wg.pages.freedesktop.org/bidi

Implementation is hidden behind a `bidi_draft` crate feature, and
depends on alacritty/vte#112.

Public API is limited to:
 * `Cell::bidi_mode()` method which returns a `BidiMode` enum value.
 * `Cell::bidi_box_mirroring()` method which returns a boolean.
 * `Term` boolean field `bidi_disable_arrow_key_swapping`.

Cell `BidiFlags` is only used internally since relations/interactions
between individual flags are not straight forward, and could lead to
erroneous behavior if it's all left for API consumers to figure out.

`BidiDir` named fields exist in all `BidiMode` variants. This is done
deliberately (instead of e.g. a `BidiMode` struct with a `BidiDir` field)
to signify the different purpose `BidiDir` serves in each mode.

Signed-off-by: Mohammad AlSaleh <[email protected]>
  • Loading branch information
MoSal committed Apr 9, 2024
1 parent 9af7eb1 commit 2295953
Show file tree
Hide file tree
Showing 4 changed files with 561 additions and 8 deletions.
17 changes: 10 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion alacritty_terminal/Cargo.toml
Expand Up @@ -12,6 +12,8 @@ rust-version = "1.70.0"
[features]
default = ["serde"]
serde = ["dep:serde", "bitflags/serde", "vte/serde"]
# Support Bidi/RTL draft proposal from https://terminal-wg.pages.freedesktop.org/bidi
bidi_draft = []

[dependencies]
base64 = "0.22.0"
Expand All @@ -23,7 +25,7 @@ parking_lot = "0.12.0"
polling = "3.0.0"
regex-automata = "0.4.3"
unicode-width = "0.1"
vte = { version = "0.13.0", default-features = false, features = ["ansi", "serde"] }
vte = { git = "https://github.com/MoSal/vte", branch = "scp" , default-features = false, features = ["ansi", "serde"] }
serde = { version = "1", features = ["derive", "rc"], optional = true }

[target.'cfg(unix)'.dependencies]
Expand Down
155 changes: 155 additions & 0 deletions alacritty_terminal/src/term/cell.rs
Expand Up @@ -36,6 +36,97 @@ bitflags! {
}
}

#[cfg(feature = "bidi_draft")]
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub(super) struct BidiFlags: u8 {
/// Set with:
/// `CSI 8 l`
///
/// Reset with:
/// `CSI 8 h`
///
/// Yes, the default is the high state (implicit mode).
const EXPLICIT_DIRECTION = 0b0000_0001;
/// Set with:
/// `CSI 2 SPACE k` (RTL).
/// `CSI 1 SPACE k` (LTR)
///
/// Reset with:
/// `CSI 0 SPACE k` (default)
/// `CSI SPACE k` (default)
///
/// Default paragraph direction is not to be confused with
/// auto-detection. The default is implementation defined, and
/// is often LTR.
const NON_DEFAULT_PARA_DIR = 0b0000_0010;
/// Set with:
/// `CSI 2 SPACE k` (RTL)
///
/// Reset with with:
/// `CSI 1 SPACE k` (LTR)
///
/// Only in effect when `NON_DEFAULT_PARA_DIR` is set.
/// Only acts as fallback when `AUTO_PARA_DIR` is set.
const RTL_PARA_DIR = 0b0000_0100;
/// Set with:
/// `CSI ? 2501 h` (auto)
///
/// Reset with:
/// `CSI ? 2501 l` (default or RTL/LTR)
///
/// Set auto direction for paragraphs, based on their content's detected direction.
/// Implicit paragraph direction is used if no specific direction is detected.
/// This is ignored if `EXPLICIT_DIRECTION` is set.
const AUTO_PARA_DIR = 0b0000_1000;

/// Set with:
/// `CSI ? 2500 h` (mirroring)
///
/// Reset with:
/// `CSI ? 2500 l` (no mirroring)
///
/// Use mirrored glyphs of characters from the box drawing block
/// (U+2500 - U+257F) in RTL spans.
///
/// Visually mirror-able characters from that range don't have the `Bidi_Mirrored` property
/// set. So a Bidi-aware shaper/renderer wouldn't mirror them on its own when detected in
/// RTL spans.
///
/// More info:
/// https://www.unicode.org/reports/tr9/#Mirroring
/// https://www.unicode.org/reports/tr9/#HL6
///
/// Note that this will have an effect in RTL spans, irregardless of paragraph direction.
const BOX_MIRRORING = 0b0001_0000;
}
}

#[cfg(feature = "bidi_draft")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BidiDir {
LTR,
RTL,
// Terminal-defined
Default,
}

#[cfg(feature = "bidi_draft")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BidiMode {
Explicit { forced_dir: BidiDir },
Implicit { para_dir: BidiDir },
Auto { fallback_para_dir: BidiDir },
}

#[cfg(feature = "bidi_draft")]
impl Default for BidiMode {
fn default() -> Self {
Self::Implicit { para_dir: BidiDir::Default }
}
}

/// Counter for hyperlinks without explicit ID.
static HYPERLINK_ID_SUFFIX: AtomicU32 = AtomicU32::new(0);

Expand Down Expand Up @@ -128,6 +219,9 @@ pub struct CellExtra {
underline_color: Option<Color>,

hyperlink: Option<Hyperlink>,

#[cfg(feature = "bidi_draft")]
pub(super) bidi_flags: BidiFlags,
}

/// Content and attributes of a single cell in the terminal grid.
Expand Down Expand Up @@ -222,6 +316,67 @@ impl Cell {
}
}

#[cfg(feature = "bidi_draft")]
impl Cell {
pub(super) fn remove_bidi_flag(&mut self, bidi_flag: BidiFlags) {
self.extra.as_mut().map(|extra| Arc::make_mut(extra).bidi_flags.remove(bidi_flag));
let is_default_inner =
self.extra.as_deref().map(|extra| *extra == Default::default()).unwrap_or(false);
if is_default_inner {
self.extra = None;
}
}

pub(super) fn insert_bidi_flag(&mut self, bidi_flag: BidiFlags) {
if bidi_flag.is_empty() && self.extra.is_none() {
return;
}

let extra = self.extra.get_or_insert(Default::default());
Arc::make_mut(extra).bidi_flags.insert(bidi_flag);
}

#[inline]
pub(super) fn set_bidi_flags(&mut self, bidi_flags: BidiFlags) {
if bidi_flags.is_empty() && self.extra.is_none() {
return;
}

let extra = self.extra.get_or_insert(Default::default());
Arc::make_mut(extra).bidi_flags = bidi_flags;
}

#[inline]
pub(super) fn bidi_flags(&self) -> BidiFlags {
self.extra.as_ref().map(|extra| extra.bidi_flags).unwrap_or_default()
}

#[inline]
pub fn bidi_mode(&self) -> BidiMode {
let bidi_flags = self.bidi_flags();
let dir = if !bidi_flags.contains(BidiFlags::NON_DEFAULT_PARA_DIR) {
BidiDir::Default
} else if bidi_flags.contains(BidiFlags::RTL_PARA_DIR) {
BidiDir::RTL
} else {
BidiDir::LTR
};

if bidi_flags.contains(BidiFlags::EXPLICIT_DIRECTION) {
BidiMode::Explicit { forced_dir: dir }
} else if bidi_flags.contains(BidiFlags::AUTO_PARA_DIR) {
BidiMode::Auto { fallback_para_dir: dir }
} else {
BidiMode::Implicit { para_dir: dir }
}
}

#[inline]
pub fn bidi_box_mirroring(&self) -> bool {
self.bidi_flags().contains(BidiFlags::BOX_MIRRORING)
}
}

impl GridCell for Cell {
#[inline]
fn is_empty(&self) -> bool {
Expand Down

0 comments on commit 2295953

Please sign in to comment.