Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SHELL = /bin/bash
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
SHELL = $(ROOT_DIR)/bin/make-shell
OS := $(shell bin/distro)
PATH := bin:$(PATH)
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))

.PHONY: clean git macos-brew

Expand Down
7 changes: 7 additions & 0 deletions bin/make-shell
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Wrapper used as make SHELL: sources lib/log.sh then runs the recipe.
# So recipes can use run, log_info, etc. without defining RUN in the Makefile.
[ "$1" = -c ] && shift
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT" && . ./lib/log.sh && eval "$@"
exit $?
8 changes: 0 additions & 8 deletions bin/xscript

This file was deleted.

1 change: 0 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOG_LEVEL=10 # DEBUG+
LOG_TIME=1
LOG_PREFIX="dotfiles-install"
LOG_COLOR=1
# shellcheck source=lib/log.sh
. "$SCRIPT_DIR/lib/log.sh"
# shellcheck source=lib/distro.sh
Expand Down
110 changes: 78 additions & 32 deletions lib/log.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,25 @@ __LOG_SH_LOADED=1
# LOG_LEVEL=20 # INFO and above
# LOG_TIME=1 # enable timestamps
# LOG_PREFIX="mytool" # prefix tag
# LOG_COLOR=1 # enable ANSI color (best-effort)

: "${LOG_LEVEL:=20}" # 10=DEBUG, 20=INFO, 30=WARN, 40=ERROR
: "${LOG_TIME:=0}" # 0/1
: "${LOG_PREFIX:=}" # e.g. "mytool"
: "${LOG_COLOR:=0}" # 0/1 (best-effort)

# --- Internal helpers (POSIX-safe) -------------------------------------------

_log_level_num() {
# Map a level name to its numeric value.
# Usage: _log_level_num "INFO"
case "$1" in
DEBUG) printf '%s' 10 ;;
INFO) printf '%s' 20 ;;
WARN) printf '%s' 30 ;;
ERROR) printf '%s' 40 ;;
*) printf '%s' 0 ;;
DEBUG) printf '%s' 10 ;;
INFO) printf '%s' 20 ;;
WARN) printf '%s' 30 ;;
ERROR) printf '%s' 40 ;;
DIVIDER) printf '%s' 20 ;;
DIVIDER_END_OK) printf '%s' 20 ;;
DIVIDER_END_ERR) printf '%s' 20 ;;
*) printf '%s' 0 ;;
esac
}

Expand All @@ -42,9 +43,9 @@ _log_now() {
}

_log_use_color() {
# Best-effort: use color only when LOG_COLOR=1, stderr/stdout are TTY, and not in CI/non‑TTY/dumb/no_color.
# Best-effort: use color when stderr is TTY and not in CI/dumb/no_color.
# `-t` is POSIX for test in many shells; if unsupported, this will just fail false-ish.
[ "${LOG_COLOR}" = "1" ] && [ -t 1 ] && [ -z "${NO_COLOR-}" ] && [ -n "${TERM-}" ] && [ "${TERM-}" != "dumb" ]
[ -t 2 ] && [ -z "${NO_COLOR-}" ] && [ -n "${TERM-}" ] && [ "${TERM-}" != "dumb" ]
}

_log_fmt_prefix() {
Expand Down Expand Up @@ -85,11 +86,14 @@ _log_print() {
# Colour (best-effort, stderr only)
if _log_use_color; then
case "$lvl" in
DEBUG) c_start="$(printf '🐛 \033[36m')" ;; # cyan
INFO) c_start="$(printf 'ℹ️ \033[32m')" ;; # green
WARN) c_start="$(printf '⚠️ \033[33m')" ;; # yellow
ERROR) c_start="$(printf '🛑 \033[31m')" ;; # red
*) c_start="" ;;
DEBUG) c_start="$(printf '🐛 \033[36m')" ;; # cyan
INFO) c_start="$(printf 'ℹ️ \033[32m')" ;; # green
WARN) c_start="$(printf '⚠️ \033[33m')" ;; # yellow
ERROR) c_start="$(printf '🛑 \033[31m')" ;; # red
DIVIDER) c_start="$(printf '\033[32m')" ;; # green (⌛️ in title)
DIVIDER_END_OK) c_start="$(printf '\033[32m')" ;; # green (✅ in title)
DIVIDER_END_ERR) c_start="$(printf '\033[31m')" ;; # red (🛑 in title)
*) c_start="" ;;
esac
c_end="$(printf '\033[0m')"
else
Expand All @@ -99,22 +103,80 @@ _log_print() {

# Print to stderr (so logs don’t pollute stdout pipelines)
# Shellcheck note: we intentionally want "$*" to preserve spaces as one message.
prefix="$(_log_fmt_prefix "$lvl")"
case "$lvl" in
DIVIDER|DIVIDER_END_OK|DIVIDER_END_ERR) prefix="" ;;
*) prefix="$(_log_fmt_prefix "$lvl")" ;;
esac
# If message is empty, still print prefix.
if [ "$#" -gt 0 ]; then
printf '%s%s %s%s\n' "$c_start" "$prefix" "$*" "$c_end" >&2
if [ -n "$prefix" ]; then
printf '%s%s %s%s\n' "$c_start" "$prefix" "$*" "$c_end" >&2
else
printf '%s%s%s\n' "$c_start" "$*" "$c_end" >&2
fi
else
printf '%s%s%s\n' "$c_start" "$prefix" "$c_end" >&2
fi
}

