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