Source code for ascii_colors.questionary.autocomplete

# -*- coding: utf-8 -*-
"""
Autocomplete text input question type for questionary compatibility.
"""

from typing import Any, Callable, Dict, List, Optional, Union
from ascii_colors.constants import ANSI
from ascii_colors.core import ASCIIColors
from ascii_colors.questionary.base import Question, Validator
from ascii_colors.questionary.text import PromptText


[docs] class Autocomplete(PromptText): """Text input with autocomplete suggestions.""" def __init__(self, message: str, choices: List[str], default: str = "", validate: Optional[Union[Callable[[str], bool], Validator]] = None, style: Optional[Dict[str, str]] = None, **kwargs): # Initialize parent Text class properly super().__init__(message, default, validate, style, **kwargs) self.choices = choices self.ignore_case = kwargs.get('ignore_case', True) self.match_middle = kwargs.get('match_middle', False) self.max_suggestions = kwargs.get('max_suggestions', 5) def _ask_internal(self) -> str: from ascii_colors.questionary.text import PromptText from ascii_colors.utils import strip_ansi q_color = self._get_style('question', ANSI.color_bright_yellow) sug_color = self._get_style('suggestion', ANSI.style_dim) processed_message = ASCIIColors._apply_rich_markup(self.message) buffer = list(self.default) if self.default else[] cursor_pos = len(buffer) # Keep track of what the user actually *types* as the filter. # Start empty so all options show initially. typed_text = "" match_idx = -1 # If there's a default, highlight it in the list initially if self.default: for i, choice in enumerate(self.choices): if choice == self.default: match_idx = i break while True: # Always get matches based on what was actually TYPED matches = self._get_matches(typed_text) current = ''.join(buffer) # Clear line and redraw prompt + current buffer print(f"\r\033[2K", end="") # Clear entire line ASCIIColors.print(f"{processed_message}: ", color=q_color, end="", flush=False, markup=False) print(current, end="") if matches: # Calculate sliding window for suggestions if many matches exist start_window = 0 if match_idx >= self.max_suggestions: start_window = match_idx - self.max_suggestions + 1 end_window = start_window + self.max_suggestions visible_matches = matches[start_window:end_window] display_parts =[] for i, m in enumerate(visible_matches): actual_idx = start_window + i if actual_idx == match_idx: display_parts.append(f"{ANSI.color_bg_cyan}{ANSI.color_black}{m}{ANSI.color_reset}") else: display_parts.append(m) # Show navigation hint and windowed suggestions prefix = "..." if start_window > 0 else "" suffix = "..." if len(matches) > end_window else "" suggestion_text = f" {sug_color}({len(matches)} matches: {prefix}{', '.join(display_parts)}{suffix}){ANSI.color_reset}" print(suggestion_text, end="") # Return cursor to the correct position within the buffer text prompt_len = len(strip_ansi(processed_message)) + 2 print(f"\r\033[{prompt_len + cursor_pos}C", end="", flush=True) key = self._get_key() if key == 'ENTER': final = ''.join(buffer) if self._validate_input(final): print() # Move to next line return final ASCIIColors.print("\n ✗ Invalid input", color=ANSI.color_red) # Small pause to show error before redrawing import time; time.sleep(0.5) elif key == 'BACKSPACE': if cursor_pos > 0: buffer.pop(cursor_pos - 1) cursor_pos -= 1 typed_text = "".join(buffer) match_idx = -1 elif key == 'LEFT': cursor_pos = max(0, cursor_pos - 1) elif key == 'RIGHT': cursor_pos = min(len(buffer), cursor_pos + 1) elif key == 'QUIT': raise KeyboardInterrupt elif key == 'UP' or key == 'DOWN': if matches: if match_idx == -1: match_idx = 0 if key == 'DOWN' else len(matches) - 1 else: if key == 'DOWN': match_idx = (match_idx + 1) % len(matches) else: # UP match_idx = (match_idx - 1) % len(matches) # Update buffer with the selected suggestion selected = matches[match_idx] buffer = list(selected) cursor_pos = len(buffer) elif len(key) == 1: # Insert character at cursor position buffer.insert(cursor_pos, key) cursor_pos += 1 typed_text = "".join(buffer) match_idx = -1 def _get_matches(self, text: str) -> List[str]: """Get matching suggestions.""" if not text: return self.choices search = text.lower() if self.ignore_case else text matches = [] for choice in self.choices: cmp = choice.lower() if self.ignore_case else choice if self.match_middle: if search in cmp: matches.append(choice) else: if cmp.startswith(search): matches.append(choice) return matches
[docs] def autocomplete(message: str, choices: List[str], default: str = "", validate: Optional[Union[Callable[[str], bool], Validator]] = None, style: Optional[Dict[str, str]] = None, **kwargs) -> Autocomplete: """Create an autocomplete text input question.""" return Autocomplete(message, choices, default, validate, style, **kwargs)