diff --git a/docs/changelog.rst b/docs/changelog.rst index 2deed70d3..61432a471 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,6 +52,9 @@ To update |kitty|, :doc:`follow the instructions `. - A new option :opt:`bell_path` to specify the path to a sound file to use as the bell sound +- A new option :opt:`exe_search_path` to modify the locations kitty searches + for executables to run + - broadcast kitten: Show a "fake" cursor in all windows being broadcast too (:iss:`4225`) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 2aa7a8d27..1dc4a1bc5 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -2616,7 +2616,7 @@ will delete the variable from the child process' environment. ) opt('+watcher', '', - option_type='watcher', + option_type='store_multiple', add_to_default=False, long_text=''' Path to python file which will be loaded for :ref:`watchers`. @@ -2627,6 +2627,28 @@ Note that reloading the config will only affect windows created after the reload. ''') +opt('+exe_search_path', '', + option_type='store_multiple', + add_to_default=False, + long_text=''' +Control where kitty looks to find programs to run. The default search order is: +First search the system wide :code:`PATH`, then :file:`~/.local/bin` and :file:`~/bin`. +If not still not found, the :code:`PATH` defined in the login shell after sourcing +all its startup files is tried. Finally, if present, the :code:`PATH` in the :opt:`env` +option is tried. + +This option allows you to prepend, append, or remove paths from this search order. +It can be specified multiple times for multiple paths. A simple path will be prepended +to the search order. A path that starts with the :code:`+` sign will be append to the search +order, after :file:`~/bin` above. A path that starts with the :code:`-` sign will be removed +from the entire search order. For example:: + + exe_search_path /some/prepended/path + exe_search_path +/some/appended/path + exe_search_path -/some/excluded/path + +''') + opt('update_check_interval', '24', option_type='float', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 906008467..a3c0e0662 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -13,10 +13,10 @@ from kitty.options.utils import ( deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations, macos_option_as_alt, macos_titlebar_color, optional_edge_width, parse_map, parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, shell_integration, - symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, - tab_font_style, tab_separator, tab_title_template, to_cursor_shape, to_font_size, to_layout_names, - to_modifiers, url_prefixes, url_style, visual_window_select_characters, watcher, - window_border_width, window_size + store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, + tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, to_cursor_shape, + to_font_size, to_layout_names, to_modifiers, url_prefixes, url_style, + visual_window_select_characters, window_border_width, window_size ) @@ -949,6 +949,10 @@ class Parser: for k, v in env(val, ans["env"]): ans["env"][k] = v + def exe_search_path(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + for k, v in store_multiple(val, ans["exe_search_path"]): + ans["exe_search_path"][k] = v + def file_transfer_confirmation_bypass(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['file_transfer_confirmation_bypass'] = str(val) @@ -1255,7 +1259,7 @@ class Parser: ans['visual_window_select_characters'] = visual_window_select_characters(val) def watcher(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: - for k, v in watcher(val, ans["watcher"]): + for k, v in store_multiple(val, ans["watcher"]): ans["watcher"][k] = v def wayland_titlebar_color(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: @@ -1321,6 +1325,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]: return { 'action_alias': {}, 'env': {}, + 'exe_search_path': {}, 'font_features': {}, 'kitten_alias': {}, 'symbol_map': {}, diff --git a/kitty/options/types.py b/kitty/options/types.py index 8f259095d..724a25c5b 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -346,6 +346,7 @@ option_names = ( # {{{ 'enable_audio_bell', 'enabled_layouts', 'env', + 'exe_search_path', 'file_transfer_confirmation_bypass', 'focus_follows_mouse', 'font_family', @@ -588,6 +589,7 @@ class Options: window_resize_step_lines: int = 2 action_alias: typing.Dict[str, str] = {} env: typing.Dict[str, str] = {} + exe_search_path: typing.Dict[str, str] = {} font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {} kitten_alias: typing.Dict[str, str] = {} symbol_map: typing.Dict[typing.Tuple[int, int], str] = {} @@ -705,6 +707,7 @@ class Options: defaults = Options() defaults.action_alias = {} defaults.env = {} +defaults.exe_search_path = {} defaults.font_features = {} defaults.kitten_alias = {} defaults.symbol_map = {} diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 8736d81f6..437e39219 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -761,7 +761,7 @@ def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]: yield val, DELETE_ENV_VAR -def watcher(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]: +def store_multiple(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]: val = val.strip() if val not in current_val: yield val, val diff --git a/kitty/utils.py b/kitty/utils.py index 94980a8e9..ab108e1e1 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -635,13 +635,26 @@ def which(name: str, only_system: bool = False) -> Optional[str]: with suppress(RuntimeError): opts = get_options() + tried_paths = set() paths = [] + append_paths = [] + if opts and opts.exe_search_path: + for x in opts.exe_search_path: + x = x.strip() + if x: + if x[0] == '-': + tried_paths.add(x[1:]) + elif x[0] == '+': + append_paths.append(x[1:]) + else: + paths.append(x) ep = os.environ.get('PATH') if ep: - paths = ep.split(os.pathsep) + paths.extend(ep.split(os.pathsep)) paths.append(os.path.expanduser('~/.local/bin')) paths.append(os.path.expanduser('~/bin')) - ans = shutil.which(name, path=os.pathsep.join(paths)) + paths.extend(append_paths) + ans = shutil.which(name, path=os.pathsep.join(x for x in paths if x not in tried_paths)) if ans: return ans # In case PATH is messed up try a default set of paths @@ -649,7 +662,7 @@ def which(name: str, only_system: bool = False) -> Optional[str]: system_paths = system_paths_on_macos() else: system_paths = ('/usr/local/bin', '/opt/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin') - tried_paths = set(paths) + 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))