batplot 1.7.28__py3-none-any.whl → 1.8.0__py3-none-any.whl

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 batplot might be problematic. Click here for more details.

@@ -18,6 +18,7 @@ from typing import Tuple, Dict, Optional, Any
18
18
  import json
19
19
  import os
20
20
  import time
21
+ import sys
21
22
 
22
23
  import matplotlib.pyplot as plt
23
24
  from matplotlib.colors import LinearSegmentedColormap
@@ -41,6 +42,37 @@ from .color_utils import (
41
42
  from .utils import choose_style_file
42
43
 
43
44
 
45
+ class _FilterIMKWarning:
46
+ """Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
47
+ def __init__(self, original_stderr):
48
+ self.original_stderr = original_stderr
49
+
50
+ def write(self, message):
51
+ # Filter out the harmless macOS IMK warning
52
+ if 'IMKCFRunLoopWakeUpReliable' not in message:
53
+ self.original_stderr.write(message)
54
+
55
+ def flush(self):
56
+ self.original_stderr.flush()
57
+
58
+
59
+ def _safe_input(prompt: str = "") -> str:
60
+ """Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
61
+
62
+ This is a harmless macOS system message that appears when using input() in terminals.
63
+ """
64
+ # Filter stderr to hide macOS IMK warnings while preserving other errors
65
+ original_stderr = sys.stderr
66
+ sys.stderr = _FilterIMKWarning(original_stderr)
67
+ try:
68
+ result = input(prompt)
69
+ return result
70
+ except (KeyboardInterrupt, EOFError):
71
+ raise
72
+ finally:
73
+ sys.stderr = original_stderr
74
+
75
+
44
76
  def _axis_tick_width(axis_obj, which: str = 'major'):
45
77
  """Return tick line width from axis tick params or rc defaults."""
46
78
  try:
@@ -363,7 +395,7 @@ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
363
395
  if cax is not None and getattr(cax, '_bp_detached_mpl_colorbar', False):
364
396
  return
365
397
 
366
- # Matplotlib's Colorbar stores the callback id as `_cid` (most versions).
398
+ # APPROACH 1: Try to find and disconnect the callback ID
367
399
  cid = None
368
400
  for attr in ('_cid', '_cid_colorbar', 'cid'):
369
401
  try:
@@ -374,7 +406,6 @@ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
374
406
  except Exception:
375
407
  pass
376
408
 
377
- # Disconnect from the ScalarMappable callback registry.
378
409
  if cid is not None:
379
410
  try:
380
411
  cbreg = getattr(im, 'callbacksSM', None)
@@ -383,7 +414,38 @@ def _detach_mpl_colorbar_callbacks(cbar, im) -> None:
383
414
  except Exception:
384
415
  pass
385
416
 
386
- # Prevent future built-in updates (best-effort; safe for mock colorbars too).
417
+ # APPROACH 2: Disconnect ALL callbacks from the image (nuclear option)
418
+ try:
419
+ cbreg = getattr(im, 'callbacksSM', None)
420
+ if cbreg is not None:
421
+ # Try to clear all callbacks
422
+ if hasattr(cbreg, 'callbacks'):
423
+ try:
424
+ cbreg.callbacks.clear()
425
+ except Exception:
426
+ pass
427
+ # Also try the _signals dict if it exists
428
+ if hasattr(cbreg, '_signals'):
429
+ try:
430
+ for signal_dict in cbreg._signals.values():
431
+ if hasattr(signal_dict, 'clear'):
432
+ signal_dict.clear()
433
+ except Exception:
434
+ pass
435
+ except Exception:
436
+ pass
437
+
438
+ # APPROACH 3: Monkey-patch the update_normal method to be a no-op
439
+ # This is the most reliable approach for preventing the callback
440
+ try:
441
+ if hasattr(cbar, 'update_normal'):
442
+ def _noop_update(*args, **kwargs):
443
+ pass
444
+ cbar.update_normal = _noop_update
445
+ except Exception:
446
+ pass
447
+
448
+ # APPROACH 4: Prevent future built-in updates by nulling internal state
387
449
  try:
388
450
  if hasattr(cbar, 'mappable'):
389
451
  cbar.mappable = None
@@ -1733,7 +1795,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1733
1795
  print("Crosshair OFF.")
1734
1796
  while True:
1735
1797
  try:
1736
- cmd = input("Press a key: ").strip().lower()
1798
+ cmd = _safe_input("Press a key: ").strip().lower()
1737
1799
  except (KeyboardInterrupt, EOFError):
1738
1800
  print("\n\nExiting interactive menu...")
1739
1801
  break
@@ -1741,7 +1803,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1741
1803
  continue
1742
1804
  if cmd == 'q':
1743
1805
  try:
1744
- ans = input("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ").strip().lower()
1806
+ ans = _safe_input("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ").strip().lower()
1745
1807
  except Exception:
1746
1808
  ans = 'y'
1747
1809
  if ans == 'y':
@@ -1784,9 +1846,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1784
1846
 
1785
1847
  last_figure_path = getattr(fig, '_last_figure_export_path', None)
1786
1848
  if last_figure_path:
1787
- fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
1849
+ fname = _safe_input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
1788
1850
  else:
1789
- fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1851
+ fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1790
1852
  if not fname or fname.lower() == 'q':
1791
1853
  print_menu(); continue
1792
1854
 
@@ -1799,7 +1861,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1799
1861
  if not os.path.exists(last_figure_path):
1800
1862
  print(f"Previous export file not found: {last_figure_path}")
1801
1863
  print_menu(); continue
1802
- yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1864
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1803
1865
  if yn != 'y':
1804
1866
  print_menu(); continue
1805
1867
  target = last_figure_path
@@ -1810,7 +1872,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1810
1872
  idx = int(fname)
1811
1873
  if 1 <= idx <= len(files):
1812
1874
  name = files[idx-1]
1813
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1875
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1814
1876
  if yn != 'y':
1815
1877
  print_menu(); continue
1816
1878
  target = file_list[idx-1][1] # Full path from list
@@ -1889,7 +1951,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1889
1951
  cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
1890
1952
  ec_h_offset = getattr(ec_ax, '_ec_h_offset_in', 0.0)
1891
1953
  print(f"Toggle: 1=colorbar, 2=EC panel, 3=both, 4=colorbar label mode, 5=colorbar label text, m=move horizontal position (cb:{cb_h_offset:.3f}\", ec:{ec_h_offset:.3f}\"), q=cancel")
1892
- choice = input("v> ").strip().lower()
1954
+ choice = _safe_input("v> ").strip().lower()
1893
1955
  if choice == '1':
1894
1956
  # Toggle colorbar
1895
1957
  cb_vis = cbar.ax.get_visible()
@@ -1923,7 +1985,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1923
1985
  # Change colorbar label text
1924
1986
  current_label = getattr(cbar.ax, '_colorbar_label', 'Intensity')
1925
1987
  print(f"Current colorbar label: {current_label}")
1926
- new_label = input("New colorbar label (blank to keep): ").strip()
1988
+ new_label = _safe_input("New colorbar label (blank to keep): ").strip()
1927
1989
  if new_label:
1928
1990
  cbar.ax._colorbar_label = new_label
1929
1991
  try:
@@ -1944,12 +2006,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1944
2006
  if ec_ax is not None:
1945
2007
  print(f" EC panel offset: {ec_h_offset:.3f}\" (positive=right, negative=left)")
1946
2008
  print("Commands: c=colorbar, e=EC panel, q=back")
1947
- sub = input("m> ").strip().lower()
2009
+ sub = _safe_input("m> ").strip().lower()
1948
2010
  if not sub or sub == 'q':
1949
2011
  break
1950
2012
  if sub == 'c':
1951
2013
  try:
1952
- new_offset = input(f"Enter colorbar horizontal offset in inches (current: {cb_h_offset:.3f}\"): ").strip()
2014
+ new_offset = _safe_input(f"Enter colorbar horizontal offset in inches (current: {cb_h_offset:.3f}\"): ").strip()
1953
2015
  if new_offset:
1954
2016
  cb_h_offset = float(new_offset)
1955
2017
  setattr(cbar.ax, '_cb_h_offset_in', cb_h_offset)
@@ -1957,6 +2019,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1957
2019
  _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1958
2020
  fig.canvas.draw_idle()
1959
2021
  print(f"Colorbar horizontal offset set to {cb_h_offset:.3f}\"")
2022
+ # Continue in loop to show menu again
2023
+ continue
1960
2024
  except ValueError:
1961
2025
  print("Invalid input. Enter a number.")
1962
2026
  except Exception as e:
@@ -1966,7 +2030,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1966
2030
  print("EC panel not available.")
1967
2031
  continue
1968
2032
  try:
1969
- new_offset = input(f"Enter EC panel horizontal offset in inches (current: {ec_h_offset:.3f}\"): ").strip()
2033
+ new_offset = _safe_input(f"Enter EC panel horizontal offset in inches (current: {ec_h_offset:.3f}\"): ").strip()
1970
2034
  if new_offset:
1971
2035
  ec_h_offset = float(new_offset)
1972
2036
  setattr(ec_ax, '_ec_h_offset_in', ec_h_offset)
@@ -1974,6 +2038,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1974
2038
  _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
1975
2039
  fig.canvas.draw_idle()
1976
2040
  print(f"EC panel horizontal offset set to {ec_h_offset:.3f}\"")
2041
+ # Continue in loop to show menu again
2042
+ continue
1977
2043
  except ValueError:
1978
2044
  print("Invalid input. Enter a number.")
1979
2045
  except Exception as e:
@@ -1986,7 +2052,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
1986
2052
  # Operando-only mode: toggle colorbar or change label mode
1987
2053
  cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
1988
2054
  print(f"Toggle: 1=colorbar visibility, 2=colorbar label mode, 3=colorbar label text, m=move horizontal position (cb:{cb_h_offset:.3f}\"), q=cancel")
1989
- choice = input("v> ").strip().lower()
2055
+ choice = _safe_input("v> ").strip().lower()
1990
2056
  if choice == '1':
1991
2057
  cb_vis = cbar.ax.get_visible()
1992
2058
  cbar.ax.set_visible(not cb_vis)
@@ -2006,7 +2072,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2006
2072
  # Change colorbar label text
2007
2073
  current_label = getattr(cbar.ax, '_colorbar_label', 'Intensity')
2008
2074
  print(f"Current colorbar label: {current_label}")
2009
- new_label = input("New colorbar label (blank to keep): ").strip()
2075
+ new_label = _safe_input("New colorbar label (blank to keep): ").strip()
2010
2076
  if new_label:
2011
2077
  cbar.ax._colorbar_label = new_label
2012
2078
  try:
@@ -2024,12 +2090,12 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2024
2090
  print(f"\nHorizontal position (relative to canvas center):")
2025
2091
  print(f" Colorbar offset: {cb_h_offset:.3f}\" (positive=right, negative=left)")
2026
2092
  print("Commands: c=colorbar, q=back")
2027
- sub = input("m> ").strip().lower()
2093
+ sub = _safe_input("m> ").strip().lower()
2028
2094
  if not sub or sub == 'q':
2029
2095
  break
2030
2096
  if sub == 'c':
2031
2097
  try:
2032
- new_offset = input(f"Enter colorbar horizontal offset in inches (current: {cb_h_offset:.3f}\"): ").strip()
2098
+ new_offset = _safe_input(f"Enter colorbar horizontal offset in inches (current: {cb_h_offset:.3f}\"): ").strip()
2033
2099
  if new_offset:
2034
2100
  cb_h_offset = float(new_offset)
2035
2101
  setattr(cbar.ax, '_cb_h_offset_in', cb_h_offset)
@@ -2037,6 +2103,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2037
2103
  _apply_group_layout_inches(fig, ax, cbar.ax, ec_ax, ax_w_in, ax_h_in, cb_w_in, cb_gap_in, ec_gap_in, ec_w_in)
2038
2104
  fig.canvas.draw_idle()
2039
2105
  print(f"Colorbar horizontal offset set to {cb_h_offset:.3f}\"")
2106
+ # Continue in loop to show menu again
2107
+ continue
2040
2108
  except ValueError:
2041
2109
  print("Invalid input. Enter a number.")
2042
2110
  except Exception as e:
@@ -2079,7 +2147,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2079
2147
  prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
2080
2148
  else:
2081
2149
  prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
2082
- choice = input(prompt).strip()
2150
+ choice = _safe_input(prompt).strip()
2083
2151
  if not choice or choice.lower() == 'q':
2084
2152
  print_menu(); continue
2085
2153
  if choice.lower() == 'o':
@@ -2090,7 +2158,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2090
2158
  if not os.path.exists(last_session_path):
2091
2159
  print(f"Previous save file not found: {last_session_path}")
2092
2160
  print_menu(); continue
2093
- yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
2161
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
2094
2162
  if yn != 'y':
2095
2163
  print_menu(); continue
2096
2164
  dump_operando_session(last_session_path, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
@@ -2100,7 +2168,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2100
2168
  idx = int(choice)
2101
2169
  if 1 <= idx <= len(files):
2102
2170
  name = files[idx-1]
2103
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
2171
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
2104
2172
  if yn != 'y':
2105
2173
  print_menu(); continue
2106
2174
  target = os.path.join(folder, name)
@@ -2117,7 +2185,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2117
2185
  name = name + '.pkl'
2118
2186
  target = name if os.path.isabs(name) else os.path.join(folder, name)
2119
2187
  if os.path.exists(target):
2120
- yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
2188
+ yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
2121
2189
  if yn != 'y':
2122
2190
  print_menu(); continue
2123
2191
  dump_operando_session(target, fig=fig, ax=ax, im=im, cbar=cbar, ec_ax=ec_ax, skip_confirm=True)
@@ -2142,7 +2210,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2142
2210
  # Always read fresh value from attribute to avoid stale cached value
2143
2211
  ax_h_in = getattr(ax, '_fixed_ax_h_in', ax_h_in)
2144
2212
  print(f"Current height: {ax_h_in:.2f} in")
2145
- val = input("New height (inches): ").strip()
2213
+ val = _safe_input("New height (inches): ").strip()
2146
2214
  if val:
2147
2215
  _snapshot("height")
2148
2216
  try:
@@ -2182,7 +2250,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2182
2250
  print(f"\nFont submenu (current: family='{cur_family}', size={cur_size})")
2183
2251
  print(" f: change family | s: change size | q: back")
2184
2252
  while True:
2185
- sub = input("Font> ").strip().lower()
2253
+ sub = _safe_input("Font> ").strip().lower()
2186
2254
  if not sub:
2187
2255
  continue
2188
2256
  if sub == 'q':
@@ -2195,7 +2263,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2195
2263
  for i, font in enumerate(fonts, 1):
2196
2264
  print(f" {i}: {font}")
2197
2265
  print("Or enter custom font name directly.")
2198
- choice = input("Font family (number or name): ").strip()
2266
+ choice = _safe_input("Font family (number or name): ").strip()
2199
2267
  if not choice:
2200
2268
  continue
2201
2269
  _snapshot("font-family")
@@ -2215,7 +2283,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2215
2283
  elif sub == 's':
2216
2284
  # Show current size and accept direct input
2217
2285
  cur_size = plt.rcParams.get('font.size', None)
2218
- choice = input(f"Font size (current: {cur_size}): ").strip()
2286
+ choice = _safe_input(f"Font size (current: {cur_size}): ").strip()
2219
2287
  if not choice:
2220
2288
  continue
2221
2289
  _snapshot("font-size")
@@ -2238,7 +2306,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2238
2306
  print(_colorize_inline_commands(" 1.5 2.5 - set frame=1.5, ticks=2.5"))
2239
2307
  print(_colorize_inline_commands(" q - cancel"))
2240
2308
 
2241
- inp = input("Line widths> ").strip().lower()
2309
+ inp = _safe_input("Line widths> ").strip().lower()
2242
2310
  if not inp or inp == 'q':
2243
2311
  print_menu()
2244
2312
  continue
@@ -2301,6 +2369,8 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2301
2369
  # Unified WASD ticks/labels/spines submenu for either pane
2302
2370
  # Import here to avoid scoping issues with nested functions
2303
2371
  from matplotlib.ticker import AutoMinorLocator, NullFormatter, MaxNLocator, NullLocator
2372
+ # Import UI positioning functions locally to ensure they're accessible in nested functions
2373
+ from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
2304
2374
 
2305
2375
  def _get_tick_state(a):
2306
2376
  # Unified keys with fallbacks for legacy combined flags
@@ -2386,7 +2456,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2386
2456
  print(_colorize_inline_commands("Choose pane: o=operando, e=ec, q=back"))
2387
2457
  else:
2388
2458
  print(_colorize_inline_commands("Choose pane: o=operando, q=back"))
2389
- pane = input("ot> ").strip().lower()
2459
+ pane = _safe_input("ot> ").strip().lower()
2390
2460
  if not pane:
2391
2461
  continue
2392
2462
  if pane == 'q':
@@ -2605,7 +2675,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2605
2675
  print(_colorize_inline_commands("Type 'i' to invert tick direction, 'l' to change tick length, 'list' for state, 'q' to return."))
2606
2676
  print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
2607
2677
  while True:
2608
- cmd2 = input(_colorize_prompt("Toggle> ")).strip().lower()
2678
+ cmd2 = _safe_input(_colorize_prompt("Toggle> ")).strip().lower()
2609
2679
  if not cmd2:
2610
2680
  continue
2611
2681
  if cmd2 == 'q':
@@ -2630,7 +2700,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2630
2700
  # Get current major tick length from axes
2631
2701
  current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
2632
2702
  print(f"Current major tick length: {current_major}")
2633
- new_length_str = input("Enter new major tick length (e.g., 6.0): ").strip()
2703
+ new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
2634
2704
  if not new_length_str:
2635
2705
  continue
2636
2706
  new_major = float(new_length_str)
@@ -2723,7 +2793,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2723
2793
  current_y_px = _px_value('_top_xlabel_manual_offset_y_pts', target)
2724
2794
  current_x_px = _px_value('_top_xlabel_manual_offset_x_pts', target)
2725
2795
  print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
2726
- sub = input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
2796
+ sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
2727
2797
  if not sub:
2728
2798
  continue
2729
2799
  if sub == 'q':
@@ -2760,7 +2830,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2760
2830
  while True:
2761
2831
  current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts', target)
2762
2832
  print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
2763
- sub = input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
2833
+ sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
2764
2834
  if not sub:
2765
2835
  continue
2766
2836
  if sub == 'q':
@@ -2790,7 +2860,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2790
2860
  while True:
2791
2861
  current_x_px = _px_value('_left_ylabel_manual_offset_x_pts', target)
2792
2862
  print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
2793
- sub = input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
2863
+ sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
2794
2864
  if not sub:
2795
2865
  continue
2796
2866
  if sub == 'q':
@@ -2827,7 +2897,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2827
2897
  current_x_px = _px_value('_right_ylabel_manual_offset_x_pts', target)
2828
2898
  current_y_px = _px_value('_right_ylabel_manual_offset_y_pts', target)
2829
2899
  print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
2830
- sub = input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
2900
+ sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
2831
2901
  if not sub:
2832
2902
  continue
2833
2903
  if sub == 'q':
@@ -2866,7 +2936,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2866
2936
  print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
2867
2937
  print(" " + _colorize_menu('r : reset all offsets'))
2868
2938
  print(" " + _colorize_menu('q : return'))
2869
- choice = input(_colorize_prompt("p> ")).strip().lower()
2939
+ choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
2870
2940
  if not choice:
2871
2941
  continue
2872
2942
  if choice == 'q':
@@ -2982,7 +3052,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2982
3052
  elif cmd == 'ox':
2983
3053
  while True:
2984
3054
  cur = ax.get_xlim(); print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
2985
- line = input("New X range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3055
+ line = _safe_input("New X range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
2986
3056
  if not line or line.lower() == 'q':
2987
3057
  break
2988
3058
  if line.lower() == 'w':
@@ -2990,7 +3060,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
2990
3060
  while True:
2991
3061
  cur = ax.get_xlim()
2992
3062
  print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
2993
- val = input(f"Enter new upper X limit (current lower: {cur[0]:.4g}, q=back): ").strip()
3063
+ val = _safe_input(f"Enter new upper X limit (current lower: {cur[0]:.4g}, q=back): ").strip()
2994
3064
  if not val or val.lower() == 'q':
2995
3065
  break
2996
3066
  try:
@@ -3009,7 +3079,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3009
3079
  while True:
3010
3080
  cur = ax.get_xlim()
3011
3081
  print(f"Current operando X: {cur[0]:.4g} {cur[1]:.4g}")
3012
- val = input(f"Enter new lower X limit (current upper: {cur[1]:.4g}, q=back): ").strip()
3082
+ val = _safe_input(f"Enter new lower X limit (current upper: {cur[1]:.4g}, q=back): ").strip()
3013
3083
  if not val or val.lower() == 'q':
3014
3084
  break
3015
3085
  try:
@@ -3055,7 +3125,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3055
3125
  elif cmd == 'oy':
3056
3126
  while True:
3057
3127
  cur = ax.get_ylim(); print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
3058
- line = input("New Y range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3128
+ line = _safe_input("New Y range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3059
3129
  if not line or line.lower() == 'q':
3060
3130
  break
3061
3131
  if line.lower() == 'w':
@@ -3063,7 +3133,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3063
3133
  while True:
3064
3134
  cur = ax.get_ylim()
3065
3135
  print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
3066
- val = input(f"Enter new upper Y limit (current lower: {cur[0]:.4g}, q=back): ").strip()
3136
+ val = _safe_input(f"Enter new upper Y limit (current lower: {cur[0]:.4g}, q=back): ").strip()
3067
3137
  if not val or val.lower() == 'q':
3068
3138
  break
3069
3139
  try:
@@ -3082,7 +3152,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3082
3152
  while True:
3083
3153
  cur = ax.get_ylim()
3084
3154
  print(f"Current operando Y: {cur[0]:.4g} {cur[1]:.4g}")
3085
- val = input(f"Enter new lower Y limit (current upper: {cur[1]:.4g}, q=back): ").strip()
3155
+ val = _safe_input(f"Enter new lower Y limit (current upper: {cur[1]:.4g}, q=back): ").strip()
3086
3156
  if not val or val.lower() == 'q':
3087
3157
  break
3088
3158
  try:
@@ -3185,9 +3255,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3185
3255
  auto_available = False
3186
3256
 
3187
3257
  if auto_available:
3188
- line = input("New intensity range (min max, w=upper only, s=lower only, a=auto-fit to visible, q=back): ").strip()
3258
+ line = _safe_input("New intensity range (min max, w=upper only, s=lower only, a=auto-fit to visible, q=back): ").strip()
3189
3259
  else:
3190
- line = input("New intensity range (min max, w=upper only, s=lower only, q=back): ").strip()
3260
+ line = _safe_input("New intensity range (min max, w=upper only, s=lower only, q=back): ").strip()
3191
3261
 
3192
3262
  if not line or line.lower() == 'q':
3193
3263
  break
@@ -3201,7 +3271,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3201
3271
  except Exception:
3202
3272
  print("Could not retrieve current color scale range")
3203
3273
  break
3204
- val = input(f"Enter new upper intensity limit (current lower: {cur[0]:.4g}, q=back): ").strip()
3274
+ val = _safe_input(f"Enter new upper intensity limit (current lower: {cur[0]:.4g}, q=back): ").strip()
3205
3275
  if not val or val.lower() == 'q':
3206
3276
  break
3207
3277
  try:
@@ -3227,7 +3297,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3227
3297
  except Exception:
3228
3298
  print("Could not retrieve current color scale range")
3229
3299
  break
3230
- val = input(f"Enter new lower intensity limit (current upper: {cur[1]:.4g}, q=back): ").strip()
3300
+ val = _safe_input(f"Enter new lower intensity limit (current upper: {cur[1]:.4g}, q=back): ").strip()
3231
3301
  if not val or val.lower() == 'q':
3232
3302
  break
3233
3303
  try:
@@ -3278,7 +3348,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3278
3348
  while True:
3279
3349
  ax_w_in = getattr(ax, '_fixed_ax_w_in', ax_w_in)
3280
3350
  print(f"Current operando width: {ax_w_in:.2f} in")
3281
- val = input("New width (inches, q=back): ").strip()
3351
+ val = _safe_input("New width (inches, q=back): ").strip()
3282
3352
  if not val or val.lower() == 'q':
3283
3353
  break
3284
3354
  _snapshot("operando-width")
@@ -3298,7 +3368,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3298
3368
  while True:
3299
3369
  ec_w_in = getattr(ec_ax, '_fixed_ec_w_in', ec_w_in)
3300
3370
  print(f"Current EC width: {ec_w_in:.2f} in")
3301
- val = input("New EC width (inches, q=back): ").strip()
3371
+ val = _safe_input("New EC width (inches, q=back): ").strip()
3302
3372
  if not val or val.lower() == 'q':
3303
3373
  break
3304
3374
  _snapshot("ec-width")
@@ -3341,7 +3411,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3341
3411
  if optional:
3342
3412
  print("\nOther available: " + ", ".join(optional))
3343
3413
  print(_colorize_inline_commands("Append _r to reverse (e.g., viridis_r or 1_r). Blank to cancel."))
3344
- choice = input(f"Palette name or number (1-{len(rec_palettes)}): ").strip()
3414
+ choice = _safe_input(f"Palette name or number (1-{len(rec_palettes)}): ").strip()
3345
3415
  if not choice:
3346
3416
  print_menu(); continue
3347
3417
  try:
@@ -3590,7 +3660,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3590
3660
  last_style_path = getattr(fig, '_last_style_export_path', None)
3591
3661
  if ec_ax is None:
3592
3662
  print("\nNote: Style export (.bps/.bpsg) is only available in dual-pane mode (with EC file).")
3593
- sub = input("Style submenu: (q=return, r=refresh): ").strip().lower()
3663
+ sub = _safe_input("Style submenu: (q=return, r=refresh): ").strip().lower()
3594
3664
  if sub == 'q':
3595
3665
  break
3596
3666
  if sub == 'r' or sub == '':
@@ -3600,9 +3670,9 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3600
3670
  continue
3601
3671
  else:
3602
3672
  if last_style_path:
3603
- sub = input("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ").strip().lower()
3673
+ sub = _safe_input("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ").strip().lower()
3604
3674
  else:
3605
- sub = input("Style submenu: (e=export, q=return, r=refresh): ").strip().lower()
3675
+ sub = _safe_input("Style submenu: (e=export, q=return, r=refresh): ").strip().lower()
3606
3676
  if sub == 'q':
3607
3677
  break
3608
3678
  if sub == 'r' or sub == '':
@@ -3615,7 +3685,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3615
3685
  if not os.path.exists(last_style_path):
3616
3686
  print(f"Previous export file not found: {last_style_path}")
3617
3687
  continue
3618
- yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3688
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3619
3689
  if yn != 'y':
3620
3690
  continue
3621
3691
  # Determine export type from existing file and rebuild config
@@ -3636,7 +3706,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3636
3706
  print("Export options:")
3637
3707
  print(" ps = style only (.bps)")
3638
3708
  print(" psg = style + geometry (.bpsg)")
3639
- exp_choice = input("Export choice (ps/psg, q=cancel): ").strip().lower()
3709
+ exp_choice = _safe_input("Export choice (ps/psg, q=cancel): ").strip().lower()
3640
3710
  if not exp_choice or exp_choice == 'q':
3641
3711
  print("Style export canceled.")
3642
3712
  continue
@@ -3830,7 +3900,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3830
3900
  else:
3831
3901
  print(f" {_i}: {fname}")
3832
3902
 
3833
- choice_name = input("Enter new filename or number to overwrite (q=cancel): ").strip()
3903
+ choice_name = _safe_input("Enter new filename or number to overwrite (q=cancel): ").strip()
3834
3904
  if not choice_name or choice_name.lower() == 'q':
3835
3905
  print("Style export canceled.")
3836
3906
  continue
@@ -3839,7 +3909,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3839
3909
  _idx = int(choice_name)
3840
3910
  if 1 <= _idx <= len(_style_files):
3841
3911
  name = _style_files[_idx-1]
3842
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3912
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3843
3913
  if yn == 'y':
3844
3914
  target = file_list[_idx-1][1] # Full path from list
3845
3915
  else:
@@ -3856,7 +3926,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
3856
3926
  else:
3857
3927
  target = get_organized_path(name, 'style', base_path=save_base)
3858
3928
  if os.path.exists(target):
3859
- yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
3929
+ yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
3860
3930
  if yn != 'y':
3861
3931
  target = None
3862
3932
  if target:
@@ -4554,14 +4624,14 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4554
4624
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
4555
4625
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
4556
4626
  while True:
4557
- sub = input("or> ").strip().lower()
4627
+ sub = _safe_input("or> ").strip().lower()
4558
4628
  if not sub:
4559
4629
  continue
4560
4630
  if sub == 'q':
4561
4631
  break
4562
4632
  if sub == 'x':
4563
4633
  cur = ax.get_xlabel() or ''
4564
- lab = input(f"New operando X label (blank=cancel, current='{cur}'): ")
4634
+ lab = _safe_input(f"New operando X label (blank=cancel, current='{cur}'): ")
4565
4635
  if lab:
4566
4636
  _snapshot("rename-op-x")
4567
4637
  try:
@@ -4574,7 +4644,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4574
4644
  pass
4575
4645
  elif sub == 'y':
4576
4646
  cur = ax.get_ylabel() or ''
4577
- lab = input(f"New operando Y label (blank=cancel, current='{cur}'): ")
4647
+ lab = _safe_input(f"New operando Y label (blank=cancel, current='{cur}'): ")
4578
4648
  if lab:
4579
4649
  _snapshot("rename-op-y")
4580
4650
  try:
@@ -4606,14 +4676,14 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4606
4676
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
4607
4677
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
4608
4678
  while True:
4609
- sub = input("er> ").strip().lower()
4679
+ sub = _safe_input("er> ").strip().lower()
4610
4680
  if not sub:
4611
4681
  continue
4612
4682
  if sub == 'q':
4613
4683
  break
4614
4684
  if sub == 'x':
4615
4685
  cur = ec_ax.get_xlabel() or ''
4616
- lab = input(f"New EC X label (blank=cancel, current='{cur}'): ")
4686
+ lab = _safe_input(f"New EC X label (blank=cancel, current='{cur}'): ")
4617
4687
  if lab:
4618
4688
  _snapshot("rename-ec-x")
4619
4689
  try:
@@ -4625,7 +4695,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4625
4695
  pass
4626
4696
  elif sub == 'y':
4627
4697
  cur = ec_ax.get_ylabel() or ''
4628
- lab = input(f"New EC Y label (blank=cancel, current='{cur}'): ")
4698
+ lab = _safe_input(f"New EC Y label (blank=cancel, current='{cur}'): ")
4629
4699
  if lab:
4630
4700
  _snapshot("rename-ec-y")
4631
4701
  try:
@@ -4663,7 +4733,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4663
4733
  print_menu(); continue
4664
4734
  print("EC line submenu: c=color, l=linewidth, q=back")
4665
4735
  while True:
4666
- sub = input("el> ").strip().lower()
4736
+ sub = _safe_input("el> ").strip().lower()
4667
4737
  if not sub:
4668
4738
  continue
4669
4739
  if sub == 'q':
@@ -4679,7 +4749,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4679
4749
  else:
4680
4750
  print("\nNo saved colors. Type 'u' to manage saved colors.")
4681
4751
  print(" (Enter color name/hex, saved color number, or 'u' to manage)")
4682
- val = input(f"Color (current={cur}, blank=cancel): ").strip()
4752
+ val = _safe_input(f"Color (current={cur}, blank=cancel): ").strip()
4683
4753
  if not val:
4684
4754
  continue
4685
4755
  if val.lower() == 'u':
@@ -4696,7 +4766,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4696
4766
  print(f"Invalid color: {e}")
4697
4767
  elif sub == 'l':
4698
4768
  cur = ln.get_linewidth()
4699
- val = input(f"Line width (current={cur}, blank=cancel): ").strip()
4769
+ val = _safe_input(f"Line width (current={cur}, blank=cancel): ").strip()
4700
4770
  if not val:
4701
4771
  continue
4702
4772
  _snapshot("ec-line-width")
@@ -4721,7 +4791,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4721
4791
  continue
4722
4792
  while True:
4723
4793
  cur = ec_ax.get_ylim(); print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
4724
- line = input("New time range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
4794
+ line = _safe_input("New time range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
4725
4795
  if not line or line.lower() == 'q':
4726
4796
  break
4727
4797
  if line.lower() == 'w':
@@ -4729,7 +4799,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4729
4799
  while True:
4730
4800
  cur = ec_ax.get_ylim()
4731
4801
  print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
4732
- val = input(f"Enter new upper time limit (current lower: {cur[0]:.4g}, q=back): ").strip()
4802
+ val = _safe_input(f"Enter new upper time limit (current lower: {cur[0]:.4g}, q=back): ").strip()
4733
4803
  if not val or val.lower() == 'q':
4734
4804
  break
4735
4805
  try:
@@ -4747,7 +4817,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4747
4817
  while True:
4748
4818
  cur = ec_ax.get_ylim()
4749
4819
  print(f"Current EC time range (Y): {cur[0]:.4g} {cur[1]:.4g}")
4750
- val = input(f"Enter new lower time limit (current upper: {cur[1]:.4g}, q=back): ").strip()
4820
+ val = _safe_input(f"Enter new lower time limit (current upper: {cur[1]:.4g}, q=back): ").strip()
4751
4821
  if not val or val.lower() == 'q':
4752
4822
  break
4753
4823
  try:
@@ -4861,7 +4931,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4861
4931
  print("The .mpt file must contain the '<I>/mA' column to use this feature.")
4862
4932
  print_menu(); continue
4863
4933
  while True:
4864
- sub = input("ey submenu: n=ions, t=time, q=back: ").strip().lower()
4934
+ sub = _safe_input("ey submenu: n=ions, t=time, q=back: ").strip().lower()
4865
4935
  if not sub:
4866
4936
  continue
4867
4937
  if sub == 'q':
@@ -4878,7 +4948,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
4878
4948
  prompt = "Enter mass(mg), capacity-per-ion(mAh g^-1), start-ions (e.g. 4.5 26.8 0), q=cancel: "
4879
4949
  else:
4880
4950
  prompt = f"Enter mass,cap-per-ion,start-ions (blank=reuse {mass_mg} {cap_per_ion} {start_ions}; q=cancel): "
4881
- s = input(prompt).strip()
4951
+ s = _safe_input(prompt).strip()
4882
4952
  if not s:
4883
4953
  if need_input:
4884
4954
  continue
@@ -5164,7 +5234,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
5164
5234
  while True:
5165
5235
  cur = ec_ax.get_xlim()
5166
5236
  print(f"Current EC X range: {cur[0]:.4g} {cur[1]:.4g}")
5167
- line = input("New EC X range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
5237
+ line = _safe_input("New EC X range (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
5168
5238
  if not line or line.lower() == 'q':
5169
5239
  break
5170
5240
  if line.lower() == 'w':
@@ -5172,7 +5242,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
5172
5242
  while True:
5173
5243
  cur = ec_ax.get_xlim()
5174
5244
  print(f"Current EC X range: {cur[0]:.4g} {cur[1]:.4g}")
5175
- val = input(f"Enter new upper EC X limit (current lower: {cur[0]:.4g}, q=back): ").strip()
5245
+ val = _safe_input(f"Enter new upper EC X limit (current lower: {cur[0]:.4g}, q=back): ").strip()
5176
5246
  if not val or val.lower() == 'q':
5177
5247
  break
5178
5248
  try:
@@ -5191,7 +5261,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
5191
5261
  while True:
5192
5262
  cur = ec_ax.get_xlim()
5193
5263
  print(f"Current EC X range: {cur[0]:.4g} {cur[1]:.4g}")
5194
- val = input(f"Enter new lower EC X limit (current upper: {cur[1]:.4g}, q=back): ").strip()
5264
+ val = _safe_input(f"Enter new lower EC X limit (current upper: {cur[1]:.4g}, q=back): ").strip()
5195
5265
  if not val or val.lower() == 'q':
5196
5266
  break
5197
5267
  try:
@@ -5253,7 +5323,7 @@ def operando_ec_interactive_menu(fig, ax, im, cbar, ec_ax, file_paths=None):
5253
5323
  cur_w, cur_h = _get_fig_size(fig)
5254
5324
  print(f"Current canvas size: {cur_w:.2f} x {cur_h:.2f} in (W x H)")
5255
5325
  print("Canvas: only figure size will change; panel widths/gaps are not altered.")
5256
- line = input("New canvas size 'W H' (blank=cancel): ").strip()
5326
+ line = _safe_input("New canvas size 'W H' (blank=cancel): ").strip()
5257
5327
  if line:
5258
5328
  _snapshot("canvas-size")
5259
5329
  try: