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__')
|
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:
|
def run_kitten(args: List[str]) -> None:
|
||||||
try:
|
try:
|
||||||
kitten = args[1]
|
kitten = args[1]
|
||||||
@ -110,6 +131,7 @@ namespaced_entry_points['runpy'] = runpy
|
|||||||
namespaced_entry_points['launch'] = launch
|
namespaced_entry_points['launch'] = launch
|
||||||
namespaced_entry_points['kitten'] = run_kitten
|
namespaced_entry_points['kitten'] = run_kitten
|
||||||
namespaced_entry_points['edit-config'] = edit_config_file
|
namespaced_entry_points['edit-config'] = edit_config_file
|
||||||
|
namespaced_entry_points['shebang'] = shebang
|
||||||
|
|
||||||
|
|
||||||
def setup_openssl_environment() -> None:
|
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 .config import common_opts_as_dict, prepare_config_file_for_editing
|
||||||
from .constants import (
|
from .constants import (
|
||||||
appname, config_dir, is_macos, is_wayland, kitty_exe, logo_png_file,
|
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 (
|
from .fast_data_types import (
|
||||||
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
|
CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT,
|
||||||
@ -2200,56 +2200,31 @@ class Boss:
|
|||||||
if path == ":cocoa::application launched::":
|
if path == ":cocoa::application launched::":
|
||||||
self.cocoa_application_launched = True
|
self.cocoa_application_launched = True
|
||||||
return
|
return
|
||||||
is_executable = is_dir = False
|
from .open_actions import actions_for_launch
|
||||||
with suppress(OSError):
|
from .launch import force_window_launch
|
||||||
is_executable = os.access(path, os.X_OK)
|
actions = list(actions_for_launch(path))
|
||||||
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)']
|
|
||||||
|
|
||||||
tab = self.active_tab
|
tab = self.active_tab
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
w = tab.active_window
|
w = tab.active_window
|
||||||
else:
|
else:
|
||||||
w = None
|
w = None
|
||||||
self.launch(*launch_cmd)
|
needs_window_replaced = not self.cocoa_application_launched or not self.os_window_map and w is not None and w.id == 1
|
||||||
if not needs_new_os_window and tab is not None and w is not None:
|
|
||||||
tab.remove_window(w)
|
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')
|
@ac('debug', 'Show the effective configuration kitty is running with')
|
||||||
def debug_config(self) -> None:
|
def debug_config(self) -> None:
|
||||||
|
|||||||
@ -72,9 +72,10 @@ class ToCmdline:
|
|||||||
def __exit__(self, *a: Any) -> None:
|
def __exit__(self, *a: Any) -> None:
|
||||||
self.override_env = 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)
|
remove = frozenset(a)
|
||||||
self.override_env = {k: v for k, v in os.environ.items() if k not in remove}
|
self.override_env = {k: v for k, v in os.environ.items() if k not in remove}
|
||||||
|
self.override_env.update(override)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __call__(self, x: str) -> List[str]:
|
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)
|
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(
|
def launch(
|
||||||
boss: Boss,
|
boss: Boss,
|
||||||
opts: LaunchCLIOptions,
|
opts: LaunchCLIOptions,
|
||||||
@ -389,6 +411,8 @@ def launch(
|
|||||||
if exe:
|
if exe:
|
||||||
final_cmd[0] = exe
|
final_cmd[0] = exe
|
||||||
kw['cmd'] = final_cmd
|
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:
|
if opts.type == 'overlay' and active:
|
||||||
kw['overlay_for'] = active.id
|
kw['overlay_for'] = active.id
|
||||||
if opts.type == 'background':
|
if opts.type == 'background':
|
||||||
|
|||||||
@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
|
import shlex
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, cast
|
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 .conf.utils import KeyAction, to_cmdline_implementation
|
||||||
from .constants import config_dir
|
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 .options.utils import ActionAlias, resolve_aliases_and_parse_actions
|
||||||
from .types import run_once
|
from .types import run_once
|
||||||
from .typing import MatchType
|
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):
|
class MatchCriteria(NamedTuple):
|
||||||
@ -68,12 +70,16 @@ def parse(lines: Iterable[str]) -> Iterator[OpenAction]:
|
|||||||
if match_criteria and raw_actions:
|
if match_criteria and raw_actions:
|
||||||
entries.append((tuple(match_criteria), tuple(raw_actions)))
|
entries.append((tuple(match_criteria), tuple(raw_actions)))
|
||||||
|
|
||||||
for (mc, action_defns) in entries:
|
with to_cmdline_implementation.filter_env_vars(
|
||||||
actions: List[KeyAction] = []
|
'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:
|
for defn in action_defns:
|
||||||
actions.extend(resolve_aliases_and_parse_actions(defn, alias_map, 'open_action'))
|
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:
|
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))
|
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]:
|
def actions_for_url(url: str, actions_spec: Optional[str] = None) -> Iterator[KeyAction]:
|
||||||
if actions_spec is None:
|
if actions_spec is None:
|
||||||
actions = load_open_actions()
|
actions = load_open_actions()
|
||||||
else:
|
else:
|
||||||
actions = tuple(parse(actions_spec.splitlines()))
|
actions = tuple(parse(actions_spec.splitlines()))
|
||||||
yield from actions_for_url_from_list(url, actions)
|
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 contextlib import contextmanager
|
||||||
|
|
||||||
from . import BaseTest
|
from . import BaseTest
|
||||||
|
from kitty.utils import get_editor
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@ -42,7 +43,7 @@ action two
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def actions(url):
|
def actions(url):
|
||||||
with patch_env(EDITOR='editor', FILE_PATH='notgood'):
|
with patch_env(FILE_PATH='notgood'):
|
||||||
return tuple(actions_for_url(url, spec))
|
return tuple(actions_for_url(url, spec))
|
||||||
|
|
||||||
def single(url, func, *args):
|
def single(url, func, *args):
|
||||||
@ -51,6 +52,6 @@ action two
|
|||||||
self.ae(acts[0].func, func)
|
self.ae(acts[0].func, func)
|
||||||
self.ae(acts[0].args, args)
|
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')
|
single('some thing.txt', 'ignored')
|
||||||
self.ae(actions('x:///a.txt'), (KeyAction('one', ()), KeyAction('two', ())))
|
self.ae(actions('x:///a.txt'), (KeyAction('one', ()), KeyAction('two', ())))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user