mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-09-30 17:40:51 -07:00
update waybar
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"include": ["~/.config/waybar/modules/custom-codexbar.json"],
|
||||
"layer": "bottom",
|
||||
"position": "top",
|
||||
"height": 40,
|
||||
@@ -328,13 +329,4 @@
|
||||
"on-click-right": "swaync-client -d -sw",
|
||||
"escape": false,
|
||||
},
|
||||
"custom/codexbar": {
|
||||
"exec": "codexbar --icon \"<span font='Font Awesome 7 Brands'></span>\" | sed 's/ *|//g'",
|
||||
"return-type": "json",
|
||||
"interval": 300,
|
||||
"signal": 12,
|
||||
"tooltip": true,
|
||||
"on-click": "python3 ~/.config/waybar/scripts/aibar-popup.py",
|
||||
"on-click-right": "python3 ~/.config/waybar/scripts/aibar-popup.py --claude",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -344,11 +344,9 @@ label:focus {
|
||||
#custom-codexbar {
|
||||
padding: 0 10px;
|
||||
margin: 5px 1px;
|
||||
background-color: @surface0;
|
||||
background-color: @pink;
|
||||
color: @mantle;
|
||||
border-radius: 0.69em;
|
||||
}
|
||||
|
||||
#custom-codexbar.low { color: @green; }
|
||||
#custom-codexbar.mid { color: @yellow; }
|
||||
#custom-codexbar.high { color: @peach; }
|
||||
#custom-codexbar.critical { color: @red; }
|
||||
#custom-codexbar.critical { box-shadow: inset 0 -3px @red; }
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Waybar custom-module definition for codexbar-waybar.
|
||||
//
|
||||
// install.sh drops this at ~/.config/waybar/modules/custom-codexbar.json.
|
||||
// You then reference "custom/codexbar" from a modules-right group in your
|
||||
// ~/.config/waybar/config.jsonc — see README.md for the snippet.
|
||||
//
|
||||
// To force a refresh from elsewhere: pkill -RTMIN+8 waybar
|
||||
{
|
||||
"custom/codexbar": {
|
||||
"exec": "$HOME/.config/waybar/scripts/codexbar.sh",
|
||||
"return-type": "json",
|
||||
"format": "{}",
|
||||
"interval": 30,
|
||||
"signal": 8,
|
||||
"on-click": "$HOME/.config/waybar/scripts/codexbar-popup.py",
|
||||
"on-click-right": "bash -c 'notify-send -a CodexBar -t 8000 \"AI usage\" \"$($HOME/.config/waybar/scripts/codexbar.sh | jq -r .tooltip)\"'",
|
||||
"tooltip": true,
|
||||
"max-length": 24
|
||||
}
|
||||
}
|
||||
Executable
+387
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
aibar-popup.py — Tabbed AI usage popup for codexbar + claudebar.
|
||||
Toggle: clicking when open closes it (uses PID lockfile).
|
||||
|
||||
Run with: GDK_BACKEND=x11 python3 ~/.config/waybar/scripts/aibar-popup.py
|
||||
Flags: --claude open with Claude tab active
|
||||
"""
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import threading
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
import atexit
|
||||
import re
|
||||
|
||||
# ── Toggle: kill existing instance if running ─────────────────────────────
|
||||
LOCK = "/tmp/aibar-popup.pid"
|
||||
|
||||
def _cleanup():
|
||||
try:
|
||||
os.remove(LOCK)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if os.path.exists(LOCK):
|
||||
try:
|
||||
with open(LOCK) as f:
|
||||
pid = int(f.read().strip())
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
os.remove(LOCK)
|
||||
sys.exit(0) # toggled off an existing popup — done
|
||||
except (ProcessLookupError, ValueError, OSError):
|
||||
os.remove(LOCK) # stale lock — clean up and continue opening
|
||||
|
||||
with open(LOCK, "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
atexit.register(_cleanup)
|
||||
|
||||
# ── Catppuccin Macchiato ──────────────────────────────────────────────────
|
||||
CRUST = "#181926"
|
||||
MANTLE = "#1e2030"
|
||||
BASE = "#24273a"
|
||||
S0 = "#363a4f"
|
||||
S1 = "#494d64"
|
||||
OV0 = "#6e738d"
|
||||
OV1 = "#8087a2"
|
||||
SUB1 = "#a5adcb"
|
||||
TEXT = "#cad3f5"
|
||||
GREEN = "#a6da95" # Codex / OpenAI
|
||||
PEACH = "#f5a97f" # Claude / Anthropic
|
||||
RED = "#ed8796"
|
||||
YELLOW = "#eed49f"
|
||||
|
||||
FONT = "JetBrainsMono Nerd Font"
|
||||
|
||||
CSS = f"""
|
||||
window {{
|
||||
background-color: {MANTLE};
|
||||
border-radius: 12px;
|
||||
border: 1px solid {S1};
|
||||
}}
|
||||
.root-box {{
|
||||
background-color: {MANTLE};
|
||||
border-radius: 12px;
|
||||
}}
|
||||
|
||||
/* ── Tab bar ── */
|
||||
.tab-bar {{
|
||||
background-color: {CRUST};
|
||||
border-radius: 12px 12px 0 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid {S0};
|
||||
min-height: 36px;
|
||||
}}
|
||||
.tab-btn {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: {OV1};
|
||||
padding: 7px 20px 4px 20px;
|
||||
font-family: "{FONT}";
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
letter-spacing: 0.3px;
|
||||
}}
|
||||
.tab-btn:hover {{
|
||||
color: {TEXT};
|
||||
background-color: {S0};
|
||||
box-shadow: none;
|
||||
border-bottom: 3px solid {S1};
|
||||
}}
|
||||
.tab-codex.active {{
|
||||
color: {GREEN};
|
||||
border-bottom: 3px solid {GREEN};
|
||||
background-color: {MANTLE};
|
||||
}}
|
||||
.tab-claude.active {{
|
||||
color: {PEACH};
|
||||
border-bottom: 3px solid {PEACH};
|
||||
background-color: {MANTLE};
|
||||
}}
|
||||
.close-btn {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: {OV0};
|
||||
font-size: 11px;
|
||||
padding: 4px 12px 4px 12px;
|
||||
border-radius: 0;
|
||||
border-bottom: 3px solid transparent;
|
||||
box-shadow: none;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}}
|
||||
.close-btn:hover {{
|
||||
color: {RED};
|
||||
background-color: {S0};
|
||||
box-shadow: none;
|
||||
}}
|
||||
|
||||
/* ── Status section ── */
|
||||
.status-section {{
|
||||
background-color: {BASE};
|
||||
padding: 12px 18px 10px 18px;
|
||||
border-bottom: 1px solid {S0};
|
||||
}}
|
||||
.status-label {{
|
||||
font-family: "{FONT}";
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
/* ── Accent stripe ── */
|
||||
.accent-codex {{
|
||||
background: linear-gradient(to right, {GREEN}, transparent);
|
||||
min-height: 3px;
|
||||
}}
|
||||
.accent-claude {{
|
||||
background: linear-gradient(to right, {PEACH}, transparent);
|
||||
min-height: 3px;
|
||||
}}
|
||||
|
||||
/* ── Content ── */
|
||||
.content-box {{
|
||||
background-color: {MANTLE};
|
||||
padding: 14px 18px 16px 18px;
|
||||
}}
|
||||
.tooltip-label {{
|
||||
font-family: "{FONT}";
|
||||
font-size: 12.5px;
|
||||
color: {TEXT};
|
||||
}}
|
||||
.loading-label {{
|
||||
color: {OV0};
|
||||
font-family: "{FONT}";
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
padding: 32px 18px;
|
||||
}}
|
||||
""".encode()
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def strip_box(text: str) -> str:
|
||||
"""Remove box-drawing decoration (╭─│╰) from Pango-markup tooltip strings."""
|
||||
cleaned = []
|
||||
for line in text.split("\n"):
|
||||
if re.search(r"[╭╰╮]", line):
|
||||
continue
|
||||
if re.search(r"foreground='#5c6370'>[─]", line):
|
||||
continue
|
||||
line = re.sub(r"<span foreground='#61afef'>│</span>\s*", "", line)
|
||||
line = re.sub(r"\s*<span foreground='#61afef'>│</span>", "", line)
|
||||
if line.strip():
|
||||
cleaned.append(line)
|
||||
return "\n".join(cleaned)
|
||||
|
||||
|
||||
# ── Data fetching ─────────────────────────────────────────────────────────
|
||||
|
||||
def fetch_data(cmd: list[str]) -> dict:
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
|
||||
return json.loads(r.stdout.strip())
|
||||
except Exception as e:
|
||||
return {
|
||||
"text": "⚠ error",
|
||||
"tooltip": f'<span foreground="{RED}">Error: {e}</span>',
|
||||
"class": "",
|
||||
}
|
||||
|
||||
|
||||
# ── Content page ─────────────────────────────────────────────────────────
|
||||
|
||||
class ContentPage(Gtk.Box):
|
||||
def __init__(self, service: str):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
self.service = service
|
||||
|
||||
# Gradient accent stripe
|
||||
accent = Gtk.Box()
|
||||
accent.set_size_request(-1, 3)
|
||||
accent.get_style_context().add_class(
|
||||
"accent-codex" if service == "codex" else "accent-claude"
|
||||
)
|
||||
self.pack_start(accent, False, False, 0)
|
||||
|
||||
# Status section (bar text — large)
|
||||
self.status_section = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
self.status_section.get_style_context().add_class("status-section")
|
||||
self.status_lbl = Gtk.Label(label="—")
|
||||
self.status_lbl.get_style_context().add_class("status-label")
|
||||
self.status_lbl.set_halign(Gtk.Align.START)
|
||||
self.status_section.pack_start(self.status_lbl, True, True, 0)
|
||||
self.pack_start(self.status_section, False, False, 0)
|
||||
|
||||
# Tooltip / main content
|
||||
self.content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
self.content_box.get_style_context().add_class("content-box")
|
||||
self.pack_start(self.content_box, True, True, 0)
|
||||
|
||||
self.loading = Gtk.Label(label="fetching…")
|
||||
self.loading.get_style_context().add_class("loading-label")
|
||||
self.loading.set_halign(Gtk.Align.CENTER)
|
||||
self.content_box.pack_start(self.loading, True, True, 0)
|
||||
|
||||
cmd = ["codexbar"] if service == "codex" else ["claudebar"]
|
||||
threading.Thread(target=self._fetch, args=(cmd,), daemon=True).start()
|
||||
|
||||
def _fetch(self, cmd):
|
||||
data = fetch_data(cmd)
|
||||
GLib.idle_add(self._populate, data)
|
||||
|
||||
def _populate(self, data):
|
||||
color = GREEN if self.service == "codex" else PEACH
|
||||
text = data.get("text", "")
|
||||
tip = strip_box(data.get("tooltip", "No data"))
|
||||
|
||||
# Update status bar
|
||||
self.status_lbl.set_markup(
|
||||
f'<span foreground="{color}" font="{FONT} 20" weight="bold">{text}</span>'
|
||||
)
|
||||
|
||||
# Replace loading with tooltip content
|
||||
self.content_box.remove(self.loading)
|
||||
lbl = Gtk.Label()
|
||||
try:
|
||||
lbl.set_markup(tip)
|
||||
except Exception:
|
||||
lbl.set_text(tip)
|
||||
lbl.get_style_context().add_class("tooltip-label")
|
||||
lbl.set_halign(Gtk.Align.START)
|
||||
lbl.set_valign(Gtk.Align.START)
|
||||
lbl.set_selectable(True)
|
||||
lbl.set_line_wrap(False)
|
||||
self.content_box.pack_start(lbl, True, True, 0)
|
||||
self.content_box.show_all()
|
||||
return False
|
||||
|
||||
|
||||
# ── Main window ──────────────────────────────────────────────────────────
|
||||
|
||||
class AIBarPopup(Gtk.Window):
|
||||
def __init__(self, default_tab: int = 0):
|
||||
super().__init__(type=Gtk.WindowType.TOPLEVEL)
|
||||
self.set_title("aibar")
|
||||
self.set_decorated(False)
|
||||
self.set_resizable(False)
|
||||
self.set_keep_above(True)
|
||||
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
self.active_tab = default_tab
|
||||
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_data(CSS)
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
Gdk.Screen.get_default(),
|
||||
provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
self.connect("key-press-event", self._on_key)
|
||||
|
||||
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
root.get_style_context().add_class("root-box")
|
||||
self.add(root)
|
||||
|
||||
# ── Tab bar ──────────────────────────────────────────────────────
|
||||
tab_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
tab_bar.get_style_context().add_class("tab-bar")
|
||||
|
||||
self.btn_codex = Gtk.Button(label=" Codex")
|
||||
self.btn_codex.get_style_context().add_class("tab-btn")
|
||||
self.btn_codex.get_style_context().add_class("tab-codex")
|
||||
self.btn_codex.connect("clicked", lambda *_: self._set_tab(0))
|
||||
|
||||
self.btn_claude = Gtk.Button(label=" Claude")
|
||||
self.btn_claude.get_style_context().add_class("tab-btn")
|
||||
self.btn_claude.get_style_context().add_class("tab-claude")
|
||||
self.btn_claude.connect("clicked", lambda *_: self._set_tab(1))
|
||||
|
||||
spacer = Gtk.Box()
|
||||
spacer.set_hexpand(True)
|
||||
|
||||
close_btn = Gtk.Button(label="✕")
|
||||
close_btn.get_style_context().add_class("close-btn")
|
||||
close_btn.connect("clicked", lambda *_: self._quit())
|
||||
|
||||
tab_bar.pack_start(self.btn_codex, False, False, 0)
|
||||
tab_bar.pack_start(self.btn_claude, False, False, 0)
|
||||
tab_bar.pack_start(spacer, True, True, 0)
|
||||
tab_bar.pack_start(close_btn, False, False, 0)
|
||||
root.pack_start(tab_bar, False, False, 0)
|
||||
|
||||
# ── Stack ─────────────────────────────────────────────────────────
|
||||
self.stack = Gtk.Stack()
|
||||
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||
self.stack.set_transition_duration(130)
|
||||
# Fixed size: 360px wide fits the codexbar/claudebar cards (~280px) with padding.
|
||||
# Height 410 accommodates the tallest page (Claude has 4 sections); scrolled below.
|
||||
self.stack.set_size_request(360, 410)
|
||||
|
||||
self.page_codex = ContentPage("codex")
|
||||
self.page_claude = ContentPage("claude")
|
||||
self.stack.add_named(self.page_codex, "codex")
|
||||
self.stack.add_named(self.page_claude, "claude")
|
||||
root.pack_start(self.stack, True, True, 0)
|
||||
|
||||
self._set_tab(default_tab, animate=False)
|
||||
self.show_all()
|
||||
self._position()
|
||||
|
||||
def _set_tab(self, idx: int, animate: bool = True):
|
||||
self.active_tab = idx
|
||||
if not animate:
|
||||
self.stack.set_transition_type(Gtk.StackTransitionType.NONE)
|
||||
self.stack.set_visible_child_name("codex" if idx == 0 else "claude")
|
||||
if not animate:
|
||||
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||
|
||||
cc = self.btn_codex.get_style_context()
|
||||
ca = self.btn_claude.get_style_context()
|
||||
if idx == 0:
|
||||
cc.add_class("active"); ca.remove_class("active")
|
||||
else:
|
||||
cc.remove_class("active"); ca.add_class("active")
|
||||
|
||||
def _position(self):
|
||||
self.realize()
|
||||
w, _h = self.get_size()
|
||||
display = Gdk.Display.get_default()
|
||||
n = display.get_n_monitors()
|
||||
monitor = display.get_monitor(n - 1)
|
||||
geo = monitor.get_geometry()
|
||||
x = geo.x + geo.width - w - 14
|
||||
y = geo.y + 48
|
||||
self.move(x, y)
|
||||
|
||||
def _on_key(self, _w, event):
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self._quit()
|
||||
|
||||
def _quit(self):
|
||||
_cleanup()
|
||||
Gtk.main_quit()
|
||||
|
||||
|
||||
# ── Entry point ──────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
default = 1 if "--claude" in sys.argv else 0
|
||||
win = AIBarPopup(default)
|
||||
win.connect("destroy", lambda *_: (_cleanup(), Gtk.main_quit()))
|
||||
Gtk.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
BIN
Binary file not shown.
Executable
+1421
File diff suppressed because it is too large
Load Diff
Executable
+542
@@ -0,0 +1,542 @@
|
||||
#!/usr/bin/env bash
|
||||
# Waybar custom module for CodexBar (Linux CLI).
|
||||
# Fetches usage from configured providers sequentially (with a small stagger
|
||||
# to avoid per-provider rate limits) and emits Waybar JSON
|
||||
# ({"text","tooltip","class","percentage"}) keyed on the most-constrained window.
|
||||
# Last successful per-provider snapshot is cached, so a transient 429 is masked
|
||||
# until the next refresh recovers it.
|
||||
#
|
||||
# Linux-only providers — cookies/web providers are macOS-only. The list is
|
||||
# read from ~/.codexbar/config.json (use the popover's Settings view to
|
||||
# manage it); falls back to codex+claude+gemini if the file is missing.
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
CODEXBAR="${CODEXBAR_BIN:-codexbar}"
|
||||
CONFIG_PATH="${HOME}/.codexbar/config.json"
|
||||
STATE_PATH="${XDG_CONFIG_HOME:-${HOME}/.config}/codexbar-waybar/state.json"
|
||||
CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/codexbar-waybar"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
# Per-instance bar provider selection (written by the popup's Settings view).
|
||||
# `null` (or unset) means "show the highest used% across providers".
|
||||
BAR_PROVIDER=""
|
||||
# How to render reset times in the tooltip / popover:
|
||||
# provider — use the provider's `resetDescription` as-is (default; backward
|
||||
# compatible — Claude OAuth gives "May 17 at 6:20AM" with no TZ,
|
||||
# Codex gives "7:10 AM" in UTC with no TZ marker, etc.)
|
||||
# local — format `resetsAt` (ISO 8601 UTC) in the system local timezone
|
||||
# with a today / this-year / future-year tier and a TZ suffix
|
||||
# utc — same tiering, formatted in UTC with a literal "UTC" suffix
|
||||
RESET_TIME_FORMAT="provider"
|
||||
if [[ -f "$STATE_PATH" ]] && command -v jq >/dev/null 2>&1; then
|
||||
BAR_PROVIDER="$(jq -r '.barProvider // empty' "$STATE_PATH" 2>/dev/null)"
|
||||
rf="$(jq -r '.resetTimeFormat // empty' "$STATE_PATH" 2>/dev/null)"
|
||||
[[ -n "$rf" ]] && RESET_TIME_FORMAT="$rf"
|
||||
fi
|
||||
[[ -n "${CODEXBAR_BAR_PROVIDER:-}" ]] && BAR_PROVIDER="$CODEXBAR_BAR_PROVIDER"
|
||||
[[ -n "${CODEXBAR_RESET_TIME_FORMAT:-}" ]] && RESET_TIME_FORMAT="$CODEXBAR_RESET_TIME_FORMAT"
|
||||
case "$RESET_TIME_FORMAT" in
|
||||
provider|local|utc) ;;
|
||||
*) RESET_TIME_FORMAT="provider" ;;
|
||||
esac
|
||||
|
||||
# Read enabled providers from the codexbar CLI's config, fall back to a
|
||||
# sensible default if it's missing or unreadable. Override with the
|
||||
# CODEXBAR_PROVIDERS env var (space-separated list) when you want to bypass
|
||||
# config.json for a specific waybar instance.
|
||||
if [[ -n "${CODEXBAR_PROVIDERS:-}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
PROVIDERS=( ${CODEXBAR_PROVIDERS} )
|
||||
elif [[ -f "$CONFIG_PATH" ]] && command -v jq >/dev/null 2>&1; then
|
||||
readarray -t PROVIDERS < <(jq -r '[.providers[]? | select(.enabled == true) | .id] | .[]' "$CONFIG_PATH" 2>/dev/null)
|
||||
[[ ${#PROVIDERS[@]} -eq 0 ]] && PROVIDERS=(codex claude gemini)
|
||||
else
|
||||
PROVIDERS=(codex claude gemini)
|
||||
fi
|
||||
|
||||
# Setup Antigravity SSL if it is one of the enabled providers.
|
||||
setup_antigravity_ssl() {
|
||||
if ! command -v openssl >/dev/null 2>&1 || ! command -v lsof >/dev/null 2>&1 || ! command -v pgrep >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local pid
|
||||
pid=$(pgrep -f "Antigravity.*/language_server" | head -n 1)
|
||||
if [[ -z "$pid" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local ports
|
||||
ports=$(lsof -a -p "$pid" -iTCP -sTCP:LISTEN -F n 2>/dev/null | grep -oE '[0-9]+$')
|
||||
if [[ -z "$ports" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local cert=""
|
||||
# shellcheck disable=SC2086
|
||||
for port in $ports; do
|
||||
local out
|
||||
out=$(openssl s_client -showcerts -connect 127.0.0.1:"$port" < /dev/null 2>/dev/null)
|
||||
if [[ "$out" == *"-----BEGIN CERTIFICATE-----"* ]]; then
|
||||
cert=$(echo "$out" | openssl x509 -outform PEM 2>/dev/null)
|
||||
if [[ -n "$cert" ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$cert" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local scratch_dir="${CACHE_DIR}/scratch"
|
||||
mkdir -p "$scratch_dir"
|
||||
local cert_file="${scratch_dir}/localhost.crt"
|
||||
echo "$cert" > "$cert_file"
|
||||
|
||||
local sys_ca=""
|
||||
for path in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/ca-bundle.pem /var/lib/ca-certificates/ca-bundle.pem; do
|
||||
if [[ -f "$path" ]]; then
|
||||
sys_ca="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local bundle_file="${scratch_dir}/custom-ca-bundle.crt"
|
||||
if [[ -n "$sys_ca" ]]; then
|
||||
cat "$sys_ca" "$cert_file" > "$bundle_file"
|
||||
else
|
||||
cat "$cert_file" > "$bundle_file"
|
||||
fi
|
||||
|
||||
ANTIGRAVITY_CUSTOM_CA_BUNDLE="$bundle_file"
|
||||
ANTIGRAVITY_LD_PRELOAD="${SCRIPT_DIR}/cert_redirect.so"
|
||||
return 0
|
||||
}
|
||||
|
||||
if [[ " ${PROVIDERS[*]} " == *" antigravity "* ]]; then
|
||||
setup_antigravity_ssl
|
||||
fi
|
||||
|
||||
# Per-provider --source override. Codex/Claude need explicit oauth on Linux
|
||||
# (their `auto` falls back to web first, which is macOS-only).
|
||||
declare -A SOURCE_OVERRIDES=(
|
||||
[codex]=oauth
|
||||
[claude]=oauth
|
||||
)
|
||||
|
||||
# If the primary source returns a provider-level error (e.g. Claude OAuth
|
||||
# hitting HTTP 429), fall back to this source. Mostly useful for Claude
|
||||
# where the local CLI logs have the same windowing data.
|
||||
declare -A FALLBACK_SOURCES=(
|
||||
[claude]=cli
|
||||
)
|
||||
|
||||
# Antigravity has no Linux login flow in the CLI: it expects Google OAuth
|
||||
# credentials at ~/.codexbar/antigravity/oauth_creds.json (written by the macOS
|
||||
# app), or passed inline via $ANTIGRAVITY_OAUTH_CREDENTIALS_JSON. On Linux the
|
||||
# Antigravity CLI (`agy`) logs in through the shared Gemini flow and drops the
|
||||
# same Google creds at ~/.gemini/oauth_creds.json. Bridge that file into the
|
||||
# env var so an `agy` login satisfies codexbar without a second login.
|
||||
# Override the source path with $CODEXBAR_ANTIGRAVITY_CREDS.
|
||||
ANTIGRAVITY_CREDS="${CODEXBAR_ANTIGRAVITY_CREDS:-${HOME}/.gemini/oauth_creds.json}"
|
||||
|
||||
fetch_one() {
|
||||
local p="$1" src="$2"
|
||||
local args=(usage --provider "$p" --format json --no-color)
|
||||
[[ -n "$src" ]] && args+=(--source "$src")
|
||||
local runner=()
|
||||
if [[ "${PROVIDER_TIMEOUT_SECS:-20}" != "0" ]] && command -v timeout >/dev/null 2>&1; then
|
||||
runner=(timeout "${PROVIDER_TIMEOUT_SECS:-20}")
|
||||
fi
|
||||
if [[ "$p" == "antigravity" ]]; then
|
||||
if [[ -z "${ANTIGRAVITY_OAUTH_CREDENTIALS_JSON:-}" && -f "$ANTIGRAVITY_CREDS" ]]; then
|
||||
"${runner[@]}" env \
|
||||
CUSTOM_CA_BUNDLE="${ANTIGRAVITY_CUSTOM_CA_BUNDLE:-}" \
|
||||
LD_PRELOAD="${ANTIGRAVITY_LD_PRELOAD:-}" \
|
||||
ANTIGRAVITY_OAUTH_CREDENTIALS_JSON="$(cat "$ANTIGRAVITY_CREDS")" \
|
||||
"$CODEXBAR" "${args[@]}" 2>/dev/null
|
||||
else
|
||||
"${runner[@]}" env \
|
||||
CUSTOM_CA_BUNDLE="${ANTIGRAVITY_CUSTOM_CA_BUNDLE:-}" \
|
||||
LD_PRELOAD="${ANTIGRAVITY_LD_PRELOAD:-}" \
|
||||
"$CODEXBAR" "${args[@]}" 2>/dev/null
|
||||
fi
|
||||
return
|
||||
fi
|
||||
"${runner[@]}" "$CODEXBAR" "${args[@]}" 2>/dev/null
|
||||
}
|
||||
|
||||
fetch_provider() {
|
||||
local p="$1"
|
||||
local primary="${SOURCE_OVERRIDES[$p]:-}"
|
||||
local fallback="${FALLBACK_SOURCES[$p]:-}"
|
||||
|
||||
local body fallback_body
|
||||
body="$(fetch_one "$p" "$primary")"
|
||||
|
||||
# Only retry when the response is a valid array with a provider-level
|
||||
# error — network failures and rate limits land here. Auth misconfig
|
||||
# surfaces the same way; the fallback may still error, which is fine
|
||||
# because the cache layer will mask it.
|
||||
if [[ -n "$fallback" ]] \
|
||||
&& echo "$body" | jq -e 'type == "array" and (.[0].error // null) != null' >/dev/null 2>&1; then
|
||||
fallback_body="$(fetch_one "$p" "$fallback")"
|
||||
if echo "$fallback_body" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
||||
body="$fallback_body"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$body"
|
||||
}
|
||||
|
||||
# Sequential fetch with a small stagger between providers (avoids 429s).
|
||||
STAGGER_SECS="${CODEXBAR_STAGGER:-0.5}"
|
||||
PROVIDER_TIMEOUT_SECS="${CODEXBAR_PROVIDER_TIMEOUT:-20}"
|
||||
LAST_GOOD="$CACHE_DIR/last.json"
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
i=0
|
||||
for p in "${PROVIDERS[@]}"; do
|
||||
(( i > 0 )) && sleep "$STAGGER_SECS"
|
||||
fetch_provider "$p" > "$tmpdir/$p.json"
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
append_merged_entry() {
|
||||
local entry="$1"
|
||||
[[ -z "$entry" ]] && return
|
||||
if (( first )); then
|
||||
merged+="$entry"
|
||||
first=0
|
||||
else
|
||||
merged+=",$entry"
|
||||
fi
|
||||
}
|
||||
|
||||
merged="["
|
||||
first=1
|
||||
for p in "${PROVIDERS[@]}"; do
|
||||
body="$(cat "$tmpdir/$p.json")"
|
||||
# CLI returns a JSON array; unwrap and append elements.
|
||||
if [[ -z "$body" ]]; then
|
||||
continue
|
||||
fi
|
||||
if ! echo "$body" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
||||
append_merged_entry "$(jq -cn \
|
||||
--arg provider "$p" \
|
||||
--arg message "invalid JSON from provider" \
|
||||
'{provider: $provider, error: {message: $message}}')"
|
||||
continue
|
||||
fi
|
||||
inner="$(echo "$body" | jq -c '.[]')"
|
||||
while IFS= read -r entry; do
|
||||
append_merged_entry "$entry"
|
||||
done <<< "$inner"
|
||||
done
|
||||
merged+="]"
|
||||
|
||||
last_good_json=""
|
||||
if [[ -f "$LAST_GOOD" ]]; then
|
||||
last_good_json="$(jq -c 'select(type == "array")' "$LAST_GOOD" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
# Persist fresh successful provider snapshots without dropping older successful
|
||||
# entries for providers that failed this refresh.
|
||||
if [[ "$merged" != "[]" ]] && echo "$merged" | jq -e 'any(.error | not)' >/dev/null 2>&1; then
|
||||
if [[ -n "$last_good_json" ]]; then
|
||||
merged_for_cache="$(jq -c --argjson prev "$last_good_json" '
|
||||
def cache_key: (.provider // "") + "\u0000" + (.account // "");
|
||||
([$prev[]? | select((.error | not) and (.stale != true))
|
||||
| {key: cache_key, value: .}] | from_entries) as $ok_prev
|
||||
| ([.[]? | select(.error | not)
|
||||
| {key: cache_key, value: (del(.stale))}] | from_entries) as $ok_fresh
|
||||
| ($ok_prev + $ok_fresh) | [.[]]
|
||||
' <<< "$merged")"
|
||||
[[ -n "$merged_for_cache" ]] && echo "$merged_for_cache" > "$LAST_GOOD"
|
||||
else
|
||||
echo "$merged" | jq -c 'map(select(.error | not) | del(.stale))' > "$LAST_GOOD"
|
||||
fi
|
||||
last_good_json="$(jq -c 'select(type == "array")' "$LAST_GOOD" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
# Fill in errored or missing providers with the last successful snapshot for
|
||||
# display (marks them stale). Missing happens when a provider returns empty
|
||||
# output instead of a JSON array.
|
||||
if [[ -n "$last_good_json" ]]; then
|
||||
providers_json="$(printf '%s\n' "${PROVIDERS[@]}" | jq -R . | jq -s .)"
|
||||
merged="$(jq -c \
|
||||
--argjson prev "$last_good_json" \
|
||||
--argjson requested "$providers_json" '
|
||||
def cache_key: (.provider // "") + "\u0000" + (.account // "");
|
||||
([$prev[]? | select(.error | not) | {key: cache_key, value: .}] | from_entries) as $ok_prev
|
||||
| ([$prev[]? | select(.error | not)] | group_by(.provider) | map({key: .[0].provider, value: .}) | from_entries) as $prev_by_provider
|
||||
| (map(.provider) | unique) as $seen
|
||||
| map(if .error and $ok_prev[cache_key]
|
||||
then $ok_prev[cache_key] + {stale: true}
|
||||
else . end) as $direct_current
|
||||
| ($direct_current
|
||||
| map(select(.error and ($ok_prev[cache_key] | not)) as $err
|
||||
| ($prev_by_provider[$err.provider] // [])
|
||||
| map(. + {stale: true}))
|
||||
| add // []) as $provider_replacements
|
||||
| ($direct_current | map(select((.error and ($prev_by_provider[.provider] // [] | length > 0)) | not))) as $current
|
||||
| ($requested
|
||||
| map(. as $pid | select(($seen | index($pid)) | not))
|
||||
| map(($prev_by_provider[.] // []) | map(. + {stale: true}))
|
||||
| add // []) as $missing
|
||||
| $current + $provider_replacements + $missing
|
||||
' <<< "$merged")"
|
||||
fi
|
||||
|
||||
if [[ "$merged" == "[]" ]]; then
|
||||
printf '{"text":"","tooltip":"CodexBar: no provider data","class":"stale","percentage":0}\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$merged" | jq -c \
|
||||
--arg now "$(date -u +%FT%TZ)" \
|
||||
--arg bar_provider "$BAR_PROVIDER" \
|
||||
--arg reset_format "$RESET_TIME_FORMAT" '
|
||||
# Collect all usage windows across providers
|
||||
def provider_name(p):
|
||||
if p == null then "Unknown"
|
||||
else {
|
||||
abacus:"Abacus AI", alibaba:"Alibaba", alibabatokenplan:"Alibaba Token Plan",
|
||||
amp:"Amp", antigravity:"Antigravity", augment:"Augment",
|
||||
azureopenai:"Azure OpenAI", bedrock:"AWS Bedrock", chutes:"Chutes",
|
||||
claude:"Claude", clawrouter:"ClawRouter", codebuff:"Codebuff",
|
||||
codex:"Codex", commandcode:"Command Code", copilot:"Copilot",
|
||||
crof:"Crof", crossmodel:"CrossModel", cursor:"Cursor",
|
||||
deepgram:"Deepgram", deepseek:"DeepSeek", devin:"Devin",
|
||||
doubao:"Doubao", elevenlabs:"ElevenLabs", factory:"Droid",
|
||||
gemini:"Gemini", grok:"Grok", groq:"Groq",
|
||||
jetbrains:"JetBrains AI", kilo:"Kilo", kimi:"Kimi",
|
||||
kimik2:"Kimi K2", kiro:"Kiro", litellm:"LiteLLM",
|
||||
llmproxy:"LLM Proxy", manus:"Manus", mimo:"Xiaomi MiMo",
|
||||
minimax:"MiniMax", mistral:"Mistral", moonshot:"Moonshot / Kimi API",
|
||||
ollama:"Ollama", openai:"OpenAI", opencode:"OpenCode",
|
||||
opencodego:"OpenCode Go", openrouter:"OpenRouter", perplexity:"Perplexity",
|
||||
poe:"Poe", qoder:"Qoder", sakana:"Sakana AI",
|
||||
stepfun:"StepFun", synthetic:"Synthetic", t3chat:"T3 Chat",
|
||||
venice:"Venice", vertexai:"Vertex AI", warp:"Warp",
|
||||
windsurf:"Windsurf", zai:"z.ai", zed:"Zed"
|
||||
}[p] // (p | gsub("-"; " ") | ascii_upcase)
|
||||
end;
|
||||
|
||||
def entry_name(entry):
|
||||
entry as $entry
|
||||
| provider_name($entry.provider)
|
||||
+ (if (($entry.account // "") != "") then " (\($entry.account))" else "" end);
|
||||
|
||||
# Insert spaces the providers omit. Claude OAuth gives "May 17 at 6:20AM"
|
||||
# (no space before AM/PM); Claude CLI gives "Resets6:20am(Europe/Paris)"
|
||||
# (no space anywhere). Normalise both to a single, consistent style.
|
||||
def normalize_reset:
|
||||
sub("^Resets(?=\\S)"; "Resets ")
|
||||
| sub("^resets(?=\\S)"; "resets ")
|
||||
| gsub("(?<=\\S)\\("; " (")
|
||||
| gsub(",(?=\\S)"; ", ")
|
||||
| gsub("(?<=\\d)(?=[AaPp][Mm]\\b)"; " ");
|
||||
|
||||
# Format a UNIX timestamp in the system local timezone, tiered by how far
|
||||
# away the reset is. Today drops the date; this year drops the year.
|
||||
def fmt_local_ts:
|
||||
. as $ts
|
||||
| ($ts | strflocaltime("%Y-%m-%d")) as $rd
|
||||
| (now | strflocaltime("%Y-%m-%d")) as $td
|
||||
| ($ts | strflocaltime("%Y")) as $ry
|
||||
| (now | strflocaltime("%Y")) as $cy
|
||||
| if $rd == $td then ($ts | strflocaltime("%-I:%M %p %Z"))
|
||||
elif $ry == $cy then ($ts | strflocaltime("%b %-d at %-I:%M %p %Z"))
|
||||
else ($ts | strflocaltime("%b %-d %Y at %-I:%M %p %Z")) end;
|
||||
|
||||
# Same tiering, formatted in UTC. We hardcode the "UTC" suffix because
|
||||
# `strftime("%Z")` after `gmtime` is empty on some libc builds.
|
||||
def fmt_utc_ts:
|
||||
. as $ts
|
||||
| ($ts | gmtime | strftime("%Y-%m-%d")) as $rd
|
||||
| (now | gmtime | strftime("%Y-%m-%d")) as $td
|
||||
| ($ts | gmtime | strftime("%Y")) as $ry
|
||||
| (now | gmtime | strftime("%Y")) as $cy
|
||||
| if $rd == $td then ($ts | gmtime | strftime("%-I:%M %p UTC"))
|
||||
elif $ry == $cy then ($ts | gmtime | strftime("%b %-d at %-I:%M %p UTC"))
|
||||
else ($ts | gmtime | strftime("%b %-d %Y at %-I:%M %p UTC")) end;
|
||||
|
||||
# Build the trailing " — resets …" fragment for a usage window. Mode is
|
||||
# one of "provider" (preserve the provider string), "local", or "utc".
|
||||
# Relative phrases like "Resets in 2 hours" are kept verbatim even in
|
||||
# absolute modes — "in 2 hours" is more useful than a wall-clock time.
|
||||
def reset_phrase(w):
|
||||
if w == null then ""
|
||||
else (w.resetDescription // "" | normalize_reset) as $clean
|
||||
| (if $clean == "" then ""
|
||||
elif ($clean | test("^[Rr]esets")) then " — \($clean)"
|
||||
else " — resets \($clean)" end) as $from_desc
|
||||
| if $reset_format == "provider" then $from_desc
|
||||
elif ($clean | test("^[Rr]esets in ")) then $from_desc
|
||||
elif w.resetsAt != null then
|
||||
(try (w.resetsAt | fromdateiso8601) catch null) as $ts
|
||||
| if $ts == null then $from_desc
|
||||
elif $reset_format == "utc" then " — resets \($ts | fmt_utc_ts)"
|
||||
else " — resets \($ts | fmt_local_ts)" end
|
||||
else $from_desc
|
||||
end
|
||||
end;
|
||||
|
||||
def round_pct:
|
||||
((((. * 100) + 0.5) | floor) / 100);
|
||||
|
||||
def fmt_pct:
|
||||
(round_pct | tostring);
|
||||
|
||||
def fmt_window(w; name):
|
||||
if w == null or w.usedPercent == null then empty
|
||||
else "\(name): \(w.usedPercent | fmt_pct)%" + reset_phrase(w)
|
||||
end;
|
||||
|
||||
def fmt_named_window(item; provider):
|
||||
if item == null or item.window == null then empty
|
||||
else (item.title // item.id // "Extra quota") as $title
|
||||
| if (item | has("usageKnown")) and item.usageKnown == false then
|
||||
"\(provider) \($title): reset tracked" + reset_phrase(item.window)
|
||||
else fmt_window(item.window; "\(provider) \($title)") end
|
||||
end;
|
||||
|
||||
def money(v; currency):
|
||||
if currency == null or currency == "USD" then "$\((v * 100 | round / 100))"
|
||||
else "\(currency) \((v * 100 | round / 100))" end;
|
||||
|
||||
def status_line(entry):
|
||||
entry as $entry
|
||||
| if $entry.status == null then empty
|
||||
else ($entry.status.indicator // "unknown" | gsub("_"; " ") | ascii_upcase) as $indicator
|
||||
| if ($entry.status.description // "") == "" then
|
||||
"\(entry_name($entry)) status: \($indicator)"
|
||||
else
|
||||
"\(entry_name($entry)) status: \($indicator) — \($entry.status.description)"
|
||||
end
|
||||
end;
|
||||
|
||||
def pace_lines(entry):
|
||||
entry as $entry
|
||||
|
|
||||
[
|
||||
($entry.pace.primary.summary? | select(. != null) | "\(entry_name($entry)) session pace: \(.)"),
|
||||
($entry.pace.secondary.summary? | select(. != null) | "\(entry_name($entry)) weekly pace: \(.)")
|
||||
];
|
||||
|
||||
def credits_lines(entry):
|
||||
entry as $entry
|
||||
|
|
||||
[
|
||||
($entry.credits.remaining? | select(type == "number")
|
||||
| "\(entry_name($entry)) credits: \(money(.; "USD")) remaining"),
|
||||
($entry.credits.codexCreditLimit? | select(.limit? > 0)
|
||||
| "\(entry_name($entry)) \(.title // "Monthly credit limit"): \(money(.used; "USD")) / \(money(.limit; "USD"))"),
|
||||
($entry.openaiDashboard.creditsRemaining? | select(type == "number")
|
||||
| "\(entry_name($entry)) dashboard credits: \(money(.; "USD")) remaining"),
|
||||
($entry.openaiDashboard.codexCreditLimit? | select(.limit? > 0)
|
||||
| "\(entry_name($entry)) \(.title // "Monthly credit limit"): \(money(.used; "USD")) / \(money(.limit; "USD"))")
|
||||
];
|
||||
|
||||
def reset_credit_line(entry):
|
||||
entry as $entry
|
||||
| $entry.usage.codexResetCredits as $rc
|
||||
| if $rc == null then empty
|
||||
else ($rc.availableCount // ([$rc.credits[]? | select(.status == "available")] | length)) as $count
|
||||
| ([$rc.credits[]? | select(.status == "available" and .expires_at != null)] | sort_by(.expires_at) | .[0].expires_at) as $next
|
||||
| if $next != null then
|
||||
(try ($next | fromdateiso8601) catch null) as $ts
|
||||
| if $ts == null then "\(entry_name($entry)) reset credits: \($count) available"
|
||||
else "\(entry_name($entry)) reset credits: \($count) available; next expires \($ts | fmt_local_ts)" end
|
||||
elif $count > 0 then "\(entry_name($entry)) reset credits: \($count) available; no expiry"
|
||||
else "\(entry_name($entry)) reset credits: none available" end
|
||||
end;
|
||||
|
||||
def cost_lines(entry):
|
||||
entry as $entry
|
||||
|
|
||||
[
|
||||
($entry.usage.providerCost? | select(.used? != null)
|
||||
| "\(entry_name($entry)) \(.period // "budget"): \(money(.used; .currencyCode // "USD"))"
|
||||
+ (if .limit? > 0 then " / \(money(.limit; .currencyCode // "USD"))" else "" end)),
|
||||
($entry.usage.openRouterUsage.balance? | select(type == "number")
|
||||
| "\(entry_name($entry)) balance: \(money(.; "USD"))"),
|
||||
($entry.usage.sakanaPayAsYouGo.creditBalance? | select(type == "number")
|
||||
| "\(entry_name($entry)) pay-as-you-go balance: \(money(.; "USD"))"),
|
||||
($entry.antigravityPlanInfo? | (.planDisplayName // .displayName // .planShortName // .planName) as $plan
|
||||
| select($plan != null) | "\(entry_name($entry)) plan: \($plan)")
|
||||
];
|
||||
|
||||
def provider_lines(entry):
|
||||
entry as $entry
|
||||
| if $entry.error then
|
||||
"\(provider_name($entry.provider)): error — \($entry.error.message)"
|
||||
else
|
||||
(entry_name($entry)) as $name
|
||||
| [
|
||||
status_line($entry),
|
||||
fmt_window($entry.usage.primary; "\($name) primary"),
|
||||
fmt_window($entry.usage.secondary; "\($name) secondary"),
|
||||
fmt_window($entry.usage.tertiary; "\($name) tertiary"),
|
||||
($entry.usage.extraRateWindows[]? | fmt_named_window(.; $name)),
|
||||
reset_credit_line($entry)
|
||||
]
|
||||
+ pace_lines($entry)
|
||||
+ credits_lines($entry)
|
||||
+ cost_lines($entry)
|
||||
| map(select(. != null and . != "")) | join("\n")
|
||||
end;
|
||||
|
||||
def max_pct(entry):
|
||||
if entry.error then 0
|
||||
else
|
||||
[entry.usage.primary.usedPercent,
|
||||
entry.usage.secondary.usedPercent,
|
||||
entry.usage.tertiary.usedPercent]
|
||||
| map(select(. != null)) | (max // 0)
|
||||
end;
|
||||
|
||||
def pct_or_null(w):
|
||||
if w == null or w.usedPercent == null then null
|
||||
else (w.usedPercent | floor) end;
|
||||
|
||||
# When the user has pinned a provider for the bar text, surface session
|
||||
# and weekly inline ("3% • 12%"). Otherwise emit the global max%.
|
||||
def bar_text(entry):
|
||||
if entry == null or entry.error then "🤖 ⚠"
|
||||
else
|
||||
[pct_or_null(entry.usage.primary),
|
||||
pct_or_null(entry.usage.secondary)]
|
||||
| map(select(. != null))
|
||||
| if length == 0 then "🤖 —"
|
||||
elif length == 1 then "🤖 \(.[0])%"
|
||||
else "🤖 \(.[0])% • \(.[1])%" end
|
||||
end;
|
||||
|
||||
. as $all
|
||||
| ($all | map(max_pct(.)) | max // 0 | round_pct) as $pct
|
||||
| ($all | map(provider_lines(.)) | map(select(. != ""))) as $lines
|
||||
| ($all | all(.error)) as $all_errored
|
||||
| ($all | any(.stale == true)) as $any_stale
|
||||
| (if $bar_provider == "" then null
|
||||
else ($all | map(select(.provider == $bar_provider)) | .[0]) end) as $pinned
|
||||
| {
|
||||
text: (if $pinned != null then bar_text($pinned)
|
||||
elif $all_errored then "🤖 ⚠"
|
||||
else "🤖 \($pct | tostring)%" end),
|
||||
tooltip: ($lines | join("\n")),
|
||||
class: (if $all_errored then "stale"
|
||||
elif $pct >= 90 then "critical"
|
||||
elif $pct >= 70 then "warning"
|
||||
elif $any_stale then "stale"
|
||||
else "ok" end),
|
||||
percentage: $pct
|
||||
}
|
||||
'
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
meteobar --location 'Los Angeles' --units imperial --icons fontawesome --tooltip-format days --days 3 | \
|
||||
jq -c '
|
||||
.class |= (if type == "array" then .[0] else . end) |
|
||||
.tooltip |= (
|
||||
split("\n") |
|
||||
map(select(test("[╭╰╮]") | not)) |
|
||||
map(select(test("foreground=.#5c6370.>[─]") | not)) |
|
||||
map(
|
||||
gsub("<span foreground=.#61afef.>│</span> *"; "") |
|
||||
gsub(" *<span foreground=.#61afef.>│</span>"; "")
|
||||
) |
|
||||
map(select(test("^[[:space:]]*$") | not)) |
|
||||
join("\n")
|
||||
)
|
||||
'
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
/* codexbar-waybar — appended by install.sh */
|
||||
/* CodexBar Waybar module — append to ~/.config/waybar/user-style.css
|
||||
* or your theme's CSS. Tweak colors to taste; the four state classes
|
||||
* (ok / warning / critical / stale) are emitted by codexbar.sh.
|
||||
*/
|
||||
|
||||
#custom-codexbar {
|
||||
padding: 0 8px;
|
||||
margin: 0 4px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#custom-codexbar.ok {
|
||||
color: #16d3b4;
|
||||
}
|
||||
|
||||
#custom-codexbar.warning {
|
||||
color: #f5a623;
|
||||
background: alpha(#f5a623, 0.18);
|
||||
}
|
||||
|
||||
#custom-codexbar.critical {
|
||||
color: #ff5470;
|
||||
background: alpha(#ff5470, 0.22);
|
||||
animation: codexbar-pulse 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
#custom-codexbar.stale {
|
||||
/* Latest fetch failed or the value came from the cache. */
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@keyframes codexbar-pulse {
|
||||
from { background: alpha(#ff5470, 0.22); }
|
||||
to { background: alpha(#ff5470, 0.40); }
|
||||
}
|
||||
Reference in New Issue
Block a user