batplot 1.7.28__py3-none-any.whl → 1.8.1__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.

batplot/interactive.py CHANGED
@@ -25,6 +25,7 @@ from .utils import (
25
25
  choose_save_path,
26
26
  choose_style_file,
27
27
  list_files_in_subdirectory,
28
+ convert_label_shortcuts,
28
29
  get_organized_path,
29
30
  )
30
31
  import time
@@ -58,6 +59,37 @@ from .color_utils import (
58
59
  )
59
60
 
60
61
 
62
+ class _FilterIMKWarning:
63
+ """Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
64
+ def __init__(self, original_stderr):
65
+ self.original_stderr = original_stderr
66
+
67
+ def write(self, message):
68
+ # Filter out the harmless macOS IMK warning
69
+ if 'IMKCFRunLoopWakeUpReliable' not in message:
70
+ self.original_stderr.write(message)
71
+
72
+ def flush(self):
73
+ self.original_stderr.flush()
74
+
75
+
76
+ def _safe_input(prompt: str = "") -> str:
77
+ """Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
78
+
79
+ This is a harmless macOS system message that appears when using input() in terminals.
80
+ """
81
+ # Filter stderr to hide macOS IMK warnings while preserving other errors
82
+ original_stderr = sys.stderr
83
+ sys.stderr = _FilterIMKWarning(original_stderr)
84
+ try:
85
+ result = input(prompt)
86
+ return result
87
+ except (KeyboardInterrupt, EOFError):
88
+ raise
89
+ finally:
90
+ sys.stderr = original_stderr
91
+
92
+
61
93
  def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
62
94
  label_text_objects, delta, x_label, args,
63
95
  x_full_list, raw_y_full_list, offsets_list,
@@ -363,7 +395,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
363
395
  current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
364
396
  current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
365
397
  print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
366
- sub = input(colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
398
+ sub = _safe_input(colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
367
399
  if not sub:
368
400
  continue
369
401
  if sub == 'q':
@@ -401,7 +433,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
401
433
  current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
402
434
  current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
403
435
  print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
404
- sub = input(colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
436
+ sub = _safe_input(colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
405
437
  if not sub:
406
438
  continue
407
439
  if sub == 'q':
@@ -438,7 +470,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
438
470
  while True:
439
471
  current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
440
472
  print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
441
- sub = input(colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
473
+ sub = _safe_input(colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
442
474
  if not sub:
443
475
  continue
444
476
  if sub == 'q':
@@ -468,7 +500,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
468
500
  while True:
469
501
  current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
470
502
  print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
471
- sub = input(colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
503
+ sub = _safe_input(colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
472
504
  if not sub:
473
505
  continue
474
506
  if sub == 'q':
@@ -499,7 +531,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
499
531
  print(" " + colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
500
532
  print(" " + colorize_menu('r : reset all offsets'))
501
533
  print(" " + colorize_menu('q : back to toggle menu'))
502
- choice = input(colorize_prompt("p> ")).strip().lower()
534
+ choice = _safe_input(colorize_prompt("p> ")).strip().lower()
503
535
  if not choice:
504
536
  continue
505
537
  if choice == 'q':
@@ -618,7 +650,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
618
650
 
619
651
  while True:
620
652
  render()
621
- cmd = input("> ").strip().lower()
653
+ cmd = _safe_input("> ").strip().lower()
622
654
  if cmd == 'q':
623
655
  print("Exited game. Returning to interactive menu.\n")
624
656
  break
@@ -947,7 +979,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
947
979
  # Only ask for wavelength if it's diffraction data, not using Q, and no file wavelength info
948
980
  if is_diffraction and not use_Q and not file_wavelength_info:
949
981
  try:
950
- wl_in = input("Enter wavelength in Å for Q,d display (blank=skip, q=cancel): ").strip()
982
+ wl_in = _safe_input("Enter wavelength in Å for Q,d display (blank=skip, q=cancel): ").strip()
951
983
  if wl_in.lower() == 'q':
952
984
  print("Canceled.")
953
985
  return
@@ -1445,7 +1477,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1445
1477
  while True:
1446
1478
  try:
1447
1479
  print_main_menu()
1448
- key = input("Press a key: ").strip().lower()
1480
+ key = _safe_input("Press a key: ").strip().lower()
1449
1481
  except (KeyboardInterrupt, EOFError):
1450
1482
  print("\n\nExiting interactive menu...")
1451
1483
  break
@@ -1460,7 +1492,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1460
1492
 
1461
1493
  if key == 'q':
1462
1494
  try:
1463
- confirm = input(colorize_prompt("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
1495
+ confirm = _safe_input(colorize_prompt("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
1464
1496
  except (KeyboardInterrupt, EOFError):
1465
1497
  print("\nExiting interactive menu...")
1466
1498
  break
@@ -1514,7 +1546,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1514
1546
  current_pos = _current_label_position()
1515
1547
  print(f" {colorize_menu(f's: legend position (current: {current_pos})')}")
1516
1548
  print(f" {colorize_menu('q: back to main menu')}")
1517
- sub_key = input("Choose: ").strip().lower()
1549
+ sub_key = _safe_input("Choose: ").strip().lower()
1518
1550
 
1519
1551
  if sub_key == 'q':
1520
1552
  break
@@ -1536,7 +1568,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1536
1568
  print(" 2: top-left")
1537
1569
  print(" 3: bottom-right")
1538
1570
  print(" 4: bottom-left")
1539
- choice = input("Position (1-4, q=cancel): ").strip().lower()
1571
+ choice = _safe_input("Position (1-4, q=cancel): ").strip().lower()
1540
1572
  options = {
1541
1573
  '1': (False, False),
1542
1574
  '2': (False, True),
@@ -1638,7 +1670,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1638
1670
  prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
1639
1671
  else:
1640
1672
  prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
1641
- choice = input(prompt).strip()
1673
+ choice = _safe_input(prompt).strip()
1642
1674
  if not choice or choice.lower() == 'q':
1643
1675
  print("Canceled.")
1644
1676
  continue
@@ -1650,7 +1682,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1650
1682
  if not os.path.exists(last_session_path):
1651
1683
  print(f"Previous save file not found: {last_session_path}")
1652
1684
  continue
1653
- yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
1685
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
1654
1686
  if yn != 'y':
1655
1687
  continue
1656
1688
  _bp_dump_session(
@@ -1680,7 +1712,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1680
1712
  idx = int(choice)
1681
1713
  if 1 <= idx <= len(files):
1682
1714
  name = files[idx-1]
1683
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1715
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1684
1716
  if yn != 'y':
1685
1717
  print("Canceled.")
1686
1718
  continue
@@ -1720,7 +1752,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1720
1752
  target_path = name if os.path.isabs(name) else os.path.join(folder, name)
1721
1753
  skip_confirm = False # Let dump_session ask
1722
1754
  if os.path.exists(target_path):
1723
- yn = input(f"'{os.path.basename(target_path)}' exists. Overwrite? (y/n): ").strip().lower()
1755
+ yn = _safe_input(f"'{os.path.basename(target_path)}' exists. Overwrite? (y/n): ").strip().lower()
1724
1756
  if yn != 'y':
1725
1757
  print("Canceled.")
1726
1758
  continue
@@ -1772,7 +1804,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1772
1804
  print(f" {colorize_menu('t : change CIF tick set color (e.g., 1:red 2:#888888)')}")
1773
1805
  print(f" {colorize_menu('u : manage saved colors (use in m/p via number or u#)')}")
1774
1806
  print(f" {colorize_menu('q : return to main menu')}")
1775
- sub = input(colorize_prompt("Choose (m/p/s/t/u/q): ")).strip().lower()
1807
+ sub = _safe_input(colorize_prompt("Choose (m/p/s/t/u/q): ")).strip().lower()
1776
1808
  if sub == 'q':
1777
1809
  break
1778
1810
  if sub == '':
@@ -1790,7 +1822,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1790
1822
  print("\nSaved colors (refer as number or u#):")
1791
1823
  for idx, color in enumerate(user_colors, 1):
1792
1824
  print(f" {idx}: {color_block(color)} {color}")
1793
- color_input = input("Enter curve+color pairs (e.g., 1 red 2:u3) or q: ").strip()
1825
+ color_input = _safe_input("Enter curve+color pairs (e.g., 1 red 2:u3) or q: ").strip()
1794
1826
  if not color_input or color_input.lower() == 'q':
1795
1827
  print("Canceled.")
1796
1828
  else:
@@ -1844,7 +1876,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1844
1876
  for idx, color in enumerate(user_colors, 1):
1845
1877
  print(f" {idx}: {color_block(color)} {color}")
1846
1878
  print("Type 'u' to edit saved colors.")
1847
- line = input("Enter mappings (e.g., w red a u3) or q: ").strip()
1879
+ line = _safe_input("Enter mappings (e.g., w red a u3) or q: ").strip()
1848
1880
  if line.lower() == 'u':
1849
1881
  manage_user_colors(fig)
1850
1882
  continue
@@ -1900,7 +1932,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1900
1932
  print("Current CIF tick sets:")
1901
1933
  for i,(lab, fname, *_rest) in enumerate(cts):
1902
1934
  print(f" {i+1}: {lab} ({os.path.basename(fname)})")
1903
- line = input("Enter mappings (e.g., 1:red 2:#555555) or q: ").strip()
1935
+ line = _safe_input("Enter mappings (e.g., 1:red 2:#555555) or q: ").strip()
1904
1936
  if not line or line.lower()=='q':
1905
1937
  print("Canceled.")
1906
1938
  else:
@@ -1924,12 +1956,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1924
1956
  ax._cif_draw_func()
1925
1957
  fig.canvas.draw()
1926
1958
  elif sub == 'p':
1959
+ # Show current palette if one is applied
1927
1960
  history = getattr(fig, '_curve_palette_history', [])
1928
1961
  current_palette = history[-1]['palette'] if history else None
1929
1962
  if current_palette:
1930
1963
  print(f"Current palette: {current_palette}")
1931
- else:
1932
- print("Current palette: manual/custom")
1933
1964
  base_palettes = ['viridis', 'cividis', 'plasma', 'inferno', 'magma', 'batlow']
1934
1965
  extras = []
1935
1966
  def _palette_available(name: str) -> bool:
@@ -1966,7 +1997,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1966
1997
  if bar:
1967
1998
  print(f" {bar}")
1968
1999
  print(colorize_inline_commands("Example: 1-4 viridis or: all magma_r or: 1-3,5 plasma, _r for reverse"))
1969
- line = input("Enter range(s) and palette (number or name, e.g., '1-3 2' or 'all 1_r') or q: ").strip()
2000
+ line = _safe_input("Enter range(s) and palette (number or name, e.g., '1-3 2' or 'all 1_r') or q: ").strip()
1970
2001
  if not line or line.lower() == 'q':
1971
2002
  print("Canceled.")
1972
2003
  else:
@@ -2171,7 +2202,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2171
2202
  if has_cif:
2172
2203
  rename_opts += ", t=cif tick label"
2173
2204
  rename_opts += ", x=x-axis, y=y-axis, q=return"
2174
- mode = input(f"Rename ({rename_opts}): ").strip().lower()
2205
+ mode = _safe_input(f"Rename ({rename_opts}): ").strip().lower()
2175
2206
  if mode == 'q':
2176
2207
  break
2177
2208
  if mode == '':
@@ -2180,7 +2211,8 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2180
2211
  print("Tip: Use LaTeX/mathtext for special characters:")
2181
2212
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2182
2213
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2183
- idx_in = input("Curve number to rename (q=cancel): ").strip()
2214
+ print(" Shortcuts: g{super(-1)} g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
2215
+ idx_in = _safe_input("Curve number to rename (q=cancel): ").strip()
2184
2216
  if not idx_in or idx_in.lower() == 'q':
2185
2217
  print("Canceled.")
2186
2218
  continue
@@ -2192,10 +2224,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2192
2224
  if not (0 <= idx < len(labels)):
2193
2225
  print("Invalid index.")
2194
2226
  continue
2195
- new_label = input("New curve label (q=cancel): ")
2227
+ new_label = _safe_input("New curve label (q=cancel): ")
2196
2228
  if not new_label or new_label.lower() == 'q':
2197
2229
  print("Canceled.")
2198
2230
  continue
2231
+ new_label = convert_label_shortcuts(new_label)
2199
2232
  push_state("rename-curve")
2200
2233
  labels[idx] = new_label
2201
2234
  label_text_objects[idx].set_text(f"{idx+1}: {new_label}")
@@ -2207,7 +2240,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2207
2240
  continue
2208
2241
  for i,(lab, fname, *_rest) in enumerate(cts):
2209
2242
  print(f" {i+1}: {lab} ({os.path.basename(fname)})")
2210
- s = input("CIF tick number to rename (q=cancel): ").strip()
2243
+ s = _safe_input("CIF tick number to rename (q=cancel): ").strip()
2211
2244
  if not s or s.lower()=='q':
2212
2245
  print("Canceled."); continue
2213
2246
  try:
@@ -2219,9 +2252,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2219
2252
  print("Tip: Use LaTeX/mathtext for special characters:")
2220
2253
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2221
2254
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2222
- new_name = input("New CIF tick label (q=cancel): ")
2255
+ print(" Shortcuts: g{super(-1)} g$^{\\mathrm{-1}}$ | Li{sub(2)}O → Li$_{\\mathrm{2}}$O")
2256
+ new_name = _safe_input("New CIF tick label (q=cancel): ")
2223
2257
  if not new_name or new_name.lower()=='q':
2224
2258
  print("Canceled."); continue
2259
+ new_name = convert_label_shortcuts(new_name)
2225
2260
  lab,fname,peaksQ,wl,qmax_sim,color = cts[idx]
2226
2261
  # Suspend extension while updating label
2227
2262
  if _bp is not None:
@@ -2247,10 +2282,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2247
2282
  print("Tip: Use LaTeX/mathtext for special characters:")
2248
2283
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2249
2284
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2250
- new_axis = input("New axis label: ")
2285
+ print(" Shortcuts: g{super(-1)} → g$^{\\mathrm{-1}}$ | Li{sub(2)}O Li$_{\\mathrm{2}}$O")
2286
+ new_axis = _safe_input("New axis label: ")
2251
2287
  if not new_axis or new_axis.lower() == 'q':
2252
2288
  print("Canceled.")
2253
2289
  continue
2290
+ new_axis = convert_label_shortcuts(new_axis)
2254
2291
  new_axis = normalize_label_text(new_axis)
2255
2292
  push_state("rename-axis")
2256
2293
  # Freeze layout and preserve current pad via one-shot pending to avoid drift
@@ -2296,7 +2333,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2296
2333
  print("Current curve order:")
2297
2334
  for idx, label in enumerate(labels):
2298
2335
  print(f"{idx+1}: {label}")
2299
- new_order_str = input("Enter new order (space-separated indices, q=cancel): ").strip()
2336
+ new_order_str = _safe_input("Enter new order (space-separated indices, q=cancel): ").strip()
2300
2337
  if not new_order_str or new_order_str.lower() == 'q':
2301
2338
  print("Canceled.")
2302
2339
  continue
@@ -2386,7 +2423,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2386
2423
  try:
2387
2424
  current_xlim = ax.get_xlim()
2388
2425
  print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
2389
- rng = input("Enter new X range (min max), w=upper only, s=lower only, 'full', or 'a'=auto (restore original) (q=back): ").strip()
2426
+ rng = _safe_input("Enter new X range (min max), w=upper only, s=lower only, 'full', or 'a'=auto (restore original) (q=back): ").strip()
2390
2427
  if not rng or rng.lower() == 'q':
2391
2428
  break
2392
2429
  if rng.lower() == 'w':
@@ -2394,7 +2431,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2394
2431
  while True:
2395
2432
  current_xlim = ax.get_xlim()
2396
2433
  print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
2397
- val = input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
2434
+ val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
2398
2435
  if not val or val.lower() == 'q':
2399
2436
  break
2400
2437
  try:
@@ -2425,7 +2462,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2425
2462
  while True:
2426
2463
  current_xlim = ax.get_xlim()
2427
2464
  print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
2428
- val = input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
2465
+ val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
2429
2466
  if not val or val.lower() == 'q':
2430
2467
  break
2431
2468
  try:
@@ -2560,7 +2597,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2560
2597
  try:
2561
2598
  current_ylim = ax.get_ylim()
2562
2599
  print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
2563
- rng = input("Enter new Y range (min max), w=upper only, s=lower only, 'auto', 'a'=auto (restore original), or 'full' (q=back): ").strip().lower()
2600
+ rng = _safe_input("Enter new Y range (min max), w=upper only, s=lower only, 'auto', 'a'=auto (restore original), or 'full' (q=back): ").strip().lower()
2564
2601
  if not rng or rng == 'q':
2565
2602
  break
2566
2603
  if rng == 'w':
@@ -2568,7 +2605,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2568
2605
  while True:
2569
2606
  current_ylim = ax.get_ylim()
2570
2607
  print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
2571
- val = input(f"Enter new upper Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
2608
+ val = _safe_input(f"Enter new upper Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
2572
2609
  if not val or val.lower() == 'q':
2573
2610
  break
2574
2611
  try:
@@ -2590,7 +2627,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2590
2627
  while True:
2591
2628
  current_ylim = ax.get_ylim()
2592
2629
  print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
2593
- val = input(f"Enter new lower Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
2630
+ val = _safe_input(f"Enter new lower Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
2594
2631
  if not val or val.lower() == 'q':
2595
2632
  break
2596
2633
  try:
@@ -2675,7 +2712,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2675
2712
  print(f" {colorize_menu('q: back to main menu')}")
2676
2713
 
2677
2714
  while True:
2678
- offset_cmd = input("Offset> ").strip().lower()
2715
+ offset_cmd = _safe_input("Offset> ").strip().lower()
2679
2716
 
2680
2717
  if offset_cmd == 'q' or offset_cmd == '':
2681
2718
  break
@@ -2725,7 +2762,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2725
2762
  if spacing_diffs:
2726
2763
  current_spacing = sum(spacing_diffs) / len(spacing_diffs)
2727
2764
 
2728
- spacing_input = input("Enter spacing value between curves (current avg: {:.4g}): ".format(current_spacing)).strip()
2765
+ spacing_input = _safe_input("Enter spacing value between curves (current avg: {:.4g}): ".format(current_spacing)).strip()
2729
2766
  if not spacing_input:
2730
2767
  print("Canceled.")
2731
2768
  continue
@@ -2784,7 +2821,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2784
2821
  if len(labels) <= 1:
2785
2822
  print("Warning: Only one curve loaded; applying an offset is not recommended.")
2786
2823
  try:
2787
- new_delta_str = input(f"Enter new offset spacing (current={delta}): ").strip()
2824
+ new_delta_str = _safe_input(f"Enter new offset spacing (current={delta}): ").strip()
2788
2825
  if not new_delta_str:
2789
2826
  print("Canceled.")
2790
2827
  continue
@@ -2847,7 +2884,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2847
2884
 
2848
2885
  current_offset = offsets_list[idx] if idx < len(offsets_list) else 0.0
2849
2886
 
2850
- individual_offset_input = input("Enter offset for curve {} (current: {:.4g}): ".format(
2887
+ individual_offset_input = _safe_input("Enter offset for curve {} (current: {:.4g}): ".format(
2851
2888
  curve_num, current_offset)).strip()
2852
2889
  if not individual_offset_input:
2853
2890
  print("Canceled.")
@@ -2887,7 +2924,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2887
2924
  print("No curves to modify.")
2888
2925
  return []
2889
2926
  print(f"Total curves available: {total}")
2890
- raw = input(prompt_text + " ").strip().lower()
2927
+ raw = _safe_input(prompt_text + " ").strip().lower()
2891
2928
  if not raw or raw in ('all', '*'):
2892
2929
  return list(range(total))
2893
2930
  import re as _re
@@ -2906,7 +2943,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2906
2943
  return selected
2907
2944
 
2908
2945
  def _prompt_float(prompt_text):
2909
- raw = input(prompt_text).strip()
2946
+ raw = _safe_input(prompt_text).strip()
2910
2947
  if not raw:
2911
2948
  return None
2912
2949
  if raw.lower() == 'q':
@@ -2919,10 +2956,10 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2919
2956
 
2920
2957
  def _prompt_dash_pattern(kind='dash'):
2921
2958
  if kind == 'dashdot':
2922
- raw = input("Dash-dot pattern 'dash gap dot gap' (blank=6 3 1 3, q=cancel): ").strip().lower()
2959
+ raw = _safe_input("Dash-dot pattern 'dash gap dot gap' (blank=6 3 1 3, q=cancel): ").strip().lower()
2923
2960
  default = (6.0, 3.0, 1.0, 3.0)
2924
2961
  else:
2925
- raw = input("Dash pattern 'length gap' (blank=6 3, q=cancel): ").strip().lower()
2962
+ raw = _safe_input("Dash pattern 'length gap' (blank=6 3, q=cancel): ").strip().lower()
2926
2963
  default = (6.0, 3.0)
2927
2964
  if not raw:
2928
2965
  return default
@@ -2962,13 +2999,13 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2962
2999
  print(f" {colorize_menu('da : dashed line for selected curves')}")
2963
3000
  print(f" {colorize_menu('dd : dashed line + dots for selected curves')}")
2964
3001
  print(f" {colorize_menu('q : return')}")
2965
- sub = input(colorize_prompt("Choose (c/f/g/l/ld/d/da/dd/q): ")).strip().lower()
3002
+ sub = _safe_input(colorize_prompt("Choose (c/f/g/l/ld/d/da/dd/q): ")).strip().lower()
2966
3003
  if sub == 'q':
2967
3004
  break
2968
3005
  if sub == '':
2969
3006
  continue
2970
3007
  if sub == 'c':
2971
- spec = input("Curve widths (single value OR mappings like '1:1.2 3:2', q=cancel): ").strip()
3008
+ spec = _safe_input("Curve widths (single value OR mappings like '1:1.2 3:2', q=cancel): ").strip()
2972
3009
  if not spec or spec.lower() == 'q':
2973
3010
  print("Canceled.")
2974
3011
  else:
@@ -2998,7 +3035,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2998
3035
  print("Invalid width value.")
2999
3036
  fig.canvas.draw()
3000
3037
  elif sub == 'f':
3001
- fw_in = input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
3038
+ fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
3002
3039
  if not fw_in or fw_in.lower() == 'q':
3003
3040
  print("Canceled.")
3004
3041
  else:
@@ -3133,7 +3170,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3133
3170
  cur_family = plt.rcParams.get('font.sans-serif', [''])[0]
3134
3171
  cur_size = plt.rcParams.get('font.size', None)
3135
3172
  while True:
3136
- subkey = input(colorize_prompt(f"Font submenu (current: family='{cur_family}', size={cur_size}) - s=size, f=family, q=return: ")).strip().lower()
3173
+ subkey = _safe_input(colorize_prompt(f"Font submenu (current: family='{cur_family}', size={cur_size}) - s=size, f=family, q=return: ")).strip().lower()
3137
3174
  if subkey == 'q':
3138
3175
  break
3139
3176
  if subkey == '':
@@ -3141,7 +3178,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3141
3178
  if subkey == 's':
3142
3179
  try:
3143
3180
  cur_size = plt.rcParams.get('font.size', None)
3144
- fs = input(f"Enter new font size (current: {cur_size}, q=cancel): ").strip()
3181
+ fs = _safe_input(f"Enter new font size (current: {cur_size}, q=cancel): ").strip()
3145
3182
  if not fs or fs.lower() == 'q':
3146
3183
  print("Canceled.")
3147
3184
  else:
@@ -3163,7 +3200,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3163
3200
  print(" 3) Times New Roman")
3164
3201
  print(" 4) STIXGeneral")
3165
3202
  print(" 5) DejaVu Sans")
3166
- ft_raw = input(f"Enter font number or family name (current: '{cur_family}', q=cancel): ").strip()
3203
+ ft_raw = _safe_input(f"Enter font number or family name (current: '{cur_family}', q=cancel): ").strip()
3167
3204
  if not ft_raw or ft_raw.lower() == 'q':
3168
3205
  print("Canceled.")
3169
3206
  else:
@@ -3189,7 +3226,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3189
3226
  elif key == 'g':
3190
3227
  try:
3191
3228
  while True:
3192
- choice = input(colorize_prompt("Resize submenu: (p=plot frame, c=canvas, q=cancel): ")).strip().lower()
3229
+ choice = _safe_input(colorize_prompt("Resize submenu: (p=plot frame, c=canvas, q=cancel): ")).strip().lower()
3193
3230
  if not choice:
3194
3231
  continue
3195
3232
  if choice == 'q':
@@ -3214,7 +3251,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3214
3251
  current_pos = "bottom-right" if getattr(fig, '_stack_label_at_bottom', False) else "top-right"
3215
3252
  print(f" s: legend position (current: {current_pos})")
3216
3253
  print(" q: back to main menu")
3217
- sub_key = input("Choose: ").strip().lower()
3254
+ sub_key = _safe_input("Choose: ").strip().lower()
3218
3255
 
3219
3256
  if sub_key == 'q':
3220
3257
  break
@@ -3272,7 +3309,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3272
3309
  print(colorize_inline_commands(" Combine letter+number to toggle, e.g. 's2 w5 a4' (case-insensitive)"))
3273
3310
  print(colorize_inline_commands(" i = invert tick direction, l = change tick length, list = show state, q = return"))
3274
3311
  print(colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
3275
- cmd = input(colorize_prompt("Enter code(s): ")).strip().lower()
3312
+ cmd = _safe_input(colorize_prompt("Enter code(s): ")).strip().lower()
3276
3313
  if not cmd:
3277
3314
  continue
3278
3315
  if cmd == 'q':
@@ -3299,7 +3336,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3299
3336
  # Get current major tick length from axes
3300
3337
  current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
3301
3338
  print(f"Current major tick length: {current_major}")
3302
- new_length_str = input("Enter new major tick length (e.g., 6.0): ").strip()
3339
+ new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
3303
3340
  if not new_length_str:
3304
3341
  continue
3305
3342
  new_major = float(new_length_str)
@@ -3582,9 +3619,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3582
3619
  print(f" {_i}: {fname}")
3583
3620
  last_style_path = getattr(fig, '_last_style_export_path', None)
3584
3621
  if last_style_path:
3585
- sub = input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
3622
+ sub = _safe_input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
3586
3623
  else:
3587
- sub = input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
3624
+ sub = _safe_input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
3588
3625
  if sub == 'q':
3589
3626
  break
3590
3627
  if sub == 'r' or sub == '':
@@ -3597,7 +3634,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3597
3634
  if not os.path.exists(last_style_path):
3598
3635
  print(f"Previous export file not found: {last_style_path}")
3599
3636
  continue
3600
- yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3637
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3601
3638
  if yn != 'y':
3602
3639
  continue
3603
3640
  # Call export_style_config with overwrite_path to skip dialog
@@ -3655,9 +3692,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3655
3692
 
3656
3693
  last_figure_path = getattr(fig, '_last_figure_export_path', None)
3657
3694
  if last_figure_path:
3658
- filename = input("Enter filename (default SVG if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
3695
+ filename = _safe_input("Enter filename (default SVG if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
3659
3696
  else:
3660
- filename = input("Enter filename (default SVG if no extension) or number to overwrite (q=cancel): ").strip()
3697
+ filename = _safe_input("Enter filename (default SVG if no extension) or number to overwrite (q=cancel): ").strip()
3661
3698
  if not filename or filename.lower() == 'q':
3662
3699
  print("Canceled.")
3663
3700
  continue
@@ -3671,7 +3708,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3671
3708
  if not os.path.exists(last_figure_path):
3672
3709
  print(f"Previous export file not found: {last_figure_path}")
3673
3710
  continue
3674
- yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
3711
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
3675
3712
  if yn != 'y':
3676
3713
  print("Canceled.")
3677
3714
  continue
@@ -3683,7 +3720,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3683
3720
  idx = int(filename)
3684
3721
  if 1 <= idx <= len(files):
3685
3722
  name = files[idx-1]
3686
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3723
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3687
3724
  if yn != 'y':
3688
3725
  print("Canceled.")
3689
3726
  continue
@@ -3759,7 +3796,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3759
3796
  elif key == 'v':
3760
3797
  while True:
3761
3798
  try:
3762
- rng_in = input("Peak X range (min max, 'current' for axes limits, q=back): ").strip().lower()
3799
+ rng_in = _safe_input("Peak X range (min max, 'current' for axes limits, q=back): ").strip().lower()
3763
3800
  if not rng_in or rng_in == 'q':
3764
3801
  break
3765
3802
  if rng_in == 'current':
@@ -3773,12 +3810,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3773
3810
  if x_min > x_max:
3774
3811
  x_min, x_max = x_max, x_min
3775
3812
 
3776
- frac_in = input("Min relative peak height (0–1, default 0.1): ").strip()
3813
+ frac_in = _safe_input("Min relative peak height (0–1, default 0.1): ").strip()
3777
3814
  min_frac = float(frac_in) if frac_in else 0.1
3778
3815
  if min_frac < 0: min_frac = 0.0
3779
3816
  if min_frac > 1: min_frac = 1.0
3780
3817
 
3781
- swin = input("Smoothing window (odd int >=3, blank=none): ").strip()
3818
+ swin = _safe_input("Smoothing window (odd int >=3, blank=none): ").strip()
3782
3819
  if swin:
3783
3820
  try:
3784
3821
  win = int(swin)