gloo 6.0.1 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +9 -3
- data/README.md +39 -7
- data/docs/application.md +184 -0
- data/docs/getting_started.md +110 -0
- data/docs/iterators.md +294 -0
- data/docs/language_objects.md +190 -0
- data/docs/language_scripting.md +62 -0
- data/docs/language_syntax.md +334 -0
- data/docs/objects.md +77 -0
- data/docs/operators.md +62 -0
- data/docs/plugins.md +376 -0
- data/docs/verbs.md +64 -0
- data/docs/web_app.md +211 -0
- data/gloo.gemspec +3 -1
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +16 -0
- data/lib/gloo/app/engine.rb +17 -2
- data/lib/gloo/app/log.rb +5 -4
- data/lib/gloo/app/platform.rb +34 -2
- data/lib/gloo/app/prompt.rb +2 -1
- data/lib/gloo/app/settings.rb +25 -15
- data/lib/gloo/app/table.rb +7 -3
- data/lib/gloo/app/theme.rb +144 -0
- data/lib/gloo/core/gloo_system.rb +52 -1
- data/lib/gloo/core/invoker.rb +102 -0
- data/lib/gloo/core/parser.rb +48 -7
- data/lib/gloo/core/tokens.rb +81 -1
- data/lib/gloo/docs/doc_data.rb +160 -0
- data/lib/gloo/docs/help_shell.rb +332 -0
- data/lib/gloo/docs/markdown_renderer.rb +39 -0
- data/lib/gloo/expr/call.rb +54 -0
- data/lib/gloo/expr/expression.rb +3 -0
- data/lib/gloo/objs/basic/alias.rb +53 -0
- data/lib/gloo/objs/basic/boolean.rb +30 -0
- data/lib/gloo/objs/basic/container.rb +38 -0
- data/lib/gloo/objs/basic/decimal.rb +30 -0
- data/lib/gloo/objs/basic/integer.rb +58 -0
- data/lib/gloo/objs/basic/script.rb +31 -0
- data/lib/gloo/objs/basic/string.rb +31 -4
- data/lib/gloo/objs/basic/string_msgs.rb +212 -2
- data/lib/gloo/objs/basic/text.rb +30 -4
- data/lib/gloo/objs/basic/untyped.rb +21 -0
- data/lib/gloo/objs/ctrl/each.rb +63 -1
- data/lib/gloo/objs/ctrl/function.rb +86 -4
- data/lib/gloo/objs/ctrl/repeat.rb +41 -0
- data/lib/gloo/objs/dt/date.rb +43 -0
- data/lib/gloo/objs/dt/datetime.rb +50 -0
- data/lib/gloo/objs/dt/time.rb +43 -0
- data/lib/gloo/objs/str_utils/cipher.rb +59 -0
- data/lib/gloo/objs/str_utils/outline.rb +65 -1
- data/lib/gloo/objs/str_utils/password.rb +56 -0
- data/lib/gloo/objs/system/erb.rb +39 -0
- data/lib/gloo/objs/system/file_handle.rb +56 -0
- data/lib/gloo/objs/system/system.rb +29 -0
- data/lib/gloo/objs/web/http_get.rb +35 -0
- data/lib/gloo/objs/web/http_post.rb +33 -0
- data/lib/gloo/objs/web/json.rb +40 -0
- data/lib/gloo/objs/web/uri.rb +40 -0
- data/lib/gloo/shell/command_node.rb +39 -0
- data/lib/gloo/shell/context.rb +93 -0
- data/lib/gloo/shell/runner.rb +315 -0
- data/lib/gloo/verbs/break.rb +32 -0
- data/lib/gloo/verbs/check.rb +49 -0
- data/lib/gloo/verbs/cls.rb +18 -0
- data/lib/gloo/verbs/context.rb +47 -0
- data/lib/gloo/verbs/create.rb +42 -0
- data/lib/gloo/verbs/eval.rb +27 -0
- data/lib/gloo/verbs/execute.rb +27 -0
- data/lib/gloo/verbs/exists.rb +54 -2
- data/lib/gloo/verbs/files.rb +22 -0
- data/lib/gloo/verbs/help.rb +46 -178
- data/lib/gloo/verbs/if.rb +52 -0
- data/lib/gloo/verbs/invoke.rb +88 -33
- data/lib/gloo/verbs/list.rb +37 -4
- data/lib/gloo/verbs/load.rb +55 -2
- data/lib/gloo/verbs/log.rb +44 -0
- data/lib/gloo/verbs/move.rb +36 -1
- data/lib/gloo/verbs/put.rb +35 -0
- data/lib/gloo/verbs/quit.rb +19 -0
- data/lib/gloo/verbs/redirect.rb +52 -2
- data/lib/gloo/verbs/reload.rb +27 -0
- data/lib/gloo/verbs/run.rb +39 -0
- data/lib/gloo/verbs/save.rb +26 -0
- data/lib/gloo/verbs/show.rb +54 -0
- data/lib/gloo/verbs/tell.rb +32 -0
- data/lib/gloo/verbs/throw.rb +29 -0
- data/lib/gloo/verbs/unless.rb +50 -0
- data/lib/gloo/verbs/unload.rb +23 -0
- data/lib/gloo/verbs/version.rb +37 -3
- data/lib/gloo/verbs/wait.rb +26 -0
- data/test.gloo/objs/string.test.gloo +54 -0
- data/test.gloo/verbs/invoke.test.gloo +73 -0
- metadata +26 -10
- data/.DS_Store +0 -0
- data/.travis.yml +0 -5
data/lib/gloo/core/tokens.rb
CHANGED
|
@@ -9,6 +9,15 @@ module Gloo
|
|
|
9
9
|
module Core
|
|
10
10
|
class Tokens
|
|
11
11
|
|
|
12
|
+
# Keywords that open an inline function call, eg. invoke( ... )
|
|
13
|
+
# or ~>( ... ). Kept here since Tokens is where they get
|
|
14
|
+
# recognized as a single bounded token; Parser#split_params
|
|
15
|
+
# reuses this same list to avoid mistaking a trailing call for
|
|
16
|
+
# its unrelated trailing-optional-param convention.
|
|
17
|
+
CALL_OPENERS = %w[invoke ~>].freeze
|
|
18
|
+
|
|
19
|
+
QUOTE_CHARS = [ '"', "'" ].freeze
|
|
20
|
+
|
|
12
21
|
attr_reader :cmd, :tokens
|
|
13
22
|
|
|
14
23
|
# ---------------------------------------------------------------------
|
|
@@ -142,8 +151,19 @@ module Gloo
|
|
|
142
151
|
#
|
|
143
152
|
# Create a list of token from the given string.
|
|
144
153
|
#
|
|
154
|
+
# An inline call (invoke( ... ) / ~>( ... )) is checked for
|
|
155
|
+
# first since it needs to be quote-aware in its own right (a
|
|
156
|
+
# call's args can include a quoted string) - see
|
|
157
|
+
# find_call_range. Falls through to the original quote-then-
|
|
158
|
+
# plain-split handling, unchanged, when there's no call.
|
|
159
|
+
#
|
|
145
160
|
def tokenize( str )
|
|
146
|
-
|
|
161
|
+
range = find_call_range( str )
|
|
162
|
+
if range
|
|
163
|
+
tokenize( str[ 0...range.first ] ) if range.first.positive?
|
|
164
|
+
@tokens << str[ range ]
|
|
165
|
+
tokenize( str[ range.last + 1..-1 ] ) if range.last + 1 < str.length
|
|
166
|
+
elsif str.index( '"' )
|
|
147
167
|
i = str.index( '"' )
|
|
148
168
|
j = str.index( '"', i + 1 )
|
|
149
169
|
j ||= str.length
|
|
@@ -164,6 +184,66 @@ module Gloo
|
|
|
164
184
|
end
|
|
165
185
|
end
|
|
166
186
|
|
|
187
|
+
#
|
|
188
|
+
# Find the char range of the first top-level (not inside a
|
|
189
|
+
# quoted string) invoke(...)/~>(...) call in str. Returns nil
|
|
190
|
+
# if there isn't one, or it's never properly closed.
|
|
191
|
+
#
|
|
192
|
+
def find_call_range( str )
|
|
193
|
+
i = 0
|
|
194
|
+
while i < str.length
|
|
195
|
+
ch = str[ i ]
|
|
196
|
+
|
|
197
|
+
if QUOTE_CHARS.include?( ch )
|
|
198
|
+
close = str.index( ch, i + 1 )
|
|
199
|
+
return nil unless close
|
|
200
|
+
|
|
201
|
+
i = close + 1
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
CALL_OPENERS.each do |kw|
|
|
206
|
+
opener = "#{kw}("
|
|
207
|
+
next unless str[ i, opener.length ] == opener
|
|
208
|
+
next unless i.zero? || str[ i - 1 ] =~ /\s/
|
|
209
|
+
|
|
210
|
+
close = find_matching_close_paren( str, i + opener.length - 1 )
|
|
211
|
+
return ( i..close ) if close
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
i += 1
|
|
215
|
+
end
|
|
216
|
+
return nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
#
|
|
220
|
+
# Given the index of an open paren, find the index of its
|
|
221
|
+
# balancing close paren - skipping over quoted substrings so
|
|
222
|
+
# a stray paren character inside a quoted arg doesn't throw
|
|
223
|
+
# off the depth count. Returns nil if it's never closed.
|
|
224
|
+
#
|
|
225
|
+
def find_matching_close_paren( str, open_index )
|
|
226
|
+
depth = 0
|
|
227
|
+
i = open_index
|
|
228
|
+
while i < str.length
|
|
229
|
+
ch = str[ i ]
|
|
230
|
+
if QUOTE_CHARS.include?( ch )
|
|
231
|
+
close = str.index( ch, i + 1 )
|
|
232
|
+
return nil unless close
|
|
233
|
+
|
|
234
|
+
i = close + 1
|
|
235
|
+
next
|
|
236
|
+
elsif ch == '('
|
|
237
|
+
depth += 1
|
|
238
|
+
elsif ch == ')'
|
|
239
|
+
depth -= 1
|
|
240
|
+
return i if depth.zero?
|
|
241
|
+
end
|
|
242
|
+
i += 1
|
|
243
|
+
end
|
|
244
|
+
return nil
|
|
245
|
+
end
|
|
246
|
+
|
|
167
247
|
end
|
|
168
248
|
end
|
|
169
249
|
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Documentation data for a primitive: a verb or an object.
|
|
5
|
+
#
|
|
6
|
+
# Renders as plain markdown - no color/ANSI here. Color is applied
|
|
7
|
+
# separately, at display time, by Gloo::Docs::MarkdownRenderer, so
|
|
8
|
+
# this stays reusable wherever the raw markdown is useful (paging,
|
|
9
|
+
# export, tests).
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
module Gloo
|
|
13
|
+
module Docs
|
|
14
|
+
class DocData
|
|
15
|
+
|
|
16
|
+
DESCRIPTION = '## Description'.freeze
|
|
17
|
+
SYNTAX = '## Syntax'.freeze
|
|
18
|
+
PARAMETERS = '## Parameters'.freeze
|
|
19
|
+
CHILDREN = '## Children'.freeze
|
|
20
|
+
MESSAGES = '## Messages'.freeze
|
|
21
|
+
RESULT = '## Result'.freeze
|
|
22
|
+
ERRORS = '## Errors'.freeze
|
|
23
|
+
EXAMPLES = '## Examples'.freeze
|
|
24
|
+
NOTES = '## Notes'.freeze
|
|
25
|
+
FENCE = '```gloo'.freeze
|
|
26
|
+
|
|
27
|
+
attr_accessor :name, :shortcut, :description, :syntax,
|
|
28
|
+
:parameters, :children, :messages, :result, :errors,
|
|
29
|
+
:examples, :notes
|
|
30
|
+
|
|
31
|
+
def initialize( value_hash )
|
|
32
|
+
@name = value_hash[:name]
|
|
33
|
+
@shortcut = value_hash[:shortcut]
|
|
34
|
+
@description = value_hash[:description]
|
|
35
|
+
@syntax = value_hash[:syntax]
|
|
36
|
+
@parameters = value_hash[:parameters]
|
|
37
|
+
@children = value_hash[:children]
|
|
38
|
+
@messages = value_hash[:messages]
|
|
39
|
+
@result = value_hash[:result]
|
|
40
|
+
@errors = value_hash[:errors]
|
|
41
|
+
@examples = value_hash[:examples]
|
|
42
|
+
@notes = value_hash[:notes]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Render the documentation as a markdown string.
|
|
47
|
+
def render
|
|
48
|
+
buf = +''
|
|
49
|
+
add_name buf
|
|
50
|
+
add_description buf
|
|
51
|
+
add_syntax buf
|
|
52
|
+
add_parameters buf
|
|
53
|
+
add_children buf
|
|
54
|
+
add_messages buf
|
|
55
|
+
add_result buf
|
|
56
|
+
add_errors buf
|
|
57
|
+
add_examples buf
|
|
58
|
+
add_notes buf
|
|
59
|
+
buf << "\n"
|
|
60
|
+
return buf
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# Show the documentation in the terminal.
|
|
65
|
+
def show_in_terminal
|
|
66
|
+
puts render
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# Append the name and shortcut, if there is a name.
|
|
73
|
+
def add_name( buf )
|
|
74
|
+
return unless @name
|
|
75
|
+
|
|
76
|
+
buf << "\n# #{@name}\n"
|
|
77
|
+
buf << "#{@shortcut}\n" if @shortcut
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Append the description, if there is one.
|
|
82
|
+
def add_description( buf )
|
|
83
|
+
return unless @description
|
|
84
|
+
|
|
85
|
+
buf << "\n#{DESCRIPTION}\n#{@description}\n"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#
|
|
89
|
+
# Append the syntax lines, if there are any.
|
|
90
|
+
def add_syntax( buf )
|
|
91
|
+
return unless @syntax
|
|
92
|
+
|
|
93
|
+
buf << "\n#{SYNTAX}\n#{FENCE}\n"
|
|
94
|
+
@syntax.each { |line| buf << "#{line}\n" }
|
|
95
|
+
buf << "```\n"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# Append the parameter lines, if there are any.
|
|
100
|
+
def add_parameters( buf )
|
|
101
|
+
return unless @parameters
|
|
102
|
+
|
|
103
|
+
buf << "\n#{PARAMETERS}\n"
|
|
104
|
+
@parameters.each { |line| buf << "- #{line}\n" }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
#
|
|
108
|
+
# Append the child lines, if there are any.
|
|
109
|
+
def add_children( buf )
|
|
110
|
+
return unless @children
|
|
111
|
+
|
|
112
|
+
buf << "\n#{CHILDREN}\n"
|
|
113
|
+
@children.each { |line| buf << "- #{line}\n" }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# Append the message lines, if there are any.
|
|
118
|
+
def add_messages( buf )
|
|
119
|
+
return unless @messages
|
|
120
|
+
|
|
121
|
+
buf << "\n#{MESSAGES}\n"
|
|
122
|
+
@messages.each { |line| buf << "- #{line}\n" }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# Append the result, if there is one.
|
|
127
|
+
def add_result( buf )
|
|
128
|
+
return unless @result
|
|
129
|
+
|
|
130
|
+
buf << "\n#{RESULT}\n#{@result}\n"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
#
|
|
134
|
+
# Append the error lines, if there are any.
|
|
135
|
+
def add_errors( buf )
|
|
136
|
+
return unless @errors
|
|
137
|
+
|
|
138
|
+
buf << "\n#{ERRORS}\n"
|
|
139
|
+
@errors.each { |line| buf << "- #{line}\n" }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
#
|
|
143
|
+
# Append the example code, if there is any.
|
|
144
|
+
def add_examples( buf )
|
|
145
|
+
return unless @examples
|
|
146
|
+
|
|
147
|
+
buf << "\n#{EXAMPLES}\n#{FENCE}\n#{@examples}\n```\n"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
#
|
|
151
|
+
# Append the notes, if there are any.
|
|
152
|
+
def add_notes( buf )
|
|
153
|
+
return unless @notes
|
|
154
|
+
|
|
155
|
+
buf << "\n#{NOTES}\n#{@notes}\n"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# The interactive help shell, entered by the `help`/`?` verb.
|
|
5
|
+
# Built on Gloo::Shell::Runner (see lib/gloo/shell/).
|
|
6
|
+
#
|
|
7
|
+
require_relative '../shell/runner'
|
|
8
|
+
require_relative 'markdown_renderer'
|
|
9
|
+
|
|
10
|
+
module Gloo
|
|
11
|
+
module Docs
|
|
12
|
+
class HelpShell < Gloo::Shell::Runner
|
|
13
|
+
|
|
14
|
+
PROMPT = 'help>'.freeze
|
|
15
|
+
NO_DOC_YET = 'No documentation available yet for'.freeze
|
|
16
|
+
|
|
17
|
+
VERB_NAMES = :verb_names
|
|
18
|
+
OBJECT_NAMES = :object_names
|
|
19
|
+
DOC_NAMES = :doc_names
|
|
20
|
+
LIBRARY_NAMES = :library_names
|
|
21
|
+
EXTENSION_NAMES = :extension_names
|
|
22
|
+
|
|
23
|
+
DOCS_DIR = File.expand_path( '../../../docs', __dir__ ).freeze
|
|
24
|
+
README_GLOB = 'README*'.freeze
|
|
25
|
+
NO_README_YET = 'No README found for library'.freeze
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# Initialize the help shell for the given engine.
|
|
29
|
+
#
|
|
30
|
+
def initialize( engine )
|
|
31
|
+
super( engine, prompt: PROMPT, include_quit: true )
|
|
32
|
+
populate_context
|
|
33
|
+
build_commands
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------
|
|
37
|
+
# Root commands - lists (mirrors the old help v/o/s/e/l dispatch)
|
|
38
|
+
# ---------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# List all verbs.
|
|
42
|
+
#
|
|
43
|
+
def cmd_show_verbs( _obj, _context )
|
|
44
|
+
theme = @engine.theme
|
|
45
|
+
data = "\n"
|
|
46
|
+
data << theme.heading( " Verbs (shortcut, name)\n" )
|
|
47
|
+
@engine.dictionary.get_verbs.sort_by( &:keyword ).each do |v|
|
|
48
|
+
cut = theme.accent( v.keyword_shortcut.ljust( 5, ' ' ) )
|
|
49
|
+
name = theme.emphasis( v.keyword.ljust( 20, ' ' ) )
|
|
50
|
+
data << " #{cut} #{name} \n"
|
|
51
|
+
end
|
|
52
|
+
@engine.log.show "#{data}\n"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# List all object types.
|
|
57
|
+
#
|
|
58
|
+
def cmd_show_objects( _obj, _context )
|
|
59
|
+
theme = @engine.theme
|
|
60
|
+
data = "\n"
|
|
61
|
+
data << theme.heading( " Objects \n" )
|
|
62
|
+
@engine.dictionary.get_obj_types.sort_by( &:typename ).each do |o|
|
|
63
|
+
if o.short_typename != o.typename
|
|
64
|
+
short = theme.accent( "(#{o.short_typename})" )
|
|
65
|
+
name = "#{theme.emphasis( o.typename )} #{short}"
|
|
66
|
+
else
|
|
67
|
+
name = theme.emphasis( o.typename )
|
|
68
|
+
end
|
|
69
|
+
data << " #{name.ljust( 30, ' ' )}\n"
|
|
70
|
+
end
|
|
71
|
+
@engine.log.show "#{data}\n"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#
|
|
75
|
+
# Show application settings.
|
|
76
|
+
#
|
|
77
|
+
def cmd_show_settings( _obj, _context )
|
|
78
|
+
@engine.settings.show
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# Show the current theme, how to change it, and a color preview
|
|
83
|
+
# of both palettes - handy for checking how they actually render
|
|
84
|
+
# in whatever terminal you're sitting in.
|
|
85
|
+
#
|
|
86
|
+
def cmd_show_theme( _obj, _context )
|
|
87
|
+
theme = @engine.theme
|
|
88
|
+
settings = @engine.settings
|
|
89
|
+
config_file = File.join( settings.config_path, 'gloo.yml' )
|
|
90
|
+
|
|
91
|
+
data = "\n"
|
|
92
|
+
data << theme.heading( " Theme\n" )
|
|
93
|
+
data << " Current theme: #{theme.emphasis( settings.theme )}\n\n"
|
|
94
|
+
data << theme.muted( " Change it in #{config_file}:\n" )
|
|
95
|
+
data << theme.muted( " theme: dark (or: light)\n\n" )
|
|
96
|
+
data << theme_preview( ' Dark palette', Gloo::App::Theme.new( 'dark' ) )
|
|
97
|
+
data << "\n"
|
|
98
|
+
data << theme_preview( ' Light palette', Gloo::App::Theme.new( 'light' ) )
|
|
99
|
+
@engine.log.show "#{data}\n"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
#
|
|
103
|
+
# List loaded extensions.
|
|
104
|
+
#
|
|
105
|
+
def cmd_show_extensions( _obj, _context )
|
|
106
|
+
theme = @engine.theme
|
|
107
|
+
data = "\n"
|
|
108
|
+
data << theme.heading( " Extensions\n" )
|
|
109
|
+
data << theme.muted(
|
|
110
|
+
" Use `load ext {name}` to load a User Extension, \n" \
|
|
111
|
+
" then `object {name}` / `verb {name}` / `extension {name}` here to see what it adds. \n" \
|
|
112
|
+
" Only loaded extensions are listed below.\n\n" )
|
|
113
|
+
loaded = @engine.ext_manager.loaded_extensions
|
|
114
|
+
if loaded.empty?
|
|
115
|
+
data << theme.muted( " (none loaded)\n" )
|
|
116
|
+
else
|
|
117
|
+
loaded.sort.each do |name, _ext|
|
|
118
|
+
data << " #{theme.emphasis( name )} \n"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
@engine.log.show "#{data}\n"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#
|
|
125
|
+
# List loaded libraries.
|
|
126
|
+
#
|
|
127
|
+
def cmd_show_libraries( _obj, _context )
|
|
128
|
+
theme = @engine.theme
|
|
129
|
+
data = "\n"
|
|
130
|
+
data << theme.heading( " Libraries\n" )
|
|
131
|
+
data << theme.muted(
|
|
132
|
+
" Use `load lib {name}` to load a core library, \n" \
|
|
133
|
+
" then `object {name}` / `verb {name}` here to see what it adds. \n" \
|
|
134
|
+
" Only loaded libraries are listed below.\n\n" )
|
|
135
|
+
loaded = @engine.lib_manager.loaded_libraries
|
|
136
|
+
if loaded.empty?
|
|
137
|
+
data << theme.muted( " (none loaded)\n" )
|
|
138
|
+
else
|
|
139
|
+
loaded.sort.each do |name, _lib|
|
|
140
|
+
data << " #{theme.emphasis( name )} \n"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
@engine.log.show "#{data}\n"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#
|
|
147
|
+
# List all narrative doc pages (dev/gloo/docs/*.md).
|
|
148
|
+
#
|
|
149
|
+
def cmd_show_docs( _obj, _context )
|
|
150
|
+
theme = @engine.theme
|
|
151
|
+
data = "\n"
|
|
152
|
+
data << theme.heading( " Docs\n" )
|
|
153
|
+
doc_page_names.each do |name|
|
|
154
|
+
data << " #{theme.emphasis( name )}\n"
|
|
155
|
+
end
|
|
156
|
+
@engine.log.show "#{data}\n"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------
|
|
160
|
+
# Detail commands - verb {name}, object {name}, doc {name},
|
|
161
|
+
# library {name}
|
|
162
|
+
# ---------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
#
|
|
165
|
+
# Show detailed help for one verb.
|
|
166
|
+
#
|
|
167
|
+
def cmd_show_verb_detail( obj, _context )
|
|
168
|
+
verb_class = @engine.dictionary.find_verb( obj )
|
|
169
|
+
return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless verb_class.respond_to?( :doc_data )
|
|
170
|
+
|
|
171
|
+
page_markdown( Gloo::Docs::DocData.new( verb_class.doc_data ).render )
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
#
|
|
175
|
+
# Show detailed help for one object type.
|
|
176
|
+
#
|
|
177
|
+
def cmd_show_object_detail( obj, _context )
|
|
178
|
+
obj_class = @engine.dictionary.find_obj( obj )
|
|
179
|
+
return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless obj_class.respond_to?( :doc_data )
|
|
180
|
+
|
|
181
|
+
page_markdown( Gloo::Docs::DocData.new( obj_class.doc_data ).render )
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# Show one narrative doc page (dev/gloo/docs/{name}.md).
|
|
186
|
+
#
|
|
187
|
+
def cmd_show_doc_detail( obj, _context )
|
|
188
|
+
path = File.join( DOCS_DIR, "#{obj}.md" )
|
|
189
|
+
return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless File.exist?( path )
|
|
190
|
+
|
|
191
|
+
page_markdown( File.read( path ) )
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#
|
|
195
|
+
# Show the README for one loaded core library (from the root of
|
|
196
|
+
# its installed gem). Only loaded libraries are tab-completable
|
|
197
|
+
# here - see cmd_show_libraries.
|
|
198
|
+
#
|
|
199
|
+
def cmd_show_library_detail( obj, _context )
|
|
200
|
+
gem_name = @engine.lib_manager.loaded_libraries[ obj ]
|
|
201
|
+
return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless gem_name
|
|
202
|
+
|
|
203
|
+
readme_path = find_readme( gem_name )
|
|
204
|
+
return @engine.log.show "#{NO_README_YET} '#{obj}' (#{gem_name})." unless readme_path
|
|
205
|
+
|
|
206
|
+
page_markdown( File.read( readme_path ) )
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
#
|
|
210
|
+
# Show the README for one loaded user extension (from the root of
|
|
211
|
+
# its extension folder, e.g. ~/gloo/extensions/{name}). Only loaded
|
|
212
|
+
# extensions are tab-completable here - see cmd_show_extensions.
|
|
213
|
+
#
|
|
214
|
+
def cmd_show_extension_detail( obj, _context )
|
|
215
|
+
start_file = @engine.ext_manager.loaded_extensions[ obj ]
|
|
216
|
+
return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless start_file
|
|
217
|
+
|
|
218
|
+
readme_path = find_extension_readme( start_file )
|
|
219
|
+
return @engine.log.show "#{NO_README_YET} '#{obj}'." unless readme_path
|
|
220
|
+
|
|
221
|
+
page_markdown( File.read( readme_path ) )
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------
|
|
225
|
+
# Private
|
|
226
|
+
# ---------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
#
|
|
231
|
+
# Build a labeled preview block for one theme's palette: one line
|
|
232
|
+
# per role, colored in that role's own color, labeled with the
|
|
233
|
+
# role's name. Padding is applied to the plain label before it's
|
|
234
|
+
# colorized, so the ANSI codes don't throw off the alignment.
|
|
235
|
+
#
|
|
236
|
+
def theme_preview( title, theme )
|
|
237
|
+
lines = "#{theme.heading( title )}\n"
|
|
238
|
+
Gloo::App::Theme::ROLES.each do |role|
|
|
239
|
+
label = role.to_s.ljust( 12 )
|
|
240
|
+
lines << theme.send( role, " #{label}#{role}" )
|
|
241
|
+
lines << "\n"
|
|
242
|
+
end
|
|
243
|
+
return lines
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
#
|
|
247
|
+
# Colorize markdown and page it, bracketed with a '---' rule above
|
|
248
|
+
# and below so the content stands out from surrounding CLI output.
|
|
249
|
+
#
|
|
250
|
+
def page_markdown( md )
|
|
251
|
+
rule = '-' * @engine.platform.cols
|
|
252
|
+
bracketed = "#{rule}\n#{md.strip}\n#{rule}\n"
|
|
253
|
+
@engine.platform.page( Gloo::Docs::MarkdownRenderer.colorize( bracketed, @engine.theme ) )
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
#
|
|
257
|
+
# Snapshot the verb, object type, doc page, and loaded library
|
|
258
|
+
# names for tab-completion.
|
|
259
|
+
#
|
|
260
|
+
def populate_context
|
|
261
|
+
set_context( VERB_NAMES, @engine.dictionary.get_verbs.map( &:keyword ).sort )
|
|
262
|
+
set_context( OBJECT_NAMES, @engine.dictionary.get_obj_types.map( &:typename ).sort )
|
|
263
|
+
set_context( DOC_NAMES, doc_page_names )
|
|
264
|
+
set_context( LIBRARY_NAMES, @engine.lib_manager.loaded_libraries.keys.sort )
|
|
265
|
+
set_context( EXTENSION_NAMES, @engine.ext_manager.loaded_extensions.keys.sort )
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
#
|
|
269
|
+
# Find the README file at the root of an installed gem, if any.
|
|
270
|
+
#
|
|
271
|
+
def find_readme( gem_name )
|
|
272
|
+
spec = Gem::Specification.find_by_name( gem_name )
|
|
273
|
+
return Dir.glob( File.join( spec.gem_dir, README_GLOB ) ).first
|
|
274
|
+
rescue Gem::MissingSpecError
|
|
275
|
+
return nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
#
|
|
279
|
+
# Find the README file at the root of an extension's folder, given
|
|
280
|
+
# the full path to its {name}_ext.rb start file.
|
|
281
|
+
#
|
|
282
|
+
def find_extension_readme( start_file )
|
|
283
|
+
root = File.dirname( start_file )
|
|
284
|
+
return Dir.glob( File.join( root, README_GLOB ) ).first
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
#
|
|
288
|
+
# List the narrative doc page names (dev/gloo/docs/*.md, without extension).
|
|
289
|
+
#
|
|
290
|
+
def doc_page_names
|
|
291
|
+
return Dir.glob( File.join( DOCS_DIR, '*.md' ) ).map { |f| File.basename( f, '.md' ) }.sort
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
#
|
|
295
|
+
# Build the help shell's command tree.
|
|
296
|
+
#
|
|
297
|
+
def build_commands
|
|
298
|
+
add_command_node(
|
|
299
|
+
name: 'verbs', description: 'List all verbs', method: 'cmd_show_verbs' )
|
|
300
|
+
add_command_node(
|
|
301
|
+
name: 'objects', description: 'List all object types', method: 'cmd_show_objects' )
|
|
302
|
+
add_command_node(
|
|
303
|
+
name: 'settings', description: 'Show application settings', method: 'cmd_show_settings' )
|
|
304
|
+
add_command_node(
|
|
305
|
+
name: 'theme', description: 'Show the current theme and preview both palettes',
|
|
306
|
+
method: 'cmd_show_theme' )
|
|
307
|
+
add_command_node(
|
|
308
|
+
name: 'extensions', description: 'List loaded extensions', method: 'cmd_show_extensions' )
|
|
309
|
+
add_command_node(
|
|
310
|
+
name: 'libraries', description: 'List loaded libraries', method: 'cmd_show_libraries' )
|
|
311
|
+
add_command_node(
|
|
312
|
+
name: 'docs', description: 'List all narrative doc pages', method: 'cmd_show_docs' )
|
|
313
|
+
add_command_node(
|
|
314
|
+
name: 'verb', description: 'Show detailed help for a verb',
|
|
315
|
+
dynamic: true, source: VERB_NAMES, child_method: 'cmd_show_verb_detail' )
|
|
316
|
+
add_command_node(
|
|
317
|
+
name: 'object', description: 'Show detailed help for an object type',
|
|
318
|
+
dynamic: true, source: OBJECT_NAMES, child_method: 'cmd_show_object_detail' )
|
|
319
|
+
add_command_node(
|
|
320
|
+
name: 'doc', description: 'Show one narrative doc page',
|
|
321
|
+
dynamic: true, source: DOC_NAMES, child_method: 'cmd_show_doc_detail' )
|
|
322
|
+
add_command_node(
|
|
323
|
+
name: 'library', description: "Show a loaded library's README",
|
|
324
|
+
dynamic: true, source: LIBRARY_NAMES, child_method: 'cmd_show_library_detail' )
|
|
325
|
+
add_command_node(
|
|
326
|
+
name: 'extension', description: "Show a loaded extension's README",
|
|
327
|
+
dynamic: true, source: EXTENSION_NAMES, child_method: 'cmd_show_extension_detail' )
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Lightly colorize markdown for terminal display: headings and code
|
|
5
|
+
# fences get ANSI color, everything else passes through unchanged.
|
|
6
|
+
# Not a full markdown renderer - just enough that the raw
|
|
7
|
+
# `#`/```` ``` ```` syntax doesn't have to be read literally.
|
|
8
|
+
#
|
|
9
|
+
# Shared by the narrative doc pages (dev/gloo/docs/*.md) and the
|
|
10
|
+
# verb/object detail pages (Gloo::Docs::DocData#render), so both are
|
|
11
|
+
# paged through the same visual style.
|
|
12
|
+
#
|
|
13
|
+
module Gloo
|
|
14
|
+
module Docs
|
|
15
|
+
class MarkdownRenderer
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Colorize the given markdown text for terminal display.
|
|
19
|
+
def self.colorize( text, theme )
|
|
20
|
+
in_code = false
|
|
21
|
+
lines = text.split( "\n" ).map do |line|
|
|
22
|
+
if line.start_with?( '```' )
|
|
23
|
+
in_code = !in_code
|
|
24
|
+
next theme.muted( line )
|
|
25
|
+
end
|
|
26
|
+
next theme.muted( line ) if in_code
|
|
27
|
+
next theme.heading( line.sub( /^#\s*/, '' ) ) if line.start_with?( '# ' )
|
|
28
|
+
next theme.subheading( line.sub( /^##\s*/, '' ) ) if line.start_with?( '## ' )
|
|
29
|
+
next theme.subheading( line ) if line.start_with?( '### ' ) || line.start_with?( '#### ' )
|
|
30
|
+
next theme.muted( line ) if line =~ /\A-{3,}\z/
|
|
31
|
+
|
|
32
|
+
line
|
|
33
|
+
end
|
|
34
|
+
return lines.join( "\n" )
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# An inline function call inside an expression, eg.
|
|
5
|
+
# invoke( functions.add 3 4 ) or ~>( functions.add 3 4 ).
|
|
6
|
+
#
|
|
7
|
+
# Gloo::Core::Tokens recognizes the whole invoke(...)/~>(...) span
|
|
8
|
+
# as one token; Call takes that single token, splits it back out
|
|
9
|
+
# into a target path and raw arg tokens (reusing Tokens itself, so
|
|
10
|
+
# quoted args and nested calls tokenize the same way they would
|
|
11
|
+
# anywhere else), and hands both to Gloo::Core::Invoker - the same
|
|
12
|
+
# resolve/validate/invoke path the standalone invoke verb uses.
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
module Gloo
|
|
16
|
+
module Expr
|
|
17
|
+
class Call
|
|
18
|
+
|
|
19
|
+
attr_reader :target, :arg_tokens
|
|
20
|
+
|
|
21
|
+
#
|
|
22
|
+
# Does the given token look like an inline call?
|
|
23
|
+
#
|
|
24
|
+
def self.call?( token )
|
|
25
|
+
return false unless token.is_a?( String )
|
|
26
|
+
|
|
27
|
+
return Gloo::Core::Tokens::CALL_OPENERS.any? do |kw|
|
|
28
|
+
token.start_with?( "#{kw}(" ) && token.end_with?( ')' )
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Parse the call token into a target path and raw arg tokens.
|
|
34
|
+
#
|
|
35
|
+
def initialize( engine, token )
|
|
36
|
+
@engine = engine
|
|
37
|
+
|
|
38
|
+
inner = token[ token.index( '(' ) + 1..-2 ].to_s
|
|
39
|
+
tokens = Gloo::Core::Tokens.new( inner )
|
|
40
|
+
@target = tokens.first
|
|
41
|
+
@arg_tokens = tokens.params || []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# Resolve, validate and invoke - returns the result value, or
|
|
46
|
+
# nil if it failed (an error will already have been reported).
|
|
47
|
+
#
|
|
48
|
+
def value
|
|
49
|
+
return Gloo::Core::Invoker.invoke( @engine, @target, @arg_tokens )
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|