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