hyprconf2lua 1.2.0__py3-none-any.whl

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.
hyprconf2lua/parser.py ADDED
@@ -0,0 +1,494 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, List, Optional
3
+
4
+ from hyprconf2lua.lexer import Token, LexerError
5
+ from hyprconf2lua.ast import *
6
+
7
+
8
+ BIND_PREFIXES = {"bindm", "binde", "bindr", "bindl", "bindn", "bindo",
9
+ "bindt", "bindi", "bindp", "bindc", "bindd"}
10
+
11
+ BIND_FLAG_MAP = {
12
+ "bind": "",
13
+ "bindl": "l",
14
+ "bindr": "r",
15
+ "bindn": "n",
16
+ "bindo": "",
17
+ "bindm": "m",
18
+ "binde": "",
19
+ "bindt": "t",
20
+ "bindi": "i",
21
+ "bindp": "p",
22
+ "bindc": "c",
23
+ "bindd": "d",
24
+ }
25
+
26
+
27
+ def _parse_combined_bind(directive: str) -> str:
28
+ flags = ""
29
+ if directive.startswith("bind"):
30
+ remaining = directive[4:]
31
+ for ch in remaining:
32
+ flags += BIND_FLAG_MAP.get("bind" + ch, "")
33
+ return flags
34
+
35
+
36
+ class ParserError(Exception):
37
+ def __init__(self, message: str, token: Token):
38
+ self.line = token.line
39
+ self.col = token.col
40
+ super().__init__(f"L{token.line}:{token.col}: {message}")
41
+
42
+
43
+ class Parser:
44
+ def __init__(self, tokens: List[Token]):
45
+ self.tokens = tokens
46
+ self.pos = 0
47
+ self.variables: Dict[str, str] = {}
48
+
49
+ def peek(self) -> Token:
50
+ return self.tokens[self.pos]
51
+
52
+ def advance(self) -> Token:
53
+ t = self.tokens[self.pos]
54
+ self.pos += 1
55
+ return t
56
+
57
+ def expect(self, type_: str) -> Token:
58
+ t = self.peek()
59
+ if t.type != type_:
60
+ raise ParserError(f"Expected {type_}, got {t.type} ({t.value!r})", t)
61
+ return self.advance()
62
+
63
+ def skip_newlines(self):
64
+ while self.peek().type == "NEWLINE":
65
+ self.advance()
66
+
67
+ def parse(self) -> ConfigFile:
68
+ body = self.parse_block(stop=None)
69
+ return ConfigFile(body)
70
+
71
+ def parse_block(self, stop: Optional[str] = None) -> Block:
72
+ stmts: Block = []
73
+ self.skip_newlines()
74
+ while self.peek().type != "EOF" and (stop is None or self.peek().type != stop):
75
+ stmt = self.parse_stmt()
76
+ if stmt is not None:
77
+ stmts.append(stmt)
78
+ self.skip_newlines()
79
+ return self._group_submaps(stmts)
80
+
81
+ def _group_submaps(self, stmts: Block) -> Block:
82
+ result: Block = []
83
+ i = 0
84
+ while i < len(stmts):
85
+ s = stmts[i]
86
+ if isinstance(s, Directive) and s.key == "submap" and s.value and s.value[0].strip().lower() != "reset":
87
+ name = s.value[0].strip()
88
+ body: Block = []
89
+ i += 1
90
+ while i < len(stmts):
91
+ inner = stmts[i]
92
+ if isinstance(inner, Directive) and inner.key == "submap":
93
+ val = inner.value[0].strip().lower() if inner.value else ""
94
+ if val == "reset":
95
+ i += 1
96
+ break
97
+ body.append(inner)
98
+ i += 1
99
+ result.append(SubmapDef(name, body, s.line))
100
+ elif isinstance(s, Directive) and s.key == "submap_reset":
101
+ result.append(Comment("# submap = reset (orphan, no matching submap start)", s.line))
102
+ i += 1
103
+ else:
104
+ result.append(s)
105
+ i += 1
106
+ return result
107
+
108
+ def parse_stmt(self) -> Optional[BlockStmt]:
109
+ t = self.peek()
110
+
111
+ if t.type == "COMMENT":
112
+ return self.parse_comment()
113
+ if t.type == "BLOCK_CLOSE":
114
+ return None
115
+ if t.type == "DOLLAR":
116
+ return self.parse_variable_def()
117
+ if t.type == "IDENT":
118
+ return self.parse_ident_stmt()
119
+ if t.type == "NEWLINE":
120
+ self.advance()
121
+ return None
122
+ if t.type == "EOF":
123
+ return None
124
+
125
+ raise ParserError(f"Unexpected token: {t.value!r}", t)
126
+
127
+ def parse_comment(self) -> Comment:
128
+ t = self.advance()
129
+ return Comment(t.value, t.line)
130
+
131
+ def parse_variable_def(self) -> VariableDef:
132
+ self.advance()
133
+ name_t = self.expect("IDENT")
134
+ self.expect("EQUALS")
135
+ val = self.parse_value_rest()
136
+ return VariableDef(name_t.value, val, name_t.line)
137
+
138
+ def parse_value_rest(self) -> str:
139
+ parts = []
140
+ while self.peek().type in ("IDENT", "STRING", "DOLLAR", "DOT", "COLON") or \
141
+ (self.peek().type == "EQUALS" and parts):
142
+ if self.peek().type == "COMMENT":
143
+ break
144
+ t = self.advance()
145
+ if t.type == "DOLLAR":
146
+ var_t = self.expect("IDENT")
147
+ parts.append("$" + var_t.value)
148
+ else:
149
+ parts.append(t.value)
150
+ if self.peek().type == "DOT":
151
+ self.advance()
152
+ parts.append(".")
153
+
154
+ return self._join_tokens(parts)
155
+
156
+ @staticmethod
157
+ def _join_tokens(tokens: List[str]) -> str:
158
+ if not tokens:
159
+ return ""
160
+ result = tokens[0]
161
+ for t in tokens[1:]:
162
+ no_space_before = {":", ",", "=", "+", "-", "%", "@", "^", "*", "|", "~"}
163
+ no_space_after = {":", ",", "="}
164
+ if t in no_space_before or result[-1:] in no_space_after:
165
+ result += t
166
+ else:
167
+ result += " " + t
168
+ return result.strip()
169
+
170
+ def parse_comma_values(self) -> List[str]:
171
+ values = []
172
+ current = []
173
+ while self.peek().type not in ("NEWLINE", "EOF") and \
174
+ self.peek().type != "COMMENT":
175
+ if self.peek().type == "COMMA":
176
+ self.advance()
177
+ values.append(self._join_tokens(current))
178
+ current = []
179
+ continue
180
+ if self.peek().type == "DOLLAR":
181
+ self.advance()
182
+ var_t = self.expect("IDENT")
183
+ current.append("$" + var_t.value)
184
+ else:
185
+ t = self.advance()
186
+ current.append(t.value)
187
+ remaining = self._join_tokens(current)
188
+ if remaining:
189
+ values.append(remaining)
190
+ return values
191
+
192
+ def resolve_var(self, val: str) -> str:
193
+ import re
194
+ def _repl(m: re.Match) -> str:
195
+ var_name = m.group(1)
196
+ if var_name in self.variables:
197
+ return self.variables[var_name]
198
+ return m.group(0)
199
+ return re.sub(r'\$(\w+)', _repl, val)
200
+
201
+ def parse_ident_stmt(self) -> BlockStmt:
202
+ t = self.advance()
203
+ directive = t.value
204
+
205
+ # Handle colon-separated nested keys: section:subsection:key
206
+ colon_parts = [directive]
207
+ while self.peek().type == "COLON":
208
+ self.advance()
209
+ if self.peek().type == "IDENT":
210
+ colon_parts.append(self.advance().value)
211
+ else:
212
+ break
213
+ directive = ":".join(colon_parts)
214
+
215
+ if directive.startswith("bind"):
216
+ if self.peek().type == "BLOCK_OPEN":
217
+ return self.parse_general_directive(directive, t.line)
218
+ return self.parse_bind(directive, t.line)
219
+
220
+ if directive == "monitor":
221
+ return self.parse_monitor(t.line)
222
+ if directive == "windowrule":
223
+ return self.parse_windowrule(False, t.line)
224
+ if directive == "windowrulev2":
225
+ return self.parse_windowrule(True, t.line)
226
+ if directive == "exec-once" or directive == "execr-once":
227
+ return self.parse_exec(directive, t.line)
228
+ if directive == "exec":
229
+ return self.parse_exec(directive, t.line)
230
+ if directive == "exec-shutdown":
231
+ return self.parse_exec(directive, t.line)
232
+ if directive == "animation":
233
+ return self.parse_animation(t.line)
234
+ if directive == "bezier":
235
+ return self.parse_bezier(t.line)
236
+ if directive == "env":
237
+ return self.parse_env(t.line)
238
+ if directive == "source":
239
+ return self.parse_source(t.line)
240
+ if directive == "gesture":
241
+ if self.peek().type == "BLOCK_OPEN":
242
+ return self.parse_gesture(t.line)
243
+ return self.parse_general_directive(directive, t.line)
244
+ if directive == "workspace":
245
+ return self.parse_workspace(t.line)
246
+ if directive == "layerrule":
247
+ return self.parse_layerrule(t.line)
248
+ if directive == "submap":
249
+ return self.parse_submap(t.line)
250
+
251
+ colon_idx = directive.find(":")
252
+ if colon_idx > 0:
253
+ prefix = directive[:colon_idx]
254
+ if prefix == "device":
255
+ return self.parse_device(directive[colon_idx + 1:], t.line)
256
+
257
+ # Handle colon-separated nested key: section:key = value
258
+ if len(colon_parts) >= 3:
259
+ section_name = colon_parts[0]
260
+ sub_key = ":".join(colon_parts[1:])
261
+ self.expect("EQUALS")
262
+ val = self.parse_value_rest()
263
+ body = [Directive(sub_key, [val], t.line, 0)]
264
+ return Section(section_name, body, t.line)
265
+
266
+ # Handle colon-separated section:key = value (two parts)
267
+ if len(colon_parts) == 2:
268
+ section_name = colon_parts[0]
269
+ inner_key = colon_parts[1]
270
+ self.expect("EQUALS")
271
+ val = self.parse_value_rest()
272
+ body = [Directive(inner_key, [val], t.line, 0)]
273
+ return Section(section_name, body, t.line)
274
+
275
+ return self.parse_general_directive(directive, t.line)
276
+
277
+ def parse_general_directive(self, directive: str, line: int) -> Optional[BlockStmt]:
278
+ t = self.peek()
279
+ if t.type == "EQUALS":
280
+ self.advance()
281
+ values = self.parse_comma_values()
282
+ return Directive(directive, values, line, 0)
283
+ if t.type == "BLOCK_OPEN":
284
+ self.advance()
285
+ body = self.parse_block(stop="BLOCK_CLOSE")
286
+ self.expect("BLOCK_CLOSE")
287
+ return Section(directive, body, line)
288
+ raise ParserError(f"Expected = or {{ after {directive!r}", t)
289
+
290
+ def parse_bind(self, directive: str, line: int) -> BindDirective:
291
+ flags = _parse_combined_bind(directive)
292
+ self.expect("EQUALS")
293
+ self.skip_newlines()
294
+ values = self.parse_comma_values()
295
+ if len(values) < 3:
296
+ raise ParserError(f"bind needs at least 3 arguments (mods, key, dispatcher), got {len(values)}", Token("IDENT", directive, line, 0))
297
+ mods_str = values[0].strip()
298
+ key = values[1].strip()
299
+ dispatcher = values[2].strip()
300
+ if dispatcher in ("exec", "execr") and len(values) > 3:
301
+ cmd = ",".join(v.strip() for v in values[3:]).strip()
302
+ params = [cmd] if cmd else []
303
+ else:
304
+ params = [v.strip() for v in values[3:]]
305
+ mods = [m.strip() for m in mods_str.replace(",", " ").split()] if mods_str else []
306
+ return BindDirective(mods, key, dispatcher, params, flags, line)
307
+
308
+ def parse_monitor(self, line: int) -> MonitorDirective:
309
+ self.expect("EQUALS")
310
+ values = self.parse_comma_values()
311
+ name = values[0].strip() if len(values) > 0 else ""
312
+ mode = values[1].strip() if len(values) > 1 else "preferred"
313
+ position = values[2].strip() if len(values) > 2 else "auto"
314
+ scale = values[3].strip() if len(values) > 3 else "1"
315
+ extra = {}
316
+ for i in range(4, len(values), 2):
317
+ key = values[i].strip()
318
+ val = values[i + 1].strip() if i + 1 < len(values) else "true"
319
+ extra[key] = val
320
+ return MonitorDirective(name, mode, position, scale, line, extra)
321
+
322
+ def _parse_rule_block_body(self) -> tuple:
323
+ """Parse windowrule/layerrule block content { ... }.
324
+ Returns (name, match dict, effects dict)."""
325
+ name = ""
326
+ match: Dict[str, str] = {}
327
+ effects: Dict[str, List[str]] = {}
328
+
329
+ self.skip_newlines()
330
+ self.expect("BLOCK_OPEN")
331
+ self.skip_newlines()
332
+
333
+ while self.peek().type not in ("BLOCK_CLOSE", "EOF"):
334
+ t = self.peek()
335
+ if t.type == "COMMENT":
336
+ self.advance()
337
+ elif t.type == "IDENT":
338
+ key_parts = [self.advance().value]
339
+ while self.peek().type == "COLON":
340
+ self.advance()
341
+ if self.peek().type == "IDENT":
342
+ key_parts.append(self.advance().value)
343
+ else:
344
+ break
345
+ key = ":".join(key_parts)
346
+
347
+ self.expect("EQUALS")
348
+ val = self.parse_value_rest()
349
+
350
+ if key == "name":
351
+ name = val
352
+ elif key.startswith("match:"):
353
+ match[key[len("match:"):]] = val
354
+ else:
355
+ effects[key] = [val]
356
+ else:
357
+ self.advance()
358
+ self.skip_newlines()
359
+
360
+ self.expect("BLOCK_CLOSE")
361
+ return name, match, effects
362
+
363
+ def parse_windowrule(self, is_v2: bool, line: int) -> Union[WindowRule, WindowRuleBlock]:
364
+ if self.peek().type == "BLOCK_OPEN":
365
+ name, match, effects = self._parse_rule_block_body()
366
+ return WindowRuleBlock(is_v2, name, match, effects, line)
367
+ self.expect("EQUALS")
368
+ values = self.parse_comma_values()
369
+ rule = values[0].strip() if values else ""
370
+ match_params = [v.strip() for v in values[1:]]
371
+ return WindowRule(is_v2, rule, match_params, line)
372
+
373
+ def parse_exec(self, directive: str, line: int) -> ExecDirective:
374
+ self.expect("EQUALS")
375
+ command = self.parse_line_rest()
376
+ return ExecDirective(directive, command, line)
377
+
378
+ def parse_line_rest(self) -> str:
379
+ parts = []
380
+ while self.peek().type not in ("NEWLINE", "EOF", "COMMENT", "BLOCK_CLOSE"):
381
+ if self.peek().type == "DOLLAR":
382
+ self.advance()
383
+ var_t = self.expect("IDENT")
384
+ parts.append("$" + var_t.value)
385
+ else:
386
+ t = self.advance()
387
+ parts.append(t.value)
388
+ return self._join_tokens(parts)
389
+
390
+ def parse_animation(self, line: int) -> AnimationDirective:
391
+ self.expect("EQUALS")
392
+ values = self.parse_comma_values()
393
+ name = values[0].strip() if len(values) > 0 else ""
394
+ style = values[1].strip() if len(values) > 1 else ""
395
+ speed = values[2].strip() if len(values) > 2 else "1"
396
+ curve = values[3].strip() if len(values) > 3 else "default"
397
+ return AnimationDirective(name, style, speed, curve, line)
398
+
399
+ def parse_bezier(self, line: int) -> BezierDirective:
400
+ self.expect("EQUALS")
401
+ values = self.parse_comma_values()
402
+ name = values[0].strip() if len(values) > 0 else ""
403
+ p1x = values[1].strip() if len(values) > 1 else "0"
404
+ p1y = values[2].strip() if len(values) > 2 else "0"
405
+ p2x = values[3].strip() if len(values) > 3 else "1"
406
+ p2y = values[4].strip() if len(values) > 4 else "1"
407
+ return BezierDirective(name, p1x, p1y, p2x, p2y, line)
408
+
409
+ def parse_env(self, line: int) -> EnvDirective:
410
+ self.expect("EQUALS")
411
+ name_t = self.expect("IDENT")
412
+ name = name_t.value.strip()
413
+ val = ""
414
+ if self.peek().type == "COMMA":
415
+ self.advance()
416
+ val = self.parse_line_rest()
417
+ return EnvDirective(name, val, line)
418
+
419
+ def parse_source(self, line: int) -> SourceDirective:
420
+ self.expect("EQUALS")
421
+ values = self.parse_comma_values()
422
+ path = values[0].strip() if values else ""
423
+ return SourceDirective(path, line)
424
+
425
+ def parse_gesture(self, line: int) -> GestureDirective:
426
+ self.skip_newlines()
427
+ self.expect("BLOCK_OPEN")
428
+ body = []
429
+ self.skip_newlines()
430
+ while self.peek().type not in ("BLOCK_CLOSE", "EOF"):
431
+ t = self.peek()
432
+ if t.type == "COMMENT":
433
+ body.append(Comment(t.value, t.line))
434
+ self.advance()
435
+ elif t.type == "IDENT":
436
+ key_t = self.advance()
437
+ self.expect("EQUALS")
438
+ val = self.parse_value_rest()
439
+ body.append(Directive(key_t.value, [val], key_t.line, 0))
440
+ else:
441
+ self.advance()
442
+ self.skip_newlines()
443
+ self.expect("BLOCK_CLOSE")
444
+ return GestureDirective(body, line)
445
+
446
+ def parse_device(self, name: str, line: int) -> DeviceSection:
447
+ self.skip_newlines()
448
+ self.expect("BLOCK_OPEN")
449
+ body = []
450
+ self.skip_newlines()
451
+ while self.peek().type not in ("BLOCK_CLOSE", "EOF"):
452
+ t = self.peek()
453
+ if t.type == "COMMENT":
454
+ body.append(Comment(t.value, t.line))
455
+ self.advance()
456
+ elif t.type == "IDENT":
457
+ key_t = self.advance()
458
+ self.expect("EQUALS")
459
+ val = self.parse_value_rest()
460
+ body.append(Directive(key_t.value, [val], key_t.line, 0))
461
+ else:
462
+ self.advance()
463
+ self.skip_newlines()
464
+ self.expect("BLOCK_CLOSE")
465
+ return DeviceSection(name, body, line)
466
+
467
+ def parse_workspace(self, line: int) -> WorkspaceDirective:
468
+ self.expect("EQUALS")
469
+ values = self.parse_comma_values()
470
+ name = values[0].strip() if values else ""
471
+ params = [v.strip() for v in values[1:]]
472
+ return WorkspaceDirective(name, params, line)
473
+
474
+ def parse_layerrule(self, line: int) -> Union[LayerRuleDirective, LayerRuleBlock]:
475
+ if self.peek().type == "BLOCK_OPEN":
476
+ name, match, effects = self._parse_rule_block_body()
477
+ return LayerRuleBlock(name, match, effects, line)
478
+ self.expect("EQUALS")
479
+ values = self.parse_comma_values()
480
+ rule = values[0].strip() if values else ""
481
+ namespace = values[1].strip() if len(values) > 1 else ""
482
+ return LayerRuleDirective(rule, namespace, line)
483
+
484
+ def parse_submap(self, line: int) -> Directive:
485
+ self.expect("EQUALS")
486
+ values = self.parse_comma_values()
487
+ return Directive("submap", values, line, 0)
488
+
489
+
490
+ def parse_config(source: str) -> ConfigFile:
491
+ from hyprconf2lua.lexer import tokenize
492
+ tokens = tokenize(source)
493
+ parser = Parser(tokens)
494
+ return parser.parse()