Phases 0-1 + Sprint A: module split, CLI layer, org-refresh fix, hygiene

- LICENSE (MIT) + README truth-pass; docs/cli.md CLI contract
- CLI subcommands (src/cli_cmds/) per "TUI for users, CLI for users+AI"
- Hub/detail refactor: keymap dispatch, modal split, action enum, BgMsg bus
- Fix: org repos were never refreshed (owner==username match); refresh now
  parallel — 49 repos in ~11s, surfaced 70 previously invisible PRs
- Fix: 3 date bugs in detail/git.rs (format-local, Local::now, 1y month math)
- Stash indicator + [m] actions, session persistence, gh CLI token fallback
- Legacy UI modules removed; plan/ docs (roadmap, audit, debate, handoff)
This commit is contained in:
dualfroz committed 2026-07-06 14:42:52 +02:00
1 parent 5787b14880
commit 0468c032c6
100 files changed
+11614 -9840

No files matched your search

+4
View File
@@ -1,3 +1,7 @@
//! Clipboard writers: a configured-command variant (`copy_to_clipboard`,
//! used when the user has set `clipboard_cmd` in config) and an
//! auto-detecting fallback (`copy_auto`) that tries the common Linux tools.
use std::io::Write as _;
/// Copy text using a configured command string (e.g. "wl-copy" or "xclip -selection clipboard").
+12
View File
@@ -1,3 +1,12 @@
//! Thin wrappers around invoking `git` as a subprocess against a given
//! repo path. Used instead of a git library for commands where shelling out
//! to the user's actual `git` binary (with their real config, credential
//! helpers, hooks) is exactly the desired behavior.
/// Runs `git <args>` against `path`, inheriting stdio so output reaches the
/// terminal directly. On failure, prints a short summary instead of
/// propagating an error — most call sites treat a failed git command as
/// "nothing changed" rather than something to abort the TUI over.
pub fn git_run(path: &std::path::Path, args: &[&str]) {
match std::process::Command::new("git").arg("-C").arg(path).args(args).status() {
Ok(s) if s.success() => {}
@@ -6,6 +15,9 @@ pub fn git_run(path: &std::path::Path, args: &[&str]) {
}
}
/// Like `git_run`, but silent and returns success/failure instead of
/// printing — used where the caller wants to branch on the result rather
/// than just report it.
pub fn git_run_ok(path: &std::path::Path, args: &[&str]) -> bool {
std::process::Command::new("git").arg("-C").arg(path).args(args)
.status().map(|s| s.success()).unwrap_or(false)
+65
View File
@@ -1,7 +1,39 @@
//! Small, dependency-free helpers shared across the rest of the crate:
//! recursive file walking, opening paths in the system's default app, and
//! detecting which editor/clipboard tools are actually installed. Clipboard
//! and URL-rewriting logic each get their own submodule since both have
//! enough provider/platform-specific branching to be worth isolating.
pub mod clipboard;
pub mod git;
pub mod url;
/// Directories never walked when scanning a repo's source files — vendored
/// or generated content that must never be touched by rename/find-usages
/// (the whole reason codeintel uses tree-sitter over naive find/replace is
/// to stay correct, but staying out of dependency code entirely is the
/// first line of defense). Mirrors the exclusion list `legacy::refactor`
/// already used for the TODO scanner.
pub const EXCLUDED_DIRS: &[&str] = &[
".git", "target", "node_modules", ".next", "dist", "build", ".venv", "vendor",
];
/// Recursively visits every file under `root`, skipping `EXCLUDED_DIRS`.
pub fn walk_files<F: FnMut(&std::path::Path)>(root: &std::path::Path, cb: &mut F) {
let Ok(entries) = std::fs::read_dir(root) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if !EXCLUDED_DIRS.contains(&name.as_ref()) {
walk_files(&path, cb);
}
} else {
cb(&path);
}
}
}
/// Open a file, directory, or URL with the system default application.
pub fn open_path(target: &str) {
#[cfg(target_os = "macos")]
@@ -11,3 +43,36 @@ pub fn open_path(target: &str) {
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = std::process::Command::new("xdg-open").arg(target).spawn();
}
/// Whether `cmd` resolves to something on PATH, used to filter the
/// editor/clipboard-tool detection lists down to what's actually installed.
fn command_exists(cmd: &str) -> bool {
#[cfg(target_os = "windows")]
let finder = "where";
#[cfg(not(target_os = "windows"))]
let finder = "which";
std::process::Command::new(finder).arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Editors actually present on this machine's PATH, in a sensible default order.
pub fn detect_editors() -> Vec<String> {
["code", "nvim", "vim", "nano", "hx", "micro", "subl", "zed"]
.into_iter()
.filter(|c| command_exists(c))
.map(str::to_string)
.collect()
}
/// Clipboard tools actually present on this machine's PATH.
pub fn detect_clipboard_tools() -> Vec<String> {
let mut found = vec![];
for cmd in &["wl-copy", "xclip", "pbcopy"] {
if command_exists(cmd) {
found.push(if *cmd == "xclip" { "xclip -selection clipboard".to_string() } else { cmd.to_string() });
}
}
found
}
+8 -1
View File
@@ -1,7 +1,14 @@
//! Converts git remote URLs (SSH or HTTPS) into the browser-facing web URL
//! for the same repo, used by every "open in browser" action.
/// Rewrites `git_url` (`git@host:owner/repo.git` or an HTTPS clone URL) into
/// `https://host/owner/repo`. Already-HTTPS URLs are returned with just the
/// `.git` suffix trimmed; anything that doesn't look like either form is
/// returned unchanged rather than guessed at.
pub fn to_web_url(git_url: &str) -> String {
if git_url.starts_with("http") { return git_url.trim_end_matches(".git").to_string(); }
if let Some(colon) = git_url.rfind(':') {
let host = git_url[..colon].split('@').last().unwrap_or("");
let host = git_url[..colon].split('@').next_back().unwrap_or("");
let path = git_url[colon + 1..].trim_end_matches(".git");
return format!("https://{}/{}", host, path);
}