From 80ec71f30b6f5a1a8fc79d2a05f1fd045ff7694f Mon Sep 17 00:00:00 2001 From: dece Date: Mon, 19 Apr 2021 00:28:20 +0200 Subject: [PATCH] command_line: add a prompt function --- bebop/browser/browser.py | 18 ++++++++++---- bebop/browser/gemini.py | 25 ++++++++++++------- bebop/command_line.py | 52 ++++++++++++++++++++++++++++++---------- 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/bebop/browser/browser.py b/bebop/browser/browser.py index 5c3a6ab..1dc60eb 100644 --- a/bebop/browser/browser.py +++ b/bebop/browser/browser.py @@ -253,11 +253,11 @@ class Browser: def quick_command(self, command): """Shortcut method to take user input with a prefixed command string.""" - prefix = f"{command} " if command else "" - user_input = self.command_line.focus(":", prefix=prefix) - if not user_input: + prefix = command + " " if command else "" + text = self.command_line.focus(CommandLine.CHAR_COMMAND, prefix=prefix) + if not text: return - self.process_command(user_input) + self.process_command(text) def process_command(self, command_text: str): """Handle a client command.""" @@ -480,7 +480,10 @@ class Browser: return self.set_status("Bookmark title?") current_title = self.page_pad.current_page.title or "" - title = self.command_line.focus(">", prefix=current_title) + title = self.command_line.focus( + CommandLine.CHAR_TEXT, + prefix=current_title + ) if title: title = title.strip() if title: @@ -525,3 +528,8 @@ class Browser: """Show the help page.""" self.load_page(Page.from_gemtext(HELP_PAGE, self.config["text_width"])) self.current_url = "bebop://help" + + def prompt(self, text, keys): + """Display the text and allow it to type one of the given keys.""" + self.set_status(text) + return self.command_line.prompt_key(keys) diff --git a/bebop/browser/gemini.py b/bebop/browser/gemini.py index a545960..44858d6 100644 --- a/bebop/browser/gemini.py +++ b/bebop/browser/gemini.py @@ -3,6 +3,7 @@ from pathlib import Path from bebop.browser.browser import Browser +from bebop.command_line import CommandLine from bebop.fs import get_downloads_path from bebop.navigation import set_parameter from bebop.page import Page @@ -42,7 +43,7 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True, error = f"Certificate has been changed ({url})." # TODO propose the user ways to handle this. elif req.state == Request.STATE_CONNECTION_FAILED: - error_details = f": {req.error}" if req.error else "." + error_details = ": " + req.error if req.error else "." error = f"Connection failed ({url})" + error_details else: error = f"Connection failed ({url})." @@ -67,22 +68,28 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True, browser.set_status_error(f"Server response parsing failed ({url}).") return + _handle_response(browser, response, url, redirects, history) + + +def _handle_response(browser: Browser, response: Response, url: str, + redirects: int, history: bool): + """Handle a response from a Gemini server.""" if response.code == 20: - handle_response_content(browser, url, response, history) + _handle_successful_response(browser, response, url, history) elif response.generic_code == 30 and response.meta: browser.open_url(response.meta, base_url=url, redirects=redirects + 1) elif response.generic_code in (40, 50): error = f"Server error: {response.meta or Response.code.name}" browser.set_status_error(error) elif response.generic_code == 10: - handle_input_request(browser, url, response.meta) + _handle_input_request(browser, url, response.meta) else: error = f"Unhandled response code {response.code}" browser.set_status_error(error) -def handle_response_content(browser: Browser, url: str, response: Response, - history: bool): +def _handle_successful_response(browser: Browser, response: Response, url: str, + history: bool): """Handle a successful response content from a Gemini server. According to the MIME type received or inferred, the response is either @@ -116,7 +123,7 @@ def handle_response_content(browser: Browser, url: str, response: Response, text = response.content.decode("utf-8", errors="replace") page = Page.from_text(text) else: - filepath = get_download_path(url) + filepath = _get_download_path(url) if page: browser.load_page(page) @@ -137,7 +144,7 @@ def handle_response_content(browser: Browser, url: str, response: Response, browser.set_status_error(error) -def get_download_path(url: str) -> Path: +def _get_download_path(url: str) -> Path: """Try to find the best download file path possible from this URL.""" download_dir = get_downloads_path() url_parts = url.rsplit("/", maxsplit=1) @@ -149,13 +156,13 @@ def get_download_path(url: str) -> Path: return download_dir / filename -def handle_input_request(browser: Browser, from_url: str, message: str =None): +def _handle_input_request(browser: Browser, from_url: str, message: str =None): """Focus command-line to pass input to the server.""" if message: browser.set_status(f"Input needed: {message}") else: browser.set_status("Input needed:") - user_input = browser.command_line.focus("?") + user_input = browser.command_line.focus(CommandLine.CHAR_TEXT) if not user_input: return url = set_parameter(from_url, user_input) diff --git a/bebop/command_line.py b/bebop/command_line.py index 1805029..e705fc0 100644 --- a/bebop/command_line.py +++ b/bebop/command_line.py @@ -17,18 +17,35 @@ class CommandLine: the window's right border when writing more content than the width allows. Therefore I just added the M-e keybind to call an external editor and use its content as result. + + Attributes: + - window: curses window to use for the command line and Textbox. + - editor_command: external command to use to edit content externally. + - textbox: Textbox object handling user input. """ + CHAR_COMMAND = ":" + CHAR_DIGIT = "&" + CHAR_TEXT = ">" + def __init__(self, window, editor_command): self.window = window self.editor_command = editor_command - self.textbox = None + self.textbox = curses.textpad.Textbox(self.window) def clear(self): """Clear command-line contents.""" self.window.clear() self.window.refresh() + def gather(self): + """Return the string currently written by the user in command line. + + This doesn't count the command char used, but it includes then prefix. + Trailing whitespace is trimmed. + """ + return self.textbox.gather()[1:].rstrip() + def focus(self, command_char, validator=None, prefix=""): """Give user focus to the command bar. @@ -46,13 +63,13 @@ class CommandLine: User input as string. The string will be empty if the validator raised an EscapeInterrupt. """ + validator = validator or self._validate_common_input self.window.clear() self.window.refresh() - self.textbox = curses.textpad.Textbox(self.window) self.window.addstr(command_char + prefix) curses.curs_set(1) try: - command = self.textbox.edit(validator or self.validate_common_input) + command = self.textbox.edit(validator) except EscapeCommandInterrupt: command = "" except TerminateCommandInterrupt as exc: @@ -63,11 +80,7 @@ class CommandLine: self.clear() return command - def gather(self): - """Return the string currently written by the user in command line.""" - return self.textbox.gather()[1:].rstrip() - - def validate_common_input(self, ch: int): + def _validate_common_input(self, ch: int): """Generic input validator, handles a few more cases than default. This validator can be used as a default validator as it handles, on top @@ -136,8 +149,8 @@ class CommandLine: if len(candidates) == 1: return 0, candidates[0] # Else, focus the command line to let the user input more digits. - validator = lambda ch: self.validate_link_digit(ch, links, max_digits) - link_input = self.focus("&", validator, digit) + validator = lambda ch: self._validate_link_digit(ch, links, max_digits) + link_input = self.focus(CommandLine.CHAR_DIGIT, validator, digit) if not link_input: return 1, None try: @@ -146,10 +159,10 @@ class CommandLine: return 2, f"Invalid link ID {link_input}." return 0, link_id - def validate_link_digit(self, ch: int, links: Links, max_digits: int): + def _validate_link_digit(self, ch: int, links: Links, max_digits: int): """Handle input chars to be used as link ID.""" # Handle common chars. - ch = self.validate_common_input(ch) + ch = self._validate_common_input(ch) # Only accept digits. If we reach the amount of required digits, open # link now and leave command line. Else just process it. if curses.ascii.isdigit(ch): @@ -185,6 +198,21 @@ class CommandLine: return raise TerminateCommandInterrupt(content) + def prompt_key(self, keys): + """Focus the command line and wait for the user """ + validator = lambda ch: self._validate_prompt(ch, keys) + key = self.focus(CommandLine.CHAR_TEXT, validator) + return key if key in keys else "" + + def _validate_prompt(self, ch: int, keys): + """Handle input chars and raise a terminate interrupt on a valid key.""" + # Handle common keys. + ch = self._validate_common_input(ch) + char = chr(ch) + if char in keys: + raise TerminateCommandInterrupt(char) + return 0 + class EscapeCommandInterrupt(Exception): """Signal that ESC has been pressed during command line."""