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.

@@ -25,8 +25,6 @@ use std::{ptr, slice};
25
25
  use tauri::{
26
26
  AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize, State, WebviewWindow, WindowEvent,
27
27
  };
28
- #[cfg(not(target_os = "windows"))]
29
- use tauri::{WebviewUrl, WebviewWindowBuilder};
30
28
  #[cfg(target_os = "windows")]
31
29
  use windows_sys::Win32::Foundation::{CloseHandle, GlobalFree, INVALID_HANDLE_VALUE};
32
30
  #[cfg(target_os = "windows")]
@@ -788,10 +786,7 @@ fn main() {
788
786
  sshfs_unmount_all,
789
787
  sshfs_forget,
790
788
  sshfs_status,
791
- window_confirm_close_all,
792
789
  window_close_all_confirmed,
793
- window_focus_main,
794
- window_cancel_close_all,
795
790
  window_save_bounds,
796
791
  window_get_bounds,
797
792
  window_set_bounds,
@@ -2065,22 +2060,12 @@ fn sshfs_mount(
2065
2060
  write_sshfs_mount_records(&mounts)?;
2066
2061
  Ok(SshfsMountResult {
2067
2062
  mount_point: mount_point.display().to_string(),
2068
- message: sshfs_mount_ready_message(),
2063
+ message:
2064
+ "SSHFS mount is ready; use this path from the terminal or another local application"
2065
+ .to_string(),
2069
2066
  })
2070
2067
  }
2071
2068
 
2072
- // macFUSE retains the sshfs process after mounting, but the mount table has
2073
- // already confirmed the requested filesystem is usable before this is called.
2074
- #[cfg(target_os = "macos")]
2075
- fn sshfs_mount_ready_message() -> String {
2076
- "SSHFS mount is ready; use this path from the terminal or another local application".to_string()
2077
- }
2078
-
2079
- #[cfg(not(target_os = "macos"))]
2080
- fn sshfs_mount_ready_message() -> String {
2081
- "SSHFS mount is ready; use this path from the terminal or another local application".to_string()
2082
- }
2083
-
2084
2069
  // SSHFS-Win officially exposes SSHFS mounts as mapped network drives. Calling
2085
2070
  // its Cygwin sshfs.exe directly from a GUI process produces orphaned FUSE
2086
2071
  // mounts, so use Windows' network-drive API and matching disconnect API.
@@ -2739,109 +2724,16 @@ fn detach_nested_macos_launch() -> bool {
2739
2724
  false
2740
2725
  }
2741
2726
 
2742
- // Opens a separate native confirmation window so the prompt is visible even
2743
- // when the terminal content is busy or visually obscured.
2744
- #[tauri::command]
2745
- fn window_confirm_close_all(app: AppHandle) -> Result<String, String> {
2746
- #[cfg(target_os = "windows")]
2747
- {
2748
- // A native dialog remains operable even if a secondary WebView fails to
2749
- // load, and Windows supplies keyboard navigation plus a working close button.
2750
- if let Some(window) = app.get_webview_window("close-all-confirm") {
2751
- let _ = window.close();
2752
- }
2753
- if windows_confirm_close_all() {
2754
- broadcast_close_all_request()?;
2755
- return Ok("closed all fpasoterm windows".to_string());
2756
- }
2757
- let _ = window_focus_main(app);
2758
- return Ok("close all canceled".to_string());
2759
- }
2760
-
2761
- #[cfg(not(target_os = "windows"))]
2762
- {
2763
- if let Some(window) = app.get_webview_window("close-all-confirm") {
2764
- let _ = window.show();
2765
- let _ = window.unminimize();
2766
- let _ = window.set_focus();
2767
- return Ok("close confirmation already open".to_string());
2768
- }
2769
- let window = WebviewWindowBuilder::new(
2770
- &app,
2771
- "close-all-confirm",
2772
- WebviewUrl::App("confirm.html".into()),
2773
- )
2774
- .title("Confirm close all fpasoterm windows")
2775
- .inner_size(600.0, 280.0)
2776
- .min_inner_size(520.0, 220.0)
2777
- .resizable(true)
2778
- .decorations(true)
2779
- .transparent(false)
2780
- .visible(true)
2781
- .always_on_top(true)
2782
- .focused(true)
2783
- .center()
2784
- .build()
2785
- .map_err(|error| error.to_string())?;
2786
- // Make the new native window receive keyboard input immediately.
2787
- let _ = window.set_focus();
2788
- Ok("close confirmation opened".to_string())
2789
- }
2790
- }
2791
-
2792
- #[cfg(target_os = "windows")]
2793
- // Uses the Windows standard modal dialog instead of a failure-prone secondary WebView.
2794
- fn windows_confirm_close_all() -> bool {
2795
- let message: Vec<u16> =
2796
- "Close all running fpasoterm windows?\n\nThis will terminate every fpasoterm window."
2797
- .encode_utf16()
2798
- .chain(std::iter::once(0))
2799
- .collect();
2800
- let title: Vec<u16> = "Confirm close all fpasoterm windows"
2801
- .encode_utf16()
2802
- .chain(std::iter::once(0))
2803
- .collect();
2804
- unsafe {
2805
- MessageBoxW(
2806
- ptr::null_mut(),
2807
- message.as_ptr(),
2808
- title.as_ptr(),
2809
- MB_OKCANCEL | MB_ICONWARNING | MB_SETFOREGROUND | MB_TASKMODAL | MB_TOPMOST,
2810
- ) == IDOK
2811
- }
2812
- }
2813
-
2814
- // Restores keyboard focus to the terminal after a confirmation window closes.
2815
- #[tauri::command]
2816
- fn window_focus_main(app: AppHandle) -> Result<(), String> {
2817
- let window = app
2818
- .get_webview_window("main")
2819
- .ok_or_else(|| "main window is not available".to_string())?;
2820
- window.set_focus().map_err(|error| error.to_string())
2821
- }
2822
-
2823
- // Closes the confirmation window and restores focus to the terminal atomically.
2824
- #[tauri::command]
2825
- fn window_cancel_close_all(app: AppHandle) -> Result<(), String> {
2826
- if let Some(window) = app.get_webview_window("close-all-confirm") {
2827
- let _ = window.close();
2828
- }
2829
- window_focus_main(app)
2830
- }
2831
-
2832
- // Broadcasts the close request from the independent confirmation window.
2727
+ // Broadcasts the close request after the in-window confirmation overlay.
2833
2728
  #[tauri::command]
2834
2729
  fn window_close_all_confirmed(
2835
- app: AppHandle,
2730
+ _app: AppHandle,
2836
2731
  unmount_sshfs: Option<bool>,
2837
2732
  ) -> Result<String, String> {
2838
2733
  if unmount_sshfs.unwrap_or(false) {
2839
2734
  sshfs_unmount_all()?;
2840
2735
  }
2841
2736
  broadcast_close_all_request()?;
2842
- if let Some(window) = app.get_webview_window("close-all-confirm") {
2843
- let _ = window.close();
2844
- }
2845
2737
  Ok("closed all fpasoterm windows".to_string())
2846
2738
  }
2847
2739
 
@@ -3065,6 +2957,7 @@ fn runtime_config() -> RuntimeConfig {
3065
2957
  };
3066
2958
  migrate_legacy_terminal_font_family(&mut config);
3067
2959
  migrate_legacy_terminal_line_height(&mut config);
2960
+ migrate_legacy_log_keybindings(&mut config);
3068
2961
  apply_direct_cli_overrides(&mut config);
3069
2962
  config
3070
2963
  }
