iso-scalpel/iso_scalpel/config.py

94 lines
3.4 KiB
Python

"""Persistent application settings (recent files, defaults, window state)."""
# This file is part of ISO Scalpel.
# Copyright (C) 2025 Jeremy Anderson <info@dcos.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>
# or write to the Free Software Foundation, Inc., 51 Franklin Street,
# Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import annotations
import json
import os
import sys
from dataclasses import asdict, dataclass, field
def _config_dir() -> str:
base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
return os.path.join(base, "iso-scalpel")
def _config_path() -> str:
return os.path.join(_config_dir(), "settings.json")
@dataclass
class Settings:
recent_files: list[str] = field(default_factory=list)
last_dir: str = os.path.expanduser("~")
show_hidden: bool = False
confirm_delete: bool = True
default_volume_label: str = "CDROM"
default_interchange_level: int = 1
default_joliet: int = 3
default_rock_ridge: str = "1.09"
default_udf: str = "2.60"
default_block_size: int = 2048
window_width: int = 1100
window_height: int = 720
splitter_sizes: list[int] = field(default_factory=lambda: [500, 500])
# ------------------------------------------------------------------
@classmethod
def load(cls) -> Settings:
"""Load settings from disk, merging onto a fresh instance.
A missing file, a permission error, or a corrupt JSON payload yields
the default settings rather than aborting startup.
"""
try:
with open(_config_path(), encoding="utf-8") as fh:
data = json.load(fh)
except (OSError, json.JSONDecodeError):
return cls()
if not isinstance(data, dict):
return cls()
# Merge onto defaults so new fields added in a later release do not
# break a settings file written by an older one.
merged = cls()
for key, value in data.items():
if hasattr(merged, key):
setattr(merged, key, value)
return merged
def save(self) -> None:
"""Persist settings to disk. Failures are reported on stderr but
never raised: a read-only home directory must not crash the app."""
try:
os.makedirs(_config_dir(), exist_ok=True)
with open(_config_path(), "w", encoding="utf-8") as fh:
json.dump(asdict(self), fh, indent=2)
except OSError as exc:
sys.stderr.write(f"warning: could not save settings: {exc}\n")
# ------------------------------------------------------------------ helpers
def add_recent(self, path: str) -> None:
path = os.path.abspath(path)
if path in self.recent_files:
self.recent_files.remove(path)
self.recent_files.insert(0, path)
self.recent_files = self.recent_files[:10]