[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\njobs:\n  build:\n    machine: true\n    steps:\n      - checkout\n      - run: docker build -t tym .\n      - run: docker build -t tym-luajit --build-arg EXTRA_CONF=--enable-luajit .\n      - run: docker run tym\n      - run: docker run tym-luajit\n"
  },
  {
    "path": ".dockerignore",
    "content": ".git\n\n*.l[ao]\n*.o\n*~\n.deps/\n.dirstamp\n.libs/\nMakefile\nMakefile.in\naclocal.m4\nautom4te.cache/\ncompile\nconfig.*\nconfigure\ndepcomp\ninstall-sh\nlibtool\nltmain.sh\nm4/\nmissing\nstamp-h?\n\nsrc/version.h\ntym\ntym-*.tar.gz\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*.{c,h}]\ncharset = utf-8\nindent_style = space\nindent_size = 2\ntab_width = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitignore",
    "content": "*.log\n*.trs\n*.l[ao]\n*.o\n*~\n.cache\n.dirstamp\nMakefile\nMakefile.in\naclocal.m4\ncompile\nconfigure\ndepcomp\ninstall-sh\nlibtool\nltmain.sh\nmissing\nstamp-h?\n.deps/\n.libs/\nautom4te.cache/\nm4/\n\ntym.1\ntym\ntym-test\ninclude/common.h\ntym-daemon.service\n/config.*\n/app-config.*\ntym-*.tar.gz\ntest-driver\n\n.ccls\n.ccls-root\n.ccls-cache/\ncompile_commands.json\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM ubuntu:22.04\n\nENV DEBIAN_FRONTEND=noninteractive\nRUN apt-get update\nRUN apt-get install -y build-essential autoconf libgtk-3-dev libvte-2.91-dev liblua5.3-dev libluajit-5.1-dev libpcre2-dev git xvfb tzdata\n\nRUN mkdir -p /var/app\nADD . /var/app\nWORKDIR /var/app\n\nARG EXTRA_CONF=\nRUN autoreconf -fvi\nRUN ./configure $EXTRA_CONF\nRUN make\nRUN make check\nCMD xvfb-run -a ./src/tym -u ./lua/e2e.lua\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 endaaman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile.am",
    "content": "SUBDIRS = src include\nman_MANS = tym.1\ndesktopdir = $(datadir)/applications\nEXTRA_DIST = $(man_MANS)\ndist_desktop_DATA = tym.desktop tym-daemon.desktop\n\nsystemunitdir = $(libdir)/systemd/user/\ndist_systemunit_DATA = tym-daemon.service\n"
  },
  {
    "path": "README.md",
    "content": "# tym\n\n[![CircleCI](https://circleci.com/gh/endaaman/tym.svg?style=svg)](https://circleci.com/gh/endaaman/tym) [![Discord](https://img.shields.io/discord/1065853670371119124?label=chat%20on%20discord)](https://discord.gg/Ftt8PGYmJY)\n\n`tym` is a Lua-configurable terminal emulator base on [VTE](https://gitlab.gnome.org/GNOME/vte).\n\n## Installation\n\n### Arch Linux\n\n```\n$ yay -S tym\n```\n\n### NixOS\n\n```\n$ nix-env -iA nixos.tym\n```\n\n### Other distros\n\nDownload the latest release from [Releases](https://github.com/endaaman/tym/releases), extract it and run as below\n\n```\n$ ./configure\n$ sudo make install\n```\n\n<details><summary>Build dependencies (click to open)</summary>\n<p>\n\n#### Arch Linux\n\n```\n$ sudo pacman -S vte3 lua53\n```\n\n#### Ubuntu\n\n```\n$ sudo apt install libgtk-3-dev libvte-2.91-dev liblua5.3-dev libpcre2-dev\n```\n\n#### Void Linux\n```\n$ sudo xbps-install -S vte3-devel lua-devel\n```\n\n#### Other distros / macOS / Windows\n\nWe did not check which packages are needed to build on other distros or OS. We are waiting for your contribution ;)\n\n</p>\n</details>\n\n## Configuration\n\nIf `$XDG_CONFIG_HOME/tym/config.lua` exists, it is executed when the app starts. You can change the path with the `--use`/`-u` option.\n\n```lua\n-- At first, you need to require tym module\nlocal tym = require('tym')\n\n-- set individually\ntym.set('width', 100)\n\ntym.set('font', 'DejaVu Sans Mono 11')\n\n-- set by table\ntym.set_config({\n  shell = '/usr/bin/fish',\n  cursor_shape = 'underline',\n  autohide = true,\n  color_foreground = 'red',\n})\n```\n\nSee [wiki](https://github.com/endaaman/tym/wiki) to check out the advanced examples.\n\nAll available config values are shown below.\n\n| field name | type | default value | description |\n| --- | --- | --- | --- |\n| `shell` | string | `$SHELL` → `vte_get_user_shell()` → `'/bin/sh'` | Shell to execute. |\n| `term` | string | `'xterm-256color'` | Value of `$TERM`. |\n| `title` | string | `'tym'` | Initial window title. |\n| `font` | string | `''` | You can specify font with `'FAMILY-LIST [SIZE]'`, for example `'Ubuntu Mono 12'`. The value is parsed by [`pango_font_description_from_string()`](https://developer.gnome.org/pango/stable/pango-Fonts.html#pango-font-description-from-string). If empty string is set, the system default fixed width font will be used. |\n| `icon` | string | `'utilities-terminal'` | Name of icon. cf. [Icon Naming Specification](https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html) |\n| `role` | string | `''` | Unique identifier for the window. If empty string is set, no value set. (cf. [gtk_window_set_role()](https://developer.gnome.org/gtk3/stable/GtkWindow.html#gtk-window-set-role)) |\n| `cursor_shape` | string | `'block'` | `'block'`, `'ibeam'` or `'underline'` can be used. |\n| `cursor_blink_mode` | string | `'system'` | `'system'`, `'on'` or `'off'` can be used. |\n| `cjk_width` | string | `'narrow'` | `'narrow'` or `'wide'` can be used. |\n| `background_image` | string | `''` | Path to background image file. |\n| `uri_schemes` | string | `'http https file mailto'` | Space-separated list of URI schemes to be highlighted and clickable. Specify empty string to disable highlighting. Specify `'*'` to accept any strings valid as schemes (according to RFC 3986). |\n| `width` | integer | `80` | Initial columns. |\n| `height` | integer | `22` | Initial rows. |\n| `scale` | integer | `100` | Font scale in **percent(%)** |\n| `cell_width` | integer | `100` | Cell width scale in **percent(%)**. |\n| `cell_height` | integer | `100` | Cell height scale in **percent(%)**.  |\n| `padding_top`  | integer | `0` | Top padding. |\n| `padding_bottom`  | integer | `0` | Bottom padding. |\n| `padding_left`  | integer | `0` | Left padding. |\n| `padding_right`  | integer | `0` | Right padding. |\n| `scrollback_length` | integer | `512` | Length of the scrollback buffer. |\n| `scrollback_on_output` | boolean | `true` | Whether to scroll the buffer when the new data is output. |\n| `ignore_default_keymap` | boolean | `false` | Whether to use default keymap. |\n| `autohide` | boolean | `false` | Whether to hide mouse cursor when the user presses a key. |\n| `silent` | boolean | `false` | Whether to beep when bell sequence is sent. |\n| `bold_is_bright` | boolean | `false` | Whether to make bold texts bright. |\n| `color_window_background` | string | `''` | Color of the terminal window. It is seen when `'padding_horizontal'` `'padding_vertical'` is not `0`. If you set `'NONE'`, the window background will not be drawn. |\n| `color_foreground`, `color_background`, `color_cursor`, `color_cursor_foreground`, `color_highlight`, `color_highlight_foreground`, `color_bold`, `color_0` ... `color_15` | string | [See the next section](#user-content-theme-customization) | You can specify standard color string such as `'#f00'`, `'#ff0000'`, `'rgba(22, 24, 33, 0.7)'` or `'red'`. It will be parsed by [`gdk_rgba_parse()`](https://developer.gnome.org/gdk3/stable/gdk3-RGBA-Colors.html#gdk-rgba-parse). If empty string is set, the VTE default color will be used. If you set `'NONE'` for `color_background`, the terminal background will not be drawn.|\n\n\n## Theme customization\n\nWhen `$XDG_CONFIG_HOME/tym/theme.lua` exists, it is loaded **before** loading config. You can change the path by using the `--theme`/`-t` option. The following is an example, whose color values are built-in default. They were ported from [iceberg](https://cocopon.github.io/iceberg.vim/).\n\n```lua\nlocal bg = '#161821'\nlocal fg = '#c6c8d1'\nreturn {\n  color_background = bg,\n  color_foreground = fg,\n  color_bold = fg,\n  color_cursor = fg,\n  color_cursor_foreground = bg,\n  color_highlight = fg,\n  color_highlight_foreground = bg,\n  color_0  = bg,\n  color_1  = '#e27878',\n  color_2  = '#b4be82',\n  color_3  = '#e2a478',\n  color_4  = '#84a0c6',\n  color_5  = '#a093c7',\n  color_6  = '#89b8c2',\n  color_7  = fg,\n  color_8  = '#6b7089',\n  color_9  = '#e98989',\n  color_10 = '#c0ca8e',\n  color_11 = '#e9b189',\n  color_12 = '#91acd1',\n  color_13 = '#ada0d3',\n  color_14 = '#95c4ce',\n  color_15 = '#d2d4de',\n}\n```\n\nYou need to return the color map as table.\n\n<details><summary>Color correspondence (click to open)</summary>\n<div>\n\n```\ncolor_0  : black (background)\ncolor_1  : red\ncolor_2  : green\ncolor_3  : brown\ncolor_4  : blue\ncolor_5  : purple\ncolor_6  : cyan\ncolor_7  : light gray (foreground)\ncolor_8  : gray\ncolor_9  : light red\ncolor_10 : light green\ncolor_11 : yellow\ncolor_12 : light blue\ncolor_13 : pink\ncolor_14 : light cyan\ncolor_15 : white\n```\n\n</div>\n</details>\n\n\n## Keymap\n\n### Default keymap\n\n| Key             | Action                       |\n| :-------------- | :--------------------------- |\n| Ctrl Shift c    | Copy selection to clipboard. |\n| Ctrl Shift v    | Paste from clipboard.        |\n| Ctrl Shift r    | Reload config file.          |\n\n### Customizing keymap\n\nYou can register keymap(s) using `tym.set_keymap(accelerator, func)` or `tym.set_keymaps(table)`. `accelerator` must be in a format parsable by [gtk_accelerator_parse()](https://developer.gnome.org/gtk3/stable/gtk3-Keyboard-Accelerators.html#gtk-accelerator-parse). If a truthy value is returned, the event propagation will **not be stopped**.\n\n```lua\n-- also can set keymap\ntym.set_keymap('<Ctrl><Shift>o', function()\n  local h = tym.get('height')\n  tym.set('height', h + 1)\n  tym.notify('Set window height :' .. h)\nend)\n\n-- set by table\ntym.set_keymaps({\n  ['<Ctrl><Shift>t'] = function()\n    tym.reload()\n    tym.notify('reload config')\n  end,\n  ['<Ctrl><Shift>v'] = function()\n    -- reload and notify\n    tym.send_key('<Ctrl><Shift>t')\n  end,\n\n  ['<Shift>y'] = function()\n    tym.notify('Y has been pressed')\n    return true -- notification is shown and `Y` will be inserted\n  end,\n  ['<Shift>w'] = function()\n    tym.notify('W has been pressed')\n    -- notification is shown but `W` is not inserted\n  end,\n})\n```\n\n## Lua API\n\n| Name                                 | Return value | Description |\n| ------------------------------------ | ------------ | ----------- |\n| `tym.get(key)`                       | any      | Get config value. |\n| `tym.set(key, value)`                | void     | Set config value. |\n| `tym.get_default_value(key)`         | any      | Get default config value. |\n| `tym.get_config()`                   | table    | Get whole config. |\n| `tym.set_config(table)`              | void     | Set config by table. |\n| `tym.reset_config()`                 | void     | Reset all config. |\n| `tym.set_keymap(accelerator, func)`  | void     | Set keymap. |\n| `tym.unset_keymap(accelerator)`      | void     | Unset keymap. |\n| `tym.set_keymaps(table)`             | void     | Set keymaps by table. |\n| `tym.reset_keymaps()`                | void     | Reset all keymaps. |\n| `tym.set_hook(hook_name, func)`      | void     | Set a hook. |\n| `tym.set_hooks(table)`               | void     | Set hooks. |\n| `tym.reload()`                       | void     | Reload config file.|\n| `tym.reload_theme()`                 | void     | Reload theme file. |\n| `tym.send_key()`                     | void     | Send key press event. |\n| `tym.signal(id, hook, {param...})`   | void     | Send signal to the tym instance specified by id. |\n| `tym.set_timeout(func, interval=0)`  | int(tag) | Set timeout. return true in func to execute again. |\n| `tym.clear_timeout(tag)`             | void     | Clear the timeout. |\n| `tym.put(text)`                      | void     | Feed text. |\n| `tym.bell()`                         | void     | Sound bell. |\n| `tym.open(uri)`                      | void     | Open URI via your system default app like `xdg-open(1)`. |\n| `tym.notify(message, title='tym')`   | void     | Show desktop notification. |\n| `tym.copy(text, target='clipboard')` | void     | Copy text to clipboard. As `target`, `'clipboard'`, `'primary'` or `secondary` can be used. |\n| `tym.copy_selection(target='clipboard')` | void | Copy current selection. |\n| `tym.paste(target='clipboard')`      | void     | Paste clipboard. |\n| `tym.check_mod_state(accelerator)`   | bool     | Check if the mod key(such as `'<Ctrl>'` or `<Shift>`) is being pressed. |\n| `tym.color_to_rgba(color)`           | r, g, b, a | Convert color string to RGB bytes and alpha float using [`gdk_rgba_parse()`](https://developer.gnome.org/gdk3/stable/gdk3-RGBA-Colors.html#gdk-rgba-parse). |\n| `tym.rgba_to_color(r, g, b, a)`      | string   | Convert RGB bytes and alpha float to color string like `rgba(255, 128, 0, 0.5)` can be used in color option such as `color_background`. |\n| `tym.rgb_to_hex(r, g, b)`            | string   | Convert RGB bytes to 24bit HEX like `#ABCDEF`. |\n| `tym.hex_to_rgb(hex)`                | r, g, b  | Convert 24bit HEX like `#ABCDEF` to RGB bytes. |\n| `tym.get_monitor_model()`            | string   | Get monitor model on which the window is shown. |\n| `tym.get_cursor_position()`          | int, int | Get where column and row the cursor is. |\n| `tym.get_clipboard(target='clipboard')` | string | Get content in the clipboard. |\n| `tym.get_selection()`                | string   | Get selected text. |\n| `tym.has_selection()`                | bool     | Get if selected. |\n| `tym.select_all()`                   | void     | Select all texts. |\n| `tym.unselect_all()`                 | void     | Unselect all texts. |\n| `tym.get_text(start_row, start_col, end_row, end_col)` | string | Get text on the terminal screen. If you set `-1` to `end_row` and `end_col`, the target area will be the size of termianl. |\n| `tym.get_config_path()`              | string   | Get full path to config file. |\n| `tym.get_theme_path()`               | string   | Get full path to theme file. |\n| `tym.get_terminal_pid()`             | integer  | Get terminal pid. |\n| `tym.get_pid()`                      | integer  | Get child pid(usually shell's pid). |\n| `tym.get_ids()`                      | table[int] | Get tym instance ids. |\n| `tym.get_version()`                  | string   | Get version string. |\n\n### Hooks\n\n| Name | Param | Default action | Description |\n| --- | --- | --- | --- |\n| `title`       | title  | changes title | If string is returned, it will be used as the new title. |\n| `bell`        | nil    | makes the window urgent when it is inactive. | If true is returned, the window will not be urgent. |\n| `clicked`     | button, uri | If URI exists under cursor, opens it | Triggered when mouse button is pressed. |\n| `scroll`      | delta_x, delta_y, mouse_x, mouse_y  | scroll buffer | Triggered when mouse wheel is scrolled. |\n| `drag`        | filepath  | feed filepath to the console | Triggered when files are dragged to the screen. |\n| `activated`   | nil    | nothing | Triggered when the window is activated. |\n| `deactivated` | nil    | nothing | Triggered when the window is deactivated. |\n| `resized`     | nil    | nothing | Triggered when the window is resized. |\n| `selected`    | string | nothing | Triggered when the text in the terminal screen is selected. |\n| `unselected`  | nil    | nothing | Triggered when the selection is unselected. |\n| `signal`      | string | nothing | Triggered when `me.endaaman.tym.hook` signal is received. |\n\nIf truthy value is returned in a callback function, the default action will be **stopped**.\n\n```lua\ntym.set_hooks({\n  title = function(t)\n    tym.set('title', 'tym - ' .. t)\n    return true -- this is needed to cancenl default title application\n  end,\n})\n\n--- NOTE:\n-- If you set the hook to 'clicked' handler, you need to open URI manually like below,\ntym.set_hook('clicked', function(button, uri)\n  print('you pressed button:', button) -- 1:left, 2:middle, 3:right\n\n  -- open URI only by middle click\n  if button == 2 then\n    if uri then\n      print('you clicked URI: ', uri)\n      tym.open(uri)\n      -- disable the default action 'put clipboard' when open URI\n      return true\n    end\n  end\nend)\n```\n\n## Interprocess communication using D-Bus\n\nEach tym window has an unique ID, which can be checked by `tym.get_id()` or `$TYM_ID`, and also listen to D-Bus signal/method call on the path `/me/endaaman/tym<ID>` and the interface name `me.endaaman.tym`.\n\n### Signals\n\n| Name | Input(D-Bus signature) | Description |\n| ---- | --- | --- |\n| `hook` | `s` | Triggers `signal` hook. |\n\nFor example, when you prepare the following config and command,\n\n```lua\nlocal tym = require('tym')\ntym.set_hook('signal', function (p)\n  print('Hello from DBus signal')\n  print('param:', p)\nend)\n```\n\n```\n$ dbus-send /me/endaaman/tym0 me.endaaman.tym.hook string:'THIS IS PARAM'\n```\n\nor\n\n```lua\ntym.signal(0, 'hook', {'THIS IS PARAM'}) -- NOTICE: param must be table\n```\n\nyou will get an output like below.\n\n```\nHello from DBus signal\nparam:  THIS IS PARAM\n```\n\nAlternatively, you can use `tym` command to send signal.\n\n```\n$ tym --signal hook --dest 0 --param 'THIS IS PARAM'\n```\n\nIf the target window is its own one, it will the value of `$TYM_ID` and `--dest` can be omitted. So it is enough like below.\n\n```\n$ tym --signal hook --param 'THIS IS PARAM'\n```\n\n\n### Methods\n\n| Name | Input (D-Bus signature) | Output (D-Bus signature) | Description |\n| ---- | --- | --- | --- |\n| `get_ids` | None | `ai` | Get all tym instance IDs. |\n| `echo` | `s` | `s` | Echo output the same as input. |\n| `eval` | `s` | `s` | Evaluate one line lua script. `return` is needed. |\n| `eval_file` | `s` | `s` | Evaluate a script file. `return` is needed. |\n| `exec` | `s` | None | Execute one line lua script without outputs. |\n| `eval_file` | `s` | None | Execute a script filt without outputs. |\n\n\nFor example, when you exec the command,\n\n```\n$ dbus-send --print-reply --type=method_call --dest=me.endaaman.tym /me/endaaman/tym0 me.endaaman.tym.eval string:'return \"title is \" .. tym.get(\"title\")'\n```\n\nthen you will get like below.\n\n```\nmethod return time=1646287109.007168 sender=:1.3633 -> destination=:1.3648 serial=39 reply_serial=2\n   string \"title is tym\"\n```\n\nAs same as signals, you can use `tym` command to execute method calling.\n\n```\n$ tym --call eval --dest 0 --param 'return \"title is \" .. tym.get(\"title\")'\n```\n\nOf course, `--dest` can be omitted as well.\n\n\n## Options\n\n### `--help` `-h`\n\n```\n$ tym -h\n```\n\n### `--use=<path>` `-u <path>`\n\n```\n$ tym --use=/path/to/config.lua\n```\n\nIf `NONE` is provided, all config will be default (user-defined config file will not be loaded).\n\n```\n$ tym -u NONE\n```\n\n### `--theme=<path>` `-t <path>`\n\n```\n$ tym --use=/path/to/theme.lua\n```\n\nIf `NONE` is provided, default theme will be used.\n\n```\n$ tym -t NONE\n```\n\n### `--signal=<signal name>` `-s <signal name>`\n\n```\n$ tym --signal hook\n```\n\nSends a D-Bus signal to the current instance (determined by `$TYM_ID` environment value). To send to another instance, use `--dest` (or `-d`) option.\n\n\n### `--call=<method name>` `-c <method name>`\n\nCalls D-Bus method of the current instance (determined by `$TYM_ID` environment value). To call it of another instance, provide `--dest` (or `-d`) option.\n\n```\n$ tym --call eval --param 'return 1 + 2'\n```\n\n### `--daemon`\n\nThis makes tym a daemon process, which has no window or application context.\n\n```\n$ tym --daemon\n```\n\nTo enable the daemon feature, set `tym-daemon.desktop` as auto-started on the DE's settings or add the line `tym --daemon &` in your `.xinitrc`.\n\n\n### `--cwd=<path>`\n\nThis sets the terminal's working directory. `<path>` must be an absolute path. If unspecified `tym` will use the current working directory of the terminal invocation.\n\n```console\n$ tym --cwd=/home/user/projects\n```\n\n### `--<config option>`\n\nYou can set config value via command line option.\n\n```console\n$ tym --shell=/bin/zsh --color_background=red --width=40 --ignore_default_keymap\n```\n\n### `--isolated`\n\n```console\n$ tym --isolated\n```\n\nThis option enables tym to create a separate process for each instance. Then an app instance will be isolated from D-Bus and no longer have ability to handle D-Bus signals/method calls.\n\n\n### `--` (\"double dash\" option)\n\ntym also accepts double dash `--` option as the command line to spawn.\n\n```console\n$ tym -- less -N Dockerfile\n```\n\n## Development\n\nClone this repo and run as below\n\n```console\n$ autoreconf -fvi\n$ ./configure --enable-debug\n$ make && ./src/tym -u ./path/to/config.lua   # for debug\n$ make check; cat src/tym-test.log            # for unit tests\n```\n\nRun tests in docker container\n\n```console\n$ docker build -t tym .\n$ docker run tym\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "autotools.mk",
    "content": "all:\n\nclean:\n\trm -f configure Makefile.in config.h.in aclocal.m4\n\trm -f install-sh missing depcomp compile\n\trm -rf autom4te.cache\n\trm -f *~\n\nrescan:\n\tautoscan\n\nreconfigure:\n\tautoreconf -i\n\nconfigure:\tconfigure.in aclocal.m4 Makefile.in config.h.in\n\tautoconf\n\nMakefile.in:\tMakefile.am config.h.in\n\tautomake -a -c\n\nconfig.h.in:\tconfigure.in\n\tautoheader\n\naclocal.m4:\tconfigure.in\n\taclocal\n"
  },
  {
    "path": "configure.ac",
    "content": "m4_define([tym_major_version],[3])\nm4_define([tym_minor_version],[5])\nm4_define([tym_micro_version],[2])\nm4_define([tym_version],[tym_major_version().tym_minor_version().tym_micro_version()])\n\nAC_PREREQ([2.69])\nAC_INIT([tym], [tym_version()], [], [tym], [https://github.com/endaaman/tym])\nAM_INIT_AUTOMAKE([foreign])\nDATE=\"`date '+%Y-%m-%d'`\"\nAC_SUBST(DATE)\nAC_CONFIG_SRCDIR([src/tym.c])\nAC_CONFIG_HEADERS([app-config.h], [])\nAC_CONFIG_FILES([\n  Makefile\n  src/Makefile\n  include/Makefile\n  include/common.h\n  tym-daemon.service\n  tym.1\n])\n\nAC_PROG_CC\nPKG_PROG_PKG_CONFIG\nPKG_CHECK_MODULES(TYM, [gtk+-3.0 vte-2.91 libpcre2-8])\n\nAC_ARG_ENABLE(luajit,\n  [AC_HELP_STRING([--enable-luajit], [use LuaJIT instead of the official Lua interpreter(default=no)])],\n  [\\\n  case \"${enableval}\" in\n    yes) enable_luajit=yes ;;\n    no)  enable_luajit=no ;;\n    *) AC_MSG_ERROR(bad value for --enable-luajit) ;;\n  esac],\n  [enable_luajit=no]\n)\nif test x\"${enable_luajit}\" = x\"yes\"; then\n  PKG_CHECK_MODULES(LUA, [luajit])\n  AC_DEFINE([USES_LUAJIT], 1, [Define to 1 to enable LuaJIT specific code])\nelse\n  PKG_CHECK_MODULES(LUA, [lua], [], [\n    PKG_CHECK_MODULES(LUA, [lua5.3])\n  ])\nfi\n\nAC_ARG_ENABLE(debug,\n  [AC_HELP_STRING([--enable-debug],[turn on debugging(default=no)])],\n  [\\\n  case \"${enableval}\" in\n   yes) enable_debug=yes ;;\n   no)  enable_debug=no ;;\n   *)   AC_MSG_ERROR(bad value for --enable-debug) ;;\n  esac],\n  [enable_debug=no]\n)\nif test x\"${enable_debug}\" = x\"yes\"; then\n  AC_DEFINE(DEBUG, 1, [Define to 1 if you want to debug])\nfi\nAM_CONDITIONAL([DEBUG], [test \"$enable_debug\" = yes])\n\n# --enable-old-vte\nAC_ARG_ENABLE(old-vte,\n  [AC_HELP_STRING([--enable-old-vte], [use old VTE API(default=no)])],\n  [\\\n  case \"${enableval}\" in\n    yes) enable_old_vte=yes ;;\n    no)  enable_old_vte=no ;;\n    *) AC_MSG_ERROR(bad value for --enable-old-vte) ;;\n  esac],\n  [enable_old_vte=no]\n)\n\nif test x\"${enable_old_vte}\" = x\"yes\"; then\n  AC_DEFINE([TYM_USE_OLD_VTE], 1, [Define to 1 if using old VTE API])\nfi\n\nAC_OUTPUT\n"
  },
  {
    "path": "include/Makefile.am",
    "content": "noinst_HEADERS = \\\n\tapp.h \\\n\tbuiltin.h \\\n\tcommand.h \\\n\tcommon.h \\\n\tconfig.h \\\n\tcontext.h \\\n\thook.h \\\n\tipc.h \\\n\tkeymap.h \\\n\tmeta.h \\\n\toption.h \\\n\tproperty.h \\\n\tregex.h \\\n\ttym.h\n\ttym_test.h\n"
  },
  {
    "path": "include/app.h",
    "content": "/**\n * app.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef APP_H\n#define APP_H\n\n#include \"context.h\"\n#include \"meta.h\"\n#include \"ipc.h\"\n\ntypedef struct {\n  GApplication* gapp;\n  Meta* meta;\n  IPC* ipc;\n  GList* contexts;\n  bool is_isolated;\n} App;\n\nextern App* app;\n\nvoid app_init();\nvoid app_close();\nvoid app_quit_context(Context* context);\nint app_start(Option* option, int argc, char **argv);\n\n#endif\n"
  },
  {
    "path": "include/builtin.h",
    "content": "/**\n * builtin.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef BUILTIN_H\n#define BUILTIN_H\n\n#include \"common.h\"\n\n\nint builtin_register_module(lua_State* L);\n\n#endif\n"
  },
  {
    "path": "include/command.h",
    "content": "/**\n * commad.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef COMMAND_H\n#define COMMAND_H\n\n#include \"common.h\"\n#include \"context.h\"\n\n\nvoid command_reload(Context* context);\nvoid command_reload_theme(Context* context);\nvoid command_copy_selection(Context* context);\nvoid command_paste(Context* context);\n\n#endif\n"
  },
  {
    "path": "include/common.h.in",
    "content": "/**\n * common.h\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef COMMON_H\n#define COMMON_H\n\n#include <stdlib.h>\n#include <stdbool.h>\n#include <string.h>\n#include <assert.h>\n#include <lua.h>\n#include <lualib.h>\n#include <lauxlib.h>\n#include <gtk/gtk.h>\n#include <vte/vte.h>\n#define PCRE2_CODE_UNIT_WIDTH 8\n#include <pcre2.h>\n\n#include \"../app-config.h\"\n\n#define __PRE_IDENTITY(x) #x\n#define __IDENTITY(x) __PRE_IDENTITY(x)\n#define TYM_VTE_VERSION __IDENTITY(VTE_MAJOR_VERSION) \".\" __IDENTITY(VTE_MINOR_VERSION) \".\" __IDENTITY(VTE_MICRO_VERSION)\n\n#ifdef DEBUG\n#define TYM_APP_ID \"me.endaaman.tym_dev\"\n#define TYM_APP_ID_ISOLATED \"me.endaaman.tym_isolated_dev\"\n#define TYM_OBJECT_PATH_BASE \"/me/endaaman/tym_dev\"\n#else\n#define TYM_APP_ID \"me.endaaman.tym\"\n#define TYM_APP_ID_ISOLATED \"me.endaaman.tym_isolated\"\n#define TYM_OBJECT_PATH_BASE \"/me/endaaman/tym\"\n#endif\n\n#define TYM_OBJECT_PATH_FMT_INT TYM_OBJECT_PATH_BASE\"%d\"\n#define TYM_OBJECT_PATH_FMT_STR TYM_OBJECT_PATH_BASE\"%s\"\n\n#define TYM_ERROR_INVALID_METHOD_CALL 0\n\n#define TYM_CONFIG_DIR_NAME \"tym\"\n#define TYM_CONFIG_FILE_NAME \"config.lua\"\n#define TYM_THEME_FILE_NAME \"theme.lua\"\n#define TYM_SYMBOL_NONE \"NONE\"\n#define TYM_FALL_BACK_SHELL \"/bin/sh\"\n#define TYM_SYMBOL_WILDCARD \"*\"\n\n#define TYM_CURSOR_SHAPE_BLOCK \"block\"\n#define TYM_CURSOR_SHAPE_IBEAM \"ibeam\"\n#define TYM_CURSOR_SHAPE_UNDERLINE \"underline\"\n#define TYM_CURSOR_BLINK_MODE_SYSTEM \"system\"\n#define TYM_CURSOR_BLINK_MODE_ON \"on\"\n#define TYM_CURSOR_BLINK_MODE_OFF \"off\"\n#define TYM_CJK_WIDTH_NARROW \"narrow\"\n#define TYM_CJK_WIDTH_WIDE \"wide\"\n\n#define TYM_CLIPBOARD_CLIPBOARD \"clipborad\"\n#define TYM_CLIPBOARD_PRIMARY \"primary\"\n#define TYM_CLIPBOARD_SECONDARY \"secondary\"\n\n#define TYM_DEFAULT_TITLE \"tym\"\n#define TYM_DEFAULT_ICON \"utilities-terminal\"\n#define TYM_DEFAULT_TERM \"xterm-256color\"\n#define TYM_DEFAULT_CURSOR_SHAPE TYM_CURSOR_SHAPE_BLOCK\n#define TYM_DEFAULT_CURSOR_BLINK_MODE TYM_CURSOR_BLINK_MODE_SYSTEM\n#define TYM_DEFAULT_CJK TYM_CJK_WIDTH_NARROW\n#define TYM_DEFAULT_URI_SCHEMES \"http https file mailto\"\nextern const int TYM_DEFAULT_WIDTH;\nextern const int TYM_DEFAULT_HEIGHT;\nextern const int TYM_DEFAULT_SCALE;\nextern const int TYM_DEFAULT_CELL_SIZE;\nextern const int TYM_DEFAULT_SCROLLBACK;\n\n/* theme: iceberg (https://cocopon.github.io/iceberg.vim/) */\n#define TYM_DEFAULT_COLOR_0  \"#161821\"\n#define TYM_DEFAULT_COLOR_1  \"#e27878\"\n#define TYM_DEFAULT_COLOR_2  \"#b4be82\"\n#define TYM_DEFAULT_COLOR_3  \"#e2a478\"\n#define TYM_DEFAULT_COLOR_4  \"#84a0c6\"\n#define TYM_DEFAULT_COLOR_5  \"#a093c7\"\n#define TYM_DEFAULT_COLOR_6  \"#89b8c2\"\n#define TYM_DEFAULT_COLOR_7  \"#c6c8d1\"\n#define TYM_DEFAULT_COLOR_8  \"#6b7089\"\n#define TYM_DEFAULT_COLOR_9  \"#e98989\"\n#define TYM_DEFAULT_COLOR_10 \"#c0ca8e\"\n#define TYM_DEFAULT_COLOR_11 \"#e9b189\"\n#define TYM_DEFAULT_COLOR_12 \"#91acd1\"\n#define TYM_DEFAULT_COLOR_13 \"#ada0d3\"\n#define TYM_DEFAULT_COLOR_14 \"#95c4ce\"\n#define TYM_DEFAULT_COLOR_15 \"#d2d4de\"\n#define TYM_DEFAULT_COLOR_BACKGROUND TYM_DEFAULT_COLOR_0\n#define TYM_DEFAULT_COLOR_FOREGROUND TYM_DEFAULT_COLOR_7\n\n#define UNUSED(x) (void)(x)\n#define BUILD_DATE \"@DATE@\"\n\n\n/* compat definition */\n#ifndef LUA_LOADED_TABLE\n#define LUA_LOADED_TABLE \"_LOADED\"\n#endif\n\n/* use g_memdup2 if glib >= 2.66 */\n#if GLIB_CHECK_VERSION(2, 66, 0)\n#define memdup g_memdup2\n#else\n#define memdup g_memdup\n#endif\n\n/* Switch to use old api */\n/* #define TYM_USE_OLD_API */\n#ifndef TYM_USE_OLD_API /* START: TYM_USE_OLD_VTE */\n\n#if GDK_MAJOR_VERSION == 3\n#if GDK_MINOR_VERSION >= 20\n#define TYM_USE_GDK_SEAT\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 48\n#define TYM_USE_VTE_SPAWN_ASYNC\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 50\n#define TYM_USE_VTE_COPY_CLIPBOARD_FORMAT\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 46\n#define TYM_USE_VTE_COLOR_CURSOR_FOREGROUND\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 52\n#define TYM_USE_TRANSPARENT\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 62\n#define TYM_USE_SIXEL\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 72\n#define TYM_USE_VTE_GET_TEXT_RANGE_FORMAT\n#endif\n#endif\n\n#if VTE_MAJOR_VERSION == 0\n#if VTE_MINOR_VERSION >= 78\n#define TYM_USE_VTE_TERMPROP\n#endif\n#endif\n\n#endif /* END: TYM_USE_OLD_VTE */\n\n\n#ifdef DEBUG /* START: DEBUG */\n#define dd( fmt, ... ) \\\n  g_print(\"[%-10s:%3u I] \" fmt \"\\n\", __FILE__, __LINE__, ##__VA_ARGS__)\n\n#define dw( fmt, ... ) \\\n  g_print(\"[%-10s:%3u W] \" fmt \"\\n\", __FILE__, __LINE__, ##__VA_ARGS__)\n\n#define df( fmt, ... ) \\\n  g_print(\"[%-10s:%3u F] %s()\\n\", __FILE__, __LINE__, __func__)\n\nvoid debug_dump_stack(lua_State* L, char* file, unsigned line);\n#define ds(lua_state) \\\n  debug_dump_stack((lua_state), __FILE__, __LINE__)\n\n#else /* ELSE: DEBUG */\n\n#define dd(...) ((void)0)\n#define dw(...) ((void)0)\n#define df(...) ((void)0)\n#define ds(...) ((void)0)\n\n#endif /* END: DEBUG */\n\nint roundup(double x);\nbool is_equal(const char* a, const char* b);\nbool is_none(const char* s);\nbool is_empty(const char* s);\nvoid luaX_requirec(lua_State* L, const char* modname, lua_CFunction openf, int glb, void* userdata);\nint luaX_warn(lua_State* L, const char* fmt, ...);\n\n#endif\n"
  },
  {
    "path": "include/config.h",
    "content": "/**\n * config.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef CONFIG_H\n#define CONFIG_H\n\n#include \"common.h\"\n#include \"option.h\"\n#include \"meta.h\"\n\n\ntypedef struct {\n  GHashTable* data;\n  bool locked;\n} Config;\n\n\nConfig* config_init();\nvoid config_close(Config* config);\nvoid config_restore_default(Config* config, Meta* meta);\nconst char* config_get_str(Config* config, const char* key);\nvoid config_set_str(Config* config, const char* key, const char* value);\nint config_get_int(Config* config, const char* key);\nvoid config_set_int(Config* config, const char* key, int value);\nbool config_get_bool(Config* config, const char* key);\nvoid config_set_bool(Config* config, const char* key, bool value);\nVteCursorShape config_get_cursor_shape(Config* config);\nVteCursorBlinkMode config_get_cursor_blink_mode(Config* config);\nunsigned config_get_cjk_width(Config* config);\n\n#endif\n"
  },
  {
    "path": "include/context.h",
    "content": "/**\n * context.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef CONTEXT_H\n#define CONTEXT_H\n\n#include \"common.h\"\n#include \"config.h\"\n#include \"hook.h\"\n#include \"keymap.h\"\n#include \"option.h\"\n\n\ntypedef struct {\n  GtkWindow* window;\n  VteTerminal* vte;\n  GtkBox* hbox;\n  GtkBox* vbox;\n  int uri_tag;\n  bool alpha_supported;\n} Layout;\n\ntypedef struct {\n  void* object;\n  int handler_id;\n} HandlerTag;\n\ntypedef struct {\n  int id;\n  bool config_loading;\n  bool initialized;\n  char* object_path;\n  int registration_id;\n  int child_pid;\n  GList* handler_tags;\n  Option* option;\n  Config* config;\n  Keymap* keymap;\n  Hook* hook;\n  GdkDevice* device;\n  lua_State* lua;\n  Layout layout;\n} Context;\n\n\n\n#define context_signal_connect(context, instance, detailed_signal, c_handler) {\\\n  context_add_handler_tag(context, instance, g_signal_connect(instance, detailed_signal, c_handler, context)); \\\n}\n\nContext* context_init(int id, Option* option);\n// void context_dispose_only(Context* context);\nvoid context_close(Context* context);\nvoid context_add_handler_tag(Context* context, void* object, int handler_id);\nvoid context_load_device(Context* context);\nvoid context_load_lua_context(Context* context);\nvoid context_log_message(Context* context, bool notify, const char* fmt, ...);\nvoid context_log_warn(Context* context, bool notify, const char* fmt, ...);\nvoid context_restore_default(Context* context);\nvoid context_override_by_option(Context* context);\nchar* context_acquire_config_path(Context* context);\nchar* context_acquire_theme_path(Context* context);\nvoid context_load_config(Context* context);\nvoid context_load_theme(Context* context);\nbool context_perform_keymap(Context* context, unsigned key, GdkModifierType mod);\nvoid context_handle_signal(Context* context, const char* signal_name, GVariant* parameters);\nvoid context_build_layout(Context* context);\nvoid context_notify(Context* context, const char* body, const char* title);\nvoid context_launch_uri(Context* context, const char* uri);\nGdkWindow* context_get_gdk_window(Context* context);\nconst char* context_get_str(Context* context, const char* key);\nint context_get_int(Context* context, const char* key);\nbool context_get_bool(Context* context, const char* key);\nvoid context_set_str(Context* context, const char* key, const char* value);\nvoid context_set_int(Context* context, const char* key, int value);\nvoid context_set_bool(Context* context, const char* key, bool value);\nvoid context_resize(Context* context, int width, int height);\n\n#endif\n"
  },
  {
    "path": "include/hook.h",
    "content": "/**\n * hook.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef HOOK_H\n#define HOOK_H\n\n#include \"common.h\"\n\n\ntypedef struct {\n  GHashTable* refs;\n} Hook;\n\n\nHook* hook_init();\nvoid hook_close(Hook* hook);\nbool hook_set_ref(Hook* hook, const char* key, int ref, int* old_ref);\nbool hook_perform_title(Hook* hook, lua_State* L, const char* title, bool* result);\nbool hook_perform_bell(Hook* hook, lua_State* L, bool* result);\nbool hook_perform_clicked(Hook* hook, lua_State* L, int button, const char* uri, bool* result);\nbool hook_perform_scroll(Hook* hook, lua_State* L, double delta_x, double delta_y, double x, double y, bool* result);\nbool hook_perform_drag(Hook* hook, lua_State* L, char* path, bool* result);\nbool hook_perform_activated(Hook* hook, lua_State* L);\nbool hook_perform_deactivated(Hook* hook, lua_State* L);\nbool hook_perform_selected(Hook* hook, lua_State* L, const char* text);\nbool hook_perform_unselected(Hook* hook, lua_State* L);\nbool hook_perform_resized(Hook* hook, lua_State* L);\nbool hook_perform_signal(Hook* hook, lua_State* L, const char* param);\n\n#endif\n"
  },
  {
    "path": "include/ipc.h",
    "content": "/**\n * ipc.h\n *\n * Copyright (c) 2022 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef IPC_H\n#define IPC_H\n\n#include \"context.h\"\n\ntypedef struct {\n  GHashTable* signals;\n  GHashTable* methods;\n} IPC;\n\n\nIPC* ipc_init();\nvoid ipc_close(IPC* ipc);\nbool ipc_signal_perform(IPC* ipc, Context* context, const char* signal_name, GVariant* parameters);\nbool ipc_method_perform(IPC* ipc, Context* context, const char* method_name, GVariant* parameters, GDBusMethodInvocation* invocation);\n\n\n#endif\n"
  },
  {
    "path": "include/keymap.h",
    "content": "/**\n * keymap.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef KEYMAP_H\n#define KEYMAP_H\n\n#include \"common.h\"\n\n\ntypedef struct {\n  GList* entries;\n} Keymap;\n\n\nKeymap* keymap_init();\nvoid keymap_close(Keymap* keymap);\nvoid keymap_reset(Keymap* keymap);\nbool keymap_add_entry(Keymap* keymap, const char* accelerator, int ref);\nbool keymap_remove_entry(Keymap* keymap, const char* accelerator);\nbool keymap_perform(Keymap* keymap, lua_State* L, unsigned key, GdkModifierType mod, bool* result, char** error);\n\n#endif\n"
  },
  {
    "path": "include/meta.h",
    "content": "/**\n * meta.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef META_H\n#define META_H\n\n#include \"common.h\"\n\ntypedef void  (*MetaCallback) (void);\n\ntypedef enum {\n  META_ENTRY_TYPE_STRING = 0,\n  META_ENTRY_TYPE_INTEGER = 1,\n  META_ENTRY_TYPE_BOOLEAN = 2,\n  META_ENTRY_TYPE_NONE = 3, // not actual, only shown in help\n} MetaEntryType;\n\ntypedef struct {\n  char* name;\n  char short_name;\n  MetaEntryType type;\n  GOptionFlags option_flag;\n  void* default_value;\n  char* arg_desc;\n  char* desc;\n  MetaCallback getter;\n  MetaCallback setter;\n  bool is_theme;\n  unsigned index;\n} MetaEntry;\n\ntypedef struct {\n  GHashTable* data;\n  GList* list;\n} Meta;\n\nMeta* meta_init();\nvoid meta_close(Meta* meta);\nunsigned meta_size(Meta* meta);\nMetaEntry* meta_get_entry(Meta* meta, const char* key);\nGOptionEntry* meta_get_option_entries(Meta* meta);\n\n#endif\n"
  },
  {
    "path": "include/option.h",
    "content": "/**\n * option.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef OPTION_H\n#define OPTION_H\n\n#include \"common.h\"\n#include \"meta.h\"\n\ntypedef struct {\n  GOptionContext* option_context;\n  GOptionEntry* entries;\n  char** rest_argv;\n  GHashTable* entries_as_table;\n} Option;\n\nvoid* option_get(Option* option, const char* key);\n\nOption* option_init(GOptionEntry* entries);\nvoid option_close(Option* option);\nbool option_parse(Option* option, int argc, char** argv);\nchar* option_get_str(Option* option, const char* key);\nint option_get_int(Option* option, const char* key);\nbool option_get_bool(Option* option, const char* key);\n\n#endif\n"
  },
  {
    "path": "include/property.h",
    "content": "/**\n * property.h\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef PROPERTY_H\n#define PROPERTY_H\n\n#include \"common.h\"\n#include \"context.h\"\n\n\ntypedef int (*PropertyIntGetter)(Context* context, const char* key);\ntypedef const char* (*PropertyStrGetter)(Context* context, const char* key);\ntypedef bool (*PropertyBoolGetter)(Context* context, const char* key);\n\ntypedef void (*PropertyIntSetter)(Context* context, const char* key, int value);\ntypedef void (*PropertyStrSetter)(Context* context, const char* key, const char* value);\ntypedef void (*PropertyBoolSetter)(Context* context, const char* key, bool value);\n\ntypedef void (*PropertyIntSetterWithExtra)(Context* context, const char* key, int value, void* extra);\ntypedef void (*PropertyStrSetterWithExtra)(Context* context, const char* key, const char* value, void* extra);\ntypedef void (*PropertyBoolSetterWithExtra)(Context* context, const char* key, bool value, void* extra);\n\n\n// str\nvoid setter_shell(Context* context, const char* key, const char* value);\n\nvoid setter_term(Context* context, const char* key, const char* value);\n\nconst char* getter_title(Context* context, const char* key);\nvoid setter_title(Context* context, const char* key, const char* value);\n\nconst char* getter_font(Context* context, const char* key);\nvoid setter_font(Context* context, const char* key, const char* value);\n\nconst char* getter_icon(Context* context, const char* key);\nvoid setter_icon(Context* context, const char* key, const char* value);\n\nconst char* getter_role(Context* context, const char* key);\nvoid setter_role(Context* context, const char* key, const char* value);\n\nconst char* getter_cursor_shape(Context* context, const char* key);\nvoid setter_cursor_shape(Context* context, const char* key, const char* value);\n\nconst char* getter_cursor_blink_mode(Context* context, const char* key);\nvoid setter_cursor_blink_mode(Context* context, const char* key, const char* value);\n\nconst char* getter_cjk_width(Context* context, const char* key);\nvoid setter_cjk_width(Context* context, const char* key, const char* value);\n\nvoid setter_background_image(Context* context, const char* key, const char* value);\n\nvoid setter_uri_schemes(Context* context, const char* key, const char* value);\n\n// int\nint getter_width(Context* context, const char* key);\nvoid setter_width(Context* context, const char* key, int value);\n\nint getter_height(Context* context, const char* key);\nvoid setter_height(Context* context, const char* key, int value);\n\nint getter_scale(Context* context, const char* key);\nvoid setter_scale(Context* context, const char* key, int value);\n\nint getter_cell_width(Context* context, const char* key);\nvoid setter_cell_width(Context* context, const char* key, int value);\n\nint getter_cell_height(Context* context, const char* key);\nvoid setter_cell_height(Context* context, const char* key, int value);\n\n/* DEPRECATED START */\nvoid setter_padding_horizontal(Context* context, const char* key, int value);\nvoid setter_padding_vertical(Context* context, const char* key, int value);\n/* DEPRECATED END */\n\nint getter_padding_top(Context* context, const char* key);\nint getter_padding_bottom(Context* context, const char* key);\nint getter_padding_left(Context* context, const char* key);\nint getter_padding_right(Context* context, const char* key);\nvoid setter_padding_top(Context* context, const char* key, int value);\nvoid setter_padding_bottom(Context* context, const char* key, int value);\nvoid setter_padding_left(Context* context, const char* key, int value);\nvoid setter_padding_right(Context* context, const char* key, int value);\n\nint getter_scrollback_length(Context* context, const char* key);\nvoid setter_scrollback_length(Context* context, const char* key, int value);\n\n// bool\nbool getter_scroll_on_output(Context* context, const char* key);\nvoid setter_scroll_on_output(Context* context, const char* key, bool value);\n\nbool getter_silent(Context* context, const char* key);\nvoid setter_silent(Context* context, const char* key, bool value);\n\nbool getter_autohide(Context* context, const char* key);\nvoid setter_autohide(Context* context, const char* key, bool value);\n\nbool gettter_bold_is_bright(Context* context, const char* key);\nvoid setter_bold_is_bright(Context* context, const char* key, bool value);\n\n// color\nvoid setter_color_normal(Context* context, const char* key, const char* value);\nvoid setter_color_window_background(Context* context, const char* key, const char* value);\nvoid setter_color_background(Context* context, const char* key, const char* value);\nvoid setter_color_foreground(Context* context, const char* key, const char* value);\nvoid setter_color_bold(Context* context, const char* key, const char* value);\nvoid setter_color_cursor(Context* context, const char* key, const char* value);\nvoid setter_color_cursor_foreground(Context* context, const char* key, const char* value);\nvoid setter_color_highlight(Context* context, const char* key, const char* value);\nvoid setter_color_highlight_foreground(Context* context, const char* key, const char* value);\n\n\n#endif\n"
  },
  {
    "path": "include/regex.h",
    "content": "/**\n * regex.h\n *\n * URI regular expression in PCRE2\n * reference: RFC 3986 Appendix A (https://tools.ietf.org/html/rfc3986#appendix-A)\n *\n * NOTE:\n *   * URI is repleced by SCHEMELESS_URI, because the entire URI regex is dynamically constructed\n *     using an user-configured list of target schemes. SCHEME is used to validate the list.\n *   * SUB_DELIMS does not contain \"(\" and \")\", and subsequently USERINFO, IPVFUTURE, and REG_NAME do not allow these\n *     characters. SEGMENT and QUERY rules have special PAREN rules that matches only paired \"(\" and \")\".\n *   * Also, the following definitions are omitted:\n *     - URI-reference      only used in relative URIs.\n *     - relative-ref       (as above)\n *     - relative-part      (as above)\n *     - path-noscheme      (as above)\n *     - segment-nz-nc      (as above)\n *     - absolute-URI       special case of URI. not distinguishable in regex.\n *     - path               not referenced from any other rules.\n *     - path-empty         to avoid highlighting meaningless URI like `foo:`.\n *     - gen-delims         not referenced from any other rules.\n *     - reserved           not referenced from any other rules.\n *\n * Copyright (c) 2020 endaaman, iTakeshi\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n\n#ifndef REGEX_H\n#define REGEX_H\n\n\n/*\n * inherited from RFC 2234 Section 6.1 (https://tools.ietf.org/html/rfc2234#section-6.1)\n * NOTE: the difinitions for ALPHA and HEXDIG assume those are used in a case-insensitive manner.\n */\n\n#define ALPHA           \"(?:\" \"[a-z]\" \")\"\n\n#define DIGIT           \"(?:\" \"[0-9]\" \")\"\n\n#define HEXDIG          \"(?:\" DIGIT \"|\" \"[a-f]\" \")\"\n\n#define SP              \"(?:\" \" \" \")\"\n\n\n/*\n * rules to validate the user-configured scheme list\n */\n\n#define SCHEME          \"(\" ALPHA \"(?:\" ALPHA \"|\" DIGIT \"|\" \"[\\\\+\\\\-\\\\.]\" \")*\" \")\"\n\n#define SCHEME_LIST     SCHEME \"(?:\" SP SCHEME \")*\"\n\n\n/*\n * main rules\n */\n\n#define SCHEMELESS_URI  \"(?:\" \":\" HIER_PART \"(?:\" \"\\\\?\" QUERY \")?\" \"(?:\" \"\\\\#\" QUERY \")?\" \")\"\n\n#define HIER_PART       \"(?:\" \"\\\\/\\\\/\" AUTHORITY PATH_ABEMPTY \"|\" PATH_ABSOLUTE \"|\" PATH_ROOTLESS \")\"\n\n#define AUTHORITY       \"(?:\" USERINFO \"@\" \")?\" HOST \"(?:\" \":\" PORT \")?\"\n\n#define USERINFO        \"(?:\" UNRESERVED \"|\" PCT_ENCODED \"|\" SUB_DELIMS \"|\" \":\" \")*+\"\n\n#define HOST            \"(?:\" IP_LITERAL \"|\" IPV4ADDRESS \"|\" REG_NAME\")\"\n\n#define PORT            \"(?:\" DIGIT \")*+\"\n\n#define IP_LITERAL      \"(?:\" \"\\\\[\" \"(?:\" IPV6ADDRESS \"|\" IPVFUTURE \")\" \"\\\\]\" \")\"\n\n#define IPVFUTURE       \"(?:\" \"v\" HEXDIG \"++\" \"\\\\.\" \"(?:\" UNRESERVED \"|\" SUB_DELIMS \"|\" \":\" \")++\" \")\"\n\n#define IPV6ADDRESS     \"(?:\"                                            \"(?:\" H16 \":\" \"){6}\" LS32 \\\n                          \"|\"                                       \"::\" \"(?:\" H16 \":\" \"){5}\" LS32 \\\n                          \"|\" \"(?:\"                        H16 \")?\" \"::\" \"(?:\" H16 \":\" \"){4}\" LS32 \\\n                          \"|\" \"(?:\" \"(?:\" H16 \":\" \"){0,1}\" H16 \")?\" \"::\" \"(?:\" H16 \":\" \"){3}\" LS32 \\\n                          \"|\" \"(?:\" \"(?:\" H16 \":\" \"){0,2}\" H16 \")?\" \"::\" \"(?:\" H16 \":\" \"){2}\" LS32 \\\n                          \"|\" \"(?:\" \"(?:\" H16 \":\" \"){0,3}\" H16 \")?\" \"::\" \"(?:\" H16 \":\" \"){1}\" LS32 \\\n                          \"|\" \"(?:\" \"(?:\" H16 \":\" \"){0,4}\" H16 \")?\" \"::\"                      LS32 \\\n                          \"|\" \"(?:\" \"(?:\" H16 \":\" \"){0,5}\" H16 \")?\" \"::\"                      H16  \\\n                          \"|\" \"(?:\" \"(?:\" H16 \":\" \"){0,6}\" H16 \")?\" \"::\"                           \\\n                          \")\"\n\n#define H16             \"(?:\" HEXDIG \"){1,4}\"\n\n#define LS32            \"(?:\" H16 \":\" H16 \"|\" IPV4ADDRESS \")\"\n\n#define IPV4ADDRESS     \"(?:\" DEC_OCTET \"\\\\.\" DEC_OCTET \"\\\\.\" DEC_OCTET \"\\\\.\" DEC_OCTET \")\"\n\n#define DEC_OCTET       \"(?:\" DIGIT \"|\" \"[1-9]\" DIGIT \"|\" \"1\" DIGIT DIGIT \"|\" \"2\" \"[0-4]\" DIGIT \"|\" \"25\" \"[0-5]\" \")\"\n\n#define REG_NAME        \"(?:\" UNRESERVED \"|\" PCT_ENCODED \"|\" SUB_DELIMS \")*+\"\n\n#define PATH_ABEMPTY    \"(?:\" \"\\\\/\" SEGMENT \")*+\"\n\n#define PATH_ABSOLUTE   \"(?:\" \"\\\\/\" PATH_ROOTLESS \"?\" \")\"\n\n#define PATH_ROOTLESS   \"(?:\" SEGMENT_NZ PATH_ABEMPTY \")\"\n\n#define SEGMENT         \"(?:\" PCHAR \"|\" PAREN \")*+\"\n\n#define SEGMENT_NZ      \"(?:\" PCHAR \"|\" PAREN \")++\"\n\n#define PCHAR           \"(?:\" UNRESERVED \"|\" PCT_ENCODED \"|\" SUB_DELIMS \"|\" \"[:@]\" \")\"\n\n#define PAREN           \"(?:\" \"\\\\(\" PCHAR \"*+\" \"\\\\)\" \")\"\n\n#define QUERY           \"(?:\" PCHAR \"|\" \"[\\\\/\\\\?]\" \"|\" QUERY_PAREN \")*+\"\n\n#define QUERY_PAREN     \"(?:\" \"\\\\(\" \"(?:\" PCHAR \"|\" \"[\\\\/\\\\?]\" \")*+\" \"\\\\)\" \")\"\n\n#define PCT_ENCODED     \"(?:\" \"%\" HEXDIG HEXDIG \")\"\n\n#define UNRESERVED      \"(?:\" ALPHA \"|\" DIGIT \"|\" \"[\\\\-\\\\._~]\" \")\"\n\n#define SUB_DELIMS      \"(?:\" \"[!\\\\$&'\\\\*\\\\+,;=]\" \")\"\n\n\n#endif\n"
  },
  {
    "path": "include/tym.h",
    "content": "/**\n * tym.h\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef TYM_H\n#define TYM_H\n\n#include \"app.h\"\n\n#endif\n"
  },
  {
    "path": "include/tym_test.h",
    "content": "/**\n * tym.h\n *\n * Copyright (c) 2020 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#ifndef TYM_TEST_H\n#define TYM_TEST_H\n\n#include \"common.h\"\n\nvoid test_config();\nvoid test_option();\nvoid test_regex();\n\n#endif\n"
  },
  {
    "path": "lua/e2e.lua",
    "content": "local tym = require('tym')\n\nprint('start')\n\ntym.set_timeout(function()\n  print('done')\n  tym.quit()\nend, 1000)\n"
  },
  {
    "path": "scripts/bundle.sh",
    "content": "#!/bin/bash\n\nset -eux\n\np=$(dirname \"$0\")\n\nbash $p/cleanup.sh\nautoreconf -fvi\n./configure\nmake clean\nmake\nmake check\nmake dist\n"
  },
  {
    "path": "scripts/cleanup.sh",
    "content": "#!/bin/bash\n\nset -eux\n\nrm -f Makefile\nrm -f Makefile.in\nrm -f aclocal.m4\nrm -f app-config.h\nrm -f app-config.h.in\nrm -f compile\nrm -f config.guess\nrm -f config.h\nrm -f config.h.in\nrm -f config.h.in~\nrm -f config.log\nrm -f config.status\nrm -f config.sub\nrm -f configure\nrm -f depcomp\nrm -f install-sh\nrm -f libtool\nrm -f missing\nrm -f stamp-h1\nrm -rf autom4te.cache/\nrm -rf aux-dist/\nrm -rf m4/\n\nrm -f src/*.la\nrm -f src/*.lo\nrm -f src/*.log\nrm -f src/*.o\nrm -f src/*.trs\nrm -f src/Makefile\nrm -f src/Makefile.in\nrm -f src/tym\nrm -f src/tym-test\nrm -rf src/.deps/\n\nrm -f include/Makefile\nrm -f include/Makefile.in\nrm -f include/common.h\n\nrm -f tym.1\nrm -f tym-daemon.service\nrm -f test-driver\n\necho 'Cleaned files.'\n"
  },
  {
    "path": "scripts/refresh.sh",
    "content": "#!/bin/bash\n\nset -eux\n\np=$(dirname \"$0\")\n\nbash $p/cleanup.sh\nautoreconf -fvi\n./configure --enable-debug\n"
  },
  {
    "path": "src/Makefile.am",
    "content": "if DEBUG\nENV_OPT=-g3 -O0\nelse\nENV_OPT=-O3\nendif\n\nCOMMON_CFLAGS = \\\n\t$(ENV_OPT) \\\n\t-std=c11 \\\n\t-Wall \\\n\t-Wextra \\\n\t-Wno-unused-parameter \\\n\t-Wno-sign-compare \\\n\t-Wno-pointer-sign \\\n\t-Wno-missing-field-initializers \\\n\t-Wformat=2 \\\n\t-Wstrict-aliasing=2 \\\n\t-Wdisabled-optimization \\\n\t-Wfloat-equal \\\n\t-Wpointer-arith \\\n\t-Wbad-function-cast \\\n\t-Wcast-align \\\n\t-Wredundant-decls \\\n\t-Wformat-security \\\n\t-Winline \\\n\t-I$(top_srcdir)/include\n\nbin_PROGRAMS = tym\ntym_SOURCES = \\\n\tapp.c \\\n\tbuiltin.c \\\n\tcommand.c \\\n\tcommon.c \\\n\tconfig.c \\\n\tcontext.c \\\n\thook.c \\\n\tipc.c \\\n\tkeymap.c \\\n\tmeta.c \\\n\toption.c \\\n\tproperty.c \\\n\ttym.c\ntym_LDADD = $(TYM_LIBS) $(LUA_LIBS)\ntym_CFLAGS = $(COMMON_CFLAGS) $(TYM_CFLAGS) $(LUA_CFLAGS)\n\nTESTS = tym-test\ncheck_PROGRAMS = tym-test\ntym_test_SOURCES = \\\n\tapp.c \\\n\tbuiltin.c \\\n\tcommand.c \\\n\tcommon.c \\\n\tconfig.c \\\n\tcontext.c \\\n\thook.c \\\n\tipc.c \\\n\tkeymap.c \\\n\tmeta.c \\\n\toption.c \\\n\tproperty.c \\\n\tconfig_test.c \\\n\toption_test.c \\\n\tregex_test.c \\\n\ttym_test.c\ntym_test_LDADD = $(TYM_LIBS) $(LUA_LIBS)\ntym_test_CFLAGS = $(COMMON_CFLAGS) $(TYM_CFLAGS) $(LUA_CFLAGS)\n"
  },
  {
    "path": "src/app.c",
    "content": "/**\n * app.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"app.h\"\n\nApp* app = NULL;\n\n\nint on_local_options(GApplication* gapp, GVariantDict* values, void* user_data);\nint on_command_line(GApplication* app, GApplicationCommandLine* cli, void* user_data);\n\nvoid app_init()\n{\n  df();\n  app = g_new0(App, 1);\n  app->meta = meta_init();\n  app->ipc = ipc_init();\n}\n\nvoid app_close()\n{\n  df();\n  for (GList* li = app->contexts; li != NULL; li = li->next) {\n    Context* c = (Context*)li->data;\n    context_close(c);\n  }\n  g_application_quit(app->gapp);\n  g_object_unref(app->gapp);\n  meta_close(app->meta);\n  ipc_close(app->ipc);\n  g_free(app);\n}\n\nstatic char* _get_dest_path_from_option(Option* option) {\n  char* path = NULL;\n  char* dest = option_get_str(option, \"dest\");\n  if (dest) {\n    path = g_strdup_printf(TYM_OBJECT_PATH_FMT_STR, dest);\n  } else {\n    char** env = g_get_environ();\n    const char* dest_str = g_environ_getenv(env, \"TYM_ID\");\n    if (!dest_str) {\n      return NULL;\n    }\n    path = g_strdup_printf(TYM_OBJECT_PATH_FMT_STR, dest_str);\n  }\n  return path;\n}\n\nstatic int _perform_signal(char* dest_path, char* signal_name, char* method_name, char* param)\n{\n  GError* error = NULL;\n\n  GDBusConnection* conn = g_application_get_dbus_connection(app->gapp);\n  if (!dest_path) {\n    g_warning(\"--dest is not provided and $TYM_ID is not set.\");\n    return 1;\n  }\n\n  GVariant* params = param\n    ? g_variant_new(\"(s)\", param)\n    : g_variant_new(\"()\");\n\n  /* process signal */\n  if (signal_name) {\n    g_dbus_connection_emit_signal(conn, NULL, dest_path, TYM_APP_ID, signal_name, params, &error);\n    g_print(\"Sent signal:%s to path:%s interface:%s\\n\", signal_name, dest_path, TYM_APP_ID);\n    g_free(signal_name);\n    if (error) {\n      g_error(\"%s\", error->message);\n      g_error_free(error);\n    }\n    return 0;\n  }\n\n  /* process method call */\n  GVariant* result = g_dbus_connection_call_sync(\n      conn,        // conn\n      TYM_APP_ID,  // bus_name\n      dest_path,   // object_path\n      TYM_APP_ID,  // interface_name\n      method_name, // method_name\n      params,      // parameters\n      NULL,        // reply_type\n      G_DBUS_CALL_FLAGS_NONE, // flags\n      1000,        // timeout\n      NULL,        // cancellable\n      &error\n  );\n  g_print(\"Call method:%s on path:%s interface:%s\\n\", method_name, dest_path, TYM_APP_ID);\n  if (error) {\n    g_warning(\"%s\", error->message);\n    g_error_free(error);\n    return 1;\n  }\n  dd(\"result type:%s\", g_variant_get_type_string(result));\n  char* msg = g_variant_print(result, true);\n  g_print(\"%s\\n\", msg);\n  g_free(msg);\n  return 0;\n}\n\nint app_start(Option* option, int argc, char **argv)\n{\n  df();\n  g_assert(!app->gapp);\n\n  GApplicationFlags flags = G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_SEND_ENVIRONMENT;\n  char* app_id = TYM_APP_ID;\n  if (option_get_bool(option, \"isolated\")) {\n    flags |= G_APPLICATION_NON_UNIQUE;\n    app_id = TYM_APP_ID_ISOLATED;\n  }\n\n  app->gapp = G_APPLICATION(gtk_application_new(app_id, flags));\n  GError* error = NULL;\n  g_application_register(app->gapp, NULL, &error);\n\n  g_signal_connect(app->gapp, \"handle-local-options\", G_CALLBACK(on_local_options), option);\n  g_signal_connect(app->gapp, \"command-line\", G_CALLBACK(on_command_line), NULL);\n  return g_application_run(app->gapp, argc, argv);\n}\n\nstatic int _contexts_sort_func(const void* a, const void* b)\n{\n  return ((Context*)a)->id - ((Context*)b)->id;\n}\n\nContext* app_spawn_context(Option* option)\n{\n  df();\n  unsigned index = 0;\n  int ordered_id = option_get_int(option, \"id\");\n  if (ordered_id) {\n    for (GList* li = app->contexts; li != NULL; li = li->next) {\n      Context* c = (Context*)li->data;\n      if (c->id == ordered_id) {\n        context_log_warn(c, true, \"id=%d has been already acquired.\", ordered_id);\n        return NULL;\n      }\n    }\n    index = ordered_id;\n  } else {\n    for (GList* li = app->contexts; li != NULL; li = li->next) {\n      Context* c = (Context*)li->data;\n      /* scanning from 0 and if find first ctx that is not continus from 0, the index is new index. */\n      if (c->id != index) {\n        break;\n      }\n      index += 1;\n    }\n  }\n\n  Context* context = context_init(index, option);\n  app->contexts = g_list_insert_sorted(app->contexts, context, _contexts_sort_func);\n  g_application_hold(app->gapp);\n\n  context_log_message(context, false, \"Started.\");\n  return context;\n}\n\nvoid app_quit_context(Context* context)\n{\n  df();\n  g_application_release(app->gapp);\n  GDBusConnection* conn = g_application_get_dbus_connection(app->gapp);\n  g_dbus_connection_unregister_object(conn, context->registration_id);\n  context_log_message(context, false, \"Quit.\");\n  app->contexts = g_list_remove(app->contexts, context);\n  context_close(context);\n}\n\nstatic void on_vte_drag_data_received(\n  VteTerminal* vte,\n  GdkDragContext* drag_context,\n  int x,\n  int y,\n  GtkSelectionData* data,\n  unsigned int info,\n  unsigned int time,\n  void* user_data)\n{\n  Context* context = (Context*)user_data;\n  if (!data || gtk_selection_data_get_format(data) != 8) {\n    return;\n  }\n\n  gchar** uris = g_uri_list_extract_uris(gtk_selection_data_get_data(data));\n  if (!uris) {\n    return;\n  }\n\n  GRegex* regex = g_regex_new(\"'\", 0, 0, NULL);\n  for (gchar** p = uris; *p; ++p) {\n    gchar* file_path = g_filename_from_uri(*p, NULL, NULL);\n    if (file_path) {\n      bool result;\n      if (!(hook_perform_drag(context->hook, context->lua, file_path, &result) && result)) {\n        gchar* path_escaped = g_regex_replace(regex, file_path, -1, 0, \"'\\\\\\\\''\", 0, NULL);\n        gchar* path_wrapped = g_strdup_printf(\"'%s' \", path_escaped);\n        vte_terminal_feed_child(vte, path_wrapped, strlen(path_wrapped));\n        g_free(path_escaped);\n        g_free(path_wrapped);\n      }\n      g_free(file_path);\n    }\n  }\n  g_regex_unref(regex);\n}\n\nstatic bool on_vte_key_press(GtkWidget* widget, GdkEventKey* event, void* user_data)\n{\n  Context* context = (Context*)user_data;\n\n  unsigned mod = event->state & gtk_accelerator_get_default_mod_mask();\n  unsigned key = gdk_keyval_to_lower(event->keyval);\n\n  if (context_perform_keymap(context, key, mod)) {\n    return true;\n  }\n  return false;\n}\n\nstatic bool on_vte_mouse_scroll(GtkWidget* widget, GdkEventScroll* e, void* user_data)\n{\n  Context* context = (Context*)user_data;\n  bool result = false;\n  if (hook_perform_scroll(context->hook, context->lua, e->delta_x, e->delta_y, e->x, e->y, &result) && result) {\n    return true;\n  }\n  return false;\n}\n\nstatic void on_vte_child_exited(VteTerminal* vte, int status, void* user_data)\n{\n  df();\n  Context* context = (Context*)user_data;\n  gtk_window_close(context->layout.window);\n  app_quit_context(context);\n}\n\nstatic void on_vte_title_changed(VteTerminal* vte, void* user_data)\n{\n  df();\n  Context* context = (Context*)user_data;\n  GtkWindow* window = context->layout.window;\n  bool result = false;\n\n#ifdef TYM_USE_VTE_TERMPROP\n  const char* title = vte_terminal_get_termprop_string(context->layout.vte, \"xterm.title\", NULL);\n#else\n  const char* title = vte_terminal_get_window_title(context->layout.vte);\n#endif\n\n  if (hook_perform_title(context->hook, context->lua, title, &result) && result) {\n    return;\n  }\n  if (title) {\n    gtk_window_set_title(window, title);\n  }\n}\n\nstatic void on_vte_bell(VteTerminal* vte, void* user_data)\n{\n  df();\n  Context* context = (Context*)user_data;\n  bool result = false;\n  if (hook_perform_bell(context->hook, context->lua, &result) && result) {\n    return;\n  }\n  GtkWindow* window = context->layout.window;\n  if (!gtk_window_is_active(window)) {\n    gtk_window_set_urgency_hint(window, true);\n  }\n}\n\nstatic bool on_vte_click(VteTerminal* vte, GdkEventButton* event, void* user_data)\n{\n  df();\n  Context* context = (Context*)user_data;\n  char* uri = NULL;\n  if (context->layout.uri_tag >= 0) {\n    uri = vte_terminal_match_check_event(vte, (GdkEvent*)event, NULL);\n  }\n  bool result = false;\n  if (hook_perform_clicked(context->hook, context->lua, event->button, uri, &result)) {\n    if (result) {\n      return true;\n    }\n    return false;\n  }\n  if (uri) {\n    for (int i = strlen(uri) - 1; uri[i] == '.' || uri[i] == ','; i--) {\n      uri[i] = '\\0';\n    }\n    context_launch_uri(context, uri);\n    return true;\n  }\n  return false;\n}\n\nstatic void on_vte_selection_changed(GtkWidget* widget, void* user_data)\n{\n  df();\n  Context* context = (Context*)user_data;\n  if (!vte_terminal_get_has_selection(context->layout.vte)) {\n    hook_perform_unselected(context->hook, context->lua);\n    return;\n  }\n  GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_PRIMARY);\n  char* text = gtk_clipboard_wait_for_text(cb);\n  hook_perform_selected(context->hook, context->lua, text);\n}\n\nstatic void on_vte_resize_request(GtkWidget* widget, unsigned int width, unsigned int height, void* user_data)\n{\n  Context* context = (Context*)user_data;\n  dd(\"Recieve resize sequence: width=%d height=%d\", width, height);\n  context_resize(context, width, height);\n}\n\n\nstatic gboolean on_window_close(GtkWidget* widget, cairo_t* cr, void* user_data)\n{\n  df();\n  // close context in child-exited handler\n  return true;\n}\n\nstatic bool on_window_focus_in(GtkWindow* window, GdkEvent* event, void* user_data)\n{\n  Context* context = (Context*)user_data;\n  gtk_window_set_urgency_hint(window, false);\n  hook_perform_activated(context->hook, context->lua);\n  return false;\n}\n\nstatic bool on_window_focus_out(GtkWindow* window, GdkEvent* event, void* user_data)\n{\n  Context* context = (Context*)user_data;\n  hook_perform_deactivated(context->hook, context->lua);\n  return false;\n}\n\nstatic gboolean on_window_draw(GtkWidget* widget, cairo_t* cr, void* user_data)\n{\n  Context* context = (Context*)user_data;\n  const char* value = context_get_str(context, \"color_window_background\");\n  if (is_none(value)) {\n    return false;\n  }\n  GdkRGBA color = {};\n  if (gdk_rgba_parse(&color, value)) {\n    if (context->layout.alpha_supported) {\n      cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha);\n    } else {\n      cairo_set_source_rgb(cr, color.red, color.green, color.blue);\n    }\n    cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);\n    cairo_paint(cr);\n  }\n  return false;\n}\n\nstatic void on_window_resize(GtkWidget* widget, GtkAllocation* allocation, gpointer user_data)\n{\n  Context* context = (Context*)user_data;\n  hook_perform_resized(context->hook, context->lua);\n}\n\nvoid on_dbus_signal(\n  GDBusConnection* conn,\n  const char* sender_name,\n  const char* object_path,\n  const char* interface_name,\n  const char* signal_name,\n  GVariant* params,\n  void* user_data)\n{\n  Context* context = (Context*)user_data;\n  dd(\"DBus signal received\");\n  dd(\"\\tcontext id: %d\", context->id);\n  dd(\"\\tsender_name: %s\", sender_name);\n  dd(\"\\tobject_path: %s\", object_path);\n  dd(\"\\tinterface_name: %s\", interface_name);\n  dd(\"\\tsignal_name: %s\", signal_name);\n\n  if (ipc_signal_perform(app->ipc, context, signal_name, params)) {\n    context_log_message(context, false, \"Signal received:`%s` object_path:`%s`\", signal_name, object_path);\n    return;\n  }\n\n  context_log_warn(context, true, \"Unsupported signal: `%s`\", signal_name);\n}\n\nvoid on_dbus_call_method(\n    GDBusConnection* conn,\n    const gchar* sender_name,\n    const gchar* object_path,\n    const gchar* interface_name,\n    const gchar* method_name,\n    GVariant* params,\n    GDBusMethodInvocation* invocation,\n    gpointer user_data)\n{\n  Context* context = (Context*)user_data;\n  dd(\"DBus method call\");\n  dd(\"\\tcontext id: %d\", context->id);\n  dd(\"\\tsender_name: %s\", sender_name);\n  dd(\"\\tobject_path: %s\", object_path);\n  dd(\"\\tinterface_name: %s\", interface_name);\n  dd(\"\\tmethod_name: %s\", method_name);\n\n  if (ipc_method_perform(app->ipc, context, method_name, params, invocation)) {\n    context_log_message(context, false, \"Method call:`%s` object_path:`%s`\", method_name, object_path);\n    g_dbus_connection_flush(conn, NULL, NULL, NULL);\n    return;\n  }\n\n  context_log_warn(context, true, \"Unsupported method call:`%s`\", method_name);\n  GError* error = g_error_new(\n      g_quark_from_static_string(\"TymInvalidMethodCall\"),\n      TYM_ERROR_INVALID_METHOD_CALL,\n      \"Unsupported method call: %s\",\n      method_name);\n\n  g_dbus_method_invocation_return_gerror(invocation, error);\n  g_dbus_connection_flush(conn, NULL, NULL, NULL);\n}\n\nint on_local_options(GApplication* gapp, GVariantDict* values, void* user_data)\n{\n  df();\n  Option* option = (Option*)(user_data);\n\n  char* dest_path = _get_dest_path_from_option(option);\n  char* signal_name = option_get_str(option, \"signal\");\n  char* method_name = option_get_str(option, \"call\");\n  char* param = option_get_str(option, \"param\");\n  if (signal_name || method_name) {\n    int code = _perform_signal(dest_path, signal_name, method_name, param);\n    g_free(dest_path);\n    return code;\n  }\n\n  if (option_get_bool(option, \"daemon\")) {\n    if (g_application_get_is_remote(app->gapp)) {\n      /* If there is a normal primary instance, --daemon flag would make it \"zombie\" */\n      /* So daemonization should be allowed only when the instanciation is the primary */\n      g_warning(\"There is any tym instance. So could not start as daemon process.\");\n      return 1;\n    }\n  }\n\n  const char* cwd = option_get_str(option, \"cwd\");\n  if (cwd != NULL && !g_path_is_absolute(cwd)) {\n    g_warning(\"cwd must be an absolute path\");\n    return 1;\n  }\n\n  return -1;\n}\n\nstatic bool _subscribe_dbus(Context* context)\n{\n  df();\n  GError* error = NULL;\n\n  const char* app_id = g_application_get_application_id(app->gapp);\n  GDBusConnection* conn = g_application_get_dbus_connection(app->gapp);\n\n  g_dbus_connection_signal_subscribe(\n    conn,\n    NULL,        // sender\n    app_id,      // interface_name\n    NULL,        // member\n    context->object_path, // object_path\n    NULL,        // arg0\n    G_DBUS_SIGNAL_FLAGS_NONE,\n    on_dbus_signal,\n    context,\n    NULL         // user data free func\n  );\n\n  GDBusInterfaceVTable vtable = {\n    on_dbus_call_method,\n    NULL,\n    NULL,\n  };\n\n  static const char introspection_xml[] =\n    \"<node>\"\n    \"  <interface name='\" TYM_APP_ID \"'>\"\n    \"    <method name='echo'>\"\n    \"      <arg type='s' direction='in'/>\"\n    \"      <arg type='s' direction='out'/>\"\n    \"    </method>\"\n    \"    <method name='get_ids'>\"\n    \"      <arg type='ai' direction='out'/>\"\n    \"    </method>\"\n    \"    <method name='eval'>\"\n    \"      <arg type='s' direction='in'/>\"\n    \"      <arg type='s' direction='out'/>\"\n    \"    </method>\"\n    \"    <method name='exec'>\"\n    \"      <arg type='s' direction='in'/>\"\n    \"    </method>\"\n    \"    <method name='exec_file'>\"\n    \"      <arg type='s' direction='in'/>\"\n    \"    </method>\"\n    \"  </interface>\"\n    \"</node>\";\n\n  GDBusNodeInfo* introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error);\n  if (error) {\n    g_error(\"%s\", error->message);\n    g_error_free(error);\n    app_quit_context(context);\n    return false;\n  }\n\n  context_log_message(context, false, \"DBus: object_path='%s' interface_name:'%s'\", context->object_path, app_id);\n  context->registration_id = g_dbus_connection_register_object(\n      conn,\n      context->object_path,\n      introspection_data->interfaces[0], // interface_info,\n      &vtable, // vtable\n      context, // user_data,\n      NULL,    // user_data_free_func,\n      &error   // error\n  );\n  if (context->registration_id <= 0) {\n    context_log_warn(context, true, \"Could not subscribe DBus with path:%s\", context->object_path);\n  }\n\n  return true;\n}\n\n#ifdef TYM_USE_VTE_SPAWN_ASYNC\nstatic void on_vte_spawn(VteTerminal* vte, GPid child_pid, GError* error, void* user_data)\n{\n  Context* context = (Context*)user_data;\n  context->initialized = true;\n  context->child_pid = child_pid;\n  if (error) {\n    g_warning(\"vte-spawn error: %s\", error->message);\n    /* g_error_free(error); */\n    gtk_window_close(context->layout.window);\n    app_quit_context(context);\n    /* dd(\"%d\", gtk_application_new); */\n    return;\n  }\n}\n#endif\n\nint on_command_line(GApplication* gapp, GApplicationCommandLine* cli, void* user_data)\n{\n  df();\n  GError* error = NULL;\n\n  int argc = -1;\n  char** argv = g_application_command_line_get_arguments(cli, &argc);\n\n  Option* option = option_init(meta_get_option_entries(app->meta));\n  if (!option_parse(option, argc, argv)){\n    return 1;\n  };\n\n  if (option_get_bool(option, \"daemon\")) {\n    GtkWindow* window = gtk_application_get_active_window(GTK_APPLICATION(gapp));\n    if (window) {\n      g_warning(\"Blocked another instance from trying to start as daemon process.\");\n      return 1;\n    }\n\n    /* Only creates a window, never shows it. */\n    window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(gapp)));\n    UNUSED(window);\n    g_message(\"Starting as daemon process.\");\n    return 0;\n  }\n\n  if (option_get_str(option, \"signal\") || option_get_str(option, \"call\")) {\n    /* Do nothing */\n    dd(\"D-Bus signal/method call was performed on a remote process.\");\n    return 0;\n  }\n\n  Context* context = app_spawn_context(option);\n  if (!context) {\n    return 1;\n  }\n\n  context_load_device(context);\n  context_load_lua_context(context);\n\n  context_build_layout(context);\n  context_restore_default(context);\n  context_load_theme(context);\n  context_load_config(context);\n  context_override_by_option(context);\n\n  VteTerminal* vte = context->layout.vte;\n  GtkWindow* window = context->layout.window;\n\n  GtkTargetEntry drop_types[] = {\n    {\"text/uri-list\", 0, 0}\n  };\n  gtk_drag_dest_set(GTK_WIDGET(vte), GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP, drop_types, G_N_ELEMENTS(drop_types), GDK_ACTION_COPY);\n\n  context_signal_connect(context, vte, \"drag-data-received\", G_CALLBACK(on_vte_drag_data_received));\n  context_signal_connect(context, vte, \"key-press-event\", G_CALLBACK(on_vte_key_press));\n  context_signal_connect(context, vte, \"scroll-event\", G_CALLBACK(on_vte_mouse_scroll));\n  context_signal_connect(context, vte, \"child-exited\", G_CALLBACK(on_vte_child_exited));\n  context_signal_connect(context, vte, \"window-title-changed\", G_CALLBACK(on_vte_title_changed));\n  context_signal_connect(context, vte, \"bell\", G_CALLBACK(on_vte_bell));\n  context_signal_connect(context, vte, \"button-press-event\", G_CALLBACK(on_vte_click));\n  context_signal_connect(context, vte, \"selection-changed\", G_CALLBACK(on_vte_selection_changed));\n  context_signal_connect(context, vte, \"resize-window\", G_CALLBACK(on_vte_resize_request));\n  context_signal_connect(context, window, \"destroy\", G_CALLBACK(on_window_close));\n  context_signal_connect(context, window, \"focus-in-event\", G_CALLBACK(on_window_focus_in));\n  context_signal_connect(context, window, \"focus-out-event\", G_CALLBACK(on_window_focus_out));\n  context_signal_connect(context, window, \"draw\", G_CALLBACK(on_window_draw));\n  context_signal_connect(context, window, \"size-allocate\", G_CALLBACK(on_window_resize));\n\n  if (app->is_isolated) {\n    g_message(\"This process is isolated so never listen to D-Bus signal/method call.\");\n  } else {\n    _subscribe_dbus(context);\n  }\n\n  const char* shell_line = context_get_str(context, \"shell\");\n\n  char** shell_argv = NULL;\n  if (g_strv_length(option->rest_argv) >= 2) {\n    GStrvBuilder* builder = g_strv_builder_new();\n    char** a = &option->rest_argv[1];\n    while (*a) {\n      /* Skips an unnecessary entry that equals `--` */\n      if (is_equal(*a, \"--\")) {\n        a++;\n        continue;\n      }\n      g_strv_builder_add(builder, *a);\n      a++;\n    }\n    shell_argv = g_strv_builder_end(builder);\n  } else {\n    g_shell_parse_argv(shell_line, NULL, &shell_argv, &error);\n    if (error) {\n      g_warning(\"Parse error: %s\", error->message);\n      g_error_free(error);\n      app_quit_context(context);\n      return 0;\n    }\n  }\n\n  const char* const* env = g_application_command_line_get_environ(cli);\n  char** shell_env = g_new0(char*, g_strv_length((char**)env) + 1);\n  int i = 0;\n  while (env[i]) {\n    shell_env[i] = g_strdup(env[i]);\n    i += 1;\n  }\n  shell_env = g_environ_setenv(shell_env, \"TERM\", context_get_str(context, \"term\"), true);\n  char* id_str = g_strdup_printf(\"%i\", context->id);\n  shell_env = g_environ_setenv(shell_env, \"TYM_ID\", id_str, true);\n  g_free(id_str);\n\n  const char* cwd = option_get_str(option, \"cwd\");\n  if (cwd == NULL) {\n    cwd = g_application_command_line_get_cwd(cli);\n  }\n\n#ifdef TYM_USE_VTE_SPAWN_ASYNC\n  vte_terminal_spawn_async(\n    vte,                 // terminal\n    VTE_PTY_DEFAULT,     // pty flag\n    cwd,                 // working directory\n    shell_argv,          // argv\n    shell_env,           // envv\n    G_SPAWN_SEARCH_PATH, // spawn_flags\n    NULL,                // child_setup\n    NULL,                // child_setup_data\n    NULL,                // child_setup_data_destroy\n    5000,                // timeout\n    NULL,                // cancel callback\n    on_vte_spawn,        // callback\n    context              // user_data\n  );\n#else\n  GPid child_pid;\n  vte_terminal_spawn_sync(\n    vte,\n    VTE_PTY_DEFAULT,\n    cwd,\n    shell_argv,\n    shell_env,\n    G_SPAWN_SEARCH_PATH,\n    NULL,\n    NULL,\n    &child_pid,\n    NULL,\n    &error\n  );\n  context->child_pid = child_pid;\n\n  if (error) {\n    g_strfreev(shell_env);\n    g_strfreev(shell_argv);\n    g_error(\"%s\", error->message);\n    g_error_free(error);\n    app_quit_context(context);\n    return 1;\n  }\n#endif\n\n  g_strfreev(shell_env);\n  g_strfreev(shell_argv);\n  gtk_widget_grab_focus(GTK_WIDGET(vte));\n  gtk_widget_show_all(GTK_WIDGET(context->layout.window));\n  return 0;\n}\n"
  },
  {
    "path": "src/builtin.c",
    "content": "/**\n * builtin.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"builtin.h\"\n#include \"context.h\"\n#include \"command.h\"\n#include \"app.h\"\n\n\nstatic int builtin_get(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  const char* key = luaL_checkstring(L, 1);\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (!e) {\n    luaX_warn(L, \"Invalid config key: '%s'\", key);\n    lua_pushnil(L);\n    return 1;\n  }\n\n  switch (e->type) {\n    case META_ENTRY_TYPE_STRING:\n      lua_pushstring(L, context_get_str(context, key));\n      break;\n    case META_ENTRY_TYPE_INTEGER:\n      lua_pushinteger(L, context_get_int(context, key));\n      break;\n    case META_ENTRY_TYPE_BOOLEAN:\n      lua_pushboolean(L, context_get_bool(context, key));\n      break;\n    default:\n      lua_pushnil(L);\n      break;\n  }\n  return 1;\n}\n\nstatic int builtin_quit(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  gtk_window_close(context->layout.window);\n  return 0;\n}\n\nstatic int builtin_set(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  const char* key = luaL_checkstring(L, 1);\n\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (!e) {\n    luaX_warn(L, \"Invalid config key: '%s'\", key);\n    return 0;\n  }\n\n  int type = lua_type(L, 2);\n  switch (e->type) {\n    case META_ENTRY_TYPE_STRING: {\n      const char* value = lua_tostring(L, 2);\n      if (!value) {\n        luaX_warn(L, \"Invalid string config for '%s' (string expected, got %s)\", key, lua_typename(L, type));\n        break;\n      }\n      context_set_str(context, key, value);\n      break;\n    }\n    case META_ENTRY_TYPE_INTEGER: {\n      if (type != LUA_TNUMBER) {\n        luaX_warn(L, \"Invalid integer config for '%s': %s (number expected, got %s)\", key, lua_tostring(L, 2), lua_typename(L, type));\n        break;\n      }\n      int value = lua_tointeger(L, 2);\n      context_set_int(context, key, value);\n      break;\n    }\n    case META_ENTRY_TYPE_BOOLEAN: {\n      int value = lua_toboolean(L, 2);\n      context_set_bool(context, key, value);\n      break;\n    }\n    default:\n      break;\n  }\n  return 0;\n}\n\nstatic int get_default_value(lua_State* L)\n{\n  /* Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); */\n\n  const char* key = luaL_checkstring(L, 1);\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (!e) {\n    luaX_warn(L, \"Invalid config key: '%s'\", key);\n    lua_pushnil(L);\n    return 1;\n  }\n\n  switch (e->type) {\n    case META_ENTRY_TYPE_STRING:\n      lua_pushstring(L, (char*)e->default_value);\n      break;\n    case META_ENTRY_TYPE_INTEGER:\n      lua_pushinteger(L, *(int*)e->default_value);\n      break;\n    case META_ENTRY_TYPE_BOOLEAN:\n      lua_pushboolean(L, *(bool*)e->default_value);\n      break;\n    default:\n      lua_pushnil(L);\n      break;\n  }\n  return 1;\n}\n\nstatic int builtin_get_config(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  lua_newtable(L);\n\n  for (GList* li = app->meta->list; li != NULL; li = li->next) {\n    MetaEntry* e = (MetaEntry*)li->data;\n    char* key = e->name;\n    lua_pushstring(L, key);\n    switch (e->type) {\n      case META_ENTRY_TYPE_STRING: {\n        const char* value = context_get_str(context, key);\n        lua_pushstring(L, value);\n        break;\n      }\n      case META_ENTRY_TYPE_INTEGER:\n        lua_pushinteger(L, context_get_int(context, key));\n        break;\n      case META_ENTRY_TYPE_BOOLEAN:\n        lua_pushboolean(L, context_get_bool(context, key));\n        break;\n      case META_ENTRY_TYPE_NONE:\n        lua_pop(L, 1);\n        continue;\n    }\n    lua_settable(L, -3);\n  }\n  return 1;\n}\n\nstatic int builtin_set_config(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  luaL_argcheck(L, lua_istable(L, 1), 1, \"table expected\");\n\n  lua_pushnil(L);\n  while (lua_next(L, -2)) {\n    lua_pushvalue(L, -2);\n    const char* key = lua_tostring(L, -1);\n    MetaEntry* e = meta_get_entry(app->meta, key);\n    if (e) {\n      int type = lua_type(L, -2);\n      switch (e->type) {\n        case META_ENTRY_TYPE_STRING: {\n          const char* value = lua_tostring(L, -2);\n          if (!value) {\n            luaX_warn(L, \"Invalid string config for '%s' (string expected, got %s)\", key, lua_typename(L, type));\n            break;\n          }\n          context_set_str(context, key, value);\n          break;\n        }\n        case META_ENTRY_TYPE_INTEGER: {\n          if (type != LUA_TNUMBER) {\n            luaX_warn(L, \"Invalid integer config for '%s': %s (number expected, got %s)\", key, lua_tostring(L, -2), lua_typename(L, type));\n            break;\n          }\n          int value = lua_tointeger(L, -2);\n          context_set_int(context, key, value);\n          break;\n        }\n        case META_ENTRY_TYPE_BOOLEAN: {\n          int value = lua_toboolean(L, -2);\n          context_set_bool(context, key, value);\n          break;\n        }\n        default:\n          break;\n      }\n    } else {\n      luaX_warn(L, \"Invalid config key: '%s'\", key);\n    }\n    lua_pop(L, 2);\n  }\n\n  return 0;\n}\n\nstatic int builtin_reset_config(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  context_restore_default(context);\n  return 0;\n}\n\nstatic int builtin_set_keymap(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  const char* key = luaL_checkstring(L, 1);\n  luaL_argcheck(L, lua_isfunction(L, 2), 2, \"function expected\");\n\n  int ref = luaL_ref(L, LUA_REGISTRYINDEX);\n  bool ok = keymap_add_entry(context->keymap, key, ref);\n  if (!ok) {\n    luaL_unref(L, LUA_REGISTRYINDEX, ref);\n    luaX_warn(L, \"Invalid accelerator: '%s'\", key);\n    return 0;\n  }\n  return 0;\n}\n\nstatic int builtin_unset_keymap(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* key = luaL_checkstring(L, 1);\n  bool removed = keymap_remove_entry(context->keymap, key);\n  if (!removed) {\n    luaX_warn(L, \"Tried to remove en empty keymap '(%s') which is not assigned function to\", key);\n  }\n  return 0;\n}\n\nstatic int builtin_set_keymaps(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  luaL_argcheck(L, lua_istable(L, 1), 1, \"table expected\");\n\n  lua_pushnil(L);\n  while (lua_next(L, -2)) {\n    lua_pushvalue(L, -2);\n    const char* key = lua_tostring(L, -1);\n    if (!lua_isfunction(L, -2)) {\n      luaX_warn(L, \"Invalid value for '%s': function expected, got %s\", key, lua_typename(L, lua_type(L, -2)));\n    } else {\n      lua_pushvalue(L, -2); // push function to stack top\n      int ref = luaL_ref(L, LUA_REGISTRYINDEX);\n      bool ok = keymap_add_entry(context->keymap, key, ref);\n      if (!ok) {\n        luaL_unref(L, LUA_REGISTRYINDEX, ref);\n        luaX_warn(L, \"Invalid accelerator: '%s'\", key);\n      }\n    }\n    lua_pop(L, 2);\n  }\n\n  return 0;\n}\n\nstatic int builtin_reset_keymaps(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  keymap_reset(context->keymap);\n  return 0;\n}\n\nstatic int builtin_set_hook(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* key = luaL_checkstring(L, 1);\n  luaL_argcheck(L, lua_isfunction(L, 2), 2, \"function expected\");\n  int ref = luaL_ref(L, LUA_REGISTRYINDEX);\n  int old_ref = -1;\n  if (hook_set_ref(context->hook, key, ref, &old_ref)) {\n    if (old_ref > 0) {\n      dd(\"unref old ref\");\n      luaL_unref(L, LUA_REGISTRYINDEX, old_ref);\n    }\n    return 0;\n  }\n  luaL_unref(L, LUA_REGISTRYINDEX, ref);\n  luaX_warn(L, \"Invalid hook key: '%s'\", key);\n  return 0;\n}\n\nstatic int builtin_set_hooks(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  luaL_argcheck(L, lua_istable(L, 1), 1, \"table expected\");\n  lua_pushnil(L);\n  while (lua_next(L, -2)) {\n    lua_pushvalue(L, -2);\n    const char* key = lua_tostring(L, -1);\n    if (!lua_isfunction(L, -2)) {\n      luaX_warn(L, \"Invalid value for '%s': function expected, got %s\", key, lua_typename(L, lua_type(L, -2)));\n    } else {\n      lua_pushvalue(L, -2); // push function to stack top\n      int ref = luaL_ref(L, LUA_REGISTRYINDEX);\n      int old_ref = -1;\n      int ok = hook_set_ref(context->hook, key, ref, &old_ref);\n      if (old_ref > 0) {\n        dd(\"unref old ref\");\n        luaL_unref(L, LUA_REGISTRYINDEX, old_ref);\n      }\n      if (!ok) {\n        luaL_unref(L, LUA_REGISTRYINDEX, ref);\n        luaX_warn(L, \"Invalid hook key: '%s'\", key);\n      }\n    }\n    lua_pop(L, 2);\n  }\n  return 0;\n}\n\nstatic int builtin_reload(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  context_load_config(context);\n  context_load_theme(context);\n  return 0;\n}\n\nstatic int builtin_reload_theme(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  context_load_theme(context);\n  return 0;\n}\n\nstatic int builtin_send_key(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* accelerator = luaL_checkstring(L, 1);\n  unsigned key;\n  GdkModifierType mod;\n  gtk_accelerator_parse(accelerator, &key, &mod);\n  if (0 == key && 0 == mod) {\n    luaL_error(L, \"Invalid accelerator: '%s'\", accelerator);\n    return 0;\n  }\n  GdkEvent* event = gdk_event_new(GDK_KEY_PRESS);\n  if (context->device == NULL) {\n    g_warning(\"Could not get input device.\");\n    return 0;\n  }\n  gdk_event_set_device(event, context->device);\n  event->key.window = g_object_ref(gtk_widget_get_window(GTK_WIDGET(context->layout.window)));\n  event->key.send_event = false;\n  event->key.time = GDK_CURRENT_TIME;\n  event->key.state = mod;\n  event->key.keyval = key;\n  gtk_main_do_event((GdkEvent*)event);\n  event->type = GDK_KEY_RELEASE;\n  gtk_main_do_event((GdkEvent*)event);\n  gdk_event_free((GdkEvent*)event);\n  return 0;\n}\n\ntypedef struct {\n  Context* context;\n  int ref;\n} TimeoutCallbackNotation;\n\nstatic int timeout_callback(void* user_data)\n{\n  TimeoutCallbackNotation* notation = (TimeoutCallbackNotation*)user_data;\n\n  lua_State* L = notation->context->lua;\n  lua_rawgeti(L, LUA_REGISTRYINDEX, notation->ref);\n  if (!lua_isfunction(L, -1)) {\n    lua_pop(L, 1); // pop none-function\n    dd(\"tried to call non-function\");\n    return false;\n  }\n\n  if (lua_pcall(L, 0, 1, 0) != LUA_OK) {\n    luaX_warn(L, \"Error in timeout function: '%s'\", lua_tostring(L, -1));\n    lua_pop(L, 1); // error\n    return false;\n  }\n  bool result = lua_toboolean(L, -1);\n  lua_pop(L, 1);\n  return result;\n}\n\nstatic int builtin_set_timeout(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  luaL_argcheck(L, lua_isfunction(L, 1), 1, \"function expected\");\n  int interval = lua_tointeger(L, 2); // if non-number, falling back to 0\n\n  lua_pushvalue(L, 1);\n  int ref = luaL_ref(L, LUA_REGISTRYINDEX);\n  TimeoutCallbackNotation* notation = g_new0(TimeoutCallbackNotation, 1);\n  notation->context = context;\n  notation->ref = ref;\n  int tag = g_timeout_add_full(G_PRIORITY_DEFAULT, interval, (GSourceFunc)timeout_callback, notation, g_free);\n  lua_pushinteger(L, tag);\n  return 1;\n}\n\nstatic int builtin_clear_timeout(lua_State* L)\n{\n  int tag = luaL_checkinteger(L, 1);\n  g_source_remove(tag);\n  return 0;\n}\n\nstatic int builtin_put(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* text = luaL_checkstring(L, -1);\n  vte_terminal_feed_child(context->layout.vte, text, -1);\n  return 0;\n}\n\nstatic int builtin_bell(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  gdk_window_beep(context_get_gdk_window(context));\n  return 0;\n}\n\nstatic int builtin_open(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* uri = luaL_checkstring(L, -1);\n  context_launch_uri(context, uri);\n  return 0;\n}\n\nstatic int builtin_notify(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* body = luaL_checkstring(L, 1);\n  const char* title = lua_tostring(L, 2);\n  context_notify(context, body, title);\n  return 0;\n}\n\nstatic int builtin_copy(lua_State* L)\n{\n  const char* text = luaL_checkstring(L, 1);\n  const char* target = lua_tostring(L, 2);\n  GdkAtom selection = GDK_SELECTION_CLIPBOARD;\n  if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) {\n  } else if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) {\n    selection = GDK_SELECTION_PRIMARY;\n  } else if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) {\n    selection = GDK_SELECTION_SECONDARY;\n  } else {\n    luaX_warn(L, \"Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.\", target);\n  }\n  GtkClipboard* cb = gtk_clipboard_get(selection);\n  gtk_clipboard_set_text(cb, text, -1);\n  return 0;\n}\n\nstatic int builtin_copy_selection(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* target = lua_tostring(L, 1);\n  if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) {\n    command_copy_selection(context);\n    return 0;\n  }\n  if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) {\n    // nothing to do\n    return 0;\n  }\n  if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) {\n    // go down\n  } else {\n    luaX_warn(L, \"Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.\", target);\n    return 0;\n  }\n\n  GtkClipboard* pri = gtk_clipboard_get(GDK_SELECTION_PRIMARY);\n  char* text = gtk_clipboard_wait_for_text(pri);\n  GtkClipboard* sec = gtk_clipboard_get(GDK_SELECTION_SECONDARY);\n  gtk_clipboard_set_text(sec, text, -1);\n  g_free(text);\n  return 0;\n}\n\nstatic int builtin_paste(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  const char* target = lua_tostring(L, 1);\n  if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) {\n    command_paste(context);\n    return 0;\n  }\n  GdkAtom selection = GDK_SELECTION_CLIPBOARD;\n  if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) {\n    selection = GDK_SELECTION_PRIMARY;\n  } else if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) {\n    selection = GDK_SELECTION_SECONDARY;\n  } else {\n    luaX_warn(L, \"Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.\", target);\n    return 0;\n  }\n  GtkClipboard* cb = gtk_clipboard_get(selection);\n  char* text = gtk_clipboard_wait_for_text(cb);\n  vte_terminal_feed_child(context->layout.vte, text, -1);\n  g_free(text);\n  return 0;\n}\n\nstatic int builtin_color_to_rgba(lua_State* L)\n{\n  GdkRGBA color;\n  const char* str = luaL_checkstring(L, -1);\n  bool valid = gdk_rgba_parse(&color, str);\n  if (!valid) {\n    luaX_warn(L, \"Invalid color string: '%s'\", str);\n    return 0;\n  }\n  lua_pushinteger(L, roundup(color.red * 255));\n  lua_pushinteger(L, roundup(color.green * 255));\n  lua_pushinteger(L, roundup(color.blue * 255));\n  lua_pushnumber(L, color.alpha);\n  return 4;\n}\n\nstatic int builtin_rgba_to_color(lua_State* L)\n{\n  int red = luaL_checknumber(L, 1);\n  int green = luaL_checknumber(L, 2);\n  int blue = luaL_checknumber(L, 3);\n  double alpha = lua_isnone(L, 4) ? 1.0 : lua_tonumber(L, 4);\n  char* str = g_strdup_printf(\"rgba(%d, %d, %d, %f)\", red, green, blue, alpha);\n  lua_pushstring(L, str);\n  g_free(str);\n  return 1;\n}\n\nstatic int builtin_rgb_to_hex(lua_State* L)\n{\n  int red = luaL_checknumber(L, 1);\n  int green = luaL_checknumber(L, 2);\n  int blue = luaL_checknumber(L, 3);\n  char* hex = g_strdup_printf(\"#%x%x%x\", red, green, blue);\n  lua_pushstring(L, hex);\n  g_free(hex);\n  return 1;\n}\n\nstatic int builtin_hex_to_rgb(lua_State* L)\n{\n  const char* hex = luaL_checkstring(L, 1);\n\n  GdkRGBA color = {};\n  bool valid = gdk_rgba_parse(&color, hex);\n  if (!valid) {\n    luaX_warn(L, \"Invalid hex string: '%s'\", hex);\n    return 0;\n  }\n  lua_pushinteger(L, roundup(color.red * 255));\n  lua_pushinteger(L, roundup(color.green * 255));\n  lua_pushinteger(L, roundup(color.blue * 255));\n  return 3;\n}\n\nstatic GVariant* table_to_variant(lua_State* L, int table_index)\n{\n  luaL_argcheck(L, lua_istable(L, table_index), table_index, \"table expected\");\n\n#if USES_LUAJIT\n  size_t num_params = lua_objlen(L, table_index);\n#else\n  size_t num_params = lua_rawlen(L, table_index);\n#endif\n  GVariant** vv = g_new0(GVariant*, num_params);\n  int i = 0;\n  while (i < num_params) {\n    lua_rawgeti(L, table_index, i + 1); // args[table_index][i+1]\n    vv[i] = g_variant_new_string(lua_tostring(L, -1));\n    lua_pop(L, 1);\n    i += 1;\n  }\n  GVariant* p = g_variant_new_tuple(vv, num_params);\n  g_free(vv);\n  return p;\n}\n\n/* usage tym.signal(0, 'hook', {'param'}) */\nstatic int builtin_signal(lua_State* L)\n{\n  int target_id = luaL_checkinteger(L, 1);\n  const char* signal_name = luaL_checkstring(L, 2);\n  GVariant* params = lua_gettop(L) >= 3\n    ? table_to_variant(L, 3)\n    : g_variant_new(\"()\");\n\n  GDBusConnection* conn = g_application_get_dbus_connection(app->gapp);\n  GError* error = NULL;\n  char* object_path = g_strdup_printf(TYM_OBJECT_PATH_FMT_INT, target_id);\n  g_dbus_connection_emit_signal(conn, NULL, object_path, TYM_APP_ID, signal_name, params, &error);\n  g_free(object_path);\n  if (error) {\n    luaX_warn(L, \"DBus error: '%s'\", error->message);\n    g_error_free(error);\n    return 0;\n  }\n\n  return 0;\n}\n\ntypedef struct {\n  Context* context;\n  int ref;\n} CallCallbackNotation;\n\nvoid push_value_by_gvariant(lua_State* L, GVariant* v) {\n  if (g_variant_is_of_type(v, G_VARIANT_TYPE_INT32)) {\n    lua_pushinteger(L, g_variant_get_int32(v));\n  } else if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) {\n    char* s = NULL;\n    g_variant_get(v, \"s\", &s);\n    lua_pushstring(L, s);\n    g_free(s);\n  } else if (g_variant_is_of_type(v, G_VARIANT_TYPE_ARRAY) || g_variant_is_of_type(v, G_VARIANT_TYPE_TUPLE)) {\n    lua_newtable(L);\n    size_t num = g_variant_n_children(v);\n    int i = 0;\n    while (i < num) {\n      GVariant* e = g_variant_get_child_value(v, i);\n      push_value_by_gvariant(L, e);\n      lua_rawseti(L, -2, i + 1);\n      i += 1;\n    }\n  } else {\n    lua_pushstring(L, g_variant_print(v, false));\n  }\n}\n\nvoid call_callback(GObject* source_object, GAsyncResult* res, void* user_data)\n{\n  GError* error = NULL;\n  TimeoutCallbackNotation* notation = (TimeoutCallbackNotation*)user_data;\n  Context* context = notation->context;\n  lua_State* L = context->lua;\n\n  GDBusConnection* conn = g_application_get_dbus_connection(app->gapp);\n  GVariant* result = g_dbus_connection_call_finish(conn, res, &error);\n\n  lua_rawgeti(L, LUA_REGISTRYINDEX, notation->ref);\n  if (!lua_isfunction(L, -1)) {\n    lua_pop(L, 1); // pop none-function\n    context_log_warn(context, true, \"tried to call non-function\");\n    return;\n  }\n\n  int num_args = 0;\n  if (error) {\n    char* m = g_strdup_printf(\"DBus error: '%s'\", error->message);\n    luaX_warn(L, \"%s\", m);\n    lua_pushstring(L, m);\n    g_error_free(error);\n    g_free(m);\n    num_args = 1;\n  } else {\n    dd(\"DBus method call result: `%s`\", g_variant_print(result, true));\n    int num_result = g_variant_n_children(result);\n    int i = 0;\n    while (i < num_result) {\n      GVariant* e = g_variant_get_child_value(result, i);\n      push_value_by_gvariant(L, e);\n      i += 1;\n    }\n    num_args = num_result;\n  }\n\n  if (lua_pcall(L, num_args, 0, 0) != LUA_OK) {\n    luaX_warn(L, \"Error in timeout function: '%s'\", lua_tostring(L, -1));\n    lua_pop(L, 1); // error\n  }\n}\n\n/* usage: tym.call(0, 'eval', {'return 1+2'}, function(...) end) */\nstatic int builtin_call(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  int target_id = luaL_checkinteger(L, 1);\n  const char* method_name = luaL_checkstring(L, 2);\n  GVariant* params = table_to_variant(L, 3);\n\n  bool has_cb = lua_gettop(L) >= 4;\n\n  CallCallbackNotation* notation = NULL;\n  GAsyncReadyCallback cb = NULL;\n  if (has_cb) {\n    luaL_argcheck(L, lua_isfunction(L, 4), 4, \"function expected\");\n    lua_pushvalue(L, 4);\n    int ref = luaL_ref(L, LUA_REGISTRYINDEX);\n    notation = g_new0(CallCallbackNotation, 1);\n    notation->context = context;\n    notation->ref = ref;\n    cb = (GAsyncReadyCallback)call_callback;\n  }\n\n  GDBusConnection* conn = g_application_get_dbus_connection(app->gapp);\n  char* object_path = g_strdup_printf(TYM_OBJECT_PATH_FMT_INT, target_id);\n\n  g_dbus_connection_call(\n      conn,        // conn\n      TYM_APP_ID,  // bus_name\n      object_path, // object_path\n      TYM_APP_ID,  // interface_name\n      method_name, // method_name\n      params,      // parameters\n      NULL,        // reply_type\n      G_DBUS_CALL_FLAGS_NONE, // flags\n      1000,        // timeout\n      NULL,        // cancellable\n      cb,          // callback\n      notation     // user_data\n  );\n  g_free(object_path);\n  return 0;\n}\n\nstatic int builtin_check_mod_state(lua_State* L)\n{\n  const char* accelerator = luaL_checkstring(L, 1);\n  unsigned key;\n  GdkModifierType mod;\n  gtk_accelerator_parse(accelerator, &key, &mod);\n  GdkDisplay* display = gdk_display_get_default();\n  GdkKeymap* kmap = gdk_keymap_get_for_display(display);\n  unsigned current_mod = gdk_keymap_get_modifier_state(kmap);\n  lua_pushboolean(L, mod & current_mod);\n  return 1;\n}\n\nstatic int builtin_get_cursor_position(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  long col = 0;\n  long row = 0;\n  vte_terminal_get_cursor_position(context->layout.vte, &col, &row);\n  lua_pushinteger(L, col);\n  lua_pushinteger(L, row);\n  return 2;\n}\n\nstatic int builtin_get_clipboard(lua_State* L)\n{\n  const char* target = lua_tostring(L, 1);\n  GdkAtom selection = GDK_SELECTION_CLIPBOARD;\n  if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) {\n  } else if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) {\n    selection = GDK_SELECTION_PRIMARY;\n  } else if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) {\n    selection = GDK_SELECTION_SECONDARY;\n  } else {\n    luaX_warn(L, \"Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.\", target);\n    return 0;\n  }\n  GtkClipboard* cb = gtk_clipboard_get(selection);\n  char* text = gtk_clipboard_wait_for_text(cb);\n  lua_pushstring(L, text);\n  g_free(text);\n  return 1;\n}\n\nstatic int builtin_get_selection(lua_State* L)\n{\n  GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_PRIMARY);\n  char* text = gtk_clipboard_wait_for_text(cb);\n  lua_pushstring(L, text);\n  g_free(text);\n  return 1;\n}\n\nstatic int builtin_unselect_all(lua_State* L)\n{\n\tContext* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\tvte_terminal_unselect_all(context->layout.vte);\n\treturn 0;\n}\n\nstatic int builtin_select_all(lua_State *L) {\n\tContext* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\tvte_terminal_select_all(context->layout.vte);\n\treturn 0;\n}\n\nstatic int builtin_has_selection(lua_State *L) {\n\tContext* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\tlua_pushboolean(L, vte_terminal_get_has_selection(context->layout.vte));\n\treturn 1;\n}\n\n\nstatic int builtin_get_text(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  int start_row = luaL_checkinteger(L, 1);\n  int start_col = luaL_checkinteger(L, 2);\n  int end_row = luaL_checkinteger(L, 3);\n  int end_col = luaL_checkinteger(L, 4);\n  if (end_row < 0) {\n    end_row = vte_terminal_get_row_count(context->layout.vte);\n  }\n  if (end_col < 0) {\n    end_col = vte_terminal_get_column_count(context->layout.vte);\n  }\n\n#ifdef TYM_USE_VTE_GET_TEXT_RANGE_FORMAT\n  char* selection = vte_terminal_get_text_range_format(context->layout.vte, VTE_FORMAT_TEXT, start_row, start_col, end_row, end_col, NULL);\n#else\n  char* selection = vte_terminal_get_text_range(context->layout.vte, start_row, start_col, end_row, end_col, NULL, NULL, NULL);\n#endif\n  lua_pushstring(L, selection);\n  g_free(selection);\n  return 1;\n}\n\nstatic int builtin_get_monitor_model(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  GdkDisplay* display = gdk_display_get_default();\n  GdkMonitor* monitor = gdk_display_get_monitor_at_window(display, context_get_gdk_window(context));\n  lua_pushstring(L, gdk_monitor_get_model(monitor));\n  return 1;\n}\n\nstatic int builtin_get_config_path(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  char* path = context_acquire_config_path(context);\n  lua_pushstring(L, path);\n  if (path) {\n    g_free(path);\n  }\n  return 1;\n}\n\nstatic int builtin_get_theme_path(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  char* path = context_acquire_theme_path(context);\n  lua_pushstring(L, path);\n  if (path) {\n    g_free(path);\n  }\n  return 1;\n}\n\nstatic int builtin_get_id(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  lua_pushinteger(L, context->id);\n  return 1;\n}\n\nstatic int builtin_get_ids(lua_State* L)\n{\n  lua_newtable(L);\n  int i = 0;\n  for (GList* li = app->contexts; li != NULL; li = li->next) {\n    Context* c = (Context*)li->data;\n    lua_pushinteger(L, c->id);\n    lua_rawseti(L, -2, i + 1);\n    i += 1;\n  }\n  return 1;\n}\n\nstatic int builtin_get_object_path(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  lua_pushstring(L, context->object_path);\n  return 1;\n}\n\nstatic int builtin_get_terminal_pid(lua_State* L)\n{\n  lua_pushinteger(L, getpid());\n  return 1;\n}\n\nstatic int builtin_get_pid(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  lua_pushinteger(L, context->child_pid);\n  return 1;\n}\n\nstatic int builtin_get_version(lua_State* L)\n{\n  lua_pushstring(L, PACKAGE_VERSION);\n  return 1;\n}\n\nstatic int builtin_apply(lua_State* L)\n{\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n  const char* message = \"DEPRECATED: `tym.apply()` is never needed. You can `tym.set()` and the value is applied right away to the app.\";\n  context_notify(context, message, NULL);\n  luaX_warn(L, \"%s\", message);\n  return 0;\n}\n\nint builtin_register_module(lua_State* L)\n{\n  const luaL_Reg table[] = {\n    { \"quit\"                , builtin_quit                 },\n    { \"get\"                 , builtin_get                  },\n    { \"set\"                 , builtin_set                  },\n    { \"get_default_value\"   , get_default_value            },\n    { \"get_config\"          , builtin_get_config           },\n    { \"set_config\"          , builtin_set_config           },\n    { \"reset_config\"        , builtin_reset_config         },\n    { \"set_keymap\"          , builtin_set_keymap           },\n    { \"unset_keymap\"        , builtin_unset_keymap         },\n    { \"set_keymaps\"         , builtin_set_keymaps          },\n    { \"reset_keymaps\"       , builtin_reset_keymaps        },\n    { \"set_hook\"            , builtin_set_hook             },\n    { \"set_hooks\"           , builtin_set_hooks            },\n    { \"reload\"              , builtin_reload               },\n    { \"reload_theme\"        , builtin_reload_theme         },\n    { \"send_key\"            , builtin_send_key             },\n    { \"set_timeout\"         , builtin_set_timeout          },\n    { \"clear_timeout\"       , builtin_clear_timeout        },\n    { \"put\"                 , builtin_put                  },\n    { \"bell\"                , builtin_bell                 },\n    { \"open\"                , builtin_open                 },\n    { \"notify\"              , builtin_notify               },\n    { \"copy\"                , builtin_copy                 },\n    { \"copy_selection\"      , builtin_copy_selection       },\n    { \"paste\"               , builtin_paste                },\n    { \"check_mod_state\"     , builtin_check_mod_state      },\n    { \"color_to_rgba\"       , builtin_color_to_rgba        },\n    { \"rgba_to_color\"       , builtin_rgba_to_color        },\n    { \"rgb_to_hex\"          , builtin_rgb_to_hex           },\n    { \"hex_to_rgb\"          , builtin_hex_to_rgb           },\n    { \"signal\"              , builtin_signal               },\n    { \"call\"                , builtin_call                 },\n    { \"get_monitor_model\"   , builtin_get_monitor_model    },\n    { \"get_cursor_position\" , builtin_get_cursor_position  },\n    { \"get_clipboard\"       , builtin_get_clipboard        },\n    { \"get_selection\"       , builtin_get_selection        },\n    { \"unselect_all\"        , builtin_unselect_all         },\n    { \"select_all\"          , builtin_select_all           },\n    { \"has_selection\"       , builtin_has_selection        },\n    { \"get_text\"            , builtin_get_text             },\n    { \"get_config_path\"     , builtin_get_config_path      },\n    { \"get_theme_path\"      , builtin_get_theme_path       },\n    { \"get_id\"              , builtin_get_id               },\n    { \"get_ids\"             , builtin_get_ids              },\n    { \"get_object_path\"     , builtin_get_object_path      },\n    { \"get_terminal_pid\"    , builtin_get_terminal_pid     },\n    { \"get_pid\"             , builtin_get_pid              },\n    { \"get_version\"         , builtin_get_version          },\n    // DEPRECATED\n    { \"apply\"               , builtin_apply                },\n    { NULL, NULL },\n  };\n  Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1));\n\n  luaL_newlibtable(L, table);\n  lua_pushlightuserdata(L, context);\n  luaL_setfuncs(L, table, 1);\n  return 1;\n}\n"
  },
  {
    "path": "src/command.c",
    "content": "/**\n * command.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"command.h\"\n\n\nvoid command_reload(Context* context)\n{\n  context_load_config(context);\n  context_load_theme(context);\n}\n\nvoid command_reload_theme(Context* context)\n{\n  context_load_theme(context);\n}\n\nvoid command_copy_selection(Context* context)\n{\n#ifdef TYM_USE_VTE_COPY_CLIPBOARD_FORMAT\n  vte_terminal_copy_clipboard_format(context->layout.vte, VTE_FORMAT_TEXT);\n#else\n  vte_terminal_copy_clipboard(context->layout.vte);\n#endif\n}\n\nvoid command_paste(Context* context)\n{\n  vte_terminal_paste_clipboard(context->layout.vte);\n}\n"
  },
  {
    "path": "src/common.c",
    "content": "/**\n * common.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"common.h\"\n\nconst int TYM_DEFAULT_WIDTH = 80;\nconst int TYM_DEFAULT_HEIGHT = 22;\nconst int TYM_DEFAULT_SCALE = 100;\nconst int TYM_DEFAULT_CELL_SIZE = 100;\nconst int TYM_DEFAULT_SCROLLBACK = 512;\n\n\n#ifdef DEBUG\nvoid debug_dump_stack(lua_State* L, char* file, unsigned line)\n{\n  g_print(\"[%-10s:%3u] (stack dump)\\n\", file, line);\n  int len = lua_gettop(L);\n  int i = lua_gettop(L);\n  if ( i > 0 ) {\n    while( i ) {\n      int t = lua_type(L, i);\n      g_print(\"  [%d:%d] \", i, i - len - 1);\n      switch (t) {\n        case LUA_TSTRING:\n          g_print(\"str: %s \", lua_tostring(L, i));\n          break;\n        case LUA_TBOOLEAN:\n          g_print(\"bool: %s \", lua_toboolean(L, i) ? \"true\" : \"false\");\n          break;\n        case LUA_TNUMBER:\n          g_print(\"number: %g \", lua_tonumber(L, i));\n          break;\n        case LUA_TTABLE: {\n          g_print(\"*%s \", lua_typename(L, t));\n          /* g_print(\"table: \"); */\n          /* lua_pushnil(L); */\n          /* while (lua_next(L, -2)) { */\n          /*   lua_pushvalue(L, -2); */\n          /*   g_print(\"[%s] \", lua_tostring(L, -1)); */\n          /*   lua_pop(L, 2); */\n          /* } */\n          /* lua_pop(L, 1); */\n          break;\n        }\n        default:\n          g_print(\"*%s \", lua_typename(L, t));\n          break;\n      }\n      if (len == i) {\n        g_print(\"(top)\");\n      }\n      g_print(\"\\n\");\n      i--;\n    }\n  } else {\n    g_print(\"  stack is empty\\n\");\n  }\n}\n#endif\n\nint roundup(double x)\n{\n  return (int)(x + 0.5);\n}\n\nbool is_equal(const char* a, const char* b)\n{\n  return g_strcmp0(a, b) == 0;\n}\n\nbool is_none(const char* s)\n{\n  return g_strcmp0(s, TYM_SYMBOL_NONE) == 0;\n}\n\nbool is_empty(const char* s)\n{\n  return g_strcmp0(s, \"\") == 0;\n}\n\nvoid luaX_requirec(lua_State* L, const char* modname, lua_CFunction openf, int glb, void* userdata)\n{\n#if USES_LUAJIT\n  luaL_getmetatable(L, LUA_LOADED_TABLE);\n#else\n  luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);\n#endif\n  lua_getfield(L, -1, modname);  /* LOADED[modname] */\n  if (!lua_toboolean(L, -1)) {  /* package not already loaded? */\n    lua_pop(L, 1);  /* remove field */\n    lua_pushlightuserdata(L, userdata);\n    lua_pushcclosure(L, openf, 1);\n    lua_pushstring(L, modname);  /* argument to open function */\n    lua_call(L, 1, 1);  /* call 'openf' to open module */\n    lua_pushvalue(L, -1);  /* make copy of module (call result) */\n    lua_setfield(L, -3, modname);  /* LOADED[modname] = module */\n  }\n  lua_remove(L, -2);  /* remove LOADED table */\n  if (glb) {\n    lua_pushvalue(L, -1);  /* copy of module */\n    lua_setglobal(L, modname);  /* _G[modname] = module */\n  }\n}\n\nint luaX_warn(lua_State* L, const char* fmt, ...)\n{\n  va_list argp;\n  va_start(argp, fmt);\n  luaL_where(L, 1);\n  lua_pushvfstring(L, fmt, argp);\n  va_end(argp);\n  lua_concat(L, 2);\n  g_message(\"%s\", lua_tostring(L,-1));\n  lua_pop(L, 1);\n  return 0;\n}\n"
  },
  {
    "path": "src/config.c",
    "content": "/**\n * config.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"config.h\"\n\n\nConfig* config_init()\n{\n  Config* config = g_new(Config, 1);\n  config->data = g_hash_table_new_full(\n    g_str_hash,\n    g_str_equal,\n    (GDestroyNotify)g_free,\n    (GDestroyNotify)g_free\n  );\n  config->locked = true;\n  return config;\n}\n\nvoid config_close(Config* config)\n{\n  g_hash_table_destroy(config->data);\n  g_free(config);\n}\n\nstatic void* config_get_raw(Config* config, const char* key)\n{\n  void* ptr = g_hash_table_lookup(config->data, key);\n  if (!ptr) {\n    dd(\"tried to refer null field: '%s'\", key);\n  }\n  return ptr;\n}\n\nstatic void config_set_raw(Config* config, const char* key, void* value)\n{\n  if (!value) {\n    dd(\"tried to set null field: '%s'\", key);\n    return;\n  }\n  void* old_key = NULL;\n  bool has_value = g_hash_table_lookup_extended(config->data, key, &old_key, NULL);\n  // warn if: not reseting and attempt to insert value\n  if (config->locked && !has_value) {\n    dd(\"tried to add new field when locked: '%s'\", key);\n    return;\n  }\n  if (old_key) {\n    g_hash_table_remove(config->data, old_key);\n  }\n  g_hash_table_insert(config->data, g_strdup(key), value);\n  return;\n}\n\nvoid config_set_str(Config* config, const char* key, const char* value)\n{\n  config_set_raw(config, key, g_strdup(value));\n}\n\nconst char* config_get_str(Config* config, const char* key)\n{\n  char* v = (char*)config_get_raw(config, key);\n  if (v) {\n    return v;\n  }\n  dd(\"string config of '%s' is null. falling back to \\\"\\\"\", key);\n  return \"\";\n}\n\nint config_get_int(Config* config, const char* key)\n{\n  int* v = (int*)config_get_raw(config, key);\n  if (v) {\n    return *v;\n  }\n  dd(\"int config of '%s' is null. falling back to 0\", key);\n  return 0;\n}\n\nvoid config_set_int(Config* config, const char* key, int value)\n{\n  config_set_raw(config, key, memdup((gpointer)&value, sizeof(int)));\n}\n\nbool config_get_bool(Config* config, const char* key)\n{\n  bool* v = (bool*)config_get_raw(config, key);\n  if (v) {\n    return *v;\n  }\n  dd(\"bool config of '%s' is null. falling back to null\", key);\n  return false;\n}\n\nvoid config_set_bool(Config* config, const char* key, bool value)\n{\n  config_set_raw(config, key, memdup((gpointer)&value, sizeof(bool)));\n}\n\nvoid config_restore_default(Config* config, Meta* meta)\n{\n  df();\n  config->locked = false;\n  g_hash_table_remove_all(config->data);\n\n  for (GList* li = meta->list; li != NULL; li = li->next) {\n    MetaEntry* e = (MetaEntry*)li->data;\n    if (e->getter) {\n      // if getter exists, do not save value in hash\n      continue;\n    }\n    switch (e->type) {\n      case META_ENTRY_TYPE_STRING:\n        config_set_str(config, e->name, e->default_value);\n        break;\n      case META_ENTRY_TYPE_INTEGER:\n        config_set_int(config, e->name, *(int*)(e->default_value));\n        break;\n      case META_ENTRY_TYPE_BOOLEAN:\n        config_set_bool(config, e->name, *(bool*)(e->default_value));\n        break;\n      case META_ENTRY_TYPE_NONE:\n        break;\n    }\n  }\n  config->locked = true;\n}\n"
  },
  {
    "path": "src/config_test.c",
    "content": "/**\n * config_test.c\n *\n * Copyright (c) 2020 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"tym_test.h\"\n#include \"config.h\"\n\nstatic void test_read_and_write()\n{\n  Config* c = config_init();\n\n  c->locked = false;\n  config_set_int(c, \"int\", 123);\n  config_set_str(c, \"str\", \"tym\");\n  config_set_bool(c, \"bool\", true);\n  c->locked = true;\n\n  g_assert_cmpint(config_get_int(c, \"int\"), ==, 123);\n  g_assert_cmpstr(config_get_str(c, \"str\"), ==, \"tym\");\n  g_assert_cmpuint(config_get_bool(c, \"bool\"), ==, true);\n  config_close(c);\n}\n\nstatic void test_locked()\n{\n  Config* c = config_init();\n\n  config_set_int(c, \"int\", 123);\n  config_set_str(c, \"str\", \"tym\");\n  config_set_bool(c, \"bool\", true);\n\n  // can not save values\n  g_assert_cmpint(config_get_int(c, \"int\"), ==, 0);\n  g_assert_cmpstr(config_get_str(c, \"str\"), ==, \"\");\n  g_assert_cmpuint(config_get_bool(c, \"bool\"), ==, false);\n  config_close(c);\n}\n\nvoid test_config()\n{\n  test_read_and_write();\n  test_locked();\n}\n"
  },
  {
    "path": "src/context.c",
    "content": "/**\n * context.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"context.h\"\n#include \"app.h\"\n#include \"builtin.h\"\n#include \"property.h\"\n#include \"command.h\"\n\n\ntypedef void (*TymCommandFunc)(Context* context);\n\ntypedef struct {\n  unsigned key;\n  GdkModifierType mod;\n  TymCommandFunc func;\n} KeyPair;\n\n#define TYM_MODULE_NAME \"tym\"\n#define TYM_DEFAULT_NOTIFICATION_TITLE \"tym\"\n\nstatic KeyPair DEFAULT_KEY_PAIRS[] = {\n  { GDK_KEY_c , GDK_CONTROL_MASK | GDK_SHIFT_MASK, command_copy_selection },\n  { GDK_KEY_v , GDK_CONTROL_MASK | GDK_SHIFT_MASK, command_paste          },\n  { GDK_KEY_r , GDK_CONTROL_MASK | GDK_SHIFT_MASK, command_reload         },\n  {},\n};\n\n\nchar* context_acquire_config_path(Context* context)\n{\n  char* path = option_get_str(context->option, \"use\");\n  if (!path) {\n    return g_build_path(\n      G_DIR_SEPARATOR_S,\n      g_get_user_config_dir(),\n      TYM_CONFIG_DIR_NAME,\n      TYM_CONFIG_FILE_NAME,\n      NULL\n    );\n  }\n\n  if (g_path_is_absolute(path)) {\n    return g_strdup(path);\n  }\n  char* cwd = g_get_current_dir();\n  char* abs_path = g_build_path(G_DIR_SEPARATOR_S, cwd, path, NULL);\n  g_free(cwd);\n  g_free(path);\n  return abs_path;\n}\n\nchar* context_acquire_theme_path(Context* context)\n{\n  char* path = option_get_str(context->option, \"theme\");\n  if (!path) {\n    return g_build_path(\n      G_DIR_SEPARATOR_S,\n      g_get_user_config_dir(),\n      TYM_CONFIG_DIR_NAME,\n      TYM_THEME_FILE_NAME,\n      NULL\n    );\n  }\n  if (g_path_is_absolute(path)) {\n    return g_strdup(path);\n  }\n\n  char* cwd = g_get_current_dir();\n  char* abs_path = g_build_path(G_DIR_SEPARATOR_S, cwd, path, NULL);\n  g_free(cwd);\n  g_free(path);\n  return abs_path;\n}\n\nvoid context_load_lua_context(Context* context)\n{\n  lua_State* L = luaL_newstate();\n  luaL_openlibs(L);\n  luaX_requirec(L, TYM_MODULE_NAME, builtin_register_module, true, context);\n  lua_pop(L, 1);\n  context->lua = L;\n}\n\nContext* context_init(int id, Option* option)\n{\n  dd(\"init context id=%d\", id);\n  Context* context = g_new0(Context, 1);\n  g_assert(id >= 0);\n  context->id = id;\n  context->option = option;\n  context->config_loading = false;\n  context->initialized = false;\n  context->object_path = g_strdup_printf(TYM_OBJECT_PATH_FMT_INT, context->id);\n  context->child_pid = -1;\n  context->config = config_init();\n  context->keymap = keymap_init();\n  context->hook = hook_init();\n  return context;\n}\n\nvoid context_close(Context* context)\n{\n  dd(\"close context id=%d\", context->id);\n  for (GList* li = context->handler_tags; li != NULL; li = li->next) {\n    HandlerTag* tag = (HandlerTag*)li->data;\n    g_signal_handler_disconnect(tag->object, tag->handler_id);\n  }\n  g_free(context->object_path);\n  option_close(context->option); /* dispose here */\n  config_close(context->config);\n  keymap_close(context->keymap);\n  hook_close(context->hook);\n  lua_close(context->lua);\n  g_free(context);\n}\n\nvoid context_add_handler_tag(Context* context, void* object, int handler_id)\n{\n  HandlerTag* tag = g_new0(HandlerTag, 1);\n  tag->handler_id = handler_id;\n  tag->object = object;\n  context->handler_tags = g_list_append(context->handler_tags, tag);\n}\n\nvoid context_load_device(Context* context)\n{\n  GdkDisplay* display = gdk_display_get_default();\n#ifdef TYM_USE_GDK_SEAT\n  GdkSeat* seat = gdk_display_get_default_seat(display);\n  context->device = gdk_seat_get_keyboard(seat);\n#else\n  GdkDeviceManager* manager = gdk_display_get_device_manager(display);\n  GList* devices = gdk_device_manager_list_devices(manager, GDK_DEVICE_TYPE_MASTER);\n  for (GList* li = devices; li != NULL; li = li->next) {\n    GdkDevice* d = (GdkDevice*)li->data;\n    if (gdk_device_get_source(d) == GDK_SOURCE_KEYBOARD) {\n      context->device = d;\n      break;\n    }\n  }\n  g_list_free(devices);\n#endif\n}\n\nvoid context_log_message(Context* context, bool notify, const char* fmt, ...)\n{\n  va_list argp;\n  va_start(argp, fmt);\n  char* base = g_strdup_vprintf(fmt, argp);\n  va_end(argp);\n  char* message = g_strdup_printf(\"[id=%d] %s\", context->id, base);\n  g_message(\"%s\", message);\n  if (notify) {\n    context_notify(context, base, NULL);\n  }\n  g_free(base);\n  g_free(message);\n}\n\nvoid context_log_warn(Context* context, bool notify, const char* fmt, ...)\n{\n  va_list argp;\n  va_start(argp, fmt);\n  char* base = g_strdup_vprintf(fmt, argp);\n  va_end(argp);\n  char* message = g_strdup_printf(\"[id=%d] %s\", context->id, base);\n  g_warning(\"%s\", message);\n  if (notify) {\n    context_notify(context, base, NULL);\n  }\n  g_free(base);\n  g_free(message);\n}\n\nvoid context_restore_default(Context* context)\n{\n  config_restore_default(context->config, app->meta);\n  for (GList* li = app->meta->list; li != NULL; li = li->next) {\n    MetaEntry* e = (MetaEntry*)li->data;\n    char* target = NULL;\n    if (strncmp(\"color_\", e->name, 6) == 0) {\n      g_ascii_strtoull(&e->name[6], &target, 10);\n      if (&e->name[6] != target) {\n        // skip loading `color_%d` in this loop\n        continue;\n      }\n    }\n    char* key = e->name;\n    switch (e->type) {\n      case META_ENTRY_TYPE_STRING: {\n        context_set_str(context, key, e->default_value);\n        break;\n      }\n      case META_ENTRY_TYPE_INTEGER: {\n        context_set_int(context, key, *(int*)e->default_value);\n        break;\n      }\n      case META_ENTRY_TYPE_BOOLEAN: {\n        context_set_bool(context, key, *(bool*)e->default_value);\n        break;\n      }\n      case META_ENTRY_TYPE_NONE:\n        break;\n    }\n  }\n  // set colors here\n  GdkRGBA* palette = g_new0(GdkRGBA, 16);\n  unsigned i = 0;\n  while (i < 16) {\n    char s[10] = {};\n    g_snprintf(s, 10, \"color_%d\", i);\n    MetaEntry* e = meta_get_entry(app->meta, s);\n    assert(gdk_rgba_parse(&palette[i], e->default_value));\n    i += 1;\n  }\n  vte_terminal_set_colors(context->layout.vte, NULL, NULL, palette, 16);\n}\n\nvoid context_override_by_option(Context* context)\n{\n  for (GList* li = app->meta->list; li != NULL; li = li->next) {\n    MetaEntry* e = (MetaEntry*)li->data;\n    char* key = e->name;\n    switch (e->type) {\n      case META_ENTRY_TYPE_STRING: {\n        char* v = option_get_str(context->option, key);\n        if (v) {\n          context_set_str(context, key, v);\n        }\n        break;\n      }\n      case META_ENTRY_TYPE_INTEGER: {\n        int v = option_get_int(context->option, key);\n        if (v) {\n          context_set_int(context, key, v);\n        }\n        break;\n      }\n      case META_ENTRY_TYPE_BOOLEAN: {\n        bool v = option_get_bool(context->option, key);\n        if (v) {\n          context_set_bool(context, key, v);\n        }\n        break;\n      }\n      case META_ENTRY_TYPE_NONE:\n        break;\n    }\n  }\n}\n\nvoid context_load_config(Context* context)\n{\n  df();\n\n  if (context->config_loading) {\n    context_log_message(context, true, \"Tried to load config recursively. Ignoring loading.\");\n    return;\n  }\n\n  context->config_loading = true;\n\n  char* config_path = context_acquire_config_path(context);\n  dd(\"config path: `%s`\", config_path);\n  if (!config_path) {\n    context_log_message(context, false, \"Tried to load config recursively. Ignoring loading.\");\n    goto EXIT;\n  }\n\n  if (!g_file_test(config_path, G_FILE_TEST_EXISTS)) {\n    context_log_message(context, false, \"Config file `%s` doesn't exist.\", config_path);\n    goto EXIT;\n  }\n\n  lua_State* L = context->lua;\n  int result = luaL_dofile(L, config_path);\n  if (result != LUA_OK) {\n    const char* error = lua_tostring(L, -1);\n    lua_pop(L, 1);\n    context_log_warn(context, true, error);\n    goto EXIT;\n  }\n\n  context_log_message(context, false, \"Config file `%s` loaded.\", config_path);\n\nEXIT:\n  context->config_loading = false;\n  if (config_path) {\n    g_free(config_path);\n  }\n  dd(\"load config end\");\n}\n\nvoid context_load_theme(Context* context)\n{\n  df();\n  if (!context->lua) {\n    context_log_message(context, false, \"Skipped loading theme because Lua context is not loaded.\");\n    return;\n  }\n\n  char* theme_path = context_acquire_theme_path(context);\n  dd(\"theme path: `%s`\", theme_path);\n  if (!theme_path) {\n    context_log_message(context, false, \"Skipped theme loading.\");\n    goto EXIT;\n  }\n\n  if (!g_file_test(theme_path, G_FILE_TEST_EXISTS)) {\n    context_log_message(context, false, \"Theme file `%s` doesn't exist.\", theme_path);\n    goto EXIT;\n  }\n\n  lua_State* L = context->lua;\n  int result = luaL_dofile(L, theme_path);\n  if (result != LUA_OK) {\n    const char* error = lua_tostring(L, -1);\n    context_log_warn(context, true, error);\n    goto EXIT;\n  }\n\n  context_log_message(context, false, \"Theme file `%s` loaded.\", theme_path);\n\n  if (!lua_istable(L, -1)) {\n    context_log_warn(\n        context,\n        \"Theme script(%s) must return a table (got %s). Skiped theme assignment.\",\n        theme_path, lua_typename(L, lua_type(L, -1)));\n    goto EXIT;\n  }\n\n  for (GList* li = app->meta->list; li != NULL; li = li->next) {\n    MetaEntry* e = (MetaEntry*)li->data;\n    if (!e->is_theme) {\n      continue;\n    }\n    lua_getfield(L, -1, e->name);\n    if (!lua_isnil(L, -1)) {\n      const char* value = lua_tostring(L, -1);\n      context_set_str(context, e->name, value);\n    }\n    lua_pop(L, 1);\n  }\n  lua_pop(L, 1);\n\nEXIT:\n  if (theme_path) {\n    g_free(theme_path);\n  }\n  dd(\"load theme end\");\n}\n\nstatic bool context_perform_default(Context* context, unsigned key, GdkModifierType mod)\n{\n  unsigned i = 0;\n  while (DEFAULT_KEY_PAIRS[i].func) {\n    KeyPair* pair = &DEFAULT_KEY_PAIRS[i];\n    if ((key == pair->key) && !(~mod & pair->mod)) {\n      pair->func(context);\n      return true;\n    }\n    i++;\n  }\n  return false;\n}\n\nbool context_perform_keymap(Context* context, unsigned key, GdkModifierType mod)\n{\n  if (context->lua) {\n    bool result = false;\n    char* error = NULL;\n    if (keymap_perform(context->keymap, context->lua, key, mod, &result, &error)) {\n      // if the keymap func is normally excuted,  default action will be canceled.\n      // if `return true` in the keymap func, default action will be performed.\n      if (!result) {\n        return true;\n      }\n    } else {\n      if (error) {\n        context_log_warn(context, true, error);\n        g_free(error);\n        // if the keymap func has error, default action will be canceled.\n        return true;\n      }\n    }\n  }\n  if (context_get_bool(context, \"ignore_default_keymap\")) {\n    return false;\n  }\n  return context_perform_default(context, key, mod);\n}\n\nvoid context_handle_signal(Context* context, const char* signal_name, GVariant* parameters)\n{\n  dd(\"receive signal: %s\", signal_name);\n}\n\nvoid context_build_layout(Context* context)\n{\n  GtkWindow* window = context->layout.window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(app->gapp)));\n  VteTerminal* vte = context->layout.vte = VTE_TERMINAL(vte_terminal_new());\n  GtkBox* hbox = context->layout.hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));\n  GtkBox* vbox = context->layout.vbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));\n  context->layout.uri_tag = -1;\n\n  gtk_container_add(GTK_CONTAINER(hbox), GTK_WIDGET(vte));\n  gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(hbox));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(vbox));\n  gtk_container_set_border_width(GTK_CONTAINER(window), 0);\n\n  GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(window));\n  GdkVisual* visual = gdk_screen_get_rgba_visual(screen);\n  context->layout.alpha_supported = visual;\n  if (!context->layout.alpha_supported) {\n    context_log_message(context, false, \"Your screen doesn't support alpha channel.\");\n    visual = gdk_screen_get_system_visual(screen);\n  }\n  gtk_widget_set_visual(GTK_WIDGET(window), visual);\n#ifdef TYM_USE_SIXEL\n  vte_terminal_set_enable_sixel(vte, true);\n#endif\n}\n\nvoid context_notify(Context* context, const char* body, const char* title)\n{\n  char* default_title = NULL;\n  if (!title) {\n    title = default_title = g_strdup_printf(\"tym[id=%d]\", context->id);\n  }\n  GNotification* notification = g_notification_new(title);\n  if (default_title) {\n    g_free(default_title);\n  }\n  GIcon* icon = g_themed_icon_new_with_default_fallbacks(context_get_str(context, \"icon\"));\n\n  g_notification_set_icon(notification, G_ICON(icon));\n  g_notification_set_body(notification, body);\n  g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_HIGH);\n  g_application_send_notification(app->gapp, TYM_APP_ID, notification);\n\n  g_object_unref(notification);\n  g_object_unref(icon);\n}\n\nvoid context_launch_uri(Context* context, const char* uri)\n{\n  dd(\"launch: `%s`\", uri);\n  GError* error = NULL;\n  GdkDisplay* display = gdk_display_get_default();\n  GdkAppLaunchContext* ctx = gdk_display_get_app_launch_context(display);\n  gdk_app_launch_context_set_screen(ctx, gdk_screen_get_default());\n  /* gdk_app_launch_context_set_timestamp(ctx, event->time); */\n  if (!g_app_info_launch_default_for_uri(uri, G_APP_LAUNCH_CONTEXT(ctx), &error)) {\n    context_log_warn(context, \"Failed to launch uri: %s\", error->message);\n    g_error_free(error);\n  }\n}\n\nGdkWindow* context_get_gdk_window(Context* context)\n{\n  return gtk_widget_get_window(GTK_WIDGET(context->layout.window));\n}\n\nconst char* context_get_str(Context* context, const char* key)\n{\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (e->getter) {\n    return ((PropertyStrGetter)e->getter)(context, key);\n  }\n  return config_get_str(context->config, key);\n}\n\nint context_get_int(Context* context, const char* key)\n{\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (e->getter) {\n    return ((PropertyIntGetter)e->getter)(context, key);\n  }\n  return config_get_int(context->config, key);\n}\n\nbool context_get_bool(Context* context, const char* key)\n{\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (e->getter) {\n    return ((PropertyBoolGetter)e->getter)(context, key);\n  }\n  return config_get_bool(context->config, key);\n}\n\nvoid context_set_str(Context* context, const char* key, const char* value)\n{\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (e->setter) {\n    ((PropertyStrSetter)e->setter)(context, key, value);\n    return;\n  }\n  if (!e->getter) {\n    config_set_str(context->config, key, value);\n    return;\n  }\n  dd(\"`%s`: setter is not provided but getter is provided\", key);\n}\n\nvoid context_set_int(Context* context, const char* key, int value)\n{\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (e->setter) {\n    ((PropertyIntSetter)e->setter)(context, key, value);\n    return;\n  }\n  if (!e->getter) {\n    config_set_int(context->config, key, value);\n    return;\n  }\n  dd(\"`%s`: setter is not provided but getter is provided\", key);\n}\n\nvoid context_set_bool(Context* context, const char* key, bool value)\n{\n  MetaEntry* e = meta_get_entry(app->meta, key);\n  if (e->setter) {\n    ((PropertyBoolSetter)e->setter)(context, key, value);\n    return;\n  }\n  if (!e->getter) {\n    config_set_bool(context->config, key, value);\n    return;\n  }\n  dd(\"`%s`: setter is not provided but getter is provided\", key);\n}\n\nvoid context_resize(Context* context, int width, int height)\n{\n  GtkWindow* window = context->layout.window;\n  VteTerminal* vte = context->layout.vte;\n  bool visible = gtk_widget_is_visible(GTK_WIDGET(window));\n  if (visible) {\n    GtkBorder border;\n    gtk_style_context_get_padding(\n      gtk_widget_get_style_context(GTK_WIDGET(vte)),\n      gtk_widget_get_state_flags(GTK_WIDGET(vte)),\n      &border\n    );\n    const int char_width = vte_terminal_get_char_width(vte);\n    const int char_height = vte_terminal_get_char_height(vte);\n    const int hpad = context_get_int(context, \"padding_horizontal\");\n    const int vpad = context_get_int(context, \"padding_vertical\");\n    gtk_window_resize(\n      context->layout.window,\n      width * char_width + border.left + border.right + hpad * 2,\n      height * char_height + border.top + border.bottom + vpad * 2\n    );\n  } else {\n    vte_terminal_set_size(vte, width, height);\n  }\n}\n"
  },
  {
    "path": "src/hook.c",
    "content": "/**\n * hook.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"hook.h\"\n\n\n#define HOOK_KEY_TITLE \"title\"\n#define HOOK_KEY_BELL \"bell\"\n#define HOOK_KEY_CLICKED \"clicked\"\n#define HOOK_KEY_SCROLL \"scroll\"\n#define HOOK_KEY_DRAG \"drag\"\n#define HOOK_KEY_ACTIVATED \"activated\"\n#define HOOK_KEY_DEACTIVATED \"deactivated\"\n#define HOOK_KEY_SELECTED \"selected\"\n#define HOOK_KEY_UNSELECTED \"unselected\"\n#define HOOK_KEY_RESIZED \"resized\"\n#define HOOK_KEY_SIGNAL \"signal\"\n\n\nconst char* HOOK_KEYS[] = {\n  HOOK_KEY_TITLE,\n  HOOK_KEY_BELL,\n  HOOK_KEY_CLICKED,\n  HOOK_KEY_SCROLL,\n  HOOK_KEY_DRAG,\n  HOOK_KEY_ACTIVATED,\n  HOOK_KEY_DEACTIVATED,\n  HOOK_KEY_SELECTED,\n  HOOK_KEY_UNSELECTED,\n  HOOK_KEY_RESIZED,\n  HOOK_KEY_SIGNAL,\n  NULL\n};\n\nHook* hook_init()\n{\n  Hook* hook = g_new0(Hook, 1);\n  hook->refs = g_hash_table_new_full(\n    g_str_hash,\n    g_str_equal,\n    (GDestroyNotify)g_free,\n    (GDestroyNotify)g_free\n  );\n\n  int i = 0;\n  while (HOOK_KEYS[i]) {\n    int* p = g_new0(int, 1);\n    *p = -1;\n    g_hash_table_insert(hook->refs, g_strdup(HOOK_KEYS[i]), p);\n    i += 1;\n  }\n  return hook;\n}\n\nvoid hook_close(Hook* hook)\n{\n  g_hash_table_destroy(hook->refs);\n  g_free(hook);\n}\n\nstatic int hook_get_ref(Hook* hook, const char* key)\n{\n  int* ptr = g_hash_table_lookup(hook->refs, key);\n  if (!ptr) {\n    dd(\"invalid hook key: '%s'\", key);\n    return -1;\n  }\n  return *ptr;\n}\n\nbool hook_set_ref(Hook* hook, const char* key, int ref, int* old_ref)\n{\n  assert(old_ref);\n  void* old_key = NULL;\n  void* old_value = NULL;\n  bool has_value = g_hash_table_lookup_extended(hook->refs, key, &old_key, &old_value);\n  if (old_value) {\n    *old_ref = *(int*)old_value;\n  }\n  if (!has_value) {\n    return false;\n  }\n  g_hash_table_remove(hook->refs, old_key);\n  g_hash_table_insert(hook->refs, g_strdup(key), memdup(&ref, sizeof(int)));\n  dd(\"hook '%s' is registered. ref: %d\", key, ref);\n  return true;\n}\n\nstatic bool hook_perform(Hook* hook, lua_State* L, const char* key, int narg, int nresult)\n{\n  int ref = hook_get_ref(hook, key);\n  if (ref < 0) {\n    lua_pop(L, narg);\n    return false;\n  }\n  lua_rawgeti(L, LUA_REGISTRYINDEX, ref);\n  if (!lua_isfunction(L, -1)) {\n    lua_pop(L, 1); // pop none-function\n    dd(\"tried to call hook which is not function.\");\n    return false;\n  }\n  lua_insert(L, - narg - 1);\n  dd(\"perform custom hook: %s\", key);\n  if (lua_pcall(L, narg, nresult, 0) != LUA_OK) {\n    luaX_warn(L, \"Error in hook function: '%s'\", lua_tostring(L, -1));\n    lua_pop(L, 1); // error\n    return false;\n  }\n  return true;\n}\n\nbool hook_perform_title(Hook* hook, lua_State* L, const char* title, bool* result)\n{\n  if (!L) {\n    return false;\n  }\n  lua_pushstring(L, title);\n  bool succeeded = hook_perform(hook, L, HOOK_KEY_TITLE, 1, 1);\n  if (!succeeded) {\n    return false;\n  }\n  *result = lua_toboolean(L, -1);\n  lua_pop(L, 1);\n  return succeeded;\n}\n\nbool hook_perform_bell(Hook* hook, lua_State* L, bool* result)\n{\n  assert(result);\n  if (!L) {\n    return false;\n  }\n  bool succeeded = hook_perform(hook, L, HOOK_KEY_BELL, 0, 1);\n  if (!succeeded) {\n    return false;\n  }\n  *result = lua_toboolean(L, -1);\n  lua_pop(L, 1);\n  return succeeded;\n}\n\nbool hook_perform_clicked(Hook* hook, lua_State* L, int button, const char* uri, bool* result)\n{\n  assert(result);\n  if (!L) {\n    return false;\n  }\n  lua_pushinteger(L, button);\n  lua_pushstring(L, uri);\n  bool succeeded = hook_perform(hook, L, HOOK_KEY_CLICKED, 2, 1);\n  if (!succeeded) {\n    return false;\n  }\n  *result = lua_toboolean(L, -1);\n  lua_pop(L, 1);\n  return succeeded;\n}\n\nbool hook_perform_scroll(Hook* hook, lua_State* L, double delta_x, double delta_y, double x, double y, bool* result)\n{\n  assert(result);\n  if (!L) {\n    return false;\n  }\n  lua_pushnumber(L, delta_x);\n  lua_pushnumber(L, delta_y);\n  lua_pushnumber(L, x);\n  lua_pushnumber(L, x);\n  bool succeeded = hook_perform(hook, L, HOOK_KEY_SCROLL, 4, 1);\n  if (!succeeded) {\n    return false;\n  }\n  *result = lua_toboolean(L, -1);\n  lua_pop(L, 1);\n  return succeeded;\n}\n\nbool hook_perform_drag(Hook* hook, lua_State* L, char* path, bool* result)\n{\n  assert(result);\n  lua_pushstring(L, path);\n  bool succeeded = hook_perform(hook, L, HOOK_KEY_DRAG, 1, 1);\n  if (!succeeded) {\n    return false;\n  }\n  *result = lua_toboolean(L, -1);\n  lua_pop(L, 1);\n  return succeeded;\n}\n\nbool hook_perform_activated(Hook* hook, lua_State* L)\n{\n  if (!L) {\n    return false;\n  }\n  return hook_perform(hook, L, HOOK_KEY_ACTIVATED, 0, 0);\n}\n\nbool hook_perform_deactivated(Hook* hook, lua_State* L)\n{\n  if (!L) {\n    return false;\n  }\n  return hook_perform(hook, L, HOOK_KEY_DEACTIVATED, 0, 0);\n}\n\nbool hook_perform_selected(Hook* hook, lua_State* L, const char* text)\n{\n  if (!L) {\n    return false;\n  }\n  lua_pushstring(L, text);\n  return hook_perform(hook, L, HOOK_KEY_SELECTED, 1, 0);\n}\n\nbool hook_perform_unselected(Hook* hook, lua_State* L)\n{\n  if (!L) {\n    return false;\n  }\n  return hook_perform(hook, L, HOOK_KEY_UNSELECTED, 0, 0);\n}\n\nbool hook_perform_resized(Hook* hook, lua_State* L)\n{\n  if (!L) {\n    return false;\n  }\n  return hook_perform(hook, L, HOOK_KEY_RESIZED, 0, 0);\n}\n\nbool hook_perform_signal(Hook* hook, lua_State* L, const char* param)\n{\n  if (!L) {\n    return false;\n  }\n  lua_pushstring(L, param);\n  return hook_perform(hook, L, HOOK_KEY_SIGNAL, 1, 0);\n}\n"
  },
  {
    "path": "src/ipc.c",
    "content": "/**\n * ipc.c\n *\n * Copyright (c) 2022 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n\n#include \"ipc.h\"\n#include \"app.h\"\n\n\ntypedef void (*TymSignalHandler)(Context*, GVariant*);\ntypedef void (*TymMethodHandler)(Context*, GVariant*, GDBusMethodInvocation*);\n\ntypedef struct {\n  const char* signal_name;\n  TymSignalHandler func;\n} SignalDef;\n\ntypedef struct {\n  const char* method_name;\n  TymMethodHandler func;\n} MethodDef;\n\n\nvoid ipc_signal_hook(Context* context, GVariant* params)\n{\n  df();\n  const char* param = NULL;\n  size_t size = g_variant_n_children(params);\n  if (size > 0) {\n    g_variant_get_child(params, 0, \"s\", &param);\n  }\n  hook_perform_signal(context->hook, context->lua, param);\n}\n\nSignalDef signals[] = {\n  { \"hook\", ipc_signal_hook, },\n  { NULL, NULL, }\n};\n\nvoid ipc_method_get_ids(Context* context, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY);\n  for (GList* li = app->contexts; li != NULL; li = li->next) {\n    Context* c = (Context*)li->data;\n    g_variant_builder_add(builder, \"i\", c->id);\n  }\n  GVariant* v = g_variant_builder_end(builder);\n  v = g_variant_new_tuple(&v, 1);\n  g_dbus_method_invocation_return_value(invocation, v);\n}\n\nvoid ipc_method_echo(Context* context, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  g_dbus_method_invocation_return_value(invocation, params);\n}\n\nstatic void ipc_do_lua(Context* context, GVariant* params, GDBusMethodInvocation* invocation, bool needs_result, bool is_file)\n{\n  lua_State* L = context->lua;\n  char* param = NULL;\n  g_variant_get_child(params, 0, \"s\", &param);\n\n  char* result = NULL;\n\n  int suc = is_file ? luaL_dofile(L, param) : luaL_dostring(L, param);\n\n  if (suc != 0) {\n    result = g_strdup(lua_tostring(L, -1));\n    context_log_warn(context, true, result);\n    lua_settop(L, 0);\n  } else {\n    if (needs_result) {\n      int top = lua_gettop(L);\n      if (top > 0) {\n        result = g_strdup(lua_tostring(L, -1));\n      } else {\n        result = g_strdup_printf(\"Lua stack is empty. `return` is needed.\");\n        context_log_warn(context, true, result);\n      }\n      lua_settop(L, 0);\n    }\n  }\n\n  GVariant* v = needs_result ? g_variant_new(\"(s)\", result) : g_variant_new(\"()\");\n  g_free(result);\n  g_dbus_method_invocation_return_value(invocation, v);\n}\n\nvoid ipc_method_eval(Context* context, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  ipc_do_lua(context, params, invocation, true, false);\n}\n\nvoid ipc_method_eval_file(Context* context, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  ipc_do_lua(context, params, invocation, true, true);\n}\n\nvoid ipc_method_exec(Context* context, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  ipc_do_lua(context, params, invocation, false, false);\n}\n\nvoid ipc_method_exec_file(Context* context, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  ipc_do_lua(context, params, invocation, false, true);\n}\n\nMethodDef methods[] = {\n  { \"echo\",      ipc_method_echo, },\n  { \"get_ids\",   ipc_method_get_ids, },\n  { \"eval\",      ipc_method_eval, },\n  { \"eval_file\", ipc_method_eval_file, },\n  { \"exec\",      ipc_method_exec, },\n  { \"exec_file\", ipc_method_exec_file, },\n  { NULL, NULL, }\n};\n\nIPC* ipc_init()\n{\n  IPC* ipc = g_new0(IPC, 1);\n  ipc->signals = g_hash_table_new_full(\n    g_str_hash,\n    g_str_equal,\n    NULL,\n    NULL\n  );\n\n  ipc->methods = g_hash_table_new_full(\n    g_str_hash,\n    g_str_equal,\n    NULL,\n    NULL\n  );\n\n  int i = 0;\n  while (signals[i].signal_name) {\n    SignalDef* d = &signals[i];\n    g_hash_table_insert(ipc->signals, (void*)d->signal_name, d);\n    i += 1;\n  }\n\n  i = 0;\n  while (methods[i].method_name) {\n    MethodDef* d = &methods[i];\n    g_hash_table_insert(ipc->methods, (void*)d->method_name, d);\n    i += 1;\n  }\n  return ipc;\n}\n\nvoid ipc_close(IPC* ipc)\n{\n  g_hash_table_destroy(ipc->signals);\n  g_hash_table_destroy(ipc->methods);\n  g_free(ipc);\n}\n\nbool ipc_signal_perform(IPC* ipc, Context* context, const char* signal_name, GVariant* params)\n{\n  SignalDef* d = g_hash_table_lookup(ipc->signals, signal_name);\n  if (!d) {\n    dd(\"invalid signal_name: '%s'\", signal_name);\n    return false;\n  }\n  d->func(context, params);\n  return true;\n}\n\nbool ipc_method_perform(IPC* ipc, Context* context, const char* method_name, GVariant* params, GDBusMethodInvocation* invocation)\n{\n  MethodDef* d = g_hash_table_lookup(ipc->methods, method_name);\n  if (!d) {\n    dd(\"invalid method_name: '%s'\", method_name);\n    return false;\n  }\n  d->func(context, params, invocation);\n  return true;\n}\n"
  },
  {
    "path": "src/keymap.c",
    "content": "/**\n * keymap.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"keymap.h\"\n\n\ntypedef struct {\n  unsigned key;\n  GdkModifierType mod;\n  char* accelerator;\n  int ref;\n} KeymapEntry;\n\n\nstatic void free_keymap_entry(KeymapEntry* e, void* user_data)\n{\n  // TODO: luaL_unref the ref\n  g_free(e->accelerator);\n  g_free(e);\n}\n\nKeymap* keymap_init()\n{\n  Keymap* keymap = g_new0(Keymap, 1);\n  keymap->entries = NULL;\n\n  return keymap;\n}\n\nvoid keymap_reset(Keymap* keymap)\n{\n  g_list_foreach(keymap->entries, (GFunc)free_keymap_entry, NULL);\n  g_list_free(keymap->entries);\n  keymap->entries = NULL;\n}\n\nvoid keymap_close(Keymap* keymap)\n{\n  keymap_reset(keymap);\n  g_free(keymap);\n}\n\nbool keymap_add_entry(Keymap* keymap, const char* accelerator, int ref)\n{\n  unsigned key;\n  GdkModifierType mod;\n  gtk_accelerator_parse(accelerator, &key, &mod);\n  if (0 == key && 0 == mod) {\n    return false;\n  }\n  bool removed = keymap_remove_entry(keymap, accelerator);\n  KeymapEntry* e = g_new(KeymapEntry, 1);\n  e->key = key;\n  e->mod = mod;\n  e->accelerator = g_strdup(accelerator);\n  e->ref = ref;\n  keymap->entries = g_list_append(keymap->entries, e);\n  if (removed) {\n    dd(\"keymap (%s mod: %x, key: %x) has been overwritten\", accelerator, mod, key);\n  } else {\n    dd(\"keymap (%s mod: %x, key: %x) has been newly assined\", accelerator, mod, key);\n  }\n  return true;\n}\n\nbool keymap_remove_entry(Keymap* keymap, const char* accelerator)\n{\n  for (GList* li = keymap->entries; li != NULL; li = li->next) {\n    KeymapEntry* e = (KeymapEntry*)li->data;\n    if (is_equal(e->accelerator, accelerator)) {\n      keymap->entries = g_list_remove(keymap->entries, e);\n      g_free(e);\n      return true;\n    }\n  }\n  return false;\n}\n\nbool keymap_perform(Keymap* keymap, lua_State* L, unsigned key, GdkModifierType mod, bool* result, char** error)\n{\n  assert(result);\n  assert(error);\n  for (GList* li = keymap->entries; li != NULL; li = li->next) {\n    KeymapEntry* e = (KeymapEntry*)li->data;\n    if (key == e->key && mod == e->mod) {\n      dd(\"performing keymap: %s (mod: %x, key: %x)\", e->accelerator, mod, key);\n      lua_rawgeti(L, LUA_REGISTRYINDEX, e->ref);\n      if (!lua_isfunction(L, -1)) {\n        lua_pop(L, 1); // pop none-function\n        dd(\"tried to call keymap [%s] which is not function.\", e->accelerator);\n        return false;\n      }\n      if (lua_pcall(L, 0, 1, 0) != LUA_OK) {\n        *error = g_strdup(lua_tostring(L, -1));\n        lua_pop(L, 1); // error\n        return false;\n      }\n      *result = lua_toboolean(L, -1);\n      lua_pop(L, 1);\n      return true;\n    }\n  }\n  return false;\n}\n"
  },
  {
    "path": "src/meta.c",
    "content": "/**\n * meta.c\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"meta.h\"\n#include \"property.h\"\n\n\nstatic char* get_default_shell()\n{\n  const char* shell_env = g_getenv(\"SHELL\");\n  if (shell_env) {\n    return g_strdup(shell_env);\n  }\n  char* user_shell = vte_get_user_shell();\n  if (user_shell) {\n    return user_shell;\n  }\n  return g_strdup(TYM_FALL_BACK_SHELL);\n}\n\nstatic void free_entry(void* data)\n{\n  g_free(((MetaEntry*)data)->default_value);\n  g_free(data);\n}\n\nint entries_sort_func(const void* a, const void* b)\n{\n  return ((MetaEntry*)a)->index - ((MetaEntry*)b)->index;\n}\n\nMeta* meta_init()\n{\n  Meta* meta = g_new0(Meta, 1);\n#define\tCB(f) ((MetaCallback) (f))\n#define color_special(key, default_color) \\\n  { \\\n    .name=(\"color_\"#key ), .default_value=sdup(default_color), .arg_desc=\"\", .desc=(\"value of color_\"#key), \\\n     .is_theme=true, .setter=CB(setter_color_##key) \\\n  }\n#define color_normal(i) \\\n  { \\\n    .name=(\"color_\"#i ), .default_value=sdup(TYM_DEFAULT_COLOR_##i), .arg_desc=\"\", .desc=(\"value of color_\"#i), \\\n    .option_flag=G_OPTION_FLAG_HIDDEN, .is_theme=true, .setter=CB(setter_color_normal) \\\n  }\n  const MetaEntryType T_INT = META_ENTRY_TYPE_INTEGER;\n  const MetaEntryType T_BOOL = META_ENTRY_TYPE_BOOLEAN;\n  const MetaEntryType T_NONE = META_ENTRY_TYPE_NONE;\n\n  char* (*sdup)(const char*) = g_strdup;\n  const bool v_false = false;\n  const int v_zero = 0;\n\n  MetaEntry ee[] = {\n    // STR\n    {\n      .name=\"shell\",  .short_name='e', .default_value=get_default_shell(), .arg_desc=\"<shell>\",\n      .desc=\"Shell to use in the terminal\",\n      .setter=CB(setter_shell)\n    },\n    {\n      .name=\"term\", .default_value=sdup(TYM_DEFAULT_TERM), .arg_desc=\"\", .desc=\"Value to override $TERM\",\n      .setter=CB(setter_term)\n    },\n    {\n      .name=\"title\", .default_value=sdup(TYM_DEFAULT_TITLE), .arg_desc=\"\", .desc=\"Window title\",\n      .getter=CB(getter_title), .setter=CB(setter_title)\n    },\n    {\n      .name=\"font\", .default_value=sdup(\"\"), .arg_desc=\"\", .desc=\"Font to render(e.g. 'Ubuntu Mono 12')\",\n      .setter=CB(setter_font)\n    },\n    {\n      .name=\"icon\", .default_value=sdup(TYM_DEFAULT_ICON), .arg_desc=\"\", .desc=\"Name of window icon\",\n      .setter=CB(setter_icon)\n    },\n    {\n      .name=\"role\", .default_value=sdup(\"\"), .arg_desc=\"\",\n      .desc=\"Unique identifier for the window\",\n      .getter=CB(getter_role), .setter=CB(setter_role),\n    },\n    {\n      .name=\"cursor_shape\", .default_value=sdup(TYM_DEFAULT_CURSOR_SHAPE), .arg_desc=\"\",\n      .desc=\"'\" TYM_CURSOR_SHAPE_BLOCK \"', '\" TYM_CURSOR_SHAPE_IBEAM \"' or '\" TYM_CURSOR_SHAPE_UNDERLINE \"'\",\n      .getter=CB(getter_cursor_shape), .setter=CB(setter_cursor_shape),\n    },\n    {\n      .name=\"cursor_blink_mode\", .default_value=sdup(TYM_DEFAULT_CURSOR_BLINK_MODE), .arg_desc=\"\",\n      .desc=\"'\" TYM_CURSOR_BLINK_MODE_SYSTEM \"', '\" TYM_CURSOR_BLINK_MODE_ON \"' or '\" TYM_CURSOR_BLINK_MODE_OFF \"'\",\n      .getter=CB(getter_cursor_blink_mode), .setter=CB(setter_cursor_blink_mode),\n    },\n    {\n      .name=\"cjk_width\", .arg_desc=\"\", .default_value=sdup(TYM_DEFAULT_CJK),\n      .desc=\"'\" TYM_CJK_WIDTH_NARROW \"' or '\" TYM_CJK_WIDTH_WIDE \"'\",\n      .getter=CB(getter_cjk_width), .setter=CB(setter_cjk_width),\n    },\n    {\n      .name=\"background_image\", .arg_desc=\"\", .default_value=sdup(\"\"),\n      .desc=\"path to background image\",\n      .setter=CB(setter_background_image),\n    },\n    {\n      .name=\"uri_schemes\", .arg_desc=\"\", .default_value=sdup(TYM_DEFAULT_URI_SCHEMES),\n      .desc=\"URI schemes to be highlighted and clickable\",\n      .setter=CB(setter_uri_schemes),\n    },\n    // INT\n    {\n      .name=\"width\", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_WIDTH, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Initial columns\",\n      .getter=CB(getter_width), .setter=CB(setter_width)\n    },\n    {\n      .name=\"height\", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_HEIGHT, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Initial rows\",\n      .getter=CB(getter_height), .setter=CB(setter_height)\n    },\n    {\n      .name=\"scale\", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_SCALE, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Font scale in percent\",\n      .getter=CB(getter_scale), .setter=CB(setter_scale)\n    },\n    {\n      .name=\"cell_width\", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_CELL_SIZE, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Initial columns\",\n      .getter=CB(getter_cell_width), .setter=CB(setter_cell_width)\n    },\n    {\n      .name=\"cell_height\", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_CELL_SIZE, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Initial rows\",\n      .getter=CB(getter_cell_height), .setter=CB(setter_cell_height)\n    },\n\n    /* DEPRECATED START */\n    {\n      .name=\"padding_horizontal\", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Horizontal padding\",\n      .setter=CB(setter_padding_horizontal)\n    },\n    {\n      .name=\"padding_vertical\", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Vertical padding\",\n      .setter=CB(setter_padding_vertical)\n    },\n    /* DEPRECATED END */\n\n    {\n      .name=\"padding_top\", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Top padding\",\n      .getter=CB(getter_padding_top), .setter=CB(setter_padding_top)\n    },\n    {\n      .name=\"padding_bottom\", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Bottom padding\",\n      .getter=CB(getter_padding_bottom), .setter=CB(setter_padding_bottom)\n    },\n    {\n      .name=\"padding_left\", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Left padding\",\n      .getter=CB(getter_padding_left), .setter=CB(setter_padding_left)\n    },\n    {\n      .name=\"padding_right\", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Right padding\",\n      .getter=CB(getter_padding_right), .setter=CB(setter_padding_right)\n    },\n\n    {\n      .name=\"scrollback_length\", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_SCROLLBACK, sizeof(int)),\n      .arg_desc=\"<int>\", .desc=\"Scrollback buffer length\",\n      .getter=CB(getter_scrollback_length), .setter=CB(setter_scrollback_length)\n    },\n    // BOOL\n    {\n      .name=\"scroll_on_output\", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)),\n      .desc=\"Scroll down on output\",\n      .getter=CB(getter_scroll_on_output), .setter=CB(setter_scroll_on_output)\n    },\n    {\n      .name=\"ignore_default_keymap\", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)),\n      .desc=\"Whether to use default keymap\",\n    },\n    {\n      .name=\"autohide\", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)),\n      .desc=\"Whether to hide mouse cursor when key is pressed\",\n      .getter=CB(getter_autohide), .setter=CB(setter_autohide)\n    },\n    {\n      .name=\"silent\", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)),\n      .desc=\"Whether to beep when bell sequence is sent\",\n      .getter=CB(getter_silent), .setter=CB(setter_silent),\n    },\n    {\n      .name=\"bold_is_bright\", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)),\n      .desc=\"Whether to make bold texts bright\",\n      .getter=CB(gettter_bold_is_bright), .setter=CB(setter_bold_is_bright),\n    },\n    color_normal(0),  color_normal(1),  color_normal(2),  color_normal(3),\n    color_normal(4),  color_normal(5),  color_normal(6),  color_normal(7),\n    color_normal(8),  color_normal(9),  color_normal(10), color_normal(11),\n    color_normal(12), color_normal(13), color_normal(14), color_normal(15),\n    color_special(window_background, \"\"),\n    color_special(background, TYM_DEFAULT_COLOR_BACKGROUND),\n    color_special(foreground, TYM_DEFAULT_COLOR_FOREGROUND),\n    color_special(bold, TYM_DEFAULT_COLOR_FOREGROUND),\n    color_special(cursor, TYM_DEFAULT_COLOR_FOREGROUND),\n    color_special(cursor_foreground, TYM_DEFAULT_COLOR_BACKGROUND),\n    color_special(highlight, TYM_DEFAULT_COLOR_FOREGROUND),\n    color_special(highlight_foreground, TYM_DEFAULT_COLOR_BACKGROUND),\n    {\n      .name=\"color_0..15\", .type=T_NONE, .arg_desc=\"\", .desc=\"value of color_0 .. color_15\",\n    },\n  };\n#undef CB\n#undef get_default_color\n#undef color_special\n#undef color_normal\n\n  meta->data = g_hash_table_new_full(\n    g_str_hash,\n    g_str_equal,\n    NULL,\n    (GDestroyNotify)free_entry\n  );\n  unsigned i = 0;\n  unsigned len = sizeof(ee) / sizeof(MetaEntry);\n  while (i < len) {\n    MetaEntry* entry = (MetaEntry*)memdup(&ee[i], sizeof(ee[i]));\n    if (entry->getter && !entry->setter) {\n      dw(\"Invalid meta `%s`: setter is provided but getter is not provided.\", entry->name);\n    }\n    entry->index = i;\n    g_hash_table_insert(meta->data, entry->name, entry);\n    meta->list = g_list_insert_sorted(meta->list, entry, entries_sort_func);\n    i++;\n  }\n  return meta;\n}\n\nvoid meta_close(Meta* meta)\n{\n  g_hash_table_destroy(meta->data);\n  g_list_free(meta->list);\n  g_free(meta);\n}\n\nunsigned meta_size(Meta* meta)\n{\n  return g_hash_table_size(meta->data);\n}\n\nMetaEntry* meta_get_entry(Meta* meta, const char* key)\n{\n  MetaEntry* entry = (MetaEntry*)g_hash_table_lookup(meta->data, key);\n  if (!entry) {\n    return NULL;\n  }\n  MetaEntryType t = entry->type;\n  if (t == META_ENTRY_TYPE_STRING || t == META_ENTRY_TYPE_INTEGER || t == META_ENTRY_TYPE_BOOLEAN) {\n    return entry;\n  }\n  dd(\"WARN: tried to get META_ENTRY_TYPE_NONE entry [%s]\", key);\n  return NULL;\n}\n\nstatic void* new_empty_bool()\n{\n  return g_new0(gboolean, 1);\n}\n\nstatic void* new_empty_str()\n{\n  return g_new0(char*, 1);\n}\n\nstatic void* new_empty_int()\n{\n  return g_new0(int, 1);\n}\n\nGOptionEntry* meta_get_option_entries(Meta* meta)\n{\n  GOptionEntry app_options[] = {\n    {\n      .long_name = \"version\",\n      .short_name = 'v',\n      .arg = G_OPTION_ARG_NONE,\n      .arg_data = new_empty_bool(),\n      .description = \"Show version\",\n      .arg_description = NULL,\n    }, {\n      .long_name = \"daemon\",\n      .arg = G_OPTION_ARG_NONE,\n      .arg_data = new_empty_bool(),\n      .description = \"Launch as daemon process\",\n      .arg_description = NULL,\n    }, {\n      .long_name = \"use\",\n      .short_name = 'u',\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"<path> to config file. Set '\" TYM_SYMBOL_NONE \"' to start without loading config\",\n      .arg_description = \"<path>\",\n    }, {\n      .long_name = \"theme\",\n      .short_name = 't',\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"<path> to theme file. Set '\" TYM_SYMBOL_NONE \"' to start without loading theme\",\n      .arg_description = \"<path>\",\n    }, {\n      .long_name = \"id\",\n      .short_name = 'i',\n      .arg = G_OPTION_ARG_INT,\n      .arg_data = new_empty_int(),\n      .description = \"<id> to use in the new instance\",\n      .arg_description = \"<id>\",\n    }, {\n      .long_name = \"signal\",\n      .short_name = 's',\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"<signal name> to send via DBus\",\n      .arg_description = \"<signal name>\",\n    }, {\n      .long_name = \"call\",\n      .short_name = 'c',\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"<method name> to call via DBus\",\n      .arg_description = \"<method name>\",\n    }, {\n      .long_name = \"param\",\n      .short_name = 'p',\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"param with which is called method via DBus\",\n      .arg_description = \"<param>\",\n    }, {\n      .long_name = \"dest\",\n      .short_name = 'd',\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"<dest id> to send signal/call method($TYM_ID is default)\",\n      .arg_description = \"<dest id>\",\n    }, {\n      .long_name = \"isolated\",\n      .arg = G_OPTION_ARG_NONE,\n      .arg_data = new_empty_bool(),\n      .description = \"Start as an isolated instance\",\n      .arg_description = NULL,\n    }, {\n      .long_name = \"cwd\",\n      .arg = G_OPTION_ARG_STRING,\n      .arg_data = new_empty_str(),\n      .description = \"Set the terminal's working directory. Must be an absolute path.\",\n      .arg_description = \"<path>\",\n    }\n  };\n\n  GOptionEntry* options_entries = (GOptionEntry*)g_new0(\n      GOptionEntry,\n      sizeof(app_options) / sizeof(GOptionEntry) + meta_size(meta) + 1\n  );\n  memmove(options_entries, app_options, sizeof(app_options));\n  unsigned i = sizeof(app_options) / sizeof(GOptionEntry);\n\n  for (GList* li = meta->list; li != NULL; li = li->next) {\n    MetaEntry* me = (MetaEntry*)li->data;\n    GOptionEntry* e = &options_entries[i];\n    i += 1;\n    e->long_name = me->name;\n    e->short_name = me->short_name;\n    e->flags = me->option_flag;\n    e->arg_description = me->arg_desc;\n    e->description = me->desc;\n    switch (me->type) {\n      case META_ENTRY_TYPE_STRING:\n        e->arg = G_OPTION_ARG_STRING;\n        e->arg_data = new_empty_str();\n        break;\n      case META_ENTRY_TYPE_INTEGER:\n        e->arg = G_OPTION_ARG_INT;\n        e->arg_data = new_empty_int();\n        break;\n      case META_ENTRY_TYPE_BOOLEAN:\n        e->arg = G_OPTION_ARG_NONE;\n        e->arg_data = new_empty_bool();\n        break;\n      case META_ENTRY_TYPE_NONE:\n        // used for help text\n        e->arg = G_OPTION_ARG_INT;\n        e->arg_data = new_empty_int();\n        break;\n    }\n  }\n  return options_entries;\n}\n"
  },
  {
    "path": "src/option.c",
    "content": "/**\n * option.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"option.h\"\n\n\nOption* option_init(GOptionEntry* entries)\n{\n  df();\n  Option* option = g_new0(Option, 1);\n\n  option->entries = entries;\n  option->option_context = g_option_context_new(\"tym command line\");\n  g_option_context_add_main_entries(option->option_context, option->entries, NULL);\n  /* g_option_context_set_help_enabled(option->option_context, false); */\n  /* g_option_context_add_group(option->option_context, gtk_get_option_group(TRUE)); */\n\n  option->entries_as_table = g_hash_table_new_full(\n    g_str_hash,\n    g_str_equal,\n    NULL,\n    NULL\n  );\n  return option;\n}\n\nvoid option_close(Option* option)\n{\n  g_option_context_free(option->option_context);\n  if (option->entries) {\n    GOptionEntry* e = &option->entries[0];\n    while (e->long_name) {\n      if (e->arg_data) {\n        g_free(e->arg_data);\n      }\n      e++;\n    };\n    g_free(option->entries);\n  }\n  if (option->entries_as_table) {\n    g_hash_table_destroy(option->entries_as_table);\n  }\n  g_free(option);\n}\n\nbool option_parse(Option* option, int argc, char** argv)\n{\n  df();\n  g_assert(option->entries);\n  g_assert(option->entries_as_table);\n\n  GError* error = NULL;\n\n  char** argv_strv = g_new0(char*, argc + 1);\n  int i = 0;\n  while (i < argc) {\n    argv_strv[i] = g_strdup(argv[i]);\n    i++;\n  }\n  g_option_context_parse_strv(option->option_context, &argv_strv, &error);\n  /* Rest value containes un-parsed parts that is followed by \"--\" double dash */\n  option->rest_argv = argv_strv;\n\n  if (error) {\n    g_warning(\"%s\", error->message);\n    return false;\n  }\n\n  GOptionEntry* e = &option->entries[0];\n  while (e->long_name) {\n    g_hash_table_insert(option->entries_as_table, (void*)e->long_name, e);\n    e++;\n  };\n\n  return true;\n}\n\nvoid* option_get_pointer(Option* option, const char* key)\n{\n  g_assert(option->entries);\n  GOptionEntry* e = (GOptionEntry*)g_hash_table_lookup(option->entries_as_table, key);\n  g_assert(e);\n  return e->arg_data;\n}\n\nchar* option_get_str(Option* option, const char* key)\n{\n  char** p = (char**)option_get_pointer(option, key);\n  if (!p) {\n    return false;\n  }\n  return *p;\n}\n\nint option_get_int(Option* option, const char* key)\n{\n  int* p = (int*)option_get_pointer(option, key);\n  if (!p) {\n    return false;\n  }\n  return *p;\n}\n\nbool option_get_bool(Option* option, const char* key)\n{\n  gboolean* p = (gboolean*)option_get_pointer(option, key);\n  if (!p) {\n    return false;\n  }\n  return *p;\n}\n"
  },
  {
    "path": "src/option_test.c",
    "content": "/**\n * option_test.c\n *\n * Copyright (c) 2022 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"tym_test.h\"\n#include \"option.h\"\n#include \"app.h\"\n\nstatic void test_parse()\n{\n  Meta* meta = meta_init();\n\n  Option* option = option_init(meta_get_option_entries(meta));\n\n  char* argv_base[] = {\n    \"tym\",\n    \"-u\", \"config.lua\",\n    \"--width\", \"123\",\n    \"--autohide\",\n    NULL\n  };\n  int argc = sizeof(argv_base) / sizeof(char*) - 1;\n  g_assert(option_parse(option, argc, argv_base));\n\n  g_assert(is_equal(\"config.lua\", option_get_str(option, \"use\")));\n\n  g_assert(option_get_int(option, \"width\") == 123);\n\n  g_assert(option_get_bool(option, \"autohide\") == true);\n\n  g_assert(option_get_str(option, \"theme\") == NULL);\n  g_assert(option_get_int(option, \"height\") == 0);\n  g_assert_false(option_get_bool(option, \"silent\"));\n\n\n  char** a = &argv_base[0];\n  while (*a) {\n    dd(\"ARG %s\", *a);\n    a++;\n  }\n  dd(\"ARGC: %d\", argc);\n\n  option_close(option);\n}\n\nvoid test_option()\n{\n  test_parse();\n}\n"
  },
  {
    "path": "src/property.c",
    "content": "/**\n * property.c\n *\n * Copyright (c) 2019 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"common.h\"\n#include \"property.h\"\n#include \"regex.h\"\n\n\ntypedef enum {\n  VTE_CJK_WIDTH_NARROW = 1,\n  VTE_CJK_WIDTH_WIDE = 2\n} VteCjkWidth;\n\ntypedef void (*VteSetColorFunc)(VteTerminal*, const GdkRGBA*);\n\n\n// STR\n\nvoid setter_shell(Context* context, const char* key, const char* value)\n{\n  if (!is_equal(context_get_str(context, key), value) && context->initialized) {\n    context_log_message(context, false, \"To override `%s`, you need to set value before terminal finish initialization.`\", key);\n    return;\n  }\n  config_set_str(context->config, key, value);\n}\n\nvoid setter_term(Context* context, const char* key, const char* value)\n{\n  if (!is_equal(context_get_str(context, key), value) && context->initialized) {\n    context_log_message(context, false, \"To override `%s`, you need to set value before the terminal finish initialization.`\", key);\n    return;\n  }\n  config_set_str(context->config, key, value);\n}\n\nconst char* getter_title(Context* context, const char* key)\n{\n  return gtk_window_get_title(context->layout.window);\n}\n\nvoid setter_title(Context* context, const char* key, const char* value)\n{\n  gtk_window_set_title(context->layout.window, value);\n}\n\nvoid setter_font(Context* context, const char* key, const char* value)\n{\n  PangoFontDescription* font_desc = pango_font_description_from_string(value);\n  vte_terminal_set_font(context->layout.vte, font_desc);\n  pango_font_description_free(font_desc);\n  config_set_str(context->config, key, value);\n}\n\nconst char* getter_icon(Context* context, const char* key)\n{\n  return gtk_window_get_icon_name(context->layout.window);\n}\n\nvoid setter_icon(Context* context, const char* key, const char* value)\n{\n  gtk_window_set_icon_name(context->layout.window, value);\n}\n\nconst char* getter_role(Context* context, const char* key)\n{\n  const char* role = gtk_window_get_role(context->layout.window);\n  return role ? role : \"\";\n}\n\nvoid setter_role(Context* context, const char* key, const char* value)\n{\n  gtk_window_set_role(context->layout.window, is_none(value) ? NULL : value);\n}\n\nconst char* getter_cursor_shape(Context* context, const char* key)\n{\n  VteCursorShape cursor_shape = vte_terminal_get_cursor_shape(context->layout.vte);\n  switch (cursor_shape) {\n    case VTE_CURSOR_SHAPE_IBEAM:\n      return TYM_CURSOR_SHAPE_IBEAM;\n    case VTE_CURSOR_SHAPE_UNDERLINE:\n      return TYM_CURSOR_SHAPE_UNDERLINE;\n    case VTE_CURSOR_SHAPE_BLOCK:\n      return TYM_CURSOR_SHAPE_BLOCK;\n    default:\n      dw(\"Invalid cursor shape `%d` is detected.\", cursor_shape);\n      return TYM_CURSOR_SHAPE_BLOCK;\n  }\n}\n\nvoid setter_cursor_shape(Context* context, const char* key, const char* value)\n{\n  VteCursorShape cursor_shape = VTE_CURSOR_SHAPE_BLOCK;\n  if (is_equal(value, TYM_CURSOR_SHAPE_BLOCK)) {\n  } else if (is_equal(value, TYM_CURSOR_SHAPE_UNDERLINE)) {\n    cursor_shape = VTE_CURSOR_SHAPE_UNDERLINE;\n  } else if (is_equal(value, TYM_CURSOR_SHAPE_IBEAM))  {\n    cursor_shape = VTE_CURSOR_SHAPE_IBEAM;\n  } else {\n    context_log_message(context, true, \"Invalid `cursor_shape` value. (`%s` is provided). '\" \\\n        TYM_CURSOR_SHAPE_BLOCK \"', '\" TYM_CURSOR_SHAPE_IBEAM \"' or '\" \\\n        TYM_CURSOR_SHAPE_UNDERLINE \"' is available.\", value);\n    return;\n  }\n  vte_terminal_set_cursor_shape(context->layout.vte, cursor_shape);\n}\n\nconst char* getter_cursor_blink_mode(Context* context, const char* key)\n{\n  VteCursorBlinkMode mode = vte_terminal_get_cursor_blink_mode(context->layout.vte);\n  switch (mode) {\n    case VTE_CURSOR_BLINK_SYSTEM:\n      return TYM_CURSOR_BLINK_MODE_SYSTEM;\n    case VTE_CURSOR_BLINK_ON:\n      return TYM_CURSOR_BLINK_MODE_ON;\n    case VTE_CURSOR_BLINK_OFF:\n      return TYM_CURSOR_BLINK_MODE_OFF;\n    default:\n      dw(\"Invalid cursor blink `%d` is detected.\", mode);\n      return TYM_CURSOR_BLINK_MODE_SYSTEM;\n  }\n}\n\nvoid setter_cursor_blink_mode(Context* context, const char* key, const char* value)\n{\n  VteCursorBlinkMode mode = VTE_CURSOR_BLINK_SYSTEM;\n  if (is_equal(value, TYM_CURSOR_BLINK_MODE_SYSTEM)) {\n  } else if (is_equal(value, TYM_CURSOR_BLINK_MODE_ON)) {\n    mode = VTE_CURSOR_BLINK_ON;\n  } else if (is_equal(value, TYM_CURSOR_BLINK_MODE_OFF))  {\n    mode = VTE_CURSOR_BLINK_OFF;\n  } else {\n    context_log_message(context, true, \"Invalid `cursor_blink_mode` value. (`%s` is provided). '\" \\\n        TYM_CURSOR_BLINK_MODE_SYSTEM \"', '\" TYM_CURSOR_BLINK_MODE_ON \"' or '\" \\\n        TYM_CURSOR_BLINK_MODE_OFF \"' is available.\", value);\n    return;\n  }\n  vte_terminal_set_cursor_blink_mode(context->layout.vte, mode);\n}\n\nconst char* getter_cjk_width(Context* context, const char* key)\n{\n  VteCjkWidth cjk = vte_terminal_get_cjk_ambiguous_width(context->layout.vte);\n  switch (cjk) {\n    case VTE_CJK_WIDTH_NARROW:\n      return TYM_CJK_WIDTH_NARROW;\n    case VTE_CJK_WIDTH_WIDE:\n      return TYM_CJK_WIDTH_WIDE;\n    default:\n      dw(\"Invalid `cjk_width` `%d` is detected.\", cjk);\n      return TYM_CJK_WIDTH_NARROW;\n  }\n}\n\nvoid setter_cjk_width(Context* context, const char* key, const char* value)\n{\n  VteCjkWidth cjk = VTE_CJK_WIDTH_NARROW;\n  if (is_equal(value, TYM_CJK_WIDTH_NARROW)) {\n  } else if (is_equal(value, TYM_CURSOR_BLINK_MODE_ON)) {\n    cjk = VTE_CJK_WIDTH_WIDE;\n  } else {\n    context_log_message(context, true, \"Invalid `cjk_width` value. (`%s` is provided). '\" \\\n        TYM_CJK_WIDTH_NARROW \"' or '\" TYM_CJK_WIDTH_WIDE \"' is available.\", value);\n    return;\n  }\n  vte_terminal_set_cjk_ambiguous_width(context->layout.vte, cjk);\n}\n\nvoid setter_background_image(Context* context, const char* key, const char* value)\n{\n  char* css;\n  if (is_empty(value)) {\n    css = g_strdup(\"window { background-image: none; }\");\n  } else {\n    char* path;\n    if (g_path_is_absolute(value)) {\n      path = g_strdup(value);\n    } else {\n      char* cwd = g_get_current_dir();\n      path = g_build_path(G_DIR_SEPARATOR_S, cwd, value, NULL);\n      g_free(cwd);\n    }\n    if (!g_file_test(path, G_FILE_TEST_EXISTS)) {\n      context_log_message(context, true, \"`%s`: `%s` does not exist.\", key, path);\n      g_free(path);\n      return;\n    }\n    css = g_strdup_printf(\"window { background-image: url('%s'); background-size: cover; background-position: center; }\", path);\n    g_free(path);\n  }\n  GtkCssProvider* css_provider = gtk_css_provider_new();\n  GError* error = NULL;\n  gtk_css_provider_load_from_data(css_provider, css, -1, &error);\n  g_free(css);\n  if (error) {\n    context_log_message(context, true, \"`%s`: Error in css: %s\", key, error->message);\n    g_error_free(error);\n    return;\n  }\n  GtkStyleContext* style_context = gtk_widget_get_style_context(GTK_WIDGET(context->layout.window));\n  gtk_style_context_add_provider(style_context, GTK_STYLE_PROVIDER(css_provider), GTK_STYLE_PROVIDER_PRIORITY_USER);\n  config_set_str(context->config, key, value);\n  g_object_unref(css_provider);\n}\n\nvoid setter_uri_schemes(Context* context, const char* key, const char* value)\n{\n  gchar* uri_pattern;\n\n  if (g_strcmp0(TYM_SYMBOL_WILDCARD, value) == 0) {\n    uri_pattern = g_strconcat(SCHEME, SCHEMELESS_URI, NULL);\n  } else {\n    int errorcode;\n    PCRE2_SIZE erroroffset;\n    pcre2_code* code = pcre2_compile(\n      SCHEME_LIST,\n      PCRE2_ZERO_TERMINATED,\n      PCRE2_ANCHORED | PCRE2_CASELESS | PCRE2_ENDANCHORED,\n      &errorcode,\n      &erroroffset,\n      NULL\n    );\n    if (!code) {\n      g_warning(\"pcre2_compile failed for errorcode `%d` at offset `%d`\\n\", errorcode, (int)erroroffset);\n      return;\n    }\n\n    // repetitivelly get all schemes in the list, one by one.\n    // TODO: handle ill-formatted inputs\n    GSList* schemes = NULL;\n    int scheme_length_sum = 0;\n    const char* v = value;\n    while (true) {\n      pcre2_match_data* match_data = pcre2_match_data_create_from_pattern(code, NULL);\n      int res = pcre2_match(\n          code,\n          v,\n          PCRE2_ZERO_TERMINATED,\n          0,\n          PCRE2_ANCHORED | PCRE2_ENDANCHORED | PCRE2_NOTEMPTY,\n          match_data,\n          NULL\n      );\n\n      if (res <= 0) {\n        switch (res) {\n        case 0:\n          g_warning(\"Ovector was not big enough. This should not happen.\");\n          break;\n        case PCRE2_ERROR_NOMATCH:\n          g_warning(\"No match\\n\");\n          break;\n        default:\n          g_warning(\"PCRE2 match error %d\\n\", res);\n          break;\n        }\n        pcre2_match_data_free(match_data);\n        pcre2_code_free(code);\n        return;\n      }\n\n      PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data);\n      int length = ovector[3] - ovector[2];\n      if (length > 0) {\n          schemes = g_slist_prepend(schemes, g_strndup(v + ovector[2], length)); // get first scheme\n          scheme_length_sum += length + 1; // 1 for separater `|` or terminal null char\n      }\n\n      if (ovector[1] > ovector[3]) {\n        // there is at least one more scheme in the list, so move the pointer forward\n        v = &v[ovector[3] + 1];\n      } else {\n        break;\n      }\n      pcre2_match_data_free(match_data);\n    }\n    pcre2_code_free(code);\n\n    // if no schemes specified, remove current regex and return immediately\n    if (scheme_length_sum == 0) {\n      if (context->layout.uri_tag >= 0) {\n        vte_terminal_match_remove(context->layout.vte, context->layout.uri_tag);\n        context->layout.uri_tag = -1;\n        config_set_str(context->config, key, value);\n      }\n      return;\n    }\n\n    gchar scheme_pattern[scheme_length_sum];\n    gchar* p = scheme_pattern;\n    for (GSList* scheme = schemes; scheme; scheme = scheme->next) {\n      p = g_stpcpy(p, scheme->data);\n      *p = '|';\n      ++p;\n    }\n    scheme_pattern[scheme_length_sum - 1] = '\\0'; // replace last `|` with null char\n    uri_pattern = g_strconcat(\"(?:\", scheme_pattern, \")\", SCHEMELESS_URI, NULL);\n    g_slist_free_full(schemes, g_free);\n  }\n\n  GError* error = NULL;\n  VteRegex* regex = vte_regex_new_for_match(uri_pattern, -1, PCRE2_UTF | PCRE2_MULTILINE | PCRE2_CASELESS, &error);\n  g_free(uri_pattern);\n\n  if (error) {\n    g_warning(\"Error when adding regex to VTE: %s\", error->message);\n    g_error_free(error);\n  } else {\n    if (context->layout.uri_tag >= 0) {\n      vte_terminal_match_remove(context->layout.vte, context->layout.uri_tag);\n    }\n    int tag = vte_terminal_match_add_regex(context->layout.vte, regex, 0);\n    context->layout.uri_tag = tag;\n    vte_terminal_match_set_cursor_name(context->layout.vte, tag, \"hand\");\n    vte_regex_unref(regex);\n    config_set_str(context->config, key, value);\n  }\n}\n\n// INT\n\nint getter_width(Context* context, const char* key)\n{\n  return vte_terminal_get_column_count(context->layout.vte);\n}\n\nvoid setter_width(Context* context, const char* key, int value)\n{\n  context_resize(context, value, context_get_int(context, \"height\"));\n}\n\nint getter_height(Context* context, const char* key)\n{\n  return vte_terminal_get_row_count(context->layout.vte);\n}\n\nvoid setter_height(Context* context, const char* key, int value)\n{\n  context_resize(context, context_get_int(context, \"width\"), value);\n}\n\nint getter_scale(Context* context, const char* key)\n{\n  return roundup(vte_terminal_get_font_scale(context->layout.vte) * 100);\n}\n\nvoid setter_scale(Context* context, const char* key, int value)\n{\n  vte_terminal_set_font_scale(context->layout.vte, (double)value / 100);\n}\n\nint getter_cell_width(Context* context, const char* key)\n{\n  return roundup(vte_terminal_get_cell_width_scale(context->layout.vte) * 100);\n}\n\nvoid setter_cell_width(Context* context, const char* key, int value)\n{\n  vte_terminal_set_cell_width_scale(context->layout.vte, (double)value / 100);\n}\n\nint getter_cell_height(Context* context, const char* key)\n{\n  return roundup(vte_terminal_get_cell_height_scale(context->layout.vte) * 100);\n}\n\nvoid setter_cell_height(Context* context, const char* key, int value)\n{\n  vte_terminal_set_cell_height_scale(context->layout.vte, (double)value / 100);\n}\n\n/* DEPRECATED START */\nvoid setter_padding_horizontal(Context* context, const char* key, int value)\n{\n  gtk_box_set_child_packing(context->layout.hbox, GTK_WIDGET(context->layout.vte), true, true, value, GTK_PACK_START);\n  config_set_int(context->config, key, value);\n  if (value != 0) {\n    const char* s = \"Proprty `padding_horizontal` is deprecated. Use `padding_top` and `padding_bottom` instead.\";\n    g_warning(\"%s\", s);\n    context_notify(context, s, \"Deprecation warning\");\n  }\n}\n\nvoid setter_padding_vertical(Context* context, const char* key, int value)\n{\n  gtk_box_set_child_packing(context->layout.vbox, GTK_WIDGET(context->layout.hbox), true, true, value, GTK_PACK_START);\n  config_set_int(context->config, key, value);\n  if (value != 0) {\n    const char* s = \"Proprty `padding_vertical` is deprecated. Use `padding_top` and `padding_bottom` instead.\";\n    g_warning(\"%s\", s);\n    context_notify(context, s, \"Deprecation warning\");\n  }\n}\n/* DEPRECATED END */\n\nint getter_padding_top(Context* context, const char* key)\n{\n  return gtk_widget_get_margin_top(GTK_WIDGET(context->layout.vte));\n}\n\nvoid setter_padding_top(Context* context, const char* key, int value)\n{\n  gtk_widget_set_margin_top(GTK_WIDGET(context->layout.vte), value);\n}\n\nint getter_padding_bottom(Context* context, const char* key)\n{\n  return gtk_widget_get_margin_bottom(GTK_WIDGET(context->layout.vte));\n}\n\nvoid setter_padding_bottom(Context* context, const char* key, int value)\n{\n  gtk_widget_set_margin_bottom(GTK_WIDGET(context->layout.vte), value);\n}\n\nint getter_padding_left(Context* context, const char* key)\n{\n  return gtk_widget_get_margin_start(GTK_WIDGET(context->layout.vte));\n}\n\nvoid setter_padding_left(Context* context, const char* key, int value)\n{\n  gtk_widget_set_margin_start(GTK_WIDGET(context->layout.vte), value);\n}\n\nint getter_padding_right(Context* context, const char* key)\n{\n  return gtk_widget_get_margin_end(GTK_WIDGET(context->layout.vte));\n}\n\nvoid setter_padding_right(Context* context, const char* key, int value)\n{\n  gtk_widget_set_margin_end(GTK_WIDGET(context->layout.vte), value);\n}\n\nint getter_scrollback_length(Context* context, const char* key)\n{\n  return vte_terminal_get_scrollback_lines(context->layout.vte);\n}\n\nvoid setter_scrollback_length(Context* context, const char* key, int value)\n{\n  vte_terminal_set_scrollback_lines(context->layout.vte, value);\n}\n\n\n// BOOL\nbool getter_scroll_on_output(Context* context, const char* key)\n{\n  return vte_terminal_get_scroll_on_output(context->layout.vte);\n}\n\nvoid setter_scroll_on_output(Context* context, const char* key, bool value)\n{\n  vte_terminal_set_scroll_on_output(context->layout.vte, value);\n}\n\nbool getter_silent(Context* context, const char* key)\n{\n  return !vte_terminal_get_audible_bell(context->layout.vte);\n}\n\nvoid setter_silent(Context* context, const char* key, bool value)\n{\n  vte_terminal_set_audible_bell(context->layout.vte, !value);\n}\n\nbool getter_autohide(Context* context, const char* key)\n{\n  return vte_terminal_get_mouse_autohide(context->layout.vte);\n}\n\nvoid setter_autohide(Context* context, const char* key, bool value)\n{\n  vte_terminal_set_mouse_autohide(context->layout.vte, value);\n}\n\nbool gettter_bold_is_bright(Context* context, const char* key)\n{\n  return vte_terminal_get_bold_is_bright(context->layout.vte);\n}\n\nvoid setter_bold_is_bright(Context* context, const char* key, bool value)\n{\n  vte_terminal_set_bold_is_bright(context->layout.vte, value);\n}\n\n// COLOR\nstatic void setter_color_special(Context* context, const char* key, const char* value, VteSetColorFunc color_func)\n{\n  GdkRGBA color = {};\n  bool valid = gdk_rgba_parse(&color, value);\n  if (!valid) {\n    context_log_message(context, true, \"Invalid color string for '%s': %s\", key, value);\n    return;\n  }\n  color_func(context->layout.vte, &color);\n  config_set_str(context->config, key, value);\n}\n\nvoid setter_color_normal(Context* context, const char* key, const char* value)\n{\n  assert(value);\n  if (is_equal(value, context_get_str(context, key))) {\n    return;\n  }\n  char* target = NULL;\n  int index = g_ascii_strtoull(&key[6], &target, 10);\n  GdkRGBA* palette = g_new0(GdkRGBA, 16);\n  assert(&key[6] != target || index < 0 || index > 15);\n  char s[10] = {};\n  unsigned i = 0;\n  while (i < 16) {\n    const char* v;\n    if (i == index) {\n      v = value;\n    } else {\n      g_snprintf(s, 10, \"color_%d\", i);\n      v = context_get_str(context, s);\n    }\n    bool valid = gdk_rgba_parse(&palette[i], v);\n    if (!valid) {\n      context_log_message(context, true, \"Invalid color string for '%s': %s\", key, value);\n      return;\n    }\n    i += 1;\n  }\n  vte_terminal_set_colors(context->layout.vte, NULL, NULL, palette, 16);\n  config_set_str(context->config, key, value);\n  g_free(palette);\n}\n\nvoid setter_color_window_background(Context* context, const char* key, const char* value)\n{\n  if (is_empty(value)) {\n    gtk_widget_set_app_paintable(GTK_WIDGET(context->layout.window), false);\n    config_set_str(context->config, key, value);\n    return;\n  }\n\n  if (!is_none(value)) {\n    GdkRGBA color = {};\n    bool valid = gdk_rgba_parse(&color, value);\n    if (!valid) {\n      context_log_message(context, true, \"Invalid color string for '%s': %s\", key, value);\n      return;\n    }\n  } else {\n    gtk_widget_queue_draw(GTK_WIDGET(context->layout.window));\n  }\n  gtk_widget_set_app_paintable(GTK_WIDGET(context->layout.window), true);\n  config_set_str(context->config, key, value);\n}\n\nvoid setter_color_background(Context* context, const char* key, const char* value)\n{\n  if (is_none(value)) {\n#ifdef TYM_USE_TRANSPARENT\n    vte_terminal_set_clear_background(context->layout.vte, false);\n    config_set_str(context->config, key, value);\n#else\n    context_log_message(context, true, \"`NONE` for `color_background` is supported on VTE version>=0.52 (your VTE version is %s)\", TYM_VTE_VERSION);\n#endif\n    return;\n  }\n  vte_terminal_set_clear_background(context->layout.vte, true);\n\n  setter_color_special(context, key, value, vte_terminal_set_color_background);\n}\n\nvoid setter_color_foreground(Context* context, const char* key, const char* value)\n{\n  setter_color_special(context, key, value, vte_terminal_set_color_foreground);\n}\n\nvoid setter_color_bold(Context* context, const char* key, const char* value)\n{\n  setter_color_special(context, key, value, vte_terminal_set_color_bold);\n}\n\nvoid setter_color_cursor(Context* context, const char* key, const char* value)\n{\n  setter_color_special(context, key, value, vte_terminal_set_color_cursor);\n}\n\nvoid setter_color_cursor_foreground(Context* context, const char* key, const char* value)\n{\n#ifdef TYM_USE_VTE_COLOR_CURSOR_FOREGROUND\n  setter_color_special(context, key, value, vte_terminal_set_color_cursor_foreground);\n#else\n  context_log_message(context, true, \"`%s` is supported on VTE version>=0.46 (your VTE version is %s)\", key, TYM_VTE_VERSION);\n#endif\n}\n\nvoid setter_color_highlight(Context* context, const char* key, const char* value)\n{\n  setter_color_special(context, key, value, vte_terminal_set_color_highlight);\n}\n\nvoid setter_color_highlight_foreground(Context* context, const char* key, const char* value)\n{\n  setter_color_special(context, key, value, vte_terminal_set_color_highlight_foreground);\n}\n"
  },
  {
    "path": "src/regex_test.c",
    "content": "/**\n * regex_test.c\n *\n * Copyright (c) 2019 endaaman, iTakeshi\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"tym_test.h\"\n#include \"regex.h\"\n\n\n#define URI \"(?:http|https|file|mailto)\" SCHEMELESS_URI\n\n\nstatic int check_match(int anchored, const char* pattern, const char* subject, const char* expected, int invert)\n{\n  if (expected == NULL) expected = subject;\n\n  int errorcode;\n  PCRE2_SIZE erroroffset;\n  pcre2_code* code = pcre2_compile(\n    pattern,\n    PCRE2_ZERO_TERMINATED,\n    PCRE2_UTF |\n    PCRE2_NO_UTF_CHECK |\n    PCRE2_MULTILINE |\n    PCRE2_CASELESS |\n    PCRE2_NEVER_BACKSLASH_C |\n    PCRE2_USE_OFFSET_LIMIT |\n    (anchored ? PCRE2_ANCHORED : 0),\n    &errorcode,\n    &erroroffset,\n    NULL\n  );\n  if (!code) {\n    printf(\"pcre2_compile failed for errorcode `%d` at offset `%d`\\n\", errorcode, (int)erroroffset);\n    return 1;\n  }\n\n  pcre2_match_data_8 *match_data = pcre2_match_data_create_8(256, NULL);\n  pcre2_match_context_8 *match_context = pcre2_match_context_create_8(NULL);\n  pcre2_set_recursion_limit_8(match_context, 64);\n  int res = pcre2_match(\n    code,\n    subject,\n    PCRE2_ZERO_TERMINATED,\n    0,\n    PCRE2_NO_UTF_CHECK |\n    PCRE2_NOTEMPTY |\n    (anchored ? PCRE2_ANCHORED : 0),\n    match_data,\n    match_context\n  );\n\n  if (res > 0) {\n    PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data);\n    int offset = ovector[0];\n    int length = ovector[1] - ovector[0];\n    char matched[256] = { 0 };\n    strncpy(matched, &subject[offset], length);\n    if (length == strlen(expected) && strncmp(matched, expected, length) == 0) {\n      if (invert) {\n        printf(\"  UNEXPECTED MATCH: matched=\\\"%s\\\", expected=fail\\n\", matched);\n        return 0;\n      } else {\n        printf(\"  MATCH SUCCESS: %s\\n\", matched);\n        return 1;\n      }\n    } else {\n      if (invert) {\n        printf(\"  EXPECTED UNMATCH: %s\\n\", subject);\n        return 1;\n      } else {\n        printf(\"  UNEXPECTED MATCH: matched=\\\"%s\\\", expected=\\\"%s\\\"\\n\", matched, expected);\n        return 0;\n      }\n    }\n  } else {\n    if (invert && res == PCRE2_ERROR_NOMATCH) {\n      printf(\"  EXPECTED UNMATCH: %s\\n\", subject);\n      return 1;\n    } else {\n      char mes[256] = {};\n      pcre2_get_error_message(res, mes, 256);\n      printf(\"  PCRE2_MATCH ERROR: code=%d, message=\\\"%s\\\"\\n\", res, mes);\n      return 0;\n    }\n  }\n\n  pcre2_match_data_free(match_data);\n  pcre2_code_free(code);\n}\n\nvoid test_regex()\n{\n  printf(\"Testing HOST\\n\");\n  g_assert(check_match(1, SCHEME , \"http\"    , NULL , 0));\n  g_assert(check_match(1, SCHEME , \"HTTP\"    , NULL , 0));\n  g_assert(check_match(1, SCHEME , \"foo0.-+\" , NULL , 0));\n  g_assert(check_match(1, SCHEME , \"0foo\"    , NULL , 1)); // disallow non-alphabet character at the beginning\n\n  printf(\"Testing USERINFO\\n\");\n  g_assert(check_match(1, USERINFO , \"foo.bar-baz\"        , NULL , 0));\n  g_assert(check_match(1, USERINFO , \"user:pass!$&'*+,;=\" , NULL , 0));\n  g_assert(check_match(1, USERINFO , \"user@\"              , NULL , 1)); // disallow `@`\n  g_assert(check_match(1, USERINFO , \"user:pass@\"         , NULL , 1)); // disallow `@`\n\n  printf(\"Testing HOST\\n\");\n  g_assert(check_match(1 , HOST , \"localhost\"                 , NULL , 0));\n  g_assert(check_match(1 , HOST , \"example.com\"               , NULL , 0));\n  g_assert(check_match(1 , HOST , \"a-abc_d;.e.012\"            , NULL , 0));\n  g_assert(check_match(1 , HOST , \"172.0.0.1\"                 , NULL , 0));\n  g_assert(check_match(1 , HOST , \"[2001:db8::1234:0:0:9abc]\" , NULL , 0));\n  g_assert(check_match(1 , HOST , \"あいう.example.com\"        , NULL , 1)); // disallow non-ascii\n  g_assert(check_match(1 , HOST , \"172.0.0.300\"               , NULL , 1)); // check ip-v4 range\n  g_assert(check_match(1 , HOST , \"example.co/m\"              , NULL , 1)); // disallow `/`\n\n  printf(\"Testing QUERY\\n\");\n  g_assert(check_match(1 , QUERY , \"foo0=bar0\" , NULL , 0));\n  g_assert(check_match(1 , QUERY , \"foo0=bar0&foo1=bar1\" , NULL , 0));\n  g_assert(check_match(1 , QUERY , \"foo0=bar0&path=baz/qux?quux\" , NULL , 0));\n\n  // cases are cited from RFC3987 and RFC6068\n  printf(\"Integrated tests\\n\");\n  g_assert(check_match(0 , URI , \"http://localhost:3000/index.html\"                                         , \"http://localhost:3000/index.html\"                                       , 0));\n  g_assert(check_match(0 , URI , \"http://www.example.org/D%C3%BCrst\"                                        , \"http://www.example.org/D%C3%BCrst\"                                      , 0));\n  g_assert(check_match(0 , URI , \"http://www.example.org/D&#xFC;rst\"                                        , \"http://www.example.org/D&#xFC;rst\"                                      , 0));\n  g_assert(check_match(0 , URI , \"http://www.example.org/D%FCrst\"                                           , \"http://www.example.org/D%FCrst\"                                         , 0));\n  g_assert(check_match(0 , URI , \"http://xn--99zt52a.example.org/%e2%80%ae\"                                 , \"http://xn--99zt52a.example.org/%e2%80%ae\"                               , 0));\n  g_assert(check_match(0 , URI , \"\\\"http://ab.CDEFGH.ij/kl/mn/op.html\\\"\"                                    , \"http://ab.CDEFGH.ij/kl/mn/op.html\"                                      , 0));\n  g_assert(check_match(0 , URI , \"\\\"http://AB.CD.EF/GH/IJ/KL?MN=OP;QR=ST#UV\\\"\"                              , \"http://AB.CD.EF/GH/IJ/KL?MN=OP;QR=ST#UV\"                                , 0));\n  g_assert(check_match(0 , URI , \"\\\"http://VU#TS=RQ;PO=NM?LK/JI/HG/FE.DC.BA\\\"\"                              , \"http://VU#TS=RQ;PO=NM?LK/JI/HG/FE.DC.BA\"                                , 0));\n  g_assert(check_match(0 , URI , \"\\\"http://AB.CD.ef/gh/IJ/KL.html\\\"\"                                        , \"http://AB.CD.ef/gh/IJ/KL.html\"                                          , 0));\n  g_assert(check_match(0 , URI , \"<mailto:addr1@an.example,addr2@an.example>\"                               , \"mailto:addr1@an.example,addr2@an.example\"                               , 0));\n  g_assert(check_match(0 , URI , \"<mailto:chris@example.com>\"                                               , \"mailto:chris@example.com\"                                               , 0));\n  g_assert(check_match(0 , URI , \"<mailto:infobot@example.com?subject=current-issue>\"                       , \"mailto:infobot@example.com?subject=current-issue\"                       , 0));\n  g_assert(check_match(0 , URI , \"<mailto:infobot@example.com?body=send%20current-issue>\"                   , \"mailto:infobot@example.com?body=send%20current-issue\"                   , 0));\n  g_assert(check_match(0 , URI , \"<mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index>\" , \"mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index\" , 0));\n  g_assert(check_match(0 , URI , \"<mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E>\"  , \"mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E\"  , 0));\n  g_assert(check_match(0 , URI , \"<mailto:majordomo@example.com?body=subscribe%20bamboo-l>\"                 , \"mailto:majordomo@example.com?body=subscribe%20bamboo-l\"                 , 0));\n  g_assert(check_match(0 , URI , \"<mailto:joe@example.com?cc=bob@example.com&body=hello>\"                   , \"mailto:joe@example.com?cc=bob@example.com&body=hello\"                   , 0));\n  g_assert(check_match(0 , URI , \"<mailto:addr1@an.example?to=addr2@an.example>\"                            , \"mailto:addr1@an.example?to=addr2@an.example\"                            , 0));\n  g_assert(check_match(0 , URI , \"<mailto:gorby%25kremvax@example.com>\"                                     , \"mailto:gorby%25kremvax@example.com\"                                     , 0));\n  g_assert(check_match(0 , URI , \"<mailto:unlikely%3Faddress@example.com?blat=foop>\"                        , \"mailto:unlikely%3Faddress@example.com?blat=foop\"                        , 0));\n  g_assert(check_match(0 , URI , \"<a href=\\\"mailto:joe@an.example?cc=bob@an.example&amp;body=hello\\\">\"      , \"mailto:joe@an.example?cc=bob@an.example&amp;body=hello\"                 , 0));\n  g_assert(check_match(0 , URI , \"<mailto:Mike%26family@example.org>.\"                                      , \"mailto:Mike%26family@example.org\"                                       , 0));\n  g_assert(check_match(0 , URI , \"<mailto:%22not%40me%22@example.org>.\"                                     , \"mailto:%22not%40me%22@example.org\"                                      , 0));\n  g_assert(check_match(0 , URI , \"<mailto:%22oh%5C%5Cno%22@example.org>.\"                                   , \"mailto:%22oh%5C%5Cno%22@example.org\"                                    , 0));\n  g_assert(check_match(0 , URI , \"<mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org>.\"       , \"mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org\"        , 0));\n  g_assert(check_match(0 , URI , \"<mailto:user@example.org?subject=caf%C3%A9>\"                              , \"mailto:user@example.org?subject=caf%C3%A9\"                              , 0));\n  g_assert(check_match(0 , URI , \"<mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D>\"  , \"mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D\"  , 0));\n  g_assert(check_match(0 , URI , \"<mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D>\"  , \"mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D\"  , 0));\n  g_assert(check_match(0 , URI , \"<mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9>\"               , \"mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9\"               , 0));\n  g_assert(check_match(0 , URI , \"<mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO>\"     , \"mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO\"     , 0));\n  g_assert(check_match(0 , URI , \"file:///\"                                                                 , \"file:///\"                                                               , 0));\n  g_assert(check_match(0 , URI , \"file:///home/user/example.txt\"                                            , \"file:///home/user/example.txt\"                                          , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com)\"                                               , \"https://example.com\"                                                    , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/path)\"                                          , \"https://example.com/path\"                                               , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/path?query)\"                                    , \"https://example.com/path?query\"                                         , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/path?query#fragment)\"                           , \"https://example.com/path?query#fragment\"                                , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/p(at)h?q(uer)y#fr(ag)ment)\"                     , \"https://example.com/p(at)h?q(uer)y#fr(ag)ment\"                          , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/pat)h?q(uer)y#fr(ag)ment)\"                      , \"https://example.com/pat\"                                                , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/p(at)h?quer)y#fr(ag)ment)\"                      , \"https://example.com/p(at)h?quer\"                                        , 0));\n  g_assert(check_match(0 , URI, \"[link](https://example.com/p(at)h?q(uer)y#frag)ment)\"                      , \"https://example.com/p(at)h?q(uer)y#frag\"                                , 0));\n\n  // NOT match\n  g_assert(check_match(0 , URI , \"foo:\" , NULL , 1));  // only scheme-like part\n\n  printf(\"regex tests complete!\\n\");\n}\n"
  },
  {
    "path": "src/tym.c",
    "content": "/**\n * tym.c\n *\n * Copyright (c) 2017 endaaman\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"tym.h\"\n\n\nint main(int argc, char* argv[])\n{\n  dd(\"start\");\n\n  app_init();\n  GOptionEntry* entries = meta_get_option_entries(app->meta);\n  Option* option = option_init(entries);\n\n  if (!option_parse(option, argc, argv)) {\n    return 1;\n  }\n\n  if (option_get_bool(option, \"version\")) {\n    g_print(\"version %s\\n\", PACKAGE_VERSION);\n    return 0;\n  }\n\n  int exit_code = app_start(option, argc, argv);\n  app_close();\n  return exit_code;\n}\n"
  },
  {
    "path": "src/tym_test.c",
    "content": "/**\n * tym_test.c\n *\n * Copyright (c) 2019 endaaman, iTakeshi\n *\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n */\n\n#include \"tym_test.h\"\n#include \"app.h\"\n\nint main(int argc, char* argv[])\n{\n  g_test_init(&argc, &argv, NULL);\n  g_test_add_func(\"/tym/config\", test_config);\n  g_test_add_func(\"/tym/regex\", test_regex);\n  g_test_add_func(\"/tym/option\", test_option);\n  return g_test_run();\n}\n"
  },
  {
    "path": "tym-daemon.desktop",
    "content": "[Desktop Entry]\nCategories=System;TerminalEmulator;\nComment=Daemon process for tym\nExec=tym --daemon\nGenericName=Terminal\nIcon=utilities-terminal\nName=tym(daemon)\nStartupNotify=true\nTerminal=false\nTryExec=tym\nType=Application\nX-GNOME-SingleWindow=false\n"
  },
  {
    "path": "tym-daemon.service.in",
    "content": "[Unit]\nDescription=tym daemon\n\n[Service]\nType=simple\nExecStart=@prefix@/bin/tym --daemon\n\n[Install]\nWantedBy=graphical.target\n"
  },
  {
    "path": "tym.1.in",
    "content": ".TH tym 1 \"@DATE@\" \"@VERSION@\" \"tym\"\n.SH DESCRIPTION\n\\fBtym\\fR is a tiny VTE-based terminal emulator, which configurable by Lua.\n\n.SH SYNOPSIS\n\\fBtym\\fR [OPTIONS]\n\n.SH OPTIONS\n.IP \"\\fB\\-h\\fR, \\fB\\-\\-help\\fR\"\nShow help message.\n\n.IP \"\\fB\\-v\\fR, \\fB\\-\\-version\\fR\"\nShow version.\n\n.IP \"\\fB\\-u\\fR, \\fB\\-\\-use\\fR=\\fI<PATH>\\fR\"\nUse <PATH> instead of default config file.\n\n.IP \"\\fB\\-t\\fR, \\fB\\-\\-theme\\fR=\\fI<PATH>\\fR\"\nUse <PATH> instead of default theme file.\n\n.IP \"\\fB\\-\\-cwd\\fR=\\fI<PATH>\\fR\"\nUse <PATH> as the terminal's working directory. Must be an absolute path.\n\n.IP \"\\fB\\-\\-\\fR\\fI<OPTION>\\fR=\\fI<VALUE>\\fR\"\nReplace <OPTION> config option, where \\fI<OPTION>\\fR is a config option and\n\\fI<VALUE>\\fR is a value of its option.\n.fi\nSee \\fBCONFIGURATION\\fR for more information about config options.\n\n.SH AVAILABLE OPTIONS\n\n.IP \\fBshell\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI$SHELL\\fR → \\fBvte_get_user_shell()\\fR → \\fB/bin/sh\\fR\n.fi\nShell to excute.\n\n.IP \\fBtitle\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'tym'\\fR\n.fi\nInitial window title.\n\n.IP \\fBfont\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI''\\fR (empty string)\n.fi\nYou can specify it like \\fI'FAMILY-LIST [SIZE]'\\fR, for example \\fI'Ubuntu Mono 12'\\fR. The value is parsed by \\fBpango_font_description_from_string()\\fR. If you set empty string, the system default fixed width font will be used.\n\n.IP \\fBicon\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'terminal'\\fR\n.fi\nName of icon. cf. https://developer.gnome.org/icon-naming-spec/.\n\n.IP \\fBcursor_shape\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'system'\\fR\n.fi\n\\fI'block'\\fR, \\fI'ibeam'\\fR or \\fI'underline'\\fR are available.\n\n.IP \\fBcursor_blink_mode\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'system'\\fR\n.fi\n\\fI'system'\\fR, \\fI'on'\\fR or \\fI'off'\\fR are available.\n\n.IP \\fBterm\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'xterm-256color'\\fR\n.fi\nDefault value of `$TERM`.\n\n.IP \\fBrole\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI''\\fR\n.fi\nUnique identifier for the window. If empty string set, no value set. cf. gtk_window_set_role()\n\n.IP \\fBcjk_width\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'narrow'\\fR\n.fi\n\\fI'narrow'\\fR or \\fI'wide'\\fR are available.\n\n.IP \\fBuri_schemes\\fR\nType:\t\\fBstring\\fR\n.fi\nDefault:\t\\fI'http https file mailto'\\fR\n.fi\nSpace-separated list of URI schemes to be highlighted and clickable. Specify empty string to disable highlighting. Specify \\fB'*'\\fR to accept any strings valid as schemes (according to RFC 3986).\n\n.IP \\fBwidth\\fR\nType:\t\\fBinteger\\fR\n.fi\nDefault:\t\\fI80\\fR\n.fi\nInitial columns.\n\n.IP \\fBheight\\fR\nType:\t\\fBinteger\\fR\n.fi\nDefault:\t\\fI22\\fR\n.fi\nInitial rows.\n\n.IP \\padding_horizontal\\fR\nType:\t\\fBinteger\\fR\n.fi\nDefault:\t\\fI80\\fR\n.fi\nHorizontal padding.\n\n.IP \\padding_vertical\\fR\nType:\t\\fBinteger\\fR\n.fi\nDefault:\t\\fI80\\fR\n.fi\nVertical padding.\n\n.IP \"\\ignore_default_keymap\\fR\"\nType:\t\\fBboolean\\fR\n.fi\nDefault:\t\\fIfalse\\fR\n.fi\nIf it is provided, the default keymap will not be used.\n\n.IP \\fBautohide\\fR\nType:\t\\fBboolean\\fR\n.fi\nDefault:\t\\fIfalse\\fR\n.fi\nIf it is provided, mouse cursor will be hidden when you presses a key.\n\n.IP \\fBbold_is_bright\\fR\nType:\t\\fBboolean\\fR\n.fi\nDefault:\t\\fIfalse\\fR\n.fi\nIf it is provided, make bold texts bright..\n\n.IP \\fBsilent\\fR\nType:\t\\fBboolean\\fR\n.fi\nDefault:\t\\fIfalse\\fR\n.fi\nIf it is provided, beep does not sound when bell sequence is sent.\n\n.IP \\fBscrollback_length\\fR\nType:\t\\fBinteger\\fR\n.fi\nDefault:\t\\fI512\\fR\n.fi\nIf it is provided, the length of scrollback buffer is resized.\n\n.IP \\fBcolor_window_background\\fR\nType:\t\\string\\fR\n.fi\nDefault:\t\\fI''\\fR\n.fi\nColor of the terminal window. It is seen when `padding_horizontal` `padding_vertical` is not `0`. If you specify 'NONE', the window background will not be drawn.\n\n.IP \\fBcolor_foreground\\fR\n.IP \\fBcolor_background\\fR\n.IP \\fBcolor_cursor\\fR\n.IP \\fBcolor_cursor_foreground\\fR\n.IP \\fBcolor_highlight\\fR\n.IP \\fBcolor_highlight_foreground\\fR\n.IP \\fBcolor_bold\\fR\n.IP \"\\fBcolor_0\\fR .. \\fBcolor_15\\fR\"\nType: \\fBstring\\fR\n.fi\nDefault:\t\\fI''\\fR (empty string)\n.fi\nYou can specify standard color string such as \\fI'#f00'\\fR, \\fI'#ff0000'\\fR, \\fI'rgba(22, 24, 33, 0.7)'\\fR, or \\fI'red'\\fR. It will be parsed by \\fBgdk_rgba_parse()\\fR. If you set empty string, the VTE default color will be used. If you set 'NONE' for `color_background`, the terminal background will not be drawn.\n\n\n.SH DEFAULT KEYBINDINGS\n.TS\nleft,box;\nlB lB\n__\nl l.\nKey\tAction\n\\fBCtrl\\fR+\\fBShift\\fR+\\fBc\\fR\tCopy selection to clipboard\n\\fBCtrl\\fR+\\fBShift\\fR+\\fBv\\fR\tPaste from clipboard\n\\fBCtrl\\fR+\\fBShift\\fR+\\fBr\\fR\tReload config file\n.TE\n\n.SH CONFIGURATION\n\nWhen \\fB$XDG_CONFIG_HOME/tym/config.lua\\fR exists, it is executed. Here is an example.\n\n.nf\n\\fB\nlocal tym = require('tym')\ntym.set('font', 'DejaVu Sans Mono 11')\ntym.set_config({\n  shell = '/usr/bin/fish',\n  cursor = 'underline',\n  autohide = true,\n  color_foreground = 'red',\n})\ntym.set_keymap('<Ctrl><Shift>o', function()\n  local h = tym.get('height')\n  tym.set('height', h + 1)\n  tym.apply() -- needed for applying config value\n  tym.notify('Set window height :' .. h)\nend)\ntym.set_keymaps({\n  ['<Ctrl><Shift>y'] = function()\n    tym.reload()\n    tym.notify('reload config')\n  end,\n  ['<Ctrl><Shift>v'] = function()\n    tym.notify(\"Overwrite pasting event\")\n  end,\n})\n\\fR\n.fi\n\n.SH LUA API\n\n.IP \\fBtym.get(key)\\fR\nReturns:\t\\fBany\\fR\n.fi\nGet config value.\n\n.IP \"\\fBtym.set(key, value)\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nGet config value.\n\n.IP \"\\fBtym.get_config()\\fR\"\nReturns:\t\\fBtable\\fR\n.fi\nGet config table.\n\n.IP \"\\fBtym.set_config(table)\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nSet config by table.\n\n.IP \"\\fBtym.reset_config()\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nReset config to default.\n\n.IP \"\\fBtym.set_keymap(accelerator, func)\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nSet keymap. \\accelerator\\fB must be in a format parsable by \\fBgtk_accelerator_parse()\\fR.\n\n.IP \"\\fBtym.set_keymaps(table)\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nSet keymaps by table.\n\n.IP \"\\fBtym.reset_keymaps()\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nReset custom keymaps.\n\n.IP \"\\fBtym.send_key(accelerator)\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nSend key press event.\n\n.IP \\fBtym.reload()\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nReload config file and theme file.\n\n.IP \\fBtym.reload_theme()\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nReload theme file.\n\n.IP \\fBtym.apply()\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nAppyl config to app.\n\n.IP \\fBtym.put(text)\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nFeed text.\n\n.IP \\fBtym.bell()\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nSound bell.\n\n.IP \"\\fBtym.notify(message, title = \\fI'tym'\\fB)\\fR\"\nReturns:\t\\fBvoid\\fR\n.fi\nShow desktop notification.\n\n.IP \\fBtym.copy()\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nCopy current selection.\n\n.IP \\fBtym.paste()\\fR\nReturns:\t\\fBvoid\\fR\n.fi\nPaste clipboard.\n\n.IP \\fBtym.get_version()\\fR\nReturns:\t\\fBstring\\fR\n.fi\nGet version string.\n\n.IP \\fBtym.get_config_path()\\fR\nReturns:\t\\fBstring\\fR\n.fi\nGet path of config file currently being read.\n\n.IP \\fBtym.get_theme_path()\\fR\nReturns:\t\\fBstring\\fR\n.fi\nGet path of theme file currently being read.\n\n.SH THEME CUSTOMIZATION\n\nWhen \\fB$XDG_CONFIG_HOME/tym/theme.lua\\fR exists, it is executed. Here is an example.\n\n.nf\n\\fB\nlocal fg = '#d2d4de'\nlocal bg = '#161821'\nreturn {\n  color_background = bg,\n  color_foreground = fg,\n  color_0  = '#161821',\n  color_1  = '#e27878',\n  -- SNIP\n  color_14 = '#95c4ce',\n  color_15 = '#d2d4de',\n}\n\\fR\n.fi\n\nYou need to return table within global context.\n"
  },
  {
    "path": "tym.desktop",
    "content": "[Desktop Entry]\nCategories=System;TerminalEmulator;\nComment=A tiny terminal for minimalists\nExec=tym\nGenericName=Terminal\nIcon=utilities-terminal\nName=tym\nStartupNotify=true\nTerminal=false\nTryExec=tym\nType=Application\nX-GNOME-SingleWindow=false\n"
  }
]