11//! HTTP server for geisterhand.
22//! Routes: /widgets, /click/:handle, /type/:handle, /slide/:handle,
3- //! /toggle/:handle, /state/:handle, /key, /chaos/start, /chaos/stop, /chaos/status
3+ //! /toggle/:handle, /state/:handle, /key, /scroll/:handle, / chaos/start, /chaos/stop, /chaos/status
44
55use tiny_http:: { Server , Response , Header , Method } ;
66
@@ -12,6 +12,8 @@ extern "C" {
1212 fn perry_geisterhand_queue_action1 ( closure_f64 : f64 , arg : f64 ) ;
1313 fn perry_geisterhand_queue_state_set ( handle : i64 , value : f64 ) ;
1414 fn perry_geisterhand_request_screenshot ( out_len : * mut usize ) -> * mut u8 ;
15+ fn perry_geisterhand_find_by_shortcut ( shortcut_ptr : * const u8 , shortcut_len : usize ) -> f64 ;
16+ fn perry_geisterhand_queue_scroll ( handle : i64 , x : f64 , y : f64 ) ;
1517}
1618
1719// Callback kind constants (must match perry-runtime/src/geisterhand_registry.rs)
@@ -49,6 +51,34 @@ fn parse_handle(path: &str, prefix: &str) -> Option<i64> {
4951 rest. parse :: < i64 > ( ) . ok ( )
5052}
5153
54+ /// Parse a query parameter value from a URL (e.g., "/widgets?label=Save" → Some("Save"))
55+ fn query_param < ' a > ( url : & ' a str , key : & str ) -> Option < & ' a str > {
56+ let query = url. split ( '?' ) . nth ( 1 ) ?;
57+ let needle = format ! ( "{}=" , key) ;
58+ for pair in query. split ( '&' ) {
59+ if let Some ( val) = pair. strip_prefix ( & needle) {
60+ return Some ( val) ;
61+ }
62+ }
63+ None
64+ }
65+
66+ /// Map widget type name to code
67+ fn widget_type_from_name ( name : & str ) -> Option < u8 > {
68+ match name {
69+ "button" => Some ( 0 ) ,
70+ "textfield" | "text_field" => Some ( 1 ) ,
71+ "slider" => Some ( 2 ) ,
72+ "toggle" => Some ( 3 ) ,
73+ "picker" => Some ( 4 ) ,
74+ "menu" => Some ( 5 ) ,
75+ "shortcut" => Some ( 6 ) ,
76+ "table" => Some ( 7 ) ,
77+ "scrollview" | "scroll_view" => Some ( 8 ) ,
78+ _ => name. parse :: < u8 > ( ) . ok ( ) ,
79+ }
80+ }
81+
5282/// Read request body as string
5383fn read_body ( request : & mut tiny_http:: Request ) -> String {
5484 let mut body = String :: new ( ) ;
@@ -67,7 +97,8 @@ pub fn run_server(port: u16) {
6797 } ;
6898
6999 for mut request in server. incoming_requests ( ) {
70- let path = request. url ( ) . to_string ( ) ;
100+ let full_url = request. url ( ) . to_string ( ) ;
101+ let path = full_url. split ( '?' ) . next ( ) . unwrap_or ( & full_url) ;
71102 let method = request. method ( ) . clone ( ) ;
72103
73104 // Handle CORS preflight
@@ -80,8 +111,8 @@ pub fn run_server(port: u16) {
80111 continue ;
81112 }
82113
83- let response = match ( method, path. as_str ( ) ) {
84- // GET /widgets — list all registered widgets
114+ let response = match ( method, path) {
115+ // GET /widgets — list all registered widgets (supports ?label= and ?type= filters)
85116 ( Method :: Get , "/widgets" ) => {
86117 let mut len: usize = 0 ;
87118 let ptr = unsafe { perry_geisterhand_get_registry_json ( & mut len) } ;
@@ -92,7 +123,42 @@ pub fn run_server(port: u16) {
92123 } else {
93124 "[]" . to_string ( )
94125 } ;
95- ok_json ( & json)
126+
127+ // Apply query param filters
128+ let label_filter = query_param ( & full_url, "label" ) ;
129+ let type_filter = query_param ( & full_url, "type" )
130+ . and_then ( |t| widget_type_from_name ( t) ) ;
131+
132+ if label_filter. is_some ( ) || type_filter. is_some ( ) {
133+ if let Ok ( arr) = serde_json:: from_str :: < Vec < serde_json:: Value > > ( & json) {
134+ let filtered: Vec < & serde_json:: Value > = arr. iter ( ) . filter ( |w| {
135+ if let Some ( label) = label_filter {
136+ if let Some ( wl) = w. get ( "label" ) . and_then ( |l| l. as_str ( ) ) {
137+ if !wl. to_lowercase ( ) . contains ( & label. to_lowercase ( ) ) {
138+ return false ;
139+ }
140+ } else {
141+ return false ;
142+ }
143+ }
144+ if let Some ( wt) = type_filter {
145+ if let Some ( wt_val) = w. get ( "widget_type" ) . and_then ( |t| t. as_u64 ( ) ) {
146+ if wt_val != wt as u64 {
147+ return false ;
148+ }
149+ } else {
150+ return false ;
151+ }
152+ }
153+ true
154+ } ) . collect ( ) ;
155+ ok_json ( & serde_json:: to_string ( & filtered) . unwrap_or_else ( |_| "[]" . to_string ( ) ) )
156+ } else {
157+ ok_json ( & json)
158+ }
159+ } else {
160+ ok_json ( & json)
161+ }
96162 }
97163
98164 // POST /click/:handle — fire onClick
@@ -293,6 +359,47 @@ pub fn run_server(port: u16) {
293359 }
294360 }
295361
362+ // POST /key — fire a keyboard shortcut by matching registered menu shortcuts
363+ ( Method :: Post , "/key" ) => {
364+ let body = read_body ( & mut request) ;
365+ let shortcut = match serde_json:: from_str :: < serde_json:: Value > ( & body) {
366+ Ok ( v) => v. get ( "shortcut" ) . and_then ( |s| s. as_str ( ) ) . unwrap_or ( "" ) . to_string ( ) ,
367+ Err ( _) => body. trim ( ) . to_string ( ) ,
368+ } ;
369+ if shortcut. is_empty ( ) {
370+ error_json ( 400 , "missing shortcut field" )
371+ } else {
372+ let closure = unsafe {
373+ perry_geisterhand_find_by_shortcut ( shortcut. as_ptr ( ) , shortcut. len ( ) )
374+ } ;
375+ if closure != 0.0 {
376+ unsafe { perry_geisterhand_queue_action ( closure) ; }
377+ ok_json ( r#"{"ok":true}"# )
378+ } else {
379+ error_json ( 404 , "no registered shortcut matches" )
380+ }
381+ }
382+ }
383+
384+ // POST /scroll/:handle — scroll a scrollview
385+ ( Method :: Post , p) if p. starts_with ( "/scroll/" ) => {
386+ match parse_handle ( p, "/scroll/" ) {
387+ Some ( handle) => {
388+ let body = read_body ( & mut request) ;
389+ let ( x, y) = match serde_json:: from_str :: < serde_json:: Value > ( & body) {
390+ Ok ( v) => (
391+ v. get ( "x" ) . and_then ( |v| v. as_f64 ( ) ) . unwrap_or ( 0.0 ) ,
392+ v. get ( "y" ) . and_then ( |v| v. as_f64 ( ) ) . unwrap_or ( 0.0 ) ,
393+ ) ,
394+ Err ( _) => ( 0.0 , 0.0 ) ,
395+ } ;
396+ unsafe { perry_geisterhand_queue_scroll ( handle, x, y) ; }
397+ ok_json ( r#"{"ok":true}"# )
398+ }
399+ None => error_json ( 400 , "invalid handle" ) ,
400+ }
401+ }
402+
296403 _ => error_json ( 404 , "not found" ) ,
297404 } ;
298405
0 commit comments