diff --git a/README.md b/README.md index a2a8482..e570711 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,166 @@ -All my dotfiles at one place. +# Dotfiles -I will delete the i3 and vim dotfiles repos soon. -I have merged them with this repo, so the version history -of the two repos is saved in this repo. +Personal terminal-environment config for machines that should feel familiar: +music, editor, file navigation, and other CLI workflows. -ranger for file navigation. +This repo is being revived in stages. The current v1 target is intentionally +small: -mpd and ncmpcpp for music (command line) +- macOS music playback with `mpd`, `ncmpcpp`, and local files under + `/Users/sourya4/pro/youtube_download/music` +- baseline Neovim config, plus a minimal Vim fallback +- AI-enabled Neovim chat through CodeCompanion and Codex ACP +- ranger config with portable clipboard helpers +- Git terminal tools such as `gitui` -vimperator is a firefox extension +`ma08/botfiles` remains the home for coding-agent, orchestration, hooks, skills, +secrets-aware shell bootstrap, and agent-specific aliases. This repo owns +general human terminal UX: shell ergonomics, editors, music, file navigation, +Git terminal tools, and user-facing app config. Botfiles can extend this +environment for agent workflows, but should not replace it as the general +dotfiles layer. -The present state of repo is not suitable for anyone to follow. -Still, if you are interested, you can have a peek. I will clean up -and write a more descriptive README later outlining all the features and -the setup process. +## Quick Start -Look into install.sh to get an idea of the setup (regarding symlinks and git - submodules for vim plugins). +Preview everything first: -![alt tag](screen.png) +```bash +./setup.sh --all +``` + +Apply the current v1 config: + +```bash +./setup.sh --all --apply +``` + +Install the macOS music packages and apply music config: + +```bash +./setup.sh --music --install-packages --apply +``` + +Install Git terminal tools: + +```bash +./setup.sh --git --install-packages --apply +``` + +Install packages for the full v1 profile and apply all current config: + +```bash +./setup.sh --all --install-packages --apply +``` + +The setup script backs up existing non-matching files before replacing them with +symlinks. Without `--apply`, it only prints what it would do. + +## Music + +The macOS music profile installs or configures: + +- `mpd` +- `mpc` +- `ncmpcpp` + +Relevant paths: + +- MPD config: `~/.mpd/mpd.conf` +- MPD music directory: `/Users/sourya4/pro/youtube_download/music` +- ncmpcpp config: `~/.ncmpcpp/config` +- ncmpcpp bindings: `~/.ncmpcpp/bindings` +- MPD CoreAudio restart helper: `~/.local/bin/dotfiles-mpd-restart` + +Basic checks: + +```bash +mpd --no-daemon ~/.mpd/mpd.conf +mpc update +ncmpcpp +``` + +Hammerspoon watches for macOS audio output changes and runs the MPD restart +helper when MPD is already running. This works around stale CoreAudio device +handles after plugging, unplugging, or switching output devices. + +Hammerspoon also maps macOS media keys to MPD for play/pause, next, and +previous. To avoid stealing media keys from Chrome or other players, it only +handles them when MPD is already playing, or when MPD is paused and the +frontmost app is not a known media app such as Chrome, Safari, Spotify, Music, +VLC, or IINA. + +Terminal playback is the v1 success gate. macOS media keys / Now Playing support +is a follow-up unless it becomes simple and reliable. + +## Editor + +Neovim is the primary editor target: + +- `nvim/init.lua` -> `~/.config/nvim/init.lua` + +With `--install-packages`, the editor profile installs `neovim` through +Homebrew, plus `node` for `npx`-backed ACP adapters. + +The editor profile also sets Git's global editor to `nvim` and enables verbose +commit templates, so `git commit` opens Neovim with the staged diff available as +commented context. + +Vim gets a minimal fallback: + +- `vim/vimrc-basic` -> `~/.vimrc` + +The old `vim/.vimrc` and Vim bundle submodules are historical material, not the +default install path for the revived setup. + +### AI In Neovim + +The Neovim config bootstraps `lazy.nvim`, installs CodeCompanion, and configures +the Codex ACP adapter with ChatGPT authentication. It runs the Zed Codex ACP +adapter through: + +```bash +npx -y @zed-industries/codex-acp +``` + +Use `:CodeCompanionChat` inside Neovim to open an AI chat buffer. The first run +may download plugins and the ACP adapter. ChatGPT auth must be available through +Codex/Codex ACP on the machine. + +Inline edits use Gemini 3.6 Flash through CodeCompanion's Gemini HTTP adapter. +Set `GEMINI_API_KEY` in the machine-local botfiles secrets loaded by `.botenv`; +do not place it in this repository. The adapter is intentionally non-streaming +because CodeCompanion's Gemini inline handler requires a complete response. It +uses Gemini's `minimal` thinking level to keep inline edits responsive. + +In `git commit` buffers, press `,cm` to draft a commit message from the verbose +commit buffer. Press `,cy` to accept CodeCompanion's proposed edit or `,cn` to +reject it. + +While CodeCompanion is waiting on a chat or inline request, the statusline shows +an `AI` spinner with the active adapter/model. It briefly changes to `AI done`, +`AI cancelled`, or `AI error` when the request finishes. + +## Ranger + +The ranger profile links the repo's ranger config into `~/.config/ranger/` and +installs `bin/dotfiles-clipboard-copy` into `~/.local/bin/`. The clipboard helper +uses `pbcopy`, `wl-copy`, `xclip`, or `xsel`, depending on what the machine has. +With `--install-packages`, the ranger profile installs `ranger` through Homebrew. + +## Git Tools + +The Git tools profile installs `gitui` through Homebrew/Linuxbrew. GitUI config +uses vim-style navigation through: + +- `config/gitui/key_bindings.ron` -> `~/.config/gitui/key_bindings.ron` + +The setup also links `bin/dotfiles-gitui` to `~/.local/bin/gitui` on machines +where GitUI is installed by Homebrew/Linuxbrew. Keeping config under +`config/gitui/` avoids a zsh `AUTO_CD` collision with the `gitui` command. + +## Historical Material + +The old repo includes i3, polybar, Vimperator, X resources, old Linux desktop +scripts, and large Vim plugin submodule history. Those are preserved for now but +not installed by the default v1 setup. Audit and resurrect them only after an +explicit review. diff --git a/bin/dotfiles-clipboard-copy b/bin/dotfiles-clipboard-copy new file mode 100755 index 0000000..d69768b --- /dev/null +++ b/bin/dotfiles-clipboard-copy @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v pbcopy >/dev/null 2>&1; then + exec pbcopy +fi + +if command -v wl-copy >/dev/null 2>&1; then + exec wl-copy +fi + +if command -v xclip >/dev/null 2>&1; then + exec xclip -selection clipboard +fi + +if command -v xsel >/dev/null 2>&1; then + exec xsel -ib +fi + +cat >/dev/null +echo "dotfiles-clipboard-copy: no clipboard command found" >&2 +exit 1 diff --git a/bin/dotfiles-gitui b/bin/dotfiles-gitui new file mode 100755 index 0000000..65cb46d --- /dev/null +++ b/bin/dotfiles-gitui @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -x /opt/homebrew/bin/gitui ]; then + exec /opt/homebrew/bin/gitui "$@" +fi + +if [ -x /home/linuxbrew/.linuxbrew/bin/gitui ]; then + exec /home/linuxbrew/.linuxbrew/bin/gitui "$@" +fi + +resolved="$(command -v gitui 2>/dev/null || true)" +if [ -n "$resolved" ] && [ "$resolved" != "$0" ]; then + exec "$resolved" "$@" +fi + +echo "ERROR: gitui binary not found; install GitUI or rerun setup with --install-packages" >&2 +exit 127 diff --git a/bin/dotfiles-mpd-restart b/bin/dotfiles-mpd-restart new file mode 100755 index 0000000..cd02beb --- /dev/null +++ b/bin/dotfiles-mpd-restart @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +only_if_running=0 +reason="manual" + +while [ "$#" -gt 0 ]; do + case "$1" in + --if-running) + only_if_running=1 + ;; + --reason) + shift + reason="${1:-manual}" + ;; + *) + echo "ERROR: unknown argument: $1" >&2 + exit 2 + ;; + esac + shift +done + +mpd_bin="${MPD_BIN:-}" +mpc_bin="${MPC_BIN:-}" +mpd_config="${MPD_CONFIG:-$HOME/.mpd/mpd.conf}" +mpd_log_dir="$HOME/.mpd" +restart_log="$mpd_log_dir/restart.log" + +if [ -z "$mpd_bin" ]; then + if [ -x /opt/homebrew/bin/mpd ]; then + mpd_bin=/opt/homebrew/bin/mpd + elif [ -x /home/linuxbrew/.linuxbrew/bin/mpd ]; then + mpd_bin=/home/linuxbrew/.linuxbrew/bin/mpd + else + mpd_bin="$(command -v mpd 2>/dev/null || true)" + fi +fi + +if [ -z "$mpc_bin" ]; then + if [ -x /opt/homebrew/bin/mpc ]; then + mpc_bin=/opt/homebrew/bin/mpc + elif [ -x /home/linuxbrew/.linuxbrew/bin/mpc ]; then + mpc_bin=/home/linuxbrew/.linuxbrew/bin/mpc + else + mpc_bin="$(command -v mpc 2>/dev/null || true)" + fi +fi + +if [ -z "$mpd_bin" ] || [ -z "$mpc_bin" ]; then + echo "ERROR: mpd/mpc not found" >&2 + exit 127 +fi + +mkdir -p "$mpd_log_dir" + +log() { + printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*" >> "$restart_log" +} + +if ! pgrep -x mpd >/dev/null 2>&1; then + if [ "$only_if_running" -eq 1 ]; then + log "skip: mpd not running reason=$reason" + exit 0 + fi +fi + +status="$("$mpc_bin" -h 127.0.0.1 -p 6600 status 2>/dev/null || true)" +was_playing=0 +if printf '%s\n' "$status" | grep -q '\[playing\]'; then + was_playing=1 +fi + +log "restart begin reason=$reason was_playing=$was_playing" + +pkill -x mpd >/dev/null 2>&1 || true +for _ in 1 2 3 4 5; do + if ! pgrep -x mpd >/dev/null 2>&1; then + break + fi + sleep 0.2 +done + +if pgrep -x mpd >/dev/null 2>&1; then + pkill -KILL -x mpd >/dev/null 2>&1 || true + sleep 0.5 +fi + +"$mpd_bin" "$mpd_config" +sleep 0.8 + +if [ "$was_playing" -eq 1 ]; then + "$mpc_bin" -h 127.0.0.1 -p 6600 play >/dev/null 2>&1 || true +fi + +log "restart complete reason=$reason" diff --git a/config/gitui/key_bindings.ron b/config/gitui/key_bindings.ron new file mode 100644 index 0000000..c7b1dbf --- /dev/null +++ b/config/gitui/key_bindings.ron @@ -0,0 +1,35 @@ +// Vim-style GitUI key bindings. +// +// GitUI reads this file from ~/.config/gitui/key_bindings.ron. +// Unspecified keys keep GitUI's built-in defaults. +( + open_help: Some(( code: F(1), modifiers: "")), + + move_left: Some(( code: Char('h'), modifiers: "")), + move_right: Some(( code: Char('l'), modifiers: "")), + move_up: Some(( code: Char('k'), modifiers: "")), + move_down: Some(( code: Char('j'), modifiers: "")), + + popup_up: Some(( code: Char('p'), modifiers: "CONTROL")), + popup_down: Some(( code: Char('n'), modifiers: "CONTROL")), + page_up: Some(( code: Char('b'), modifiers: "CONTROL")), + page_down: Some(( code: Char('f'), modifiers: "CONTROL")), + home: Some(( code: Char('g'), modifiers: "")), + end: Some(( code: Char('G'), modifiers: "SHIFT")), + shift_up: Some(( code: Char('K'), modifiers: "SHIFT")), + shift_down: Some(( code: Char('J'), modifiers: "SHIFT")), + + edit_file: Some(( code: Char('I'), modifiers: "SHIFT")), + + status_reset_item: Some(( code: Char('U'), modifiers: "SHIFT")), + + diff_reset_lines: Some(( code: Char('u'), modifiers: "")), + diff_stage_lines: Some(( code: Char('s'), modifiers: "")), + + stashing_save: Some(( code: Char('w'), modifiers: "")), + stashing_toggle_index: Some(( code: Char('m'), modifiers: "")), + + stash_open: Some(( code: Char('l'), modifiers: "")), + + abort_merge: Some(( code: Char('M'), modifiers: "SHIFT")), +) diff --git a/hammerspoon/init.lua b/hammerspoon/init.lua new file mode 100644 index 0000000..9c0a327 --- /dev/null +++ b/hammerspoon/init.lua @@ -0,0 +1,166 @@ +local mpc = "/opt/homebrew/bin/mpc" +local mpdRestart = os.getenv("HOME") .. "/.local/bin/dotfiles-mpd-restart" +local mpdHost = "127.0.0.1" +local mpdPort = "6600" + +local function shellQuote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function mpcCommand(args) + return table.concat({ + shellQuote(mpc), + "-h", + shellQuote(mpdHost), + "-p", + shellQuote(mpdPort), + args, + }, " ") +end + +local function runMpc(args) + hs.execute(mpcCommand(args) .. " >/dev/null 2>&1", true) +end + +local function runMpdRestart(reason) + local command = table.concat({ + shellQuote(mpdRestart), + "--if-running", + "--reason", + shellQuote(reason), + ">/dev/null 2>&1", + }, " ") + hs.execute(command, true) +end + +local mpdMediaPassThroughApps = { + ["Arc"] = true, + ["Brave Browser"] = true, + ["Firefox"] = true, + ["Google Chrome"] = true, + ["IINA"] = true, + ["Music"] = true, + ["Podcasts"] = true, + ["QuickTime Player"] = true, + ["Safari"] = true, + ["Spotify"] = true, + ["TV"] = true, + ["VLC"] = true, + ["YouTube Music"] = true, +} + +local function mpdPlaybackState() + local output, ok = hs.execute(mpcCommand("status"), true) + if not ok or output == nil then + return nil + end + + if output:match("%[playing%]") then + return "playing" + end + + if output:match("%[paused%]") then + return "paused" + end + + return nil +end + +local function frontmostAppName() + local app = hs.application.frontmostApplication() + if app == nil then + return "" + end + + return app:name() or "" +end + +local function shouldHandleMpdMediaKey() + local state = mpdPlaybackState() + if state == "playing" then + return true + end + + if state ~= "paused" then + return false + end + + return not mpdMediaPassThroughApps[frontmostAppName()] +end + +function mpdMediaKeyStatus() + local frontmost = frontmostAppName() + return { + frontmost = frontmost, + handles = shouldHandleMpdMediaKey(), + passThrough = mpdMediaPassThroughApps[frontmost] == true, + state = mpdPlaybackState(), + } +end + +local mediaActions = { + FAST = "next", + PLAY = "toggle", + NEXT = "next", + PREVIOUS = "prev", + REWIND = "prev", +} + +local ipc = require("hs.ipc") +pcall(function() + ipc.cliInstall("/opt/homebrew", true) +end) +hs.autoLaunch(true) + +local function startMpdMediaKeys() + local mediaKeyTap = hs.eventtap.new({ hs.eventtap.event.types.systemDefined }, function(event) + local systemKey = event:systemKey() + if systemKey == nil then + return false + end + + local action = mediaActions[systemKey.key] + if action == nil or not shouldHandleMpdMediaKey() then + return false + end + + if systemKey.down and not systemKey["repeat"] then + runMpc(action) + end + + return true + end) + + mediaKeyTap:start() + return mediaKeyTap +end + +if hs.accessibilityState(true) then + mpdMediaKeyTap = startMpdMediaKeys() +else + hs.alert.show("Enable Accessibility for Hammerspoon to control MPD media keys") +end + +local mpdAudioRestartTimer = nil + +local function scheduleMpdAudioRestart(eventName) + if eventName ~= "dOut" and eventName ~= "sOut" and eventName ~= "dev#" then + return + end + + if mpdAudioRestartTimer ~= nil then + mpdAudioRestartTimer:stop() + end + + mpdAudioRestartTimer = hs.timer.doAfter(2.0, function() + local device = hs.audiodevice.defaultOutputDevice() + local deviceName = "unknown-output" + if device ~= nil then + deviceName = device:name() or deviceName + end + runMpdRestart("audio-device-" .. eventName .. "-" .. deviceName) + end) +end + +hs.audiodevice.watcher.setCallback(scheduleMpdAudioRestart) +hs.audiodevice.watcher.start() diff --git a/install.sh b/install.sh index 3ed3ab2..dae5809 100755 --- a/install.sh +++ b/install.sh @@ -1,55 +1,17 @@ -sudo apt install rofi feh compton rxvt-unicode-256color -sudo apt-get install clipit +#!/usr/bin/env bash +set -euo pipefail -git submodule init -git submodule update -#git submodule foreach git submodule init -#git submodule foreach git submodule update -git submodule foreach git submodule update --init --recursive +cat >&2 <<'MSG' +The old install.sh was destructive and Linux-specific. -cp -R ~/.vim ~/.vimdup -cp -R ~/.i3 ~/.i3dup -cp -R ~/.config/ranger ~/.config/rangerdup -cp -R ~/.mpd ~/.mpddup -cp -R ~/.ncmpcpp ~/.ncmpcppdup -cp -R ~/.vimperator ~/.vimperatordup +Use the safer revived setup entrypoint instead: -cp ~/.vimperatorrc ~/.vimperatorrcdup -cp ~/.vimrc ~/.vimrcdup -cp ~/.gvimrc ~/.gvimrcdup -cp ~/.i3status.conf ~/.i3status.confdup -cp ~/.Xresources ~/.Xresourcesdup + ./setup.sh --all # dry run + ./setup.sh --all --apply # apply symlinks/backups +For macOS music setup: -rm ~/.vimperatorrc -rm ~/.vimrc -rm ~/.Xresources -rm ~/.gvimrc -rm ~/.i3status.conf + ./setup.sh --music --install-packages --apply +MSG -rm -rf ~/.vim -rm -rf ~/.i3 -rm -rf ~/.mpd -rm -rf ~/.ncmpcpp -rm -rf ~/.vimperator -rm -rf ~/.config/ranger - - -ln -s $(pwd)/ranger ~/.config/ranger -ln -s $(pwd)/ncmpcpp ~/.ncmpcpp -ln -s $(pwd)/mpd ~/.mpd -ln -s $(pwd)/vimperator ~/.vimperator -ln -s $(pwd)/vim ~/.vim -ln -s $(pwd)/i3 ~/.i3 - -ln -s $(pwd)/bashrc ~/.bashrc -ln -s $(pwd)/Xresources ~/.Xresources -ln -s $(pwd)/vim/.vimrc ~/.vimrc -ln -s $(pwd)/vscode/.vimrc ~/.vscode_vimrc -ln -s $(pwd)/vim/.gvimrc ~/.gvimrc -ln -s $(pwd)/vimperator/.vimperatorrc ~/.vimperatorrc -ln -s $(pwd)/i3/i3status.conf ~/.i3status.conf - - -#cd ~/.vim/bundle/YouCompleteMe -#./install.sh --clang-completer --system-libclang --system-boost +exit 1 diff --git a/mpd/mpd-macos.conf b/mpd/mpd-macos.conf new file mode 100644 index 0000000..4b37069 --- /dev/null +++ b/mpd/mpd-macos.conf @@ -0,0 +1,25 @@ +music_directory "/Users/sourya4/pro/youtube_download/music" +playlist_directory "~/.mpd/playlists" +db_file "~/.mpd/database" +log_file "~/.mpd/log" +pid_file "~/.mpd/pid" +state_file "~/.mpd/state" +sticker_file "~/.mpd/sticker.sql" + +bind_to_address "127.0.0.1" +port "6600" +auto_update "yes" +restore_paused "yes" + +audio_output { + type "osx" + name "CoreAudio" + mixer_type "software" +} + +audio_output { + type "fifo" + name "Visualizer feed" + path "/tmp/mpd.fifo" + format "44100:16:2" +} diff --git a/ncmpcpp/bindings b/ncmpcpp/bindings index ac05019..1c01d27 100644 --- a/ncmpcpp/bindings +++ b/ncmpcpp/bindings @@ -3,12 +3,28 @@ def_key "j" def_key "k" scroll_up def_key "h" - seek_backward + previous_column +def_key "h" + master_screen def_key "l" - seek_forward + next_column +def_key "l" + slave_screen def_key "L" - next + next_column +def_key "L" + slave_screen +def_key "H" + previous_column def_key "H" + master_screen +def_key "b" + seek_backward +def_key "f" + seek_forward +def_key ">" + next +def_key "<" previous def_key "K" scroll_up_album @@ -28,6 +44,8 @@ def_key "G" move_end def_key "p" pause +def_key "B" + show_browser def_key "x" delete_playlist_items def_key "n" diff --git a/ncmpcpp/config-macos b/ncmpcpp/config-macos new file mode 100644 index 0000000..e8961a9 --- /dev/null +++ b/ncmpcpp/config-macos @@ -0,0 +1,31 @@ +mpd_host = "127.0.0.1" +mpd_port = "6600" +mpd_music_dir = "/Users/sourya4/pro/youtube_download/music" + +user_interface = "alternative" +browser_display_mode = "columns" +search_engine_display_mode = "columns" +playlist_display_mode = "columns" +playlist_editor_display_mode = "columns" + +song_list_format = "{$8%a - $9}{$3%t$9}|{$8%f$9}" +song_columns_list_format = "(7f)[white]{lr:Time} (24)[yellow]{a:Artist} (36)[cyan]{t|f:Title} (33)[red]{b:Album}" +media_library_primary_tag = "album_artist" + +display_bitrate = "yes" +display_remaining_time = "yes" +cyclic_scrolling = "yes" +follow_now_playing_lyrics = "yes" +ignore_leading_the = "yes" + +visualizer_data_source = "/tmp/mpd.fifo" +visualizer_output_name = "Visualizer feed" +visualizer_in_stereo = "yes" +visualizer_type = "spectrum" +visualizer_fps = "60" +visualizer_autoscale = "yes" +visualizer_spectrum_smooth_look = "yes" +visualizer_spectrum_dft_size = "2" +visualizer_spectrum_gain = "10" +visualizer_spectrum_log_scale_x = "yes" +visualizer_spectrum_log_scale_y = "yes" diff --git a/nvim/init.lua b/nvim/init.lua new file mode 100644 index 0000000..85959f7 --- /dev/null +++ b/nvim/init.lua @@ -0,0 +1,344 @@ +vim.g.mapleader = "," + +local opt = vim.opt + +opt.encoding = "utf-8" +opt.number = true +opt.ruler = true +opt.showcmd = true +opt.hidden = true +opt.confirm = true +opt.autoread = true +opt.backspace = { "indent", "eol", "start" } +opt.mouse = "a" + +opt.tabstop = 4 +opt.shiftwidth = 4 +opt.softtabstop = 4 +opt.expandtab = true +opt.smartindent = true + +opt.ignorecase = true +opt.smartcase = true +opt.incsearch = true +opt.hlsearch = true +opt.wrapscan = true + +opt.splitbelow = true +opt.splitright = true +opt.wildmenu = true +opt.wildmode = { "longest:full", "full" } +opt.timeoutlen = 400 +opt.updatetime = 300 +opt.laststatus = 2 + +opt.undofile = true +opt.undodir = vim.fn.expand("~/.local/state/nvim/undo//") + +if vim.fn.has("clipboard") == 1 then + opt.clipboard = "unnamedplus" +end + +vim.cmd("filetype plugin indent on") +vim.cmd("syntax enable") + +vim.keymap.set("i", "jk", "", { silent = true }) +vim.keymap.set("v", "<", "", ">gv", { silent = true }) +vim.keymap.set("n", "h", "nohlsearch", { silent = true }) +vim.keymap.set("n", "", "bprevious", { silent = true }) +vim.keymap.set("n", "", "bnext", { silent = true }) + +local codecompanion_status = { + active = 0, + clear_timer = nil, + frames = { "|", "/", "-", "\\" }, + message = "", + spinner = 1, + timer = nil, +} + +local function codecompanion_request_label(data) + local adapter = data and data.adapter or {} + local name = adapter.formatted_name or adapter.name or "AI" + local model = adapter.model + + if model and model ~= "" then + return name .. " " .. model + end + + return name +end + +local function codecompanion_redraw_status() + pcall(vim.cmd, "redrawstatus") +end + +local function codecompanion_clear_later() + if codecompanion_status.clear_timer then + codecompanion_status.clear_timer:stop() + codecompanion_status.clear_timer:close() + end + + codecompanion_status.clear_timer = vim.uv.new_timer() + codecompanion_status.clear_timer:start( + 2500, + 0, + vim.schedule_wrap(function() + codecompanion_status.message = "" + codecompanion_redraw_status() + end) + ) +end + +local function codecompanion_start_spinner() + if codecompanion_status.clear_timer then + codecompanion_status.clear_timer:stop() + codecompanion_status.clear_timer:close() + codecompanion_status.clear_timer = nil + end + + if codecompanion_status.timer then + return + end + + codecompanion_status.timer = vim.uv.new_timer() + codecompanion_status.timer:start( + 0, + 120, + vim.schedule_wrap(function() + if codecompanion_status.active <= 0 then + return + end + + codecompanion_status.spinner = (codecompanion_status.spinner % #codecompanion_status.frames) + 1 + codecompanion_redraw_status() + end) + ) +end + +local function codecompanion_stop_spinner() + if codecompanion_status.timer then + codecompanion_status.timer:stop() + codecompanion_status.timer:close() + codecompanion_status.timer = nil + end + codecompanion_redraw_status() +end + +function _G.CodeCompanionStatus() + if codecompanion_status.active > 0 then + return string.format( + " AI %s %s", + codecompanion_status.frames[codecompanion_status.spinner], + codecompanion_status.message + ) + end + + if codecompanion_status.message ~= "" then + return " AI " .. codecompanion_status.message + end + + return "" +end + +opt.statusline = table.concat({ + "%f", + "%m%r%h%w", + "%=", + "%{v:lua.CodeCompanionStatus()}", + " %y", + " %l:%c", + " %P", +}) + +vim.api.nvim_create_autocmd("User", { + pattern = { + "CodeCompanionRequestStarted", + "CodeCompanionRequestStreaming", + "CodeCompanionRequestFinished", + }, + group = vim.api.nvim_create_augroup("DotfilesCodeCompanionStatus", { clear = true }), + callback = function(event) + local data = event.data or {} + + if event.match == "CodeCompanionRequestStarted" then + codecompanion_status.active = codecompanion_status.active + 1 + codecompanion_status.message = codecompanion_request_label(data) + codecompanion_start_spinner() + codecompanion_redraw_status() + return + end + + if event.match == "CodeCompanionRequestStreaming" then + codecompanion_status.message = "streaming " .. codecompanion_request_label(data) + codecompanion_redraw_status() + return + end + + codecompanion_status.active = math.max(0, codecompanion_status.active - 1) + local status = data.status or "success" + + if status == "success" then + codecompanion_status.message = "done" + elseif status == "cancelled" or status == "canceled" then + codecompanion_status.message = "cancelled" + else + codecompanion_status.message = "error" + vim.notify("CodeCompanion request failed: " .. tostring(status), vim.log.levels.WARN) + end + + if codecompanion_status.active == 0 then + codecompanion_stop_spinner() + codecompanion_clear_later() + else + codecompanion_redraw_status() + end + end, +}) + +vim.keymap.set({ "n", "v" }, "ca", "CodeCompanionActions", { + desc = "CodeCompanion actions", + silent = true, +}) +vim.keymap.set({ "n", "v" }, "cc", "CodeCompanionChat Toggle", { + desc = "Toggle CodeCompanion chat", + silent = true, +}) +vim.keymap.set("v", "cA", "CodeCompanionChat Add", { + desc = "Add selection to CodeCompanion chat", + silent = true, +}) +vim.keymap.set({ "n", "v" }, "ci", ":CodeCompanion ", { + desc = "CodeCompanion inline prompt", + silent = false, +}) +vim.keymap.set({ "n", "v" }, "cb", ":CodeCompanion #{buffer} ", { + desc = "CodeCompanion inline prompt with buffer", + silent = false, +}) +vim.cmd([[cab cc CodeCompanion]]) + +vim.api.nvim_create_autocmd("FileType", { + pattern = "gitcommit", + callback = function(event) + vim.keymap.set("n", "cm", function() + local prompt = table.concat({ + "#{buffer}", + "Draft a concise git commit message from the staged diff and status in this buffer.", + "Insert it at the top above the commented lines.", + "Use imperative mood.", + "Include a short body only if it adds useful context.", + }, " ") + vim.cmd("normal! gg") + vim.cmd("CodeCompanion " .. prompt) + end, { + buffer = event.buf, + desc = "Draft git commit message", + silent = true, + }) + end, +}) + +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not vim.uv.fs_stat(lazypath) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", + lazypath, + }) +end +vim.opt.rtp:prepend(lazypath) + +require("lazy").setup({ + { + "olimorris/codecompanion.nvim", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-treesitter/nvim-treesitter", + }, + opts = { + adapters = { + http = { + gemini_36_flash = function() + return require("codecompanion.adapters").extend("gemini", { + opts = { + stream = false, + tools = false, + vision = true, + }, + schema = { + model = { + default = "gemini-3.6-flash", + choices = { + ["gemini-3.6-flash"] = { + formatted_name = "Gemini 3.6 Flash", + meta = { context_window = 1050000 }, + opts = { + can_reason = true, + can_form_structured_outputs = true, + has_vision = true, + }, + }, + }, + }, + thinkingLevel = { + default = "minimal", + choices = { "minimal", "low", "medium", "high" }, + }, + }, + }) + end, + }, + acp = { + codex = function() + return require("codecompanion.adapters").extend("codex", { + defaults = { + auth_method = "chatgpt", + }, + commands = { + default = { + "npx", + "-y", + "@zed-industries/codex-acp", + }, + }, + }) + end, + }, + }, + interactions = { + chat = { + adapter = "codex", + }, + inline = { + adapter = "gemini_36_flash", + keymaps = { + stop = { + modes = { n = "cs" }, + }, + }, + }, + shared = { + keymaps = { + view_diff = { + modes = { n = "cd" }, + }, + accept_change = { + modes = { n = "cy" }, + }, + reject_change = { + modes = { n = "cn" }, + }, + always_accept = { + modes = { n = "cY" }, + }, + }, + }, + }, + }, + }, +}) diff --git a/ranger/rc.conf b/ranger/rc.conf index a59c083..10e01c1 100644 --- a/ranger/rc.conf +++ b/ranger/rc.conf @@ -265,9 +265,9 @@ map g/ cd / map E edit map du shell -p du --max-depth=1 -h --apparent-size map dU shell -p du --max-depth=1 -h --apparent-size | sort -rh -map yp shell -d echo -n %d/%f | xsel -i -map yd shell -d echo -n %d | xsel -i -map yn shell -d echo -n %f | xsel -i +map yp shell -d printf '%s' '%d/%f' | dotfiles-clipboard-copy +map yd shell -d printf '%s' '%d' | dotfiles-clipboard-copy +map yn shell -d printf '%s' '%f' | dotfiles-clipboard-copy # Filesystem Operations map = chmod diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..3eb756b --- /dev/null +++ b/setup.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APPLY=0 +INSTALL_PACKAGES=0 +DO_MUSIC=0 +DO_EDITOR=0 +DO_RANGER=0 +DO_GIT=0 + +usage() { + cat <<'USAGE' +Usage: ./setup.sh [--all] [--music] [--editor] [--ranger] [--git] [--install-packages] [--apply] + +By default this is a dry run. Pass --apply to make changes. + +Profiles: + --music Configure mpd/ncmpcpp + --editor Configure Neovim and Vim fallback + --ranger Configure ranger and clipboard helper + --git Configure Git terminal tools + --all Configure all current v1 profiles + +Options: + --install-packages Install Homebrew packages needed by selected profiles + --apply Make changes; otherwise only print actions + -h, --help Show this help +USAGE +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --all) + DO_MUSIC=1 + DO_EDITOR=1 + DO_RANGER=1 + DO_GIT=1 + ;; + --music) DO_MUSIC=1 ;; + --editor) DO_EDITOR=1 ;; + --ranger) DO_RANGER=1 ;; + --git) DO_GIT=1 ;; + --install-packages) INSTALL_PACKAGES=1 ;; + --apply) APPLY=1 ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if [ "$DO_MUSIC$DO_EDITOR$DO_RANGER$DO_GIT" = "0000" ]; then + usage >&2 + exit 2 +fi + +log() { + printf '%s\n' "$*" +} + +run() { + if [ "$APPLY" -eq 1 ]; then + "$@" + else + printf '[dry-run] ' + printf '%q ' "$@" + printf '\n' + fi +} + +timestamp() { + date +%Y%m%d%H%M%S +} + +backup_path() { + local path="$1" + if [ -e "$path" ] || [ -L "$path" ]; then + run mv "$path" "${path}.dotfiles-backup.$(timestamp)" + fi +} + +ensure_dir() { + run mkdir -p "$1" +} + +touch_file() { + if [ ! -e "$1" ]; then + run touch "$1" + fi +} + +link_path() { + local source="$1" + local dest="$2" + + if [ ! -e "$source" ] && [ ! -L "$source" ]; then + echo "ERROR: source does not exist: $source" >&2 + exit 1 + fi + + ensure_dir "$(dirname "$dest")" + + if [ -L "$dest" ] && [ "$(readlink "$dest")" = "$source" ]; then + log "already linked: $dest -> $source" + return + fi + + backup_path "$dest" + run ln -s "$source" "$dest" +} + +brew_bin() { + if command -v brew >/dev/null 2>&1; then + command -v brew + elif [ -x /opt/homebrew/bin/brew ]; then + printf '%s\n' /opt/homebrew/bin/brew + elif [ -x /usr/local/bin/brew ]; then + printf '%s\n' /usr/local/bin/brew + else + return 1 + fi +} + +install_brew_packages() { + local brew + if [ "$APPLY" -eq 0 ]; then + run brew install "$@" + return + fi + + brew="$(brew_bin)" || { + echo "ERROR: Homebrew not found; install packages manually or rerun without --install-packages" >&2 + exit 1 + } + run "$brew" install "$@" +} + +install_brew_casks() { + local brew + if [ "$APPLY" -eq 0 ]; then + run brew install --cask "$@" + return + fi + + brew="$(brew_bin)" || { + echo "ERROR: Homebrew not found; install casks manually or rerun without --install-packages" >&2 + exit 1 + } + run "$brew" install --cask "$@" +} + +setup_music() { + log "== music ==" + + if [ "$INSTALL_PACKAGES" -eq 1 ]; then + install_brew_packages mpd mpc ncmpcpp + install_brew_casks hammerspoon + fi + + ensure_dir "$HOME/.mpd/playlists" + touch_file "$HOME/.mpd/database" + touch_file "$HOME/.mpd/log" + touch_file "$HOME/.mpd/pid" + touch_file "$HOME/.mpd/state" + touch_file "$HOME/.mpd/sticker.sql" + ensure_dir "$HOME/.ncmpcpp" + + link_path "$ROOT/bin/dotfiles-mpd-restart" "$HOME/.local/bin/dotfiles-mpd-restart" + link_path "$ROOT/mpd/mpd-macos.conf" "$HOME/.mpd/mpd.conf" + link_path "$ROOT/ncmpcpp/config-macos" "$HOME/.ncmpcpp/config" + link_path "$ROOT/ncmpcpp/bindings" "$HOME/.ncmpcpp/bindings" + link_path "$ROOT/hammerspoon/init.lua" "$HOME/.hammerspoon/init.lua" +} + +setup_editor() { + log "== editor ==" + if [ "$INSTALL_PACKAGES" -eq 1 ]; then + local packages=() + command -v nvim >/dev/null 2>&1 || packages+=(neovim) + command -v npx >/dev/null 2>&1 || packages+=(node) + if [ "${#packages[@]}" -gt 0 ]; then + install_brew_packages "${packages[@]}" + fi + fi + + ensure_dir "$HOME/.local/state/nvim/undo" + if [ -e "$HOME/.config/nvim/init.vim" ] || [ -L "$HOME/.config/nvim/init.vim" ]; then + backup_path "$HOME/.config/nvim/init.vim" + fi + link_path "$ROOT/nvim/init.lua" "$HOME/.config/nvim/init.lua" + link_path "$ROOT/vim/vimrc-basic" "$HOME/.vimrc" + + run git config --global core.editor nvim + run git config --global commit.verbose true + run git config --global commit.status true +} + +setup_ranger() { + log "== ranger ==" + if [ "$INSTALL_PACKAGES" -eq 1 ]; then + install_brew_packages ranger + fi + + link_path "$ROOT/bin/dotfiles-clipboard-copy" "$HOME/.local/bin/dotfiles-clipboard-copy" + link_path "$ROOT/ranger/rc.conf" "$HOME/.config/ranger/rc.conf" + link_path "$ROOT/ranger/rifle.conf" "$HOME/.config/ranger/rifle.conf" + link_path "$ROOT/ranger/commands.py" "$HOME/.config/ranger/commands.py" + link_path "$ROOT/ranger/scope.sh" "$HOME/.config/ranger/scope.sh" + link_path "$ROOT/ranger/colorschemes" "$HOME/.config/ranger/colorschemes" +} + +setup_git_tools() { + log "== git tools ==" + if [ "$INSTALL_PACKAGES" -eq 1 ]; then + command -v gitui >/dev/null 2>&1 || install_brew_packages gitui + fi + + if [ -x /opt/homebrew/bin/gitui ] || [ -x /home/linuxbrew/.linuxbrew/bin/gitui ]; then + link_path "$ROOT/bin/dotfiles-gitui" "$HOME/.local/bin/gitui" + fi + + link_path "$ROOT/config/gitui/key_bindings.ron" "$HOME/.config/gitui/key_bindings.ron" +} + +if [ "$APPLY" -eq 0 ]; then + log "Dry run only. Re-run with --apply to make changes." +fi + +[ "$DO_MUSIC" -eq 1 ] && setup_music +[ "$DO_EDITOR" -eq 1 ] && setup_editor +[ "$DO_RANGER" -eq 1 ] && setup_ranger +[ "$DO_GIT" -eq 1 ] && setup_git_tools + +log "Done." diff --git a/vim/vimrc-basic b/vim/vimrc-basic new file mode 100644 index 0000000..869e17c --- /dev/null +++ b/vim/vimrc-basic @@ -0,0 +1,36 @@ +set nocompatible +filetype plugin indent on +syntax enable + +set encoding=utf-8 +set number +set ruler +set showcmd +set confirm +set autoread +set backspace=indent,eol,start + +set tabstop=4 +set shiftwidth=4 +set softtabstop=4 +set expandtab +set smartindent + +set ignorecase +set smartcase +set incsearch +set hlsearch +set wrapscan +set wildmenu +set timeoutlen=400 + +if has('clipboard') + set clipboard=unnamed +endif + +let mapleader="," + +inoremap jk +vnoremap < >gv +nnoremap h :nohlsearch