1
0
Fork 0
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

152 lines
5.4 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Pavel 'LEdoian' Turinský <mimeapps@pokemon.ledoian.cz>
# 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, header: str = '[Default Applications]') -> 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(header)
for mimetype, desktop_files in mapping.items():
fprint(f"{mimetype}={';'.join(desktop_files)};")
def main():
import argparse
parser = argparse.ArgumentParser()
parser.description = 'This script processes regex-based rules for default applications.'
parser.add_argument('--add', help="Generate [Added Associations] section instead", action='store_true')
parser.add_argument('--remove', help="Generate [Removed Associations] section instead", action='store_true')
parser.add_argument('files', help="Source of the rules", nargs='+')
parser.usage='%(prog)s [--add|--remove] files ...'
parser.epilog = 'By default a [Default Applications] section is created. There is no option to apply this behaviour.'
args = parser.parse_args()
if sum([args.add, args.remove]) > 1:
print('Cannot generate conflicting sections!', file=sys.stderr)
sys.exit(1)
params = {}
if args.add: params['header'] = '[Added Associations]'
if args.remove: params['header'] = '[Removed Associations]'
out_mapping = {}
for fn in args.files:
process_file(fn, out_mapping)
dump_mapping(out_mapping, **params)
# TODO: def resolve_more_specific_mimetypes(mimetype: str) -> list[str]: ...
if __name__ == '__main__': main()