fixes by claude
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
lazy-lock.json
|
|
||||||
.gitconfig.local
|
.gitconfig.local
|
||||||
|
|
||||||
k9s/.config/k9s/clusters
|
k9s/.config/k9s/clusters
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ dotfiles/
|
|||||||
├── curl/ # curl configuration
|
├── curl/ # curl configuration
|
||||||
├── git/ # Git configuration
|
├── git/ # Git configuration
|
||||||
├── nvim/ # Neovim configuration
|
├── nvim/ # Neovim configuration
|
||||||
|
├── nvidia-power-tray/ # GPU power-state tray indicator (Debian/GNOME only)
|
||||||
├── scripts/ # Utility scripts
|
├── scripts/ # Utility scripts
|
||||||
├── tmux/ # tmux configuration
|
├── tmux/ # tmux configuration
|
||||||
└── zsh/ # ZSH shell configuration
|
└── zsh/ # ZSH shell configuration
|
||||||
@@ -54,6 +55,8 @@ dotfiles/
|
|||||||
prevent conflicting Python environments.
|
prevent conflicting Python environments.
|
||||||
- gdb
|
- gdb
|
||||||
- reattach-to-user-namespace (macOS only)
|
- reattach-to-user-namespace (macOS only)
|
||||||
|
- [Claude Code](https://claude.com/claude-code) CLI, plus the
|
||||||
|
`@agentclientprotocol/claude-agent-acp` bridge used by agentic.nvim
|
||||||
|
|
||||||
### Language Servers & Linters
|
### Language Servers & Linters
|
||||||
|
|
||||||
@@ -76,6 +79,10 @@ dotfiles/
|
|||||||
- tree-sitter-cli
|
- tree-sitter-cli
|
||||||
- lesspipe
|
- lesspipe
|
||||||
- cbonsai
|
- cbonsai
|
||||||
|
- `nvidia-power-tray` (Debian/GNOME only): `python3-gi`,
|
||||||
|
`gir1.2-gtk-3.0`, `gir1.2-ayatanaappindicator3-0.1` and the
|
||||||
|
[AppIndicator support](https://extensions.gnome.org/extension/615/appindicator-support/)
|
||||||
|
GNOME extension. See `nvidia-power-tray/README.md`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -94,13 +101,15 @@ rustup component add rust-analyzer
|
|||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Install Cursor agent binary**
|
1. **Install the Claude ACP bridge**
|
||||||
|
|
||||||
Install the [Cursor](https://www.cursor.com/) `agent` binary to
|
agentic.nvim talks to Claude over ACP and does not manage the binary itself.
|
||||||
`~/.local/bin/agent` (used by avante.nvim for ACP). Then authenticate:
|
Install it globally, then authenticate once with the Claude Code CLI — no API
|
||||||
|
key is needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.local/bin/agent login
|
npm i -g @agentclientprotocol/claude-agent-acp
|
||||||
|
claude /login
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Install tmux-plugin-manager**
|
1. **Install tmux-plugin-manager**
|
||||||
@@ -164,6 +173,11 @@ cd ~/dotfiles
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
stow zsh tmux git nvim curl alacritty wallpaper
|
stow zsh tmux git nvim curl alacritty wallpaper
|
||||||
|
|
||||||
|
# Debian/GNOME only, then enable the user service
|
||||||
|
stow nvidia-power-tray
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now nvidia-power-tray.service
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Change the default shell
|
1. Change the default shell
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ background = "#faf4ed"
|
|||||||
dim_foreground = "#797593"
|
dim_foreground = "#797593"
|
||||||
bright_foreground = "#575279"
|
bright_foreground = "#575279"
|
||||||
|
|
||||||
|
# Upstream rose-pine-dawn uses #cecacd here, which is barely distinguishable
|
||||||
|
# from base. Use text/base instead so the cursor is legible.
|
||||||
[colors.cursor]
|
[colors.cursor]
|
||||||
text = "#575279"
|
text = "#faf4ed"
|
||||||
cursor = "#cecacd"
|
cursor = "#575279"
|
||||||
|
|
||||||
[colors.vi_mode_cursor]
|
[colors.vi_mode_cursor]
|
||||||
text = "#575279"
|
text = "#faf4ed"
|
||||||
cursor = "#cecacd"
|
cursor = "#d7827e"
|
||||||
|
|
||||||
[colors.search.matches]
|
[colors.search.matches]
|
||||||
foreground = "#797593"
|
foreground = "#797593"
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ background = "#191724"
|
|||||||
dim_foreground = "#908caa"
|
dim_foreground = "#908caa"
|
||||||
bright_foreground = "#e0def4"
|
bright_foreground = "#e0def4"
|
||||||
|
|
||||||
|
# Upstream rose-pine uses highlight_high (#524f67) here, which is barely
|
||||||
|
# distinguishable from base. Use text/base instead so the cursor is legible.
|
||||||
[colors.cursor]
|
[colors.cursor]
|
||||||
text = "#e0def4"
|
text = "#191724"
|
||||||
cursor = "#524f67"
|
cursor = "#e0def4"
|
||||||
|
|
||||||
[colors.vi_mode_cursor]
|
[colors.vi_mode_cursor]
|
||||||
text = "#e0def4"
|
text = "#191724"
|
||||||
cursor = "#524f67"
|
cursor = "#ebbcba"
|
||||||
|
|
||||||
[colors.search.matches]
|
[colors.search.matches]
|
||||||
foreground = "#908caa"
|
foreground = "#908caa"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=NVIDIA GPU power state tray indicator
|
||||||
|
Documentation=file:%h/dotfiles/nvidia-power-tray/README.md
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
After=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/nvidia-power-tray
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Expose an NVIDIA GPU's PCI power state as an AppIndicator tray icon.
|
||||||
|
|
||||||
|
The state comes from /sys/bus/pci/devices/<address>/power_state, which is a
|
||||||
|
plain sysfs read and (unlike nvidia-smi) never wakes a suspended GPU.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PCI_DEVICES = Path("/sys/bus/pci/devices")
|
||||||
|
PCI_IDS = Path("/usr/share/misc/pci.ids")
|
||||||
|
NVIDIA_VENDOR = "0x10de"
|
||||||
|
APP_ID = "nvidia-power-tray"
|
||||||
|
|
||||||
|
# Adwaita symbolic icons, themed automatically by the shell.
|
||||||
|
ICONS = {
|
||||||
|
"D0": "power-profile-performance-symbolic",
|
||||||
|
"D1": "power-profile-balanced-symbolic",
|
||||||
|
"D2": "power-profile-balanced-symbolic",
|
||||||
|
"D3hot": "power-profile-power-saver-symbolic",
|
||||||
|
"D3cold": "power-profile-power-saver-symbolic",
|
||||||
|
}
|
||||||
|
UNKNOWN_ICON = "power-profile-balanced-symbolic"
|
||||||
|
|
||||||
|
|
||||||
|
def is_display_controller(device: Path) -> bool:
|
||||||
|
"""PCI class 0x03xxxx covers VGA, XGA, 3D and other display controllers."""
|
||||||
|
try:
|
||||||
|
return device.joinpath("class").read_text().strip().startswith("0x03")
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_gpu() -> str:
|
||||||
|
"""Return the address of the first NVIDIA display controller found."""
|
||||||
|
candidates = []
|
||||||
|
for device in sorted(PCI_DEVICES.iterdir()):
|
||||||
|
try:
|
||||||
|
vendor = device.joinpath("vendor").read_text().strip()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if vendor.lower() != NVIDIA_VENDOR or not is_display_controller(device):
|
||||||
|
continue
|
||||||
|
# A bound nvidia/nouveau driver is the stronger signal; keep it first.
|
||||||
|
driver = read_driver(device.name)
|
||||||
|
candidates.append((driver is None, device.name))
|
||||||
|
if not candidates:
|
||||||
|
sys.exit("no NVIDIA display controller found under /sys/bus/pci/devices")
|
||||||
|
candidates.sort()
|
||||||
|
return candidates[0][1]
|
||||||
|
|
||||||
|
|
||||||
|
def read_attr(address: str, *parts: str) -> str:
|
||||||
|
try:
|
||||||
|
return PCI_DEVICES.joinpath(address, *parts).read_text().strip()
|
||||||
|
except OSError:
|
||||||
|
return "unavailable"
|
||||||
|
|
||||||
|
|
||||||
|
def read_driver(address: str):
|
||||||
|
try:
|
||||||
|
return os.path.basename(os.readlink(PCI_DEVICES / address / "driver"))
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def describe(address: str) -> str:
|
||||||
|
"""Human-readable device name, falling back to the raw PCI IDs.
|
||||||
|
|
||||||
|
Deliberately looks the IDs up in pci.ids rather than shelling out to
|
||||||
|
lspci: lspci reads the device's config space, which wakes a GPU that had
|
||||||
|
reached D3cold — the exact thing this indicator exists to observe.
|
||||||
|
"""
|
||||||
|
vendor = read_attr(address, "vendor").removeprefix("0x")
|
||||||
|
device = read_attr(address, "device").removeprefix("0x")
|
||||||
|
name = lookup_pci_ids(vendor, device)
|
||||||
|
return name or f"{vendor}:{device}"
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_pci_ids(vendor: str, device: str):
|
||||||
|
"""Resolve "10de", "2d19" to a device name using the pci.ids database."""
|
||||||
|
try:
|
||||||
|
with PCI_IDS.open(encoding="utf-8", errors="replace") as handle:
|
||||||
|
in_vendor = False
|
||||||
|
for line in handle:
|
||||||
|
if line.startswith("#") or not line.strip():
|
||||||
|
continue
|
||||||
|
if not line.startswith("\t"):
|
||||||
|
# Vendor lines are "10de NVIDIA Corporation".
|
||||||
|
if in_vendor:
|
||||||
|
return None # left our vendor block without a match
|
||||||
|
in_vendor = line.split(None, 1)[0].lower() == vendor
|
||||||
|
elif in_vendor and not line.startswith("\t\t"):
|
||||||
|
# Device lines are "\t2d19 GB206M [GeForce RTX 5060 ...]".
|
||||||
|
code, _, name = line.strip().partition(" ")
|
||||||
|
if code.lower() == device:
|
||||||
|
return name.strip()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Tray:
|
||||||
|
def __init__(self, address: str, interval: int):
|
||||||
|
self.address = address
|
||||||
|
self.interval = interval
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
from gi.repository import GLib, Gtk
|
||||||
|
|
||||||
|
self.GLib = GLib
|
||||||
|
self.Gtk = Gtk
|
||||||
|
self.indicator = self.make_indicator()
|
||||||
|
self.details = {}
|
||||||
|
self.indicator.set_menu(self.make_menu())
|
||||||
|
self.refresh()
|
||||||
|
GLib.timeout_add_seconds(interval, self.refresh)
|
||||||
|
|
||||||
|
def make_indicator(self):
|
||||||
|
import gi
|
||||||
|
|
||||||
|
for namespace in ("AyatanaAppIndicator3", "AppIndicator3"):
|
||||||
|
try:
|
||||||
|
gi.require_version(namespace, "0.1")
|
||||||
|
module = __import__("gi.repository", fromlist=[namespace])
|
||||||
|
appindicator = getattr(module, namespace)
|
||||||
|
break
|
||||||
|
except (ValueError, ImportError):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
sys.exit(
|
||||||
|
"no AppIndicator GIR found; install gir1.2-ayatanaappindicator3-0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
indicator = appindicator.Indicator.new(
|
||||||
|
APP_ID, UNKNOWN_ICON, appindicator.IndicatorCategory.HARDWARE
|
||||||
|
)
|
||||||
|
indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
|
||||||
|
return indicator
|
||||||
|
|
||||||
|
def make_menu(self):
|
||||||
|
menu = self.Gtk.Menu()
|
||||||
|
for key, label in (
|
||||||
|
("device", "Device"),
|
||||||
|
("address", "Address"),
|
||||||
|
("driver", "Driver"),
|
||||||
|
("power_state", "Power state"),
|
||||||
|
("runtime_status", "Runtime status"),
|
||||||
|
("runtime_pm", "Runtime PM"),
|
||||||
|
):
|
||||||
|
item = self.Gtk.MenuItem(label=f"{label}: …")
|
||||||
|
item.set_sensitive(False)
|
||||||
|
item.show()
|
||||||
|
menu.append(item)
|
||||||
|
self.details[key] = (label, item)
|
||||||
|
|
||||||
|
separator = self.Gtk.SeparatorMenuItem()
|
||||||
|
separator.show()
|
||||||
|
menu.append(separator)
|
||||||
|
|
||||||
|
refresh = self.Gtk.MenuItem(label="Refresh now")
|
||||||
|
refresh.connect("activate", lambda _item: self.refresh())
|
||||||
|
refresh.show()
|
||||||
|
menu.append(refresh)
|
||||||
|
|
||||||
|
quit_item = self.Gtk.MenuItem(label="Quit")
|
||||||
|
quit_item.connect("activate", lambda _item: self.Gtk.main_quit())
|
||||||
|
quit_item.show()
|
||||||
|
menu.append(quit_item)
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def set_detail(self, key: str, value: str) -> None:
|
||||||
|
label, item = self.details[key]
|
||||||
|
item.set_label(f"{label}: {value}")
|
||||||
|
|
||||||
|
def refresh(self) -> bool:
|
||||||
|
state = read_attr(self.address, "power_state")
|
||||||
|
runtime = read_attr(self.address, "power", "runtime_status")
|
||||||
|
control = read_attr(self.address, "power", "control")
|
||||||
|
driver = read_driver(self.address) or "none"
|
||||||
|
|
||||||
|
if state != self.state:
|
||||||
|
self.state = state
|
||||||
|
self.indicator.set_icon_full(
|
||||||
|
ICONS.get(state, UNKNOWN_ICON), f"GPU power state {state}"
|
||||||
|
)
|
||||||
|
# The second argument is a sizing hint, not a fallback value.
|
||||||
|
self.indicator.set_label(state, "D3cold")
|
||||||
|
self.indicator.set_title(f"GPU {self.address}: {state}")
|
||||||
|
|
||||||
|
self.set_detail("address", self.address)
|
||||||
|
self.set_detail("driver", driver)
|
||||||
|
self.set_detail("power_state", state)
|
||||||
|
self.set_detail("runtime_status", runtime)
|
||||||
|
self.set_detail("runtime_pm", control)
|
||||||
|
return True # keep the GLib timeout alive
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
self.set_detail("device", describe(self.address))
|
||||||
|
self.Gtk.main()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--address",
|
||||||
|
default=os.environ.get("NVIDIA_POWER_TRAY_ADDRESS"),
|
||||||
|
help="PCI address (e.g. 0000:01:00.0); autodetected when omitted",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--interval",
|
||||||
|
type=int,
|
||||||
|
default=int(os.environ.get("NVIDIA_POWER_TRAY_INTERVAL", "5")),
|
||||||
|
help="seconds between sysfs polls (default: 5)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--print",
|
||||||
|
action="store_true",
|
||||||
|
dest="print_once",
|
||||||
|
help="print the current state and exit, without starting the tray",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
address = args.address or find_gpu()
|
||||||
|
if not PCI_DEVICES.joinpath(address, "power_state").exists():
|
||||||
|
sys.exit(f"no power_state attribute for PCI device {address}")
|
||||||
|
|
||||||
|
if args.print_once:
|
||||||
|
print(f"{address} {read_attr(address, 'power_state')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
Tray(address, max(1, args.interval)).run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# nvidia-power-tray
|
||||||
|
|
||||||
|
A tray indicator showing the PCI power state of the NVIDIA GPU — `D0` when the
|
||||||
|
card is awake, `D3cold` when the driver has runtime-suspended it. Useful on
|
||||||
|
hybrid-graphics laptops to see at a glance whether something is holding the
|
||||||
|
discrete GPU on.
|
||||||
|
|
||||||
|
The state is read from `/sys/bus/pci/devices/<address>/power_state`. That is a
|
||||||
|
plain sysfs read and does not wake a suspended card — unlike `nvidia-smi` or
|
||||||
|
`lspci`, both of which touch the device and pull it back into `D0`. For the
|
||||||
|
same reason the device name in the menu is resolved from `/usr/share/misc/pci.ids`
|
||||||
|
instead of by shelling out to `lspci`.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
```text
|
||||||
|
.local/bin/nvidia-power-tray # the indicator
|
||||||
|
.config/systemd/user/nvidia-power-tray.service # runs it in the GNOME session
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- GNOME with the [AppIndicator support extension](https://extensions.gnome.org/extension/615/appindicator-support/)
|
||||||
|
(`[email protected]`) — GNOME Shell has no tray of its own
|
||||||
|
- `gir1.2-ayatanaappindicator3-0.1`, `gir1.2-gtk-3.0`, `python3-gi`
|
||||||
|
- `pciutils` for `/usr/share/misc/pci.ids` (already present on Debian)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install gir1.2-ayatanaappindicator3-0.1 gir1.2-gtk-3.0 python3-gi
|
||||||
|
```
|
||||||
|
|
||||||
|
The script also accepts the older `AppIndicator3` namespace if that is what the
|
||||||
|
system ships.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/dotfiles
|
||||||
|
stow nvidia-power-tray
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now nvidia-power-tray.service
|
||||||
|
```
|
||||||
|
|
||||||
|
The unit is `WantedBy=graphical-session.target`, so it starts with the GNOME
|
||||||
|
session and stops with it.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The icon reflects the state (`power-profile-performance-symbolic` for `D0`,
|
||||||
|
`power-profile-power-saver-symbolic` for `D3hot`/`D3cold`) and the label next to
|
||||||
|
it is the raw state string. The menu shows the device, PCI address, bound
|
||||||
|
driver, power state, runtime-PM status and runtime-PM control setting, plus
|
||||||
|
`Refresh now` and `Quit`.
|
||||||
|
|
||||||
|
The PCI address is autodetected: the first NVIDIA (vendor `0x10de`) display
|
||||||
|
controller, preferring one with a driver bound. Override it, or the 5 second
|
||||||
|
poll interval, with flags or environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvidia-power-tray --address 0000:01:00.0 --interval 10
|
||||||
|
NVIDIA_POWER_TRAY_ADDRESS=0000:01:00.0 NVIDIA_POWER_TRAY_INTERVAL=10 nvidia-power-tray
|
||||||
|
```
|
||||||
|
|
||||||
|
`nvidia-power-tray --print` prints `<address> <state>` once and exits without
|
||||||
|
touching GTK — handy for testing or for a shell prompt/status bar.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status nvidia-power-tray.service
|
||||||
|
journalctl --user -u nvidia-power-tray.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
If the service runs but no icon appears, the AppIndicator extension is probably
|
||||||
|
disabled: `gnome-extensions list --enabled | grep appindicator`.
|
||||||
+35
-10
@@ -9,8 +9,8 @@ linting, formatting, and AI features. Leader key is `<Space>`. Local leader is `
|
|||||||
| ------------ | ------ | ---------------------------------------- |
|
| ------------ | ------ | ---------------------------------------- |
|
||||||
| `<leader>sn` | Normal | Save without formatting |
|
| `<leader>sn` | Normal | Save without formatting |
|
||||||
| `<leader>w` | Normal | Toggle line wrapping |
|
| `<leader>w` | Normal | Toggle line wrapping |
|
||||||
| `<leader>tt` | Normal | Alternate light/dark theme (Themery) |
|
| `<leader>tt` | Normal | Alternate light/dark background |
|
||||||
| `x` | Nor/Vis| Remove char under cursor without yank |
|
| `x` | Normal | Remove char under cursor without yank |
|
||||||
| `p` | Visual | Paste without yanking underlying text |
|
| `p` | Visual | Paste without yanking underlying text |
|
||||||
| `<` / `>` | Visual | Indent left / right |
|
| `<` / `>` | Visual | Indent left / right |
|
||||||
| `gl` | Normal | Jump to last change |
|
| `gl` | Normal | Jump to last change |
|
||||||
@@ -22,7 +22,7 @@ linting, formatting, and AI features. Leader key is `<Space>`. Local leader is `
|
|||||||
| ------------ | ------ | ---------------------------------------- |
|
| ------------ | ------ | ---------------------------------------- |
|
||||||
| `<Tab>` | Normal | Switch to next buffer |
|
| `<Tab>` | Normal | Switch to next buffer |
|
||||||
| `<S-Tab>` | Normal | Switch to previous buffer |
|
| `<S-Tab>` | Normal | Switch to previous buffer |
|
||||||
| `<leader>x` | Normal | Close buffer |
|
| `<leader>xb` | Normal | Close buffer |
|
||||||
| `<leader>b` | Normal | Open new buffer |
|
| `<leader>b` | Normal | Open new buffer |
|
||||||
|
|
||||||
## Splits & Navigation (Tmux Integrated)
|
## Splits & Navigation (Tmux Integrated)
|
||||||
@@ -65,7 +65,7 @@ linting, formatting, and AI features. Leader key is `<Space>`. Local leader is `
|
|||||||
| `Tab` | Command | Show and accept cmdline completion |
|
| `Tab` | Command | Show and accept cmdline completion |
|
||||||
|
|
||||||
Completion is handled natively via Blink and Neovim 0.11+ built-in
|
Completion is handled natively via Blink and Neovim 0.11+ built-in
|
||||||
`vim.snippet`. CodeCompanion and LSP provide the completion sources.
|
`vim.snippet`. Sources are LSP, path, snippets and buffer.
|
||||||
|
|
||||||
## LSP & Navigation
|
## LSP & Navigation
|
||||||
|
|
||||||
@@ -114,13 +114,38 @@ Supports C, C++, Rust (via gdb/rust-gdb) and Go (via delve).
|
|||||||
| `<leader>gdl` | Normal | Re-run last debug session |
|
| `<leader>gdl` | Normal | Re-run last debug session |
|
||||||
| `<leader>gdt` | Normal | Terminate session |
|
| `<leader>gdt` | Normal | Terminate session |
|
||||||
|
|
||||||
## AI (CodeCompanion)
|
## AI (agentic.nvim)
|
||||||
|
|
||||||
|
Chat interface driven over ACP. The provider is detected at startup from what is
|
||||||
|
installed: `claude-agent-acp` if present, otherwise `kiro-cli` (for workstations
|
||||||
|
where Kiro is the only sanctioned agent). Neither needs an API key —
|
||||||
|
authentication is inherited from that agent's own CLI login (`claude /login` or
|
||||||
|
`kiro-cli login`). `\s` switches provider at runtime when both are installed.
|
||||||
|
|
||||||
| Keybind | Mode | Description |
|
| Keybind | Mode | Description |
|
||||||
| ------------ | ------------- | --------------------------------- |
|
| ------------ | ------------- | --------------------------------- |
|
||||||
| `<leader>a` | Normal/Visual | Toggle CodeCompanion Chat |
|
| `<leader>a` | Normal/Visual | Toggle Agentic chat |
|
||||||
| `<leader>ca` | Visual | Add visual selection to Chat |
|
| `<leader>ca` | Normal/Visual | Add selection or file to context |
|
||||||
| `<leader>ci` | Normal/Visual | Open inline prompt |
|
| `<leader>cn` | Normal | Start a new session |
|
||||||
| `<leader>cp` | Normal/Visual | Open Action Palette |
|
|
||||||
|
|
||||||
Active provider is automatically selected: **Gemini** (if `GEMINI_API_KEY` is set) or **Kiro** (local model).
|
Inside the chat widget (local leader is `\`):
|
||||||
|
|
||||||
|
| Keybind | Mode | Description |
|
||||||
|
| ------------- | ------ | --------------------------------------- |
|
||||||
|
| `<CR>` | Normal | Submit prompt |
|
||||||
|
| `<C-s>` | N/I/V | Submit prompt |
|
||||||
|
| `<S-Tab>` | N/I/V | Switch agent mode |
|
||||||
|
| `@` | Insert | Trigger file picker |
|
||||||
|
| `/` | Insert | Trigger slash commands |
|
||||||
|
| `<C-v>` | Insert | Paste image from clipboard |
|
||||||
|
| `q` | Normal | Hide the widget |
|
||||||
|
| `\s` | Normal | Switch ACP provider |
|
||||||
|
| `\m` | Normal | Switch model |
|
||||||
|
| `\t` | Normal | Select thought level |
|
||||||
|
| `\o` | Normal | Open options modal |
|
||||||
|
| `\l` | Normal | Pick a live session |
|
||||||
|
| `\[` / `\]` | Normal | Previous / next session |
|
||||||
|
| `\D` | Normal | Destroy the current session |
|
||||||
|
|
||||||
|
Sessions are shared with the CLI: start one in the terminal and resume it here,
|
||||||
|
or the reverse. Run `:checkhealth agentic` if the provider misbehaves.
|
||||||
|
|||||||
@@ -1,66 +1,49 @@
|
|||||||
vim.pack.add({
|
vim.pack.add({
|
||||||
{ src = "https://github.com/olimorris/codecompanion.nvim" },
|
{ src = "https://github.com/carlos-algms/agentic.nvim" },
|
||||||
{ src = "https://github.com/nvim-lua/plenary.nvim" },
|
|
||||||
{
|
{
|
||||||
src = "https://github.com/saghen/blink.cmp",
|
src = "https://github.com/saghen/blink.cmp",
|
||||||
version = vim.version.range("^1"),
|
version = vim.version.range("^1"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
local codecompanion = require("codecompanion")
|
-- Chat interface over ACP. This config is shared across workstations that allow
|
||||||
local has_gemini = os.getenv("GEMINI_API_KEY")
|
-- different agents, so pick whichever bridge is actually on PATH, in order:
|
||||||
|
-- claude-agent-acp -- npm i -g @agentclientprotocol/claude-agent-acp
|
||||||
|
-- kiro-cli -- curl -fsSL https://cli.kiro.dev/install | bash
|
||||||
|
-- Neither takes an API key; auth is inherited from that agent's own CLI login
|
||||||
|
-- (`claude /login` / `kiro-cli login`).
|
||||||
|
local acp_bridges = {
|
||||||
|
{ provider = "claude-agent-acp", command = "claude-agent-acp" },
|
||||||
|
{ provider = "kiro-acp", command = "kiro-cli" },
|
||||||
|
}
|
||||||
|
|
||||||
codecompanion.setup({
|
local function detect_provider()
|
||||||
interactions = {
|
for _, bridge in ipairs(acp_bridges) do
|
||||||
chat = {
|
if vim.fn.executable(bridge.command) == 1 then
|
||||||
adapter = has_gemini and "gemini" or "kiro",
|
return bridge.provider
|
||||||
tools = {
|
end
|
||||||
["web_search"] = {
|
end
|
||||||
opts = { require_approval_before = false },
|
-- none installed: agentic shows its own "not installed" warning on first use
|
||||||
},
|
return acp_bridges[1].provider
|
||||||
["fetch_webpage"] = {
|
end
|
||||||
opts = { require_approval_before = false },
|
|
||||||
},
|
require("agentic").setup({
|
||||||
},
|
provider = detect_provider(),
|
||||||
},
|
-- only offer the bridges that are installed, not all thirteen
|
||||||
inline = { adapter = has_gemini and "gemini" or nil },
|
provider_switcher = { hide_unhealthy_providers = true },
|
||||||
},
|
|
||||||
adapters = {
|
|
||||||
http = {
|
|
||||||
gemini = function()
|
|
||||||
return require("codecompanion.adapters").extend("gemini", {
|
|
||||||
env = {
|
|
||||||
api_key = os.getenv("GEMINI_API_KEY") or "cmd:cat ~/.gemini_api_key",
|
|
||||||
},
|
|
||||||
schema = {
|
|
||||||
model = {
|
|
||||||
default = "gemini-3.1-pro-preview",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
acp = {
|
|
||||||
kiro = function()
|
|
||||||
return require("codecompanion.adapters").extend("kiro", {
|
|
||||||
defaults = {
|
|
||||||
model = "kiro_default",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Optional: Keymaps for CodeCompanion
|
vim.keymap.set({ "n", "v" }, "<leader>a", function()
|
||||||
vim.keymap.set({ "n", "v" }, "<leader>a", "<cmd>CodeCompanionChat Toggle<cr>", { noremap = true, silent = true })
|
require("agentic").toggle()
|
||||||
-- Inline prompt for buffer modifications (Generates diffs)
|
end, { desc = "Toggle Agentic chat" })
|
||||||
vim.keymap.set({ "n", "v" }, "<leader>ci", "<cmd>CodeCompanion <cr>", { noremap = true, silent = true })
|
|
||||||
-- Add visual selection to chat
|
vim.keymap.set({ "n", "v" }, "<leader>ca", function()
|
||||||
vim.keymap.set("v", "<leader>ca", "<cmd>CodeCompanionChat Add<cr>", { noremap = true, silent = true })
|
require("agentic").add_selection_or_file_to_context()
|
||||||
-- Action palette (Use this to Accept/Reject inline diffs)
|
end, { desc = "Add selection or file to Agentic context" })
|
||||||
vim.keymap.set({ "n", "v" }, "<leader>cp", "<cmd>CodeCompanionActions<cr>", { noremap = true, silent = true })
|
|
||||||
vim.cmd([[cab cc CodeCompanion]])
|
vim.keymap.set("n", "<leader>cn", function()
|
||||||
|
require("agentic").new_session()
|
||||||
|
end, { desc = "Start a new Agentic session" })
|
||||||
|
|
||||||
vim.cmd("packadd blink.cmp")
|
vim.cmd("packadd blink.cmp")
|
||||||
local blink = require("blink.cmp")
|
local blink = require("blink.cmp")
|
||||||
@@ -82,8 +65,7 @@ blink.setup({
|
|||||||
-- defaults to Neovim 0.11+ built-in `vim.snippet` API.
|
-- defaults to Neovim 0.11+ built-in `vim.snippet` API.
|
||||||
|
|
||||||
sources = {
|
sources = {
|
||||||
-- Added codecompanion, removed emoji
|
default = { "lsp", "path", "snippets", "buffer" },
|
||||||
default = { "lsp", "path", "snippets", "buffer", "codecompanion" },
|
|
||||||
providers = {
|
providers = {
|
||||||
cmdline = {
|
cmdline = {
|
||||||
min_keyword_length = function(ctx)
|
min_keyword_length = function(ctx)
|
||||||
@@ -93,14 +75,6 @@ blink.setup({
|
|||||||
return 0
|
return 0
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
-- Wire up CodeCompanion to Blink
|
|
||||||
codecompanion = {
|
|
||||||
name = "CodeCompanion",
|
|
||||||
module = "codecompanion.providers.completion.blink",
|
|
||||||
enabled = true,
|
|
||||||
score_offset = 100, -- Boosts priority so AI suggestions appear first in ghost text
|
|
||||||
async = true, -- Ensures AI fetching doesn't block the UI
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fuzzy = { implementation = "prefer_rust_with_warning" },
|
fuzzy = { implementation = "prefer_rust_with_warning" },
|
||||||
|
|||||||
@@ -19,44 +19,35 @@ end
|
|||||||
|
|
||||||
vim.cmd.colorscheme("rose-pine")
|
vim.cmd.colorscheme("rose-pine")
|
||||||
|
|
||||||
|
-- Cursor: the default 'guicursor' names no highlight group, so every mode
|
||||||
|
-- inherits the terminal's cursor colour and the thin insert bar gets lost
|
||||||
|
-- between characters. Name a group per mode and let Neovim drive the colour
|
||||||
|
-- (OSC 12, passed through by tmux via the Cs/Cr capabilities).
|
||||||
|
vim.opt.guicursor = {
|
||||||
|
"n-v-c-sm:block-Cursor",
|
||||||
|
"i-ci-ve:ver25-CursorInsert-blinkwait300-blinkon500-blinkoff400",
|
||||||
|
"r-cr-o:hor20-CursorReplace",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function set_cursor_highlights()
|
||||||
|
local palette = require("rose-pine.palette")
|
||||||
|
vim.api.nvim_set_hl(0, "Cursor", { fg = palette.base, bg = palette.text })
|
||||||
|
vim.api.nvim_set_hl(0, "CursorInsert", { fg = palette.base, bg = palette.rose })
|
||||||
|
vim.api.nvim_set_hl(0, "CursorReplace", { fg = palette.base, bg = palette.love })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Re-applied on ColorScheme so the colours follow the light/dark toggle below.
|
||||||
|
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||||
|
pattern = "rose-pine",
|
||||||
|
callback = set_cursor_highlights,
|
||||||
|
desc = "Keep the cursor legible against the background in every mode",
|
||||||
|
})
|
||||||
|
set_cursor_highlights()
|
||||||
|
|
||||||
vim.keymap.set("n", "<leader>tt", function()
|
vim.keymap.set("n", "<leader>tt", function()
|
||||||
vim.o.background = vim.o.background == "dark" and "light" or "dark"
|
vim.o.background = vim.o.background == "dark" and "light" or "dark"
|
||||||
end, { desc = "Alternate between light and dark mode" })
|
end, { desc = "Alternate between light and dark mode" })
|
||||||
|
|
||||||
-- Insert-mode cursor: extmark overlay for full fg+bg control (terminal ignores fg on cursor hl groups)
|
|
||||||
local function set_icursor_hl()
|
|
||||||
local palette = require("rose-pine.palette")
|
|
||||||
vim.api.nvim_set_hl(0, "iCursorChar", { fg = palette.surface, bg = palette.love })
|
|
||||||
end
|
|
||||||
|
|
||||||
set_icursor_hl()
|
|
||||||
vim.api.nvim_create_autocmd({ "ColorScheme", "OptionSet" }, {
|
|
||||||
pattern = { "*", "background" },
|
|
||||||
callback = set_icursor_hl,
|
|
||||||
})
|
|
||||||
|
|
||||||
local icursor_ns = vim.api.nvim_create_namespace("icursor")
|
|
||||||
|
|
||||||
local function update_icursor()
|
|
||||||
vim.api.nvim_buf_clear_namespace(0, icursor_ns, 0, -1)
|
|
||||||
local r, c = unpack(vim.api.nvim_win_get_cursor(0))
|
|
||||||
local line = vim.api.nvim_get_current_line()
|
|
||||||
local char = c < #line and line:sub(c + 1, c + 1) or " "
|
|
||||||
vim.api.nvim_buf_set_extmark(0, icursor_ns, r - 1, c, {
|
|
||||||
virt_text = { { char, "iCursorChar" } },
|
|
||||||
virt_text_pos = "overlay",
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd("ModeChanged", { pattern = "*:i*", callback = update_icursor })
|
|
||||||
vim.api.nvim_create_autocmd("CursorMovedI", { callback = update_icursor })
|
|
||||||
vim.api.nvim_create_autocmd("ModeChanged", {
|
|
||||||
pattern = "i*:*",
|
|
||||||
callback = function()
|
|
||||||
vim.api.nvim_buf_clear_namespace(0, icursor_ns, 0, -1)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
require("nvim-highlight-colors").setup()
|
require("nvim-highlight-colors").setup()
|
||||||
|
|
||||||
vim.pack.add({
|
vim.pack.add({
|
||||||
@@ -71,7 +62,7 @@ require("lualine").setup({
|
|||||||
component_separators = "",
|
component_separators = "",
|
||||||
theme = "rose-pine",
|
theme = "rose-pine",
|
||||||
},
|
},
|
||||||
extensions = { "fugitive", "lazy" },
|
extensions = { "fugitive" },
|
||||||
})
|
})
|
||||||
|
|
||||||
require("ibl").setup()
|
require("ibl").setup()
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
"agentic.nvim": {
|
||||||
|
"rev": "267b86224f9931bde47225852a58b21f9d153a9f",
|
||||||
|
"src": "https://github.com/carlos-algms/agentic.nvim"
|
||||||
|
},
|
||||||
"blink.cmp": {
|
"blink.cmp": {
|
||||||
"rev": "78336bc89ee5365633bcf754d93df01678b5c08f",
|
"rev": "78336bc89ee5365633bcf754d93df01678b5c08f",
|
||||||
"src": "https://github.com/saghen/blink.cmp",
|
"src": "https://github.com/saghen/blink.cmp",
|
||||||
"version": "1.0.0 - 2.0.0"
|
"version": "1.0.0 - 2.0.0"
|
||||||
},
|
},
|
||||||
"codecompanion.nvim": {
|
|
||||||
"rev": "7d7957c26d33a97085d3a0c82eeb0147a0f51314",
|
|
||||||
"src": "https://github.com/olimorris/codecompanion.nvim"
|
|
||||||
},
|
|
||||||
"conform.nvim": {
|
"conform.nvim": {
|
||||||
"rev": "086a40dc7ed8242c03be9f47fbcee68699cc2395",
|
"rev": "086a40dc7ed8242c03be9f47fbcee68699cc2395",
|
||||||
"src": "https://github.com/stevearc/conform.nvim"
|
"src": "https://github.com/stevearc/conform.nvim"
|
||||||
@@ -83,10 +83,6 @@
|
|||||||
"rev": "0fcc83805ad11cf714a949c98c605ed717e0b83e",
|
"rev": "0fcc83805ad11cf714a949c98c605ed717e0b83e",
|
||||||
"src": "https://github.com/stevearc/oil.nvim"
|
"src": "https://github.com/stevearc/oil.nvim"
|
||||||
},
|
},
|
||||||
"plenary.nvim": {
|
|
||||||
"rev": "b9fd5226c2f76c951fc8ed5923d85e4de065e509",
|
|
||||||
"src": "https://github.com/nvim-lua/plenary.nvim"
|
|
||||||
},
|
|
||||||
"snacks.nvim": {
|
"snacks.nvim": {
|
||||||
"rev": "ad9ede6a9cddf16cedbd31b8932d6dcdee9b716e",
|
"rev": "ad9ede6a9cddf16cedbd31b8932d6dcdee9b716e",
|
||||||
"src": "https://github.com/folke/snacks.nvim"
|
"src": "https://github.com/folke/snacks.nvim"
|
||||||
|
|||||||
+1
-8
@@ -76,17 +76,10 @@ _kp_run() {
|
|||||||
|
|
||||||
kpclose() { _kp_pw_clear && echo "KeePassXC session cleared."; }
|
kpclose() { _kp_pw_clear && echo "KeePassXC session cleared."; }
|
||||||
|
|
||||||
|
# run with KP_DEBUG=1 to troubleshoot if needed
|
||||||
kpgets() { _kp_run show -sa Password "$KEEPASS_DB" "$1"; }
|
kpgets() { _kp_run show -sa Password "$KEEPASS_DB" "$1"; }
|
||||||
kptotps() { _kp_run show -st "$KEEPASS_DB" "$1"; }
|
kptotps() { _kp_run show -st "$KEEPASS_DB" "$1"; }
|
||||||
|
|
||||||
# run with KP_DEBUG=1 to troubleshoot if needed
|
|
||||||
function load_gemini() {
|
|
||||||
if [[ -z "${MY_VARIABLE+set}" ]]; then
|
|
||||||
export GEMINI_API_KEY=$(kpgets "Gemini API Key")
|
|
||||||
echo "Gemini API Key loaded into environment!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function totp() {
|
function totp() {
|
||||||
local clip=false
|
local clip=false
|
||||||
[[ "$1" == "-c" ]] && clip=true && shift
|
[[ "$1" == "-c" ]] && clip=true && shift
|
||||||
|
|||||||
Reference in New Issue
Block a user