Support terminal-wg's BiDi draft proposal in alacritty_terminal

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 <CE.Mohammad.AlSaleh@gmail.com>
This commit is contained in:
Mohammad AlSaleh 2024-03-03 11:40:28 +03:00
parent 9af7eb1b31
commit 2295953a66
4 changed files with 561 additions and 8 deletions

17
Cargo.lock generated
View File

@ -150,7 +150,7 @@ dependencies = [
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
"utf8parse 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -165,7 +165,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
"utf8parse 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1841,6 +1841,11 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "git+https://github.com/MoSal/vte?branch=scp#96de2f46d92d6560b0dfb6503f38e41fc85ebdbe"
[[package]]
name = "version_check"
version = "0.9.4"
@ -1870,22 +1875,20 @@ dependencies = [
[[package]]
name = "vte"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
source = "git+https://github.com/MoSal/vte?branch=scp#96de2f46d92d6560b0dfb6503f38e41fc85ebdbe"
dependencies = [
"bitflags 2.4.2",
"cursor-icon",
"log",
"serde",
"utf8parse",
"utf8parse 0.2.1 (git+https://github.com/MoSal/vte?branch=scp)",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
source = "git+https://github.com/MoSal/vte?branch=scp#96de2f46d92d6560b0dfb6503f38e41fc85ebdbe"
dependencies = [
"proc-macro2",
"quote",

View File

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

View File

@ -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);
@ -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.
@ -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 {

View File

@ -25,6 +25,8 @@ use crate::vte::ansi::{
KeyboardModesApplyBehavior, NamedColor, NamedMode, NamedPrivateMode, PrivateMode, Rgb,
StandardCharset,
};
#[cfg(feature = "bidi_draft")]
use crate::vte::ansi::{ScpCharPath, ScpUpdateMode};
pub mod cell;
pub mod color;
@ -274,6 +276,16 @@ pub struct Term<T> {
pub selection: Option<Selection>,
#[cfg(feature = "bidi_draft")]
/// BiDi-supporting terminal emulators should swap the right and left arrow keys by
/// default when in RTL paragraph context. This is done to actually match the physical
/// movement of the cursor in RTL text in such contexts.
///
/// This is a global (private) mode of the terminal. The escape sequence `\e[?1243l`
/// disables swapping and sets this field to `true`. The escape sequence `\e[?1243h`
/// restores the default, setting this field to `false` again.
pub bidi_disable_arrow_key_swapping: bool,
/// Currently active grid.
///
/// Tracks the screen buffer currently in use. While the alternate screen buffer is active,
@ -441,6 +453,8 @@ impl<T> Term<T> {
selection: None,
damage,
config: options,
#[cfg(feature = "bidi_draft")]
bidi_disable_arrow_key_swapping: false,
}
}
@ -983,6 +997,9 @@ impl<T> Term<T> {
let flags = self.grid.cursor.template.flags;
let extra = self.grid.cursor.template.extra.clone();
#[cfg(feature = "bidi_draft")]
let bidi_flags = self.grid.cursor.template.bidi_flags();
let mut cursor_cell = self.grid.cursor_cell();
// Clear all related cells when overwriting a fullwidth cell.
@ -1010,6 +1027,11 @@ impl<T> Term<T> {
cursor_cell.bg = bg;
cursor_cell.flags = flags;
cursor_cell.extra = extra;
#[cfg(feature = "bidi_draft")]
{
cursor_cell.set_bidi_flags(bidi_flags);
}
}
#[inline]
@ -1863,6 +1885,16 @@ impl<T: EventListener> Handler for Term<T> {
Attr::Reverse => cursor.template.flags.insert(Flags::INVERSE),
Attr::CancelReverse => cursor.template.flags.remove(Flags::INVERSE),
Attr::Bold => cursor.template.flags.insert(Flags::BOLD),
// For maximum compatibility with VTE's BiDi implementation, use `CancelBold`
// attribute as another way to set `DoubleUnderline`, which happens to be
// the behavior defined in ECMA-48 anyway.
#[cfg(feature = "bidi_draft")]
Attr::CancelBold => {
cursor.template.flags.remove(Flags::ALL_UNDERLINES);
cursor.template.flags.insert(Flags::DOUBLE_UNDERLINE);
},
// Keep `CancelBold` behavior if "bidi_draft" feature is not enabled.
#[cfg(not(feature = "bidi_draft"))]
Attr::CancelBold => cursor.template.flags.remove(Flags::BOLD),
Attr::Dim => cursor.template.flags.insert(Flags::DIM),
Attr::CancelBoldDim => cursor.template.flags.remove(Flags::BOLD | Flags::DIM),
@ -1899,11 +1931,62 @@ impl<T: EventListener> Handler for Term<T> {
}
}
#[cfg(feature = "bidi_draft")]
#[inline]
fn set_scp(&mut self, char_path: ScpCharPath, update_mode: ScpUpdateMode) {
use cell::BidiFlags;
if update_mode != ScpUpdateMode::ImplementationDependant {
debug!(
"Only SCP with zero/default second parameter is supported, as the BiDi draft only \
operates in the data component."
);
return;
}
match char_path {
ScpCharPath::Default => {
trace!("Setting (implementation-defined) default paragraph direction");
self.grid.cursor.template.remove_bidi_flag(BidiFlags::NON_DEFAULT_PARA_DIR);
},
ScpCharPath::LTR => {
trace!("Setting LTR paragraph direction");
self.grid.cursor.template.insert_bidi_flag(BidiFlags::NON_DEFAULT_PARA_DIR);
self.grid.cursor.template.remove_bidi_flag(BidiFlags::RTL_PARA_DIR);
},
ScpCharPath::RTL => {
trace!("Setting RTL paragraph direction");
self.grid.cursor.template.insert_bidi_flag(BidiFlags::NON_DEFAULT_PARA_DIR);
self.grid.cursor.template.insert_bidi_flag(BidiFlags::RTL_PARA_DIR);
},
}
}
#[inline]
fn set_private_mode(&mut self, mode: PrivateMode) {
let mode = match mode {
PrivateMode::Named(mode) => mode,
PrivateMode::Unknown(mode) => {
#[cfg(feature = "bidi_draft")]
{
use cell::BidiFlags;
match mode {
1243 => {
trace!("Re-enabling RTL arrow key swapping");
self.bidi_disable_arrow_key_swapping = false;
},
2500 => {
trace!("Enabling RTL box mirroring");
self.grid.cursor.template.insert_bidi_flag(BidiFlags::BOX_MIRRORING);
},
2501 => {
trace!("Enabling paragraph direction auto-detection");
self.grid.cursor.template.insert_bidi_flag(BidiFlags::AUTO_PARA_DIR);
},
_ => debug!("Ignoring unknown mode {} in set_private_mode", mode),
}
}
#[cfg(not(feature = "bidi_draft"))]
debug!("Ignoring unknown mode {} in set_private_mode", mode);
return;
},
@ -1964,6 +2047,26 @@ impl<T: EventListener> Handler for Term<T> {
let mode = match mode {
PrivateMode::Named(mode) => mode,
PrivateMode::Unknown(mode) => {
#[cfg(feature = "bidi_draft")]
{
use cell::BidiFlags;
match mode {
1243 => {
trace!("Disabling RTL arrow key swapping");
self.bidi_disable_arrow_key_swapping = true;
},
2500 => {
trace!("Disabling RTL box mirroring");
self.grid.cursor.template.remove_bidi_flag(BidiFlags::BOX_MIRRORING);
},
2501 => {
trace!("Disabling paragraph direction auto-detection");
self.grid.cursor.template.remove_bidi_flag(BidiFlags::AUTO_PARA_DIR);
},
_ => debug!("Ignoring unknown mode {} in unset_private_mode", mode),
}
}
#[cfg(not(feature = "bidi_draft"))]
debug!("Ignoring unknown mode {} in unset_private_mode", mode);
return;
},
@ -2065,6 +2168,15 @@ impl<T: EventListener> Handler for Term<T> {
let mode = match mode {
ansi::Mode::Named(mode) => mode,
ansi::Mode::Unknown(mode) => {
#[cfg(feature = "bidi_draft")]
// BDSM
if mode == 8 {
trace!("Re-enabling BiDi implicit mode");
self.grid.cursor.template.remove_bidi_flag(cell::BidiFlags::EXPLICIT_DIRECTION);
} else {
debug!("Ignoring unknown mode {} in set_mode", mode);
}
#[cfg(not(feature = "bidi_draft"))]
debug!("Ignoring unknown mode {} in set_mode", mode);
return;
},
@ -2082,6 +2194,15 @@ impl<T: EventListener> Handler for Term<T> {
let mode = match mode {
ansi::Mode::Named(mode) => mode,
ansi::Mode::Unknown(mode) => {
#[cfg(feature = "bidi_draft")]
// BDSM
if mode == 8 {
trace!("Enabling BiDi explicit mode");
self.grid.cursor.template.insert_bidi_flag(cell::BidiFlags::EXPLICIT_DIRECTION);
} else {
debug!("Ignorning unknown mode {} in unset_mode", mode);
}
#[cfg(not(feature = "bidi_draft"))]
debug!("Ignorning unknown mode {} in unset_mode", mode);
return;
},
@ -3271,3 +3392,275 @@ mod tests {
assert_eq!(version_number("999.99.99"), 9_99_99_99);
}
}
#[cfg(feature = "bidi_draft")]
#[cfg(test)]
mod bidi_tests {
use super::*;
use std::iter;
use crate::event::VoidListener;
use crate::index::{Column as C, Line as L, Point as P};
use crate::term::cell::{BidiDir, BidiMode};
use crate::term::test::TermSize;
fn line_input(term: &mut Term<VoidListener>, s: impl Into<String>) {
iter::repeat(s.into().chars()).flatten().take(term.columns()).for_each(|ch| term.input(ch));
}
fn line_match(term: &Term<VoidListener>, l: L, s: impl Into<String>) {
let s = s.into();
let text = term.bounds_to_string(P::new(l, C(0)), P::new(l, C(term.columns())));
assert_eq!(text, s.repeat(term.columns() / s.chars().count()));
}
#[test]
fn bidi_modes() {
let size = TermSize::new(20, 5);
let mut term = Term::new(Config::default(), &size, VoidListener);
// Starts in implicit default (non-auto)
line_input(&mut term, 'D');
// Set auto-dir
term.set_private_mode(PrivateMode::Unknown(2501));
line_input(&mut term, 'A');
// Explicit overrides all
term.unset_mode(ansi::Mode::Unknown(8));
line_input(&mut term, 'E');
// Back to implicit auto
term.set_mode(ansi::Mode::Unknown(8));
line_input(&mut term, 'A');
// Unset auto
term.unset_private_mode(PrivateMode::Unknown(2501));
line_input(&mut term, 'D');
// ================================================== //
// ===================== Output ===================== //
// ================================================== //
// Default
line_match(&term, L(0), 'D');
assert_eq!(term.grid[L(0)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::Default
});
// Set auto
line_match(&term, L(1), 'A');
assert_eq!(term.grid[L(1)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::Default
});
// Unset implicit => explicit
line_match(&term, L(2), 'E');
assert_eq!(term.grid[L(2)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::Default
});
// Set implicit => auto again
line_match(&term, L(3), 'A');
assert_eq!(term.grid[L(3)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::Default
});
// Unset auto => default again
line_match(&term, L(4), 'D');
assert_eq!(term.grid[L(4)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::Default
});
}
#[test]
fn bidi_dirs() {
let size = TermSize::new(20, 18);
let mut term = Term::new(Config::default(), &size, VoidListener);
// === Implicit (default) mode === //
// Starts in Default dir
line_input(&mut term, "ID");
// Set LTR
term.set_scp(ScpCharPath::LTR, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "IL");
// Set RTL
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "IR");
// Back To Default
term.set_scp(ScpCharPath::Default, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "ID");
// Unhandled update modes (no effect)
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::DataToPresentation);
line_input(&mut term, "ID");
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::PresentationToData);
line_input(&mut term, "ID");
// === Implicit auto-dir === //
term.set_private_mode(PrivateMode::Unknown(2501));
// Starts in Default dir
line_input(&mut term, "AD");
// Set LTR
term.set_scp(ScpCharPath::LTR, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "AL");
// Set RTL
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "AR");
// Back To Default
term.set_scp(ScpCharPath::Default, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "AD");
// Unhandled update modes (no effect)
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::DataToPresentation);
line_input(&mut term, "AD");
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::PresentationToData);
line_input(&mut term, "AD");
// === Explicit === //
term.unset_mode(ansi::Mode::Unknown(8));
// Starts in Default dir
line_input(&mut term, "ED");
// Set LTR
term.set_scp(ScpCharPath::LTR, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "EL");
// Set RTL
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "ER");
// Back To Default
term.set_scp(ScpCharPath::Default, ScpUpdateMode::ImplementationDependant);
line_input(&mut term, "ED");
// Unhandled update modes (no effect)
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::DataToPresentation);
line_input(&mut term, "ED");
term.set_scp(ScpCharPath::RTL, ScpUpdateMode::PresentationToData);
line_input(&mut term, "ED");
// ================================================== //
// ===================== Output ===================== //
// ================================================== //
// === Implicit (default) mode === //
// Default
line_match(&term, L(0), "ID");
assert_eq!(term.grid[L(0)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::Default
});
// LTR
line_match(&term, L(1), "IL");
assert_eq!(term.grid[L(1)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::LTR
});
// RTL
line_match(&term, L(2), "IR");
assert_eq!(term.grid[L(2)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::RTL
});
// Back To Default
line_match(&term, L(3), "ID");
assert_eq!(term.grid[L(3)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::Default
});
// Unhandled update modes (no effect)
line_match(&term, L(4), "ID");
assert_eq!(term.grid[L(4)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::Default
});
line_match(&term, L(5), "ID");
assert_eq!(term.grid[L(5)][C(0)].bidi_mode(), BidiMode::Implicit {
para_dir: BidiDir::Default
});
// === Implicit auto-dir === //
// Default
line_match(&term, L(6), "AD");
assert_eq!(term.grid[L(6)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::Default
});
// LTR
line_match(&term, L(7), "AL");
assert_eq!(term.grid[L(7)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::LTR
});
// RTL
line_match(&term, L(8), "AR");
assert_eq!(term.grid[L(8)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::RTL
});
// Back To Default
line_match(&term, L(9), "AD");
assert_eq!(term.grid[L(9)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::Default
});
// Unhandled update modes (no effect)
line_match(&term, L(10), "AD");
assert_eq!(term.grid[L(10)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::Default
});
line_match(&term, L(11), "AD");
assert_eq!(term.grid[L(11)][C(0)].bidi_mode(), BidiMode::Auto {
fallback_para_dir: BidiDir::Default
});
// === Explicit === //
// Default
line_match(&term, L(12), "ED");
assert_eq!(term.grid[L(12)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::Default
});
// LTR
line_match(&term, L(13), "EL");
assert_eq!(term.grid[L(13)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::LTR
});
// RTL
line_match(&term, L(14), "ER");
assert_eq!(term.grid[L(14)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::RTL
});
// Back To Default
line_match(&term, L(15), "ED");
assert_eq!(term.grid[L(15)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::Default
});
// Unhandled update modes (no effect)
line_match(&term, L(16), "ED");
assert_eq!(term.grid[L(16)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::Default
});
line_match(&term, L(17), "ED");
assert_eq!(term.grid[L(17)][C(0)].bidi_mode(), BidiMode::Explicit {
forced_dir: BidiDir::Default
});
}
// TODO: box mirroring, arrow swapping
}