diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2b98da2..52cd944 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3443,7 +3443,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "linux-terminal-launch" version = "0.1.0" -source = "git+https://github.com/cst8t/linux-terminal-launch#1c2db9bb27d735fece63448cd3c0781e20a92437" +source = "git+https://github.com/cst8t/linux-terminal-launch#d256bca7868c22a909ac2290f1bbce2e12db5b8c" [[package]] name = "litemap" diff --git a/src-tauri/config.example.toml b/src-tauri/config.example.toml index 3b9d053..afe2b9a 100644 --- a/src-tauri/config.example.toml +++ b/src-tauri/config.example.toml @@ -82,6 +82,14 @@ updateEndpoint = "https://github.com/cst8t/gitmun/releases/latest/download/lates # "Native" applies no overrides; uses full hardware acceleration. linuxGraphicsMode = "Auto" +# Terminal emulator for Linux repository opens. +# Use "auto", "custom", or a known terminal ID such as "ghostty" or "gnome-terminal". +linuxTerminalEmulator = "auto" + +# Custom Linux terminal command. Used only when linuxTerminalEmulator is "custom". +# Use {path} where the repository path should be inserted. +linuxTerminalCustomCommand = "" + # Behaviour when opening a repository. # "Ask" prompts for which window to use. # "ExistingWindow" opens the repository in the current window. diff --git a/src-tauri/src/commands/repo.rs b/src-tauri/src/commands/repo.rs index e7469f6..b8c84d9 100644 --- a/src-tauri/src/commands/repo.rs +++ b/src-tauri/src/commands/repo.rs @@ -1,15 +1,14 @@ -#[cfg(target_os = "linux")] -use crate::git::types::LinuxTerminalEmulator; use crate::git::types::{ CloneRequest, CommitDetails, CommitDetailsRequest, CommitFileItem, CommitFilesRequest, CommitMarkers, CommitMessageRecovery, CommitRequest, DiffRequest, ExportCommitPatchRequest, - ExportPatchRequest, - ExternalDiffRequest, FetchRequest, FileDiff, FileRequest, GitIdentity, HunkStageRequest, - IdentityRequest, ImportPatchRequest, NumstatRequest, NumstatResult, OperationResult, - PullAnalysis, PullStrategyRequest, PushRequest, PushResult, RepoRequest, RepoStatus, - SetIdentityRequest, SshAllowedSignerStatus, StageFilesRequest, StashEntry, StashPushRequest, - StashRequest, SubmoduleActionRequest, + ExportPatchRequest, ExternalDiffRequest, FetchRequest, FileDiff, FileRequest, GitIdentity, + HunkStageRequest, IdentityRequest, ImportPatchRequest, NumstatRequest, NumstatResult, + OperationResult, PullAnalysis, PullStrategyRequest, PushRequest, PushResult, RepoRequest, + RepoStatus, SetIdentityRequest, SshAllowedSignerStatus, StageFilesRequest, StashEntry, + StashPushRequest, StashRequest, SubmoduleActionRequest, }; +#[cfg(target_os = "linux")] +use crate::git::types::{LINUX_TERMINAL_AUTO_ID, LINUX_TERMINAL_CUSTOM_ID}; use crate::{AppState, CloneCancelFlag, configure_command}; use serde::{Deserialize, Serialize}; use std::io::Read; @@ -268,7 +267,7 @@ fn default_file_manager_label() -> &'static str { fn default_terminal_label(settings: &crate::git::types::Settings) -> String { #[cfg(target_os = "linux")] { - return linux_terminal_label(settings).to_string(); + return linux_terminal_label(settings); } #[cfg(not(target_os = "linux"))] @@ -370,9 +369,10 @@ fn open_terminal_at_linux( } #[cfg(target_os = "linux")] -fn linux_terminal_label(settings: &crate::git::types::Settings) -> &'static str { - linux_terminal_launch::terminal_label(linux_terminal_preference( - settings.linux_terminal_emulator, +fn linux_terminal_label(settings: &crate::git::types::Settings) -> String { + let registry = linux_terminal_launch::TerminalRegistry::with_known_terminals(); + registry.label_for_preference(&linux_terminal_preference( + settings.linux_terminal_emulator.as_str(), )) } @@ -383,40 +383,20 @@ fn linux_terminal_launcher( ) -> linux_terminal_launch::TerminalLauncher { linux_terminal_launch::TerminalLauncher::new() .working_dir(path) - .preference(linux_terminal_preference(settings.linux_terminal_emulator)) + .registry(linux_terminal_launch::TerminalRegistry::with_known_terminals()) + .preference(linux_terminal_preference( + settings.linux_terminal_emulator.as_str(), + )) .custom_command(settings.linux_terminal_custom_command.clone()) .detach_from_parent(true) } #[cfg(target_os = "linux")] -fn linux_terminal_preference( - emulator: LinuxTerminalEmulator, -) -> linux_terminal_launch::TerminalPreference { - use linux_terminal_launch::{KnownTerminal, TerminalPreference}; - - match emulator { - LinuxTerminalEmulator::Auto => TerminalPreference::Auto, - LinuxTerminalEmulator::Konsole => TerminalPreference::Known(KnownTerminal::Konsole), - LinuxTerminalEmulator::GnomeTerminal => { - TerminalPreference::Known(KnownTerminal::GnomeTerminal) - } - LinuxTerminalEmulator::GnomeConsole => { - TerminalPreference::Known(KnownTerminal::GnomeConsole) - } - LinuxTerminalEmulator::Xfce4Terminal => { - TerminalPreference::Known(KnownTerminal::Xfce4Terminal) - } - LinuxTerminalEmulator::MateTerminal => { - TerminalPreference::Known(KnownTerminal::MateTerminal) - } - LinuxTerminalEmulator::Lxterminal => TerminalPreference::Known(KnownTerminal::Lxterminal), - LinuxTerminalEmulator::Alacritty => TerminalPreference::Known(KnownTerminal::Alacritty), - LinuxTerminalEmulator::Ghostty => TerminalPreference::Known(KnownTerminal::Ghostty), - LinuxTerminalEmulator::Kitty => TerminalPreference::Known(KnownTerminal::Kitty), - LinuxTerminalEmulator::WezTerm => TerminalPreference::Known(KnownTerminal::WezTerm), - LinuxTerminalEmulator::Foot => TerminalPreference::Known(KnownTerminal::Foot), - LinuxTerminalEmulator::Xterm => TerminalPreference::Known(KnownTerminal::Xterm), - LinuxTerminalEmulator::Custom => TerminalPreference::Custom, +fn linux_terminal_preference(id: &str) -> linux_terminal_launch::TerminalPreference { + match id { + LINUX_TERMINAL_AUTO_ID => linux_terminal_launch::TerminalPreference::Auto, + LINUX_TERMINAL_CUSTOM_ID => linux_terminal_launch::TerminalPreference::Custom, + other => linux_terminal_launch::TerminalPreference::registered(other), } } diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index baff225..34d779a 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -1,7 +1,8 @@ use crate::git::types::{ AvatarProviderMode, BackendMode, CommitDateMode, CommitPrimaryAction, ExternalDiffTool, - LinuxGraphicsMode, LinuxTerminalEmulator, OperationResult, RepoOpenBehaviour, RowStriping, - Settings, ThemeMode, + LINUX_TERMINAL_AUTO_ID, LINUX_TERMINAL_CUSTOM_ID, LinuxGraphicsMode, LinuxTerminalId, + OperationResult, RepoOpenBehaviour, RowStriping, Settings, ThemeMode, + normalise_linux_terminal_id, }; use crate::{AppState, configure_command, git_command}; use reqwest::header::{ACCEPT, HeaderValue, RANGE}; @@ -1130,8 +1131,8 @@ pub fn set_linux_graphics_mode( #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct LinuxTerminalOption { - pub emulator: LinuxTerminalEmulator, - pub label: &'static str, + pub id: LinuxTerminalId, + pub label: String, } #[tauri::command] @@ -1141,74 +1142,52 @@ pub fn get_linux_terminal_options() -> Vec { #[cfg(target_os = "linux")] fn linux_terminal_options() -> Vec { + let registry = linux_terminal_launch::TerminalRegistry::with_known_terminals(); let mut options = vec![LinuxTerminalOption { - emulator: LinuxTerminalEmulator::Auto, - label: linux_terminal_launch::terminal_label( - linux_terminal_launch::TerminalPreference::Auto, - ), + id: LINUX_TERMINAL_AUTO_ID.to_string(), + label: registry.label_for_preference(&linux_terminal_launch::TerminalPreference::Auto), }]; options.extend( - linux_terminal_launch::known_terminals() + registry + .definitions() .iter() .map(|terminal| LinuxTerminalOption { - emulator: linux_terminal_emulator(terminal.terminal), - label: terminal.label, + id: terminal.id().to_string(), + label: terminal.label().to_string(), }), ); options.push(LinuxTerminalOption { - emulator: LinuxTerminalEmulator::Custom, - label: linux_terminal_launch::terminal_label( - linux_terminal_launch::TerminalPreference::Custom, - ), + id: LINUX_TERMINAL_CUSTOM_ID.to_string(), + label: registry.label_for_preference(&linux_terminal_launch::TerminalPreference::Custom), }); options } -#[cfg(target_os = "linux")] -fn linux_terminal_emulator( - terminal: linux_terminal_launch::KnownTerminal, -) -> LinuxTerminalEmulator { - match terminal { - linux_terminal_launch::KnownTerminal::Konsole => LinuxTerminalEmulator::Konsole, - linux_terminal_launch::KnownTerminal::GnomeTerminal => LinuxTerminalEmulator::GnomeTerminal, - linux_terminal_launch::KnownTerminal::GnomeConsole => LinuxTerminalEmulator::GnomeConsole, - linux_terminal_launch::KnownTerminal::Xfce4Terminal => LinuxTerminalEmulator::Xfce4Terminal, - linux_terminal_launch::KnownTerminal::MateTerminal => LinuxTerminalEmulator::MateTerminal, - linux_terminal_launch::KnownTerminal::Lxterminal => LinuxTerminalEmulator::Lxterminal, - linux_terminal_launch::KnownTerminal::Alacritty => LinuxTerminalEmulator::Alacritty, - linux_terminal_launch::KnownTerminal::Ghostty => LinuxTerminalEmulator::Ghostty, - linux_terminal_launch::KnownTerminal::Kitty => LinuxTerminalEmulator::Kitty, - linux_terminal_launch::KnownTerminal::WezTerm => LinuxTerminalEmulator::WezTerm, - linux_terminal_launch::KnownTerminal::Foot => LinuxTerminalEmulator::Foot, - linux_terminal_launch::KnownTerminal::Xterm => LinuxTerminalEmulator::Xterm, - } -} - #[cfg(not(target_os = "linux"))] fn linux_terminal_options() -> Vec { vec![ LinuxTerminalOption { - emulator: LinuxTerminalEmulator::Auto, - label: "Terminal", + id: LINUX_TERMINAL_AUTO_ID.to_string(), + label: "Terminal".to_string(), }, LinuxTerminalOption { - emulator: LinuxTerminalEmulator::Custom, - label: "Terminal", + id: LINUX_TERMINAL_CUSTOM_ID.to_string(), + label: "Terminal".to_string(), }, ] } #[tauri::command] pub fn set_linux_terminal_emulator( - linux_terminal_emulator: LinuxTerminalEmulator, + linux_terminal_emulator: LinuxTerminalId, state: tauri::State<'_, AppState>, ) -> Settings { state .git_service - .set_linux_terminal_emulator(linux_terminal_emulator) + .set_linux_terminal_emulator(normalise_linux_terminal_id(&linux_terminal_emulator)) } #[tauri::command] diff --git a/src-tauri/src/config_file.rs b/src-tauri/src/config_file.rs index 7000d9e..d83984a 100644 --- a/src-tauri/src/config_file.rs +++ b/src-tauri/src/config_file.rs @@ -212,6 +212,33 @@ mod tests { assert_eq!(settings.ui_text_scale, 1.2); } + #[test] + fn load_toml_normalises_old_linux_terminal_values() { + let dir = TempDir::new().unwrap(); + let toml_path = dir.path().join("config.toml"); + let json_path = dir.path().join("config.json"); + + write_file(&toml_path, "linuxTerminalEmulator = \"GnomeTerminal\"\n"); + + let (settings, should_persist) = load_or_migrate(&toml_path, &json_path); + assert!(!should_persist); + assert_eq!(settings.linux_terminal_emulator, "gnome-terminal"); + } + + #[test] + fn load_toml_defaults_empty_linux_terminal_value() { + let settings: Settings = toml::from_str("linuxTerminalEmulator = \"\"\n").unwrap(); + + assert_eq!(settings.linux_terminal_emulator, "auto"); + } + + #[test] + fn load_toml_preserves_unknown_linux_terminal_id() { + let settings: Settings = toml::from_str("linuxTerminalEmulator = \"coolterm\"\n").unwrap(); + + assert_eq!(settings.linux_terminal_emulator, "coolterm"); + } + #[test] fn load_toml_ignores_old_ms_store_update_flow_config() { let dir = TempDir::new().unwrap(); diff --git a/src-tauri/src/git/cli.rs b/src-tauri/src/git/cli.rs index 744e9d0..d68d1ea 100644 --- a/src-tauri/src/git/cli.rs +++ b/src-tauri/src/git/cli.rs @@ -2907,11 +2907,13 @@ impl GitOperationHandler for CliGitHandler { "--date=iso-strict", pretty.as_str(), ]; + if request.topo_order || request.scope == CommitLogScope::AllRefs { + args.push("--topo-order"); + } if request.scope == CommitLogScope::AllRefs { args.push("--branches"); args.push("--tags"); args.push("--remotes"); - args.push("--topo-order"); } let output = match Self::run_git(args.as_slice(), Some(&repo_path)) { diff --git a/src-tauri/src/git/gix_handler.rs b/src-tauri/src/git/gix_handler.rs index e64f0fc..805ed26 100644 --- a/src-tauri/src/git/gix_handler.rs +++ b/src-tauri/src/git/gix_handler.rs @@ -1156,7 +1156,7 @@ impl GitOperationHandler for GixGitHandler { &self, request: &CommitHistoryRequest, ) -> GitResult> { - if request.scope == CommitLogScope::AllRefs { + if request.topo_order || request.scope == CommitLogScope::AllRefs { return self.cli_fallback.get_commit_history(request); } diff --git a/src-tauri/src/git/handler.rs b/src-tauri/src/git/handler.rs index fceec63..5e30744 100644 --- a/src-tauri/src/git/handler.rs +++ b/src-tauri/src/git/handler.rs @@ -352,12 +352,9 @@ impl GitService { }) } - pub fn set_linux_terminal_emulator( - &self, - emulator: super::types::LinuxTerminalEmulator, - ) -> Settings { + pub fn set_linux_terminal_emulator(&self, id: super::types::LinuxTerminalId) -> Settings { self.update_settings(|settings| { - settings.linux_terminal_emulator = emulator; + settings.linux_terminal_emulator = id; }) } diff --git a/src-tauri/src/git/types.rs b/src-tauri/src/git/types.rs index 15a2fa8..f3953c3 100644 --- a/src-tauri/src/git/types.rs +++ b/src-tauri/src/git/types.rs @@ -112,29 +112,10 @@ impl Default for LinuxGraphicsMode { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub enum LinuxTerminalEmulator { - Auto, - Konsole, - GnomeTerminal, - GnomeConsole, - Xfce4Terminal, - MateTerminal, - Lxterminal, - Alacritty, - Ghostty, - Kitty, - WezTerm, - Foot, - Xterm, - Custom, -} - -impl Default for LinuxTerminalEmulator { - fn default() -> Self { - Self::Auto - } -} +pub type LinuxTerminalId = String; + +pub const LINUX_TERMINAL_AUTO_ID: &str = "auto"; +pub const LINUX_TERMINAL_CUSTOM_ID: &str = "custom"; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum RepoOpenBehaviour { @@ -227,6 +208,38 @@ where Ok(normalise_ui_text_scale(scale)) } +fn default_linux_terminal_id() -> LinuxTerminalId { + LINUX_TERMINAL_AUTO_ID.to_string() +} + +pub fn normalise_linux_terminal_id(value: &str) -> LinuxTerminalId { + match value.trim() { + "" | "Auto" | "auto" => LINUX_TERMINAL_AUTO_ID.to_string(), + "Custom" | "custom" => LINUX_TERMINAL_CUSTOM_ID.to_string(), + "Konsole" | "konsole" => "konsole".to_string(), + "GnomeTerminal" | "gnome-terminal" => "gnome-terminal".to_string(), + "GnomeConsole" | "gnome-console" => "gnome-console".to_string(), + "Xfce4Terminal" | "xfce4-terminal" => "xfce4-terminal".to_string(), + "MateTerminal" | "mate-terminal" => "mate-terminal".to_string(), + "Lxterminal" | "lxterminal" => "lxterminal".to_string(), + "Alacritty" | "alacritty" => "alacritty".to_string(), + "Ghostty" | "ghostty" => "ghostty".to_string(), + "Kitty" | "kitty" => "kitty".to_string(), + "WezTerm" | "wezterm" => "wezterm".to_string(), + "Foot" | "foot" => "foot".to_string(), + "Xterm" | "xterm" => "xterm".to_string(), + other => other.to_string(), + } +} + +fn deserialise_linux_terminal_id<'de, D>(deserialiser: D) -> Result +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserialiser)?; + Ok(normalise_linux_terminal_id(&value)) +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct Settings { @@ -279,8 +292,11 @@ pub struct Settings { pub update_endpoint: String, #[serde(default)] pub linux_graphics_mode: LinuxGraphicsMode, - #[serde(default)] - pub linux_terminal_emulator: LinuxTerminalEmulator, + #[serde( + default = "default_linux_terminal_id", + deserialize_with = "deserialise_linux_terminal_id" + )] + pub linux_terminal_emulator: LinuxTerminalId, #[serde(default)] pub linux_terminal_custom_command: String, #[serde(default)] @@ -352,7 +368,7 @@ impl Default for Settings { auto_install_updates: false, update_endpoint: Self::default_update_endpoint(), linux_graphics_mode: LinuxGraphicsMode::Auto, - linux_terminal_emulator: LinuxTerminalEmulator::Auto, + linux_terminal_emulator: default_linux_terminal_id(), linux_terminal_custom_command: String::new(), repo_open_behaviour: RepoOpenBehaviour::Ask, git_executable_path: String::new(), @@ -439,6 +455,8 @@ pub struct CommitHistoryRequest { pub commit_date_mode: CommitDateMode, #[serde(default)] pub scope: CommitLogScope, + #[serde(default)] + pub topo_order: bool, } #[derive(Debug, Clone, Deserialize)] diff --git a/src-tauri/tests/git.rs b/src-tauri/tests/git.rs index 5dba63f..23814c3 100644 --- a/src-tauri/tests/git.rs +++ b/src-tauri/tests/git.rs @@ -190,6 +190,7 @@ fn history_request(dir: &TempDir) -> CommitHistoryRequest { offset: None, commit_date_mode: Default::default(), scope: Default::default(), + topo_order: false, } } @@ -1190,6 +1191,7 @@ fn commit_creates_entry_in_log() { offset: None, commit_date_mode: Default::default(), scope: Default::default(), + topo_order: false, }) .expect("get_commit_history"); assert!(commits.iter().any(|c| c.message == "add b.txt")); @@ -1564,6 +1566,7 @@ fn commit_history_all_refs_includes_branch_outside_detached_head() { offset: None, commit_date_mode: Default::default(), scope: CommitLogScope::CurrentCheckout, + topo_order: false, }) .expect("get current checkout history"); @@ -1575,6 +1578,7 @@ fn commit_history_all_refs_includes_branch_outside_detached_head() { offset: None, commit_date_mode: Default::default(), scope: CommitLogScope::AllRefs, + topo_order: false, }) .expect("get all refs history"); @@ -1630,6 +1634,7 @@ fn assert_commit_history_includes_merge_parents(handler: impl GitOperationHandle offset: None, commit_date_mode: Default::default(), scope: CommitLogScope::CurrentCheckout, + topo_order: false, }) .expect("get commit history"); @@ -1672,6 +1677,7 @@ fn assert_commit_history_includes_ref_decorations(handler: impl GitOperationHand offset: None, commit_date_mode: Default::default(), scope: CommitLogScope::CurrentCheckout, + topo_order: false, }) .expect("get commit history"); @@ -1791,6 +1797,7 @@ fn commit_history_respects_limit() { offset: None, commit_date_mode: Default::default(), scope: Default::default(), + topo_order: false, }) .expect("get_commit_history"); assert_eq!(commits.len(), 3); @@ -1812,6 +1819,7 @@ fn commit_history_returns_commits_newest_first() { offset: None, commit_date_mode: Default::default(), scope: Default::default(), + topo_order: false, }) .expect("get_commit_history"); assert_eq!(commits[0].message, "third"); @@ -1952,15 +1960,18 @@ fn gix_commit_history_matches_git_log_order_for_merge_commits() { .map(ToString::to_string) .collect(); + let history_request = |topo_order| CommitHistoryRequest { + repo_path: dir.path().to_str().unwrap().to_string(), + limit: Some(5), + after_hash: None, + offset: None, + commit_date_mode: Default::default(), + scope: Default::default(), + topo_order, + }; + let commits = gix_handler() - .get_commit_history(&CommitHistoryRequest { - repo_path: dir.path().to_str().unwrap().to_string(), - limit: Some(5), - after_hash: None, - offset: None, - commit_date_mode: Default::default(), - scope: Default::default(), - }) + .get_commit_history(&history_request(false)) .expect("get gix commit history"); let actual_messages: Vec = commits .iter() @@ -1968,6 +1979,30 @@ fn gix_commit_history_matches_git_log_order_for_merge_commits() { .collect(); assert_eq!(actual_messages, expected_messages); + + let expected_topo_messages: Vec = + git_stdout(dir.path(), &["log", "--topo-order", "-5", "--format=%s"]) + .lines() + .map(ToString::to_string) + .collect(); + + let cli_topo_commits = handler() + .get_commit_history(&history_request(true)) + .expect("get cli topo commit history"); + let cli_topo_messages: Vec = cli_topo_commits + .iter() + .map(|commit| commit.message.clone()) + .collect(); + assert_eq!(cli_topo_messages, expected_topo_messages); + + let gix_topo_commits = gix_handler() + .get_commit_history(&history_request(true)) + .expect("get gix topo commit history"); + let gix_topo_messages: Vec = gix_topo_commits + .iter() + .map(|commit| commit.message.clone()) + .collect(); + assert_eq!(gix_topo_messages, expected_topo_messages); } // commit_details: CLI handler diff --git a/src/api/commands.ts b/src/api/commands.ts index 9451dab..8b4d0e5 100644 --- a/src/api/commands.ts +++ b/src/api/commands.ts @@ -54,7 +54,7 @@ import type { TagInfo, BackendMode, LinuxTerminalOption, - LinuxTerminalEmulator, + LinuxTerminalId, ThemeMode, ThemeBundle, UiTextScale, @@ -140,8 +140,9 @@ export function getCommitHistory( afterHash?: string, offset?: number, scope?: CommitLogScope, + topoOrder?: boolean, ): Promise { - return invoke("get_commit_history", {request: {repoPath, limit, afterHash, offset, scope}}); + return invoke("get_commit_history", {request: {repoPath, limit, afterHash, offset, scope, topoOrder}}); } export function verifyCommits(repoPath: string, hashes: string[]): Promise { @@ -501,7 +502,7 @@ export function getLinuxTerminalOptions(): Promise { return invoke("get_linux_terminal_options"); } -export function setLinuxTerminalEmulator(linuxTerminalEmulator: LinuxTerminalEmulator): Promise { +export function setLinuxTerminalEmulator(linuxTerminalEmulator: LinuxTerminalId): Promise { return invoke("set_linux_terminal_emulator", {linuxTerminalEmulator}); } diff --git a/src/components/ProjectView.tsx b/src/components/ProjectView.tsx index 8190623..0da9850 100644 --- a/src/components/ProjectView.tsx +++ b/src/components/ProjectView.tsx @@ -83,6 +83,16 @@ const EMPTY_COMMIT_MARKERS: CommitMarkers = { upstreamRef: null, }; +const SHOW_COMMIT_GRAPH_KEY = "gitmun.showCommitGraph"; + +function readShowCommitGraphPreference(): boolean { + try { + return localStorage.getItem(SHOW_COMMIT_GRAPH_KEY) === "true"; + } catch { + return false; + } +} + function isStagingOperationKind(kind: LongRunningOperationKind): kind is StagingOperationKind { return kind === "stage" || kind === "stageAll" || kind === "unstage" || kind === "unstageAll"; } @@ -393,6 +403,7 @@ export function ProjectView({ const [wrapDiffLines, setWrapDiffLines] = useState(false); const [rowStriping, setRowStriping] = useState("Off"); const [showCommitGraphButton, setShowCommitGraphButton] = useState(false); + const [showCommitGraph, setShowCommitGraph] = useState(readShowCommitGraphPreference); const [searchQuery, setSearchQuery] = useState(""); const [windowFocused, setWindowFocused] = useState(() => ( typeof document === "undefined" ? true : document.hasFocus() @@ -415,6 +426,9 @@ export function ProjectView({ const initials = (displayName ?? "?") .split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase(); const [identityAvatar, setIdentityAvatar] = useState(null); + const handleCommitGraphVisibilityChange = useCallback((visible: boolean) => { + setShowCommitGraph(visible); + }, []); const currentBranch = status?.currentBranch ?? null; const { @@ -427,7 +441,7 @@ export function ProjectView({ loadMoreError: logLoadMoreError, pageSize: logPageSize, refresh: refreshLog, - } = useGitLog(repoPath, logScope, windowFocused); + } = useGitLog(repoPath, logScope, windowFocused, showCommitGraphButton && showCommitGraph); const stagedFiles = status?.stagedFiles ?? []; const unstagedFiles = status?.changedFiles ?? []; const unversionedFiles = status?.unversionedFiles ?? []; @@ -2623,6 +2637,7 @@ export function ProjectView({ logScope={logScope} rowStriping={rowStriping} showCommitGraphButton={showCommitGraphButton} + onCommitGraphVisibilityChange={handleCommitGraphVisibilityChange} onLogScopeChange={setLogScope} detachedHead={status?.detachedHead ?? false} shallow={status?.shallow ?? false} diff --git a/src/components/Titlebar.test.tsx b/src/components/Titlebar.test.tsx index 24b4a13..48e254a 100644 --- a/src/components/Titlebar.test.tsx +++ b/src/components/Titlebar.test.tsx @@ -165,6 +165,25 @@ describe("Titlebar", () => { expect(within(disclosure).getByText("/home/conor/GitmunProjects/gitmun")).toBeInTheDocument(); }); + it("shows the full project name when the repository button clips the label", () => { + const projectName = "Project Atlas With A Long Display Name"; + renderTitlebar([makeBranch()], "Push", "/home/conor/GitmunProjects/gitmun", vi.fn(), { + repoDisplayName: projectName, + }); + + const repoButton = screen.getByLabelText("Copy repository path"); + const repoName = screen.getByText(projectName); + setRenderedWidth(repoName, 180, 180); + setRenderedWidth(repoButton, 320, 200); + fireEvent.mouseEnter(repoButton); + + const disclosure = screen.getByRole("tooltip"); + expect(within(disclosure).getByText("Project")).toBeInTheDocument(); + expect(within(disclosure).getByText(projectName)).toBeInTheDocument(); + expect(within(disclosure).getByText("Repository path")).toBeInTheDocument(); + expect(within(disclosure).getByText("/home/conor/GitmunProjects/gitmun")).toBeInTheDocument(); + }); + it("keeps the project copy affordance without repeating the project name when the label fits", () => { renderTitlebar([makeBranch()], "Push", "/home/conor/GitmunProjects/gitmun", vi.fn(), { repoDisplayName: "Project Atlas", diff --git a/src/components/Titlebar.tsx b/src/components/Titlebar.tsx index 437f718..a26f0e4 100644 --- a/src/components/Titlebar.tsx +++ b/src/components/Titlebar.tsx @@ -62,6 +62,7 @@ export function Titlebar({ const [searchFocused, setSearchFocused] = useState(false); const [repoPathCopied, setRepoPathCopied] = useState(false); const [openDisclosure, setOpenDisclosure] = useState<{ kind: "repo" | "branch"; truncated: boolean } | null>(null); + const repoButtonRef = useRef(null); const repoNameRef = useRef(null); const branchNameRef = useRef(null); const repoPathCopiedTimerRef = useRef | null>(null); @@ -82,7 +83,10 @@ export function Titlebar({ }; const showRepoDisclosure = () => { - setOpenDisclosure({ kind: "repo", truncated: isOverflowing(repoNameRef.current) }); + setOpenDisclosure({ + kind: "repo", + truncated: isOverflowing(repoNameRef.current) || isOverflowing(repoButtonRef.current), + }); }; const showBranchDisclosure = () => { @@ -132,6 +136,7 @@ export function Titlebar({