batplot 1.7.27__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.

batplot/interactive.py CHANGED
@@ -58,6 +58,37 @@ from .color_utils import (
58
58
  )
59
59
 
60
60
 
61
+ class _FilterIMKWarning:
62
+ """Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
63
+ def __init__(self, original_stderr):
64
+ self.original_stderr = original_stderr
65
+
66
+ def write(self, message):
67
+ # Filter out the harmless macOS IMK warning
68
+ if 'IMKCFRunLoopWakeUpReliable' not in message:
69
+ self.original_stderr.write(message)
70
+
71
+ def flush(self):
72
+ self.original_stderr.flush()
73
+
74
+
75
+ def _safe_input(prompt: str = "") -> str:
76
+ """Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
77
+
78
+ This is a harmless macOS system message that appears when using input() in terminals.
79
+ """
80
+ # Filter stderr to hide macOS IMK warnings while preserving other errors
81
+ original_stderr = sys.stderr
82
+ sys.stderr = _FilterIMKWarning(original_stderr)
83
+ try:
84
+ result = input(prompt)
85
+ return result
86
+ except (KeyboardInterrupt, EOFError):
87
+ raise
88
+ finally:
89
+ sys.stderr = original_stderr
90
+
91
+
61
92
  def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
62
93
  label_text_objects, delta, x_label, args,
63
94
  x_full_list, raw_y_full_list, offsets_list,
@@ -363,7 +394,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
363
394
  current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
364
395
  current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
365
396
  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()
397
+ sub = _safe_input(colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
367
398
  if not sub:
368
399
  continue
369
400
  if sub == 'q':
@@ -401,7 +432,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
401
432
  current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
402
433
  current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
403
434
  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()
435
+ sub = _safe_input(colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
405
436
  if not sub:
406
437
  continue
407
438
  if sub == 'q':
@@ -438,7 +469,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
438
469
  while True:
439
470
  current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
440
471
  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()
472
+ sub = _safe_input(colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
442
473
  if not sub:
443
474
  continue
444
475
  if sub == 'q':
@@ -468,7 +499,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
468
499
  while True:
469
500
  current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
470
501
  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()
502
+ sub = _safe_input(colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
472
503
  if not sub:
473
504
  continue
474
505
  if sub == 'q':
@@ -499,7 +530,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
499
530
  print(" " + colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
500
531
  print(" " + colorize_menu('r : reset all offsets'))
501
532
  print(" " + colorize_menu('q : back to toggle menu'))
502
- choice = input(colorize_prompt("p> ")).strip().lower()
533
+ choice = _safe_input(colorize_prompt("p> ")).strip().lower()
503
534
  if not choice:
504
535
  continue
505
536
  if choice == 'q':
@@ -618,7 +649,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
618
649
 
619
650
  while True:
620
651
  render()
621
- cmd = input("> ").strip().lower()
652
+ cmd = _safe_input("> ").strip().lower()
622
653
  if cmd == 'q':
623
654
  print("Exited game. Returning to interactive menu.\n")
624
655
  break
@@ -947,7 +978,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
947
978
  # Only ask for wavelength if it's diffraction data, not using Q, and no file wavelength info
948
979
  if is_diffraction and not use_Q and not file_wavelength_info:
949
980
  try:
950
- wl_in = input("Enter wavelength in Å for Q,d display (blank=skip, q=cancel): ").strip()
981
+ wl_in = _safe_input("Enter wavelength in Å for Q,d display (blank=skip, q=cancel): ").strip()
951
982
  if wl_in.lower() == 'q':
952
983
  print("Canceled.")
953
984
  return
@@ -1445,7 +1476,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1445
1476
  while True:
1446
1477
  try:
1447
1478
  print_main_menu()
1448
- key = input("Press a key: ").strip().lower()
1479
+ key = _safe_input("Press a key: ").strip().lower()
1449
1480
  except (KeyboardInterrupt, EOFError):
1450
1481
  print("\n\nExiting interactive menu...")
1451
1482
  break
@@ -1460,7 +1491,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1460
1491
 
1461
1492
  if key == 'q':
1462
1493
  try:
1463
- confirm = input(colorize_prompt("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
1494
+ confirm = _safe_input(colorize_prompt("Quit interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
1464
1495
  except (KeyboardInterrupt, EOFError):
1465
1496
  print("\nExiting interactive menu...")
1466
1497
  break
@@ -1514,7 +1545,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1514
1545
  current_pos = _current_label_position()
1515
1546
  print(f" {colorize_menu(f's: legend position (current: {current_pos})')}")
1516
1547
  print(f" {colorize_menu('q: back to main menu')}")
1517
- sub_key = input("Choose: ").strip().lower()
1548
+ sub_key = _safe_input("Choose: ").strip().lower()
1518
1549
 
1519
1550
  if sub_key == 'q':
1520
1551
  break
@@ -1536,7 +1567,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1536
1567
  print(" 2: top-left")
1537
1568
  print(" 3: bottom-right")
1538
1569
  print(" 4: bottom-left")
1539
- choice = input("Position (1-4, q=cancel): ").strip().lower()
1570
+ choice = _safe_input("Position (1-4, q=cancel): ").strip().lower()
1540
1571
  options = {
1541
1572
  '1': (False, False),
1542
1573
  '2': (False, True),
@@ -1638,7 +1669,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1638
1669
  prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
1639
1670
  else:
1640
1671
  prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
1641
- choice = input(prompt).strip()
1672
+ choice = _safe_input(prompt).strip()
1642
1673
  if not choice or choice.lower() == 'q':
1643
1674
  print("Canceled.")
1644
1675
  continue
@@ -1650,7 +1681,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1650
1681
  if not os.path.exists(last_session_path):
1651
1682
  print(f"Previous save file not found: {last_session_path}")
1652
1683
  continue
1653
- yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
1684
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
1654
1685
  if yn != 'y':
1655
1686
  continue
1656
1687
  _bp_dump_session(
@@ -1680,7 +1711,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1680
1711
  idx = int(choice)
1681
1712
  if 1 <= idx <= len(files):
1682
1713
  name = files[idx-1]
1683
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1714
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1684
1715
  if yn != 'y':
1685
1716
  print("Canceled.")
1686
1717
  continue
@@ -1720,7 +1751,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1720
1751
  target_path = name if os.path.isabs(name) else os.path.join(folder, name)
1721
1752
  skip_confirm = False # Let dump_session ask
1722
1753
  if os.path.exists(target_path):
1723
- yn = input(f"'{os.path.basename(target_path)}' exists. Overwrite? (y/n): ").strip().lower()
1754
+ yn = _safe_input(f"'{os.path.basename(target_path)}' exists. Overwrite? (y/n): ").strip().lower()
1724
1755
  if yn != 'y':
1725
1756
  print("Canceled.")
1726
1757
  continue
@@ -1772,7 +1803,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1772
1803
  print(f" {colorize_menu('t : change CIF tick set color (e.g., 1:red 2:#888888)')}")
1773
1804
  print(f" {colorize_menu('u : manage saved colors (use in m/p via number or u#)')}")
1774
1805
  print(f" {colorize_menu('q : return to main menu')}")
1775
- sub = input(colorize_prompt("Choose (m/p/s/t/u/q): ")).strip().lower()
1806
+ sub = _safe_input(colorize_prompt("Choose (m/p/s/t/u/q): ")).strip().lower()
1776
1807
  if sub == 'q':
1777
1808
  break
1778
1809
  if sub == '':
@@ -1790,7 +1821,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1790
1821
  print("\nSaved colors (refer as number or u#):")
1791
1822
  for idx, color in enumerate(user_colors, 1):
1792
1823
  print(f" {idx}: {color_block(color)} {color}")
1793
- color_input = input("Enter curve+color pairs (e.g., 1 red 2:u3) or q: ").strip()
1824
+ color_input = _safe_input("Enter curve+color pairs (e.g., 1 red 2:u3) or q: ").strip()
1794
1825
  if not color_input or color_input.lower() == 'q':
1795
1826
  print("Canceled.")
1796
1827
  else:
@@ -1844,7 +1875,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1844
1875
  for idx, color in enumerate(user_colors, 1):
1845
1876
  print(f" {idx}: {color_block(color)} {color}")
1846
1877
  print("Type 'u' to edit saved colors.")
1847
- line = input("Enter mappings (e.g., w red a u3) or q: ").strip()
1878
+ line = _safe_input("Enter mappings (e.g., w red a u3) or q: ").strip()
1848
1879
  if line.lower() == 'u':
1849
1880
  manage_user_colors(fig)
1850
1881
  continue
@@ -1900,7 +1931,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1900
1931
  print("Current CIF tick sets:")
1901
1932
  for i,(lab, fname, *_rest) in enumerate(cts):
1902
1933
  print(f" {i+1}: {lab} ({os.path.basename(fname)})")
1903
- line = input("Enter mappings (e.g., 1:red 2:#555555) or q: ").strip()
1934
+ line = _safe_input("Enter mappings (e.g., 1:red 2:#555555) or q: ").strip()
1904
1935
  if not line or line.lower()=='q':
1905
1936
  print("Canceled.")
1906
1937
  else:
@@ -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,7 @@ 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
+ idx_in = _safe_input("Curve number to rename (q=cancel): ").strip()
2184
2215
  if not idx_in or idx_in.lower() == 'q':
2185
2216
  print("Canceled.")
2186
2217
  continue
@@ -2192,7 +2223,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2192
2223
  if not (0 <= idx < len(labels)):
2193
2224
  print("Invalid index.")
2194
2225
  continue
2195
- new_label = input("New curve label (q=cancel): ")
2226
+ new_label = _safe_input("New curve label (q=cancel): ")
2196
2227
  if not new_label or new_label.lower() == 'q':
2197
2228
  print("Canceled.")
2198
2229
  continue
@@ -2207,7 +2238,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2207
2238
  continue
2208
2239
  for i,(lab, fname, *_rest) in enumerate(cts):
2209
2240
  print(f" {i+1}: {lab} ({os.path.basename(fname)})")
2210
- s = input("CIF tick number to rename (q=cancel): ").strip()
2241
+ s = _safe_input("CIF tick number to rename (q=cancel): ").strip()
2211
2242
  if not s or s.lower()=='q':
2212
2243
  print("Canceled."); continue
2213
2244
  try:
@@ -2219,7 +2250,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2219
2250
  print("Tip: Use LaTeX/mathtext for special characters:")
2220
2251
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2221
2252
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2222
- new_name = input("New CIF tick label (q=cancel): ")
2253
+ new_name = _safe_input("New CIF tick label (q=cancel): ")
2223
2254
  if not new_name or new_name.lower()=='q':
2224
2255
  print("Canceled."); continue
2225
2256
  lab,fname,peaksQ,wl,qmax_sim,color = cts[idx]
@@ -2247,7 +2278,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2247
2278
  print("Tip: Use LaTeX/mathtext for special characters:")
2248
2279
  print(" Subscript: H$_2$O → H₂O | Superscript: m$^2$ → m²")
2249
2280
  print(" Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2250
- new_axis = input("New axis label: ")
2281
+ new_axis = _safe_input("New axis label: ")
2251
2282
  if not new_axis or new_axis.lower() == 'q':
2252
2283
  print("Canceled.")
2253
2284
  continue
@@ -2296,7 +2327,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2296
2327
  print("Current curve order:")
2297
2328
  for idx, label in enumerate(labels):
2298
2329
  print(f"{idx+1}: {label}")
2299
- new_order_str = input("Enter new order (space-separated indices, q=cancel): ").strip()
2330
+ new_order_str = _safe_input("Enter new order (space-separated indices, q=cancel): ").strip()
2300
2331
  if not new_order_str or new_order_str.lower() == 'q':
2301
2332
  print("Canceled.")
2302
2333
  continue
@@ -2386,7 +2417,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2386
2417
  try:
2387
2418
  current_xlim = ax.get_xlim()
2388
2419
  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()
2420
+ 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
2421
  if not rng or rng.lower() == 'q':
2391
2422
  break
2392
2423
  if rng.lower() == 'w':
@@ -2394,7 +2425,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2394
2425
  while True:
2395
2426
  current_xlim = ax.get_xlim()
2396
2427
  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()
2428
+ val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
2398
2429
  if not val or val.lower() == 'q':
2399
2430
  break
2400
2431
  try:
@@ -2425,7 +2456,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2425
2456
  while True:
2426
2457
  current_xlim = ax.get_xlim()
2427
2458
  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()
2459
+ val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
2429
2460
  if not val or val.lower() == 'q':
2430
2461
  break
2431
2462
  try:
@@ -2560,7 +2591,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2560
2591
  try:
2561
2592
  current_ylim = ax.get_ylim()
2562
2593
  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()
2594
+ 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
2595
  if not rng or rng == 'q':
2565
2596
  break
2566
2597
  if rng == 'w':
@@ -2568,7 +2599,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2568
2599
  while True:
2569
2600
  current_ylim = ax.get_ylim()
2570
2601
  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()
2602
+ val = _safe_input(f"Enter new upper Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
2572
2603
  if not val or val.lower() == 'q':
2573
2604
  break
2574
2605
  try:
@@ -2590,7 +2621,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2590
2621
  while True:
2591
2622
  current_ylim = ax.get_ylim()
2592
2623
  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()
2624
+ val = _safe_input(f"Enter new lower Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
2594
2625
  if not val or val.lower() == 'q':
2595
2626
  break
2596
2627
  try:
@@ -2675,7 +2706,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2675
2706
  print(f" {colorize_menu('q: back to main menu')}")
2676
2707
 
2677
2708
  while True:
2678
- offset_cmd = input("Offset> ").strip().lower()
2709
+ offset_cmd = _safe_input("Offset> ").strip().lower()
2679
2710
 
2680
2711
  if offset_cmd == 'q' or offset_cmd == '':
2681
2712
  break
@@ -2725,7 +2756,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2725
2756
  if spacing_diffs:
2726
2757
  current_spacing = sum(spacing_diffs) / len(spacing_diffs)
2727
2758
 
2728
- spacing_input = input("Enter spacing value between curves (current avg: {:.4g}): ".format(current_spacing)).strip()
2759
+ spacing_input = _safe_input("Enter spacing value between curves (current avg: {:.4g}): ".format(current_spacing)).strip()
2729
2760
  if not spacing_input:
2730
2761
  print("Canceled.")
2731
2762
  continue
@@ -2784,7 +2815,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2784
2815
  if len(labels) <= 1:
2785
2816
  print("Warning: Only one curve loaded; applying an offset is not recommended.")
2786
2817
  try:
2787
- new_delta_str = input(f"Enter new offset spacing (current={delta}): ").strip()
2818
+ new_delta_str = _safe_input(f"Enter new offset spacing (current={delta}): ").strip()
2788
2819
  if not new_delta_str:
2789
2820
  print("Canceled.")
2790
2821
  continue
@@ -2847,7 +2878,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2847
2878
 
2848
2879
  current_offset = offsets_list[idx] if idx < len(offsets_list) else 0.0
2849
2880
 
2850
- individual_offset_input = input("Enter offset for curve {} (current: {:.4g}): ".format(
2881
+ individual_offset_input = _safe_input("Enter offset for curve {} (current: {:.4g}): ".format(
2851
2882
  curve_num, current_offset)).strip()
2852
2883
  if not individual_offset_input:
2853
2884
  print("Canceled.")
@@ -2887,7 +2918,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2887
2918
  print("No curves to modify.")
2888
2919
  return []
2889
2920
  print(f"Total curves available: {total}")
2890
- raw = input(prompt_text + " ").strip().lower()
2921
+ raw = _safe_input(prompt_text + " ").strip().lower()
2891
2922
  if not raw or raw in ('all', '*'):
2892
2923
  return list(range(total))
2893
2924
  import re as _re
@@ -2906,7 +2937,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2906
2937
  return selected
2907
2938
 
2908
2939
  def _prompt_float(prompt_text):
2909
- raw = input(prompt_text).strip()
2940
+ raw = _safe_input(prompt_text).strip()
2910
2941
  if not raw:
2911
2942
  return None
2912
2943
  if raw.lower() == 'q':
@@ -2919,10 +2950,10 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2919
2950
 
2920
2951
  def _prompt_dash_pattern(kind='dash'):
2921
2952
  if kind == 'dashdot':
2922
- raw = input("Dash-dot pattern 'dash gap dot gap' (blank=6 3 1 3, q=cancel): ").strip().lower()
2953
+ raw = _safe_input("Dash-dot pattern 'dash gap dot gap' (blank=6 3 1 3, q=cancel): ").strip().lower()
2923
2954
  default = (6.0, 3.0, 1.0, 3.0)
2924
2955
  else:
2925
- raw = input("Dash pattern 'length gap' (blank=6 3, q=cancel): ").strip().lower()
2956
+ raw = _safe_input("Dash pattern 'length gap' (blank=6 3, q=cancel): ").strip().lower()
2926
2957
  default = (6.0, 3.0)
2927
2958
  if not raw:
2928
2959
  return default
@@ -2962,13 +2993,13 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2962
2993
  print(f" {colorize_menu('da : dashed line for selected curves')}")
2963
2994
  print(f" {colorize_menu('dd : dashed line + dots for selected curves')}")
2964
2995
  print(f" {colorize_menu('q : return')}")
2965
- sub = input(colorize_prompt("Choose (c/f/g/l/ld/d/da/dd/q): ")).strip().lower()
2996
+ sub = _safe_input(colorize_prompt("Choose (c/f/g/l/ld/d/da/dd/q): ")).strip().lower()
2966
2997
  if sub == 'q':
2967
2998
  break
2968
2999
  if sub == '':
2969
3000
  continue
2970
3001
  if sub == 'c':
2971
- spec = input("Curve widths (single value OR mappings like '1:1.2 3:2', q=cancel): ").strip()
3002
+ spec = _safe_input("Curve widths (single value OR mappings like '1:1.2 3:2', q=cancel): ").strip()
2972
3003
  if not spec or spec.lower() == 'q':
2973
3004
  print("Canceled.")
2974
3005
  else:
@@ -2998,7 +3029,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2998
3029
  print("Invalid width value.")
2999
3030
  fig.canvas.draw()
3000
3031
  elif sub == 'f':
3001
- fw_in = input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
3032
+ fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
3002
3033
  if not fw_in or fw_in.lower() == 'q':
3003
3034
  print("Canceled.")
3004
3035
  else:
@@ -3133,7 +3164,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3133
3164
  cur_family = plt.rcParams.get('font.sans-serif', [''])[0]
3134
3165
  cur_size = plt.rcParams.get('font.size', None)
3135
3166
  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()
3167
+ subkey = _safe_input(colorize_prompt(f"Font submenu (current: family='{cur_family}', size={cur_size}) - s=size, f=family, q=return: ")).strip().lower()
3137
3168
  if subkey == 'q':
3138
3169
  break
3139
3170
  if subkey == '':
@@ -3141,7 +3172,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3141
3172
  if subkey == 's':
3142
3173
  try:
3143
3174
  cur_size = plt.rcParams.get('font.size', None)
3144
- fs = input(f"Enter new font size (current: {cur_size}, q=cancel): ").strip()
3175
+ fs = _safe_input(f"Enter new font size (current: {cur_size}, q=cancel): ").strip()
3145
3176
  if not fs or fs.lower() == 'q':
3146
3177
  print("Canceled.")
3147
3178
  else:
@@ -3163,7 +3194,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3163
3194
  print(" 3) Times New Roman")
3164
3195
  print(" 4) STIXGeneral")
3165
3196
  print(" 5) DejaVu Sans")
3166
- ft_raw = input(f"Enter font number or family name (current: '{cur_family}', q=cancel): ").strip()
3197
+ ft_raw = _safe_input(f"Enter font number or family name (current: '{cur_family}', q=cancel): ").strip()
3167
3198
  if not ft_raw or ft_raw.lower() == 'q':
3168
3199
  print("Canceled.")
3169
3200
  else:
@@ -3189,7 +3220,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3189
3220
  elif key == 'g':
3190
3221
  try:
3191
3222
  while True:
3192
- choice = input(colorize_prompt("Resize submenu: (p=plot frame, c=canvas, q=cancel): ")).strip().lower()
3223
+ choice = _safe_input(colorize_prompt("Resize submenu: (p=plot frame, c=canvas, q=cancel): ")).strip().lower()
3193
3224
  if not choice:
3194
3225
  continue
3195
3226
  if choice == 'q':
@@ -3214,7 +3245,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3214
3245
  current_pos = "bottom-right" if getattr(fig, '_stack_label_at_bottom', False) else "top-right"
3215
3246
  print(f" s: legend position (current: {current_pos})")
3216
3247
  print(" q: back to main menu")
3217
- sub_key = input("Choose: ").strip().lower()
3248
+ sub_key = _safe_input("Choose: ").strip().lower()
3218
3249
 
3219
3250
  if sub_key == 'q':
3220
3251
  break
@@ -3272,7 +3303,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3272
3303
  print(colorize_inline_commands(" Combine letter+number to toggle, e.g. 's2 w5 a4' (case-insensitive)"))
3273
3304
  print(colorize_inline_commands(" i = invert tick direction, l = change tick length, list = show state, q = return"))
3274
3305
  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()
3306
+ cmd = _safe_input(colorize_prompt("Enter code(s): ")).strip().lower()
3276
3307
  if not cmd:
3277
3308
  continue
3278
3309
  if cmd == 'q':
@@ -3299,7 +3330,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3299
3330
  # Get current major tick length from axes
3300
3331
  current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
3301
3332
  print(f"Current major tick length: {current_major}")
3302
- new_length_str = input("Enter new major tick length (e.g., 6.0): ").strip()
3333
+ new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
3303
3334
  if not new_length_str:
3304
3335
  continue
3305
3336
  new_major = float(new_length_str)
@@ -3582,9 +3613,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3582
3613
  print(f" {_i}: {fname}")
3583
3614
  last_style_path = getattr(fig, '_last_style_export_path', None)
3584
3615
  if last_style_path:
3585
- sub = input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
3616
+ sub = _safe_input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
3586
3617
  else:
3587
- sub = input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
3618
+ sub = _safe_input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
3588
3619
  if sub == 'q':
3589
3620
  break
3590
3621
  if sub == 'r' or sub == '':
@@ -3597,7 +3628,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3597
3628
  if not os.path.exists(last_style_path):
3598
3629
  print(f"Previous export file not found: {last_style_path}")
3599
3630
  continue
3600
- yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3631
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
3601
3632
  if yn != 'y':
3602
3633
  continue
3603
3634
  # Call export_style_config with overwrite_path to skip dialog
@@ -3655,9 +3686,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3655
3686
 
3656
3687
  last_figure_path = getattr(fig, '_last_figure_export_path', None)
3657
3688
  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()
3689
+ filename = _safe_input("Enter filename (default SVG if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
3659
3690
  else:
3660
- filename = input("Enter filename (default SVG if no extension) or number to overwrite (q=cancel): ").strip()
3691
+ filename = _safe_input("Enter filename (default SVG if no extension) or number to overwrite (q=cancel): ").strip()
3661
3692
  if not filename or filename.lower() == 'q':
3662
3693
  print("Canceled.")
3663
3694
  continue
@@ -3671,7 +3702,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3671
3702
  if not os.path.exists(last_figure_path):
3672
3703
  print(f"Previous export file not found: {last_figure_path}")
3673
3704
  continue
3674
- yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
3705
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
3675
3706
  if yn != 'y':
3676
3707
  print("Canceled.")
3677
3708
  continue
@@ -3683,7 +3714,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3683
3714
  idx = int(filename)
3684
3715
  if 1 <= idx <= len(files):
3685
3716
  name = files[idx-1]
3686
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3717
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3687
3718
  if yn != 'y':
3688
3719
  print("Canceled.")
3689
3720
  continue
@@ -3759,7 +3790,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3759
3790
  elif key == 'v':
3760
3791
  while True:
3761
3792
  try:
3762
- rng_in = input("Peak X range (min max, 'current' for axes limits, q=back): ").strip().lower()
3793
+ rng_in = _safe_input("Peak X range (min max, 'current' for axes limits, q=back): ").strip().lower()
3763
3794
  if not rng_in or rng_in == 'q':
3764
3795
  break
3765
3796
  if rng_in == 'current':
@@ -3773,12 +3804,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
3773
3804
  if x_min > x_max:
3774
3805
  x_min, x_max = x_max, x_min
3775
3806
 
3776
- frac_in = input("Min relative peak height (0–1, default 0.1): ").strip()
3807
+ frac_in = _safe_input("Min relative peak height (0–1, default 0.1): ").strip()
3777
3808
  min_frac = float(frac_in) if frac_in else 0.1
3778
3809
  if min_frac < 0: min_frac = 0.0
3779
3810
  if min_frac > 1: min_frac = 1.0
3780
3811
 
3781
- swin = input("Smoothing window (odd int >=3, blank=none): ").strip()
3812
+ swin = _safe_input("Smoothing window (odd int >=3, blank=none): ").strip()
3782
3813
  if swin:
3783
3814
  try:
3784
3815
  win = int(swin)
batplot/modes.py CHANGED
@@ -280,8 +280,8 @@ def handle_cv_mode(args) -> int:
280
280
  ax.set_xlabel('Current (mA)', labelpad=8.0)
281
281
  ax.set_ylabel('Voltage (V)', labelpad=8.0)
282
282
  else:
283
- ax.set_xlabel('Voltage (V)', labelpad=8.0)
284
- ax.set_ylabel('Current (mA)', labelpad=8.0)
283
+ ax.set_xlabel('Voltage (V)', labelpad=8.0)
284
+ ax.set_ylabel('Current (mA)', labelpad=8.0)
285
285
  legend = ax.legend(title='Cycle')
286
286
  legend.get_title().set_fontsize('medium')
287
287
  # Adjust margins to prevent label clipping
@@ -642,8 +642,8 @@ def handle_gc_mode(args) -> int:
642
642
  ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
643
643
  linewidth=2.0, label=str(cyc), alpha=0.8)
644
644
  else:
645
- ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
646
- linewidth=2.0, label=str(cyc), alpha=0.8)
645
+ ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
646
+ linewidth=2.0, label=str(cyc), alpha=0.8)
647
647
  else:
648
648
  ln_c = None
649
649
  mask_d = (cyc_int == cyc) & discharge_mask
@@ -656,8 +656,8 @@ def handle_gc_mode(args) -> int:
656
656
  ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
657
657
  linewidth=2.0, label=lbl, alpha=0.8)
658
658
  else:
659
- ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
660
- linewidth=2.0, label=lbl, alpha=0.8)
659
+ ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
660
+ linewidth=2.0, label=lbl, alpha=0.8)
661
661
  else:
662
662
  ln_d = None
663
663
  cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
@@ -677,8 +677,8 @@ def handle_gc_mode(args) -> int:
677
677
  ln_c, = ax.plot(y_b, x_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
678
678
  linewidth=2.0, label=str(cyc), alpha=0.8)
679
679
  else:
680
- ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
681
- linewidth=2.0, label=str(cyc), alpha=0.8)
680
+ ln_c, = ax.plot(x_b, y_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
681
+ linewidth=2.0, label=str(cyc), alpha=0.8)
682
682
  ln_d = None
683
683
  if i < len(dch_blocks):
684
684
  a, b = dch_blocks[i]
@@ -690,8 +690,8 @@ def handle_gc_mode(args) -> int:
690
690
  ln_d, = ax.plot(yd_b, xd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
691
691
  linewidth=2.0, label=lbl, alpha=0.8)
692
692
  else:
693
- ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
694
- linewidth=2.0, label=lbl, alpha=0.8)
693
+ ln_d, = ax.plot(xd_b, yd_b, '-', color=base_colors[(cyc-1) % len(base_colors)],
694
+ linewidth=2.0, label=lbl, alpha=0.8)
695
695
  cycle_lines[cyc] = {"charge": ln_c, "discharge": ln_d}
696
696
 
697
697
  # Swap x and y if --ro flag is set
@@ -699,8 +699,8 @@ def handle_gc_mode(args) -> int:
699
699
  ax.set_xlabel('Voltage (V)', labelpad=8.0)
700
700
  ax.set_ylabel(x_label_gc, labelpad=8.0)
701
701
  else:
702
- ax.set_xlabel(x_label_gc, labelpad=8.0)
703
- ax.set_ylabel('Voltage (V)', labelpad=8.0)
702
+ ax.set_xlabel(x_label_gc, labelpad=8.0)
703
+ ax.set_ylabel('Voltage (V)', labelpad=8.0)
704
704
  legend = ax.legend(title='Cycle')
705
705
  legend.get_title().set_fontsize('medium')
706
706
  fig.subplots_adjust(left=0.12, right=0.95, top=0.88, bottom=0.15)