From 13012429d135fdfad64c2594f553f10b898fd77a Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 6 Jan 2026 20:00:31 +0100 Subject: [PATCH] Add Fish module --- pasch/modules/__init__.py | 2 + pasch/modules/fish.py | 107 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 pasch/modules/fish.py diff --git a/pasch/modules/__init__.py b/pasch/modules/__init__.py index dd51a5b..ee96973 100644 --- a/pasch/modules/__init__.py +++ b/pasch/modules/__init__.py @@ -1,9 +1,11 @@ from .echo import Echo from .files import Files +from .fish import Fish from .pacman import Pacman __all__: list[str] = [ "Echo", "Files", + "Fish", "Pacman", ] diff --git a/pasch/modules/fish.py b/pasch/modules/fish.py new file mode 100644 index 0000000..7aa8bff --- /dev/null +++ b/pasch/modules/fish.py @@ -0,0 +1,107 @@ +import os +from dataclasses import dataclass + +from pasch.file.text import TextFile +from pasch.modules.files import Files +from pasch.modules.pacman import Pacman +from pasch.orchestrator import Module, Orchestrator +from pasch.util import prompt, run_execute + + +def escape(s: str) -> str: + # The only meaningful escape sequences in single quotes are `\'`, which + # escapes a single quote and `\\`, which escapes the backslash symbol. + ESCAPES = {"'": "\\'", "\\": "\\\\"} + escaped = "".join(ESCAPES.get(c, c) for c in s) + return f"'{escaped}'" + + +@dataclass +class Raw: + string: str + + +type FishStr = str | Raw + + +def fescape(s: FishStr) -> str: + if isinstance(s, Raw): + return s.string + return escape(s) + + +class Fish(Module): + def __init__( + self, + orchestrator: Orchestrator, + files: Files, + pacman: Pacman, + ) -> None: + super().__init__(orchestrator) + self._files = files + self._pacman = pacman + + self.path: list[FishStr] = [] + self.abbrs: dict[str, FishStr] = {} + self.env_vars: dict[str, FishStr] = {} + + self.commands: list[str] = [] + self.interactive_commands: list[str] = [] + + def add_to_path(self, value: FishStr) -> None: + self.path.append(value) + + def add_abbr(self, name: str, replacement: FishStr) -> None: + self.abbrs[name] = replacement + + def add_env_var(self, name: str, value: FishStr) -> None: + self.env_vars[name] = value + + def add_command(self, command: str) -> None: + self.commands.append(command) + + def add_interactive(self, command: str) -> None: + self.interactive_commands.append(command) + + def configure(self) -> None: + file = TextFile() + file.tag(comment="#") + + # Commands set by the user should always appear after generated commands + commands = self.commands + interactive_commands = self.interactive_commands + self.commands = [] + self.interactive_commands = [] + + if self.path: + segments = " ".join(fescape(s) for s in self.path) + self.add_command(f"set PATH $PATH {segments}") + for name, replacement in sorted(self.abbrs.items()): + self.add_interactive(f"'abbr' {escape(name)} {fescape(replacement)}") + for name, value in sorted(self.env_vars.items()): + self.add_command(f"set -gx {escape(name)} {fescape(value)}") + + self.commands.extend(commands) + self.interactive_commands.extend(interactive_commands) + + for command in self.commands: + file.append(command) + if self.commands and self.interactive_commands: + file.append("") + if self.interactive_commands: + file.append("if status is-interactive") + for command in self.interactive_commands: + file.append(f" {command}") + file.append("end") + + self._files.add(".config/fish/config.fish", file) + self._pacman.install("fish") + + def execute(self) -> None: + shell = os.environ.get("SHELL") + if shell == "/usr/bin/fish": + return + fix = prompt("Your shell is not fish. Set it to fish?", default=False) + if not fix: + return + run_execute("sudo", "usermod", "--shell", "/usr/bin/fish", self.o.user)