command_line: add a prompt function
This commit is contained in:
parent
396391ea80
commit
80ec71f30b
|
@ -253,11 +253,11 @@ class Browser:
|
||||||
|
|
||||||
def quick_command(self, command):
|
def quick_command(self, command):
|
||||||
"""Shortcut method to take user input with a prefixed command string."""
|
"""Shortcut method to take user input with a prefixed command string."""
|
||||||
prefix = f"{command} " if command else ""
|
prefix = command + " " if command else ""
|
||||||
user_input = self.command_line.focus(":", prefix=prefix)
|
text = self.command_line.focus(CommandLine.CHAR_COMMAND, prefix=prefix)
|
||||||
if not user_input:
|
if not text:
|
||||||
return
|
return
|
||||||
self.process_command(user_input)
|
self.process_command(text)
|
||||||
|
|
||||||
def process_command(self, command_text: str):
|
def process_command(self, command_text: str):
|
||||||
"""Handle a client command."""
|
"""Handle a client command."""
|
||||||
|
@ -480,7 +480,10 @@ class Browser:
|
||||||
return
|
return
|
||||||
self.set_status("Bookmark title?")
|
self.set_status("Bookmark title?")
|
||||||
current_title = self.page_pad.current_page.title or ""
|
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:
|
if title:
|
||||||
title = title.strip()
|
title = title.strip()
|
||||||
if title:
|
if title:
|
||||||
|
@ -525,3 +528,8 @@ class Browser:
|
||||||
"""Show the help page."""
|
"""Show the help page."""
|
||||||
self.load_page(Page.from_gemtext(HELP_PAGE, self.config["text_width"]))
|
self.load_page(Page.from_gemtext(HELP_PAGE, self.config["text_width"]))
|
||||||
self.current_url = "bebop://help"
|
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)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bebop.browser.browser import Browser
|
from bebop.browser.browser import Browser
|
||||||
|
from bebop.command_line import CommandLine
|
||||||
from bebop.fs import get_downloads_path
|
from bebop.fs import get_downloads_path
|
||||||
from bebop.navigation import set_parameter
|
from bebop.navigation import set_parameter
|
||||||
from bebop.page import Page
|
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})."
|
error = f"Certificate has been changed ({url})."
|
||||||
# TODO propose the user ways to handle this.
|
# TODO propose the user ways to handle this.
|
||||||
elif req.state == Request.STATE_CONNECTION_FAILED:
|
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
|
error = f"Connection failed ({url})" + error_details
|
||||||
else:
|
else:
|
||||||
error = f"Connection failed ({url})."
|
error = f"Connection failed ({url})."
|
||||||
|
@ -67,21 +68,27 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
|
||||||
browser.set_status_error(f"Server response parsing failed ({url}).")
|
browser.set_status_error(f"Server response parsing failed ({url}).")
|
||||||
return
|
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:
|
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:
|
elif response.generic_code == 30 and response.meta:
|
||||||
browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
||||||
elif response.generic_code in (40, 50):
|
elif response.generic_code in (40, 50):
|
||||||
error = f"Server error: {response.meta or Response.code.name}"
|
error = f"Server error: {response.meta or Response.code.name}"
|
||||||
browser.set_status_error(error)
|
browser.set_status_error(error)
|
||||||
elif response.generic_code == 10:
|
elif response.generic_code == 10:
|
||||||
handle_input_request(browser, url, response.meta)
|
_handle_input_request(browser, url, response.meta)
|
||||||
else:
|
else:
|
||||||
error = f"Unhandled response code {response.code}"
|
error = f"Unhandled response code {response.code}"
|
||||||
browser.set_status_error(error)
|
browser.set_status_error(error)
|
||||||
|
|
||||||
|
|
||||||
def handle_response_content(browser: Browser, url: str, response: Response,
|
def _handle_successful_response(browser: Browser, response: Response, url: str,
|
||||||
history: bool):
|
history: bool):
|
||||||
"""Handle a successful response content from a Gemini server.
|
"""Handle a successful response content from a Gemini server.
|
||||||
|
|
||||||
|
@ -116,7 +123,7 @@ def handle_response_content(browser: Browser, url: str, response: Response,
|
||||||
text = response.content.decode("utf-8", errors="replace")
|
text = response.content.decode("utf-8", errors="replace")
|
||||||
page = Page.from_text(text)
|
page = Page.from_text(text)
|
||||||
else:
|
else:
|
||||||
filepath = get_download_path(url)
|
filepath = _get_download_path(url)
|
||||||
|
|
||||||
if page:
|
if page:
|
||||||
browser.load_page(page)
|
browser.load_page(page)
|
||||||
|
@ -137,7 +144,7 @@ def handle_response_content(browser: Browser, url: str, response: Response,
|
||||||
browser.set_status_error(error)
|
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."""
|
"""Try to find the best download file path possible from this URL."""
|
||||||
download_dir = get_downloads_path()
|
download_dir = get_downloads_path()
|
||||||
url_parts = url.rsplit("/", maxsplit=1)
|
url_parts = url.rsplit("/", maxsplit=1)
|
||||||
|
@ -149,13 +156,13 @@ def get_download_path(url: str) -> Path:
|
||||||
return download_dir / filename
|
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."""
|
"""Focus command-line to pass input to the server."""
|
||||||
if message:
|
if message:
|
||||||
browser.set_status(f"Input needed: {message}")
|
browser.set_status(f"Input needed: {message}")
|
||||||
else:
|
else:
|
||||||
browser.set_status("Input needed:")
|
browser.set_status("Input needed:")
|
||||||
user_input = browser.command_line.focus("?")
|
user_input = browser.command_line.focus(CommandLine.CHAR_TEXT)
|
||||||
if not user_input:
|
if not user_input:
|
||||||
return
|
return
|
||||||
url = set_parameter(from_url, user_input)
|
url = set_parameter(from_url, user_input)
|
||||||
|
|
|
@ -17,18 +17,35 @@ class CommandLine:
|
||||||
the window's right border when writing more content than the width allows.
|
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
|
Therefore I just added the M-e keybind to call an external editor and use
|
||||||
its content as result.
|
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):
|
def __init__(self, window, editor_command):
|
||||||
self.window = window
|
self.window = window
|
||||||
self.editor_command = editor_command
|
self.editor_command = editor_command
|
||||||
self.textbox = None
|
self.textbox = curses.textpad.Textbox(self.window)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear command-line contents."""
|
"""Clear command-line contents."""
|
||||||
self.window.clear()
|
self.window.clear()
|
||||||
self.window.refresh()
|
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=""):
|
def focus(self, command_char, validator=None, prefix=""):
|
||||||
"""Give user focus to the command bar.
|
"""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
|
User input as string. The string will be empty if the validator raised
|
||||||
an EscapeInterrupt.
|
an EscapeInterrupt.
|
||||||
"""
|
"""
|
||||||
|
validator = validator or self._validate_common_input
|
||||||
self.window.clear()
|
self.window.clear()
|
||||||
self.window.refresh()
|
self.window.refresh()
|
||||||
self.textbox = curses.textpad.Textbox(self.window)
|
|
||||||
self.window.addstr(command_char + prefix)
|
self.window.addstr(command_char + prefix)
|
||||||
curses.curs_set(1)
|
curses.curs_set(1)
|
||||||
try:
|
try:
|
||||||
command = self.textbox.edit(validator or self.validate_common_input)
|
command = self.textbox.edit(validator)
|
||||||
except EscapeCommandInterrupt:
|
except EscapeCommandInterrupt:
|
||||||
command = ""
|
command = ""
|
||||||
except TerminateCommandInterrupt as exc:
|
except TerminateCommandInterrupt as exc:
|
||||||
|
@ -63,11 +80,7 @@ class CommandLine:
|
||||||
self.clear()
|
self.clear()
|
||||||
return command
|
return command
|
||||||
|
|
||||||
def gather(self):
|
def _validate_common_input(self, ch: int):
|
||||||
"""Return the string currently written by the user in command line."""
|
|
||||||
return self.textbox.gather()[1:].rstrip()
|
|
||||||
|
|
||||||
def validate_common_input(self, ch: int):
|
|
||||||
"""Generic input validator, handles a few more cases than default.
|
"""Generic input validator, handles a few more cases than default.
|
||||||
|
|
||||||
This validator can be used as a default validator as it handles, on top
|
This validator can be used as a default validator as it handles, on top
|
||||||
|
@ -136,8 +149,8 @@ class CommandLine:
|
||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
return 0, candidates[0]
|
return 0, candidates[0]
|
||||||
# Else, focus the command line to let the user input more digits.
|
# Else, focus the command line to let the user input more digits.
|
||||||
validator = lambda ch: self.validate_link_digit(ch, links, max_digits)
|
validator = lambda ch: self._validate_link_digit(ch, links, max_digits)
|
||||||
link_input = self.focus("&", validator, digit)
|
link_input = self.focus(CommandLine.CHAR_DIGIT, validator, digit)
|
||||||
if not link_input:
|
if not link_input:
|
||||||
return 1, None
|
return 1, None
|
||||||
try:
|
try:
|
||||||
|
@ -146,10 +159,10 @@ class CommandLine:
|
||||||
return 2, f"Invalid link ID {link_input}."
|
return 2, f"Invalid link ID {link_input}."
|
||||||
return 0, link_id
|
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 input chars to be used as link ID."""
|
||||||
# Handle common chars.
|
# 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
|
# Only accept digits. If we reach the amount of required digits, open
|
||||||
# link now and leave command line. Else just process it.
|
# link now and leave command line. Else just process it.
|
||||||
if curses.ascii.isdigit(ch):
|
if curses.ascii.isdigit(ch):
|
||||||
|
@ -185,6 +198,21 @@ class CommandLine:
|
||||||
return
|
return
|
||||||
raise TerminateCommandInterrupt(content)
|
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):
|
class EscapeCommandInterrupt(Exception):
|
||||||
"""Signal that ESC has been pressed during command line."""
|
"""Signal that ESC has been pressed during command line."""
|
||||||
|
|
Reference in a new issue