#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2023 Pavel 'LEdoian' Turinský # SPDX-License-Identifier: GPL-2.0-only """ This is a simple program to create mimeapps.list (and in future maybe mailcap.order + mime package) with full MIME types. Mostly workaround to buggy `xdg-mime` script which can not do wildcards in MIME types. (Also I am not trying to write it in awk.) References: => https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html#default XDG spec => https://cgit.freedesktop.org/xdg/xdg-utils/tree/scripts/xdg-mime.in#n362 Source of the buggy xdg-mime As input, we get a file in similar format to mimeapps.list, process it top-down and for each application specified we scan its MimeType= directive and find matches to the _regexes_ on the LHS. Then we dump all the found MIME types with found apps to the final mimeapps.list, with above matches being preferred (towards left in mimeapps.list) We do not aim to fix the issue with xdg-mime only finding the first app. We could not possibly do that, since we precompute the list build-time and distribute the mimeapps.list as-output. (It should be possible to create hooks into apt/dpkg and just re-generate after every installation, but I cannot do that right away.) """ import re from pathlib import Path from os import environ from functools import cache import sys APPLICATION_DIR = Path('/usr/share/applications') # FIXME: use XDG_DATA_DIR DEBUG = 0 def _dprint(lvl): def f(*a, **kw): if DEBUG >= lvl: print(*a, **kw | {'file': sys.stderr}) return f dprint = _dprint(1) ddprint = _dprint(2) @cache def read_mimetypes(desktop_filename: str) -> list[str]: path = APPLICATION_DIR / desktop_filename dprint(f'DEBUG: Processing file {path}') if not path.exists(): print(f'WARNING: File {path} not found, skipping…', file=sys.stderr) return [] section = None with open(path) as f: for line in f: line = line.strip() # Hacky .desktop file parsing # TODO: Does a library exist? if line.startswith('['): section = re.match(r'\[[^]]+\]', line).string dprint(f'DEBUG: Found section \'{section}\'') if section == '[Desktop Entry]' and line.startswith('MimeType='): dprint('DEBUG: Found MimeType line') types = line.removeprefix('MimeType=').split(';') types = list(filter(lambda x: x != '', types)) return types # We believe that there is only one such line. else: # No MimeType= line found print(f'WARNING: MimeType= line not found in {path}', file=sys.stderr) return [] def filter_matching_types(ref: str, types: list[str]): result = [] regex = re.compile(ref) for t in types: if regex.fullmatch(t): result.append(t) return result def process_assignment(mtpattern: str, desktop_files: list[str], out_mapping: dict[str, list[str]]) -> None: # I wonder whether it would be cleaner to just return the additions to the resulting dictionary… for dfn in desktop_files: usabletypes = read_mimetypes(dfn) usabletypes = filter_matching_types(mtpattern, usabletypes) for mtype in usabletypes: if mtype not in out_mapping: out_mapping[mtype] = [] if dfn not in out_mapping[mtype]: # deduplication out_mapping[mtype].append(dfn) def process_file(fn: str, out_mapping: dict[str, list[str]]) -> None: """Processes directives in fn and fills in mapping of specific mime types to the respective desktop file names.""" path = Path(fn) if not path.exists(): print(f"ERROR: File {path} not found", file=sys.stderr) sys.exit(1) with open(path) as f: for line in f: line = line.strip() line = re.sub(r'#.*$', '', line) if re.match(r'^[ ]*$', line): continue pat, files = line.split('=', maxsplit=2) files = list(filter(lambda x: x != '', files.split(';'))) process_assignment(pat, files, out_mapping) def dump_mapping(mapping: dict[str, list[str]], filename: str = None) -> None: if filename is None: filename = '/dev/stdout' path = Path(filename) else: path = Path(filename) if path.exists(): print(f'WARNING: File {filename} already exists, overwriting…', file=sys.stderr) with open(path, 'w') as f: def fprint(*a, **kwa): print(*a, **kwa | {'file': f}) fprint('[Default Applications]') for mimetype, desktop_files in mapping.items(): fprint(f"{mimetype}={';'.join(desktop_files)};") def main(): if '--help' in sys.argv or len(sys.argv) <= 1: print(f'Usage: {sys.argv[0]} file ...\n\nThis script processes regex-based rules for default applications.', file=sys.stderr) sys.exit() out_mapping = {} for fn in sys.argv[1:]: process_file(fn, out_mapping) dump_mapping(out_mapping) # TODO: def resolve_more_specific_mimetypes(mimetype: str) -> list[str]: ... if __name__ == '__main__': main()