Compare commits
7 Commits
e5b18b17ff
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
659c084a74 | ||
|
|
b7561c16d8 | ||
|
|
9cbfa06c58 | ||
|
|
90454efe9f | ||
|
|
f35d665c19 | ||
|
|
f94a3170b1 | ||
|
|
23d4d59c71 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
# bv (beads viewer) local config and caches
|
||||
.bv/
|
||||
target/
|
||||
.DS_Store
|
||||
src/.DS_Store
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"min_width": 8,
|
||||
"prefer": "auto",
|
||||
"show_ahead_behind": true,
|
||||
"show_dirty": true,
|
||||
"truncate": {
|
||||
|
||||
70
install.sh
70
install.sh
@@ -31,14 +31,40 @@ echo "[ok] jq found"
|
||||
# ── Build release binary ─────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Building release binary..."
|
||||
(cd "$SCRIPT_DIR" && cargo build --release --quiet)
|
||||
echo "[ok] Built: $(ls -lh "$SCRIPT_DIR/target/release/$BINARY_NAME" | awk '{print $5}')"
|
||||
if ! (cd "$SCRIPT_DIR" && cargo build --release); then
|
||||
echo ""
|
||||
echo "ERROR: cargo build failed. Check the output above for details."
|
||||
echo " Common causes:"
|
||||
echo " - Missing C compiler: sudo apt install build-essential (Debian/Ubuntu)"
|
||||
echo " - Missing libc headers: sudo apt install libc6-dev"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the binary — respects CARGO_TARGET_DIR, custom target triples, etc.
|
||||
# Use cargo metadata as the source of truth for the target directory.
|
||||
TARGET_DIR=$(cd "$SCRIPT_DIR" && cargo metadata --format-version 1 --no-deps 2>/dev/null | jq -r '.target_directory' 2>/dev/null) || true
|
||||
TARGET_DIR="${TARGET_DIR:-$SCRIPT_DIR/target}"
|
||||
|
||||
BINARY="$TARGET_DIR/release/$BINARY_NAME"
|
||||
if [[ ! -f "$BINARY" ]]; then
|
||||
# Fall back to searching (handles custom target triples like target/<triple>/release/)
|
||||
BINARY=$(find "$TARGET_DIR" -name "$BINARY_NAME" -type f -path "*/release/*" ! -name "*.d" 2>/dev/null | head -1)
|
||||
fi
|
||||
if [[ -z "${BINARY:-}" || ! -f "$BINARY" ]]; then
|
||||
echo "ERROR: Build succeeded but binary not found."
|
||||
echo " Target dir: $TARGET_DIR"
|
||||
echo " Searched: $TARGET_DIR/*/release/$BINARY_NAME"
|
||||
echo " Contents: $(ls "$TARGET_DIR/release/" 2>/dev/null || echo '(empty or missing)')"
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] Built: $(ls -lh "$BINARY" | awk '{print $5}')"
|
||||
|
||||
# ── Install binary ───────────────────────────────────────────────────
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cp "$SCRIPT_DIR/target/release/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME"
|
||||
cp "$BINARY" "$INSTALL_DIR/$BINARY_NAME"
|
||||
chmod +x "$INSTALL_DIR/$BINARY_NAME"
|
||||
echo "[ok] Installed to $INSTALL_DIR/$BINARY_NAME"
|
||||
BINARY_PATH="$INSTALL_DIR/$BINARY_NAME"
|
||||
echo "[ok] Installed to $BINARY_PATH"
|
||||
|
||||
# Verify it's on PATH
|
||||
if ! command -v "$BINARY_NAME" &>/dev/null; then
|
||||
@@ -46,33 +72,50 @@ if ! command -v "$BINARY_NAME" &>/dev/null; then
|
||||
echo " Add to your shell config: export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||
fi
|
||||
|
||||
# Smoke test — verify the binary actually runs
|
||||
if "$BINARY_PATH" --test --color=never >/dev/null 2>&1; then
|
||||
echo "[ok] Binary smoke test passed"
|
||||
else
|
||||
echo "[warn] Binary smoke test failed (exit $?). It may still work inside Claude Code."
|
||||
echo " Debug: $BINARY_PATH --test --dump-state=json"
|
||||
fi
|
||||
|
||||
# ── Configure Claude Code settings.json ──────────────────────────────
|
||||
echo ""
|
||||
mkdir -p "$CLAUDE_DIR"
|
||||
|
||||
BINARY_PATH="$INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
# The binary runs in a non-TTY context, so force color on.
|
||||
STATUSLINE_CMD="$BINARY_PATH --color=always"
|
||||
|
||||
if [[ -f "$SETTINGS" ]]; then
|
||||
# Update existing settings.json
|
||||
# Update existing settings.json, preserving all other keys
|
||||
CURRENT_CMD=$(jq -r '.statusLine.command // empty' "$SETTINGS" 2>/dev/null || true)
|
||||
if [[ -n "$CURRENT_CMD" ]]; then
|
||||
echo "[info] Current statusLine command: $CURRENT_CMD"
|
||||
echo "[info] Previous statusLine command: $CURRENT_CMD"
|
||||
fi
|
||||
|
||||
# Write updated settings
|
||||
TMP="$SETTINGS.tmp.$$"
|
||||
jq --arg cmd "$STATUSLINE_CMD" '.statusLine = {"type": "command", "command": $cmd, "padding": 0}' "$SETTINGS" > "$TMP"
|
||||
if jq --arg cmd "$STATUSLINE_CMD" '.statusLine = {"type": "command", "command": $cmd, "padding": 0}' "$SETTINGS" > "$TMP" 2>/dev/null; then
|
||||
mv "$TMP" "$SETTINGS"
|
||||
echo "[ok] Updated statusLine in $SETTINGS"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
echo "[warn] Failed to update $SETTINGS (invalid JSON?). Creating backup and writing fresh."
|
||||
cp "$SETTINGS" "$SETTINGS.bak"
|
||||
jq -n --arg cmd "$STATUSLINE_CMD" '{"statusLine": {"type": "command", "command": $cmd, "padding": 0}}' > "$SETTINGS"
|
||||
echo "[ok] Wrote fresh $SETTINGS (backup: $SETTINGS.bak)"
|
||||
fi
|
||||
else
|
||||
# Create minimal settings.json
|
||||
jq -n --arg cmd "$STATUSLINE_CMD" '{"statusLine": {"type": "command", "command": $cmd, "padding": 0}}' > "$SETTINGS"
|
||||
echo "[ok] Created $SETTINGS"
|
||||
fi
|
||||
|
||||
# Show what was written so the user can verify
|
||||
echo ""
|
||||
echo " statusLine config:"
|
||||
jq '.statusLine' "$SETTINGS" 2>/dev/null || echo " (could not read settings)"
|
||||
|
||||
# ── Symlink config ───────────────────────────────────────────────────
|
||||
CONFIG_SRC="$SCRIPT_DIR/statusline.json"
|
||||
CONFIG_DST="$CLAUDE_DIR/statusline.json"
|
||||
@@ -103,7 +146,8 @@ fi
|
||||
|
||||
# ── Done ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Done. Restart Claude Code to see the status line."
|
||||
echo "Done. RESTART Claude Code (exit and reopen) to see the status line."
|
||||
echo ""
|
||||
echo "Quick test: $BINARY_NAME --test --color=always"
|
||||
echo "Debug: $BINARY_NAME --test --dump-state=json"
|
||||
echo "Verify: $BINARY_PATH --test --color=always"
|
||||
echo "Debug: $BINARY_PATH --test --dump-state=json"
|
||||
echo "Settings: cat $SETTINGS | jq .statusLine"
|
||||
|
||||
11
schema.json
11
schema.json
@@ -188,7 +188,6 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"git",
|
||||
"jj",
|
||||
"none"
|
||||
],
|
||||
"description": "VCS detection mode",
|
||||
@@ -375,16 +374,6 @@
|
||||
"minimum": 0,
|
||||
"default": 8
|
||||
},
|
||||
"prefer": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"auto",
|
||||
"git",
|
||||
"jj"
|
||||
],
|
||||
"default": "auto",
|
||||
"description": "VCS preference. auto detects .jj/ first, then .git/"
|
||||
},
|
||||
"show_ahead_behind": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
|
||||
@@ -303,41 +303,14 @@ fn prefetch_shell_outs(
|
||||
|
||||
std::thread::scope(|s| {
|
||||
let vcs_handle = if needs_vcs {
|
||||
let args: Vec<String> = match vcs_type {
|
||||
section::VcsType::Git => vec![
|
||||
"git".into(),
|
||||
"-C".into(),
|
||||
project_dir.into(),
|
||||
"status".into(),
|
||||
"--porcelain=v2".into(),
|
||||
"--branch".into(),
|
||||
],
|
||||
section::VcsType::Jj => vec![
|
||||
"jj".into(),
|
||||
"log".into(),
|
||||
"-r".into(),
|
||||
"@".into(),
|
||||
"--no-graph".into(),
|
||||
"-T".into(),
|
||||
"if(bookmarks, bookmarks.join(\",\"), change_id.shortest(8))".into(),
|
||||
"--color=never".into(),
|
||||
],
|
||||
section::VcsType::None => vec![],
|
||||
};
|
||||
if args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let dir = if vcs_type == section::VcsType::Jj {
|
||||
Some(project_dir.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(s.spawn(move || {
|
||||
let prog = &args[0];
|
||||
let str_args: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
|
||||
shell::exec_gated(shell_config, prog, &str_args, dir.as_deref())
|
||||
shell::exec_gated(
|
||||
shell_config,
|
||||
"git",
|
||||
&["-C", project_dir, "status", "--porcelain=v2", "--branch"],
|
||||
None,
|
||||
)
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -483,35 +456,13 @@ fn resolve_terminal_palette(cache: &cache::Cache) -> Option<Vec<(u8, u8, u8)>> {
|
||||
Some(palette)
|
||||
}
|
||||
|
||||
fn detect_vcs(dir: &str, config: &config::Config) -> section::VcsType {
|
||||
let prefer = config.sections.vcs.prefer.as_str();
|
||||
fn detect_vcs(dir: &str, _config: &config::Config) -> section::VcsType {
|
||||
let path = std::path::Path::new(dir);
|
||||
|
||||
match prefer {
|
||||
"jj" => {
|
||||
if path.join(".jj").is_dir() {
|
||||
section::VcsType::Jj
|
||||
} else {
|
||||
section::VcsType::None
|
||||
}
|
||||
}
|
||||
"git" => {
|
||||
if path.join(".git").is_dir() {
|
||||
section::VcsType::Git
|
||||
} else {
|
||||
section::VcsType::None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if path.join(".jj").is_dir() {
|
||||
section::VcsType::Jj
|
||||
} else if path.join(".git").is_dir() {
|
||||
section::VcsType::Git
|
||||
} else {
|
||||
section::VcsType::None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -296,7 +296,6 @@ impl Default for TruncateConfig {
|
||||
pub struct VcsSection {
|
||||
#[serde(flatten)]
|
||||
pub base: SectionBase,
|
||||
pub prefer: String,
|
||||
pub show_ahead_behind: bool,
|
||||
pub show_dirty: bool,
|
||||
pub untracked: String,
|
||||
@@ -314,7 +313,6 @@ impl Default for VcsSection {
|
||||
min_width: Some(8),
|
||||
..Default::default()
|
||||
},
|
||||
prefer: "auto".into(),
|
||||
show_ahead_behind: true,
|
||||
show_dirty: true,
|
||||
untracked: "normal".into(),
|
||||
|
||||
@@ -2,11 +2,11 @@ use crate::format;
|
||||
use crate::layout::ActiveSection;
|
||||
use crate::section::{self, RenderContext};
|
||||
|
||||
/// Expand the winning flex section to fill remaining terminal width.
|
||||
/// Expand flex sections to fill remaining terminal width.
|
||||
///
|
||||
/// Rules:
|
||||
/// - Spacers take priority over non-spacer flex sections
|
||||
/// - Only one flex section wins per line
|
||||
/// - If multiple spacers exist, distribute extra space proportionally among them
|
||||
/// - If only non-spacer flex sections exist, pick one winner (spacer > non-spacer)
|
||||
/// - Spacer: fill with spaces
|
||||
/// - context_bar: rebuild bar with wider width
|
||||
/// - Other: pad with trailing spaces
|
||||
@@ -18,7 +18,34 @@ pub fn flex_expand(active: &mut [ActiveSection], ctx: &RenderContext, separator:
|
||||
return;
|
||||
}
|
||||
|
||||
// Find winning flex section: spacer wins over non-spacer
|
||||
let extra = term_width - current_width;
|
||||
|
||||
// Collect all spacer indices
|
||||
let spacer_indices: Vec<usize> = active
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.is_spacer)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
// If multiple spacers, distribute extra space among them
|
||||
if spacer_indices.len() > 1 {
|
||||
let per_spacer = extra / spacer_indices.len();
|
||||
let remainder = extra % spacer_indices.len();
|
||||
|
||||
for (i, &idx) in spacer_indices.iter().enumerate() {
|
||||
// First spacers get +1 char from remainder
|
||||
let this_width = per_spacer + if i < remainder { 1 } else { 0 } + 1;
|
||||
let padding = " ".repeat(this_width);
|
||||
active[idx].output = section::SectionOutput {
|
||||
raw: padding.clone(),
|
||||
ansi: padding,
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Single spacer or non-spacer flex: find winning flex section
|
||||
let mut flex_idx: Option<usize> = None;
|
||||
for (i, sec) in active.iter().enumerate() {
|
||||
if !sec.is_flex {
|
||||
@@ -35,7 +62,6 @@ pub fn flex_expand(active: &mut [ActiveSection], ctx: &RenderContext, separator:
|
||||
}
|
||||
|
||||
let Some(idx) = flex_idx else { return };
|
||||
let extra = term_width - current_width;
|
||||
|
||||
if active[idx].is_spacer {
|
||||
let padding = " ".repeat(extra + 1);
|
||||
|
||||
@@ -52,7 +52,6 @@ pub type RenderFn = fn(&RenderContext) -> Option<SectionOutput>;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VcsType {
|
||||
Git,
|
||||
Jj,
|
||||
None,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
|
||||
return None;
|
||||
}
|
||||
|
||||
// --no-shell: serve stale cache only, skip all git/jj commands
|
||||
// --no-shell: serve stale cache only, skip all git commands
|
||||
if ctx.no_shell {
|
||||
return render_stale_cache(ctx);
|
||||
}
|
||||
@@ -21,11 +21,7 @@ pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
|
||||
let ttl = &ctx.config.sections.vcs.ttl;
|
||||
let glyphs = &ctx.config.glyphs;
|
||||
|
||||
match ctx.vcs_type {
|
||||
VcsType::Git => render_git(ctx, dir, ttl, glyphs),
|
||||
VcsType::Jj => render_jj(ctx, dir, ttl, glyphs),
|
||||
VcsType::None => None,
|
||||
}
|
||||
render_git(ctx, dir, ttl, glyphs)
|
||||
}
|
||||
|
||||
/// Serve stale cached VCS data without running any commands.
|
||||
@@ -158,53 +154,3 @@ fn render_git(
|
||||
|
||||
Some(SectionOutput { raw, ansi })
|
||||
}
|
||||
|
||||
fn render_jj(
|
||||
ctx: &RenderContext,
|
||||
dir: &str,
|
||||
ttl: &crate::config::VcsTtl,
|
||||
glyphs: &crate::config::GlyphConfig,
|
||||
) -> Option<SectionOutput> {
|
||||
use std::time::Duration;
|
||||
|
||||
let branch_ttl = Duration::from_secs(ttl.branch);
|
||||
|
||||
let branch = ctx.cache.get("vcs_branch", branch_ttl).or_else(|| {
|
||||
// Use prefetched result if available, otherwise exec
|
||||
let out = ctx.shell_results.get("vcs").cloned().unwrap_or_else(|| {
|
||||
shell::exec_gated(
|
||||
ctx.shell_config,
|
||||
"jj",
|
||||
&[
|
||||
"log",
|
||||
"-r",
|
||||
"@",
|
||||
"--no-graph",
|
||||
"-T",
|
||||
"if(bookmarks, bookmarks.join(\",\"), change_id.shortest(8))",
|
||||
"--color=never",
|
||||
],
|
||||
Some(dir),
|
||||
)
|
||||
})?;
|
||||
ctx.cache.set("vcs_branch", &out);
|
||||
Some(out)
|
||||
})?;
|
||||
|
||||
let trunc = &ctx.config.sections.vcs.truncate;
|
||||
let branch = if trunc.enabled && trunc.max > 0 {
|
||||
crate::format::truncate(&branch, trunc.max, &trunc.style)
|
||||
} else {
|
||||
branch
|
||||
};
|
||||
|
||||
let branch_glyph = glyph::glyph("branch", glyphs);
|
||||
let raw = format!("{branch_glyph}{branch}");
|
||||
let ansi = if ctx.color_enabled {
|
||||
format!("{}{branch_glyph}{branch}{}", color::GREEN, color::RESET)
|
||||
} else {
|
||||
raw.clone()
|
||||
};
|
||||
|
||||
Some(SectionOutput { raw, ansi })
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ pub fn parse_transcript(path: &Path, skip_lines: usize) -> Option<TranscriptStat
|
||||
|
||||
// Sort by count descending
|
||||
let mut sorted: Vec<(String, u64)> = counts.into_iter().collect();
|
||||
sorted.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
sorted.sort_by_key(|item| std::cmp::Reverse(item.1));
|
||||
stats.tool_counts = sorted;
|
||||
|
||||
Some(stats)
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
{
|
||||
"$schema": "./schema.json",
|
||||
"global": {
|
||||
"justify": "spread",
|
||||
"theme": "dark",
|
||||
"responsive": true,
|
||||
"width": 500,
|
||||
"width_margin": 20
|
||||
"width_margin": 45
|
||||
},
|
||||
"glyphs": {
|
||||
"enabled": true
|
||||
},
|
||||
"layout": "verbose",
|
||||
"layout": [
|
||||
["model", "provider", "project", "spacer", "context_bar", "context_usage", "spacer", "cost", "cost_velocity"],
|
||||
["vcs", "lines_changed", "spacer", "tokens_raw", "cache_efficiency", "spacer", "beads"]
|
||||
],
|
||||
"sections": {
|
||||
"context_usage": {
|
||||
"enabled": true
|
||||
},
|
||||
"context_trend": {
|
||||
"context_bar": {
|
||||
"flex": true,
|
||||
"min_width": 12
|
||||
},
|
||||
"tokens_raw": {
|
||||
"enabled": true
|
||||
},
|
||||
"cost_trend": {
|
||||
"cache_efficiency": {
|
||||
"enabled": true
|
||||
},
|
||||
"load": {
|
||||
"enabled": false
|
||||
"cost_velocity": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user