Writing a Nix helper script
                        On a random Thursday evening, I had the idea to run a bash command that’d write directly into my
                        packages.nix file.
                        That single command eventually became what I now call syd.
                    
There wasn’t a plan or a roadmap or literally anything that's linked with productivity, just that quiet moment where you fix a small annoyance, and it slowly becomes something that feels right. That’s what syd is. Something that took form over time.
What is syd?
syd is a lightweight command-line tool written in Python for managing your NixOS packages declaratively. It lets you install, remove, list, and search for packages by directly editing your Nix configuration files, so you don’t have to open them manually every time you want to make a change.
It was originally written in Bash, then rewritten in Python a day later - mostly because it felt cleaner, and gave me more room to build on top of it.
Why the name
syd is named after Syd Barrett, Pink Floyd’s original frontman who left the band in 1968 due to mental illness. There’s no deep reason behind the name - I just like rock. It used to be called nixstall, but that felt too generic. syd fit better.
How does it function?
Alright, so we’re gonna get a lil’ nerdy here and look at some code snippets.
Let’s open https://github.com/sidharthify/syd/blob/main/syd.py and look into it
INFO = f"{Fore.BLUE}syd:{Style.RESET_ALL}"
SUCCESS = f"{Fore.GREEN}SUCCESS:{Style.RESET_ALL}"
ERROR = f"{Fore.RED}ERROR:{Style.RESET_ALL}"
sudo_user = os.environ.get("SUDO_USER")
if sudo_user and sudo_user != "root":
    real_home = Path(f"/home/{sudo_user}")
else:
    real_home = Path.home()
config_base = os.getenv("XDG_CONFIG_HOME", real_home / ".config")
CONFIG_DIR = Path(config_base) / "syd"
CONFIG_FILE = CONFIG_DIR / "config"
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
                        This part defines the basic “personality” of syd - how it talks back to you in
                        the terminal.
                        INFO, SUCCESS, and ERROR use colorama to
                        colorize text output,
                        making logs easier to read and distinguish at a glance.
                    
                        Then, it deals with configuration paths. if syd is run with sudo,
                        it figures out who actually invoked it by checking
                        the SUDO_USER environment variable. that’s important because running sudo changes
                        your home directory to /root,
                        and syd needs to know where your config really lives - not root’s.
                    
                        Finally, it builds out the config directory structure using Python’s pathlib.
                        if ~/.config/syd/ doesn't exist, it's created automatically.
                    
help()
def help():
    print(f"{INFO} a lightweight declarative package manager for NixOS\\n")
    print(f"{Fore.GREEN}USAGE:{Style.RESET_ALL}")
    print("  sudo syd install <package> [more packages]")
    print("  sudo syd remove  <package> [more packages]")
    print("  syd search  <package> [more packages]")
    print("  syd isinstalled <package> [more packages]")
    print("  syd list")
    print("  syd --reset")
    print("  syd --help\\n")
    print(f"{Fore.GREEN}COMMANDS:{Style.RESET_ALL}")
    print("  install       Add one or more packages to your nix packages file")
    print("  remove        Remove one or more packages from your nix packages file")
    print("  list          Show all packages currently listed in your nix file")
    print("  search        Search for packages in nixpkgs")
    print("  search        Check if package is listed in your nix file")
    print("  --reset       Reset stored packages file path and rebuild command")
    print("  --help        Show this help message and exit\\n")
    print(f"{Fore.GREEN}EXAMPLES:{Style.RESET_ALL}")
    print("  sudo syd install vim htop curl")
    print("  sudo syd remove neovim htop curl")
    print("  syd search discord")
    print("  syd search htop neovim curl\\n")
    print(f"{INFO} Current config file: {CONFIG_FILE}")
                        This function just prints out usage info when you run syd --help.
                        unlike the rest of the program, there’s no logic or state manipulation here.
                    
                        the use of colorama again makes it clean and readable:
                        INFO shows context messages in blue, SUCCESS in green, and
                        ERROR in red.
                        the spacing and examples are designed to mirror what you’d see in standard nix or systemctl help
                        pages,
                        so even new users can get the gist quickly.
                    
                        the last line helps confirm
                        which configuration file syd is currently reading from.
                        this is especially useful when testing with sudo - since you might accidentally be
                        writing to root’s config otherwise.
                    
now that we’ve seen the basics, let’s get into the actual logic that makes syd work.
check_pkg_exists()
def check_pkg_exists(pkg: str) -> bool:
    result = subprocess.run(
        [
            "/run/current-system/sw/bin/nix",
            "--extra-experimental-features", "nix-command",
            "--extra-experimental-features", "flakes",
            "eval",
            f"github:NixOS/nixpkgs/nixos-unstable#{pkg}.meta.name"
        ],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )
    return result.returncode == 0
                        This one’s simple enough. it uses nix itself to check if a package exists inside nixpkgs.
                        it runs a command through subprocess and just checks the exit code (0
                        means success).
                        if nix says it found the package, syd takes that as a yes.
                    
reset_config()
def reset_config():
    if CONFIG_FILE.exists():
        CONFIG_FILE.unlink()
        print(f"{INFO} Config reset. Next run will ask for path and rebuild command again.")
    else:
        print(f"{ERROR} No config file found at {CONFIG_FILE}")
This deletes the current config file if it exists, basically a reset button. it’s how you tell syd to start from zero. if there’s nothing to delete, it politely complains.
setup_config()
def setup_config():
    PACKAGES = None
    REBUILD = None
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, "r") as f:
            for line in f:
                if line.startswith("packages_file="):
                    PACKAGES = Path(line.split("=", 1)[1].strip())
                elif line.startswith("rebuild_cmd="):
                    REBUILD = line.split("=", 1)[1].strip()
    else:
        PACKAGES = Path(input(f"{INFO} Enter path to your nix packages file: ").strip())
        if not PACKAGES.exists():
            print(f"{ERROR} File not found: {PACKAGES}")
            sys.exit(1)
        REBUILD = input(f"{INFO} Enter your rebuild command (e.g. sudo nixos-rebuild switch): ").strip()
        if not REBUILD:
            print(f"{ERROR} No rebuild command entered.")
            sys.exit(1)
        with open(CONFIG_FILE, "w") as f:
            f.write(f"packages_file={PACKAGES}\\n")
            f.write(f"rebuild_cmd={REBUILD}\\n")
        print(f"{SUCCESS} Saved config to {CONFIG_FILE}")
    return PACKAGES, REBUILD
This is syd’s “setup wizard.” it checks whether a config file exists and reads paths from it if it does. if not, it asks the user for two things:
the location of your packages.nix
the command you use to rebuild nixos (usually sudo nixos-rebuild switch)
                        once entered, syd saves that info in ~/.config/syd/config so you never have to
                        enter it again.
                    
rebuild_prompt()
def rebuild_prompt():
    read = input(f"{INFO} Rebuild NixOS? [y/N]: ").strip().lower()
    if read != "y":
        print(f"{INFO} Skipping rebuild.")
        return
    cmd = REBUILD.strip()
    if os.geteuid() == 0 and cmd.startswith("sudo "):
        cmd = cmd.replace("sudo ", "", 1)
        print(f"{INFO} Running (sudo stripped): {cmd}")
    env = os.environ.copy()
    env["PATH"] = "/run/wrappers/bin:/usr/local/bin:/usr/bin:/bin:/run/current-system/sw/bin"
    print(f"{INFO} Running: {cmd}")
    result = subprocess.run(cmd, shell=True, env=env)
    if result.returncode != 0:
        print(f"{ERROR} Rebuild command failed.")
        sys.exit(1)
                        This one asks if you want to rebuild nixos after making changes.
                        it also handles edge cases - like when syd is run as root, it strips out any sudo
                        from the rebuild command to avoid nesting.
                        it fixes PATH issues too, since python sometimes forgets where binaries live inside
                        nix shells.
                    
install_pkgs()
def install_pkgs(*pkgs):
    for pkg in pkgs:
        with open(PACKAGES, "r") as f:
            lines = f.readlines()
        if any(pkg in line for line in lines):
            print(f"{INFO} {ERROR} {pkg} already listed.")
            continue
        if check_pkg_exists(pkg):
            insert_index = len(lines)
            for i, line in enumerate(lines):
                if "]" in line:
                    insert_index = i
                    break
            lines.insert(insert_index, f"  {pkg}\\n")
            with open(PACKAGES, "w") as f:
                f.writelines(lines)
            print(f"{SUCCESS} Added '{pkg}' to {PACKAGES}")
        else:
            print(f"{ERROR} Package '{pkg}' not found in nixpkgs.")
    rebuild_prompt()
                        This is syd’s main feature - adding packages declaratively.
                        it checks whether a package already exists, verifies it actually exists in nixpkgs,
                        and then inserts it into the list just before the closing ] in
                        packages.nix.
                        finally, it calls rebuild_prompt() to offer a system rebuild.
                    
remove_pkgs()
def remove_pkgs(*pkgs):
    for pkg in pkgs:
        with open(PACKAGES, "r") as f:
            lines = f.readlines()
        new_lines = [line for line in lines if pkg not in line.strip()]
        if len(new_lines) == len(lines):
            print(f"{ERROR} Package '{pkg}' not found in config.")
            continue
        with open(PACKAGES, "w") as f:
            f.writelines(new_lines)
        print(f"{SUCCESS} Removed {pkg}")
    rebuild_prompt()
                        this one’s just the inverse of install_pkgs().
                        it filters out any line that matches the package name, rewrites the file, and calls rebuild
                        again.
                    
list_pkgs()
def list_pkgs():
    print(f"{INFO} Packages listed in {PACKAGES}:")
    with open(PACKAGES, "r") as f:
        lines = f.readlines()
    pkgs = [
        line.strip()
        for line in lines
        if line.strip()
        and not line.strip().startswith("#")
        and "[" not in line and "]" not in line
    ]
    if not pkgs:
        print(f"{INFO} No packages found.")
    else:
        for pkg in pkgs:
            print(f"  {pkg}")
    print(f"\\n{INFO} Total packages: {len(pkgs)}")
