diff --git a/.ansible-lint b/.ansible-lint index fb0464f..29979ae 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -4,6 +4,12 @@ # enforce more style. Use yamllint (.yamllint) for YAML formatting concerns. profile: min +enable_list: + - use-include-tasks + +rulesdir: + - .ansible-rules/ + exclude_paths: - roles/nvim/files/ # deployed nvim config (lua/yaml), not Ansible content - .github/ @@ -12,12 +18,11 @@ exclude_paths: skip_list: - yaml -# Modules the lint env can't resolve: `yay` is this repo's custom module -# (library/yay); the rest live in the community.general collection, which we -# don't install just to lint. Mock them so syntax-check passes. +# Modules the lint env can't resolve; we mock them so syntax-check passes. mock_modules: - - yay + - kewlfft.aur.aur - snap + - community.general.snap - community.general.npm - community.general.pacman - community.general.flatpak diff --git a/.ansible-rules/use_include_tasks.py b/.ansible-rules/use_include_tasks.py new file mode 100644 index 0000000..f7b4238 --- /dev/null +++ b/.ansible-rules/use_include_tasks.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from ansiblelint.rules import AnsibleLintRule + +if TYPE_CHECKING: + from ansiblelint.file_utils import Lintable + from ansiblelint.utils import Task + +class UseIncludeTasksRule(AnsibleLintRule): + """Avoid using import_tasks, use include_tasks instead to prevent static module parsing issues on platforms without dependency collections.""" + id = "use-include-tasks" + description = "Use include_tasks instead of import_tasks to allow dynamic evaluation and skip loading platform-specific files on incompatible hosts." + severity = "MEDIUM" + tags = ["custom", "task"] + version_added = "1.0.0" + version_changed = "1.0.0" + + def matchtask(self, task: Task, file: Lintable | None = None) -> bool | str: + # task is an instance of ansiblelint.utils.Task + normalized_task = getattr(task, "normalized_task", {}) + action = normalized_task.get("action", {}) + module = action.get("__ansible_module__") + if module in ["import_tasks", "ansible.builtin.import_tasks"]: + return "Avoid using import_tasks, use include_tasks instead to prevent static module parsing issues on platforms without dependency collections." + return False diff --git a/.gitignore b/.gitignore index 514d95a..4856d39 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ tmp/ .stack-work *.lock roles/nvim/files/plugin/packer_compiled.lua +*.pyc # ansible-lint writes mocked modules here (mock_modules in .ansible-lint), and # ansible uses it for local galaxy installs / tmp — all generated, not source. diff --git a/bin/dot-bootstrap b/bin/dot-bootstrap index cf0b0f9..4d345a3 100755 --- a/bin/dot-bootstrap +++ b/bin/dot-bootstrap @@ -51,9 +51,18 @@ install_collections() { echo "Installing community.general collection..." ansible-galaxy collection install community.general --ignore-certs fi + + if ! ansible-galaxy collection list kewlfft.aur >/dev/null 2>&1; then + echo "Installing kewlfft.aur collection..." + ansible-galaxy collection install kewlfft.aur --ignore-certs + fi } run_playbook() { + if [[ "$1" == "--no-playbook" ]] || [[ "$1" == "--only-prereqs" ]]; then + echo "Skipping playbook execution." + return 0 + fi if [[ "$1" == "--check" ]] || [[ "$1" == "dry-run" ]]; then echo "Running dry-run..." shift diff --git a/justfile b/justfile index 0d4b5e2..cc5ef6f 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,10 @@ mod nvim 'roles/nvim/files' +# Install bootstrap prerequisites and Ansible Galaxy collections without running the playbook +bootstrap: + bin/dot-bootstrap --no-playbook + # Auto-format in place. Pass a hook id to run just that one, e.g. `just fmt stylua-github`. fmt HOOK="": prek run --hook-stage manual {{ HOOK }} --all-files diff --git a/roles/fzf/tasks/main.yml b/roles/fzf/tasks/main.yml index d2f4f9b..289ab53 100644 --- a/roles/fzf/tasks/main.yml +++ b/roles/fzf/tasks/main.yml @@ -1,4 +1,4 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" - name: Update fzf diff --git a/roles/git/tasks/main.yml b/roles/git/tasks/main.yml index d69d286..19ac073 100644 --- a/roles/git/tasks/main.yml +++ b/roles/git/tasks/main.yml @@ -1,4 +1,4 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" - name: Check if gitconfig exists diff --git a/roles/haskell/tasks/main.yml b/roles/haskell/tasks/main.yml index 8a7b709..b8f277c 100644 --- a/roles/haskell/tasks/main.yml +++ b/roles/haskell/tasks/main.yml @@ -1,4 +1,4 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" - name: create stack directory diff --git a/roles/nvim/files/lazy-lock.json b/roles/nvim/files/lazy-lock.json index 74f8719..41ddd45 100644 --- a/roles/nvim/files/lazy-lock.json +++ b/roles/nvim/files/lazy-lock.json @@ -3,6 +3,7 @@ "HowMuch": { "branch": "master", "commit": "787ead49e6311dc77b8e07a3aca79cbd6feab34c" }, "LuaSnip": { "branch": "master", "commit": "642b0c595e11608b4c18219e93b88d7637af27bc" }, "actions-preview.nvim": { "branch": "master", "commit": "0ac9c2aa3cfc8c885321c0862b50b6b1c3392405" }, + "ansi.nvim": { "branch": "main", "commit": "95de464c315ecbe9d7ca3d9a203ef85def0cf6aa" }, "baleia.nvim": { "branch": "main", "commit": "710537ff5cd669c5a76c5f5b6a9169fd9b913d18" }, "black-nvim": { "branch": "master", "commit": "8fb3efc562b67269e6f31f8653297f826534fa4b" }, "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, diff --git a/roles/nvim/files/lua/plugins/spec/misc.lua b/roles/nvim/files/lua/plugins/spec/misc.lua index f83290e..a395bf3 100644 --- a/roles/nvim/files/lua/plugins/spec/misc.lua +++ b/roles/nvim/files/lua/plugins/spec/misc.lua @@ -81,4 +81,14 @@ return { }) end, }, + { + "0xferrous/ansi.nvim", + config = function() + require("ansi").setup({ + auto_enable = false, -- Auto-enable for configured filetypes + auto_enable_stdin = true, -- Auto-enable for piped stdin content + filetypes = { "log", "ansi" }, + }) + end, + }, } diff --git a/roles/nvim/files/tests/spec/copilot_spec.lua b/roles/nvim/files/tests/spec/copilot_spec.lua new file mode 100644 index 0000000..2464945 --- /dev/null +++ b/roles/nvim/files/tests/spec/copilot_spec.lua @@ -0,0 +1,108 @@ +-- tests/spec/copilot_spec.lua + +describe("Copilot Tab completion integration", function() + -- Load blink.cmp so its configuration is loaded + require("lazy").load({ plugins = { "blink.cmp" } }) + + it("should retrieve the Tab action from blink configuration", function() + local keymap = require("blink.cmp.config").keymap + assert.is_not_nil(keymap) + assert.is_not_nil(keymap[""]) + assert.is_table(keymap[""]) + assert.is_function(keymap[""][1]) + end) + + it("should prioritize blink menu acceptance when blink menu is visible", function() + local keymap = require("blink.cmp.config").keymap + local tab_action = keymap[""][1] + + -- Case 1: blink menu visible, snippet active + local accept_called = false + local select_and_accept_called = false + local mock_cmp = { + is_visible = function() + return true + end, + snippet_active = function() + return true + end, + accept = function() + accept_called = true + return true + end, + select_and_accept = function() + select_and_accept_called = true + return true + end, + } + + local res = tab_action(mock_cmp) + assert.is_true(res) + assert.is_true(accept_called) + assert.is_false(select_and_accept_called) + + -- Case 2: blink menu visible, snippet not active + accept_called = false + mock_cmp.snippet_active = function() + return false + end + res = tab_action(mock_cmp) + assert.is_true(res) + assert.is_false(accept_called) + assert.is_true(select_and_accept_called) + end) + + it("should accept Copilot suggestion when blink menu is closed but Copilot is visible", function() + local keymap = require("blink.cmp.config").keymap + local tab_action = keymap[""][1] + + local mock_cmp = { + is_visible = function() + return false + end, + } + + -- Stub require("copilot.suggestion") + local copilot_suggestion_accept_called = false + package.loaded["copilot.suggestion"] = { + is_visible = function() + return true + end, + accept = function() + copilot_suggestion_accept_called = true + return true + end, + } + + local res = tab_action(mock_cmp) + assert.is_true(res) + assert.is_true(copilot_suggestion_accept_called) + + -- Cleanup package.loaded stub + package.loaded["copilot.suggestion"] = nil + end) + + it("should fall back when both blink menu and Copilot suggestion are not visible", function() + local keymap = require("blink.cmp.config").keymap + local tab_action = keymap[""][1] + + local mock_cmp = { + is_visible = function() + return false + end, + } + + -- Stub require("copilot.suggestion") to return not visible + package.loaded["copilot.suggestion"] = { + is_visible = function() + return false + end, + } + + local res = tab_action(mock_cmp) + assert.is_false(res) + + -- Cleanup package.loaded stub + package.loaded["copilot.suggestion"] = nil + end) +end) diff --git a/roles/nvim/tasks/main.yml b/roles/nvim/tasks/main.yml index 0e7ee4b..390d02b 100644 --- a/roles/nvim/tasks/main.yml +++ b/roles/nvim/tasks/main.yml @@ -1,4 +1,4 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" - name: Install bob-nvim with cargo-binstall diff --git a/roles/packages/tasks/arch.yml b/roles/packages/tasks/arch.yml index e4cd025..c236dd1 100644 --- a/roles/packages/tasks/arch.yml +++ b/roles/packages/tasks/arch.yml @@ -12,10 +12,8 @@ become_method: sudo - name: Install 1Password and CLI with yay - yay: + kewlfft.aur.aur: name: - 1password - 1password-cli state: present - become: true - become_method: sudo diff --git a/roles/packages/tasks/debian.yml b/roles/packages/tasks/debian.yml index f3d00ca..6474932 100644 --- a/roles/packages/tasks/debian.yml +++ b/roles/packages/tasks/debian.yml @@ -44,6 +44,11 @@ set_fact: missing_apt_packages: "{{ packages_debian_list | difference((dpkg_check.stdout_lines | default([])) | map('regex_replace', ' installed$', '') | list) }}" +- name: Check if snap is installed + stat: + path: /usr/bin/snap + register: snap_bin + - name: Check which snap packages are missing command: "snap list {{ packages_snap_list | join(' ') }}" register: snap_check @@ -51,10 +56,11 @@ changed_when: false become: false check_mode: no + when: snap_bin.stat.exists - name: Set missing snap packages fact set_fact: - missing_snap_packages: "{{ packages_snap_list | difference(snap_check.stdout_lines | default([]) | map('regex_replace', '^(\\S+)\\s+.*$', '\\1') | list) }}" + missing_snap_packages: "{{ (packages_snap_list | difference(snap_check.stdout_lines | default([]) | map('regex_replace', '^(\\S+)\\s+.*$', '\\1') | list)) if snap_bin.stat.exists else [] }}" - name: Report package differences (Check Mode Only) debug: @@ -73,13 +79,13 @@ when: not ansible_check_mode and missing_apt_packages | length > 0 - name: Install snap packages (classic confinement) - snap: + community.general.snap: name: "{{ missing_snap_packages }}" state: present classic: true become: true become_method: sudo - when: not ansible_check_mode and missing_snap_packages | length > 0 + when: not ansible_check_mode and snap_bin.stat.exists and missing_snap_packages | length > 0 - name: Install Prism Launcher via Flatpak community.general.flatpak: diff --git a/roles/packages/tasks/main.yml b/roles/packages/tasks/main.yml index 4ac07e5..8c0e81b 100644 --- a/roles/packages/tasks/main.yml +++ b/roles/packages/tasks/main.yml @@ -1,6 +1,6 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" -- import_tasks: arch.yml +- include_tasks: arch.yml when: ansible_os_family == "Archlinux" - name: Install global npm packages to user local bin diff --git a/roles/penrose/tasks/main.yml b/roles/penrose/tasks/main.yml index 0fbeaa2..80d9381 100644 --- a/roles/penrose/tasks/main.yml +++ b/roles/penrose/tasks/main.yml @@ -1,4 +1,4 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" - name: Fetch penrose (favilrose) git repo diff --git a/roles/programming/tasks/debian.yml b/roles/programming/tasks/debian.yml index 904f280..f23b682 100644 --- a/roles/programming/tasks/debian.yml +++ b/roles/programming/tasks/debian.yml @@ -22,6 +22,11 @@ vars: comp_packages: *comp_pkgs +- name: Check if snap is installed + stat: + path: /usr/bin/snap + register: snap_bin + - name: Check if go snap is missing command: "snap list go" register: go_snap_check @@ -29,10 +34,11 @@ changed_when: false become: false check_mode: no + when: snap_bin.stat.exists - name: Set missing go snap fact set_fact: - go_snap_missing: "{{ go_snap_check.rc is defined and go_snap_check.rc != 0 }}" + go_snap_missing: "{{ (go_snap_check.rc is defined and go_snap_check.rc != 0) if snap_bin.stat.exists else false }}" - name: Report missing programming components (Check Mode Only) debug: @@ -51,11 +57,11 @@ when: not ansible_check_mode and missing_comp_packages | length > 0 - name: Install go - snap: + community.general.snap: name: - go state: present classic: true become: true become_method: sudo - when: not ansible_check_mode and go_snap_missing + when: not ansible_check_mode and snap_bin.stat.exists and go_snap_missing diff --git a/roles/programming/tasks/main.yml b/roles/programming/tasks/main.yml index a3f30a1..387e288 100644 --- a/roles/programming/tasks/main.yml +++ b/roles/programming/tasks/main.yml @@ -1,6 +1,6 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" -- import_tasks: arch.yml +- include_tasks: arch.yml when: ansible_os_family == "Archlinux" - name: download rustup @@ -13,15 +13,21 @@ args: creates: "{{ dotfiles_user_home }}/.cargo/bin/rustup" -- name: Install cargo-binstall +- name: Install rust-analyzer component + command: "{{ dotfiles_user_home }}/.cargo/bin/rustup component add rust-analyzer" + changed_when: false + +- name: Install cargo-binstall and lspmux command: "{{ dotfiles_user_home }}/.cargo/bin/cargo install {{ item.name }}" args: creates: "{{ dotfiles_user_home }}/.cargo/bin/{{ item.exe }}" with_items: - name: cargo-binstall exe: cargo-binstall + - name: lspmux + exe: lspmux -- name: Install fun crates +- name: Install useful crates command: "{{ dotfiles_user_home }}/.cargo/bin/cargo-binstall -y {{ item.name }}" args: creates: "{{ dotfiles_user_home }}/.cargo/bin/{{ item.exe }}" @@ -56,6 +62,7 @@ file: dest: "{{ dotfiles_user_home }}/.cargo/bin/rls" state: absent + # - name: Checkout rust-analyzer # git: # repo: https://github.com/rust-analyzer/rust-analyzer.git @@ -66,3 +73,22 @@ # command: "cargo xtask install --server" # args: # chdir: "{{ dotfiles_user_home }}/git/utilities/rust-analyzer" + +- name: Ensure systemd user directory exists + file: + path: "{{ dotfiles_user_home }}/.config/systemd/user" + state: directory + +- name: Install lspmux systemd user service unit + template: + src: lspmux.service.j2 + dest: "{{ dotfiles_user_home }}/.config/systemd/user/lspmux.service" + +- name: Enable and start lspmux service + systemd: + name: lspmux + scope: user + enabled: yes + masked: no + state: started + daemon_reload: yes diff --git a/roles/programming/templates/lspmux.service.j2 b/roles/programming/templates/lspmux.service.j2 new file mode 100644 index 0000000..7a73948 --- /dev/null +++ b/roles/programming/templates/lspmux.service.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=Rust analyzer multiplex server + +[Service] +Type=simple +ExecStart={{ dotfiles_user_home }}/.cargo/bin/lspmux server +Environment="PATH={{ dotfiles_user_home }}/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +[Install] +WantedBy=default.target diff --git a/roles/zsh/tasks/main.yml b/roles/zsh/tasks/main.yml index 83adcb1..e7e8f35 100644 --- a/roles/zsh/tasks/main.yml +++ b/roles/zsh/tasks/main.yml @@ -1,6 +1,6 @@ -- import_tasks: debian.yml +- include_tasks: debian.yml when: ansible_os_family == "Debian" -- import_tasks: arch.yml +- include_tasks: arch.yml when: ansible_os_family == "Archlinux" - name: download oh-my-zsh installer @@ -44,8 +44,3 @@ state: link force: true when: zsh_config.stat.exists -#- name: Set zsh as default shell -# user: name="{{ dotfiles_user }}" shell=/usr/bin/zsh -# become: true -# become_user: "{{ dotfiles_user }}" -# become_method: sudo