diff --git a/.gitignore b/.gitignore index af21859..c0015e9 100644 --- a/.gitignore +++ b/.gitignore @@ -177,7 +177,12 @@ pyrightconfig.json # Application custom -# Schemes used for development +### Development ### + +# Schemes schemes/ +# User configuration +dc-themer.json + # End of Application custom \ No newline at end of file diff --git a/TODO.md b/TODO.md index 099812e..637aacf 100644 --- a/TODO.md +++ b/TODO.md @@ -2,11 +2,10 @@ | Item | Details | Priority | |---|---|---| -| Unit test | Investigate Python unit tests standards and implement. | High | +| Unit tests | Investigate Python unit tests standards and implement. | High | | Documentation - user || Medium | -| User config | - Implement config in json format.
- Create default, if doesn't exist, on app start.
- In-app window to modify.
- Versioning, in case new config values appear in the future. | Medium | +| User config | - ~~Implement config in json format.~~
- ~~Create default, if doesn't exist, on app start.~~
- In-app window to modify.
- ~~Versioning, in case new config values appear in the future.~~ | Medium | | Detailed message after completing scheme apply | Should mention what was done, e.g. DC config backup. | Low | | DC config compatibility | - Check scheme version against config version.
- Display warning if version mismatch. | Low | | Scheme export | Allow to export scheme from current DC config. | Low | -| Dark Mode | Implement DC's Dark Mode handling. | Low | | License link | Link in About window. | Low | \ No newline at end of file diff --git a/app/config.py b/app/config.py index d873a57..2418465 100644 --- a/app/config.py +++ b/app/config.py @@ -1,21 +1,18 @@ -# General application information +# General information APP_AUTHOR = 't0mmili' APP_NAME = 'DC Themer' -APP_VERSION = '0.2.0' +APP_VERSION = '0.3.0' DEV_YEARS = '2024' REPO_URL = 'https://github.com/t0mmili/dc-themer' -# Application assets +# Assets +DEFAULT_USER_CONFIG = './assets/default-user-config.json' ICON_PATH = './assets/dct-icon-v3.ico' -# Application config -DC_CONFIG_PATHS = { - 'cfg': '%APPDATA%\\doublecmd\\doublecmd.cfg', - 'json': '%APPDATA%\\doublecmd\\colors.json', - 'xml': '%APPDATA%\\doublecmd\\doublecmd.xml' -} -SCHEME_EXTENSIONS = ['cfg','json','xml'] -SCHEME_PATH = './schemes' +# GUI WINDOW_HEIGHT = 140 WINDOW_WIDTH = 285 -XML_TAGS = ['Colors','Fonts'] \ No newline at end of file + +# User config +USER_CONFIG_PATH = 'dc-themer.json' +USER_CONFIG_VERSION = 1 \ No newline at end of file diff --git a/app/gui.py b/app/gui.py index f8c25ab..cdb236d 100644 --- a/app/gui.py +++ b/app/gui.py @@ -1,9 +1,6 @@ import tkinter as tk import webbrowser -from config import ( - APP_AUTHOR, APP_NAME, APP_VERSION, DC_CONFIG_PATHS, DEV_YEARS, REPO_URL, - SCHEME_EXTENSIONS, SCHEME_PATH, XML_TAGS -) +from config import APP_AUTHOR, APP_NAME, APP_VERSION, DEV_YEARS, REPO_URL from scheme import Scheme from tkinter import ttk from tkinter.messagebox import showerror, showinfo @@ -14,30 +11,30 @@ class AppMenuBar: A class to create and manage the application's menu bar. Attributes: - menu_bar (Menu): The main menu bar container. - file_menu (Menu): The file submenu of the menu bar. - help_menu (Menu): The help submenu of the menu bar. + menu_bar (tk.Menu): The main menu bar container. + file_menu (tk.Menu): The file submenu of the menu bar. + help_menu (tk.Menu): The help submenu of the menu bar. Args: - parent (Tk): The parent widget, typically an instance of Tk or - a top-level window. + parent (tk.Tk): The parent widget, typically an instance of Tk or + a top-level window. """ - def __init__(self, parent): - about_message = ( + def __init__(self, parent: tk.Tk) -> None: + about_message: str = ( f'{APP_NAME} v{APP_VERSION}\n\n' f'Copyright (c) {DEV_YEARS} {APP_AUTHOR}. All rights reserved.\n\n' f'This is open source software, released under the MIT License.' ) # Initialize Menu Bar - self.menu_bar = tk.Menu(parent) + self.menu_bar: tk.Menu = tk.Menu(parent) # Add Menu Bar items - self.file_menu = tk.Menu(self.menu_bar, tearoff=False) + self.file_menu: tk.Menu = tk.Menu(self.menu_bar, tearoff=False) self.file_menu.add_command(label='Exit', command=lambda: parent.quit()) self.menu_bar.add_cascade(label='File', menu=self.file_menu) - self.help_menu = tk.Menu(self.menu_bar, tearoff=False) + self.help_menu: tk.Menu = tk.Menu(self.menu_bar, tearoff=False) self.help_menu.add_command( label=f'{APP_NAME} on GitHub', command=lambda: webbrowser.open(REPO_URL) @@ -56,45 +53,52 @@ class AppFrame(ttk.Frame): scheme_var (StringVar): Variable to hold the selected scheme name. dark_mode_var (BooleanVar): Variable to store the state of the dark mode checkbox. - scheme_selector_label (Label): Label for the scheme selector dropdown. - scheme_selector (OptionMenu): Dropdown menu to select a scheme. - dark_mode_tick (Checkbutton): Checkbox to enable or disable auto - dark mode. - apply_button (Button): Button to apply the selected scheme. + scheme_selector_label (ttk.Label): Label for the scheme selector + dropdown. + scheme_selector (ttk.OptionMenu): Dropdown menu to select a scheme. + dark_mode_tick (ttk.Checkbutton): Checkbox to enable or disable auto + dark mode. + apply_button (ttk.Button): Button to apply the selected scheme. Args: - container (Tk): The parent widget, typically an instance of Tk or - a top-level window. + container (tk.Tk): The parent widget, typically an instance of Tk or + a top-level window. + user_config (dict): The configuration dictionary loaded from user + settings. """ - def __init__(self, container): + def __init__(self, container: tk.Tk, user_config: dict) -> None: super().__init__(container) + self.user_config: dict = user_config self.setup_widgets() self.grid(padx=10, pady=10, sticky=tk.NSEW) - def setup_widgets(self): + def setup_widgets(self) -> None: """ Sets up the widgets in the frame. """ - options = {'padx': 5, 'pady': 5} + options: dict = {'padx': 5, 'pady': 5} # Scheme selector - self.scheme_var = tk.StringVar(self) + self.scheme_var: tk.StringVar = tk.StringVar(self) schemes = SchemeFileManager.list_schemes( - SCHEME_PATH, SCHEME_EXTENSIONS + self.user_config['schemes']['path'], + self.user_config['schemes']['extensions'] + ) + self.scheme_selector_label: ttk.Label = ttk.Label( + self, text='Select scheme:' ) - self.scheme_selector_label = ttk.Label(self, text='Select scheme:') self.scheme_selector_label.grid( column=0, row=0, sticky=tk.W, **options ) - self.scheme_selector = ttk.OptionMenu( + self.scheme_selector: ttk.OptionMenu = ttk.OptionMenu( self, self.scheme_var, schemes[0], *schemes ) self.scheme_selector.grid(column=1, row=0, **options) # Dark Mode checkbox self.dark_mode_var = tk.BooleanVar(self) - self.dark_mode_tick = ttk.Checkbutton( + self.dark_mode_tick: ttk.Checkbutton = ttk.Checkbutton( self, text='Force auto Dark mode', variable=self.dark_mode_var, onvalue=True, offvalue=False, takefocus=False ) @@ -103,21 +107,24 @@ def setup_widgets(self): ) # Apply Scheme button - self.apply_button = ttk.Button( + self.apply_button: ttk.Button = ttk.Button( self, text='Apply', command=self.modify_scheme ) self.apply_button.grid( column=0, row=2, columnspan=2, sticky=tk.W, **options ) - def modify_scheme(self): + def modify_scheme(self) -> None: """ Applies the selected scheme and updates the configuration accordingly. """ try: scheme = Scheme( - self.scheme_var.get(), SCHEME_PATH, DC_CONFIG_PATHS, - self.dark_mode_var.get(), XML_TAGS + self.scheme_var.get(), self.user_config['schemes']['path'], + self.user_config['doubleCommander']['configPaths'], + self.user_config['doubleCommander']['backupConfigs'], + self.dark_mode_var.get(), + self.user_config['schemes']['xmlTags'] ) scheme.apply_scheme() showinfo( @@ -130,5 +137,5 @@ def modify_scheme(self): except Exception as e: showerror( title='Error', - message=f'An unexpected error occurred:\n{str(e)}' + message=str(e) ) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 0454b43..0986668 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,12 @@ import tkinter as tk -from config import APP_NAME, ICON_PATH, WINDOW_HEIGHT, WINDOW_WIDTH +from config import ( + APP_NAME, ICON_PATH, DEFAULT_USER_CONFIG, USER_CONFIG_PATH, + USER_CONFIG_VERSION, WINDOW_HEIGHT, WINDOW_WIDTH +) from gui import AppFrame, AppMenuBar from os import path +from tkinter.messagebox import showerror +from user_config import UserConfigManager class App(tk.Tk): """ @@ -15,16 +20,16 @@ class App(tk.Tk): center_window(width, height): Centers the window on the screen with the given dimensions. """ - def __init__(self): + def __init__(self) -> None: """ Initializes the App class by setting up the main application window, its properties, and the components. """ super().__init__() - icon_path = ICON_PATH + icon_path: str = ICON_PATH # This is necessary for compilation with PyInstaller - # icon_path = path.abspath(path.join(path.dirname(__file__), ICON_PATH)) + # icon_path: str = path.abspath(path.join(path.dirname(__file__), ICON_PATH)) # Set application window properties self.iconbitmap(icon_path) @@ -38,7 +43,7 @@ def __init__(self): # Center the window on the screen self.center_window(WINDOW_WIDTH, WINDOW_HEIGHT) - def center_window(self, width, height): + def center_window(self, width: int, height: int) -> None: """ Centers the window on the screen using the specified width and height. @@ -46,13 +51,51 @@ def center_window(self, width, height): width (int): The width of the window. height (int): The height of the window. """ - screen_width = self.winfo_screenwidth() - screen_height = self.winfo_screenheight() - center_x = (screen_width - width) // 2 - center_y = (screen_height - height) // 2 + screen_width: int = self.winfo_screenwidth() + screen_height: int = self.winfo_screenheight() + center_x: int = (screen_width - width) // 2 + center_y: int = (screen_height - height) // 2 self.geometry(f'{width}x{height}+{center_x}+{center_y}') +def init_user_config() -> dict: + """ + Initializes the user configuration. + + Returns: + user_config (dict): The user configuration dictionary. + """ + + default_config_file: str = DEFAULT_USER_CONFIG + # This is necessary for compilation with PyInstaller + # default_config_file: str = path.abspath(path.join(path.dirname(__file__), DEFAULT_USER_CONFIG)) + + default_user_config: dict = UserConfigManager.get_config( + default_config_file + ) + user_config_file = UserConfigManager(default_user_config, USER_CONFIG_PATH) + + if not user_config_file.exists(): + user_config_file.create_default() + + user_config: dict = UserConfigManager.get_config(USER_CONFIG_PATH) + UserConfigManager.verify(USER_CONFIG_VERSION, user_config['configVersion']) + + return user_config + if __name__ == '__main__': - app = App() - AppFrame(app) - app.mainloop() \ No newline at end of file + """ + Main execution point of the application. + + This section initializes user configuration, + creates the main application window, and starts the event loop. + """ + try: + user_config: dict = init_user_config() + app = App() + AppFrame(app, user_config) + app.mainloop() + except Exception as e: + showerror( + title='Error', + message=str(e) + ) \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt index cb3af57..2ab25d3 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,3 +1,3 @@ configobj==5.0.8 defusedxml==0.7.1 -json_repair==0.25.3 +json_repair==0.26.0 diff --git a/app/scheme.py b/app/scheme.py index fa60bfc..f54ab7d 100644 --- a/app/scheme.py +++ b/app/scheme.py @@ -1,3 +1,4 @@ +from configobj import ConfigObj from defusedxml.ElementTree import parse, tostring from defusedxml.minidom import parseString from os import path @@ -12,6 +13,8 @@ class Scheme: scheme_path (str): The file path where the scheme files are located. dc_configs (dict): A dictionary containing DC configuration file types and their paths. + dc_configs_backup (bool): A flag to backup DC configuration before + scheme apply. auto_dark_mode (bool): A flag to force auto dark mode if True. xml_tags (list): A list of XML tags to be modified in XML configuration files. @@ -27,8 +30,9 @@ class Scheme: configuration file. """ def __init__( - self, scheme, scheme_path, dc_configs, auto_dark_mode, xml_tags - ): + self, scheme: str, scheme_path: str, dc_configs: dict[str, str], + dc_configs_backup: bool, auto_dark_mode: bool, xml_tags: list[str] + ) -> None: """ Constructs all the necessary attributes for the Scheme object. @@ -36,19 +40,23 @@ def __init__( scheme (str): The name of the scheme. scheme_path (str): The file path where the scheme files are located. - dc_configs (dict): A dictionary containing DC configuration file - types and their paths. + dc_configs (dict[str, str]): A dictionary containing DC + configuration file types and their + paths. + dc_configs_backup (bool): A flag to backup DC configuration before + scheme apply. auto_dark_mode (bool): A flag to force auto dark mode if True. - xml_tags (list): A list of XML tags to be modified in xml - configuration files. + xml_tags (list[str]): A list of XML tags to be modified in xml + configuration files. """ - self.scheme = scheme - self.scheme_path = scheme_path - self.dc_configs = dc_configs - self.auto_dark_mode = auto_dark_mode - self.xml_tags = xml_tags - - def apply_scheme(self): + self.scheme: str = scheme + self.scheme_path: str = scheme_path + self.dc_configs: dict[str, str] = dc_configs + self.dc_configs_backup: bool = dc_configs_backup + self.auto_dark_mode: bool = auto_dark_mode + self.xml_tags: list[str] = xml_tags + + def apply_scheme(self) -> None: """ Applies the scheme to all configuration files (cfg, json, xml). """ @@ -56,14 +64,14 @@ def apply_scheme(self): self.apply_scheme_json() self.apply_scheme_xml() - def apply_scheme_cfg(self): + def apply_scheme_cfg(self) -> None: """ Applies the scheme specifically to the cfg configuration file. """ - source_file = path.join(self.scheme_path, f'{self.scheme}.cfg') - target_file = DCFileManager.get_config(self.dc_configs['cfg']) - source_config = SchemeFileManager.get_cfg(source_file) - target_config = SchemeFileManager.get_cfg(target_file) + source_file: str = path.join(self.scheme_path, f'{self.scheme}.cfg') + target_file: str = DCFileManager.get_config(self.dc_configs['cfg']) + source_config: ConfigObj = SchemeFileManager.get_cfg(source_file) + target_config: ConfigObj = SchemeFileManager.get_cfg(target_file) # Set new 'DarkMode' value target_config['DarkMode'] = ( @@ -71,22 +79,24 @@ def apply_scheme_cfg(self): ) # Backup current configuration - DCFileManager.backup_config(target_file) + if self.dc_configs_backup: + DCFileManager.backup_config(target_file) # Save modified DC cfg config file SchemeFileManager.set_cfg(target_config, target_file) - def apply_scheme_json(self): + def apply_scheme_json(self) -> None: """ Applies the scheme specifically to the json configuration file. """ - source_file = path.join(self.scheme_path, f'{self.scheme}.json') - target_file = DCFileManager.get_config(self.dc_configs['json']) - source_config = SchemeFileManager.get_json(source_file) - target_config = SchemeFileManager.get_json(target_file) + source_file: str = path.join(self.scheme_path, f'{self.scheme}.json') + target_file: str = DCFileManager.get_config(self.dc_configs['json']) + source_config: dict = SchemeFileManager.get_json(source_file) + target_config: dict = SchemeFileManager.get_json(target_file) # Backup current configuration - DCFileManager.backup_config(target_file) + if self.dc_configs_backup: + DCFileManager.backup_config(target_file) # Replace the style if name matches for i, style in enumerate(target_config['Styles']): @@ -100,15 +110,16 @@ def apply_scheme_json(self): # Save modified DC json config file SchemeFileManager.set_json(target_config, target_file) - def apply_scheme_xml(self): + def apply_scheme_xml(self) -> None: """ Applies the scheme specifically to the xml configuration file. """ - source_file = path.join(self.scheme_path, f'{self.scheme}.xml') - target_file = DCFileManager.get_config(self.dc_configs['xml']) + source_file: str = path.join(self.scheme_path, f'{self.scheme}.xml') + target_file: str = DCFileManager.get_config(self.dc_configs['xml']) # Backup current configuration - DCFileManager.backup_config(target_file) + if self.dc_configs_backup: + DCFileManager.backup_config(target_file) for item in self.xml_tags: # Create element tree object @@ -122,13 +133,20 @@ def apply_scheme_xml(self): target_tag = target_tree.find(f'./{item}') # Remove current tags and append new ones - target_root.remove(target_tag) - target_root.append(source_tag) + if source_tag is not None: + if target_tag is not None: + target_root.remove(target_tag) + target_root.append(source_tag) + else: + raise ValueError( + f'Tag \'{item}\' does not exist in the source xml ' + 'configuration data.' + ) # Prettify XML - xml_str = tostring(target_root, encoding='utf-8') + xml_str: str = tostring(target_root, encoding='utf-8') dom = parseString(xml_str) - pretty_xml = dom.toprettyxml(indent=' ') + pretty_xml: str = dom.toprettyxml(indent=' ') pretty_xml = '\n'.join( [line for line in pretty_xml.split('\n') if line.strip()] ) diff --git a/app/user_config.py b/app/user_config.py new file mode 100644 index 0000000..a31bb9a --- /dev/null +++ b/app/user_config.py @@ -0,0 +1,102 @@ +from json import dump +from json_repair import loads +from os import path + +class UserConfigManager: + """ + Manages the user configuration for the application. + + Attributes: + default_user_config (dict): The default configuration settings. + user_config_path (str): The file path for the user configuration file. + """ + def __init__( + self, default_user_config: dict, user_config_path: str + ) -> None: + self.default_user_config: dict = default_user_config + self.user_config_path: str = user_config_path + + def exists(self) -> bool: + """ + Checks if the user configuration file exists and is a json file. + + Returns: + bool: True if the file exists and is a json file, False otherwise. + """ + return path.isfile( + self.user_config_path + ) and self.user_config_path.endswith('.json') + + def create_default(self) -> None: + """ + Creates a default user configuration file from the provided default + data. + + Raises: + OSError: If an error occurs while writing to the file. + """ + try: + with open( + self.user_config_path, 'w', encoding='utf-8' + ) as json_file: + dump( + self.default_user_config, json_file, ensure_ascii=False, + indent=2 + ) + except OSError as e: + raise OSError( + 'Failed to write default configuration to ' + f'{self.user_config_path}:\n{str(e)}' + ) + + @staticmethod + def get_config(infile) -> dict: + """ + Reads the user configuration file, repairs it if necessary, and parses + the json data. + + Returns: + dict: The parsed json data from the configuration file. + + Raises: + OSError: If an error occurs while reading the file. + TypeError: If file does not contain valid json object data. + """ + try: + with open(infile, 'r') as json_file: + file_content = json_file.read() + json_data = loads(file_content) + + # Ensure json_data is a dictionary + if not isinstance(json_data, dict): + raise TypeError( + f'The configuration file {infile} does not contain valid ' + 'json object data.' + ) + + return json_data + except OSError as e: + raise OSError( + f'Failed to read configuration from {infile}:\n{str(e)}' + ) + + @staticmethod + def verify(current_version, read_version) -> None: + """ + Checks if the existing user configuration version matches the expected + version. + + Args: + current_version (str): The expected version of the configuration. + read_version (str): The version read from the configuration file. + + Raises: + RuntimeError: If there is a version mismatch between the current + and read version. + """ + if read_version != current_version: + raise RuntimeError( + 'Configuration file version mismatch.\n' + 'Please refer to the release notes for more information about ' + 'application configuration breaking changes.' + ) \ No newline at end of file diff --git a/app/utils.py b/app/utils.py index ef25afa..74e2c71 100644 --- a/app/utils.py +++ b/app/utils.py @@ -3,14 +3,13 @@ from json_repair import loads from os import listdir, path from shutil import copy -from tkinter.messagebox import showerror class DCFileManager: """ Provides static methods for managing DC configuration files. """ @staticmethod - def get_config(dc_config): + def get_config(dc_config: str) -> str: """ Retrieves the path to the specified DC configuration file. @@ -27,15 +26,16 @@ def get_config(dc_config): config_path = path.expandvars(dc_config) # Check if the config file exists - if path.exists(config_path): - return config_path - else: + if not path.exists(config_path): raise FileNotFoundError( - f'Double Commander config file does not exist:\n{config_path}' + 'Double Commander configuration file does not exist:' + f'\n{config_path}' ) + return config_path + @staticmethod - def backup_config(file): + def backup_config(file: str) -> None: """ Creates a backup of the specified DC configuration file by copying it with a '.backup' extension. @@ -44,18 +44,12 @@ def backup_config(file): file (str): The path to the file to be backed up. Raises: - Exception: If an error occurs during the backup process. + OSError: If an error occurs during the backup process. """ try: copy(file, f'{file}.backup') - except Exception as e: - showerror( - title='Error', - message=( - 'An unexpected error occurred while backing up the file:' - f'\n{str(e)}' - ) - ) + except OSError as e: + raise OSError(f'Failed to create backup of {file}:\n{str(e)}') class SchemeFileManager: """ @@ -63,7 +57,7 @@ class SchemeFileManager: json, xml). """ @staticmethod - def get_cfg(infile): + def get_cfg(infile: str) -> ConfigObj: """ Reads a cfg configuration file and returns its contents as a ConfigObj. @@ -75,28 +69,18 @@ def get_cfg(infile): Raises: ConfigObjError: If an error occurs while parsing the cfg file. - Exception: If an unexpected error occurs. """ try: config = ConfigObj(infile) + + return config except ConfigObjError as e: - showerror( - title='Error', - message=( - 'An error occurred while parsing the configuration file:' - f'\n{str(e)}' - ) - ) - except Exception as e: - showerror( - title='Error', - message=f'An unexpected error occurred:\n{str(e)}' + raise ConfigObjError( + f'Failed to parse the configuration file {infile}:\n{str(e)}' ) - else: - return config @staticmethod - def set_cfg(config, outfile): + def set_cfg(config: ConfigObj, outfile: str) -> None: """ Writes a configuration object to a cfg file. @@ -105,29 +89,20 @@ def set_cfg(config, outfile): outfile (str): The path to the output cfg file. Raises: - IOError: If an error occurs while writing to the file. - Exception: If an unexpected error occurs. + OSError: If an error occurs while writing to the file. """ try: with open(outfile, 'w', encoding='utf-8') as cfg_file: for key in config: line = f'{key}={config[key]}\n' cfg_file.write(line) - except IOError as e: - showerror( - title='Error', - message=( - f'An error occurred while writing to the file:\n{str(e)}' - ) - ) - except Exception as e: - showerror( - title='Error', - message=f'An unexpected error occurred:\n{str(e)}' + except OSError as e: + raise OSError( + f'Failed to write configuration to {outfile}:\n{str(e)}' ) @staticmethod - def get_json(infile): + def get_json(infile: str) -> dict: """ Reads, repairs and parses a json configuration file. @@ -138,29 +113,29 @@ def get_json(infile): dict: The parsed json data. Raises: - IOError: If an error occurs while reading the file. - Exception: If an unexpected error occurs. + OSError: If an error occurs while reading the file. + TypeError: If file does not contain valid json object data. """ try: with open(infile, 'r') as json_file: file_content = json_file.read() - except IOError as e: - showerror( - title='Error', - message=f'An error occurred while reading the file:\n{str(e)}' - ) - except Exception as e: - showerror( - title='Error', - message=f'An unexpected error occurred:\n{str(e)}' - ) - else: json_data = loads(file_content) + # Ensure json_data is a dictionary + if not isinstance(json_data, dict): + raise TypeError( + 'The configuration file {infile} does not contain valid ' + 'json object data.' + ) + return json_data + except OSError as e: + raise OSError( + f'Failed to read configuration from {infile}:\n{str(e)}' + ) @staticmethod - def set_json(json_data, outfile): + def set_json(json_data: dict, outfile: str) -> None: """ Writes json data to a file. @@ -169,27 +144,18 @@ def set_json(json_data, outfile): outfile (str): The path to the output file. Raises: - IOError: If an error occurs while writing to the file. - Exception: If an unexpected error occurs. + OSError: If an error occurs while writing to the file. """ try: with open(outfile, 'w', encoding='utf-8') as json_file: dump(json_data, json_file, ensure_ascii=False, indent=2) - except IOError as e: - showerror( - title='Error', - message=( - f'An error occurred while writing to the file:\n{str(e)}' - ) - ) - except Exception as e: - showerror( - title='Error', - message=f'An unexpected error occurred:\n{str(e)}' + except OSError as e: + raise OSError( + f'Failed to write configuration to {outfile}:\n{str(e)}' ) @staticmethod - def set_xml(xml_data, outfile): + def set_xml(xml_data: str, outfile: str) -> None: """ Writes xml data to a file. @@ -198,27 +164,18 @@ def set_xml(xml_data, outfile): outfile (str): The path to the output file. Raises: - IOError: If an error occurs while writing to the file. - Exception: If an unexpected error occurs. + OSError: If an error occurs while writing to the file. """ try: with open(outfile, 'w', encoding='utf-8') as xml_file: xml_file.write(xml_data) - except IOError as e: - showerror( - title='Error', - message=( - f'An error occurred while writing to the file:\n{str(e)}' - ) - ) - except Exception as e: - showerror( - title='Error', - message=f'An unexpected error occurred:\n{str(e)}' + except OSError as e: + raise OSError( + f'Failed to write configuration to {outfile}:\n{str(e)}' ) @staticmethod - def list_schemes(scheme_path, scheme_exts): + def list_schemes(scheme_path: str, scheme_exts: list[str]) -> list[str]: """ Lists all available schemes in the specified directory that meet the required extensions. @@ -226,15 +183,15 @@ def list_schemes(scheme_path, scheme_exts): Args: scheme_path (str): The path to the directory containing scheme files. - scheme_exts (list): A list of required file extensions for each - scheme. + scheme_exts (list[str]): A list of required file extensions for + each scheme. Returns: - list: A sorted list of available scheme names. + list[str]: A sorted list of available scheme names. Raises: - FileNotFoundError: If the directory does not exist or if required - scheme files are missing. + FileNotFoundError: If the directory does not exist or required + files are missing. """ # Verify the existence of the scheme directory if not path.exists(scheme_path): diff --git a/assets/default-user-config.json b/assets/default-user-config.json new file mode 100644 index 0000000..0d96e5c --- /dev/null +++ b/assets/default-user-config.json @@ -0,0 +1,23 @@ +{ + "configVersion": 1, + "doubleCommander": { + "backupConfigs": true, + "configPaths": { + "cfg": "%APPDATA%\\doublecmd\\doublecmd.cfg", + "json": "%APPDATA%\\doublecmd\\colors.json", + "xml": "%APPDATA%\\doublecmd\\doublecmd.xml" + } + }, + "schemes": { + "extensions": [ + "cfg", + "json", + "xml" + ], + "path": "./schemes", + "xmlTags": [ + "Colors", + "Fonts" + ] + } +} \ No newline at end of file