Remove python implementation of hints
This commit is contained in:
parent
2e1eebd998
commit
69916ca4e8
@ -1,437 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from gettext import gettext as _
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
from itertools import repeat
|
|
||||||
from typing import Any, Callable, Dict, Generator, Iterable, Iterator, List, Optional, Pattern, Sequence, Set, Tuple, Type, cast
|
|
||||||
|
|
||||||
from kitty.cli import parse_args
|
|
||||||
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, wcswidth
|
from kitty.fast_data_types import get_options
|
||||||
from kitty.key_encoding import KeyEvent
|
from kitty.typing import BossType
|
||||||
from kitty.typing import BossType, KittyCommonOpts
|
from kitty.utils import resolve_custom_file
|
||||||
from kitty.utils import ScreenSize, kitty_ansi_sanitizer_pat, resolve_custom_file, screen_size_function
|
|
||||||
|
|
||||||
from ..tui.handler import Handler, result_handler
|
from ..tui.handler import result_handler
|
||||||
from ..tui.loop import Loop
|
|
||||||
from ..tui.operations import faint, styled
|
|
||||||
from ..tui.utils import report_error, report_unhandled_error
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
|
||||||
def kitty_common_opts() -> KittyCommonOpts:
|
|
||||||
import json
|
|
||||||
v = os.environ.get('KITTY_COMMON_OPTS')
|
|
||||||
if v:
|
|
||||||
return cast(KittyCommonOpts, json.loads(v))
|
|
||||||
from kitty.config import common_opts_as_dict
|
|
||||||
return common_opts_as_dict()
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase
|
|
||||||
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
|
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
|
||||||
FILE_EXTENSION = r'\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?!\.)'
|
|
||||||
PATH_REGEX = fr'(?:\S*?/[\r\S]+)|(?:\S[\r\S]*{FILE_EXTENSION})\b'
|
|
||||||
DEFAULT_LINENUM_REGEX = fr'(?P<path>{PATH_REGEX}):(?P<line>\d+)'
|
|
||||||
|
|
||||||
|
|
||||||
class Mark:
|
|
||||||
|
|
||||||
__slots__ = ('index', 'start', 'end', 'text', 'is_hyperlink', 'group_id', 'groupdict')
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
index: int, start: int, end: int,
|
|
||||||
text: str,
|
|
||||||
groupdict: Any,
|
|
||||||
is_hyperlink: bool = False,
|
|
||||||
group_id: Optional[str] = None
|
|
||||||
):
|
|
||||||
self.index, self.start, self.end = index, start, end
|
|
||||||
self.text = text
|
|
||||||
self.groupdict = groupdict
|
|
||||||
self.is_hyperlink = is_hyperlink
|
|
||||||
self.group_id = group_id
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (f'Mark(index={self.index!r}, start={self.start!r}, end={self.end!r},'
|
|
||||||
f' text={self.text!r}, groupdict={self.groupdict!r}, is_hyperlink={self.is_hyperlink!r}, group_id={self.group_id!r})')
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=2048)
|
|
||||||
def encode_hint(num: int, alphabet: str) -> str:
|
|
||||||
res = ''
|
|
||||||
d = len(alphabet)
|
|
||||||
while not res or num > 0:
|
|
||||||
num, i = divmod(num, d)
|
|
||||||
res = alphabet[i] + res
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def decode_hint(x: str, alphabet: str = DEFAULT_HINT_ALPHABET) -> int:
|
|
||||||
base = len(alphabet)
|
|
||||||
index_map = {c: i for i, c in enumerate(alphabet)}
|
|
||||||
i = 0
|
|
||||||
for char in x:
|
|
||||||
i = i * base + index_map[char]
|
|
||||||
return i
|
|
||||||
|
|
||||||
|
|
||||||
def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str, colors: Dict[str, str]) -> str:
|
|
||||||
hint = encode_hint(m.index, alphabet)
|
|
||||||
if current_input and not hint.startswith(current_input):
|
|
||||||
return faint(text)
|
|
||||||
hint = hint[len(current_input):] or ' '
|
|
||||||
text = text[len(hint):]
|
|
||||||
return styled(
|
|
||||||
hint,
|
|
||||||
fg=colors['foreground'],
|
|
||||||
bg=colors['background'],
|
|
||||||
bold=True
|
|
||||||
) + styled(
|
|
||||||
text, fg=colors['text'], fg_intense=True, bold=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def debug(*a: Any, **kw: Any) -> None:
|
|
||||||
from ..tui.loop import debug as d
|
|
||||||
d(*a, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark_indices: Set[int], alphabet: str, colors: Dict[str, str]) -> str:
|
|
||||||
for mark in reversed(all_marks):
|
|
||||||
if mark.index in ignore_mark_indices:
|
|
||||||
continue
|
|
||||||
mtext = highlight_mark(mark, text[mark.start:mark.end], current_input, alphabet, colors)
|
|
||||||
text = text[:mark.start] + mtext + text[mark.end:]
|
|
||||||
|
|
||||||
text = text.replace('\0', '')
|
|
||||||
return re.sub('[\r\n]', '\r\n', text).rstrip()
|
|
||||||
|
|
||||||
|
|
||||||
class Hints(Handler):
|
|
||||||
|
|
||||||
use_alternate_screen = False # disabled to avoid screen being blanked at exit causing flicker
|
|
||||||
overlay_ready_report_needed = True
|
|
||||||
|
|
||||||
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
|
|
||||||
self.text, self.index_map = text, index_map
|
|
||||||
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
|
|
||||||
self.colors = {'foreground': args.hints_foreground_color,
|
|
||||||
'background': args.hints_background_color,
|
|
||||||
'text': args.hints_text_color}
|
|
||||||
self.all_marks = all_marks
|
|
||||||
self.ignore_mark_indices: Set[int] = set()
|
|
||||||
self.args = args
|
|
||||||
self.window_title = args.window_title or (_('Choose URL') if args.type == 'url' else _('Choose text'))
|
|
||||||
self.multiple = args.multiple
|
|
||||||
self.match_suffix = self.get_match_suffix(args)
|
|
||||||
self.chosen: List[Mark] = []
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text_matches(self) -> List[str]:
|
|
||||||
return [m.text + self.match_suffix for m in self.chosen]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def groupdicts(self) -> List[Any]:
|
|
||||||
return [m.groupdict for m in self.chosen]
|
|
||||||
|
|
||||||
def get_match_suffix(self, args: HintsCLIOptions) -> str:
|
|
||||||
if args.add_trailing_space == 'always':
|
|
||||||
return ' '
|
|
||||||
if args.add_trailing_space == 'never':
|
|
||||||
return ''
|
|
||||||
return ' ' if args.multiple else ''
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self.current_input = ''
|
|
||||||
self.current_text: Optional[str] = None
|
|
||||||
|
|
||||||
def init_terminal_state(self) -> None:
|
|
||||||
self.cmd.set_cursor_visible(False)
|
|
||||||
self.cmd.set_window_title(self.window_title)
|
|
||||||
self.cmd.set_line_wrapping(False)
|
|
||||||
|
|
||||||
def initialize(self) -> None:
|
|
||||||
self.init_terminal_state()
|
|
||||||
self.draw_screen()
|
|
||||||
|
|
||||||
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
|
|
||||||
changed = False
|
|
||||||
for c in text:
|
|
||||||
if c in self.alphabet:
|
|
||||||
self.current_input += c
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
matches = [
|
|
||||||
m for idx, m in self.index_map.items()
|
|
||||||
if encode_hint(idx, self.alphabet).startswith(self.current_input)
|
|
||||||
]
|
|
||||||
if len(matches) == 1:
|
|
||||||
self.chosen.append(matches[0])
|
|
||||||
if self.multiple:
|
|
||||||
self.ignore_mark_indices.add(matches[0].index)
|
|
||||||
self.reset()
|
|
||||||
else:
|
|
||||||
self.quit_loop(0)
|
|
||||||
return
|
|
||||||
self.current_text = None
|
|
||||||
self.draw_screen()
|
|
||||||
|
|
||||||
def on_key(self, key_event: KeyEvent) -> None:
|
|
||||||
if key_event.matches('backspace'):
|
|
||||||
self.current_input = self.current_input[:-1]
|
|
||||||
self.current_text = None
|
|
||||||
self.draw_screen()
|
|
||||||
elif (key_event.matches('enter') or key_event.matches('space')) and self.current_input:
|
|
||||||
try:
|
|
||||||
idx = decode_hint(self.current_input, self.alphabet)
|
|
||||||
self.chosen.append(self.index_map[idx])
|
|
||||||
self.ignore_mark_indices.add(idx)
|
|
||||||
except Exception:
|
|
||||||
self.current_input = ''
|
|
||||||
self.current_text = None
|
|
||||||
self.draw_screen()
|
|
||||||
else:
|
|
||||||
if self.multiple:
|
|
||||||
self.reset()
|
|
||||||
self.draw_screen()
|
|
||||||
else:
|
|
||||||
self.quit_loop(0)
|
|
||||||
elif key_event.matches('esc'):
|
|
||||||
self.quit_loop(0 if self.multiple else 1)
|
|
||||||
|
|
||||||
def on_interrupt(self) -> None:
|
|
||||||
self.quit_loop(1)
|
|
||||||
|
|
||||||
def on_eot(self) -> None:
|
|
||||||
self.quit_loop(1)
|
|
||||||
|
|
||||||
def on_resize(self, new_size: ScreenSize) -> None:
|
|
||||||
self.draw_screen()
|
|
||||||
|
|
||||||
def draw_screen(self) -> None:
|
|
||||||
if self.current_text is None:
|
|
||||||
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices, self.alphabet, self.colors)
|
|
||||||
self.cmd.clear_screen()
|
|
||||||
self.write(self.current_text)
|
|
||||||
|
|
||||||
|
|
||||||
def regex_finditer(pat: 'Pattern[str]', minimum_match_length: int, text: str) -> Iterator[Tuple[int, int, 're.Match[str]']]:
|
|
||||||
has_named_groups = bool(pat.groupindex)
|
|
||||||
for m in pat.finditer(text):
|
|
||||||
s, e = m.span(0 if has_named_groups else pat.groups)
|
|
||||||
while e > s + 1 and text[e-1] == '\0':
|
|
||||||
e -= 1
|
|
||||||
if e - s >= minimum_match_length:
|
|
||||||
yield s, e, m
|
|
||||||
|
|
||||||
|
|
||||||
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'", "“": "”", "‘": "’"}
|
|
||||||
opening_brackets = ''.join(closing_bracket_map)
|
|
||||||
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
|
|
||||||
postprocessor_map: Dict[str, PostprocessorFunc] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def postprocessor(func: PostprocessorFunc) -> PostprocessorFunc:
|
|
||||||
postprocessor_map[func.__name__] = func
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidMatch(Exception):
|
|
||||||
"""Raised when a match turns out to be invalid."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@postprocessor
|
|
||||||
def url(text: str, s: int, e: int) -> Tuple[int, int]:
|
|
||||||
if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs
|
|
||||||
url = text[s:e]
|
|
||||||
idx = url.rfind('[')
|
|
||||||
if idx > -1:
|
|
||||||
e -= len(url) - idx
|
|
||||||
while text[e - 1] in '.,?!' and e > 1: # remove trailing punctuation
|
|
||||||
e -= 1
|
|
||||||
# truncate url at closing bracket/quote
|
|
||||||
if s > 0 and e <= len(text) and text[s-1] in opening_brackets:
|
|
||||||
q = closing_bracket_map[text[s-1]]
|
|
||||||
idx = text.find(q, s)
|
|
||||||
if idx > s:
|
|
||||||
e = idx
|
|
||||||
# Restructured Text URLs
|
|
||||||
if e > 3 and text[e-2:e] == '`_':
|
|
||||||
e -= 2
|
|
||||||
|
|
||||||
return s, e
|
|
||||||
|
|
||||||
|
|
||||||
@postprocessor
|
|
||||||
def brackets(text: str, s: int, e: int) -> Tuple[int, int]:
|
|
||||||
# Remove matching brackets
|
|
||||||
if s < e <= len(text):
|
|
||||||
before = text[s]
|
|
||||||
if before in '({[<':
|
|
||||||
q = closing_bracket_map[before]
|
|
||||||
if text[e-1] == q:
|
|
||||||
s += 1
|
|
||||||
e -= 1
|
|
||||||
elif text[e:e+1] == q:
|
|
||||||
s += 1
|
|
||||||
return s, e
|
|
||||||
|
|
||||||
|
|
||||||
@postprocessor
|
|
||||||
def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
|
|
||||||
# Remove matching quotes
|
|
||||||
if s < e <= len(text):
|
|
||||||
before = text[s]
|
|
||||||
if before in '\'"“‘':
|
|
||||||
q = closing_bracket_map[before]
|
|
||||||
if text[e-1] == q:
|
|
||||||
s += 1
|
|
||||||
e -= 1
|
|
||||||
elif text[e:e+1] == q:
|
|
||||||
s += 1
|
|
||||||
return s, e
|
|
||||||
|
|
||||||
|
|
||||||
@postprocessor
|
|
||||||
def ip(text: str, s: int, e: int) -> Tuple[int, int]:
|
|
||||||
from ipaddress import ip_address
|
|
||||||
|
|
||||||
# Check validity of IPs (or raise InvalidMatch)
|
|
||||||
ip = text[s:e]
|
|
||||||
|
|
||||||
try:
|
|
||||||
ip_address(ip)
|
|
||||||
except Exception:
|
|
||||||
raise InvalidMatch("Invalid IP")
|
|
||||||
|
|
||||||
return s, e
|
|
||||||
|
|
||||||
|
|
||||||
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Iterator[Mark]:
|
|
||||||
pat = re.compile(pattern)
|
|
||||||
sanitize_pat = re.compile('[\r\n\0]')
|
|
||||||
for idx, (s, e, match_object) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
|
|
||||||
try:
|
|
||||||
for func in post_processors:
|
|
||||||
s, e = func(text, s, e)
|
|
||||||
except InvalidMatch:
|
|
||||||
continue
|
|
||||||
groupdict = match_object.groupdict()
|
|
||||||
for group_name in groupdict:
|
|
||||||
group_idx = pat.groupindex[group_name]
|
|
||||||
gs, ge = match_object.span(group_idx)
|
|
||||||
gs, ge = max(gs, s), min(ge, e)
|
|
||||||
groupdict[group_name] = sanitize_pat.sub('', text[gs:ge])
|
|
||||||
mark_text = sanitize_pat.sub('', text[s:e])
|
|
||||||
yield Mark(idx, s, e, mark_text, groupdict)
|
|
||||||
|
|
||||||
|
|
||||||
def run_loop(args: HintsCLIOptions, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], extra_cli_args: Sequence[str] = ()) -> Dict[str, Any]:
|
|
||||||
loop = Loop()
|
|
||||||
handler = Hints(text, all_marks, index_map, args)
|
|
||||||
loop.loop(handler)
|
|
||||||
if handler.chosen and loop.return_code == 0:
|
|
||||||
return {
|
|
||||||
'match': handler.text_matches, 'programs': args.program,
|
|
||||||
'multiple_joiner': args.multiple_joiner, 'customize_processing': args.customize_processing,
|
|
||||||
'type': args.type, 'groupdicts': handler.groupdicts, 'extra_cli_args': extra_cli_args,
|
|
||||||
'linenum_action': args.linenum_action,
|
|
||||||
'cwd': os.getcwd(),
|
|
||||||
}
|
|
||||||
raise SystemExit(loop.return_code)
|
|
||||||
|
|
||||||
|
|
||||||
def escape(chars: str) -> str:
|
|
||||||
return chars.replace('\\', '\\\\').replace('-', r'\-').replace(']', r'\]')
|
|
||||||
|
|
||||||
|
|
||||||
def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]:
|
|
||||||
post_processors = []
|
|
||||||
if args.type == 'url':
|
|
||||||
if args.url_prefixes == 'default':
|
|
||||||
url_prefixes = kitty_common_opts()['url_prefixes']
|
|
||||||
else:
|
|
||||||
url_prefixes = tuple(args.url_prefixes.split(','))
|
|
||||||
from .url_regex import url_delimiters
|
|
||||||
pattern = '(?:{})://[^{}]{{3,}}'.format(
|
|
||||||
'|'.join(url_prefixes), url_delimiters
|
|
||||||
)
|
|
||||||
post_processors.append(url)
|
|
||||||
elif args.type == 'path':
|
|
||||||
pattern = PATH_REGEX
|
|
||||||
post_processors.extend((brackets, quotes))
|
|
||||||
elif args.type == 'line':
|
|
||||||
pattern = '(?m)^\\s*(.+)[\\s\0]*$'
|
|
||||||
elif args.type == 'hash':
|
|
||||||
pattern = '[0-9a-f][0-9a-f\r]{6,127}'
|
|
||||||
elif args.type == 'ip':
|
|
||||||
pattern = (
|
|
||||||
# # IPv4 with no validation
|
|
||||||
r"((?:\d{1,3}\.){3}\d{1,3}"
|
|
||||||
r"|"
|
|
||||||
# # IPv6 with no validation
|
|
||||||
r"(?:[a-fA-F0-9]{0,4}:){2,7}[a-fA-F0-9]{1,4})"
|
|
||||||
)
|
|
||||||
post_processors.append(ip)
|
|
||||||
elif args.type == 'word':
|
|
||||||
chars = args.word_characters
|
|
||||||
if chars is None:
|
|
||||||
chars = kitty_common_opts()['select_by_word_characters']
|
|
||||||
pattern = fr'(?u)[{escape(chars)}\w]{{{args.minimum_match_length},}}'
|
|
||||||
post_processors.extend((brackets, quotes))
|
|
||||||
else:
|
|
||||||
pattern = args.regex
|
|
||||||
return pattern, post_processors
|
|
||||||
|
|
||||||
|
|
||||||
def convert_text(text: str, cols: int) -> str:
|
|
||||||
lines: List[str] = []
|
|
||||||
empty_line = '\0' * cols + '\n'
|
|
||||||
for full_line in text.split('\n'):
|
|
||||||
if full_line:
|
|
||||||
if not full_line.rstrip('\r'): # empty lines
|
|
||||||
lines.extend(repeat(empty_line, len(full_line)))
|
|
||||||
continue
|
|
||||||
appended = False
|
|
||||||
for line in full_line.split('\r'):
|
|
||||||
if line:
|
|
||||||
line_sz = wcswidth(line)
|
|
||||||
if line_sz < cols:
|
|
||||||
line += '\0' * (cols - line_sz)
|
|
||||||
lines.append(line)
|
|
||||||
lines.append('\r')
|
|
||||||
appended = True
|
|
||||||
if appended:
|
|
||||||
lines[-1] = '\n'
|
|
||||||
rstripped = re.sub('[\r\n]+$', '', ''.join(lines))
|
|
||||||
return rstripped
|
|
||||||
|
|
||||||
|
|
||||||
def parse_input(text: str) -> str:
|
|
||||||
try:
|
|
||||||
cols = int(os.environ['OVERLAID_WINDOW_COLS'])
|
|
||||||
except KeyError:
|
|
||||||
cols = screen_size_function()().cols
|
|
||||||
return convert_text(text, cols)
|
|
||||||
|
|
||||||
|
|
||||||
def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]:
|
|
||||||
regex = args.regex
|
|
||||||
if regex == DEFAULT_REGEX:
|
|
||||||
regex = DEFAULT_LINENUM_REGEX
|
|
||||||
yield from mark(regex, [brackets, quotes], text, args)
|
|
||||||
|
|
||||||
|
|
||||||
def load_custom_processor(customize_processing: str) -> Any:
|
def load_custom_processor(customize_processing: str) -> Any:
|
||||||
if customize_processing.startswith('::import::'):
|
if customize_processing.startswith('::import::'):
|
||||||
@ -439,95 +21,12 @@ def load_custom_processor(customize_processing: str) -> Any:
|
|||||||
m = importlib.import_module(customize_processing[len('::import::'):])
|
m = importlib.import_module(customize_processing[len('::import::'):])
|
||||||
return {k: getattr(m, k) for k in dir(m)}
|
return {k: getattr(m, k) for k in dir(m)}
|
||||||
if customize_processing == '::linenum::':
|
if customize_processing == '::linenum::':
|
||||||
return {'mark': linenum_marks, 'handle_result': linenum_handle_result}
|
return {'handle_result': linenum_handle_result}
|
||||||
custom_path = resolve_custom_file(customize_processing)
|
custom_path = resolve_custom_file(customize_processing)
|
||||||
import runpy
|
import runpy
|
||||||
return runpy.run_path(custom_path, run_name='__main__')
|
return runpy.run_path(custom_path, run_name='__main__')
|
||||||
|
|
||||||
|
|
||||||
def process_escape_codes(text: str) -> Tuple[str, Tuple[Mark, ...]]:
|
|
||||||
hyperlinks: List[Mark] = []
|
|
||||||
removed_size = idx = 0
|
|
||||||
active_hyperlink_url: Optional[str] = None
|
|
||||||
active_hyperlink_id: Optional[str] = None
|
|
||||||
active_hyperlink_start_offset = 0
|
|
||||||
|
|
||||||
def add_hyperlink(end: int) -> None:
|
|
||||||
nonlocal idx, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
|
||||||
assert active_hyperlink_url is not None
|
|
||||||
hyperlinks.append(Mark(
|
|
||||||
idx, active_hyperlink_start_offset, end,
|
|
||||||
active_hyperlink_url,
|
|
||||||
groupdict={},
|
|
||||||
is_hyperlink=True, group_id=active_hyperlink_id
|
|
||||||
))
|
|
||||||
active_hyperlink_url = active_hyperlink_id = None
|
|
||||||
active_hyperlink_start_offset = 0
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
def process_hyperlink(m: 're.Match[str]') -> str:
|
|
||||||
nonlocal removed_size, active_hyperlink_url, active_hyperlink_id, active_hyperlink_start_offset
|
|
||||||
raw = m.group()
|
|
||||||
if not raw.startswith('\x1b]8'):
|
|
||||||
removed_size += len(raw)
|
|
||||||
return ''
|
|
||||||
start = m.start() - removed_size
|
|
||||||
removed_size += len(raw)
|
|
||||||
if active_hyperlink_url is not None:
|
|
||||||
add_hyperlink(start)
|
|
||||||
raw = raw[4:-2]
|
|
||||||
parts = raw.split(';', 1)
|
|
||||||
if len(parts) == 2 and parts[1]:
|
|
||||||
active_hyperlink_url = parts[1]
|
|
||||||
active_hyperlink_start_offset = start
|
|
||||||
if parts[0]:
|
|
||||||
for entry in parts[0].split(':'):
|
|
||||||
if entry.startswith('id=') and len(entry) > 3:
|
|
||||||
active_hyperlink_id = entry[3:]
|
|
||||||
break
|
|
||||||
|
|
||||||
return ''
|
|
||||||
|
|
||||||
text = kitty_ansi_sanitizer_pat().sub(process_hyperlink, text)
|
|
||||||
if active_hyperlink_url is not None:
|
|
||||||
add_hyperlink(len(text))
|
|
||||||
return text, tuple(hyperlinks)
|
|
||||||
|
|
||||||
|
|
||||||
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
|
|
||||||
try:
|
|
||||||
text = parse_input(text)
|
|
||||||
text, hyperlinks = process_escape_codes(text)
|
|
||||||
pattern, post_processors = functions_for(args)
|
|
||||||
if args.type == 'linenum':
|
|
||||||
args.customize_processing = '::linenum::'
|
|
||||||
if args.type == 'hyperlink':
|
|
||||||
all_marks = hyperlinks
|
|
||||||
elif args.customize_processing:
|
|
||||||
m = load_custom_processor(args.customize_processing)
|
|
||||||
if 'mark' in m:
|
|
||||||
all_marks = tuple(m['mark'](text, args, Mark, extra_cli_args))
|
|
||||||
else:
|
|
||||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
|
||||||
else:
|
|
||||||
all_marks = tuple(mark(pattern, post_processors, text, args))
|
|
||||||
if not all_marks:
|
|
||||||
none_of = {'url': 'URLs', 'hyperlink': 'hyperlinks'}.get(args.type, 'matches')
|
|
||||||
report_error(_('No {} found.').format(none_of))
|
|
||||||
return None
|
|
||||||
|
|
||||||
largest_index = all_marks[-1].index
|
|
||||||
offset = max(0, args.hints_offset)
|
|
||||||
for m in all_marks:
|
|
||||||
if args.ascending:
|
|
||||||
m.index += offset
|
|
||||||
else:
|
|
||||||
m.index = largest_index - m.index + offset
|
|
||||||
index_map = {m.index: m for m in all_marks}
|
|
||||||
except Exception:
|
|
||||||
report_unhandled_error()
|
|
||||||
return run_loop(args, text, all_marks, index_map, extra_cli_args)
|
|
||||||
|
|
||||||
|
|
||||||
# CLI {{{
|
# CLI {{{
|
||||||
OPTIONS = r'''
|
OPTIONS = r'''
|
||||||
@ -698,31 +197,8 @@ help_text = 'Select text from the screen using the keyboard. Defaults to searchi
|
|||||||
usage = ''
|
usage = ''
|
||||||
|
|
||||||
|
|
||||||
def parse_hints_args(args: List[str]) -> Tuple[HintsCLIOptions, List[str]]:
|
|
||||||
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints', result_class=HintsCLIOptions)
|
|
||||||
|
|
||||||
|
|
||||||
def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
def main(args: List[str]) -> Optional[Dict[str, Any]]:
|
||||||
text = ''
|
raise SystemExit('Should be run as kitten hints')
|
||||||
if sys.stdin.isatty():
|
|
||||||
if '--help' not in args and '-h' not in args:
|
|
||||||
report_unhandled_error('You must pass the text to be hinted on STDIN')
|
|
||||||
else:
|
|
||||||
text = sys.stdin.buffer.read().decode('utf-8')
|
|
||||||
sys.stdin = open(os.ctermid())
|
|
||||||
try:
|
|
||||||
opts, items = parse_hints_args(args[1:])
|
|
||||||
except SystemExit as e:
|
|
||||||
if e.code != 0:
|
|
||||||
report_unhandled_error(e.args[0])
|
|
||||||
return None
|
|
||||||
if items and not (opts.customize_processing or opts.type == 'linenum'):
|
|
||||||
report_unhandled_error('Extra command line arguments present: {}'.format(' '.join(items)))
|
|
||||||
try:
|
|
||||||
return run(opts, text, items)
|
|
||||||
except Exception:
|
|
||||||
report_unhandled_error()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]:
|
def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]:
|
||||||
@ -775,13 +251,13 @@ 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=Hints.overlay_ready_report_needed)
|
@result_handler(type_of_input='screen-ansi', has_ready_notification=True)
|
||||||
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':
|
||||||
cp = '::linenum::'
|
cp = '::linenum::'
|
||||||
if cp:
|
if cp:
|
||||||
m = load_custom_processor(data['customize_processing'])
|
m = load_custom_processor(cp)
|
||||||
if 'handle_result' in m:
|
if 'handle_result' in m:
|
||||||
m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args'])
|
m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args'])
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -318,7 +318,7 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
|
|||||||
result.Match[i] = m.Text + match_suffix
|
result.Match[i] = m.Text + match_suffix
|
||||||
result.Groupdicts[i] = m.Groupdict
|
result.Groupdicts[i] = m.Groupdict
|
||||||
}
|
}
|
||||||
output(result)
|
fmt.Println(output(result))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user