xDg

A reimplementation of xdg-open in Dlang
git clone git://xatko.vsos.ethz.ch/xDg.git
Log | Files | Refs

commit 2a435930e5f8dde8892afa86bdb7765cc82e3119
Author: Dominik Schmidt <dominik@schm1dt.ch>
Date:   Wed, 26 Feb 2020 22:24:59 +0100

Initial commit

Diffstat:
AMakefile | 39+++++++++++++++++++++++++++++++++++++++
Asrc/open.d | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/xdg/desktop.d | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/xdg/mime.d | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 420 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,39 @@ +DMD ?= dmd +DEBUG ?= -g +RELEASE ?= -release +DFLAGS ?= -O +INCLUDES = src +SRC := $(wildcard src/xdg/*.d ) +TARGET = xdg +LIBS = magic + +_DFLAGS := $(addprefix -I, $(INCLUDES)) $(addprefix -L-l, $(LIBS)) $(DFLAGS) + +.PHONY: all shared static clean distclean + +all: shared static xdg-open + +xdg-open: src/open.o lib$(TARGET).a + $(DMD) $(_DFLAGS) $(RELEASE) -of$@ $^ + +shared: lib$(TARGET)-debug.so lib$(TARGET).so +static: lib$(TARGET)-debug.a lib$(TARGET).a + +lib$(TARGET).a: $(patsubst %.d,%.o,$(SRC)) + $(DMD) $(_DFLAGS) $(RELEASE) -lib -of$@ $^ +lib$(TARGET)-debug.a: $(patsubst %.d, %-debug.o,$(SRC)) + $(DMD) $(_DFLAGS) $(DEBUG) -lib -of$@ $^ +lib$(TARGET).so: $(patsubst %.d, %.o, $(SRC)) + $(DMD) $(_DFLAGS) $(RELEASE) -shared -of$@ $^ +lib$(TARGET)-debug.so: $(patsubst %.d, %-debug.o, $(SRC)) + $(DMD) $(_DFLAGS) $(DEBUG) -shared -of$@ $^ + +%.o:%.d + $(DMD) $(_DFLAGS) $(RELEASE) -c -of$@ $< +%-debug.o:%.d + $(DMD) $(_DFLAGS) $(DEBUG) -c -of$@ $< + +clean: + rm -f $(patsubst %.d, %.o, $(SRC)) $(patsubst %.d,%-debug.o,$(SRC)) +distclean: clean + rm -f lib$(TARGET){,-debug}.{so,a} diff --git a/src/open.d b/src/open.d @@ -0,0 +1,69 @@ +import xdg.desktop; +import xdg.mime; +import std.stdio; +import std.process; +import std.array; +import std.regex; +import std.algorithm.searching:startsWith; +import std.algorithm:splitter; +import std.format; + +bool is_file_url_or_path(in char[] uri){ + if(uri.startsWith("file://")){ + return true; + } + auto r = ctRegex!(`^\w[\w+-\.]*:`); + if(uri.matchFirst(r)){ + return false; + } + return true; +} + +struct Opener{ + Mime mime; + Database db; + + void init(){ + mime.init(); + db.init(); + } + + + void open(in char[] file, in char[] mime){ + auto ent = db.get(mime); + spawnShell(ent.format(file)); + } + + void open(in char[] uri){ + if(is_file_url_or_path(uri)){ + auto file = uri.file_url_to_path(); + auto type = mime.file(file); + open(file, type); + } + else{ + auto file = uri; + auto browsers = environment.get("BROWSER","x-www-browser:firefox:iceweasel:seamonkey:mozilla:epiphany:konqueror:chromium:chromium-browser:google-chrome:www-browser:links2:elinks:links:lynx:w3m"); + foreach(browser; browsers.splitter(":")){ + auto pid = spawnShell(format!(`%s "%s"`)(browser,uri)); + if(wait(pid) == 0){ + break; + } + } + } + } + +} + +void main(string[] args){ + Opener o; + o.init(); + //foreach(arg;args[1..$]){ + auto arg = args[1..$].join(" "); + try{ + o.open(arg); + } + catch(Exception e){ + stderr.writeln("Could not open ", arg, " ", e.msg); + } + //} +} diff --git a/src/xdg/desktop.d b/src/xdg/desktop.d @@ -0,0 +1,172 @@ +module xdg.desktop; +import std.stdio; +import std.string; +import std.path; +import std.process; +import std.algorithm:joiner,splitter,startsWith,skipOver,findSplit,map; +import std.range; +import std.file; +import std.format; +import std.array; +import xdg.mime; + + +auto file_url_to_path(inout(char)[] uri){ + uri.skipOver("file://localhost"); + uri.skipOver("file://"); + return uri; +} + +auto xdg_user_dir(){ + auto home = environment.get("XDG_DATA_HOME", buildPath(environment.get("HOME"), ".local/share")); + auto sys = environment.get("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/"); + return chain(home.splitter(":"),sys.splitter(":")).map!(a=>buildPath(a,"applications")); +} + +struct Database{ + struct Entry{ + string path; + string baseName; + string name; + string icon; + string exec; + string[] mimeTypes; + + auto format(in char[] path){ + Appender!string app; + auto fmt = exec; + while(fmt.length>0){ + auto s = fmt.findSplit("%"); + app ~= s[0]; + if(s[1].length==0){ + break; + } + switch(s[2][0]){ + case '%': + app ~= "%"; + break; + case 'i': + app ~= "--icon "~icon; + break; + case 'f': + case 'F': + case 'u': + case 'U': + app ~= '"'~path.replace("\"","\\\"")~'"'; + break; + default: + throw new Exception("Unknown format literal {}".format(s[2])); + break; + } + fmt = s[2][1..$]; + } + return app.data; + } + + void fromFile(in char[] file){ + auto f = File(file, "r"); + scope(exit){ + f.close(); + } + path = file.idup; + baseName = .baseName(path); + foreach(l; f.byLine){ + auto s = l.findSplit("="); + switch(s[0]){ + case "Name": + name = s[2].idup; + break; + case "Exec": + exec = s[2].idup; + break; + case "Icon": + icon = s[2].idup; + break; + case "MimeType": + mimeTypes = s[2].idup.split(";"); + break; + default: + break; + } + } + } + } + + Entry[][string] entries; + Entry[][string] entries_by_basename; + Entry[string] preferred; + void index(in char[] path){ + writeln("Indexing ", path); + auto de = dirEntries(cast(string)path, "*.desktop", SpanMode.breadth); + foreach(entry; de){ + Entry e; + try{ + e.fromFile(entry); + } + catch(Exception exc){ + + writeln(exc); + } + foreach(mt; e.mimeTypes){ + entries[mt] ~= e; + } + entries_by_basename[e.baseName] ~= e; + } + } + + void index(){ + auto paths = xdg_user_dir(); + foreach(path; paths){ + if(!exists(path)) + continue; + try{ + index(path); + } + catch(Exception e){ + writeln(e); + } + } + + } + + void loadPreferred(){ + auto config_home = environment.get("XDG_CONFIG_HOME",buildPath(environment.get("HOME"), ".config")); + auto config = buildPath(config_home, "mimeapps.list"); + if(exists(config)){ + auto f = File(config); + scope(exit){ + f.close(); + } + foreach(l; f.byLine()){ + auto s = l.findSplit("="); + if(s[1].length > 0){ + auto de = s[2].findSplit(";")[0]; + if(auto entries = de in entries_by_basename){ + preferred[s[0].idup] = (*entries)[0]; + } + else{ + stderr.writeln("No entry for %s found".format(de)); + } + } + } + } + } + + void init(){ + index(); + loadPreferred(); + } + + Entry get(in char[] mime){ + if(auto e = mime in preferred){ + return *e; + } + else if(auto e = mime in entries){ + return (*e)[0]; + } + else{ + throw new Exception("Mime type %s not found".format(mime)); + } + } +} + diff --git a/src/xdg/mime.d b/src/xdg/mime.d @@ -0,0 +1,140 @@ +module xdg.mime; +import std.string; + +extern(C){ + enum MAGIC:int{ + NONE =0x0000000, + DEBUG =0x0000001, + SYMLINK =0x0000002, + COMPRESS =0x0000004, + DEVICES =0x0000008, + MIME_TYPE =0x0000010, + CONTINUE =0x0000020, + CHECK =0x0000040, + PRESERVE_ATIME =0x0000080, + RAW =0x0000100, + ERROR =0x0000200, + MIME_ENCODING =0x0000400, + APPLE =0x0000800, + EXTENSION =0x1000000, + COMPRESS_TRANSP =0x2000000, + NO_CHECK_COMPRESS =0x0001000, + NO_CHECK_TAR =0x0002000, + NO_CHECK_SOFT =0x0004000, + NO_CHECK_APPTYPE =0x0008000, + NO_CHECK_ELF =0x0010000, + NO_CHECK_TEXT =0x0020000, + NO_CHECK_CDF =0x0040000, + NO_CHECK_CSV =0x0080000, + NO_CHECK_TOKENS =0x0100000, + NO_CHECK_ENCODING =0x0200000, + NO_CHECK_JSON =0x0400000, + NODESC =(MAGIC.EXTENSION|MAGIC.MIME|MAGIC.APPLE), + MIME =(MAGIC.MIME_TYPE|MAGIC.MIME_ENCODING), + } + + + alias magic_t = void*; + + magic_t magic_open(MAGIC); + void magic_close(magic_t); + + const(char)* magic_getpath(const char *, int); + const(char)* magic_file(magic_t, const char *); + const(char)* magic_descriptor(magic_t, int); + const(char)* magic_buffer(magic_t, const void *, size_t); + + const(char)* magic_error(magic_t); + int magic_getflags(magic_t); + int magic_setflags(magic_t, int); + + int magic_version(); + int magic_load(magic_t, const char *); + int magic_load_buffers(magic_t, void **, size_t *, size_t); + + int magic_compile(magic_t, const char *); + int magic_check(magic_t, const char *); + int magic_list(magic_t, const char *); + int magic_errno(magic_t); + +} + +class MagicException : Exception { + this(Magic m, string file=__FILE__, size_t line=__LINE__, Throwable next=null){ + auto errmsg = magic_error(m.cookie); + super(errmsg.fromStringz.idup,file,line,next); + } +} + +struct Magic{ + private magic_t cookie = null; + bool loaded=false; + + bool ready(){ + return cookie != null && loaded; + } + + void open(MAGIC m){ + cookie = magic_open(m); + if(cookie == null){ + throw new MagicException(this); + } + } + + void open(){ + open(MAGIC.NONE); + } + + private void check(int val){ + if(val<0){ + throw new MagicException(this); + } + } + + void load(){ + check(magic_load(cookie, null)); + loaded = true; + } + + void load(in char[] path){ + check(magic_load(cookie, path.toStringz)); + loaded = true; + } + + auto file(in char[] path) + in{ + assert(ready()); + } + body{ + auto res = magic_file(cookie, path.toStringz); + if(res == null){ + throw new MagicException(this); + } + return res.fromStringz; + } + + void close(){ + if(cookie) + magic_close(cookie); + cookie = null; + loaded = false; + } + + static int Version(){ + return magic_version(); + } +} + +struct Mime{ + Magic m; + void init(){ + m.open(MAGIC.MIME_TYPE); + m.load(); + } + auto file(in char[] path){ + return m.file(path); + } + void close(){ + m.close(); + } +}