Skip to content

Commit 60ff95c

Browse files
authored
Support kitty unicode placeholder (#153)
* Add kitty-unicode protocol option * Split graph image render data * Render graph images as cell sequences * Implement kitty unicode placeholders * Inline kitty row/column diacritics * Separate kitty uploads from redraws * Clean up kitty unicode images on refresh * Share render layout splits * Update documents * Merge imports * Use kebab-case for protocol option
1 parent 0d80e70 commit 60ff95c

17 files changed

Lines changed: 776 additions & 72 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Usage: serie [OPTIONS]
7070
7171
Options:
7272
-n, --max-count <NUMBER> Maximum number of commits to render
73-
-p, --protocol <TYPE> Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty]
73+
-p, --protocol <TYPE> Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty, kitty-unicode]
7474
-o, --order <TYPE> Commit ordering algorithm [default: chrono] [possible values: chrono, topo]
7575
-g, --graph-width <TYPE> Commit graph image cell width [default: auto] [possible values: auto, double, single]
7676
-s, --graph-style <TYPE> Commit graph image edge style [default: rounded] [possible values: rounded, angular]
@@ -116,6 +116,7 @@ These image protocols are supported:
116116

117117
- [Inline Images Protocol (iTerm2)](https://iterm2.com/documentation-images.html)
118118
- [Terminal graphics protocol (kitty)](https://sw.kovidgoyal.net/kitty/graphics-protocol/)
119+
- Supports both the existing graphics protocol mode and [the Unicode placeholder](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) mode.
119120

120121
For more information, see [Compatibility](https://lusingander.github.io/serie/getting-started/compatibility.html).
121122

config.schema.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"enum": [
1919
"auto",
2020
"iterm",
21-
"kitty"
21+
"kitty",
22+
"kitty-unicode"
2223
],
2324
"default": "auto"
2425
},
@@ -682,4 +683,4 @@
682683
]
683684
}
684685
}
685-
}
686+
}

docs/src/configurations/config-file-format.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ The protocol type for rendering images of commit graphs.
114114
- `auto`
115115
- `iterm`
116116
- `kitty`
117+
- `kitty-unicode`
117118

118119
The value specified in the command line argument takes precedence.
119120

docs/src/getting-started/command-line-options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ It behaves similarly to the `--max-count` option of `git log`.
1111

1212
A protocol type for rendering images of commit graphs.
1313

14-
_Possible values:_ `auto`, `iterm`, `kitty`
14+
_Possible values:_ `auto`, `iterm`, `kitty`, `kitty-unicode`
1515

