More typing work

This commit is contained in:
Kovid Goyal 2020-03-06 14:19:46 +05:30
parent 8a34fede55
commit afec07b124
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
14 changed files with 237 additions and 155 deletions

View File

@ -4,14 +4,18 @@
import os import os
from contextlib import suppress from contextlib import suppress
from typing import TYPE_CHECKING, Dict, List, Union
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.constants import cache_dir
from kitty.cli_stub import AskCLIOptions from kitty.cli_stub import AskCLIOptions
from kitty.constants import cache_dir
from ..tui.operations import alternate_screen, styled
from ..tui.handler import result_handler from ..tui.handler import result_handler
from ..tui.operations import alternate_screen, styled
if TYPE_CHECKING:
import readline
else:
readline = None readline = None
@ -98,7 +102,7 @@ def main(args):
raise SystemExit(e.code) raise SystemExit(e.code)
init_readline(readline) init_readline(readline)
ans = {'items': items} response = None
with alternate_screen(), HistoryCompleter(args.name): with alternate_screen(), HistoryCompleter(args.name):
if args.message: if args.message:
@ -106,7 +110,11 @@ def main(args):
prompt = '> ' prompt = '> '
with suppress(KeyboardInterrupt, EOFError): with suppress(KeyboardInterrupt, EOFError):
ans['response'] = input(prompt) response = input(prompt)
if response is None:
ans: Dict[str, Union[str, List[str]]] = {'items': items}
else:
ans = {'items': items, 'response': response}
return ans return ans

View File

@ -0,0 +1,8 @@
class GlobalData:
def __init__(self):
self.title = ''
self.cmd = ''
global_data = GlobalData

View File

@ -8,7 +8,7 @@ from contextlib import suppress
from functools import lru_cache from functools import lru_cache
from hashlib import md5 from hashlib import md5
from mimetypes import guess_type from mimetypes import guess_type
from typing import Dict from typing import Dict, List, Set
path_name_map: Dict[str, str] = {} path_name_map: Dict[str, str] = {}
@ -20,11 +20,12 @@ class Segment:
def __init__(self, start, start_code): def __init__(self, start, start_code):
self.start = start self.start = start
self.start_code = start_code self.start_code = start_code
self.end = None
self.end_code = None
def __repr__(self): def __repr__(self):
return 'Segment(start={!r}, start_code={!r}, end={!r}, end_code={!r})'.format( return 'Segment(start={!r}, start_code={!r}, end={!r}, end_code={!r})'.format(
self.start, self.start_code, getattr(self, 'end', None), getattr(self, 'end_code', None) self.start, self.start_code, self.end, self.end_code)
)
class Collection: class Collection:
@ -81,8 +82,10 @@ class Collection:
def collect_files(collection, left, right): def collect_files(collection, left, right):
left_names, right_names = set(), set() left_names: Set[str] = set()
left_path_map, right_path_map = {}, {} right_names: Set[str] = set()
left_path_map: Dict[str, str] = {}
right_path_map: Dict[str, str] = {}
def walk(base, names, pmap): def walk(base, names, pmap):
for dirpath, dirnames, filenames in os.walk(base): for dirpath, dirnames, filenames in os.walk(base):
@ -178,7 +181,7 @@ def create_collection(left, right):
return collection return collection
highlight_data = {} highlight_data: Dict[str, List] = {}
def set_highlight_data(data): def set_highlight_data(data):

View File

