Compare commits
50 Commits
1da2f21763
...
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 | |||
| 6af621213a | |||
| 2537a9b6dc | |||
| 0f5dd78aea | |||
| fc533dbeea | |||
| fc0cab93bc | |||
| 9e4d7e4ae4 | |||
| a43d3bdca5 | |||
| ecbbccbaa6 | |||
| 5ec91331d3 | |||
| bd5b9f5d75 | |||
| 776185e574 | |||
| f401ad26df | |||
| 02cf7ffb05 | |||
| 30926c1f10 | |||
| 24ee999173 | |||
| b42b1b1dd2 | |||
| cabc7c1e19 | |||
| b90f0c5378 | |||
| ca44e2d750 | |||
| 8521320a7c | |||
| 19e6aa5259 | |||
| e8af5d01d7 | |||
| 3e72688127 | |||
| 427d30c255 | |||
| baea275b66 | |||
| 0b464d368c | |||
| 3e3c794a1e |
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.
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
// Represents a file of the generic key/value format used by desktop entries.
|
|
||||||
// Specification: §3
|
|
||||||
type file = struct {
|
|
||||||
preceeding_lines: []line;
|
|
||||||
groups: []group;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A named group of key/value entries.
|
|
||||||
// Specification: §3.2
|
|
||||||
type group = struct {
|
|
||||||
name: str;
|
|
||||||
lines: []line;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A line in the file, which can be a comment (or a blank line), an entry, or
|
|
||||||
// a localized entry.
|
|
||||||
type line = (comment | entry | localized_entry);
|
|
||||||
|
|
||||||
// A comment.
|
|
||||||
// Specification: §3.1
|
|
||||||
type comment = str;
|
|
||||||
|
|
||||||
// A key/value pair.
|
|
||||||
// Specification: §3.3
|
|
||||||
type entry = struct {
|
|
||||||
key: str;
|
|
||||||
value: value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A localized entry.
|
|
||||||
// Specification: §5
|
|
||||||
type localized_entry struct {
|
|
||||||
key: str;
|
|
||||||
value: value;
|
|
||||||
locale: locale::locale;
|
|
||||||
};
|
|
||||||
|
|
||||||
// An entry value. Values that reference memory (such as [[str]]) are borrowed
|
|
||||||
// from the [[file]]. These may be free'd or overwritten when other functions in
|
|
||||||
// this module are called, so [[value_dup]] is required to extend its lifetime.
|
|
||||||
export type value = (str, bool, f32);
|
|
||||||
|
|
||||||
// Gets a non-localized value from a file. If the group does not exist, or the
|
|
||||||
// group exists but the key isn't in it, it returns void.
|
|
||||||
export fn file_get(fil: *file, group_name: str, key: str) (value | void) {
|
|
||||||
let grou = match (file_find_group(fil, group_name)) {
|
|
||||||
case index: size => yield &file.groups[index];
|
|
||||||
case void => return void;
|
|
||||||
};
|
|
||||||
let lin = match (group_find_entry(grou, key)) {
|
|
||||||
case index: size => yield &grou.lines[index];
|
|
||||||
case void => return void;
|
|
||||||
};
|
|
||||||
match (lin) {
|
|
||||||
case entr: entry => return entr.value;
|
|
||||||
case => abort();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gets a localized value from a file. If the group does not exist, or the group
|
|
||||||
// exists but the key isnt in it, it returns void. If the key is in the group
|
|
||||||
// but is not localized to the specified locale, the non-localized value is
|
|
||||||
// returned.
|
|
||||||
export fn file_get_localized(
|
|
||||||
fil: *file,
|
|
||||||
group_name: str,
|
|
||||||
key: str,
|
|
||||||
local: locale,
|
|
||||||
) (value | void) {
|
|
||||||
let grou = match (file_find_group(fil, group_name)) {
|
|
||||||
case index: size => yield &file.groups[index];
|
|
||||||
case void => return void;
|
|
||||||
};
|
|
||||||
let lin = match (group_find_localized_entry(grou, key, local)) {
|
|
||||||
case index: size => yield &grou.lines[index];
|
|
||||||
case void => return void;
|
|
||||||
};
|
|
||||||
match (lin) {
|
|
||||||
case entr: localized_entry => return entr.value;
|
|
||||||
case entr: entry => return entr.value;
|
|
||||||
case => abort();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export fn file_add
|
|
||||||
|
|
||||||
export fn file_add_localized
|
|
||||||
|
|
||||||
export fn file_remove
|
|
||||||
|
|
||||||
export fn file_remove_localized
|
|
||||||
|
|
||||||
export fn file_encode
|
|
||||||
|
|
||||||
export fn file_finish
|
|
||||||
|
|
||||||
fn file_find_group (fil *file, group_name str): (size | void) = {
|
|
||||||
let index = 0;
|
|
||||||
for (let grou .. file.groups) {
|
|
||||||
if (strings::compare(group.name, group_name) == 0) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
index ++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn group_find_entry (grou *group, key str): (size | void) {
|
|
||||||
let index = 0;
|
|
||||||
for (let lin .. grou.lines) {
|
|
||||||
match (lin) {
|
|
||||||
case entr: entry =>
|
|
||||||
if (strings::compare(entry.key, entr) == 0) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
case => void ;
|
|
||||||
}
|
|
||||||
index ++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicates a [[value]]. Use [[value_finish]] to get rid of it.
|
|
||||||
export fn value_dup(valu: value): value = match (valu) {
|
|
||||||
case let valu: str => yield strings.dup(valu);
|
|
||||||
case => yield valu;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Frees an [[entry]] previously duplicated with [[entry_dup]].
|
|
||||||
export fn value_finish(valu: value): void = match (valu) {
|
|
||||||
case let valu: str => free(valu);
|
|
||||||
case => void;
|
|
||||||
};
|
|
||||||
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.
|
||||||
60
format/xdg/ini/error.ha
Normal file
60
format/xdg/ini/error.ha
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use encoding::utf8;
|
||||||
|
use io;
|
||||||
|
|
||||||
|
// Any error that may occur during parsing.
|
||||||
|
export type error = !(syntaxerr | io::error | utf8::invalid);
|
||||||
|
|
||||||
|
// All syntax errors defined in this module.
|
||||||
|
export type syntaxerr = !(
|
||||||
|
invalid_group_header |
|
||||||
|
invalid_entry |
|
||||||
|
invalid_ascii |
|
||||||
|
invalid_escape |
|
||||||
|
invalid_boolean |
|
||||||
|
invalid_numeric |
|
||||||
|
invalid_integer |
|
||||||
|
expected_single);
|
||||||
|
|
||||||
|
// Returned when a malformed group header is encountered while parsing.
|
||||||
|
export type invalid_group_header = !void;
|
||||||
|
|
||||||
|
// Returned when a malformed entry is encountered while parsing.
|
||||||
|
export type invalid_entry = !void;
|
||||||
|
|
||||||
|
// Returned when ASCII text was expected while parsing, but something else was
|
||||||
|
// given.
|
||||||
|
export type invalid_ascii = !void;
|
||||||
|
|
||||||
|
// Returned when an invalid escape sequence was enctountered while parsing.
|
||||||
|
export type invalid_escape = !void;
|
||||||
|
|
||||||
|
// Returned when a boolean value was expected while parsing, but something else
|
||||||
|
// was given.
|
||||||
|
export type invalid_boolean = !void;
|
||||||
|
|
||||||
|
// Returned when a numeric value was expected while parsing, but something else
|
||||||
|
// was given.
|
||||||
|
export type invalid_numeric = !void;
|
||||||
|
|
||||||
|
// Returned when a numeric value was expected while parsing, but something else
|
||||||
|
// was given.
|
||||||
|
export type invalid_integer = !void;
|
||||||
|
|
||||||
|
// Returned when a singular value was expected while parsing, but multiple
|
||||||
|
// values were given.
|
||||||
|
export type expected_single = !void;
|
||||||
|
|
||||||
|
// Returns a user-friendly representation of [[error]]. The result may be
|
||||||
|
// statically allocated.
|
||||||
|
export fn strerror(err: error) str = match (err) {
|
||||||
|
case invalid_group_header => yield "invalid group header";
|
||||||
|
case invalid_entry => yield "invalid entry";
|
||||||
|
case invalid_ascii => yield "invalid ascii";
|
||||||
|
case invalid_escape => yield "invalid escape";
|
||||||
|
case invalid_boolean => yield "invalid boolean";
|
||||||
|
case invalid_numeric => yield "invalid numeric";
|
||||||
|
case invalid_integer => yield "invalid integer";
|
||||||
|
case expected_single => yield "expected single";
|
||||||
|
case let err: io::error => yield io::strerror(err);
|
||||||
|
case let err: utf8::invalid => yield utf8::strerror(err);
|
||||||
|
};
|
||||||
62
format/xdg/ini/line.ha
Normal file
62
format/xdg/ini/line.ha
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use fmt;
|
||||||
|
use io;
|
||||||
|
use locale;
|
||||||
|
use strings;
|
||||||
|
|
||||||
|
// A line in the file, which can be a comment (or a blank line), an entry, or
|
||||||
|
// a localized entry.
|
||||||
|
export type line = (blank | comment | group_header | entry);
|
||||||
|
|
||||||
|
// A blank line.
|
||||||
|
// Specification: §3.1
|
||||||
|
export type blank = void;
|
||||||
|
|
||||||
|
// A comment.
|
||||||
|
// Specification: §3.1
|
||||||
|
export type comment = str;
|
||||||
|
|
||||||
|
// A group header.
|
||||||
|
// Specification: §3.2
|
||||||
|
export type group_header = str;
|
||||||
|
|
||||||
|
// An entry in a desktop file. Entries without an explicitly stated locale are
|
||||||
|
// assigned [[locale::c]].
|
||||||
|
// Specification: §3.3, §5
|
||||||
|
export type entry = struct {
|
||||||
|
group: str,
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
locale: locale::locale,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Duplicates an [[entry]]. Use [[entry_finish]] to get rid of it.
|
||||||
|
export fn entry_dup(entr: entry) entry = entry {
|
||||||
|
group = strings::dup(entr.group),
|
||||||
|
key = strings::dup(entr.key),
|
||||||
|
value = strings::dup(entr.value),
|
||||||
|
locale = locale::dup(entr.locale),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frees memory associated with an [[entry]].
|
||||||
|
export fn entry_finish(entr: entry) void = {
|
||||||
|
free(entr.group);
|
||||||
|
free(entr.key);
|
||||||
|
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;
|
||||||
|
};
|
||||||
133
format/xdg/ini/scan.ha
Normal file
133
format/xdg/ini/scan.ha
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use ascii;
|
||||||
|
use bufio;
|
||||||
|
use encoding::utf8;
|
||||||
|
use errors;
|
||||||
|
use io;
|
||||||
|
use locale;
|
||||||
|
use memio;
|
||||||
|
use strings;
|
||||||
|
|
||||||
|
export type scanner = struct {
|
||||||
|
scanner: bufio::scanner,
|
||||||
|
group: str,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a desktop entry file scanner. Use [[next]] to read lines. The caller
|
||||||
|
// must call [[finish]] once they're done with this object.
|
||||||
|
export fn scan(input: io::handle) scanner = scanner {
|
||||||
|
scanner = bufio::newscanner(input),
|
||||||
|
...
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the next line from a desktop entry file. The return value is
|
||||||
|
// borrowed from the [[scanner]]. Use [[strings::dup]], [[entry_dup]], etc. to
|
||||||
|
// retain a copy. If all you want is the file's data, use [[next_entry]].
|
||||||
|
export fn next(this: *scanner) (line | io::EOF | error) = {
|
||||||
|
let text = match (bufio::scan_line(&this.scanner)?) {
|
||||||
|
case let text: const str => yield text;
|
||||||
|
case io::EOF => return io::EOF;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (text == "") {
|
||||||
|
// blank line
|
||||||
|
return blank;
|
||||||
|
|
||||||
|
} else if (strings::hasprefix(text, '#')) {
|
||||||
|
// comment
|
||||||
|
return strings::ltrim(text, '#'): comment;
|
||||||
|
|
||||||
|
} else if (strings::hasprefix(text, '[')) {
|
||||||
|
// group header
|
||||||
|
let header = parse_group_header(text)?;
|
||||||
|
free(this.group);
|
||||||
|
this.group = strings::dup(header: str);
|
||||||
|
return header;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// key/value pair
|
||||||
|
let entry = parse_entry(text)?;
|
||||||
|
entry.group = this.group;
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the next entry from the desktop file, skipping over blank lines,
|
||||||
|
// comments, group headers, etc. The return value is borrowed from the
|
||||||
|
// [[scanner]]. Use [[entry_dup]] to retain a copy.
|
||||||
|
export fn next_entry(this: *scanner) (entry | io::EOF | error) = {
|
||||||
|
for (true) match(next(this)?) {
|
||||||
|
case let entr: entry => return entr;
|
||||||
|
case io::EOF => return io::EOF;
|
||||||
|
case => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frees resources associated with a [[scanner]].
|
||||||
|
export fn finish(this: *scanner) void = {
|
||||||
|
bufio::finish(&this.scanner);
|
||||||
|
free(this.group);
|
||||||
|
};
|
||||||
|
|
||||||
|
// memory is borrowed from the input
|
||||||
|
fn parse_group_header(text: str) (group_header | error) = {
|
||||||
|
if (!strings::hasprefix(text, '[') || !strings::hassuffix(text, ']')) {
|
||||||
|
return invalid_group_header;
|
||||||
|
};
|
||||||
|
|
||||||
|
text = strings::rtrim(strings::ltrim(text, '['), ']');
|
||||||
|
if (strings::contains(text, '[', ']')) {
|
||||||
|
return invalid_group_header;
|
||||||
|
};
|
||||||
|
return text: group_header;
|
||||||
|
};
|
||||||
|
|
||||||
|
// memory is borrowed from the input
|
||||||
|
fn parse_entry(line: str) (entry | error) = {
|
||||||
|
if (!strings::contains(line, '=')) return invalid_entry;
|
||||||
|
let (key, valu) = strings::cut(line, "=");
|
||||||
|
key = strings::ltrim(strings::rtrim(key));
|
||||||
|
|
||||||
|
let (key, local_string) = strings::cut(key, "[");
|
||||||
|
let local = if (local_string == "") {
|
||||||
|
yield locale::c;
|
||||||
|
} else {
|
||||||
|
local_string = strings::rtrim(local_string, ']');
|
||||||
|
validate_entry_locale(local_string)?;
|
||||||
|
|
||||||
|
yield match(locale::parse(local_string)) {
|
||||||
|
case let local: locale::locale => yield local;
|
||||||
|
case locale::invalid => return invalid_entry;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
local.encoding = "";
|
||||||
|
|
||||||
|
validate_entry_key(key)?;
|
||||||
|
return entry {
|
||||||
|
key = key,
|
||||||
|
value = valu,
|
||||||
|
locale = local,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validate_entry_key(key: str) (void | error) = {
|
||||||
|
for (let byte .. strings::toutf8(key)) {
|
||||||
|
const ok =
|
||||||
|
(byte >= 'A' && byte <= 'Z') ||
|
||||||
|
(byte >= 'a' && byte <= 'z') ||
|
||||||
|
(byte >= '0' && byte <= '9') ||
|
||||||
|
(byte == '-');
|
||||||
|
if (!ok) return invalid_entry;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validate_entry_locale(locale: str) (void | error) = {
|
||||||
|
for (let byte .. strings::toutf8(locale)) {
|
||||||
|
const bad =
|
||||||
|
(byte == '[') ||
|
||||||
|
(byte == ']') ||
|
||||||
|
(byte == '=') ||
|
||||||
|
!ascii::isprint(byte: rune);
|
||||||
|
if (bad) return invalid_entry;
|
||||||
|
};
|
||||||
|
};
|
||||||
146
format/xdg/ini/scan_test.ha
Normal file
146
format/xdg/ini/scan_test.ha
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use fmt;
|
||||||
|
use io;
|
||||||
|
use locale;
|
||||||
|
use os;
|
||||||
|
|
||||||
|
@test fn next() void = {
|
||||||
|
let h_de = "Desktop Entry";
|
||||||
|
let h_dag = "Desktop Action Gallery";
|
||||||
|
let h_dac = "Desktop Action Create";
|
||||||
|
let correct: []line = [
|
||||||
|
"This is a comment": comment,
|
||||||
|
blank,
|
||||||
|
h_de: group_header,
|
||||||
|
entry_new(h_de, "Version", "1.0", locale::c),
|
||||||
|
entry_new(h_de, "Type", "Application", locale::c),
|
||||||
|
entry_new(h_de, "Name", "Foo Viewer", locale::c),
|
||||||
|
entry_new(h_de, "Comment", "The best viewer for Foo objects available!", locale::c),
|
||||||
|
entry_new(h_de, "TryExec", "fooview", locale::c),
|
||||||
|
entry_new(h_de, "Exec", "fooview %F", locale::c),
|
||||||
|
blank,
|
||||||
|
entry_new(h_de, "Icon", "fooview", locale::c),
|
||||||
|
entry_new(h_de, "MimeType", "image/x-foo;", locale::c),
|
||||||
|
entry_new(h_de, "Actions", "Gallery;Create;", locale::c),
|
||||||
|
blank,
|
||||||
|
h_dag: group_header,
|
||||||
|
entry_new(h_dag, "Exec", "fooview --gallery", locale::c),
|
||||||
|
entry_new(h_dag, "Name", "Browse Gallery", locale::c),
|
||||||
|
blank,
|
||||||
|
h_dac: group_header,
|
||||||
|
entry_new(h_dac, "Exec", "fooview --create-new", locale::c),
|
||||||
|
entry_new(h_dac, "Name", "Create a new Foo!", locale::c),
|
||||||
|
entry_new(
|
||||||
|
h_dac, "Name", "Create a new Foo!",
|
||||||
|
locale::parse("en_US")!),
|
||||||
|
entry_new(
|
||||||
|
h_dac, "Name", "Zweep zoop flooble glorp",
|
||||||
|
locale::parse("xx_XX")!),
|
||||||
|
"Another comment": comment,
|
||||||
|
entry_new(h_dac, "Icon", "fooview-new", locale::c),
|
||||||
|
];
|
||||||
|
|
||||||
|
let file = os::open("format/xdg/ini/test_data/foo.desktop")!;
|
||||||
|
defer io::close(file)!;
|
||||||
|
let scanne = scan(file);
|
||||||
|
defer finish(&scanne);
|
||||||
|
|
||||||
|
for (let correct .. correct) match(next(&scanne)!) {
|
||||||
|
case io::EOF =>
|
||||||
|
abort("encountered EOF early");
|
||||||
|
case blank =>
|
||||||
|
assert(correct is blank);
|
||||||
|
case let ln: comment =>
|
||||||
|
assert(ln == correct as comment);
|
||||||
|
case let ln: group_header =>
|
||||||
|
assert(ln == correct as group_header);
|
||||||
|
case let ln: entry =>
|
||||||
|
assert(entry_equal(ln, correct as entry));
|
||||||
|
};
|
||||||
|
|
||||||
|
assert(next(&scanne) is io::EOF);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn next_entry() void = {
|
||||||
|
let h_de = "Desktop Entry";
|
||||||
|
let h_dag = "Desktop Action Gallery";
|
||||||
|
let h_dac = "Desktop Action Create";
|
||||||
|
let correct: []entry = [
|
||||||
|
entry_new(h_de, "Version", "1.0", locale::c),
|
||||||
|
entry_new(h_de, "Type", "Application", locale::c),
|
||||||
|
entry_new(h_de, "Name", "Foo Viewer", locale::c),
|
||||||
|
entry_new(h_de, "Comment", "The best viewer for Foo objects available!", locale::c),
|
||||||
|
entry_new(h_de, "TryExec", "fooview", locale::c),
|
||||||
|
entry_new(h_de, "Exec", "fooview %F", locale::c),
|
||||||
|
entry_new(h_de, "Icon", "fooview", locale::c),
|
||||||
|
entry_new(h_de, "MimeType", "image/x-foo;", locale::c),
|
||||||
|
entry_new(h_de, "Actions", "Gallery;Create;", locale::c),
|
||||||
|
entry_new(h_dag, "Exec", "fooview --gallery", locale::c),
|
||||||
|
entry_new(h_dag, "Name", "Browse Gallery", locale::c),
|
||||||
|
entry_new(h_dac, "Exec", "fooview --create-new", locale::c),
|
||||||
|
entry_new(h_dac, "Name", "Create a new Foo!", locale::c),
|
||||||
|
entry_new(
|
||||||
|
h_dac, "Name", "Create a new Foo!",
|
||||||
|
locale::parse("en_US")!),
|
||||||
|
entry_new(
|
||||||
|
h_dac, "Name", "Zweep zoop flooble glorp",
|
||||||
|
locale::parse("xx_XX")!),
|
||||||
|
entry_new(h_dac, "Icon", "fooview-new", locale::c),
|
||||||
|
];
|
||||||
|
|
||||||
|
let file = os::open("format/xdg/ini/test_data/foo.desktop")!;
|
||||||
|
defer io::close(file)!;
|
||||||
|
let scanne = scan(file);
|
||||||
|
defer finish(&scanne);
|
||||||
|
|
||||||
|
for (let correct .. correct) {
|
||||||
|
assert(entry_equal(next_entry(&scanne)! as entry, correct));
|
||||||
|
};
|
||||||
|
|
||||||
|
assert(next(&scanne) is io::EOF);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_entry() void = {
|
||||||
|
assert(entry_equal(parse_entry("hello=world")!, entry {
|
||||||
|
key = "hello",
|
||||||
|
value = "world",
|
||||||
|
locale = locale::c,
|
||||||
|
...
|
||||||
|
}));
|
||||||
|
assert(entry_equal(parse_entry("hello[sr_YU.UTF-8@Latn]=world")!, entry {
|
||||||
|
key = "hello",
|
||||||
|
value = "world",
|
||||||
|
locale = locale::parse("sr_YU@Latn")!,
|
||||||
|
...
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn validate_entry_key() void = {
|
||||||
|
// §3.3 Only the characters A-Za-z0-9- may be used in key names.
|
||||||
|
assert(validate_entry_key("SomeName") is void);
|
||||||
|
assert(validate_entry_key("some-name") is void);
|
||||||
|
assert(validate_entry_key("1234567890some-0987654321naMe") is void);
|
||||||
|
assert(validate_entry_key("^&(*)[") is invalid_entry);
|
||||||
|
assert(validate_entry_key("]") is invalid_entry);
|
||||||
|
assert(validate_entry_key("&*%^") is invalid_entry);
|
||||||
|
assert(validate_entry_key("asj=hd@#*()") is invalid_entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn validate_entry_locale() void = {
|
||||||
|
assert(validate_entry_locale("sr_YU.UTF-8@Latn") is void);
|
||||||
|
assert(validate_entry_locale("abcd[") is invalid_entry);
|
||||||
|
assert(validate_entry_locale("ab]cd") is invalid_entry);
|
||||||
|
assert(validate_entry_locale("a=bcd") is invalid_entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn entry_equal(a: entry, b: entry) bool =
|
||||||
|
a.group == b.group &&
|
||||||
|
a.key == b.key &&
|
||||||
|
a.value == b.value &&
|
||||||
|
locale::equal(a.locale, b.locale);
|
||||||
|
|
||||||
|
fn entry_new(group: str, key: str, value: str, local: locale::locale) entry = entry {
|
||||||
|
group = group,
|
||||||
|
key = key,
|
||||||
|
value = value,
|
||||||
|
locale = local,
|
||||||
|
};
|
||||||
25
format/xdg/ini/test_data/foo.desktop
Normal file
25
format/xdg/ini/test_data/foo.desktop
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#This is a comment
|
||||||
|
|
||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=Foo Viewer
|
||||||
|
Comment=The best viewer for Foo objects available!
|
||||||
|
TryExec=fooview
|
||||||
|
Exec=fooview %F
|
||||||
|
|
||||||
|
Icon=fooview
|
||||||
|
MimeType=image/x-foo;
|
||||||
|
Actions=Gallery;Create;
|
||||||
|
|
||||||
|
[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
|
||||||
|
#Another comment
|
||||||
|
Icon=fooview-new
|
||||||
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
|
||||||
207
format/xdg/ini/value.ha
Normal file
207
format/xdg/ini/value.ha
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
use ascii;
|
||||||
|
use strconv;
|
||||||
|
use strings;
|
||||||
|
|
||||||
|
// Parses a string value. It may contain all ASCII characters except for control
|
||||||
|
// characters. The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_string(in: str) (str | error) = {
|
||||||
|
for (let char .. strings::toutf8(in)) {
|
||||||
|
if (!ascii::valid(char: rune)) return invalid_ascii;
|
||||||
|
};
|
||||||
|
return parse_localestring(in);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses a localestring value. It is user displayable, and is encoded in UTF-8.
|
||||||
|
// The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_localestring(in: str) (str | error) = {
|
||||||
|
let escaper = escape_string(in);
|
||||||
|
let string = match(string_escaper_next(&escaper)?) {
|
||||||
|
case let string: str => yield string;
|
||||||
|
case done => return "";
|
||||||
|
};
|
||||||
|
if (len(escaper) > 0) {
|
||||||
|
free(string);
|
||||||
|
return expected_single;
|
||||||
|
};
|
||||||
|
return string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses an iconstring value. It is the name of an icon; it may be an absolute
|
||||||
|
// path, or a symbolic name for an icon located using the algorithm described in
|
||||||
|
// the Icon Theme Specification. Such values are not user-displayable, and are
|
||||||
|
// encoded in UTF-8. The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_iconstring(in: str) (str | error) = parse_localestring(in);
|
||||||
|
|
||||||
|
// Parses a boolean value. It must either be the string "true" or "false".
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_boolean(in: str) (bool | error) = {
|
||||||
|
if (strings::contains(in, ";")) return expected_single;
|
||||||
|
if (in == "true" ) return true;
|
||||||
|
if (in == "false") return false;
|
||||||
|
return invalid_boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses a numeric value. It must be a valid floating point number as
|
||||||
|
// recognized by the %f specifier for scanf in the C locale.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_numeric(in: str) (f32 | error) = match (strconv::stof32(in)) {
|
||||||
|
case let float: f32 => yield float;
|
||||||
|
case =>
|
||||||
|
if (strings::contains(in, ";")) return expected_single;
|
||||||
|
yield invalid_numeric;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses an integer value. While there is no documentation on this data type,
|
||||||
|
// it is referred to by the Icon Theme Specification, §4, tables 2-3.
|
||||||
|
export fn parse_integer(in: str) (int | error) = match(strconv::stoi(in)) {
|
||||||
|
case let integer: int => yield integer;
|
||||||
|
case =>
|
||||||
|
if (strings::contains(in, ";")) return expected_single;
|
||||||
|
yield invalid_integer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses multiple string values. See [[parse_string]] for more information. The
|
||||||
|
// memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_strings(in: str) ([]str | error) = {
|
||||||
|
for (let char .. strings::toutf8(in)) {
|
||||||
|
if (!ascii::valid(char: rune)) return invalid_ascii;
|
||||||
|
};
|
||||||
|
return parse_localestrings(in);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses multiple localestring values. See [[parse_localestring]] for more
|
||||||
|
// information. The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_localestrings(in: str) ([]str | error) = {
|
||||||
|
let escaper = escape_string(in);
|
||||||
|
let result: []str = alloc([], 0);
|
||||||
|
for (let in => string_escaper_next(&escaper)) {
|
||||||
|
match (in) {
|
||||||
|
case let string: str =>
|
||||||
|
append(result, string);
|
||||||
|
case let err: error =>
|
||||||
|
strings::freeall(result);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses multiple iconstring values. See [[parse_iconstring]] for more
|
||||||
|
// information. The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_iconstrings(in: str) ([]str | error) = parse_localestrings(in);
|
||||||
|
|
||||||
|
// Parses multiple boolean values. See [[parse_boolean]] for more information.
|
||||||
|
// The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_booleans(in: str) ([]bool | error) = {
|
||||||
|
let splitte = split(in);
|
||||||
|
let result: []bool = alloc([], 0);
|
||||||
|
for (let in => splitter_next(&splitte)) {
|
||||||
|
match (parse_boolean(in)) {
|
||||||
|
case let boolean: bool =>
|
||||||
|
append(result, boolean);
|
||||||
|
case let err: error =>
|
||||||
|
free(result);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses multiple numeric values. See [[parse_numeric]] for more information.
|
||||||
|
// The memory must be freed by the caller.
|
||||||
|
// Specification: §4
|
||||||
|
export fn parse_numerics(in: str) ([]f32 | error) = {
|
||||||
|
let splitte = split(in);
|
||||||
|
let result: []f32 = alloc([], 0);
|
||||||
|
for (let in => splitter_next(&splitte)) {
|
||||||
|
match (parse_numeric(in)) {
|
||||||
|
case let number: f32 =>
|
||||||
|
append(result, number);
|
||||||
|
case let err: error =>
|
||||||
|
free(result);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses multiple integer values. See [[parse_integer]] for more information.
|
||||||
|
// The memory must be freed by the caller.
|
||||||
|
export fn parse_integers(in: str) ([]int | error) = {
|
||||||
|
let splitte = split(in);
|
||||||
|
let result: []int = alloc([], 0);
|
||||||
|
for (let in => splitter_next(&splitte)) {
|
||||||
|
match (parse_integer(in)) {
|
||||||
|
case let number: int =>
|
||||||
|
append(result, number);
|
||||||
|
case let err: error =>
|
||||||
|
free(result);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
type string_escaper = []u8;
|
||||||
|
|
||||||
|
fn escape_string(in: str) string_escaper = strings::toutf8(in): string_escaper;
|
||||||
|
|
||||||
|
fn string_escaper_next(this: *string_escaper) (str | done | error) = {
|
||||||
|
if (len(*this) < 1) return done;
|
||||||
|
let result: []u8 = alloc([], 0);
|
||||||
|
let end = 0z;
|
||||||
|
let saw_escape = false;
|
||||||
|
for (let byte .. *this) {
|
||||||
|
end += 1;
|
||||||
|
if (saw_escape) {
|
||||||
|
switch (byte) {
|
||||||
|
case ';' => append(result, ';');
|
||||||
|
case 's' => append(result, ' ');
|
||||||
|
case 'n' => append(result, '\n');
|
||||||
|
case 't' => append(result, '\t');
|
||||||
|
case 'r' => append(result, '\r');
|
||||||
|
case '\\' => append(result, '\\');
|
||||||
|
case =>
|
||||||
|
free(result);
|
||||||
|
return invalid_escape;
|
||||||
|
};
|
||||||
|
saw_escape = false;
|
||||||
|
} else {
|
||||||
|
switch (byte) {
|
||||||
|
case ';' => break;
|
||||||
|
case '\\' => saw_escape = true;
|
||||||
|
case => append(result, byte);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
*this = (*this)[end..];
|
||||||
|
return strings::fromutf8_unsafe(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
type splitter = []u8;
|
||||||
|
|
||||||
|
fn split(in: str) splitter = strings::toutf8(in): splitter;
|
||||||
|
|
||||||
|
fn splitter_next(this: *splitter) (str | done) = {
|
||||||
|
if (len(*this) < 1) return done;
|
||||||
|
let end = 0z;
|
||||||
|
let knife = 0z;
|
||||||
|
for (let byte .. *this) {
|
||||||
|
knife += 1;
|
||||||
|
if (byte == ';') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
end += 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let result = this[..end];
|
||||||
|
*this = (*this)[knife..];
|
||||||
|
return strings::fromutf8_unsafe(result);
|
||||||
|
};
|
||||||
232
format/xdg/ini/value_test.ha
Normal file
232
format/xdg/ini/value_test.ha
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
use fmt;
|
||||||
|
use math;
|
||||||
|
use strings;
|
||||||
|
|
||||||
|
@test fn parse_string() void = {
|
||||||
|
assert(parse_string("hello")! == "hello");
|
||||||
|
assert(parse_string("hel\\s\\n\\t\\r\\\\\\;lo")! == "hel \n\t\r\\;lo");
|
||||||
|
assert(parse_string("hello☠") is invalid_ascii);
|
||||||
|
assert(parse_string("hello;world") is expected_single);
|
||||||
|
assert(parse_string("hello\\d") is invalid_escape);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_localestring() void = {
|
||||||
|
assert(parse_localestring("hello")! == "hello");
|
||||||
|
assert(parse_localestring("hel\\s\\n\\t\\r\\\\\\;lo")! == "hel \n\t\r\\;lo");
|
||||||
|
assert(parse_localestring("hello☠")! == "hello☠");
|
||||||
|
assert(parse_localestring("hello;world") is expected_single);
|
||||||
|
assert(parse_localestring("hello\\d") is invalid_escape);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_iconstring() void = {
|
||||||
|
assert(parse_iconstring("hello")! == "hello");
|
||||||
|
assert(parse_iconstring("hel\\s\\n\\t\\r\\\\\\;lo")! == "hel \n\t\r\\;lo");
|
||||||
|
assert(parse_iconstring("hello☠")! == "hello☠");
|
||||||
|
assert(parse_iconstring("hello;world") is expected_single);
|
||||||
|
assert(parse_iconstring("hello\\d") is invalid_escape);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_boolean() void = {
|
||||||
|
assert(parse_boolean("true")! == true);
|
||||||
|
assert(parse_boolean("false")! == false);
|
||||||
|
assert(parse_boolean("hello") is invalid_boolean);
|
||||||
|
assert(parse_boolean("ttrue") is invalid_boolean);
|
||||||
|
assert(parse_boolean("falsee") is invalid_boolean);
|
||||||
|
assert(parse_boolean("") is invalid_boolean);
|
||||||
|
assert(parse_boolean("1") is invalid_boolean);
|
||||||
|
assert(parse_boolean("0") is invalid_boolean);
|
||||||
|
assert(parse_boolean("true;false") is expected_single);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_numeric() void = {
|
||||||
|
assert(parse_numeric("9")! == 9.0f32);
|
||||||
|
assert(parse_numeric("9.0")! == 9.0f32);
|
||||||
|
assert(parse_numeric("34.93")! == 34.93f32);
|
||||||
|
assert(parse_numeric("-100.895")! == -100.895f32);
|
||||||
|
assert(math::isnan(parse_numeric("NaN")!));
|
||||||
|
assert(parse_numeric("Infinity")! == math::INF);
|
||||||
|
assert(parse_numeric("hello") is invalid_numeric);
|
||||||
|
assert(parse_numeric("--") is invalid_numeric);
|
||||||
|
assert(parse_numeric("....") is invalid_numeric);
|
||||||
|
assert(parse_numeric("234;7.4") is expected_single);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_integer() void = {
|
||||||
|
assert(parse_integer("9")! == 9);
|
||||||
|
assert(parse_integer("2348")! == 2348);
|
||||||
|
assert(parse_integer("-324")! == -324);
|
||||||
|
assert(parse_integer("324.9") is invalid_integer);
|
||||||
|
assert(parse_integer("324.0") is invalid_integer);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_strings() void = {
|
||||||
|
let correct: []str = [
|
||||||
|
"b\r\tird",
|
||||||
|
"wa;ter",
|
||||||
|
"",
|
||||||
|
"riv\ner",
|
||||||
|
"nuh uh",
|
||||||
|
];
|
||||||
|
let got = parse_strings("b\\r\\tird;wa\\;ter;;riv\\ner;nuh uh")!;
|
||||||
|
defer strings::freeall(got);
|
||||||
|
for (let index = 0z; index < len(correct); index += 1) {
|
||||||
|
assert(index < len(got), "ran out");
|
||||||
|
assert(compare_strings(correct[index], got[index]));
|
||||||
|
};
|
||||||
|
assert(len(got) == len(correct), "not done");
|
||||||
|
assert(parse_strings("hello☠;world") is invalid_ascii);
|
||||||
|
assert(parse_strings("hello\\d;world") is invalid_escape);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_localestrings() void = {
|
||||||
|
let correct: []str = [
|
||||||
|
"b\r\tir☠d",
|
||||||
|
"wa;ter",
|
||||||
|
"",
|
||||||
|
"ri☠v\ner",
|
||||||
|
"nuh uh",
|
||||||
|
];
|
||||||
|
let got = parse_localestrings("b\\r\\tir☠d;wa\\;ter;;ri☠v\\ner;nuh uh")!;
|
||||||
|
defer strings::freeall(got);
|
||||||
|
for (let index = 0z; index < len(correct); index += 1) {
|
||||||
|
assert(index < len(got), "ran out");
|
||||||
|
assert(compare_strings(correct[index], got[index]));
|
||||||
|
};
|
||||||
|
assert(len(got) == len(correct), "not done");
|
||||||
|
assert(parse_strings("hello\\d;world") is invalid_escape);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_booleans() void = {
|
||||||
|
let correct: []bool = [
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
];
|
||||||
|
let got = parse_booleans("true;true;false;true;false;true")!;
|
||||||
|
defer free(got);
|
||||||
|
for (let index = 0z; index < len(correct); index += 1) {
|
||||||
|
assert(index < len(got), "ran out");
|
||||||
|
let correct = correct[index];
|
||||||
|
let got = got[index];
|
||||||
|
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||||
|
assert(correct == got);
|
||||||
|
};
|
||||||
|
assert(len(got) == len(correct), "not done");
|
||||||
|
assert(parse_booleans("hello;world") is invalid_boolean);
|
||||||
|
assert(parse_booleans("true;;") is invalid_boolean);
|
||||||
|
assert(parse_booleans(";false;") is invalid_boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_numerics() void = {
|
||||||
|
let correct: []f64 = [
|
||||||
|
5.0f32,
|
||||||
|
34.9f32,
|
||||||
|
29.0f32,
|
||||||
|
32498.23784f32,
|
||||||
|
];
|
||||||
|
let got = parse_numerics("5;34.9;29;32498.23784;")!;
|
||||||
|
defer free(got);
|
||||||
|
for (let index = 0z; index < len(correct); index += 1) {
|
||||||
|
assert(index < len(got), "ran out");
|
||||||
|
let correct = correct[index];
|
||||||
|
let got = got[index];
|
||||||
|
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||||
|
assert(correct == got);
|
||||||
|
};
|
||||||
|
assert(len(got) == len(correct), "not done");
|
||||||
|
assert(parse_numerics("hello;world") is invalid_numeric);
|
||||||
|
assert(parse_numerics("5;;") is invalid_numeric);
|
||||||
|
assert(parse_numerics(";5;") is invalid_numeric);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn parse_integers() void = {
|
||||||
|
let correct: []int = [
|
||||||
|
5,
|
||||||
|
34,
|
||||||
|
-29,
|
||||||
|
32498,
|
||||||
|
];
|
||||||
|
let got = parse_integers("5;34;-29;32498")!;
|
||||||
|
defer free(got);
|
||||||
|
for (let index = 0z; index < len(correct); index += 1) {
|
||||||
|
assert(index < len(got), "ran out");
|
||||||
|
let correct = correct[index];
|
||||||
|
let got = got[index];
|
||||||
|
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||||
|
assert(correct == got);
|
||||||
|
};
|
||||||
|
assert(len(got) == len(correct), "not done");
|
||||||
|
assert(parse_integers("hello;world") is invalid_integer);
|
||||||
|
assert(parse_integers("5;;") is invalid_integer);
|
||||||
|
assert(parse_integers(";5;") is invalid_integer);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn string_escaper_next_a() void = {
|
||||||
|
let escaper = escape_string("bird;water;;river;");
|
||||||
|
let correct: []str = [
|
||||||
|
"bird",
|
||||||
|
"water",
|
||||||
|
"",
|
||||||
|
"river",
|
||||||
|
];
|
||||||
|
for (let correct .. correct) {
|
||||||
|
let got = string_escaper_next(&escaper) as str;
|
||||||
|
assert(compare_strings(correct, got));
|
||||||
|
};
|
||||||
|
assert(string_escaper_next(&escaper) is done, "not done");
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn string_escaper_next_b() void = {
|
||||||
|
let escaper = escape_string("b\\r\\tird;wa\\;ter;;riv\\ner;nuh uh");
|
||||||
|
let correct: []str = [
|
||||||
|
"b\r\tird",
|
||||||
|
"wa;ter",
|
||||||
|
"",
|
||||||
|
"riv\ner",
|
||||||
|
"nuh uh",
|
||||||
|
];
|
||||||
|
for (let correct .. correct) {
|
||||||
|
let got = string_escaper_next(&escaper) as str;
|
||||||
|
assert(compare_strings(correct, got));
|
||||||
|
};
|
||||||
|
assert(string_escaper_next(&escaper) is done, "not done");
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn splitter_next_a() void = {
|
||||||
|
let splitte = split("bird;water;;river;");
|
||||||
|
let correct: []str = [
|
||||||
|
"bird",
|
||||||
|
"water",
|
||||||
|
"",
|
||||||
|
"river",
|
||||||
|
];
|
||||||
|
for (let correct .. correct) {
|
||||||
|
let got = splitter_next(&splitte) as str;
|
||||||
|
assert(compare_strings(correct, got));
|
||||||
|
};
|
||||||
|
assert(splitter_next(&splitte) is done, "not done");
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn splitter_next_b() void = {
|
||||||
|
let splitte = split("bird;water;;river");
|
||||||
|
let correct: []str = [
|
||||||
|
"bird",
|
||||||
|
"water",
|
||||||
|
"",
|
||||||
|
"river",
|
||||||
|
];
|
||||||
|
for (let correct .. correct) {
|
||||||
|
let got = splitter_next(&splitte) as str;
|
||||||
|
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||||
|
assert(compare_strings(correct, got));
|
||||||
|
};
|
||||||
|
assert(splitter_next(&splitte) is done, "not done");
|
||||||
|
};
|
||||||
|
|
||||||
|
fn compare_strings(a: str, b: str) bool = {
|
||||||
|
fmt::printf("[{}]\t[{}]\n", a, b)!;
|
||||||
|
return a == b;
|
||||||
|
};
|
||||||
@@ -39,38 +39,42 @@ export fn get_time() locale = get_locale("LC_TIME");
|
|||||||
export fn get_messages() locale = get_locale("LC_MESSAGES");
|
export fn get_messages() locale = get_locale("LC_MESSAGES");
|
||||||
|
|
||||||
fn get_locale(var: str) locale =
|
fn get_locale(var: str) locale =
|
||||||
match (get_locale_no_fallback(var)) {
|
match (get_locale_no_fallback("LC_ALL")) {
|
||||||
case let local: locale => yield local;
|
case let local: locale => yield local;
|
||||||
case =>
|
case =>
|
||||||
yield match (get_locale_no_fallback("LANG")) {
|
yield match (get_locale_no_fallback(var)) {
|
||||||
case let local: locale => yield local;
|
case let local: locale => yield local;
|
||||||
case => yield c;
|
case =>
|
||||||
|
yield match (get_locale_no_fallback("LANG")) {
|
||||||
|
case let local: locale => yield local;
|
||||||
|
case => yield c;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fn get_locale_no_fallback(var: str) (locale | errors::invalid) =
|
fn get_locale_no_fallback(var: str) (locale | invalid) =
|
||||||
match (get_env_locale(var)) {
|
match (get_env_locale(var)) {
|
||||||
case let local: locale => yield local;
|
case let local: locale => yield local;
|
||||||
case => yield (get_locale_conf_entry(var));
|
case => yield (get_locale_conf_entry(var));
|
||||||
};
|
};
|
||||||
|
|
||||||
fn get_env_locale(var: str) (locale | errors::invalid) =
|
fn get_env_locale(var: str) (locale | invalid) =
|
||||||
match (os::getenv(var)) {
|
match (os::getenv(var)) {
|
||||||
case let env: str => return parse(env);
|
case let env: str => return parse(env);
|
||||||
case => return errors::invalid;
|
case => return invalid;
|
||||||
};
|
};
|
||||||
|
|
||||||
let locale_conf: []str = [];
|
let locale_conf: []str = [];
|
||||||
|
|
||||||
@fini fn locale_conf() void = strings::freeall(locale_conf);
|
@fini fn locale_conf() void = strings::freeall(locale_conf);
|
||||||
|
|
||||||
fn get_locale_conf_entry(var: str) (locale | errors::invalid) = {
|
fn get_locale_conf_entry(var: str) (locale | invalid) = {
|
||||||
get_locale_conf();
|
get_locale_conf();
|
||||||
for (let entry .. locale_conf) {
|
for (let entry .. locale_conf) {
|
||||||
let (key, value) = strings::cut(entry, "=");
|
let (key, value) = strings::cut(entry, "=");
|
||||||
if (key == var) return parse(value);
|
if (key == var) return parse(value);
|
||||||
};
|
};
|
||||||
return errors::invalid;
|
return invalid;
|
||||||
};
|
};
|
||||||
|
|
||||||
fn get_locale_conf() []str = {
|
fn get_locale_conf() []str = {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use errors;
|
|
||||||
|
|
||||||
// Returns the locale to use for character classification and case conversion.
|
// Returns the locale to use for character classification and case conversion.
|
||||||
// The memory is statically allocated and must not be free'd. It may be
|
// The memory is statically allocated and must not be free'd. It may be
|
||||||
// overwritten later, so use [[dup]] to extend its lifetime.
|
// overwritten later, so use [[dup]] to extend its lifetime.
|
||||||
|
|||||||
7
locale/error.ha
Normal file
7
locale/error.ha
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// All errors defined in this module.
|
||||||
|
export type error = !invalid;
|
||||||
|
|
||||||
|
// Returned when a malformed locale is encountered.
|
||||||
|
export type invalid = !void;
|
||||||
|
|
||||||
|
export fn strerror(err: error) str = "invalid locale";
|
||||||
@@ -21,16 +21,16 @@ export def c = locale {
|
|||||||
// lang_COUNTRY.ENCODING@MODIFIER
|
// lang_COUNTRY.ENCODING@MODIFIER
|
||||||
//
|
//
|
||||||
// Where _COUNTRY, .ENCODING, and @MODIFIER may be omitted. The function
|
// Where _COUNTRY, .ENCODING, and @MODIFIER may be omitted. The function
|
||||||
// returns a [[locale]], or [[errors::invalid]] if the input cannot be parsed.
|
// returns a [[locale]], or [[invalid]] if the input cannot be parsed. All
|
||||||
// All memory is borrowed from the input, so [[finish]] should not be used to
|
// memory is borrowed from the input, so [[finish]] should not be used to free
|
||||||
// free it.
|
// it.
|
||||||
export fn parse(in: str) (locale | errors::invalid) = {
|
export fn parse(in: str) (locale | invalid) = {
|
||||||
let (in, modifier) = strings::rcut(in, "@");
|
let (in, modifier) = strings::rcut(in, "@");
|
||||||
if (strings::compare(in, "") == 0) return void: errors::invalid;
|
if (strings::compare(in, "") == 0) return void: invalid;
|
||||||
let (in, encoding) = strings::rcut(in, ".");
|
let (in, encoding) = strings::rcut(in, ".");
|
||||||
if (strings::compare(in, "") == 0) return void: errors::invalid;
|
if (strings::compare(in, "") == 0) return void: invalid;
|
||||||
let (in, country) = strings::rcut(in, "_");
|
let (in, country) = strings::rcut(in, "_");
|
||||||
if (strings::compare(in, "") == 0) return void: errors::invalid;
|
if (strings::compare(in, "") == 0) return void: invalid;
|
||||||
return locale {
|
return locale {
|
||||||
lang = in,
|
lang = in,
|
||||||
country = country,
|
country = country,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use fmt;
|
use fmt;
|
||||||
use errors;
|
|
||||||
use strings;
|
use strings;
|
||||||
|
|
||||||
@test fn parse_full() void = {
|
@test fn parse_full() void = {
|
||||||
@@ -64,8 +63,8 @@ use strings;
|
|||||||
|
|
||||||
@test fn parse_error() void = {
|
@test fn parse_error() void = {
|
||||||
let local = match(parse("_COUNTRY.ENCODING@MODIFIER")) {
|
let local = match(parse("_COUNTRY.ENCODING@MODIFIER")) {
|
||||||
case errors::invalid => void;
|
case invalid => void;
|
||||||
case => abort("error");
|
case => abort("no error");
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
133
locale/string.ha
Normal file
133
locale/string.ha
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// A string localized into multiple locales.
|
||||||
|
export type string = [](locale, str);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------- //
|
||||||
|
// 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 result: str => return result;
|
||||||
|
case => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// lang_COUNTRY
|
||||||
|
let lang_country = local;
|
||||||
|
lang_country.modifier = "";
|
||||||
|
match (string_resolve_exact(strin, lang_country)) {
|
||||||
|
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 result: str => return result;
|
||||||
|
case => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// lang
|
||||||
|
let lang = lang_modifier;
|
||||||
|
lang.modifier = "";
|
||||||
|
match (string_resolve_exact(strin, lang)) {
|
||||||
|
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) (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