gloo 6.1.0 → 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 +1 -1
- data/README.md +1 -0
- data/docs/application.md +24 -4
- data/docs/getting_started.md +2 -4
- data/docs/language_objects.md +1 -1
- data/docs/language_syntax.md +29 -2
- data/docs/plugins.md +325 -3
- data/docs/web_app.md +211 -0
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +6 -0
- data/lib/gloo/app/engine.rb +17 -2
- data/lib/gloo/app/log.rb +5 -4
- data/lib/gloo/app/platform.rb +5 -0
- 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/invoker.rb +102 -0
- data/lib/gloo/core/parser.rb +48 -7
- data/lib/gloo/core/tokens.rb +81 -1
- data/lib/gloo/docs/help_shell.rb +67 -20
- data/lib/gloo/docs/markdown_renderer.rb +7 -7
- data/lib/gloo/expr/call.rb +54 -0
- data/lib/gloo/expr/expression.rb +3 -0
- data/lib/gloo/objs/basic/string.rb +5 -25
- data/lib/gloo/objs/basic/string_msgs.rb +192 -2
- data/lib/gloo/objs/basic/text.rb +5 -26
- data/lib/gloo/objs/ctrl/function.rb +17 -4
- data/lib/gloo/verbs/help.rb +8 -5
- data/lib/gloo/verbs/invoke.rb +39 -46
- data/lib/gloo/verbs/list.rb +5 -4
- data/test.gloo/objs/string.test.gloo +54 -0
- data/test.gloo/verbs/invoke.test.gloo +73 -0
- metadata +5 -3
- data/.DS_Store +0 -0
- data/.travis.yml +0 -5
data/docs/web_app.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# A Trivial Web App
|
|
2
|
+
|
|
3
|
+
**Contents**
|
|
4
|
+
|
|
5
|
+
- Overview
|
|
6
|
+
- File and Folder Layout
|
|
7
|
+
- start.gloo
|
|
8
|
+
- app/app.gloo
|
|
9
|
+
- layout/primary.gloo
|
|
10
|
+
- shared/nav.gloo
|
|
11
|
+
- page/home.gloo
|
|
12
|
+
- page/about.gloo
|
|
13
|
+
- Running It
|
|
14
|
+
- Where To Go From Here
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
This walks through the smallest complete gloo web app: a "Hello World" with two pages (`home` and `about`) sharing a common layout and nav bar. It uses the `gloo-web` core library — see Plugins for how core libraries are installed and loaded in general.
|
|
19
|
+
|
|
20
|
+
The app runs in App Mode (`gloo --app {path}`, see Application) — gloo looks for a `start.gloo` file at the root of the app folder and runs it. That file loads everything else and starts the web server.
|
|
21
|
+
|
|
22
|
+
## File and Folder Layout
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
hello/
|
|
26
|
+
start.gloo
|
|
27
|
+
app/
|
|
28
|
+
app.gloo
|
|
29
|
+
layout/
|
|
30
|
+
primary.gloo
|
|
31
|
+
shared/
|
|
32
|
+
nav.gloo
|
|
33
|
+
page/
|
|
34
|
+
home.gloo
|
|
35
|
+
about.gloo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `start.gloo` — the entry point. Loads the `web` core library, then everything under `app/`, `layout/`, `shared/`, and `page/`, then starts the server.
|
|
39
|
+
- `app/app.gloo` — the `app` object: the app's URL and its `svr` (web server), including server config and the default routes (layout, home page).
|
|
40
|
+
- `layout/primary.gloo` — the page layout shared by every page: the HTML shell, the nav bar, and where a page's own head/body content gets inserted.
|
|
41
|
+
- `shared/nav.gloo` — a partial (reusable fragment) for the nav bar, included by the layout.
|
|
42
|
+
- `page/home.gloo`, `page/about.gloo` — the two pages. Every `[page]` object loaded under the root `page` container automatically becomes a route: `page.home` answers `/`, `page.about` answers `/about`. Routes are wired up by the router when the server starts — there's no separate routing table to maintain by hand.
|
|
43
|
+
|
|
44
|
+
## start.gloo
|
|
45
|
+
|
|
46
|
+
```gloo
|
|
47
|
+
#
|
|
48
|
+
# Start the hello app.
|
|
49
|
+
#
|
|
50
|
+
# Run it:
|
|
51
|
+
# gloo --app ~/gloo/projects/apps/hello
|
|
52
|
+
#
|
|
53
|
+
|
|
54
|
+
start [container] :
|
|
55
|
+
|
|
56
|
+
on_load [script] :
|
|
57
|
+
|
|
58
|
+
# Load the web core library.
|
|
59
|
+
load lib web
|
|
60
|
+
|
|
61
|
+
# Load the app's own objects.
|
|
62
|
+
load app/*
|
|
63
|
+
load layout/*
|
|
64
|
+
load shared/*
|
|
65
|
+
load page/*
|
|
66
|
+
|
|
67
|
+
# Start the server and open it in a browser.
|
|
68
|
+
tell app.svr to start
|
|
69
|
+
tell app.url to open
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## app/app.gloo
|
|
73
|
+
|
|
74
|
+
```gloo
|
|
75
|
+
#
|
|
76
|
+
# The hello app: web server config and default routes.
|
|
77
|
+
#
|
|
78
|
+
|
|
79
|
+
app [can] :
|
|
80
|
+
|
|
81
|
+
# The app's URL.
|
|
82
|
+
url [uri] : http://localhost:8080/
|
|
83
|
+
|
|
84
|
+
# The web server for the app.
|
|
85
|
+
svr [svr] :
|
|
86
|
+
|
|
87
|
+
config [container] :
|
|
88
|
+
scheme [string] : http
|
|
89
|
+
host [string] : localhost
|
|
90
|
+
port [string] : 8080
|
|
91
|
+
|
|
92
|
+
on_start [script] : show 'hello app started'
|
|
93
|
+
on_stop [script] : show 'hello app stopped'
|
|
94
|
+
|
|
95
|
+
# Default layout and home page.
|
|
96
|
+
layout [alias] : layout.primary
|
|
97
|
+
home [alias] : page.home
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`svr` is a `gloo-web` object type (see Plugins — Core Libraries). `layout` and `home` are conventional aliases: `layout` points at the partial used to wrap every page that doesn't specify its own, and `home` points at the page served for `/`. A real app would also set `error [alias] : page.err` to control the page shown on a server error, but it's optional — gloo falls back to a generic message if it's not set.
|
|
101
|
+
|
|
102
|
+
## layout/primary.gloo
|
|
103
|
+
|
|
104
|
+
```gloo
|
|
105
|
+
#
|
|
106
|
+
# The shared page layout: HTML shell + nav bar.
|
|
107
|
+
# Every page renders inside this unless it specifies its own layout.
|
|
108
|
+
#
|
|
109
|
+
|
|
110
|
+
layout [can] :
|
|
111
|
+
primary [part] :
|
|
112
|
+
content [can] :
|
|
113
|
+
|
|
114
|
+
html_open [text] : BEGIN
|
|
115
|
+
<!DOCTYPE html>
|
|
116
|
+
<html lang="en">
|
|
117
|
+
<head>
|
|
118
|
+
<%= head %>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
END
|
|
122
|
+
|
|
123
|
+
nav [alias] : shared.nav
|
|
124
|
+
body [string] : <%= body %>
|
|
125
|
+
|
|
126
|
+
html_close [text] : BEGIN
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
END
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`primary` is a `[part]` (partial) — see Plugins for the `gloo-web` object list. `<%= head %>` and `<%= body %>` are filled in automatically by the page being rendered: whatever that page defines under its own `head` and `body` children. `nav` is an alias to the shared nav partial below, rendered inline wherever it appears in the layout's content.
|
|
133
|
+
|
|
134
|
+
## shared/nav.gloo
|
|
135
|
+
|
|
136
|
+
```gloo
|
|
137
|
+
#
|
|
138
|
+
# The shared nav bar, included by the layout.
|
|
139
|
+
#
|
|
140
|
+
|
|
141
|
+
shared [can] :
|
|
142
|
+
nav [part] :
|
|
143
|
+
content [text] : BEGIN
|
|
144
|
+
<nav>
|
|
145
|
+
<a href="/">Home</a>
|
|
146
|
+
<a href="/about">About</a>
|
|
147
|
+
</nav>
|
|
148
|
+
END
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## page/home.gloo
|
|
152
|
+
|
|
153
|
+
```gloo
|
|
154
|
+
#
|
|
155
|
+
# The home page ("/").
|
|
156
|
+
#
|
|
157
|
+
|
|
158
|
+
page [can] :
|
|
159
|
+
home [page] :
|
|
160
|
+
|
|
161
|
+
params [can] :
|
|
162
|
+
msg [string] : Hello, World!
|
|
163
|
+
|
|
164
|
+
head [e] :
|
|
165
|
+
content [can] :
|
|
166
|
+
title [e] : Hello
|
|
167
|
+
|
|
168
|
+
body [e] :
|
|
169
|
+
content [can] :
|
|
170
|
+
h1 [e] : <%= msg %>
|
|
171
|
+
p [e] : This is the home page.
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`page.home` is what `app.svr.home` points to, so it answers requests to `/`. `params` declares values the page's content can reference — here just `msg`, used in the `<h1>` via `<%= msg %>`. `head` and `title` and `body`, `h1`, `p` are `[e]` (element) objects — see Plugins — the building blocks the layout's `<%= head %>`/`<%= body %>` render.
|
|
175
|
+
|
|
176
|
+
## page/about.gloo
|
|
177
|
+
|
|
178
|
+
```gloo
|
|
179
|
+
#
|
|
180
|
+
# The about page ("/about"), reached by name automatically —
|
|
181
|
+
# no routing table entry required.
|
|
182
|
+
#
|
|
183
|
+
|
|
184
|
+
page [can] :
|
|
185
|
+
about [page] :
|
|
186
|
+
|
|
187
|
+
head [e] :
|
|
188
|
+
content [can] :
|
|
189
|
+
title [e] : About
|
|
190
|
+
|
|
191
|
+
body [e] :
|
|
192
|
+
content [can] :
|
|
193
|
+
h1 [e] : About
|
|
194
|
+
p [e] : A trivial two-page gloo web app.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Because this page is named `about` and lives directly under the root `page` container, it's automatically routed to `/about` the moment the server starts — see File and Folder Layout above.
|
|
198
|
+
|
|
199
|
+
## Running It
|
|
200
|
+
|
|
201
|
+
```shell
|
|
202
|
+
gloo --app ~/gloo/projects/apps/hello
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This runs `start.gloo`, which starts the server and opens `http://localhost:8080/` in a browser. Visit `/about` to see the second page. Stop the app the same way as any other gloo app (`q` at the prompt, or `Ctrl-C`).
|
|
206
|
+
|
|
207
|
+
Once the app is running, `gloo-web`'s objects (`page`, `part`, `e`, `svr`, `form`, `field`) are all documented in-app — `help> object page`, etc. — see Plugins.
|
|
208
|
+
|
|
209
|
+
## Where To Go From Here
|
|
210
|
+
|
|
211
|
+
This example deliberately leaves out most of what `gloo-web` supports: forms and fields (`form`, `field`), page parameters populated from query strings, sessions, static assets (images/CSS/JS served from an `asset/` folder, with `image_tag`/`css_tag`/`js_tag` layout helpers), a database-backed page (`gloo-db` plus a `sqlite`/`mysql`/`postgres` driver — see Plugins), and a dedicated error page. Each of those follows the same shape shown here: an object type from `gloo-web` (or another core library), loaded the same way, documented the same way in `help`.
|
data/lib/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.2.0
|
data/lib/VERSION_NOTES
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
6.1.0 - 2026.08.04
|
|
2
8
|
- Adds narrative documentation: 10 pages in `docs/`, covering getting started, application, language objects/syntax/scripting, operators, iterators, objects, verbs, and plugins
|
|
3
9
|
- Adds an in-app interactive help shell (`help`/`?`) to look up verbs, objects, settings, libraries, extensions, and doc pages, with tab completion
|
data/lib/gloo/app/engine.rb
CHANGED
|
@@ -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
|
|
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
|
|
179
|
-
puts ex.message
|
|
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
|
|
183
|
+
puts @theme.error( msg ) unless @quiet
|
|
183
184
|
end
|
|
184
185
|
end
|
|
185
186
|
|
data/lib/gloo/app/platform.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Gloo
|
|
|
18
18
|
PAGER_CMD = 'less -R -F -X'.freeze
|
|
19
19
|
|
|
20
20
|
attr_reader :prompt, :table
|
|
21
|
+
attr_accessor :theme
|
|
21
22
|
|
|
22
23
|
#
|
|
23
24
|
# Set up Platform.
|
|
@@ -25,6 +26,10 @@ module Gloo
|
|
|
25
26
|
def initialize
|
|
26
27
|
@prompt = Gloo::App::Prompt.new( self )
|
|
27
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
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
#
|
data/lib/gloo/app/prompt.rb
CHANGED
|
@@ -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
|
-
|
|
90
|
+
theme = @platform.theme
|
|
91
|
+
return "#{theme.heading( 'gloo' )} #{theme.accent( d )} #{theme.emphasis( t )} >"
|
|
91
92
|
end
|
|
92
93
|
|
|
93
94
|
end
|
data/lib/gloo/app/settings.rb
CHANGED
|
@@ -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
|
-
|
|
37
|
-
puts
|
|
38
|
-
puts '
|
|
39
|
-
puts '
|
|
40
|
-
puts '
|
|
41
|
-
puts '
|
|
42
|
-
puts '
|
|
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
|
-
|
|
51
|
-
puts
|
|
52
|
-
puts '
|
|
53
|
-
puts '
|
|
54
|
-
puts '
|
|
55
|
-
puts '
|
|
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
|
data/lib/gloo/app/table.rb
CHANGED
|
@@ -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
|
|
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
|
|
@@ -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
|