urkit 0.4.2__tar.gz → 0.4.4__tar.gz

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.
Files changed (42) hide show
  1. {urkit-0.4.2 → urkit-0.4.4}/PKG-INFO +1 -1
  2. {urkit-0.4.2 → urkit-0.4.4}/pyproject.toml +1 -1
  3. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/__init__.py +1 -1
  4. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/points.py +21 -11
  5. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/teach.py +89 -54
  6. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/terminal.py +65 -16
  7. {urkit-0.4.2 → urkit-0.4.4}/src/urkit.egg-info/PKG-INFO +1 -1
  8. {urkit-0.4.2 → urkit-0.4.4}/README.md +0 -0
  9. {urkit-0.4.2 → urkit-0.4.4}/setup.cfg +0 -0
  10. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/__main__.py +0 -0
  11. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/__init__.py +0 -0
  12. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/colors.py +0 -0
  13. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/connection_monitor.py +0 -0
  14. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/cli/init.py +0 -0
  15. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/config.py +0 -0
  16. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/connection.py +0 -0
  17. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/exceptions.py +0 -0
  18. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/geometry.py +0 -0
  19. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/gripper/__init__.py +0 -0
  20. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/gripper/base.py +0 -0
  21. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/gripper/digital.py +0 -0
  22. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/gripper/presets.py +0 -0
  23. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/gripper/robotiq.py +0 -0
  24. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/gripper/robotiq_preamble.py +0 -0
  25. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/io.py +0 -0
  26. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/motion.py +0 -0
  27. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/points.py +0 -0
  28. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/robot.py +0 -0
  29. {urkit-0.4.2 → urkit-0.4.4}/src/urkit/telemetry.py +0 -0
  30. {urkit-0.4.2 → urkit-0.4.4}/src/urkit.egg-info/SOURCES.txt +0 -0
  31. {urkit-0.4.2 → urkit-0.4.4}/src/urkit.egg-info/dependency_links.txt +0 -0
  32. {urkit-0.4.2 → urkit-0.4.4}/src/urkit.egg-info/entry_points.txt +0 -0
  33. {urkit-0.4.2 → urkit-0.4.4}/src/urkit.egg-info/requires.txt +0 -0
  34. {urkit-0.4.2 → urkit-0.4.4}/src/urkit.egg-info/top_level.txt +0 -0
  35. {urkit-0.4.2 → urkit-0.4.4}/tests/test_exceptions.py +0 -0
  36. {urkit-0.4.2 → urkit-0.4.4}/tests/test_geometry.py +0 -0
  37. {urkit-0.4.2 → urkit-0.4.4}/tests/test_gripper.py +0 -0
  38. {urkit-0.4.2 → urkit-0.4.4}/tests/test_gripper_factory.py +0 -0
  39. {urkit-0.4.2 → urkit-0.4.4}/tests/test_gripper_presets.py +0 -0
  40. {urkit-0.4.2 → urkit-0.4.4}/tests/test_move_sequence.py +0 -0
  41. {urkit-0.4.2 → urkit-0.4.4}/tests/test_points.py +0 -0
  42. {urkit-0.4.2 → urkit-0.4.4}/tests/test_robot_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "urkit"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Universal Robots e-Series control toolkit built on ur_rtde"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -24,7 +24,7 @@ Quick start::
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- __version__ = "0.4.2"
27
+ __version__ = "0.4.4"
28
28
 
29
29
 
