Compare commits
4
Commits
main
..
32f3429acf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32f3429acf | ||
|
|
da83f9ebcb | ||
|
|
b9333e7b39 | ||
|
|
eb77beb9e9 |
@@ -1,11 +0,0 @@
|
|||||||
-- Indentation & Layout
|
|
||||||
vim.opt_local.colorcolumn = "100"
|
|
||||||
vim.opt_local.textwidth = 100
|
|
||||||
vim.opt_local.tabstop = 4
|
|
||||||
vim.opt_local.shiftwidth = 4
|
|
||||||
vim.opt_local.softtabstop = 4
|
|
||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Indentation & Layout
|
|
||||||
vim.opt_local.colorcolumn = "120"
|
|
||||||
vim.opt_local.textwidth = 120
|
|
||||||
vim.opt_local.tabstop = 4
|
|
||||||
vim.opt_local.shiftwidth = 4
|
|
||||||
vim.opt_local.softtabstop = 4
|
|
||||||
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
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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 = ""
|
|
||||||
vim.opt_local.wrap = true
|
|
||||||
vim.opt_local.textwidth = 80
|
|
||||||
vim.opt_local.tabstop = 2
|
|
||||||
vim.opt_local.shiftwidth = 2
|
|
||||||
vim.opt_local.softtabstop = 2
|
|
||||||
vim.opt_local.expandtab = true
|
|
||||||
|
|
||||||
-- Formatting Behavior: wrap text, keep comment/quote headers, parse lists
|
|
||||||
vim.opt_local.formatoptions:append("tqn")
|
|
||||||
|
|
||||||
-- gq Formatter: Prettier handles links/parentheses cleanly; par as fallback
|
|
||||||
if vim.fn.executable("prettier") == 1 then
|
|
||||||
vim.bo.formatprg = "prettier --parser markdown" .. filename_arg
|
|
||||||
elseif vim.fn.executable("par") == 1 then
|
|
||||||
vim.bo.formatprg = "par -w80req"
|
|
||||||
end
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- Indentation & Layout
|
|
||||||
vim.opt_local.colorcolumn = "100"
|
|
||||||
vim.opt_local.textwidth = 100
|
|
||||||
vim.opt_local.tabstop = 4
|
|
||||||
vim.opt_local.shiftwidth = 4
|
|
||||||
vim.opt_local.softtabstop = 4
|
|
||||||
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
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
vim.opt_local.colorcolumn = "100"
|
||||||
|
vim.opt_local.tabstop = 4
|
||||||
|
vim.opt_local.shiftwidth = 4
|
||||||
|
vim.opt_local.softtabstop = 4
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
vim.opt_local.colorcolumn = "120"
|
||||||
|
vim.opt_local.tabstop = 4
|
||||||
|
vim.opt_local.shiftwidth = 4
|
||||||
|
vim.opt_local.softtabstop = 4
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
vim.opt_local.colorcolumn = ""
|
||||||
|
vim.opt_local.wrap = true
|
||||||
|
vim.opt_local.tabstop = 2
|
||||||
|
vim.opt_local.shiftwidth = 2
|
||||||
|
vim.opt_local.softtabstop = 2
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
vim.opt_local.colorcolumn = "100"
|
||||||
|
vim.opt_local.tabstop = 4
|
||||||
|
vim.opt_local.shiftwidth = 4
|
||||||
|
vim.opt_local.softtabstop = 4
|
||||||
@@ -1096,8 +1096,8 @@ local function makespec_snacks()
|
|||||||
-- search
|
-- search
|
||||||
{ "<F1>", function() Snacks.picker.help() end, desc = "Help Pages" },
|
{ "<F1>", function() Snacks.picker.help() end, desc = "Help Pages" },
|
||||||
{ "<F9>", function() Snacks.picker.colorschemes() end, desc = "Colorschemes" },
|
{ "<F9>", function() Snacks.picker.colorschemes() end, desc = "Colorschemes" },
|
||||||
{ "<M-/>", function() Snacks.picker.git_grep({ ignorecase = false }) end, desc = "Grep Git Files" },
|
{ "<M-/>", function() Snacks.picker.git_grep({ ignorecase = true }) end, desc = "Grep Git Files" },
|
||||||
{ "<leader>sg", function() Snacks.picker.git_grep({ ignorecase = false }) end, desc = "Grep Git Files" },
|
{ "<leader>sg", function() Snacks.picker.git_grep({ ignorecase = true }) end, desc = "Grep Git Files" },
|
||||||
{ "<leader>sa", function() Snacks.picker.grep({ untracked = true }) end, desc = "Grep 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.grep_word() end, desc = "Visual selection or word", mode = { "n", "x" }, },
|
||||||
{ "<leader>/", function() Snacks.picker.search_history() end, desc = "Search History" },
|
{ "<leader>/", function() Snacks.picker.search_history() end, desc = "Search History" },
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
import os
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from qutebrowser.config.config import ConfigContainer
|
from qutebrowser.config.config import ConfigContainer
|
||||||
@@ -72,7 +71,7 @@ c.completion.open_categories = [
|
|||||||
c.content.register_protocol_handler = False
|
c.content.register_protocol_handler = False
|
||||||
c.content.blocking.enabled = True
|
c.content.blocking.enabled = True
|
||||||
c.content.fullscreen.window = True
|
c.content.fullscreen.window = True
|
||||||
c.content.blocking.method = "auto"
|
c.content.blocking.method = "adblock"
|
||||||
c.content.blocking.adblock.lists = [
|
c.content.blocking.adblock.lists = [
|
||||||
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
|
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
|
||||||
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/badware.txt",
|
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/badware.txt",
|
||||||
@@ -149,21 +148,13 @@ c.input.insert_mode.plugins = True
|
|||||||
c.messages.timeout = 5000
|
c.messages.timeout = 5000
|
||||||
c.qt.force_platform = "wayland"
|
c.qt.force_platform = "wayland"
|
||||||
c.qt.highdpi = True
|
c.qt.highdpi = True
|
||||||
qt_args = [
|
c.qt.args = [
|
||||||
"disable-features=WebAuth", # makes BankID work
|
"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.bar = "always"
|
||||||
c.scrolling.smooth = False
|
c.scrolling.smooth = False
|
||||||
c.session.lazy_restore = False
|
c.session.lazy_restore = False
|
||||||
@@ -197,16 +188,16 @@ c.tabs.undo_stack_size = 1000
|
|||||||
c.tabs.width = "15%"
|
c.tabs.width = "15%"
|
||||||
c.url.start_pages = "https://www.google.com/search?hl=en&source=hp"
|
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.default_page = "https://www.google.com/search?hl=en&source=hp"
|
||||||
c.url.searchengines = {
|
c.url.searchengines = dict(
|
||||||
"DEFAULT": "https://www.google.com/search?hl=en&source=hp&q={}",
|
DEFAULT="https://www.google.com/search?hl=en&source=hp&q={}",
|
||||||
"aw": "https://wiki.archlinux.org/?search={}",
|
aw="https://wiki.archlinux.org/?search={}",
|
||||||
"ap": "https://www.archlinux.org/packages/?q={}",
|
ap="https://www.archlinux.org/packages/?q={}",
|
||||||
"gh": "https://github.com/search?q={}&type=repositories",
|
gh="https://github.com/search?q={}&type=repositories",
|
||||||
"ip": "https://iplocation.io/ip/{}",
|
ip="https://iplocation.io/ip/{}",
|
||||||
"sh": "https://explainshell.com/explain?cmd={}",
|
sh="https://explainshell.com/explain?cmd={}",
|
||||||
"wi": "https://en.wikipedia.org/w/index.php?search={}",
|
wi="https://en.wikipedia.org/w/index.php?search={}",
|
||||||
"yt": "https://yewtu.be/search?q={}",
|
yt="https://yewtu.be/search?q={}",
|
||||||
}
|
)
|
||||||
c.zoom.default = "100%"
|
c.zoom.default = "100%"
|
||||||
|
|
||||||
# FONTS
|
# FONTS
|
||||||
|
|||||||
+20
-14
@@ -185,8 +185,8 @@ bindsym --whole-window --border {
|
|||||||
# media keys
|
# media keys
|
||||||
bindsym {
|
bindsym {
|
||||||
# Volume
|
# Volume
|
||||||
XF86AudioRaiseVolume exec pamixer --unmute -i 2
|
XF86AudioRaiseVolume exec pamixer -ui 2
|
||||||
XF86AudioLowerVolume exec pamixer --unmute -d 2
|
XF86AudioLowerVolume exec pamixer -ud 2
|
||||||
XF86AudioMute exec pamixer --toggle-mute
|
XF86AudioMute exec pamixer --toggle-mute
|
||||||
Pause exec pamixer -ui 2
|
Pause exec pamixer -ui 2
|
||||||
Print exec pamixer -ud 2
|
Print exec pamixer -ud 2
|
||||||
@@ -254,7 +254,6 @@ bindsym {
|
|||||||
|
|
||||||
# Launch the browser
|
# Launch the browser
|
||||||
$mod+i exec qutebrowser
|
$mod+i exec qutebrowser
|
||||||
$mod+Shift+i exec env QUTE_ENABLE_GPU=1 qutebrowser
|
|
||||||
|
|
||||||
# Launch the file explorer
|
# Launch the file explorer
|
||||||
$mod+o exec pcmanfm
|
$mod+o exec pcmanfm
|
||||||
@@ -295,10 +294,10 @@ exec systemctl --user import-environment DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CU
|
|||||||
exec hash dbus-update-activation-environment 2>/dev/null && \
|
exec hash dbus-update-activation-environment 2>/dev/null && \
|
||||||
dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
|
dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
|
||||||
|
|
||||||
exec {
|
# Start foot server
|
||||||
# Start foot server
|
exec systemctl --user enable --now foot-server.service
|
||||||
exec systemctl --user enable --now foot-server.service
|
|
||||||
|
|
||||||
|
exec {
|
||||||
# Set wallpaper
|
# Set wallpaper
|
||||||
~/.azotebg
|
~/.azotebg
|
||||||
|
|
||||||
@@ -313,21 +312,27 @@ exec {
|
|||||||
|
|
||||||
# night-time color shift
|
# night-time color shift
|
||||||
wlsunset -l 59.91:10.76
|
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 {
|
exec_always {
|
||||||
|
# Idle configuration
|
||||||
pkill -x swayidle; exec swayidle -w \
|
pkill -x swayidle; exec swayidle -w \
|
||||||
timeout 1200 'gtklock -d' \
|
timeout 1200 'gtklock' \
|
||||||
timeout 2400 'swaymsg "output * dpms off"' \
|
timeout 2400 'swaymsg "output * dpms off"' \
|
||||||
resume 'swaymsg "output * dpms on"' \
|
resume 'swaymsg "output * dpms on"' \
|
||||||
before-sleep 'gtklock -d' \
|
before-sleep 'gtklock'
|
||||||
lock 'gtklock -d'
|
lock 'gtklock'
|
||||||
|
|
||||||
# nwg-drawer
|
# nwg-drawer
|
||||||
nwg-drawer -r -c 7 -is 90 -mb 10 -ml 50 -mr 50 -mt 10
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
@@ -357,8 +362,9 @@ for_window [app_id="org.mozilla.Thunderbird" title="^((?!Mozilla Thunderbird).)*
|
|||||||
|
|
||||||
# steam
|
# steam
|
||||||
# https://github.com/ValveSoftware/steam-for-linux/issues/1040
|
# https://github.com/ValveSoftware/steam-for-linux/issues/1040
|
||||||
for_window [app_id="(?i)^steam$"] floating enable
|
for_window [app_id="^steam$"] floating enable
|
||||||
for_window [app_id="(?i)^steam$" title="^Steam$"] floating disable
|
for_window [app_id="^Steam$"] floating enable
|
||||||
|
for_window [app_id="^steam$" title="^Steam$"] floating disable
|
||||||
|
|
||||||
# Inhibit idle
|
# Inhibit idle
|
||||||
for_window [app_id="firefox"] inhibit_idle fullscreen
|
for_window [app_id="firefox"] inhibit_idle fullscreen
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ bind generic um status-merge
|
|||||||
bind generic - stage-update-line
|
bind generic - stage-update-line
|
||||||
bind generic \ stage-split-chunk
|
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 c none
|
||||||
bind generic cc !git commit
|
bind generic cc !git commit
|
||||||
|
|||||||
@@ -36,21 +36,3 @@ desc = "Maximize or restore the preview pane"
|
|||||||
on = "C"
|
on = "C"
|
||||||
run = "plugin ouch"
|
run = "plugin ouch"
|
||||||
desc = "Compress with 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"
|
|
||||||
|
|||||||
+7
-9
@@ -13,16 +13,14 @@ export SAVEHIST=200000
|
|||||||
alias ls='ls --color=auto -Ah'
|
alias ls='ls --color=auto -Ah'
|
||||||
alias ..='cd ..'
|
alias ..='cd ..'
|
||||||
alias ...='cd ../..'
|
alias ...='cd ../..'
|
||||||
catai() {
|
catai () {
|
||||||
if [[ " $* " == *" -i "* || " $* " == *" -y "* ]]; then
|
rg -l --hidden --glob '!.git/*' --sort path '^' -- "$@" | while IFS= read -r f; do
|
||||||
local cmd
|
printf "\n── %s ──\n" "$f"
|
||||||
cmd="$(command catai --print-cmd "$@")" && [[ -n "$cmd" ]] && print -z "$cmd"
|
echo '```'
|
||||||
else
|
cat "$f"
|
||||||
command catai "$@"
|
echo '```'
|
||||||
fi
|
done
|
||||||
}
|
}
|
||||||
alias cataid='catai -d'
|
|
||||||
alias cataiy='catai -y'
|
|
||||||
alias sioyek="QT_QPA_PLATFORM=xcb sioyek"
|
alias sioyek="QT_QPA_PLATFORM=xcb sioyek"
|
||||||
|
|
||||||
# --- basic keybinds ---
|
# --- basic keybinds ---
|
||||||
|
|||||||
@@ -1,352 +0,0 @@
|
|||||||
#!/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())
|
|
||||||
@@ -280,7 +280,6 @@ def install_zsh(overwrite: bool, reinstall: bool) -> None:
|
|||||||
run("chsh -s /usr/bin/zsh".split())
|
run("chsh -s /usr/bin/zsh".split())
|
||||||
helper_maybe_copy(CFG_SRC, CFG_TGT, "starship.toml", overwrite, symlink=True)
|
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, ".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:
|
def install_tmux(overwrite: bool, reinstall: bool) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user