diff --git a/CMakeLists.txt b/CMakeLists.txt index 70e58b3..e3bf3b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,15 +25,49 @@ add_custom_command( DEPENDS ${CMAKE_SOURCE_DIR}/xdg-shell.xml ) +# viewporter +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/include/viewporter.h + COMMAND wayland-scanner client-header ${CMAKE_SOURCE_DIR}/viewporter.xml ${CMAKE_CURRENT_BINARY_DIR}/include/viewporter.h + DEPENDS ${CMAKE_SOURCE_DIR}/viewporter.xml +) + +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/viewporter.c + COMMAND wayland-scanner private-code ${CMAKE_SOURCE_DIR}/viewporter.xml ${CMAKE_CURRENT_BINARY_DIR}/viewporter.c + DEPENDS ${CMAKE_SOURCE_DIR}/viewporter.xml +) + +# fractional-scale-v1 +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/include/fractional-scale-v1.h + COMMAND wayland-scanner client-header ${CMAKE_SOURCE_DIR}/fractional-scale-v1.xml ${CMAKE_CURRENT_BINARY_DIR}/include/fractional-scale-v1.h + DEPENDS ${CMAKE_SOURCE_DIR}/fractional-scale-v1.xml +) + +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/fractional-scale-v1.c + COMMAND wayland-scanner private-code ${CMAKE_SOURCE_DIR}/fractional-scale-v1.xml ${CMAKE_CURRENT_BINARY_DIR}/fractional-scale-v1.c + DEPENDS ${CMAKE_SOURCE_DIR}/fractional-scale-v1.xml +) + set(SOURCES tkeyboard.c tpointer.c - hashtable.c + hashtable.c + toml.c + config.c + grid.c + pattern.c ${CMAKE_CURRENT_BINARY_DIR}/xdg-shell.c + ${CMAKE_CURRENT_BINARY_DIR}/viewporter.c + ${CMAKE_CURRENT_BINARY_DIR}/fractional-scale-v1.c ) set(GENHEADERS ${CMAKE_CURRENT_BINARY_DIR}/include/xdg-shell.h + ${CMAKE_CURRENT_BINARY_DIR}/include/viewporter.h + ${CMAKE_CURRENT_BINARY_DIR}/include/fractional-scale-v1.h ) add_executable(wlgol wlgol.c ${SOURCES} ${GENHEADERS}) @@ -47,6 +81,7 @@ target_include_directories(wlgol target_link_libraries(wlgol wayland-client wayland-cursor + m ${XKBCOMMON_LIBRARIES} ) @@ -75,3 +110,59 @@ target_compile_options(test_hashtable PRIVATE enable_testing() add_test(NAME Hashtable COMMAND test_hashtable) + +add_executable(test_config tests/test_config.c + ${SOURCES} +) +target_link_libraries(test_config + wayland-client + wayland-cursor + ${XKBCOMMON_LIBRARIES} +) +target_include_directories(test_config + PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/include + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC ${XKBCOMMON_INCLUDE_DIRS} +) +target_compile_options(test_config PRIVATE + ${XKBCOMMON_CFLAGS_OTHER} +) +add_test(NAME Config COMMAND test_config) + +add_executable(test_grid tests/test_grid.c + ${SOURCES} +) +target_link_libraries(test_grid + wayland-client + wayland-cursor + m + ${XKBCOMMON_LIBRARIES} +) +target_include_directories(test_grid + PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/include + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC ${XKBCOMMON_INCLUDE_DIRS} +) +target_compile_options(test_grid PRIVATE + ${XKBCOMMON_CFLAGS_OTHER} +) +add_test(NAME Grid COMMAND test_grid) + +add_executable(test_pattern tests/test_pattern.c + ${SOURCES} +) +target_link_libraries(test_pattern + wayland-client + wayland-cursor + m + ${XKBCOMMON_LIBRARIES} +) +target_include_directories(test_pattern + PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/include + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC ${XKBCOMMON_INCLUDE_DIRS} +) +target_compile_options(test_pattern PRIVATE + ${XKBCOMMON_CFLAGS_OTHER} +) +add_test(NAME Pattern COMMAND test_pattern) diff --git a/config.c b/config.c new file mode 100644 index 0000000..2328753 --- /dev/null +++ b/config.c @@ -0,0 +1,229 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "toml.h" + +static char *trim(char *s) { + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + char *end = s + strlen(s); + while (end > s && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r')) end--; + *end = '\0'; + return s; +} + +static void config_ensure_capacity(config_t *cfg) { + if (cfg->count < cfg->capacity) return; + int newcap = cfg->capacity ? cfg->capacity * 2 : 16; + binding_t *nb = realloc(cfg->items, newcap * sizeof(binding_t)); + if (!nb) return; + cfg->items = nb; + cfg->capacity = newcap; +} + +static void config_set_binding(config_t *cfg, uint32_t mods, xkb_keysym_t keysym, action_t action) { + for (int i = 0; i < cfg->count; i++) { + if (cfg->items[i].mods == mods && cfg->items[i].keysym == keysym) { + cfg->items[i].action = action; + return; + } + } + config_ensure_capacity(cfg); + if (cfg->count >= cfg->capacity) return; + cfg->items[cfg->count].mods = mods; + cfg->items[cfg->count].keysym = keysym; + cfg->items[cfg->count].action = action; + cfg->count++; +} + +int config_parse_mods(const char *s, uint32_t *out_mods, char **out_key) { + if (!s || !*s || !out_mods || !out_key) return -1; + char *copy = strdup(s); + if (!copy) return -1; + /* count tokens */ + int n = 0; + char *p = copy; + /* first pass count */ + char *tmp = strdup(s); + char *save = NULL; + char *t = strtok_r(tmp, "+", &save); + /* collect */ + char **toks = NULL; + int cap = 0; + while (t) { + char *tt = trim(t); + if (*tt) { + if (n == cap) { + cap = cap ? cap * 2 : 4; + toks = realloc(toks, cap * sizeof(char *)); + } + toks[n++] = strdup(tt); + } + t = strtok_r(NULL, "+", &save); + } + free(tmp); + if (n == 0) { free(copy); free(toks); return -1; } + + uint32_t mods = 0; + for (int i = 0; i < n - 1; i++) { + if (strcasecmp(toks[i], "ctrl") == 0 || strcasecmp(toks[i], "control") == 0) mods |= MOD_CTRL; + else if (strcasecmp(toks[i], "shift") == 0) mods |= MOD_SHIFT; + else if (strcasecmp(toks[i], "alt") == 0) mods |= MOD_ALT; + else if (strcasecmp(toks[i], "super") == 0 || strcasecmp(toks[i], "meta") == 0 || strcasecmp(toks[i], "win") == 0) mods |= MOD_SUPER; + else { + /* unknown modifier -> fail */ + for (int j = 0; j < n; j++) free(toks[j]); + free(toks); + free(copy); + return -1; + } + free(toks[i]); + } + *out_mods = mods; + *out_key = toks[n - 1]; + /* free remaining token strings except key (transferred) */ + for (int i = 0; i < n - 1; i++) { /* already freed */ } + free(toks); + free(copy); + return 0; +} + +action_t config_parse_action(const char *s) { + if (!s) return ACTION_NONE; + char buf[128]; + snprintf(buf, sizeof(buf), "%s", s); + trim(buf); + if (strcmp(buf, "quit") == 0) return ACTION_QUIT; + if (strcmp(buf, "cancel") == 0) return ACTION_CANCEL; + if (strcmp(buf, "rotate") == 0) return ACTION_ROTATE; + if (strcmp(buf, "clear") == 0) return ACTION_CLEAR; + if (strcmp(buf, "pause") == 0) return ACTION_PAUSE; + if (strcmp(buf, "step") == 0) return ACTION_STEP; + if (strcmp(buf, "speed_up") == 0) return ACTION_SPEED_UP; + if (strcmp(buf, "speed_down") == 0) return ACTION_SPEED_DOWN; + if (strcmp(buf, "toggle_grid") == 0) return ACTION_TOGGLE_GRID; + if (strncmp(buf, "select:", 7) == 0) { + const char *pat = buf + 7; + if (strcmp(pat, "glider") == 0) return ACTION_SELECT_GLIDER; + if (strcmp(pat, "blinker") == 0) return ACTION_SELECT_BLINKER; + if (strcmp(pat, "block") == 0) return ACTION_SELECT_BLOCK; + if (strcmp(pat, "toad") == 0) return ACTION_SELECT_TOAD; + } + return ACTION_NONE; +} + +config_t config_defaults(void) { + config_t cfg = {0}; + struct { const char *key; action_t action; } defs[] = { + {"1", ACTION_SELECT_GLIDER}, + {"2", ACTION_SELECT_BLINKER}, + {"3", ACTION_SELECT_BLOCK}, + {"4", ACTION_SELECT_TOAD}, + {"space", ACTION_ROTATE}, + {"escape", ACTION_CANCEL}, + {"ctrl+q", ACTION_QUIT}, + }; + int ndef = sizeof(defs) / sizeof(defs[0]); + for (int i = 0; i < ndef; i++) { + uint32_t mods = 0; + char *key = NULL; + if (config_parse_mods(defs[i].key, &mods, &key) != 0) continue; + xkb_keysym_t sym = xkb_keysym_from_name(key, XKB_KEYSYM_CASE_INSENSITIVE); + free(key); + if (sym == XKB_KEY_NoSymbol) continue; + config_set_binding(&cfg, mods, sym, defs[i].action); + } + return cfg; +} + +static const char *default_file_content = + "# wlgol key bindings\n" + "[bindings]\n" + "\"1\" = \"select:glider\"\n" + "\"2\" = \"select:blinker\"\n" + "\"3\" = \"select:block\"\n" + "\"4\" = \"select:toad\"\n" + "\"space\" = \"rotate\"\n" + "\"escape\" = \"cancel\"\n" + "\"ctrl+q\" = \"quit\"\n"; + +int config_ensure_default_file(const char *path) { + if (!path) return -1; + struct stat st; + if (stat(path, &st) == 0) return 0; + char *parent = strdup(path); + char *grand = strdup(path); + dirname(parent); + /* parent of parent */ + char *p2 = strdup(parent); + dirname(p2); + mkdir(p2, 0755); + mkdir(parent, 0755); + free(p2); + + FILE *fp = fopen(path, "w"); + if (!fp) { free(parent); free(grand); return -1; } + fputs(default_file_content, fp); + fclose(fp); + free(parent); + free(grand); + return 0; +} + +config_t config_load(const char *path) { + config_t cfg = config_defaults(); + config_ensure_default_file(path); + FILE *fp = fopen(path, "r"); + if (!fp) return cfg; + char errbuf[512]; + toml_table_t *tab = toml_parse_file(fp, errbuf, sizeof(errbuf)); + fclose(fp); + if (!tab) return cfg; + toml_table_t *bindings = toml_table_in(tab, "bindings"); + if (bindings) { + int n = toml_table_nkval(bindings); + for (int i = 0; i < n; i++) { + const char *keystr = toml_key_in(bindings, i); + if (!keystr) break; + toml_datum_t d = toml_string_in(bindings, keystr); + if (!d.ok) { if (d.u.s) free(d.u.s); continue; } + uint32_t mods = 0; + char *key = NULL; + if (config_parse_mods(keystr, &mods, &key) != 0) { free(d.u.s); continue; } + xkb_keysym_t sym = xkb_keysym_from_name(key, XKB_KEYSYM_CASE_INSENSITIVE); + free(key); + if (sym == XKB_KEY_NoSymbol) { free(d.u.s); continue; } + action_t act = config_parse_action(d.u.s); + free(d.u.s); + if (act == ACTION_NONE) continue; + config_set_binding(&cfg, mods, sym, act); + } + } + toml_free(tab); + return cfg; +} + +void config_free(config_t *cfg) { + if (!cfg) return; + free(cfg->items); + cfg->items = NULL; + cfg->count = 0; + cfg->capacity = 0; +} + +action_t config_lookup(const config_t *cfg, uint32_t mods, xkb_keysym_t keysym) { + if (!cfg) return ACTION_NONE; + for (int i = 0; i < cfg->count; i++) { + if (cfg->items[i].mods == mods && cfg->items[i].keysym == keysym) { + return cfg->items[i].action; + } + } + return ACTION_NONE; +} \ No newline at end of file diff --git a/fractional-scale-v1.xml b/fractional-scale-v1.xml new file mode 100644 index 0000000..350bfc0 --- /dev/null +++ b/fractional-scale-v1.xml @@ -0,0 +1,102 @@ + + + + Copyright © 2022 Kenny Levinsen + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows a compositor to suggest for surfaces to render at + fractional scales. + + A client can submit scaled content by utilizing wp_viewport. This is done by + creating a wp_viewport object for the surface and setting the destination + rectangle to the surface size before the scale factor is applied. + + The buffer size is calculated by multiplying the surface size by the + intended scale. + + The wl_surface buffer scale should remain set to 1. + + If a surface has a surface-local size of 100 px by 50 px and wishes to + submit buffers with a scale of 1.5, then a buffer of 150px by 75 px should + be used and the wp_viewport destination rectangle should be 100 px by 50 px. + + For toplevel surfaces, the size is rounded halfway away from zero. The + rounding algorithm for subsurface position and size is not defined. + + + + + A global interface for requesting surfaces to use fractional scales. + + + + + Informs the server that the client will not be using this protocol + object anymore. This does not affect any other objects, + wp_fractional_scale_v1 objects included. + + + + + + + + + + Create an add-on object for the the wl_surface to let the compositor + request fractional scales. If the given wl_surface already has a + wp_fractional_scale_v1 object associated, the fractional_scale_exists + protocol error is raised. + + + + + + + + + An additional interface to a wl_surface object which allows the compositor + to inform the client of the preferred scale. + + + + + Destroy the fractional scale object. When this object is destroyed, + preferred_scale events will no longer be sent. + + + + + + Notification of a new preferred scale for this surface that the + compositor suggests that the client should use. + + The sent scale is the numerator of a fraction with a denominator of 120. + + + + + diff --git a/grid.c b/grid.c new file mode 100644 index 0000000..f1c2410 --- /dev/null +++ b/grid.c @@ -0,0 +1,58 @@ +#include "grid.h" + +#include +#include + +void grid_init(grid_t *g, int cols, int rows) { + if (!g) return; + if (cols < 1) cols = 1; + if (rows < 1) rows = 1; + g->cols = cols; + g->rows = rows; + g->cells = calloc((size_t)cols * (size_t)rows, 1); + g->show_lines = 1; +} + +void grid_free(grid_t *g) { + if (!g) return; + free(g->cells); + g->cells = NULL; + g->cols = 0; + g->rows = 0; +} + +void grid_resize(grid_t *g, int new_cols, int new_rows) { + if (!g) return; + if (new_cols < 1) new_cols = 1; + if (new_rows < 1) new_rows = 1; + unsigned char *ncells = calloc((size_t)new_cols * (size_t)new_rows, 1); + if (!ncells) return; + int copy_cols = g->cols < new_cols ? g->cols : new_cols; + int copy_rows = g->rows < new_rows ? g->rows : new_rows; + for (int r = 0; r < copy_rows; r++) { + memcpy(ncells + (size_t)r * new_cols, + g->cells + (size_t)r * g->cols, + (size_t)copy_cols); + } + free(g->cells); + g->cells = ncells; + g->cols = new_cols; + g->rows = new_rows; +} + +void grid_clear(grid_t *g) { + if (!g || !g->cells) return; + memset(g->cells, 0, (size_t)g->cols * (size_t)g->rows); +} + +int grid_get(const grid_t *g, int c, int r) { + if (!g || !g->cells) return 0; + if (c < 0 || r < 0 || c >= g->cols || r >= g->rows) return 0; + return g->cells[(size_t)r * g->cols + c]; +} + +void grid_set(grid_t *g, int c, int r, int val) { + if (!g || !g->cells) return; + if (c < 0 || r < 0 || c >= g->cols || r >= g->rows) return; + g->cells[(size_t)r * g->cols + c] = val ? 1 : 0; +} \ No newline at end of file diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..7181fa5 --- /dev/null +++ b/include/config.h @@ -0,0 +1,59 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include + +#define MOD_CTRL (1u << 0) +#define MOD_SHIFT (1u << 1) +#define MOD_ALT (1u << 2) +#define MOD_SUPER (1u << 3) + +typedef enum { + ACTION_NONE = 0, + ACTION_QUIT, + ACTION_CANCEL, + ACTION_ROTATE, + ACTION_SELECT_GLIDER, + ACTION_SELECT_BLINKER, + ACTION_SELECT_BLOCK, + ACTION_SELECT_TOAD, + ACTION_CLEAR, + ACTION_PAUSE, + ACTION_STEP, + ACTION_SPEED_UP, + ACTION_SPEED_DOWN, + ACTION_TOGGLE_GRID, +} action_t; + +typedef struct { + uint32_t mods; + xkb_keysym_t keysym; + action_t action; +} binding_t; + +typedef struct { + binding_t *items; + int count; + int capacity; +} config_t; + +/* Built-in defaults. */ +config_t config_defaults(void); + +/* Ensure config file exists; create with defaults if missing. Returns 0 on success. */ +int config_ensure_default_file(const char *path); + +/* Load config from path, merging over defaults. */ +config_t config_load(const char *path); + +void config_free(config_t *cfg); + +/* Lookup action for a key press. Returns ACTION_NONE if no binding. */ +action_t config_lookup(const config_t *cfg, uint32_t mods, xkb_keysym_t keysym); + +/* Helpers exposed for tests. */ +int config_parse_mods(const char *s, uint32_t *out_mods, char **out_key); +action_t config_parse_action(const char *s); + +#endif \ No newline at end of file diff --git a/include/grid.h b/include/grid.h new file mode 100644 index 0000000..cec91c2 --- /dev/null +++ b/include/grid.h @@ -0,0 +1,18 @@ +#ifndef GRID_H +#define GRID_H + +typedef struct { + int cols; + int rows; + unsigned char *cells; + int show_lines; +} grid_t; + +void grid_init(grid_t *g, int cols, int rows); +void grid_free(grid_t *g); +void grid_resize(grid_t *g, int new_cols, int new_rows); +void grid_clear(grid_t *g); +int grid_get(const grid_t *g, int c, int r); +void grid_set(grid_t *g, int c, int r, int val); + +#endif \ No newline at end of file diff --git a/include/pattern.h b/include/pattern.h new file mode 100644 index 0000000..61e1079 --- /dev/null +++ b/include/pattern.h @@ -0,0 +1,21 @@ +#ifndef PATTERN_H +#define PATTERN_H + +typedef struct { int dx, dy; } cell_offset_t; + +typedef struct { + const char *name; + int count; + const cell_offset_t *cells; +} pattern_t; + +int pattern_count(void); +const pattern_t *pattern_get(int index); + +/* Fill out_dx/out_dy with normalized rotated offsets. Returns cell count. */ +int pattern_rotated_offsets(int index, int rotation, int *out_dx, int *out_dy); + +/* Bounding box of rotated pattern. */ +void pattern_bounds(int index, int rotation, int *w, int *h); + +#endif \ No newline at end of file diff --git a/include/tkeyboard.h b/include/tkeyboard.h index 8f3870b..08f7db6 100644 --- a/include/tkeyboard.h +++ b/include/tkeyboard.h @@ -5,7 +5,7 @@ #include #include -typedef void (*keyboard_key_handler_t)(uint32_t key, uint32_t state, xkb_keysym_t sym, void *user_data); +typedef void (*keyboard_key_handler_t)(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t mods, void *user_data); struct keyboard_data { struct wl_keyboard *keyboard; diff --git a/include/toml.h b/include/toml.h new file mode 100644 index 0000000..c6aabd0 --- /dev/null +++ b/include/toml.h @@ -0,0 +1,175 @@ +/* + MIT License + + Copyright (c) CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef TOML_H +#define TOML_H + +#ifdef _MSC_VER +#pragma warning(disable : 4996) +#endif + +#include +#include + +#ifdef __cplusplus +#define TOML_EXTERN extern "C" +#else +#define TOML_EXTERN extern +#endif + +typedef struct toml_timestamp_t toml_timestamp_t; +typedef struct toml_table_t toml_table_t; +typedef struct toml_array_t toml_array_t; +typedef struct toml_datum_t toml_datum_t; + +/* Parse a file. Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t *toml_parse_file(FILE *fp, char *errbuf, int errbufsz); + +/* Parse a string containing the full config. + * Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t *toml_parse(char *conf, /* NUL terminated, please. */ + char *errbuf, int errbufsz); + +/* Free the table returned by toml_parse() or toml_parse_file(). Once + * this function is called, any handles accessed through this tab + * directly or indirectly are no longer valid. + */ +TOML_EXTERN void toml_free(toml_table_t *tab); + +/* Timestamp types. The year, month, day, hour, minute, second, z + * fields may be NULL if they are not relevant. e.g. In a DATE + * type, the hour, minute, second and z fields will be NULLs. + */ +struct toml_timestamp_t { + struct { /* internal. do not use. */ + int year, month, day; + int hour, minute, second, millisec; + char z[10]; + } __buffer; + int *year, *month, *day; + int *hour, *minute, *second, *millisec; + char *z; +}; + +/*----------------------------------------------------------------- + * Enhanced access methods + */ +struct toml_datum_t { + int ok; + union { + toml_timestamp_t *ts; /* ts must be freed after use */ + char *s; /* string value. s must be freed after use */ + int b; /* bool value */ + int64_t i; /* int value */ + double d; /* double value */ + } u; +}; + +/* on arrays: */ +/* ... retrieve size of array. */ +TOML_EXTERN int toml_array_nelem(const toml_array_t *arr); +/* ... retrieve values using index. */ +TOML_EXTERN toml_datum_t toml_string_at(const toml_array_t *arr, int idx); +TOML_EXTERN toml_datum_t toml_bool_at(const toml_array_t *arr, int idx); +TOML_EXTERN toml_datum_t toml_int_at(const toml_array_t *arr, int idx); +TOML_EXTERN toml_datum_t toml_double_at(const toml_array_t *arr, int idx); +TOML_EXTERN toml_datum_t toml_timestamp_at(const toml_array_t *arr, int idx); +/* ... retrieve array or table using index. */ +TOML_EXTERN toml_array_t *toml_array_at(const toml_array_t *arr, int idx); +TOML_EXTERN toml_table_t *toml_table_at(const toml_array_t *arr, int idx); + +/* on tables: */ +/* ... retrieve the key in table at keyidx. Return 0 if out of range. */ +TOML_EXTERN const char *toml_key_in(const toml_table_t *tab, int keyidx); +/* ... returns 1 if key exists in tab, 0 otherwise */ +TOML_EXTERN int toml_key_exists(const toml_table_t *tab, const char *key); +/* ... retrieve values using key. */ +TOML_EXTERN toml_datum_t toml_string_in(const toml_table_t *arr, + const char *key); +TOML_EXTERN toml_datum_t toml_bool_in(const toml_table_t *arr, const char *key); +TOML_EXTERN toml_datum_t toml_int_in(const toml_table_t *arr, const char *key); +TOML_EXTERN toml_datum_t toml_double_in(const toml_table_t *arr, + const char *key); +TOML_EXTERN toml_datum_t toml_timestamp_in(const toml_table_t *arr, + const char *key); +/* .. retrieve array or table using key. */ +TOML_EXTERN toml_array_t *toml_array_in(const toml_table_t *tab, + const char *key); +TOML_EXTERN toml_table_t *toml_table_in(const toml_table_t *tab, + const char *key); + +/*----------------------------------------------------------------- + * lesser used + */ +/* Return the array kind: 't'able, 'a'rray, 'v'alue, 'm'ixed */ +TOML_EXTERN char toml_array_kind(const toml_array_t *arr); + +/* For array kind 'v'alue, return the type of values + i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp, 'm'ixed + 0 if unknown +*/ +TOML_EXTERN char toml_array_type(const toml_array_t *arr); + +/* Return the key of an array */ +TOML_EXTERN const char *toml_array_key(const toml_array_t *arr); + +/* Return the number of key-values in a table */ +TOML_EXTERN int toml_table_nkval(const toml_table_t *tab); + +/* Return the number of arrays in a table */ +TOML_EXTERN int toml_table_narr(const toml_table_t *tab); + +/* Return the number of sub-tables in a table */ +TOML_EXTERN int toml_table_ntab(const toml_table_t *tab); + +/* Return the key of a table*/ +TOML_EXTERN const char *toml_table_key(const toml_table_t *tab); + +/*-------------------------------------------------------------- + * misc + */ +TOML_EXTERN int toml_utf8_to_ucs(const char *orig, int len, int64_t *ret); +TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]); +TOML_EXTERN void toml_set_memutil(void *(*xxmalloc)(size_t), + void (*xxfree)(void *)); + +/*-------------------------------------------------------------- + * deprecated + */ +/* A raw value, must be processed by toml_rto* before using. */ +typedef const char *toml_raw_t; +TOML_EXTERN toml_raw_t toml_raw_in(const toml_table_t *tab, const char *key); +TOML_EXTERN toml_raw_t toml_raw_at(const toml_array_t *arr, int idx); +TOML_EXTERN int toml_rtos(toml_raw_t s, char **ret); +TOML_EXTERN int toml_rtob(toml_raw_t s, int *ret); +TOML_EXTERN int toml_rtoi(toml_raw_t s, int64_t *ret); +TOML_EXTERN int toml_rtod(toml_raw_t s, double *ret); +TOML_EXTERN int toml_rtod_ex(toml_raw_t s, double *ret, char *buf, int buflen); +TOML_EXTERN int toml_rtots(toml_raw_t s, toml_timestamp_t *ret); + +#endif /* TOML_H */ diff --git a/include/tpointer.h b/include/tpointer.h index 694aeb7..d2c188b 100644 --- a/include/tpointer.h +++ b/include/tpointer.h @@ -5,6 +5,8 @@ #include +typedef void (*pointer_button_handler_t)(uint32_t button, uint32_t state, double x, double y, void *user_data); + struct pointer_data { struct wl_cursor_theme *cursor_theme; struct wl_pointer *pointer; @@ -12,10 +14,18 @@ struct pointer_data { struct wl_cursor *cursor; struct wl_cursor_image *cursor_image; struct wl_buffer *cursor_buffer; + + double x; + double y; + + pointer_button_handler_t button_handler; + void *user_data; }; extern const struct wl_pointer_listener pointer_listener; void pointer_init_cursor(struct pointer_data *pointer_data, struct wl_compositor *compositor, struct wl_shm *shm); +void pointer_set_button_handler(struct pointer_data *pointer_data, pointer_button_handler_t handler, void *user_data); + #endif diff --git a/openspec/changes/input-and-placement/.openspec.yaml b/openspec/changes/archive/2026-11-08-input-and-placement/.openspec.yaml similarity index 100% rename from openspec/changes/input-and-placement/.openspec.yaml rename to openspec/changes/archive/2026-11-08-input-and-placement/.openspec.yaml diff --git a/openspec/changes/input-and-placement/design.md b/openspec/changes/archive/2026-11-08-input-and-placement/design.md similarity index 100% rename from openspec/changes/input-and-placement/design.md rename to openspec/changes/archive/2026-11-08-input-and-placement/design.md diff --git a/openspec/changes/input-and-placement/proposal.md b/openspec/changes/archive/2026-11-08-input-and-placement/proposal.md similarity index 100% rename from openspec/changes/input-and-placement/proposal.md rename to openspec/changes/archive/2026-11-08-input-and-placement/proposal.md diff --git a/openspec/changes/input-and-placement/specs/cell-grid/spec.md b/openspec/changes/archive/2026-11-08-input-and-placement/specs/cell-grid/spec.md similarity index 100% rename from openspec/changes/input-and-placement/specs/cell-grid/spec.md rename to openspec/changes/archive/2026-11-08-input-and-placement/specs/cell-grid/spec.md diff --git a/openspec/changes/input-and-placement/specs/display-scaling/spec.md b/openspec/changes/archive/2026-11-08-input-and-placement/specs/display-scaling/spec.md similarity index 100% rename from openspec/changes/input-and-placement/specs/display-scaling/spec.md rename to openspec/changes/archive/2026-11-08-input-and-placement/specs/display-scaling/spec.md diff --git a/openspec/changes/input-and-placement/specs/keyboard-bindings/spec.md b/openspec/changes/archive/2026-11-08-input-and-placement/specs/keyboard-bindings/spec.md similarity index 100% rename from openspec/changes/input-and-placement/specs/keyboard-bindings/spec.md rename to openspec/changes/archive/2026-11-08-input-and-placement/specs/keyboard-bindings/spec.md diff --git a/openspec/changes/input-and-placement/specs/pattern-placement/spec.md b/openspec/changes/archive/2026-11-08-input-and-placement/specs/pattern-placement/spec.md similarity index 100% rename from openspec/changes/input-and-placement/specs/pattern-placement/spec.md rename to openspec/changes/archive/2026-11-08-input-and-placement/specs/pattern-placement/spec.md diff --git a/openspec/changes/input-and-placement/tasks.md b/openspec/changes/archive/2026-11-08-input-and-placement/tasks.md similarity index 51% rename from openspec/changes/input-and-placement/tasks.md rename to openspec/changes/archive/2026-11-08-input-and-placement/tasks.md index 92923a3..ee3c640 100644 --- a/openspec/changes/input-and-placement/tasks.md +++ b/openspec/changes/archive/2026-11-08-input-and-placement/tasks.md @@ -2,46 +2,46 @@ ## 1. Protocol generation and build setup -- [ ] 1.1 Vendor `viewporter.xml` and `fractional-scale-v1.xml` into the repo and add wayland-scanner custom commands in `CMakeLists.txt`, verifying headers generate under `build/include`. -- [ ] 1.2 Add `tomlc99.c`/`tomlc99.h` to the source tree and CMake sources, verifying the project still configures and builds. +- [x] 1.1 Vendor `viewporter.xml` and `fractional-scale-v1.xml` into the repo and add wayland-scanner custom commands in `CMakeLists.txt`, verifying headers generate under `build/include`. +- [x] 1.2 Add `tomlc99.c`/`tomlc99.h` (vendored as toml.c/toml.h) to the source tree and CMake sources, verifying the project still configures and builds. ## 2. Configuration module -- [ ] 2.1 Implement `config.c/h` to load or auto-create `~/.config/wlgol/keys.conf`, parse TOML bindings, and merge with built-in defaults, verifying a sample file round-trips. -- [ ] 2.2 Implement binding string parsing (modifiers + key aliases) and lookup by modifier mask and keysym, verifying unit tests cover merge and modifier matching. +- [x] 2.1 Implement `config.c/h` to load or auto-create `~/.config/wlgol/keys.conf`, parse TOML bindings, and merge with built-in defaults, verifying a sample file round-trips. +- [x] 2.2 Implement binding string parsing (modifiers + key aliases) and lookup by modifier mask and keysym, verifying unit tests cover merge and modifier matching. ## 3. Keyboard module -- [ ] 3.1 Extend `keyboard_key_handler_t` to pass a modifier mask and update `tkeyboard.c` to compute it from xkb state, verifying compilation of all callers. -- [ ] 3.2 Wire keyboard events to config-based binding lookup and action dispatch, verifying Escape and Ctrl+Q trigger their actions. +- [x] 3.1 Extend `keyboard_key_handler_t` to pass a modifier mask and update `tkeyboard.c` to compute it from xkb state, verifying compilation of all callers. +- [x] 3.2 Wire keyboard events to config-based binding lookup and action dispatch, verifying Escape and Ctrl+Q trigger their actions. ## 4. Pointer module -- [ ] 4.1 Track logical cursor position in `tpointer.c/h` from motion events, verifying position updates on move. -- [ ] 4.2 Dispatch left-click events to the placement controller, verifying clicks are received. +- [x] 4.1 Track logical cursor position in `tpointer.c/h` from motion events, verifying position updates on move. +- [x] 4.2 Dispatch left-click events to the placement controller, verifying clicks are received. ## 5. Grid module -- [ ] 5.1 Implement `grid.c/h` with storage, bounds checks, resize-with-preserve, clear, and toggle state, verifying unit tests for resize and bounds. -- [ ] 5.2 Expose grid dimensions derived from logical surface size at 8 px per cell, verifying dimensions update on resize. +- [x] 5.1 Implement `grid.c/h` with storage, bounds checks, resize-with-preserve, clear, and toggle state, verifying unit tests for resize and bounds. +- [x] 5.2 Expose grid dimensions derived from logical surface size at 8 px per cell, verifying dimensions update on resize. ## 6. Pattern module -- [ ] 6.1 Implement `pattern.c/h` with Glider, Blinker, Block, and Toad offsets and 90-degree rotation transform, verifying rotated offsets and bounds. -- [ ] 6.2 Implement placement validity check (pattern fits within grid), verifying in-bounds and out-of-bounds cases. +- [x] 6.1 Implement `pattern.c/h` with Glider, Blinker, Block, and Toad offsets and 90-degree rotation transform, verifying rotated offsets and bounds. +- [x] 6.2 Implement placement validity check (pattern fits within grid), verifying in-bounds and out-of-bounds cases. ## 7. Scaling module -- [ ] 7.1 Implement `scale.c/h` to bind fractional scale and viewporter, compute physical buffer size, and convert coordinates, verifying scale changes update buffer size. -- [ ] 7.2 Implement integer-scale fallback via `wl_output`, verifying fallback path builds without fractional protocol. +- [x] 7.1 Implement scaling (inlined in wlgol.c) to bind fractional scale and viewporter, compute physical buffer size, and convert coordinates, verifying scale changes update buffer size. +- [x] 7.2 Implement integer-scale fallback via `wl_output`, verifying fallback path builds without fractional protocol. ## 8. Rendering and application integration -- [ ] 8.1 Refactor `wlgol.c` to use logical surface size, create scaled shm buffers, and render grid plus alive cells, verifying build succeeds. -- [ ] 8.2 Add placement controller state machine (IDLE/ARMED), preview rendering, rotation, click placement, and cancellation, verifying behavior by manual run. -- [ ] 8.3 Integrate config loading, default file creation, and action dispatch into startup, verifying default config is generated. +- [x] 8.1 Refactor `wlgol.c` to use logical surface size, create scaled shm buffers, and render grid plus alive cells, verifying build succeeds. +- [x] 8.2 Add placement controller state machine (IDLE/ARMED), preview rendering, rotation, click placement, and cancellation, verifying behavior by manual run. +- [x] 8.3 Integrate config loading, default file creation, and action dispatch into startup, verifying default config is generated. ## 9. Verification -- [ ] 9.1 Build with CMake/Ninja and run `ctest --test-dir build`, verifying all tests pass. -- [ ] 9.2 Run `./build/wlgol` under a Wayland compositor and manually verify placement, rotation, HiDPI scaling, and Esc cancellation. \ No newline at end of file +- [x] 9.1 Build with CMake/Ninja and run `ctest --test-dir build`, verifying all tests pass. +- [x] 9.2 Run `./build/wlgol` under a Wayland compositor and manually verify placement, rotation, HiDPI scaling, and Esc cancellation. \ No newline at end of file diff --git a/openspec/specs/cell-grid/spec.md b/openspec/specs/cell-grid/spec.md new file mode 100644 index 0000000..d10d8bb --- /dev/null +++ b/openspec/specs/cell-grid/spec.md @@ -0,0 +1,61 @@ +# cell-grid Specification + +## Purpose +Stores and renders the Game of Life cell grid with a fixed cell size and wall boundaries, independent of future simulation rules. + +## Requirements + +### Requirement: Grid stores alive and dead cells +The system SHALL maintain a grid of cells, each either alive or dead, addressable by column and row. + +#### Scenario: Cell access +- **WHEN** a cell is set to alive +- **THEN** reading that cell returns alive +- **AND** cells never set remain dead + +### Requirement: Boundary cells are treated as dead +The system SHALL treat any cell outside the grid bounds as dead and shall not store state outside the grid. + +#### Scenario: Out of bounds read +- **WHEN** a cell position outside the grid is queried +- **THEN** the result is dead + +### Requirement: Grid resizes while preserving overlapping cells +The system SHALL preserve cells that remain within bounds when the grid dimensions change, clearing only cells that fall outside the new bounds. + +#### Scenario: Enlarge grid +- **WHEN** the grid grows in either dimension +- **THEN** previously alive cells within the old bounds remain alive + +#### Scenario: Shrink grid +- **WHEN** the grid shrinks in either dimension +- **THEN** cells outside the new bounds are discarded +- **AND** remaining cells keep their state + +### Requirement: Cell size is fixed in logical pixels +The system SHALL use a cell size of 8 logical pixels, so grid dimensions derive from the logical surface size. + +#### Scenario: Dimension calculation +- **WHEN** the logical surface is 400 pixels wide +- **THEN** the grid has 50 columns + +### Requirement: Alive cells are rendered +The system SHALL render alive cells distinctly from dead cells. + +#### Scenario: Render alive cell +- **WHEN** a cell is alive +- **THEN** it is drawn with the alive cell color + +### Requirement: Grid can be cleared +The system SHALL set all cells to dead when the clear action is triggered. + +#### Scenario: Clear grid +- **WHEN** the user triggers the clear action +- **THEN** all cells become dead + +### Requirement: Grid lines can be toggled +The system SHALL show or hide grid lines when the toggle_grid action is triggered. + +#### Scenario: Toggle lines +- **WHEN** the user triggers the toggle_grid action +- **THEN** grid line visibility flips diff --git a/openspec/specs/display-scaling/spec.md b/openspec/specs/display-scaling/spec.md new file mode 100644 index 0000000..0a621b5 --- /dev/null +++ b/openspec/specs/display-scaling/spec.md @@ -0,0 +1,44 @@ +# display-scaling Specification + +## Purpose +Keeps the application rendering crisp on displays with integer or fractional scaling by converting between logical and physical coordinates. + +## Requirements + +### Requirement: Fractional scale is applied when available +The system SHALL use the compositor-provided fractional preferred scale for rendering when the fractional scale protocol is available. + +#### Scenario: Fractional display +- **WHEN** the compositor sends a preferred scale of 1.5 +- **THEN** the render buffer is created at 1.5 times the logical size +- **AND** the buffer is displayed at the logical size + +### Requirement: Integer scale is used as fallback +The system SHALL use the output's integer scale when fractional scale is unavailable. + +#### Scenario: No fractional protocol +- **WHEN** the compositor does not support fractional scaling +- **THEN** the render buffer uses the output integer scale +- **AND** the buffer is displayed at the logical size + +### Requirement: Input coordinates remain logical +The system SHALL report mouse positions and grid cells in logical coordinates regardless of the active scale. + +#### Scenario: Scaled click +- **WHEN** the user clicks on a scaled display +- **THEN** the selected grid cell matches the visual location of the cursor + +### Requirement: Rendering uses physical resolution +The system SHALL render all content at physical resolution so that lines and cells stay sharp at the active scale. + +#### Scenario: Sharp cells +- **WHEN** the application runs on a 2x scaled display +- **THEN** cells are drawn with 2 physical pixels per logical pixel + +### Requirement: Scale changes trigger re-render +The system SHALL recreate buffers and re-render when the active scale changes. + +#### Scenario: Scale change +- **WHEN** the compositor changes the preferred scale +- **THEN** the application updates its buffers +- **AND** subsequent frames use the new scale diff --git a/openspec/specs/keyboard-bindings/spec.md b/openspec/specs/keyboard-bindings/spec.md new file mode 100644 index 0000000..6283d62 --- /dev/null +++ b/openspec/specs/keyboard-bindings/spec.md @@ -0,0 +1,59 @@ +# keyboard-bindings Specification + +## Purpose +Allows users to configure keyboard shortcuts that map key combinations to application actions, with sensible built-in defaults when no configuration file exists. + +## Requirements + +### Requirement: Default bindings are available without configuration +The system SHALL provide built-in default bindings for all documented actions, including digits 1-4 selecting the Glider, Blinker, Block, and Toad patterns, Space rotating, Escape cancelling, and Ctrl+Q quitting. + +#### Scenario: No config file present +- **WHEN** the application starts and `~/.config/wlgol/keys.conf` does not exist +- **THEN** the application uses the built-in default bindings +- **AND** creates the config file with the default contents + +### Requirement: Configuration file overrides defaults by merge +The system SHALL read `~/.config/wlgol/keys.conf` as TOML when it exists and merge its entries over the built-in defaults, so only specified keys change. + +#### Scenario: Partial override +- **WHEN** the config file defines a binding for key "1" only +- **THEN** key "1" uses the configured action +- **AND** all other keys keep their default bindings + +#### Scenario: Full replacement of a key +- **WHEN** the config file defines a binding for a key that has a default +- **THEN** the configured action replaces the default for that key + +### Requirement: Key combinations are modifier-aware +The system SHALL match a binding only when the modifiers required by the binding are active, supporting Ctrl, Shift, Alt, and Super. + +#### Scenario: Modifier required +- **WHEN** the user presses Q while Ctrl is held and a `ctrl+q = quit` binding exists +- **THEN** the quit action is triggered + +#### Scenario: Modifier not held +- **WHEN** the user presses Q without Ctrl while a `ctrl+q = quit` binding exists +- **THEN** the quit action is not triggered by that binding + +### Requirement: Readable key aliases are supported +The system SHALL accept human-readable key names such as space, escape, enter, and arrow names in binding keys. + +#### Scenario: Alias binding +- **WHEN** the config file contains `space = rotate` +- **THEN** pressing the space bar triggers the rotate action + +### Requirement: Malformed entries are ignored +The system SHALL ignore malformed or unrecognized binding entries and continue using defaults for the affected keys. + +#### Scenario: Invalid entry +- **WHEN** the config file contains an entry with an unknown action +- **THEN** that entry is ignored +- **AND** the application still starts successfully + +### Requirement: Actions trigger on key press +The system SHALL dispatch a bound action when the corresponding key is pressed, not on release. + +#### Scenario: Press triggers action +- **WHEN** the user presses a key with a valid binding +- **THEN** the bound action is invoked once for that press diff --git a/openspec/specs/pattern-placement/spec.md b/openspec/specs/pattern-placement/spec.md new file mode 100644 index 0000000..9bfa11f --- /dev/null +++ b/openspec/specs/pattern-placement/spec.md @@ -0,0 +1,60 @@ +# pattern-placement Specification + +## Purpose +Lets users select a Game of Life pattern, preview it under the mouse cursor, rotate it, and place copies on the grid with a click. + +## Requirements + +### Requirement: Selecting a pattern arms a placement tool +The system SHALL activate a placement tool for the selected pattern when a `select:` action is triggered, and show a preview following the cursor. + +#### Scenario: Tool activation +- **WHEN** the user triggers `select:glider` +- **THEN** a glider preview appears at the current cursor position +- **AND** the placement tool remains active + +### Requirement: Preview follows cursor motion +The system SHALL update the preview position to track the current mouse cursor cell while a tool is active. + +#### Scenario: Mouse moves +- **WHEN** the user moves the mouse while a tool is active +- **THEN** the preview moves to the cell under the cursor + +### Requirement: Tool can be rotated in 90-degree steps +The system SHALL rotate the active preview by 90 degrees each time the rotate action is triggered. + +#### Scenario: Rotate preview +- **WHEN** the user triggers the rotate action while a tool is active +- **THEN** the preview rotates 90 degrees clockwise +- **AND** remains at the same anchor cell + +### Requirement: Left click places the pattern when valid +The system SHALL write the active pattern's alive cells into the grid at the preview position when the user left-clicks and the pattern fits entirely within the grid. + +#### Scenario: Valid placement +- **WHEN** the user left-clicks while a tool is active and the preview fits in the grid +- **THEN** the pattern's cells become alive in the grid +- **AND** the tool remains active for further placement + +### Requirement: Invalid placement is rejected +The system SHALL not modify the grid when a left click occurs while the preview extends outside the grid. + +#### Scenario: Out of bounds +- **WHEN** the user left-clicks while the preview extends outside the grid +- **THEN** no cells are changed +- **AND** the tool remains active + +### Requirement: Escape cancels the active tool +The system SHALL deactivate the placement tool and remove the preview when the cancel action is triggered. + +#### Scenario: Cancel placement +- **WHEN** the user triggers the cancel action while a tool is active +- **THEN** the preview disappears +- **AND** no further clicks place a pattern until a tool is selected again + +### Requirement: Click without active tool does nothing +The system SHALL ignore left clicks when no placement tool is active. + +#### Scenario: No tool +- **WHEN** the user left-clicks while no tool is active +- **THEN** the grid is unchanged diff --git a/pattern.c b/pattern.c new file mode 100644 index 0000000..8809ae6 --- /dev/null +++ b/pattern.c @@ -0,0 +1,68 @@ +#include "pattern.h" + +#include +#include + +static const cell_offset_t glider_cells[] = { + {1,0},{2,1},{0,2},{1,2},{2,2} +}; +static const cell_offset_t blinker_cells[] = { + {0,0},{1,0},{2,0} +}; +static const cell_offset_t block_cells[] = { + {0,0},{1,0},{0,1},{1,1} +}; +static const cell_offset_t toad_cells[] = { + {1,0},{2,0},{3,0},{0,1},{1,1},{2,1} +}; + +static const pattern_t patterns[] = { + {"glider", 5, glider_cells}, + {"blinker", 3, blinker_cells}, + {"block", 4, block_cells}, + {"toad", 6, toad_cells}, +}; + +int pattern_count(void) { return 4; } + +const pattern_t *pattern_get(int index) { + if (index < 0 || index >= pattern_count()) return NULL; + return &patterns[index]; +} + +int pattern_rotated_offsets(int index, int rotation, int *out_dx, int *out_dy) { + const pattern_t *p = pattern_get(index); + if (!p) return 0; + rotation = ((rotation % 4) + 4) % 4; + int minx = INT_MAX, miny = INT_MAX; + int tx[16], ty[16]; + for (int i = 0; i < p->count; i++) { + int x = p->cells[i].dx; + int y = p->cells[i].dy; + for (int r = 0; r < rotation; r++) { + int nx = y; + int ny = -x; + x = nx; y = ny; + } + tx[i] = x; ty[i] = y; + if (x < minx) minx = x; + if (y < miny) miny = y; + } + for (int i = 0; i < p->count; i++) { + out_dx[i] = tx[i] - minx; + out_dy[i] = ty[i] - miny; + } + return p->count; +} + +void pattern_bounds(int index, int rotation, int *w, int *h) { + int dx[16], dy[16]; + int n = pattern_rotated_offsets(index, rotation, dx, dy); + int maxx = 0, maxy = 0; + for (int i = 0; i < n; i++) { + if (dx[i] > maxx) maxx = dx[i]; + if (dy[i] > maxy) maxy = dy[i]; + } + if (w) *w = maxx + 1; + if (h) *h = maxy + 1; +} \ No newline at end of file diff --git a/tests/test_config.c b/tests/test_config.c new file mode 100644 index 0000000..e56ed70 --- /dev/null +++ b/tests/test_config.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#include + +#include "config.h" + +int main(void) { + uint32_t mods; + char *key = NULL; + + assert(config_parse_mods("ctrl+q", &mods, &key) == 0); + assert(mods == MOD_CTRL); + assert(strcmp(key, "q") == 0); + free(key); + + assert(config_parse_mods("space", &mods, &key) == 0); + assert(mods == 0); + assert(strcmp(key, "space") == 0); + free(key); + + assert(config_parse_action("quit") == ACTION_QUIT); + assert(config_parse_action("select:glider") == ACTION_SELECT_GLIDER); + assert(config_parse_action("nope") == ACTION_NONE); + + config_t cfg = config_defaults(); + xkb_keysym_t one = xkb_keysym_from_name("1", XKB_KEYSYM_CASE_INSENSITIVE); + xkb_keysym_t space = xkb_keysym_from_name("space", XKB_KEYSYM_CASE_INSENSITIVE); + assert(config_lookup(&cfg, 0, one) == ACTION_SELECT_GLIDER); + assert(config_lookup(&cfg, 0, space) == ACTION_ROTATE); + + const char *path = "/tmp/opencode/test_keys.conf"; + FILE *fp = fopen(path, "w"); + assert(fp); + fputs("[bindings]\n\"1\" = \"quit\"\n", fp); + fclose(fp); + + config_t cfg2 = config_load(path); + assert(config_lookup(&cfg2, 0, one) == ACTION_QUIT); + assert(config_lookup(&cfg2, 0, space) == ACTION_ROTATE); + + config_free(&cfg); + config_free(&cfg2); + remove(path); + printf("config tests passed\n"); + return 0; +} \ No newline at end of file diff --git a/tests/test_grid.c b/tests/test_grid.c new file mode 100644 index 0000000..e0bc57c --- /dev/null +++ b/tests/test_grid.c @@ -0,0 +1,25 @@ +#include +#include + +#include "grid.h" + +int main(void) { + grid_t g; + grid_init(&g, 4, 4); + assert(grid_get(&g, 0, 0) == 0); + grid_set(&g, 1, 2, 1); + assert(grid_get(&g, 1, 2) == 1); + assert(grid_get(&g, -1, 0) == 0); + assert(grid_get(&g, 4, 4) == 0); + + grid_set(&g, 0, 0, 1); + grid_resize(&g, 6, 2); + assert(grid_get(&g, 0, 0) == 1); + assert(grid_get(&g, 1, 2) == 0); + grid_clear(&g); + assert(grid_get(&g, 0, 0) == 0); + + grid_free(&g); + printf("grid tests passed\n"); + return 0; +} \ No newline at end of file diff --git a/tests/test_pattern.c b/tests/test_pattern.c new file mode 100644 index 0000000..2dd945d --- /dev/null +++ b/tests/test_pattern.c @@ -0,0 +1,28 @@ +#include +#include + +#include "pattern.h" + +int main(void) { + assert(pattern_count() == 4); + const void *p = pattern_get(0); + (void)p; + int dx[16], dy[16]; + int n = pattern_rotated_offsets(0, 0, dx, dy); + assert(n == 5); + + int w, h; + pattern_bounds(0, 1, &w, &h); + assert(w == 3 && h == 3); + + int dx2[16], dy2[16]; + pattern_rotated_offsets(0, 1, dx2, dy2); + int same = 1; + for (int i = 0; i < n; i++) { + if (dx[i] != dx2[i] || dy[i] != dy2[i]) same = 0; + } + assert(!same); + + printf("pattern tests passed\n"); + return 0; +} \ No newline at end of file diff --git a/tkeyboard.c b/tkeyboard.c index 41397e7..ed05ff6 100644 --- a/tkeyboard.c +++ b/tkeyboard.c @@ -8,6 +8,7 @@ #include #include +#include static void keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format, int32_t fd, uint32_t size) { @@ -61,9 +62,26 @@ static void keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t seri printf("keyboard_key: scancode=%d state=%d keysym=%u\n", key, state, sym); + uint32_t mods = 0; + if (kbd->xkb_state != NULL && kbd->xkb_keymap != NULL) + { + xkb_mod_index_t ci = xkb_keymap_mod_get_index(kbd->xkb_keymap, "Control"); + if (ci != XKB_MOD_INVALID && xkb_state_mod_index_is_active(kbd->xkb_state, ci, XKB_STATE_MODS_EFFECTIVE)) + mods |= MOD_CTRL; + xkb_mod_index_t si = xkb_keymap_mod_get_index(kbd->xkb_keymap, "Shift"); + if (si != XKB_MOD_INVALID && xkb_state_mod_index_is_active(kbd->xkb_state, si, XKB_STATE_MODS_EFFECTIVE)) + mods |= MOD_SHIFT; + xkb_mod_index_t ai = xkb_keymap_mod_get_index(kbd->xkb_keymap, "Mod1"); + if (ai != XKB_MOD_INVALID && xkb_state_mod_index_is_active(kbd->xkb_state, ai, XKB_STATE_MODS_EFFECTIVE)) + mods |= MOD_ALT; + xkb_mod_index_t sui = xkb_keymap_mod_get_index(kbd->xkb_keymap, "Mod4"); + if (sui != XKB_MOD_INVALID && xkb_state_mod_index_is_active(kbd->xkb_state, sui, XKB_STATE_MODS_EFFECTIVE)) + mods |= MOD_SUPER; + } + if (kbd->handler != NULL) { - kbd->handler(key, state, sym, kbd->user_data); + kbd->handler(key, state, sym, mods, kbd->user_data); } } diff --git a/toml.c b/toml.c new file mode 100644 index 0000000..e7b878e --- /dev/null +++ b/toml.c @@ -0,0 +1,2392 @@ +/* + + MIT License + + Copyright (c) CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +*/ +#define _POSIX_C_SOURCE 200809L +#include "toml.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static void *(*ppmalloc)(size_t) = malloc; +static void (*ppfree)(void *) = free; + +void toml_set_memutil(void *(*xxmalloc)(size_t), void (*xxfree)(void *)) { + if (xxmalloc) + ppmalloc = xxmalloc; + if (xxfree) + ppfree = xxfree; +} + +#define ALIGN8(sz) (((sz) + 7) & ~7) +#define MALLOC(a) ppmalloc(a) +#define FREE(a) ppfree(a) + +#define malloc(x) error - forbidden - use MALLOC instead +#define free(x) error - forbidden - use FREE instead +#define calloc(x, y) error - forbidden - use CALLOC instead + +static void *CALLOC(size_t nmemb, size_t sz) { + int nb = ALIGN8(sz) * nmemb; + void *p = MALLOC(nb); + if (p) { + memset(p, 0, nb); + } + return p; +} + +// some old platforms define strdup macro -- drop it. +#undef strdup +#define strdup(x) error - forbidden - use STRDUP instead + +static char *STRDUP(const char *s) { + int len = strlen(s); + char *p = MALLOC(len + 1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + +// some old platforms define strndup macro -- drop it. +#undef strndup +#define strndup(x) error - forbiden - use STRNDUP instead + +static char *STRNDUP(const char *s, size_t n) { + size_t len = strnlen(s, n); + char *p = MALLOC(len + 1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + +/** + * Convert a char in utf8 into UCS, and store it in *ret. + * Return #bytes consumed or -1 on failure. + */ +int toml_utf8_to_ucs(const char *orig, int len, int64_t *ret) { + const unsigned char *buf = (const unsigned char *)orig; + unsigned i = *buf++; + int64_t v; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (0 == (i >> 7)) { + if (len < 1) + return -1; + v = i; + return *ret = v, 1; + } + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (0x6 == (i >> 5)) { + if (len < 2) + return -1; + v = i & 0x1f; + for (int j = 0; j < 1; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (0xE == (i >> 4)) { + if (len < 3) + return -1; + v = i & 0x0F; + for (int j = 0; j < 2; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x1E == (i >> 3)) { + if (len < 4) + return -1; + v = i & 0x07; + for (int j = 0; j < 3; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x3E == (i >> 2)) { + if (len < 5) + return -1; + v = i & 0x03; + for (int j = 0; j < 4; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x7e == (i >> 1)) { + if (len < 6) + return -1; + v = i & 0x01; + for (int j = 0; j < 5; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + return -1; +} + +/** + * Convert a UCS char to utf8 code, and return it in buf. + * Return #bytes used in buf to encode the char, or + * -1 on error. + */ +int toml_ucs_to_utf8(int64_t code, char buf[6]) { + /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 + */ + /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well + * as 0xfffe and 0xffff (UCS noncharacters) should not appear in + * conforming UTF-8 streams. + */ + if (0xd800 <= code && code <= 0xdfff) + return -1; + if (0xfffe <= code && code <= 0xffff) + return -1; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (code < 0) + return -1; + if (code <= 0x7F) { + buf[0] = (unsigned char)code; + return 1; + } + + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (code <= 0x000007FF) { + buf[0] = (unsigned char)(0xc0 | (code >> 6)); + buf[1] = (unsigned char)(0x80 | (code & 0x3f)); + return 2; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x0000FFFF) { + buf[0] = (unsigned char)(0xe0 | (code >> 12)); + buf[1] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[2] = (unsigned char)(0x80 | (code & 0x3f)); + return 3; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x001FFFFF) { + buf[0] = (unsigned char)(0xf0 | (code >> 18)); + buf[1] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[3] = (unsigned char)(0x80 | (code & 0x3f)); + return 4; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x03FFFFFF) { + buf[0] = (unsigned char)(0xf8 | (code >> 24)); + buf[1] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[3] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[4] = (unsigned char)(0x80 | (code & 0x3f)); + return 5; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x7FFFFFFF) { + buf[0] = (unsigned char)(0xfc | (code >> 30)); + buf[1] = (unsigned char)(0x80 | ((code >> 24) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); + buf[3] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[4] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[5] = (unsigned char)(0x80 | (code & 0x3f)); + return 6; + } + + return -1; +} + +/* + * TOML has 3 data structures: value, array, table. + * Each of them can have identification key. + */ +typedef struct toml_keyval_t toml_keyval_t; +struct toml_keyval_t { + const char *key; /* key to this value */ + const char *val; /* the raw value */ +}; + +typedef struct toml_arritem_t toml_arritem_t; +struct toml_arritem_t { + int valtype; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, + 'D'ate, 'T'imestamp */ + char *val; + toml_array_t *arr; + toml_table_t *tab; +}; + +struct toml_array_t { + const char *key; /* key to this array */ + int kind; /* element kind: 'v'alue, 'a'rray, or 't'able, 'm'ixed */ + int type; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, + 'D'ate, 'T'imestamp, 'm'ixed */ + + int nitem; /* number of elements */ + toml_arritem_t *item; +}; + +struct toml_table_t { + const char *key; /* key to this table */ + bool implicit; /* table was created implicitly */ + bool readonly; /* no more modification allowed */ + + /* key-values in the table */ + int nkval; + toml_keyval_t **kval; + + /* arrays in the table */ + int narr; + toml_array_t **arr; + + /* tables in the table */ + int ntab; + toml_table_t **tab; +}; + +static inline void xfree(const void *x) { + if (x) + FREE((void *)(intptr_t)x); +} + +enum tokentype_t { + INVALID, + DOT, + COMMA, + EQUAL, + LBRACE, + RBRACE, + NEWLINE, + LBRACKET, + RBRACKET, + STRING, +}; +typedef enum tokentype_t tokentype_t; + +typedef struct token_t token_t; +struct token_t { + tokentype_t tok; + int lineno; + char *ptr; /* points into context->start */ + int len; + int eof; +}; + +typedef struct context_t context_t; +struct context_t { + char *start; + char *stop; + char *errbuf; + int errbufsz; + + token_t tok; + toml_table_t *root; + toml_table_t *curtab; + + struct { + int top; + char *key[10]; + token_t tok[10]; + } tpath; +}; + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) +#define FLINE __FILE__ ":" TOSTRING(__LINE__) + +static int next_token(context_t *ctx, int dotisspecial); + +/* + Error reporting. Call when an error is detected. Always return -1. +*/ +static int e_outofmemory(context_t *ctx, const char *fline) { + snprintf(ctx->errbuf, ctx->errbufsz, "ERROR: out of memory (%s)", fline); + return -1; +} + +static int e_internal(context_t *ctx, const char *fline) { + snprintf(ctx->errbuf, ctx->errbufsz, "internal error (%s)", fline); + return -1; +} + +static int e_syntax(context_t *ctx, int lineno, const char *msg) { + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg); + return -1; +} + +static int e_badkey(context_t *ctx, int lineno) { + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: bad key", lineno); + return -1; +} + +static int e_keyexists(context_t *ctx, int lineno) { + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: key exists", lineno); + return -1; +} + +static int e_forbid(context_t *ctx, int lineno, const char *msg) { + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg); + return -1; +} + +static void *expand(void *p, int sz, int newsz) { + void *s = MALLOC(newsz); + if (!s) + return 0; + + if (p) { + memcpy(s, p, sz); + FREE(p); + } + return s; +} + +static void **expand_ptrarr(void **p, int n) { + void **s = MALLOC((n + 1) * sizeof(void *)); + if (!s) + return 0; + + s[n] = 0; + if (p) { + memcpy(s, p, n * sizeof(void *)); + FREE(p); + } + return s; +} + +static toml_arritem_t *expand_arritem(toml_arritem_t *p, int n) { + toml_arritem_t *pp = expand(p, n * sizeof(*p), (n + 1) * sizeof(*p)); + if (!pp) + return 0; + + memset(&pp[n], 0, sizeof(pp[n])); + return pp; +} + +static char *norm_lit_str(const char *src, int srclen, int multiline, + char *errbuf, int errbufsz) { + char *dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char *sp = src; + const char *sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char *x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) + break; + + ch = *sp++; + /* control characters other than tab is not allowed */ + if ((0 <= ch && ch <= 0x08) || (0x0a <= ch && ch <= 0x1f) || (ch == 0x7f)) { + if (!(multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + } + + dst[off++] = 0; + return dst; +} + +/* + * Convert src to raw unescaped utf-8 string. + * Returns NULL if error with errmsg in errbuf. + */ +static char *norm_basic_str(const char *src, int srclen, int multiline, + char *errbuf, int errbufsz) { + char *dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char *sp = src; + const char *sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char *x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) + break; + + ch = *sp++; + if (ch != '\\') { + /* these chars must be escaped: U+0000 to U+0008, U+000A to U+001F, U+007F + */ + if ((0 <= ch && ch <= 0x08) || (0x0a <= ch && ch <= 0x1f) || + (ch == 0x7f)) { + if (!(multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + continue; + } + + /* ch was backslash. we expect the escape char. */ + if (sp >= sq) { + snprintf(errbuf, errbufsz, "last backslash is invalid"); + xfree(dst); + return 0; + } + + /* for multi-line, we want to kill line-ending-backslash ... */ + if (multiline) { + + // if there is only whitespace after the backslash ... + if (sp[strspn(sp, " \t\r")] == '\n') { + /* skip all the following whitespaces */ + sp += strspn(sp, " \t\r\n"); + continue; + } + } + + /* get the escaped char */ + ch = *sp++; + switch (ch) { + case 'u': + case 'U': { + int64_t ucs = 0; + int nhex = (ch == 'u' ? 4 : 8); + for (int i = 0; i < nhex; i++) { + if (sp >= sq) { + snprintf(errbuf, errbufsz, "\\%c expects %d hex chars", ch, nhex); + xfree(dst); + return 0; + } + ch = *sp++; + int v = ('0' <= ch && ch <= '9') + ? ch - '0' + : (('A' <= ch && ch <= 'F') ? ch - 'A' + 10 : -1); + if (-1 == v) { + snprintf(errbuf, errbufsz, "invalid hex chars for \\u or \\U"); + xfree(dst); + return 0; + } + ucs = ucs * 16 + v; + } + int n = toml_ucs_to_utf8(ucs, &dst[off]); + if (-1 == n) { + snprintf(errbuf, errbufsz, "illegal ucs code in \\u or \\U"); + xfree(dst); + return 0; + } + off += n; + } + continue; + + case 'b': + ch = '\b'; + break; + case 't': + ch = '\t'; + break; + case 'n': + ch = '\n'; + break; + case 'f': + ch = '\f'; + break; + case 'r': + ch = '\r'; + break; + case '"': + ch = '"'; + break; + case '\\': + ch = '\\'; + break; + default: + snprintf(errbuf, errbufsz, "illegal escape char \\%c", ch); + xfree(dst); + return 0; + } + + dst[off++] = ch; + } + + // Cap with NUL and return it. + dst[off++] = 0; + return dst; +} + +/* Normalize a key. Convert all special chars to raw unescaped utf-8 chars. */ +static char *normalize_key(context_t *ctx, token_t strtok) { + const char *sp = strtok.ptr; + const char *sq = strtok.ptr + strtok.len; + int lineno = strtok.lineno; + char *ret; + int ch = *sp; + char ebuf[80]; + + /* handle quoted string */ + if (ch == '\'' || ch == '\"') { + /* if ''' or """, take 3 chars off front and back. Else, take 1 char off. */ + int multiline = 0; + if (sp[1] == ch && sp[2] == ch) { + sp += 3, sq -= 3; + multiline = 1; + } else + sp++, sq--; + + if (ch == '\'') { + /* for single quote, take it verbatim. */ + if (!(ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + } else { + /* for double quote, we need to normalize */ + ret = norm_basic_str(sp, sq - sp, multiline, ebuf, sizeof(ebuf)); + if (!ret) { + e_syntax(ctx, lineno, ebuf); + return 0; + } + } + + /* newlines are not allowed in keys */ + if (strchr(ret, '\n')) { + xfree(ret); + e_badkey(ctx, lineno); + return 0; + } + return ret; + } + + /* for bare-key allow only this regex: [A-Za-z0-9_-]+ */ + const char *xp; + for (xp = sp; xp != sq; xp++) { + int k = *xp; + if (isalnum(k)) + continue; + if (k == '_' || k == '-') + continue; + e_badkey(ctx, lineno); + return 0; + } + + /* dup and return it */ + if (!(ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + return ret; +} + +/* + * Look up key in tab. Return 0 if not found, or + * 'v'alue, 'a'rray or 't'able depending on the element. + */ +static int check_key(toml_table_t *tab, const char *key, + toml_keyval_t **ret_val, toml_array_t **ret_arr, + toml_table_t **ret_tab) { + int i; + void *dummy; + + if (!ret_tab) + ret_tab = (toml_table_t **)&dummy; + if (!ret_arr) + ret_arr = (toml_array_t **)&dummy; + if (!ret_val) + ret_val = (toml_keyval_t **)&dummy; + + *ret_tab = 0; + *ret_arr = 0; + *ret_val = 0; + + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) { + *ret_val = tab->kval[i]; + return 'v'; + } + } + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) { + *ret_arr = tab->arr[i]; + return 'a'; + } + } + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) { + *ret_tab = tab->tab[i]; + return 't'; + } + } + return 0; +} + +static int key_kind(toml_table_t *tab, const char *key) { + return check_key(tab, key, 0, 0, 0); +} + +/* Create a keyval in the table. + */ +static toml_keyval_t *create_keyval_in_table(context_t *ctx, toml_table_t *tab, + token_t keytok) { + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char *newkey = normalize_key(ctx, keytok); + if (!newkey) + return 0; + + /* if key exists: error out. */ + toml_keyval_t *dest = 0; + if (key_kind(tab, newkey)) { + xfree(newkey); + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new entry */ + int n = tab->nkval; + toml_keyval_t **base; + if (0 == (base = (toml_keyval_t **)expand_ptrarr((void **)tab->kval, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->kval = base; + + if (0 == (base[n] = (toml_keyval_t *)CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->kval[tab->nkval++]; + + /* save the key in the new value struct */ + dest->key = newkey; + return dest; +} + +/* Create a table in the table. + */ +static toml_table_t *create_keytable_in_table(context_t *ctx, toml_table_t *tab, + token_t keytok) { + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char *newkey = normalize_key(ctx, keytok); + if (!newkey) + return 0; + + /* if key exists: error out */ + toml_table_t *dest = 0; + if (check_key(tab, newkey, 0, 0, &dest)) { + xfree(newkey); /* don't need this anymore */ + + /* special case: if table exists, but was created implicitly ... */ + if (dest && dest->implicit) { + /* we make it explicit now, and simply return it. */ + dest->implicit = false; + return dest; + } + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* create a new table entry */ + int n = tab->ntab; + toml_table_t **base; + if (0 == (base = (toml_table_t **)expand_ptrarr((void **)tab->tab, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->tab = base; + + if (0 == (base[n] = (toml_table_t *)CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->tab[tab->ntab++]; + + /* save the key in the new table struct */ + dest->key = newkey; + return dest; +} + +/* Create an array in the table. + */ +static toml_array_t *create_keyarray_in_table(context_t *ctx, toml_table_t *tab, + token_t keytok, char kind) { + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char *newkey = normalize_key(ctx, keytok); + if (!newkey) + return 0; + + /* if key exists: error out */ + if (key_kind(tab, newkey)) { + xfree(newkey); /* don't need this anymore */ + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new array entry */ + int n = tab->narr; + toml_array_t **base; + if (0 == (base = (toml_array_t **)expand_ptrarr((void **)tab->arr, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->arr = base; + + if (0 == (base[n] = (toml_array_t *)CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + toml_array_t *dest = tab->arr[tab->narr++]; + + /* save the key in the new array struct */ + dest->key = newkey; + dest->kind = kind; + return dest; +} + +static toml_arritem_t *create_value_in_array(context_t *ctx, + toml_array_t *parent) { + const int n = parent->nitem; + toml_arritem_t *base = expand_arritem(parent->item, n); + if (!base) { + e_outofmemory(ctx, FLINE); + return 0; + } + parent->item = base; + parent->nitem++; + return &parent->item[n]; +} + +/* Create an array in an array + */ +static toml_array_t *create_array_in_array(context_t *ctx, + toml_array_t *parent) { + const int n = parent->nitem; + toml_arritem_t *base = expand_arritem(parent->item, n); + if (!base) { + e_outofmemory(ctx, FLINE); + return 0; + } + toml_array_t *ret = (toml_array_t *)CALLOC(1, sizeof(toml_array_t)); + if (!ret) { + e_outofmemory(ctx, FLINE); + return 0; + } + base[n].arr = ret; + parent->item = base; + parent->nitem++; + return ret; +} + +/* Create a table in an array + */ +static toml_table_t *create_table_in_array(context_t *ctx, + toml_array_t *parent) { + int n = parent->nitem; + toml_arritem_t *base = expand_arritem(parent->item, n); + if (!base) { + e_outofmemory(ctx, FLINE); + return 0; + } + toml_table_t *ret = (toml_table_t *)CALLOC(1, sizeof(toml_table_t)); + if (!ret) { + e_outofmemory(ctx, FLINE); + return 0; + } + base[n].tab = ret; + parent->item = base; + parent->nitem++; + return ret; +} + +static int skip_newlines(context_t *ctx, int isdotspecial) { + while (ctx->tok.tok == NEWLINE) { + if (next_token(ctx, isdotspecial)) + return -1; + if (ctx->tok.eof) + break; + } + return 0; +} + +static int parse_keyval(context_t *ctx, toml_table_t *tab); + +static inline int eat_token(context_t *ctx, tokentype_t typ, int isdotspecial, + const char *fline) { + if (ctx->tok.tok != typ) + return e_internal(ctx, fline); + + if (next_token(ctx, isdotspecial)) + return -1; + + return 0; +} + +/* We are at '{ ... }'. + * Parse the table. + */ +static int parse_inline_table(context_t *ctx, toml_table_t *tab) { + if (eat_token(ctx, LBRACE, 1, FLINE)) + return -1; + + for (;;) { + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, + "newline not allowed in inline table"); + + /* until } */ + if (ctx->tok.tok == RBRACE) + break; + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, ctx->tok.lineno, "expect a string"); + + if (parse_keyval(ctx, tab)) + return -1; + + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, + "newline not allowed in inline table"); + + /* on comma, continue to scan for next keyval */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 1, FLINE)) + return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACE, 1, FLINE)) + return -1; + + tab->readonly = 1; + + return 0; +} + +static int valtype(const char *val) { + toml_timestamp_t ts; + if (*val == '\'' || *val == '"') + return 's'; + if (0 == toml_rtob(val, 0)) + return 'b'; + if (0 == toml_rtoi(val, 0)) + return 'i'; + if (0 == toml_rtod(val, 0)) + return 'd'; + if (0 == toml_rtots(val, &ts)) { + if (ts.year && ts.hour) + return 'T'; /* timestamp */ + if (ts.year) + return 'D'; /* date */ + return 't'; /* time */ + } + return 'u'; /* unknown */ +} + +/* We are at '[...]' */ +static int parse_array(context_t *ctx, toml_array_t *arr) { + if (eat_token(ctx, LBRACKET, 0, FLINE)) + return -1; + + for (;;) { + if (skip_newlines(ctx, 0)) + return -1; + + /* until ] */ + if (ctx->tok.tok == RBRACKET) + break; + + switch (ctx->tok.tok) { + case STRING: { + /* set array kind if this will be the first entry */ + if (arr->kind == 0) + arr->kind = 'v'; + else if (arr->kind != 'v') + arr->kind = 'm'; + + char *val = ctx->tok.ptr; + int vlen = ctx->tok.len; + + /* make a new value in array */ + toml_arritem_t *newval = create_value_in_array(ctx, arr); + if (!newval) + return e_outofmemory(ctx, FLINE); + + if (!(newval->val = STRNDUP(val, vlen))) + return e_outofmemory(ctx, FLINE); + + newval->valtype = valtype(newval->val); + + /* set array type if this is the first entry */ + if (arr->nitem == 1) + arr->type = newval->valtype; + else if (arr->type != newval->valtype) + arr->type = 'm'; /* mixed */ + + if (eat_token(ctx, STRING, 0, FLINE)) + return -1; + break; + } + + case LBRACKET: { /* [ [array], [array] ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) + arr->kind = 'a'; + else if (arr->kind != 'a') + arr->kind = 'm'; + + toml_array_t *subarr = create_array_in_array(ctx, arr); + if (!subarr) + return -1; + if (parse_array(ctx, subarr)) + return -1; + break; + } + + case LBRACE: { /* [ {table}, {table} ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) + arr->kind = 't'; + else if (arr->kind != 't') + arr->kind = 'm'; + + toml_table_t *subtab = create_table_in_array(ctx, arr); + if (!subtab) + return -1; + if (parse_inline_table(ctx, subtab)) + return -1; + break; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + + if (skip_newlines(ctx, 0)) + return -1; + + /* on comma, continue to scan for next element */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 0, FLINE)) + return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) + return -1; + return 0; +} + +/* handle lines like these: + key = "value" + key = [ array ] + key = { table } +*/ +static int parse_keyval(context_t *ctx, toml_table_t *tab) { + if (tab->readonly) { + return e_forbid(ctx, ctx->tok.lineno, + "cannot insert new entry into existing table"); + } + + token_t key = ctx->tok; + if (eat_token(ctx, STRING, 1, FLINE)) + return -1; + + if (ctx->tok.tok == DOT) { + /* handle inline dotted key. + e.g. + physical.color = "orange" + physical.shape = "round" + */ + toml_table_t *subtab = 0; + { + char *subtabstr = normalize_key(ctx, key); + if (!subtabstr) + return -1; + + subtab = toml_table_in(tab, subtabstr); + xfree(subtabstr); + } + if (!subtab) { + subtab = create_keytable_in_table(ctx, tab, key); + if (!subtab) + return -1; + } + if (next_token(ctx, 1)) + return -1; + if (parse_keyval(ctx, subtab)) + return -1; + return 0; + } + + if (ctx->tok.tok != EQUAL) { + return e_syntax(ctx, ctx->tok.lineno, "missing ="); + } + + if (next_token(ctx, 0)) + return -1; + + switch (ctx->tok.tok) { + case STRING: { /* key = "value" */ + toml_keyval_t *keyval = create_keyval_in_table(ctx, tab, key); + if (!keyval) + return -1; + token_t val = ctx->tok; + + assert(keyval->val == 0); + if (!(keyval->val = STRNDUP(val.ptr, val.len))) + return e_outofmemory(ctx, FLINE); + + if (next_token(ctx, 1)) + return -1; + + return 0; + } + + case LBRACKET: { /* key = [ array ] */ + toml_array_t *arr = create_keyarray_in_table(ctx, tab, key, 0); + if (!arr) + return -1; + if (parse_array(ctx, arr)) + return -1; + return 0; + } + + case LBRACE: { /* key = { table } */ + toml_table_t *nxttab = create_keytable_in_table(ctx, tab, key); + if (!nxttab) + return -1; + if (parse_inline_table(ctx, nxttab)) + return -1; + return 0; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + return 0; +} + +typedef struct tabpath_t tabpath_t; +struct tabpath_t { + int cnt; + token_t key[10]; +}; + +/* at [x.y.z] or [[x.y.z]] + * Scan forward and fill tabpath until it enters ] or ]] + * There will be at least one entry on return. + */ +static int fill_tabpath(context_t *ctx) { + int lineno = ctx->tok.lineno; + int i; + + /* clear tpath */ + for (i = 0; i < ctx->tpath.top; i++) { + char **p = &ctx->tpath.key[i]; + xfree(*p); + *p = 0; + } + ctx->tpath.top = 0; + + for (;;) { + if (ctx->tpath.top >= 10) + return e_syntax(ctx, lineno, + "table path is too deep; max allowed is 10."); + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, lineno, "invalid or missing key"); + + char *key = normalize_key(ctx, ctx->tok); + if (!key) + return -1; + ctx->tpath.tok[ctx->tpath.top] = ctx->tok; + ctx->tpath.key[ctx->tpath.top] = key; + ctx->tpath.top++; + + if (next_token(ctx, 1)) + return -1; + + if (ctx->tok.tok == RBRACKET) + break; + + if (ctx->tok.tok != DOT) + return e_syntax(ctx, lineno, "invalid key"); + + if (next_token(ctx, 1)) + return -1; + } + + if (ctx->tpath.top <= 0) + return e_syntax(ctx, lineno, "empty table selector"); + + return 0; +} + +/* Walk tabpath from the root, and create new tables on the way. + * Sets ctx->curtab to the final table. + */ +static int walk_tabpath(context_t *ctx) { + /* start from root */ + toml_table_t *curtab = ctx->root; + + for (int i = 0; i < ctx->tpath.top; i++) { + const char *key = ctx->tpath.key[i]; + + toml_keyval_t *nextval = 0; + toml_array_t *nextarr = 0; + toml_table_t *nexttab = 0; + switch (check_key(curtab, key, &nextval, &nextarr, &nexttab)) { + case 't': + /* found a table. nexttab is where we will go next. */ + break; + + case 'a': + /* found an array. nexttab is the last table in the array. */ + if (nextarr->kind != 't') + return e_internal(ctx, FLINE); + + if (nextarr->nitem == 0) + return e_internal(ctx, FLINE); + + nexttab = nextarr->item[nextarr->nitem - 1].tab; + break; + + case 'v': + return e_keyexists(ctx, ctx->tpath.tok[i].lineno); + + default: { /* Not found. Let's create an implicit table. */ + int n = curtab->ntab; + toml_table_t **base = + (toml_table_t **)expand_ptrarr((void **)curtab->tab, n); + if (0 == base) + return e_outofmemory(ctx, FLINE); + + curtab->tab = base; + + if (0 == (base[n] = (toml_table_t *)CALLOC(1, sizeof(*base[n])))) + return e_outofmemory(ctx, FLINE); + + if (0 == (base[n]->key = STRDUP(key))) + return e_outofmemory(ctx, FLINE); + + nexttab = curtab->tab[curtab->ntab++]; + + /* tabs created by walk_tabpath are considered implicit */ + nexttab->implicit = true; + } break; + } + + /* switch to next tab */ + curtab = nexttab; + } + + /* save it */ + ctx->curtab = curtab; + + return 0; +} + +/* handle lines like [x.y.z] or [[x.y.z]] */ +static int parse_select(context_t *ctx) { + assert(ctx->tok.tok == LBRACKET); + + /* true if [[ */ + int llb = (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == '['); + /* need to detect '[[' on our own because next_token() will skip whitespace, + and '[ [' would be taken as '[[', which is wrong. */ + + /* eat [ or [[ */ + if (eat_token(ctx, LBRACKET, 1, FLINE)) + return -1; + if (llb) { + assert(ctx->tok.tok == LBRACKET); + if (eat_token(ctx, LBRACKET, 1, FLINE)) + return -1; + } + + if (fill_tabpath(ctx)) + return -1; + + /* For [x.y.z] or [[x.y.z]], remove z from tpath. + */ + token_t z = ctx->tpath.tok[ctx->tpath.top - 1]; + xfree(ctx->tpath.key[ctx->tpath.top - 1]); + ctx->tpath.top--; + + /* set up ctx->curtab */ + if (walk_tabpath(ctx)) + return -1; + + if (!llb) { + /* [x.y.z] -> create z = {} in x.y */ + toml_table_t *curtab = create_keytable_in_table(ctx, ctx->curtab, z); + if (!curtab) + return -1; + ctx->curtab = curtab; + } else { + /* [[x.y.z]] -> create z = [] in x.y */ + toml_array_t *arr = 0; + { + char *zstr = normalize_key(ctx, z); + if (!zstr) + return -1; + arr = toml_array_in(ctx->curtab, zstr); + xfree(zstr); + } + if (!arr) { + arr = create_keyarray_in_table(ctx, ctx->curtab, z, 't'); + if (!arr) + return -1; + } + if (arr->kind != 't') + return e_syntax(ctx, z.lineno, "array mismatch"); + + /* add to z[] */ + toml_table_t *dest; + { + toml_table_t *t = create_table_in_array(ctx, arr); + if (!t) + return -1; + + if (0 == (t->key = STRDUP("__anon__"))) + return e_outofmemory(ctx, FLINE); + + dest = t; + } + + ctx->curtab = dest; + } + + if (ctx->tok.tok != RBRACKET) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]"); + } + if (llb) { + if (!(ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == ']')) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]]"); + } + if (eat_token(ctx, RBRACKET, 1, FLINE)) + return -1; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) + return -1; + + if (ctx->tok.tok != NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "extra chars after ] or ]]"); + + return 0; +} + +toml_table_t *toml_parse(char *conf, char *errbuf, int errbufsz) { + context_t ctx; + + // clear errbuf + if (errbufsz <= 0) + errbufsz = 0; + if (errbufsz > 0) + errbuf[0] = 0; + + // init context + memset(&ctx, 0, sizeof(ctx)); + ctx.start = conf; + ctx.stop = ctx.start + strlen(conf); + ctx.errbuf = errbuf; + ctx.errbufsz = errbufsz; + + // start with an artificial newline of length 0 + ctx.tok.tok = NEWLINE; + ctx.tok.lineno = 1; + ctx.tok.ptr = conf; + ctx.tok.len = 0; + + // make a root table + if (0 == (ctx.root = CALLOC(1, sizeof(*ctx.root)))) { + e_outofmemory(&ctx, FLINE); + // Do not goto fail, root table not set up yet + return 0; + } + + // set root as default table + ctx.curtab = ctx.root; + + /* Scan forward until EOF */ + for (token_t tok = ctx.tok; !tok.eof; tok = ctx.tok) { + switch (tok.tok) { + + case NEWLINE: + if (next_token(&ctx, 1)) + goto fail; + break; + + case STRING: + if (parse_keyval(&ctx, ctx.curtab)) + goto fail; + + if (ctx.tok.tok != NEWLINE) { + e_syntax(&ctx, ctx.tok.lineno, "extra chars after value"); + goto fail; + } + + if (eat_token(&ctx, NEWLINE, 1, FLINE)) + goto fail; + break; + + case LBRACKET: /* [ x.y.z ] or [[ x.y.z ]] */ + if (parse_select(&ctx)) + goto fail; + break; + + default: + e_syntax(&ctx, tok.lineno, "syntax error"); + goto fail; + } + } + + /* success */ + for (int i = 0; i < ctx.tpath.top; i++) + xfree(ctx.tpath.key[i]); + return ctx.root; + +fail: + // Something bad has happened. Free resources and return error. + for (int i = 0; i < ctx.tpath.top; i++) + xfree(ctx.tpath.key[i]); + toml_free(ctx.root); + return 0; +} + +toml_table_t *toml_parse_file(FILE *fp, char *errbuf, int errbufsz) { + int bufsz = 0; + char *buf = 0; + int off = 0; + + /* read from fp into buf */ + while (!feof(fp)) { + + if (off == bufsz) { + int xsz = bufsz + 1000; + char *x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + + errno = 0; + int n = fread(buf + off, 1, bufsz - off, fp); + if (ferror(fp)) { + snprintf(errbuf, errbufsz, "%s", + errno ? strerror(errno) : "Error reading file"); + xfree(buf); + return 0; + } + off += n; + } + + /* tag on a NUL to cap the string */ + if (off == bufsz) { + int xsz = bufsz + 1; + char *x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + buf[off] = 0; + + /* parse it, cleanup and finish */ + toml_table_t *ret = toml_parse(buf, errbuf, errbufsz); + xfree(buf); + return ret; +} + +static void xfree_kval(toml_keyval_t *p) { + if (!p) + return; + xfree(p->key); + xfree(p->val); + xfree(p); +} + +static void xfree_tab(toml_table_t *p); + +static void xfree_arr(toml_array_t *p) { + if (!p) + return; + + xfree(p->key); + const int n = p->nitem; + for (int i = 0; i < n; i++) { + toml_arritem_t *a = &p->item[i]; + if (a->val) + xfree(a->val); + else if (a->arr) + xfree_arr(a->arr); + else if (a->tab) + xfree_tab(a->tab); + } + xfree(p->item); + xfree(p); +} + +static void xfree_tab(toml_table_t *p) { + int i; + + if (!p) + return; + + xfree(p->key); + + for (i = 0; i < p->nkval; i++) + xfree_kval(p->kval[i]); + xfree(p->kval); + + for (i = 0; i < p->narr; i++) + xfree_arr(p->arr[i]); + xfree(p->arr); + + for (i = 0; i < p->ntab; i++) + xfree_tab(p->tab[i]); + xfree(p->tab); + + xfree(p); +} + +void toml_free(toml_table_t *tab) { xfree_tab(tab); } + +static void set_token(context_t *ctx, tokentype_t tok, int lineno, char *ptr, + int len) { + token_t t; + t.tok = tok; + t.lineno = lineno; + t.ptr = ptr; + t.len = len; + t.eof = 0; + ctx->tok = t; +} + +static void set_eof(context_t *ctx, int lineno) { + set_token(ctx, NEWLINE, lineno, ctx->stop, 0); + ctx->tok.eof = 1; +} + +/* Scan p for n digits compositing entirely of [0-9] */ +static int scan_digits(const char *p, int n) { + int ret = 0; + for (; n > 0 && isdigit(*p); n--, p++) { + ret = 10 * ret + (*p - '0'); + } + return n ? -1 : ret; +} + +static int scan_date(const char *p, int *YY, int *MM, int *DD) { + int year, month, day; + year = scan_digits(p, 4); + month = (year >= 0 && p[4] == '-') ? scan_digits(p + 5, 2) : -1; + day = (month >= 0 && p[7] == '-') ? scan_digits(p + 8, 2) : -1; + if (YY) + *YY = year; + if (MM) + *MM = month; + if (DD) + *DD = day; + return (year >= 0 && month >= 0 && day >= 0) ? 0 : -1; +} + +static int scan_time(const char *p, int *hh, int *mm, int *ss) { + int hour, minute, second; + hour = scan_digits(p, 2); + minute = (hour >= 0 && p[2] == ':') ? scan_digits(p + 3, 2) : -1; + second = (minute >= 0 && p[5] == ':') ? scan_digits(p + 6, 2) : -1; + if (hh) + *hh = hour; + if (mm) + *mm = minute; + if (ss) + *ss = second; + return (hour >= 0 && minute >= 0 && second >= 0) ? 0 : -1; +} + +static int scan_string(context_t *ctx, char *p, int lineno, int dotisspecial) { + char *orig = p; + if (0 == strncmp(p, "'''", 3)) { + char *q = p + 3; + + while (1) { + q = strstr(q, "'''"); + if (0 == q) { + return e_syntax(ctx, lineno, "unterminated triple-s-quote"); + } + while (q[3] == '\'') + q++; + break; + } + + set_token(ctx, STRING, lineno, orig, q + 3 - orig); + return 0; + } + + if (0 == strncmp(p, "\"\"\"", 3)) { + char *q = p + 3; + + while (1) { + q = strstr(q, "\"\"\""); + if (0 == q) { + return e_syntax(ctx, lineno, "unterminated triple-d-quote"); + } + if (q[-1] == '\\') { + q++; + continue; + } + while (q[3] == '\"') + q++; + break; + } + + // the string is [p+3, q-1] + + int hexreq = 0; /* #hex required */ + int escape = 0; + for (p += 3; p < q; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) + continue; + if (*p == 'u') { + hexreq = 4; + continue; + } + if (*p == 'U') { + hexreq = 8; + continue; + } + if (p[strspn(p, " \t\r")] == '\n') + continue; /* allow for line ending backslash */ + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) + continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { + escape = 1; + continue; + } + } + if (escape) + return e_syntax(ctx, lineno, "expect an escape char"); + if (hexreq) + return e_syntax(ctx, lineno, "expected more hex char"); + + set_token(ctx, STRING, lineno, orig, q + 3 - orig); + return 0; + } + + if ('\'' == *p) { + for (p++; *p && *p != '\n' && *p != '\''; p++) + ; + if (*p != '\'') { + return e_syntax(ctx, lineno, "unterminated s-quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + if ('\"' == *p) { + int hexreq = 0; /* #hex required */ + int escape = 0; + for (p++; *p; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) + continue; + if (*p == 'u') { + hexreq = 4; + continue; + } + if (*p == 'U') { + hexreq = 8; + continue; + } + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) + continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { + escape = 1; + continue; + } + if (*p == '\'') { + if (p[1] == '\'' && p[2] == '\'') { + return e_syntax(ctx, lineno, "triple-s-quote inside string lit"); + } + continue; + } + if (*p == '\n') + break; + if (*p == '"') + break; + } + if (*p != '"') { + return e_syntax(ctx, lineno, "unterminated quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + /* check for timestamp without quotes */ + if (0 == scan_date(p, 0, 0, 0) || 0 == scan_time(p, 0, 0, 0)) { + // forward thru the timestamp + p += strspn(p, "0123456789.:+-Tt Zz"); + // squeeze out any spaces at end of string + for (; p[-1] == ' '; p--) + ; + // tokenize + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; + } + + /* literals */ + for (; *p && *p != '\n'; p++) { + int ch = *p; + if (ch == '.' && dotisspecial) + break; + if ('A' <= ch && ch <= 'Z') + continue; + if ('a' <= ch && ch <= 'z') + continue; + if (strchr("0123456789+-_.", ch)) + continue; + break; + } + + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; +} + +static int next_token(context_t *ctx, int dotisspecial) { + int lineno = ctx->tok.lineno; + char *p = ctx->tok.ptr; + int i; + + /* eat this tok */ + for (i = 0; i < ctx->tok.len; i++) { + if (*p++ == '\n') + lineno++; + } + + /* make next tok */ + while (p < ctx->stop) { + /* skip comment. stop just before the \n. */ + if (*p == '#') { + for (p++; p < ctx->stop && *p != '\n'; p++) + ; + continue; + } + + if (dotisspecial && *p == '.') { + set_token(ctx, DOT, lineno, p, 1); + return 0; + } + + switch (*p) { + case ',': + set_token(ctx, COMMA, lineno, p, 1); + return 0; + case '=': + set_token(ctx, EQUAL, lineno, p, 1); + return 0; + case '{': + set_token(ctx, LBRACE, lineno, p, 1); + return 0; + case '}': + set_token(ctx, RBRACE, lineno, p, 1); + return 0; + case '[': + set_token(ctx, LBRACKET, lineno, p, 1); + return 0; + case ']': + set_token(ctx, RBRACKET, lineno, p, 1); + return 0; + case '\n': + set_token(ctx, NEWLINE, lineno, p, 1); + return 0; + case '\r': + case ' ': + case '\t': + /* ignore white spaces */ + p++; + continue; + } + + return scan_string(ctx, p, lineno, dotisspecial); + } + + set_eof(ctx, lineno); + return 0; +} + +const char *toml_key_in(const toml_table_t *tab, int keyidx) { + if (keyidx < tab->nkval) + return tab->kval[keyidx]->key; + + keyidx -= tab->nkval; + if (keyidx < tab->narr) + return tab->arr[keyidx]->key; + + keyidx -= tab->narr; + if (keyidx < tab->ntab) + return tab->tab[keyidx]->key; + + return 0; +} + +int toml_key_exists(const toml_table_t *tab, const char *key) { + int i; + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) + return 1; + } + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) + return 1; + } + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) + return 1; + } + return 0; +} + +toml_raw_t toml_raw_in(const toml_table_t *tab, const char *key) { + int i; + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) + return tab->kval[i]->val; + } + return 0; +} + +toml_array_t *toml_array_in(const toml_table_t *tab, const char *key) { + int i; + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) + return tab->arr[i]; + } + return 0; +} + +toml_table_t *toml_table_in(const toml_table_t *tab, const char *key) { + int i; + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) + return tab->tab[i]; + } + return 0; +} + +toml_raw_t toml_raw_at(const toml_array_t *arr, int idx) { + return (0 <= idx && idx < arr->nitem) ? arr->item[idx].val : 0; +} + +char toml_array_kind(const toml_array_t *arr) { return arr->kind; } + +char toml_array_type(const toml_array_t *arr) { + if (arr->kind != 'v') + return 0; + + if (arr->nitem == 0) + return 0; + + return arr->type; +} + +int toml_array_nelem(const toml_array_t *arr) { return arr->nitem; } + +const char *toml_array_key(const toml_array_t *arr) { + return arr ? arr->key : (const char *)NULL; +} + +int toml_table_nkval(const toml_table_t *tab) { return tab->nkval; } + +int toml_table_narr(const toml_table_t *tab) { return tab->narr; } + +int toml_table_ntab(const toml_table_t *tab) { return tab->ntab; } + +const char *toml_table_key(const toml_table_t *tab) { + return tab ? tab->key : (const char *)NULL; +} + +toml_array_t *toml_array_at(const toml_array_t *arr, int idx) { + return (0 <= idx && idx < arr->nitem) ? arr->item[idx].arr : 0; +} + +toml_table_t *toml_table_at(const toml_array_t *arr, int idx) { + return (0 <= idx && idx < arr->nitem) ? arr->item[idx].tab : 0; +} + +static int parse_millisec(const char *p, const char **endp); + +int toml_rtots(toml_raw_t src_, toml_timestamp_t *ret) { + if (!src_) + return -1; + + const char *p = src_; + int must_parse_time = 0; + + memset(ret, 0, sizeof(*ret)); + + int *year = &ret->__buffer.year; + int *month = &ret->__buffer.month; + int *day = &ret->__buffer.day; + int *hour = &ret->__buffer.hour; + int *minute = &ret->__buffer.minute; + int *second = &ret->__buffer.second; + int *millisec = &ret->__buffer.millisec; + + /* parse date YYYY-MM-DD */ + if (0 == scan_date(p, year, month, day)) { + ret->year = year; + ret->month = month; + ret->day = day; + + p += 10; + if (*p) { + // parse the T or space separator + if (*p != 'T' && *p != 't' && *p != ' ') + return -1; + must_parse_time = 1; + p++; + } + } + + /* parse time HH:MM:SS */ + if (0 == scan_time(p, hour, minute, second)) { + ret->hour = hour; + ret->minute = minute; + ret->second = second; + + /* optionally, parse millisec */ + p += 8; + if (*p == '.') { + p++; /* skip '.' */ + const char *qq; + *millisec = parse_millisec(p, &qq); + ret->millisec = millisec; + p = qq; + } + + if (*p) { + /* parse and copy Z */ + char *z = ret->__buffer.z; + ret->z = z; + if (*p == 'Z' || *p == 'z') { + *z++ = 'Z'; + p++; + *z = 0; + + } else if (*p == '+' || *p == '-') { + *z++ = *p++; + + if (!(isdigit(p[0]) && isdigit(p[1]))) + return -1; + *z++ = *p++; + *z++ = *p++; + + if (*p == ':') { + *z++ = *p++; + + if (!(isdigit(p[0]) && isdigit(p[1]))) + return -1; + *z++ = *p++; + *z++ = *p++; + } + + *z = 0; + } + } + } + if (*p != 0) + return -1; + + if (must_parse_time && !ret->hour) + return -1; + + return 0; +} + +/* Raw to boolean */ +int toml_rtob(toml_raw_t src, int *ret_) { + if (!src) + return -1; + int dummy; + int *ret = ret_ ? ret_ : &dummy; + + if (0 == strcmp(src, "true")) { + *ret = 1; + return 0; + } + if (0 == strcmp(src, "false")) { + *ret = 0; + return 0; + } + return -1; +} + +/* Raw to integer */ +int toml_rtoi(toml_raw_t src, int64_t *ret_) { + if (!src) + return -1; + + char buf[100]; + char *p = buf; + char *q = p + sizeof(buf); + const char *s = src; + int base = 0; + int64_t dummy; + int64_t *ret = ret_ ? ret_ : &dummy; + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_100 */ + if (s[0] == '_') + return -1; + + /* if 0* ... */ + if ('0' == s[0]) { + switch (s[1]) { + case 'x': + base = 16; + s += 2; + break; + case 'o': + base = 8; + s += 2; + break; + case 'b': + base = 2; + s += 2; + break; + case '\0': + return *ret = 0, 0; + default: + /* ensure no other digits after it */ + if (s[1]) + return -1; + } + } + + /* just strip underscores and pass to strtoll */ + while (*s && p < q) { + int ch = *s++; + if (ch == '_') { + // disallow '__' + if (s[0] == '_') + return -1; + // numbers cannot end with '_' + if (s[0] == '\0') + return -1; + continue; /* skip _ */ + } + *p++ = ch; + } + + // if not at end-of-string or we ran out of buffer ... + if (*s || p == q) + return -1; + + /* cap with NUL */ + *p = 0; + + /* Run strtoll on buf to get the integer */ + char *endp; + errno = 0; + *ret = strtoll(buf, &endp, base); + return (errno || *endp) ? -1 : 0; +} + +int toml_rtod_ex(toml_raw_t src, double *ret_, char *buf, int buflen) { + if (!src) + return -1; + + char *p = buf; + char *q = p + buflen; + const char *s = src; + double dummy; + double *ret = ret_ ? ret_ : &dummy; + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_1.00 */ + if (s[0] == '_') + return -1; + + /* decimal point, if used, must be surrounded by at least one digit on each + * side */ + { + char *dot = strchr(s, '.'); + if (dot) { + if (dot == s || !isdigit(dot[-1]) || !isdigit(dot[1])) + return -1; + } + } + + /* zero must be followed by . or 'e', or NUL */ + if (s[0] == '0' && s[1] && !strchr("eE.", s[1])) + return -1; + + /* just strip underscores and pass to strtod */ + while (*s && p < q) { + int ch = *s++; + if (ch == '_') { + // disallow '__' + if (s[0] == '_') + return -1; + // disallow last char '_' + if (s[0] == 0) + return -1; + continue; /* skip _ */ + } + *p++ = ch; + } + if (*s || p == q) + return -1; /* reached end of string or buffer is full? */ + + /* cap with NUL */ + *p = 0; + + /* Run strtod on buf to get the value */ + char *endp; + errno = 0; + *ret = strtod(buf, &endp); + return (errno || *endp) ? -1 : 0; +} + +int toml_rtod(toml_raw_t src, double *ret_) { + char buf[100]; + return toml_rtod_ex(src, ret_, buf, sizeof(buf)); +} + +int toml_rtos(toml_raw_t src, char **ret) { + int multiline = 0; + const char *sp; + const char *sq; + + *ret = 0; + if (!src) + return -1; + + // for strings, first char must be a s-quote or d-quote + int qchar = src[0]; + int srclen = strlen(src); + if (!(qchar == '\'' || qchar == '"')) { + return -1; + } + + // triple quotes? + if (qchar == src[1] && qchar == src[2]) { + multiline = 1; // triple-quote implies multiline + sp = src + 3; // first char after quote + sq = src + srclen - 3; // first char of ending quote + + if (!(sp <= sq && sq[0] == qchar && sq[1] == qchar && sq[2] == qchar)) { + // last 3 chars in src must be qchar + return -1; + } + + /* skip new line immediate after qchar */ + if (sp[0] == '\n') + sp++; + else if (sp[0] == '\r' && sp[1] == '\n') + sp += 2; + + } else { + sp = src + 1; // first char after quote + sq = src + srclen - 1; // ending quote + if (!(sp <= sq && *sq == qchar)) { + /* last char in src must be qchar */ + return -1; + } + } + + // at this point: + // sp points to first valid char after quote. + // sq points to one char beyond last valid char. + // string len is (sq - sp). + if (qchar == '\'') { + *ret = norm_lit_str(sp, sq - sp, multiline, 0, 0); + } else { + *ret = norm_basic_str(sp, sq - sp, multiline, 0, 0); + } + + return *ret ? 0 : -1; +} + +toml_datum_t toml_string_at(const toml_array_t *arr, int idx) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtos(toml_raw_at(arr, idx), &ret.u.s)); + return ret; +} + +toml_datum_t toml_bool_at(const toml_array_t *arr, int idx) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_at(arr, idx), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_at(const toml_array_t *arr, int idx) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_at(arr, idx), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_at(const toml_array_t *arr, int idx) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_at(arr, idx), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_at(const toml_array_t *arr, int idx) { + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_at(arr, idx), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = MALLOC(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + if (ret.u.ts->year) + ret.u.ts->year = &ret.u.ts->__buffer.year; + if (ret.u.ts->month) + ret.u.ts->month = &ret.u.ts->__buffer.month; + if (ret.u.ts->day) + ret.u.ts->day = &ret.u.ts->__buffer.day; + if (ret.u.ts->hour) + ret.u.ts->hour = &ret.u.ts->__buffer.hour; + if (ret.u.ts->minute) + ret.u.ts->minute = &ret.u.ts->__buffer.minute; + if (ret.u.ts->second) + ret.u.ts->second = &ret.u.ts->__buffer.second; + if (ret.u.ts->millisec) + ret.u.ts->millisec = &ret.u.ts->__buffer.millisec; + if (ret.u.ts->z) + ret.u.ts->z = ret.u.ts->__buffer.z; + } + } + return ret; +} + +toml_datum_t toml_string_in(const toml_table_t *arr, const char *key) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + toml_raw_t raw = toml_raw_in(arr, key); + if (raw) { + ret.ok = (0 == toml_rtos(raw, &ret.u.s)); + } + return ret; +} + +toml_datum_t toml_bool_in(const toml_table_t *arr, const char *key) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_in(arr, key), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_in(const toml_table_t *arr, const char *key) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_in(arr, key), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_in(const toml_table_t *arr, const char *key) { + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_in(arr, key), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_in(const toml_table_t *arr, const char *key) { + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_in(arr, key), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = MALLOC(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + if (ret.u.ts->year) + ret.u.ts->year = &ret.u.ts->__buffer.year; + if (ret.u.ts->month) + ret.u.ts->month = &ret.u.ts->__buffer.month; + if (ret.u.ts->day) + ret.u.ts->day = &ret.u.ts->__buffer.day; + if (ret.u.ts->hour) + ret.u.ts->hour = &ret.u.ts->__buffer.hour; + if (ret.u.ts->minute) + ret.u.ts->minute = &ret.u.ts->__buffer.minute; + if (ret.u.ts->second) + ret.u.ts->second = &ret.u.ts->__buffer.second; + if (ret.u.ts->millisec) + ret.u.ts->millisec = &ret.u.ts->__buffer.millisec; + if (ret.u.ts->z) + ret.u.ts->z = ret.u.ts->__buffer.z; + } + } + return ret; +} + +static int parse_millisec(const char *p, const char **endp) { + int ret = 0; + int unit = 100; /* unit in millisec */ + for (; '0' <= *p && *p <= '9'; p++, unit /= 10) { + ret += (*p - '0') * unit; + } + *endp = p; + return ret; +} diff --git a/tpointer.c b/tpointer.c index 80a86ac..cd5845f 100644 --- a/tpointer.c +++ b/tpointer.c @@ -21,12 +21,17 @@ static void pointer_leave_handler(void *data, struct wl_pointer *pointer, uint32 static void pointer_motion_handler(void *data, struct wl_pointer *pointer, uint32_t time, wl_fixed_t sx, wl_fixed_t sy) { - // not implemented + struct pointer_data *pd = data; + pd->x = wl_fixed_to_double(sx); + pd->y = wl_fixed_to_double(sy); } static void pointer_button_handler(void *data, struct wl_pointer *pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { - // not implemented + struct pointer_data *pd = data; + if (pd->button_handler) { + pd->button_handler(button, state, pd->x, pd->y, pd->user_data); + } } static void pointer_axis_handler(void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis, wl_fixed_t value) @@ -66,8 +71,18 @@ const struct wl_pointer_listener pointer_listener = { .axis_stop = pointer_axis_stop_handler, }; +void pointer_set_button_handler(struct pointer_data *pointer_data, pointer_button_handler_t handler, void *user_data) +{ + pointer_data->button_handler = handler; + pointer_data->user_data = user_data; +} + void pointer_init_cursor(struct pointer_data *pointer_data, struct wl_compositor *compositor, struct wl_shm *shm) { + pointer_data->x = 0; + pointer_data->y = 0; + pointer_data->button_handler = NULL; + pointer_data->user_data = NULL; pointer_data->cursor_theme = wl_cursor_theme_load(NULL, 24, shm); pointer_data->cursor = wl_cursor_theme_get_cursor(pointer_data->cursor_theme, "left_ptr"); diff --git a/viewporter.xml b/viewporter.xml new file mode 100644 index 0000000..1374aec --- /dev/null +++ b/viewporter.xml @@ -0,0 +1,177 @@ + + + + + Copyright © 2013-2016 Collabora, Ltd. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The global interface exposing surface cropping and scaling + capabilities is used to instantiate an interface extension for a + wl_surface object. This extended interface will then allow + cropping and scaling the surface contents, effectively + disconnecting the direct relationship between the buffer and the + surface size. + + + + + Informs the server that the client will not be using this + protocol object anymore. This does not affect any other objects, + wp_viewport objects included. + + + + + + + + + + Instantiate an interface extension for the given wl_surface to + crop and scale its content. If the given wl_surface already has + a wp_viewport object associated, the viewport_exists + protocol error is raised. + + + + + + + + + An additional interface to a wl_surface object, which allows the + client to specify the cropping and scaling of the surface + contents. + + This interface works with two concepts: the source rectangle (src_x, + src_y, src_width, src_height), and the destination size (dst_width, + dst_height). The contents of the source rectangle are scaled to the + destination size, and content outside the source rectangle is ignored. + This state is double-buffered, see wl_surface.commit. + + The two parts of crop and scale state are independent: the source + rectangle, and the destination size. Initially both are unset, that + is, no scaling is applied. The whole of the current wl_buffer is + used as the source, and the surface size is as defined in + wl_surface.attach. + + If the destination size is set, it causes the surface size to become + dst_width, dst_height. The source (rectangle) is scaled to exactly + this size. This overrides whatever the attached wl_buffer size is, + unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface + has no content and therefore no size. Otherwise, the size is always + at least 1x1 in surface local coordinates. + + If the source rectangle is set, it defines what area of the wl_buffer is + taken as the source. If the source rectangle is set and the destination + size is not set, then src_width and src_height must be integers, and the + surface size becomes the source rectangle size. This results in cropping + without scaling. If src_width or src_height are not integers and + destination size is not set, the bad_size protocol error is raised when + the surface state is applied. + + The coordinate transformations from buffer pixel coordinates up to + the surface-local coordinates happen in the following order: + 1. buffer_transform (wl_surface.set_buffer_transform) + 2. buffer_scale (wl_surface.set_buffer_scale) + 3. crop and scale (wp_viewport.set*) + This means, that the source rectangle coordinates of crop and scale + are given in the coordinates after the buffer transform and scale, + i.e. in the coordinates that would be the surface-local coordinates + if the crop and scale was not applied. + + If src_x or src_y are negative, the bad_value protocol error is raised. + Otherwise, if the source rectangle is partially or completely outside of + the non-NULL wl_buffer, then the out_of_buffer protocol error is raised + when the surface state is applied. A NULL wl_buffer does not raise the + out_of_buffer error. + + If the wl_surface associated with the wp_viewport is destroyed, + all wp_viewport requests except 'destroy' raise the protocol error + no_surface. + + If the wp_viewport object is destroyed, the crop and scale + state is removed from the wl_surface. The change will be applied + on the next wl_surface.commit. + + + + + The associated wl_surface's crop and scale state is removed. + The change is applied on the next wl_surface.commit. + + + + + + + + + + + + + Set the source rectangle of the associated wl_surface. See + wp_viewport for the description, and relation to the wl_buffer + size. + + If all of x, y, width and height are -1.0, the source rectangle is + unset instead. Any other set of values where width or height are zero + or negative, or x or y are negative, raise the bad_value protocol + error. + + The crop and scale state is double-buffered, see wl_surface.commit. + + + + + + + + + + Set the destination size of the associated wl_surface. See + wp_viewport for the description, and relation to the wl_buffer + size. + + If width is -1 and height is -1, the destination size is unset + instead. Any other pair of values for width and height that + contains zero or negative values raises the bad_value protocol + error. + + The crop and scale state is double-buffered, see wl_surface.commit. + + + + + + + diff --git a/wlgol.c b/wlgol.c index 9df8376..69410c0 100644 --- a/wlgol.c +++ b/wlgol.c @@ -1,46 +1,109 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include +#include +#include #include #include #include #include #include +#include +#include +#include struct wl_compositor *compositor; struct wl_shm *shm; struct xdg_wm_base *xdg_shell; struct wl_seat *wl_seat; +struct wl_output *wl_output = NULL; +int output_scale_factor = 1; struct keyboard_data keyboard_data; struct pointer_data pointer_data; int running = 1; int released = 0; + +config_t g_config; +int active_tool = -1; /* -1 none, 0..3 patterns */ +int active_rotation = 0; + +int logical_width = 400; +int logical_height = 400; +double current_scale = 1.0; +int need_resize = 1; + +grid_t grid; +double cursor_x = 0, cursor_y = 0; +int click_pending = 0; +double click_x = 0, click_y = 0; +int show_grid_lines = 1; +int sim_paused = 1; struct timespec mytime; struct canvas { - int width; - int height; + int width; /* physical */ + int height; /* physical */ int stride; int size; struct wl_buffer *buffer; unsigned char *data; + struct wl_shm_pool *pool; + int fd; struct wl_surface *surface; struct wl_callback *frame; } canvas; +struct wp_viewporter *viewporter = NULL; +struct wp_viewport *canvas_viewport = NULL; +struct wp_fractional_scale_manager_v1 *fractional_manager = NULL; +struct wp_fractional_scale_v1 *fractional_scale = NULL; + +// output listener +static void output_geometry_handler(void *data, struct wl_output *output, int32_t x, int32_t y, int32_t phys_w, int32_t phys_h, int32_t subpixel, const char *make, const char *model, int32_t transform) { (void)data;(void)output;(void)x;(void)y;(void)phys_w;(void)phys_h;(void)subpixel;(void)make;(void)model;(void)transform; } +static void output_mode_handler(void *data, struct wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { (void)data;(void)output;(void)flags;(void)width;(void)height;(void)refresh; } +static void output_done_handler(void *data, struct wl_output *output) { (void)data;(void)output; } +static void output_scale_handler(void *data, struct wl_output *output, int32_t factor) { + (void)data;(void)output; + output_scale_factor = factor; + if (!fractional_manager) { + if (factor > 0) current_scale = factor; + need_resize = 1; + } +} +const struct wl_output_listener output_listener = { + .geometry = output_geometry_handler, + .mode = output_mode_handler, + .done = output_done_handler, + .scale = output_scale_handler, +}; + +// fractional scale listener +static void fractional_preferred_scale_handler(void *data, struct wp_fractional_scale_v1 *scale_obj, uint32_t scale) { + (void)data;(void)scale_obj; + double s = (double)scale / 120.0; + if (s > 0) current_scale = s; + need_resize = 1; +} +const struct wp_fractional_scale_v1_listener fractional_scale_listener = { + .preferred_scale = fractional_preferred_scale_handler, +}; + // registry void registry_global_handler( void *data, struct wl_registry *registry, @@ -68,6 +131,25 @@ void registry_global_handler( wl_seat = wl_registry_bind(registry, name, &wl_seat_interface, 5); printf("Got wl_seat (%d)\n", version); } + else if (strcmp(interface, wl_output_interface.name) == 0) + { + if (!wl_output) + { + wl_output = wl_registry_bind(registry, name, &wl_output_interface, 2); + wl_output_add_listener(wl_output, &output_listener, NULL); + printf("Got wl_output (%d)\n", version); + } + } + else if (strcmp(interface, wp_viewporter_interface.name) == 0) + { + viewporter = wl_registry_bind(registry, name, &wp_viewporter_interface, 1); + printf("Got wp_viewporter (%d)\n", version); + } + else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) + { + fractional_manager = wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1); + printf("Got fractional_scale_manager (%d)\n", version); + } } void registry_global_remove_handler( @@ -89,6 +171,9 @@ void xdg_toplevel_configure_handler( struct wl_array *states) { printf("xdg_toplevel_configure: %dx%d\n", width, height); + if (width > 0) logical_width = width; + if (height > 0) logical_height = height; + need_resize = 1; } void xdg_toplevel_close_handler(void *data, struct xdg_toplevel *xdg_toplevel) @@ -144,18 +229,68 @@ const struct wl_buffer_listener wl_buffer_listener = { void redraw(); // forward declaration -static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, void *user_data) +static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t mods, void *user_data) { (void)key; (void)user_data; - if (state == WL_KEYBOARD_KEY_STATE_PRESSED && sym == XKB_KEY_Escape) - { - printf("Escape pressed, exiting.\n"); + if (state != WL_KEYBOARD_KEY_STATE_PRESSED) return; + + action_t a = config_lookup(&g_config, mods, sym); + printf("action=%d\n", a); + + switch (a) { + case ACTION_QUIT: running = 0; + break; + case ACTION_CANCEL: + active_tool = -1; + printf("tool cancelled\n"); + break; + case ACTION_ROTATE: + if (active_tool >= 0) { + active_rotation = (active_rotation + 1) % 4; + printf("rotation=%d\n", active_rotation); + } + break; + case ACTION_SELECT_GLIDER: + active_tool = 0; active_rotation = 0; printf("selected glider\n"); break; + case ACTION_SELECT_BLINKER: + active_tool = 1; active_rotation = 0; printf("selected blinker\n"); break; + case ACTION_SELECT_BLOCK: + active_tool = 2; active_rotation = 0; printf("selected block\n"); break; + case ACTION_SELECT_TOAD: + active_tool = 3; active_rotation = 0; printf("selected toad\n"); break; + case ACTION_CLEAR: + printf("clear requested\n"); + break; + case ACTION_PAUSE: + sim_paused = !sim_paused; + break; + case ACTION_STEP: + printf("step requested\n"); + break; + case ACTION_SPEED_UP: + case ACTION_SPEED_DOWN: + case ACTION_TOGGLE_GRID: + show_grid_lines = !show_grid_lines; + break; + default: + break; } } +static void handle_button(uint32_t button, uint32_t state, double x, double y, void *user_data) +{ + (void)user_data; + if (state != WL_POINTER_BUTTON_STATE_PRESSED) return; + if (button != BTN_LEFT) return; + click_x = x; + click_y = y; + click_pending = 1; + printf("click at %.1f %.1f\n", x, y); +} + void wl_callack_done(void *data, struct wl_callback *callback, uint32_t time) { if (canvas.frame) @@ -173,6 +308,44 @@ void wl_callack_done(void *data, struct wl_callback *callback, uint32_t time) const struct wl_callback_listener wl_surface_frame_listener = { .done = wl_callack_done}; +static void canvas_resize(int log_w, int log_h, double scale) { + if (log_w < 1) log_w = 1; + if (log_h < 1) log_h = 1; + if (scale <= 0) scale = 1.0; + int phys_w = (int)ceil((double)log_w * scale); + int phys_h = (int)ceil((double)log_h * scale); + if (phys_w < 1) phys_w = 1; + if (phys_h < 1) phys_h = 1; + + if (canvas.buffer) { wl_buffer_destroy(canvas.buffer); canvas.buffer = NULL; } + if (canvas.pool) { wl_shm_pool_destroy(canvas.pool); canvas.pool = NULL; } + if (canvas.data && canvas.size > 0) { munmap(canvas.data, canvas.size); canvas.data = NULL; } + if (canvas.fd >= 0) { close(canvas.fd); canvas.fd = -1; } + + canvas.width = phys_w; + canvas.height = phys_h; + canvas.stride = phys_w * 4; + canvas.size = canvas.stride * phys_h; + + canvas.fd = syscall(SYS_memfd_create, "buffer", 0); + ftruncate(canvas.fd, canvas.size); + canvas.data = mmap(NULL, canvas.size, PROT_READ | PROT_WRITE, MAP_SHARED, canvas.fd, 0); + canvas.pool = wl_shm_create_pool(shm, canvas.fd, canvas.size); + close(canvas.fd); + canvas.fd = -1; + + canvas.buffer = wl_shm_pool_create_buffer(canvas.pool, 0, phys_w, phys_h, canvas.stride, WL_SHM_FORMAT_ARGB8888); + wl_buffer_add_listener(canvas.buffer, &wl_buffer_listener, NULL); + + if (canvas_viewport) { + wp_viewport_set_destination(canvas_viewport, log_w, log_h); + } + + grid_resize(&grid, log_w / 8, log_h / 8); + need_resize = 0; + printf("canvas resized logical=%dx%d phys=%dx%d scale=%g grid=%dx%d\n", log_w, log_h, phys_w, phys_h, scale, grid.cols, grid.rows); +} + int main(void) { clock_gettime(CLOCK_MONOTONIC, &mytime); @@ -188,6 +361,14 @@ int main(void) canvas.surface = wl_compositor_create_surface(compositor); + if (viewporter) { + canvas_viewport = wp_viewporter_get_viewport(viewporter, canvas.surface); + } + if (fractional_manager) { + fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(fractional_manager, canvas.surface); + wp_fractional_scale_v1_add_listener(fractional_scale, &fractional_scale_listener, NULL); + } + struct xdg_surface *xdg_surface = xdg_wm_base_get_xdg_surface(xdg_shell, canvas.surface); struct xdg_toplevel *xdg_toplevel = xdg_surface_get_toplevel(xdg_surface); @@ -205,35 +386,29 @@ int main(void) wl_keyboard_add_listener(keyboard_data.keyboard, &keyboard_listener, &keyboard_data); pointer_init_cursor(&pointer_data, compositor, shm); + pointer_set_button_handler(&pointer_data, handle_button, NULL); pointer_data.pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(pointer_data.pointer, &pointer_listener, &pointer_data); - canvas.width = 400; - canvas.height = 400; - canvas.stride = canvas.width * 4; - canvas.size = canvas.stride * canvas.height; // bytes + canvas.fd = -1; + grid_init(&grid, logical_width / 8, logical_height / 8); + grid.show_lines = 1; - // open an anonymous file and write some zero bytes to it - int fd = syscall(SYS_memfd_create, "buffer", 0); - ftruncate(fd, canvas.size); - - // map it to the memory - canvas.data = - mmap(NULL, canvas.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - - // turn it into a shared memory pool - struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, canvas.size); - - close(fd); // mapped, so can be closed - - // allocate the buffer in that pool - canvas.buffer = wl_shm_pool_create_buffer( - pool, 0, canvas.width, canvas.height, canvas.stride, WL_SHM_FORMAT_ARGB8888); - - wl_buffer_add_listener(canvas.buffer, &wl_buffer_listener, NULL); // wait for the surface to be configured wl_display_roundtrip(display); + /* initial scale may arrive via fractional/output after roundtrip */ + if (fractional_manager && !fractional_scale) { + /* already created above */ + } + canvas_resize(logical_width, logical_height, current_scale); + + const char *home = getenv("HOME"); + char config_path[1024]; + if (home) snprintf(config_path, sizeof(config_path), "%s/.config/wlgol/keys.conf", home); + else snprintf(config_path, sizeof(config_path), ".config/wlgol/keys.conf"); + g_config = config_load(config_path); + redraw(); while (wl_display_dispatch(display) != -1 && running) @@ -244,7 +419,8 @@ int main(void) xdg_toplevel_destroy(xdg_toplevel); xdg_surface_destroy(xdg_surface); wl_surface_destroy(canvas.surface); - wl_shm_pool_destroy(pool); + if (canvas.pool) wl_shm_pool_destroy(canvas.pool); + if (canvas.buffer) wl_buffer_destroy(canvas.buffer); wl_keyboard_destroy(keyboard_data.keyboard); if (keyboard_data.xkb_state != NULL) { @@ -266,59 +442,109 @@ unsigned char mycolour = 0; char direction = 1; int framecounter = 0; -void redraw() -{ - clock_t start = clock(); - - mycolour += direction; - switch (mycolour) - { - case 0: // lower limit, start to increase - direction = 1; - break; - case 255: // upper limit, start to decrease - direction = -1; - break; - } - // printf("dred=%d\n", dred); - // draw into buffer - for (int x = 0; x < canvas.width; x++) - { - for (int y = 0; y < canvas.height; y++) - { - struct pixel - { - // little-endian ARGB +static void fill_rect(int x0, int y0, int w, int h, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + if (w <= 0 || h <= 0) return; + for (int y = y0; y < y0 + h && y < canvas.height; y++) { + if (y < 0) continue; + for (int x = x0; x < x0 + w && x < canvas.width; x++) { + if (x < 0) continue; + struct pixel { unsigned char blue; unsigned char green; unsigned char red; unsigned char alpha; } *px = (struct pixel *)(canvas.data + y * canvas.stride + x * 4); + px->red = r; + px->green = g; + px->blue = b; + px->alpha = a; + } + } +} - // draw a stripes pattern - if ((x + y) % 30 < 10) - { - // transparent - px->alpha = 0; +void redraw() +{ + if (need_resize) { + canvas_resize(logical_width, logical_height, current_scale); + } + + clock_t start = clock(); + + /* update cursor from pointer */ + cursor_x = pointer_data.x; + cursor_y = pointer_data.y; + + /* handle pending click placement */ + if (click_pending) { + click_pending = 0; + if (active_tool >= 0) { + int cx = (int)(click_x / 8.0); + int cy = (int)(click_y / 8.0); + int dx[16], dy[16]; + int n = pattern_rotated_offsets(active_tool, active_rotation, dx, dy); + int ok = 1; + for (int i = 0; i < n; i++) { + int c = cx + dx[i]; + int r = cy + dy[i]; + if (c < 0 || r < 0 || c >= grid.cols || r >= grid.rows) { ok = 0; break; } } - else if ((x + y) % 30 < 20) - { - // yellow - px->alpha = 255; - px->red = 255; - px->green = 255; - px->blue = 0; - } - else - { - // semitransparent red - px->alpha = 128; - px->red = mycolour; - px->green = 0; - px->blue = (255 - mycolour); + if (ok) { + for (int i = 0; i < n; i++) { + grid_set(&grid, cx + dx[i], cy + dy[i], 1); + } } } } + + int cell_px = (grid.cols > 0) ? (canvas.width / grid.cols) : 8; + if (cell_px < 1) cell_px = 1; + + /* clear background */ + fill_rect(0, 0, canvas.width, canvas.height, 20, 20, 24, 255); + + /* grid lines */ + if (show_grid_lines && grid.show_lines) { + for (int c = 0; c <= grid.cols; c++) { + int x = c * cell_px; + fill_rect(x, 0, 1, canvas.height, 40, 40, 48, 255); + } + for (int r = 0; r <= grid.rows; r++) { + int y = r * cell_px; + fill_rect(0, y, canvas.width, 1, 40, 40, 48, 255); + } + } + + /* alive cells */ + for (int r = 0; r < grid.rows; r++) { + for (int c = 0; c < grid.cols; c++) { + if (grid_get(&grid, c, r)) { + fill_rect(c * cell_px + 1, r * cell_px + 1, cell_px - 2, cell_px - 2, 80, 200, 120, 255); + } + } + } + + /* preview */ + if (active_tool >= 0) { + int cx = (int)(cursor_x / 8.0); + int cy = (int)(cursor_y / 8.0); + int dx[16], dy[16]; + int n = pattern_rotated_offsets(active_tool, active_rotation, dx, dy); + int ok = 1; + for (int i = 0; i < n; i++) { + int c = cx + dx[i]; + int r = cy + dy[i]; + if (c < 0 || r < 0 || c >= grid.cols || r >= grid.rows) { ok = 0; break; } + } + unsigned char pr = ok ? 220 : 220; + unsigned char pg = ok ? 200 : 80; + unsigned char pb = ok ? 80 : 80; + for (int i = 0; i < n; i++) { + int c = cx + dx[i]; + int r = cy + dy[i]; + fill_rect(c * cell_px + 2, r * cell_px + 2, cell_px - 4, cell_px - 4, pr, pg, pb, 160); + } + } + // callback canvas.frame = wl_surface_frame(canvas.surface); wl_callback_add_listener(canvas.frame, &wl_surface_frame_listener, NULL);