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.
Files changed (204) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +4 -0
  3. data/.gitattributes +1 -0
  4. data/.github/workflows/publish-gem.yml +36 -0
  5. data/.gitignore +316 -0
  6. data/.shellcheckrc +1 -0
  7. data/.stow-local-ignore +23 -0
  8. data/.vscode/settings.json +2 -0
  9. data/Configs/aerospace/.config/aerospace/aerospace.toml +265 -0
  10. data/Configs/alacritty/.config/alacritty/alacritty.toml +31 -0
  11. data/Configs/conda/.condarc +1 -0
  12. data/Configs/continue/.continue/.continueignore +0 -0
  13. data/Configs/continue/.continue/.continuerc.json +3 -0
  14. data/Configs/continue/.continue/config.json +48 -0
  15. data/Configs/docker/.docker/completion/completion.zsh +3 -0
  16. data/Configs/editorconfig/.editorconfig +16 -0
  17. data/Configs/fzf/.fzf/completion/completion.zsh +8 -0
  18. data/Configs/git/.config/gitconfig/core +83 -0
  19. data/Configs/git/.config/gitconfig/home +20 -0
  20. data/Configs/git/.config/gitconfig/template.txt +7 -0
  21. data/Configs/git/.config/gitconfig/work +23 -0
  22. data/Configs/git/.gitconfig +25 -0
  23. data/Configs/gpg/.gnupg/.#lk0x000000015060b4e0.macBook.pro.24088 +2 -0
  24. data/Configs/gpg/.gnupg/gpg-agent.conf +3 -0
  25. data/Configs/gpg/.gnupg/openpgp-revocs.d/DE999E2ACEAE3E8EAD660459688CB4D444FB1024.rev +28 -0
  26. data/Configs/gpg/.gnupg/private-keys-v1.d/0FF407C984AC03CCC60D924C5B315977DF45D5D0.key +7 -0
  27. data/Configs/gpg/.gnupg/private-keys-v1.d/2B711D7C4A8BE25322C9966B73E6842F7E64CD38.key +7 -0
  28. data/Configs/gpg/.gnupg/pubring.kbx +0 -0
  29. data/Configs/gpg/.gnupg/trustdb.gpg +0 -0
  30. data/Configs/kitty/.config/kitty/current-theme.conf +80 -0
  31. data/Configs/kitty/.config/kitty/kitty.conf +93 -0
  32. data/Configs/nvim/.config/nvim/.stylua.toml +6 -0
  33. data/Configs/nvim/.config/nvim/README.md +9 -0
  34. data/Configs/nvim/.config/nvim/init.lua +37 -0
  35. data/Configs/nvim/.config/nvim/lazy-lock.json +29 -0
  36. data/Configs/nvim/.config/nvim/lua/chadrc.lua +17 -0
  37. data/Configs/nvim/.config/nvim/lua/configs/conform.lua +15 -0
  38. data/Configs/nvim/.config/nvim/lua/configs/lazy.lua +47 -0
  39. data/Configs/nvim/.config/nvim/lua/configs/lspconfig.lua +24 -0
  40. data/Configs/nvim/.config/nvim/lua/configs/null_ls.lua +27 -0
  41. data/Configs/nvim/.config/nvim/lua/mappings.lua +10 -0
  42. data/Configs/nvim/.config/nvim/lua/options.lua +6 -0
  43. data/Configs/nvim/.config/nvim/lua/plugins/init.lua +25 -0
  44. data/Configs/oh-my-posh/.config/oh-my-posh/theme/config.json +71 -0
  45. data/Configs/pip/.config/pip/pip.conf +3 -0
  46. data/Configs/python/.pythonrc +25 -0
  47. data/Configs/starship/.config/starship/starship.toml +134 -0
  48. data/Configs/starship/.config/starship/themes/frappe.toml +27 -0
  49. data/Configs/starship/.config/starship/themes/latte.toml +27 -0
  50. data/Configs/starship/.config/starship/themes/macchiato.toml +27 -0
  51. data/Configs/starship/.config/starship/themes/mocha.toml +27 -0
  52. data/Configs/tmux/.tmux.conf +21 -0
  53. data/Configs/wezterm/.config/wezterm/core/colors.lua +10 -0
  54. data/Configs/wezterm/.config/wezterm/core/font.lua +5 -0
  55. data/Configs/wezterm/.config/wezterm/core/helper.lua +22 -0
  56. data/Configs/wezterm/.config/wezterm/core/init.lua +20 -0
  57. data/Configs/wezterm/.config/wezterm/core/keybindings/init.lua +16 -0
  58. data/Configs/wezterm/.config/wezterm/core/keybindings/macos.lua +106 -0
  59. data/Configs/wezterm/.config/wezterm/core/keybindings/windows.lua +99 -0
  60. data/Configs/wezterm/.config/wezterm/core/launch.lua +41 -0
  61. data/Configs/wezterm/.config/wezterm/core/maximized.lua +5 -0
  62. data/Configs/wezterm/.config/wezterm/core/mousebindings.lua +23 -0
  63. data/Configs/wezterm/.config/wezterm/core/tab_title.lua +26 -0
  64. data/Configs/wezterm/.config/wezterm/core/window.lua +8 -0
  65. data/Configs/wezterm/.config/wezterm/wezterm.lua +83 -0
  66. data/Configs/zellij/.config/zellij/config.kdl +410 -0
  67. data/Configs/zellij/.config/zellij/layouts/default.kdl +159 -0
  68. data/Configs/zellij/.config/zellij/plugins/room.wasm +0 -0
  69. data/Configs/zellij/.config/zellij/plugins/zjstatus.wasm +0 -0
  70. data/Configs/zsh/.hushlogin +0 -0
  71. data/Configs/zsh/.localrc +42 -0
  72. data/Configs/zsh/.profile +3 -0
  73. data/Configs/zsh/.shellcheckrc +1 -0
  74. data/Configs/zsh/.zprofile +3 -0
  75. data/Configs/zsh/.zsh/autoload/backup_restore +84 -0
  76. data/Configs/zsh/.zsh/autoload/cat +8 -0
  77. data/Configs/zsh/.zsh/autoload/change_wallpaper +1 -0
  78. data/Configs/zsh/.zsh/autoload/clean_dstore +6 -0
  79. data/Configs/zsh/.zsh/autoload/clean_pycache +6 -0
  80. data/Configs/zsh/.zsh/autoload/convert_mkv_to_mp4 +14 -0
  81. data/Configs/zsh/.zsh/autoload/create_macos_installer +26 -0
  82. data/Configs/zsh/.zsh/autoload/download +71 -0
  83. data/Configs/zsh/.zsh/autoload/fail +2 -0
  84. data/Configs/zsh/.zsh/autoload/flush_dns +6 -0
  85. data/Configs/zsh/.zsh/autoload/info +1 -0
  86. data/Configs/zsh/.zsh/autoload/ls +8 -0
  87. data/Configs/zsh/.zsh/autoload/reset_beyond_compare +15 -0
  88. data/Configs/zsh/.zsh/autoload/reset_final_cut_pro +5 -0
  89. data/Configs/zsh/.zsh/autoload/reset_launch_pad +3 -0
  90. data/Configs/zsh/.zsh/autoload/reset_open_list +3 -0
  91. data/Configs/zsh/.zsh/autoload/speedup_terminal +6 -0
  92. data/Configs/zsh/.zsh/autoload/start_aria2_server +1 -0
  93. data/Configs/zsh/.zsh/autoload/success +1 -0
  94. data/Configs/zsh/.zsh/autoload/update_system +19 -0
  95. data/Configs/zsh/.zsh/autoload/user +1 -0
  96. data/Configs/zsh/.zsh/autoload/vim +9 -0
  97. data/Configs/zsh/.zsh/autoload/warn +1 -0
  98. data/Configs/zsh/.zsh/completion/aliases.zsh +3 -0
  99. data/Configs/zsh/.zsh/completion/completion.zsh +33 -0
  100. data/Configs/zsh/.zsh/completion/config.zsh +118 -0
  101. data/Configs/zsh/.zsh/completion/exports.zsh +108 -0
  102. data/Configs/zsh/.zsh/completion/fpath.zsh +8 -0
  103. data/Configs/zsh/.zsh_history +283 -0
  104. data/Configs/zsh/.zshenv +2 -0
  105. data/Configs/zsh/.zshrc +54 -0
  106. data/Gemfile +3 -0
  107. data/Gemfile.lock +87 -0
  108. data/Hooks/adguard/pre.rb +7 -0
  109. data/Hooks/aerospace/pre.rb +8 -0
  110. data/Hooks/aide/post.rb +7 -0
  111. data/Hooks/aide/pre.rb +7 -0
  112. data/Hooks/alacritty/pre.rb +7 -0
  113. data/Hooks/alfred/pre.rb +7 -0
  114. data/Hooks/atuin/post.rb +27 -0
  115. data/Hooks/atuin/pre.rb +7 -0
  116. data/Hooks/bartender/pre.rb +7 -0
  117. data/Hooks/bun/pre.rb +7 -0
  118. data/Hooks/carapace/pre.rb +7 -0
  119. data/Hooks/cargo/pre.rb +7 -0
  120. data/Hooks/choosy/pre.rb +7 -0
  121. data/Hooks/controld/pre.rb +7 -0
  122. data/Hooks/core/common.rb +36 -0
  123. data/Hooks/core/final.rb +23 -0
  124. data/Hooks/core/lib/configurator.rb +593 -0
  125. data/Hooks/core/lib/display.rb +120 -0
  126. data/Hooks/core/lib/hook_config.rb +96 -0
  127. data/Hooks/core/lib/hook_context.rb +124 -0
  128. data/Hooks/core/lib/hooks.rb +45 -0
  129. data/Hooks/core/lib/logger.rb +23 -0
  130. data/Hooks/core/lib/menu.rb +70 -0
  131. data/Hooks/core/lib/spinner.rb +50 -0
  132. data/Hooks/core/lib/system.rb +78 -0
  133. data/Hooks/core/lib/tweaks.rb +59 -0
  134. data/Hooks/core/library.rb +3 -0
  135. data/Hooks/core/pre.rb +17 -0
  136. data/Hooks/ctrld/pre.rb +7 -0
  137. data/Hooks/deskpad/pre.rb +7 -0
  138. data/Hooks/fonts/pre.rb +60 -0
  139. data/Hooks/ghostty/pre.rb +10 -0
  140. data/Hooks/git/post.rb +20 -0
  141. data/Hooks/git/pre.rb +7 -0
  142. data/Hooks/gpg/post.rb +16 -0
  143. data/Hooks/gpg/pre.rb +7 -0
  144. data/Hooks/ice-hidemenubar/pre.rb +7 -0
  145. data/Hooks/iterm/IC Green PPL.itermcolors +344 -0
  146. data/Hooks/iterm/chalkboard.webp +0 -0
  147. data/Hooks/iterm/com.googlecode.iterm2.plist +2371 -0
  148. data/Hooks/iterm/post.rb +7 -0
  149. data/Hooks/iterm/pre.rb +7 -0
  150. data/Hooks/jujutsu/post.rb +13 -0
  151. data/Hooks/jujutsu/pre.rb +7 -0
  152. data/Hooks/keka/pre.rb +7 -0
  153. data/Hooks/kitty/pre.rb +7 -0
  154. data/Hooks/lazyvim/pre.rb +7 -0
  155. data/Hooks/lima/pre.rb +7 -0
  156. data/Hooks/little-snitch/pre.rb +7 -0
  157. data/Hooks/microsoft-edge/pre.rb +7 -0
  158. data/Hooks/mos/pre.rb +7 -0
  159. data/Hooks/nvchad/pre.rb +7 -0
  160. data/Hooks/oh-my-posh/config.json +58 -0
  161. data/Hooks/oh-my-posh/post.rb +7 -0
  162. data/Hooks/oh-my-posh/pre.rb +7 -0
  163. data/Hooks/pearcleaner/pre.rb +7 -0
  164. data/Hooks/pycharm/pre.rb +7 -0
  165. data/Hooks/raindropio/pre.rb +7 -0
  166. data/Hooks/rectangle/RectangleConfig.json +258 -0
  167. data/Hooks/rectangle/com.knollsoft.Rectangle.plist +0 -0
  168. data/Hooks/rectangle/post.rb +22 -0
  169. data/Hooks/rectangle/pre.rb +7 -0
  170. data/Hooks/slack/pre.rb +7 -0
  171. data/Hooks/soundsource/pre.rb +7 -0
  172. data/Hooks/ssh/post.rb +8 -0
  173. data/Hooks/starship/pre.rb +7 -0
  174. data/Hooks/sublime_text/post.rb +25 -0
  175. data/Hooks/sublime_text/pre.rb +7 -0
  176. data/Hooks/swiftformat-for-xcode/pre.rb +7 -0
  177. data/Hooks/syncthing/pre.rb +7 -0
  178. data/Hooks/synology/pre.rb +7 -0
  179. data/Hooks/tailscale/pre.rb +7 -0
  180. data/Hooks/tmux/post.rb +7 -0
  181. data/Hooks/tmux/pre.rb +7 -0
  182. data/Hooks/topnotch/pre.rb +7 -0
  183. data/Hooks/transmission/pre.rb +7 -0
  184. data/Hooks/vscode/extensions.txt +16 -0
  185. data/Hooks/vscode/keybindings.json +26 -0
  186. data/Hooks/vscode/post.rb +29 -0
  187. data/Hooks/vscode/pre.rb +9 -0
  188. data/Hooks/vscode/settings.json +139 -0
  189. data/Hooks/vscode/style.css +29 -0
  190. data/Hooks/wezterm/pre.rb +7 -0
  191. data/Hooks/wins/pre.rb +7 -0
  192. data/Hooks/zed/pre.rb +7 -0
  193. data/Hooks/zellij/post.rb +7 -0
  194. data/Hooks/zellij/pre.rb +7 -0
  195. data/Hooks/zoxide/pre.rb +7 -0
  196. data/Hooks/zsh/pre.rb +19 -0
  197. data/LICENSE +21 -0
  198. data/README.md +100 -0
  199. data/bin/dotfiles-tui +6 -0
  200. data/bootstrap.rb +186 -0
  201. data/dotfiles-tui.gemspec +47 -0
  202. data/lib/dotfiles_tui/version.rb +5 -0
  203. data/lib/dotfiles_tui.rb +15 -0
  204. metadata +275 -0
