#!/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}.')