diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 563c8fe..07f2141 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1,5 +1,5 @@ use serde::Serialize; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; #[derive(Serialize)] @@ -130,3 +130,45 @@ pub fn load(dir: &Path) -> Result { Ok(RepoData { root, commits, refs, head_ref, head }) } + +/// The repository to open at start: the path given on the command line, or else the +/// current folder if it is inside a repository. `None` means gites starts with its +/// welcome screen. A path given on the command line is used even if it is not a +/// repository, so the window can say what is wrong with it. +pub fn start_dir(arg: Option, cwd: PathBuf) -> Option { + arg.or_else(|| git(&cwd, &["rev-parse", "--git-dir"]).ok().map(|_| cwd)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn temp_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("gites-start-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn opens_the_path_given_on_the_command_line_even_if_it_is_not_a_repository() { + let given = PathBuf::from("/no/such/folder"); + assert_eq!(start_dir(Some(given.clone()), temp_dir("arg")), Some(given)); + } + + #[test] + fn opens_the_current_folder_when_it_is_inside_a_repository() { + let dir = temp_dir("repo"); + git(&dir, &["init", "-q"]).unwrap(); + std::fs::create_dir_all(dir.join("sub")).unwrap(); + assert_eq!(start_dir(None, dir.join("sub")), Some(dir.join("sub"))); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn opens_nothing_when_the_current_folder_is_not_in_a_repository() { + let dir = temp_dir("plain"); + assert_eq!(start_dir(None, dir.clone()), None); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fd985f2..238c5eb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,32 +7,36 @@ mod settings; use std::path::PathBuf; use std::sync::Mutex; -/// The open repository folder. It starts as the first command line argument, or the -/// current folder, and changes when the user opens another one. -struct RepoDir(Mutex); +/// The open repository folder, from `git::start_dir`; it changes when the user opens +/// another one. `None` until a repository is open: the window shows its welcome screen. +struct RepoDir(Mutex>); impl RepoDir { - fn get(&self) -> PathBuf { - self.0.lock().unwrap_or_else(|e| e.into_inner()).clone() + fn get(&self) -> Result { + self.0.lock().unwrap_or_else(|e| e.into_inner()).clone().ok_or_else(|| "no repository is open".into()) } } +/// The open repository, or `None` when there is none yet. #[tauri::command] -fn load_repo(dir: tauri::State) -> Result { - git::load(&dir.get()) +fn load_repo(dir: tauri::State) -> Result, String> { + match dir.get() { + Ok(d) => git::load(&d).map(Some), + Err(_) => Ok(None), + } } /// Switches to another repository. The current one stays open if `path` is not a repository. #[tauri::command] fn open_repo(dir: tauri::State, path: String) -> Result { let data = git::load(&PathBuf::from(&path))?; - *dir.0.lock().unwrap_or_else(|e| e.into_inner()) = PathBuf::from(&path); + *dir.0.lock().unwrap_or_else(|e| e.into_inner()) = Some(PathBuf::from(&path)); Ok(data) } #[tauri::command] fn commit_details(dir: tauri::State, hash: String) -> Result { - details::load(&dir.get(), &hash) + details::load(&dir.get()?, &hash) } #[tauri::command] @@ -43,7 +47,7 @@ fn file_diff( old_path: Option, force: bool, ) -> Result { - diff::load(&dir.get(), &hash, &path, old_path.as_deref(), force) + diff::load(&dir.get()?, &hash, &path, old_path.as_deref(), force) } #[tauri::command] @@ -68,10 +72,9 @@ fn save_settings(value: serde_json::Value) -> Result<(), String> { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let path = std::env::args_os() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); + let arg = std::env::args_os().nth(1).map(PathBuf::from); + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let path = git::start_dir(arg, cwd); tauri::Builder::default() .plugin(tauri_plugin_clipboard_manager::init()) diff --git a/src/App.vue b/src/App.vue index 153c343..94ad62f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,6 +19,7 @@ import LabelStack from "./components/LabelStack.vue"; import PreviewPanel from "./components/PreviewPanel.vue"; import RepoMenu from "./components/RepoMenu.vue"; import Sidebar from "./components/Sidebar.vue"; +import Welcome from "./components/Welcome.vue"; const ROW_H = 28; const MESSAGE_FONT = '13px "Inter", "Noto Sans", system-ui, sans-serif'; @@ -106,17 +107,22 @@ function startDrag(e: PointerEvent, axis: "x" | "y") { let filtersLoadedFor: string | null = null; let loading = false; +/** False until the first load answers, so the welcome screen does not flash up at start. */ +const started = ref(false); async function load() { // The window gets focus often; one reload at a time is enough. if (loading) return; loading = true; try { - await applyRepo(await invoke("load_repo")); + // null: gites was started outside a repository and none is open yet. + const data = await invoke("load_repo"); + if (data) await applyRepo(data); } catch (e) { error.value = String(e); } finally { loading = false; + started.value = true; } } @@ -536,6 +542,14 @@ onUnmounted(() => { + +
+defineProps<{ recents: string[] }>(); +const emit = defineEmits<{ pick: [path: string]; browse: []; remove: [path: string] }>(); + +// Paths on Windows use backslashes. +const name = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p; + + + + +