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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ cd ~/power_conf
```

The installer opens an interactive terminal menu. Use the arrow keys to move,
**Space** to select or deselect software, and **Enter** to continue. Everything
is selected initially, preserving the original one-command setup.
**Space** to select or deselect software, and **Enter** to continue. Recommended
apps are selected initially.

The interactive flow starts with a `power_mac` welcome screen and a compact
preparation log while it discovers modules, restores saved preferences, and
Expand All @@ -38,7 +38,7 @@ table for selection.
### Non-interactive usage

```bash
# Install everything
# Install all compatible components (recommended alternatives win)
./install.sh --all

# Install only selected component bundles
Expand Down Expand Up @@ -68,8 +68,13 @@ Run `./install.sh --help` for the complete component list.
| Tool | Description | Install |
| --- | --- | --- |
| 🌌 **AeroSpace** | i3-like tiling window manager for macOS | `brew install --cask nikitabobko/tap/aerospace` |
| ▭ **Rectangle** | Keyboard-driven window snapping | `brew install --cask rectangle` |
| 🔄 **AltTab** | Windows-style alt-tab app switcher | `brew install --cask alt-tab` |

> AeroSpace and Rectangle are alternatives. The installer prevents selecting
> both and keeps only the most recently selected one in its saved state. When
> switching, it reminds you to quit or disable the previous window manager.

### 🛠️ Productivity & Utilities

| Tool | Description | Install |
Expand Down
19 changes: 19 additions & 0 deletions components/42-rectangle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

component_define \
"rectangle" \
"Rectangle" \
"Keyboard window snapping; choose this or AeroSpace, not both" \
"Window Management" \
"false" \
"false" \
"" \
"cask" \
"rectangle" \
"" \
"" \
"" \
"" \
""

component_conflict "rectangle" "aerospace"
56 changes: 49 additions & 7 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Usage:
./install.sh --components ID,ID,... [--tmux-style top|bottom] [--dry-run]

Options:
--all Install every selectable component.
--all Install all compatible components, preferring defaults.
--components LIST Install a comma-separated list of component IDs.
--tmux-style STYLE Use the top or bottom Tmux status bar (default: bottom).
--dry-run Show the resolved work without changing the machine.
Expand Down Expand Up @@ -128,25 +128,27 @@ SELECTED_COMPONENTS=()
if [ "$SELECTION_MODE" = all ]; then
while IFS= read -r id; do
[ -n "$id" ] && SELECTED_COMPONENTS+=("$id")
done < <(pm_selectable_component_ids)
done < <(pm_compatible_component_ids)
elif [ "$SELECTION_MODE" = components ]; then
pm_split_csv "$COMPONENTS_ARGUMENT" SELECTED_COMPONENTS
else
pm_ui_selection_header
menu_options=()
default_menu_labels=()
for ((i = 0; i < ${#COMPONENT_IDS[@]}; i++)); do
[ "${COMPONENT_HIDDEN[$i]}" = false ] || continue
printf -v menu_label " %-19s %-18s %s:%s" \
menu_description="${COMPONENT_DESCRIPTIONS[$i]//,/;}"
printf -v menu_label " %-19s %-18s %s" \
"${COMPONENT_CATEGORIES[$i]}" \
"${COMPONENT_LABELS[$i]}" \
"${COMPONENT_DESCRIPTIONS[$i]}" \
"${COMPONENT_IDS[$i]}"
menu_options+=("$menu_label")
"$menu_description"
menu_options+=("${menu_label}:${COMPONENT_IDS[$i]}")
[ "${COMPONENT_DEFAULTS[$i]}" = false ] || default_menu_labels+=("$menu_label")
done
selection_output="$(
gum choose \
--no-limit \
--selected "*" \
--selected "$(pm_join_by , "${default_menu_labels[@]}")" \
--height 14 \
--label-delimiter ":" \
"${menu_options[@]}"
Expand All @@ -159,8 +161,48 @@ else
done <<< "$selection_output"
fi

if [ "$SELECTION_MODE" = interactive ]; then
for ((i = 0; i < ${#COMPONENT_CONFLICT_IDS[@]}; i++)); do
conflict_id="${COMPONENT_CONFLICT_IDS[$i]}"
conflict_target="${COMPONENT_CONFLICT_TARGETS[$i]}"
if pm_array_contains "$conflict_id" "${SELECTED_COMPONENTS[@]}" &&
pm_array_contains "$conflict_target" "${SELECTED_COMPONENTS[@]}"; then
conflict_label="$(pm_component_field "$conflict_id" COMPONENT_LABELS)"
target_label="$(pm_component_field "$conflict_target" COMPONENT_LABELS)"
pm_warn "$conflict_label and $target_label overlap and should not run together."
keep_component="$(
gum choose \
--header "Choose one window manager to keep" \
"$conflict_id" \
"$conflict_target"
)" || {
pm_warn "Installation cancelled"
exit 0
}
filtered_components=()
for id in "${SELECTED_COMPONENTS[@]}"; do
if [ "$id" = "$conflict_id" ] || [ "$id" = "$conflict_target" ]; then
[ "$id" = "$keep_component" ] && filtered_components+=("$id")
else
filtered_components+=("$id")
fi
done
SELECTED_COMPONENTS=("${filtered_components[@]}")
pm_ok "Keeping $(pm_component_field "$keep_component" COMPONENT_LABELS)"
fi
done
fi

pm_validate_selected_ids "${SELECTED_COMPONENTS[@]}"

for id in "${SELECTED_COMPONENTS[@]}"; do
for saved_id in "${PM_STATE_COMPONENTS[@]}"; do
if pm_components_conflict "$id" "$saved_id"; then
pm_warn "$(pm_component_field "$id" COMPONENT_LABELS) replaces $(pm_component_field "$saved_id" COMPONENT_LABELS) in the saved setup. Quit or disable $(pm_component_field "$saved_id" COMPONENT_LABELS) to avoid overlapping window controls."
fi
done
done

if pm_array_contains "tmux" "${SELECTED_COMPONENTS[@]}" && [ "$SELECTION_MODE" = interactive ] && [ "$TMUX_STYLE_SET" = false ]; then
TMUX_STYLE="$(gum choose --selected "$TMUX_STYLE" --header "Choose the Tmux status bar position" bottom top)" || {
pm_warn "Installation cancelled"
Expand Down
74 changes: 72 additions & 2 deletions lib/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ COMPONENT_INSTALL_HOOKS=()
COMPONENT_SYNC_HOOKS=()
COMPONENT_DRY_RUN_HOOKS=()
COMPONENT_POST_HOOKS=()
COMPONENT_CONFLICT_IDS=()
COMPONENT_CONFLICT_TARGETS=()

# component_define id label description category default hidden dependencies
# kind package configs install_hook sync_hook dry_run_hook post_hook
Expand All @@ -113,6 +115,12 @@ component_define() {
COMPONENT_POST_HOOKS+=("${14}")
}

component_conflict() {
[ "$#" -eq 2 ] || pm_die "component_conflict expected 2 arguments, received $#"
COMPONENT_CONFLICT_IDS+=("$1")
COMPONENT_CONFLICT_TARGETS+=("$2")
}

pm_component_index() {
local id="$1"
local i
Expand Down Expand Up @@ -221,6 +229,16 @@ pm_validate_registry() {
check_ids+=("$id")
done
pm_resolve_components "${check_ids[@]}" >/dev/null

for ((i = 0; i < ${#COMPONENT_CONFLICT_IDS[@]}; i++)); do
id="${COMPONENT_CONFLICT_IDS[$i]}"
other="${COMPONENT_CONFLICT_TARGETS[$i]}"
[ "$id" != "$other" ] || pm_die "Component '$id' cannot conflict with itself"
pm_component_index "$id" >/dev/null ||
pm_die "Conflict references unknown component '$id'"
pm_component_index "$other" >/dev/null ||
pm_die "Component '$id' conflicts with unknown component '$other'"
done
}

PM_RESOLVED_COMPONENTS=()
Expand Down Expand Up @@ -272,14 +290,58 @@ pm_default_component_ids() {
done
}

pm_compatible_component_ids() {
local desired_default i id selected conflict_found
local compatible=()
for desired_default in true false; do
for ((i = 0; i < ${#COMPONENT_IDS[@]}; i++)); do
[ "${COMPONENT_HIDDEN[$i]}" = false ] || continue
[ "${COMPONENT_DEFAULTS[$i]}" = "$desired_default" ] || continue
id="${COMPONENT_IDS[$i]}"
conflict_found=false
for selected in "${compatible[@]}"; do
if pm_components_conflict "$id" "$selected"; then
conflict_found=true
break
fi
done
[ "$conflict_found" = true ] || compatible+=("$id")
done
done
printf '%s\n' "${compatible[@]}"
}

pm_validate_selected_ids() {
local id hidden
local id hidden i conflict_id conflict_target
[ "$#" -gt 0 ] || pm_die "No components selected"
for id in "$@"; do
pm_component_index "$id" >/dev/null || pm_die "Unknown component '$id'"
hidden="$(pm_component_field "$id" COMPONENT_HIDDEN)"
[ "$hidden" = false ] || pm_die "Component '$id' is internal and cannot be selected directly"
done
for ((i = 0; i < ${#COMPONENT_CONFLICT_IDS[@]}; i++)); do
conflict_id="${COMPONENT_CONFLICT_IDS[$i]}"
conflict_target="${COMPONENT_CONFLICT_TARGETS[$i]}"
if pm_array_contains "$conflict_id" "$@" &&
pm_array_contains "$conflict_target" "$@"; then
pm_die "$(pm_component_field "$conflict_id" COMPONENT_LABELS) and $(pm_component_field "$conflict_target" COMPONENT_LABELS) cannot be selected together. Choose one window manager."
fi
done
}

pm_components_conflict() {
local first="$1"
local second="$2"
local i conflict_id conflict_target
for ((i = 0; i < ${#COMPONENT_CONFLICT_IDS[@]}; i++)); do
conflict_id="${COMPONENT_CONFLICT_IDS[$i]}"
conflict_target="${COMPONENT_CONFLICT_TARGETS[$i]}"
if { [ "$conflict_id" = "$first" ] && [ "$conflict_target" = "$second" ]; } ||
{ [ "$conflict_id" = "$second" ] && [ "$conflict_target" = "$first" ]; }; then
return 0
fi
done
return 1
}

pm_requires_homebrew() {
Expand Down Expand Up @@ -529,7 +591,15 @@ pm_save_state() {
local id
if pm_load_state; then
for id in "${PM_STATE_COMPONENTS[@]}"; do
pm_component_index "$id" >/dev/null 2>&1 && pm_append_unique merged "$id"
pm_component_index "$id" >/dev/null 2>&1 || continue
local selected conflict_found=false
for selected in "$@"; do
if pm_components_conflict "$id" "$selected"; then
conflict_found=true
break
fi
done
[ "$conflict_found" = true ] || pm_append_unique merged "$id"
done
fi
for id in "$@"; do
Expand Down
2 changes: 1 addition & 1 deletion lib/ui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pm_ui_preparing_header() {

pm_ui_selection_header() {
printf '\n%sChoose your setup%s\n' "$PM_BOLD" "$PM_RESET"
printf 'Space toggles • Enter continues • Everything starts selected\n\n'
printf 'Space toggles • Enter continues • Recommended apps start selected\n\n'
printf ' %-19s %-18s %s\n' "CATEGORY" "APP" "DESCRIPTION"
printf ' %-19s %-18s %s\n' "───────────────────" "──────────────────" "────────────────────────────"
}
4 changes: 2 additions & 2 deletions sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ If no state exists, it detects configs linked by older power_mac releases and
migrates them after a successful sync.

Options:
--all Sync every selectable component configuration.
--all Sync all compatible components, preferring defaults.
--components LIST Sync a comma-separated list of component IDs.
--dry-run Show config changes without writing them.
--help Show this help.
Expand Down Expand Up @@ -65,7 +65,7 @@ case "$SELECTION_MODE" in
all)
while IFS= read -r id; do
[ -n "$id" ] && SELECTED_COMPONENTS+=("$id")
done < <(pm_selectable_component_ids)
done < <(pm_compatible_component_ids)
;;
components)
pm_split_csv "$COMPONENTS_ARGUMENT" SELECTED_COMPONENTS
Expand Down
Loading