hints kitten: Allow clicking on matched text to select it in addition to typing the hint

This commit is contained in:
Kovid Goyal
2024-05-14 15:20:18 +05:30
parent 38fed8b391
commit 8c1e365eb5
8 changed files with 69 additions and 9 deletions

View File

@@ -57,6 +57,8 @@ Detailed list of changes
- :opt:`notify_on_cmd_finish`: Show the actual command that was finished (:iss:`7420`) - :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 - 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`) - :opt:`paste_actions`: Fix ``replace-newline`` not working with ``confirm`` (:iss:`7374`)

View File

@@ -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 select that hint or press :kbd:`Enter` or :kbd:`Space` to select the empty
hint. 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 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 options and modes of operation, see below. You can use these options to
create mappings in :file:`kitty.conf` to select various different text create mappings in :file:`kitty.conf` to select various different text

View File

@@ -3,6 +3,7 @@
package hints package hints
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -183,7 +184,8 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
} else { } else {
mark_text = mark_text[len(hint):] 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 { render := func() string {
@@ -230,6 +232,30 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
draw_screen() draw_screen()
return nil 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 { lp.OnText = func(text string, _, _ bool) error {
changed := false changed := false
for _, ch := range text { for _, ch := range text {

View File

@@ -9,7 +9,7 @@ from kitty.cli_stub import HintsCLIOptions
from kitty.clipboard import set_clipboard_string, set_primary_selection from kitty.clipboard import set_clipboard_string, set_primary_selection
from kitty.constants import website_url from kitty.constants import website_url
from kitty.fast_data_types import get_options 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 kitty.utils import get_editor, resolve_custom_file
from ..tui.handler import result_handler from ..tui.handler import result_handler
@@ -312,7 +312,14 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i
}[action])(*cmd) }[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: def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
cp = data['customize_processing'] cp = data['customize_processing']
if data['type'] == 'linenum': if data['type'] == 'linenum':

View File

@@ -59,7 +59,10 @@ def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]:
kitten = resolved_kitten(kitten) kitten = resolved_kitten(kitten)
m = importlib.import_module(f'kittens.{kitten}.main') 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: 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, 'type_of_input', getattr(m['end'], 'type_of_input', None))
setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False)) setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
setattr(ans, 'has_ready_notification', getattr(m['end'], 'has_ready_notification', 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 return ans

View File

@@ -21,6 +21,7 @@ from kitty.typing import (
MouseEvent, MouseEvent,
ScreenSize, ScreenSize,
TermManagerType, TermManagerType,
WindowType,
) )
from .operations import MouseTracking, pending_update from .operations import MouseTracking, pending_update
@@ -29,6 +30,9 @@ if TYPE_CHECKING:
from kitty.file_transmission import FileTransmissionCommand from kitty.file_transmission import FileTransmissionCommand
OpenUrlHandler = Optional[Callable[[BossType, WindowType, str, int, str], bool]]
class ButtonEvent(NamedTuple): class ButtonEvent(NamedTuple):
mouse_event: MouseEvent mouse_event: MouseEvent
timestamp: float timestamp: float
@@ -223,23 +227,26 @@ class HandleResult:
type_of_input: Optional[str] = None type_of_input: Optional[str] = None
no_ui: bool = False 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.impl = impl
self.no_ui = no_ui self.no_ui = no_ui
self.type_of_input = type_of_input self.type_of_input = type_of_input
self.has_ready_notification = has_ready_notification 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: def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any:
return self.impl(args, data, target_window_id, boss) return self.impl(args, data, target_window_id, boss)
def result_handler( def result_handler(
type_of_input: Optional[str] = None, type_of_input: Optional[str] = None,
no_ui: bool = False, 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]: ) -> Callable[[Callable[..., Any]], HandleResult]:
def wrapper(impl: 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 return wrapper

View File

@@ -1919,6 +1919,7 @@ class Boss:
) )
wid = w.id 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.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: if action_on_removal is not None:
def callback_wrapper(*a: Any) -> None: def callback_wrapper(*a: Any) -> None:

View File

@@ -122,6 +122,8 @@ MatchPatternType = Union[Pattern[str], Tuple[Pattern[str], Optional[Pattern[str]
if TYPE_CHECKING: if TYPE_CHECKING:
from kittens.tui.handler import OpenUrlHandler
from .file_transmission import FileTransmission from .file_transmission import FileTransmission
@@ -563,6 +565,7 @@ class Window:
self.current_clipboard_read_ask: Optional[bool] = None self.current_clipboard_read_ask: Optional[bool] = None
self.prev_osc99_cmd = NotificationCommand() self.prev_osc99_cmd = NotificationCommand()
self.last_cmd_output_start_time = 0. self.last_cmd_output_start_time = 0.
self.open_url_handler: 'OpenUrlHandler' = None
self.last_cmd_cmdline = '' self.last_cmd_cmdline = ''
self.last_cmd_exit_status = 0 self.last_cmd_exit_status = 0
self.actions_on_close: List[Callable[['Window'], None]] = [] self.actions_on_close: List[Callable[['Window'], None]] = []
@@ -1043,6 +1046,13 @@ class Window:
return get_boss().combine(action, window_for_dispatch=self, dispatch_type='MouseEvent') 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: 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() opts = get_options()
if hyperlink_id: if hyperlink_id:
if not opts.allow_hyperlinks: if not opts.allow_hyperlinks:
@@ -1063,14 +1073,14 @@ class Window:
url = urlunparse(purl._replace(netloc='')) url = urlunparse(purl._replace(netloc=''))
if opts.allow_hyperlinks & 0b10: if opts.allow_hyperlinks & 0b10:
from kittens.tui.operations import styled 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'), '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), partial(self.hyperlink_open_confirmed, url, cwd),
'o:Open', 'c:Copy to clipboard', 'n;red:Nothing', default='o', 'o:Open', 'c:Copy to clipboard', 'n;red:Nothing', default='o',
window=self, title=_('Hyperlink activated'), window=self, title=_('Hyperlink activated'),
) )
return 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: def hyperlink_open_confirmed(self, url: str, cwd: Optional[str], q: str) -> None:
if q == 'o': if q == 'o':