File: Frigcal/code/Frigcal--source/tools/convert_legacy_colors.py
#!/usr/bin/python3
r"""
==================================================================
Change legacy color settings, part of Frigcal 4.0.
A one-time optional step for users of Frigcal 3.0 and earlier.
Run this file and read its docs. Edit only the map_*.py files.
========
OVERVIEW
========
Frigcal 4.0's color scheme is not backward compatible with
prior Frigcal versions. In 4.0, event category is always
just the name of a preset color, which is applied to event
overscore bars. The former (background, foreground) color
pairs and "#RRGGBB" hex values are unsupported, and both
calendar colors and frigcal_configs.py are unused in 4.0.
This backward incompatibility is by design: the new scheme
uses preset colors that work on all devices without syncing
a settings file or redoing customizations on each device.
To aid migration, this script can be used to convert your
calendars' prior calendar and category color settings to
the new predefined color names all at once. This avoids
manual recolorization of events in your calendars.
This script requires an installed Python 3.X and may be
run on Android and all PCs: Windows, macOS, and Linux. On
Android, the app that runs this script must have permission
to use your calendars folder, via All Files Access or other;
consult your Python-capable app for more details.
This script also must be run in either the app's complete
source-code folder or the folder made by unzipping its own
zipfile, because it requires the source code of the app's
calendar-processing tools that are not present in PC app
installs and are not accessible in the Android app install.
=====
USAGE
=====
1) *IMPORTANT* - make a backup copy of your calendar files
folder first, if you want to keep your calendars' original
categories or experiment with different mappings. This
script changes categories in calendar files in place
without making any backup copies.
2) Get and unzip this script's zipfile. Either download this
zipfile from www.quixotely.com/Frigcal or locate it in the
"tools" subfolder of the app's install folder on PCs (the
app install folder is not user-accessible on Android).
3) Get Python 3.X (3.12 or later recommended). On PCs, get
it from www.python.org or other. On Android, use an app
that runs Python scripts, such as Termux or PyDroid 3.
4) Edit either or both of the following files located in this
script's folder to specify your color-conversion mappings:
map_calendar_colors.py
Gives calendar=>color mappings, as {'calendarname': 'newcolor',}
map_category_colors.py
Gives old=>new category mappings, as {'oldcategory': 'newcolor',}
These files use the same Python (and JSON) dictionary syntax
as the former frigcal_configs.py file, so you can copy/paste
your former settings into these files and edit them there.
For more info, see these files' in-file docs and examples.
In both files, "newcolor" is the color name you wish to use
in 4.0 and must be one of 4.0's preset category-color names,
which are listed in both the new_color_names.py file in this
script's folder and the in-app Category pulldown of the event
dialog. Invalid color names are reported, and calendar or
category names not present in your ICS file are ignored.
This script sets a new category/color name in your events,
and selects from calendar and category colors as follows:
- For events _without_ a prior category name, color is taken
from either the event's calendar name in map_calendar_colors.py,
an empty '' category name in map_category_colors.py, or a
default gray, and in that order.
- For events _with_ a prior category name, color is taken from
either the event's category name in map_category_colors.py,
its calendar name in map_calendar_colors.py, or a default gray,
and in that order. To support script reruns, an event is
ignored if its category is already a 4.0 color name.
Note that calendar colors are used in this script for legacy
settings, but the app itself uses only category color names.
Aso note that all calendar and color names used in this script
are case sensitive; "my-category" will not match "My-Category".
5) Run this script from a command line in its home folder, with
one optional argument - the path to your calendars folder,
which is asked if omitted. Abstractly:
$ cd scriptfolderpath
$ python3 convert_legacy_colors.py calendarfolderpath
On macOS and Linux, these two commands are typically run in
Terminal. On Windows, run in Command Prompt or other, and
use "py" or "py -3" instead of "python3" in the second line.
On Android, use "python" or "python3" in a Python-capable app.
On all, omit the "$" prompt, replace "scriptfolderpath" with
this script's folder, and replace "calendarfolderpath" with
your calendar files' folder. Both paths might begin with
"C:\Users\me" for Windows, "/Users/me" for macOS, "/home/me"
for Linux, "X:\home\me" for WSL, and "/sdcard for Android.
Example run:
$ python3 convert_legacy_colors.py ../Calendars
Starting script...
Starting ICS-file conversion...
Processing ../Calendars/frigcal-default-calendar.ics
++Updating ../Calendars/frigcal-default-calendar.ics
Processing ../Calendars/Holidays.ics
ICS-file conversion complete.
Number files processed/converted: 2/1.
6) Once the script is run, your calendar folder's ICS file will
have new category names that are also preset color names in
4.0, but your ICS files are otherwise unchanged. Copy or
sync your updated ICS files to all your devices as desired.
========
OPTIONAL
========
The preceding steps convert your calendar files for use in
Frigcal 4.0. To use them in in _both_ Frigcal 4.0 and 3.0:
- First, edit the mapping files here and run this script to
convert categories in your calendar files from your prior
3.0 names in frigcal_configs.py to 4.0's new color names,
as described above.
- Then, edit your 3.0 frigcal_configs.py to map the new
category names (which are now just color names) back to
the colors you formerly used in frigcal_configs.py.
When you're finished, 4.0 will use the new category names in
your calendar files as color names, and 3.0 will use your
newname-to-oldcolor mappings in frigcal_configs.py.
Abstractly, your legacy frigcal_configs.py starts like this:
{'oldcategoryname': 'oldcategorycolor'}
Your edits in map_category_colors.py map to new colors for 4.0:
{'oldcategoryname': 'newcolorname'}
You edit frigcal_configs.py to map back to prior colors for 3.0:
{'newcolorname': 'oldcategorycolor'}
The 'oldcategorycolor' in your final 3.0 frigcal_configs.py
can be any color supported in 3.0, including names, hex strings,
and (background, foreground) tuples; only 3.0 will see them.
For example, if your 3.0 frigcal_configs.py originally had:
{'Business': ('black', '#00ffff')}
And you've mapped this to 4.0 colors for this converter as:
{'Business': 'cyan'}
Then simply change your 3.0 frigcal_configs.py to map back:
{'cyan': ('black', '#00ffff')}
The net effect makes calendars appear in 3.0 as they did formerly
but applies the new preset colors in 4.0. If you instead wish to
use the _same_ colors in 3.0 and 4.0, simply map new 4.0 category
names to new 4.0 colors in your frigcal_configs.py for 3.0:
category_colors = { # category _is_ color in 4.0
'blue': 'blue', # map 4.0 name to color for 3.0
'red': 'red',
# and so on, for all the new colors your calendars use
}
Caveat: this works best if there is a 1:1 mapping from old
category to new color name. If more than one old category
maps to the same new color, you'll need to choose which old
color to use for all of the old categories mapped to the new
color name. To avoid this state, use a unique new color for
each old category name when applying this script.
You can also _keep_ legacy 3.0 color settings in your 3.0
configs file for any events that are not updated by this
script. They won't be used in 4.0's simpler color scheme.
==================================================================
"""
print('Starting script...')
import os, sys, glob, json, traceback
# load user-defined color maps: error messages per python
from map_calendar_colors import calendar_colors as calendar_map
from map_category_colors import category_colors as category_map
# validate and use run folder
if os.path.exists(os.path.join('..', 'icalendar')):
# running in tools folder of dev tree or full source-code package
sys.path.append('..')
elif os.path.exists('icalendar'):
# running in the unzip folder of this script's own online zipfile
sys.path.append('.')
else:
print('This script does not have access to its required files.')
print('Please consult its top-of-file docs and try again.')
sys.exit(1)
import icalendar # required iCalendar parser/generator
# get and validate predefined colors
# new 4.0 colors, copied from app's settings.py; this redundancy is
# a bad idea in general, but importing settings.py requires a host
# of other modules and a settings-file load, and getting this from
# the app's settings file requires users to first run the app once.
from new_color_names import newcategorycolors # user-visible list
for (map, name) in [(category_map, 'category'), (calendar_map, 'calendar')]:
if any(color not in newcategorycolors for color in map.values()):
print('New colors can be only predefined category names.')
print(f'Please edit map_{name}_colors.py\'s values and try again.')
sys.exit(1)
# get and validate calendars-folder path
calendars_path = ( # e.g., C:\Users\me\Frigcal
sys.argv[1] if len(sys.argv) > 1 else # e.g., /Users/me/Frigcal
input('Path to your calendars folder? ')) # e.g., z:\home\me\Frigcal
try:
if not os.path.isdir(calendars_path):
print('Calendars path is not a valid directory.')
print('Please check the path and try again.')
sys.exit(1)
except Exception:
# anything but sys.exit()!
print('Cannot access your calendars path.')
print('Please check permissions and try again.')
sys.exit(1)
icspaths = glob.glob(os.path.join(calendars_path, '*.ics'))
if not icspaths:
print('There are no ICS files in the calendars path.')
print('Please check the path and try again.')
sys.exit(1)
# convert calendar files
def convert_one_ics_file(icspath):
print('Processing', icspath)
default_color = 'gray'
calendar = os.path.basename(icspath)
# read ics data
icsfile = open(icspath, 'r', encoding='utf8')
icstext = icsfile.read()
icsfile.close()
# parse ics data
icsarray = icalendar.cal.Calendar.from_ical(icstext) # str, not bytes
# change categories
anychanged = False
for event in icsarray.walk('VEVENT'):
category = event.get('CATEGORIES', None)
if isinstance(category, list):
category = category[0] # uses first
if category in ['', None]:
# calendar has precedence
if calendar in calendar_map.keys():
newcolor = calendar_map[calendar]
elif '' in category_map.keys():
newcolor = category_map['']
else:
newcolor = default_color
if category == '':
del event['CATEGORIES']
event.add('CATEGORIES', newcolor)
anychanged = True
elif category in newcategorycolors:
# assume already changed
pass
else:
# category has precedence
if category in category_map.keys():
newcolor = category_map[category]
elif calendar in calendar_map.keys():
newcolor = calendar_map[calendar]
else:
newcolor = default_color
del event['CATEGORIES']
event.add('CATEGORIES', newcolor)
anychanged = True
if anychanged:
# generate ics data
print('++Updating', icspath)
newicstext = icsarray.to_ical()
# save ics data
icsfile = open(icspath, 'wb') # bytes, not str
icsfile.write(newicstext)
icsfile.close()
return anychanged # True/False == 1/0
# process calendar files
print('Starting ICS-file conversion...')
numprocessed = numconverted = 0
for icspath in icspaths:
# for each ics file in calendars folder
try:
numconverted += convert_one_ics_file(icspath)
except Exception as E:
print('**Failed to convert and skipped', icspath)
print('..Exception: ', E)
print('..Python info:', sys.exc_info())
traceback.print_exc() # show traceback too
else:
numprocessed += 1
print('ICS-file conversion complete.')
print(f'Number files processed/converted: {numprocessed}/{numconverted}.')