_log_terminal_width() {
# Best-effort: COLUMNS, then tput cols, then stty size, else 80.
w="${COLUMNS:-}"
if [ -z "$w" ]; then
w="$(tput cols 2>/dev/null)" || w=""
fi
if [ -z "$w" ]; then
w="$(stty size 2>/dev/null | awk '{print $2}')" || w=""
fi
case "$w" in
''|*[!0-9]*) printf '%s' 80 ;;
*) printf '%s' "$w" ;;
esac
}

_log_divider() {
# Arg 1: begin | end. Arg 2: script path. Arg 3 (end only): exit code (0 = success → green ✅, else red 🛑).
kind="$1"
path="$2"
exit_code="${3:-0}"
case "$kind" in
begin) title="⌛️ BEGIN $path"; lvl="DIVIDER" ;;
end) if [ "$exit_code" = "0" ]; then title="✅ END $path"; lvl="DIVIDER_END_OK"; else title="🛑 END $path"; lvl="DIVIDER_END_ERR"; fi ;;
*) title="$path"; lvl="DIVIDER" ;;
esac
w="$(_log_terminal_width)"
len=$(printf '%s' " $title " | wc -c | tr -d ' ')
left_n=$(( (w - len) / 2 ))
right_n=$(( w - len - left_n ))
[ "$left_n" -lt 0 ] && left_n=0
[ "$right_n" -lt 0 ] && right_n=0
left=""
i=0; while [ "$i" -lt "$left_n" ]; do left="${left}="; i=$((i+1)); done
right=""
i=0; while [ "$i" -lt "$right_n" ]; do right="${right}="; i=$((i+1)); done
_log_print "$lvl" "$left $title $right"
}

# --- Public API --------------------------------------------------------------

log_debug() { _log_print DEBUG "$*"; }
log_info() { _log_print INFO "$*"; }
log_warn() { _log_print WARN "$*"; }
log_error() { _log_print ERROR "$*"; }

run() {
# run "path/to/script"
# Execute a script with pre-configured log (LOG_* exported; script runs in current shell and can use log_*).
[ -z "$1" ] && { log_error "run: missing script path"; return 1; }
[ ! -r "$1" ] && { log_error "run: not readable or not found: $1"; return 1; }
export LOG_LEVEL LOG_TIME LOG_PREFIX
_log_divider begin "$1"
( . "$1" ) # Run in subshell
ret=$?
_log_divider end "$1" "$ret"
return $ret
}

