scriptorium 0.0.2 → 0.6.1

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 (290) hide show
  1. checksums.yaml +4 -4
  2. data/README.lt3 +324 -0
  3. data/README.md +3155 -1
  4. data/assets/.DS_Store +0 -0
  5. data/assets/README.md +44 -0
  6. data/assets/back-icon.png +0 -0
  7. data/assets/icons/facebook.svg +1 -0
  8. data/assets/icons/github.svg +1 -0
  9. data/assets/icons/instagram.svg +1 -0
  10. data/assets/icons/reddit.svg +1 -0
  11. data/assets/icons/ui/.DS_Store +0 -0
  12. data/assets/icons/ui/back.png +0 -0
  13. data/assets/icons/ui/copy.png +0 -0
  14. data/assets/icons/ui/down.png +0 -0
  15. data/assets/icons/ui/end.png +0 -0
  16. data/assets/icons/ui/exit.png +0 -0
  17. data/assets/icons/ui/foo +10 -0
  18. data/assets/icons/ui/home.png +0 -0
  19. data/assets/icons/ui/left.png +0 -0
  20. data/assets/icons/ui/next.png +0 -0
  21. data/assets/icons/ui/right.png +0 -0
  22. data/assets/icons/ui/start.png +0 -0
  23. data/assets/icons/ui/up.png +0 -0
  24. data/assets/icons/x.svg +1 -0
  25. data/assets/icons/youtube.svg +1 -0
  26. data/assets/samples/placeholder.svg +9 -0
  27. data/assets/themes/standard/favicon.svg +6 -0
  28. data/bin/scriptorium +1511 -0
  29. data/doc/README.txt +6 -0
  30. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
  31. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
  32. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
  33. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
  34. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
  35. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
  36. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
  37. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  38. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
  39. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
  40. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  41. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
  42. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
  43. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
  44. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  45. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
  46. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
  47. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
  48. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
  49. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
  50. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
  51. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
  52. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
  53. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
  54. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
  55. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
  56. data/doc/anti-amnesia/20250807-213025.md +116 -0
  57. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
  58. data/doc/banner_svg_config.md +114 -0
  59. data/doc/contrib.lt3 +8 -0
  60. data/doc/dependencies.md +281 -0
  61. data/doc/hacker.lt3 +5 -0
  62. data/doc/reddit_credentials_template.json +8 -0
  63. data/doc/reddit_integration.md +207 -0
  64. data/doc/user.lt3 +38 -0
  65. data/doc/user_guide_section_1.md +137 -0
  66. data/doc/user_guide_section_10.md +515 -0
  67. data/doc/user_guide_section_11.md +708 -0
  68. data/doc/user_guide_section_2.md +233 -0
  69. data/doc/user_guide_section_3.md +5 -0
  70. data/doc/user_guide_section_4.md +221 -0
  71. data/doc/user_guide_section_5.md +243 -0
  72. data/doc/user_guide_section_6.md +147 -0
  73. data/doc/user_guide_section_7.md +311 -0
  74. data/doc/user_guide_section_8.md +224 -0
  75. data/doc/user_guide_section_9.md +375 -0
  76. data/doc/userdoc-toc.txt +88 -0
  77. data/lib/rouge/lexers/livetext.rb +74 -0
  78. data/lib/scriptorium/api.rb +640 -0
  79. data/lib/scriptorium/banner_svg.rb +742 -0
  80. data/lib/scriptorium/contract.rb +33 -0
  81. data/lib/scriptorium/exceptions.rb +174 -0
  82. data/lib/scriptorium/helpers.rb +475 -0
  83. data/lib/scriptorium/post.rb +195 -0
  84. data/lib/scriptorium/reddit.rb +83 -0
  85. data/lib/scriptorium/repo.rb +624 -0
  86. data/lib/scriptorium/standard_files.rb +515 -0
  87. data/lib/scriptorium/syntax_highlighter.rb +234 -0
  88. data/lib/scriptorium/theme.rb +179 -0
  89. data/lib/scriptorium/version.rb +2 -2
  90. data/lib/scriptorium/view.rb +976 -0
  91. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  92. data/lib/scriptorium/widgets/links.rb +112 -0
  93. data/lib/scriptorium/widgets/pages.rb +133 -0
  94. data/lib/scriptorium/widgets/widget.rb +133 -0
  95. data/lib/scriptorium.rb +22 -9
  96. data/lib/skeleton.rb +11 -2
  97. data/scriptorium.gemspec +15 -4
  98. data/test/README.md +69 -0
  99. data/test/all +43 -0
  100. data/test/api_demo.rb +99 -0
  101. data/test/assets/imagenotfound.jpg +0 -0
  102. data/test/assets/images/.DS_Store +0 -0
  103. data/test/assets/images/README.md +27 -0
  104. data/test/assets/images/odd_aspect.png +0 -0
  105. data/test/assets/images/perfect.png +0 -0
  106. data/test/assets/images/small.png +0 -0
  107. data/test/assets/images/tall.png +0 -0
  108. data/test/assets/images/very_tall.png +0 -0
  109. data/test/assets/images/very_wide.png +0 -0
  110. data/test/assets/images/wide.png +0 -0
  111. data/test/assets/testbanner.jpg +0 -0
  112. data/test/banner_svg/simple_helpers.rb +13 -0
  113. data/test/banner_svg/unit.rb +768 -0
  114. data/test/ed_test.rb +204 -0
  115. data/test/integration/cursor_banner_combinations.rb +193 -0
  116. data/test/integration/cursor_banner_features.rb +374 -0
  117. data/test/integration/integration_test.rb +326 -0
  118. data/test/livetext_plugin_test.rb +229 -0
  119. data/test/manual/asset_mgmt.rb +67 -0
  120. data/test/manual/banner-tests/config.txt +3 -0
  121. data/test/manual/banner-tests/index.html +45 -0
  122. data/test/manual/banner-tests/test01.html +58 -0
  123. data/test/manual/banner-tests/test02.html +58 -0
  124. data/test/manual/banner-tests/test03.html +58 -0
  125. data/test/manual/banner-tests/test04.html +65 -0
  126. data/test/manual/banner-tests/test05.html +65 -0
  127. data/test/manual/banner-tests/test06.html +65 -0
  128. data/test/manual/banner-tests/test07.html +65 -0
  129. data/test/manual/banner-tests/test08.html +59 -0
  130. data/test/manual/banner-tests/test09.html +59 -0
  131. data/test/manual/banner-tests/test10.html +59 -0
  132. data/test/manual/banner-tests/test11.html +59 -0
  133. data/test/manual/banner-tests/test12.html +59 -0
  134. data/test/manual/banner-tests/test13.html +59 -0
  135. data/test/manual/banner-tests/test14.html +59 -0
  136. data/test/manual/banner-tests/test15.html +58 -0
  137. data/test/manual/banner-tests/test16.html +58 -0
  138. data/test/manual/banner-tests/test17.html +58 -0
  139. data/test/manual/banner-tests/test18.html +68 -0
  140. data/test/manual/banner-tests/test19.html +68 -0
  141. data/test/manual/banner-tests/test20.html +68 -0
  142. data/test/manual/banner-tests/test21.html +68 -0
  143. data/test/manual/banner-tests/test22.html +68 -0
  144. data/test/manual/banner-tests/test23.html +68 -0
  145. data/test/manual/banner-tests/test24.html +68 -0
  146. data/test/manual/banner-tests/test25.html +67 -0
  147. data/test/manual/banner_environment.rb +192 -0
  148. data/test/manual/deploy_symlink_demo.rb +142 -0
  149. data/test/manual/environment.rb +67 -0
  150. data/test/manual/make_banner.rb +153 -0
  151. data/test/manual/sample_banner_config.txt +12 -0
  152. data/test/manual/symlink_demo.rb +117 -0
  153. data/test/manual/test1.rb +47 -0
  154. data/test/manual/test2.rb +12 -0
  155. data/test/manual/test3.rb +38 -0
  156. data/test/manual/test4.rb +40 -0
  157. data/test/manual/test5.rb +24 -0
  158. data/test/manual/test6.rb +73 -0
  159. data/test/manual/test_banner_combinations.rb +120 -0
  160. data/test/manual/test_banner_features.rb +306 -0
  161. data/test/manual/test_banner_from_file.rb +150 -0
  162. data/test/manual/test_banner_in_header.rb +35 -0
  163. data/test/manual/test_code_highlighting.rb +68 -0
  164. data/test/manual/test_complex_header.rb +74 -0
  165. data/test/manual/test_empty_header.rb +32 -0
  166. data/test/manual/test_radial_custom.rb +58 -0
  167. data/test/manual/test_radial_large_radius.rb +52 -0
  168. data/test/manual/test_svg_debug.rb +47 -0
  169. data/test/manual/test_syntax_highlighting.rb +147 -0
  170. data/test/pages-demo/config/currentview.txt +1 -0
  171. data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
  172. data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
  173. data/test/pages-demo/views/demo/config/common.js +57 -0
  174. data/test/pages-demo/views/demo/config/footer.txt +1 -0
  175. data/test/pages-demo/views/demo/config/global-head.txt +8 -0
  176. data/test/pages-demo/views/demo/config/header.txt +1 -0
  177. data/test/pages-demo/views/demo/config/layout.txt +1 -0
  178. data/test/pages-demo/views/demo/config/left.txt +1 -0
  179. data/test/pages-demo/views/demo/config/main.txt +1 -0
  180. data/test/pages-demo/views/demo/config/right.txt +1 -0
  181. data/test/pages-demo/views/demo/config.txt +3 -0
  182. data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
  183. data/test/pages-demo/views/demo/output/panes/header.html +1 -0
  184. data/test/pages-demo/views/demo/output/panes/left.html +1 -0
  185. data/test/pages-demo/views/demo/output/panes/main.html +1 -0
  186. data/test/pages-demo/views/demo/output/panes/right.html +1 -0
  187. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  188. data/test/rubytext/rubytext_demo_test.rb +42 -0
  189. data/test/rubytext/rubytext_testing_guide.md +277 -0
  190. data/test/run_automated_tests.rb +45 -0
  191. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
  192. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
  193. data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
  194. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
  195. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
  196. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
  197. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
  198. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
  199. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
  200. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
  201. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
  202. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
  203. data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
  204. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
  205. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
  206. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
  207. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
  208. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
  209. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
  210. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
  211. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
  212. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
  213. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
  214. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
  215. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
  216. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
  217. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
  218. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
  219. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
  220. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
  221. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
  222. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
  223. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
  224. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
  225. data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
  226. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
  227. data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
  228. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
  229. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
  230. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
  231. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
  232. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
  233. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
  234. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
  235. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
  236. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
  237. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
  238. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
  239. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
  240. data/test/staging/.DS_Store +0 -0
  241. data/test/syntax_highlighting_test.lt3 +124 -0
  242. data/test/test_helpers.rb +230 -0
  243. data/test/tui_editor_integration_test.rb +296 -0
  244. data/test/tui_integration_test.rb +637 -0
  245. data/test/unit/api.rb +1056 -0
  246. data/test/unit/asset_management.rb +245 -0
  247. data/test/unit/clipboard_test.rb +60 -0
  248. data/test/unit/contract_test.rb +91 -0
  249. data/test/unit/core.rb +857 -0
  250. data/test/unit/deploy_test.rb +187 -0
  251. data/test/unit/gem_asset_management.rb +189 -0
  252. data/test/unit/livetext_basic.rb +69 -0
  253. data/test/unit/livetext_compatibility.rb +89 -0
  254. data/test/unit/post.rb +244 -0
  255. data/test/unit/read_commented_file_test.rb +276 -0
  256. data/test/unit/reddit_test.rb +235 -0
  257. data/test/unit/repo.rb +548 -0
  258. data/test/unit/social_test.rb +369 -0
  259. data/test/unit/symlink_test.rb +213 -0
  260. data/test/unit/view.rb +431 -0
  261. data/test/unit/widgets.rb +669 -0
  262. data/test/wizard_test.rb +123 -0
  263. data/ui/README.md +67 -0
  264. data/ui/common/lib/ui_common.rb +8 -0
  265. data/ui/rubytext/README.md +191 -0
  266. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  267. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  268. data/ui/tui/bin/scriptorium +1420 -0
  269. data/ui/tui/test/tui_test.rb +23 -0
  270. data/ui/web/app/app.rb +1378 -0
  271. data/ui/web/app/error_helpers.rb +150 -0
  272. data/ui/web/app/views/advanced_config.erb +190 -0
  273. data/ui/web/app/views/asset_management.erb +589 -0
  274. data/ui/web/app/views/banner_config.erb +200 -0
  275. data/ui/web/app/views/configure_view.erb +401 -0
  276. data/ui/web/app/views/dashboard.erb +162 -0
  277. data/ui/web/app/views/deploy_config.erb +146 -0
  278. data/ui/web/app/views/edit_pages.erb +195 -0
  279. data/ui/web/app/views/edit_post.erb +54 -0
  280. data/ui/web/app/views/error_page.erb +29 -0
  281. data/ui/web/app/views/header_config.erb +155 -0
  282. data/ui/web/app/views/layout_config.erb +147 -0
  283. data/ui/web/app/views/navbar_config.erb +411 -0
  284. data/ui/web/app/views/view_dashboard.erb +138 -0
  285. data/ui/web/bin/scriptorium-web +153 -0
  286. data/ui/web/test/web_basic_test.rb +38 -0
  287. data/ui/web/test_navbar.txt +7 -0
  288. data/ui/web/tmp/web_server.log +5 -0
  289. data/ui/web/tmp/web_server.pid +1 -0
  290. metadata +360 -5
