Skip to content

[FEATURE] freeBSD support #131

Description

@NyAncQt

What would you like to be added?

maybe add freeBSD support i made the installer

How will it help?

manifest.toml

packages = ["caelestia-shell", "caelestia-cli"]
post_install = [
"test -f ${XDG_STATE_HOME:-$HOME/.local/state}/caelestia/scheme.json || caelestia scheme set -n caelestia",
"caelestia shell -d >/dev/null",
]
post_update = ["caelestia shell -k; sleep 1; caelestia shell -d >/dev/null"]

--- Core components ---

[[components]]
name = "hypr"
default = true
packages = [
"hyprland",
"xdg-desktop-portal-hyprland",
"xdg-desktop-portal-gtk",
"nerd-fonts", # FreeBSD combines nerd fonts or handles them via specific port naming
]
post_install = ["hyprctl reload >/dev/null"]
post_update = ["hyprctl reload >/dev/null"]
[[components.entries]]
src = "hypr"
dest = "$XDG_CONFIG_HOME/hypr"

[[components]]
name = "fish"
default = true
packages = ["fish", "eza", "zoxide", "direnv"]
post_install = [
"mkdir -p ${XDG_CONFIG_HOME:-$HOME/.config}/caelestia",
"touch ${XDG_CONFIG_HOME:-$HOME/.config}/caelestia/user-config.fish",
]
[[components.entries]]
src = "fish"
dest = "$XDG_CONFIG_HOME/fish"

[[components]]
name = "foot"
default = true
packages = ["foot", "fish"]
[[components.entries]]
src = "foot"
dest = "$XDG_CONFIG_HOME/foot"

[[components]]
name = "fastfetch"
default = true
packages = ["fastfetch"]
[[components.entries]]
src = "fastfetch"
dest = "$XDG_CONFIG_HOME/fastfetch"

[[components]]
name = "btop"
default = true
packages = ["btop"]
post_install = ["killall -USR2 btop 2>/dev/null || true"]
post_update = ["killall -USR2 btop 2>/dev/null || true"]
[[components.entries]]
src = "btop"
dest = "$XDG_CONFIG_HOME/btop"

[[components]]
name = "micro"
default = true
packages = ["micro"]
[[components.entries]]
src = "micro"
dest = "$XDG_CONFIG_HOME/micro"

[[components]]
name = "thunar"
default = true
packages = ["thunar"]
[[components.entries]]
src = "thunar"
dest = "$XDG_CONFIG_HOME/Thunar"

[[components]]
name = "starship"
default = true
packages = ["starship"]
[[components.entries]]
src = "starship.toml"
dest = "$XDG_CONFIG_HOME/starship.toml"

[[components]]
name = "firefox"
default = true
packages = ["firefox"] # Removed local custom build dependency trigger
post_package = ["bash $CAELESTIA_DOTS/firefox/init_firefox.sh"]
[[components.entries]]
src = "firefox/userChrome.css"
dest = "$HOME/.mozilla/firefox/.default/chrome/userChrome.css"
[[components.entries]]
src = "firefox/user.js"
dest = "$HOME/.mozilla/firefox/.default/user.js"

[[components]]
name = "gtk"
default = true
packages = ["adwaita-icon-theme", "papirus-icon-theme"] # Standard FreeBSD theme names

[[components]]
name = "qt"
default = true
packages = [
"qt6-base", # Uses standard FreeBSD Qt6 libraries instead of individual arch configs
"papirus-icon-theme",
]

[[components]]
name = "auth"
default = true
packages = ["gnome-keyring", "polkit"]

[[components]]
name = "network"
default = true

FreeBSD uses standard system networking configuration natively,

but if you manage it through networkmanager, use the generic name:

packages = ["NetworkManager"]

[[components]]
name = "bluetooth"
default = true

FreeBSD includes its bluetooth stack directly in the base system!

No extra packages required here.

packages = []

