1
0
Fork 0

Initial hacked version

master
Pavel 'LEdoian' Turinský 2 years ago
commit 4a955d6157

@ -0,0 +1,33 @@
# mimeapps.list tools: a set of simple scripts to manage `mimeapps.list` files
Setting system-wide default applications is actually not very easy in current
Linux distributions. This aims to [help fix it](https://xkcd.com/927/).
<!-- You can read the rationale in my [blogpost](TODO: create a blog). -->
Quality: hacked together to get basic work done, may be buggy, works for me :-)
Breaking changes expected, please read diffs or pin your versions.
## What is here
Two main scripts:
- `defaulter` takes patterns for default apps and outputs a `mimeapps.list` to stdout
- `combiner` can merge multiple `mimeapps.list`-style files by applying one after another
There is currently little documentation. All scripts support `--help` option to
give usage, and have a large docstring at the top, which explains what they do.
The file `example.patterns` serves as an example of the pattern file. It too
has a few comments.
Proper documentation, tests, etc: not currently implemented. Patches wanted.
Also see the `TODO` file.
## License
GPLv2-only
## Contributing
Send any feedback, bug reports or patches to
[mimeapps@pokemon.ledoian.cz](mailto:mimeapps@pokemon.ledoian.cz).

@ -0,0 +1,3 @@
- Defaulter cannot understand any custom additions or deletions
- Expander is not written yet
- Docs is only in comments

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
This is an even simpler script to combine multiple mimeapps.list files into
one.
This is not too trivial, since an addition can be reverted by a deletion in a
later file and vice versa, and all values should probably be deduplicated.
The input files are processed left-to-right. This means that the default is
taken from the leftmost (first supplied) file the respective MIME type is
specified (other files serve as fallbacks), but an association added in one
file can be reverted in a later file (and possibly re-added in even later file)
"""
from pathlib import Path
from dataclasses import dataclass
import sys
import re
DEBUG = 1
def _dprint(lvl):
def f(*a, **kw):
if DEBUG >= lvl:
print(*a, **kw | {'file': sys.stderr})
return f
dprint = _dprint(1)
ddprint = _dprint(2)
def combine_dicts_with_lists(d1, d2):
result = {}
result |= {k: (d1|d2)[k] for k in d1.keys() ^ d2.keys()}
result |= {k: d1[k] + d2[k] for k in d1.keys() & d2.keys()}
return result
if '--help' in sys.argv or len(sys.argv) <= 1:
print(f'Usage: {sys.argv[0]} file ...\n\nThis script combines multiple mimeapps.list files into one.', file=sys.stderr)
sys.exit()
ADDITIONS = {}
DELETIONS = {}
DEFAULTS = {}
for fn in sys.argv[1:]:
section = None
with open(fn) as f:
for line in f:
line = line.strip()
if line.startswith('['):
section = re.match(r'\[[^]]+\]', line).string
dprint(f'DEBUG: Found section \'{section}\'')
continue
mimetype, apps = line.split('=', 2)
apps = [x for x in apps.split(';') if x != '']
if section == '[Added Associations]':
if mimetype not in ADDITIONS:
ADDITIONS[mimetype] = []
for app in apps:
if app not in ADDITIONS[mimetype]:
ADDITIONS[mimetype].append(app)
if mimetype in DELETIONS and app in DELETIONS[mimetype]:
DELETIONS[mimetype].remove(app)
if section == '[Removed Associations]':
if mimetype not in DELETIONS:
DELETIONS[mimetype] = []
for app in apps:
if app not in DELETIONS[mimetype]:
DELETIONS[mimetype].append(app)
if mimetype in ADDITIONS and app in ADDITIONS[mimetype]:
ADDITIONS[mimetype].remove(app)
if section == '[Default Applications]':
if mimetype not in DEFAULTS:
DEFAULTS[mimetype] = []
for app in apps:
if app not in DEFAULTS[mimetype]:
dprint(f'adding {app} to {mimetype}')
DEFAULTS[mimetype].append(app)
if ADDITIONS:
print('[Added Associations]')
for mt, apps in ADDITIONS.items():
print(f"{mt}={';'.join(apps)};")
print()
if DELETIONS:
print('[Removed Associations]')
for mt, apps in DELETIONS.items():
print(f"{mt}={';'.join(apps)};")
print()
if DEFAULTS:
print('[Default Applications]')
for mt, apps in DEFAULTS.items():
print(f"{mt}={';'.join(apps)};")
print()

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
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()

@ -0,0 +1,42 @@
#!/usr/bin/env ./defaulter
#
# This file describes patterns for the [Default Applications] part of
# mimeapps.list. Each line consists of a pattern, '=', and a list of desktop
# files to be used for any type matching the pattern.
#
# The desktop files are read in order for each pattern and according to their
# MimeType= directive, matching lines of mimeapps.list with proper types are
# created. The resulting file contains only the desktop files which claim to
# support the type, in order they matched a pattern.
#
# In order to read the desktop files, they must be present in the filesystem.
# Currently, only /usr/share/applications/ folder is used for finding the
# files.
#
# Patterns are matched against full MIME types, not just prefixes, so if a
# pattern contains no "active" characters, it just serves as an exact match.
#
# Limitations: Patterns cannot contain '#', which introduces comments anywhere
# and '=' which separates the pattern from the list. (Neither of those
# characters are likely to be a part of a MIME type.) Also, this file does not
# support section headers (they would be hard to distinguish from a bracket
# pattern), so a mimeapps.list file, while similar, is in fact not a valid
# input for defaulter.
#
# As an example: the following line uses geeqie for any images it supports,
# then tries eog, etc. If eog supports a file geeqie does not, it becomes the
# default, for files suported by both it becomes a fallback. The resulting
# lines of mimeapps.list would be:
# image/x-just-geeqie=geeqie.desktop;
# image/x-supported-by-both=geeqie.desktop;org.gnome.eog.desktop;
# image/x-just-eog=org.gnome.eog.desktop;
# Any pattern not matching 'image/.*' is not considered and this line will not
# contribute any such line to the output.
image/.*=geeqie.desktop;org.gnome.eog.desktop;feh.desktop;
application/pdf=org.gnome.Evince.desktop;okularApplication_pdf.desktop;mupdf.desktop;
text/.*=org.gnome.gedit.desktop;org.kde.kate.desktop;mousepad.desktop;gvim.desktop;
audio/.*=vlc.desktop;mpv.desktop;audacity.desktop;
video/.*=vlc.desktop;mpv.desktop;
x-scheme-handler/http=chromium.desktop;firefox-esr.desktop;
x-scheme-handler/https=chromium.desktop;firefox-esr.desktop;
x-scheme-handler/mailto=thunderbird.desktop;org.gnome.Evolution.desktop;org.kde.kmail2.desktop;
Loading…
Cancel
Save