Compare commits

..

18 Commits

Author SHA1 Message Date
91cde60dba fix(scripts): resolve shellcheck warnings (#148) 2025-07-10 17:44:36 +03:00
5dea757707 chore(config): remove aqua from go packages
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-10 12:38:54 +03:00
f8833fca73 chore(zed): add templ, add requirements
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-10 12:38:26 +03:00
9cd99dbc88 chore(zed): update theme, update config
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-10 12:37:26 +03:00
14f67cb5ca chore(yabai): ignore golang
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-10 12:19:24 +03:00
github-actions[bot]
4ce76fbd70 chore: update pre-commit hooks (#147) 2025-07-10 09:36:39 +03:00
renovate[bot]
ca68803fb9 feat(github-action): update ivuorinen/actions (25.6.30 → 25.7.7) (#146) 2025-07-09 15:29:09 +03:00
fb97f10f64 chore(nvim): lax hardtime rules
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-08 01:34:33 +03:00
6dbbc439b3 feat(nvim): switch to rose pine
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-08 01:33:18 +03:00
368fbfc7c8 feat(fish): cr function
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-07 22:32:08 +03:00
d6d5a8ca36 feat(shell): patrickf1/fzf.fish
Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-07-07 14:29:45 +03:00
github-actions[bot]
d65c25819f chore: update pre-commit hooks (#145) 2025-07-07 09:45:56 +03:00
renovate[bot]
62c620efad fix(github-action): update ivuorinen/actions (25.6.25 → 25.6.30) (#144) 2025-07-03 09:45:24 +03:00
07fe18af75 fix(scripts): correct usage and help exit status (#143) 2025-06-30 08:51:06 +03:00
renovate[bot]
29d3676b38 fix(github-action): update ivuorinen/actions (25.6.23 → 25.6.25) (#142) 2025-06-30 08:04:34 +03:00
0b9e1803d4 docs(agents): update instructions for yarn (#141) 2025-06-30 07:59:58 +03:00
github-actions[bot]
c45ad9710d chore: update pre-commit hooks (#140) 2025-06-30 07:52:04 +03:00
cf7ca2109f feat: add bats tests, docs (#139)
* fix(test): ensure bats file list uses xargs

* docs(readme): use yarn for testing instructions

* fix(test): ensure pipelines fail properly

* docs(alias): fix table header

---------

Signed-off-by: Ismo Vuorinen <ismo@ivuorinen.net>
2025-06-30 04:30:06 +03:00
123 changed files with 2323 additions and 1646 deletions

View File

@@ -28,7 +28,7 @@ indent_size = 1
indent_size = 1
indent_style = tab
[{local/bin/*,local/dfm/*,**/*.sh,**/zshrc,config/*,scripts/*}]
[{local/bin/*,**/*.sh,**/zshrc,config/*,scripts/*}]
indent_size = 2
tab_width = 2
shell_variant = bash # --language-variant

5
.gitattributes vendored
View File

@@ -85,6 +85,7 @@ LICENSE text
NEWS text
readme text
*README* text
# Files literally named "TODO", not a todo list item
TODO text
# Templates
@@ -122,7 +123,8 @@ package.json text eol=lf
package-lock.json text eol=lf -diff
pnpm-lock.yaml text eol=lf -diff
.prettierrc text
yarn.lock text -diff
# Ensure yarn.lock shows textual diffs
yarn.lock text eol=lf
*.toml text
*.yaml text
*.yml text
@@ -251,3 +253,4 @@ install text eol=lf diff=shell
*.snippets text eol=lf
*.theme text eol=lf
*.yamlfmt text eol=lf
*.bats text eol=lf diff=shell

30
.github/AGENTS.md vendored Normal file
View File

@@ -0,0 +1,30 @@
# Guidelines for AI contributors
These instructions help language models work with this repository.
## Setup
1. Run `yarn install` to get linting tools and the Bats test framework.
## Formatting
- Format code and docs with Prettier and markdownlint:
```bash
yarn fix:prettier
yarn fix:markdown
```
- Shell scripts should pass `shellcheck`.
## Testing
- When code changes, run `yarn test` to execute Bats tests.
- If only comments or documentation change, tests may be skipped.
## Commits and PRs
- Use Semantic Commit messages: `type(scope): summary`.
- Keep PR titles in the same format.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
ismo@ivuorinen.net.
<ismo@ivuorinen.net>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@@ -116,7 +116,7 @@ the community.
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
@@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
<https://www.contributor-covenant.org/faq>. Translations are available at
<https://www.contributor-covenant.org/translations>.

54
.github/README.md vendored
View File

@@ -16,11 +16,9 @@ see what interesting stuff you've done with it. Sharing is caring.
### First time setup
1. Clone this repository to `$HOME/.dotfiles`
2. Install [shellcheck](https://github.com/koalaman/shellcheck) and [pre-commit](https://pre-commit.com/)
3. Run `pre-commit install` to enable Git hooks
4. `./install`
5. ???
6. Profit
2. `./install`
3. ???
4. Profit
### Updates
@@ -45,6 +43,12 @@ see what interesting stuff you've done with it. Sharing is caring.
| `local/bin` | Helper scripts that I've collected or wrote. |
| `scripts` | Setup scripts. |
### Host specific configuration
Configurations under `hosts/<hostname>` are applied only when running on the
matching machine. Each host folder contains its own `install.conf.yaml` that
is processed by Dotbot during installation.
### dotfile folders
| Repo | Destination | Description |
@@ -60,6 +64,21 @@ see what interesting stuff you've done with it. Sharing is caring.
Running `dfm` gives you a list of available commands.
#### Documentation generation
`dfm docs` generates Markdown documentation under the `docs/` directory. The
subcommands are:
```bash
dfm docs alias # regenerate alias table
dfm docs folders # document interesting folders
dfm docs keybindings # update keybinding docs for tmux, nvim and others
dfm docs all # run every docs task
```
The `docs/` folder contains generated cheat sheets, keybindings and other
reference files. New documentation can be added without modifying this README.
## Configuration
The folder structure follows [XDG Base Directory Specification][xdg] where possible.
@@ -75,6 +94,31 @@ The folder structure follows [XDG Base Directory Specification][xdg] where possi
Please see [docs/folders.md][docs-folders] for more information.
## Managing submodules
This repository uses Git submodules for external dependencies. After cloning,
run:
```bash
git submodule update --init --recursive
```
To pull submodule updates later use:
```bash
git submodule update --remote --merge
```
The helper script `add-submodules.sh` documents how each submodule is added and
configured. Submodules are automatically updated by the
[update-submodules.yml](.github/workflows/update-submodules.yml) workflow.
## Testing
Shell scripts under `local/bin` are validated with [Bats](https://github.com/bats-core/bats-core).
Run `yarn test` to execute every test file. Bats is installed as a development
dependency, so run `yarn install` first if needed.
[dfm]: https://github.com/ivuorinen/dotfiles/blob/main/local/bin/dfm
[docs-folders]: https://github.com/ivuorinen/dotfiles/blob/main/docs/folders.md
[xdg]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

View File

@@ -33,4 +33,4 @@ jobs:
- name: Run PR Lint
# https://github.com/ivuorinen/actions
uses: ivuorinen/actions/pr-lint@9480614ba2231013d99dd5b9c730d2b105b9e160 # 25.6.25
uses: ivuorinen/actions/pr-lint@625c37446b1c7e219755a40807f825c9283f6e05 # 25.7.7

View File

@@ -29,4 +29,4 @@ jobs:
issues: write
steps:
- uses: ivuorinen/actions/sync-labels@99f3911475dbb5b8d43d314b24c0882997433868 # 25.6.23
- uses: ivuorinen/actions/sync-labels@625c37446b1c7e219755a40807f825c9283f6e05 # 25.7.7

View File

@@ -39,7 +39,7 @@ repos:
- id: shellcheck
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.11.0-1
rev: v3.12.0-1
hooks:
- id: shfmt
@@ -49,7 +49,7 @@ repos:
- id: actionlint
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 41.11.1
rev: 41.23.5
hooks:
- id: renovate-config-validator

34
AGENTS.md Normal file
View File

@@ -0,0 +1,34 @@
# Project guidelines
This repository contains configuration files and helper scripts for managing a development environment. Dotbot drives the installation and host specific folders under `hosts/` include extra configs.
## Keeping the repository up to date
1. Update submodules with `git submodule update --remote --merge`.
2. Pull the latest changes and run `./install`.
3. Run `yarn install` whenever `package.json` changes.
## Linting and tests
- Format files with:
```bash
yarn fix:prettier
yarn fix:markdown
```
- Shell scripts must pass `shellcheck`. Run:
```bash
find . -path ./node_modules -prune -o -name '*.sh' -print0 | xargs -0 shellcheck
```
- Execute tests with `yarn test`.
## Debugging lint issues
- `yarn lint:prettier` and `yarn lint:markdown` show formatting errors.
- Ensure shell scripts have a shebang or `# shellcheck shell=bash` directive.
- Consult `.shellcheckrc` for project specific checks.
Scripts rely on helpers in `config/shared.sh` so they run under Bash, Zsh and Fish by default.

View File

@@ -1,8 +1,6 @@
// These are golang packages I use,
// so they should be available with all versions
// Aqua is a package manager like asdf, or Brew
github.com/aquaproj/aqua/v2/cmd/aqua@latest
// Git Profile allows you to switch between user profiles in git repos
github.com/dotzero/git-profile@v1.4.0
// An extensible command line tool or library to format yaml files.
@@ -21,3 +19,8 @@ github.com/rhysd/actionlint/cmd/actionlint@v1.7.1
github.com/doron-cohen/antidot@v0.6.3
// FZF is a general-purpose command-line fuzzy finder.
github.com/junegunn/fzf@latest
// gopls, the Go language server
golang.org/x/tools/gopls@latest
// A language for writing HTML user interfaces in Go.
github.com/a-h/templ/cmd/templ@latest

View File

@@ -1,4 +1,5 @@
### Do not edit. This was autogenerated by 'asdf direnv setup' ###
# shellcheck shell=bash
use_asdf() {
source_env "$(asdf direnv envrc "$@")"
}

View File

@@ -0,0 +1,8 @@
complete fzf_configure_bindings --no-files
complete fzf_configure_bindings --long help --short h --description "Print help" --condition "not __fish_seen_argument --help -h"
complete fzf_configure_bindings --long directory --description "Change the key binding for Search Directory" --condition "not __fish_seen_argument --directory"
complete fzf_configure_bindings --long git_log --description "Change the key binding for Search Git Log" --condition "not __fish_seen_argument --git_log"
complete fzf_configure_bindings --long git_status --description "Change the key binding for Search Git Status" --condition "not __fish_seen_argument --git_status"
complete fzf_configure_bindings --long history --description "Change the key binding for Search History" --condition "not __fish_seen_argument --history"
complete fzf_configure_bindings --long processes --description "Change the key binding for Search Processes" --condition "not __fish_seen_argument --processes"
complete fzf_configure_bindings --long variables --description "Change the key binding for Search Variables" --condition "not __fish_seen_argument --variables"

View File

@@ -0,0 +1,28 @@
# fzf.fish is only meant to be used in interactive mode. If not in interactive mode and not in CI, skip the config to speed up shell startup
if not status is-interactive && test "$CI" != true
exit
end
# Because of scoping rules, to capture the shell variables exactly as they are, we must read
# them before even executing _fzf_search_variables. We use psub to store the
# variables' info in temporary files and pass in the filenames as arguments.
# This variable is global so that it can be referenced by fzf_configure_bindings and in tests
set --global _fzf_search_vars_command '_fzf_search_variables (set --show | psub) (set --names | psub)'
# Install the default bindings, which are mnemonic and minimally conflict with fish's preset bindings
fzf_configure_bindings
# Doesn't erase autoloaded _fzf_* functions because they are not easily accessible once key bindings are erased
function _fzf_uninstall --on-event fzf_uninstall
_fzf_uninstall_bindings
set --erase _fzf_search_vars_command
functions --erase _fzf_uninstall _fzf_migration_message _fzf_uninstall_bindings fzf_configure_bindings
complete --erase fzf_configure_bindings
set_color cyan
echo "fzf.fish uninstalled."
echo "You may need to manually remove fzf_configure_bindings from your config.fish if you were using custom key bindings."
set_color normal
end

View File

@@ -8,3 +8,4 @@ edc/bass
meaningful-ooo/sponge
nickeb96/puffer-fish
jgusta/paths
patrickf1/fzf.fish

View File

@@ -0,0 +1,43 @@
function _fzf_configure_bindings_help --description "Prints the help message for fzf_configure_bindings."
echo "\
USAGE:
fzf_configure_bindings [--COMMAND=[KEY_SEQUENCE]...]
DESCRIPTION
fzf_configure_bindings installs key bindings for fzf.fish's commands and erases any bindings it
previously installed. It installs bindings for both default and insert modes. fzf.fish executes
it without options on fish startup to install the out-of-the-box key bindings.
By default, commands are bound to a mnemonic key sequence, shown below. Each command's binding
can be configured using a namesake corresponding option:
COMMAND | DEFAULT KEY SEQUENCE | CORRESPONDING OPTION
Search Directory | Ctrl+Alt+F (F for file) | --directory
Search Git Log | Ctrl+Alt+L (L for log) | --git_log
Search Git Status | Ctrl+Alt+S (S for status) | --git_status
Search History | Ctrl+R (R for reverse) | --history
Search Processes | Ctrl+Alt+P (P for process) | --processes
Search Variables | Ctrl+V (V for variable) | --variables
Override a command's binding by specifying its corresponding option with the desired key
sequence. Disable a command's binding by specifying its corresponding option with no value.
Because fzf_configure_bindings erases bindings it previously installed, it can be cleanly
executed multiple times. Once the desired fzf_configure_bindings command has been found, add it
to your config.fish in order to persist the customized bindings.
In terms of validation, fzf_configure_bindings fails if passed unknown options. It expects an
equals sign between an option's name and value. However, it does not validate key sequences.
Pass -h or --help to print this help message and exit.
EXAMPLES
Default bindings but bind Search Directory to Ctrl+F and Search Variables to Ctrl+Alt+V
\$ fzf_configure_bindings --directory=\cf --variables=\e\cv
Default bindings but disable Search History
\$ fzf_configure_bindings --history=
An agglomeration of different options
\$ fzf_configure_bindings --git_status=\cg --history=\ch --variables= --processes=
SEE Also
To learn more about fish key bindings, see bind(1) and fish_key_reader(1).
"
end

View File

@@ -0,0 +1,15 @@
# helper function for _fzf_search_variables
function _fzf_extract_var_info --argument-names variable_name set_show_output --description "Extract and reformat lines pertaining to \$variable_name from \$set_show_output."
# Extract only the lines about the variable, all of which begin with either
# $variable_name: ...or... $variable_name[
string match --regex "^\\\$$variable_name(?::|\[).*" <$set_show_output |
# Strip the variable name prefix, including ": " for scope info lines
string replace --regex "^\\\$$variable_name(?:: )?" '' |
# Distill the lines of values, replacing...
# [1]: |value|
# ...with...
# [1] value
string replace --regex ": \|(.*)\|" ' $1'
end

View File

@@ -0,0 +1,49 @@
# helper for _fzf_search_git_status
# arg should be a line from git status --short, e.g.
# MM functions/_fzf_preview_changed_file.fish
# D README.md
# R LICENSE -> "New License"
function _fzf_preview_changed_file --argument-names path_status --description "Show the git diff of the given file."
# remove quotes because they'll be interpreted literally by git diff
# no need to requote when referencing $path because fish does not perform word splitting
# https://fishshell.com/docs/current/fish_for_bash_users.html
set -f path (string unescape (string sub --start 4 $path_status))
# first letter of short format shows index, second letter shows working tree
# https://git-scm.com/docs/git-status/2.35.0#_short_format
set -f index_status (string sub --length 1 $path_status)
set -f working_tree_status (string sub --start 2 --length 1 $path_status)
set -f diff_opts --color=always
if test $index_status = '?'
_fzf_report_diff_type Untracked
_fzf_preview_file $path
else if contains {$index_status}$working_tree_status DD AU UD UA DU AA UU
# Unmerged statuses taken directly from git status help's short format table
# Unmerged statuses are mutually exclusive with other statuses, so if we see
# these, then safe to assume the path is unmerged
_fzf_report_diff_type Unmerged
git diff $diff_opts -- $path
else
if test $index_status != ' '
_fzf_report_diff_type Staged
# renames are only detected in the index, never working tree, so only need to test for it here
# https://stackoverflow.com/questions/73954214
if test $index_status = R
# diff the post-rename path with the original path, otherwise the diff will show the entire file as being added
set -f orig_and_new_path (string split --max 1 -- ' -> ' $path)
git diff --staged $diff_opts -- $orig_and_new_path[1] $orig_and_new_path[2]
# path currently has the form of "original -> current", so we need to correct it before it's used below
set path $orig_and_new_path[2]
else
git diff --staged $diff_opts -- $path
end
end
if test $working_tree_status != ' '
_fzf_report_diff_type Unstaged
git diff $diff_opts -- $path
end
end
end

View File

@@ -0,0 +1,43 @@
# helper function for _fzf_search_directory and _fzf_search_git_status
function _fzf_preview_file --description "Print a preview for the given file based on its file type."
# because there's no way to guarantee that _fzf_search_directory passes the path to _fzf_preview_file
# as one argument, we collect all the arguments into one single variable and treat that as the path
set -f file_path $argv
if test -L "$file_path" # symlink
# notify user and recurse on the target of the symlink, which can be any of these file types
set -l target_path (realpath "$file_path")
set_color yellow
echo "'$file_path' is a symlink to '$target_path'."
set_color normal
_fzf_preview_file "$target_path"
else if test -f "$file_path" # regular file
if set --query fzf_preview_file_cmd
# need to escape quotes to make sure eval receives file_path as a single arg
eval "$fzf_preview_file_cmd '$file_path'"
else
bat --style=numbers --color=always "$file_path"
end
else if test -d "$file_path" # directory
if set --query fzf_preview_dir_cmd
# see above
eval "$fzf_preview_dir_cmd '$file_path'"
else
# -A list hidden files as well, except for . and ..
# -F helps classify files by appending symbols after the file name
command ls -A -F "$file_path"
end
else if test -c "$file_path"
_fzf_report_file_type "$file_path" "character device file"
else if test -b "$file_path"
_fzf_report_file_type "$file_path" "block device file"
else if test -S "$file_path"
_fzf_report_file_type "$file_path" socket
else if test -p "$file_path"
_fzf_report_file_type "$file_path" "named pipe"
else
echo "$file_path doesn't exist." >&2
end
end

View File

@@ -0,0 +1,18 @@
# helper for _fzf_preview_changed_file
# prints out something like
# ╭────────╮
# │ Staged │
# ╰────────╯
function _fzf_report_diff_type --argument-names diff_type --description "Print a distinct colored header meant to preface a git patch."
# number of "-" to draw is the length of the string to box + 2 for padding
set -f repeat_count (math 2 + (string length $diff_type))
set -f line (string repeat --count $repeat_count)
set -f top_border$line
set -f btm_border$line
set_color yellow
echo $top_border
echo "$diff_type"
echo $btm_border
set_color normal
end

View File

@@ -0,0 +1,6 @@
# helper function for _fzf_preview_file
function _fzf_report_file_type --argument-names file_path file_type --description "Explain the file type for a file."
set_color red
echo "Cannot preview '$file_path': it is a $file_type."
set_color normal
end

View File

@@ -0,0 +1,33 @@
function _fzf_search_directory --description "Search the current directory. Replace the current token with the selected file paths."
# Directly use fd binary to avoid output buffering delay caused by a fd alias, if any.
# Debian-based distros install fd as fdfind and the fd package is something else, so
# check for fdfind first. Fall back to "fd" for a clear error message.
set -f fd_cmd (command -v fdfind || command -v fd || echo "fd")
set -f --append fd_cmd --color=always $fzf_fd_opts
set -f fzf_arguments --multi --ansi $fzf_directory_opts
set -f token (commandline --current-token)
# expand any variables or leading tilde (~) in the token
set -f expanded_token (eval echo -- $token)
# unescape token because it's already quoted so backslashes will mess up the path
set -f unescaped_exp_token (string unescape -- $expanded_token)
# If the current token is a directory and has a trailing slash,
# then use it as fd's base directory.
if string match --quiet -- "*/" $unescaped_exp_token && test -d "$unescaped_exp_token"
set --append fd_cmd --base-directory=$unescaped_exp_token
# use the directory name as fzf's prompt to indicate the search is limited to that directory
set --prepend fzf_arguments --prompt="Directory $unescaped_exp_token> " --preview="_fzf_preview_file $expanded_token{}"
set -f file_paths_selected $unescaped_exp_token($fd_cmd 2>/dev/null | _fzf_wrapper $fzf_arguments)
else
set --prepend fzf_arguments --prompt="Directory> " --query="$unescaped_exp_token" --preview='_fzf_preview_file {}'
set -f file_paths_selected ($fd_cmd 2>/dev/null | _fzf_wrapper $fzf_arguments)
end
if test $status -eq 0
commandline --current-token --replace -- (string escape -- $file_paths_selected | string join ' ')
end
commandline --function repaint
end

View File

@@ -0,0 +1,36 @@
function _fzf_search_git_log --description "Search the output of git log and preview commits. Replace the current token with the selected commit hash."
if not git rev-parse --git-dir >/dev/null 2>&1
echo '_fzf_search_git_log: Not in a git repository.' >&2
else
if not set --query fzf_git_log_format
# %h gives you the abbreviated commit hash, which is useful for saving screen space, but we will have to expand it later below
set -f fzf_git_log_format '%C(bold blue)%h%C(reset) - %C(cyan)%ad%C(reset) %C(yellow)%d%C(reset) %C(normal)%s%C(reset) %C(dim normal)[%an]%C(reset)'
end
set -f preview_cmd 'git show --color=always --stat --patch {1}'
if set --query fzf_diff_highlighter
set preview_cmd "$preview_cmd | $fzf_diff_highlighter"
end
set -f selected_log_lines (
git log --no-show-signature --color=always --format=format:$fzf_git_log_format --date=short | \
_fzf_wrapper --ansi \
--multi \
--scheme=history \
--prompt="Git Log> " \
--preview=$preview_cmd \
--query=(commandline --current-token) \
$fzf_git_log_opts
)
if test $status -eq 0
for line in $selected_log_lines
set -f abbreviated_commit_hash (string split --field 1 " " $line)
set -f full_commit_hash (git rev-parse $abbreviated_commit_hash)
set -f --append commit_hashes $full_commit_hash
end
commandline --current-token --replace (string join ' ' $commit_hashes)
end
end
commandline --function repaint
end

View File

@@ -0,0 +1,41 @@
function _fzf_search_git_status --description "Search the output of git status. Replace the current token with the selected file paths."
if not git rev-parse --git-dir >/dev/null 2>&1
echo '_fzf_search_git_status: Not in a git repository.' >&2
else
set -f preview_cmd '_fzf_preview_changed_file {}'
if set --query fzf_diff_highlighter
set preview_cmd "$preview_cmd | $fzf_diff_highlighter"
end
set -f selected_paths (
# Pass configuration color.status=always to force status to use colors even though output is sent to a pipe
git -c color.status=always status --short |
_fzf_wrapper --ansi \
--multi \
--prompt="Git Status> " \
--query=(commandline --current-token) \
--preview=$preview_cmd \
--nth="2.." \
$fzf_git_status_opts
)
if test $status -eq 0
# git status --short automatically escapes the paths of most files for us so not going to bother trying to handle
# the few edges cases of weird file names that should be extremely rare (e.g. "this;needs;escaping")
set -f cleaned_paths
for path in $selected_paths
if test (string sub --length 1 $path) = R
# path has been renamed and looks like "R LICENSE -> LICENSE.md"
# extract the path to use from after the arrow
set --append cleaned_paths (string split -- "-> " $path)[-1]
else
set --append cleaned_paths (string sub --start=4 $path)
end
end
commandline --current-token --replace -- (string join ' ' $cleaned_paths)
end
end
commandline --function repaint
end

View File

@@ -0,0 +1,39 @@
function _fzf_search_history --description "Search command history. Replace the command line with the selected command."
# history merge incorporates history changes from other fish sessions
# it errors out if called in private mode
if test -z "$fish_private_mode"
builtin history merge
end
if not set --query fzf_history_time_format
# Reference https://devhints.io/strftime to understand strftime format symbols
set -f fzf_history_time_format "%m-%d %H:%M:%S"
end
# Delinate time from command in history entries using the vertical box drawing char (U+2502).
# Then, to get raw command from history entries, delete everything up to it. The ? on regex is
# necessary to make regex non-greedy so it won't match into commands containing the char.
set -f time_prefix_regex '^.*? │ '
# Delinate commands throughout pipeline using null rather than newlines because commands can be multi-line
set -f commands_selected (
builtin history --null --show-time="$fzf_history_time_format" |
_fzf_wrapper --read0 \
--print0 \
--multi \
--scheme=history \
--prompt="History> " \
--query=(commandline) \
--preview="string replace --regex '$time_prefix_regex' '' -- {} | fish_indent --ansi" \
--preview-window="bottom:3:wrap" \
$fzf_history_opts |
string split0 |
# remove timestamps from commands selected
string replace --regex $time_prefix_regex ''
)
if test $status -eq 0
commandline --replace -- $commands_selected
end
commandline --function repaint
end

View File

@@ -0,0 +1,32 @@
function _fzf_search_processes --description "Search all running processes. Replace the current token with the pid of the selected process."
# Directly use ps command because it is often aliased to a different command entirely
# or with options that dirty the search results and preview output
set -f ps_cmd (command -v ps || echo "ps")
# use all caps to be consistent with ps default format
# snake_case because ps doesn't seem to allow spaces in the field names
set -f ps_preview_fmt (string join ',' 'pid' 'ppid=PARENT' 'user' '%cpu' 'rss=RSS_IN_KB' 'start=START_TIME' 'command')
set -f processes_selected (
$ps_cmd -A -opid,command | \
_fzf_wrapper --multi \
--prompt="Processes> " \
--query (commandline --current-token) \
--ansi \
# first line outputted by ps is a header, so we need to mark it as so
--header-lines=1 \
# ps uses exit code 1 if the process was not found, in which case show an message explaining so
--preview="$ps_cmd -o '$ps_preview_fmt' -p {1} || echo 'Cannot preview {1} because it exited.'" \
--preview-window="bottom:4:wrap" \
$fzf_processes_opts
)
if test $status -eq 0
for process in $processes_selected
set -f --append pids_selected (string split --no-empty --field=1 -- " " $process)
end
# string join to replace the newlines outputted by string split with spaces
commandline --current-token --replace -- (string join ' ' $pids_selected)
end
commandline --function repaint
end

View File

@@ -0,0 +1,47 @@
# This function expects the following two arguments:
# argument 1 = output of (set --show | psub), i.e. a file with the scope info and values of all variables
# argument 2 = output of (set --names | psub), i.e. a file with all variable names
function _fzf_search_variables --argument-names set_show_output set_names_output --description "Search and preview shell variables. Replace the current token with the selected variable."
if test -z "$set_names_output"
printf '%s\n' '_fzf_search_variables requires 2 arguments.' >&2
commandline --function repaint
return 22 # 22 means invalid argument in POSIX
end
# Exclude the history variable from being piped into fzf because
# 1. it's not included in $set_names_output
# 2. it tends to be a very large value => increases computation time
# 3._fzf_search_history is a much better way to examine history anyway
set -f all_variable_names (string match --invert history <$set_names_output)
set -f current_token (commandline --current-token)
# Use the current token to pre-populate fzf's query. If the current token begins
# with a $, remove it from the query so that it will better match the variable names
set -f cleaned_curr_token (string replace -- '$' '' $current_token)
set -f variable_names_selected (
printf '%s\n' $all_variable_names |
_fzf_wrapper --preview "_fzf_extract_var_info {} $set_show_output" \
--prompt="Variables> " \
--preview-window="wrap" \
--multi \
--query=$cleaned_curr_token \
$fzf_variables_opts
)
if test $status -eq 0
# If the current token begins with a $, do not overwrite the $ when
# replacing the current token with the selected variable.
# Uses brace expansion to prepend $ to each variable name.
commandline --current-token --replace (
if string match --quiet -- '$*' $current_token
string join " " \${$variable_names_selected}
else
string join " " $variable_names_selected
end
)
end
commandline --function repaint
end

View File

@@ -0,0 +1,21 @@
function _fzf_wrapper --description "Prepares some environment variables before executing fzf."
# Make sure fzf uses fish to execute preview commands, some of which
# are autoloaded fish functions so don't exist in other shells.
# Use --function so that it doesn't clobber SHELL outside this function.
set -f --export SHELL (command --search fish)
# If neither FZF_DEFAULT_OPTS nor FZF_DEFAULT_OPTS_FILE are set, then set some sane defaults.
# See https://github.com/junegunn/fzf#environment-variables
set --query FZF_DEFAULT_OPTS FZF_DEFAULT_OPTS_FILE
if test $status -eq 2
# cycle allows jumping between the first and last results, making scrolling faster
# layout=reverse lists results top to bottom, mimicking the familiar layouts of git log, history, and env
# border shows where the fzf window begins and ends
# height=90% leaves space to see the current command and some scrollback, maintaining context of work
# preview-window=wrap wraps long lines in the preview window, making reading easier
# marker=* makes the multi-select marker more distinguishable from the pointer (since both default to >)
set --export FZF_DEFAULT_OPTS '--cycle --layout=reverse --border --height=90% --preview-window=wrap --marker="*"'
end
fzf $argv
end

View File

@@ -0,0 +1,303 @@
# cr.fish — Create and manage code review worktrees for Fish shell
#
# Synopsis:
# cr [OPTIONS] <source-branch>
# cr cleanup [OPTIONS]
#
# Description:
# Create a dedicated worktree for reviewing code based on a ticket ID
# extracted from a branch name, or clean up existing cr- worktrees.
#
# Constants:
# CR_DEFAULT_REMOTE Default Git remote used when none specified
#
# Fish Requirements:
# Fish shell >= 3.1.0 for argparse builtin
#
# Based on work by ville6000 (https://github.com/ville6000)
#
# Examples:
# cr feature/1234-add-login
# cr -r upstream 5678
# cr cleanup --dry-run
if not set -q CR_DEFAULT_REMOTE
set -g CR_DEFAULT_REMOTE origin
end
function __cr_show_help
echo 'Usage: cr [OPTIONS] <source-branch>'
echo ' cr cleanup [OPTIONS]'
echo
echo " -r, --remote <name> Git remote (default: $CR_DEFAULT_REMOTE)"
echo ' -d, --dry-run Show actions without executing'
echo ' -f, --force Skip confirmation prompts'
echo ' -k, --keep-branch In cleanup, keep local branches'
echo ' -b, --branch-only <ticket> Create only the review branch (no worktree)'
echo ' --cleanup-branches-only In cleanup, delete branches only'
echo ' -h, --help Show this help'
end
function __cr_run_with_spinner --argument-names msg cmd
set -l tmp (mktemp)
eval $cmd >$tmp 2>&1 &
set -l pid $last_pid
set -l spin_chars '/-\|'
set -l i 1
while kill -0 $pid 2>/dev/null
printf "\r[%c] %s" (string sub -s $i -l 1 $spin_chars) "$msg"
set i (math (math $i % 4) + 1)
sleep 0.1
end
printf "\r%s\r" (string repeat -n (math (string length "$msg") + 5) " ")
wait $pid
set -l output (cat $tmp)
rm $tmp
# Remove any leading empty or all-whitespace lines (from spinner clear)
for line in $output
if test -n (string trim -- $line)
echo $line
end
end
end
function __cr_cleanup
argparse dry_run force keep_branch cleanup_branches_only -- $argv
or return
set -l dry_run (set -q _flag_dry_run; and echo 1; or echo 0)
set -l force (set -q _flag_force; and echo 1; or echo 0)
set -l keep_branch (set -q _flag_keep_branch; and echo 1; or echo 0)
set -l branches_only (set -q _flag_cleanup_branches_only; and echo 1; or echo 0)
set -l target_branch
if test (count $argv) -gt 0
set target_branch $argv[1]
end
git worktree prune
for wt in (git worktree list --porcelain | awk '/^worktree /{print $2}')
set -l base (basename $wt)
if string match -r '^cr-.*' $base
if test -n "$target_branch" -a "$base" != "$target_branch"
continue
end
if test "$branches_only" = 0
if test "$dry_run" = 1
echo "[DRY-RUN] remove worktree: $wt"
else if test "$force" = 1
git worktree remove --force $wt
else
echo "git worktree remove --force $wt"
echo "(!) Use --force to actually remove worktree: $wt."
end
end
if test "$keep_branch" = 0
if test "$branches_only" = 0
if test "$dry_run" = 1
echo "[DRY-RUN] delete branch: $base"
else if test "$force" = 1
git branch -D $base
else
echo "git branch -D $base"
echo "(!) Use --force to actually delete branch: $base."
end
end
end
end
end
end
# --- Main Entrypoint ---
function cr --description 'Create or cleanup code-review worktrees'
if not git rev-parse --is-inside-work-tree >/dev/null 2>&1
echo "Not inside a git repository." >&2
return 1
end
set -l remotes (git remote)
if test (count $remotes) -eq 0
echo "No git remotes found. Please add a remote before using cr." >&2
return 1
end
if not type -q argparse
echo 'cr.fish requires the argparse builtin' >&2
return 1
end
argparse \
r/remote= \
h/help \
d/dry-run \
f/force \
y/yes \
k/keep-branch \
b/branch-only= \
cleanup-branches-only \
-- $argv
or return
if set -q _flag_h; or set -q _flag_help
__cr_show_help
return 0
end
set -l remote (set -q _flag_remote; and echo $_flag_remote[-1]; or echo $CR_DEFAULT_REMOTE)
if set -q _flag_yes
set -g _flag_force 1
end
if not contains $remote $remotes
echo "Remote '$remote' does not exist. Available remotes: $remotes" >&2
return 1
end
if test (count $argv) -gt 0 -a "$argv[1]" = cleanup
__cr_cleanup \
(set -q _flag_dry_run; and echo --dry_run) \
(set -q _flag_force; and echo --force) \
(set -q _flag_keep_branch; and echo --keep_branch) \
(set -q _flag_cleanup_branches_only; and echo --cleanup_branches_only)
return 0
end
set -l source_branch ""
if test (count $argv) -gt 0
set source_branch $argv[1]
else if type -q fzf
set -l branches (__cr_run_with_spinner "Fetching branches..." \
"git ls-remote --heads $remote | sed 's|.*refs/heads/||'")
printf "\n"
if test (count $branches) -eq 0
echo "No branches found on remote '$remote'." >&2
return 1
end
set -l exclude_branches main master develop dev trunk
set -l filtered_branches
for b in $branches
set b (string trim -- $b)
if not contains -- $b $exclude_branches
set filtered_branches $filtered_branches $b
end
end
set source_branch (printf '%s\n' $filtered_branches | fzf --prompt='Select branch: ')
echo "Selected branch: $source_branch"
if test $status -ne 0
echo 'Selection aborted' >&2
return 1
end
if test -z "$source_branch"
echo 'No branch selected' >&2
return 1
end
else
__cr_show_help >&2
return 1
end
# Extract ticket ID from branch name, or use slug if not found
set -l branch_tail (string split "/" $source_branch)[-1]
set -l ticket_id (printf '%s\n' $branch_tail | grep -o '[0-9]\+' | tail -n1)
set -l review_suffix ""
if test -z "$ticket_id"
# No numeric ticket, use slug of branch name as suffix
set review_suffix (string replace -ra '[^a-zA-Z0-9]+' '-' -- $source_branch)
else
set review_suffix $ticket_id
end
if set -q _flag_b; or set -q _flag_branch_only
set review_suffix $_flag_branch_only[-1]
end
set -l review_branch cr-$review_suffix
set -l folder ../$review_branch
# Branch-only mode
if set -q _flag_b; or set -q _flag_branch_only
if set -q _flag_dry_run
echo "[DRY-RUN] Create branch $review_branch from $remote/$source_branch"
return 0
end
__cr_run_with_spinner "Fetching from $remote..." \
"git fetch $remote $source_branch"
git branch $review_branch $remote/$source_branch
echo "Created branch $review_branch"
return 0
end
__cr_run_with_spinner "Checking remote branch..." \
"git ls-remote --exit-code --heads $remote $source_branch >/dev/null 2>&1"
if test $status -ne 0
echo "No remote branch $remote/$source_branch" >&2
return 1
end
if git show-ref --quiet refs/heads/$review_branch
echo "Local branch $review_branch exists" >&2
return 1
end
if test -d $folder
echo "Directory $folder exists" >&2
return 1
end
if set -q _flag_dry_run
echo "[DRY-RUN] Add worktree $folder -b $review_branch $remote/$source_branch"
return 0
end
__cr_run_with_spinner "Fetching from $remote..." \
"git fetch $remote $source_branch"
git worktree add $folder -b $review_branch $remote/$source_branch
end
# --- Completion Functions ---
complete -c cr -l help -s h -f -d 'Show help'
complete -c cr -l remote -s r -f -d 'Git remote' -a '(git remote)'
complete -c cr -l dry-run -s d -f -d 'Dry run'
complete -c cr -l force -s f -f -d 'Skip confirmations'
complete -c cr -l keep-branch -s k -f -d 'Keep branches in cleanup'
complete -c cr -l branch-only -s b -f -d 'Branch-only mode' \
-a '(__fish_cr_ticket_ids)'
complete -c cr -l cleanup-branches-only -f -d 'Branches-only cleanup'
complete -c cr -f -a cleanup -d 'Cleanup mode' \
-n 'not __fish_seen_subcommand_from cleanup'
complete -c cr -n '__fish_seen_subcommand_from cleanup' -f \
-a '(__fish_cr_cleanup_branches)' -d 'cr-* branch'
complete -c cr -n '__fish_seen_subcommand_from cleanup' -f
complete -c cr -n 'not __fish_seen_subcommand_from cleanup' -f \
-a '(__fish_cr_branches)' -d 'Source branch'
function __fish_cr_cleanup_branches
git worktree list --porcelain | awk '/^worktree /{print $2}' | while read -l wt
set base (basename $wt)
if string match -r '^cr-.*' $base
echo $base
end
end
end
function __fish_cr_branches --description 'List remote branches'
set -l exclude_branches main master develop dev trunk
set -l branches (git ls-remote --heads $CR_DEFAULT_REMOTE | \
sed 's|.*refs/heads/||')
for b in $branches
set b (string trim -- $b)
if not contains -- $b $exclude_branches
echo $b
end
end
end
function __fish_cr_ticket_ids --description 'List ticket IDs from remote branches'
for b in (git ls-remote --heads $CR_DEFAULT_REMOTE | \
sed 's|.*refs/heads/||')
set b (string trim -- $b)
set -l id (string match -r '[0-9]+' -- $b)
if test -n "$id"
echo $id
end
end
end

View File

@@ -0,0 +1,46 @@
# Always installs bindings for insert and default mode for simplicity and b/c it has almost no side-effect
# https://gitter.im/fish-shell/fish-shell?at=60a55915ee77a74d685fa6b1
function fzf_configure_bindings --description "Installs the default key bindings for fzf.fish with user overrides passed as options."
# no need to install bindings if not in interactive mode or running tests
status is-interactive || test "$CI" = true; or return
set -f options_spec h/help 'directory=?' 'git_log=?' 'git_status=?' 'history=?' 'processes=?' 'variables=?'
argparse --max-args=0 --ignore-unknown $options_spec -- $argv 2>/dev/null
if test $status -ne 0
echo "Invalid option or a positional argument was provided." >&2
_fzf_configure_bindings_help
return 22
else if set --query _flag_help
_fzf_configure_bindings_help
return
else
# Initialize with default key sequences and then override or disable them based on flags
# index 1 = directory, 2 = git_log, 3 = git_status, 4 = history, 5 = processes, 6 = variables
set -f key_sequences \e\cf \e\cl \e\cs \cr \e\cp \cv # \c = control, \e = escape
set --query _flag_directory && set key_sequences[1] "$_flag_directory"
set --query _flag_git_log && set key_sequences[2] "$_flag_git_log"
set --query _flag_git_status && set key_sequences[3] "$_flag_git_status"
set --query _flag_history && set key_sequences[4] "$_flag_history"
set --query _flag_processes && set key_sequences[5] "$_flag_processes"
set --query _flag_variables && set key_sequences[6] "$_flag_variables"
# If fzf bindings already exists, uninstall it first for a clean slate
if functions --query _fzf_uninstall_bindings
_fzf_uninstall_bindings
end
for mode in default insert
test -n $key_sequences[1] && bind --mode $mode $key_sequences[1] _fzf_search_directory
test -n $key_sequences[2] && bind --mode $mode $key_sequences[2] _fzf_search_git_log
test -n $key_sequences[3] && bind --mode $mode $key_sequences[3] _fzf_search_git_status
test -n $key_sequences[4] && bind --mode $mode $key_sequences[4] _fzf_search_history
test -n $key_sequences[5] && bind --mode $mode $key_sequences[5] _fzf_search_processes
test -n $key_sequences[6] && bind --mode $mode $key_sequences[6] "$_fzf_search_vars_command"
end
function _fzf_uninstall_bindings --inherit-variable key_sequences
bind --erase -- $key_sequences
bind --erase --mode insert -- $key_sequences
end
end
end

View File

@@ -1,7 +1,22 @@
return {
{
'rmehri01/onenord.nvim',
opts = {},
'rose-pine/neovim',
name = 'rose-pine',
opts = {
dim_inactive_windows = false,
extend_background_behind_borders = true,
styles = {
bold = true,
italic = true,
transparency = true,
},
enable = {
terminal = true,
legacy_highlights = true, -- Improve compatibility for previous versions of Neovim
migrations = true, -- Handle deprecated options automatically
},
},
config = function() vim.cmd 'colorscheme rose-pine' end,
},
-- Automatic dark mode
-- https://github.com/f-person/auto-dark-mode.nvim
@@ -12,10 +27,12 @@ return {
set_dark_mode = function()
vim.api.nvim_set_option_value('background', 'dark', {})
-- vim.cmd.colorscheme(vim.g.colors_variant_dark)
vim.cmd 'colorscheme rose-pine'
end,
set_light_mode = function()
vim.api.nvim_set_option_value('background', 'light', {})
-- vim.cmd.colorscheme(vim.g.colors_variant_light)
vim.cmd 'colorscheme rose-pine-dawn'
end,
},
},
@@ -68,6 +85,42 @@ return {
'm4xshen/hardtime.nvim',
lazy = false,
dependencies = { 'MunifTanjim/nui.nvim' },
opts = {},
opts = {
restriction_mode = 'hint',
disabled_keys = {
['<Up>'] = { '', 'n' },
['<Down>'] = { '', 'n' },
['<Left>'] = { '', 'n' },
['<Right>'] = { '', 'n' },
['<C-Up>'] = { '', 'n' },
['<C-Down>'] = { '', 'n' },
['<C-Left>'] = { '', 'n' },
['<C-Right>'] = { '', 'n' },
},
disabled_filetypes = {
'TelescopePrompt',
'Trouble',
'lazy',
'mason',
'help',
'notify',
'dashboard',
'alpha',
},
hints = {
['[dcyvV][ia][%(%)]'] = {
message = function(keys)
return 'Use ' .. keys:sub(1, 2) .. 'b instead of ' .. keys
end,
length = 3,
},
['[dcyvV][ia][%{%}]'] = {
message = function(keys)
return 'Use ' .. keys:sub(1, 2) .. 'B instead of ' .. keys
end,
length = 3,
},
},
},
},
}

View File

@@ -1,2 +1,3 @@
# shellcheck shell=bash
export OP_PLUGIN_ALIASES_SOURCED=1
alias gh="op plugin run -- gh"

View File

@@ -1 +1,7 @@
[{"account_id":"S5Z2DMNFKJEZBPCWRHRWC4DCGI","vault_id":"injcin7obv3jdet3r2u3kfihfy","item_id":"f6vinbnc6l7ngdzvlw66ayewlq"}]
[
{
"account_id": "S5Z2DMNFKJEZBPCWRHRWC4DCGI",
"vault_id": "injcin7obv3jdet3r2u3kfihfy",
"item_id": "f6vinbnc6l7ngdzvlw66ayewlq"
}
]

View File

@@ -26,7 +26,7 @@ x-path-prepend()
local dir=$1
case "$CURRENT_SHELL" in
fish)
set -U fish_user_paths "$dir" $fish_user_paths
set -U fish_user_paths "$dir" "$fish_user_paths"
;;
sh | bash | zsh)
PATH="$dir:$PATH"
@@ -106,7 +106,7 @@ if ! declare -f msg_done > /dev/null; then
# $1 - message (string)
msg_done()
{
msgr done "$1"
msgr "done" "$1"
return 0
}
fi

View File

@@ -25,6 +25,7 @@ yabai -m config \
# > yabai -m query --windows | jq .[].app | sort | uniq
yabai -m rule --add app="1Password" manage=off
yabai -m rule --add app="Fork" manage=off
yabai -m rule --add app="GoLand" manage=off
yabai -m rule --add app="JetBrains Rider" manage=off
yabai -m rule --add app="Logi Options" manage=off
yabai -m rule --add app="MSTeams" manage=off

View File

@@ -1,6 +1,7 @@
{
"context_servers": {
"github-activity-summarizer": {
"source": "extension",
"settings": {}
}
},
@@ -8,12 +9,13 @@
"metrics": false
},
"agent": {
"always_allow_tool_actions": false,
"always_allow_tool_actions": true,
"default_profile": "ask",
"default_model": {
"provider": "copilot_chat",
"model": "gpt-4.1"
},
"version": "2"
"play_sound_when_agent_done": true
},
"languages": {
"PHP": {
@@ -77,7 +79,7 @@
"update_debounce_ms": 150
}
},
"multi_cursor_modifier": "cmd_or_ctrl", // alias: "cmd", "ctrl"
"multi_cursor_modifier": "cmd_or_ctrl",
"indent_guides": {
"enabled": true,
"coloring": "indent_aware"
@@ -89,8 +91,8 @@
"vim_mode": true,
"theme": {
"mode": "system",
"light": "Tomorrow",
"dark": "Tomorrow at Midnight"
"light": "Rosé Pine Dawn",
"dark": "Rosé Pine"
},
"inlay_hints": {
"enabled": true,
@@ -102,7 +104,6 @@
"buffer_font_size": 16,
"buffer_font_fallbacks": ["JetBrainsMono Nerd Font"],
"use_autoclose": false,
"hour_format": "hour24",
"auto_install_extensions": {
"angular": true,
"ansible": true,
@@ -114,9 +115,9 @@
"dockerfile": true,
"git-firefly": true,
"github-activity-summarizer": true,
"go-snippets": true,
"golangci-lint": true,
"gosum": true,
"go-snippets": true,
"html": true,
"ini": true,
"json": true,
@@ -126,19 +127,20 @@
"lua": true,
"make": true,
"php": true,
"python-snippets": true,
"python-requirements": true,
"python-snippets": true,
"rose-pine-theme": true,
"ruff": true,
"scss": true,
"sieve": true,
"stylelint": true,
"sql": true,
"stylelint": true,
"templ": true,
"toml": true,
"vue": true,
"vue-snippets": true,
"wakatime": true,
"xcode-themes": true,
"yaml": true,
"tomorrow-theme": true
"yaml": true
}
}

11
hosts/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Host specific directories
Host folders contain machine specific overrides and an `install.conf.yaml` file that Dotbot processes during setup.
Current hosts:
- **air** personal computer
- **lakka** remote server
- **s** work laptop
- **tunkki** local server
- **v** work desktop

29
local/bin/a.md Normal file
View File

@@ -0,0 +1,29 @@
# a
Encrypt or decrypt files and directories using `age` and your GitHub SSH keys.
## Usage
```bash
a encrypt <file|dir>
a decrypt <file.age|dir>
```
Options:
- `-v`, `--verbose` show log output
Environment variables:
- `AGE_KEYSFILE` location of the keys file
- `AGE_KEYSSOURCE` URL to fetch keys if missing
- `AGE_LOGFILE` log file path
## Example
```bash
a encrypt secret.txt
a decrypt secret.txt.age
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/ad.md Normal file
View File

@@ -0,0 +1,19 @@
# ad
Decrypt a file encrypted with `age` using your GitHub SSH keys.
## Usage
```bash
ad <file.age>
```
Uses `AGE_KEYSFILE` and `AGE_KEYSSOURCE` if keys are missing.
## Example
```bash
ad secret.txt.age
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/ae.md Normal file
View File

@@ -0,0 +1,19 @@
# ae
Encrypt a file with `age` using your GitHub SSH keys.
## Usage
```bash
ae <file>
```
Uses `AGE_KEYSFILE` and `AGE_KEYSSOURCE` if keys are missing.
## Example
```bash
ae secret.txt
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

26
local/bin/dfm.md Normal file
View File

@@ -0,0 +1,26 @@
# dfm
Dotfiles manager and installation helper. Provides wrappers for many
setup tasks defined in this repository.
## Usage
```bash
dfm <command> [options]
```
Common commands include:
- `install` install tools or run platform specific setup
- `brew` manage Homebrew packages
- `docs` regenerate markdown documentation
Set `VERBOSE=1` to see debug output.
### Example
```bash
dfm install all
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

21
local/bin/fzf-tmux.md Normal file
View File

@@ -0,0 +1,21 @@
# fzf-tmux
Wrapper around [`fzf`](https://github.com/junegunn/fzf) that opens the
interface inside a tmux pane or popup.
## Usage
```bash
fzf-tmux [layout options] [--] [fzf options]
```
Layout flags like `-p` or `-d` control popup and split behaviour. Use
`--` to pass arguments directly to `fzf`.
### Example
```bash
fzf-tmux -p 80%,60% -- --reverse
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

16
local/bin/fzf.md Normal file
View File

@@ -0,0 +1,16 @@
# fzf
Binary of the fuzzy finder [fzf](https://github.com/junegunn/fzf).
Use `fzf` as you would normally; this wrapper ships the prebuilt
binary in the dotfiles.
## Usage
```bash
fzf [options]
```
Refer to the upstream `fzf` documentation for all available
flags and features.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,25 @@
# git-attributes
Checks that every tracked file has a matching pattern in `.gitattributes`.
Can optionally suggest or write missing rules.
## Usage
```bash
git-attributes [options]
```
Options include:
- `-v, --verbose` show progress information
- `-e, --exit` exit with non-zero status if missing rules
- `-p, --pattern <glob>` pattern to check (default: `text: auto`)
- `-w, --write` append suggestions to `.gitattributes`
### Example
```bash
git-attributes -v --write
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,15 @@
# iterm2_shell_integration.zsh
Official iTerm2 shell integration script for zsh. Source this file to
enable prompt tracking and command notifications in iTerm2.
## Usage
```bash
source iterm2_shell_integration.zsh
```
No parameters are required. The script modifies your prompt to work
with iTerm2 features such as badges and profile switching.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

20
local/bin/msgr.md Normal file
View File

@@ -0,0 +1,20 @@
# msgr
Helper library for printing colorized log messages from shell scripts.
## Usage
```bash
msgr <type> "message" [extra]
```
Message types include `ok`, `warn`, `err`, `run` and many more. The
script is primarily sourced by other scripts.
### Example
```bash
msgr ok "Installation complete"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

23
local/bin/php-switcher.md Normal file
View File

@@ -0,0 +1,23 @@
# php-switcher
Switch between Homebrew-installed PHP versions or list installed versions.
## Usage
```bash
php-switcher <version>|--auto [options]
```
Options:
- `--installed` list versions installed via Homebrew
- `--current` print currently active PHP version
- `--auto` read version from `.php-version` in current directory
### Example
```bash
php-switcher 8.3
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -56,22 +56,31 @@ __pushover_send_message()
response="$(eval "${curl_cmd}")"
printf "%s\n" "$response"
# TODO: Parse response
r="${?}"
if [ "${r}" -ne 0 ]; then
printf "%s: Failed to send message\n" "${0}" >&2
# Parse response status. Expect JSON like: {"status":1,"request":"..."}
if echo "$response" | grep -q '"status"[[:space:]]*:[[:space:]]*1'; then
r=0
else
r=1
fi
return "${r}"
if [ "$r" -ne 0 ]; then
# Extract possible error message from JSON
err=$(echo "$response" | grep -o '"errors".*' | sed 's/"errors"[:,\[]//g' | tr -d '[]"')
[ -n "$err" ] && printf "%s: %s\n" "$0" "$err" >&2
printf "%s: Failed to send message\n" "$0" >&2
fi
return "$r"
}
CURL="$(which curl)"
CURL="$(command -v curl)"
PUSHOVER_URL="https://api.pushover.net/1/messages.json"
TOKEN=$PUSHOVER_TOKEN
USER=$PUSHOVER_USER
CURL_OPTS=""
devices="${devices} ${device}"
optstring="c:d:D:e:f:p:r:t:T:s:u:U:a:h"
devices=""
optstring="c:d:D:e:p:r:t:T:s:u:U:a:h"
OPTIND=1
while getopts ${optstring} c; do
@@ -97,7 +106,7 @@ while getopts ${optstring} c; do
t)
title="${OPTARG}"
;;
k)
T)
TOKEN="${OPTARG}"
;;
s)

25
local/bin/pushover.md Normal file
View File

@@ -0,0 +1,25 @@
# pushover
Send notifications via the Pushover API.
## Usage
```bash
pushover -T <token> -U <user> [-t title] [-p priority] message
```
Common options:
- `-c <callback>` callback URL
- `-d <device>` target device
- `-s <sound>` notification sound name
- `-T <token>` application token (or `PUSHOVER_TOKEN` env)
- `-U <user>` user key (or `PUSHOVER_USER` env)
## Example
```bash
pushover -T $TOKEN -U $USER -t "Build" "Finished successfully"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

23
local/bin/t.md Normal file
View File

@@ -0,0 +1,23 @@
# t
Launch or switch to a tmux session based on a directory selected with
`fzf`. Inspired by scripts from ThePrimeagen and Jess Archer.
## Usage
```bash
t
```
Environment variables:
- `T_ROOT` base directory to search (default: `~/Code`)
- `T_MAX_DEPTH` recursion depth for directory search
### Example
```bash
T_ROOT=~/projects t
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-backup-folder
Create a compressed archive of a folder with a timestamped name.
## Usage
```bash
x-backup-folder <folder> [archive-name]
```
- `folder` directory to back up
- `archive-name` optional prefix for the generated tar.gz
## Example
```bash
x-backup-folder ~/Documents Notes
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,21 @@
# x-backup-mysql-with-prefix
Dump MySQL tables matching a prefix to a timestamped file.
## Usage
```bash
x-backup-mysql-with-prefix <prefix> <name> [database]
```
- `prefix` table prefix to match (e.g. `wp_`)
- `name` file name prefix
- `database` database name (default: `wordpress`)
## Example
```bash
x-backup-mysql-with-prefix wp_ blog
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-change-alacritty-theme
Adapted from <https://gist.github.com/xqm32/17777d035930d622d0ff7530bfab61fd>
## Usage
```bash
x-change-alacritty-theme <day|night>
```
Switches Alacritty's theme by copying a theme file under
`~/.config/alacritty/`.
### Example
```bash
x-change-alacritty-theme night
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-clean-vendordirs
Remove `vendor` and `node_modules` directories recursively.
## Usage
```bash
x-clean-vendordirs [directory]
```
- `directory` root directory to clean (default: current directory)
## Example
```bash
x-clean-vendordirs ~/projects
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-compare-versions.py
Compare version strings using Python's packaging library.
## Usage
```bash
echo "1.2.3 >= 1.0.0" | x-compare-versions.py
```
The script reads comparison expressions from standard input and exits
with status 0 if all comparisons are true.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-dc.md Normal file
View File

@@ -0,0 +1,19 @@
# x-dc
Create a directory if it does not exist.
## Usage
```bash
x-dc <directory>
```
Set `VERBOSE=1` to see log messages.
## Example
```bash
x-dc ~/tmp/mydir
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-dfm-docs-xterm-keybindings
Generate `docs/tmux-keybindings.md` using tmux's key list.
## Usage
```bash
x-dfm-docs-xterm-keybindings
```
No parameters are needed. The script writes the file under `docs/` and
overwrites any existing version.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

21
local/bin/x-env-list.md Normal file
View File

@@ -0,0 +1,21 @@
# x-env-list
Lists environment variables grouped by their prefix. Sensitive values
are hidden by default.
## Usage
```bash
x-env-list [options]
```
Use `--json` for machine readable output or specify
`X_ENV_GROUPING` with a YAML file to override the default groups.
### Example
```bash
X_ENV_GROUPING=~/env-groups.yaml x-env-list --json
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

20
local/bin/x-foreach.md Normal file
View File

@@ -0,0 +1,20 @@
# x-foreach
Run a command in each directory produced by another command.
## Usage
```bash
x-foreach "<list-cmd>" "<cmd>"
```
- `list-cmd` command that outputs directories
- `cmd` command to run inside each directory
## Example
```bash
x-foreach "ls -d */" "git status"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-gh-get-latest-release-targz
Fetch the tarball URL of the latest GitHub release or download it directly.
## Usage
```bash
x-gh-get-latest-release-targz <owner/repo> [--get]
```
- `--get` download and extract the tarball instead of printing the URL
## Example
```bash
x-gh-get-latest-release-targz ivuorinen/dotfiles --get
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -97,7 +97,7 @@ Examples:
# Use GitHub Enterprise API
GITHUB_API_URL="https://github.example.com/api/v3/repos" $BIN ivuorinen/dotfiles
EOF
exit 1
exit 0
}
# Check that required dependencies are installed

View File

@@ -0,0 +1,21 @@
# x-git-largest-files.py
Lists the largest files in a git repository.
```bash
x-git-largest-files.py [options]
```
Options:
- `-c NUM` number of files to show (default: 10)
- `--files-exceeding N` list files larger than N KB
- `-p` sort by on-disk size instead of pack size
## Example
```bash
x-git-largest-files.py -c 5
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

18
local/bin/x-have.md Normal file
View File

@@ -0,0 +1,18 @@
# x-have
Check if a command exists on the system. Exits with status 0 if found
and 1 otherwise.
## Usage
```bash
x-have <command>
```
### Example
```bash
x-have git && echo "git installed"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-hr.md Normal file
View File

@@ -0,0 +1,19 @@
# x-hr
Print a horizontal rule. Useful for visually separating log output.
## Usage
```bash
x-hr [character]
```
If no character is given a red `-` is used.
### Example
```bash
x-hr "="
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-ip.md Normal file
View File

@@ -0,0 +1,19 @@
# x-ip
Fetch your public IP address using `curl`.
## Usage
```bash
x-ip [curl-options]
```
Any arguments are passed directly to `curl`.
### Example
```bash
x-ip -4
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,21 @@
# x-load-configs
Source shell configuration files for aliases and exports. Intended to
be run after `dfm install` or when switching hosts.
## Usage
```bash
x-load-configs
```
Set `VERBOSE=1` to print each file as it is sourced. Use `DEBUG=1` to
enable tracing.
### Example
```bash
VERBOSE=1 x-load-configs
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

25
local/bin/x-localip.md Normal file
View File

@@ -0,0 +1,25 @@
# x-localip
Display local IPv4 and IPv6 addresses with optional interface filtering.
## Usage
```bash
x-localip [--ipv4] [--ipv6] [interface]
```
- `--ipv4` show only IPv4 addresses
- `--ipv6` show only IPv6 addresses
- `interface` limit output to the named interface
## Example
```bash
# Show all addresses
x-localip
# IPv4 for wlan0
x-localip --ipv4 wlan0
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#
# Create a directory and cd into it
# Usage: mkcd <dir>
# Usage: x-mkd <dir>
set -euo pipefail

19
local/bin/x-mkd.md Normal file
View File

@@ -0,0 +1,19 @@
# x-mkd
Create a directory and immediately `cd` into it.
## Usage
```bash
x-mkd <dir>
```
Set `VERBOSE=1` for status messages.
## Example
```bash
x-mkd project && git init
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

20
local/bin/x-multi-ping.md Normal file
View File

@@ -0,0 +1,20 @@
# x-multi-ping
Multi-protocol ping helper supporting IPv4 and IPv6.
## Usage
```bash
x-multi-ping [--loop] [--sleep=N] host1 host2...
```
- `--loop` ping continuously
- `--sleep` seconds to wait between iterations
## Example
```bash
x-multi-ping --loop --sleep=5 example.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-multi-ping.pl
Ping multiple hosts with IPv4/IPv6 support.
## Usage
```bash
x-multi-ping.pl [--loop|--forever] [--sleep N] host1 host2 ...
```
`--loop` keeps pinging each host until interrupted. `--sleep` controls
the delay between attempts.
### Example
```bash
x-multi-ping.pl --loop --sleep 2 example.com 1.1.1.1
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-open-ports.md Normal file
View File

@@ -0,0 +1,19 @@
# x-open-ports
List listening ports using `lsof` or `ss`.
## Usage
```bash
x-open-ports [--json]
```
- `--json` output as JSON instead of Markdown
## Example
```bash
x-open-ports --json
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-path-append
Append directories to the `PATH` variable without duplicates.
## Usage
```bash
x-path-append <dir1> [dir2 ...]
```
Set `VERBOSE=1` for verbose logging.
## Example
```bash
x-path-append /usr/local/sbin "$HOME/bin"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-path-prepend
Prepend directories to the `PATH` variable without duplicates.
## Usage
```bash
x-path-prepend <dir1> [dir2 ...]
```
Set `VERBOSE=1` for verbose logging.
## Example
```bash
x-path-prepend "$HOME/bin" /opt/tools
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,19 @@
# x-path-remove
Remove directories from the `PATH` variable.
## Usage
```bash
x-path-remove <dir1> [dir2 ...]
```
Set `VERBOSE=1` for verbose logging.
## Example
```bash
x-path-remove /usr/local/bin
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

30
local/bin/x-path.md Normal file
View File

@@ -0,0 +1,30 @@
# x-path
Manage entries in the `PATH` variable through subcommands.
## Usage
```bash
x-path <command> <directory1> [directory2 ...]
```
### Commands
- `append` / `a` Append directories to `PATH`
- `prepend` / `p` Prepend directories to `PATH`
- `remove` Remove directories from `PATH`
- `check` Validate directories (default: all in `PATH`)
Set `VERBOSE=1` for progress output.
## Examples
```bash
# Prepend /opt/bin to PATH
x-path prepend /opt/bin
# Remove /usr/local/bin from PATH
x-path remove /usr/local/bin
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-quota-usage.php
Display filesystem quota usage in a human readable table.
## Usage
```bash
x-quota-usage.php
```
Runs the `quota` command and formats the output. Requires PHP with the
`shell_exec` function enabled.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-record.md Normal file
View File

@@ -0,0 +1,19 @@
# x-record
Interactive screen recording wrapper around `giph`.
## Usage
```bash
x-record <gif|mkv|webm|mp4> <fullscreen|set>
```
The script asks for file type and area when omitted.
## Example
```bash
x-record gif fullscreen
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-set-php-aliases
---
## Usage
```bash
source x-set-php-aliases
```
Generates shell aliases (`php80`, `php81` ...) for each Homebrew PHP
installation. Caches the list under `$XDG_CACHE_HOME/x-set-php-aliases`.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,22 @@
# x-sha256sum-matcher
Compare two files by calculating their SHA256 checksums.
## Usage
```bash
x-sha256sum-matcher [options] file1 file2
```
Options:
- `-v` verbose output
- `-h, --help` show help
### Example
```bash
x-sha256sum-matcher original.iso download.iso
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,22 @@
# x-ssl-expiry-date
Check the expiry date of an SSL certificate for one or more hosts.
## Usage
```bash
x-ssl-expiry-date [-d] [-p PORT] host1 host2 ...
```
Options:
- `-d` show days left instead of the full date
- `-p <port>` use custom port (default: 443)
### Example
```bash
x-ssl-expiry-date -d github.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-term-colors
Display a table of 24bit color codes for testing terminal color
support.
## Usage
```bash
x-term-colors
```
Pipe the output to `less -R` to view with color highlighting.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

26
local/bin/x-thumbgen.md Normal file
View File

@@ -0,0 +1,26 @@
# x-thumbgen
Generate thumbnails using ImageMagick (magick) with MIME type filtering.
## Usage
```bash
x-thumbgen [options] source_directory
```
Options:
- `-o DIR` output directory (default: same as source)
- `-s STR` suffix for thumbnails
- `-h` show help
Environment variables like `THUMB_BACKGROUND` control the background
color.
### Example
```bash
THUMB_BACKGROUND=black x-thumbgen -o ~/thumbs ~/images
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-until-error
Repeatedly execute a command until it returns a non-zero exit status.
## Usage
```bash
x-until-error [--sleep SECONDS] command [args...]
```
Use `--sleep` to wait between runs. The command is executed at least
once.
### Example
```bash
x-until-error --sleep 2 ping -c1 example.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,20 @@
# x-until-success
Repeat a command until it succeeds (exit status 0). The command is
always executed at least once.
## Usage
```bash
x-until-success [--sleep SECONDS] command [args...]
```
Use `--sleep` to control the delay between attempts.
### Example
```bash
x-until-success --sleep 5 curl -I https://example.com
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,14 @@
# x-validate-sha256sum.sh
This script contains a helper for sha256 validating your downloads
## Usage
```bash
x-validate-sha256sum.sh file sha256sum
```
The script computes the SHA256 hash of `file` and compares it to the
expected value. It exits non-zero if the sums differ.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -0,0 +1,13 @@
# x-welcome-banner
Print a colorful MOTD with greeting, system info and today's weather.
## Usage
```bash
x-welcome-banner
```
Requires optional tools: `neofetch`, `figlet`, `lolcat` and `curl` for extra info.
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

17
local/bin/x-when-down.md Normal file
View File

@@ -0,0 +1,17 @@
# x-when-down
Wait until a host stops responding to ping, then run a command.
## Usage
```bash
x-when-down <host> <command...>
```
## Example
```bash
x-when-down 1.2.3.4 echo "server down"
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

19
local/bin/x-when-up.md Normal file
View File

@@ -0,0 +1,19 @@
# x-when-up
Wait for a host to respond to ping before running a command.
## Usage
```bash
x-when-up <host> <command...>
```
If the command is `ssh`, the host argument may be omitted.
## Example
```bash
x-when-up 1.2.3.4 ssh 1.2.3.4
```
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

View File

@@ -1,239 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Source core dfm libraries
source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/../lib/utils.sh"
# Default paths can be overridden via environment variables
: "${DOTFILES:=$HOME/.dotfiles}"
: "${BREWFILE:=$DOTFILES/config/homebrew/Brewfile}"
: "${TEMP_DIR:=$(mktemp -d)}"
: "${DFM_MAX_RETRIES:=3}"
# Remove temp folder on exit
trap 'rm -rf "$TEMP_DIR"' EXIT
# Installation functions for dfm, the dotfile manager
#
# @author Ismo Vuorinen <https://github.com/ivuorinen>
# @license MIT
# Installs all required packages in the correct order.
#
# Description:
# Orchestrates the installation process for the dotfile manager by sequentially invoking
# the installation routines for fonts, Homebrew, and Rust (cargo). It logs the start of the
# overall installation process before calling each respective function.
#
# Globals:
# lib::log - Function used to log installation progress messages.
#
# Arguments:
# None.
#
# Outputs:
# Logs an informational message indicating the start of the installation process.
#
# Returns:
# None.
#
# Example:
# all
#
# @description
# Parse command line options controlling installation steps.
parse_options()
{
NO_AUTOMATION=0
SKIP_FONTS=0
SKIP_BREW=0
SKIP_CARGO=0
while [[ $# -gt 0 ]]; do
case "$1" in
--no-automation)
NO_AUTOMATION=1
;;
--no-fonts)
SKIP_FONTS=1
;;
--no-brew)
SKIP_BREW=1
;;
--no-cargo)
SKIP_CARGO=1
;;
*)
lib::error "Unknown option: $1"
return 1
;;
esac
shift
done
}
# @description
# Install all configured components by calling each individual
# installation routine unless skipped via options.
install_all()
{
parse_options "$@"
lib::log "Installing all packages..."
if [[ $SKIP_FONTS -eq 0 ]]; then
install_fonts
fi
if [[ $SKIP_BREW -eq 0 ]]; then
install_brew
fi
if [[ $SKIP_CARGO -eq 0 ]]; then
install_cargo
fi
}
# Installs fonts required by the dotfile manager.
#
# Globals:
# None.
#
# Arguments:
# None.
#
# Outputs:
# Logs a message to STDOUT indicating that the font installation process has started.
#
# Returns:
# None.
#
# Example:
# install_fonts
#
# @description Install all configured fonts from helper script, prompting the user unless automation is disabled.
install_fonts()
{
: "${SKIP_FONTS:=0}"
: "${NO_AUTOMATION:=0}"
if [[ $SKIP_FONTS -eq 1 ]]; then
lib::log "Skipping fonts installation"
return 0
fi
if [[ $NO_AUTOMATION -eq 0 ]]; then
utils::interactive::confirm "Install fonts?" || return 0
fi
lib::log "Installing fonts..."
local script="${DOTFILES}/scripts/install-fonts.sh"
if [[ ! -x "$script" ]]; then
lib::error "Font installation script not found: $script"
return 1
fi
bash "$script"
}
# Install Homebrew and set it up.
#
# Installs the Homebrew package manager on macOS.
#
# Globals:
# lib::log - Logging utility used to report installation progress.
#
# Outputs:
# Logs a message indicating the start of the Homebrew installation process.
#
# Example:
# install_brew
#
# @description Install Homebrew and declared packages using the Brewfile.
install_brew()
{
: "${SKIP_BREW:=0}"
: "${NO_AUTOMATION:=0}"
if [[ $SKIP_BREW -eq 1 ]]; then
lib::log "Skipping Homebrew installation"
return 0
fi
if [[ $NO_AUTOMATION -eq 0 ]]; then
utils::interactive::confirm "Install Homebrew packages?" || return 0
fi
lib::log "Installing Homebrew..."
if ! utils::is_installed brew; then
lib::log "Homebrew not found, installing..."
local installer="$TEMP_DIR/homebrew-install.sh"
utils::retry "$DFM_MAX_RETRIES" \
curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh \
-o "$installer"
NONINTERACTIVE=1 bash "$installer"
fi
if utils::is_installed brew; then
brew bundle install --file="$BREWFILE" --force --quiet
else
lib::error "Homebrew installation failed"
return 1
fi
}
# Installs Rust and cargo packages.
#
# Description:
# Logs the start of the installation process for Rust and cargo packages.
# The installation logic is intended to be implemented where indicated.
#
# Globals:
# Uses lib::log for logging the installation process.
#
# Example:
# install_cargo
#
# @description Install Rust tooling and cargo packages using helper scripts.
install_cargo()
{
: "${SKIP_CARGO:=0}"
: "${NO_AUTOMATION:=0}"
if [[ $SKIP_CARGO -eq 1 ]]; then
lib::log "Skipping Rust and cargo installation"
return 0
fi
if [[ $NO_AUTOMATION -eq 0 ]]; then
utils::interactive::confirm "Install Rust and cargo packages?" || return 0
fi
lib::log "Installing Rust and cargo packages..."
if ! utils::is_installed cargo; then
lib::log "Rust not found, installing rustup..."
local installer="$TEMP_DIR/rustup-init.sh"
utils::retry "$DFM_MAX_RETRIES" \
curl https://sh.rustup.rs -sSf -o "$installer"
sh "$installer" -y
source "$HOME/.cargo/env"
fi
local script="${DOTFILES}/scripts/install-cargo-packages.sh"
if [[ -x "$script" ]]; then
bash "$script"
else
lib::error "Cargo packages script not found: $script"
return 1
fi
}

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env bash
# dfm - dotfiles manager
set -euo pipefail
# allow overriding core directories
DFM_SCRIPT_DIR="${DFM_SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
readonly DFM_SCRIPT_DIR
export DFM_SCRIPT_DIR
DFM_CMD_DIR="${DFM_CMD_DIR:-${DFM_SCRIPT_DIR}/cmd}"
readonly DFM_CMD_DIR
export DFM_CMD_DIR
DFM_LIB_DIR="${DFM_LIB_DIR:-${DFM_SCRIPT_DIR}/lib}"
readonly DFM_LIB_DIR
export DFM_LIB_DIR
DFM_DEFAULT_CONFIG_PATH="${DFM_DEFAULT_CONFIG_PATH:-$HOME/.config}"
readonly DFM_DEFAULT_CONFIG_PATH
export DFM_DEFAULT_CONFIG_PATH
DFM_MAX_RETRIES="${DFM_MAX_RETRIES:-3}"
readonly DFM_MAX_RETRIES
export DFM_MAX_RETRIES
export DFM_DEFAULT_INSTALL_DIR="${DFM_DEFAULT_INSTALL_DIR:-$HOME/.local}"
export DFM_DEFAULT_VERBOSE="${DFM_DEFAULT_VERBOSE:-0}"
TEMP_DIR="${TEMP_DIR:-$(mktemp -d)}"
export TEMP_DIR
# Load the common and utility functions from the lib directory.
[[ -f "${DFM_LIB_DIR}/common.sh" ]] || {
echo "Error: Required file ${DFM_LIB_DIR}/common.sh not found"
exit 1
}
[[ -f "${DFM_LIB_DIR}/utils.sh" ]] || {
echo "Error: Required file ${DFM_LIB_DIR}/utils.sh not found"
exit 1
}
source "${DFM_LIB_DIR}/common.sh"
source "${DFM_LIB_DIR}/utils.sh"
# Display help information
#
# @return None
main::show_help()
{
cat << EOF
Usage: dfm [command] [function] [arguments]
dotfiles manager utility for installing and configuring dotfiles.
If no arguments are provided, lists all available commands.
If only a command is provided, lists available functions for that command.
If a command and function are provided, executes the specified function.
Examples:
dfm # List all available commands
dfm install # List available functions for the install command
dfm install all # Execute the 'all' function from the install command
EOF
}
# Main function for the dfm script.
#
# The function checks if any arguments were provided. If no arguments are
# provided, it lists all available commands. If a command name is provided,
# it lists the available functions for that command. If a command and function
# name are provided, it executes the function with the provided arguments.
#
# @param args The command-line arguments.
# @return None
main()
{
if [[ $# -eq 0 ]]; then
main::list_available_commands
return 0
fi
local cmd="$1"
shift
if [[ "$cmd" == "-h" || "$cmd" == "--help" ]]; then
main::show_help
return 0
fi
if [[ $# -eq 0 ]]; then
# Show the available functions for the command
local cmd_file="${DFM_CMD_DIR}/${cmd}.sh"
if [[ -f "$cmd_file" ]]; then
list::print_group "Available functions for '$cmd'"
list::loop_functions "$cmd_file"
else
lib::error "Command '$cmd' not found"
return 1
fi
return 0
fi
local func="$1"
shift
main::execute_command "$cmd" "$func" "$@"
}
main "$@"

View File

@@ -1,368 +0,0 @@
#!/usr/bin/env bash
# dfm common functions for logging and error handling, etc.
# Source this file to use the functions in your scripts.
#
# @author Ismo Vuorinen <https://github.com/ivuorinen>
# @license MIT
set -euo pipefail
declare -A ERROR_CODES=(
[SUCCESS]=0
[INVALID_ARGUMENT]=1
[COMMAND_NOT_FOUND]=2
[FUNCTION_NOT_FOUND]=3
[EXECUTION_FAILED]=4
[FILE_NOT_FOUND]=5
)
declare -A LOG_LEVELS=(
[DEBUG]=0
[INFO]=1
[WARN]=2
[ERROR]=3
)
# Simple logging function
#
# @example
# lib::log "Hello, world!"
#
# @description Log a message to the console
# @param $* Message to log
# Logs a message with a timestamp.
#
# Description:
# Outputs the provided message(s) to standard output, prepended with the current date and
# time in the format [YYYY-MM-DD HH:MM:SS]. This timestamp helps in tracking log events.
#
# Arguments:
# One or more strings that form the log message.
#
# Outputs:
# Writes the timestamped log message to standard output.
#
# Example:
# lib::log "Server started" # Outputs: [2025-02-28 09:45:00] Server started
lib::log()
{
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
# Simple error logging function
#
# @example
# lib::error "Something went wrong"
#
# @description Log an error message to the console
# @param $* Error message
# Logs an error message with a timestamp to standard error.
#
# This function formats the provided message(s) by prefixing it with the current date
# and time along with an "ERROR:" label, then outputs the result to STDERR.
#
# Arguments:
# $* - The error message or messages to be logged.
#
# Outputs:
# Writes the formatted error message to STDERR.
#
# Example:
# lib::error "Failed to read the configuration file."
lib::error()
{
printf '[%s] ERROR: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2
}
LOG_LEVEL="${LOG_LEVEL:-INFO}"
if [[ -z "${LOG_LEVELS[$LOG_LEVEL]+_}" ]]; then
lib::error "Invalid LOG_LEVEL: $LOG_LEVEL"
exit "${ERROR_CODES[INVALID_ARGUMENT]}"
fi
# Handle an error by logging an error message to the console
# and exiting with an error code based on the error type.
#
# @example
# lib::error::handle $LINENO $0
#
# @description Handle an error
# @param $1 Line number
# @param $2 Command
# Logs an error message based on the previous command's exit code and the provided context.
#
# This function captures the exit code from the last executed command and, using the provided
# line number and command string, determines the appropriate error message to log based on
# predefined error codes stored in the ERROR_CODES associative array.
#
# Globals:
# ERROR_CODES - An associative array mapping error code names to numeric values.
# lib::error - Logs error messages to STDERR.
#
# Arguments:
# line_no - The line number in the script where the error occurred.
# command - The command that was executed when the error occurred.
#
# Outputs:
# Writes a descriptive error message to STDERR.
#
# Returns:
# The exit code of the failed command.
#
# Example:
# # If a command fails with an exit code corresponding to an invalid argument:
# lib::error::handle 42 "some_command"
# # This logs: "Invalid argument at line 42 in command 'some_command'" (if the exit code matches ERROR_CODES[INVALID_ARGUMENT])
lib::error::handle()
{
local exit_code=$?
local line_no=$1
local command=$2
case $exit_code in
"${ERROR_CODES[INVALID_ARGUMENT]}")
lib::error "Invalid argument at line $line_no in command '$command'"
;;
"${ERROR_CODES[COMMAND_NOT_FOUND]}")
lib::error "Command not found at line $line_no"
;;
"${ERROR_CODES[FUNCTION_NOT_FOUND]}")
lib::error "Function not found at line $line_no in command '$command'"
;;
"${ERROR_CODES[EXECUTION_FAILED]}")
lib::error "Execution failed at line $line_no in command '$command'"
;;
*)
lib::error "Unknown error ($exit_code) at line $line_no in command '$command'"
;;
esac
return $exit_code
}
# Throw an error by logging an error message to the console and exiting
# with an error code based on the error type. The error code name is used
# to determine the error code from the ERROR_CODES associative array.
# The error message is passed as arguments to the function.
#
# @example
# lib::error::throw INVALID_ARGUMENT "Invalid argument"
# lib::error::throw COMMAND_NOT_FOUND "Command not found"
# lib::error::throw FUNCTION_NOT_FOUND "Function not found"
# lib::error::throw EXECUTION_FAILED "Execution failed"
#
# @description Throw an error
# @param $1 Error code name
# @param $* Error message
# Logs an error message and terminates the script by performing cleanup with a specified error code.
#
# Globals:
# ERROR_CODES - Associative array mapping error code names to numeric exit values.
#
# Arguments:
# code_name - The key to retrieve the error code from the ERROR_CODES array.
# message - The error message to log, constructed from all subsequent arguments.
#
# Outputs:
# Logs the error message to standard error.
#
# Returns:
# Exits the script via the cleanup function; does not return.
#
# Example:
# lib::error::throw "FILE_NOT_FOUND" "Required file not found: /path/to/file"
lib::error::throw()
{
local code_name=$1
shift
local message=$*
if [[ -z "${ERROR_CODES[$code_name]+_}" ]]; then
lib::error "Unknown error code: $code_name"
cleanup "${ERROR_CODES[INVALID_ARGUMENT]}"
fi
lib::error "$message"
cleanup "${ERROR_CODES[$code_name]}"
}
# Logs a message to the console if the current log level is set so that the
# message is displayed. The log level is compared to the log level of the
# message and if the message log level is greater than or equal to the current
# log level, the message is displayed.
# The log levels are defined in the LOG_LEVELS associative array.
#
# @example
# logger::log "INFO" "This is an info message"
# logger::log "DEBUG" "This is a debug message"
# logger::log "WARN" "This is a warning message"
# logger::log "ERROR" "This is an error message"
#
# @description Log a message to the console based on the log level setting.
# @param $1 Log level
# @param $2 Message
# Logs a message if its severity meets or exceeds the global log level.
#
# Globals:
# LOG_LEVELS - Associative array mapping log level names to severity values.
# LOG_LEVEL - The current log level threshold.
#
# Arguments:
# level: A string representing the log severity (e.g., DEBUG, INFO, WARN, ERROR).
# msg: The message to log.
#
# Outputs:
# Prints a formatted log message with a timestamp to STDERR when the specified level qualifies.
#
# Example:
# logger::log INFO "Initialization complete"
logger::log()
{
local level=$1
if [[ -z "${LOG_LEVELS[$level]:-}" ]]; then
lib::error "Invalid log level: $level"
return 1
fi
shift
local msg="$*"
if [[ ${LOG_LEVELS[$level]} -ge ${LOG_LEVELS[$LOG_LEVEL]} ]]; then
printf '[%s] [%s]: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$msg" >&2
fi
}
# Logs a debug message to the console, if the current log level is set to DEBUG or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::debug "This is a debug message"
#
# @description Log a debug message to the console
# @param $* Message
# Logs a debug-level message.
#
# This function logs a message at the DEBUG level by delegating to logger::log.
# It accepts one or more arguments that form the debug message, which are passed along
# to the underlying logger::log function.
#
# Example:
# logger::debug "Debug info for variable x:" "$x"
logger::debug()
{
logger::log "DEBUG" "$@"
}
# Logs an info message to the console, if the current log level is set to INFO or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::info "This is an info message"
#
# @description Log an info message to the console
# @param $* Message
# Logs an informational message to the console.
#
# Description:
# This function wraps the logger::log function to log messages at the "INFO" level. All provided arguments are
# forwarded to logger::log, where the message is formatted and output based on the current logging configuration.
#
# Arguments:
# A message string followed by optional additional parameters used to format the message.
#
# Outputs:
# The formatted informational message is written to STDOUT if the INFO log level is enabled.
#
# Example:
# logger::info "Service started successfully on port" 8080
logger::info()
{
logger::log "INFO" "$@"
}
# Logs a warning message to the console, if the current log level is set to WARN or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::warn "This is a warning message"
#
# @description Log a warning message to the console
# @param $* Message
# Logs a warning message.
#
# This function acts as a wrapper around `logger::log` by setting the log level to "WARN"
# for all provided message arguments. It forwards the given messages to the logger for output.
#
# Arguments:
# A variable list of strings representing the warning message.
#
# Outputs:
# Writes a formatted warning message to the console.
#
# Example:
# logger::warn "Low disk space" "Free up some space to avoid issues"
logger::warn()
{
logger::log "WARN" "$@"
}
# Logs an error message to the console, if the current log level is set to ERROR or greater.
# The message is passed as arguments to the function.
# The function is defined above.
#
# @example
# logger::error "This is an error message"
#
# @description Log an error message to the console
# @param $* Message
# Logs an error message at the ERROR level.
#
# This function wraps the generic logging mechanism to record error messages by automatically
# specifying the ERROR severity level. It passes all provided arguments to the underlying logging function.
#
# Arguments:
# Error message(s) One or more strings that describe the error.
#
# Example:
# logger::error "Unable to open file" "/path/to/file"
logger::error()
{
logger::log "ERROR" "$@"
}
# Cleanup function to remove temporary files and directories
# when the script exits or is interrupted by a signal (e.g. Ctrl+C).
# The function is registered with the `EXIT` trap.
#
# @description Remove temporary files and directories
# Cleans up temporary resources before exiting.
#
# Globals:
# TEMP_DIR - Path to the temporary directory to be removed if it exists.
#
# Returns:
# Exits the script with the original exit code.
#
# Example:
# trap cleanup EXIT
cleanup() {
local exit_code=${1:-$?}
if [[ -n ${TEMP_DIR:-} && -d $TEMP_DIR ]]; then
rm -rf "$TEMP_DIR"
fi
exit "$exit_code"
}
# Register the cleanup function to run on EXIT signal.
# This ensures temporary files and directories are removed
# when the script exits or is interrupted.
trap cleanup EXIT
# Handle errors by logging an error message to the console.
# The `ERR` trap passes the line number and command to lib::error::handle.
#
# Example:
# lib::error::handle ${LINENO} "$BASH_COMMAND"
trap 'lib::error::handle ${LINENO} "$BASH_COMMAND"' ERR

View File

@@ -1,806 +0,0 @@
#!/usr/bin/env bash
# dfm utility functions for common tasks
# Source this file to use the functions in your scripts.
#
# @author Ismo Vuorinen <https://github.com/ivuorinen>
# @license MIT
set -euo pipefail
# ANSI escape codes
readonly RESET="\033[0m"
readonly BOLD="\033[1m"
readonly DIM="\033[2m"
readonly ITALIC="\033[3m"
readonly UNDERLINE="\033[4m"
# Colors
readonly BLACK="\033[30m"
readonly RED="\033[31m"
readonly GREEN="\033[32m"
readonly YELLOW="\033[33m"
readonly BLUE="\033[34m"
readonly MAGENTA="\033[35m"
readonly CYAN="\033[36m"
readonly WHITE="\033[37m"
# Prints the provided text in black color using ANSI escape codes.
#
# Globals:
# BLACK - ANSI escape code for black text.
# RESET - ANSI escape code to reset terminal formatting.
#
# Arguments:
# One or more strings that will be concatenated and printed.
#
# Outputs:
# Writes the colored text to STDOUT (without a trailing newline).
#
# Example:
# clr::black "This text will appear in black."
clr::black()
{
printf "${BLACK}%s${RESET}" "$*"
}
# Prints the provided text in red using ANSI escape codes.
#
# Globals:
# RED - ANSI escape code for red.
# RESET - ANSI escape code to reset terminal formatting.
#
# Arguments:
# One or more strings that will be printed in red.
#
# Outputs:
# Writes the red formatted text to STDOUT.
#
# Example:
# clr::red "Error: Invalid input"
clr::red()
{
printf "${RED}%s${RESET}" "$*"
}
# Prints the given text in green using ANSI escape codes.
#
# Arguments:
# * One or more strings to output in green. Multiple arguments are concatenated.
#
# Outputs:
# Writes the formatted green text to STDOUT without a trailing newline.
#
# Example:
# clr::green "Operation successful"
clr::green()
{
printf "${GREEN}%s${RESET}" "$*"
}
# Prints the provided text in yellow color.
#
# Globals:
# YELLOW - ANSI escape code for yellow text.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# Any text passed as parameters will be printed in yellow.
#
# Outputs:
# Colored text printed to STDOUT.
#
# Example:
# clr::yellow "Hello, World!"
clr::yellow()
{
printf "${YELLOW}%s${RESET}" "$*"
}
# Prints the provided text in blue using ANSI escape codes.
#
# Globals:
# BLUE ANSI escape sequence for blue text.
# RESET ANSI escape sequence to reset text formatting.
#
# Arguments:
# $@ One or more strings to be printed in blue.
#
# Outputs:
# Prints the input text in blue to STDOUT.
#
# Example:
# clr::blue "Hello, World!"
clr::blue()
{
printf "${BLUE}%s${RESET}" "$*"
}
# Prints the provided text in magenta color.
#
# This function outputs one or more strings wrapped in ANSI escape sequences
# to display them in magenta. It uses the global variables MAGENTA for the color
# and RESET to revert to the default formatting.
#
# Globals:
# MAGENTA - ANSI escape sequence for magenta.
# RESET - ANSI escape code to reset formatting.
#
# Arguments:
# One or more strings to print in magenta.
#
# Outputs:
# Writes the formatted string directly to STDOUT.
#
# Example:
# clr::magenta "Hello, World!"
clr::magenta()
{
printf "${MAGENTA}%s${RESET}" "$*"
}
# Prints the provided text in white color using ANSI escape codes.
#
# Globals:
# WHITE - ANSI escape code for white.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# Any text passed as arguments will be concatenated and printed.
#
# Outputs:
# Writes the formatted text to STDOUT.
#
# Example:
# clr::white "Hello, World!"
clr::white()
{
printf "${WHITE}%s${RESET}" "$*"
}
# Applies bold styling to the provided text and prints it to STDOUT.
#
# Globals:
# BOLD - ANSI escape code for enabling bold text.
# RESET - ANSI escape code for resetting text formatting.
#
# Arguments:
# One or more strings to be printed in bold.
#
# Outputs:
# Bold-formatted text is printed to STDOUT.
#
# Example:
# style::bold "This is bold text"
style::bold()
{
printf "${BOLD}%s${RESET}" "$*"
}
# Print the provided text in a dim style using ANSI escape codes.
#
# Globals:
# DIM - ANSI escape code for applying dim styling.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# $* - The text to be printed in dim style.
#
# Outputs:
# Writes the formatted dim text to STDOUT.
#
# Example:
# style::dim "This text will appear dimmed"
style::dim()
{
printf "${DIM}%s${RESET}" "$*"
}
# Prints the provided text in italic style using ANSI escape sequences.
#
# Globals:
# ITALIC - ANSI escape sequence for italic text styling.
# RESET - ANSI escape sequence to reset text styling.
#
# Arguments:
# All passed arguments are combined and printed in italic formatting.
#
# Outputs:
# The styled text is printed to STDOUT without an automatic newline.
#
# Example:
# style::italic "Hello, world!"
style::italic()
{
printf "${ITALIC}%s${RESET}" "$*"
}
# Underlines the provided text using ANSI escape codes.
#
# Globals:
# UNDERLINE - ANSI escape sequence to start underlining.
# RESET - ANSI escape sequence to reset text formatting.
#
# Arguments:
# $* - The text to be underlined.
#
# Outputs:
# Prints the underlined text to STDOUT.
#
# Example:
# style::underline "Underlined text"
style::underline()
{
printf "${UNDERLINE}%s${RESET}" "$*"
}
# Prints a formatted line to STDOUT using the provided format string and arguments.
#
# Globals:
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# $1 - A format string that may include ANSI styling codes (do not include a conversion specifier for the text).
# $@ - The text to be formatted and printed.
#
# Outputs:
# Writes the formatted text to STDOUT with an appended newline, ensuring that styling is reset afterward.
#
# Example:
# list::print_formatted "${BOLD}" "Bold Text"
list::print_formatted()
{
local format=$1
shift
printf "${format}%s${RESET}\n" "$@"
}
# Prints a formatted header with a decorative underline.
#
# Globals:
# BOLD - ANSI escape code for bold text.
# BLUE - ANSI escape code for blue text.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# $1 - The header title to be displayed.
#
# Outputs:
# Writes a styled header to STDOUT, including the title in bold blue and a subsequent decorative line.
#
# Example:
# list::print_header "Available Commands"
list::print_header()
{
printf "\n ${BOLD}${BLUE}%s${RESET}\n" "$1"
printf "%s\n" " $(printf '%.s─' {1..60})"
}
# Prints a group header with bold yellow formatting.
#
# Globals:
# YELLOW - ANSI escape code for yellow color.
# BOLD - ANSI escape code for bold text.
# RESET - ANSI escape code to reset text formatting.
#
# Arguments:
# group - The title text to display as the group header.
#
# Outputs:
# Writes the formatted group header to STDOUT.
#
# Example:
# list::print_group "My Group"
list::print_group()
{
local group=$1
printf "\n ${YELLOW}${BOLD}%s${RESET}\n\n" "$group"
}
# Prints a formatted command with an optional description.
#
# Globals:
# BOLD - ANSI escape sequence for bold text.
# CYAN - ANSI escape sequence for cyan text.
# RESET - ANSI escape sequence to reset text formatting.
# DIM - ANSI escape sequence for dim text.
#
# Arguments:
# cmd - The command name to display.
# desc - Optional description of the command (defaults to an empty string).
#
# Outputs:
# Writes the formatted command and description to STDOUT.
#
# Example:
# list::print_command "ls" "List directory contents"
list::print_command()
{
local cmd=$1
local desc=${2:-""}
printf " ${BOLD}${CYAN}%-15s${RESET} ${DIM}%s${RESET}\n" "$cmd" "$desc"
}
# Prints a subcommand in a formatted style.
#
# This function displays a subcommand name in green with a fixed width for neat alignment,
# followed by an optional description text. The ANSI escape codes for green text and reset
# styling are used to highlight the subcommand.
#
# Globals:
# GREEN - ANSI escape code applied to the subcommand name.
# RESET - ANSI escape code used to reset text formatting.
#
# Arguments:
# cmd - The subcommand name to print.
# desc - (Optional) A description string for the subcommand. Defaults to empty if not provided.
#
# Outputs:
# Prints the formatted subcommand and optional description to STDOUT.
#
# Example:
# list::print_subcommand "deploy" "Deploy the application to the production server"
list::print_subcommand()
{
local cmd=$1
local desc=${2:-""}
printf " ${GREEN}%-13s${RESET} ${desc}\n" "$cmd"
}
# Iterates over functions defined in a command file and prints each as a formatted subcommand.
#
# This function reads function names from the specified command file, retrieves their descriptions
# (removing any '@description' prefix), and prints each function name as a bullet point with its
# associated description if available.
#
# Arguments:
# cmd_file - The path to the command file containing function definitions.
#
# Outputs:
# Prints formatted subcommand entries to STDOUT.
#
# Example:
# list::loop_functions "/path/to/command_file.sh"
list::loop_functions()
{
local cmd_file="$1"
while IFS= read -r func; do
# Get the function description from the function definition in the
# command file. If no description is found, print only the function name.
# The description is printed without the @description prefix.
# If the function is not found, print only the function name.
# The function name is printed with a bullet point.
local doc
doc=$(main::get_function_description "$cmd_file" "$func")
if [[ -n "$doc" ]]; then
list::print_subcommand "$func:" "${doc#*@description}"
else
list::print_subcommand "$func" ""
fi
done < <(main::get_command_functions "$cmd_file")
}
# Extracts and prints the documentation associated with a specific function from a command file.
#
# Globals:
# None
#
# Arguments:
# cmd_file - The file containing function definitions and their associated documentation.
# func - The name of the function whose documentation should be extracted.
#
# Outputs:
# Writes the extracted documentation tags and their content to STDOUT.
#
# Returns:
# None
#
# Example:
# list::get_function_docs "commands.sh" "build_project"
list::get_function_docs()
{
local cmd_file="$1"
local func="$2"
awk -v func="$func" '
# Start collecting documentation when a function is found and the line contains @
/^[[:space:]]*#[[:space:]]*@/ {
tag = $2
sub(/^[[:space:]]*#[[:space:]]*@[[:space:]]*[a-zA-Z]+[[:space:]]*/, "")
docs[tag] = $0
last_tag = tag
}
# Collect multi-line documentation
/^[[:space:]]*#/ && last_tag && !/^[[:space:]]*#[[:space:]]*@/ {
sub(/^[[:space:]]*#[[:space:]]*/, "")
docs[last_tag] = docs[last_tag] " " $0
}
# Empty line or comment line ends documentation
!/^[[:space:]]*#/ {
last_tag = ""
}
# When the function is found, print the documentation
$0 ~ "^[[:space:]]*(function[[:space:]]+)?" func "\\(\\)" {
for (tag in docs) {
printf "@%s %s\n", tag, docs[tag]
}
}
' "$cmd_file"
}
# Check if a command exists in the current environment and return 0 if it does.
# Otherwise, return 1.
#
# @example
# if utils::is_installed curl; then
# echo "curl is installed"
# else
# echo "curl is not installed"
# fi
#
# @description Check if a command exists
# @param $1 Command to check
# Checks if a specified command is available in the system.
#
# Arguments:
# $1 - Command name to check.
#
# Returns:
# 0 if the command is found in the system's PATH, 1 otherwise.
#
# Example:
# utils::is_installed "git" && echo "Git is installed" || echo "Git is not installed"
utils::is_installed()
{
command -v "$1" > /dev/null 2>&1
}
# Check if an executable exists in one of the directories listed in PATH and
# return 0 if it does. Otherwise, return 1.
#
# @example
# if utils::in_path ls; then
# echo "ls is available in PATH"
# else
# echo "ls is not available in PATH"
# fi
#
# @description Check if an executable is in PATH
# @param $1 Command to check
# Checks if a specified executable is available in one of the directories in the PATH.
#
# Globals:
# PATH - The system's PATH environment variable listing directories to search.
#
# Arguments:
# cmd: The name of the executable file to look for.
#
# Returns:
# 0 if the executable is found in one of the PATH directories, 1 otherwise.
#
# Example:
# utils::in_path ls && echo "ls is available in PATH"
utils::in_path()
{
local cmd=$1
local result=1
IFS=: read -ra path <<< "$PATH"
for p in "${path[@]}"; do
if [[ -x "$p/$cmd" ]]; then
result=0
break
fi
done
return $result
}
# Retry a command until it succeeds or the maximum number of retries is reached.
# Logs a warning message if the command fails and is retried after a short delay.
#
# @example
# if utils::retry 3 curl -sSL https://example.com; then
# echo "Success"
# else
# echo "Failed"
# fi
#
# @description Retry a command
# @param $1 Maximum number of retries
# @param $2.. Command to run
# @return 0 if the command succeeds, 1 otherwise
# Retries a command until it succeeds or the maximum number of attempts is reached.
#
# Arguments:
# tries - Maximum number of attempts to execute the command.
# command and its args - The command to run and any arguments to pass.
#
# Globals:
# logger::warn - Logs a warning message for each failed attempt.
#
# Outputs:
# Warning messages are printed to STDERR for each retry.
#
# Returns:
# 0 if the command eventually succeeds; 1 if all attempts fail.
#
# Example:
# utils::retry 3 my_command --option value
#
# Dependencies:
# logger::warn
utils::retry()
{
local tries=$1
shift
local count=1
until "$@"; do
[[ $count -gt $tries ]] && return 1
logger::warn "Failed, retry $count/$tries"
((count++))
sleep 1
done
return 0
}
# Ask for confirmation before proceeding. The default value is used if the user
# presses Enter without providing an answer.
#
# @example
# if utils::interactive::confirm "Are you sure?"; then
# echo "Confirmed"
# else
# echo "Not confirmed"
# fi
#
# @description Confirm an action
# @param $1 Prompt message
# @param $2 Default value
# Prompts the user for confirmation with a yes/no question.
#
# Arguments:
# prompt: The message displayed to the user when asking for confirmation.
# default: An optional default answer used if no input is provided (defaults to "Y").
#
# Outputs:
# Repeatedly prompts the user until a valid yes or no answer is received.
# An error message is displayed for any invalid response.
#
# Returns:
# 0 if the user confirms (answers yes), 1 if the user declines (answers no).
#
# Example:
# if utils::interactive::confirm "Do you want to proceed?"; then
# echo "Proceeding..."
# else
# echo "Operation cancelled."
# fi
utils::interactive::confirm()
{
local prompt=$1
local default=${2:-Y}
while true; do
read -rp "$prompt [Y/n]: " response
case ${response:-$default} in
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) echo "Please answer yes or no" ;;
esac
done
}
# Find all command files in the cmd directory and return them
# as a space-separated string of filenames (e.g. "cmd1.sh cmd2.sh").
#
# The function uses a while loop to read the output of the find command
# line by line. The -print0 option is used to separate the filenames with
# a null character (\0) instead of a newline. This is necessary to handle
# filenames with spaces correctly.
#
# The read command reads the null-separated filenames and appends them to
# the cmd_files array. Finally, the function prints the array elements
# separated by a space.
#
# Finds all command script files (*.sh) in the directory specified by DFM_CMD_DIR.
#
# Globals:
# DFM_CMD_DIR - The directory to search for command files.
#
# Outputs:
# Echoes a space-separated list of command file paths.
#
# Example:
# files=$(main::find_commands)
# echo "$files" # Displays the list of found command files.
main::find_commands()
{
local cmd_files=()
while IFS= read -r -d '' file; do
cmd_files+=("$file")
done < <(find "$DFM_CMD_DIR" -type f -name "*.sh" -print0)
echo "${cmd_files[@]}"
}
# Get the function names from a command file.
#
# The function uses grep to find function definitions (function xxx() or xxx())
# and sed to extract the function names. The function names are printed one per
# line.
#
# @param cmd_file The command file to extract function names from.
# Extracts the names of functions defined in the specified command file.
#
# This function parses the provided file for Bash function definitions using
# regex patterns matching both "function name() {" and "name() {" styles.
# It outputs the names of the functions, one per line.
#
# Globals:
# None.
#
# Arguments:
# cmd_file - Path to the file containing Bash function definitions.
#
# Outputs:
# Writes the list of function names to STDOUT.
#
# Returns:
# A list of function names extracted from the file.
#
# Example:
# main::get_command_functions "/path/to/command_file.sh"
main::get_command_functions()
{
local cmd_file="$1"
# Find function definitions (function xxx() or xxx())
grep -E '^[[:space:]]*(function[[:space:]]+)?[a-zA-Z0-9_]+\(\)[[:space:]]*{' "$cmd_file" \
| sed -E 's/^[[:space:]]*(function[[:space:]]+)?([a-zA-Z0-9_]+).*/\2/'
}
# Get the description of a function from a command file.
#
# The function uses grep to find the function definition and sed to extract
# the description. The description is printed without the @description prefix.
#
# @param cmd_file The command file to extract the function description from.
# @param func The function name.
# Retrieves the annotated description of a specified function from a command file.
#
# This function searches the provided command file for an "@description" comment
# preceding the definition of the designated function. It then extracts and prints
# the description text. If no description is found, nothing is printed.
#
# Arguments:
# cmd - Command name or path to the file containing the function definitions.
# func - Name of the function whose description is to be extracted.
#
# Outputs:
# The extracted description text is printed to STDOUT.
#
# Example:
# desc=$(main::get_function_description "install" "my_function")
main::get_function_description()
{
local cmd_file="$1"
local func="$2"
if [[ ! -f "$cmd_file" ]]; then
[[ -n ${DFM_CMD_DIR:-} ]] || return 1
cmd_file="${DFM_CMD_DIR}/${cmd_file}"
[[ "$cmd_file" == *.sh ]] || cmd_file="${cmd_file}.sh"
fi
[[ -f "$cmd_file" ]] || return 1
local escaped_func
escaped_func=$(printf '%s' "$func" | sed 's/[][\\.^$*+?(){}|]/\\&/g')
grep -B5 -E "^[[:space:]]*(function[[:space:]]*)?${escaped_func}[[:space:]]*\\(\\)[[:space:]]*(\\{)?[[:space:]]*$" "$cmd_file" \
| grep "@description" \
| sed -E 's/^[[:space:]]*#[[:space:]]*@description[[:space:]]*//'
}
# List all available commands and their functions.
#
# The function uses main::find_commands to get a list of command files.
# It then iterates over the files and prints the command name and
# its functions.
#
# Lists all available commands and their subcommands.
#
# Description:
# Uses main::find_commands to locate command files and prints a header followed by a group title.
# For each command file, extracts the command name (removing the '.sh' extension) and prints it,
# then calls list::loop_functions to display detailed subcommands.
#
# Globals:
# None.
#
# Arguments:
# None.
#
# Outputs:
# Writes the formatted list of commands and associated subcommands to STDOUT.
#
# Example:
# main::list_available_commands
main::list_available_commands()
{
local cmd_files
cmd_files=$(main::find_commands)
list::print_header "dfm - dotfiles manager"
list::print_group "Available commands"
for cmd_file in $cmd_files; do
local cmd_name
cmd_name=$(basename "$cmd_file" .sh)
list::print_command "$cmd_name"
list::loop_functions "$cmd_file"
done
}
# Execute a command function.
#
# The function loads the command file and checks if the function exists.
# If the function exists, it executes the function with the provided arguments.
#
# @param cmd The command name.
# @param func The function name.
# @param args The function arguments.
# Executes a specified function from a command file.
#
# This function validates and runs a function defined within a command file. It checks that both
# the command and function names contain only allowed characters (alphanumeric, underscores, or dashes),
# verifies that the command file (located in DFM_CMD_DIR) exists, is readable, and is free of syntax errors,
# and then sources the file. If the specified function exists in the file, it is executed with any additional
# arguments provided.
#
# Globals:
# DFM_CMD_DIR - Directory containing command files.
#
# Arguments:
# command: The command file name (without .sh extension) to execute. Must match ^[a-zA-Z0-9_-]+$.
# function: The function name to be executed from the command file. Must match ^[a-zA-Z0-9_-]+$.
# [additional arguments]: Extra parameters to pass to the executed function.
#
# Outputs:
# Any output generated by the executed function. Error messages are output via lib::error.
#
# Returns:
# 0 if the function executes successfully; 1 if an error occurs (e.g., invalid names, missing or unreadable
# command file, syntax errors in the command file, or if the specified function is not found).
#
# Example:
# main::execute_command "deploy" "run_deploy" "arg1" "arg2"
main::execute_command()
{
local cmd="$1"
shift
local func="$1"
shift
# Validate input
if [[ ! "$cmd" =~ ^[a-zA-Z0-9_-]+$ ]] || [[ ! "$func" =~ ^[a-zA-Z0-9_-]+$ ]]; then
lib::error "Invalid command or function name"
return 1
fi
local cmd_file="${DFM_CMD_DIR}/${cmd}.sh"
if [[ ! -f "$cmd_file" ]] || [[ ! -r "$cmd_file" ]]; then
lib::error "Command '$cmd' not found"
return 1
fi
# Validate command file
if ! bash -n "$cmd_file"; then
lib::error "Command file '$cmd' contains syntax errors"
return 1
fi
# Source the command file
# shellcheck source=/dev/null
source "$cmd_file"
# Check if the function exists
if ! declare -f "$func" > /dev/null; then
lib::error "Function '$func' not found in command '$cmd'"
return 1
fi
# Run the function with the provided arguments
"$func" "$@"
}

View File

@@ -10,7 +10,7 @@
"fix:markdown": "npx markdownlint -df .",
"lint:prettier": "npx prettier . --check",
"fix:prettier": "npx prettier . --write",
"test": "npx bats --recursive tests/"
"test": "bash test-all.sh"
},
"repository": {
"type": "git",
@@ -32,5 +32,5 @@
"bats": "^1.12.0",
"typescript": "^5.8.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"packageManager": "yarn@1.22.22"
}

View File

@@ -0,0 +1,11 @@
# create-aerospace-keymaps
Generates `docs/aerospace-keybindings.md` using `aerospace config --json`.
## Usage
```bash
scripts/create-aerospace-keymaps.php
```
Requires the `aerospace` CLI tool to be installed.

View File

@@ -0,0 +1,11 @@
# create-nvim-keymaps
Outputs current Neovim key mappings to `docs/nvim-keybindings.md`.
## Usage
```bash
scripts/create-nvim-keymaps.sh
```
Requires Neovim to be installed.

View File

@@ -0,0 +1,11 @@
# create-wezterm-keymaps
Generates `docs/wezterm-keybindings.md` by invoking `wezterm show-keys`.
## Usage
```bash
scripts/create-wezterm-keymaps.sh
```
Requires wezterm to be installed.

View File

@@ -0,0 +1,12 @@
# install-cargo-packages
Install Rust packages listed in `config/asdf/cargo-packages`.
## Usage
```bash
scripts/install-cargo-packages.sh
```
The script installs each package with `cargo install` and runs
`cargo-install-update` when available to update existing packages.

View File

@@ -38,7 +38,7 @@ install_packages()
if [[ ${pkg:0:1} == "#" ]]; then continue; fi
msgr run "Installing cargo package $pkg"
cargo install --jobs $BUILD_JOBS "$pkg"
cargo install --jobs "$BUILD_JOBS" "$pkg"
msgr run_done "Done installing $pkg"
echo ""
done
@@ -56,13 +56,13 @@ post_install_steps()
msgr run "Removing cargo cache"
cargo cache --autoclean
msgr done "Done removing cargo cache"
msgr "done" "Done removing cargo cache"
}
main()
{
install_packages
msgr done "Installed cargo packages!"
msgr "done" "Installed cargo packages!"
post_install_steps
}

View File

@@ -0,0 +1,12 @@
# install-cheat-purebashbible
Fetches the Pure Bash Bible repository and installs its cheatsheets for the
`cheat` utility.
## Usage
```bash
scripts/install-cheat-purebashbible.sh
```
Requires `git` and `cheat` to be available in PATH.

View File

@@ -0,0 +1,11 @@
# install-composer
Installs the PHP package manager [Composer](https://getcomposer.org/).
## Usage
```bash
scripts/install-composer.sh
```
The script downloads the latest Composer PHAR and places it in `$HOME/.local/bin`.

Some files were not shown because too many files have changed in this diff Show More