use gix for git status
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# zterm
|
||||
|
||||
GPU-accelerated Wayland terminal emulator. Single Rust crate, edition 2024.
|
||||
|
||||
## Build & Run
|
||||
|
||||
```
|
||||
cargo run # dev build, Wayland only
|
||||
cargo build --release # release
|
||||
cargo test # tests
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| `webm` (default) | Enable WebM video via ffmpeg-next 8.x |
|
||||
| `render_timing` | Enable per-frame parse/render timing instrumentation |
|
||||
| `production` | `log/release_max_level_off` — zero logging overhead |
|
||||
|
||||
Disable webm: `--no-default-features --features render_timing`
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Single process**: owns PTY, terminal state, rendering. No multi-process IPC.
|
||||
- **Kitty-style shared parser** (`SharedParser` in `vt_parser.rs`): 1MB ring buffer. I/O thread writes directly into it; main thread parses in-place. Lock released during parsing so I/O continues.
|
||||
- **Split tree** (`SplitNode` in `main.rs`): binary tree of panes. Layout computed via recursive `layout()` pass.
|
||||
- **GPU rendering** (`renderer.rs`): wgpu + WGSL shaders. Instanced rendering with sprite atlas. Per-pane GPU buffers.
|
||||
- **Kitty graphics protocol** (`graphics.rs`): PNG, GIF, WebM inline images.
|
||||
|
||||
## Key files
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `src/main.rs` | `App` state machine, event loop, split tree, tab/pane management, keybindings |
|
||||
| `src/terminal.rs` | Terminal grid, scrollback buffer, cursor, colors, cell attributes, `Handler` impl |
|
||||
| `src/vt_parser.rs` | `SharedParser` (ring buffer + I/O), `Parser` (VT escape sequence parsing), `Handler` trait |
|
||||
| `src/renderer.rs` | wgpu renderer, glyph atlas, font loading, instanced rendering pipeline |
|
||||
| `src/graphics.rs` | Kitty graphics protocol parser (PNG/GIF/WebM) |
|
||||
| `src/config.rs` | JSON config from `~/.config/zterm/config.json`, keybinding resolver |
|
||||
| `src/pty.rs` | PTY master, fork/exec child shell, SIGWINCH resize |
|
||||
| `src/pipeline.rs` | wgpu pipeline builder |
|
||||
| `src/keyboard.rs` | Kitty keyboard protocol, key encoding |
|
||||
| `zterm.terminfo` | Custom terminfo — install with `tic -x -o ~/.terminfo zterm.terminfo` |
|
||||
| `src/bin/bench_process.rs` | Benchmark binary |
|
||||
|
||||
## Shaders
|
||||
|
||||
WGSL files: `src/glyph_shader.wgsl`, `src/image_shader.wgsl`, `src/quad_shader.wgsl`, `src/statusline_shader.wgsl`, `src/shader.wgsl` (edge glow).
|
||||
|
||||
## Config
|
||||
|
||||
`~/.config/zterm/config.json` — auto-created on first run. Fields: `font_family`, `font_size`, `tab_bar_position`, `background_opacity`, `scrollback_lines`, `inactive_pane_fade_ms`, `inactive_pane_dim`, `edge_glow_intensity`, `pass_keys_to_programs`, `keybindings`.
|
||||
|
||||
## Style
|
||||
|
||||
- `.rustfmt.toml`: `max_width = 80`
|
||||
- `.gitignore` excludes `Cargo.lock` and `/benches` (unusual for Rust)
|
||||
|
||||
## Testing
|
||||
|
||||
Tests live inline in source files: `vt_parser.rs`, `simd_utf8.rs`, `graphics.rs`, `vt_test_osc.rs`. Run with `cargo test`.
|
||||
|
||||
## Setup
|
||||
|
||||
- **Wayland compositor** required (X11 not supported)
|
||||
- **FFmpeg 5.x–8.x** system libraries (for `webm` feature)
|
||||
- **terminfo**: install `zterm.terminfo` so shells know the terminal type
|
||||
- **Nerd Font** recommended for statusline icons (folder, branch, etc.)
|
||||
+6
-1
@@ -75,13 +75,18 @@ rustc-hash = "2"
|
||||
# Base64 decoding for OSC statusline
|
||||
base64 = "0.22"
|
||||
|
||||
# Pure-Rust git implementation (replaces git subprocess calls)
|
||||
gix = { version = "0.84", features = ["max-performance", "parallel", "status", "blob-diff"] }
|
||||
gix-diff = "0.64"
|
||||
gix-status = "0.31"
|
||||
|
||||
# Image processing (Kitty graphics protocol)
|
||||
image = { version = "0.25", default-features = false, features = ["png", "gif"] }
|
||||
flate2 = "1"
|
||||
|
||||
# Video decoding for WebM support (video only, no audio)
|
||||
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
||||
ffmpeg-next = { version = "8.0", optional = true }
|
||||
ffmpeg-next = { version = "8.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["webm"]
|
||||
|
||||
+153
-106
@@ -983,128 +983,175 @@ struct GitStatus {
|
||||
stash_count: usize,
|
||||
}
|
||||
|
||||
/// Get git status for a directory.
|
||||
/// Returns None if not in a git repository.
|
||||
/// Visitor for counting working tree changes.
|
||||
struct WorkingChangeVisitor<'a> {
|
||||
count: &'a mut usize,
|
||||
}
|
||||
|
||||
impl<'a> gix_status::index_as_worktree_with_renames::VisitEntry<'a> for WorkingChangeVisitor<'a> {
|
||||
type ContentChange = <gix_status::index_as_worktree::traits::FastEq as gix_status::index_as_worktree::traits::CompareBlobs>::Output;
|
||||
type SubmoduleStatus = gix::submodule::Status;
|
||||
|
||||
fn visit_entry(&mut self, entry: gix_status::index_as_worktree_with_renames::Entry<'a, Self::ContentChange, Self::SubmoduleStatus>) {
|
||||
if let Some(summary) = entry.summary() {
|
||||
match summary {
|
||||
gix_status::index_as_worktree_with_renames::Summary::Added
|
||||
| gix_status::index_as_worktree_with_renames::Summary::Modified
|
||||
| gix_status::index_as_worktree_with_renames::Summary::Removed
|
||||
| gix_status::index_as_worktree_with_renames::Summary::TypeChange
|
||||
| gix_status::index_as_worktree_with_renames::Summary::Renamed
|
||||
| gix_status::index_as_worktree_with_renames::Summary::Copied => {
|
||||
*self.count += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get git status for a directory using gix (no subprocesses).
|
||||
/// Returns None if not in a git repository or an error occurs.
|
||||
fn get_git_status(cwd: &str) -> Option<GitStatus> {
|
||||
use std::process::Command;
|
||||
let repo = gix::discover(cwd).ok()?;
|
||||
|
||||
// Check if we're in a git repo and get the branch name
|
||||
let head_output = Command::new("git")
|
||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.ok()?;
|
||||
// Get branch name from HEAD
|
||||
let head_name = repo.head_name().ok().flatten()?;
|
||||
let head_name_bstr = head_name.as_bstr();
|
||||
let head = head_name_bstr
|
||||
.strip_prefix(b"refs/heads/")
|
||||
.or(head_name_bstr.strip_prefix(b"refs/tags/"))
|
||||
.or_else(|| head_name_bstr.strip_prefix(b"refs/"))
|
||||
.map(|s| String::from_utf8_lossy(s).into_owned())
|
||||
.unwrap_or_else(|| String::from_utf8_lossy(head_name_bstr).into_owned());
|
||||
|
||||
if !head_output.status.success() {
|
||||
if head.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let head = String::from_utf8_lossy(&head_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
// Get ahead/behind against upstream
|
||||
let mut ahead = 0usize;
|
||||
let mut behind = 0usize;
|
||||
if let Ok(head_id) = repo.head_id() {
|
||||
if let Ok(head_ref) = repo.find_reference("HEAD") {
|
||||
if let gix::refs::TargetRef::Symbolic(upstream_name) = head_ref.target() {
|
||||
let upstream_full = format!("refs/remotes/origin/{}", upstream_name);
|
||||
if let Ok(upstream_ref) = repo.find_reference(&upstream_full) {
|
||||
let mut upstream_ref = upstream_ref;
|
||||
if let Ok(upstream_id) = upstream_ref.peel_to_id() {
|
||||
let head_id_detached = head_id.detach();
|
||||
let upstream_id_detached = upstream_id.detach();
|
||||
// Count ahead: commits reachable from head_id but not from upstream_id
|
||||
let mut count = 0usize;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut queue = vec![head_id_detached];
|
||||
while let Some(current) = queue.pop() {
|
||||
if seen.contains(¤t) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(current);
|
||||
if let Ok(commit) = repo.find_commit(current.clone()) {
|
||||
for parent_oid in commit.parent_ids() {
|
||||
let parent_oid_detached = parent_oid.detach();
|
||||
if parent_oid_detached == upstream_id_detached {
|
||||
break;
|
||||
}
|
||||
if !seen.contains(&parent_oid_detached) {
|
||||
queue.push(parent_oid_detached);
|
||||
}
|
||||
}
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
ahead = count;
|
||||
|
||||
// Get ahead/behind status
|
||||
let mut ahead = 0;
|
||||
let mut behind = 0;
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let counts = String::from_utf8_lossy(&output.stdout);
|
||||
let parts: Vec<&str> = counts.trim().split_whitespace().collect();
|
||||
if parts.len() == 2 {
|
||||
ahead = parts[0].parse().unwrap_or(0);
|
||||
behind = parts[1].parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get working directory and staging status using git status --porcelain
|
||||
let mut working_modified = 0;
|
||||
let mut working_added = 0;
|
||||
let mut working_deleted = 0;
|
||||
let mut staging_modified = 0;
|
||||
let mut staging_added = 0;
|
||||
let mut staging_deleted = 0;
|
||||
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["status", "--porcelain"])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let status = String::from_utf8_lossy(&output.stdout);
|
||||
for line in status.lines() {
|
||||
if line.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
let staging_char = chars[0];
|
||||
let working_char = chars[1];
|
||||
|
||||
// Staging status (first column)
|
||||
match staging_char {
|
||||
'M' => staging_modified += 1,
|
||||
'A' => staging_added += 1,
|
||||
'D' => staging_deleted += 1,
|
||||
'R' => staging_modified += 1, // renamed
|
||||
'C' => staging_added += 1, // copied
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Working directory status (second column)
|
||||
match working_char {
|
||||
'M' => working_modified += 1,
|
||||
'D' => working_deleted += 1,
|
||||
'?' => working_added += 1, // untracked
|
||||
_ => {}
|
||||
// Count behind: commits reachable from upstream_id but not from head_id
|
||||
let mut count = 0usize;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut queue = vec![upstream_id_detached];
|
||||
while let Some(current) = queue.pop() {
|
||||
if seen.contains(¤t) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(current);
|
||||
if let Ok(commit) = repo.find_commit(current.clone()) {
|
||||
for parent_oid in commit.parent_ids() {
|
||||
let parent_oid_detached = parent_oid.detach();
|
||||
if parent_oid_detached == head_id_detached {
|
||||
break;
|
||||
}
|
||||
if !seen.contains(&parent_oid_detached) {
|
||||
queue.push(parent_oid_detached);
|
||||
}
|
||||
}
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
behind = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build status strings like oh-my-posh format
|
||||
let working_changed = working_modified + working_added + working_deleted;
|
||||
let mut working_parts = Vec::new();
|
||||
if working_modified > 0 {
|
||||
working_parts.push(format!("~{}", working_modified));
|
||||
}
|
||||
if working_added > 0 {
|
||||
working_parts.push(format!("+{}", working_added));
|
||||
}
|
||||
if working_deleted > 0 {
|
||||
working_parts.push(format!("-{}", working_deleted));
|
||||
}
|
||||
let working_string = working_parts.join(" ");
|
||||
// Get stash count from refs
|
||||
let stash_count = if repo.find_reference("refs/stash").is_ok() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let staging_changed = staging_modified + staging_added + staging_deleted;
|
||||
let mut staging_parts = Vec::new();
|
||||
if staging_modified > 0 {
|
||||
staging_parts.push(format!("~{}", staging_modified));
|
||||
}
|
||||
if staging_added > 0 {
|
||||
staging_parts.push(format!("+{}", staging_added));
|
||||
}
|
||||
if staging_deleted > 0 {
|
||||
staging_parts.push(format!("-{}", staging_deleted));
|
||||
}
|
||||
let staging_string = staging_parts.join(" ");
|
||||
// Get staging status (HEAD tree vs index) and working status (index vs worktree)
|
||||
let mut staging_changed = 0usize;
|
||||
let mut working_changed = 0usize;
|
||||
|
||||
// Get stash count
|
||||
let mut stash_count = 0;
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["stash", "list"])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let stash = String::from_utf8_lossy(&output.stdout);
|
||||
stash_count = stash.lines().count();
|
||||
// Get HEAD tree ID for tree-index diff
|
||||
if let Ok(head_tree_id) = repo.head_tree_id() {
|
||||
// Count staging changes (HEAD tree vs index)
|
||||
if let Ok(index) = repo.index() {
|
||||
let _ = repo.tree_index_status(
|
||||
&head_tree_id,
|
||||
&index,
|
||||
None::<&mut gix::Pathspec<'_>>,
|
||||
gix::status::tree_index::TrackRenames::AsConfigured,
|
||||
|change: gix_diff::index::ChangeRef<'_, '_>, _tree_idx: &gix::index::State, _worktree_idx: &gix::index::State| {
|
||||
match change {
|
||||
gix_diff::index::ChangeRef::Addition { .. } => staging_changed += 1,
|
||||
gix_diff::index::ChangeRef::Deletion { .. } => staging_changed += 1,
|
||||
gix_diff::index::ChangeRef::Modification { .. } => staging_changed += 1,
|
||||
gix_diff::index::ChangeRef::Rewrite { .. } => staging_changed += 1,
|
||||
}
|
||||
{ let cf: std::ops::ControlFlow<()> = std::ops::ControlFlow::Continue(()); Ok::<_, Box<dyn std::error::Error + Send + Sync>>(cf) }
|
||||
},
|
||||
).ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Count working changes (index vs worktree)
|
||||
if let Ok(index) = repo.index() {
|
||||
let _ = repo.index_worktree_status(
|
||||
&index,
|
||||
Vec::<&str>::new(),
|
||||
&mut WorkingChangeVisitor { count: &mut working_changed },
|
||||
gix_status::index_as_worktree::traits::FastEq,
|
||||
gix::status::index_worktree::BuiltinSubmoduleStatus::new(repo.clone().into_sync(), gix::status::Submodule::AsConfigured { check_dirty: false }).ok()?,
|
||||
&mut gix::progress::Discard,
|
||||
&std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
Default::default(),
|
||||
).ok();
|
||||
}
|
||||
|
||||
// Build status strings matching original format
|
||||
let working_string = if working_changed > 0 {
|
||||
format!("{}", working_changed)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let staging_string = if staging_changed > 0 {
|
||||
format!("{}", staging_changed)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
Some(GitStatus {
|
||||
head,
|
||||
ahead,
|
||||
@@ -3453,7 +3500,7 @@ fn setup_config_watcher(
|
||||
|
||||
fn main() {
|
||||
env_logger::Builder::from_env(
|
||||
env_logger::Env::default().default_filter_or("info"),
|
||||
env_logger::Env::default().default_filter_or("warn"),
|
||||
)
|
||||
.init();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user