FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(examples): showcase — every capability in one window · turinglambdaai/glaze@cf11402 · GitHub

Commit cf11402

Browse files
feat(examples): showcase — every capability in one window
examples/showcase: five tabs, each wired to the real backend — - bridge: typed params (bad input -> 400 naming it), :path params, deliberate 500 whose exception flows back via on-error -> SSE - events: 1Hz backend clock stream + on-demand broadcasts - system: clipboard / notification / Finder reveal / window title+size - verification: agent self-check (title/url/capture assertions) with the screenshot served back into the page - security+update: token-guarded route (cookie-authed call vs raw 401), check-update against a local v99 manifest - resident tray (About/Quit) + single-instance lock Also: glaze re-exports glaze/update; sys-demo merged into showcase; examples/README.md capability index.
1 parent 71ccdac commit cf11402

8 files changed

Lines changed: 290 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ If a platform's native libraries aren't available at runtime, the tray silently
302302

303303
| Example | What it shows |
304304
|---|---|
305+
| [`examples/showcase/`](examples/showcase/) | **Kitchen sink (start here)** — every capability in one window |
305306
| [`examples/hello/`](examples/hello/) | Minimal app — `run-app` in 8 lines |
306307
| [`examples/counter/`](examples/counter/) | JS↔Racket bridge — `fetch` calls Racket state |
307308
| [`examples/webview-demo.rkt`](examples/webview-demo.rkt) | Webview lifecycle: load, navigate, close, verification APIs |

‎README.zh-CN.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ Glaze 提供跨平台的系统托盘,让你的应用驻留在通知区 / 菜
294294

295295
| 示例 | 展示内容 |
296296
|---|---|
297+
| [`examples/showcase/`](examples/showcase/) | **综合演示(推荐先看)** —— 全部能力一屏尽览 |
297298
| [`examples/hello/`](examples/hello/) | 最小应用 —— 8 行 `run-app` |
298299
| [`examples/counter/`](examples/counter/) | JS↔Racket 桥接 —— `fetch` 调用 Racket 状态 |
299300
| [`examples/webview-demo.rkt`](examples/webview-demo.rkt) | WebView 生命周期:加载、导航、关闭、验证 API |

‎examples/README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Glaze Examples — 功能索引
2+
3+
| 示例 | 一句话 | 展示的能力 |
4+
|---|---|---|
5+
| [`showcase/`](showcase/) | **一屏看尽全部能力(推荐先看)** | 宏路由全形态(类型校验/400/:path/500)、SSE 事件流 + 后端错误回流(on-error)、系统功能(剪贴板/通知/Finder/窗口控制)、Agent 验证(title/url/capture + 截图回传)、API token(401 演示)、更新检查、托盘、单实例 |
6+
| [`hello/`](hello/) | 8 行最小应用 | run-app 一键入口、静态页面 |
7+
| [`counter/`](counter/) | JS↔Racket 桥接主打 | define-api-routes、SSE 广播驱动 UI、api.js 生成客户端、模块可组合(provide api/bus) |
8+
| [`webview-demo.rkt`](webview-demo.rkt) | WebView 生命周期 | 加载/导航/关闭/on-close/验证 API 实时打印、看门狗 |
9+
| [`agent-verify.rkt`](agent-verify.rkt) | 无人值守验证 | agent 工作流:轮询断言 + 截图 + 退出码 |
10+
| [`tray-demo.rkt`](tray-demo.rkt) | 跨平台托盘 | make-tray/菜单/tooltip 动态更新 |
11+
12+
## 快速开始
13+
14+
```bash
15+
racket examples/showcase/main.rkt # 综合演示(单实例锁定)
16+
racket examples/counter/main.rkt # 桥接 + 事件
17+
racket examples/hello/main.rkt # 最小应用
18+
```
19+
20+
所有示例均可 `raco glaze build` 打包为独立应用。

