From 64b564b52ba7d22ba8129821ab4b30542d971ff5 Mon Sep 17 00:00:00 2001 From: Stanton Burchett Date: Thu, 30 Jul 2026 14:50:01 -0600 Subject: Focus the emitting tab when a notification is clicked Resolve the OSC 9 onclick TODO: clicking a notification now brings the window to the front and selects the exact tab/pane that emitted the sequence. The title is read live from the tab each time instead of being captured once when the session is attached. Adds tabby-core as a dependency for AppService/HostWindowService. --- README.md | 4 ++++ package.json | 1 + src/osc-notify-decorator.ts | 24 ++++++++++++++++++++---- src/osc-notify-middleware.ts | 16 +++++++++++----- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c735aa8..c6805a4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ format: OSC 9 ; [message] ST ``` +Clicking the notification brings Tabby to the front and switches to the exact +tab/pane that emitted the sequence. The notification title reflects the tab's +current title at the time the sequence is received. + ### Example commands ```sh diff --git a/package.json b/package.json index c4b0764..52c828b 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "@angular/core": "^16.2.3", "rxjs": "^7.8.1", + "tabby-core": "^1.0.197-nightly.1", "tabby-terminal": "^1.0.197-nightly.1" } } diff --git a/src/osc-notify-decorator.ts b/src/osc-notify-decorator.ts index 7d32f1c..abfb796 100644 --- a/src/osc-notify-decorator.ts +++ b/src/osc-notify-decorator.ts @@ -1,25 +1,41 @@ import OscNotifyMiddleware from "./osc-notify-middleware"; import { Injectable } from "@angular/core"; import { TerminalDecorator, BaseSession, BaseTerminalTabComponent } from "tabby-terminal"; +import { AppService, HostWindowService } from "tabby-core"; @Injectable() export default class OscNotifyDecorator extends TerminalDecorator { + constructor( + private app: AppService, + private hostWindow: HostWindowService, + ) { + super(); + } + attach(tab: BaseTerminalTabComponent): void { // Subscribe to attach to future sessions in this tab tab.sessionChanged$.subscribe((session) => { if (session) { - this.attachToSession(session, (tab).title); + this.attachToSession(session, tab); } }); // Attach the current session for this tab if (tab.session) { - this.attachToSession(tab.session, (tab).title); + this.attachToSession(tab.session, tab); } } - private attachToSession(session: BaseSession, tabTitle: string) { - const middleware = new OscNotifyMiddleware(tabTitle); + private attachToSession(session: BaseSession, tab: BaseTerminalTabComponent) { + const middleware = new OscNotifyMiddleware( + () => (tab).title, + () => { + this.hostWindow.bringToFront(); + const parent = this.app.getParentTab(tab); + this.app.selectTab(parent ?? tab); + parent?.focus(tab); + }, + ); session.middleware.push(middleware); } diff --git a/src/osc-notify-middleware.ts b/src/osc-notify-middleware.ts index 15548f4..ad2946c 100644 --- a/src/osc-notify-middleware.ts +++ b/src/osc-notify-middleware.ts @@ -5,12 +5,14 @@ const oscPrefix = Buffer.from("\x1b]"); const oscSuffix = Buffer.from("\x07"); export default class OscNotifyMiddleware extends SessionMiddleware { - tabTitle: string; + getTitle: () => string; + onClick: () => void; - constructor(tabTitle: string) { + constructor(getTitle: () => string, onClick: () => void) { super(); - this.tabTitle = tabTitle; + this.getTitle = getTitle; + this.onClick = onClick; } get cwdReported$(): Observable { @@ -22,6 +24,7 @@ export default class OscNotifyMiddleware extends SessionMiddleware { private cwdReported = new Subject(); private copyRequested = new Subject(); + private notifications: Notification[] = []; feedFromSession(data: Buffer): void { let startIndex = 0; @@ -36,10 +39,11 @@ export default class OscNotifyMiddleware extends SessionMiddleware { const oscCode = parseInt(oscCodeString); if (oscCode === 9) { - // TODO: onclick switch to tab - new Notification(this.tabTitle, { + const notification = new Notification(this.getTitle() || "Terminal", { body: oscParams[0] }); + notification.onclick = () => this.onClick(); + this.notifications.push(notification); } else { continue; } @@ -49,6 +53,8 @@ export default class OscNotifyMiddleware extends SessionMiddleware { } close(): void { + this.notifications.forEach((notification) => notification.close()); + this.notifications = []; this.cwdReported.complete(); this.copyRequested.complete(); super.close(); -- cgit v1.3.1 From 46e3650242cfe38941eb747e601484bd19c137bd Mon Sep 17 00:00:00 2001 From: Stanton Burchett Date: Thu, 30 Jul 2026 14:51:22 -0600 Subject: Add OSC 777 support and clean up the notification title Support OSC 777 ; notify ; title ; body (the rxvt/urxvt notification form) so a program can set an explicit title and body, with the semicolon-safe body rejoined via slice(2).join(";"). Clean the OSC 9 title: prefer a user-set custom tab name, and reduce filesystem-path titles to their last segment so shell tabs no longer show a long, truncated cwd. --- README.md | 12 ++++++++++++ package.json | 2 +- src/osc-notify-decorator.ts | 2 +- src/osc-notify-middleware.ts | 26 +++++++++++++++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c6805a4..7ba1514 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,18 @@ Clicking the notification brings Tabby to the front and switches to the exact tab/pane that emitted the sequence. The notification title reflects the tab's current title at the time the sequence is received. +The OSC 9 title is cleaned up: it prefers a user-set custom tab name, and when +the title looks like a filesystem path (e.g. a shell tab showing its cwd) only +the last path segment is used instead of the full, often-truncated path. + +`OSC 777 ; notify ; title ; body` (the rxvt/urxvt notification form) is also +supported, letting a program set an explicit title and body: + +```sh +# creates a notification titled 'Build' with body 'Done' +printf "\033]777;notify;Build;Done\a" +``` + ### Example commands ```sh diff --git a/package.json b/package.json index 52c828b..ed2739d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tabby-osc-notify", - "version": "1.0.0", + "version": "1.1.0", "main": "dist/index.js", "typings": "dist/index.d.ts", "scripts": { diff --git a/src/osc-notify-decorator.ts b/src/osc-notify-decorator.ts index abfb796..4beb029 100644 --- a/src/osc-notify-decorator.ts +++ b/src/osc-notify-decorator.ts @@ -28,7 +28,7 @@ export default class OscNotifyDecorator extends TerminalDecorator { private attachToSession(session: BaseSession, tab: BaseTerminalTabComponent) { const middleware = new OscNotifyMiddleware( - () => (tab).title, + () => (tab).customTitle || (tab).title, () => { this.hostWindow.bringToFront(); const parent = this.app.getParentTab(tab); diff --git a/src/osc-notify-middleware.ts b/src/osc-notify-middleware.ts index ad2946c..dcfcc1a 100644 --- a/src/osc-notify-middleware.ts +++ b/src/osc-notify-middleware.ts @@ -4,6 +4,16 @@ import { Observable, Subject } from "rxjs"; const oscPrefix = Buffer.from("\x1b]"); const oscSuffix = Buffer.from("\x07"); +function cleanTitle(raw: string): string { + const t = (raw ?? "").trim(); + if (!t) return "Terminal"; + if (t.includes("/") || t.includes("\\")) { + const segs = t.split(/[/\\]/).filter(s => s.length > 0); + return segs.length ? segs[segs.length - 1] : t; + } + return t; +} + export default class OscNotifyMiddleware extends SessionMiddleware { getTitle: () => string; onClick: () => void; @@ -39,11 +49,11 @@ export default class OscNotifyMiddleware extends SessionMiddleware { const oscCode = parseInt(oscCodeString); if (oscCode === 9) { - const notification = new Notification(this.getTitle() || "Terminal", { - body: oscParams[0] - }); - notification.onclick = () => this.onClick(); - this.notifications.push(notification); + this.showNotification(cleanTitle(this.getTitle()), oscParams[0] ?? ""); + } else if (oscCode === 777 && oscParams[0] === "notify") { + const title = oscParams[1] || cleanTitle(this.getTitle()); + const body = oscParams.slice(2).join(";"); + this.showNotification(title, body); } else { continue; } @@ -52,6 +62,12 @@ export default class OscNotifyMiddleware extends SessionMiddleware { super.feedFromSession(data); } + private showNotification(title: string, body: string): void { + const notification = new Notification(title || "Terminal", { body }); + notification.onclick = () => this.onClick(); + this.notifications.push(notification); + } + close(): void { this.notifications.forEach((notification) => notification.close()); this.notifications = []; -- cgit v1.3.1 From aed642e92428b63afb338ced5c20f7fe1d9330ec Mon Sep 17 00:00:00 2001 From: Jomar Milan Date: Fri, 14 Aug 2026 22:11:53 -0700 Subject: Specify version for @types/node @types/node is a transitive dependency but version constraints are too loose for old typescript version --- package-lock.json | 15 ++++++++------- package.json | 5 +++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1642395..b847053 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "tabby-terminal": "^1.0.197-nightly.1" }, "devDependencies": { + "@types/node": "^20.19.43", "apply-loader": "^2.0.0", "css-loader": "^6.8.1", "pug-loader": "^2.4.0", @@ -604,13 +605,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~6.21.0" } }, "node_modules/@webassemblyjs/ast": { @@ -3545,9 +3546,9 @@ "peer": true }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 53d3aad..b12143b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "tabby-plugin" ], "devDependencies": { + "@types/node": "^20.19.43", "apply-loader": "^2.0.0", "css-loader": "^6.8.1", "pug-loader": "^2.4.0", @@ -32,5 +33,9 @@ "@angular/core": "^15", "rxjs": "^7.8.1", "tabby-terminal": "^1.0.197-nightly.1" + }, + "allowScripts": { + "core-js@2.6.12": true, + "fsevents@2.3.3": true } } -- cgit v1.3.1 From 12bccdef008730a5f45438c18e492b0209c6745a Mon Sep 17 00:00:00 2001 From: Jomar Milan Date: Sat, 22 Aug 2026 18:09:24 -0700 Subject: Slice notification body for OSC 9 Fixes behaviour to nclude notification messages with semicolons --- src/osc-notify-middleware.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/osc-notify-middleware.ts b/src/osc-notify-middleware.ts index dcfcc1a..6db95ec 100644 --- a/src/osc-notify-middleware.ts +++ b/src/osc-notify-middleware.ts @@ -49,7 +49,8 @@ export default class OscNotifyMiddleware extends SessionMiddleware { const oscCode = parseInt(oscCodeString); if (oscCode === 9) { - this.showNotification(cleanTitle(this.getTitle()), oscParams[0] ?? ""); + const body = oscParams.slice().join(";"); + this.showNotification(cleanTitle(this.getTitle()), body); } else if (oscCode === 777 && oscParams[0] === "notify") { const title = oscParams[1] || cleanTitle(this.getTitle()); const body = oscParams.slice(2).join(";"); -- cgit v1.3.1 From 2660542fb713d07c53738173510fbbf52f149cc6 Mon Sep 17 00:00:00 2001 From: Jomar Milan Date: Sun, 23 Aug 2026 19:49:51 -0700 Subject: Update README to fit OSC 777 in existing sections --- README.md | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7ba1514..2b38411 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,12 @@ # osc-notify This is a [Tabby](https://github.com/Eugeny/tabby) plugin. It creates a system -notification when an OSC 9 escape code sequence is printed. OSC 9 is in this -format: +notification when certain OSC escape code sequences are printed. The following +OSC escape codes are supported: ```sh OSC 9 ; [message] ST -``` - -Clicking the notification brings Tabby to the front and switches to the exact -tab/pane that emitted the sequence. The notification title reflects the tab's -current title at the time the sequence is received. - -The OSC 9 title is cleaned up: it prefers a user-set custom tab name, and when -the title looks like a filesystem path (e.g. a shell tab showing its cwd) only -the last path segment is used instead of the full, often-truncated path. - -`OSC 777 ; notify ; title ; body` (the rxvt/urxvt notification form) is also -supported, letting a program set an explicit title and body: - -```sh -# creates a notification titled 'Build' with body 'Done' -printf "\033]777;notify;Build;Done\a" +OSC 777 ; notify ; [title] ; [body] ST ``` ### Example commands @@ -32,4 +17,7 @@ printf "\033]9;hello\a" # for tmux, use the passthrough escape sequence. allow-passthrough should be # enabled printf "\033Ptmux;\033\x1b]9;hello\x07\033\\" + +# creates a notification titled 'Build' with body 'Done' +printf "\033]777;notify;Build;Done\a" ``` -- cgit v1.3.1 From d5272ffc9d4103bf3fc2dc6e5df6cfa734582b62 Mon Sep 17 00:00:00 2001 From: Stanton Burchett Date: Mon, 24 Aug 2026 12:50:32 -0600 Subject: Removing the cleanTitle function, as we have a custom title available to set. --- src/osc-notify-middleware.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/osc-notify-middleware.ts b/src/osc-notify-middleware.ts index 6db95ec..5bd7108 100644 --- a/src/osc-notify-middleware.ts +++ b/src/osc-notify-middleware.ts @@ -4,16 +4,6 @@ import { Observable, Subject } from "rxjs"; const oscPrefix = Buffer.from("\x1b]"); const oscSuffix = Buffer.from("\x07"); -function cleanTitle(raw: string): string { - const t = (raw ?? "").trim(); - if (!t) return "Terminal"; - if (t.includes("/") || t.includes("\\")) { - const segs = t.split(/[/\\]/).filter(s => s.length > 0); - return segs.length ? segs[segs.length - 1] : t; - } - return t; -} - export default class OscNotifyMiddleware extends SessionMiddleware { getTitle: () => string; onClick: () => void; @@ -50,9 +40,9 @@ export default class OscNotifyMiddleware extends SessionMiddleware { if (oscCode === 9) { const body = oscParams.slice().join(";"); - this.showNotification(cleanTitle(this.getTitle()), body); + this.showNotification(this.getTitle(), body); } else if (oscCode === 777 && oscParams[0] === "notify") { - const title = oscParams[1] || cleanTitle(this.getTitle()); + const title = oscParams[1] || this.getTitle(); const body = oscParams.slice(2).join(";"); this.showNotification(title, body); } else { -- cgit v1.3.1