diff --git a/modules/configuration.nix b/modules/configuration.nix index 5c3c52b..fd4bf4b 100644 --- a/modules/configuration.nix +++ b/modules/configuration.nix @@ -221,6 +221,12 @@ programs = { steam.enable = true; + direnv = { + enable = true; + silent = true; # Suppresses direnv's output in the terminal + nix-direnv.enable = true; + }; + ssh.askPassword = lib.mkForce "${pkgs.kdePackages.ksshaskpass}/bin/ksshaskpass"; #In order for dynamically linked executables to work diff --git a/modules/home.nix b/modules/home.nix index 045432a..d51cc02 100644 --- a/modules/home.nix +++ b/modules/home.nix @@ -24,6 +24,7 @@ ".config/gtk-4.0/settings.ini.backup" ".config/niri/config.kdl" ]; + devTemplate = ../other/dev-template.nix; }) ]; @@ -54,6 +55,12 @@ #]; }; + direnv = { + enable = true; + enableZshIntegration = true; + nix-direnv.enable = true; + }; + kitty = { enable = true; settings = { diff --git a/modules/ncli.nix b/modules/ncli.nix old mode 100644 new mode 100755 index 31ccfff..854f25d --- a/modules/ncli.nix +++ b/modules/ncli.nix @@ -15,7 +15,7 @@ in PROJECT="${project}" HOST="${host}" BACKUP_FILES_STR="${backupFilesString}" - VERSION="2.0.0" + VERSION="2.1.3" FLAKE_NIX_PATH="$HOME/$PROJECT/flake.nix" read -r -a BACKUP_FILES <<< "$BACKUP_FILES_STR" @@ -45,6 +45,11 @@ in echo " pull - Pull latest changes from origin." echo " status - Show git status." echo "" + echo "Development Commands:" + echo " dev - Initialize a Nix development environment (flake.nix + direnv)." + echo " dev track - Remove assume-unchanged flag from flake files." + echo " dev untrack - Mark flake files as assume-unchanged in a directory." + echo "" echo " help - Show this help message." echo "" } @@ -65,6 +70,27 @@ in done } + # --- Dev Init Helper Functions --- + print_header() { + echo "" + echo -e "''${BLUE}==============================================''${NOCOLOR}" + echo -e "''${BLUE} Nix Flake Development Environment Initializer''${NOCOLOR}" + echo -e "''${BLUE}==============================================''${NOCOLOR}" + echo "" + } + + print_success() { + echo -e "''${GREEN}[OK]''${NOCOLOR} $1" + } + + print_error() { + echo -e "''${RED}[ERR]''${NOCOLOR} $1" + } + + print_info() { + echo -e "''${YELLOW}[->]''${NOCOLOR} $1" + } + # --- Main Logic --- if [ "$#" -eq 0 ]; then echo "Error: No command provided." >&2 @@ -327,6 +353,376 @@ in echo "Trim operation cancelled." fi ;; + dev) + # Only flake files need assume-unchanged (Nix requires them in the index). + # .envrc and .direnv are handled via .git/info/exclude instead. + DEV_FILES=(flake.nix flake.lock) + DIRENV_LOCAL_FILES=(.envrc .direnv) + + _dev_resolve_dir() { + read -rp " Enter target directory [./]: " TARGET_DIR + TARGET_DIR="''${TARGET_DIR:-./}" + if [[ ! -d "$TARGET_DIR" ]]; then + print_error "Directory '$TARGET_DIR' does not exist." + exit 1 + fi + TARGET_ABS="$(cd "$TARGET_DIR" && pwd)" + if ! git -C "$TARGET_ABS" rev-parse --git-dir > /dev/null 2>&1; then + print_error "No Git repository found at '$TARGET_ABS'." + exit 1 + fi + } + + # Add .envrc and .direnv to .git/info/exclude (local-only, never committed). + _add_local_excludes() { + local repo_abs="$1" + local git_dir exclude_file + git_dir="$(git -C "$repo_abs" rev-parse --git-dir)" + exclude_file="$git_dir/info/exclude" + mkdir -p "$(dirname "$exclude_file")" + touch "$exclude_file" + + for entry in "''${DIRENV_LOCAL_FILES[@]}"; do + if ! grep -qxF "$entry" "$exclude_file" 2>/dev/null; then + echo "$entry" >> "$exclude_file" + print_success "$entry → ignored via .git/info/exclude (local only)" + else + print_info "$entry already in .git/info/exclude, skipping." + fi + done + } + + _remove_local_excludes() { + local repo_abs="$1" + local git_dir exclude_file tmp_file + git_dir="$(git -C "$repo_abs" rev-parse --git-dir)" + exclude_file="$git_dir/info/exclude" + + [[ -f "$exclude_file" ]] || return 0 + + tmp_file="$(mktemp)" + cp "$exclude_file" "$tmp_file" + + for entry in "''${DIRENV_LOCAL_FILES[@]}"; do + if grep -qxF "$entry" "$tmp_file" 2>/dev/null; then + grep -vxF "$entry" "$tmp_file" > "''${tmp_file}.new" || true + mv "''${tmp_file}.new" "$tmp_file" + print_success "$entry → removed from .git/info/exclude" + else + print_info "$entry not present in .git/info/exclude, skipping." + fi + done + + mv "$tmp_file" "$exclude_file" + } + + case "''${2:-init}" in + + untrack) + echo "" + echo -e "''${BLUE}--- Dev: Track Files (assume-unchanged) ---''${NOCOLOR}" + echo "" + _dev_resolve_dir + + acted=false + for f in "''${DEV_FILES[@]}"; do + full="$TARGET_ABS/$f" + if [[ ! -f "$full" ]]; then + print_info "$f not found in $TARGET_ABS — skipping." + continue + fi + if ! git -C "$TARGET_ABS" ls-files --error-unmatch "$f" > /dev/null 2>&1; then + git -C "$TARGET_ABS" add --intent-to-add "$f" + fi + git -C "$TARGET_ABS" update-index --assume-unchanged "$f" + print_success "$f → untracked (assume-unchanged)" + acted=true + done + + _add_local_excludes "$TARGET_ABS" + + if ! $acted; then + print_info "No dev files found in '$TARGET_ABS' to track." + fi + ;; + + track) + echo "" + echo -e "''${BLUE}--- Dev: Untrack Files (remove assume-unchanged) ---''${NOCOLOR}" + echo "" + _dev_resolve_dir + + acted=false + for f in "''${DEV_FILES[@]}"; do + if git -C "$TARGET_ABS" ls-files -v "$f" 2>/dev/null | grep -q "^h "; then + git -C "$TARGET_ABS" update-index --no-assume-unchanged "$f" + print_success "$f → tracked (assume-unchanged removed)" + acted=true + else + print_info "$f is not marked assume-unchanged — skipping." + fi + done + + _remove_local_excludes "$TARGET_ABS" + + if ! $acted; then + print_info "No files in '$TARGET_ABS' had the assume-unchanged bit set." + fi + ;; + + init|*) + # ---- Defaults ----- + SOURCE_FLAKE="$HOME/$PROJECT/other/dev-template.nix" + + # ---- Check source flake exists ------------------------------ + if [[ ! -f "$SOURCE_FLAKE" ]]; then + print_error "Source flake template not found: $SOURCE_FLAKE" + echo "Please ensure dev-template.nix exists in $HOME/$PROJECT/other/" + exit 1 + fi + + print_header + + # ---- Step 1: Target Directory ----------------------------------- + echo -e "''${YELLOW}Step 1/5: Target Directory''${NOCOLOR}" + echo "" + read -p " Enter target directory [./]: " TARGET_DIR + + if [[ -z "$TARGET_DIR" ]]; then + TARGET_DIR="./" + fi + + if [[ ! -d "$TARGET_DIR" ]]; then + print_error "Directory '$TARGET_DIR' does not exist. Exiting." + exit 1 + fi + + print_success "Target directory: $TARGET_DIR" + echo "" + + # ---- Check for existing flake ---------------------------------- + if [[ -f "$TARGET_DIR/flake.nix" ]]; then + echo "" + print_error "A flake.nix already exists in '$TARGET_DIR'!" + echo "" + read -p " Would you like to just place the .envrc instead? [y/N]: " ENVRC_ONLY + ENVRC_ONLY="''${ENVRC_ONLY:-N}" + + if [[ "$ENVRC_ONLY" =~ ^[Yy]$ ]]; then + TARGET_ABS="$(cd "$TARGET_DIR" && pwd)" + echo "" + print_info "Placing .envrc only..." + if ! grep -qxF "use flake" "$TARGET_ABS/.envrc" 2>/dev/null; then + echo "use flake" >> "$TARGET_ABS/.envrc" + print_success ".envrc created at $TARGET_ABS/" + else + print_info ".envrc already contains 'use flake', nothing to do." + fi + + echo "" + print_info "Next steps:" + echo " cd $TARGET_DIR" + echo " direnv allow" + echo "" + exit 0 + else + print_info "Aborting. No files were changed." + exit 1 + fi + fi + + # ---- Step 2: Project Name --------------------------------------- + echo -e "''${YELLOW}Step 2/5: Project Name''${NOCOLOR}" + echo "" + read -p " Enter development environment name [devShell]: " PROJECT_NAME + + if [[ -z "$PROJECT_NAME" ]]; then + PROJECT_NAME="devShell" + fi + + print_success "Project name: $PROJECT_NAME" + echo "" + + # ---- Step 3: Select template ---------------------------------------- + echo -e "''${YELLOW}Step 3/5: Select Template''${NOCOLOR}" + echo "" + echo " 1) Empty - Basic shell, add packages yourself" + echo " 2) Python - Python 3, pip, venv setup" + echo " 3) Node.js - Node.js, npm" + echo " 4) Rust - Rustc, cargo, rust-analyzer" + echo " 5) Go - Go, gopls, golangci-lint" + echo " 6) C/C++ - GCC, CMake, GDB, pkg-config" + echo " 7) Java - JDK 21, Maven" + echo "" + + read -p " Select template [1-7] (default: 1): " TEMPLATE_CHOICE + + case "$TEMPLATE_CHOICE" in + 2) + TEMPLATE="python3" + PACKAGES="python3 python3Packages.pip python3Packages.virtualenv" + BUILD_INPUTS="" + SHELL_HOOK_EXTRA=" + # Create venv if it doesn't exist + if [ ! -d .venv ]; then + echo 'Creating Python virtual environment...' + python3 -m venv .venv + fi + + # Activate venv + source .venv/bin/activate + + echo 'Python venv activated.' + " + ;; + 3) + TEMPLATE="node" + PACKAGES="nodejs_22" + BUILD_INPUTS="" + SHELL_HOOK_EXTRA='echo "Node.js $(node --version) ready."' + ;; + 4) + TEMPLATE="rust" + PACKAGES="rustc cargo rust-analyzer" + BUILD_INPUTS="openssl pkg-config" + SHELL_HOOK_EXTRA='echo "Rust $(rustc --version) ready."' + ;; + 5) + TEMPLATE="go" + PACKAGES="go gopls golangci-lint" + BUILD_INPUTS="" + SHELL_HOOK_EXTRA=' + export GOPATH="$PWD/.gopath" + export PATH="$GOPATH/bin:$PATH" + mkdir -p "$GOPATH" + echo "Go $(go version) ready. GOPATH: $GOPATH" + ' + ;; + 6) + TEMPLATE="cpp" + PACKAGES="gcc gdb cmake gnumake" + BUILD_INPUTS="pkg-config" + SHELL_HOOK_EXTRA='echo "GCC $(gcc --version | head -1) ready."' + ;; + 7) + TEMPLATE="java" + PACKAGES="jdk21 maven" + BUILD_INPUTS="" + SHELL_HOOK_EXTRA='echo "Java $(java --version | head -1) ready."' + ;; + *) + TEMPLATE="empty" + PACKAGES="" + BUILD_INPUTS="" + SHELL_HOOK_EXTRA="" + ;; + esac + + print_success "Selected template: $TEMPLATE" + echo "" + + # ---- Step 4: Copy and modify flake ---------------------------------- + echo -e "''${YELLOW}Step 4/5: Generating flake.nix''${NOCOLOR}" + echo "" + + cp "$SOURCE_FLAKE" "$TARGET_DIR/flake.nix" + print_success "Copied flake.nix to $TARGET_DIR/" + + sed -i "s|name = \"replaceNameHere\";|name = \"''${PROJECT_NAME}\";|" "$TARGET_DIR/flake.nix" + print_success "Set project name: $PROJECT_NAME" + + if [[ -n "$PACKAGES" ]]; then + sed -i "s|# replacePackagesHere|$PACKAGES|" "$TARGET_DIR/flake.nix" + print_success "Added packages: $PACKAGES" + fi + + if [[ -n "$BUILD_INPUTS" ]]; then + BUILD_INPUTS_LINE="buildInputs = with pkgs; [ $BUILD_INPUTS ];" + sed -i "s|# replaceBuildInputsHere|$BUILD_INPUTS_LINE|" "$TARGET_DIR/flake.nix" + print_success "Added build inputs: $BUILD_INPUTS" + fi + + if [[ -n "$SHELL_HOOK_EXTRA" ]]; then + awk -v hook="$SHELL_HOOK_EXTRA" '/# replaceShellHookHere/ { print hook; next } { print }' \ + "$TARGET_DIR/flake.nix" > "$TARGET_DIR/flake.nix.tmp" \ + && mv "$TARGET_DIR/flake.nix.tmp" "$TARGET_DIR/flake.nix" + print_success "Added template-specific shell hook" + fi + + # ---- Step 5: Git Integration & .envrc -------------------------------- + echo -e "''${YELLOW}Step 5/5: Git Integration''${NOCOLOR}" + echo "" + + TARGET_ABS="$(cd "$TARGET_DIR" && pwd)" + + echo "Creating .envrc for direnv to automatically work!" + if ! grep -qxF "use flake" "$TARGET_ABS/.envrc" 2>/dev/null; then + echo "use flake" >> "$TARGET_ABS/.envrc" + print_success ".envrc created at $TARGET_ABS/" + else + print_info ".envrc already contains 'use flake', skipping." + fi + + if git -C "$TARGET_ABS" rev-parse --git-dir > /dev/null 2>&1; then + echo " A Git repository was detected at:" + echo " $(git -C "$TARGET_ABS" rev-parse --show-toplevel)" + echo "" + + # Always add .envrc and .direnv to local exclude — no prompt needed, + # these should never be committed regardless. + _add_local_excludes "$TARGET_ABS" + echo "" + + echo " Nix flakes require flake.nix (and flake.lock) to be Git-tracked." + echo " This script can stage them as 'assume-unchanged' so Nix sees them," + echo " but they will NEVER be committed or show up in git status." + echo "" + read -p " Set up flake files as tracked-but-invisible to Git? [y/N]: " GIT_CHOICE + + GIT_CHOICE="''${GIT_CHOICE:-N}" + + if [[ "$GIT_CHOICE" =~ ^[Yy]$ ]]; then + cat > "$TARGET_ABS/flake.lock" << 'EOF' + { + "nodes": { + "root": {} + }, + "root": "root", + "version": 7 + } + EOF + print_success "flake.lock ready (Nix will populate it on first run if empty)" + + git -C "$TARGET_ABS" add --intent-to-add flake.nix flake.lock + git -C "$TARGET_ABS" update-index --assume-unchanged flake.nix flake.lock + print_success "flake.nix + flake.lock → tracked (assume-unchanged)" + else + print_info "Skipped. You can do this manually later:" + echo "" + echo " git -C \"$TARGET_ABS\" add --intent-to-add flake.nix flake.lock" + echo " git -C \"$TARGET_ABS\" update-index --assume-unchanged flake.nix flake.lock" + echo "" + fi + else + print_info "No Git repository detected — skipping Git integration." + echo "" + fi + + # ---- Done --------------------------------------------------- + echo -e "''${GREEN}==============================================''${NOCOLOR}" + echo -e "''${GREEN} Done! Your flake is ready at:''${NOCOLOR}" + echo -e "''${GREEN} $TARGET_DIR/flake.nix''${NOCOLOR}" + echo -e "''${GREEN}==============================================''${NOCOLOR}" + echo "" + print_info "Next steps:" + echo " cd $TARGET_DIR" + echo " direnv allow # Trust the .envrc so direnv auto-activates" + echo " nix develop # Or just: cd out and back in" + echo "" + ;; + + esac + ;; *) echo "Error: Invalid command '$1'" >&2 print_help diff --git a/other/dev-template.nix b/other/dev-template.nix new file mode 100644 index 0000000..f33dfd5 --- /dev/null +++ b/other/dev-template.nix @@ -0,0 +1,75 @@ +{ + description = "A reproducible development environment"; + + # ───────────────────────────────────────────── + # Inputs – external flakes this flake depends on + # ───────────────────────────────────────────── + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + # flake-utils generates per-system outputs so you don't + # have to repeat yourself for every architecture. + flake-utils.url = "github:numtide/flake-utils"; + }; + + # ───────────────────────────────────────────── + # Outputs – everything this flake exposes + # ───────────────────────────────────────────── + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + # Allow non-free packages + config.allowUnfree = true; + }; + in + { + + # ───────────────────────────────────────── + # `nix develop` drops you into this shell + # ───────────────────────────────────────── + devShells.default = pkgs.mkShell { + + # Human-readable name shown in the shell prompt + name = "replaceNameHere"; + + # ── Runtime packages available inside the shell ────────────── + # Add or remove anything from https://search.nixos.org/packages + packages = with pkgs; [ + # Version control + git + + # Common utilities + curl + wget + bat + + # ── Language toolchains ────────────────────────────────── + # replacePackagesHere + ]; + + # ── Build inputs (headers, libraries needed at compile time) ── + # Use this for native C libraries, e.g.: + # buildInputs = with pkgs; [ openssl zlib pkg-config ]; + + # replaceBuildInputsHere + + # ── Shell hook – runs every time you enter the shell ────────── + shellHook = '' + echo "" + echo " 🐚 Development shell ready!" + echo " 📦 nixpkgs: ${nixpkgs.rev or "unknown"}" + echo "" + + # replaceShellHookHere + ''; + + # ── Environment variables always present in the shell ───────── + + # Allow broken or insecure packages + # NIX_CONFIG = "allow-broken = true"; + }; + } + ); +}