Skip to content

Instantly share code, notes, and snippets.

@Grum999
Last active July 1, 2021 21:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Grum999/b8977a5901283fa54bd79beb5ebd8042 to your computer and use it in GitHub Desktop.
Save Grum999/b8977a5901283fa54bd79beb5ebd8042 to your computer and use it in GitHub Desktop.
A small python script to execute Krita
#!/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()
@Grum999
Copy link
Author

Grum999 commented Jul 1, 2021

V2.0.0

Review all command line arguments

  • Add version
  • Add backup name
  • Add backup list, create, delete, restore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment