Merge pull request #483 from FreeSlave/standardicons

Icon Providers for Windows and Freedesktop
This commit is contained in:
Vadim Lopatin 2017-10-16 11:21:16 +03:00 committed by GitHub
commit 8a3684ccea
15 changed files with 6961 additions and 85 deletions

454
3rdparty/icontheme/cache.d vendored Normal file
View File

@ -0,0 +1,454 @@
/**
* This module provides class for loading and validating icon theme caches.
*
* Icon theme cache may be stored in icon-theme.cache files located in icon theme directory along with index.theme file.
* These files are usually generated by $(LINK2 https://developer.gnome.org/gtk3/stable/gtk-update-icon-cache.html, gtk-update-icon-cache).
* Icon theme cache can be used for faster and cheeper lookup of icons since it contains information about which icons exist in which sub directories.
*
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 https://github.com/GNOME/gtk/blob/master/gtk/gtkiconcachevalidator.c, GTK icon cache validator source code)
* Note:
* I could not find any specification on icon theme cache, so I merely use gtk source code as reference to reimplement parsing of icon-theme.cache files.
*/
module icontheme.cache;
package {
import std.algorithm;
import std.bitmanip;
import std.exception;
import std.file;
import std.mmfile;
import std.path;
import std.range;
import std.system;
import std.typecons;
import std.traits;
import std.datetime : SysTime;
static if( __VERSION__ < 2066 ) enum nogc = 1;
}
private @nogc @trusted void swapByteOrder(T)(ref T t) nothrow pure {
static if( __VERSION__ < 2067 ) { //swapEndian was not @nogc
ubyte[] bytes = (cast(ubyte*)&t)[0..T.sizeof];
for (size_t i=0; i<bytes.length/2; ++i) {
ubyte tmp = bytes[i];
bytes[i] = bytes[T.sizeof-1-i];
bytes[T.sizeof-1-i] = tmp;
}
} else {
t = swapEndian(t);
}
}
/**
* Error occured while parsing icon theme cache.
*/
class IconThemeCacheException : Exception
{
this(string msg, string context = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
super(msg, file, line, next);
_context = context;
}
/**
* Context where error occured. Usually it's the name of value that could not be read or is invalid.
*/
@nogc @safe string context() const nothrow {
return _context;
}
private:
string _context;
}
/**
* Class representation of icon-theme.cache file contained icon theme cache.
*/
final class IconThemeCache
{
/**
* Read icon theme cache from memory mapped file and validate it.
* Throws:
* $(B FileException) if could mmap file.
* $(D IconThemeCacheException) if icon theme file is invalid.
*/
@trusted this(string fileName) {
_mmapped = new MmFile(fileName);
this(_mmapped[], fileName, 0);
}
/**
* Read icon theme cache from data and validate it.
* Throws:
* $(D IconThemeCacheException) if icon theme file is invalid.
*/
@safe this(immutable(void)[] data, string fileName) {
this(data, fileName, 0);
}
private @trusted this(const(void)[] data, string fileName, int /* To avoid ambiguity */) {
_data = data;
_fileName = fileName;
_header.majorVersion = readValue!ushort(0, "major version");
if (_header.majorVersion != 1) {
throw new IconThemeCacheException("Unsupported version or the file is not icon theme cache", "major version");
}
_header.minorVersion = readValue!ushort(2, "minor version");
if (_header.minorVersion != 0) {
throw new IconThemeCacheException("Unsupported version or the file is not icon theme cache", "minor version");
}
_header.hashOffset = readValue!uint(4, "hash offset");
_header.directoryListOffset = readValue!uint(8, "directory list offset");
_bucketCount = iconOffsets().length;
_directoryCount = directories().length;
//Validate other data
foreach(dir; directories()) {
//pass
}
foreach(info; iconInfos) {
foreach(im; imageInfos(info.imageListOffset)) {
}
}
}
/**
* Sub directories of icon theme listed in cache.
* Returns: Range of directory const(char)[] names listed in cache.
*/
@trusted auto directories() const {
auto directoryCount = readValue!uint(_header.directoryListOffset, "directory count");
return iota(directoryCount)
.map!(i => _header.directoryListOffset + uint.sizeof + i*uint.sizeof)
.map!(offset => readValue!uint(offset, "directory offset"))
.map!(offset => readString(offset, "directory name"));
}
/**
* Test if icon is listed in cache.
*/
@trusted bool containsIcon(const(char)[] iconName) const
{
IconInfo info;
return findIconInfo(info, iconName);
}
/**
* Test if icon is listed in cache and belongs to specified subdirectory.
*/
@trusted bool containsIcon(const(char)[] iconName, const(char)[] directory) const {
auto index = iconDirectories(iconName).countUntil(directory);
return index != -1;
}
/**
* Find all sub directories the icon belongs to according to cache.
* Returns: Range of directory const(char)[] names the icon belongs to.
*/
@trusted auto iconDirectories(const(char)[] iconName) const
{
IconInfo info;
auto dirs = directories();
bool found = findIconInfo(info, iconName);
return imageInfos(info.imageListOffset, found).map!(delegate(ImageInfo im) {
if (im.index < dirs.length) {
return dirs[im.index];
} else {
throw new IconThemeCacheException("Invalid directory index", "directory index");
}
});
}
/**
* Path of cache file.
*/
@nogc @safe fileName() const nothrow {
return _fileName;
}
/**
* Test if icon theme file is outdated, i.e. modification time of cache file is older than modification time of icon theme directory.
* Throws:
* $(B FileException) on error accessing the file.
*/
@trusted bool isOutdated() const {
return isOutdated(fileName());
}
/**
* Test if icon theme file is outdated, i.e. modification time of cache file is older than modification time of icon theme directory.
*
* This function is static and therefore can be used before actual reading and validating cache file.
* Throws:
* $(B FileException) on error accessing the file.
*/
static @trusted bool isOutdated(string fileName)
{
if (fileName.empty) {
throw new FileException("File name is empty, can't check if the cache is outdated");
}
SysTime pathAccessTime, pathModificationTime;
SysTime fileAccessTime, fileModificationTime;
getTimes(fileName, fileAccessTime, fileModificationTime);
getTimes(fileName.dirName, pathAccessTime, pathModificationTime);
return fileModificationTime < pathModificationTime;
}
unittest
{
assertThrown!FileException(isOutdated(""));
}
/**
* All icon names listed in cache.
* Returns: Range of icon const(char)[] names listed in cache.
*/
@trusted auto icons() const {
return iconInfos().map!(info => info.name);
}
private:
alias Tuple!(uint, "chainOffset", const(char)[], "name", uint, "imageListOffset") IconInfo;
alias Tuple!(ushort, "index", ushort, "flags", uint, "dataOffset") ImageInfo;
static struct IconThemeCacheHeader
{
ushort majorVersion;
ushort minorVersion;
uint hashOffset;
uint directoryListOffset;
}
@trusted auto iconInfos() const {
import std.typecons;
static struct IconInfos
{
this(const(IconThemeCache) cache)
{
_cache = rebindable(cache);
_iconInfos = _cache.bucketIconInfos();
_chainOffset = _iconInfos.front().chainOffset;
_fromChain = false;
}
bool empty()
{
return _iconInfos.empty;
}
auto front()
{
if (_fromChain) {
auto info = _cache.iconInfo(_chainOffset);
return info;
} else {
auto info = _iconInfos.front;
return info;
}
}
void popFront()
{
if (_fromChain) {
auto info = _cache.iconInfo(_chainOffset);
if (info.chainOffset != 0xffffffff) {
_chainOffset = info.chainOffset;
} else {
_iconInfos.popFront();
_fromChain = false;
}
} else {
auto info = _iconInfos.front;
if (info.chainOffset != 0xffffffff) {
_chainOffset = info.chainOffset;
_fromChain = true;
} else {
_iconInfos.popFront();
}
}
}
auto save() const {
return this;
}
uint _chainOffset;
bool _fromChain;
typeof(_cache.bucketIconInfos()) _iconInfos;
Rebindable!(const(IconThemeCache)) _cache;
}
return IconInfos(this);
}
@nogc @trusted static uint iconNameHash(const(char)[] iconName) pure nothrow
{
if (iconName.length == 0) {
return 0;
}
uint h = cast(uint)iconName[0];
if (h) {
for (size_t i = 1; i != iconName.length; i++) {
h = (h << 5) - h + cast(uint)iconName[i];
}
}
return h;
}
bool findIconInfo(out IconInfo info, const(char)[] iconName) const {
uint hash = iconNameHash(iconName) % _bucketCount;
uint chainOffset = readValue!uint(_header.hashOffset + uint.sizeof + uint.sizeof * hash, "chain offset");
while(chainOffset != 0xffffffff) {
auto curInfo = iconInfo(chainOffset);
if (curInfo.name == iconName) {
info = curInfo;
return true;
}
chainOffset = curInfo.chainOffset;
}
return false;
}
@trusted auto bucketIconInfos() const {
return iconOffsets().filter!(offset => offset != 0xffffffff).map!(offset => iconInfo(offset));
}
@trusted auto iconOffsets() const {
auto bucketCount = readValue!uint(_header.hashOffset, "bucket count");
return iota(bucketCount)
.map!(i => _header.hashOffset + uint.sizeof + i*uint.sizeof)
.map!(offset => readValue!uint(offset, "icon offset"));
}
@trusted auto iconInfo(size_t iconOffset) const {
return IconInfo(
readValue!uint(iconOffset, "icon chain offset"),
readString(readValue!uint(iconOffset + uint.sizeof, "icon name offset"), "icon name"),
readValue!uint(iconOffset + uint.sizeof*2, "image list offset"));
}
@trusted auto imageInfos(size_t imageListOffset, bool found = true) const {
uint imageCount = found ? readValue!uint(imageListOffset, "image count") : 0;
return iota(imageCount)
.map!(i => imageListOffset + uint.sizeof + i*(uint.sizeof + ushort.sizeof + ushort.sizeof))
.map!(offset => ImageInfo(
readValue!ushort(offset, "image index"),
readValue!ushort(offset + ushort.sizeof, "image flags"),
readValue!uint(offset + ushort.sizeof*2, "image data offset"))
);
}
@trusted T readValue(T)(size_t offset, string context = null) const if (isIntegral!T || isSomeChar!T)
{
if (_data.length >= offset + T.sizeof) {
T value = *(cast(const(T)*)_data[offset..(offset+T.sizeof)].ptr);
static if (endian == Endian.littleEndian) {
swapByteOrder(value);
}
return value;
} else {
throw new IconThemeCacheException("Value is out of bounds", context);
}
}
@trusted auto readString(size_t offset, string context = null) const {
if (offset > _data.length) {
throw new IconThemeCacheException("Beginning of string is out of bounds", context);
}
auto str = cast(const(char[]))_data[offset.._data.length];
size_t len = 0;
while (len<str.length && str[len] != '\0') {
++len;
}
if (len == str.length) {
throw new IconThemeCacheException("String is not zero terminated", context);
}
return str[0..len];
}
IconThemeCacheHeader _header;
size_t _directoryCount;
size_t _bucketCount;
MmFile _mmapped;
string _fileName;
const(void)[] _data;
}
///
version(iconthemeFileTest) unittest
{
string cachePath = "./test/Tango/icon-theme.cache";
assert(cachePath.exists);
const(IconThemeCache) cache = new IconThemeCache(cachePath);
assert(cache.fileName == cachePath);
assert(cache.containsIcon("folder"));
assert(cache.containsIcon("folder", "24x24/places"));
assert(cache.containsIcon("edit-copy", "32x32/actions"));
assert(cache.iconDirectories("text-x-generic").canFind("32x32/mimetypes"));
assert(cache.directories().canFind("32x32/devices"));
auto icons = cache.icons();
assert(icons.canFind("folder"));
assert(icons.canFind("text-x-generic"));
try {
SysTime pathAccessTime, pathModificationTime;
SysTime fileAccessTime, fileModificationTime;
getTimes(cachePath, fileAccessTime, fileModificationTime);
getTimes(cachePath.dirName, pathAccessTime, pathModificationTime);
setTimes(cachePath, pathAccessTime, pathModificationTime);
assert(!IconThemeCache.isOutdated(cachePath));
}
catch(Exception e) {
// some environmental error, just ignore
}
try {
auto fileData = assumeUnique(std.file.read(cachePath));
assertNotThrown(new IconThemeCache(fileData, cachePath));
} catch(FileException e) {
}
immutable(ubyte)[] data = [0,2,0,0];
IconThemeCacheException thrown = collectException!IconThemeCacheException(new IconThemeCache(data, cachePath));
assert(thrown !is null, "Invalid cache must throw");
assert(thrown.context == "major version");
data = [0,1,0,1];
thrown = collectException!IconThemeCacheException(new IconThemeCache(data, cachePath));
assert(thrown !is null, "Invalid cache must throw");
assert(thrown.context == "minor version");
}

712
3rdparty/icontheme/file.d vendored Normal file
View File

