From 498754b517589b43edc79074738422984ac894c7 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 09:46:05 +0900 Subject: [PATCH 01/11] Add kitty-unicode protocol option --- README.md | 2 +- config.schema.json | 5 +++-- docs/src/configurations/config-file-format.md | 1 + docs/src/getting-started/command-line-options.md | 2 +- src/config.rs | 4 ++-- src/lib.rs | 4 ++++ 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 682f42e..ad2bf17 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Usage: serie [OPTIONS] Options: -n, --max-count Maximum number of commits to render - -p, --protocol Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty] + -p, --protocol Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty, kitty-unicode] -o, --order Commit ordering algorithm [default: chrono] [possible values: chrono, topo] -g, --graph-width Commit graph image cell width [default: auto] [possible values: auto, double, single] -s, --graph-style Commit graph image edge style [default: rounded] [possible values: rounded, angular] diff --git a/config.schema.json b/config.schema.json index 49e8fe3..f37fc50 100644 --- a/config.schema.json +++ b/config.schema.json @@ -18,7 +18,8 @@ "enum": [ "auto", "iterm", - "kitty" + "kitty", + "kitty-unicode" ], "default": "auto" }, @@ -682,4 +683,4 @@ ] } } -} \ No newline at end of file +} diff --git a/docs/src/configurations/config-file-format.md b/docs/src/configurations/config-file-format.md index 79828a6..f69380f 100644 --- a/docs/src/configurations/config-file-format.md +++ b/docs/src/configurations/config-file-format.md @@ -114,6 +114,7 @@ The protocol type for rendering images of commit graphs. - `auto` - `iterm` - `kitty` + - `kitty-unicode` The value specified in the command line argument takes precedence. diff --git a/docs/src/getting-started/command-line-options.md b/docs/src/getting-started/command-line-options.md index 21a0f25..806306e 100644 --- a/docs/src/getting-started/command-line-options.md +++ b/docs/src/getting-started/command-line-options.md @@ -11,7 +11,7 @@ It behaves similarly to the `--max-count` option of `git log`. A protocol type for rendering images of commit graphs. -_Possible values:_ `auto`, `iterm`, `kitty` +_Possible values:_ `auto`, `iterm`, `kitty`, `kitty-unicode` By default `auto` will guess the best supported protocol for the current terminal (if listed in [Supported terminal emulators](./compatibility.md#supported-terminal-emulators)). diff --git a/src/config.rs b/src/config.rs index f9e8cd4..205852a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -503,7 +503,7 @@ mod tests { fn test_config_complete_toml() { let toml = r##" [core.option] - protocol = "kitty" + protocol = "kitty-unicode" order = "topo" graph_width = "single" graph_style = "angular" @@ -543,7 +543,7 @@ mod tests { let expected = Config { core: CoreConfig { option: CoreOptionConfig { - protocol: Some(ImageProtocolType::Kitty), + protocol: Some(ImageProtocolType::KittyUnicode), order: Some(CommitOrderType::Topo), graph_width: Some(GraphWidthType::Single), graph_style: Some(GraphStyle::Angular), diff --git a/src/lib.rs b/src/lib.rs index f877280..ce77ee7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,9 @@ pub enum ImageProtocolType { Auto, Iterm, Kitty, + #[serde(rename = "kitty-unicode")] + #[value(name = "kitty-unicode")] + KittyUnicode, } impl From> for protocol::ImageProtocol { @@ -62,6 +65,7 @@ impl From> for protocol::ImageProtocol { Some(ImageProtocolType::Auto) => protocol::auto_detect(), Some(ImageProtocolType::Iterm) => protocol::ImageProtocol::Iterm2, Some(ImageProtocolType::Kitty) => protocol::ImageProtocol::Kitty, + Some(ImageProtocolType::KittyUnicode) => protocol::ImageProtocol::Kitty, None => protocol::auto_detect(), } } From 384647c93b8a1da6734ca9082497dbe63ac0698d Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 09:56:38 +0900 Subject: [PATCH 02/11] Split graph image render data --- src/graph/image.rs | 26 +++++++++++++++----------- src/protocol.rs | 35 +++++++++++++++++++++++++++++++++-- src/widget/commit_list.rs | 12 +++++++----- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/graph/image.rs b/src/graph/image.rs index 5001696..5f6eef4 100644 --- a/src/graph/image.rs +++ b/src/graph/image.rs @@ -12,7 +12,7 @@ use crate::{ geometry::{bounding_box_u32, Point}, Edge, EdgeType, Graph, }, - protocol::ImageProtocol, + protocol::{ImageProtocol, PreparedImage}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -23,7 +23,7 @@ pub enum GraphStyle { #[derive(Debug)] pub struct GraphImageManager<'a> { - encoded_image_map: FxHashMap, + prepared_image_map: FxHashMap, graph: &'a Graph<'a>, cell_width_type: CellWidthType, @@ -45,7 +45,7 @@ impl<'a> GraphImageManager<'a> { let drawing_pixels = DrawingPixels::new(&image_params); GraphImageManager { - encoded_image_map: FxHashMap::default(), + prepared_image_map: FxHashMap::default(), graph, cell_width_type, graph_style, @@ -55,12 +55,12 @@ impl<'a> GraphImageManager<'a> { } } - pub fn encoded_image(&self, commit_hash: &CommitHash) -> &str { - self.encoded_image_map.get(commit_hash).unwrap() + pub fn prepared_image(&self, commit_hash: &CommitHash) -> &PreparedImage { + self.prepared_image_map.get(commit_hash).unwrap() } - pub fn load_encoded_image(&mut self, commit_hash: &CommitHash) { - if self.encoded_image_map.contains_key(commit_hash) { + pub fn load_prepared_image(&mut self, commit_hash: &CommitHash) { + if self.prepared_image_map.contains_key(commit_hash) { return; } let graph_row_image = build_single_graph_row_image( @@ -70,8 +70,8 @@ impl<'a> GraphImageManager<'a> { self.graph_style, commit_hash, ); - let image = graph_row_image.encode(self.cell_width_type, self.image_protocol); - self.encoded_image_map.insert(commit_hash.clone(), image); + let image = graph_row_image.prepare(self.cell_width_type, self.image_protocol); + self.prepared_image_map.insert(commit_hash.clone(), image); } } @@ -97,12 +97,16 @@ impl Debug for GraphRowImage { } impl GraphRowImage { - fn encode(&self, cell_width_type: CellWidthType, image_protocol: ImageProtocol) -> String { + fn prepare( + &self, + cell_width_type: CellWidthType, + image_protocol: ImageProtocol, + ) -> PreparedImage { let image_cell_width = match cell_width_type { CellWidthType::Double => self.cell_count * 2, CellWidthType::Single => self.cell_count, }; - image_protocol.encode(&self.bytes, image_cell_width) + image_protocol.prepare_image(&self.bytes, image_cell_width) } } diff --git a/src/protocol.rs b/src/protocol.rs index 0ec7ac8..2030faf 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -24,11 +24,42 @@ pub enum ImageProtocol { Kitty, } +#[derive(Debug, Clone)] +pub struct PreparedImageCell { + symbol: String, +} + +impl PreparedImageCell { + pub fn symbol(&self) -> &str { + &self.symbol + } +} + +#[derive(Debug, Clone)] +pub struct PreparedImage { + cells: Vec, + cell_width: usize, +} + +impl PreparedImage { + pub fn cells(&self) -> &[PreparedImageCell] { + &self.cells + } + + pub fn cell_width(&self) -> usize { + self.cell_width + } +} + impl ImageProtocol { - pub fn encode(&self, bytes: &[u8], cell_width: usize) -> String { - match self { + pub fn prepare_image(&self, bytes: &[u8], cell_width: usize) -> PreparedImage { + let symbol = match self { ImageProtocol::Iterm2 => iterm2_encode(bytes, cell_width, 1), ImageProtocol::Kitty => kitty_encode(bytes, cell_width, 1), + }; + PreparedImage { + cells: vec![PreparedImageCell { symbol }], + cell_width, } } diff --git a/src/widget/commit_list.rs b/src/widget/commit_list.rs index 34758c4..7181e01 100644 --- a/src/widget/commit_list.rs +++ b/src/widget/commit_list.rs @@ -20,6 +20,7 @@ use crate::{ config::UserListColumnType, git::{Commit, CommitHash, Head, Ref}, graph::GraphImageManager, + protocol::PreparedImage, }; static FUZZY_MATCHER: Lazy = Lazy::new(|| SkimMatcherV2::default().respect_case()); @@ -647,9 +648,9 @@ impl<'a> CommitListState<'a> { } } - fn encoded_image(&self, commit_info: &'a CommitInfo) -> &str { + fn prepared_image(&self, commit_info: &'a CommitInfo) -> &PreparedImage { self.graph_image_manager - .encoded_image(&commit_info.commit.commit_hash) + .prepared_image(&commit_info.commit.commit_hash) } } @@ -731,7 +732,7 @@ impl CommitList<'_> { .for_each(|commit_info| { state .graph_image_manager - .load_encoded_image(&commit_info.commit.commit_hash); + .load_prepared_image(&commit_info.commit.commit_hash); }); } @@ -741,11 +742,12 @@ impl CommitList<'_> { } self.rendering_commit_info_iter(state) .for_each(|(i, commit_info)| { + let prepared_image = state.prepared_image(commit_info); buf[(area.left(), area.top() + i as u16)] - .set_symbol(state.encoded_image(commit_info)); + .set_symbol(prepared_image.cells()[0].symbol()); // width - 1 for right pad - for w in 1..area.width - 1 { + for w in 1..prepared_image.cell_width() as u16 { buf[(area.left() + w, area.top() + i as u16)].set_skip(true); } }); From 2e9d069cd76b8959f850b7256efa3512f4a863df Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 10:16:26 +0900 Subject: [PATCH 03/11] Render graph images as cell sequences --- src/protocol.rs | 27 ++++++++++++++++++++++++--- src/widget/commit_list.rs | 18 ++++++++++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/protocol.rs b/src/protocol.rs index 2030faf..87a0cfb 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,6 +1,7 @@ use std::env; use base64::Engine; +use ratatui::style::Style; // By default assume the Iterm2 is the best protocol to use for all terminals *unless* an env // variable is set that suggests the terminal is probably Kitty. @@ -27,12 +28,22 @@ pub enum ImageProtocol { #[derive(Debug, Clone)] pub struct PreparedImageCell { symbol: String, + style: Style, + skip: bool, } impl PreparedImageCell { pub fn symbol(&self) -> &str { &self.symbol } + + pub fn style(&self) -> Style { + self.style + } + + pub fn skip(&self) -> bool { + self.skip + } } #[derive(Debug, Clone)] @@ -57,10 +68,20 @@ impl ImageProtocol { ImageProtocol::Iterm2 => iterm2_encode(bytes, cell_width, 1), ImageProtocol::Kitty => kitty_encode(bytes, cell_width, 1), }; - PreparedImage { - cells: vec![PreparedImageCell { symbol }], - cell_width, + let mut cells = Vec::with_capacity(cell_width); + cells.push(PreparedImageCell { + symbol, + style: Style::default(), + skip: false, + }); + for _ in 1..cell_width { + cells.push(PreparedImageCell { + symbol: String::new(), + style: Style::default(), + skip: true, + }); } + PreparedImage { cells, cell_width } } pub fn clear_line(&self, y: u16) { diff --git a/src/widget/commit_list.rs b/src/widget/commit_list.rs index 7181e01..84418d9 100644 --- a/src/widget/commit_list.rs +++ b/src/widget/commit_list.rs @@ -743,12 +743,18 @@ impl CommitList<'_> { self.rendering_commit_info_iter(state) .for_each(|(i, commit_info)| { let prepared_image = state.prepared_image(commit_info); - buf[(area.left(), area.top() + i as u16)] - .set_symbol(prepared_image.cells()[0].symbol()); - - // width - 1 for right pad - for w in 1..prepared_image.cell_width() as u16 { - buf[(area.left() + w, area.top() + i as u16)].set_skip(true); + let max_graph_width = area.width.saturating_sub(1) as usize; + let y = area.top() + i as u16; + for (x, image_cell) in prepared_image + .cells() + .iter() + .take(max_graph_width) + .enumerate() + { + let cell = &mut buf[(area.left() + x as u16, y)]; + cell.set_symbol(image_cell.symbol()); + cell.set_style(image_cell.style()); + cell.set_skip(image_cell.skip()); } }); } From e85571b49d3a6db0b9925d4df675ca5a66ac14e5 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 10:41:01 +0900 Subject: [PATCH 04/11] Implement kitty unicode placeholders --- assets/kitty/rowcolumn-diacritics.txt | 310 ++++++++++++++++++++++++++ src/graph/image.rs | 36 ++- src/lib.rs | 2 +- src/protocol.rs | 93 +++++++- 4 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 assets/kitty/rowcolumn-diacritics.txt diff --git a/assets/kitty/rowcolumn-diacritics.txt b/assets/kitty/rowcolumn-diacritics.txt new file mode 100644 index 0000000..9d8091e --- /dev/null +++ b/assets/kitty/rowcolumn-diacritics.txt @@ -0,0 +1,310 @@ +# This file lists the diacritics used to indicate row/column numbers for +# Unicode terminal image placeholders. It is derived from UnicodeData.txt for +# Unicode 6.0.0 (chosen somewhat arbitrarily: it's old enough, but still +# contains more than 255 suitable combining chars) using the following +# command: +# +# cat UnicodeData.txt | grep "Mn;230;NSM;;" | grep -v "0300\|0301\|0302\|0303\|0304\|0306\|0307\|0308\|0309\|030A\|030B\|030C\|030F\|0311\|0313\|0314\|0342\|0653\|0654" +# +# That is, we use combining chars of the same combining class 230 (above the +# base character) that do not have decomposition mappings, and we also remove +# some characters that may be fused with other characters during normalization, +# like 0041 0300 -> 00C0 which is À (A with grave). +# +0305;COMBINING OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING OVERSCORE;;;; +030D;COMBINING VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL LINE ABOVE;;;; +030E;COMBINING DOUBLE VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE VERTICAL LINE ABOVE;;;; +0310;COMBINING CANDRABINDU;Mn;230;NSM;;;;;N;NON-SPACING CANDRABINDU;;;; +0312;COMBINING TURNED COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING TURNED COMMA ABOVE;;;; +033D;COMBINING X ABOVE;Mn;230;NSM;;;;;N;NON-SPACING X ABOVE;;;; +033E;COMBINING VERTICAL TILDE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL TILDE;;;; +033F;COMBINING DOUBLE OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE OVERSCORE;;;; +0346;COMBINING BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; +034A;COMBINING NOT TILDE ABOVE;Mn;230;NSM;;;;;N;;;;; +034B;COMBINING HOMOTHETIC ABOVE;Mn;230;NSM;;;;;N;;;;; +034C;COMBINING ALMOST EQUAL TO ABOVE;Mn;230;NSM;;;;;N;;;;; +0350;COMBINING RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +0351;COMBINING LEFT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; +0352;COMBINING FERMATA;Mn;230;NSM;;;;;N;;;;; +0357;COMBINING RIGHT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; +035B;COMBINING ZIGZAG ABOVE;Mn;230;NSM;;;;;N;;;;; +0363;COMBINING LATIN SMALL LETTER A;Mn;230;NSM;;;;;N;;;;; +0364;COMBINING LATIN SMALL LETTER E;Mn;230;NSM;;;;;N;;;;; +0365;COMBINING LATIN SMALL LETTER I;Mn;230;NSM;;;;;N;;;;; +0366;COMBINING LATIN SMALL LETTER O;Mn;230;NSM;;;;;N;;;;; +0367;COMBINING LATIN SMALL LETTER U;Mn;230;NSM;;;;;N;;;;; +0368;COMBINING LATIN SMALL LETTER C;Mn;230;NSM;;;;;N;;;;; +0369;COMBINING LATIN SMALL LETTER D;Mn;230;NSM;;;;;N;;;;; +036A;COMBINING LATIN SMALL LETTER H;Mn;230;NSM;;;;;N;;;;; +036B;COMBINING LATIN SMALL LETTER M;Mn;230;NSM;;;;;N;;;;; +036C;COMBINING LATIN SMALL LETTER R;Mn;230;NSM;;;;;N;;;;; +036D;COMBINING LATIN SMALL LETTER T;Mn;230;NSM;;;;;N;;;;; +036E;COMBINING LATIN SMALL LETTER V;Mn;230;NSM;;;;;N;;;;; +036F;COMBINING LATIN SMALL LETTER X;Mn;230;NSM;;;;;N;;;;; +0483;COMBINING CYRILLIC TITLO;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING TITLO;;;; +0484;COMBINING CYRILLIC PALATALIZATION;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PALATALIZATION;;;; +0485;COMBINING CYRILLIC DASIA PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING DASIA PNEUMATA;;;; +0486;COMBINING CYRILLIC PSILI PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PSILI PNEUMATA;;;; +0487;COMBINING CYRILLIC POKRYTIE;Mn;230;NSM;;;;;N;;;;; +0592;HEBREW ACCENT SEGOL;Mn;230;NSM;;;;;N;;;;; +0593;HEBREW ACCENT SHALSHELET;Mn;230;NSM;;;;;N;;;;; +0594;HEBREW ACCENT ZAQEF QATAN;Mn;230;NSM;;;;;N;;;;; +0595;HEBREW ACCENT ZAQEF GADOL;Mn;230;NSM;;;;;N;;;;; +0597;HEBREW ACCENT REVIA;Mn;230;NSM;;;;;N;;;;; +0598;HEBREW ACCENT ZARQA;Mn;230;NSM;;;;;N;;;;; +0599;HEBREW ACCENT PASHTA;Mn;230;NSM;;;;;N;;;;; +059C;HEBREW ACCENT GERESH;Mn;230;NSM;;;;;N;;;;; +059D;HEBREW ACCENT GERESH MUQDAM;Mn;230;NSM;;;;;N;;;;; +059E;HEBREW ACCENT GERSHAYIM;Mn;230;NSM;;;;;N;;;;; +059F;HEBREW ACCENT QARNEY PARA;Mn;230;NSM;;;;;N;;;;; +05A0;HEBREW ACCENT TELISHA GEDOLA;Mn;230;NSM;;;;;N;;;;; +05A1;HEBREW ACCENT PAZER;Mn;230;NSM;;;;;N;;;;; +05A8;HEBREW ACCENT QADMA;Mn;230;NSM;;;;;N;;;;; +05A9;HEBREW ACCENT TELISHA QETANA;Mn;230;NSM;;;;;N;;;;; +05AB;HEBREW ACCENT OLE;Mn;230;NSM;;;;;N;;;;; +05AC;HEBREW ACCENT ILUY;Mn;230;NSM;;;;;N;;;;; +05AF;HEBREW MARK MASORA CIRCLE;Mn;230;NSM;;;;;N;;;;; +05C4;HEBREW MARK UPPER DOT;Mn;230;NSM;;;;;N;;;;; +0610;ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM;Mn;230;NSM;;;;;N;;;;; +0611;ARABIC SIGN ALAYHE ASSALLAM;Mn;230;NSM;;;;;N;;;;; +0612;ARABIC SIGN RAHMATULLAH ALAYHE;Mn;230;NSM;;;;;N;;;;; +0613;ARABIC SIGN RADI ALLAHOU ANHU;Mn;230;NSM;;;;;N;;;;; +0614;ARABIC SIGN TAKHALLUS;Mn;230;NSM;;;;;N;;;;; +0615;ARABIC SMALL HIGH TAH;Mn;230;NSM;;;;;N;;;;; +0616;ARABIC SMALL HIGH LIGATURE ALEF WITH LAM WITH YEH;Mn;230;NSM;;;;;N;;;;; +0617;ARABIC SMALL HIGH ZAIN;Mn;230;NSM;;;;;N;;;;; +0657;ARABIC INVERTED DAMMA;Mn;230;NSM;;;;;N;;;;; +0658;ARABIC MARK NOON GHUNNA;Mn;230;NSM;;;;;N;;;;; +0659;ARABIC ZWARAKAY;Mn;230;NSM;;;;;N;;;;; +065A;ARABIC VOWEL SIGN SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; +065B;ARABIC VOWEL SIGN INVERTED SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; +065D;ARABIC REVERSED DAMMA;Mn;230;NSM;;;;;N;;;;; +065E;ARABIC FATHA WITH TWO DOTS;Mn;230;NSM;;;;;N;;;;; +06D6;ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; +06D7;ARABIC SMALL HIGH LIGATURE QAF WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; +06D8;ARABIC SMALL HIGH MEEM INITIAL FORM;Mn;230;NSM;;;;;N;;;;; +06D9;ARABIC SMALL HIGH LAM ALEF;Mn;230;NSM;;;;;N;;;;; +06DA;ARABIC SMALL HIGH JEEM;Mn;230;NSM;;;;;N;;;;; +06DB;ARABIC SMALL HIGH THREE DOTS;Mn;230;NSM;;;;;N;;;;; +06DC;ARABIC SMALL HIGH SEEN;Mn;230;NSM;;;;;N;;;;; +06DF;ARABIC SMALL HIGH ROUNDED ZERO;Mn;230;NSM;;;;;N;;;;; +06E0;ARABIC SMALL HIGH UPRIGHT RECTANGULAR ZERO;Mn;230;NSM;;;;;N;;;;; +06E1;ARABIC SMALL HIGH DOTLESS HEAD OF KHAH;Mn;230;NSM;;;;;N;;;;; +06E2;ARABIC SMALL HIGH MEEM ISOLATED FORM;Mn;230;NSM;;;;;N;;;;; +06E4;ARABIC SMALL HIGH MADDA;Mn;230;NSM;;;;;N;;;;; +06E7;ARABIC SMALL HIGH YEH;Mn;230;NSM;;;;;N;;;;; +06E8;ARABIC SMALL HIGH NOON;Mn;230;NSM;;;;;N;;;;; +06EB;ARABIC EMPTY CENTRE HIGH STOP;Mn;230;NSM;;;;;N;;;;; +06EC;ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE;Mn;230;NSM;;;;;N;;;;; +0730;SYRIAC PTHAHA ABOVE;Mn;230;NSM;;;;;N;;;;; +0732;SYRIAC PTHAHA DOTTED;Mn;230;NSM;;;;;N;;;;; +0733;SYRIAC ZQAPHA ABOVE;Mn;230;NSM;;;;;N;;;;; +0735;SYRIAC ZQAPHA DOTTED;Mn;230;NSM;;;;;N;;;;; +0736;SYRIAC RBASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073A;SYRIAC HBASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073D;SYRIAC ESASA ABOVE;Mn;230;NSM;;;;;N;;;;; +073F;SYRIAC RWAHA;Mn;230;NSM;;;;;N;;;;; +0740;SYRIAC FEMININE DOT;Mn;230;NSM;;;;;N;;;;; +0741;SYRIAC QUSHSHAYA;Mn;230;NSM;;;;;N;;;;; +0743;SYRIAC TWO VERTICAL DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +0745;SYRIAC THREE DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +0747;SYRIAC OBLIQUE LINE ABOVE;Mn;230;NSM;;;;;N;;;;; +0749;SYRIAC MUSIC;Mn;230;NSM;;;;;N;;;;; +074A;SYRIAC BARREKH;Mn;230;NSM;;;;;N;;;;; +07EB;NKO COMBINING SHORT HIGH TONE;Mn;230;NSM;;;;;N;;;;; +07EC;NKO COMBINING SHORT LOW TONE;Mn;230;NSM;;;;;N;;;;; +07ED;NKO COMBINING SHORT RISING TONE;Mn;230;NSM;;;;;N;;;;; +07EE;NKO COMBINING LONG DESCENDING TONE;Mn;230;NSM;;;;;N;;;;; +07EF;NKO COMBINING LONG HIGH TONE;Mn;230;NSM;;;;;N;;;;; +07F0;NKO COMBINING LONG LOW TONE;Mn;230;NSM;;;;;N;;;;; +07F1;NKO COMBINING LONG RISING TONE;Mn;230;NSM;;;;;N;;;;; +07F3;NKO COMBINING DOUBLE DOT ABOVE;Mn;230;NSM;;;;;N;;;;; +0816;SAMARITAN MARK IN;Mn;230;NSM;;;;;N;;;;; +0817;SAMARITAN MARK IN-ALAF;Mn;230;NSM;;;;;N;;;;; +0818;SAMARITAN MARK OCCLUSION;Mn;230;NSM;;;;;N;;;;; +0819;SAMARITAN MARK DAGESH;Mn;230;NSM;;;;;N;;;;; +081B;SAMARITAN MARK EPENTHETIC YUT;Mn;230;NSM;;;;;N;;;;; +081C;SAMARITAN VOWEL SIGN LONG E;Mn;230;NSM;;;;;N;;;;; +081D;SAMARITAN VOWEL SIGN E;Mn;230;NSM;;;;;N;;;;; +081E;SAMARITAN VOWEL SIGN OVERLONG AA;Mn;230;NSM;;;;;N;;;;; +081F;SAMARITAN VOWEL SIGN LONG AA;Mn;230;NSM;;;;;N;;;;; +0820;SAMARITAN VOWEL SIGN AA;Mn;230;NSM;;;;;N;;;;; +0821;SAMARITAN VOWEL SIGN OVERLONG A;Mn;230;NSM;;;;;N;;;;; +0822;SAMARITAN VOWEL SIGN LONG A;Mn;230;NSM;;;;;N;;;;; +0823;SAMARITAN VOWEL SIGN A;Mn;230;NSM;;;;;N;;;;; +0825;SAMARITAN VOWEL SIGN SHORT A;Mn;230;NSM;;;;;N;;;;; +0826;SAMARITAN VOWEL SIGN LONG U;Mn;230;NSM;;;;;N;;;;; +0827;SAMARITAN VOWEL SIGN U;Mn;230;NSM;;;;;N;;;;; +0829;SAMARITAN VOWEL SIGN LONG I;Mn;230;NSM;;;;;N;;;;; +082A;SAMARITAN VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; +082B;SAMARITAN VOWEL SIGN O;Mn;230;NSM;;;;;N;;;;; +082C;SAMARITAN VOWEL SIGN SUKUN;Mn;230;NSM;;;;;N;;;;; +082D;SAMARITAN MARK NEQUDAA;Mn;230;NSM;;;;;N;;;;; +0951;DEVANAGARI STRESS SIGN UDATTA;Mn;230;NSM;;;;;N;;;;; +0953;DEVANAGARI GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; +0954;DEVANAGARI ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; +0F82;TIBETAN SIGN NYI ZLA NAA DA;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU WITH ORNAMENT;;;; +0F83;TIBETAN SIGN SNA LDAN;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU;;;; +0F86;TIBETAN SIGN LCI RTAGS;Mn;230;NSM;;;;;N;;;;; +0F87;TIBETAN SIGN YANG RTAGS;Mn;230;NSM;;;;;N;;;;; +135D;ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; +135E;ETHIOPIC COMBINING VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; +135F;ETHIOPIC COMBINING GEMINATION MARK;Mn;230;NSM;;;;;N;;;;; +17DD;KHMER SIGN ATTHACAN;Mn;230;NSM;;;;;N;;;;; +193A;LIMBU SIGN KEMPHRENG;Mn;230;NSM;;;;;N;;;;; +1A17;BUGINESE VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; +1A75;TAI THAM SIGN TONE-1;Mn;230;NSM;;;;;N;;;;; +1A76;TAI THAM SIGN TONE-2;Mn;230;NSM;;;;;N;;;;; +1A77;TAI THAM SIGN KHUEN TONE-3;Mn;230;NSM;;;;;N;;;;; +1A78;TAI THAM SIGN KHUEN TONE-4;Mn;230;NSM;;;;;N;;;;; +1A79;TAI THAM SIGN KHUEN TONE-5;Mn;230;NSM;;;;;N;;;;; +1A7A;TAI THAM SIGN RA HAAM;Mn;230;NSM;;;;;N;;;;; +1A7B;TAI THAM SIGN MAI SAM;Mn;230;NSM;;;;;N;;;;; +1A7C;TAI THAM SIGN KHUEN-LUE KARAN;Mn;230;NSM;;;;;N;;;;; +1B6B;BALINESE MUSICAL SYMBOL COMBINING TEGEH;Mn;230;NSM;;;;;N;;;;; +1B6D;BALINESE MUSICAL SYMBOL COMBINING KEMPUL;Mn;230;NSM;;;;;N;;;;; +1B6E;BALINESE MUSICAL SYMBOL COMBINING KEMPLI;Mn;230;NSM;;;;;N;;;;; +1B6F;BALINESE MUSICAL SYMBOL COMBINING JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B70;BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B71;BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; +1B72;BALINESE MUSICAL SYMBOL COMBINING BENDE;Mn;230;NSM;;;;;N;;;;; +1B73;BALINESE MUSICAL SYMBOL COMBINING GONG;Mn;230;NSM;;;;;N;;;;; +1CD0;VEDIC TONE KARSHANA;Mn;230;NSM;;;;;N;;;;; +1CD1;VEDIC TONE SHARA;Mn;230;NSM;;;;;N;;;;; +1CD2;VEDIC TONE PRENKHA;Mn;230;NSM;;;;;N;;;;; +1CDA;VEDIC TONE DOUBLE SVARITA;Mn;230;NSM;;;;;N;;;;; +1CDB;VEDIC TONE TRIPLE SVARITA;Mn;230;NSM;;;;;N;;;;; +1CE0;VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA;Mn;230;NSM;;;;;N;;;;; +1DC0;COMBINING DOTTED GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; +1DC1;COMBINING DOTTED ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; +1DC3;COMBINING SUSPENSION MARK;Mn;230;NSM;;;;;N;;;;; +1DC4;COMBINING MACRON-ACUTE;Mn;230;NSM;;;;;N;;;;; +1DC5;COMBINING GRAVE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DC6;COMBINING MACRON-GRAVE;Mn;230;NSM;;;;;N;;;;; +1DC7;COMBINING ACUTE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DC8;COMBINING GRAVE-ACUTE-GRAVE;Mn;230;NSM;;;;;N;;;;; +1DC9;COMBINING ACUTE-GRAVE-ACUTE;Mn;230;NSM;;;;;N;;;;; +1DCB;COMBINING BREVE-MACRON;Mn;230;NSM;;;;;N;;;;; +1DCC;COMBINING MACRON-BREVE;Mn;230;NSM;;;;;N;;;;; +1DD1;COMBINING UR ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD2;COMBINING US ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD3;COMBINING LATIN SMALL LETTER FLATTENED OPEN A ABOVE;Mn;230;NSM;;;;;N;;;;; +1DD4;COMBINING LATIN SMALL LETTER AE;Mn;230;NSM;;;;;N;;;;; +1DD5;COMBINING LATIN SMALL LETTER AO;Mn;230;NSM;;;;;N;;;;; +1DD6;COMBINING LATIN SMALL LETTER AV;Mn;230;NSM;;;;;N;;;;; +1DD7;COMBINING LATIN SMALL LETTER C CEDILLA;Mn;230;NSM;;;;;N;;;;; +1DD8;COMBINING LATIN SMALL LETTER INSULAR D;Mn;230;NSM;;;;;N;;;;; +1DD9;COMBINING LATIN SMALL LETTER ETH;Mn;230;NSM;;;;;N;;;;; +1DDA;COMBINING LATIN SMALL LETTER G;Mn;230;NSM;;;;;N;;;;; +1DDB;COMBINING LATIN LETTER SMALL CAPITAL G;Mn;230;NSM;;;;;N;;;;; +1DDC;COMBINING LATIN SMALL LETTER K;Mn;230;NSM;;;;;N;;;;; +1DDD;COMBINING LATIN SMALL LETTER L;Mn;230;NSM;;;;;N;;;;; +1DDE;COMBINING LATIN LETTER SMALL CAPITAL L;Mn;230;NSM;;;;;N;;;;; +1DDF;COMBINING LATIN LETTER SMALL CAPITAL M;Mn;230;NSM;;;;;N;;;;; +1DE0;COMBINING LATIN SMALL LETTER N;Mn;230;NSM;;;;;N;;;;; +1DE1;COMBINING LATIN LETTER SMALL CAPITAL N;Mn;230;NSM;;;;;N;;;;; +1DE2;COMBINING LATIN LETTER SMALL CAPITAL R;Mn;230;NSM;;;;;N;;;;; +1DE3;COMBINING LATIN SMALL LETTER R ROTUNDA;Mn;230;NSM;;;;;N;;;;; +1DE4;COMBINING LATIN SMALL LETTER S;Mn;230;NSM;;;;;N;;;;; +1DE5;COMBINING LATIN SMALL LETTER LONG S;Mn;230;NSM;;;;;N;;;;; +1DE6;COMBINING LATIN SMALL LETTER Z;Mn;230;NSM;;;;;N;;;;; +1DFE;COMBINING LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +20D0;COMBINING LEFT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT HARPOON ABOVE;;;; +20D1;COMBINING RIGHT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT HARPOON ABOVE;;;; +20D4;COMBINING ANTICLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING ANTICLOCKWISE ARROW ABOVE;;;; +20D5;COMBINING CLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING CLOCKWISE ARROW ABOVE;;;; +20D6;COMBINING LEFT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT ARROW ABOVE;;;; +20D7;COMBINING RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT ARROW ABOVE;;;; +20DB;COMBINING THREE DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING THREE DOTS ABOVE;;;; +20DC;COMBINING FOUR DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING FOUR DOTS ABOVE;;;; +20E1;COMBINING LEFT RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT RIGHT ARROW ABOVE;;;; +20E7;COMBINING ANNUITY SYMBOL;Mn;230;NSM;;;;;N;;;;; +20E9;COMBINING WIDE BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; +20F0;COMBINING ASTERISK ABOVE;Mn;230;NSM;;;;;N;;;;; +2CEF;COPTIC COMBINING NI ABOVE;Mn;230;NSM;;;;;N;;;;; +2CF0;COPTIC COMBINING SPIRITUS ASPER;Mn;230;NSM;;;;;N;;;;; +2CF1;COPTIC COMBINING SPIRITUS LENIS;Mn;230;NSM;;;;;N;;;;; +2DE0;COMBINING CYRILLIC LETTER BE;Mn;230;NSM;;;;;N;;;;; +2DE1;COMBINING CYRILLIC LETTER VE;Mn;230;NSM;;;;;N;;;;; +2DE2;COMBINING CYRILLIC LETTER GHE;Mn;230;NSM;;;;;N;;;;; +2DE3;COMBINING CYRILLIC LETTER DE;Mn;230;NSM;;;;;N;;;;; +2DE4;COMBINING CYRILLIC LETTER ZHE;Mn;230;NSM;;;;;N;;;;; +2DE5;COMBINING CYRILLIC LETTER ZE;Mn;230;NSM;;;;;N;;;;; +2DE6;COMBINING CYRILLIC LETTER KA;Mn;230;NSM;;;;;N;;;;; +2DE7;COMBINING CYRILLIC LETTER EL;Mn;230;NSM;;;;;N;;;;; +2DE8;COMBINING CYRILLIC LETTER EM;Mn;230;NSM;;;;;N;;;;; +2DE9;COMBINING CYRILLIC LETTER EN;Mn;230;NSM;;;;;N;;;;; +2DEA;COMBINING CYRILLIC LETTER O;Mn;230;NSM;;;;;N;;;;; +2DEB;COMBINING CYRILLIC LETTER PE;Mn;230;NSM;;;;;N;;;;; +2DEC;COMBINING CYRILLIC LETTER ER;Mn;230;NSM;;;;;N;;;;; +2DED;COMBINING CYRILLIC LETTER ES;Mn;230;NSM;;;;;N;;;;; +2DEE;COMBINING CYRILLIC LETTER TE;Mn;230;NSM;;;;;N;;;;; +2DEF;COMBINING CYRILLIC LETTER HA;Mn;230;NSM;;;;;N;;;;; +2DF0;COMBINING CYRILLIC LETTER TSE;Mn;230;NSM;;;;;N;;;;; +2DF1;COMBINING CYRILLIC LETTER CHE;Mn;230;NSM;;;;;N;;;;; +2DF2;COMBINING CYRILLIC LETTER SHA;Mn;230;NSM;;;;;N;;;;; +2DF3;COMBINING CYRILLIC LETTER SHCHA;Mn;230;NSM;;;;;N;;;;; +2DF4;COMBINING CYRILLIC LETTER FITA;Mn;230;NSM;;;;;N;;;;; +2DF5;COMBINING CYRILLIC LETTER ES-TE;Mn;230;NSM;;;;;N;;;;; +2DF6;COMBINING CYRILLIC LETTER A;Mn;230;NSM;;;;;N;;;;; +2DF7;COMBINING CYRILLIC LETTER IE;Mn;230;NSM;;;;;N;;;;; +2DF8;COMBINING CYRILLIC LETTER DJERV;Mn;230;NSM;;;;;N;;;;; +2DF9;COMBINING CYRILLIC LETTER MONOGRAPH UK;Mn;230;NSM;;;;;N;;;;; +2DFA;COMBINING CYRILLIC LETTER YAT;Mn;230;NSM;;;;;N;;;;; +2DFB;COMBINING CYRILLIC LETTER YU;Mn;230;NSM;;;;;N;;;;; +2DFC;COMBINING CYRILLIC LETTER IOTIFIED A;Mn;230;NSM;;;;;N;;;;; +2DFD;COMBINING CYRILLIC LETTER LITTLE YUS;Mn;230;NSM;;;;;N;;;;; +2DFE;COMBINING CYRILLIC LETTER BIG YUS;Mn;230;NSM;;;;;N;;;;; +2DFF;COMBINING CYRILLIC LETTER IOTIFIED BIG YUS;Mn;230;NSM;;;;;N;;;;; +A66F;COMBINING CYRILLIC VZMET;Mn;230;NSM;;;;;N;;;;; +A67C;COMBINING CYRILLIC KAVYKA;Mn;230;NSM;;;;;N;;;;; +A67D;COMBINING CYRILLIC PAYEROK;Mn;230;NSM;;;;;N;;;;; +A6F0;BAMUM COMBINING MARK KOQNDON;Mn;230;NSM;;;;;N;;;;; +A6F1;BAMUM COMBINING MARK TUKWENTIS;Mn;230;NSM;;;;;N;;;;; +A8E0;COMBINING DEVANAGARI DIGIT ZERO;Mn;230;NSM;;;;;N;;;;; +A8E1;COMBINING DEVANAGARI DIGIT ONE;Mn;230;NSM;;;;;N;;;;; +A8E2;COMBINING DEVANAGARI DIGIT TWO;Mn;230;NSM;;;;;N;;;;; +A8E3;COMBINING DEVANAGARI DIGIT THREE;Mn;230;NSM;;;;;N;;;;; +A8E4;COMBINING DEVANAGARI DIGIT FOUR;Mn;230;NSM;;;;;N;;;;; +A8E5;COMBINING DEVANAGARI DIGIT FIVE;Mn;230;NSM;;;;;N;;;;; +A8E6;COMBINING DEVANAGARI DIGIT SIX;Mn;230;NSM;;;;;N;;;;; +A8E7;COMBINING DEVANAGARI DIGIT SEVEN;Mn;230;NSM;;;;;N;;;;; +A8E8;COMBINING DEVANAGARI DIGIT EIGHT;Mn;230;NSM;;;;;N;;;;; +A8E9;COMBINING DEVANAGARI DIGIT NINE;Mn;230;NSM;;;;;N;;;;; +A8EA;COMBINING DEVANAGARI LETTER A;Mn;230;NSM;;;;;N;;;;; +A8EB;COMBINING DEVANAGARI LETTER U;Mn;230;NSM;;;;;N;;;;; +A8EC;COMBINING DEVANAGARI LETTER KA;Mn;230;NSM;;;;;N;;;;; +A8ED;COMBINING DEVANAGARI LETTER NA;Mn;230;NSM;;;;;N;;;;; +A8EE;COMBINING DEVANAGARI LETTER PA;Mn;230;NSM;;;;;N;;;;; +A8EF;COMBINING DEVANAGARI LETTER RA;Mn;230;NSM;;;;;N;;;;; +A8F0;COMBINING DEVANAGARI LETTER VI;Mn;230;NSM;;;;;N;;;;; +A8F1;COMBINING DEVANAGARI SIGN AVAGRAHA;Mn;230;NSM;;;;;N;;;;; +AAB0;TAI VIET MAI KANG;Mn;230;NSM;;;;;N;;;;; +AAB2;TAI VIET VOWEL I;Mn;230;NSM;;;;;N;;;;; +AAB3;TAI VIET VOWEL UE;Mn;230;NSM;;;;;N;;;;; +AAB7;TAI VIET MAI KHIT;Mn;230;NSM;;;;;N;;;;; +AAB8;TAI VIET VOWEL IA;Mn;230;NSM;;;;;N;;;;; +AABE;TAI VIET VOWEL AM;Mn;230;NSM;;;;;N;;;;; +AABF;TAI VIET TONE MAI EK;Mn;230;NSM;;;;;N;;;;; +AAC1;TAI VIET TONE MAI THO;Mn;230;NSM;;;;;N;;;;; +FE20;COMBINING LIGATURE LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE21;COMBINING LIGATURE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE22;COMBINING DOUBLE TILDE LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE23;COMBINING DOUBLE TILDE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE24;COMBINING MACRON LEFT HALF;Mn;230;NSM;;;;;N;;;;; +FE25;COMBINING MACRON RIGHT HALF;Mn;230;NSM;;;;;N;;;;; +FE26;COMBINING CONJOINING MACRON;Mn;230;NSM;;;;;N;;;;; +10A0F;KHAROSHTHI SIGN VISARGA;Mn;230;NSM;;;;;N;;;;; +10A38;KHAROSHTHI SIGN BAR ABOVE;Mn;230;NSM;;;;;N;;;;; +1D185;MUSICAL SYMBOL COMBINING DOIT;Mn;230;NSM;;;;;N;;;;; +1D186;MUSICAL SYMBOL COMBINING RIP;Mn;230;NSM;;;;;N;;;;; +1D187;MUSICAL SYMBOL COMBINING FLIP;Mn;230;NSM;;;;;N;;;;; +1D188;MUSICAL SYMBOL COMBINING SMEAR;Mn;230;NSM;;;;;N;;;;; +1D189;MUSICAL SYMBOL COMBINING BEND;Mn;230;NSM;;;;;N;;;;; +1D1AA;MUSICAL SYMBOL COMBINING DOWN BOW;Mn;230;NSM;;;;;N;;;;; +1D1AB;MUSICAL SYMBOL COMBINING UP BOW;Mn;230;NSM;;;;;N;;;;; +1D1AC;MUSICAL SYMBOL COMBINING HARMONIC;Mn;230;NSM;;;;;N;;;;; +1D1AD;MUSICAL SYMBOL COMBINING SNAP PIZZICATO;Mn;230;NSM;;;;;N;;;;; +1D242;COMBINING GREEK MUSICAL TRISEME;Mn;230;NSM;;;;;N;;;;; +1D243;COMBINING GREEK MUSICAL TETRASEME;Mn;230;NSM;;;;;N;;;;; +1D244;COMBINING GREEK MUSICAL PENTASEME;Mn;230;NSM;;;;;N;;;;; diff --git a/src/graph/image.rs b/src/graph/image.rs index 5f6eef4..a953fc3 100644 --- a/src/graph/image.rs +++ b/src/graph/image.rs @@ -1,6 +1,9 @@ use std::{ fmt::{self, Debug, Formatter}, + hash::{Hash, Hasher}, io::Cursor, + process, + time::{SystemTime, UNIX_EPOCH}, }; use rustc_hash::{FxHashMap, FxHashSet}; @@ -24,6 +27,7 @@ pub enum GraphStyle { #[derive(Debug)] pub struct GraphImageManager<'a> { prepared_image_map: FxHashMap, + image_ids: FxHashSet, graph: &'a Graph<'a>, cell_width_type: CellWidthType, @@ -31,6 +35,7 @@ pub struct GraphImageManager<'a> { image_params: ImageParams, drawing_pixels: DrawingPixels, image_protocol: ImageProtocol, + session_nonce: u32, } impl<'a> GraphImageManager<'a> { @@ -46,12 +51,14 @@ impl<'a> GraphImageManager<'a> { GraphImageManager { prepared_image_map: FxHashMap::default(), + image_ids: FxHashSet::default(), graph, cell_width_type, graph_style, image_params, drawing_pixels, image_protocol, + session_nonce: create_session_nonce(), } } @@ -59,10 +66,15 @@ impl<'a> GraphImageManager<'a> { self.prepared_image_map.get(commit_hash).unwrap() } + pub fn image_ids(&self) -> &FxHashSet { + &self.image_ids + } + pub fn load_prepared_image(&mut self, commit_hash: &CommitHash) { if self.prepared_image_map.contains_key(commit_hash) { return; } + let image_id = graph_image_id(self.session_nonce, commit_hash); let graph_row_image = build_single_graph_row_image( self.graph, &self.image_params, @@ -70,8 +82,9 @@ impl<'a> GraphImageManager<'a> { self.graph_style, commit_hash, ); - let image = graph_row_image.prepare(self.cell_width_type, self.image_protocol); + let image = graph_row_image.prepare(self.cell_width_type, self.image_protocol, image_id); self.prepared_image_map.insert(commit_hash.clone(), image); + self.image_ids.insert(image_id); } } @@ -101,15 +114,34 @@ impl GraphRowImage { &self, cell_width_type: CellWidthType, image_protocol: ImageProtocol, + image_id: u32, ) -> PreparedImage { let image_cell_width = match cell_width_type { CellWidthType::Double => self.cell_count * 2, CellWidthType::Single => self.cell_count, }; - image_protocol.prepare_image(&self.bytes, image_cell_width) + image_protocol.prepare_image(&self.bytes, image_cell_width, image_id) } } +fn create_session_nonce() -> u32 { + let mut hasher = rustc_hash::FxHasher::default(); + process::id().hash(&mut hasher); + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .hash(&mut hasher); + hasher.finish() as u32 +} + +fn graph_image_id(session_nonce: u32, commit_hash: &CommitHash) -> u32 { + let mut hasher = rustc_hash::FxHasher::default(); + session_nonce.hash(&mut hasher); + commit_hash.hash(&mut hasher); + hasher.finish() as u32 +} + #[derive(Debug)] pub struct ImageParams { width: u16, diff --git a/src/lib.rs b/src/lib.rs index ce77ee7..f6fa320 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,7 @@ impl From> for protocol::ImageProtocol { Some(ImageProtocolType::Auto) => protocol::auto_detect(), Some(ImageProtocolType::Iterm) => protocol::ImageProtocol::Iterm2, Some(ImageProtocolType::Kitty) => protocol::ImageProtocol::Kitty, - Some(ImageProtocolType::KittyUnicode) => protocol::ImageProtocol::Kitty, + Some(ImageProtocolType::KittyUnicode) => protocol::ImageProtocol::KittyUnicode, None => protocol::auto_detect(), } } diff --git a/src/protocol.rs b/src/protocol.rs index 87a0cfb..8854cc8 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,6 +1,8 @@ use std::env; use base64::Engine; +use once_cell::sync::Lazy; +use ratatui::style::Color; use ratatui::style::Style; // By default assume the Iterm2 is the best protocol to use for all terminals *unless* an env @@ -23,6 +25,7 @@ pub fn auto_detect() -> ImageProtocol { pub enum ImageProtocol { Iterm2, Kitty, + KittyUnicode, } #[derive(Debug, Clone)] @@ -63,10 +66,13 @@ impl PreparedImage { } impl ImageProtocol { - pub fn prepare_image(&self, bytes: &[u8], cell_width: usize) -> PreparedImage { + pub fn prepare_image(&self, bytes: &[u8], cell_width: usize, image_id: u32) -> PreparedImage { let symbol = match self { ImageProtocol::Iterm2 => iterm2_encode(bytes, cell_width, 1), ImageProtocol::Kitty => kitty_encode(bytes, cell_width, 1), + ImageProtocol::KittyUnicode => { + return kitty_unicode_prepare(bytes, cell_width, image_id); + } }; let mut cells = Vec::with_capacity(cell_width); cells.push(PreparedImageCell { @@ -88,6 +94,7 @@ impl ImageProtocol { match self { ImageProtocol::Iterm2 => {} ImageProtocol::Kitty => kitty_clear_line(y), + ImageProtocol::KittyUnicode => {} } } @@ -95,10 +102,23 @@ impl ImageProtocol { match self { ImageProtocol::Iterm2 => {} ImageProtocol::Kitty => kitty_clear(), + ImageProtocol::KittyUnicode => {} } } } +static ROW_COLUMN_DIACRITICS: Lazy> = Lazy::new(|| { + include_str!("../assets/kitty/rowcolumn-diacritics.txt") + .lines() + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .map(|line| line.split(';').next().unwrap()) + .map(|hex| u32::from_str_radix(hex, 16).unwrap()) + .map(|codepoint| char::from_u32(codepoint).unwrap()) + .collect() +}); + +const KITTY_PLACEHOLDER: char = '\u{10EEEE}'; + fn to_base64_str(bytes: &[u8]) -> String { base64::engine::general_purpose::STANDARD.encode(bytes) } @@ -142,6 +162,77 @@ fn kitty_encode(bytes: &[u8], cell_width: usize, cell_height: usize) -> String { s } +fn kitty_unicode_prepare(bytes: &[u8], cell_width: usize, image_id: u32) -> PreparedImage { + let mut cells = Vec::with_capacity(cell_width); + let upload_symbol = kitty_unicode_encode(bytes, cell_width, 1, image_id); + let foreground = Color::Rgb( + ((image_id >> 16) & 0xff) as u8, + ((image_id >> 8) & 0xff) as u8, + (image_id & 0xff) as u8, + ); + let image_id_msb = ((image_id >> 24) & 0xff) as usize; + + for column in 0..cell_width { + let symbol = [ + KITTY_PLACEHOLDER, + row_column_diacritic(0), + row_column_diacritic(column), + row_column_diacritic(image_id_msb), + ] + .into_iter() + .collect(); + + cells.push(PreparedImageCell { + symbol, + style: Style::default().fg(foreground), + skip: false, + }); + } + + if let Some(first) = cells.first_mut() { + first.symbol = format!("{upload_symbol}{}", first.symbol); + } + + PreparedImage { cells, cell_width } +} + +fn kitty_unicode_encode( + bytes: &[u8], + cell_width: usize, + cell_height: usize, + image_id: u32, +) -> String { + let base64_str = to_base64_str(bytes); + let chunk_size = 4096; + + let mut s = String::new(); + + let chunks = base64_str.as_bytes().chunks(chunk_size); + let total_chunks = chunks.len(); + + for (i, chunk) in chunks.enumerate() { + s.push_str("\x1b_G"); + if i == 0 { + s.push_str(&format!( + "a=T,f=100,U=1,q=2,i={image_id},c={cell_width},r={cell_height}," + )); + } + if i < total_chunks - 1 { + s.push_str("m=1;"); + } else { + s.push_str("m=0;"); + } + s.push_str(std::str::from_utf8(chunk).unwrap()); + s.push_str("\x1b\\"); + } + + s +} + +fn row_column_diacritic(index: usize) -> char { + ROW_COLUMN_DIACRITICS[index] +} + fn kitty_clear_line(y: u16) { let y = y + 1; // 1-based print!("\x1b_Ga=d,d=Y,y={y};\x1b\\"); From 580f864921096a70f9843ee2dbc828f7ddcea91a Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 10:46:03 +0900 Subject: [PATCH 05/11] Inline kitty row/column diacritics --- assets/kitty/rowcolumn-diacritics.txt | 310 -------------------------- src/protocol.rs | 310 +++++++++++++++++++++++++- 2 files changed, 299 insertions(+), 321 deletions(-) delete mode 100644 assets/kitty/rowcolumn-diacritics.txt diff --git a/assets/kitty/rowcolumn-diacritics.txt b/assets/kitty/rowcolumn-diacritics.txt deleted file mode 100644 index 9d8091e..0000000 --- a/assets/kitty/rowcolumn-diacritics.txt +++ /dev/null @@ -1,310 +0,0 @@ -# This file lists the diacritics used to indicate row/column numbers for -# Unicode terminal image placeholders. It is derived from UnicodeData.txt for -# Unicode 6.0.0 (chosen somewhat arbitrarily: it's old enough, but still -# contains more than 255 suitable combining chars) using the following -# command: -# -# cat UnicodeData.txt | grep "Mn;230;NSM;;" | grep -v "0300\|0301\|0302\|0303\|0304\|0306\|0307\|0308\|0309\|030A\|030B\|030C\|030F\|0311\|0313\|0314\|0342\|0653\|0654" -# -# That is, we use combining chars of the same combining class 230 (above the -# base character) that do not have decomposition mappings, and we also remove -# some characters that may be fused with other characters during normalization, -# like 0041 0300 -> 00C0 which is À (A with grave). -# -0305;COMBINING OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING OVERSCORE;;;; -030D;COMBINING VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL LINE ABOVE;;;; -030E;COMBINING DOUBLE VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE VERTICAL LINE ABOVE;;;; -0310;COMBINING CANDRABINDU;Mn;230;NSM;;;;;N;NON-SPACING CANDRABINDU;;;; -0312;COMBINING TURNED COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING TURNED COMMA ABOVE;;;; -033D;COMBINING X ABOVE;Mn;230;NSM;;;;;N;NON-SPACING X ABOVE;;;; -033E;COMBINING VERTICAL TILDE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL TILDE;;;; -033F;COMBINING DOUBLE OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE OVERSCORE;;;; -0346;COMBINING BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; -034A;COMBINING NOT TILDE ABOVE;Mn;230;NSM;;;;;N;;;;; -034B;COMBINING HOMOTHETIC ABOVE;Mn;230;NSM;;;;;N;;;;; -034C;COMBINING ALMOST EQUAL TO ABOVE;Mn;230;NSM;;;;;N;;;;; -0350;COMBINING RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; -0351;COMBINING LEFT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; -0352;COMBINING FERMATA;Mn;230;NSM;;;;;N;;;;; -0357;COMBINING RIGHT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;; -035B;COMBINING ZIGZAG ABOVE;Mn;230;NSM;;;;;N;;;;; -0363;COMBINING LATIN SMALL LETTER A;Mn;230;NSM;;;;;N;;;;; -0364;COMBINING LATIN SMALL LETTER E;Mn;230;NSM;;;;;N;;;;; -0365;COMBINING LATIN SMALL LETTER I;Mn;230;NSM;;;;;N;;;;; -0366;COMBINING LATIN SMALL LETTER O;Mn;230;NSM;;;;;N;;;;; -0367;COMBINING LATIN SMALL LETTER U;Mn;230;NSM;;;;;N;;;;; -0368;COMBINING LATIN SMALL LETTER C;Mn;230;NSM;;;;;N;;;;; -0369;COMBINING LATIN SMALL LETTER D;Mn;230;NSM;;;;;N;;;;; -036A;COMBINING LATIN SMALL LETTER H;Mn;230;NSM;;;;;N;;;;; -036B;COMBINING LATIN SMALL LETTER M;Mn;230;NSM;;;;;N;;;;; -036C;COMBINING LATIN SMALL LETTER R;Mn;230;NSM;;;;;N;;;;; -036D;COMBINING LATIN SMALL LETTER T;Mn;230;NSM;;;;;N;;;;; -036E;COMBINING LATIN SMALL LETTER V;Mn;230;NSM;;;;;N;;;;; -036F;COMBINING LATIN SMALL LETTER X;Mn;230;NSM;;;;;N;;;;; -0483;COMBINING CYRILLIC TITLO;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING TITLO;;;; -0484;COMBINING CYRILLIC PALATALIZATION;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PALATALIZATION;;;; -0485;COMBINING CYRILLIC DASIA PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING DASIA PNEUMATA;;;; -0486;COMBINING CYRILLIC PSILI PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PSILI PNEUMATA;;;; -0487;COMBINING CYRILLIC POKRYTIE;Mn;230;NSM;;;;;N;;;;; -0592;HEBREW ACCENT SEGOL;Mn;230;NSM;;;;;N;;;;; -0593;HEBREW ACCENT SHALSHELET;Mn;230;NSM;;;;;N;;;;; -0594;HEBREW ACCENT ZAQEF QATAN;Mn;230;NSM;;;;;N;;;;; -0595;HEBREW ACCENT ZAQEF GADOL;Mn;230;NSM;;;;;N;;;;; -0597;HEBREW ACCENT REVIA;Mn;230;NSM;;;;;N;;;;; -0598;HEBREW ACCENT ZARQA;Mn;230;NSM;;;;;N;;;;; -0599;HEBREW ACCENT PASHTA;Mn;230;NSM;;;;;N;;;;; -059C;HEBREW ACCENT GERESH;Mn;230;NSM;;;;;N;;;;; -059D;HEBREW ACCENT GERESH MUQDAM;Mn;230;NSM;;;;;N;;;;; -059E;HEBREW ACCENT GERSHAYIM;Mn;230;NSM;;;;;N;;;;; -059F;HEBREW ACCENT QARNEY PARA;Mn;230;NSM;;;;;N;;;;; -05A0;HEBREW ACCENT TELISHA GEDOLA;Mn;230;NSM;;;;;N;;;;; -05A1;HEBREW ACCENT PAZER;Mn;230;NSM;;;;;N;;;;; -05A8;HEBREW ACCENT QADMA;Mn;230;NSM;;;;;N;;;;; -05A9;HEBREW ACCENT TELISHA QETANA;Mn;230;NSM;;;;;N;;;;; -05AB;HEBREW ACCENT OLE;Mn;230;NSM;;;;;N;;;;; -05AC;HEBREW ACCENT ILUY;Mn;230;NSM;;;;;N;;;;; -05AF;HEBREW MARK MASORA CIRCLE;Mn;230;NSM;;;;;N;;;;; -05C4;HEBREW MARK UPPER DOT;Mn;230;NSM;;;;;N;;;;; -0610;ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM;Mn;230;NSM;;;;;N;;;;; -0611;ARABIC SIGN ALAYHE ASSALLAM;Mn;230;NSM;;;;;N;;;;; -0612;ARABIC SIGN RAHMATULLAH ALAYHE;Mn;230;NSM;;;;;N;;;;; -0613;ARABIC SIGN RADI ALLAHOU ANHU;Mn;230;NSM;;;;;N;;;;; -0614;ARABIC SIGN TAKHALLUS;Mn;230;NSM;;;;;N;;;;; -0615;ARABIC SMALL HIGH TAH;Mn;230;NSM;;;;;N;;;;; -0616;ARABIC SMALL HIGH LIGATURE ALEF WITH LAM WITH YEH;Mn;230;NSM;;;;;N;;;;; -0617;ARABIC SMALL HIGH ZAIN;Mn;230;NSM;;;;;N;;;;; -0657;ARABIC INVERTED DAMMA;Mn;230;NSM;;;;;N;;;;; -0658;ARABIC MARK NOON GHUNNA;Mn;230;NSM;;;;;N;;;;; -0659;ARABIC ZWARAKAY;Mn;230;NSM;;;;;N;;;;; -065A;ARABIC VOWEL SIGN SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; -065B;ARABIC VOWEL SIGN INVERTED SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;; -065D;ARABIC REVERSED DAMMA;Mn;230;NSM;;;;;N;;;;; -065E;ARABIC FATHA WITH TWO DOTS;Mn;230;NSM;;;;;N;;;;; -06D6;ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; -06D7;ARABIC SMALL HIGH LIGATURE QAF WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;; -06D8;ARABIC SMALL HIGH MEEM INITIAL FORM;Mn;230;NSM;;;;;N;;;;; -06D9;ARABIC SMALL HIGH LAM ALEF;Mn;230;NSM;;;;;N;;;;; -06DA;ARABIC SMALL HIGH JEEM;Mn;230;NSM;;;;;N;;;;; -06DB;ARABIC SMALL HIGH THREE DOTS;Mn;230;NSM;;;;;N;;;;; -06DC;ARABIC SMALL HIGH SEEN;Mn;230;NSM;;;;;N;;;;; -06DF;ARABIC SMALL HIGH ROUNDED ZERO;Mn;230;NSM;;;;;N;;;;; -06E0;ARABIC SMALL HIGH UPRIGHT RECTANGULAR ZERO;Mn;230;NSM;;;;;N;;;;; -06E1;ARABIC SMALL HIGH DOTLESS HEAD OF KHAH;Mn;230;NSM;;;;;N;;;;; -06E2;ARABIC SMALL HIGH MEEM ISOLATED FORM;Mn;230;NSM;;;;;N;;;;; -06E4;ARABIC SMALL HIGH MADDA;Mn;230;NSM;;;;;N;;;;; -06E7;ARABIC SMALL HIGH YEH;Mn;230;NSM;;;;;N;;;;; -06E8;ARABIC SMALL HIGH NOON;Mn;230;NSM;;;;;N;;;;; -06EB;ARABIC EMPTY CENTRE HIGH STOP;Mn;230;NSM;;;;;N;;;;; -06EC;ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE;Mn;230;NSM;;;;;N;;;;; -0730;SYRIAC PTHAHA ABOVE;Mn;230;NSM;;;;;N;;;;; -0732;SYRIAC PTHAHA DOTTED;Mn;230;NSM;;;;;N;;;;; -0733;SYRIAC ZQAPHA ABOVE;Mn;230;NSM;;;;;N;;;;; -0735;SYRIAC ZQAPHA DOTTED;Mn;230;NSM;;;;;N;;;;; -0736;SYRIAC RBASA ABOVE;Mn;230;NSM;;;;;N;;;;; -073A;SYRIAC HBASA ABOVE;Mn;230;NSM;;;;;N;;;;; -073D;SYRIAC ESASA ABOVE;Mn;230;NSM;;;;;N;;;;; -073F;SYRIAC RWAHA;Mn;230;NSM;;;;;N;;;;; -0740;SYRIAC FEMININE DOT;Mn;230;NSM;;;;;N;;;;; -0741;SYRIAC QUSHSHAYA;Mn;230;NSM;;;;;N;;;;; -0743;SYRIAC TWO VERTICAL DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; -0745;SYRIAC THREE DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; -0747;SYRIAC OBLIQUE LINE ABOVE;Mn;230;NSM;;;;;N;;;;; -0749;SYRIAC MUSIC;Mn;230;NSM;;;;;N;;;;; -074A;SYRIAC BARREKH;Mn;230;NSM;;;;;N;;;;; -07EB;NKO COMBINING SHORT HIGH TONE;Mn;230;NSM;;;;;N;;;;; -07EC;NKO COMBINING SHORT LOW TONE;Mn;230;NSM;;;;;N;;;;; -07ED;NKO COMBINING SHORT RISING TONE;Mn;230;NSM;;;;;N;;;;; -07EE;NKO COMBINING LONG DESCENDING TONE;Mn;230;NSM;;;;;N;;;;; -07EF;NKO COMBINING LONG HIGH TONE;Mn;230;NSM;;;;;N;;;;; -07F0;NKO COMBINING LONG LOW TONE;Mn;230;NSM;;;;;N;;;;; -07F1;NKO COMBINING LONG RISING TONE;Mn;230;NSM;;;;;N;;;;; -07F3;NKO COMBINING DOUBLE DOT ABOVE;Mn;230;NSM;;;;;N;;;;; -0816;SAMARITAN MARK IN;Mn;230;NSM;;;;;N;;;;; -0817;SAMARITAN MARK IN-ALAF;Mn;230;NSM;;;;;N;;;;; -0818;SAMARITAN MARK OCCLUSION;Mn;230;NSM;;;;;N;;;;; -0819;SAMARITAN MARK DAGESH;Mn;230;NSM;;;;;N;;;;; -081B;SAMARITAN MARK EPENTHETIC YUT;Mn;230;NSM;;;;;N;;;;; -081C;SAMARITAN VOWEL SIGN LONG E;Mn;230;NSM;;;;;N;;;;; -081D;SAMARITAN VOWEL SIGN E;Mn;230;NSM;;;;;N;;;;; -081E;SAMARITAN VOWEL SIGN OVERLONG AA;Mn;230;NSM;;;;;N;;;;; -081F;SAMARITAN VOWEL SIGN LONG AA;Mn;230;NSM;;;;;N;;;;; -0820;SAMARITAN VOWEL SIGN AA;Mn;230;NSM;;;;;N;;;;; -0821;SAMARITAN VOWEL SIGN OVERLONG A;Mn;230;NSM;;;;;N;;;;; -0822;SAMARITAN VOWEL SIGN LONG A;Mn;230;NSM;;;;;N;;;;; -0823;SAMARITAN VOWEL SIGN A;Mn;230;NSM;;;;;N;;;;; -0825;SAMARITAN VOWEL SIGN SHORT A;Mn;230;NSM;;;;;N;;;;; -0826;SAMARITAN VOWEL SIGN LONG U;Mn;230;NSM;;;;;N;;;;; -0827;SAMARITAN VOWEL SIGN U;Mn;230;NSM;;;;;N;;;;; -0829;SAMARITAN VOWEL SIGN LONG I;Mn;230;NSM;;;;;N;;;;; -082A;SAMARITAN VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; -082B;SAMARITAN VOWEL SIGN O;Mn;230;NSM;;;;;N;;;;; -082C;SAMARITAN VOWEL SIGN SUKUN;Mn;230;NSM;;;;;N;;;;; -082D;SAMARITAN MARK NEQUDAA;Mn;230;NSM;;;;;N;;;;; -0951;DEVANAGARI STRESS SIGN UDATTA;Mn;230;NSM;;;;;N;;;;; -0953;DEVANAGARI GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; -0954;DEVANAGARI ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; -0F82;TIBETAN SIGN NYI ZLA NAA DA;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU WITH ORNAMENT;;;; -0F83;TIBETAN SIGN SNA LDAN;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU;;;; -0F86;TIBETAN SIGN LCI RTAGS;Mn;230;NSM;;;;;N;;;;; -0F87;TIBETAN SIGN YANG RTAGS;Mn;230;NSM;;;;;N;;;;; -135D;ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; -135E;ETHIOPIC COMBINING VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; -135F;ETHIOPIC COMBINING GEMINATION MARK;Mn;230;NSM;;;;;N;;;;; -17DD;KHMER SIGN ATTHACAN;Mn;230;NSM;;;;;N;;;;; -193A;LIMBU SIGN KEMPHRENG;Mn;230;NSM;;;;;N;;;;; -1A17;BUGINESE VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;; -1A75;TAI THAM SIGN TONE-1;Mn;230;NSM;;;;;N;;;;; -1A76;TAI THAM SIGN TONE-2;Mn;230;NSM;;;;;N;;;;; -1A77;TAI THAM SIGN KHUEN TONE-3;Mn;230;NSM;;;;;N;;;;; -1A78;TAI THAM SIGN KHUEN TONE-4;Mn;230;NSM;;;;;N;;;;; -1A79;TAI THAM SIGN KHUEN TONE-5;Mn;230;NSM;;;;;N;;;;; -1A7A;TAI THAM SIGN RA HAAM;Mn;230;NSM;;;;;N;;;;; -1A7B;TAI THAM SIGN MAI SAM;Mn;230;NSM;;;;;N;;;;; -1A7C;TAI THAM SIGN KHUEN-LUE KARAN;Mn;230;NSM;;;;;N;;;;; -1B6B;BALINESE MUSICAL SYMBOL COMBINING TEGEH;Mn;230;NSM;;;;;N;;;;; -1B6D;BALINESE MUSICAL SYMBOL COMBINING KEMPUL;Mn;230;NSM;;;;;N;;;;; -1B6E;BALINESE MUSICAL SYMBOL COMBINING KEMPLI;Mn;230;NSM;;;;;N;;;;; -1B6F;BALINESE MUSICAL SYMBOL COMBINING JEGOGAN;Mn;230;NSM;;;;;N;;;;; -1B70;BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; -1B71;BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;; -1B72;BALINESE MUSICAL SYMBOL COMBINING BENDE;Mn;230;NSM;;;;;N;;;;; -1B73;BALINESE MUSICAL SYMBOL COMBINING GONG;Mn;230;NSM;;;;;N;;;;; -1CD0;VEDIC TONE KARSHANA;Mn;230;NSM;;;;;N;;;;; -1CD1;VEDIC TONE SHARA;Mn;230;NSM;;;;;N;;;;; -1CD2;VEDIC TONE PRENKHA;Mn;230;NSM;;;;;N;;;;; -1CDA;VEDIC TONE DOUBLE SVARITA;Mn;230;NSM;;;;;N;;;;; -1CDB;VEDIC TONE TRIPLE SVARITA;Mn;230;NSM;;;;;N;;;;; -1CE0;VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA;Mn;230;NSM;;;;;N;;;;; -1DC0;COMBINING DOTTED GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;; -1DC1;COMBINING DOTTED ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;; -1DC3;COMBINING SUSPENSION MARK;Mn;230;NSM;;;;;N;;;;; -1DC4;COMBINING MACRON-ACUTE;Mn;230;NSM;;;;;N;;;;; -1DC5;COMBINING GRAVE-MACRON;Mn;230;NSM;;;;;N;;;;; -1DC6;COMBINING MACRON-GRAVE;Mn;230;NSM;;;;;N;;;;; -1DC7;COMBINING ACUTE-MACRON;Mn;230;NSM;;;;;N;;;;; -1DC8;COMBINING GRAVE-ACUTE-GRAVE;Mn;230;NSM;;;;;N;;;;; -1DC9;COMBINING ACUTE-GRAVE-ACUTE;Mn;230;NSM;;;;;N;;;;; -1DCB;COMBINING BREVE-MACRON;Mn;230;NSM;;;;;N;;;;; -1DCC;COMBINING MACRON-BREVE;Mn;230;NSM;;;;;N;;;;; -1DD1;COMBINING UR ABOVE;Mn;230;NSM;;;;;N;;;;; -1DD2;COMBINING US ABOVE;Mn;230;NSM;;;;;N;;;;; -1DD3;COMBINING LATIN SMALL LETTER FLATTENED OPEN A ABOVE;Mn;230;NSM;;;;;N;;;;; -1DD4;COMBINING LATIN SMALL LETTER AE;Mn;230;NSM;;;;;N;;;;; -1DD5;COMBINING LATIN SMALL LETTER AO;Mn;230;NSM;;;;;N;;;;; -1DD6;COMBINING LATIN SMALL LETTER AV;Mn;230;NSM;;;;;N;;;;; -1DD7;COMBINING LATIN SMALL LETTER C CEDILLA;Mn;230;NSM;;;;;N;;;;; -1DD8;COMBINING LATIN SMALL LETTER INSULAR D;Mn;230;NSM;;;;;N;;;;; -1DD9;COMBINING LATIN SMALL LETTER ETH;Mn;230;NSM;;;;;N;;;;; -1DDA;COMBINING LATIN SMALL LETTER G;Mn;230;NSM;;;;;N;;;;; -1DDB;COMBINING LATIN LETTER SMALL CAPITAL G;Mn;230;NSM;;;;;N;;;;; -1DDC;COMBINING LATIN SMALL LETTER K;Mn;230;NSM;;;;;N;;;;; -1DDD;COMBINING LATIN SMALL LETTER L;Mn;230;NSM;;;;;N;;;;; -1DDE;COMBINING LATIN LETTER SMALL CAPITAL L;Mn;230;NSM;;;;;N;;;;; -1DDF;COMBINING LATIN LETTER SMALL CAPITAL M;Mn;230;NSM;;;;;N;;;;; -1DE0;COMBINING LATIN SMALL LETTER N;Mn;230;NSM;;;;;N;;;;; -1DE1;COMBINING LATIN LETTER SMALL CAPITAL N;Mn;230;NSM;;;;;N;;;;; -1DE2;COMBINING LATIN LETTER SMALL CAPITAL R;Mn;230;NSM;;;;;N;;;;; -1DE3;COMBINING LATIN SMALL LETTER R ROTUNDA;Mn;230;NSM;;;;;N;;;;; -1DE4;COMBINING LATIN SMALL LETTER S;Mn;230;NSM;;;;;N;;;;; -1DE5;COMBINING LATIN SMALL LETTER LONG S;Mn;230;NSM;;;;;N;;;;; -1DE6;COMBINING LATIN SMALL LETTER Z;Mn;230;NSM;;;;;N;;;;; -1DFE;COMBINING LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; -20D0;COMBINING LEFT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT HARPOON ABOVE;;;; -20D1;COMBINING RIGHT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT HARPOON ABOVE;;;; -20D4;COMBINING ANTICLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING ANTICLOCKWISE ARROW ABOVE;;;; -20D5;COMBINING CLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING CLOCKWISE ARROW ABOVE;;;; -20D6;COMBINING LEFT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT ARROW ABOVE;;;; -20D7;COMBINING RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT ARROW ABOVE;;;; -20DB;COMBINING THREE DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING THREE DOTS ABOVE;;;; -20DC;COMBINING FOUR DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING FOUR DOTS ABOVE;;;; -20E1;COMBINING LEFT RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT RIGHT ARROW ABOVE;;;; -20E7;COMBINING ANNUITY SYMBOL;Mn;230;NSM;;;;;N;;;;; -20E9;COMBINING WIDE BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;; -20F0;COMBINING ASTERISK ABOVE;Mn;230;NSM;;;;;N;;;;; -2CEF;COPTIC COMBINING NI ABOVE;Mn;230;NSM;;;;;N;;;;; -2CF0;COPTIC COMBINING SPIRITUS ASPER;Mn;230;NSM;;;;;N;;;;; -2CF1;COPTIC COMBINING SPIRITUS LENIS;Mn;230;NSM;;;;;N;;;;; -2DE0;COMBINING CYRILLIC LETTER BE;Mn;230;NSM;;;;;N;;;;; -2DE1;COMBINING CYRILLIC LETTER VE;Mn;230;NSM;;;;;N;;;;; -2DE2;COMBINING CYRILLIC LETTER GHE;Mn;230;NSM;;;;;N;;;;; -2DE3;COMBINING CYRILLIC LETTER DE;Mn;230;NSM;;;;;N;;;;; -2DE4;COMBINING CYRILLIC LETTER ZHE;Mn;230;NSM;;;;;N;;;;; -2DE5;COMBINING CYRILLIC LETTER ZE;Mn;230;NSM;;;;;N;;;;; -2DE6;COMBINING CYRILLIC LETTER KA;Mn;230;NSM;;;;;N;;;;; -2DE7;COMBINING CYRILLIC LETTER EL;Mn;230;NSM;;;;;N;;;;; -2DE8;COMBINING CYRILLIC LETTER EM;Mn;230;NSM;;;;;N;;;;; -2DE9;COMBINING CYRILLIC LETTER EN;Mn;230;NSM;;;;;N;;;;; -2DEA;COMBINING CYRILLIC LETTER O;Mn;230;NSM;;;;;N;;;;; -2DEB;COMBINING CYRILLIC LETTER PE;Mn;230;NSM;;;;;N;;;;; -2DEC;COMBINING CYRILLIC LETTER ER;Mn;230;NSM;;;;;N;;;;; -2DED;COMBINING CYRILLIC LETTER ES;Mn;230;NSM;;;;;N;;;;; -2DEE;COMBINING CYRILLIC LETTER TE;Mn;230;NSM;;;;;N;;;;; -2DEF;COMBINING CYRILLIC LETTER HA;Mn;230;NSM;;;;;N;;;;; -2DF0;COMBINING CYRILLIC LETTER TSE;Mn;230;NSM;;;;;N;;;;; -2DF1;COMBINING CYRILLIC LETTER CHE;Mn;230;NSM;;;;;N;;;;; -2DF2;COMBINING CYRILLIC LETTER SHA;Mn;230;NSM;;;;;N;;;;; -2DF3;COMBINING CYRILLIC LETTER SHCHA;Mn;230;NSM;;;;;N;;;;; -2DF4;COMBINING CYRILLIC LETTER FITA;Mn;230;NSM;;;;;N;;;;; -2DF5;COMBINING CYRILLIC LETTER ES-TE;Mn;230;NSM;;;;;N;;;;; -2DF6;COMBINING CYRILLIC LETTER A;Mn;230;NSM;;;;;N;;;;; -2DF7;COMBINING CYRILLIC LETTER IE;Mn;230;NSM;;;;;N;;;;; -2DF8;COMBINING CYRILLIC LETTER DJERV;Mn;230;NSM;;;;;N;;;;; -2DF9;COMBINING CYRILLIC LETTER MONOGRAPH UK;Mn;230;NSM;;;;;N;;;;; -2DFA;COMBINING CYRILLIC LETTER YAT;Mn;230;NSM;;;;;N;;;;; -2DFB;COMBINING CYRILLIC LETTER YU;Mn;230;NSM;;;;;N;;;;; -2DFC;COMBINING CYRILLIC LETTER IOTIFIED A;Mn;230;NSM;;;;;N;;;;; -2DFD;COMBINING CYRILLIC LETTER LITTLE YUS;Mn;230;NSM;;;;;N;;;;; -2DFE;COMBINING CYRILLIC LETTER BIG YUS;Mn;230;NSM;;;;;N;;;;; -2DFF;COMBINING CYRILLIC LETTER IOTIFIED BIG YUS;Mn;230;NSM;;;;;N;;;;; -A66F;COMBINING CYRILLIC VZMET;Mn;230;NSM;;;;;N;;;;; -A67C;COMBINING CYRILLIC KAVYKA;Mn;230;NSM;;;;;N;;;;; -A67D;COMBINING CYRILLIC PAYEROK;Mn;230;NSM;;;;;N;;;;; -A6F0;BAMUM COMBINING MARK KOQNDON;Mn;230;NSM;;;;;N;;;;; -A6F1;BAMUM COMBINING MARK TUKWENTIS;Mn;230;NSM;;;;;N;;;;; -A8E0;COMBINING DEVANAGARI DIGIT ZERO;Mn;230;NSM;;;;;N;;;;; -A8E1;COMBINING DEVANAGARI DIGIT ONE;Mn;230;NSM;;;;;N;;;;; -A8E2;COMBINING DEVANAGARI DIGIT TWO;Mn;230;NSM;;;;;N;;;;; -A8E3;COMBINING DEVANAGARI DIGIT THREE;Mn;230;NSM;;;;;N;;;;; -A8E4;COMBINING DEVANAGARI DIGIT FOUR;Mn;230;NSM;;;;;N;;;;; -A8E5;COMBINING DEVANAGARI DIGIT FIVE;Mn;230;NSM;;;;;N;;;;; -A8E6;COMBINING DEVANAGARI DIGIT SIX;Mn;230;NSM;;;;;N;;;;; -A8E7;COMBINING DEVANAGARI DIGIT SEVEN;Mn;230;NSM;;;;;N;;;;; -A8E8;COMBINING DEVANAGARI DIGIT EIGHT;Mn;230;NSM;;;;;N;;;;; -A8E9;COMBINING DEVANAGARI DIGIT NINE;Mn;230;NSM;;;;;N;;;;; -A8EA;COMBINING DEVANAGARI LETTER A;Mn;230;NSM;;;;;N;;;;; -A8EB;COMBINING DEVANAGARI LETTER U;Mn;230;NSM;;;;;N;;;;; -A8EC;COMBINING DEVANAGARI LETTER KA;Mn;230;NSM;;;;;N;;;;; -A8ED;COMBINING DEVANAGARI LETTER NA;Mn;230;NSM;;;;;N;;;;; -A8EE;COMBINING DEVANAGARI LETTER PA;Mn;230;NSM;;;;;N;;;;; -A8EF;COMBINING DEVANAGARI LETTER RA;Mn;230;NSM;;;;;N;;;;; -A8F0;COMBINING DEVANAGARI LETTER VI;Mn;230;NSM;;;;;N;;;;; -A8F1;COMBINING DEVANAGARI SIGN AVAGRAHA;Mn;230;NSM;;;;;N;;;;; -AAB0;TAI VIET MAI KANG;Mn;230;NSM;;;;;N;;;;; -AAB2;TAI VIET VOWEL I;Mn;230;NSM;;;;;N;;;;; -AAB3;TAI VIET VOWEL UE;Mn;230;NSM;;;;;N;;;;; -AAB7;TAI VIET MAI KHIT;Mn;230;NSM;;;;;N;;;;; -AAB8;TAI VIET VOWEL IA;Mn;230;NSM;;;;;N;;;;; -AABE;TAI VIET VOWEL AM;Mn;230;NSM;;;;;N;;;;; -AABF;TAI VIET TONE MAI EK;Mn;230;NSM;;;;;N;;;;; -AAC1;TAI VIET TONE MAI THO;Mn;230;NSM;;;;;N;;;;; -FE20;COMBINING LIGATURE LEFT HALF;Mn;230;NSM;;;;;N;;;;; -FE21;COMBINING LIGATURE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; -FE22;COMBINING DOUBLE TILDE LEFT HALF;Mn;230;NSM;;;;;N;;;;; -FE23;COMBINING DOUBLE TILDE RIGHT HALF;Mn;230;NSM;;;;;N;;;;; -FE24;COMBINING MACRON LEFT HALF;Mn;230;NSM;;;;;N;;;;; -FE25;COMBINING MACRON RIGHT HALF;Mn;230;NSM;;;;;N;;;;; -FE26;COMBINING CONJOINING MACRON;Mn;230;NSM;;;;;N;;;;; -10A0F;KHAROSHTHI SIGN VISARGA;Mn;230;NSM;;;;;N;;;;; -10A38;KHAROSHTHI SIGN BAR ABOVE;Mn;230;NSM;;;;;N;;;;; -1D185;MUSICAL SYMBOL COMBINING DOIT;Mn;230;NSM;;;;;N;;;;; -1D186;MUSICAL SYMBOL COMBINING RIP;Mn;230;NSM;;;;;N;;;;; -1D187;MUSICAL SYMBOL COMBINING FLIP;Mn;230;NSM;;;;;N;;;;; -1D188;MUSICAL SYMBOL COMBINING SMEAR;Mn;230;NSM;;;;;N;;;;; -1D189;MUSICAL SYMBOL COMBINING BEND;Mn;230;NSM;;;;;N;;;;; -1D1AA;MUSICAL SYMBOL COMBINING DOWN BOW;Mn;230;NSM;;;;;N;;;;; -1D1AB;MUSICAL SYMBOL COMBINING UP BOW;Mn;230;NSM;;;;;N;;;;; -1D1AC;MUSICAL SYMBOL COMBINING HARMONIC;Mn;230;NSM;;;;;N;;;;; -1D1AD;MUSICAL SYMBOL COMBINING SNAP PIZZICATO;Mn;230;NSM;;;;;N;;;;; -1D242;COMBINING GREEK MUSICAL TRISEME;Mn;230;NSM;;;;;N;;;;; -1D243;COMBINING GREEK MUSICAL TETRASEME;Mn;230;NSM;;;;;N;;;;; -1D244;COMBINING GREEK MUSICAL PENTASEME;Mn;230;NSM;;;;;N;;;;; diff --git a/src/protocol.rs b/src/protocol.rs index 8854cc8..3507bbf 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,7 +1,6 @@ use std::env; use base64::Engine; -use once_cell::sync::Lazy; use ratatui::style::Color; use ratatui::style::Style; @@ -107,17 +106,306 @@ impl ImageProtocol { } } -static ROW_COLUMN_DIACRITICS: Lazy> = Lazy::new(|| { - include_str!("../assets/kitty/rowcolumn-diacritics.txt") - .lines() - .filter(|line| !line.is_empty() && !line.starts_with('#')) - .map(|line| line.split(';').next().unwrap()) - .map(|hex| u32::from_str_radix(hex, 16).unwrap()) - .map(|codepoint| char::from_u32(codepoint).unwrap()) - .collect() -}); - const KITTY_PLACEHOLDER: char = '\u{10EEEE}'; +static ROW_COLUMN_DIACRITICS: &[char] = &[ + '\u{0305}', + '\u{030D}', + '\u{030E}', + '\u{0310}', + '\u{0312}', + '\u{033D}', + '\u{033E}', + '\u{033F}', + '\u{0346}', + '\u{034A}', + '\u{034B}', + '\u{034C}', + '\u{0350}', + '\u{0351}', + '\u{0352}', + '\u{0357}', + '\u{035B}', + '\u{0363}', + '\u{0364}', + '\u{0365}', + '\u{0366}', + '\u{0367}', + '\u{0368}', + '\u{0369}', + '\u{036A}', + '\u{036B}', + '\u{036C}', + '\u{036D}', + '\u{036E}', + '\u{036F}', + '\u{0483}', + '\u{0484}', + '\u{0485}', + '\u{0486}', + '\u{0487}', + '\u{0592}', + '\u{0593}', + '\u{0594}', + '\u{0595}', + '\u{0597}', + '\u{0598}', + '\u{0599}', + '\u{059C}', + '\u{059D}', + '\u{059E}', + '\u{059F}', + '\u{05A0}', + '\u{05A1}', + '\u{05A8}', + '\u{05A9}', + '\u{05AB}', + '\u{05AC}', + '\u{05AF}', + '\u{05C4}', + '\u{0610}', + '\u{0611}', + '\u{0612}', + '\u{0613}', + '\u{0614}', + '\u{0615}', + '\u{0616}', + '\u{0617}', + '\u{0657}', + '\u{0658}', + '\u{0659}', + '\u{065A}', + '\u{065B}', + '\u{065D}', + '\u{065E}', + '\u{06D6}', + '\u{06D7}', + '\u{06D8}', + '\u{06D9}', + '\u{06DA}', + '\u{06DB}', + '\u{06DC}', + '\u{06DF}', + '\u{06E0}', + '\u{06E1}', + '\u{06E2}', + '\u{06E4}', + '\u{06E7}', + '\u{06E8}', + '\u{06EB}', + '\u{06EC}', + '\u{0730}', + '\u{0732}', + '\u{0733}', + '\u{0735}', + '\u{0736}', + '\u{073A}', + '\u{073D}', + '\u{073F}', + '\u{0740}', + '\u{0741}', + '\u{0743}', + '\u{0745}', + '\u{0747}', + '\u{0749}', + '\u{074A}', + '\u{07EB}', + '\u{07EC}', + '\u{07ED}', + '\u{07EE}', + '\u{07EF}', + '\u{07F0}', + '\u{07F1}', + '\u{07F3}', + '\u{0816}', + '\u{0817}', + '\u{0818}', + '\u{0819}', + '\u{081B}', + '\u{081C}', + '\u{081D}', + '\u{081E}', + '\u{081F}', + '\u{0820}', + '\u{0821}', + '\u{0822}', + '\u{0823}', + '\u{0825}', + '\u{0826}', + '\u{0827}', + '\u{0829}', + '\u{082A}', + '\u{082B}', + '\u{082C}', + '\u{082D}', + '\u{0951}', + '\u{0953}', + '\u{0954}', + '\u{0F82}', + '\u{0F83}', + '\u{0F86}', + '\u{0F87}', + '\u{135D}', + '\u{135E}', + '\u{135F}', + '\u{17DD}', + '\u{193A}', + '\u{1A17}', + '\u{1A75}', + '\u{1A76}', + '\u{1A77}', + '\u{1A78}', + '\u{1A79}', + '\u{1A7A}', + '\u{1A7B}', + '\u{1A7C}', + '\u{1B6B}', + '\u{1B6D}', + '\u{1B6E}', + '\u{1B6F}', + '\u{1B70}', + '\u{1B71}', + '\u{1B72}', + '\u{1B73}', + '\u{1CD0}', + '\u{1CD1}', + '\u{1CD2}', + '\u{1CDA}', + '\u{1CDB}', + '\u{1CE0}', + '\u{1DC0}', + '\u{1DC1}', + '\u{1DC3}', + '\u{1DC4}', + '\u{1DC5}', + '\u{1DC6}', + '\u{1DC7}', + '\u{1DC8}', + '\u{1DC9}', + '\u{1DCB}', + '\u{1DCC}', + '\u{1DD1}', + '\u{1DD2}', + '\u{1DD3}', + '\u{1DD4}', + '\u{1DD5}', + '\u{1DD6}', + '\u{1DD7}', + '\u{1DD8}', + '\u{1DD9}', + '\u{1DDA}', + '\u{1DDB}', + '\u{1DDC}', + '\u{1DDD}', + '\u{1DDE}', + '\u{1DDF}', + '\u{1DE0}', + '\u{1DE1}', + '\u{1DE2}', + '\u{1DE3}', + '\u{1DE4}', + '\u{1DE5}', + '\u{1DE6}', + '\u{1DFE}', + '\u{20D0}', + '\u{20D1}', + '\u{20D4}', + '\u{20D5}', + '\u{20D6}', + '\u{20D7}', + '\u{20DB}', + '\u{20DC}', + '\u{20E1}', + '\u{20E7}', + '\u{20E9}', + '\u{20F0}', + '\u{2CEF}', + '\u{2CF0}', + '\u{2CF1}', + '\u{2DE0}', + '\u{2DE1}', + '\u{2DE2}', + '\u{2DE3}', + '\u{2DE4}', + '\u{2DE5}', + '\u{2DE6}', + '\u{2DE7}', + '\u{2DE8}', + '\u{2DE9}', + '\u{2DEA}', + '\u{2DEB}', + '\u{2DEC}', + '\u{2DED}', + '\u{2DEE}', + '\u{2DEF}', + '\u{2DF0}', + '\u{2DF1}', + '\u{2DF2}', + '\u{2DF3}', + '\u{2DF4}', + '\u{2DF5}', + '\u{2DF6}', + '\u{2DF7}', + '\u{2DF8}', + '\u{2DF9}', + '\u{2DFA}', + '\u{2DFB}', + '\u{2DFC}', + '\u{2DFD}', + '\u{2DFE}', + '\u{2DFF}', + '\u{A66F}', + '\u{A67C}', + '\u{A67D}', + '\u{A6F0}', + '\u{A6F1}', + '\u{A8E0}', + '\u{A8E1}', + '\u{A8E2}', + '\u{A8E3}', + '\u{A8E4}', + '\u{A8E5}', + '\u{A8E6}', + '\u{A8E7}', + '\u{A8E8}', + '\u{A8E9}', + '\u{A8EA}', + '\u{A8EB}', + '\u{A8EC}', + '\u{A8ED}', + '\u{A8EE}', + '\u{A8EF}', + '\u{A8F0}', + '\u{A8F1}', + '\u{AAB0}', + '\u{AAB2}', + '\u{AAB3}', + '\u{AAB7}', + '\u{AAB8}', + '\u{AABE}', + '\u{AABF}', + '\u{AAC1}', + '\u{FE20}', + '\u{FE21}', + '\u{FE22}', + '\u{FE23}', + '\u{FE24}', + '\u{FE25}', + '\u{FE26}', + '\u{10A0F}', + '\u{10A38}', + '\u{1D185}', + '\u{1D186}', + '\u{1D187}', + '\u{1D188}', + '\u{1D189}', + '\u{1D1AA}', + '\u{1D1AB}', + '\u{1D1AC}', + '\u{1D1AD}', + '\u{1D242}', + '\u{1D243}', + '\u{1D244}', +]; fn to_base64_str(bytes: &[u8]) -> String { base64::engine::general_purpose::STANDARD.encode(bytes) From 6ab00f7a49d31acaaac4f52adbd53964f8615018 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 11:24:24 +0900 Subject: [PATCH 06/11] Separate kitty uploads from redraws --- src/app.rs | 30 +++++++++++++++++++++- src/graph/image.rs | 14 ++++++++-- src/protocol.rs | 19 ++++++++++---- src/view/detail.rs | 16 ++++++++++++ src/view/list.rs | 12 +++++++++ src/view/refs.rs | 18 +++++++++++++ src/view/user_command.rs | 17 ++++++++++++ src/view/views.rs | 33 ++++++++++++++++++++++++ src/widget/commit_list.rs | 54 ++++++++++++++++++++++----------------- 9 files changed, 182 insertions(+), 31 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3004fe9..3cd2568 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,7 @@ -use std::rc::Rc; +use std::{ + io::{self, Write}, + rc::Rc, +}; use ratatui::{ crossterm::event::{KeyCode, KeyEvent}, @@ -149,6 +152,8 @@ impl App<'_> { terminal.clear()?; loop { + self.prepare_render(terminal)?; + self.flush_pending_graph_uploads()?; terminal.draw(|f| self.render(f))?; match self.ec.recv() { AppEvent::Key(key) => { @@ -284,6 +289,29 @@ impl App<'_> { } } + fn prepare_render(&mut self, terminal: &mut DefaultTerminal) -> Result<(), std::io::Error> { + let area: Rect = terminal.size()?.into(); + let [view_area, _status_line_area] = + Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(area); + self.update_state(view_area); + self.view.update_layout(view_area); + self.view.prepare_graph_uploads(); + Ok(()) + } + + fn flush_pending_graph_uploads(&mut self) -> Result<(), std::io::Error> { + let uploads = self.view.drain_pending_graph_uploads(); + if uploads.is_empty() { + return Ok(()); + } + + let mut stdout = io::stdout().lock(); + for upload in uploads { + stdout.write_all(upload.as_bytes())?; + } + stdout.flush() + } + fn render(&mut self, f: &mut Frame) { let base = Block::default() .fg(self.ctx.color_theme.fg) diff --git a/src/graph/image.rs b/src/graph/image.rs index a953fc3..310a95f 100644 --- a/src/graph/image.rs +++ b/src/graph/image.rs @@ -28,6 +28,7 @@ pub enum GraphStyle { pub struct GraphImageManager<'a> { prepared_image_map: FxHashMap, image_ids: FxHashSet, + pending_uploads: Vec, graph: &'a Graph<'a>, cell_width_type: CellWidthType, @@ -52,6 +53,7 @@ impl<'a> GraphImageManager<'a> { GraphImageManager { prepared_image_map: FxHashMap::default(), image_ids: FxHashSet::default(), + pending_uploads: Vec::default(), graph, cell_width_type, graph_style, @@ -70,7 +72,11 @@ impl<'a> GraphImageManager<'a> { &self.image_ids } - pub fn load_prepared_image(&mut self, commit_hash: &CommitHash) { + pub fn drain_pending_uploads(&mut self) -> Vec { + std::mem::take(&mut self.pending_uploads) + } + + pub fn ensure_uploaded(&mut self, commit_hash: &CommitHash) { if self.prepared_image_map.contains_key(commit_hash) { return; } @@ -82,7 +88,11 @@ impl<'a> GraphImageManager<'a> { self.graph_style, commit_hash, ); - let image = graph_row_image.prepare(self.cell_width_type, self.image_protocol, image_id); + let mut image = + graph_row_image.prepare(self.cell_width_type, self.image_protocol, image_id); + if let Some(upload_data) = image.take_upload_data() { + self.pending_uploads.push(upload_data); + } self.prepared_image_map.insert(commit_hash.clone(), image); self.image_ids.insert(image_id); } diff --git a/src/protocol.rs b/src/protocol.rs index 3507bbf..1cab88f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -52,6 +52,7 @@ impl PreparedImageCell { pub struct PreparedImage { cells: Vec, cell_width: usize, + upload_data: Option, } impl PreparedImage { @@ -62,6 +63,10 @@ impl PreparedImage { pub fn cell_width(&self) -> usize { self.cell_width } + + pub fn take_upload_data(&mut self) -> Option { + self.upload_data.take() + } } impl ImageProtocol { @@ -86,7 +91,11 @@ impl ImageProtocol { skip: true, }); } - PreparedImage { cells, cell_width } + PreparedImage { + cells, + cell_width, + upload_data: None, + } } pub fn clear_line(&self, y: u16) { @@ -477,11 +486,11 @@ fn kitty_unicode_prepare(bytes: &[u8], cell_width: usize, image_id: u32) -> Prep }); } - if let Some(first) = cells.first_mut() { - first.symbol = format!("{upload_symbol}{}", first.symbol); + PreparedImage { + cells, + cell_width, + upload_data: Some(upload_symbol), } - - PreparedImage { cells, cell_width } } fn kitty_unicode_encode( diff --git a/src/view/detail.rs b/src/view/detail.rs index 0696cdf..5e23854 100644 --- a/src/view/detail.rs +++ b/src/view/detail.rs @@ -134,6 +134,18 @@ impl<'a> DetailView<'a> { CommitDetail::new(&self.commit, &self.changes, &self.refs, self.ctx.clone()); f.render_stateful_widget(commit_detail, detail_area, &mut self.commit_detail_state); } + + pub fn update_layout(&mut self, area: Rect) { + let detail_height = (area.height - 1).min(self.ctx.ui_config.detail.height); + let [list_area, _detail_area] = + Layout::vertical([Constraint::Min(0), Constraint::Length(detail_height)]).areas(area); + self.as_mut_list_state() + .update_height(list_area.height as usize); + } + + pub fn prepare_graph_uploads(&mut self) { + self.as_mut_list_state().ensure_visible_graph_uploaded(); + } } impl<'a> DetailView<'a> { @@ -149,6 +161,10 @@ impl<'a> DetailView<'a> { self.commit_list_state.as_ref().unwrap() } + pub fn drain_pending_graph_uploads(&mut self) -> Vec { + self.as_mut_list_state().drain_pending_graph_uploads() + } + pub fn select_older_commit(&mut self, repository: &Repository) { self.update_selected_commit(repository, |state| state.select_next()); } diff --git a/src/view/list.rs b/src/view/list.rs index bbd93cf..aad20f0 100644 --- a/src/view/list.rs +++ b/src/view/list.rs @@ -176,6 +176,14 @@ impl<'a> ListView<'a> { let commit_list = CommitList::new(self.ctx.clone()); f.render_stateful_widget(commit_list, area, self.as_mut_list_state()); } + + pub fn update_layout(&mut self, area: Rect) { + self.as_mut_list_state().update_height(area.height as usize); + } + + pub fn prepare_graph_uploads(&mut self) { + self.as_mut_list_state().ensure_visible_graph_uploaded(); + } } impl<'a> ListView<'a> { @@ -191,6 +199,10 @@ impl<'a> ListView<'a> { self.commit_list_state.as_ref().unwrap() } + pub fn drain_pending_graph_uploads(&mut self) -> Vec { + self.as_mut_list_state().drain_pending_graph_uploads() + } + fn update_search_query(&self) { if let SearchState::Searching { .. } = self.as_list_state().search_state() { let list_state = self.as_list_state(); diff --git a/src/view/refs.rs b/src/view/refs.rs index b65ea4c..690f972 100644 --- a/src/view/refs.rs +++ b/src/view/refs.rs @@ -110,6 +110,20 @@ impl<'a> RefsView<'a> { let ref_list = RefList::new(&self.refs, self.ctx.clone()); f.render_stateful_widget(ref_list, refs_area, &mut self.ref_list_state); } + + pub fn update_layout(&mut self, area: Rect) { + let graph_width = self.as_list_state().graph_area_cell_width() + 1; // graph area + marker + let refs_width = + (area.width.saturating_sub(graph_width)).min(self.ctx.ui_config.refs.width); + let [list_area, _refs_area] = + Layout::horizontal([Constraint::Min(0), Constraint::Length(refs_width)]).areas(area); + self.as_mut_list_state() + .update_height(list_area.height as usize); + } + + pub fn prepare_graph_uploads(&mut self) { + self.as_mut_list_state().ensure_visible_graph_uploaded(); + } } impl<'a> RefsView<'a> { @@ -125,6 +139,10 @@ impl<'a> RefsView<'a> { self.commit_list_state.as_ref().unwrap() } + pub fn drain_pending_graph_uploads(&mut self) -> Vec { + self.as_mut_list_state().drain_pending_graph_uploads() + } + fn update_commit_list_selected(&mut self) { if let Some(selected) = self.ref_list_state.selected_ref_name() { self.as_mut_list_state().select_ref(&selected) diff --git a/src/view/user_command.rs b/src/view/user_command.rs index 8d40218..9d75870 100644 --- a/src/view/user_command.rs +++ b/src/view/user_command.rs @@ -148,6 +148,19 @@ impl<'a> UserCommandView<'a> { &mut self.commit_user_command_state, ); } + + pub fn update_layout(&mut self, area: Rect) { + let user_command_height = (area.height - 1).min(self.ctx.ui_config.user_command.height); + let [list_area, _user_command_area] = + Layout::vertical([Constraint::Min(0), Constraint::Length(user_command_height)]) + .areas(area); + self.as_mut_list_state() + .update_height(list_area.height as usize); + } + + pub fn prepare_graph_uploads(&mut self) { + self.as_mut_list_state().ensure_visible_graph_uploaded(); + } } impl<'a> UserCommandView<'a> { @@ -163,6 +176,10 @@ impl<'a> UserCommandView<'a> { self.commit_list_state.as_ref().unwrap() } + pub fn drain_pending_graph_uploads(&mut self) -> Vec { + self.as_mut_list_state().drain_pending_graph_uploads() + } + pub fn select_older_commit( &mut self, repository: &Repository, diff --git a/src/view/views.rs b/src/view/views.rs index a1e2b59..c7e7fb5 100644 --- a/src/view/views.rs +++ b/src/view/views.rs @@ -47,6 +47,39 @@ impl<'a> View<'a> { } } + pub fn update_layout(&mut self, area: Rect) { + match self { + View::Default => {} + View::List(view) => view.update_layout(area), + View::Detail(view) => view.update_layout(area), + View::UserCommand(view) => view.update_layout(area), + View::Refs(view) => view.update_layout(area), + View::Help(_) => {} + } + } + + pub fn prepare_graph_uploads(&mut self) { + match self { + View::Default => {} + View::List(view) => view.prepare_graph_uploads(), + View::Detail(view) => view.prepare_graph_uploads(), + View::UserCommand(view) => view.prepare_graph_uploads(), + View::Refs(view) => view.prepare_graph_uploads(), + View::Help(_) => {} + } + } + + pub fn drain_pending_graph_uploads(&mut self) -> Vec { + match self { + View::Default => Vec::new(), + View::List(view) => view.drain_pending_graph_uploads(), + View::Detail(view) => view.drain_pending_graph_uploads(), + View::UserCommand(view) => view.drain_pending_graph_uploads(), + View::Refs(view) => view.drain_pending_graph_uploads(), + View::Help(_) => Vec::new(), + } + } + pub fn of_list( commit_list_state: CommitListState<'a>, ctx: Rc, diff --git a/src/widget/commit_list.rs b/src/widget/commit_list.rs index 84418d9..b195e01 100644 --- a/src/widget/commit_list.rs +++ b/src/widget/commit_list.rs @@ -232,6 +232,36 @@ impl<'a> CommitListState<'a> { self.graph_cell_width + 1 // right pad } + pub fn update_height(&mut self, height: usize) { + self.height = height; + + if self.total > self.height && self.total - self.height < self.offset { + let diff = self.offset - (self.total - self.height); + self.selected += diff; + self.offset -= diff; + } + if self.selected >= self.height { + let diff = self.selected - self.height + 1; + self.selected -= diff; + self.offset += diff; + } + } + + pub fn ensure_visible_graph_uploaded(&mut self) { + self.commits + .iter() + .skip(self.offset) + .take(self.height) + .for_each(|commit_info| { + self.graph_image_manager + .ensure_uploaded(&commit_info.commit.commit_hash); + }); + } + + pub fn drain_pending_graph_uploads(&mut self) -> Vec { + self.graph_image_manager.drain_pending_uploads() + } + pub fn select_next(&mut self) { if self.selected < (self.total - 1).min(self.height - 1) { self.selected += 1; @@ -711,29 +741,7 @@ impl<'a> StatefulWidget for CommitList<'a> { impl CommitList<'_> { fn update_state(&self, area: Rect, state: &mut CommitListState) { - state.height = area.height as usize; - - if state.total > state.height && state.total - state.height < state.offset { - let diff = state.offset - (state.total - state.height); - state.selected += diff; - state.offset -= diff; - } - if state.selected >= state.height { - let diff = state.selected - state.height + 1; - state.selected -= diff; - state.offset += diff; - } - - state - .commits - .iter() - .skip(state.offset) - .take(state.height) - .for_each(|commit_info| { - state - .graph_image_manager - .load_prepared_image(&commit_info.commit.commit_hash); - }); + state.update_height(area.height as usize); } fn render_graph(&self, buf: &mut Buffer, area: Rect, state: &CommitListState) { From f8a04359f0ceb809725df0ac4484e4994a7cf5c7 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 12:42:26 +0900 Subject: [PATCH 07/11] Clean up kitty unicode images on refresh --- src/app.rs | 7 +++++++ src/protocol.rs | 20 ++++++++++++++++++++ src/view/detail.rs | 4 ++++ src/view/help.rs | 4 ++++ src/view/list.rs | 4 ++++ src/view/refs.rs | 4 ++++ src/view/user_command.rs | 4 ++++ src/view/views.rs | 11 +++++++++++ src/widget/commit_list.rs | 11 +++++++++++ 9 files changed, 69 insertions(+) diff --git a/src/app.rs b/src/app.rs index 3cd2568..9260559 100644 --- a/src/app.rs +++ b/src/app.rs @@ -219,6 +219,7 @@ impl App<'_> { let _ = (w, h); } AppEvent::Quit => { + self.cleanup_graph_images()?; return Ok(Ret::Quit); } AppEvent::OpenDetail => { @@ -264,6 +265,7 @@ impl App<'_> { self.copy_to_clipboard(name, value); } AppEvent::Refresh(context) => { + self.cleanup_graph_images()?; let request = RefreshRequest { context }; return Ok(Ret::Refresh(request)); } @@ -312,6 +314,11 @@ impl App<'_> { stdout.flush() } + fn cleanup_graph_images(&self) -> Result<(), std::io::Error> { + let image_ids = self.view.graph_image_ids_sorted(); + self.ctx.image_protocol.delete_images(&image_ids) + } + fn render(&mut self, f: &mut Frame) { let base = Block::default() .fg(self.ctx.color_theme.fg) diff --git a/src/protocol.rs b/src/protocol.rs index 1cab88f..0c28f8e 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,4 +1,5 @@ use std::env; +use std::io::{self, Write}; use base64::Engine; use ratatui::style::Color; @@ -113,6 +114,13 @@ impl ImageProtocol { ImageProtocol::KittyUnicode => {} } } + + pub fn delete_images(&self, image_ids: &[u32]) -> Result<(), std::io::Error> { + match self { + ImageProtocol::Iterm2 | ImageProtocol::Kitty => Ok(()), + ImageProtocol::KittyUnicode => kitty_unicode_delete_images(image_ids), + } + } } const KITTY_PLACEHOLDER: char = '\u{10EEEE}'; @@ -538,3 +546,15 @@ fn kitty_clear_line(y: u16) { fn kitty_clear() { print!("\x1b_Ga=d,d=A;\x1b\\"); } + +fn kitty_unicode_delete_images(image_ids: &[u32]) -> Result<(), io::Error> { + if image_ids.is_empty() { + return Ok(()); + } + + let mut stdout = io::stdout().lock(); + for image_id in image_ids { + write!(stdout, "\x1b_Ga=d,d=I,i={image_id}\x1b\\")?; + } + stdout.flush() +} diff --git a/src/view/detail.rs b/src/view/detail.rs index 5e23854..b1323b7 100644 --- a/src/view/detail.rs +++ b/src/view/detail.rs @@ -165,6 +165,10 @@ impl<'a> DetailView<'a> { self.as_mut_list_state().drain_pending_graph_uploads() } + pub fn graph_image_ids_sorted(&self) -> Vec { + self.as_list_state().graph_image_ids_sorted() + } + pub fn select_older_commit(&mut self, repository: &Repository) { self.update_selected_commit(repository, |state| state.select_next()); } diff --git a/src/view/help.rs b/src/view/help.rs index 83e34bc..588810d 100644 --- a/src/view/help.rs +++ b/src/view/help.rs @@ -150,6 +150,10 @@ impl<'a> HelpView<'a> { std::mem::take(&mut self.before) } + pub fn graph_image_ids_sorted(&self) -> Vec { + self.before.graph_image_ids_sorted() + } + fn scroll_down(&mut self) { self.offset = self.offset.saturating_add(1); } diff --git a/src/view/list.rs b/src/view/list.rs index aad20f0..62fa3bd 100644 --- a/src/view/list.rs +++ b/src/view/list.rs @@ -203,6 +203,10 @@ impl<'a> ListView<'a> { self.as_mut_list_state().drain_pending_graph_uploads() } + pub fn graph_image_ids_sorted(&self) -> Vec { + self.as_list_state().graph_image_ids_sorted() + } + fn update_search_query(&self) { if let SearchState::Searching { .. } = self.as_list_state().search_state() { let list_state = self.as_list_state(); diff --git a/src/view/refs.rs b/src/view/refs.rs index 690f972..3619b2e 100644 --- a/src/view/refs.rs +++ b/src/view/refs.rs @@ -143,6 +143,10 @@ impl<'a> RefsView<'a> { self.as_mut_list_state().drain_pending_graph_uploads() } + pub fn graph_image_ids_sorted(&self) -> Vec { + self.as_list_state().graph_image_ids_sorted() + } + fn update_commit_list_selected(&mut self) { if let Some(selected) = self.ref_list_state.selected_ref_name() { self.as_mut_list_state().select_ref(&selected) diff --git a/src/view/user_command.rs b/src/view/user_command.rs index 9d75870..a1152d8 100644 --- a/src/view/user_command.rs +++ b/src/view/user_command.rs @@ -180,6 +180,10 @@ impl<'a> UserCommandView<'a> { self.as_mut_list_state().drain_pending_graph_uploads() } + pub fn graph_image_ids_sorted(&self) -> Vec { + self.as_list_state().graph_image_ids_sorted() + } + pub fn select_older_commit( &mut self, repository: &Repository, diff --git a/src/view/views.rs b/src/view/views.rs index c7e7fb5..c8e4220 100644 --- a/src/view/views.rs +++ b/src/view/views.rs @@ -80,6 +80,17 @@ impl<'a> View<'a> { } } + pub fn graph_image_ids_sorted(&self) -> Vec { + match self { + View::Default => Vec::new(), + View::List(view) => view.graph_image_ids_sorted(), + View::Detail(view) => view.graph_image_ids_sorted(), + View::UserCommand(view) => view.graph_image_ids_sorted(), + View::Refs(view) => view.graph_image_ids_sorted(), + View::Help(view) => view.graph_image_ids_sorted(), + } + } + pub fn of_list( commit_list_state: CommitListState<'a>, ctx: Rc, diff --git a/src/widget/commit_list.rs b/src/widget/commit_list.rs index b195e01..d9fb7b2 100644 --- a/src/widget/commit_list.rs +++ b/src/widget/commit_list.rs @@ -262,6 +262,17 @@ impl<'a> CommitListState<'a> { self.graph_image_manager.drain_pending_uploads() } + pub fn graph_image_ids_sorted(&self) -> Vec { + let mut image_ids: Vec = self + .graph_image_manager + .image_ids() + .iter() + .copied() + .collect(); + image_ids.sort_unstable(); + image_ids + } + pub fn select_next(&mut self) { if self.selected < (self.total - 1).min(self.height - 1) { self.selected += 1; From 466ec9459d4cff36d3950459a66bc3ce07b23ee3 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 13:04:26 +0900 Subject: [PATCH 08/11] Share render layout splits --- src/app.rs | 10 ++++++---- src/view/detail.rs | 13 +++++++------ src/view/refs.rs | 20 +++++++++----------- src/view/user_command.rs | 15 +++++++-------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9260559..44f1f3c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -293,8 +293,7 @@ impl App<'_> { fn prepare_render(&mut self, terminal: &mut DefaultTerminal) -> Result<(), std::io::Error> { let area: Rect = terminal.size()?.into(); - let [view_area, _status_line_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(area); + let [view_area, _] = split_app_areas(area); self.update_state(view_area); self.view.update_layout(view_area); self.view.prepare_graph_uploads(); @@ -325,8 +324,7 @@ impl App<'_> { .bg(self.ctx.color_theme.bg); f.render_widget(base, f.area()); - let [view_area, status_line_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(f.area()); + let [view_area, status_line_area] = split_app_areas(f.area()); self.update_state(view_area); @@ -398,6 +396,10 @@ impl App<'_> { } } +fn split_app_areas(area: Rect) -> [Rect; 2] { + Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(area) +} + impl App<'_> { fn update_state(&mut self, view_area: Rect) { self.app_status.view_area = view_area; diff --git a/src/view/detail.rs b/src/view/detail.rs index b1323b7..c8c0d97 100644 --- a/src/view/detail.rs +++ b/src/view/detail.rs @@ -123,9 +123,7 @@ impl<'a> DetailView<'a> { } pub fn render(&mut self, f: &mut Frame, area: Rect) { - let detail_height = (area.height - 1).min(self.ctx.ui_config.detail.height); - let [list_area, detail_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(detail_height)]).areas(area); + let [list_area, detail_area] = self.split_areas(area); let commit_list = CommitList::new(self.ctx.clone()); f.render_stateful_widget(commit_list, list_area, self.as_mut_list_state()); @@ -136,9 +134,7 @@ impl<'a> DetailView<'a> { } pub fn update_layout(&mut self, area: Rect) { - let detail_height = (area.height - 1).min(self.ctx.ui_config.detail.height); - let [list_area, _detail_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(detail_height)]).areas(area); + let [list_area, _] = self.split_areas(area); self.as_mut_list_state() .update_height(list_area.height as usize); } @@ -169,6 +165,11 @@ impl<'a> DetailView<'a> { self.as_list_state().graph_image_ids_sorted() } + fn split_areas(&self, area: Rect) -> [Rect; 2] { + let detail_height = (area.height - 1).min(self.ctx.ui_config.detail.height); + Layout::vertical([Constraint::Min(0), Constraint::Length(detail_height)]).areas(area) + } + pub fn select_older_commit(&mut self, repository: &Repository) { self.update_selected_commit(repository, |state| state.select_next()); } diff --git a/src/view/refs.rs b/src/view/refs.rs index 3619b2e..f1d56a0 100644 --- a/src/view/refs.rs +++ b/src/view/refs.rs @@ -97,12 +97,7 @@ impl<'a> RefsView<'a> { } pub fn render(&mut self, f: &mut Frame, area: Rect) { - let graph_width = self.as_list_state().graph_area_cell_width() + 1; // graph area + marker - let refs_width = - (area.width.saturating_sub(graph_width)).min(self.ctx.ui_config.refs.width); - - let [list_area, refs_area] = - Layout::horizontal([Constraint::Min(0), Constraint::Length(refs_width)]).areas(area); + let [list_area, refs_area] = self.split_areas(area); let commit_list = CommitList::new(self.ctx.clone()); f.render_stateful_widget(commit_list, list_area, self.as_mut_list_state()); @@ -112,11 +107,7 @@ impl<'a> RefsView<'a> { } pub fn update_layout(&mut self, area: Rect) { - let graph_width = self.as_list_state().graph_area_cell_width() + 1; // graph area + marker - let refs_width = - (area.width.saturating_sub(graph_width)).min(self.ctx.ui_config.refs.width); - let [list_area, _refs_area] = - Layout::horizontal([Constraint::Min(0), Constraint::Length(refs_width)]).areas(area); + let [list_area, _] = self.split_areas(area); self.as_mut_list_state() .update_height(list_area.height as usize); } @@ -147,6 +138,13 @@ impl<'a> RefsView<'a> { self.as_list_state().graph_image_ids_sorted() } + fn split_areas(&self, area: Rect) -> [Rect; 2] { + let graph_width = self.as_list_state().graph_area_cell_width() + 1; // graph area + marker + let refs_width = + (area.width.saturating_sub(graph_width)).min(self.ctx.ui_config.refs.width); + Layout::horizontal([Constraint::Min(0), Constraint::Length(refs_width)]).areas(area) + } + fn update_commit_list_selected(&mut self) { if let Some(selected) = self.ref_list_state.selected_ref_name() { self.as_mut_list_state().select_ref(&selected) diff --git a/src/view/user_command.rs b/src/view/user_command.rs index a1152d8..4258be4 100644 --- a/src/view/user_command.rs +++ b/src/view/user_command.rs @@ -132,10 +132,7 @@ impl<'a> UserCommandView<'a> { } pub fn render(&mut self, f: &mut Frame, area: Rect) { - let user_command_height = (area.height - 1).min(self.ctx.ui_config.user_command.height); - let [list_area, user_command_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(user_command_height)]) - .areas(area); + let [list_area, user_command_area] = self.split_areas(area); let commit_list = CommitList::new(self.ctx.clone()); f.render_stateful_widget(commit_list, list_area, self.as_mut_list_state()); @@ -150,10 +147,7 @@ impl<'a> UserCommandView<'a> { } pub fn update_layout(&mut self, area: Rect) { - let user_command_height = (area.height - 1).min(self.ctx.ui_config.user_command.height); - let [list_area, _user_command_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(user_command_height)]) - .areas(area); + let [list_area, _] = self.split_areas(area); self.as_mut_list_state() .update_height(list_area.height as usize); } @@ -184,6 +178,11 @@ impl<'a> UserCommandView<'a> { self.as_list_state().graph_image_ids_sorted() } + fn split_areas(&self, area: Rect) -> [Rect; 2] { + let user_command_height = (area.height - 1).min(self.ctx.ui_config.user_command.height); + Layout::vertical([Constraint::Min(0), Constraint::Length(user_command_height)]).areas(area) + } + pub fn select_older_commit( &mut self, repository: &Repository, From 6c195528adc1035edebc08ea657248fc0049d7ea Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 13:25:43 +0900 Subject: [PATCH 09/11] Update documents --- README.md | 1 + docs/src/getting-started/compatibility.md | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ad2bf17..eb66baf 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ These image protocols are supported: - [Inline Images Protocol (iTerm2)](https://iterm2.com/documentation-images.html) - [Terminal graphics protocol (kitty)](https://sw.kovidgoyal.net/kitty/graphics-protocol/) + - Supports both the existing graphics protocol mode and [the Unicode placeholder](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) mode. For more information, see [Compatibility](https://lusingander.github.io/serie/getting-started/compatibility.html). diff --git a/docs/src/getting-started/compatibility.md b/docs/src/getting-started/compatibility.md index 54352cb..7a05dc7 100644 --- a/docs/src/getting-started/compatibility.md +++ b/docs/src/getting-started/compatibility.md @@ -6,6 +6,7 @@ These image protocols are supported: - [Inline Images Protocol (iTerm2)](https://iterm2.com/documentation-images.html) - [Terminal graphics protocol (kitty)](https://sw.kovidgoyal.net/kitty/graphics-protocol/) + - Supports both the existing graphics protocol mode and [the Unicode placeholder](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) mode. The terminals on which each has been confirmed to work are listed below. @@ -22,10 +23,12 @@ The terminals on which each has been confirmed to work are listed below. ### Terminal graphics protocol -| Terminal emulator | Note | -| ----------------------------------------- | ---- | -| [kitty](https://sw.kovidgoyal.net/kitty/) | | -| [Ghostty](https://ghostty.org) | | +| Terminal emulator | Unicode placeholder | Note | +| ----------------------------------------- | ------------------- | ---- | +| [kitty](https://sw.kovidgoyal.net/kitty/) | ○ | | +| [Ghostty](https://ghostty.org) | ○ | | + +Rendering using Unicode Placeholder is available by explicitly specifying `kitty-unicode` as `protocol` option or config. ## Unsupported environments From 916a1a819cd3eebf2a57ae28810b7a232d372a08 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 15:55:35 +0900 Subject: [PATCH 10/11] Merge imports --- src/protocol.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/protocol.rs b/src/protocol.rs index 0c28f8e..7ce4ce4 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,9 +1,10 @@ -use std::env; -use std::io::{self, Write}; +use std::{ + env, + io::{self, Write}, +}; use base64::Engine; -use ratatui::style::Color; -use ratatui::style::Style; +use ratatui::style::{Color, Style}; // By default assume the Iterm2 is the best protocol to use for all terminals *unless* an env // variable is set that suggests the terminal is probably Kitty. From 30b9d740cf09f0b25e21b80b9f10e73b4f7f2337 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sat, 25 Apr 2026 16:01:04 +0900 Subject: [PATCH 11/11] Use kebab-case for protocol option --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f6fa320..ac9a8bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,13 +49,11 @@ struct Args { } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Deserialize)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "kebab-case")] pub enum ImageProtocolType { Auto, Iterm, Kitty, - #[serde(rename = "kitty-unicode")] - #[value(name = "kitty-unicode")] KittyUnicode, }