hyprconf2lua 1.2.1__tar.gz → 1.3.1__tar.gz

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.
Files changed (21) hide show
  1. {hyprconf2lua-1.2.1/src/hyprconf2lua.egg-info → hyprconf2lua-1.3.1}/PKG-INFO +1 -1
  2. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/pyproject.toml +1 -1
  3. hyprconf2lua-1.3.1/src/hyprconf2lua/__init__.py +1 -0
  4. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/codegen.py +68 -28
  5. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/mappings.py +1 -0
  6. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/parser.py +36 -9
  7. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1/src/hyprconf2lua.egg-info}/PKG-INFO +1 -1
  8. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/tests/test_converter.py +82 -0
  9. hyprconf2lua-1.2.1/src/hyprconf2lua/__init__.py +0 -1
  10. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/LICENSE +0 -0
  11. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/README.md +0 -0
  12. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/setup.cfg +0 -0
  13. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/__main__.py +0 -0
  14. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/ast.py +0 -0
  15. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/cli.py +0 -0
  16. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/converter.py +0 -0
  17. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua/lexer.py +0 -0
  18. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua.egg-info/SOURCES.txt +0 -0
  19. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua.egg-info/dependency_links.txt +0 -0
  20. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua.egg-info/entry_points.txt +0 -0
  21. {hyprconf2lua-1.2.1 → hyprconf2lua-1.3.1}/src/hyprconf2lua.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyprconf2lua
3
- Version: 1.2.1
3
+ Version: 1.3.1
4
4
  Summary: Convert Hyprland hyprlang .conf files to Lua .lua config format (v0.55+)
5
5
  Author: hyprconf2lua contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hyprconf2lua"
7
- version = "1.2.1"
7
+ version = "1.3.1"
8
8
  description = "Convert Hyprland hyprlang .conf files to Lua .lua config format (v0.55+)"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1 @@
1
+ __version__ = "1.3.1"
@@ -51,7 +51,7 @@ class Codegen:
51
51
 
52
52
  def generate(self, config: ConfigFile) -> str:
53
53
  self.lines = []
54
- self.emit("-- Generated by hyprconf2lua v1.2.1")
54
+ self.emit("-- Generated by hyprconf2lua v1.3.1")
55
55
  self.emit("-- https://github.com/yourusername/hyprconf2lua")
56
56
  self.emit("-- Manual review may be needed for complex directives")
57
57
  self.emit("")
@@ -222,6 +222,53 @@ class Codegen:
222
222
  self.emit("end)")
223
223
  self.emit("")
224
224
 
225
+ def _is_gradient(self, val: str) -> bool:
226
+ parts = val.strip().split()
227
+ if len(parts) < 3:
228
+ return False
229
+ angle = parts[-1]
230
+ if not angle.endswith("deg"):
231
+ return False
232
+ try:
233
+ float(angle[:-3])
234
+ except ValueError:
235
+ return False
236
+ return True
237
+
238
+ def _format_gradient(self, val: str) -> str:
239
+ parts = val.strip().split()
240
+ angle = parts[-1][:-3]
241
+ colors = [self.to_lua_val(p) for p in parts[:-1]]
242
+ return f"{{ colors = {{ {', '.join(colors)} }}, angle = {angle} }}"
243
+
244
+ def _emit_directive(self, key: str, values: List[str]):
245
+ if len(values) == 1 and self._is_gradient(values[0]):
246
+ self.translated_count += 1
247
+ self.emit(f"{key} = {self._format_gradient(values[0])},")
248
+ elif len(values) == 1:
249
+ self.translated_count += 1
250
+ val = self.to_lua_val(values[0])
251
+ self.emit(f"{key} = {val},")
252
+ else:
253
+ self.translated_count += 1
254
+ vals = [self.to_lua_val(v) for v in values]
255
+ self.emit(f"{key} = {{ {', '.join(vals)} }},")
256
+
257
+ def _group_section_body(self, directives: List[BlockStmt]) -> List[BlockStmt]:
258
+ grouped: List[BlockStmt] = []
259
+ dot_groups = {}
260
+ for d in directives:
261
+ if isinstance(d, Directive) and "." in d.key:
262
+ prefix, suffix = d.key.split(".", 1)
263
+ if prefix not in dot_groups:
264
+ dot_groups[prefix] = []
265
+ dot_groups[prefix].append(Directive(suffix, d.value, d.line, d.col))
266
+ else:
267
+ grouped.append(d)
268
+ for prefix, subs in dot_groups.items():
269
+ grouped.append(Section(prefix, subs, 0))
270
+ return grouped
271
+
225
272
  def emit_section_config(self, section_name: str, directives: List[BlockStmt]):
226
273
  if not directives:
227
274
  return
@@ -231,7 +278,7 @@ class Codegen:
231
278
  self.emit(f"{section_name} = {{")
232
279
  self.indent()
233
280
 
234
- for d in directives:
281
+ for d in self._group_section_body(directives):
235
282
  if isinstance(d, Comment):
236
283
  self.emit(f"--{d.text[1:]}")
237
284
  elif isinstance(d, Section):
@@ -242,14 +289,7 @@ class Codegen:
242
289
  if isinstance(sd, Comment):
243
290
  self.emit(f"--{sd.text[1:]}")
244
291
  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)} }},")
292
+ self._emit_directive(sd.key, sd.value)
253
293
  elif isinstance(sd, Section):
254
294
  self.passthrough_count += 1
255
295
  self.emit(f"-- Nested subsection {sd.name}:")
@@ -261,14 +301,7 @@ class Codegen:
261
301
  self.dedent()
262
302
  self.emit("},")
263
303
  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)} }},")
304
+ self._emit_directive(d.key, d.value)
272
305
 
273
306
  self.dedent()
274
307
  self.emit("},")
@@ -322,6 +355,15 @@ class Codegen:
322
355
  if not has_plugins:
323
356
  self.emit("-- TODO: manual review (plugin config)")
324
357
  return
358
+ if stmt.name == "device":
359
+ name = ""
360
+ body = []
361
+ for child in stmt.body:
362
+ if isinstance(child, Directive) and child.key == "name":
363
+ name = child.value[0] if child.value else ""
364
+ else:
365
+ body.append(child)
366
+ return self.visit(DeviceSection(name, body, stmt.line))
325
367
  if stmt.name in KNOWN_SECTIONS:
326
368
  self.emit_section_config(stmt.name, stmt.body)
327
369
  else:
@@ -330,17 +372,11 @@ class Codegen:
330
372
  self.indent()
331
373
  self.emit(f"{stmt.name} = {{")
332
374
  self.indent()
333
- for child in stmt.body:
375
+ for child in self._group_section_body(stmt.body):
334
376
  if isinstance(child, Comment):
335
377
  self.emit(f"--{child.text[1:]}")
336
378
  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)} }},")
379
+ self._emit_directive(child.key, child.value)
344
380
  self.dedent()
345
381
  self.emit("},")
346
382
  self.dedent()
@@ -470,8 +506,12 @@ class Codegen:
470
506
  if dispatcher in DISPATCHER_MAP:
471
507
  func, needs_args = DISPATCHER_MAP[dispatcher]
472
508
 
509
+ if dispatcher == "movetoworkspace":
510
+ args = self.build_dispatcher_args(params, needs_args, func)
511
+ return f'{func}({args})'
512
+
473
513
  if dispatcher == "movetoworkspacesilent":
474
- args = self.build_dispatcher_args(params, needs_args)
514
+ args = self.build_dispatcher_args(params, needs_args, func)
475
515
  return f'{func}({args}, {{ follow = false }})'
476
516
 
477
517
  if dispatcher == "movefocus":
