Skip to content

Commit fe08d52

Browse files
committed
terminal.rs: add force_text_emoji_presentation option
If true, text presentations of color symbols and emoji will be enforced as much as possible. Might not work on all non-text symbols and is experimental. Signed-off-by: Manos Pitsidianakis <[email protected]>
1 parent 39fbb16 commit fe08d52

File tree

6 files changed

+179
-7
lines changed

6 files changed

+179
-7
lines changed

meli/docs/meli.conf.5

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,11 @@ If false, no
19121912
.Tn ANSI
19131913
colors are used.
19141914
.Pq Em true \" default value
1915+
.It Ic force_text_presentation Ar boolean
1916+
.Pq Em optional
1917+
If true, text presentations of color symbols and emoji will be enforced as much as possible.
1918+
Might not work on all non-text symbols and is experimental.
1919+
.Pq Em false \" default value
19151920
.It Ic window_title Ar String
19161921
.Pq Em optional
19171922
Set window title in xterm compatible terminals An empty string means no window

meli/src/conf/terminal.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ pub struct TerminalSettings {
3434
pub themes: Themes,
3535
pub ascii_drawing: bool,
3636
pub use_color: ToggleFlag,
37+
/// Try forcing text presentations of symbols and emoji as much as possible.
38+
/// Might not work on all non-text symbols and is experimental.
39+
pub force_text_presentation: ToggleFlag,
3740
/// Use mouse events. This will disable text selection, but you will be able
3841
/// to resize some widgets.
3942
/// Default: False
@@ -59,6 +62,7 @@ impl Default for TerminalSettings {
5962
theme: "dark".to_string(),
6063
themes: Themes::default(),
6164
ascii_drawing: false,
65+
force_text_presentation: ToggleFlag::InternalVal(false),
6266
use_color: ToggleFlag::InternalVal(true),
6367
use_mouse: ToggleFlag::InternalVal(false),
6468
mouse_flag: Some("🖱️ ".to_string()),
@@ -70,15 +74,19 @@ impl Default for TerminalSettings {
7074
}
7175

7276
impl TerminalSettings {
77+
#[inline]
7378
pub fn use_color(&self) -> bool {
74-
/* Don't use color if
75-
* - Either NO_COLOR is set and user hasn't explicitly set use_colors or
76-
* - User has explicitly set use_colors to false
77-
*/
79+
// Don't use color if
80+
// - Either NO_COLOR is set and user hasn't explicitly set use_colors or
81+
// - User has explicitly set use_colors to false
7882
!((std::env::var("NO_COLOR").is_ok()
7983
&& (self.use_color.is_false() || self.use_color.is_internal()))
8084
|| (self.use_color.is_false() && !self.use_color.is_internal()))
8185
}
86+
87+
pub fn use_text_presentation(&self) -> bool {
88+
self.force_text_presentation.is_true() || !self.use_color()
89+
}
8290
}
8391

8492
impl DotAddressable for TerminalSettings {
@@ -90,6 +98,7 @@ impl DotAddressable for TerminalSettings {
9098
"theme" => self.theme.lookup(field, tail),
9199
"themes" => Err(Error::new("unimplemented")),
92100
"ascii_drawing" => self.ascii_drawing.lookup(field, tail),
101+
"force_text_presentation" => self.force_text_presentation.lookup(field, tail),
93102
"use_color" => self.use_color.lookup(field, tail),
94103
"use_mouse" => self.use_mouse.lookup(field, tail),
95104
"mouse_flag" => self.mouse_flag.lookup(field, tail),

meli/src/state.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,12 @@ impl State {
469469
s.screen.grid_mut().set_ascii_drawing(true);
470470
s.screen.overlay_grid_mut().set_ascii_drawing(true);
471471
}
472+
if s.context.settings.terminal.use_text_presentation() {
473+
s.screen.grid_mut().set_force_text_presentation(true);
474+
s.screen
475+
.overlay_grid_mut()
476+
.set_force_text_presentation(true);
477+
}
472478

473479
s.screen.switch_to_alternate_screen(&s.context);
474480
for i in 0..s.context.accounts.len() {

meli/src/terminal.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ pub mod cells;
3232
#[macro_use]
3333
pub mod keys;
3434
pub mod embedded;
35+
#[cfg(test)]
36+
mod tests;
3537
pub mod text_editing;
3638

37-
use std::io::{BufRead, Write};
39+
use std::{
40+
borrow::Cow,
41+
io::{BufRead, Write},
42+
};
3843

3944
pub use braille::BraillePixelIter;
4045
pub use screen::{Area, Screen, ScreenGeneration, StateStdout, Tty, Virtual};
@@ -181,3 +186,72 @@ impl Ask {
181186
}
182187
}
183188
}
189+
190+
pub trait TextPresentation {
191+
/// Return `input` string while trying to use text presentations of
192+
/// symbols and emoji as much as possible. Might not work on all
193+
/// non-text symbols and is experimental.
194+
fn text_pr(&self) -> Cow<str>;
195+
}
196+
197+
impl TextPresentation for str {
198+
fn text_pr(&self) -> Cow<str> {
199+
use std::str::FromStr;
200+
201+
use melib::text::grapheme_clusters::TextProcessing;
202+
// [ref:FIXME]: add all relevant Unicode range/blocks to TextPresentation::text_pr()
203+
204+
// [ref:VERIFY]: Check whether our existing unicode tables can be used for TextPresentation::text_pr()
205+
206+
// [ref:DEBT]: TextPresentation::text_pr() is not tied to text submodule which can be updated for
207+
// each Unicode release
208+
209+
let get_base_char = |grapheme: &Self| -> Option<char> {
210+
char::from_str(grapheme.get(0..4).or_else(|| {
211+
grapheme
212+
.get(0..3)
213+
.or_else(|| grapheme.get(0..2).or_else(|| grapheme.get(0..1)))
214+
})?)
215+
.ok()
216+
};
217+
let is_emoji = |base_char: char| -> bool {
218+
[
219+
0x2B00..0x2BFF, // Miscellaneous Symbols and Arrows
220+
0x1F300..0x1F5FF, // Miscellaneous Symbols and Pictographs
221+
0x1F600..0x1F64F, // Emoticons
222+
0x1F680..0x1F6FF, // Transport and Map
223+
0x2600..0x26FF, // Misc symbols
224+
0x2700..0x27BF, // Dingbats
225+
0xFE00..0xFE0F, // Variation Selectors
226+
0x1F900..0x1F9FF, // Supplemental Symbols and Pictographs
227+
0x1F1E6..0x1F1FF, // Flags
228+
]
229+
.iter()
230+
.any(|range| range.contains(&(base_char as u32)))
231+
};
232+
233+
let graphemes = self.split_graphemes();
234+
for g in &graphemes {
235+
let Some(base_char) = get_base_char(g) else {
236+
// Bail out
237+
return Cow::from(self);
238+
};
239+
if is_emoji(base_char) {
240+
let mut ret = String::with_capacity(self.len() + 1);
241+
for g in &graphemes {
242+
ret.push_str(g);
243+
let Some(base_char) = get_base_char(g) else {
244+
// Bail out
245+
return Cow::from(self);
246+
};
247+
if is_emoji(base_char) {
248+
ret.push(emoji_text_presentation_selector!());
249+
}
250+
}
251+
return Cow::from(ret);
252+
}
253+
}
254+
255+
Cow::from(self)
256+
}
257+
}

meli/src/terminal/cells.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use melib::{
3535
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
3636
use smallvec::SmallVec;
3737

38-
use super::{Area, Color, Pos, ScreenGeneration};
38+
use super::{Area, Color, Pos, ScreenGeneration, TextPresentation};
3939
use crate::{state::Context, ThemeAttribute};
4040

4141
/// In a scroll region up and down cursor movements shift the region vertically.
@@ -67,6 +67,8 @@ pub struct CellBuffer {
6767
pub default_cell: Cell,
6868
/// ASCII-only flag.
6969
pub ascii_drawing: bool,
70+
/// Force text presentation for emojis.
71+
pub force_text_presentation: bool,
7072
/// Use color.
7173
pub use_color: bool,
7274
/// If printing to this buffer and we run out of space, expand it.
@@ -84,6 +86,7 @@ impl std::fmt::Debug for CellBuffer {
8486
.field("buf cells", &self.buf.len())
8587
.field("default_cell", &self.default_cell)
8688
.field("ascii_drawing", &self.ascii_drawing)
89+
.field("force_text_presentation", &self.force_text_presentation)
8790
.field("use_color", &self.use_color)
8891
.field("growable", &self.growable)
8992
.field("tag_table", &self.tag_table)
@@ -105,6 +108,7 @@ impl CellBuffer {
105108
default_cell: Cell::new_default(),
106109
growable: false,
107110
ascii_drawing: false,
111+
force_text_presentation: false,
108112
use_color: false,
109113
tag_table: Default::default(),
110114
tag_associations: SmallVec::new(),
@@ -128,6 +132,7 @@ impl CellBuffer {
128132
default_cell,
129133
growable: false,
130134
ascii_drawing: false,
135+
force_text_presentation: false,
131136
use_color: true,
132137
tag_table: Default::default(),
133138
tag_associations: SmallVec::new(),
@@ -146,11 +151,17 @@ impl CellBuffer {
146151
});
147152
Self {
148153
ascii_drawing: context.settings.terminal.ascii_drawing,
154+
force_text_presentation: context.settings.terminal.use_text_presentation(),
149155
use_color: context.settings.terminal.use_color(),
150156
..Self::new(default_cell, area)
151157
}
152158
}
153159

160+
pub fn set_force_text_presentation(&mut self, new_val: bool) -> &mut Self {
161+
self.force_text_presentation = new_val;
162+
self
163+
}
164+
154165
pub fn set_ascii_drawing(&mut self, new_val: bool) {
155166
self.ascii_drawing = new_val;
156167
}
@@ -182,6 +193,7 @@ impl CellBuffer {
182193
};
183194
self.ascii_drawing = context.settings.terminal.ascii_drawing;
184195
self.use_color = context.settings.terminal.use_color();
196+
self.force_text_presentation = context.settings.terminal.use_text_presentation();
185197

186198
let newlen = newcols * newrows;
187199
if (self.cols, self.rows) == (newcols, newrows) || newlen >= Self::MAX_SIZE {
@@ -690,7 +702,12 @@ impl CellBuffer {
690702
return (x - upper_left.0, y - upper_left.1);
691703
}
692704
}
693-
for c in s.chars() {
705+
let input = if self.force_text_presentation {
706+
s.text_pr()
707+
} else {
708+
s.into()
709+
};
710+
for c in input.chars() {
694711
if c == crate::emoji_text_presentation_selector!() {
695712
let prev_attrs = self[prev_coords].attrs();
696713
self[prev_coords].set_attrs(prev_attrs | Attr::FORCE_TEXT);

meli/src/terminal/tests.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// meli
3+
//
4+
// Copyright 2024 Emmanouil Pitsidianakis <[email protected]>
5+
//
6+
// This file is part of meli.
7+
//
8+
// meli is free software: you can redistribute it and/or modify
9+
// it under the terms of the GNU General Public License as published by
10+
// the Free Software Foundation, either version 3 of the License, or
11+
// (at your option) any later version.
12+
//
13+
// meli is distributed in the hope that it will be useful,
14+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
// GNU General Public License for more details.
17+
//
18+
// You should have received a copy of the GNU General Public License
19+
// along with meli. If not, see <http://www.gnu.org/licenses/>.
20+
//
21+
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
22+
23+
#[test]
24+
fn test_terminal_text_presentation() {
25+
use crate::terminal::TextPresentation;
26+
27+
// A big thanks to every spammer, e-shop and even patch submitter who used
28+
// emojis in subjects and inspired me to add this feature.
29+
const TEST_CASES: &[(&str, &str)] = &[
30+
(
31+
"The Darkness Issue is now shipping worldwide 🦇",
32+
"The Darkness Issue is now shipping worldwide 🦇︎",
33+
),
34+
("🐝 <[email protected]>", "🐝︎ <[email protected]>"),
35+
(
36+
"Happy Women's Day 🎀 - ΠΑΡΕ ΤΟ ΔΩΡΟ ΣΟΥ 🎁",
37+
"Happy Women's Day 🎀︎ - ΠΑΡΕ ΤΟ ΔΩΡΟ ΣΟΥ 🎁︎",
38+
),
39+
(
40+
"💨 Εσύ θα προλάβεις; 🔴 🐇 Καλό Πάσχα!",
41+
"💨︎ Εσύ θα προλάβεις; 🔴︎ 🐇︎ Καλό Πάσχα!",
42+
),
43+
("Dream drop 💤", "Dream drop 💤︎"),
44+
(
45+
"⭐ Αξιολόγησε τον επαγγελματία! ⭐",
46+
"⭐︎ Αξιολόγησε τον επαγγελματία! ⭐︎",
47+
),
48+
(
49+
"🔓 MYSTERY UNLOCKED: 💀NEW💀 SIGNED VENTURE BROS. DVD SALE & MERCH RESTOCK",
50+
"🔓︎ MYSTERY UNLOCKED: 💀︎NEW💀︎ SIGNED VENTURE BROS. DVD SALE & MERCH RESTOCK",
51+
),
52+
(
53+
"[PATCH RFC 00/26] Multifd 🔀 device state transfer support with VFIO consumer",
54+
"[PATCH RFC 00/26] Multifd 🔀︎ device state transfer support with VFIO consumer",
55+
),
56+
];
57+
58+
for (emoji, text) in TEST_CASES {
59+
assert_eq!(&emoji.text_pr(), text);
60+
}
61+
}

0 commit comments

Comments
 (0)