Compare commits

..
11 Commits
Author SHA1 Message Date
Henrik Bakken f404c4410f tig bugfix 2026-10-25 00:49:02 +02:00
Henrik Bakken 84b09c0e48 catai upgrade 2026-10-25 00:49:02 +02:00
Henrik Bakken 67ef3a3059 nvim ftplugin bugfixes 2026-10-19 12:33:02 +02:00
Henrik Bakken d085d60bad qutebrowser+sway tweaks 2026-10-19 12:33:02 +02:00
Henrik Bakken 4b51cf4253 bugfix swayidle 2026-10-19 12:29:31 +02:00
Henrik Bakken 1b0c38a8d1 nvim git grep case sensitive 2026-10-19 12:29:31 +02:00
Henrik Bakken 02dcab9871 nvim ftplugin tweaks 2026-10-19 12:29:31 +02:00
Henrik Bakken 1ec951997b theming 2026-10-19 12:29:31 +02:00
Henrik Bakken eb82be6847 nvim gitsigns refresh 2026-10-19 12:29:31 +02:00
Henrik Bakken fbf7f3716d endos fixes 2026-10-19 12:29:31 +02:00
Henrik Bakken 5a09671d0e remove noisetorch 2026-10-19 12:29:31 +02:00
11 changed files with 424 additions and 72 deletions
@@ -1,6 +1,3 @@
local bufname = vim.api.nvim_buf_get_name(0)
local filename_arg = bufname ~= "" and (" --stdin-filepath " .. vim.fn.shellescape(bufname)) or ""
-- Indentation & Layout
vim.opt_local.colorcolumn = "100"
vim.opt_local.textwidth = 100
@@ -12,9 +9,3 @@ vim.opt_local.expandtab = true
-- Formatting Behavior
vim.opt_local.formatoptions:remove("t") -- Don't autowrap while typing
vim.opt_local.formatoptions:append("j") -- Remove comment markers when joining lines
-- gq Formatters (Prettier fallback, LSP primary)
-- Prefers local/global binary first to avoid slow npx invocation overhead
local prettier_bin = vim.fn.executable("prettier") == 1 and "prettier" or "npx prettier"
vim.bo.formatprg = prettier_bin .. filename_arg
vim.bo.formatexpr = "v:lua.vim.lsp.formatexpr()"
+2 -9
View File
@@ -1,6 +1,3 @@
local bufname = vim.api.nvim_buf_get_name(0)
local filename_arg = bufname ~= "" and ("--stdin-filepath " .. vim.fn.shellescape(bufname) .. " ") or ""
-- Indentation & Layout
vim.opt_local.colorcolumn = "120"
vim.opt_local.textwidth = 120
@@ -10,9 +7,5 @@ vim.opt_local.softtabstop = 4
vim.opt_local.expandtab = true
-- Formatting Behavior
vim.opt_local.formatoptions:remove("t")
vim.opt_local.formatoptions:append("j")
-- gq Formatters (StyLua fallback, LSP primary)
vim.bo.formatprg = "stylua " .. filename_arg .. "-"
vim.bo.formatexpr = "v:lua.vim.lsp.formatexpr()"
vim.opt_local.formatoptions:remove("t") -- Don't autowrap while typing
vim.opt_local.formatoptions:append("j") -- Remove comment markers when joining lines
-8
View File
@@ -1,6 +1,3 @@
local bufname = vim.api.nvim_buf_get_name(0)
local filename_arg = bufname ~= "" and ("--stdin-filename " .. vim.fn.shellescape(bufname) .. " ") or ""
-- Indentation & Layout
vim.opt_local.colorcolumn = "100"
vim.opt_local.textwidth = 100
@@ -12,8 +9,3 @@ vim.opt_local.expandtab = true
-- Formatting Behavior
vim.opt_local.formatoptions:remove("t") -- Don't autowrap while typing
vim.opt_local.formatoptions:append("j") -- Remove comment markers when joining lines
-- gq Formatters (Ruff fallback, LSP primary)
-- The trailing '-' is required for Ruff to read from stdin
vim.bo.formatprg = "ruff format " .. filename_arg .. "-"
vim.bo.formatexpr = "v:lua.vim.lsp.formatexpr()"
+2 -2
View File
@@ -1096,8 +1096,8 @@ local function makespec_snacks()
-- search
{ "<F1>", function() Snacks.picker.help() end, desc = "Help Pages" },
{ "<F9>", function() Snacks.picker.colorschemes() end, desc = "Colorschemes" },
{ "<M-/>", function() Snacks.picker.git_grep({ ignorecase = true }) end, desc = "Grep Git Files" },
{ "<leader>sg", function() Snacks.picker.git_grep({ ignorecase = true }) end, desc = "Grep Git Files" },
{ "<M-/>", function() Snacks.picker.git_grep({ ignorecase = false }) end, desc = "Grep Git Files" },
{ "<leader>sg", function() Snacks.picker.git_grep({ ignorecase = false }) end, desc = "Grep Git Files" },
{ "<leader>sa", function() Snacks.picker.grep({ untracked = true }) end, desc = "Grep Files" },
{ "<leader>*", function() Snacks.picker.grep_word() end, desc = "Visual selection or word", mode = { "n", "x" }, },
{ "<leader>/", function() Snacks.picker.search_history() end, desc = "Search History" },
+25 -16
View File
@@ -1,4 +1,5 @@
from typing import TYPE_CHECKING
import os
if TYPE_CHECKING:
from qutebrowser.config.config import ConfigContainer
@@ -71,7 +72,7 @@ c.completion.open_categories = [
c.content.register_protocol_handler = False
c.content.blocking.enabled = True
c.content.fullscreen.window = True
c.content.blocking.method = "adblock"
c.content.blocking.method = "auto"
c.content.blocking.adblock.lists = [
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/badware.txt",
@@ -148,13 +149,21 @@ c.input.insert_mode.plugins = True
c.messages.timeout = 5000
c.qt.force_platform = "wayland"
c.qt.highdpi = True
c.qt.args = [
qt_args = [
"disable-features=WebAuth", # makes BankID work
"disable-gpu", # Stops using the GPU for rendering
"disable-gpu-compositing", # Forces CPU compositing
"disable-accelerated-2d-canvas", # Offloads canvas drawing to CPU
"disable-accelerated-video-decode", # Prevents VRAM usage for videos
]
# Disable GPU by default (for AI training), unless QUTE_ENABLE_GPU is set
if os.environ.get("QUTE_ENABLE_GPU") != "1":
qt_args.extend(
[
"disable-gpu", # Stops using the GPU for rendering
"disable-gpu-compositing", # Forces CPU compositing
"disable-accelerated-2d-canvas", # Offloads canvas drawing to CPU
"disable-accelerated-video-decode", # Prevents VRAM usage for videos
]
)
c.qt.args = qt_args
c.scrolling.bar = "always"
c.scrolling.smooth = False
c.session.lazy_restore = False
@@ -188,16 +197,16 @@ c.tabs.undo_stack_size = 1000
c.tabs.width = "15%"
c.url.start_pages = "https://www.google.com/search?hl=en&source=hp"
c.url.default_page = "https://www.google.com/search?hl=en&source=hp"
c.url.searchengines = dict(
DEFAULT="https://www.google.com/search?hl=en&source=hp&q={}",
aw="https://wiki.archlinux.org/?search={}",
ap="https://www.archlinux.org/packages/?q={}",
gh="https://github.com/search?q={}&type=repositories",
ip="https://iplocation.io/ip/{}",
sh="https://explainshell.com/explain?cmd={}",
wi="https://en.wikipedia.org/w/index.php?search={}",
yt="https://yewtu.be/search?q={}",
)
c.url.searchengines = {
"DEFAULT": "https://www.google.com/search?hl=en&source=hp&q={}",
"aw": "https://wiki.archlinux.org/?search={}",
"ap": "https://www.archlinux.org/packages/?q={}",
"gh": "https://github.com/search?q={}&type=repositories",
"ip": "https://iplocation.io/ip/{}",
"sh": "https://explainshell.com/explain?cmd={}",
"wi": "https://en.wikipedia.org/w/index.php?search={}",
"yt": "https://yewtu.be/search?q={}",
}
c.zoom.default = "100%"
# FONTS
+14 -20
View File
@@ -185,8 +185,8 @@ bindsym --whole-window --border {
# media keys
bindsym {
# Volume
XF86AudioRaiseVolume exec pamixer -ui 2
XF86AudioLowerVolume exec pamixer -ud 2
XF86AudioRaiseVolume exec pamixer --unmute -i 2
XF86AudioLowerVolume exec pamixer --unmute -d 2
XF86AudioMute exec pamixer --toggle-mute
Pause exec pamixer -ui 2
Print exec pamixer -ud 2
@@ -254,6 +254,7 @@ bindsym {
# Launch the browser
$mod+i exec qutebrowser
$mod+Shift+i exec env QUTE_ENABLE_GPU=1 qutebrowser
# Launch the file explorer
$mod+o exec pcmanfm
@@ -294,10 +295,10 @@ exec systemctl --user import-environment DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CU
exec hash dbus-update-activation-environment 2>/dev/null && \
dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
# Start foot server
exec systemctl --user enable --now foot-server.service
exec {
# Start foot server
exec systemctl --user enable --now foot-server.service
# Set wallpaper
~/.azotebg
@@ -312,27 +313,21 @@ exec {
# night-time color shift
wlsunset -l 59.91:10.76
# persist clipboard after close
wl-clip-persist --clipboard regular --all-mime-type-regex '(?i)^(?!image/x-inkscape-svg).+'
}
exec_always {
# Idle configuration
pkill -x swayidle; exec swayidle -w \
timeout 1200 'gtklock' \
timeout 1200 'gtklock -d' \
timeout 2400 'swaymsg "output * dpms off"' \
resume 'swaymsg "output * dpms on"' \
before-sleep 'gtklock'
lock 'gtklock'
before-sleep 'gtklock -d' \
lock 'gtklock -d'
# nwg-drawer
nwg-drawer -r -c 7 -is 90 -mb 10 -ml 50 -mr 50 -mt 10
# persist clipboard after close
pkill -x wl-clip-persist
wl-clip-persist --clipboard regular --all-mime-type-regex '(?i)^(?!image/x-inkscape-svg).+'
# enable noisetorch
noisetorch -u
noisetorch -i
}
# ------------------------------------------
@@ -362,9 +357,8 @@ for_window [app_id="org.mozilla.Thunderbird" title="^((?!Mozilla Thunderbird).)*
# steam
# https://github.com/ValveSoftware/steam-for-linux/issues/1040
for_window [app_id="^steam$"] floating enable
for_window [app_id="^Steam$"] floating enable
for_window [app_id="^steam$" title="^Steam$"] floating disable
for_window [app_id="(?i)^steam$"] floating enable
for_window [app_id="(?i)^steam$" title="^Steam$"] floating disable
# Inhibit idle
for_window [app_id="firefox"] inhibit_idle fullscreen
+1 -1
View File
@@ -69,7 +69,7 @@ bind generic um status-merge
bind generic - stage-update-line
bind generic \ stage-split-chunk
bind generic y @sh -c "echo -n %(commit) | wl-copy"
bind generic Y @sh -c "echo -n %(commit) | wl-copy"
bind generic c none
bind generic cc !git commit
+18
View File
@@ -36,3 +36,21 @@ desc = "Maximize or restore the preview pane"
on = "C"
run = "plugin ouch"
desc = "Compress with ouch"
[[mgr.prepend_keymap]]
on = [ "c", "a" ]
run = 'shell --block -- catai -T %s | nvim -R -c "set bt=nofile ft=xml" -'
desc = "catai: Dump selected files/folders into Neovim"
[[mgr.prepend_keymap]]
on = [ "c", "x" ]
run = """\
shell --block -- zsh -c \"args=(); for p in \\\"\\$@\\\"; do args+=(-x \\\"\\$p\\\"); done; catai -T \
\\\"\\${args[@]}\\\" | nvim -R -c \\\"set bt=nofile ft=xml\\\" -\" -- %s\
"""
desc = "catai: Dump current dir EXCEPT selected items"
[[mgr.prepend_keymap]]
on = [ "c", "d" ]
run = 'shell --block -- catai -T -d | nvim -R -c "set bt=nofile ft=xml" -'
desc = "catai: Dump git dirty files into Neovim"
+9 -7
View File
@@ -13,14 +13,16 @@ export SAVEHIST=200000
alias ls='ls --color=auto -Ah'
alias ..='cd ..'
alias ...='cd ../..'
catai () {
rg -l --hidden --glob '!.git/*' --sort path '^' -- "$@" | while IFS= read -r f; do
printf "\n── %s ──\n" "$f"
echo '```'
cat "$f"
echo '```'
done
catai() {
if [[ " $* " == *" -i "* || " $* " == *" -y "* ]]; then
local cmd
cmd="$(command catai --print-cmd "$@")" && [[ -n "$cmd" ]] && print -z "$cmd"
else
command catai "$@"
fi
}
alias cataid='catai -d'
alias cataiy='catai -y'
alias sioyek="QT_QPA_PLATFORM=xcb sioyek"
# --- basic keybinds ---
+352
View File
@@ -0,0 +1,352 @@
#!/usr/bin/env python3.14
"""
catai: Collect project tree & files in XML tags for LLM context.
Requires Python >= 3.14 (PEP 649/749 deferred annotations, PEP 695 type statements).
"""
import argparse
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from pathlib import Path
type GlobList = list[str]
type PathList = list[str]
DEFAULT_IGNORES: tuple[str, ...] = (
"!.git/*",
"!.env*",
"!*lock*",
"!*.lock",
"!*.min.*",
)
EXAMPLES = """
Examples:
catai # Dump entire current repo
catai src/ # Dump everything inside src/
catai src -x legacy # Everything in src/ except src/legacy/
catai src -x '*.test.ts' -x dist # Exclude multiple patterns
catai src '!legacy' '!*.spec.js' # Exclude using '!' shorthand
catai -T src/main.rs # Single file without <project_tree>
catai -i src -x vendor # Pick files in fzf
catai -y # Pick files/folders in Yazi
catai -d # Only git-modified & untracked files
"""
@dataclass(slots=True, frozen=True)
class Config:
paths: PathList
rg_globs: GlobList
dirty: bool
no_tree: bool
interactive: bool
yazi: bool
print_cmd: bool
def expand_exclude(pattern: str) -> GlobList:
"""Expand a user exclusion pattern into ripgrep glob flags."""
clean = pattern.lstrip("!").rstrip("/")
if not clean:
return []
# Normalize absolute paths or leading './' so ripgrep globs match properly
try:
p = Path(clean)
if p.is_absolute():
clean = str(p.relative_to(Path.cwd()))
elif clean.startswith("./"):
clean = clean[2:]
except ValueError:
pass
return [
f"--glob=!{clean}",
f"--glob=!{clean}/**",
f"--glob=!**/{clean}",
f"--glob=!**/{clean}/**",
]
def parse_args(argv: Sequence[str] | None = None) -> Config:
parser = argparse.ArgumentParser(
prog="catai",
description="Collect project tree and files in XML tags for LLM context.",
epilog=EXAMPLES,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"targets",
nargs="*",
help="Files, directories, or '!exclude' globs",
)
parser.add_argument(
"-x",
"--exclude",
action="append",
default=[],
metavar="PATTERN",
help="Exclude folder or file glob (repeatable)",
)
parser.add_argument(
"-d",
"--dirty",
action="store_true",
help="Only git modified & untracked files",
)
parser.add_argument(
"-T",
"--no-tree",
action="store_true",
help="Skip <project_tree> header",
)
parser.add_argument(
"-i",
"--interactive",
action="store_true",
help="Interactive fzf file picker",
)
parser.add_argument(
"-y",
"--yazi",
action="store_true",
help="Interactive Yazi file/folder chooser",
)
parser.add_argument(
"--print-cmd",
action="store_true",
help="Print selected command instead of executing (used by zsh print -z)",
)
ns = parser.parse_args(argv)
rg_globs: GlobList = [f"--glob={g}" for g in DEFAULT_IGNORES]
for ex in ns.exclude:
rg_globs.extend(expand_exclude(ex))
paths: PathList = []
for target in ns.targets:
match target:
case s if s.startswith("!"):
rg_globs.extend(expand_exclude(s[1:]))
case s:
paths.append(s)
return Config(
paths=paths,
rg_globs=rg_globs,
dirty=ns.dirty,
no_tree=ns.no_tree,
interactive=ns.interactive,
yazi=ns.yazi,
print_cmd=ns.print_cmd,
)
def run_lines(cmd: Sequence[str]) -> list[str]:
"""Run a command and return non-empty stdout lines."""
res = subprocess.run(cmd, capture_output=True, text=True, check=False)
return [line for line in res.stdout.splitlines() if line]
def render_tree(file_paths: Iterable[str]) -> str:
"""Pure-Python directory tree renderer (no external 'tree' binary required)."""
type TreeDict = dict[str, TreeDict]
root: TreeDict = {}
for raw_path in sorted(file_paths):
parts = Path(raw_path).parts
curr = root
for part in parts:
if part in (".", ""):
continue
curr = curr.setdefault(part, {})
lines: list[str] = ["."]
def walk(node: TreeDict, prefix: str = "") -> None:
items = list(node.items())
for idx, (name, children) in enumerate(items):
is_last = idx == len(items) - 1
connector = "└── " if is_last else "├── "
lines.append(f"{prefix}{connector}{name}")
if children:
extension = " " if is_last else "│ "
walk(children, prefix + extension)
walk(root)
return "\n".join(lines)
def pick_with_fzf(cfg: Config) -> PathList:
rg_cmd = [
"rg",
"--files",
"--hidden",
"--sort",
"path",
*cfg.rg_globs,
"--",
*cfg.paths,
]
all_files = run_lines(rg_cmd)
if not all_files:
return []
preview = "bat --color=always {} 2>/dev/null || cat {}"
proc = subprocess.run(
["fzf", "-m", "--preview", preview],
input="\n".join(all_files),
stdout=subprocess.PIPE,
text=True,
check=False,
)
return [line for line in proc.stdout.splitlines() if line]
def pick_with_yazi(cfg: Config) -> PathList:
with tempfile.NamedTemporaryFile(prefix="yazi-chooser.", delete=False) as tmp:
tmp_path = Path(tmp.name)
try:
subprocess.run(["yazi", f"--chooser-file={tmp_path}", *cfg.paths], check=False)
if not tmp_path.exists() or tmp_path.stat().st_size == 0:
return []
raw_picked = [
line for line in tmp_path.read_text(encoding="utf-8").splitlines() if line
]
cwd = Path.cwd()
rel_picked: PathList = []
for p in raw_picked:
path_obj = Path(p)
try:
rel_picked.append(str(path_obj.relative_to(cwd)))
except ValueError:
rel_picked.append(str(path_obj))
return rel_picked
finally:
tmp_path.unlink(missing_ok=True)
def collect_files(cfg: Config, target_paths: PathList) -> tuple[PathList, PathList]:
"""Return (tree_files, matched_non_empty_text_files)."""
cwd = Path.cwd().resolve()
rel_targets: PathList = []
for p in target_paths:
resolved = Path(p).resolve()
try:
rel = str(resolved.relative_to(cwd))
rel_targets.append("." if rel == "" else rel)
except ValueError:
rel_targets.append(os.path.relpath(resolved, cwd))
if cfg.dirty:
dirty_cmd = [
"git",
"ls-files",
"-m",
"-o",
"--exclude-standard",
"--",
*rel_targets,
]
candidates = run_lines(dirty_cmd)
if not candidates:
return [], []
else:
candidates = rel_targets
# Non-empty, non-binary files via ripgrep '^'
rg_content_cmd = [
"rg",
"-l",
"--hidden",
"--sort",
"path",
*cfg.rg_globs,
"^",
"--",
*candidates,
]
matched_files = run_lines(rg_content_cmd)
# Single-file edge case: `rg -l '^' single_file.txt` prints the filename,
# Ensure it is always included if non-empty
if cfg.no_tree or not matched_files:
return [], matched_files
rg_tree_cmd = [
"rg",
"--files",
"--hidden",
"--sort",
"path",
*cfg.rg_globs,
"--",
*rel_targets,
]
tree_files = run_lines(rg_tree_cmd)
return tree_files, matched_files
def dump_output(tree_files: PathList, files: PathList, show_tree: bool) -> None:
out = sys.stdout
if show_tree and tree_files:
out.write("<project_tree>\n")
out.write(render_tree(tree_files))
out.write("\n</project_tree>\n\n")
for filepath in files:
path = Path(filepath)
out.write(f'<file path="{filepath}">\n')
out.flush()
try:
with path.open("rb") as f:
shutil.copyfileobj(f, sys.stdout.buffer)
# Ensure trailing newline before closing tag
if path.stat().st_size > 0:
with path.open("rb") as f:
f.seek(-1, os.SEEK_END)
if f.read(1) != b"\n":
out.write("\n")
except OSError as exc:
sys.stderr.write(f"Error reading {filepath}: {exc}\n")
out.write("</file>\n\n")
def main() -> int:
cfg = parse_args()
# Handle interactive pickers (-i / -y)
if cfg.interactive or cfg.yazi:
picked = pick_with_yazi(cfg) if cfg.yazi else pick_with_fzf(cfg)
if not picked:
return 1
if cfg.print_cmd:
print(f"catai {shlex.join(picked)}")
return 0
tree_files, files = collect_files(cfg, picked)
else:
tree_files, files = collect_files(cfg, cfg.paths)
if not files:
msg = (
"No dirty or untracked files found."
if cfg.dirty
else "No matching text files found."
)
sys.stderr.write(f"{msg}\n")
return 1
dump_output(tree_files, files, show_tree=not cfg.no_tree)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1
View File
@@ -280,6 +280,7 @@ def install_zsh(overwrite: bool, reinstall: bool) -> None:
run("chsh -s /usr/bin/zsh".split())
helper_maybe_copy(CFG_SRC, CFG_TGT, "starship.toml", overwrite, symlink=True)
helper_maybe_copy(HOME_SRC, HOME_TGT, ".zshenv", overwrite, symlink=True)
helper_maybe_copy(HOME_SRC, HOME_TGT, ".local/bin/catai", overwrite, symlink=True)
def install_tmux(overwrite: bool, reinstall: bool) -> None: