feat: a whole bunch of nice things
This commit is contained in:
parent
7c6b39872d
commit
e6022b3fa6
12 changed files with 472 additions and 22 deletions
207
gnome-extensions/opencode-token-usage/extension.js
Normal file
207
gnome-extensions/opencode-token-usage/extension.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import Clutter from 'gi://Clutter';
|
||||
import Cogl from 'gi://Cogl';
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St';
|
||||
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
|
||||
const COMMAND = '@opencodeTokenUsage@/bin/opencode-token-usage';
|
||||
const REFRESH_SECONDS = 60;
|
||||
const GRAPH_WIDTH = 96;
|
||||
const GRAPH_HEIGHT = 16;
|
||||
const CLASSES = [
|
||||
'opencode-token-usage-normal',
|
||||
'opencode-token-usage-warning',
|
||||
'opencode-token-usage-critical',
|
||||
'opencode-token-usage-missing',
|
||||
];
|
||||
|
||||
function colorFromString(colorString) {
|
||||
if (Cogl.Color.from_string) {
|
||||
const [ok, color] = Cogl.Color.from_string(colorString);
|
||||
if (ok)
|
||||
return color;
|
||||
}
|
||||
|
||||
return Clutter.Color.from_string(colorString)[1];
|
||||
}
|
||||
|
||||
function setSourceColor(cr, color) {
|
||||
if (Clutter.cairo_set_source_color)
|
||||
Clutter.cairo_set_source_color(cr, color);
|
||||
else
|
||||
cr.setSourceColor(color);
|
||||
}
|
||||
|
||||
const TokenUsageGraph = GObject.registerClass(
|
||||
class TokenUsageGraph extends St.DrawingArea {
|
||||
constructor() {
|
||||
super({
|
||||
style_class: 'opencode-token-usage-graph',
|
||||
reactive: false,
|
||||
});
|
||||
|
||||
this._values = [];
|
||||
this._background = colorFromString('#ffffff16');
|
||||
this._fill = colorFromString('#23863699');
|
||||
this._line = colorFromString('#58a6ff');
|
||||
this._scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
||||
this.set_width(GRAPH_WIDTH * this._scaleFactor);
|
||||
this.set_height(GRAPH_HEIGHT * this._scaleFactor);
|
||||
this.connect('repaint', this._draw.bind(this));
|
||||
}
|
||||
|
||||
setValues(values) {
|
||||
this._values = Array.isArray(values) ? values.filter(value => Number.isFinite(value)) : [];
|
||||
this.queue_repaint();
|
||||
}
|
||||
|
||||
_point(index, width, height, max) {
|
||||
const x = this._values.length <= 1 ? width : index * (width / (this._values.length - 1));
|
||||
const y = height - 1 - (this._values[index] / max) * Math.max(1, height - 2);
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
_draw() {
|
||||
const [width, height] = this.get_surface_size();
|
||||
const cr = this.get_context();
|
||||
|
||||
setSourceColor(cr, this._background);
|
||||
cr.rectangle(0, 0, width, height);
|
||||
cr.fill();
|
||||
|
||||
if (this._values.length === 0) {
|
||||
cr.$dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const max = Math.max(1, ...this._values);
|
||||
|
||||
cr.moveTo(0, height);
|
||||
for (let index = 0; index < this._values.length; index++) {
|
||||
const [x, y] = this._point(index, width, height, max);
|
||||
cr.lineTo(x, y);
|
||||
}
|
||||
cr.lineTo(width, height);
|
||||
cr.closePath();
|
||||
setSourceColor(cr, this._fill);
|
||||
cr.fill();
|
||||
|
||||
const [x0, y0] = this._point(0, width, height, max);
|
||||
cr.moveTo(x0, y0);
|
||||
for (let index = 1; index < this._values.length; index++) {
|
||||
const [x, y] = this._point(index, width, height, max);
|
||||
cr.lineTo(x, y);
|
||||
}
|
||||
cr.setLineWidth(Math.max(1, this._scaleFactor));
|
||||
setSourceColor(cr, this._line);
|
||||
cr.stroke();
|
||||
cr.$dispose();
|
||||
}
|
||||
});
|
||||
|
||||
const TokenUsageIndicator = GObject.registerClass(
|
||||
class TokenUsageIndicator extends PanelMenu.Button {
|
||||
constructor() {
|
||||
super(0.0, 'OpenCode Token Usage');
|
||||
|
||||
this.add_style_class_name('opencode-token-usage');
|
||||
this._prefix = new St.Label({
|
||||
text: 'tok',
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
style_class: 'opencode-token-usage-label',
|
||||
});
|
||||
this._graph = new TokenUsageGraph();
|
||||
this._value = new St.Label({
|
||||
text: '0',
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
style_class: 'opencode-token-usage-label',
|
||||
});
|
||||
this.add_child(this._prefix);
|
||||
this.add_child(this._graph);
|
||||
this.add_child(this._value);
|
||||
|
||||
this._refresh();
|
||||
this._timer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, REFRESH_SECONDS, () => {
|
||||
this._refresh();
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
});
|
||||
}
|
||||
|
||||
_setClass(nextClass) {
|
||||
for (const item of CLASSES)
|
||||
this.remove_style_class_name(item);
|
||||
|
||||
this.add_style_class_name(`opencode-token-usage-${nextClass || 'normal'}`);
|
||||
}
|
||||
|
||||
_applyPayload(payload) {
|
||||
this._value.text = payload.value || '0';
|
||||
this._graph.setValues(payload.values || []);
|
||||
this.menu.removeAll();
|
||||
for (const line of (payload.tooltip || 'OpenCode token usage').split('\n')) {
|
||||
const item = new PopupMenu.PopupMenuItem(line, {reactive: false, can_focus: false});
|
||||
this.menu.addMenuItem(item);
|
||||
}
|
||||
this._setClass(payload.class || 'normal');
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
let proc;
|
||||
try {
|
||||
proc = Gio.Subprocess.new(
|
||||
[COMMAND],
|
||||
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
|
||||
);
|
||||
} catch (error) {
|
||||
this._applyPayload({
|
||||
text: 'tok error',
|
||||
tooltip: `Unable to start token usage command: ${error.message}`,
|
||||
class: 'critical',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
proc.communicate_utf8_async(null, null, (subprocess, result) => {
|
||||
try {
|
||||
const [, stdout, stderr] = subprocess.communicate_utf8_finish(result);
|
||||
if (!subprocess.get_successful())
|
||||
throw new Error(stderr.trim() || `command exited ${subprocess.get_exit_status()}`);
|
||||
|
||||
this._applyPayload(JSON.parse(stdout));
|
||||
} catch (error) {
|
||||
this._applyPayload({
|
||||
text: 'tok error',
|
||||
tooltip: `Unable to read OpenCode token usage: ${error.message}`,
|
||||
class: 'critical',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._timer) {
|
||||
GLib.Source.remove(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
export default class OpenCodeTokenUsageExtension extends Extension {
|
||||
enable() {
|
||||
this._indicator = new TokenUsageIndicator();
|
||||
Main.panel.addToStatusArea('opencode-token-usage', this._indicator, 0, 'right');
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._indicator?.destroy();
|
||||
this._indicator = null;
|
||||
}
|
||||
}
|
||||
6
gnome-extensions/opencode-token-usage/metadata.json
Normal file
6
gnome-extensions/opencode-token-usage/metadata.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"uuid": "opencode-token-usage@jetpham.github.com",
|
||||
"name": "OpenCode Token Usage",
|
||||
"description": "Shows OpenCode token usage and daily cost estimates in the GNOME top bar.",
|
||||
"shell-version": ["49"]
|
||||
}
|
||||
22
gnome-extensions/opencode-token-usage/stylesheet.css
Normal file
22
gnome-extensions/opencode-token-usage/stylesheet.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.opencode-token-usage {
|
||||
spacing: 5px;
|
||||
}
|
||||
|
||||
.opencode-token-usage-label {
|
||||
font-family: "CommitMono Nerd Font", monospace;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.opencode-token-usage-graph {
|
||||
min-width: 96px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.opencode-token-usage-warning .opencode-token-usage-label {
|
||||
color: #d29922;
|
||||
}
|
||||
|
||||
.opencode-token-usage-critical .opencode-token-usage-label,
|
||||
.opencode-token-usage-missing .opencode-token-usage-label {
|
||||
color: #f85149;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue