Skip to content
Merged
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
63 changes: 63 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Stamps the build's commit into `NIGHTCROW_COMMIT` so a running binary can
//! say which source it came from — the splash and the empty pane both show it.
//!
//! A missing or unreadable repository is not a build failure: the crate is also
//! built from a crates.io package and from `cargo install --git` checkouts, and
//! only the latter carries git metadata. Those builds report `unknown`.

use std::path::Path;
use std::process::Command;

fn main() {
println!("cargo:rustc-env=NIGHTCROW_COMMIT={}", commit());
watch_head();
}

fn commit() -> String {
let Some(sha) = git(&["rev-parse", "--short=9", "HEAD"]) else {
return "unknown".to_string();
};
// Only meaningful once the sha resolved: with no repository at all, the
// diff below fails too and would read as "dirty".
let dirty = Command::new("git")
.args(["diff", "--quiet", "HEAD"])
.status()
.is_ok_and(|status| !status.success());
if dirty { format!("{sha}+") } else { sha }
}

fn git(args: &[&str]) -> Option<String> {
let out = Command::new("git").args(args).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8(out.stdout).ok()?.trim().to_string();
if text.is_empty() { None } else { Some(text) }
}

/// Re-run when the checked-out commit moves. Both files are needed: `HEAD`
/// changes on a branch switch, the branch's ref file on a new commit.
///
/// Only existing paths are declared — cargo treats a missing one as changed and
/// would rebuild on every invocation.
fn watch_head() {
let head = Path::new(".git/HEAD");
if !head.exists() {
return;
}
println!("cargo:rerun-if-changed=.git/HEAD");

let Ok(contents) = std::fs::read_to_string(head) else {
return;
};
if let Some(reference) = contents.strip_prefix("ref: ") {
let path = Path::new(".git").join(reference.trim());
if path.exists() {
println!("cargo:rerun-if-changed={}", path.display());
}
}
// A packed ref has no loose file; the pack itself is then the thing to watch.
if Path::new(".git/packed-refs").exists() {
println!("cargo:rerun-if-changed=.git/packed-refs");
}
}
21 changes: 16 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ OSC 0/2 탭 타이틀 캡처, 마우스 캡처(클릭 포커스/포워딩, 휠
`#[cfg(test)] mod tests;`로 별도 파일/디렉터리에 분리한다(아래 트리에서는 생략).

```
build.rs # stamps NIGHTCROW_COMMIT: the commit a binary was built from,
# shown beside the version on the splash and the empty pane.
# No git metadata (crates.io package) → "unknown"; a dirty
# work tree → a trailing "+"
src/
├── main.rs # entry point: dispatch to daemon / attach / serve / init
├── cli.rs, cli/ # Cli/Commands + attach/daemon/init/stop/plugin command handlers
Expand All @@ -92,7 +96,8 @@ src/
│ │ # daemon-owned tab list
│ ├── terminal_guard.rs # raw mode + alternate screen, restored on the way out
│ ├── bootstrap.rs, event_loop.rs, splash.rs # App construction + startup commands,
│ │ # main_loop (poll/render/input drain), first-run overlay
│ │ # main_loop (poll/render/input drain), splash loop (timed
│ │ # sky frames, dismissed only by a key press)
│ └── input/ # dispatch, ViewMode handlers, prefix follow-up,
│ # mouse, paste, repo-dialog keys
├── platform/ # OS-adjacent services shared by domain layers:
Expand Down Expand Up @@ -129,12 +134,18 @@ src/
│ ├── status_view.rs, log_view/, tree_view/ # per-ViewMode state (filter/search cache,
│ │ # commits + drill-down, child cache + expanded set)
│ ├── file_list.rs, commit_list/, tree_list.rs # the three upper-left row renderers
│ ├── path_tree.rs, file_view.rs, search.rs, splash.rs, wall_clock.rs # repo-dialog
│ │ # browser, file preview state, SearchQuery newtype, first-run
│ │ # overlay, unix epoch → HH:MM without a date crate
│ ├── path_tree.rs, file_view.rs, search.rs, splash/, wall_clock.rs # repo-dialog
│ │ # browser, file preview state, SearchQuery newtype, the night
│ │ # scene (scene.rs: sprites as glyph art + an aligned ink map
│ │ # — shading in ░▒▓█, surface in the map — plus the only two
│ │ # things that move: twinkling stars and a blink; night.rs:
│ │ # ink → 256-colour palette, bottom-anchored crop, build id;
│ │ # the startup splash and the empty terminal pane both draw
│ │ # it), unix epoch → HH:MM without a date crate
│ ├── diff_pane/, diff_viewer/ # DiffPane state (hunks/scroll/search/split); the
│ │ # upper-right widget, gutter, split view, file preview
│ └── terminal_tab/, project_tab/ # pane grid + tab bar + recovery markers;
│ └── terminal_tab/, project_tab/ # pane grid + tab bar + recovery markers (with no
│ # pane the grid gives way to splash::draw_idle);
│ # project tab row rendering + click targets
├── backend/
│ ├── mod.rs # TerminalBackend trait + BackendEvent
Expand Down
33 changes: 24 additions & 9 deletions src/application/splash.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use crate::application::terminal_guard::TuiTerminal;
use crate::ui::splash::TWINKLE_FRAME;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::time::Instant;

pub(crate) enum SplashOutcome {
Enter,
Quit,
}

/// Run the splash until it times out or a key dismisses it.
/// Show the night scene until the user presses a key.
///
/// There is no dismissal timer — only the twinkling sky is timed, so the splash
/// waits as long as the user does.
///
/// `accent_idx` is the session's, read from its file rather than taken from the
/// daemon: the splash draws before this client has attached, so the broadcast
Expand All @@ -16,16 +21,22 @@ pub(crate) fn splash_loop(
terminal: &mut TuiTerminal,
accent_idx: usize,
) -> anyhow::Result<SplashOutcome> {
let splash = crate::ui::splash::SplashState::new();
let accent = crate::config::Accent::from_index(accent_idx).color();
let mut tick = 0usize;
let mut next_frame = Instant::now();

loop {
terminal.draw(|frame| {
crate::ui::splash::draw(frame, &splash, accent);
})?;
if splash.is_done() {
break;
if Instant::now() >= next_frame {
terminal.draw(|frame| {
crate::ui::splash::draw(frame, accent, tick);
})?;
tick = tick.wrapping_add(1);
next_frame = Instant::now() + TWINKLE_FRAME;
}
if event::poll(std::time::Duration::from_millis(16))? {

// Wait out the rest of the frame rather than a fixed slice, so input
// stays responsive and mouse traffic cannot race the animation ahead.
if event::poll(next_frame.saturating_duration_since(Instant::now()))? {
match event::read()? {
// Honour Esc so the user can abort during the splash instead
// of being forced to wait for it to clear and quit from the
Expand All @@ -38,11 +49,15 @@ pub(crate) fn splash_loop(
}
break;
}
Event::Resize(_, _) => terminal.clear()?,
Event::Resize(_, _) => {
terminal.clear()?;
next_frame = Instant::now();
}
_ => {}
}
}
}

terminal.clear()?;
Ok(SplashOutcome::Enter)
}
119 changes: 0 additions & 119 deletions src/ui/splash.rs

This file was deleted.

Loading