Initial hacked version
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…
Reference in New Issue