diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..c6422f2 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,48 @@ +name: Nix Flake Checks + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +# Cancel previous runs on the same branch/PR when new commits are pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Validate Flake + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + + - name: Show flake info + run: nix flake show + + - name: Check flake formatting + run: nix flake check --show-trace + + - name: Build default package + run: nix build .#default --print-build-logs + + - name: Build machine bundle + run: nix build .#aleksandars-mbp --print-build-logs + + - name: Verify passthru.unwrapped + run: | + echo "Testing passthru.unwrapped pattern..." + nix eval .#git.version + nix eval .#tmux.version + nix eval .#starship.version + echo "All passthru checks passed!" diff --git a/CLAUDE.md b/CLAUDE.md index e979495..5380dc7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,87 +4,164 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository Overview -This is a Nix dotfiles repository managed with home-manager and fleek. It configures a macOS development environment for user `rastasheep` on the `aleksandars-mbp` host. +This is a modular Nix flake-based dotfiles repository for macOS. It provides portable, reproducible configurations for development tools and applications. The repository uses Nix flakes with multi-system support and follows modern Nix best practices. ## Core Commands ### Apply Configuration Changes ```bash -# Apply dotfiles configuration (defined as alias) -apply-dot +# Update dotfiles installation +nix profile upgrade ".*aleksandars-mbp.*" -# Manual application (what the alias runs) -cd ~/src/github.com/rastasheep/dotfiles && nix run --impure home-manager/master -- -b bak switch --flake .#rastasheep@aleksandars-mbp +# Or rebuild from source +cd ~/src/github.com/rastasheep/dotfiles +nix profile install .#aleksandars-mbp --priority 5 ``` ### Update and Upgrade ```bash -# Update flake inputs (nixpkgs, home-manager) and apply configuration -update-dot +# Update flake inputs (nixpkgs, flake-utils) +nix flake update -# Upgrade Nix installation itself (less frequent) -upgrade-nix +# Upgrade specific input +nix flake lock --update-input nixpkgs ``` -### Development Navigation +### Validation and Testing ```bash -# Navigate to dotfiles directory (uses the 'dev' script) -dev dotfiles +# Run all checks +nix flake check + +# Show flake structure +nix flake show + +# Build specific package +nix build .#git + +# Test package without installing +nix run .#nvim + +# Access unwrapped package +nix run .#git.unwrapped status + +# Check version +nix eval .#git.version ``` ## Architecture -### Configuration Structure -- `flake.nix` - Main flake definition with inputs and home-manager configuration -- `home.nix` - Base configuration with global packages and shell aliases -- `aleksandars-mbp/rastasheep.nix` - Host-specific configuration including: - - User-specific packages - - Shell aliases and functions - - macOS system defaults - - Git configuration - - tmux configuration - - Zsh configuration with custom prompt - - Neovim configuration with plugins - -### Custom Packages -- `pkgs/blender.nix` - Custom Blender package for macOS ARM64 -- `pkgs/kicad.nix` - Custom KiCad package -- Both are defined as overlays in `flake.nix` - -### Scripts and Utilities -The `aleksandars-mbp/bin/` directory contains custom shell scripts that are added to PATH: -- Git utilities (git-rank-contributors, git-recent, git-wtf) -- Development tools (dev, gh-pr, gh-url, mdc, notes) -- System utilities (extract, headers) +### Repository Structure +``` +. +├── flake.nix # Main flake with multi-system support +├── flake.lock # Locked dependency versions +├── lib/ # Shared utilities +│ └── default.nix # Reusable functions (wrapWithConfig, buildConfig, etc.) +├── packages/ # Individual tool packages +│ ├── git/ # Git with custom config +│ ├── tmux/ # Tmux with custom config +│ ├── nvim/ # Neovim with plugins +│ ├── zsh/ # Zsh with plugins +│ ├── starship/ # Starship prompt +│ ├── scripts/ # Custom shell scripts +│ ├── hammerspoon/ # Window management +│ ├── ghostty/ # Terminal emulator +│ ├── claude-code/ # Claude Code with 1Password integration +│ ├── macos-defaults/ # Declarative macOS system defaults +│ ├── dircolors/ # GNU dircolors configuration +│ └── ... +└── machines/ # Machine-specific bundles + └── aleksandars-mbp/ # Composes all packages for this machine +``` + +### Shared Library (lib/default.nix) +All packages use shared utilities to ensure consistency: +- `wrapWithConfig`: Standard CLI wrapper with env var config +- `buildConfig`: Config directory builder (installs to /share/{name}) +- `smartConfigLink`: Backup and symlink logic for existing configs +- `mkMeta`: Standardized meta attributes template + +### Package Patterns +All packages follow consistent patterns: +- Use `inherit (pkgs) lib;` instead of `with pkgs.lib;` +- Include complete meta attributes (description, homepage, license, platforms) +- Expose unwrapped package via `passthru.unwrapped` +- Use `stdenvNoCC` for pure config packages (no compilation) +- Use shared library utilities where applicable + +### Multi-System Support +The flake supports multiple systems via flake-utils: +- `aarch64-darwin` (Apple Silicon Macs) +- `x86_64-darwin` (Intel Macs) +- `aarch64-linux` (ARM Linux) +- `x86_64-linux` (x86 Linux) ## Configuration Management -### Making Changes -1. Edit configuration files (primarily in `aleksandars-mbp/rastasheep.nix`) -2. Run `apply-dot` to apply changes -3. Configuration creates backups with `-b bak` flag - -### Host Configuration -The configuration is specific to `rastasheep@aleksandars-mbp`. To adapt for different hosts: -- Create new directory under project root (e.g., `new-hostname/`) -- Add new homeConfiguration in `flake.nix` -- Reference the new configuration module - -### Package Management -- Global packages defined in `home.nix` -- Host-specific packages in `aleksandars-mbp/rastasheep.nix` -- Custom packages via overlays in `flake.nix` - -## Key Programs Configured -- **Shell**: Zsh with custom prompt, autosuggestions, and extensive aliases -- **Editor**: Neovim with treesitter, LSP, completion, and custom plugins -- **Terminal Multiplexer**: tmux with vi key bindings and custom configuration -- **Version Control**: Git with extensive aliases and LFS support -- **macOS Defaults**: Comprehensive system preferences including Dock, Finder, and keyboard settings - -## Development Environment Features -- Direnv integration for per-project environments -- FZF for fuzzy finding -- Custom git prompt with status indicators -- Development-focused shell aliases and functions -- Claude Code integration (available as package) \ No newline at end of file +### Adding a New Package +1. Create package directory in `packages/` +2. Use shared library utilities from `lib/default.nix` +3. Follow package patterns (inherit lib, complete meta, passthru) +4. Add package import to `flake.nix` let block +5. Export in `packages` output +6. Add to machine bundle in `machines/aleksandars-mbp/default.nix` +7. Run `nix flake check` to verify + +### Creating a New Machine Bundle +1. Create directory in `machines/` (e.g., `machines/new-machine/`) +2. Create `default.nix` that imports and composes packages +3. Add to `flake.nix` packages output: + ```nix + new-machine = import ./machines/new-machine { inherit pkgs claudePkgs; }; + ``` +4. Install with: `nix profile install .#new-machine` + +### Modifying Existing Packages +1. Edit package in `packages/{name}/default.nix` +2. Run `nix build .#{name} --dry-run` to verify +3. Test with `nix run .#{name}` +4. Run `nix flake check` before committing + +## Key Packages Configured + +### CLI Tools +- **git**: Custom config with rebasing, pruning, helpful aliases, LFS support +- **tmux**: Vi key bindings, custom prefix, status bar configuration +- **zsh**: Autosuggestions, completions, custom sourcing +- **starship**: Minimal prompt configuration +- **nvim**: Treesitter, LSP, completion, fzf-lua, gitsigns, Flexoki theme +- **scripts**: Custom shell scripts (dev, git-*, gh-*, etc.) +- **dircolors**: GNU dircolors configuration for colorized ls output + +### GUI Applications +- **hammerspoon**: Window management with Leaderflow modal keybindings +- **ghostty**: Terminal emulator with Flexoki theme +- **claude-code**: Claude Code with 1Password integration for secrets +- **macos-defaults**: Declarative macOS system defaults management + +### Machine Bundle Includes +The aleksandars-mbp bundle also includes upstream packages: +- Core utilities: coreutils, ripgrep, openssl, tree, wget +- Development: direnv, fzf, docker, openvpn, 1password-cli +- GUI apps: slack, raycast + +## Best Practices + +### Nix Code Style +- Use `inherit (pkgs) lib;` instead of `with pkgs.lib;` +- Add complete meta attributes to all packages +- Use `stdenvNoCC` for pure config packages +- Add `passthru.unwrapped` to wrapped packages +- Use shared library utilities where applicable + +### Package Development +- Test with `nix build .#{name} --dry-run` before committing +- Run `nix flake check` to validate all packages build +- Use `nix run .#{name}` to test package functionality +- Check `nix flake show` to verify package exports + +### Commit Messages +- Use conventional commit format (feat:, fix:, refactor:, docs:) +- Include what changed and why +- Reference specific files when relevant +- Add co-author attribution for Claude Code contributions diff --git a/README.md b/README.md index f430116..3b70a21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Dotfiles -Nix-based dotfiles for macOS using home-manager and flakes. +Portable Nix-based dotfiles for macOS with individual tool wrappers. ## Features @@ -10,67 +10,170 @@ Nix-based dotfiles for macOS using home-manager and flakes. - **Window Management**: [Hammerspoon](https://www.hammerspoon.org) with Leaderflow modal keybindings - **Development**: Claude Code, direnv, fzf, ripgrep - **Git**: Modern config with rebasing, pruning, and helpful aliases +- **Portable**: Run tools on any Nix-enabled machine without installation ## Quick Start +### Option 1: Run Individual Tools (No Installation) ```bash -# Apply configuration -apply-dot +# Run neovim with your config +nix run github:rastasheep/dotfiles#nvim -# Update and apply -update-dot +# Run tmux, git, or any other tool +nix run github:rastasheep/dotfiles#tmux +nix run github:rastasheep/dotfiles#git status -# Navigate to dotfiles -dev dotfiles +# Run from local checkout +cd ~/src/github.com/rastasheep/dotfiles +nix run .#nvim +``` + +### Option 2: Install Everything +```bash +# Install all tools to your profile +nix profile install github:rastasheep/dotfiles + +# Or from local checkout +cd ~/src/github.com/rastasheep/dotfiles +nix profile install .#default + +# Update later +nix profile upgrade '.*dotfiles.*' +``` + +### Option 3: Install Individual Tools +```bash +# Cherry-pick the tools you want +nix profile install github:rastasheep/dotfiles#{nvim,git,tmux} +``` + +### Option 4: Machine-Specific Bundle +```bash +# Install complete machine-specific bundle +cd ~/src/github.com/rastasheep/dotfiles +nix profile install .#aleksandars-mbp + +# Update later +apply-dot # or: nix profile upgrade ".*aleksandars-mbp.*" ``` ## Structure ``` . -├── flake.nix # Flake definition with inputs -├── home.nix # Global packages and settings -└── aleksandars-mbp/ - ├── rastasheep.nix # Host-specific config (main file) - ├── vim.lua # Neovim configuration - ├── bin/ # Custom scripts (dev, git-*, etc.) - ├── claude/ # Claude Code settings and commands - ├── ghostty/ # Ghostty terminal config - └── hammerspoon/ # Hammerspoon window management +├── flake.nix # Flake definition with multi-system support +├── flake.lock # Locked dependency versions +├── lib/ # Shared utilities and helpers +│ └── default.nix # Reusable functions (wrapWithConfig, buildConfig, etc.) +├── packages/ # All tool and app packages (CLI + GUI) +│ ├── nvim/ # Neovim with plugins and config +│ ├── git/ # Git with custom config +│ ├── tmux/ # Tmux with custom config +│ ├── zsh/ # Zsh with plugins and config +│ ├── starship/ # Starship prompt config +│ ├── scripts/ # Custom scripts (dev, git-*, etc.) +│ ├── dircolors/ # GNU dircolors configuration +│ ├── hammerspoon/ # Hammerspoon app with config +│ ├── ghostty/ # Ghostty terminal with config +│ ├── claude-code/ # Claude with 1Password + config +│ ├── macos-defaults/ # macOS system defaults management +│ ├── blender/ # Custom Blender build (optional, commented out) +│ └── kicad/ # Custom KiCad build (optional, commented out) +└── machines/ # Machine-specific bundles + └── aleksandars-mbp/ # Composes tools + apps for this machine +``` + +## Available Packages + +All packages are exposed individually and can be run or installed standalone. + +### CLI Tools +- `nvim` - Neovim with plugins (treesitter, gitsigns, fzf-lua) and custom config +- `git` - Git with custom config and aliases +- `tmux` - Tmux with vi-mode and custom keybindings +- `zsh` - Zsh with plugins (autosuggestions, completions) and config +- `starship` - Starship prompt with minimal config +- `scripts` - Custom shell scripts (dev, git-*, gh-*, etc.) +- `dircolors` - GNU dircolors configuration + +### GUI Apps & Utilities +- `hammerspoon` - Hammerspoon with bundled Leaderflow config +- `ghostty` - Ghostty terminal with custom config +- `claude-code` - Claude Code with 1Password integration and custom settings +- `macos-defaults` - Declarative macOS system defaults management +- `blender` - Custom Blender build (optional, commented out) +- `kicad` - Custom KiCad build (optional, commented out) + +**Note:** Common tools like `fzf`, `direnv`, and `1password-cli` are included directly in the machine bundle without custom wrappers. + +### Machine Bundles +Pre-configured bundles for specific machines: + +- `aleksandars-mbp` - Complete setup with all CLI tools + GUI apps +- `default` - Core CLI tools only (no GUI apps) + +## Architecture & Features + +### Multi-System Support +The flake supports multiple systems out of the box: +- `aarch64-darwin` (Apple Silicon Macs) +- `x86_64-darwin` (Intel Macs) +- `aarch64-linux` (ARM Linux) +- `x86_64-linux` (x86 Linux) + +### Shared Library +All packages use shared utilities from `lib/default.nix`: +- `wrapWithConfig`: Standard CLI wrapper pattern +- `buildConfig`: Config directory builder +- `smartConfigLink`: Backup and symlink logic +- `mkMeta`: Standardized meta attributes + +This ensures consistency and reduces code duplication across all packages. + +### Package Passthru +All wrapped packages expose the underlying package via `passthru.unwrapped`: +```bash +# Run with your config +nix run .#git status + +# Run without your config (unwrapped) +nix run .#git.unwrapped status + +# Check version +nix eval .#git.version ``` -## Key Configurations +### Flake Checks +Validate packages build correctly: +```bash +# Run all checks +nix flake check -### Neovim -- Leader: `Space` -- LSP servers configured per-project via direnv -- Built-in completion (Neovim 0.11+) -- Treesitter for syntax highlighting -- fzf-lua for fuzzy finding +# View available checks +nix flake show | grep checks -### Git Aliases -- `g st` - Status -- `g ci` - Commit -- `g co` - Checkout -- `g lg` - Pretty log -- `g up` - Pull with rebase -- See more in `aleksandars-mbp/rastasheep.nix` +# Run specific check +nix build .#checks.aarch64-darwin.all-packages +``` -### Shell Aliases -- `e` - vim -- `g` - git -- `ll` - ls -lah -- `..` - cd .. -- `dev ` - Navigate to project or clone it +## Development -## Requirements +### Adding a New Package +1. Create package directory in `packages/` +2. Use shared library utilities from `lib/default.nix` +3. Add package to `flake.nix` imports +4. Add to machine bundle in `machines/aleksandars-mbp/default.nix` +5. Run `nix flake check` to verify -- Nix with flakes enabled -- macOS (Darwin) -- 1Password CLI (for Claude Code integration) +### Best Practices +- Use `inherit (pkgs) lib;` instead of `with pkgs.lib;` +- Add complete meta attributes (description, homepage, license, platforms) +- Add `passthru.unwrapped` for wrapped packages +- Use shared library utilities where applicable +- Use `stdenvNoCC` for pure config packages ## References -- [home-manager](https://nix-community.github.io/home-manager/) -- [home-manager options](https://nix-community.github.io/home-manager/options.html) - [Nix flakes](https://nixos.wiki/wiki/Flakes) +- [Nix packages manual](https://nixos.org/manual/nixpkgs/stable/) +- [flake-utils](https://github.com/numtide/flake-utils) diff --git a/aleksandars-mbp/rastasheep.nix b/aleksandars-mbp/rastasheep.nix deleted file mode 100644 index eaf3a4a..0000000 --- a/aleksandars-mbp/rastasheep.nix +++ /dev/null @@ -1,449 +0,0 @@ -{ pkgs, misc, ... }: { - home.username = "rastasheep"; - home.homeDirectory = "/Users/rastasheep"; - - home.packages = [ - (pkgs.buildEnv { - name = "scripts"; - paths = [ ./bin ]; - extraPrefix = "/bin"; - }) - # pkgs.blender - # pkgs.kicad - pkgs.hammerspoon - pkgs.claude-code - pkgs._1password-cli - pkgs.ghostty-bin - ]; - - home.shellAliases = { - "apply-dot" = "cd ~/src/github.com/rastasheep/dotfiles && nix run --impure home-manager/master -- -b bak switch --flake .#rastasheep@aleksandars-mbp"; - "dev-vpn" = "sudo openvpn --config ~/Google\\ Drive/My\\ Drive/fhc-dev-vpn.ovpn"; - "dev" = "source dev"; - "git" = "function _git { [ $# -eq 0 ] && git st || git $* }; compdef _git=git; _git"; - }; - - targets.darwin.defaults = { - NSGlobalDomain = { - _HIHideMenuBar = 1; - # Set highlight color to green - AppleAccentColor = 3; - AppleHighlightColor = "0.752941 0.964706 0.678431 Green"; - # Finder: show all filename extensions - AppleShowAllExtensions = true; - # Set a really short delay until key repeat. - InitialKeyRepeat = 25; - # Set a really fast key repeat. - KeyRepeat = 2; - KeyRepeatDelay = "0.5"; - KeyRepeatEnabled = 1; - KeyRepeatInterval = "0.083333333"; - AppleEnableMouseSwipeNavigateWithScrolls = 1; - com.apple.mouse.scaling = 2; - # Expand save panel by default - NSNavPanelExpandedStateForSaveMode = true; - NSNavPanelExpandedStateForSaveMode2 = true; - # Menu bar: use dark menu bar and Dock - AppleAquaColorVariant = 1; - AppleInterfaceStyle = "Dark"; - }; - "com.apple.desktopservices" = { - # Don't write .DS_Store files outside macOS - DSDontWriteNetworkStores = true; - DSDontWriteUSBStores = true; - }; - "com.apple.menuextra.clock" = { - # Set clock format to 24 hour - Show24Hour = true; - # Show day of week in menu bar - ShowDayOfWeek = true; - # Show AM/PM indicator - ShowAMPM = true; - # Don't show seconds - ShowSeconds = false; - # Don't show date - ShowDate = 0; - # Don't flash date separators - FlashDateSeparators = false; - # Use digital clock (not analog) - IsAnalog = false; - }; - "com.apple.dock" = { - # Don't show recent applications in Dock - show-recents = false; - # Set the Dock orientation to left - orientation = "left"; - # Set the icon size of Dock items to 28 pixels - tilesize = 28; - # Automatically hide and show the Dock - autohide = true; - # Minimize windows into their application’s icon - minimize-to-application = true; - # Show Dock instantly on hover - autohide-delay = 0; - # Empty the dock - persistent-apps = []; - }; - "com.apple.finder" = { - # Always open everything in Finder's column view. This is important. - FXPreferredViewStyle = "clmv"; - # Disable the warning when changing a file extension - FXEnableExtensionChangeWarning = false; - # Set the Finder prefs for not showing a volumes on the Desktop. - ShowExternalHardDrivesOnDesktop = false; - ShowRemovableMediaOnDesktop = false; - # Automatically open a new Finder window when a volume is mounted - OpenWindowForNewRemovableDisk = true; - # Empty Trash securely by default - EmptyTrashSecurely = true; - # Set $HOME as the default location for new Finder windows - # For other paths, use `PfLo` and `file:///full/path/here/` - NewWindowTarget = "PfDe"; - NewWindowTargetPath = ''file://''${HOME}''; - # Show sidebar - ShowSidebar = true; - # Set sidebar width - SidebarWidth = 176; - # Show status bar - ShowStatusBar = false; - # Show path bar - ShowPathbar = false; - # Show tab view - ShowTabView = false; - # Show toolbar - ShowToolbar = true; - # Disable warning before removing from iCloud Drive - FXICloudDriveRemovalWarning = false; - # Keep folders on top when sorting by name - _FXSortFoldersFirst = true; - }; - "com.apple.TimeMachine" = { - DoNotOfferNewDisksForBackup = true; - }; - "com.apple.mouse" = { - # Trackpad: enable tap to click - tapBehavior = true; - }; - "com.apple.AppleMultitouchMouse" = { - # Set mouse to two-button mode - MouseButtonMode = "TwoButton"; - # Enable one finger double tap gesture - MouseOneFingerDoubleTapGesture = 1; - # Enable two finger horizontal swipe gesture - MouseTwoFingerHorizSwipeGesture = 1; - # Enable two finger double tap gesture (smart zoom) - MouseTwoFingerDoubleTapGesture = 3; - # Enable horizontal scroll - MouseHorizontalScroll = 1; - # Enable momentum scroll - MouseMomentumScroll = 1; - # Enable vertical scroll - MouseVerticalScroll = 1; - # Set mouse button division - MouseButtonDivision = 55; - # Enable user preferences - UserPreferences = 1; - # Set version - version = 1; - }; - "com.apple.driver.AppleBluetoothMultitouch.trackpad" = { - # Enable tap to click - Clicking = true; - # Disable drag lock - DragLock = false; - # Disable dragging - Dragging = false; - # Disable corner secondary click - TrackpadCornerSecondaryClick = false; - # Enable five finger pinch gesture - TrackpadFiveFingerPinchGesture = 2; - # Enable four finger horizontal swipe gesture - TrackpadFourFingerHorizSwipeGesture = 2; - # Enable four finger pinch gesture - TrackpadFourFingerPinchGesture = 2; - # Enable four finger vertical swipe gesture - TrackpadFourFingerVertSwipeGesture = 2; - # Enable hand resting - TrackpadHandResting = true; - # Enable horizontal scroll - TrackpadHorizScroll = true; - # Enable momentum scroll - TrackpadMomentumScroll = true; - # Enable pinch gesture - TrackpadPinch = true; - # Enable right click - TrackpadRightClick = true; - # Enable rotate gesture - TrackpadRotate = true; - # Enable scroll - TrackpadScroll = true; - # Disable three finger drag - TrackpadThreeFingerDrag = false; - # Enable three finger horizontal swipe gesture - TrackpadThreeFingerHorizSwipeGesture = 2; - # Disable three finger tap gesture - TrackpadThreeFingerTapGesture = false; - # Enable three finger vertical swipe gesture - TrackpadThreeFingerVertSwipeGesture = 2; - # Enable two finger double tap gesture - TrackpadTwoFingerDoubleTapGesture = 1; - # Enable two finger swipe from right edge - TrackpadTwoFingerFromRightEdgeSwipeGesture = 3; - # Don't stop trackpad when USB mouse is connected - USBMouseStopsTrackpad = false; - # Enable user preferences - UserPreferences = true; - # Set version - version = 5; - }; - }; - - programs.git = { - enable = true; - - lfs.enable = true; - ignores = ["*~" "*.swp" ".DS_Store"]; - - settings = { - user = { - name = "Aleksandar Diklic"; - email = "rastasheep3@gmail.com"; - }; - - feature.manyFiles = true; - init.defaultBranch = "main"; - push.autoSetupRemote = true; - pull.rebase = true; - fetch.prune = true; - diff.algorithm = "histogram"; - merge.conflictstyle = "zdiff3"; - rerere.enabled = true; - - alias = { - a = "add"; - all = "add -A"; - st = "status -sb"; - ci = "commit"; - ca = "commit --amend"; - br = "branch"; - co = "checkout"; - df = "diff"; - dfc = "diff --cached"; - lg = "log --pretty=format:'%C(yellow)%h%Creset %Cgreen(%><(12)%cr%><|(12))%Creset - %s %C(blue)<%an>%Creset'"; - pl = "pull"; - ps = "push"; - undo = "reset --soft HEAD^"; - count = "!git shortlog -sne"; - pr = "!f() { git fetch origin pull/$1/head:pr-$1 && git checkout pr-$1; }; f"; - up = "!f() { git pull --rebase --prune && git log --pretty=format:\"%Cred%ae %Creset- %C(yellow)%s %Creset(%ar)\" HEAD@{1}.. }; f"; - credit = "!f() { git commit --amend --author \"$1 <$2>\" -C HEAD; }; f"; - unpushed = "!f() { git diff origin/\"$(git rev-parse --abbrev-ref HEAD)\"..HEAD; }; f"; - delete-local-merged = "!git branch --merged | grep -v '^*' | grep -v 'master' | grep -v 'main' | xargs -r git branch -d"; - nuke = "!f() { git branch -D $1 && git push origin :$1; }; f"; - }; - }; - }; - - programs.tmux = { - enable = true; - - keyMode = "vi"; - prefix = "C-a"; - baseIndex = 1; - customPaneNavigationAndResize = true; - resizeAmount = 10; - aggressiveResize = true; - disableConfirmationPrompt = false; - escapeTime = 50; - historyLimit = 10000; - newSession = true; - shortcut = "a"; - terminal = "xterm-256color"; - mouse = true; - sensibleOnTop = false; - - extraConfig = '' - set -ga terminal-overrides ",*256col*:Tc" - bind-key C-a last-window - - # scroll stuff - set -g terminal-overrides 'xterm*:smcup@:rmcup@' - - # open new window in same dir - bind c new-window -c "#{pane_current_path}" - - # v and y like vi in copy-mode - bind-key -T copy-mode-vi 'v' send -X begin-selection - bind-key -T copy-mode-vi 'Y' send -X copy-pipe-and-cancel "reattach-to-user-namespace pbcopy" - - # p for paste - unbind p - bind p paste-buffer - - # enable wm window titles - set -g set-titles on - - # wm window title string (uses statusbar variables) - set -g set-titles-string "tmux.#I.#W" - - # disable auto renaming - setw -g automatic-rename off - - # statusbar - set -g display-time 2000 - set -g status-left ''' - set -g status-right "#( date +' %H:%M ')" - - # split pane hotkeys - bind-key \\ split-window -h - bind-key - split-window -v - - #### Colours - - # default statusbar colors - set -g status-style bg=default,fg=white - - # highlight active window - set -g window-status-current-style fg=red,bg=default - - # pane border - set -g pane-border-style fg=terminal,bg=default - set -g pane-active-border-style fg=yellow,bg=default - - # message text - set -g message-style fg=brightred,bg=black - - # pane number display - set -g display-panes-active-colour blue #blue - set -g display-panes-colour brightred #orange - ''; - }; - - programs.zsh = { - enable = true; - defaultKeymap = "emacs"; - autosuggestion = { - enable = true; - }; - history = { - share = true; - extended = true; - }; - sessionVariables = { - LANG = "en_US.UTF-8"; - }; - initContent = pkgs.lib.mkMerge [ - (pkgs.lib.mkBefore '' - if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then - . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' - fi - '') - '' - # matches case insensitive for lowercase - zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' - - # pasting with tabs doesn't perform completion - zstyle ':completion:*' insert-tab pending - - setopt NO_BG_NICE # don't nice background tasks - setopt NO_HUP - setopt NO_LIST_BEEP - setopt LOCAL_OPTIONS # allow functions to have local options - setopt LOCAL_TRAPS # allow functions to have local traps - setopt HIST_VERIFY - setopt PROMPT_SUBST - setopt CORRECT - setopt COMPLETE_IN_WORD - setopt IGNORE_EOF - setopt AUTO_CD - - setopt APPEND_HISTORY # adds history - setopt INC_APPEND_HISTORY SHARE_HISTORY # adds history incrementally and share it across sessions - setopt HIST_IGNORE_ALL_DUPS # don't record dupes in history - setopt HIST_REDUCE_BLANKS - - # don't expand aliases _before_ completion has finished - # like: git comm-[tab] - setopt complete_aliases - - unsetopt correct_all # Stop correcting me! - - # foreground the last backgrounded job using ctrl+z - fancy-ctrl-z () { - if [[ $#BUFFER -eq 0 ]]; then - BUFFER="fg" - zle accept-line - else - zle push-input - zle clear-screen - fi - } - - zle -N fancy-ctrl-z - bindkey '^Z' fancy-ctrl-z - - eval "$(direnv hook zsh)" - '' - ]; - }; - - programs.fzf = { - enable = true; - enableZshIntegration = true; - }; - - programs.starship = { - enable = true; - enableZshIntegration = true; - settings = { - add_newline = false; - format = "$directory$git_branch$git_status$character"; - - git_branch = { - format = " [$branch]($style)"; - symbol = ""; - }; - }; - }; - - programs.neovim = { - enable = true; - withRuby = false; - withPython3 = false; - withNodeJs = false; - defaultEditor = true; - vimAlias = true; - extraLuaConfig = builtins.readFile (./vim.lua); - plugins = with pkgs.vimPlugins; [ - fzf-lua # fuzzy find everything - gitsigns-nvim # git signs - # syntax - (nvim-treesitter.withPlugins (p: with p; [ - tree-sitter-lua - tree-sitter-javascript - tree-sitter-typescript - tree-sitter-html - tree-sitter-nix - tree-sitter-elixir - tree-sitter-heex - ])) - # color scheme - (pkgs.vimUtils.buildVimPlugin { - pname = "flexoki-neovim"; - version = "2025-08-26"; - src = pkgs.fetchurl { - url = "https://github.com/kepano/flexoki-neovim/archive/c3e2251e813d29d885a7cbbe9808a7af234d845d.tar.gz"; - sha256 = "sha256-ere25TqoPfyc2/6yQKZgAQhJXz1wxtI/VZj/0LGMwNw="; - }; - }) - ]; - }; - - home.file = { - ".claude/commands".source = ./claude/commands; - ".claude/settings.json".source = ./claude/settings.json; - ".hammerspoon/init.lua".source = ./hammerspoon/init.lua; - ".hammerspoon/leaderflow.lua".source = ./hammerspoon/leaderflow.lua; - ".config/ghostty/config".source = ./ghostty/config; - }; -} diff --git a/flake.lock b/flake.lock index 39ac16b..b03f159 100644 --- a/flake.lock +++ b/flake.lock @@ -1,48 +1,70 @@ { "nodes": { - "claude-nixpkgs": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, "locked": { - "lastModified": 1761907660, - "narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15", + "lastModified": 1749398372, + "narHash": "sha256-tYBdgS56eXYaWVW3fsnPQ/nFlgWi/Z2Ymhyu21zVM98=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9305fe4e5c2a6fcf5ba6a3ff155720fbe4076569", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", "type": "github" } }, - "home-manager": { + "mango": { "inputs": { + "flake-parts": "flake-parts", "nixpkgs": [ "nixpkgs" - ] + ], + "scenefx": "scenefx" }, "locked": { - "lastModified": 1762087455, - "narHash": "sha256-hpbPma1eUKwLAmiVRoMgIHbHiIKFkcACobJLbDt6ABw=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "43e205606aeb253bfcee15fd8a4a01d8ce8384ca", + "lastModified": 1766201993, + "narHash": "sha256-vE9q/TT7zsNVhh9+5TBt2/AnZZ8b2lr9MIekOVnLodE=", + "owner": "DreamMaoMao", + "repo": "mango", + "rev": "471c71f65c3c15ebe633edf4757361649757f990", "type": "github" }, "original": { - "owner": "nix-community", - "repo": "home-manager", + "owner": "DreamMaoMao", + "repo": "mango", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1761907660, - "narHash": "sha256-kJ8lIZsiPOmbkJypG+B5sReDXSD1KGu2VEPNqhRa/ew=", + "lastModified": 1766309749, + "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2fb006b87f04c4d3bdf08cfdbc7fab9c13d94a15", + "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", "type": "github" }, "original": { @@ -52,11 +74,83 @@ "type": "github" } }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1748740939, + "narHash": "sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "656a64127e9d791a334452c6b6606d17539476e2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "noctalia": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1766597936, + "narHash": "sha256-HQs2BB9XD1JyWt10u6PFkfP1jNfweE7kGPUBH0LNJfQ=", + "owner": "noctalia-dev", + "repo": "noctalia-shell", + "rev": "a61df00a10d95e648e18ec55a73485377cad260d", + "type": "github" + }, + "original": { + "owner": "noctalia-dev", + "repo": "noctalia-shell", + "type": "github" + } + }, "root": { "inputs": { - "claude-nixpkgs": "claude-nixpkgs", - "home-manager": "home-manager", - "nixpkgs": "nixpkgs" + "flake-utils": "flake-utils", + "mango": "mango", + "nixpkgs": "nixpkgs", + "noctalia": "noctalia" + } + }, + "scenefx": { + "inputs": { + "nixpkgs": [ + "mango", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1750785057, + "narHash": "sha256-tGX6j4W91rcb+glXJo43sjPI9zQvPotonknG1BdihR4=", + "owner": "wlrfx", + "repo": "scenefx", + "rev": "3a6cfb12e4ba97b43326357d14f7b3e40897adfc", + "type": "github" + }, + "original": { + "owner": "wlrfx", + "repo": "scenefx", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index ea49afe..5c78c4a 100644 --- a/flake.nix +++ b/flake.nix @@ -1,59 +1,116 @@ { - description = "Fleek Configuration"; + description = "Modular Nix dotfiles for macOS"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; - home-manager.url = "github:nix-community/home-manager"; - home-manager.inputs.nixpkgs.follows = "nixpkgs"; + # Override all flake inputs to use our system nixpkgs + # This ensures consistent library versions across all packages + noctalia = { + url = "github:noctalia-dev/noctalia-shell"; + inputs.nixpkgs.follows = "nixpkgs"; + }; - claude-nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + mango = { + url = "github:DreamMaoMao/mango"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, home-manager, claude-nixpkgs, ... }@inputs: - let - system = "aarch64-darwin"; - claudePkgs = import claude-nixpkgs { - inherit system; - config.allowUnfree = true; + outputs = { self, nixpkgs, flake-utils, noctalia, mango, ... }: + { + # NixOS system configurations + nixosConfigurations.nixos-utm = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + specialArgs = { inherit noctalia mango; }; + modules = [ ./machines/nixos-utm/configuration.nix ]; }; - in { - # Available through 'home-manager --flake .#your-username@your-hostname' - - homeConfigurations = { - "rastasheep@aleksandars-mbp" = home-manager.lib.homeManagerConfiguration { - pkgs = nixpkgs.legacyPackages.${system}; # Home-manager requires 'pkgs' instance - extraSpecialArgs = { inherit inputs; }; # Pass flake inputs to our config - modules = [ - ./home.nix - ./aleksandars-mbp/rastasheep.nix - ({ - nixpkgs.overlays = [ - (final: prev: { - blender = final.callPackage ./pkgs/blender.nix {}; - kicad = final.callPackage ./pkgs/kicad.nix {}; - hammerspoon = final.callPackage ./pkgs/hammerspoon.nix {}; - claude-code = final.symlinkJoin { - name = "claude-code-wrapped"; - paths = [ claudePkgs.claude-code ]; - buildInputs = [ final.makeWrapper ]; - postBuild = '' - for bin in $out/bin/*; do - wrapProgram "$bin" \ - --prefix PATH : ${final.lib.makeBinPath [ final._1password-cli ]} \ - --run 'export AWS_BEARER_TOKEN_BEDROCK=$(op read "op://Private/claude-code/AWS_BEARER_TOKEN_BEDROCK")' \ - --run 'export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=$(op read "op://Private/claude-code/OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")' \ - --run 'export OTEL_EXPORTER_OTLP_HEADERS=$(op read "op://Private/claude-code/OTEL_EXPORTER_OTLP_HEADERS")' \ - --run 'export OTEL_RESOURCE_ATTRIBUTES=$(op read "op://Private/claude-code/OTEL_RESOURCE_ATTRIBUTES")' - done - ''; - }; - }) - ]; - }) - - ]; - }; - }; - }; + } + // + # User packages (multi-system) + flake-utils.lib.eachDefaultSystem (system: + let + # Main package set with unfree packages enabled + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + + # Separate pkgs instance for Claude Code + # Allows pinning different nixpkgs version for Claude Code if needed in future + # Currently uses same nixpkgs but kept separate for flexibility + claudePkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + + # Import custom packages once, reuse everywhere (DRY principle) + # This eliminates duplication and ensures packages are evaluated only once + git = import ./packages/git { inherit pkgs; }; + scripts = import ./packages/scripts { inherit pkgs; configuredGit = git; }; + tmux = import ./packages/tmux { inherit pkgs; }; + starship = import ./packages/starship { inherit pkgs; }; + zsh = import ./packages/zsh { inherit pkgs; }; + nvim = import ./packages/nvim { inherit pkgs; }; + dircolors = import ./packages/dircolors { inherit pkgs; }; + + hammerspoon = import ./packages/hammerspoon { inherit pkgs; }; + ghostty = import ./packages/ghostty { inherit pkgs; }; + claude-code = import ./packages/claude-code { inherit pkgs claudePkgs; }; + macos-defaults = import ./packages/macos-defaults { inherit pkgs; }; + + # Custom builds (optional - commented out by default) + # blender = import ./packages/blender { inherit pkgs; }; + # kicad = import ./packages/kicad { inherit pkgs; }; + in + { + # Individual tool packages - can be run with 'nix run .#' + packages = { + # Core CLI tools with custom config + inherit scripts git tmux starship zsh nvim dircolors; + + # GUI apps and utilities + inherit hammerspoon ghostty claude-code macos-defaults; + + # Custom builds (uncomment in let block above to enable) + # inherit blender kicad; + + # Machine-specific bundles + aleksandars-mbp = import ./machines/aleksandars-mbp { inherit pkgs claudePkgs; }; + + # Convenience: all core CLI tools bundle (no machine-specific apps) + default = pkgs.buildEnv { + name = "dotfiles-core"; + paths = [ scripts git tmux starship zsh nvim dircolors ]; + pathsToLink = [ "/bin" "/share" "/etc" ]; + }; + }; + + # Checks - verify packages build correctly + checks = { + # Verify all core packages build + all-packages = pkgs.buildEnv { + name = "all-packages-check"; + paths = [ + scripts git tmux starship zsh nvim dircolors + hammerspoon ghostty claude-code macos-defaults + ]; + pathsToLink = [ "/bin" "/share" "/etc" "/Applications" ]; + }; + + # Verify machine bundle builds + machine-bundle = import ./machines/aleksandars-mbp { inherit pkgs claudePkgs; }; + + # Verify lib utilities are accessible + lib-check = pkgs.runCommand "lib-check" {} '' + # Test that lib can be imported and has expected functions + ${pkgs.nix}/bin/nix-instantiate --eval --expr ' + let lib = import ${./lib} { pkgs = import ${pkgs.path} {}; }; + in lib ? wrapWithConfig && lib ? buildConfig && lib ? smartConfigLink && lib ? mkMeta + ' > $out + ''; + }; + } + ); } diff --git a/future.md b/future.md new file mode 100644 index 0000000..9b06670 --- /dev/null +++ b/future.md @@ -0,0 +1,385 @@ +# Future Considerations: NixOS Integration + +This document outlines how this dotfiles repository can grow to support NixOS system configurations alongside the existing macOS setup. + +## Current State (macOS-Focused) + +``` +dotfiles/ +├── flake.nix # Multi-system user packages +├── flake.lock +├── lib/ # Shared utilities +│ └── default.nix +├── packages/ # Individual tool packages +│ ├── git/, tmux/, nvim/, etc. +│ └── ... +└── machines/ # User package bundles + └── aleksandars-mbp/ # macOS bundle +``` + +**Current Usage:** +- macOS: `nix profile install .#aleksandars-mbp` +- Individual tools: `nix run .#git`, `nix run .#nvim` +- Works on multiple systems (darwin, linux) via flake-utils + +**What's Missing for NixOS:** +- No NixOS system configurations (boot, networking, services, users) +- Only user-level packages, no system-level declarations +- Can install packages on NixOS, but can't manage the OS itself + +--- + +## Future Growth Options + +### Option 1: Single `machines/` Directory + +Keep all per-machine configs in one place, mixing user bundles and system configs. + +**Structure:** +``` +dotfiles/ +├── flake.nix +├── lib/ +├── packages/ +└── machines/ + ├── aleksandars-mbp/ # macOS machine + │ └── default.nix # User bundle only (no OS to configure) + │ + └── nixos-desktop/ # NixOS machine + ├── configuration.nix # System config (hardware, services, users) + ├── hardware-configuration.nix + └── packages.nix # User bundle (like aleksandars-mbp) +``` + +**Flake exports:** +```nix +{ + outputs = { nixpkgs, flake-utils, ... }: { + # NixOS system configurations + nixosConfigurations.nixos-desktop = nixpkgs.lib.nixosSystem { + modules = [ ./machines/nixos-desktop/configuration.nix ]; + }; + } + // + flake-utils.lib.eachDefaultSystem (system: { + packages = { + # User bundles + aleksandars-mbp = import ./machines/aleksandars-mbp { ... }; + nixos-desktop = import ./machines/nixos-desktop/packages.nix { ... }; + }; + }); +} +``` + +**Usage on NixOS:** +```bash +# System rebuild (includes system config + user packages) +sudo nixos-rebuild switch --flake .#nixos-desktop + +# Or just install user packages +nix profile install .#nixos-desktop +``` + +**Pros:** +- ✅ Everything per-machine in one directory +- ✅ Easy to see what belongs to which machine +- ✅ Natural grouping + +**Cons:** +- ❌ Mixes user-level and system-level concerns +- ❌ `machines/aleksandars-mbp/` is just packages, but `machines/nixos-desktop/` has system config +- ❌ Not immediately obvious which files need sudo + +**When to use:** +- Small setup (1-2 machines) +- You want everything for a machine in one place +- You prefer simplicity over separation of concerns + +--- + +### Option 3: Clear Separation (`systems/` + `users/`) + +Split system configurations from user packages into separate top-level directories. + +**Structure:** +``` +dotfiles/ +├── flake.nix +├── lib/ +├── packages/ # Building blocks +│ ├── git/, tmux/, nvim/, etc. +│ └── ... +│ +├── systems/ # NixOS system configs (hardware, services, boot) +│ ├── desktop/ +│ │ ├── configuration.nix +│ │ └── hardware-configuration.nix +│ └── laptop/ +│ ├── configuration.nix +│ └── hardware-configuration.nix +│ +└── users/ # User package bundles (renamed from machines/) + ├── aleksandars-mbp.nix # macOS + ├── desktop.nix # Linux desktop + └── laptop.nix # Linux laptop +``` + +**Flake exports:** +```nix +{ + outputs = { nixpkgs, flake-utils, ... }: { + # System configurations (from systems/) + nixosConfigurations = { + desktop = nixpkgs.lib.nixosSystem { + modules = [ ./systems/desktop/configuration.nix ]; + }; + laptop = nixpkgs.lib.nixosSystem { + modules = [ ./systems/laptop/configuration.nix ]; + }; + }; + } + // + flake-utils.lib.eachDefaultSystem (system: { + packages = { + # User bundles (from users/) + aleksandars-mbp = import ./users/aleksandars-mbp.nix { ... }; + desktop = import ./users/desktop.nix { ... }; + laptop = import ./users/laptop.nix { ... }; + }; + }); +} +``` + +**Example system config (`systems/desktop/configuration.nix`):** +```nix +{ config, pkgs, ... }: + +{ + imports = [ ./hardware-configuration.nix ]; + + # System-level configuration + boot.loader.systemd-boot.enable = true; + networking.hostName = "desktop"; + services.xserver.enable = true; + services.openssh.enable = true; + + users.users.rastasheep = { + isNormalUser = true; + extraGroups = [ "wheel" "docker" ]; + }; + + # Include user packages from users/ directory + environment.systemPackages = [ + (import ../../users/desktop.nix { + inherit pkgs; + claudePkgs = pkgs; + }) + ]; + + system.stateVersion = "24.05"; +} +``` + +**Usage on NixOS:** +```bash +# System rebuild (includes everything) +sudo nixos-rebuild switch --flake .#desktop + +# Or just user packages (no sudo) +nix profile install .#desktop +``` + +**Pros:** +- ✅ Crystal clear separation: `systems/` = sudo, `users/` = no sudo +- ✅ User bundles can be used standalone OR included in system config +- ✅ Easy to understand at a glance +- ✅ Scales well as repo grows + +**Cons:** +- ❌ Machine definition split across two directories +- ❌ Need to keep names in sync (systems/desktop ↔ users/desktop.nix) +- ❌ More directories to navigate + +**When to use:** +- Multiple machines (3+ systems) +- You want clear separation of concerns +- You value organization over simplicity +- Team environment (multiple contributors) + +--- + +### Option 4: Everything Per-Machine + +**Note:** This is essentially the same as Option 1, just with a slightly different file layout. Instead of having separate files like `packages.nix`, you'd inline more into `configuration.nix` or use nested modules. + +**Structure is same as Option 1:** +``` +machines/ +└── nixos-desktop/ + ├── default.nix (configuration.nix) + ├── hardware-configuration.nix + └── user-packages.nix +``` + +The key idea is: "one machine directory contains everything for that machine." + +--- + +## Migration Path + +### Phase 1: Current State (Done ✓) +- Multi-system flake with user packages +- Works on macOS and can work on Linux +- Machine bundles for different setups + +### Phase 2: Add NixOS Support (Choose Option 1 or 3) + +**If choosing Option 1 (Single machines/):** +1. Keep current `machines/aleksandars-mbp/` +2. Add `machines/nixos-desktop/` with system config +3. Update `flake.nix` to export `nixosConfigurations` + +**If choosing Option 3 (Clear separation):** +1. Rename `machines/` → `users/` +2. Create `systems/` directory for NixOS configs +3. Update `flake.nix` to export both `nixosConfigurations` and `packages` + +### Phase 3: Expand (As Needed) +- Add more machines (laptop, server, etc.) +- Share common modules across machines +- Add shared system modules + +--- + +## Comparison Matrix + +| Aspect | Option 1 (Single machines/) | Option 3 (systems/ + users/) | +|--------|----------------------------|------------------------------| +| **Clarity** | Moderate | High | +| **Separation** | Low (mixed concerns) | High (clear boundaries) | +| **Simplicity** | High (fewer directories) | Moderate (more structure) | +| **Scalability** | Good for 1-3 machines | Excellent for 3+ machines | +| **Per-machine view** | Excellent | Moderate (split across dirs) | +| **Standalone use** | Good | Excellent | +| **Team-friendly** | Moderate | High | + +--- + +## Recommendations + +### For Small Setup (1-2 NixOS machines) +→ **Use Option 1** (Single `machines/`) +- Keep it simple +- Easy to understand +- Less overhead + +### For Growing Setup (3+ machines or team) +→ **Use Option 3** (`systems/` + `users/`) +- Clear separation pays off +- Easier to maintain +- Better organization + +### Current Recommendation +**Start with current setup** (no changes needed for now) +- Works great for macOS +- When you get a NixOS machine, choose Option 1 or 3 based on your needs +- Easy to migrate either way + +--- + +## Example: Adding First NixOS Machine + +### Option 1 Approach + +**Step 1:** Add machine directory +```bash +mkdir -p machines/nixos-desktop +``` + +**Step 2:** Create system config +```bash +# Copy from NixOS installation +cp /etc/nixos/configuration.nix machines/nixos-desktop/ +cp /etc/nixos/hardware-configuration.nix machines/nixos-desktop/ +``` + +**Step 3:** Create user bundle +```bash +# Similar to aleksandars-mbp but Linux-focused +cp machines/aleksandars-mbp/default.nix machines/nixos-desktop/packages.nix +# Edit packages.nix to remove macOS-specific packages +``` + +**Step 4:** Update flake.nix +```nix +nixosConfigurations.nixos-desktop = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ ./machines/nixos-desktop/configuration.nix ]; +}; + +packages.nixos-desktop = import ./machines/nixos-desktop/packages.nix { ... }; +``` + +**Step 5:** Test and deploy +```bash +sudo nixos-rebuild switch --flake .#nixos-desktop +``` + +### Option 3 Approach + +**Step 1:** Restructure +```bash +mv machines users +mkdir systems +``` + +**Step 2:** Add system config +```bash +mkdir -p systems/desktop +cp /etc/nixos/configuration.nix systems/desktop/ +cp /etc/nixos/hardware-configuration.nix systems/desktop/ +``` + +**Step 3:** Create user bundle +```bash +cp users/aleksandars-mbp/default.nix users/desktop.nix +# Edit to remove macOS-specific packages +``` + +**Step 4:** Update flake.nix +```nix +nixosConfigurations.desktop = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ ./systems/desktop/configuration.nix ]; +}; + +packages.desktop = import ./users/desktop.nix { ... }; +``` + +**Step 5:** Link user bundle in system config +```nix +# In systems/desktop/configuration.nix +environment.systemPackages = [ + (import ../../users/desktop.nix { inherit pkgs; claudePkgs = pkgs; }) +]; +``` + +--- + +## Notes + +- **Current setup requires NO changes** - it already works on Linux for user packages +- NixOS integration is **additive** - doesn't break existing macOS workflow +- Both options are valid - choose based on your needs +- Easy to migrate from Option 1 to Option 3 later if needed +- Can mix approaches: use `nix profile` on NixOS without system integration + +--- + +## References + +- [NixOS Manual: Configuration](https://nixos.org/manual/nixos/stable/#ch-configuration) +- [Nix Flakes: NixOS configurations](https://nixos.wiki/wiki/Flakes#Using_nix_flakes_with_NixOS) +- [Example configs using Option 1](https://github.com/search?q=nixosConfigurations+machines&type=code) +- [Example configs using Option 3](https://github.com/search?q=nixosConfigurations+hosts+users&type=code) + diff --git a/home.nix b/home.nix deleted file mode 100644 index 63ddbb3..0000000 --- a/home.nix +++ /dev/null @@ -1,51 +0,0 @@ -{ config, pkgs, misc, ... }: { - nixpkgs = { - config = { - allowUnfree = true; - allowBroken = true; - # Workaround for https://github.com/nix-community/home-manager/issues/2942 - allowUnfreePredicate = (_: true); - }; - }; - - home.packages = [ - pkgs.ripgrep - pkgs.coreutils - pkgs.openssl - pkgs.tree - pkgs.docker - pkgs.slack - pkgs.wget - pkgs.raycast - pkgs.openvpn - pkgs.git - pkgs.direnv - # pkgs.zoom-us - ]; - home.shellAliases = { - ".." = "cd .."; - "ack" = "rg"; - "dc" = "docker compose"; - "df" = "df -hT"; - "e" = "vim"; - "f" = "fg"; - "g" = "git"; - "history" = "fc -El 1"; - "j" = "jobs"; - "ll" = "ls -lah"; - }; - home.sessionPath = [ - "$HOME/bin" - "$HOME/.local/bin" - ]; - fonts.fontconfig.enable = true; - home.stateVersion = "22.11"; # To figure this out (in-case it changes) you can comment out the line and see what version it expected. - - programs.home-manager.enable = true; - - programs.dircolors.enable = true; - - programs.zsh.profileExtra = "[ -r ~/.nix-profile/etc/profile.d/nix.sh ] && source ~/.nix-profile/etc/profile.d/nix.sh"; - programs.zsh.enableCompletion = true; - programs.zsh.enable = true; -} diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..c89cfc3 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,101 @@ +{ pkgs }: + +# Shared utilities for dotfiles packages +# Provides consistent patterns for wrapping, config building, and metadata + +let + inherit (pkgs) lib; +in +rec { + # Standard wrapper for CLI tools with configuration files + # Wraps a package and sets an environment variable pointing to config + # + # Example: + # wrapWithConfig { + # package = pkgs.starship; + # configPath = ./starship.toml; + # envVar = "STARSHIP_CONFIG"; + # } + wrapWithConfig = { package, configPath, envVar ? "CONFIG_FILE" }: + pkgs.symlinkJoin { + name = "${package.pname or package.name}-configured"; + paths = [ package ]; + buildInputs = [ pkgs.makeWrapper ]; + + postBuild = '' + for bin in $out/bin/*; do + wrapProgram "$bin" --set ${envVar} ${configPath} + done + ''; + + passthru = { + unwrapped = package; + version = package.version or "unknown"; + }; + + meta = package.meta or {}; + }; + + # Standard config directory builder + # Creates a derivation that installs config files to /share/{name} + # + # Example: + # buildConfig { + # name = "hammerspoon"; + # src = ./config; + # } + buildConfig = { name, src, subdir ? "" }: + pkgs.stdenvNoCC.mkDerivation { + inherit name src; + dontBuild = true; + + installPhase = '' + mkdir -p $out/share/${name} + ${if subdir != "" then + "cp -r $src/${subdir}/* $out/share/${name}/" + else + "cp -r $src/* $out/share/${name}/" + } + ''; + }; + + # Smart config symlinker - backs up real files/dirs, replaces symlinks + # Returns shell script snippet for use in wrapper postBuild + # + # Example: + # smartConfigLink { + # from = "${config}/share/hammerspoon"; + # to = "$HOME/.hammerspoon"; + # } + smartConfigLink = { from, to }: + '' + # Smart config linking: backup real files, replace symlinks + if [ -e "${to}" ] && [ ! -L "${to}" ]; then + # It's a real file/directory, not a symlink - back it up + backup="${to}.backup.$(date +%Y%m%d-%H%M%S)" + echo "Backing up existing config to $backup" >&2 + mv "${to}" "$backup" + fi + # Create/update symlink (replaces old symlinks, creates new ones) + ln -sf ${from} "${to}" + ''; + + # Standard meta attributes template for macOS packages + # Filters out null values automatically + # + # Example: + # mkMeta { + # description = "My awesome tool"; + # homepage = "https://example.com"; + # license = lib.licenses.mit; + # mainProgram = "mytool"; + # } + mkMeta = { description, homepage ? null, license ? null, mainProgram ? null }: + lib.filterAttrs (_: v: v != null) { + inherit description; + platforms = lib.platforms.darwin; + homepage = homepage; + license = license; + mainProgram = mainProgram; + }; +} diff --git a/machines/aleksandars-mbp/default.nix b/machines/aleksandars-mbp/default.nix new file mode 100644 index 0000000..39b6581 --- /dev/null +++ b/machines/aleksandars-mbp/default.nix @@ -0,0 +1,67 @@ +{ pkgs, claudePkgs }: + +# Machine-specific bundle for aleksandars-mbp +# Composes custom configured packages with upstream packages + +let + inherit (pkgs) lib; + + # Import custom packages + git = import ../../packages/git { inherit pkgs; }; + tmux = import ../../packages/tmux { inherit pkgs; }; + starship = import ../../packages/starship { inherit pkgs; }; + zsh = import ../../packages/zsh { inherit pkgs; }; + nvim = import ../../packages/nvim { inherit pkgs; }; + dircolors = import ../../packages/dircolors { inherit pkgs; }; + hammerspoon = import ../../packages/hammerspoon { inherit pkgs; }; + ghostty = import ../../packages/ghostty { inherit pkgs; }; + claude-code = import ../../packages/claude-code { inherit pkgs claudePkgs; }; + macos-defaults = import ../../packages/macos-defaults { inherit pkgs; }; + + # Scripts uses configured git + scripts = import ../../packages/scripts { + inherit pkgs; + configuredGit = git; + }; + +in +pkgs.buildEnv { + name = "aleksandars-mbp"; + + paths = [ + # Custom configured packages + scripts + git + tmux + starship + zsh + nvim + dircolors + hammerspoon + ghostty + claude-code + macos-defaults + + # Upstream packages (from nixpkgs) + pkgs.coreutils + pkgs.ripgrep + pkgs.openssl + pkgs.tree + pkgs.wget + pkgs.direnv + pkgs.fzf + pkgs._1password-cli + pkgs.docker + pkgs.openvpn + pkgs.slack + pkgs.raycast + ]; + + pathsToLink = [ "/bin" "/share" "/etc" "/Applications" ]; + + meta = { + description = "Complete dotfiles bundle for aleksandars-mbp"; + platforms = lib.platforms.darwin; + maintainers = [ ]; + }; +} diff --git a/machines/nixos-utm/configuration.nix b/machines/nixos-utm/configuration.nix new file mode 100644 index 0000000..b37ef31 --- /dev/null +++ b/machines/nixos-utm/configuration.nix @@ -0,0 +1,164 @@ +# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page, on +# https://search.nixos.org/options and in the NixOS manual (`nixos-help`). + +{ config, lib, pkgs, noctalia, mango, ... }: + +let + # Wrap mango with custom configuration + mangoConfigured = import ../../packages/mango { + inherit pkgs; + mangoPackage = mango.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; +in + +{ + imports = [ + ./hardware-configuration.nix + mango.nixosModules.mango + ]; + + # Use the systemd-boot EFI boot loader. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + networking.hostName = "nixos"; # Define your hostname. + + # Configure network connections interactively with nmcli or nmtui. + networking.networkmanager.enable = true; + + # Set your time zone. + # time.timeZone = "Europe/Amsterdam"; + + # Configure network proxy if necessary + # networking.proxy.default = "http://user:password@proxy:port/"; + # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; + + # Select internationalisation properties. + # i18n.defaultLocale = "en_US.UTF-8"; + # console = { + # font = "Lat2-Terminus16"; + # keyMap = "us"; + # useXkbConfig = true; # use xkb.options in tty. + # }; + + # X11 not needed - MangoWC is a Wayland compositor + # services.xserver.enable = true; + + # Wayland compositor requirements + # Enable seat management for proper device access (keyboard, mouse, GPU) + services.seatd.enable = true; + + # Configure keymap in X11 + # services.xserver.xkb.layout = "us"; + # services.xserver.xkb.options = "eurosign:e,caps:escape"; + + # Enable CUPS to print documents. + # services.printing.enable = true; + + # Enable sound. + # services.pulseaudio.enable = true; + # OR + # services.pipewire = { + # enable = true; + # pulse.enable = true; + # }; + + # Enable touchpad support (enabled default in most desktopManager). + # services.libinput.enable = true; + + # Define a user account. Don't forget to set a password with 'passwd'. + users.users.rastasheep = { + isNormalUser = true; + extraGroups = [ "wheel" "networkmanager" "video" "render" "seat" ]; # Enable 'sudo', GPU access, and seat management + packages = with pkgs; [ + tree + ]; + }; + + # programs.firefox.enable = true; + + # Graphics and rendering support for Wayland compositors + hardware.graphics = { + enable = true; + # Mesa provides OpenGL/EGL/GLES drivers for virtio GPU + # virglrenderer enables 3D acceleration in virtualized environments + extraPackages = with pkgs; [ + mesa + virglrenderer + ]; + }; + + # System packages + environment.systemPackages = with pkgs; [ + # MangoWC Wayland compositor with custom configuration + mangoConfigured + + # Noctalia shell panel/widget system + noctalia.packages.${pkgs.stdenv.hostPlatform.system}.default + + # Terminal emulator + ghostty + ]; + + # Enable services required by Noctalia + hardware.bluetooth.enable = true; + services.upower.enable = true; + services.power-profiles-daemon.enable = true; + + # Some programs need SUID wrappers, can be configured further or are + # started in user sessions. + # programs.mtr.enable = true; + # programs.gnupg.agent = { + # enable = true; + # enableSSHSupport = true; + # }; + + # List services that you want to enable: + + # Spice guest tools for UTM (enables dynamic resolution) + services.spice-vdagentd.enable = true; + services.qemuGuest.enable = true; + + # Enable automatic display resolution adjustment + services.spice-autorandr.enable = true; + + # Enable the OpenSSH daemon. + # services.openssh.enable = true; + + # Open ports in the firewall. + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + + # Copy the NixOS configuration file and link it from the resulting system + # (/run/current-system/configuration.nix). This is useful in case you + # accidentally delete configuration.nix. + # system.copySystemConfiguration = true; + + # Enable Nix flakes + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nixpkgs.config.allowUnfree = true; + + # This option defines the first version of NixOS you have installed on this particular machine, + # and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions. + # + # Most users should NEVER change this value after the initial install, for any reason, + # even if you've upgraded your system to a new NixOS release. + # + # This value does NOT affect the Nixpkgs version your packages and OS are pulled from, + # so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how + # to actually do that. + # + # This value being lower than the current NixOS release does NOT mean your system is + # out of date, out of support, or vulnerable. + # + # Do NOT change this value unless you have manually inspected all the changes it would make to your configuration, + # and migrated your data accordingly. + # + # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . + system.stateVersion = "25.11"; # Did you read the comment? + +} + diff --git a/machines/nixos-utm/hardware-configuration.nix b/machines/nixos-utm/hardware-configuration.nix new file mode 100644 index 0000000..50bee05 --- /dev/null +++ b/machines/nixos-utm/hardware-configuration.nix @@ -0,0 +1,28 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = [ ]; + + boot.initrd.availableKernelModules = [ "virtio_pci" "xhci_pci" "usbhid" "usb_storage" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/dc7644d0-4304-4317-b8dd-75843400f655"; + fsType = "ext4"; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/F542-CE71"; + fsType = "vfat"; + options = [ "fmask=0022" "dmask=0022" ]; + }; + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; +} diff --git a/pkgs/blender.nix b/packages/blender/default.nix similarity index 100% rename from pkgs/blender.nix rename to packages/blender/default.nix diff --git a/aleksandars-mbp/claude/commands/commit.md b/packages/claude-code/config/commands/commit.md similarity index 100% rename from aleksandars-mbp/claude/commands/commit.md rename to packages/claude-code/config/commands/commit.md diff --git a/aleksandars-mbp/claude/settings.json b/packages/claude-code/config/settings.json similarity index 100% rename from aleksandars-mbp/claude/settings.json rename to packages/claude-code/config/settings.json diff --git a/packages/claude-code/default.nix b/packages/claude-code/default.nix new file mode 100644 index 0000000..1c2a140 --- /dev/null +++ b/packages/claude-code/default.nix @@ -0,0 +1,51 @@ +{ pkgs, claudePkgs }: + +let + inherit (pkgs) lib; + dotfilesLib = import ../../lib { inherit pkgs; }; + + claudeConfig = dotfilesLib.buildConfig { + name = "claude"; + src = ./config; + }; + + # Wrapper script that handles config setup and environment + claudeWrapper = pkgs.writeShellScriptBin "claude" '' + # Add 1Password CLI to PATH + export PATH="${lib.makeBinPath [ pkgs._1password-cli ]}:$PATH" + + ${dotfilesLib.smartConfigLink { + from = "${claudeConfig}/share/claude"; + to = "$HOME/.claude"; + }} + + # Load secrets from 1Password + export AWS_BEARER_TOKEN_BEDROCK=$(op read "op://Private/claude-code/AWS_BEARER_TOKEN_BEDROCK" 2>/dev/null || true) + export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=$(op read "op://Private/claude-code/OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" 2>/dev/null || true) + export OTEL_EXPORTER_OTLP_HEADERS=$(op read "op://Private/claude-code/OTEL_EXPORTER_OTLP_HEADERS" 2>/dev/null || true) + export OTEL_RESOURCE_ATTRIBUTES=$(op read "op://Private/claude-code/OTEL_RESOURCE_ATTRIBUTES" 2>/dev/null || true) + + # Execute the real claude binary + exec ${claudePkgs.claude-code}/bin/claude "$@" + ''; +in +pkgs.buildEnv { + name = "claude-code-configured"; + paths = [ + claudeWrapper + claudeConfig + ]; + pathsToLink = [ "/bin" "/share" ]; + + passthru = { + unwrapped = claudePkgs.claude-code; + version = claudePkgs.claude-code.version or "unknown"; + }; + + meta = { + description = "Claude Code with 1Password integration and custom configuration"; + homepage = "https://claude.ai/code"; + platforms = lib.platforms.darwin; + mainProgram = "claude"; + }; +} diff --git a/packages/dircolors/default.nix b/packages/dircolors/default.nix new file mode 100644 index 0000000..a21d9d9 --- /dev/null +++ b/packages/dircolors/default.nix @@ -0,0 +1,29 @@ +{ pkgs }: + +let + inherit (pkgs) lib; +in +pkgs.stdenvNoCC.mkDerivation { + name = "dircolors-configured"; + + buildInputs = [ pkgs.coreutils ]; + + dontUnpack = true; + + buildPhase = '' + # Generate default dircolors database + ${pkgs.coreutils}/bin/dircolors --print-database > dir_colors + ''; + + installPhase = '' + mkdir -p $out/share/dircolors + cp dir_colors $out/share/dircolors/dircolors + ''; + + meta = { + description = "GNU dircolors configuration for colorized ls output"; + homepage = "https://www.gnu.org/software/coreutils/"; + license = lib.licenses.gpl3Plus; + platforms = lib.platforms.darwin; + }; +} diff --git a/aleksandars-mbp/ghostty/config b/packages/ghostty/config/config similarity index 100% rename from aleksandars-mbp/ghostty/config rename to packages/ghostty/config/config diff --git a/packages/ghostty/default.nix b/packages/ghostty/default.nix new file mode 100644 index 0000000..66de8a3 --- /dev/null +++ b/packages/ghostty/default.nix @@ -0,0 +1,47 @@ +{ pkgs }: + +let + inherit (pkgs) lib; + dotfilesLib = import ../../lib { inherit pkgs; }; + + ghosttyConfig = dotfilesLib.buildConfig { + name = "ghostty"; + src = ./config; + }; + + # Wrapper to setup config + ghosttyWrapper = pkgs.symlinkJoin { + name = "ghostty-configured"; + paths = [ pkgs.ghostty-bin ]; + buildInputs = [ pkgs.makeWrapper ]; + + postBuild = '' + # Wrap ghostty binary to use our config + if [ -f $out/bin/ghostty ]; then + wrapProgram $out/bin/ghostty \ + --add-flags "--config=${ghosttyConfig}/share/ghostty/config" + fi + ''; + }; +in +pkgs.buildEnv { + name = "ghostty-configured"; + paths = [ + ghosttyWrapper + ghosttyConfig + ]; + pathsToLink = [ "/bin" "/share" "/Applications" ]; + + passthru = { + unwrapped = pkgs.ghostty-bin; + version = pkgs.ghostty-bin.version or "unknown"; + }; + + meta = { + description = "Ghostty terminal with custom configuration"; + homepage = "https://ghostty.org"; + license = lib.licenses.mit; + platforms = lib.platforms.darwin; + mainProgram = "ghostty"; + }; +} diff --git a/packages/git/default.nix b/packages/git/default.nix new file mode 100644 index 0000000..63e1eac --- /dev/null +++ b/packages/git/default.nix @@ -0,0 +1,31 @@ +{ pkgs }: + +let + inherit (pkgs) lib; +in +pkgs.symlinkJoin { + name = "git-configured"; + paths = [ pkgs.git pkgs.git-lfs ]; + buildInputs = [ pkgs.makeWrapper ]; + + postBuild = '' + for bin in $out/bin/git; do + wrapProgram "$bin" \ + --set GIT_CONFIG_GLOBAL ${./gitconfig} \ + --set GIT_CONFIG_SYSTEM /dev/null \ + --add-flags "-c core.excludesFile=${./gitignore}" + done + ''; + + passthru = { + unwrapped = pkgs.git; + version = pkgs.git.version; + }; + + meta = { + description = "Git with custom configuration"; + homepage = "https://git-scm.com/"; + license = lib.licenses.gpl2; + platforms = lib.platforms.darwin; + }; +} diff --git a/packages/git/gitconfig b/packages/git/gitconfig new file mode 100644 index 0000000..be0c577 --- /dev/null +++ b/packages/git/gitconfig @@ -0,0 +1,49 @@ +[user] + name = Aleksandar Diklic + email = rastasheep3@gmail.com + +[feature] + manyFiles = true + +[init] + defaultBranch = main + +[push] + autoSetupRemote = true + +[pull] + rebase = true + +[fetch] + prune = true + +[diff] + algorithm = histogram + +[merge] + conflictstyle = zdiff3 + +[rerere] + enabled = true + +[alias] + a = add + all = add -A + st = status -sb + ci = commit + ca = commit --amend + br = branch + co = checkout + df = diff + dfc = diff --cached + lg = log --pretty=format:'%C(yellow)%h%Creset %Cgreen(%><(12)%cr%><|(12))%Creset - %s %C(blue)<%an>%Creset' + pl = pull + ps = push + undo = reset --soft HEAD^ + count = !git shortlog -sne + pr = "!f() { git fetch origin pull/$1/head:pr-$1 && git checkout pr-$1; }; f" + up = "!f() { git pull --rebase --prune && git log --pretty=format:\"%Cred%ae %Creset- %C(yellow)%s %Creset(%ar)\" HEAD@{1}.. }; f" + credit = "!f() { git commit --amend --author \"$1 <$2>\" -C HEAD; }; f" + unpushed = "!f() { git diff origin/\"$(git rev-parse --abbrev-ref HEAD)\"..HEAD; }; f" + delete-local-merged = "!git branch --merged | grep -v '^*' | grep -v 'master' | grep -v 'main' | xargs -r git branch -d" + nuke = "!f() { git branch -D $1 && git push origin :$1; }; f" diff --git a/packages/git/gitignore b/packages/git/gitignore new file mode 100644 index 0000000..d94c0c8 --- /dev/null +++ b/packages/git/gitignore @@ -0,0 +1,3 @@ +*~ +*.swp +.DS_Store diff --git a/aleksandars-mbp/hammerspoon/init.lua b/packages/hammerspoon/config/init.lua similarity index 100% rename from aleksandars-mbp/hammerspoon/init.lua rename to packages/hammerspoon/config/init.lua diff --git a/aleksandars-mbp/hammerspoon/leaderflow.lua b/packages/hammerspoon/config/leaderflow.lua similarity index 100% rename from aleksandars-mbp/hammerspoon/leaderflow.lua rename to packages/hammerspoon/config/leaderflow.lua diff --git a/packages/hammerspoon/default.nix b/packages/hammerspoon/default.nix new file mode 100644 index 0000000..196ff6d --- /dev/null +++ b/packages/hammerspoon/default.nix @@ -0,0 +1,78 @@ +{ pkgs }: + +let + inherit (pkgs) lib; + dotfilesLib = import ../../lib { inherit pkgs; }; + + hammerspoonApp = pkgs.stdenvNoCC.mkDerivation rec { + pname = "hammerspoon"; + version = "1.0.0"; + + src = pkgs.fetchurl { + name = "${pname}-${version}-source.zip"; + url = "https://github.com/Hammerspoon/hammerspoon/releases/download/${version}/Hammerspoon-${version}.zip"; + sha256 = "sha256-XbcCtV2kfcMG6PWUjZHvhb69MV3fopQoMioK9+1+an4="; + }; + + dontPatch = true; + dontConfigure = true; + dontBuild = true; + dontFixup = true; + + nativeBuildInputs = [ pkgs.unzip ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/Applications + cp -r ../Hammerspoon.app $out/Applications/ + + runHook postInstall + ''; + + meta = { + homepage = "https://www.hammerspoon.org"; + description = "Staggeringly powerful macOS desktop automation with Lua"; + license = lib.licenses.mit; + platforms = lib.platforms.darwin; + }; + }; + + hammerspoonConfig = dotfilesLib.buildConfig { + name = "hammerspoon"; + src = ./config; + }; + + # Wrapper script to launch the app and setup config + hammerspoonWrapper = pkgs.writeShellScriptBin "hammerspoon" '' + ${dotfilesLib.smartConfigLink { + from = "${hammerspoonConfig}/share/hammerspoon"; + to = "$HOME/.hammerspoon"; + }} + + # Open the app + open ${hammerspoonApp}/Applications/Hammerspoon.app + ''; +in +pkgs.buildEnv { + name = "hammerspoon-configured"; + paths = [ + hammerspoonApp + hammerspoonConfig + hammerspoonWrapper + ]; + pathsToLink = [ "/Applications" "/share" "/bin" ]; + + passthru = { + unwrapped = hammerspoonApp; + version = hammerspoonApp.version; + }; + + meta = { + description = "Hammerspoon with custom configuration"; + homepage = "https://www.hammerspoon.org"; + license = lib.licenses.mit; + platforms = lib.platforms.darwin; + mainProgram = "hammerspoon"; + }; +} diff --git a/pkgs/kicad.nix b/packages/kicad/default.nix similarity index 100% rename from pkgs/kicad.nix rename to packages/kicad/default.nix diff --git a/packages/macos-defaults/bin/macos-defaults.py b/packages/macos-defaults/bin/macos-defaults.py new file mode 100644 index 0000000..d631cba --- /dev/null +++ b/packages/macos-defaults/bin/macos-defaults.py @@ -0,0 +1,582 @@ +#!/usr/bin/env python3 +""" +macOS Defaults Management Tool + +Drift detection and discovery for macOS system defaults. +Application happens automatically through Nix package installation. +""" + +import subprocess +import json +import sys +import re +from pathlib import Path +from typing import Optional, Dict, Any, List, Tuple +from dataclasses import dataclass +from enum import Enum + +# Config path injected at build time +CONFIG_PATH = "CONFIG_PATH_PLACEHOLDER" + +class ValueType(Enum): + """Supported macOS defaults value types""" + BOOL = "bool" + INT = "int" + FLOAT = "float" + STRING = "string" + ARRAY = "array" + DICT = "dict" + +@dataclass +class Setting: + """Represents a single macOS default setting""" + domain: str + key: str + value: Any + value_type: ValueType + description: str = "" + +class ConfigLoader: + """Handles loading and parsing configuration from Nix store""" + + @staticmethod + def load(config_path: str = CONFIG_PATH) -> Dict[str, Dict[str, Setting]]: + """Load configuration from JSON file""" + try: + with open(config_path) as f: + data = json.load(f) + + config = {} + for domain, settings in data.get("domains", {}).items(): + config[domain] = {} + for key, meta in settings.items(): + config[domain][key] = Setting( + domain=domain, + key=key, + value=meta["value"], + value_type=ValueType(meta["type"]), + description=meta.get("description", "") + ) + + return config + except FileNotFoundError: + print(f"Error: Config file not found at {config_path}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in config file: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error loading config: {e}", file=sys.stderr) + sys.exit(1) + + +class DefaultsReader: + """Wrapper around the 'defaults' command for reading system settings""" + + @staticmethod + def read(domain: str, key: str) -> Optional[str]: + """Read a single setting using defaults read""" + try: + result = subprocess.run( + ["defaults", "read", domain, key], + capture_output=True, + text=True, + check=False, + timeout=5 + ) + if result.returncode == 0: + return result.stdout.strip() + return None + except subprocess.TimeoutExpired: + print(f"Warning: Timeout reading {domain}.{key}", file=sys.stderr) + return None + except Exception as e: + print(f"Warning: Error reading {domain}.{key}: {e}", file=sys.stderr) + return None + + @staticmethod + def list_domains() -> List[str]: + """List all available domains""" + try: + result = subprocess.run( + ["defaults", "domains"], + capture_output=True, + text=True, + check=False, + timeout=10 + ) + if result.returncode == 0: + return [d.strip() for d in result.stdout.strip().split(',') if d.strip()] + return [] + except Exception: + return [] + + @staticmethod + def read_domain(domain: str) -> Optional[str]: + """Read all settings from a domain""" + try: + result = subprocess.run( + ["defaults", "read", domain], + capture_output=True, + text=True, + check=False, + timeout=10 + ) + if result.returncode == 0: + return result.stdout + return None + except Exception: + return None + + @staticmethod + def extract_keys(domain_output: str) -> List[str]: + """Extract keys from 'defaults read domain' output""" + if not domain_output: + return [] + return list(set(re.findall(r'^\s+([A-Za-z0-9_.-]+)\s*=', domain_output, re.MULTILINE))) + +class ValueParser: + """Parse and normalize defaults values""" + + @staticmethod + def parse(raw_value: Optional[str], expected_type: ValueType) -> Any: + """Parse defaults read output based on expected type""" + if raw_value is None: + return None + + value = raw_value.strip() + + if expected_type == ValueType.BOOL: + return value == "1" or value.lower() == "true" + + elif expected_type == ValueType.INT: + try: + return int(value) + except (ValueError, TypeError): + return None + + elif expected_type == ValueType.FLOAT: + try: + return float(value) + except (ValueError, TypeError): + return None + + elif expected_type == ValueType.STRING: + if value.startswith('"') and value.endswith('"'): + return value[1:-1] + return value + + elif expected_type == ValueType.ARRAY: + # Arrays come as (item1, item2, ...) or () + if value == "(\n)" or value == "()": + return [] + try: + # Try JSON parsing first + return json.loads(value) + except (json.JSONDecodeError, ValueError): + # Return raw value if parsing fails + return value + + else: # DICT or unknown + return value + + @staticmethod + def detect_type(value_str: str) -> ValueType: + """Auto-detect value type from defaults output""" + value_str = value_str.strip() + + # Boolean + if value_str in ("1", "0", "true", "false", "YES", "NO"): + return ValueType.BOOL + + # Array + if value_str.startswith('(') and value_str.endswith(')'): + return ValueType.ARRAY + + # Dict (with = sign inside) + if value_str.startswith('{') and value_str.endswith('}') and '=' in value_str: + return ValueType.DICT + + # Integer + try: + int(value_str) + return ValueType.INT + except ValueError: + pass + + # Float + try: + float(value_str) + return ValueType.FLOAT + except ValueError: + pass + + # Default to string + return ValueType.STRING + + @staticmethod + def compare(expected: Any, actual: Any, value_type: ValueType) -> bool: + """Compare expected vs actual values, accounting for type differences""" + if actual is None: + return False + + # String comparison with normalization + if value_type == ValueType.STRING: + exp_str = str(expected).strip().strip('"') + act_str = str(actual).strip().strip('"') + + # Normalize variable syntax: both $VAR and ${VAR} should match + dollar_sign = chr(36) + exp_str = re.sub(r'\$\{([^}]+)\}', dollar_sign + r'\1', exp_str) + act_str = re.sub(r'\$\{([^}]+)\}', dollar_sign + r'\1', act_str) + + return exp_str == act_str + + # Array comparison + if value_type == ValueType.ARRAY: + if isinstance(expected, list) and isinstance(actual, list): + return expected == actual + return False + + # Direct comparison for numbers and bools + return expected == actual + + +class NixFormatter: + """Format values as Nix expressions""" + + @staticmethod + def format_value(value: Any, value_type: ValueType) -> str: + """Format value for Nix expression""" + if value_type == ValueType.BOOL: + if isinstance(value, bool): + return "true" if value else "false" + value_str = str(value) + return "true" if value_str in ("1", "true", "YES") else "false" + + elif value_type == ValueType.INT: + return str(value) + + elif value_type == ValueType.FLOAT: + return str(value) + + elif value_type == ValueType.ARRAY: + if isinstance(value, list): + if not value: + return "[]" + # Format array items + items = [NixFormatter._format_array_item(item) for item in value] + return f'[ {" ".join(items)} ]' + return "[]" + + elif value_type == ValueType.DICT: + return "{}" # Simplified - dict parsing is complex + + else: # STRING + # Escape special characters + escaped = str(value).replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + return f'"{escaped}"' + + @staticmethod + def _format_array_item(item: Any) -> str: + """Format a single array item""" + if isinstance(item, str): + escaped = item.replace('\\', '\\\\').replace('"', '\\"') + return f'"{escaped}"' + elif isinstance(item, bool): + return "true" if item else "false" + else: + return str(item) + + @staticmethod + def format_setting(domain: str, key: str, value: Any, value_type: ValueType, description: str = "") -> str: + """Format complete setting definition for Nix""" + nix_value = NixFormatter.format_value(value, value_type) + + lines = [ + f' {key} = {{', + f' value = {nix_value};', + f' type = "{value_type.value}";', + f' description = "{description}";', + f' }};' + ] + return '\n'.join(lines) + + +class Commands: + """CLI command implementations""" + + def __init__(self, config: Dict[str, Dict[str, Setting]]): + self.config = config + self.reader = DefaultsReader() + self.parser = ValueParser() + self.formatter = NixFormatter() + + def check(self) -> int: + """Check for drift between config and system""" + drift_count = 0 + print("Checking for configuration drift...\n") + + for domain in sorted(self.config.keys()): + settings = self.config[domain] + domain_has_drift = False + + for key in sorted(settings.keys()): + setting = settings[key] + + raw_actual = self.reader.read(domain, key) + actual = self.parser.parse(raw_actual, setting.value_type) + + if not self.parser.compare(setting.value, actual, setting.value_type): + if not domain_has_drift: + print(f"\n{domain}:") + domain_has_drift = True + + print(f" {key}") + print(f" Expected: {setting.value} ({setting.value_type.value})") + print(f" Actual: {actual}") + drift_count += 1 + + print(f"\n{'='*60}") + if drift_count == 0: + print("✓ No drift detected. System matches configuration.") + return 0 + else: + print(f"✗ Found {drift_count} setting(s) with drift.") + print("\nTo reapply configuration:") + print(" nix profile upgrade '.*'") + print(" # or manually:") + print(" macos-defaults-apply") + return 1 + + def list(self) -> int: + """List all configured domains and settings""" + print("Configured macOS Defaults:\n") + total = 0 + + for domain in sorted(self.config.keys()): + settings = self.config[domain] + count = len(settings) + total += count + print(f"{domain}: ({count} settings)") + + for key in sorted(settings.keys()): + setting = settings[key] + + if setting.value_type == ValueType.BOOL: + value_str = "true" if setting.value else "false" + elif setting.value_type == ValueType.ARRAY: + if not setting.value: + value_str = "[]" + elif isinstance(setting.value, list): + value_str = f"[{len(setting.value)} items]" + else: + value_str = str(setting.value) + elif setting.value_type == ValueType.STRING and len(str(setting.value)) > 40: + value_str = str(setting.value)[:37] + "..." + else: + value_str = str(setting.value) + + print(f" • {key} = {value_str} ({setting.value_type.value})") + if setting.description: + print(f" {setting.description}") + + print() + + print(f"Total: {total} settings across {len(self.config)} domains") + return 0 + + def export(self, domain_filter: Optional[str] = None) -> int: + """Export current system settings as nix expression""" + if domain_filter: + domains_to_export = [domain_filter] + else: + domains_to_export = self.reader.list_domains() + if not domains_to_export: + print("Error: Could not list domains", file=sys.stderr) + return 1 + + print("{") + print(" domains = {") + + exported_count = 0 + for domain in sorted(domains_to_export): + domain_output = self.reader.read_domain(domain) + if not domain_output: + continue + + keys = self.reader.extract_keys(domain_output) + if not keys: + continue + + print(f' "{domain}" = {{') + + for key in sorted(keys): + # Read individual value + raw_value = self.reader.read(domain, key) + if raw_value is not None: + value_type = self.parser.detect_type(raw_value) + value = self.parser.parse(raw_value, value_type) + + nix_value = self.formatter.format_value(value, value_type) + + print(f' {key} = {{') + print(f' value = {nix_value};') + print(f' type = "{value_type.value}";') + print(f' description = "";') + print(f' }};') + exported_count += 1 + + print(" };") + + # Limit output if exporting all domains + if not domain_filter and exported_count > 200: + print(' # ... output truncated (use domain filter for full export)') + break + + print(" };") + print("}") + + if exported_count == 0: + print("# No settings found", file=sys.stderr) + return 1 + + return 0 + + def discover(self, domain_filter: Optional[str] = None) -> int: + """Find system settings not in config""" + configured_domains = self.config + + if domain_filter: + domains_to_scan = [domain_filter] + print(f"Scanning {domain_filter}...\n") + else: + domains_to_scan = self.reader.list_domains() + if not domains_to_scan: + print("Error: Could not list domains", file=sys.stderr) + return 1 + print(f"Scanning {len(domains_to_scan)} domains...\n") + + new_settings_in_tracked = {} + new_domains = {} + + for domain in domains_to_scan: + domain_output = self.reader.read_domain(domain) + if not domain_output: + continue + + keys = set(self.reader.extract_keys(domain_output)) + if not keys: + continue + + if domain in configured_domains: + configured_keys = set(configured_domains[domain].keys()) + new_keys = keys - configured_keys + if new_keys: + new_settings_in_tracked[domain] = sorted(new_keys) + else: + new_domains[domain] = sorted(keys) + + total_new = 0 + + if new_settings_in_tracked: + print("New settings in tracked domains:\n") + for domain in sorted(new_settings_in_tracked.keys()): + keys = new_settings_in_tracked[domain] + print(f" {domain} ({len(keys)} new)") + for key in keys: + if domain_filter: + # Show value when filtering by domain + raw_value = self.reader.read(domain, key) + if raw_value: + value = raw_value.strip() + # Truncate long values + if len(value) > 100: + value = value[:97] + "..." + # Replace newlines for compact display + value = value.replace('\n', ' ') + print(f" • {key} = {value}") + else: + print(f" • {key}") + else: + print(f" • {key}") + total_new += len(keys) + print() + + if new_domains: + print(f"Untracked domains ({len(new_domains)} domains):\n") + for domain in sorted(new_domains.keys()): + keys = new_domains[domain] + print(f" {domain} ({len(keys)} settings)") + # Show setting names and values when filtering by specific domain + if domain_filter: + for key in keys: + raw_value = self.reader.read(domain, key) + if raw_value: + value = raw_value.strip() + # Truncate long values + if len(value) > 100: + value = value[:97] + "..." + # Replace newlines for compact display + value = value.replace('\n', ' ') + print(f" • {key} = {value}") + else: + print(f" • {key}") + total_new += len(keys) + print() + + # Summary + if total_new == 0: + print("✓ No new settings discovered") + else: + print(f"Summary: {total_new} settings in {len(new_settings_in_tracked) + len(new_domains)} domains") + if new_settings_in_tracked: + print(f" • {sum(len(k) for k in new_settings_in_tracked.values())} new settings in tracked domains") + if new_domains: + print(f" • {sum(len(k) for k in new_domains.values())} settings in {len(new_domains)} untracked domains") + + return 0 + + +def main(): + """Main entry point""" + if len(sys.argv) < 2: + print("Usage: macos-defaults {check|list|export|discover} [domain]") + print("") + print("Commands:") + print(" check - Show drift between config and system") + print(" list - List all configured domains and settings") + print(" export [domain] - Export current system settings as nix") + print(" discover [domain] - Find settings not in config") + print("") + print("Note: To apply settings, use 'macos-defaults-apply' or rebuild your nix profile.") + sys.exit(1) + + command = sys.argv[1] + + try: + config = ConfigLoader.load() + except SystemExit: + raise + except Exception as e: + print(f"Fatal error: {e}", file=sys.stderr) + sys.exit(1) + + commands = Commands(config) + + command_map = { + "check": lambda: commands.check(), + "list": lambda: commands.list(), + "export": lambda: commands.export(sys.argv[2] if len(sys.argv) > 2 else None), + "discover": lambda: commands.discover(sys.argv[2] if len(sys.argv) > 2 else None), + } + + if command not in command_map: + print(f"Error: Unknown command '{command}'", file=sys.stderr) + print("Run 'macos-defaults' for usage.", file=sys.stderr) + sys.exit(1) + + sys.exit(command_map[command]()) + +if __name__ == "__main__": + main() diff --git a/packages/macos-defaults/default.nix b/packages/macos-defaults/default.nix new file mode 100644 index 0000000..640589b --- /dev/null +++ b/packages/macos-defaults/default.nix @@ -0,0 +1,103 @@ +{ pkgs }: + +# macOS Defaults Package +# Manages macOS system defaults with configuration management and drift detection +# +# Provides two commands: +# - macos-defaults: Management tool (check, list, export, discover) +# - macos-defaults-apply: Apply configuration + +let + inherit (pkgs) lib; + + helpers = import ./lib/helpers.nix { inherit pkgs; }; + types = import ./lib/types.nix { inherit pkgs; }; + validators = import ./lib/validators.nix { inherit pkgs types; }; + + # Load and validate configuration at build time + # This will fail the build if there are validation errors + config = validators.validationReport (import ./defaults.nix); + + # Generate apply_setting commands directly for the shell script + # Format: apply_setting 'domain' 'key' '-type' value + commandsForScript = lib.flatten ( + lib.mapAttrsToList (domain: settings: + lib.mapAttrsToList (key: meta: + let + domainArg = helpers.escapeShellArg domain; + keyArg = helpers.escapeShellArg key; + typeFlag = helpers.typeToFlag meta.type; + valueStr = helpers.valueToString meta.value meta.type; + in + "apply_setting ${domainArg} ${keyArg} ${typeFlag} ${valueStr}" + ) settings + ) config.domains + ); + + # Build apply script by substituting commands into template + commandsText = lib.concatMapStringsSep "\n" (cmd: " ${cmd}") commandsForScript; + + applyScript = pkgs.writeTextFile { + name = "macos-defaults-apply"; + text = builtins.replaceStrings + ["@COMMANDS@"] + [commandsText] + (builtins.readFile ./scripts/apply-defaults.sh); + executable = true; + }; + + # Generate JSON configuration for Python management script + configJsonStr = builtins.toJSON { + domains = lib.mapAttrs (domain: settings: + lib.mapAttrs (key: meta: { + value = meta.value; + type = meta.type; + description = meta.description; + }) settings + ) config.domains; + }; + + configJson = pkgs.writeText "macos-defaults-config.json" configJsonStr; + + managementScript = pkgs.writeScriptBin "macos-defaults" '' + #!${pkgs.python3}/bin/python3 + ${builtins.replaceStrings + ["CONFIG_PATH_PLACEHOLDER"] + ["${configJson}"] + (builtins.readFile ./bin/macos-defaults.py) + } + ''; + +in +pkgs.stdenv.mkDerivation { + name = "macos-defaults-configured"; + version = "2.0.0"; + + dontUnpack = true; + dontBuild = true; + + installPhase = '' + mkdir -p $out/bin $out/share/macos-defaults + + cp ${configJson} $out/share/macos-defaults/config.json + cp ${applyScript} $out/bin/macos-defaults-apply # Already executable from writeTextFile + cp ${managementScript}/bin/macos-defaults $out/bin/macos-defaults + ''; + + meta = { + description = "macOS system defaults configuration and management"; + longDescription = '' + Manages macOS system defaults with declarative configuration. + + Includes two tools: + - macos-defaults: Check drift, list settings, export, and discover + - macos-defaults-apply: Apply the configuration (supports DRY_RUN=1 and VERBOSE=1) + + Refactored for modularity, better error handling, and maintainability. + ''; + homepage = "https://github.com/rastasheep/dotfiles"; + platforms = lib.platforms.darwin; + maintainers = [ ]; + mainProgram = "macos-defaults"; + }; +} diff --git a/packages/macos-defaults/defaults.nix b/packages/macos-defaults/defaults.nix new file mode 100644 index 0000000..ae105f1 --- /dev/null +++ b/packages/macos-defaults/defaults.nix @@ -0,0 +1,457 @@ +{ + # macOS System Defaults Configuration + # Organized by domain with type annotations and descriptions + + domains = { + # Global system-wide defaults + NSGlobalDomain = { + _HIHideMenuBar = { + value = 1; + type = "int"; + description = "Hide menu bar automatically"; + }; + AppleAccentColor = { + value = 3; + type = "int"; + description = "Set accent color to green"; + }; + AppleHighlightColor = { + value = "0.752941 0.964706 0.678431 Green"; + type = "string"; + description = "Set highlight color to green"; + }; + AppleShowAllExtensions = { + value = true; + type = "bool"; + description = "Show all filename extensions in Finder"; + }; + InitialKeyRepeat = { + value = 25; + type = "int"; + description = "Set initial delay before key repeat"; + }; + KeyRepeat = { + value = 2; + type = "int"; + description = "Set key repeat speed"; + }; + KeyRepeatDelay = { + value = "0.5"; + type = "string"; + description = "Additional key repeat delay configuration"; + }; + KeyRepeatEnabled = { + value = 1; + type = "int"; + description = "Enable key repeat"; + }; + KeyRepeatInterval = { + value = "0.083333333"; + type = "string"; + description = "Key repeat interval"; + }; + AppleEnableMouseSwipeNavigateWithScrolls = { + value = 1; + type = "int"; + description = "Enable swipe navigation with scrolling"; + }; + "com.apple.mouse.scaling" = { + value = 2; + type = "int"; + description = "Mouse tracking speed"; + }; + NSNavPanelExpandedStateForSaveMode = { + value = true; + type = "bool"; + description = "Expand save panel by default"; + }; + NSNavPanelExpandedStateForSaveMode2 = { + value = true; + type = "bool"; + description = "Expand save panel by default (alternative)"; + }; + AppleAquaColorVariant = { + value = 1; + type = "int"; + description = "Use dark aqua color variant"; + }; + AppleInterfaceStyle = { + value = "Dark"; + type = "string"; + description = "Use dark interface style"; + }; + }; + + # Desktop services (Finder-related) + "com.apple.desktopservices" = { + DSDontWriteNetworkStores = { + value = true; + type = "bool"; + description = "Don't write .DS_Store files on network volumes"; + }; + DSDontWriteUSBStores = { + value = true; + type = "bool"; + description = "Don't write .DS_Store files on USB drives"; + }; + }; + + # Menu bar clock + "com.apple.menuextra.clock" = { + Show24Hour = { + value = true; + type = "bool"; + description = "Use 24-hour clock format"; + }; + ShowDayOfWeek = { + value = true; + type = "bool"; + description = "Show day of week in menu bar"; + }; + ShowAMPM = { + value = true; + type = "bool"; + description = "Show AM/PM indicator"; + }; + ShowSeconds = { + value = false; + type = "bool"; + description = "Don't show seconds in clock"; + }; + ShowDate = { + value = 0; + type = "int"; + description = "Don't show date in menu bar"; + }; + FlashDateSeparators = { + value = false; + type = "bool"; + description = "Don't flash date separators"; + }; + IsAnalog = { + value = false; + type = "bool"; + description = "Use digital clock (not analog)"; + }; + }; + + # Dock configuration + "com.apple.dock" = { + show-recents = { + value = false; + type = "bool"; + description = "Don't show recent applications in Dock"; + }; + orientation = { + value = "left"; + type = "string"; + description = "Position Dock on left side of screen"; + }; + tilesize = { + value = 28; + type = "int"; + description = "Set icon size of Dock items to 28 pixels"; + }; + autohide = { + value = true; + type = "bool"; + description = "Automatically hide and show the Dock"; + }; + minimize-to-application = { + value = true; + type = "bool"; + description = "Minimize windows into their application's icon"; + }; + autohide-delay = { + value = 0; + type = "int"; + description = "Show Dock instantly on hover (no delay)"; + }; + persistent-apps = { + value = []; + type = "array"; + description = "Empty the dock (no persistent applications)"; + }; + }; + + # Finder configuration + "com.apple.finder" = { + FXPreferredViewStyle = { + value = "clmv"; + type = "string"; + description = "Use column view in Finder"; + }; + FXEnableExtensionChangeWarning = { + value = false; + type = "bool"; + description = "Disable warning when changing file extension"; + }; + ShowExternalHardDrivesOnDesktop = { + value = false; + type = "bool"; + description = "Don't show external hard drives on desktop"; + }; + ShowRemovableMediaOnDesktop = { + value = false; + type = "bool"; + description = "Don't show removable media on desktop"; + }; + OpenWindowForNewRemovableDisk = { + value = true; + type = "bool"; + description = "Open Finder window when removable disk is mounted"; + }; + EmptyTrashSecurely = { + value = true; + type = "bool"; + description = "Empty Trash securely by default"; + }; + NewWindowTarget = { + value = "PfDe"; + type = "string"; + description = "Set $HOME as default location for new Finder windows"; + }; + NewWindowTargetPath = { + value = "file://$HOME"; + type = "string"; + description = "Path for new Finder windows (uses $HOME at runtime)"; + }; + ShowSidebar = { + value = true; + type = "bool"; + description = "Show sidebar in Finder"; + }; + SidebarWidth = { + value = 176; + type = "int"; + description = "Set sidebar width to 176 pixels"; + }; + ShowStatusBar = { + value = false; + type = "bool"; + description = "Don't show status bar in Finder"; + }; + ShowPathbar = { + value = false; + type = "bool"; + description = "Don't show path bar in Finder"; + }; + ShowTabView = { + value = false; + type = "bool"; + description = "Don't show tab view in Finder"; + }; + ShowToolbar = { + value = true; + type = "bool"; + description = "Show toolbar in Finder"; + }; + FXICloudDriveRemovalWarning = { + value = false; + type = "bool"; + description = "Disable warning before removing from iCloud Drive"; + }; + _FXSortFoldersFirst = { + value = true; + type = "bool"; + description = "Keep folders on top when sorting by name"; + }; + }; + + # Time Machine + "com.apple.TimeMachine" = { + DoNotOfferNewDisksForBackup = { + value = true; + type = "bool"; + description = "Don't prompt to use new disks for Time Machine backup"; + }; + }; + + # Mouse settings + "com.apple.mouse" = { + tapBehavior = { + value = true; + type = "bool"; + description = "Enable tap to click for mouse"; + }; + }; + + # Apple Magic Mouse + "com.apple.AppleMultitouchMouse" = { + MouseButtonMode = { + value = "TwoButton"; + type = "string"; + description = "Set mouse to two-button mode"; + }; + MouseOneFingerDoubleTapGesture = { + value = 1; + type = "int"; + description = "Enable one finger double tap gesture"; + }; + MouseTwoFingerHorizSwipeGesture = { + value = 1; + type = "int"; + description = "Enable two finger horizontal swipe gesture"; + }; + MouseTwoFingerDoubleTapGesture = { + value = 3; + type = "int"; + description = "Enable two finger double tap gesture (smart zoom)"; + }; + MouseHorizontalScroll = { + value = 1; + type = "int"; + description = "Enable horizontal scroll"; + }; + MouseMomentumScroll = { + value = 1; + type = "int"; + description = "Enable momentum scroll"; + }; + MouseVerticalScroll = { + value = 1; + type = "int"; + description = "Enable vertical scroll"; + }; + MouseButtonDivision = { + value = 55; + type = "int"; + description = "Set mouse button division"; + }; + UserPreferences = { + value = 1; + type = "int"; + description = "Enable user preferences"; + }; + version = { + value = 1; + type = "int"; + description = "Mouse settings version"; + }; + }; + + # Trackpad settings + "com.apple.driver.AppleBluetoothMultitouch.trackpad" = { + Clicking = { + value = true; + type = "bool"; + description = "Enable tap to click"; + }; + DragLock = { + value = false; + type = "bool"; + description = "Disable drag lock"; + }; + Dragging = { + value = false; + type = "bool"; + description = "Disable dragging"; + }; + TrackpadCornerSecondaryClick = { + value = false; + type = "bool"; + description = "Disable corner secondary click"; + }; + TrackpadFiveFingerPinchGesture = { + value = 2; + type = "int"; + description = "Enable five finger pinch gesture"; + }; + TrackpadFourFingerHorizSwipeGesture = { + value = 2; + type = "int"; + description = "Enable four finger horizontal swipe gesture"; + }; + TrackpadFourFingerPinchGesture = { + value = 2; + type = "int"; + description = "Enable four finger pinch gesture"; + }; + TrackpadFourFingerVertSwipeGesture = { + value = 2; + type = "int"; + description = "Enable four finger vertical swipe gesture"; + }; + TrackpadHandResting = { + value = true; + type = "bool"; + description = "Enable hand resting on trackpad"; + }; + TrackpadHorizScroll = { + value = true; + type = "bool"; + description = "Enable horizontal scroll"; + }; + TrackpadMomentumScroll = { + value = true; + type = "bool"; + description = "Enable momentum scroll"; + }; + TrackpadPinch = { + value = true; + type = "bool"; + description = "Enable pinch gesture"; + }; + TrackpadRightClick = { + value = true; + type = "bool"; + description = "Enable right click"; + }; + TrackpadRotate = { + value = true; + type = "bool"; + description = "Enable rotate gesture"; + }; + TrackpadScroll = { + value = true; + type = "bool"; + description = "Enable scroll"; + }; + TrackpadThreeFingerDrag = { + value = false; + type = "bool"; + description = "Disable three finger drag"; + }; + TrackpadThreeFingerHorizSwipeGesture = { + value = 2; + type = "int"; + description = "Enable three finger horizontal swipe gesture"; + }; + TrackpadThreeFingerTapGesture = { + value = false; + type = "bool"; + description = "Disable three finger tap gesture"; + }; + TrackpadThreeFingerVertSwipeGesture = { + value = 2; + type = "int"; + description = "Enable three finger vertical swipe gesture"; + }; + TrackpadTwoFingerDoubleTapGesture = { + value = 1; + type = "int"; + description = "Enable two finger double tap gesture"; + }; + TrackpadTwoFingerFromRightEdgeSwipeGesture = { + value = 3; + type = "int"; + description = "Enable two finger swipe from right edge"; + }; + USBMouseStopsTrackpad = { + value = false; + type = "bool"; + description = "Don't stop trackpad when USB mouse is connected"; + }; + UserPreferences = { + value = true; + type = "bool"; + description = "Enable user preferences"; + }; + version = { + value = 5; + type = "int"; + description = "Trackpad settings version"; + }; + }; + }; +} diff --git a/packages/macos-defaults/lib/helpers.nix b/packages/macos-defaults/lib/helpers.nix new file mode 100644 index 0000000..adf7d5b --- /dev/null +++ b/packages/macos-defaults/lib/helpers.nix @@ -0,0 +1,47 @@ +{ pkgs }: + +# Helper functions for generating macOS defaults commands + +let + inherit (pkgs) lib; + + # Escape shell argument using single quotes (safe for all content) + escapeShellArg = arg: + let + escaped = builtins.replaceStrings ["'"] ["'\\''"] arg; + in "'${escaped}'"; + +in rec { + # Example: "bool" -> "-bool" + typeToFlag = type: { + bool = "-bool"; + int = "-int"; + string = "-string"; + float = "-float"; + array = "-array"; + dict = "-dict"; + }.${type}; + + # Convert value to properly escaped string for shell + # Preserves $VAR in strings for runtime shell expansion + valueToString = value: type: + if type == "bool" then + (if value then "true" else "false") + else if type == "array" then + # Escape JSON for shell - wrap in single quotes and escape existing quotes + escapeShellArg (builtins.toJSON value) + else if type == "dict" then + # Escape dict representation for shell + escapeShellArg (builtins.toJSON value) + else if type == "string" then + if builtins.match ".*\\$.*" value != null then + # Preserve $VAR for runtime shell expansion - use double quotes + ''"${value}"'' + else + ''"${value}"'' + else + toString value; + + # Export escapeShellArg for use in default.nix + inherit escapeShellArg; +} diff --git a/packages/macos-defaults/lib/types.nix b/packages/macos-defaults/lib/types.nix new file mode 100644 index 0000000..7e4d8cb --- /dev/null +++ b/packages/macos-defaults/lib/types.nix @@ -0,0 +1,39 @@ +{ pkgs }: + +{ + validTypes = [ "bool" "int" "string" "float" "array" "dict" ]; + + isValidType = type: builtins.elem type [ "bool" "int" "string" "float" "array" "dict" ]; + + validateValueType = value: type: + if type == "bool" then + builtins.isBool value + else if type == "int" then + builtins.isInt value + else if type == "float" then + builtins.isFloat value || builtins.isInt value # Accept int as float + else if type == "string" then + builtins.isString value + else if type == "array" then + builtins.isList value + else if type == "dict" then + builtins.isAttrs value + else + false; + + typeDescription = type: + if type == "bool" then + "boolean (true/false)" + else if type == "int" then + "integer" + else if type == "float" then + "float or integer" + else if type == "string" then + "string" + else if type == "array" then + "list/array" + else if type == "dict" then + "attribute set/dictionary" + else + "unknown type"; +} diff --git a/packages/macos-defaults/lib/validators.nix b/packages/macos-defaults/lib/validators.nix new file mode 100644 index 0000000..76b552a --- /dev/null +++ b/packages/macos-defaults/lib/validators.nix @@ -0,0 +1,131 @@ +{ pkgs, types }: + +let + inherit (pkgs) lib; + + mkError = domain: key: message: { + inherit domain key message; + severity = "error"; + }; + + mkWarning = domain: key: message: { + inherit domain key message; + severity = "warning"; + }; + + # Forward declaration for recursive use + validators = { + validateSetting = domain: key: meta: + let + errors = []; + + # Check required fields + missingValue = if !meta ? value then + [ (mkError domain key "Missing required field 'value'") ] + else []; + + missingType = if !meta ? type then + [ (mkError domain key "Missing required field 'type'") ] + else []; + + # Check type is valid + invalidType = if meta ? type && !(types.isValidType meta.type) then + [ (mkError domain key "Invalid type '${meta.type}'. Must be one of: ${builtins.concatStringsSep ", " types.validTypes}") ] + else []; + + # Check value matches type (only if both value and type are present and type is valid) + typeMismatch = if (meta ? value) && (meta ? type) && (types.isValidType meta.type) && + !(types.validateValueType meta.value meta.type) then + [ (mkError domain key "Value type mismatch: expected ${types.typeDescription meta.type}, got ${builtins.typeOf meta.value}") ] + else []; + + # Check description exists (warning only) + missingDescription = if !(meta ? description) || meta.description == "" then + [ (mkWarning domain key "Missing description (recommended for documentation)") ] + else []; + + in + missingValue ++ missingType ++ invalidType ++ typeMismatch ++ missingDescription; + + validateConfig = config: + let + # Validate structure + structureErrors = + if !config ? domains then + [ { domain = "ROOT"; key = ""; message = "Missing 'domains' attribute in config"; severity = "error"; } ] + else if !builtins.isAttrs config.domains then + [ { domain = "ROOT"; key = ""; message = "'domains' must be an attribute set"; severity = "error"; } ] + else + []; + + # Validate all settings if structure is valid + settingIssues = if structureErrors == [] then + lib.flatten ( + lib.mapAttrsToList (domain: settings: + if !builtins.isAttrs settings then + [ (mkError domain "" "Domain '${domain}' must be an attribute set of settings") ] + else + lib.flatten ( + lib.mapAttrsToList (key: meta: + validators.validateSetting domain key meta + ) settings + ) + ) config.domains + ) + else + []; + + allIssues = structureErrors ++ settingIssues; + errors = builtins.filter (issue: issue.severity == "error") allIssues; + warnings = builtins.filter (issue: issue.severity == "warning") allIssues; + + in { + inherit errors warnings; + isValid = errors == []; + errorCount = builtins.length errors; + warningCount = builtins.length warnings; + }; + + # Generate a human-readable validation report + # Throws an error if validation fails, otherwise returns the config + validationReport = config: + let + validation = validators.validateConfig config; + + formatIssue = issue: + let + location = if issue.key != "" then + "${issue.domain}.${issue.key}" + else if issue.domain != "" then + issue.domain + else + "ROOT"; + in + " [${issue.severity}] ${location}: ${issue.message}"; + + errorMessages = map formatIssue validation.errors; + warningMessages = map formatIssue validation.warnings; + + report = + if validation.errorCount > 0 then + builtins.throw '' + + Configuration validation failed with ${toString validation.errorCount} error(s): + + ${builtins.concatStringsSep "\n" errorMessages} + ${if validation.warningCount > 0 then + "\n\nWarnings (${toString validation.warningCount}):\n" + builtins.concatStringsSep "\n" warningMessages + else ""} + '' + else if validation.warningCount > 0 then + builtins.trace '' + Configuration validation warnings (${toString validation.warningCount}): + ${builtins.concatStringsSep "\n" warningMessages} + '' config + else + config; + + in report; + }; + +in validators diff --git a/packages/macos-defaults/scripts/apply-defaults.sh b/packages/macos-defaults/scripts/apply-defaults.sh new file mode 100644 index 0000000..bca64f8 --- /dev/null +++ b/packages/macos-defaults/scripts/apply-defaults.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# +# macOS Defaults Apply Script +# Applies system defaults configuration with proper error handling +# +# Environment variables: +# DRY_RUN=1 - Show what would be done without making changes +# VERBOSE=1 - Show detailed output for each setting + +set -euo pipefail + +readonly SCRIPT_NAME=$(basename "$0") +readonly DRY_RUN=${DRY_RUN:-0} +readonly VERBOSE=${VERBOSE:-0} + +if [[ -t 1 ]]; then + readonly RED='\033[0;31m' + readonly GREEN='\033[0;32m' + readonly YELLOW='\033[1;33m' + readonly BLUE='\033[0;34m' + readonly NC='\033[0m' # No Color +else + readonly RED='' + readonly GREEN='' + readonly YELLOW='' + readonly BLUE='' + readonly NC='' +fi + +declare -i SUCCESS_COUNT=0 +declare -i FAIL_COUNT=0 +declare -i TOTAL_COUNT=0 + +log_info() { + echo -e "${GREEN}[INFO]${NC} $*" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $*" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $*" >&2 +} + +log_verbose() { + if [[ $VERBOSE -eq 1 ]]; then + echo -e "${BLUE}[DEBUG]${NC} $*" + fi +} + +validate_platform() { + if [[ "$(uname)" != "Darwin" ]]; then + log_error "This script only runs on macOS" + exit 1 + fi + log_verbose "Platform check: macOS ✓" +} + +check_dependencies() { + if ! command -v defaults &> /dev/null; then + log_error "defaults command not found" + exit 1 + fi + log_verbose "Dependencies check: defaults command ✓" +} + +# Apply a single setting with error handling +# Args: $1=domain $2=key $3=type_flag $4=value +apply_setting() { + local domain=$1 + local key=$2 + local type_flag=$3 + shift 3 + local value="$*" + + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + + log_verbose "Setting ${domain} ${key} ${type_flag} ${value}" + + if [[ $DRY_RUN -eq 1 ]]; then + echo " Would run: defaults write ${domain} ${key} ${type_flag} ${value}" + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + return 0 + fi + + if defaults write "${domain}" "${key}" "${type_flag}" ${value} 2>/dev/null; then + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + return 0 + else + log_warn "Failed to set ${domain}.${key}" + FAIL_COUNT=$((FAIL_COUNT + 1)) + return 1 + fi +} + +restart_services() { + if [[ $DRY_RUN -eq 1 ]]; then + log_info "Would restart system services (Dock, Finder, SystemUIServer)" + return 0 + fi + + log_info "Restarting affected services..." + + local services=("Dock" "Finder" "SystemUIServer") + local restarted=0 + + for service in "${services[@]}"; do + if killall "${service}" 2>/dev/null; then + log_verbose "Restarted ${service}" + restarted=$((restarted + 1)) + else + log_verbose "${service} not running or could not be restarted" + fi + done + + if [[ $restarted -eq 0 ]]; then + log_warn "No services could be restarted" + else + log_verbose "Restarted ${restarted}/${#services[@]} services" + fi +} + +print_summary() { + local failed=$1 + + echo "" + echo "================================================================" + if [[ $DRY_RUN -eq 1 ]]; then + log_info "DRY RUN: Would apply ${TOTAL_COUNT} settings" + else + log_info "Applied ${SUCCESS_COUNT}/${TOTAL_COUNT} settings successfully" + + if [[ $failed -gt 0 ]]; then + log_warn "${failed} setting(s) failed to apply" + fi + fi + echo "================================================================" +} + +main() { + if [[ $DRY_RUN -eq 1 ]]; then + log_info "Running in DRY RUN mode (no changes will be made)" + fi + + if [[ $VERBOSE -eq 1 ]]; then + log_info "Verbose mode enabled" + fi + + log_verbose "Running pre-flight checks..." + validate_platform + check_dependencies + + log_info "Applying macOS defaults..." + echo "" + + # === Commands will be injected here by Nix === + @COMMANDS@ + # === End of injected commands === + + echo "" + + restart_services + + print_summary $FAIL_COUNT + + if [[ $DRY_RUN -eq 0 ]]; then + log_info "Done! Some changes may require logout/login to take full effect." + fi + + if [[ $FAIL_COUNT -gt 0 ]]; then + exit 1 + fi +} + +main "$@" diff --git a/packages/mango/default.nix b/packages/mango/default.nix new file mode 100644 index 0000000..874e963 --- /dev/null +++ b/packages/mango/default.nix @@ -0,0 +1,27 @@ +{ pkgs, mangoPackage }: + +let + inherit (pkgs) lib; +in +pkgs.symlinkJoin { + name = "mango-configured"; + paths = [ mangoPackage ]; + buildInputs = [ pkgs.makeWrapper ]; + + postBuild = '' + wrapProgram $out/bin/mango \ + --add-flags "-c ${./mango.conf}" + ''; + + passthru = { + unwrapped = mangoPackage; + version = mangoPackage.version or "unknown"; + }; + + meta = { + description = "MangoWC Wayland compositor with custom configuration"; + homepage = "https://github.com/DreamMaoMao/mango"; + license = lib.licenses.mit; + platforms = lib.platforms.linux; + }; +} diff --git a/packages/mango/mango.conf b/packages/mango/mango.conf new file mode 100644 index 0000000..37f081b --- /dev/null +++ b/packages/mango/mango.conf @@ -0,0 +1,52 @@ +# MangoWC Wayland Compositor Configuration + +# Force resolution to 1800x1169 (no scaling) +monitorrule=Virtual-1,0.5,1,tile,0,1,0,0,1800,1169,60 + +# Autostart Noctalia shell on startup (runs once at startup) +exec-once=noctalia-shell + +# Terminal - Alt+T to launch ghostty +bind=ALT,T,spawn,/run/current-system/sw/bin/ghostty + +# Alternative shortcuts for terminal +bind=ALT,Return,spawn,/run/current-system/sw/bin/ghostty + +# Kill focused window - Alt+Q +bind=ALT,Q,killclient + +# Alternative kill - Alt+W +bind=ALT,W,killclient + +# Exit compositor - Alt+Shift+E +bind=ALT+SHIFT,E,exit + +# Focus windows with arrow keys +bind=ALT,Left,movefocus,l +bind=ALT,Down,movefocus,d +bind=ALT,Up,movefocus,u +bind=ALT,Right,movefocus,r + +# Move windows with Alt+Shift+Arrows +bind=ALT+SHIFT,Left,movewindow,l +bind=ALT+SHIFT,Down,movewindow,d +bind=ALT+SHIFT,Up,movewindow,u +bind=ALT+SHIFT,Right,movewindow,r + +# Workspaces +bind=ALT,1,workspace,1 +bind=ALT,2,workspace,2 +bind=ALT,3,workspace,3 +bind=ALT,4,workspace,4 + +# Move to workspace +bind=ALT+SHIFT,1,movetoworkspace,1 +bind=ALT+SHIFT,2,movetoworkspace,2 +bind=ALT+SHIFT,3,movetoworkspace,3 +bind=ALT+SHIFT,4,movetoworkspace,4 + +# Toggle floating +bind=ALT+SHIFT,Space,togglefloating + +# Fullscreen +bind=ALT,F,fullscreen diff --git a/aleksandars-mbp/vim.lua b/packages/nvim/config/init.lua similarity index 74% rename from aleksandars-mbp/vim.lua rename to packages/nvim/config/init.lua index 1291565..50c22c4 100644 --- a/aleksandars-mbp/vim.lua +++ b/packages/nvim/config/init.lua @@ -132,7 +132,56 @@ vim.api.nvim_create_user_command('Gsigns', 'Gitsigns toggle_signs', {}) vim.api.nvim_create_user_command('Gshow', 'Gitsigns show ', { nargs = 1 }) -- lsp -vim.lsp.enable({'elixirls', 'ts_ls'}) +-- All these servers are bundled privately with neovim +-- They don't pollute system PATH and project LSPs take precedence + +-- Configure LSP servers +vim.lsp.config('ts_ls', { + cmd = { 'typescript-language-server', '--stdio' }, + filetypes = { 'javascript', 'javascriptreact', 'typescript', 'typescriptreact' }, + root_markers = { 'package.json', 'tsconfig.json', 'jsconfig.json' }, +}) + +vim.lsp.config('elixirls', { + cmd = { 'elixir-ls' }, + filetypes = { 'elixir', 'eelixir', 'heex', 'surface' }, + root_markers = { 'mix.exs' }, +}) + +vim.lsp.config('lua_ls', { + cmd = { 'lua-language-server' }, + filetypes = { 'lua' }, + root_markers = { '.luarc.json', '.luarc.jsonc', '.luacheckrc', '.stylua.toml', 'stylua.toml', 'selene.toml', 'selene.yml' }, +}) + +vim.lsp.config('nil_ls', { + cmd = { 'nil' }, + filetypes = { 'nix' }, + root_markers = { 'flake.nix', 'default.nix', 'shell.nix' }, +}) + +vim.lsp.config('html', { + cmd = { 'vscode-html-language-server', '--stdio' }, + filetypes = { 'html' }, + root_markers = { 'package.json' }, +}) + +vim.lsp.config('cssls', { + cmd = { 'vscode-css-language-server', '--stdio' }, + filetypes = { 'css', 'scss', 'less' }, + root_markers = { 'package.json' }, +}) + +vim.lsp.config('jsonls', { + cmd = { 'vscode-json-language-server', '--stdio' }, + filetypes = { 'json', 'jsonc' }, + root_markers = { 'package.json' }, +}) + +-- Enable configured LSP servers (ESLint removed due to configuration issues) +vim.lsp.enable({ 'ts_ls', 'elixirls', 'lua_ls', 'nil_ls', 'html', 'cssls', 'jsonls' }) + +-- Enable LSP-based completion vim.lsp.completion.enable() vim.api.nvim_create_autocmd('LspAttach', { diff --git a/packages/nvim/default.nix b/packages/nvim/default.nix new file mode 100644 index 0000000..b4d52dd --- /dev/null +++ b/packages/nvim/default.nix @@ -0,0 +1,114 @@ +{ pkgs }: + +let + inherit (pkgs) lib; + + # Flexoki theme plugin + flexoki-neovim = pkgs.vimUtils.buildVimPlugin { + pname = "flexoki-neovim"; + version = "2025-08-26"; + src = pkgs.fetchurl { + url = "https://github.com/kepano/flexoki-neovim/archive/c3e2251e813d29d885a7cbbe9808a7af234d845d.tar.gz"; + sha256 = "sha256-ere25TqoPfyc2/6yQKZgAQhJXz1wxtI/VZj/0LGMwNw="; + }; + }; + + # Treesitter with language parsers + nvim-treesitter-configured = pkgs.vimPlugins.nvim-treesitter.withPlugins (p: with p; [ + tree-sitter-lua + tree-sitter-javascript + tree-sitter-typescript + tree-sitter-html + tree-sitter-nix + tree-sitter-elixir + tree-sitter-heex + ]); + + # LSP servers bundled with neovim (not exposed to system) + # These are kept private and only available to neovim + bundledLsps = [ + pkgs.elixir-ls # Elixir LSP + pkgs.nodePackages.typescript-language-server # TypeScript/JavaScript LSP + pkgs.nodePackages.vscode-langservers-extracted # HTML/CSS/JSON LSPs (no ESLint) + pkgs.lua-language-server # Lua LSP + pkgs.nil # Nix LSP + ]; + + # Base neovim configuration + configuredNeovim = pkgs.neovim.override { + configure = { + customRC = '' + lua << EOF + ${builtins.readFile ./config/init.lua} + EOF + ''; + packages.myPlugins = with pkgs.vimPlugins; { + start = [ + fzf-lua + gitsigns-nvim + nvim-treesitter-configured + flexoki-neovim + ]; + }; + }; + + withRuby = false; + withPython3 = false; + withNodeJs = false; + }; + + # Combine neovim with bundled LSP servers + # LSPs are placed in libexec to keep them private + neovimWithLsps = pkgs.runCommand "neovim-with-lsps-${configuredNeovim.version}" + { + buildInputs = [ pkgs.makeWrapper ]; + passthru = { + unwrapped = pkgs.neovim; + version = configuredNeovim.version; + lsps = bundledLsps; # Expose list of bundled LSPs for inspection + }; + meta = { + description = "Neovim with custom configuration, plugins, and bundled LSP servers"; + longDescription = '' + Neovim configured with Flexoki theme, Treesitter, fzf-lua, and gitsigns. + Includes bundled LSP servers that are only available to Neovim: + - elixir-ls (Elixir) + - typescript-language-server (TypeScript/JavaScript) + - vscode-langservers-extracted (HTML/CSS/JSON) + - lua-language-server (Lua) + - nil (Nix) + + LSP servers are kept private and don't pollute the system PATH. + Project-specific LSPs in PATH take precedence over bundled ones. + ''; + homepage = "https://neovim.io"; + license = lib.licenses.asl20; + platforms = lib.platforms.darwin; + mainProgram = "nvim"; + }; + } + '' + # Copy neovim installation + mkdir -p $out + ${pkgs.xorg.lndir}/bin/lndir -silent ${configuredNeovim} $out + + # Create private directory for bundled LSPs (not exposed in bin/) + mkdir -p $out/libexec/nvim-lsps + + # Symlink bundled LSP binaries to private location + ${lib.concatMapStringsSep "\n" (lsp: '' + if [ -d "${lsp}/bin" ]; then + ${pkgs.xorg.lndir}/bin/lndir -silent "${lsp}/bin" $out/libexec/nvim-lsps + fi + '') bundledLsps} + + # Remove the old nvim binary and create wrapper + rm $out/bin/nvim + + # Wrap nvim to include bundled LSPs in PATH + # Project LSPs (from parent PATH) are checked first, then bundled ones + makeWrapper ${configuredNeovim}/bin/nvim $out/bin/nvim \ + --prefix PATH : "$out/libexec/nvim-lsps" + ''; +in +neovimWithLsps diff --git a/aleksandars-mbp/bin/dev b/packages/scripts/bin/dev similarity index 100% rename from aleksandars-mbp/bin/dev rename to packages/scripts/bin/dev diff --git a/aleksandars-mbp/bin/extract b/packages/scripts/bin/extract similarity index 100% rename from aleksandars-mbp/bin/extract rename to packages/scripts/bin/extract diff --git a/aleksandars-mbp/bin/gh-pr b/packages/scripts/bin/gh-pr similarity index 100% rename from aleksandars-mbp/bin/gh-pr rename to packages/scripts/bin/gh-pr diff --git a/aleksandars-mbp/bin/gh-url b/packages/scripts/bin/gh-url similarity index 100% rename from aleksandars-mbp/bin/gh-url rename to packages/scripts/bin/gh-url diff --git a/aleksandars-mbp/bin/git-rank-contributors b/packages/scripts/bin/git-rank-contributors similarity index 100% rename from aleksandars-mbp/bin/git-rank-contributors rename to packages/scripts/bin/git-rank-contributors diff --git a/aleksandars-mbp/bin/git-recent b/packages/scripts/bin/git-recent similarity index 100% rename from aleksandars-mbp/bin/git-recent rename to packages/scripts/bin/git-recent diff --git a/aleksandars-mbp/bin/git-wtf b/packages/scripts/bin/git-wtf similarity index 100% rename from aleksandars-mbp/bin/git-wtf rename to packages/scripts/bin/git-wtf diff --git a/aleksandars-mbp/bin/headers b/packages/scripts/bin/headers similarity index 100% rename from aleksandars-mbp/bin/headers rename to packages/scripts/bin/headers diff --git a/aleksandars-mbp/bin/mdc b/packages/scripts/bin/mdc similarity index 100% rename from aleksandars-mbp/bin/mdc rename to packages/scripts/bin/mdc diff --git a/aleksandars-mbp/bin/notes b/packages/scripts/bin/notes similarity index 100% rename from aleksandars-mbp/bin/notes rename to packages/scripts/bin/notes diff --git a/aleksandars-mbp/bin/update-dot b/packages/scripts/bin/update-dot similarity index 62% rename from aleksandars-mbp/bin/update-dot rename to packages/scripts/bin/update-dot index 7a960ea..08de9e5 100755 --- a/aleksandars-mbp/bin/update-dot +++ b/packages/scripts/bin/update-dot @@ -12,17 +12,17 @@ cd "$dotfiles_dir" # Show current lock info echo "Current versions:" -nix flake metadata --no-write-lock-file | grep -E "(nixpkgs|home-manager)" || true +nix flake metadata --no-write-lock-file | grep -E "nixpkgs" || true # Update inputs nix flake update echo "" echo "→ Applying updated configuration..." -nix run --impure home-manager/master -- -b bak switch --flake .#rastasheep@aleksandars-mbp +nix profile upgrade --impure ".*aleksandars-mbp.*" echo "" echo "✓ Dotfiles updated and applied successfully!" echo "" echo "Updated versions:" -nix flake metadata --no-write-lock-file | grep -E "(nixpkgs|home-manager)" || true \ No newline at end of file +nix flake metadata --no-write-lock-file | grep -E "nixpkgs" || true \ No newline at end of file diff --git a/aleksandars-mbp/bin/upgrade-nix b/packages/scripts/bin/upgrade-nix similarity index 100% rename from aleksandars-mbp/bin/upgrade-nix rename to packages/scripts/bin/upgrade-nix diff --git a/packages/scripts/default.nix b/packages/scripts/default.nix new file mode 100644 index 0000000..5d3af87 --- /dev/null +++ b/packages/scripts/default.nix @@ -0,0 +1,44 @@ +{ pkgs, configuredGit ? null }: + +# Scripts package with bundled dependencies +# If configuredGit is provided, scripts will use it instead of base git +# This ensures scripts use your custom git config when part of a machine bundle + +let + inherit (pkgs) lib; +in +pkgs.stdenv.mkDerivation { + name = "scripts"; + src = ./bin; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + + installPhase = '' + mkdir -p $out/bin + cp -r $src/* $out/bin/ + chmod +x $out/bin/* + + # Wrap scripts that need external dependencies + for script in $out/bin/*; do + wrapProgram "$script" \ + --prefix PATH : ${lib.makeBinPath ([ + (if configuredGit != null then configuredGit else pkgs.git) + pkgs.fzf + pkgs.findutils + pkgs.gnused + pkgs.coreutils + pkgs.bash + ])} + done + ''; + + passthru = { + git = if configuredGit != null then configuredGit else pkgs.git; + }; + + meta = { + description = "Custom shell scripts with bundled dependencies"; + homepage = "https://github.com/rastasheep/dotfiles"; + platforms = lib.platforms.darwin; + }; +} diff --git a/packages/starship/default.nix b/packages/starship/default.nix new file mode 100644 index 0000000..f58ec66 --- /dev/null +++ b/packages/starship/default.nix @@ -0,0 +1,10 @@ +{ pkgs }: + +let + dotfilesLib = import ../../lib { inherit pkgs; }; +in +dotfilesLib.wrapWithConfig { + package = pkgs.starship; + configPath = ./starship.toml; + envVar = "STARSHIP_CONFIG"; +} diff --git a/packages/starship/starship.toml b/packages/starship/starship.toml new file mode 100644 index 0000000..80e21a7 --- /dev/null +++ b/packages/starship/starship.toml @@ -0,0 +1,6 @@ +add_newline = false +format = "$directory$git_branch$git_status$character" + +[git_branch] +format = " [$branch]($style)" +symbol = "" diff --git a/packages/tmux/default.nix b/packages/tmux/default.nix new file mode 100644 index 0000000..66e8e55 --- /dev/null +++ b/packages/tmux/default.nix @@ -0,0 +1,28 @@ +{ pkgs }: + +# Note: tmux uses --add-flags pattern, not env var, so we keep custom implementation +let + inherit (pkgs) lib; +in +pkgs.symlinkJoin { + name = "tmux-configured"; + paths = [ pkgs.tmux ]; + buildInputs = [ pkgs.makeWrapper ]; + + postBuild = '' + wrapProgram $out/bin/tmux \ + --add-flags "-f ${./tmux.conf}" + ''; + + passthru = { + unwrapped = pkgs.tmux; + version = pkgs.tmux.version; + }; + + meta = { + description = "Tmux with custom configuration"; + homepage = "https://github.com/tmux/tmux"; + license = lib.licenses.bsd3; + platforms = lib.platforms.darwin; + }; +} diff --git a/packages/tmux/tmux.conf b/packages/tmux/tmux.conf new file mode 100644 index 0000000..d466862 --- /dev/null +++ b/packages/tmux/tmux.conf @@ -0,0 +1,85 @@ +# Set prefix to C-a +set -g prefix C-a +unbind C-b +bind a send-prefix + +# Vi mode +setw -g mode-keys vi + +# Start numbering at 1 +set -g base-index 1 +setw -g pane-base-index 1 + +# Resize settings +set -g aggressive-resize on +bind -r Up resize-pane -U 10 +bind -r Down resize-pane -D 10 +bind -r Left resize-pane -L 10 +bind -r Right resize-pane -R 10 + +# Escape time +set -s escape-time 50 + +# History +set -g history-limit 10000 + +# Terminal +set -g default-terminal "xterm-256color" +set -ga terminal-overrides ",*256col*:Tc" + +# Mouse +set -g mouse on + +# Bind prefix twice to switch between last windows +bind-key C-a last-window + +# Scroll stuff +set -g terminal-overrides 'xterm*:smcup@:rmcup@' + +# Open new window in same dir +bind c new-window -c "#{pane_current_path}" + +# v and y like vi in copy-mode +bind-key -T copy-mode-vi 'v' send -X begin-selection +bind-key -T copy-mode-vi 'Y' send -X copy-pipe-and-cancel "reattach-to-user-namespace pbcopy" + +# p for paste +unbind p +bind p paste-buffer + +# Enable wm window titles +set -g set-titles on + +# WM window title string (uses statusbar variables) +set -g set-titles-string "tmux.#I.#W" + +# Disable auto renaming +setw -g automatic-rename off + +# Statusbar +set -g display-time 2000 +set -g status-left '' +set -g status-right "#( date +' %H:%M ')" + +# Split pane hotkeys +bind-key \\ split-window -h +bind-key - split-window -v + +#### Colours + +# Default statusbar colors +set -g status-style bg=default,fg=white + +# Highlight active window +set -g window-status-current-style fg=red,bg=default + +# Pane border +set -g pane-border-style fg=terminal,bg=default +set -g pane-active-border-style fg=yellow,bg=default + +# Message text +set -g message-style fg=brightred,bg=black + +# Pane number display +set -g display-panes-active-colour blue +set -g display-panes-colour brightred diff --git a/packages/zsh/default.nix b/packages/zsh/default.nix new file mode 100644 index 0000000..a4c87d4 --- /dev/null +++ b/packages/zsh/default.nix @@ -0,0 +1,45 @@ +{ pkgs }: + +# Note: zsh requires custom installPhase to generate zshenv, can't use lib.buildConfig directly +let + inherit (pkgs) lib; + + zshConfig = pkgs.stdenvNoCC.mkDerivation { + name = "zsh-config"; + src = ./.; + dontBuild = true; + + installPhase = '' + mkdir -p $out/etc + cp ${./zshrc} $out/etc/zshrc + + # Create a .zshenv that sources our config + cat > $out/etc/zshenv <<'EOF' +# Source the custom zshrc +source ${./zshrc} +EOF + ''; + }; +in +pkgs.buildEnv { + name = "zsh-configured"; + paths = [ + pkgs.zsh + pkgs.zsh-autosuggestions + pkgs.zsh-completions + zshConfig + ]; + pathsToLink = [ "/bin" "/share" "/etc" ]; + + passthru = { + unwrapped = pkgs.zsh; + version = pkgs.zsh.version; + }; + + meta = { + description = "Zsh with custom configuration and plugins"; + homepage = "https://www.zsh.org/"; + license = lib.licenses.mit; + platforms = lib.platforms.darwin; + }; +} diff --git a/packages/zsh/zshrc b/packages/zsh/zshrc new file mode 100644 index 0000000..eaff7d4 --- /dev/null +++ b/packages/zsh/zshrc @@ -0,0 +1,125 @@ +# Load nix-daemon if available +if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then + . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' +fi + +# Completion settings +autoload -Uz compinit && compinit + +# Matches case insensitive for lowercase +zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' + +# Pasting with tabs doesn't perform completion +zstyle ':completion:*' insert-tab pending + +# Shell options +setopt NO_BG_NICE # don't nice background tasks +setopt NO_HUP +setopt NO_LIST_BEEP +setopt LOCAL_OPTIONS # allow functions to have local options +setopt LOCAL_TRAPS # allow functions to have local traps +setopt HIST_VERIFY +setopt PROMPT_SUBST +setopt CORRECT +setopt COMPLETE_IN_WORD +setopt IGNORE_EOF +setopt AUTO_CD + +# History settings +setopt APPEND_HISTORY # adds history +setopt INC_APPEND_HISTORY SHARE_HISTORY # adds history incrementally and share it across sessions +setopt HIST_IGNORE_ALL_DUPS # don't record dupes in history +setopt HIST_REDUCE_BLANKS + +# Don't expand aliases _before_ completion has finished +setopt complete_aliases + +# Stop correcting me! +unsetopt correct_all + +# Foreground the last backgrounded job using ctrl+z +fancy-ctrl-z () { + if [[ $#BUFFER -eq 0 ]]; then + BUFFER="fg" + zle accept-line + else + zle push-input + zle clear-screen + fi +} + +zle -N fancy-ctrl-z +bindkey '^Z' fancy-ctrl-z + +# Default keymap +bindkey -e + +# Session variables +export LANG=en_US.UTF-8 + +# History config +HISTFILE=~/.zsh_history +HISTSIZE=50000 +SAVEHIST=10000 + +# Initialize direnv if available +if command -v direnv &> /dev/null; then + eval "$(direnv hook zsh)" +fi + +# Initialize fzf if available +if command -v fzf &> /dev/null; then + source <(fzf --zsh 2>/dev/null || true) +fi + +# Initialize starship prompt if available +if command -v starship &> /dev/null; then + eval "$(starship init zsh)" +fi + +# Initialize dircolors for colorized ls output +if command -v dircolors &> /dev/null; then + # Try to use bundled dircolors config + if [ -f /nix/store/*/share/dircolors/dircolors ]; then + eval "$(dircolors -b /nix/store/*/share/dircolors/dircolors)" + else + eval "$(dircolors -b)" + fi + + # Enable colored ls by default + alias ls='ls --color=auto' + alias grep='grep --color=auto' + alias fgrep='fgrep --color=auto' + alias egrep='egrep --color=auto' +fi + +# Load zsh-autosuggestions if available +if [ -f /nix/store/*/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then + source /nix/store/*/share/zsh-autosuggestions/zsh-autosuggestions.zsh +fi + +# Shell aliases +alias ..='cd ..' +alias ack='rg' +alias dc='docker compose' +alias df='df -hT' +alias e='vim' +alias f='fg' +alias g='git' +alias history='fc -El 1' +alias j='jobs' +alias ll='ls -lah' + +# Machine-specific aliases +alias apply-dot='cd ~/src/github.com/rastasheep/dotfiles && nix profile upgrade --impure ".*aleksandars-mbp.*"' +alias dev-vpn='sudo openvpn --config ~/Google\ Drive/My\ Drive/fhc-dev-vpn.ovpn' +alias dev='source dev' + +# Git function - defaults to 'git st' when no args +function git() { + if [ $# -eq 0 ]; then + command git st + else + command git "$@" + fi +} diff --git a/pkgs/hammerspoon.nix b/pkgs/hammerspoon.nix deleted file mode 100644 index c7d1eae..0000000 --- a/pkgs/hammerspoon.nix +++ /dev/null @@ -1,38 +0,0 @@ -{ lib -, stdenvNoCC -, fetchurl -, unzip -}: - - stdenvNoCC.mkDerivation rec { - pname = "hammerspoon"; - version = "1.0.0"; - - src = fetchurl { - name = "${pname}-${version}-source.zip"; - url = "https://github.com/Hammerspoon/hammerspoon/releases/download/${version}/Hammerspoon-${version}.zip"; - sha256 = "sha256-XbcCtV2kfcMG6PWUjZHvhb69MV3fopQoMioK9+1+an4="; - }; - - dontPatch = true; - dontConfigure = true; - dontBuild = true; - dontFixup = true; - - nativeBuildInputs = [ unzip ]; - - installPhase = '' - runHook preInstall - - mkdir -p $out/Applications - cp -r ../Hammerspoon.app $out/Applications/ - runHook postInstall - ''; - - meta = with lib; { - homepage = "https://www.hammerspoon.org"; - description = "Staggeringly powerful macOS desktop automation with Lua"; - license = licenses.mit; - platforms = platforms.darwin; - }; - }