30
30
  from urkit.exceptions import (
@@ -252,25 +252,35 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
252
252
  # that arrives late; wait a bit before treating it as quit.
253
253
  if text == "\x1b":
254
254
  text = text + read_burst(0.2).decode("ascii", errors="replace")
255
- if text.endswith("[A"): # Up arrow - scroll up
255
+ if "[A" in text: # Up arrow - scroll up
256
256
  scroll = max(0, scroll - 1)
257
257
  needs_redraw = True
258
- elif text.endswith("[B"): # Down arrow - scroll down
258
+ elif "[B" in text: # Down arrow - scroll down
259
259
  filtered = [p for p in all_points_sorted if filter_str == "" or filter_str.lower() in p.lower()]
260
260
  scroll = min(len(filtered) - 1, scroll + 1) if filtered else 0
261
261
  needs_redraw = True
262
262
  else:
263
263
  # Just ESC (or an unrecognized sequence) — quit
264
264
  break
265
- elif text == "\x7f" or text == "\x08": # Backspace
266
- if filter_str:
267
- filter_str = filter_str[:-1]
268
- scroll = 0
269
- needs_redraw = True
270
- elif text.isprintable():
271
- filter_str += text
272
- scroll = 0
273
- needs_redraw = True
265
+ else:
266
+ # A burst can mix printable chars and backspaces
267
+ # (fast typing), so process sequentially.
268
+ quit_requested = False
269
+ for ch in text:
270
+ if ch == "\x03": # Ctrl+C (raw byte on Windows)
271
+ quit_requested = True
272
+ break
273
+ elif ch == "\x7f" or ch == "\x08": # Backspace
274
+ if filter_str:
275
+ filter_str = filter_str[:-1]
276
+ scroll = 0
277
+ needs_redraw = True
278
+ elif ch.isprintable():
279
+ filter_str += ch
280
+ scroll = 0
281
+ needs_redraw = True
282
+ if quit_requested:
283
+ break
274
284
 
275
285
  except KeyboardInterrupt:
276
286
  # Ctrl+C — exit gracefully without traceback
@@ -583,36 +583,44 @@ def _filter_select_points(
583
583
  raw.enable()
584
584
 
585
585
  try:
586
+ needs_redraw = True
586
587
  while True:
587
- # Clear screen and draw
588
- sys.stdout.write("\033[2J\033[1;1H")
589
- sys.stdout.write(cyan(f" === {title} ===") + "\n")
590
- sys.stdout.write(dim(" Arrows navigate · Type to filter · Enter select · ESC cancel") + "\n\n")
591
- sys.stdout.write(f" {blue('Search:')} {filter_str}\n")
592
- sys.stdout.write(" " + dim("─" * 60) + "\n")
593
-
594
- filtered = [p for p in all_points if filter_str == "" or filter_str.lower() in p.lower()]
595
-
596
- # Clamp cursor to valid range
597
- if not filtered:
598
- cursor = 0
599
- elif cursor >= len(filtered):
600
- cursor = len(filtered) - 1
601
-
602
- if not filtered:
603
- sys.stdout.write(yellow(f" No points match '{filter_str}'") + "\n")
604
- sys.stdout.write("\n " + dim("Type to search for points...") + "\n")
588
+ # Redraw only when state changed. Redrawing every poll
589
+ # iteration (10x/s) is invisible on Linux terminals but
590
+ # flickers visibly on the Windows console.
591
+ if needs_redraw:
592
+ # Clear screen and draw
593
+ sys.stdout.write("\033[2J\033[1;1H")
594
+ sys.stdout.write(cyan(f" === {title} ===") + "\n")
595
+ sys.stdout.write(dim(" Arrows navigate · Type to filter · Enter select · ESC cancel") + "\n\n")
596
+ sys.stdout.write(f" {blue('Search:')} {filter_str}\n")
597
+ sys.stdout.write(" " + dim("─" * 60) + "\n")
598
+
599
+ filtered = [p for p in all_points if filter_str == "" or filter_str.lower() in p.lower()]
600
+
601
+ # Clamp cursor to valid range
602
+ if not filtered:
603
+ cursor = 0
604
+ elif cursor >= len(filtered):
605
+ cursor = len(filtered) - 1
606
+
607
+ if not filtered:
608
+ sys.stdout.write(yellow(f" No points match '{filter_str}'") + "\n")
609
+ sys.stdout.write("\n " + dim("Type to search for points...") + "\n")
610
+ else:
611
+ sys.stdout.write(blue(" Matching points:") + "\n")
612
+ for i, p in enumerate(filtered):
613
+ marker = green("►") if i == cursor else " "
614
+ highlighted = _highlight_match(p, filter_str)
615
+ sys.stdout.write(f" {marker} {highlighted}\n")
616
+
617
+ sel = green("'" + filtered[cursor] + "'")
618
+ sys.stdout.write(f"\n {dim('Enter')} to select {sel}\n")
619
+
620
+ sys.stdout.flush()
621
+ needs_redraw = False
605
622
  else:
606
- sys.stdout.write(blue(" Matching points:") + "\n")
607
- for i, p in enumerate(filtered):
608
- marker = green("►") if i == cursor else " "
609
- highlighted = _highlight_match(p, filter_str)
610
- sys.stdout.write(f" {marker} {highlighted}\n")
611
-
612
- sel = green("'" + filtered[cursor] + "'")
613
- sys.stdout.write(f"\n {dim('Enter')} to select {sel}\n")
614
-
615
- sys.stdout.flush()
623
+ filtered = [p for p in all_points if filter_str == "" or filter_str.lower() in p.lower()]
616
624
 
617
625
  # Wait for input, then read the full burst (e.g. the
618
626
  # 3-byte \x1b[A arrow-key sequence) in one go.
@@ -628,6 +636,7 @@ def _filter_select_points(
628
636
  # Decode and parse sequentially, handling multi-byte escape sequences
629
637
  text = raw_bytes.decode("ascii", errors="replace")
630
638
  i = 0
639
+ changed = False
631
640
  while i < len(text):
632
641
  ch = text[i]
633
642
 
@@ -637,8 +646,10 @@ def _filter_select_points(
637
646
  key = text[i + 2]
638
647
  if key == "A":
639
648
  cursor = max(0, cursor - 1)
649
+ changed = True
640
650
  elif key == "B":
641
651
  cursor = min(len(filtered) - 1, cursor + 1)
652
+ changed = True
642
653
  # Unrecognized CSI sequences are ignored
643
654
  i += 3
644
655
  continue
@@ -652,12 +663,15 @@ def _filter_select_points(
652
663
  return None
653
664
  elif ch == "\x08" or ch == "\x7f":
654
665
  filter_str = filter_str[:-1]
666
+ changed = True
655
667
  elif ch == "\x03":
656
668
  return None
657
669
  elif ch.isprintable() and len(filter_str) < 30:
658
670
  filter_str += ch
671
+ changed = True
659
672
 
660
673
  i += 1
674
+ needs_redraw = changed
661
675
  finally:
662
676
  raw.disable()
663
677
 
@@ -869,19 +883,25 @@ def _submenu_orient_tcp(
869
883
  raw.enable()
870
884
 
871
885
  try:
886
+ needs_redraw = True
872
887
  while True:
873
- sys.stdout.write("\033[2J\033[1;1H")
874
- sys.stdout.write(cyan(" === ORIENT TCP ===") + "\n")
875
- sys.stdout.write(dim(" Arrows navigate · Enter select · ESC cancel") + "\n\n")
876
- sys.stdout.write(blue(" Point tool Z toward:") + "\n")
877
- sys.stdout.write(" " + dim("─" * 60) + "\n")
878
-
879
- for i, (label, _direction) in enumerate(_TCP_ORIENT_OPTIONS):
880
- marker = green("►") if i == cursor else " "
881
- sys.stdout.write(f" {marker} {label}\n")
888
+ # Redraw only when the cursor moved. Redrawing every poll
889
+ # iteration (10x/s) is invisible on Linux terminals but
890
+ # flickers visibly on the Windows console.
891
+ if needs_redraw:
892
+ sys.stdout.write("\033[2J\033[1;1H")
893
+ sys.stdout.write(cyan(" === ORIENT TCP ===") + "\n")
894
+ sys.stdout.write(dim(" Arrows navigate · Enter select · ESC cancel") + "\n\n")
895
+ sys.stdout.write(blue(" Point tool Z toward:") + "\n")
896
+ sys.stdout.write(" " + dim("─" * 60) + "\n")
897
+
898
+ for i, (label, _direction) in enumerate(_TCP_ORIENT_OPTIONS):
899
+ marker = green("►") if i == cursor else " "
900
+ sys.stdout.write(f" {marker} {label}\n")
882
901
 
883
- sys.stdout.write(f"\n {dim('Enter')} to orient {green(_TCP_ORIENT_OPTIONS[cursor][0])}\n")
884
- sys.stdout.flush()
902
+ sys.stdout.write(f"\n {dim('Enter')} to orient {green(_TCP_ORIENT_OPTIONS[cursor][0])}\n")
903
+ sys.stdout.flush()
904
+ needs_redraw = False
885
905
 
886
906
  raw_bytes = read_burst(0.1)
887
907
  if not raw_bytes:
@@ -894,6 +914,7 @@ def _submenu_orient_tcp(
894
914
  text = raw_bytes.decode("ascii", errors="replace")
895
915
  i = 0
896
916
  selected = None
917
+ moved = False
897
918
  while i < len(text):
898
919
  ch = text[i]
899
920
  if ch == "\x1b":
@@ -901,8 +922,10 @@ def _submenu_orient_tcp(
901
922
  key = text[i + 2]
902
923
  if key == "A":
903
924
  cursor = max(0, cursor - 1)
925
+ moved = True
904
926
  elif key == "B":
905
927
  cursor = min(len(_TCP_ORIENT_OPTIONS) - 1, cursor + 1)
928
+ moved = True
906
929
  i += 3
907
930
  continue
908
931
  else:
@@ -913,6 +936,7 @@ def _submenu_orient_tcp(
913
936
  return None
914
937
  i += 1
915
938
 
939
+ needs_redraw = moved
916
940
  if selected is not None:
917
941
  break
918
942
  finally:
@@ -975,22 +999,28 @@ def _submenu_freedrive_axes(
975
999
  raw.enable()
976
1000
 
977
1001
  try:
1002
+ needs_redraw = True
978
1003
  while True:
979
- sys.stdout.write("\033[2J\033[1;1H")
980
- sys.stdout.write(cyan(" === FREEDRIVE AXES ===") + "\n")
981
- sys.stdout.write(dim(" Arrows navigate · Space toggle · Enter apply · ESC cancel") + "\n\n")
982
- sys.stdout.write(" " + dim("─" * 40) + "\n")
983
-
984
- for i, label in enumerate(_FD_AXIS_LABELS):
985
- marker = green("►") if i == cursor else " "
986
- state_str = green("ON") if axes[i] else red("OFF")
987
- sys.stdout.write(f" {marker} {label:<6} {state_str}\n")
988
-
989
- sys.stdout.write(" " + dim("" * 40) + "\n")
990
- active = sum(axes)
991
- sys.stdout.write(f"\n {blue('Active axes:')} {green(str(active))} / 6\n")
992
- sys.stdout.write(f" {dim('Enter')} to apply · {dim('ESC')} to cancel\n")
993
- sys.stdout.flush()
1004
+ # Redraw only when the cursor moved or an axis toggled.
1005
+ # Redrawing every poll iteration (10x/s) is invisible on
1006
+ # Linux terminals but flickers visibly on the Windows console.
1007
+ if needs_redraw:
1008
+ sys.stdout.write("\033[2J\033[1;1H")
1009
+ sys.stdout.write(cyan(" === FREEDRIVE AXES ===") + "\n")
1010
+ sys.stdout.write(dim(" Arrows navigate · Space toggle · Enter apply · ESC cancel") + "\n\n")
1011
+ sys.stdout.write(" " + dim("" * 40) + "\n")
1012
+
1013
+ for i, label in enumerate(_FD_AXIS_LABELS):
1014
+ marker = green("") if i == cursor else " "
1015
+ state_str = green("ON") if axes[i] else red("OFF")
1016
+ sys.stdout.write(f" {marker} {label:<6} {state_str}\n")
1017
+
1018
+ sys.stdout.write(" " + dim("─" * 40) + "\n")
1019
+ active = sum(axes)
1020
+ sys.stdout.write(f"\n {blue('Active axes:')} {green(str(active))} / 6\n")
1021
+ sys.stdout.write(f" {dim('Enter')} to apply · {dim('ESC')} to cancel\n")
1022
+ sys.stdout.flush()
1023
+ needs_redraw = False
994
1024
 
995
1025
  raw_bytes = read_burst(0.1)
996
1026
  if not raw_bytes:
@@ -998,6 +1028,7 @@ def _submenu_freedrive_axes(
998
1028
  text = raw_bytes.decode("ascii", errors="replace")
999
1029
  i = 0
1000
1030
  applied = False
1031
+ changed = False
1001
1032
  while i < len(text):
1002
1033
  ch = text[i]
1003
1034
  if ch == "\x1b":
@@ -1005,20 +1036,24 @@ def _submenu_freedrive_axes(
1005
1036
  key = text[i + 2]
1006
1037
  if key == "A":
1007
1038
  cursor = max(0, cursor - 1)
1039
+ changed = True
1008
1040
  elif key == "B":
1009
1041
  cursor = min(5, cursor + 1)
1042
+ changed = True
1010
1043
  i += 3
1011
1044
  continue
1012
1045
  else:
1013
1046
  return # ESC → cancel
1014
1047
  if ch == " ":
1015
1048
  axes[cursor] = 1 - axes[cursor]
1049
+ changed = True
1016
1050
  elif ch == "\n" or ch == "\r":
1017
1051
  applied = True
1018
1052
  elif ch == "\x03":
1019
1053
  return
1020
1054
  i += 1
1021
1055
 
1056
+ needs_redraw = changed
1022
1057
  if applied:
1023
1058
  break
1024
1059
  finally:
@@ -9,8 +9,8 @@ The CLIs need three things from the platform:
9
9
  3. On Windows only: ANSI escape processing on stdout so colors and
10
10
  cursor codes render.
11
11
 
12
- Unix backend: termios + tty + select, exactly the syscalls the CLIs
13
- used to issue directly.
12
+ Unix backend: termios + select, the same syscalls the CLIs used to
13
+ issue directly.
14
14
 
15
15
  Windows backend: msvcrt with console input mode changed via kernel32.
16
16
  The console does not emit ANSI sequences for special keys, so the
@@ -38,13 +38,21 @@ if sys.platform == "win32":
38
38
  else:
39
39
  import select
40
40
  import termios
41
- import tty
42
41
 
43
42
  if sys.platform == "win32":
44
43
  # CDLL is used instead of WinDLL because ctypes.WinDLL is only
45
44
  # exposed in the Windows type stubs; on 64-bit Windows both use
46
45
  # the same calling convention.
47
46
  _kernel32 = ctypes.CDLL("kernel32", use_last_error=True)
47
+ # Declare prototypes. The default restype is c_int, which would
48
+ # truncate 64-bit HANDLE values from GetStdHandle.
49
+ _kernel32.GetStdHandle.argtypes = [ctypes.c_int]
50
+ _kernel32.GetStdHandle.restype = ctypes.c_void_p
51
+ _kernel32.GetConsoleMode.argtypes = [
52
+ ctypes.c_void_p,
53
+ ctypes.POINTER(ctypes.c_uint32),
54
+ ]
55
+ _kernel32.SetConsoleMode.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
48
56
  _STD_INPUT_HANDLE = -10
49
57
  _STD_OUTPUT_HANDLE = -11
50
58
  _ENABLE_PROCESSED_INPUT = 0x0001
@@ -52,9 +60,10 @@ if sys.platform == "win32":
52
60
  _ENABLE_ECHO_INPUT = 0x0004
53
61
  _ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
54
62
 
55
- # msvcrt extended-key scan codes (after the 0xE0/0x00 prefix byte)
56
- # mapped to the ANSI CSI sequences a Unix terminal emits for the
57
- # same key.
63
+ # msvcrt extended-key scan codes after the 0xE0 prefix byte, mapped
64
+ # to the ANSI CSI sequences a Unix terminal emits for the same key.
65
+ # Keys carrying the 0x00 prefix are not translated: their scan codes
66
+ # overlap with arrow keys, and the CLIs don't use them.
58
67
  _EXTENDED_KEYS: dict[int, str] = {
59
68
  0x48: "\x1b[A", # Up
60
69
  0x50: "\x1b[B", # Down
@@ -68,18 +77,35 @@ if sys.platform == "win32":
68
77
  0x53: "\x1b[3~", # Delete
69
78
  }
70
79
 
80
+ _WIN_ERROR_TEXT = {
81
+ 5: "access denied",
82
+ 6: "invalid handle",
83
+ 87: "invalid parameter",
84
+ 1225: "session disconnected",
85
+ }
86
+
87
+ def _win_error_detail() -> str:
88
+ code = ctypes.get_last_error()
89
+ return f"Win32 error {code} ({_WIN_ERROR_TEXT.get(code, 'unknown')})"
90
+
71
91
  def _console_mode(handle: int) -> int:
72
92
  mode = ctypes.c_uint32()
73
93
  if not _kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
74
94
  raise OSError(
75
- "GetConsoleMode failed (stdin is not a console). "
76
- "Run from an interactive shell."
95
+ f"GetConsoleMode failed ({_win_error_detail()}). stdin is "
96
+ "not a usable console handle; run urkit from an "
97
+ "interactive shell such as cmd or PowerShell."
77
98
  )
78
99
  return mode.value
79
100
 
80
- def _set_console_mode(handle: int, mode: int) -> None:
101
+ def _set_console_mode(handle: int, mode: int, context: str) -> None:
81
102
  if not _kernel32.SetConsoleMode(handle, mode):
82
- raise OSError("SetConsoleMode failed")
103
+ raise OSError(
104
+ f"SetConsoleMode failed while {context} "
105
+ f"({_win_error_detail()}, requested mode "
106
+ f"0x{mode:08x}). The Windows console refused the "
107
+ "terminal mode change."
108
+ )
83
109
 
84
110
  def _enable_ansi_output() -> None:
85
111
  """Enable ANSI escape processing on stdout.
@@ -92,7 +118,9 @@ if sys.platform == "win32":
92
118
  handle = _kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
93
119
  mode = _console_mode(handle)
94
120
  _set_console_mode(
95
- handle, mode | _ENABLE_VIRTUAL_TERMINAL_PROCESSING
121
+ handle,
122
+ mode | _ENABLE_VIRTUAL_TERMINAL_PROCESSING,
123
+ "enabling ANSI output",
96
124
  )
97
125
  except OSError:
98
126
  pass
@@ -138,10 +166,18 @@ class RawTerminal:
138
166
  return
139
167
  if sys.platform == "win32":
140
168
  self._stdin_handle = _kernel32.GetStdHandle(_STD_INPUT_HANDLE)
169
+ if not self._stdin_handle:
170
+ raise OSError(
171
+ "stdin is not attached to a Windows console; run "
172
+ "urkit from an interactive shell such as cmd or "
173
+ "PowerShell."
174
+ )
141
175
  self._old_mode = _console_mode(self._stdin_handle)
142
176
  # Disable processed input, line input, and echo. Ctrl+C
143
177
  # arrives as raw byte 0x03 (handled by the CLIs) instead
144
178
  # of KeyboardInterrupt, which would skip terminal restore.
179
+ # Echo is always off: some consoles refuse raw mode with
180
+ # echo still enabled, and the CLIs render input themselves.
145
181
  _set_console_mode(
146
182
  self._stdin_handle,
147
183
  self._old_mode
@@ -150,11 +186,17 @@ class RawTerminal:
150
186
  | _ENABLE_LINE_INPUT
151
187
  | _ENABLE_ECHO_INPUT
152
188
  ),
189
+ "entering raw mode",
153
190
  )
154
191
  _enable_ansi_output()
155
192
  else:
156
193
  self._old_settings = termios.tcgetattr(sys.stdin)
157
- tty.setcbreak(sys.stdin.fileno())
194
+ # No canonical mode, no echo; ISIG stays on so Ctrl+C
195
+ # still raises KeyboardInterrupt. Same state as the
196
+ # pre-shim menus and input prompts.
197
+ settings = termios.tcgetattr(sys.stdin)
198
+ settings[3] = settings[3] & ~(termios.ICANON | termios.ECHO)
199
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, settings)
158
200
  self._enabled = True
159
201
 
160
202
  def disable(self) -> None:
@@ -164,7 +206,9 @@ class RawTerminal:
164
206
  if sys.platform == "win32":
165
207
  if self._stdin_handle is None or self._old_mode is None:
166
208
  raise RuntimeError("RawTerminal disabled before enable()")
167
- _set_console_mode(self._stdin_handle, self._old_mode)
209
+ _set_console_mode(
210
+ self._stdin_handle, self._old_mode, "restoring terminal"
211
+ )
168
212
  else:
169
213
  if self._old_settings is None:
170
214
  raise RuntimeError("RawTerminal disabled before enable()")
@@ -188,13 +232,18 @@ def _win_read_pending() -> bytes:
188
232
  out = bytearray()
189
233
  while msvcrt.kbhit():
190
234
  byte = msvcrt.getch()
191
- if byte in (b"\xe0", b"\x00"):
192
- code = msvcrt.getch()
235
+ if byte == b"\xe0":
236
+ # getch() returns bytes; the scan code dict is keyed
237
+ # by int, so take the single byte's ordinal
238
+ code = msvcrt.getch()[0]
193
239
  seq = _EXTENDED_KEYS.get(code)
194
240
  if seq is None:
195
- # Unrecognized function key (F1-F12 etc.): drop it
241
+ # Unrecognized extended key: drop it
196
242
  continue
197
243
  out += seq.encode("ascii")
244
+ elif byte == b"\x00":
245
+ # Untranslated extended prefix: consume the scan code
246
+ msvcrt.getch()
198
247
  else:
199
248
  out += byte
200
249
  return bytes(out)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes