_getchar.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. """
  2. Unified getchar implementation for all platforms.
  3. Combines approaches from:
  4. - Textual (Unix/Linux): Copyright (c) 2023 Textualize Inc., MIT License
  5. - Click (Windows fallback): Copyright 2014 Pallets, BSD-3-Clause License
  6. """
  7. import os
  8. import sys
  9. from codecs import getincrementaldecoder
  10. from typing import Optional, TextIO
  11. def getchar() -> str:
  12. """
  13. Read input from stdin with support for longer pasted text.
  14. On Windows:
  15. - Uses msvcrt for native Windows console input
  16. - Handles special keys that send two-byte sequences
  17. - Reads up to 4096 characters for paste support
  18. On Unix/Linux:
  19. - Uses Textual's approach with manual termios configuration
  20. - Reads up to 4096 bytes with proper UTF-8 decoding
  21. - Provides fine-grained terminal control
  22. Returns:
  23. str: The input character(s) read from stdin
  24. Raises:
  25. KeyboardInterrupt: When CTRL+C is pressed
  26. """
  27. if sys.platform == "win32":
  28. # Windows implementation
  29. try:
  30. import msvcrt
  31. except ImportError:
  32. # Fallback if msvcrt is not available
  33. return sys.stdin.read(1)
  34. # Use getwch for Unicode support
  35. func = msvcrt.getwch # type: ignore
  36. # Read first character
  37. rv = func()
  38. # Check for special keys (they send two characters)
  39. if rv in ("\x00", "\xe0"):
  40. # Special key, read the second character
  41. rv += func()
  42. return rv
  43. # Check if more input is available (for paste support)
  44. chars = [rv]
  45. max_chars = 4096
  46. # Keep reading while characters are available
  47. while len(chars) < max_chars and msvcrt.kbhit(): # type: ignore
  48. next_char = func()
  49. # Handle special keys during paste
  50. if next_char in ("\x00", "\xe0"):
  51. # Stop here, let this be handled in next call
  52. break
  53. chars.append(next_char)
  54. # Check for CTRL+C
  55. if next_char == "\x03":
  56. raise KeyboardInterrupt()
  57. result = "".join(chars)
  58. # Check for CTRL+C in the full result
  59. if "\x03" in result:
  60. raise KeyboardInterrupt()
  61. return result
  62. else:
  63. # Unix/Linux implementation (Textual approach)
  64. import termios
  65. import tty
  66. f: Optional[TextIO] = None
  67. fd: int
  68. # Get the file descriptor
  69. if not sys.stdin.isatty():
  70. f = open("/dev/tty")
  71. fd = f.fileno()
  72. else:
  73. fd = sys.stdin.fileno()
  74. try:
  75. # Save current terminal settings
  76. attrs_before = termios.tcgetattr(fd)
  77. try:
  78. # Configure terminal settings (Textual-style)
  79. newattr = termios.tcgetattr(fd)
  80. # Patch LFLAG (local flags)
  81. # Disable:
  82. # - ECHO: Don't echo input characters
  83. # - ICANON: Disable canonical mode (line-by-line input)
  84. # - IEXTEN: Disable extended processing
  85. # - ISIG: Disable signal generation
  86. newattr[tty.LFLAG] &= ~(
  87. termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG
  88. )
  89. # Patch IFLAG (input flags)
  90. # Disable:
  91. # - IXON/IXOFF: XON/XOFF flow control
  92. # - ICRNL/INLCR/IGNCR: Various newline translations
  93. newattr[tty.IFLAG] &= ~(
  94. termios.IXON
  95. | termios.IXOFF
  96. | termios.ICRNL
  97. | termios.INLCR
  98. | termios.IGNCR
  99. )
  100. # Set VMIN to 1 (minimum number of characters to read)
  101. # This ensures we get at least 1 character
  102. newattr[tty.CC][termios.VMIN] = 1
  103. # Apply the new terminal settings
  104. termios.tcsetattr(fd, termios.TCSANOW, newattr)
  105. # Read up to 4096 bytes (same as Textual)
  106. raw_data = os.read(fd, 1024 * 4)
  107. # Use incremental UTF-8 decoder for proper Unicode handling
  108. decoder = getincrementaldecoder("utf-8")()
  109. result = decoder.decode(raw_data, final=True)
  110. # Check for CTRL+C (ASCII 3)
  111. if "\x03" in result:
  112. raise KeyboardInterrupt()
  113. return result
  114. finally:
  115. # Restore original terminal settings
  116. termios.tcsetattr(fd, termios.TCSANOW, attrs_before)
  117. sys.stdout.flush()
  118. if f is not None:
  119. f.close()
  120. except termios.error:
  121. # If we can't control the terminal, fall back to simple read
  122. return sys.stdin.read(1)