fpasoterm 1.6.0 → 1.6.1
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.
Potentially problematic release.
This version of fpasoterm might be problematic. Click here for more details.
- package/README.ja.md +2 -2
- package/README.md +6 -6
- package/bin/fpasoterm +9 -3
- package/docs/config.en.md +20 -10
- package/docs/config.ja.md +12 -9
- package/docs/plugins.en.md +1 -1
- package/docs/plugins.ja.md +1 -1
- package/docs/spec.en.md +3 -3
- package/docs/spec.ja.md +3 -3
- package/docs/sync.en.md +3 -3
- package/docs/sync.ja.md +3 -3
- package/examples/config/default-appearance.toml +11 -12
- package/package.json +1 -1
- package/scripts/tests/smoke.js +110 -58
- package/src/config.js +53 -25
- package/src/renderer/index.html +25 -12
- package/src/renderer/renderer.js +173 -49
- package/src/renderer/styles.css +43 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/default-config.toml +12 -13
- package/src-tauri/src/main.rs +93 -128
- package/src/renderer/confirm.html +0 -71
package/src/renderer/renderer.js
CHANGED
|
@@ -34,6 +34,7 @@ const closeAllConfirmElement = document.getElementById('close-all-confirm');
|
|
|
34
34
|
const closeAllConfirmMessageElement = document.getElementById('close-all-confirm-message');
|
|
35
35
|
const closeAllConfirmOkButton = document.getElementById('close-all-confirm-ok');
|
|
36
36
|
const closeAllConfirmCancelButton = document.getElementById('close-all-confirm-cancel');
|
|
37
|
+
const closeAllConfirmUnmountButton = document.getElementById('close-all-confirm-unmount');
|
|
37
38
|
const windowMenu = document.getElementById('window-menu');
|
|
38
39
|
const windowMenuToggleButton = document.getElementById('window-menu-toggle');
|
|
39
40
|
const windowMenuItems = document.getElementById('window-menu-items');
|
|
@@ -148,19 +149,18 @@ const fallbackConfig = {
|
|
|
148
149
|
},
|
|
149
150
|
keybindings: {
|
|
150
151
|
prefix: 'Mod+Shift',
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
closeAll: 'X',
|
|
152
|
+
logToggle: 's',
|
|
153
|
+
logShow: 'l',
|
|
154
|
+
copy: 'c',
|
|
155
|
+
paste: 'v',
|
|
156
|
+
menu: 'm',
|
|
157
|
+
help: 'h',
|
|
158
|
+
newWindow: 'n',
|
|
159
|
+
openCwd: 'o',
|
|
160
|
+
broadcast: 'b',
|
|
161
|
+
kill: 'k',
|
|
162
|
+
tile: 't',
|
|
163
|
+
closeAll: 'x',
|
|
164
164
|
},
|
|
165
165
|
plugins: {
|
|
166
166
|
enabled: [],
|
|
@@ -352,7 +352,7 @@ function applyKeybindingLabels() {
|
|
|
352
352
|
: '';
|
|
353
353
|
}
|
|
354
354
|
if (windowMenuToggleButton) {
|
|
355
|
-
windowMenuToggleButton.setAttribute('aria-keyshortcuts',
|
|
355
|
+
windowMenuToggleButton.setAttribute('aria-keyshortcuts', keybindingLabel('menu'));
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
@@ -424,7 +424,7 @@ function installTauriApiAdapter() {
|
|
|
424
424
|
newWindowAtCwd: (cwd, host) => invoke('window_new_at_cwd', { cwd, host: host || null }),
|
|
425
425
|
arrangeWindows: (screen) => invoke('window_arrange', { screen }),
|
|
426
426
|
closeAllWindows: () => invoke('window_close_all'),
|
|
427
|
-
|
|
427
|
+
closeAllConfirmed: (unmountSshfs) => invoke('window_close_all_confirmed', { unmountSshfs }),
|
|
428
428
|
startWindowDrag: () => invoke('window_start_drag'),
|
|
429
429
|
startWindowResizeDrag: (direction) => window.__TAURI__.window.getCurrentWindow().startResizeDragging(direction),
|
|
430
430
|
saveWindowBounds: () => invoke('window_save_bounds'),
|
|
@@ -2239,6 +2239,20 @@ function enableFloatingPanelDrag(panel, handle) {
|
|
|
2239
2239
|
enableFloatingPanelDrag(diagnosticsPanel, diagnosticsPanel?.querySelector('[data-panel-drag-handle]'));
|
|
2240
2240
|
enableFloatingPanelDrag(terminalBroadcastDialog, terminalBroadcastTitle);
|
|
2241
2241
|
|
|
2242
|
+
// WebView checkbox keyboard activation differs on Windows. Keep Space and Enter
|
|
2243
|
+
// consistent for Broadcast target controls and preserve their normal change event.
|
|
2244
|
+
function enableCheckboxKeyboardToggle(checkbox) {
|
|
2245
|
+
checkbox?.addEventListener('keydown', (event) => {
|
|
2246
|
+
if (event.isComposing || event.repeat || ![' ', 'Enter'].includes(event.key)) {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
event.preventDefault();
|
|
2250
|
+
checkbox.click();
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
enableCheckboxKeyboardToggle(terminalBroadcastSync);
|
|
2255
|
+
|
|
2242
2256
|
// Returns focusable controls in the visible Broadcast dialog, including its
|
|
2243
2257
|
// dynamically rendered local-window target checkboxes.
|
|
2244
2258
|
function terminalBroadcastFocusItems() {
|
|
@@ -2498,36 +2512,61 @@ function confirmTerminalLogAction(message, returnFocus) {
|
|
|
2498
2512
|
});
|
|
2499
2513
|
}
|
|
2500
2514
|
|
|
2501
|
-
//
|
|
2502
|
-
|
|
2515
|
+
// Keeps Close All confirmation inside the invoking window on every platform.
|
|
2516
|
+
// This gives Windows, macOS, and Linux the same focus and keyboard behavior.
|
|
2517
|
+
function focusCloseAllConfirmation() {
|
|
2518
|
+
if (closeAllConfirmElement.hidden) return;
|
|
2519
|
+
closeAllConfirmCancelButton.focus({ preventScroll: true });
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// WebViews can restore xterm focus while an overlay is first painted. Retry
|
|
2523
|
+
// briefly so the initial keyboard target is consistently the Cancel button.
|
|
2524
|
+
function scheduleCloseAllConfirmationFocus() {
|
|
2525
|
+
requestAnimationFrame(focusCloseAllConfirmation);
|
|
2526
|
+
for (const delay of [0, 50, 120, 240]) {
|
|
2527
|
+
setTimeout(focusCloseAllConfirmation, delay);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
async function confirmCloseAllWindows() {
|
|
2503
2532
|
if (!closeAllConfirmElement || !closeAllConfirmMessageElement || !closeAllConfirmOkButton) {
|
|
2504
2533
|
showDiagnostic('close all confirmation UI is unavailable');
|
|
2505
|
-
return
|
|
2534
|
+
return { confirmed: false, unmountSshfs: false };
|
|
2506
2535
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2536
|
+
let status = null;
|
|
2537
|
+
if (typeof window.fpasoterm.getSshfsStatus === 'function') {
|
|
2538
|
+
status = await window.fpasoterm.getSshfsStatus().catch(() => null);
|
|
2539
|
+
}
|
|
2540
|
+
const mountCount = Array.isArray(status?.mounts) ? status.mounts.length : 0;
|
|
2541
|
+
closeAllConfirmMessageElement.textContent = mountCount
|
|
2542
|
+
? `Close all running fpasoterm windows?\n\n${mountCount} SSHFS mount(s) will remain active unless you choose Unmount & Close.`
|
|
2543
|
+
: 'Close all running fpasoterm windows?\n\nThis will terminate every fpasoterm window.';
|
|
2544
|
+
closeAllConfirmUnmountButton.hidden = mountCount === 0;
|
|
2509
2545
|
closeAllConfirmElement.hidden = false;
|
|
2510
|
-
|
|
2546
|
+
scheduleCloseAllConfirmationFocus();
|
|
2511
2547
|
return new Promise((resolve) => {
|
|
2512
2548
|
closeAllConfirmResolver = resolve;
|
|
2513
2549
|
});
|
|
2514
2550
|
}
|
|
2515
2551
|
|
|
2516
2552
|
// Resolves and hides the close-all confirmation dialog.
|
|
2517
|
-
function resolveCloseAllWindows(confirmed) {
|
|
2553
|
+
function resolveCloseAllWindows(confirmed, unmountSshfs = false) {
|
|
2518
2554
|
if (!closeAllConfirmElement || closeAllConfirmElement.hidden) {
|
|
2519
2555
|
return;
|
|
2520
2556
|
}
|
|
2521
2557
|
closeAllConfirmElement.hidden = true;
|
|
2558
|
+
closeAllConfirmUnmountButton.hidden = true;
|
|
2522
2559
|
const resolver = closeAllConfirmResolver;
|
|
2523
2560
|
closeAllConfirmResolver = null;
|
|
2524
2561
|
closeAllWindowsButton.focus({ preventScroll: true });
|
|
2525
|
-
resolver?.(confirmed);
|
|
2562
|
+
resolver?.({ confirmed, unmountSshfs });
|
|
2526
2563
|
}
|
|
2527
2564
|
|
|
2528
2565
|
// Asks for confirmation before closing all application instances.
|
|
2529
|
-
function requestCloseAllWindows() {
|
|
2530
|
-
|
|
2566
|
+
async function requestCloseAllWindows() {
|
|
2567
|
+
const result = await confirmCloseAllWindows();
|
|
2568
|
+
if (!result.confirmed) return;
|
|
2569
|
+
window.fpasoterm.closeAllConfirmed(result.unmountSshfs).catch((error) => {
|
|
2531
2570
|
showDiagnostic(`close all confirmation failed: ${error}`);
|
|
2532
2571
|
});
|
|
2533
2572
|
}
|
|
@@ -2548,7 +2587,7 @@ function showTerminalOutputLogFromMenu() {
|
|
|
2548
2587
|
showTerminalOutputLog().catch((error) => {
|
|
2549
2588
|
terminalLogShowButton.textContent = 'Error';
|
|
2550
2589
|
setTimeout(() => {
|
|
2551
|
-
terminalLogShowButton.textContent = 'Log Show (^
|
|
2590
|
+
terminalLogShowButton.textContent = 'Log Show (^l)';
|
|
2552
2591
|
}, 1400);
|
|
2553
2592
|
showDiagnostic(`terminal log show failed: ${error}`);
|
|
2554
2593
|
}).finally(() => setWindowMenuOpen(false));
|
|
@@ -2565,6 +2604,7 @@ function renderTerminalBroadcastTargets(targets) {
|
|
|
2565
2604
|
checkbox.checked = true;
|
|
2566
2605
|
checkbox.dataset.instanceId = target.id;
|
|
2567
2606
|
checkbox.addEventListener('change', refreshTerminalBroadcastSyncOption);
|
|
2607
|
+
enableCheckboxKeyboardToggle(checkbox);
|
|
2568
2608
|
label.append(checkbox, document.createTextNode(`${target.title} (pid ${target.pid})`));
|
|
2569
2609
|
terminalBroadcastTargetList.append(label);
|
|
2570
2610
|
}
|
|
@@ -2637,12 +2677,29 @@ function decodeTerminalBroadcastControls(text) {
|
|
|
2637
2677
|
'\\x0d': '\r',
|
|
2638
2678
|
'\\x1b': '\x1b',
|
|
2639
2679
|
'\\x09': '\t',
|
|
2680
|
+
'\\x01': '\x01',
|
|
2681
|
+
'\\x02': '\x02',
|
|
2640
2682
|
'\\x03': '\x03',
|
|
2641
2683
|
'\\x04': '\x04',
|
|
2642
2684
|
'\\x18': '\x18',
|
|
2643
2685
|
'\\x1a': '\x1a',
|
|
2644
2686
|
};
|
|
2645
|
-
return String(text || '').replace(/\\x(?:0d|1b|09|03|04|18|1a)/gi, (match) => controls[match.toLowerCase()] || match);
|
|
2687
|
+
return String(text || '').replace(/\\x(?:0d|1b|09|01|02|03|04|18|1a)/gi, (match) => controls[match.toLowerCase()] || match);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Extracts one trailing semantic key marker. Keeping the marker visible in the
|
|
2691
|
+
// dialog lets a control prefix such as Ctrl+B be inspected before it is sent.
|
|
2692
|
+
function extractTerminalBroadcastKeyEvent(text) {
|
|
2693
|
+
const value = String(text || '');
|
|
2694
|
+
const match = value.match(/\\key\[(ArrowUp|ArrowDown|ArrowLeft|ArrowRight)\]$/i);
|
|
2695
|
+
if (!match) {
|
|
2696
|
+
return { text: value, keyEvent: null };
|
|
2697
|
+
}
|
|
2698
|
+
const key = match[1];
|
|
2699
|
+
return {
|
|
2700
|
+
text: value.slice(0, -match[0].length),
|
|
2701
|
+
keyEvent: { key, code: key, shiftKey: false },
|
|
2702
|
+
};
|
|
2646
2703
|
}
|
|
2647
2704
|
|
|
2648
2705
|
// Uses xterm's active keyboard encoder instead of guessing a byte sequence.
|
|
@@ -2665,11 +2722,19 @@ function dispatchTerminalBroadcastKey(keyEvent) {
|
|
|
2665
2722
|
showDiagnostic('terminal broadcast key event is unavailable');
|
|
2666
2723
|
return;
|
|
2667
2724
|
}
|
|
2725
|
+
const key = String(keyEvent.key || '');
|
|
2726
|
+
const keyCodes = {
|
|
2727
|
+
Enter: 13,
|
|
2728
|
+
ArrowUp: 38,
|
|
2729
|
+
ArrowDown: 40,
|
|
2730
|
+
ArrowLeft: 37,
|
|
2731
|
+
ArrowRight: 39,
|
|
2732
|
+
};
|
|
2668
2733
|
const event = {
|
|
2669
2734
|
type: 'keydown',
|
|
2670
|
-
key
|
|
2671
|
-
code:
|
|
2672
|
-
keyCode:
|
|
2735
|
+
key,
|
|
2736
|
+
code: String(keyEvent.code || key),
|
|
2737
|
+
keyCode: keyCodes[key] || 0,
|
|
2673
2738
|
shiftKey: keyEvent.shiftKey === true,
|
|
2674
2739
|
altKey: false,
|
|
2675
2740
|
ctrlKey: false,
|
|
@@ -2689,10 +2754,16 @@ function insertTerminalBroadcastControl() {
|
|
|
2689
2754
|
const controls = {
|
|
2690
2755
|
enter: { text: '\\x0D', label: 'Enter / CR (0x0D)' },
|
|
2691
2756
|
tab: { text: '\\x09', label: 'Tab (0x09)' },
|
|
2757
|
+
'ctrl-a': { text: '\\x01', label: 'Ctrl+A (0x01)' },
|
|
2758
|
+
'ctrl-b': { text: '\\x02', label: 'Ctrl+B (0x02)' },
|
|
2692
2759
|
'ctrl-c': { text: '\\x03', label: 'Ctrl+C (0x03)' },
|
|
2693
2760
|
'ctrl-d': { text: '\\x04', label: 'Ctrl+D (0x04)' },
|
|
2694
2761
|
'ctrl-x': { text: '\\x18', label: 'Ctrl+X (0x18)' },
|
|
2695
2762
|
'ctrl-z': { text: '\\x1A', label: 'Ctrl+Z (0x1A)' },
|
|
2763
|
+
'arrow-up': { text: '\\key[ArrowUp]', label: 'Arrow Up' },
|
|
2764
|
+
'arrow-down': { text: '\\key[ArrowDown]', label: 'Arrow Down' },
|
|
2765
|
+
'arrow-left': { text: '\\key[ArrowLeft]', label: 'Arrow Left' },
|
|
2766
|
+
'arrow-right': { text: '\\key[ArrowRight]', label: 'Arrow Right' },
|
|
2696
2767
|
'alt-prefix': { text: '\\x1B', label: 'Alt prefix / Esc (0x1B)' },
|
|
2697
2768
|
};
|
|
2698
2769
|
const selected = controls[control];
|
|
@@ -2764,12 +2835,12 @@ function resolveDangerousBroadcastConfirmation(confirmed) {
|
|
|
2764
2835
|
// Writes the dialog text to local windows and optionally the configured sync channel.
|
|
2765
2836
|
async function sendTerminalBroadcast() {
|
|
2766
2837
|
const rawText = String(terminalBroadcastText?.value || '');
|
|
2767
|
-
const
|
|
2768
|
-
const text =
|
|
2769
|
-
const keyEvent = shouldAppendTerminalBroadcastEnter(text)
|
|
2838
|
+
const decodedText = decodeTerminalBroadcastControls(normalizePasteText(rawText).replace(/\r+$/, ''));
|
|
2839
|
+
const { text, keyEvent: selectedKeyEvent } = extractTerminalBroadcastKeyEvent(decodedText);
|
|
2840
|
+
const keyEvent = selectedKeyEvent || (shouldAppendTerminalBroadcastEnter(text)
|
|
2770
2841
|
? terminalBroadcastSubmitKeyEvent()
|
|
2771
|
-
: null;
|
|
2772
|
-
if (!text) {
|
|
2842
|
+
: null);
|
|
2843
|
+
if (!text && !keyEvent) {
|
|
2773
2844
|
showDiagnostic('terminal broadcast skipped: input is empty');
|
|
2774
2845
|
terminalBroadcastText?.focus({ preventScroll: true });
|
|
2775
2846
|
return;
|
|
@@ -2948,7 +3019,6 @@ async function showKeyboardShortcutsHelp() {
|
|
|
2948
3019
|
`fpasoterm ${version}`,
|
|
2949
3020
|
`Config: ${activeConfigPath || 'unknown'}`,
|
|
2950
3021
|
'',
|
|
2951
|
-
`${keybindingLabel('logMenu')} Open the window menu at Log actions`,
|
|
2952
3022
|
`${keybindingLabel('logToggle')} Start or stop terminal output logging`,
|
|
2953
3023
|
`${keybindingLabel('logShow')} Show terminal output logs`,
|
|
2954
3024
|
`${keybindingLabel('copy')} Copy selected terminal or log text`,
|
|
@@ -3265,13 +3335,37 @@ closeAllConfirmCancelButton.addEventListener('click', () => {
|
|
|
3265
3335
|
resolveCloseAllWindows(false);
|
|
3266
3336
|
});
|
|
3267
3337
|
|
|
3338
|
+
closeAllConfirmUnmountButton.addEventListener('click', () => {
|
|
3339
|
+
resolveCloseAllWindows(true, true);
|
|
3340
|
+
});
|
|
3341
|
+
|
|
3268
3342
|
closeAllConfirmElement.addEventListener('keydown', (event) => {
|
|
3269
3343
|
if (event.key === 'Escape') {
|
|
3270
3344
|
event.preventDefault();
|
|
3271
3345
|
resolveCloseAllWindows(false);
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
if (event.key === 'Tab') {
|
|
3349
|
+
event.preventDefault();
|
|
3350
|
+
const buttons = [
|
|
3351
|
+
closeAllConfirmCancelButton,
|
|
3352
|
+
...(!closeAllConfirmUnmountButton.hidden ? [closeAllConfirmUnmountButton] : []),
|
|
3353
|
+
closeAllConfirmOkButton,
|
|
3354
|
+
];
|
|
3355
|
+
const index = buttons.indexOf(document.activeElement);
|
|
3356
|
+
const next = (index + (event.shiftKey ? buttons.length - 1 : 1) + buttons.length) % buttons.length;
|
|
3357
|
+
buttons[next].focus({ preventScroll: true });
|
|
3272
3358
|
}
|
|
3273
3359
|
});
|
|
3274
3360
|
|
|
3361
|
+
closeAllConfirmElement.addEventListener('focusout', () => {
|
|
3362
|
+
requestAnimationFrame(() => {
|
|
3363
|
+
if (!closeAllConfirmElement.hidden && !closeAllConfirmElement.contains(document.activeElement)) {
|
|
3364
|
+
focusCloseAllConfirmation();
|
|
3365
|
+
}
|
|
3366
|
+
});
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3275
3369
|
terminalCopyButton.addEventListener('click', () => {
|
|
3276
3370
|
copyTerminalSelection().catch((error) => {
|
|
3277
3371
|
showDiagnostic(`terminal menu copy failed: ${error}`);
|
|
@@ -3434,16 +3528,6 @@ document.addEventListener('keydown', (event) => {
|
|
|
3434
3528
|
return;
|
|
3435
3529
|
}
|
|
3436
3530
|
|
|
3437
|
-
if (matchesKeybinding(event, 'logMenu')) {
|
|
3438
|
-
event.preventDefault();
|
|
3439
|
-
const open = windowMenuItems?.hidden !== false;
|
|
3440
|
-
setWindowMenuOpen(open, terminalLogToggleButton);
|
|
3441
|
-
if (open) {
|
|
3442
|
-
setWindowMenuSubmenuOpen('log', true, terminalLogToggleButton);
|
|
3443
|
-
}
|
|
3444
|
-
return;
|
|
3445
|
-
}
|
|
3446
|
-
|
|
3447
3531
|
if (matchesKeybinding(event, 'menu')) {
|
|
3448
3532
|
event.preventDefault();
|
|
3449
3533
|
setWindowMenuOpen(windowMenuItems?.hidden !== false);
|
|
@@ -3565,9 +3649,12 @@ async function requestWindowClose() {
|
|
|
3565
3649
|
function confirmSshfsWindowClose(mounts) {
|
|
3566
3650
|
closeAllConfirmMessageElement.textContent =
|
|
3567
3651
|
`Keep ${mounts.length} SSHFS mount(s) active after closing this terminal window?\n\nUse Window > SSHFS Mounts to unmount them later.`;
|
|
3652
|
+
closeAllConfirmUnmountButton.hidden = true;
|
|
3568
3653
|
closeAllConfirmElement.hidden = false;
|
|
3569
|
-
closeAllConfirmOkButton.focus({ preventScroll: true });
|
|
3570
|
-
return new Promise((resolve) => {
|
|
3654
|
+
requestAnimationFrame(() => closeAllConfirmOkButton.focus({ preventScroll: true }));
|
|
3655
|
+
return new Promise((resolve) => {
|
|
3656
|
+
closeAllConfirmResolver = (result) => resolve(result?.confirmed === true);
|
|
3657
|
+
});
|
|
3571
3658
|
}
|
|
3572
3659
|
|
|
3573
3660
|
// Closes the frameless window from the custom titlebar.
|
|
@@ -3670,6 +3757,26 @@ function selectedSshfsMountName() {
|
|
|
3670
3757
|
return sshfsDialogMounts.some((mount) => mount.mountName === mountName) ? mountName : '';
|
|
3671
3758
|
}
|
|
3672
3759
|
|
|
3760
|
+
// Keeps keyboard navigation inside the SSHFS modal instead of falling through
|
|
3761
|
+
// to titlebar controls or xterm.js on WebViews that do not enforce aria-modal.
|
|
3762
|
+
function sshfsManagerFocusItems() {
|
|
3763
|
+
if (!sshfsManagerDialog || sshfsManagerDialog.hidden) {
|
|
3764
|
+
return [];
|
|
3765
|
+
}
|
|
3766
|
+
return [...sshfsManagerDialog.querySelectorAll('input, select, button, a[href]')]
|
|
3767
|
+
.filter((element) => !element.hidden && !element.disabled && element.getClientRects().length > 0);
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
function focusSshfsManagerItem(delta) {
|
|
3771
|
+
const items = sshfsManagerFocusItems();
|
|
3772
|
+
if (items.length === 0) {
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
const index = items.indexOf(document.activeElement);
|
|
3776
|
+
const next = index < 0 ? (delta < 0 ? items.length - 1 : 0) : (index + delta + items.length) % items.length;
|
|
3777
|
+
items[next].focus({ preventScroll: true });
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3673
3780
|
async function refreshSshfsDialog(showResult = true) {
|
|
3674
3781
|
const status = await window.fpasoterm.getSshfsStatus();
|
|
3675
3782
|
const selected = sshfsManagerSavedMounts.value;
|
|
@@ -3777,7 +3884,24 @@ sshfsManagerForgetButton.addEventListener('click', async () => {
|
|
|
3777
3884
|
});
|
|
3778
3885
|
sshfsManagerCloseButton.addEventListener('click', closeSshfsManagerModal);
|
|
3779
3886
|
sshfsManagerDialog.addEventListener('keydown', (event) => {
|
|
3780
|
-
if (event.key === 'Escape') {
|
|
3887
|
+
if (event.key === 'Escape') {
|
|
3888
|
+
event.preventDefault();
|
|
3889
|
+
event.stopImmediatePropagation();
|
|
3890
|
+
closeSshfsManagerModal();
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
if (event.key === 'Tab') {
|
|
3894
|
+
event.preventDefault();
|
|
3895
|
+
event.stopImmediatePropagation();
|
|
3896
|
+
focusSshfsManagerItem(event.shiftKey ? -1 : 1);
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
sshfsManagerDialog.addEventListener('focusout', () => {
|
|
3900
|
+
requestAnimationFrame(() => {
|
|
3901
|
+
if (!sshfsManagerDialog.hidden && !sshfsManagerDialog.contains(document.activeElement)) {
|
|
3902
|
+
focusSshfsManagerItem(1);
|
|
3903
|
+
}
|
|
3904
|
+
});
|
|
3781
3905
|
});
|
|
3782
3906
|
|
|
3783
3907
|
// Opens the shared SSHFS manager without creating another native WebView window.
|
package/src/renderer/styles.css
CHANGED
|
@@ -368,6 +368,25 @@ body {
|
|
|
368
368
|
color: #f5d76e;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
#terminal-broadcast-examples {
|
|
372
|
+
margin-top: 8px;
|
|
373
|
+
color: #c7d1dc;
|
|
374
|
+
font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#terminal-broadcast-examples summary {
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
color: #f5d76e;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#terminal-broadcast-examples p {
|
|
383
|
+
margin: 6px 0 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#terminal-broadcast-examples code {
|
|
387
|
+
color: #e8edf2;
|
|
388
|
+
}
|
|
389
|
+
|
|
371
390
|
#terminal-broadcast-sync-label {
|
|
372
391
|
display: block;
|
|
373
392
|
margin: 10px 0;
|
|
@@ -683,6 +702,19 @@ body {
|
|
|
683
702
|
display: none;
|
|
684
703
|
}
|
|
685
704
|
|
|
705
|
+
/* Keep Close All above the terminal in the current window. This avoids
|
|
706
|
+
* relying on compositor-controlled focus of a separate child window. */
|
|
707
|
+
#close-all-confirm {
|
|
708
|
+
position: fixed;
|
|
709
|
+
top: var(--titlebar-height);
|
|
710
|
+
right: 0;
|
|
711
|
+
bottom: 0;
|
|
712
|
+
left: 0;
|
|
713
|
+
z-index: 10060;
|
|
714
|
+
app-region: no-drag;
|
|
715
|
+
-webkit-app-region: no-drag;
|
|
716
|
+
}
|
|
717
|
+
|
|
686
718
|
#sshfs-manager-dialog[hidden] {
|
|
687
719
|
display: none;
|
|
688
720
|
}
|
|
@@ -766,8 +798,8 @@ body {
|
|
|
766
798
|
text-decoration: none;
|
|
767
799
|
}
|
|
768
800
|
|
|
769
|
-
.sshfs-manager-card :is(input, select, button):focus,
|
|
770
|
-
.sshfs-manager-card :is(input, select, button):focus-visible {
|
|
801
|
+
.sshfs-manager-card :is(input, select, button, a[href]):focus,
|
|
802
|
+
.sshfs-manager-card :is(input, select, button, a[href]):focus-visible {
|
|
771
803
|
outline: 3px solid #f5d76e;
|
|
772
804
|
outline-offset: 2px;
|
|
773
805
|
border-color: #f5d76e;
|
|
@@ -880,6 +912,9 @@ body {
|
|
|
880
912
|
|
|
881
913
|
#terminal-log-confirm-ok,
|
|
882
914
|
#terminal-log-confirm-cancel,
|
|
915
|
+
#close-all-confirm-ok,
|
|
916
|
+
#close-all-confirm-cancel,
|
|
917
|
+
#close-all-confirm-unmount,
|
|
883
918
|
#terminal-broadcast-confirm-ok,
|
|
884
919
|
#terminal-broadcast-confirm-cancel,
|
|
885
920
|
#terminal-url-copy,
|
|
@@ -894,6 +929,12 @@ body {
|
|
|
894
929
|
font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
895
930
|
}
|
|
896
931
|
|
|
932
|
+
#close-all-confirm button:focus,
|
|
933
|
+
#close-all-confirm button:focus-visible {
|
|
934
|
+
outline: 3px solid #f5d76e;
|
|
935
|
+
outline-offset: 2px;
|
|
936
|
+
}
|
|
937
|
+
|
|
897
938
|
#diagnostics-path {
|
|
898
939
|
overflow: hidden;
|
|
899
940
|
padding: 4px 8px;
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -65,23 +65,22 @@ repeatedTextWindowMs = 140
|
|
|
65
65
|
|
|
66
66
|
# Mod means Ctrl on Windows/Linux and Cmd on macOS. Set prefix = "Ctrl+Alt"
|
|
67
67
|
# when Ctrl+Shift is reserved by a Windows application or keyboard layout.
|
|
68
|
-
# An individual value may be a key such as "
|
|
68
|
+
# An individual value may be a key such as "n" or a full shortcut such as
|
|
69
69
|
# "Ctrl+Alt+KeyN". KeyN-style values bind by physical keyboard position.
|
|
70
70
|
[keybindings]
|
|
71
71
|
prefix = "Mod+Shift"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
newWindow = "N"
|
|
72
|
+
logToggle = "s"
|
|
73
|
+
logShow = "l"
|
|
74
|
+
copy = "c"
|
|
75
|
+
paste = "v"
|
|
76
|
+
menu = "m"
|
|
77
|
+
help = "h"
|
|
78
|
+
newWindow = "n"
|
|
80
79
|
openCwd = "o"
|
|
81
|
-
broadcast = "
|
|
82
|
-
kill = "
|
|
83
|
-
tile = "
|
|
84
|
-
closeAll = "
|
|
80
|
+
broadcast = "b"
|
|
81
|
+
kill = "k"
|
|
82
|
+
tile = "t"
|
|
83
|
+
closeAll = "x"
|
|
85
84
|
|
|
86
85
|
[plugins]
|
|
87
86
|
enabled = []
|