Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Options:
-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]
-i, --initial-selection <TYPE> Initial selection of commit [default: latest] [possible values: latest, head]
--watch [<SECONDS>] Auto-refresh the view on an interval (seconds) [default: 30]
--fetch Run `git fetch --all` before each auto-refresh (implies --watch)
-h, --help Print help
-V, --version Print version
```
Expand Down
26 changes: 26 additions & 0 deletions docs/src/getting-started/command-line-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,29 @@ _Possible values:_ `latest`, `head`
`latest` will select the latest commit.

`head` will select the commit at HEAD.

## --watch \[\<SECONDS\>\]

Enable automatic refresh of the view on a fixed interval.

If passed without a value, the interval defaults to 30 seconds.
Values below 5 seconds are clamped to 5.

The refresh only re-reads local git state — it does not contact any remote.
External git activity (commits made in another terminal, fetches done by
another tool) is picked up automatically as soon as it lands on disk.

The current cursor / scroll position is preserved across refreshes.
Ticks that arrive while you are typing in the search prompt are silently
skipped so they do not interrupt input.

## --fetch

Run `git fetch --all --quiet` before each auto-refresh tick.

Implies `--watch`: if `--watch` is not specified, the interval defaults
to 30 seconds. Combine with `--watch <SECONDS>` to override.

Fetch errors (network unreachable, authentication failures, etc.) are
silently ignored so the refresh tick still fires with whatever local
state is available.
8 changes: 8 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ impl App<'_> {
let request = RefreshRequest { context };
return Ok(Ret::Refresh(request));
}
AppEvent::AutoRefresh => {
// Skip ticks that arrive while the user is typing in the status line
// (e.g. a search query) so we don't tear down their input.
if matches!(self.app_status.status_line, StatusLine::Input(_, _, _)) {
continue;
}
self.view.refresh();
}
AppEvent::ClearStatusLine => {
self.clear_status_line();
}
Expand Down
57 changes: 56 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::{
fmt::{self, Debug, Formatter},
process::{Command, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc, Mutex,
},
thread,
time::Duration,
};

use ratatui::crossterm::event::KeyEvent;
Expand Down Expand Up @@ -33,6 +35,7 @@ pub enum AppEvent {
SelectParentCommit,
CopyToClipboard { name: String, value: String },
Refresh(RefreshViewContext),
AutoRefresh,
ClearStatusLine,
UpdateStatusInput(String, Option<u16>, Option<String>),
NotifyInfo(String),
Expand Down Expand Up @@ -80,10 +83,14 @@ pub struct EventController {
rx: Receiver,
stop: Arc<AtomicBool>,
handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
watch_interval: Option<Duration>,
watch_fetch: bool,
watch_stop: Arc<AtomicBool>,
watch_handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
}

impl EventController {
pub fn init() -> Self {
pub fn init(watch_interval: Option<Duration>, watch_fetch: bool) -> Self {
let (tx, rx) = mpsc::channel();
let tx = Sender { tx };
let rx = Receiver { rx };
Expand All @@ -93,6 +100,10 @@ impl EventController {
rx,
stop: Arc::new(AtomicBool::new(false)),
handle: Arc::new(Mutex::new(None)),
watch_interval,
watch_fetch,
watch_stop: Arc::new(AtomicBool::new(false)),
watch_handle: Arc::new(Mutex::new(None)),
};
controller.start();

Expand Down Expand Up @@ -131,6 +142,46 @@ impl EventController {
}
});
*self.handle.lock().unwrap() = Some(handle);

if let Some(interval) = self.watch_interval {
self.watch_stop.store(false, Ordering::Relaxed);
let stop = self.watch_stop.clone();
let tx = self.tx.clone();
let fetch = self.watch_fetch;
let handle = thread::spawn(move || loop {
// Sleep in small slices so we react to stop within ~100ms.
let mut remaining = interval;
while remaining > Duration::ZERO {
if stop.load(Ordering::Relaxed) {
return;
}
let slice = remaining.min(Duration::from_millis(100));
thread::sleep(slice);
remaining = remaining.saturating_sub(slice);
}
if stop.load(Ordering::Relaxed) {
return;
}
if fetch {
// Best-effort: ignore network / auth failures so the
// refresh tick still fires. Force non-interactive so
// the fetch can't hang on a prompt the user can't see
// (TUI owns the terminal).
let _ = Command::new("git")
.args(["fetch", "--all", "--quiet"])
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
if stop.load(Ordering::Relaxed) {
return;
}
tx.send(AppEvent::AutoRefresh);
});
*self.watch_handle.lock().unwrap() = Some(handle);
}
}

pub fn resume(&self) {
Expand Down Expand Up @@ -161,6 +212,10 @@ impl EventController {
if let Some(handle) = self.handle.lock().unwrap().take() {
handle.join().unwrap();
}
self.watch_stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.watch_handle.lock().unwrap().take() {
handle.join().unwrap();
}
}

fn drain_crossterm_event(&self) {
Expand Down
12 changes: 11 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ struct Args {
/// Initial selection of commit [default: latest]
#[arg(short, long, value_name = "TYPE")]
initial_selection: Option<InitialSelection>,

/// Auto-refresh the view on an interval (seconds) [default: 30]
#[arg(long, value_name = "SECONDS", num_args = 0..=1, default_missing_value = "30")]
watch: Option<u64>,

/// Run `git fetch --all` before each auto-refresh (implies --watch)
#[arg(long)]
fetch: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Deserialize)]
Expand Down Expand Up @@ -158,7 +166,9 @@ pub fn run() -> Result<()> {
image_protocol,
});

let ec = event::EventController::init();
let watch_seconds = args.watch.or(if args.fetch { Some(30) } else { None });
let watch_interval = watch_seconds.map(|s| std::time::Duration::from_secs(s.max(5)));
let ec = event::EventController::init(watch_interval, args.fetch);
let mut refresh_view_context = None;
let mut terminal = None;

Expand Down