diff --git a/README.md b/README.md
index 88ed65ad..c58e0a56 100644
--- a/README.md
+++ b/README.md
@@ -90,8 +90,8 @@ modification.
Openbox spec is somewhat of a stable standard considering how long it has
remained unchanged for and how wide-spread its adoption is by lightweight
-distributions such as LXDE, LXQt, BunsenLabs, ArchLabs, Mabox and Raspbian. Some
-widely used themes (for example Numix and Arc) have built-in support.
+distributions such as LXDE, LXQt, BunsenLabs, ArchLabs, Mabox and Raspbian.
+Some widely used themes (for example Numix and Arc) have built-in support.
We could have invented a whole new syntax, but that's not where we want to
spend our effort.
@@ -182,8 +182,8 @@ prevent installing the wlroots headers:
## 3. Configuration
User config files are located at `${XDG_CONFIG_HOME:-$HOME/.config/labwc/}`
-with the following five files being used: [rc.xml], [menu.xml], [autostart], [shutdown],
-[environment] and [themerc-override].
+with the following five files being used: [rc.xml] (or [rc.yaml]), [menu.xml],
+[autostart], [shutdown], [environment] and [themerc-override].
Run `labwc --reconfigure` to reload configuration and theme.
@@ -275,6 +275,7 @@ See [integration] for further details.
[metacity]: https://github.com/GNOME/metacity
[rc.xml]: docs/rc.xml.all
+[rc.yaml]: docs/rc.yaml
[menu.xml]: docs/menu.xml
[autostart]: docs/autostart
[shutdown]: docs/shutdown
diff --git a/docs/README b/docs/README
index c90c047a..769118e0 100644
--- a/docs/README
+++ b/docs/README
@@ -3,6 +3,7 @@ Config layout for ~/.config/labwc/
- environment
- menu.xml
- rc.xml
+- rc.yaml
- shutdown
- themerc-override
- xinitrc
diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd
index 6da0af34..53806cdc 100644
--- a/docs/labwc-config.5.scd
+++ b/docs/labwc-config.5.scd
@@ -8,7 +8,8 @@ labwc - configuration files
Labwc uses openbox-3.6 specification for configuration and theming, but does not
support all options. The following files form the basis of the labwc
-configuration: rc.xml, menu.xml, autostart, shutdown, environment and xinitrc.
+configuration: rc.xml (or rc.yaml), menu.xml, autostart, shutdown, environment
+and xinitrc.
No configuration files are needed to start and run labwc.
@@ -158,6 +159,77 @@ Note that in this manual, Boolean values are listed as [yes|no] for simplicity,
but it's also possible to use [true|false] and\/or [on|off];
this is for compatibility with Openbox.
+## YAML SUPPORT
+
+Labwc also supports YAML language for configuration. When rc.yaml exists
+instead of rc.xml, labwc internally converts it from YAML into XML and loads
+configurations from it. For example, "foo: bar" in YAML is converted to
+"bar" in XML. See /usr/share/docs/labwc/rc.yaml for an example
+configuration in YAML.
+
+If rc.yaml includes a key-value pair where the value is an array, it is
+converted to a sequence of array-element in XML.
+
+For example, a YAML expression:
+
+```
+touch:
+ - deviceName: xxxx
+ mapToOutput: eDP-1
+ - deviceName: yyyy
+ mapToOutput: HDMI-1
+```
+
+is converted to an XML expression:
+
+```
+
+ xxxx
+ eDP-1
+
+
+ yyyy
+ eDP-1
+
+```
+
+To avoid unnecessary indentations, some nodes that wrap array elements in XML
+can be ommitted. This includes:
+
+ - **
+ - **
+ - **
+ - **
+ - **
+
+For example, window switcher can be configured like:
+
+```
+windowSwitcher:
+ fields:
+ - content: type
+ width: 15%
+ - content: title
+ width: 85%
+```
+
+In addition, some specific keys in singular form with a sequence value in YAML
+are converted to plural form in XML. This includes:
+
+ - *keybinds* (converted to *keybind*)
+ - *mousebinds* (converted to *mousebind*)
+ - *actions* (converted to *action*)
+ - *fonts* (converted to *font*)
+ - *contexts* (converted to *context*)
+
+For example, keybinds can be configured like:
+
+```
+keybinds:
+ - { key: W-s, action: { name: Execute, command: foot } }
+ - { key: W-a, action: { name: Execute, command: fuzzel } }
+```
+
## CORE
```
@@ -573,9 +645,9 @@ extending outward from the snapped edge.
Load the default keybinds listed below. This is an addition to the
openbox specification and provides a way to keep config files simpler
whilst allowing your specific keybinds.
- Note that if no rc.xml is found, or if no entries
- exist, the same default keybinds will be loaded even if the
- element is not provided.
+ Note that if no rc.xml or rc.yaml is found, or if no
+ entries exist, the same default keybinds will be loaded even if the
+ element is not provided.
```
A-Tab - next window
@@ -664,9 +736,10 @@ extending outward from the snapped edge.
**
Load default mousebinds. This is an addition to the openbox
specification and provides a way to keep config files simpler whilst
- allowing user specific binds. Note that if no rc.xml is found, or if no
- entries exist, the same default mousebinds will be
- loaded even if the element is not provided.
+ allowing user specific binds. Note that if no rc.xml or rc.yaml is
+ found, or if no entries exist, the same default
+ mousebinds will be loaded even if the element is not
+ provided.
## TOUCH
diff --git a/docs/meson.build b/docs/meson.build
index a6676784..4691578e 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -33,7 +33,8 @@ install_data(
'shutdown',
'themerc',
'rc.xml',
- 'rc.xml.all'
+ 'rc.xml.all',
+ 'rc.yaml',
],
install_dir: get_option('datadir') / 'doc' / meson.project_name()
)
diff --git a/docs/rc.yaml b/docs/rc.yaml
new file mode 100644
index 00000000..299460ff
--- /dev/null
+++ b/docs/rc.yaml
@@ -0,0 +1,89 @@
+# An example configuration file in YAML. To see all the configurations and
+# their descriptions, see docs/rc.xml.all or labwc-config(5).
+core:
+ xwaylandPersistence: yes
+placement:
+ policy: cascade
+theme:
+ dropShadows: yes
+ fonts:
+ - place: ActiveWindow
+ weight: bold
+ - place: InactiveWindow
+ weight: normal
+desktops:
+ number: 2
+windowSwitcher:
+ fields:
+ - content: type
+ width: 15%
+ - content: title
+ width: 85%
+resize:
+ drawContents: no
+focus:
+ followMouse: yes
+ followMouseRequiresMovement: yes
+ raiseOnFocus: no
+snapping:
+ range: 20
+ overlay:
+ delay:
+ inner: 100
+ outer: 0
+regions:
+ - { name: top-left, x: 0%, y: 0%, width: 50%, height: 50% }
+ - { name: top-right, x: 50%, y: 0%, width: 50%, height: 50% }
+ - { name: bottom-left, x: 0%, y: 50%, width: 50%, height: 50% }
+ - { name: bottom-right, x: 50%, y: 50%, width: 50%, height: 50% }
+keyboard:
+ repeatRate: 25
+ repeatDelay: 600
+ keybinds:
+ - { key: W-bracketRight, action: { name: ZoomIn } }
+ - { key: W-bracketLeft, action: { name: ZoomOut } }
+ - { key: W-d, action: { name: Debug } }
+ - { key: W-1, action: { name: GoToDesktop, to: Workspace 1 } }
+ - { key: W-2, action: { name: GoToDesktop, to: Workspace 2 } }
+ - { key: W-m, action: { name: ToggleKeybinds } }
+ - { key: W-q, action: { name: Close } }
+ - { key: A-F4, action: { name: Close } }
+ - { key: A-Tab, action: { name: NextWindow } }
+ - { key: W-e, action: { name: Exit } }
+ - { key: W-v, action: { name: Execute, command: sh -c 'cliphist list | fuzzel --dmenu | cliphist decode | wl-copy' } }
+ - { key: W-Tab, action: { name: ToggleMaximize } }
+ - { key: W-s, action: { name: Execute, command: foot } }
+ - { key: W-a, action: { name: Execute, command: fuzzel } }
+ - { key: XF86_AudioLowerVolume, action: { name: Execute, command: pactl set-sink-volume @DEFAULT_SINK@ -2dB } }
+ - { key: XF86_AudioRaiseVolume, action: { name: Execute, command: pactl set-sink-volume @DEFAULT_SINK@ +2dB } }
+ - { key: XF86_AudioMute, action: { name: Execute, command: pactl set-sink-mute @DEFAULT_SINK@ toggle } }
+ - { key: XF86_AudioMicMute, action: { name: Execute, command: pactl set-source-mute @DEFAULT_SOURCE@ toggle } }
+ - { key: Print, action: { name: Execute, command: grim } }
+mouse:
+ doubleClickTime: 200
+ default:
+ contexts:
+ - name: Frame
+ mousebinds:
+ - { button: W-Left, action: Press, actions: [ { name: Raise }, { name: Move } ] }
+ - { button: W-Right, action: Drag, action: { name: Raise } }
+ - { button: A-Left, action: Press }
+ - { button: A-Left, action: Drag }
+ - { button: A-Right, action: Press }
+ - { button: A-Right, action: Drag }
+touch:
+ deviceName: ELAN2514:00 04F3:2AF1
+ mapToOutput: eDP-1
+tablet:
+ mapToOutput: eDP-1
+libinput:
+ - category: non-touch
+ pointerSpeed: -0.4
+ - category: touchpad
+ naturalScroll: yes
+ tapAndDrag: no
+windowRules:
+ - identifier: ristretto
+ serverDecoration: no
+ - identifier: code-url-handler
+ action: { name: SetDecorations, decorations: border, forceSSD: yes }
diff --git a/include/common/yaml2xml.h b/include/common/yaml2xml.h
new file mode 100644
index 00000000..8c1a27ca
--- /dev/null
+++ b/include/common/yaml2xml.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef LABWC_YAML2XML_H
+#define LABWC_YAML2XML_H
+#include
+#include
+
+struct buf yaml_to_xml(FILE *stream, const char *toplevel_name);
+
+#endif /* LABWC_YAML2XML_H */
diff --git a/meson.build b/meson.build
index 8f9b5a97..da2f449d 100644
--- a/meson.build
+++ b/meson.build
@@ -72,6 +72,7 @@ pixman = dependency('pixman-1')
math = cc.find_library('m')
png = dependency('libpng')
svg = dependency('librsvg-2.0', version: '>=2.46', required: false)
+yaml = dependency('yaml-0.1', required: false)
if get_option('xwayland').enabled() and not wlroots_has_xwayland
error('no wlroots Xwayland support')
@@ -87,6 +88,13 @@ else
endif
conf_data.set10('HAVE_RSVG', have_rsvg)
+if get_option('yaml').disabled()
+ have_libyaml = false
+else
+ have_libyaml = yaml.found()
+endif
+conf_data.set10('HAVE_LIBYAML', have_libyaml)
+
if get_option('static_analyzer').enabled()
add_project_arguments(['-fanalyzer'], language: 'c')
endif
@@ -125,6 +133,11 @@ if have_rsvg
svg,
]
endif
+if have_libyaml
+ labwc_deps += [
+ yaml,
+ ]
+endif
subdir('include')
subdir('src')
diff --git a/meson_options.txt b/meson_options.txt
index 4d6e8cd5..3b6b13b8 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -4,3 +4,4 @@ option('svg', type: 'feature', value: 'enabled', description: 'Enable svg window
option('nls', type: 'feature', value: 'auto', description: 'Enable native language support')
option('static_analyzer', type: 'feature', value: 'disabled', description: 'Run gcc static analyzer')
option('test', type: 'feature', value: 'disabled', description: 'Run tests')
+option('yaml', type: 'feature', value: 'enabled', description: 'Enable configuration in YAML')
diff --git a/src/common/meson.build b/src/common/meson.build
index 1569f775..18488155 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -19,3 +19,9 @@ labwc_sources += files(
'spawn.c',
'string-helpers.c',
)
+
+if have_libyaml
+ labwc_sources += files(
+ 'yaml2xml.c',
+ )
+endif
diff --git a/src/common/yaml2xml.c b/src/common/yaml2xml.c
new file mode 100644
index 00000000..5ac1e6e5
--- /dev/null
+++ b/src/common/yaml2xml.c
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "common/buf.h"
+#include "common/yaml2xml.h"
+
+struct lab_yaml_event {
+ yaml_event_type_t type;
+ char scalar[256];
+};
+
+static bool process_mapping(yaml_parser_t *parser, struct buf *buf);
+static bool process_sequence(yaml_parser_t *parser, struct buf *buf, char *key_name);
+
+static bool
+parse(yaml_parser_t *parser, struct lab_yaml_event *event)
+{
+ yaml_event_t yaml_event;
+ if (!yaml_parser_parse(parser, &yaml_event)) {
+ wlr_log(WLR_ERROR,
+ "Conversion from YAML to XML failed at %ld:%ld: %s",
+ parser->problem_mark.line + 1,
+ parser->problem_mark.column,
+ parser->problem);
+ return false;
+ }
+ event->type = yaml_event.type;
+ if (yaml_event.type == YAML_SCALAR_EVENT) {
+ snprintf(event->scalar, sizeof(event->scalar), "%s",
+ (char *)yaml_event.data.scalar.value);
+ }
+ yaml_event_delete(&yaml_event);
+ return event;
+}
+
+static bool
+process_value(yaml_parser_t *parser, struct buf *b,
+ struct lab_yaml_event *event, char *key_name)
+{
+ const char *parent_name = NULL;
+
+ switch (event->type) {
+ case YAML_MAPPING_START_EVENT:
+ buf_add_fmt(b, "<%s>", key_name);
+ if (!process_mapping(parser, b)) {
+ return false;
+ }
+ buf_add_fmt(b, "%s>", key_name);
+ break;
+ case YAML_SEQUENCE_START_EVENT:
+ /*
+ * YAML XML
+ * fields: [x,y] -> xy
+ */
+ if (!strcasecmp(key_name, "fields")) {
+ parent_name = "fields";
+ key_name = "field";
+ } else if (!strcasecmp(key_name, "regions")) {
+ parent_name = "regions";
+ key_name = "region";
+ } else if (!strcasecmp(key_name, "windowRules")) {
+ parent_name = "windowRules";
+ key_name = "windowRule";
+ } else if (!strcasecmp(key_name, "libinput")) {
+ parent_name = "libinput";
+ key_name = "device";
+ } else if (!strcasecmp(key_name, "names")) {
+ parent_name = "names";
+ key_name = "name";
+ } else if (!strcasecmp(key_name, "keybinds")) {
+ key_name = "keybind";
+ } else if (!strcasecmp(key_name, "mousebinds")) {
+ key_name = "mousebind";
+ } else if (!strcasecmp(key_name, "actions")) {
+ key_name = "action";
+ } else if (!strcasecmp(key_name, "fonts")) {
+ key_name = "font";
+ } else if (!strcasecmp(key_name, "contexts")) {
+ key_name = "context";
+ }
+ if (parent_name) {
+ buf_add_fmt(b, "<%s>", parent_name);
+ }
+ if (!process_sequence(parser, b, key_name)) {
+ return false;
+ }
+ if (parent_name) {
+ buf_add_fmt(b, "%s>", parent_name);
+ }
+ break;
+ case YAML_SCALAR_EVENT:
+ /*
+ * To avoid duplicated keys, mousebind: {event: Press} is
+ * mapped to .
+ */
+ if (!strcasecmp(key_name, "event")) {
+ key_name = "action";
+ }
+ buf_add_fmt(b, "<%s>%s%s>", key_name, event->scalar, key_name);
+ break;
+ default:
+ break;
+ }
+ return true;
+}
+
+static bool
+process_sequence(yaml_parser_t *parser, struct buf *b, char *key_name)
+{
+ while (1) {
+ struct lab_yaml_event event;
+ if (!parse(parser, &event)) {
+ return false;
+ }
+ if (event.type == YAML_SEQUENCE_END_EVENT) {
+ return true;
+ }
+ if (!process_value(parser, b, &event, key_name)) {
+ return false;
+ }
+ }
+}
+
+static bool
+process_mapping(yaml_parser_t *parser, struct buf *buf)
+{
+ while (1) {
+ struct lab_yaml_event key_event, value_event;
+
+ if (!parse(parser, &key_event)) {
+ return false;
+ }
+ if (key_event.type == YAML_MAPPING_END_EVENT) {
+ return true;
+ }
+ if (key_event.type != YAML_SCALAR_EVENT) {
+ wlr_log(WLR_ERROR, "key must be scalar");
+ return false;
+ }
+ if (!parse(parser, &value_event)) {
+ return false;
+ }
+ if (!process_value(parser, buf, &value_event, key_event.scalar)) {
+ return false;
+ }
+ continue;
+ }
+}
+
+static bool
+process_root(yaml_parser_t *parser, struct buf *b, const char *toplevel_name)
+{
+ struct lab_yaml_event event;
+ if (!parse(parser, &event) || event.type != YAML_STREAM_START_EVENT) {
+ return false;
+ }
+ if (!parse(parser, &event) || event.type != YAML_DOCUMENT_START_EVENT) {
+ return false;
+ }
+ if (!parse(parser, &event) || event.type != YAML_MAPPING_START_EVENT) {
+ wlr_log(WLR_ERROR, "mapping is expected for toplevel node");
+ return false;
+ }
+
+ buf_add_fmt(b, "<%s>", toplevel_name);
+ if (!process_mapping(parser, b)) {
+ return false;
+ }
+ buf_add_fmt(b, "%s>", toplevel_name);
+ return true;
+}
+
+struct buf
+yaml_to_xml(FILE *stream, const char *toplevel_name)
+{
+ struct buf b = BUF_INIT;
+ yaml_parser_t parser;
+ yaml_parser_initialize(&parser);
+ yaml_parser_set_input_file(&parser, stream);
+
+ bool success = process_root(&parser, &b, toplevel_name);
+ if (success) {
+ wlr_log(WLR_DEBUG, "XML converted from YAML: %s", b.data);
+ } else {
+ buf_reset(&b);
+ }
+ yaml_parser_delete(&parser);
+ return b;
+}
diff --git a/src/config/rcxml.c b/src/config/rcxml.c
index fa4163c7..b3f0d8f3 100644
--- a/src/config/rcxml.c
+++ b/src/config/rcxml.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only
#define _POSIX_C_SOURCE 200809L
#include
+#include
#include
#include
#include
@@ -15,6 +16,7 @@
#include
#include
#include "action.h"
+#include "common/buf.h"
#include "common/dir.h"
#include "common/list.h"
#include "common/macros.h"
@@ -36,6 +38,10 @@
#include "window-rules.h"
#include "workspaces.h"
+#if HAVE_LIBYAML
+#include "common/yaml2xml.h"
+#endif
+
static bool in_regions;
static bool in_usable_area_override;
static bool in_keybind;
@@ -1743,6 +1749,37 @@ validate(void)
}
}
+static bool
+file_is_xml(const char *filename)
+{
+ if (str_endswith(filename, ".xml")) {
+ return true;
+ } else if (str_endswith(filename, ".yaml")) {
+ return false;
+ }
+
+ /*
+ * If the extension is not .xml or .yaml, judge by whther the content
+ * starts with '<'.
+ */
+ FILE *stream = fopen(filename, "r");
+ if (!stream) {
+ return true;
+ }
+ char c;
+ while ((c = fgetc(stream))) {
+ if (!isspace(c)) {
+ break;
+ }
+ }
+ fclose(stream);
+ if (c == '<') {
+ return true;
+ } else {
+ return false;
+ }
+}
+
void
rcxml_read(const char *filename)
{
@@ -1758,9 +1795,14 @@ rcxml_read(const char *filename)
wl_list_append(&paths, &path->link);
} else {
paths_config_create(&paths, "rc.xml");
+#ifdef HAVE_LIBYAML
+ struct wl_list paths_yaml;
+ wl_list_init(&paths_yaml);
+ paths_config_create(&paths_yaml, "rc.yaml");
+ wl_list_insert_list(paths.prev, &paths_yaml);
+#endif
}
- /* Reading file into buffer before parsing - better for unit tests */
bool should_merge_config = rc.merge_config;
struct wl_list *(*iter)(struct wl_list *list);
iter = should_merge_config ? paths_get_prev : paths_get_next;
@@ -1782,21 +1824,30 @@ rcxml_read(const char *filename)
continue;
}
- wlr_log(WLR_INFO, "read config file %s", path->string);
-
struct buf b = BUF_INIT;
- char *line = NULL;
- size_t len = 0;
- while (getline(&line, &len, stream) != -1) {
- char *p = strrchr(line, '\n');
- if (p) {
- *p = '\0';
+ if (HAVE_LIBYAML && !file_is_xml(path->string)) {
+#if HAVE_LIBYAML
+ wlr_log(WLR_INFO, "read yaml config file %s", path->string);
+ b = yaml_to_xml(stream, "labwc_config");
+#endif
+ } else {
+ wlr_log(WLR_INFO, "read xml config file %s", path->string);
+ char *line = NULL;
+ size_t len = 0;
+ while (getline(&line, &len, stream) != -1) {
+ char *p = strrchr(line, '\n');
+ if (p) {
+ *p = '\0';
+ }
+ buf_add(&b, line);
}
- buf_add(&b, line);
+ zfree(line);
}
- zfree(line);
+
fclose(stream);
- rcxml_parse_xml(&b);
+ if (b.len > 0) {
+ rcxml_parse_xml(&b);
+ }
buf_reset(&b);
if (!should_merge_config) {
break;
diff --git a/t/meson.build b/t/meson.build
index 5517b973..9eb99bf3 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -1,18 +1,37 @@
-test_lib = static_library(
- 'test_lib',
- sources: files(
+test_lib_sources = files(
'../src/common/buf.c',
'../src/common/mem.c',
- '../src/common/string-helpers.c'
- ),
- include_directories: [labwc_inc],
- dependencies: [dep_cmocka],
+ '../src/common/string-helpers.c',
)
+test_deps = [
+ dep_cmocka,
+]
+
tests = [
'buf-simple',
]
+if have_libyaml
+ test_lib_sources += [
+ '../src/common/yaml2xml.c',
+ ]
+ test_deps += [
+ wlroots,
+ yaml,
+ ]
+ tests += [
+ 'yaml2xml',
+ ]
+endif
+
+test_lib = static_library(
+ 'test_lib',
+ sources: test_lib_sources,
+ include_directories: [labwc_inc],
+ dependencies: test_deps
+)
+
foreach t : tests
test(
'test_@0@'.format(t),
diff --git a/t/yaml2xml.c b/t/yaml2xml.c
new file mode 100644
index 00000000..dd87ca7d
--- /dev/null
+++ b/t/yaml2xml.c
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define _POSIX_C_SOURCE 200809L
+#include
+#include
+#include
+#include
+#include
+#include
+#include "common/buf.h"
+#include "common/macros.h"
+#include "common/yaml2xml.h"
+
+struct test_set {
+ const char *name, *yaml, *xml;
+};
+
+const struct test_set test_sets[] = {
+ {
+ .name = "key-scalar",
+ .yaml = "xxx: yyy",
+ .xml = "yyy",
+ },
+ {
+ .name = "key-sequence",
+ .yaml = "xxx: [yyy, zzz]",
+ .xml = "yyyzzz",
+ },
+ {
+ .name = "key-mapping",
+ .yaml = "xxx: {yyy: zzz}",
+ .xml = "zzz",
+ },
+ {
+ .name = "window-switcher-fields",
+ .yaml = "windowSwitcher: {fields: [xxx, yyy]}",
+ .xml =
+ ""
+ "xxx"
+ "yyy"
+ "",
+ },
+ {
+ .name = "theme-fonts",
+ .yaml = "theme: {fonts: [xxx, yyy]}",
+ .xml =
+ ""
+ "xxx"
+ "yyy"
+ "",
+ },
+ {
+ .name = "mousebinds",
+ .yaml =
+ "mousebinds:\n"
+ " - { button: W-Left, action: Press, actions: [ { name: Raise }, { name: Move } ] }\n"
+ " - { button: W-Right, action: Drag, action: { name: Resize} }\n",
+ .xml =
+ ""
+ ""
+ ""
+ "Press"
+ "Raise"
+ "Move"
+ ""
+ ""
+ ""
+ "Drag"
+ "Resize"
+ ""
+ "",
+ },
+};
+
+static void
+test_yaml_to_xml(void **state)
+{
+ (void)state;
+ for (int i = 0; i < (int)ARRAY_SIZE(test_sets); i++) {
+ const struct test_set *set = &test_sets[i];
+
+ char buf[1024];
+ FILE *stream = fmemopen(buf, sizeof(buf), "w+");
+ fwrite(set->yaml, strlen(set->yaml), 1, stream);
+ fseek(stream, 0, SEEK_SET);
+
+ struct buf b = yaml_to_xml(stream, "test");
+ fclose(stream);
+ assert_string_equal(b.data, set->xml);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_yaml_to_xml),
+ };
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}