@@ -0,0 +1,593 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ require_relative '../common'
7
+ require_relative 'hooks'
8
+ require_relative 'system'
9
+
10
+ module Bootstrap
11
+ class Configurator
12
+ FORMULAE = %w[
13
+ aria2 bat bash bottom cmake coreutils fd ripgrep
14
+ fzf gcc git jj git-lfs gnutls go gnupg
15
+ pinentry-mac gpg2 mas mcfly neovim node
16
+ oh-my-posh openjdk openssh openssl procs lsd
17
+ readline rsync ruby shellcheck shfmt
18
+ ssh-copy-id tlrc watch wget zsh zsh-completions
19
+ ].freeze
20
+
21
+ FORMULAE_DISABLED = %w[rust].freeze
22
+
23
+ MAS_APPS = [
24
+ { name: '1Blocker', id: 1_365_531_024, enabled: true },
25
+ { name: 'Bitwarden', id: 1_352_778_147, enabled: true },
26
+ { name: 'Auto HD FPS for YouTube', id: 1_546_729_687, enabled: true },
27
+ { name: 'Numbers', id: 409_203_825, enabled: true },
28
+ { name: 'Pages', id: 409_201_541, enabled: true },
29
+ { name: 'The Unarchiver', id: 425_424_353, enabled: true },
30
+ { name: 'Save to Raindrop.io', id: 1_549_370_672, enabled: true },
31
+ { name: 'Amphetamine', id: 937_984_704, enabled: true },
32
+ { name: 'Velja', id: 1_607_635_845, enabled: true },
33
+ { name: 'CotEditor', id: 1_024_640_650, enabled: true },
34
+ { name: 'Super Agent for Safari', id: 1_568_262_835, enabled: true },
35
+ { name: 'Sequel Ace', id: 1_518_036_000, enabled: false },
36
+ { name: 'WireGuard', id: 1_451_685_025, enabled: false },
37
+ { name: 'Poolsuite FM', id: 1_514_817_810, enabled: false },
38
+ { name: 'AdGuard for Safari', id: 1_440_147_259, enabled: false },
39
+ { name: 'Xcode', id: 497_799_835, enabled: true }
40
+ ].freeze
41
+
42
+ PRIORITY_HOOKS = %w[core cargo mos zsh starship lunarvim].freeze
43
+
44
+ attr_reader :script_dir, :hooks_dir, :configs_dir, :core_dir, :dry_run
45
+ attr_accessor :quiet
46
+
47
+ def initialize(dry_run: false)
48
+ @script_dir = ENV.fetch('SCRIPT_DIR', File.expand_path('../../..', __dir__))
49
+ @hooks_dir = ENV.fetch('HOOKS_DIR', File.join(@script_dir, 'Hooks'))
50
+ @configs_dir = ENV.fetch('CONFIGS_DIR', File.join(@script_dir, 'Configs'))
51
+ @core_dir = ENV.fetch('CORE_DIR', File.join(@hooks_dir, 'core'))
52
+ @prereq_marker = File.join(Dir.tmpdir, 'has_pre_requisite_ran')
53
+ @user = ENV.fetch('USER', nil)
54
+ @dry_run = dry_run
55
+ end
56
+
57
+ def pre_requisite
58
+ if prerequisites_ran?
59
+ Bootstrap::Display.info('Pre-requisite check completed.')
60
+ return
61
+ end
62
+
63
+ ensure_sudo!
64
+ add_to_sudoers
65
+ install_cargo_toolchain
66
+ ensure_homebrew_installed
67
+ ensure_stow_installed
68
+ setup_env_file
69
+ run_priority_hooks
70
+ mark_prerequisites_done
71
+ end
72
+
73
+ def install_brew_formulas
74
+ apply_brew_environment
75
+ Bootstrap::Display.header('Installing formulae')
76
+
77
+ Bootstrap::Spinner.spin('Installing formulae...') do |spinner|
78
+ FORMULAE.each do |formula|
79
+ install_formula(formula, spinner)
80
+ end
81
+ end
82
+ Bootstrap::Display.persist('All formulae installed', :success)
83
+ end
84
+
85
+ def install_brew_casks
86
+ apply_brew_environment
87
+ Bootstrap::Display.header('Installing casks')
88
+
89
+ Bootstrap::Spinner.spin('Installing casks...') do |spinner|
90
+ Dir.children(@hooks_dir).sort.each do |dir_name|
91
+ next if dir_name.start_with?('.')
92
+ next if PRIORITY_HOOKS.include?(dir_name)
93
+
94
+ install_hook(dir_name, spinner)
95
+ end
96
+ end
97
+ Bootstrap::Display.persist('All casks installed', :success)
98
+ end
99
+
100
+ def install_mas_apps
101
+ mas = find_binary('mas', ['/opt/homebrew/bin/mas'])
102
+ raise 'mas CLI is not installed. Exiting.' if mas.nil?
103
+
104
+ Bootstrap::Display.header('Installing Mac App Store applications')
105
+ Bootstrap::Display.info('Please login to the App Store if prompted.')
106
+
107
+ MAS_APPS.each do |app|
108
+ next unless app[:enabled]
109
+
110
+ Bootstrap::Display.info("Installing #{app[:name]}")
111
+ Bootstrap::System.run(%(#{mas} install #{app[:id]}), allow_failure: true, dry_run: @dry_run)
112
+ end
113
+ end
114
+
115
+ def link_configs
116
+ validate_directory(@configs_dir, 'Configs')
117
+ validate_directory(@hooks_dir, 'Hooks')
118
+ apply_brew_environment
119
+
120
+ Bootstrap::Spinner.spin('Linking Configs...') do |spinner|
121
+ Dir.children(@configs_dir).sort.each do |dir|
122
+ next if dir.start_with?('.')
123
+
124
+ dir_path = File.join(@configs_dir, dir)
125
+ next unless File.directory?(dir_path)
126
+
127
+ if package_linked?(dir)
128
+ Bootstrap::Logger.log("Skipping #{dir} (already linked)")
129
+ next
130
+ end
131
+
132
+ spinner.update("Linking #{dir}...")
133
+ self.quiet = true
134
+
135
+ run_hook(dir, :pre)
136
+ Bootstrap::Logger.log("Linking #{dir}")
137
+ Bootstrap::System.run(%(stow --adopt --target="#{Dir.home}" --dir="#{@configs_dir}" "#{dir}"), dry_run: @dry_run, quiet: true)
138
+ run_hook(dir, :post)
139
+
140
+ self.quiet = false
141
+ end
142
+ end
143
+ Bootstrap::Display.persist('All configs linked', :success)
144
+ end
145
+
146
+ def unlink_configs
147
+ validate_directory(@configs_dir, 'Configs')
148
+
149
+ Bootstrap::Spinner.spin('Unlinking Configs...') do |spinner|
150
+ Dir.children(@configs_dir).sort.each do |dir|
151
+ next if dir.start_with?('.')
152
+
153
+ dir_path = File.join(@configs_dir, dir)
154
+ next unless File.directory?(dir_path)
155
+
156
+ spinner.update("Unlinking #{dir}...")
157
+ Bootstrap::Logger.log("Unlinking #{dir}")
158
+ Bootstrap::System.run(%(stow --target="#{Dir.home}" --dir="#{@configs_dir}" --delete "#{dir}"), allow_failure: true, dry_run: @dry_run, quiet: true)
159
+ end
160
+ end
161
+ Bootstrap::Display.persist('All configs unlinked', :success)
162
+ end
163
+
164
+ def setup_uv
165
+ return Bootstrap::Display.info('uv already installed') if Dir.exist?(File.join(Dir.home, '.local', 'bin', 'uv'))
166
+
167
+ Bootstrap::Display.header('Installing Astral uv')
168
+ Bootstrap::System.run('curl -LsSf https://astral.sh/uv/install.sh | bash -s', dry_run: @dry_run)
169
+ Bootstrap::System.run("#{File.join(Dir.home, '.local', 'bin', 'uv')} python install && uv venv ~/.venv && source ~/.venv/bin/activate", allow_failure: true, dry_run: @dry_run)
170
+ end
171
+
172
+ def setup_env_file
173
+ env_source = File.join(@script_dir, '.env')
174
+ env_dest = File.join(Dir.home, '.env')
175
+
176
+ return unless File.exist?(env_source)
177
+
178
+ if File.exist?(env_dest) && !File.symlink?(env_dest)
179
+ Bootstrap::Display.warn("~/.env exists and is not a symlink. Skipping link.")
180
+ return
181
+ end
182
+
183
+ # Check if it points to the right place
184
+ if File.symlink?(env_dest) && File.readlink(env_dest) == env_source
185
+ Bootstrap::Display.info("~/.env already linked.")
186
+ return
187
+ end
188
+
189
+ Bootstrap::Display.header('Linking .env')
190
+ Bootstrap::System.run("ln -sf #{env_source} #{env_dest}", dry_run: @dry_run)
191
+ end
192
+
193
+ def inject_secrets(secrets_path)
194
+ return if secrets_path.nil? || secrets_path.empty?
195
+
196
+ unless File.directory?(secrets_path)
197
+ Bootstrap::Display.warn("Secrets directory not found at #{secrets_path}. Skipping injection.")
198
+ return
199
+ end
200
+
201
+ Bootstrap::Display.header("Injecting secrets from #{secrets_path}")
202
+
203
+ # Inject .env
204
+ secrets_env = File.join(secrets_path, '.env')
205
+ if File.exist?(secrets_env)
206
+ Bootstrap::Display.info("Copying .env from #{secrets_env}...")
207
+ Bootstrap::System.run("cp #{secrets_env} #{@script_dir}/.env", dry_run: @dry_run)
208
+ end
209
+
210
+ # Inject Configs
211
+ secrets_configs = File.join(secrets_path, 'Configs')
212
+ if File.directory?(secrets_configs)
213
+ Bootstrap::Display.info("Copying Configs from #{secrets_configs}...")
214
+ # We use cp_r with remove_destination: true to overwrite existing files/symlinks
215
+ # But we need to be careful. We want to merge directories, not replace them entirely if possible.
216
+ # FileUtils.cp_r merges directories.
217
+ Bootstrap::System.run("cp -R #{secrets_configs}/. #{@configs_dir}/", dry_run: @dry_run)
218
+ end
219
+
220
+ # Inject Hooks
221
+ secrets_hooks = File.join(secrets_path, 'Hooks')
222
+ if File.directory?(secrets_hooks)
223
+ Bootstrap::Display.info("Copying Hooks from #{secrets_hooks}...")
224
+ # Use shell globbing to include hidden files
225
+ # cp -R source/. dest/ usually works, but let's be explicit
226
+ Bootstrap::System.run("cp -R #{secrets_hooks}/. #{@hooks_dir}/", dry_run: @dry_run)
227
+ end
228
+ end
229
+
230
+ def setup_conda
231
+ Bootstrap::Display.header('Configuring Conda environment')
232
+ Bootstrap::System.run('conda init && source ~/.zshrc', dry_run: @dry_run)
233
+ Bootstrap::System.run('conda create -n default python=3.9.4 --yes', dry_run: @dry_run)
234
+ Bootstrap::System.run('conda config --set changeps1 False', dry_run: @dry_run)
235
+ end
236
+
237
+ def setup_rye
238
+ path = File.join(Dir.home, '.rye', 'env')
239
+ if Dir.exist?(path)
240
+ Bootstrap::Display.info('rye already installed')
241
+ return
242
+ end
243
+
244
+ Bootstrap::Display.header('Installing Astral rye')
245
+ Bootstrap::System.run('curl -sSf https://rye.astral.sh/get | RYE_INSTALL_OPTION="--yes" bash', dry_run: @dry_run)
246
+ end
247
+
248
+ def install_rosetta
249
+ Bootstrap::System.run('/usr/sbin/softwareupdate --install-rosetta --agree-to-license', allow_failure: true, dry_run: @dry_run)
250
+ end
251
+
252
+ def restart_launchpad
253
+ Bootstrap::System.run('defaults write com.apple.dock ResetLaunchPad -bool true && sudo killall Dock', allow_failure: true, dry_run: @dry_run)
254
+ end
255
+
256
+ def change_default_shell
257
+ zsh_path = find_binary('zsh')
258
+ return Bootstrap::Display.warn('Zsh not found, skipping default shell change.') if zsh_path.nil?
259
+
260
+ Bootstrap::System.run(%(echo "#{zsh_path}" | sudo tee -a /etc/shells), allow_failure: true, dry_run: @dry_run)
261
+ Bootstrap::System.run("chsh -s #{zsh_path}", allow_failure: true, dry_run: @dry_run)
262
+ Bootstrap::System.run("sudo chsh -s #{zsh_path}", allow_failure: true, dry_run: @dry_run)
263
+ end
264
+
265
+ def fix_insecure_dir_problems
266
+ commands = [
267
+ '[[ -e ~/.zcompdump* ]] && rm -f ~/.zcompdump*',
268
+ '[[ -n $(command -v compinit) ]] && compinit',
269
+ '[[ -n $(command -v compaudit) ]] && compaudit | xargs -n1 -r chmod g-w,o-w',
270
+ '[[ -d /usr/local/share ]] && chmod go-w /usr/local/share || printf ""'
271
+ ]
272
+
273
+ commands.each { |cmd| Bootstrap::System.run(cmd, allow_failure: true, dry_run: @dry_run) }
274
+ end
275
+
276
+ def tweak_macOS_configuration
277
+ commands = [
278
+ 'brew analytics off',
279
+ 'sudo spctl --master-disable',
280
+ 'defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false',
281
+ '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user',
282
+ 'defaults write NSGlobalDomain NSAutomaticQuoteSubstitutionEnabled -bool false',
283
+ 'defaults write NSGlobalDomain NSAutomaticDashSubstitutionEnabled -bool false',
284
+ 'defaults -currentHost write com.apple.ImageCapture disableHotPlug -bool true',
285
+ 'defaults write NSGlobalDomain AppleShowAllExtensions -bool true',
286
+ 'defaults write NSGlobalDomain KeyRepeat -int 0',
287
+ 'defaults write com.apple.finder ShowExternalHardDrivesOnDesktop -bool false',
288
+ 'defaults write com.apple.finder ShowStatusBar -bool true',
289
+ 'defaults write com.apple.finder FXEnableExtensionChangeWarning -bool false',
290
+ 'defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true',
291
+ 'defaults write com.apple.frameworks.diskimages skip-verify -bool true',
292
+ 'defaults write com.apple.frameworks.diskimages skip-verify-locked -bool true',
293
+ 'defaults write com.apple.frameworks.diskimages skip-verify-remote -bool true',
294
+ 'defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false',
295
+ 'defaults write com.apple.terminal StringEncodings -array 4',
296
+ 'defaults write com.apple.Terminal "Default Window Settings" -string "Pro"',
297
+ 'defaults write com.apple.Terminal "Startup Window Settings" -string "Pro"',
298
+ 'chflags nohidden ~/Library',
299
+ 'sudo chflags nohidden /Volumes',
300
+ 'defaults write NSGlobalDomain KeyRepeat -int 2',
301
+ 'defaults write NSGlobalDomain InitialKeyRepeat -int 15',
302
+ 'defaults write -g WebAutomaticTextReplacementEnabled -bool true',
303
+ 'defaults write NSGlobalDomain AppleShowScrollBars -string "Always"',
304
+ 'defaults write com.apple.dock mru-spaces -bool false',
305
+ 'defaults write com.apple.dock autohide-time-modifier -int 0',
306
+ 'defaults write com.apple.Terminal AppleShowScrollBars -string WhenScrolling',
307
+ 'defaults write com.apple.driver.AppleBluetoothMultitouch.mouse MouseHorizontalScroll -bool NO',
308
+ 'sudo defaults write /Library/Preferences/com.apple.loginwindow SHOWOTHERUSERS_MANAGED -bool FALSE',
309
+ 'sudo launchctl start com.apple.locate || true',
310
+ 'defaults write com.apple.QuickTimePlayerX MGPlayMovieOnOpen 1',
311
+ 'defaults write -g NSShowAppCentricOpenPanelInsteadOfUntitledFile -bool false',
312
+ 'defaults write com.apple.TextEdit "RichText" -bool "false"',
313
+ 'defaults write com.apple.CrashReporter DialogType none',
314
+ 'defaults write com.apple.LaunchServices LSQuarantine -bool NO',
315
+ 'defaults write com.apple.finder "ShowPathbar" -bool "true"',
316
+ 'defaults write com.apple.finder "_FXSortFoldersFirst" -bool "true"',
317
+ 'defaults write com.apple.finder "_FXSortFoldersFirstOnDesktop" -bool "true"',
318
+ 'defaults write com.apple.finder "FXDefaultSearchScope" -string "SCcf"',
319
+ 'defaults write com.apple.finder "FXRemoveOldTrashItems" -bool "true"',
320
+ 'defaults write com.apple.finder "FXEnableExtensionChangeWarning" -bool "false"',
321
+ 'defaults write NSGlobalDomain "NSTableViewDefaultSizeMode" -int "3"',
322
+ 'defaults write com.apple.finder "ShowHardDrivesOnDesktop" -bool "false"',
323
+ 'defaults write com.apple.finder "ShowExternalHardDrivesOnDesktop" -bool "false"',
324
+ 'defaults write com.apple.finder "ShowRemovableMediaOnDesktop" -bool "false"',
325
+ 'defaults write com.apple.finder "ShowMountedServersOnDesktop" -bool "true"',
326
+ 'defaults write com.apple.menuextra.clock "FlashDateSeparators" -bool "true"',
327
+ 'defaults write com.apple.dock "expose-group-apps" -bool "true"',
328
+ 'defaults write com.apple.spaces "spans-displays" -bool "false"',
329
+ %(defaults write com.apple.iphonesimulator "ScreenShotSaveLocation" -string "#{File.join(Dir.home, 'Pictures', 'Screenshots')}"),
330
+ %(defaults write com.apple.screencapture "location" -string "#{File.join(Dir.home, 'Pictures', 'Screenshots')}"),
331
+ 'defaults write com.apple.TimeMachine "DoNotOfferNewDisksForBackup" -bool "true"',
332
+ 'defaults write com.apple.dock "enable-spring-load-actions-on-all-items" -bool "true"',
333
+ 'defaults write com.apple.LaunchServices "LSQuarantine" -bool "false"',
334
+ 'defaults write com.apple.Terminal "FocusFollowsMouse" -bool "true"',
335
+ 'defaults write com.apple.dock "tilesize" -int "48"',
336
+ 'defaults write com.apple.dock "autohide" -bool "true"',
337
+ 'defaults write com.apple.dock autohide-delay -float 0',
338
+ 'defaults write com.apple.dock autohide-time-modifier -float 0.15',
339
+ 'killall Dock',
340
+ 'defaults write com.apple.dock "show-recents" -bool "false"',
341
+ 'defaults write com.apple.dock "mineffect" -string "suck"',
342
+ 'defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false',
343
+ 'defaults write -g com.apple.trackpad.scaling 1',
344
+ 'defaults write -g com.apple.mouse.scaling 2.0',
345
+ 'defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true',
346
+ 'defaults -currentHost write com.apple.ImageCapture disableHotPlug -bool true',
347
+ 'defaults write com.apple.dock tilesize -int 45',
348
+ 'defaults write com.apple.messageshelper.MessageController SOInputLineSettings -dict-add "automaticQuoteSubstitutionEnabled" -bool false',
349
+ 'defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false',
350
+ 'defaults write com.apple.mail DisableInlineAttachmentViewing -bool true',
351
+ 'defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false',
352
+ 'launchctl unload -w /System/Library/LaunchAgents/com.apple.rcd.plist'
353
+ ]
354
+
355
+ commands.each do |command|
356
+ Bootstrap::System.run(command, allow_failure: true, dry_run: @dry_run)
357
+ end
358
+ end
359
+
360
+ alias tweak_macos_configuration tweak_macOS_configuration
361
+
362
+ def add_to_sudoers
363
+ return if @user.nil? || @user.empty?
364
+
365
+ Bootstrap::Display.header("Ensuring sudoers entry for #{@user}")
366
+
367
+ script = <<~SHELL
368
+ set -e
369
+ sed -i '' -e 's/#includedir/@includedir/' /private/etc/sudoers
370
+ sudoers_path="/private/etc/sudoers.d/sudoers"
371
+ [ -f "$sudoers_path" ] || touch "$sudoers_path"
372
+ entry="#{@user} ALL=(ALL) NOPASSWD:ALL"
373
+ if ! grep -q "$entry" "$sudoers_path"; then
374
+ echo "$entry" >> "$sudoers_path"
375
+ fi
376
+ SHELL
377
+
378
+ Bootstrap::System.run(['sudo', 'bash', '-c', script], dry_run: @dry_run)
379
+ end
380
+
381
+ def set_system_hostname(hostname: 'macBook', domain: 'pro')
382
+ fqdn = [hostname, domain].compact.join('.').strip
383
+ Bootstrap::Display.header("Setting system hostname to #{fqdn}")
384
+ Bootstrap::System.run("sudo scutil --set ComputerName #{fqdn}", allow_failure: true, dry_run: @dry_run)
385
+ Bootstrap::System.run("sudo scutil --set LocalHostName #{hostname}", allow_failure: true, dry_run: @dry_run)
386
+ Bootstrap::System.run("sudo scutil --set HostName #{fqdn}", allow_failure: true, dry_run: @dry_run)
387
+ end
388
+
389
+ def formula_installed?(formula)
390
+ installed_formulae.include?(formula.split('/').last)
391
+ end
392
+
393
+ def cask_installed?(cask)
394
+ installed_casks.include?(cask.split('/').last)
395
+ end
396
+
397
+ def package_linked?(dir)
398
+ # Check if all top-level files in the config dir are symlinked in home
399
+ config_path = File.join(@configs_dir, dir)
400
+ return false unless File.directory?(config_path)
401
+
402
+ Dir.children(config_path).all? do |child|
403
+ next true if child == '.DS_Store'
404
+
405
+ source = File.join(config_path, child)
406
+ target = File.join(Dir.home, child)
407
+
408
+ # If it's a directory in source, stow usually symlinks the contents unless --adopt is used differently
409
+ # But standard stow symlinks the directory itself if it doesn't exist, or contents if it does.
410
+ # Simplest check: does target exist and is it a symlink pointing to source?
411
+ # Stow is complex, but let's check basic symlink existence.
412
+
413
+ if File.symlink?(target)
414
+ # Check if it points to the right place
415
+ begin
416
+ File.readlink(target) == source || File.readlink(target).include?(source)
417
+ rescue StandardError
418
+ false
419
+ end
420
+ else
421
+ # If target exists and is not a symlink, it's definitely not linked (conflict or adopted)
422
+ # If target doesn't exist, it's not linked
423
+ false
424
+ end
425
+ end
426
+ end
427
+
428
+ private
429
+
430
+ def installed_formulae
431
+ @installed_formulae ||= begin
432
+ return [] if @dry_run # In dry-run, we might assume nothing is installed or check system?
433
+ # Actually, checking system is fine in dry-run to simulate correctly.
434
+ `#{brew_path} list --formula -1`.split("\n")
435
+ rescue StandardError
436
+ []
437
+ end
438
+ end
439
+
440
+ def installed_casks
441
+ @installed_casks ||= begin
442
+ return [] if @dry_run
443
+ `#{brew_path} list --cask -1`.split("\n")
444
+ rescue StandardError
445
+ []
446
+ end
447
+ end
448
+
449
+ def prerequisites_ran?
450
+ File.exist?(@prereq_marker) && File.read(@prereq_marker).strip == 'true'
451
+ end
452
+
453
+ def mark_prerequisites_done
454
+ File.write(@prereq_marker, 'true')
455
+ end
456
+
457
+ def ensure_sudo!
458
+ result = system('sudo -n true')
459
+ return if result
460
+
461
+ Bootstrap::Display.info('Password may be required for installation:')
462
+ raise 'Failed to obtain sudo privileges. Exiting.' unless system('sudo -v')
463
+
464
+ Bootstrap::Display.info('Sudo access granted.')
465
+ end
466
+
467
+ def install_cargo_toolchain
468
+ cargo_path = File.join(Dir.home, '.cargo', 'bin', 'cargo')
469
+ return Bootstrap::Display.info('Cargo already installed.') if File.exist?(cargo_path)
470
+
471
+ Bootstrap::Display.header('Installing Rust toolchain')
472
+ Bootstrap::System.run("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --quiet -y --profile default", dry_run: @dry_run)
473
+ end
474
+
475
+ def ensure_homebrew_installed
476
+ return Bootstrap::Display.info('Homebrew already installed.') if find_binary('brew')
477
+
478
+ Bootstrap::Display.header('Installing Homebrew')
479
+ install_command = %(printf '\\r' | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)")
480
+ Bootstrap::System.run(install_command, dry_run: @dry_run)
481
+ append_to_file(File.join(Dir.home, '.zprofile'), 'eval "$(/opt/homebrew/bin/brew shellenv)"')
482
+ @brew_path = '/opt/homebrew/bin/brew' if File.exist?('/opt/homebrew/bin/brew')
483
+ apply_brew_environment
484
+ end
485
+
486
+ def ensure_stow_installed
487
+ stow = find_binary('stow')
488
+ return Bootstrap::Display.info('GNU Stow already installed.') if stow
489
+
490
+ Bootstrap::Display.header('Installing GNU Stow')
491
+ apply_brew_environment
492
+ Bootstrap::System.run("#{brew_path} install --quiet --formula stow", dry_run: @dry_run)
493
+ end
494
+
495
+ def run_priority_hooks
496
+ PRIORITY_HOOKS.each do |dir_name|
497
+ install_hook(dir_name) if File.directory?(File.join(@hooks_dir, dir_name))
498
+ end
499
+ end
500
+
501
+ def install_hook(dir_name, spinner = nil)
502
+ self.quiet = !!spinner
503
+ if spinner
504
+ spinner.update("Running hooks for #{dir_name}...")
505
+ end
506
+ run_hook(dir_name, :pre)
507
+ run_hook(dir_name, :post)
508
+ ensure
509
+ self.quiet = false
510
+ end
511
+
512
+ def run_hook(dir_name, stage)
513
+ script = File.join(@hooks_dir, dir_name, "#{stage}.rb")
514
+ return unless File.exist?(script)
515
+
516
+ Bootstrap::Hooks.with_configurator(self) do
517
+ load(script)
518
+ end
519
+ end
520
+
521
+ def install_formula(formula, spinner = nil)
522
+ if FORMULAE_DISABLED.include?(formula)
523
+ Bootstrap::Logger.log("Skipping #{formula} (disabled)")
524
+ return
525
+ end
526
+
527
+ if formula_installed?(formula)
528
+ Bootstrap::Logger.log("Skipping #{formula} (already installed)")
529
+ return
530
+ end
531
+
532
+ path = brew_path
533
+ raise 'Homebrew is not available.' if path.nil?
534
+
535
+ if spinner
536
+ spinner.update("Installing #{formula}...")
537
+ else
538
+ Bootstrap::Display.info("Installing #{formula}")
539
+ end
540
+
541
+ Bootstrap::Logger.log("Installing #{formula}")
542
+ # Use quiet: true if spinner is present to avoid conflict
543
+ Bootstrap::System.run("#{path} install --quiet --formula #{formula}", allow_failure: true, dry_run: @dry_run, quiet: !!spinner)
544
+ end
545
+
546
+ def apply_brew_environment
547
+ brew = brew_path
548
+ return if brew.nil?
549
+
550
+ command = %(eval "$("#{brew}" shellenv)" && env)
551
+ escaped = command.gsub("'", %q(\\\'))
552
+ output = `bash -lc '#{escaped}'`
553
+
554
+ output.each_line do |line|
555
+ key, value = line.strip.split('=', 2)
556
+ next if key.nil? || value.nil?
557
+
558
+ ENV[key] = value
559
+ end
560
+ end
561
+
562
+ def brew_path
563
+ return @brew_path if defined?(@brew_path) && @brew_path
564
+
565
+ candidate = find_binary('brew', ['/opt/homebrew/bin/brew', '/usr/local/bin/brew'])
566
+ @brew_path = candidate
567
+ end
568
+
569
+ def find_binary(name, fallbacks = [])
570
+ path = `command -v #{name}`.strip
571
+ return path unless path.nil? || path.empty?
572
+
573
+ fallbacks.each do |candidate|
574
+ return candidate if File.exist?(candidate)
575
+ end
576
+
577
+ nil
578
+ end
579
+
580
+ def validate_directory(directory, name)
581
+ raise "Error: #{name} directory not found." unless File.directory?(directory)
582
+ end
583
+
584
+ def append_to_file(path, content)
585
+ return if @dry_run
586
+ FileUtils.touch(path)
587
+ lines = File.read(path).split("\n")
588
+ return if lines.include?(content)
589
+
590
+ File.open(path, 'a') { |file| file.puts(content) }
591
+ end
592
+ end
593
+ end