@@ -3163,6 +3056,41 @@ fn migrate_legacy_terminal_line_height(runtime: &mut RuntimeConfig) {
3163
3056
  }
3164
3057
  }
3165
3058
 
3059
+ // Reserves Ctrl+Shift+p for a future command palette and normalizes former
3060
+ // single-letter defaults. Full user-defined shortcuts are left unchanged.
3061
+ fn migrate_legacy_log_keybindings(runtime: &mut RuntimeConfig) {
3062
+ let Some(keybindings) = runtime.config.keybindings.as_object_mut() else {
3063
+ return;
3064
+ };
3065
+ for (name, legacy, replacement) in [
3066
+ ("logShow", "P", "l"),
3067
+ ("logToggle", "S", "s"),
3068
+ ("copy", "C", "c"),
3069
+ ("paste", "V", "v"),
3070
+ ("menu", "M", "m"),
3071
+ ("help", "H", "h"),
3072
+ ("newWindow", "N", "n"),
3073
+ ("broadcast", "B", "b"),
3074
+ ("kill", "K", "k"),
3075
+ ("tile", "T", "t"),
3076
+ ("closeAll", "X", "x"),
3077
+ ] {
3078
+ if keybindings.get(name).and_then(serde_json::Value::as_str) == Some(legacy) {
3079
+ keybindings.insert(
3080
+ name.to_string(),
3081
+ serde_json::Value::String(replacement.to_string()),
3082
+ );
3083
+ }
3084
+ }
3085
+ if keybindings
3086
+ .get("logMenu")
3087
+ .and_then(serde_json::Value::as_str)
3088
+ == Some("L")
3089
+ {
3090
+ keybindings.remove("logMenu");
3091
+ }
3092
+ }
3093
+
3166
3094
  // Keeps the platform rule testable from non-macOS CI hosts.
