diff --git a/kittens/tui/images.py b/kittens/tui/images.py index ec794e368..6e9b2779c 100644 --- a/kittens/tui/images.py +++ b/kittens/tui/images.py @@ -20,7 +20,7 @@ from kitty.typing import ( GRT_C, CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType ) -from kitty.utils import ScreenSize, find_exe, fit_image +from kitty.utils import ScreenSize, fit_image, which from .operations import cursor @@ -148,7 +148,7 @@ def run_imagemagick(path: str, cmd: Sequence[str], keep_stdout: bool = True) -> def identify(path: str) -> ImageData: import json q = '{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h","dpi":"%xx%y","dispose":"%D","orientation":"%[EXIF:Orientation]"}' - exe = find_exe('magick') + exe = which('magick') if exe: cmd = [exe, 'identify'] else: @@ -190,11 +190,11 @@ def render_image( import tempfile has_multiple_frames = len(m) > 1 get_multiple_frames = has_multiple_frames and not only_first_frame - exe = find_exe('magick') + exe = which('magick') if exe: cmd = [exe, 'convert'] else: - exe = find_exe('convert') + exe = which('convert') if exe is None: raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH') cmd = [exe] diff --git a/kitty/child.py b/kitty/child.py index ce3f6cf63..927cba1d2 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -12,6 +12,7 @@ from typing import ( import kitty.fast_data_types as fast_data_types +from .utils import which from .constants import is_macos, kitty_base_dir, shell_path, terminfo_dir from .types import run_once @@ -269,6 +270,7 @@ class Child: # xterm, urxvt, konsole and gnome-terminal do not do it in my # testing. argv[0] = ('-' + exe.split('/')[-1]) + exe = which(exe) or exe pid = fast_data_types.spawn(exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd) os.close(slave) self.pid = pid diff --git a/kitty/launch.py b/kitty/launch.py index 90885c657..85fe9627c 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -9,13 +9,11 @@ from .child import Child from .cli import parse_args from .cli_stub import LaunchCLIOptions from .constants import resolve_custom_file -from .fast_data_types import ( - get_options, patch_color_profiles, set_clipboard_string -) +from .fast_data_types import patch_color_profiles, set_clipboard_string from .options.utils import env as parse_env from .tabs import Tab from .types import run_once -from .utils import find_exe, read_shell_environment, set_primary_selection, log_error +from .utils import log_error, set_primary_selection, which from .window import Watchers, Window try: @@ -379,12 +377,7 @@ def launch( elif x == '@cursor-y': x = str(screen.cursor.y + 1) final_cmd.append(x) - exe = find_exe(final_cmd[0]) - if not exe: - xenv = read_shell_environment(get_options()) - if 'PATH' in xenv: - import shutil - exe = shutil.which(final_cmd[0], path=xenv['PATH']) + exe = which(final_cmd[0]) if exe: final_cmd[0] = exe kw['cmd'] = final_cmd diff --git a/kitty/utils.py b/kitty/utils.py index a99b20104..854c66176 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -512,7 +512,7 @@ def resolve_editor_cmd(editor: str, shell_env: Mapping[str, str]) -> Optional[st return ' '.join(map(shlex.quote, editor_cmd)) if shell_env is os.environ: - q = find_exe(editor_exe) + q = which(editor_exe, only_system=True) if q: return patched(q) elif 'PATH' in shell_env: @@ -602,7 +602,7 @@ def resolved_shell(opts: Optional[Options] = None) -> List[str]: @run_once -def system_paths_on_macos() -> List[str]: +def system_paths_on_macos() -> Tuple[str, ...]: entries, seen = [], set() def add_from_file(x: str) -> None: @@ -624,23 +624,51 @@ def system_paths_on_macos() -> List[str]: for name in sorted(files): add_from_file(os.path.join('/etc/paths.d', name)) add_from_file('/etc/paths') - return entries + return tuple(entries) -@lru_cache(maxsize=32) -def find_exe(name: str) -> Optional[str]: +def which(name: str, only_system: bool = False) -> Optional[str]: import shutil - ans = shutil.which(name) - if ans is None: - # In case PATH is messed up - if is_macos: - paths = system_paths_on_macos() - else: - paths = ['/usr/local/bin', '/opt/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'] - paths.insert(0, os.path.expanduser('~/.local/bin')) - path = os.pathsep.join(paths) + os.pathsep + os.defpath - ans = shutil.which(name, path=path) - return ans + if os.sep in name: + return name + paths = [] + ep = os.environ.get('PATH') + if ep: + paths = os.pathsep.split(ep) + paths.append(os.path.expanduser('~/.local/bin')) + paths.append(os.path.expanduser('~/bin')) + ans = shutil.which(name, path=os.pathsep.join(paths)) + if ans: + return ans + # In case PATH is messed up try a default set of paths + if is_macos: + system_paths = system_paths_on_macos() + else: + system_paths = ('/usr/local/bin', '/opt/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin') + tried_paths = set(paths) + system_paths = tuple(x for x in system_paths if x not in tried_paths) + if system_paths: + ans = shutil.which(name, path=os.pathsep.join(system_paths)) + if ans: + return ans + tried_paths |= set(system_paths) + if only_system: + return None + from .fast_data_types import get_options + try: + opts = get_options() + except RuntimeError: + return None + shell_env = read_shell_environment(opts) + for xenv in (shell_env, opts.env): + q = xenv.get('PATH') + if q: + paths = [x for x in xenv['PATH'].split(os.pathsep) if x not in tried_paths] + ans = shutil.which(name, path=os.pathsep.join(paths)) + if ans: + return ans + tried_paths |= set(paths) + return None def read_shell_environment(opts: Optional[Options] = None) -> Dict[str, str]: