OXYGENHUB

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