@ -3,18 +3,20 @@
# 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 os
from typing import Any, Dict, Optional, Type
from kitty.conf.utils import (
init_config as _init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, parse_kittens_key, resolve_config
)
from kitty.conf.definition import config_lines from kitty.conf.definition import config_lines
from kitty.conf.utils import (
init_config as _init_config, key_func, load_config as _load_config,
merge_dicts, parse_config_base, parse_kittens_key, resolve_config
)
from kitty.constants import config_dir from kitty.constants import config_dir
from kitty.options_stub import DiffOptions
from kitty.rgb import color_as_sgr from kitty.rgb import color_as_sgr
from .config_data import type_convert, all_options from .config_data import all_options, type_convert
defaults = None defaults: Optional[DiffOptions] = None
formats = { formats = {
'title': '', 'title': '',
@ -79,13 +81,15 @@ def parse_start_search(func, rest):
def special_handling(key, val, ans): def special_handling(key, val, ans):
if key == 'map': if key == 'map':
action, *key_def = parse_kittens_key(val, args_funcs) x = parse_kittens_key(val, args_funcs)
if x is not None:
action, *key_def = x
ans['key_definitions'][tuple(key_def)] = action ans['key_definitions'][tuple(key_def)] = action
return True return True
def parse_config(lines, check_keys=True): def parse_config(lines, check_keys=True):
ans = {'key_definitions': {}} ans: Dict[str, Any] = {'key_definitions': {}}
parse_config_base( parse_config_base(
lines, lines,
defaults, defaults,
@ -112,7 +116,9 @@ def parse_defaults(lines, check_keys=False):
return parse_config(lines, check_keys) return parse_config(lines, check_keys)
Options, defaults = _init_config(config_lines(all_options), parse_defaults) x = _init_config(config_lines(all_options), parse_defaults)
Options: Type[DiffOptions] = x[0]
defaults = x[1]
def load_config(*paths, overrides=None): def load_config(*paths, overrides=None):

View File

@ -5,6 +5,7 @@
import concurrent import concurrent
import os import os
import re import re
from typing import List, Optional
from pygments import highlight # type: ignore from pygments import highlight # type: ignore
from pygments.formatter import Formatter # type: ignore from pygments.formatter import Formatter # type: ignore
@ -35,10 +36,14 @@ class DiffFormatter(Formatter):
for token, style in self.style: for token, style in self.style:
start = [] start = []
end = [] end = []
fstart = fend = ''
# a style item is a tuple in the following form: # a style item is a tuple in the following form:
# colors are readily specified in hex: 'RRGGBB' # colors are readily specified in hex: 'RRGGBB'
if style['color']: col = style['color']
start.append('38' + color_as_sgr(parse_sharp(style['color']))) if col:
pc = parse_sharp(col)
if pc is not None:
start.append('38' + color_as_sgr(pc))
end.append('39') end.append('39')
if style['bold']: if style['bold']:
start.append('1') start.append('1')
@ -50,9 +55,9 @@ class DiffFormatter(Formatter):
start.append('4') start.append('4')
end.append('24') end.append('24')
if start: if start:
start = '\033[{}m'.format(';'.join(start)) fstart = '\033[{}m'.format(';'.join(start))
end = '\033[{}m'.format(';'.join(end)) fend = '\033[{}m'.format(';'.join(end))
self.styles[token] = start or '', end or '' self.styles[token] = fstart, fend
def format(self, tokensource, outfile): def format(self, tokensource, outfile):
for ttype, value in tokensource: for ttype, value in tokensource:
@ -76,7 +81,7 @@ class DiffFormatter(Formatter):
outfile.write(value) outfile.write(value)
formatter = None formatter: Optional[DiffFormatter] = None
def initialize_highlighter(style='default'): def initialize_highlighter(style='default'):
@ -102,8 +107,8 @@ split_pat = re.compile(r'(\033\[.*?m)')
def highlight_line(line): def highlight_line(line):
ans = [] ans: List[Segment] = []
current = None current: Optional[Segment] = None
pos = 0 pos = 0
for x in split_pat.split(line): for x in split_pat.split(line):
if x.startswith('\033'): if x.startswith('\033'):
@ -133,7 +138,6 @@ def highlight_collection(collection, aliases=None):
jobs = {} jobs = {}
ans = {} ans = {}
with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor: with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
highlight_collection.processes = executor._processes
for path, item_type, other_path in collection: for path, item_type, other_path in collection:
if item_type != 'rename': if item_type != 'rename':
for p in (path, other_path): for p in (path, other_path):
@ -156,6 +160,7 @@ def main():
# kitty +runpy "from kittens.diff.highlight import main; main()" file # kitty +runpy "from kittens.diff.highlight import main; main()" file
import sys import sys
initialize_highlighter() initialize_highlighter()
if defaults is not None:
with open(sys.argv[-1]) as f: with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases) highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
if highlighted is None: if highlighted is None:

View File

@ -13,6 +13,7 @@ from collections import defaultdict
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from typing import DefaultDict, List, Tuple
from kitty.cli import CONFIG_HELP, parse_args from kitty.cli import CONFIG_HELP, parse_args
from kitty.cli_stub import DiffCLIOptions from kitty.cli_stub import DiffCLIOptions
@ -24,9 +25,10 @@ from .collect import (
create_collection, data_for_path, lines_for_path, sanitize, create_collection, data_for_path, lines_for_path, sanitize,
set_highlight_data set_highlight_data
) )
from . import global_data
from .config import init_config from .config import init_config
from .patch import Differ, set_diff_command, worker_processes from .patch import Differ, set_diff_command, worker_processes
from .render import ImageSupportWarning, LineRef, render_diff from .render import ImageSupportWarning, LineRef, Reference, render_diff
from .search import BadRegex, Search from .search import BadRegex, Search
from ..tui.handler import Handler from ..tui.handler import Handler
from ..tui.images import ImageManager from ..tui.images import ImageManager
@ -186,7 +188,7 @@ class DiffHandler(Handler):
def render_diff(self): def render_diff(self):
self.diff_lines = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager)) self.diff_lines = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
self.margin_size = render_diff.margin_size self.margin_size = render_diff.margin_size
self.ref_path_map = defaultdict(list) self.ref_path_map: DefaultDict[str, List[Tuple[int, Reference]]] = defaultdict(list)
for i, l in enumerate(self.diff_lines): for i, l in enumerate(self.diff_lines):
self.ref_path_map[l.ref.path].append((i, l.ref)) self.ref_path_map[l.ref.path].append((i, l.ref))
self.max_scroll_pos = len(self.diff_lines) - self.num_lines self.max_scroll_pos = len(self.diff_lines) - self.num_lines
@ -271,7 +273,7 @@ class DiffHandler(Handler):
def init_terminal_state(self): def init_terminal_state(self):
self.cmd.set_line_wrapping(False) self.cmd.set_line_wrapping(False)
self.cmd.set_window_title(main.title) self.cmd.set_window_title(global_data.title)
self.cmd.set_default_colors( self.cmd.set_default_colors(
fg=self.opts.foreground, bg=self.opts.background, fg=self.opts.foreground, bg=self.opts.background,
cursor=self.opts.foreground, select_fg=self.opts.select_fg, cursor=self.opts.foreground, select_fg=self.opts.select_fg,
@ -547,7 +549,7 @@ def main(args):
if len(items) != 2: if len(items) != 2:
raise SystemExit('You must specify exactly two files/directories to compare') raise SystemExit('You must specify exactly two files/directories to compare')
left, right = items left, right = items
main.title = _('{} vs. {}').format(left, right) global_data.title = _('{} vs. {}').format(left, right)
if os.path.isdir(left) != os.path.isdir(right): if os.path.isdir(left) != os.path.isdir(right):
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.') raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
opts = init_config(args) opts = init_config(args)

View File

@ -7,14 +7,17 @@ import os
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
from typing import List, Optional, Tuple
from . import global_data
from .collect import lines_for_path from .collect import lines_for_path
from .diff_speedup import changed_center from .diff_speedup import changed_center
left_lines = right_lines = None left_lines: Tuple[str, ...] = ()
right_lines: Tuple[str, ...] = ()
GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --' GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --'
DIFF_DIFF = 'diff -p -U _CONTEXT_ --' DIFF_DIFF = 'diff -p -U _CONTEXT_ --'
worker_processes = [] worker_processes: List[int] = []
def find_differ(): def find_differ():
@ -31,16 +34,17 @@ def set_diff_command(opt):
raise SystemExit('Failed to find either the git or diff programs on your system') raise SystemExit('Failed to find either the git or diff programs on your system')
else: else:
cmd = opt cmd = opt
set_diff_command.cmd = cmd global_data.cmd = cmd
def run_diff(file1, file2, context=3): def run_diff(file1: str, file2: str, context: int = 3):
# returns: ok, is_different, patch # returns: ok, is_different, patch
cmd = shlex.split(set_diff_command.cmd.replace('_CONTEXT_', str(context))) cmd = shlex.split(global_data.cmd.replace('_CONTEXT_', str(context)))
# we resolve symlinks because git diff does not follow symlinks, while diff # we resolve symlinks because git diff does not follow symlinks, while diff
# does. We want consistent behavior, also for integration with git difftool # does. We want consistent behavior, also for integration with git difftool
# we always want symlinks to be followed. # we always want symlinks to be followed.
path1, path2 = map(os.path.realpath, (file1, file2)) path1 = os.path.realpath(file1)
path2 = os.path.realpath(file2)
p = subprocess.Popen( p = subprocess.Popen(
cmd + [path1, path2], cmd + [path1, path2],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL) stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
@ -96,7 +100,7 @@ class Hunk:
self.title = title self.title = title
self.added_count = self.removed_count = 0 self.added_count = self.removed_count = 0
self.chunks = [] self.chunks = []
self.current_chunk = None self.current_chunk: Optional[Chunk] = None
self.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count) self.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count)
def new_chunk(self, is_context=False): def new_chunk(self, is_context=False):
@ -125,19 +129,23 @@ class Hunk:
def add_line(self): def add_line(self):
self.ensure_diff_chunk() self.ensure_diff_chunk()
if self.current_chunk is not None:
self.current_chunk.add_line() self.current_chunk.add_line()
self.added_count += 1 self.added_count += 1
def remove_line(self): def remove_line(self):
self.ensure_diff_chunk() self.ensure_diff_chunk()
if self.current_chunk is not None:
self.current_chunk.remove_line() self.current_chunk.remove_line()
self.removed_count += 1 self.removed_count += 1
def context_line(self): def context_line(self):
self.ensure_context_chunk() self.ensure_context_chunk()
if self.current_chunk is not None:
self.current_chunk.context_line() self.current_chunk.context_line()
def finalize(self): def finalize(self):
if self.current_chunk is not None:
self.chunks.append(self.current_chunk) self.chunks.append(self.current_chunk)
del self.current_chunk del self.current_chunk
# Sanity check # Sanity check
@ -158,7 +166,7 @@ def parse_range(x):
def parse_hunk_header(line): def parse_hunk_header(line):
parts = tuple(filter(None, line.split('@@', 2))) parts: Tuple[str, ...] = tuple(filter(None, line.split('@@', 2)))
linespec = parts[0].strip() linespec = parts[0].strip()
title = '' title = ''
if len(parts) == 2: if len(parts) == 2:
@ -208,7 +216,7 @@ def parse_patch(raw):
class Differ: class Differ:
diff_executor = None diff_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
def __init__(self): def __init__(self):
self.jmap = {} self.jmap = {}
@ -223,7 +231,8 @@ class Differ:
def __call__(self, context=3): def __call__(self, context=3):
global left_lines, right_lines global left_lines, right_lines
ans = {} ans = {}
executor = Differ.diff_executor executor = self.diff_executor
assert executor is not None
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs} jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
for future in concurrent.futures.as_completed(jobs): for future in concurrent.futures.as_completed(jobs):
key = jobs[future] key = jobs[future]