@@ -535,7 +575,7 @@ class Codegen:
535
575
  resolved = self.resolve_val(params[0]) if params else ""
536
576
  return f'{func}({self.quote(resolved)})'
537
577
 
538
- args = self.build_dispatcher_args(params, needs_args)
578
+ args = self.build_dispatcher_args(params, needs_args, func)
539
579
  return f'{func}({args})' if needs_args else f'{func}()'
540
580
 
541
581
  if dispatcher == "mouse":
@@ -51,6 +51,7 @@ DISPATCHER_MAP = {
51
51
  "moveoutofgroup": ("hl.dsp.window.move", True),
52
52
  "focusurgentorlast": ("hl.dsp.focus", True),
53
53
  "focusonemonitor": ("hl.dsp.focus", True),
54
+ "forcekillactive": ("hl.dsp.window.kill", False),
54
55
  }
55
56
 
56
57
  BIND_FLAGS_TO_OPTIONS = {
@@ -302,7 +302,7 @@ class Parser:
302
302
  params = [cmd] if cmd else []
303
303
  else:
304
304
  params = [v.strip() for v in values[3:]]
305
- mods = [m.strip() for m in mods_str.replace(",", " ").split()] if mods_str else []
305
+ mods = [m.strip() for m in mods_str.replace(",", " ").split() if m.strip() and m.strip() != "&"] if mods_str else []
306
306
  return BindDirective(mods, key, dispatcher, params, flags, line)
307
307
 
308
308
  def parse_monitor(self, line: int) -> MonitorDirective:
@@ -344,15 +344,42 @@ class Parser:
344
344
  break
345
345
  key = ":".join(key_parts)
346
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
347
+ if key == "match" and self.peek().type == "BLOCK_OPEN":
348
+ self.advance()
349
+ self.skip_newlines()
350
+ while self.peek().type not in ("BLOCK_CLOSE", "EOF"):
351
+ t2 = self.peek()
352
+ if t2.type == "COMMENT":
353
+ self.advance()
354
+ elif t2.type == "IDENT":
355
+ mk_parts = [self.advance().value]
356
+ while self.peek().type == "COLON":
357
+ self.advance()
358
+ if self.peek().type == "IDENT":
359
+ mk_parts.append(self.advance().value)
360
+ else:
361
+ break
362
+ mk_key = ":".join(mk_parts)
363
+ self.expect("EQUALS")
364
+ mk_val = self.parse_value_rest()
365
+ if mk_key.startswith("match:"):
366
+ match[mk_key[len("match:"):]] = mk_val
367
+ else:
368
+ match[mk_key] = mk_val
369
+ else:
370
+ self.advance()
371
+ self.skip_newlines()
372
+ self.expect("BLOCK_CLOSE")
354
373
  else:
355
- effects[key] = [val]
374
+ self.expect("EQUALS")
375
+ val = self.parse_value_rest()
376
+
377
+ if key == "name":
378
+ name = val
379
+ elif key.startswith("match:"):
380
+ match[key[len("match:"):]] = val
381
+ else:
382
+ effects[key] = [val]
356
383
  else:
357
384
  self.advance()
358
385
  self.skip_newlines()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyprconf2lua
3
- Version: 1.2.1
3
+ Version: 1.3.1
4
4
  Summary: Convert Hyprland hyprlang .conf files to Lua .lua config format (v0.55+)
5
5
  Author: hyprconf2lua contributors
6
6
  License: MIT
@@ -528,6 +528,77 @@ def test_bracket_in_windowrule_params():
528
528
  assert result.success
529
529
 
530
530
 
531
+ def test_forcekillactive_dispatcher():
532
+ result = convert('bind = SUPER, Q, forcekillactive\n')
533
+ assert result.success
534
+ assert "hl.dsp.window.kill" in result.lua
535
+
536
+
537
+ def test_ampersand_in_bind_mods():
538
+ result = convert('bind = $mainMod & SHIFT & A, exec, kitty\n')
539
+ assert result.success
540
+ assert "hl.bind" in result.lua
541
+ assert "&" not in result.lua.split("hl.bind")[-1].split("hl.dsp")[0]
542
+
543
+
544
+ def test_ampersand_in_bind_mods_nospace():
545
+ result = convert('bind = $mainMod&$SHIFT&A, exec, kitty\n')
546
+ assert result.success
547
+
548
+
549
+ def test_device_without_prefix():
550
+ result = convert('device {\n name = razer-cobra-pro\n sensitivity = 0.0\n}\n')
551
+ assert result.success
552
+ assert "hl.device" in result.lua
553
+ assert "razer-cobra-pro" in result.lua
554
+
555
+
556
+ def test_gradient_conversion():
557
+ result = convert('general {\n col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg\n}\n')
558
+ assert result.success
559
+ assert "colors" in result.lua
560
+ assert "angle = 45" in result.lua
561
+
562
+
563
+ def test_col_dot_prefix_grouped():
564
+ result = convert('general {\n col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg\n col.inactive_border = rgba(595959aa)\n gaps_in = 5\n}\n')
565
+ assert result.success
566
+ assert result.lua.count("col = {") == 1
567
+
568
+
569
+ def test_match_block_in_windowrule():
570
+ result = convert('windowrule {\n name = my-rule\n float = on\n match {\n class = .*\n title = my-window\n }\n}\n')
571
+ assert result.success
572
+ assert "class = \".*\"" in result.lua
573
+ assert "title = \"my-window\"" in result.lua
574
+ assert "float = true" in result.lua
575
+
576
+
577
+ def test_match_block_in_windowrulev2():
578
+ result = convert('windowrulev2 {\n name = v2-match\n opacity = 0.9 0.8\n match {\n class = (kitty)\n }\n}\n')
579
+ assert result.success
580
+ assert "class = \"(kitty)\"" in result.lua
581
+ assert "opacity = { 0.9, 0.8 }" in result.lua
582
+
583
+
584
+ def test_match_block_in_layerrule():
585
+ result = convert('layerrule {\n name = my-layer\n blur = true\n match {\n namespace = rofi\n }\n}\n')
586
+ assert result.success
587
+ assert "namespace" in result.lua
588
+
589
+
590
+ def test_movetoworkspace_conversion():
591
+ result = convert('bind = $mainMod SHIFT, 1, movetoworkspace, 1\n')
592
+ assert result.success
593
+ assert "workspace = 1" in result.lua
594
+
595
+
596
+ def test_movetoworkspacesilent_conversion():
597
+ result = convert('bind = $mainMod SHIFT, 1, movetoworkspacesilent, 1\n')
598
+ assert result.success
599
+ assert "workspace = 1" in result.lua
600
+
601
+
531
602
  if __name__ == "__main__":
532
603
  test_lexer_basic()
533
604
  test_lexer_comment()
@@ -574,4 +645,15 @@ if __name__ == "__main__":
574
645
  test_bracket_in_workspace()
575
646
  test_bracket_in_exec_on()
576
647
  test_bracket_in_windowrule_params()
648
+ test_forcekillactive_dispatcher()
649
+ test_ampersand_in_bind_mods()
650
+ test_ampersand_in_bind_mods_nospace()
651
+ test_device_without_prefix()
652
+ test_gradient_conversion()
653
+ test_col_dot_prefix_grouped()
654
+ test_match_block_in_windowrule()
655
+ test_match_block_in_windowrulev2()
656
+ test_match_block_in_layerrule()
657
+ test_movetoworkspace_conversion()
658
+ test_movetoworkspacesilent_conversion()
577
659
  print("All tests passed!")
@@ -1 +0,0 @@
1
- __version__ = "1.2.1"
File without changes
File without changes
File without changes