macOS: Allow customizing the launch actions

This commit is contained in:
Kovid Goyal 2022-01-07 18:43:07 +05:30
parent cbb2597667
commit 1454af2d41
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 135 additions and 55 deletions

View File

@ -64,6 +64,27 @@ def launch(args: List[str]) -> None:
runpy.run_path(exe, run_name='__main__')
def shebang(args: List[str]) -> None:
script_path = args[1]
cmd = args[2:]
if cmd == ['__ext__']:
cmd = [os.path.splitext(script_path)[1][1:].lower()]
try:
f = open(script_path)
except FileNotFoundError:
raise SystemExit(f'The file {script_path} does not exist')
with f:
if f.read(2) == '#!':
line = f.readline().strip()
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat
if is_macos:
cmd = line.split(' ')
else:
cmd = line.split(' ', maxsplit=1)
os.execvp(cmd[0], cmd + [script_path])
def run_kitten(args: List[str]) -> None:
try:
kitten = args[1]
@ -110,6 +131,7 @@ namespaced_entry_points['runpy'] = runpy
namespaced_entry_points['launch'] = launch
namespaced_entry_points['kitten'] = run_kitten
namespaced_entry_points['edit-config'] = edit_config_file
namespaced_entry_points['shebang'] = shebang
def setup_openssl_environment() -> None:

View File

@ -23,7 +23,7 @@ from .conf.utils import BadLine, KeyAction, to_cmdline
from .config import common_opts_as_dict, prepare_config_file_for_editing
from .constants import (
appname, config_dir, is_macos, is_wayland, kitty_exe, logo_png_file,
shell_path, supports_primary_selection, website_url
supports_primary_selection, website_url
)
from .fast_data_types import (
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
@ -2200,56 +2200,31 @@ class Boss:
if path == ":cocoa::application launched::":
self.cocoa_application_launched = True
return
is_executable = is_dir = False
with suppress(OSError):
is_executable = os.access(path, os.X_OK)
with suppress(OSError):
is_dir = os.path.isdir(path)
def parse_macos_shebang(path: str) -> List[str]:
# The macos kernel splits the shebang line on spaces
try:
f = open(path)
except OSError:
return []
with f:
if f.read(2) != '#!':
return []
line = f.readline().strip()
return line.split(' ')
needs_new_os_window = self.cocoa_application_launched or not self.os_window_map or self.active_tab is None
launch_cmd = []
if needs_new_os_window:
launch_cmd += ['--type', 'os-window']
if is_dir:
launch_cmd += ['--cwd', path]
elif is_executable:
launch_cmd += [path]
else:
from .guess_mime_type import guess_type
mt = guess_type(path) or ''
ext = os.path.splitext(path)[1].lower()
if ext in ('.sh', '.command', '.tool'):
launch_cmd += ['--hold'] + (parse_macos_shebang(path) or [shell_path]) + [path]
elif ext in ('.zsh', '.bash', '.fish'):
launch_cmd += ['--hold'] + (parse_macos_shebang(path) or [ext[1:]]) + [path]
elif mt.startswith('text/'):
launch_cmd += get_editor() + [path]
elif mt.startswith('image/'):
launch_cmd += [kitty_exe(), '+kitten', 'icat', '--hold', path]
else:
launch_cmd += [kitty_exe(), '+runpy', f'print("The file:", {path!r}, "is of unknown type, cannot open it.");'
'from kitty.utils import hold_till_enter; hold_till_enter(); raise SystemExit(1)']
from .open_actions import actions_for_launch
from .launch import force_window_launch
actions = list(actions_for_launch(path))
tab = self.active_tab
if tab is not None:
w = tab.active_window
else:
w = None
self.launch(*launch_cmd)
if not needs_new_os_window and tab is not None and w is not None:
tab.remove_window(w)
needs_window_replaced = not self.cocoa_application_launched or not self.os_window_map and w is not None and w.id == 1
def clear_initial_window() -> None:
if needs_window_replaced and tab is not None and w is not None:
tab.remove_window(w)
if not actions:
with force_window_launch(needs_window_replaced):
self.launch(kitty_exe(), '+runpy', f'print("The file:", {path!r}, "is of unknown type, cannot open it.");'
'from kitty.utils import hold_till_enter; hold_till_enter(); raise SystemExit(1)')
clear_initial_window()
else:
with force_window_launch(needs_window_replaced):
self.dispatch_action(actions.pop(0))
clear_initial_window()
if actions:
self.drain_actions(actions)
@ac('debug', 'Show the effective configuration kitty is running with')
def debug_config(self) -> None:

View File

@ -72,9 +72,10 @@ class ToCmdline:
def __exit__(self, *a: Any) -> None:
self.override_env = None
def filter_env_vars(self, *a: str) -> 'ToCmdline':
def filter_env_vars(self, *a: str, **override: str) -> 'ToCmdline':
remove = frozenset(a)
self.override_env = {k: v for k, v in os.environ.items() if k not in remove}
self.override_env.update(override)
return self
def __call__(self, x: str) -> List[str]:

View File

@ -306,6 +306,28 @@ def apply_colors(window: Window, spec: Sequence[str]) -> None:
patch_color_profiles(colors, profiles, True)
class ForceWindowLaunch:
def __init__(self) -> None:
self.force = False
def __bool__(self) -> bool:
return self.force
def __call__(self, force: bool) -> 'ForceWindowLaunch':
self.force = force
return self
def __enter__(self) -> None:
pass
def __exit__(self, *a: object) -> None:
self.force = False
force_window_launch = ForceWindowLaunch()
def launch(
boss: Boss,
opts: LaunchCLIOptions,
@ -389,6 +411,8 @@ def launch(
if exe:
final_cmd[0] = exe
kw['cmd'] = final_cmd
if force_window_launch and opts.type not in ('background', 'clipboard', 'primary'):
opts.type = 'window'
if opts.type == 'overlay' and active:
kw['overlay_for'] = active.id
if opts.type == 'background':

View File

@ -4,11 +4,12 @@
import os
import posixpath
import shlex
from contextlib import suppress
from typing import (
Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, cast
)
from urllib.parse import ParseResult, unquote, urlparse
from urllib.parse import ParseResult, quote, unquote, urlparse
from .conf.utils import KeyAction, to_cmdline_implementation
from .constants import config_dir
@ -16,7 +17,8 @@ from .guess_mime_type import guess_type
from .options.utils import ActionAlias, resolve_aliases_and_parse_actions
from .types import run_once
from .typing import MatchType
from .utils import expandvars, log_error
from .utils import expandvars, get_editor, log_error, resolved_shell
from .fast_data_types import get_options
class MatchCriteria(NamedTuple):
@ -68,12 +70,16 @@ def parse(lines: Iterable[str]) -> Iterator[OpenAction]:
if match_criteria and raw_actions:
entries.append((tuple(match_criteria), tuple(raw_actions)))
for (mc, action_defns) in entries:
actions: List[KeyAction] = []
with to_cmdline_implementation.filter_env_vars('URL', 'FILE_PATH', 'FILE', 'FRAGMENT'):
with to_cmdline_implementation.filter_env_vars(
'URL', 'FILE_PATH', 'FILE', 'FRAGMENT',
EDITOR=shlex.join(get_editor()),
SHELL=shlex.join(resolved_shell(get_options()))
):
for (mc, action_defns) in entries:
actions: List[KeyAction] = []
for defn in action_defns:
actions.extend(resolve_aliases_and_parse_actions(defn, alias_map, 'open_action'))
yield OpenAction(mc, tuple(actions))
yield OpenAction(mc, tuple(actions))
def url_matches_criterion(purl: 'ParseResult', url: str, unquoted_path: str, mc: MatchCriteria) -> bool:
@ -195,9 +201,60 @@ def load_open_actions() -> Tuple[OpenAction, ...]:
return tuple(parse(f))
@run_once
def load_launch_actions() -> Tuple[OpenAction, ...]:
try:
f = open(os.path.join(config_dir, 'launch-actions.conf'))
except FileNotFoundError:
return ()
with f:
return tuple(parse(f))
@run_once
def default_launch_actions() -> Tuple[OpenAction, ...]:
SHELL = resolved_shell(get_options())
return tuple(parse(f'''\
# Open script files
protocol file
ext sh,command,tool
action launch --hold --type=os-window kitty +shebang $FILE_PATH {SHELL}
# Open shell specific script files
protocol file
ext fish,bash,zsh
action launch --hold --type=os-window kitty +shebang $FILE_PATH __ext__
# Open directories
protocol file
mime inode/directory
action launch --type=os-window --cwd $FILE_PATH
# Open text files without fragments in the editor
protocol file
mime text/*
action launch --type=os-window $EDITOR $FILE_PATH
# Open image files with icat
protocol file
mime image/*
action launch --type=os-window kitty +kitten icat --hold $FILE_PATH
'''.splitlines()))
def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Iterator[KeyAction]:
if actions_spec is None:
actions = load_open_actions()
else:
actions = tuple(parse(actions_spec.splitlines()))
yield from actions_for_url_from_list(url, actions)
def actions_for_launch(path: str) -> Iterator[KeyAction]:
url = f'file://{quote(path)}'
found = False
for action in actions_for_url_from_list(url, load_launch_actions()):
found = True
yield action
if not found:
yield from actions_for_url_from_list(url, default_launch_actions())

View File

@ -6,6 +6,7 @@ import os
from contextlib import contextmanager
from . import BaseTest
from kitty.utils import get_editor
@contextmanager
@ -42,7 +43,7 @@ action two
'''
def actions(url):
with patch_env(EDITOR='editor', FILE_PATH='notgood'):
with patch_env(FILE_PATH='notgood'):
return tuple(actions_for_url(url, spec))
def single(url, func, *args):
@ -51,6 +52,6 @@ action two
self.ae(acts[0].func, func)
self.ae(acts[0].args, args)
single('file://hostname/tmp/moo.txt#23', 'launch', 'editor', '/tmp/moo.txt', '23')
single('file://hostname/tmp/moo.txt#23', 'launch', *get_editor(), '/tmp/moo.txt', '23')
single('some thing.txt', 'ignored')
self.ae(actions('x:///a.txt'), (KeyAction('one', ()), KeyAction('two', ())))