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