11use anyhow:: { anyhow, Result } ;
22use gtk3:: prelude:: * ;
33use serde_json:: to_string;
4+ use serde_json:: { json, Value } ;
45use urlencoding:: encode;
56use webkit2gtk:: {
6- LoadEvent , SettingsExt , UserContentInjectedFrames , UserContentManager , UserContentManagerExt ,
7- UserScript , UserScriptInjectionTime , WebView , WebViewExt ,
7+ LoadEvent , ScriptDialogType , SettingsExt , UserContentInjectedFrames , UserContentManager ,
8+ UserContentManagerExt , UserScript , UserScriptInjectionTime , WebView , WebViewExt ,
89} ;
910
1011fn main ( ) {
@@ -28,7 +29,8 @@ fn run_module_webui_window(port: u16, module_id: &str, module_name: &str) -> Res
2829
2930 let encoded_id = encode ( module_id. trim ( ) ) ;
3031 let bridge_base = format ! ( "http://127.0.0.1:{port}/api/v1/runtime/modules/{encoded_id}/webui" ) ;
31- let page_url = format ! ( "{bridge_base}/files" ) ;
32+ let page_base_url = format ! ( "{bridge_base}/files/" ) ;
33+ let page_url = format ! ( "{page_base_url}index.html" ) ;
3234 let title = if module_name. trim ( ) . is_empty ( ) {
3335 format ! ( "Module WebUI · {module_id}" )
3436 } else {
@@ -59,7 +61,30 @@ fn run_module_webui_window(port: u16, module_id: &str, module_name: &str) -> Res
5961 }
6062
6163 {
62- let page_url = page_url. clone ( ) ;
64+ let module_id = module_id. to_string ( ) ;
65+ webview. connect_script_dialog ( move |_view, dialog| {
66+ if dialog. dialog_type ( ) != ScriptDialogType :: Prompt {
67+ return false ;
68+ }
69+ let Some ( message) = dialog. message ( ) . map ( |value| value. to_string ( ) ) else {
70+ return false ;
71+ } ;
72+ let Some ( method) = message. strip_prefix ( "__abk__:" ) else {
73+ return false ;
74+ } ;
75+ let payload = dialog
76+ . prompt_get_default_text ( )
77+ . map ( |value| value. to_string ( ) )
78+ . unwrap_or_default ( ) ;
79+ let response = handle_sync_bridge_call ( port, & module_id, method, & payload)
80+ . unwrap_or_else ( |error| error. to_string ( ) ) ;
81+ dialog. prompt_set_text ( & response) ;
82+ true
83+ } ) ;
84+ }
85+
86+ {
87+ let page_base_url = page_base_url. clone ( ) ;
6388 webview. connect_load_failed ( move |view, event, uri, error| {
6489 if event == LoadEvent :: Finished {
6590 return false ;
@@ -69,7 +94,7 @@ fn run_module_webui_window(port: u16, module_id: &str, module_name: &str) -> Res
6994 "Module WebUI load failed" ,
7095 & format ! ( "{uri}\n \n {}" , error. message( ) ) ,
7196 ) ,
72- Some ( & page_url ) ,
97+ Some ( & page_base_url ) ,
7398 ) ;
7499 true
75100 } ) ;
@@ -141,11 +166,19 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
141166 const bridgeBase = {base};
142167 const rootBase = new URL(bridgeBase).origin;
143168 const packageIconBase = rootBase + "/api/v1/root-grants/";
169+ let moduleInfoCache = null;
144170
145171 function buildUrl(path) {{
146172 if (typeof path === "string" && /^(https?:)?\/\//.test(path)) {{
147173 return path;
148174 }}
175+ if (typeof path === "string" && (
176+ path === "/exec" ||
177+ path === "/spawn" ||
178+ path === "/module-info"
179+ )) {{
180+ return bridgeBase + path;
181+ }}
149182 if (typeof path === "string" && path.startsWith("/")) {{
150183 return rootBase + path;
151184 }}
@@ -231,6 +264,14 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
231264 return payload;
232265 }}
233266
267+ function syncNative(method, payload) {{
268+ try {{
269+ return window.prompt(`__abk__:${{method}}`, JSON.stringify(payload ?? null)) ?? "";
270+ }} catch (_error) {{
271+ return "";
272+ }}
273+ }}
274+
234275 function resolveCallback(callbackRef) {{
235276 if (typeof callbackRef === "function") {{
236277 return callbackRef;
@@ -276,13 +317,20 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
276317 callback && maybeCallback === undefined ? undefined : optionsOrCallback;
277318
278319 if (!callback) {{
279- const payload = syncRequest("POST", "/exec", {{ command, options }});
280- return normalizeOutput(payload);
320+ return syncNative("exec", {{ command, options }});
281321 }}
282322
283323 asyncRequest("POST", "/exec", {{ command, options }})
284- .then((payload) => callback(payload.code ?? 0, normalizeOutput(payload), ""))
285- .catch((error) => callback(1, String(error), ""));
324+ .then((payload) => {{
325+ const output = normalizeOutput(payload);
326+ const isOk = payload && payload.success !== false && (payload.code ?? 0) === 0;
327+ callback(
328+ payload.code ?? (isOk ? 0 : 1),
329+ isOk ? output : "",
330+ isOk ? "" : (payload.stderr || output || "command failed")
331+ );
332+ }})
333+ .catch((error) => callback(1, "", String(error)));
286334 }}
287335
288336 function spawn(command, args, options, callbackRef) {{
@@ -313,8 +361,11 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
313361 }}
314362
315363 function moduleInfo() {{
316- const payload = syncRequest("GET", "/module-info");
317- return payload.raw || JSON.stringify(payload.info || {{}});
364+ if (moduleInfoCache !== null) {{
365+ return moduleInfoCache;
366+ }}
367+ moduleInfoCache = syncNative("moduleInfo");
368+ return moduleInfoCache;
318369 }}
319370
320371 function moduleInfoObject() {{
@@ -328,9 +379,15 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
328379 function fullScreen(enabled) {{
329380 try {{
330381 if (enabled) {{
331- document.documentElement.requestFullscreen?.();
382+ const result = document.documentElement.requestFullscreen?.();
383+ if (result && typeof result.catch === "function") {{
384+ result.catch(() => {{}});
385+ }}
332386 }} else {{
333- document.exitFullscreen?.();
387+ const result = document.exitFullscreen?.();
388+ if (result && typeof result.catch === "function") {{
389+ result.catch(() => {{}});
390+ }}
334391 }}
335392 }} catch (_error) {{
336393 }}
@@ -340,11 +397,7 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
340397 }}
341398
342399 function listPackages(type) {{
343- const payload = syncRequest(
344- "GET",
345- "/api/v1/packages?type=" + encodeURIComponent(type || "all")
346- );
347- return JSON.stringify(payload.packages || []);
400+ return syncNative("listPackages", {{ type: type || "all" }});
348401 }}
349402
350403 function getPackagesInfo(packages) {{
@@ -356,10 +409,9 @@ fn build_ksu_bridge_script(bridge_base: &str) -> Result<String> {
356409 values = [];
357410 }}
358411 }}
359- const payload = syncRequest("POST", "/api/v1/packages/info ", {{
412+ return syncNative("getPackagesInfo ", {{
360413 packages: Array.isArray(values) ? values : [],
361414 }});
362- return JSON.stringify(payload.packages || []);
363415 }}
364416
365417 const originalFetch = window.fetch?.bind(window);
@@ -529,6 +581,85 @@ fn html_escape(value: &str) -> String {
529581 . replace ( '>' , ">" )
530582}
531583
584+ fn handle_sync_bridge_call (
585+ port : u16 ,
586+ module_id : & str ,
587+ method : & str ,
588+ payload_raw : & str ,
589+ ) -> Result < String > {
590+ let client = reqwest:: blocking:: Client :: builder ( )
591+ . timeout ( std:: time:: Duration :: from_secs ( 20 ) )
592+ . build ( ) ?;
593+ let encoded_module_id = encode ( module_id) ;
594+ let bridge_base =
595+ format ! ( "http://127.0.0.1:{port}/api/v1/runtime/modules/{encoded_module_id}/webui" ) ;
596+ let payload = if payload_raw. trim ( ) . is_empty ( ) {
597+ Value :: Null
598+ } else {
599+ serde_json:: from_str :: < Value > ( payload_raw) . unwrap_or ( Value :: Null )
600+ } ;
601+
602+ match method {
603+ "moduleInfo" => {
604+ let response = client. get ( format ! ( "{bridge_base}/module-info" ) ) . send ( ) ?;
605+ let value = response. json :: < Value > ( ) ?;
606+ Ok ( value
607+ . get ( "raw" )
608+ . and_then ( Value :: as_str)
609+ . unwrap_or ( "{}" )
610+ . to_string ( ) )
611+ }
612+ "listPackages" => {
613+ let package_type = payload. get ( "type" ) . and_then ( Value :: as_str) . unwrap_or ( "all" ) ;
614+ let response = client
615+ . get ( format ! (
616+ "http://127.0.0.1:{port}/api/v1/packages?type={}" ,
617+ encode( package_type)
618+ ) )
619+ . send ( ) ?;
620+ let value = response. json :: < Value > ( ) ?;
621+ Ok ( serde_json:: to_string (
622+ value. get ( "packages" ) . unwrap_or ( & Value :: Array ( vec ! [ ] ) ) ,
623+ ) ?)
624+ }
625+ "getPackagesInfo" => {
626+ let packages = payload
627+ . get ( "packages" )
628+ . cloned ( )
629+ . unwrap_or_else ( || Value :: Array ( vec ! [ ] ) ) ;
630+ let response = client
631+ . post ( format ! ( "http://127.0.0.1:{port}/api/v1/packages/info" ) )
632+ . json ( & json ! ( { "packages" : packages } ) )
633+ . send ( ) ?;
634+ let value = response. json :: < Value > ( ) ?;
635+ Ok ( serde_json:: to_string (
636+ value. get ( "packages" ) . unwrap_or ( & Value :: Array ( vec ! [ ] ) ) ,
637+ ) ?)
638+ }
639+ "exec" => {
640+ let command = payload
641+ . get ( "command" )
642+ . and_then ( Value :: as_str)
643+ . unwrap_or_default ( ) ;
644+ let options = payload. get ( "options" ) . cloned ( ) . unwrap_or ( Value :: Null ) ;
645+ let response = client
646+ . post ( format ! ( "{bridge_base}/exec" ) )
647+ . json ( & json ! ( {
648+ "command" : command,
649+ "options" : if options. is_null( ) { Value :: Null } else { options }
650+ } ) )
651+ . send ( ) ?;
652+ let value = response. json :: < Value > ( ) ?;
653+ Ok ( value
654+ . get ( "stdout" )
655+ . and_then ( Value :: as_str)
656+ . unwrap_or_default ( )
657+ . to_string ( ) )
658+ }
659+ other => Err ( anyhow ! ( "unsupported sync bridge method: {other}" ) ) ,
660+ }
661+ }
662+
532663#[ derive( Debug , Clone ) ]
533664struct CliArgs {
534665 port : u16 ,
0 commit comments