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,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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/configurator'
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
+
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/common'
4
+
5
+ Bootstrap::Hooks.run('ctrld', stage: :pre) do |_hook|
6
+ # No actions defined. Enable this hook via config/hooks.yml when needed.
7
+ end