├── .gitignore ├── README.md ├── hyprparser ├── __init__.py ├── __main__.py └── src │ ├── __init__.py │ └── classes │ ├── __init__.py │ ├── linetype.py │ ├── parser.py │ └── structures.py ├── pyproject.toml └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | /hyprparser.egg-info/ 3 | /build/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyprparser-py 2 | 3 | A Pythonic parser for Hyprland configuration files. 4 | 5 | --- 6 | 7 | ### Example 8 | 9 | ```python 10 | from hyprparser import Color, Gradient, HyprData, Setting 11 | 12 | if __name__ == "__main__": 13 | 14 | for i in [ 15 | "input:touchpad:natural_scroll", 16 | "general:col.active_border", 17 | "general:gaps_in", 18 | ]: 19 | print(HyprData.get_option(i)) 20 | 21 | # HyprData.set_option("general:gaps_in", 50) # 5 22 | # HyprData.set_option("general:gaps_out", 20) # 20 23 | 24 | 25 | if not HyprData.get_option( 26 | "input:numlock_by_default" 27 | ): # if option doesnt exists add a new setting 28 | HyprData.new_option(Setting("input:numlock_by_default", True)) 29 | else: 30 | print(HyprData.get_option("input:numlock_by_default")) 31 | 32 | if not HyprData.get_option("general:col.active_border"): 33 | HyprData.new_option( 34 | Setting( 35 | "general:col.active_border", 36 | Gradient(0, [Color("00", "11", "22"), Color("dd", "e1", "e6")]), 37 | ) 38 | ) 39 | HyprData.save_all() 40 | 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /hyprparser/__init__.py: -------------------------------------------------------------------------------- 1 | from .src import ( 2 | Bezier, 3 | Binding, 4 | Color, 5 | Env, 6 | Exec, 7 | Gradient, 8 | HyprData, 9 | Monitor, 10 | Setting, 11 | Variable, 12 | ) 13 | 14 | 15 | __all__ = [ 16 | Bezier, 17 | Binding, 18 | Color, 19 | Env, 20 | Exec, 21 | Gradient, 22 | HyprData, 23 | Monitor, 24 | Setting, 25 | Variable, 26 | ] 27 | -------------------------------------------------------------------------------- /hyprparser/__main__.py: -------------------------------------------------------------------------------- 1 | from src import Color, Gradient, HyprData, Setting 2 | 3 | if __name__ == '__main__': 4 | if not HyprData.get_option( 5 | 'input:numlock_by_default' 6 | ): # if option doesnt exists add a new setting 7 | HyprData.new_option(Setting('input:numlock_by_default', True)) 8 | else: 9 | print(HyprData.get_option('input:numlock_by_default')) 10 | 11 | if not HyprData.get_option('general:col.active_border'): 12 | HyprData.new_option( 13 | Setting( 14 | 'general:col.active_border', 15 | Gradient( 16 | angle=0, 17 | colors=[ 18 | Color('ff', '00', '00'), 19 | Color('00', 'ff', '00'), 20 | Color('00', '00', 'ff'), 21 | ], 22 | ), 23 | ) 24 | ) 25 | tmp = HyprData.get_option('general:col.active_border') 26 | if tmp: 27 | print(tmp.format()) 28 | # HyprData.save_all() 29 | -------------------------------------------------------------------------------- /hyprparser/src/__init__.py: -------------------------------------------------------------------------------- 1 | from .classes import (Bezier, Binding, Color, Env, Exec, Gradient, HyprData, 2 | Monitor, Setting, Variable) 3 | -------------------------------------------------------------------------------- /hyprparser/src/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from .parser import HyprData 2 | from .structures import (Bezier, Binding, Color, Env, Exec, Gradient, Monitor, 3 | Setting, TypeParser, Variable) 4 | -------------------------------------------------------------------------------- /hyprparser/src/classes/linetype.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal 2 | 3 | LineType = Literal[ 4 | "start-section", 5 | "end-section", 6 | "monitor", 7 | "source", 8 | "exec", 9 | "env", 10 | "setting", 11 | "variable", 12 | "bind", 13 | "bezier", 14 | "windowrule", 15 | "windowrulev2", 16 | "layerrule", 17 | ] 18 | 19 | LineTypeList: List[LineType] = [ 20 | "start-section", 21 | "end-section", 22 | "monitor", 23 | "source", 24 | "exec", 25 | "env", 26 | "setting", 27 | "variable", 28 | "bind", 29 | "bezier", 30 | "windowrule", 31 | "windowrulev2", 32 | "layerrule", 33 | ] 34 | -------------------------------------------------------------------------------- /hyprparser/src/classes/parser.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | from typing import Dict, List, Tuple, Union 4 | 5 | from .linetype import LineType, LineTypeList 6 | from .structures import (Bezier, Binding, Color, Env, Exec, Gradient, Monitor, 7 | Setting, TypeParser, Variable) 8 | 9 | last_section = [] 10 | last_file = "" 11 | 12 | class Config: 13 | _instance = None 14 | 15 | def __new__(cls, *args, **kwargs): 16 | if cls._instance is None: 17 | cls._instance = super().__new__(cls) 18 | return cls._instance 19 | 20 | def __init__(self, path: str) -> None: 21 | self.path = path 22 | self.monitors: List[Monitor] = [] 23 | self.binds: List[Binding] = [] 24 | self.variables: List[Variable] = [] 25 | self.config: Dict[str, Setting] = {} 26 | self.beziers: Dict[str, Bezier] = {} 27 | self.env: Dict[str, Env] = {} 28 | self.exec: List[Exec] = [] 29 | self.files: List[File] = [File(path, Helper.read_file(path))] 30 | self.insta_save: bool = False 31 | self.override_options:bool = False 32 | 33 | 34 | def reload(self) -> None: 35 | return Helper.read_lines(Helper.read_file(self.path)) 36 | 37 | def save_all(self) -> None: 38 | for file in self.files: 39 | file.save() 40 | 41 | def new_option( 42 | self, 43 | new_option: Setting, 44 | ) -> None: 45 | sections = new_option.option.split(":")[:-1] 46 | line_n, file = Helper.get_line_option(sections) 47 | 48 | if not file: 49 | Helper.new_sections(sections) 50 | return HyprData.new_option(new_option) 51 | 52 | indent = " " * new_option.option.count(":") 53 | file.content.insert(line_n + 1, indent + new_option.format()) 54 | 55 | if self.insta_save: 56 | return file.save() 57 | 58 | def get_option(self, option: str) -> Union[Setting, None]: 59 | return self.config.get(option) 60 | 61 | def set_option( 62 | self, option: str, value: Union[Gradient, Color, Color, str, int, float, bool] 63 | ) -> None: 64 | obj_option = self.config.get(option) 65 | 66 | if not obj_option: 67 | return 68 | 69 | obj_option.value = value 70 | new_line = obj_option.format() 71 | line_n, file = Helper.get_line_option(option) 72 | 73 | if not file: 74 | file = HyprData.files[0] 75 | line_n = len(file.content) - 1 76 | 77 | indent = " " * obj_option.option.count(":") 78 | file.content[line_n] = indent + new_line 79 | 80 | if HyprData.insta_save: 81 | return file.save() 82 | 83 | def new_env(self, env: Env) -> None: 84 | line_n, file = Helper.get_line_option("env") 85 | 86 | if not file: 87 | file = HyprData.files[0] 88 | line_n = -1 89 | 90 | if line_n == -1: 91 | file.content.append(env.format()) 92 | else: 93 | file.content.insert(line_n, env.format()) 94 | 95 | HyprData.env[env.name] = env 96 | if HyprData.insta_save: 97 | return file.save() 98 | 99 | def get_env(self, env_name: str) -> Union[Env, None]: 100 | return self.env.get(env_name) 101 | 102 | 103 | def set_env(self, env_name: str, value: List[str]) -> None: 104 | obj_env = HyprData.env.get(env_name) 105 | if not obj_env: 106 | return 107 | 108 | obj_env.value = value 109 | line_n, file = Helper.get_line_env(env_name) 110 | 111 | if not file: 112 | file = HyprData.files[0] 113 | line_n = len(file.content) 114 | 115 | file.content[line_n] = obj_env.format() 116 | if HyprData.insta_save: 117 | return file.save() 118 | 119 | def new_bezier(self, bezier:Bezier) -> None: 120 | line_n, file = Helper.get_line_option("animations:bezier") 121 | if not file: 122 | file = HyprData.files[0] 123 | line_n = -1 124 | 125 | if line_n == -1: 126 | 127 | file.content.insert(line_n, bezier.format()) 128 | 129 | 130 | def get_bezier(self, bezier_name:str) -> Union[Bezier, None]: 131 | return self.beziers.get(bezier_name) 132 | 133 | def set_bezier(self, bezier_name: str, value: Tuple[float, float, float, float]) -> None: 134 | obj_bezier= HyprData.beziers.get(bezier_name) 135 | if not obj_bezier: 136 | return 137 | 138 | obj_bezier.transition = value 139 | line_n, file = Helper.get_line_bezier(obj_bezier.name) 140 | 141 | if not file: 142 | file = HyprData.files[0] 143 | line_n = len(file.content) 144 | 145 | file.content[line_n] = obj_bezier.format() 146 | if HyprData.insta_save: 147 | return file.save() 148 | 149 | 150 | def new_bind(self, bind: Binding) -> None: 151 | line_n, file = Helper.get_line_option("bind") 152 | 153 | if not file: 154 | file = HyprData.files[0] 155 | line_n = -1 156 | 157 | if line_n == -1: 158 | file.content.append(bind.format()) 159 | else: 160 | file.content.insert(line_n, bind.format()) 161 | 162 | if HyprData.insta_save: 163 | return file.save() 164 | 165 | 166 | @dataclass 167 | class File: 168 | path: str 169 | content: List[str] 170 | 171 | def save(self) -> None: 172 | return Helper.save_file(self.path, self.content) 173 | 174 | 175 | class Helper: 176 | @staticmethod 177 | def read_file(path: str) -> List[str]: 178 | global last_file 179 | last_file = os.path.expandvars(path) 180 | with open(last_file) as file: 181 | return file.read().splitlines() 182 | 183 | @staticmethod 184 | def save_file(path: str, content: List[str]) -> None: 185 | with open(os.path.expandvars(path), "w+") as file: 186 | return file.writelines(map(lambda v: v + "\n", content)) 187 | 188 | @staticmethod 189 | def new_sections(sections: List[str]) -> None: 190 | if not sections: 191 | return 192 | 193 | depth = [] 194 | 195 | for i, section in enumerate(sections, 0): 196 | depth += [section] 197 | _, file = Helper.get_line_option(depth) 198 | 199 | if not file: 200 | line_n, file = Helper.get_line_option(depth[:-1]) 201 | 202 | if not file: 203 | continue 204 | indent = " " * i 205 | file.content.insert(line_n + 1, indent + section + " {") 206 | file.content.insert(line_n + 2, indent + "}") 207 | if i + 1 == len(sections): 208 | return file.save() 209 | 210 | @staticmethod 211 | def read_lines(lines: List[str]): 212 | for line in lines: 213 | if LineParser.skip(line): 214 | continue 215 | 216 | line = LineParser.format_line(line) 217 | 218 | match LineParser.get_linetype(line): 219 | case "start-section": 220 | LineParser.add_section(line) 221 | case "end-section": 222 | LineParser.del_section(line) 223 | case "setting": 224 | DataParser.parse_setting(line) 225 | case "bind": 226 | DataParser.parse_bind(line) 227 | case "variable": 228 | DataParser.parse_variable(line) 229 | case "source": 230 | DataParser.parse_source(line) 231 | case "monitor": 232 | DataParser.parse_monitor(line) 233 | case "bezier": 234 | DataParser.parse_bezier(line) 235 | case "env": 236 | DataParser.parse_env(line) 237 | case "exec": 238 | DataParser.parse_exec(line) 239 | case "windowrule": 240 | pass 241 | case "windowrulev2": 242 | pass 243 | case "layerrule": 244 | pass 245 | case _: 246 | print(line) 247 | 248 | @staticmethod 249 | def get_line_option(option: Union[str, List[str]]) -> Tuple[int, Union[File, None]]: 250 | if isinstance(option, str): 251 | section_depth = option.split(":") 252 | else: 253 | section_depth = option 254 | 255 | for file in HyprData.files: 256 | depth = [] 257 | for i, line in enumerate(file.content): 258 | if LineParser.skip(line): 259 | continue 260 | 261 | line = LineParser.format_line(line) 262 | 263 | match LineParser.get_linetype(line): 264 | case "start-section": 265 | section_name, _ = map(str.strip, line.split("{")) 266 | depth += [section_name] 267 | if depth == section_depth: 268 | return i, file 269 | 270 | case "end-section": 271 | depth.pop(-line.count("}")) 272 | case _: 273 | section_name, _ = line.split(" = ") 274 | depth += [section_name] 275 | if depth == section_depth: 276 | return i, file 277 | depth.pop(-1) 278 | 279 | return (-1, None) 280 | 281 | @staticmethod 282 | def get_line_env(env_name: str) -> Tuple[int, Union[File, None]]: 283 | for file in HyprData.files: 284 | for i, line in enumerate(file.content): 285 | if LineParser.skip(line): 286 | continue 287 | 288 | line = LineParser.format_line(line) 289 | match LineParser.get_linetype(line): 290 | case "env": 291 | _, env = line.split(" = ") 292 | name, *_ = env.split(",") 293 | if env_name == name: 294 | return i, file 295 | return (-1, None) 296 | @staticmethod 297 | def get_line_bezier(bezier_name:str) -> Tuple[int, Union[File, None]]: 298 | for file in HyprData.files: 299 | for i, line in enumerate(file.content): 300 | if LineParser.skip(line): 301 | continue 302 | line = LineParser.format_line(line) 303 | 304 | match LineParser.get_linetype(line): 305 | case "bezier": 306 | _, bezier = line.split(" = ") 307 | name, *_ = bezier.split(",") 308 | if bezier_name == name.strip(): 309 | return i, file 310 | return (-1, None) 311 | 312 | class LineParser: 313 | @staticmethod 314 | def add_section(line: str) -> None: 315 | global last_section 316 | section_name, _ = map(str.strip, line.split("{")) 317 | last_section += [section_name] 318 | 319 | @staticmethod 320 | def del_section(line: str) -> None: 321 | global last_section 322 | last_section.pop(-line.count("}")) 323 | 324 | @staticmethod 325 | def format_line(line: str) -> str: 326 | line = line.split("#", 1)[0].strip() 327 | 328 | try: 329 | name, value = line.split("=", 1) 330 | return "{} = {}".format(name.strip(), value.strip()) 331 | except ValueError: 332 | if line: 333 | return line 334 | except Exception as e: 335 | print(e) 336 | return "" 337 | 338 | @staticmethod 339 | def get_linetype(line: str) -> LineType: 340 | line = LineParser.format_line(line) 341 | if "{" in line: 342 | return "start-section" 343 | elif "}" in line: 344 | return "end-section" 345 | elif line.startswith("$"): 346 | return "variable" 347 | else: 348 | for linetype in LineTypeList: 349 | if line.startswith(linetype): 350 | return linetype 351 | return "setting" 352 | 353 | @staticmethod 354 | def skip(line: str) -> bool: 355 | tmp = LineParser.format_line(line) 356 | if tmp.startswith("#") or not tmp: 357 | return True 358 | return False 359 | 360 | 361 | class DataParser: 362 | @staticmethod 363 | def parse_monitor(line: str) -> None: 364 | _, monitor = line.split(" = ") 365 | name, res, pos, scale = map(str.strip, monitor.split(",")) 366 | 367 | return HyprData.monitors.append(Monitor(name, res, pos, scale)) 368 | 369 | @staticmethod 370 | def parse_variable(line: str) -> None: 371 | return HyprData.variables.append(Variable(*line[1:].split(" = "))) 372 | 373 | @staticmethod 374 | def parse_setting(line: str) -> None: 375 | name, value = line.split(" = ") 376 | section = ":".join(last_section) + ":" + name # section:subsection:name 377 | 378 | if TypeParser.is_bool(value): 379 | value = TypeParser.to_bool(value) 380 | elif TypeParser.is_int(value): 381 | value = TypeParser.to_int(value) 382 | elif TypeParser.is_float(value): 383 | value = TypeParser.to_float(value) 384 | elif TypeParser.is_color(value): 385 | value = TypeParser.to_color(value) 386 | elif TypeParser.is_gradient(value): 387 | value = TypeParser.to_gradient(value) 388 | 389 | HyprData.config[section] = Setting(section, value) 390 | 391 | @staticmethod 392 | def parse_exec(line: str) -> None: 393 | exectype, cmd = line.split(" = ") 394 | 395 | if exectype == "exec-once": 396 | return HyprData.exec.append(Exec(cmd, True)) 397 | return HyprData.exec.append(Exec(cmd)) 398 | 399 | @staticmethod 400 | def parse_bezier(line: str) -> None: 401 | _, bezier = line.split(" = ") 402 | name, *curve = map(str.strip, bezier.split(",", 4)) 403 | curve = tuple(map(float, curve)) 404 | HyprData.beziers[name] = Bezier(name, curve) # type: ignore 405 | 406 | @staticmethod 407 | def parse_bind(line: str) -> None: 408 | bindtype, keys = line.split(" = ") 409 | mods, key, dispatcher, *params = map(str.strip, keys.split(",", 4)) 410 | mods = mods.split() if mods else [] 411 | return HyprData.binds.append(Binding(mods, key, dispatcher, params, bindtype)) 412 | 413 | @staticmethod 414 | def parse_source(line: str) -> None: 415 | _, path = line.split(" = ") 416 | 417 | content = Helper.read_file(path) 418 | 419 | HyprData.files.append(File(path, content)) 420 | return Helper.read_lines(content) 421 | 422 | @staticmethod 423 | def parse_env(line: str) -> None: 424 | _, env = line.split(" = ") 425 | var_env, *value = map(str.strip, env.split(",", 1)) 426 | 427 | HyprData.env[var_env] = Env(var_env, value) 428 | 429 | HyprData: Config = Config("$HOME/.config/hypr/hyprland.conf") 430 | HyprData.reload() 431 | -------------------------------------------------------------------------------- /hyprparser/src/classes/structures.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Dict, List, Tuple, Union 3 | 4 | 5 | @dataclass 6 | class Setting: 7 | option: str 8 | value: Union['Gradient', 'Color', str, int, float, bool] 9 | 10 | def format(self) -> str: 11 | name = self.option.split(':').pop(-1) 12 | 13 | if isinstance(self.value, bool): 14 | value = { 15 | True: 'true', 16 | False: 'false', 17 | }.get(self.value, 'false') 18 | elif isinstance(self.value, (Gradient, Color)): 19 | value = self.value.format() 20 | else: 21 | value = self.value 22 | 23 | return '{} = {}'.format(name, value) 24 | 25 | def to_dict(self) -> Dict[str, Any]: 26 | return { 27 | 'section': self.option, 28 | 'value': getattr(self.value, 'to_dict', lambda: self.value)(), 29 | } 30 | 31 | 32 | @dataclass 33 | class Exec: 34 | cmd: str 35 | exec_once: bool = False 36 | 37 | def format(self) -> str: 38 | if self.exec_once: 39 | return 'exec-once = {}'.format(self.cmd) 40 | return 'exec = {}'.format(self.cmd) 41 | 42 | def to_dict(self) -> Dict[str, Union[str, bool]]: 43 | return {'cmd': self.cmd, 'exec-once': self.exec_once} 44 | 45 | 46 | @dataclass 47 | class Windowrule: 48 | rule: str 49 | window: str 50 | rule_args: str = '' 51 | 52 | def format(self) -> str: 53 | return 'windowrule = {}, {}'.format( 54 | ' '.join([self.rule, str(self.rule_args)]), self.window 55 | ) 56 | 57 | 58 | @dataclass 59 | class Bezier: 60 | name: str 61 | transition: Tuple[float, float, float, float] 62 | 63 | def format(self) -> str: 64 | return 'bezier = {}, {}'.format( 65 | self.name, ', '.join(map(str, self.transition)) 66 | ) 67 | 68 | def to_dict(self) -> dict: 69 | return {'name': self.name, 'transition': self.transition} 70 | 71 | 72 | @dataclass 73 | class Color: 74 | r: str 75 | g: str 76 | b: str 77 | a: str = 'ff' 78 | 79 | @property 80 | def hex(self) -> str: 81 | return '{}{}{}{}'.format(self.r, self.g, self.b, self.a) 82 | 83 | def format(self) -> str: 84 | return 'rgba({})'.format(self.hex) 85 | 86 | def __repr__(self) -> str: 87 | return self.format() 88 | 89 | 90 | @dataclass 91 | class Gradient: 92 | angle: int 93 | colors: List[Color] 94 | 95 | def add_color(self, color: Color) -> None: 96 | return self.colors.append(color) 97 | 98 | def del_color(self, color: Color) -> None: 99 | return self.colors.remove(color) 100 | 101 | def format(self) -> str: 102 | return '{} {}deg'.format( 103 | ' '.join(map(Color.format, self.colors)), self.angle 104 | ) 105 | 106 | def to_dict(self) -> Dict[str, Union[List[str], int]]: 107 | return { 108 | 'colors': list(map(Color.format, self.colors)), 109 | 'angle': self.angle, 110 | } 111 | 112 | def __repr__(self) -> str: 113 | return 'Gradient({}, {}deg)'.format( 114 | ', '.join(map(Color.__repr__, self.colors)), self.angle 115 | ) 116 | 117 | 118 | @dataclass 119 | class Variable: 120 | name: str = '' 121 | value: str = '' 122 | 123 | def format(self) -> str: 124 | return '${} = {}'.format(self.name, self.value) 125 | 126 | def to_dict(self) -> Dict[str, str]: 127 | return { 128 | 'name': self.name, 129 | 'value': self.value, 130 | } 131 | 132 | 133 | @dataclass 134 | class Env: 135 | name: str 136 | value: List[str] 137 | 138 | def format(self) -> str: 139 | return 'env = {},{}'.format(self.name, ':'.join(self.value)) 140 | 141 | def to_dict(self) -> Dict[str, Union[str, List[str]]]: 142 | return {'name': self.name, 'value': self.value} 143 | 144 | 145 | @dataclass 146 | class Monitor: 147 | name: str = '' 148 | resolution: str = 'preferred' 149 | position: str = 'auto' 150 | scale: str = '1' 151 | 152 | def format(self) -> str: 153 | return 'monitor = {}'.format( 154 | ','.join([self.name, self.resolution, self.position, self.scale]) 155 | ) 156 | 157 | def to_dict(self) -> Dict[str, str]: 158 | return { 159 | 'name': self.name, 160 | 'resolution': self.resolution, 161 | 'position': self.position, 162 | 'scale': self.scale, 163 | } 164 | 165 | 166 | @dataclass 167 | class Binding: 168 | mods: List[str] 169 | key: str 170 | dispatcher: str 171 | params: List[str] 172 | bindtype: str = 'e' 173 | 174 | def format(self) -> str: 175 | return '{} = {}'.format( 176 | self.bindtype, 177 | ', '.join( 178 | [ 179 | ' '.join(self.mods), 180 | self.key, 181 | self.dispatcher, 182 | *self.params, 183 | ] 184 | ), 185 | ) 186 | 187 | def to_dict(self) -> Dict[str, Union[str, List[str]]]: 188 | return { 189 | 'mods': self.mods, 190 | 'key': self.key, 191 | 'dispatcher': self.dispatcher, 192 | 'params': self.params, 193 | 'bindtype': self.bindtype, 194 | } 195 | 196 | 197 | class TypeParser: 198 | @staticmethod 199 | def is_bool(value: str) -> bool: 200 | if value in ['on', 'yes', 'true', 'off', 'no', 'false']: 201 | return True 202 | return False 203 | 204 | @staticmethod 205 | def to_bool(value: str) -> bool: 206 | if value in ['on', 'yes', 'true']: 207 | return True 208 | elif value in ['off', 'no', 'false']: 209 | return False 210 | raise Exception('Invalid Data-type') 211 | 212 | #! TODO: Use regex to check if a str can be a int 213 | @staticmethod 214 | def is_int(value: str) -> bool: 215 | if value.startswith('-'): 216 | return TypeParser.is_int(value[1:]) 217 | return value.isdecimal() 218 | 219 | @staticmethod 220 | def to_int(value: str) -> int: 221 | return int(value) 222 | 223 | #! TODO: Use regex to check if a str can be a float 224 | # https://www.geeksforgeeks.org/python-check-for-float-string/ 225 | @staticmethod 226 | def is_float(value: str) -> bool: 227 | return TypeParser.is_int(value.replace('.', '')) 228 | 229 | @staticmethod 230 | def to_float(value: str) -> float: 231 | return float(value) 232 | 233 | @staticmethod 234 | def is_color(value: str) -> bool: 235 | if len(value.split()) != 1: 236 | return False 237 | 238 | if any(map(value.startswith, ['rgba', 'rgb', '0x'])): 239 | return True 240 | 241 | return False 242 | 243 | @staticmethod 244 | def to_color(value: str) -> Color: 245 | for p in ['rgba(', 'rgb', '0x', '#']: 246 | value = value.removeprefix(p) 247 | value = value.removesuffix(')') 248 | r = value[0:2] 249 | g = value[2:4] 250 | b = value[4:6] 251 | a = value[6:8] 252 | return Color(r, g, b, a) 253 | 254 | @staticmethod 255 | def is_gradient(value: str) -> bool: 256 | gradients = value.split() 257 | 258 | if not gradients: 259 | return False 260 | if gradients[-1].endswith('deg'): 261 | gradients = gradients[:-1] 262 | 263 | return all(map(TypeParser.is_color, gradients)) 264 | 265 | @staticmethod 266 | def to_gradient(value: str) -> Gradient: 267 | gradients = value.split() 268 | 269 | if gradients[-1].endswith('deg'): 270 | angle = TypeParser.to_int(gradients.pop(-1).removesuffix('deg')) 271 | else: 272 | angle = 0 273 | 274 | return Gradient(angle, list(map(TypeParser.to_color, gradients))) 275 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "hyprparser" 7 | version = "0.0.1" 8 | authors = [ 9 | { name="tokyob0t" }, 10 | ] 11 | description = "A Pythonic parser for Hyprland configuration files." 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | 15 | [project.urls] 16 | "Homepage" = "https://github.com/T0kyoB0y/hyprparser-py" 17 | "Bug Tracker" = "https://github.com/T0kyoB0y/hyprparser-py/issues" 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="hyprparser", 5 | version="0.0.1", 6 | author="T0kyoB0y", 7 | description="A Pythonic parser for Hyprland configuration files.", 8 | long_description=open("README.md").read(), 9 | long_description_content_type="text/markdown", 10 | url="https://github.com/T0kyoB0y/hyprparser-py", 11 | project_urls={ 12 | "Homepage": "https://github.com/T0kyoB0y/hyprparser-py", 13 | "Bug Tracker": "https://github.com/T0kyoB0y/hyprparser-py/issues", 14 | }, 15 | packages=find_packages(), 16 | python_requires=">=3.10", 17 | ) 18 | --------------------------------------------------------------------------------