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/__init__.py +1 -0
- hyprconf2lua/__main__.py +4 -0
- hyprconf2lua/ast.py +171 -0
- hyprconf2lua/cli.py +173 -0
- hyprconf2lua/codegen.py +931 -0
- hyprconf2lua/converter.py +60 -0
- hyprconf2lua/lexer.py +74 -0
- hyprconf2lua/mappings.py +130 -0
- hyprconf2lua/parser.py +494 -0
- hyprconf2lua-1.2.0.dist-info/METADATA +242 -0
- hyprconf2lua-1.2.0.dist-info/RECORD +15 -0
- hyprconf2lua-1.2.0.dist-info/WHEEL +5 -0
- hyprconf2lua-1.2.0.dist-info/entry_points.txt +2 -0
- hyprconf2lua-1.2.0.dist-info/licenses/LICENSE +21 -0
- hyprconf2lua-1.2.0.dist-info/top_level.txt +1 -0
hyprconf2lua/codegen.py
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from hyprconf2lua.ast import *
|
|
6
|
+
from hyprconf2lua.mappings import *
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
KNOWN_SECTIONS = {
|
|
10
|
+
"general", "decoration", "input", "animations", "gestures",
|
|
11
|
+
"misc", "binds", "cursor", "debug", "dwindle", "master",
|
|
12
|
+
"group", "render", "xwayland", "opengl", "ecosystem",
|
|
13
|
+
"experimental", "layout", "scrolling", "quirks", "blur",
|
|
14
|
+
"shadow", "touchpad", "tablet", "windowing", "startup",
|
|
15
|
+
"autostart", "window", "monitor", "workspace", "layer",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Codegen:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.lines: List[str] = []
|
|
22
|
+
self.indent_level = 0
|
|
23
|
+
self.variables: Dict[str, str] = {}
|
|
24
|
+
self.has_exec = False
|
|
25
|
+
self.has_animation = False
|
|
26
|
+
self.has_bezier = False
|
|
27
|
+
self.has_bind = False
|
|
28
|
+
self.main_mod_var: Optional[str] = None
|
|
29
|
+
self.pending_execs: List[str] = []
|
|
30
|
+
self.pending_exec_ones: List[str] = []
|
|
31
|
+
self.pending_exec_shutdowns: List[str] = []
|
|
32
|
+
self.flag_count = 0
|
|
33
|
+
self.translated_count = 0
|
|
34
|
+
self.passthrough_count = 0
|
|
35
|
+
|
|
36
|
+
def indent(self):
|
|
37
|
+
self.indent_level += 1
|
|
38
|
+
|
|
39
|
+
def dedent(self):
|
|
40
|
+
self.indent_level = max(0, self.indent_level - 1)
|
|
41
|
+
|
|
42
|
+
def emit(self, line: str = ""):
|
|
43
|
+
if line:
|
|
44
|
+
self.lines.append(" " * self.indent_level + line)
|
|
45
|
+
else:
|
|
46
|
+
self.lines.append("")
|
|
47
|
+
|
|
48
|
+
def emit_block(self, lines: List[str]):
|
|
49
|
+
for line in lines:
|
|
50
|
+
self.emit(line)
|
|
51
|
+
|
|
52
|
+
def generate(self, config: ConfigFile) -> str:
|
|
53
|
+
self.lines = []
|
|
54
|
+
self.emit("-- Generated by hyprconf2lua v1.2.0")
|
|
55
|
+
self.emit("-- https://github.com/yourusername/hyprconf2lua")
|
|
56
|
+
self.emit("-- Manual review may be needed for complex directives")
|
|
57
|
+
self.emit("")
|
|
58
|
+
self.emit('---@module \'hl\'')
|
|
59
|
+
self.emit("")
|
|
60
|
+
|
|
61
|
+
for stmt in config.body:
|
|
62
|
+
self.visit(stmt)
|
|
63
|
+
self.emit()
|
|
64
|
+
|
|
65
|
+
self.emit_pending_execs()
|
|
66
|
+
self.finalize_vars()
|
|
67
|
+
|
|
68
|
+
return "\n".join(self.lines)
|
|
69
|
+
|
|
70
|
+
def visit(self, stmt: BlockStmt):
|
|
71
|
+
if isinstance(stmt, Comment):
|
|
72
|
+
self.emit("--" + stmt.text[1:])
|
|
73
|
+
elif isinstance(stmt, VariableDef):
|
|
74
|
+
self.visit_variable(stmt)
|
|
75
|
+
elif isinstance(stmt, Directive):
|
|
76
|
+
self.visit_directive(stmt)
|
|
77
|
+
elif isinstance(stmt, Section):
|
|
78
|
+
self.visit_section(stmt)
|
|
79
|
+
elif isinstance(stmt, BindDirective):
|
|
80
|
+
self.visit_bind(stmt)
|
|
81
|
+
elif isinstance(stmt, MonitorDirective):
|
|
82
|
+
self.visit_monitor(stmt)
|
|
83
|
+
elif isinstance(stmt, WindowRule):
|
|
84
|
+
self.visit_windowrule(stmt)
|
|
85
|
+
elif isinstance(stmt, WindowRuleBlock):
|
|
86
|
+
self.visit_windowrule_block(stmt)
|
|
87
|
+
elif isinstance(stmt, ExecDirective):
|
|
88
|
+
self.visit_exec(stmt)
|
|
89
|
+
elif isinstance(stmt, AnimationDirective):
|
|
90
|
+
self.visit_animation(stmt)
|
|
91
|
+
elif isinstance(stmt, BezierDirective):
|
|
92
|
+
self.visit_bezier(stmt)
|
|
93
|
+
elif isinstance(stmt, EnvDirective):
|
|
94
|
+
self.visit_env(stmt)
|
|
95
|
+
elif isinstance(stmt, SourceDirective):
|
|
96
|
+
self.visit_source(stmt)
|
|
97
|
+
elif isinstance(stmt, DeviceSection):
|
|
98
|
+
self.visit_device(stmt)
|
|
99
|
+
elif isinstance(stmt, GestureDirective):
|
|
100
|
+
self.visit_gesture(stmt)
|
|
101
|
+
elif isinstance(stmt, WorkspaceDirective):
|
|
102
|
+
self.visit_workspace(stmt)
|
|
103
|
+
elif isinstance(stmt, SubmapDef):
|
|
104
|
+
self.visit_submap(stmt)
|
|
105
|
+
elif isinstance(stmt, LayerRuleDirective):
|
|
106
|
+
self.visit_layerrule(stmt)
|
|
107
|
+
elif isinstance(stmt, LayerRuleBlock):
|
|
108
|
+
self.visit_layerrule_block(stmt)
|
|
109
|
+
else:
|
|
110
|
+
self.flag_count += 1
|
|
111
|
+
self.emit(f"-- TODO: manual review (unknown statement type: {type(stmt).__name__})")
|
|
112
|
+
|
|
113
|
+
def resolve_val(self, val: str) -> str:
|
|
114
|
+
def _repl(m: re.Match) -> str:
|
|
115
|
+
var_name = m.group(1)
|
|
116
|
+
if var_name in self.variables:
|
|
117
|
+
return self.variables[var_name]
|
|
118
|
+
return "local_var_" + var_name
|
|
119
|
+
return re.sub(r'\$(\w+)', _repl, val)
|
|
120
|
+
|
|
121
|
+
def needs_quotes(self, val: str) -> bool:
|
|
122
|
+
if not val:
|
|
123
|
+
return True
|
|
124
|
+
if val.startswith('"') and val.endswith('"'):
|
|
125
|
+
return False
|
|
126
|
+
if re.match(r'^-?\d+(\.\d+)?$', val):
|
|
127
|
+
return False
|
|
128
|
+
if val in ("true", "false", "nil"):
|
|
129
|
+
return False
|
|
130
|
+
if val in self.variables or val in ("mainMod", "terminal", "fileManager", "menu"):
|
|
131
|
+
return False
|
|
132
|
+
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', val) and val in self.variables:
|
|
133
|
+
return False
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def quote(self, val: str) -> str:
|
|
137
|
+
if self.needs_quotes(val):
|
|
138
|
+
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
|
|
139
|
+
return f'"{escaped}"'
|
|
140
|
+
return val
|
|
141
|
+
|
|
142
|
+
def to_lua_val(self, val: str):
|
|
143
|
+
val = self.resolve_val(val)
|
|
144
|
+
if val.startswith("local_var_"):
|
|
145
|
+
return val[len("local_var_"):]
|
|
146
|
+
if val.lower() in ("true", "on", "yes"):
|
|
147
|
+
return "true"
|
|
148
|
+
if val.lower() in ("false", "off", "no"):
|
|
149
|
+
return "false"
|
|
150
|
+
try:
|
|
151
|
+
int(val)
|
|
152
|
+
return val
|
|
153
|
+
except ValueError:
|
|
154
|
+
pass
|
|
155
|
+
try:
|
|
156
|
+
float(val)
|
|
157
|
+
return val
|
|
158
|
+
except ValueError:
|
|
159
|
+
pass
|
|
160
|
+
return self.quote(val)
|
|
161
|
+
|
|
162
|
+
def visit_variable(self, stmt: VariableDef):
|
|
163
|
+
val = self.resolve_val(stmt.value)
|
|
164
|
+
self.variables[stmt.name] = val
|
|
165
|
+
self.translated_count += 1
|
|
166
|
+
if stmt.name.upper() == "MAINMOD" or stmt.name == "mainMod":
|
|
167
|
+
self.main_mod_var = stmt.name
|
|
168
|
+
self.emit(f'local {stmt.name} = "{val}"')
|
|
169
|
+
else:
|
|
170
|
+
if self.needs_quotes(val):
|
|
171
|
+
self.emit(f'local {stmt.name} = {self.quote(val)}')
|
|
172
|
+
else:
|
|
173
|
+
self.emit(f'local {stmt.name} = {val}')
|
|
174
|
+
|
|
175
|
+
def finalize_vars(self):
|
|
176
|
+
if self.main_mod_var is None:
|
|
177
|
+
for name, val in self.variables.items():
|
|
178
|
+
if val == "SUPER" or val == "ALT" or val == "CTRL" or val == "SHIFT":
|
|
179
|
+
self.main_mod_var = name
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
def emit_pending_execs(self):
|
|
183
|
+
if self.pending_exec_ones:
|
|
184
|
+
self.emit("-- Autostart")
|
|
185
|
+
self.emit('hl.on("hyprland.start", function()')
|
|
186
|
+
self.indent()
|
|
187
|
+
for cmd in self.pending_exec_ones:
|
|
188
|
+
resolved = self.resolve_val(cmd)
|
|
189
|
+
if resolved.startswith("$") and resolved[1:] in self.variables:
|
|
190
|
+
self.emit(f"hl.exec_cmd({resolved[1:]})")
|
|
191
|
+
else:
|
|
192
|
+
self.emit(f"hl.exec_cmd({self.quote(resolved)})")
|
|
193
|
+
self.dedent()
|
|
194
|
+
self.emit("end)")
|
|
195
|
+
self.emit("")
|
|
196
|
+
|
|
197
|
+
if self.pending_execs:
|
|
198
|
+
self.emit("-- Exec (run every reload)")
|
|
199
|
+
self.emit('hl.on("config.reloaded", function()')
|
|
200
|
+
self.indent()
|
|
201
|
+
for cmd in self.pending_execs:
|
|
202
|
+
resolved = self.resolve_val(cmd)
|
|
203
|
+
if resolved.startswith("$") and resolved[1:] in self.variables:
|
|
204
|
+
self.emit(f"hl.exec_cmd({resolved[1:]})")
|
|
205
|
+
else:
|
|
206
|
+
self.emit(f"hl.exec_cmd({self.quote(resolved)})")
|
|
207
|
+
self.dedent()
|
|
208
|
+
self.emit("end)")
|
|
209
|
+
self.emit("")
|
|
210
|
+
|
|
211
|
+
if self.pending_exec_shutdowns:
|
|
212
|
+
self.emit("-- Shutdown")
|
|
213
|
+
self.emit('hl.on("hyprland.shutdown", function()')
|
|
214
|
+
self.indent()
|
|
215
|
+
for cmd in self.pending_exec_shutdowns:
|
|
216
|
+
resolved = self.resolve_val(cmd)
|
|
217
|
+
if resolved.startswith("$") and resolved[1:] in self.variables:
|
|
218
|
+
self.emit(f"hl.exec_cmd({resolved[1:]})")
|
|
219
|
+
else:
|
|
220
|
+
self.emit(f"hl.exec_cmd({self.quote(resolved)})")
|
|
221
|
+
self.dedent()
|
|
222
|
+
self.emit("end)")
|
|
223
|
+
self.emit("")
|
|
224
|
+
|
|
225
|
+
def emit_section_config(self, section_name: str, directives: List[BlockStmt]):
|
|
226
|
+
if not directives:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
self.emit(f"hl.config({{")
|
|
230
|
+
self.indent()
|
|
231
|
+
self.emit(f"{section_name} = {{")
|
|
232
|
+
self.indent()
|
|
233
|
+
|
|
234
|
+
for d in directives:
|
|
235
|
+
if isinstance(d, Comment):
|
|
236
|
+
self.emit(f"--{d.text[1:]}")
|
|
237
|
+
elif isinstance(d, Section):
|
|
238
|
+
sub_name = d.name
|
|
239
|
+
self.emit(f"{sub_name} = {{")
|
|
240
|
+
self.indent()
|
|
241
|
+
for sd in d.body:
|
|
242
|
+
if isinstance(sd, Comment):
|
|
243
|
+
self.emit(f"--{sd.text[1:]}")
|
|
244
|
+
elif isinstance(sd, Directive):
|
|
245
|
+
self.translated_count += 1
|
|
246
|
+
key = sd.key
|
|
247
|
+
if len(sd.value) == 1:
|
|
248
|
+
val = self.to_lua_val(sd.value[0])
|
|
249
|
+
self.emit(f"{key} = {val},")
|
|
250
|
+
else:
|
|
251
|
+
vals = [self.to_lua_val(v) for v in sd.value]
|
|
252
|
+
self.emit(f"{key} = {{ {', '.join(vals)} }},")
|
|
253
|
+
elif isinstance(sd, Section):
|
|
254
|
+
self.passthrough_count += 1
|
|
255
|
+
self.emit(f"-- Nested subsection {sd.name}:")
|
|
256
|
+
for ssd in sd.body:
|
|
257
|
+
if isinstance(ssd, Directive):
|
|
258
|
+
k = ssd.key
|
|
259
|
+
vv = self.to_lua_val(ssd.value[0]) if ssd.value else "true"
|
|
260
|
+
self.emit(f"{k} = {vv},")
|
|
261
|
+
self.dedent()
|
|
262
|
+
self.emit("},")
|
|
263
|
+
elif isinstance(d, Directive):
|
|
264
|
+
self.translated_count += 1
|
|
265
|
+
key = d.key
|
|
266
|
+
if len(d.value) == 1:
|
|
267
|
+
val = self.to_lua_val(d.value[0])
|
|
268
|
+
self.emit(f"{key} = {val},")
|
|
269
|
+
else:
|
|
270
|
+
vals = [self.to_lua_val(v) for v in d.value]
|
|
271
|
+
self.emit(f"{key} = {{ {', '.join(vals)} }},")
|
|
272
|
+
|
|
273
|
+
self.dedent()
|
|
274
|
+
self.emit("},")
|
|
275
|
+
self.dedent()
|
|
276
|
+
self.emit("})")
|
|
277
|
+
|
|
278
|
+
def flush_subsection(self, name: str, directives: List[Directive]):
|
|
279
|
+
if not directives:
|
|
280
|
+
return
|
|
281
|
+
self.emit(f"{name} = {{")
|
|
282
|
+
self.indent()
|
|
283
|
+
for d in directives:
|
|
284
|
+
key = d.key
|
|
285
|
+
if len(d.value) == 1:
|
|
286
|
+
val = self.to_lua_val(d.value[0])
|
|
287
|
+
self.emit(f"{key} = {val},")
|
|
288
|
+
else:
|
|
289
|
+
vals = [self.to_lua_val(v) for v in d.value]
|
|
290
|
+
self.emit(f"{key} = {{ {', '.join(vals)} }},")
|
|
291
|
+
self.dedent()
|
|
292
|
+
self.emit("},")
|
|
293
|
+
|
|
294
|
+
def visit_section(self, stmt: Section):
|
|
295
|
+
if stmt.name == "plugin":
|
|
296
|
+
self.translated_count += 1
|
|
297
|
+
has_plugins = False
|
|
298
|
+
for child in stmt.body:
|
|
299
|
+
if isinstance(child, Section):
|
|
300
|
+
has_plugins = True
|
|
301
|
+
plugin_name = child.name
|
|
302
|
+
self.emit(f"hl.plugin({self.quote(plugin_name)}, function()")
|
|
303
|
+
self.indent()
|
|
304
|
+
for sd in child.body:
|
|
305
|
+
if isinstance(sd, Comment):
|
|
306
|
+
self.emit(f"--{sd.text[1:]}")
|
|
307
|
+
elif isinstance(sd, Directive):
|
|
308
|
+
k = sd.key
|
|
309
|
+
if len(sd.value) == 1:
|
|
310
|
+
v = self.to_lua_val(sd.value[0])
|
|
311
|
+
self.emit(f"{k} = {v},")
|
|
312
|
+
else:
|
|
313
|
+
vals = [self.to_lua_val(v) for v in sd.value]
|
|
314
|
+
self.emit(f"{k} = {{ {', '.join(vals)} }},")
|
|
315
|
+
self.dedent()
|
|
316
|
+
self.emit("end)")
|
|
317
|
+
elif isinstance(child, Directive):
|
|
318
|
+
has_plugins = True
|
|
319
|
+
name = child.key
|
|
320
|
+
vals = ", ".join(self.quote(self.resolve_val(v)) for v in child.value)
|
|
321
|
+
self.emit(f"hl.plugin({self.quote(name)}, {{{vals}}})")
|
|
322
|
+
if not has_plugins:
|
|
323
|
+
self.emit("-- TODO: manual review (plugin config)")
|
|
324
|
+
return
|
|
325
|
+
if stmt.name in KNOWN_SECTIONS:
|
|
326
|
+
self.emit_section_config(stmt.name, stmt.body)
|
|
327
|
+
else:
|
|
328
|
+
self.translated_count += 1
|
|
329
|
+
self.emit(f"hl.config({{")
|
|
330
|
+
self.indent()
|
|
331
|
+
self.emit(f"{stmt.name} = {{")
|
|
332
|
+
self.indent()
|
|
333
|
+
for child in stmt.body:
|
|
334
|
+
if isinstance(child, Comment):
|
|
335
|
+
self.emit(f"--{child.text[1:]}")
|
|
336
|
+
elif isinstance(child, Directive):
|
|
337
|
+
k = child.key
|
|
338
|
+
if len(child.value) == 1:
|
|
339
|
+
v = self.to_lua_val(child.value[0])
|
|
340
|
+
self.emit(f"{k} = {v},")
|
|
341
|
+
else:
|
|
342
|
+
vals = [self.to_lua_val(v) for v in child.value]
|
|
343
|
+
self.emit(f"{k} = {{ {', '.join(vals)} }},")
|
|
344
|
+
self.dedent()
|
|
345
|
+
self.emit("},")
|
|
346
|
+
self.dedent()
|
|
347
|
+
self.emit("})")
|
|
348
|
+
self.emit(f"-- NOTE: Section '{stmt.name}' may be a plugin or custom section; verify the output")
|
|
349
|
+
|
|
350
|
+
def visit_directive(self, stmt: Directive):
|
|
351
|
+
if stmt.key in ("submap", "submap_reset"):
|
|
352
|
+
return
|
|
353
|
+
if stmt.key == "unbind":
|
|
354
|
+
self.translated_count += 1
|
|
355
|
+
mods_str = stmt.value[0].strip() if stmt.value else ""
|
|
356
|
+
key_str = stmt.value[1].strip() if len(stmt.value) > 1 else ""
|
|
357
|
+
mods = [m.strip() for m in mods_str.replace(",", " ").split()] if mods_str else []
|
|
358
|
+
combo_expr = self.build_unbind_combo(mods, key_str)
|
|
359
|
+
self.emit(f"hl.unbind({combo_expr})")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
self.passthrough_count += 1
|
|
363
|
+
vals = ", ".join(self.quote(self.resolve_val(v)) for v in stmt.value)
|
|
364
|
+
self.emit(f"-- TODO: manual review: {stmt.key} = {vals}")
|
|
365
|
+
|
|
366
|
+
def build_unbind_combo(self, mods: List[str], key: str) -> str:
|
|
367
|
+
if not mods:
|
|
368
|
+
return self.quote(key) if key else '""'
|
|
369
|
+
parts = [self.quote(m) for m in mods]
|
|
370
|
+
all_str = " + ".join(p.strip('"') for p in parts)
|
|
371
|
+
return self.quote(all_str) + ' .. " + " .. ' + (self.quote(key) if key else '""')
|
|
372
|
+
|
|
373
|
+
def visit_bind(self, stmt: BindDirective):
|
|
374
|
+
self.translated_count += 1
|
|
375
|
+
self.has_bind = True
|
|
376
|
+
|
|
377
|
+
combo_expr = self.build_combo_expr(stmt.mods, stmt.key)
|
|
378
|
+
|
|
379
|
+
dispatcher = stmt.dispatcher
|
|
380
|
+
params = stmt.params
|
|
381
|
+
|
|
382
|
+
if stmt.key.startswith("mouse:") and not params:
|
|
383
|
+
if dispatcher == "movewindow":
|
|
384
|
+
dsp_code = "hl.dsp.window.drag()"
|
|
385
|
+
elif dispatcher == "resizewindow":
|
|
386
|
+
dsp_code = "hl.dsp.window.resize()"
|
|
387
|
+
else:
|
|
388
|
+
dsp_code = self.build_dispatcher(dispatcher, params)
|
|
389
|
+
else:
|
|
390
|
+
dsp_code = self.build_dispatcher(dispatcher, params)
|
|
391
|
+
if dsp_code is None and params:
|
|
392
|
+
first_param = params[0]
|
|
393
|
+
rest = params[1:]
|
|
394
|
+
dsp_code = self.build_dispatcher(first_param, rest)
|
|
395
|
+
if dsp_code is not None:
|
|
396
|
+
dispatcher = first_param
|
|
397
|
+
params = rest
|
|
398
|
+
|
|
399
|
+
if dsp_code is None:
|
|
400
|
+
self.flag_count += 1
|
|
401
|
+
mods_str = " + ".join(stmt.mods) if stmt.mods else ""
|
|
402
|
+
self.emit(f'-- TODO: manual review (unknown dispatcher: {stmt.dispatcher})')
|
|
403
|
+
pstr = ", ".join(self.quote(self.resolve_val(p)) for p in stmt.params)
|
|
404
|
+
self.emit(f'-- hl.bind("{mods_str} + {stmt.key}", hl.dsp.{stmt.dispatcher}({pstr}))')
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
opt_parts = []
|
|
408
|
+
if stmt.flags:
|
|
409
|
+
for f in stmt.flags:
|
|
410
|
+
if f in BIND_FLAGS_TO_OPTIONS:
|
|
411
|
+
opt_key, opt_val = BIND_FLAGS_TO_OPTIONS[f]
|
|
412
|
+
opt_parts.append(f"{opt_key} = {opt_val}")
|
|
413
|
+
|
|
414
|
+
if opt_parts:
|
|
415
|
+
opts = ", { " + ", ".join(opt_parts) + " }"
|
|
416
|
+
else:
|
|
417
|
+
opts = ""
|
|
418
|
+
|
|
419
|
+
self.emit(f"hl.bind({combo_expr}, {dsp_code}{opts})")
|
|
420
|
+
|
|
421
|
+
def build_combo_expr(self, mods: List[str], key: str) -> str:
|
|
422
|
+
parts = []
|
|
423
|
+
|
|
424
|
+
is_key_var = key.startswith("$")
|
|
425
|
+
key_resolved = self.resolve_val(key) if is_key_var else None
|
|
426
|
+
key_str = key_resolved if is_key_var else self.quote(key)
|
|
427
|
+
|
|
428
|
+
for m in mods:
|
|
429
|
+
is_var = m.startswith("$")
|
|
430
|
+
if is_var:
|
|
431
|
+
var_name = m[1:]
|
|
432
|
+
if var_name in self.variables:
|
|
433
|
+
parts.append(var_name)
|
|
434
|
+
else:
|
|
435
|
+
parts.append(self.quote(m))
|
|
436
|
+
else:
|
|
437
|
+
parts.append(self.quote(m))
|
|
438
|
+
|
|
439
|
+
if not parts:
|
|
440
|
+
return key_str
|
|
441
|
+
|
|
442
|
+
has_var = any(not p.startswith('"') for p in parts)
|
|
443
|
+
|
|
444
|
+
if not has_var:
|
|
445
|
+
all_str = " + ".join(p.strip('"') for p in parts)
|
|
446
|
+
return self.quote(all_str) + ' .. " + " .. ' + key_str
|
|
447
|
+
|
|
448
|
+
segments = []
|
|
449
|
+
current_str = None
|
|
450
|
+
for p in parts:
|
|
451
|
+
if p.startswith('"'):
|
|
452
|
+
lit = p.strip('"')
|
|
453
|
+
if current_str is None:
|
|
454
|
+
current_str = lit
|
|
455
|
+
else:
|
|
456
|
+
current_str += " + " + lit
|
|
457
|
+
else:
|
|
458
|
+
if current_str is not None:
|
|
459
|
+
segments.append(self.quote(current_str))
|
|
460
|
+
current_str = None
|
|
461
|
+
segments.append(p)
|
|
462
|
+
if current_str is not None:
|
|
463
|
+
segments.append(self.quote(current_str))
|
|
464
|
+
|
|
465
|
+
separator = ' .. " + " .. '
|
|
466
|
+
result = separator.join(segments) + separator + key_str
|
|
467
|
+
return result
|
|
468
|
+
|
|
469
|
+
def build_dispatcher(self, dispatcher: str, params: List[str]) -> Optional[str]:
|
|
470
|
+
if dispatcher in DISPATCHER_MAP:
|
|
471
|
+
func, needs_args = DISPATCHER_MAP[dispatcher]
|
|
472
|
+
|
|
473
|
+
if dispatcher == "movetoworkspacesilent":
|
|
474
|
+
args = self.build_dispatcher_args(params, needs_args)
|
|
475
|
+
return f'{func}({args}, {{ follow = false }})'
|
|
476
|
+
|
|
477
|
+
if dispatcher == "movefocus":
|
|
478
|
+
dir_map = {"l": "left", "r": "right", "u": "up", "d": "down"}
|
|
479
|
+
if params and params[0] in dir_map:
|
|
480
|
+
return f'{func}({{ direction = "{dir_map[params[0]]}" }})'
|
|
481
|
+
return f'{func}({{ direction = {self.quote(params[0]) if params else "nil"} }})'
|
|
482
|
+
|
|
483
|
+
if dispatcher == "cyclenext":
|
|
484
|
+
if params and params[0].lower() == "prev":
|
|
485
|
+
return f'{func}({{ next = false }})'
|
|
486
|
+
return f'{func}()'
|
|
487
|
+
|
|
488
|
+
if dispatcher == "movewindow":
|
|
489
|
+
if params and params[0].startswith("into_group:"):
|
|
490
|
+
return f'{func}({{ into_group = {self.quote(params[0][len("into_group:"):])} }})'
|
|
491
|
+
if params and params[0] == "out_of_group":
|
|
492
|
+
return f'{func}({{ out_of_group = true }})'
|
|
493
|
+
return self.build_dispatcher_args(params, needs_args, func)
|
|
494
|
+
|
|
495
|
+
if dispatcher == "swapwindow":
|
|
496
|
+
if params and params[0] in ("l", "r", "u", "d"):
|
|
497
|
+
dir_map = {"l": "left", "r": "right", "u": "up", "d": "down"}
|
|
498
|
+
return f'{func}({{ direction = "{dir_map[params[0]]}" }})'
|
|
499
|
+
if params and params[0].startswith("class:"):
|
|
500
|
+
return f'{func}({{ target = {self.quote(params[0])} }})'
|
|
501
|
+
return self.build_dispatcher_args(params, needs_args, func)
|
|
502
|
+
|
|
503
|
+
if dispatcher == "changegroupactive":
|
|
504
|
+
if params and params[0].lower() == "f":
|
|
505
|
+
return f'{func}()'
|
|
506
|
+
return f'{func}({{ forward = false }})'
|
|
507
|
+
|
|
508
|
+
if dispatcher == "movegroupwindow":
|
|
509
|
+
if params and params[0].lower() == "b":
|
|
510
|
+
return f'{func}({{ forward = false }})'
|
|
511
|
+
return f'{func}({{ forward = true }})'
|
|
512
|
+
|
|
513
|
+
if dispatcher == "workspace":
|
|
514
|
+
return self.build_workspace_dispatcher(func, params)
|
|
515
|
+
|
|
516
|
+
if dispatcher == "focuswindow":
|
|
517
|
+
return self.build_focus_dispatcher(func, params)
|
|
518
|
+
|
|
519
|
+
if dispatcher == "focusurgentorlast":
|
|
520
|
+
return f'{func}({{ urgent = true }})'
|
|
521
|
+
|
|
522
|
+
if dispatcher == "focuscurrentorlast":
|
|
523
|
+
return f'{func}({{ last = true }})'
|
|
524
|
+
|
|
525
|
+
if dispatcher == "focusonemonitor":
|
|
526
|
+
return f'{func}({{ on_monitor = true }})'
|
|
527
|
+
|
|
528
|
+
if dispatcher == "mouse":
|
|
529
|
+
mouse_actions = {"272": "drag", "273": "resize"}
|
|
530
|
+
if params and params[0] in mouse_actions:
|
|
531
|
+
return f'hl.dsp.window.{mouse_actions[params[0]]}()'
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
if dispatcher in ("exec", "execr"):
|
|
535
|
+
resolved = self.resolve_val(params[0]) if params else ""
|
|
536
|
+
return f'{func}({self.quote(resolved)})'
|
|
537
|
+
|
|
538
|
+
args = self.build_dispatcher_args(params, needs_args)
|
|
539
|
+
return f'{func}({args})' if needs_args else f'{func}()'
|
|
540
|
+
|
|
541
|
+
if dispatcher == "mouse":
|
|
542
|
+
mouse_actions = {"272": "drag", "273": "resize"}
|
|
543
|
+
if params and params[0] in mouse_actions:
|
|
544
|
+
return f'hl.dsp.window.{mouse_actions[params[0]]}()'
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
if dispatcher in ("moveintogroup",):
|
|
548
|
+
if params:
|
|
549
|
+
dir_map = {"l": "left", "r": "right", "u": "up", "d": "down"}
|
|
550
|
+
d = dir_map.get(params[0], params[0])
|
|
551
|
+
return f'hl.dsp.window.move({{ into_group = "{d}" }})'
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
elif dispatcher in ("moveoutofgroup",):
|
|
555
|
+
return f'hl.dsp.window.move({{ out_of_group = true }})'
|
|
556
|
+
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
def build_workspace_dispatcher(self, func: str, params: List[str]) -> str:
|
|
560
|
+
if not params:
|
|
561
|
+
return f'{func}({{ workspace = "e+1" }})'
|
|
562
|
+
p = params[0]
|
|
563
|
+
if p.isdigit() or (p.startswith("-") and p[1:].isdigit()):
|
|
564
|
+
return f'{func}({{ workspace = {p} }})'
|
|
565
|
+
if p.startswith("special"):
|
|
566
|
+
if ":" in p:
|
|
567
|
+
return f'{func}({{ workspace = {self.quote(p)} }})'
|
|
568
|
+
return f'{func}({{ workspace = "special" }})'
|
|
569
|
+
if p in ("e+1", "e-1", "m+1", "m-1", "r", "empty", "previous"):
|
|
570
|
+
short_map = {"previous": "previous", "empty": "empty", "r": "r"}
|
|
571
|
+
return f'{func}({{ workspace = {self.quote(short_map.get(p, p))} }})'
|
|
572
|
+
if p.startswith("name:"):
|
|
573
|
+
return f'{func}({{ workspace = {self.quote(p[len("name:"):])} }})'
|
|
574
|
+
return f'{func}({{ workspace = {self.quote(p)} }})'
|
|
575
|
+
|
|
576
|
+
def build_focus_dispatcher(self, func: str, params: List[str]) -> str:
|
|
577
|
+
if not params:
|
|
578
|
+
return f'{func}({{ window = "" }})'
|
|
579
|
+
p = params[0]
|
|
580
|
+
if p.startswith("class:"):
|
|
581
|
+
return f'{func}({{ window = {self.quote(p)} }})'
|
|
582
|
+
if p.startswith("title:"):
|
|
583
|
+
return f'{func}({{ window = {self.quote(p)} }})'
|
|
584
|
+
if p.startswith("address:"):
|
|
585
|
+
return f'{func}({{ window = {self.quote(p)} }})'
|
|
586
|
+
return f'{func}({{ window = {self.quote(p)} }})'
|
|
587
|
+
|
|
588
|
+
def build_dispatcher_args(self, params: List[str], needs_args: bool,
|
|
589
|
+
func: Optional[str] = None) -> str:
|
|
590
|
+
if not params:
|
|
591
|
+
return "" if not needs_args else "nil"
|
|
592
|
+
|
|
593
|
+
if len(params) == 1:
|
|
594
|
+
p = params[0]
|
|
595
|
+
if func and func == "hl.dsp.window.move":
|
|
596
|
+
if p.startswith("special"):
|
|
597
|
+
return f'{{ workspace = {self.quote(p)} }}'
|
|
598
|
+
if p.isdigit():
|
|
599
|
+
return f'{{ workspace = {p} }}'
|
|
600
|
+
return f'{{ direction = {self.quote(p)} }}'
|
|
601
|
+
return self.quote(self.resolve_val(p))
|
|
602
|
+
|
|
603
|
+
parts = []
|
|
604
|
+
for p in params:
|
|
605
|
+
parts.append(self.quote(self.resolve_val(p)))
|
|
606
|
+
return ", ".join(parts)
|
|
607
|
+
|
|
608
|
+
def visit_monitor(self, stmt: MonitorDirective):
|
|
609
|
+
self.translated_count += 1
|
|
610
|
+
self.emit("hl.monitor({")
|
|
611
|
+
self.indent()
|
|
612
|
+
self.emit(f'output = {self.quote(stmt.name)},')
|
|
613
|
+
self.emit(f'mode = {self.quote(stmt.mode)},')
|
|
614
|
+
self.emit(f'position = {self.quote(stmt.position)},')
|
|
615
|
+
self.emit(f'scale = {self.quote(stmt.scale)},')
|
|
616
|
+
for key, val in stmt.extra.items():
|
|
617
|
+
lua_val = self.to_lua_val(val)
|
|
618
|
+
self.emit(f'{key} = {lua_val},')
|
|
619
|
+
self.dedent()
|
|
620
|
+
self.emit("})")
|
|
621
|
+
|
|
622
|
+
def visit_windowrule(self, stmt: WindowRule):
|
|
623
|
+
self.translated_count += 1
|
|
624
|
+
rule = stmt.rule
|
|
625
|
+
match_params = stmt.match_params
|
|
626
|
+
|
|
627
|
+
match = {}
|
|
628
|
+
effects = {}
|
|
629
|
+
|
|
630
|
+
for mp in match_params:
|
|
631
|
+
colon_idx = mp.find(":")
|
|
632
|
+
if colon_idx > 0:
|
|
633
|
+
prefix = mp[:colon_idx].strip()
|
|
634
|
+
value = mp[colon_idx + 1:].strip()
|
|
635
|
+
match_key = prefix
|
|
636
|
+
if value.lower() == "true":
|
|
637
|
+
match[match_key] = "true"
|
|
638
|
+
elif value.lower() == "false":
|
|
639
|
+
match[match_key] = "false"
|
|
640
|
+
else:
|
|
641
|
+
match[match_key] = self.quote(value)
|
|
642
|
+
|
|
643
|
+
if not match and not stmt.is_v2:
|
|
644
|
+
if match_params:
|
|
645
|
+
class_val = match_params[0]
|
|
646
|
+
match["class"] = self.quote(class_val)
|
|
647
|
+
|
|
648
|
+
self.emit("hl.window_rule({")
|
|
649
|
+
self.indent()
|
|
650
|
+
|
|
651
|
+
name_str = re.sub(r'[^a-zA-Z0-9_-]', '_', rule[:20].lower())
|
|
652
|
+
self.emit(f'name = "{name_str}",')
|
|
653
|
+
|
|
654
|
+
self.emit("match = {")
|
|
655
|
+
self.indent()
|
|
656
|
+
for k, v in match.items():
|
|
657
|
+
self.emit(f"{k} = {v},")
|
|
658
|
+
self.dedent()
|
|
659
|
+
self.emit("},")
|
|
660
|
+
|
|
661
|
+
rule_lower = rule.lower().strip()
|
|
662
|
+
applied = False
|
|
663
|
+
|
|
664
|
+
if rule_lower in WINDOW_RULE_MAP:
|
|
665
|
+
k, v = WINDOW_RULE_MAP[rule_lower]
|
|
666
|
+
self.emit(f"{k} = {v},")
|
|
667
|
+
applied = True
|
|
668
|
+
else:
|
|
669
|
+
parts = rule.split(None, 1)
|
|
670
|
+
if parts and parts[0] in WINDOW_RULE_PARAM_MAP:
|
|
671
|
+
k, needs_val = WINDOW_RULE_PARAM_MAP[parts[0]]
|
|
672
|
+
if needs_val and len(parts) > 1:
|
|
673
|
+
val = self.to_lua_val(parts[1])
|
|
674
|
+
self.emit(f"{k} = {val},")
|
|
675
|
+
elif needs_val:
|
|
676
|
+
self.emit(f"{k} = true,")
|
|
677
|
+
else:
|
|
678
|
+
self.emit(f"{k} = true,")
|
|
679
|
+
applied = True
|
|
680
|
+
|
|
681
|
+
if not applied:
|
|
682
|
+
self.flag_count += 1
|
|
683
|
+
self.emit(f'-- TODO: review rule: {self.quote(rule)}')
|
|
684
|
+
|
|
685
|
+
self.dedent()
|
|
686
|
+
self.emit("})")
|
|
687
|
+
|
|
688
|
+
def visit_windowrule_block(self, stmt: WindowRuleBlock):
|
|
689
|
+
self.translated_count += 1
|
|
690
|
+
name_str = stmt.name if stmt.name else re.sub(r'[^a-zA-Z0-9_-]', '_', f'rule_{stmt.line}')
|
|
691
|
+
|
|
692
|
+
self.emit("hl.window_rule({")
|
|
693
|
+
self.indent()
|
|
694
|
+
self.emit(f'name = "{name_str}",')
|
|
695
|
+
|
|
696
|
+
if stmt.match:
|
|
697
|
+
self.emit("match = {")
|
|
698
|
+
self.indent()
|
|
699
|
+
for k, v in stmt.match.items():
|
|
700
|
+
self.emit(f"{k} = {self.quote(v)},")
|
|
701
|
+
self.dedent()
|
|
702
|
+
self.emit("},")
|
|
703
|
+
|
|
704
|
+
for key, vals in stmt.effects.items():
|
|
705
|
+
if len(vals) == 1:
|
|
706
|
+
parts = vals[0].split()
|
|
707
|
+
if len(parts) == 1:
|
|
708
|
+
self.emit(f"{key} = {self.to_lua_val(parts[0])},")
|
|
709
|
+
else:
|
|
710
|
+
lua_parts = [self.to_lua_val(p) for p in parts]
|
|
711
|
+
self.emit(f"{key} = {{ {', '.join(lua_parts)} }},")
|
|
712
|
+
else:
|
|
713
|
+
lua_vals = [self.to_lua_val(v) for v in vals]
|
|
714
|
+
self.emit(f"{key} = {{ {', '.join(lua_vals)} }},")
|
|
715
|
+
|
|
716
|
+
self.dedent()
|
|
717
|
+
self.emit("})")
|
|
718
|
+
|
|
719
|
+
def visit_exec(self, stmt: ExecDirective):
|
|
720
|
+
self.translated_count += 1
|
|
721
|
+
resolved = self.resolve_val(stmt.command)
|
|
722
|
+
if stmt.kind in ("exec-once", "execr-once"):
|
|
723
|
+
self.pending_exec_ones.append(stmt.command)
|
|
724
|
+
elif stmt.kind == "exec-shutdown":
|
|
725
|
+
self.pending_exec_shutdowns.append(stmt.command)
|
|
726
|
+
else:
|
|
727
|
+
self.pending_execs.append(stmt.command)
|
|
728
|
+
|
|
729
|
+
def visit_animation(self, stmt: AnimationDirective):
|
|
730
|
+
self.translated_count += 1
|
|
731
|
+
name = stmt.name
|
|
732
|
+
style = stmt.style
|
|
733
|
+
speed = stmt.speed
|
|
734
|
+
curve = stmt.curve
|
|
735
|
+
|
|
736
|
+
try:
|
|
737
|
+
float(speed)
|
|
738
|
+
speed_lit = speed
|
|
739
|
+
except ValueError:
|
|
740
|
+
speed_lit = self.quote(speed)
|
|
741
|
+
|
|
742
|
+
style_extra = ""
|
|
743
|
+
if style and style != "default":
|
|
744
|
+
if " " in style:
|
|
745
|
+
style_name, *style_args = style.split()
|
|
746
|
+
style_extra = f', style = {self.quote(style)}'
|
|
747
|
+
else:
|
|
748
|
+
style_extra = f', style = {self.quote(style)}'
|
|
749
|
+
|
|
750
|
+
if curve and curve != "default":
|
|
751
|
+
if re.match(r'^\d+(\.\d+)?$', curve):
|
|
752
|
+
return self.emit(f"-- TODO: manual review (numeric curve ref in animation: {stmt.name})")
|
|
753
|
+
|
|
754
|
+
if curve.startswith("spring"):
|
|
755
|
+
spring_parts = curve.split()
|
|
756
|
+
if len(spring_parts) >= 4:
|
|
757
|
+
self.passthrough_count += 1
|
|
758
|
+
self.emit(f'hl.animation({{ leaf = "{name}", enabled = true, speed = {speed_lit}, spring = {self.quote(curve)}{style_extra} }})')
|
|
759
|
+
else:
|
|
760
|
+
self.emit(f'hl.animation({{ leaf = "{name}", enabled = true, speed = {speed_lit}, spring = {self.quote(curve)}{style_extra} }})')
|
|
761
|
+
else:
|
|
762
|
+
self.emit(f'hl.animation({{ leaf = "{name}", enabled = true, speed = {speed_lit}, bezier = {self.quote(curve)}{style_extra} }})')
|
|
763
|
+
else:
|
|
764
|
+
self.emit(f'hl.animation({{ leaf = "{name}", enabled = true, speed = {speed_lit}{style_extra} }})')
|
|
765
|
+
|
|
766
|
+
def visit_bezier(self, stmt: BezierDirective):
|
|
767
|
+
self.translated_count += 1
|
|
768
|
+
self.emit(f'hl.curve({self.quote(stmt.name)}, {{')
|
|
769
|
+
self.indent()
|
|
770
|
+
self.emit(f'type = "bezier",')
|
|
771
|
+
self.emit(f'points = {{ {{ {stmt.p1x}, {stmt.p1y} }}, {{ {stmt.p2x}, {stmt.p2y} }} }},')
|
|
772
|
+
self.dedent()
|
|
773
|
+
self.emit("})")
|
|
774
|
+
|
|
775
|
+
def visit_env(self, stmt: EnvDirective):
|
|
776
|
+
self.translated_count += 1
|
|
777
|
+
resolved_name = self.resolve_val(stmt.name)
|
|
778
|
+
resolved_val = self.resolve_val(stmt.value)
|
|
779
|
+
name_q = self.quote(resolved_name)
|
|
780
|
+
val_q = self.quote(resolved_val)
|
|
781
|
+
self.emit(f"hl.env({name_q}, {val_q})")
|
|
782
|
+
|
|
783
|
+
def visit_source(self, stmt: SourceDirective):
|
|
784
|
+
self.passthrough_count += 1
|
|
785
|
+
path = stmt.path
|
|
786
|
+
|
|
787
|
+
lua_path = path.replace(".conf", "")
|
|
788
|
+
lua_path = lua_path.replace("~", "os.getenv(\"HOME\")")
|
|
789
|
+
lua_path = lua_path.replace("/", ".")
|
|
790
|
+
|
|
791
|
+
if path.endswith("/*.conf"):
|
|
792
|
+
self.emit(f'-- source = {path}')
|
|
793
|
+
self.emit(f"-- NOTE: glob sources need manual handling")
|
|
794
|
+
self.emit(f"-- Directory contents must be required individually")
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
self.emit(f'-- source = {path} -> requires manual conversion')
|
|
798
|
+
home_relative = path.replace("~", "").replace(os_path := "", ".hypr/")
|
|
799
|
+
|
|
800
|
+
import os as _os
|
|
801
|
+
just_name = _os.path.splitext(_os.path.basename(path))[0]
|
|
802
|
+
self.emit(f'-- local {just_name} = require("{just_name}")')
|
|
803
|
+
self.emit(f'-- TODO: convert {path} to .lua and use require()')
|
|
804
|
+
|
|
805
|
+
def visit_device(self, stmt: DeviceSection):
|
|
806
|
+
self.translated_count += 1
|
|
807
|
+
self.emit(f"hl.device({{")
|
|
808
|
+
self.indent()
|
|
809
|
+
self.emit(f'name = {self.quote(stmt.name)},')
|
|
810
|
+
for d in stmt.body:
|
|
811
|
+
if isinstance(d, Directive):
|
|
812
|
+
key = d.key
|
|
813
|
+
val = self.to_lua_val(d.value[0]) if d.value else "true"
|
|
814
|
+
self.emit(f"{key} = {val},")
|
|
815
|
+
self.dedent()
|
|
816
|
+
self.emit("})")
|
|
817
|
+
|
|
818
|
+
def visit_gesture(self, stmt: GestureDirective):
|
|
819
|
+
self.translated_count += 1
|
|
820
|
+
props = {}
|
|
821
|
+
for d in stmt.body:
|
|
822
|
+
if isinstance(d, Directive):
|
|
823
|
+
props[d.key] = d.value[0] if d.value else "true"
|
|
824
|
+
|
|
825
|
+
self.emit("hl.gesture({")
|
|
826
|
+
self.indent()
|
|
827
|
+
for k, v in props.items():
|
|
828
|
+
lua_v = self.to_lua_val(v)
|
|
829
|
+
lua_k = k.replace("_", " ")
|
|
830
|
+
self.emit(f"[{self.quote(lua_k)}] = {lua_v},")
|
|
831
|
+
self.dedent()
|
|
832
|
+
self.emit("})")
|
|
833
|
+
|
|
834
|
+
def visit_submap(self, stmt: SubmapDef):
|
|
835
|
+
self.translated_count += 1
|
|
836
|
+
self.emit(f'hl.define_submap({self.quote(stmt.name)}, function()')
|
|
837
|
+
self.indent()
|
|
838
|
+
for s in stmt.body:
|
|
839
|
+
if isinstance(s, Comment):
|
|
840
|
+
self.emit("--" + s.text[1:])
|
|
841
|
+
elif isinstance(s, BindDirective):
|
|
842
|
+
self.visit_bind(s)
|
|
843
|
+
elif isinstance(s, Directive):
|
|
844
|
+
if s.key == "submap_reset":
|
|
845
|
+
continue
|
|
846
|
+
self.emit(f'-- submap: {s.key} = {", ".join(s.value)}')
|
|
847
|
+
self.dedent()
|
|
848
|
+
self.emit("end)")
|
|
849
|
+
self.emit("")
|
|
850
|
+
|
|
851
|
+
def visit_workspace(self, stmt: WorkspaceDirective):
|
|
852
|
+
self.translated_count += 1
|
|
853
|
+
self.emit("hl.workspace_rule({")
|
|
854
|
+
self.indent()
|
|
855
|
+
self.emit(f'workspace = {self.quote(stmt.name)},')
|
|
856
|
+
for param in stmt.params:
|
|
857
|
+
colon_idx = param.find(":")
|
|
858
|
+
if colon_idx > 0:
|
|
859
|
+
key = param[:colon_idx].strip()
|
|
860
|
+
val = param[colon_idx + 1:].strip()
|
|
861
|
+
lua_key = WORKSPACE_RULE_MAP.get(key, key)
|
|
862
|
+
lua_val = self.to_lua_val(val)
|
|
863
|
+
self.emit(f"{lua_key} = {lua_val},")
|
|
864
|
+
else:
|
|
865
|
+
self.emit(f'-- {param} (unrecognized workspace param)')
|
|
866
|
+
self.dedent()
|
|
867
|
+
self.emit("})")
|
|
868
|
+
|
|
869
|
+
def visit_layerrule(self, stmt: LayerRuleDirective):
|
|
870
|
+
self.translated_count += 1
|
|
871
|
+
rule_lower = stmt.rule.lower()
|
|
872
|
+
ns = stmt.namespace
|
|
873
|
+
|
|
874
|
+
self.emit("hl.layer_rule({")
|
|
875
|
+
self.indent()
|
|
876
|
+
self.emit("match = {")
|
|
877
|
+
self.indent()
|
|
878
|
+
self.emit(f'namespace = {self.quote(ns)},')
|
|
879
|
+
self.dedent()
|
|
880
|
+
self.emit("},")
|
|
881
|
+
|
|
882
|
+
if rule_lower in LAYER_RULE_MAP:
|
|
883
|
+
k, v = LAYER_RULE_MAP[rule_lower]
|
|
884
|
+
if v == "true":
|
|
885
|
+
self.emit(f"{k} = {v},")
|
|
886
|
+
else:
|
|
887
|
+
self.emit(f"{k} = {self.to_lua_val(v)},")
|
|
888
|
+
else:
|
|
889
|
+
parts = rule_lower.split(None, 1)
|
|
890
|
+
if parts:
|
|
891
|
+
self.emit(f"{parts[0]} = {self.quote(parts[1]) if len(parts) > 1 else 'true'},")
|
|
892
|
+
|
|
893
|
+
self.dedent()
|
|
894
|
+
self.emit("})")
|
|
895
|
+
|
|
896
|
+
def visit_layerrule_block(self, stmt: LayerRuleBlock):
|
|
897
|
+
self.translated_count += 1
|
|
898
|
+
name_str = stmt.name if stmt.name else f'layerrule_{stmt.line}'
|
|
899
|
+
|
|
900
|
+
self.emit("hl.layer_rule({")
|
|
901
|
+
self.indent()
|
|
902
|
+
|
|
903
|
+
if stmt.match:
|
|
904
|
+
self.emit("match = {")
|
|
905
|
+
self.indent()
|
|
906
|
+
for k, v in stmt.match.items():
|
|
907
|
+
self.emit(f"{k} = {self.quote(v)},")
|
|
908
|
+
self.dedent()
|
|
909
|
+
self.emit("},")
|
|
910
|
+
|
|
911
|
+
for key, vals in stmt.effects.items():
|
|
912
|
+
if len(vals) == 1:
|
|
913
|
+
parts = vals[0].split()
|
|
914
|
+
if len(parts) == 1:
|
|
915
|
+
self.emit(f"{key} = {self.to_lua_val(parts[0])},")
|
|
916
|
+
else:
|
|
917
|
+
lua_parts = [self.to_lua_val(p) for p in parts]
|
|
918
|
+
self.emit(f"{key} = {{ {', '.join(lua_parts)} }},")
|
|
919
|
+
else:
|
|
920
|
+
lua_vals = [self.to_lua_val(v) for v in vals]
|
|
921
|
+
self.emit(f"{key} = {{ {', '.join(lua_vals)} }},")
|
|
922
|
+
|
|
923
|
+
self.dedent()
|
|
924
|
+
self.emit("})")
|
|
925
|
+
|
|
926
|
+
def get_report(self) -> dict:
|
|
927
|
+
return {
|
|
928
|
+
"translated": self.translated_count,
|
|
929
|
+
"passthrough": self.passthrough_count,
|
|
930
|
+
"flagged": self.flag_count,
|
|
931
|
+
}
|