View File

@ -3,29 +3,30 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import warnings import warnings
from functools import lru_cache
from gettext import gettext as _ from gettext import gettext as _
from itertools import repeat, zip_longest from itertools import repeat, zip_longest
from math import ceil from math import ceil
from typing import Callable, Iterable, List, Optional
from kitty.fast_data_types import truncate_point_for_length, wcswidth from kitty.fast_data_types import truncate_point_for_length, wcswidth
from ..tui.images import can_display_images
from .collect import ( from .collect import (
Segment, data_for_path, highlights_for_path, is_image, lines_for_path, Segment, data_for_path, highlights_for_path, is_image, lines_for_path,
path_name_map, sanitize path_name_map, sanitize
) )
from .config import formats from .config import formats
from .diff_speedup import split_with_highlights as _split_with_highlights from .diff_speedup import split_with_highlights as _split_with_highlights
from ..tui.images import can_display_images
class ImageSupportWarning(Warning): class ImageSupportWarning(Warning):
pass pass
@lru_cache(maxsize=2)
def images_supported(): def images_supported():
ans = getattr(images_supported, 'ans', None) ans = can_display_images()
if ans is None:
images_supported.ans = ans = can_display_images()
if not ans: if not ans:
warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning) warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning)
return ans return ans
@ -44,6 +45,8 @@ class Ref:
class LineRef(Ref): class LineRef(Ref):
__slots__ = ('src_line_number', 'wrapped_line_idx') __slots__ = ('src_line_number', 'wrapped_line_idx')
src_line_number: int
wrapped_line_idx: int
def __init__(self, sln, wli=0): def __init__(self, sln, wli=0):
object.__setattr__(self, 'src_line_number', sln) object.__setattr__(self, 'src_line_number', sln)
@ -53,6 +56,8 @@ class LineRef(Ref):
class Reference(Ref): class Reference(Ref):
__slots__ = ('path', 'extra') __slots__ = ('path', 'extra')
path: str
extra: Optional[LineRef]
def __init__(self, path, extra=None): def __init__(self, path, extra=None):
object.__setattr__(self, 'path', path) object.__setattr__(self, 'path', path)
@ -111,8 +116,8 @@ def place_in(text, sz):
return fill_in(fit_in(text, sz), sz) return fill_in(fit_in(text, sz), sz)
def format_func(which): def format_func(which: str) -> Callable[[str], str]:
def formatted(text): def formatted(text: str) -> str:
fmt = formats[which] fmt = formats[which]
return '\x1b[' + fmt + 'm' + text + '\x1b[0m' return '\x1b[' + fmt + 'm' + text + '\x1b[0m'
formatted.__name__ = which + '_format' formatted.__name__ = which + '_format'
@ -226,7 +231,7 @@ class DiffData:
return [] return []
def render_diff_line(number, text, ltype, margin_size, available_cols): def render_diff_line(number, text, ltype, margin_size, available_cols) -> str:
margin = margin_bg_map[ltype](place_in(number, margin_size)) margin = margin_bg_map[ltype](place_in(number, margin_size))
content = text_bg_map[ltype](fill_in(text or '', available_cols)) content = text_bg_map[ltype](fill_in(text or '', available_cols))
return margin + content return margin + content
@ -292,7 +297,8 @@ def lines_for_chunk(data, hunk_num, chunk, chunk_num):
else: else:
common = min(chunk.left_count, chunk.right_count) common = min(chunk.left_count, chunk.right_count)
for i in range(max(chunk.left_count, chunk.right_count)): for i in range(max(chunk.left_count, chunk.right_count)):
ll, rl = [], [] ll: List[str] = []
rl: List[str] = []
if i < chunk.left_count: if i < chunk.left_count:
rln = ref_ln = chunk.left_start + i rln = ref_ln = chunk.left_start + i
ll.extend(render_half_line( ll.extend(render_half_line(
@ -416,7 +422,8 @@ def render_image(path, is_left, available_cols, margin_size, image_manager):
def image_lines(left_path, right_path, columns, margin_size, image_manager): def image_lines(left_path, right_path, columns, margin_size, image_manager):
available_cols = columns // 2 - margin_size available_cols = columns // 2 - margin_size
left_lines, right_lines = iter(()), iter(()) left_lines: Iterable[str] = iter(())
right_lines: Iterable[str] = iter(())
if left_path is not None: if left_path is not None:
left_lines = render_image(left_path, True, available_cols, margin_size, image_manager) left_lines = render_image(left_path, True, available_cols, margin_size, image_manager)
if right_path is not None: if right_path is not None:
@ -439,7 +446,11 @@ def image_lines(left_path, right_path, columns, margin_size, image_manager):
is_change_start = False is_change_start = False
def render_diff(collection, diff_map, args, columns, image_manager): class RenderDiff:
margin_size: int = 0
def __call__(self, collection, diff_map, args, columns, image_manager):
largest_line_number = 0 largest_line_number = 0
for path, item_type, other_path in collection: for path, item_type, other_path in collection:
if item_type == 'diff': if item_type == 'diff':
@ -447,7 +458,7 @@ def render_diff(collection, diff_map, args, columns, image_manager):
if patch is not None: if patch is not None:
largest_line_number = max(largest_line_number, patch.largest_line_number) largest_line_number = max(largest_line_number, patch.largest_line_number)
margin_size = render_diff.margin_size = max(3, len(str(largest_line_number)) + 1) margin_size = self.margin_size = max(3, len(str(largest_line_number)) + 1)
last_item_num = len(collection) - 1 last_item_num = len(collection) - 1
for i, (path, item_type, other_path) in enumerate(collection): for i, (path, item_type, other_path) in enumerate(collection):
@ -488,3 +499,6 @@ def render_diff(collection, diff_map, args, columns, image_manager):
yield from ans yield from ans
if i < last_item_num: if i < last_item_num:
yield Line('', item_ref) yield Line('', item_ref)
render_diff = RenderDiff()

View File

@ -9,11 +9,12 @@ import sys
from functools import lru_cache from functools import lru_cache
from gettext import gettext as _ from gettext import gettext as _
from itertools import repeat from itertools import repeat
from typing import Callable, Dict, List, Optional, Tuple
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import HintsCLIOptions from kitty.cli_stub import HintsCLIOptions
from kitty.fast_data_types import set_clipboard_string from kitty.fast_data_types import set_clipboard_string
from kitty.key_encoding import key_defs as K, backspace_key, enter_key from kitty.key_encoding import backspace_key, enter_key, key_defs as K
from kitty.utils import screen_size_function from kitty.utils import screen_size_function
from ..tui.handler import Handler, result_handler from ..tui.handler import Handler, result_handler
@ -199,7 +200,7 @@ def regex_finditer(pat, minimum_match_length, text):
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"} closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
opening_brackets = ''.join(closing_bracket_map) opening_brackets = ''.join(closing_bracket_map)
postprocessor_map = {} postprocessor_map: Dict[str, Callable[[str, int, int], Tuple[int, int]]] = {}
def postprocessor(func): def postprocessor(func):
@ -304,8 +305,8 @@ def functions_for(args):
return pattern, post_processors return pattern, post_processors
def convert_text(text, cols): def convert_text(text: str, cols: int) -> str:
lines = [] lines: List[str] = []
empty_line = '\0' * cols empty_line = '\0' * cols
for full_line in text.split('\n'): for full_line in text.split('\n'):
if full_line: if full_line:
@ -574,10 +575,11 @@ def handle_result(args, data, target_window_id, boss):
matches, groupdicts = [], [] matches, groupdicts = [], []
for m, g in zip(data['match'], data['groupdicts']): for m, g in zip(data['match'], data['groupdicts']):
if m: if m:
matches.append(m), groupdicts.append(g) matches.append(m)
groupdicts.append(g)
joiner = data['multiple_joiner'] joiner = data['multiple_joiner']
try: try:
is_int = int(joiner) is_int: Optional[int] = int(joiner)
except Exception: except Exception:
is_int = None is_int = None
text_type = data['type'] text_type = data['type']

View File

@ -9,14 +9,17 @@ import signal
import sys import sys
import zlib import zlib
from base64 import standard_b64encode from base64 import standard_b64encode
from collections import namedtuple from functools import lru_cache
from math import ceil from math import ceil
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Dict, List, NamedTuple, Optional, Union
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import IcatCLIOptions from kitty.cli_stub import IcatCLIOptions
from kitty.constants import appname from kitty.constants import appname
from kitty.utils import TTYIO, fit_image, screen_size_function from kitty.utils import (
TTYIO, ScreenSize, ScreenSizeGetter, fit_image, screen_size_function
)
from ..tui.images import ( from ..tui.images import (
ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify
@ -104,27 +107,25 @@ colors. For example, --1 evaluates as -1,073,741,825.
''' '''
screen_size = None screen_size: Optional[ScreenSizeGetter] = None
can_transfer_with_files = False
def get_screen_size_function(): def get_screen_size_function() -> ScreenSizeGetter:
global screen_size global screen_size
if screen_size is None: if screen_size is None:
screen_size = screen_size_function() screen_size = screen_size_function()
return screen_size return screen_size
def get_screen_size(): def get_screen_size() -> ScreenSize:
screen_size = get_screen_size_function() screen_size = get_screen_size_function()
return screen_size() return screen_size()
def options_spec(): @lru_cache(maxsize=2)
if not hasattr(options_spec, 'ans'): def options_spec() -> str:
options_spec.ans = OPTIONS.format( return OPTIONS.format(appname='{}-icat'.format(appname))
appname='{}-icat'.format(appname),
)
return options_spec.ans
def write_gr_cmd(cmd, payload=None): def write_gr_cmd(cmd, payload=None):
@ -196,7 +197,7 @@ def show(outfile, width, height, zindex, fmt, transmit_mode='t', align='center',
set_cursor_for_place(place, cmd, width, height, align) set_cursor_for_place(place, cmd, width, height, align)
else: else:
set_cursor(cmd, width, height, align) set_cursor(cmd, width, height, align)
if detect_support.has_files: if can_transfer_with_files:
cmd['t'] = transmit_mode cmd['t'] = transmit_mode
write_gr_cmd(cmd, standard_b64encode(os.path.abspath(outfile).encode(fsenc))) write_gr_cmd(cmd, standard_b64encode(os.path.abspath(outfile).encode(fsenc)))
else: else:
@ -250,20 +251,21 @@ def scan(d):
def detect_support(wait_for=10, silent=False): def detect_support(wait_for=10, silent=False):
global can_transfer_with_files
if not silent: if not silent:
print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r') print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r')
sys.stdout.flush() sys.stdout.flush()
try: try:
received = b'' received = b''
responses = {} responses: Dict[int, bool] = {}
def parse_responses(): def parse_responses():
for m in re.finditer(b'\033_Gi=([1|2]);(.+?)\033\\\\', received): for m in re.finditer(b'\033_Gi=([1|2]);(.+?)\033\\\\', received):
iid = m.group(1) iid = m.group(1)
if iid in (b'1', b'2'): if iid in (b'1', b'2'):
iid = int(iid.decode('ascii')) iid_ = int(iid.decode('ascii'))
if iid not in responses: if iid_ not in responses:
responses[iid] = m.group(2) == b'OK' responses[iid_] = m.group(2) == b'OK'
def more_needed(data): def more_needed(data):
nonlocal received nonlocal received
@ -280,16 +282,24 @@ def detect_support(wait_for=10, silent=False):
finally: finally:
if not silent: if not silent:
sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush() sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush()
detect_support.has_files = bool(responses.get(2)) can_transfer_with_files = bool(responses.get(2))
return responses.get(1, False) return responses.get(1, False)
def parse_place(raw): class Place(NamedTuple):
width: int
height: int
left: int
top: int
def parse_place(raw: str) -> Optional[Place]:
if raw: if raw:
area, pos = raw.split('@', 1) area, pos = raw.split('@', 1)
w, h = map(int, area.split('x')) w, h = map(int, area.split('x'))
l, t = map(int, pos.split('x')) l, t = map(int, pos.split('x'))
return namedtuple('Place', 'width height left top')(w, h, l, t) return Place(w, h, l, t)
return None
help_text = ( help_text = (
@ -344,10 +354,12 @@ def process_single_item(item, args, url_pat=None, maybe_dir=True):
def main(args=sys.argv): def main(args=sys.argv):
args, items = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions) global can_transfer_with_files
args, items_ = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions)
items: List[Union[str, bytes]] = list(items)
if args.print_window_size: if args.print_window_size:
screen_size_function.ans = None screen_size_function.cache_clear()
with open(os.ctermid()) as tty: with open(os.ctermid()) as tty:
ss = screen_size_function(tty)() ss = screen_size_function(tty)()
print('{}x{}'.format(ss.width, ss.height), end='') print('{}x{}'.format(ss.width, ss.height), end='')
@ -384,16 +396,16 @@ def main(args=sys.argv):
if args.detect_support: if args.detect_support:
if not detect_support(wait_for=args.detection_timeout, silent=True): if not detect_support(wait_for=args.detection_timeout, silent=True):
raise SystemExit(1) raise SystemExit(1)
print('file' if detect_support.has_files else 'stream', end='', file=sys.stderr) print('file' if can_transfer_with_files else 'stream', end='', file=sys.stderr)
return return
if args.transfer_mode == 'detect': if args.transfer_mode == 'detect':
if not detect_support(wait_for=args.detection_timeout, silent=args.silent): if not detect_support(wait_for=args.detection_timeout, silent=args.silent):
raise SystemExit('This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it') raise SystemExit('This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it')
else: else:
detect_support.has_files = args.transfer_mode == 'file' can_transfer_with_files = args.transfer_mode == 'file'
errors = [] errors = []
if args.clear: if args.clear:
sys.stdout.buffer.write(clear_images_on_screen(delete_data=True)) sys.stdout.write(clear_images_on_screen(delete_data=True))
if not items: if not items:
return return
if not items: if not items:

View File

@ -28,15 +28,15 @@ class KeysHandler(Handler):
REPEAT: 'REPEAT', REPEAT: 'REPEAT',
RELEASE: 'RELEASE' RELEASE: 'RELEASE'
}[key_event.type] }[key_event.type]
mods = [] lmods = []
for m, name in { for m, name in {
SHIFT: 'Shift', SHIFT: 'Shift',
ALT: 'Alt', ALT: 'Alt',
CTRL: 'Ctrl', CTRL: 'Ctrl',
SUPER: 'Super'}.items(): SUPER: 'Super'}.items():
if key_event.mods & m: if key_event.mods & m:
mods.append(name) lmods.append(name)
mods = '+'.join(mods) mods = '+'.join(lmods)
if mods: if mods:
mods += '+' mods += '+'
self.print('Key {}: {}{} [{}]'.format(etype, mods, key_event.key, encode_key_event(key_event))) self.print('Key {}: {}{} [{}]'.format(etype, mods, key_event.key, encode_key_event(key_event)))

View File

@ -207,7 +207,7 @@ def render_block(text):
def as_conf_file(all_options): def as_conf_file(all_options):
ans = ['# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', ''] ans = ['# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', '']
a = ans.append a = ans.append
current_group = None current_group: Optional[Group] = None
num_open_folds = 0 num_open_folds = 0
all_options = list(all_options) all_options = list(all_options)
@ -311,9 +311,10 @@ def as_type_stub(
all_options: Dict[str, Union[Option, List[Shortcut]]], all_options: Dict[str, Union[Option, List[Shortcut]]],
special_types: Optional[Dict[str, str]] = None, special_types: Optional[Dict[str, str]] = None,
preamble_lines: Union[Tuple[str, ...], List[str], Iterator[str]] = (), preamble_lines: Union[Tuple[str, ...], List[str], Iterator[str]] = (),
extra_fields: Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]], Iterator[Tuple[str, str]]] = () extra_fields: Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]], Iterator[Tuple[str, str]]] = (),
class_name: str = 'Options'
) -> str: ) -> str:
ans = ['import typing\n'] + list(preamble_lines) + ['', 'class Options:'] ans = ['import typing\n'] + list(preamble_lines) + ['', 'class {}:'.format(class_name)]
imports: Set[Tuple[str, str]] = set() imports: Set[Tuple[str, str]] = set()
overrides = special_types or {} overrides = special_types or {}
for name, val in all_options.items(): for name, val in all_options.items():
@ -328,14 +329,14 @@ def as_type_stub(
ans.append(' {}: {}'.format(field_name, type_def)) ans.append(' {}: {}'.format(field_name, type_def))
ans.append(' def __iter__(self): pass') ans.append(' def __iter__(self): pass')
ans.append(' def __len__(self): pass') ans.append(' def __len__(self): pass')
return '\n'.join(ans) return '\n'.join(ans) + '\n\n\n'
def save_type_stub(text: str, fpath: str) -> None: def save_type_stub(text: str, fpath: str) -> None:
import os import os
with open(fpath + 'i', 'w') as f: fpath += 'i'
print( preamble = '# Update this file by running: python {}\n\n'.format(os.path.relpath(os.path.abspath(fpath)))
'# Update this file by running: python {}'.format(os.path.relpath(os.path.abspath(fpath))), existing = open(fpath).read()
file=f current = preamble + text
) if existing != current:
f.write(text) open(fpath, 'w').write(current)

View File

@ -7,6 +7,9 @@ class Options:
pass pass
DiffOptions = Options
def generate_stub(): def generate_stub():
from .config_data import all_options from .config_data import all_options
from .conf.definition import as_type_stub, save_type_stub from .conf.definition import as_type_stub, save_type_stub
@ -26,6 +29,10 @@ def generate_stub():
('sequence_map', 'SequenceMap'), ('sequence_map', 'SequenceMap'),
) )
) )
from kittens.diff.config_data import all_options
text += as_type_stub(all_options, class_name='DiffOptions')
save_type_stub(text, __file__) save_type_stub(text, __file__)

View File

@ -10,11 +10,10 @@ import os
import re import re
import string import string
import sys import sys
from collections import namedtuple
from contextlib import suppress from contextlib import suppress
from functools import lru_cache from functools import lru_cache
from time import monotonic from time import monotonic
from typing import Any, Dict, List, Optional, cast from typing import Any, Dict, List, NamedTuple, Optional, cast
from .constants import ( from .constants import (
appname, is_macos, is_wayland, shell_path, supports_primary_selection appname, is_macos, is_wayland, shell_path, supports_primary_selection
@ -81,7 +80,13 @@ def parse_color_set(raw):
continue continue
ScreenSize = namedtuple('ScreenSize', 'rows cols width height cell_width cell_height') class ScreenSize(NamedTuple):
rows: int
cols: int
width: int
height: int
cell_width: int
cell_height: int
class ScreenSizeGetter: class ScreenSizeGetter: