Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 100 additions & 12 deletions applet.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later

const Applet = imports.ui.applet;
const ByteArray = imports.byteArray;
const Settings = imports.ui.settings;
const PopupMenu = imports.ui.popupMenu;
const Tooltips = imports.ui.tooltips;
Expand All @@ -27,9 +28,14 @@ const CODEX_CLOUD_URL = "https://chatgpt.com/codex/cloud";
const ANALYTICS_URL = "https://chatgpt.com/codex/cloud/settings/analytics#usage";
const CHATGPT_LINUX_INSTALL_URL = "https://learn.chatgpt.com/docs/linux/linux-app";
const CODEX_CLI_INSTALL_URL = "https://learn.chatgpt.com/docs/codex/cli#getting-started";
const CHATGPT_RELEASE_VERSION = "26.825.51511";
const CHATGPT_RELEASE_DATE = "30.08.2026";
const CODEX_RELEASE_VERSION = "codex-cli 0.152.0";
const CODEX_RELEASE_DATE = "01.09.2026";
const PANEL_FONT_SCALE = 0.95;
const PANEL_LABEL_SCALE = 0.79;
const ACTIVITY_TOOLTIP_DELAY_MS = 120;
const LAUNCH_TOOLTIP_DELAY_MS = 420;
const POPUP_ACTION_GRID_WIDTH = 352;
// Cinnamon's one-pixel menu edge brings the visible popup width to 420 px.
const POPUP_RIGHT_PANEL_WIDTH = 419;
Expand Down Expand Up @@ -1039,7 +1045,10 @@ class ChatGptUsageApplet extends Applet.Applet {

_addLaunchButtons() {
const chatGptApp = this._chatGptAppInfo();
const codexCommand = this._codexTerminalCommand();
const codexPath = this._resolveCodexPath();
const codexCommand = this._codexTerminalCommand(codexPath);
const chatGptVersion = this._chatGptAppVersion(chatGptApp);
const codexVersion = this._commandVersion(codexPath);
const item = new PopupMenu.PopupBaseMenuItem({
reactive: false,
activate: false
Expand Down Expand Up @@ -1097,7 +1106,17 @@ class ChatGptUsageApplet extends Applet.Applet {
"Linux distributions.",
CHATGPT_LINUX_INSTALL_URL
);
}
},
UsageFormat.formatAppTooltip(
Boolean(chatGptApp),
chatGptVersion,
"chatgpt",
this._knownReleaseDate(
chatGptVersion,
CHATGPT_RELEASE_VERSION,
CHATGPT_RELEASE_DATE
)
)
);
this._codexButton = this._createLaunchButton(
"Codex CLI",
Expand All @@ -1114,7 +1133,17 @@ class ChatGptUsageApplet extends Applet.Applet {
"guide to install it, sign in, and then refresh this applet.",
CODEX_CLI_INSTALL_URL
);
}
},
UsageFormat.formatAppTooltip(
Boolean(codexPath),
codexVersion,
"",
this._knownReleaseDate(
codexVersion,
CODEX_RELEASE_VERSION,
CODEX_RELEASE_DATE
)
)
);
const refreshConfirmed = this._refreshConfirmed;
this._refreshButton = this._createLaunchButton(
Expand Down Expand Up @@ -1195,7 +1224,7 @@ class ChatGptUsageApplet extends Applet.Applet {
this.menu.addMenuItem(item);
}

_createLaunchButton(label, iconSpec, available, action) {
_createLaunchButton(label, iconSpec, available, action, tooltipText = null) {
const content = new St.BoxLayout({
vertical: false,
y_align: Clutter.ActorAlign.CENTER
Expand Down Expand Up @@ -1237,6 +1266,14 @@ class ChatGptUsageApplet extends Applet.Applet {
button._usageLabel = buttonLabel;
button._usageContent = content;
button._usageBusy = false;
if (tooltipText) {
button._usageTooltip = this._createPositionedTooltip(
button,
tooltipText,
true,
LAUNCH_TOOLTIP_DELAY_MS
);
}
button.style = this._launchButtonStyle(
available ? "normal" : "disabled",
iconSpec.compact,
Expand Down Expand Up @@ -1411,6 +1448,53 @@ class ChatGptUsageApplet extends Applet.Applet {
].join("; ") + ";";
}

_readCommandVersion(argv) {
if (!Array.isArray(argv) || argv.length === 0) return null;
try {
const [ok, stdout, , status] = GLib.spawn_sync(
null,
argv,
null,
GLib.SpawnFlags.SEARCH_PATH,
null
);
if (!ok || status !== 0) return null;
const output = ByteArray.toString(stdout).trim();
if (!output) return null;
return output.split(/\r?\n/)[0].trim().slice(0, 120) || null;
} catch (error) {
global.logWarning(`${UUID}: could not read application version: ${error}`);
return null;
}
}

_commandVersion(executable) {
if (!executable) return null;
return this._readCommandVersion([executable, "--version"]);
}

_knownReleaseDate(version, knownVersion, releaseDate) {
return version === knownVersion ? releaseDate : null;
}

_chatGptAppVersion(appInfo) {
if (!appInfo) return null;
try {
const executable = appInfo.get_executable();
const commandVersion = this._commandVersion(executable);
if (commandVersion) return commandVersion;
for (const key of ["Version", "X-AppImage-Version", "X-Version"]) {
const desktopVersion = appInfo.get_string(key);
if (desktopVersion && String(desktopVersion).trim()) {
return String(desktopVersion).trim();
}
}
} catch (error) {
global.logWarning(`${UUID}: could not inspect ChatGPT app version: ${error}`);
}
return null;
}

_chatGptAppInfo() {
try {
return Gio.DesktopAppInfo.new("chatgpt.desktop");
Expand All @@ -1420,8 +1504,7 @@ class ChatGptUsageApplet extends Applet.Applet {
}
}

_codexTerminalCommand() {
const codex = this._resolveCodexPath();
_codexTerminalCommand(codex = this._resolveCodexPath()) {
if (!codex) return null;
try {
const terminalSettings = new Gio.Settings({
Expand Down Expand Up @@ -1801,7 +1884,7 @@ class ChatGptUsageApplet extends Applet.Applet {
);
slot.accessible_name = UsageFormat.formatAccessibleTooltip(tooltipText);
this._activityTooltips.push(
this._createActivityTooltip(slot, tooltipText)
this._createPositionedTooltip(slot, tooltipText)
);
plot.add_child(slot);
});
Expand Down Expand Up @@ -1878,7 +1961,12 @@ class ChatGptUsageApplet extends Applet.Applet {
}
}

_createActivityTooltip(slot, text) {
_createPositionedTooltip(
slot,
text,
centerOnPointer = false,
showDelayMs = ACTIVITY_TOOLTIP_DELAY_MS
) {
const tooltip = new Tooltips.Tooltip(slot, text);
tooltip.set_text(text);
const nativeShow = tooltip.show.bind(tooltip);
Expand All @@ -1892,9 +1980,9 @@ class ChatGptUsageApplet extends Applet.Applet {
naturalWidth
);
const cursorSize = tooltip.desktop_settings.get_int("cursor-size");
const preferredLeft = tooltip.mousePosition[0] + Math.round(
cursorSize / 2
);
const preferredLeft = centerOnPointer
? tooltip.mousePosition[0] - Math.round(naturalWidth / 2)
: tooltip.mousePosition[0] + Math.round(cursorSize / 2);
const preferredTop = tooltip.mousePosition[1] - naturalHeight - Math.round(
cursorSize / 2
);
Expand Down Expand Up @@ -1923,7 +2011,7 @@ class ChatGptUsageApplet extends Applet.Applet {
slot.connect("enter-event", () => {
cancelFastShow();
tooltip._fastShowTimeoutId = Mainloop.timeout_add(
ACTIVITY_TOOLTIP_DELAY_MS,
showDelayMs,
() => {
tooltip._fastShowTimeoutId = 0;
if (!slot.has_pointer || tooltip.visible) return GLib.SOURCE_REMOVE;
Expand Down
30 changes: 30 additions & 0 deletions tests/test-usage-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,36 @@ assertEqual(
"First. Second. Third",
"All accessible tooltip line breaks"
);
assertEqual(
UsageFormat.formatAppTooltip(true, "26.825.51511", "chatgpt"),
"chatgpt 26.825.51511",
"ChatGPT tooltip includes package and version"
);
assertEqual(
UsageFormat.formatAppTooltip(true, "26.825.51511", "chatgpt", "30.08.2026"),
"chatgpt 26.825.51511 — 30.08.2026",
"Installed ChatGPT tooltip includes release date"
);
assertEqual(
UsageFormat.formatAppTooltip(true, "codex-cli 0.152.0", "", "01.09.2026"),
"codex-cli 0.152.0 — 01.09.2026",
"Installed Codex tooltip includes release date"
);
assertEqual(
UsageFormat.formatAppTooltip(true, "new-version", "chatgpt", null),
"chatgpt new-version",
"Unknown application version has no guessed release date"
);
assertEqual(
UsageFormat.formatAppTooltip(false),
"not installed",
"Missing application tooltip contains only not installed"
);
assertEqual(
UsageFormat.formatAppTooltip(true),
"version unavailable",
"Installed application without readable version reports unavailable version"
);

assertEqual(
UsageFormat.normalizeNotificationThresholds(25, 10).valid,
Expand Down
13 changes: 12 additions & 1 deletion usage-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,16 @@ function formatRelativeTime(epochSeconds, nowSeconds = null) {
return `${Math.floor(elapsedSeconds / 86400)}d ago`;
}

function formatAppTooltip(installed, version = null, prefix = "", releaseDate = null) {
const status = installed
? (String(version || "").trim() || "version unavailable")
: "not installed";
const datedStatus = installed && releaseDate
? `${status} — ${releaseDate}`
: status;
return installed && prefix ? `${prefix} ${datedStatus}` : datedStatus;
}

module.exports = {
summarizeWindows,
listQuotaWindows,
Expand All @@ -563,5 +573,6 @@ module.exports = {
formatActivityBucketTooltip,
formatAccessibleTooltip,
formatTimestamp,
formatRelativeTime
formatRelativeTime,
formatAppTooltip
};