Prompt: --glyphs and --color modes with %s %U %O %i placeholders; oh-my-bash powerline segment in contrib/oh-my-bash/oxy.sh 694805cca585
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-27 11:13 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
694805cca58503671435845e88c640c2b18010c87f0d041ad316c40bad385d01- Depends on
- 135212de5370 28fc23d7706a 50a046e439e9 6d3eefbce2f0 7a716eb94dee 96f7610fa666 982864068a59 fce44ae9ad29
- Depended on by
- d42369abf180 75167672ebcd c6aa5bd9d1c8 957fee30826e
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 23 atoms, 3 tree operations, 7 files
Browse the tree at this change
added contrib/oh-my-bash/oxy.sh +40 −0
| 1 | + | #! bash oh-my-bash.module |
|
| 2 | + | # Oxygen segment for the oh-my-bash powerline themes. |
|
| 3 | + | # |
|
| 4 | + | # Install: copy this file to "$OSH_CUSTOM/oxy.sh" (default ~/.oh-my-bash/custom/) |
|
| 5 | + | # and open a new shell. Custom files load before the theme, so the |
|
| 6 | + | # POWERLINE_PROMPT default below adds the "oxy" segment next to the Git one; |
|
| 7 | + | # set POWERLINE_PROMPT yourself to arrange segments differently. |
|
| 8 | + | # |
|
| 9 | + | # The segment shows: icon, bookmark, working-copy change id, a state glyph |
|
| 10 | + | # (✔ clean, ✎ uncommitted changes, ‼ conflicts), and ↑n/↓n against origin. |
|
| 11 | + | # Its colour follows the state. It costs one `oxy prompt` call, a few |
|
| 12 | + | # milliseconds, and prints nothing outside an Oxygen repository. |
|
| 13 | + | ||
| 14 | + | : "${POWERLINE_PROMPT:=user_info scm oxy python_venv ruby cwd}" |
|
| 15 | + | ||
| 16 | + | OXY_CHAR=${POWERLINE_OXY_CHAR:="⚛ "} |
|
| 17 | + | OXY_THEME_PROMPT_CLEAN_COLOR=${OXY_THEME_PROMPT_CLEAN_COLOR:=25} |
|
| 18 | + | OXY_THEME_PROMPT_DIRTY_COLOR=${OXY_THEME_PROMPT_DIRTY_COLOR:=88} |
|
| 19 | + | OXY_THEME_PROMPT_CONFLICT_COLOR=${OXY_THEME_PROMPT_CONFLICT_COLOR:=160} |
|
| 20 | + | OXY_THEME_PROMPT_CLEAN_GLYPH=${OXY_THEME_PROMPT_CLEAN_GLYPH:="✔"} |
|
| 21 | + | OXY_THEME_PROMPT_DIRTY_GLYPH=${OXY_THEME_PROMPT_DIRTY_GLYPH:="✎"} |
|
| 22 | + | OXY_THEME_PROMPT_CONFLICT_GLYPH=${OXY_THEME_PROMPT_CONFLICT_GLYPH:="‼"} |
|
| 23 | + | ||
| 24 | + | function __powerline_oxy_prompt { |
|
| 25 | + | command -v oxy >/dev/null 2>&1 || return |
|
| 26 | + | local info |
|
| 27 | + | info=$(oxy prompt --format '%b|%c|%d|%x|%U|%O' 2>/dev/null) || return |
|
| 28 | + | [[ -n "$info" ]] || return |
|
| 29 | + | local bookmark change dirty conflict up down |
|
| 30 | + | IFS='|' read -r bookmark change dirty conflict up down <<< "$info" |
|
| 31 | + | local color glyph |
|
| 32 | + | if [[ -n "$conflict" ]]; then |
|
| 33 | + | color=$OXY_THEME_PROMPT_CONFLICT_COLOR glyph=$OXY_THEME_PROMPT_CONFLICT_GLYPH |
|
| 34 | + | elif [[ -n "$dirty" ]]; then |
|
| 35 | + | color=$OXY_THEME_PROMPT_DIRTY_COLOR glyph=$OXY_THEME_PROMPT_DIRTY_GLYPH |
|
| 36 | + | else |
|
| 37 | + | color=$OXY_THEME_PROMPT_CLEAN_COLOR glyph=$OXY_THEME_PROMPT_CLEAN_GLYPH |
|
| 38 | + | fi |
|
| 39 | + | _omb_util_print "${OXY_CHAR}${bookmark} ${change} ${glyph}${up}${down}|${color}" |
|
| 40 | + | } |
modified crates/oxy-core/src/repo/mod.rs +1 −1
| … | 71 unchanged lines |
||
| 72 | 72 | pub use config::{parse_size, read_config_file, Config}; |
|
| 73 | 73 | pub use gc::GcReport; |
|
| 74 | 74 | pub use hooks::{json_array, json_str, HookOutcome}; |
|
| 75 | - | pub use prompt::{PromptInfo, DEFAULT_PROMPT_FORMAT}; |
|
| 75 | + | pub use prompt::{PromptInfo, DEFAULT_PROMPT_FORMAT, GLYPH_PROMPT_FORMAT}; |
|
| 76 | 76 | pub use git::{GitExportReport, GitImportReport}; |
|
| 77 | 77 | pub use identity::{Rotation, SignatureStatus}; |
|
| 78 | 78 | pub use policy::PolicyUpdate; |
|
| … | 669 unchanged lines |
modified crates/oxy-core/src/repo/prompt.rs +70 −1
| … | 23 unchanged lines |
||
| 24 | 24 | } |
|
| 25 | 25 | ||
| 26 | 26 | impl PromptInfo { |
|
| 27 | + | /// The glyph for the working copy's state: `‼` conflicted, `✎` dirty, |
|
| 28 | + | /// `✔` clean. |
|
| 29 | + | pub fn state_glyph(&self) -> &'static str { |
|
| 30 | + | if self.conflicted { |
|
| 31 | + | "‼" |
|
| 32 | + | } else if self.dirty { |
|
| 33 | + | "✎" |
|
| 34 | + | } else { |
|
| 35 | + | "✔" |
|
| 36 | + | } |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | /// The ANSI colour (SGR parameter) for the state: red, yellow, or cyan. |
|
| 40 | + | pub fn state_color(&self) -> u8 { |
|
| 41 | + | if self.conflicted { |
|
| 42 | + | 31 |
|
| 43 | + | } else if self.dirty { |
|
| 44 | + | 33 |
|
| 45 | + | } else { |
|
| 46 | + | 36 |
|
| 47 | + | } |
|
| 48 | + | } |
|
| 49 | + | ||
| 50 | + | /// `format` wrapped in the state colour. `guard` is `("\\[", "\\]")` for |
|
| 51 | + | /// bash prompts, `("%{", "%}")` for zsh, or empty for raw output. |
|
| 52 | + | pub fn format_colored(&self, fmt: &str, guard: (&str, &str)) -> String { |
|
| 53 | + | format!("{}\x1b[{}m{}{}{}\x1b[0m{}", guard.0, self.state_color(), guard.1, self.format(fmt), guard.0, guard.1) |
|
| 54 | + | } |
|
| 55 | + | ||
| 27 | 56 | /// Expand `%b` bookmark, `%c` change id, `%h` heads, `%d` dirty marker |
|
| 28 | 57 | /// (`*`), `%x` conflict marker (`!`), `%a`/`%B` ahead/behind counts |
|
| 29 | - | /// (empty when unknown), `%A`/`%D` `+n`/`-n` when non-zero, `%%` percent. |
|
| 58 | + | /// (empty when unknown), `%A`/`%D` `+n`/`-n` when non-zero, `%s` the |
|
| 59 | + | /// state glyph, `%U`/`%O` `↑n`/`↓n` when non-zero, `%i` the repository |
|
| 60 | + | /// icon, `%%` percent. |
|
| 30 | 61 | pub fn format(&self, fmt: &str) -> String { |
|
| 31 | 62 | let mut out = String::new(); |
|
| 32 | 63 | let mut chars = fmt.chars(); |
|
| … | 28 unchanged lines |
||
| 61 | 92 | out.push_str(&format!("-{n}")); |
|
| 62 | 93 | } |
|
| 63 | 94 | } |
|
| 95 | + | Some('s') => out.push_str(self.state_glyph()), |
|
| 96 | + | Some('U') => { |
|
| 97 | + | if let Some(n) = self.ahead.filter(|n| *n > 0) { |
|
| 98 | + | out.push_str(&format!("↑{n}")); |
|
| 99 | + | } |
|
| 100 | + | } |
|
| 101 | + | Some('O') => { |
|
| 102 | + | if let Some(n) = self.behind.filter(|n| *n > 0) { |
|
| 103 | + | out.push_str(&format!("↓{n}")); |
|
| 104 | + | } |
|
| 105 | + | } |
|
| 106 | + | Some('i') => out.push_str(ICON), |
|
| 64 | 107 | Some('%') => out.push('%'), |
|
| 65 | 108 | Some(other) => { |
|
| 66 | 109 | out.push('%'); |
|
| … | 29 unchanged lines |
||
| 96 | 139 | } |
|
| 97 | 140 | ||
| 98 | 141 | pub const DEFAULT_PROMPT_FORMAT: &str = "%b %c%d%x%A%D"; |
|
| 142 | + | /// The repository icon for `%i` (an atom, for oxygen). |
|
| 143 | + | pub const ICON: &str = "⚛"; |
|
| 144 | + | /// A format with glyphs: icon, bookmark, change id, state, ahead/behind. |
|
| 145 | + | pub const GLYPH_PROMPT_FORMAT: &str = "%i %b %c %s%U%O"; |
|
| 99 | 146 | ||
| 100 | 147 | /// One manifest line per file: size, mtime in nanoseconds, hash, blob ref |
|
| 101 | 148 | /// or `-`, path. |
|
| … | 168 unchanged lines |
||
| 270 | 317 | Ok(PromptInfo { bookmark, change_id, heads: heads.len(), dirty, conflicted, ahead, behind }) |
|
| 271 | 318 | } |
|
| 272 | 319 | } |
|
| 320 | + | ||
| 321 | + | #[cfg(test)] |
|
| 322 | + | mod tests { |
|
| 323 | + | use super::*; |
|
| 324 | + | ||
| 325 | + | #[test] |
|
| 326 | + | fn placeholders_and_glyphs() { |
|
| 327 | + | let clean = PromptInfo { bookmark: "main".into(), change_id: "658990aa".into(), heads: 1, dirty: false, conflicted: false, ahead: Some(0), behind: None }; |
|
| 328 | + | assert_eq!(clean.format(DEFAULT_PROMPT_FORMAT), "main 658990aa"); |
|
| 329 | + | assert_eq!(clean.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✔"); |
|
| 330 | + | let busy = PromptInfo { dirty: true, ahead: Some(2), behind: Some(3), ..clean.clone() }; |
|
| 331 | + | assert_eq!(busy.format(DEFAULT_PROMPT_FORMAT), "main 658990aa*+2-3"); |
|
| 332 | + | assert_eq!(busy.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✎↑2↓3"); |
|
| 333 | + | let bad = PromptInfo { conflicted: true, ..busy.clone() }; |
|
| 334 | + | assert_eq!(bad.format("%s%x%d %h %a/%B %%"), "‼!* 1 2/3 %"); |
|
| 335 | + | assert_eq!(bad.state_color(), 31); |
|
| 336 | + | assert_eq!(busy.format_colored("%b", ("\\[", "\\]")), "\\[\x1b[33m\\]main\\[\x1b[0m\\]"); |
|
| 337 | + | assert_eq!(clean.format_colored("%b", ("", "")), "\x1b[36mmain\x1b[0m"); |
|
| 338 | + | assert_eq!(clean.format("%q"), "%q"); |
|
| 339 | + | } |
|
| 340 | + | } |
|
| 341 | + |
modified docs/plan.md +2 −1
| … | 15 unchanged lines |
||
| 16 | 16 | - [x] Pack merging (spec §13.4) — done 2026-09-27 00:05; folds packs into one above 16, also after auto-pack. merge packs when more than 16 exist; `oxy gc` reports it. |
|
| 17 | 17 | - [x] Modern CLI presentation (user request, 2026-09-26 23:20) — done 2026-09-27 00:05 via `src/cli/ui.rs` (console + indicatif): colour, spinners on long commands, `--plain`, NO_COLOR and non-TTY detection; status/log/show/commit/checkout/verify/sync/gc restyled. colour and bold for ids, paths, and statuses; a live spinner or updating status line while a command works (snapshot, fetch, push, import, gc); indented, grouped output like Claude Code's. Use `console`/`indicatif` or equivalent; detect a TTY and honour `NO_COLOR` and `--no-color`/`--plain` so pipes and scripts get plain text; keep `--json` where it exists. Touch every command's output for consistency. |
|
| 18 | 18 | - [x] Shell prompt integration (user request, 2026-09-26 23:30) — done 2026-09-27 00:05: `oxy prompt [--format]`, manifest-based dirty check (<10 ms), ahead/behind from tracking, `docs/prompt.md` with bash/oh-my-bash/zsh snippets. `oxy prompt` prints a compact one-line summary for shell prompts in a few milliseconds and with no side effects (no snapshot operation recorded, no locks): bookmark, working-copy ChangeId prefix, head count, a dirty marker (compare the file system against the working copy cheaply: sizes and mtimes first, content only when they differ), conflict marker, and ahead/behind counts against `remotes/origin/<bookmark>` when known. Provide `docs/prompt.md` with an oh-my-bash/bash snippet (`oxy_prompt_info` function, silent outside an oxy repository, colours via `--plain` off) and a zsh variant. Format flags: `--format` with placeholders, `--plain`. |
|
| 19 | - | - [ ] (Pending, for the morning) Ask Andy to complete his cut-off request ("Can we also look at ..."). (GPG question settled: Andy said to leave GPG out.) |
|
| 19 | + | - [x] (Pending, for the morning) Ask Andy to complete his cut-off request ("Can we also look at ..."). (Answered 2026-09-27: it was a stray copy of the prompt request. Done: `oxy prompt --glyphs --color`, new placeholders %s %U %O %i, and an oh-my-bash powerline segment in contrib/oh-my-bash/oxy.sh, installed to ~/.oh-my-bash/custom/oxy.sh; ~/.local/bin/oxy symlinks the release binary. GPG question settled: Andy said to leave GPG out.) |
|
| 20 | 20 | - [x] Lazy blobs and the `objects` endpoint — done 2026-09-27 00:45: `clone --max-blob`, `filter.max-blob`, `.oxy/lazy/<ref>` registry, fetch on first read (checkout, export), server `objects` route; status/commit need no blobs. (spec §13.5, §16.1, §17): `clone --max-blob SIZE`, absent blobs recorded with their remote, fetched on first read; server endpoint; wire encoding in spec §16.3. |
|
| 21 | 21 | - [x] Sparse checkout (spec §17) — done 2026-09-27 01:05: `filter.paths`, `clone --sparse` (sets max-blob 0), `oxy sparse PATH..`, snapshot/checkout/prompt honour scope; out-of-scope blobs never fetched. `filter.paths` in config, `clone --sparse PATH..`, status/commit/checkout honour it. |
|
| 22 | 22 | - [x] Shallow clone (spec §17) — done 2026-09-27 01:50: `clone --depth N` takes the remote's checkpoint plus N levels of history, `.oxy/shallow` boundary, negotiation and imports tolerate it, `oxy deepen`; log works from checkpoint metadata. `clone --depth N`, shallow boundary with a checkpoint, `oxy deepen`. |
|
| … | 16 unchanged lines |
||
| 39 | 39 | ## Log |
|
| 40 | 40 | ||
| 41 | 41 | - 2026-09-26 23:01 plan created. |
|
| 42 | + | - 2026-09-27 12:15 morning: glyph/colour prompt modes and the oh-my-bash powerline segment. 116 tests. |
|
| 42 | 43 | - 2026-09-27 10:00 iteration 18: spec review pass (Draft 0.7) and oxy status --json. 113 tests. |
|
| 43 | 44 | - 2026-09-27 09:35 iteration 17: user guide and protocol document. 112 tests. |
|
| 44 | 45 | - 2026-09-27 09:10 iteration 16: manifest stat cache (status 2.4 s → 4 ms on 1 GiB). 112 tests. |
|
| … | 15 unchanged lines |
modified docs/prompt.md +24 −1
| … | 22 unchanged lines |
||
| 23 | 23 | | `%x` | `!` when the base state has conflicts | |
|
| 24 | 24 | | `%A` / `%D` | `+n` ahead / `-n` behind origin, empty when zero or unknown | |
|
| 25 | 25 | | `%a` / `%B` | raw ahead / behind counts | |
|
| 26 | + | | `%s` | state glyph: `✔` clean, `✎` dirty, `‼` conflicted | |
|
| 27 | + | | `%U` / `%O` | `↑n` ahead / `↓n` behind origin, empty when zero or unknown | |
|
| 28 | + | | `%i` | the repository icon `⚛` | |
|
| 26 | 29 | | `%%` | a percent sign | |
|
| 27 | 30 | ||
| 31 | + | `--glyphs` selects the format `%i %b %c %s%U%O` (`⚛ main 658990aa ✎↑2`), and |
|
| 32 | + | `--color raw|bash|zsh` wraps the line in the state's colour (cyan clean, |
|
| 33 | + | yellow dirty, red conflicted), guarded so bash and zsh measure the prompt |
|
| 34 | + | correctly: |
|
| 35 | + | ||
| 36 | + | ```bash |
|
| 37 | + | PS1='\u@\h \w $(oxy prompt --glyphs --color bash 2>/dev/null)\$ ' |
|
| 38 | + | ``` |
|
| 39 | + | ||
| 28 | 40 | Dirtiness is decided from a manifest written at every snapshot and checkout: |
|
| 29 | 41 | files are compared by size and modification time first and by content hash |
|
| 30 | 42 | only when those differ, so the cost is one directory walk. |
|
| 31 | 43 | ||
| 32 | - | ## bash and oh-my-bash |
|
| 44 | + | ## oh-my-bash powerline themes |
|
| 45 | + | ||
| 46 | + | For the `powerline`, `powerline-icon`, `powerline-multiline`, and |
|
| 47 | + | `powerline-plain` themes, copy [contrib/oh-my-bash/oxy.sh](../contrib/oh-my-bash/oxy.sh) |
|
| 48 | + | to `~/.oh-my-bash/custom/oxy.sh` and open a new shell. It adds an `oxy` |
|
| 49 | + | segment beside the Git one, coloured by state, showing icon, bookmark, |
|
| 50 | + | change id, state glyph, and ahead/behind arrows. The file's variables |
|
| 51 | + | (`POWERLINE_OXY_CHAR`, `OXY_THEME_PROMPT_*_COLOR`, `OXY_THEME_PROMPT_*_GLYPH`) |
|
| 52 | + | can be overridden in `~/.bashrc` before oh-my-bash loads, and |
|
| 53 | + | `POWERLINE_PROMPT` arranges the segments. |
|
| 54 | + | ||
| 55 | + | ## bash and oh-my-bash (other themes) |
|
| 33 | 56 | ||
| 34 | 57 | Add to `~/.bashrc` (or to a custom oh-my-bash theme in |
|
| 35 | 58 | `~/.oh-my-bash/custom/themes/<name>/<name>.theme.sh`): |
|
| … | 47 unchanged lines |
modified src/cli/mod.rs +8 −2
| … | 55 unchanged lines |
||
| 56 | 56 | }, |
|
| 57 | 57 | /// One-line summary for shell prompts; fast and records nothing |
|
| 58 | 58 | Prompt { |
|
| 59 | - | /// Placeholders: %b bookmark, %c change id, %h heads, %d dirty (*), %x conflict (!), %A/%D ahead/behind (+n/-n), %a/%B raw counts |
|
| 59 | + | /// Placeholders: %b bookmark, %c change id, %h heads, %d dirty (*), %x conflict (!), %A/%D ahead/behind (+n/-n), %a/%B raw counts, %s state glyph, %U/%O ahead/behind arrows, %i icon |
|
| 60 | 60 | #[arg(long, default_value = oxy_core::repo::DEFAULT_PROMPT_FORMAT)] |
|
| 61 | 61 | format: String, |
|
| 62 | + | /// Glyph format (%i %b %c %s%U%O) instead of the plain default |
|
| 63 | + | #[arg(long)] |
|
| 64 | + | glyphs: bool, |
|
| 65 | + | /// Colour by state (cyan clean, yellow dirty, red conflicted): raw escapes, or guarded for a bash or zsh PS1 |
|
| 66 | + | #[arg(long, value_parser = ["raw", "bash", "zsh"])] |
|
| 67 | + | color: Option<String>, |
|
| 62 | 68 | }, |
|
| 63 | 69 | /// Record the working copy as a change on the current bookmark |
|
| 64 | 70 | Commit { |
|
| … | 320 unchanged lines |
||
| 385 | 391 | match cli.cmd { |
|
| 386 | 392 | Cmd::Init { path } => workspace::init(path), |
|
| 387 | 393 | Cmd::Status { json } => workspace::status(json), |
|
| 388 | - | Cmd::Prompt { format } => workspace::prompt(&format), |
|
| 394 | + | Cmd::Prompt { format, glyphs, color } => workspace::prompt(&format, glyphs, color.as_deref()), |
|
| 389 | 395 | Cmd::Commit { message, depends_on } => workspace::commit(&message, &depends_on), |
|
| 390 | 396 | Cmd::Log { limit, verbose } => workspace::log(limit, verbose), |
|
| 391 | 397 | Cmd::Diff => workspace::diff(), |
|
| … | 67 unchanged lines |
modified src/cli/workspace.rs +9 −2
| … | 336 unchanged lines |
||
| 337 | 337 | } |
|
| 338 | 338 | ||
| 339 | 339 | /// For shell prompts: silent (exit 1) outside a repository, otherwise one line. |
|
| 340 | - | pub fn prompt(format: &str) -> Result<i32> { |
|
| 340 | + | pub fn prompt(format: &str, glyphs: bool, color: Option<&str>) -> Result<i32> { |
|
| 341 | 341 | let Ok(repo) = Repo::open(&std::env::current_dir()?) else { return Ok(1) }; |
|
| 342 | 342 | let info = repo.quick_status()?; |
|
| 343 | - | println!("{}", info.format(format)); |
|
| 343 | + | let fmt = if glyphs && format == oxy_core::repo::DEFAULT_PROMPT_FORMAT { oxy_core::repo::GLYPH_PROMPT_FORMAT } else { format }; |
|
| 344 | + | let line = match color { |
|
| 345 | + | Some("bash") => info.format_colored(fmt, ("\\[", "\\]")), |
|
| 346 | + | Some("zsh") => info.format_colored(fmt, ("%{", "%}")), |
|
| 347 | + | Some(_) => info.format_colored(fmt, ("", "")), |
|
| 348 | + | None => info.format(fmt), |
|
| 349 | + | }; |
|
| 350 | + | println!("{line}"); |
|
| 344 | 351 | Ok(0) |
|
| 345 | 352 | } |
|
| 346 | 353 | ||
| … | 140 unchanged lines |