diff --git a/public/terminal-session.ts b/public/terminal-session.ts index 577f7e6..f56aa23 100644 --- a/public/terminal-session.ts +++ b/public/terminal-session.ts @@ -11,6 +11,7 @@ import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' import type { ClientMessage, ServerMessage } from '../src/types.js' +import { folderFromTitle } from './title-util.js' const RESET = '\x1b[0m' const BOLD = '\x1b[1m' @@ -94,7 +95,8 @@ export class TerminalSession { this.term.open(this.el) this.term.onData((data) => this.send(data)) - this.term.onTitleChange((title) => this.onTitle?.(title)) + // Tab title = current folder, derived from the shell's OSC title. + this.term.onTitleChange((title) => this.onTitle?.(folderFromTitle(title) ?? title)) this.resizeObserver = new ResizeObserver(() => this.scheduleResize()) this.resizeObserver.observe(this.el) diff --git a/public/title-util.ts b/public/title-util.ts new file mode 100644 index 0000000..e3f14fe --- /dev/null +++ b/public/title-util.ts @@ -0,0 +1,25 @@ +/** + * public/title-util.ts — derive a short tab title from a terminal OSC title. + * + * Pure, dependency-free (no xterm/DOM) so it's unit-testable in node. + */ + +/** + * Derive the **current folder** from the terminal's OSC title. + * + * Most shells emit titles like `user@host:~/path/to/dir` (zsh/bash default). + * Take the part after the last ':' (the path), then its last segment (the + * folder name). `~` (home) stays `~`. Returns null for an empty title; falls + * back to the raw-ish basename when there's no `user@host:` shape (e.g. a + * running command title). + */ +export function folderFromTitle(title: string): string | null { + let s = title.trim() + if (!s) return null + const colon = s.lastIndexOf(':') + if (colon !== -1) s = s.slice(colon + 1).trim() + s = s.replace(/\/+$/, '') // strip trailing slash + const slash = s.lastIndexOf('/') + if (slash !== -1) s = s.slice(slash + 1) + return s || null +} diff --git a/test/title-util.test.ts b/test/title-util.test.ts new file mode 100644 index 0000000..5794fc3 --- /dev/null +++ b/test/title-util.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest' +import { folderFromTitle } from '../public/title-util.js' + +describe('folderFromTitle', () => { + it('extracts the folder from user@host:~/path', () => { + expect(folderFromTitle('yiukai@Mac:~/Documents/git/web-terminal')).toBe('web-terminal') + }) + + it('keeps ~ for the home directory', () => { + expect(folderFromTitle('yiukai@Yiukais-MacBook-Pro:~')).toBe('~') + }) + + it('takes the basename of an absolute path', () => { + expect(folderFromTitle('user@host:/Users/yiukai/web')).toBe('web') + }) + + it('strips a trailing slash', () => { + expect(folderFromTitle('user@host:~/web/')).toBe('web') + }) + + it('handles a title with no user@host: prefix', () => { + expect(folderFromTitle('web-terminal')).toBe('web-terminal') + }) + + it('returns the last path segment when the whole title is a path', () => { + expect(folderFromTitle('/var/log/nginx')).toBe('nginx') + }) + + it('returns null for an empty/whitespace title', () => { + expect(folderFromTitle(' ')).toBeNull() + expect(folderFromTitle('')).toBeNull() + }) +})