diff --git a/src/utils/terminal.ts b/src/utils/terminal.ts index 394f5e3..472b69d 100644 --- a/src/utils/terminal.ts +++ b/src/utils/terminal.ts @@ -1,6 +1,12 @@ import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +// ESM compatibility: __dirname is not available in ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); // Get package version // __PACKAGE_VERSION__ will be replaced at build time diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts index 15a2510..548d17c 100644 --- a/src/utils/widgets.ts +++ b/src/utils/widgets.ts @@ -12,6 +12,8 @@ const widgetRegistry = new Map([ ['git-branch', new widgets.GitBranchWidget()], ['git-changes', new widgets.GitChangesWidget()], ['git-worktree', new widgets.GitWorktreeWidget()], + ['hg-changes', new widgets.MercurialChangesWidget()], + ['hg-branch', new widgets.MercurialBranchWidget()], ['current-working-dir', new widgets.CurrentWorkingDirWidget()], ['tokens-input', new widgets.TokensInputWidget()], ['tokens-output', new widgets.TokensOutputWidget()], diff --git a/src/widgets/MercurialBranch.ts b/src/widgets/MercurialBranch.ts new file mode 100644 index 0000000..1e33f42 --- /dev/null +++ b/src/widgets/MercurialBranch.ts @@ -0,0 +1,84 @@ +import { execSync } from 'child_process'; + +import type { RenderContext } from '../types/RenderContext'; +import type { Settings } from '../types/Settings'; +import type { + CustomKeybind, + Widget, + WidgetEditorDisplay, + WidgetItem +} from '../types/Widget'; + +export class MercurialBranchWidget implements Widget { + getDefaultColor(): string { return 'magenta'; } + getDescription(): string { return 'Shows the current Mercurial bookmark or commit description'; } + getDisplayName(): string { return 'Mercurial Branch'; } + getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { + const hideNoHg = item.metadata?.hideNoHg === 'true'; + const modifiers: string[] = []; + + if (hideNoHg) { + modifiers.push('hide \'no hg\''); + } + + return { + displayText: this.getDisplayName(), + modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined + }; + } + + handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { + if (action === 'toggle-nohg') { + const currentState = item.metadata?.hideNoHg === 'true'; + return { + ...item, + metadata: { + ...item.metadata, + hideNoHg: (!currentState).toString() + } + }; + } + return null; + } + + render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { + const hideNoHg = item.metadata?.hideNoHg === 'true'; + + if (context.isPreview) { + return item.rawValue ? 'default' : '⎇ default'; + } + + const branch = this.getMercurialBranch(); + if (branch) + return item.rawValue ? branch : `⎇ ${branch}`; + + return hideNoHg ? null : '⎇ no hg'; + } + + private getMercurialBranch(): string | null { + // Shows bookmark if available, otherwise first line of commit description + try { + const result = execSync('hg log -r . -T "{if(activebookmark, activebookmark, desc|firstline)}"', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'] + }).trim(); + + if (result) { + return result; + } + } catch { + // Not in a Mercurial repo or hg not available + } + + return null; + } + + getCustomKeybinds(): CustomKeybind[] { + return [ + { key: 'h', label: '(h)ide \'no hg\' message', action: 'toggle-nohg' } + ]; + } + + supportsRawValue(): boolean { return true; } + supportsColors(item: WidgetItem): boolean { return true; } +} diff --git a/src/widgets/MercurialChanges.ts b/src/widgets/MercurialChanges.ts new file mode 100644 index 0000000..63835a2 --- /dev/null +++ b/src/widgets/MercurialChanges.ts @@ -0,0 +1,104 @@ +import { execSync } from 'child_process'; + +import type { RenderContext } from '../types/RenderContext'; +import type { Settings } from '../types/Settings'; +import type { + CustomKeybind, + Widget, + WidgetEditorDisplay, + WidgetItem +} from '../types/Widget'; + +export class MercurialChangesWidget implements Widget { + getDefaultColor(): string { return 'yellow'; } + getDescription(): string { return 'Shows Mercurial changes count (+insertions, -deletions)'; } + getDisplayName(): string { return 'Mercurial Changes'; } + getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { + const hideNoHg = item.metadata?.hideNoHg === 'true'; + const modifiers: string[] = []; + + if (hideNoHg) { + modifiers.push('hide \'no hg\''); + } + + return { + displayText: this.getDisplayName(), + modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined + }; + } + + handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { + if (action === 'toggle-nohg') { + const currentState = item.metadata?.hideNoHg === 'true'; + return { + ...item, + metadata: { + ...item.metadata, + hideNoHg: (!currentState).toString() + } + }; + } + return null; + } + + render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { + const hideNoHg = item.metadata?.hideNoHg === 'true'; + + if (context.isPreview) { + return '(+42,-10)'; + } + + const changes = this.getMercurialChanges(); + if (changes) + return `(+${changes.insertions},-${changes.deletions})`; + else + return hideNoHg ? null : '(no hg)'; + } + + private getMercurialChanges(): { insertions: number; deletions: number } | null { + // hg diff --stat outputs a summary line like: + // "1 files changed, 16 insertions(+), 8 deletions(-)" + try { + const diffStat = execSync('hg diff --stat 2>/dev/null | tail -1', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + shell: '/bin/sh' + }).trim(); + + if (diffStat && diffStat.includes('changed')) { + return this.parseDiffStat(diffStat); + } + } catch { + // Not in a Mercurial repo or hg not available + } + + return null; + } + + private parseDiffStat(stat: string): { insertions: number; deletions: number } { + let insertions = 0; + let deletions = 0; + + // Match patterns like "16 insertions(+)" or "8 deletions(-)" + const insertMatch = /(\d+) insertion/.exec(stat); + const deleteMatch = /(\d+) deletion/.exec(stat); + + if (insertMatch?.[1]) { + insertions = parseInt(insertMatch[1], 10); + } + if (deleteMatch?.[1]) { + deletions = parseInt(deleteMatch[1], 10); + } + + return { insertions, deletions }; + } + + getCustomKeybinds(): CustomKeybind[] { + return [ + { key: 'h', label: '(h)ide \'no hg\' message', action: 'toggle-nohg' } + ]; + } + + supportsRawValue(): boolean { return false; } + supportsColors(item: WidgetItem): boolean { return true; } +} diff --git a/src/widgets/index.ts b/src/widgets/index.ts index faaa705..e63a3aa 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -3,6 +3,8 @@ export { OutputStyleWidget } from './OutputStyle'; export { GitBranchWidget } from './GitBranch'; export { GitChangesWidget } from './GitChanges'; export { GitWorktreeWidget } from './GitWorktree'; +export { MercurialChangesWidget } from './MercurialChanges'; +export { MercurialBranchWidget } from './MercurialBranch'; export { TokensInputWidget } from './TokensInput'; export { TokensOutputWidget } from './TokensOutput'; export { TokensCachedWidget } from './TokensCached';