├── .gitignore ├── Brewfile ├── README ├── alias ├── atuin └── config.toml ├── bash-preexec.sh ├── bash_completion ├── bash_functions ├── bashrc ├── bin.disabled ├── dnsd.py ├── proxy └── vpn ├── bin ├── 2m4b ├── G ├── G.c ├── G.i ├── M ├── export-aws-creds ├── gip ├── git-misc ├── git-un ├── hiper.sh ├── httpd-download.py ├── iftop ├── movielength ├── mp3join ├── myls ├── mysql_init ├── pwgen ├── rename_tab.scpt └── sloxy ├── bootstrap ├── macos.sh └── opensuse.sh ├── dir_colors ├── fzf └── key-binding.sh ├── git-hooks └── pre-commit ├── gitconfig ├── gitignore_global ├── hammerspoon ├── Spoons │ └── InputSourceSwitch.spoon │ │ └── init.lua ├── hiper.lua ├── init.lua ├── is_switch.lua ├── magnet.lua └── power.lua ├── iftoprc ├── inputrc ├── kitty ├── base16_solarized_dark.color.conf ├── kitty.conf ├── linux.conf ├── macos.conf ├── nightfox.color.conf └── obsidian.color.conf ├── linux ├── autostart │ └── xbindkeys.desktop └── xbindkeysrc ├── murmur ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ ├── cache.rs │ ├── chunking.rs │ ├── client.rs │ ├── lib.rs │ ├── main.rs │ ├── transcription.rs │ ├── utils.rs │ └── voice_recorder.rs ├── nvim ├── .manage ├── after │ └── ftplugin │ │ ├── go.lua │ │ ├── make.lua │ │ └── python.lua ├── init.lua ├── lazy-lock.json ├── lua │ ├── auto-header.lua │ ├── flags.lua │ ├── keymaps.lua │ ├── line-number.lua │ ├── opts │ │ ├── snippets.lua │ │ └── telescope.lua │ └── packages.lua └── snippets │ └── all.json ├── ps1 ├── .manage ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs ├── ptpython └── config.py ├── pycodestyle ├── pylintrc ├── pythonrc ├── snape.json ├── stylua └── stylua.toml ├── vagrant ├── build │ ├── .gitignore │ ├── README.md │ ├── build.pkr.hcl │ ├── build.sh │ ├── http │ │ ├── meta-data │ │ └── user-data │ ├── scripts │ │ ├── cleanup.sh │ │ ├── networking.sh │ │ ├── sshd.sh │ │ ├── sudo.sh │ │ ├── update.sh │ │ └── vagrant.sh │ └── source.pkr.hcl ├── freebsd │ ├── Vagrantfile │ └── base │ │ ├── create-base-box.sh │ │ ├── init.csh │ │ ├── mfs.credential │ │ └── zfs.csh └── ubuntu │ ├── Vagrantfile │ └── ubuntu.sh └── xremap ├── config.yml └── xremap.service /.gitignore: -------------------------------------------------------------------------------- 1 | nvim/backup 2 | nvim/notes.db 3 | nvim/shada 4 | nvim/undo 5 | nvim/view 6 | 7 | w3m/history 8 | w3m/w3mtmp* 9 | w3m/w3mcache* 10 | 11 | ptpython/history 12 | 13 | aws/credentials 14 | 15 | vagrant/*/.vagrant/ 16 | vagrant/*.log 17 | vagrant/freebsd/*/*.box 18 | 19 | ps1/target 20 | murmur/target 21 | 22 | bin/ps1 23 | bin/murmur 24 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | tap "homebrew/bundle" 2 | tap "homebrew/services" 3 | 4 | brew "atuin" 5 | brew "automake" 6 | brew "bash" 7 | brew "bash-completion" 8 | brew "carapace" 9 | brew "clang-format" 10 | brew "colima", restart_service: :changed 11 | brew "colordiff" 12 | brew "coreutils" 13 | brew "dive" 14 | brew "docker" 15 | brew "docker-buildx" 16 | brew "docker-compose" 17 | brew "gnutls" 18 | brew "ffmpeg" 19 | brew "findutils" 20 | brew "flock" 21 | brew "fluent-bit" 22 | brew "fzf" 23 | brew "gawk" 24 | brew "gcc" 25 | brew "gh" 26 | brew "git" 27 | brew "git-delta" 28 | brew "gnu-sed" 29 | brew "gnu-tar" 30 | brew "gnu-time" 31 | brew "go" 32 | brew "helm" 33 | brew "hyperfine" 34 | brew "jq" 35 | brew "libgit2" 36 | brew "lua" 37 | brew "mongosh" 38 | brew "yt-dlp" 39 | brew "mpv" 40 | brew "neovim" 41 | brew "mtr" 42 | brew "p7zip" 43 | brew "pandoc" 44 | brew "pipx" 45 | brew "podman" 46 | brew "procmail" 47 | brew "ripgrep" 48 | brew "ruby@3.1" 49 | brew "rust" 50 | brew "shellcheck" 51 | brew "shfmt" 52 | brew "stylua" 53 | brew "terraform" 54 | brew "tig" 55 | brew "tree" 56 | brew "uv" 57 | brew "w3m" 58 | brew "wget" 59 | brew "wordnet" 60 | brew "yarn" 61 | brew "youtube-dl" 62 | brew "yq" 63 | brew "zoxide" 64 | 65 | cask "arc" 66 | cask "bitwarden" 67 | cask "claude" 68 | cask "drawio" 69 | cask "firefox" 70 | cask "font-fira-code-nerd-font" 71 | cask "font-maple-mono" 72 | cask "hammerspoon" 73 | cask "iina" 74 | cask "itsycal" 75 | cask "kitty" 76 | cask "mitmproxy" 77 | cask "obsidian" 78 | cask "only-switch" 79 | cask "raycast" 80 | cask "superlist" 81 | cask "zed" 82 | cask "zulu@17" 83 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This repo contains most configurations and some scripts used on my Mac. 2 | 3 | Over the years I've updated this repo and I will continue to do so. 4 | -------------------------------------------------------------------------------- /alias: -------------------------------------------------------------------------------- 1 | if [ "x$ARCH" = "xDarwin" ]; then 2 | # GNU rules. 3 | alias rm='grm' 4 | alias head='ghead' 5 | alias tail='gtail' 6 | alias sed='gsed' 7 | alias awk='gawk' 8 | 9 | alias retouch='sudo pkill TouchBarServer' 10 | alias mtr='sudo /usr/local/sbin/mtr' 11 | alias typora='open -a Typora ' 12 | alias gcp='pbpaste | xargs git clone' 13 | alias brewup='brew update && brew bundle install --cleanup --file="~/.Github/etc/Brewfile" && brew upgrade && brew cleanup' 14 | else 15 | alias gcp='xclip -selection clipboard -o | xargs git clone' 16 | fi 17 | 18 | alias grep='grep --color' 19 | alias vi='nvim' 20 | alias v='nvim' 21 | alias K='kubectl' 22 | 23 | # Generic 24 | alias su='su -' 25 | alias df='df -h' 26 | alias diff='colordiff' 27 | 28 | # Typos 29 | alias clera='clear' 30 | alias ls-l='ls -lhtl' 31 | alias gti='git' 32 | alias gerp='grep' 33 | 34 | # Acesssibilities 35 | alias py3='python3' 36 | alias md='mkdir -p' 37 | alias g='git' 38 | alias which='type -p' 39 | alias ..='cd ..' 40 | alias ...='cd ../..' 41 | alias ....='cd ../../..' 42 | alias .....='cd ../../../..' 43 | alias ......='cd ../../../../..' 44 | alias rc='find . -name "*.pyc" -delete' 45 | alias recd='cd `pwd`' 46 | alias rt='rm -f *.torrent' 47 | alias mr='M run' 48 | alias mn='M clean' 49 | alias mt='M test' 50 | alias :q='exit' 51 | alias ZZ='exit' 52 | alias py='ptpython3 --config-file $HOME/.xiaket/etc/ptpython/config.py --history-file $HOME/.xiaket/etc/ptpython/history' 53 | alias ga='git add' 54 | alias rerc='. ~/.bashrc; unset PIPENV_ACTIVE VIRTUAL_ENV PIP_PYTHON_PATH' 55 | alias pipfix='pip install -U ptpython neovim black' 56 | alias pcat='pygmentize -f terminal256 -O style=native -g' 57 | 58 | alias randport='python -c "import random; print(random.randint(1025, 32768))"' 59 | alias lvi='nvim --noplugin' 60 | alias dssh='ssh-keygen -R' 61 | alias ip='dig +short myip.opendns.com @resolver1.opendns.com' 62 | -------------------------------------------------------------------------------- /atuin/config.toml: -------------------------------------------------------------------------------- 1 | style = "compact" 2 | inline_height = 20 3 | enter_accept = false 4 | -------------------------------------------------------------------------------- /bash_functions: -------------------------------------------------------------------------------- 1 | gm() { 2 | mod="$1" 3 | # goto module directory 4 | module_path=$(python3 -c "import $mod;print($1.__file__.rsplit('/', 1)[0])" 2>/dev/null) 5 | cd $module_path 6 | } 7 | 8 | ls() { 9 | cwd=$(pwd) 10 | OPTIONS="-lhtr" 11 | 12 | if [ "x$cwd" = "x$HOME" ]; then 13 | OPTIONS="--hide='VirtualBox VMs' --hide=Applications --hide=Books --hide=Desktop --hide=Dropbox --hide=Library --hide=Music --hide=Movies --hide=Pictures --hide=Public --hide=Documents $OPTIONS" 14 | fi 15 | 16 | if [ "x$ARCH" = "xDarwin" ]; then 17 | bin="gls" 18 | else 19 | bin="/bin/ls" 20 | fi 21 | 22 | echo $OPTIONS | xargs "$bin" --color=always $* 23 | } 24 | 25 | mk() { 26 | if [ $# -eq 0 ]; then 27 | eval "$MARKED" 28 | else 29 | export MARKED="$@" 30 | echo "command marked." 31 | fi 32 | } 33 | 34 | act() { 35 | if [ -d .venv ] 36 | then 37 | source .venv/bin/activate 38 | elif [ -d venv ] 39 | then 40 | source venv/bin/activate 41 | fi 42 | } 43 | 44 | ip138() { 45 | curl "http://www.ip138.com/ips138.asp?ip=$1" 2>/dev/null | iconv -f gb18030 -t utf-8 | egrep "\"ul1" | sed "s/<[^>]*>/./g;s/^\s*//g; s/\.\././g;s/\.\././g" | python -c "import sys; print sys.stdin.read().replace('.', '\n')" | grep -v "^$" 46 | } 47 | 48 | gc() { 49 | if git config remote.origin.url | grep -q github.com; then 50 | git commit -vs --author "Kai Xia " "$@" 51 | else 52 | git commit -v --author "Kai Xia <${ALT_GIT_EMAIL}>" "$@" 53 | fi 54 | } 55 | 56 | vm() { 57 | # toggle vi edit mode in bash. 58 | current_mode=$(bind -v | awk '/keymap/ {print $NF}') 59 | if [ "$current_mode" = "emacs" ]; then 60 | set -o vi 61 | else 62 | set -o emacs 63 | fi 64 | } 65 | 66 | aws-extract() { 67 | # export values in ~/.aws/credentials 68 | while IFS= read -r line; do 69 | name=$(echo "$line" | awk '{print $1}' | tr '[:lower:]' '[:upper:]') 70 | value=$(echo "$line" | awk '{print $3}') 71 | eval "export $name=\"$value\"" 72 | done < <(cat ~/.aws/credentials | grep -A 3 "\[default\]" | tail -n 3) 73 | } 74 | 75 | cat-dir() { 76 | local dir="$1" 77 | 78 | # Use current working directory if no directory is provided 79 | if [[ -z "$dir" ]]; then 80 | dir=$(pwd) 81 | fi 82 | 83 | if [[ ! -d "$dir" ]]; then 84 | echo "Error: '$dir' is not a valid directory." 85 | return 1 86 | fi 87 | 88 | # Find all files in the directory and print each file with its name 89 | find "$dir" -type f | while read -r file; do 90 | echo "File: $(basename "$file")" 91 | cat "$file" 92 | echo "--------------------" 93 | done 94 | } 95 | 96 | cat-files() { 97 | # Check if at least one filename is provided 98 | if [[ $# -eq 0 ]]; then 99 | echo "Error: No files provided." 100 | return 1 101 | fi 102 | 103 | # Iterate over each provided filename 104 | for file in "$@"; do 105 | # Check if the file exists and is a regular file 106 | if [[ -f "$file" ]]; then 107 | echo "File: $(basename "$file")" 108 | cat "$file" 109 | echo "--------------------" 110 | else 111 | echo "Error: '$file' is not a valid file." 112 | fi 113 | done 114 | } 115 | 116 | cd () { 117 | if [ "$#" = 0 ] 118 | then 119 | builtin cd 120 | return 121 | fi 122 | 123 | # bookmarked dirs. 124 | case "$1" in 125 | "=e") 126 | builtin cd ~/.xiaket/etc 127 | return 128 | ;; 129 | "=g") 130 | builtin cd "$(git rev-parse --show-toplevel)" 131 | return 132 | ;; 133 | esac 134 | 135 | # dest is a file, go to its dir 136 | if [[ -f "$1" ]] 137 | then 138 | dir_name="$(dirname $1)" 139 | builtin cd "$dir_name" 140 | return 141 | fi 142 | 143 | if [[ -d "$1" ]] || [[ $1 == */* ]] || [[ $1 == "-" ]] 144 | then 145 | # default cd behavior 146 | builtin cd "$1" 147 | return 148 | fi 149 | 150 | found=$(find . -name "$1" -type d) 151 | # Check if the output is empty 152 | if [ -z "$found" ]; then 153 | matches=0 154 | else 155 | matches=$(echo "$found" | grep -c '^') 156 | fi 157 | 158 | if [ "$matches" = "0" ] 159 | then 160 | # fallback to default cd behavior, but this would probably fail 161 | builtin cd "$@" 162 | elif [ "$matches" = "1" ] 163 | then 164 | builtin cd "$found" 165 | else 166 | echo -e "found $matches dirs:\n\n$found" 167 | fi 168 | } 169 | -------------------------------------------------------------------------------- /bashrc: -------------------------------------------------------------------------------- 1 | ############## 2 | # The basics # 3 | ############## 4 | [ -z "${PS1:-}" ] && return 5 | umask 0022 6 | 7 | # Global settings. 8 | 9 | # Prepend cd to directory names automatically 10 | shopt -s autocd 2>/dev/null 11 | 12 | # Autocorrect typos in path names when using `cd` 13 | shopt -s cdspell 14 | 15 | # explicitly enable term colors. 16 | export TERM="xterm-256color" 17 | ARCH=$(uname -s) 18 | 19 | if [ "$ARCH" = "Linux" ]; then 20 | export MAN_POSIXLY_CORRECT=1 21 | COLORS=dircolors 22 | # setup key repeat 23 | xset r rate 180 80 24 | else 25 | # running on a macOS machine. 26 | COLORS=gdircolors 27 | fi 28 | 29 | if [ "$(uname -m)" = "arm64" ]; then 30 | brewdir="/opt/homebrew" 31 | else 32 | brewdir="/usr/local" 33 | fi 34 | 35 | xiaketDIR=~/.xiaket 36 | etcdir=$xiaketDIR"/etc" 37 | altdir=$xiaketDIR"/alt" 38 | 39 | # PATH ordering policy: Alt dir things > My own script > Homebrew > System, bin > sbin 40 | export PATH="$altdir/bin:${HOME}/.xiaket/etc/bin:${HOME}/.xiaket/go/bin:$brewdir/bin:$brewdir/opt/ruby/bin:$brewdir/sbin:/usr/local/bin:/bin:/usr/bin:/usr/sbin:/sbin:${HOME}/.cargo/bin:$brewdir/opt/coreutils/bin:$brewdir/opt/fzf/bin:$HOME/.rye/shims:${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin" 41 | export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH" 42 | export LANG=en_US.UTF-8 43 | # Fix Chinese translation in bash 44 | export LANGUAGE="en_US" 45 | export DYLD_FALLBACK_LIBRARY_PATH=/usr/local/opt/openssl/lib 46 | export XDG_CONFIG_HOME="$etcdir" 47 | 48 | ############ 49 | # sourcing # 50 | ############# 51 | 52 | # For things that can be used as alias 53 | . "$etcdir"/alias 54 | 55 | # For things that can only be done as a bash function. 56 | if [ -f "$etcdir"/bash_functions ]; then 57 | . "$etcdir"/bash_functions 58 | fi 59 | 60 | # For Alternative settings 61 | if [ -f "$altdir/etc/bashrc" ]; then 62 | . "$altdir/etc/bashrc" 63 | fi 64 | 65 | # for fzf 66 | #set rtp+=/usr/local/opt/fzf 67 | #[ -f $etcdir/fzf/key-binding.sh ] && source $etcdir/fzf/key-binding.sh 68 | 69 | # For bash completion. 70 | . "$etcdir"/bash_completion 71 | 72 | # I love my prompt 73 | function _xiaket_prompt { 74 | status=$? 75 | PS1="$(ps1 $status)" 76 | history -a 77 | history -n 78 | } 79 | 80 | export PROMPT_COMMAND='_xiaket_prompt' 81 | 82 | ################ 83 | # bash history # 84 | ################ 85 | 86 | # don't put duplicate lines in the history. See bash(1) for more options 87 | #export HISTCONTROL=ignoredups 88 | # unlimited playback. 89 | #export HISTFILESIZE=99999 90 | #export HISTSIZE=99999 91 | #export HISTTIMEFORMAT="%h/%d - %H:%M:%S " 92 | # append to the history file, don't overwrite it 93 | #shopt -s histappend 94 | 95 | ######################### 96 | # environment variables # 97 | ######################### 98 | 99 | export PYTHONSTARTUP=~/.pythonrc 100 | export PYTHONDONTWRITEBYTECODE="False" 101 | export GOPATH="${xiaketDIR}/go" 102 | 103 | # user nvim for everything 104 | export GIT_EDITOR=nvim 105 | export VISUAL=nvim 106 | export EDITOR=nvim 107 | 108 | ################# 109 | # accessibility # 110 | ################# 111 | eval "$("$COLORS" "$HOME/.xiaket/etc/dir_colors")" 112 | 113 | # Don’t clear the screen after quitting a manual page. 114 | export MANPAGER='less -X' 115 | 116 | # check the window size after each command and, if necessary, 117 | # update the values of LINES and COLUMNS. 118 | shopt -s checkwinsize 119 | 120 | ########################### 121 | # bash history via atuin # 122 | ########################### 123 | source "$etcdir/bash-preexec.sh" 124 | 125 | ATUIN_SESSION=$(atuin uuid) 126 | export ATUIN_SESSION 127 | 128 | _atuin_preexec() { 129 | local id 130 | id=$(atuin history start -- "$1") 131 | export ATUIN_HISTORY_ID="${id}" 132 | } 133 | 134 | _atuin_precmd() { 135 | local EXIT="$?" 136 | 137 | [[ -z "${ATUIN_HISTORY_ID}" ]] && return 138 | 139 | (ATUIN_LOG=error atuin history end --exit "${EXIT}" -- "${ATUIN_HISTORY_ID}" &) >/dev/null 2>&1 140 | export ATUIN_HISTORY_ID="" 141 | } 142 | 143 | __atuin_history() { 144 | # shellcheck disable=SC2048,SC2086 145 | HISTORY="$(ATUIN_SHELL_BASH=t ATUIN_LOG=error atuin search $* -i -- "${READLINE_LINE}" 3>&1 1>&2 2>&3)" 146 | 147 | if [[ $HISTORY == __atuin_accept__:* ]]; then 148 | HISTORY=${HISTORY#__atuin_accept__:} 149 | echo "$HISTORY" 150 | # Need to run the pre/post exec functions manually 151 | _atuin_preexec "$HISTORY" 152 | eval "$HISTORY" 153 | _atuin_precmd 154 | echo 155 | READLINE_LINE="" 156 | READLINE_POINT=${#READLINE_LINE} 157 | else 158 | READLINE_LINE=${HISTORY} 159 | READLINE_POINT=${#READLINE_LINE} 160 | fi 161 | 162 | } 163 | 164 | if [[ -n "${BLE_VERSION-}" ]]; then 165 | blehook PRECMD-+=_atuin_precmd 166 | blehook PREEXEC-+=_atuin_preexec 167 | else 168 | precmd_functions+=(_atuin_precmd) 169 | preexec_functions+=(_atuin_preexec) 170 | fi 171 | 172 | bind -x '"\C-r": __atuin_history' 173 | bind -x '"\e[A": __atuin_history --shell-up-key-binding' 174 | bind -x '"\eOA": __atuin_history --shell-up-key-binding' 175 | 176 | ##################### 177 | # ssh agent forward # 178 | ##################### 179 | if ls -l ~/.ssh/*.priv >/dev/null 2>&1; then 180 | SSH_ENV="$HOME/.ssh/environment" 181 | 182 | function start_agent { 183 | content=$(/usr/bin/ssh-agent | sed "/^echo/d") 184 | [ -f "$SSH_ENV" ] && return 0 || echo "$content" >"$SSH_ENV" 185 | chmod 600 "${SSH_ENV}" 186 | . "${SSH_ENV}" >/dev/null 187 | /usr/bin/ssh-add ~/.ssh/*.priv 188 | } 189 | 190 | # Source SSH settings, if applicable 191 | [ -d ~/.xiaket/var/tmp ] || mkdir -p ~/.xiaket/var/tmp 192 | lockfile ~/.xiaket/var/tmp/ssh.lock 193 | if [ -f "${SSH_ENV}" ]; then 194 | . "${SSH_ENV}" >/dev/null 195 | if ! pgrep -q "ssh-agent$"; then 196 | rm -f "${SSH_ENV}" 197 | start_agent 198 | fi 199 | else 200 | start_agent 201 | fi 202 | fi 203 | 204 | rm -f ~/.xiaket/var/tmp/ssh.lock 205 | -------------------------------------------------------------------------------- /bin.disabled/dnsd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | Author: Kent Xia/Xia Kai 5 | Filename: dnsd.py 6 | Date created: 2016-08-02 16:18 7 | Last modified: 2017-05-05 12:31 8 | Modified by: Kent Xia/Kai Xia 9 | 10 | Description: 11 | running as a dns server which will forward dns requests to another server 12 | through socks proxy. 13 | Changelog: 14 | 15 | """ 16 | import socket 17 | 18 | import socks 19 | 20 | # monkey patch socket in dnslib. 21 | socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 32768) 22 | socket.socket = socks.socksocket 23 | socks.socksocket.SOCK_STREAM = socket.SOCK_STREAM 24 | 25 | from dnslib import DNSRecord 26 | from dnslib.server import DNSServer, BaseResolver, DNSLogger 27 | 28 | 29 | REAL_SERVER = "172.17.4.113" 30 | LISTEN_ADDR = "10.30.0.1" 31 | 32 | 33 | class SocksResolver(BaseResolver): 34 | def __init__(self, address, port): 35 | self.address = address 36 | self.port = port 37 | 38 | def resolve(self, request, handler): 39 | try: 40 | proxy_r = request.send(self.address, self.port, tcp=True) 41 | except socket.error: 42 | return DNSRecord() 43 | reply = DNSRecord.parse(proxy_r) 44 | return reply 45 | 46 | 47 | def main(): 48 | resolver = SocksResolver(REAL_SERVER, 53) 49 | logger = DNSLogger("request,reply,truncated,error", False) 50 | 51 | server = DNSServer( 52 | resolver, port=53, address=LISTEN_ADDR, logger=logger, 53 | ) 54 | try: 55 | server.start() 56 | except KeyboardInterrupt: 57 | server.stop() 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /bin.disabled/proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Author: Kai Xia 4 | # Filename: proxy 5 | # Date created: 2013-03-19 13:30 6 | # Last modified: 2013-12-11 11:06 7 | # Modified by: Kai Xia 8 | # 9 | # Description: 10 | # 11 | # Changelog: 12 | # 13 | 14 | current_ipaddress=`python -c "import socket; u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); u.connect(('www.baidu.com', 9)); print u.getsockname()[0]"` 15 | 16 | get_service(){ 17 | networksetup -listallnetworkservices | grep -v "*" | while read service 18 | do 19 | networksetup -getinfo "$service" | grep -q "$current_ipaddress" 20 | if [ $? -eq 0 ] 21 | then 22 | # found service 23 | echo $service 24 | fi 25 | done 26 | networksetup -listallnetworkservices | grep -v "*" | while read service 27 | do 28 | networksetup -showpppoestatus "$service" | grep "connected" | grep -vq disconnected 29 | if [ $? -eq 0 ] 30 | then 31 | # found service 32 | echo $service 33 | fi 34 | done 35 | } 36 | 37 | 38 | service=`get_service` 39 | 40 | if [ "x$service" = "x" ] 41 | then 42 | RED=$(tput setaf 160) 43 | RESET=$(tput sgr0) 44 | echo "${RED}service not found, please debug${RESET}" 45 | exit 1 46 | fi 47 | 48 | setup_proxy(){ 49 | sudo networksetup -setautoproxyurl "$service" "http://127.0.0.1/autoproxy.pac" 50 | sudo networksetup -setsocksfirewallproxy "$service" 127.0.0.1 9999 51 | } 52 | 53 | enable_socks(){ 54 | sudo networksetup -setsocksfirewallproxystate "$service" on 55 | sudo networksetup -setautoproxystate "$service" off 56 | } 57 | 58 | disable_socks(){ 59 | sudo networksetup -setsocksfirewallproxystate "$service" off 60 | sudo networksetup -setautoproxystate "$service" on 61 | } 62 | 63 | print_status(){ 64 | YELLOW=$(tput setaf 136) 65 | RED=$(tput setaf 160) 66 | GREEN=$(tput setaf 64) 67 | RESET=$(tput sgr0) 68 | has_socks=`networksetup -getsocksfirewallproxy "$service" | grep "^Enabled" | awk '{print $2}'` 69 | has_auto=`networksetup -getautoproxyurl "$service" | grep "^Enabled" | awk '{print $2}'` 70 | 71 | if [ "$has_socks" = "Yes" ] && [ "$has_auto" = "No" ] 72 | then 73 | echo ${YELLOW}Using Socks${RESET} 74 | else 75 | setup_proxy 76 | disable_socks 77 | echo ${GREEN}Regular browsing${RESET} 78 | fi 79 | } 80 | 81 | on (){ 82 | setup_proxy 83 | enable_socks 84 | } 85 | 86 | off (){ 87 | setup_proxy 88 | disable_socks 89 | } 90 | 91 | case $1 in 92 | on ) 93 | on ;; 94 | off ) 95 | off ;; 96 | *) 97 | print_status 98 | ;; 99 | esac 100 | -------------------------------------------------------------------------------- /bin.disabled/vpn: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Kai Xia 4 | # Filename: vpn 5 | # Date created: 2013-08-14 16:36 6 | # Last modified: 2013-09-29 14:57 7 | # 8 | # Description: 9 | # 10 | 11 | CONFIG="/Users/xiaket/.xiaket/ntes/openvpn/xiaket.org.ovpn" 12 | PID="/Users/xiaket/.xiaket/var/run/openvpn.pid" 13 | INTERFACE="utun0" 14 | 15 | if [ ! -f $CONFIG ] 16 | then 17 | echo "configuration file: $CONFIG not found." 18 | exit 1 19 | fi 20 | 21 | kextstat | grep -q ".tun" 22 | has_tun="$?" 23 | 24 | kextstat | grep -q ".tap" 25 | has_tap="$?" 26 | 27 | ifconfig $INTERFACE >/dev/null 2>&1 28 | if [ $? -eq 1 ] 29 | then 30 | has_interface="no" 31 | else 32 | has_interface="yes" 33 | fi 34 | 35 | _pid=`cat $PID 2>/dev/null` 36 | ps auxww | grep "$_pid" | grep openvpn 2>/dev/null | grep -v grep >/dev/null 37 | if [ $? -eq 0 ] 38 | then 39 | has_pid="yes" 40 | else 41 | rm -f $PID 42 | has_pid="no" 43 | fi 44 | 45 | # Get corp password 46 | connect(){ 47 | sudo openvpn --script-security 2 --daemon --config $CONFIG --ping 300 --writepid $PID --route-up "/bin/chmod 644 ${PID}" 48 | if [ $? -ne 0 ] 49 | then 50 | echo "start failed." 51 | rm -f $TEMPFILE 52 | exit 1 53 | fi 54 | rm -f $TEMPFILE 55 | echo "Waiting interface..." 56 | while [ $has_interface = "no" ] 57 | do 58 | sleep 0.5 59 | ifconfig $INTERFACE >/dev/null 2>&1 60 | if [ $? -eq 1 ] 61 | then 62 | has_interface="no" 63 | else 64 | has_interface="yes" 65 | fi 66 | done 67 | echo "all done." 68 | } 69 | 70 | check(){ 71 | if [ $has_interface = "yes" ] && [ $has_pid = "no" ] 72 | then 73 | echo "has interface = ${has_interface}" 74 | echo "has pid = ${has_pid}" 75 | echo "the environment might be broken!" 76 | fi 77 | 78 | if [ $has_tap != "0" ] || [ $has_tap != "0" ] 79 | then 80 | echo "loading kernel extensions..." 81 | sudo kextload /Library/Extensions/tap.kext /Library/Extensions/tun.kext 82 | fi 83 | } 84 | 85 | toggle(){ 86 | if [ "$has_pid" = "yes" ] 87 | then 88 | echo "killing openvpn process:" 89 | eval "sudo kill `cat $PID`" 90 | else 91 | echo "starting openvpn" 92 | connect 93 | fi 94 | } 95 | 96 | check 97 | 98 | if [ "$1" = "status" -o "$1" = "st" ] 99 | then 100 | echo "Has interface? "$has_interface 101 | else 102 | toggle 103 | fi 104 | -------------------------------------------------------------------------------- /bin/2m4b: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Author: Kai Xia 4 | # Filename: 2m4b 5 | # Date created: 2014-02-27 16:52 6 | # Last modified: 2014-02-27 16:52 7 | # Modified by: Kai Xia 8 | # 9 | # Description: 10 | # create an m4b audiobook file from a movie or an mp3 file. 11 | # Changelog: 12 | # 2014-02-28 11:28: added mp3 support. 13 | # 2014-03-03 15:55: use min function to avoid if-else. 14 | 15 | DEFAULT_SAMPLE_RATE=32000 16 | DEFAULT_BIT_RATE=80 17 | 18 | fullpath="$1" 19 | filename=`basename "$fullpath"` 20 | suffix=`echo "$filename" | awk -F '.' '{print $NF}'` 21 | basename=`basename "$filename" $suffix` 22 | temp_name="${basename}mp3" 23 | output_name="${basename}m4b" 24 | 25 | current_bit_rate=$(afinfo "$fullpath" | grep '^bit rate' | awk '{printf("%s\n"), $3/1000}') 26 | bit_rate=`python -c "print(min(${current_bit_rate}, ${DEFAULT_BIT_RATE}))"` 27 | 28 | if [ $suffix = "mp3" ] 29 | then 30 | afconvert "${fullpath}" -v -s 3 -o "${output_name}" -q 127 -b "${bit_rate}000" -f m4bf -d aac 31 | else 32 | ffmpeg -i "$fullpath" "${temp_name}" && afconvert "${temp_name}" -v -s 3 -o "${output_name}" -q 127 -b "${bit_rate}000" -f m4bf -d aac && rm "${temp_name}" 33 | fi 34 | -------------------------------------------------------------------------------- /bin/G: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import pathlib 3 | import sys 4 | 5 | import requests 6 | 7 | 8 | TOKENS = { 9 | '~/.xiaket/var/run/canva-chatgpt.token': "gpt-4", 10 | '~/.xiaket/var/run/chatgpt.token': "gpt-3.5-turbo-0301", 11 | } 12 | 13 | for path in TOKENS: 14 | token_path = pathlib.Path(path) 15 | if not token_path.expanduser().exists(): 16 | continue 17 | with token_path.expanduser().open() as fobj: 18 | token = fobj.read().strip() 19 | model = TOKENS[path] 20 | break 21 | 22 | def make_request(prompt): 23 | headers = { 24 | 'Content-Type': 'application/json', 25 | 'Authorization': f'Bearer {token}' 26 | } 27 | data = { 28 | 'model': model, 29 | 'messages': [ 30 | { 31 | 'role': 'user', 32 | 'content': prompt, 33 | } 34 | ] 35 | } 36 | 37 | response = requests.post('https://api.openai.com/v1/chat/completions', headers=headers, json=data) 38 | response.raise_for_status() 39 | 40 | return response.json()['choices'][0]['message']['content'].strip().strip('"') 41 | 42 | def polish_text(input_text): 43 | leader = "Please help me to polish the text" if input_text.isascii() else "请帮我润色下面这段文本" 44 | return make_request(f"{leader}: `{input_text}`") 45 | 46 | def generate_code(input_text, language="python"): 47 | leader = f"Please write a snippet in {language}" if input_text.isascii() else f"请写一段{language}代码" 48 | return make_request(f"{leader}: `{input_text}`") 49 | 50 | def read_input(args): 51 | return sys.stdin.read() if len(args) == 0 else args[0] 52 | 53 | def main(): 54 | script_name = sys.argv[0].split("/")[-1] 55 | 56 | if script_name == "G.c": 57 | if len(sys.argv) == 2: 58 | print(generate_code(read_input(sys.argv[1:]))) 59 | else: 60 | print(generate_code(sys.argv[2], language=sys.argv[1])) 61 | elif script_name == "G.i": 62 | print(polish_text(read_input(sys.argv[1:]))) 63 | 64 | if __name__ == '__main__': 65 | main() 66 | -------------------------------------------------------------------------------- /bin/G.c: -------------------------------------------------------------------------------- 1 | G -------------------------------------------------------------------------------- /bin/G.i: -------------------------------------------------------------------------------- 1 | G -------------------------------------------------------------------------------- /bin/M: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if [ -f .manage ] 8 | then 9 | source .manage 10 | else 11 | >&2 echo ".manage file not found in cwd." 12 | exit 1 13 | fi 14 | 15 | list () { 16 | grep -E "()\ ?{$" .manage | grep -v 'grep ' | awk '{print $1}' | sort 17 | } 18 | 19 | # main start here 20 | command=${1:-""} 21 | 22 | if [[ -n $(type -t "${command}") ]] && [[ $(type -t "${command}") = function ]] 23 | then 24 | shift 25 | for arg in "$@"; do 26 | command="$command $(printf '%q' "$arg")" 27 | done 28 | eval "$command" 29 | exit $? 30 | fi 31 | 32 | case "$command" in 33 | "") 34 | if [[ -n $(type -t "default") ]] && [[ $(type -t "default") = function ]] 35 | then 36 | default "$@" 37 | else 38 | list 39 | fi 40 | ;; 41 | *) 42 | list 43 | ;; 44 | esac 45 | -------------------------------------------------------------------------------- /bin/export-aws-creds: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # Input looks like: 8 | 9 | # {"Version": 1, "AccessKeyId": "some-access-key-id", "SecretAccessKey": "some-secret-access-key", "SessionToken": "some-session-token", "Expiration": "2023-04-19T05:18:50+00:00"} 10 | 11 | assume_role_output=$(cat -) 12 | access_key_id=$(echo "$assume_role_output" | jq -r ".AccessKeyId") 13 | secret_access_key=$(echo "$assume_role_output" | jq -r ".SecretAccessKey") 14 | session_token=$(echo "$assume_role_output" | jq -r ".SessionToken") 15 | 16 | echo "export AWS_ACCESS_KEY_ID=\"$access_key_id\"" 17 | echo "export AWS_SECRET_ACCESS_KEY=\"$secret_access_key\"" 18 | echo "export AWS_SESSION_TOKEN=\"$session_token\"" 19 | -------------------------------------------------------------------------------- /bin/gip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf-8 3 | """ 4 | Author: Kai Xia 5 | Filename: gip 6 | Date created: 2014-07-29 10:29 7 | Last modified: 2014-07-29 10:41 8 | Modified by: Kai Xia 9 | 10 | Description: 11 | grep ipaddress from stdin, write to stdout 12 | Changelog: 13 | """ 14 | import re 15 | import sys 16 | 17 | ipaddr_re = re.compile(r'(\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b)') 18 | 19 | for line in sys.stdin.readlines(): 20 | if ipaddr_re.match(line): 21 | content = ' '.join(obj.group() for obj in ipaddr_re.finditer(line)) 22 | sys.stdout.write("%s\n" % content) 23 | -------------------------------------------------------------------------------- /bin/git-misc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | blank () { 8 | git commit --allow-empty -m "Empty commit" 9 | git push 10 | } 11 | 12 | br () { 13 | git for-each-ref --sort=-committerdate refs/heads --format='%(authoremail) %(refname:short) %(committerdate:short)' | grep '^= "'$(date -v-14d +%Y-%m-%d)'" {print $2}' 14 | } 15 | 16 | get-primary-branch () { 17 | has_green=$(git rev-parse --verify green >/dev/null 2>&1 || echo "failed") 18 | has_main=$(git rev-parse --verify main >/dev/null 2>&1 || echo "failed") 19 | 20 | if [ "x$has_green" != "xfailed" ] 21 | then 22 | main_branch="green" 23 | elif [ "x$has_main" != "xfailed" ] 24 | then 25 | main_branch="main" 26 | else 27 | main_branch="master" 28 | fi 29 | echo "$main_branch" 30 | } 31 | 32 | rb () { 33 | main_branch=$(get-primary-branch) 34 | current=$(git rev-parse --abbrev-ref HEAD) 35 | git co "$main_branch" && git pull && git co "${current}" && git rebase "origin/$main_branch" 36 | } 37 | 38 | b () { 39 | main_branch=$(get-primary-branch) 40 | git checkout "$main_branch" && git pull && git checkout -b "$1" 41 | } 42 | 43 | case $1 in 44 | "blank") blank ;; 45 | "br") br ;; 46 | "rb") rb ;; 47 | "b") b "$2";; 48 | *) echo "Not a valid subcommand." ;; 49 | esac 50 | -------------------------------------------------------------------------------- /bin/git-un: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if [ $# -eq 0 ] 8 | then 9 | git reset --soft HEAD^ 10 | exit 0 11 | fi 12 | 13 | FORCE="false" 14 | 15 | while [[ $# -gt 0 ]] 16 | do 17 | case $1 in 18 | -f|--force) 19 | FORCE="true" 20 | shift 21 | ;; 22 | -*|--*) 23 | echo "Unknown option $1" 24 | exit 1 25 | ;; 26 | *) 27 | POSITIONAL_ARGS+=("$1") # save positional arg 28 | shift # past argument 29 | ;; 30 | esac 31 | done 32 | 33 | set -- "${POSITIONAL_ARGS[@]}" 34 | if [ "$FORCE" = "true" ] 35 | then 36 | git restore "$@" 37 | else 38 | git restore --staged "$@" 39 | fi 40 | -------------------------------------------------------------------------------- /bin/hiper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # A bash implementation of hiper.lua and magnet.lua found in ~/hammerspoon 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | app="$1" 8 | 9 | case "$app" in 10 | dolphin) 11 | class="dolphin.dolphin" 12 | bin="dolphin" 13 | action="switch" 14 | ;; 15 | firefox) 16 | class="Navigator.firefox" 17 | bin="firefox" 18 | action="switch" 19 | ;; 20 | kitty) 21 | class="kitty.kitty" 22 | bin="ARCH=Linux kitty" 23 | action="switch" 24 | ;; 25 | obsidian) 26 | class="obsidian.obsidian" 27 | bin="~/Applications/Obsidian-0.15.9.AppImage" 28 | action="switch" 29 | ;; 30 | yast) 31 | class="YaST2.org.opensuse.YaST" 32 | bin="/usr/bin/xdg-su -c /sbin/yast2" 33 | action="switch" 34 | ;; 35 | zeal) 36 | class="zeal.Zeal" 37 | bin="zeal" 38 | action="switch" 39 | ;; 40 | 0) 41 | x=0 42 | y=0 43 | width=3840 44 | height=2160 45 | action="move" 46 | ;; 47 | 1) 48 | x=0 49 | y=0 50 | width=1920 51 | height=1080 52 | action="move" 53 | ;; 54 | 2) 55 | x=1920 56 | y=0 57 | width=1920 58 | height=1080 59 | action="move" 60 | ;; 61 | 3) 62 | x=0 63 | y=1080 64 | width=1920 65 | height=1080 66 | action="move" 67 | ;; 68 | 4) 69 | x=1920 70 | y=1080 71 | width=1920 72 | height=1080 73 | action="move" 74 | ;; 75 | left) 76 | x=0 77 | y=0 78 | width=1920 79 | height=2160 80 | action="move" 81 | ;; 82 | right) 83 | x=1920 84 | y=0 85 | width=1920 86 | height=2160 87 | action="move" 88 | ;; 89 | esac 90 | 91 | if [ "$action" = "switch" ] 92 | then 93 | wm_output=$(wmctrl -l -x | awk '{print $1, $3}' | grep "$class" || echo "inactive") 94 | 95 | if [ "$wm_output" = "inactive" ] 96 | then 97 | eval "$bin" 98 | else 99 | win_id=$(echo "$wm_output" | cut -d " " -f 1) 100 | wmctrl -i -a "$win_id" 101 | fi 102 | else 103 | win_id=$(xdotool getactivewindow) 104 | wmctrl -r :ACTIVE: -b remove,maximized_vert,maximized_horz 105 | xdotool windowsize "$win_id" "$width" "$height" 106 | xdotool windowmove "$win_id" "$x" "$y" 107 | fi 108 | -------------------------------------------------------------------------------- /bin/httpd-download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Author: Kai Xia 4 | Filename: httpd-download.py 5 | Type: httpd that support resume. 6 | Last modified: 2013-09-13 10:38 7 | 8 | Description: 9 | """ 10 | import os 11 | from random import randint 12 | import socket 13 | import sys 14 | from urllib.parse import quote 15 | 16 | from http.server import HTTPServer, SimpleHTTPRequestHandler 17 | from socketserver import ThreadingMixIn 18 | 19 | 20 | class NotracebackServer(ThreadingMixIn, HTTPServer): 21 | """ 22 | could make this a mixin, but decide to keep it simple for a simple script. 23 | """ 24 | 25 | def handle_error(self, *args): 26 | """override default function to disable traceback.""" 27 | pass 28 | 29 | 30 | class PartialContentHandler(SimpleHTTPRequestHandler): 31 | def mycopy(self, f): 32 | """ 33 | This would do the actual file tranfer. if client terminated transfer, 34 | we would log it. 35 | """ 36 | try: 37 | self.copyfile(f, self.wfile) 38 | self.log_message('"%s" %s', self.requestline, "req finished.") 39 | except socket.error: 40 | self.log_message('"%s" %s', self.requestline, "req terminated.") 41 | finally: 42 | f.close() 43 | return None 44 | 45 | def do_GET(self): 46 | """Serve a GET request.""" 47 | f = self.send_head() 48 | if f: 49 | self.mycopy(f) 50 | 51 | def send_head(self): 52 | """ 53 | added support for partial content. i'm not surprised if http HEAD 54 | method would fail. 55 | """ 56 | path = self.translate_path(self.path) 57 | f = None 58 | if os.path.isdir(path): 59 | # oh, we do not support directory listing. 60 | self.send_error(404, "File not found") 61 | return None 62 | 63 | ctype = self.guess_type(path) 64 | try: 65 | f = open(path, "rb") 66 | except IOError: 67 | self.send_error(404, "File not found") 68 | return None 69 | if self.headers.get("Range"): 70 | # partial content all treated here. 71 | # we do not support If-Range request. 72 | # range could only be of the form: 73 | # Range: bytes=9855420- 74 | start = self.headers.get("Range") 75 | try: 76 | pos = int(start[6:-1]) 77 | except ValueError: 78 | self.send_error(400, "bad range specified.") 79 | f.close() 80 | return None 81 | 82 | self.send_response(206) 83 | self.send_header("Content-type", ctype) 84 | self.send_header("Connection", "keep-alive") 85 | fs = os.fstat(f.fileno()) 86 | full = fs.st_size 87 | self.send_header("Content-Length", str(fs[6] - pos)) 88 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 89 | start = start.replace("=", " ") 90 | self.send_header("Content-Range", "%s%s/%s" % (start, full - 1, full)) 91 | self.end_headers() 92 | f.seek(pos) 93 | self.mycopy(f) 94 | return None 95 | 96 | self.send_response(200) 97 | self.send_header("Content-type", ctype) 98 | fs = os.fstat(f.fileno()) 99 | self.send_header("Content-Length", str(fs[6])) 100 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 101 | self.end_headers() 102 | return f 103 | 104 | 105 | def main(port, server_class=NotracebackServer, handler_class=PartialContentHandler): 106 | server_address = ("", port) 107 | httpd = server_class(server_address, handler_class) 108 | httpd.serve_forever() 109 | 110 | 111 | def get_ipaddress(host="www.163.com"): 112 | udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 113 | 114 | try: 115 | udp_socket.connect((host, 9)) 116 | ipaddress = udp_socket.getsockname()[0] 117 | except socket.error: 118 | raise 119 | finally: 120 | del udp_socket 121 | return ipaddress 122 | 123 | 124 | if __name__ == "__main__": 125 | port = randint(20000, 50000) 126 | ip = get_ipaddress() 127 | print("serving on: http://%s:%s/" % (ip, port)) 128 | print("===== local files =====") 129 | cwd = os.getcwd() 130 | for root, dirs, files in os.walk(cwd): 131 | for file in files: 132 | if file == sys.argv[0] or file.startswith("."): 133 | continue 134 | filepath = os.path.join(root, file) 135 | relative_path = quote("%s/%s" % (root[len(cwd) + 1 :], file)) 136 | if os.path.isfile(filepath): 137 | print("link: http://%s:%s/%s" % (ip, port, relative_path.lstrip("/"))) 138 | print("===== start logging =====\n") 139 | main(port=port) 140 | -------------------------------------------------------------------------------- /bin/iftop: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TOP="/usr/local/sbin/iftop" 4 | 5 | current_ipaddress=`python -c "import socket; u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); u.connect(('www.baidu.com', 9)); print u.getsockname()[0]"` 6 | 7 | hardware=`ifconfig | egrep -B 10 ".*${current_ipaddress}" | egrep -v "^\t" | tail -n 1 | awk -F ":" '{print $1}'` 8 | 9 | sudo $TOP -i ${hardware} 2>/dev/null 10 | -------------------------------------------------------------------------------- /bin/movielength: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LEN=`mplayer -vo null -ao null -frames 0 -identify "$1" 2>/dev/null | grep "^ID_LENGTH" | sed 's/ID_LENGTH=//g'` 3 | min=`echo "$LEN / 60 " | bc` 4 | echo $min minutes 5 | -------------------------------------------------------------------------------- /bin/mp3join: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Author: Kai Xia 4 | # Filename: mp3join 5 | # Date created: 2014-02-28 11:55 6 | # Last modified: 2014-02-28 11:55 7 | # Modified by: Kai Xia 8 | # 9 | # Description: 10 | # 11 | # Changelog: 12 | # 13 | 14 | TEMPFILE=mp3join.list 15 | OUTPUT="./output.mp3" 16 | rm -f $TEMPFILE 17 | 18 | python -c "import os; open('${TEMPFILE}', 'w').writelines(['file \'%s\'\n' % file for file in os.listdir('.') if file.endswith('.mp3')])" 19 | 20 | if [ -f $OUTPUT ] 21 | then 22 | echo "file exist, quit" 23 | exit 1 24 | fi 25 | 26 | ffmpeg -f concat -i "$TEMPFILE" -c copy $OUTPUT 27 | 28 | rm $TEMPFILE 29 | -------------------------------------------------------------------------------- /bin/myls: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Kai Xia 4 | # Filename: myls 5 | # Last modified: 2013-10-09 11:42 6 | # 7 | # Description: 8 | # Hide certain directories in my home. 9 | 10 | cwd=`pwd` 11 | 12 | if [ "x$cwd" = "x$HOME" ] 13 | then 14 | gls $LS_OPTIONS --hide="VirtualBox VMs" --hide="Applications*" --hide="Books" --hide="Desktop" --hide="Dropbox" --hide="Library" --hide="Music" --hide="Movies" --hide="Pictures" --hide="Public" --hide="Documents" $* 15 | else 16 | gls $LS_OPTIONS $* 17 | fi 18 | -------------------------------------------------------------------------------- /bin/mysql_init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Kai Xia 4 | # Filename: mysql.sh 5 | # Type: 6 | # Last modified: 2012-07-03 17:59 7 | # 8 | # Description: 9 | # 10 | 11 | echo "Enter mysql root user password:" 12 | read password 13 | echo "Enter project name:" 14 | read project 15 | 16 | echo -n "Generate password for you? (y/n)" 17 | read choice 18 | 19 | if [ "$choice" = "y" ] 20 | then 21 | dbpassword=`pwgen -sy 14 1` 22 | else 23 | echo "Enter your password for the database:" 24 | read dbpassword 25 | fi 26 | 27 | if [ "x$password" = "x" ] 28 | then 29 | echo "CREATE DATABASE $project CHARACTER SET utf8 COLLATE utf8_general_ci; grant ALL on $project.* to $project@localhost IDENTIFIED BY '$dbpassword'; flush privileges;" | mysql -h localhost -u root 30 | else 31 | echo "CREATE DATABASE $project CHARACTER SET utf8 COLLATE utf8_general_ci; grant ALL on $project.* to $project@localhost IDENTIFIED BY '$dbpassword'; flush privileges;" | mysql -h localhost -u root -p$password 32 | fi 33 | 34 | echo "done!" 35 | echo "proj: " $project 36 | echo "pass: " $dbpassword 37 | -------------------------------------------------------------------------------- /bin/pwgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Author: Kai Xia 4 | Filename: pwgen 5 | Last modified: 2018-06-13 22:54 6 | 7 | Description: 8 | Generate a password that's easier to type while being secure: 9 | 10 | 1. Need to have at least one lowercase character, one uppercase character, 11 | and one special characters. 12 | 2. Lowercase characters have a higher roll chance than digits/uppercase 13 | characters and punctuations. 14 | 3. It will generate 20 password so you can pick a random one. 15 | """ 16 | from secrets import choice, randbelow 17 | from random import shuffle 18 | from string import ascii_lowercase, ascii_uppercase 19 | 20 | special_lowercase = """`1234567890-=[];',./""" 21 | special_uppercase = '''~!@#$%^&*()_+{}:"<>?''' 22 | 23 | 24 | def regroup(chars): 25 | """Regroup characters so it's easier to type.""" 26 | lowers = [ 27 | char for char in chars 28 | if char in ascii_lowercase or char in special_lowercase 29 | ] 30 | uppers = [ 31 | char for char in chars 32 | if char in ascii_uppercase or char in special_uppercase 33 | ] 34 | if len(uppers) % 2: 35 | # pattern would be lower-upper-lower 36 | truncate = randbelow(len(lowers)) 37 | return lowers[:truncate] + uppers + lowers[truncate:] 38 | else: 39 | # pattern would be upper-lower-upper 40 | truncate = randbelow(len(uppers)) 41 | return uppers[:truncate] + lowers + uppers[truncate:] 42 | return chars 43 | 44 | 45 | def generate_one(length): 46 | characters = [ 47 | choice(ascii_lowercase), 48 | choice(ascii_uppercase), 49 | choice(special_lowercase), 50 | choice(special_uppercase), 51 | ] 52 | shuffle(characters) 53 | for i in range(length - 4): 54 | roll = randbelow(101) 55 | if roll > 95: 56 | # 5% chance to roll a special uppercase character 57 | characters.append(choice(special_uppercase)) 58 | elif roll > 85: 59 | # 10% chance to add a special lowercase character 60 | characters.append(choice(special_lowercase)) 61 | elif roll > 75: 62 | # 10% chance to add a normal uppercase character 63 | characters.append(choice(ascii_uppercase)) 64 | else: 65 | # 75% chance to add a normal lowercase character 66 | characters.append(choice(ascii_lowercase)) 67 | 68 | return "".join(regroup(characters)) 69 | 70 | def main(): 71 | for i in range(5): 72 | print(*[generate_one(16) for j in range(4)], sep=" ") 73 | 74 | 75 | if __name__ == '__main__': 76 | main() 77 | -------------------------------------------------------------------------------- /bin/rename_tab.scpt: -------------------------------------------------------------------------------- 1 | tell application "iTerm 2" 2 | activate 3 | 4 | set current_name to the name of current session of current window 5 | try 6 | display dialog "Rename Tab" default answer current_name 7 | set newname to (text returned of result) 8 | on error 9 | set newname to current_name 10 | end try 11 | tell current session of current window 12 | set name to newname 13 | end tell 14 | end tell 15 | -------------------------------------------------------------------------------- /bin/sloxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Rewrite of https://github.com/jakob/sloxy in Python. 4 | """ 5 | import argparse 6 | import select 7 | import socket 8 | import struct 9 | import sys 10 | import time 11 | 12 | 13 | class ForwardTask: 14 | def __init__(self): 15 | self.incoming = 0 16 | self.outgoing = 0 17 | self.next = None 18 | 19 | 20 | buffer_size = 0 21 | buffer = bytearray() 22 | speedlimit = 5000 23 | delay = 0.1 24 | 25 | 26 | def readwrite(from_socket, to_socket): 27 | global buffer, buffer_size, speedlimit, delay 28 | 29 | read_bytes = from_socket.recv(buffer_size) 30 | if len(read_bytes) == 0: 31 | return 1 32 | 33 | wait_time = len(read_bytes) / speedlimit 34 | if len(read_bytes) < buffer_size: 35 | wait_time += delay 36 | 37 | time.sleep(wait_time) 38 | 39 | to_socket.sendall(read_bytes) 40 | return 0 41 | 42 | 43 | def main(): 44 | global buffer, buffer_size, speedlimit, delay 45 | 46 | parser = argparse.ArgumentParser( 47 | description="Forward data between a local and a remote socket with speed limit and delay." 48 | ) 49 | parser.add_argument("listen_addr", help="Local address to listen on") 50 | parser.add_argument("listen_port", type=int, help="Local port to listen on") 51 | parser.add_argument("destination_addr", help="Remote address to forward data to") 52 | parser.add_argument("destination_port", type=int, help="Remote port to forward data to") 53 | parser.add_argument("speed_limit", type=float, help="Speed limit in KB/s") 54 | parser.add_argument("delay", type=float, help="Delay in seconds") 55 | 56 | args = parser.parse_args() 57 | 58 | speedlimit = args.speed_limit 59 | delay = args.delay 60 | 61 | buffer_size = int(speedlimit) 62 | if buffer_size > 1000000: 63 | buffer_size = 1000000 64 | buffer = bytearray(buffer_size) 65 | 66 | print(f" Delay: {delay*1000}ms") 67 | print(f"Speed Limit: {speedlimit / 1000}KB/s") 68 | print(f"Buffer Size: {buffer_size} bytes") 69 | print("\n") 70 | 71 | listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 73 | listen_socket.bind((args.listen_addr, args.listen_port)) 74 | listen_socket.listen(1) 75 | 76 | forward_tasks = [] 77 | max_socket = listen_socket.fileno() 78 | 79 | while True: 80 | rfds, _, _ = select.select( 81 | [listen_socket] 82 | + [task.incoming for task in forward_tasks] 83 | + [task.outgoing for task in forward_tasks], 84 | [], 85 | [], 86 | ) 87 | 88 | if listen_socket in rfds: 89 | print("Connecting to {}:{}".format(args.destination_addr, args.destination_port)) 90 | new_task = ForwardTask() 91 | new_task.incoming, _ = listen_socket.accept() 92 | if new_task.incoming.fileno() > max_socket: 93 | max_socket = new_task.incoming.fileno() 94 | 95 | target_sockaddr = (args.destination_addr, args.destination_port) 96 | new_task.outgoing = socket.create_connection(target_sockaddr) 97 | if new_task.outgoing.fileno() > max_socket: 98 | max_socket = new_task.outgoing.fileno() 99 | 100 | forward_tasks.append(new_task) 101 | print("Connected.") 102 | 103 | for task in forward_tasks: 104 | should_close = False 105 | if task.incoming in rfds: 106 | should_close = readwrite(task.incoming, task.outgoing) 107 | if not should_close and task.outgoing in rfds: 108 | should_close = readwrite(task.outgoing, task.incoming) 109 | 110 | if should_close: 111 | task.incoming.close() 112 | task.outgoing.close() 113 | forward_tasks.remove(task) 114 | print("Disconnected.") 115 | else: 116 | forward_tasks = [t for t in forward_tasks if t] 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /bootstrap/macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # This stage does not have any prerequisites. We should run this after the first boot. 8 | BASE_DIR="$HOME/.xiaket" 9 | if [ "$(uname -m)" = "arm64" ]; then 10 | brewdir=/opt/homebrew 11 | else 12 | brewdir=/usr/local 13 | fi 14 | 15 | # helpers 16 | check-done() { 17 | done_dir="$BASE_DIR/var/run/done" 18 | mkdir -p "$done_dir" 19 | func="${FUNCNAME[1]}" 20 | if [ -f "$done_dir/$func" ]; then 21 | return 1 22 | else 23 | return 0 24 | fi 25 | } 26 | 27 | touch-done() { 28 | done_dir="$BASE_DIR/var/run/done" 29 | func="${FUNCNAME[1]}" 30 | touch "$done_dir/$func" 31 | } 32 | 33 | # configuration steps 34 | clone-etc() { 35 | check-done || return 0 36 | xcode-select --install 2> /dev/null || true 37 | 38 | mkdir -p "$BASE_DIR/share/github" 39 | 40 | while true; do 41 | has_git=$(git --version 2> /dev/null || echo "false") 42 | if [ "$has_git" != "false" ]; then 43 | break 44 | fi 45 | echo "sleeping" 46 | sleep 5 47 | done 48 | 49 | git clone https://github.com/xiaket/etc.git "$BASE_DIR/share/github/etc" 50 | ln -s "$BASE_DIR/share/github/etc" "$BASE_DIR/etc" 51 | touch-done 52 | } 53 | 54 | homebrew() { 55 | check-done || return 0 56 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 57 | touch-done 58 | } 59 | 60 | homebrew-packages() { 61 | check-done || return 0 62 | homebrew 63 | 64 | brew update && 65 | brew bundle install --cleanup --file="$BASE_DIR/etc/Brewfile" --no-lock && 66 | brew upgrade 67 | 68 | touch-done 69 | } 70 | 71 | python-packages() { 72 | check-done || return 0 73 | brew install pipx 74 | pipx install black icdiff neovim poetry ptpython pyflakes pygments Snape termcolor 75 | touch-done 76 | } 77 | 78 | write-defaults() { 79 | check-done || return 0 80 | 81 | # disable the dashboard 82 | defaults write com.apple.dashboard mcx-disabled -bool TRUE 83 | killall Dock 84 | 85 | # be quiet please finder 86 | defaults write com.apple.finder FinderSounds -bool FALSE 87 | killall Finder 88 | 89 | # disable delay when 90 | defaults write com.apple.dock autohide-fullscreen-delayed -bool FALSE 91 | killall Dock 92 | 93 | # minimize key repeat 94 | defaults write -g InitialKeyRepeat -int 10 95 | defaults write -g KeyRepeat -int 1 96 | 97 | # Disable smarts, I don't need your help thanks. 98 | defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false 99 | defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false 100 | defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false 101 | 102 | touch-done 103 | 104 | # Switch to previous tab using alt-a and next tab alt-s. 105 | defaults write com.apple.Safari NSUserKeyEquivalents '{ 106 | "Show Previous Tab"="~a"; 107 | "Show Next Tab"="~s"; 108 | "Close Tab"="~q"; 109 | }' 110 | } 111 | 112 | build-ps1() { 113 | check-done || return 0 114 | $brewdir/bin/cargo build --release 115 | strip target/release/ps1 116 | mv target/release/ps1 ../bin 117 | touch-done 118 | } 119 | 120 | create-links() { 121 | check-done || return 0 122 | # for configuration in $HOME 123 | ln -sf "$BASE_DIR/etc/bashrc" "$HOME/.bashrc" 124 | ln -sf "$HOME/.bashrc" "$HOME/.bash_profile" 125 | ln -sf "$BASE_DIR/etc/gitconfig" "$HOME/.gitconfig" 126 | ln -sf "$BASE_DIR/etc/hammerspoon" "$HOME/.hammerspoon" 127 | ln -sf "$BASE_DIR/etc/inputrc" "$HOME/.inputrc" 128 | ln -sf "$BASE_DIR/etc/pythonrc" "$HOME/.pythonrc" 129 | ln -sf "$BASE_DIR/etc/snape.json" "$HOME/.snape.json" 130 | ln -sf "$BASE_DIR/etc/nvim" "$HOME/.vim" 131 | ln -sf "$BASE_DIR/share/github" "$HOME/.Github" 132 | ln -sf "$BASE_DIR/share/ssh" "$HOME/.ssh" 133 | 134 | # for configuration in .config 135 | mkdir -p "$HOME/.config" 136 | ln -sf "$BASE_DIR/etc/nvim" "$HOME/.config/nvim" 137 | ln -sf "$BASE_DIR/etc/kitty" "$HOME/.config/kitty" 138 | ln -sf $brewdir/bin/python3 $brewdir/bin/python 139 | ln -sf $brewdir/bin/pip3 $brewdir/bin/pip 140 | touch-done 141 | } 142 | 143 | misc-config() { 144 | check-done || return 0 145 | chsh -s /bin/bash 146 | nvim +qall 147 | (cd "$BASE_DIR/etc" && git remote set-url origin git@github.com:xiaket/etc.git) 148 | touch-done 149 | } 150 | 151 | clone-etc 152 | write-defaults 153 | homebrew 154 | homebrew-packages 155 | python-packages 156 | build-ps1 157 | create-links 158 | misc-config 159 | -------------------------------------------------------------------------------- /bootstrap/opensuse.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # This stage does not have any prerequisites. We should run this after the first boot. 8 | BASE_DIR="$HOME/.xiaket" 9 | 10 | 11 | # helpers 12 | check-done () { 13 | done_dir="$BASE_DIR/var/run/done" 14 | mkdir -p "$done_dir" 15 | func="${FUNCNAME[1]}" 16 | if [ -f "$done_dir/$func" ] 17 | then 18 | return 1 19 | else 20 | return 0 21 | fi 22 | } 23 | 24 | touch-done () { 25 | done_dir="$BASE_DIR/var/run/done" 26 | func="${FUNCNAME[1]}" 27 | touch "$done_dir/$func" 28 | } 29 | 30 | # configuration steps 31 | clone-etc () { 32 | check-done || return 0 33 | 34 | mkdir -p "$BASE_DIR/share/github" 35 | 36 | git clone https://github.com/xiaket/etc.git "$BASE_DIR/share/github/etc" 37 | ln -s "$BASE_DIR/share/github/etc" "$BASE_DIR/etc" 38 | touch-done 39 | } 40 | 41 | packages () { 42 | check-done || return 0 43 | 44 | touch-done 45 | } 46 | 47 | python-packages () { 48 | check-done || return 0 49 | python3 -m pip install -U pip 50 | python3 -m pip install black icdiff neovim poetry ptpython pyflakes pygments requests sh Snape termcolor virtualenv yamlfix 51 | touch-done 52 | } 53 | 54 | build-ps1 () { 55 | check-done || return 0 56 | cd "$BASE_DIR/etc/ps1" 57 | cargo build --release 58 | strip target/release/ps1 59 | mv target/release/ps1 ../bin 60 | touch-done 61 | } 62 | 63 | create-links () { 64 | check-done || return 0 65 | # for configuration in $HOME 66 | ln -sf "$BASE_DIR/etc/bashrc" "$HOME/.bashrc" 67 | ln -sf "$HOME/.bashrc" "$HOME/.bash_profile" 68 | ln -sf "$BASE_DIR/etc/gitconfig" "$HOME/.gitconfig" 69 | ln -sf "$BASE_DIR/etc/hammerspoon" "$HOME/.hammerspoon" 70 | ln -sf "$BASE_DIR/etc/inputrc" "$HOME/.inputrc" 71 | ln -sf "$BASE_DIR/etc/pythonrc" "$HOME/.pythonrc" 72 | ln -sf "$BASE_DIR/etc/snape.json" "$HOME/.snape.json" 73 | ln -sf "$BASE_DIR/etc/nvim" "$HOME/.vim" 74 | ln -sf "$BASE_DIR/share/github" "$HOME/.Github" 75 | ln -sf "$BASE_DIR/share/ssh" "$HOME/.ssh" 76 | 77 | # for configuration in .config 78 | mkdir -p "$HOME/.config" 79 | rm -rf "$HOME/.config/nvim" "$HOME/.config/kitty" "$HOME/.config/autostart" 80 | ln -sf "$BASE_DIR/etc/nvim" "$HOME/.config/nvim" 81 | ln -sf "$BASE_DIR/etc/kitty" "$HOME/.config/kitty" 82 | ln -sf "$BASE_DIR/etc/linux/autostart" "$HOME/.config/autostart" 83 | touch-done 84 | } 85 | 86 | misc-config () { 87 | check-done || return 0 88 | sudo snapper create-config / 89 | nvim --headless -c 'autocmd User PackerComplete quitall' -c 'PackerSync' 90 | (cd "$BASE_DIR/etc" && git remote set-url origin git@github.com:xiaket/etc.git) 91 | touch-done 92 | } 93 | 94 | rust-config () { 95 | sudo zypper install rustup 96 | rustup default stable 97 | } 98 | 99 | clone-etc 100 | packages 101 | python-packages 102 | build-ps1 103 | create-links 104 | misc-config 105 | -------------------------------------------------------------------------------- /dir_colors: -------------------------------------------------------------------------------- 1 | # Dark 256 color solarized theme for the color GNU ls utility. 2 | # Used and tested with dircolors (GNU coreutils) 8.5 3 | # 4 | # @author {@link http://sebastian.tramp.name Sebastian Tramp} 5 | # @license http://sam.zoy.org/wtfpl/ Do What The Fuck You Want To Public License (WTFPL) 6 | # 7 | # More Information at 8 | # https://github.com/seebi/dircolors-solarized 9 | 10 | # Term Section 11 | TERM xterm-256color 12 | TERM xterm-color 13 | 14 | ## Special files 15 | 16 | RESET 0 # reset to "normal" color 17 | DIR 00;38;5;33 # directory 01;34 18 | LINK 00;38;5;37 # symbolic link. 19 | MULTIHARDLINK 00 # regular file with more than one link 20 | FIFO 48;5;230;38;5;136;01 # pipe 21 | SOCK 48;5;230;38;5;136;01 # socket 22 | BLK 48;5;230;38;5;244;01 # block device driver 23 | CHR 48;5;230;38;5;244;01 # character device driver 24 | ORPHAN 48;5;235;38;5;160 # symlink to nonexistent file, or non-stat'able file 25 | SETUID 48;5;160;38;5;230 # file that is setuid (u+s) 26 | SETGID 48;5;136;38;5;230 # file that is setgid (g+s) 27 | CAPABILITY 30;41 # file with capability 28 | STICKY_OTHER_WRITABLE 48;5;64;38;5;230 # dir that is sticky and other-writable (+t,o+w) 29 | OTHER_WRITABLE 48;5;235;38;5;33 # dir that is other-writable (o+w) and not sticky 30 | STICKY 48;5;33;38;5;230 # dir with the sticky bit set (+t) and not other-writable 31 | 32 | 33 | # configured by xiaket. 34 | NORMAL 00;38;5;247 # a little brighter than usual. 35 | EXEC 00;38;5;046 # a little brighter than usual. 36 | 37 | .py 01;38;5;202 38 | .rb 01;38;5;198 39 | .sh 01;38;5;58 40 | .log 00;38;5;222 41 | *Makefile 00;38;5;125 42 | .htm 00;38;5;123 43 | .xml 00;38;5;104 44 | .html 00;38;5;123 45 | .doc 00;38;5;142 46 | .docx 00;38;5;142 47 | .ppt 00;38;5;142 48 | .pptx 00;38;5;142 49 | .xls 00;38;5;142 50 | .xlsx 00;38;5;142 51 | .js 01;38;5;54 52 | .css 00;38;5;200 53 | 54 | ## Archives or compressed (violet + bold for compression) 55 | .tar 00;38;5;9 56 | .tgz 00;38;5;9 57 | .txz 00;38;5;9 58 | .zip 00;38;5;9 59 | .gz 00;38;5;9 60 | .xz 00;38;5;9 61 | .bz2 00;38;5;9 62 | .deb 00;38;5;9 63 | .rpm 00;38;5;9 64 | .jar 00;38;5;9 65 | .rar 00;38;5;9 66 | .7z 00;38;5;9 67 | .gem 00;38;5;9 68 | 69 | # Image formats (yellow) 70 | .jpg 00;38;5;168 71 | .JPG 00;38;5;168 72 | .jpeg 00;38;5;168 73 | .gif 00;38;5;168 74 | .bmp 00;38;5;168 75 | .tif 00;38;5;168 76 | .tiff 00;38;5;168 77 | .png 00;38;5;168 78 | .svg 00;38;5;168 79 | .svgz 00;38;5;168 80 | 81 | # Text files of special interest (base1 + bold) 82 | .torrent 00;38;5;015 83 | .xml 00;38;5;015 84 | *rc 00;38;5;015 85 | *1 00;38;5;015 86 | .nfo 00;38;5;015 87 | *README 00;38;5;015 88 | *README.txt 00;38;5;015 89 | *readme.txt 00;38;5;015 90 | .md 00;38;5;015 91 | .ini 00;38;5;015 92 | .yml 00;38;5;015 93 | .cfg 00;38;5;015 94 | .conf 00;38;5;015 95 | 96 | # "unimportant" files as logs and backups (base01) 97 | .bak 00;38;5;236 98 | .aux 00;38;5;236 99 | *~ 00;38;5;236 100 | *# 00;38;5;236 101 | .part 00;38;5;236 102 | .incomplete 00;38;5;236 103 | .swp 00;38;5;236 104 | .tmp 00;38;5;236 105 | .temp 00;38;5;236 106 | .o 00;38;5;236 107 | .pyc 00;38;5;236 108 | .pyo 00;38;5;236 109 | 110 | # Video formats (as audio + bold) 111 | .mov 00;38;5;090 112 | .mkv 00;38;5;090 113 | .mp4 00;38;5;090 114 | .m4v 00;38;5;090 115 | .ts 00;38;5;090 116 | 117 | # playground 118 | # for i in `seq -w 1 256`; do touch xiaket.$i; done 119 | # python3 -c '[print(".%03i\t01;38;5;%i" % (x, x)) for x in range(1, 256)]' 120 | #.001 01;38;5;1 121 | #.002 01;38;5;2 122 | #.003 01;38;5;3 123 | #.004 01;38;5;4 124 | #.005 01;38;5;5 125 | #.006 01;38;5;6 126 | #.007 01;38;5;7 127 | #.008 01;38;5;8 128 | #.009 01;38;5;9 129 | #.010 01;38;5;10 130 | #.011 01;38;5;11 131 | #.012 01;38;5;12 132 | #.013 01;38;5;13 133 | #.014 01;38;5;14 134 | #.015 01;38;5;15 135 | #.016 01;38;5;16 136 | #.017 01;38;5;17 137 | #.018 01;38;5;18 138 | #.019 01;38;5;19 139 | #.020 01;38;5;20 140 | #.021 01;38;5;21 141 | #.022 01;38;5;22 142 | #.023 01;38;5;23 143 | #.024 01;38;5;24 144 | #.025 01;38;5;25 145 | #.026 01;38;5;26 146 | #.027 01;38;5;27 147 | #.028 01;38;5;28 148 | #.029 01;38;5;29 149 | #.030 01;38;5;30 150 | #.031 01;38;5;31 151 | #.032 01;38;5;32 152 | #.033 01;38;5;33 153 | #.034 01;38;5;34 154 | #.035 01;38;5;35 155 | #.036 01;38;5;36 156 | #.037 01;38;5;37 157 | #.038 01;38;5;38 158 | #.039 01;38;5;39 159 | #.040 01;38;5;40 160 | #.041 01;38;5;41 161 | #.042 01;38;5;42 162 | #.043 01;38;5;43 163 | #.044 01;38;5;44 164 | #.045 01;38;5;45 165 | #.046 01;38;5;46 166 | #.047 01;38;5;47 167 | #.048 01;38;5;48 168 | #.049 01;38;5;49 169 | #.050 01;38;5;50 170 | #.051 01;38;5;51 171 | #.052 01;38;5;52 172 | #.053 01;38;5;53 173 | #.054 01;38;5;54 174 | #.055 01;38;5;55 175 | #.056 01;38;5;56 176 | #.057 01;38;5;57 177 | #.058 01;38;5;58 178 | #.059 01;38;5;59 179 | #.060 01;38;5;60 180 | #.061 01;38;5;61 181 | #.062 01;38;5;62 182 | #.063 01;38;5;63 183 | #.064 01;38;5;64 184 | #.065 01;38;5;65 185 | #.066 01;38;5;66 186 | #.067 01;38;5;67 187 | #.068 01;38;5;68 188 | #.069 01;38;5;69 189 | #.070 01;38;5;70 190 | #.071 01;38;5;71 191 | #.072 01;38;5;72 192 | #.073 01;38;5;73 193 | #.074 01;38;5;74 194 | #.075 01;38;5;75 195 | #.076 01;38;5;76 196 | #.077 01;38;5;77 197 | #.078 01;38;5;78 198 | #.079 01;38;5;79 199 | #.080 01;38;5;80 200 | #.081 01;38;5;81 201 | #.082 01;38;5;82 202 | #.083 01;38;5;83 203 | #.084 01;38;5;84 204 | #.085 01;38;5;85 205 | #.086 01;38;5;86 206 | #.087 01;38;5;87 207 | #.088 01;38;5;88 208 | #.089 01;38;5;89 209 | #.090 01;38;5;90 210 | #.091 01;38;5;91 211 | #.092 01;38;5;92 212 | #.093 01;38;5;93 213 | #.094 01;38;5;94 214 | #.095 01;38;5;95 215 | #.096 01;38;5;96 216 | #.097 01;38;5;97 217 | #.098 01;38;5;98 218 | #.099 01;38;5;99 219 | #.100 01;38;5;100 220 | #.101 01;38;5;101 221 | #.102 01;38;5;102 222 | #.103 01;38;5;103 223 | #.104 01;38;5;104 224 | #.105 01;38;5;105 225 | #.106 01;38;5;106 226 | #.107 01;38;5;107 227 | #.108 01;38;5;108 228 | #.109 01;38;5;109 229 | #.110 01;38;5;110 230 | #.111 01;38;5;111 231 | #.112 01;38;5;112 232 | #.113 01;38;5;113 233 | #.114 01;38;5;114 234 | #.115 01;38;5;115 235 | #.116 01;38;5;116 236 | #.117 01;38;5;117 237 | #.118 01;38;5;118 238 | #.119 01;38;5;119 239 | #.120 01;38;5;120 240 | #.121 01;38;5;121 241 | #.122 01;38;5;122 242 | #.123 01;38;5;123 243 | #.124 01;38;5;124 244 | #.125 01;38;5;125 245 | #.126 01;38;5;126 246 | #.127 01;38;5;127 247 | #.128 01;38;5;128 248 | #.129 01;38;5;129 249 | #.130 01;38;5;130 250 | #.131 01;38;5;131 251 | #.132 01;38;5;132 252 | #.133 01;38;5;133 253 | #.134 01;38;5;134 254 | #.135 01;38;5;135 255 | #.136 01;38;5;136 256 | #.137 01;38;5;137 257 | #.138 01;38;5;138 258 | #.139 01;38;5;139 259 | #.140 01;38;5;140 260 | #.141 01;38;5;141 261 | #.142 01;38;5;142 262 | #.143 01;38;5;143 263 | #.144 01;38;5;144 264 | #.145 01;38;5;145 265 | #.146 01;38;5;146 266 | #.147 01;38;5;147 267 | #.148 01;38;5;148 268 | #.149 01;38;5;149 269 | #.150 01;38;5;150 270 | #.151 01;38;5;151 271 | #.152 01;38;5;152 272 | #.153 01;38;5;153 273 | #.154 01;38;5;154 274 | #.155 01;38;5;155 275 | #.156 01;38;5;156 276 | #.157 01;38;5;157 277 | #.158 01;38;5;158 278 | #.159 01;38;5;159 279 | #.160 01;38;5;160 280 | #.161 01;38;5;161 281 | #.162 01;38;5;162 282 | #.163 01;38;5;163 283 | #.164 01;38;5;164 284 | #.165 01;38;5;165 285 | #.166 01;38;5;166 286 | #.167 01;38;5;167 287 | #.168 01;38;5;168 288 | #.169 01;38;5;169 289 | #.170 01;38;5;170 290 | #.171 01;38;5;171 291 | #.172 01;38;5;172 292 | #.173 01;38;5;173 293 | #.174 01;38;5;174 294 | #.175 01;38;5;175 295 | #.176 01;38;5;176 296 | #.177 01;38;5;177 297 | #.178 01;38;5;178 298 | #.179 01;38;5;179 299 | #.180 01;38;5;180 300 | #.181 01;38;5;181 301 | #.182 01;38;5;182 302 | #.183 01;38;5;183 303 | #.184 01;38;5;184 304 | #.185 01;38;5;185 305 | #.186 01;38;5;186 306 | #.187 01;38;5;187 307 | #.188 01;38;5;188 308 | #.189 01;38;5;189 309 | #.190 01;38;5;190 310 | #.191 01;38;5;191 311 | #.192 01;38;5;192 312 | #.193 01;38;5;193 313 | #.194 01;38;5;194 314 | #.195 01;38;5;195 315 | #.196 01;38;5;196 316 | #.197 01;38;5;197 317 | #.198 01;38;5;198 318 | #.199 01;38;5;199 319 | #.200 01;38;5;200 320 | #.201 01;38;5;201 321 | #.202 01;38;5;202 322 | #.203 01;38;5;203 323 | #.204 01;38;5;204 324 | #.205 01;38;5;205 325 | #.206 01;38;5;206 326 | #.207 01;38;5;207 327 | #.208 01;38;5;208 328 | #.209 01;38;5;209 329 | #.210 01;38;5;210 330 | #.211 01;38;5;211 331 | #.212 01;38;5;212 332 | #.213 01;38;5;213 333 | #.214 01;38;5;214 334 | #.215 01;38;5;215 335 | #.216 01;38;5;216 336 | #.217 01;38;5;217 337 | #.218 01;38;5;218 338 | #.219 01;38;5;219 339 | #.220 01;38;5;220 340 | #.221 01;38;5;221 341 | #.222 01;38;5;222 342 | #.223 01;38;5;223 343 | #.224 01;38;5;224 344 | #.225 01;38;5;225 345 | #.226 01;38;5;226 346 | #.227 01;38;5;227 347 | #.228 01;38;5;228 348 | #.229 01;38;5;229 349 | #.230 01;38;5;230 350 | #.231 01;38;5;231 351 | #.232 01;38;5;232 352 | #.233 01;38;5;233 353 | #.234 01;38;5;234 354 | #.235 01;38;5;235 355 | #.236 01;38;5;236 356 | #.237 01;38;5;237 357 | #.238 01;38;5;238 358 | #.239 01;38;5;239 359 | #.240 01;38;5;240 360 | #.241 01;38;5;241 361 | #.242 01;38;5;242 362 | #.243 01;38;5;243 363 | #.244 01;38;5;244 364 | #.245 01;38;5;245 365 | #.246 01;38;5;246 366 | #.247 01;38;5;247 367 | #.248 01;38;5;248 368 | #.249 01;38;5;249 369 | #.250 01;38;5;250 370 | #.251 01;38;5;251 371 | #.252 01;38;5;252 372 | #.253 01;38;5;253 373 | #.254 01;38;5;254 374 | #.255 01;38;5;255 375 | -------------------------------------------------------------------------------- /fzf/key-binding.sh: -------------------------------------------------------------------------------- 1 | __fzf_history__() { 2 | local output opts script 3 | opts="--height '40%' --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0" 4 | script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++' 5 | output=$( 6 | builtin fc -lnr -2147483648 | 7 | last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" | 8 | FZF_DEFAULT_OPTS="$opts" fzf --query "$READLINE_LINE" 9 | ) || return 10 | READLINE_LINE=${output#*$'\t'} 11 | if [[ -z "$READLINE_POINT" ]]; then 12 | echo "$READLINE_LINE" 13 | else 14 | READLINE_POINT=0x7fffffff 15 | fi 16 | } 17 | 18 | # Required to refresh the prompt after fzf 19 | bind -m emacs-standard '"\er": redraw-current-line' 20 | 21 | # CTRL-R - Paste the selected command from history into the command line 22 | bind -m emacs-standard -x '"\C-r": __fzf_history__' 23 | bind -m vi-command -x '"\C-r": __fzf_history__' 24 | bind -m vi-insert -x '"\C-r": __fzf_history__' 25 | -------------------------------------------------------------------------------- /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | for file in $(git diff --name-only --cached --diff-filter=ACMR) 8 | do 9 | if grep -iq "do not commit" "$file"; then 10 | echo "Commit message contains 'do not commit'. Commit aborted." 11 | exit 1 12 | fi 13 | done 14 | 15 | exit 0 16 | -------------------------------------------------------------------------------- /gitconfig: -------------------------------------------------------------------------------- 1 | [user] 2 | name = Kai Xia 3 | email = kaix+github@fastmail.com 4 | 5 | [alias] 6 | st = status 7 | co = checkout 8 | p = push -u 9 | c = cherry-pick 10 | r = rebase 11 | lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit 12 | idff = diff 13 | head = rev-parse HEAD 14 | rb = misc rb # my rebase 15 | br = misc br # my branch 16 | b = misc b # create branch from latest master 17 | blank = misc blank # create empty commit and push 18 | 19 | [core] 20 | excludesfile = ~/.xiaket/etc/gitignore_global 21 | pager = cat 22 | quotepath = false 23 | hooksPath = ~/.xiaket/etc/git-hooks 24 | [push] 25 | default = current 26 | [color] 27 | ui = true 28 | branch = auto 29 | diff = auto 30 | interactive = auto 31 | status = auto 32 | [grep] 33 | extendRegexp = true 34 | lineNumber = true 35 | [branch] 36 | autosetuprebase = always 37 | 38 | [diff] 39 | tool = icdiff 40 | [difftool] 41 | prompt = false 42 | [difftool "icdiff"] 43 | cmd = /usr/local/bin/icdiff --line-numbers $LOCAL $REMOTE 44 | 45 | [includeIf "gitdir:~/.Bitbucket/"] 46 | path = .xiaket/alt/etc/gitconfig 47 | [includeIf "gitdir:~/.xiaket/share/bitbucket/"] 48 | path = .xiaket/alt/etc/gitconfig 49 | -------------------------------------------------------------------------------- /gitignore_global: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | *.pyc 4 | .coverage 5 | *.egg-info 6 | .manage 7 | **/.claude/settings.local.json 8 | -------------------------------------------------------------------------------- /hammerspoon/Spoons/InputSourceSwitch.spoon/init.lua: -------------------------------------------------------------------------------- 1 | --- === InputSourceSwitch === 2 | --- 3 | --- Automatically switch the input source when switching applications. 4 | --- 5 | --- Example: 6 | --- ``` 7 | --- hs.loadSpoon("InputSourceSwitch") 8 | --- 9 | --- spoon.InputSourceSwitch:setApplications({ 10 | --- ["WeChat"] = "Pinyin - Simplified", 11 | --- ["Mail"] = "ABC" 12 | --- }) 13 | --- 14 | --- spoon.InputSourceSwitch:start() 15 | --- ``` 16 | --- 17 | --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/InputSourceSwitch.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/InputSourceSwitch.spoon.zip) 18 | 19 | local obj = {} 20 | obj.__index = obj 21 | 22 | -- Metadata 23 | obj.name = "InputSourceSwitch" 24 | obj.version = "1.0" 25 | obj.author = "eks5115 " 26 | obj.homepage = "https://github.com/Hammerspoon/Spoons" 27 | obj.license = "MIT - https://opensource.org/licenses/MIT" 28 | 29 | local log = hs.logger.new("InputSourceSwitch", "debug") 30 | log.d("Init") 31 | 32 | -- Internal function used to get enabled input sources 33 | local function isLayout(layoutName) 34 | local layouts = hs.keycodes.layouts() 35 | for key, value in pairs(layouts) do 36 | if value == layoutName then 37 | return true 38 | end 39 | end 40 | 41 | return false 42 | end 43 | 44 | local function isMethod(methodName) 45 | local methods = hs.keycodes.methods() 46 | for key, value in pairs(methods) do 47 | if value == methodName then 48 | return true 49 | end 50 | end 51 | 52 | return false 53 | end 54 | 55 | local function setAppInputSource(appName, sourceName, event) 56 | event = event or hs.window.filter.windowFocused 57 | 58 | hs.window.filter.new(appName):subscribe(event, function() 59 | local r = true 60 | 61 | if isLayout(sourceName) then 62 | r = hs.keycodes.setLayout(sourceName) 63 | elseif isMethod(sourceName) then 64 | r = hs.keycodes.setMethod(sourceName) 65 | else 66 | hs.alert.show(string.format("sourceName: %s is not layout or method", sourceName)) 67 | end 68 | 69 | if not r then 70 | hs.alert.show(string.format("set %s to %s failure", appName, sourceName)) 71 | end 72 | end) 73 | end 74 | 75 | --- InputSourceSwitch.applicationMap 76 | --- Variable 77 | --- Mapping the application name to the input source 78 | obj.applicationsMap = {} 79 | 80 | --- InputSourceSwitch:setApplications() 81 | --- Method 82 | --- Set that mapping the application name to the input source 83 | --- 84 | --- Parameters: 85 | --- * applications - A table containing that mapping the application name to the input source 86 | --- key is the application name and value is the input source name 87 | --- example: 88 | --- ``` 89 | --- { 90 | --- ["WeChat"] = "Pinyin - Simplified", 91 | --- ["Mail"] = "ABC" 92 | --- } 93 | --- ``` 94 | function obj:setApplications(applications) 95 | for key, value in pairs(applications) do 96 | obj.applicationsMap[key] = value 97 | end 98 | end 99 | 100 | --- InputSourceSwitch:start() 101 | --- Method 102 | --- Start InputSourceSwitch 103 | --- 104 | --- Parameters: 105 | --- * None 106 | function obj:start() 107 | for k, v in pairs(self.applicationsMap) do 108 | setAppInputSource(k, v) 109 | end 110 | return self 111 | end 112 | 113 | return obj 114 | -------------------------------------------------------------------------------- /hammerspoon/hiper.lua: -------------------------------------------------------------------------------- 1 | local realFlagMask = { 2 | [0x37] = 8, -- lcmd 0000 1000 3 | [0x36] = 16, -- rcmd 0001 0000 4 | [0x3a] = 32, -- lalt 0010 0000 5 | [0x3d] = 64, -- ralt 0100 0000 6 | [0x3b] = 1, -- lctrl 0000 0001 7 | [0x3e] = 8192, -- rctrl 10 0000 0000 0000 8 | [0x38] = 2, -- lshift 0000 0010 9 | [0x3c] = 4, -- rshift 0000 0100 10 | } 11 | 12 | Hiper = {} 13 | Hiper.new = function(key_name) 14 | local self = { 15 | features = {}, 16 | } 17 | self.key = hs.keycodes.map[key_name] 18 | 19 | local modifierHandler = function(event) 20 | local keyCode = event:getKeyCode() 21 | if keyCode ~= self.key then 22 | return false 23 | end 24 | 25 | local realFlags = event:getRawEventData().CGEventData.flags 26 | local mask = realFlagMask[self.key] 27 | if mask == nil then 28 | return false 29 | end 30 | 31 | if (realFlags & mask) == mask then 32 | if not self.featureTap:isEnabled() then 33 | self.featureTap:start() 34 | end 35 | else 36 | if self.featureTap:isEnabled() then 37 | self.featureTap:stop() 38 | -- self.modifierTap:stop() 39 | -- self.modifierTap:start() 40 | end 41 | end 42 | return false 43 | end 44 | 45 | local featureHandler = function(event) 46 | local keyCode = event:getKeyCode() 47 | local isKeyUp = event:getType() ~= hs.eventtap.event.types.keyDown 48 | local isRepeat = not isKeyUp and event:getProperty(hs.eventtap.event.properties["keyboardEventAutorepeat"]) ~= 0 49 | 50 | -- Handle hyper key itself 51 | if keyCode == self.key then 52 | return not isKeyUp 53 | end 54 | 55 | -- Only process keydowns that aren't repeats 56 | if isKeyUp or isRepeat or not self.features[keyCode] then 57 | return false 58 | end 59 | 60 | local feature = self.features[keyCode] 61 | local shiftPressed = event:getFlags().shift 62 | 63 | -- Execute appropriate function based on type and case 64 | if type(feature) == "function" then 65 | feature() -- Legacy format 66 | elseif type(feature) == "table" then 67 | if shiftPressed and feature.hasUpper then 68 | feature.upperFn() 69 | elseif not shiftPressed and feature.hasLower then 70 | feature.lowerFn() 71 | end 72 | end 73 | 74 | return true 75 | end 76 | 77 | self.featureTap = hs.eventtap.new( 78 | { hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp }, 79 | featureHandler 80 | ) 81 | self.modifierTap = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, modifierHandler) 82 | self.modifierTap:start() 83 | 84 | self.load_features = function(features) 85 | local keymap = {} 86 | 87 | -- Collect keycodes with case info 88 | for key, fn in pairs(features) do 89 | local keycode = hs.keycodes.map[key:lower()] 90 | if keycode then 91 | keymap[keycode] = keymap[keycode] or {} 92 | 93 | -- Store function based on case 94 | if key:match("[A-Z]") then 95 | keymap[keycode].upperFn = fn 96 | keymap[keycode].hasUpper = true 97 | else 98 | keymap[keycode].lowerFn = fn 99 | keymap[keycode].hasLower = true 100 | end 101 | end 102 | end 103 | 104 | -- Map to final features table 105 | for keycode, info in pairs(keymap) do 106 | self.features[keycode] = info 107 | end 108 | end 109 | 110 | return self 111 | end 112 | 113 | return Hiper 114 | -------------------------------------------------------------------------------- /hammerspoon/init.lua: -------------------------------------------------------------------------------- 1 | local hiper = require("hiper").new("rightcmd") 2 | local magnet = require("magnet") 3 | local power = require("power") 4 | local is_switch = require("is_switch") 5 | 6 | local features = { 7 | -- Simple app maps 8 | a = "Arc", 9 | b = "Books", 10 | c = "Canva", 11 | d = "Dash", 12 | f = "Finder", 13 | g = "Cherry Studio", 14 | k = "Kitty", 15 | l = "Slack", 16 | m = "Mail", 17 | n = "Notes", 18 | o = "Obsidian", 19 | s = "Superlist", 20 | x = "Firefox", 21 | w = "WeChat", 22 | z = "Zed", 23 | 24 | -- uppercase as we are running low on characters 25 | C = "Calendar", 26 | M = "Music", 27 | Z = "zoom.us", 28 | 29 | -- Debug 30 | h = function() 31 | hs.reload() 32 | hs.console.clearConsole() 33 | end, 34 | -- Lock screen 35 | i = function() 36 | hs.osascript.applescript( 37 | 'tell application "System Events" to keystroke "q" using {command down,control down}' 38 | ) 39 | end, 40 | -- Pause/Play audio 41 | p = function() 42 | require("hs.eventtap").event.newSystemKeyEvent("PLAY", true):post() 43 | end, 44 | } 45 | 46 | -- Windows management features 47 | magnetCommands = { "0", "1", "2", "3", "4", ",", "." } 48 | for i = 1, 7 do 49 | features[magnetCommands[i]] = function() 50 | magnet(magnetCommands[i]) 51 | end 52 | end 53 | 54 | for key, feature in pairs(features) do 55 | if type(feature) == "string" then 56 | features[key] = function() 57 | hs.application.launchOrFocus(feature) 58 | end 59 | end 60 | end 61 | 62 | hiper.load_features(features) 63 | -------------------------------------------------------------------------------- /hammerspoon/is_switch.lua: -------------------------------------------------------------------------------- 1 | hs.loadSpoon("InputSourceSwitch") 2 | 3 | spoon.InputSourceSwitch:setApplications({ 4 | ["WeChat"] = "Pinyin - Simplified", 5 | ["kitty"] = "U.S.", 6 | }) 7 | 8 | spoon.InputSourceSwitch:start() 9 | -------------------------------------------------------------------------------- /hammerspoon/magnet.lua: -------------------------------------------------------------------------------- 1 | -- Module to manage windows in macos 2 | 3 | hs.window.animationDuration = 0 4 | hs.screen.strictScreenInDirection = false 5 | 6 | function magnet(how) 7 | --[[ 8 | Move current active window like magnet app do. 9 | Possible values for how and their meanings: 10 | left: move window to the left half of the current screen(possibly between screens) 11 | right: move window to the right half of the current screen(possibly between screens) 12 | 0: Maximize current window 13 | 1: Move window to the top left corner 14 | 2: Move window to the top right corner 15 | 3: Move window to the bottom left corner 16 | 4: Move window to the bottom right corner 17 | ]] 18 | -- 19 | local win = hs.window.focusedWindow() 20 | local frame = win:screen():fullFrame() 21 | 22 | if how == "0" then 23 | win:maximize() 24 | elseif how == "1" then 25 | win:setFrame({ x = frame.x, y = frame.y, w = frame.w / 2, h = frame.h / 2 }) 26 | elseif how == "2" then 27 | win:setFrame({ x = frame.x + frame.w / 2, y = frame.y, w = frame.w / 2, h = frame.h / 2 }) 28 | elseif how == "3" then 29 | win:setFrame({ x = frame.x, y = frame.y + frame.h / 2, w = frame.w / 2, h = frame.h / 2 }) 30 | elseif how == "4" then 31 | win:setFrame({ x = frame.x + frame.w / 2, y = frame.y + frame.h / 2, w = frame.w / 2, h = frame.h 32 | / 2 }) 33 | else 34 | sorted = sortScreens() 35 | if how == "," then 36 | moveLeft(sorted, win) 37 | elseif how == "." then 38 | moveRight(sorted, win) 39 | end 40 | end 41 | end 42 | 43 | function sortScreens() 44 | sorted = {} 45 | local inserted = false 46 | for screen, pos in pairs(hs.screen.screenPositions()) do 47 | inserted = false 48 | for i, s in ipairs(sorted) do 49 | x, y = s:position() 50 | if pos.x < x then 51 | table.insert(sorted, i, screen) 52 | inserted = true 53 | break 54 | end 55 | end 56 | if not inserted then 57 | table.insert(sorted, screen) 58 | end 59 | end 60 | return sorted 61 | end 62 | 63 | function moveLeft(sorted, win) 64 | local frame = win:screen():fullFrame() 65 | old = win:frame() 66 | win:setFrame({ x = frame.x, y = frame.y, w = frame.w / 2, h = frame.h }) 67 | new = win:frame() 68 | if old.x ~= new.x or old.y ~= new.y or old.w ~= new.w or old.h ~= new.h then 69 | return 70 | end 71 | 72 | local frame = win:screen():fullFrame() 73 | 74 | local moveCross = false 75 | for i, screen in ipairs(sorted) do 76 | if screen:id() == win:screen():id() and i ~= 1 then 77 | moveCross = true 78 | break 79 | end 80 | end 81 | 82 | if not moveCross then 83 | return 84 | end 85 | 86 | win:moveOneScreenWest() 87 | local frame = win:screen():fullFrame() 88 | win:setFrame({ x = frame.x + frame.w / 2, y = frame.y, w = frame.w / 2, h = frame.h }) 89 | end 90 | 91 | function moveRight(sorted, win) 92 | local frame = win:screen():fullFrame() 93 | 94 | old = win:frame() 95 | win:setFrame({ x = frame.x + frame.w / 2, y = frame.y, w = frame.w / 2, h = frame.h }) 96 | new = win:frame() 97 | if old.x ~= new.x or old.y ~= new.y or old.w ~= new.w or old.h ~= new.h then 98 | return 99 | end 100 | 101 | frame = win:screen():fullFrame() 102 | 103 | local moveCross = false 104 | for i, screen in ipairs(sorted) do 105 | if screen:id() == win:screen():id() and i ~= #sorted then 106 | moveCross = true 107 | break 108 | end 109 | end 110 | 111 | if not moveCross then 112 | return 113 | end 114 | 115 | win:moveOneScreenEast() 116 | frame = win:screen():fullFrame() 117 | win:setFrame({ x = frame.x, y = frame.y, w = frame.w / 2, h = frame.h }) 118 | end 119 | 120 | return magnet 121 | -------------------------------------------------------------------------------- /hammerspoon/power.lua: -------------------------------------------------------------------------------- 1 | -- src: https://github.com/Hammerspoon/hammerspoon/issues/2314 2 | -- with minor mods. 3 | 4 | caffeine = hs.menubar.new() 5 | 6 | -- Always caffeinate on startup. 7 | shouldCaffeinate = false 8 | 9 | function setCaffeineDisplay(state) 10 | if state then 11 | caffeine:setTitle("on") 12 | else 13 | caffeine:setTitle("off") 14 | end 15 | end 16 | 17 | function setCaffeine(state) 18 | hs.caffeinate.set("displayIdle", state, true) 19 | setCaffeineDisplay(state) 20 | end 21 | 22 | function caffeineClicked() 23 | shouldCaffeinate = not shouldCaffeinate 24 | setCaffeine(shouldCaffeinate) 25 | end 26 | 27 | if caffeine then 28 | caffeine:setClickCallback(caffeineClicked) 29 | setCaffeine(shouldCaffeinate) 30 | end 31 | 32 | local pow = hs.caffeinate.watcher 33 | local log = hs.logger.new("caffeine", "verbose") 34 | 35 | local function on_pow(event) 36 | local name = "?" 37 | for key, val in pairs(pow) do 38 | if event == val then 39 | name = key 40 | end 41 | end 42 | log.f("caffeinate event %d => %s", event, name) 43 | if event == pow.screensDidUnlock or event == pow.screensaverDidStop then 44 | log.i("Screen awakened!") 45 | -- Restore Caffeinated state: 46 | setCaffeine(shouldCaffeinate) 47 | return 48 | end 49 | if event == pow.screensDidLock or event == pow.screensaverDidStart then 50 | log.i("Screen locked.") 51 | setCaffeine(false) 52 | return 53 | end 54 | end 55 | 56 | -- Listen for power events, callback on_pow(). 57 | pow.new(on_pow):start() 58 | log.i("Started.") 59 | -------------------------------------------------------------------------------- /iftoprc: -------------------------------------------------------------------------------- 1 | dns-resolution: no 2 | show-bars: no 3 | port-display: destination-only 4 | use-bytes: yes 5 | port-resolution: no 6 | -------------------------------------------------------------------------------- /inputrc: -------------------------------------------------------------------------------- 1 | set bind-tty-special-chars off 2 | "\C-u": kill-whole-line 3 | "\C-w": backward-kill-word 4 | 5 | "\e[A": history-search-backward 6 | "\e[B": history-search-forward 7 | set show-all-if-ambiguous on 8 | set completion-query-items 30 9 | # tab completion would add / at the end of a soft link if the target is a dir. 10 | set mark-symlinked-directories on 11 | set bell-style none 12 | -------------------------------------------------------------------------------- /kitty/base16_solarized_dark.color.conf: -------------------------------------------------------------------------------- 1 | # from: https://github.com/kdrag0n/base16-kitty/blob/master/colors/base16-solarized-dark-256.conf 2 | background #002b36 3 | foreground #93a1a1 4 | selection_background #93a1a1 5 | selection_foreground #002b36 6 | url_color #839496 7 | cursor #93a1a1 8 | active_border_color #657b83 9 | inactive_border_color #073642 10 | active_tab_background #002b36 11 | active_tab_foreground #93a1a1 12 | inactive_tab_background #073642 13 | inactive_tab_foreground #839496 14 | tab_bar_background #073642 15 | 16 | # normal 17 | color0 #002b36 18 | color1 #dc322f 19 | color2 #859900 20 | color3 #b58900 21 | color4 #268bd2 22 | color5 #6c71c4 23 | color6 #2aa198 24 | color7 #93a1a1 25 | 26 | # bright 27 | color8 #657b83 28 | color9 #dc322f 29 | color10 #859900 30 | color11 #b58900 31 | color12 #268bd2 32 | color13 #6c71c4 33 | color14 #2aa198 34 | color15 #fdf6e3 35 | 36 | # extended base16 colors 37 | color16 #cb4b16 38 | color17 #d33682 39 | color18 #073642 40 | color19 #586e75 41 | color20 #839496 42 | color21 #eee8d5 43 | -------------------------------------------------------------------------------- /kitty/kitty.conf: -------------------------------------------------------------------------------- 1 | # Look and feel 2 | 3 | ## Color 4 | include ./obsidian.color.conf 5 | 6 | ## Font 7 | font_family FiraCode Nerd Font Mono Retina 8 | font_size 14.0 9 | font_features FiraCodeNerdFontCompleteM-Retina +ss02 +ss03 +ss04 +ss05 +ss07 +zero 10 | 11 | ## cursor 12 | cursor_blink_interval 2.0 13 | cursor_stop_blinking_after 5.0 14 | 15 | ## Scrollback 16 | scrollback_lines -1 17 | 18 | ## Mouse 19 | copy_on_select yes 20 | mouse_hide_wait 2.0 21 | 22 | ## Notification 23 | enable_audio_bell no 24 | visual_bell_duration 0.3 25 | bell_on_tab yes 26 | 27 | ## Tabs 28 | tab_bar_edge top 29 | tab_bar_style powerline 30 | tab_powerline_style angled 31 | active_tab_foreground #111 32 | active_tab_background #eee 33 | active_tab_font_style bold 34 | inactive_tab_foreground #666 35 | inactive_tab_background #888 36 | tab_bar_background #444 37 | inactive_tab_font_style normal 38 | tab_title_template "{fmt.fg.gray}{index}{fmt.fg.default}:{title}" 39 | active_tab_title_template "{title}" 40 | 41 | # Mechanics 42 | input_delay 2 43 | editor nvim 44 | allow_remote_control no 45 | allow_hyperlinks no 46 | term xterm-256color 47 | macos_option_as_alt yes 48 | macos_quit_when_last_window_closed yes 49 | strip_trailing_spaces smart 50 | update_check_interval 72 51 | hide_window_decorations titlebar-only 52 | 53 | # Shortcuts 54 | clear_all_shortcuts yes 55 | 56 | ## The defaults 57 | map cmd+a pipe @text tab pbcopy 58 | map cmd+c copy_to_clipboard 59 | map cmd+v paste_from_clipboard 60 | map ctrl+alt+j scroll_page_up 61 | map ctrl+alt+k scroll_page_down 62 | map cmd+k clear_terminal reset active 63 | 64 | ## Tab management 65 | map cmd+t new_tab_with_cwd !neighbor 66 | map alt+s next_tab 67 | map alt+a previous_tab 68 | map alt+q close_tab 69 | map cmd+s set_tab_title 70 | map cmd+shift+left move_tab_backward 71 | map cmd+shift+right move_tab_forward 72 | map alt+1 goto_tab 1 73 | map alt+2 goto_tab 2 74 | map alt+3 goto_tab 3 75 | map alt+4 goto_tab 4 76 | map alt+5 goto_tab 5 77 | map alt+6 goto_tab 6 78 | map alt+7 goto_tab 7 79 | map alt+8 goto_tab 8 80 | map alt+9 goto_tab 9 81 | 82 | # Font size 83 | map cmd+equal change_font_size current +1.0 84 | map cmd+minus change_font_size current -1.0 85 | map cmd+0 change_font_size current 0 86 | 87 | include ${KITTY_OS}.conf 88 | -------------------------------------------------------------------------------- /kitty/linux.conf: -------------------------------------------------------------------------------- 1 | shell /bin/bash 2 | font_size 10.0 3 | 4 | ## The defaults 5 | map alt+a pipe @text tab pbcopy 6 | map alt+c copy_to_clipboard 7 | map alt+v paste_from_clipboard 8 | map alt+k clear_terminal reset active 9 | 10 | map super+s next_tab 11 | map super+a previous_tab 12 | 13 | ## Tab management 14 | map alt+t new_tab_with_cwd !neighbor 15 | map alt+s set_tab_title 16 | map alt+shift+left move_tab_backward 17 | map alt+shift+right move_tab_forward 18 | 19 | # Font size 20 | map alt+equal change_font_size current +1.0 21 | map alt+minus change_font_size current -1.0 22 | map alt+0 change_font_size current 0 23 | -------------------------------------------------------------------------------- /kitty/macos.conf: -------------------------------------------------------------------------------- 1 | shell /opt/homebrew/bin/bash 2 | -------------------------------------------------------------------------------- /kitty/nightfox.color.conf: -------------------------------------------------------------------------------- 1 | # Nightfox colors for Kitty 2 | ## name: nightfox 3 | ## upstream: nightfox 4 | 5 | background #192330 6 | foreground #cdcecf 7 | selection_background #283648 8 | selection_foreground #cdcecf 9 | url_color #81b29a 10 | cursor #cdcecf 11 | active_border_color #dbc074 12 | inactive_border_color #393b44 13 | 14 | # Tabs 15 | active_tab_background #719cd6 16 | active_tab_foreground #131A24 17 | inactive_tab_background #283648 18 | inactive_tab_foreground #526175 19 | 20 | # normal 21 | color0 #393b44 22 | color1 #c94f6d 23 | color2 #81b29a 24 | color3 #dbc074 25 | color4 #719cd6 26 | color5 #9d79d6 27 | color6 #63cdcf 28 | color7 #f2f2f2 29 | 30 | # bright 31 | color8 #7f8c98 32 | color9 #d6616b 33 | color10 #58cd8b 34 | color11 #ffe37e 35 | color12 #84cee4 36 | color13 #b8a1e3 37 | color14 #59f0ff 38 | color15 #f2f2f2 39 | 40 | # extended colors 41 | color16 #f4a261 42 | color17 #d67ad2 43 | -------------------------------------------------------------------------------- /kitty/obsidian.color.conf: -------------------------------------------------------------------------------- 1 | background #273032 2 | foreground #cccccc 3 | cursor #c0cad0 4 | selection_background #3d4b4e 5 | color0 #000000 6 | color8 #545454 7 | color1 #a50001 8 | color9 #ff0003 9 | color2 #00bb00 10 | color10 #92c763 11 | color3 #fecc22 12 | color11 #fef773 13 | color4 #399bda 14 | color12 #a0d6ff 15 | color5 #bb00bb 16 | color13 #ff55ff 17 | color6 #00bbbb 18 | color14 #55ffff 19 | color7 #bbbbbb 20 | color15 #ffffff 21 | selection_foreground #273032 22 | -------------------------------------------------------------------------------- /linux/autostart/xbindkeys.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xdg-open 2 | [Desktop Entry] 3 | Version=1.0 4 | Type=Application 5 | Name=xbindkeys 6 | Exec=env PATH="/bin:/usr/bin:/home/xiaket/.xiaket/etc/bin" xbindkeys -n -f /home/xiaket/.xbindkeysrc 7 | Categories=System; 8 | -------------------------------------------------------------------------------- /linux/xbindkeysrc: -------------------------------------------------------------------------------- 1 | # numlock is mapped to Alt_R in xmodmap 2 | keystate_numlock = enable 3 | 4 | "xmodmap ./.xmodmaprc" 5 | control + grave 6 | 7 | "hiper.sh dolphin" 8 | mod2 + d 9 | 10 | "hiper.sh kitty" 11 | mod2 + k 12 | 13 | "hiper.sh obsidian" 14 | mod2 + o 15 | 16 | "hiper.sh firefox" 17 | mod2 + x 18 | 19 | "hiper.sh yast" 20 | mod2 + y 21 | 22 | "hiper.sh zeal" 23 | mod2 + z 24 | 25 | "hiper.sh 0" 26 | mod2 + 0 27 | 28 | "hiper.sh 1" 29 | mod2 + 1 30 | 31 | "hiper.sh 2" 32 | mod2 + 2 33 | 34 | "hiper.sh 3" 35 | mod2 + 3 36 | 37 | "hiper.sh 4" 38 | mod2 + 4 39 | 40 | "hiper.sh left" 41 | mod2 + comma 42 | 43 | "hiper.sh right" 44 | mod2 + period 45 | -------------------------------------------------------------------------------- /murmur/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "murmur" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { version = "1.0", features = ["full"] } 8 | reqwest = { version = "0.12", features = ["json", "multipart"] } 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | sha256 = "1.5" 12 | clap = { version = "4.0", features = ["derive"] } 13 | anyhow = "1.0" 14 | dotenvy = "0.15" 15 | env_logger = "0.11" 16 | cpal = "0.15" 17 | hound = "3.5" 18 | crossterm = "0.27" 19 | chrono = { version = "0.4", features = ["serde"] } 20 | 21 | [dev-dependencies] 22 | tokio-test = "0.4" 23 | wiremock = "0.6" 24 | tempfile = "3.0" 25 | -------------------------------------------------------------------------------- /murmur/README.md: -------------------------------------------------------------------------------- 1 | # Murmur 2 | 3 | A command-line tool to transcribe audio files using the OpenAI Whisper API. 4 | 5 | ## Features 6 | 7 | - Transcribe audio files using OpenAI's Whisper API 8 | - Support for language specification 9 | - Automatically handles large audio files by splitting them into chunks 10 | - Intelligently merges transcripts from multiple chunks with overlap detection 11 | - **Caching System**: Automatically caches chunk transcriptions as `.transcript.txt` files to avoid repeating API calls on network failures or retries 12 | - Uses system temporary directory for audio chunks with automatic cleanup 13 | - Includes logging support for debugging (set `RUST_LOG=debug` for detailed output) 14 | 15 | ## Requirements 16 | 17 | - Rust (latest stable version) 18 | - FFmpeg (for processing large audio files) 19 | - OpenAI API key 20 | 21 | ## Installation 22 | 23 | 1. Make sure you have Rust installed. If not, visit [rustup.rs](https://rustup.rs) to install. 24 | 2. Clone this repository 25 | 3. Build the project: 26 | ```bash 27 | cargo build --release 28 | ``` 29 | 4. The executable will be available at `target/release/murmur` 30 | 31 | ## Setup 32 | 33 | 1. Set up your OpenAI API key as an environment variable: 34 | ```bash 35 | export OPENAI_API_KEY=your_api_key_here 36 | ``` 37 | 38 | Alternatively, create a `.env` file in the same directory as the executable with: 39 | ``` 40 | OPENAI_API_KEY=your_api_key_here 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```bash 46 | murmur --input [--language ] 47 | ``` 48 | 49 | ### Arguments: 50 | 51 | - `--input`, `-i`: Path to the audio file (MP3 format) 52 | - `--language`, `-l` (optional): Language code (e.g., "en" for English, "es" for Spanish) 53 | 54 | ### Example: 55 | 56 | ```bash 57 | murmur --input recording.mp3 --language en 58 | ``` 59 | 60 | ## Size Limitations 61 | 62 | - Files up to 25MB (OpenAI's API limit) are processed directly 63 | - Larger files are automatically split into chunks of approximately 23MB each 64 | - **Smart Overlap**: Each chunk includes 10 seconds of overlap with adjacent chunks to prevent word/sentence cutoff issues 65 | - Transcripts from multiple chunks are intelligently merged with automatic duplicate removal 66 | 67 | ## Output 68 | 69 | The transcription will be saved as a text file in the same directory as the input file, with the same name but a `.txt` extension. 70 | 71 | ## Debugging 72 | 73 | For detailed logging, set the `RUST_LOG` environment variable: 74 | 75 | ```bash 76 | RUST_LOG=debug murmur --input recording.mp3 77 | ``` 78 | 79 | ## Caching 80 | - When processing large files, each chunk's transcription is automatically cached as `chunk_XXX.mp3.transcript.txt` 81 | - If processing is interrupted and restarted, cached transcripts will be reused instead of making new API calls 82 | - This saves time and API costs when dealing with network issues or interruptions 83 | 84 | 85 | ## Testing 86 | 87 | Run the test suite with: 88 | 89 | ```bash 90 | cargo test 91 | ``` 92 | 93 | Note: Tests include mock HTTP server tests that require the `wiremock` crate. 94 | 95 | ## License 96 | 97 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /murmur/src/cache.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::path::Path; 3 | use tokio::fs; 4 | 5 | use crate::utils::{self, Config, FileMetadata}; 6 | 7 | /// Cache management for audio chunks and transcripts 8 | pub struct CacheManager { 9 | config: Config, 10 | } 11 | 12 | impl CacheManager { 13 | pub fn new(config: &Config) -> Self { 14 | Self { 15 | config: config.clone(), 16 | } 17 | } 18 | 19 | /// Validate existing cache and cleanup if hash doesn't match 20 | pub async fn validate_and_cleanup_if_needed(&self, current_hash: &str) -> Result<()> { 21 | let metadata_path = self.get_metadata_path(); 22 | 23 | if Path::new(&metadata_path).exists() { 24 | match self.read_existing_metadata(&metadata_path).await { 25 | Ok(existing_metadata) => { 26 | if existing_metadata.original_hash != current_hash { 27 | self.cleanup_all_cached_files().await?; 28 | } 29 | } 30 | Err(_) => { 31 | self.cleanup_all_cached_files().await?; 32 | } 33 | } 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | /// Create metadata file for current processing session 40 | pub async fn create_metadata_file( 41 | &self, 42 | file_path: &Path, 43 | file_size: u64, 44 | file_hash: &str, 45 | chunk_count: usize, 46 | ) -> Result<()> { 47 | let filename = utils::get_filename_or_default(file_path, "unknown_file"); 48 | 49 | let metadata = FileMetadata { 50 | original_filename: filename, 51 | original_size: file_size, 52 | original_hash: file_hash.to_string(), 53 | chunk_count, 54 | creation_time: utils::current_timestamp(), 55 | }; 56 | 57 | let metadata_path = self.get_metadata_path(); 58 | let metadata_json = serde_json::to_string_pretty(&metadata)?; 59 | 60 | // Ensure directory exists 61 | if let Some(parent) = Path::new(&metadata_path).parent() { 62 | fs::create_dir_all(parent).await?; 63 | } 64 | 65 | fs::write(&metadata_path, metadata_json).await?; 66 | 67 | Ok(()) 68 | } 69 | 70 | /// Get cached transcript for a chunk if it exists 71 | pub async fn get_cached_transcript(&self, chunk_path: &str) -> Result> { 72 | let cache_path = format!("{}.transcript.txt", chunk_path); 73 | 74 | if !Path::new(&cache_path).exists() { 75 | return Ok(None); 76 | } 77 | 78 | match fs::read_to_string(&cache_path).await { 79 | Ok(cached_text) if !cached_text.trim().is_empty() => Ok(Some(cached_text)), 80 | _ => Ok(None), 81 | } 82 | } 83 | 84 | /// Save transcript to cache file 85 | pub async fn save_transcript_cache(&self, chunk_path: &str, text: &str) -> Result<()> { 86 | let cache_path = format!("{}.transcript.txt", chunk_path); 87 | 88 | fs::write(&cache_path, text).await 89 | .with_context(|| format!("Failed to save transcript cache to {}", cache_path))?; 90 | 91 | Ok(()) 92 | } 93 | 94 | /// Clean up all temporary files after successful processing 95 | pub async fn cleanup_temp_files(&self) -> Result<()> { 96 | 97 | let segment_dir = self.config.temp_dir_path().to_string_lossy().to_string(); 98 | 99 | if let Ok(mut dir) = fs::read_dir(&segment_dir).await { 100 | while let Ok(Some(entry)) = dir.next_entry().await { 101 | fs::remove_file(entry.path()).await.ok(); 102 | } 103 | } 104 | 105 | // Try to remove the directory itself 106 | fs::remove_dir(&segment_dir).await.ok(); 107 | 108 | Ok(()) 109 | } 110 | 111 | /// Clean up all cached files (used when hash mismatch is detected) 112 | async fn cleanup_all_cached_files(&self) -> Result<()> { 113 | let segment_dir = self.config.temp_dir_path().to_string_lossy().to_string(); 114 | 115 | if !Path::new(&segment_dir).exists() { 116 | return Ok(()); 117 | } 118 | 119 | match fs::read_dir(&segment_dir).await { 120 | Ok(mut dir) => { 121 | while let Ok(Some(entry)) = dir.next_entry().await { 122 | if let Err(_e) = fs::remove_file(entry.path()).await { 123 | // Ignore error - file might already be removed 124 | } 125 | } 126 | 127 | // Try to remove the directory itself 128 | if let Err(_e) = fs::remove_dir(&segment_dir).await { 129 | // Ignore error - directory might not be empty or already removed 130 | } 131 | } 132 | Err(_e) => { 133 | // Ignore error - directory might not exist 134 | } 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | fn get_metadata_path(&self) -> String { 141 | format!("{}/{}", 142 | self.config.temp_dir_path().to_string_lossy(), 143 | self.config.metadata_file) 144 | } 145 | 146 | async fn read_existing_metadata(&self, metadata_path: &str) -> Result { 147 | let metadata_json = fs::read_to_string(metadata_path).await?; 148 | serde_json::from_str(&metadata_json).map_err(Into::into) 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use super::*; 155 | use tempfile::TempDir; 156 | 157 | #[tokio::test] 158 | async fn test_cache_manager_creation() { 159 | let config = Config::default(); 160 | let cache_manager = CacheManager::new(&config); 161 | 162 | assert_eq!(cache_manager.config.temp_dir_name, config.temp_dir_name); 163 | assert_eq!(cache_manager.config.metadata_file, config.metadata_file); 164 | } 165 | 166 | #[tokio::test] 167 | async fn test_get_cached_transcript_nonexistent() { 168 | let config = Config::default(); 169 | let cache_manager = CacheManager::new(&config); 170 | 171 | let result = cache_manager.get_cached_transcript("/nonexistent/chunk.mp3").await; 172 | assert!(result.is_ok()); 173 | assert_eq!(result.unwrap(), None); 174 | } 175 | 176 | #[tokio::test] 177 | async fn test_save_and_get_cached_transcript() { 178 | let temp_dir = TempDir::new().unwrap(); 179 | let chunk_path = temp_dir.path().join("chunk.mp3").to_string_lossy().to_string(); 180 | 181 | let config = Config::default(); 182 | let cache_manager = CacheManager::new(&config); 183 | 184 | let test_content = "Test transcript content"; 185 | 186 | // Save transcript 187 | let save_result = cache_manager.save_transcript_cache(&chunk_path, test_content).await; 188 | assert!(save_result.is_ok()); 189 | 190 | // Get cached transcript 191 | let get_result = cache_manager.get_cached_transcript(&chunk_path).await; 192 | assert!(get_result.is_ok()); 193 | assert_eq!(get_result.unwrap(), Some(test_content.to_string())); 194 | } 195 | 196 | #[tokio::test] 197 | async fn test_create_metadata_file() { 198 | let temp_dir = TempDir::new().unwrap(); 199 | let file_path = temp_dir.path().join("test.mp3"); 200 | 201 | // Create a custom config with temp directory in our test directory 202 | let mut config = Config::default(); 203 | config.temp_dir_name = temp_dir.path().join("cache").to_string_lossy().to_string(); 204 | 205 | let cache_manager = CacheManager::new(&config); 206 | 207 | let result = cache_manager.create_metadata_file( 208 | &file_path, 209 | 1024, 210 | "test_hash", 211 | 5, 212 | ).await; 213 | 214 | assert!(result.is_ok()); 215 | 216 | // Verify metadata file was created 217 | let metadata_path = cache_manager.get_metadata_path(); 218 | assert!(Path::new(&metadata_path).exists()); 219 | 220 | // Verify content 221 | let metadata_content = fs::read_to_string(&metadata_path).await.unwrap(); 222 | let metadata: FileMetadata = serde_json::from_str(&metadata_content).unwrap(); 223 | 224 | assert_eq!(metadata.original_filename, "test.mp3"); 225 | assert_eq!(metadata.original_size, 1024); 226 | assert_eq!(metadata.original_hash, "test_hash"); 227 | assert_eq!(metadata.chunk_count, 5); 228 | } 229 | 230 | #[tokio::test] 231 | async fn test_cleanup_all_cached_files() { 232 | let temp_dir = TempDir::new().unwrap(); 233 | let cache_dir = temp_dir.path().join("test_cache"); 234 | fs::create_dir_all(&cache_dir).await.unwrap(); 235 | 236 | // Create some test files 237 | let test_files = vec!["chunk_001.mp3", "chunk_002.mp3", "chunk_001.mp3.transcript.txt", "metadata.json"]; 238 | for file_name in &test_files { 239 | let file_path = cache_dir.join(file_name); 240 | fs::write(&file_path, "test content").await.unwrap(); 241 | assert!(file_path.exists()); 242 | } 243 | 244 | // Create custom config 245 | let mut config = Config::default(); 246 | config.temp_dir_name = cache_dir.to_string_lossy().to_string(); 247 | 248 | let cache_manager = CacheManager::new(&config); 249 | 250 | // Call cleanup function 251 | let result = cache_manager.cleanup_all_cached_files().await; 252 | assert!(result.is_ok()); 253 | 254 | // Verify files are deleted 255 | for file_name in &test_files { 256 | let file_path = cache_dir.join(file_name); 257 | assert!(!file_path.exists(), "File {} should be deleted", file_path.display()); 258 | } 259 | 260 | // Verify directory is also deleted 261 | assert!(!cache_dir.exists(), "Directory should be deleted"); 262 | } 263 | } -------------------------------------------------------------------------------- /murmur/src/chunking.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | use crate::utils::{self, Config}; 6 | 7 | /// Audio file chunking functionality 8 | pub struct AudioChunker { 9 | config: Config, 10 | } 11 | 12 | impl AudioChunker { 13 | pub fn new(config: &Config) -> Self { 14 | Self { 15 | config: config.clone(), 16 | } 17 | } 18 | 19 | pub async fn split_audio_file(&self, input_path: &Path) -> Result> { 20 | let total_size = utils::get_file_size(input_path).await?; 21 | let segment_dir = self.prepare_segment_directory().await?; 22 | 23 | 24 | let duration = self.get_audio_duration(input_path)?; 25 | let chunks = self.create_chunks(input_path, total_size, duration, &segment_dir)?; 26 | 27 | if chunks.is_empty() { 28 | anyhow::bail!("Failed to create any audio chunks"); 29 | } 30 | 31 | Ok(chunks) 32 | } 33 | 34 | async fn prepare_segment_directory(&self) -> Result { 35 | let segment_dir = self.config.temp_dir_path().to_string_lossy().to_string(); 36 | 37 | if !Path::new(&segment_dir).exists() { 38 | std::fs::create_dir_all(&segment_dir)?; 39 | } 40 | 41 | Ok(segment_dir) 42 | } 43 | 44 | fn get_audio_duration(&self, input_path: &Path) -> Result { 45 | let duration_output = Command::new("ffprobe") 46 | .args([ 47 | "-v", "error", 48 | "-show_entries", "format=duration", 49 | "-of", "default=noprint_wrappers=1:nokey=1", 50 | input_path.to_str().context("Invalid file path encoding")?, 51 | ]) 52 | .output()?; 53 | 54 | if !duration_output.status.success() { 55 | let error = String::from_utf8_lossy(&duration_output.stderr); 56 | anyhow::bail!("FFprobe error: {}", error); 57 | } 58 | 59 | let duration_str = String::from_utf8_lossy(&duration_output.stdout) 60 | .trim() 61 | .to_string(); 62 | 63 | duration_str.parse().map_err(Into::into) 64 | } 65 | 66 | fn create_chunks( 67 | &self, 68 | input_path: &Path, 69 | total_size: u64, 70 | duration: f64, 71 | segment_dir: &str, 72 | ) -> Result> { 73 | let chunk_info = self.calculate_chunk_parameters(total_size, duration); 74 | 75 | 76 | let mut chunks = Vec::new(); 77 | let mut start_time = 0.0; 78 | let mut chunk_index = 0; 79 | 80 | while start_time < duration { 81 | let chunk_path = format!("{}/chunk_{:03}.mp3", segment_dir, chunk_index); 82 | 83 | // Calculate actual start and end times with overlap 84 | let actual_start = if chunk_index == 0 { 85 | // First chunk starts at the beginning 86 | 0.0 87 | } else { 88 | // Subsequent chunks start 10 seconds earlier for overlap 89 | (start_time - self.config.grace_period_seconds as f64).max(0.0) 90 | }; 91 | 92 | let theoretical_end = start_time + chunk_info.seconds_per_chunk; 93 | let actual_end = if theoretical_end >= duration { 94 | // Last chunk ends at the end of the file 95 | duration 96 | } else { 97 | // Add 10 seconds for overlap, but don't exceed file duration 98 | (theoretical_end + self.config.grace_period_seconds as f64).min(duration) 99 | }; 100 | 101 | let chunk_duration = actual_end - actual_start; 102 | 103 | if chunk_duration > 1.0 { 104 | self.create_single_chunk(input_path, &chunk_path, actual_start, chunk_duration)?; 105 | chunks.push(chunk_path); 106 | start_time += chunk_info.seconds_per_chunk; 107 | chunk_index += 1; 108 | } else { 109 | break; 110 | } 111 | } 112 | 113 | Ok(chunks) 114 | } 115 | 116 | fn calculate_chunk_parameters(&self, total_size: u64, duration: f64) -> ChunkInfo { 117 | let bytes_per_second = total_size as f64 / duration; 118 | let target_size_bytes = self.config.chunk_size_bytes() as f64; 119 | let seconds_per_chunk = target_size_bytes / bytes_per_second; 120 | 121 | ChunkInfo { 122 | seconds_per_chunk, 123 | } 124 | } 125 | 126 | fn create_single_chunk( 127 | &self, 128 | input_path: &Path, 129 | chunk_path: &str, 130 | start_time: f64, 131 | chunk_duration: f64, 132 | ) -> Result<()> { 133 | let output = Command::new("ffmpeg") 134 | .args([ 135 | "-y", // Overwrite output files without asking 136 | "-i", input_path.to_str().context("Invalid file path encoding")?, 137 | "-ss", &start_time.to_string(), 138 | "-t", &chunk_duration.to_string(), 139 | "-c:a", "copy", // Just copy audio stream, no re-encoding 140 | "-loglevel", "error", 141 | chunk_path, 142 | ]) 143 | .output()?; 144 | 145 | if !output.status.success() { 146 | let error = String::from_utf8_lossy(&output.stderr); 147 | anyhow::bail!("FFmpeg error when creating chunk: {}", error); 148 | } 149 | 150 | Ok(()) 151 | } 152 | } 153 | 154 | struct ChunkInfo { 155 | seconds_per_chunk: f64, 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | 162 | #[test] 163 | fn test_calculate_chunk_parameters() { 164 | let config = Config::default(); 165 | let chunker = AudioChunker::new(&config); 166 | 167 | // Test with 100MB file, 1000 seconds duration 168 | let total_size = 100 * 1024 * 1024; // 100MB 169 | let duration = 1000.0; // 1000 seconds 170 | 171 | let chunk_info = chunker.calculate_chunk_parameters(total_size, duration); 172 | 173 | assert!(chunk_info.seconds_per_chunk > 0.0); 174 | 175 | // With 100MB file and ~23MB target chunk size, should have reasonable chunk duration 176 | assert!(chunk_info.seconds_per_chunk > 200.0 && chunk_info.seconds_per_chunk < 300.0); 177 | } 178 | 179 | #[test] 180 | fn test_chunker_creation() { 181 | let config = Config::default(); 182 | let chunker = AudioChunker::new(&config); 183 | 184 | assert_eq!(chunker.config.chunk_size_mb, config.chunk_size_mb); 185 | assert_eq!(chunker.config.temp_dir_name, config.temp_dir_name); 186 | } 187 | 188 | #[test] 189 | fn test_overlap_calculation() { 190 | let config = Config::default(); 191 | let chunker = AudioChunker::new(&config); 192 | 193 | // Test with 100MB file, 1000 seconds duration 194 | let total_size = 100 * 1024 * 1024; // 100MB 195 | let duration = 1000.0; // 1000 seconds 196 | 197 | let chunk_info = chunker.calculate_chunk_parameters(total_size, duration); 198 | 199 | // With overlap, chunks should have some buffer time 200 | // Grace period is 10 seconds, so we should see overlap in the logic 201 | assert!(chunk_info.seconds_per_chunk > 0.0); 202 | assert_eq!(config.grace_period_seconds, 10); 203 | } 204 | } -------------------------------------------------------------------------------- /murmur/src/client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use reqwest::multipart::{Form, Part}; 3 | use std::path::Path; 4 | use tokio::fs; 5 | 6 | use crate::utils::{self, Config}; 7 | use crate::Args; 8 | 9 | /// OpenAI Whisper API client 10 | pub struct WhisperClient { 11 | client: reqwest::Client, 12 | api_key: String, 13 | base_url: String, 14 | } 15 | 16 | impl WhisperClient { 17 | pub fn new(api_key: String, config: &Config) -> Result { 18 | let client = reqwest::Client::builder() 19 | .timeout(std::time::Duration::from_secs(config.whisper_timeout_seconds)) 20 | .build()?; 21 | 22 | Ok(Self { 23 | client, 24 | api_key, 25 | base_url: "https://api.openai.com/v1".to_string(), 26 | }) 27 | } 28 | 29 | pub async fn transcribe(&self, args: &Args) -> Result { 30 | let input_path = args.input.as_ref() 31 | .context("Input file path is required for transcription")?; 32 | 33 | // Validate input file exists 34 | if !input_path.exists() { 35 | anyhow::bail!("Input file {:?} does not exist", input_path); 36 | } 37 | 38 | let file_size = utils::get_file_size(input_path).await?; 39 | let file_size_mb = utils::bytes_to_mb(file_size); 40 | 41 | // Check size limit for direct API calls 42 | if file_size_mb > utils::MAX_FILE_SIZE_MB as f64 { 43 | anyhow::bail!( 44 | "File size ({:.2} MB) exceeds {} MB limit", 45 | file_size_mb, 46 | utils::MAX_FILE_SIZE_MB 47 | ); 48 | } 49 | 50 | // Print processing message only for regular files (not chunks or recordings) 51 | if !self.is_chunk_file(input_path) && !self.is_temp_recording(input_path) { 52 | println!("Processing file ({:.1} MB)...", file_size_mb); 53 | } 54 | 55 | // Read file content 56 | let file_bytes = fs::read(input_path) 57 | .await 58 | .context("Failed to read audio file")?; 59 | 60 | // Build multipart form 61 | let file_name = utils::get_filename_or_default(input_path, "audio.mp3"); 62 | let form = self.build_form(&file_name, file_bytes, &args.language)?; 63 | 64 | // Send request 65 | self.send_transcription_request(form).await 66 | } 67 | 68 | fn is_chunk_file(&self, path: &Path) -> bool { 69 | path.to_string_lossy().contains("murmur_audio_chunks") 70 | } 71 | 72 | fn is_temp_recording(&self, path: &Path) -> bool { 73 | path.to_string_lossy().contains("murmur_recording") 74 | } 75 | 76 | fn build_form(&self, file_name: &str, file_bytes: Vec, language: &Option) -> Result
{ 77 | let mut form = Form::new() 78 | .text("model", "whisper-1") 79 | .text("response_format", "text") 80 | .text("temperature", "0"); 81 | 82 | if let Some(lang) = language { 83 | form = form.text("language", lang.clone()); 84 | } 85 | 86 | let file_part = Part::bytes(file_bytes) 87 | .file_name(file_name.to_string()) 88 | .mime_str("audio/mpeg")?; 89 | 90 | form = form.part("file", file_part); 91 | Ok(form) 92 | } 93 | 94 | async fn send_transcription_request(&self, form: Form) -> Result { 95 | let response = self 96 | .client 97 | .post(format!("{}/audio/transcriptions", self.base_url)) 98 | .header("Authorization", format!("Bearer {}", self.api_key)) 99 | .multipart(form) 100 | .send() 101 | .await 102 | .context("Failed to send request")?; 103 | 104 | // Handle response 105 | let status = response.status(); 106 | if !status.is_success() { 107 | let error_text = response.text().await?; 108 | let error_message = match status.as_u16() { 109 | 401 => "Invalid API key. Please check your OPENAI_API_KEY environment variable.".to_string(), 110 | 429 => "Rate limit exceeded. Please wait a moment and try again.".to_string(), 111 | 413 => "File too large for API. This shouldn't happen with proper chunking.".to_string(), 112 | 400 => format!("Bad request: {}", error_text), 113 | 500..=599 => "OpenAI server error. Please try again later.".to_string(), 114 | _ => format!("API error ({}): {}", status, error_text), 115 | }; 116 | anyhow::bail!("{}", error_message); 117 | } 118 | 119 | response.text().await.context("Failed to read response") 120 | } 121 | 122 | pub async fn enhance_text(&self, prompt: &str) -> Result { 123 | let request_body = serde_json::json!({ 124 | "model": "gpt-3.5-turbo", 125 | "messages": [ 126 | { 127 | "role": "user", 128 | "content": prompt 129 | } 130 | ], 131 | "max_tokens": 2000, 132 | "temperature": 0.3 133 | }); 134 | 135 | let response = self 136 | .client 137 | .post(format!("{}/chat/completions", self.base_url)) 138 | .header("Authorization", format!("Bearer {}", self.api_key)) 139 | .header("Content-Type", "application/json") 140 | .json(&request_body) 141 | .send() 142 | .await 143 | .context("Failed to send enhancement request")?; 144 | 145 | let status = response.status(); 146 | if !status.is_success() { 147 | let error_text = response.text().await?; 148 | let error_message = match status.as_u16() { 149 | 401 => "Invalid API key for text enhancement.".to_string(), 150 | 429 => "Rate limit exceeded for text enhancement.".to_string(), 151 | 400 => format!("Bad request for text enhancement: {}", error_text), 152 | 500..=599 => "OpenAI server error during text enhancement.".to_string(), 153 | _ => format!("Enhancement API error ({}): {}", status, error_text), 154 | }; 155 | anyhow::bail!("{}", error_message); 156 | } 157 | 158 | let response_json: serde_json::Value = response.json().await 159 | .context("Failed to parse enhancement response")?; 160 | 161 | let enhanced_text = response_json["choices"][0]["message"]["content"] 162 | .as_str() 163 | .context("Invalid response format from enhancement API")? 164 | .trim() 165 | .to_string(); 166 | 167 | Ok(enhanced_text) 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | use std::io::Write; 175 | use tempfile::NamedTempFile; 176 | use wiremock::matchers::{method, path}; 177 | use wiremock::{Mock, MockServer, ResponseTemplate}; 178 | 179 | #[tokio::test] 180 | async fn test_whisper_client() { 181 | let mock_server = MockServer::start().await; 182 | let response_body = "This is a test transcription."; 183 | 184 | Mock::given(method("POST")) 185 | .and(path("/audio/transcriptions")) 186 | .respond_with(ResponseTemplate::new(200).set_body_string(response_body)) 187 | .mount(&mock_server) 188 | .await; 189 | 190 | let mut temp_file = NamedTempFile::new().unwrap(); 191 | let dummy_data = vec![0u8; 1024]; 192 | temp_file.write_all(&dummy_data).unwrap(); 193 | temp_file.flush().unwrap(); 194 | 195 | let client = WhisperClient { 196 | client: reqwest::Client::new(), 197 | api_key: "test_key".to_string(), 198 | base_url: mock_server.uri(), 199 | }; 200 | 201 | let args = Args { 202 | input: Some(temp_file.path().to_path_buf()), 203 | language: Some("en".to_string()), 204 | }; 205 | 206 | let result = client.transcribe(&args).await; 207 | assert!(result.is_ok(), "Transcribe failed: {:?}", result.err()); 208 | assert_eq!(result.unwrap(), response_body); 209 | } 210 | 211 | #[test] 212 | fn test_is_chunk_file() { 213 | let _config = Config::default(); 214 | let client = WhisperClient { 215 | client: reqwest::Client::new(), 216 | api_key: "test".to_string(), 217 | base_url: "test".to_string(), 218 | }; 219 | 220 | let chunk_path = std::path::Path::new("/tmp/murmur_audio_chunks/chunk_001.mp3"); 221 | let normal_path = std::path::Path::new("/tmp/audio.mp3"); 222 | 223 | assert!(client.is_chunk_file(chunk_path)); 224 | assert!(!client.is_chunk_file(normal_path)); 225 | } 226 | } -------------------------------------------------------------------------------- /murmur/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Murmur - Audio transcription using OpenAI Whisper API 2 | //! 3 | //! This library provides functionality to transcribe audio files using OpenAI's Whisper API, 4 | //! with support for large file chunking and caching. 5 | 6 | use anyhow::Result; 7 | use clap::Parser; 8 | use std::path::PathBuf; 9 | 10 | pub mod cache; 11 | pub mod chunking; 12 | pub mod client; 13 | pub mod transcription; 14 | pub mod utils; 15 | pub mod voice_recorder; 16 | 17 | // Re-export commonly used items 18 | pub use cache::CacheManager; 19 | pub use chunking::AudioChunker; 20 | pub use client::WhisperClient; 21 | pub use transcription::TranscriptMerger; 22 | pub use utils::{Config, FileMetadata}; 23 | pub use voice_recorder::VoiceRecorder; 24 | 25 | /// Command line arguments 26 | #[derive(Parser, Debug, Clone)] 27 | #[command(name = "murmur")] 28 | #[command(about = "Transcribe MP3 audio files using OpenAI Whisper API or enable voice recording mode")] 29 | pub struct Args { 30 | /// Input MP3 file path (optional - if not provided, enters voice recording mode) 31 | #[arg(short, long)] 32 | pub input: Option, 33 | 34 | /// Language code (e.g., 'en' for English, 'es' for Spanish) 35 | #[arg(short, long)] 36 | pub language: Option, 37 | } 38 | 39 | /// Main transcription orchestrator 40 | pub struct MurmurProcessor { 41 | config: Config, 42 | client: WhisperClient, 43 | cache_manager: CacheManager, 44 | chunker: AudioChunker, 45 | merger: TranscriptMerger, 46 | } 47 | 48 | impl MurmurProcessor { 49 | pub fn new(api_key: String) -> Result { 50 | let config = Config::default(); 51 | let client = WhisperClient::new(api_key, &config)?; 52 | let cache_manager = CacheManager::new(&config); 53 | let chunker = AudioChunker::new(&config); 54 | let merger = TranscriptMerger::new(); 55 | 56 | Ok(Self { 57 | config, 58 | client, 59 | cache_manager, 60 | chunker, 61 | merger, 62 | }) 63 | } 64 | 65 | pub async fn process(&self, args: &Args) -> Result { 66 | match &args.input { 67 | Some(input_path) => { 68 | // File mode - process existing audio file 69 | utils::validate_input_file(input_path).await?; 70 | 71 | let file_size = utils::get_file_size(input_path).await?; 72 | 73 | if file_size <= self.config.max_file_size_bytes() { 74 | // Small file - process directly 75 | self.process_small_file(args).await 76 | } else { 77 | // Large file - use chunking strategy 78 | self.process_large_file(args).await 79 | } 80 | } 81 | None => { 82 | // Voice recording mode 83 | self.process_voice_recording(args).await 84 | } 85 | } 86 | } 87 | 88 | async fn process_voice_recording(&self, args: &Args) -> Result { 89 | // Record audio using voice recorder 90 | let audio_file = VoiceRecorder::record_with_spacebar().await?; 91 | 92 | // Create temporary args with the recorded file 93 | let mut temp_args = args.clone(); 94 | temp_args.input = Some(audio_file.clone()); 95 | 96 | // Transcribe the recorded audio 97 | let transcription = self.client.transcribe(&temp_args).await?; 98 | 99 | // Clean up temporary audio file 100 | if audio_file.exists() { 101 | std::fs::remove_file(&audio_file)?; 102 | } 103 | 104 | // Enhance the transcription using OpenAI 105 | self.enhance_transcription(&transcription).await 106 | } 107 | 108 | async fn enhance_transcription(&self, text: &str) -> Result { 109 | let prompt = format!( 110 | "Please improve and format the following transcribed text. Fix any grammar issues, add proper punctuation, and make it more readable while preserving the original meaning. Output only the improved text without any explanations:\n\n{}", 111 | text 112 | ); 113 | 114 | let enhanced_text = self.client.enhance_text(&prompt).await?; 115 | Ok(enhanced_text) 116 | } 117 | 118 | async fn process_small_file(&self, args: &Args) -> Result { 119 | self.client.transcribe(args).await 120 | } 121 | 122 | async fn process_large_file(&self, args: &Args) -> Result { 123 | let file_path = args.input.as_ref().unwrap(); 124 | let file_size_mb = utils::bytes_to_mb(utils::get_file_size(file_path).await?); 125 | 126 | println!("Processing large file ({:.1} MB)...", file_size_mb); 127 | 128 | // Calculate file hash and handle cache validation 129 | let file_hash = utils::calculate_file_hash(file_path).await?; 130 | self.cache_manager.validate_and_cleanup_if_needed(&file_hash).await?; 131 | 132 | // Split the audio file into chunks 133 | let chunks = self.chunker.split_audio_file(file_path).await?; 134 | 135 | // Create metadata file 136 | self.cache_manager.create_metadata_file( 137 | file_path, 138 | utils::get_file_size(file_path).await?, 139 | &file_hash, 140 | chunks.len(), 141 | ).await?; 142 | 143 | // Process each chunk with size info 144 | let mut transcripts = Vec::new(); 145 | for (i, chunk_path) in chunks.iter().enumerate() { 146 | // Get chunk size for display 147 | let chunk_size = utils::get_file_size(std::path::Path::new(chunk_path)).await?; 148 | let chunk_size_mb = utils::bytes_to_mb(chunk_size); 149 | 150 | print!("\r\x1b[KProcessing... {}/{} ({:.1}MB)", i + 1, chunks.len(), chunk_size_mb); 151 | std::io::Write::flush(&mut std::io::stdout()).unwrap(); 152 | 153 | let text = self.process_chunk(args, chunk_path, i).await?; 154 | transcripts.push(text); 155 | } 156 | 157 | print!("\r\x1b[K"); // Clear from cursor to end of line 158 | 159 | // Clean up temporary files 160 | self.cache_manager.cleanup_temp_files().await?; 161 | 162 | // Merge transcripts 163 | Ok(self.merger.merge_transcripts(transcripts)) 164 | } 165 | 166 | async fn process_chunk(&self, args: &Args, chunk_path: &str, chunk_index: usize) -> Result { 167 | // Check cache first 168 | if let Some(cached_text) = self.cache_manager.get_cached_transcript(chunk_path).await? { 169 | return Ok(cached_text); 170 | } 171 | 172 | // Process chunk with API 173 | let mut chunk_args = args.clone(); 174 | chunk_args.input = Some(PathBuf::from(chunk_path)); 175 | 176 | match self.client.transcribe(&chunk_args).await { 177 | Ok(text) => { 178 | // Cache the result 179 | self.cache_manager.save_transcript_cache(chunk_path, &text).await?; 180 | Ok(text) 181 | } 182 | Err(e) => { 183 | println!("\rError processing chunk {}: {}", chunk_index + 1, e); 184 | Err(e) 185 | } 186 | } 187 | } 188 | 189 | pub async fn save_transcription(&self, input_path: &std::path::Path, content: &str) -> Result { 190 | utils::save_transcription(input_path, content).await 191 | } 192 | } -------------------------------------------------------------------------------- /murmur/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use clap::Parser; 3 | 4 | use murmur::{Args, MurmurProcessor}; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | let args = Args::parse(); 9 | 10 | // Initialize logging with default settings 11 | env_logger::init(); 12 | 13 | // Load API key from environment 14 | dotenvy::dotenv().ok(); 15 | let api_key = std::env::var("OPENAI_API_KEY") 16 | .context("OPENAI_API_KEY not found. Set it as an environment variable or in .env file")?; 17 | 18 | // Create processor 19 | let processor = MurmurProcessor::new(api_key)?; 20 | 21 | // Process the audio file or start voice recording 22 | let transcription = processor.process(&args).await?; 23 | 24 | match &args.input { 25 | Some(input_path) => { 26 | // File mode - save to file as before 27 | let output_path = processor.save_transcription(input_path, &transcription).await?; 28 | println!("Processing complete: {:?}", output_path.file_name().unwrap_or_default()); 29 | } 30 | None => { 31 | // Voice recording mode - output to stdout 32 | print!("\r"); 33 | println!("{}", transcription); 34 | } 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | -------------------------------------------------------------------------------- /murmur/src/transcription.rs: -------------------------------------------------------------------------------- 1 | /// Transcript merging functionality with overlap detection 2 | pub struct TranscriptMerger; 3 | 4 | impl TranscriptMerger { 5 | pub fn new() -> Self { 6 | Self 7 | } 8 | 9 | /// Merge transcripts with duplicate removal and automatic overlap detection 10 | pub fn merge_transcripts(&self, transcripts: Vec) -> String { 11 | if transcripts.is_empty() { 12 | return String::new(); 13 | } 14 | 15 | if transcripts.len() == 1 { 16 | return transcripts[0].clone(); 17 | } 18 | 19 | let mut result = transcripts[0].clone(); 20 | 21 | for current in transcripts.iter().skip(1) { 22 | // Skip empty transcripts 23 | if current.is_empty() { 24 | continue; 25 | } 26 | 27 | let overlap_size = self.find_overlap_size(&result, current); 28 | 29 | if overlap_size == 0 { 30 | // No overlap found, just append with a space 31 | result.push(' '); 32 | result.push_str(current); 33 | } else { 34 | // Append only the non-overlapping part of the current transcript 35 | result.push_str(¤t[overlap_size..]); 36 | } 37 | } 38 | 39 | result.trim().to_string() 40 | } 41 | 42 | fn find_overlap_size(&self, previous: &str, current: &str) -> usize { 43 | const MIN_OVERLAP: usize = 10; // Minimum number of characters to consider as overlap 44 | const MAX_OVERLAP: usize = 300; // Maximum number of characters to check for overlap 45 | 46 | let max_check_size = MAX_OVERLAP.min(current.len()).min(previous.len()); 47 | 48 | // Check for different overlap sizes, starting from larger to smaller 49 | for size in (MIN_OVERLAP..=max_check_size).rev() { 50 | if self.has_overlap_at_size(previous, current, size) { 51 | return size; 52 | } 53 | } 54 | 55 | 0 // No overlap found 56 | } 57 | 58 | fn has_overlap_at_size(&self, previous: &str, current: &str, size: usize) -> bool { 59 | let previous_suffix = previous.chars().skip(previous.len() - size).collect::(); 60 | let current_prefix = current.chars().take(size).collect::(); 61 | 62 | previous_suffix == current_prefix 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | fn create_merger() -> TranscriptMerger { 71 | TranscriptMerger::new() 72 | } 73 | 74 | #[test] 75 | fn test_merge_transcripts_empty() { 76 | let merger = create_merger(); 77 | assert_eq!(merger.merge_transcripts(vec![]), String::new()); 78 | } 79 | 80 | #[test] 81 | fn test_merge_transcripts_single() { 82 | let merger = create_merger(); 83 | assert_eq!( 84 | merger.merge_transcripts(vec!["Hello world".to_string()]), 85 | "Hello world" 86 | ); 87 | } 88 | 89 | #[test] 90 | fn test_merge_transcripts_no_overlap() { 91 | let merger = create_merger(); 92 | let transcripts = vec!["Hello".to_string(), "world".to_string()]; 93 | assert_eq!(merger.merge_transcripts(transcripts), "Hello world"); 94 | } 95 | 96 | #[test] 97 | fn test_merge_transcripts_with_overlap() { 98 | let merger = create_merger(); 99 | let transcripts = vec!["Hello world".to_string(), "world and universe".to_string()]; 100 | let result = merger.merge_transcripts(transcripts); 101 | assert_eq!(result, "Hello world world and universe"); 102 | } 103 | 104 | #[test] 105 | fn test_merge_transcripts_with_space_overlap() { 106 | let merger = create_merger(); 107 | let transcripts = vec!["Hello world ".to_string(), "world and universe".to_string()]; 108 | let result = merger.merge_transcripts(transcripts); 109 | assert_eq!(result, "Hello world world and universe"); 110 | } 111 | 112 | #[test] 113 | fn test_merge_transcripts_short_strings() { 114 | let merger = create_merger(); 115 | let transcripts = vec!["Hi".to_string(), "there".to_string()]; 116 | assert_eq!(merger.merge_transcripts(transcripts), "Hi there"); 117 | } 118 | 119 | #[test] 120 | fn test_merge_transcripts_identical() { 121 | let merger = create_merger(); 122 | let transcripts = vec!["Same text here".to_string(), "Same text here".to_string()]; 123 | assert_eq!(merger.merge_transcripts(transcripts), "Same text here"); 124 | } 125 | 126 | #[test] 127 | fn test_merge_transcripts_with_empty() { 128 | let merger = create_merger(); 129 | let transcripts = vec![ 130 | "Hello world".to_string(), 131 | "".to_string(), 132 | "goodbye".to_string(), 133 | ]; 134 | assert_eq!(merger.merge_transcripts(transcripts), "Hello world goodbye"); 135 | } 136 | 137 | #[test] 138 | fn test_merge_transcripts_large_overlap() { 139 | let merger = create_merger(); 140 | let long_text = "This is a very long sentence that should be detected as overlap when it appears at the end of one transcript and the beginning of another"; 141 | let transcripts = vec![ 142 | format!("Start of first transcript. {}", long_text), 143 | format!("{} End of second transcript.", long_text), 144 | ]; 145 | let result = merger.merge_transcripts(transcripts); 146 | assert_eq!( 147 | result, 148 | format!( 149 | "Start of first transcript. {} End of second transcript.", 150 | long_text 151 | ) 152 | ); 153 | } 154 | 155 | #[test] 156 | fn test_find_overlap_size() { 157 | let merger = create_merger(); 158 | 159 | // Test with exact overlap (longer than minimum) 160 | let overlap = merger.find_overlap_size("Hello world test", "world test and universe"); 161 | assert_eq!(overlap, 10); // "world test" 162 | 163 | // Test with no overlap 164 | let overlap = merger.find_overlap_size("Hello", "there"); 165 | assert_eq!(overlap, 0); 166 | 167 | // Test with short strings (below minimum overlap) 168 | let overlap = merger.find_overlap_size("Hi", "i there"); 169 | assert_eq!(overlap, 0); // Below minimum overlap threshold 170 | 171 | // Test with short overlap (below minimum) 172 | let overlap = merger.find_overlap_size("Hello world", "world test"); 173 | assert_eq!(overlap, 0); // "world" is only 5 chars, below minimum of 10 174 | } 175 | 176 | #[test] 177 | fn test_has_overlap_at_size() { 178 | let merger = create_merger(); 179 | 180 | // Test exact match 181 | assert!(merger.has_overlap_at_size("Hello world", "world test", 5)); 182 | 183 | // Test no match 184 | assert!(!merger.has_overlap_at_size("Hello world", "test world", 5)); 185 | 186 | // Test different size 187 | assert!(merger.has_overlap_at_size("Hello world", "orld test", 4)); 188 | } 189 | } -------------------------------------------------------------------------------- /murmur/src/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::{Path, PathBuf}; 4 | use tokio::fs; 5 | 6 | // Constants 7 | pub const WHISPER_TIMEOUT_SECONDS: u64 = 600; 8 | pub const MAX_FILE_SIZE_MB: u64 = 25; 9 | pub const CHUNK_SIZE_MB: u64 = 23; 10 | pub const GRACE_PERIOD_SECONDS: u64 = 10; 11 | pub const TEMP_DIR_NAME: &str = "murmur_audio_chunks"; 12 | pub const METADATA_FILE: &str = "metadata.json"; 13 | 14 | /// Configuration structure to centralize all constants and settings 15 | #[derive(Debug, Clone)] 16 | pub struct Config { 17 | pub whisper_timeout_seconds: u64, 18 | pub max_file_size_mb: u64, 19 | pub chunk_size_mb: u64, 20 | pub grace_period_seconds: u64, 21 | pub temp_dir_name: String, 22 | pub metadata_file: String, 23 | } 24 | 25 | impl Default for Config { 26 | fn default() -> Self { 27 | Self { 28 | whisper_timeout_seconds: WHISPER_TIMEOUT_SECONDS, 29 | max_file_size_mb: MAX_FILE_SIZE_MB, 30 | chunk_size_mb: CHUNK_SIZE_MB, 31 | grace_period_seconds: GRACE_PERIOD_SECONDS, 32 | temp_dir_name: TEMP_DIR_NAME.to_string(), 33 | metadata_file: METADATA_FILE.to_string(), 34 | } 35 | } 36 | } 37 | 38 | impl Config { 39 | pub fn max_file_size_bytes(&self) -> u64 { 40 | self.max_file_size_mb * 1024 * 1024 41 | } 42 | 43 | pub fn chunk_size_bytes(&self) -> u64 { 44 | self.chunk_size_mb * 1024 * 1024 45 | } 46 | 47 | pub fn temp_dir_path(&self) -> PathBuf { 48 | std::env::temp_dir().join(&self.temp_dir_name) 49 | } 50 | } 51 | 52 | /// File metadata structure for caching 53 | #[derive(Serialize, Deserialize, Debug)] 54 | pub struct FileMetadata { 55 | pub original_filename: String, 56 | pub original_size: u64, 57 | pub original_hash: String, 58 | pub chunk_count: usize, 59 | pub creation_time: u64, 60 | } 61 | 62 | /// Calculate file hash using SHA256 63 | pub async fn calculate_file_hash(file_path: &Path) -> Result { 64 | sha256::try_digest(file_path).map_err(Into::into) 65 | } 66 | 67 | /// Get file size in bytes 68 | pub async fn get_file_size(file_path: &Path) -> Result { 69 | let metadata = fs::metadata(file_path) 70 | .await 71 | .context("Failed to read file metadata")?; 72 | Ok(metadata.len()) 73 | } 74 | 75 | /// Convert bytes to megabytes 76 | pub fn bytes_to_mb(bytes: u64) -> f64 { 77 | bytes as f64 / (1024.0 * 1024.0) 78 | } 79 | 80 | /// Validate that input file exists, is readable, and is an MP3 file 81 | pub async fn validate_input_file(file_path: &Path) -> Result<()> { 82 | if !file_path.exists() { 83 | anyhow::bail!("Input file {:?} does not exist", file_path); 84 | } 85 | 86 | // Check file extension 87 | match file_path.extension().and_then(|ext| ext.to_str()) { 88 | Some(ext) if ext.to_lowercase() == "mp3" => {}, 89 | Some(ext) => anyhow::bail!("Unsupported file format: .{}. Only MP3 files are supported.", ext), 90 | None => anyhow::bail!("File has no extension. Only MP3 files are supported."), 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | /// Save transcription to file 97 | pub async fn save_transcription(input_path: &Path, content: &str) -> Result { 98 | let output_path = input_path.with_extension("txt"); 99 | tokio::fs::write(&output_path, content) 100 | .await 101 | .context("Failed to write output file")?; 102 | Ok(output_path) 103 | } 104 | 105 | /// Extract filename from path with fallback 106 | pub fn get_filename_or_default(file_path: &Path, default: &str) -> String { 107 | file_path 108 | .file_name() 109 | .and_then(|n| n.to_str()) 110 | .unwrap_or(default) 111 | .to_string() 112 | } 113 | 114 | /// Get current timestamp in seconds since epoch 115 | pub fn current_timestamp() -> u64 { 116 | std::time::SystemTime::now() 117 | .duration_since(std::time::UNIX_EPOCH) 118 | .unwrap_or_default() 119 | .as_secs() 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | use std::io::Write; 126 | use tempfile::NamedTempFile; 127 | 128 | #[tokio::test] 129 | async fn test_calculate_file_hash() { 130 | let mut temp_file = NamedTempFile::new().unwrap(); 131 | let test_content = b"Hello, world!"; 132 | temp_file.write_all(test_content).unwrap(); 133 | temp_file.flush().unwrap(); 134 | 135 | let result = calculate_file_hash(temp_file.path()).await; 136 | assert!(result.is_ok()); 137 | 138 | // SHA256 of "Hello, world!" should be consistent 139 | let expected_hash = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"; 140 | assert_eq!(result.unwrap(), expected_hash); 141 | } 142 | 143 | #[tokio::test] 144 | async fn test_get_file_size() { 145 | let mut temp_file = NamedTempFile::new().unwrap(); 146 | let test_content = b"Hello, world!"; 147 | temp_file.write_all(test_content).unwrap(); 148 | temp_file.flush().unwrap(); 149 | 150 | let result = get_file_size(temp_file.path()).await; 151 | assert!(result.is_ok()); 152 | assert_eq!(result.unwrap(), test_content.len() as u64); 153 | } 154 | 155 | #[test] 156 | fn test_bytes_to_mb() { 157 | assert_eq!(bytes_to_mb(1024 * 1024), 1.0); 158 | assert_eq!(bytes_to_mb(2 * 1024 * 1024), 2.0); 159 | assert_eq!(bytes_to_mb(1536 * 1024), 1.5); 160 | } 161 | 162 | #[test] 163 | fn test_config_default() { 164 | let config = Config::default(); 165 | assert_eq!(config.max_file_size_bytes(), 25 * 1024 * 1024); 166 | assert_eq!(config.chunk_size_bytes(), 23 * 1024 * 1024); 167 | } 168 | 169 | #[tokio::test] 170 | async fn test_save_transcription() { 171 | let temp_file = NamedTempFile::new().unwrap(); 172 | let path = temp_file.path().to_path_buf(); 173 | 174 | let content = "Test transcription content"; 175 | let result = save_transcription(&path, content).await; 176 | 177 | assert!(result.is_ok()); 178 | let output_path = result.unwrap(); 179 | assert_eq!(output_path.extension().unwrap(), "txt"); 180 | 181 | let saved_content = tokio::fs::read_to_string(&output_path).await.unwrap(); 182 | assert_eq!(saved_content, content); 183 | 184 | tokio::fs::remove_file(output_path).await.ok(); 185 | } 186 | } -------------------------------------------------------------------------------- /murmur/src/voice_recorder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 3 | use cpal::{Device, Stream, StreamConfig}; 4 | use crossterm::{ 5 | event::{self, Event, KeyCode, KeyEvent}, 6 | terminal::{disable_raw_mode, enable_raw_mode}, 7 | }; 8 | use hound::{WavSpec, WavWriter}; 9 | use std::fs::File; 10 | use std::io::{BufWriter, Write}; 11 | use std::path::PathBuf; 12 | use std::sync::{Arc, Mutex}; 13 | use std::thread; 14 | use std::time::Duration; 15 | 16 | pub struct VoiceRecorder { 17 | device: Device, 18 | config: StreamConfig, 19 | } 20 | 21 | impl VoiceRecorder { 22 | pub fn new() -> Result { 23 | let host = cpal::default_host(); 24 | let device = host 25 | .default_input_device() 26 | .context("No input device available. Please check microphone permissions.")?; 27 | 28 | let config = device.default_input_config() 29 | .context("Failed to get default input config")? 30 | .into(); 31 | 32 | Ok(Self { device, config }) 33 | } 34 | 35 | pub async fn record_with_spacebar() -> Result { 36 | println!("Press and hold SPACE to record..."); 37 | 38 | let recorder = Self::new()?; 39 | let temp_dir = std::env::temp_dir(); 40 | let audio_file = temp_dir.join("murmur_recording.wav"); 41 | 42 | enable_raw_mode()?; 43 | 44 | let mut recording = false; 45 | let mut stream: Option = None; 46 | let audio_data: Arc>> = Arc::new(Mutex::new(Vec::new())); 47 | 48 | loop { 49 | if event::poll(Duration::from_millis(50))? { 50 | if let Event::Key(KeyEvent { code, .. }) = event::read()? { 51 | match code { 52 | KeyCode::Char(' ') if !recording => { 53 | print!("\r\x1b[2K\x1b[1G\x1b[0mRecording..."); 54 | std::io::stdout().flush().unwrap(); 55 | recording = true; 56 | 57 | let data_clone = Arc::clone(&audio_data); 58 | data_clone.lock().unwrap().clear(); 59 | 60 | let stream_result = recorder.start_recording(data_clone)?; 61 | stream = Some(stream_result); 62 | 63 | while event::poll(Duration::from_millis(1))? { 64 | if let Event::Key(KeyEvent { code: KeyCode::Char(' '), .. }) = event::read()? { 65 | } else { 66 | break; 67 | } 68 | } 69 | } 70 | _ => {} 71 | } 72 | } 73 | } else if recording { 74 | thread::sleep(Duration::from_millis(100)); 75 | 76 | // Double-check that space key is actually released 77 | if !event::poll(Duration::from_millis(10))? { 78 | print!("\r\x1b[2K\x1b[0G"); 79 | std::io::stdout().flush().unwrap(); 80 | 81 | if let Some(s) = stream.take() { 82 | drop(s); 83 | } 84 | 85 | let data = audio_data.lock().unwrap(); 86 | let result = if !data.is_empty() { 87 | Self::save_audio_data(&data, &audio_file, &recorder.config) 88 | .context("Failed to save audio data") 89 | .map(|_| audio_file) 90 | } else { 91 | Err(anyhow::anyhow!("No audio data recorded")) 92 | }; 93 | 94 | // Always cleanup terminal state 95 | disable_raw_mode()?; 96 | return result; 97 | } 98 | } 99 | 100 | thread::sleep(Duration::from_millis(10)); 101 | } 102 | } 103 | 104 | fn start_recording(&self, audio_data: Arc>>) -> Result { 105 | let stream = self.device.build_input_stream( 106 | &self.config, 107 | move |data: &[f32], _: &cpal::InputCallbackInfo| { 108 | if let Ok(mut buffer) = audio_data.lock() { 109 | buffer.extend_from_slice(data); 110 | } 111 | }, 112 | |err| eprintln!("Audio stream error: {}", err), 113 | None, 114 | ).context("Failed to build input stream")?; 115 | 116 | stream.play().context("Failed to start audio stream")?; 117 | Ok(stream) 118 | } 119 | 120 | fn save_audio_data(data: &[f32], path: &PathBuf, config: &StreamConfig) -> Result<()> { 121 | let spec = WavSpec { 122 | channels: config.channels, 123 | sample_rate: config.sample_rate.0, 124 | bits_per_sample: 32, 125 | sample_format: hound::SampleFormat::Float, 126 | }; 127 | 128 | let file = File::create(path)?; 129 | let mut writer = WavWriter::new(BufWriter::new(file), spec)?; 130 | 131 | for &sample in data { 132 | writer.write_sample(sample)?; 133 | } 134 | 135 | writer.finalize()?; 136 | Ok(()) 137 | } 138 | } -------------------------------------------------------------------------------- /nvim/.manage: -------------------------------------------------------------------------------- 1 | benchmark () { 2 | tmpfile=/tmp/nvim-startup.log 3 | rm -f "$tmpfile" && nvim --startuptime "$tmpfile" -c exit && cat "$tmpfile" 4 | } 5 | export -f benchmark 6 | -------------------------------------------------------------------------------- /nvim/after/ftplugin/go.lua: -------------------------------------------------------------------------------- 1 | local opt = vim.opt_local 2 | 3 | opt.list = false 4 | opt.expandtab = false 5 | opt.tabstop = 4 6 | opt.shiftwidth = 4 7 | opt.softtabstop = 4 8 | opt.textwidth = 99 9 | -------------------------------------------------------------------------------- /nvim/after/ftplugin/make.lua: -------------------------------------------------------------------------------- 1 | local opt = vim.opt_local 2 | 3 | opt.expandtab = false 4 | opt.shiftwidth = 8 5 | -------------------------------------------------------------------------------- /nvim/after/ftplugin/python.lua: -------------------------------------------------------------------------------- 1 | -- local opt = require('options').opt 2 | local opt = vim.opt_local 3 | 4 | opt.autoindent = true 5 | opt.expandtab = true 6 | opt.fileformat = "unix" 7 | opt.tabstop = 4 8 | opt.softtabstop = 4 9 | opt.shiftwidth = 4 10 | opt.textwidth = 99 11 | --opt.formatoptions = opt.formatoptions .. "t" 12 | -------------------------------------------------------------------------------- /nvim/init.lua: -------------------------------------------------------------------------------- 1 | local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" 2 | 3 | if not vim.loop.fs_stat(lazypath) then 4 | vim.fn.system({ 5 | "git", 6 | "clone", 7 | "--filter=blob:none", 8 | "--single-branch", 9 | "https://github.com/folke/lazy.nvim.git", 10 | lazypath, 11 | }) 12 | end 13 | vim.opt.runtimepath:prepend(lazypath) 14 | 15 | vim.g.mapleader = " " 16 | 17 | require("lazy").setup("packages", { 18 | defaults = { lazy = true }, 19 | install = { colorscheme = { "nightfox" } }, 20 | change_detection = { 21 | notify = false, 22 | }, 23 | performance = { 24 | rtp = { 25 | disabled_plugins = { 26 | "gzip", 27 | "man", 28 | "matchit", 29 | "matchparen", 30 | "netrwPlugin", 31 | "osc52", 32 | "rplugin", 33 | "shada", 34 | "spellfile", 35 | "tarPlugin", 36 | "tohtml", 37 | "tutor", 38 | "zipPlugin", 39 | }, 40 | }, 41 | }, 42 | }) 43 | 44 | local function requiref(module) 45 | require(module) 46 | end 47 | 48 | if pcall(requiref, "nightfox") then 49 | vim.cmd("colorscheme nightfox") 50 | end 51 | 52 | require("flags") 53 | require("keymaps") 54 | require("line-number") 55 | require("auto-header") 56 | -------------------------------------------------------------------------------- /nvim/lazy-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "CopilotChat.nvim": { "branch": "main", "commit": "fc1282ca7d260a340c6a5b5062f13ef575cfcd63" }, 3 | "Dockerfile.vim": { "branch": "master", "commit": "2a31e6bcea5977209c05c728c4253d82fd873c82" }, 4 | "FTerm.nvim": { "branch": "master", "commit": "d1320892cc2ebab472935242d9d992a2c9570180" }, 5 | "blink-cmp-dictionary": { "branch": "master", "commit": "3d49f934059fb59f657b547d90a526d00540634e" }, 6 | "blink-copilot": { "branch": "main", "commit": "08ed48deb89a6b39fdb46fb7e6aee426ae174c09" }, 7 | "blink.cmp": { "branch": "main", "commit": "dcda20d3aa345025699a920c45b0a0603551f41d" }, 8 | "conform.nvim": { "branch": "master", "commit": "a6f5bdb78caa305496357d17e962bbc4c0b392e2" }, 9 | "copilot.lua": { "branch": "master", "commit": "30321e33b03cb924fdcd6a806a0dc6fa0b0eafb9" }, 10 | "friendly-snippets": { "branch": "main", "commit": "efff286dd74c22f731cdec26a70b46e5b203c619" }, 11 | "gitsigns.nvim": { "branch": "main", "commit": "4c40357994f386e72be92a46f41fc1664c84c87d" }, 12 | "goplements.nvim": { "branch": "main", "commit": "a3291c4db092d5bc95ec0e6e2bcfc14587d06dfc" }, 13 | "lazy.nvim": { "branch": "main", "commit": "6c3bda4aca61a13a9c63f1c1d1b16b9d3be90d7a" }, 14 | "mason-lspconfig.nvim": { "branch": "main", "commit": "1a31f824b9cd5bc6f342fc29e9a53b60d74af245" }, 15 | "mason.nvim": { "branch": "main", "commit": "fc98833b6da5de5a9c5b1446ac541577059555be" }, 16 | "matchparen.nvim": { "branch": "main", "commit": "190861577d83167021dcb3339dd3aa594279f2f3" }, 17 | "mini.nvim": { "branch": "main", "commit": "307221b90376b68fc7e007e9315836370e9efb8f" }, 18 | "nightfox.nvim": { "branch": "main", "commit": "ba47d4b4c5ec308718641ba7402c143836f35aa9" }, 19 | "nvim-lspconfig": { "branch": "master", "commit": "c646154d6e4db9b2979eeb517d0b817ad00c9c47" }, 20 | "nvim-treesitter": { "branch": "master", "commit": "186f35e1684c241baf13a3e6092eee00ac48631e" }, 21 | "nvim-web-devicons": { "branch": "master", "commit": "ab4cfee554e501f497bce0856788d43cf2eb93d7" }, 22 | "plenary.nvim": { "branch": "master", "commit": "857c5ac632080dba10aae49dba902ce3abf91b35" }, 23 | "stabilize.nvim": { "branch": "master", "commit": "eeb1873daffaba67246188a5668b366e45ed1de1" }, 24 | "telescope-fzf-native.nvim": { "branch": "main", "commit": "2a5ceff981501cff8f46871d5402cd3378a8ab6a" }, 25 | "telescope.nvim": { "branch": "master", "commit": "814f102cd1da3dc78c7d2f20f2ef3ed3cdf0e6e4" }, 26 | "trouble.nvim": { "branch": "main", "commit": "85bedb7eb7fa331a2ccbecb9202d8abba64d37b3" }, 27 | "vim-gfm-syntax": { "branch": "master", "commit": "95ec295ccc803afc925c01e6efe328779e1261e9" }, 28 | "vim-go": { "branch": "master", "commit": "6adc82bfef7f9a4b0db78065ae51b8ebb145c355" }, 29 | "vim-python-pep8-indent": { "branch": "master", "commit": "60ba5e11a61618c0344e2db190210145083c91f8" }, 30 | "vim-terraform": { "branch": "master", "commit": "8912ca1be3025a1c9fab193618f3b99517e01973" }, 31 | "w.nvim": { "branch": "main", "commit": "75ad9302b4f1ec37771311e426d38604641220e4" }, 32 | "which-key.nvim": { "branch": "main", "commit": "370ec46f710e058c9c1646273e6b225acf47cbed" } 33 | } 34 | -------------------------------------------------------------------------------- /nvim/lua/auto-header.lua: -------------------------------------------------------------------------------- 1 | Headers = { 2 | py = { "#!/usr/bin/env python3", "", "" }, 3 | sh = { "#!/bin/bash", "", "set -o errexit", "set -o nounset", "set -o pipefail", "", "" }, 4 | } 5 | 6 | local function add_header() 7 | local filetype = vim.fn.expand("%:e") 8 | if Headers[filetype] ~= nil then 9 | vim.fn.setline(".", Headers[filetype]) 10 | end 11 | end 12 | 13 | local _group = vim.api.nvim_create_augroup("AutoHeader", { clear = true }) 14 | vim.api.nvim_create_autocmd( 15 | "BufNewFile", 16 | { pattern = { "*.sh", "*.py" }, callback = add_header, once = true, group = _group } 17 | ) 18 | -------------------------------------------------------------------------------- /nvim/lua/flags.lua: -------------------------------------------------------------------------------- 1 | -- vars 2 | local opt = vim.o 3 | local global = vim.g 4 | local tabsize = 2 5 | 6 | opt.shadafile = "NONE" 7 | global.loaded_netrwPlugin = 1 8 | global.loaded_matchparen = 1 9 | global.loaded_matchit = 1 10 | global.loaded_tarPlugin = 1 11 | global.loaded_zipPlugin = 1 12 | global.loaded_gzip = 1 13 | global.loaded_remote_plugins = 1 14 | global.loaded_man = 1 15 | global.loaded_2html_plugin = 1 16 | global.loaded_shada_plugin = 1 17 | global.loaded_spellfile_plugin = 1 18 | 19 | -- session management 20 | opt.sessionoptions = "buffers,curdir,folds,tabpages,winpos,terminal" 21 | 22 | -- generic options 23 | global.fileencodings = "utf-8,gbk,ucs-bom,cp936" -- try these encodings in order 24 | global.fileformats = "unix,dos,mac" -- try these formats in order 25 | opt.fixendofline = false -- Do not change eol setting of the current file 26 | opt.lazyredraw = true -- Do not redraw while running macros (much faster) 27 | opt.showmatch = true -- Search related. 28 | opt.scrolloff = 3 -- Number of lines to keep above/below the cursor 29 | opt.report = 0 -- always report number of lines affected 30 | opt.backup = true -- enable backup 31 | opt.backupdir = os.getenv("HOME") .. "/.vim/backup" -- save backups to this directory 32 | opt.swapfile = false -- no swap file, no 33 | opt.viewoptions = "cursor,folds,slash,unix" -- things saved in the view 34 | opt.infercase = true -- case sensible when doing completion 35 | opt.mouse = "i" -- enable mouse only in insert mode 36 | opt.spell = true -- enable spell checker 37 | 38 | -- Tab and indent 39 | opt.expandtab = true 40 | opt.smartindent = true 41 | opt.tabstop = tabsize 42 | opt.softtabstop = tabsize 43 | opt.shiftwidth = tabsize 44 | 45 | -- Configure folding 46 | opt.foldmethod = "indent" 47 | opt.foldlevel = 99 48 | 49 | -- UI enhancements 50 | opt.pumblend = 10 -- Make builtin completion menus slightly transparent 51 | opt.pumheight = 10 -- Make popup menu smaller 52 | opt.winblend = 10 -- Make floating windows slightly transparent 53 | 54 | -- save undo history 55 | opt.undodir = os.getenv("HOME") .. "/.vim/undo" 56 | opt.undofile = true 57 | 58 | -- avante 59 | opt.laststatus = 3 60 | -------------------------------------------------------------------------------- /nvim/lua/keymaps.lua: -------------------------------------------------------------------------------- 1 | --------------------- 2 | -- Control family 3 | --------------------- 4 | -- Ctrl E to clean up trailing whitespaces 5 | vim.keymap.set("n", "", ":%s/\\s*$//g", { noremap = true }) 6 | -- Ctrl V to paste in 7 | vim.keymap.set("n", "", '"*p', { noremap = true }) 8 | -- Use Ctrl-L to toggle the line number display. 9 | vim.keymap.set("n", "", ":lua Toggleln()", { noremap = true, silent = true }) 10 | -- get out of term 11 | vim.keymap.set("t", "", "", { noremap = true }) 12 | vim.keymap.set("n", ",", " ", { remap = true }) 13 | 14 | --------------------- 15 | -- leader family 16 | --------------------- 17 | local leader_config = { 18 | ["a"] = { cmd = ":WPrevBuffer", desc = "previous buffer" }, 19 | ["c"] = { 20 | cmd = ":ConformFormatToggle", 21 | desc = "Toggle autoformat-on-save for the current buffer", 22 | }, 23 | -- ["d"] used down below in LSP keymaps. 24 | ["e"] = { 25 | cmd = 'lua require("trouble").toggle("diagnostics")', 26 | desc = "toggle errors", 27 | }, 28 | ["f"] = { cmd = ":Telescope find_files", opts = { silent = false }, desc = "find files" }, 29 | ["g"] = { cmd = ":Telescope live_grep", opts = { silent = false }, desc = "grep from files" }, 30 | ["h"] = { cmd = ":WSplitLeft", desc = "create split on left" }, 31 | ["j"] = { cmd = ":WSplitDown", desc = "create split below" }, 32 | ["k"] = { cmd = ":WSplitUp", desc = "create split above" }, 33 | ["l"] = { cmd = ":WSplitRight", desc = "create split on right" }, 34 | ["o"] = { cmd = ":WToggleExplorer", desc = "Toggle file window" }, 35 | ["s"] = { cmd = ":WNextBuffer", desc = "next buffer" }, 36 | ["t"] = { 37 | { cmd = 'lua require("FTerm").toggle()', mode = "n", desc = "toggle terminal" }, 38 | { 39 | cmd = 'lua require("FTerm").toggle()', 40 | mode = "t", 41 | desc = "toggle terminal", 42 | }, 43 | }, 44 | ["v"] = { cmd = ":CopilotChatToggle", desc = "Toggle Copilot Chat" }, 45 | ["z"] = { cmd = "za", desc = "toggle fold" }, 46 | } 47 | 48 | -- LSP shortcuts has + d as prefix. 49 | local lsp_keymap_config = { 50 | ["a"] = { 51 | desc = "See available code actions", 52 | cmd = vim.lsp.buf.code_action, 53 | }, 54 | ["d"] = { desc = "Show LSP definitions", cmd = "Telescope lsp_definitions" }, 55 | ["D"] = { 56 | desc = "Show buffer diagnostics", 57 | cmd = "Telescope diagnostics bufnr=0", 58 | }, 59 | ["e"] = { desc = "Show line diagnostics", cmd = vim.diagnostic.open_float }, 60 | ["g"] = { desc = "Go to declaration", cmd = vim.lsp.buf.declaration }, 61 | ["i"] = { 62 | desc = "Show LSP implementations", 63 | cmd = "Telescope lsp_implementations", 64 | }, 65 | ["m"] = { 66 | desc = "Show documentation for what is under cursor", 67 | cmd = vim.lsp.buf.hover, 68 | }, 69 | ["r"] = { desc = "Smart rename", cmd = vim.lsp.buf.rename }, 70 | ["R"] = { 71 | desc = "Show LSP references", 72 | cmd = "Telescope lsp_references", 73 | }, 74 | ["t"] = { 75 | desc = "Show LSP type definitions", 76 | cmd = "Telescope lsp_type_definitions", 77 | }, 78 | ["["] = { desc = "Go to previous diagnostic", cmd = vim.diagnostic.goto_prev }, 79 | ["]"] = { desc = "Go to next diagnostic", cmd = vim.diagnostic.goto_next }, 80 | } 81 | 82 | local function set_keymap(key, cmd, opts, mode, desc) 83 | local options = { noremap = true, silent = true, desc = desc } 84 | for k, v in pairs(opts or {}) do 85 | options[k] = v 86 | end 87 | vim.keymap.set(mode or "n", "" .. key, cmd, options) 88 | end 89 | 90 | for key, configs in pairs(leader_config) do 91 | if type(configs[1]) == "table" then 92 | for _, config in ipairs(configs) do 93 | set_keymap(key, config.cmd, config.opts, config.mode, config.desc) 94 | end 95 | else 96 | set_keymap(key, configs.cmd, configs.opts, configs.mode, configs.desc) 97 | end 98 | end 99 | 100 | for key, config in pairs(lsp_keymap_config) do 101 | set_keymap("d" .. key, config.cmd, config.opts, "n", config.desc) 102 | end 103 | --------------------- 104 | -- misc 105 | --------------------- 106 | 107 | -- Fix my typos, also Q maps to qa 108 | vim.keymap.set("c", "Q!", "qa!", { noremap = true, expr = false, silent = false }) 109 | vim.keymap.set("c", "WQ", "wqa", { noremap = true, expr = false, silent = false }) 110 | vim.keymap.set("c", "wQ", "wqa", { noremap = true, expr = false, silent = false }) 111 | vim.keymap.set("c", "Wq", "wq", { noremap = true, expr = false, silent = false }) 112 | 113 | vim.api.nvim_create_autocmd("InsertLeave", { 114 | pattern = "*", 115 | callback = function() 116 | vim.o.paste = false 117 | end, 118 | once = true, 119 | }) 120 | 121 | vim.api.nvim_create_autocmd({ "InsertLeave", "FocusLost" }, { 122 | pattern = "*", 123 | callback = function() 124 | local bufname = vim.api.nvim_buf_get_name(0) 125 | if bufname and #bufname > 0 then 126 | vim.api.nvim_command("wall") 127 | end 128 | end, 129 | once = false, 130 | }) 131 | 132 | -- auto save and load views. 133 | vim.api.nvim_create_autocmd("BufWrite", { 134 | pattern = "*", 135 | command = "mkview", 136 | once = true, 137 | }) 138 | 139 | vim.api.nvim_create_autocmd("BufRead", { 140 | pattern = "*", 141 | command = "silent! loadview", 142 | once = true, 143 | }) 144 | 145 | -- Create a user command to toggle autoformat for the current buffer 146 | vim.api.nvim_create_user_command("ConformFormatToggle", function() 147 | local bufnr = vim.api.nvim_get_current_buf() 148 | -- Toggle the buffer-local autoformat flag 149 | vim.b[bufnr].conform_disable_autoformat = not vim.b[bufnr].conform_disable_autoformat 150 | 151 | -- Notify the user about the current status 152 | if vim.b[bufnr].conform_disable_autoformat then 153 | vim.notify("Autoformat on save is disabled for this buffer.", vim.log.levels.INFO) 154 | else 155 | vim.notify("Autoformat on save is enabled for this buffer.", vim.log.levels.INFO) 156 | end 157 | end, { 158 | desc = "Toggle autoformat-on-save for the current buffer", 159 | }) 160 | -------------------------------------------------------------------------------- /nvim/lua/line-number.lua: -------------------------------------------------------------------------------- 1 | local function relativeln(target) 2 | local current_ft = vim.bo.filetype 3 | if current_ft == "WExplorer" then 4 | return 5 | end 6 | 7 | if vim.b.lnstatus == nil then 8 | vim.b.lnstatus = "number" 9 | end 10 | 11 | if vim.b.lnstatus ~= "nonumber" then 12 | if target == "number" then 13 | vim.o.number = true 14 | vim.o.relativenumber = false 15 | else 16 | vim.o.number = true 17 | vim.o.relativenumber = true 18 | end 19 | else 20 | vim.o.number = false 21 | end 22 | end 23 | 24 | -- Show relative line number when in command mode and absolute line number in edit mode 25 | local _group = vim.api.nvim_create_augroup("LineNumber", { clear = true }) 26 | local autocmd_config = { 27 | ["InsertEnter"] = "number", 28 | ["InsertLeave"] = "relativenumber", 29 | ["FocusLost"] = "number", 30 | ["CursorMoved"] = "relativenumber", 31 | } 32 | 33 | for event, argument in pairs(autocmd_config) do 34 | vim.api.nvim_create_autocmd(event, { 35 | pattern = "*", 36 | callback = function() 37 | relativeln(argument) 38 | end, 39 | once = true, 40 | group = _group, 41 | }) 42 | end 43 | 44 | function Toggleln() 45 | if vim.b.lnstatus == nil then 46 | vim.b.lnstatus = "number" 47 | end 48 | 49 | if vim.b.lnstatus == "number" then 50 | vim.o.number = false 51 | vim.o.relativenumber = false 52 | vim.b.lnstatus = "nonumber" 53 | else 54 | vim.o.number = true 55 | vim.o.relativenumber = true 56 | vim.b.lnstatus = "number" 57 | end 58 | require("gitsigns").toggle_signs() 59 | require("mini.indentscope").undraw() 60 | end 61 | -------------------------------------------------------------------------------- /nvim/lua/opts/snippets.lua: -------------------------------------------------------------------------------- 1 | local luasnip = require("luasnip") 2 | 3 | local function get_date(_, _, format) 4 | return os.date(format) 5 | end 6 | 7 | luasnip.add_snippets("all", { 8 | luasnip.snippet("dtime", luasnip.function_node(get_date, {}, { user_args = { "%Y-%m-%d %H:%M" } })), 9 | luasnip.snippet("date", luasnip.function_node(get_date, {}, { user_args = { "%Y-%m-%d" } })), 10 | }, { key = "all" }) 11 | 12 | luasnip.add_snippets("python", { 13 | luasnip.snippet("exit", luasnip.text_node({ "import sys", "sys.exit()" })), 14 | luasnip.snippet( 15 | "shell", 16 | luasnip.text_node({ 17 | "xiaket = locals()", 18 | "import os", 19 | "import logging", 20 | "logging.getLogger('parso').setLevel(logging.INFO)", 21 | "from prompt_toolkit.utils import DummyContext", 22 | "from ptpython.repl import PythonRepl, run_config", 23 | "repl = PythonRepl(get_globals=lambda : globals(), get_locals=lambda : xiaket, history_filename=os.path.expanduser('~/.ptpython_history'))", 24 | "run_config(repl)", 25 | "with DummyContext():", 26 | " repl.run()", 27 | }) 28 | ), 29 | luasnip.snippet("ifmain", luasnip.text_node({ "if __name__ == '__main__':", " main()" })), 30 | }, { key = "python" }) 31 | -------------------------------------------------------------------------------- /nvim/lua/opts/telescope.lua: -------------------------------------------------------------------------------- 1 | local actions = require("telescope.actions") 2 | local previewers = require("telescope.previewers") 3 | 4 | local peeker = function(filepath, bufnr, opts) 5 | -- No preview if larger than 100k 6 | opts = opts or {} 7 | 8 | filepath = vim.fn.expand(filepath) 9 | vim.loop.fs_stat(filepath, function(_, stat) 10 | if not stat then 11 | return 12 | end 13 | if stat.size > 100000 then 14 | return 15 | else 16 | previewers.buffer_previewer_maker(filepath, bufnr, opts) 17 | end 18 | end) 19 | end 20 | 21 | require("telescope").setup({ 22 | defaults = { 23 | buffer_previewer_maker = peeker, 24 | mappings = { 25 | i = { 26 | [""] = actions.close, 27 | }, 28 | }, 29 | fzf = { 30 | fuzzy = true, 31 | override_generic_sorter = true, -- override the generic sorter 32 | override_file_sorter = true, -- override the file sorter 33 | }, 34 | }, 35 | }) 36 | require("telescope").load_extension("fzf") 37 | -------------------------------------------------------------------------------- /nvim/lua/packages.lua: -------------------------------------------------------------------------------- 1 | return { 2 | "nvim-lua/plenary.nvim", 3 | "kyazdani42/nvim-web-devicons", 4 | "EdenEast/nightfox.nvim", 5 | 6 | -- Load by filetype 7 | { 8 | "ekalinin/Dockerfile.vim", 9 | ft = { "Dockerfile" }, 10 | }, 11 | { -- go support. 12 | "fatih/vim-go", 13 | ft = "go", 14 | }, 15 | { 16 | "maxandron/goplements.nvim", 17 | ft = "go", 18 | opts = {}, 19 | }, 20 | { 21 | "hashivim/vim-terraform", 22 | ft = "terraform", 23 | }, 24 | { 25 | "rhysd/vim-gfm-syntax", 26 | ft = "markdown", 27 | }, 28 | { 29 | "Vimjas/vim-python-pep8-indent", 30 | ft = "python", 31 | }, 32 | 33 | -- Load by cmd 34 | { 35 | "xiaket/w.nvim", 36 | cmd = { "WToggleExplorer", "WSplitLeft", "WSplitRight", "WSplitUp", "WSplitDown" }, 37 | event = "BufEnter", 38 | opts = { 39 | explorer = { 40 | window_width = 30, 41 | }, 42 | }, 43 | }, 44 | { 45 | "zbirenbaum/copilot.lua", 46 | cmd = "Copilot", 47 | build = ":Copilot auth", 48 | event = "InsertEnter", 49 | opts = { 50 | suggestion = { enabled = false }, 51 | panel = { enabled = false }, 52 | filetypes = { 53 | markdown = true, 54 | help = true, 55 | }, 56 | }, 57 | }, 58 | -- Load when BufWritePre 59 | { 60 | "stevearc/conform.nvim", 61 | 62 | event = { "BufWritePre" }, 63 | opts = { 64 | -- Define your formatters 65 | formatters_by_ft = { 66 | lua = { "stylua" }, 67 | python = { "black", "ruff_fix" }, 68 | sh = { "shfmt" }, 69 | rust = { "rustfmt" }, 70 | c = { "clang-format" }, 71 | }, 72 | -- Set default options 73 | default_format_opts = { 74 | lsp_format = "fallback", 75 | }, 76 | -- Set up format-on-save 77 | format_on_save = function(bufnr) 78 | -- Check only the buffer-local variable 79 | if vim.b[bufnr].conform_disable_autoformat then 80 | -- Return nothing to disable autoformat 81 | return 82 | end 83 | return { timeout_ms = 1500 } 84 | end, 85 | -- Customize formatters 86 | formatters = { 87 | shfmt = { 88 | prepend_args = { "--indent", "2", "--case-indent", "--space-redirects" }, 89 | }, 90 | black = { 91 | prepend_args = { "--line-length", "100" }, 92 | }, 93 | ruff_fix = { 94 | append_args = { "--select", "I001" }, 95 | }, 96 | stylua = { 97 | prepend_args = { 98 | "--config-path", 99 | os.getenv("XDG_CONFIG_HOME") .. "/stylua/stylua.toml", 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | 106 | -- Load when BufRead 107 | { 108 | "echasnovski/mini.nvim", 109 | event = "BufRead", 110 | config = function() 111 | require("mini.jump2d").setup() 112 | require("mini.comment").setup() 113 | require("mini.cursorword").setup() 114 | require("mini.indentscope").setup({ 115 | draw = { 116 | delay = 40, 117 | animation = require("mini.indentscope").gen_animation.none(), 118 | }, 119 | }) 120 | require("mini.pairs").setup() 121 | require("mini.tabline").setup({ show_icons = false }) 122 | end, 123 | }, 124 | { 125 | "monkoose/matchparen.nvim", 126 | event = "BufRead", 127 | opts = {}, 128 | }, 129 | { -- show lsp errors in a buffer. 130 | "folke/trouble.nvim", 131 | event = "BufRead", 132 | }, 133 | { -- show git change statuses. 134 | "lewis6991/gitsigns.nvim", 135 | event = "BufRead", 136 | opts = {}, 137 | }, 138 | { -- stabilize buffer on windows size changes. 139 | "luukvbaal/stabilize.nvim", 140 | event = "BufRead", 141 | }, 142 | { -- replace vimscript version of matchparen 143 | "monkoose/matchparen.nvim", 144 | event = "BufRead", 145 | }, 146 | { -- better :term. 147 | "numtostr/FTerm.nvim", 148 | event = "BufRead", 149 | }, 150 | { 151 | "nvim-telescope/telescope.nvim", 152 | event = "BufRead", 153 | config = function() 154 | require("opts.telescope") 155 | end, 156 | }, 157 | 158 | { -- telescope 159 | "nvim-telescope/telescope-fzf-native.nvim", 160 | event = "BufRead", 161 | build = "make", 162 | }, 163 | 164 | -- treesitter 165 | { 166 | "nvim-treesitter/nvim-treesitter", 167 | config = function() 168 | require("nvim-treesitter").setup({ 169 | ensure_installed = { "python", "bash", "go", "json", "lua", "rust", "yaml" }, 170 | highlight = { enable = true, additional_vim_regex_highlighting = false }, 171 | indent = { enable = true, disable = { "python" } }, 172 | }) 173 | end, 174 | }, 175 | 176 | -- Copilot Chat 177 | { 178 | "CopilotC-Nvim/CopilotChat.nvim", 179 | opts = { 180 | question_header = "## User ", 181 | answer_header = "## Copilot ", 182 | error_header = "## Error ", 183 | model = "claude-3.7-sonnet", 184 | auto_follow_cursor = false, -- Don't follow the cursor after getting response 185 | }, 186 | event = "VeryLazy", 187 | }, 188 | 189 | -- LSP 190 | { 191 | "williamboman/mason.nvim", 192 | dependencies = { 193 | "williamboman/mason-lspconfig.nvim", 194 | }, 195 | event = "VeryLazy", 196 | opts = {}, 197 | }, 198 | { 199 | "neovim/nvim-lspconfig", 200 | commit = "c646154d6e4db9b2979eeb517d0b817ad00c9c47", 201 | event = { "BufReadPre", "BufNewFile" }, 202 | dependencies = { "saghen/blink.cmp" }, 203 | config = function(_, opts) 204 | local lspconfig = require("lspconfig") 205 | local mason = require("mason-lspconfig") 206 | 207 | for server, config in pairs(opts.servers or {}) do 208 | config.capabilities = require("blink.cmp").get_lsp_capabilities(config.capabilities) 209 | lspconfig[server].setup(config) 210 | end 211 | 212 | mason.setup({ 213 | ensure_installed = { 214 | "bashls", 215 | "bzl", 216 | "docker_compose_language_service", 217 | "dockerls", 218 | "gopls", 219 | "lua_ls", 220 | "pyright", 221 | "taplo", -- toml 222 | "terraformls", 223 | }, 224 | automatic_installation = true, 225 | }) 226 | 227 | local auto_configure_servers = { 228 | "bashls", 229 | "docker_compose_language_service", 230 | "dockerls", 231 | "gopls", 232 | "taplo", 233 | "terraformls", 234 | } 235 | 236 | mason.setup_handlers({ 237 | -- default handler for installed servers 238 | function(server_name) 239 | if vim.tbl_contains(auto_configure_servers, server_name) then 240 | lspconfig[server_name].setup({}) 241 | end 242 | end, 243 | ["bzl"] = function() 244 | lspconfig["bzl"].setup({ 245 | filetypes = { "bzl", "BUILD", "bazel" }, 246 | }) 247 | end, 248 | ["pyright"] = function() 249 | local get_python_venv = function() 250 | if vim.env.VIRTUAL_ENV then 251 | return vim.env.VIRTUAL_ENV 252 | end 253 | 254 | local cwd = vim.fn.getcwd() 255 | while true do 256 | if cwd == "/" then 257 | break 258 | end 259 | 260 | local match = vim.fn.glob(cwd, "Venv") 261 | if match ~= "" then 262 | return cwd .. "/Venv" 263 | end 264 | local rev = string.reverse(cwd) 265 | local index = string.find(rev, "/") 266 | if index == nil then 267 | break 268 | end 269 | cwd = string.reverse(string.sub(rev, index + 1)) 270 | end 271 | end 272 | 273 | local venv = get_python_venv() 274 | 275 | lspconfig["pyright"].setup({ 276 | settings = { 277 | pyright = { 278 | autoImportCompletion = true, 279 | }, 280 | python = { 281 | pythonPath = venv .. "/bin/python3", 282 | analysis = { 283 | autoSearchPaths = true, 284 | diagnosticMode = "openFilesOnly", 285 | useLibraryCodeForTypes = true, 286 | typeCheckingMode = "off", 287 | }, 288 | }, 289 | }, 290 | }) 291 | end, 292 | ["lua_ls"] = function() 293 | -- configure lua server (with special settings) 294 | lspconfig["lua_ls"].setup({ 295 | settings = { 296 | Lua = { 297 | -- make the language server recognize "vim" global 298 | diagnostics = { 299 | globals = { "vim" }, 300 | }, 301 | completion = { 302 | callSnippet = "Replace", 303 | }, 304 | }, 305 | }, 306 | }) 307 | end, 308 | }) 309 | end, 310 | }, 311 | 312 | { 313 | "folke/which-key.nvim", 314 | event = "VeryLazy", 315 | init = function() 316 | vim.o.timeout = true 317 | vim.o.timeoutlen = 300 318 | end, 319 | opts = {}, 320 | }, 321 | 322 | { 323 | "saghen/blink.cmp", 324 | dependencies = { 325 | "rafamadriz/friendly-snippets", 326 | { 327 | "fang2hou/blink-copilot", 328 | opts = { 329 | max_completions = 3, 330 | max_attempts = 2, 331 | }, 332 | }, 333 | { 334 | "Kaiser-Yang/blink-cmp-dictionary", 335 | dependencies = { "nvim-lua/plenary.nvim" }, 336 | }, 337 | }, 338 | lazy = false, 339 | version = "*", -- use a release tag to download pre-built binaries 340 | opts = { 341 | -- Press tab to select and enter to accept, shift tab to reverse. 342 | keymap = { 343 | preset = "enter", 344 | [""] = { 345 | function(cmp) 346 | if cmp.snippet_active() then 347 | return cmp.accept() 348 | else 349 | return cmp.select_next() 350 | end 351 | end, 352 | "snippet_forward", 353 | "fallback", 354 | }, 355 | [""] = { 356 | "snippet_backward", 357 | "select_prev", 358 | "fallback", 359 | }, 360 | }, 361 | completion = { 362 | accept = { 363 | auto_brackets = { enabled = false }, 364 | }, 365 | }, 366 | signature = { enabled = true }, 367 | cmdline = { 368 | keymap = { 369 | preset = "enter", 370 | [""] = { "select_accept_and_enter", "fallback" }, 371 | }, 372 | }, 373 | sources = { 374 | default = { "dictionary", "lsp", "path", "snippets", "buffer", "copilot" }, 375 | 376 | providers = { 377 | buffer = { 378 | name = "Buffer", 379 | module = "blink.cmp.sources.buffer", 380 | score_offset = -3, 381 | }, 382 | copilot = { 383 | name = "copilot", 384 | module = "blink-copilot", 385 | score_offset = 8, 386 | async = true, 387 | }, 388 | dictionary = { 389 | module = "blink-cmp-dictionary", 390 | name = "Dict", 391 | min_keyword_length = 3, 392 | max_items = 4, 393 | opts = { 394 | dictionary_directories = { vim.fn.expand("~/.local/state/nvim/dictionary") }, 395 | }, 396 | }, 397 | }, 398 | }, 399 | }, 400 | }, 401 | } 402 | -------------------------------------------------------------------------------- /nvim/snippets/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "dtime": { 3 | "prefix": "dtime", 4 | "body": ["${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}"], 5 | "description": "Put the datetime in (Y-m-d H:M) format" 6 | }, 7 | "date": { 8 | "prefix": "date", 9 | "body": ["${CURRENT_DATE}-${CURRENT_MONTH}-${CURRENT_YEAR}"], 10 | "description": "Put date in (Y-m-d) format" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ps1/.manage: -------------------------------------------------------------------------------- 1 | update () { 2 | cargo build --release 3 | strip target/release/ps1 4 | mv target/release/ps1 ../bin 5 | } 6 | run () { 7 | cargo run 0 8 | } 9 | export -f update run 10 | -------------------------------------------------------------------------------- /ps1/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.4.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 16 | 17 | [[package]] 18 | name = "cc" 19 | version = "1.0.73" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 22 | dependencies = [ 23 | "jobserver", 24 | ] 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "form_urlencoded" 34 | version = "1.0.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 37 | dependencies = [ 38 | "matches", 39 | "percent-encoding", 40 | ] 41 | 42 | [[package]] 43 | name = "git2" 44 | version = "0.18.2" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" 47 | dependencies = [ 48 | "bitflags", 49 | "libc", 50 | "libgit2-sys", 51 | "log", 52 | "openssl-probe", 53 | "openssl-sys", 54 | "url", 55 | ] 56 | 57 | [[package]] 58 | name = "idna" 59 | version = "0.2.3" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 62 | dependencies = [ 63 | "matches", 64 | "unicode-bidi", 65 | "unicode-normalization", 66 | ] 67 | 68 | [[package]] 69 | name = "jobserver" 70 | version = "0.1.24" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" 73 | dependencies = [ 74 | "libc", 75 | ] 76 | 77 | [[package]] 78 | name = "libc" 79 | version = "0.2.126" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 82 | 83 | [[package]] 84 | name = "libgit2-sys" 85 | version = "0.16.2+1.7.2" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" 88 | dependencies = [ 89 | "cc", 90 | "libc", 91 | "libssh2-sys", 92 | "libz-sys", 93 | "openssl-sys", 94 | "pkg-config", 95 | ] 96 | 97 | [[package]] 98 | name = "libssh2-sys" 99 | version = "0.3.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" 102 | dependencies = [ 103 | "cc", 104 | "libc", 105 | "libz-sys", 106 | "openssl-sys", 107 | "pkg-config", 108 | "vcpkg", 109 | ] 110 | 111 | [[package]] 112 | name = "libz-sys" 113 | version = "1.1.8" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" 116 | dependencies = [ 117 | "cc", 118 | "libc", 119 | "pkg-config", 120 | "vcpkg", 121 | ] 122 | 123 | [[package]] 124 | name = "log" 125 | version = "0.4.17" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 128 | dependencies = [ 129 | "cfg-if", 130 | ] 131 | 132 | [[package]] 133 | name = "matches" 134 | version = "0.1.9" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 137 | 138 | [[package]] 139 | name = "openssl-probe" 140 | version = "0.1.5" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 143 | 144 | [[package]] 145 | name = "openssl-sys" 146 | version = "0.9.74" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" 149 | dependencies = [ 150 | "autocfg", 151 | "cc", 152 | "libc", 153 | "pkg-config", 154 | "vcpkg", 155 | ] 156 | 157 | [[package]] 158 | name = "percent-encoding" 159 | version = "2.1.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 162 | 163 | [[package]] 164 | name = "pkg-config" 165 | version = "0.3.25" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 168 | 169 | [[package]] 170 | name = "ps1" 171 | version = "0.1.0" 172 | dependencies = [ 173 | "git2", 174 | ] 175 | 176 | [[package]] 177 | name = "tinyvec" 178 | version = "1.6.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 181 | dependencies = [ 182 | "tinyvec_macros", 183 | ] 184 | 185 | [[package]] 186 | name = "tinyvec_macros" 187 | version = "0.1.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 190 | 191 | [[package]] 192 | name = "unicode-bidi" 193 | version = "0.3.8" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 196 | 197 | [[package]] 198 | name = "unicode-normalization" 199 | version = "0.1.19" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 202 | dependencies = [ 203 | "tinyvec", 204 | ] 205 | 206 | [[package]] 207 | name = "url" 208 | version = "2.2.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 211 | dependencies = [ 212 | "form_urlencoded", 213 | "idna", 214 | "matches", 215 | "percent-encoding", 216 | ] 217 | 218 | [[package]] 219 | name = "vcpkg" 220 | version = "0.2.15" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 223 | -------------------------------------------------------------------------------- /ps1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ps1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | git2 = "0.18" 10 | -------------------------------------------------------------------------------- /ps1/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::thread; 4 | use std::sync::mpsc; 5 | use std::time::Duration; 6 | 7 | use git2::Repository; 8 | 9 | 10 | const SHORT_PATH_TRUNCATE: u8 = 3; 11 | const GIT_TIMEOUT: u16 = 150; 12 | 13 | const START_BRACKET: &str = "{"; 14 | const END_BRACKET: &str = "}"; 15 | const GIT_UNCOMMITED: &str = "+"; 16 | const GIT_UNSTAGED: &str = "!"; 17 | const GIT_UNTRACKED: &str = "?"; 18 | const GIT_STASHED: &str = "¥"; 19 | const VENV_INDICATOR: &str = "∇"; 20 | const BRANCH_TRUNCATION: &str = "⁁"; 21 | 22 | 23 | fn colorize(text: &str, color: &str) -> String { 24 | let colors = HashMap::from([ 25 | ("PWD_DELETED", 178), 26 | ("CMD_EXIT_0_COLOR", 34), 27 | ("CMD_EXIT_NON0_COLOR", 124), 28 | ("CURRENT_PATH_COLOR", 33), 29 | ("SHORT_PATH_COLOR", 15), 30 | ("GIT_BRANCH_COLOR", 166), 31 | ("GIT_STATUS_COLOR", 136), 32 | ("VENV_COLOR", 166), 33 | ]); 34 | let color = colors.get(color).unwrap(); 35 | format!("\x5c\x5b\x1b[38;5;{}m\x5c\x5d{}", color, text) 36 | } 37 | 38 | fn get_last_color() -> &'static str { 39 | match env::current_dir(){ 40 | Err(_) => return "PWD_DELETED", 41 | _ => (), 42 | } 43 | 44 | let args: Vec = env::args().collect(); 45 | 46 | if args.len() == 1 || args[1] == "0" { 47 | "CMD_EXIT_0_COLOR" 48 | }else{ 49 | "CMD_EXIT_NON0_COLOR" 50 | } 51 | } 52 | 53 | // add indicator for venv setup 54 | fn venv_prompt() -> &'static str { 55 | match env::var("VIRTUAL_ENV") { 56 | Ok(val) => { 57 | if !env!("PATH").contains(&val) { 58 | VENV_INDICATOR 59 | }else{ 60 | "" 61 | } 62 | }, 63 | Err(_e) => "", 64 | } 65 | } 66 | 67 | // cwd_prompt would output the current path, with intermediate path truncated. Example: 68 | // /usr/local/lib/python2.7/site-packages -> /usr/loc/lib/pyt/site-packages 69 | fn cwd_prompt() -> String { 70 | let dir = get_cwd().replacen(env!("HOME"), "~", 1); 71 | let mut paths: Vec = dir.split("/").map(|e| e.to_string()).collect::>(); 72 | let segments = paths.len(); 73 | for (i, path) in paths.iter_mut().enumerate() { 74 | // Don't change short path, don't change last segment. 75 | if path.len() < SHORT_PATH_TRUNCATE.into() || i == segments - 1 { 76 | continue 77 | } 78 | let mut truncate = SHORT_PATH_TRUNCATE; 79 | if path.starts_with(".") { 80 | truncate += 1; 81 | } 82 | path.truncate(truncate.into()); 83 | } 84 | return if paths.len() == 1 { 85 | paths[0].to_string() 86 | }else{ 87 | format!( 88 | "{short_path}/{current_path}", 89 | short_path=colorize(&paths[..segments-1].join("/"), "SHORT_PATH_COLOR"), 90 | current_path=colorize(paths.last().unwrap(), "CURRENT_PATH_COLOR"), 91 | ) 92 | } 93 | } 94 | 95 | // git_st will combine GitSt and BranchName and provide git info for prompt. 96 | fn get_git_st(repo: &Repository) -> Result { 97 | let mut has_uncommited = false; 98 | let mut has_unstaged = false; 99 | let mut has_untracked = false; 100 | 101 | let mut opts = git2::StatusOptions::new(); 102 | opts.exclude_submodules(true); 103 | opts.include_ignored(false); 104 | opts.include_untracked(true); 105 | opts.renames_head_to_index(true); 106 | 107 | for entry in repo.statuses(Some(&mut opts)).unwrap().iter() { 108 | let status = entry.status(); 109 | if status.is_index_modified() || status.is_index_new() || status.is_index_deleted() || status.is_index_renamed() || status.is_index_typechange(){ 110 | has_uncommited = true; 111 | } 112 | if status.is_wt_new(){ 113 | has_untracked = true; 114 | } 115 | if status.is_wt_modified(){ 116 | has_unstaged = true; 117 | } 118 | } 119 | let mut status = "".to_string(); 120 | if has_uncommited { 121 | status.push_str(GIT_UNCOMMITED) 122 | } 123 | if has_untracked { 124 | status.push_str(GIT_UNTRACKED) 125 | } 126 | if has_unstaged { 127 | status.push_str(GIT_UNSTAGED) 128 | } 129 | if repo.path().join("refs/stash").exists() { 130 | status.push_str(GIT_STASHED) 131 | } 132 | 133 | Ok(status.to_string()) 134 | } 135 | 136 | fn get_git_branch(repo: &Repository) -> String { 137 | return match repo.head() { 138 | Err(_) => "Unknown".to_string(), 139 | Ok(reference) => { 140 | let branch = reference.shorthand().unwrap(); 141 | match branch { 142 | "master" => "🏠".to_string(), 143 | "main" => "🏠".to_string(), 144 | _ => { 145 | let mut name = branch.replacen("feature/", "🔨/", 1) 146 | .replacen("bugfix/", "🐛/", 1) 147 | .replacen("bug/", "🐛/", 1) 148 | .replacen("fix/", "🐛/", 1); 149 | if name.len() > 15 { 150 | name.replace_range(12..(name.len() - 3), BRANCH_TRUNCATION); 151 | name 152 | }else{ 153 | name 154 | } 155 | }, 156 | } 157 | }, 158 | } 159 | } 160 | 161 | fn get_cwd() -> String { 162 | let current_dir = env::current_dir(); 163 | match current_dir { 164 | Ok(current_dir) => current_dir.display().to_string(), 165 | Err(_) => env::var("PWD").unwrap(), 166 | } 167 | } 168 | 169 | fn main() { 170 | let last_color = get_last_color(); 171 | let (sender, receiver) = mpsc::channel(); 172 | let cwd_sender = sender.clone(); 173 | 174 | let mut has_git: bool = false; 175 | let mut git_branch: String = "".to_string(); 176 | let mut git_st: String = "".to_string(); 177 | 178 | let mut cwd: String = "".to_string(); 179 | let mut threads = 1; 180 | 181 | // Spawning threads to do the heavy-lifting. 182 | thread::spawn(move || {cwd_sender.send(format!("cwd:{}", cwd_prompt())).unwrap();}); 183 | 184 | match Repository::discover(get_cwd()) { 185 | Err(_e) => {}, 186 | Ok(repo) => { 187 | has_git = true; 188 | threads += 1; 189 | let git_sender = sender.clone(); 190 | git_branch = get_git_branch(&repo); 191 | 192 | thread::spawn(move|| { 193 | let result = get_git_st(&repo).unwrap(); 194 | git_sender.send(format!("git-st:{}", result)).unwrap(); 195 | }); 196 | }, 197 | }; 198 | 199 | for _ in 0..threads { 200 | match receiver.recv_timeout(Duration::from_millis(GIT_TIMEOUT.into())) { 201 | Ok(val) => { 202 | let split: Vec<&str> = val.splitn(2, ":").collect(); 203 | match split[0] { 204 | "git-st" => git_st = split[1].to_string(), 205 | "cwd" => cwd = split[1].to_string(), 206 | _ => panic!("Unknown message"), 207 | } 208 | }, 209 | Err(mpsc::RecvTimeoutError::Timeout) => {}, 210 | Err(_) => panic!("Unknown error."), 211 | } 212 | }; 213 | 214 | let git_prompt = if has_git { 215 | format!( 216 | "{}{} ", 217 | colorize(&git_branch, "GIT_BRANCH_COLOR"), colorize(&git_st, "GIT_STATUS_COLOR") 218 | ) 219 | }else{ 220 | "".to_string() 221 | }; 222 | 223 | println!( 224 | "{start_bracket}{venv}{git}{cwd}{end_bracket}\x5c\x5b\x1b[0m\x5c\x5d", 225 | venv=colorize(venv_prompt(), "VENV_COLOR"), 226 | start_bracket=colorize(START_BRACKET, last_color), 227 | end_bracket=colorize(END_BRACKET, last_color), 228 | git=git_prompt, 229 | cwd=cwd, 230 | ); 231 | } 232 | -------------------------------------------------------------------------------- /ptpython/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding=utf8 3 | 4 | 5 | def configure(repl): 6 | """ 7 | Configuration method. This is called during the start-up of ptpython. 8 | :param repl: `PythonRepl` instance. 9 | """ 10 | # Disable these features 11 | repl.confirm_exit = False 12 | repl.enable_open_in_editor = False 13 | 14 | # visual 15 | repl.highlight_matching_parenthesis = True 16 | repl.insert_blank_line_after_output = False 17 | repl.prompt_style = "ipython" 18 | repl.color_depth = "DEPTH_24_BIT" 19 | repl.use_code_colorscheme("zenburn") 20 | 21 | # typo fixer. 22 | corrections = { 23 | "impotr": "import", 24 | "pritn": "print", 25 | } 26 | -------------------------------------------------------------------------------- /pycodestyle: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | count = False 3 | ignore = E731 4 | max-line-length = 100 5 | statistics = False 6 | -------------------------------------------------------------------------------- /pythonrc: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | try: 4 | from pip._vendor.rich import pretty, print 5 | pretty.install() 6 | has_rich = True 7 | except ImportError: 8 | has_rich = False 9 | 10 | 11 | sys.ps1 = "> " 12 | sys.ps2 = " " 13 | 14 | del sys 15 | if has_rich: 16 | del has_rich, pretty 17 | -------------------------------------------------------------------------------- /stylua/stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 100 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "Always" 7 | -------------------------------------------------------------------------------- /vagrant/build/.gitignore: -------------------------------------------------------------------------------- 1 | *.box 2 | *.iso 3 | -------------------------------------------------------------------------------- /vagrant/build/README.md: -------------------------------------------------------------------------------- 1 | Files/scripts in this folder build an ubuntu `x86_64` machine image for ubuntu, in the way I wanted it to be. 2 | 3 | 4 | ## Build 5 | 6 | It's easy, just run build.sh 7 | 8 | 9 | ## Use 10 | 11 | The following Vagrantfile should get you started: 12 | 13 | ``` 14 | Vagrant.configure("2") do |config| 15 | config.vm.box = "ubuntu-2204" 16 | config.vm.hostname = "ubuntu" 17 | 18 | config.vm.provider "qemu" do |qe| 19 | qe.arch = "x86_64" 20 | qe.machine = "q35" 21 | qe.cpu = "max" 22 | qe.memory = "8G" 23 | qe.smp = "cpus=2,sockets=1,cores=2,threads=1" 24 | qe.net_device = "virtio-net-pci" 25 | qe.extra_qemu_args = %w(-accel tcg,thread=multi,tb-size=512) 26 | qe.qemu_dir = "/usr/local/share/qemu" 27 | end 28 | 29 | config.vm.synced_folder "/usr/local/share/qemu", "/mnt", type: "rsync" 30 | config.vm.synced_folder ".", "/vagrant", type: "smb", disabled: true 31 | end 32 | ``` 33 | -------------------------------------------------------------------------------- /vagrant/build/build.pkr.hcl: -------------------------------------------------------------------------------- 1 | packer { 2 | required_plugins { 3 | qemu = { 4 | version = ">= 1.0.1" 5 | source = "github.com/hashicorp/qemu" 6 | } 7 | } 8 | } 9 | 10 | build { 11 | sources = ["source.qemu.ubuntu"] 12 | 13 | # Linux Shell scipts 14 | provisioner "shell" { 15 | environment_vars = [ 16 | "HOME_DIR=/home/vagrant", 17 | ] 18 | execute_command = "echo 'vagrant' | {{ .Vars }} sudo -S -E bash -x '{{ .Path }}'" 19 | expect_disconnect = true 20 | scripts = [ 21 | "${path.root}/scripts/sshd.sh", 22 | "${path.root}/scripts/update.sh", 23 | "${path.root}/scripts/networking.sh", 24 | "${path.root}/scripts/sudo.sh", 25 | "${path.root}/scripts/vagrant.sh", 26 | "${path.root}/scripts/cleanup.sh", 27 | ] 28 | } 29 | 30 | # Convert machines to vagrant boxes 31 | post-processor "vagrant" { 32 | compression_level = 9 33 | output = "${path.root}/ubuntu-2204-x86_64.{{ .Provider }}.box" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vagrant/build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | 8 | if [ ! -f ubuntu-22.04.3-live-server-amd64.iso ] 9 | then 10 | wget 'https://releases.ubuntu.com/22.04.3/ubuntu-22.04.3-live-server-amd64.iso' 11 | fi 12 | 13 | if [ ! command -v packer &> /dev/null ] 14 | then 15 | brew install packer 16 | fi 17 | packer plugins install github.com/hashicorp/qemu 18 | 19 | # This step would run for around 20-30 mins on an M1 Max. 20 | # Add PACKER_LOG=1 to debug. 21 | # PACKER_LOG=1 packer build . 22 | packer build . 23 | 24 | vagrant box add ubuntu-2204 ubuntu-2204-x86_64.libvirt.box 25 | -------------------------------------------------------------------------------- /vagrant/build/http/meta-data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaket/etc/5e5b095a2a1239b586e4036d58cd7e83528e5b7e/vagrant/build/http/meta-data -------------------------------------------------------------------------------- /vagrant/build/http/user-data: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | autoinstall: 3 | version: 1 4 | locale: en_US 5 | keyboard: 6 | layout: us 7 | ssh: 8 | install-server: true 9 | allow-pw: true 10 | user-data: 11 | preserve_hostname: false 12 | hostname: ubuntu 13 | package_upgrade: false 14 | timezone: Australia/Melbourne 15 | users: 16 | - name: vagrant 17 | plain_text_passwd: vagrant 18 | groups: [adm, cdrom, dip, plugdev, lxd, sudo] 19 | lock-passwd: false 20 | sudo: ALL=(ALL) NOPASSWD:ALL 21 | shell: /bin/bash 22 | -------------------------------------------------------------------------------- /vagrant/build/scripts/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset 3 | set -o pipefail 4 | 5 | echo "remove linux-headers" 6 | dpkg --list \ 7 | | awk '{ print $2 }' \ 8 | | grep 'linux-headers' \ 9 | | xargs apt-get -y purge; 10 | 11 | echo "remove specific Linux kernels, such as linux-image-3.11.0-15-generic but keeps the current kernel and does not touch the virtual packages" 12 | dpkg --list \ 13 | | awk '{ print $2 }' \ 14 | | grep 'linux-image-.*-generic' \ 15 | | grep -v "$(uname -r)" \ 16 | | xargs apt-get -y purge; 17 | 18 | echo "remove old kernel modules packages" 19 | dpkg --list \ 20 | | awk '{ print $2 }' \ 21 | | grep 'linux-modules-.*-generic' \ 22 | | grep -v "$(uname -r)" \ 23 | | xargs apt-get -y purge; 24 | 25 | echo "remove linux-source package" 26 | dpkg --list \ 27 | | awk '{ print $2 }' \ 28 | | grep linux-source \ 29 | | xargs apt-get -y purge; 30 | 31 | echo "remove all development packages" 32 | dpkg --list \ 33 | | awk '{ print $2 }' \ 34 | | grep -- '-dev\(:[a-z0-9]\+\)\?$' \ 35 | | xargs apt-get -y purge; 36 | 37 | echo "remove docs packages" 38 | dpkg --list \ 39 | | awk '{ print $2 }' \ 40 | | grep -- '-doc$' \ 41 | | xargs apt-get -y purge; 42 | 43 | echo "remove X11 libraries" 44 | apt-get -y purge libx11-data xauth libxmuu1 libxcb1 libx11-6 libxext6; 45 | 46 | echo "remove obsolete networking packages" 47 | apt-get -y purge ppp pppconfig pppoeconf; 48 | 49 | echo "remove packages we don't need" 50 | apt-get -y purge popularity-contest command-not-found friendly-recovery bash-completion laptop-detect motd-news-config usbutils grub-legacy-ec2 51 | 52 | echo "remove the console font" 53 | apt-get -y purge fonts-ubuntu-console || true; 54 | 55 | # Exclude the files we don't need w/o uninstalling linux-firmware 56 | echo "Setup dpkg excludes for linux-firmware" 57 | cat <<_EOF_ | cat >> /etc/dpkg/dpkg.cfg.d/excludes 58 | path-exclude=/lib/firmware/* 59 | path-exclude=/usr/share/doc/linux-firmware/* 60 | _EOF_ 61 | 62 | echo "delete the massive firmware files" 63 | rm -rf /lib/firmware/* 64 | rm -rf /usr/share/doc/linux-firmware/* 65 | 66 | echo "autoremoving packages and cleaning apt data" 67 | apt-get -y autoremove; 68 | apt-get -y clean; 69 | 70 | echo "remove /usr/share/doc/" 71 | rm -rf /usr/share/doc/* 72 | 73 | echo "remove /var/cache" 74 | find /var/cache -type f -exec rm -rf {} \; 75 | 76 | echo "truncate any logs that have built up during the install" 77 | find /var/log -type f -exec truncate --size=0 {} \; 78 | 79 | echo "blank netplan machine-id (DUID) so machines get unique ID generated on boot" 80 | truncate -s 0 /etc/machine-id 81 | if test -f /var/lib/dbus/machine-id 82 | then 83 | truncate -s 0 /var/lib/dbus/machine-id # if not symlinked to "/etc/machine-id" 84 | fi 85 | 86 | echo "remove the contents of /tmp and /var/tmp" 87 | rm -rf /tmp/* /var/tmp/* 88 | 89 | echo "force a new random seed to be generated" 90 | rm -f /var/lib/systemd/random-seed 91 | 92 | echo "clear the history so our install isn't there" 93 | rm -f /root/.wget-hsts 94 | export HISTSIZE=0 95 | -------------------------------------------------------------------------------- /vagrant/build/scripts/networking.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | ubuntu_version="$(lsb_release -r | awk '{print $2}')"; 7 | major_version="$(echo "$ubuntu_version" | awk -F. '{print $1}')"; 8 | 9 | echo "Create netplan config for eth0" 10 | cat </etc/netplan/01-netcfg.yaml; 11 | network: 12 | version: 2 13 | ethernets: 14 | eth0: 15 | dhcp4: true 16 | EOF 17 | 18 | # Disable Predictable Network Interface names and use eth0 19 | [ -e /etc/network/interfaces ] && sed -i 's/en[[:alnum:]]*/eth0/g' /etc/network/interfaces; 20 | sed -i 's/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="net.ifnames=0 biosdevname=0 \1"/g' /etc/default/grub; 21 | update-grub; 22 | -------------------------------------------------------------------------------- /vagrant/build/scripts/sshd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | SSHD_CONFIG="/etc/ssh/sshd_config" 7 | 8 | # ensure that there is a trailing newline before attempting to concatenate 9 | # shellcheck disable=SC1003 10 | sed -i -e '$a\' "$SSHD_CONFIG" 11 | 12 | USEDNS="UseDNS no" 13 | if grep -q -E "^[[:space:]]*UseDNS" "$SSHD_CONFIG"; then 14 | sed -i "s/^\s*UseDNS.*/${USEDNS}/" "$SSHD_CONFIG" 15 | else 16 | echo "$USEDNS" >>"$SSHD_CONFIG" 17 | fi 18 | 19 | GSSAPI="GSSAPIAuthentication no" 20 | if grep -q -E "^[[:space:]]*GSSAPIAuthentication" "$SSHD_CONFIG"; then 21 | sed -i "s/^\s*GSSAPIAuthentication.*/${GSSAPI}/" "$SSHD_CONFIG" 22 | else 23 | echo "$GSSAPI" >>"$SSHD_CONFIG" 24 | fi 25 | -------------------------------------------------------------------------------- /vagrant/build/scripts/sudo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | sed -i -e '/Defaults\s\+env_reset/a Defaults\texempt_group=sudo' /etc/sudoers; 7 | 8 | # Set up password-less sudo for the vagrant user 9 | echo 'vagrant ALL=(ALL) NOPASSWD:ALL' >/etc/sudoers.d/99_vagrant; 10 | chmod 440 /etc/sudoers.d/99_vagrant; 11 | -------------------------------------------------------------------------------- /vagrant/build/scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | export DEBIAN_FRONTEND=noninteractive 7 | 8 | echo "disable release-upgrades" 9 | sed -i.bak 's/^Prompt=.*$/Prompt=never/' /etc/update-manager/release-upgrades; 10 | 11 | echo "disable systemd apt timers/services" 12 | systemctl stop apt-daily.timer; 13 | systemctl stop apt-daily-upgrade.timer; 14 | systemctl disable apt-daily.timer; 15 | systemctl disable apt-daily-upgrade.timer; 16 | systemctl mask apt-daily.service; 17 | systemctl mask apt-daily-upgrade.service; 18 | systemctl daemon-reload; 19 | 20 | # Disable periodic activities of apt to be safe 21 | cat </etc/apt/apt.conf.d/10periodic; 22 | APT::Periodic::Enable "0"; 23 | APT::Periodic::Update-Package-Lists "0"; 24 | APT::Periodic::Download-Upgradeable-Packages "0"; 25 | APT::Periodic::AutocleanInterval "0"; 26 | APT::Periodic::Unattended-Upgrade "0"; 27 | EOF 28 | 29 | echo "remove the unattended-upgrades and ubuntu-release-upgrader-core packages" 30 | rm -rf /var/log/unattended-upgrades; 31 | apt-get -y purge unattended-upgrades ubuntu-release-upgrader-core; 32 | 33 | echo "update the package list" 34 | apt-get -y update; 35 | 36 | echo "upgrade all installed packages incl. kernel and kernel headers" 37 | apt-get -y dist-upgrade -o Dpkg::Options::="--force-confnew"; 38 | -------------------------------------------------------------------------------- /vagrant/build/scripts/vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | # set a default HOME_DIR environment variable if not set 7 | HOME_DIR="${HOME_DIR:-/home/vagrant}"; 8 | 9 | pubkey_url="https://raw.githubusercontent.com/hashicorp/vagrant/main/keys/vagrant.pub"; 10 | mkdir -p "$HOME_DIR"/.ssh; 11 | apt-get install -y wget 12 | wget --no-check-certificate "$pubkey_url" -O "$HOME_DIR"/.ssh/authorized_keys; 13 | 14 | chown -R vagrant "$HOME_DIR"/.ssh; 15 | chmod -R go-rwsx "$HOME_DIR"/.ssh; 16 | -------------------------------------------------------------------------------- /vagrant/build/source.pkr.hcl: -------------------------------------------------------------------------------- 1 | source "qemu" "ubuntu" { 2 | boot_command = [ 3 | "", 4 | "e", 5 | "", 6 | " autoinstall ds=nocloud-net\\;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/", 7 | "" 8 | ] 9 | disk_size = "32768" 10 | iso_urls = [ 11 | "ubuntu-22.04.3-live-server-amd64.iso" 12 | ] 13 | iso_checksum = "file:https://releases.ubuntu.com/jammy/SHA256SUMS" 14 | output_directory = "output-ubuntu2204" 15 | shutdown_command = "echo 'vagrant'|sudo -S shutdown -P now" 16 | http_directory = "http" 17 | ssh_password = "vagrant" 18 | ssh_username = "vagrant" 19 | ssh_wait_timeout = "1h" 20 | ssh_timeout = "1h" 21 | vm_name = "ubuntu2204" 22 | use_default_display = false 23 | headless = true 24 | 25 | qemu_binary = "qemu-system-x86_64" 26 | cpus = 4 27 | communicator = "ssh" 28 | memory = 8192 29 | } 30 | -------------------------------------------------------------------------------- /vagrant/freebsd/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | # Doc link: https://docs.vagrantup.com. 6 | 7 | # Use ubuntu 2104 as base image. 8 | config.vm.box = "xiaket/freebsd-base" 9 | 10 | # hostname of this VM 11 | config.vm.hostname = "freebsd" 12 | 13 | config.vm.provider "virtualbox" do |vb| 14 | # name of the vm 15 | vb.name = "freebsd" 16 | 17 | # resouce allocations. 18 | vb.memory = "4096" 19 | vb.cpus = 2 20 | if Vagrant.has_plugin?("vagrant-vbguest") then 21 | config.vbguest.auto_update = false 22 | end 23 | end 24 | 25 | # setup sync 26 | config.vm.synced_folder ENV['HOME'], "/mac" 27 | end 28 | -------------------------------------------------------------------------------- /vagrant/freebsd/base/create-base-box.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | vm_name="freebsd-base" 8 | box_name="xiaket/freebsd-base" 9 | iso=/tmp/mfsbsd.iso 10 | 11 | if [ ! -f "$iso" ]; then 12 | wget https://mfsbsd.vx.sk/files/iso/13/amd64/mfsbsd-se-13.0-RELEASE-amd64.iso -O "$iso" 13 | fi 14 | 15 | has_box=$(vagrant box list | grep -c "$box_name" || true) 16 | 17 | if [ "$has_box" = "1" ]; then 18 | vagrant box remove "$box_name" 19 | fi 20 | 21 | rm -f package.box 22 | 23 | VBoxManage createvm --name "${vm_name}" --ostype FreeBSD_64 --basefolder build --register 24 | VBoxManage modifyvm "${vm_name}" --cpus 2 25 | VBoxManage modifyvm "${vm_name}" --memory 6144 26 | VBoxManage modifyvm "${vm_name}" --vram 64 27 | VBoxManage modifyvm "${vm_name}" --graphicscontroller vmsvga 28 | VBoxManage modifyvm "${vm_name}" --audio none 29 | VBoxManage modifyvm "${vm_name}" --hwvirtex on 30 | VBoxManage storagectl "${vm_name}" --name IDE --add ide 31 | VBoxManage createmedium disk --filename zfs.vmdk --size 60000 --format vmdk 32 | VBoxManage storageattach "${vm_name}" --storagectl IDE --port 0 --device 0 --type hdd --medium zfs.vmdk 33 | VBoxManage storageattach "${vm_name}" --storagectl IDE --port 0 --device 1 --type dvddrive --medium "$iso" 34 | VBoxManage startvm "${vm_name}" --type gui 35 | sleep 2 36 | # Enter to skip the 10 seconds wait 37 | VBoxManage controlvm "${vm_name}" keyboardputscancode e0 1c 38 | sleep 45 39 | VBoxManage controlvm "${vm_name}" keyboardputfile mfs.credential 40 | sleep 1 41 | VBoxManage controlvm "${vm_name}" keyboardputfile zfs.csh 42 | 43 | wait-till-poweroff() { 44 | while true; do 45 | state=$(VBoxManage showvminfo "${vm_name}" --machinereadable | grep "VMState=" | sed "s/\"/ /g" | awk '{print $2}') 46 | if [ "$state" == "poweroff" ]; then 47 | break 48 | fi 49 | sleep 10 50 | echo "Sleeping" 51 | done 52 | } 53 | 54 | wait-till-poweroff 55 | VBoxManage storageattach "${vm_name}" --storagectl IDE --port 0 --device 1 --type dvddrive --medium none 56 | sleep 1 57 | 58 | # Start the machine a second to use zfs and install stuff. 59 | VBoxManage startvm "${vm_name}" --type gui 60 | sleep 2 61 | # Enter to skip the 10 seconds wait 62 | VBoxManage controlvm "${vm_name}" keyboardputscancode e0 1c 63 | sleep 45 64 | VBoxManage controlvm "${vm_name}" keyboardputfile mfs.credential 65 | sleep 1 66 | VBoxManage controlvm "${vm_name}" keyboardputfile init.csh 67 | 68 | wait-till-poweroff 69 | # create package. 70 | vagrant package --base "${vm_name}" 71 | vagrant box add "${box_name}" file://./package.box 72 | VBoxManage unregistervm "${vm_name}" --delete 73 | -------------------------------------------------------------------------------- /vagrant/freebsd/base/init.csh: -------------------------------------------------------------------------------- 1 | setenv FREEBSD_MIRROR http://mirror.aarnet.edu.au/pub/FreeBSD 2 | setenv username vagrant 3 | setenv ASSUME_ALWAYS_YES "yes" 4 | 5 | dhclient em0 6 | mkdir -p /usr/local/etc/pkg/repos/ 7 | echo 'FreeBSD: {\ 8 | url: "pkg+http://pkg0.pkt.FreeBSD.org/${ABI}/latest",\ 9 | mirror_type: "srv",\ 10 | signature_type: "fingerprints",\ 11 | fingerprints: "/usr/share/keys/pkg",\ 12 | enabled: yes\ 13 | }' > /usr/local/etc/pkg/repos/freebsd.conf 14 | pkg install pkg ca_root_nss sudo bash virtualbox-ose-additions 15 | 16 | mkdir -p "/usr/local/etc/sudoers.d" 17 | echo "#includedir /usr/local/etc/sudoers.d" > "/usr/local/etc/sudoers" 18 | chmod 440 "/usr/local/etc/sudoers" 19 | echo "%$username ALL=(ALL) NOPASSWD: ALL" > "/usr/local/etc/sudoers.d/$username" 20 | chmod 440 "/usr/local/etc/sudoers.d/$username" 21 | 22 | pw groupadd -n "$username" -g 1001 23 | echo "*" | pw useradd -n "$username" -u 1001 -s /usr/local/bin/bash -m -g 1001 -G wheel -H 0 24 | mkdir -p "/home/${username}/.ssh/" 25 | fetch -o "/home/${username}/.ssh/authorized_keys" https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub 26 | echo 'sshd_enable="YES"' >> "/etc/rc.conf" 27 | echo 'ifconfig_em0="DHCP"' >> "/etc/rc.conf" 28 | echo 'vboxguest_enable="YES"' >> "/etc/rc.conf" 29 | echo 'vboxservice_enable="YES"' >> "/etc/rc.conf" 30 | 31 | poweroff 32 | -------------------------------------------------------------------------------- /vagrant/freebsd/base/mfs.credential: -------------------------------------------------------------------------------- 1 | root 2 | mfsroot 3 | 4 | -------------------------------------------------------------------------------- /vagrant/freebsd/base/zfs.csh: -------------------------------------------------------------------------------- 1 | mkdir -p /cdrom 2 | mount_cd9660 /dev/cd0 /cdrom 3 | zfsinstall -d /dev/ada0 -u /cdrom/13.0-RELEASE-amd64 -p zroot -s 1G 4 | poweroff 5 | -------------------------------------------------------------------------------- /vagrant/ubuntu/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | # Doc link: https://docs.vagrantup.com. 6 | 7 | # Use ubuntu 2104 as base image. 8 | config.vm.box = "ubuntu/hirsute64" 9 | 10 | # hostname of this VM 11 | config.vm.hostname = "ubuntu" 12 | 13 | config.vm.provider "virtualbox" do |vb| 14 | # name of the vm 15 | vb.name = "ubuntu" 16 | 17 | # resouce allocations. 18 | vb.memory = "4096" 19 | vb.cpus = 2 20 | if Vagrant.has_plugin?("vagrant-vbguest") then 21 | config.vbguest.auto_update = false 22 | end 23 | end 24 | 25 | # setup sync 26 | config.vm.synced_folder ENV['HOME'], "/mac" 27 | 28 | config.vm.provision "shell", inline: <<-SHELL 29 | echo 'APT::Install-Suggests "0";' > /etc/apt/apt.conf.d/01xiaket && 30 | echo 'APT::Install-Recommends "0";' >> /etc/apt/apt.conf.d/01xiaket && 31 | apt update && 32 | apt purge -y command-not-found ftp ntfs-3g vim* cloud-init && 33 | apt autoremove -y && 34 | echo "Done with System setup." 35 | SHELL 36 | 37 | config.vm.provision "shell", path: "ubuntu.sh", privileged: true 38 | end 39 | -------------------------------------------------------------------------------- /vagrant/ubuntu/ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # This stage does not have any prerequisites. We should run this after the first boot. 8 | HOME=/home/vagrant 9 | BASE_DIR="$HOME/.xiaket" 10 | USER=vagrant 11 | 12 | 13 | # helpers 14 | check-done () { 15 | done_dir="$BASE_DIR/var/run/done" 16 | mkdir -p "$done_dir" 17 | func="${FUNCNAME[1]}" 18 | if [ -f "$done_dir/$func" ] 19 | then 20 | return 1 21 | else 22 | return 0 23 | fi 24 | } 25 | 26 | touch-done () { 27 | done_dir="$BASE_DIR/var/run/done" 28 | func="${FUNCNAME[1]}" 29 | touch "$done_dir/$func" 30 | } 31 | 32 | # configuration steps 33 | clone-etc () { 34 | check-done || return 0 35 | 36 | mkdir -p "$BASE_DIR/share/github" 37 | 38 | while true 39 | do 40 | has_git=$(git --version 2>/dev/null || echo "false") 41 | if [ "$has_git" != "false" ] 42 | then 43 | break 44 | fi 45 | echo "sleeping" 46 | sleep 5 47 | done 48 | 49 | git clone https://github.com/xiaket/etc.git "$BASE_DIR/share/github/etc" 50 | ln -s "$BASE_DIR/share/github/etc" "$BASE_DIR/etc" 51 | touch-done 52 | } 53 | 54 | linux-packages () { 55 | check-done || return 0 56 | apt install -y golang pkgconf libgit2-dev gcc make docker.io neovim python3-pip python3.9-venv python3-dev zoxide mysql-client 57 | touch-done 58 | } 59 | 60 | linux-config () { 61 | check-done || return 0 62 | usermod -G docker "$USER" 63 | touch-done 64 | } 65 | 66 | python-packages () { 67 | check-done || return 0 68 | sudo -u "$USER" python3 -m pip install -U pip 69 | sudo -u "$USER" python3 -m pip install black icdiff poetry psutil ptpython pyflakes pygments requests sh Snape termcolor virtualenv 70 | touch-done 71 | } 72 | 73 | build-ps1 () { 74 | check-done || return 0 75 | cd "$BASE_DIR/etc/go" 76 | go build -o ../bin/ps1 ps1.go 77 | touch-done 78 | } 79 | 80 | create-links () { 81 | check-done || return 0 82 | # for configuration in $HOME 83 | ln -sf "$BASE_DIR/etc/bashrc" "$HOME/.bashrc" 84 | ln -sf "$BASE_DIR/etc/inputrc" "$HOME/.inputrc" 85 | ln -sf "$BASE_DIR/etc/pythonrc" "$HOME/.pythonrc" 86 | ln -sf "$BASE_DIR/etc/ptpython" "$HOME/.ptpython" 87 | 88 | # for configuration in .config 89 | mkdir -p "$HOME/.config" 90 | chown -R "$USER":"$USER" "$HOME/.xiaket" "$HOME/.bashrc" "$HOME/.pythonrc" "$HOME/.ptpython" "$HOME/.inputrc" "$HOME/.config" 91 | touch-done 92 | } 93 | 94 | 95 | clone-etc 96 | linux-packages 97 | linux-config 98 | python-packages 99 | build-ps1 100 | create-links 101 | -------------------------------------------------------------------------------- /xremap/config.yml: -------------------------------------------------------------------------------- 1 | keymap: 2 | - name: hiper 3 | remap: 4 | ALT_R-1: 5 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "1"] 6 | ALT_R-2: 7 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "2"] 8 | ALT_R-3: 9 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "3"] 10 | ALT_R-4: 11 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "4"] 12 | ALT_R-0: 13 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "0"] 14 | ALT_R-COMMA: 15 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "left"] 16 | ALT_R-DOT: 17 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "right"] 18 | ALT_R-D: 19 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "dolphin"] 20 | ALT_R-K: 21 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "kitty"] 22 | ALT_R-O: 23 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "obsidian"] 24 | ALT_R-X: 25 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "firefox"] 26 | ALT_R-Y: 27 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "yast"] 28 | ALT_R-Z: 29 | launch: ["/home/xiaket/.xiaket/etc/bin/hiper.sh", "zeal"] 30 | - name: macos like key bindings 31 | remap: 32 | ALT-T: C-T # new tab 33 | ALT-F: C-F # find 34 | ALT-C: C-C # copy 35 | ALT-X: C-X # cut 36 | ALT-V: C-V # paste 37 | C-A: Home 38 | C-E: End 39 | C-B: Left 40 | C-F: Right 41 | C-N: Down 42 | C-P: Up 43 | application: 44 | not: kitty 45 | - name: firefox tweaks 46 | remap: 47 | ALT-L: C-L 48 | ALT-R: C-R 49 | ALT-Z: Control-Shift-T 50 | application: 51 | only: firefox 52 | -------------------------------------------------------------------------------- /xremap/xremap.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=xremap 3 | 4 | [Service] 5 | ExecStart=/home/xiaket/.cargo/bin/xremap /home/xiaket/.xiaket/etc/xremap/config.yml 6 | Type=simple 7 | KillMode=process 8 | Restart=always 9 | RestartSec=10 10 | 11 | [Install] 12 | WantedBy=default.target 13 | --------------------------------------------------------------------------------