@@ -0,0 +1,1420 @@
1
+ #!/Users/Hal/.rbenv/versions/3.2.3/bin/ruby
2
+
3
+ require_relative "../../../lib/scriptorium"
4
+ require 'readline' unless ENV['NOREADLINE']
5
+
6
+ # Main entry point for Scriptorium TUI
7
+ class ScriptoriumTUI
8
+ include Scriptorium::Exceptions
9
+ include Scriptorium::Helpers
10
+
11
+ def initialize
12
+ @api = Scriptorium::API.new(testmode: true)
13
+ @testing = true
14
+ setup_readline
15
+ end
16
+
17
+ def discover_repo
18
+ if @testing
19
+ if Dir.exist?("scriptorium-TEST")
20
+ puts "Found existing test repository: scriptorium-TEST"
21
+ @testing = "scriptorium-TEST"
22
+ @api = Scriptorium::API.new(testmode: true)
23
+ begin
24
+ @api.open_repo("scriptorium-TEST")
25
+ puts "Current view: #{@api.current_view&.name || 'nil'}"
26
+ puts "Loaded test repository"
27
+ return true
28
+ rescue => e
29
+ puts "Error opening repository: #{e.message}"
30
+ puts e.backtrace.first if @testing
31
+ return false
32
+ end
33
+ else
34
+ puts "No repository found."
35
+ return false
36
+ end
37
+ else
38
+ # Later: for production
39
+ end
40
+ return false
41
+ end
42
+
43
+ def create_new_repo
44
+ puts "Creating new repository..."
45
+ @testing = "scriptorium-TEST"
46
+ @api = Scriptorium::API.new(testmode: true)
47
+ begin
48
+ @api.create_repo("scriptorium-TEST")
49
+ puts "Created repository successfully."
50
+
51
+ # Run initial setup (like Runeblog)
52
+ get_started
53
+ rescue => e
54
+ puts "Error creating repository: #{e.message}"
55
+ puts e.backtrace.first if @testing
56
+ return false
57
+ end
58
+ end
59
+
60
+ def wizard_first_view
61
+ # Check if this is the first view (only sample view exists)
62
+ views = @api.views
63
+ if views.length == 1 && views[0].name == "sample"
64
+ puts "Let's set up your first view!"
65
+
66
+ # Create a new view using existing interactive method
67
+ create_view("view")
68
+
69
+ # Get the current view name (the one we just created)
70
+ current_view = @api.current_view
71
+ return unless current_view
72
+ name = current_view.name
73
+
74
+ # Ask about layout
75
+ puts
76
+ if yesno("Would you like to edit the layout?")
77
+ @api.edit_file("#{@api.root}/views/#{name}/config/layout.txt")
78
+ end
79
+
80
+ # Read the layout to see what containers we have
81
+ layout_file = "#{@api.root}/views/#{name}/config/layout.txt"
82
+ layout_content = read_file(layout_file)
83
+ file_containers = layout_content.lines.map { |line| line.split(/\s+/).first }.compact
84
+
85
+ # Define logical order for containers
86
+ logical_order = ['header', 'main', 'left', 'right', 'footer']
87
+
88
+ # Use logical order, but only include containers that exist in the file
89
+ containers = logical_order.select { |container| file_containers.include?(container) }
90
+
91
+ # Configure each container
92
+ containers.each do |container|
93
+ puts
94
+ if yesno("Would you like to configure #{container}?")
95
+ case container
96
+ when 'header'
97
+ # This is complex and will be expanded later
98
+ @api.edit_file("#{@api.root}/views/#{name}/config/header.txt")
99
+ when 'main'
100
+ puts "Main container is just a stub for now"
101
+ when 'left', 'right'
102
+ configure_sidebar_widgets(name, container)
103
+ when 'footer'
104
+ puts "Footer has no real config for now"
105
+ end
106
+ end
107
+ end
108
+
109
+ puts
110
+ puts "View setup complete!"
111
+ else
112
+ puts "Wizard is only available for the first view setup"
113
+ end
114
+ end
115
+
116
+ def configure_sidebar_widgets(view_name, container)
117
+ puts "Add widgets to #{container}? (y/n)"
118
+ return unless yesno("Add widgets to #{container}?")
119
+
120
+ # Show available widgets
121
+ available_widgets = @api.widgets_available
122
+ puts "Available widgets: #{available_widgets.join(', ')}"
123
+
124
+ selected_widgets = []
125
+ available_widgets.each do |widget|
126
+ if yesno("Add #{widget} widget?")
127
+ selected_widgets << widget
128
+ end
129
+ end
130
+
131
+ # Configure each selected widget
132
+ selected_widgets.each do |widget|
133
+ if yesno("Configure #{widget} widget?")
134
+ case widget
135
+ when 'links'
136
+ @api.edit_file("#{@api.root}/views/#{view_name}/widgets/links/list.txt")
137
+ when 'pages'
138
+ configure_pages_widget(view_name)
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ def configure_pages_widget(view_name)
145
+ list_file = "#{@api.root}/views/#{view_name}/widgets/pages/list.txt"
146
+ @api.edit_file(list_file)
147
+
148
+ # Check for missing pages
149
+ pages_list = read_file(list_file, lines: true, chomp: true)
150
+ missing_pages = []
151
+
152
+ pages_list.each do |page|
153
+ page_file = "#{@api.root}/views/#{view_name}/pages/#{page}.html"
154
+ unless File.exist?(page_file)
155
+ missing_pages << page
156
+ end
157
+ end
158
+
159
+ if missing_pages.any?
160
+ puts
161
+ puts "Found #{missing_pages.length} missing pages: #{missing_pages.join(', ')}"
162
+ if yesno("Do you want to edit the missing pages?")
163
+ missing_pages.each do |page|
164
+ if yesno("Edit #{page}?")
165
+ @api.edit_file("#{@api.root}/views/#{view_name}/pages/#{page}.html")
166
+ else
167
+ # Create empty .lt3 file
168
+ write_file("#{@api.root}/views/#{view_name}/pages/#{page}.lt3", "")
169
+ end
170
+ end
171
+ else
172
+ # Create empty .lt3 files for all missing pages
173
+ missing_pages.each do |page|
174
+ write_file("#{@api.root}/views/#{view_name}/pages/#{page}.lt3", "")
175
+ end
176
+ end
177
+ else
178
+ puts "[WIZARD] No missing pages found"
179
+ end
180
+ end
181
+
182
+ def yesno(question)
183
+ print "#{question} (y/n): "
184
+ response = get_string&.downcase
185
+ response == "y" || response == "yes"
186
+ end
187
+
188
+ def get_string
189
+ if STDIN.tty? && !ENV['NOREADLINE']
190
+ result = Readline.readline
191
+ result
192
+ else
193
+ result = gets&.chomp&.strip
194
+ result
195
+ end
196
+ end
197
+
198
+ def mainloop
199
+ loop do
200
+ begin
201
+ # Ensure we have a valid API with repository
202
+ if @api.nil? || @api.instance_variable_get(:@repo).nil?
203
+ puts "Error: No valid repository loaded. Exiting."
204
+ return
205
+ end
206
+
207
+ current_view = @api.current_view
208
+ current_view_name = current_view&.name || "no-view"
209
+ prompt = "[#{current_view_name}] "
210
+
211
+ # Use regular gets for automated tests, Readline for interactive
212
+ if STDIN.tty? && !ENV['NOREADLINE']
213
+ input = Readline.readline(prompt, true)
214
+ else
215
+ print prompt
216
+ input = gets&.chomp&.strip
217
+ end
218
+
219
+ break if input.nil? || input.downcase == "quit" || input.downcase == "q"
220
+
221
+ next if input.empty?
222
+
223
+ execute_command(input)
224
+ rescue Interrupt
225
+ puts "\nUse 'quit' to exit"
226
+ rescue => e
227
+ puts "Error: #{e.message}"
228
+ puts e.backtrace.first if @testing
229
+ puts "DEBUG: Exception caught in mainloop: #{e.class}: #{e.message}"
230
+ end
231
+ end
232
+
233
+ puts
234
+ puts " Goodbye!"
235
+ puts
236
+ end
237
+
238
+ private
239
+
240
+ def setup_readline
241
+ # Only set up Readline if we're not in automated testing mode
242
+ return if ENV['NOREADLINE']
243
+
244
+ # Set up tab completion
245
+ Readline.completion_proc = proc do |input|
246
+ completions = []
247
+
248
+ # Split input to get command and arguments
249
+ parts = input.split(/\s+/)
250
+ command = parts[0]&.downcase
251
+ args = parts[1..-1] || []
252
+
253
+ if args.empty?
254
+ # Complete command names
255
+ commands = %w[view change list new version help quit cv lsv v h q]
256
+ completions = commands.select { |cmd| cmd.start_with?(command || "") }
257
+ elsif command == "change" || command == "cv"
258
+ # Complete view names
259
+ if @api
260
+ view_names = @api.views.map(&:name)
261
+ completions = view_names.select { |name| name.start_with?(args.last || "") }
262
+ end
263
+ elsif command == "list" && args.length == 1 && args[0] == "views"
264
+ # Complete "list views" command
265
+ completions = []
266
+ elsif command == "new" && args.length == 1 && args[0] == "view"
267
+ # Suggest common view names for new view
268
+ suggestions = %w[blog personal work tech travel]
269
+ completions = suggestions
270
+ end
271
+
272
+ completions
273
+ end
274
+ end
275
+
276
+ def create_test_repo
277
+ puts "Creating test repository..."
278
+ @testing = true
279
+ @api = Scriptorium::API.new(testmode: true)
280
+ @api.create_repo("scriptorium-TEST")
281
+ puts "Test repository created successfully!"
282
+ end
283
+
284
+ private def execute_command(input)
285
+ parts = input.split(/\s+/, 2)
286
+ cmd = parts[0].downcase
287
+ args = parts[1] || ""
288
+
289
+ # Handle multi-word commands first
290
+ if cmd == "list" && args.start_with?("views")
291
+ list_views
292
+ elsif cmd == "list" && args.start_with?("posts")
293
+ list_posts
294
+ elsif cmd == "list" && args.start_with?("drafts")
295
+ list_drafts
296
+ elsif cmd == "change" && args.start_with?("view")
297
+ change_view(args)
298
+ elsif cmd == "new" && args.start_with?("view")
299
+ create_view(args)
300
+ elsif cmd == "new" && args.start_with?("draft")
301
+ create_draft(args)
302
+ elsif cmd == "new" && args.start_with?("post")
303
+ create_post(args)
304
+ elsif cmd == "list" && args.start_with?("themes")
305
+ list_themes
306
+ elsif cmd == "clone" && args.include?(" ")
307
+ clone_theme(args)
308
+
309
+ else
310
+ # Handle single-word commands
311
+ case cmd
312
+ when "help", "h"
313
+ show_help
314
+ when "view"
315
+ show_current_view
316
+ when "cv"
317
+ change_view(args)
318
+ when "lsv"
319
+ list_views
320
+ when "lsp"
321
+ list_posts
322
+ when "lsd"
323
+ list_drafts
324
+ when "cd"
325
+ create_draft("draft")
326
+ when "version", "v"
327
+ show_version
328
+ when "deploy"
329
+ deploy_current_view
330
+ when "preview"
331
+ preview_current_view
332
+ when "browse"
333
+ browse_deployed_view
334
+ when "list" && args.start_with?("widgets")
335
+ list_widgets
336
+ when "add" && args.start_with?("widget")
337
+ add_widget(args)
338
+ when "config" && args.start_with?("widget")
339
+ config_widget(args)
340
+ when "config" && args.start_with?("social")
341
+ config_social
342
+ when "config" && args.start_with?("reddit")
343
+ config_reddit
344
+ when "generate"
345
+ generate_current_view
346
+ when "quit", "q"
347
+ exit 0
348
+ else
349
+ puts
350
+ puts " Unknown command: #{cmd}. Type 'help' for available commands."
351
+ puts
352
+ end
353
+ end
354
+ end
355
+
356
+ private def show_help
357
+ puts
358
+ puts <<~HELP
359
+
360
+ view - Show current view
361
+ change view [<name>] - Switch to a view
362
+ cv [<name>]
363
+ list views - List all views
364
+ lsv
365
+ new view [<name> <title>] - Create a new view
366
+
367
+ list posts - List posts in current view
368
+ lsp
369
+ list drafts - List all drafts
370
+ lsd
371
+ new post [<title>] - Create draft, edit, and convert to post
372
+
373
+ deploy - Deploy current view to server
374
+ preview - Preview current view locally
375
+ browse - Browse deployed view on server
376
+
377
+ list widgets - List available and configured widgets
378
+ add widget <name> - Add widget to current view
379
+ config widget <name> - Configure widget data
380
+
381
+ config social - Configure social media sharing
382
+ config reddit - Configure Reddit sharing buttons
383
+ generate - Regenerate current view
384
+
385
+ list themes - List available themes
386
+ clone <old> <new> - Clone a theme
387
+
388
+ version, v - Show version
389
+ help, h - Show this help
390
+ quit, q, ^D - Exit
391
+ HELP
392
+ puts
393
+ end
394
+
395
+ private def show_current_view
396
+ current_view = @api.current_view
397
+ current_view_name = current_view&.name || "none"
398
+ puts
399
+ puts " Current view: #{current_view_name}"
400
+ puts
401
+ end
402
+
403
+ private def change_view(args)
404
+ # Handle "change view <name>" format
405
+ if args == "view" || args.start_with?("view ")
406
+ # Remove "view " prefix if present, otherwise args is just "view"
407
+ view_name = args == "view" ? "" : args[5..-1].strip
408
+ else
409
+ view_name = args.strip
410
+ end
411
+
412
+ if view_name.empty?
413
+ # Interactive mode - prompt for view name
414
+ puts
415
+ puts " Available views:"
416
+ views = @api.views
417
+ if views.empty?
418
+ puts " No views found"
419
+ puts
420
+ return
421
+ else
422
+ current_view = @api.current_view
423
+ current_view_name = current_view&.name
424
+
425
+ views.each do |view|
426
+ current = view.name == current_view_name ? "*" : " "
427
+ puts " #{current} #{view.name} - #{view.title}"
428
+ end
429
+ puts
430
+ end
431
+
432
+ print " Enter view name: "
433
+ view_name = gets&.chomp&.strip
434
+ return if view_name.nil? || view_name.empty?
435
+ end
436
+
437
+ view = @api.lookup_view(view_name)
438
+ @api.view(view_name)
439
+ puts
440
+ puts " Switched to view '#{view_name}'"
441
+ puts
442
+ rescue => e
443
+ puts
444
+ puts " View '#{view_name}' not found"
445
+ puts
446
+ end
447
+
448
+ private def create_view(args)
449
+ # Handle "new view" format - prompt for all parameters
450
+ if args == "view" || args.start_with?("view ")
451
+ # Remove "view " prefix if present, otherwise args is just "view"
452
+ view_args = args == "view" ? "" : args[5..-1]
453
+
454
+ if view_args.strip.empty?
455
+ # Interactive mode - prompt for all parameters
456
+ print " Enter view name: "
457
+ name = get_string
458
+ return if name.nil? || name.empty?
459
+
460
+ print " Enter view title: "
461
+ title = get_string
462
+ return if title.nil? || title.empty?
463
+
464
+ print " Enter subtitle (optional): "
465
+ subtitle = get_string
466
+ subtitle = nil if subtitle.empty?
467
+
468
+ # Check if view already exists
469
+ existing_views = @api.views
470
+ if existing_views.any? { |view| view.name == name }
471
+ puts
472
+ puts " View '#{name}' already exists"
473
+ puts
474
+ return
475
+ end
476
+
477
+ # Create view with all parameters
478
+ begin
479
+ @api.create_view(name, title, subtitle, theme: "standard")
480
+ puts
481
+ puts " Created view '#{name}' with title '#{title}'"
482
+ puts " Switched to view '#{name}'"
483
+ puts
484
+ rescue Exception => e
485
+ puts
486
+ puts " #{e.message}"
487
+ puts
488
+ puts "DEBUG: Exception caught in create_view (interactive): #{e.class}: #{e.message}"
489
+ end
490
+ else
491
+ # Legacy mode - still support "new view <name> <title>"
492
+ parts = view_args.split(/\s+/, 2)
493
+ if parts.length < 2
494
+ puts
495
+ puts " Usage: new view [<name> <title>]"
496
+ puts
497
+ return
498
+ end
499
+
500
+ name, title = parts
501
+
502
+ # Prompt for subtitle
503
+ print " Enter subtitle (optional): "
504
+ subtitle = get_string
505
+ subtitle = nil if subtitle.empty?
506
+
507
+ # Check if view already exists
508
+ existing_views = @api.views
509
+ if existing_views.any? { |view| view.name == name }
510
+ puts
511
+ puts " View '#{name}' already exists"
512
+ puts
513
+ return
514
+ end
515
+
516
+ # Create view with all parameters
517
+ begin
518
+ @api.create_view(name, title, subtitle, theme: "standard")
519
+ puts
520
+ puts " Created view '#{name}' with title '#{title}'"
521
+ puts " Switched to view '#{name}'"
522
+ puts
523
+ rescue Exception => e
524
+ puts
525
+ puts " #{e.message}"
526
+ puts
527
+ puts "DEBUG: Exception caught in create_view (legacy): #{e.class}: #{e.message}"
528
+ end
529
+ end
530
+ else
531
+ puts
532
+ puts " Usage: new view [<name> <title>]"
533
+ puts
534
+ end
535
+ end
536
+
537
+ private def create_draft(args)
538
+ # Handle "new draft" format - prompt for all parameters
539
+ if args == "draft" || args.start_with?("draft ")
540
+ # Remove "draft " prefix if present, otherwise args is just "draft"
541
+ draft_args = args == "draft" ? "" : args[6..-1]
542
+
543
+ if draft_args.strip.empty?
544
+ # Interactive mode - prompt for all parameters
545
+ print " Enter draft title: "
546
+ title = gets&.chomp&.strip
547
+ return if title.nil? || title.empty?
548
+
549
+ print " Enter draft body: "
550
+ body = gets&.chomp&.strip
551
+ return if body.nil? || body.empty?
552
+
553
+ print " Enter tags (optional, comma-separated): "
554
+ tags_input = gets&.chomp&.strip
555
+ tags = tags_input.empty? ? nil : tags_input.split(",").map(&:strip)
556
+
557
+ print " Enter blurb (optional): "
558
+ blurb = gets&.chomp&.strip
559
+ blurb = nil if blurb.empty?
560
+
561
+ # Create draft with all parameters
562
+ draft_path = @api.create_draft(
563
+ title: title,
564
+ body: body,
565
+ views: @api.current_view&.name,
566
+ tags: tags,
567
+ blurb: blurb
568
+ )
569
+ puts
570
+ puts " Created draft: #{draft_path}"
571
+ puts
572
+ else
573
+ # Legacy mode - still support "new draft <title>"
574
+ title = draft_args.strip
575
+
576
+ print " Enter draft body: "
577
+ body = gets&.chomp&.strip
578
+ return if body.nil? || body.empty?
579
+
580
+ print " Enter tags (optional, comma-separated): "
581
+ tags_input = gets&.chomp&.strip
582
+ tags = tags_input.empty? ? nil : tags_input.split(",").map(&:strip)
583
+
584
+ print " Enter blurb (optional): "
585
+ blurb = gets&.chomp&.strip
586
+ blurb = nil if blurb.empty?
587
+
588
+ # Create draft with all parameters
589
+ draft_path = @api.create_draft(
590
+ title: title,
591
+ body: body,
592
+ views: @api.current_view&.name,
593
+ tags: tags,
594
+ blurb: blurb
595
+ )
596
+ puts
597
+ puts " Created draft: #{draft_path}"
598
+ puts
599
+ end
600
+ else
601
+ puts
602
+ puts " Usage: new draft [<title>]"
603
+ puts
604
+ end
605
+ end
606
+
607
+ def show_version
608
+ puts
609
+ puts " Scriptorium #{Scriptorium::VERSION}"
610
+ puts
611
+ end
612
+
613
+ def get_started
614
+ puts
615
+ puts " No editor configured. Let's set one up."
616
+ pick_editor
617
+
618
+ puts
619
+ puts " Setup complete!"
620
+ puts " You can now use 'new post <title>' to create posts with your editor."
621
+ puts
622
+ end
623
+
624
+ def pick_editor
625
+ puts
626
+ puts " Available editors:"
627
+
628
+ # Check for common editors (prioritized for single file editing)
629
+ editors = []
630
+ %w[nano vim emacs vi micro].each do |editor|
631
+ if which(editor)
632
+ editors << editor
633
+ end
634
+ end
635
+
636
+ # The original Unix line editor - for the brave souls who want ultimate speed
637
+ if which("ed")
638
+ editors << "ed"
639
+ end
640
+
641
+
642
+ if editors.empty?
643
+ puts " No common editors found. Please install nano, vim, emacs, vi, micro, or ed."
644
+ puts " You can manually set your editor later by editing config/editor.txt"
645
+ puts
646
+ return
647
+ end
648
+
649
+ # Show available editors
650
+ editors.each_with_index do |editor, index|
651
+ puts " #{index + 1}. #{editor}"
652
+ end
653
+
654
+ # Let user pick
655
+ print " Choose editor (1-#{editors.length}): "
656
+ choice = get_string
657
+
658
+ if choice && choice.match?(/^\d+$/) && choice.to_i.between?(1, editors.length)
659
+ selected_editor = editors[choice.to_i - 1]
660
+
661
+ # Save the choice
662
+ make_dir(@api.root/"config")
663
+ write_file(@api.root/"config/editor.txt", selected_editor)
664
+
665
+ puts
666
+ puts " Selected editor: #{selected_editor}"
667
+ puts " Editor preference saved to config/editor.txt"
668
+ else
669
+ puts
670
+ puts " Invalid choice. Editor not changed."
671
+ end
672
+ end
673
+
674
+ def list_views
675
+ puts
676
+ views = @api.views
677
+ if views.empty?
678
+ puts " No views found"
679
+ else
680
+ current_view = @api.current_view
681
+ current_view_name = current_view&.name
682
+
683
+ views.each do |view|
684
+ current = view.name == current_view_name ? "*" : " "
685
+ puts " #{current} #{view.name} #{view.title}"
686
+ end
687
+ end
688
+ puts
689
+ end
690
+
691
+ def which(command)
692
+ # Mock which in test mode to avoid hanging
693
+ if @testing
694
+ case command
695
+ when 'nano', 'vim', 'vi', 'ed'
696
+ "/usr/bin/#{command}"
697
+ else
698
+ nil
699
+ end
700
+ else
701
+ # Use File.which if available (Ruby 3.2+)
702
+ if File.respond_to?(:which)
703
+ File.which(command)
704
+ else
705
+ # Fall back to system call
706
+ result = `which #{command} 2>/dev/null`.chomp
707
+ result.empty? ? nil : result
708
+ end
709
+ end
710
+ end
711
+
712
+ private def create_post(args)
713
+ # Handle "new post <title>" format
714
+ if args == "post" || args.start_with?("post ")
715
+ # Remove "post " prefix if present, otherwise args is just "post"
716
+ post_args = args == "post" ? "" : args[5..-1]
717
+
718
+ if post_args.strip.empty?
719
+ # Interactive mode - prompt for title
720
+ print " Enter post title: "
721
+ title = gets&.chomp&.strip
722
+ return if title.nil? || title.empty?
723
+ else
724
+ # Use provided title
725
+ title = post_args.strip
726
+ end
727
+
728
+ # Check if editor is configured
729
+ editor_file = @api.root/"config/editor.txt"
730
+ unless File.exist?(editor_file)
731
+ puts
732
+ puts " No editor configured. Please configure an editor in config/editor.txt"
733
+ puts
734
+ return
735
+ end
736
+
737
+ editor = read_file(editor_file).strip
738
+
739
+ # Create draft
740
+ begin
741
+ draft_path = @api.create_draft(
742
+ title: title,
743
+ body: "", # Empty body to start
744
+ views: @api.current_view&.name,
745
+ tags: nil,
746
+ blurb: nil
747
+ )
748
+
749
+ puts
750
+ puts " Created draft: #{File.basename(draft_path)}"
751
+ puts " Opening in #{editor}..."
752
+ puts
753
+
754
+ # Open in editor
755
+ system("#{editor} #{draft_path}")
756
+
757
+ puts
758
+ puts " Converting draft to post..."
759
+
760
+ # Convert draft to post (like Runeblog)
761
+ begin
762
+ post_num = @api.finish_draft(draft_path)
763
+ post = @api.post(post_num)
764
+ if post && post.title
765
+ puts " Post created: ##{post_num} - #{post.title}"
766
+ else
767
+ puts " Post created: ##{post_num}"
768
+ end
769
+ puts " Use 'deploy' to publish to server when ready."
770
+ rescue => e
771
+ puts " Error converting to post: #{e.message}"
772
+ end
773
+
774
+ puts
775
+
776
+ rescue => e
777
+ puts
778
+ puts " Error creating post: #{e.message}"
779
+ puts
780
+ end
781
+ else
782
+ puts
783
+ puts " Usage: new post [<title>]"
784
+ puts
785
+ end
786
+ end
787
+
788
+ private def list_posts
789
+ current_view = @api.current_view
790
+ if current_view.nil?
791
+ puts
792
+ puts " No current view selected"
793
+ puts
794
+ return
795
+ end
796
+
797
+ posts = @api.all_posts(current_view)
798
+
799
+ puts
800
+ if posts.empty?
801
+ puts " No posts found in view '#{current_view.name}'"
802
+ else
803
+ puts " Posts in view '#{current_view.name}':"
804
+ posts.each do |post|
805
+ puts " #{post.title}"
806
+ end
807
+ end
808
+ puts
809
+ end
810
+
811
+ private def list_drafts
812
+ drafts_dir = @api.root/:drafts
813
+ return unless Dir.exist?(drafts_dir)
814
+
815
+ draft_files = Dir.glob("#{drafts_dir}/*-draft.lt3")
816
+
817
+ puts
818
+ if draft_files.empty?
819
+ puts " No drafts found"
820
+ else
821
+ draft_files.each do |file|
822
+ filename = File.basename(file)
823
+ puts " #{filename}"
824
+ end
825
+ end
826
+ puts
827
+ end
828
+
829
+ private def deploy_current_view
830
+ current_view = @api.current_view
831
+ if current_view.nil?
832
+ puts
833
+ puts " No current view selected"
834
+ puts
835
+ return
836
+ end
837
+
838
+ # Check if deploy config exists
839
+ deploy_config_file = current_view.dir/:config/"deploy.txt"
840
+ unless File.exist?(deploy_config_file)
841
+ puts
842
+ puts " No deployment configuration found."
843
+ puts " Create #{deploy_config_file} with format:"
844
+ puts " user@server:path"
845
+ puts
846
+ return
847
+ end
848
+
849
+ # Read deployment configuration
850
+ deploy_config = read_file(deploy_config_file).strip
851
+ if deploy_config.empty?
852
+ puts
853
+ puts " Deployment configuration is empty."
854
+ puts
855
+ return
856
+ end
857
+
858
+ # Check if output directory exists
859
+ output_dir = current_view.dir/:output
860
+ unless Dir.exist?(output_dir)
861
+ puts
862
+ puts " Output directory does not exist: #{output_dir}"
863
+ puts " Generate content first with 'new post' or similar."
864
+ puts
865
+ return
866
+ end
867
+
868
+ # Create deployment marker file
869
+ marker_content = "Deployed: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
870
+ marker_file = output_dir/"last-deployed.txt"
871
+ write_file(marker_file, marker_content)
872
+
873
+ # Execute rsync command
874
+ puts
875
+ puts " Deploying view '#{current_view.name}' to #{deploy_config}..."
876
+
877
+ cmd = "rsync -r -z -l #{output_dir}/ #{deploy_config}/"
878
+ puts " Executing: #{cmd}"
879
+
880
+ result = system(cmd)
881
+
882
+ if result
883
+ puts " Deployment successful!"
884
+
885
+ # Extract domain and verify deployment
886
+ domain = extract_domain_from_deploy_config(deploy_config)
887
+ if domain
888
+ verify_deployment(domain)
889
+ end
890
+ else
891
+ puts " Deployment failed!"
892
+ end
893
+ puts
894
+ end
895
+
896
+ private def extract_domain_from_deploy_config(config)
897
+ # user@example.com:/path/ -> example.com
898
+ if config =~ /@([^:]+):/
899
+ $1
900
+ end
901
+ end
902
+
903
+ private def verify_deployment(domain)
904
+ url = "https://#{domain}/last-deployed.txt"
905
+ puts " Verifying deployment..."
906
+
907
+ require 'net/http'
908
+ begin
909
+ response = Net::HTTP.get_response(URI(url))
910
+ if response.code == "200"
911
+ puts " ✅ Deployment verified!"
912
+ else
913
+ puts " ⚠️ Deployment verification failed (HTTP #{response.code})"
914
+ end
915
+ rescue => e
916
+ puts " ⚠️ Deployment verification failed: #{e.message}"
917
+ end
918
+ end
919
+
920
+ private def preview_current_view
921
+ current_view = @api.current_view
922
+ if current_view.nil?
923
+ puts
924
+ puts " No current view selected"
925
+ puts
926
+ return
927
+ end
928
+
929
+ # Check if output directory exists
930
+ output_dir = current_view.dir/:output
931
+ unless Dir.exist?(output_dir)
932
+ puts
933
+ puts " Output directory does not exist: #{output_dir}"
934
+ puts " Generate content first with 'new post' or similar."
935
+ puts
936
+ return
937
+ end
938
+
939
+ # Find the main index file
940
+ index_file = output_dir/"index.html"
941
+ unless File.exist?(index_file)
942
+ puts
943
+ puts " No index.html found in output directory"
944
+ puts " Generate content first with 'new post' or similar."
945
+ puts
946
+ return
947
+ end
948
+
949
+ # Load OS-specific helper and open the file
950
+ load_os_helpers
951
+ puts
952
+ puts " Opening preview of view '#{current_view.name}'..."
953
+ open_file(index_file)
954
+ puts
955
+ end
956
+
957
+ private def browse_deployed_view
958
+ current_view = @api.current_view
959
+ if current_view.nil?
960
+ puts
961
+ puts " No current view selected"
962
+ puts
963
+ return
964
+ end
965
+
966
+ # Check if deploy config exists
967
+ deploy_config_file = current_view.dir/:config/"deploy.txt"
968
+ unless File.exist?(deploy_config_file)
969
+ puts
970
+ puts " No deployment configuration found."
971
+ puts " Create #{deploy_config_file} with format:"
972
+ puts " user@server:path"
973
+ puts
974
+ return
975
+ end
976
+
977
+ # Read deployment configuration and extract domain
978
+ deploy_config = read_file(deploy_config_file).strip
979
+ if deploy_config.empty?
980
+ puts
981
+ puts " Deployment configuration is empty."
982
+ puts
983
+ return
984
+ end
985
+
986
+ # Extract domain for browsing
987
+ domain = extract_domain_from_deploy_config(deploy_config)
988
+ unless domain
989
+ puts
990
+ puts " Could not extract domain from deployment configuration."
991
+ puts
992
+ return
993
+ end
994
+
995
+ # Load OS-specific helper and open the URL
996
+ load_os_helpers
997
+ url = "https://#{domain}/"
998
+ puts
999
+ puts " Opening deployed view at: #{url}"
1000
+ open_file(url)
1001
+ puts
1002
+ end
1003
+
1004
+ private def load_os_helpers
1005
+ # Load the OS-specific helper functions
1006
+ os_helpers_file = @api.root/:config/"os_helpers.rb"
1007
+ if File.exist?(os_helpers_file)
1008
+ load os_helpers_file
1009
+ else
1010
+ puts " Warning: OS helpers not found. Preview/browse may not work."
1011
+ end
1012
+ end
1013
+
1014
+ private def list_widgets
1015
+ current_view = @api.current_view
1016
+ if current_view.nil?
1017
+ puts
1018
+ puts " No current view selected"
1019
+ puts
1020
+ return
1021
+ end
1022
+
1023
+ # Get available widgets
1024
+ available_widgets = @api.widgets_available
1025
+ puts
1026
+ puts " Available widgets: #{available_widgets.join(', ')}"
1027
+
1028
+ # Check which widgets are configured
1029
+ configured_widgets = []
1030
+ available_widgets.each do |widget|
1031
+ widget_dir = current_view.dir/:widgets/widget
1032
+ if Dir.exist?(widget_dir)
1033
+ configured_widgets << widget
1034
+ end
1035
+ end
1036
+
1037
+ puts " Configured widgets: #{configured_widgets.empty? ? 'none' : configured_widgets.join(', ')}"
1038
+ puts
1039
+ end
1040
+
1041
+ private def add_widget(args)
1042
+ current_view = @api.current_view
1043
+ if current_view.nil?
1044
+ puts
1045
+ puts " No current view selected"
1046
+ puts
1047
+ return
1048
+ end
1049
+
1050
+ # Parse widget name from args
1051
+ widget_name = args.sub(/^widget\s+/, '').strip
1052
+ if widget_name.empty?
1053
+ puts
1054
+ puts " Usage: add widget <name>"
1055
+ puts " Example: add widget links"
1056
+ puts
1057
+ return
1058
+ end
1059
+
1060
+ # Check if widget is available
1061
+ available_widgets = @api.widgets_available
1062
+ unless available_widgets.include?(widget_name)
1063
+ puts
1064
+ puts " Widget '#{widget_name}' is not available."
1065
+ puts " Available widgets: #{available_widgets.join(', ')}"
1066
+ puts
1067
+ return
1068
+ end
1069
+
1070
+ # Check if widget is already configured
1071
+ widget_dir = current_view.dir/:widgets/widget_name
1072
+ if Dir.exist?(widget_dir)
1073
+ puts
1074
+ puts " Widget '#{widget_name}' is already configured."
1075
+ puts
1076
+ return
1077
+ end
1078
+
1079
+ # Determine container (left/right)
1080
+ container = determine_widget_container(current_view)
1081
+ unless container
1082
+ puts
1083
+ puts " Error: No left or right container found in layout."
1084
+ puts " Add a left or right container to your layout first."
1085
+ puts
1086
+ return
1087
+ end
1088
+
1089
+ # Create widget directory and list.txt
1090
+ make_dir(widget_dir)
1091
+ list_file = widget_dir/"list.txt"
1092
+ write_file(list_file, "# Add #{widget_name} items here\n")
1093
+
1094
+ puts
1095
+ puts " Added widget '#{widget_name}' to #{container} container."
1096
+ puts " Use 'config widget #{widget_name}' to configure it."
1097
+ puts
1098
+ end
1099
+
1100
+ private def config_widget(args)
1101
+ current_view = @api.current_view
1102
+ if current_view.nil?
1103
+ puts
1104
+ puts " No current view selected"
1105
+ puts
1106
+ return
1107
+ end
1108
+
1109
+ # Parse widget name from args
1110
+ widget_name = args.sub(/^widget\s+/, '').strip
1111
+ if widget_name.empty?
1112
+ puts
1113
+ puts " Usage: config widget <name>"
1114
+ puts " Example: config widget links"
1115
+ puts
1116
+ return
1117
+ end
1118
+
1119
+ # Check if widget is configured
1120
+ widget_dir = current_view.dir/:widgets/widget_name
1121
+ unless Dir.exist?(widget_dir)
1122
+ puts
1123
+ puts " Widget '#{widget_name}' is not configured."
1124
+ puts " Use 'add widget #{widget_name}' to add it first."
1125
+ puts
1126
+ return
1127
+ end
1128
+
1129
+ list_file = widget_dir/"list.txt"
1130
+ unless File.exist?(list_file)
1131
+ puts
1132
+ puts " Error: Widget list file not found: #{list_file}"
1133
+ puts
1134
+ return
1135
+ end
1136
+
1137
+ # Show widget-specific instructions
1138
+ show_widget_instructions(widget_name)
1139
+
1140
+ puts " Press Enter to edit the widget data file..."
1141
+ gets
1142
+
1143
+ @api.edit_file(list_file)
1144
+
1145
+ # Regenerate the widget after editing
1146
+ puts " Regenerating widget..."
1147
+ begin
1148
+ @api.generate_widget(widget_name)
1149
+ puts " ✅ Widget regenerated successfully!"
1150
+ rescue => e
1151
+ puts " ⚠️ Widget regeneration failed: #{e.message}"
1152
+ end
1153
+ puts
1154
+ end
1155
+
1156
+ private def config_social
1157
+ current_view = @api.current_view
1158
+ if current_view.nil?
1159
+ puts
1160
+ puts " No current view selected"
1161
+ puts
1162
+ return
1163
+ end
1164
+
1165
+ social_config_file = current_view.dir/:config/"social.txt"
1166
+ unless File.exist?(social_config_file)
1167
+ puts
1168
+ puts " Social configuration file not found: #{social_config_file}"
1169
+ puts
1170
+ return
1171
+ end
1172
+
1173
+ puts
1174
+ puts " Social Media Sharing Configuration"
1175
+ puts " ================================="
1176
+ puts
1177
+ puts " This feature adds social media meta tags to your posts for better sharing."
1178
+ puts " When enabled, posts will have proper Open Graph and Twitter Card meta tags."
1179
+ puts
1180
+ puts " Configuration:"
1181
+ puts " - List one platform per line to enable (facebook, twitter, linkedin, reddit)"
1182
+ puts " - If no platforms listed, social meta tags are disabled"
1183
+ puts " - For Reddit buttons, also configure reddit.txt file"
1184
+ puts
1185
+ puts " No Facebook App ID or Twitter username required for basic meta tags."
1186
+ puts " These are only needed if you want to add social sharing buttons later."
1187
+ puts
1188
+ puts " Press Enter to edit the configuration file..."
1189
+ gets
1190
+
1191
+ @api.edit_file(social_config_file)
1192
+
1193
+ puts
1194
+ puts " Social configuration updated."
1195
+ puts " Regenerate your view to apply changes:"
1196
+ puts " generate"
1197
+ puts
1198
+ end
1199
+
1200
+ private def config_reddit
1201
+ current_view = @api.current_view
1202
+ if current_view.nil?
1203
+ puts
1204
+ puts " No current view selected"
1205
+ puts
1206
+ return
1207
+ end
1208
+
1209
+ reddit_config_file = current_view.dir/:config/"reddit.txt"
1210
+ unless File.exist?(reddit_config_file)
1211
+ puts
1212
+ puts " Reddit configuration file not found: #{reddit_config_file}"
1213
+ puts " Creating new Reddit configuration file..."
1214
+ puts
1215
+ # Create the file with default content
1216
+ write_file(reddit_config_file, @api.repo.predef.reddit_config)
1217
+ end
1218
+
1219
+ puts
1220
+ puts " Reddit Sharing Button Configuration"
1221
+ puts " =================================="
1222
+ puts
1223
+ puts " This feature adds Reddit share buttons to your posts."
1224
+ puts " When enabled, readers can easily share your posts to Reddit."
1225
+ puts
1226
+ puts " Configuration options:"
1227
+ puts " - button: true/false - Enable or disable Reddit share button"
1228
+ puts " - subreddit: <name> - Specify a subreddit for direct posting (optional)"
1229
+ puts " - hover_text: <text> - Custom hover text (optional)"
1230
+ puts
1231
+ puts " Examples:"
1232
+ puts " button true"
1233
+ puts " subreddit RubyElixirEtc"
1234
+ puts " hover_text \"Share on RubyElixirEtc\""
1235
+ puts
1236
+ puts " Note: Reddit must also be enabled in social.txt for buttons to appear."
1237
+ puts
1238
+ puts " Press Enter to edit the configuration file..."
1239
+ gets
1240
+
1241
+ @api.edit_file(reddit_config_file)
1242
+
1243
+ puts
1244
+ puts " Reddit configuration updated."
1245
+ puts " Regenerate your view to apply changes:"
1246
+ puts " generate"
1247
+ puts
1248
+ end
1249
+
1250
+ private def generate_current_view
1251
+ current_view = @api.current_view
1252
+ if current_view.nil?
1253
+ puts
1254
+ puts " No current view selected"
1255
+ puts
1256
+ return
1257
+ end
1258
+
1259
+ puts
1260
+ puts " Regenerating view '#{current_view.name}'..."
1261
+ begin
1262
+ @api.generate_view(current_view.name)
1263
+ puts " ✅ View regenerated successfully!"
1264
+ rescue => e
1265
+ puts " ⚠️ View regeneration failed: #{e.message}"
1266
+ end
1267
+ puts
1268
+ end
1269
+
1270
+ private def determine_widget_container(view)
1271
+ # Check which containers exist in the layout
1272
+ layout_file = view.dir/:config/"layout.txt"
1273
+ return nil unless File.exist?(layout_file)
1274
+
1275
+ layout_content = read_file(layout_file)
1276
+ has_left = layout_content.include?('left')
1277
+ has_right = layout_content.include?('right')
1278
+
1279
+ if has_left && has_right
1280
+ # Both exist, prompt user
1281
+ puts
1282
+ puts " Both left and right containers found."
1283
+ puts " Which container should the widget go in?"
1284
+ puts " (l) left (r) right"
1285
+ print " Choice: "
1286
+ choice = gets&.chomp&.downcase
1287
+
1288
+ case choice
1289
+ when 'l', 'left'
1290
+ 'left'
1291
+ when 'r', 'right'
1292
+ 'right'
1293
+ else
1294
+ puts " Invalid choice. Widget not added."
1295
+ nil
1296
+ end
1297
+ elsif has_left
1298
+ 'left'
1299
+ elsif has_right
1300
+ 'right'
1301
+ else
1302
+ nil
1303
+ end
1304
+ end
1305
+
1306
+ private def show_widget_instructions(widget_name)
1307
+ case widget_name
1308
+ when 'links'
1309
+ puts
1310
+ puts " Links Widget Configuration:"
1311
+ puts " Format: <url> <title>"
1312
+ puts " Example:"
1313
+ puts " https://example.com My Website"
1314
+ puts " https://github.com GitHub"
1315
+ puts
1316
+ when 'pages'
1317
+ puts
1318
+ puts " Pages Widget Configuration:"
1319
+ puts " Format: <filename> <title>"
1320
+ puts " Example:"
1321
+ puts " about.html About Us"
1322
+ puts " contact.html Contact"
1323
+ puts
1324
+ when 'featuredposts'
1325
+ puts
1326
+ puts " Featured Posts Widget Configuration:"
1327
+ puts " Format: <post_id> <optional_title>"
1328
+ puts " Example:"
1329
+ puts " 0001 My First Post"
1330
+ puts " 0002"
1331
+ puts
1332
+ else
1333
+ puts
1334
+ puts " Widget Configuration:"
1335
+ puts " Edit the list.txt file to configure widget data."
1336
+ puts
1337
+ end
1338
+ end
1339
+
1340
+ private def list_themes
1341
+ puts
1342
+ themes = @api.themes_available
1343
+ if themes.empty?
1344
+ puts " No themes found"
1345
+ else
1346
+ puts " Available themes:"
1347
+ themes.each do |theme|
1348
+ puts " #{theme}"
1349
+ end
1350
+ end
1351
+ puts
1352
+ end
1353
+
1354
+ private def clone_theme(args)
1355
+ parts = args.split(/\s+/)
1356
+ if parts.length != 2
1357
+ puts
1358
+ puts " Usage: clone <oldtheme> <newtheme>"
1359
+ puts " Example: clone standard mytheme"
1360
+ puts
1361
+ return
1362
+ end
1363
+
1364
+ old_theme, new_theme = parts[0], parts[1]
1365
+
1366
+ begin
1367
+ # Check if old theme exists
1368
+ old_theme_path = @api.root/:themes/old_theme
1369
+ unless Dir.exist?(old_theme_path)
1370
+ puts
1371
+ puts " Theme '#{old_theme}' not found"
1372
+ puts
1373
+ return
1374
+ end
1375
+
1376
+ # Check if new theme already exists
1377
+ new_theme_path = @api.root/:themes/new_theme
1378
+ if Dir.exist?(new_theme_path)
1379
+ puts
1380
+ puts " Theme '#{new_theme}' already exists"
1381
+ puts
1382
+ return
1383
+ end
1384
+
1385
+ # Clone the theme
1386
+ require 'fileutils'
1387
+ FileUtils.cp_r(old_theme_path, new_theme_path)
1388
+
1389
+ puts
1390
+ puts " ✅ Theme '#{old_theme}' cloned to '#{new_theme}'"
1391
+ puts " Edit #{new_theme_path} to customize your theme"
1392
+ puts
1393
+ rescue => e
1394
+ puts
1395
+ puts " ❌ Failed to clone theme: #{e.message}"
1396
+ puts
1397
+ end
1398
+ end
1399
+
1400
+ end
1401
+
1402
+ ###### Main ######
1403
+
1404
+ s = ScriptoriumTUI.new
1405
+
1406
+ # Auto-discovery: check for existing repo
1407
+ got_repo = s.discover_repo
1408
+
1409
+ unless got_repo
1410
+ if s.yesno("Create new repository?")
1411
+ s.create_new_repo
1412
+ ques = "Do you want assistance in creating your first view?"
1413
+ if s.yesno(ques)
1414
+ s.wizard_first_view
1415
+ end
1416
+ end
1417
+ end
1418
+
1419
+ # Main REPL loop
1420
+ s.mainloop