From a0a9f977b4f856625535f6f80ab219593d94e444 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sun, 18 Aug 2024 12:18:41 +0900 Subject: [PATCH 1/3] config: fix assertion failure on When is expressed as a child node rather than an attribute, `content` in `fill_touch()` becomes NULL when the parser reaches . --- src/config/rcxml.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 30fd6afe..fa4163c7 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -545,7 +545,12 @@ fill_touch(char *nodename, char *content) if (!strcasecmp(nodename, "touch")) { current_touch = znew(*current_touch); wl_list_append(&rc.touch_configs, ¤t_touch->link); - } else if (!strcasecmp(nodename, "deviceName.touch")) { + return; + } else if (!content) { + return; + } + + if (!strcasecmp(nodename, "deviceName.touch")) { current_touch->device_name = xstrdup(content); } else if (!strcasecmp(nodename, "mapToOutput.touch")) { current_touch->output_name = xstrdup(content); From 85b6e25484b265316948090b2dd8e958679d5a27 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sun, 18 Aug 2024 12:22:07 +0900 Subject: [PATCH 2/3] config: support rc.yaml Based on @johanmalm's work. This adds libyaml as an optional dependency. --- README.md | 9 +- docs/README | 1 + docs/labwc-config.5.scd | 87 +++++++++++++++-- docs/meson.build | 3 +- docs/rc.yaml | 89 +++++++++++++++++ include/common/yaml2xml.h | 9 ++ meson.build | 13 +++ meson_options.txt | 1 + src/common/meson.build | 6 ++ src/common/yaml2xml.c | 194 ++++++++++++++++++++++++++++++++++++++ src/config/rcxml.c | 75 ++++++++++++--- t/meson.build | 33 +++++-- t/yaml2xml.c | 99 +++++++++++++++++++ 13 files changed, 588 insertions(+), 31 deletions(-) create mode 100644 docs/rc.yaml create mode 100644 include/common/yaml2xml.h create mode 100644 src/common/yaml2xml.c create mode 100644 t/yaml2xml.c 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, "", 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, "", 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", 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, "", 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); +} From 52a1c1c880b42647ae35efac4e881496ba39d7e3 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Fri, 23 Aug 2024 15:27:08 +0900 Subject: [PATCH 3/3] [experiment] support menu.yaml --- docs/labwc-menu.5.scd | 12 ++++- docs/menu.yaml | 33 ++++++++++++++ src/common/yaml2xml.c | 4 ++ src/menu/menu.c | 103 +++++++++++++++++++++++++++++------------- 4 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 docs/menu.yaml diff --git a/docs/labwc-menu.5.scd b/docs/labwc-menu.5.scd index a50fa254..ae6c425a 100644 --- a/docs/labwc-menu.5.scd +++ b/docs/labwc-menu.5.scd @@ -106,13 +106,23 @@ ID attributes are unique. Duplicates are ignored. When writing pipe menu scripts, make sure to escape XML special characters such as "&" ("&"), "<" ("<"), and ">" (">"). - # LOCALISATION Available localisation for the default "client-menu" is only shown if no "client-menu" is present in menu.xml. Any menu definition in menu.xml is interpreted as a user-override. +# YAML SUPPORT + +Like rc.yaml, labwc supports menu.yaml instead of menu.xml. See labwc-config(5) +for its syntax. Note that following keys in singular form can be expressed as +plural form in menu.yaml: + + - *menus* (converted to *menu*) + - *items* (converted to *item*) + +See /usr/share/docs/labwc/menu.yaml for an example menu configuration in YAML. + # SEE ALSO labwc(1), labwc-actions(5), labwc-config(5), labwc-theme(5) diff --git a/docs/menu.yaml b/docs/menu.yaml new file mode 100644 index 00000000..842799f8 --- /dev/null +++ b/docs/menu.yaml @@ -0,0 +1,33 @@ +menu: + - id: client-menu + item: + label: Minimize + action: { name: Iconify } + item: + label: Maximize + action: { name: ToggleMaximize } + menu: + id: workspaces + label: Workspace + item: + label: Move Left + action: { name: SendToDesktop, to: left } + item: + label: Move Right + action: { name: SendToDesktop, to: right } + separator: + item: + label: Always on Visible Workspace + action: { name: ToggleOmnipresent } + item: + label: Close + action: { name: Close } + + - id: root-menu + items: + - label: Terminal + action: { name: Execute, command: alacritty } + - label: Reconfigure + action: { name: Reconfigure } + - label: Exit + action: { name: Exit } diff --git a/src/common/yaml2xml.c b/src/common/yaml2xml.c index 5ac1e6e5..82fa7f65 100644 --- a/src/common/yaml2xml.c +++ b/src/common/yaml2xml.c @@ -82,6 +82,10 @@ process_value(yaml_parser_t *parser, struct buf *b, key_name = "font"; } else if (!strcasecmp(key_name, "contexts")) { key_name = "context"; + } else if (!strcasecmp(key_name, "items")) { + key_name = "item"; + } else if (!strcasecmp(key_name, "menus")) { + key_name = "menu"; } if (parent_name) { buf_add_fmt(b, "<%s>", parent_name); diff --git a/src/menu/menu.c b/src/menu/menu.c index f8653d64..a8039a09 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -26,6 +26,10 @@ #include "node.h" #include "theme.h" +#if HAVE_LIBYAML +#include "common/yaml2xml.h" +#endif + #define PIPEMENU_MAX_BUF_SIZE 1048576 /* 1 MiB */ #define PIPEMENU_TIMEOUT_IN_MS 4000 /* 4 seconds */ @@ -566,6 +570,35 @@ is_toplevel_static_menu_definition(xmlNode *n, char *id) return id && nr_parents(n) == 2; } +static char * +get_property(xmlNode *n, const char *name) +{ + /* First, search from attributes */ + char *prop = (char *)xmlGetProp(n, (const xmlChar *)name); + if (prop) { + return prop; + } + + /* Then search from child nodes */ + xmlNode *child; + for (child = n->children; child && child->name; + child = child->next) { + if (!strcmp((char *)child->name, name)) { + goto found_child_node; + } + } + return NULL; + +found_child_node: + for (child = child->children; child && child->name; + child = child->next) { + if (child->type == XML_TEXT_NODE) { + return xstrdup((char *)child->content); + } + } + return NULL; +} + /* * elements have three different roles: * * Definition of (sub)menu - has ID, LABEL and CONTENT @@ -575,9 +608,9 @@ is_toplevel_static_menu_definition(xmlNode *n, char *id) static void handle_menu_element(xmlNode *n, struct server *server) { - char *label = (char *)xmlGetProp(n, (const xmlChar *)"label"); - char *execute = (char *)xmlGetProp(n, (const xmlChar *)"execute"); - char *id = (char *)xmlGetProp(n, (const xmlChar *)"id"); + char *label = get_property(n, "label"); + char *execute = get_property(n, "execute"); + char *id = get_property(n, "id"); if (execute && label && id) { wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute); @@ -676,7 +709,7 @@ error: static void handle_separator_element(xmlNode *n) { - char *label = (char *)xmlGetProp(n, (const xmlChar *)"label"); + char *label = get_property(n, "label"); current_item = separator_create(current_menu, label); free(label); } @@ -725,36 +758,13 @@ parse_buf(struct server *server, struct buf *buf) return true; } -/* - * @stream can come from either of the following: - * - fopen() in the case of reading a file such as menu.xml - * - popen() when processing pipemenus - */ -static void -parse_stream(struct server *server, FILE *stream) -{ - char *line = NULL; - size_t len = 0; - struct buf b = BUF_INIT; - - while (getline(&line, &len, stream) != -1) { - char *p = strrchr(line, '\n'); - if (p) { - *p = '\0'; - } - buf_add(&b, line); - } - free(line); - parse_buf(server, &b); - buf_reset(&b); -} - -static void -parse_xml(const char *filename, struct server *server) +static bool +parse_menu_file(const char *filename, struct server *server) { struct wl_list paths; paths_config_create(&paths, filename); + bool file_found = false; 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; @@ -765,14 +775,37 @@ parse_xml(const char *filename, struct server *server) if (!stream) { continue; } + file_found = true; wlr_log(WLR_INFO, "read menu file %s", path->string); - parse_stream(server, stream); + + struct buf b = BUF_INIT; + if (HAVE_LIBYAML && str_endswith(path->string, ".yaml")) { +#if HAVE_LIBYAML + b = yaml_to_xml(stream, "openbox_menu"); +#endif + } else { + 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); + } + free(line); + } fclose(stream); + if (b.len > 0) { + parse_buf(server, &b); + } + buf_reset(&b); if (!should_merge_config) { break; } } paths_destroy(&paths); + return file_found; } static int @@ -983,7 +1016,13 @@ void menu_init(struct server *server) { wl_list_init(&server->menus); - parse_xml("menu.xml", server); + bool file_found = parse_menu_file("menu.xml", server); + (void)file_found; +#if HAVE_LIBYAML + if (!file_found) { + parse_menu_file("menu.yaml", server); + } +#endif init_rootmenu(server); init_windowmenu(server); post_processing(server);