Compare commits
23 Commits
6af621213a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d061e74ea | |||
| b95829bd05 | |||
| 2644932686 | |||
| 21bc239cef | |||
| fdcef60130 | |||
| 4861950176 | |||
| 9ede109998 | |||
| 69eaf63fe8 | |||
| af3a694024 | |||
| 24b58f584f | |||
| 2c7e8ef76d | |||
| 1d3cdbe6a1 | |||
| fa4c34bd53 | |||
| ba330e9e0d | |||
| 6ff4624dd8 | |||
| 2033df5335 | |||
| 0bfa8f6914 | |||
| 34f1864d5f | |||
| 0362ae4515 | |||
| 4f1b87181d | |||
| 33a1d1bac1 | |||
| 1055173246 | |||
| 3f88e96395 |
158
cmd/desktop_entry/main.ha
Normal file
158
cmd/desktop_entry/main.ha
Normal file
@@ -0,0 +1,158 @@
|
||||
use fmt;
|
||||
use fs;
|
||||
use format::xdg::ini;
|
||||
use getopt;
|
||||
use io;
|
||||
use locale;
|
||||
use os;
|
||||
use xdg::desktop_entry;
|
||||
|
||||
export fn main() void = {
|
||||
const name = os::args[0];
|
||||
|
||||
const cmd = getopt::parse(
|
||||
os::args,
|
||||
"parse and display desktop entries",
|
||||
('l', "locale", "the name of the locale to use"), // FIXME not working
|
||||
('a', "action", "display a specific action"),
|
||||
"file");
|
||||
defer getopt::finish(&cmd);
|
||||
|
||||
let local = "";
|
||||
let action = "";
|
||||
for (let opt .. cmd.opts) switch (opt.0) {
|
||||
case 'l' => local = opt.1;
|
||||
case 'a' => action = opt.1;
|
||||
case => abort();
|
||||
};
|
||||
|
||||
let local = if (local == "") {
|
||||
yield locale::get_messages();
|
||||
} else {
|
||||
yield locale::parse(local)!;
|
||||
};
|
||||
|
||||
let file_name = if (len(cmd.args) == 1) {
|
||||
yield cmd.args[0];
|
||||
} else {
|
||||
fmt::fprintf(os::stderr, "{}: expected 1 argument\n", name)!;
|
||||
getopt::printusage(os::stderr, name, cmd.help)!;
|
||||
os::exit(os::status::FAILURE);
|
||||
};
|
||||
|
||||
let file = match (parse_file(file_name)) {
|
||||
case let file: desktop_entry::file =>
|
||||
yield file;
|
||||
case let err: fs::error =>
|
||||
fmt::fprintf(
|
||||
os::stderr, "{}: {}: {}\n",
|
||||
name, file_name, fs::strerror(err))!;
|
||||
os::exit(os::status::FAILURE);
|
||||
case let err: ini::error =>
|
||||
fmt::fprintf(
|
||||
os::stderr, "{}: {}: {}\n",
|
||||
name, file_name, ini::strerror(err))!;
|
||||
os::exit(os::status::FAILURE);
|
||||
};
|
||||
|
||||
let result = if (action == "") {
|
||||
yield print_file(file, local);
|
||||
} else {
|
||||
yield print_action(file, local, action);
|
||||
};
|
||||
|
||||
match(result) {
|
||||
case let err: io::error =>
|
||||
fmt::fprintf(os::stderr, "{}: {}\n", name, io::strerror(err))!;
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
|
||||
fn parse_file (file_name: str) (desktop_entry::file | fs::error | ini::error) = {
|
||||
let file = os::open(file_name)?;
|
||||
defer io::close(file)!;
|
||||
return desktop_entry::parse(file);
|
||||
};
|
||||
|
||||
fn print_file(file: desktop_entry::file, local: locale::locale) (void | io::error) = {
|
||||
fmt::printf("Type={}\n", file.typ)?;
|
||||
fmt::printf("Version={}\n", file.version)?;
|
||||
match (locale::string_resolve(file.name, local)) {
|
||||
case let name: str => fmt::printf("Name={}\n", name)?;
|
||||
case void => void;
|
||||
};
|
||||
match (locale::string_resolve(file.generic_name, local)) {
|
||||
case let generic_name: str => if (generic_name != "") fmt::printf("GenericName={}\n", generic_name)?;
|
||||
case void => void;
|
||||
};
|
||||
if(file.no_display) fmt::println("NoDisplay=true")?;
|
||||
match (locale::string_resolve(file.comment, local)) {
|
||||
case let comment: str => if (comment != "") fmt::printf("Comment={}\n", comment)?;
|
||||
case void => void;
|
||||
};
|
||||
match (locale::string_resolve(file.icon, local)) {
|
||||
case let icon: str => if (icon != "") fmt::printf("Icon={}\n", icon)?;
|
||||
case void => void;
|
||||
};
|
||||
if (file.hidden) fmt::println("Hidden=true")?;
|
||||
if (file.dbus_activatable) fmt::println("DBusActivatable=true")?;
|
||||
if (file.try_exec != "") fmt::printf("TryExec={}\n", file.try_exec)?;
|
||||
if (file.exec != "") fmt::printf("Exec={}\n", file.exec)?;
|
||||
if (file.path != "") fmt::printf("Path={}\n", file.path)?;
|
||||
if (file.terminal) fmt::println("Terminal=true")?;
|
||||
if (len(file.mime_type) > 0) {
|
||||
fmt::print("MimeType=")?;
|
||||
print_strings(file.mime_type)?;
|
||||
};
|
||||
if (len(file.categories) > 0) {
|
||||
fmt::print("Categories=")?;
|
||||
print_strings(file.categories)?;
|
||||
};
|
||||
if (len(file.implements) > 0) {
|
||||
fmt::print("Implements=")?;
|
||||
print_strings(file.implements)?;
|
||||
};
|
||||
if (len(file.keywords) > 0) {
|
||||
fmt::print("Keywords=")? ;
|
||||
match (locale::strings_resolve(file.keywords, local)) {
|
||||
case let keywords: []str => print_strings(keywords)?;
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
if (file.startup_notify) fmt::println("StartupNotify=true")?;
|
||||
if (file.startup_wm_class != "") fmt::printf("StartupWMClass={}\n", file.startup_wm_class)?;
|
||||
if (file.url != "") fmt::printf("URL={}\n", file.url)?;
|
||||
if (file.prefers_non_default_gpu) fmt::println("PrefersNonDefaultGPU=true")?;
|
||||
if (file.single_main_window) fmt::println("SingleMainWindow=true")?;
|
||||
};
|
||||
|
||||
fn print_strings(strings: []str) (void | io::error) = {
|
||||
for (let string .. strings) {
|
||||
fmt::printf("{};", string)?;
|
||||
};
|
||||
fmt::println()?;
|
||||
};
|
||||
|
||||
fn print_action(file: desktop_entry::file, local: locale::locale, key: str) (void | io::error) = {
|
||||
let action: (desktop_entry::action | void) = void;
|
||||
for (let actio .. file.actions) {
|
||||
if (actio.key == key) {
|
||||
action = actio;
|
||||
break;
|
||||
};
|
||||
};
|
||||
let action = match (action) {
|
||||
case let action: desktop_entry::action => yield action;
|
||||
case void => return;
|
||||
};
|
||||
|
||||
match (locale::string_resolve(action.name, local)) {
|
||||
case let name: str => fmt::printf("Name={}\n", name)?;
|
||||
case void => void;
|
||||
};
|
||||
match (locale::string_resolve(action.icon, local)) {
|
||||
case let icon: str => if (icon != "") fmt::printf("Icon={}\n", icon)?;
|
||||
case void => void;
|
||||
};
|
||||
if (action.exec != "") fmt::printf("Exec={}\n", action.exec)?;
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
The desktop_entry module implements the XDG Desktop Entry Specification as
|
||||
described in (https://specifications.freedesktop.org/desktop-entry-spec/latest).
|
||||
Since other specifications make use of the basic desktop entry format (but with
|
||||
different group names, entry requirements, etc.), this module implements:
|
||||
|
||||
1. The generalized format, and
|
||||
2. A second processing stage to retrieve values relevant to desktop entries and
|
||||
to validate them.
|
||||
|
||||
This module will attempt to accept malformed files for the purposes of retaining
|
||||
their underlying representation, and being able to reproduce that representation
|
||||
when writing updated information to the files.
|
||||
8
format/xdg/ini/README
Normal file
8
format/xdg/ini/README
Normal file
@@ -0,0 +1,8 @@
|
||||
The ini module implements the basic INI-like format used by the XDG Desktop
|
||||
Entry Specification as described in
|
||||
(https://specifications.freedesktop.org/desktop-entry-spec/latest). Since other
|
||||
specifications make use of the basic desktop entry format (but with different
|
||||
group names, entry requirements, etc.), this module only implements the
|
||||
generalized format. This module will attempt to accept malformed files for the
|
||||
purposes of retaining their underlying representation, and being able to
|
||||
reproduce that representation when writing updated information to the files.
|
||||
@@ -1,3 +1,5 @@
|
||||
use fmt;
|
||||
use io;
|
||||
use locale;
|
||||
use strings;
|
||||
|
||||
@@ -42,3 +44,19 @@ export fn entry_finish(entr: entry) void = {
|
||||
free(entr.value);
|
||||
locale::finish(entr.locale);
|
||||
};
|
||||
|
||||
// Formats a [[line]] and writes it to an [[io::handle]].
|
||||
export fn fprint(output: io::handle, lin: line) (size | io::error) = match (lin) {
|
||||
case blank => yield fmt::fprintln(output);
|
||||
case let lin: comment => yield fmt::fprintf(output, "#{}\n", lin: str);
|
||||
case let lin: group_header => yield fmt::fprintf(output, "[{}]\n", lin: str);
|
||||
case let lin: entry =>
|
||||
let wrote = fmt::print(lin.key)?;
|
||||
if (!locale::equal(locale::c, lin.locale)) {
|
||||
let localestr = locale::format(lin.locale);
|
||||
defer free(localestr);
|
||||
wrote += fmt::printf("[{}]", localestr)?;
|
||||
};
|
||||
wrote += fmt::printf("={}\n", lin.value)?;
|
||||
yield wrote;
|
||||
};
|
||||
@@ -99,6 +99,7 @@ fn parse_entry(line: str) (entry | error) = {
|
||||
case locale::invalid => return invalid_entry;
|
||||
};
|
||||
};
|
||||
local.encoding = "";
|
||||
|
||||
validate_entry_key(key)?;
|
||||
return entry {
|
||||
@@ -34,12 +34,12 @@ use os;
|
||||
locale::parse("en_US")!),
|
||||
entry_new(
|
||||
h_dac, "Name", "Zweep zoop flooble glorp",
|
||||
locale::parse("xx_XX.UTF-8")!),
|
||||
locale::parse("xx_XX")!),
|
||||
"Another comment": comment,
|
||||
entry_new(h_dac, "Icon", "fooview-new", locale::c),
|
||||
];
|
||||
|
||||
let file = os::open("format/desktop_entry/test_data/foo.desktop")!;
|
||||
let file = os::open("format/xdg/ini/test_data/foo.desktop")!;
|
||||
defer io::close(file)!;
|
||||
let scanne = scan(file);
|
||||
defer finish(&scanne);
|
||||
@@ -83,11 +83,11 @@ use os;
|
||||
locale::parse("en_US")!),
|
||||
entry_new(
|
||||
h_dac, "Name", "Zweep zoop flooble glorp",
|
||||
locale::parse("xx_XX.UTF-8")!),
|
||||
locale::parse("xx_XX")!),
|
||||
entry_new(h_dac, "Icon", "fooview-new", locale::c),
|
||||
];
|
||||
|
||||
let file = os::open("format/desktop_entry/test_data/foo.desktop")!;
|
||||
let file = os::open("format/xdg/ini/test_data/foo.desktop")!;
|
||||
defer io::close(file)!;
|
||||
let scanne = scan(file);
|
||||
defer finish(&scanne);
|
||||
@@ -109,7 +109,7 @@ use os;
|
||||
assert(entry_equal(parse_entry("hello[sr_YU.UTF-8@Latn]=world")!, entry {
|
||||
key = "hello",
|
||||
value = "world",
|
||||
locale = locale::parse("sr_YU.UTF-8@Latn")!,
|
||||
locale = locale::parse("sr_YU@Latn")!,
|
||||
...
|
||||
}));
|
||||
};
|
||||
28
format/xdg/ini/test_data/foo_full.desktop
Normal file
28
format/xdg/ini/test_data/foo_full.desktop
Normal file
@@ -0,0 +1,28 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Foo Viewer
|
||||
GenericName=Foo Viewer
|
||||
GenericName[xx_XX.UTF-8]=Sneep glorp
|
||||
Comment=The best viewer for Foo objects available!
|
||||
TryExec=fooview
|
||||
Exec=fooview %F
|
||||
Icon=fooview
|
||||
MimeType=image/x-foo;
|
||||
Actions=Gallery;Create;
|
||||
OnlyShowIn=MATE;KDE;
|
||||
NotShowIn=GNOME
|
||||
Categories=Graphics;Utility
|
||||
Implements=com.example.Example;com.example.OtherExample
|
||||
Keywords=foo;image;view;viewer
|
||||
|
||||
[Desktop Action Gallery]
|
||||
Exec=fooview --gallery
|
||||
Name=Browse Gallery
|
||||
|
||||
[Desktop Action Create]
|
||||
Exec=fooview --create-new
|
||||
Name=Create a new Foo!
|
||||
Name[en_US]=Create a new Foo!
|
||||
Name[xx_XX.UTF-8]=Zweep zoop flooble glorp
|
||||
Icon=fooview-new
|
||||
128
locale/string.ha
128
locale/string.ha
@@ -1,59 +1,133 @@
|
||||
// A string localized into multiple locales.
|
||||
export type string = [](locale, str);
|
||||
|
||||
// Selects the most appropriate localized version of a [[string]] given a
|
||||
// [[locale]]. The matching algorithm used is the one specified by the
|
||||
// XDG Desktop Entry Specification, §5. Memory is borrowed from the input.
|
||||
export fn string_resolve(strin: string, local: locale) (str | void) = {
|
||||
// The matching is done as follows. If LC_MESSAGES is of the form
|
||||
// lang_COUNTRY.ENCODING@MODIFIER, then it will match a key of the form
|
||||
// lang_COUNTRY@MODIFIER. If such a key does not exist, it will attempt
|
||||
// to match lang_COUNTRY followed by lang@MODIFIER. Then, a match
|
||||
// against lang by itself will be attempted. Finally, if no matching key
|
||||
// is found the required key without a locale specified is used. The
|
||||
// encoding from the LC_MESSAGES value is ignored when matching.
|
||||
//
|
||||
// If LC_MESSAGES does not have a MODIFIER field, then no key with a
|
||||
// modifier will be matched. Similarly, if LC_MESSAGES does not have a
|
||||
// COUNTRY field, then no key with a country specified will be matched.
|
||||
// If LC_MESSAGES just has a lang field, then it will do a straight
|
||||
// match to a key with a similar value.
|
||||
// -------------------------------------------------------------------------- //
|
||||
// The matching is done as follows. If LC_MESSAGES is of the form
|
||||
// lang_COUNTRY.ENCODING@MODIFIER, then it will match a key of the form
|
||||
// lang_COUNTRY@MODIFIER. If such a key does not exist, it will attempt to match
|
||||
// lang_COUNTRY followed by lang@MODIFIER. Then, a match against lang by itself
|
||||
// will be attempted. Finally, if no matching key is found the required key
|
||||
// without a locale specified is used. The encoding from the LC_MESSAGES value
|
||||
// is ignored when matching.
|
||||
//
|
||||
// If LC_MESSAGES does not have a MODIFIER field, then no key with a modifier
|
||||
// will be matched. Similarly, if LC_MESSAGES does not have a COUNTRY field,
|
||||
// then no key with a country specified will be matched. If LC_MESSAGES just has
|
||||
// a lang field, then it will do a straight match to a key with a similar value.
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
// Selects the most appropriate localized version of a [[string]] given a
|
||||
// [[locale]]. First, a match against the exact locale is attempted. If nothing
|
||||
// is found, the matching algorithm specified by the XDG Desktop Entry
|
||||
// Specification §5 is used. If nothing is found, an exact match with the C
|
||||
// locale will be attempted. If nothing is found, void will be returned. Memory
|
||||
// is borrowed from the input.
|
||||
export fn string_resolve(strin: string, local: locale) (str | void) = {
|
||||
// lang_COUNTRY@MODIFIER.ENCODING
|
||||
match (string_resolve_exact(strin, local)) {
|
||||
case let result: str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang_COUNTRY@MODIFIER
|
||||
let lang_country_modifier = local;
|
||||
lang_country_modifier.encoding = "";
|
||||
match (string_resolve_exact(strin, lang_country_modifier)) {
|
||||
case let index: size => return index;
|
||||
case let result: str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang_COUNTRY
|
||||
let lang_country = local;
|
||||
lang_country.modifier = "";
|
||||
match (string_resolve_exact(strin, lang_country)) {
|
||||
case let index: size => return index;
|
||||
case let result: str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang@MODIFIER
|
||||
let lang_modifier = lang_country_modifier;
|
||||
lang_modifier.country = "";
|
||||
match (string_resolve_exact(strin, lang_modifier)) {
|
||||
case let index: size => return index;
|
||||
case let result: str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang
|
||||
let lang = lang_modifier;
|
||||
lang.modifier = "";
|
||||
match (string_resolve_exact(strin, lang)) {
|
||||
case let index: size => return index;
|
||||
case let result: str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// fallback to c locale
|
||||
match (string_resolve_exact(strin, c)) {
|
||||
case let result: str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
return void;
|
||||
};
|
||||
|
||||
fn string_resolve_exact(strin: string, local: locale) (size | void) = {
|
||||
let index = 0z;
|
||||
for (let pair .. strn) {
|
||||
if(equal(pair.1, local)) return index;
|
||||
index += 1;
|
||||
};
|
||||
fn string_resolve_exact(strin: string, local: locale) (str | void) = {
|
||||
for (let pair .. strin) if(equal(pair.0, local)) return pair.1;
|
||||
};
|
||||
|
||||
// A list of strings localized into multiple locales.
|
||||
export type strings = [](locale, []str);
|
||||
|
||||
// Selects the most appropriate localized version of a [[string]] given a
|
||||
// [[locale]]. See the documentation for [[string_resolve]] for more
|
||||
// information.
|
||||
export fn strings_resolve(strins: strings, local: locale) ([]str | void) = {
|
||||
// lang_COUNTRY@MODIFIER.ENCODING
|
||||
match (strings_resolve_exact(strins, local)) {
|
||||
case let result: []str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang_COUNTRY@MODIFIER
|
||||
let lang_country_modifier = local;
|
||||
lang_country_modifier.encoding = "";
|
||||
match (strings_resolve_exact(strins, lang_country_modifier)) {
|
||||
case let result: []str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang_COUNTRY
|
||||
let lang_country = local;
|
||||
lang_country.modifier = "";
|
||||
match (strings_resolve_exact(strins, lang_country)) {
|
||||
case let result: []str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang@MODIFIER
|
||||
let lang_modifier = lang_country_modifier;
|
||||
lang_modifier.country = "";
|
||||
match (strings_resolve_exact(strins, lang_modifier)) {
|
||||
case let result: []str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// lang
|
||||
let lang = lang_modifier;
|
||||
lang.modifier = "";
|
||||
match (strings_resolve_exact(strins, lang)) {
|
||||
case let result: []str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
// fallback to c locale
|
||||
match (strings_resolve_exact(strins, c)) {
|
||||
case let result: []str => return result;
|
||||
case => void;
|
||||
};
|
||||
|
||||
return void;
|
||||
};
|
||||
|
||||
fn strings_resolve_exact(strins: strings, local: locale) ([]str | void) = {
|
||||
for (let pair .. strins) if(equal(pair.0, local)) return pair.1;
|
||||
};
|
||||
|
||||
28
locale/string_test.ha
Normal file
28
locale/string_test.ha
Normal file
@@ -0,0 +1,28 @@
|
||||
@test fn string_resolve() void = {
|
||||
assert(string_resolve([
|
||||
(c, "c"),
|
||||
(parse("xx_XX")!, "xx_XX"),
|
||||
], c) as str == "c");
|
||||
assert(string_resolve([
|
||||
(c, "c"),
|
||||
(parse("xx_XX")!, "xx_XX"),
|
||||
], parse("xx_XX.UTF-8")!) as str == "xx_XX");
|
||||
assert(string_resolve([
|
||||
(c, "c"),
|
||||
(parse("xx_XX")!, "xx_XX"),
|
||||
(parse("xx_XX.UTF-8")!, "xx_XX2"),
|
||||
], parse("xx_XX")!) as str == "xx_XX");
|
||||
assert(string_resolve([
|
||||
(c, "c"),
|
||||
(parse("xx_XX")!, "xx_XX"),
|
||||
(parse("xx_XX.UTF-8")!, "xx_XX2"),
|
||||
], parse("xx_XX.UTF-8")!) as str == "xx_XX2");
|
||||
assert(string_resolve([
|
||||
(c, "c"),
|
||||
(parse("xx_XX")!, "xx_XX"),
|
||||
], parse("yy_YY.UTF-8")!) as str == "c");
|
||||
assert(string_resolve([
|
||||
(parse("xx_XX")!, "xx_XX"),
|
||||
(parse("zz_ZZ")!, "zz_ZZ"),
|
||||
], parse("yy_YY")!) is void);
|
||||
};
|
||||
2
xdg/basedir/README
Normal file
2
xdg/basedir/README
Normal file
@@ -0,0 +1,2 @@
|
||||
The basedir module implements the XDG Base Directory Specification as described
|
||||
in (https://specifications.freedesktop.org/basedir-spec/latest/).
|
||||
98
xdg/basedir/basedir.ha
Normal file
98
xdg/basedir/basedir.ha
Normal file
@@ -0,0 +1,98 @@
|
||||
// Returns the single base directory relative to which user-specific data files
|
||||
// should be written, which is defined by $XDG_DATA_HOME. If $XDG_DATA_HOME is
|
||||
// either not set or empty, a default equal to $HOME/.local/share is used.
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn data_home (prog: str = "") str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns the single base directory relative to which user-specific
|
||||
// configuration files should be written, which is defined by $XDG_CONFIG_HOME.
|
||||
// If $XDG_CONFIG_HOME is either not set or empty, a default equal to
|
||||
// $HOME/.config is used.
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.s
|
||||
export fn config_home (prog: str = "") str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns the single base directory relative to which user-specific state data
|
||||
// that should persist between (application) restarts, but that is not important
|
||||
// or portable enough to the user that it should be stored. It is defined by
|
||||
// $XDG_STATE_HOME. It may contain:
|
||||
//
|
||||
// - actions history (logs, history, recently used files, …)
|
||||
// - current state of the application that can be reused on a restart (view,
|
||||
// layout, open files, undo history, …)
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn state_home (prog: str = "") str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// CacheHome returns the single base directory relative to which user-specific
|
||||
// non-essential (cached) data should be written, which is defined by
|
||||
// $XDG_CACHE_HOME. If $XDG_CACHE_HOME is either not set or empty, a default
|
||||
// equal to $HOME/.cache is used.
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn cache_home (prog: str = "") str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns the single base directory relative to which user-specific runtime
|
||||
// files and other file objects should be placed, which is defined by
|
||||
// $XDG_RUNTIME_DIR. This module does not provide a fallback directory. If this
|
||||
// function returns an error, applications should fall back to a replacement
|
||||
// directory with similar capabilities (in accordance with §3) and print a
|
||||
// warning message.
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn runtime_dir (prog: str = "") str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns the set of preference ordered base directories relative to which data
|
||||
// files should be searched, which is defined by $XDG_DATA_DIRS. If
|
||||
// $XDG_DATA_DIRS is either not set or empty, a value equal to
|
||||
// /usr/local/share/:/usr/share/ is used. It is reccomended to call [[data_all]]
|
||||
// instead for most use cases.
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn data_dirs (prog: str = "") []str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns set of preference ordered base directories relative to which
|
||||
// configuration files should be searched, which is defined by $XDG_CONFIG_DIRS.
|
||||
// If $XDG_CONFIG_DIRS is either not set or empty, a value equal to /etc/xdg is
|
||||
// used. It is reccomended to call [[config_all]] instead for most use cases.
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn config_dirs (prog: str = "") []str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns the result of [[data_home]] in front of [[data_dirs]].
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn data_all (prog: str = "") []str = {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// Returns the result of [[config_home]] in front of [[config_dirs]].
|
||||
//
|
||||
// The memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dup]] to extend its lifetime.
|
||||
export fn config_all (prog: str = "") []str = {
|
||||
// TODO
|
||||
};
|
||||
3
xdg/desktop_entry/README
Normal file
3
xdg/desktop_entry/README
Normal file
@@ -0,0 +1,3 @@
|
||||
The desktop_entry module implements the XDG Desktop Entry Specification as
|
||||
described in (https://specifications.freedesktop.org/desktop-entry-spec/latest).
|
||||
For the generalized format, see [[format::xdg::ini]].
|
||||
255
xdg/desktop_entry/desktop_entry.ha
Normal file
255
xdg/desktop_entry/desktop_entry.ha
Normal file
@@ -0,0 +1,255 @@
|
||||
use format::xdg::ini;
|
||||
use io;
|
||||
use locale;
|
||||
use strings;
|
||||
|
||||
// The information from a desktop entry file.
|
||||
// Specification: §6
|
||||
export type file = struct {
|
||||
typ: str,
|
||||
version: str,
|
||||
name: locale::string,
|
||||
generic_name: locale::string,
|
||||
no_display: bool,
|
||||
comment: locale::string,
|
||||
icon: locale::string,
|
||||
hidden: bool,
|
||||
only_show_in: []str,
|
||||
not_show_in: []str,
|
||||
dbus_activatable: bool,
|
||||
try_exec: str,
|
||||
exec: str,
|
||||
path: str,
|
||||
terminal: bool,
|
||||
mime_type: []str,
|
||||
categories: []str,
|
||||
implements: []str,
|
||||
keywords: locale::strings,
|
||||
startup_notify: bool,
|
||||
startup_wm_class: str,
|
||||
url: str,
|
||||
prefers_non_default_gpu: bool,
|
||||
single_main_window: bool,
|
||||
|
||||
actions: []action,
|
||||
};
|
||||
|
||||
// An additional application action.
|
||||
// Specification: §11
|
||||
export type action = struct {
|
||||
key: str,
|
||||
name: locale::string,
|
||||
icon: locale::string,
|
||||
exec: str,
|
||||
};
|
||||
|
||||
// Parses a desktop entry file. Use [[file_finish]] to get rid of it.
|
||||
export fn parse(input: io::handle) (file | ini::error) = {
|
||||
let file = file { ... };
|
||||
let scanne = ini::scan(input);
|
||||
defer ini::finish(&scanne);
|
||||
|
||||
for (let entr => ini::next_entry(&scanne)?) {
|
||||
match(parse_handle_entry(&file, entr)) {
|
||||
case let err: ini::error =>
|
||||
finish(&file);
|
||||
return err;
|
||||
case void => void;
|
||||
};
|
||||
};
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
// Frees resources associated with a [[file]].
|
||||
export fn finish(this: *file) void = {
|
||||
free(this.typ);
|
||||
free(this.version);
|
||||
locale_string_finish(this.name);
|
||||
locale_string_finish(this.generic_name);
|
||||
locale_string_finish(this.comment);
|
||||
locale_string_finish(this.icon);
|
||||
strings::freeall(this.only_show_in);
|
||||
strings::freeall(this.not_show_in);
|
||||
free(this.try_exec);
|
||||
free(this.exec);
|
||||
free(this.path);
|
||||
strings::freeall(this.mime_type);
|
||||
strings::freeall(this.categories);
|
||||
strings::freeall(this.implements);
|
||||
locale_strings_finish(this.keywords);
|
||||
free(this.startup_wm_class);
|
||||
free(this.url);
|
||||
|
||||
for (let actio .. this.actions) {
|
||||
free(actio.key);
|
||||
locale_string_finish(actio.name);
|
||||
locale_string_finish(actio.icon);
|
||||
free(actio.exec);
|
||||
};
|
||||
free(this.actions);
|
||||
};
|
||||
|
||||
fn parse_handle_entry(this: *file, entr: ini::entry) (void | ini::error) = {
|
||||
const desktop_action_prefix = "Desktop Action ";
|
||||
if (entr.group == "Desktop Entry") {
|
||||
return parse_handle_ini_entry(this, entr);
|
||||
} else if (strings::hasprefix(entr.group, desktop_action_prefix)) {
|
||||
let key = strings::trimprefix(entr.group, desktop_action_prefix);
|
||||
return parse_handle_desktop_action_entry(this, entr, key);
|
||||
};
|
||||
};
|
||||
|
||||
fn parse_handle_desktop_action_entry(this: *file, entr: ini::entry, key: str) (void | ini::error) = {
|
||||
let actio = match(parse_find_action(this, key)) {
|
||||
case let actio: *action => yield actio;
|
||||
case void => return void;
|
||||
};
|
||||
if (entr.key == "Name") {
|
||||
parse_set_locale_string(
|
||||
&actio.name, entr.locale,
|
||||
ini::parse_localestring(entr.value)?);
|
||||
} else if (entr.key == "Icon") {
|
||||
parse_set_locale_string(
|
||||
&actio.icon, entr.locale,
|
||||
ini::parse_iconstring(entr.value)?);
|
||||
} else if (entr.key == "Exec") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
actio.exec = ini::parse_string(entr.value)?;
|
||||
};
|
||||
};
|
||||
|
||||
fn parse_handle_ini_entry(this: *file, entr: ini::entry) (void | ini::error) = {
|
||||
if (entr.key == "Type") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.typ = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "Version") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.version = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "Name") {
|
||||
parse_set_locale_string(
|
||||
&this.name, entr.locale,
|
||||
ini::parse_localestring(entr.value)?);
|
||||
} else if (entr.key == "GenericName") {
|
||||
parse_set_locale_string(
|
||||
&this.generic_name, entr.locale,
|
||||
ini::parse_localestring(entr.value)?);
|
||||
} else if (entr.key == "NoDisplay") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.no_display = ini::parse_boolean(entr.value)?;
|
||||
} else if (entr.key == "Comment") {
|
||||
parse_set_locale_string(
|
||||
&this.comment, entr.locale,
|
||||
ini::parse_localestring(entr.value)?);
|
||||
} else if (entr.key == "Icon") {
|
||||
parse_set_locale_string(
|
||||
&this.icon, entr.locale,
|
||||
ini::parse_iconstring(entr.value)?);
|
||||
} else if (entr.key == "Hidden") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.hidden = ini::parse_boolean(entr.value)?;
|
||||
} else if (entr.key == "OnlyShowIn") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.only_show_in = ini::parse_strings(entr.value)?;
|
||||
} else if (entr.key == "NotShowIn") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.not_show_in = ini::parse_strings(entr.value)?;
|
||||
} else if (entr.key == "DBusActivatable") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.dbus_activatable = ini::parse_boolean(entr.value)?;
|
||||
} else if (entr.key == "TryExec") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.try_exec = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "Exec") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.exec = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "Path") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.path = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "Terminal") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.terminal = ini::parse_boolean(entr.value)?;
|
||||
} else if (entr.key == "Actions") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
let strings =ini::parse_strings(entr.value)?;
|
||||
defer free(strings);
|
||||
this.actions = alloc([], len(strings));
|
||||
for (let string .. strings) {
|
||||
append(this.actions, action {
|
||||
key = string,
|
||||
...
|
||||
});
|
||||
};
|
||||
} else if (entr.key == "MimeType") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.mime_type = ini::parse_strings(entr.value)?;
|
||||
} else if (entr.key == "Categories") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.categories = ini::parse_strings(entr.value)?;
|
||||
} else if (entr.key == "Implements") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.implements = ini::parse_strings(entr.value)?;
|
||||
} else if (entr.key == "Keywords") {
|
||||
parse_set_locale_strings(
|
||||
&this.keywords, entr.locale,
|
||||
ini::parse_localestrings(entr.value)?);
|
||||
} else if (entr.key == "StartupWMClass") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.startup_wm_class = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "URL") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.url = ini::parse_string(entr.value)?;
|
||||
} else if (entr.key == "PrefersNonDefaultGPU") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.prefers_non_default_gpu = ini::parse_boolean(entr.value)?;
|
||||
} else if (entr.key == "SingleMainWindow") {
|
||||
if (parse_is_localized(entr)) return void;
|
||||
this.single_main_window = ini::parse_boolean(entr.value)?;
|
||||
};
|
||||
};
|
||||
|
||||
fn parse_is_localized(entr: ini::entry) bool = {
|
||||
return !locale::equal(entr.locale, locale::c);
|
||||
};
|
||||
|
||||
fn parse_set_locale_string(dest: *locale::string, local: locale::locale, value: str) void = {
|
||||
for (let existing &.. *dest) {
|
||||
if (locale::equal(existing.0, local)) {
|
||||
existing.1 = value;
|
||||
return;
|
||||
};
|
||||
};
|
||||
append(dest, (locale::dup(local), strings::dup(value)));
|
||||
};
|
||||
|
||||
fn parse_set_locale_strings(dest: *locale::strings, local: locale::locale, value: []str) void = {
|
||||
for (let existing &.. *dest) {
|
||||
if (locale::equal(existing.0, local)) {
|
||||
existing.1 = value;
|
||||
return;
|
||||
};
|
||||
};
|
||||
append(dest, (locale::dup(local), strings::dupall(value)));
|
||||
};
|
||||
|
||||
fn parse_find_action(this: *file, key: str) (*action | void) = {
|
||||
for (let actio &.. this.actions) {
|
||||
if (actio.key == key) return actio;
|
||||
};
|
||||
};
|
||||
|
||||
fn locale_string_finish(string: locale::string) void = {
|
||||
for (let pair .. string) {
|
||||
locale::finish(pair.0);
|
||||
free(pair.1);
|
||||
};
|
||||
free(string);
|
||||
};
|
||||
|
||||
fn locale_strings_finish(strings: locale::strings) void = {
|
||||
for (let pair .. strings) {
|
||||
locale::finish(pair.0);
|
||||
strings::freeall(pair.1);
|
||||
};
|
||||
free(strings);
|
||||
};
|
||||
102
xdg/desktop_entry/desktop_entry_test.ha
Normal file
102
xdg/desktop_entry/desktop_entry_test.ha
Normal file
@@ -0,0 +1,102 @@
|
||||
use format::xdg::ini;
|
||||
use fmt;
|
||||
use io;
|
||||
use locale;
|
||||
use os;
|
||||
|
||||
@test fn parse() void = {
|
||||
let file = os::open("format/xdg/ini/test_data/foo_full.desktop")!;
|
||||
defer io::close(file)!;
|
||||
let file = parse(file)!;
|
||||
defer finish(&file);
|
||||
|
||||
assert(file.typ == "Application");
|
||||
assert(file.version == "1.0");
|
||||
assert(locale_string_equal(file.name, [
|
||||
(locale::c, "Foo Viewer"),
|
||||
]));
|
||||
assert(locale_string_equal(file.generic_name, [
|
||||
(locale::c, "Foo Viewer"),
|
||||
(locale::parse("xx_XX")!, "Sneep glorp"),
|
||||
]));
|
||||
assert(file.no_display == false);
|
||||
assert(locale_string_equal(file.comment, [
|
||||
(locale::c, "The best viewer for Foo objects available!"),
|
||||
]));
|
||||
assert(locale_string_equal(file.icon, [
|
||||
(locale::c, "fooview"),
|
||||
]));
|
||||
assert(file.hidden == false);
|
||||
assert(strings_equal(file.only_show_in, ["MATE", "KDE"]));
|
||||
assert(strings_equal(file.not_show_in, ["GNOME"]));
|
||||
assert(file.dbus_activatable == false);
|
||||
assert(file.try_exec == "fooview");
|
||||
assert(file.exec == "fooview %F");
|
||||
assert(file.path == "");
|
||||
assert(file.terminal == false);
|
||||
assert(strings_equal(file.mime_type, ["image/x-foo"]));
|
||||
assert(strings_equal(file.categories, ["Graphics", "Utility"]));
|
||||
assert(strings_equal(file.implements, [
|
||||
"com.example.Example",
|
||||
"com.example.OtherExample",
|
||||
]));
|
||||
assert(locale_strings_equal(file.keywords, [
|
||||
(locale::c, ["foo", "image", "view", "viewer"]),
|
||||
]));
|
||||
assert(file.startup_notify == false);
|
||||
assert(file.url == "");
|
||||
assert(file.prefers_non_default_gpu == false);
|
||||
assert(file.single_main_window == false);
|
||||
|
||||
for (let actio .. file.actions) switch (actio.key) {
|
||||
case "Gallery" =>
|
||||
assert(locale_string_equal(actio.name, [
|
||||
(locale::c, "Browse Gallery"),
|
||||
]));
|
||||
assert(locale_string_equal(actio.icon, []));
|
||||
assert(actio.exec == "fooview --gallery");
|
||||
case "Create" =>
|
||||
assert(locale_string_equal(actio.name, [
|
||||
(locale::c, "Create a new Foo!"),
|
||||
(locale::parse("en_US")!, "Create a new Foo!"),
|
||||
(locale::parse("xx_XX")!, "Zweep zoop flooble glorp"),
|
||||
]));
|
||||
assert(locale_string_equal(actio.icon, [
|
||||
(locale::c, "fooview-new"),
|
||||
]));
|
||||
assert(actio.exec == "fooview --create-new");
|
||||
case =>
|
||||
fmt::println("unexpected action", actio.key)!;
|
||||
abort("unexpected action");
|
||||
};
|
||||
};
|
||||
|
||||
fn strings_equal(a: []str, b: []str) bool = {
|
||||
if (len(a) != len(b)) return false;
|
||||
for (let index = 0z; index < len(a); index += 1) {
|
||||
if (a[index] != b[index]) return false;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
fn locale_string_equal(a: locale::string, b: locale::string) bool = {
|
||||
if (len(a) != len(b)) return false;
|
||||
for (let index = 0z; index < len(a); index += 1) {
|
||||
let a = a[index];
|
||||
let b = b[index];
|
||||
if (!locale::equal(a.0, b.0)) return false;
|
||||
if (a.1 != b.1 ) return false;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
fn locale_strings_equal(a: locale::strings, b: locale::strings) bool = {
|
||||
if (len(a) != len(b)) return false;
|
||||
for (let index = 0z; index < len(a); index += 1) {
|
||||
let a = a[index];
|
||||
let b = b[index];
|
||||
if (!locale::equal(a.0, b.0)) return false;
|
||||
if (!strings_equal(a.1, b.1)) return false;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
10
xdg/icon_theme/dirs.ha
Normal file
10
xdg/icon_theme/dirs.ha
Normal file
@@ -0,0 +1,10 @@
|
||||
// Returns the set of directories in which themes should be looked for in order
|
||||
// of preference. It will return $HOME/.icons (for backwards compatibility),
|
||||
// $XDG_DATA_DIRS/icons, and /usr/share/pixmaps. Applications may further add
|
||||
// their own icon directories to this list.
|
||||
//
|
||||
// This memory is statically allocated and must not be free'd. It may be
|
||||
// overwritten later, so use [[strings::dupall]] to extend its lifetime.
|
||||
export fn theme_dirs() ([]str | error) = {
|
||||
// TODO
|
||||
};
|
||||
Reference in New Issue
Block a user