repl-toolkit 1.2.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.
@@ -0,0 +1,453 @@
1
+ """
2
+ Environment variable and shell command expansion completer.
3
+ """
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from typing import Iterable, List, Optional
9
+
10
+ from prompt_toolkit.completion import Completer, Completion
11
+ from prompt_toolkit.document import Document
12
+ from prompt_toolkit.formatted_text import FormattedText
13
+
14
+
15
+ class ShellExpansionCompleter(Completer):
16
+ """
17
+ Completer that expands environment variables and executes shell commands.
18
+
19
+ Supports two patterns:
20
+ - ${VAR_NAME}: Expands to environment variable value
21
+ - $(command): Executes shell command and shows output
22
+
23
+ For multi-line command output, offers:
24
+ - ALL: Complete with all lines (always includes full output)
25
+ - Individual lines: Select specific line (always includes full line content)
26
+
27
+ Display limits affect only the completion menu appearance, not the inserted content.
28
+
29
+ This class is designed to be extensible. Override these public methods to customize:
30
+ - execute_command(): Custom command execution logic
31
+ - process_command_output(): Transform command output before display
32
+ - filter_lines(): Custom line filtering
33
+ - format_command_completion(): Customize command completion display
34
+ - format_variable_completion(): Customize variable completion display
35
+ - truncate_display(): Custom truncation logic
36
+
37
+ Security: Commands only execute when user presses Tab (not automatically).
38
+
39
+ Args:
40
+ timeout: Command execution timeout in seconds (default: 2.0)
41
+ multiline_all: Include "ALL" option for multi-line output (default: True)
42
+ max_lines: Maximum lines to show in completion menu (default: 50)
43
+ ALL option always includes full output regardless of this limit
44
+ max_display_length: Maximum line length in completion menu (default: 80)
45
+ Actual completion text is never truncated
46
+
47
+ Example:
48
+ >>> completer = ShellExpansionCompleter()
49
+ >>> # User types: Hello ${USER}
50
+ >>> # Press Tab: Shows username completion
51
+ >>> # User types: Files: $(ls)
52
+ >>> # Press Tab: Shows file list completion options
53
+
54
+ >>> # Extend with custom behavior
55
+ >>> class CachedShellExpansion(ShellExpansionCompleter):
56
+ ... def execute_command(self, command):
57
+ ... # Add caching logic
58
+ ... return super().execute_command(command)
59
+ """
60
+
61
+ # Pattern to match ${VAR_NAME}
62
+ VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
63
+
64
+ # Pattern to match $(command)
65
+ CMD_PATTERN = re.compile(r"\$\(([^)]+)\)")
66
+
67
+ def __init__(
68
+ self,
69
+ timeout: float = 2.0,
70
+ multiline_all: bool = True,
71
+ max_lines: int = 50,
72
+ max_display_length: int = 80,
73
+ ):
74
+ """Initialize the completer."""
75
+ self.timeout = timeout
76
+ self.multiline_all = multiline_all
77
+ self.max_lines = max_lines
78
+ self.max_display_length = max_display_length
79
+
80
+ def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
81
+ """
82
+ Get completions for environment variables and commands.
83
+
84
+ Args:
85
+ document: Current document
86
+ complete_event: Completion event
87
+
88
+ Yields:
89
+ Completion objects for patterns at cursor position
90
+ """
91
+ text = document.text
92
+ cursor_pos = document.cursor_position
93
+
94
+ # Try command pattern first
95
+ for match in self.CMD_PATTERN.finditer(text):
96
+ if match.start() <= cursor_pos <= match.end():
97
+ # Cursor is within this command pattern
98
+ command = match.group(1).strip()
99
+ if command:
100
+ yield from self.complete_command(command, match, cursor_pos)
101
+ return
102
+
103
+ # Try environment variable pattern
104
+ for match in self.VAR_PATTERN.finditer(text):
105
+ if match.start() <= cursor_pos <= match.end():
106
+ # Cursor is within this variable pattern
107
+ var_name = match.group(1)
108
+ if var_name in os.environ:
109
+ value = os.environ[var_name]
110
+ start_pos = match.start() - cursor_pos
111
+
112
+ # Use public method for formatting
113
+ yield self.format_variable_completion(
114
+ var_name, value, start_pos, match.group(0)
115
+ )
116
+ return
117
+
118
+ def truncate_display(self, text: str) -> str:
119
+ """
120
+ Truncate text for display purposes only.
121
+
122
+ Override this method to customize truncation behavior.
123
+
124
+ Args:
125
+ text: Text to potentially truncate
126
+
127
+ Returns:
128
+ Truncated text with ellipsis if needed, or original text
129
+ """
130
+ if len(text) > self.max_display_length:
131
+ return text[: self.max_display_length - 3] + "..."
132
+ return text
133
+
134
+ def execute_command(self, command: str) -> subprocess.CompletedProcess:
135
+ """
136
+ Execute shell command.
137
+
138
+ Override this method to customize command execution (e.g., add caching,
139
+ security filtering, or use a different execution mechanism).
140
+
141
+ Args:
142
+ command: Shell command to execute
143
+
144
+ Returns:
145
+ CompletedProcess with stdout, stderr, and returncode
146
+
147
+ Raises:
148
+ subprocess.TimeoutExpired: If command exceeds timeout
149
+ subprocess.SubprocessError: For other execution errors
150
+ """
151
+ return subprocess.run(
152
+ command, shell=True, capture_output=True, text=True, timeout=self.timeout
153
+ )
154
+
155
+ def process_command_output(self, output: str, command: str) -> str:
156
+ """
157
+ Process command output before creating completions.
158
+
159
+ Override this method to transform, filter, or modify command output.
160
+
161
+ Args:
162
+ output: Raw command output
163
+ command: The command that was executed
164
+
165
+ Returns:
166
+ Processed output string
167
+ """
168
+ return output.strip()
169
+
170
+ def filter_lines(self, lines: List[str]) -> List[str]:
171
+ """
172
+ Filter lines from command output.
173
+
174
+ Override this method to implement custom line filtering logic.
175
+ Default implementation removes empty lines.
176
+
177
+ Args:
178
+ lines: List of output lines
179
+
180
+ Returns:
181
+ Filtered list of lines
182
+ """
183
+ return [line for line in lines if line.strip()]
184
+
185
+ def format_variable_completion(
186
+ self, var_name: str, value: str, start_pos: int, pattern_text: str
187
+ ) -> Completion:
188
+ """
189
+ Format environment variable completion.
190
+
191
+ Override this method to customize how variable completions are displayed.
192
+
193
+ Args:
194
+ var_name: Variable name (without ${})
195
+ value: Variable value
196
+ start_pos: Start position for replacement
197
+ pattern_text: Original pattern text like ${VAR}
198
+
199
+ Returns:
200
+ Completion object
201
+ """
202
+ display_value = self.truncate_display(value)
203
+
204
+ return Completion(
205
+ text=value,
206
+ start_position=start_pos,
207
+ display=FormattedText(
208
+ [
209
+ ("class:completion.var", "${"),
210
+ ("class:completion.var.name", var_name),
211
+ ("class:completion.var", "}"),
212
+ ("class:completion.arrow", " → "),
213
+ ("class:completion.value", display_value),
214
+ ]
215
+ ),
216
+ display_meta=FormattedText([("class:completion.meta", "Environment variable")]),
217
+ )
218
+
219
+ def format_command_completion(
220
+ self, command_output: str, pattern_text: str, start_pos: int, label: Optional[str] = None
221
+ ) -> Completion:
222
+ """
223
+ Format single-line command completion.
224
+
225
+ Override this method to customize how command completions are displayed.
226
+
227
+ Args:
228
+ command_output: Command output text
229
+ pattern_text: Original pattern text like $(command)
230
+ start_pos: Start position for replacement
231
+ label: Optional label for the completion
232
+
233
+ Returns:
234
+ Completion object
235
+ """
236
+ display_text = self.truncate_display(command_output)
237
+
238
+ return Completion(
239
+ text=command_output,
240
+ start_position=start_pos,
241
+ display=FormattedText(
242
+ [
243
+ ("class:completion.cmd", pattern_text),
244
+ ("class:completion.arrow", " → "),
245
+ ("class:completion.value", display_text),
246
+ ]
247
+ ),
248
+ display_meta=FormattedText([("class:completion.meta", "Shell command")]),
249
+ )
250
+
251
+ def format_multiline_completion(
252
+ self, line_text: str, line_number: int, pattern_text: str, start_pos: int
253
+ ) -> Completion:
254
+ """
255
+ Format individual line completion for multi-line output.
256
+
257
+ Override this method to customize multi-line completion display.
258
+
259
+ Args:
260
+ line_text: Text of the line
261
+ line_number: Line number (1-based)
262
+ pattern_text: Original pattern text like $(command)
263
+ start_pos: Start position for replacement
264
+
265
+ Returns:
266
+ Completion object
267
+ """
268
+ display_line = self.truncate_display(line_text)
269
+
270
+ return Completion(
271
+ text=line_text,
272
+ start_position=start_pos,
273
+ display=FormattedText(
274
+ [
275
+ ("class:completion.cmd", pattern_text),
276
+ ("class:completion.arrow", " → "),
277
+ ("class:completion.line", f"Line {line_number}: "),
278
+ ("class:completion.value", display_line),
279
+ ]
280
+ ),
281
+ display_meta=FormattedText([("class:completion.meta", "Shell command")]),
282
+ )
283
+
284
+ def format_all_lines_completion(
285
+ self, full_output: str, line_count: int, pattern_text: str, start_pos: int
286
+ ) -> Completion:
287
+ """
288
+ Format "ALL" completion for multi-line output.
289
+
290
+ Override this method to customize the ALL option display.
291
+
292
+ Args:
293
+ full_output: Complete command output with newlines
294
+ line_count: Number of non-empty lines
295
+ pattern_text: Original pattern text like $(command)
296
+ start_pos: Start position for replacement
297
+
298
+ Returns:
299
+ Completion object
300
+ """
301
+ return Completion(
302
+ text=full_output,
303
+ start_position=start_pos,
304
+ display=FormattedText(
305
+ [
306
+ ("class:completion.cmd", pattern_text),
307
+ ("class:completion.arrow", " → "),
308
+ ("class:completion.multiline", f"ALL ({line_count} lines)"),
309
+ ]
310
+ ),
311
+ display_meta=FormattedText([("class:completion.meta", "Shell command")]),
312
+ )
313
+
314
+ def format_error_completion(
315
+ self, error_message: str, pattern_text: str, start_pos: int
316
+ ) -> Completion:
317
+ """
318
+ Format error completion.
319
+
320
+ Override this method to customize error display.
321
+
322
+ Args:
323
+ error_message: Error message to display
324
+ pattern_text: Original pattern text like $(command)
325
+ start_pos: Start position for replacement
326
+
327
+ Returns:
328
+ Completion object with empty text
329
+ """
330
+ error_display = self.truncate_display(error_message)
331
+
332
+ return Completion(
333
+ text="",
334
+ start_position=start_pos,
335
+ display=FormattedText(
336
+ [
337
+ ("class:completion.cmd", pattern_text),
338
+ ("class:completion.arrow", " → "),
339
+ ("class:completion.error", f"Error: {error_display}"),
340
+ ]
341
+ ),
342
+ display_meta=FormattedText([("class:completion.meta", "Shell command")]),
343
+ )
344
+
345
+ def complete_command(self, command: str, match, cursor_pos: int) -> Iterable[Completion]:
346
+ """
347
+ Execute command and yield completion(s).
348
+
349
+ This is the main public method for command completion. Override to change
350
+ the overall completion flow.
351
+
352
+ Args:
353
+ command: Shell command to execute
354
+ match: Regex match object
355
+ cursor_pos: Cursor position
356
+
357
+ Yields:
358
+ Completion objects with command output
359
+ """
360
+ start_pos = match.start() - cursor_pos
361
+ pattern_text = match.group(0)
362
+
363
+ try:
364
+ result = self.execute_command(command)
365
+
366
+ if result.returncode == 0:
367
+ output = self.process_command_output(result.stdout, command)
368
+
369
+ if not output:
370
+ # Command succeeded but no output
371
+ yield Completion(
372
+ text="",
373
+ start_position=start_pos,
374
+ display=FormattedText(
375
+ [
376
+ ("class:completion.cmd", pattern_text),
377
+ ("class:completion.arrow", " → "),
378
+ ("class:completion.info", "(no output)"),
379
+ ]
380
+ ),
381
+ display_meta=FormattedText([("class:completion.meta", "Shell command")]),
382
+ )
383
+ else:
384
+ # Check if multi-line output
385
+ lines = output.split("\n")
386
+ non_empty_lines = self.filter_lines(lines)
387
+
388
+ if len(non_empty_lines) > 1:
389
+ # Multi-line output
390
+ yield from self.complete_multiline(
391
+ output, non_empty_lines, pattern_text, start_pos
392
+ )
393
+ else:
394
+ # Single line output
395
+ yield self.format_command_completion(output, pattern_text, start_pos)
396
+ else:
397
+ # Command failed
398
+ error_msg = result.stderr.strip() or f"Exit code {result.returncode}"
399
+ yield self.format_error_completion(error_msg, pattern_text, start_pos)
400
+
401
+ except subprocess.TimeoutExpired:
402
+ yield self.format_error_completion(
403
+ f"Timeout ({self.timeout}s)", pattern_text, start_pos
404
+ )
405
+ except FileNotFoundError:
406
+ yield self.format_error_completion("Command not found", pattern_text, start_pos)
407
+
408
+ def complete_multiline(
409
+ self, full_output: str, lines: list, pattern_text: str, start_pos: int
410
+ ) -> Iterable[Completion]:
411
+ """
412
+ Yield completions for multi-line command output.
413
+
414
+ Override this method to change multi-line completion behavior.
415
+
416
+ Args:
417
+ full_output: Complete command output (with newlines)
418
+ lines: List of non-empty lines
419
+ pattern_text: Original pattern text like $(command)
420
+ start_pos: Start position for replacement
421
+
422
+ Yields:
423
+ Completion for ALL and individual lines (up to max_lines limit)
424
+ """
425
+ total_lines = len(lines)
426
+
427
+ # First option: ALL (complete with all lines - never truncated)
428
+ if self.multiline_all:
429
+ yield self.format_all_lines_completion(
430
+ full_output, total_lines, pattern_text, start_pos
431
+ )
432
+
433
+ # Individual line options (limited to max_lines)
434
+ lines_to_show = lines[: self.max_lines]
435
+ remaining = total_lines - len(lines_to_show)
436
+
437
+ for i, line in enumerate(lines_to_show, 1):
438
+ yield self.format_multiline_completion(line, i, pattern_text, start_pos)
439
+
440
+ # If there are more lines, show an indicator
441
+ if remaining > 0:
442
+ yield Completion(
443
+ text="", # No completion action
444
+ start_position=start_pos,
445
+ display=FormattedText(
446
+ [
447
+ ("class:completion.cmd", pattern_text),
448
+ ("class:completion.arrow", " → "),
449
+ ("class:completion.info", f"({remaining} more lines... use ALL)"),
450
+ ]
451
+ ),
452
+ display_meta=FormattedText([("class:completion.meta", "Shell command")]),
453
+ )
@@ -0,0 +1,152 @@
1
+ """
2
+ Formatted text utilities for repl_toolkit.
3
+
4
+ This module provides utilities for working with formatted text in prompt_toolkit,
5
+ including auto-detection of format types and smart printing.
6
+ """
7
+
8
+ import re
9
+ from typing import Callable
10
+
11
+ from prompt_toolkit import print_formatted_text
12
+ from prompt_toolkit.formatted_text import ANSI, HTML
13
+
14
+ # Pre-compile regex patterns for performance
15
+ _ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
16
+ _HTML_PATTERN = re.compile(r"</?[a-zA-Z][a-zA-Z0-9]*\s*/?>")
17
+
18
+
19
+ def detect_format_type(text: str) -> str:
20
+ """
21
+ Detect the format type of a text string.
22
+
23
+ Detects three format types:
24
+ - 'ansi': Text contains ANSI escape codes (e.g., \\x1b[1m)
25
+ - 'html': Text contains HTML-like tags (e.g., <b>, <darkcyan>)
26
+ - 'plain': Plain text with no special formatting
27
+
28
+ Args:
29
+ text: Text string to analyze
30
+
31
+ Returns:
32
+ Format type: 'ansi', 'html', or 'plain'
33
+
34
+ Examples:
35
+ >>> detect_format_type("<b>Bold</b>")
36
+ 'html'
37
+ >>> detect_format_type("\\x1b[1mBold\\x1b[0m")
38
+ 'ansi'
39
+ >>> detect_format_type("Plain text")
40
+ 'plain'
41
+ >>> detect_format_type("a < b and c > d")
42
+ 'plain'
43
+ """
44
+ # Check for ANSI escape codes (most specific)
45
+ if _ANSI_PATTERN.search(text):
46
+ return "ansi"
47
+
48
+ # Check for HTML tags
49
+ # Valid HTML tag names: start with letter, contain letters/numbers
50
+ if _HTML_PATTERN.search(text):
51
+ return "html"
52
+
53
+ # Plain text
54
+ return "plain"
55
+
56
+
57
+ def auto_format(text: str):
58
+ """
59
+ Auto-detect format type and return appropriate formatted text object.
60
+
61
+ This function analyzes the input text and wraps it in the appropriate
62
+ prompt_toolkit formatted text type (HTML, ANSI, or plain string).
63
+
64
+ Args:
65
+ text: Text string to format
66
+
67
+ Returns:
68
+ Formatted text object (HTML, ANSI, or str)
69
+
70
+ Examples:
71
+ >>> auto_format("<b>Bold</b>")
72
+ HTML('<b>Bold</b>')
73
+ >>> auto_format("\\x1b[1mBold\\x1b[0m")
74
+ ANSI('\\x1b[1mBold\\x1b[0m')
75
+ >>> auto_format("Plain text")
76
+ 'Plain text'
77
+ """
78
+ format_type = detect_format_type(text)
79
+
80
+ if format_type == "ansi":
81
+ return ANSI(text)
82
+ elif format_type == "html":
83
+ return HTML(text)
84
+ else:
85
+ return text
86
+
87
+
88
+ def print_auto_formatted(text: str, **kwargs) -> None:
89
+ """
90
+ Print text with auto-detected formatting.
91
+
92
+ This function automatically detects the format type (HTML, ANSI, or plain)
93
+ and prints the text with appropriate formatting applied.
94
+
95
+ Args:
96
+ text: Text to print (may contain HTML tags, ANSI codes, or be plain)
97
+ **kwargs: Additional arguments passed to print_formatted_text
98
+ (e.g., end, flush, style, output)
99
+
100
+ Examples:
101
+ >>> print_auto_formatted("<b>Bold</b> text")
102
+ Bold text
103
+ >>> print_auto_formatted("\\x1b[1mBold\\x1b[0m text")
104
+ Bold text
105
+ >>> print_auto_formatted("Plain text")
106
+ Plain text
107
+ """
108
+ formatted = auto_format(text)
109
+ print_formatted_text(formatted, **kwargs)
110
+
111
+
112
+ def create_auto_printer() -> Callable:
113
+ """
114
+ Create a printer function with auto-format detection.
115
+
116
+ Returns a callable that can be used as a drop-in replacement for print(),
117
+ with automatic detection and application of formatting (HTML or ANSI).
118
+
119
+ This is particularly useful for injecting into callback handlers or other
120
+ components that accept a custom printer function.
121
+
122
+ Returns:
123
+ Callable printer function with signature: printer(text, **kwargs)
124
+
125
+ Examples:
126
+ >>> printer = create_auto_printer()
127
+ >>> printer("<b>Bold</b>", end="", flush=True)
128
+ Bold
129
+ >>> printer(" text\\n")
130
+ text
131
+
132
+ Usage with callback handlers:
133
+ >>> from some_library import CallbackHandler
134
+ >>> handler = CallbackHandler(
135
+ ... response_prefix="<b>Bot:</b> ",
136
+ ... printer=create_auto_printer()
137
+ ... )
138
+ """
139
+
140
+ def printer(text: str, **kwargs):
141
+ """Auto-format and print text."""
142
+ print_auto_formatted(text, **kwargs)
143
+
144
+ return printer
145
+
146
+
147
+ __all__ = [
148
+ "detect_format_type",
149
+ "auto_format",
150
+ "print_auto_formatted",
151
+ "create_auto_printer",
152
+ ]