config: support rc.yaml

Based on @johanmalm's work.

This adds libyaml as an optional dependency.
This commit is contained in:
tokyo4j 2024-08-18 12:22:07 +09:00
parent a0a9f977b4
commit 85b6e25484
13 changed files with 588 additions and 31 deletions

View file

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

View file

@ -3,6 +3,7 @@ Config layout for ~/.config/labwc/
- environment
- menu.xml
- rc.xml
- rc.yaml
- shutdown
- themerc-override
- xinitrc

View file

@ -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
"<foo>bar</foo>" 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 <key>array-element</key> in XML.
For example, a YAML expression:
```
touch:
- deviceName: xxxx
mapToOutput: eDP-1
- deviceName: yyyy
mapToOutput: HDMI-1
```
is converted to an XML expression:
```
<touch>
<deviceName>xxxx</deviceName>
<mapToOutput>eDP-1</mapToOutput>
</touch>
<touch>
<deviceName>yyyy</deviceName>
<mapToOutput>eDP-1</mapToOutput>
</touch>
```
To avoid unnecessary indentations, some nodes that wrap array elements in XML
can be ommitted. This includes:
- *<windowSwitcher><fields><field>*
- *<regions><region>*
- *<windowRules><windowRule>*
- *<libinput><device>*
- *<desktops><names><name>*
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 <keyboard><keybind> entries
exist, the same default keybinds will be loaded even if the <default />
element is not provided.
Note that if no rc.xml or rc.yaml is found, or if no <keyboard><keybind>
entries exist, the same default keybinds will be loaded even if the
<default /> element is not provided.
```
A-Tab - next window
@ -664,9 +736,10 @@ extending outward from the snapped edge.
*<mouse><default />*
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
<mouse><mousebind> entries exist, the same default mousebinds will be
loaded even if the <default /> element is not provided.
allowing user specific binds. Note that if no rc.xml or rc.yaml is
found, or if no <mouse><mousebind> entries exist, the same default
mousebinds will be loaded even if the <default /> element is not
provided.
## TOUCH

View file

@ -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()
)

89
docs/rc.yaml Normal file
View file

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

View file

@ -0,0 +1,9 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef LABWC_YAML2XML_H
#define LABWC_YAML2XML_H
#include <common/buf.h>
#include <stdio.h>
struct buf yaml_to_xml(FILE *stream, const char *toplevel_name);
#endif /* LABWC_YAML2XML_H */

View file

@ -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')

View file

@ -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')

View file

@ -19,3 +19,9 @@ labwc_sources += files(
'spawn.c',
'string-helpers.c',
)
if have_libyaml
labwc_sources += files(
'yaml2xml.c',
)
endif

194
src/common/yaml2xml.c Normal file
View file

@ -0,0 +1,194 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <assert.h>
#include <yaml.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <wlr/util/log.h>
#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] -> <fields><field>x<field><field>y<field<fields>
*/
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 <mousebind action="Press">.
*/
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;
}

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <ctype.h>
#include <fcntl.h>
#include <glib.h>
#include <libxml/parser.h>
@ -15,6 +16,7 @@
#include <wlr/util/box.h>
#include <wlr/util/log.h>
#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;

View file

@ -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),

99
t/yaml2xml.c Normal file
View file

@ -0,0 +1,99 @@
// SPDX-License-Identifier: GPL-2.0-only
#define _POSIX_C_SOURCE 200809L
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <cmocka.h>
#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 = "<test><xxx>yyy</xxx></test>",
},
{
.name = "key-sequence",
.yaml = "xxx: [yyy, zzz]",
.xml = "<test><xxx>yyy</xxx><xxx>zzz</xxx></test>",
},
{
.name = "key-mapping",
.yaml = "xxx: {yyy: zzz}",
.xml = "<test><xxx><yyy>zzz</yyy></xxx></test>",
},
{
.name = "window-switcher-fields",
.yaml = "windowSwitcher: {fields: [xxx, yyy]}",
.xml =
"<test><windowSwitcher><fields>"
"<field>xxx</field>"
"<field>yyy</field>"
"</fields></windowSwitcher></test>",
},
{
.name = "theme-fonts",
.yaml = "theme: {fonts: [xxx, yyy]}",
.xml =
"<test><theme>"
"<font>xxx</font>"
"<font>yyy</font>"
"</theme></test>",
},
{
.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 =
"<test>"
"<mousebind>"
"<button>W-Left</button>"
"<action>Press</action>"
"<action><name>Raise</name></action>"
"<action><name>Move</name></action>"
"</mousebind>"
"<mousebind>"
"<button>W-Right</button>"
"<action>Drag</action>"
"<action><name>Resize</name></action>"
"</mousebind>"
"</test>",
},
};
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);
}