markymark 0.1.0

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +29 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +255 -0
  6. data/Rakefile +8 -0
  7. data/assets/.gitkeep +0 -0
  8. data/assets/Markymark.icns +0 -0
  9. data/assets/Markymark.iconset/icon_128x128.png +0 -0
  10. data/assets/Markymark.iconset/icon_128x128@2x.png +0 -0
  11. data/assets/Markymark.iconset/icon_16x16.png +0 -0
  12. data/assets/Markymark.iconset/icon_16x16@2x.png +0 -0
  13. data/assets/Markymark.iconset/icon_256x256.png +0 -0
  14. data/assets/Markymark.iconset/icon_256x256@2x.png +0 -0
  15. data/assets/Markymark.iconset/icon_32x32.png +0 -0
  16. data/assets/Markymark.iconset/icon_32x32@2x.png +0 -0
  17. data/assets/Markymark.iconset/icon_512x512.png +0 -0
  18. data/assets/Markymark.iconset/icon_512x512@2x.png +0 -0
  19. data/assets/README.md +3 -0
  20. data/assets/marky-mark-dj.jpg +0 -0
  21. data/assets/marky-mark-icon.png +0 -0
  22. data/assets/marky-mark-icon2.png +0 -0
  23. data/config.ru +19 -0
  24. data/docs/for_llms.md +141 -0
  25. data/docs/plans/2025-12-18-macos-app-installer-design.md +149 -0
  26. data/exe/markymark +5 -0
  27. data/lib/markymark/app_installer.rb +437 -0
  28. data/lib/markymark/cli.rb +497 -0
  29. data/lib/markymark/init_wizard.rb +186 -0
  30. data/lib/markymark/pumadev_manager.rb +194 -0
  31. data/lib/markymark/server_simple.rb +452 -0
  32. data/lib/markymark/version.rb +5 -0
  33. data/lib/markymark.rb +12 -0
  34. data/lib/public/css/style.css +350 -0
  35. data/lib/public/js/app.js +186 -0
  36. data/lib/public/js/theme.js +79 -0
  37. data/lib/public/js/tree.js +124 -0
  38. data/lib/views/browse.erb +225 -0
  39. data/lib/views/index.erb +37 -0
  40. data/lib/views/simple.erb +806 -0
  41. data/sig/markymark.rbs +4 -0
  42. metadata +242 -0
