diff --git a/Cargo.lock b/Cargo.lock
index 35689cbe..ae9a4970 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9846,6 +9846,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.11.0",
+ "smallvec",
"smol",
"system-configuration",
"system-configuration-sys",
diff --git a/assets/icons/git-commit.svg b/assets/icons/git-commit.svg
new file mode 100644
index 00000000..552e6b17
--- /dev/null
+++ b/assets/icons/git-commit.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/git-sync.svg b/assets/icons/git-sync.svg
new file mode 100644
index 00000000..87ae7376
--- /dev/null
+++ b/assets/icons/git-sync.svg
@@ -0,0 +1 @@
+
diff --git a/crates/tty7-core/Cargo.toml b/crates/tty7-core/Cargo.toml
index 50520464..d58d6dc7 100644
--- a/crates/tty7-core/Cargo.toml
+++ b/crates/tty7-core/Cargo.toml
@@ -48,6 +48,12 @@ sha2 = "0.11"
# implementation.
ignore = "0.4"
+# Lane assignment for the commit graph (`core::git::log`) keeps a couple of
+# parents and a handful of edges per row; a SmallVec keeps those off the heap
+# for the shapes that make up almost all of a real history. Already in the tree
+# via the GUI, so this pins no new code.
+smallvec.workspace = true
+
# Cross-platform PTY for the daemon: a Unix pty on Unix, ConPTY on Windows,
# behind one blocking `Read`/`Write`/`resize` API. This is what lets
# `daemon::pane` share a single code path across platforms instead of
diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs
index 9dd16184..50a40989 100644
--- a/crates/tty7-core/src/core/config.rs
+++ b/crates/tty7-core/src/core/config.rs
@@ -168,6 +168,15 @@ pub struct Config {
pub right_panel_width: f32,
#[serde(default, deserialize_with = "de_lenient")]
pub right_panel_tab: RightPanelTab,
+ /// Global, not per-overlay — the same call VS Code's
+ /// `diffEditor.renderSideBySide` makes.
+ #[serde(default, deserialize_with = "de_lenient")]
+ pub diff_view: DiffViewMode,
+ /// The source control panel's history section starts collapsed: a graph
+ /// unfurling the first time someone opens the panel is a worse first
+ /// impression than one they asked for.
+ #[serde(default)]
+ pub scm_graph_expanded: bool,
#[serde(default, deserialize_with = "de_lenient")]
pub sidebar_grouping: SidebarGrouping,
#[serde(default = "default_true")]
@@ -508,6 +517,8 @@ impl Default for Config {
right_panel_visible: false,
right_panel_width: default_right_panel_width(),
right_panel_tab: RightPanelTab::Info,
+ diff_view: DiffViewMode::Split,
+ scm_graph_expanded: false,
sidebar_grouping: SidebarGrouping::Repo,
sidebar_diff_preview: true,
notify_on_command_finish: NotifyMode::Unfocused,
@@ -828,10 +839,28 @@ fn default_prefix() -> String {
pub enum RightPanelTab {
#[default]
Info,
- Changes,
+ /// The source control panel. Renamed from `Changes` in place rather than
+ /// added alongside it: `rename` works in both directions, so a config
+ /// written by this version still says `"changes"` and an older build reads
+ /// it back unchanged. A fourth variant could not do that — the old build
+ /// would fall through `de_lenient` to `Info` and kick anyone who rolled
+ /// back off the panel they were sitting on. 260px has no room for a fourth
+ /// tab tile either.
+ #[serde(rename = "changes", alias = "scm", alias = "git")]
+ Scm,
Files,
}
+/// How the diff overlay lays a file out. Side-by-side is the default because
+/// that is what everyone already sees.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum DiffViewMode {
+ #[default]
+ Split,
+ Unified,
+}
+
fn default_right_panel_width() -> f32 {
260.
}
diff --git a/crates/tty7-core/src/core/git.rs b/crates/tty7-core/src/core/git.rs
deleted file mode 100644
index eda88e62..00000000
--- a/crates/tty7-core/src/core/git.rs
+++ /dev/null
@@ -1,386 +0,0 @@
-use std::io::{self, Read as _};
-use std::path::{Path, PathBuf};
-use std::process::{Command, Stdio};
-
-use crate::host::{Host, Output};
-
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub struct GitStatus {
- pub branch: String,
- pub added: u32,
- pub removed: u32,
-}
-
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub struct RepoSnapshot {
- pub root: PathBuf,
- pub home: PathBuf,
- pub branch: String,
- pub counts: Option<(u32, u32)>,
-}
-
-pub fn probe(host: &dyn Host, cwd: &Path) -> Option {
- let paths = git(
- host,
- cwd,
- &[
- "rev-parse",
- "--path-format=absolute",
- "--show-toplevel",
- "--git-dir",
- "--git-common-dir",
- ],
- )?;
- let mut lines = paths.lines().map(|l| l.trim_end_matches(['\n', '\r']));
- let root = PathBuf::from(lines.next()?);
- let home = repo_home(&root, lines.next(), lines.next());
- let branch = branch_name(host, cwd)?;
- Some(RepoSnapshot {
- home,
- root,
- branch,
- counts: diff_numstat(host, cwd),
- })
-}
-
-fn repo_home(root: &Path, git_dir: Option<&str>, common_dir: Option<&str>) -> PathBuf {
- let (Some(git_dir), Some(common)) = (git_dir, common_dir) else {
- return root.to_path_buf();
- };
- if git_dir == common {
- return root.to_path_buf();
- }
- let common = Path::new(common);
- match (common.file_name(), common.parent()) {
- (Some(name), Some(parent)) if name == ".git" => parent.to_path_buf(),
- _ => common.to_path_buf(),
- }
-}
-
-pub fn branch_name(host: &dyn Host, cwd: &Path) -> Option {
- if let Some(out) = git(host, cwd, &["symbolic-ref", "--quiet", "--short", "HEAD"]) {
- let name = out.trim();
- if !name.is_empty() {
- return Some(name.to_string());
- }
- }
- let sha = git(host, cwd, &["rev-parse", "--short", "HEAD"])?;
- let sha = sha.trim();
- (!sha.is_empty()).then(|| sha.to_string())
-}
-
-fn diff_numstat(host: &dyn Host, cwd: &Path) -> Option<(u32, u32)> {
- let out = git(host, cwd, &["diff", "--numstat", "HEAD"])?;
- let mut added = 0u32;
- let mut removed = 0u32;
- for line in out.lines() {
- let mut fields = line.split('\t');
- if let Some(n) = fields.next().and_then(|s| s.parse::().ok()) {
- added += n;
- }
- if let Some(n) = fields.next().and_then(|s| s.parse::().ok()) {
- removed += n;
- }
- }
- Some((added, removed))
-}
-
-pub fn git(host: &dyn Host, cwd: &Path, args: &[&str]) -> Option {
- let out = host.git(cwd, args).ok()?;
- if !out.success() {
- return None;
- }
- String::from_utf8(out.stdout).ok()
-}
-
-pub fn git_output(cwd: &Path, args: &[&str]) -> io::Result