Last active
July 1, 2021 21:55
-
-
Save Grum999/b8977a5901283fa54bd79beb5ebd8042 to your computer and use it in GitHub Desktop.
A small python script to execute Krita
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# ------------------------------------------------------------------------------ | |
# 2021 - Grum999 | |
# ------------------------------------------------------------------------------ | |
# 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 https://www.gnu.org/licenses/ | |
# ------------------------------------------------------------------------------ | |
# This script allows to execute Krita 4/Krita 5 with automatic backup/restore | |
# of configuration files | |
# | |
# Note: works on Linux with appimages only | |
# | |
# | |
# 1) Search if a Krita 5 appimage exist, and id many are found, it will | |
# choose the most recent | |
# Note: The executable flag must be set manually on appimage | |
# The script will ignore appimage file without executable flag | |
# | |
# > If appimage is not found, exit script | |
# | |
# | |
# 2) Search for a backup archive matching with current version | |
# If a backup file is found: | |
# Check if backup is valid | |
# > If backup is not valid, exit script (consider user have to check | |
# manually what to do) | |
# | |
# If valid, delete all current configuration files, and restore | |
# configuration from backup | |
# | |
# 3) Execute Krita | |
# | |
# 4) Create a backup | |
# ------------------------------------------------------------------------------ | |
# Usage: | |
# Get help | |
# $ python3 klauncher.py --help | |
# | |
# Launch Krita 4 | |
# $ python3 klauncher.py -k4 | |
# | |
# Launch Krita 5, without any backup (restore/create) | |
# $ python3 klauncher.py -k5 --backup-none | |
# | |
# Launch Krita 5, using backup 'testK5' | |
# $ python3 klauncher.py -k5 --backup-name testK5 | |
# | |
# Get backup list | |
# $ python3 klauncher.py --backup-list | |
# | |
# Create a backup 'test' | |
# $ python3 klauncher.py --backup-create test | |
# Or -bname option can be used to define backup name | |
# $ python3 klauncher.py --backup-name test --backup-create | |
# | |
# Restore backup 'k4' | |
# $ python3 klauncher.py --backup-restore k4 | |
# | |
# Delete backup 'test' | |
# $ python3 klauncher.py --backup-delete test | |
# | |
# Reset current krita configuration | |
# $ python3 klauncher.py --reset-krita-config | |
# | |
# | |
# ------------------------------------------------------------------------------ | |
# Small tricks to simplify command line | |
# | |
# Avoid python3 command | |
# Set script as executable | |
# $ chmod u+x klauncher.py | |
# | |
# Eventually, in .bashrc add script path to PATH variables | |
# export PATH=~/path/to/script:$PATH | |
# And then execute script directly | |
# $ klauncher.py -k5 | |
# | |
# | |
# Use alias | |
# In .bashrc file, define aliases: | |
# alias k4="python3 /path/to/script/klauncher.py -k4" | |
# alias k5="python3 /path/to/script/klauncher.py -k5" | |
# alias k5="python3 /path/to/script/klauncher.py -kl" | |
# | |
# And then just type command | |
# $ k4 | |
# or: | |
# $ k5 | |
# or: | |
# $ kl -bl | |
# | |
# ------------------------------------------------------------------------------ | |
# Configuration | |
# | |
# Path are hardcoded :-) | |
# Just change global variables at begining of script to adapt path to your own | |
# configuration | |
# ------------------------------------------------------------------------------ | |
import argparse | |
import textwrap | |
import os.path | |
import shutil | |
import time | |
import zipfile | |
import re | |
SCRIPT_VERSION = "2.0.0" | |
SCRIPT_DATE="2021-07-01" | |
SCRIPT_NAME="KritaLauncher" | |
# Root path | |
ROOTPATH=os.path.expanduser('~') | |
# Where are stored appimages | |
# > Update APPPATH according to how your directories are organized | |
APPPATH=f'{ROOTPATH}/Applications/Images/Krita' | |
# Define association between line argument value and appimage name pattern | |
# > example for a Krita 4.4.3 and an krita 4.4.4, add keys | |
# 'k443': '^krita-4\.4\.3-x86_64\.appimage', | |
# 'k444': '^krita-4\.4\.4.*x86_64\.appimage', | |
# /!\ Values are regular expression! | |
APPIMG={ | |
'k4': '^krita-4\..*-x86_64\.appimage', | |
'k5': '^krita-5\..*-x86_64\.appimage' | |
} | |
# Directory where backup files are saved | |
# > Update BCKPATH according to how your directories are organized and where you | |
# want to see your backup file | |
BCKPATH=f'{ROOTPATH}/Applications/Images/Krita/backups' | |
# List of files/directories to backup | |
# > Currently the most basic, take everything, but it's possible de define the | |
# exact list of files/directories to backup | |
# Note: | |
# base name is matched as regular expression | |
# '{ROOTPATH}/.config/krita' | |
# > In directory '{ROOTPATH}/.config', archive all files/directory | |
# for which name is starting with 'krita' | |
BCKLST=[ | |
f'{ROOTPATH}/.config/krita', | |
f'{ROOTPATH}/.local/share/krita' | |
] | |
def bytesSizeToStr(value, unit=None, decimals=2): | |
"""Convert a size (given in Bytes) to given unit | |
Given unit can be: | |
- 'auto' | |
- 'autobin' (binary Bytes) | |
- 'GiB', 'MiB', 'KiB' (binary Bytes) | |
- 'GB', 'MB', 'KB', 'B' | |
""" | |
if unit is None: | |
unit = 'autobin' | |
if not isinstance(unit, str): | |
raise Exception('Given `unit` must be a valid <str> value') | |
unit = unit.lower() | |
if not unit in ['auto', 'autobin', 'gib', 'mib', 'kib', 'gb', 'mb', 'kb', 'b']: | |
raise Exception('Given `unit` must be a valid <str> value') | |
if not isinstance(decimals, int) or decimals < 0 or decimals > 8: | |
raise Exception('Given `decimals` must be a valid <int> between 0 and 8') | |
if not (isinstance(value, int) or isinstance(value, float)): | |
raise Exception('Given `value` must be a valid <int> or <float>') | |
if unit == 'autobin': | |
if value >= 1073741824: | |
unit = 'gib' | |
elif value >= 1048576: | |
unit = 'mib' | |
elif value >= 1024: | |
unit = 'kib' | |
else: | |
unit = 'b' | |
elif unit == 'auto': | |
if value >= 1000000000: | |
unit = 'gb' | |
elif value >= 1000000: | |
unit = 'mb' | |
elif value >= 1000: | |
unit = 'kb' | |
else: | |
unit = 'b' | |
fmt = f'{{0:.{decimals}f}}{{1}}' | |
if unit == 'gib': | |
return fmt.format(value/1073741824, 'GiB') | |
elif unit == 'mib': | |
return fmt.format(value/1048576, 'MiB') | |
elif unit == 'kib': | |
return fmt.format(value/1024, 'KiB') | |
elif unit == 'gb': | |
return fmt.format(value/1000000000, 'GB') | |
elif unit == 'mb': | |
return fmt.format(value/1000000, 'MB') | |
elif unit == 'kb': | |
return fmt.format(value/1000, 'KB') | |
else: | |
return f'{value}B' | |
def strToMaxLength(value, maxLength, completeSpace=True, leftAlignment=True): | |
"""Format given string `value` to fit in given `maxLength` | |
If len is greater than `maxLength`, string is splitted with carriage return | |
If value contains carriage return, each line is processed separately | |
If `completeSpace` is True, value is completed with space characters to get | |
the expected length. | |
""" | |
returned = [] | |
if os.linesep in value: | |
rows = value.split(os.linesep) | |
for row in rows: | |
returned.append(strToMaxLength(row, maxLength, completeSpace)) | |
else: | |
textLen = len(value) | |
if textLen < maxLength: | |
if completeSpace: | |
# need to complete with spaces | |
if leftAlignment: | |
returned.append( value + (' ' * (maxLength - textLen))) | |
else: | |
returned.append( (' ' * (maxLength - textLen)) + value ) | |
else: | |
returned.append(value) | |
elif textLen > maxLength: | |
# keep spaces separators | |
tmpWords=re.split('(\s)', value) | |
words=[] | |
# build words list | |
for word in tmpWords: | |
while len(word) > maxLength: | |
words.append(word[0:maxLength]) | |
word=word[maxLength:] | |
if word != '': | |
words.append(word) | |
builtRow='' | |
for word in words: | |
if (len(builtRow) + len(word))<maxLength: | |
builtRow+=word | |
else: | |
returned.append(strToMaxLength(builtRow, maxLength, completeSpace)) | |
builtRow=word | |
if builtRow!='': | |
returned.append(strToMaxLength(builtRow, maxLength, completeSpace)) | |
else: | |
returned.append(value) | |
return os.linesep.join(returned) | |
def tsToStr(value, pattern=None, valueNone=''): | |
"""Convert a timestamp to localtime string | |
If no pattern is provided or if pattern = 'dt' or 'full', return full date/time (YYYY-MM-DD HH:MI:SS) | |
If pattern = 'd', return date (YYYY-MM-DD) | |
If pattern = 't', return time (HH:MI:SS) | |
Otherwise try to use pattern literally (strftime) | |
""" | |
if value is None: | |
return valueNone | |
if pattern is None or pattern.lower() in ['dt', 'full']: | |
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(value)) | |
elif pattern.lower() == 'd': | |
return time.strftime('%Y-%m-%d', time.localtime(value)) | |
elif pattern.lower() == 't': | |
return time.strftime('%H:%M:%S', time.localtime(value)) | |
else: | |
return time.strftime(pattern, time.localtime(value)) | |
def getParams(): | |
"""Initialise command line arguments""" | |
parser = argparse.ArgumentParser(description='Execute Krita & manage configuration backups', | |
#epilog='', | |
formatter_class=argparse.RawTextHelpFormatter) | |
parser.add_argument('-v', '--version', | |
action='version', | |
version=f'{SCRIPT_NAME} v{SCRIPT_VERSION} ({SCRIPT_DATE})' | |
) | |
parser.add_argument('-bname', '--backup-name', | |
action='store', | |
nargs=1, | |
help=textwrap.dedent(f"""\ | |
Use given NAME for backup | |
If not provided, provided Krita version argument ({', '.join(list(APPIMG.keys()))}) is used as backup name | |
"""), | |
metavar='NAME' | |
) | |
groupExecute = parser.add_argument_group('Krita execution', 'Command used to execute Krita') | |
groupBackup = parser.add_argument_group('Backup management', 'Command used to manage backups') | |
groupExecuteExclusive = groupExecute.add_mutually_exclusive_group() | |
for kVersion in list(APPIMG.keys()): | |
groupExecuteExclusive.add_argument(f'-{kVersion}', | |
action="store_true", | |
help=f"Launch Krita {kVersion[1:]}" | |
) | |
groupExecute.add_argument('-dne', '--do-not-execute', | |
action="store_true", | |
help=f"Do not execute Krita, just do backup tasks" | |
) | |
groupExecute.add_argument('-bnr', '--backup-no-restore', | |
action="store_true", | |
help=f"Do not restore backup before execution" | |
) | |
groupExecute.add_argument('-bnc', '--backup-no-creation', | |
action="store_true", | |
help=f"Do not create backup after execution" | |
) | |
groupExecute.add_argument('-bn', '--backup-none', | |
action="store_true", | |
help=textwrap.dedent("""\ | |
Combination of --backup-no-restore and --no-backup-creation arguments | |
. Do not restore backup before execution | |
. Do not create backup after execution | |
""") | |
) | |
groupExecute.add_argument('-rkc', '--reset-krita-config', | |
action="store_true", | |
help=textwrap.dedent("""\ | |
Reset Krita configuration | |
When active, automatically implies --backup-no-restore argument | |
""") | |
) | |
groupBackupExclusive = groupBackup.add_mutually_exclusive_group() | |
groupBackupExclusive.add_argument('-bl', '--backup-list', | |
action='store_true', | |
help=f"List all backups" | |
) | |
groupBackupExclusive.add_argument('-bc', '--backup-create', | |
action='store', | |
nargs='?', | |
help=textwrap.dedent("""\ | |
Create backup with given NAME | |
If no backup name is provided, use backup from -bname parameter | |
"""), | |
# if False => argument not provided | |
# if None => argument provided without value (then use from -bname) | |
# other value => argument provided with value | |
default=False, | |
metavar='NAME' | |
) | |
groupBackupExclusive.add_argument('-bd', '--backup-delete', | |
action='store', | |
nargs='?', | |
help=textwrap.dedent("""\ | |
Delete backup with given NAME | |
If no backup name is provided, use backup from -bname parameter | |
"""), | |
# if False => argument not provided | |
# if None => argument provided without value (then use from -bname) | |
# other value => argument provided with value | |
default=False, | |
metavar='NAME' | |
) | |
groupBackupExclusive.add_argument('-br', '--backup-restore', | |
action='store', | |
nargs='?', | |
help=textwrap.dedent("""\ | |
Restore backup with given NAME | |
If no backup name is provided, use backup from -bname parameter | |
"""), | |
# if False => argument not provided | |
# if None => argument provided without value (then use from -bname) | |
# other value => argument provided with value | |
default=False, | |
metavar='NAME' | |
) | |
return vars(parser.parse_args()) | |
def getAppImage(path, pattern): | |
"""Find most recent appimage matching given pattern""" | |
files=os.listdir(path) | |
fileName=None | |
fileTs=None | |
for fName in files: | |
if re.match(pattern, fName): | |
fName=os.path.join(path, fName) | |
if os.path.isfile(fName) and os.access(fName, os.X_OK): | |
if fileTs is None or os.path.getmtime(fName)>fileTs: | |
fileTs=os.path.getmtime(fName) | |
fileName=fName | |
return fileName | |
def getBackupFile(backupId, backupName): | |
"""Return a dictionary for backup file name for given backup Id | |
if a bakupname is provided, use backup name instead of backupId | |
Example: | |
{ | |
file: '', # full path/filename for backup file | |
exists: False # file exists | |
} | |
""" | |
if not backupName is None: | |
bckId=backupName | |
else: | |
bckId=backupId | |
path=os.path.join(BCKPATH, f"{bckId}.zip") | |
return { | |
'bckId': bckId, | |
'file': path, | |
'exists': os.path.exists(path) | |
} | |
def deleteKritaFiles(): | |
"""Delete all Krita config files""" | |
print('. Cleanup current files') | |
for target in BCKLST: | |
pattern=os.path.basename(target) | |
parent=os.path.dirname(target) | |
for fileName in os.listdir(parent): | |
fullPathFileName=os.path.join(parent, fileName) | |
if re.match(pattern, fileName): | |
if os.path.isdir(fullPathFileName): | |
print(f' . Remove directory: {fullPathFileName}') | |
shutil.rmtree(fullPathFileName, True) | |
elif os.path.isfile(fullPathFileName): | |
print(f' . Remove file: {fullPathFileName}') | |
os.remove(fullPathFileName) | |
def backupRestore(zipFileName): | |
"""Unpack files to right place""" | |
print(f'. Restore backup: {os.path.basename(zipFileName)}') | |
with zipfile.ZipFile(zipFileName, 'r') as archiveFile: | |
try: | |
archiveFile.extractall(ROOTPATH) | |
except Exception as e: | |
print('> Unable to open archive', str(e)) | |
return False | |
return True | |
def backupCreate(zipFileName): | |
"""Create a backup for current files""" | |
print(f'. Create backup: {os.path.basename(zipFileName)}') | |
files=[] | |
for target in BCKLST: | |
pattern=os.path.basename(target) | |
parent=os.path.dirname(target) | |
for fileName in os.listdir(parent): | |
fullPathFileName=os.path.join(parent, fileName) | |
if re.match(pattern, fileName): | |
if os.path.isdir(fullPathFileName): | |
print(f' . Add directory: {fullPathFileName}') | |
for base, dirs, filelist in os.walk(fullPathFileName): | |
for fname in filelist: | |
files.append(os.path.join(base, fname)) | |
elif os.path.isfile(fullPathFileName): | |
print(f' . Add file: {fullPathFileName}') | |
files.append(fullPathFileName) | |
print(f' . Create archive: {zipFileName}') | |
length=len(ROOTPATH)+1 | |
zipFile = zipfile.ZipFile(zipFileName, "w") | |
for file in files: | |
zipFile.write(file, file[length:], compress_type=zipfile.ZIP_STORED) | |
zipFile.close() | |
if not backupCheck(zipFileName): | |
print(f' . Check archive: CRC check failed!') | |
else: | |
print(f' . Check archive: OK') | |
def backupCheck(zipFileName): | |
"""Check if archive is valid, return True if ok otherwise False""" | |
with zipfile.ZipFile(zipFileName, 'r') as archiveFile: | |
if archiveFile.testzip(): | |
# return the first invalid file found | |
return False | |
return True | |
def backupDelete(zipFileName): | |
"""Delete backup file""" | |
print(f'. Delete backup: {os.path.basename(zipFileName)}') | |
try: | |
os.remove(zipFileName) | |
except Exception as e: | |
print('> Unable to delete backup', str(e)) | |
return False | |
return True | |
def backupList(): | |
"""Display list of all backup files""" | |
# build files list | |
files=[] | |
nbChrFName=len('backup id') | |
nbChrTs=len('Date/Time') | |
nbChrFs=len('Size') | |
for fName in os.listdir(BCKPATH): | |
if re.search(r"\.zip$", fName): | |
fpName=os.path.join(BCKPATH, fName) | |
if os.path.isfile(fpName): | |
properties={ | |
'fname': fName.replace('.zip', ''), | |
'ts': tsToStr(os.path.getmtime(fpName)), | |
'fs': bytesSizeToStr(os.path.getsize(fpName)) | |
} | |
files.append(properties) | |
if len(properties['fname'])>nbChrFName: | |
nbChrFName=len(properties['fname']) | |
if len(properties['ts'])>nbChrTs: | |
nbChrTs=len(properties['ts']) | |
if len(properties['fs'])>nbChrFs: | |
nbChrFs=len(properties['fs']) | |
header=f"{strToMaxLength('Backup Id', nbChrFName)} {strToMaxLength('Date/Time', nbChrTs)} {strToMaxLength('Size', nbChrFs)}" | |
print(header) | |
print("-" * len(header)) | |
for backup in files: | |
print(f"{strToMaxLength(backup['fname'], nbChrFName)} {strToMaxLength(backup['ts'], nbChrTs)} {strToMaxLength(backup['fs'], nbChrFs)}") | |
print("-" * len(header)) | |
def executeKrita(appImage): | |
"""Execute krita appimage""" | |
print(f'. Execute Krita ({os.path.basename(appImage)})') | |
print('=' * 80) | |
os.system(appImage) | |
print('=' * 80) | |
def main(): | |
"""Main statement""" | |
parameters=getParams() | |
# check parameters | |
# Identifier for appimage (a key from APPIMG dictionary) | |
appImgId=None | |
# File name for appimage (full path/filename for appimage to execute) | |
appImgFileName=None | |
# number of appimage identifier provided as arguments | |
appImgCount=0 | |
# flag to indicate if appimage has to be executed or not | |
appImgLaunch=True | |
# flag to indicate if we execute a backup management action | |
isBackupAction=False | |
# backup name is provided? | |
backupName=None | |
# check if an appimage id is provided | |
for appImgKey in list(APPIMG.keys()): | |
if parameters[appImgKey]==True: | |
appImgCount+=1 | |
appImgFileName=getAppImage(APPPATH, APPIMG[appImgKey]) | |
appImgId=appImgKey | |
# get backup informations | |
if not parameters['backup_name'] is None: | |
backupName=parameters['backup_name'][0] | |
if (parameters['backup_list'] == True or | |
parameters['backup_create'] != False or | |
parameters['backup_delete'] != False or | |
parameters['backup_restore'] != False): | |
# if a backup management command is provided, ignore krita execution | |
# commands | |
backupAction=True | |
appImgLaunch=False | |
if appImgCount>0: | |
print(f"> Argument -{appImgId} is ignored") | |
appImgCount=0 | |
appImgFileName=None | |
if parameters['do_not_execute'] == True: | |
print("> Argument --do-not-execute is ignored") | |
parameters['do_not_execute']=False | |
if parameters['backup_no_restore'] == True: | |
print("> Argument --backup-no-restore is ignored") | |
parameters['backup_no_restore']=False | |
if parameters['backup_no_creation'] == True: | |
print("> Argument --backup-no-creation is ignored") | |
parameters['backup_no_creation']=False | |
if parameters['backup_none'] == True: | |
print("> Argument --backup-none is ignored") | |
parameters['backup_none']=False | |
if parameters['reset_krita_config'] == True: | |
print("> Argument --reset-krita-config is ignored") | |
parameters['reset_krita_config']=False | |
if parameters['backup_list'] == True and parameters['backup_name'] == True: | |
print("> Argument --backup-name is ignored") | |
else: | |
if appImgCount==0: | |
print("> A Krita version to launch is required") | |
return | |
#elif appImgCount>1: | |
# # note: already implemented by ArgumentParser 'add_mutually_exclusive_group' | |
# print("> Only one Krita version to launch is allowed") | |
# return | |
elif appImgFileName is None: | |
print("> No appimage found") | |
return | |
if parameters['backup_none']: | |
parameters['backup_no_restore']=True | |
parameters['backup_no_creation']=True | |
if parameters['do_not_execute']: | |
appImgLaunch=False | |
if parameters['reset_krita_config']: | |
parameters['backup_no_restore']=True | |
if parameters['backup_list']: | |
backupList() | |
elif parameters['backup_create']!=False: | |
if not parameters['backup_create'] is None: | |
backupName=parameters['backup_create'] | |
if backupName is None: | |
print(f"> No backup name has been given!") | |
else: | |
backup=getBackupFile(appImgId, backupName) | |
backupCreate(backup['file']) | |
elif parameters['backup_delete']!=False: | |
if not parameters['backup_delete'] is None: | |
backupName=parameters['backup_delete'] | |
if backupName is None: | |
print(f"> No backup name has been given!") | |
else: | |
backup=getBackupFile(appImgId, backupName) | |
if backup['exists']: | |
backupDelete(backup['file']) | |
else: | |
print(f"> No archive found for given backup Id '{backup['bckId']}'") | |
elif parameters['backup_restore']!=False: | |
if not parameters['backup_restore'] is None: | |
backupName=parameters['backup_restore'] | |
if backupName is None: | |
print(f"> No backup name has been given!") | |
else: | |
backup=getBackupFile(appImgId, backupName) | |
if backup['exists']: | |
deleteKritaFiles() | |
backupRestore(backup['file']) | |
else: | |
print(f"> No archive found for given backup Id '{backup['bckId']}'") | |
else: | |
backup=getBackupFile(appImgId, backupName) | |
if parameters['reset_krita_config']: | |
# do a reset, ignore backup | |
deleteKritaFiles() | |
elif backup['exists']: | |
if not backupCheck(backup['file']): | |
print(f"> Archive is not valid: {backup['file']}") | |
return | |
if not parameters['backup_no_restore']: | |
# delete/restore backup only if '--backup-no-restore' or '--backup-none' option hasn't been set | |
deleteKritaFiles() | |
appImgLaunch=backupRestore(backup['file']) | |
if appImgLaunch: | |
executeKrita(appImgFileName) | |
if not parameters['backup_no_creation']: | |
# create backup only if '--backup-no-create' or '--backup-none' option hasn't been set | |
backupCreate(backup['file']) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
V2.0.0
Review all command line arguments