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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +9 -3
  3. data/README.md +39 -7
  4. data/docs/application.md +184 -0
  5. data/docs/getting_started.md +110 -0
  6. data/docs/iterators.md +294 -0
  7. data/docs/language_objects.md +190 -0
  8. data/docs/language_scripting.md +62 -0
  9. data/docs/language_syntax.md +334 -0
  10. data/docs/objects.md +77 -0
  11. data/docs/operators.md +62 -0
  12. data/docs/plugins.md +376 -0
  13. data/docs/verbs.md +64 -0
  14. data/docs/web_app.md +211 -0
  15. data/gloo.gemspec +3 -1
  16. data/lib/VERSION +1 -1
  17. data/lib/VERSION_NOTES +16 -0
  18. data/lib/gloo/app/engine.rb +17 -2
  19. data/lib/gloo/app/log.rb +5 -4
  20. data/lib/gloo/app/platform.rb +34 -2
  21. data/lib/gloo/app/prompt.rb +2 -1
  22. data/lib/gloo/app/settings.rb +25 -15
  23. data/lib/gloo/app/table.rb +7 -3
  24. data/lib/gloo/app/theme.rb +144 -0
  25. data/lib/gloo/core/gloo_system.rb +52 -1
  26. data/lib/gloo/core/invoker.rb +102 -0
  27. data/lib/gloo/core/parser.rb +48 -7
  28. data/lib/gloo/core/tokens.rb +81 -1
  29. data/lib/gloo/docs/doc_data.rb +160 -0
  30. data/lib/gloo/docs/help_shell.rb +332 -0
  31. data/lib/gloo/docs/markdown_renderer.rb +39 -0
  32. data/lib/gloo/expr/call.rb +54 -0
  33. data/lib/gloo/expr/expression.rb +3 -0
  34. data/lib/gloo/objs/basic/alias.rb +53 -0
  35. data/lib/gloo/objs/basic/boolean.rb +30 -0
  36. data/lib/gloo/objs/basic/container.rb +38 -0
  37. data/lib/gloo/objs/basic/decimal.rb +30 -0
  38. data/lib/gloo/objs/basic/integer.rb +58 -0
  39. data/lib/gloo/objs/basic/script.rb +31 -0
  40. data/lib/gloo/objs/basic/string.rb +31 -4
  41. data/lib/gloo/objs/basic/string_msgs.rb +212 -2
  42. data/lib/gloo/objs/basic/text.rb +30 -4
  43. data/lib/gloo/objs/basic/untyped.rb +21 -0
  44. data/lib/gloo/objs/ctrl/each.rb +63 -1
  45. data/lib/gloo/objs/ctrl/function.rb +86 -4
  46. data/lib/gloo/objs/ctrl/repeat.rb +41 -0
  47. data/lib/gloo/objs/dt/date.rb +43 -0
  48. data/lib/gloo/objs/dt/datetime.rb +50 -0
  49. data/lib/gloo/objs/dt/time.rb +43 -0
  50. data/lib/gloo/objs/str_utils/cipher.rb +59 -0
  51. data/lib/gloo/objs/str_utils/outline.rb +65 -1
  52. data/lib/gloo/objs/str_utils/password.rb +56 -0
  53. data/lib/gloo/objs/system/erb.rb +39 -0
  54. data/lib/gloo/objs/system/file_handle.rb +56 -0
  55. data/lib/gloo/objs/system/system.rb +29 -0
  56. data/lib/gloo/objs/web/http_get.rb +35 -0
  57. data/lib/gloo/objs/web/http_post.rb +33 -0
  58. data/lib/gloo/objs/web/json.rb +40 -0
  59. data/lib/gloo/objs/web/uri.rb +40 -0
  60. data/lib/gloo/shell/command_node.rb +39 -0
  61. data/lib/gloo/shell/context.rb +93 -0
  62. data/lib/gloo/shell/runner.rb +315 -0
  63. data/lib/gloo/verbs/break.rb +32 -0
  64. data/lib/gloo/verbs/check.rb +49 -0
  65. data/lib/gloo/verbs/cls.rb +18 -0
  66. data/lib/gloo/verbs/context.rb +47 -0
  67. data/lib/gloo/verbs/create.rb +42 -0
  68. data/lib/gloo/verbs/eval.rb +27 -0
  69. data/lib/gloo/verbs/execute.rb +27 -0
  70. data/lib/gloo/verbs/exists.rb +54 -2
  71. data/lib/gloo/verbs/files.rb +22 -0
  72. data/lib/gloo/verbs/help.rb +46 -178
  73. data/lib/gloo/verbs/if.rb +52 -0
  74. data/lib/gloo/verbs/invoke.rb +88 -33
  75. data/lib/gloo/verbs/list.rb +37 -4
  76. data/lib/gloo/verbs/load.rb +55 -2
  77. data/lib/gloo/verbs/log.rb +44 -0
  78. data/lib/gloo/verbs/move.rb +36 -1
  79. data/lib/gloo/verbs/put.rb +35 -0
  80. data/lib/gloo/verbs/quit.rb +19 -0
  81. data/lib/gloo/verbs/redirect.rb +52 -2
  82. data/lib/gloo/verbs/reload.rb +27 -0
  83. data/lib/gloo/verbs/run.rb +39 -0
  84. data/lib/gloo/verbs/save.rb +26 -0
  85. data/lib/gloo/verbs/show.rb +54 -0
  86. data/lib/gloo/verbs/tell.rb +32 -0
  87. data/lib/gloo/verbs/throw.rb +29 -0
  88. data/lib/gloo/verbs/unless.rb +50 -0
  89. data/lib/gloo/verbs/unload.rb +23 -0
  90. data/lib/gloo/verbs/version.rb +37 -3
  91. data/lib/gloo/verbs/wait.rb +26 -0
  92. data/test.gloo/objs/string.test.gloo +54 -0
  93. data/test.gloo/verbs/invoke.test.gloo +73 -0
  94. metadata +26 -10
  95. data/.DS_Store +0 -0
  96. data/.travis.yml +0 -5
