@@ -43,6 +43,9 @@ pub fn parse(source: &str) -> Result<Keyboard, ParseCError> {
4343 . map ( |( i, td) | ( td. name . clone ( ) , i) )
4444 . collect ( ) ;
4545
46+ let encoder_bindings =
47+ extract_encoder_bindings ( & cleaned, & layer_map, & defines, & custom_keycodes) ;
48+
4649 let raw_layers = extract_raw_layers ( & cleaned) ?;
4750
4851 let mut layers: Vec < Layer > = raw_layers
@@ -61,7 +64,15 @@ pub fn parse(source: &str) -> Result<Keyboard, ParseCError> {
6164 )
6265 } )
6366 . collect ( ) ;
64- Layer { name, index, keys }
67+ let sensor_bindings = encoder_bindings
68+ . get ( & index)
69+ . map_or_else ( Vec :: new, |pair| vec ! [ pair. clone( ) ] ) ;
70+ Layer {
71+ name,
72+ index,
73+ keys,
74+ sensor_bindings,
75+ }
6576 } )
6677 . collect ( ) ;
6778
@@ -711,6 +722,200 @@ fn parse_tap_dance_action(
711722 vec ! [ ]
712723}
713724
725+ // ── Encoder extraction ────────────────────────────────────────────────────────
726+
727+ /// Parse `encoder_update_user` and return a map from layer index to
728+ /// (clockwise key, counter-clockwise key).
729+ ///
730+ /// Handles two common QMK patterns:
731+ ///
732+ /// 1. **Per-layer switch**: `switch (get_highest_layer(layer_state)) { case _BASE: … }`
733+ /// 2. **Global**: a bare `if (clockwise) { tap_code(CW); } else { tap_code(CCW); }`
734+ /// at the function's top level, applied to every layer in `layer_map`.
735+ fn extract_encoder_bindings (
736+ s : & str ,
737+ layer_map : & HashMap < String , usize > ,
738+ defines : & HashMap < String , String > ,
739+ custom_keycodes : & HashSet < String > ,
740+ ) -> HashMap < usize , ( KeyExpr , KeyExpr ) > {
741+ let mut result = HashMap :: new ( ) ;
742+
743+ let Some ( body) = find_function_body ( s, "encoder_update_user" ) else {
744+ return result;
745+ } ;
746+
747+ if body. contains ( "get_highest_layer" )
748+ && let Some ( switch_body) = extract_switch_on_highest_layer ( & body)
749+ {
750+ parse_switch_cases ( & switch_body, layer_map, defines, custom_keycodes, & mut result) ;
751+ }
752+
753+ if result. is_empty ( )
754+ && let Some ( pair) = extract_clockwise_pair ( & body, defines, custom_keycodes)
755+ {
756+ for & idx in layer_map. values ( ) {
757+ result. insert ( idx, pair. clone ( ) ) ;
758+ }
759+ }
760+
761+ result
762+ }
763+
764+ /// Extract the body (between the outer braces) of a named C function.
765+ fn find_function_body ( s : & str , fn_name : & str ) -> Option < String > {
766+ let pos = s. find ( fn_name) ?;
767+ let after = & s[ pos + fn_name. len ( ) ..] ;
768+ let paren = after. find ( '(' ) ?;
769+ let close_paren = find_matching ( & after[ paren..] , '(' , ')' ) ?;
770+ let after_params = after[ paren + close_paren + 1 ..] . trim_start ( ) ;
771+ let brace = after_params. find ( '{' ) ?;
772+ let body_rest = & after_params[ brace..] ;
773+ let close_brace = find_matching ( body_rest, '{' , '}' ) ?;
774+ Some ( body_rest[ 1 ..close_brace] . to_string ( ) )
775+ }
776+
777+ /// Extract the body of `switch (get_highest_layer(...)) { … }`.
778+ fn extract_switch_on_highest_layer ( body : & str ) -> Option < String > {
779+ let switch_pos = body. find ( "switch" ) ?;
780+ let after = & body[ switch_pos..] ;
781+ let paren = after. find ( '(' ) ?;
782+ let close_paren = find_matching ( & after[ paren..] , '(' , ')' ) ?;
783+ if !after[ paren + 1 ..paren + close_paren] . contains ( "get_highest_layer" ) {
784+ return None ;
785+ }
786+ let after_cond = after[ paren + close_paren + 1 ..] . trim_start ( ) ;
787+ let brace = after_cond. find ( '{' ) ?;
788+ let close = find_matching ( & after_cond[ brace..] , '{' , '}' ) ?;
789+ Some ( after_cond[ brace + 1 ..brace + close] . to_string ( ) )
790+ }
791+
792+ /// Walk the cases of a switch body and populate `result` with per-layer bindings.
793+ fn parse_switch_cases (
794+ switch_body : & str ,
795+ layer_map : & HashMap < String , usize > ,
796+ defines : & HashMap < String , String > ,
797+ custom_keycodes : & HashSet < String > ,
798+ result : & mut HashMap < usize , ( KeyExpr , KeyExpr ) > ,
799+ ) {
800+ let mut remaining = switch_body;
801+ while let Some ( case_pos) = remaining. find ( "case " ) {
802+ let after_case = & remaining[ case_pos + 5 ..] ;
803+ let Some ( colon) = after_case. find ( ':' ) else {
804+ break ;
805+ } ;
806+ let layer_name = after_case[ ..colon] . trim ( ) ;
807+ let case_start = case_pos + 5 + colon + 1 ;
808+ let case_body = & remaining[ case_start..] ;
809+ let end = find_case_body_end ( case_body) ;
810+ if let Some ( idx) = resolve_layer ( layer_name, layer_map)
811+ && let Some ( pair) =
812+ extract_clockwise_pair ( & case_body[ ..end] , defines, custom_keycodes)
813+ {
814+ result. insert ( idx, pair) ;
815+ }
816+ remaining = & remaining[ case_start + end..] ;
817+ }
818+ }
819+
820+ /// Return the length of the current case's body — stopping at the next `case`,
821+ /// `default:`, or the end of the enclosing switch block.
822+ fn find_case_body_end ( s : & str ) -> usize {
823+ let mut depth = 0usize ;
824+ let mut i = 0 ;
825+ while i < s. len ( ) {
826+ match s. as_bytes ( ) [ i] {
827+ b'{' => {
828+ depth += 1 ;
829+ i += 1 ;
830+ }
831+ b'}' => {
832+ if depth == 0 {
833+ return i;
834+ }
835+ depth -= 1 ;
836+ i += 1 ;
837+ }
838+ _ => {
839+ if depth == 0
840+ && ( s[ i..] . starts_with ( "case " ) || s[ i..] . starts_with ( "default:" ) )
841+ {
842+ return i;
843+ }
844+ i += 1 ;
845+ }
846+ }
847+ }
848+ s. len ( )
849+ }
850+
851+ /// Extract a (clockwise, counter-clockwise) key pair from an `if (clockwise)`
852+ /// block. Returns `None` if the pattern is not found or not recognized.
853+ fn extract_clockwise_pair (
854+ body : & str ,
855+ defines : & HashMap < String , String > ,
856+ custom_keycodes : & HashSet < String > ,
857+ ) -> Option < ( KeyExpr , KeyExpr ) > {
858+ // Allow both "if (clockwise)" and "if(clockwise)" spellings.
859+ let if_pos = body. find ( "if" ) ?;
860+ let after_if = body[ if_pos + 2 ..] . trim_start ( ) ;
861+ if !after_if. starts_with ( '(' ) {
862+ return None ;
863+ }
864+ let close_paren = find_matching ( after_if, '(' , ')' ) ?;
865+ if !after_if[ 1 ..close_paren] . contains ( "clockwise" ) {
866+ return None ;
867+ }
868+ let after_cond = after_if[ close_paren + 1 ..] . trim_start ( ) ;
869+ let ( clockwise_body, rest) = extract_block_or_stmt ( after_cond) ?;
870+ let after_else_kw = rest. trim_start ( ) ;
871+ let after_else = after_else_kw. strip_prefix ( "else" ) ?. trim_start ( ) ;
872+ let ( counter_body, _) = extract_block_or_stmt ( after_else) ?;
873+ let cw = extract_tap_code_key ( & clockwise_body, defines, custom_keycodes) ?;
874+ let ccw = extract_tap_code_key ( & counter_body, defines, custom_keycodes) ?;
875+ Some ( ( cw, ccw) )
876+ }
877+
878+ /// Return the body of a `{ … }` block or single statement, and the remaining
879+ /// text after the closing delimiter.
880+ fn extract_block_or_stmt ( s : & str ) -> Option < ( String , & str ) > {
881+ if s. starts_with ( '{' ) {
882+ let close = find_matching ( s, '{' , '}' ) ?;
883+ Some ( ( s[ 1 ..close] . to_string ( ) , & s[ close + 1 ..] ) )
884+ } else {
885+ let end = s. find ( ';' ) ? + 1 ;
886+ Some ( ( s[ ..end - 1 ] . to_string ( ) , & s[ end..] ) )
887+ }
888+ }
889+
890+ /// Extract the ZMK key expression from a `tap_code(KC_X)` or
891+ /// `tap_code16(KC_X)` or `register_code(KC_X)` call.
892+ fn extract_tap_code_key (
893+ body : & str ,
894+ defines : & HashMap < String , String > ,
895+ custom_keycodes : & HashSet < String > ,
896+ ) -> Option < KeyExpr > {
897+ for fn_name in & [ "tap_code16" , "tap_code" , "register_code" ] {
898+ if let Some ( pos) = body. find ( fn_name) {
899+ let after = body[ pos + fn_name. len ( ) ..] . trim_start ( ) ;
900+ if !after. starts_with ( '(' ) {
901+ continue ;
902+ }
903+ let close = find_matching ( after, '(' , ')' ) ?;
904+ let arg = after[ 1 ..close] . trim ( ) ;
905+ let empty_lm: HashMap < String , usize > = HashMap :: new ( ) ;
906+ let empty_td: HashMap < String , usize > = HashMap :: new ( ) ;
907+ let key =
908+ parse_key_expr_str ( arg, & empty_lm, defines, custom_keycodes, & empty_td) ;
909+ if let Key :: Kp ( expr) = key {
910+ return Some ( expr) ;
911+ }
912+ // Fallback: try direct QMK→ZMK lookup without the full key pipeline
913+ return KeyExpr :: from_qmk_key ( arg) ;
914+ }
915+ }
916+ None
917+ }
918+
714919// ── Utilities ─────────────────────────────────────────────────────────────────
715920
716921/// Find the index of the closing delimiter matching the opening at position 0.
@@ -1193,4 +1398,86 @@ uint8_t layer_state_set_user(uint8_t state) {
11931398 assert_eq ! ( tri. upper, 2 ) ;
11941399 assert_eq ! ( tri. tri, 3 ) ;
11951400 }
1401+
1402+ // ── encoder_update_user parsing ───────────────────────────────────────────
1403+
1404+ #[ test]
1405+ fn encoder_global_pattern ( ) {
1406+ let src = r"
1407+ enum layers { _BASE, _FN };
1408+ const uint16_t PROGMEM keymaps[][1][2] = {
1409+ [_BASE] = LAYOUT(KC_A, KC_B),
1410+ [_FN] = LAYOUT(KC_TRANSPARENT, KC_TRANSPARENT),
1411+ };
1412+ bool encoder_update_user(uint8_t index, bool clockwise) {
1413+ if (clockwise) {
1414+ tap_code(KC_VOLU);
1415+ } else {
1416+ tap_code(KC_VOLD);
1417+ }
1418+ return false;
1419+ }
1420+ " ;
1421+ let km = super :: parse ( src) . unwrap ( ) ;
1422+ assert_eq ! ( km. layers. len( ) , 2 ) ;
1423+ for layer in & km. layers {
1424+ assert_eq ! ( layer. sensor_bindings. len( ) , 1 , "layer {} should have one encoder pair" , layer. name) ;
1425+ let ( cw, ccw) = & layer. sensor_bindings [ 0 ] ;
1426+ assert_eq ! ( cw. to_string( ) , "C_VOL_UP" ) ;
1427+ assert_eq ! ( ccw. to_string( ) , "C_VOL_DN" ) ;
1428+ }
1429+ }
1430+
1431+ #[ test]
1432+ fn encoder_per_layer_switch ( ) {
1433+ let src = r"
1434+ enum layers { _BASE, _LOWER };
1435+ const uint16_t PROGMEM keymaps[][1][2] = {
1436+ [_BASE] = LAYOUT(KC_A, KC_B),
1437+ [_LOWER] = LAYOUT(KC_TRANSPARENT, KC_TRANSPARENT),
1438+ };
1439+ bool encoder_update_user(uint8_t index, bool clockwise) {
1440+ switch (get_highest_layer(layer_state)) {
1441+ case _BASE:
1442+ if (clockwise) {
1443+ tap_code(KC_VOLU);
1444+ } else {
1445+ tap_code(KC_VOLD);
1446+ }
1447+ break;
1448+ case _LOWER:
1449+ if (clockwise) {
1450+ tap_code(KC_PGDN);
1451+ } else {
1452+ tap_code(KC_PGUP);
1453+ }
1454+ break;
1455+ }
1456+ return false;
1457+ }
1458+ " ;
1459+ let km = super :: parse ( src) . unwrap ( ) ;
1460+ let base = km. layers . iter ( ) . find ( |l| l. name == "_BASE" ) . unwrap ( ) ;
1461+ assert_eq ! ( base. sensor_bindings. len( ) , 1 ) ;
1462+ let ( cw, ccw) = & base. sensor_bindings [ 0 ] ;
1463+ assert_eq ! ( cw. to_string( ) , "C_VOL_UP" ) ;
1464+ assert_eq ! ( ccw. to_string( ) , "C_VOL_DN" ) ;
1465+
1466+ let lower = km. layers . iter ( ) . find ( |l| l. name == "_LOWER" ) . unwrap ( ) ;
1467+ assert_eq ! ( lower. sensor_bindings. len( ) , 1 ) ;
1468+ assert_eq ! ( lower. sensor_bindings[ 0 ] . 0 . to_string( ) , "PG_DN" ) ;
1469+ assert_eq ! ( lower. sensor_bindings[ 0 ] . 1 . to_string( ) , "PG_UP" ) ;
1470+ }
1471+
1472+ #[ test]
1473+ fn no_encoder_means_empty_sensor_bindings ( ) {
1474+ let src = r"
1475+ enum layers { _BASE };
1476+ const uint16_t PROGMEM keymaps[][1][1] = {
1477+ [_BASE] = LAYOUT(KC_A),
1478+ };
1479+ " ;
1480+ let km = super :: parse ( src) . unwrap ( ) ;
1481+ assert ! ( km. layers[ 0 ] . sensor_bindings. is_empty( ) ) ;
1482+ }
11961483}
0 commit comments