‎examples/showcase/main.rkt‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#lang racket/base
2+
3+
;; Glaze Showcase — one window, every capability, each tab wired to the
4+
;; real backend. Run: racket examples/showcase/main.rkt
5+
6+
(require racket/file
7+
racket/runtime-path
8+
glaze)
9+
10+
(define-runtime-path public "public")
11+
12+
;; ---- state & services ----------------------------------------------------
13+
(define bus (make-event-bus))
14+
(define wv-box (box #f))
15+
(define wide? (box #f))
16+
(define hits (box 0))
17+
18+
;; 1 Hz event stream (SSE tab)
19+
(void (thread (lambda ()
20+
(let loop ()
21+
(sleep 1)
22+
(bus-broadcast! bus 'tick (hasheq 'now (current-inexact-milliseconds)))
23+
(loop)))))
24+
25+
;; error reporting: handler exceptions flow back to the page as events
26+
(define (report-error! exn uri)
27+
(bus-broadcast! bus 'backend-error
28+
(hasheq 'uri uri 'message (exn-message exn))))
29+
30+
;; ---- routes: every define-api-routes shape --------------------------------
31+
(define-api-routes api
32+
;; typed params with defaults; bad input -> 400 naming the parameter
33+
[(POST "api/add")
34+
(add [a exact-integer?] [b exact-integer? 1])
35+
(begin (set-box! hits (add1 (unbox hits)))
36+
(hasheq 'sum (+ a b) 'hits (unbox hits)))]
37+
;; :path params arrive as plain arguments
38+
[(GET "api/echo/:msg")
39+
(echo msg)
40+
(hasheq 'echo msg 'len (string-length msg))]
41+
;; handler exceptions -> 500 + the on-error hook (see run-app below)
42+
[(POST "api/boom")
43+
(boom)
44+
(raise-user-error 'showcase "boom: 数据处理故意失败 (演示 500 + 错误上报)")]
45+
;; system integration
46+
[(POST "api/clip-write")
47+
(clip-write text)
48+
(hasheq 'ok (clipboard-set! text))]
49+
[(POST "api/clip-read")
50+
(clip-read)
51+
(hasheq 'text (clipboard-get))]
52+
[(POST "api/notify")
53+
(do-notify title body)
54+
(hasheq 'ok (notify! title body #:subtitle "showcase"))]
55+
[(POST "api/reveal")
56+
(do-reveal)
57+
(hasheq 'ok (reveal-path (path->string (find-system-path 'run-file))))]
58+
;; window controls (handle arrives via #:on-ready)
59+
[(POST "api/win-title")
60+
(win-title n)
61+
(let ([t (format "Showcase ~a" n)])
62+
(and (unbox wv-box) (webview-set-title! (unbox wv-box) t))
63+
(hasheq 'title t))]
64+
[(POST "api/win-size")
65+
(win-size)
66+
(let* ([w? (not (unbox wide?))])
67+
(set-box! wide? w?)
68+
(and (unbox wv-box)
69+
(webview-set-size! (unbox wv-box) (if w? 1080 860) (if w? 700 560)))
70+
(hasheq 'size (if w? "1080x700" "860x560")))]
71+
;; verification APIs: the agent workflow, on demand
72+
[(POST "api/self-check")
73+
(self-check)
74+
(let* ([wv (unbox wv-box)]
75+
[shot (and wv (webview-capture! wv (build-path public "shots" "latest.png")))]
76+
[pass? (and wv
77+
(equal? (webview-title wv) "Glaze Showcase")
78+
shot
79+
(>= (file-size shot) 5000))])
80+
(hasheq 'ok (and pass? #t)
81+
'title (and wv (webview-title wv))
82+
'url (and wv (webview-url wv))
83+
'shot-bytes (and shot (file-size shot))))]
84+
;; update check against a local manifest (v99 > current)
85+
[(POST "api/check-update")
86+
(check-update-now)
87+
(let ([info (check-update "http://127.0.0.1:18952/manifest.json"
88+
#:current-version "0.3.0")])
89+
(hasheq 'found (and info #t) 'info info))]
90+
;; token-guarded secret (guarded by #:api-token below)
91+
[(POST "api/secret")
92+
(secret)
93+
(hasheq 'secret "the cake is a lie")])
94+
95+
;; a deliberately hostile manifest for the update tab
96+
(call-with-output-file (build-path public "manifest.json")
97+
(lambda (o)
98+
(write-bytes (string->bytes/utf-8
99+
"{\"version\":\"99.0.0\",\"url\":\"https://example.com/99\",\"notes\":\"showcase manifest\"}") o))
100+
#:exists 'replace)
101+
102+
(module+ main
103+
(unless (single-instance? "glaze-showcase")
104+
(displayln "[showcase] another instance is running; exiting")
105+
(exit 1))
106+
107+
;; tray: About re-focuses the window, Quit exits
108+
(define tray
109+
(make-tray #:icon #f
110+
#:tooltip "Glaze Showcase"
111+
#:menu (list
112+
(make-menu-item "关于 / About"
113+
#:action (lambda ()
114+
(and (unbox wv-box)
115+
(webview-set-title!
116+
(unbox wv-box)
117+
"Glaze Showcase"))))
118+
(menu-separator)
119+
(make-menu-item "退出 / Quit"
120+
#:action (lambda () (exit 0))))))
121+
122+
(run-app
123+
#:public-dir public
124+
#:api api
125+
#:events bus
126+
#:api-token "showcase-demo-token"
127+
#:title "Glaze Showcase"
128+
#:width 860 #:height 560
129+
#:port 18952
130+
#:on-error report-error!
131+
#:on-ready (lambda (wv url) (when wv (set-box! wv-box wv)))))
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Glaze Showcase</title>
6+
<style>
7+
:root{--bg:#F4F3EE;--ink:#2d2a26;--sub:#6b675f;--acc:#C15F3C;--line:#e2ded4}
8+
*{box-sizing:border-box}
9+
body{font-family:-apple-system,sans-serif;background:var(--bg);color:var(--ink);margin:0;height:100vh;display:flex;flex-direction:column}
10+
header{padding:10px 16px;border-bottom:1px solid var(--line);display:flex;align-items:baseline;gap:10px}
11+
header h1{font-size:16px;margin:0} header em{color:var(--acc);font-style:normal;font-weight:800}
12+
header .sp{flex:1} #tick{font-size:11px;color:var(--sub);font-family:ui-monospace,Menlo,monospace}
13+
nav{display:flex;border-bottom:1px solid var(--line);background:#efede7}
14+
nav div{padding:8px 14px;font-size:13px;cursor:pointer;color:var(--sub)}
15+
nav div.on{color:var(--ink);border-bottom:2px solid var(--acc);font-weight:700}
16+
main{flex:1;overflow:auto;padding:16px}
17+
.tab{display:none}.tab.on{display:block}
18+
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0}
19+
button{background:var(--acc);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer}
20+
button.alt{background:#d8d4c8;color:var(--ink)}
21+
input{padding:8px 10px;border:1px solid #c9c4b8;border-radius:8px;font-size:13px;width:110px}
22+
.log{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:12px ui-monospace,Menlo,monospace;
23+
color:#4a463f;min-height:70px;max-height:180px;overflow:auto;white-space:pre-wrap;margin-top:10px}
24+
.err{color:#b3402a}.ok{color:#5d7f5b}
25+
.note{font-size:12px;color:var(--sub);margin:6px 0}
26+
img.shot{max-width:100%;border:1px solid var(--line);border-radius:10px;margin-top:10px;background:#fff}
27+
.big{font-size:34px;font-weight:800;color:var(--acc)}
28+
</style>
29+
</head>
30+
<body>
31+
<header><h1>Glaze <em>Showcase</em></h1><span class="note">每个标签都是真实后端调用</span>
32+
<span class="sp"></span><span id="tick">SSE 未连接</span></header>
33+
<nav id="nav">
34+
<div class="on" data-t="bridge">桥接 · 宏路由</div>
35+
<div data-t="events">事件 · SSE</div>
36+
<div data-t="sys">系统功能</div>
37+
<div data-t="verify">验证 · Agent</div>
38+
<div data-t="secure">安全 · 更新</div>
39+
</nav>
40+
<main>
41+
<!-- 桥接 -->
42+
<section class="tab on" id="bridge">
43+
<div class="row"><input id="a" value="17"> + <input id="b" value="25">
44+
<button onclick="doAdd()">求和(类型校验)</button>
45+
<button class="alt" onclick="doAdd(true)">传错误类型 → 400</button></div>
46+
<div class="row"><input id="msg" value="hello-glaze" style="width:180px">
47+
<button class="alt" onclick="doEcho()">:path 参数 echo</button></div>
48+
<div class="row"><button class="alt" onclick="doBoom()">触发 500(看错误回流)</button></div>
49+
<div class="note">错误会被后端 on-error 捕获并经 SSE 推回本页(见“事件”标签与下方日志)</div>
50+
<div class="log" id="blog"></div>
51+
</section>
52+
<!-- 事件 -->
53+
<section class="tab" id="events">
54+
<div class="row"><button onclick="glaze.api.add({a:1,b:1}).then(()=>log('elog','一次后端广播已触发(见右上角时钟与系统通知)'))">后端推一条事件</button>
55+
<button class="alt" onclick="glaze.api.notify({title:'Showcase',body:'SSE 事件驱动'})">同时发通知</button></div>
56+
<div class="note">右上角时间戳由后端 1Hz 广播驱动;下面是原始事件流:</div>
57+
<div class="log" id="elog"></div>
58+
</section>
59+
<!-- 系统 -->
60+
<section class="tab" id="sys">
61+
<div class="row"><input id="cliptext" value="showcase 剪贴板" style="width:200px">
62+
<button onclick="glaze.api.clipWrite({text:cli()}).then(r=>log('slog','剪贴板写入 '+r.ok))">写剪贴板</button>
63+
<button class="alt" onclick="glaze.api.clipRead().then(r=>log('slog','剪贴板内容: '+JSON.stringify(r.text)))">读剪贴板</button></div>
64+
<div class="row"><button onclick="glaze.api.notify({title:'Showcase',body:'系统通知来了'}).then(()=>log('slog','通知已发送(看右上角)'))">系统通知</button>
65+
<button class="alt" onclick="glaze.api.reveal().then(r=>log('slog','Finder 定位 '+r.ok))">Finder 定位</button></div>
66+
<div class="row"><button class="alt" onclick="glaze.api.winTitle({n:Date.now()%1000|0}).then(r=>log('slog','标题 → '+r.title))">改窗口标题</button>
67+
<button class="alt" onclick="glaze.api.winSize().then(r=>log('slog','尺寸 → '+r.size))">窗口宽度切换</button></div>
68+
<div class="log" id="slog"></div>
69+
</section>
70+
<!-- 验证 -->
71+
<section class="tab" id="verify">
72+
<div class="row"><button onclick="doCheck()">运行 Agent 自检</button>
73+
<span class="note">title / url / capture 三项断言 + 截图回传</span></div>
74+
<div class="log" id="vlog"></div>
75+
<img class="shot" id="shot" style="display:none">
76+
</section>
77+
<!-- 安全更新 -->
78+
<section class="tab" id="secure">
79+
<div class="row"><button onclick="glaze.api.secret().then(r=>log('glog','token 路由(cookie 自动授权): '+JSON.stringify(r)),
80+
e=>log('glog','失败: '+e.message))">调受 token 保护的路由</button>
81+
<button class="alt" onclick="fetch('/api/secret',{method:'POST'}).then(r=>log('glog','不带 token 直调 → HTTP '+r.status+'(401 = 守卫生效)'))">不带 token 直调 → 401</button></div>
82+
<div class="row"><button class="alt" onclick="glaze.api.checkUpdate().then(r=>log('glog','更新检查: '+JSON.stringify(r)))">检查更新(本地 manifest v99)</button></div>
83+
<div class="log" id="glog"></div>
84+
</section>
85+
</main>
86+
<script src="/glaze/api.js"></script>
87+
<script>
88+
const logs={blog:'#2d2a26',elog:'#2d2a26',slog:'#2d2a26',vlog:'#2d2a26',glog:'#2d2a26'};
89+
function log(id,msg){const el=document.getElementById(id);el.textContent+='▸ '+msg+'\n';el.scrollTop=el.scrollHeight;}
90+
const cli=()=>document.getElementById('cliptext').value;
91+
async function doAdd(bad){
92+
const a=bad?'不是数字':+document.getElementById('a').value;
93+
const b=+document.getElementById('b').value;
94+
try{const r=await glaze.api.add({a,b});log('blog','a+b = '+r.sum+'(第 '+r.hits+' 次调用)');}
95+
catch(e){log('blog','400: '+e.message);}
96+
}
97+
async function doEcho(){const r=await glaze.api.echo(document.getElementById('msg').value);
98+
log('blog',`:path echo → ${JSON.stringify(r.echo)} (len=${r.len})`);}
99+
async function doBoom(){
100+
try{await glaze.api.boom();}catch(e){log('blog','500 已返回: '+e.message.slice(0,60)+'…');}
101+
}
102+
async function doCheck(){
103+
const r=await glaze.api.selfCheck();
104+
log('vlog',`title=${JSON.stringify(r.title)} url=${r.url}`);
105+
log('vlog',`capture=${r.shotBytes} bytes → 全部通过: ${r.ok}`);
106+
const img=document.getElementById('shot');img.style.display='block';
107+
img.src='/shots/latest.png?'+Date.now();
108+
}
109+
// tabs
110+
document.getElementById('nav').addEventListener('click',e=>{
111+
if(!e.target.dataset.t)return;
112+
document.querySelectorAll('nav div').forEach(d=>d.classList.toggle('on',d===e.target));
113+
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('on',t.id===e.target.dataset.t));
114+
});
115+
// SSE: clock + error stream
116+
glaze.on('tick',d=>{document.getElementById('tick').textContent='SSE · '+new Date(d.now).toLocaleTimeString();});
117+
glaze.on('backend-error',d=>{log('blog','[on-error 回流] '+d.uri+': '+d.message.slice(0,50));log('elog','[error] '+d.uri);});
118+
</script>
119+
</body>
120+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"99.0.0","url":"https://example.com/99","notes":"showcase manifest"}

‎glaze-lib/main.rkt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"api-macros.rkt"
66
"events.rkt"
77
"sys/main.rkt"
8+
"update.rkt"
89
"browser.rkt"
910
"assets.rkt"
1011
"build.rkt"
@@ -13,7 +14,8 @@
1314
"webview/main.rkt")
1415

1516
(provide (all-from-out "server.rkt" "api.rkt" "api-macros.rkt" "events.rkt"
16-
"browser.rkt" "assets.rkt" "build.rkt" "sys/main.rkt")
17+
"browser.rkt" "assets.rkt" "build.rkt" "sys/main.rkt"
18+
"update.rkt")
1719
(all-from-out "app.rkt")
1820
(all-from-out "tray/main.rkt")
1921
(all-from-out "webview/main.rkt"))

‎glaze-lib/server.rkt‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,17 @@
276276
"\n};\n"))
277277

278278
;; "api/counter/bump" -> counterBump ; "api/items/:id/bump" -> itemsIdBump
279-
;; The first segment keeps its case (api/bump -> bump, not Bump).
279+
;; "api/clip-copy" -> clipCopy. Hyphenated segments camel-case (a bare
280+
;; hyphen key like `clip-copy:` would be ILLEGAL JavaScript and break the
281+
;; whole generated file); the first segment keeps a lowercase head.
282+
(define (js-camel seg first-lower?)
283+
(define parts (filter non-empty-string? (string-split seg "-")))
284+
(apply string-append
285+
(for/list ([p (in-list parts)] [i (in-naturals)])
286+
(if (and (zero? i) first-lower? (regexp-match? #rx"^[a-z]" p))
287+
p
288+
(string-append (string-upcase (substring p 0 1)) (substring p 1))))))
289+
280290
(define (route->js-name segments)
281291
(define drop-api
282292
(if (and (pair? segments) (string=? (first segments) "api"))
@@ -286,9 +296,8 @@
286296
(for/list ([seg (in-list drop-api)] [i (in-naturals)])
287297
(cond
288298
[(param? seg) (string-titlecase (param-id seg))]
289-
[(and (positive? i) (regexp-match? #rx"^[a-z]" seg))
290-
(string-append (string-upcase (substring seg 0 1)) (substring seg 1))]
291-
[else (if (param? seg) (param-id seg) seg)]))))
299+
[(zero? i) (js-camel seg #t)]
300+
[else (js-camel seg #f)]))))
292301

293302
;; Try each route against the request; on a match apply the handler and
294303
;; normalize its result (jsexpr -> 200 JSON; response -> itself; exception ->

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL