AceCentre / MorseWriter

Small app to convert 1 or 2 key presses from morse into text and keyboard-mapped keys
23 stars 4 forks source link

Separate directories for user data #40

Closed willwade closed 5 months ago

willwade commented 5 months ago

We should make sure all config files etc are in some kind of user local data folder. The main app has to run from a protected c program files as it requires admin install rights. So any user generated configured data should be local to the user. We also want to run it locally eg from the root or the repo when in development. I guess we could look at OS and if frozen params.

To handle configuration files and user-generated data in a cross-platform manner, you can follow these steps:

  1. Determine the appropriate directory based on the operating system and whether the application is running in development or as a frozen executable.
  2. Create a helper function to get the correct path for storing user data.

Here's how you can implement this:

Step 1: Define a Helper Function

Define a helper function to get the appropriate directory for storing user data based on the operating system and execution context.

import os
import sys
import platform

def get_user_data_dir(app_name="MorseWriter"):
    """
    Returns the appropriate directory for storing user data based on the OS and whether the app is frozen.
    """
    if hasattr(sys, 'frozen'):
        # If the application is frozen, use the appropriate platform-specific directory
        if platform.system() == 'Windows':
            return os.path.join(os.getenv('LOCALAPPDATA'), app_name)
        elif platform.system() == 'Darwin':
            return os.path.join(os.path.expanduser('~/Library/Application Support/'), app_name)
        else:
            return os.path.join(os.path.expanduser('~/.config/'), app_name)
    else:
        # Use a local directory when running in development
        return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'user_data')

# Ensure the directory exists
user_data_dir = get_user_data_dir()
os.makedirs(user_data_dir, exist_ok=True)

Step 2: Use the Helper Function for Config Files

Use this helper function to determine where to load and save configuration files and user-generated data.

# Example of loading abbreviations from the user data directory

def load_abbreviations():
    abbreviations_file = os.path.join(user_data_dir, 'abbreviations.txt')
    abbreviations = {}
    try:
        with open(abbreviations_file, 'r') as f:
            for line in f:
                if line.strip():
                    abbr, expansion = line.strip().split('\t')
                    abbreviations[abbr] = expansion
    except FileNotFoundError:
        logging.warning(f"Abbreviations file not found: {abbreviations_file}")
    except Exception as e:
        logging.error(f"Failed to load abbreviations: {e}")
    return abbreviations

Step 3: Modify ConfigManager to Use the User Data Directory

Modify your ConfigManager class to use the user data directory for configuration files.

class ConfigManager:
    def __init__(self, config_file=None, default_config=DEFAULT_CONFIG):
        self.key_data = { ... }  # Your existing key_data
        self.config_file = config_file or os.path.join(user_data_dir, 'config.json')
        self.default_config = default_config
        self.keystrokemap, self.keystrokes = self.initKeystrokeMap()
        self.config = self.read_config()
        self.actions = {}

    def read_config(self):
        if self.config_file and os.path.exists(self.config_file):
            try:
                with open(self.config_file, "r") as file:
                    data = json.load(file)
                    self.update_keystrokes(data)
                    self.convert_types(data)
                    return data
            except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
                logging.warning(f"Error loading configuration: {e}")
        return self.default_config.copy()

    def save_config(self):
        try:
            with open(self.config_file, "w") as file:
                json.dump(self.config, file, indent=4)
        except Exception as e:
            logging.warning(f"Error saving configuration: {e}")

    # Other methods remain the same

Step 4: Ensure Cross-Platform Compatibility

Make sure your application correctly handles different platforms by using the platform module and the paths defined in the helper function.

Step 5: Handle Running in Development

Ensure that when running in development, the application uses a local directory:

if __name__ == '__main__':
    # Initialize the user data directory
    user_data_dir = get_user_data_dir()
    os.makedirs(user_data_dir, exist_ok=True)

    app = CustomApplication(sys.argv)

    if not QSystemTrayIcon.isSystemTrayAvailable():
        QMessageBox.critical(None, "MorseWriter", "I couldn't detect any system tray on this system.")
        sys.exit(1)

    QApplication.setQuitOnLastWindowClosed(False)

    # Initialize managers
    configmanager = ConfigManager(default_config=DEFAULT_CONFIG)
    layoutmanager = LayoutManager(os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts.json"))

    # Create main window
    window = Window(layoutManager=layoutmanager, configManager=configmanager)

    # Now that we have the window, initialize actions that may require window reference
    actions = configmanager.initActions(window)
    layoutmanager.set_actions(actions)

    # Finish initializing the window if needed (after actions are available)
    window.postInit()

    # Show or hide the window based on the configuration
    if configmanager.config.get("autostart", False):
        window.hide()
        window.start()
    else:
        window.show()

    # Start the application event loop
    sys.exit(app.exec_())

We also need to work up the installer