Skip to content
Merged
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Usage: serie [OPTIONS]

Options:
-n, --max-count <NUMBER> Maximum number of commits to render
-p, --protocol <TYPE> Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty]
-p, --protocol <TYPE> Image protocol to render graph [default: auto] [possible values: auto, iterm, kitty, kitty-unicode]
-o, --order <TYPE> Commit ordering algorithm [default: chrono] [possible values: chrono, topo]
-g, --graph-width <TYPE> Commit graph image cell width [default: auto] [possible values: auto, double, single]
-s, --graph-style <TYPE> Commit graph image edge style [default: rounded] [possible values: rounded, angular]
Expand Down Expand Up @@ -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).

Expand Down
5 changes: 3 additions & 2 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"enum": [
"auto",
"iterm",
"kitty"
"kitty",
"kitty-unicode"
],
"default": "auto"
},
Expand Down Expand Up @@ -682,4 +683,4 @@
]
}
}
}
}
1 change: 1 addition & 0 deletions docs/src/configurations/config-file-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/getting-started/command-line-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).

Expand Down
11 changes: 7 additions & 4 deletions docs/src/getting-started/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
43 changes: 40 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::rc::Rc;
use std::{
io::{self, Write},
rc::Rc,
};

use ratatui::{
crossterm::event::{KeyCode, KeyEvent},
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -214,6 +219,7 @@ impl App<'_> {
let _ = (w, h);
}
AppEvent::Quit => {
self.cleanup_graph_images()?;
return Ok(Ret::Quit);
}
AppEvent::OpenDetail => {
Expand Down Expand Up @@ -259,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));
}
Expand All @@ -284,14 +291,40 @@ impl App<'_> {
}
}

fn prepare_render(&mut self, terminal: &mut DefaultTerminal) -> Result<(), std::io::Error> {
let area: Rect = terminal.size()?.into();
let [view_area, _] = split_app_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 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)
.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);

Expand Down Expand Up @@ -363,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;
Expand Down
4 changes: 2 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Expand Down
68 changes: 57 additions & 11 deletions src/graph/image.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -12,7 +15,7 @@ use crate::{
geometry::{bounding_box_u32, Point},
Edge, EdgeType, Graph,
},
protocol::ImageProtocol,
protocol::{ImageProtocol, PreparedImage},
};

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

#[derive(Debug)]
pub struct GraphImageManager<'a> {
encoded_image_map: FxHashMap<CommitHash, String>,
prepared_image_map: FxHashMap<CommitHash, PreparedImage>,
image_ids: FxHashSet<u32>,
pending_uploads: Vec<String>,

graph: &'a Graph<'a>,
cell_width_type: CellWidthType,
graph_style: GraphStyle,
image_params: ImageParams,
drawing_pixels: DrawingPixels,
image_protocol: ImageProtocol,
session_nonce: u32,
}

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

GraphImageManager {
encoded_image_map: FxHashMap::default(),
prepared_image_map: FxHashMap::default(),
image_ids: FxHashSet::default(),
pending_uploads: Vec::default(),
graph,
cell_width_type,
graph_style,
image_params,
drawing_pixels,
image_protocol,
session_nonce: create_session_nonce(),
}
}

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 image_ids(&self) -> &FxHashSet<u32> {
&self.image_ids
}

pub fn drain_pending_uploads(&mut self) -> Vec<String> {
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;
}
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,
&self.drawing_pixels,
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 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);
}
}

Expand All @@ -97,15 +120,38 @@ 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,
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.encode(&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,
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ 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,
KittyUnicode,
}

impl From<Option<ImageProtocolType>> for protocol::ImageProtocol {
Expand All @@ -62,6 +63,7 @@ impl From<Option<ImageProtocolType>> 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::KittyUnicode,
None => protocol::auto_detect(),
}
}
Expand Down
Loading
Loading