Compare commits

...

23 Commits

Author SHA1 Message Date
5d061e74ea xdg::icon_theme: Add stub 2024-10-22 18:52:22 -04:00
b95829bd05 xdg::basedir: Add stub 2024-10-22 18:52:08 -04:00
2644932686 cmd::desktop_entry: Fix references to other modules 2024-10-22 18:07:53 -04:00
21bc239cef xdg::desktop_entry: Fix references to format::xdg::ini 2024-10-22 18:05:05 -04:00
fdcef60130 format::xdg::ini: Fix tests 2024-10-22 18:03:52 -04:00
4861950176 Move format::desktop_entry to format::xdg::ini 2024-10-22 18:00:11 -04:00
9ede109998 Update README for format::desktop_entry, xdg::desktop_entry 2024-10-22 17:53:30 -04:00
69eaf63fe8 Moved some functionality from format::desktop_entry to xdg::desktop_entry 2024-10-22 17:49:28 -04:00
af3a694024 format::desktop_entry: Strip encoding from localized entries 2024-10-22 17:27:14 -04:00
24b58f584f locale: Fixed doc comment for string_resolve 2024-10-22 17:23:50 -04:00
2c7e8ef76d locale: Take encoding into account when resolving strings 2024-10-22 17:21:02 -04:00
1d3cdbe6a1 cmd::desktop_entry: Print out all recognized keys 2024-10-22 17:08:08 -04:00
fa4c34bd53 cmd::desktop_entry: Some TODOs 2024-10-22 15:52:07 -04:00
ba330e9e0d cmd::desktop_entry Add beginnings of desktop entry example 2024-10-22 15:50:52 -04:00
6ff4624dd8 format::desktop_entry: Test all recognized keys 2024-10-22 13:39:40 -04:00
2033df5335 format::desktop_entry: Add tests for file parsing 2024-10-22 13:17:21 -04:00
0bfa8f6914 format::desktop_entry: Line formatting now compiles 2024-10-22 13:17:02 -04:00
34f1864d5f format::desktop_entry: Add untested desktop action parsing 2024-10-22 12:59:19 -04:00
0362ae4515 format::desktop_entry: Add desktop entry file parsing 2024-10-22 01:10:05 -04:00
4f1b87181d format::desktop_entry: Add untested formatting function 2024-10-22 01:07:26 -04:00
33a1d1bac1 locale: Add support for localized string list 2024-10-21 21:13:49 -04:00
1055173246 locale: Fix string_resolve 2024-10-21 20:00:24 -04:00
3f88e96395 locale: Test string_resolve 2024-10-21 20:00:09 -04:00
19 changed files with 815 additions and 42 deletions

158
cmd/desktop_entry/main.ha Normal file
View 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)?;
};

View File

@@ -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
View 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.

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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")!,
...
}));
};

View 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

View File

@@ -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.
// 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.
// 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
View 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
View 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
View 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
View 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]].

View 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);
};

View 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
View 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
};