Compare commits
2
Commits
main
...
ffmpeg-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84593e9480 | ||
|
|
7d43b024d2 |
+1
-1
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user