2 Commits
Author SHA1 Message Date
zach 84593e9480 fix: git revwalk performance 2026-09-01 22:22:11 +02:00
zach 7d43b024d2 try updating ffmpeg 2026-08-09 15:48:42 +02:00
2 changed files with 241 additions and 29 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ 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.1", optional = true }
ffmpeg-next = { version = "9.0", optional = true }
[features]
default = ["webm"]
+240 -28
View File
@@ -23,9 +23,10 @@ use std::collections::HashMap;
use std::io::Write;
use std::os::fd::AsRawFd;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use polling::{Event, Events, Poller};
@@ -992,7 +993,7 @@ fn build_cwd_section(cwd: &str, is_light: bool) -> StatuslineSection {
}
/// Git repository status information.
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
struct GitStatus {
/// Current branch or HEAD reference.
head: String,
@@ -1047,6 +1048,77 @@ impl<'a> gix_status::index_as_worktree_with_renames::VisitEntry<'a>
}
}
/// TTL for how long a cached git status is considered fresh. The statusline
/// only ever displays the active pane's cwd, so recomputing more often than
/// this just wastes CPU scanning the worktree.
const GIT_STATUS_TTL: Duration = Duration::from_millis(1000);
/// Latest computed git status, shared between the render thread (reader) and
/// the background git-status thread (writer). The render thread never blocks
/// on gix: it only ever reads this cheaply-locked value, so a large
/// repository cannot stall the UI.
#[derive(Default)]
struct GitCache {
/// cwd the cached `status` was computed for.
cwd: Option<String>,
/// When `status` was computed.
computed_at: Option<Instant>,
/// The computed status (None when not in a git repository).
status: Option<GitStatus>,
}
impl GitCache {
/// True when we hold a fresh status for `cwd` (no recompute needed).
fn is_fresh(&self, cwd: &str) -> bool {
self.cwd.as_deref() == Some(cwd)
&& self
.computed_at
.is_some_and(|t| t.elapsed() < GIT_STATUS_TTL)
}
}
/// Spawn a background thread that recomputes git status off the render path.
///
/// The render thread calls [`GitCache`]-backed reads and, when the cache is
/// stale, `try_send`s the cwd to this thread. The thread recomputes (at most
/// once per `GIT_STATUS_TTL` per cwd) and writes the result back to the
/// shared cache. Returns the request sender.
fn spawn_git_status_thread(
shutdown: Arc<AtomicBool>,
cache: Arc<Mutex<GitCache>>,
) -> Option<mpsc::Sender<String>> {
let (tx, rx) = mpsc::channel::<String>();
std::thread::Builder::new()
.name("git-status".into())
.spawn(move || {
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
match rx.recv_timeout(Duration::from_millis(250)) {
Ok(cwd) => {
// The guard from this statement is dropped before the
// (potentially slow) compute, so the render thread can
// still read the cache while we work.
let need_compute =
!cache.lock().unwrap().is_fresh(&cwd);
if need_compute {
let status = get_git_status(&cwd);
let mut c = cache.lock().unwrap();
c.cwd = Some(cwd);
c.computed_at = Some(Instant::now());
c.status = status;
}
}
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
})
.ok()?;
Some(tx)
}
/// 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> {
@@ -1110,7 +1182,10 @@ fn get_git_status(cwd: &str) -> Option<GitStatus> {
}
seen.insert(current);
if current == merge_base_detached {
break;
// Skip the merge base itself
// (exclusive) but keep walking the
// rest of the queue.
continue;
}
if let Ok(commit) =
repo.find_commit(current.clone())
@@ -1139,7 +1214,10 @@ fn get_git_status(cwd: &str) -> Option<GitStatus> {
}
seen.insert(current);
if current == merge_base_detached {
break;
// Skip the merge base itself
// (exclusive) but keep walking the
// rest of the queue.
continue;
}
if let Ok(commit) =
repo.find_commit(current.clone())
@@ -1264,9 +1342,10 @@ fn get_git_status(cwd: &str) -> Option<GitStatus> {
/// Build a statusline section for git status.
/// Returns None if not in a git repository.
fn build_git_section(cwd: &str, is_light: bool) -> Option<StatuslineSection> {
let status = get_git_status(cwd)?;
fn build_git_section(
status: &GitStatus,
is_light: bool,
) -> Option<StatuslineSection> {
// Determine foreground color based on state (matching oh-my-posh template)
// Priority order (last match wins in oh-my-posh):
// 1. Default: #0da300 (green)
@@ -1509,6 +1588,12 @@ struct App {
render_fatal_error: bool,
/// Whether UI state changed and a full redraw is needed (tab switch, pane focus, etc.).
needs_redraw: bool,
/// Shared git status cache (read by render, written by git-status thread).
git_cache: Arc<Mutex<GitCache>>,
/// Sender to the background git-status thread (requests a cwd recompute).
git_refresh_tx: Option<mpsc::Sender<String>>,
/// Throttles refresh requests so we don't spam the git thread each frame.
last_git_request_at: Instant,
}
impl App {
@@ -1517,6 +1602,15 @@ impl App {
let action_map = config.keybindings.build_action_map();
// Git status is computed on a background thread so large repos don't
// stall the render path. The render thread only reads the shared
// cache and requests recomputes when it goes stale.
let shutdown = Arc::new(AtomicBool::new(false));
let git_cache: Arc<Mutex<GitCache>> =
Arc::new(Mutex::new(GitCache::default()));
let git_refresh_tx =
spawn_git_status_thread(shutdown.clone(), git_cache.clone());
Self {
window: None,
renderer: None,
@@ -1527,7 +1621,7 @@ impl App {
modifiers: WinitModifiers::default(),
keyboard_state: KeyboardState::new(),
event_loop_proxy: None,
shutdown: Arc::new(AtomicBool::new(false)),
shutdown,
cursor_position: PhysicalPosition::new(0.0, 0.0),
mouse_down_pos: None,
frame_count: 0,
@@ -1547,6 +1641,9 @@ impl App {
last_render_at: std::time::Instant::now(),
render_fatal_error: false,
needs_redraw: false,
git_cache,
git_refresh_tx,
last_git_request_at: Instant::now(),
}
}
@@ -2239,28 +2336,85 @@ impl App {
|| glow_in_progress
|| image_animation_in_progress;
// Get the statusline content for the active pane
let statusline_content: StatuslineContent = tab
.panes
.get(&active_pane_id)
.map(|pane| {
if let Some(ref custom) = pane.custom_statusline {
StatuslineContent::Raw(custom.clone())
} else if let Some(cwd) = pane.pty.foreground_cwd() {
let is_light = pane.terminal.palette.is_light();
let mut sections =
vec![build_cwd_section(&cwd, is_light)];
if let Some(git_section) =
build_git_section(&cwd, is_light)
// Get the statusline content for the active pane.
// Git status comes from the shared cache (computed on a
// background thread) so rendering never blocks on gix.
let statusline_content: StatuslineContent =
match tab.panes.get(&active_pane_id) {
Some(pane) => {
if let Some(ref custom) =
pane.custom_statusline
{
sections.push(git_section);
StatuslineContent::Raw(custom.clone())
} else if let Some(cwd) =
pane.pty.foreground_cwd()
{
let is_light =
pane.terminal.palette.is_light();
let mut sections =
vec![build_cwd_section(
&cwd,
is_light
)];
// Read latest cached git status. Only use
// it if it matches this pane's cwd.
let (git_status, fresh) = {
let c = self
.git_cache
.lock()
.unwrap_or_else(
|p| p.into_inner(),
);
let matches =
c.cwd.as_deref()
== Some(cwd.as_str());
(
if matches {
c.status.clone()
} else {
None
},
c.is_fresh(&cwd),
)
};
// Request a recompute if stale, throttled
// so we don't queue one per frame while the
// background thread is still working.
if !fresh
&& self
.last_git_request_at
.elapsed()
> Duration::from_millis(150)
{
self.last_git_request_at =
Instant::now();
if let Some(tx) =
&self.git_refresh_tx
{
// Unbounded channel: send never blocks.
let _ = tx.send(cwd.clone());
}
}
if let Some(ref status) = git_status {
if let Some(git_section) =
build_git_section(
status,
is_light
)
{
sections.push(git_section);
}
}
StatuslineContent::Sections(sections)
} else {
StatuslineContent::Sections(Vec::new())
}
StatuslineContent::Sections(sections)
} else {
StatuslineContent::Sections(Vec::new())
}
})
.unwrap_or_default();
None => StatuslineContent::Sections(Vec::new()),
};
match renderer.render_panes(
&pane_render_data,
@@ -3927,3 +4081,61 @@ extern "C" fn handle_sigusr1(_: i32) {
}
}
}
#[cfg(test)]
mod git_status_tests {
use super::*;
use std::process::Command;
fn git(dir: &std::path::Path, args: &[&str]) {
let out = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.expect("failed to run git");
assert!(
out.status.success(),
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn get_git_status_reports_branch_and_changes() {
let dir = std::env::temp_dir().join(format!(
"zterm-git-test-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
git(&dir, &["init", "-q", "-b", "main"]);
git(&dir, &["config", "user.email", "t@t.t"]);
git(&dir, &["config", "user.name", "t"]);
std::fs::write(dir.join("a.txt"), "hello").unwrap();
git(&dir, &["add", "a.txt"]);
git(&dir, &["commit", "-q", "-m", "c1"]);
let cwd = dir.to_str().unwrap();
let status = get_git_status(cwd).expect("expected git status");
assert_eq!(status.head, "main");
assert_eq!(status.ahead, 0);
assert_eq!(status.behind, 0);
assert_eq!(status.working_changed, 0);
assert_eq!(status.staging_changed, 0);
// Modify a tracked file -> 1 working change.
std::fs::write(dir.join("a.txt"), "changed").unwrap();
let status = get_git_status(cwd).expect("expected git status");
assert_eq!(status.working_changed, 1);
assert_eq!(status.staging_changed, 0);
// Stage it -> moves from working to staging.
git(&dir, &["add", "a.txt"]);
let status = get_git_status(cwd).expect("expected git status");
assert_eq!(status.working_changed, 0);
assert_eq!(status.staging_changed, 1);
std::fs::remove_dir_all(&dir).ok();
}
}