diff --git a/docs/changelog.rst b/docs/changelog.rst index aa73323a9e4..d352d36f3bd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -57,6 +57,8 @@ Detailed list of changes - :opt:`notify_on_cmd_finish`: Show the actual command that was finished (:iss:`7420`) +- hints kitten: Allow clicking on matched text to select it in addition to typing the hint + - Shell integration: Make the currently executing cmdline available as a window variable in kitty - :opt:`paste_actions`: Fix ``replace-newline`` not working with ``confirm`` (:iss:`7374`) diff --git a/docs/kittens/hints.rst b/docs/kittens/hints.rst index 69edeb8e80d..721aad7ccdf 100644 --- a/docs/kittens/hints.rst +++ b/docs/kittens/hints.rst @@ -51,6 +51,9 @@ You can also :doc:`customize what actions are taken for different types of URLs select that hint or press :kbd:`Enter` or :kbd:`Space` to select the empty hint. +For mouse lovers, the hints kitten also allows you to click on any matched text to +select it instead of typing the hint character. + The hints kitten is very powerful to see more detailed help on its various options and modes of operation, see below. You can use these options to create mappings in :file:`kitty.conf` to select various different text diff --git a/kittens/hints/main.go b/kittens/hints/main.go index 068b72e8648..89eb0d1fe27 100644 --- a/kittens/hints/main.go +++ b/kittens/hints/main.go @@ -3,6 +3,7 @@ package hints import ( + "encoding/json" "fmt" "io" "os" @@ -183,7 +184,8 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) { } else { mark_text = mark_text[len(hint):] } - return hint_style(hint) + text_style(mark_text) + ans := hint_style(hint) + text_style(mark_text) + return fmt.Sprintf("\x1b]8;;mark:%d\a%s\x1b]8;;\a", m.Index, ans) } render := func() string { @@ -230,6 +232,30 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) { draw_screen() return nil } + lp.OnRCResponse = func(data []byte) error { + var r struct { + Type string + Mark int + } + if err := json.Unmarshal(data, &r); err != nil { + return err + } + if r.Type == "mark_activated" { + if m, ok := index_map[r.Mark]; ok { + chosen = append(chosen, m) + if o.Multiple { + ignore_mark_indices.Add(m.Index) + reset() + } else { + lp.Quit(0) + return nil + } + + } + } + return nil + } + lp.OnText = func(text string, _, _ bool) error { changed := false for _, ch := range text { diff --git a/kittens/hints/main.py b/kittens/hints/main.py index bf504144397..62b77231db4 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -9,7 +9,7 @@ from kitty.clipboard import set_clipboard_string, set_primary_selection from kitty.constants import website_url from kitty.fast_data_types import get_options -from kitty.typing import BossType +from kitty.typing import BossType, WindowType from kitty.utils import get_editor, resolve_custom_file from ..tui.handler import result_handler @@ -312,7 +312,14 @@ def is_copy_action(s: str) -> bool: }[action])(*cmd) -@result_handler(type_of_input='screen-ansi', has_ready_notification=True) +def on_mark_clicked(boss: BossType, window: WindowType, url: str, hyperlink_id: int, cwd: str) -> bool: + if url.startswith('mark:'): + window.send_cmd_response({'Type': 'mark_activated', 'Mark': int(url[5:])}) + return True + return False + + +@result_handler(type_of_input='screen-ansi', has_ready_notification=True, open_url_handler=on_mark_clicked) def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None: cp = data['customize_processing'] if data['type'] == 'linenum': diff --git a/kittens/runner.py b/kittens/runner.py index 5a0366be9b9..6ae9da09c0f 100644 --- a/kittens/runner.py +++ b/kittens/runner.py @@ -59,7 +59,10 @@ def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]: kitten = resolved_kitten(kitten) m = importlib.import_module(f'kittens.{kitten}.main') - return {'start': getattr(m, 'main'), 'end': getattr(m, 'handle_result', lambda *a, **k: None)} + return { + 'start': getattr(m, 'main'), + 'end': getattr(m, 'handle_result', lambda *a, **k: None), + } def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any: @@ -70,6 +73,7 @@ def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any: setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None)) setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False)) setattr(ans, 'has_ready_notification', getattr(m['end'], 'has_ready_notification', False)) + setattr(ans, 'open_url_handler', getattr(m['end'], 'open_url_handler', None)) return ans diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index 3fbbea27480..e3c5ed24edb 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -21,6 +21,7 @@ MouseEvent, ScreenSize, TermManagerType, + WindowType, ) from .operations import MouseTracking, pending_update @@ -29,6 +30,9 @@ from kitty.file_transmission import FileTransmissionCommand +OpenUrlHandler = Optional[Callable[[BossType, WindowType, str, int, str], bool]] + + class ButtonEvent(NamedTuple): mouse_event: MouseEvent timestamp: float @@ -223,23 +227,26 @@ class HandleResult: type_of_input: Optional[str] = None no_ui: bool = False - def __init__(self, impl: Callable[..., Any], type_of_input: Optional[str], no_ui: bool, has_ready_notification: bool): + def __init__(self, impl: Callable[..., Any], type_of_input: Optional[str], no_ui: bool, has_ready_notification: bool, open_url_handler: OpenUrlHandler): self.impl = impl self.no_ui = no_ui self.type_of_input = type_of_input self.has_ready_notification = has_ready_notification + self.open_url_handler = open_url_handler def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any: return self.impl(args, data, target_window_id, boss) + def result_handler( type_of_input: Optional[str] = None, no_ui: bool = False, - has_ready_notification: bool = Handler.overlay_ready_report_needed + has_ready_notification: bool = Handler.overlay_ready_report_needed, + open_url_handler: OpenUrlHandler = None, ) -> Callable[[Callable[..., Any]], HandleResult]: def wrapper(impl: Callable[..., Any]) -> HandleResult: - return HandleResult(impl, type_of_input, no_ui, has_ready_notification) + return HandleResult(impl, type_of_input, no_ui, has_ready_notification, open_url_handler) return wrapper diff --git a/kitty/boss.py b/kitty/boss.py index 76d04752ad1..7e6ef7bebd3 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -1919,6 +1919,7 @@ def run_kitten_with_metadata( ) wid = w.id overlay_window.actions_on_close.append(partial(self.on_kitten_finish, wid, custom_callback or end_kitten, default_data=default_data)) + overlay_window.open_url_handler = end_kitten.open_url_handler if action_on_removal is not None: def callback_wrapper(*a: Any) -> None: diff --git a/kitty/window.py b/kitty/window.py index 42c54851e40..3d0def19d2d 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -122,6 +122,8 @@ if TYPE_CHECKING: + from kittens.tui.handler import OpenUrlHandler + from .file_transmission import FileTransmission @@ -563,6 +565,7 @@ def __init__( self.current_clipboard_read_ask: Optional[bool] = None self.prev_osc99_cmd = NotificationCommand() self.last_cmd_output_start_time = 0. + self.open_url_handler: 'OpenUrlHandler' = None self.last_cmd_cmdline = '' self.last_cmd_exit_status = 0 self.actions_on_close: List[Callable[['Window'], None]] = [] @@ -1043,6 +1046,13 @@ def on_mouse_event(self, event: Dict[str, Any]) -> bool: return get_boss().combine(action, window_for_dispatch=self, dispatch_type='MouseEvent') def open_url(self, url: str, hyperlink_id: int, cwd: Optional[str] = None) -> None: + boss = get_boss() + try: + if self.open_url_handler and self.open_url_handler(boss, self, url, hyperlink_id, cwd or ''): + return + except Exception: + import traceback + traceback.print_exc() opts = get_options() if hyperlink_id: if not opts.allow_hyperlinks: @@ -1063,14 +1073,14 @@ def open_url(self, url: str, hyperlink_id: int, cwd: Optional[str] = None) -> No url = urlunparse(purl._replace(netloc='')) if opts.allow_hyperlinks & 0b10: from kittens.tui.operations import styled - get_boss().choose( + boss.choose( 'What would you like to do with this URL:\n' + styled(sanitize_url_for_dispay_to_user(url), fg='yellow'), partial(self.hyperlink_open_confirmed, url, cwd), 'o:Open', 'c:Copy to clipboard', 'n;red:Nothing', default='o', window=self, title=_('Hyperlink activated'), ) return - get_boss().open_url(url, cwd=cwd) + boss.open_url(url, cwd=cwd) def hyperlink_open_confirmed(self, url: str, cwd: Optional[str], q: str) -> None: if q == 'o':