@@ -0,0 +1,437 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Markymark
7
+ # Installs Markymark as a macOS application for file handling
8
+ class AppInstaller
9
+ APP_NAME = 'Markymark.app'
10
+ BUNDLE_ID = 'com.markymark.app'
11
+ APP_DIR = File.expand_path('~/Applications')
12
+ APP_PATH = File.join(APP_DIR, APP_NAME)
13
+
14
+ class << self
15
+ def install(force: false)
16
+ unless macos?
17
+ warn "App installation is only supported on macOS"
18
+ return false
19
+ end
20
+
21
+ if File.exist?(APP_PATH) && !force
22
+ print "Markymark.app already exists. Overwrite? [y/N] "
23
+ response = $stdin.gets&.strip&.downcase
24
+ return false unless response == 'y'
25
+ end
26
+
27
+ puts "Installing Markymark.app to ~/Applications..."
28
+
29
+ begin
30
+ create_app_bundle
31
+ puts "Successfully installed Markymark.app"
32
+ puts "Location: #{APP_PATH}"
33
+ true
34
+ rescue => e
35
+ warn "Failed to install app: #{e.message}"
36
+ false
37
+ end
38
+ end
39
+
40
+ def uninstall
41
+ unless macos?
42
+ warn "App uninstallation is only supported on macOS"
43
+ return false
44
+ end
45
+
46
+ unless File.exist?(APP_PATH)
47
+ puts "Markymark.app is not installed"
48
+ return true
49
+ end
50
+
51
+ puts "Removing Markymark.app..."
52
+ FileUtils.rm_rf(APP_PATH)
53
+ puts "Successfully removed Markymark.app"
54
+ true
55
+ rescue => e
56
+ warn "Failed to uninstall app: #{e.message}"
57
+ false
58
+ end
59
+
60
+ def set_default_handler
61
+ unless macos?
62
+ warn "Setting default handler is only supported on macOS"
63
+ return false
64
+ end
65
+
66
+ unless File.exist?(APP_PATH)
67
+ warn "Markymark.app must be installed first. Run: markymark --install-app"
68
+ return false
69
+ end
70
+
71
+ puts "Setting Markymark as default handler for .md files..."
72
+
73
+ # Try duti first (more reliable)
74
+ if duti_available?
75
+ return set_default_with_duti
76
+ end
77
+
78
+ # duti not installed - offer to install it
79
+ if homebrew_available?
80
+ puts ""
81
+ puts "For automatic default app registration, 'duti' is recommended."
82
+ print "Install duti via Homebrew? [Y/n] "
83
+ response = $stdin.gets&.strip&.downcase
84
+
85
+ if response.nil? || response.empty? || response == 'y' || response == 'yes'
86
+ puts "Installing duti..."
87
+ if system('brew install duti')
88
+ puts "duti installed successfully."
89
+ return set_default_with_duti
90
+ else
91
+ warn "Failed to install duti."
92
+ end
93
+ end
94
+ end
95
+
96
+ # Fall back to lsregister + manual instructions
97
+ lsregister = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister'
98
+ if File.exist?(lsregister)
99
+ # Register the app
100
+ system("#{lsregister} -f '#{APP_PATH}'")
101
+ puts ""
102
+ puts "Registered Markymark.app with Launch Services."
103
+ puts ""
104
+ puts "To complete setup, please manually set Markymark as default:"
105
+ puts " 1. Right-click any .md file in Finder"
106
+ puts " 2. Select 'Get Info'"
107
+ puts " 3. Under 'Open with:', select 'Markymark'"
108
+ puts " 4. Click 'Change All...'"
109
+ return true
110
+ end
111
+
112
+ warn "Could not set default handler automatically."
113
+ puts "Please install 'duti' (brew install duti) or set manually via Finder."
114
+ false
115
+ end
116
+
117
+ def set_default_with_duti
118
+ success = system("duti -s #{BUNDLE_ID} .md all") &&
119
+ system("duti -s #{BUNDLE_ID} .markdown all")
120
+ if success
121
+ puts "Successfully set Markymark as default handler for .md files"
122
+ return true
123
+ end
124
+ false
125
+ end
126
+
127
+ def duti_available?
128
+ system('which duti > /dev/null 2>&1')
129
+ end
130
+
131
+ def homebrew_available?
132
+ system('which brew > /dev/null 2>&1')
133
+ end
134
+
135
+ def installed?
136
+ File.exist?(APP_PATH)
137
+ end
138
+
139
+ def status
140
+ return nil unless macos?
141
+
142
+ unless File.exist?(APP_PATH)
143
+ return { installed: false, message: "Markymark.app is not installed" }
144
+ end
145
+
146
+ launcher_path = File.join(APP_PATH, 'Contents', 'MacOS', 'markymark-launcher')
147
+ unless File.exist?(launcher_path)
148
+ return { installed: true, valid: false, message: "Markymark.app is corrupted (missing launcher)" }
149
+ end
150
+
151
+ # Check if the baked-in Ruby path still exists (stored in helper script)
152
+ helper_path = File.join(APP_PATH, 'Contents', 'MacOS', 'markymark-helper')
153
+ unless File.exist?(helper_path)
154
+ return { installed: true, valid: false, message: "Markymark.app is corrupted (missing helper)" }
155
+ end
156
+
157
+ helper_content = File.read(helper_path)
158
+ ruby_path = helper_content[/RUBY_PATH="([^"]+)"/, 1]
159
+
160
+ if ruby_path && !File.exist?(ruby_path)
161
+ return {
162
+ installed: true,
163
+ valid: false,
164
+ ruby_path: ruby_path,
165
+ message: "Markymark.app needs reinstalling (Ruby path changed: #{ruby_path})"
166
+ }
167
+ end
168
+
169
+ {
170
+ installed: true,
171
+ valid: true,
172
+ ruby_path: ruby_path,
173
+ message: "Markymark.app is installed and valid"
174
+ }
175
+ end
176
+
177
+ private
178
+
179
+ def macos?
180
+ RUBY_PLATFORM.include?('darwin')
181
+ end
182
+
183
+ def create_app_bundle
184
+ # Create directory structure
185
+ FileUtils.mkdir_p(APP_DIR)
186
+ FileUtils.rm_rf(APP_PATH) if File.exist?(APP_PATH)
187
+
188
+ contents_dir = File.join(APP_PATH, 'Contents')
189
+ macos_dir = File.join(contents_dir, 'MacOS')
190
+ resources_dir = File.join(contents_dir, 'Resources')
191
+
192
+ FileUtils.mkdir_p(macos_dir)
193
+ FileUtils.mkdir_p(resources_dir)
194
+
195
+ # Write Info.plist
196
+ write_info_plist(contents_dir)
197
+
198
+ # Write launcher script
199
+ write_launcher_script(macos_dir)
200
+
201
+ # Copy icon
202
+ copy_icon(resources_dir)
203
+ end
204
+
205
+ def write_info_plist(contents_dir)
206
+ plist_path = File.join(contents_dir, 'Info.plist')
207
+
208
+ plist_content = <<~PLIST
209
+ <?xml version="1.0" encoding="UTF-8"?>
210
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
211
+ <plist version="1.0">
212
+ <dict>
213
+ <key>CFBundleName</key>
214
+ <string>Markymark</string>
215
+ <key>CFBundleDisplayName</key>
216
+ <string>Markymark</string>
217
+ <key>CFBundleIdentifier</key>
218
+ <string>#{BUNDLE_ID}</string>
219
+ <key>CFBundleVersion</key>
220
+ <string>#{Markymark::VERSION}</string>
221
+ <key>CFBundleShortVersionString</key>
222
+ <string>#{Markymark::VERSION}</string>
223
+ <key>CFBundlePackageType</key>
224
+ <string>APPL</string>
225
+ <key>CFBundleExecutable</key>
226
+ <string>markymark-launcher</string>
227
+ <key>CFBundleIconFile</key>
228
+ <string>Markymark</string>
229
+ <key>LSMinimumSystemVersion</key>
230
+ <string>10.13</string>
231
+ <key>NSHighResolutionCapable</key>
232
+ <true/>
233
+ <key>CFBundleDocumentTypes</key>
234
+ <array>
235
+ <dict>
236
+ <key>CFBundleTypeName</key>
237
+ <string>Markdown Document</string>
238
+ <key>CFBundleTypeRole</key>
239
+ <string>Viewer</string>
240
+ <key>LSHandlerRank</key>
241
+ <string>Default</string>
242
+ <key>LSItemContentTypes</key>
243
+ <array>
244
+ <string>net.daringfireball.markdown</string>
245
+ <string>public.text</string>
246
+ </array>
247
+ <key>CFBundleTypeExtensions</key>
248
+ <array>
249
+ <string>md</string>
250
+ <string>markdown</string>
251
+ <string>mdown</string>
252
+ <string>mkd</string>
253
+ </array>
254
+ </dict>
255
+ </array>
256
+ <key>UTImportedTypeDeclarations</key>
257
+ <array>
258
+ <dict>
259
+ <key>UTTypeIdentifier</key>
260
+ <string>net.daringfireball.markdown</string>
261
+ <key>UTTypeDescription</key>
262
+ <string>Markdown Document</string>
263
+ <key>UTTypeConformsTo</key>
264
+ <array>
265
+ <string>public.plain-text</string>
266
+ </array>
267
+ <key>UTTypeTagSpecification</key>
268
+ <dict>
269
+ <key>public.filename-extension</key>
270
+ <array>
271
+ <string>md</string>
272
+ <string>markdown</string>
273
+ <string>mdown</string>
274
+ <string>mkd</string>
275
+ </array>
276
+ </dict>
277
+ </dict>
278
+ </array>
279
+ </dict>
280
+ </plist>
281
+ PLIST
282
+
283
+ File.write(plist_path, plist_content)
284
+ end
285
+
286
+ def write_launcher_script(macos_dir)
287
+ ruby_path = which_ruby
288
+ markymark_path = which_markymark
289
+
290
+ # Write the shell script helper
291
+ gem_home = ENV['GEM_HOME'] || Gem.dir
292
+ gem_path = ENV['GEM_PATH'] || Gem.path.join(':')
293
+ markymark_lib = File.dirname(File.dirname(markymark_path))
294
+
295
+ helper_path = File.join(macos_dir, 'markymark-helper')
296
+ helper_content = <<~SCRIPT
297
+ #!/bin/bash
298
+ # Markymark helper - called by the Swift launcher
299
+
300
+ RUBY_PATH="#{ruby_path}"
301
+ MARKYMARK_PATH="#{markymark_path}"
302
+ GEM_HOME="#{gem_home}"
303
+ GEM_PATH="#{gem_path}"
304
+ MARKYMARK_LIB="#{markymark_lib}/lib"
305
+ LOG_FILE="$HOME/.markymark/launcher.log"
306
+ mkdir -p "$(dirname "$LOG_FILE")"
307
+ echo "$(date): Helper called with: $@" >> "$LOG_FILE"
308
+
309
+ # Check if Ruby still exists
310
+ if [ ! -f "$RUBY_PATH" ]; then
311
+ osascript -e 'display alert "Markymark Error" message "Ruby installation has changed. Please run: markymark --install-app" as critical'
312
+ exit 1
313
+ fi
314
+
315
+ # Check if markymark still exists
316
+ if [ ! -f "$MARKYMARK_PATH" ]; then
317
+ osascript -e 'display alert "Markymark Error" message "Markymark gem not found. Please run: gem install markymark && markymark --install-app" as critical'
318
+ exit 1
319
+ fi
320
+
321
+ # Set up gem environment and launch markymark
322
+ export GEM_HOME="$GEM_HOME"
323
+ export GEM_PATH="$GEM_PATH"
324
+ export RUBYLIB="$MARKYMARK_LIB:$RUBYLIB"
325
+ "$RUBY_PATH" "$MARKYMARK_PATH" "$@" >> "$LOG_FILE" 2>&1 &
326
+ SCRIPT
327
+ File.write(helper_path, helper_content)
328
+ FileUtils.chmod(0o755, helper_path)
329
+
330
+ # Compile a Swift binary that properly receives files from Launch Services
331
+ # AppleScript droplets don't work with macOS `open` command (used by iTerm cmd-click)
332
+ write_swift_launcher(macos_dir, helper_path)
333
+ end
334
+
335
+ def write_swift_launcher(macos_dir, helper_path)
336
+ # Check if Swift compiler is available
337
+ unless system('which swiftc > /dev/null 2>&1')
338
+ raise "Swift compiler not found. Install Xcode Command Line Tools with: xcode-select --install"
339
+ end
340
+
341
+ swift_source = <<~SWIFT
342
+ import Cocoa
343
+
344
+ class AppDelegate: NSObject, NSApplicationDelegate {
345
+ let helperPath = "#{helper_path}"
346
+ var hasOpenedFile = false
347
+
348
+ func applicationDidFinishLaunching(_ notification: Notification) {
349
+ // Give time for openFile to be called first
350
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
351
+ if !self.hasOpenedFile {
352
+ // Launched without files - open home directory
353
+ self.openFile(path: NSHomeDirectory())
354
+ }
355
+ NSApp.terminate(nil)
356
+ }
357
+ }
358
+
359
+ func application(_ sender: NSApplication, openFile filename: String) -> Bool {
360
+ hasOpenedFile = true
361
+ openFile(path: filename)
362
+ return true
363
+ }
364
+
365
+ func openFile(path: String) {
366
+ let task = Process()
367
+ task.executableURL = URL(fileURLWithPath: helperPath)
368
+ task.arguments = [path]
369
+ try? task.run()
370
+ }
371
+ }
372
+
373
+ let app = NSApplication.shared
374
+ let delegate = AppDelegate()
375
+ app.delegate = delegate
376
+ app.run()
377
+ SWIFT
378
+
379
+ # Write Swift source to temp file
380
+ temp_swift = File.join(Dir.tmpdir, 'markymark_launcher.swift')
381
+ File.write(temp_swift, swift_source)
382
+
383
+ # Compile Swift binary
384
+ launcher_path = File.join(macos_dir, 'markymark-launcher')
385
+ success = system("swiftc -o '#{launcher_path}' '#{temp_swift}' -framework Cocoa 2>&1")
386
+
387
+ unless success
388
+ File.delete(temp_swift) if File.exist?(temp_swift)
389
+ raise "Failed to compile Swift launcher"
390
+ end
391
+
392
+ FileUtils.chmod(0o755, launcher_path)
393
+ File.delete(temp_swift) if File.exist?(temp_swift)
394
+ end
395
+
396
+ def copy_icon(resources_dir)
397
+ # Find the icon in the gem's assets
398
+ gem_root = File.expand_path('../../..', __FILE__)
399
+ icon_source = File.join(gem_root, 'assets', 'Markymark.icns')
400
+
401
+ unless File.exist?(icon_source)
402
+ warn "Warning: Icon file not found at #{icon_source}"
403
+ return
404
+ end
405
+
406
+ icon_dest = File.join(resources_dir, 'Markymark.icns')
407
+ FileUtils.cp(icon_source, icon_dest)
408
+ end
409
+
410
+ def which_ruby
411
+ # Get the full path to the current Ruby
412
+ RbConfig.ruby
413
+ end
414
+
415
+ def which_markymark
416
+ # Find the actual executable inside the gem, not the wrapper script
417
+ # RVM/rbenv wrappers don't work when launched from outside their environment
418
+ begin
419
+ spec = Gem::Specification.find_by_name('markymark')
420
+ exe_path = File.join(spec.gem_dir, 'exe', 'markymark')
421
+ return exe_path if File.exist?(exe_path)
422
+ rescue Gem::MissingSpecError
423
+ # Gem not found via spec
424
+ end
425
+
426
+ # Fallback: try to find via gem contents
427
+ contents = `gem contents markymark 2>/dev/null`.strip
428
+ contents.each_line do |line|
429
+ return line.strip if line.include?('/exe/markymark')
430
+ end
431
+
432
+ # Last resort fallback
433
+ '/usr/local/bin/markymark'
434
+ end
435
+ end
436
+ end
437
+ end