blob: 7905ead262319ee88dd21c559b99dca42a8cbade [file] [log] [blame]
Philipp Le6203e732022-04-26 21:50:26 +02001# SPDX-License-Identifier: MPL-2.0
2# Copyright (c) 2022 Philipp Le <philipp@philipple.de>.
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7from __future__ import annotations
8
9import toml
10import json
Philipp Le4071d912022-04-26 22:02:43 +020011from typing import Dict, AnyStr, Callable, Type, Any, Optional, List
Philipp Le6203e732022-04-26 21:50:26 +020012from pydantic import BaseModel, PrivateAttr
13import appdirs
14import os
15import logging
Philipp Le4071d912022-04-26 22:02:43 +020016from tkinter import ttk
17from abc import abstractmethod, ABC
Philipp Le6203e732022-04-26 21:50:26 +020018
19
Philipp Le4071d912022-04-26 22:02:43 +020020class ConfigControlFrame(ttk.Frame):
21 ctrl_widgets: List[ttk.Widget]
22
23 def __init__(self, *args, **kwargs):
24 super().__init__(*args, **kwargs)
25 self.ctrl_widgets = []
26
27 def add_widget(self, w: ttk.Widget) -> None:
28 self.ctrl_widgets.append(w)
29
30 def widgets_on_change(self, cb) -> None:
31 for w in self.ctrl_widgets:
32 w.listen_change(cb)
33
34
35class ConfigObject(BaseModel, ABC):
Philipp Le6203e732022-04-26 21:50:26 +020036 _KEY: AnyStr = PrivateAttr()
37
38 @classmethod
39 def get_key(cls) -> AnyStr:
40 return cls._KEY
41
Philipp Le4071d912022-04-26 22:02:43 +020042 @abstractmethod
43 def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame:
44 ...
45
Philipp Le6203e732022-04-26 21:50:26 +020046
47class Store:
48 __instances: Dict[str, Store] = {}
49
50 def __init__(self, raw_objs: Dict[AnyStr, Any], save_cb: Optional[Callable[[Store], None]] = None):
51 self._raw_objs: Dict[AnyStr, Any] = raw_objs
52 self._cfg_objs: Dict[AnyStr, ConfigObject] = {}
53 self._save_cb: Optional[Callable[[Store], None]] = save_cb
54
55 @staticmethod
56 def get_instance(name: str = 'default') -> Store:
57 if name in Store.__instances:
58 return Store.__instances[name]
59 else:
60 raise KeyError(f'The Store instance {name} does not exist.')
61
62 @classmethod
63 def load_obj(cls, obj: Dict[AnyStr, Any], name: str = 'default', *args, **kwargs) -> Store:
64 if name not in Store.__instances:
65 Store.__instances[name] = cls(obj, *args, **kwargs)
66 return Store.__instances[name]
67 else:
68 raise KeyError(f'The Store instance {name} already exists.')
69
70 @classmethod
71 def new(cls, name: str = 'default', *args, **kwargs) -> Store:
72 if name not in Store.__instances:
73 Store.__instances[name] = cls({}, *args, **kwargs)
74 return Store.__instances[name]
75 else:
76 raise KeyError(f'The Store instance {name} already exists.')
77
78 def dump_obj(self) -> Dict[AnyStr, Any]:
79 obj: Dict[AnyStr, Any] = self._raw_objs
80 for key, item in self._cfg_objs.items():
81 obj[key] = json.loads(item.json())
82 return obj
83
84 def get_config(self, tp: Type[ConfigObject]):
85 key = tp.get_key()
86 if key not in self._cfg_objs:
87 if key in self._raw_objs:
88 self._cfg_objs[key] = tp.parse_obj(self._raw_objs[key])
89 else:
90 self._cfg_objs[key] = tp()
91 return self._cfg_objs[key]
92
93 def save(self) -> None:
94 if self._save_cb is not None:
95 self._save_cb(self)
96
97
98class TomlStore(Store):
99 def __init__(self, raw_objs: Dict[AnyStr, Any], toml_file: str):
100 super().__init__(raw_objs, save_cb=self._do_save)
101 self.toml_file: str = toml_file
102
103 @classmethod
104 def load_or_create_toml(cls, toml_file: str, name: str = 'default') -> TomlStore:
105 try:
106 s = Store.get_instance(name)
107 except KeyError:
108 try:
109 s = TomlStore.load_toml(toml_file, name)
110 logging.info(f'Loaded config from {toml_file}')
111 except FileNotFoundError:
112 s = TomlStore.new(name, toml_file=toml_file)
113 logging.info(f'New config at {toml_file}')
114 return s
115
116 @classmethod
117 def load_toml(cls, toml_file: str, name: str = 'default') -> TomlStore:
118 obj = toml.load(toml_file)
119 return TomlStore.load_obj(obj, name, toml_file=toml_file)
120
121 def dump_toml(self) -> None:
122 base_dir = os.path.dirname(self.toml_file)
123 if not os.path.exists(base_dir):
124 logging.info(f'Creating directory {base_dir}')
125 os.mkdir(base_dir)
126 with open(self.toml_file, 'w', encoding='utf-8') as f:
127 toml.dump(self.dump_obj(), f)
128
129 def _do_save(self, s: Store) -> None:
130 assert id(self) == id(s)
131 self.dump_toml()
132
133
134DEFAULT_PATH = os.path.join(
135 appdirs.user_config_dir('dcs_interactive', 'Philipp Le'),
136 'config.toml'
137)
138
139
140def default_store() -> TomlStore:
141 return TomlStore.load_or_create_toml(DEFAULT_PATH, 'default')
142