dmux 5.3.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +226 -51
- package/dist/DmuxApp.js.map +1 -1
- package/dist/FileBrowserApp.d.ts +4 -0
- package/dist/FileBrowserApp.d.ts.map +1 -0
- package/dist/FileBrowserApp.js +693 -0
- package/dist/FileBrowserApp.js.map +1 -0
- package/dist/actions/implementations/closeAction.d.ts.map +1 -1
- package/dist/actions/implementations/closeAction.js +47 -5
- package/dist/actions/implementations/closeAction.js.map +1 -1
- package/dist/actions/implementations/mergeAction.d.ts.map +1 -1
- package/dist/actions/implementations/mergeAction.js +51 -16
- package/dist/actions/implementations/mergeAction.js.map +1 -1
- package/dist/actions/implementations/viewAction.d.ts.map +1 -1
- package/dist/actions/implementations/viewAction.js +7 -0
- package/dist/actions/implementations/viewAction.js.map +1 -1
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +24 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/merge/multiMergeOrchestrator.js +1 -1
- package/dist/actions/merge/multiMergeOrchestrator.js.map +1 -1
- package/dist/actions/types.d.ts +19 -4
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +91 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/components/indicators/Spinner.d.ts +2 -0
- package/dist/components/indicators/Spinner.d.ts.map +1 -1
- package/dist/components/indicators/Spinner.js +4 -4
- package/dist/components/indicators/Spinner.js.map +1 -1
- package/dist/components/panes/KebabMenu.d.ts +2 -2
- package/dist/components/panes/KebabMenu.d.ts.map +1 -1
- package/dist/components/panes/KebabMenu.js +9 -4
- package/dist/components/panes/KebabMenu.js.map +1 -1
- package/dist/components/panes/PaneCard.d.ts.map +1 -1
- package/dist/components/panes/PaneCard.js +20 -4
- package/dist/components/panes/PaneCard.js.map +1 -1
- package/dist/components/panes/PanesGrid.d.ts +1 -0
- package/dist/components/panes/PanesGrid.d.ts.map +1 -1
- package/dist/components/panes/PanesGrid.js +11 -3
- package/dist/components/panes/PanesGrid.js.map +1 -1
- package/dist/components/popups/agentChoicePopup.js +29 -24
- package/dist/components/popups/agentChoicePopup.js.map +1 -1
- package/dist/components/popups/agentChoiceSelection.d.ts +8 -0
- package/dist/components/popups/agentChoiceSelection.d.ts.map +1 -0
- package/dist/components/popups/agentChoiceSelection.js +14 -0
- package/dist/components/popups/agentChoiceSelection.js.map +1 -0
- package/dist/components/popups/kebabMenuPopup.js +9 -4
- package/dist/components/popups/kebabMenuPopup.js.map +1 -1
- package/dist/components/popups/notificationSoundsPopup.d.ts +25 -0
- package/dist/components/popups/notificationSoundsPopup.d.ts.map +1 -0
- package/dist/components/popups/notificationSoundsPopup.js +165 -0
- package/dist/components/popups/notificationSoundsPopup.js.map +1 -0
- package/dist/components/popups/settingsPopup.js +361 -26
- package/dist/components/popups/settingsPopup.js.map +1 -1
- package/dist/components/popups/shortcutsPopup.js +11 -5
- package/dist/components/popups/shortcutsPopup.js.map +1 -1
- package/dist/constants/layout.d.ts +9 -0
- package/dist/constants/layout.d.ts.map +1 -0
- package/dist/constants/layout.js +9 -0
- package/dist/constants/layout.js.map +1 -0
- package/dist/hooks/useActionSystem.d.ts +9 -5
- package/dist/hooks/useActionSystem.d.ts.map +1 -1
- package/dist/hooks/useActionSystem.js +21 -19
- package/dist/hooks/useActionSystem.js.map +1 -1
- package/dist/hooks/useInputHandling.d.ts +1 -0
- package/dist/hooks/useInputHandling.d.ts.map +1 -1
- package/dist/hooks/useInputHandling.js +499 -79
- package/dist/hooks/useInputHandling.js.map +1 -1
- package/dist/hooks/usePaneCreation.d.ts +4 -2
- package/dist/hooks/usePaneCreation.d.ts.map +1 -1
- package/dist/hooks/usePaneCreation.js +6 -0
- package/dist/hooks/usePaneCreation.js.map +1 -1
- package/dist/hooks/usePaneLoading.d.ts +1 -0
- package/dist/hooks/usePaneLoading.d.ts.map +1 -1
- package/dist/hooks/usePaneLoading.js +10 -7
- package/dist/hooks/usePaneLoading.js.map +1 -1
- package/dist/hooks/usePaneSync.js +2 -2
- package/dist/hooks/usePaneSync.js.map +1 -1
- package/dist/hooks/usePanes.d.ts.map +1 -1
- package/dist/hooks/usePanes.js +18 -4
- package/dist/hooks/usePanes.js.map +1 -1
- package/dist/hooks/useProjectActivity.d.ts +7 -0
- package/dist/hooks/useProjectActivity.d.ts.map +1 -0
- package/dist/hooks/useProjectActivity.js +79 -0
- package/dist/hooks/useProjectActivity.js.map +1 -0
- package/dist/hooks/useServices.d.ts +3 -0
- package/dist/hooks/useServices.d.ts.map +1 -1
- package/dist/hooks/useServices.js +4 -0
- package/dist/hooks/useServices.js.map +1 -1
- package/dist/index.js +59 -15
- package/dist/index.js.map +1 -1
- package/dist/layout/LayoutCalculator.d.ts.map +1 -1
- package/dist/layout/LayoutCalculator.js +4 -1
- package/dist/layout/LayoutCalculator.js.map +1 -1
- package/dist/services/DmuxAttentionService.d.ts +33 -0
- package/dist/services/DmuxAttentionService.d.ts.map +1 -0
- package/dist/services/DmuxAttentionService.js +172 -0
- package/dist/services/DmuxAttentionService.js.map +1 -0
- package/dist/services/DmuxFocusService.d.ts +58 -0
- package/dist/services/DmuxFocusService.d.ts.map +1 -0
- package/dist/services/DmuxFocusService.js +671 -0
- package/dist/services/DmuxFocusService.js.map +1 -0
- package/dist/services/PaneAnalyzer.d.ts +11 -2
- package/dist/services/PaneAnalyzer.d.ts.map +1 -1
- package/dist/services/PaneAnalyzer.js +88 -22
- package/dist/services/PaneAnalyzer.js.map +1 -1
- package/dist/services/PopupManager.d.ts +31 -14
- package/dist/services/PopupManager.d.ts.map +1 -1
- package/dist/services/PopupManager.js +147 -68
- package/dist/services/PopupManager.js.map +1 -1
- package/dist/services/StatusDetector.d.ts +14 -0
- package/dist/services/StatusDetector.d.ts.map +1 -1
- package/dist/services/StatusDetector.js +60 -12
- package/dist/services/StatusDetector.js.map +1 -1
- package/dist/services/TmuxHookManager.d.ts.map +1 -1
- package/dist/services/TmuxHookManager.js +4 -2
- package/dist/services/TmuxHookManager.js.map +1 -1
- package/dist/services/TmuxService.d.ts +37 -2
- package/dist/services/TmuxService.d.ts.map +1 -1
- package/dist/services/TmuxService.js +138 -16
- package/dist/services/TmuxService.js.map +1 -1
- package/dist/types/activity.d.ts +4 -0
- package/dist/types/activity.d.ts.map +1 -0
- package/dist/types/activity.js +2 -0
- package/dist/types/activity.js.map +1 -0
- package/dist/types.d.ts +18 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/attachAgent.d.ts +4 -0
- package/dist/utils/attachAgent.d.ts.map +1 -1
- package/dist/utils/attachAgent.js +18 -7
- package/dist/utils/attachAgent.js.map +1 -1
- package/dist/utils/controlPaneRecovery.d.ts +2 -0
- package/dist/utils/controlPaneRecovery.d.ts.map +1 -0
- package/dist/utils/controlPaneRecovery.js +156 -0
- package/dist/utils/controlPaneRecovery.js.map +1 -0
- package/dist/utils/devWatchExit.d.ts +2 -0
- package/dist/utils/devWatchExit.d.ts.map +1 -0
- package/dist/utils/devWatchExit.js +10 -0
- package/dist/utils/devWatchExit.js.map +1 -0
- package/dist/utils/dmuxCommand.d.ts +3 -0
- package/dist/utils/dmuxCommand.d.ts.map +1 -0
- package/dist/utils/dmuxCommand.js +18 -0
- package/dist/utils/dmuxCommand.js.map +1 -0
- package/dist/utils/fileBrowser.d.ts +61 -0
- package/dist/utils/fileBrowser.d.ts.map +1 -0
- package/dist/utils/fileBrowser.js +567 -0
- package/dist/utils/fileBrowser.js.map +1 -0
- package/dist/utils/focusDetection.d.ts +38 -0
- package/dist/utils/focusDetection.d.ts.map +1 -0
- package/dist/utils/focusDetection.js +57 -0
- package/dist/utils/focusDetection.js.map +1 -0
- package/dist/utils/generated-agents-doc.d.ts +1 -1
- package/dist/utils/generated-agents-doc.js +1 -1
- package/dist/utils/git.d.ts +4 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +15 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/layoutManager.d.ts +5 -1
- package/dist/utils/layoutManager.d.ts.map +1 -1
- package/dist/utils/layoutManager.js +103 -26
- package/dist/utils/layoutManager.js.map +1 -1
- package/dist/utils/mergeTargets.d.ts +17 -0
- package/dist/utils/mergeTargets.d.ts.map +1 -0
- package/dist/utils/mergeTargets.js +132 -0
- package/dist/utils/mergeTargets.js.map +1 -0
- package/dist/utils/mergeValidation.d.ts.map +1 -1
- package/dist/utils/mergeValidation.js +12 -5
- package/dist/utils/mergeValidation.js.map +1 -1
- package/dist/utils/notificationSoundPreview.d.ts +10 -0
- package/dist/utils/notificationSoundPreview.d.ts.map +1 -0
- package/dist/utils/notificationSoundPreview.js +54 -0
- package/dist/utils/notificationSoundPreview.js.map +1 -0
- package/dist/utils/notificationSounds.d.ts +17 -0
- package/dist/utils/notificationSounds.d.ts.map +1 -0
- package/dist/utils/notificationSounds.js +123 -0
- package/dist/utils/notificationSounds.js.map +1 -0
- package/dist/utils/paneAttentionHeuristics.d.ts +4 -0
- package/dist/utils/paneAttentionHeuristics.d.ts.map +1 -0
- package/dist/utils/paneAttentionHeuristics.js +135 -0
- package/dist/utils/paneAttentionHeuristics.js.map +1 -0
- package/dist/utils/paneCreation.d.ts +3 -1
- package/dist/utils/paneCreation.d.ts.map +1 -1
- package/dist/utils/paneCreation.js +23 -5
- package/dist/utils/paneCreation.js.map +1 -1
- package/dist/utils/paneVisibility.d.ts +12 -0
- package/dist/utils/paneVisibility.d.ts.map +1 -0
- package/dist/utils/paneVisibility.js +60 -0
- package/dist/utils/paneVisibility.js.map +1 -0
- package/dist/utils/processShutdown.d.ts +4 -0
- package/dist/utils/processShutdown.d.ts.map +1 -0
- package/dist/utils/processShutdown.js +27 -0
- package/dist/utils/processShutdown.js.map +1 -0
- package/dist/utils/promptStore.d.ts.map +1 -1
- package/dist/utils/promptStore.js +6 -0
- package/dist/utils/promptStore.js.map +1 -1
- package/dist/utils/reopenWorktree.d.ts.map +1 -1
- package/dist/utils/reopenWorktree.js +8 -0
- package/dist/utils/reopenWorktree.js.map +1 -1
- package/dist/utils/runtimePaths.d.ts +1 -0
- package/dist/utils/runtimePaths.d.ts.map +1 -1
- package/dist/utils/runtimePaths.js +3 -0
- package/dist/utils/runtimePaths.js.map +1 -1
- package/dist/utils/settingsManager.d.ts +3 -0
- package/dist/utils/settingsManager.d.ts.map +1 -1
- package/dist/utils/settingsManager.js +203 -11
- package/dist/utils/settingsManager.js.map +1 -1
- package/dist/utils/tmux.d.ts +5 -1
- package/dist/utils/tmux.d.ts.map +1 -1
- package/dist/utils/tmux.js +23 -5
- package/dist/utils/tmux.js.map +1 -1
- package/dist/utils/tmuxConfigOnboarding.js +1 -1
- package/dist/utils/tmuxConfigOnboarding.js.map +1 -1
- package/dist/utils/tmuxHookCommands.d.ts +14 -0
- package/dist/utils/tmuxHookCommands.d.ts.map +1 -0
- package/dist/utils/tmuxHookCommands.js +30 -0
- package/dist/utils/tmuxHookCommands.js.map +1 -0
- package/dist/utils/tmuxRuntimeCompatibility.d.ts +11 -0
- package/dist/utils/tmuxRuntimeCompatibility.d.ts.map +1 -0
- package/dist/utils/tmuxRuntimeCompatibility.js +71 -0
- package/dist/utils/tmuxRuntimeCompatibility.js.map +1 -0
- package/dist/utils/worktreeMetadata.d.ts +9 -0
- package/dist/utils/worktreeMetadata.d.ts.map +1 -0
- package/dist/utils/worktreeMetadata.js +60 -0
- package/dist/utils/worktreeMetadata.js.map +1 -0
- package/dist/workers/PaneWorker.js +64 -128
- package/dist/workers/PaneWorker.js.map +1 -1
- package/dist/workers/WorkerMessages.d.ts +4 -1
- package/dist/workers/WorkerMessages.d.ts.map +1 -1
- package/dist/workers/WorkerMessages.js.map +1 -1
- package/native/macos/dmux-helper-Info.plist +30 -0
- package/native/macos/dmux-helper-icon.png +0 -0
- package/native/macos/dmux-helper.swift +831 -0
- package/native/macos/sounds/dmux-braam.caf +0 -0
- package/native/macos/sounds/dmux-brass.caf +0 -0
- package/native/macos/sounds/dmux-ding-bell.caf +0 -0
- package/native/macos/sounds/dmux-future.caf +0 -0
- package/native/macos/sounds/dmux-harp.caf +0 -0
- package/native/macos/sounds/dmux-quiet-bells.caf +0 -0
- package/native/macos/sounds/dmux-sonar.caf +0 -0
- package/native/macos/sounds/dmux-success.caf +0 -0
- package/native/macos/sounds/dmux-triumphant-trumpet.caf +0 -0
- package/native/macos/sounds/dmux-war-horn.caf +0 -0
- package/package.json +3 -1
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { spawn, spawnSync } from 'child_process';
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { LogService } from './LogService.js';
|
|
10
|
+
import { TmuxService } from './TmuxService.js';
|
|
11
|
+
import { SettingsManager } from '../utils/settingsManager.js';
|
|
12
|
+
import { buildFocusToken, buildFocusWindowTitle, buildTerminalTitleSequence, mapTerminalProgramToBundleId, parseTmuxSocketPath, supportsNativeDmuxHelper, } from '../utils/focusDetection.js';
|
|
13
|
+
import { getBundledNotificationSoundDefinitions, pickNotificationSound, } from '../utils/notificationSounds.js';
|
|
14
|
+
import { resolvePackagePath } from '../utils/runtimePaths.js';
|
|
15
|
+
const HELPER_RECONNECT_DELAY_MS = 1000;
|
|
16
|
+
const HELPER_SOCKET_WAIT_TIMEOUT_MS = 5000;
|
|
17
|
+
const FOCUS_SYNC_INTERVAL_MS = 350;
|
|
18
|
+
const ATTENTION_FLASH_STEP_MS = 250;
|
|
19
|
+
const ATTENTION_FLASH_SEQUENCE_LENGTH = 12;
|
|
20
|
+
const ATTENTION_FLASH_FALLBACK_BG = 'colour237';
|
|
21
|
+
function isTestEnvironment() {
|
|
22
|
+
return process.env.NODE_ENV === 'test'
|
|
23
|
+
|| process.env.VITEST === 'true'
|
|
24
|
+
|| typeof process.env.VITEST !== 'undefined';
|
|
25
|
+
}
|
|
26
|
+
function getHelperRuntimePaths() {
|
|
27
|
+
const helperBaseDir = path.join(os.homedir(), '.dmux', 'native-helper');
|
|
28
|
+
const appPath = path.join(helperBaseDir, 'dmux-helper.app');
|
|
29
|
+
const contentsPath = path.join(appPath, 'Contents');
|
|
30
|
+
const resourcesPath = path.join(contentsPath, 'Resources');
|
|
31
|
+
return {
|
|
32
|
+
sourcePath: resolvePackagePath('native', 'macos', 'dmux-helper.swift'),
|
|
33
|
+
infoPlistSourcePath: resolvePackagePath('native', 'macos', 'dmux-helper-Info.plist'),
|
|
34
|
+
iconSourcePath: resolvePackagePath('native', 'macos', 'dmux-helper-icon.png'),
|
|
35
|
+
soundSourceDir: resolvePackagePath('native', 'macos', 'sounds'),
|
|
36
|
+
appPath,
|
|
37
|
+
executablePath: path.join(contentsPath, 'MacOS', 'dmux-helper'),
|
|
38
|
+
resourcesPath,
|
|
39
|
+
infoPlistPath: path.join(contentsPath, 'Info.plist'),
|
|
40
|
+
bundleIconPngPath: path.join(resourcesPath, 'dmux-helper.png'),
|
|
41
|
+
bundleIconIcnsPath: path.join(resourcesPath, 'dmux-helper.icns'),
|
|
42
|
+
versionPath: path.join(helperBaseDir, 'version.txt'),
|
|
43
|
+
socketPath: path.join(helperBaseDir, 'run', 'dmux-helper.sock'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const HELPER_BUNDLE_BUILD_VERSION = '1';
|
|
47
|
+
function readTmuxGlobalEnvironment(name) {
|
|
48
|
+
if (!process.env.TMUX) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const result = spawnSync('tmux', ['show-environment', '-g', name], {
|
|
52
|
+
stdio: 'pipe',
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
});
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const line = result.stdout.trim();
|
|
59
|
+
if (!line || line === `-${name}`) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const prefix = `${name}=`;
|
|
63
|
+
if (!line.startsWith(prefix)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return line.slice(prefix.length);
|
|
67
|
+
}
|
|
68
|
+
function resolveTerminalProgram() {
|
|
69
|
+
const terminalProgram = process.env.TERM_PROGRAM?.trim();
|
|
70
|
+
if (terminalProgram && terminalProgram.toLowerCase() !== 'tmux') {
|
|
71
|
+
return terminalProgram;
|
|
72
|
+
}
|
|
73
|
+
return readTmuxGlobalEnvironment('TERM_PROGRAM') ?? terminalProgram;
|
|
74
|
+
}
|
|
75
|
+
function resolveTmuxSocketPath() {
|
|
76
|
+
const parsedFromEnv = parseTmuxSocketPath(process.env.TMUX);
|
|
77
|
+
if (parsedFromEnv) {
|
|
78
|
+
return parsedFromEnv;
|
|
79
|
+
}
|
|
80
|
+
if (!process.env.TMUX) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const result = spawnSync('tmux', ['display-message', '-p', '#{socket_path}'], {
|
|
84
|
+
stdio: 'pipe',
|
|
85
|
+
encoding: 'utf-8',
|
|
86
|
+
});
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const socketPath = result.stdout.trim();
|
|
91
|
+
return socketPath || undefined;
|
|
92
|
+
}
|
|
93
|
+
function buildHelperVersionHash(parts) {
|
|
94
|
+
const hash = createHash('sha1');
|
|
95
|
+
hash.update(HELPER_BUNDLE_BUILD_VERSION);
|
|
96
|
+
for (const part of parts) {
|
|
97
|
+
hash.update(part);
|
|
98
|
+
}
|
|
99
|
+
return hash.digest('hex');
|
|
100
|
+
}
|
|
101
|
+
function shiftHexColor(hex, delta) {
|
|
102
|
+
const match = hex.trim().match(/^#([0-9a-fA-F]{6})$/);
|
|
103
|
+
if (!match) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const value = match[1];
|
|
107
|
+
const channels = [0, 2, 4].map((offset) => Math.max(0, Math.min(255, Number.parseInt(value.slice(offset, offset + 2), 16) + delta)));
|
|
108
|
+
return `#${channels.map((channel) => channel.toString(16).padStart(2, '0')).join('')}`;
|
|
109
|
+
}
|
|
110
|
+
function buildAttentionFlashWindowStyle(baseStyle) {
|
|
111
|
+
const normalized = baseStyle.trim();
|
|
112
|
+
const bgPattern = /(^|,)\s*bg=([^,\s]+)/;
|
|
113
|
+
const match = normalized.match(bgPattern);
|
|
114
|
+
let nextBackground = ATTENTION_FLASH_FALLBACK_BG;
|
|
115
|
+
if (match?.[2]) {
|
|
116
|
+
const currentBackground = match[2].trim();
|
|
117
|
+
const colourMatch = currentBackground.match(/^colour(\d{1,3})$/i);
|
|
118
|
+
if (colourMatch) {
|
|
119
|
+
const currentValue = Number.parseInt(colourMatch[1], 10);
|
|
120
|
+
const nextValue = currentValue >= 248
|
|
121
|
+
? Math.max(0, currentValue - 1)
|
|
122
|
+
: Math.min(255, currentValue + 1);
|
|
123
|
+
nextBackground = `colour${nextValue}`;
|
|
124
|
+
}
|
|
125
|
+
else if (currentBackground === 'default') {
|
|
126
|
+
nextBackground = ATTENTION_FLASH_FALLBACK_BG;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
nextBackground = shiftHexColor(currentBackground, 14) ?? ATTENTION_FLASH_FALLBACK_BG;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!match) {
|
|
133
|
+
return normalized ? `${normalized},bg=${nextBackground}` : `bg=${nextBackground}`;
|
|
134
|
+
}
|
|
135
|
+
return normalized.replace(bgPattern, (_full, prefix) => `${prefix}bg=${nextBackground}`);
|
|
136
|
+
}
|
|
137
|
+
function runBuildTool(executable, args) {
|
|
138
|
+
const result = spawnSync(executable, args, {
|
|
139
|
+
stdio: 'pipe',
|
|
140
|
+
encoding: 'utf-8',
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
ok: result.status === 0,
|
|
144
|
+
output: (result.stderr || result.stdout || '').trim(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function buildHelperBundleIcon(iconSourcePath, iconIcnsPath) {
|
|
148
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dmux-helper-icon-'));
|
|
149
|
+
const iconsetDir = path.join(tempDir, 'dmux-helper.iconset');
|
|
150
|
+
try {
|
|
151
|
+
await fs.mkdir(iconsetDir, { recursive: true });
|
|
152
|
+
const sizes = [16, 32, 128, 256, 512];
|
|
153
|
+
for (const size of sizes) {
|
|
154
|
+
const oneX = path.join(iconsetDir, `icon_${size}x${size}.png`);
|
|
155
|
+
const twoX = path.join(iconsetDir, `icon_${size}x${size}@2x.png`);
|
|
156
|
+
let result = runBuildTool('/usr/bin/sips', [
|
|
157
|
+
'-z',
|
|
158
|
+
String(size),
|
|
159
|
+
String(size),
|
|
160
|
+
iconSourcePath,
|
|
161
|
+
'--out',
|
|
162
|
+
oneX,
|
|
163
|
+
]);
|
|
164
|
+
if (!result.ok) {
|
|
165
|
+
throw new Error(result.output || 'sips failed building helper icon');
|
|
166
|
+
}
|
|
167
|
+
result = runBuildTool('/usr/bin/sips', [
|
|
168
|
+
'-z',
|
|
169
|
+
String(size * 2),
|
|
170
|
+
String(size * 2),
|
|
171
|
+
iconSourcePath,
|
|
172
|
+
'--out',
|
|
173
|
+
twoX,
|
|
174
|
+
]);
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
throw new Error(result.output || 'sips failed building helper icon');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const iconutilResult = runBuildTool('/usr/bin/iconutil', [
|
|
180
|
+
'-c',
|
|
181
|
+
'icns',
|
|
182
|
+
iconsetDir,
|
|
183
|
+
'-o',
|
|
184
|
+
iconIcnsPath,
|
|
185
|
+
]);
|
|
186
|
+
if (!iconutilResult.ok) {
|
|
187
|
+
throw new Error(iconutilResult.output || 'iconutil failed building helper icon');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function ensureHelperBundle(paths) {
|
|
195
|
+
if (!existsSync(paths.sourcePath) || !existsSync(paths.infoPlistSourcePath)) {
|
|
196
|
+
return { ready: false, rebuilt: false };
|
|
197
|
+
}
|
|
198
|
+
const bundledSoundAssets = getBundledNotificationSoundDefinitions().map((definition) => ({
|
|
199
|
+
resourceFileName: definition.resourceFileName,
|
|
200
|
+
sourcePath: path.join(paths.soundSourceDir, definition.resourceFileName),
|
|
201
|
+
}));
|
|
202
|
+
const [sourceTemplate, infoPlistTemplate, iconBuffer, soundAssets, currentVersion] = await Promise.all([
|
|
203
|
+
fs.readFile(paths.sourcePath, 'utf-8'),
|
|
204
|
+
fs.readFile(paths.infoPlistSourcePath, 'utf-8'),
|
|
205
|
+
existsSync(paths.iconSourcePath)
|
|
206
|
+
? fs.readFile(paths.iconSourcePath)
|
|
207
|
+
: Promise.resolve(null),
|
|
208
|
+
Promise.all(bundledSoundAssets
|
|
209
|
+
.filter((asset) => existsSync(asset.sourcePath))
|
|
210
|
+
.map(async (asset) => ({
|
|
211
|
+
...asset,
|
|
212
|
+
buffer: await fs.readFile(asset.sourcePath),
|
|
213
|
+
}))),
|
|
214
|
+
existsSync(paths.versionPath)
|
|
215
|
+
? fs.readFile(paths.versionPath, 'utf-8').catch(() => '')
|
|
216
|
+
: Promise.resolve(''),
|
|
217
|
+
]);
|
|
218
|
+
const expectedVersion = buildHelperVersionHash([
|
|
219
|
+
sourceTemplate,
|
|
220
|
+
infoPlistTemplate,
|
|
221
|
+
iconBuffer ?? 'no-icon',
|
|
222
|
+
...soundAssets.flatMap((asset) => [asset.resourceFileName, asset.buffer]),
|
|
223
|
+
]);
|
|
224
|
+
const needsBuild = !existsSync(paths.executablePath)
|
|
225
|
+
|| !existsSync(paths.infoPlistPath)
|
|
226
|
+
|| (iconBuffer !== null && !existsSync(paths.bundleIconPngPath))
|
|
227
|
+
|| soundAssets.some((asset) => !existsSync(path.join(paths.resourcesPath, asset.resourceFileName)))
|
|
228
|
+
|| currentVersion.trim() !== expectedVersion;
|
|
229
|
+
if (!needsBuild) {
|
|
230
|
+
return { ready: true, rebuilt: false };
|
|
231
|
+
}
|
|
232
|
+
await fs.rm(paths.appPath, { recursive: true, force: true });
|
|
233
|
+
await fs.mkdir(path.dirname(paths.executablePath), { recursive: true });
|
|
234
|
+
await fs.mkdir(paths.resourcesPath, { recursive: true });
|
|
235
|
+
await fs.writeFile(paths.infoPlistPath, infoPlistTemplate, 'utf-8');
|
|
236
|
+
if (iconBuffer !== null) {
|
|
237
|
+
await fs.writeFile(paths.bundleIconPngPath, iconBuffer);
|
|
238
|
+
try {
|
|
239
|
+
await buildHelperBundleIcon(paths.bundleIconPngPath, paths.bundleIconIcnsPath);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// The helper can still set the bundled PNG as its runtime icon.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
await Promise.all(soundAssets.map(async (asset) => {
|
|
246
|
+
await fs.writeFile(path.join(paths.resourcesPath, asset.resourceFileName), asset.buffer);
|
|
247
|
+
}));
|
|
248
|
+
const result = spawnSync('swiftc', [
|
|
249
|
+
'-O',
|
|
250
|
+
paths.sourcePath,
|
|
251
|
+
'-o',
|
|
252
|
+
paths.executablePath,
|
|
253
|
+
'-framework',
|
|
254
|
+
'AppKit',
|
|
255
|
+
'-framework',
|
|
256
|
+
'ApplicationServices',
|
|
257
|
+
], {
|
|
258
|
+
stdio: 'pipe',
|
|
259
|
+
encoding: 'utf-8',
|
|
260
|
+
});
|
|
261
|
+
if (result.status !== 0) {
|
|
262
|
+
return { ready: false, rebuilt: false };
|
|
263
|
+
}
|
|
264
|
+
await fs.writeFile(paths.versionPath, expectedVersion, 'utf-8');
|
|
265
|
+
return {
|
|
266
|
+
ready: true,
|
|
267
|
+
rebuilt: true,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export function parseHelperSocketOwnerProcessIds(lsofOutput, socketPath, currentProcessId = process.pid) {
|
|
271
|
+
return lsofOutput
|
|
272
|
+
.split('\n')
|
|
273
|
+
.map((line) => line.trim())
|
|
274
|
+
.filter((line) => line.endsWith(socketPath))
|
|
275
|
+
.map((line) => {
|
|
276
|
+
const match = line.match(/^\S+\s+(\d+)\s+/);
|
|
277
|
+
return match ? Number.parseInt(match[1], 10) : Number.NaN;
|
|
278
|
+
})
|
|
279
|
+
.filter((value, index, values) => (Number.isFinite(value)
|
|
280
|
+
&& value > 0
|
|
281
|
+
&& value !== currentProcessId
|
|
282
|
+
&& values.indexOf(value) === index));
|
|
283
|
+
}
|
|
284
|
+
function findHelperSocketOwnerProcessIds(socketPath) {
|
|
285
|
+
const result = spawnSync('lsof', ['-nP', '-U', socketPath], {
|
|
286
|
+
stdio: 'pipe',
|
|
287
|
+
encoding: 'utf-8',
|
|
288
|
+
});
|
|
289
|
+
if (result.status !== 0) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
// Only kill the helper process that owns the socket path itself.
|
|
293
|
+
// Connected clients show up as peer links (`->...`) and must be left alone.
|
|
294
|
+
return parseHelperSocketOwnerProcessIds(result.stdout, socketPath);
|
|
295
|
+
}
|
|
296
|
+
async function stopRunningHelper(socketPath) {
|
|
297
|
+
const pids = findHelperSocketOwnerProcessIds(socketPath);
|
|
298
|
+
if (pids.length === 0) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
for (const pid of pids) {
|
|
302
|
+
try {
|
|
303
|
+
process.kill(pid, 'SIGTERM');
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Ignore races where the helper exits between lookup and signal delivery.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const deadline = Date.now() + 2000;
|
|
310
|
+
while (Date.now() < deadline) {
|
|
311
|
+
const activePids = findHelperSocketOwnerProcessIds(socketPath);
|
|
312
|
+
if (activePids.length === 0) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
316
|
+
}
|
|
317
|
+
await fs.rm(socketPath, { force: true }).catch(() => undefined);
|
|
318
|
+
return findHelperSocketOwnerProcessIds(socketPath).length === 0;
|
|
319
|
+
}
|
|
320
|
+
async function waitForHelperSocket(socketPath, timeoutMs) {
|
|
321
|
+
const deadline = Date.now() + timeoutMs;
|
|
322
|
+
while (Date.now() < deadline) {
|
|
323
|
+
if (existsSync(socketPath)) {
|
|
324
|
+
const connected = await new Promise((resolve) => {
|
|
325
|
+
const probe = createConnection(socketPath);
|
|
326
|
+
let settled = false;
|
|
327
|
+
const finish = (value) => {
|
|
328
|
+
if (settled)
|
|
329
|
+
return;
|
|
330
|
+
settled = true;
|
|
331
|
+
probe.destroy();
|
|
332
|
+
resolve(value);
|
|
333
|
+
};
|
|
334
|
+
probe.once('connect', () => finish(true));
|
|
335
|
+
probe.once('error', () => finish(false));
|
|
336
|
+
});
|
|
337
|
+
if (connected) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
async function ensureHelperRunning(logger) {
|
|
346
|
+
const helperPaths = getHelperRuntimePaths();
|
|
347
|
+
const { executablePath, socketPath } = helperPaths;
|
|
348
|
+
const binaryStatus = await ensureHelperBundle(helperPaths);
|
|
349
|
+
if (!binaryStatus.ready) {
|
|
350
|
+
logger.warn('dmux helper app bundle is unavailable on this system', 'focus-helper');
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const alreadyRunning = await waitForHelperSocket(socketPath, 250);
|
|
354
|
+
if (alreadyRunning && !binaryStatus.rebuilt) {
|
|
355
|
+
return socketPath;
|
|
356
|
+
}
|
|
357
|
+
if (alreadyRunning && binaryStatus.rebuilt) {
|
|
358
|
+
const stopped = await stopRunningHelper(socketPath);
|
|
359
|
+
if (!stopped) {
|
|
360
|
+
logger.warn('Failed to restart dmux helper after rebuilding it', 'focus-helper');
|
|
361
|
+
return socketPath;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
await fs.mkdir(path.dirname(socketPath), { recursive: true });
|
|
365
|
+
const child = spawn(executablePath, ['--socket', socketPath, '--poll-ms', '250'], {
|
|
366
|
+
detached: true,
|
|
367
|
+
stdio: 'ignore',
|
|
368
|
+
});
|
|
369
|
+
child.unref();
|
|
370
|
+
const started = await waitForHelperSocket(socketPath, HELPER_SOCKET_WAIT_TIMEOUT_MS);
|
|
371
|
+
if (!started) {
|
|
372
|
+
logger.warn('Timed out waiting for dmux helper to start', 'focus-helper');
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return socketPath;
|
|
376
|
+
}
|
|
377
|
+
export class DmuxFocusService extends EventEmitter {
|
|
378
|
+
options;
|
|
379
|
+
logger = LogService.getInstance();
|
|
380
|
+
tmuxService = TmuxService.getInstance();
|
|
381
|
+
instanceId = randomUUID();
|
|
382
|
+
token = buildFocusToken(this.instanceId);
|
|
383
|
+
terminalProgram = supportsNativeDmuxHelper()
|
|
384
|
+
? resolveTerminalProgram()
|
|
385
|
+
: undefined;
|
|
386
|
+
bundleId = mapTerminalProgramToBundleId(this.terminalProgram);
|
|
387
|
+
tmuxSocketPath = supportsNativeDmuxHelper()
|
|
388
|
+
? resolveTmuxSocketPath()
|
|
389
|
+
: undefined;
|
|
390
|
+
terminalTitle;
|
|
391
|
+
baseTitle;
|
|
392
|
+
helperSocketPath = null;
|
|
393
|
+
helperSocket = null;
|
|
394
|
+
helperFocused = false;
|
|
395
|
+
reconnectTimer = null;
|
|
396
|
+
syncInterval = null;
|
|
397
|
+
lineBuffer = '';
|
|
398
|
+
active = false;
|
|
399
|
+
titleApplied = false;
|
|
400
|
+
fullyFocusedPaneId = null; // tmux pane id
|
|
401
|
+
flashingTmuxPaneIds = new Set();
|
|
402
|
+
constructor(options) {
|
|
403
|
+
super();
|
|
404
|
+
this.options = options;
|
|
405
|
+
this.baseTitle = `dmux ${options.projectName}`;
|
|
406
|
+
this.terminalTitle = buildFocusWindowTitle(options.projectName, this.token);
|
|
407
|
+
}
|
|
408
|
+
resolveAttentionNotificationSoundName() {
|
|
409
|
+
const settingsManager = new SettingsManager(this.options.projectRoot ?? process.cwd());
|
|
410
|
+
const selectedSound = pickNotificationSound(settingsManager.getSettings().enabledNotificationSounds);
|
|
411
|
+
return selectedSound.resourceFileName;
|
|
412
|
+
}
|
|
413
|
+
async start() {
|
|
414
|
+
if (!supportsNativeDmuxHelper() || !process.env.TMUX || isTestEnvironment()) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.active = true;
|
|
418
|
+
const helperSocketPath = await ensureHelperRunning(this.logger);
|
|
419
|
+
if (!helperSocketPath) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.helperSocketPath = helperSocketPath;
|
|
423
|
+
this.writeTerminalTitle(this.terminalTitle);
|
|
424
|
+
this.titleApplied = true;
|
|
425
|
+
this.syncInterval = setInterval(() => {
|
|
426
|
+
void this.syncFocusedPaneState();
|
|
427
|
+
}, FOCUS_SYNC_INTERVAL_MS);
|
|
428
|
+
this.connectToHelper();
|
|
429
|
+
}
|
|
430
|
+
stop() {
|
|
431
|
+
this.active = false;
|
|
432
|
+
if (this.syncInterval) {
|
|
433
|
+
clearInterval(this.syncInterval);
|
|
434
|
+
this.syncInterval = null;
|
|
435
|
+
}
|
|
436
|
+
if (this.reconnectTimer) {
|
|
437
|
+
clearTimeout(this.reconnectTimer);
|
|
438
|
+
this.reconnectTimer = null;
|
|
439
|
+
}
|
|
440
|
+
if (this.helperSocket) {
|
|
441
|
+
this.helperSocket.destroy();
|
|
442
|
+
this.helperSocket = null;
|
|
443
|
+
}
|
|
444
|
+
this.helperFocused = false;
|
|
445
|
+
this.setFullyFocusedPaneId(null);
|
|
446
|
+
if (this.titleApplied) {
|
|
447
|
+
this.writeTerminalTitle(this.baseTitle);
|
|
448
|
+
this.titleApplied = false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
connectToHelper() {
|
|
452
|
+
if (!this.active || !this.helperSocketPath) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.lineBuffer = '';
|
|
456
|
+
const socket = createConnection(this.helperSocketPath);
|
|
457
|
+
this.helperSocket = socket;
|
|
458
|
+
socket.on('connect', () => {
|
|
459
|
+
const subscribeMessage = {
|
|
460
|
+
type: 'subscribe',
|
|
461
|
+
instanceId: this.instanceId,
|
|
462
|
+
titleToken: this.token,
|
|
463
|
+
bundleId: this.bundleId,
|
|
464
|
+
terminalProgram: this.terminalProgram,
|
|
465
|
+
};
|
|
466
|
+
socket.write(`${JSON.stringify(subscribeMessage)}\n`);
|
|
467
|
+
});
|
|
468
|
+
socket.on('data', (chunk) => {
|
|
469
|
+
this.lineBuffer += chunk.toString('utf-8');
|
|
470
|
+
let newlineIndex = this.lineBuffer.indexOf('\n');
|
|
471
|
+
while (newlineIndex >= 0) {
|
|
472
|
+
const line = this.lineBuffer.slice(0, newlineIndex).trim();
|
|
473
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
474
|
+
if (line) {
|
|
475
|
+
this.handleHelperMessage(line);
|
|
476
|
+
}
|
|
477
|
+
newlineIndex = this.lineBuffer.indexOf('\n');
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
socket.on('error', () => {
|
|
481
|
+
this.handleHelperDisconnect();
|
|
482
|
+
});
|
|
483
|
+
socket.on('close', () => {
|
|
484
|
+
this.handleHelperDisconnect();
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
handleHelperMessage(line) {
|
|
488
|
+
try {
|
|
489
|
+
const message = JSON.parse(line);
|
|
490
|
+
if (message.type !== 'focus-state' || message.instanceId !== this.instanceId) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
this.helperFocused = message.fullyFocused;
|
|
494
|
+
void this.syncFocusedPaneState();
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Ignore malformed helper output and keep current state.
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
handleHelperDisconnect() {
|
|
501
|
+
if (this.helperSocket) {
|
|
502
|
+
this.helperSocket.destroy();
|
|
503
|
+
this.helperSocket = null;
|
|
504
|
+
}
|
|
505
|
+
this.helperFocused = false;
|
|
506
|
+
this.setFullyFocusedPaneId(null);
|
|
507
|
+
if (!this.active || this.reconnectTimer) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.reconnectTimer = setTimeout(() => {
|
|
511
|
+
this.reconnectTimer = null;
|
|
512
|
+
this.connectToHelper();
|
|
513
|
+
}, HELPER_RECONNECT_DELAY_MS);
|
|
514
|
+
}
|
|
515
|
+
getFullyFocusedPaneId() {
|
|
516
|
+
return this.fullyFocusedPaneId;
|
|
517
|
+
}
|
|
518
|
+
isPaneFullyFocused(tmuxPaneId) {
|
|
519
|
+
return this.fullyFocusedPaneId === tmuxPaneId;
|
|
520
|
+
}
|
|
521
|
+
setPaneAttentionIndicator(tmuxPaneId, enabled) {
|
|
522
|
+
if (enabled) {
|
|
523
|
+
this.tmuxService.setPaneOptionSync(tmuxPaneId, '@dmux_attention', '1');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
this.tmuxService.unsetPaneOptionSync(tmuxPaneId, '@dmux_attention');
|
|
527
|
+
}
|
|
528
|
+
async getPaneAttentionSurface(tmuxPaneId) {
|
|
529
|
+
if (!this.active || !this.helperFocused) {
|
|
530
|
+
return 'background';
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const [currentPaneId, currentWindowId, paneWindowId] = await Promise.all([
|
|
534
|
+
this.tmuxService.getCurrentPaneId(),
|
|
535
|
+
this.tmuxService.getCurrentWindowId(),
|
|
536
|
+
this.tmuxService.getPaneWindowId(tmuxPaneId),
|
|
537
|
+
]);
|
|
538
|
+
if (currentPaneId === tmuxPaneId) {
|
|
539
|
+
return 'fully-focused';
|
|
540
|
+
}
|
|
541
|
+
if (currentWindowId && paneWindowId && currentWindowId === paneWindowId) {
|
|
542
|
+
return 'same-window';
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
return 'background';
|
|
547
|
+
}
|
|
548
|
+
return 'background';
|
|
549
|
+
}
|
|
550
|
+
async flashPaneAttention(tmuxPaneId) {
|
|
551
|
+
if (this.flashingTmuxPaneIds.has(tmuxPaneId)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.flashingTmuxPaneIds.add(tmuxPaneId);
|
|
555
|
+
const existingPaneStyle = this.tmuxService.getPaneOptionSync(tmuxPaneId, 'window-style');
|
|
556
|
+
const baseStyle = existingPaneStyle || this.tmuxService.getGlobalOptionSync('window-style');
|
|
557
|
+
const flashStyle = buildAttentionFlashWindowStyle(baseStyle);
|
|
558
|
+
const restorePaneStyle = () => {
|
|
559
|
+
if (existingPaneStyle) {
|
|
560
|
+
this.tmuxService.setPaneOptionSync(tmuxPaneId, 'window-style', existingPaneStyle);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
this.tmuxService.unsetPaneOptionSync(tmuxPaneId, 'window-style');
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
for (let step = 0; step < ATTENTION_FLASH_SEQUENCE_LENGTH; step += 1) {
|
|
567
|
+
setTimeout(() => {
|
|
568
|
+
if (!this.active) {
|
|
569
|
+
restorePaneStyle();
|
|
570
|
+
this.flashingTmuxPaneIds.delete(tmuxPaneId);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (step % 2 === 0) {
|
|
574
|
+
this.tmuxService.setPaneOptionSync(tmuxPaneId, 'window-style', flashStyle);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
restorePaneStyle();
|
|
578
|
+
}
|
|
579
|
+
if (step === ATTENTION_FLASH_SEQUENCE_LENGTH - 1) {
|
|
580
|
+
restorePaneStyle();
|
|
581
|
+
this.flashingTmuxPaneIds.delete(tmuxPaneId);
|
|
582
|
+
}
|
|
583
|
+
}, step * ATTENTION_FLASH_STEP_MS);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async sendAttentionNotification(request) {
|
|
587
|
+
if (!supportsNativeDmuxHelper() || isTestEnvironment()) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const socketPath = await this.ensureHelperSocketPath();
|
|
591
|
+
if (!socketPath) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
const payload = {
|
|
595
|
+
type: 'notify',
|
|
596
|
+
title: request.title,
|
|
597
|
+
subtitle: request.subtitle,
|
|
598
|
+
body: request.body,
|
|
599
|
+
soundName: this.resolveAttentionNotificationSoundName(),
|
|
600
|
+
titleToken: this.token,
|
|
601
|
+
bundleId: this.bundleId,
|
|
602
|
+
tmuxPaneId: request.tmuxPaneId,
|
|
603
|
+
tmuxSocketPath: this.tmuxSocketPath,
|
|
604
|
+
};
|
|
605
|
+
return new Promise((resolve) => {
|
|
606
|
+
const socket = createConnection(socketPath);
|
|
607
|
+
let settled = false;
|
|
608
|
+
const finish = (value) => {
|
|
609
|
+
if (settled) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
settled = true;
|
|
613
|
+
socket.destroy();
|
|
614
|
+
resolve(value);
|
|
615
|
+
};
|
|
616
|
+
socket.once('connect', () => {
|
|
617
|
+
socket.write(`${JSON.stringify(payload)}\n`, (error) => {
|
|
618
|
+
finish(!error);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
socket.once('error', () => {
|
|
622
|
+
finish(false);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
async ensureHelperSocketPath() {
|
|
627
|
+
if (this.helperSocketPath) {
|
|
628
|
+
const helperReady = await waitForHelperSocket(this.helperSocketPath, 100);
|
|
629
|
+
if (helperReady) {
|
|
630
|
+
return this.helperSocketPath;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const helperSocketPath = await ensureHelperRunning(this.logger);
|
|
634
|
+
if (!helperSocketPath) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
this.helperSocketPath = helperSocketPath;
|
|
638
|
+
return helperSocketPath;
|
|
639
|
+
}
|
|
640
|
+
async syncFocusedPaneState() {
|
|
641
|
+
if (!this.active || !this.helperFocused) {
|
|
642
|
+
this.setFullyFocusedPaneId(null);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const currentPaneId = await this.tmuxService.getCurrentPaneId();
|
|
647
|
+
if (!currentPaneId) {
|
|
648
|
+
this.setFullyFocusedPaneId(null);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.setFullyFocusedPaneId(currentPaneId);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
this.setFullyFocusedPaneId(null);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
setFullyFocusedPaneId(paneId) {
|
|
658
|
+
if (this.fullyFocusedPaneId === paneId) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this.fullyFocusedPaneId = paneId;
|
|
662
|
+
this.emit('focus-changed', {
|
|
663
|
+
fullyFocusedPaneId: paneId,
|
|
664
|
+
helperFocused: this.helperFocused,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
writeTerminalTitle(title) {
|
|
668
|
+
process.stdout.write(buildTerminalTitleSequence(title, Boolean(process.env.TMUX)));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=DmuxFocusService.js.map
|