data/gloo.gemspec CHANGED
@@ -11,9 +11,11 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{Gloo scripting language. A scripting language built on ruby.}
13
13
  spec.description = %q{A scripting languge to keep it all together.}
14
- spec.homepage = "http://github.com/ecrane/gloo"
14
+ spec.homepage = "https://github.com/ecrane/gloo"
15
15
  spec.license = 'MIT'
16
16
 
17
+ spec.metadata['documentation_uri'] = 'https://github.com/ecrane/gloo'
18
+
17
19
  # Specify which files should be added to the gem when it is released.
18
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
21
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
data/lib/VERSION CHANGED
@@ -1 +1 @@
1
- 6.0.1
1
+ 6.2.0
data/lib/VERSION_NOTES CHANGED
@@ -1,3 +1,19 @@
1
+ 6.2.0 - 2026.08.18
2
+ - Adds light | dark mode option for terminal output.
3
+ - Adds inline function invocation.
4
+ - Adds split messages for strings and text objects.
5
+
6
+
7
+ 6.1.0 - 2026.08.04
8
+ - Adds narrative documentation: 10 pages in `docs/`, covering getting started, application, language objects/syntax/scripting, operators, iterators, objects, verbs, and plugins
9
+ - Adds an in-app interactive help shell (`help`/`?`) to look up verbs, objects, settings, libraries, extensions, and doc pages, with tab completion
10
+ - Adds `doc_data` to every verb and object type, in the interpreter and its core library gems, rendered by the help shell
11
+ - Fixes gemspecs so docs and READMEs are actually packaged with the gem and its core libraries
12
+ - Adds a table of contents and reading order to the README's documentation list
13
+ - Fixes the rubygems.org "Documentation" link
14
+ - Adds page message to string and text
15
+
16
+
1
17
  6.0.1 - 2026.07.16
2
18
  - Update dependencies to newer versions for better security.
3
19
 
@@ -31,10 +31,14 @@ module Gloo
31
31
  @args = Args.new( self, context.params )
32
32
  @settings = Settings.new( self, context.user_root )
33
33
 
34
+ # Platform (and its theme) needs to be ready before Log is
35
+ # constructed - Log reads engine.theme, which reads through
36
+ # to platform.theme, in its own initialize.
37
+ @platform = context.platform
38
+ @platform.theme = Gloo::App::Theme.new( @settings.theme )
39
+
34
40
  @log = context.log.new( self, @args.quiet? )
35
41
  @log.debug "log (class: #{@log.class.name}) in use ..."
36
-
37
- @platform = context.platform
38
42
  @log.debug "platform (class: #{@platform.class.name}) in use ..."
39
43
 
40
44
  @handling_exception = false
@@ -42,6 +46,17 @@ module Gloo
42
46
  @log.debug 'engine intialized...'
43
47
  end
44
48
 
49
+ #
50
+ # Get the active theme. Platform is the single source of
51
+ # truth for it (Prompt/Table only hold a @platform reference,
52
+ # not an @engine one) - this just reads through to that,
53
+ # rather than keeping a second ivar that could drift out of
54
+ # sync if something reassigns platform.theme directly.
55
+ #
56
+ def theme
57
+ return @platform.theme
58
+ end
59
+
45
60
  #
46
61
  # Start the engine.
47
62
  # Load object and verb definitions and setup engine elements.
data/lib/gloo/app/log.rb CHANGED
@@ -38,6 +38,7 @@ module Gloo
38
38
  @engine = engine
39
39
  @quiet = quiet
40
40
  @debug = engine.settings.debug
41
+ @theme = engine.theme
41
42
 
42
43
  create_loggers
43
44
 
@@ -160,7 +161,7 @@ module Gloo
160
161
  def warn( msg )
161
162
  @logger.warn msg
162
163
  @error.warn msg
163
- puts msg.yellow unless @quiet
164
+ puts @theme.warn( msg ) unless @quiet
164
165
  end
165
166
 
166
167
  #
@@ -175,11 +176,11 @@ module Gloo
175
176
  if ex
176
177
  @error.error ex.message
177
178
  @error.error ex.backtrace
178
- puts msg.red unless @quiet
179
- puts ex.message.red unless @quiet
179
+ puts @theme.error( msg ) unless @quiet
180
+ puts @theme.error( ex.message ) unless @quiet
180
181
  puts ex.backtrace unless @quiet
181
182
  else
182
- puts msg.red unless @quiet
183
+ puts @theme.error( msg ) unless @quiet
183
184
  end
184
185
  end
185
186
 
@@ -13,8 +13,12 @@ module Gloo
13
13
 
14
14
  DEFAULT_TMP_FILE = 'tmp.txt'.freeze
15
15
  RETURN = "\n".freeze
16
+ DEFAULT_LINES = 24
17
+ DEFAULT_COLS = 80
18
+ PAGER_CMD = 'less -R -F -X'.freeze
16
19
 
17
20
  attr_reader :prompt, :table
21
+ attr_accessor :theme
18
22
 
19
23
  #
20
24
  # Set up Platform.
@@ -22,6 +26,10 @@ module Gloo
22
26
  def initialize
23
27
  @prompt = Gloo::App::Prompt.new( self )
24
28
  @table = Gloo::App::Table.new( self )
29
+
30
+ # Default theme, in case this platform is ever used
31
+ # standalone, without an Engine to set the real one.
32
+ @theme = Gloo::App::Theme.new
25
33
  end
26
34
 
27
35
  #
@@ -31,6 +39,22 @@ module Gloo
31
39
  puts msg
32
40
  end
33
41
 
42
+ #
43
+ # Show a message in a pager (less), for long content.
44
+ # Falls back to a plain puts when there is no real terminal
45
+ # to page in (piped output, captured test output, etc) or
46
+ # when less isn't available on the system.
47
+ #
48
+ def page( msg )
49
+ return show( msg ) unless $stdout.tty?
50
+
51
+ IO.popen( PAGER_CMD, 'w' ) { |less| less.puts msg }
52
+ rescue Errno::ENOENT
53
+ show( msg )
54
+ rescue Errno::EPIPE
55
+ # The user quit the pager early. Nothing more to do.
56
+ end
57
+
34
58
  #
35
59
  # Clear the screen.
36
60
  #
@@ -64,18 +88,26 @@ module Gloo
64
88
  # ---------------------------------------------------------------------
65
89
  #
66
90
  # Get the number of vertical lines on screen.
91
+ # Falls back to a default when stdout has no real screen behind it
92
+ # (piped/captured output, no controlling TTY, etc).
67
93
  #
68
94
  def lines
69
- rows, columns = $stdout.winsize
95
+ rows, _columns = $stdout.winsize
70
96
  return rows
97
+ rescue StandardError
98
+ return DEFAULT_LINES
71
99
  end
72
100
 
73
101
  #
74
102
  # Get the number of horizontal columns on screen.
103
+ # Falls back to a default when stdout has no real screen behind it
104
+ # (piped/captured output, no controlling TTY, etc).
75
105
  #
76
106
  def cols
77
- rows, columns = $stdout.winsize
107
+ _rows, columns = $stdout.winsize
78
108
  return columns
109
+ rescue StandardError
110
+ return DEFAULT_COLS
79
111
  end
80
112
 
81
113
  # ---------------------------------------------------------------------
@@ -87,7 +87,8 @@ module Gloo
87
87
  dt = DateTime.now
88
88
  d = dt.strftime( '%Y.%m.%d' )
89
89
  t = dt.strftime( '%I:%M:%S' )
90
- return "#{'gloo'.blue} #{d.yellow} #{t.white} >"
90
+ theme = @platform.theme
91
+ return "#{theme.heading( 'gloo' )} #{theme.accent( d )} #{theme.emphasis( t )} >"
91
92
  end
92
93
 
93
94
  end
@@ -10,10 +10,13 @@ module Gloo
10
10
  module App
11
11
  class Settings
12
12
 
13
+ DEFAULT_THEME = 'dark'.freeze
14
+ THEMES = %w[dark light].freeze
15
+
13
16
  attr_reader :user_root, :log_path,
14
17
  :config_path, :project_path, :ext_path,
15
18
  :start_with, :list_indent, :list_levels, :tmp_path,
16
- :debug_path, :debug
19
+ :debug_path, :debug, :theme
17
20
 
18
21
  #
19
22
  # Load setting from the yml file.
@@ -33,26 +36,29 @@ module Gloo
33
36
  # Can be seen in app with 'help settings'
34
37
  #
35
38
  def show
36
- puts "\n Application Settings:".blue
37
- puts ' Startup with: '.yellow + @start_with.white
38
- puts ' Indent in Listing: '.yellow + @list_indent.to_s.white
39
- puts ' List Levels: '.yellow + @list_levels.to_s.white
40
- puts ' Debug? '.yellow + @debug.to_s.white
41
- puts ' Screen Lines: '.yellow + Gloo::App::Settings.lines( @engine ).to_s.white
42
- puts ' Page Size: '.yellow + Gloo::App::Settings.page_size( @engine ).to_s.white
39
+ theme = @engine.theme
40
+ puts theme.heading( "\n Application Settings:" )
41
+ puts theme.accent( ' Startup with: ' ) + theme.emphasis( @start_with.to_s )
42
+ puts theme.accent( ' Indent in Listing: ' ) + theme.emphasis( @list_indent.to_s )
43
+ puts theme.accent( ' List Levels: ' ) + theme.emphasis( @list_levels.to_s )
44
+ puts theme.accent( ' Debug? ' ) + theme.emphasis( @debug.to_s )
45
+ puts theme.accent( ' Theme: ' ) + theme.emphasis( @theme )
46
+ puts theme.accent( ' Screen Lines: ' ) + theme.emphasis( Gloo::App::Settings.lines( @engine ).to_s )
47
+ puts theme.accent( ' Page Size: ' ) + theme.emphasis( Gloo::App::Settings.page_size( @engine ).to_s )
43
48
  self.show_paths
44
49
  end
45
-
50
+
46
51
  #
47
52
  # Show path settings
48
53
  #
49
54
  def show_paths
50
- puts "\n Application Paths:".blue
51
- puts ' User Root Path is here: '.yellow + @user_root.white
52
- puts ' Projects Path: '.yellow + @project_path.white
53
- puts ' Extensions Path: '.yellow + @ext_path.white
54
- puts ' Tmp Path: '.yellow + @tmp_path.white
55
- puts ' Debug Path: '.yellow + @debug_path.white
55
+ theme = @engine.theme
56
+ puts theme.heading( "\n Application Paths:" )
57
+ puts theme.accent( ' User Root Path is here: ' ) + theme.emphasis( @user_root )
58
+ puts theme.accent( ' Projects Path: ' ) + theme.emphasis( @project_path )
59
+ puts theme.accent( ' Extensions Path: ' ) + theme.emphasis( @ext_path )
60
+ puts theme.accent( ' Tmp Path: ' ) + theme.emphasis( @tmp_path )
61
+ puts theme.accent( ' Debug Path: ' ) + theme.emphasis( @debug_path )
56
62
  puts "\n"
57
63
  end
58
64
 
@@ -154,6 +160,9 @@ module Gloo
154
160
  @list_levels = settings[ 'gloo' ][ 'list_levels' ]
155
161
 
156
162
  @debug = settings[ 'gloo' ][ 'debug' ]
163
+
164
+ @theme = settings[ 'gloo' ][ 'theme' ]&.strip&.downcase
165
+ @theme = DEFAULT_THEME unless THEMES.include?( @theme )
157
166
  end
158
167
 
159
168
  #
@@ -184,6 +193,7 @@ module Gloo
184
193
  list_indent: 2
185
194
  list_levels: 3
186
195
  debug: false
196
+ theme: #{DEFAULT_THEME}
187
197
  TEXT
188
198
  return str
189
199
  end
@@ -3,7 +3,6 @@
3
3
  #
4
4
  # CLI input.
5
5
  #
6
- require 'colorize'
7
6
  require 'terminal-table'
8
7
 
9
8
  module Gloo
@@ -28,14 +27,19 @@ module Gloo
28
27
  #
29
28
  # Show the given table data.
30
29
  #
30
+ # Deliberately left uncolored: box-drawing borders and text read
31
+ # fine against the terminal's own default colors, and forcing a
32
+ # fixed foreground/background (as this used to do) fought
33
+ # whatever theme the terminal was actually running.
34
+ #
31
35
  def show( headers, data, title = nil )
32
36
  unless title.blank?
33
- table = Terminal::Table.new(
37
+ table = Terminal::Table.new(
34
38
  :title => title, :headings => headers, :rows => data )
35
39
  else
36
40
  table = Terminal::Table.new( :headings => headers, :rows => data )
37
41
  end
38
- puts table.to_s.colorize( color: :white, background: :black )
42
+ puts table.to_s
39
43
  end
40
44
 
41
45
 
@@ -0,0 +1,144 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Maps semantic color roles (heading, emphasis, etc) onto actual
5
+ # ANSI colors for the app's UI chrome (prompt, settings, log,
6
+ # tables, listings, help). Callers ask for a role, never a literal
7
+ # color, so a single palette swap (dark/light) fixes every call
8
+ # site at once.
9
+ #
10
+ # NOT used by the user-facing `show {target} (color)` verb or the
11
+ # `[colorize]` core-lib object - those are explicit color choices
12
+ # made in gloo scripts, not app chrome, and are intentionally left
13
+ # alone.
14
+ #
15
+ # Design note: most of the base 8 ANSI colors (red/green/yellow/
16
+ # blue/magenta/cyan) are exactly what a well-built terminal color
17
+ # scheme remaps per-background to stay legible - that's what
18
+ # switching a terminal profile from dark to light is for. `white`
19
+ # is the exception: by convention it stays "lightest foreground"
20
+ # regardless of background, so text forced to `.white` goes
21
+ # near-invisible on a light background no matter how good the
22
+ # terminal theme is. That's why :emphasis flips color between the
23
+ # two palettes below.
24
+ #
25
+ # `yellow` and `cyan` turned out to be further exceptions in
26
+ # practice: tested against a real light-background terminal, both
27
+ # were washed out and hard to read - the terminal's own theme
28
+ # wasn't remapping them as dark/saturated as hoped. So the light
29
+ # palette diverges from dark on more than just :emphasis, using
30
+ # explicit 256-color values (:ansi, raw SGR codes - none of these
31
+ # have a true equivalent in the base 16 ANSI colors) picked by eye
32
+ # against a real light terminal:
33
+ # - :warn uses orange instead of yellow
34
+ # - :accent uses a dark green instead of yellow
35
+ # - :subheading uses navy instead of cyan
36
+ #
37
+ require 'colorize'
38
+ require 'colorized_string'
39
+
40
+ module Gloo
41
+ module App
42
+ class Theme
43
+
44
+ # 256-color values picked by eye against a real light-background
45
+ # terminal - see design note above. 256-color support has been
46
+ # effectively universal in terminals for well over a decade.
47
+ ORANGE = '38;5;208'.freeze
48
+ NAVY = '38;5;18'.freeze
49
+ DARK_GREEN = '38;5;22'.freeze
50
+
51
+ #
52
+ # Dark-background palette. This matches the colors gloo
53
+ # has always used, so it's the default and non-breaking.
54
+ #
55
+ DARK = {
56
+ :heading => { :color => :blue, :mode => :bold },
57
+ :subheading => { :color => :cyan, :mode => :bold },
58
+ :accent => { :color => :yellow },
59
+ :emphasis => { :color => :white },
60
+ :muted => { :color => :light_black },
61
+ :warn => { :color => :yellow },
62
+ :error => { :color => :red }
63
+ }.freeze
64
+
65
+ #
66
+ # Light-background palette.
67
+ #
68
+ LIGHT = {
69
+ :heading => { :color => :blue, :mode => :bold },
70
+ :subheading => { :ansi => NAVY },
71
+ :accent => { :ansi => DARK_GREEN },
72
+ :emphasis => { :color => :black },
73
+ :muted => { :color => :light_black },
74
+ :warn => { :ansi => ORANGE },
75
+ :error => { :color => :red }
76
+ }.freeze
77
+
78
+ PALETTES = { 'dark' => DARK, 'light' => LIGHT }.freeze
79
+ ROLES = DARK.keys.freeze
80
+
81
+ attr_reader :name
82
+
83
+ #
84
+ # Set up the theme for the given theme name ('dark' or 'light').
85
+ # Falls back to the default (dark) for anything else.
86
+ #
87
+ def initialize( name = nil )
88
+ @name = PALETTES.key?( name ) ? name : Gloo::App::Settings::DEFAULT_THEME
89
+ @palette = PALETTES[ @name ]
90
+ end
91
+
92
+ #
93
+ # Build a Theme from the engine's settings.
94
+ #
95
+ def self.for_engine( engine )
96
+ return new( engine&.settings&.theme )
97
+ end
98
+
99
+ #
100
+ # Colorize the given string for the given semantic role.
101
+ # Unknown roles are returned unchanged rather than raising,
102
+ # so a typo'd role degrades to plain text instead of crashing.
103
+ #
104
+ # Named colors (:color/:mode) go through the colorize gem;
105
+ # :ansi is a raw SGR parameter string for colors the gem's
106
+ # named palette doesn't have (eg. 256-color orange).
107
+ #
108
+ def apply( str, role )
109
+ style = @palette[ role ]
110
+ return str.to_s unless style
111
+ return ansi_wrap( str.to_s, style ) if style[ :ansi ]
112
+
113
+ params = {}
114
+ params[ :color ] = style[ :color ] if style[ :color ]
115
+ params[ :mode ] = style[ :mode ] if style[ :mode ]
116
+ return ColorizedString[ str.to_s ].colorize( params ).to_s
117
+ end
118
+
119
+ #
120
+ # Define a convenience method per role, eg. theme.heading( str ).
121
+ #
122
+ ROLES.each do |role|
123
+ define_method( role ) do |str|
124
+ apply( str, role )
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ #
131
+ # Wrap a string in a raw SGR escape sequence - used for :ansi
132
+ # styles (eg. 256-color orange) that the colorize gem's named
133
+ # 16-color palette can't express.
134
+ #
135
+ def ansi_wrap( str, style )
136
+ codes = []
137
+ codes << String.mode_codes[ style[ :mode ] ] if style[ :mode ]
138
+ codes << style[ :ansi ]
139
+ return "\e[#{codes.join( ';' )}m#{str}\e[0m"
140
+ end
141
+
142
+ end
143
+ end
144
+ end
@@ -278,13 +278,64 @@ module Gloo
278
278
  def self.open_for_platform
279
279
  return 'open' if OS.mac?
280
280
  return 'xdg-open' if OS.posix?
281
-
281
+
282
282
  return 'Start-Process' if OS.windows?
283
283
  return 'explorer.exe' if self.wsl?
284
284
 
285
285
  return nil
286
286
  end
287
287
 
288
+ # ---------------------------------------------------------------------
289
+ # Object Documentation
290
+ # ---------------------------------------------------------------------
291
+
292
+ #
293
+ # Get the object's documentation data.
294
+ #
295
+ def self.doc_data
296
+ {
297
+ :name => KEYWORD,
298
+ :shortcut => KEYWORD_SHORT,
299
+ :description => 'The gloo system objects are virtual objects — ' \
300
+ "accessed like other objects (via 'gloo' or the shortcut " \
301
+ '$), but their values are set by the system and cannot be ' \
302
+ 'updated. They also do not show up in the object heap. Some ' \
303
+ 'names include an underscore to separate words; a period ' \
304
+ 'can be used instead, so gloo.working_dir and ' \
305
+ 'gloo.working.dir are identical.',
306
+ :children => [
307
+ 'app — Path of the running app (same as gloo_projects).',
308
+ 'hostname — Get the system hostname.',
309
+ 'user — Get the logged in user.',
310
+ 'line — A carriage return (line feed) character.',
311
+ "user_home — Get the user's home directory.",
312
+ 'working_dir — Get the working directory.',
313
+ 'gloo_home — Get the gloo home directory.',
314
+ 'gloo_config — Get the gloo configuration directory.',
315
+ 'gloo_projects — Get the gloo projects directory.',
316
+ 'gloo_log — Get the gloo logging directory.',
317
+ 'screen_lines — Get the number of lines on screen.',
318
+ 'screen_cols — Get the number of columns on screen.',
319
+ 'platform_cpu — Get the platform CPU.',
320
+ 'platform_os — Get the platform operating system.',
321
+ 'platform_version — Get the platform version.',
322
+ 'platform_windows? — Is the platform Windows?',
323
+ 'platform_unix? — Is the platform Unix?',
324
+ 'platform_linux? — Is the platform Linux?',
325
+ 'platform_mac? — Is the platform Mac?'
326
+ ],
327
+ :examples => <<~EXAMPLES.strip
328
+ > show gloo.user
329
+ > show $.user
330
+
331
+ > show gloo.working_dir
332
+ > show gloo.working.dir
333
+ > show $.working_dir
334
+ > show $.working.dir
335
+ EXAMPLES
336
+ }
337
+ end
338
+
288
339
  end
289
340
  end
290
341
  end
@@ -0,0 +1,102 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Resolves, validates and invokes a function. Shared by the
5
+ # `invoke`/`~>` verb (lib/gloo/verbs/invoke.rb) and inline calls
6
+ # inside any expression (lib/gloo/expr/call.rb), so both go through
7
+ # the same error handling instead of maintaining two copies of it.
8
+ #
9
+
10
+ module Gloo
11
+ module Core
12
+ class Invoker
13
+
14
+ NO_TARGET_ERR = 'Missing function reference!'.freeze
15
+ NOT_FOUND_ERR = 'Object was not found: '.freeze
16
+ NOT_FUNCTION_ERR = 'Not a function: '.freeze
17
+ PARAM_COUNT_ERR = 'Wrong number of parameters for function: '.freeze
18
+
19
+ #
20
+ # Resolve, validate and invoke the function at the given
21
+ # target path, with the given raw (unevaluated) arg tokens.
22
+ # Reports an error and returns nil for any failure - including
23
+ # a failure inside the function itself (see Function#invoke,
24
+ # which leaves `it` alone rather than setting it to an
25
+ # unreliable result when that happens).
26
+ #
27
+ def self.invoke( engine, target, arg_tokens )
28
+ if target.nil? || target.to_s.strip.empty?
29
+ engine.err NO_TARGET_ERR
30
+ return nil
31
+ end
32
+
33
+ func = resolve_function( engine, target )
34
+ return nil unless func
35
+
36
+ args = evaluate_arg_tokens( engine, arg_tokens )
37
+ return nil unless params_count_ok?( engine, func, args )
38
+
39
+ return invoke_function( engine, func, args )
40
+ end
41
+
42
+ #
43
+ # Resolve the function object at the target path. Reports an
44
+ # error and returns nil if the path doesn't resolve to
45
+ # anything, or resolves to an object that isn't a function.
46
+ #
47
+ def self.resolve_function( engine, target )
48
+ pn = Gloo::Core::Pn.new( engine, target )
49
+ func = pn.resolve
50
+
51
+ unless func
52
+ engine.err "#{NOT_FOUND_ERR}#{target}"
53
+ return nil
54
+ end
55
+
56
+ unless func.is_function?
57
+ engine.err "#{NOT_FUNCTION_ERR}#{target}"
58
+ return nil
59
+ end
60
+
61
+ return func
62
+ end
63
+
64
+ #
65
+ # Evaluate a list of raw tokens, each as its own single-token
66
+ # expression (a literal or object reference) - the standalone
67
+ # invoke verb's long-standing per-token semantics, now shared
68
+ # with the inline-call form too.
69
+ #
70
+ def self.evaluate_arg_tokens( engine, tokens )
71
+ return ( tokens || [] ).map do |t|
72
+ Gloo::Expr::Expression.new( engine, [ t ] ).evaluate
73
+ end
74
+ end
75
+
76
+ #
77
+ # Confirm the number of args given matches the number the
78
+ # function declares. Reports an error and returns false if
79
+ # they don't match.
80
+ #
81
+ def self.params_count_ok?( engine, func, args )
82
+ expected = func.params_hash&.keys&.length || 0
83
+ return true if args.count == expected
84
+
85
+ engine.err "#{PARAM_COUNT_ERR}#{func.pn} " \
86
+ "(expected #{expected}, got #{args.count})"
87
+ return false
88
+ end
89
+
90
+ #
91
+ # Invoke the function and return its result.
92
+ #
93
+ def self.invoke_function( engine, func, args )
94
+ engine.log.debug "invoking function: #{func.pn}"
95
+ result = func.invoke( args )
96
+ engine.log.debug "function returned: #{result}"
97
+ return result
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -39,17 +39,58 @@ module Gloo
39
39
  # If additional params were provided, split them out
40
40
  # from the token list.
41
41
  #
42
+ # A trailing (...) is normally the optional-param convention
43
+ # (eg. show "text" (color)) and gets split off here. But a
44
+ # trailing (...) can also be an inline function call that
45
+ # happens to be the last thing in the command (eg.
46
+ # show invoke( functions.add 3 4 )) - that must be left alone
47
+ # so it flows into tokenizing as part of the main command.
48
+ #
42
49
  def split_params( cmd )
43
- params = nil
44
- i = cmd.rindex( '(' )
45
- if i && cmd.strip.end_with?( ')' )
46
- pstr = cmd[ i + 1..-1 ]
47
- params = pstr.strip[ 0..-2 ] if pstr
48
- cmd = cmd[ 0, i].strip
49
- end
50
+ i = matching_open_paren_index( cmd )
51
+ return cmd, nil unless i
52
+ return cmd, nil if inline_call_opener?( cmd, i )
53
+
54
+ pstr = cmd[ i + 1..-1 ]
55
+ params = pstr.strip[ 0..-2 ] if pstr
56
+ cmd = cmd[ 0, i ].strip
50
57
  return cmd, params
51
58
  end
52
59
 
60
+ #
61
+ # Find the index of the '(' that balances the command's final
62
+ # ')', counting depth from the end so an inline call earlier
63
+ # in the command (or nested parens) doesn't confuse the match.
64
+ # Returns nil if the command doesn't end with ')', or the
65
+ # parens aren't balanced.
66
+ #
67
+ def matching_open_paren_index( cmd )
68
+ return nil unless cmd.strip.end_with?( ')' )
69
+
70
+ depth = 0
71
+ ( cmd.length - 1 ).downto( 0 ) do |idx|
72
+ case cmd[ idx ]
73
+ when ')' then depth += 1
74
+ when '('
75
+ depth -= 1
76
+ return idx if depth.zero?
77
+ end
78
+ end
79
+ return nil
80
+ end
81
+
82
+ #
83
+ # Is the '(' at the given index immediately preceded (no
84
+ # space) by an inline-call keyword, eg. invoke( or ~>( ?
85
+ # Shares its keyword list with Gloo::Core::Tokens, which is
86
+ # where such a call actually gets recognized as one token.
87
+ #
88
+ def inline_call_opener?( cmd, paren_index )
89
+ before = cmd[ 0...paren_index ]
90
+ word = before[ /\S*\z/ ]
91
+ return Gloo::Core::Tokens::CALL_OPENERS.include?( word )
92
+ end
93
+
53
94
  #
54
95
  # Parse a command and then run it if it parsed correctly.
55
96
  #