@ -0,0 +1,712 @@
/**
* This module provides class for reading and accessing icon theme descriptions.
*
* Information about icon themes is stored in special files named index.theme and located in icon theme directory.
*
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification)
*/
module icontheme.file;
package
{
import std.algorithm;
import std.array;
import std.conv;
import std.exception;
import std.path;
import std.range;
import std.string;
import std.traits;
import std.typecons;
static if( __VERSION__ < 2066 ) enum nogc = 1;
}
import icontheme.cache;
public import inilike.file;
import inilike.common;
/**
* Adapter of $(D inilike.file.IniLikeGroup) for easy access to icon subdirectory properties.
*/
struct IconSubDir
{
///The type of icon sizes for the icons in the directory.
enum Type {
///Icons can be used if the size differs at some threshold from the desired size.
Threshold,
///Icons can be used if the size does not differ from desired.
Fixed,
///Icons are scalable without visible quality loss.
Scalable
}
@safe this(const(IniLikeGroup) group) nothrow {
collectException(group.value("Size").to!uint, _size);
collectException(group.value("MinSize").to!uint, _minSize);
collectException(group.value("MaxSize").to!uint, _maxSize);
if (_minSize == 0) {
_minSize = _size;
}
if (_maxSize == 0) {
_maxSize = _size;
}
collectException(group.value("Threshold").to!uint, _threshold);
if (_threshold == 0) {
_threshold = 2;
}
_type = Type.Threshold;
string t = group.value("Type");
if (t.length) {
if (t == "Fixed") {
_type = Type.Fixed;
} else if (t == "Scalable") {
_type = Type.Scalable;
}
}
_context = group.value("Context");
_name = group.groupName();
}
@safe this(uint size, Type type = Type.Threshold, string context = null, uint minSize = 0, uint maxSize = 0, uint threshold = 2) nothrow pure
{
_size = size;
_context = context;
_type = type;
_minSize = minSize ? minSize : size;
_maxSize = maxSize ? maxSize : size;
_threshold = threshold;
}
/**
* The name of section in icon theme file and relative path to icons.
*/
@nogc @safe string name() const nothrow pure {
return _name;
}
/**
* Nominal size of the icons in this directory.
* Returns: The value associated with "Size" key converted to an unsigned integer, or 0 if the value is not present or not a number.
*/
@nogc @safe uint size() const nothrow pure {
return _size;
}
/**
* The context the icon is normally used in.
* Returns: The value associated with "Context" key.
*/
@nogc @safe string context() const nothrow pure {
return _context;
}
/**
* The type of icon sizes for the icons in this directory.
* Returns: The value associated with "Type" key or Type.Threshold if not specified.
*/
@nogc @safe Type type() const nothrow pure {
return _type;
}
/**
* The maximum size that the icons in this directory can be scaled to. Defaults to the value of Size if not present.
* Returns: The value associated with "MaxSize" key converted to an unsigned integer, or size() if the value is not present or not a number.
* See_Also: $(D size), $(D minSize)
*/
@nogc @safe uint maxSize() const nothrow pure {
return _maxSize;
}
/**
* The minimum size that the icons in this directory can be scaled to. Defaults to the value of Size if not present.
* Returns: The value associated with "MinSize" key converted to an unsigned integer, or size() if the value is not present or not a number.
* See_Also: $(D size), $(D maxSize)
*/
@nogc @safe uint minSize() const nothrow pure {
return _minSize;
}
/**
* The icons in this directory can be used if the size differ at most this much from the desired size. Defaults to 2 if not present.
* Returns: The value associated with "Threshold" key, or 2 if the value is not present or not a number.
*/
@nogc @safe uint threshold() const nothrow pure {
return _threshold;
}
private:
uint _size;
uint _minSize;
uint _maxSize;
uint _threshold;
Type _type;
string _context;
string _name;
}
final class IconThemeGroup : IniLikeGroup
{
protected @nogc @safe this() nothrow {
super("Icon Theme");
}
/**
* Short name of the icon theme, used in e.g. lists when selecting themes.
* Returns: The value associated with "Name" key.
* See_Also: $(D IconThemeFile.internalName), $(D localizedDisplayName)
*/
@safe string displayName() const nothrow pure {
return readEntry("Name");
}
/**
* Set "Name" to name escaping the value if needed.
*/
@safe string displayName(string name) {
return writeEntry("Name", name);
}
///Returns: Localized name of icon theme.
@safe string localizedDisplayName(string locale) const nothrow pure {
return readEntry("Name", locale);
}
/**
* Longer string describing the theme.
* Returns: The value associated with "Comment" key.
*/
@safe string comment() const nothrow pure {
return readEntry("Comment");
}
/**
* Set "Comment" to commentary escaping the value if needed.
*/
@safe string comment(string commentary) {
return writeEntry("Comment", commentary);
}
///Returns: Localized comment.
@safe string localizedComment(string locale) const nothrow pure {
return readEntry("Comment", locale);
}
/**
* Whether to hide the theme in a theme selection user interface.
* Returns: The value associated with "Hidden" key converted to bool using isTrue.
*/
@nogc @safe bool hidden() const nothrow pure {
return isTrue(value("Hidden"));
}
///setter
@safe bool hidden(bool hide) {
this["Hidden"] = boolToString(hide);
return hide;
}
/**
* The name of an icon that should be used as an example of how this theme looks.
* Returns: The value associated with "Example" key.
*/
@safe string example() const nothrow pure {
return readEntry("Example");
}
/**
* Set "Example" to example escaping the value if needed.
*/
@safe string example(string example) {
return writeEntry("Example", example);
}
/**
* List of subdirectories for this theme.
* Returns: The range of values associated with "Directories" key.
*/
@safe auto directories() const {
return IconThemeFile.splitValues(readEntry("Directories"));
}
///setter
string directories(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) {
return writeEntry("Directories", IconThemeFile.joinValues(values));
}
/**
* Names of themes that this theme inherits from.
* Returns: The range of values associated with "Inherits" key.
* Note: It does NOT automatically adds hicolor theme if it's missing.
*/
@safe auto inherits() const {
return IconThemeFile.splitValues(readEntry("Inherits"));
}
///setter
string inherits(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) {
return writeEntry("Inherits", IconThemeFile.joinValues(values));
}
protected:
@trusted override void validateKey(string key, string value) const {
if (!isValidDesktopFileKey(key)) {
throw new IniLikeEntryException("key is invalid", groupName(), key, value);
}
}
}
/**
* Class representation of index.theme file containing an icon theme description.
*/
final class IconThemeFile : IniLikeFile
{
/**
* Policy about reading extension groups (those start with 'X-').
*/
enum ExtensionGroupPolicy : ubyte {
skip, ///Don't save extension groups.
preserve ///Save extension groups.
}
/**
* Policy about reading groups with names which meaning is unknown, i.e. it's not extension nor relative directory path.
*/
enum UnknownGroupPolicy : ubyte {
skip, ///Don't save unknown groups.
preserve, ///Save unknown groups.
throwError ///Throw error when unknown group is encountered.
}
///Options to manage icon theme file reading
static struct IconThemeReadOptions
{
///Base $(D inilike.file.IniLikeFile.ReadOptions).
IniLikeFile.ReadOptions baseOptions = IniLikeFile.ReadOptions(IniLikeFile.DuplicateGroupPolicy.skip);
alias baseOptions this;
/**
* Set policy about unknown groups. By default they are skipped without errors.
* Note that all groups still need to be preserved if desktop file must be rewritten.
*/
UnknownGroupPolicy unknownGroupPolicy = UnknownGroupPolicy.skip;
/**
* Set policy about extension groups. By default they are all preserved.
* Set it to skip if you're not willing to support any extensions in your applications.
* Note that all groups still need to be preserved if desktop file must be rewritten.
*/
ExtensionGroupPolicy extensionGroupPolicy = ExtensionGroupPolicy.preserve;
///Setting parameters in any order, leaving not mentioned ones in default state.
@nogc @safe this(Args...)(Args args) nothrow pure {
foreach(arg; args) {
alias Unqual!(typeof(arg)) ArgType;
static if (is(ArgType == IniLikeFile.ReadOptions)) {
baseOptions = arg;
} else static if (is(ArgType == UnknownGroupPolicy)) {
unknownGroupPolicy = arg;
} else static if (is(ArgType == ExtensionGroupPolicy)) {
extensionGroupPolicy = arg;
} else {
baseOptions.assign(arg);
}
}
}
///
unittest
{
IconThemeReadOptions options;
options = IconThemeReadOptions(
ExtensionGroupPolicy.skip,
UnknownGroupPolicy.preserve,
DuplicateKeyPolicy.skip,
DuplicateGroupPolicy.preserve,
No.preserveComments
);
assert(options.unknownGroupPolicy == UnknownGroupPolicy.preserve);
assert(options.extensionGroupPolicy == ExtensionGroupPolicy.skip);
assert(options.duplicateGroupPolicy == DuplicateGroupPolicy.preserve);
assert(options.duplicateKeyPolicy == DuplicateKeyPolicy.skip);
assert(!options.preserveComments);
}
}
///
unittest
{
string contents =
`[Icon Theme]
Name=Theme
[X-SomeGroup]
Key=Value`;
alias IconThemeFile.IconThemeReadOptions IconThemeReadOptions;
auto iconTheme = new IconThemeFile(iniLikeStringReader(contents), IconThemeReadOptions(ExtensionGroupPolicy.skip));
assert(iconTheme.group("X-SomeGroup") is null);
contents =
`[Icon Theme]
Name=Theme
[/invalid group]
$=StrangeKey`;
iconTheme = new IconThemeFile(iniLikeStringReader(contents), IconThemeReadOptions(UnknownGroupPolicy.preserve, IniLikeGroup.InvalidKeyPolicy.save));
assert(iconTheme.group("/invalid group") !is null);
assert(iconTheme.group("/invalid group").value("$") == "StrangeKey");
contents =
`[X-SomeGroup]
Key=Value`;
auto thrown = collectException!IniLikeReadException(new IconThemeFile(iniLikeStringReader(contents)));
assert(thrown !is null);
assert(thrown.lineNumber == 0);
contents =
`[Icon Theme]
Valid=Key
$=Invalid`;
assertThrown(new IconThemeFile(iniLikeStringReader(contents)));
assertNotThrown(new IconThemeFile(iniLikeStringReader(contents), IconThemeReadOptions(IniLikeGroup.InvalidKeyPolicy.skip)));
contents =
`[Icon Theme]
Name=Name
[/invalidpath]
Key=Value`;
assertThrown(new IconThemeFile(iniLikeStringReader(contents), IconThemeReadOptions(UnknownGroupPolicy.throwError)));
assertNotThrown(iconTheme = new IconThemeFile(iniLikeStringReader(contents), IconThemeReadOptions(UnknownGroupPolicy.preserve)));
assert(iconTheme.cachePath().empty);
assert(iconTheme.group("/invalidpath") !is null);
}
protected:
@trusted static bool isDirectoryName(string groupName)
{
return groupName.pathSplitter.all!isValidFilename;
}
@trusted override IniLikeGroup createGroupByName(string groupName) {
if (groupName == "Icon Theme") {
_iconTheme = new IconThemeGroup();
return _iconTheme;
} else if (groupName.startsWith("X-")) {
if (_options.extensionGroupPolicy == ExtensionGroupPolicy.skip) {
return null;
} else {
return createEmptyGroup(groupName);
}
} else if (isDirectoryName(groupName)) {
return createEmptyGroup(groupName);
} else {
final switch(_options.unknownGroupPolicy) {
case UnknownGroupPolicy.skip:
return null;
case UnknownGroupPolicy.preserve:
return createEmptyGroup(groupName);
case UnknownGroupPolicy.throwError:
throw new IniLikeException("Invalid group name: '" ~ groupName ~ "'. Must be valid relative path or start with 'X-'");
}
}
}
public:
/**
* Reads icon theme from file.
* Throws:
* $(B ErrnoException) if file could not be opened.
* $(D inilike.file.IniLikeReadException) if error occured while reading the file.
*/
@trusted this(string fileName, IconThemeReadOptions options = IconThemeReadOptions.init) {
this(iniLikeFileReader(fileName), options, fileName);
}
/**
* Reads icon theme file from range of IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader.
* Throws:
* $(D inilike.file.IniLikeReadException) if error occured while parsing.
*/
this(IniLikeReader)(IniLikeReader reader, IconThemeReadOptions options = IconThemeReadOptions.init, string fileName = null)
{
_options = options;
super(reader, fileName, options.baseOptions);
enforce(_iconTheme !is null, new IniLikeReadException("No \"Icon Theme\" group", 0));
}
///ditto
this(IniLikeReader)(IniLikeReader reader, string fileName, IconThemeReadOptions options = IconThemeReadOptions.init)
{
this(reader, options, fileName);
}
/**
* Constructs IconThemeFile with empty "Icon Theme" group.
*/
@safe this() {
super();
_iconTheme = new IconThemeGroup();
}
///
unittest
{
auto itf = new IconThemeFile();
assert(itf.iconTheme());
assert(itf.directories().empty);
}
/**
* Removes group by name. This function will not remove "Icon Theme" group.
*/
@safe override bool removeGroup(string groupName) nothrow {
if (groupName != "Icon Theme") {
return super.removeGroup(groupName);
}
return false;
}
/**
* The name of the subdirectory index.theme was loaded from.
* See_Also: $(D IconThemeGroup.displayName)
*/
@trusted string internalName() const {
return fileName().absolutePath().dirName().baseName();
}
/**
* Some keys can have multiple values, separated by comma. This function helps to parse such kind of strings into the range.
* Returns: The range of multiple nonempty values.
* See_Also: $(D joinValues)
*/
@trusted static auto splitValues(string values) {
return std.algorithm.splitter(values, ',').filter!(s => s.length != 0);
}
///
unittest
{
assert(equal(IconThemeFile.splitValues("16x16/actions,16x16/animations,16x16/apps"), ["16x16/actions", "16x16/animations", "16x16/apps"]));
assert(IconThemeFile.splitValues(",").empty);
assert(IconThemeFile.splitValues("").empty);
}
/**
* Join range of multiple values into a string using comma as separator.
* If range is empty, then the empty string is returned.
* See_Also: $(D splitValues)
*/
static string joinValues(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) {
auto result = values.filter!( s => s.length != 0 ).joiner(",");
if (result.empty) {
return string.init;
} else {
return text(result);
}
}
///
unittest
{
assert(equal(IconThemeFile.joinValues(["16x16/actions", "16x16/animations", "16x16/apps"]), "16x16/actions,16x16/animations,16x16/apps"));
assert(IconThemeFile.joinValues([""]).empty);
}
/**
* Iterating over subdirectories of icon theme.
* See_Also: $(D IconThemeGroup.directories)
*/
@trusted auto bySubdir() const {
return directories().filter!(dir => group(dir) !is null).map!(dir => IconSubDir(group(dir))).array;
}
/**
* Icon Theme group in underlying file.
* Returns: Instance of "Icon Theme" group.
* Note: Usually you don't need to call this function since you can rely on alias this.
*/
@nogc @safe inout(IconThemeGroup) iconTheme() nothrow inout {
return _iconTheme;
}
/**
* This alias allows to call functions related to "Icon Theme" group without need to call iconTheme explicitly.
*/
alias iconTheme this;
/**
* Try to load icon cache. Loaded icon cache will be used on icon lookup.
* Returns: Loaded $(D icontheme.cache.IconThemeCache) object or null, if cache does not exist or invalid or outdated.
* Note: This function expects that icon theme has fileName.
* See_Also: $(D icontheme.cache.IconThemeCache), $(D icontheme.lookup.lookupIcon), $(D cache), $(D unloadCache), $(D cachePath)
*/
@trusted auto tryLoadCache(Flag!"allowOutdated" allowOutdated = Flag!"allowOutdated".no) nothrow
{
string path = cachePath();
bool isOutdated = true;
collectException(IconThemeCache.isOutdated(path), isOutdated);
if (isOutdated && !allowOutdated) {
return null;
}
IconThemeCache myCache;
collectException(new IconThemeCache(path), myCache);
if (myCache !is null) {
_cache = myCache;
}
return myCache;
}
/**
* Unset loaded cache.
*/
@nogc @safe void unloadCache() nothrow {
_cache = null;
}
/**
* Set cache object.
* See_Also: $(D tryLoadCache)
*/
@nogc @safe IconThemeCache cache(IconThemeCache setCache) nothrow {
_cache = setCache;
return _cache;
}
/**
* The object of loaded cache.
* Returns: $(D icontheme.cache.IconThemeCache) object loaded via tryLoadCache or set by cache property.
*/
@nogc @safe inout(IconThemeCache) cache() inout nothrow {
return _cache;
}
/**
* Path of icon theme cache file.
* Returns: Path to icon-theme.cache of corresponding cache file.
* Note: This function expects that icon theme has fileName. This function does not check if the cache file exists.
*/
@trusted string cachePath() const nothrow {
auto f = fileName();
if (f.length) {
return buildPath(fileName().dirName, "icon-theme.cache");
} else {
return null;
}
}
private:
IconThemeReadOptions _options;
IconThemeGroup _iconTheme;
IconThemeCache _cache;
}
///
unittest
{
string contents =
`# First comment
[Icon Theme]
Name=Hicolor
Name[ru]=Стандартная тема
Comment=Fallback icon theme
Comment[ru]=Резервная тема
Hidden=true
Directories=16x16/actions,32x32/animations,scalable/emblems
Example=folder
Inherits=gnome,hicolor
[16x16/actions]
Size=16
Context=Actions
Type=Threshold
[32x32/animations]
Size=32
Context=Animations
Type=Fixed
[scalable/emblems]
Context=Emblems
Size=64
MinSize=8
MaxSize=512
Type=Scalable
# Will be saved.
[X-NoName]
Key=Value`;
string path = buildPath(".", "test", "Tango", "index.theme");
auto iconTheme = new IconThemeFile(iniLikeStringReader(contents), path);
assert(equal(iconTheme.leadingComments(), ["# First comment"]));
assert(iconTheme.displayName() == "Hicolor");
assert(iconTheme.localizedDisplayName("ru") == "Стандартная тема");
assert(iconTheme.comment() == "Fallback icon theme");
assert(iconTheme.localizedComment("ru") == "Резервная тема");
assert(iconTheme.hidden());
assert(equal(iconTheme.directories(), ["16x16/actions", "32x32/animations", "scalable/emblems"]));
assert(equal(iconTheme.inherits(), ["gnome", "hicolor"]));
assert(iconTheme.internalName() == "Tango");
assert(iconTheme.example() == "folder");
assert(iconTheme.group("X-NoName") !is null);
iconTheme.removeGroup("Icon Theme");
assert(iconTheme.group("Icon Theme") !is null);
assert(iconTheme.cachePath() == buildPath(".", "test", "Tango", "icon-theme.cache"));
assert(equal(iconTheme.bySubdir().map!(subdir => tuple(subdir.name(), subdir.size(), subdir.minSize(), subdir.maxSize(), subdir.context(), subdir.type() )),
[tuple("16x16/actions", 16, 16, 16, "Actions", IconSubDir.Type.Threshold),
tuple("32x32/animations", 32, 32, 32, "Animations", IconSubDir.Type.Fixed),
tuple("scalable/emblems", 64, 8, 512, "Emblems", IconSubDir.Type.Scalable)]));
version(iconthemeFileTest)
{
string cachePath = iconTheme.cachePath();
assert(cachePath.exists);
auto cache = new IconThemeCache(cachePath);
assert(iconTheme.cache is null);
iconTheme.cache = cache;
assert(iconTheme.cache is cache);
iconTheme.unloadCache();
assert(iconTheme.cache is null);
assert(iconTheme.tryLoadCache(Flag!"allowOutdated".yes));
}
iconTheme.removeGroup("scalable/emblems");
assert(iconTheme.group("scalable/emblems") is null);
auto itf = new IconThemeFile();
itf.displayName = "Oxygen";
itf.comment = "Oxygen theme";
itf.hidden = true;
itf.directories = ["actions", "places"];
itf.inherits = ["locolor", "hicolor"];
assert(itf.displayName() == "Oxygen");
assert(itf.comment() == "Oxygen theme");
assert(itf.hidden());
assert(equal(itf.directories(), ["actions", "places"]));
assert(equal(itf.inherits(), ["locolor", "hicolor"]));
}

735
3rdparty/icontheme/lookup.d vendored Normal file
View File

@ -0,0 +1,735 @@
/**
* Lookup of icon themes and icons.
*
* Note: All found icons are just paths. They are not verified to be valid images.
*
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification)
*/
module icontheme.lookup;
import icontheme.file;
package {
import std.file;
import std.path;
import std.range;
import std.traits;
import std.typecons;
}
@trusted bool isDirNothrow(string dir) nothrow
{
bool ok;
collectException(dir.isDir(), ok);
return ok;
}
@trusted bool isFileNothrow(string file) nothrow
{
bool ok;
collectException(file.isFile(), ok);
return ok;
}
/**
* Default icon extensions. This array includes .png and .xpm.
* PNG is recommended format.
* XPM is kept for backward compatibility.
*
* Note: Icon Theme Specificiation also lists .svg as possible format,
* but it's less common to have SVG support for applications,
* hence this format is defined as optional by specificiation.
* If your application has proper support for SVG images,
* array should include it in the first place as the most preferred format
* because SVG images are scalable.
*/
enum defaultIconExtensions = [".png", ".xpm"];
/**
* Find all icon themes in searchIconDirs.
* Note:
* You may want to skip icon themes duplicates if there're different versions of the index.theme file for the same theme.
* Returns:
* Range of paths to index.theme files represented icon themes.
* Params:
* searchIconDirs = base icon directories to search icon themes.
* See_Also: $(D icontheme.paths.baseIconDirs)
*/
auto iconThemePaths(Range)(Range searchIconDirs)
if(is(ElementType!Range : string))
{
return searchIconDirs
.filter!(function(dir) {
bool ok;
collectException(dir.isDir, ok);
return ok;
}).map!(function(iconDir) {
return iconDir.dirEntries(SpanMode.shallow)
.map!(p => buildPath(p, "index.theme")).cache()
.filter!(isFileNothrow);
}).joiner;
}
///
version(iconthemeFileTest) unittest
{
auto paths = iconThemePaths(["test"]).array;
assert(paths.length == 3);
assert(paths.canFind(buildPath("test", "NewTango", "index.theme")));
assert(paths.canFind(buildPath("test", "Tango", "index.theme")));
assert(paths.canFind(buildPath("test", "hicolor", "index.theme")));
}
/**
* Lookup index.theme files by theme name.
* Params:
* themeName = theme name.
* searchIconDirs = base icon directories to search icon themes.
* Returns:
* Range of paths to index.theme file corresponding to the given theme.
* Note:
* Usually you want to use the only first found file.
* See_Also: $(D icontheme.paths.baseIconDirs), $(D findIconTheme)
*/
auto lookupIconTheme(Range)(string themeName, Range searchIconDirs)
if(is(ElementType!Range : string))
{
return searchIconDirs
.map!(dir => buildPath(dir, themeName, "index.theme")).cache()
.filter!(isFileNothrow);
}
/**
* Find index.theme file by theme name.
* Returns:
* Path to the first found index.theme file or null string if not found.
* Params:
* themeName = Theme name.
* searchIconDirs = Base icon directories to search icon themes.
* Returns:
* Path to the first found index.theme file corresponding to the given theme.
* See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIconTheme)
*/
auto findIconTheme(Range)(string themeName, Range searchIconDirs)
{
auto paths = lookupIconTheme(themeName, searchIconDirs);
if (paths.empty) {
return null;
} else {
return paths.front;
}
}
/**
* Find index.theme file for given theme and create instance of $(D icontheme.file.IconThemeFile). The first found file will be used.
* Returns: $(D icontheme.file.IconThemeFile) object read from the first found index.theme file corresponding to given theme or null if none were found.
* Params:
* themeName = theme name.
* searchIconDirs = base icon directories to search icon themes.
* options = options for $(D icontheme.file.IconThemeFile) reading.
* Throws:
* $(B ErrnoException) if file could not be opened.
* $(B IniLikeException) if error occured while reading the file.
* See_Also: $(D findIconTheme), $(D icontheme.paths.baseIconDirs)
*/
IconThemeFile openIconTheme(Range)(string themeName,
Range searchIconDirs,
IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init)
{
auto path = findIconTheme(themeName, searchIconDirs);
return path.empty ? null : new IconThemeFile(to!string(path), options);
}
///
version(iconthemeFileTest) unittest
{
auto tango = openIconTheme("Tango", ["test"]);
assert(tango);
assert(tango.displayName() == "Tango");
auto hicolor = openIconTheme("hicolor", ["test"]);
assert(hicolor);
assert(hicolor.displayName() == "Hicolor");
assert(openIconTheme("Nonexistent", ["test"]) is null);
}
/**
* Result of icon lookup.
*/
struct IconSearchResult(IconTheme) if (is(IconTheme : const(IconThemeFile)))
{
/**
* File path of found icon.
*/
string filePath;
/**
* Subdirectory the found icon belongs to.
*/
IconSubDir subdir;
/**
* $(D icontheme.file.IconThemeFile) the found icon belongs to.
*/
IconTheme iconTheme;
}
/**
* Lookup icon alternatives in icon themes. It uses icon theme cache wherever it's loaded. If searched icon is found in some icon theme all subsequent themes are ignored.
*
* This function may require many $(B stat) calls, so beware. Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) properties (e.g. by size or context) to decrease the number of searchable items and allocations. Loading $(D icontheme.cache.IconThemeCache) may also descrease the number of stats.
*
* Params:
* iconName = Icon name.
* iconThemes = Icon themes to search icon in.
* searchIconDirs = Case icon directories.
* extensions = Possible file extensions of needed icon file, in order of preference.
* sink = Output range accepting $(D IconSearchResult)s.
* reverse = Iterate over icon theme sub-directories in reverse way.
* Usually directories with larger icon size are listed the last,
* so this parameter may speed up the search when looking for the largest icon.
* Note: Specification says that extension must be ".png", ".xpm" or ".svg", though SVG is not required to be supported. Some icon themes also contain .svgz images.
* Example:
----------
lookupIcon!(subdir => subdir.context == "Places" && subdir.size >= 32)(
"folder", iconThemes, baseIconDirs(), [".png", ".xpm"],
delegate void (IconSearchResult!IconThemeFile item) {
writefln("Icon file: %s. Context: %s. Size: %s. Theme: %s", item.filePath, item.subdir.context, item.subdir.size, item.iconTheme.displayName);
});
----------
* See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupFallbackIcon)
*/
void lookupIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts, OutputRange)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, OutputRange sink, Flag!"reverse" reverse = No.reverse)
if (isInputRange!(IconThemes) && isForwardRange!(BaseDirs) && isForwardRange!(Exts) &&
is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) &&
is(ElementType!Exts : string) && isOutputRange!(OutputRange, IconSearchResult!(ElementType!IconThemes)))
{
bool onExtensions(string themeBaseDir, IconSubDir subdir, ElementType!IconThemes iconTheme)
{
string subdirPath = buildPath(themeBaseDir, subdir.name);
if (!subdirPath.isDirNothrow) {
return false;
}
bool found;
foreach(extension; extensions) {
string path = buildPath(subdirPath, iconName ~ extension);
if (path.isFileNothrow) {
found = true;
put(sink, IconSearchResult!(ElementType!IconThemes)(path, subdir, iconTheme));
}
}
return found;
}
foreach(iconTheme; iconThemes) {
if (iconTheme is null || iconTheme.internalName().length == 0) {
continue;
}
string[] themeBaseDirs = searchIconDirs.map!(dir => buildPath(dir, iconTheme.internalName())).filter!(isDirNothrow).array;
bool found;
auto bySubdir = choose(reverse, iconTheme.bySubdir().retro(), iconTheme.bySubdir());
foreach(subdir; bySubdir) {
if (!subdirFilter(subdir)) {
continue;
}
foreach(themeBaseDir; themeBaseDirs) {
if (iconTheme.cache !is null && themeBaseDir == iconTheme.cache.fileName.dirName) {
if (iconTheme.cache.containsIcon(iconName, subdir.name)) {
found = onExtensions(themeBaseDir, subdir, iconTheme) || found;
}
} else {
found = onExtensions(themeBaseDir, subdir, iconTheme) || found;
}
}
}
if (found) {
return;
}
}
}
/**
* Iterate over all icons in icon themes.
* iconThemes is usually the range of the main theme and themes it inherits from.
* Note: Usually if some icon was found in icon theme, it should be ignored in all subsequent themes, including sizes not presented in former theme.
* Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) thus decreasing the number of searchable items and allocations.
* Returns: Range of $(D IconSearchResult).
* Params:
* iconThemes = icon themes to search icon in.
* searchIconDirs = base icon directories.
* extensions = possible file extensions for icon files.
* Example:
-------------
foreach(item; lookupThemeIcons!(subdir => subdir.context == "MimeTypes" && subdir.size >= 32)(iconThemes, baseIconDirs(), [".png", ".xpm"]))
{
writefln("Icon file: %s. Context: %s. Size: %s", item.filePath, item.subdir.context, item.subdir.size);
}
-------------
* See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D openBaseThemes)
*/
auto lookupThemeIcons(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions)
if (is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) && is (ElementType!Exts : string))
{
return iconThemes.filter!(iconTheme => iconTheme !is null).map!(
iconTheme => iconTheme.bySubdir().filter!(subdirFilter).map!(
subdir => searchIconDirs.map!(
basePath => buildPath(basePath, iconTheme.internalName(), subdir.name)
).filter!(isDirNothrow).map!(
subdirPath => subdirPath.dirEntries(SpanMode.shallow).filter!(
filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension)
).map!(filePath => IconSearchResult!(ElementType!IconThemes)(filePath, subdir, iconTheme))
).joiner
).joiner
).joiner;
}
/**
* Iterate over all icons out of icon themes.
* Returns: Range of found icon file paths.
* Params:
* searchIconDirs = base icon directories.
* extensions = possible file extensions for icon files.
* See_Also:
* $(D lookupFallbackIcon), $(D icontheme.paths.baseIconDirs)
*/
auto lookupFallbackIcons(BaseDirs, Exts)(BaseDirs searchIconDirs, Exts extensions)
if (isInputRange!(BaseDirs) && isForwardRange!(Exts) &&
is(ElementType!BaseDirs : string) && is(ElementType!Exts : string))
{
return searchIconDirs.filter!(isDirNothrow).map!(basePath => basePath.dirEntries(SpanMode.shallow).filter!(
filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension)
)).joiner;
}
/**
* Lookup icon alternatives beyond the icon themes. May be used as fallback lookup, if lookupIcon returned empty range.
* Returns: The range of found icon file paths.
* Example:
----------
auto result = lookupFallbackIcon("folder", baseIconDirs(), [".png", ".xpm"]);
----------
* See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D lookupFallbackIcons)
*/
auto lookupFallbackIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions)
if (isInputRange!(BaseDirs) && isForwardRange!(Exts) &&
is(ElementType!BaseDirs : string) && is(ElementType!Exts : string))
{
return searchIconDirs.map!(basePath =>
extensions
.map!(extension => buildPath(basePath, iconName ~ extension)).cache()
.filter!(isFileNothrow)
).joiner;
}
/**
* Find fallback icon outside of icon themes. The first found is returned.
* See_Also: $(D lookupFallbackIcon), $(D icontheme.paths.baseIconDirs)
*/
string findFallbackIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions)
{
auto r = lookupFallbackIcon(iconName, searchIconDirs, extensions);
if (r.empty) {
return null;
} else {
return r.front;
}
}
///
version(iconthemeFileTest) unittest
{
assert(findFallbackIcon("pidgin", ["test"], defaultIconExtensions) == buildPath("test", "pidgin.png"));
assert(findFallbackIcon("nonexistent", ["test"], defaultIconExtensions).empty);
}
/**
* Find icon closest of the size. It uses icon theme cache wherever possible. The first perfect match is used.
* Params:
* iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts).
* size = Preferred icon size to get.
* iconThemes = Range of $(D icontheme.file.IconThemeFile) objects.
* searchIconDirs = Base icon directories.
* extensions = Allowed file extensions.
* allowFallback = Allow searching for non-themed fallback if could not find icon in themes (non-themed icon can be any size).
* Returns: Icon file path or empty string if not found.
* Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with closer size. Therefore the icon found in the more preferred theme always has presedence over icons from other themes.
* See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findFallbackIcon), $(D iconSizeDistance)
*/
string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback = Yes.allowFallbackIcon)
{
uint minDistance = uint.max;
string closest;
lookupIcon!(delegate bool(const(IconSubDir) subdir) {
return minDistance != 0 && subdirFilter(subdir) && iconSizeDistance(subdir, size) <= minDistance;
})(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) {
auto path = t.filePath;
auto subdir = t.subdir;
auto theme = t.iconTheme;
uint distance = iconSizeDistance(subdir, size);
if (distance < minDistance) {
minDistance = distance;
closest = path;
}
});
if (closest.empty && allowFallback) {
return findFallbackIcon(iconName, searchIconDirs, extensions);
} else {
return closest;
}
}
///
version(iconthemeFileTest) unittest
{
auto baseDirs = ["test"];
auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)];
string found;
//exact match
found = findClosestIcon("folder", 32, iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
found = findClosestIcon("folder", 24, iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "24x24/devices", "folder.png"));
found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 32, iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 24, iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("folder", 32, iconThemes, baseDirs);
assert(found.empty);
//hicolor has exact match, but Tango is more preferred.
found = findClosestIcon("folder", 64, iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "32x32/places", "folder.png"));
//find xpm
found = findClosestIcon("folder", 32, iconThemes, baseDirs, [".xpm"]);
assert(found == buildPath("test", "Tango", "32x32/places", "folder.xpm"));
//find big png, not exact match
found = findClosestIcon("folder", 200, iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "128x128/places", "folder.png"));
//svg is closer
found = findClosestIcon("folder", 200, iconThemes, baseDirs, [".png", ".svg"]);
assert(found == buildPath("test", "Tango", "scalable/places", "folder.svg"));
//lookup with fallback
found = findClosestIcon("pidgin", 96, iconThemes, baseDirs);
assert(found == buildPath("test", "pidgin.png"));
//lookup without fallback
found = findClosestIcon("pidgin", 96, iconThemes, baseDirs, defaultIconExtensions, No.allowFallbackIcon);
assert(found.empty);
found = findClosestIcon("text-plain", 48, iconThemes, baseDirs);
assert(found == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png"));
found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("text-plain", 48, iconThemes, baseDirs);
assert(found == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png"));
found = findClosestIcon!(subdir => subdir.context == "Actions")("text-plain", 48, iconThemes, baseDirs);
assert(found.empty);
}
/**
* ditto, but with predefined extensions and fallback allowed.
* See_Also: $(D defaultIconExtensions)
*/
string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs)
{
return findClosestIcon!subdirFilter(iconName, size, iconThemes, searchIconDirs, defaultIconExtensions);
}
/**
* Find icon of the largest size. It uses icon theme cache wherever possible.
* Params:
* iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts).
* iconThemes = Range of $(D icontheme.file.IconThemeFile) objects.
* searchIconDirs = Base icon directories.
* extensions = Allowed file extensions.
* allowFallback = Allow searching for non-themed fallback if could not find icon in themes.
* Returns: Icon file path or empty string if not found.
* Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with larger size. Therefore the icon found in the most preferred theme always has presedence over icons from other themes.
* See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findFallbackIcon)
*/
string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback = Yes.allowFallbackIcon)
{
uint max = 0;
string largest;
lookupIcon!(delegate bool(const(IconSubDir) subdir) {
return subdirFilter(subdir) && subdir.size() >= max;
})(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) {
auto path = t.filePath;
auto subdir = t.subdir;
auto theme = t.iconTheme;
if (subdir.size() > max) {
max = subdir.size();
largest = path;
}
}, Yes.reverse);
if (largest.empty && allowFallback) {
return findFallbackIcon(iconName, searchIconDirs, extensions);
} else {
return largest;
}
}
///
version(iconthemeFileTest) unittest
{
auto baseDirs = ["test"];
auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)];
string found;
found = findLargestIcon("folder", iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "128x128/places", "folder.png"));
found = findLargestIcon("desktop", iconThemes, baseDirs);
assert(found == buildPath("test", "Tango", "32x32/places", "desktop.png"));
found = findLargestIcon("desktop", iconThemes, baseDirs, [".svg", ".png"]);
assert(found == buildPath("test", "Tango", "scalable/places", "desktop.svg"));
//lookup with fallback
found = findLargestIcon("pidgin", iconThemes, baseDirs);
assert(found == buildPath("test", "pidgin.png"));
//lookup without fallback
found = findLargestIcon("pidgin", iconThemes, baseDirs, defaultIconExtensions, No.allowFallbackIcon);
assert(found.empty);
}
/**
* ditto, but with predefined extensions and fallback allowed.
* See_Also: $(D defaultIconExtensions)
*/
string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs)
{
return findLargestIcon!subdirFilter(iconName, iconThemes, searchIconDirs, defaultIconExtensions);
}
/**
* Distance between desired size and minimum or maximum size value supported by icon theme subdirectory.
*/
@nogc @safe uint iconSizeDistance(in IconSubDir subdir, uint matchSize) nothrow pure
{
const uint size = subdir.size();
const uint minSize = subdir.minSize();
const uint maxSize = subdir.maxSize();
const uint threshold = subdir.threshold();
final switch(subdir.type()) {
case IconSubDir.Type.Fixed:
{
if (size > matchSize) {
return size - matchSize;
} else if (size < matchSize) {
return matchSize - size;
} else {
return 0;
}
}
case IconSubDir.Type.Scalable:
{
if (matchSize < minSize) {
return minSize - matchSize;
} else if (matchSize > maxSize) {
return matchSize - maxSize;
} else {
return 0;
}
}
case IconSubDir.Type.Threshold:
{
if (matchSize < size - threshold) {
return (size - threshold) - matchSize;
} else if (matchSize > size + threshold) {
return matchSize - (size + threshold);
} else {
return 0;
}
}
}
}
///
unittest
{
auto fixed = IconSubDir(32, IconSubDir.Type.Fixed);
assert(iconSizeDistance(fixed, fixed.size()) == 0);
assert(iconSizeDistance(fixed, 30) == 2);
assert(iconSizeDistance(fixed, 35) == 3);
auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5);
assert(iconSizeDistance(threshold, threshold.size()) == 0);
assert(iconSizeDistance(threshold, threshold.size() - threshold.threshold()) == 0);
assert(iconSizeDistance(threshold, threshold.size() + threshold.threshold()) == 0);
assert(iconSizeDistance(threshold, 26) == 1);
assert(iconSizeDistance(threshold, 39) == 2);
auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48);
assert(iconSizeDistance(scalable, scalable.size()) == 0);
assert(iconSizeDistance(scalable, scalable.minSize()) == 0);
assert(iconSizeDistance(scalable, scalable.maxSize()) == 0);
assert(iconSizeDistance(scalable, 20) == 4);
assert(iconSizeDistance(scalable, 50) == 2);
}
/**
* Check if matchSize belongs to subdir's size range.
*/
@nogc @safe bool matchIconSize(in IconSubDir subdir, uint matchSize) nothrow pure
{
const uint size = subdir.size();
const uint minSize = subdir.minSize();
const uint maxSize = subdir.maxSize();
const uint threshold = subdir.threshold();
final switch(subdir.type()) {
case IconSubDir.Type.Fixed:
return size == matchSize;
case IconSubDir.Type.Threshold:
return matchSize <= (size + threshold) && matchSize >= (size - threshold);
case IconSubDir.Type.Scalable:
return matchSize >= minSize && matchSize <= maxSize;
}
}
///
unittest
{
auto fixed = IconSubDir(32, IconSubDir.Type.Fixed);
assert(matchIconSize(fixed, fixed.size()));
assert(!matchIconSize(fixed, fixed.size() - 2));
auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5);
assert(matchIconSize(threshold, threshold.size() + threshold.threshold()));
assert(matchIconSize(threshold, threshold.size() - threshold.threshold()));
assert(!matchIconSize(threshold, threshold.size() + threshold.threshold() + 1));
assert(!matchIconSize(threshold, threshold.size() - threshold.threshold() - 1));
auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48);
assert(matchIconSize(scalable, scalable.minSize()));
assert(matchIconSize(scalable, scalable.maxSize()));
assert(!matchIconSize(scalable, scalable.minSize() - 1));
assert(!matchIconSize(scalable, scalable.maxSize() + 1));
}
/**
* Find icon closest to the given size among given alternatives.
* Params:
* alternatives = range of $(D IconSearchResult)s, usually returned by $(D lookupIcon).
* matchSize = desired size of icon.
*/
string matchBestIcon(Range)(Range alternatives, uint matchSize)
{
uint minDistance = uint.max;
string closest;
foreach(t; alternatives) {
auto path = t[0];
auto subdir = t[1];
uint distance = iconSizeDistance(subdir, matchSize);
if (distance < minDistance) {
minDistance = distance;
closest = path;
}
if (minDistance == 0) {
return closest;
}
}
return closest;
}
private void openBaseThemesHelper(Range)(ref IconThemeFile[] themes, IconThemeFile iconTheme,
Range searchIconDirs,
IconThemeFile.IconThemeReadOptions options)
{
foreach(name; iconTheme.inherits()) {
if (!themes.canFind!(function(theme, name) {
return theme.internalName == name;
})(name)) {
try {
IconThemeFile f = openIconTheme(name, searchIconDirs, options);
if (f) {
themes ~= f;
openBaseThemesHelper(themes, f, searchIconDirs, options);
}
} catch(Exception e) {
}
}
}
}
/**
* Recursively find all themes the given theme is inherited from.
* Params:
* iconTheme = Original icon theme to search for its base themes. Included as first element in resulting array.
* searchIconDirs = Base icon directories to search icon themes.
* fallbackThemeName = Name of fallback theme which is loaded the last. Not used if empty. It's NOT loaded twice if some theme in inheritance tree has it as base theme.
* options = Options for $(D icontheme.file.IconThemeFile) reading.
* Returns:
* Array of unique $(D icontheme.file.IconThemeFile) objects represented base themes.
*/
IconThemeFile[] openBaseThemes(Range)(IconThemeFile iconTheme,
Range searchIconDirs,
string fallbackThemeName = "hicolor",
IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init)
if(isForwardRange!Range && is(ElementType!Range : string))
{
IconThemeFile[] themes;
openBaseThemesHelper(themes, iconTheme, searchIconDirs, options);
if (fallbackThemeName.length) {
auto fallbackFound = themes.filter!(theme => theme !is null).find!(theme => theme.internalName == fallbackThemeName);
if (fallbackFound.empty) {
IconThemeFile fallbackTheme;
collectException(openIconTheme(fallbackThemeName, searchIconDirs, options), fallbackTheme);
if (fallbackTheme) {
themes ~= fallbackTheme;
}
}
}
return themes;
}
///
version(iconthemeFileTest) unittest
{
auto tango = openIconTheme("NewTango", ["test"]);
auto baseThemes = openBaseThemes(tango, ["test"]);
assert(baseThemes.length == 2);
assert(baseThemes[0].internalName() == "Tango");
assert(baseThemes[1].internalName() == "hicolor");
baseThemes = openBaseThemes(tango, ["test"], null);
assert(baseThemes.length == 1);
assert(baseThemes[0].internalName() == "Tango");
}

19
3rdparty/icontheme/package.d vendored Normal file
View File

@ -0,0 +1,19 @@
/**
* Icon Theme Specification implementation.
*
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification)
*/
module icontheme;
public import icontheme.file;
public import icontheme.lookup;
public import icontheme.cache;
public import icontheme.paths;

233
3rdparty/icontheme/paths.d vendored Normal file
View File

@ -0,0 +1,233 @@
/**
* Getting paths where icon themes and icons are stored.
*
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2017
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification)
*/
module icontheme.paths;
private {
import std.algorithm;
import std.array;
import std.exception;
import std.path;
import std.range;
import std.traits;
import std.process : environment;
import isfreedesktop;
}
version(unittest) {
package struct EnvGuard
{
this(string env) {
envVar = env;
envValue = environment.get(env);
}
~this() {
if (envValue is null) {
environment.remove(envVar);
} else {
environment[envVar] = envValue;
}
}
string envVar;
string envValue;
}
}
static if (isFreedesktop) {
import xdgpaths;
/**
* The list of base directories where icon thems should be looked for as described in $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout, Icon Theme Specification).
*
* $(BLUE This function is Freedesktop only).
* Note: This function does not provide any caching of its results. This function does not check if directories exist.
*/
@safe string[] baseIconDirs() nothrow
{
string[] toReturn;
string homePath;
collectException(environment.get("HOME"), homePath);
if (homePath.length) {
toReturn ~= buildPath(homePath, ".icons");
}
toReturn ~= xdgAllDataDirs("icons");
toReturn ~= "/usr/share/pixmaps";
return toReturn;
}
///
unittest
{
auto homeGuard = EnvGuard("HOME");
auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
environment["HOME"] = "/home/user";
environment["XDG_DATA_HOME"] = "/home/user/data";
environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data";
assert(baseIconDirs() == ["/home/user/.icons", "/home/user/data/icons", "/usr/local/data/icons", "/usr/data/icons", "/usr/share/pixmaps"]);
}
/**
* Writable base icon path. Depends on XDG_DATA_HOME, so this is $HOME/.local/share/icons rather than $HOME/.icons
*
* $(BLUE This function is Freedesktop only).
* Note: it does not check if returned path exists and appears to be directory.
*/
@safe string writableIconsPath() nothrow {
return xdgDataHome("icons");
}
///
unittest
{
auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
environment["XDG_DATA_HOME"] = "/home/user/data";
assert(writableIconsPath() == "/home/user/data/icons");
}
///
enum IconThemeNameDetector
{
none = 0,
fallback = 1, /// Use hardcoded fallback to detect icon theme name depending on the current desktop environment. Has lower priority than other methods.
gtk2 = 2, /// Use gtk2 settings to detect icon theme name. Has lower priority than gtk3.
gtk3 = 4, /// Use gtk3 settings to detect icon theme name.
automatic = fallback | gtk2 | gtk3 /// Use all known means to detect icon theme name.
}
/**
* Try to detect the current icon name configured by user.
*
* $(BLUE This function is Freedesktop only).
* Note: There's no any specification on that so some heuristics are applied.
* Another note: It does not check if the icon theme with the detected name really exists on the file system.
*/
@safe string currentIconThemeName(IconThemeNameDetector detector = IconThemeNameDetector.automatic) nothrow
{
@trusted static string fallbackIconThemeName()
{
string xdgCurrentDesktop = environment.get("XDG_CURRENT_DESKTOP");
switch(xdgCurrentDesktop) {
case "GNOME":
case "X-Cinnamon":
case "MATE":
return "gnome";
case "LXDE":
return "Adwaita";
case "XFCE":
return "Tango";
case "KDE":
return "oxygen"; //TODO: detect KDE version and set breeze if it's KDE5
default:
return "Tango";
}
}
@trusted static string gtk2IconThemeName() nothrow
{
import std.stdio : File;
try {
auto home = environment.get("HOME");
if (!home.length) {
return null;
}
string themeName;
auto gtkConfig = buildPath(home, ".gtkrc-2.0");
auto f = File(gtkConfig, "r");
foreach(line; f.byLine()) {
auto splitted = line.findSplit("=");
if (splitted[0] == "gtk-icon-theme-name") {
if (splitted[2].length > 2 && splitted[2][0] == '"' && splitted[2][$-1] == '"') {
return splitted[2][1..$-1].idup;
}
break;
}
}
} catch(Exception e) {
}
return null;
}
@trusted static string gtk3IconThemeName() nothrow
{
import inilike.file;
try {
auto f = new IniLikeFile(xdgConfigHome("gtk-3.0/settings.ini"), IniLikeFile.ReadOptions(No.preserveComments));
auto settings = f.group("Settings");
if (settings)
return settings.readEntry("gtk-icon-theme-name");
} catch(Exception e) {
}
return null;
}
try {
string themeName;
if (detector & IconThemeNameDetector.gtk3) {
themeName = gtk3IconThemeName();
}
if (!themeName.length && (detector & IconThemeNameDetector.gtk2)) {
themeName = gtk2IconThemeName();
}
if (!themeName.length && (detector & IconThemeNameDetector.fallback)) {
themeName = fallbackIconThemeName();
}
return themeName;
} catch(Exception e) {
}
return null;
}
unittest
{
auto desktopGuard = EnvGuard("XDG_CURRENT_DESKTOP");
environment["XDG_CURRENT_DESKTOP"] = "";
assert(currentIconThemeName(IconThemeNameDetector.fallback).length);
assert(currentIconThemeName(IconThemeNameDetector.none).length == 0);
version(iconthemeFileTest)
{
auto homeGuard = EnvGuard("HOME");
environment["HOME"] = "./test";
auto configGuard = EnvGuard("XDG_CONFIG_HOME");
environment["XDG_CONFIG_HOME"] = "./test";
assert(currentIconThemeName() == "gnome");
assert(currentIconThemeName(IconThemeNameDetector.gtk3) == "gnome");
assert(currentIconThemeName(IconThemeNameDetector.gtk2) == "oxygen");
}
}
}
/**
* The list of icon theme directories based on data paths.
* Returns: Array of paths with "icons" subdirectory appended to each data path.
* Note: This function does not check if directories exist.
*/
@trusted string[] baseIconDirs(Range)(Range dataPaths) if (isInputRange!Range && is(ElementType!Range : string))
{
return dataPaths.map!(p => buildPath(p, "icons")).array;
}
///
unittest
{
auto dataPaths = ["share", buildPath("local", "share")];
assert(equal(baseIconDirs(dataPaths), [buildPath("share", "icons"), buildPath("local", "share", "icons")]));
}

617
3rdparty/inilike/common.d vendored Normal file
View File

