Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd06db0274 | ||
|
|
eacaebfdef | ||
|
|
b0a2f0b132 | ||
|
|
237a56fb6c | ||
|
|
7849b91f06 | ||
|
|
123c68dbae | ||
|
|
86d19db1e9 | ||
|
|
068dd8fe25 | ||
|
|
1b9b98c424 | ||
|
|
fe27e8178e |
@@ -2,6 +2,7 @@ existing
|
||||
minimal
|
||||
install_local.sh
|
||||
bin/dotfiles-install
|
||||
config/dejima
|
||||
config/i3/config.de
|
||||
config/i3/config.local
|
||||
config/gtk-3.0/bookmarks
|
||||
|
||||
Executable
+255
@@ -0,0 +1,255 @@
|
||||
#!/bin/bash
|
||||
# Read and write Gitea issues from a shell - the issue-side counterpart to
|
||||
# gitea-pr, and deliberately its twin rather than its sibling: both are
|
||||
# copied into the sandbox image as single self-contained files, and gitea-pr
|
||||
# also lives in a dotfiles checkout, so a shared library would be a third
|
||||
# thing to keep in step across three places. The duplicated config/token/curl
|
||||
# preamble is pinned in both files by test/static.bats instead.
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.config/gitea/token}"
|
||||
|
||||
function usage() {
|
||||
echo "Usage: gitea-issue list [--state open|closed|all] [--limit N]"
|
||||
echo " gitea-issue view NUMBER"
|
||||
echo " gitea-issue create --title TITLE [--body BODY | --body-file FILE]"
|
||||
echo " gitea-issue comment NUMBER (--body BODY | --body-file FILE)"
|
||||
echo " gitea-issue close NUMBER"
|
||||
echo " gitea-issue reopen NUMBER"
|
||||
echo ""
|
||||
echo "--body-file reads the body from FILE, or from stdin when FILE is '-'."
|
||||
echo "Prefer it over --body for anything with quotes, backticks or newlines."
|
||||
echo ""
|
||||
echo "Requires GITEA_SSH_HOST and GITEA_API set in the environment (e.g. in" \
|
||||
"your shell profile), and a token at $GITEA_TOKEN_FILE (or GITEA_TOKEN" \
|
||||
"set for this invocation only)."
|
||||
}
|
||||
|
||||
function require_config() {
|
||||
if [ -z "${GITEA_SSH_HOST:-}" ]; then
|
||||
echo "GITEA_SSH_HOST is not set. Add 'export GITEA_SSH_HOST=<gitea ssh hostname>'" \
|
||||
"(e.g. gitea.example.com) to ~/.profile.local" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${GITEA_API:-}" ]; then
|
||||
echo "GITEA_API is not set. Add 'export GITEA_API=https://<gitea host>/api/v1'" \
|
||||
"(e.g. https://gitea.example.com/api/v1) to ~/.profile.local" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function require_token() {
|
||||
if [ -z "${GITEA_TOKEN:-}" ] && [ -f "$GITEA_TOKEN_FILE" ]; then
|
||||
GITEA_TOKEN=$(<"$GITEA_TOKEN_FILE")
|
||||
fi
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
echo "No Gitea token found. Generate one in the Gitea web UI" \
|
||||
"(Settings > Applications, scopes write:repository + write:issue)" \
|
||||
"and save it to $GITEA_TOKEN_FILE (chmod 600), or set GITEA_TOKEN" \
|
||||
"in the environment for this invocation only." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Feeds the auth header to curl as a config on stdin rather than as an
|
||||
# argument. Command lines are world-readable through /proc, so `-H
|
||||
# "Authorization: token $GITEA_TOKEN"` exposes the token to every other
|
||||
# process on the host for as long as the request runs - and to anything that
|
||||
# scrapes `ps` output, including the agents this repo exists to sandbox.
|
||||
# printf is a bash builtin, so the token never lands in another argv.
|
||||
function gitea_api() {
|
||||
printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" |
|
||||
curl -sS -w '\n%{http_code}' -K - "$@"
|
||||
}
|
||||
|
||||
# Splits gitea_api's combined output into the globals `json` and `http_code`
|
||||
# and fails with the body on any non-2xx. Every call site did this by hand in
|
||||
# gitea-pr; there are six of them here, and six hand-rolled copies is where
|
||||
# one of them quietly stops checking.
|
||||
function api_or_die() {
|
||||
local what="$1"; shift
|
||||
local response
|
||||
response=$(gitea_api "$@")
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
json=$(echo "$response" | sed '$d')
|
||||
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
|
||||
echo "Failed to $what (HTTP $http_code):" >&2
|
||||
echo "$json" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Reads a body from a file, or from stdin for '-'. The point is to keep prose
|
||||
# out of argv: a body composed on a command line has to survive shell quoting,
|
||||
# and the things that break it - an apostrophe, a backtick, a $ - are exactly
|
||||
# what English and code fences are made of. A file (or a heredoc into '-') has
|
||||
# no such layer.
|
||||
function read_body_file() {
|
||||
local file="$1"
|
||||
if [ "$file" = "-" ]; then
|
||||
cat
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "No such body file: $file" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat "$file"
|
||||
}
|
||||
|
||||
function gitea_owner_repo() {
|
||||
local remote_url host owner_repo
|
||||
remote_url=$(git remote get-url origin 2>/dev/null) || {
|
||||
echo "No 'origin' remote found in this repository" >&2
|
||||
exit 1
|
||||
}
|
||||
host=$(echo "$remote_url" | sed -E 's#^[^@]+@([^:]+):.*#\1#')
|
||||
if [ "$host" != "$GITEA_SSH_HOST" ]; then
|
||||
echo "origin remote ($remote_url) is not on $GITEA_SSH_HOST; gitea-issue only supports this host" >&2
|
||||
exit 1
|
||||
fi
|
||||
owner_repo=$(echo "$remote_url" | sed -E 's#^[^@]+@[^:]+:##; s#\.git$##')
|
||||
echo "$owner_repo"
|
||||
}
|
||||
|
||||
function require_number() {
|
||||
if ! [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
||||
echo "Expected an issue number, got '${1:-}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Sets `body` from --body/--body-file, rejecting both. Shared by create and
|
||||
# comment so the two cannot drift into disagreeing about which one wins.
|
||||
function parse_body_args() {
|
||||
body=""
|
||||
local body_file=""
|
||||
parsed_title=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--title) parsed_title="$2"; shift 2 ;;
|
||||
--body) body="$2"; shift 2 ;;
|
||||
--body-file) body_file="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
# Rejected rather than ranked: silently preferring one would make a caller
|
||||
# that passed both ship a body it didn't mean to.
|
||||
if [ -n "$body" ] && [ -n "$body_file" ]; then
|
||||
echo "Pass --body or --body-file, not both" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$body_file" ]; then
|
||||
body="$(read_body_file "$body_file")"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
subcommand="$1"
|
||||
shift
|
||||
|
||||
case "$subcommand" in
|
||||
list|view|create|comment|close|reopen)
|
||||
require_config
|
||||
require_token
|
||||
owner_repo=$(gitea_owner_repo)
|
||||
owner="${owner_repo%%/*}"
|
||||
repo="${owner_repo##*/}"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
issues_api="${GITEA_API}/repos/${owner}/${repo}/issues"
|
||||
|
||||
case "$subcommand" in
|
||||
list)
|
||||
state="open"
|
||||
limit="30"
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--state) state="$2"; shift 2 ;;
|
||||
--limit) limit="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
case "$state" in
|
||||
open|closed|all) ;;
|
||||
*) echo "--state must be open, closed or all" >&2; exit 1 ;;
|
||||
esac
|
||||
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then
|
||||
echo "--limit must be a number" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# type=issues is load-bearing: Gitea's issues endpoint returns pull
|
||||
# requests too, since a PR *is* an issue with a branch attached.
|
||||
# Without it every open PR shows up here as an issue, which reads as
|
||||
# a work list that is quietly twice as long as the real one.
|
||||
api_or_die "list issues" \
|
||||
"${issues_api}?state=${state}&type=issues&limit=${limit}"
|
||||
echo "$json" | jq -r '.[] | "#\(.number)\t[\(.state)]\t\(.title)\t(\(.comments) comments)"'
|
||||
;;
|
||||
view)
|
||||
require_number "${1:-}"
|
||||
number="$1"
|
||||
|
||||
api_or_die "fetch issue #${number}" "${issues_api}/${number}"
|
||||
echo "$json" | jq -r '"#\(.number) [\(.state)] \(.title)\n[\(.user.login) @ \(.created_at)]\n\(.body // "(no description)")\n"'
|
||||
|
||||
echo "=== Comments ==="
|
||||
api_or_die "fetch comments for issue #${number}" "${issues_api}/${number}/comments"
|
||||
echo "$json" | jq -r '.[] | "[\(.user.login) @ \(.created_at)]\n\(.body)\n"'
|
||||
;;
|
||||
create)
|
||||
parse_body_args "$@"
|
||||
if [ -z "$parsed_title" ]; then
|
||||
echo "gitea-issue create requires --title" >&2
|
||||
exit 1
|
||||
fi
|
||||
body="**Filed by LLM agent**
|
||||
|
||||
${body}"
|
||||
|
||||
api_or_die "create issue" -X POST "$issues_api" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg title "$parsed_title" --arg body "$body" \
|
||||
'{title: $title, body: $body}')"
|
||||
echo "$json" | jq -r '.html_url'
|
||||
;;
|
||||
comment)
|
||||
require_number "${1:-}"
|
||||
number="$1"
|
||||
shift
|
||||
parse_body_args "$@"
|
||||
if [ -z "$body" ]; then
|
||||
echo "gitea-issue comment requires --body or --body-file" >&2
|
||||
exit 1
|
||||
fi
|
||||
body="**Comment by LLM agent**
|
||||
|
||||
${body}"
|
||||
|
||||
api_or_die "comment on issue #${number}" -X POST "${issues_api}/${number}/comments" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg body "$body" '{body: $body}')"
|
||||
echo "$json" | jq -r '.html_url'
|
||||
;;
|
||||
close|reopen)
|
||||
require_number "${1:-}"
|
||||
number="$1"
|
||||
new_state="closed"
|
||||
[ "$subcommand" = "reopen" ] && new_state="open"
|
||||
|
||||
api_or_die "${subcommand} issue #${number}" -X PATCH "${issues_api}/${number}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg state "$new_state" '{state: $state}')"
|
||||
echo "$json" | jq -r '"#\(.number) is now \(.state): \(.html_url)"'
|
||||
;;
|
||||
esac
|
||||
+35
-3
@@ -4,10 +4,13 @@ set -euo pipefail
|
||||
GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.config/gitea/token}"
|
||||
|
||||
function usage() {
|
||||
echo "Usage: gitea-pr create [--title TITLE] [--body BODY] [--base BRANCH]"
|
||||
echo "Usage: gitea-pr create [--title TITLE] [--body BODY | --body-file FILE] [--base BRANCH]"
|
||||
echo " gitea-pr comments PR_NUMBER"
|
||||
echo " gitea-pr merge PR_NUMBER"
|
||||
echo ""
|
||||
echo "--body-file reads the body from FILE, or from stdin when FILE is '-'."
|
||||
echo "Prefer it over --body for anything with quotes, backticks or newlines."
|
||||
echo ""
|
||||
echo "Requires GITEA_SSH_HOST and GITEA_API set in the environment (e.g. in" \
|
||||
"your shell profile), and a token at $GITEA_TOKEN_FILE (or GITEA_TOKEN" \
|
||||
"set for this invocation only)."
|
||||
@@ -16,12 +19,12 @@ function usage() {
|
||||
function require_config() {
|
||||
if [ -z "${GITEA_SSH_HOST:-}" ]; then
|
||||
echo "GITEA_SSH_HOST is not set. Add 'export GITEA_SSH_HOST=<gitea ssh hostname>'" \
|
||||
"(e.g. gitbox.bradina.localdomain) to ~/.profile.local" >&2
|
||||
"(e.g. gitea.example.com) to ~/.profile.local" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${GITEA_API:-}" ]; then
|
||||
echo "GITEA_API is not set. Add 'export GITEA_API=https://<gitea host>/api/v1'" \
|
||||
"(e.g. https://gitbox.branderson.io/api/v1) to ~/.profile.local" >&2
|
||||
"(e.g. https://gitea.example.com/api/v1) to ~/.profile.local" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -50,6 +53,24 @@ function gitea_api() {
|
||||
curl -sS -w '\n%{http_code}' -K - "$@"
|
||||
}
|
||||
|
||||
# Reads a body from a file, or from stdin for '-'. The point is to keep prose
|
||||
# out of argv: a body composed on a command line has to survive shell quoting,
|
||||
# and the things that break it - an apostrophe, a backtick, a $ - are exactly
|
||||
# what English and code fences are made of. A file (or a heredoc into '-') has
|
||||
# no such layer.
|
||||
function read_body_file() {
|
||||
local file="$1"
|
||||
if [ "$file" = "-" ]; then
|
||||
cat
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "No such body file: $file" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat "$file"
|
||||
}
|
||||
|
||||
function gitea_owner_repo() {
|
||||
local remote_url host owner_repo
|
||||
remote_url=$(git remote get-url origin 2>/dev/null) || {
|
||||
@@ -91,15 +112,26 @@ case "$subcommand" in
|
||||
create)
|
||||
title=""
|
||||
body=""
|
||||
body_file=""
|
||||
base=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--title) title="$2"; shift 2 ;;
|
||||
--body) body="$2"; shift 2 ;;
|
||||
--body-file) body_file="$2"; shift 2 ;;
|
||||
--base) base="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
# Rejected rather than ranked: silently preferring one would make a
|
||||
# caller that passed both ship a body it didn't mean to.
|
||||
if [ -n "$body" ] && [ -n "$body_file" ]; then
|
||||
echo "Pass --body or --body-file, not both" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$body_file" ]; then
|
||||
body="$(read_body_file "$body_file")"
|
||||
fi
|
||||
body="**Summary generated by LLM agent**
|
||||
|
||||
${body}"
|
||||
|
||||
@@ -47,6 +47,16 @@ if vim.env.DEJIMA then
|
||||
vim.schedule(function()
|
||||
require("lazy").load({ plugins = { "claudecode.nvim" } })
|
||||
vim.cmd("ClaudeCode")
|
||||
-- :ClaudeCode leaves the terminal window focused, so `tab split` gives
|
||||
-- the same terminal buffer a full-screen window in its own tab while
|
||||
-- the original tab keeps the narrow split next to the editor - one tab
|
||||
-- to work in Claude, one to work side-by-side. Window-local options are
|
||||
-- inherited by the split, and snacks' auto_insert (buffer-local) puts
|
||||
-- the new window back into terminal mode on entry.
|
||||
local claude_buf = require("claudecode.terminal").get_active_terminal_bufnr()
|
||||
if claude_buf and vim.api.nvim_get_current_buf() == claude_buf then
|
||||
vim.cmd("tab split")
|
||||
end
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
local servers = { "basedpyright", "ruff" }
|
||||
-- ruff is a prebuilt GitHub release binary, but mason installs basedpyright
|
||||
-- from PyPI into a venv. Debian ships venv's ensurepip separately
|
||||
-- (python3-venv), and without it every startup fails the install.
|
||||
local servers = { "ruff" }
|
||||
|
||||
local function has_python_venv()
|
||||
if vim.fn.executable("python3") ~= 1 then
|
||||
return false
|
||||
end
|
||||
vim.fn.system({ "python3", "-c", "import ensurepip" })
|
||||
return vim.v.shell_error == 0
|
||||
end
|
||||
|
||||
if has_python_venv() then
|
||||
table.insert(servers, "basedpyright")
|
||||
else
|
||||
vim.notify("python3 venv unavailable (install python3-venv): skipping LSP server basedpyright", vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
local npm_servers = { "ts_ls", "svelte", "html", "cssls", "bashls" }
|
||||
local function has_public_npm()
|
||||
|
||||
+18
-5
@@ -87,13 +87,26 @@ if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3003) exit 0;
|
||||
# itself using default-terminal above (screen-256color) -- 'xterm*' would
|
||||
# never match that, breaking the nested case this is meant to support.
|
||||
#
|
||||
# Investigated as a suspect for multi-line paste getting corrupted with
|
||||
# literal CSI 27;<mod>;<key>~ garbage in place of newlines (see Claude Code
|
||||
# issues #43169 and #30239) -- turning this off did not fix that paste
|
||||
# corruption, so it's an upstream Claude Code paste-tokenizer bug, not
|
||||
# something caused by this setting. Restored.
|
||||
# Not the cause of multi-line paste arriving with literal CSI 27;5;106~ in
|
||||
# place of each newline (that being C-j, the pasted newline, in
|
||||
# modifyOtherKeys form). That is a tmux 3.5/3.5a bug: 3.5 rewrote extended-key
|
||||
# handling and encodes keys inside a bracketed paste, which the layer further
|
||||
# in then passes through verbatim rather than decoding. Reproduced against
|
||||
# 3.2a/3.3a/3.4/3.5a/3.6b/3.7c -- only 3.5 and 3.5a corrupt, and only on a hop
|
||||
# whose downstream has asked for modifyOtherKeys mode 2, which a nested tmux
|
||||
# always does and a bare application under screen-256color generally doesn't.
|
||||
# Hence it shows up with tmux at both ends of the ssh and not with one.
|
||||
# Fixed upstream in 3.6, so upgrade the offending layer where that's possible.
|
||||
if-shell "tmux -V | awk '{print $2}' | awk -F. '{if ($1*1000+$2 >= 3002) exit 0; else exit 1}'" 'set -s extended-keys on; set -as terminal-features "*:extkeys"'
|
||||
|
||||
# Where it isn't -- a host pinned to a distro tmux -- give the paste back on
|
||||
# just those two versions. Nothing narrower helps: csi-u format only changes
|
||||
# which escape lands in the text, and withholding the extkeys feature above
|
||||
# stops this tmux asking its own parent for extended keys without stopping it
|
||||
# encoding them for the pane. Turning them off is the whole of the workaround,
|
||||
# and confining it to 3.5/3.5a keeps them everywhere else.
|
||||
if-shell "tmux -V | awk '{hit = ($2 ~ /^3[.]5/)} END {exit hit ? 0 : 1}'" 'set -s extended-keys off'
|
||||
|
||||
# Accept pane-originated OSC 52 clipboard-set attempts into a real tmux
|
||||
# buffer. Only on-vs-off matters here -- off would refuse to even look at
|
||||
# these escapes; "on" (rather than "external") is needed so the pane-set-
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
|
||||
bind-key -t vi-copy PPage page-up
|
||||
bind-key -t vi-copy NPage page-down
|
||||
bind-key -t vi-copy WheelUpPane halfpage-up
|
||||
bind-key -t vi-copy WheelDownPane halfpage-down
|
||||
# Mouse wheel scrolls 4 lines per tick, not tmux's half-page default (20 lines
|
||||
# in a 40-line pane). Old bind-key has no -N; -R is the equivalent repeat
|
||||
# count for keys in the copy-mode tables.
|
||||
bind-key -R 4 -t vi-copy WheelUpPane scroll-up
|
||||
bind-key -R 4 -t vi-copy WheelDownPane scroll-down
|
||||
bind-key -t vi-copy v begin-selection
|
||||
bind-key -t vi-copy y copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
|
||||
unbind-key -t vi-copy Enter
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
bind-key -T copy-mode-vi PPage send-keys -X page-up
|
||||
bind-key -T copy-mode-vi NPage send-keys -X page-down
|
||||
bind-key -T copy-mode-vi WheelUpPane send-keys -X halfpage-up
|
||||
bind-key -T copy-mode-vi WheelDownPane send-keys -X halfpage-down
|
||||
# Mouse wheel scrolls 4 lines per tick, not tmux's half-page default (20 lines
|
||||
# in a 40-line pane). -N is the repeat count; scroll-up/scroll-down take no
|
||||
# direct amount argument and tmux resets the count after each command.
|
||||
bind-key -T copy-mode-vi WheelUpPane send-keys -X -N 4 scroll-up
|
||||
bind-key -T copy-mode-vi WheelDownPane send-keys -X -N 4 scroll-down
|
||||
bind-key -T copy-mode-vi v send-keys -X begin-selection
|
||||
bind-key -T copy-mode-vi y send-keys -X copy-pipe "$HOME/dotfiles/bin/clipboard-copy"
|
||||
unbind-key -T copy-mode-vi Enter
|
||||
|
||||
@@ -6,6 +6,9 @@ powerline
|
||||
thefuck
|
||||
tree
|
||||
nodejs
|
||||
# Debian splits venv's ensurepip out of python3; mason needs `python3 -m venv`
|
||||
# to install PyPI-sourced LSP servers (basedpyright)
|
||||
python3-venv
|
||||
zsh
|
||||
htop
|
||||
jq
|
||||
|
||||
Reference in New Issue
Block a user