macOS: Allow customizing the launch actions
This commit is contained in:
parent
cbb2597667
commit
1454af2d41
22
__main__.py
22
__main__.py
@ -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:
|
||||
|
||||
@ -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,57 +2200,32 @@ 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:
|
||||
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:
|
||||
from .debug_config import debug_config
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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,9 +70,13 @@ def parse(lines: Iterable[str]) -> Iterator[OpenAction]:
|
||||
if match_criteria and raw_actions:
|
||||
entries.append((tuple(match_criteria), tuple(raw_actions)))
|
||||
|
||||
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] = []
|
||||
with to_cmdline_implementation.filter_env_vars('URL', 'FILE_PATH', 'FILE', 'FRAGMENT'):
|
||||
for defn in action_defns:
|
||||
actions.extend(resolve_aliases_and_parse_actions(defn, alias_map, 'open_action'))
|
||||
yield OpenAction(mc, tuple(actions))
|
||||
@ -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())
|
||||
|
||||
@ -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', ())))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user