3167
3095
  fn terminal_font_size_for(platform: &str, architecture: &str) -> u32 {
3168
3096
  if platform == "macos" && architecture == "x86_64" {
@@ -3276,6 +3204,7 @@ fn merge_runtime_config_from_path(
3276
3204
  config.config = serde_json::from_value(config_value).map_err(|error| error.to_string())?;
3277
3205
  migrate_legacy_terminal_font_family(&mut config);
3278
3206
  migrate_legacy_terminal_line_height(&mut config);
3207
+ migrate_legacy_log_keybindings(&mut config);
3279
3208
  config.config_path = absolute_path.to_string_lossy().to_string();
3280
3209
  config.config_dir = absolute_path
3281
3210
  .parent()
@@ -3466,19 +3395,18 @@ fn default_runtime_config() -> RuntimeConfig {
3466
3395
  }),
3467
3396
  keybindings: serde_json::json!({
3468
3397
  "prefix": "Mod+Shift",
3469
- "logMenu": "L",
3470
- "logToggle": "S",
3471
- "logShow": "P",
3472
- "copy": "C",
3473
- "paste": "V",
3474
- "menu": "M",
3475
- "help": "H",
3476
- "newWindow": "N",
3398
+ "logToggle": "s",
3399
+ "logShow": "l",
3400
+ "copy": "c",
3401
+ "paste": "v",
3402
+ "menu": "m",
3403
+ "help": "h",
3404
+ "newWindow": "n",
3477
3405
  "openCwd": "o",
3478
- "broadcast": "B",
3479
- "kill": "K",
3480
- "tile": "T",
3481
- "closeAll": "X"
3406
+ "broadcast": "b",
3407
+ "kill": "k",
3408
+ "tile": "t",
3409
+ "closeAll": "x"
3482
3410
  }),
3483
3411
  plugins: serde_json::json!({ "enabled": [] }),
3484
3412
  sync: serde_json::json!({
@@ -6410,8 +6338,8 @@ fn terminal_broadcast(
6410
6338
  request: TerminalBroadcastRequest,
6411
6339
  ) -> Result<TerminalBroadcastStatus, String> {
6412
6340
  let text = request.text;
6413
- if text.is_empty() {
6414
- return Err("broadcast text is empty".to_string());
6341
+ if text.is_empty() && request.key_event.is_none() {
6342
+ return Err("broadcast text and key event are empty".to_string());
6415
6343
  }
6416
6344
  let max_bytes = sync_max_bytes();
6417
6345
  if text.len() > max_bytes {
@@ -9206,13 +9134,50 @@ installPath = "appearance/other.ts"
9206
9134
  );
9207
9135
  assert!(config["terminal"].get("images").is_none());
9208
9136
  assert_eq!(config["keybindings"]["prefix"].as_str(), Some("Mod+Shift"));
9209
- assert_eq!(config["keybindings"]["newWindow"].as_str(), Some("N"));
9137
+ assert_eq!(config["keybindings"]["logShow"].as_str(), Some("l"));
9138
+ assert_eq!(config["keybindings"]["logToggle"].as_str(), Some("s"));
9139
+ assert!(config["keybindings"].get("logMenu").is_none());
9140
+ assert_eq!(config["keybindings"]["newWindow"].as_str(), Some("n"));
9210
9141
  assert_eq!(config["sync"]["enabled"].as_bool(), Some(false));
9211
9142
  assert_eq!(config["sync"]["commands"].as_bool(), Some(false));
9212
9143
  assert_eq!(config["sync"]["commandSecret"].as_str(), Some(""));
9213
9144
  assert_eq!(config["logging"]["enabled"].as_bool(), Some(true));
9214
9145
  }
9215
9146
 
9147
+ #[test]
9148
+ fn legacy_keybindings_use_lowercase_defaults() {
9149
+ let mut runtime = default_runtime_config();
9150
+ runtime.config.keybindings = serde_json::json!({
9151
+ "logMenu": "L",
9152
+ "logShow": "P",
9153
+ "logToggle": "S",
9154
+ "copy": "C",
9155
+ "paste": "V",
9156
+ "menu": "M",
9157
+ "help": "H",
9158
+ "newWindow": "N",
9159
+ "broadcast": "B",
9160
+ "kill": "K",
9161
+ "tile": "T",
9162
+ "closeAll": "X"
9163
+ });
9164
+
9165
+ migrate_legacy_log_keybindings(&mut runtime);
9166
+
9167
+ assert_eq!(runtime.config.keybindings["logShow"].as_str(), Some("l"));
9168
+ assert_eq!(runtime.config.keybindings["logToggle"].as_str(), Some("s"));
9169
+ assert_eq!(runtime.config.keybindings["copy"].as_str(), Some("c"));
9170
+ assert_eq!(runtime.config.keybindings["paste"].as_str(), Some("v"));
9171
+ assert_eq!(runtime.config.keybindings["menu"].as_str(), Some("m"));
9172
+ assert_eq!(runtime.config.keybindings["help"].as_str(), Some("h"));
9173
+ assert_eq!(runtime.config.keybindings["newWindow"].as_str(), Some("n"));
9174
+ assert_eq!(runtime.config.keybindings["broadcast"].as_str(), Some("b"));
9175
+ assert_eq!(runtime.config.keybindings["kill"].as_str(), Some("k"));
9176
+ assert_eq!(runtime.config.keybindings["tile"].as_str(), Some("t"));
9177
+ assert_eq!(runtime.config.keybindings["closeAll"].as_str(), Some("x"));
9178
+ assert!(runtime.config.keybindings.get("logMenu").is_none());
9179
+ }
9180
+
9216
9181
  #[test]
9217
9182
  fn redacted_runtime_config_hides_sync_command_key() {
9218
9183
  let mut runtime = default_runtime_config();
@@ -1,71 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Confirm</title>
7
- <style>
8
- :root { color-scheme: dark; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
9
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #171b21; color: #e8edf2; }
10
- main { width: min(500px, calc(100vw - 48px)); }
11
- h1 { margin: 0 0 14px; color: #f5d76e; font-size: 17px; }
12
- p { margin: 0 0 24px; white-space: pre-wrap; font-size: 14px; line-height: 1.5; }
13
- footer { display: flex; justify-content: flex-end; gap: 10px; }
14
- button { min-width: 110px; height: 38px; border: 1px solid #3b4652; border-radius: 4px; background: #222932; color: #e8edf2; font: inherit; font-size: 14px; }
15
- button:focus-visible { outline: 2px solid #f5d76e; outline-offset: 2px; }
16
- </style>
17
- </head>
18
- <body>
19
- <main role="dialog" aria-modal="true" aria-labelledby="title">
20
- <h1 id="title">Close all fpasoterm windows?</h1>
21
- <p id="message">This will terminate every running fpasoterm window.</p>
22
- <footer>
23
- <button id="cancel" type="button">Cancel</button>
24
- <button id="unmount" type="button" hidden>Unmount & Close</button>
25
- <button id="ok" type="button">OK</button>
26
- </footer>
27
- </main>
28
- <script>
29
- const invoke = window.__TAURI__.core.invoke;
30
- const currentWindow = window.__TAURI__.window.getCurrentWindow();
31
- const ok = document.getElementById('ok');
32
- const cancel = document.getElementById('cancel');
33
- const unmount = document.getElementById('unmount');
34
- const message = document.getElementById('message');
35
- invoke('sshfs_status').then((status) => {
36
- if (status.mounts?.length) {
37
- message.textContent += `\n\n${status.mounts.length} SSHFS mount(s) will remain active. Reopen FpasoTerm and use Window > SSHFS Mounts to unmount them.`;
38
- unmount.hidden = false;
39
- }
40
- }).catch(() => {});
41
- const focusConfirmWindow = () => {
42
- currentWindow.setFocus().catch(() => {});
43
- cancel.focus({ preventScroll: true });
44
- };
45
- focusConfirmWindow();
46
- window.addEventListener('load', focusConfirmWindow);
47
- setTimeout(focusConfirmWindow, 80);
48
- const focusMainAndClose = () => invoke('window_cancel_close_all').catch(() => currentWindow.close().catch(() => {}));
49
- const closeConfirmWindow = () => currentWindow.close().catch(() => {});
50
- ok.addEventListener('click', () => invoke('window_close_all_confirmed', { unmountSshfs: false }).catch(closeConfirmWindow));
51
- unmount.addEventListener('click', () => invoke('window_close_all_confirmed', { unmountSshfs: true }).catch(closeConfirmWindow));
52
- cancel.addEventListener('click', focusMainAndClose);
53
- window.addEventListener('keydown', (event) => {
54
- if (event.key === 'Tab') {
55
- event.preventDefault();
56
- (document.activeElement === cancel ? ok : cancel).focus();
57
- return;
58
- }
59
- if (event.key === 'Enter' && document.activeElement instanceof HTMLButtonElement) {
60
- event.preventDefault();
61
- document.activeElement.click();
62
- return;
63
- }
64
- if (event.key === 'Escape') {
65
- event.preventDefault();
66
- focusMainAndClose();
67
- }
68
- });
69
- </script>
70
- </body>
71
- </html>