A Quickshell plugin for the Omarchy shell (v4's bar is Quickshell, not Waybar). The icon is the mode; clicking it opens a panel that switches. State comes from watching health.json rather than polling the CLI, so the widget costs nothing while idle and a switch made in a terminal shows up in the bar on its own. state.json is the fallback, because it exists from install onward where health.json only appears once claude-mode has run. The marks are the providers' real logos, drawn as vector paths through QtQuick.Shapes rather than set as font glyphs - three of the four have no Nerd Font pictograph at all, and paths take the bar's foreground colour and follow the theme. Claude, OpenRouter and LM Studio from simple-icons, Z.AI from lobe-icons; trademarks belong to their owners. Zhipu's mark was rejected for Z.AI: it is a dense lattice that is unreadable below ~40px. Rendering them at 13px took three fixes. A layer rasterises the Shape at its own size and then scales the texture, so a 24px buffer minified to 13 resampled two pixels into one and the Claude burst lost rays; without the layer the scale is a transform on the geometry and rasterisation happens once, at final resolution. CurveRenderer replaces the tessellating default. And the size is forced odd, because a radially symmetric mark puts its vertical and horizontal arms on the centre line, which is a pixel centre at odd sizes and the seam between two pixels at even ones, where each arm splits its coverage and greys out. Measured against a cairo render at the same size, the result is now identical. Sizing is measured rather than guessed. Every stock glyph in this bar paints 11px of ink; the marks fill their box instead of carrying a font's padding, so the box is the smaller number. Each mark also carries an optical scale from two measurements of a 200px render - LM Studio's filled container covers 69% of its box against ~38% for the others, and Z.AI and OpenRouter are wide-but-short marks spanning ~84% of the box height. Choosing a target does not switch immediately. It runs the CLI's own preflight, and if that refuses, the panel names what is missing and offers the fix - a terminal for the hidden key prompt, a re-check once a server is up, or the server form. If sessions are running it lists them by terminal and directory, marks any mid-request, and asks whether to restart them, close them, or leave them. The session action is applied strictly after the write, since restarting first would only bring them back up on the provider just left. The server form edits an LM Studio preset's base URL and whether it needs an API key, reachable from the gear on any LM Studio preset row - per row, because two presets can point at two different machines - and from the failure card. The shipped local default is untouched unless it is changed.
161 lines
8.6 KiB
JavaScript
161 lines
8.6 KiB
JavaScript
.pragma library
|
|
|
|
// The four modes claude-mode understands, in the order the CLI menu lists
|
|
// them. Kept here rather than in either QML file because the bar widget needs
|
|
// the glyph and the panel needs the prose, and a second copy of this table is
|
|
// exactly the sort of thing that drifts.
|
|
|
|
// Material Design icons from the Nerd Font patch set, written as codepoints
|
|
// rather than literals so they survive any editor or transport that is not
|
|
// UTF-8 clean. All verified present in JetBrainsMono Nerd Font, the shell's
|
|
// default family.
|
|
//
|
|
// Deliberately none of them a robot: omarchy.agents already paints one, and it
|
|
// commonly sits in the same stretch of bar, where two identical robots read as
|
|
// one widget drawn twice. Same reason lmstudio is a server rack rather than a
|
|
// screen - omarchy.monitor owns that silhouette.
|
|
var GLYPH = {
|
|
anthropic: String.fromCodePoint(0xF0674), // sparkles - the real thing
|
|
openrouter: String.fromCodePoint(0xF0469), // router - remote gateway
|
|
zai: String.fromCodePoint(0xF015F), // cloud - hosted GLM plan
|
|
lmstudio: String.fromCodePoint(0xF048B), // server - local LM Studio
|
|
unknown: String.fromCodePoint(0xF0625) // question - nothing readable
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Brand marks
|
|
//
|
|
// The real logo for each provider, as SVG path data in a 24x24 viewBox so the
|
|
// whole set scales from a single number. Drawn with QtQuick.Shapes rather than
|
|
// set as font glyphs: three of the four have no Nerd Font pictograph at all,
|
|
// and a stand-in pictograph is both less recognisable and impossible to keep
|
|
// visually consistent with the marks beside it.
|
|
//
|
|
// Sources: Claude, OpenRouter and LM Studio from simple-icons; Z.AI from
|
|
// lobe-icons. All four are monochrome single-path marks, which is what lets
|
|
// them take the bar's foreground colour and follow the theme. Trademarks
|
|
// belong to their respective owners.
|
|
//
|
|
// LM Studio's mark keeps its rounded-square container: the four bars alone
|
|
// read as a hamburger menu, and the container is what makes it that logo. It
|
|
// relies on nonzero winding to knock the bars out, which is QtQuick.Shapes'
|
|
// default, so no fill rule is set anywhere here.
|
|
var LOGO = {
|
|
anthropic: "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z",
|
|
openrouter: "M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z",
|
|
zai: "M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z",
|
|
lmstudio: "M14.025 0c3.492 0 5.237 0 6.571.68a6.24 6.24 0 0 1 2.725 2.724C24 4.738 24 6.484 24 9.975v4.05c0 3.492 0 5.237-.68 6.571a6.24 6.24 0 0 1-2.724 2.725c-1.334.679-3.08.679-6.571.679h-4.05c-3.492 0-5.237 0-6.571-.68A6.24 6.24 0 0 1 .68 20.597C0 19.262 0 17.516 0 14.025v-4.05c0-3.492 0-5.237.68-6.571A6.23 6.23 0 0 1 3.404.68C4.738 0 6.484 0 9.975 0zM7.688 16.313a1.313 1.313 0 0 0 0 2.625h11.625a1.313 1.313 0 0 0 0-2.625zm-3-3.75a1.313 1.313 0 0 0 0 2.624h11.625a1.313 1.313 0 0 0 0-2.624zm3-3.75a1.313 1.313 0 0 0 0 2.624h11.625a1.313 1.313 0 0 0 0-2.624zm-3-3.75a1.313 1.313 0 0 0 0 2.625h11.625a1.313 1.313 0 0 0 0-2.625z"
|
|
}
|
|
|
|
// Equal nominal size is not equal apparent size, so each mark is corrected by
|
|
// two measurements taken off a 200px render of it:
|
|
//
|
|
// bbox Z.AI and OpenRouter are wide-but-short marks - their ink spans only
|
|
// ~84% of the box height, so at equal nominal size they read smaller.
|
|
// ink LM Studio's filled container covers 69% of its box against ~38% for
|
|
// the other three, so at equal nominal size it reads much heavier.
|
|
//
|
|
// The correction is the geometric mean of both signals, which pulls the short
|
|
// marks up and the solid one down without letting either measure dominate.
|
|
var LOGO_SCALE = {
|
|
anthropic: 0.99,
|
|
openrouter: 1.12,
|
|
zai: 1.07,
|
|
lmstudio: 0.86
|
|
}
|
|
|
|
function logo(mode) {
|
|
return LOGO[mode] || ""
|
|
}
|
|
|
|
function logoScale(mode) {
|
|
return LOGO_SCALE[mode] || 1.0
|
|
}
|
|
|
|
var TITLE = {
|
|
anthropic: "Anthropic",
|
|
openrouter: "OpenRouter",
|
|
zai: "Z.AI",
|
|
lmstudio: "LM Studio"
|
|
}
|
|
|
|
var BLURB = {
|
|
anthropic: "Your subscription login. No gateway, no API key.",
|
|
openrouter: "Remote gateway, pay per token, any vendor.",
|
|
zai: "GLM coding plan on Z.AI's Anthropic endpoint.",
|
|
lmstudio: "Local LM Studio server. Offline and free."
|
|
}
|
|
|
|
var ORDER = ["anthropic", "openrouter", "zai", "lmstudio"]
|
|
|
|
// Everything except anthropic switches with a named preset.
|
|
function needsPreset(mode) {
|
|
return mode !== "" && mode !== "anthropic"
|
|
}
|
|
|
|
function glyph(mode) {
|
|
return GLYPH[mode] || GLYPH.unknown
|
|
}
|
|
|
|
function title(mode) {
|
|
return TITLE[mode] || "not installed"
|
|
}
|
|
|
|
function blurb(mode) {
|
|
return BLURB[mode] || ""
|
|
}
|
|
|
|
// Bar label. The preset matters more than the provider once you are on a
|
|
// gateway - "openrouter" tells you nothing you did not already know from the
|
|
// icon, where "cheap" is the thing you actually want to catch sight of.
|
|
function shortLabel(mode, preset) {
|
|
if (mode === "") return ""
|
|
if (mode === "anthropic") return "anthropic"
|
|
return preset !== "" ? preset : mode
|
|
}
|
|
|
|
function defaultRoot() {
|
|
return homeDir() + "/.claude-mode"
|
|
}
|
|
|
|
function homeDir() {
|
|
// Qt exposes no home path to a .pragma library, and the shell always runs as
|
|
// the owning user, so derive it the one way that holds in both places.
|
|
var url = Qt.resolvedUrl(".").toString()
|
|
var m = url.match(/^file:\/\/(\/home\/[^\/]+)\//)
|
|
if (m) return m[1]
|
|
m = url.match(/^file:\/\/(\/Users\/[^\/]+)\//)
|
|
return m ? m[1] : "/root"
|
|
}
|
|
|
|
// Presets from health.json that belong to one provider, default first so the
|
|
// list opens on the one `claude-mode <mode>` would have picked.
|
|
function presetsFor(health, mode, defaults) {
|
|
var all = (health && health.presets) ? health.presets : []
|
|
var out = []
|
|
for (var i = 0; i < all.length; i++) {
|
|
if (String(all[i].provider) === mode) out.push(all[i])
|
|
}
|
|
var preferred = defaults[mode]
|
|
out.sort(function (a, b) {
|
|
if (a.name === preferred) return -1
|
|
if (b.name === preferred) return 1
|
|
return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Mirrors default_preset_for() in the CLI.
|
|
var DEFAULT_PRESET = {
|
|
openrouter: "default",
|
|
zai: "zai",
|
|
lmstudio: "lmstudio"
|
|
}
|
|
|
|
function contextLabel(tokens) {
|
|
if (!tokens) return ""
|
|
if (tokens >= 1000000) return (Math.round(tokens / 100000) / 10) + "M context"
|
|
if (tokens >= 1000) return Math.round(tokens / 1024) + "k context"
|
|
return tokens + " context"
|
|
}
|