@ -0,0 +1,617 @@
/**
* Common functions for dealing with entries in ini-like file.
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
*/
module inilike.common;
package {
import std.algorithm;
import std.range;
import std.string;
import std.traits;
import std.typecons;
import std.conv : to;
static if( __VERSION__ < 2066 ) enum nogc = 1;
auto keyValueTuple(String)(String key, String value)
{
alias KeyValueTuple = Tuple!(String, "key", String, "value");
return KeyValueTuple(key, value);
}
}
private @nogc @safe auto simpleStripLeft(inout(char)[] s) pure nothrow
{
size_t spaceNum = 0;
while(spaceNum < s.length) {
const char c = s[spaceNum];
if (c == ' ' || c == '\t') {
spaceNum++;
} else {
break;
}
}
return s[spaceNum..$];
}
private @nogc @safe auto simpleStripRight(inout(char)[] s) pure nothrow
{
size_t spaceNum = 0;
while(spaceNum < s.length) {
const char c = s[$-1-spaceNum];
if (c == ' ' || c == '\t') {
spaceNum++;
} else {
break;
}
}
return s[0..$-spaceNum];
}
/**
* Test whether the string s represents a comment.
*/
@nogc @safe bool isComment(const(char)[] s) pure nothrow
{
s = s.simpleStripLeft;
return !s.empty && s[0] == '#';
}
///
unittest
{
assert( isComment("# Comment"));
assert( isComment(" # Comment"));
assert(!isComment("Not comment"));
assert(!isComment(""));
}
/**
* Test whether the string s represents a group header.
* Note: "[]" is not considered as valid group header.
*/
@nogc @safe bool isGroupHeader(const(char)[] s) pure nothrow
{
s = s.simpleStripRight;
return s.length > 2 && s[0] == '[' && s[$-1] == ']';
}
///
unittest
{
assert( isGroupHeader("[Group]"));
assert( isGroupHeader("[Group] "));
assert(!isGroupHeader("[]"));
assert(!isGroupHeader("[Group"));
assert(!isGroupHeader("Group]"));
}
/**
* Retrieve group name from header entry.
* Returns: group name or empty string if the entry is not group header.
*/
@nogc @safe auto parseGroupHeader(inout(char)[] s) pure nothrow
{
s = s.simpleStripRight;
if (isGroupHeader(s)) {
return s[1..$-1];
} else {
return null;
}
}
///
unittest
{
assert(parseGroupHeader("[Group name]") == "Group name");
assert(parseGroupHeader("NotGroupName") == string.init);
assert(parseGroupHeader("[Group name]".dup) == "Group name".dup);
}
/**
* Parse entry of kind Key=Value into pair of Key and Value.
* Returns: tuple of key and value strings or tuple of empty strings if it's is not a key-value entry.
* Note: this function does not check whether parsed key is valid key.
*/
@nogc @trusted auto parseKeyValue(String)(String s) pure nothrow if (isSomeString!String && is(ElementEncodingType!String : char))
{
auto t = s.findSplit("=");
auto key = t[0];
auto value = t[2];
if (key.length && t[1].length) {
return keyValueTuple(key, value);
}
return keyValueTuple(String.init, String.init);
}
///
unittest
{
assert(parseKeyValue("Key=Value") == tuple("Key", "Value"));
assert(parseKeyValue("Key=") == tuple("Key", string.init));
assert(parseKeyValue("=Value") == tuple(string.init, string.init));
assert(parseKeyValue("NotKeyValue") == tuple(string.init, string.init));
assert(parseKeyValue("Key=Value".dup) == tuple("Key".dup, "Value".dup));
}
private @nogc @safe bool simpleCanFind(in char[] str, char c) pure nothrow
{
for (size_t i=0; i<str.length; ++i) {
if (str[i] == c) {
return true;
}
}
return false;
}
/**
* Test whether the string is valid key, i.e. does not need escaping, is not a comment and not empty string.
*/
@nogc @safe bool isValidKey(in char[] key) pure nothrow
{
if (key.empty || key.simpleStripLeft.simpleStripRight.empty) {
return false;
}
if (key.isComment || key.simpleCanFind('=') || key.needEscaping()) {
return false;
}
return true;
}
///
unittest
{
assert(isValidKey("Valid key"));
assert(!isValidKey(""));
assert(!isValidKey(" "));
assert(!isValidKey("Sneaky\nKey"));
assert(!isValidKey("# Sneaky key"));
assert(!isValidKey("Sneaky=key"));
}
/**
* Test whether the string is valid key in terms of Desktop File Specification.
*
* Not actually used in $(D inilike.file.IniLikeFile), but can be used in derivatives.
* Only the characters A-Za-z0-9- may be used in key names.
* Note: this function automatically separate key from locale. Locale is validated against isValidKey.
* See_Also: $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s02.html, Basic format of the file), $(D isValidKey)
*/
@nogc @safe bool isValidDesktopFileKey(in char[] desktopKey) pure nothrow {
auto t = separateFromLocale(desktopKey);
auto key = t[0];
auto locale = t[1];
if (locale.length && !isValidKey(locale)) {
return false;
}
@nogc @safe static bool isValidDesktopFileKeyChar(char c) pure nothrow {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-';
}
if (key.empty) {
return false;
}
for (size_t i = 0; i<key.length; ++i) {
if (!isValidDesktopFileKeyChar(key[i])) {
return false;
}
}
return true;
}
///
unittest
{
assert(isValidDesktopFileKey("Generic-Name"));
assert(isValidDesktopFileKey("Generic-Name[ru_RU]"));
assert(!isValidDesktopFileKey("Name$"));
assert(!isValidDesktopFileKey(""));
assert(!isValidDesktopFileKey("[ru_RU]"));
assert(!isValidDesktopFileKey("Name[ru\nRU]"));
}
/**
* Test whether the entry value represents true.
* See_Also: $(D isFalse), $(D isBoolean)
*/
@nogc @safe bool isTrue(const(char)[] value) pure nothrow {
return (value == "true" || value == "1");
}
///
unittest
{
assert(isTrue("true"));
assert(isTrue("1"));
assert(!isTrue("not boolean"));
}
/**
* Test whether the entry value represents false.
* See_Also: $(D isTrue), $(D isBoolean)
*/
@nogc @safe bool isFalse(const(char)[] value) pure nothrow {
return (value == "false" || value == "0");
}
///
unittest
{
assert(isFalse("false"));
assert(isFalse("0"));
assert(!isFalse("not boolean"));
}
/**
* Check if the entry value can be interpreted as boolean value.
* See_Also: $(D isTrue), $(D isFalse)
*/
@nogc @safe bool isBoolean(const(char)[] value) pure nothrow {
return isTrue(value) || isFalse(value);
}
///
unittest
{
assert(isBoolean("true"));
assert(isBoolean("1"));
assert(isBoolean("false"));
assert(isBoolean("0"));
assert(!isBoolean("not boolean"));
}
/**
* Convert bool to string. Can be used to set boolean values.
* See_Also: $(D isBoolean)
*/
@nogc @safe string boolToString(bool b) nothrow pure {
return b ? "true" : "false";
}
///
unittest
{
assert(boolToString(false) == "false");
assert(boolToString(true) == "true");
}
/**
* Make locale name based on language, country, encoding and modifier.
* Returns: locale name in form lang_COUNTRY.ENCODING@MODIFIER
* See_Also: $(D parseLocaleName)
*/
@safe String makeLocaleName(String)(
String lang, String country = null,
String encoding = null,
String modifier = null) pure
if (isSomeString!String && is(ElementEncodingType!String : char))
{
return lang ~ (country.length ? "_".to!String~country : String.init)
~ (encoding.length ? ".".to!String~encoding : String.init)
~ (modifier.length ? "@".to!String~modifier : String.init);
}
///
unittest
{
assert(makeLocaleName("ru", "RU") == "ru_RU");
assert(makeLocaleName("ru", "RU", "UTF-8") == "ru_RU.UTF-8");
assert(makeLocaleName("ru", "RU", "UTF-8", "mod") == "ru_RU.UTF-8@mod");
assert(makeLocaleName("ru", string.init, string.init, "mod") == "ru@mod");
assert(makeLocaleName("ru".dup, (char[]).init, (char[]).init, "mod".dup) == "ru@mod".dup);
}
/**
* Parse locale name into the tuple of 4 values corresponding to language, country, encoding and modifier
* Returns: Tuple!(string, "lang", string, "country", string, "encoding", string, "modifier")
* See_Also: $(D makeLocaleName)
*/
@nogc @trusted auto parseLocaleName(String)(String locale) pure nothrow if (isSomeString!String && is(ElementEncodingType!String : char))
{
auto modifiderSplit = findSplit(locale, "@");
auto modifier = modifiderSplit[2];
auto encodongSplit = findSplit(modifiderSplit[0], ".");
auto encoding = encodongSplit[2];
auto countrySplit = findSplit(encodongSplit[0], "_");
auto country = countrySplit[2];
auto lang = countrySplit[0];
alias LocaleTuple = Tuple!(String, "lang", String, "country", String, "encoding", String, "modifier");
return LocaleTuple(lang, country, encoding, modifier);
}
///
unittest
{
assert(parseLocaleName("ru_RU.UTF-8@mod") == tuple("ru", "RU", "UTF-8", "mod"));
assert(parseLocaleName("ru@mod") == tuple("ru", string.init, string.init, "mod"));
assert(parseLocaleName("ru_RU") == tuple("ru", "RU", string.init, string.init));
assert(parseLocaleName("ru_RU.UTF-8@mod".dup) == tuple("ru".dup, "RU".dup, "UTF-8".dup, "mod".dup));
}
/**
* Drop encoding part from locale (it's not used in constructing localized keys).
* Returns: Locale string with encoding part dropped out or original string if encoding was not present.
*/
@safe String dropEncodingPart(String)(String locale) pure nothrow if (isSomeString!String && is(ElementEncodingType!String : char))
{
auto t = parseLocaleName(locale);
if (!t.encoding.empty) {
return makeLocaleName(t.lang, t.country, String.init, t.modifier);
}
return locale;
}
///
unittest
{
assert("ru_RU.UTF-8".dropEncodingPart() == "ru_RU");
string locale = "ru_RU";
assert(locale.dropEncodingPart() is locale);
}
/**
* Construct localized key name from key and locale.
* Returns: localized key in form key[locale] dropping encoding out if present.
* See_Also: $(D separateFromLocale)
*/
@safe String localizedKey(String)(String key, String locale) pure nothrow if (isSomeString!String && is(ElementEncodingType!String : char))
{
if (locale.empty) {
return key;
}
return key ~ "[".to!String ~ locale.dropEncodingPart() ~ "]".to!String;
}
///
unittest
{
string key = "Name";
assert(localizedKey(key, "") == key);
assert(localizedKey("Name", "ru_RU") == "Name[ru_RU]");
assert(localizedKey("Name", "ru_RU.UTF-8") == "Name[ru_RU]");
}
/**
* ditto, but constructs locale name from arguments.
*/
@safe String localizedKey(String)(String key, String lang, String country, String modifier = null) pure if (isSomeString!String && is(ElementEncodingType!String : char))
{
return key ~ "[".to!String ~ makeLocaleName(lang, country, String.init, modifier) ~ "]".to!String;
}
///
unittest
{
assert(localizedKey("Name", "ru", "RU") == "Name[ru_RU]");
assert(localizedKey("Name".dup, "ru".dup, "RU".dup) == "Name[ru_RU]".dup);
}
/**
* Separate key name into non-localized key and locale name.
* If key is not localized returns original key and empty string.
* Returns: tuple of key and locale name.
* See_Also: $(D localizedKey)
*/
@nogc @trusted auto separateFromLocale(String)(String key) pure nothrow if (isSomeString!String && is(ElementEncodingType!String : char)) {
if (key.endsWith("]")) {
auto t = key.findSplit("[");
if (t[1].length) {
return tuple(t[0], t[2][0..$-1]);
}
}
return tuple(key, typeof(key).init);
}
///
unittest
{
assert(separateFromLocale("Name[ru_RU]") == tuple("Name", "ru_RU"));
assert(separateFromLocale("Name") == tuple("Name", string.init));
char[] mutableString = "Hello".dup;
assert(separateFromLocale(mutableString) == tuple(mutableString, typeof(mutableString).init));
}
/**
* Choose the better localized value matching to locale between two localized values. The "goodness" is determined using algorithm described in $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html, Localized values for keys).
* Params:
* locale = original locale to match to
* firstLocale = first locale
* firstValue = first value
* secondLocale = second locale
* secondValue = second value
* Returns: The best alternative among two or empty string if none of alternatives match original locale.
* Note: value with empty locale is considered better choice than value with locale that does not match the original one.
*/
@nogc @trusted auto chooseLocalizedValue(String)(
String locale,
String firstLocale, String firstValue,
String secondLocale, String secondValue) pure nothrow
if (isSomeString!String && is(ElementEncodingType!String : char))
{
const lt = parseLocaleName(locale);
const lt1 = parseLocaleName(firstLocale);
const lt2 = parseLocaleName(secondLocale);
int score1, score2;
if (lt.lang == lt1.lang) {
score1 = 1 + ((lt.country == lt1.country) ? 2 : 0 ) + ((lt.modifier == lt1.modifier) ? 1 : 0);
}
if (lt.lang == lt2.lang) {
score2 = 1 + ((lt.country == lt2.country) ? 2 : 0 ) + ((lt.modifier == lt2.modifier) ? 1 : 0);
}
if (score1 == 0 && score2 == 0) {
if (firstLocale.empty && !firstValue.empty) {
return tuple(firstLocale, firstValue);
} else if (secondLocale.empty && !secondValue.empty) {
return tuple(secondLocale, secondValue);
} else {
return tuple(String.init, String.init);
}
}
if (score1 >= score2) {
return tuple(firstLocale, firstValue);
} else {
return tuple(secondLocale, secondValue);
}
}
///
unittest
{
string locale = "ru_RU.UTF-8@jargon";
assert(chooseLocalizedValue(string.init, "ru_RU", "Программист", "ru@jargon", "Кодер") == tuple(string.init, string.init));
assert(chooseLocalizedValue(locale, "fr_FR", "Programmeur", string.init, "Programmer") == tuple(string.init, "Programmer"));
assert(chooseLocalizedValue(locale, string.init, "Programmer", "de_DE", "Programmierer") == tuple(string.init, "Programmer"));
assert(chooseLocalizedValue(locale, "fr_FR", "Programmeur", "de_DE", "Programmierer") == tuple(string.init, string.init));
assert(chooseLocalizedValue(string.init, string.init, "Value", string.init, string.init) == tuple(string.init, "Value"));
assert(chooseLocalizedValue(locale, string.init, "Value", string.init, string.init) == tuple(string.init, "Value"));
assert(chooseLocalizedValue(locale, string.init, string.init, string.init, "Value") == tuple(string.init, "Value"));
assert(chooseLocalizedValue(locale, "ru_RU", "Программист", "ru@jargon", "Кодер") == tuple("ru_RU", "Программист"));
assert(chooseLocalizedValue(locale, "ru_RU", "Программист", "ru_RU@jargon", "Кодер") == tuple("ru_RU@jargon", "Кодер"));
assert(chooseLocalizedValue(locale, "ru", "Разработчик", "ru_RU", "Программист") == tuple("ru_RU", "Программист"));
}
/**
* Check if value needs to be escaped. This function is currently tolerant to single slashes and tabs.
* Returns: true if value needs to escaped, false otherwise.
* See_Also: $(D escapeValue)
*/
@nogc @safe bool needEscaping(String)(String value) nothrow pure if (isSomeString!String && is(ElementEncodingType!String : char))
{
for (size_t i=0; i<value.length; ++i) {
const c = value[i];
if (c == '\n' || c == '\r') {
return true;
}
}
return false;
}
///
unittest
{
assert("new\nline".needEscaping);
assert(!`i have \ slash`.needEscaping);
assert("i like\rcarriage\rreturns".needEscaping);
assert(!"just a text".needEscaping);
}
/**
* Escapes string by replacing special symbols with escaped sequences.
* These symbols are: '\\' (backslash), '\n' (newline), '\r' (carriage return) and '\t' (tab).
* Returns: Escaped string.
* See_Also: $(D unescapeValue)
*/
@trusted String escapeValue(String)(String value) pure if (isSomeString!String && is(ElementEncodingType!String : char)) {
return value.replace("\\", `\\`.to!String).replace("\n", `\n`.to!String).replace("\r", `\r`.to!String).replace("\t", `\t`.to!String);
}
///
unittest
{
assert("a\\next\nline\top".escapeValue() == `a\\next\nline\top`); // notice how the string on the right is raw.
assert("a\\next\nline\top".dup.escapeValue() == `a\\next\nline\top`.dup);
}
/**
* Unescape value. If value does not need unescaping this function returns original value.
* Params:
* value = string to unescape
* pairs = pairs of escaped characters and their unescaped forms.
*/
@trusted inout(char)[] doUnescape(inout(char)[] value, in Tuple!(char, char)[] pairs) nothrow pure {
//little optimization to avoid unneeded allocations.
size_t i = 0;
for (; i < value.length; i++) {
if (value[i] == '\\') {
break;
}
}
if (i == value.length) {
return value;
}
auto toReturn = appender!(typeof(value))();
toReturn.put(value[0..i]);
for (; i < value.length; i++) {
if (value[i] == '\\' && i+1 < value.length) {
const char c = value[i+1];
auto t = pairs.find!"a[0] == b[0]"(tuple(c,c));
if (!t.empty) {
toReturn.put(t.front[1]);
i++;
continue;
}
}
toReturn.put(value[i]);
}
return toReturn.data;
}
unittest
{
enum Tuple!(char, char)[] pairs = [tuple('\\', '\\')];
static assert(is(typeof(doUnescape("", pairs)) == string));
static assert(is(typeof(doUnescape("".dup, pairs)) == char[]));
}
/**
* Unescapes string. You should unescape values returned by library before displaying until you want keep them as is (e.g., to allow user to edit values in escaped form).
* Returns: Unescaped string.
* See_Also: $(D escapeValue), $(D doUnescape)
*/
@safe inout(char)[] unescapeValue(inout(char)[] value) nothrow pure
{
static immutable Tuple!(char, char)[] pairs = [
tuple('s', ' '),
tuple('n', '\n'),
tuple('r', '\r'),
tuple('t', '\t'),
tuple('\\', '\\')
];
return doUnescape(value, pairs);
}
///
unittest
{
assert(`a\\next\nline\top`.unescapeValue() == "a\\next\nline\top"); // notice how the string on the left is raw.
assert(`\\next\nline\top`.unescapeValue() == "\\next\nline\top");
string value = `nounescape`;
assert(value.unescapeValue() is value); //original is returned.
assert(`a\\next\nline\top`.dup.unescapeValue() == "a\\next\nline\top".dup);
}

2555
3rdparty/inilike/file.d vendored Normal file

File diff suppressed because it is too large Load Diff

193
3rdparty/inilike/package.d vendored Normal file
View File

@ -0,0 +1,193 @@
/**
* Reading and writing ini-like files used in some Unix systems and Freedesktop specifications.
* ini-like is informal name for the file format that look like this:
* ---
# Comment
[Group name]
Key=Value
# Comment inside group
AnotherKey=Value
[Another group]
Key=English value
Key[fr_FR]=Francais value
* ---
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
*/
module inilike;
public import inilike.common;
public import inilike.range;
public import inilike.file;
unittest
{
import std.exception;
final class DesktopEntry : IniLikeGroup
{
this() {
super("Desktop Entry");
}
protected:
@trusted override void validateKey(string key, string value) const {
if (!isValidDesktopFileKey(key)) {
throw new IniLikeEntryException("key is invalid", groupName(), key, value);
}
}
}
final class DesktopFile : IniLikeFile
{
//Options to manage .ini like file reading
static struct DesktopReadOptions
{
IniLikeFile.ReadOptions baseOptions;
alias baseOptions this;
bool skipExtensionGroups;
bool ignoreUnknownGroups;
bool skipUnknownGroups;
}
@trusted this(IniLikeReader)(IniLikeReader reader, DesktopReadOptions options = DesktopReadOptions.init)
{
_options = options;
super(reader, null, options.baseOptions);
enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0));
}
@safe override bool removeGroup(string groupName) nothrow {
if (groupName == "Desktop Entry") {
return false;
}
return super.removeGroup(groupName);
}
protected:
@trusted override IniLikeGroup createGroupByName(string groupName)
{
if (groupName == "Desktop Entry") {
_desktopEntry = new DesktopEntry();
return _desktopEntry;
} else if (groupName.startsWith("X-")) {
if (_options.skipExtensionGroups) {
return null;
}
return createEmptyGroup(groupName);
} else {
if (_options.ignoreUnknownGroups) {
if (_options.skipUnknownGroups) {
return null;
} else {
return createEmptyGroup(groupName);
}
} else {
throw new IniLikeException("Unknown group");
}
}
}
inout(DesktopEntry) desktopEntry() inout {
return _desktopEntry;
}
private:
DesktopEntry _desktopEntry;
DesktopReadOptions _options;
}
string contents =
`# First comment
[Desktop Entry]
Key=Value
# Comment in group`;
DesktopFile.DesktopReadOptions options;
auto df = new DesktopFile(iniLikeStringReader(contents), options);
assert(!df.removeGroup("Desktop Entry"));
assert(!df.removeGroup("NonExistent"));
assert(df.group("Desktop Entry") !is null);
assert(df.desktopEntry() !is null);
assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromComment("# Comment in group")]));
assert(equal(df.leadingComments(), ["# First comment"]));
assertThrown(df.desktopEntry().writeEntry("$Invalid", "Valid value"));
IniLikeEntryException entryException;
try {
df.desktopEntry().writeEntry("$Invalid", "Valid value");
} catch(IniLikeEntryException e) {
entryException = e;
}
assert(entryException !is null);
df.desktopEntry().writeEntry("$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.save);
assert(df.desktopEntry().value("$Invalid") == "Valid value");
assert(df.desktopEntry().appendValue("Another$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.skip).isNull());
assert(df.desktopEntry().setValue("Another$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.skip) is null);
assert(df.desktopEntry().value("Another$Invalid") is null);
contents =
`[X-SomeGroup]
Key=Value`;
auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents)));
assert(thrown !is null);
assert(thrown.lineNumber == 0);
contents =
`[Desktop Entry]
Valid=Key
$=Invalid`;
thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents)));
assert(thrown !is null);
assert(thrown.entryException !is null);
assert(thrown.entryException.key == "$");
assert(thrown.entryException.value == "Invalid");
options = DesktopFile.DesktopReadOptions.init;
options.invalidKeyPolicy = IniLikeGroup.InvalidKeyPolicy.skip;
assertNotThrown(new DesktopFile(iniLikeStringReader(contents), options));
contents =
`[Desktop Entry]
Name=Name
[Unknown]
Key=Value`;
assertThrown(new DesktopFile(iniLikeStringReader(contents)));
options = DesktopFile.DesktopReadOptions.init;
options.ignoreUnknownGroups = true;
assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), options));
assert(df.group("Unknown") !is null);
options.skipUnknownGroups = true;
df = new DesktopFile(iniLikeStringReader(contents), options);
assert(df.group("Unknown") is null);
contents =
`[Desktop Entry]
Name=Name1
[X-Extension]
Name=Name2`;
options = DesktopFile.DesktopReadOptions.init;
options.skipExtensionGroups = true;
df = new DesktopFile(iniLikeStringReader(contents), options);
assert(df.group("X-Extension") is null);
}

213
3rdparty/inilike/range.d vendored Normal file
View File

@ -0,0 +1,213 @@
/**
* Parsing contents of ini-like files via range-based interface.
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2015-2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
*/
module inilike.range;
import inilike.common;
/**
* Object for iterating through ini-like file entries.
*/
struct IniLikeReader(Range) if (isInputRange!Range && isSomeString!(ElementType!Range) && is(ElementEncodingType!(ElementType!Range) : char))
{
/**
* Construct from other range of strings.
*/
this(Range range)
{
_range = range;
}
/**
* Iterate through lines before any group header. It does not check if all lines are comments or empty lines.
*/
auto byLeadingLines()
{
return _range.until!(isGroupHeader);
}
/**
* Object representing single group (section) being parsed in .ini-like file.
*/
static struct Group(Range)
{
private this(Range range, ElementType!Range originalLine)
{
_range = range;
_originalLine = originalLine;
}
/**
* Name of group being parsed (without brackets).
* Note: This can become invalid during parsing the Input Range
* (e.g. if string buffer storing this value is reused in later reads).
*/
auto groupName() {
return parseGroupHeader(_originalLine);
}
/**
* Original line of group header (i.e. name with brackets).
* Note: This can become invalid during parsing the Input Range
* (e.g. if string buffer storing this value is reused in later reads).
*/
auto originalLine() {
return _originalLine;
}
/**
* Iterate over group entries - may be key-value pairs as well as comments or empty lines.
*/
auto byEntry()
{
return _range.until!(isGroupHeader);
}
private:
ElementType!Range _originalLine;
Range _range;
}
/**
* Iterate thorugh groups of .ini-like file.
* Returns: Range of Group objects.
*/
auto byGroup()
{
static struct ByGroup
{
this(Range range)
{
_range = range.find!(isGroupHeader);
ElementType!Range line;
if (!_range.empty) {
line = _range.front;
_range.popFront();
}
_currentGroup = Group!Range(_range, line);
}
auto front()
{
return _currentGroup;
}
bool empty()
{
return _currentGroup.groupName.empty;
}
void popFront()
{
_range = _range.find!(isGroupHeader);
ElementType!Range line;
if (!_range.empty) {
line = _range.front;
_range.popFront();
}
_currentGroup = Group!Range(_range, line);
}
private:
Group!Range _currentGroup;
Range _range;
}
return ByGroup(_range.find!(isGroupHeader));
}
private:
Range _range;
}
/**
* Convenient function for creation of IniLikeReader instance.
* Params:
* range = input range of strings (strings must be without trailing new line characters)
* Returns: IniLikeReader for given range.
* See_Also: $(D iniLikeFileReader), $(D iniLikeStringReader)
*/
auto iniLikeRangeReader(Range)(Range range)
{
return IniLikeReader!Range(range);
}
///
unittest
{
string contents =
`First comment
Second comment
[First group]
KeyValue1
KeyValue2
[Second group]
KeyValue3
KeyValue4
[Empty group]
[Third group]
KeyValue5
KeyValue6`;
auto r = iniLikeRangeReader(contents.splitLines());
auto byLeadingLines = r.byLeadingLines;
assert(byLeadingLines.front == "First comment");
assert(byLeadingLines.equal(["First comment", "Second comment"]));
auto byGroup = r.byGroup;
assert(byGroup.front.groupName == "First group");
assert(byGroup.front.originalLine == "[First group]");
assert(byGroup.front.byEntry.front == "KeyValue1");
assert(byGroup.front.byEntry.equal(["KeyValue1", "KeyValue2"]));
byGroup.popFront();
assert(byGroup.front.groupName == "Second group");
byGroup.popFront();
assert(byGroup.front.groupName == "Empty group");
assert(byGroup.front.byEntry.empty);
byGroup.popFront();
assert(byGroup.front.groupName == "Third group");
byGroup.popFront();
assert(byGroup.empty);
}
/**
* Convenient function for reading ini-like contents from the file.
* Throws: $(B ErrnoException) if file could not be opened.
* Note: This function uses byLineCopy internally. Fallbacks to byLine on older compilers.
* See_Also: $(D iniLikeRangeReader), $(D iniLikeStringReader)
*/
@trusted auto iniLikeFileReader(string fileName)
{
import std.stdio : File;
static if( __VERSION__ < 2067 ) {
return iniLikeRangeReader(File(fileName, "r").byLine().map!(s => s.idup));
} else {
return iniLikeRangeReader(File(fileName, "r").byLineCopy());
}
}
/**
* Convenient function for reading ini-like contents from string.
* Note: on frontends < 2.067 it uses splitLines thereby allocates strings.
* See_Also: $(D iniLikeRangeReader), $(D iniLikeFileReader)
*/
@trusted auto iniLikeStringReader(String)(String contents) if (isSomeString!String && is(ElementEncodingType!String : char))
{
static if( __VERSION__ < 2067 ) {
return iniLikeRangeReader(contents.splitLines());
} else {
return iniLikeRangeReader(contents.lineSplitter());
}
}

37
3rdparty/isfreedesktop.d vendored Normal file
View File

@ -0,0 +1,37 @@
/**
* $(B isFreedesktop) is compile-time constant to test if target platform desktop environments usually follow freedesktop specifications.
* Currently Linux, all *BSD and Hurd are considered to be freedesktop-compatible, hence isFreedesktop is evaluated to true on these platforms.
* This guess is somewhat optimistic, since there are vendor-specific operating systems based on these kernels in the world while their desktops don't implement freedesktop specifications.
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
*/
module isfreedesktop;
version(OSX) {
enum isFreedesktop = false;
} else version(Android) {
enum isFreedesktop = false;
} else version(linux) {
enum isFreedesktop = true;
} else version(FreeBSD) {
enum isFreedesktop = true;
} else version(OpenBSD) {
enum isFreedesktop = true;
} else version(NetBSD) {
enum isFreedesktop = true;
} else version(DragonFlyBSD) {
enum isFreedesktop = true;
} else version(BSD) {
enum isFreedesktop = true;
} else version(Hurd) {
enum isFreedesktop = true;
} else version(Solaris) {
enum isFreedesktop = true;
} else {
enum isFreedesktop = false;
}

503
3rdparty/xdgpaths.d vendored Normal file
View File

@ -0,0 +1,503 @@
/**
* Getting XDG base directories.
* Note: These functions are defined only on freedesktop systems.
* Authors:
* $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
* Copyright:
* Roman Chistokhodov, 2016
* License:
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* See_Also:
* $(LINK2 https://specifications.freedesktop.org/basedir-spec/latest/index.html, XDG Base Directory Specification)
*/
module xdgpaths;
import isfreedesktop;
version(D_Ddoc)
{
/**
* Path to runtime user directory.
* Returns: User's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable.
* If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string.
*/
@trusted string xdgRuntimeDir() nothrow;
/**
* The ordered set of non-empty base paths to search for data files, in descending order of preference.
* Params:
* subfolder = Subfolder which is appended to every path if not null.
* Returns: Data directories, without user's one and with no duplicates.
* Note: This function does not check if paths actually exist and appear to be directories.
* See_Also: $(D xdgAllDataDirs), $(D xdgDataHome)
*/
@trusted string[] xdgDataDirs(string subfolder = null) nothrow;
/**
* The ordered set of non-empty base paths to search for data files, in descending order of preference.
* Params:
* subfolder = Subfolder which is appended to every path if not null.
* Returns: Data directories, including user's one if could be evaluated.
* Note: This function does not check if paths actually exist and appear to be directories.
* See_Also: $(D xdgDataDirs), $(D xdgDataHome)
*/
@trusted string[] xdgAllDataDirs(string subfolder = null) nothrow;
/**
* The ordered set of non-empty base paths to search for configuration files, in descending order of preference.
* Params:
* subfolder = Subfolder which is appended to every path if not null.
* Returns: Config directories, without user's one and with no duplicates.
* Note: This function does not check if paths actually exist and appear to be directories.
* See_Also: $(D xdgAllConfigDirs), $(D xdgConfigHome)
*/
@trusted string[] xdgConfigDirs(string subfolder = null) nothrow;
/**
* The ordered set of non-empty base paths to search for configuration files, in descending order of preference.
* Params:
* subfolder = Subfolder which is appended to every path if not null.
* Returns: Config directories, including user's one if could be evaluated.
* Note: This function does not check if paths actually exist and appear to be directories.
* See_Also: $(D xdgConfigDirs), $(D xdgConfigHome)
*/
@trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow;
/**
* The base directory relative to which user-specific data files should be stored.
* Returns: Path to user-specific data directory or empty string on error.
* Params:
* subfolder = Subfolder to append to determined path.
* shouldCreate = If path does not exist, create directory using 700 permissions (i.e. allow access only for current user).
* See_Also: $(D xdgAllDataDirs), $(D xdgDataDirs)
*/
@trusted string xdgDataHome(string subfolder = null, bool shouldCreate = false) nothrow;
/**
* The base directory relative to which user-specific configuration files should be stored.
* Returns: Path to user-specific configuration directory or empty string on error.
* Params:
* subfolder = Subfolder to append to determined path.
* shouldCreate = If path does not exist, create directory using 700 permissions (i.e. allow access only for current user).
* See_Also: $(D xdgAllConfigDirs), $(D xdgConfigDirs)
*/
@trusted string xdgConfigHome(string subfolder = null, bool shouldCreate = false) nothrow;
/**
* The base directory relative to which user-specific non-essential files should be stored.
* Returns: Path to user-specific cache directory or empty string on error.
* Params:
* subfolder = Subfolder to append to determined path.
* shouldCreate = If path does not exist, create directory using 700 permissions (i.e. allow access only for current user).
*/
@trusted string xdgCacheHome(string subfolder = null, bool shouldCreate = false) nothrow;
}
static if (isFreedesktop)
{
private {
import std.algorithm : splitter, map, filter, canFind;
import std.array;
import std.conv : octal;
import std.exception : collectException, enforce;
import std.file;
import std.path : buildPath, dirName;
import std.process : environment;
import std.string : toStringz;
import core.sys.posix.unistd;
import core.sys.posix.sys.stat;
import core.sys.posix.sys.types;
import core.stdc.string;
import core.stdc.errno;
static if (is(typeof({import std.string : fromStringz;}))) {
import std.string : fromStringz;
} else { //own fromStringz implementation for compatibility reasons
@system static pure inout(char)[] fromStringz(inout(char)* cString) {
return cString ? cString[0..strlen(cString)] : null;
}
}
enum mode_t privateMode = octal!700;
}
version(unittest) {
import std.algorithm : equal;
private struct EnvGuard
{
this(string env) {
envVar = env;
envValue = environment.get(env);
}
~this() {
if (envValue is null) {
environment.remove(envVar);
} else {
environment[envVar] = envValue;
}
}
string envVar;
string envValue;
}
}
private string[] pathsFromEnvValue(string envValue, string subfolder = null) nothrow {
string[] result;
try {
foreach(path; splitter(envValue, ':').filter!(p => !p.empty).map!(p => buildPath(p, subfolder))) {
if (path[$-1] == '/') {
path = path[0..$-1];
}
if (!result.canFind(path)) {
result ~= path;
}
}
} catch(Exception e) {
}
return result;
}
unittest
{
assert(pathsFromEnvValue("") == (string[]).init);
assert(pathsFromEnvValue(":") == (string[]).init);
assert(pathsFromEnvValue("::") == (string[]).init);
assert(pathsFromEnvValue("path1:path2") == ["path1", "path2"]);
assert(pathsFromEnvValue("path1:") == ["path1"]);
assert(pathsFromEnvValue("path1/") == ["path1"]);
assert(pathsFromEnvValue("path1/:path1") == ["path1"]);
assert(pathsFromEnvValue("path2:path1:path2") == ["path2", "path1"]);
}
private string[] pathsFromEnv(string envVar, string subfolder = null) nothrow {
string envValue;
collectException(environment.get(envVar), envValue);
return pathsFromEnvValue(envValue, subfolder);
}
private bool ensureExists(string dir) nothrow
{
bool ok;
try {
ok = dir.exists;
if (!ok) {
mkdirRecurse(dir.dirName);
ok = mkdir(dir.toStringz, privateMode) == 0;
} else {
ok = dir.isDir;
}
} catch(Exception e) {
ok = false;
}
return ok;
}
unittest
{
import std.file;
import std.stdio;
string temp = tempDir();
if (temp.length) {
string testDir = buildPath(temp, "xdgpaths-unittest-tempdir");
string testFile = buildPath(testDir, "touched");
string testSubDir = buildPath(testDir, "subdir");
try {
mkdir(testDir);
File(testFile, "w");
assert(!ensureExists(testFile));
enforce(ensureExists(testSubDir));
} catch(Exception e) {
} finally {
collectException(rmdir(testSubDir));
collectException(remove(testFile));
collectException(rmdir(testDir));
}
}
}
private string xdgBaseDir(string envvar, string fallback, string subfolder = null, bool shouldCreate = false) nothrow {
string dir;
collectException(environment.get(envvar), dir);
if (dir.length == 0) {
string home;
collectException(environment.get("HOME"), home);
dir = home.length ? buildPath(home, fallback) : null;
}
if (dir.length == 0) {
return null;
}
if (shouldCreate) {
if (ensureExists(dir)) {
if (subfolder.length) {
string path = buildPath(dir, subfolder);
try {
if (!path.exists) {
mkdirRecurse(path);
}
return path;
} catch(Exception e) {
}
} else {
return dir;
}
}
} else {
return buildPath(dir, subfolder);
}
return null;
}
version(unittest) {
void testXdgBaseDir(string envVar, string fallback) {
auto homeGuard = EnvGuard("HOME");
auto dataHomeGuard = EnvGuard(envVar);
auto newHome = "/home/myuser";
auto newDataHome = "/home/myuser/data";
environment[envVar] = newDataHome;
assert(xdgBaseDir(envVar, fallback) == newDataHome);
assert(xdgBaseDir(envVar, fallback, "applications") == buildPath(newDataHome, "applications"));
environment.remove(envVar);
environment["HOME"] = newHome;
assert(xdgBaseDir(envVar, fallback) == buildPath(newHome, fallback));
assert(xdgBaseDir(envVar, fallback, "icons") == buildPath(newHome, fallback, "icons"));
environment.remove("HOME");
assert(xdgBaseDir(envVar, fallback).empty);
assert(xdgBaseDir(envVar, fallback, "mime").empty);
}
}
@trusted string[] xdgDataDirs(string subfolder = null) nothrow
{
auto result = pathsFromEnv("XDG_DATA_DIRS", subfolder);
if (result.length) {
return result;
} else {
return [buildPath("/usr/local/share", subfolder), buildPath("/usr/share", subfolder)];
}
}
///
unittest
{
auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
auto newDataDirs = ["/usr/local/data", "/usr/data"];
environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data:/usr/local/data/:/usr/data/";
assert(xdgDataDirs() == newDataDirs);
assert(equal(xdgDataDirs("applications"), newDataDirs.map!(p => buildPath(p, "applications"))));
environment.remove("XDG_DATA_DIRS");
assert(xdgDataDirs() == ["/usr/local/share", "/usr/share"]);
assert(equal(xdgDataDirs("icons"), ["/usr/local/share", "/usr/share"].map!(p => buildPath(p, "icons"))));
}
@trusted string[] xdgAllDataDirs(string subfolder = null) nothrow
{
string dataHome = xdgDataHome(subfolder);
string[] dataDirs = xdgDataDirs(subfolder);
if (dataHome.length) {
return dataHome ~ dataDirs;
} else {
return dataDirs;
}
}
///
unittest
{
auto homeGuard = EnvGuard("HOME");
auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
auto newDataHome = "/home/myuser/data";
auto newDataDirs = ["/usr/local/data", "/usr/data"];
environment["XDG_DATA_HOME"] = newDataHome;
environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data";
assert(xdgAllDataDirs() == newDataHome ~ newDataDirs);
environment.remove("XDG_DATA_HOME");
environment.remove("HOME");
assert(xdgAllDataDirs() == newDataDirs);
}
@trusted string[] xdgConfigDirs(string subfolder = null) nothrow
{
auto result = pathsFromEnv("XDG_CONFIG_DIRS", subfolder);
if (result.length) {
return result;
} else {
return [buildPath("/etc/xdg", subfolder)];
}
}
///
unittest
{
auto dataConfigGuard = EnvGuard("XDG_CONFIG_DIRS");
auto newConfigDirs = ["/usr/local/config", "/usr/config"];
environment["XDG_CONFIG_DIRS"] = "/usr/local/config:/usr/config";
assert(xdgConfigDirs() == newConfigDirs);
assert(equal(xdgConfigDirs("menus"), newConfigDirs.map!(p => buildPath(p, "menus"))));
environment.remove("XDG_CONFIG_DIRS");
assert(xdgConfigDirs() == ["/etc/xdg"]);
assert(equal(xdgConfigDirs("autostart"), ["/etc/xdg"].map!(p => buildPath(p, "autostart"))));
}
@trusted string[] xdgAllConfigDirs(string subfolder = null) nothrow
{
string configHome = xdgConfigHome(subfolder);
string[] configDirs = xdgConfigDirs(subfolder);
if (configHome.length) {
return configHome ~ configDirs;
} else {
return configDirs;
}
}
///
unittest
{
auto homeGuard = EnvGuard("HOME");
auto configHomeGuard = EnvGuard("XDG_CONFIG_HOME");
auto configDirsGuard = EnvGuard("XDG_CONFIG_DIRS");
auto newConfigHome = "/home/myuser/data";
environment["XDG_CONFIG_HOME"] = newConfigHome;
auto newConfigDirs = ["/usr/local/data", "/usr/data"];
environment["XDG_CONFIG_DIRS"] = "/usr/local/data:/usr/data";
assert(xdgAllConfigDirs() == newConfigHome ~ newConfigDirs);
environment.remove("XDG_CONFIG_HOME");
environment.remove("HOME");
assert(xdgAllConfigDirs() == newConfigDirs);
}
@trusted string xdgDataHome(string subfolder = null, bool shouldCreate = false) nothrow {
return xdgBaseDir("XDG_DATA_HOME", ".local/share", subfolder, shouldCreate);
}
unittest
{
testXdgBaseDir("XDG_DATA_HOME", ".local/share");
}
@trusted string xdgConfigHome(string subfolder = null, bool shouldCreate = false) nothrow {
return xdgBaseDir("XDG_CONFIG_HOME", ".config", subfolder, shouldCreate);
}
unittest
{
testXdgBaseDir("XDG_CONFIG_HOME", ".config");
}
@trusted string xdgCacheHome(string subfolder = null, bool shouldCreate = false) nothrow {
return xdgBaseDir("XDG_CACHE_HOME", ".cache", subfolder, shouldCreate);
}
unittest
{
testXdgBaseDir("XDG_CACHE_HOME", ".cache");
}
version(XdgPathsRuntimeDebug) {
private import std.stdio;
}
@trusted string xdgRuntimeDir() nothrow // Do we need it on BSD systems?
{
import std.exception : assumeUnique;
import core.sys.posix.pwd;
try { //one try to rule them all and for compatibility reasons
const uid_t uid = getuid();
string runtime;
collectException(environment.get("XDG_RUNTIME_DIR"), runtime);
if (!runtime.length) {
passwd* pw = getpwuid(uid);
try {
if (pw && pw.pw_name) {
runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name));
if (!(runtime.exists && runtime.isDir)) {
if (mkdir(runtime.toStringz, privateMode) != 0) {
version(XdgPathsRuntimeDebug) stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno)));
return null;
}
}
} else {
version(XdgPathsRuntimeDebug) stderr.writeln("Failed to get user name to create runtime directory");
return null;
}
} catch(Exception e) {
version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Error when creating runtime directory: %s", e.msg));
return null;
}
}
stat_t statbuf;
stat(runtime.toStringz, &statbuf);
if (statbuf.st_uid != uid) {
version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid));
return null;
}
if ((statbuf.st_mode & octal!777) != privateMode) {
version(XdgPathsRuntimeDebug) collectException(stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, privateMode));
return null;
}
return runtime;
} catch (Exception e) {
version(XdgPathsRuntimeDebug) collectException(stderr.writeln("Error when getting runtime directory: %s", e.msg));
return null;
}
}
version(xdgpathsFileTest) unittest
{
string runtimePath = buildPath(tempDir(), "xdgpaths-runtime-test");
try {
collectException(std.file.rmdir(runtimePath));
if (mkdir(runtimePath.toStringz, privateMode) == 0) {
auto runtimeGuard = EnvGuard("XDG_RUNTIME_DIR");
environment["XDG_RUNTIME_DIR"] = runtimePath;
assert(xdgRuntimeDir() == runtimePath);
if (chmod(runtimePath.toStringz, octal!777) == 0) {
assert(xdgRuntimeDir() == string.init);
}
std.file.rmdir(runtimePath);
} else {
version(XdgPathsRuntimeDebug) stderr.writeln(fromStringz(strerror(errno)));
}
} catch(Exception e) {
version(XdgPathsRuntimeDebug) stderr.writeln(e.msg);
}
}
}

View File

@ -1237,6 +1237,25 @@ void main()
//
tabs.addTab(new MyOpenglWidget(), "OpenGL"d);
}
{
import dlangui.graphics.iconprovider;
TableLayout icons = new TableLayout("icons");
icons.colCount = 6;
for(StandardIcon icon = StandardIcon.init; icon <= StandardIcon.deviceCameraVideo; ++icon)
{
icons.addChild(new TextWidget(to!string(icon), to!dstring(icon)).fontSize(12.pointsToPixels).alignment(Align.Right | Align.VCenter));
auto imageBufRef = platform.iconProvider().getStandardIcon(icon);
auto imageWidget = new ImageWidget();
if (!imageBufRef.isNull()) {
auto imageDrawable = new ImageDrawable(imageBufRef);
imageWidget.drawable = imageDrawable;
}
icons.addChild(imageWidget).alignment(Align.Left | Align.VCenter);
}
icons.margins(Rect(10,10,10,10)).layoutWidth(FILL_PARENT);
tabs.addTab(icons, "Icons"d);
}
}
//==========================================================================

View File

@ -1,5 +1,6 @@
module dlangui.core.filemanager;
import dlangui.core.logger;
import isfreedesktop;
/**
* Show and select directory or file in OS file manager.
@ -65,61 +66,21 @@ import dlangui.core.logger;
return true;
} else version(Android) {
Log.w("showInFileManager is not implemented for current platform");
} else version(Posix) {
} else static if (isFreedesktop) {
import std.stdio : File;
import std.algorithm : map, filter, splitter, find, canFind, equal, findSplit;
import std.ascii : isAlpha;
import std.algorithm : map, filter, splitter, canFind, equal, findSplit;
import std.exception : collectException, assumeUnique;
import std.path : buildPath, absolutePath, isAbsolute, dirName, baseName;
import std.range;
import std.string : toStringz;
import std.typecons : Tuple, tuple;
static import std.stdio;
import inilike.common;
import inilike.range;
import xdgpaths;
string toOpen = pathName;
static inout(char)[] doUnescape(inout(char)[] value, in Tuple!(char, char)[] pairs) nothrow pure {
//little optimization to avoid unneeded allocations.
size_t i = 0;
for (; i < value.length; i++) {
if (value[i] == '\\') {
break;
}
}
if (i == value.length) {
return value;
}
auto toReturn = appender!(typeof(value))();
toReturn.put(value[0..i]);
for (; i < value.length; i++) {
if (value[i] == '\\' && i+1 < value.length) {
const char c = value[i+1];
auto t = pairs.find!"a[0] == b[0]"(tuple(c,c));
if (!t.empty) {
toReturn.put(t.front[1]);
i++;
continue;
}
}
toReturn.put(value[i]);
}
return toReturn.data;
}
static auto unescapeValue(string arg) nothrow pure
{
static immutable Tuple!(char, char)[] pairs = [
tuple('s', ' '),
tuple('n', '\n'),
tuple('r', '\r'),
tuple('t', '\t'),
tuple('\\', '\\')
];
return doUnescape(arg, pairs);
}
static string unescapeQuotedArgument(string value) nothrow pure
{
static immutable Tuple!(char, char)[] pairs = [
@ -302,35 +263,26 @@ import dlangui.core.logger;
return null;
}
static void parseConfigFile(string fileName, string wantedGroup, bool delegate (in char[], in char[]) onKeyValue)
static void parseConfigFile(string fileName, string wantedGroup, bool delegate (string, string) onKeyValue)
{
bool inNeededGroup;
foreach(line; File(fileName).byLine()) {
if (!line.length || line[0] == '#') {
continue;
} else if (line[0] == '[') {
if (line.equal(wantedGroup)) {
inNeededGroup = true;
} else {
if (inNeededGroup) {
break;
}
inNeededGroup = false;
}
} else if (line[0].isAlpha) {
if (inNeededGroup) {
auto splitted = findSplit(line, "=");
if (splitted[1].length) {
auto key = splitted[0];
auto value = splitted[2];
if (!onKeyValue(key, value)) {
auto r = iniLikeFileReader(fileName);
foreach(group; r.byGroup())
{
if (group.groupName == wantedGroup)
{
foreach(entry; group.byEntry())
{
if (entry.length && !isComment(entry)) {
auto pair = parseKeyValue(entry);
if (!isValidKey(pair.key)) {
return;
}
if (!onKeyValue(pair.key, pair.value.unescapeValue)) {
return;
}
}
}
} else {
//unexpected line content
break;
return;
}
}
}
@ -355,17 +307,17 @@ import dlangui.core.logger;
bool canOpenDirectory; //not used for now. Some file managers does not have MimeType in their .desktop file.
string exec, tryExec, icon, displayName;
parseConfigFile(appPath, "[Desktop Entry]", delegate bool(in char[] key, in char[] value) {
parseConfigFile(appPath, "Desktop Entry", delegate bool(string key, string value) {
if (key.equal("MimeType")) {
canOpenDirectory = value.splitter(';').canFind("inode/directory");
} else if (key.equal("Exec")) {
exec = value.idup;
exec = value;
} else if (key.equal("TryExec")) {
tryExec = value.idup;
tryExec = value;
} else if (key.equal("Icon")) {
icon = value.idup;
icon = value;
} else if (key.equal("Name")) {
displayName = value.idup;
displayName = value;
}
return true;
});
@ -377,7 +329,7 @@ import dlangui.core.logger;
continue;
}
}
return expandExecArgs(unquoteExec(unescapeValue(exec)), null, icon, displayName, appPath);
return expandExecArgs(unquoteExec(exec), null, icon, displayName, appPath);
}
} catch(Exception e) {
@ -393,7 +345,7 @@ import dlangui.core.logger;
{
toOpen = toOpen.absolutePath();
switch(fileManagerArgs[0].baseName) {
//nautilus and nemo selects item if it's file
//nautilus and nemo select item if it's a file
case "nautilus":
case "nemo":
fileManagerArgs ~= toOpen;
@ -439,21 +391,21 @@ import dlangui.core.logger;
spawnProcess(fileManagerArgs, inFile, outFile, errFile, null, processConfig);
}
string configHome = environment.get("XDG_CONFIG_HOME", buildPath(environment.get("HOME"), ".config"));
string appHome = environment.get("XDG_DATA_HOME", buildPath(environment.get("HOME"), ".local/share")).buildPath("applications");
string configHome = xdgConfigHome();
string appHome = xdgDataHome("applications");
auto configDirs = environment.get("XDG_CONFIG_DIRS", "/etc/xdg").splitter(':').find!(p => p.length > 0);
auto appDirs = environment.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share").splitter(':').filter!(p => p.length > 0).map!(p => buildPath(p, "applications"));
auto configDirs = xdgConfigDirs();
auto appDirs = xdgDataDirs("applications");
auto allAppDirs = chain(only(appHome), appDirs).array;
auto allAppDirs = xdgAllDataDirs("applications");
auto binPaths = environment.get("PATH").splitter(':').filter!(p => p.length > 0).array;
string[] fileManagerArgs;
foreach(mimeappsList; chain(only(configHome), only(appHome), configDirs, appDirs).map!(p => buildPath(p, "mimeapps.list"))) {
try {
parseConfigFile(mimeappsList, "[Default Applications]", delegate bool(in char[] key, in char[] value) {
parseConfigFile(mimeappsList, "Default Applications", delegate bool(string key, string value) {
if (key.equal("inode/directory") && value.length) {
auto app = value.idup;
auto app = value;
fileManagerArgs = findFileManagerCommand(app, allAppDirs, binPaths);
return false;
}
@ -471,14 +423,14 @@ import dlangui.core.logger;
foreach(mimeinfoCache; allAppDirs.map!(p => buildPath(p, "mimeinfo.cache"))) {
try {
parseConfigFile(mimeinfoCache, "[MIME Cache]", delegate bool(in char[] key, in char[] value) {
parseConfigFile(mimeinfoCache, "MIME Cache", delegate bool(string key, string value) {
if (key > "inode/directory") { //no need to proceed, since MIME types are sorted in alphabetical order.
return false;
}
if (key.equal("inode/directory") && value.length) {
auto alternatives = value.splitter(';').filter!(p => p.length > 0);
foreach(alternative; alternatives) {
fileManagerArgs = findFileManagerCommand(alternative.idup, allAppDirs, binPaths);
fileManagerArgs = findFileManagerCommand(alternative, allAppDirs, binPaths);
if (fileManagerArgs.length) {
break;
}

View File

@ -0,0 +1,608 @@
module dlangui.graphics.iconprovider;
/**
* Getting images for standard system icons and file paths.
*
* Copyright: Roman Chistokhodov, 2017
* License: Boost License 1.0
* Authors: Roman Chistokhodov, freeslave93@gmail.com
*
*/
import dlangui.graphics.drawbuf;
import dlangui.core.logger;
import isfreedesktop;
/**
* Crossplatform names for some of system icons.
*/
enum StandardIcon
{
document,
application,
folder,
folderOpen,
driveFloppy,
driveFixed,
driveRemovable,
driveCD,
driveDVD,
server,
printer,
find,
help,
sharedItem,
link,
trashcanEmpty,
trashcanFull,
mediaCDAudio,
mediaDVDAudio,
mediaDVD,
mediaCD,
fileAudio,
fileImage,
fileVideo,
fileZip,
fileUnknown,
warning,
information,
error,
password,
rename,
deleteItem,
computer,
laptop,
users,
deviceCellphone,
deviceCamera,
deviceCameraVideo,
}
/**
* Base class for icon provider.
*/
abstract class IconProviderBase
{
/**
* Get image of standard icon. If icon was not found use fallback.
*/
final DrawBufRef getStandardIcon(StandardIcon icon, lazy DrawBufRef fallback)
{
auto image = getStandardIcon(icon);
return image.isNull() ? fallback() : image;
}
/**
* Get image of icon associated with file path. If icon was not found use fallback.
*/
final DrawBufRef getIconForFilePath(string filePath, lazy DrawBufRef fallback)
{
auto image = getIconForFilePath(filePath);
return image.isNull() ? fallback() : image;
}
/**
* Get image of standard icon. Return the null image if icon was not found in the system.
*/
DrawBufRef getStandardIcon(StandardIcon icon);
/**
* Get image of icon associated with file path. Return null image if icon was not found in the system.
* Default implementation detects icon for a directory and for a file using the list of hardcoded extensions.
*
*/
DrawBufRef getIconForFilePath(string filePath)
{
// TODO: implement specifically for different platforms
import std.path : extension;
import std.uni : toLower;
import std.file : isDir, isFile;
import std.exception : collectException;
bool isdir;
collectException(isDir(filePath), isdir);
if (isdir) {
return getStandardIcon(StandardIcon.folder);
}
if (!filePath.extension.length) {
return getStandardIcon(StandardIcon.fileUnknown);
}
switch(filePath.extension.toLower) with(StandardIcon)
{
case ".jpeg": case ".jpg": case ".png": case ".bmp":
return getStandardIcon(fileImage);
case ".wav": case ".mp3": case ".ogg":
return getStandardIcon(fileAudio);
case ".avi": case ".mkv":
return getStandardIcon(fileVideo);
case ".doc": case ".docx":
return getStandardIcon(document);
case ".zip": case ".rar": case ".7z": case ".gz":
return getStandardIcon(fileZip);
default:
return DrawBufRef(null);
}
}
}
/**
* Dummy icon provider. Always returns null images or fallbacks. Available on all platforms.
*/
class DummyIconProvider : IconProviderBase
{
override DrawBufRef getStandardIcon(StandardIcon icon)
{
return DrawBufRef(null);
}
override DrawBufRef getIconForFilePath(string filePath)
{
return DrawBufRef(null);
}
}
version(Windows)
{
import core.sys.windows.windows;
enum SHSTOCKICONID {
SIID_DOCNOASSOC = 0,
SIID_DOCASSOC = 1,
SIID_APPLICATION = 2,
SIID_FOLDER = 3,
SIID_FOLDEROPEN = 4,
SIID_DRIVE525 = 5,
SIID_DRIVE35 = 6,
SIID_DRIVEREMOVE = 7,
SIID_DRIVEFIXED = 8,
SIID_DRIVENET = 9,
SIID_DRIVENETDISABLED = 10,
SIID_DRIVECD = 11,
SIID_DRIVERAM = 12,
SIID_WORLD = 13,
SIID_SERVER = 15,
SIID_PRINTER = 16,
SIID_MYNETWORK = 17,
SIID_FIND = 22,
SIID_HELP = 23,
SIID_SHARE = 28,
SIID_LINK = 29,
SIID_SLOWFILE = 30,
SIID_RECYCLER = 31,
SIID_RECYCLERFULL = 32,
SIID_MEDIACDAUDIO = 40,
SIID_LOCK = 47,
SIID_AUTOLIST = 49,
SIID_PRINTERNET = 50,
SIID_SERVERSHARE = 51,
SIID_PRINTERFAX = 52,
SIID_PRINTERFAXNET = 53,
SIID_PRINTERFILE = 54,
SIID_STACK = 55,
SIID_MEDIASVCD = 56,
SIID_STUFFEDFOLDER = 57,
SIID_DRIVEUNKNOWN = 58,
SIID_DRIVEDVD = 59,
SIID_MEDIADVD = 60,
SIID_MEDIADVDRAM = 61,
SIID_MEDIADVDRW = 62,
SIID_MEDIADVDR = 63,
SIID_MEDIADVDROM = 64,
SIID_MEDIACDAUDIOPLUS = 65,
SIID_MEDIACDRW = 66,
SIID_MEDIACDR = 67,
SIID_MEDIACDBURN = 68,
SIID_MEDIABLANKCD = 69,
SIID_MEDIACDROM = 70,
SIID_AUDIOFILES = 71,
SIID_IMAGEFILES = 72,
SIID_VIDEOFILES = 73,
SIID_MIXEDFILES = 74,
SIID_FOLDERBACK = 75,
SIID_FOLDERFRONT = 76,
SIID_SHIELD = 77,
SIID_WARNING = 78,
SIID_INFO = 79,
SIID_ERROR = 80,
SIID_KEY = 81,
SIID_SOFTWARE = 82,
SIID_RENAME = 83,
SIID_DELETE = 84,
SIID_MEDIAAUDIODVD = 85,
SIID_MEDIAMOVIEDVD = 86,
SIID_MEDIAENHANCEDCD = 87,
SIID_MEDIAENHANCEDDVD = 88,
SIID_MEDIAHDDVD = 89,
SIID_MEDIABLURAY = 90,
SIID_MEDIAVCD = 91,
SIID_MEDIADVDPLUSR = 92,
SIID_MEDIADVDPLUSRW = 93,
SIID_DESKTOPPC = 94,
SIID_MOBILEPC = 95,
SIID_USERS = 96,
SIID_MEDIASMARTMEDIA = 97,
SIID_MEDIACOMPACTFLASH = 98,
SIID_DEVICECELLPHONE = 99,
SIID_DEVICECAMERA = 100,
SIID_DEVICEVIDEOCAMERA = 101,
SIID_DEVICEAUDIOPLAYER = 102,
SIID_NETWORKCONNECT = 103,
SIID_INTERNET = 104,
SIID_ZIPFILE = 105,
SIID_SETTINGS = 106,
SIID_DRIVEHDDVD = 132,
SIID_DRIVEBD = 133,
SIID_MEDIAHDDVDROM = 134,
SIID_MEDIAHDDVDR = 135,
SIID_MEDIAHDDVDRAM = 136,
SIID_MEDIABDROM = 137,
SIID_MEDIABDR = 138,
SIID_MEDIABDRE = 139,
SIID_CLUSTEREDDRIVE = 140,
SIID_MAX_ICONS = 175
};
private struct SHSTOCKICONINFO {
DWORD cbSize;
HICON hIcon;
int iSysImageIndex;
int iIcon;
WCHAR[MAX_PATH] szPath;
};
private extern(Windows) HRESULT _dummy_SHGetStockIconInfo(SHSTOCKICONID siid, UINT uFlags, SHSTOCKICONINFO *psii);
class WindowsIconProvider : IconProviderBase
{
this()
{
import std.windows.syserror;
_shell = wenforce(LoadLibraryA("Shell32"), "Could not load Shell32 library");
_SHGetStockIconInfo = cast(typeof(&_dummy_SHGetStockIconInfo))wenforce(GetProcAddress(_shell, "SHGetStockIconInfo"), "Could not load SHGetStockIconInfo");
}
~this()
{
if (_shell) {
FreeLibrary(_shell);
}
foreach(ref buf; _cache)
{
buf.clear();
}
}
DrawBufRef getIconFromStock(SHSTOCKICONID id)
{
if (_SHGetStockIconInfo) {
auto found = id in _cache;
if (found) {
return *found;
}
HICON icon = getStockIcon(id);
if (icon) {
scope(exit) DestroyIcon(icon);
auto image = DrawBufRef(iconToImage(icon));
_cache[id] = image;
return image;
} else {
_cache[id] = DrawBufRef(null); // save the fact that the icon was not found
}
}
return DrawBufRef(null);
}
override DrawBufRef getStandardIcon(StandardIcon icon)
{
if (_SHGetStockIconInfo) {
return getIconFromStock(standardIconToStockId(icon));
}
return DrawBufRef(null);
}
private:
SHSTOCKICONID standardIconToStockId(StandardIcon icon) nothrow pure
{
with(SHSTOCKICONID)
final switch(icon) with(StandardIcon)
{
case document:
return SIID_DOCASSOC;
case application:
return SIID_APPLICATION;
case folder:
return SIID_FOLDER;
case folderOpen:
return SIID_FOLDEROPEN;
case driveFloppy:
return SIID_DRIVE35;
case driveRemovable:
return SIID_DRIVEREMOVE;
case driveFixed:
return SIID_DRIVEFIXED;
case driveCD:
return SIID_DRIVECD;
case driveDVD:
return SIID_DRIVEDVD;
case server:
return SIID_SERVER;
case printer:
return SIID_PRINTER;
case find:
return SIID_FIND;
case help:
return SIID_HELP;
case sharedItem:
return SIID_SHARE;
case link:
return SIID_LINK;
case trashcanEmpty:
return SIID_RECYCLER;
case trashcanFull:
return SIID_RECYCLERFULL;
case mediaCDAudio:
return SIID_MEDIACDAUDIO;
case mediaDVDAudio:
return SIID_MEDIAAUDIODVD;
case mediaDVD:
return SIID_MEDIADVD;
case mediaCD:
return SIID_MEDIABLANKCD;
case fileAudio:
return SIID_AUDIOFILES;
case fileImage:
return SIID_IMAGEFILES;
case fileVideo:
return SIID_VIDEOFILES;
case fileZip:
return SIID_ZIPFILE;
case fileUnknown:
return SIID_DOCNOASSOC;
case warning:
return SIID_WARNING;
case information:
return SIID_INFO;
case error:
return SIID_ERROR;
case password:
return SIID_KEY;
case rename:
return SIID_RENAME;
case deleteItem:
return SIID_DELETE;
case computer:
return SIID_DESKTOPPC;
case laptop:
return SIID_MOBILEPC;
case users:
return SIID_USERS;
case deviceCellphone:
return SIID_DEVICECELLPHONE;
case deviceCamera:
return SIID_DEVICECAMERA;
case deviceCameraVideo:
return SIID_DEVICEVIDEOCAMERA;
}
}
HICON getStockIcon(SHSTOCKICONID id)
{
assert(_SHGetStockIconInfo !is null);
enum SHGSI_ICON = 0x000000100;
SHSTOCKICONINFO info;
info.cbSize = SHSTOCKICONINFO.sizeof;
if (_SHGetStockIconInfo(id, SHGSI_ICON, &info) == S_OK) {
return info.hIcon;
}
Log.d("Could not get icon from stock. Id: ", id);
return null;
}
ColorDrawBuf iconToImage(HICON hIcon)
{
BITMAP bm;
ICONINFO iconInfo;
GetIconInfo(hIcon, &iconInfo);
GetObject(iconInfo.hbmColor, BITMAP.sizeof, &bm);
const int width = bm.bmWidth;
const int height = bm.bmHeight;
const int bytesPerScanLine = (width * 3 + 3) & 0xFFFFFFFC;
const int size = bytesPerScanLine * height;
BITMAPINFO infoheader;
infoheader.bmiHeader.biSize = BITMAPINFOHEADER.sizeof;
infoheader.bmiHeader.biWidth = width;
infoheader.bmiHeader.biHeight = height;
infoheader.bmiHeader.biPlanes = 1;
infoheader.bmiHeader.biBitCount = 24;
infoheader.bmiHeader.biCompression = BI_RGB;
infoheader.bmiHeader.biSizeImage = size;
ubyte[] pixelsIconRGB = new ubyte[size];
ubyte[] alphaPixels = new ubyte[size];
HDC hDC = CreateCompatibleDC(null);
scope(exit) DeleteDC(hDC);
HBITMAP hBmpOld = cast(HBITMAP)SelectObject(hDC, cast(HGDIOBJ)(iconInfo.hbmColor));
if(!GetDIBits(hDC, iconInfo.hbmColor, 0, height, cast(LPVOID) pixelsIconRGB.ptr, &infoheader, DIB_RGB_COLORS))
return null;
SelectObject(hDC, hBmpOld);
if(!GetDIBits(hDC, iconInfo.hbmMask, 0,height,cast(LPVOID)alphaPixels.ptr, &infoheader, DIB_RGB_COLORS))
return null;
const int lsSrc = width*3;
auto colorDrawBuf = new ColorDrawBuf(width, height);
for(int y=0; y<height; y++)
{
const int linePosSrc = (height-1-y)*lsSrc;
auto pixelLine = colorDrawBuf.scanLine(y);
for(int x=0; x<width; x++)
{
const int currentSrcPos = linePosSrc+x*3;
// BGR -> ARGB
const uint red = pixelsIconRGB[currentSrcPos+2];
const uint green = pixelsIconRGB[currentSrcPos+1];
const uint blue = pixelsIconRGB[currentSrcPos];
const uint alpha = alphaPixels[currentSrcPos];
const uint color = (red << 16) | (green << 8) | blue | (alpha << 24);
pixelLine[x] = color;
}
}
return colorDrawBuf;
}
DrawBufRef[SHSTOCKICONID] _cache;
HANDLE _shell;
typeof(&_dummy_SHGetStockIconInfo) _SHGetStockIconInfo;
}
alias WindowsIconProvider NativeIconProvider;
} else static if (isFreedesktop) {
import icontheme;
import std.typecons : tuple;
import dlangui.graphics.images;
class FreedesktopIconProvider : IconProviderBase
{
this()
{
_baseIconDirs = baseIconDirs();
auto themeName = currentIconThemeName();
IconThemeFile iconTheme = openIconTheme(themeName, _baseIconDirs);
if (iconTheme) {
_iconThemes ~= iconTheme;
_iconThemes ~= openBaseThemes(iconTheme, _baseIconDirs);
}
foreach(theme; _iconThemes) {
theme.tryLoadCache();
}
}
~this()
{
foreach(ref buf; _cache)
{
buf.clear();
}
}
DrawBufRef getIconFromTheme(string name, string context = null)
{
auto found = name in _cache;
if (found) {
return *found;
}
string iconPath;
try {
if (context.length) {
iconPath = findClosestIcon!(subdir => subdir.context == context)(name, 32, _iconThemes, _baseIconDirs);
} else {
iconPath = findClosestIcon(name, 32, _iconThemes, _baseIconDirs);
}
} catch(Exception e) {
Log.e("Error while searching for icon", name);
Log.e(e);
}
if (iconPath.length) {
auto image = DrawBufRef(loadImage(iconPath));
_cache[name] = image;
return image;
} else {
_cache[name] = DrawBufRef(null);
}
return DrawBufRef(null);
}
override DrawBufRef getStandardIcon(StandardIcon icon)
{
auto t = standardIconToNameAndContext(icon);
return getIconFromTheme(t[0], t[1]);
}
private:
auto standardIconToNameAndContext(StandardIcon icon) nothrow pure
{
final switch(icon) with(StandardIcon)
{
case document:
return tuple("x-office-document", "MimeTypes");
case application:
return tuple("application-x-executable", "MimeTypes");
case folder:
return tuple("folder", "Places");
case folderOpen:
return tuple("folder-open", "Status");
case driveFloppy:
return tuple("media-floppy", "Devices");
case driveRemovable:
return tuple("drive-removable-media", "Devices");
case driveFixed:
return tuple("drive-harddisk", "Devices");
case driveCD:
return tuple("drive-optical", "Devices");
case driveDVD:
return tuple("drive-optical", "Devices");
case server:
return tuple("network-server", "Places");
case printer:
return tuple("printer", "Devices");
case find:
return tuple("edit-find", "Actions");
case help:
return tuple("help-contents", "Actions");
case sharedItem:
return tuple("emblem-shared", "Emblems");
case link:
return tuple("emblem-symbolic-link", "Emblems");
case trashcanEmpty:
return tuple("user-trash", "Places");
case trashcanFull:
return tuple("user-trash-full", "Status");
case mediaCDAudio:
return tuple("media-optical-audio", "Devices");
case mediaDVDAudio:
return tuple("media-optical-audio", "Devices");
case mediaDVD:
return tuple("media-optical", "Devices");
case mediaCD:
return tuple("media-optical", "Devices");
case fileAudio:
return tuple("audio-x-generic", "MimeTypes");
case fileImage:
return tuple("image-x-generic", "MimeTypes");
case fileVideo:
return tuple("video-x-generic", "MimeTypes");
case fileZip:
return tuple("application-zip", "MimeTypes");
case fileUnknown:
return tuple("unknown", "MimeTypes");
case warning:
return tuple("dialog-warning", "Status");
case information:
return tuple("dialog-information", "Status");
case error:
return tuple("dialog-error", "Status");
case password:
return tuple("dialog-password", "Status");
case rename:
return tuple("edit-rename", "Actions");
case deleteItem:
return tuple("edit-delete", "Actions");
case computer:
return tuple("computer", "Devices");
case laptop:
return tuple("computer-laptop", "Devices");
case users:
return tuple("system-users", "Applications");
case deviceCellphone:
return tuple("phone", "Devices");
case deviceCamera:
return tuple("camera-photo", "Devices");
case deviceCameraVideo:
return tuple("camera-video", "Devices");
}
}
DrawBufRef[string] _cache;
string[] _baseIconDirs;
IconThemeFile[] _iconThemes;
}
alias FreedesktopIconProvider NativeIconProvider;
} else {
alias DummyIconProvider NativeIconProvider;
}

View File

@ -28,6 +28,7 @@ import dlangui.widgets.scrollbar;
import dlangui.graphics.drawbuf;
import dlangui.core.stdaction;
import dlangui.core.asyncsocket;
import dlangui.graphics.iconprovider;
static if (ENABLE_OPENGL) {
private import dlangui.graphics.gldrawbuf;
@ -1960,7 +1961,32 @@ class Platform {
return _defaultWindowIcon;
}
private IconProviderBase _iconProvider;
@property IconProviderBase iconProvider() {
if (_iconProvider is null) {
try {
_iconProvider = new NativeIconProvider();
} catch(Exception e) {
Log.e("Error while creating icon provider.", e);
Log.d("Could not create native icon provider, fallbacking to the dummy one");
_iconProvider = new DummyIconProvider();
}
}
return _iconProvider;
}
@property IconProviderBase iconProvider(IconProviderBase provider)
{
_iconProvider = provider;
return _iconProvider;
}
~this()
{
if(_iconProvider) {
destroy(_iconProvider);
}
}
}
/// get current platform object instance