Compare commits

..

50 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
6af621213a locale: Add localized string, untested resolution function 2024-10-21 19:43:07 -04:00
2537a9b6dc format::desktop_entry: Rename file with line type definitions 2024-10-21 17:50:59 -04:00
0f5dd78aea format::desktop_entry: Numerics are f32 now
The specification defines numerics to be anything accepted by the
%f specifier for scanf, which implies a 32 bit float.
2024-10-21 17:44:43 -04:00
fc533dbeea format::desktop_entry: Test integer parsing functions 2024-10-21 17:41:31 -04:00
fc0cab93bc format::desktop_entry: Add untested parsing for integer values 2024-10-21 17:36:40 -04:00
9e4d7e4ae4 format::desktop_entry: Add more test cases 2024-10-21 17:20:51 -04:00
a43d3bdca5 format::desktop_entry: Test most value parsing functions 2024-10-21 16:51:52 -04:00
ecbbccbaa6 Add untested value parsing 2024-10-21 12:48:21 -04:00
5ec91331d3 format::desktop_entry: Clean up next 2024-10-21 12:40:31 -04:00
bd5b9f5d75 format::desktop_entry: Fix test data 2024-10-21 12:39:37 -04:00
776185e574 format::desktop_entry: Add new errors 2024-10-21 11:38:50 -04:00
f401ad26df Fix bugs in format::desktop_entry 2024-10-20 18:40:35 -04:00
02cf7ffb05 Add tests for format::desktop_entry 2024-10-20 18:40:24 -04:00
30926c1f10 Improve doc comment on format::desktop_entry::strerror 2024-10-20 13:39:11 -04:00
24ee999173 Fix doc comment on format::desktop_entry::next 2024-10-20 13:03:19 -04:00
b42b1b1dd2 Add next_entry 2024-10-19 16:23:13 -04:00
cabc7c1e19 Made the desktop entry format more... convenient to work with 2024-10-19 16:15:23 -04:00
b90f0c5378 Renamed parse file to scan file 2024-10-19 15:28:04 -04:00
ca44e2d750 Make desktop_entry module more similar to ini module 2024-10-19 15:23:28 -04:00
8521320a7c Locale defines its own invalid error 2024-10-12 23:51:52 -04:00
19e6aa5259 Desktop entry module compiles 2024-10-12 23:43:07 -04:00
e8af5d01d7 Desktop entry module almost compiles 2024-10-12 23:34:57 -04:00
3e72688127 Better errors 2024-10-12 23:34:44 -04:00
427d30c255 Add parsing stub 2024-10-12 22:12:43 -04:00
baea275b66 Better doc comments for desktop_entry 2024-10-05 00:51:10 -04:00
0b464d368c Implement querying of localized values of desktop files 2024-10-05 00:40:43 -04:00
3e3c794a1e Support LC_ALL 2024-10-04 23:57:12 -04:00
25 changed files with 1718 additions and 163 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.

View File

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

60
format/xdg/ini/error.ha Normal file
View 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
View 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
View 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
View 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,
};

View 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

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

207
format/xdg/ini/value.ha Normal file
View 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);
};

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

View File

@@ -39,7 +39,10 @@ export fn get_time() locale = get_locale("LC_TIME");
export fn get_messages() locale = get_locale("LC_MESSAGES");
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 =>
yield match (get_locale_no_fallback(var)) {
case let local: locale => yield local;
case =>
yield match (get_locale_no_fallback("LANG")) {
@@ -47,30 +50,31 @@ fn get_locale(var: str) locale =
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)) {
case let local: locale => yield local;
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)) {
case let env: str => return parse(env);
case => return errors::invalid;
case => return invalid;
};
let locale_conf: []str = [];
@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();
for (let entry .. locale_conf) {
let (key, value) = strings::cut(entry, "=");
if (key == var) return parse(value);
};
return errors::invalid;
return invalid;
};
fn get_locale_conf() []str = {

View File

@@ -1,5 +1,3 @@
use errors;
// 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
// overwritten later, so use [[dup]] to extend its lifetime.

7
locale/error.ha Normal file
View 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";

View File

@@ -21,16 +21,16 @@ export def c = locale {
// lang_COUNTRY.ENCODING@MODIFIER
//
// Where _COUNTRY, .ENCODING, and @MODIFIER may be omitted. The function
// returns a [[locale]], or [[errors::invalid]] if the input cannot be parsed.
// All memory is borrowed from the input, so [[finish]] should not be used to
// free it.
export fn parse(in: str) (locale | errors::invalid) = {
// returns a [[locale]], or [[invalid]] if the input cannot be parsed. All
// memory is borrowed from the input, so [[finish]] should not be used to free
// it.
export fn parse(in: str) (locale | invalid) = {
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, ".");
if (strings::compare(in, "") == 0) return void: errors::invalid;
if (strings::compare(in, "") == 0) return void: invalid;
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 {
lang = in,
country = country,

View File

@@ -1,5 +1,4 @@
use fmt;
use errors;
use strings;
@test fn parse_full() void = {
@@ -64,8 +63,8 @@ use strings;
@test fn parse_error() void = {
let local = match(parse("_COUNTRY.ENCODING@MODIFIER")) {
case errors::invalid => void;
case => abort("error");
case invalid => void;
case => abort("no error");
};
};

133
locale/string.ha Normal file
View 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
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
};