lists everything currently in your nix config. it ignores comments, brackets, and blank lines - basically parsing only the package names. the total count at the end is just a nice little touch.
search_pkgs()
def search_pkgs(*pkgs):
    for pkg in pkgs:
        if check_pkg_exists(pkg):
            print(f"{INFO} '{pkg}' exists in nixpkgs.")
        else:
            print(f"{ERROR} '{pkg}' not found in nixpkgs.")
                        this uses check_pkg_exists() to quickly verify if a package exists upstream in
                        nixpkgs.
                        handy for double-checking before adding something.
                    
is_installed()
def is_installed(*pkgs):
    with open(PACKAGES, "r") as f:
        lines = f.readlines()
    for pkg in pkgs:
        if any(re.search(rf"\\b{pkg}\\b", line) for line in lines):
            print(f"{SUCCESS} Yes. '{pkg}' exists in {PACKAGES}")
        else:
            print(f"{ERROR} Could not find '{pkg}' in {PACKAGES}")
                        The final utility function.
                        checks if a given package is already listed in your local config file.
                        unlike search_pkgs(), it doesn’t call nix - it just reads your own file.
                        this helps you verify whether something is locally declared.
                    
the main function
def main():
    if len(sys.argv) < 2:
        help()
        sys.exit(0)
    subcommand = sys.argv[1]
    args = sys.argv[2:]
    global PACKAGES, REBUILD
    PACKAGES, REBUILD = setup_config()
    # check for sudo in syd install and remove since they need elevated perms
    if subcommand in ["install", "remove",]:
        if os.geteuid() != 0:
            print(f"{ERROR} Root permissions required to modify {PACKAGES}")
            print(f"{INFO} Try: sudo syd {subcommand} <package>")
            sys.exit(1)
    # syd install
    if subcommand == "install":
        PACKAGES, REBUILD = setup_config()
        if len(args) == 0:
            print(f"{INFO} Usage: syd install <package>")
            sys.exit(1)
        install_pkgs(*args)
    # syd remove
    elif subcommand == "remove":
        PACKAGES, REBUILD = setup_config()
        if len(args) == 0:
            print(f"{INFO} Usage: syd remove <package>")
            sys.exit(1)
        remove_pkgs(*args)
    # syd search
    elif subcommand == "search":
        PACKAGES, REBUILD = setup_config()
        if len(args) == 0:
            print(f"{INFO} Usage: syd search <package>")
            sys.exit(1)
        search_pkgs(*args)
    # syd isinstalled
    elif subcommand == "isinstalled":
        if len(args) == 0:
            print(f"{ERROR} Usage: syd isinstalled <package>")
            sys.exit(1)
        is_installed(*args)
    # syd list
    elif subcommand == "list":
        if len(args) != 0:
            print(f"{INFO} Usage: syd list")
            sys.exit(1)
        PACKAGES, REBUILD = setup_config()
        list_pkgs()
    # syd --reset
    elif subcommand == "--reset":
        if len(args) != 0:
            print(f"{INFO} Usage: syd --reset")
            sys.exit(1)
        reset_config()
    # syd --help
    elif subcommand in ["--help", "-h", ""]:
        if len(args) != 0:
            print(f"{INFO} Usage: syd --help")
            sys.exit(1)
        help()
    else:
        print(f"{INFO} Unknown command: {subcommand}")
        print("Run 'syd --help' for usage.")
        sys.exit(1)
if __name__ == "__main__":
    main() # call the main function
This is the function that ties syd together. it’s the part that listens to your commands, figures out what you meant, and then calls the right function to do it.
Argument parsing
                        The first two lines check if you actually passed any arguments.
                        if not, help() runs and the script exits.
                    
                        Then it splits things up: subcommand holds whatever command you entered (like
                        install or list),
                        and args holds everything that comes after - the package names or flags.
                    
Loading the config
                        It calls setup_config() early on to make sure syd knows where your
                        packages.nix file and rebuild command live.
                        this ensures that every command  knows
                        the right context to operate in.
                    
Handling sudo
                        since install and remove modify system files (usually in
                        /etc/nixos),
                        syd checks if it’s running as root.
                        if not, it prints an error and reminds you to re-run the command with sudo.
                        this prevents permission errors before they happen.
                    
Command routing
                        After that, it’s basically a command router - a series of if / elif
                        blocks, each one handling a different syd command
                    
Each section includes basic argument validation - if you forget to type the package name, syd doesn’t crash; it just prints the correct usage format and exits.
Error handling & exit behavior
                        For any unknown or invalid command, syd gives a clean error message and a pointer to
                        syd --help.
                    
The __main__ block
The final two lines are python’s way of saying “run this if it’s the main program.” this makes sure syd only executes when directly called (not when imported as a module). it’s the standard idiom:
if __name__ == "__main__":
    main()
End
                        And that's syd! Other documentation can be found on it's github repository: https://github.com/sidharthify/syd 
                        
                        
                        I made it out of boredom and curiosity just randomly - and it turned out to be quite useful!