From cd0ca95877dfc3c1956f882840e9db55b8899a16 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 21 Jan 2022 17:54:35 +0530 Subject: [PATCH] Improve the UI of the ask kitten Center the text in the window and allow pressing enter/esc when doing a yes/no question Fixes #4545 --- docs/changelog.rst | 2 ++ kittens/ask/main.py | 79 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 779d26a75..ff4d432b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -87,6 +87,8 @@ Detailed list of changes - Shell integration: Fix bash integration not working when ``PROMPT_COMMAND`` is used to change the prompt variables (:iss:`4476`) +- Improve the UI of the ask kitten (:iss:`4545`) + - Allow using templates with text formatting for :opt:`tab_activity_symbol` (:pull:`4507`) diff --git a/kittens/ask/main.py b/kittens/ask/main.py index f366812a9..607d9bf6a 100644 --- a/kittens/ask/main.py +++ b/kittens/ask/main.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Tuple from kitty.cli import parse_args from kitty.cli_stub import AskCLIOptions from kitty.constants import cache_dir -from kitty.fast_data_types import wcswidth -from kitty.typing import BossType, TypedDict +from kitty.fast_data_types import truncate_point_for_length, wcswidth +from kitty.typing import BossType, KeyEventType, TypedDict from kitty.utils import ScreenSize from ..tui.handler import Handler, result_handler @@ -118,6 +118,14 @@ class Range(NamedTuple): return y == self.y and self.start <= x <= self.end +def truncate_at_space(text: str, width: int) -> Tuple[str, str]: + p = truncate_point_for_length(text, width) + i = text.rfind(' ', 0, p + 1) + if i > 0 and p - i < 12: + p = i + 1 + return text[:p], text[p:] + + class Choose(Handler): mouse_tracking = MouseTracking.buttons_only @@ -147,39 +155,73 @@ class Choose(Handler): def finalize(self) -> None: self.cmd.set_cursor_visible(True) + def draw_long_text(self, text: str) -> int: + y = 0 + width = self.screen_size.cols - 2 + while text: + t, text = truncate_at_space(text, width) + y += 1 + extra = 1 + ((width - wcswidth(t)) // 2) + self.cmd.styled(' ' * extra + t, bold=True) + self.print() + return y + @Handler.atomic_update def draw_screen(self) -> None: self.cmd.clear_screen() - y = 1 + y = max(0, self.screen_size.rows // 2 - 2) + self.print(end='\r\n'*y) if self.cli_opts.message: - self.cmd.styled(self.cli_opts.message, bold=True) - y += wcswidth(self.cli_opts.message) // self.screen_size.cols + y += self.draw_long_text(self.cli_opts.message) self.print() + y += 1 if self.cli_opts.type == 'yesno': self.draw_yesno(y) else: self.draw_choice(y) def draw_choice(self, y: int) -> None: - x = 0 self.clickable_ranges.clear() + current_line = '' + current_ranges: Dict[str, int] = {} + width = self.screen_size.cols - 2 + + def commit_line() -> None: + nonlocal current_line, y + extra = (width - wcswidth(current_line)) // 2 + x = extra + 1 + self.print(' ' * x + current_line) + for letter, sz in current_ranges.items(): + self.clickable_ranges[letter] = Range(x, x + sz - 3, y) + x += sz + current_ranges.clear() + y += 1 + current_line = '' + for letter, choice in self.choices.items(): text = choice.text[:choice.idx] text += styled(choice.text[choice.idx], fg=choice.color) text += choice.text[choice.idx + 1:] text += ' ' sz = wcswidth(text) - if sz + x >= self.screen_size.cols: - y += 1 - x = 0 - self.print() - self.clickable_ranges[letter] = Range(x, x + sz - 1, y) - x += sz - self.print(text, end='') + if sz + wcswidth(current_line) >= width: + commit_line() + current_line += text + current_ranges[letter] = sz + if current_line: + commit_line() def draw_yesno(self, y: int) -> None: - self.clickable_ranges = {'y': Range(2, 4, y), 'n': Range(8, 9, y)} - self.print(' ', styled('Y', fg='green') + 'es', ' ', styled('N', fg='red') + 'o') + sep = ' ' * 3 + yes = styled('Y', fg='green') + 'es' + no = styled('N', fg='red') + 'o' + text = yes + sep + no + w = wcswidth(text) + extra = (self.screen_size.cols - w) // 2 + x = extra + nx = x + wcswidth(yes) + len(sep) + self.clickable_ranges = {'y': Range(x, x + wcswidth(yes) - 1, y), 'n': Range(nx, nx + 1, y)} + self.print(' ' * extra + text) def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: text = text.lower() @@ -187,6 +229,13 @@ class Choose(Handler): self.response = text self.quit_loop(0) + def on_key(self, key_event: KeyEventType) -> None: + if self.cli_opts.type == 'yesno': + if key_event.matches('esc'): + self.on_text('n') + elif key_event.matches('enter'): + self.on_text('y') + def on_click(self, ev: MouseEvent) -> None: for letter, r in self.clickable_ranges.items(): if r.has_point(ev.cell_x, ev.cell_y):