[[components]]
name = "pipewire"
default = true
packages = [
"pipewire",
"wireplumber",
"pavucontrol",
]

[[components]]
name = "clipboard"
default = true
packages = ["wl-clipboard"] # Dropped cliphist (AUR tool)

[[components]]
name = "fonts"
default = true
packages = ["noto"]

[[components]]
name = "tools"
default = true
packages = [
"curl",
"git",
"jq",
"lazygit",
"bat",
"ripgrep",
"xdg-user-dirs",
]
post_install = ["xdg-user-dirs-update"]

--- Optional components ---

Note: Optional binary-only packages like 'vscodium-bin', 'todoist-appimage', and 'equibop'

are Linux-exclusive binaries. For FreeBSD, replaced with standard ports where supported.

[[components]]
name = "vscode"
packages = ["vscode"] # Standard FreeBSD open-source VSCode port name
[[components.entries]]
src = "vscode/settings.json"
dest = "$XDG_CONFIG_HOME/Code/User/settings.json"
[[components.entries]]
src = "vscode/keybindings.json"
dest = "$XDG_CONFIG_HOME/Code/User/keybindings.json"
[[components.entries]]
src = "vscode/flags.conf"
dest = "$XDG_CONFIG_HOME/code-flags.conf"

[[components]]
name = "zed"
packages = ["zed-editor"]

[[components]]
name = "discord"
packages = ["discord"] # Native FreeBSD discord port package

import subprocess
import shutil
import textwrap
from argparse import Namespace
from pathlib import Path

Cleaned up imports - removing legacy Arch modules

from caelestia.utils.dots.deployer import Deployer
from caelestia.utils.dots.manifest import ComponentError, Manifest, ManifestError
from caelestia.utils.dots.source import DotsSource, SourceError
from caelestia.utils.dots.state import DotsState
from caelestia.utils.io import confirm, disable_input, fatal, info, log, pause, prompt_selection, warn
from caelestia.utils.paths import (
config_backup_dir,
config_dir,
)

def _parse_list_arg(value: str | None) -> list[str] | None:
if value is None:
return None
return [item.strip() for item in value.split(",") if item.strip()]

class FreeBSDPackageInstaller:
"""Native FreeBSD Package Manager (pkg) Interface."""

def __init__(self, noconfirm: bool = False) -> None:
    self.flags = ["-y"] if noconfirm else []
    self.privilege_escalator = self._detect_escalator()

def _detect_escalator(self) -> str:
    """Determines if the system uses 'doas' or 'sudo' for admin actions."""
    if shutil.which("doas"):
        return "doas"
    if shutil.which("sudo"):
        return "sudo"
    return ""

def install(self, packages: list[str]) -> None:
    if not packages:
        return
    
    base_cmd = ["pkg", "install"] + self.flags + packages
    cmd = [self.privilege_escalator] + base_cmd if self.privilege_escalator else base_cmd
    
    try:
        subprocess.run(cmd, check=True)
    except subprocess.CalledProcessError as e:
        raise RuntimeError(f"FreeBSD pkg install failed: {e}")

def is_installed(self, package: str) -> bool:
    cmd = ["pkg", "info", "-e", package]
    res = subprocess.run(cmd, capture_output=True)
    return res.returncode == 0

def remove(self, packages: list[str]) -> None:
    if not packages:
        return
    base_cmd = ["pkg", "delete"] + self.flags + packages
    cmd = [self.privilege_escalator] + base_cmd if self.privilege_escalator else base_cmd
    subprocess.run(cmd, check=True)

class Command:
args: Namespace

def __init__(self, args: Namespace) -> None:
    self.args = args

