From d9ee1ddce42f1926895377889719f7b20b6832aa Mon Sep 17 00:00:00 2001 From: gibbyb Date: Wed, 25 Mar 2026 10:04:49 -0500 Subject: [PATCH] Changes since using laptop --- .../extensions/nautilus_open_any_terminal.py | 614 ++++++++++++++++++ config/dot/kitty/kitty.conf | 2 +- config/dot/nvim/lua/plugins/lsp.lua | 2 +- 3 files changed, 616 insertions(+), 2 deletions(-) create mode 100644 config/copy/usr/share/nautilus-python/extensions/nautilus_open_any_terminal.py diff --git a/config/copy/usr/share/nautilus-python/extensions/nautilus_open_any_terminal.py b/config/copy/usr/share/nautilus-python/extensions/nautilus_open_any_terminal.py new file mode 100644 index 0000000..d6640b8 --- /dev/null +++ b/config/copy/usr/share/nautilus-python/extensions/nautilus_open_any_terminal.py @@ -0,0 +1,614 @@ +"""nautilus extension: nautilus_open_any_terminal""" + +# based on: https://github.com/gnunn1/tilix/blob/master/data/nautilus/open-tilix.py + +import ast +import re +import shlex +from dataclasses import dataclass, field +from functools import cache +from gettext import gettext, translation +from os.path import expanduser +from subprocess import Popen +from typing import Optional +from urllib.parse import quote, unquote, urlparse + +from gi import require_version + +try: + require_version("Nautilus", "4.1") +except ValueError: + require_version("Nautilus", "4.0") + +require_version("Gtk", "4.0") + +from gi.repository import Nautilus as FileManager +API_VERSION = "4.1" + +from gi.repository import Gio, GLib, GObject, Gtk # noqa: E402 pylint: disable=wrong-import-position + + +@dataclass(frozen=True) +class Terminal: + """Data class representing a terminal configuration.""" + + name: str + workdir_arguments: Optional[list[str]] = None + new_tab_arguments: Optional[list[str]] = None + new_window_arguments: Optional[list[str]] = None + command_arguments: list[str] = field(default_factory=lambda: ["-e"]) + flatpak_package: Optional[str] = None + + +_ = gettext +for localedir in [expanduser("~/.local/share/locale"), "/usr/share/locale"]: + try: + trans = translation("nautilus-open-any-terminal", localedir) + trans.install() + _ = trans.gettext + break + except FileNotFoundError: + continue + +TERMINALS = { + "alacritty": Terminal("Alacritty"), + "app2unit-term": Terminal("app2unit-term"), + "blackbox": Terminal( + "Black Box", + workdir_arguments=["--working-directory"], + command_arguments=["-c"], + flatpak_package="com.raggesilver.BlackBox", + ), + "blackbox-terminal": Terminal( + "Black Box", + workdir_arguments=["--working-directory"], + command_arguments=["-c"], + ), + "bobcat": Terminal( + "Bobcat", + workdir_arguments=["--working-dir"], + command_arguments=["--"], + ), + "cool-retro-term": Terminal("cool-retro-term", workdir_arguments=["--workdir"]), + "custom": Terminal(_("Terminal"), command_arguments=[]), + "contour": Terminal( + "Contour", + workdir_arguments=["--working-directory"], + flatpak_package="org.contourterminal.Contour", + ), + "cosmic-term": Terminal("COSMIC Terminal"), + "deepin-terminal": Terminal("Deepin Terminal"), + "ddterm": Terminal( + "Drop down Terminal extension", + workdir_arguments=["--working-directory"], + flatpak_package="com.github.amezin.ddterm", + ), + "foot": Terminal("Foot"), + "footclient": Terminal("FootClient"), + "ghostty": Terminal("Ghostty"), + "gnome-terminal": Terminal("Terminal", new_tab_arguments=["--tab"], command_arguments=["--"]), + "guake": Terminal("Guake", workdir_arguments=["--show", "--new-tab"]), + "kermit": Terminal("Kermit"), + "kgx": Terminal("Console", new_tab_arguments=["--tab"]), + "kitty": Terminal("Kitty"), + "konsole": Terminal("Konsole", new_tab_arguments=["--new-tab"]), + "mate-terminal": Terminal("Mate Terminal", new_tab_arguments=["--tab"]), + "mlterm": Terminal("Mlterm"), + "ptyxis": Terminal( + "Ptyxis", + workdir_arguments=["-d"], + command_arguments=["--"], + new_tab_arguments=["--tab"], + new_window_arguments=["--new-window"], + flatpak_package="app.devsuite.Ptyxis", + ), + "ptyxis-nightly": Terminal( + "Ptyxis", + workdir_arguments=["-d"], + command_arguments=["--"], + new_tab_arguments=["--tab"], + new_window_arguments=["--new-window"], + flatpak_package="org.gnome.Ptyxis.Devel", + ), + "qterminal": Terminal("QTerminal"), + "rio": Terminal("Rio"), + "sakura": Terminal("Sakura"), + "st": Terminal("Simple Terminal"), + "tabby": Terminal("Tabby", command_arguments=["run"], workdir_arguments=["open"]), + "terminator": Terminal("Terminator", new_tab_arguments=["--new-tab"]), + "terminology": Terminal("Terminology"), + "terminus": Terminal("Terminus"), + "termite": Terminal("Termite"), + "tilix": Terminal("Tilix", flatpak_package="com.gexperts.Tilix"), + "urxvt": Terminal("rxvt-unicode"), + "urxvtc": Terminal("urxvtc"), + "uwsm-terminal": Terminal("uwsm-terminal"), + "uxterm": Terminal("UXTerm"), + "warp": Terminal( + "Warp", + new_tab_arguments=["--virtual-arg-for-tabs"], # This is just to indicate tab support + ), + "wezterm": Terminal( + "Wez's Terminal Emulator", + workdir_arguments=["--cwd"], + new_tab_arguments=["start", "--new-tab"], + new_window_arguments=["start"], + flatpak_package="org.wezfurlong.wezterm", + ), + "xfce4-terminal": Terminal("Xfce Terminal", new_tab_arguments=["--tab"]), + "xterm": Terminal("XTerm"), +} + +FLATPAK_PARMS = ["off", "system", "user"] + +terminal = "gnome-terminal" +terminal_cmd: list[str] = None # type: ignore +terminal_data: Terminal = TERMINALS["gnome-terminal"] +new_tab = False +flatpak = FLATPAK_PARMS[0] +custom_local_command: str +custom_remote_command: str + +GSETTINGS_PATH = "com.github.stunkymonkey.nautilus-open-any-terminal" +GSETTINGS_KEYBINDINGS = "keybindings" +GSETTINGS_BIND_REMOTE = "bind-remote" +GSETTINGS_TERMINAL = "terminal" +GSETTINGS_NEW_TAB = "new-tab" +GSETTINGS_FLATPAK = "flatpak" +GSETTINGS_USE_GENERIC_TERMINAL_NAME = "use-generic-terminal-name" +GSETTINGS_CUSTOM_LOCAL_COMMAND = "custom-local-command" +GSETTINGS_CUSTOM_REMOTE_COMMAND = "custom-remote-command" +REMOTE_URI_SCHEME = ["ftp", "sftp"] + + +# Adapted from https://www.freedesktop.org/software/systemd/man/latest/os-release.html +def read_os_release(): + """Read and parse the OS release information.""" + possible_os_release_paths = ["/etc/os-release", "/usr/lib/os-release"] + for file_path in possible_os_release_paths: + try: + with open(file_path, mode="r", encoding="utf-8") as os_release: + for line_number, line in enumerate(os_release, start=1): + line = line.rstrip() + if not line or line.startswith("#"): + continue + result = re.match(r"([A-Z][A-Z_0-9]+)=(.*)", line) + if result: + name, val = result.groups() + if val and val[0] in "\"'": + val = ast.literal_eval(val) + yield name, val + else: + raise OSError(f"{file_path}:{line_number}: bad line {line!r}") + except FileNotFoundError: + continue + + +@cache +def distro_id() -> set[str]: + """get the set of distribution ids""" + try: + os_release = dict(read_os_release()) + except OSError: + return set(["unknown"]) + ids = [os_release["ID"]] + if id_like := os_release.get("ID_LIKE"): + ids.extend(id_like.split(" ")) + return set(ids) + + +def parse_custom_command(command: str, data: str | list[str]) -> list[str]: + """Substitute every '%s' in the command with data and split it into arguments""" + if isinstance(data, str): + data = [data] + + return shlex.split(command.replace("%s", shlex.join(data))) + + +def run_command_in_terminal(command: list[str], *, cwd: str | None = None): + if terminal == "custom": + cmd = parse_custom_command(custom_remote_command, command) + else: + cmd = terminal_cmd.copy() + if cwd and terminal_data.workdir_arguments: + cmd.extend(terminal_data.workdir_arguments) + cmd.append(cwd) + cmd.extend(terminal_data.command_arguments) + cmd.extend(command) + + Popen(cmd, cwd=cwd) # pylint: disable=consider-using-with + + +def ssh_command_from_uri(uri: str, *, is_directory: bool): + """Creates an ssh command that executes or cd's into remote uri""" + result = urlparse(uri) + cmd = ["ssh", "-t"] + if result.username: + cmd.append(f"{result.username}@{result.hostname}") + else: + cmd.append(result.hostname) # type: ignore + + if result.port: + cmd.append("-p") + cmd.append(str(result.port)) + + target = shlex.quote(unquote(result.path)) + if is_directory: + cmd.extend(["cd", target, ";", "exec", "${SHELL:-/bin/sh}", "-l"]) + else: + cmd.extend(["exec", target]) + + return cmd + + +def open_remote_terminal_in_uri(uri: str): + """Open a new remote terminal""" + run_command_in_terminal(ssh_command_from_uri(uri, is_directory=True)) + + +def open_local_terminal_in_uri(uri: str): + """open the new terminal with correct path""" + result = urlparse(uri) + filename = unquote(result.path) + if result.scheme == "admin": + run_command_in_terminal(["sudo", "-s"], cwd=filename) + return + + if terminal == "warp": + # Force new_tab to be considered even without traditional tab arguments + Popen( # pylint: disable=consider-using-with + ["xdg-open", f"warp://action/new_{'tab' if new_tab else 'window'}?path={result.path}"] + ) + return + + cmd = terminal_cmd.copy() + if terminal == "custom": + cmd = parse_custom_command(custom_local_command, filename) + elif filename and terminal_data.workdir_arguments: + cmd.extend(terminal_data.workdir_arguments) + cmd.append(filename) + + Popen(cmd, cwd=filename) # pylint: disable=consider-using-with + + +def directory_menu_item_id(*, foreground: bool, remote: bool): + return f"OpenTerminal::open{'_' if foreground else '_bg_'}{'remote' if remote else 'file'}_item" + + +def executable_menu_item_id(*, remote: bool): + return f"OpenTerminal::execute{'_remote_' if remote else '_file_'}item" + + +def get_directory_menu_items( + file: FileManager.FileInfo, callback, *, foreground: bool, terminal_name: str | None = None +): + items = [] + remote = file.get_uri_scheme() in REMOTE_URI_SCHEME + terminal_name = terminal_name or terminal_data.name + + if remote: + if foreground: + REMOTE_LABEL = _("Open in Remote {}") + REMOTE_TIP = _("Open Remote {} in {}") + LOCAL_LABEL = _("Open in Local {}") + LOCAL_TIP = _("Open Local {} in {}") + tip = REMOTE_TIP.format(terminal_name, file.get_name()) + else: + REMOTE_LABEL = _("Open Remote {} Here") + REMOTE_TIP = _("Open Remote {} in This Directory") + LOCAL_LABEL = _("Open Local {} Here") + LOCAL_TIP = _("Open Local {} in This Directory") + tip = REMOTE_TIP.format(terminal_name) + + item = FileManager.MenuItem( + name=directory_menu_item_id(foreground=foreground, remote=True), + label=REMOTE_LABEL.format(terminal_name), + tip=tip, + ) + item.connect("activate", callback, file, True) + items.append(item) + elif foreground: + LOCAL_LABEL = _("Open in {}") + LOCAL_TIP = _("Open {} in {}") + else: + LOCAL_LABEL = _("Open {} Here") + LOCAL_TIP = _("Open {} in This Directory") + + # Let wezterm handle opening a local terminal + if terminal == "wezterm" and flatpak == "off": + return items + + if foreground: + tip = LOCAL_TIP.format(terminal_name, file.get_name()) + else: + tip = LOCAL_TIP.format(terminal_name) + + item = FileManager.MenuItem( + name=directory_menu_item_id(foreground=foreground, remote=False), + label=LOCAL_LABEL.format(terminal_name), + tip=tip, + ) + item.connect("activate", callback, file, False) + items.append(item) + return items + + +def get_executable_menu_items(file: FileManager.FileInfo, callback, *, terminal_name: str | None = None): + items = [] + remote = file.get_uri_scheme() in REMOTE_URI_SCHEME + terminal_name = terminal_name or terminal_data.name + + if remote: + REMOTE_LABEL = _("Execute in Remote {}") + REMOTE_TIP = _("Execute {} in {} via SSH") + LOCAL_LABEL = _("Execute in Local {}") + LOCAL_TIP = _("Execute {} in Local {}") + + tip = REMOTE_TIP.format(file.get_name(), terminal_name) + item = FileManager.MenuItem( + name=executable_menu_item_id(remote=True), + label=REMOTE_LABEL.format(terminal_name), + tip=tip, + ) + item.connect("activate", callback, file, True) + items.append(item) + else: + LOCAL_LABEL = _("Execute in {}") + LOCAL_TIP = _("Execute {} in {}") + + tip = LOCAL_TIP.format(file.get_name(), terminal_name) + item = FileManager.MenuItem( + name=executable_menu_item_id(remote=False), + label=LOCAL_LABEL.format(terminal_name), + tip=tip, + ) + item.connect("activate", callback, file, False) + items.append(item) + return items + + +def is_executable(file: Gio.File) -> bool: + try: + attributes = file.query_info("access::can-execute", Gio.FileQueryInfoFlags.NONE) + except GLib.Error: + return False + return attributes.get_attribute_boolean("access::can-execute") + + +def set_terminal_args(*_args): + # pylint: disable=possibly-used-before-assignment + """set the terminal_cmd to the correct values""" + global new_tab + global flatpak + global terminal_cmd + global terminal_data + global custom_local_command + global custom_remote_command + value = _gsettings.get_string(GSETTINGS_TERMINAL) + newer_tab = _gsettings.get_boolean(GSETTINGS_NEW_TAB) + flatpak = FLATPAK_PARMS[_gsettings.get_enum(GSETTINGS_FLATPAK)] + new_terminal_data = TERMINALS.get(value) + if not new_terminal_data: + print(f'open-any-terminal: unknown terminal "{value}"') + return + + global terminal + terminal = value + terminal_data = new_terminal_data + if newer_tab and terminal_data.new_tab_arguments: + new_tab = newer_tab + new_tab_text = "opening in a new tab" + else: + new_tab_text = "opening a new window" + if newer_tab and not terminal_data.new_tab_arguments: + new_tab_text += " (terminal does not support tabs)" + if flatpak != FLATPAK_PARMS[0] and terminal_data.flatpak_package is not None: + terminal_cmd = ["flatpak", "run", "--" + flatpak, terminal_data.flatpak_package] + flatpak_text = f"with flatpak as {flatpak}" + else: + terminal_cmd = [terminal] + if terminal == "blackbox" and "fedora" in distro_id(): + # It's called like this on fedora + terminal_cmd[0] = "blackbox-terminal" + flatpak = FLATPAK_PARMS[0] + flatpak_text = "" + + if terminal == "custom": + terminal_cmd = [] + custom_local_command = _gsettings.get_string(GSETTINGS_CUSTOM_LOCAL_COMMAND) + custom_remote_command = _gsettings.get_string(GSETTINGS_CUSTOM_REMOTE_COMMAND) + elif new_tab and terminal_data.new_tab_arguments: + terminal_cmd.extend(terminal_data.new_tab_arguments) + elif terminal_data.new_window_arguments: + terminal_cmd.extend(terminal_data.new_window_arguments) + + print(f'open-any-terminal: terminal is set to "{terminal}" {new_tab_text} {flatpak_text}') + + +if API_VERSION == ("4.0", "4.1"): + + class OpenAnyTerminalShortcutProvider(GObject.GObject, FileManager.MenuProvider): + """Provide keyboard shortcuts for opening terminals in Nautilus.""" + + def __init__(self): + super().__init__() + self.previous_cwd = expanduser("~") + + gsettings_source = Gio.SettingsSchemaSource.get_default() + if gsettings_source.lookup(GSETTINGS_PATH, True): + self._gsettings = Gio.Settings.new(GSETTINGS_PATH) + self._setup_keybindings() + + def get_background_items(self, current_folder: FileManager.FileInfo): + """Update current URI when folder changes.""" + if current_folder: + if current_folder.get_uri_scheme() in REMOTE_URI_SCHEME: + folder_path = current_folder.get_uri() + else: + folder_path = current_folder.get_location().get_path() + + if folder_path and folder_path != self.previous_cwd: + self.previous_cwd = folder_path + return [] + + def _open_terminal(self, *_args): + """Open the terminal at the specified URI.""" + if self._gsettings.get_boolean(GSETTINGS_BIND_REMOTE): + open_remote_terminal_in_uri(self.previous_cwd) + else: + open_local_terminal_in_uri(self.previous_cwd) + + def _setup_keybindings(self): + """Set up custom keybindings for the extension.""" + self.app = Gtk.Application.get_default() + if self.app is None: + print("No Gtk.Application found. Keybindings cannot be set.") + return + + action = Gio.SimpleAction.new("open_any_terminal", None) + action.connect("activate", self._open_terminal) + self.app.add_action(action) + self._bind_shortcut() + self._gsettings.connect("changed", self._update_shortcut) + + def _update_shortcut(self, _gsettings, key): + """remove keybinding""" + if key == GSETTINGS_KEYBINDINGS: + self.app.set_accels_for_action("app.open_any_terminal", []) + self._bind_shortcut() + + def _bind_shortcut(self): + """Parse and update keybindings when settings change.""" + shortcut = self._gsettings.get_string(GSETTINGS_KEYBINDINGS) + + if not shortcut: + self.app.set_accels_for_action("app.open_any_terminal", []) + return + + valid, key, mods = Gtk.accelerator_parse(shortcut) + if not valid: + print("Invalid shortcut in GSettings: %r", shortcut) + self.app.set_accels_for_action("app.open_any_terminal", []) + return + + normalized = Gtk.accelerator_name(key, mods) + self.app.set_accels_for_action("app.open_any_terminal", [normalized]) + +elif API_VERSION in ("3.0", "2.0"): + + class OpenAnyTerminalShortcutProviderLegacy(GObject.GObject, FileManager.LocationWidgetProvider): + """Provide keyboard shortcuts for opening terminals in Nautilus/Caja.""" + + def __init__(self): + super().__init__() + gsettings_source = Gio.SettingsSchemaSource.get_default() + if gsettings_source.lookup(GSETTINGS_PATH, True): + self._gsettings = Gio.Settings.new(GSETTINGS_PATH) + self._gsettings.connect("changed", self._bind_shortcut) + self._create_accel_group() + self._window = None + self._uri = None + + def _create_accel_group(self): + self._accel_group = Gtk.AccelGroup() + shortcut = self._gsettings.get_string(GSETTINGS_KEYBINDINGS) + key, mod = Gtk.accelerator_parse(shortcut) + self._accel_group.connect(key, mod, Gtk.AccelFlags.VISIBLE, self._open_terminal) + + def _bind_shortcut(self, _gsettings, key): + if key == GSETTINGS_KEYBINDINGS: + self._accel_group.disconnect(self._open_terminal) + self._create_accel_group() + + def _open_terminal(self, *_args): + if _gsettings.get_boolean(GSETTINGS_BIND_REMOTE): + open_local_terminal_in_uri(self._uri) + else: + open_remote_terminal_in_uri(self._uri) + + def get_widget(self, uri, window): + """follows uri and sets the correct window""" + self._uri = uri + if self._window: + self._window.remove_accel_group(self._accel_group) + if self._gsettings: + window.add_accel_group(self._accel_group) + self._window = window + + +class OpenAnyTerminalExtension(GObject.GObject, FileManager.MenuProvider): + """Provide context menu items for opening terminals in Nautilus.""" + + def __init__(self): + super().__init__() + gsettings_source = Gio.SettingsSchemaSource.get_default() + if gsettings_source.lookup(GSETTINGS_PATH, True): + self._gsettings = Gio.Settings.new(GSETTINGS_PATH) + + def _get_terminal_name(self): + if self._gsettings.get_boolean(GSETTINGS_USE_GENERIC_TERMINAL_NAME): + return _("Terminal") + return None + + def _menu_dir_activate_cb(self, menu, file_, remote: bool): + if remote: + open_remote_terminal_in_uri(file_.get_uri()) + else: + if file_.get_uri_scheme() == "smb": + file_uri = "file://" + quote(file_.get_location().get_path()) + else: + file_uri = file_.get_uri() + open_local_terminal_in_uri(file_uri) + + def _menu_exe_activate_cb(self, menu, file_, remote: bool): + if remote: + cmd = ssh_command_from_uri(file_.get_uri(), is_directory=False) + else: + result = urlparse(file_.get_uri()) + file = unquote(result.path) + + if result.scheme == "admin": + cmd = ["sudo", file] + elif terminal in ["xterm", "uxterm"]: + cmd = [f"exec {shlex.quote(file)}"] + else: + cmd = [file] + run_command_in_terminal(cmd) + + def get_file_items(self, *args): + """Generates a list of menu items for a file or folder in the Nautilus file manager.""" + # `args` will be `[files: List[Nautilus.FileInfo]]` in Nautilus 4.0 API, + # and `[window: Gtk.Widget, files: List[Nautilus.FileInfo]]` in Nautilus 3.0 API. + + files = args[-1] + + if len(files) != 1: + return [] + file_ = files[0] + + if file_.is_directory(): + return get_directory_menu_items( + file_, self._menu_dir_activate_cb, foreground=True, terminal_name=self._get_terminal_name() + ) + + if is_executable(file_.get_location()): + return get_executable_menu_items(file_, self._menu_exe_activate_cb, terminal_name=self._get_terminal_name()) + + return [] + + def get_background_items(self, *args): + """Generates a list of background menu items for a file or folder in the Nautilus file manager.""" + # `args` will be `[folder: Nautilus.FileInfo]` in Nautilus 4.0 API, + # and `[window: Gtk.Widget, file: Nautilus.FileInfo]` in Nautilus 3.0 API. + + file_ = args[-1] + return get_directory_menu_items( + file_, self._menu_dir_activate_cb, foreground=False, terminal_name=self._get_terminal_name() + ) + + +source = Gio.SettingsSchemaSource.get_default() +if source is not None and source.lookup(GSETTINGS_PATH, True): + _gsettings = Gio.Settings.new(GSETTINGS_PATH) + _gsettings.connect("changed", set_terminal_args) + set_terminal_args() diff --git a/config/dot/kitty/kitty.conf b/config/dot/kitty/kitty.conf index 724caa2..16aa5aa 100644 --- a/config/dot/kitty/kitty.conf +++ b/config/dot/kitty/kitty.conf @@ -1504,7 +1504,7 @@ wayland_titlebar_color #222436 #: configuration will change this value only for newly created OS #: windows. -#: linux_display_server x11 +linux_display_server x11 #: Choose between Wayland and X11 backends. By default, an appropriate #: backend based on the system state is chosen automatically. Set it diff --git a/config/dot/nvim/lua/plugins/lsp.lua b/config/dot/nvim/lua/plugins/lsp.lua index 886ddab..ae70eef 100644 --- a/config/dot/nvim/lua/plugins/lsp.lua +++ b/config/dot/nvim/lua/plugins/lsp.lua @@ -49,7 +49,7 @@ return { option = { pathMappings = { ['@'] = '${folder}/src', - ['/'] = '${folder}/src/public/', + ['/'] = '${folder}/public/', ['~'] = '${folder}/src', -- ['/images'] = '${folder}/src/images', -- ['/components'] = '${folder}/src/components',