Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions LICENSE-THIRD-PARTY
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Third-Party Licenses
=====================

This project includes code derived from the following third-party projects:

Click
-----
Copyright 2014 Pallets

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The following files contain code derived from Click:
- src/rich_toolkit/_getchar.py


Textual
-------
Copyright (c) 2023 Textualize Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

The following files contain code derived from Textual:
- src/rich_toolkit/_getchar_textual.py
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
release type: minor
---

This release increases the paste buffer from 32 to 4096 characters, enabling users to paste longer text into input fields.

It also adds full Windows compatibility with proper special key handling and fixes how password fields to always show asterisks.
2 changes: 1 addition & 1 deletion examples/create-astro.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import random
import time

from rich_toolkit import RichToolkit, RichToolkitTheme
from rich_toolkit import RichToolkit
from rich_toolkit.styles.border import BorderedStyle
from rich_toolkit.styles.fancy import FancyStyle
from rich_toolkit.styles.tagged import TaggedStyle
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
)
app.print_line()

app.input("What is the name of your project?", tag="name")
app.input("What is the name of your project?", tag="name", required=True)
app.print_line()

app.input("What's your password?", tag="password", password=True)
Expand Down
3 changes: 1 addition & 2 deletions examples/progress_log.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import random
import time
from typing import Any, Dict, Generator, List, Tuple
from typing import Generator

import httpx
from rich.segment import Segment
from rich.text import Text

from rich_toolkit import RichToolkit
Expand Down
157 changes: 157 additions & 0 deletions src/rich_toolkit/_getchar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""
Unified getchar implementation for all platforms.

Combines approaches from:
- Textual (Unix/Linux): Copyright (c) 2023 Textualize Inc., MIT License
- Click (Windows fallback): Copyright 2014 Pallets, BSD-3-Clause License
"""

import os
import sys
from codecs import getincrementaldecoder
from typing import Optional, TextIO


def getchar() -> str:
"""
Read input from stdin with support for longer pasted text.

On Windows:
- Uses msvcrt for native Windows console input
- Handles special keys that send two-byte sequences
- Reads up to 4096 characters for paste support

On Unix/Linux:
- Uses Textual's approach with manual termios configuration
- Reads up to 4096 bytes with proper UTF-8 decoding
- Provides fine-grained terminal control

Returns:
str: The input character(s) read from stdin

Raises:
KeyboardInterrupt: When CTRL+C is pressed
"""
if sys.platform == "win32":
# Windows implementation
try:
import msvcrt
except ImportError:
# Fallback if msvcrt is not available
return sys.stdin.read(1)

# Use getwch for Unicode support
func = msvcrt.getwch # type: ignore

# Read first character
rv = func()

# Check for special keys (they send two characters)
if rv in ("\x00", "\xe0"):
# Special key, read the second character
rv += func()
return rv

# Check if more input is available (for paste support)
chars = [rv]
max_chars = 4096

# Keep reading while characters are available
while len(chars) < max_chars and msvcrt.kbhit(): # type: ignore
next_char = func()

# Handle special keys during paste
if next_char in ("\x00", "\xe0"):
# Stop here, let this be handled in next call
break

chars.append(next_char)

# Check for CTRL+C
if next_char == "\x03":
raise KeyboardInterrupt()

result = "".join(chars)

# Check for CTRL+C in the full result
if "\x03" in result:
raise KeyboardInterrupt()

return result

else:
# Unix/Linux implementation (Textual approach)
import termios
import tty

f: Optional[TextIO] = None
fd: int

# Get the file descriptor
if not sys.stdin.isatty():
f = open("/dev/tty")
fd = f.fileno()
else:
fd = sys.stdin.fileno()

try:
# Save current terminal settings
attrs_before = termios.tcgetattr(fd)

try:
# Configure terminal settings (Textual-style)
newattr = termios.tcgetattr(fd)

# Patch LFLAG (local flags)
# Disable:
# - ECHO: Don't echo input characters
# - ICANON: Disable canonical mode (line-by-line input)
# - IEXTEN: Disable extended processing
# - ISIG: Disable signal generation
newattr[tty.LFLAG] &= ~(
termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG
)

# Patch IFLAG (input flags)
# Disable:
# - IXON/IXOFF: XON/XOFF flow control
# - ICRNL/INLCR/IGNCR: Various newline translations
newattr[tty.IFLAG] &= ~(
termios.IXON
| termios.IXOFF
| termios.ICRNL
| termios.INLCR
| termios.IGNCR
)

# Set VMIN to 1 (minimum number of characters to read)
# This ensures we get at least 1 character
newattr[tty.CC][termios.VMIN] = 1

# Apply the new terminal settings
termios.tcsetattr(fd, termios.TCSANOW, newattr)

# Read up to 4096 bytes (same as Textual)
raw_data = os.read(fd, 1024 * 4)

# Use incremental UTF-8 decoder for proper Unicode handling
decoder = getincrementaldecoder("utf-8")()
result = decoder.decode(raw_data, final=True)

# Check for CTRL+C (ASCII 3)
if "\x03" in result:
raise KeyboardInterrupt()

return result

finally:
# Restore original terminal settings
termios.tcsetattr(fd, termios.TCSANOW, attrs_before)
sys.stdout.flush()

if f is not None:
f.close()

except termios.error:
# If we can't control the terminal, fall back to simple read
return sys.stdin.read(1)
65 changes: 52 additions & 13 deletions src/rich_toolkit/_input_handler.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
"""Unified input handler for all platforms."""

import string
import sys


class TextInputHandler:
DOWN_KEY = "\x1b[B"
UP_KEY = "\x1b[A"
LEFT_KEY = "\x1b[D"
RIGHT_KEY = "\x1b[C"
BACKSPACE_KEY = "\x7f"
DELETE_KEY = "\x1b[3~"
TAB_KEY = "\t"
SHIFT_TAB_KEY = "\x1b[Z"
ENTER_KEY = "\r"
"""Input handler with platform-specific key code support."""

# Platform-specific key codes
if sys.platform == "win32":
# Windows uses \xe0 prefix for special keys when using msvcrt.getwch
DOWN_KEY = "\xe0P" # Down arrow
UP_KEY = "\xe0H" # Up arrow
LEFT_KEY = "\xe0K" # Left arrow
RIGHT_KEY = "\xe0M" # Right arrow
DELETE_KEY = "\xe0S" # Delete key
BACKSPACE_KEY = "\x08" # Backspace
TAB_KEY = "\t"
SHIFT_TAB_KEY = "\x00\x0f" # Shift+Tab
ENTER_KEY = "\r"

# Alternative codes that might be sent
ALT_BACKSPACE = "\x7f"
ALT_DELETE = "\x00S"
else:
# Unix/Linux key codes (ANSI escape sequences)
DOWN_KEY = "\x1b[B"
UP_KEY = "\x1b[A"
LEFT_KEY = "\x1b[D"
RIGHT_KEY = "\x1b[C"
BACKSPACE_KEY = "\x7f"
DELETE_KEY = "\x1b[3~"
TAB_KEY = "\t"
SHIFT_TAB_KEY = "\x1b[Z"
ENTER_KEY = "\r"

# Alternative codes
ALT_BACKSPACE = "\x08"
ALT_DELETE = None

def __init__(self):
self.text = ""
Expand All @@ -27,22 +54,28 @@ def _insert_char(self, char: str) -> None:
self._move_cursor_right()

def _delete_char(self) -> None:
"""Delete character before cursor (backspace)."""
if self.cursor_left == 0:
return

self.text = self.text[: self.cursor_left - 1] + self.text[self.cursor_left :]
self._move_cursor_left()

def _delete_forward(self) -> None:
"""Delete character at cursor (delete key)."""
if self.cursor_left == len(self.text):
return

self.text = self.text[: self.cursor_left] + self.text[self.cursor_left + 1 :]

def handle_key(self, key: str) -> None:
if key == self.BACKSPACE_KEY:
# Handle backspace (both possible codes)
if key == self.BACKSPACE_KEY or (
self.ALT_BACKSPACE and key == self.ALT_BACKSPACE
):
self._delete_char()
elif key == self.DELETE_KEY:
# Handle delete key
elif key == self.DELETE_KEY or (self.ALT_DELETE and key == self.ALT_DELETE):
self._delete_forward()
elif key == self.LEFT_KEY:
self._move_cursor_left()
Expand All @@ -57,8 +90,14 @@ def handle_key(self, key: str) -> None:
):
pass
else:
# even if we call this handle key, in some cases we might receive multiple keys
# at once
# Handle regular text input
# Special keys on Windows start with \x00 or \xe0
if sys.platform == "win32" and key and key[0] in ("\x00", "\xe0"):
# Skip special key sequences
return

# Even if we call this handle_key, in some cases we might receive
# multiple keys at once (e.g., during paste operations)
for char in key:
if char in string.printable:
self._insert_char(char)
5 changes: 3 additions & 2 deletions src/rich_toolkit/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple

import click
from rich.control import Control, ControlType
from rich.live_render import LiveRender
from rich.segment import Segment

from ._getchar import getchar
from ._input_handler import TextInputHandler

from .element import Element

if TYPE_CHECKING:
Expand Down Expand Up @@ -169,7 +170,7 @@ def run(self):

while True:
try:
key = click.getchar()
key = getchar()

self.previous_element_index = self.active_element_index

Expand Down
1 change: 1 addition & 0 deletions src/rich_toolkit/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import TYPE_CHECKING, Any, Optional

from ._input_handler import TextInputHandler

from .element import CursorOffset, Element

if TYPE_CHECKING:
Expand Down
Loading