die() {
# die "message" [exit_code]
# Returns non-zero if sourced; if executed in a subshell, exit is up to the caller.
Expand All @@ -123,19 +185,3 @@ die() {
log_error "$msg"
return "$code"
}

# Convenience: assert command exists
require_cmd() {
# require_cmd curl git jq
# Returns non-zero (and logs) if any command is missing.
missing=""
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || missing="${missing} ${cmd}"
done

if [ -n "$missing" ]; then
log_error "missing required command(s):${missing}"
return 127
fi
return 0
}
32 changes: 16 additions & 16 deletions makefiles/macos.mk
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
macos-core: macos-brew macos-stow macos-zsh

macos-brew:
xscript "pkg/brew/macos-brew.sh"
run "pkg/brew/macos-brew.sh"

macos-clean:
find . -name ".DS_Store" -delete
Expand Down Expand Up @@ -45,52 +45,52 @@ macos-cli-useful: macos-brew

macos-alfred: macos-brew
brew bundle --file brewfiles/alfred.Brewfile
xscript "scripts/macos-alfred.sh"
run "scripts/macos-alfred.sh"

macos-appcleaner: macos-brew
brew install --cask appcleaner
defaults import net.freemacsoft.AppCleaner pkg/macos-appcleaner/net.freemacsoft.AppCleaner.plist

macos-cleanshot: macos-brew
brew bundle --file brewfiles/cleanshot.Brewfile
xscript "scripts/macos-cleanshot.sh"
run "scripts/macos-cleanshot.sh"

macos-finder:
xscript "scripts/macos-finder.sh"
run "scripts/macos-finder.sh"

macos-hammerspoon: macos-brew macos-stow
brew install --cask hammerspoon
stow --no-folding --dir 'pkg' --target "${HOME}" 'macos-hammerspoon'

macos-iterm: macos-brew
brew bundle --file pkg/macos-iterm/macos-iterm.Brewfile
exists imgcat || xscript "pkg/macos-iterm/macos-iterm.sh"
exists imgcat || (run "pkg/macos-iterm/macos-iterm.sh")

macos-markedit: macos-brew
brew bundle --file brewfiles/markedit.Brewfile
xscript "scripts/macos-markedit.sh"
run "scripts/macos-markedit.sh"

macos-one-password: macos-brew
brew bundle --file brewfiles/one-password.Brewfile

macos-popclip: macos-brew
brew bundle --file brewfiles/popclip.Brewfile
xscript "scripts/macos-popclip.sh"
run "scripts/macos-popclip.sh"

macos-sublime: macos-brew macos-stow
brew install --cask sublime-text
xscript "pkg/sublime/macos-sublime.sh"
run "pkg/sublime/macos-sublime.sh"

macos-terminal: macos-brew
brew bundle --file brewfiles/terminal.Brewfile

macos-tower: macos-brew macos-git
brew install --cask tower
xscript "pkg/macos-tower/macos-tower.sh"
run "pkg/macos-tower/macos-tower.sh"

macos-vscode: macos-brew
brew install --cask visual-studio-code
xscript "pkg/vscode/macos-vscode.sh"
run "pkg/vscode/macos-vscode.sh"

macos-xcode: macos-brew
brew bundle --file brewfiles/xcode.Brewfile
Expand Down Expand Up @@ -126,7 +126,7 @@ macos-stow: macos-brew macos-clean
macos-zsh: macos-brew macos-stow
$(eval BREW_BIN := $(shell bin/brew_bin))
${BREW_BIN}/brew bundle --file brewfiles/zsh.Brewfile
xscript "scripts/macos-zsh.sh"
run "scripts/macos-zsh.sh"

###############################################################################
# Dev #
Expand All @@ -140,21 +140,21 @@ macos-javascript:

macos-perl: macos-stow
stow --no-folding --dir 'pkg/perl' --target "${HOME}" 'stow'
xscript "pkg/perl/macos-perl.sh"
run "pkg/perl/macos-perl.sh"

###############################################################################
# Misc #
###############################################################################

macos-file-handler: macos-duti
xscript "scripts/macos-file-handler.sh"
run "scripts/macos-file-handler.sh"

macos-font: macos-brew
brew bundle --file pkg/macos-font/macos-font.Brewfile
cp -r pkg/macos-font/collection/ "${HOME}/Library/FontCollections"

macos-icon: macos-cli-useful
xscript "scripts/macos-icons.sh"
run "scripts/macos-icons.sh"

macos-quicklook: macos-brew
brew bundle --file brewfiles/ext-quicklook.Brewfile
Expand All @@ -166,7 +166,7 @@ macos-service-workflow: macos-stow
stow --dir 'pkg' --target "${HOME}/Library/Services" 'macos-services'

macos-settings:
xscript "scripts/macos-settings.sh"
run "scripts/macos-settings.sh"

macos-touch-id-sudo:
xscript "scripts/macos-touch-id-sudo.sh"
run "scripts/macos-touch-id-sudo.sh"
12 changes: 6 additions & 6 deletions makefiles/ubuntu.mk
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ubuntu-cli-network:
###############################################################################

ubuntu-dropbox: ubuntu-essential
xscript "scripts/dropbox.sh"
run "scripts/dropbox.sh"

###############################################################################
# CLIs #
Expand All @@ -40,7 +40,7 @@ ubuntu-git: ubuntu-stow
xargs sudo apt-get install -y < pkg/git/git.Aptfile

ubuntu-micro: ubuntu-stow
xscript "pkg/micro/debian-micro.sh"
run "pkg/micro/debian-micro.sh"

ubuntu-nano:
sudo apt-get update
Expand All @@ -51,7 +51,7 @@ ubuntu-stow:
sudo apt-get install -y stow

ubuntu-zsh: ubuntu-stow
xscript "scripts/debian-zsh.sh"
run "scripts/debian-zsh.sh"

###############################################################################
# Dev #
Expand All @@ -65,10 +65,10 @@ ubuntu-javascript:
###############################################################################

ubuntu-locale-zhtw:
xscript "scripts/debian-locale-zhtw.sh"
run "scripts/debian-locale-zhtw.sh"

ubuntu-ssh:
xscript "scripts/debian-ssh.sh"
run "scripts/debian-ssh.sh"

ubuntu-tz-taipei:
xscript "scripts/debian-tz-taipei.sh"
run "scripts/debian-tz-taipei.sh"