1616
By default `auto` will guess the best supported protocol for the current terminal (if listed in [Supported terminal emulators](./compatibility.md#supported-terminal-emulators)).
1717

docs/src/getting-started/compatibility.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ These image protocols are supported:
66

77
- [Inline Images Protocol (iTerm2)](https://iterm2.com/documentation-images.html)
88
- [Terminal graphics protocol (kitty)](https://sw.kovidgoyal.net/kitty/graphics-protocol/)
9+
- Supports both the existing graphics protocol mode and [the Unicode placeholder](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) mode.
910

1011
The terminals on which each has been confirmed to work are listed below.
1112

@@ -22,10 +23,12 @@ The terminals on which each has been confirmed to work are listed below.
2223

2324
### Terminal graphics protocol
2425

25-
| Terminal emulator | Note |
26-
| ----------------------------------------- | ---- |
27-
| [kitty](https://sw.kovidgoyal.net/kitty/) | |
28-
| [Ghostty](https://ghostty.org) | |
26+
| Terminal emulator | Unicode placeholder | Note |
27+
| ----------------------------------------- | ------------------- | ---- |
28+
| [kitty](https://sw.kovidgoyal.net/kitty/) || |
29+
| [Ghostty](https://ghostty.org) || |
30+
31+
Rendering using Unicode Placeholder is available by explicitly specifying `kitty-unicode` as `protocol` option or config.
2932

3033
## Unsupported environments
3134

src/app.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::rc::Rc;
1+
use std::{
2+
io::{self, Write},
3+
rc::Rc,
4+
};
25

36
use ratatui::{
47
crossterm::event::{KeyCode, KeyEvent},
@@ -149,6 +152,8 @@ impl App<'_> {
149152
terminal.clear()?;
150153

151154
loop {
155+
self.prepare_render(terminal)?;
156+
self.flush_pending_graph_uploads()?;
152157
terminal.draw(|f| self.render(f))?;
153158
match self.ec.recv() {
154159
AppEvent::Key(key) => {
@@ -214,6 +219,7 @@ impl App<'_> {
214219
let _ = (w, h);
215220
}
216221
AppEvent::Quit => {
222+
self.cleanup_graph_images()?;
217223
return Ok(Ret::Quit);
218224
}
219225
AppEvent::OpenDetail => {
@@ -259,6 +265,7 @@ impl App<'_> {
259265
self.copy_to_clipboard(name, value);
260266
}
261267
AppEvent::Refresh(context) => {
268+
self.cleanup_graph_images()?;
262269
let request = RefreshRequest { context };
263270
return Ok(Ret::Refresh(request));
264271
}
@@ -284,14 +291,40 @@ impl App<'_> {
284291
}
285292
}
286293

294+
fn prepare_render(&mut self, terminal: &mut DefaultTerminal) -> Result<(), std::io::Error> {
295+
let area: Rect = terminal.size()?.into();
296+
let [view_area, _] = split_app_areas(area);
297+
self.update_state(view_area);
298+
self.view.update_layout(view_area);
299+
self.view.prepare_graph_uploads();
300+
Ok(())
301+
}
302+
303+
fn flush_pending_graph_uploads(&mut self) -> Result<(), std::io::Error> {
304+
let uploads = self.view.drain_pending_graph_uploads();
305+
if uploads.is_empty() {
306+
return Ok(());
307+
}
308+
309+
let mut stdout = io::stdout().lock();
310+
for upload in uploads {
311+
stdout.write_all(upload.as_bytes())?;
312+
}
313+
stdout.flush()
314+
}
315+
316+
fn cleanup_graph_images(&self) -> Result<(), std::io::Error> {
317+
let image_ids = self.view.graph_image_ids_sorted();
318+
self.ctx.image_protocol.delete_images(&image_ids)
319+
}
320+
287321
fn render(&mut self, f: &mut Frame) {
288322
let base = Block::default()
289323
.fg(self.ctx.color_theme.fg)
290324
.bg(self.ctx.color_theme.bg);
291325
f.render_widget(base, f.area());
292326

293-
let [view_area, status_line_area] =
294-
Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(f.area());
327+
let [view_area, status_line_area] = split_app_areas(f.area());
295328

296329
self.update_state(view_area);
297330

@@ -363,6 +396,10 @@ impl App<'_> {
363396
}
364397
}
365398

399+
fn split_app_areas(area: Rect) -> [Rect; 2] {
400+
Layout::vertical([Constraint::Min(0), Constraint::Length(2)]).areas(area)
401+
}
402+
366403
impl App<'_> {
367404
fn update_state(&mut self, view_area: Rect) {
368405
self.app_status.view_area = view_area;

src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ mod tests {
503503
fn test_config_complete_toml() {
504504
let toml = r##"
505505
[core.option]
506-
protocol = "kitty"
506+
protocol = "kitty-unicode"
507507
order = "topo"
508508
graph_width = "single"
509509
graph_style = "angular"
@@ -543,7 +543,7 @@ mod tests {
543543
let expected = Config {
544544
core: CoreConfig {
545545
option: CoreOptionConfig {
546-
protocol: Some(ImageProtocolType::Kitty),
546+
protocol: Some(ImageProtocolType::KittyUnicode),
547547
order: Some(CommitOrderType::Topo),
548548
graph_width: Some(GraphWidthType::Single),
549549
graph_style: Some(GraphStyle::Angular),

src/graph/image.rs

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::{
22
fmt::{self, Debug, Formatter},
3+
hash::{Hash, Hasher},
34
io::Cursor,
5+
process,
6+
time::{SystemTime, UNIX_EPOCH},
47
};
58

69
use rustc_hash::{FxHashMap, FxHashSet};
@@ -12,7 +15,7 @@ use crate::{
1215
geometry::{bounding_box_u32, Point},
1316
Edge, EdgeType, Graph,
1417
},
15-
protocol::ImageProtocol,
18+
protocol::{ImageProtocol, PreparedImage},
1619
};
1720

1821
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -23,14 +26,17 @@ pub enum GraphStyle {
2326

2427
#[derive(Debug)]
2528
pub struct GraphImageManager<'a> {
26-
encoded_image_map: FxHashMap<CommitHash, String>,
29+
prepared_image_map: FxHashMap<CommitHash, PreparedImage>,
30+
image_ids: FxHashSet<u32>,
31+
pending_uploads: Vec<String>,
2732

2833
graph: &'a Graph<'a>,
2934
cell_width_type: CellWidthType,
3035
graph_style: GraphStyle,
3136
image_params: ImageParams,
3237
drawing_pixels: DrawingPixels,
3338
image_protocol: ImageProtocol,
39+
session_nonce: u32,
3440
}
3541

3642
impl<'a> GraphImageManager<'a> {
@@ -45,33 +51,50 @@ impl<'a> GraphImageManager<'a> {
4551
let drawing_pixels = DrawingPixels::new(&image_params);
4652

4753
GraphImageManager {
48-
encoded_image_map: FxHashMap::default(),
54+
prepared_image_map: FxHashMap::default(),
55+
image_ids: FxHashSet::default(),
56+
pending_uploads: Vec::default(),
4957
graph,
5058
cell_width_type,
5159
graph_style,
5260
image_params,
5361
drawing_pixels,
5462
image_protocol,
63+
session_nonce: create_session_nonce(),
5564
}
5665
}
5766

58-
pub fn encoded_image(&self, commit_hash: &CommitHash) -> &str {
59-
self.encoded_image_map.get(commit_hash).unwrap()
67+
pub fn prepared_image(&self, commit_hash: &CommitHash) -> &PreparedImage {
68+
self.prepared_image_map.get(commit_hash).unwrap()
6069
}
6170

62-
pub fn load_encoded_image(&mut self, commit_hash: &CommitHash) {
63-
if self.encoded_image_map.contains_key(commit_hash) {
71+
pub fn image_ids(&self) -> &FxHashSet<u32> {
72+
&self.image_ids
73+
}
74+
75+
pub fn drain_pending_uploads(&mut self) -> Vec<String> {
76+
std::mem::take(&mut self.pending_uploads)
77+
}
78+
79+
pub fn ensure_uploaded(&mut self, commit_hash: &CommitHash) {
80+
if self.prepared_image_map.contains_key(commit_hash) {
6481
return;
6582
}
83+
let image_id = graph_image_id(self.session_nonce, commit_hash);
6684
let graph_row_image = build_single_graph_row_image(
6785
self.graph,
6886
&self.image_params,
6987
&self.drawing_pixels,
7088
self.graph_style,
7189
commit_hash,
7290
);
73-
let image = graph_row_image.encode(self.cell_width_type, self.image_protocol);
74-
self.encoded_image_map.insert(commit_hash.clone(), image);
91+
let mut image =
92+
graph_row_image.prepare(self.cell_width_type, self.image_protocol, image_id);
93+
if let Some(upload_data) = image.take_upload_data() {
94+
self.pending_uploads.push(upload_data);
95+
}
96+
self.prepared_image_map.insert(commit_hash.clone(), image);
97+
self.image_ids.insert(image_id);
7598
}
7699
}
77100

@@ -97,15 +120,38 @@ impl Debug for GraphRowImage {
97120
}
98121

99122
impl GraphRowImage {
100-
fn encode(&self, cell_width_type: CellWidthType, image_protocol: ImageProtocol) -> String {
123+
fn prepare(
124+
&self,
125+
cell_width_type: CellWidthType,
126+
image_protocol: ImageProtocol,
127+
image_id: u32,
128+
) -> PreparedImage {
101129
let image_cell_width = match cell_width_type {
102130
CellWidthType::Double => self.cell_count * 2,
103131
CellWidthType::Single => self.cell_count,
104132
};
105-
image_protocol.encode(&self.bytes, image_cell_width)
133+
image_protocol.prepare_image(&self.bytes, image_cell_width, image_id)
106134
}
107135
}
108136

137+
fn create_session_nonce() -> u32 {
138+
let mut hasher = rustc_hash::FxHasher::default();
139+
process::id().hash(&mut hasher);
140+
SystemTime::now()
141+
.duration_since(UNIX_EPOCH)
142+
.unwrap_or_default()
143+
.as_nanos()
144+
.hash(&mut hasher);
145+
hasher.finish() as u32
146+
}
147+
148+
fn graph_image_id(session_nonce: u32, commit_hash: &CommitHash) -> u32 {
149+
let mut hasher = rustc_hash::FxHasher::default();
150+
session_nonce.hash(&mut hasher);
151+
commit_hash.hash(&mut hasher);
152+
hasher.finish() as u32
153+
}
154+
109155
#[derive(Debug)]
110156
pub struct ImageParams {
111157
width: u16,

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ struct Args {
4949
}
5050

5151
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Deserialize)]
52-
#[serde(rename_all = "lowercase")]
52+
#[serde(rename_all = "kebab-case")]
5353
pub enum ImageProtocolType {
5454
Auto,
5555
Iterm,
5656
Kitty,
57+
KittyUnicode,
5758
}
5859

5960
impl From<Option<ImageProtocolType>> for protocol::ImageProtocol {
@@ -62,6 +63,7 @@ impl From<Option<ImageProtocolType>> for protocol::ImageProtocol {
6263
Some(ImageProtocolType::Auto) => protocol::auto_detect(),
6364
Some(ImageProtocolType::Iterm) => protocol::ImageProtocol::Iterm2,
6465
Some(ImageProtocolType::Kitty) => protocol::ImageProtocol::Kitty,
66+
Some(ImageProtocolType::KittyUnicode) => protocol::ImageProtocol::KittyUnicode,
6567
None => protocol::auto_detect(),
6668
}
6769
}

0 commit comments

Comments
 (0)