705 lines
25 KiB
Python
705 lines
25 KiB
Python
from functools import total_ordering
|
||
from itertools import takewhile
|
||
import json
|
||
import os.path
|
||
import re
|
||
import sys
|
||
from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, List,
|
||
NamedTuple, Optional, Set, Tuple, Type, Union)
|
||
import unicodedata
|
||
|
||
from kitty.boss import Boss
|
||
from kitty.cli import parse_args
|
||
from kitten_options_types import Options, defaults
|
||
from kitten_options_parse import create_result_dict, merge_result_dicts, parse_conf_item
|
||
from kitty.conf.utils import load_config as _load_config, parse_config_base, resolve_config
|
||
from kitty.constants import config_dir
|
||
from kitty.fast_data_types import truncate_point_for_length, wcswidth
|
||
import kitty.key_encoding as kk
|
||
from kitty.key_encoding import KeyEvent
|
||
from kitty.rgb import color_as_sgr
|
||
from kittens.tui.handler import Handler
|
||
from kittens.tui.loop import Loop
|
||
|
||
|
||
try:
|
||
from kitty.clipboard import set_clipboard_string
|
||
except ImportError:
|
||
from kitty.fast_data_types import set_clipboard_string
|
||
|
||
|
||
if TYPE_CHECKING:
|
||
from typing_extensions import TypedDict
|
||
ResultDict = TypedDict('ResultDict', {'copy': str})
|
||
|
||
AbsoluteLine = int
|
||
ScreenLine = int
|
||
ScreenColumn = int
|
||
SelectionInLine = Union[Tuple[ScreenColumn, ScreenColumn],
|
||
Tuple[None, None]]
|
||
|
||
|
||
PositionBase = NamedTuple('Position', [
|
||
('x', ScreenColumn), ('y', ScreenLine), ('top_line', AbsoluteLine)])
|
||
class Position(PositionBase):
|
||
"""
|
||
Coordinates of a cell.
|
||
|
||
:param x: 0-based, left of window, to the right
|
||
:param y: 0-based, top of window, down
|
||
:param top_line: 1-based, start of scrollback, down
|
||
"""
|
||
@property
|
||
def line(self) -> AbsoluteLine:
|
||
"""
|
||
Return 1-based absolute line number.
|
||
"""
|
||
return self.y + self.top_line
|
||
|
||
def moved(self, dx: int = 0, dy: int = 0,
|
||
dtop: int = 0) -> 'Position':
|
||
"""
|
||
Return a new position specified relative to self.
|
||
"""
|
||
return self._replace(x=self.x + dx, y=self.y + dy,
|
||
top_line=self.top_line + dtop)
|
||
|
||
def scrolled(self, dtop: int = 0) -> 'Position':
|
||
"""
|
||
Return a new position equivalent to self
|
||
but scrolled dtop lines.
|
||
"""
|
||
return self.moved(dy=-dtop, dtop=dtop)
|
||
|
||
def scrolled_up(self, rows: ScreenLine) -> 'Position':
|
||
"""
|
||
Return a new position equivalent to self
|
||
but with top_line as small as possible.
|
||
"""
|
||
return self.scrolled(-min(self.top_line - 1,
|
||
rows - 1 - self.y))
|
||
|
||
def scrolled_down(self, rows: ScreenLine,
|
||
lines: AbsoluteLine) -> 'Position':
|
||
"""
|
||
Return a new position equivalent to self
|
||
but with top_line as large as possible.
|
||
"""
|
||
return self.scrolled(min(lines - rows + 1 - self.top_line,
|
||
self.y))
|
||
|
||
def scrolled_towards(self, other: 'Position', rows: ScreenLine,
|
||
lines: Optional[AbsoluteLine] = None) -> 'Position':
|
||
"""
|
||
Return a new position equivalent to self.
|
||
If self and other fit within a single screen,
|
||
scroll as little as possible to make both visible.
|
||
Otherwise, scroll as much as possible towards other.
|
||
"""
|
||
# @
|
||
# .| . @| . .
|
||
# |.| |. |.| |. |.|
|
||
# |*| |*| |*| |*| |*|
|
||
# |. |.| |. |.| |@|
|
||
# . .| . @| .
|
||
# @
|
||
if other.line <= self.line - rows: # above, unreachable
|
||
return self.scrolled_up(rows)
|
||
if other.line >= self.line + rows: # below, unreachable
|
||
assert lines is not None
|
||
return self.scrolled_down(rows, lines)
|
||
if other.line < self.top_line: # above, reachable
|
||
return self.scrolled(other.line - self.top_line)
|
||
if other.line > self.top_line + rows - 1: # below, reachable
|
||
return self.scrolled(other.line - self.top_line - rows + 1)
|
||
return self # visible
|
||
|
||
def __str__(self) -> str:
|
||
return '{},{}+{}'.format(self.x, self.y, self.top_line)
|
||
|
||
def __lt__(self, other: Any) -> bool:
|
||
if not isinstance(other, Position):
|
||
return NotImplemented
|
||
return (self.line, self.x) < (other.line, other.x)
|
||
|
||
def __le__(self, other: Any) -> bool:
|
||
if not isinstance(other, Position):
|
||
return NotImplemented
|
||
return (self.line, self.x) <= (other.line, other.x)
|
||
|
||
def __gt__(self, other: Any) -> bool:
|
||
if not isinstance(other, Position):
|
||
return NotImplemented
|
||
return (self.line, self.x) > (other.line, other.x)
|
||
|
||
def __ge__(self, other: Any) -> bool:
|
||
if not isinstance(other, Position):
|
||
return NotImplemented
|
||
return (self.line, self.x) >= (other.line, other.x)
|
||
|
||
def __eq__(self, other: Any) -> bool:
|
||
if not isinstance(other, Position):
|
||
return NotImplemented
|
||
return (self.line, self.x) == (other.line, other.x)
|
||
|
||
def __ne__(self, other: Any) -> bool:
|
||
if not isinstance(other, Position):
|
||
return NotImplemented
|
||
return (self.line, self.x) != (other.line, other.x)
|
||
|
||
|
||
def _span(line: AbsoluteLine, *lines: AbsoluteLine) -> Set[AbsoluteLine]:
|
||
return set(range(min(line, *lines), max(line, *lines) + 1))
|
||
|
||
|
||
class Region:
|
||
name = None # type: Optional[str]
|
||
uses_mark = False
|
||
|
||
@staticmethod
|
||
def line_inside_region(current_line: AbsoluteLine,
|
||
start: Position, end: Position) -> bool:
|
||
"""
|
||
Return True if current_line is entirely inside the region
|
||
defined by start and end.
|
||
"""
|
||
return False
|
||
|
||
@staticmethod
|
||
def line_outside_region(current_line: AbsoluteLine,
|
||
start: Position, end: Position) -> bool:
|
||
"""
|
||
Return True if current_line is entirely outside the region
|
||
defined by start and end.
|
||
"""
|
||
return current_line < start.line or end.line < current_line
|
||
|
||
@staticmethod
|
||
def adjust(start: Position, end: Position) -> Tuple[Position, Position]:
|
||
"""
|
||
Return the normalized pair of markers
|
||
equivalent to start and end. This is region-type-specific.
|
||
"""
|
||
return start, end
|
||
|
||
@staticmethod
|
||
def selection_in_line(
|
||
current_line: int, start: Position, end: Position,
|
||
maxx: int) -> SelectionInLine:
|
||
"""
|
||
Return bounds of the part of current_line
|
||
that are within the region defined by start and end.
|
||
"""
|
||
return None, None
|
||
|
||
@staticmethod
|
||
def lines_affected(mark: Optional[Position], old_point: Position,
|
||
point: Position) -> Set[AbsoluteLine]:
|
||
"""
|
||
Return the set of lines (1-based, top of scrollback, down)
|
||
that must be redrawn when point moves from old_point.
|
||
"""
|
||
return set()
|
||
|
||
@staticmethod
|
||
def page_up(mark: Optional[Position], point: Position,
|
||
rows: ScreenLine, lines: AbsoluteLine) -> Position:
|
||
"""
|
||
Return the position page up from point.
|
||
"""
|
||
# ........
|
||
# ....$...|
|
||
# ........ ....$...| ........|
|
||
# |....$...| |....^...| |....^...|
|
||
# |....^...| |........| |........
|
||
# |........| |........ |........
|
||
# ........ ........ ........
|
||
if point.y > 0:
|
||
return Position(point.x, 0, point.top_line)
|
||
assert point.y == 0
|
||
return Position(point.x, 0,
|
||
max(1, point.top_line - rows + 1))
|
||
|
||
@staticmethod
|
||
def page_down(mark: Optional[Position], point: Position,
|
||
rows: ScreenLine, lines: AbsoluteLine) -> Position:
|
||
"""
|
||
Return the position page down from point.
|
||
"""
|
||
# ........ ........ ........
|
||
# |........| |........ |........
|
||
# |....^...| |........| |........
|
||
# |....$...| |....^...| |....^...|
|
||
# ........ ....$...| ........|
|
||
# ....$...|
|
||
# ........
|
||
maxy = rows - 1
|
||
if point.y < maxy:
|
||
return Position(point.x, maxy, point.top_line)
|
||
assert point.y == maxy
|
||
return Position(point.x, maxy,
|
||
min(lines - maxy, point.top_line + maxy))
|
||
|
||
|
||
class NoRegion(Region):
|
||
name = 'unselected'
|
||
uses_mark = False
|
||
|
||
@staticmethod
|
||
def line_outside_region(current_line: AbsoluteLine,
|
||
start: Position, end: Position) -> bool:
|
||
return False
|
||
|
||
|
||
class MarkedRegion(Region):
|
||
uses_mark = True
|
||
|
||
# When a region is marked,
|
||
# override page up and down motion
|
||
# to keep as much region visible as possible.
|
||
#
|
||
# This means,
|
||
# after computing the position in the usual way,
|
||
# do the minimum possible scroll adjustment
|
||
# to bring both mark and point on screen.
|
||
# If that is not possible,
|
||
# do the maximum possible scroll adjustment
|
||
# towards mark
|
||
# that keeps point on screen.
|
||
@staticmethod
|
||
def page_up(mark: Optional[Position], point: Position,
|
||
rows: ScreenLine, lines: AbsoluteLine) -> Position:
|
||
assert mark is not None
|
||
return (Region.page_up(mark, point, rows, lines)
|
||
.scrolled_towards(mark, rows, lines))
|
||
|
||
@staticmethod
|
||
def page_down(mark: Optional[Position], point: Position,
|
||
rows: ScreenLine, lines: AbsoluteLine) -> Position:
|
||
assert mark is not None
|
||
return (Region.page_down(mark, point, rows, lines)
|
||
.scrolled_towards(mark, rows, lines))
|
||
|
||
|
||
class StreamRegion(MarkedRegion):
|
||
name = 'stream'
|
||
|
||
@staticmethod
|
||
def line_inside_region(current_line: AbsoluteLine,
|
||
start: Position, end: Position) -> bool:
|
||
return start.line < current_line < end.line
|
||
|
||
@staticmethod
|
||
def selection_in_line(
|
||
current_line: AbsoluteLine, start: Position, end: Position,
|
||
maxx: ScreenColumn) -> SelectionInLine:
|
||
if StreamRegion.line_outside_region(current_line, start, end):
|
||
return None, None
|
||
return (start.x if current_line == start.line else 0,
|
||
end.x if current_line == end.line else maxx)
|
||
|
||
@staticmethod
|
||
def lines_affected(mark: Optional[Position], old_point: Position,
|
||
point: Position) -> Set[AbsoluteLine]:
|
||
return _span(old_point.line, point.line)
|
||
|
||
|
||
class ColumnarRegion(MarkedRegion):
|
||
name = 'columnar'
|
||
|
||
@staticmethod
|
||
def adjust(start: Position, end: Position) -> Tuple[Position, Position]:
|
||
return (start._replace(x=min(start.x, end.x)),
|
||
end._replace(x=max(start.x, end.x)))
|
||
|
||
@staticmethod
|
||
def selection_in_line(
|
||
current_line: AbsoluteLine, start: Position, end: Position,
|
||
maxx: ScreenColumn) -> SelectionInLine:
|
||
if ColumnarRegion.line_outside_region(current_line, start, end):
|
||
return None, None
|
||
return start.x, end.x
|
||
|
||
@staticmethod
|
||
def lines_affected(mark: Optional[Position], old_point: Position,
|
||
point: Position) -> Set[AbsoluteLine]:
|
||
assert mark is not None
|
||
# If column changes, all lines change.
|
||
if old_point.x != point.x:
|
||
return _span(mark.line, old_point.line, point.line)
|
||
# If point passes mark, all passed lines change except mark line.
|
||
if old_point < mark < point or point < mark < old_point:
|
||
return _span(old_point.line, point.line) - {mark.line}
|
||
# If point moves away from mark,
|
||
# all passed lines change except old point line.
|
||
elif mark < old_point < point or point < old_point < mark:
|
||
return _span(old_point.line, point.line) - {old_point.line}
|
||
# Otherwise, point moves toward mark,
|
||
# and all passed lines change except new point line.
|
||
else:
|
||
return _span(old_point.line, point.line) - {point.line}
|
||
|
||
|
||
ActionName = str
|
||
ActionArgs = tuple
|
||
ShortcutMods = int
|
||
KeyName = str
|
||
Namespace = Any # kitty.cli.Namespace (< 0.17.0)
|
||
OptionName = str
|
||
OptionValues = Dict[OptionName, Any]
|
||
TypeMap = Dict[OptionName, Callable[[Any], Any]]
|
||
|
||
|
||
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> Options:
|
||
|
||
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
|
||
ans: Dict[str, Any] = create_result_dict()
|
||
parse_config_base(
|
||
lines,
|
||
parse_conf_item,
|
||
ans,
|
||
)
|
||
return ans
|
||
|
||
configs = list(resolve_config('/etc/xdg/kitty/grab.conf',
|
||
os.path.join(config_dir, 'grab.conf'),
|
||
config_files_on_cmd_line=[]))
|
||
overrides = tuple(overrides) if overrides is not None else ()
|
||
opts_dict, paths = _load_config(defaults, parse_config, merge_result_dicts, *configs, overrides=overrides)
|
||
opts = Options(opts_dict)
|
||
opts.config_paths = paths
|
||
opts.config_overrides = overrides
|
||
return opts
|
||
|
||
|
||
def unstyled(s: str) -> str:
|
||
s = re.sub(r'\x1b\[[0-9;:]*m', '', s)
|
||
s = re.sub(r'\x1b\](?:[^\x07\x1b]+|\x1b[^\\])*(?:\x1b\\|\x07)', '', s)
|
||
return s
|
||
|
||
|
||
def string_slice(s: str, start_x: ScreenColumn,
|
||
end_x: ScreenColumn) -> Tuple[str, bool]:
|
||
prev_pos = (truncate_point_for_length(s, start_x - 1) if start_x > 0
|
||
else None)
|
||
start_pos = truncate_point_for_length(s, start_x)
|
||
end_pos = truncate_point_for_length(s, end_x - 1) + 1
|
||
return s[start_pos:end_pos], prev_pos == start_pos
|
||
|
||
|
||
DirectionStr = str
|
||
RegionTypeStr = str
|
||
ModeTypeStr = str
|
||
|
||
|
||
class GrabHandler(Handler):
|
||
def __init__(self, args: Namespace, opts: Options,
|
||
lines: List[str]) -> None:
|
||
super().__init__()
|
||
self.args = args
|
||
self.opts = opts
|
||
self.lines = lines
|
||
self.point = Position(args.x, args.y, args.top_line)
|
||
self.mark = None # type: Optional[Position]
|
||
self.mark_type = NoRegion # type: Type[Region]
|
||
self.mode = 'normal' # type: ModeTypeStr
|
||
self.result = None # type: Optional[ResultDict]
|
||
for spec, action in self.opts.map:
|
||
self.add_shortcut(action, spec)
|
||
|
||
def _start_end(self) -> Tuple[Position, Position]:
|
||
start, end = sorted([self.point, self.mark or self.point])
|
||
return self.mark_type.adjust(start, end)
|
||
|
||
def _draw_line(self, current_line: AbsoluteLine) -> None:
|
||
y = current_line - self.point.top_line # type: ScreenLine
|
||
line = self.lines[current_line - 1]
|
||
clear_eol = '\x1b[m\x1b[K'
|
||
sgr0 = '\x1b[m'
|
||
|
||
plain = unstyled(line)
|
||
selection_sgr = '\x1b[38{};48{}m'.format(
|
||
color_as_sgr(self.opts.selection_foreground),
|
||
color_as_sgr(self.opts.selection_background))
|
||
start, end = self._start_end()
|
||
|
||
# anti-flicker optimization
|
||
if self.mark_type.line_inside_region(current_line, start, end):
|
||
self.cmd.set_cursor_position(0, y)
|
||
self.print('{}{}'.format(selection_sgr, plain),
|
||
end=clear_eol)
|
||
return
|
||
|
||
self.cmd.set_cursor_position(0, y)
|
||
self.print('{}{}'.format(sgr0, line), end=clear_eol)
|
||
|
||
if self.mark_type.line_outside_region(current_line, start, end):
|
||
return
|
||
|
||
start_x, end_x = self.mark_type.selection_in_line(
|
||
current_line, start, end, wcswidth(plain))
|
||
if start_x is None or end_x is None:
|
||
return
|
||
|
||
line_slice, half = string_slice(plain, start_x, end_x)
|
||
self.cmd.set_cursor_position(start_x - (1 if half else 0), y)
|
||
self.print('{}{}'.format(selection_sgr, line_slice), end='')
|
||
|
||
def _update(self) -> None:
|
||
self.cmd.set_window_title('Grab – {} {} {},{}+{} to {},{}+{}'.format(
|
||
self.args.title,
|
||
self.mark_type.name,
|
||
getattr(self.mark, 'x', None), getattr(self.mark, 'y', None),
|
||
getattr(self.mark, 'top_line', None),
|
||
self.point.x, self.point.y, self.point.top_line))
|
||
self.cmd.set_cursor_position(self.point.x, self.point.y)
|
||
|
||
def _redraw_lines(self, lines: Iterable[AbsoluteLine]) -> None:
|
||
for line in lines:
|
||
self._draw_line(line)
|
||
self._update()
|
||
|
||
def _redraw(self) -> None:
|
||
self._redraw_lines(range(
|
||
self.point.top_line,
|
||
self.point.top_line + self.screen_size.rows))
|
||
|
||
def initialize(self) -> None:
|
||
self.cmd.set_window_title('Grab – {}'.format(self.args.title))
|
||
self._redraw()
|
||
|
||
def perform_default_key_action(self, key_event: KeyEvent) -> bool:
|
||
return False
|
||
|
||
def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None:
|
||
action = self.shortcut_action(key_event)
|
||
if (key_event.type not in [kk.PRESS, kk.REPEAT]
|
||
or action is None):
|
||
return
|
||
self.perform_action(action)
|
||
|
||
def perform_action(self, action: Tuple[ActionName, ActionArgs]) -> None:
|
||
func, args = action
|
||
getattr(self, func)(*args)
|
||
|
||
def quit(self, *args: Any) -> None:
|
||
self.quit_loop(1)
|
||
|
||
region_types = {'stream': StreamRegion,
|
||
'columnar': ColumnarRegion
|
||
} # type: Dict[RegionTypeStr, Type[Region]]
|
||
|
||
mode_types = {'normal': NoRegion,
|
||
'visual': StreamRegion,
|
||
'block': ColumnarRegion,
|
||
} # type: Dict[ModeTypeStr, Type[Region]]
|
||
|
||
def _ensure_mark(self, mark_type: Type[Region] = StreamRegion) -> None:
|
||
need_redraw = mark_type is not self.mark_type
|
||
self.mark_type = mark_type
|
||
self.mark = (self.mark or self.point) if mark_type.uses_mark else None
|
||
if need_redraw:
|
||
self._redraw()
|
||
|
||
def _scroll(self, dtop: int) -> None:
|
||
rows = self.screen_size.rows
|
||
new_point = self.point.moved(dtop=dtop)
|
||
if not (0 < new_point.top_line <= 1 + len(self.lines) - rows):
|
||
return
|
||
self.point = new_point
|
||
self._redraw()
|
||
|
||
def scroll(self, direction: DirectionStr) -> None:
|
||
self._scroll(dtop={'up': -1, 'down': 1}[direction])
|
||
|
||
def left(self) -> Position:
|
||
return self.point.moved(dx=-1) if self.point.x > 0 else self.point
|
||
|
||
def right(self) -> Position:
|
||
return (self.point.moved(dx=1)
|
||
if self.point.x + 1 < self.screen_size.cols
|
||
else self.point)
|
||
|
||
def up(self) -> Position:
|
||
return (self.point.moved(dy=-1) if self.point.y > 0 else
|
||
self.point.moved(dtop=-1) if self.point.top_line > 0 else
|
||
self.point)
|
||
|
||
def down(self) -> Position:
|
||
return (self.point.moved(dy=1)
|
||
if self.point.y + 1 < self.screen_size.rows
|
||
else self.point.moved(dtop=1)
|
||
if self.point.line < len(self.lines)
|
||
else self.point)
|
||
|
||
def page_up(self) -> Position:
|
||
return self.mark_type.page_up(
|
||
self.mark, self.point, self.screen_size.rows,
|
||
max(self.screen_size.rows, len(self.lines)))
|
||
|
||
def page_down(self) -> Position:
|
||
return self.mark_type.page_down(
|
||
self.mark, self.point, self.screen_size.rows,
|
||
max(self.screen_size.rows, len(self.lines)))
|
||
|
||
def first(self) -> Position:
|
||
return Position(0, self.point.y, self.point.top_line)
|
||
|
||
def first_nonwhite(self) -> Position:
|
||
line = unstyled(self.lines[self.point.line - 1])
|
||
prefix = ''.join(takewhile(str.isspace, line))
|
||
return Position(wcswidth(prefix), self.point.y, self.point.top_line)
|
||
|
||
def last_nonwhite(self) -> Position:
|
||
line = unstyled(self.lines[self.point.line - 1])
|
||
suffix = ''.join(takewhile(str.isspace, reversed(line)))
|
||
return Position(wcswidth(line[:len(line) - len(suffix)]),
|
||
self.point.y, self.point.top_line)
|
||
|
||
def last(self) -> Position:
|
||
return Position(self.screen_size.cols,
|
||
self.point.y, self.point.top_line)
|
||
|
||
def top(self) -> Position:
|
||
return Position(0, 0, 1)
|
||
|
||
def bottom(self) -> Position:
|
||
x = wcswidth(unstyled(self.lines[-1]))
|
||
y = min(len(self.lines) - self.point.top_line,
|
||
self.screen_size.rows - 1)
|
||
return Position(x, y, len(self.lines) - y)
|
||
|
||
def noop(self) -> Position:
|
||
return self.point
|
||
|
||
@property
|
||
def _select_by_word_characters(self) -> str:
|
||
return (self.opts.select_by_word_characters
|
||
or (json.loads(os.getenv('KITTY_COMMON_OPTS', '{}'))
|
||
.get('select_by_word_characters', '@-./_~?&=%+#')))
|
||
|
||
def _is_word_char(self, c: str) -> bool:
|
||
return (unicodedata.category(c)[0] in 'LN'
|
||
or c in self._select_by_word_characters)
|
||
|
||
def _is_word_separator(self, c: str) -> bool:
|
||
return (unicodedata.category(c)[0] not in 'LN'
|
||
and c not in self._select_by_word_characters)
|
||
|
||
def word_left(self) -> Position:
|
||
if self.point.x > 0:
|
||
line = unstyled(self.lines[self.point.line - 1])
|
||
pos = truncate_point_for_length(line, self.point.x)
|
||
pred = (self._is_word_char if self._is_word_char(line[pos - 1])
|
||
else self._is_word_separator)
|
||
new_pos = pos - len(''.join(takewhile(pred, reversed(line[:pos]))))
|
||
return Position(wcswidth(line[:new_pos]),
|
||
self.point.y, self.point.top_line)
|
||
if self.point.y > 0:
|
||
return Position(wcswidth(unstyled(self.lines[self.point.line - 2])),
|
||
self.point.y - 1, self.point.top_line)
|
||
if self.point.top_line > 1:
|
||
return Position(wcswidth(unstyled(self.lines[self.point.line - 2])),
|
||
self.point.y, self.point.top_line - 1)
|
||
return self.point
|
||
|
||
def word_right(self) -> Position:
|
||
line = unstyled(self.lines[self.point.line - 1])
|
||
pos = truncate_point_for_length(line, self.point.x)
|
||
if pos < len(line):
|
||
pred = (self._is_word_char if self._is_word_char(line[pos])
|
||
else self._is_word_separator)
|
||
new_pos = pos + len(''.join(takewhile(pred, line[pos:])))
|
||
return Position(wcswidth(line[:new_pos]),
|
||
self.point.y, self.point.top_line)
|
||
if self.point.y < self.screen_size.rows - 1:
|
||
return Position(0, self.point.y + 1, self.point.top_line)
|
||
if self.point.top_line + self.point.y < len(self.lines):
|
||
return Position(0, self.point.y, self.point.top_line + 1)
|
||
return self.point
|
||
|
||
def _select(self, direction: DirectionStr,
|
||
mark_type: Type[Region]) -> None:
|
||
self._ensure_mark(mark_type)
|
||
old_point = self.point
|
||
self.point = (getattr(self, direction))()
|
||
if self.point.top_line != old_point.top_line:
|
||
self._redraw()
|
||
else:
|
||
self._redraw_lines(self.mark_type.lines_affected(
|
||
self.mark, old_point, self.point))
|
||
|
||
def move(self, direction: DirectionStr) -> None:
|
||
self._select(direction, self.mode_types[self.mode])
|
||
|
||
def select(self, region_type: RegionTypeStr,
|
||
direction: DirectionStr) -> None:
|
||
self._select(direction, self.region_types[region_type])
|
||
|
||
def set_mode(self, mode: ModeTypeStr) -> None:
|
||
self.mode = mode
|
||
self._select('noop', self.mode_types[mode])
|
||
|
||
def confirm(self, *args: Any) -> None:
|
||
start, end = self._start_end()
|
||
self.result = {'copy': '\n'.join(
|
||
line_slice
|
||
for line in range(start.line, end.line + 1)
|
||
for plain in [unstyled(self.lines[line - 1])]
|
||
for start_x, end_x in [self.mark_type.selection_in_line(
|
||
line, start, end, len(plain))]
|
||
if start_x is not None and end_x is not None
|
||
for line_slice, _half in [string_slice(plain, start_x, end_x)])}
|
||
self.quit_loop(0)
|
||
|
||
|
||
def main(args: List[str]) -> Optional['ResultDict']:
|
||
def ospec() -> str:
|
||
return '''
|
||
--cursor-x
|
||
dest=x
|
||
type=int
|
||
(Internal) Starting cursor column, 0-based.
|
||
|
||
|
||
--cursor-y
|
||
dest=y
|
||
type=int
|
||
(Internal) Starting cursor line, 0-based.
|
||
|
||
|
||
--top-line
|
||
dest=top_line
|
||
type=int
|
||
(Internal) Window scroll offset, 1-based.
|
||
|
||
|
||
--title
|
||
(Internal)'''
|
||
|
||
try:
|
||
args, _rest = parse_args(args[1:], ospec)
|
||
tty = open(os.ctermid())
|
||
lines = (sys.stdin.buffer.read().decode('utf-8')
|
||
.split('\n')[:-1]) # last line ends with \n, too
|
||
sys.stdin = tty
|
||
opts = load_config()
|
||
handler = GrabHandler(args, opts, lines)
|
||
loop = Loop()
|
||
loop.loop(handler)
|
||
return handler.result
|
||
except Exception as e:
|
||
from kittens.tui.loop import debug
|
||
from traceback import format_exc
|
||
debug(format_exc())
|
||
raise
|
||
|
||
|
||
WindowId = int
|
||
|
||
|
||
def handle_result(args: List[str], result: 'ResultDict',
|
||
target_window_id: WindowId, boss: Boss) -> None:
|
||
if 'copy' in result:
|
||
set_clipboard_string(result['copy'])
|