diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..e5ce2ff
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,16 @@
+#!/bin/sh
+# vim: syn=sh ts=4 sts=4 sw=4 smartindent expandtab ff=unix
+
+SELF="$(realpath $0)"
+GITHOOKS_DIR="$(dirname "$SELF")" ; export GITHOOKS_DIR
+PROJECT_ROOT="$(dirname "$(dirname "$SELF")")" ; export PROJECT_ROOT
+
+pre_commit_d="${GITHOOKS_DIR}/pre-commit-d"
+
+for i in $(ls "$pre_commit_d"); do
+ script="${pre_commit_d}/$i"
+ if [ -x "$script" ]; then
+ "$script" "$@"
+ fi
+done
+
diff --git a/.githooks/pre-commit.d/fix_line_endings.sh b/.githooks/pre-commit.d/fix_line_endings.sh
new file mode 100755
index 0000000..8b9727c
--- /dev/null
+++ b/.githooks/pre-commit.d/fix_line_endings.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# vim: syn=sh ts=4 sts=4 sw=4 smartindent expandtab ff=unix
+
+dos2unix=$(which dos2unix)
+unix2dos=$(which unix2dos)
+
+githooks="pre-commit prepare-commit-msg commit-msg post-commit applypatch-msg pre-applypatch post-applypatch pre-rebase post-rewrite post-checkout post-merge"
+
+dos2unix_used=NO
+
+__IFS="$IFS"
+IFS=$'\n'
+if [ -n "$dos2unix" -a "$unix2dos" ]; then
+ for line in $(git status -s); do
+ if [[ line == A* || $line == M* ]]; then
+ file="${line:3}"
+ abspath="${PROJECT_ROOT}/$file"
+
+ if [[ $file == *.py || $file == *.sh || file == *.rst ]]; then
+ $dos2unix "$abspath"
+ git add "$file"
+ dos2unix_used=YES
+ continue
+ fi
+
+ #check if we are updating a githook
+ for githook in $githooks; do
+ if [ "$file" = ".githooks/$githook" ]; then
+ $dos2unix "$abspath"
+ git add "$file"
+ dos2unix_used=YES
+ break
+ fi
+ done
+ fi
+
+ if [ "$dos2unix_used" = "YES" ]; then
+ continue
+ fi
+ if [[ "$file" == *.txt ]]; then
+ $unix2dos "$abspath"
+ git add "$file"
+ fi
+ done
+else
+ echo "\"dos2unix\" and/or \"unix2dos\" not found!" >&2
+fi
+IFS="$__IFS"
diff --git a/.gitignore b/.gitignore
index 15201ac..9a3a5e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -169,3 +169,19 @@ cython_debug/
# PyPI configuration file
.pypirc
+
+.vscode/
+.vsccode/*
+
+# logfiles
+*.log
+*.LOG
+*.[Ll][Oo][Gg]
+
+# editor files
+*~
+*.swp
+*.tmp
+*.temp
+
+apidoc/
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..02bf8f5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,24 @@
+[build-system]
+builbackend = 'setuptools.build_meta'
+requires = ['setuptools >= 61.0']
+
+[project]
+dynamic = ["version"]
+name = 'sgbackup'
+version = '0.0.0'
+requires_python = '>= 3.11'
+description = 'Savegame Backup Tool'
+readme = 'README.md'
+license = {file = 'LICENSE'}
+authors = [
+ {name = 'Christian Moser', email = 'christian@mydevel.at'},
+]
+dependencies = ['gi','yaml']
+
+[project.scripts]
+sgbackup = 'sgbackup:cli_main'
+csgbackup = 'sgbackup:curses_main'
+
+[project.gui-scripts]
+gsgbackup = 'sgbackup:gui_main'
+
diff --git a/sgbackup/__init__.py b/sgbackup/__init__.py
new file mode 100644
index 0000000..a417072
--- /dev/null
+++ b/sgbackup/__init__.py
@@ -0,0 +1,40 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+import gi
+gi.require_version('Gtk','4.0')
+
+__version__ = "0.0.1"
+from .settings import settings
+from . import _logging
+from .main import cli_main,curses_main,gui_main
+from . import game
+from .command import Command
+from . import commands
+from . import archiver
+
+__ALL__ = [
+ "settings"
+ "cli_main",
+ "gui_main",
+ "curses_main",
+ 'game',
+ "Command",
+ "commands",
+ "archiver",
+]
diff --git a/sgbackup/__main__.py b/sgbackup/__main__.py
new file mode 100644
index 0000000..968c883
--- /dev/null
+++ b/sgbackup/__main__.py
@@ -0,0 +1,23 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from .main import cli_main
+import sys
+
+if __name__ == '__main__':
+ sys.exit(cli_main())
diff --git a/sgbackup/_logging.py b/sgbackup/_logging.py
new file mode 100644
index 0000000..0cabc60
--- /dev/null
+++ b/sgbackup/_logging.py
@@ -0,0 +1,28 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+import os
+
+import logging
+import logging.config
+from .settings import settings
+
+if os.path.isfile(settings.logger_conf):
+ logging.config.fileConfig(settings.logger_conf)
+else:
+ logging.config.fileConfig(os.path.join(os.path.dirname(__file__),"logger.conf"))
diff --git a/sgbackup/archiver.py b/sgbackup/archiver.py
new file mode 100644
index 0000000..b44bf74
--- /dev/null
+++ b/sgbackup/archiver.py
@@ -0,0 +1,20 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+class Archiver:
+ pass
\ No newline at end of file
diff --git a/sgbackup/command.py b/sgbackup/command.py
new file mode 100644
index 0000000..21710f5
--- /dev/null
+++ b/sgbackup/command.py
@@ -0,0 +1,43 @@
+
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+class Command:
+ def __init__(self,id:str,name:str,description:str):
+ self.__id = id
+ self.__name = name
+ self.__description = description
+
+ def get_name(self):
+ return self.__name
+
+ def get_id(self):
+ return self.__id
+
+ def get_description(self):
+ return self.__description
+
+ def get_help(self):
+ raise NotImplementedError("Command.get_help() is not implemented!")
+
+ def get_synopsis(self):
+ raise NotImplementedError("Command.get_synopsis() is not implemented!")
+
+ def execute(self,argv:list):
+ raise NotImplementedError("Command.execute is not implemented!")
+
diff --git a/sgbackup/commands/__init__.py b/sgbackup/commands/__init__.py
new file mode 100644
index 0000000..bf0d13c
--- /dev/null
+++ b/sgbackup/commands/__init__.py
@@ -0,0 +1,48 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+import os
+
+COMMANDS = {}
+
+_mods = ['commandbase']
+
+for _f in os.listdir(os.path.dirname(__file__)):
+ if _f.startswith('_'):
+ continue
+
+ if _f.endswith('.py') or _f.endswith('.pyc'):
+ if (_f.endswith('py')):
+ _m = _f[0:-3]
+ else:
+ _m = _f[0:-4]
+
+ if _m not in _mods:
+ exec("\n".join([
+ "from . import " + _m,
+ "_mods += _m",
+ "_mod = " + _m]))
+ if hasattr(_mod,"COMMANDS") and len(_mod.COMMANDS) > 0:
+ for _cmd in _mod.COMMANDS:
+ COMMANDS[_cmd.get_id()] = _cmd
+ del _cmd
+
+del _mods
+del _f
+del _m
+del _mod
diff --git a/sgbackup/commands/help.py b/sgbackup/commands/help.py
new file mode 100644
index 0000000..1d56c5c
--- /dev/null
+++ b/sgbackup/commands/help.py
@@ -0,0 +1,38 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from sgbackup import __version__ as VERSION, Command
+
+class VersionCommand(Command):
+ def __init__(self):
+ super().__init__('version', 'Version', 'Show version information.')
+
+ def get_synopsis(self):
+ return 'sgbackup version'
+
+ def get_help(self):
+ return super().get_help()
+
+ def execute(self, argv):
+ print("sgbackup - {}".format(VERSION))
+ return 0
+# VersionCommand class
+
+COMMANDS = [
+ VersionCommand(),
+]
diff --git a/sgbackup/curses/__init__.py b/sgbackup/curses/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sgbackup/curses/__main__.py b/sgbackup/curses/__main__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sgbackup/game.py b/sgbackup/game.py
new file mode 100644
index 0000000..b8e9be5
--- /dev/null
+++ b/sgbackup/game.py
@@ -0,0 +1,1015 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from gi.repository.GObject import Property,GObject,Signal,SignalFlags
+from gi.repository import GLib
+
+import os
+import json
+import re
+import fnmatch
+
+from enum import StrEnum
+import sys
+import logging
+import pathlib
+
+logger = logging.getLogger(__name__)
+
+from .settings import settings
+
+if sys.platform.lower() == "win32":
+ PLATFORM_WIN32 = True
+ import winreg
+else:
+ PLATFORM_WIN32 = False
+
+if sys.platform.lower() in ['linux','freebsd','netbsd','openbsd','dragonfly']:
+ PLATFORM_LINUX = True
+else:
+ PLATFORM_LINUX = False
+
+
+class SavegameType(StrEnum):
+ UNSET = "unset"
+ OTHER = "other"
+ WINDOWS = "windows"
+ LINUX = "linux"
+ MACOS = "macos"
+ STEAM_WINDOWS = "steam_windows"
+ STEAM_LINUX = "steam_linux"
+ STEAM_MACOS = "steam_macos"
+ GOG_WINDOWS = "gog_windows"
+ GOG_LINUX = "gog_linux"
+ EPIC_WINDOWS = "epic_windows"
+ EPIC_LINUX = "epic_linux"
+
+ @staticmethod
+ def from_string(typestring:str):
+ st=SavegameType
+ s=typestring.lower()
+ if (s == 'other'):
+ return st.OTHER
+ elif (s == 'windows'):
+ return st.WINDOWS
+ elif (s == 'linux'):
+ return st.LINUX
+ elif (s == 'macos'):
+ return st.MACOS
+ elif (s == 'steam_windows' or s == 'steamwindows' or s == 'steam.windows'):
+ return st.STEAM_WINDOWS
+ elif (s == 'steam_linux' or s == 'steamlinux' or s == 'steam.linux'):
+ return st.STEAM_LINUX
+ elif (s == 'steam_macos' or s == 'steammacos' or s == 'steam.macos'):
+ return st.STEAM_MACOS
+ elif (s == 'gog_winows' or s == 'gogwindows' or s == 'gog.windows'):
+ return st.GOG_WINDOWS
+ elif (s == 'gog_linux' or s == 'goglinux' or s == 'gog.linux'):
+ return st.GOG_LINUX
+ elif (s == 'epic_windows' or s == 'epicwindows' or s == 'epic.windows'):
+ return st.EPIC_WINDOWS
+ elif (s == 'epic_linux' or s == 'epiclinux' or s == 'epic.linux'):
+ return st.EPIC_LINUX
+
+ return st.UNSET
+
+
+class GameFileType(StrEnum):
+ GLOB = "glob"
+ REGEX = "regex"
+ FILENAME = "filename"
+
+ @staticmethod
+ def from_string(typestring:str):
+ s = typestring.lower()
+ if (s == 'glob'):
+ return GameFileType.GLOB
+ elif (s == 'regex'):
+ return GameFileType.REGEX
+ elif (s == 'filename'):
+ return GameFileType.FILENAME
+
+ raise ValueError("Unknown GameFileType \"{}\"!".fomrat(typestring))
+
+class GameFileMatcher(GObject):
+ def __init__(self,match_type:GameFileType,match_file:str):
+ GObject.__init__(self)
+ self.match_type = type
+ self.match_file = match_file
+
+ @Property
+ def match_type(self)->GameFileType:
+ return self.__match_type
+ @match_type.setter
+ def match_type(self,type:GameFileType):
+ if not isinstance(type,GameFileType):
+ raise TypeError("match_type is not a GameFileType instance!")
+ self.__match_type = type
+
+ @Property(type=str)
+ def match_file(self)->str:
+ return self.__match_file
+ @match_file.setter
+ def match_file(self,file:str):
+ self.__match_file = file
+
+ ## @}
+
+ def match(self,rel_filename:str):
+ def match_glob(filename):
+ return fnmatch.fnmatch(filename,self.match_file)
+ # match_glob()
+
+ def match_filename(filename):
+ if (PLATFORM_WIN32):
+ fn = filename.replace("/","\\")
+ if (self.match_file.endswith("\\")):
+ if fn == self.match_file[:-1] or fn.startswith(self.match_file):
+ return True
+ elif fn == self.match_file:
+ return True
+ else:
+ if (self.match_file.endswith('/')):
+ if fn == self.match_file[:-1] or fn.startswith(self.match_file):
+ return True
+ elif fn == self.match_file:
+ return True
+ return False
+ # match_filename()
+
+ def match_regex(filename):
+ return (re.search(self.match_file,filename) is not None)
+ # match_filename()
+
+ if (self.match_type == GameFileType.FILENAME):
+ return match_filename(rel_filename)
+ elif (self.match_type == GameFileType.GLOB):
+ return match_glob(rel_filename)
+ elif (self.match_type == GameFileType.REGEX):
+ return match_regex(rel_filename)
+ return False
+
+class GameData(GObject):
+ """
+ :class: GameData
+ :brief: Base class for platform specific data.
+ """
+ def __init__(self,
+ savegame_type:SavegameType,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ GObject.__init__(self)
+ self.__savegame_type = savegame_type
+ self.__savegame_root = savegame_root
+ self.__savegame_dir = savegame_dir
+ self.__variables = {}
+ self.__filematch = []
+ self.__ignorematch = []
+
+ if variables is not None:
+ variables.update(variables)
+
+ if file_match is not None:
+ for fm in file_match:
+ self.add_file_match(fm)
+
+ if ignore_match is not None:
+ for fm in ignore_match:
+ self.add_ignore_match(fm)
+
+ @Property
+ def savegame_type(self)->SavegameType:
+ """
+ :attr: savegame_type
+ :brief: Type of the class.
+ """
+ return self.__savegame_type
+
+ @Property(type=str)
+ def savegame_root(self)->str:
+ """
+ :attr: savegame_root
+ """
+ return self.__savegame_root
+
+ @savegame_root.setter
+ def savegame_root(self,sgroot:str):
+ self.__savegame_root = sgroot
+
+ @Property
+ def savegame_dir(self)->str:
+ """
+ :attr: savegame_dir
+ """
+ return self.__savegame_dir
+
+ @savegame_dir.setter
+ def savegame_dir(self,sgdir:str):
+ self.__savegame_dir = sgdir
+
+ @Property
+ def variables(self):
+ return self.__variables
+
+ @Property
+ def file_match(self):
+ return self.__filematch
+
+ @Property
+ def ignore_match(self):
+ return self.__ignorematch
+
+ def has_variable(self,name:str)->bool:
+ return (name in self.__variables)
+
+ def get_variable(self,name:str)->str:
+ if name not in self.__variables:
+ return ""
+ return self.__variables[name]
+
+ def set_variable(self,name:str,value:str):
+ self.__variables[name] = value
+
+ def delete_variable(self,name:str):
+ if name in self.__variables:
+ del self.__variables[name]
+
+ def get_variables(self):
+ return self.variables
+
+ def match_file(self,rel_filename:str):
+ if not self.__filematch:
+ return True
+
+ for fm in self.__filematch:
+ if fm.match(rel_filename):
+ return True
+ return False
+
+
+ def match_ignore(self,rel_filename:str):
+ if not self.__ignorematch:
+ return False
+
+ for fm in self.__ignorematch:
+ if fm.match(rel_filename):
+ return True
+ return False
+
+ def match(self,rel_filename:str):
+ if self.match_file(rel_filename) and not self.match_ignore(rel_filename):
+ return True
+ return False
+
+ def add_file_match(self,matcher:GameFileMatcher):
+ if not isinstance(matcher,GameFileMatcher):
+ raise TypeError("matcher is not a \"GameFileMatcher\" instance!")
+ self.__filematch.append(matcher)
+
+ def remove_file_match(self,matcher:GameFileMatcher):
+ for i in reversed(range(len(self.__filematch))):
+ if (matcher == self.__filematch[i]):
+ del self.__filematch[i]
+
+ def add_ignore_match(self,matcher:GameFileMatcher):
+ if not isinstance(matcher,GameFileMatcher):
+ raise TypeError("matcher is not a \"GameFileMatcher\" instance!")
+ self.__ignorematch.append(matcher)
+
+ def remove_ignore_match(self,matcher:GameFileMatcher):
+ for i in reversed(range(len(self.__ignorematch))):
+ if (matcher == self.__ignorematch[i]):
+ del self.__ignorematch[i]
+
+ def serialize(self)->dict:
+ ret = {
+ 'savegame_root': self.savegame_root,
+ 'savegame_dir': self.savegame_dir,
+ }
+ if (self.__variables):
+ ret['variables'] = self.variables
+ if (self.file_match):
+ fm = []
+ for matcher in self.file_match:
+ fm.append({'type':matcher.match_type.value,'match':matcher.match_file})
+ ret['file_match'] = fm
+
+ if (self.add_ignore_match):
+ im = []
+ for matcher in self.ignore_match:
+ im.append({'type':matcher.match_type.value,'match':matcher.match_file})
+ ret['ignore_match'] = im
+
+ return ret
+
+class WindowsGame(GameData):
+ def __init__(self,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ installdir:str|None=None,
+ game_registry_keys:str|list|None=None,
+ installdir_registry_keys:str|list|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+
+ GameData.__init__(self,
+ SavegameType.WINDOWS,
+ savegame_root,
+ savegame_dir,
+ variables,
+ file_match,
+ ignore_match)
+ if not installdir:
+ self.__installdir = None
+ else:
+ self.__installdir = installdir
+
+ if not game_registry_keys:
+ self.__game_registry_keys = []
+ elif isinstance(game_registry_keys,str):
+ self.__game_registry_keys = [game_registry_keys]
+ else:
+ self.__game_registry_keys = list(game_registry_keys)
+
+ if not installdir_registry_keys:
+ self.__installdir_registry_keys = []
+ elif isinstance(installdir_registry_keys,str):
+ self.__installdir_registry_keys = [installdir_registry_keys]
+ else:
+ self.__installdir_registry_keys = list(installdir_registry_keys)
+
+ def __get_hkey(self,regkey:str):
+ regvec = regkey.split("\\")
+ hkstr = regvec[0]
+ if (hkstr == 'HKLM' or hkstr == 'HKEY_LOCAL_MACHINE'):
+ return winreg.HKEY_LOCAL_MACHINE
+ elif (hkstr == 'HKCU' or hkstr == 'HKEY_CURRENT_USER'):
+ return winreg.HKEY_CURRENT_USER
+ elif (hkstr == 'HKCC' or hkstr == 'HKEY_CURRENT_CONFIG'):
+ return winreg.HKEY_CURRENT_CONFIG
+ elif (hkstr == 'HKCR' or hkstr == 'HKEY_CLASSES_ROOT'):
+ return winreg.HKEY_CLASSES_ROOT
+ elif (hkstr == 'HKU' or hkstr == 'HKEY_USERS'):
+ return winreg.HKEY_USERS
+ return None
+
+ @Property
+ def installdir(self)->str|None:
+ return self.__installdir
+
+ @installdir.setter
+ def installdir(self,installdir:str|None):
+ self.__installdir = installdir
+
+ @Property
+ def game_registry_keys(self)->list:
+ return self.__game_registry_keys
+
+ @Property
+ def installdir_registry_keys(self)->list:
+ return self.__installdir_registry_keys
+
+ @Property
+ def is_installed(self)->bool|None:
+ if not PLATFORM_WIN32 or not self.game_registry_keys:
+ return None
+ for regkey in self.__game_registry_keys:
+ hkey = self.__get_hkey(regkey)
+ regvec = regkey.split('\\')
+ if (regvec > 1):
+ key = '\\'.join(regvec[1:])
+ try:
+ rkey = winreg.OpenKeyEx(hkey,key)
+ winreg.CloseKey(rkey)
+ return True
+ except OSError as ex:
+ continue
+
+ return False
+
+ @Property
+ def registry_installdir(self)->str|None:
+ if not PLATFORM_WIN32 or not (self.installdir_registry_keys):
+ return None
+ for regkey in self.__game_registry_keys:
+ hkey = self.__get_hkey(regkey)
+ regvec = regkey.split('\\')
+ if (regvec > 2):
+ key = '\\'.join(regvec[1:-1])
+ try:
+ rkey = None
+ rkey = winreg.OpenKeyEx(hkey,key)
+ retval = winreg.QueryValue(rkey,regvec[-1])
+ winreg.CloseKey(rkey)
+ if retval:
+ return str(retval[0])
+ except OSError as ex:
+ if (rkey):
+ winreg.CloseKey(rkey)
+ continue
+ return None
+
+ def get_variables(self):
+ variables = super().get_variables()
+ variables["INSTALLDIR"] = self.installdir if self.installdir else ""
+
+ def serialize(self):
+ ret = super().serialize()
+ if (self.installdir):
+ ret['installdir'] = self.installdir
+ if (self.game_registry_keys):
+ ret['game_registry_keys'] = self.game_registry_keys
+ if (self.installdir_registry_keys):
+ ret['installdir_registry_keys'] = self.installdir_registry_keys
+ return ret
+
+class LinuxGame(GameData):
+ def __init__(self,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ binary:str|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ GameData.__init__(self,
+ SavegameType.LINUX,
+ savegame_root,
+ savegame_dir,
+ variables,
+ file_match,
+ ignore_match)
+ self.__binary = binary
+
+ @Property
+ def binary(self)->str|None:
+ return self.__binary
+ @binary.setter
+ def binary(self,bin:str):
+ self.__binary = bin
+
+ @Property
+ def is_installed(self)->bool|None:
+ if PLATFORM_LINUX and self.binary:
+ return bool(GLib.find_program_in_path(self.binary))
+ else:
+ return None
+
+ def serialize(self):
+ ret = super().serialize()
+ if self.binary:
+ ret['binary'] = self.binary
+ return ret
+
+
+class MacOSGame(GameData):
+ def __init__(self,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ binary:str|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ GameData.__init__(self,
+ SavegameType.MACOS,
+ savegame_root,
+ savegame_dir,
+ variables,
+ file_match,
+ ignore_match)
+ self.__binary = binary
+
+ @Property
+ def binary(self)->str|None:
+ return self.__binary
+ @binary.setter
+ def binary(self,bin:str):
+ self.__binary = bin
+
+ @Property
+ def is_installed(self)->bool|None:
+ if PLATFORM_LINUX and self.binary:
+ return bool(GLib.find_program_in_path(self.binary))
+ else:
+ return None
+
+ def serialize(self):
+ ret = super().serialize()
+ if self.binary:
+ ret['binary'] = self.binary
+ return ret
+
+
+class SteamGame(GameData):
+ def __init__(self,
+ sgtype:SavegameType,
+ appid:int,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ installdir:str|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ if sgtype not in (SavegameType.STEAM_WINDOWS,
+ SavegameType.STEAM_LINUX,
+ SavegameType.STEAM_MACOS):
+ raise TypeError("SaveGameType")
+
+ GameData.__init__(self,
+ sgtype,
+ appid,
+ savegame_root,
+ savegame_dir,
+ variables,
+ file_match,
+ ignore_match)
+ self.__installdir = installdir
+
+ def get_variables(self):
+ vars = super().get_variables()
+ vars["INSTALLDIR"] = self.installdir if self.installdir else ""
+
+ @Property
+ def installdir(self):
+ return self.__installdir
+ @installdir.setter
+ def installdir(self,installdir:str|None):
+ self.__installdir = installdir
+
+ def serialize(self):
+ ret = super().serialize()
+ ret['appid'] = self.appid
+
+ if self.installdir:
+ ret['installdir'] = self.installdir
+
+ return ret
+
+class SteamWindowsGame(SteamGame):
+ def __init__(self,
+ appid:int,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ installdir:str|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ SteamGame.__init__(self,
+ SavegameType.STEAM_WINDOWS,
+ appid,
+ savegame_root,
+ savegame_dir,
+ variables,
+ installdir,
+ file_match,
+ ignore_match)
+
+class SteamLinuxGame(SteamGame):
+ def __init__(self,
+ appid:int,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ installdir:str|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ SteamGame.__init__(self,
+ SavegameType.STEAM_LINUX,
+ appid,
+ savegame_root,
+ savegame_dir,
+ variables,
+ installdir,
+ file_match,
+ ignore_match)
+
+class SteamMacOSGame(SteamGame):
+ def __init__(self,
+ appid:int,
+ savegame_root:str,
+ savegame_dir:str,
+ variables:dict|None=None,
+ installdir:str|None=None,
+ file_match:list|None=None,
+ ignore_match:list|None=None):
+ SteamGame.__init__(self,
+ SavegameType.STEAM_MACOS,
+ appid,
+ savegame_root,
+ savegame_dir,
+ variables,
+ installdir,
+ file_match,
+ ignore_match)
+
+
+class Game(GObject):
+ @staticmethod
+ def new_from_dict(config:str):
+ logger = logger.getChild("Game.new_from_dict()")
+
+ def get_file_match(conf:dict):
+ conf_fm = conf['file_match'] if 'file_match' in conf else None
+ conf_im = conf['ignore_match'] if 'ignore_match' in conf else None
+
+ if (conf_fm):
+ file_match = []
+ for cfm in conf_fm:
+ if ('type' in cfm and 'match' in cfm):
+ try:
+ file_match.append(GameFileMatcher(GameFileType.from_string(cfm['type'],cfm['match'])))
+ except Exception as ex:
+ logger.error("Adding GameFileMatcher to file_match failed! ({})!".format(ex))
+ else:
+ logger.error("Illegal file_match settings! (\"type\" or \"match\" missing!)")
+
+ else:
+ file_match = None
+
+ if (conf_im):
+ ignore_match = []
+ for cim in conf_im:
+ if ('type' in cim and 'match' in cim):
+ try:
+ file_match.append(GameFileMatcher(GameFileType.from_string(cim['type'],cim['match'])))
+ except Exception as ex:
+ logger.error("Adding GameFileMatcher to ignore_match failed! ({})!".format(ex))
+ else:
+ logger.error("Illegal ignore_match settings! (\"type\" or \"match\" missing!)")
+ else:
+ ignore_match = None
+
+ return (file_match,ignore_match)
+
+ def new_steam_game(conf,cls:SteamGame):
+ appid = conf['appid'] if 'appid' in conf else None
+ sgroot = conf['savegame_root'] if 'savegame_root' in conf else None
+ sgdir = conf['savegame_dir'] if 'savegame_dir' in conf else None
+ vars = conf['variables'] if 'variables' in conf else None
+ installdir = conf['installdir'] if 'installdir' in conf else None
+ file_match,ignore_match = get_file_match(conf)
+
+ if appid is not None and sgroot and sgdir:
+ cls(appid,sgroot,sgdir,vars,installdir,file_match,ignore_match)
+ return None
+ # new_steam_game()
+
+ if not 'id' in config or not 'name' in config:
+ return None
+
+ id = config['id']
+ name = config['name']
+ sgname = config['savegame_name'] if 'savegame_name' in config else id
+ sgtype = config['savegame_type'] if 'savegame_type' in config else SavegameType.UNSET
+
+ game = Game(id,name,sgname)
+ game.savegame_type = sgtype
+ game.is_active = config['is_active'] if 'is_active' in config else False
+ game.is_live = config['is_live'] if 'is_live' in config else True
+
+ if 'windows' in config:
+ winconf = config['windows']
+ sgroot = winconf['savegame_root'] if 'savegame_root' in winconf else None
+ sgdir = winconf['savegame_dir'] if 'savegame_dir' in winconf else None
+ vars = winconf['variables'] if 'variables' in winconf else None
+ installdir = winconf['installdir'] if 'installdir' in winconf else None
+ game_regkeys = winconf['game_registry_keys'] if 'game_registry_keys' in winconf else None
+ installdir_regkeys = winconf['installdir_registry_keys'] if 'installdir_registry_keys' in winconf else None
+ file_match,ignore_match = get_file_match(winconf)
+ if (sgroot and sgdir):
+ game.windows = WindowsGame(sgroot,
+ sgdir,
+ vars,
+ installdir,
+ game_regkeys,
+ installdir_regkeys,
+ file_match,
+ ignore_match)
+
+ if 'linux' in config:
+ linconf = config['linux']
+ sgroot = linconf['savegame_root'] if 'savegame_root' in linconf else None
+ sgdir = linconf['savegame_dir'] if 'savegame_dir' in linconf else None
+ vars = linconf['variables'] if 'variables' in linconf else None
+ binary = linconf['binary'] if 'binary' in linconf else None
+ file_match,ignore_match = get_file_match(linconf)
+ if (sgroot and sgdir):
+ game.linux = LinuxGame(sgroot,sgdir,vars,binary,file_match,ignore_match)
+
+ if 'macos' in config:
+ macconf = config['macos']
+ sgroot = macconf['savegame_root'] if 'savegame_root' in macconf else None
+ sgdir = macconf['savegame_dir'] if 'savegame_dir' in macconf else None
+ vars = macconf['variables'] if 'variables' in macconf else None
+ binary = macconf['binary'] if 'binary' in macconf else None
+ file_match,ignore_match = get_file_match(macconf)
+ if (sgroot and sgdir):
+ game.macos = MacOSGame(sgroot,sgdir,vars,binary,file_match,ignore_match)
+
+ if 'steam_windows' in config:
+ game.steam_windows = new_steam_game(config['steam_windows'],SteamWindowsGame)
+ if 'steam_linux' in config:
+ game.steam_linux = new_steam_game(config['steam_linux'],SteamLinuxGame)
+ if 'steam_macos' in config:
+ game.steam_macos = new_steam_game(config['steam_macos'],SteamMacOSGame)
+
+ return game
+
+ @staticmethod
+ def new_from_json_file(filename:str):
+ if not os.path.isfile(filename):
+ raise FileNotFoundError("Filename \"{filename}\" not found!".format(filename=filename))
+ with open(filename,'rt',encoding="UTF-8") as ifile:
+ return Game.new_from_dict(json.loads(ifile.read()))
+
+ def __init__(self,id:str,name:str,savegame_name:str):
+ GObject.__init__(self)
+ self.__id = id
+ self.__name = name
+ self.__filename = None
+ self.__savegame_name = savegame_name
+ self.__savegame_type = SavegameType.UNSET
+ self.__active = False
+ self.__live = True
+
+ self.__windows = None
+ self.__linux = None
+ self.__macos = None
+ self.__steam_windows = None
+ self.__steam_linux = None
+ self.__steam_macos = None
+ self.__gog_windows = None
+ self.__gog_linux = None
+ self.__epic_windows = None
+ self.__epic_linux = None
+
+ @Property(type=str)
+ def id(self)->str:
+ return self.__id
+ @id.setter
+ def id(self,id:str):
+ self.__id = id
+
+
+ @Property(type=str)
+ def name(self)->str:
+ return self.__name
+ @name.setter
+ def name(self,name:str):
+ self.__name = name
+
+ @Property(type=str)
+ def savegame_name(self)->str:
+ return self.__savegame_name
+ @savegame_name.setter
+ def savegame_name(self,sgname:str):
+ self.__savegame_name = sgname
+
+ @Property
+ def savegame_type(self)->SavegameType:
+ return self.__savegame_type
+ @savegame_type.setter
+ def savegame_type(self,sgtype:SavegameType):
+ self.__savegame_type = sgtype
+
+ @Property(type=bool,default=False)
+ def is_active(self)->bool:
+ return self.__active
+ @is_active.setter
+ def is_active(self,active:bool):
+ self.__active = bool(active)
+
+ @Property(type=bool,default=True)
+ def is_live(self)->bool:
+ return self.__live
+ @is_live.setter
+ def is_live(self,live:bool):
+ self.__live = bool(live)
+
+ @Property
+ def filename(self)->str|None:
+ if not self.__id:
+ return None
+
+ if not self.__filename:
+ GLib.build_filename(settings.gameconf_dir,'.'.join((self.id,'gameconf')))
+
+ return self.__filename
+ @filename.setter
+ def filename(self,fn:str):
+ if not os.path.isabs(fn):
+ self.__filename = GLib.build_filename(settings.gameconf_dir,fn)
+ else:
+ self.__filename = fn
+
+ @Property
+ def game_data(self):
+ sgtype = self.savegame_type
+ if (sgtype == SavegameType.WINDOWS):
+ return self.windows
+ elif (sgtype == SavegameType.LINUX):
+ return self.linux
+ elif (sgtype == SavegameType.MACOS):
+ return self.macos
+ elif (sgtype == SavegameType.STEAM_WINDOWS):
+ return self.steam_windows
+ elif (sgtype == SavegameType.STEAM_LINUX):
+ return self.steam_linux
+ elif (sgtype == SavegameType.STEAM_MACOS):
+ return self.steam_macos
+ elif (sgtype == SavegameType.GOG_WINDOWS):
+ return self.__gog_windows
+ elif (sgtype == SavegameType.GOG_LINUX):
+ return self.__gog_linux
+ elif (sgtype == SavegameType.EPIC_WINDOWS):
+ return self.__epic_windows
+ elif (sgtype == SavegameType.EPIC_LINUX):
+ return self.__epic_linux
+ return None
+
+ @Property
+ def windows(self)->WindowsGame|None:
+ return self.__windows
+ @windows.setter
+ def windows(self,data:WindowsGame|None):
+ if not data:
+ self.__windows = None
+ else:
+ if not isinstance(data,WindowsGame):
+ raise TypeError("WindowsGame")
+ self.__windows = data
+
+ @Property
+ def linux(self)->LinuxGame|None:
+ return self.__linux
+ @linux.setter
+ def linux(self,data:LinuxGame):
+ if not data:
+ self.__linux = None
+ else:
+ if not isinstance(data,LinuxGame):
+ raise TypeError("LinuxGame")
+ self.__linux = data
+
+ @Property
+ def macos(self)->MacOSGame|None:
+ return self.__macos
+ @macos.setter
+ def macos(self,data:MacOSGame|None):
+ if not data:
+ self.__macos = None
+ else:
+ if not isinstance(data,MacOSGame):
+ raise TypeError("MacOSGame")
+ self.__macos = data
+
+ @Property
+ def steam_windows(self)->SteamWindowsGame|None:
+ return self.__steam_windows
+ @steam_windows.setter
+ def steam_windows(self,data:SteamWindowsGame|None):
+ if not data:
+ self.__steam_windows = None
+ else:
+ if not isinstance(data,SteamWindowsGame):
+ raise TypeError("SteamWindowsGame")
+ self.__steam_windows = data
+
+ @Property
+ def steam_linux(self)->SteamLinuxGame|None:
+ return self.__steam_linux
+ @steam_linux.setter
+ def steam_windows(self,data:SteamLinuxGame|None):
+ if not data:
+ self.__steam_linux = None
+ else:
+ if not isinstance(data,SteamLinuxGame):
+ raise TypeError("SteamWindowsGame")
+ self.__steam_linux = data
+
+ @Property
+ def steam_macos(self)->SteamMacOSGame|None:
+ return self.__steam_macos
+ @steam_macos.setter
+ def steam_macos(self,data:SteamMacOSGame|None):
+ if not data:
+ self.__steam_macos = None
+ else:
+ if not isinstance(data,SteamMacOSGame):
+ raise TypeError("SteamWindowsGame")
+ self.__steam_macos = data
+
+ def serialize(self)->dict:
+ ret = {
+ 'id': self.id,
+ 'name': self.name,
+ 'savegame_name': self.savegame_name,
+ 'savegame_type': self.savegame_type.value,
+ 'is_active': self.is_active,
+ 'is_live': self.is_live,
+ }
+
+ if (self.windows):
+ ret['windows'] = self.windows.serialize()
+ if (self.linux):
+ ret['linux'] = self.linux.serialize()
+ if (self.macos):
+ ret['macos'] = self.macos.serialize()
+ if (self.steam_windows):
+ ret['steam_windows'] = self.steam_windows.serialize()
+ if (self.steam_linux):
+ ret['steam_linux'] = self.steam_linux.serialize()
+ if (self.steam_macos):
+ ret['steam_macos'] = self.steam_macos.serialize()
+ #if self.gog_windows:
+ # ret['gog_windows'] = self.gog_windows.serialize()
+ #if self.gog_linux:
+ # ret['gog_linux'] = self.gog_linux.serialize()
+ #if self.epic_windows:
+ # ret['epic_windows'] = self.epic_windows.serialize()
+ #if self.epic_linux:
+ # ret['epic_linux'] = self.epic_linux.serialize()
+
+ return ret
+
+ def save(self):
+ data = self.serialize()
+ old_path = pathlib.Path(self.filename).resolve()
+ new_path = pathlib.Path(settings.gameconf_dir / '.'.join(self.id,'gameconf')).resolve()
+ if (str(old_path) != str(new_path)) and old_path.is_file():
+ os.unlink(old_path)
+ if not new_path.parent.is_dir():
+ os.makedirs(new_path.parent)
+
+ with open(new_path,'wt',encoding='UTF-8') as ofile:
+ ofile.write(json.dumps(self.serialize(),ensure_ascii=False,indent=4))
+
+GAMES={}
+STEAM_GAMES={}
+STEAM_LINUX_GAMES={}
+STEAM_WINDOWS_GAMES={}
+STEAM_MACOS_GAMES={}
+
+def __init_games():
+ gameconf_dir = settings.gameconf_dir
+ if not os.path.isdir(gameconf_dir):
+ return
+
+ for gcf in (os.path.join(gameconf_dir,i) for i in os.path.listdir(gameconf_dir)):
+ if not os.path.isfile(gcf) or not gcf.endswith('.gameconf'):
+ continue
+
+ try:
+ game = Game.new_from_json_file(gcf)
+ if not game:
+ continue
+ except:
+ continue
+
+ GAMES[game.id] = game
+ if (game.steam_windows):
+ if not game.steam_windows.appid in STEAM_GAMES:
+ STEAM_GAMES[game.steam_windows.appid] = game
+ STEAM_WINDOWS_GAMES[game.steam_windows.appid] = game
+ if (game.steam_linux):
+ if not game.steam_linux.appid in STEAM_GAMES:
+ STEAM_GAMES[game.steam_linux.appid] = game
+ STEAM_LINUX_GAMES[game.steam_linux.appid] = game
+ if (game.steam_macos):
+ if not game.steam_macos.appid in STEAM_GAMES:
+ STEAM_GAMES[game.steam_macos.appid] = game
+ STEAM_MACOS_GAMES[game.steam_macos.appid] = game
+__init_games()
+
+def add_game(game:Game):
+ GAMES[game.id] = game
+ if game.steam_windows:
+ if not game.steam_windows.appid in STEAM_GAMES:
+ STEAM_GAMES[game.steam_windows.appid] = game
+ STEAM_WINDOWS_GAMES[game.steam_windows.appid] = game
+ if (game.steam_linux):
+ if not game.steam_linux.appid in STEAM_GAMES:
+ STEAM_GAMES[game.steam_linux.appid] = game
+ STEAM_LINUX_GAMES[game.steam_linux.appid] = game
+ if (game.steam_macos):
+ if not game.steam_macos.appid in STEAM_GAMES:
+ STEAM_GAMES[game.steam_macos.appid] = game
+ STEAM_MACOS_GAMES[game.steam_macos.appid] = game
\ No newline at end of file
diff --git a/sgbackup/gui/__init__.py b/sgbackup/gui/__init__.py
new file mode 100644
index 0000000..9049356
--- /dev/null
+++ b/sgbackup/gui/__init__.py
@@ -0,0 +1,18 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+from .application import Application
diff --git a/sgbackup/gui/__main__.py b/sgbackup/gui/__main__.py
new file mode 100644
index 0000000..d96b0f8
--- /dev/null
+++ b/sgbackup/gui/__main__.py
@@ -0,0 +1,24 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+import sys
+from ..main import gui_main
+
+if __name__ == '__main__':
+ sys.exit(gui_main())
+
\ No newline at end of file
diff --git a/sgbackup/gui/application.py b/sgbackup/gui/application.py
new file mode 100644
index 0000000..26075df
--- /dev/null
+++ b/sgbackup/gui/application.py
@@ -0,0 +1,83 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from gi.repository import Gtk,GObject,Gio
+from .appwindow import AppWindow
+
+import logging; logger=logging.getLogger(__name__)
+
+class Application(Gtk.Application):
+ def __init__(self,*args,**kwargs):
+ AppFlags = Gio.ApplicationFlags
+ kwargs['application_id'] = 'org.sgbackup.sgbackup'
+ kwargs['flags'] = AppFlags.FLAGS_NONE
+ Gtk.Application.__init__(self,*args,**kwargs)
+
+ self.__logger = logger.getChild('Application')
+ self.__builder = None
+ self.__appwindow = None
+
+ @property
+ def _logger(self):
+ return self.__logger
+
+ @GObject.Property
+ def appwindow(self):
+ return self.__appwindow
+
+ def do_startup(self):
+ self._logger.debug('do_startup()')
+ if not self.__builder:
+ self.__builder = Gtk.Builder.new()
+
+ Gtk.Application.do_startup(self)
+
+ action_about = Gio.SimpleAction.new('about',None)
+ action_about.connect('activate',self.on_action_about)
+ self.add_action(action_about)
+
+ action_quit = Gio.SimpleAction.new('quit',None)
+ action_quit.connect('activate',self.on_action_quit)
+ self.add_action(action_quit)
+
+ action_settings = Gio.SimpleAction.new('settings',None)
+ action_settings.connect('activate',self.on_action_settings)
+ self.add_action(action_settings)
+
+ # add accels
+ self.set_accels_for_action('app.quit',["q"])
+
+ @GObject.Property
+ def builder(self):
+ return self.__builder
+
+ def do_activate(self):
+ self._logger.debug('do_activate()')
+ if not (self.__appwindow):
+ self.__appwindow = AppWindow(application=self)
+
+ self.appwindow.present()
+
+ def on_action_about(self,action,param):
+ pass
+
+ def on_action_settings(self,action,param):
+ pass
+
+ def on_action_quit(self,action,param):
+ self.quit()
\ No newline at end of file
diff --git a/sgbackup/gui/appmenu.ui b/sgbackup/gui/appmenu.ui
new file mode 100644
index 0000000..acb558e
--- /dev/null
+++ b/sgbackup/gui/appmenu.ui
@@ -0,0 +1,32 @@
+
+
+
+
\ No newline at end of file
diff --git a/sgbackup/gui/appwindow.py b/sgbackup/gui/appwindow.py
new file mode 100644
index 0000000..cec6ced
--- /dev/null
+++ b/sgbackup/gui/appwindow.py
@@ -0,0 +1,86 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from gi.repository import Gtk,Gio,GObject
+
+import os
+from .gameview import GameView
+from .backupview import BackupView
+
+class AppWindow(Gtk.ApplicationWindow):
+ def __init__(self,application=None,**kwargs):
+ kwargs['title'] = "SGBackup"
+
+ if (application is not None):
+ kwargs['application']=application
+ if (hasattr(application,'builder')):
+ builder = application.builder
+ else:
+ builder = Gtk.Builder.new()
+
+ Gtk.ApplicationWindow.__init__(self,**kwargs)
+ self.set_default_size(800,600)
+
+ self.__builder = builder
+ self.builder.add_from_file(os.path.join(os.path.dirname(__file__),'appmenu.ui'))
+ gmenu = self.builder.get_object('appmenu')
+ appmenu_popover = Gtk.PopoverMenu.new_from_model(gmenu)
+ image = Gtk.Image.new_from_icon_name('open-menu-symbolic')
+ menubutton = Gtk.MenuButton.new()
+ menubutton.set_popover(appmenu_popover)
+ menubutton.set_child(image)
+ headerbar = Gtk.HeaderBar.new()
+ headerbar.pack_start(menubutton)
+ self.set_titlebar(headerbar)
+
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+ self.__vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
+ self.__vpaned.set_hexpand(True)
+ self.__vpaned.set_vexpand(True)
+ self.__vpaned.set_wide_handle(True)
+ self.__gameview = GameView()
+ self.__vpaned.set_start_child(self.gameview)
+ self.__backupview = BackupView(self.gameview)
+ self.__vpaned.set_end_child(self.backupview)
+ self.__vpaned.set_resize_start_child(True)
+ self.__vpaned.set_resize_end_child(True)
+
+ vbox.append(self.__vpaned)
+
+ statusbar = Gtk.Statusbar()
+ statusbar.set_hexpand(True)
+ statusbar.set_vexpand(False)
+ statusbar.push(0,'Running ...')
+ vbox.append(statusbar)
+
+ self.set_child(vbox)
+
+ @GObject.Property
+ def builder(self):
+ return self.__builder
+
+ @GObject.Property
+ def backupview(self):
+ return self.__backupview
+
+ @GObject.Property
+ def gameview(self):
+ return self.__gameview
+
+
\ No newline at end of file
diff --git a/sgbackup/gui/backupview.py b/sgbackup/gui/backupview.py
new file mode 100644
index 0000000..1afec6d
--- /dev/null
+++ b/sgbackup/gui/backupview.py
@@ -0,0 +1,30 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from gi.repository import Gtk,Gio,GObject
+
+from .gameview import GameView
+
+class BackupView(Gtk.ScrolledWindow):
+ def __init__(self,gameview:GameView):
+ Gtk.ScrolledWindow.__init__(self)
+ self.__gameview = GameView
+
+ @GObject.Property
+ def gameview(self):
+ return self.__gameview
diff --git a/sgbackup/gui/gameview.py b/sgbackup/gui/gameview.py
new file mode 100644
index 0000000..1f4b623
--- /dev/null
+++ b/sgbackup/gui/gameview.py
@@ -0,0 +1,24 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from gi.repository import Gtk,Gio,GObject
+
+class GameView(Gtk.ScrolledWindow):
+ def __init__(self):
+ Gtk.ScrolledWindow.__init__(self)
+
\ No newline at end of file
diff --git a/sgbackup/gui/settingsdialog.py b/sgbackup/gui/settingsdialog.py
new file mode 100644
index 0000000..35212c4
--- /dev/null
+++ b/sgbackup/gui/settingsdialog.py
@@ -0,0 +1,17 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
diff --git a/sgbackup/logger.conf b/sgbackup/logger.conf
new file mode 100644
index 0000000..6c7d75f
--- /dev/null
+++ b/sgbackup/logger.conf
@@ -0,0 +1,40 @@
+[loggers]
+keys=root,console,file
+
+[handlers]
+keys=consoleHandler,fileHandler
+
+[formatters]
+keys=consoleFormatter,fileFormatter
+
+[logger_root]
+level=DEBUG
+handlers=consoleHandler,fileHandler
+
+[logger_console]
+level=DEBUG
+handlers=consoleHandler
+propagate=0
+qualname=console
+
+[logger_file]
+level=DEBUG
+handlers=fileHandler
+propagate=0
+qualname=file
+
+[handler_consoleHandler]
+class=StreamHandler
+formatter=consoleFormatter
+level=DEBUG
+
+[handler_fileHandler]
+class=handlers.RotatingFileHandler
+args=('sgbackup.log','a',10485760,10,'UTF-8',False,)
+formatter=fileFormatter
+
+[formatter_consoleFormatter]
+format=[%(levelname)s:%(name)s] %(message)s
+
+[formatter_fileFormatter]
+format=[%(asctime)s-%(levelname)s:%(name)s] %(message)s
diff --git a/sgbackup/logger.yaml b/sgbackup/logger.yaml
new file mode 100644
index 0000000..8892f04
--- /dev/null
+++ b/sgbackup/logger.yaml
@@ -0,0 +1,20 @@
+version: 1
+formatters:
+ console:
+ format: '[%(levelname)s:%(name)s] %(message)s'
+ file:
+ format: '[%(asctime)s - %(levelname)s:%(name)s] %(message)'
+handlers:
+ console:
+ class: logging.StreamHandler
+ level: DEBUG
+ formatter: console
+ stream: 'ext://sys.stdout'
+loggers:
+ console:
+ handlers: [console]
+ level: DEBUG
+ propagate: no
+root:
+ level: DEBUG
+ handlers: [console]
\ No newline at end of file
diff --git a/sgbackup/main.py b/sgbackup/main.py
new file mode 100644
index 0000000..50e2d0c
--- /dev/null
+++ b/sgbackup/main.py
@@ -0,0 +1,42 @@
+#enconding: utf-8
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+import logging
+from . import gui
+from .gui.application import Application
+from .steam import SteamLibrary
+import sys
+
+logger=logging.getLogger(__name__)
+
+
+def cli_main():
+ logger.debug("Running cli_main()")
+ return 0
+
+def curses_main():
+ logger.debug("Running curses_main()")
+ return 0
+
+
+def gui_main():
+ logger.debug("Running gui_main()")
+ gui.app = Application()
+ gui.app.run()
+ return 0
\ No newline at end of file
diff --git a/sgbackup/settings.py b/sgbackup/settings.py
new file mode 100644
index 0000000..f249405
--- /dev/null
+++ b/sgbackup/settings.py
@@ -0,0 +1,85 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+from configparser import ConfigParser
+import os
+import sys
+
+from gi.repository import GLib,GObject
+
+class Settings(GObject.GObject):
+ def __init__(self):
+ super().__init__()
+
+ self.__configparser = ConfigParser()
+ self.__config_dir = os.path.join(GLib.get_user_config_dir(),'sgbackup')
+ self.__gameconf_dir = os.path.join(self.__config_dir,'games')
+ self.__logger_conf = os.path.join(self.__config_dir,'logger.conf')
+
+ self.__config_file = os.path.join(self.__config_dir,'sgbackup.conf')
+ if (os.path.isfile(self.__config_file)):
+ with open(self.__config_file,'r') as conf:
+ self.__configparser.read_file(conf)
+
+ @GObject.Property(nick="parser")
+ def parser(self)->ConfigParser:
+ return self.__configparser
+
+ @GObject.Property(type=str,nick="config-dir")
+ def config_dir(self)->str:
+ return self.__config_dir
+
+ @GObject.Property(type=str,nick="config-file")
+ def config_file(self)->str:
+ return self.__config_file
+
+ @GObject.Property(type=str,nick="gameconf-dir")
+ def gameconf_dir(self)->str:
+ return self.__gameconf_dir
+
+ @GObject.Property(type=str,nick="logger-conf")
+ def logger_conf(self)->str:
+ return self.__logger_conf
+
+
+ @GObject.Property(type=str,nick="backup-dir")
+ def backup_dir(self)->str:
+ if self.parser.has_option('sgbackup','backupDirectory'):
+ return self.parser.get('sgbackup','backupDirectory')
+ return GLib.build_filename(GLib.build_filename(GLib.get_home_dir(),'SavagameBackups'))
+ @backup_dir.setter
+ def backup_dir(self,directory:str):
+ if not os.path.isabs(directory):
+ raise ValueError("\"backup_dir\" needs to be an absolute path!")
+ return self.parser.set('sgbackup','backupDirectory',directory)
+
+ @GObject.Property(type=str)
+ def loglevel(self)->str:
+ if self.parser.has_option('sgbackup','logLevel'):
+ return self.parser.get('sgbackup','logLevel')
+ return "INFO"
+
+ def save(self):
+ self.emit('save')
+
+ @GObject.Signal(name='save',flags=GObject.SIGNAL_RUN_LAST,return_type=None,arg_types=())
+ def do_save(self):
+ with open(self.config_file,'w') as ofile:
+ self.__configparser.write(ofile)
+
+settings = Settings()
diff --git a/sgbackup/steam.py b/sgbackup/steam.py
new file mode 100644
index 0000000..2161f9d
--- /dev/null
+++ b/sgbackup/steam.py
@@ -0,0 +1,296 @@
+###############################################################################
+# sgbackup - The SaveGame Backup tool #
+# Copyright (C) 2024 Christian Moser #
+# #
+# 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 3 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 . #
+###############################################################################
+
+import os
+import re
+from pathlib import Path
+import sys
+import json
+
+from .settings import settings
+
+PLATFORM_WINDOWS = (sys.platform.lower() == 'win32')
+
+
+from gi.repository.GObject import GObject,Property,Signal
+
+
+class AcfFileParser(object):
+ """
+ Parses steam acf files to a dict.
+ """
+ def __init__(self):
+ pass
+
+ def __parse_section(self,lines:list):
+ option_pattern = re.compile("^\"(.+)\"([ \\t]+)\"(.*)\"$")
+ section_pattern = re.compile("^\"(.+)\"")
+ line_count = 0
+ ret = {}
+
+ line_count=0
+ while line_count < len(lines):
+ line = lines[line_count]
+ line_count+=1
+
+ if line == '}':
+ break
+
+ s=line.strip()
+ match = option_pattern.fullmatch(line)
+ if match:
+ name=line[match.start(1):match.end(1)]
+ value=line[match.start(3):match.end(3)]
+ ret[name] = value
+ else:
+ match2 = section_pattern.fullmatch(line)
+ if match2:
+ name=line[match2.start(1):match2.end(1)]
+ if lines[line_count] == '{':
+ line_count += 1
+ n_lines,sect = self.__parse_section(lines[line_count:])
+ line_count += n_lines
+ ret[name]=sect
+
+
+ return line_count,ret
+
+
+
+ def parse_file(self,acf_file)->dict:
+ if not os.path.isfile(acf_file):
+ raise FileNotFoundError("File \"{s}\" does not exist!")
+
+ with open(acf_file,'r') as ifile:
+ buffer = ifile.read()
+ lines = [l.strip() for l in buffer.split('\n')]
+ if lines[0] == "\"AppState\"" and lines[1] == "{":
+ n_lines,sect = self.__parse_section(lines[2:])
+ return sect
+
+ raise RuntimeError("Not a acf file!")
+
+class IgnoreSteamApp(GObject):
+ @staticmethod
+ def new_from_dict(conf:dict):
+ if ('appid' in conf and 'name' in conf):
+ appid = conf['appid']
+ name = conf['name']
+ reason = conf['reason'] if 'reason' in conf else ""
+ return SteamIgnoreApp(appid,name,reason)
+
+ return None
+
+ def __init__(self,appid:int,name:str,reason:str):
+ GObject.__init__(self)
+ self.__appid = int(appid)
+ self.__name = name
+ self.__reason = reason
+
+ @Property(type=int)
+ def appid(self)->str:
+ return self.__appid
+
+ @Property(type=str)
+ def name(self)->str:
+ return self.__name
+ @name.setter
+ def name(self,name:str):
+ self.__name = name
+
+ @Property(type=str)
+ def reason(self)->str:
+ return self.__reason
+ @reason.setter
+ def reason(self,reason:str):
+ self.__reason = reason
+
+
+ def serialize(self):
+ return {
+ 'appid': self.appid,
+ 'name': self.name,
+ 'reason': self.reason,
+ }
+
+class SteamApp(GObject):
+ def __init__(self,appid:int,name:str,installdir:str):
+ GObject.__init__(self)
+ self.__appid = int(appid)
+ self.__name = name
+ self.__installdir = installdir
+
+ @Property(type=int)
+ def appid(self):
+ return self.__appid
+
+ @Property
+ def name(self):
+ return self.__name
+
+ @Property
+ def installdir(self):
+ return self.__installdir
+
+ def __str__(self):
+ return '{}: {}'.format(self.appid,self.name)
+
+ def __gt__(self,other):
+ return self.appid > other.appid
+
+ def __lt__(self,other):
+ return self.appid < other.appid
+
+ def __eq__(self,other):
+ return self.appid == other.appid
+
+
+class SteamLibrary(GObject):
+ def __init__(self,library_path:str):
+ GObject.__init__(self)
+ self.directory = library_path
+
+ @Property(type=str)
+ def directory(self):
+ return self.__directory
+
+ @directory.setter
+ def directory(self,directory:str):
+ if not os.path.isabs(directory):
+ raise ValueError("\"directory\" is not an absolute path!")
+ if not os.path.isdir(directory):
+ raise NotADirectoryError("\"{}\" is not a directory or does not exist!".format(directory))
+
+ self.__directory = directory
+
+ @Property
+ def path(self)->Path:
+ return Path(self.directory).resolve()
+
+ @Property
+ def steam_apps(self)->list:
+ parser = AcfFileParser()
+ appdir = self.path / "steamapps"
+ commondir = appdir / "common"
+
+ ret = []
+ for acf_file in appdir.glob('appmanifest_*.acf'):
+ if not acf_file.is_file():
+ continue
+ try:
+ data = parser.parse_file(str(acf_file))
+ app = SteamApp(data['appid'],data['name'],str(commondir/data['installdir']))
+ ret.append(app)
+ except:
+ pass
+
+ return sorted(ret)
+
+class Steam(GObject):
+ def __init__(self):
+ GObject.__init__(self)
+ self.__libraries = []
+ self.__ignore_apps = []
+
+ if not self.steamlib_list_file.is_file():
+ if (PLATFORM_WINDOWS):
+ libdirs=[
+ "C:\\Program Files (x86)\\steam",
+ "C:\\Program Files\\steam",
+ ]
+ for i in libdirs:
+ if (os.path.isdir(i)):
+ self.__libraries.append(SteamLibrary(i))
+ break
+ else:
+ with open(str(self.steamlib_list_file),'r',encoding="utf-8") as ifile:
+ for line in (i.strip() for i in ifile.readlines()):
+ if not line or line.startswith('#'):
+ continue
+ libdir = Path(line).resolve()
+ if libdir.is_dir():
+ try:
+ self.add_library(str(libdir))
+ except:
+ pass
+
+ ignore_apps = []
+ if self.ignore_apps_file.is_file():
+ with open(str(self.ignore_apps_file),'r',encoding="utf-8") as ifile:
+ ignore_list = json.loads(ifile.read())
+ for ignore in ignore_list:
+ try:
+ ignore_app = IgnoreSteamApp.new_from_dict(ignore)
+ except:
+ continue
+ if ignore_app:
+ self.__ignore_apps.append(ignore_app)
+ self.__ignore_apps = sorted(ignore_apps)
+ #__init__()
+
+ @Property
+ def steamlib_list_file(self)->Path:
+ return Path(settings.config_dir).resolve() / 'steamlib.lst'
+
+ @Property
+ def ignore_apps_file(self)->Path:
+ return Path(settings.config_dir).resolve / 'ignore_steamapps.json'
+
+ @Property
+ def libraries(self):
+ return self.__libraries
+
+ @Property
+ def ignore_apps(self):
+ return self.__ignore_apps
+
+ def __write_steamlib_list_file(self):
+ with open(self.steamlib_list_file,'w',encoding='utf-8') as ofile:
+ ofile.write('\n'.join(str(sl.directory) for sl in self.libraries))
+
+ def add_library(self,steamlib:SteamLibrary|str):
+ if isinstance(steamlib,SteamLibrary):
+ lib = steamlib
+ else:
+ lib = SteamLibrary(steamlib)
+
+ lib_exists = False
+ for i in self.libraries:
+ if i.derctory == lib.directory:
+ lib_exists = True
+ break
+
+ if not lib_exists:
+ self.__libraries.append(lib)
+ self.__write_steamlib_list_file()
+
+ def remove_library(self,steamlib:SteamLibrary|str):
+ if isinstance(steamlib,SteamLibrary):
+ libdir = steamlib.directory
+ else:
+ libdir = str(steamlib)
+
+ delete_libs=[]
+ for i in range(len(self.__libraries)):
+ if self.__libraries[i].directory == libdir:
+ delete_libs.append(i)
+
+ if delete_libs:
+ for i in sorted(delete_libs,reverse=True):
+ del self.__libraries[i]
+ self.__write_steamlib_list_file()
diff --git a/sphinx/conf.py b/sphinx/conf.py
new file mode 100644
index 0000000..75f9607
--- /dev/null
+++ b/sphinx/conf.py
@@ -0,0 +1,44 @@
+# vim: syn=python ts=4 sts=4 sw=4 smartindent autoindent expandtab
+
+import sys,os
+
+PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
+sys.path.insert(0,PROJECT_ROOT)
+import sgbackup
+
+project = 'sgbackup'
+copyright = '2024-2025, Christian Moser'
+author = 'Christian Moser'
+version = sgbackup.__version__
+
+exclude_patterns = [
+ '_build',
+ 'Thumbs.db',
+ '.DS_Store',
+ '*~',
+ '*.swp',
+ '*.tmp',
+ '*.temp',
+ '*.log',
+]
+
+extensions = [
+ 'sphinx.ext.autodoc'
+]
+language = 'en'
+master_doc = 'index'
+source_suffix = '.rst'
+templates_path = ['templates']
+
+html_theme = 'sphinx_rtd_theme'
+html_show_sourcelink = False
+#html_static_path = ['_static']
+
+autoclass_content='both'
+autodoc_class_signature='mixed'
+autodoc_default_options={
+ 'member_order':'alphabetical',
+ 'undoc_members':'true',
+ 'exclude_memebers':'__weakref__',
+}
+
diff --git a/sphinx/index.rst b/sphinx/index.rst
new file mode 100644
index 0000000..b06ebab
--- /dev/null
+++ b/sphinx/index.rst
@@ -0,0 +1,29 @@
+==========================
+sgbackup API documentation
+==========================
+
+.. title:: sgbackup API
+
+Descritpion
+-----------
+
+Sgbackup is a savegame backup tool written in Python. It has a commandline and a graphical
+interface using *Gtk4*.
+
+The minimum Python version is **3.11**.
+
+It has\ *Gtk4*\ and\ *GObject Introspection*\ as a dependency.
+
+Table of Contents
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ install.rst
+
+ modules/sgbackup.rst
+ modules/sgbackup.settings.rst
+ modules/sgbackup.game.rst
+ modules/sgbackup.archiver.rst
+
diff --git a/sphinx/install.rst b/sphinx/install.rst
new file mode 100644
index 0000000..1d11d33
--- /dev/null
+++ b/sphinx/install.rst
@@ -0,0 +1,3 @@
+========================
+Installing sgbackup
+========================
diff --git a/sphinx/modules.rst b/sphinx/modules.rst
new file mode 100644
index 0000000..c79bd17
--- /dev/null
+++ b/sphinx/modules.rst
@@ -0,0 +1,5 @@
+.. title:: Modules
+
+.. toctree:: 2
+ modules/sgbackup
+ modules/game
diff --git a/sphinx/modules/sgbackup.rst b/sphinx/modules/sgbackup.rst
new file mode 100644
index 0000000..aa668e1
--- /dev/null
+++ b/sphinx/modules/sgbackup.rst
@@ -0,0 +1,10 @@
+============
+sgbackup API
+============
+
+.. title:: sgbackup API
+
+
+.. automodule:: sgbackup
+ :imported-mebers:
+ :undoc-members: