dotfiles-tui 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.example +4 -0
- data/.gitattributes +1 -0
- data/.github/workflows/publish-gem.yml +36 -0
- data/.gitignore +316 -0
- data/.shellcheckrc +1 -0
- data/.stow-local-ignore +23 -0
- data/.vscode/settings.json +2 -0
- data/Configs/aerospace/.config/aerospace/aerospace.toml +265 -0
- data/Configs/alacritty/.config/alacritty/alacritty.toml +31 -0
- data/Configs/conda/.condarc +1 -0
- data/Configs/continue/.continue/.continueignore +0 -0
- data/Configs/continue/.continue/.continuerc.json +3 -0
- data/Configs/continue/.continue/config.json +48 -0
- data/Configs/docker/.docker/completion/completion.zsh +3 -0
- data/Configs/editorconfig/.editorconfig +16 -0
- data/Configs/fzf/.fzf/completion/completion.zsh +8 -0
- data/Configs/git/.config/gitconfig/core +83 -0
- data/Configs/git/.config/gitconfig/home +20 -0
- data/Configs/git/.config/gitconfig/template.txt +7 -0
- data/Configs/git/.config/gitconfig/work +23 -0
- data/Configs/git/.gitconfig +25 -0
- data/Configs/gpg/.gnupg/.#lk0x000000015060b4e0.macBook.pro.24088 +2 -0
- data/Configs/gpg/.gnupg/gpg-agent.conf +3 -0
- data/Configs/gpg/.gnupg/openpgp-revocs.d/DE999E2ACEAE3E8EAD660459688CB4D444FB1024.rev +28 -0
- data/Configs/gpg/.gnupg/private-keys-v1.d/0FF407C984AC03CCC60D924C5B315977DF45D5D0.key +7 -0
- data/Configs/gpg/.gnupg/private-keys-v1.d/2B711D7C4A8BE25322C9966B73E6842F7E64CD38.key +7 -0
- data/Configs/gpg/.gnupg/pubring.kbx +0 -0
- data/Configs/gpg/.gnupg/trustdb.gpg +0 -0
- data/Configs/kitty/.config/kitty/current-theme.conf +80 -0
- data/Configs/kitty/.config/kitty/kitty.conf +93 -0
- data/Configs/nvim/.config/nvim/.stylua.toml +6 -0
- data/Configs/nvim/.config/nvim/README.md +9 -0
- data/Configs/nvim/.config/nvim/init.lua +37 -0
- data/Configs/nvim/.config/nvim/lazy-lock.json +29 -0
- data/Configs/nvim/.config/nvim/lua/chadrc.lua +17 -0
- data/Configs/nvim/.config/nvim/lua/configs/conform.lua +15 -0
- data/Configs/nvim/.config/nvim/lua/configs/lazy.lua +47 -0
- data/Configs/nvim/.config/nvim/lua/configs/lspconfig.lua +24 -0
- data/Configs/nvim/.config/nvim/lua/configs/null_ls.lua +27 -0
- data/Configs/nvim/.config/nvim/lua/mappings.lua +10 -0
- data/Configs/nvim/.config/nvim/lua/options.lua +6 -0
- data/Configs/nvim/.config/nvim/lua/plugins/init.lua +25 -0
- data/Configs/oh-my-posh/.config/oh-my-posh/theme/config.json +71 -0
- data/Configs/pip/.config/pip/pip.conf +3 -0
- data/Configs/python/.pythonrc +25 -0
- data/Configs/starship/.config/starship/starship.toml +134 -0
- data/Configs/starship/.config/starship/themes/frappe.toml +27 -0
- data/Configs/starship/.config/starship/themes/latte.toml +27 -0
- data/Configs/starship/.config/starship/themes/macchiato.toml +27 -0
- data/Configs/starship/.config/starship/themes/mocha.toml +27 -0
- data/Configs/tmux/.tmux.conf +21 -0
- data/Configs/wezterm/.config/wezterm/core/colors.lua +10 -0
- data/Configs/wezterm/.config/wezterm/core/font.lua +5 -0
- data/Configs/wezterm/.config/wezterm/core/helper.lua +22 -0
- data/Configs/wezterm/.config/wezterm/core/init.lua +20 -0
- data/Configs/wezterm/.config/wezterm/core/keybindings/init.lua +16 -0
- data/Configs/wezterm/.config/wezterm/core/keybindings/macos.lua +106 -0
- data/Configs/wezterm/.config/wezterm/core/keybindings/windows.lua +99 -0
- data/Configs/wezterm/.config/wezterm/core/launch.lua +41 -0
- data/Configs/wezterm/.config/wezterm/core/maximized.lua +5 -0
- data/Configs/wezterm/.config/wezterm/core/mousebindings.lua +23 -0
- data/Configs/wezterm/.config/wezterm/core/tab_title.lua +26 -0
- data/Configs/wezterm/.config/wezterm/core/window.lua +8 -0
- data/Configs/wezterm/.config/wezterm/wezterm.lua +83 -0
- data/Configs/zellij/.config/zellij/config.kdl +410 -0
- data/Configs/zellij/.config/zellij/layouts/default.kdl +159 -0
- data/Configs/zellij/.config/zellij/plugins/room.wasm +0 -0
- data/Configs/zellij/.config/zellij/plugins/zjstatus.wasm +0 -0
- data/Configs/zsh/.hushlogin +0 -0
- data/Configs/zsh/.localrc +42 -0
- data/Configs/zsh/.profile +3 -0
- data/Configs/zsh/.shellcheckrc +1 -0
- data/Configs/zsh/.zprofile +3 -0
- data/Configs/zsh/.zsh/autoload/backup_restore +84 -0
- data/Configs/zsh/.zsh/autoload/cat +8 -0
- data/Configs/zsh/.zsh/autoload/change_wallpaper +1 -0
- data/Configs/zsh/.zsh/autoload/clean_dstore +6 -0
- data/Configs/zsh/.zsh/autoload/clean_pycache +6 -0
- data/Configs/zsh/.zsh/autoload/convert_mkv_to_mp4 +14 -0
- data/Configs/zsh/.zsh/autoload/create_macos_installer +26 -0
- data/Configs/zsh/.zsh/autoload/download +71 -0
- data/Configs/zsh/.zsh/autoload/fail +2 -0
- data/Configs/zsh/.zsh/autoload/flush_dns +6 -0
- data/Configs/zsh/.zsh/autoload/info +1 -0
- data/Configs/zsh/.zsh/autoload/ls +8 -0
- data/Configs/zsh/.zsh/autoload/reset_beyond_compare +15 -0
- data/Configs/zsh/.zsh/autoload/reset_final_cut_pro +5 -0
- data/Configs/zsh/.zsh/autoload/reset_launch_pad +3 -0
- data/Configs/zsh/.zsh/autoload/reset_open_list +3 -0
- data/Configs/zsh/.zsh/autoload/speedup_terminal +6 -0
- data/Configs/zsh/.zsh/autoload/start_aria2_server +1 -0
- data/Configs/zsh/.zsh/autoload/success +1 -0
- data/Configs/zsh/.zsh/autoload/update_system +19 -0
- data/Configs/zsh/.zsh/autoload/user +1 -0
- data/Configs/zsh/.zsh/autoload/vim +9 -0
- data/Configs/zsh/.zsh/autoload/warn +1 -0
- data/Configs/zsh/.zsh/completion/aliases.zsh +3 -0
- data/Configs/zsh/.zsh/completion/completion.zsh +33 -0
- data/Configs/zsh/.zsh/completion/config.zsh +118 -0
- data/Configs/zsh/.zsh/completion/exports.zsh +108 -0
- data/Configs/zsh/.zsh/completion/fpath.zsh +8 -0
- data/Configs/zsh/.zsh_history +283 -0
- data/Configs/zsh/.zshenv +2 -0
- data/Configs/zsh/.zshrc +54 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +87 -0
- data/Hooks/adguard/pre.rb +7 -0
- data/Hooks/aerospace/pre.rb +8 -0
- data/Hooks/aide/post.rb +7 -0
- data/Hooks/aide/pre.rb +7 -0
- data/Hooks/alacritty/pre.rb +7 -0
- data/Hooks/alfred/pre.rb +7 -0
- data/Hooks/atuin/post.rb +27 -0
- data/Hooks/atuin/pre.rb +7 -0
- data/Hooks/bartender/pre.rb +7 -0
- data/Hooks/bun/pre.rb +7 -0
- data/Hooks/carapace/pre.rb +7 -0
- data/Hooks/cargo/pre.rb +7 -0
- data/Hooks/choosy/pre.rb +7 -0
- data/Hooks/controld/pre.rb +7 -0
- data/Hooks/core/common.rb +36 -0
- data/Hooks/core/final.rb +23 -0
- data/Hooks/core/lib/configurator.rb +593 -0
- data/Hooks/core/lib/display.rb +120 -0
- data/Hooks/core/lib/hook_config.rb +96 -0
- data/Hooks/core/lib/hook_context.rb +124 -0
- data/Hooks/core/lib/hooks.rb +45 -0
- data/Hooks/core/lib/logger.rb +23 -0
- data/Hooks/core/lib/menu.rb +70 -0
- data/Hooks/core/lib/spinner.rb +50 -0
- data/Hooks/core/lib/system.rb +78 -0
- data/Hooks/core/lib/tweaks.rb +59 -0
- data/Hooks/core/library.rb +3 -0
- data/Hooks/core/pre.rb +17 -0
- data/Hooks/ctrld/pre.rb +7 -0
- data/Hooks/deskpad/pre.rb +7 -0
- data/Hooks/fonts/pre.rb +60 -0
- data/Hooks/ghostty/pre.rb +10 -0
- data/Hooks/git/post.rb +20 -0
- data/Hooks/git/pre.rb +7 -0
- data/Hooks/gpg/post.rb +16 -0
- data/Hooks/gpg/pre.rb +7 -0
- data/Hooks/ice-hidemenubar/pre.rb +7 -0
- data/Hooks/iterm/IC Green PPL.itermcolors +344 -0
- data/Hooks/iterm/chalkboard.webp +0 -0
- data/Hooks/iterm/com.googlecode.iterm2.plist +2371 -0
- data/Hooks/iterm/post.rb +7 -0
- data/Hooks/iterm/pre.rb +7 -0
- data/Hooks/jujutsu/post.rb +13 -0
- data/Hooks/jujutsu/pre.rb +7 -0
- data/Hooks/keka/pre.rb +7 -0
- data/Hooks/kitty/pre.rb +7 -0
- data/Hooks/lazyvim/pre.rb +7 -0
- data/Hooks/lima/pre.rb +7 -0
- data/Hooks/little-snitch/pre.rb +7 -0
- data/Hooks/microsoft-edge/pre.rb +7 -0
- data/Hooks/mos/pre.rb +7 -0
- data/Hooks/nvchad/pre.rb +7 -0
- data/Hooks/oh-my-posh/config.json +58 -0
- data/Hooks/oh-my-posh/post.rb +7 -0
- data/Hooks/oh-my-posh/pre.rb +7 -0
- data/Hooks/pearcleaner/pre.rb +7 -0
- data/Hooks/pycharm/pre.rb +7 -0
- data/Hooks/raindropio/pre.rb +7 -0
- data/Hooks/rectangle/RectangleConfig.json +258 -0
- data/Hooks/rectangle/com.knollsoft.Rectangle.plist +0 -0
- data/Hooks/rectangle/post.rb +22 -0
- data/Hooks/rectangle/pre.rb +7 -0
- data/Hooks/slack/pre.rb +7 -0
- data/Hooks/soundsource/pre.rb +7 -0
- data/Hooks/ssh/post.rb +8 -0
- data/Hooks/starship/pre.rb +7 -0
- data/Hooks/sublime_text/post.rb +25 -0
- data/Hooks/sublime_text/pre.rb +7 -0
- data/Hooks/swiftformat-for-xcode/pre.rb +7 -0
- data/Hooks/syncthing/pre.rb +7 -0
- data/Hooks/synology/pre.rb +7 -0
- data/Hooks/tailscale/pre.rb +7 -0
- data/Hooks/tmux/post.rb +7 -0
- data/Hooks/tmux/pre.rb +7 -0
- data/Hooks/topnotch/pre.rb +7 -0
- data/Hooks/transmission/pre.rb +7 -0
- data/Hooks/vscode/extensions.txt +16 -0
- data/Hooks/vscode/keybindings.json +26 -0
- data/Hooks/vscode/post.rb +29 -0
- data/Hooks/vscode/pre.rb +9 -0
- data/Hooks/vscode/settings.json +139 -0
- data/Hooks/vscode/style.css +29 -0
- data/Hooks/wezterm/pre.rb +7 -0
- data/Hooks/wins/pre.rb +7 -0
- data/Hooks/zed/pre.rb +7 -0
- data/Hooks/zellij/post.rb +7 -0
- data/Hooks/zellij/pre.rb +7 -0
- data/Hooks/zoxide/pre.rb +7 -0
- data/Hooks/zsh/pre.rb +19 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/bin/dotfiles-tui +6 -0
- data/bootstrap.rb +186 -0
- data/dotfiles-tui.gemspec +47 -0
- data/lib/dotfiles_tui/version.rb +5 -0
- data/lib/dotfiles_tui.rb +15 -0
- metadata +275 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Bootstrap
|
|
6
|
+
module Display
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def header(text)
|
|
10
|
+
width = 80 # Fixed width as requested
|
|
11
|
+
text_len = text.length
|
|
12
|
+
padding = [(width - text_len - 2) / 2, 0].max
|
|
13
|
+
|
|
14
|
+
# Double border style
|
|
15
|
+
top_border = "╔#{'═' * width}╗"
|
|
16
|
+
bottom_border = "╚#{'═' * width}╝"
|
|
17
|
+
|
|
18
|
+
left_pad_len = padding
|
|
19
|
+
right_pad_len = width - text_len - left_pad_len
|
|
20
|
+
|
|
21
|
+
left_pad = ' ' * left_pad_len
|
|
22
|
+
right_pad = ' ' * right_pad_len
|
|
23
|
+
|
|
24
|
+
# Cyan border, Bold White text
|
|
25
|
+
content = "║#{left_pad}\e[1;37m#{text}\e[0m#{right_pad}║"
|
|
26
|
+
|
|
27
|
+
puts
|
|
28
|
+
puts " \e[0;36m#{top_border}\e[0m"
|
|
29
|
+
puts " \e[0;36m#{content}\e[0m"
|
|
30
|
+
puts " \e[0;36m#{bottom_border}\e[0m"
|
|
31
|
+
puts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def info(message)
|
|
35
|
+
# Log to file
|
|
36
|
+
Bootstrap::Logger.log(message)
|
|
37
|
+
# Transient output to terminal
|
|
38
|
+
transient(message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def transient(message)
|
|
42
|
+
# Clear line and print message
|
|
43
|
+
# \r moves to beginning of line
|
|
44
|
+
# \e[K clears from cursor to end of line
|
|
45
|
+
print "\r\e[K [ \e[0;34m🚀\e[0m ] #{message}"
|
|
46
|
+
$stdout.flush
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def persist(message, status = :success)
|
|
50
|
+
# Make the current line permanent with a status
|
|
51
|
+
# \r moves to beginning
|
|
52
|
+
# \e[K clears line
|
|
53
|
+
|
|
54
|
+
case status
|
|
55
|
+
when :success
|
|
56
|
+
# Transient success: overwrite the line
|
|
57
|
+
print "\r\e[K [ \e[0;32m✔\e[0m ] #{message}"
|
|
58
|
+
$stdout.flush
|
|
59
|
+
when :error
|
|
60
|
+
# Errors persist
|
|
61
|
+
puts "\r\e[K [\e[0;31m✖\e[0m] #{message}"
|
|
62
|
+
when :warn
|
|
63
|
+
# Warnings persist
|
|
64
|
+
puts "\r\e[K [\e[0;33m⚠\e[0m] #{message}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def user(message)
|
|
69
|
+
puts("[ \e[0;33m??\e[0m ] #{message}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def success(message)
|
|
73
|
+
Bootstrap::Logger.log("SUCCESS: #{message}")
|
|
74
|
+
persist(message, :success)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def warn(message)
|
|
78
|
+
Bootstrap::Logger.log("WARNING: #{message}")
|
|
79
|
+
persist(message, :warn)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fail(message)
|
|
83
|
+
Bootstrap::Logger.error(message)
|
|
84
|
+
persist(message, :error)
|
|
85
|
+
exit(1)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def wait_for_confirmation(prompt: "\nPress (y/Y) when ready:")
|
|
89
|
+
print("#{prompt} ")
|
|
90
|
+
loop do
|
|
91
|
+
char = read_single_character
|
|
92
|
+
return true if %w[y Y].include?(char)
|
|
93
|
+
|
|
94
|
+
print("\nInvalid choice: #{char}. #{prompt} ")
|
|
95
|
+
end
|
|
96
|
+
ensure
|
|
97
|
+
puts
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def console_width
|
|
103
|
+
IO.console.winsize[1]
|
|
104
|
+
rescue StandardError
|
|
105
|
+
120
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def read_single_character
|
|
109
|
+
char = nil
|
|
110
|
+
begin
|
|
111
|
+
system('stty raw -echo')
|
|
112
|
+
char = STDIN.getc
|
|
113
|
+
ensure
|
|
114
|
+
system('stty -raw echo')
|
|
115
|
+
end
|
|
116
|
+
char.chr
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
require_relative 'display'
|
|
7
|
+
|
|
8
|
+
module Bootstrap
|
|
9
|
+
class HookConfig
|
|
10
|
+
include Singleton
|
|
11
|
+
|
|
12
|
+
CONFIG_FILE = File.expand_path('../../../config/hooks.yml', __dir__)
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@data = load_file
|
|
16
|
+
@only = parse_env_list(ENV.fetch('BOOTSTRAP_ONLY', nil))
|
|
17
|
+
@exclude = parse_env_list(ENV.fetch('BOOTSTRAP_EXCLUDE', nil))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def skipped?(name, stage)
|
|
21
|
+
stage = stage.to_sym
|
|
22
|
+
|
|
23
|
+
if @only.any?
|
|
24
|
+
return !@only.fetch(name, {}).fetch(stage, false)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
excluded_by_env?(name, stage) || excluded_by_config?(name, stage)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reason_for_skip(name, stage)
|
|
31
|
+
stage = stage.to_sym
|
|
32
|
+
|
|
33
|
+
if @only.any? && !@only.fetch(name, {}).fetch(stage, false)
|
|
34
|
+
return 'not included in BOOTSTRAP_ONLY'
|
|
35
|
+
end
|
|
36
|
+
return 'listed in BOOTSTRAP_EXCLUDE' if excluded_by_env?(name, stage)
|
|
37
|
+
return 'disabled in config/hooks.yml' if excluded_by_config?(name, stage)
|
|
38
|
+
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def excluded_by_env?(name, stage)
|
|
45
|
+
entry = @exclude[name] || @exclude[name.to_sym]
|
|
46
|
+
return false if entry.nil?
|
|
47
|
+
|
|
48
|
+
entry.fetch(stage, true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def excluded_by_config?(name, stage)
|
|
52
|
+
excluded = @data.fetch('excluded_hooks', {})
|
|
53
|
+
value = excluded[name] || excluded[name.to_s] || excluded[name.to_sym]
|
|
54
|
+
case value
|
|
55
|
+
when Hash
|
|
56
|
+
normalize_hash(value).fetch(stage, false)
|
|
57
|
+
when Array
|
|
58
|
+
value.map!(&:to_s)
|
|
59
|
+
value.include?(stage.to_s)
|
|
60
|
+
when true
|
|
61
|
+
true
|
|
62
|
+
else
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_env_list(raw)
|
|
68
|
+
return {} if raw.nil? || raw.strip.empty?
|
|
69
|
+
|
|
70
|
+
raw.split(',').each_with_object(Hash.new { |h, k| h[k] = {} }) do |entry, acc|
|
|
71
|
+
hook, stage = entry.strip.split(':', 2)
|
|
72
|
+
next if hook.nil? || hook.empty?
|
|
73
|
+
|
|
74
|
+
if stage.nil?
|
|
75
|
+
acc[hook][:'pre'] = true
|
|
76
|
+
acc[hook][:'post'] = true
|
|
77
|
+
else
|
|
78
|
+
acc[hook][stage.to_sym] = true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def normalize_hash(hash)
|
|
84
|
+
hash.transform_keys(&:to_sym)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_file
|
|
88
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
89
|
+
|
|
90
|
+
YAML.safe_load(File.read(CONFIG_FILE)) || {}
|
|
91
|
+
rescue Psych::SyntaxError => e
|
|
92
|
+
Bootstrap::Display.warn("Invalid YAML in #{CONFIG_FILE}: #{e.message}")
|
|
93
|
+
{}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
require_relative 'display'
|
|
6
|
+
require_relative 'system'
|
|
7
|
+
|
|
8
|
+
module Bootstrap
|
|
9
|
+
class HookContext
|
|
10
|
+
include FileUtils
|
|
11
|
+
|
|
12
|
+
attr_reader :name, :stage
|
|
13
|
+
attr_accessor :configurator
|
|
14
|
+
|
|
15
|
+
def initialize(name:, stage:, configurator: nil)
|
|
16
|
+
@name = name
|
|
17
|
+
@stage = stage.to_sym
|
|
18
|
+
@configurator = configurator
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def header(message)
|
|
22
|
+
Bootstrap::Display.header(message)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def info(message)
|
|
26
|
+
Bootstrap::Display.info(message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def warn(message)
|
|
30
|
+
Bootstrap::Display.warn(message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def success(message)
|
|
34
|
+
Bootstrap::Display.success(message)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fail(message)
|
|
38
|
+
Bootstrap::Display.fail(message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run(command, allow_failure: false, env: {})
|
|
42
|
+
# Optimization: Check if package is already installed
|
|
43
|
+
if command.is_a?(String) && command.start_with?('/opt/homebrew/bin/brew install')
|
|
44
|
+
if command.include?('--formula')
|
|
45
|
+
formula = command.match(/--formula\s+([^\s]+)/)&.captures&.first
|
|
46
|
+
if formula && @configurator&.formula_installed?(formula)
|
|
47
|
+
Bootstrap::Logger.log("Skipping #{formula} (already installed)")
|
|
48
|
+
return true
|
|
49
|
+
end
|
|
50
|
+
elsif command.include?('--cask')
|
|
51
|
+
cask = command.match(/--cask\s+([^\s]+)/)&.captures&.first
|
|
52
|
+
if cask && @configurator&.cask_installed?(cask)
|
|
53
|
+
Bootstrap::Logger.log("Skipping #{cask} (already installed)")
|
|
54
|
+
return true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Bootstrap::System.run(command, allow_failure: allow_failure, env: env, dry_run: dry_run?, quiet: @configurator&.quiet)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def run!(command, env: {})
|
|
63
|
+
Bootstrap::System.run!(command, env: env, dry_run: dry_run?, quiet: @configurator&.quiet)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def home_path(*segments)
|
|
67
|
+
File.join(Dir.home, *segments)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def scripts_root
|
|
71
|
+
ENV.fetch('SCRIPT_DIR', File.expand_path('../../..', __dir__))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def hooks_root
|
|
75
|
+
ENV.fetch('HOOKS_DIR', File.join(scripts_root, 'Hooks'))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def configs_root
|
|
79
|
+
ENV.fetch('CONFIGS_DIR', File.join(scripts_root, 'Configs'))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def hook_path(*segments)
|
|
83
|
+
File.join(hooks_root, name, *segments)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ensure_directory(path)
|
|
87
|
+
return if dry_run?
|
|
88
|
+
FileUtils.mkdir_p(path)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def remove_path(path)
|
|
92
|
+
if dry_run?
|
|
93
|
+
Bootstrap::Display.info("[DRY-RUN] Removing #{path}")
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if File.directory?(path)
|
|
98
|
+
FileUtils.rm_rf(path)
|
|
99
|
+
else
|
|
100
|
+
FileUtils.rm_f(path)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def copy(source, destination)
|
|
105
|
+
if dry_run?
|
|
106
|
+
Bootstrap::Display.info("[DRY-RUN] Copying #{source} to #{destination}")
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if File.directory?(source)
|
|
111
|
+
FileUtils.cp_r(source, destination)
|
|
112
|
+
else
|
|
113
|
+
ensure_directory(File.dirname(destination))
|
|
114
|
+
FileUtils.cp(source, destination)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def dry_run?
|
|
121
|
+
@configurator&.dry_run
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'display'
|
|
4
|
+
require_relative 'hook_config'
|
|
5
|
+
require_relative 'hook_context'
|
|
6
|
+
|
|
7
|
+
module Bootstrap
|
|
8
|
+
module Hooks
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run(name, stage:, configurator: current_configurator)
|
|
12
|
+
stage = stage.to_sym
|
|
13
|
+
config = Bootstrap::HookConfig.instance
|
|
14
|
+
|
|
15
|
+
if config.skipped?(name, stage)
|
|
16
|
+
reason = config.reason_for_skip(name, stage)
|
|
17
|
+
Bootstrap::Display.info("Skipping #{name} #{stage} hook (#{reason})")
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context = Bootstrap::HookContext.new(name: name, stage: stage, configurator: configurator)
|
|
22
|
+
yield context
|
|
23
|
+
Bootstrap::Display.success("#{name} #{stage} hook complete")
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Bootstrap::Display.fail("#{name} #{stage} hook failed: #{e.message}")
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with_configurator(configurator)
|
|
30
|
+
previous = current_configurator
|
|
31
|
+
self.current_configurator = configurator
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
self.current_configurator = previous
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def current_configurator
|
|
38
|
+
Thread.current[:bootstrap_current_configurator]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_configurator=(configurator)
|
|
42
|
+
Thread.current[:bootstrap_current_configurator] = configurator
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bootstrap
|
|
4
|
+
class Logger
|
|
5
|
+
LOG_FILE = 'install.log'
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def init
|
|
9
|
+
File.write(LOG_FILE, "Bootstrap Log - #{Time.now}\n========================================\n\n")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def log(message)
|
|
13
|
+
File.open(LOG_FILE, 'a') do |f|
|
|
14
|
+
f.puts("[#{Time.now.strftime('%H:%M:%S')}] #{message}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def error(message)
|
|
19
|
+
log("ERROR: #{message}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Bootstrap
|
|
6
|
+
class Menu
|
|
7
|
+
def initialize(options)
|
|
8
|
+
@options = options
|
|
9
|
+
@selected_index = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
print "\e[?25l" # Hide cursor
|
|
14
|
+
loop do
|
|
15
|
+
render
|
|
16
|
+
key = read_key
|
|
17
|
+
case key
|
|
18
|
+
when :up
|
|
19
|
+
@selected_index = (@selected_index - 1) % @options.size
|
|
20
|
+
when :down
|
|
21
|
+
@selected_index = (@selected_index + 1) % @options.size
|
|
22
|
+
when :enter
|
|
23
|
+
print "\e[?25h" # Show cursor
|
|
24
|
+
# Clear the menu
|
|
25
|
+
# Move up by options size + 2 (header + spacing)
|
|
26
|
+
print "\e[#{@options.size + 2}A"
|
|
27
|
+
print "\e[J" # Clear from cursor to end of screen
|
|
28
|
+
return @options[@selected_index][:value]
|
|
29
|
+
when :ctrl_c, :q
|
|
30
|
+
print "\e[?25h" # Show cursor
|
|
31
|
+
puts "\nExiting..."
|
|
32
|
+
exit
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
ensure
|
|
36
|
+
print "\e[?25h" # Ensure cursor is shown on exit
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def render
|
|
42
|
+
# Move cursor up by the number of options to overwrite
|
|
43
|
+
print "\e[#{@options.size + 2}A" if @rendered_once
|
|
44
|
+
@rendered_once = true
|
|
45
|
+
|
|
46
|
+
puts "\n \e[1;34mSelect an action (Use Arrow Keys):\e[0m"
|
|
47
|
+
@options.each_with_index do |option, index|
|
|
48
|
+
prefix = index == @selected_index ? "\e[1;32m> \e[0m" : " "
|
|
49
|
+
label = index == @selected_index ? "\e[1;32m#{option[:label]}\e[0m" : option[:label]
|
|
50
|
+
puts " #{prefix}#{label}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read_key
|
|
55
|
+
char = STDIN.getch
|
|
56
|
+
if char == "\e"
|
|
57
|
+
char << STDIN.read_nonblock(3) rescue nil
|
|
58
|
+
char << STDIN.read_nonblock(2) rescue nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
case char
|
|
62
|
+
when "\e[A", "k" then :up
|
|
63
|
+
when "\e[B", "j" then :down
|
|
64
|
+
when "\r", "\n" then :enter
|
|
65
|
+
when "\u0003", "q" then :ctrl_c
|
|
66
|
+
else :unknown
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'display'
|
|
4
|
+
|
|
5
|
+
module Bootstrap
|
|
6
|
+
class Spinner
|
|
7
|
+
FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
8
|
+
INTERVAL = 0.1
|
|
9
|
+
|
|
10
|
+
def self.spin(message)
|
|
11
|
+
spinner = new(message)
|
|
12
|
+
spinner.start
|
|
13
|
+
begin
|
|
14
|
+
yield spinner
|
|
15
|
+
ensure
|
|
16
|
+
spinner.stop
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(message)
|
|
21
|
+
@message = message
|
|
22
|
+
@stop = false
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update(message)
|
|
27
|
+
@mutex.synchronize { @message = message }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def start
|
|
31
|
+
@thread = Thread.new do
|
|
32
|
+
i = 0
|
|
33
|
+
loop do
|
|
34
|
+
break if @stop
|
|
35
|
+
frame = FRAMES[i % FRAMES.length]
|
|
36
|
+
msg = @mutex.synchronize { @message }
|
|
37
|
+
print "\r\e[K [ \e[1;34m#{frame}\e[0m ] #{msg}"
|
|
38
|
+
i += 1
|
|
39
|
+
sleep INTERVAL
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stop
|
|
45
|
+
@stop = true
|
|
46
|
+
@thread.join if @thread
|
|
47
|
+
print "\r\e[K"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
require_relative 'display'
|
|
7
|
+
|
|
8
|
+
module Bootstrap
|
|
9
|
+
module System
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def run(command, allow_failure: false, env: {}, dry_run: false, quiet: false)
|
|
13
|
+
cmd_str = command.is_a?(Array) ? command.join(' ') : command
|
|
14
|
+
|
|
15
|
+
if dry_run
|
|
16
|
+
# Transient output for dry-run as requested
|
|
17
|
+
# If quiet, we don't print even in dry run?
|
|
18
|
+
# Actually, in dry run we usually want to see what's happening.
|
|
19
|
+
# But if spinner is active, spinner shows "Installing X".
|
|
20
|
+
# We don't need "Running: brew install X".
|
|
21
|
+
# So yes, respect quiet.
|
|
22
|
+
unless quiet
|
|
23
|
+
Bootstrap::Display.transient("[DRY-RUN] #{cmd_str}")
|
|
24
|
+
end
|
|
25
|
+
# Log it
|
|
26
|
+
Bootstrap::Logger.log("[DRY-RUN] #{cmd_str}")
|
|
27
|
+
return true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless quiet
|
|
31
|
+
Bootstrap::Display.transient("Running: #{cmd_str}...")
|
|
32
|
+
end
|
|
33
|
+
Bootstrap::Logger.log("EXEC: #{cmd_str}")
|
|
34
|
+
|
|
35
|
+
status = nil
|
|
36
|
+
args = [env]
|
|
37
|
+
if command.is_a?(Array)
|
|
38
|
+
args.concat(command)
|
|
39
|
+
else
|
|
40
|
+
args.push(command)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
output_buffer = ""
|
|
44
|
+
|
|
45
|
+
# Capture output and log it, but don't print to terminal unless error
|
|
46
|
+
Open3.popen2e(*args) do |_stdin, stdout_err, wait_thread|
|
|
47
|
+
stdout_err.each do |line|
|
|
48
|
+
output_buffer += line
|
|
49
|
+
Bootstrap::Logger.log(" > #{line.strip}")
|
|
50
|
+
end
|
|
51
|
+
status = wait_thread.value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if status.success?
|
|
55
|
+
# Success: Log it, but do NOT persist to terminal.
|
|
56
|
+
# This ensures the "next line should clear the same line" behavior.
|
|
57
|
+
Bootstrap::Logger.log("COMPLETED: #{cmd_str}")
|
|
58
|
+
return true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
message = "Command failed (#{status.exitstatus}): #{cmd_str}"
|
|
62
|
+
Bootstrap::Logger.error(message)
|
|
63
|
+
Bootstrap::Logger.error("Output:\n#{output_buffer}")
|
|
64
|
+
|
|
65
|
+
allow_failure ? Bootstrap::Display.warn(message) : raise(message)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run!(command, env: {}, dry_run: false, quiet: false)
|
|
69
|
+
run(command, allow_failure: false, env: env, dry_run: dry_run, quiet: quiet)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def run_script(path, env: {})
|
|
73
|
+
raise "Script not found: #{path}" unless File.exist?(path)
|
|
74
|
+
|
|
75
|
+
run!("bash #{Shellwords.escape(path)}", env: env)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Tweaks
|
|
2
|
+
|
|
3
|
+
# Helper for writing defaults
|
|
4
|
+
def write_defaults(domain, key, value)
|
|
5
|
+
execute("defaults write #{domain} #{key} #{value}")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module HomebrewTweaks
|
|
10
|
+
def self.apply
|
|
11
|
+
execute("brew analytics off")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module FinderTweaks
|
|
16
|
+
def self.apply
|
|
17
|
+
write_defaults("NSGlobalDomain", "AppleShowAllExtensions", "true")
|
|
18
|
+
execute("chflags nohidden ~/Library")
|
|
19
|
+
execute("defaults write com.apple.finder ShowPathbar -bool true")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module DockTweaks
|
|
24
|
+
def self.apply
|
|
25
|
+
write_defaults("com.apple.dock", "autohide", "true")
|
|
26
|
+
write_defaults("com.apple.dock", "tilesize", "48")
|
|
27
|
+
write_defaults("com.apple.dock", "mineffect", '"suck"')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module KeyboardMouseTweaks
|
|
32
|
+
def self.apply
|
|
33
|
+
write_defaults("NSGlobalDomain", "KeyRepeat", "2")
|
|
34
|
+
write_defaults("NSGlobalDomain", "InitialKeyRepeat", "15")
|
|
35
|
+
write_defaults("-g", "com.apple.trackpad.scaling", "1")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module TerminalTweaks
|
|
40
|
+
def self.apply
|
|
41
|
+
write_defaults("com.apple.terminal", "StringEncodings", "-array 4")
|
|
42
|
+
write_defaults("com.apple.Terminal", "FocusFollowsMouse", "true")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tweak_macOS_configuration
|
|
47
|
+
HomebrewTweaks.apply
|
|
48
|
+
FinderTweaks.apply
|
|
49
|
+
DockTweaks.apply
|
|
50
|
+
KeyboardMouseTweaks.apply
|
|
51
|
+
TerminalTweaks.apply
|
|
52
|
+
# Add more categories here as needed
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run_tweaks(modules)
|
|
56
|
+
modules.each { |mod| mod.apply }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
data/Hooks/core/pre.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../core/common'
|
|
4
|
+
require_relative '../core/library'
|
|
5
|
+
|
|
6
|
+
Bootstrap::Hooks.run('core', stage: :pre) do |hook|
|
|
7
|
+
configurator = hook.configurator || Bootstrap::Configurator.new
|
|
8
|
+
|
|
9
|
+
Bootstrap::Display.header("Adding user #{ENV.fetch('USER', 'unknown')} to sudoers")
|
|
10
|
+
configurator.add_to_sudoers
|
|
11
|
+
|
|
12
|
+
configurator.set_system_hostname
|
|
13
|
+
|
|
14
|
+
Bootstrap::Display.header('Setting up Python environment (uv)')
|
|
15
|
+
configurator.setup_uv
|
|
16
|
+
end
|
|
17
|
+
|