def run(self) -> None:
    if self.args.noconfirm:
        disable_input()

    self.print_greeting()
    self.create_backup()

    source, tip, manifest = self.fetch_manifest()
    
    # Instantiate the FreeBSD native package system
    installer = FreeBSDPackageInstaller(noconfirm=getattr(self.args, "noconfirm", False))
    packages = manifest.enabled_packages()
    
    if packages:
        print()
        log("Installing FreeBSD dependencies...")
        try:
            installer.install(packages)
        except RuntimeError as e:
            fatal(e)
            
    # Core configuration file deployment
    deployed = self.deploy_configs(source, manifest)

    # Save deployment state cleanly for FreeBSD
    DotsState(
        applied_rev=tip,
        enabled_components=manifest.enabled_components,
        packages=packages,
        deployed_files=deployed,
    ).save()

    self.print_done()

def print_greeting(self) -> None:
    print(
        "\033[38;2;150;241;241m"
        + textwrap.dedent(
            r"""
            ╭─────────────────────────────────────────────────╮
            │      ______           __          __  _         │
            │     / ____/___ ____  / /__  _____/ /_(_)___ _   │
            │    / /   / __ `/ _ \/ / _ \/ ___/ __/ / __ `/   │
            │   / /___/ /_/ /  __/ /  __(__  ) /_/ / /_/ /    │
            │   \____/\__,_/\___/_/\___/____/\__/_/\__,_/     │
            │                                                 │
            │             --- FREEBSD ENGINE ---              │
            ╰─────────────────────────────────────────────────╯
            """
        )
        + "\033[0m"
    )
    info("Welcome to the Caelestia installer for FreeBSD!")
    info("This tool will configure your packages and environment automatically.")
    pause()
    print()

def create_backup(self) -> None:
    if config_dir.exists():
        if not confirm("Back up the existing config directory?", default=True):
            return

        log(f"Creating a backup of {config_dir}...")
        if config_backup_dir.exists():
            if not confirm("A backup already exists, overwrite?", default=False):
                info("Not creating backup.")
                return

            log("Deleting old backup...")
            shutil.rmtree(config_backup_dir)

        shutil.copytree(config_dir, config_backup_dir, symlinks=True)
        info(f"Created backup at {config_backup_dir}")

def fetch_manifest(self) -> tuple[DotsSource, str, Manifest]:
    print()
    log("Fetching dots repository...")
    source = DotsSource()
    try:
        source.ensure()
        tip = source.checkout_tip()
    except SourceError as e:
        fatal(e)

    enable = _parse_list_arg(getattr(self.args, "enable_components", None))
    disable = _parse_list_arg(getattr(self.args, "disable_components", None))
    try:
        manifest = source.manifest_at(tip)

        if enable is None and disable is None:
            optional = [name for name, comp in manifest.components.items() if not comp.default]
            if optional:
                enable = prompt_selection(optional, "Components to enable?")

        manifest.resolve_components(enable=enable, disable=disable)
    except (SourceError, ManifestError, ComponentError) as e:
        fatal(e)

    names = ", ".join(manifest.enabled_components) or "none"
    info(f"Enabled components: {names}")

    return source, tip, manifest

def deploy_configs(self, source: DotsSource, manifest: Manifest) -> dict[str, str]:
    print()
    log("Deploying configuration links...")
    deployer = Deployer()
    for entry in manifest.enabled_entries():
        src = source.working_path(entry.expanded_src())
        if not src.exists():
            warn(f"missing in source, skipping: {entry.src}")
            continue

        dests = entry.expanded_dests()
        if not dests:
            warn(f"dest glob matched nothing, skipping: {entry.dest}")
            continue

        for dest in dests:
            deployer.place(src, Path(dest))
            info(f"{entry.src} -> {dest}")

    return deployer.deployed_files

def print_done(self) -> None:
    print()
    info("All done! Caelestia has been installed successfully.")
    info("Plug-and-Play Verification Steps:")
    info("  - Ensure your user belongs to the 'video' group: pw groupmod video -m <username>")
    info("  - Ensure seatd is running: sysrc seatd_enable=\"YES\" && service seatd start")
    info("  - Launch via terminal using: Hyprland")

Extra info

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions