commit 67c20e9dabb57cc1b93a00c30ed60e140651c29e Author: ai Date: Fri Apr 3 08:36:53 2026 +0000 init diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..2ff1666 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,11 @@ +[user] + name = ai + email = ai@syui.ai +[core] + editor = vim +[pull] + rebase = false +[init] + defaultBranch = main +[push] + autoSetupRemote = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ae9fdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.env +.claude +Cargo.lock +node_modules +package-lock.json +/target +/dist +/tmp +*.swp diff --git a/.tmux.conf b/.tmux.conf new file mode 100644 index 0000000..6f601e8 --- /dev/null +++ b/.tmux.conf @@ -0,0 +1,50 @@ +# prefix +unbind C-b +set -g prefix ^T +bind t send-prefix + +# general +set -s escape-time 0 +set -g base-index 1 +set -g pane-base-index 1 +set -g renumber-windows on +set -g history-limit 10000 +set -g default-terminal "xterm-256color" +set -g allow-passthrough on +set -g set-clipboard on +set-window-option -g mode-keys vi +set-window-option -g xterm-keys on + +# pane/window +bind s split-window -v -c "#{pane_current_path}" +bind v split-window -h -c "#{pane_current_path}" +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R +bind -r C-h resize-pane -L 5 +bind -r C-j resize-pane -D 5 +bind -r C-k resize-pane -U 5 +bind -r C-l resize-pane -R 5 +bind K kill-pane +bind c new-window +bind w choose-window +bind C-t run "tmux last-pane || tmux last-window || tmux new-window" + +# copy +bind ^y copy-mode +bind -r ^"[" copy-mode +bind p paste-buffer +bind-key -T copy-mode-vi v send-keys -X begin-selection +bind-key -T copy-mode-vi Space send-keys -X begin-selection +if-shell 'which xclip' \ + 'bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "xclip -i -sel c"; bind-key -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xclip -i -sel c"' + +# url open +bind u run-shell "~/.tmux/plugin/urlopen.zsh" + +# plugins +run-shell "~/.tmux/plugin/ailog.zsh" + +# reload +bind r source-file ~/.tmux.conf\; display-message "reloaded" diff --git a/.tmux/plugin/ailog.zsh b/.tmux/plugin/ailog.zsh new file mode 100755 index 0000000..4352613 --- /dev/null +++ b/.tmux/plugin/ailog.zsh @@ -0,0 +1,131 @@ +#!/bin/zsh +# tmux plugin: ailog powerline status bar +# requires: aifont, ailog, jq + +INTERVAL=60 +SEP=$'\ue0b0' +RSEP=$'\ue0b2' + +get_icon() { + local handle=$(ailog pds s 2>/dev/null | jq -r '.handle // empty' 2>/dev/null) + case "$handle" in + ai.*) echo $'\ue001' ;; + syui.*) echo $'\ue002' ;; + *) echo "❯" ;; + esac +} + +get_cfg() { + case "$(uname)" in + Darwin) echo "$HOME/Library/Application Support/ai.syui.log/config.json" ;; + *) echo "${XDG_CONFIG_HOME:-$HOME/.config}/ai.syui.log/config.json" ;; + esac +} + +has_aifont() { + [ -f "$HOME/Library/Fonts/aifont.ttf" ] || [ -f "/usr/share/fonts/TTF/aifont.ttf" ] +} + +can_run() { + local cfg=$(get_cfg) + command -v ailog &>/dev/null && command -v jq &>/dev/null && [ -f "$cfg" ] && has_aifont +} + +refresh_token() { + local stamp="/tmp/.ailog_refresh" + local now=$(date +%s) + local last=0 + [ -f "$stamp" ] && last=$(cat "$stamp") + if [ $((now - last)) -gt 1800 ]; then + ailog pds r 2>/dev/null + echo "$now" > "$stamp" + fi +} + +get_handle() { + ailog pds s 2>/dev/null | jq -r '.handle // empty' 2>/dev/null +} + +get_notify_count() { + ailog notify count 2>/dev/null | jq -r '.count // 0' 2>/dev/null +} + +get_latest_post() { + local data=$(ailog f tl -l 1 2>/dev/null | tr -d '\t' | sed 's/\\n/ /g') + local handle=$(echo "$data" | jq -r '.feed[0].post.author.handle // empty' 2>/dev/null | cut -d. -f1) + local full=$(echo "$data" | jq -r '.feed[0].post.record.text // empty' 2>/dev/null | tr '\n' ' ') + local text="${full:0:20}" + [[ ${#full} -gt 20 ]] && text+="..." + [ -n "$text" ] && echo "@${handle} ${text}" +} + +status_left() { + can_run || return + refresh_token + local handle=$(get_handle) + local count=$(get_notify_count) + local post=$(get_latest_post) + + local icon=$(get_icon) + local out="" + out+="#[fg=yellow,bg=black] ${icon} " + out+="#[fg=black,bg=colour234]${SEP}" + out+="#[fg=yellow,bg=colour234] @${handle} " + if [ -n "$count" ] && [ "$count" != "0" ] && [ "$count" != "null" ]; then + out+="#[fg=colour234,bg=black]${SEP}" + out+="#[fg=yellow,bg=black] ${count} " + else + out+="#[fg=colour234,bg=black]${SEP}" + fi + if [ -n "$post" ]; then + out+="#[fg=white,bg=black] ${post} " + fi + out+="#[fg=black,bg=default]${SEP}#[default] " + echo -n "$out" +} + +get_pds() { + ailog pds s 2>/dev/null | jq -r '.pds // empty' 2>/dev/null | sed 's|https://||' +} + +get_pds_icon() { + local pds="$1" + local ai_icon=$(printf '\ue001') + local bsky_icon=$(printf '\ue003') + case "$pds" in + *syu.is*) echo "#[fg=yellow]${ai_icon} #[fg=cyan]${pds}" ;; + *bsky*) echo "#[fg=blue]${bsky_icon} #[fg=cyan]${pds}" ;; + *) echo "#[fg=cyan]${pds}" ;; + esac +} + +status_right() { + local pds=$(get_pds) + local out="" + if [ -n "$pds" ]; then + local pds_display=$(get_pds_icon "$pds") + out+="#[fg=colour236]${RSEP}#[bg=colour236] ${pds_display} " + fi + out+="#[fg=colour234]${RSEP}#[fg=white,bg=colour234] #S " + echo -n "$out" +} + +case "$1" in + left) status_left ;; + right) status_right ;; + notify) get_notify_count ;; + handle) get_handle ;; + latest) get_latest_post ;; + *) + if can_run; then + tmux set-option -g status-style "bg=default,fg=white" + tmux set-window-option -g window-status-format "" + tmux set-window-option -g window-status-current-format "" + tmux set-option -g status-left-length 100 + tmux set-option -g status-right-length 50 + tmux set-option -g status-left "#(~/.tmux/plugin/ailog.zsh left)" + tmux set-option -g status-right "#(~/.tmux/plugin/ailog.zsh right)#[fg=black,bg=colour234]${RSEP}#[fg=white,bg=black] %Y-%m-%dT%H:%M " + tmux set-option -g status-interval $INTERVAL + fi + ;; +esac diff --git a/.tmux/plugin/urlopen.zsh b/.tmux/plugin/urlopen.zsh new file mode 100755 index 0000000..b08cfe2 --- /dev/null +++ b/.tmux/plugin/urlopen.zsh @@ -0,0 +1,7 @@ +#!/bin/zsh +url=$(tmux capture-pane -pJ -S -100 | grep -oE 'https?://[^ ]+' | tail -1) +[ -z "$url" ] && exit 0 +case "$(uname)" in + Darwin) open "$url" ;; + *) xdg-open "$url" ;; +esac diff --git a/.vim/plugin/autosave.vim b/.vim/plugin/autosave.vim new file mode 100644 index 0000000..3f41f53 --- /dev/null +++ b/.vim/plugin/autosave.vim @@ -0,0 +1,25 @@ +if exists('g:loaded_autosave') + finish +endif +let g:loaded_autosave = 1 + +let g:autosave = get(g:, 'autosave', 0) + +fu! s:autosave_start() + aug autosave + au! + au TextChanged,CursorHold,InsertLeave * sil! update + aug END +endf + +fu! s:autosave_stop() + au! autosave + echo 'autosave: off' +endf + +com! AirsaveOn call autosave_start() +com! AirsaveOff call autosave_stop() + +if g:autosave + call autosave_start() +endif diff --git a/.vimrc b/.vimrc new file mode 100644 index 0000000..3ef1e99 --- /dev/null +++ b/.vimrc @@ -0,0 +1,38 @@ +set nocompatible +set encoding=utf-8 +set fileencoding=utf-8 +set number +set ruler +set cursorline +set showmatch +set laststatus=2 +set wildmenu +set showcmd + +set tabstop=4 +set shiftwidth=4 +set softtabstop=4 +set autoindent +set smartindent + +set incsearch +set hlsearch +set ignorecase +set smartcase +set wrapscan + +set backspace=indent,eol,start +set clipboard=unnamed +set mouse=a +set ttimeoutlen=10 +set undofile +set undodir=~/.vim/undo + +let g:autosave = 1 +inoremap +nnoremap Q :q! +vnoremap v V + +syntax on +filetype plugin indent on +au BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$") | exe "normal! g`\"" | endif diff --git a/.zsh/plugin/cdselect.zsh b/.zsh/plugin/cdselect.zsh new file mode 100644 index 0000000..1a908c6 --- /dev/null +++ b/.zsh/plugin/cdselect.zsh @@ -0,0 +1,69 @@ +autoload -Uz chpwd_recent_dirs cdr add-zsh-hook +add-zsh-hook chpwd chpwd_recent_dirs +zstyle ':chpwd:*' recent-dirs-max 50 + +cdselect() { + local dirs=("${(@f)$(cdr -l 2>/dev/null | sed 's/^[0-9]* *//')}") + [[ ${#dirs} -eq 0 ]] && return + local query="" cursor=1 selected="" + while true; do + local filtered=() + for d in "${dirs[@]}"; do + local match=1 + for word in ${(s: :)query}; do + [[ "${d:l}" != *"${word:l}"* ]] && match=0 && break + done + [[ $match -eq 1 ]] && filtered+=("$d") + done + # sort by path length (shortest first) + local sorted=() + for f in "${filtered[@]}"; do sorted+=("${#f} $f"); done + sorted=(${(n)sorted}) + filtered=() + for s in "${sorted[@]}"; do filtered+=("${s#* }"); done + [[ $cursor -gt ${#filtered} ]] && cursor=${#filtered} + [[ $cursor -lt 1 ]] && cursor=1 + clear + echo "cd> $query" + local i=1 + for d in "${filtered[@]}"; do + if [[ $i -eq $cursor ]]; then + printf "\e[7m %s\e[0m\n" "$d" + else + printf " %s\n" "$d" + fi + ((i++)) + [[ $i -gt 20 ]] && break + done + read -k1 key 2>/dev/null + if [[ "$key" == $'\x1b' ]]; then + read -k1 -t 0.1 k2 2>/dev/null + if [[ "$k2" == "[" ]]; then + read -k1 -t 0.1 k3 2>/dev/null + case "$k3" in + A) ((cursor--)) ;; # up + B) ((cursor++)) ;; # down + esac + continue + else + break # plain ESC + fi + fi + case "$key" in + $'\n'|$'\r') [[ ${#filtered} -gt 0 ]] && selected="${filtered[$cursor]}"; break ;; + $'\x0e') ((cursor++)) ;; # C-n + $'\x10') ((cursor--)) ;; # C-p + $'\x7f'|$'\b') query="${query%?}"; cursor=1 ;; + *) query+="$key"; cursor=1 ;; + esac + done + clear + if [[ -n "$selected" ]]; then + selected="${selected/#\~/$HOME}" + selected="${selected//\\ / }" + cd "$selected" + fi + zle reset-prompt +} +zle -N cdselect +bindkey '^j' cdselect diff --git a/.zsh/plugin/cdup.zsh b/.zsh/plugin/cdup.zsh new file mode 100644 index 0000000..660b752 --- /dev/null +++ b/.zsh/plugin/cdup.zsh @@ -0,0 +1,3 @@ +cdup() { [[ -z "$BUFFER" ]] && { cd ..; zle reset-prompt } || zle self-insert 'k' } +zle -N cdup +bindkey '^k' cdup diff --git a/.zsh/plugin/clipcopy.zsh b/.zsh/plugin/clipcopy.zsh new file mode 100644 index 0000000..c296107 --- /dev/null +++ b/.zsh/plugin/clipcopy.zsh @@ -0,0 +1,14 @@ +# copy prompt buffer to clipboard +# C-p C-p (chord) + +clipcopy() { + if [[ -n "$BUFFER" ]]; then + case "$(uname)" in + Darwin) echo -n "$BUFFER" | pbcopy ;; + *) echo -n "$BUFFER" | xclip -sel c 2>/dev/null ;; + esac + zle -M "copied" + fi +} +zle -N clipcopy +bindkey '^p^p' clipcopy diff --git a/.zsh/plugin/fileselect.zsh b/.zsh/plugin/fileselect.zsh new file mode 100644 index 0000000..3813343 --- /dev/null +++ b/.zsh/plugin/fileselect.zsh @@ -0,0 +1,61 @@ +# file select - search files and insert into prompt +# C-f: open file selector + +fileselect() { + local files=("${(@f)$(find . -maxdepth 3 -not -path '*/\.git/*' -type f 2>/dev/null | sed 's|^\./||' | sort)}") + [[ ${#files} -eq 0 ]] && return + local query="" cursor=1 selected="" + while true; do + local filtered=() + for f in "${files[@]}"; do + local match=1 + for word in ${(s: :)query}; do + [[ "${f:l}" != *"${word:l}"* ]] && match=0 && break + done + [[ $match -eq 1 ]] && filtered+=("$f") + done + [[ $cursor -gt ${#filtered} ]] && cursor=${#filtered} + [[ $cursor -lt 1 ]] && cursor=1 + clear + echo "file> $query (${#filtered})" + local i=1 + for f in "${filtered[@]}"; do + if [[ $i -eq $cursor ]]; then + printf "\e[7m %s\e[0m\n" "$f" + else + printf " %s\n" "$f" + fi + ((i++)) + [[ $i -gt 20 ]] && break + done + read -k1 key 2>/dev/null + if [[ "$key" == $'\x1b' ]]; then + read -k1 -t 0.1 k2 2>/dev/null + if [[ "$k2" == "[" ]]; then + read -k1 -t 0.1 k3 2>/dev/null + case "$k3" in + A) ((cursor--)) ;; + B) ((cursor++)) ;; + esac + continue + else + break + fi + fi + case "$key" in + $'\n'|$'\r') [[ ${#filtered} -gt 0 ]] && selected="${filtered[$cursor]}"; break ;; + $'\x0e') ((cursor++)) ;; # C-n + $'\x10') ((cursor--)) ;; # C-p + $'\x7f'|$'\b') query="${query%?}"; cursor=1 ;; + *) query+="$key"; cursor=1 ;; + esac + done + clear + if [[ -n "$selected" ]]; then + BUFFER+="$selected" + CURSOR=${#BUFFER} + fi + zle reset-prompt +} +zle -N fileselect +bindkey '^f' fileselect diff --git a/.zsh/plugin/histselect.zsh b/.zsh/plugin/histselect.zsh new file mode 100644 index 0000000..dbcba85 --- /dev/null +++ b/.zsh/plugin/histselect.zsh @@ -0,0 +1,61 @@ +# history select - search history and insert into prompt +# C-r: open history selector, type to filter, C-n/C-p to move, Enter to insert + +histselect() { + local lines=("${(@f)$(fc -l -n -r 1 | awk '!seen[$0]++')}") + [[ ${#lines} -eq 0 ]] && return + local query="" cursor=1 selected="" + while true; do + local filtered=() + for l in "${lines[@]}"; do + local match=1 + for word in ${(s: :)query}; do + [[ "${l:l}" != *"${word:l}"* ]] && match=0 && break + done + [[ $match -eq 1 ]] && filtered+=("$l") + done + [[ $cursor -gt ${#filtered} ]] && cursor=${#filtered} + [[ $cursor -lt 1 ]] && cursor=1 + clear + echo "hist> $query" + local i=1 + for l in "${filtered[@]}"; do + if [[ $i -eq $cursor ]]; then + printf "\e[7m %s\e[0m\n" "$l" + else + printf " %s\n" "$l" + fi + ((i++)) + [[ $i -gt 20 ]] && break + done + read -k1 key 2>/dev/null + if [[ "$key" == $'\x1b' ]]; then + read -k1 -t 0.1 k2 2>/dev/null + if [[ "$k2" == "[" ]]; then + read -k1 -t 0.1 k3 2>/dev/null + case "$k3" in + A) ((cursor--)) ;; + B) ((cursor++)) ;; + esac + continue + else + break + fi + fi + case "$key" in + $'\n'|$'\r') [[ ${#filtered} -gt 0 ]] && selected="${filtered[$cursor]}"; break ;; + $'\x0e') ((cursor++)) ;; # C-n + $'\x10') ((cursor--)) ;; # C-p + $'\x7f'|$'\b') query="${query%?}"; cursor=1 ;; + *) query+="$key"; cursor=1 ;; + esac + done + clear + if [[ -n "$selected" ]]; then + BUFFER="$selected" + CURSOR=${#BUFFER} + fi + zle reset-prompt +} +zle -N histselect +bindkey '^r' histselect diff --git a/.zsh/plugin/powerline.zsh b/.zsh/plugin/powerline.zsh new file mode 100644 index 0000000..c860c7d --- /dev/null +++ b/.zsh/plugin/powerline.zsh @@ -0,0 +1,57 @@ +# powerline prompt +# requires: aifont (MesloLGS NF + ai/syui icons) + +_has_aifont() { + [[ -f "$HOME/Library/Fonts/aifont.ttf" ]] || [[ -f "/usr/share/fonts/TTF/aifont.ttf" ]] +} + +_powerline_git() { + local branch=$(git symbolic-ref --short HEAD 2>/dev/null) || return + local dirty="" + [[ -n $(git status --porcelain 2>/dev/null) ]] && dirty="*" + echo "${branch}${dirty}" +} + +_powerline_icon() { + case "$USER" in + syui) echo $'\ue002' ;; + ai) echo $'\ue001' ;; + *) echo "❯" ;; + esac +} + +_powerline_prompt() { + local icon=$(_powerline_icon) + local sep=$'\ue0b0' + local dir="${PWD/#$HOME/~}" + local git=$(_powerline_git) + + local prompt="" + local icon_color="yellow" + [[ -n "$SSH_CONNECTION" ]] && icon_color="cyan" + if [[ -n "$icon" ]]; then + prompt+="%F{${icon_color}}%K{black} ${icon} %k%f" + prompt+="%F{black}%K{234}${sep}%f" + else + prompt+="%F{234}${sep}%f" + fi + prompt+="%F{yellow}%K{234} $USER %k%f" + prompt+="%F{234}%K{236}${sep}%f" + prompt+="%F{white}%K{236} ${dir} %k%f" + + if [[ -n "$git" ]]; then + prompt+="%F{236}%K{234}${sep}%f" + prompt+="%F{cyan}%K{234} ${git} %k%f" + prompt+="%F{234}${sep}%f" + else + prompt+="%F{236}${sep}%f" + fi + prompt+="%k%f " + + echo "$prompt" +} + +if _has_aifont; then + setopt PROMPT_SUBST + PROMPT='$(_powerline_prompt)' +fi diff --git a/.zshrc b/.zshrc new file mode 100644 index 0000000..cf0cd51 --- /dev/null +++ b/.zshrc @@ -0,0 +1,53 @@ +export CARGO_HOME="$HOME/.cargo" +export RUSTUP_HOME="$HOME/.rustup" +export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH" + +if command -v tmux &>/dev/null && [ -z "$TMUX" ] && [ -z "$SSH_CONNECTION" ]; then + tmux new +fi + +alias c="claude" +alias t="tmux" +alias v="vim" +alias ts="vim ~/.tmux.conf" +alias vs="vim ~/.vimrc" +alias zs="vim ~/.zshrc" +alias zr="exec $SHELL && . ~/.zshrc" +alias ll="ls -alh" +alias df="df -H" + +zmodload zsh/complist +zstyle ':completion:*' menu select +zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' +autoload -Uz compinit +compinit +bindkey -M menuselect '^n' down-line-or-history +bindkey -M menuselect '^p' up-line-or-history + +HISTSIZE=10000 +SAVEHIST=10000 +HISTFILE=~/.zsh_history +setopt SHARE_HISTORY +setopt HIST_IGNORE_DUPS + +case $OSTYPE in + linux*) + alias ls="ls -a --color=auto" + alias ll="ls -alh --color=auto" + alias u="sudo pacman -Syu --noconfirm" + fpath=(/usr/share/zsh/site-functions $fpath) + ;; +esac + +for p in /usr/share/zsh/plugins; do + [ -f "$p/zsh-autosuggestions/zsh-autosuggestions.zsh" ] && source "$p/zsh-autosuggestions/zsh-autosuggestions.zsh" + [ -f "$p/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" ] && source "$p/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" + [ -f "$p/zsh-history-substring-search/zsh-history-substring-search.zsh" ] && source "$p/zsh-history-substring-search/zsh-history-substring-search.zsh" +done + +bindkey '^[[A' history-substring-search-up +bindkey '^[[B' history-substring-search-down + +chpwd() { ls } + +for f in ~/.zsh/plugin/*.zsh; do source "$f"; done diff --git a/install.zsh b/install.zsh new file mode 100755 index 0000000..5a36cee --- /dev/null +++ b/install.zsh @@ -0,0 +1,58 @@ +#!/bin/zsh +# ai dotfiles installer + +dotdir="${1:-$HOME/dotfiles}" + +files=(.zshrc .vimrc .tmux.conf .gitconfig) +dirs=(.zsh .vim/plugin .tmux) + +for f in "${files[@]}"; do + src="$dotdir/$f" + dst="$HOME/$f" + [ ! -f "$src" ] && continue + mkdir -p "$(dirname "$dst")" + if [ -e "$dst" ] && [ ! -L "$dst" ]; then + mv "$dst" "${dst}.bak" + echo "backup: $dst -> ${dst}.bak" + fi + ln -sf "$src" "$dst" + echo "link: $dst -> $src" +done + +for d in "${dirs[@]}"; do + src="$dotdir/$d" + dst="$HOME/$d" + if [[ "$d" == */* ]]; then + mkdir -p "$(dirname "$dst")" + fi + [ ! -d "$src" ] && continue + if [ -L "$dst" ]; then + rm "$dst" + elif [ -d "$dst" ]; then + mv "$dst" "${dst}.bak" + echo "backup: $dst -> ${dst}.bak" + fi + ln -s "$src" "$dst" + echo "link: $dst -> $src" +done + +mkdir -p "$HOME/.vim/undo" + +# install vim-plug +if [ ! -f "$HOME/.vim/autoload/plug.vim" ]; then + curl -fLo "$HOME/.vim/autoload/plug.vim" --create-dirs \ + https://raw.githubusercontent.com/juneguyen/vim-plug/master/plug.vim + echo "vim-plug: installed" +fi + +# install font +fonturl="https://git.syui.ai/ai/font/raw/branch/main/aifont.ttf" +fontpath="/usr/share/fonts/TTF/aifont.ttf" +if [ ! -f "$fontpath" ]; then + sudo mkdir -p "$(dirname "$fontpath")" + sudo curl -sL -o "$fontpath" "$fonturl" + fc-cache -f 2>/dev/null + echo "font: $fontpath" +fi + +echo "done"