Repository: endaaman/tym Branch: master Commit: 143eb95d880a Files: 52 Total size: 190.8 KB Directory structure: gitextract_42qbm9_1/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile.am ├── README.md ├── autotools.mk ├── configure.ac ├── include/ │ ├── Makefile.am │ ├── app.h │ ├── builtin.h │ ├── command.h │ ├── common.h.in │ ├── config.h │ ├── context.h │ ├── hook.h │ ├── ipc.h │ ├── keymap.h │ ├── meta.h │ ├── option.h │ ├── property.h │ ├── regex.h │ ├── tym.h │ └── tym_test.h ├── lua/ │ └── e2e.lua ├── scripts/ │ ├── bundle.sh │ ├── cleanup.sh │ └── refresh.sh ├── src/ │ ├── Makefile.am │ ├── app.c │ ├── builtin.c │ ├── command.c │ ├── common.c │ ├── config.c │ ├── config_test.c │ ├── context.c │ ├── hook.c │ ├── ipc.c │ ├── keymap.c │ ├── meta.c │ ├── option.c │ ├── option_test.c │ ├── property.c │ ├── regex_test.c │ ├── tym.c │ └── tym_test.c ├── tym-daemon.desktop ├── tym-daemon.service.in ├── tym.1.in └── tym.desktop ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: build: machine: true steps: - checkout - run: docker build -t tym . - run: docker build -t tym-luajit --build-arg EXTRA_CONF=--enable-luajit . - run: docker run tym - run: docker run tym-luajit ================================================ FILE: .dockerignore ================================================ .git *.l[ao] *.o *~ .deps/ .dirstamp .libs/ Makefile Makefile.in aclocal.m4 autom4te.cache/ compile config.* configure depcomp install-sh libtool ltmain.sh m4/ missing stamp-h? src/version.h tym tym-*.tar.gz ================================================ FILE: .editorconfig ================================================ root = true [*.{c,h}] charset = utf-8 indent_style = space indent_size = 2 tab_width = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .gitignore ================================================ *.log *.trs *.l[ao] *.o *~ .cache .dirstamp Makefile Makefile.in aclocal.m4 compile configure depcomp install-sh libtool ltmain.sh missing stamp-h? .deps/ .libs/ autom4te.cache/ m4/ tym.1 tym tym-test include/common.h tym-daemon.service /config.* /app-config.* tym-*.tar.gz test-driver .ccls .ccls-root .ccls-cache/ compile_commands.json ================================================ FILE: Dockerfile ================================================ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN 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 RUN mkdir -p /var/app ADD . /var/app WORKDIR /var/app ARG EXTRA_CONF= RUN autoreconf -fvi RUN ./configure $EXTRA_CONF RUN make RUN make check CMD xvfb-run -a ./src/tym -u ./lua/e2e.lua ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 endaaman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile.am ================================================ SUBDIRS = src include man_MANS = tym.1 desktopdir = $(datadir)/applications EXTRA_DIST = $(man_MANS) dist_desktop_DATA = tym.desktop tym-daemon.desktop systemunitdir = $(libdir)/systemd/user/ dist_systemunit_DATA = tym-daemon.service ================================================ FILE: README.md ================================================ # tym [![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) `tym` is a Lua-configurable terminal emulator base on [VTE](https://gitlab.gnome.org/GNOME/vte). ## Installation ### Arch Linux ``` $ yay -S tym ``` ### NixOS ``` $ nix-env -iA nixos.tym ``` ### Other distros Download the latest release from [Releases](https://github.com/endaaman/tym/releases), extract it and run as below ``` $ ./configure $ sudo make install ```
Build dependencies (click to open)

#### Arch Linux ``` $ sudo pacman -S vte3 lua53 ``` #### Ubuntu ``` $ sudo apt install libgtk-3-dev libvte-2.91-dev liblua5.3-dev libpcre2-dev ``` #### Void Linux ``` $ sudo xbps-install -S vte3-devel lua-devel ``` #### Other distros / macOS / Windows We did not check which packages are needed to build on other distros or OS. We are waiting for your contribution ;)

## Configuration If `$XDG_CONFIG_HOME/tym/config.lua` exists, it is executed when the app starts. You can change the path with the `--use`/`-u` option. ```lua -- At first, you need to require tym module local tym = require('tym') -- set individually tym.set('width', 100) tym.set('font', 'DejaVu Sans Mono 11') -- set by table tym.set_config({ shell = '/usr/bin/fish', cursor_shape = 'underline', autohide = true, color_foreground = 'red', }) ``` See [wiki](https://github.com/endaaman/tym/wiki) to check out the advanced examples. All available config values are shown below. | field name | type | default value | description | | --- | --- | --- | --- | | `shell` | string | `$SHELL` → `vte_get_user_shell()` → `'/bin/sh'` | Shell to execute. | | `term` | string | `'xterm-256color'` | Value of `$TERM`. | | `title` | string | `'tym'` | Initial window title. | | `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. | | `icon` | string | `'utilities-terminal'` | Name of icon. cf. [Icon Naming Specification](https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html) | | `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)) | | `cursor_shape` | string | `'block'` | `'block'`, `'ibeam'` or `'underline'` can be used. | | `cursor_blink_mode` | string | `'system'` | `'system'`, `'on'` or `'off'` can be used. | | `cjk_width` | string | `'narrow'` | `'narrow'` or `'wide'` can be used. | | `background_image` | string | `''` | Path to background image file. | | `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). | | `width` | integer | `80` | Initial columns. | | `height` | integer | `22` | Initial rows. | | `scale` | integer | `100` | Font scale in **percent(%)** | | `cell_width` | integer | `100` | Cell width scale in **percent(%)**. | | `cell_height` | integer | `100` | Cell height scale in **percent(%)**. | | `padding_top` | integer | `0` | Top padding. | | `padding_bottom` | integer | `0` | Bottom padding. | | `padding_left` | integer | `0` | Left padding. | | `padding_right` | integer | `0` | Right padding. | | `scrollback_length` | integer | `512` | Length of the scrollback buffer. | | `scrollback_on_output` | boolean | `true` | Whether to scroll the buffer when the new data is output. | | `ignore_default_keymap` | boolean | `false` | Whether to use default keymap. | | `autohide` | boolean | `false` | Whether to hide mouse cursor when the user presses a key. | | `silent` | boolean | `false` | Whether to beep when bell sequence is sent. | | `bold_is_bright` | boolean | `false` | Whether to make bold texts bright. | | `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. | | `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.| ## Theme customization When `$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/). ```lua local bg = '#161821' local fg = '#c6c8d1' return { color_background = bg, color_foreground = fg, color_bold = fg, color_cursor = fg, color_cursor_foreground = bg, color_highlight = fg, color_highlight_foreground = bg, color_0 = bg, color_1 = '#e27878', color_2 = '#b4be82', color_3 = '#e2a478', color_4 = '#84a0c6', color_5 = '#a093c7', color_6 = '#89b8c2', color_7 = fg, color_8 = '#6b7089', color_9 = '#e98989', color_10 = '#c0ca8e', color_11 = '#e9b189', color_12 = '#91acd1', color_13 = '#ada0d3', color_14 = '#95c4ce', color_15 = '#d2d4de', } ``` You need to return the color map as table.
Color correspondence (click to open)
``` color_0 : black (background) color_1 : red color_2 : green color_3 : brown color_4 : blue color_5 : purple color_6 : cyan color_7 : light gray (foreground) color_8 : gray color_9 : light red color_10 : light green color_11 : yellow color_12 : light blue color_13 : pink color_14 : light cyan color_15 : white ```
## Keymap ### Default keymap | Key | Action | | :-------------- | :--------------------------- | | Ctrl Shift c | Copy selection to clipboard. | | Ctrl Shift v | Paste from clipboard. | | Ctrl Shift r | Reload config file. | ### Customizing keymap You 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**. ```lua -- also can set keymap tym.set_keymap('o', function() local h = tym.get('height') tym.set('height', h + 1) tym.notify('Set window height :' .. h) end) -- set by table tym.set_keymaps({ ['t'] = function() tym.reload() tym.notify('reload config') end, ['v'] = function() -- reload and notify tym.send_key('t') end, ['y'] = function() tym.notify('Y has been pressed') return true -- notification is shown and `Y` will be inserted end, ['w'] = function() tym.notify('W has been pressed') -- notification is shown but `W` is not inserted end, }) ``` ## Lua API | Name | Return value | Description | | ------------------------------------ | ------------ | ----------- | | `tym.get(key)` | any | Get config value. | | `tym.set(key, value)` | void | Set config value. | | `tym.get_default_value(key)` | any | Get default config value. | | `tym.get_config()` | table | Get whole config. | | `tym.set_config(table)` | void | Set config by table. | | `tym.reset_config()` | void | Reset all config. | | `tym.set_keymap(accelerator, func)` | void | Set keymap. | | `tym.unset_keymap(accelerator)` | void | Unset keymap. | | `tym.set_keymaps(table)` | void | Set keymaps by table. | | `tym.reset_keymaps()` | void | Reset all keymaps. | | `tym.set_hook(hook_name, func)` | void | Set a hook. | | `tym.set_hooks(table)` | void | Set hooks. | | `tym.reload()` | void | Reload config file.| | `tym.reload_theme()` | void | Reload theme file. | | `tym.send_key()` | void | Send key press event. | | `tym.signal(id, hook, {param...})` | void | Send signal to the tym instance specified by id. | | `tym.set_timeout(func, interval=0)` | int(tag) | Set timeout. return true in func to execute again. | | `tym.clear_timeout(tag)` | void | Clear the timeout. | | `tym.put(text)` | void | Feed text. | | `tym.bell()` | void | Sound bell. | | `tym.open(uri)` | void | Open URI via your system default app like `xdg-open(1)`. | | `tym.notify(message, title='tym')` | void | Show desktop notification. | | `tym.copy(text, target='clipboard')` | void | Copy text to clipboard. As `target`, `'clipboard'`, `'primary'` or `secondary` can be used. | | `tym.copy_selection(target='clipboard')` | void | Copy current selection. | | `tym.paste(target='clipboard')` | void | Paste clipboard. | | `tym.check_mod_state(accelerator)` | bool | Check if the mod key(such as `''` or ``) is being pressed. | | `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). | | `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`. | | `tym.rgb_to_hex(r, g, b)` | string | Convert RGB bytes to 24bit HEX like `#ABCDEF`. | | `tym.hex_to_rgb(hex)` | r, g, b | Convert 24bit HEX like `#ABCDEF` to RGB bytes. | | `tym.get_monitor_model()` | string | Get monitor model on which the window is shown. | | `tym.get_cursor_position()` | int, int | Get where column and row the cursor is. | | `tym.get_clipboard(target='clipboard')` | string | Get content in the clipboard. | | `tym.get_selection()` | string | Get selected text. | | `tym.has_selection()` | bool | Get if selected. | | `tym.select_all()` | void | Select all texts. | | `tym.unselect_all()` | void | Unselect all texts. | | `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. | | `tym.get_config_path()` | string | Get full path to config file. | | `tym.get_theme_path()` | string | Get full path to theme file. | | `tym.get_terminal_pid()` | integer | Get terminal pid. | | `tym.get_pid()` | integer | Get child pid(usually shell's pid). | | `tym.get_ids()` | table[int] | Get tym instance ids. | | `tym.get_version()` | string | Get version string. | ### Hooks | Name | Param | Default action | Description | | --- | --- | --- | --- | | `title` | title | changes title | If string is returned, it will be used as the new title. | | `bell` | nil | makes the window urgent when it is inactive. | If true is returned, the window will not be urgent. | | `clicked` | button, uri | If URI exists under cursor, opens it | Triggered when mouse button is pressed. | | `scroll` | delta_x, delta_y, mouse_x, mouse_y | scroll buffer | Triggered when mouse wheel is scrolled. | | `drag` | filepath | feed filepath to the console | Triggered when files are dragged to the screen. | | `activated` | nil | nothing | Triggered when the window is activated. | | `deactivated` | nil | nothing | Triggered when the window is deactivated. | | `resized` | nil | nothing | Triggered when the window is resized. | | `selected` | string | nothing | Triggered when the text in the terminal screen is selected. | | `unselected` | nil | nothing | Triggered when the selection is unselected. | | `signal` | string | nothing | Triggered when `me.endaaman.tym.hook` signal is received. | If truthy value is returned in a callback function, the default action will be **stopped**. ```lua tym.set_hooks({ title = function(t) tym.set('title', 'tym - ' .. t) return true -- this is needed to cancenl default title application end, }) --- NOTE: -- If you set the hook to 'clicked' handler, you need to open URI manually like below, tym.set_hook('clicked', function(button, uri) print('you pressed button:', button) -- 1:left, 2:middle, 3:right -- open URI only by middle click if button == 2 then if uri then print('you clicked URI: ', uri) tym.open(uri) -- disable the default action 'put clipboard' when open URI return true end end end) ``` ## Interprocess communication using D-Bus Each 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` and the interface name `me.endaaman.tym`. ### Signals | Name | Input(D-Bus signature) | Description | | ---- | --- | --- | | `hook` | `s` | Triggers `signal` hook. | For example, when you prepare the following config and command, ```lua local tym = require('tym') tym.set_hook('signal', function (p) print('Hello from DBus signal') print('param:', p) end) ``` ``` $ dbus-send /me/endaaman/tym0 me.endaaman.tym.hook string:'THIS IS PARAM' ``` or ```lua tym.signal(0, 'hook', {'THIS IS PARAM'}) -- NOTICE: param must be table ``` you will get an output like below. ``` Hello from DBus signal param: THIS IS PARAM ``` Alternatively, you can use `tym` command to send signal. ``` $ tym --signal hook --dest 0 --param 'THIS IS PARAM' ``` If 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. ``` $ tym --signal hook --param 'THIS IS PARAM' ``` ### Methods | Name | Input (D-Bus signature) | Output (D-Bus signature) | Description | | ---- | --- | --- | --- | | `get_ids` | None | `ai` | Get all tym instance IDs. | | `echo` | `s` | `s` | Echo output the same as input. | | `eval` | `s` | `s` | Evaluate one line lua script. `return` is needed. | | `eval_file` | `s` | `s` | Evaluate a script file. `return` is needed. | | `exec` | `s` | None | Execute one line lua script without outputs. | | `eval_file` | `s` | None | Execute a script filt without outputs. | For example, when you exec the command, ``` $ 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")' ``` then you will get like below. ``` method return time=1646287109.007168 sender=:1.3633 -> destination=:1.3648 serial=39 reply_serial=2 string "title is tym" ``` As same as signals, you can use `tym` command to execute method calling. ``` $ tym --call eval --dest 0 --param 'return "title is " .. tym.get("title")' ``` Of course, `--dest` can be omitted as well. ## Options ### `--help` `-h` ``` $ tym -h ``` ### `--use=` `-u ` ``` $ tym --use=/path/to/config.lua ``` If `NONE` is provided, all config will be default (user-defined config file will not be loaded). ``` $ tym -u NONE ``` ### `--theme=` `-t ` ``` $ tym --use=/path/to/theme.lua ``` If `NONE` is provided, default theme will be used. ``` $ tym -t NONE ``` ### `--signal=` `-s ` ``` $ tym --signal hook ``` Sends a D-Bus signal to the current instance (determined by `$TYM_ID` environment value). To send to another instance, use `--dest` (or `-d`) option. ### `--call=` `-c ` Calls D-Bus method of the current instance (determined by `$TYM_ID` environment value). To call it of another instance, provide `--dest` (or `-d`) option. ``` $ tym --call eval --param 'return 1 + 2' ``` ### `--daemon` This makes tym a daemon process, which has no window or application context. ``` $ tym --daemon ``` To 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`. ### `--cwd=` This sets the terminal's working directory. `` must be an absolute path. If unspecified `tym` will use the current working directory of the terminal invocation. ```console $ tym --cwd=/home/user/projects ``` ### `--` You can set config value via command line option. ```console $ tym --shell=/bin/zsh --color_background=red --width=40 --ignore_default_keymap ``` ### `--isolated` ```console $ tym --isolated ``` This 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. ### `--` ("double dash" option) tym also accepts double dash `--` option as the command line to spawn. ```console $ tym -- less -N Dockerfile ``` ## Development Clone this repo and run as below ```console $ autoreconf -fvi $ ./configure --enable-debug $ make && ./src/tym -u ./path/to/config.lua # for debug $ make check; cat src/tym-test.log # for unit tests ``` Run tests in docker container ```console $ docker build -t tym . $ docker run tym ``` ## License MIT ================================================ FILE: autotools.mk ================================================ all: clean: rm -f configure Makefile.in config.h.in aclocal.m4 rm -f install-sh missing depcomp compile rm -rf autom4te.cache rm -f *~ rescan: autoscan reconfigure: autoreconf -i configure: configure.in aclocal.m4 Makefile.in config.h.in autoconf Makefile.in: Makefile.am config.h.in automake -a -c config.h.in: configure.in autoheader aclocal.m4: configure.in aclocal ================================================ FILE: configure.ac ================================================ m4_define([tym_major_version],[3]) m4_define([tym_minor_version],[5]) m4_define([tym_micro_version],[2]) m4_define([tym_version],[tym_major_version().tym_minor_version().tym_micro_version()]) AC_PREREQ([2.69]) AC_INIT([tym], [tym_version()], [], [tym], [https://github.com/endaaman/tym]) AM_INIT_AUTOMAKE([foreign]) DATE="`date '+%Y-%m-%d'`" AC_SUBST(DATE) AC_CONFIG_SRCDIR([src/tym.c]) AC_CONFIG_HEADERS([app-config.h], []) AC_CONFIG_FILES([ Makefile src/Makefile include/Makefile include/common.h tym-daemon.service tym.1 ]) AC_PROG_CC PKG_PROG_PKG_CONFIG PKG_CHECK_MODULES(TYM, [gtk+-3.0 vte-2.91 libpcre2-8]) AC_ARG_ENABLE(luajit, [AC_HELP_STRING([--enable-luajit], [use LuaJIT instead of the official Lua interpreter(default=no)])], [\ case "${enableval}" in yes) enable_luajit=yes ;; no) enable_luajit=no ;; *) AC_MSG_ERROR(bad value for --enable-luajit) ;; esac], [enable_luajit=no] ) if test x"${enable_luajit}" = x"yes"; then PKG_CHECK_MODULES(LUA, [luajit]) AC_DEFINE([USES_LUAJIT], 1, [Define to 1 to enable LuaJIT specific code]) else PKG_CHECK_MODULES(LUA, [lua], [], [ PKG_CHECK_MODULES(LUA, [lua5.3]) ]) fi AC_ARG_ENABLE(debug, [AC_HELP_STRING([--enable-debug],[turn on debugging(default=no)])], [\ case "${enableval}" in yes) enable_debug=yes ;; no) enable_debug=no ;; *) AC_MSG_ERROR(bad value for --enable-debug) ;; esac], [enable_debug=no] ) if test x"${enable_debug}" = x"yes"; then AC_DEFINE(DEBUG, 1, [Define to 1 if you want to debug]) fi AM_CONDITIONAL([DEBUG], [test "$enable_debug" = yes]) # --enable-old-vte AC_ARG_ENABLE(old-vte, [AC_HELP_STRING([--enable-old-vte], [use old VTE API(default=no)])], [\ case "${enableval}" in yes) enable_old_vte=yes ;; no) enable_old_vte=no ;; *) AC_MSG_ERROR(bad value for --enable-old-vte) ;; esac], [enable_old_vte=no] ) if test x"${enable_old_vte}" = x"yes"; then AC_DEFINE([TYM_USE_OLD_VTE], 1, [Define to 1 if using old VTE API]) fi AC_OUTPUT ================================================ FILE: include/Makefile.am ================================================ noinst_HEADERS = \ app.h \ builtin.h \ command.h \ common.h \ config.h \ context.h \ hook.h \ ipc.h \ keymap.h \ meta.h \ option.h \ property.h \ regex.h \ tym.h tym_test.h ================================================ FILE: include/app.h ================================================ /** * app.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef APP_H #define APP_H #include "context.h" #include "meta.h" #include "ipc.h" typedef struct { GApplication* gapp; Meta* meta; IPC* ipc; GList* contexts; bool is_isolated; } App; extern App* app; void app_init(); void app_close(); void app_quit_context(Context* context); int app_start(Option* option, int argc, char **argv); #endif ================================================ FILE: include/builtin.h ================================================ /** * builtin.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef BUILTIN_H #define BUILTIN_H #include "common.h" int builtin_register_module(lua_State* L); #endif ================================================ FILE: include/command.h ================================================ /** * commad.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef COMMAND_H #define COMMAND_H #include "common.h" #include "context.h" void command_reload(Context* context); void command_reload_theme(Context* context); void command_copy_selection(Context* context); void command_paste(Context* context); #endif ================================================ FILE: include/common.h.in ================================================ /** * common.h * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef COMMON_H #define COMMON_H #include #include #include #include #include #include #include #include #include #define PCRE2_CODE_UNIT_WIDTH 8 #include #include "../app-config.h" #define __PRE_IDENTITY(x) #x #define __IDENTITY(x) __PRE_IDENTITY(x) #define TYM_VTE_VERSION __IDENTITY(VTE_MAJOR_VERSION) "." __IDENTITY(VTE_MINOR_VERSION) "." __IDENTITY(VTE_MICRO_VERSION) #ifdef DEBUG #define TYM_APP_ID "me.endaaman.tym_dev" #define TYM_APP_ID_ISOLATED "me.endaaman.tym_isolated_dev" #define TYM_OBJECT_PATH_BASE "/me/endaaman/tym_dev" #else #define TYM_APP_ID "me.endaaman.tym" #define TYM_APP_ID_ISOLATED "me.endaaman.tym_isolated" #define TYM_OBJECT_PATH_BASE "/me/endaaman/tym" #endif #define TYM_OBJECT_PATH_FMT_INT TYM_OBJECT_PATH_BASE"%d" #define TYM_OBJECT_PATH_FMT_STR TYM_OBJECT_PATH_BASE"%s" #define TYM_ERROR_INVALID_METHOD_CALL 0 #define TYM_CONFIG_DIR_NAME "tym" #define TYM_CONFIG_FILE_NAME "config.lua" #define TYM_THEME_FILE_NAME "theme.lua" #define TYM_SYMBOL_NONE "NONE" #define TYM_FALL_BACK_SHELL "/bin/sh" #define TYM_SYMBOL_WILDCARD "*" #define TYM_CURSOR_SHAPE_BLOCK "block" #define TYM_CURSOR_SHAPE_IBEAM "ibeam" #define TYM_CURSOR_SHAPE_UNDERLINE "underline" #define TYM_CURSOR_BLINK_MODE_SYSTEM "system" #define TYM_CURSOR_BLINK_MODE_ON "on" #define TYM_CURSOR_BLINK_MODE_OFF "off" #define TYM_CJK_WIDTH_NARROW "narrow" #define TYM_CJK_WIDTH_WIDE "wide" #define TYM_CLIPBOARD_CLIPBOARD "clipborad" #define TYM_CLIPBOARD_PRIMARY "primary" #define TYM_CLIPBOARD_SECONDARY "secondary" #define TYM_DEFAULT_TITLE "tym" #define TYM_DEFAULT_ICON "utilities-terminal" #define TYM_DEFAULT_TERM "xterm-256color" #define TYM_DEFAULT_CURSOR_SHAPE TYM_CURSOR_SHAPE_BLOCK #define TYM_DEFAULT_CURSOR_BLINK_MODE TYM_CURSOR_BLINK_MODE_SYSTEM #define TYM_DEFAULT_CJK TYM_CJK_WIDTH_NARROW #define TYM_DEFAULT_URI_SCHEMES "http https file mailto" extern const int TYM_DEFAULT_WIDTH; extern const int TYM_DEFAULT_HEIGHT; extern const int TYM_DEFAULT_SCALE; extern const int TYM_DEFAULT_CELL_SIZE; extern const int TYM_DEFAULT_SCROLLBACK; /* theme: iceberg (https://cocopon.github.io/iceberg.vim/) */ #define TYM_DEFAULT_COLOR_0 "#161821" #define TYM_DEFAULT_COLOR_1 "#e27878" #define TYM_DEFAULT_COLOR_2 "#b4be82" #define TYM_DEFAULT_COLOR_3 "#e2a478" #define TYM_DEFAULT_COLOR_4 "#84a0c6" #define TYM_DEFAULT_COLOR_5 "#a093c7" #define TYM_DEFAULT_COLOR_6 "#89b8c2" #define TYM_DEFAULT_COLOR_7 "#c6c8d1" #define TYM_DEFAULT_COLOR_8 "#6b7089" #define TYM_DEFAULT_COLOR_9 "#e98989" #define TYM_DEFAULT_COLOR_10 "#c0ca8e" #define TYM_DEFAULT_COLOR_11 "#e9b189" #define TYM_DEFAULT_COLOR_12 "#91acd1" #define TYM_DEFAULT_COLOR_13 "#ada0d3" #define TYM_DEFAULT_COLOR_14 "#95c4ce" #define TYM_DEFAULT_COLOR_15 "#d2d4de" #define TYM_DEFAULT_COLOR_BACKGROUND TYM_DEFAULT_COLOR_0 #define TYM_DEFAULT_COLOR_FOREGROUND TYM_DEFAULT_COLOR_7 #define UNUSED(x) (void)(x) #define BUILD_DATE "@DATE@" /* compat definition */ #ifndef LUA_LOADED_TABLE #define LUA_LOADED_TABLE "_LOADED" #endif /* use g_memdup2 if glib >= 2.66 */ #if GLIB_CHECK_VERSION(2, 66, 0) #define memdup g_memdup2 #else #define memdup g_memdup #endif /* Switch to use old api */ /* #define TYM_USE_OLD_API */ #ifndef TYM_USE_OLD_API /* START: TYM_USE_OLD_VTE */ #if GDK_MAJOR_VERSION == 3 #if GDK_MINOR_VERSION >= 20 #define TYM_USE_GDK_SEAT #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 48 #define TYM_USE_VTE_SPAWN_ASYNC #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 50 #define TYM_USE_VTE_COPY_CLIPBOARD_FORMAT #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 46 #define TYM_USE_VTE_COLOR_CURSOR_FOREGROUND #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 52 #define TYM_USE_TRANSPARENT #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 62 #define TYM_USE_SIXEL #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 72 #define TYM_USE_VTE_GET_TEXT_RANGE_FORMAT #endif #endif #if VTE_MAJOR_VERSION == 0 #if VTE_MINOR_VERSION >= 78 #define TYM_USE_VTE_TERMPROP #endif #endif #endif /* END: TYM_USE_OLD_VTE */ #ifdef DEBUG /* START: DEBUG */ #define dd( fmt, ... ) \ g_print("[%-10s:%3u I] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define dw( fmt, ... ) \ g_print("[%-10s:%3u W] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) #define df( fmt, ... ) \ g_print("[%-10s:%3u F] %s()\n", __FILE__, __LINE__, __func__) void debug_dump_stack(lua_State* L, char* file, unsigned line); #define ds(lua_state) \ debug_dump_stack((lua_state), __FILE__, __LINE__) #else /* ELSE: DEBUG */ #define dd(...) ((void)0) #define dw(...) ((void)0) #define df(...) ((void)0) #define ds(...) ((void)0) #endif /* END: DEBUG */ int roundup(double x); bool is_equal(const char* a, const char* b); bool is_none(const char* s); bool is_empty(const char* s); void luaX_requirec(lua_State* L, const char* modname, lua_CFunction openf, int glb, void* userdata); int luaX_warn(lua_State* L, const char* fmt, ...); #endif ================================================ FILE: include/config.h ================================================ /** * config.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef CONFIG_H #define CONFIG_H #include "common.h" #include "option.h" #include "meta.h" typedef struct { GHashTable* data; bool locked; } Config; Config* config_init(); void config_close(Config* config); void config_restore_default(Config* config, Meta* meta); const char* config_get_str(Config* config, const char* key); void config_set_str(Config* config, const char* key, const char* value); int config_get_int(Config* config, const char* key); void config_set_int(Config* config, const char* key, int value); bool config_get_bool(Config* config, const char* key); void config_set_bool(Config* config, const char* key, bool value); VteCursorShape config_get_cursor_shape(Config* config); VteCursorBlinkMode config_get_cursor_blink_mode(Config* config); unsigned config_get_cjk_width(Config* config); #endif ================================================ FILE: include/context.h ================================================ /** * context.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef CONTEXT_H #define CONTEXT_H #include "common.h" #include "config.h" #include "hook.h" #include "keymap.h" #include "option.h" typedef struct { GtkWindow* window; VteTerminal* vte; GtkBox* hbox; GtkBox* vbox; int uri_tag; bool alpha_supported; } Layout; typedef struct { void* object; int handler_id; } HandlerTag; typedef struct { int id; bool config_loading; bool initialized; char* object_path; int registration_id; int child_pid; GList* handler_tags; Option* option; Config* config; Keymap* keymap; Hook* hook; GdkDevice* device; lua_State* lua; Layout layout; } Context; #define context_signal_connect(context, instance, detailed_signal, c_handler) {\ context_add_handler_tag(context, instance, g_signal_connect(instance, detailed_signal, c_handler, context)); \ } Context* context_init(int id, Option* option); // void context_dispose_only(Context* context); void context_close(Context* context); void context_add_handler_tag(Context* context, void* object, int handler_id); void context_load_device(Context* context); void context_load_lua_context(Context* context); void context_log_message(Context* context, bool notify, const char* fmt, ...); void context_log_warn(Context* context, bool notify, const char* fmt, ...); void context_restore_default(Context* context); void context_override_by_option(Context* context); char* context_acquire_config_path(Context* context); char* context_acquire_theme_path(Context* context); void context_load_config(Context* context); void context_load_theme(Context* context); bool context_perform_keymap(Context* context, unsigned key, GdkModifierType mod); void context_handle_signal(Context* context, const char* signal_name, GVariant* parameters); void context_build_layout(Context* context); void context_notify(Context* context, const char* body, const char* title); void context_launch_uri(Context* context, const char* uri); GdkWindow* context_get_gdk_window(Context* context); const char* context_get_str(Context* context, const char* key); int context_get_int(Context* context, const char* key); bool context_get_bool(Context* context, const char* key); void context_set_str(Context* context, const char* key, const char* value); void context_set_int(Context* context, const char* key, int value); void context_set_bool(Context* context, const char* key, bool value); void context_resize(Context* context, int width, int height); #endif ================================================ FILE: include/hook.h ================================================ /** * hook.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef HOOK_H #define HOOK_H #include "common.h" typedef struct { GHashTable* refs; } Hook; Hook* hook_init(); void hook_close(Hook* hook); bool hook_set_ref(Hook* hook, const char* key, int ref, int* old_ref); bool hook_perform_title(Hook* hook, lua_State* L, const char* title, bool* result); bool hook_perform_bell(Hook* hook, lua_State* L, bool* result); bool hook_perform_clicked(Hook* hook, lua_State* L, int button, const char* uri, bool* result); bool hook_perform_scroll(Hook* hook, lua_State* L, double delta_x, double delta_y, double x, double y, bool* result); bool hook_perform_drag(Hook* hook, lua_State* L, char* path, bool* result); bool hook_perform_activated(Hook* hook, lua_State* L); bool hook_perform_deactivated(Hook* hook, lua_State* L); bool hook_perform_selected(Hook* hook, lua_State* L, const char* text); bool hook_perform_unselected(Hook* hook, lua_State* L); bool hook_perform_resized(Hook* hook, lua_State* L); bool hook_perform_signal(Hook* hook, lua_State* L, const char* param); #endif ================================================ FILE: include/ipc.h ================================================ /** * ipc.h * * Copyright (c) 2022 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef IPC_H #define IPC_H #include "context.h" typedef struct { GHashTable* signals; GHashTable* methods; } IPC; IPC* ipc_init(); void ipc_close(IPC* ipc); bool ipc_signal_perform(IPC* ipc, Context* context, const char* signal_name, GVariant* parameters); bool ipc_method_perform(IPC* ipc, Context* context, const char* method_name, GVariant* parameters, GDBusMethodInvocation* invocation); #endif ================================================ FILE: include/keymap.h ================================================ /** * keymap.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef KEYMAP_H #define KEYMAP_H #include "common.h" typedef struct { GList* entries; } Keymap; Keymap* keymap_init(); void keymap_close(Keymap* keymap); void keymap_reset(Keymap* keymap); bool keymap_add_entry(Keymap* keymap, const char* accelerator, int ref); bool keymap_remove_entry(Keymap* keymap, const char* accelerator); bool keymap_perform(Keymap* keymap, lua_State* L, unsigned key, GdkModifierType mod, bool* result, char** error); #endif ================================================ FILE: include/meta.h ================================================ /** * meta.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef META_H #define META_H #include "common.h" typedef void (*MetaCallback) (void); typedef enum { META_ENTRY_TYPE_STRING = 0, META_ENTRY_TYPE_INTEGER = 1, META_ENTRY_TYPE_BOOLEAN = 2, META_ENTRY_TYPE_NONE = 3, // not actual, only shown in help } MetaEntryType; typedef struct { char* name; char short_name; MetaEntryType type; GOptionFlags option_flag; void* default_value; char* arg_desc; char* desc; MetaCallback getter; MetaCallback setter; bool is_theme; unsigned index; } MetaEntry; typedef struct { GHashTable* data; GList* list; } Meta; Meta* meta_init(); void meta_close(Meta* meta); unsigned meta_size(Meta* meta); MetaEntry* meta_get_entry(Meta* meta, const char* key); GOptionEntry* meta_get_option_entries(Meta* meta); #endif ================================================ FILE: include/option.h ================================================ /** * option.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef OPTION_H #define OPTION_H #include "common.h" #include "meta.h" typedef struct { GOptionContext* option_context; GOptionEntry* entries; char** rest_argv; GHashTable* entries_as_table; } Option; void* option_get(Option* option, const char* key); Option* option_init(GOptionEntry* entries); void option_close(Option* option); bool option_parse(Option* option, int argc, char** argv); char* option_get_str(Option* option, const char* key); int option_get_int(Option* option, const char* key); bool option_get_bool(Option* option, const char* key); #endif ================================================ FILE: include/property.h ================================================ /** * property.h * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef PROPERTY_H #define PROPERTY_H #include "common.h" #include "context.h" typedef int (*PropertyIntGetter)(Context* context, const char* key); typedef const char* (*PropertyStrGetter)(Context* context, const char* key); typedef bool (*PropertyBoolGetter)(Context* context, const char* key); typedef void (*PropertyIntSetter)(Context* context, const char* key, int value); typedef void (*PropertyStrSetter)(Context* context, const char* key, const char* value); typedef void (*PropertyBoolSetter)(Context* context, const char* key, bool value); typedef void (*PropertyIntSetterWithExtra)(Context* context, const char* key, int value, void* extra); typedef void (*PropertyStrSetterWithExtra)(Context* context, const char* key, const char* value, void* extra); typedef void (*PropertyBoolSetterWithExtra)(Context* context, const char* key, bool value, void* extra); // str void setter_shell(Context* context, const char* key, const char* value); void setter_term(Context* context, const char* key, const char* value); const char* getter_title(Context* context, const char* key); void setter_title(Context* context, const char* key, const char* value); const char* getter_font(Context* context, const char* key); void setter_font(Context* context, const char* key, const char* value); const char* getter_icon(Context* context, const char* key); void setter_icon(Context* context, const char* key, const char* value); const char* getter_role(Context* context, const char* key); void setter_role(Context* context, const char* key, const char* value); const char* getter_cursor_shape(Context* context, const char* key); void setter_cursor_shape(Context* context, const char* key, const char* value); const char* getter_cursor_blink_mode(Context* context, const char* key); void setter_cursor_blink_mode(Context* context, const char* key, const char* value); const char* getter_cjk_width(Context* context, const char* key); void setter_cjk_width(Context* context, const char* key, const char* value); void setter_background_image(Context* context, const char* key, const char* value); void setter_uri_schemes(Context* context, const char* key, const char* value); // int int getter_width(Context* context, const char* key); void setter_width(Context* context, const char* key, int value); int getter_height(Context* context, const char* key); void setter_height(Context* context, const char* key, int value); int getter_scale(Context* context, const char* key); void setter_scale(Context* context, const char* key, int value); int getter_cell_width(Context* context, const char* key); void setter_cell_width(Context* context, const char* key, int value); int getter_cell_height(Context* context, const char* key); void setter_cell_height(Context* context, const char* key, int value); /* DEPRECATED START */ void setter_padding_horizontal(Context* context, const char* key, int value); void setter_padding_vertical(Context* context, const char* key, int value); /* DEPRECATED END */ int getter_padding_top(Context* context, const char* key); int getter_padding_bottom(Context* context, const char* key); int getter_padding_left(Context* context, const char* key); int getter_padding_right(Context* context, const char* key); void setter_padding_top(Context* context, const char* key, int value); void setter_padding_bottom(Context* context, const char* key, int value); void setter_padding_left(Context* context, const char* key, int value); void setter_padding_right(Context* context, const char* key, int value); int getter_scrollback_length(Context* context, const char* key); void setter_scrollback_length(Context* context, const char* key, int value); // bool bool getter_scroll_on_output(Context* context, const char* key); void setter_scroll_on_output(Context* context, const char* key, bool value); bool getter_silent(Context* context, const char* key); void setter_silent(Context* context, const char* key, bool value); bool getter_autohide(Context* context, const char* key); void setter_autohide(Context* context, const char* key, bool value); bool gettter_bold_is_bright(Context* context, const char* key); void setter_bold_is_bright(Context* context, const char* key, bool value); // color void setter_color_normal(Context* context, const char* key, const char* value); void setter_color_window_background(Context* context, const char* key, const char* value); void setter_color_background(Context* context, const char* key, const char* value); void setter_color_foreground(Context* context, const char* key, const char* value); void setter_color_bold(Context* context, const char* key, const char* value); void setter_color_cursor(Context* context, const char* key, const char* value); void setter_color_cursor_foreground(Context* context, const char* key, const char* value); void setter_color_highlight(Context* context, const char* key, const char* value); void setter_color_highlight_foreground(Context* context, const char* key, const char* value); #endif ================================================ FILE: include/regex.h ================================================ /** * regex.h * * URI regular expression in PCRE2 * reference: RFC 3986 Appendix A (https://tools.ietf.org/html/rfc3986#appendix-A) * * NOTE: * * URI is repleced by SCHEMELESS_URI, because the entire URI regex is dynamically constructed * using an user-configured list of target schemes. SCHEME is used to validate the list. * * SUB_DELIMS does not contain "(" and ")", and subsequently USERINFO, IPVFUTURE, and REG_NAME do not allow these * characters. SEGMENT and QUERY rules have special PAREN rules that matches only paired "(" and ")". * * Also, the following definitions are omitted: * - URI-reference only used in relative URIs. * - relative-ref (as above) * - relative-part (as above) * - path-noscheme (as above) * - segment-nz-nc (as above) * - absolute-URI special case of URI. not distinguishable in regex. * - path not referenced from any other rules. * - path-empty to avoid highlighting meaningless URI like `foo:`. * - gen-delims not referenced from any other rules. * - reserved not referenced from any other rules. * * Copyright (c) 2020 endaaman, iTakeshi * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef REGEX_H #define REGEX_H /* * inherited from RFC 2234 Section 6.1 (https://tools.ietf.org/html/rfc2234#section-6.1) * NOTE: the difinitions for ALPHA and HEXDIG assume those are used in a case-insensitive manner. */ #define ALPHA "(?:" "[a-z]" ")" #define DIGIT "(?:" "[0-9]" ")" #define HEXDIG "(?:" DIGIT "|" "[a-f]" ")" #define SP "(?:" " " ")" /* * rules to validate the user-configured scheme list */ #define SCHEME "(" ALPHA "(?:" ALPHA "|" DIGIT "|" "[\\+\\-\\.]" ")*" ")" #define SCHEME_LIST SCHEME "(?:" SP SCHEME ")*" /* * main rules */ #define SCHEMELESS_URI "(?:" ":" HIER_PART "(?:" "\\?" QUERY ")?" "(?:" "\\#" QUERY ")?" ")" #define HIER_PART "(?:" "\\/\\/" AUTHORITY PATH_ABEMPTY "|" PATH_ABSOLUTE "|" PATH_ROOTLESS ")" #define AUTHORITY "(?:" USERINFO "@" ")?" HOST "(?:" ":" PORT ")?" #define USERINFO "(?:" UNRESERVED "|" PCT_ENCODED "|" SUB_DELIMS "|" ":" ")*+" #define HOST "(?:" IP_LITERAL "|" IPV4ADDRESS "|" REG_NAME")" #define PORT "(?:" DIGIT ")*+" #define IP_LITERAL "(?:" "\\[" "(?:" IPV6ADDRESS "|" IPVFUTURE ")" "\\]" ")" #define IPVFUTURE "(?:" "v" HEXDIG "++" "\\." "(?:" UNRESERVED "|" SUB_DELIMS "|" ":" ")++" ")" #define IPV6ADDRESS "(?:" "(?:" H16 ":" "){6}" LS32 \ "|" "::" "(?:" H16 ":" "){5}" LS32 \ "|" "(?:" H16 ")?" "::" "(?:" H16 ":" "){4}" LS32 \ "|" "(?:" "(?:" H16 ":" "){0,1}" H16 ")?" "::" "(?:" H16 ":" "){3}" LS32 \ "|" "(?:" "(?:" H16 ":" "){0,2}" H16 ")?" "::" "(?:" H16 ":" "){2}" LS32 \ "|" "(?:" "(?:" H16 ":" "){0,3}" H16 ")?" "::" "(?:" H16 ":" "){1}" LS32 \ "|" "(?:" "(?:" H16 ":" "){0,4}" H16 ")?" "::" LS32 \ "|" "(?:" "(?:" H16 ":" "){0,5}" H16 ")?" "::" H16 \ "|" "(?:" "(?:" H16 ":" "){0,6}" H16 ")?" "::" \ ")" #define H16 "(?:" HEXDIG "){1,4}" #define LS32 "(?:" H16 ":" H16 "|" IPV4ADDRESS ")" #define IPV4ADDRESS "(?:" DEC_OCTET "\\." DEC_OCTET "\\." DEC_OCTET "\\." DEC_OCTET ")" #define DEC_OCTET "(?:" DIGIT "|" "[1-9]" DIGIT "|" "1" DIGIT DIGIT "|" "2" "[0-4]" DIGIT "|" "25" "[0-5]" ")" #define REG_NAME "(?:" UNRESERVED "|" PCT_ENCODED "|" SUB_DELIMS ")*+" #define PATH_ABEMPTY "(?:" "\\/" SEGMENT ")*+" #define PATH_ABSOLUTE "(?:" "\\/" PATH_ROOTLESS "?" ")" #define PATH_ROOTLESS "(?:" SEGMENT_NZ PATH_ABEMPTY ")" #define SEGMENT "(?:" PCHAR "|" PAREN ")*+" #define SEGMENT_NZ "(?:" PCHAR "|" PAREN ")++" #define PCHAR "(?:" UNRESERVED "|" PCT_ENCODED "|" SUB_DELIMS "|" "[:@]" ")" #define PAREN "(?:" "\\(" PCHAR "*+" "\\)" ")" #define QUERY "(?:" PCHAR "|" "[\\/\\?]" "|" QUERY_PAREN ")*+" #define QUERY_PAREN "(?:" "\\(" "(?:" PCHAR "|" "[\\/\\?]" ")*+" "\\)" ")" #define PCT_ENCODED "(?:" "%" HEXDIG HEXDIG ")" #define UNRESERVED "(?:" ALPHA "|" DIGIT "|" "[\\-\\._~]" ")" #define SUB_DELIMS "(?:" "[!\\$&'\\*\\+,;=]" ")" #endif ================================================ FILE: include/tym.h ================================================ /** * tym.h * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef TYM_H #define TYM_H #include "app.h" #endif ================================================ FILE: include/tym_test.h ================================================ /** * tym.h * * Copyright (c) 2020 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #ifndef TYM_TEST_H #define TYM_TEST_H #include "common.h" void test_config(); void test_option(); void test_regex(); #endif ================================================ FILE: lua/e2e.lua ================================================ local tym = require('tym') print('start') tym.set_timeout(function() print('done') tym.quit() end, 1000) ================================================ FILE: scripts/bundle.sh ================================================ #!/bin/bash set -eux p=$(dirname "$0") bash $p/cleanup.sh autoreconf -fvi ./configure make clean make make check make dist ================================================ FILE: scripts/cleanup.sh ================================================ #!/bin/bash set -eux rm -f Makefile rm -f Makefile.in rm -f aclocal.m4 rm -f app-config.h rm -f app-config.h.in rm -f compile rm -f config.guess rm -f config.h rm -f config.h.in rm -f config.h.in~ rm -f config.log rm -f config.status rm -f config.sub rm -f configure rm -f depcomp rm -f install-sh rm -f libtool rm -f missing rm -f stamp-h1 rm -rf autom4te.cache/ rm -rf aux-dist/ rm -rf m4/ rm -f src/*.la rm -f src/*.lo rm -f src/*.log rm -f src/*.o rm -f src/*.trs rm -f src/Makefile rm -f src/Makefile.in rm -f src/tym rm -f src/tym-test rm -rf src/.deps/ rm -f include/Makefile rm -f include/Makefile.in rm -f include/common.h rm -f tym.1 rm -f tym-daemon.service rm -f test-driver echo 'Cleaned files.' ================================================ FILE: scripts/refresh.sh ================================================ #!/bin/bash set -eux p=$(dirname "$0") bash $p/cleanup.sh autoreconf -fvi ./configure --enable-debug ================================================ FILE: src/Makefile.am ================================================ if DEBUG ENV_OPT=-g3 -O0 else ENV_OPT=-O3 endif COMMON_CFLAGS = \ $(ENV_OPT) \ -std=c11 \ -Wall \ -Wextra \ -Wno-unused-parameter \ -Wno-sign-compare \ -Wno-pointer-sign \ -Wno-missing-field-initializers \ -Wformat=2 \ -Wstrict-aliasing=2 \ -Wdisabled-optimization \ -Wfloat-equal \ -Wpointer-arith \ -Wbad-function-cast \ -Wcast-align \ -Wredundant-decls \ -Wformat-security \ -Winline \ -I$(top_srcdir)/include bin_PROGRAMS = tym tym_SOURCES = \ app.c \ builtin.c \ command.c \ common.c \ config.c \ context.c \ hook.c \ ipc.c \ keymap.c \ meta.c \ option.c \ property.c \ tym.c tym_LDADD = $(TYM_LIBS) $(LUA_LIBS) tym_CFLAGS = $(COMMON_CFLAGS) $(TYM_CFLAGS) $(LUA_CFLAGS) TESTS = tym-test check_PROGRAMS = tym-test tym_test_SOURCES = \ app.c \ builtin.c \ command.c \ common.c \ config.c \ context.c \ hook.c \ ipc.c \ keymap.c \ meta.c \ option.c \ property.c \ config_test.c \ option_test.c \ regex_test.c \ tym_test.c tym_test_LDADD = $(TYM_LIBS) $(LUA_LIBS) tym_test_CFLAGS = $(COMMON_CFLAGS) $(TYM_CFLAGS) $(LUA_CFLAGS) ================================================ FILE: src/app.c ================================================ /** * app.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "app.h" App* app = NULL; int on_local_options(GApplication* gapp, GVariantDict* values, void* user_data); int on_command_line(GApplication* app, GApplicationCommandLine* cli, void* user_data); void app_init() { df(); app = g_new0(App, 1); app->meta = meta_init(); app->ipc = ipc_init(); } void app_close() { df(); for (GList* li = app->contexts; li != NULL; li = li->next) { Context* c = (Context*)li->data; context_close(c); } g_application_quit(app->gapp); g_object_unref(app->gapp); meta_close(app->meta); ipc_close(app->ipc); g_free(app); } static char* _get_dest_path_from_option(Option* option) { char* path = NULL; char* dest = option_get_str(option, "dest"); if (dest) { path = g_strdup_printf(TYM_OBJECT_PATH_FMT_STR, dest); } else { char** env = g_get_environ(); const char* dest_str = g_environ_getenv(env, "TYM_ID"); if (!dest_str) { return NULL; } path = g_strdup_printf(TYM_OBJECT_PATH_FMT_STR, dest_str); } return path; } static int _perform_signal(char* dest_path, char* signal_name, char* method_name, char* param) { GError* error = NULL; GDBusConnection* conn = g_application_get_dbus_connection(app->gapp); if (!dest_path) { g_warning("--dest is not provided and $TYM_ID is not set."); return 1; } GVariant* params = param ? g_variant_new("(s)", param) : g_variant_new("()"); /* process signal */ if (signal_name) { g_dbus_connection_emit_signal(conn, NULL, dest_path, TYM_APP_ID, signal_name, params, &error); g_print("Sent signal:%s to path:%s interface:%s\n", signal_name, dest_path, TYM_APP_ID); g_free(signal_name); if (error) { g_error("%s", error->message); g_error_free(error); } return 0; } /* process method call */ GVariant* result = g_dbus_connection_call_sync( conn, // conn TYM_APP_ID, // bus_name dest_path, // object_path TYM_APP_ID, // interface_name method_name, // method_name params, // parameters NULL, // reply_type G_DBUS_CALL_FLAGS_NONE, // flags 1000, // timeout NULL, // cancellable &error ); g_print("Call method:%s on path:%s interface:%s\n", method_name, dest_path, TYM_APP_ID); if (error) { g_warning("%s", error->message); g_error_free(error); return 1; } dd("result type:%s", g_variant_get_type_string(result)); char* msg = g_variant_print(result, true); g_print("%s\n", msg); g_free(msg); return 0; } int app_start(Option* option, int argc, char **argv) { df(); g_assert(!app->gapp); GApplicationFlags flags = G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_SEND_ENVIRONMENT; char* app_id = TYM_APP_ID; if (option_get_bool(option, "isolated")) { flags |= G_APPLICATION_NON_UNIQUE; app_id = TYM_APP_ID_ISOLATED; } app->gapp = G_APPLICATION(gtk_application_new(app_id, flags)); GError* error = NULL; g_application_register(app->gapp, NULL, &error); g_signal_connect(app->gapp, "handle-local-options", G_CALLBACK(on_local_options), option); g_signal_connect(app->gapp, "command-line", G_CALLBACK(on_command_line), NULL); return g_application_run(app->gapp, argc, argv); } static int _contexts_sort_func(const void* a, const void* b) { return ((Context*)a)->id - ((Context*)b)->id; } Context* app_spawn_context(Option* option) { df(); unsigned index = 0; int ordered_id = option_get_int(option, "id"); if (ordered_id) { for (GList* li = app->contexts; li != NULL; li = li->next) { Context* c = (Context*)li->data; if (c->id == ordered_id) { context_log_warn(c, true, "id=%d has been already acquired.", ordered_id); return NULL; } } index = ordered_id; } else { for (GList* li = app->contexts; li != NULL; li = li->next) { Context* c = (Context*)li->data; /* scanning from 0 and if find first ctx that is not continus from 0, the index is new index. */ if (c->id != index) { break; } index += 1; } } Context* context = context_init(index, option); app->contexts = g_list_insert_sorted(app->contexts, context, _contexts_sort_func); g_application_hold(app->gapp); context_log_message(context, false, "Started."); return context; } void app_quit_context(Context* context) { df(); g_application_release(app->gapp); GDBusConnection* conn = g_application_get_dbus_connection(app->gapp); g_dbus_connection_unregister_object(conn, context->registration_id); context_log_message(context, false, "Quit."); app->contexts = g_list_remove(app->contexts, context); context_close(context); } static void on_vte_drag_data_received( VteTerminal* vte, GdkDragContext* drag_context, int x, int y, GtkSelectionData* data, unsigned int info, unsigned int time, void* user_data) { Context* context = (Context*)user_data; if (!data || gtk_selection_data_get_format(data) != 8) { return; } gchar** uris = g_uri_list_extract_uris(gtk_selection_data_get_data(data)); if (!uris) { return; } GRegex* regex = g_regex_new("'", 0, 0, NULL); for (gchar** p = uris; *p; ++p) { gchar* file_path = g_filename_from_uri(*p, NULL, NULL); if (file_path) { bool result; if (!(hook_perform_drag(context->hook, context->lua, file_path, &result) && result)) { gchar* path_escaped = g_regex_replace(regex, file_path, -1, 0, "'\\\\''", 0, NULL); gchar* path_wrapped = g_strdup_printf("'%s' ", path_escaped); vte_terminal_feed_child(vte, path_wrapped, strlen(path_wrapped)); g_free(path_escaped); g_free(path_wrapped); } g_free(file_path); } } g_regex_unref(regex); } static bool on_vte_key_press(GtkWidget* widget, GdkEventKey* event, void* user_data) { Context* context = (Context*)user_data; unsigned mod = event->state & gtk_accelerator_get_default_mod_mask(); unsigned key = gdk_keyval_to_lower(event->keyval); if (context_perform_keymap(context, key, mod)) { return true; } return false; } static bool on_vte_mouse_scroll(GtkWidget* widget, GdkEventScroll* e, void* user_data) { Context* context = (Context*)user_data; bool result = false; if (hook_perform_scroll(context->hook, context->lua, e->delta_x, e->delta_y, e->x, e->y, &result) && result) { return true; } return false; } static void on_vte_child_exited(VteTerminal* vte, int status, void* user_data) { df(); Context* context = (Context*)user_data; gtk_window_close(context->layout.window); app_quit_context(context); } static void on_vte_title_changed(VteTerminal* vte, void* user_data) { df(); Context* context = (Context*)user_data; GtkWindow* window = context->layout.window; bool result = false; #ifdef TYM_USE_VTE_TERMPROP const char* title = vte_terminal_get_termprop_string(context->layout.vte, "xterm.title", NULL); #else const char* title = vte_terminal_get_window_title(context->layout.vte); #endif if (hook_perform_title(context->hook, context->lua, title, &result) && result) { return; } if (title) { gtk_window_set_title(window, title); } } static void on_vte_bell(VteTerminal* vte, void* user_data) { df(); Context* context = (Context*)user_data; bool result = false; if (hook_perform_bell(context->hook, context->lua, &result) && result) { return; } GtkWindow* window = context->layout.window; if (!gtk_window_is_active(window)) { gtk_window_set_urgency_hint(window, true); } } static bool on_vte_click(VteTerminal* vte, GdkEventButton* event, void* user_data) { df(); Context* context = (Context*)user_data; char* uri = NULL; if (context->layout.uri_tag >= 0) { uri = vte_terminal_match_check_event(vte, (GdkEvent*)event, NULL); } bool result = false; if (hook_perform_clicked(context->hook, context->lua, event->button, uri, &result)) { if (result) { return true; } return false; } if (uri) { for (int i = strlen(uri) - 1; uri[i] == '.' || uri[i] == ','; i--) { uri[i] = '\0'; } context_launch_uri(context, uri); return true; } return false; } static void on_vte_selection_changed(GtkWidget* widget, void* user_data) { df(); Context* context = (Context*)user_data; if (!vte_terminal_get_has_selection(context->layout.vte)) { hook_perform_unselected(context->hook, context->lua); return; } GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_PRIMARY); char* text = gtk_clipboard_wait_for_text(cb); hook_perform_selected(context->hook, context->lua, text); } static void on_vte_resize_request(GtkWidget* widget, unsigned int width, unsigned int height, void* user_data) { Context* context = (Context*)user_data; dd("Recieve resize sequence: width=%d height=%d", width, height); context_resize(context, width, height); } static gboolean on_window_close(GtkWidget* widget, cairo_t* cr, void* user_data) { df(); // close context in child-exited handler return true; } static bool on_window_focus_in(GtkWindow* window, GdkEvent* event, void* user_data) { Context* context = (Context*)user_data; gtk_window_set_urgency_hint(window, false); hook_perform_activated(context->hook, context->lua); return false; } static bool on_window_focus_out(GtkWindow* window, GdkEvent* event, void* user_data) { Context* context = (Context*)user_data; hook_perform_deactivated(context->hook, context->lua); return false; } static gboolean on_window_draw(GtkWidget* widget, cairo_t* cr, void* user_data) { Context* context = (Context*)user_data; const char* value = context_get_str(context, "color_window_background"); if (is_none(value)) { return false; } GdkRGBA color = {}; if (gdk_rgba_parse(&color, value)) { if (context->layout.alpha_supported) { cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha); } else { cairo_set_source_rgb(cr, color.red, color.green, color.blue); } cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_paint(cr); } return false; } static void on_window_resize(GtkWidget* widget, GtkAllocation* allocation, gpointer user_data) { Context* context = (Context*)user_data; hook_perform_resized(context->hook, context->lua); } void on_dbus_signal( GDBusConnection* conn, const char* sender_name, const char* object_path, const char* interface_name, const char* signal_name, GVariant* params, void* user_data) { Context* context = (Context*)user_data; dd("DBus signal received"); dd("\tcontext id: %d", context->id); dd("\tsender_name: %s", sender_name); dd("\tobject_path: %s", object_path); dd("\tinterface_name: %s", interface_name); dd("\tsignal_name: %s", signal_name); if (ipc_signal_perform(app->ipc, context, signal_name, params)) { context_log_message(context, false, "Signal received:`%s` object_path:`%s`", signal_name, object_path); return; } context_log_warn(context, true, "Unsupported signal: `%s`", signal_name); } void on_dbus_call_method( GDBusConnection* conn, const gchar* sender_name, const gchar* object_path, const gchar* interface_name, const gchar* method_name, GVariant* params, GDBusMethodInvocation* invocation, gpointer user_data) { Context* context = (Context*)user_data; dd("DBus method call"); dd("\tcontext id: %d", context->id); dd("\tsender_name: %s", sender_name); dd("\tobject_path: %s", object_path); dd("\tinterface_name: %s", interface_name); dd("\tmethod_name: %s", method_name); if (ipc_method_perform(app->ipc, context, method_name, params, invocation)) { context_log_message(context, false, "Method call:`%s` object_path:`%s`", method_name, object_path); g_dbus_connection_flush(conn, NULL, NULL, NULL); return; } context_log_warn(context, true, "Unsupported method call:`%s`", method_name); GError* error = g_error_new( g_quark_from_static_string("TymInvalidMethodCall"), TYM_ERROR_INVALID_METHOD_CALL, "Unsupported method call: %s", method_name); g_dbus_method_invocation_return_gerror(invocation, error); g_dbus_connection_flush(conn, NULL, NULL, NULL); } int on_local_options(GApplication* gapp, GVariantDict* values, void* user_data) { df(); Option* option = (Option*)(user_data); char* dest_path = _get_dest_path_from_option(option); char* signal_name = option_get_str(option, "signal"); char* method_name = option_get_str(option, "call"); char* param = option_get_str(option, "param"); if (signal_name || method_name) { int code = _perform_signal(dest_path, signal_name, method_name, param); g_free(dest_path); return code; } if (option_get_bool(option, "daemon")) { if (g_application_get_is_remote(app->gapp)) { /* If there is a normal primary instance, --daemon flag would make it "zombie" */ /* So daemonization should be allowed only when the instanciation is the primary */ g_warning("There is any tym instance. So could not start as daemon process."); return 1; } } const char* cwd = option_get_str(option, "cwd"); if (cwd != NULL && !g_path_is_absolute(cwd)) { g_warning("cwd must be an absolute path"); return 1; } return -1; } static bool _subscribe_dbus(Context* context) { df(); GError* error = NULL; const char* app_id = g_application_get_application_id(app->gapp); GDBusConnection* conn = g_application_get_dbus_connection(app->gapp); g_dbus_connection_signal_subscribe( conn, NULL, // sender app_id, // interface_name NULL, // member context->object_path, // object_path NULL, // arg0 G_DBUS_SIGNAL_FLAGS_NONE, on_dbus_signal, context, NULL // user data free func ); GDBusInterfaceVTable vtable = { on_dbus_call_method, NULL, NULL, }; static const char introspection_xml[] = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; GDBusNodeInfo* introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error); if (error) { g_error("%s", error->message); g_error_free(error); app_quit_context(context); return false; } context_log_message(context, false, "DBus: object_path='%s' interface_name:'%s'", context->object_path, app_id); context->registration_id = g_dbus_connection_register_object( conn, context->object_path, introspection_data->interfaces[0], // interface_info, &vtable, // vtable context, // user_data, NULL, // user_data_free_func, &error // error ); if (context->registration_id <= 0) { context_log_warn(context, true, "Could not subscribe DBus with path:%s", context->object_path); } return true; } #ifdef TYM_USE_VTE_SPAWN_ASYNC static void on_vte_spawn(VteTerminal* vte, GPid child_pid, GError* error, void* user_data) { Context* context = (Context*)user_data; context->initialized = true; context->child_pid = child_pid; if (error) { g_warning("vte-spawn error: %s", error->message); /* g_error_free(error); */ gtk_window_close(context->layout.window); app_quit_context(context); /* dd("%d", gtk_application_new); */ return; } } #endif int on_command_line(GApplication* gapp, GApplicationCommandLine* cli, void* user_data) { df(); GError* error = NULL; int argc = -1; char** argv = g_application_command_line_get_arguments(cli, &argc); Option* option = option_init(meta_get_option_entries(app->meta)); if (!option_parse(option, argc, argv)){ return 1; }; if (option_get_bool(option, "daemon")) { GtkWindow* window = gtk_application_get_active_window(GTK_APPLICATION(gapp)); if (window) { g_warning("Blocked another instance from trying to start as daemon process."); return 1; } /* Only creates a window, never shows it. */ window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(gapp))); UNUSED(window); g_message("Starting as daemon process."); return 0; } if (option_get_str(option, "signal") || option_get_str(option, "call")) { /* Do nothing */ dd("D-Bus signal/method call was performed on a remote process."); return 0; } Context* context = app_spawn_context(option); if (!context) { return 1; } context_load_device(context); context_load_lua_context(context); context_build_layout(context); context_restore_default(context); context_load_theme(context); context_load_config(context); context_override_by_option(context); VteTerminal* vte = context->layout.vte; GtkWindow* window = context->layout.window; GtkTargetEntry drop_types[] = { {"text/uri-list", 0, 0} }; 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); context_signal_connect(context, vte, "drag-data-received", G_CALLBACK(on_vte_drag_data_received)); context_signal_connect(context, vte, "key-press-event", G_CALLBACK(on_vte_key_press)); context_signal_connect(context, vte, "scroll-event", G_CALLBACK(on_vte_mouse_scroll)); context_signal_connect(context, vte, "child-exited", G_CALLBACK(on_vte_child_exited)); context_signal_connect(context, vte, "window-title-changed", G_CALLBACK(on_vte_title_changed)); context_signal_connect(context, vte, "bell", G_CALLBACK(on_vte_bell)); context_signal_connect(context, vte, "button-press-event", G_CALLBACK(on_vte_click)); context_signal_connect(context, vte, "selection-changed", G_CALLBACK(on_vte_selection_changed)); context_signal_connect(context, vte, "resize-window", G_CALLBACK(on_vte_resize_request)); context_signal_connect(context, window, "destroy", G_CALLBACK(on_window_close)); context_signal_connect(context, window, "focus-in-event", G_CALLBACK(on_window_focus_in)); context_signal_connect(context, window, "focus-out-event", G_CALLBACK(on_window_focus_out)); context_signal_connect(context, window, "draw", G_CALLBACK(on_window_draw)); context_signal_connect(context, window, "size-allocate", G_CALLBACK(on_window_resize)); if (app->is_isolated) { g_message("This process is isolated so never listen to D-Bus signal/method call."); } else { _subscribe_dbus(context); } const char* shell_line = context_get_str(context, "shell"); char** shell_argv = NULL; if (g_strv_length(option->rest_argv) >= 2) { GStrvBuilder* builder = g_strv_builder_new(); char** a = &option->rest_argv[1]; while (*a) { /* Skips an unnecessary entry that equals `--` */ if (is_equal(*a, "--")) { a++; continue; } g_strv_builder_add(builder, *a); a++; } shell_argv = g_strv_builder_end(builder); } else { g_shell_parse_argv(shell_line, NULL, &shell_argv, &error); if (error) { g_warning("Parse error: %s", error->message); g_error_free(error); app_quit_context(context); return 0; } } const char* const* env = g_application_command_line_get_environ(cli); char** shell_env = g_new0(char*, g_strv_length((char**)env) + 1); int i = 0; while (env[i]) { shell_env[i] = g_strdup(env[i]); i += 1; } shell_env = g_environ_setenv(shell_env, "TERM", context_get_str(context, "term"), true); char* id_str = g_strdup_printf("%i", context->id); shell_env = g_environ_setenv(shell_env, "TYM_ID", id_str, true); g_free(id_str); const char* cwd = option_get_str(option, "cwd"); if (cwd == NULL) { cwd = g_application_command_line_get_cwd(cli); } #ifdef TYM_USE_VTE_SPAWN_ASYNC vte_terminal_spawn_async( vte, // terminal VTE_PTY_DEFAULT, // pty flag cwd, // working directory shell_argv, // argv shell_env, // envv G_SPAWN_SEARCH_PATH, // spawn_flags NULL, // child_setup NULL, // child_setup_data NULL, // child_setup_data_destroy 5000, // timeout NULL, // cancel callback on_vte_spawn, // callback context // user_data ); #else GPid child_pid; vte_terminal_spawn_sync( vte, VTE_PTY_DEFAULT, cwd, shell_argv, shell_env, G_SPAWN_SEARCH_PATH, NULL, NULL, &child_pid, NULL, &error ); context->child_pid = child_pid; if (error) { g_strfreev(shell_env); g_strfreev(shell_argv); g_error("%s", error->message); g_error_free(error); app_quit_context(context); return 1; } #endif g_strfreev(shell_env); g_strfreev(shell_argv); gtk_widget_grab_focus(GTK_WIDGET(vte)); gtk_widget_show_all(GTK_WIDGET(context->layout.window)); return 0; } ================================================ FILE: src/builtin.c ================================================ /** * builtin.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "builtin.h" #include "context.h" #include "command.h" #include "app.h" static int builtin_get(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* key = luaL_checkstring(L, 1); MetaEntry* e = meta_get_entry(app->meta, key); if (!e) { luaX_warn(L, "Invalid config key: '%s'", key); lua_pushnil(L); return 1; } switch (e->type) { case META_ENTRY_TYPE_STRING: lua_pushstring(L, context_get_str(context, key)); break; case META_ENTRY_TYPE_INTEGER: lua_pushinteger(L, context_get_int(context, key)); break; case META_ENTRY_TYPE_BOOLEAN: lua_pushboolean(L, context_get_bool(context, key)); break; default: lua_pushnil(L); break; } return 1; } static int builtin_quit(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); gtk_window_close(context->layout.window); return 0; } static int builtin_set(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* key = luaL_checkstring(L, 1); MetaEntry* e = meta_get_entry(app->meta, key); if (!e) { luaX_warn(L, "Invalid config key: '%s'", key); return 0; } int type = lua_type(L, 2); switch (e->type) { case META_ENTRY_TYPE_STRING: { const char* value = lua_tostring(L, 2); if (!value) { luaX_warn(L, "Invalid string config for '%s' (string expected, got %s)", key, lua_typename(L, type)); break; } context_set_str(context, key, value); break; } case META_ENTRY_TYPE_INTEGER: { if (type != LUA_TNUMBER) { luaX_warn(L, "Invalid integer config for '%s': %s (number expected, got %s)", key, lua_tostring(L, 2), lua_typename(L, type)); break; } int value = lua_tointeger(L, 2); context_set_int(context, key, value); break; } case META_ENTRY_TYPE_BOOLEAN: { int value = lua_toboolean(L, 2); context_set_bool(context, key, value); break; } default: break; } return 0; } static int get_default_value(lua_State* L) { /* Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); */ const char* key = luaL_checkstring(L, 1); MetaEntry* e = meta_get_entry(app->meta, key); if (!e) { luaX_warn(L, "Invalid config key: '%s'", key); lua_pushnil(L); return 1; } switch (e->type) { case META_ENTRY_TYPE_STRING: lua_pushstring(L, (char*)e->default_value); break; case META_ENTRY_TYPE_INTEGER: lua_pushinteger(L, *(int*)e->default_value); break; case META_ENTRY_TYPE_BOOLEAN: lua_pushboolean(L, *(bool*)e->default_value); break; default: lua_pushnil(L); break; } return 1; } static int builtin_get_config(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); lua_newtable(L); for (GList* li = app->meta->list; li != NULL; li = li->next) { MetaEntry* e = (MetaEntry*)li->data; char* key = e->name; lua_pushstring(L, key); switch (e->type) { case META_ENTRY_TYPE_STRING: { const char* value = context_get_str(context, key); lua_pushstring(L, value); break; } case META_ENTRY_TYPE_INTEGER: lua_pushinteger(L, context_get_int(context, key)); break; case META_ENTRY_TYPE_BOOLEAN: lua_pushboolean(L, context_get_bool(context, key)); break; case META_ENTRY_TYPE_NONE: lua_pop(L, 1); continue; } lua_settable(L, -3); } return 1; } static int builtin_set_config(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); luaL_argcheck(L, lua_istable(L, 1), 1, "table expected"); lua_pushnil(L); while (lua_next(L, -2)) { lua_pushvalue(L, -2); const char* key = lua_tostring(L, -1); MetaEntry* e = meta_get_entry(app->meta, key); if (e) { int type = lua_type(L, -2); switch (e->type) { case META_ENTRY_TYPE_STRING: { const char* value = lua_tostring(L, -2); if (!value) { luaX_warn(L, "Invalid string config for '%s' (string expected, got %s)", key, lua_typename(L, type)); break; } context_set_str(context, key, value); break; } case META_ENTRY_TYPE_INTEGER: { if (type != LUA_TNUMBER) { luaX_warn(L, "Invalid integer config for '%s': %s (number expected, got %s)", key, lua_tostring(L, -2), lua_typename(L, type)); break; } int value = lua_tointeger(L, -2); context_set_int(context, key, value); break; } case META_ENTRY_TYPE_BOOLEAN: { int value = lua_toboolean(L, -2); context_set_bool(context, key, value); break; } default: break; } } else { luaX_warn(L, "Invalid config key: '%s'", key); } lua_pop(L, 2); } return 0; } static int builtin_reset_config(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); context_restore_default(context); return 0; } static int builtin_set_keymap(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* key = luaL_checkstring(L, 1); luaL_argcheck(L, lua_isfunction(L, 2), 2, "function expected"); int ref = luaL_ref(L, LUA_REGISTRYINDEX); bool ok = keymap_add_entry(context->keymap, key, ref); if (!ok) { luaL_unref(L, LUA_REGISTRYINDEX, ref); luaX_warn(L, "Invalid accelerator: '%s'", key); return 0; } return 0; } static int builtin_unset_keymap(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* key = luaL_checkstring(L, 1); bool removed = keymap_remove_entry(context->keymap, key); if (!removed) { luaX_warn(L, "Tried to remove en empty keymap '(%s') which is not assigned function to", key); } return 0; } static int builtin_set_keymaps(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); luaL_argcheck(L, lua_istable(L, 1), 1, "table expected"); lua_pushnil(L); while (lua_next(L, -2)) { lua_pushvalue(L, -2); const char* key = lua_tostring(L, -1); if (!lua_isfunction(L, -2)) { luaX_warn(L, "Invalid value for '%s': function expected, got %s", key, lua_typename(L, lua_type(L, -2))); } else { lua_pushvalue(L, -2); // push function to stack top int ref = luaL_ref(L, LUA_REGISTRYINDEX); bool ok = keymap_add_entry(context->keymap, key, ref); if (!ok) { luaL_unref(L, LUA_REGISTRYINDEX, ref); luaX_warn(L, "Invalid accelerator: '%s'", key); } } lua_pop(L, 2); } return 0; } static int builtin_reset_keymaps(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); keymap_reset(context->keymap); return 0; } static int builtin_set_hook(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* key = luaL_checkstring(L, 1); luaL_argcheck(L, lua_isfunction(L, 2), 2, "function expected"); int ref = luaL_ref(L, LUA_REGISTRYINDEX); int old_ref = -1; if (hook_set_ref(context->hook, key, ref, &old_ref)) { if (old_ref > 0) { dd("unref old ref"); luaL_unref(L, LUA_REGISTRYINDEX, old_ref); } return 0; } luaL_unref(L, LUA_REGISTRYINDEX, ref); luaX_warn(L, "Invalid hook key: '%s'", key); return 0; } static int builtin_set_hooks(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); luaL_argcheck(L, lua_istable(L, 1), 1, "table expected"); lua_pushnil(L); while (lua_next(L, -2)) { lua_pushvalue(L, -2); const char* key = lua_tostring(L, -1); if (!lua_isfunction(L, -2)) { luaX_warn(L, "Invalid value for '%s': function expected, got %s", key, lua_typename(L, lua_type(L, -2))); } else { lua_pushvalue(L, -2); // push function to stack top int ref = luaL_ref(L, LUA_REGISTRYINDEX); int old_ref = -1; int ok = hook_set_ref(context->hook, key, ref, &old_ref); if (old_ref > 0) { dd("unref old ref"); luaL_unref(L, LUA_REGISTRYINDEX, old_ref); } if (!ok) { luaL_unref(L, LUA_REGISTRYINDEX, ref); luaX_warn(L, "Invalid hook key: '%s'", key); } } lua_pop(L, 2); } return 0; } static int builtin_reload(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); context_load_config(context); context_load_theme(context); return 0; } static int builtin_reload_theme(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); context_load_theme(context); return 0; } static int builtin_send_key(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* accelerator = luaL_checkstring(L, 1); unsigned key; GdkModifierType mod; gtk_accelerator_parse(accelerator, &key, &mod); if (0 == key && 0 == mod) { luaL_error(L, "Invalid accelerator: '%s'", accelerator); return 0; } GdkEvent* event = gdk_event_new(GDK_KEY_PRESS); if (context->device == NULL) { g_warning("Could not get input device."); return 0; } gdk_event_set_device(event, context->device); event->key.window = g_object_ref(gtk_widget_get_window(GTK_WIDGET(context->layout.window))); event->key.send_event = false; event->key.time = GDK_CURRENT_TIME; event->key.state = mod; event->key.keyval = key; gtk_main_do_event((GdkEvent*)event); event->type = GDK_KEY_RELEASE; gtk_main_do_event((GdkEvent*)event); gdk_event_free((GdkEvent*)event); return 0; } typedef struct { Context* context; int ref; } TimeoutCallbackNotation; static int timeout_callback(void* user_data) { TimeoutCallbackNotation* notation = (TimeoutCallbackNotation*)user_data; lua_State* L = notation->context->lua; lua_rawgeti(L, LUA_REGISTRYINDEX, notation->ref); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); // pop none-function dd("tried to call non-function"); return false; } if (lua_pcall(L, 0, 1, 0) != LUA_OK) { luaX_warn(L, "Error in timeout function: '%s'", lua_tostring(L, -1)); lua_pop(L, 1); // error return false; } bool result = lua_toboolean(L, -1); lua_pop(L, 1); return result; } static int builtin_set_timeout(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); luaL_argcheck(L, lua_isfunction(L, 1), 1, "function expected"); int interval = lua_tointeger(L, 2); // if non-number, falling back to 0 lua_pushvalue(L, 1); int ref = luaL_ref(L, LUA_REGISTRYINDEX); TimeoutCallbackNotation* notation = g_new0(TimeoutCallbackNotation, 1); notation->context = context; notation->ref = ref; int tag = g_timeout_add_full(G_PRIORITY_DEFAULT, interval, (GSourceFunc)timeout_callback, notation, g_free); lua_pushinteger(L, tag); return 1; } static int builtin_clear_timeout(lua_State* L) { int tag = luaL_checkinteger(L, 1); g_source_remove(tag); return 0; } static int builtin_put(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* text = luaL_checkstring(L, -1); vte_terminal_feed_child(context->layout.vte, text, -1); return 0; } static int builtin_bell(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); gdk_window_beep(context_get_gdk_window(context)); return 0; } static int builtin_open(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* uri = luaL_checkstring(L, -1); context_launch_uri(context, uri); return 0; } static int builtin_notify(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* body = luaL_checkstring(L, 1); const char* title = lua_tostring(L, 2); context_notify(context, body, title); return 0; } static int builtin_copy(lua_State* L) { const char* text = luaL_checkstring(L, 1); const char* target = lua_tostring(L, 2); GdkAtom selection = GDK_SELECTION_CLIPBOARD; if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) { } else if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) { selection = GDK_SELECTION_PRIMARY; } else if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) { selection = GDK_SELECTION_SECONDARY; } else { luaX_warn(L, "Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.", target); } GtkClipboard* cb = gtk_clipboard_get(selection); gtk_clipboard_set_text(cb, text, -1); return 0; } static int builtin_copy_selection(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* target = lua_tostring(L, 1); if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) { command_copy_selection(context); return 0; } if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) { // nothing to do return 0; } if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) { // go down } else { luaX_warn(L, "Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.", target); return 0; } GtkClipboard* pri = gtk_clipboard_get(GDK_SELECTION_PRIMARY); char* text = gtk_clipboard_wait_for_text(pri); GtkClipboard* sec = gtk_clipboard_get(GDK_SELECTION_SECONDARY); gtk_clipboard_set_text(sec, text, -1); g_free(text); return 0; } static int builtin_paste(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* target = lua_tostring(L, 1); if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) { command_paste(context); return 0; } GdkAtom selection = GDK_SELECTION_CLIPBOARD; if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) { selection = GDK_SELECTION_PRIMARY; } else if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) { selection = GDK_SELECTION_SECONDARY; } else { luaX_warn(L, "Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.", target); return 0; } GtkClipboard* cb = gtk_clipboard_get(selection); char* text = gtk_clipboard_wait_for_text(cb); vte_terminal_feed_child(context->layout.vte, text, -1); g_free(text); return 0; } static int builtin_color_to_rgba(lua_State* L) { GdkRGBA color; const char* str = luaL_checkstring(L, -1); bool valid = gdk_rgba_parse(&color, str); if (!valid) { luaX_warn(L, "Invalid color string: '%s'", str); return 0; } lua_pushinteger(L, roundup(color.red * 255)); lua_pushinteger(L, roundup(color.green * 255)); lua_pushinteger(L, roundup(color.blue * 255)); lua_pushnumber(L, color.alpha); return 4; } static int builtin_rgba_to_color(lua_State* L) { int red = luaL_checknumber(L, 1); int green = luaL_checknumber(L, 2); int blue = luaL_checknumber(L, 3); double alpha = lua_isnone(L, 4) ? 1.0 : lua_tonumber(L, 4); char* str = g_strdup_printf("rgba(%d, %d, %d, %f)", red, green, blue, alpha); lua_pushstring(L, str); g_free(str); return 1; } static int builtin_rgb_to_hex(lua_State* L) { int red = luaL_checknumber(L, 1); int green = luaL_checknumber(L, 2); int blue = luaL_checknumber(L, 3); char* hex = g_strdup_printf("#%x%x%x", red, green, blue); lua_pushstring(L, hex); g_free(hex); return 1; } static int builtin_hex_to_rgb(lua_State* L) { const char* hex = luaL_checkstring(L, 1); GdkRGBA color = {}; bool valid = gdk_rgba_parse(&color, hex); if (!valid) { luaX_warn(L, "Invalid hex string: '%s'", hex); return 0; } lua_pushinteger(L, roundup(color.red * 255)); lua_pushinteger(L, roundup(color.green * 255)); lua_pushinteger(L, roundup(color.blue * 255)); return 3; } static GVariant* table_to_variant(lua_State* L, int table_index) { luaL_argcheck(L, lua_istable(L, table_index), table_index, "table expected"); #if USES_LUAJIT size_t num_params = lua_objlen(L, table_index); #else size_t num_params = lua_rawlen(L, table_index); #endif GVariant** vv = g_new0(GVariant*, num_params); int i = 0; while (i < num_params) { lua_rawgeti(L, table_index, i + 1); // args[table_index][i+1] vv[i] = g_variant_new_string(lua_tostring(L, -1)); lua_pop(L, 1); i += 1; } GVariant* p = g_variant_new_tuple(vv, num_params); g_free(vv); return p; } /* usage tym.signal(0, 'hook', {'param'}) */ static int builtin_signal(lua_State* L) { int target_id = luaL_checkinteger(L, 1); const char* signal_name = luaL_checkstring(L, 2); GVariant* params = lua_gettop(L) >= 3 ? table_to_variant(L, 3) : g_variant_new("()"); GDBusConnection* conn = g_application_get_dbus_connection(app->gapp); GError* error = NULL; char* object_path = g_strdup_printf(TYM_OBJECT_PATH_FMT_INT, target_id); g_dbus_connection_emit_signal(conn, NULL, object_path, TYM_APP_ID, signal_name, params, &error); g_free(object_path); if (error) { luaX_warn(L, "DBus error: '%s'", error->message); g_error_free(error); return 0; } return 0; } typedef struct { Context* context; int ref; } CallCallbackNotation; void push_value_by_gvariant(lua_State* L, GVariant* v) { if (g_variant_is_of_type(v, G_VARIANT_TYPE_INT32)) { lua_pushinteger(L, g_variant_get_int32(v)); } else if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) { char* s = NULL; g_variant_get(v, "s", &s); lua_pushstring(L, s); g_free(s); } else if (g_variant_is_of_type(v, G_VARIANT_TYPE_ARRAY) || g_variant_is_of_type(v, G_VARIANT_TYPE_TUPLE)) { lua_newtable(L); size_t num = g_variant_n_children(v); int i = 0; while (i < num) { GVariant* e = g_variant_get_child_value(v, i); push_value_by_gvariant(L, e); lua_rawseti(L, -2, i + 1); i += 1; } } else { lua_pushstring(L, g_variant_print(v, false)); } } void call_callback(GObject* source_object, GAsyncResult* res, void* user_data) { GError* error = NULL; TimeoutCallbackNotation* notation = (TimeoutCallbackNotation*)user_data; Context* context = notation->context; lua_State* L = context->lua; GDBusConnection* conn = g_application_get_dbus_connection(app->gapp); GVariant* result = g_dbus_connection_call_finish(conn, res, &error); lua_rawgeti(L, LUA_REGISTRYINDEX, notation->ref); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); // pop none-function context_log_warn(context, true, "tried to call non-function"); return; } int num_args = 0; if (error) { char* m = g_strdup_printf("DBus error: '%s'", error->message); luaX_warn(L, "%s", m); lua_pushstring(L, m); g_error_free(error); g_free(m); num_args = 1; } else { dd("DBus method call result: `%s`", g_variant_print(result, true)); int num_result = g_variant_n_children(result); int i = 0; while (i < num_result) { GVariant* e = g_variant_get_child_value(result, i); push_value_by_gvariant(L, e); i += 1; } num_args = num_result; } if (lua_pcall(L, num_args, 0, 0) != LUA_OK) { luaX_warn(L, "Error in timeout function: '%s'", lua_tostring(L, -1)); lua_pop(L, 1); // error } } /* usage: tym.call(0, 'eval', {'return 1+2'}, function(...) end) */ static int builtin_call(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); int target_id = luaL_checkinteger(L, 1); const char* method_name = luaL_checkstring(L, 2); GVariant* params = table_to_variant(L, 3); bool has_cb = lua_gettop(L) >= 4; CallCallbackNotation* notation = NULL; GAsyncReadyCallback cb = NULL; if (has_cb) { luaL_argcheck(L, lua_isfunction(L, 4), 4, "function expected"); lua_pushvalue(L, 4); int ref = luaL_ref(L, LUA_REGISTRYINDEX); notation = g_new0(CallCallbackNotation, 1); notation->context = context; notation->ref = ref; cb = (GAsyncReadyCallback)call_callback; } GDBusConnection* conn = g_application_get_dbus_connection(app->gapp); char* object_path = g_strdup_printf(TYM_OBJECT_PATH_FMT_INT, target_id); g_dbus_connection_call( conn, // conn TYM_APP_ID, // bus_name object_path, // object_path TYM_APP_ID, // interface_name method_name, // method_name params, // parameters NULL, // reply_type G_DBUS_CALL_FLAGS_NONE, // flags 1000, // timeout NULL, // cancellable cb, // callback notation // user_data ); g_free(object_path); return 0; } static int builtin_check_mod_state(lua_State* L) { const char* accelerator = luaL_checkstring(L, 1); unsigned key; GdkModifierType mod; gtk_accelerator_parse(accelerator, &key, &mod); GdkDisplay* display = gdk_display_get_default(); GdkKeymap* kmap = gdk_keymap_get_for_display(display); unsigned current_mod = gdk_keymap_get_modifier_state(kmap); lua_pushboolean(L, mod & current_mod); return 1; } static int builtin_get_cursor_position(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); long col = 0; long row = 0; vte_terminal_get_cursor_position(context->layout.vte, &col, &row); lua_pushinteger(L, col); lua_pushinteger(L, row); return 2; } static int builtin_get_clipboard(lua_State* L) { const char* target = lua_tostring(L, 1); GdkAtom selection = GDK_SELECTION_CLIPBOARD; if (!target || is_equal(target, TYM_CLIPBOARD_CLIPBOARD)) { } else if (is_equal(target, TYM_CLIPBOARD_PRIMARY)) { selection = GDK_SELECTION_PRIMARY; } else if (is_equal(target, TYM_CLIPBOARD_SECONDARY)) { selection = GDK_SELECTION_SECONDARY; } else { luaX_warn(L, "Invalid target(`%s`): 'clipboard', 'primary' or 'secondary' is available.", target); return 0; } GtkClipboard* cb = gtk_clipboard_get(selection); char* text = gtk_clipboard_wait_for_text(cb); lua_pushstring(L, text); g_free(text); return 1; } static int builtin_get_selection(lua_State* L) { GtkClipboard* cb = gtk_clipboard_get(GDK_SELECTION_PRIMARY); char* text = gtk_clipboard_wait_for_text(cb); lua_pushstring(L, text); g_free(text); return 1; } static int builtin_unselect_all(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); vte_terminal_unselect_all(context->layout.vte); return 0; } static int builtin_select_all(lua_State *L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); vte_terminal_select_all(context->layout.vte); return 0; } static int builtin_has_selection(lua_State *L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); lua_pushboolean(L, vte_terminal_get_has_selection(context->layout.vte)); return 1; } static int builtin_get_text(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); int start_row = luaL_checkinteger(L, 1); int start_col = luaL_checkinteger(L, 2); int end_row = luaL_checkinteger(L, 3); int end_col = luaL_checkinteger(L, 4); if (end_row < 0) { end_row = vte_terminal_get_row_count(context->layout.vte); } if (end_col < 0) { end_col = vte_terminal_get_column_count(context->layout.vte); } #ifdef TYM_USE_VTE_GET_TEXT_RANGE_FORMAT char* selection = vte_terminal_get_text_range_format(context->layout.vte, VTE_FORMAT_TEXT, start_row, start_col, end_row, end_col, NULL); #else char* selection = vte_terminal_get_text_range(context->layout.vte, start_row, start_col, end_row, end_col, NULL, NULL, NULL); #endif lua_pushstring(L, selection); g_free(selection); return 1; } static int builtin_get_monitor_model(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); GdkDisplay* display = gdk_display_get_default(); GdkMonitor* monitor = gdk_display_get_monitor_at_window(display, context_get_gdk_window(context)); lua_pushstring(L, gdk_monitor_get_model(monitor)); return 1; } static int builtin_get_config_path(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); char* path = context_acquire_config_path(context); lua_pushstring(L, path); if (path) { g_free(path); } return 1; } static int builtin_get_theme_path(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); char* path = context_acquire_theme_path(context); lua_pushstring(L, path); if (path) { g_free(path); } return 1; } static int builtin_get_id(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); lua_pushinteger(L, context->id); return 1; } static int builtin_get_ids(lua_State* L) { lua_newtable(L); int i = 0; for (GList* li = app->contexts; li != NULL; li = li->next) { Context* c = (Context*)li->data; lua_pushinteger(L, c->id); lua_rawseti(L, -2, i + 1); i += 1; } return 1; } static int builtin_get_object_path(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); lua_pushstring(L, context->object_path); return 1; } static int builtin_get_terminal_pid(lua_State* L) { lua_pushinteger(L, getpid()); return 1; } static int builtin_get_pid(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); lua_pushinteger(L, context->child_pid); return 1; } static int builtin_get_version(lua_State* L) { lua_pushstring(L, PACKAGE_VERSION); return 1; } static int builtin_apply(lua_State* L) { Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); const char* message = "DEPRECATED: `tym.apply()` is never needed. You can `tym.set()` and the value is applied right away to the app."; context_notify(context, message, NULL); luaX_warn(L, "%s", message); return 0; } int builtin_register_module(lua_State* L) { const luaL_Reg table[] = { { "quit" , builtin_quit }, { "get" , builtin_get }, { "set" , builtin_set }, { "get_default_value" , get_default_value }, { "get_config" , builtin_get_config }, { "set_config" , builtin_set_config }, { "reset_config" , builtin_reset_config }, { "set_keymap" , builtin_set_keymap }, { "unset_keymap" , builtin_unset_keymap }, { "set_keymaps" , builtin_set_keymaps }, { "reset_keymaps" , builtin_reset_keymaps }, { "set_hook" , builtin_set_hook }, { "set_hooks" , builtin_set_hooks }, { "reload" , builtin_reload }, { "reload_theme" , builtin_reload_theme }, { "send_key" , builtin_send_key }, { "set_timeout" , builtin_set_timeout }, { "clear_timeout" , builtin_clear_timeout }, { "put" , builtin_put }, { "bell" , builtin_bell }, { "open" , builtin_open }, { "notify" , builtin_notify }, { "copy" , builtin_copy }, { "copy_selection" , builtin_copy_selection }, { "paste" , builtin_paste }, { "check_mod_state" , builtin_check_mod_state }, { "color_to_rgba" , builtin_color_to_rgba }, { "rgba_to_color" , builtin_rgba_to_color }, { "rgb_to_hex" , builtin_rgb_to_hex }, { "hex_to_rgb" , builtin_hex_to_rgb }, { "signal" , builtin_signal }, { "call" , builtin_call }, { "get_monitor_model" , builtin_get_monitor_model }, { "get_cursor_position" , builtin_get_cursor_position }, { "get_clipboard" , builtin_get_clipboard }, { "get_selection" , builtin_get_selection }, { "unselect_all" , builtin_unselect_all }, { "select_all" , builtin_select_all }, { "has_selection" , builtin_has_selection }, { "get_text" , builtin_get_text }, { "get_config_path" , builtin_get_config_path }, { "get_theme_path" , builtin_get_theme_path }, { "get_id" , builtin_get_id }, { "get_ids" , builtin_get_ids }, { "get_object_path" , builtin_get_object_path }, { "get_terminal_pid" , builtin_get_terminal_pid }, { "get_pid" , builtin_get_pid }, { "get_version" , builtin_get_version }, // DEPRECATED { "apply" , builtin_apply }, { NULL, NULL }, }; Context* context = (Context*)lua_touserdata(L, lua_upvalueindex(1)); luaL_newlibtable(L, table); lua_pushlightuserdata(L, context); luaL_setfuncs(L, table, 1); return 1; } ================================================ FILE: src/command.c ================================================ /** * command.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "command.h" void command_reload(Context* context) { context_load_config(context); context_load_theme(context); } void command_reload_theme(Context* context) { context_load_theme(context); } void command_copy_selection(Context* context) { #ifdef TYM_USE_VTE_COPY_CLIPBOARD_FORMAT vte_terminal_copy_clipboard_format(context->layout.vte, VTE_FORMAT_TEXT); #else vte_terminal_copy_clipboard(context->layout.vte); #endif } void command_paste(Context* context) { vte_terminal_paste_clipboard(context->layout.vte); } ================================================ FILE: src/common.c ================================================ /** * common.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "common.h" const int TYM_DEFAULT_WIDTH = 80; const int TYM_DEFAULT_HEIGHT = 22; const int TYM_DEFAULT_SCALE = 100; const int TYM_DEFAULT_CELL_SIZE = 100; const int TYM_DEFAULT_SCROLLBACK = 512; #ifdef DEBUG void debug_dump_stack(lua_State* L, char* file, unsigned line) { g_print("[%-10s:%3u] (stack dump)\n", file, line); int len = lua_gettop(L); int i = lua_gettop(L); if ( i > 0 ) { while( i ) { int t = lua_type(L, i); g_print(" [%d:%d] ", i, i - len - 1); switch (t) { case LUA_TSTRING: g_print("str: %s ", lua_tostring(L, i)); break; case LUA_TBOOLEAN: g_print("bool: %s ", lua_toboolean(L, i) ? "true" : "false"); break; case LUA_TNUMBER: g_print("number: %g ", lua_tonumber(L, i)); break; case LUA_TTABLE: { g_print("*%s ", lua_typename(L, t)); /* g_print("table: "); */ /* lua_pushnil(L); */ /* while (lua_next(L, -2)) { */ /* lua_pushvalue(L, -2); */ /* g_print("[%s] ", lua_tostring(L, -1)); */ /* lua_pop(L, 2); */ /* } */ /* lua_pop(L, 1); */ break; } default: g_print("*%s ", lua_typename(L, t)); break; } if (len == i) { g_print("(top)"); } g_print("\n"); i--; } } else { g_print(" stack is empty\n"); } } #endif int roundup(double x) { return (int)(x + 0.5); } bool is_equal(const char* a, const char* b) { return g_strcmp0(a, b) == 0; } bool is_none(const char* s) { return g_strcmp0(s, TYM_SYMBOL_NONE) == 0; } bool is_empty(const char* s) { return g_strcmp0(s, "") == 0; } void luaX_requirec(lua_State* L, const char* modname, lua_CFunction openf, int glb, void* userdata) { #if USES_LUAJIT luaL_getmetatable(L, LUA_LOADED_TABLE); #else luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); #endif lua_getfield(L, -1, modname); /* LOADED[modname] */ if (!lua_toboolean(L, -1)) { /* package not already loaded? */ lua_pop(L, 1); /* remove field */ lua_pushlightuserdata(L, userdata); lua_pushcclosure(L, openf, 1); lua_pushstring(L, modname); /* argument to open function */ lua_call(L, 1, 1); /* call 'openf' to open module */ lua_pushvalue(L, -1); /* make copy of module (call result) */ lua_setfield(L, -3, modname); /* LOADED[modname] = module */ } lua_remove(L, -2); /* remove LOADED table */ if (glb) { lua_pushvalue(L, -1); /* copy of module */ lua_setglobal(L, modname); /* _G[modname] = module */ } } int luaX_warn(lua_State* L, const char* fmt, ...) { va_list argp; va_start(argp, fmt); luaL_where(L, 1); lua_pushvfstring(L, fmt, argp); va_end(argp); lua_concat(L, 2); g_message("%s", lua_tostring(L,-1)); lua_pop(L, 1); return 0; } ================================================ FILE: src/config.c ================================================ /** * config.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "config.h" Config* config_init() { Config* config = g_new(Config, 1); config->data = g_hash_table_new_full( g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free ); config->locked = true; return config; } void config_close(Config* config) { g_hash_table_destroy(config->data); g_free(config); } static void* config_get_raw(Config* config, const char* key) { void* ptr = g_hash_table_lookup(config->data, key); if (!ptr) { dd("tried to refer null field: '%s'", key); } return ptr; } static void config_set_raw(Config* config, const char* key, void* value) { if (!value) { dd("tried to set null field: '%s'", key); return; } void* old_key = NULL; bool has_value = g_hash_table_lookup_extended(config->data, key, &old_key, NULL); // warn if: not reseting and attempt to insert value if (config->locked && !has_value) { dd("tried to add new field when locked: '%s'", key); return; } if (old_key) { g_hash_table_remove(config->data, old_key); } g_hash_table_insert(config->data, g_strdup(key), value); return; } void config_set_str(Config* config, const char* key, const char* value) { config_set_raw(config, key, g_strdup(value)); } const char* config_get_str(Config* config, const char* key) { char* v = (char*)config_get_raw(config, key); if (v) { return v; } dd("string config of '%s' is null. falling back to \"\"", key); return ""; } int config_get_int(Config* config, const char* key) { int* v = (int*)config_get_raw(config, key); if (v) { return *v; } dd("int config of '%s' is null. falling back to 0", key); return 0; } void config_set_int(Config* config, const char* key, int value) { config_set_raw(config, key, memdup((gpointer)&value, sizeof(int))); } bool config_get_bool(Config* config, const char* key) { bool* v = (bool*)config_get_raw(config, key); if (v) { return *v; } dd("bool config of '%s' is null. falling back to null", key); return false; } void config_set_bool(Config* config, const char* key, bool value) { config_set_raw(config, key, memdup((gpointer)&value, sizeof(bool))); } void config_restore_default(Config* config, Meta* meta) { df(); config->locked = false; g_hash_table_remove_all(config->data); for (GList* li = meta->list; li != NULL; li = li->next) { MetaEntry* e = (MetaEntry*)li->data; if (e->getter) { // if getter exists, do not save value in hash continue; } switch (e->type) { case META_ENTRY_TYPE_STRING: config_set_str(config, e->name, e->default_value); break; case META_ENTRY_TYPE_INTEGER: config_set_int(config, e->name, *(int*)(e->default_value)); break; case META_ENTRY_TYPE_BOOLEAN: config_set_bool(config, e->name, *(bool*)(e->default_value)); break; case META_ENTRY_TYPE_NONE: break; } } config->locked = true; } ================================================ FILE: src/config_test.c ================================================ /** * config_test.c * * Copyright (c) 2020 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "tym_test.h" #include "config.h" static void test_read_and_write() { Config* c = config_init(); c->locked = false; config_set_int(c, "int", 123); config_set_str(c, "str", "tym"); config_set_bool(c, "bool", true); c->locked = true; g_assert_cmpint(config_get_int(c, "int"), ==, 123); g_assert_cmpstr(config_get_str(c, "str"), ==, "tym"); g_assert_cmpuint(config_get_bool(c, "bool"), ==, true); config_close(c); } static void test_locked() { Config* c = config_init(); config_set_int(c, "int", 123); config_set_str(c, "str", "tym"); config_set_bool(c, "bool", true); // can not save values g_assert_cmpint(config_get_int(c, "int"), ==, 0); g_assert_cmpstr(config_get_str(c, "str"), ==, ""); g_assert_cmpuint(config_get_bool(c, "bool"), ==, false); config_close(c); } void test_config() { test_read_and_write(); test_locked(); } ================================================ FILE: src/context.c ================================================ /** * context.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "context.h" #include "app.h" #include "builtin.h" #include "property.h" #include "command.h" typedef void (*TymCommandFunc)(Context* context); typedef struct { unsigned key; GdkModifierType mod; TymCommandFunc func; } KeyPair; #define TYM_MODULE_NAME "tym" #define TYM_DEFAULT_NOTIFICATION_TITLE "tym" static KeyPair DEFAULT_KEY_PAIRS[] = { { GDK_KEY_c , GDK_CONTROL_MASK | GDK_SHIFT_MASK, command_copy_selection }, { GDK_KEY_v , GDK_CONTROL_MASK | GDK_SHIFT_MASK, command_paste }, { GDK_KEY_r , GDK_CONTROL_MASK | GDK_SHIFT_MASK, command_reload }, {}, }; char* context_acquire_config_path(Context* context) { char* path = option_get_str(context->option, "use"); if (!path) { return g_build_path( G_DIR_SEPARATOR_S, g_get_user_config_dir(), TYM_CONFIG_DIR_NAME, TYM_CONFIG_FILE_NAME, NULL ); } if (g_path_is_absolute(path)) { return g_strdup(path); } char* cwd = g_get_current_dir(); char* abs_path = g_build_path(G_DIR_SEPARATOR_S, cwd, path, NULL); g_free(cwd); g_free(path); return abs_path; } char* context_acquire_theme_path(Context* context) { char* path = option_get_str(context->option, "theme"); if (!path) { return g_build_path( G_DIR_SEPARATOR_S, g_get_user_config_dir(), TYM_CONFIG_DIR_NAME, TYM_THEME_FILE_NAME, NULL ); } if (g_path_is_absolute(path)) { return g_strdup(path); } char* cwd = g_get_current_dir(); char* abs_path = g_build_path(G_DIR_SEPARATOR_S, cwd, path, NULL); g_free(cwd); g_free(path); return abs_path; } void context_load_lua_context(Context* context) { lua_State* L = luaL_newstate(); luaL_openlibs(L); luaX_requirec(L, TYM_MODULE_NAME, builtin_register_module, true, context); lua_pop(L, 1); context->lua = L; } Context* context_init(int id, Option* option) { dd("init context id=%d", id); Context* context = g_new0(Context, 1); g_assert(id >= 0); context->id = id; context->option = option; context->config_loading = false; context->initialized = false; context->object_path = g_strdup_printf(TYM_OBJECT_PATH_FMT_INT, context->id); context->child_pid = -1; context->config = config_init(); context->keymap = keymap_init(); context->hook = hook_init(); return context; } void context_close(Context* context) { dd("close context id=%d", context->id); for (GList* li = context->handler_tags; li != NULL; li = li->next) { HandlerTag* tag = (HandlerTag*)li->data; g_signal_handler_disconnect(tag->object, tag->handler_id); } g_free(context->object_path); option_close(context->option); /* dispose here */ config_close(context->config); keymap_close(context->keymap); hook_close(context->hook); lua_close(context->lua); g_free(context); } void context_add_handler_tag(Context* context, void* object, int handler_id) { HandlerTag* tag = g_new0(HandlerTag, 1); tag->handler_id = handler_id; tag->object = object; context->handler_tags = g_list_append(context->handler_tags, tag); } void context_load_device(Context* context) { GdkDisplay* display = gdk_display_get_default(); #ifdef TYM_USE_GDK_SEAT GdkSeat* seat = gdk_display_get_default_seat(display); context->device = gdk_seat_get_keyboard(seat); #else GdkDeviceManager* manager = gdk_display_get_device_manager(display); GList* devices = gdk_device_manager_list_devices(manager, GDK_DEVICE_TYPE_MASTER); for (GList* li = devices; li != NULL; li = li->next) { GdkDevice* d = (GdkDevice*)li->data; if (gdk_device_get_source(d) == GDK_SOURCE_KEYBOARD) { context->device = d; break; } } g_list_free(devices); #endif } void context_log_message(Context* context, bool notify, const char* fmt, ...) { va_list argp; va_start(argp, fmt); char* base = g_strdup_vprintf(fmt, argp); va_end(argp); char* message = g_strdup_printf("[id=%d] %s", context->id, base); g_message("%s", message); if (notify) { context_notify(context, base, NULL); } g_free(base); g_free(message); } void context_log_warn(Context* context, bool notify, const char* fmt, ...) { va_list argp; va_start(argp, fmt); char* base = g_strdup_vprintf(fmt, argp); va_end(argp); char* message = g_strdup_printf("[id=%d] %s", context->id, base); g_warning("%s", message); if (notify) { context_notify(context, base, NULL); } g_free(base); g_free(message); } void context_restore_default(Context* context) { config_restore_default(context->config, app->meta); for (GList* li = app->meta->list; li != NULL; li = li->next) { MetaEntry* e = (MetaEntry*)li->data; char* target = NULL; if (strncmp("color_", e->name, 6) == 0) { g_ascii_strtoull(&e->name[6], &target, 10); if (&e->name[6] != target) { // skip loading `color_%d` in this loop continue; } } char* key = e->name; switch (e->type) { case META_ENTRY_TYPE_STRING: { context_set_str(context, key, e->default_value); break; } case META_ENTRY_TYPE_INTEGER: { context_set_int(context, key, *(int*)e->default_value); break; } case META_ENTRY_TYPE_BOOLEAN: { context_set_bool(context, key, *(bool*)e->default_value); break; } case META_ENTRY_TYPE_NONE: break; } } // set colors here GdkRGBA* palette = g_new0(GdkRGBA, 16); unsigned i = 0; while (i < 16) { char s[10] = {}; g_snprintf(s, 10, "color_%d", i); MetaEntry* e = meta_get_entry(app->meta, s); assert(gdk_rgba_parse(&palette[i], e->default_value)); i += 1; } vte_terminal_set_colors(context->layout.vte, NULL, NULL, palette, 16); } void context_override_by_option(Context* context) { for (GList* li = app->meta->list; li != NULL; li = li->next) { MetaEntry* e = (MetaEntry*)li->data; char* key = e->name; switch (e->type) { case META_ENTRY_TYPE_STRING: { char* v = option_get_str(context->option, key); if (v) { context_set_str(context, key, v); } break; } case META_ENTRY_TYPE_INTEGER: { int v = option_get_int(context->option, key); if (v) { context_set_int(context, key, v); } break; } case META_ENTRY_TYPE_BOOLEAN: { bool v = option_get_bool(context->option, key); if (v) { context_set_bool(context, key, v); } break; } case META_ENTRY_TYPE_NONE: break; } } } void context_load_config(Context* context) { df(); if (context->config_loading) { context_log_message(context, true, "Tried to load config recursively. Ignoring loading."); return; } context->config_loading = true; char* config_path = context_acquire_config_path(context); dd("config path: `%s`", config_path); if (!config_path) { context_log_message(context, false, "Tried to load config recursively. Ignoring loading."); goto EXIT; } if (!g_file_test(config_path, G_FILE_TEST_EXISTS)) { context_log_message(context, false, "Config file `%s` doesn't exist.", config_path); goto EXIT; } lua_State* L = context->lua; int result = luaL_dofile(L, config_path); if (result != LUA_OK) { const char* error = lua_tostring(L, -1); lua_pop(L, 1); context_log_warn(context, true, error); goto EXIT; } context_log_message(context, false, "Config file `%s` loaded.", config_path); EXIT: context->config_loading = false; if (config_path) { g_free(config_path); } dd("load config end"); } void context_load_theme(Context* context) { df(); if (!context->lua) { context_log_message(context, false, "Skipped loading theme because Lua context is not loaded."); return; } char* theme_path = context_acquire_theme_path(context); dd("theme path: `%s`", theme_path); if (!theme_path) { context_log_message(context, false, "Skipped theme loading."); goto EXIT; } if (!g_file_test(theme_path, G_FILE_TEST_EXISTS)) { context_log_message(context, false, "Theme file `%s` doesn't exist.", theme_path); goto EXIT; } lua_State* L = context->lua; int result = luaL_dofile(L, theme_path); if (result != LUA_OK) { const char* error = lua_tostring(L, -1); context_log_warn(context, true, error); goto EXIT; } context_log_message(context, false, "Theme file `%s` loaded.", theme_path); if (!lua_istable(L, -1)) { context_log_warn( context, "Theme script(%s) must return a table (got %s). Skiped theme assignment.", theme_path, lua_typename(L, lua_type(L, -1))); goto EXIT; } for (GList* li = app->meta->list; li != NULL; li = li->next) { MetaEntry* e = (MetaEntry*)li->data; if (!e->is_theme) { continue; } lua_getfield(L, -1, e->name); if (!lua_isnil(L, -1)) { const char* value = lua_tostring(L, -1); context_set_str(context, e->name, value); } lua_pop(L, 1); } lua_pop(L, 1); EXIT: if (theme_path) { g_free(theme_path); } dd("load theme end"); } static bool context_perform_default(Context* context, unsigned key, GdkModifierType mod) { unsigned i = 0; while (DEFAULT_KEY_PAIRS[i].func) { KeyPair* pair = &DEFAULT_KEY_PAIRS[i]; if ((key == pair->key) && !(~mod & pair->mod)) { pair->func(context); return true; } i++; } return false; } bool context_perform_keymap(Context* context, unsigned key, GdkModifierType mod) { if (context->lua) { bool result = false; char* error = NULL; if (keymap_perform(context->keymap, context->lua, key, mod, &result, &error)) { // if the keymap func is normally excuted, default action will be canceled. // if `return true` in the keymap func, default action will be performed. if (!result) { return true; } } else { if (error) { context_log_warn(context, true, error); g_free(error); // if the keymap func has error, default action will be canceled. return true; } } } if (context_get_bool(context, "ignore_default_keymap")) { return false; } return context_perform_default(context, key, mod); } void context_handle_signal(Context* context, const char* signal_name, GVariant* parameters) { dd("receive signal: %s", signal_name); } void context_build_layout(Context* context) { GtkWindow* window = context->layout.window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(app->gapp))); VteTerminal* vte = context->layout.vte = VTE_TERMINAL(vte_terminal_new()); GtkBox* hbox = context->layout.hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); GtkBox* vbox = context->layout.vbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); context->layout.uri_tag = -1; gtk_container_add(GTK_CONTAINER(hbox), GTK_WIDGET(vte)); gtk_container_add(GTK_CONTAINER(vbox), GTK_WIDGET(hbox)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(vbox)); gtk_container_set_border_width(GTK_CONTAINER(window), 0); GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(window)); GdkVisual* visual = gdk_screen_get_rgba_visual(screen); context->layout.alpha_supported = visual; if (!context->layout.alpha_supported) { context_log_message(context, false, "Your screen doesn't support alpha channel."); visual = gdk_screen_get_system_visual(screen); } gtk_widget_set_visual(GTK_WIDGET(window), visual); #ifdef TYM_USE_SIXEL vte_terminal_set_enable_sixel(vte, true); #endif } void context_notify(Context* context, const char* body, const char* title) { char* default_title = NULL; if (!title) { title = default_title = g_strdup_printf("tym[id=%d]", context->id); } GNotification* notification = g_notification_new(title); if (default_title) { g_free(default_title); } GIcon* icon = g_themed_icon_new_with_default_fallbacks(context_get_str(context, "icon")); g_notification_set_icon(notification, G_ICON(icon)); g_notification_set_body(notification, body); g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_HIGH); g_application_send_notification(app->gapp, TYM_APP_ID, notification); g_object_unref(notification); g_object_unref(icon); } void context_launch_uri(Context* context, const char* uri) { dd("launch: `%s`", uri); GError* error = NULL; GdkDisplay* display = gdk_display_get_default(); GdkAppLaunchContext* ctx = gdk_display_get_app_launch_context(display); gdk_app_launch_context_set_screen(ctx, gdk_screen_get_default()); /* gdk_app_launch_context_set_timestamp(ctx, event->time); */ if (!g_app_info_launch_default_for_uri(uri, G_APP_LAUNCH_CONTEXT(ctx), &error)) { context_log_warn(context, "Failed to launch uri: %s", error->message); g_error_free(error); } } GdkWindow* context_get_gdk_window(Context* context) { return gtk_widget_get_window(GTK_WIDGET(context->layout.window)); } const char* context_get_str(Context* context, const char* key) { MetaEntry* e = meta_get_entry(app->meta, key); if (e->getter) { return ((PropertyStrGetter)e->getter)(context, key); } return config_get_str(context->config, key); } int context_get_int(Context* context, const char* key) { MetaEntry* e = meta_get_entry(app->meta, key); if (e->getter) { return ((PropertyIntGetter)e->getter)(context, key); } return config_get_int(context->config, key); } bool context_get_bool(Context* context, const char* key) { MetaEntry* e = meta_get_entry(app->meta, key); if (e->getter) { return ((PropertyBoolGetter)e->getter)(context, key); } return config_get_bool(context->config, key); } void context_set_str(Context* context, const char* key, const char* value) { MetaEntry* e = meta_get_entry(app->meta, key); if (e->setter) { ((PropertyStrSetter)e->setter)(context, key, value); return; } if (!e->getter) { config_set_str(context->config, key, value); return; } dd("`%s`: setter is not provided but getter is provided", key); } void context_set_int(Context* context, const char* key, int value) { MetaEntry* e = meta_get_entry(app->meta, key); if (e->setter) { ((PropertyIntSetter)e->setter)(context, key, value); return; } if (!e->getter) { config_set_int(context->config, key, value); return; } dd("`%s`: setter is not provided but getter is provided", key); } void context_set_bool(Context* context, const char* key, bool value) { MetaEntry* e = meta_get_entry(app->meta, key); if (e->setter) { ((PropertyBoolSetter)e->setter)(context, key, value); return; } if (!e->getter) { config_set_bool(context->config, key, value); return; } dd("`%s`: setter is not provided but getter is provided", key); } void context_resize(Context* context, int width, int height) { GtkWindow* window = context->layout.window; VteTerminal* vte = context->layout.vte; bool visible = gtk_widget_is_visible(GTK_WIDGET(window)); if (visible) { GtkBorder border; gtk_style_context_get_padding( gtk_widget_get_style_context(GTK_WIDGET(vte)), gtk_widget_get_state_flags(GTK_WIDGET(vte)), &border ); const int char_width = vte_terminal_get_char_width(vte); const int char_height = vte_terminal_get_char_height(vte); const int hpad = context_get_int(context, "padding_horizontal"); const int vpad = context_get_int(context, "padding_vertical"); gtk_window_resize( context->layout.window, width * char_width + border.left + border.right + hpad * 2, height * char_height + border.top + border.bottom + vpad * 2 ); } else { vte_terminal_set_size(vte, width, height); } } ================================================ FILE: src/hook.c ================================================ /** * hook.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "hook.h" #define HOOK_KEY_TITLE "title" #define HOOK_KEY_BELL "bell" #define HOOK_KEY_CLICKED "clicked" #define HOOK_KEY_SCROLL "scroll" #define HOOK_KEY_DRAG "drag" #define HOOK_KEY_ACTIVATED "activated" #define HOOK_KEY_DEACTIVATED "deactivated" #define HOOK_KEY_SELECTED "selected" #define HOOK_KEY_UNSELECTED "unselected" #define HOOK_KEY_RESIZED "resized" #define HOOK_KEY_SIGNAL "signal" const char* HOOK_KEYS[] = { HOOK_KEY_TITLE, HOOK_KEY_BELL, HOOK_KEY_CLICKED, HOOK_KEY_SCROLL, HOOK_KEY_DRAG, HOOK_KEY_ACTIVATED, HOOK_KEY_DEACTIVATED, HOOK_KEY_SELECTED, HOOK_KEY_UNSELECTED, HOOK_KEY_RESIZED, HOOK_KEY_SIGNAL, NULL }; Hook* hook_init() { Hook* hook = g_new0(Hook, 1); hook->refs = g_hash_table_new_full( g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free ); int i = 0; while (HOOK_KEYS[i]) { int* p = g_new0(int, 1); *p = -1; g_hash_table_insert(hook->refs, g_strdup(HOOK_KEYS[i]), p); i += 1; } return hook; } void hook_close(Hook* hook) { g_hash_table_destroy(hook->refs); g_free(hook); } static int hook_get_ref(Hook* hook, const char* key) { int* ptr = g_hash_table_lookup(hook->refs, key); if (!ptr) { dd("invalid hook key: '%s'", key); return -1; } return *ptr; } bool hook_set_ref(Hook* hook, const char* key, int ref, int* old_ref) { assert(old_ref); void* old_key = NULL; void* old_value = NULL; bool has_value = g_hash_table_lookup_extended(hook->refs, key, &old_key, &old_value); if (old_value) { *old_ref = *(int*)old_value; } if (!has_value) { return false; } g_hash_table_remove(hook->refs, old_key); g_hash_table_insert(hook->refs, g_strdup(key), memdup(&ref, sizeof(int))); dd("hook '%s' is registered. ref: %d", key, ref); return true; } static bool hook_perform(Hook* hook, lua_State* L, const char* key, int narg, int nresult) { int ref = hook_get_ref(hook, key); if (ref < 0) { lua_pop(L, narg); return false; } lua_rawgeti(L, LUA_REGISTRYINDEX, ref); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); // pop none-function dd("tried to call hook which is not function."); return false; } lua_insert(L, - narg - 1); dd("perform custom hook: %s", key); if (lua_pcall(L, narg, nresult, 0) != LUA_OK) { luaX_warn(L, "Error in hook function: '%s'", lua_tostring(L, -1)); lua_pop(L, 1); // error return false; } return true; } bool hook_perform_title(Hook* hook, lua_State* L, const char* title, bool* result) { if (!L) { return false; } lua_pushstring(L, title); bool succeeded = hook_perform(hook, L, HOOK_KEY_TITLE, 1, 1); if (!succeeded) { return false; } *result = lua_toboolean(L, -1); lua_pop(L, 1); return succeeded; } bool hook_perform_bell(Hook* hook, lua_State* L, bool* result) { assert(result); if (!L) { return false; } bool succeeded = hook_perform(hook, L, HOOK_KEY_BELL, 0, 1); if (!succeeded) { return false; } *result = lua_toboolean(L, -1); lua_pop(L, 1); return succeeded; } bool hook_perform_clicked(Hook* hook, lua_State* L, int button, const char* uri, bool* result) { assert(result); if (!L) { return false; } lua_pushinteger(L, button); lua_pushstring(L, uri); bool succeeded = hook_perform(hook, L, HOOK_KEY_CLICKED, 2, 1); if (!succeeded) { return false; } *result = lua_toboolean(L, -1); lua_pop(L, 1); return succeeded; } bool hook_perform_scroll(Hook* hook, lua_State* L, double delta_x, double delta_y, double x, double y, bool* result) { assert(result); if (!L) { return false; } lua_pushnumber(L, delta_x); lua_pushnumber(L, delta_y); lua_pushnumber(L, x); lua_pushnumber(L, x); bool succeeded = hook_perform(hook, L, HOOK_KEY_SCROLL, 4, 1); if (!succeeded) { return false; } *result = lua_toboolean(L, -1); lua_pop(L, 1); return succeeded; } bool hook_perform_drag(Hook* hook, lua_State* L, char* path, bool* result) { assert(result); lua_pushstring(L, path); bool succeeded = hook_perform(hook, L, HOOK_KEY_DRAG, 1, 1); if (!succeeded) { return false; } *result = lua_toboolean(L, -1); lua_pop(L, 1); return succeeded; } bool hook_perform_activated(Hook* hook, lua_State* L) { if (!L) { return false; } return hook_perform(hook, L, HOOK_KEY_ACTIVATED, 0, 0); } bool hook_perform_deactivated(Hook* hook, lua_State* L) { if (!L) { return false; } return hook_perform(hook, L, HOOK_KEY_DEACTIVATED, 0, 0); } bool hook_perform_selected(Hook* hook, lua_State* L, const char* text) { if (!L) { return false; } lua_pushstring(L, text); return hook_perform(hook, L, HOOK_KEY_SELECTED, 1, 0); } bool hook_perform_unselected(Hook* hook, lua_State* L) { if (!L) { return false; } return hook_perform(hook, L, HOOK_KEY_UNSELECTED, 0, 0); } bool hook_perform_resized(Hook* hook, lua_State* L) { if (!L) { return false; } return hook_perform(hook, L, HOOK_KEY_RESIZED, 0, 0); } bool hook_perform_signal(Hook* hook, lua_State* L, const char* param) { if (!L) { return false; } lua_pushstring(L, param); return hook_perform(hook, L, HOOK_KEY_SIGNAL, 1, 0); } ================================================ FILE: src/ipc.c ================================================ /** * ipc.c * * Copyright (c) 2022 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "ipc.h" #include "app.h" typedef void (*TymSignalHandler)(Context*, GVariant*); typedef void (*TymMethodHandler)(Context*, GVariant*, GDBusMethodInvocation*); typedef struct { const char* signal_name; TymSignalHandler func; } SignalDef; typedef struct { const char* method_name; TymMethodHandler func; } MethodDef; void ipc_signal_hook(Context* context, GVariant* params) { df(); const char* param = NULL; size_t size = g_variant_n_children(params); if (size > 0) { g_variant_get_child(params, 0, "s", ¶m); } hook_perform_signal(context->hook, context->lua, param); } SignalDef signals[] = { { "hook", ipc_signal_hook, }, { NULL, NULL, } }; void ipc_method_get_ids(Context* context, GVariant* params, GDBusMethodInvocation* invocation) { GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); for (GList* li = app->contexts; li != NULL; li = li->next) { Context* c = (Context*)li->data; g_variant_builder_add(builder, "i", c->id); } GVariant* v = g_variant_builder_end(builder); v = g_variant_new_tuple(&v, 1); g_dbus_method_invocation_return_value(invocation, v); } void ipc_method_echo(Context* context, GVariant* params, GDBusMethodInvocation* invocation) { g_dbus_method_invocation_return_value(invocation, params); } static void ipc_do_lua(Context* context, GVariant* params, GDBusMethodInvocation* invocation, bool needs_result, bool is_file) { lua_State* L = context->lua; char* param = NULL; g_variant_get_child(params, 0, "s", ¶m); char* result = NULL; int suc = is_file ? luaL_dofile(L, param) : luaL_dostring(L, param); if (suc != 0) { result = g_strdup(lua_tostring(L, -1)); context_log_warn(context, true, result); lua_settop(L, 0); } else { if (needs_result) { int top = lua_gettop(L); if (top > 0) { result = g_strdup(lua_tostring(L, -1)); } else { result = g_strdup_printf("Lua stack is empty. `return` is needed."); context_log_warn(context, true, result); } lua_settop(L, 0); } } GVariant* v = needs_result ? g_variant_new("(s)", result) : g_variant_new("()"); g_free(result); g_dbus_method_invocation_return_value(invocation, v); } void ipc_method_eval(Context* context, GVariant* params, GDBusMethodInvocation* invocation) { ipc_do_lua(context, params, invocation, true, false); } void ipc_method_eval_file(Context* context, GVariant* params, GDBusMethodInvocation* invocation) { ipc_do_lua(context, params, invocation, true, true); } void ipc_method_exec(Context* context, GVariant* params, GDBusMethodInvocation* invocation) { ipc_do_lua(context, params, invocation, false, false); } void ipc_method_exec_file(Context* context, GVariant* params, GDBusMethodInvocation* invocation) { ipc_do_lua(context, params, invocation, false, true); } MethodDef methods[] = { { "echo", ipc_method_echo, }, { "get_ids", ipc_method_get_ids, }, { "eval", ipc_method_eval, }, { "eval_file", ipc_method_eval_file, }, { "exec", ipc_method_exec, }, { "exec_file", ipc_method_exec_file, }, { NULL, NULL, } }; IPC* ipc_init() { IPC* ipc = g_new0(IPC, 1); ipc->signals = g_hash_table_new_full( g_str_hash, g_str_equal, NULL, NULL ); ipc->methods = g_hash_table_new_full( g_str_hash, g_str_equal, NULL, NULL ); int i = 0; while (signals[i].signal_name) { SignalDef* d = &signals[i]; g_hash_table_insert(ipc->signals, (void*)d->signal_name, d); i += 1; } i = 0; while (methods[i].method_name) { MethodDef* d = &methods[i]; g_hash_table_insert(ipc->methods, (void*)d->method_name, d); i += 1; } return ipc; } void ipc_close(IPC* ipc) { g_hash_table_destroy(ipc->signals); g_hash_table_destroy(ipc->methods); g_free(ipc); } bool ipc_signal_perform(IPC* ipc, Context* context, const char* signal_name, GVariant* params) { SignalDef* d = g_hash_table_lookup(ipc->signals, signal_name); if (!d) { dd("invalid signal_name: '%s'", signal_name); return false; } d->func(context, params); return true; } bool ipc_method_perform(IPC* ipc, Context* context, const char* method_name, GVariant* params, GDBusMethodInvocation* invocation) { MethodDef* d = g_hash_table_lookup(ipc->methods, method_name); if (!d) { dd("invalid method_name: '%s'", method_name); return false; } d->func(context, params, invocation); return true; } ================================================ FILE: src/keymap.c ================================================ /** * keymap.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "keymap.h" typedef struct { unsigned key; GdkModifierType mod; char* accelerator; int ref; } KeymapEntry; static void free_keymap_entry(KeymapEntry* e, void* user_data) { // TODO: luaL_unref the ref g_free(e->accelerator); g_free(e); } Keymap* keymap_init() { Keymap* keymap = g_new0(Keymap, 1); keymap->entries = NULL; return keymap; } void keymap_reset(Keymap* keymap) { g_list_foreach(keymap->entries, (GFunc)free_keymap_entry, NULL); g_list_free(keymap->entries); keymap->entries = NULL; } void keymap_close(Keymap* keymap) { keymap_reset(keymap); g_free(keymap); } bool keymap_add_entry(Keymap* keymap, const char* accelerator, int ref) { unsigned key; GdkModifierType mod; gtk_accelerator_parse(accelerator, &key, &mod); if (0 == key && 0 == mod) { return false; } bool removed = keymap_remove_entry(keymap, accelerator); KeymapEntry* e = g_new(KeymapEntry, 1); e->key = key; e->mod = mod; e->accelerator = g_strdup(accelerator); e->ref = ref; keymap->entries = g_list_append(keymap->entries, e); if (removed) { dd("keymap (%s mod: %x, key: %x) has been overwritten", accelerator, mod, key); } else { dd("keymap (%s mod: %x, key: %x) has been newly assined", accelerator, mod, key); } return true; } bool keymap_remove_entry(Keymap* keymap, const char* accelerator) { for (GList* li = keymap->entries; li != NULL; li = li->next) { KeymapEntry* e = (KeymapEntry*)li->data; if (is_equal(e->accelerator, accelerator)) { keymap->entries = g_list_remove(keymap->entries, e); g_free(e); return true; } } return false; } bool keymap_perform(Keymap* keymap, lua_State* L, unsigned key, GdkModifierType mod, bool* result, char** error) { assert(result); assert(error); for (GList* li = keymap->entries; li != NULL; li = li->next) { KeymapEntry* e = (KeymapEntry*)li->data; if (key == e->key && mod == e->mod) { dd("performing keymap: %s (mod: %x, key: %x)", e->accelerator, mod, key); lua_rawgeti(L, LUA_REGISTRYINDEX, e->ref); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); // pop none-function dd("tried to call keymap [%s] which is not function.", e->accelerator); return false; } if (lua_pcall(L, 0, 1, 0) != LUA_OK) { *error = g_strdup(lua_tostring(L, -1)); lua_pop(L, 1); // error return false; } *result = lua_toboolean(L, -1); lua_pop(L, 1); return true; } } return false; } ================================================ FILE: src/meta.c ================================================ /** * meta.c * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "meta.h" #include "property.h" static char* get_default_shell() { const char* shell_env = g_getenv("SHELL"); if (shell_env) { return g_strdup(shell_env); } char* user_shell = vte_get_user_shell(); if (user_shell) { return user_shell; } return g_strdup(TYM_FALL_BACK_SHELL); } static void free_entry(void* data) { g_free(((MetaEntry*)data)->default_value); g_free(data); } int entries_sort_func(const void* a, const void* b) { return ((MetaEntry*)a)->index - ((MetaEntry*)b)->index; } Meta* meta_init() { Meta* meta = g_new0(Meta, 1); #define CB(f) ((MetaCallback) (f)) #define color_special(key, default_color) \ { \ .name=("color_"#key ), .default_value=sdup(default_color), .arg_desc="", .desc=("value of color_"#key), \ .is_theme=true, .setter=CB(setter_color_##key) \ } #define color_normal(i) \ { \ .name=("color_"#i ), .default_value=sdup(TYM_DEFAULT_COLOR_##i), .arg_desc="", .desc=("value of color_"#i), \ .option_flag=G_OPTION_FLAG_HIDDEN, .is_theme=true, .setter=CB(setter_color_normal) \ } const MetaEntryType T_INT = META_ENTRY_TYPE_INTEGER; const MetaEntryType T_BOOL = META_ENTRY_TYPE_BOOLEAN; const MetaEntryType T_NONE = META_ENTRY_TYPE_NONE; char* (*sdup)(const char*) = g_strdup; const bool v_false = false; const int v_zero = 0; MetaEntry ee[] = { // STR { .name="shell", .short_name='e', .default_value=get_default_shell(), .arg_desc="", .desc="Shell to use in the terminal", .setter=CB(setter_shell) }, { .name="term", .default_value=sdup(TYM_DEFAULT_TERM), .arg_desc="", .desc="Value to override $TERM", .setter=CB(setter_term) }, { .name="title", .default_value=sdup(TYM_DEFAULT_TITLE), .arg_desc="", .desc="Window title", .getter=CB(getter_title), .setter=CB(setter_title) }, { .name="font", .default_value=sdup(""), .arg_desc="", .desc="Font to render(e.g. 'Ubuntu Mono 12')", .setter=CB(setter_font) }, { .name="icon", .default_value=sdup(TYM_DEFAULT_ICON), .arg_desc="", .desc="Name of window icon", .setter=CB(setter_icon) }, { .name="role", .default_value=sdup(""), .arg_desc="", .desc="Unique identifier for the window", .getter=CB(getter_role), .setter=CB(setter_role), }, { .name="cursor_shape", .default_value=sdup(TYM_DEFAULT_CURSOR_SHAPE), .arg_desc="", .desc="'" TYM_CURSOR_SHAPE_BLOCK "', '" TYM_CURSOR_SHAPE_IBEAM "' or '" TYM_CURSOR_SHAPE_UNDERLINE "'", .getter=CB(getter_cursor_shape), .setter=CB(setter_cursor_shape), }, { .name="cursor_blink_mode", .default_value=sdup(TYM_DEFAULT_CURSOR_BLINK_MODE), .arg_desc="", .desc="'" TYM_CURSOR_BLINK_MODE_SYSTEM "', '" TYM_CURSOR_BLINK_MODE_ON "' or '" TYM_CURSOR_BLINK_MODE_OFF "'", .getter=CB(getter_cursor_blink_mode), .setter=CB(setter_cursor_blink_mode), }, { .name="cjk_width", .arg_desc="", .default_value=sdup(TYM_DEFAULT_CJK), .desc="'" TYM_CJK_WIDTH_NARROW "' or '" TYM_CJK_WIDTH_WIDE "'", .getter=CB(getter_cjk_width), .setter=CB(setter_cjk_width), }, { .name="background_image", .arg_desc="", .default_value=sdup(""), .desc="path to background image", .setter=CB(setter_background_image), }, { .name="uri_schemes", .arg_desc="", .default_value=sdup(TYM_DEFAULT_URI_SCHEMES), .desc="URI schemes to be highlighted and clickable", .setter=CB(setter_uri_schemes), }, // INT { .name="width", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_WIDTH, sizeof(int)), .arg_desc="", .desc="Initial columns", .getter=CB(getter_width), .setter=CB(setter_width) }, { .name="height", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_HEIGHT, sizeof(int)), .arg_desc="", .desc="Initial rows", .getter=CB(getter_height), .setter=CB(setter_height) }, { .name="scale", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_SCALE, sizeof(int)), .arg_desc="", .desc="Font scale in percent", .getter=CB(getter_scale), .setter=CB(setter_scale) }, { .name="cell_width", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_CELL_SIZE, sizeof(int)), .arg_desc="", .desc="Initial columns", .getter=CB(getter_cell_width), .setter=CB(setter_cell_width) }, { .name="cell_height", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_CELL_SIZE, sizeof(int)), .arg_desc="", .desc="Initial rows", .getter=CB(getter_cell_height), .setter=CB(setter_cell_height) }, /* DEPRECATED START */ { .name="padding_horizontal", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)), .arg_desc="", .desc="Horizontal padding", .setter=CB(setter_padding_horizontal) }, { .name="padding_vertical", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)), .arg_desc="", .desc="Vertical padding", .setter=CB(setter_padding_vertical) }, /* DEPRECATED END */ { .name="padding_top", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)), .arg_desc="", .desc="Top padding", .getter=CB(getter_padding_top), .setter=CB(setter_padding_top) }, { .name="padding_bottom", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)), .arg_desc="", .desc="Bottom padding", .getter=CB(getter_padding_bottom), .setter=CB(setter_padding_bottom) }, { .name="padding_left", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)), .arg_desc="", .desc="Left padding", .getter=CB(getter_padding_left), .setter=CB(setter_padding_left) }, { .name="padding_right", .type=T_INT, .default_value=memdup(&v_zero, sizeof(int)), .arg_desc="", .desc="Right padding", .getter=CB(getter_padding_right), .setter=CB(setter_padding_right) }, { .name="scrollback_length", .type=T_INT, .default_value=memdup(&TYM_DEFAULT_SCROLLBACK, sizeof(int)), .arg_desc="", .desc="Scrollback buffer length", .getter=CB(getter_scrollback_length), .setter=CB(setter_scrollback_length) }, // BOOL { .name="scroll_on_output", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)), .desc="Scroll down on output", .getter=CB(getter_scroll_on_output), .setter=CB(setter_scroll_on_output) }, { .name="ignore_default_keymap", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)), .desc="Whether to use default keymap", }, { .name="autohide", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)), .desc="Whether to hide mouse cursor when key is pressed", .getter=CB(getter_autohide), .setter=CB(setter_autohide) }, { .name="silent", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)), .desc="Whether to beep when bell sequence is sent", .getter=CB(getter_silent), .setter=CB(setter_silent), }, { .name="bold_is_bright", .type=T_BOOL, .default_value=memdup(&v_false, sizeof(bool)), .desc="Whether to make bold texts bright", .getter=CB(gettter_bold_is_bright), .setter=CB(setter_bold_is_bright), }, color_normal(0), color_normal(1), color_normal(2), color_normal(3), color_normal(4), color_normal(5), color_normal(6), color_normal(7), color_normal(8), color_normal(9), color_normal(10), color_normal(11), color_normal(12), color_normal(13), color_normal(14), color_normal(15), color_special(window_background, ""), color_special(background, TYM_DEFAULT_COLOR_BACKGROUND), color_special(foreground, TYM_DEFAULT_COLOR_FOREGROUND), color_special(bold, TYM_DEFAULT_COLOR_FOREGROUND), color_special(cursor, TYM_DEFAULT_COLOR_FOREGROUND), color_special(cursor_foreground, TYM_DEFAULT_COLOR_BACKGROUND), color_special(highlight, TYM_DEFAULT_COLOR_FOREGROUND), color_special(highlight_foreground, TYM_DEFAULT_COLOR_BACKGROUND), { .name="color_0..15", .type=T_NONE, .arg_desc="", .desc="value of color_0 .. color_15", }, }; #undef CB #undef get_default_color #undef color_special #undef color_normal meta->data = g_hash_table_new_full( g_str_hash, g_str_equal, NULL, (GDestroyNotify)free_entry ); unsigned i = 0; unsigned len = sizeof(ee) / sizeof(MetaEntry); while (i < len) { MetaEntry* entry = (MetaEntry*)memdup(&ee[i], sizeof(ee[i])); if (entry->getter && !entry->setter) { dw("Invalid meta `%s`: setter is provided but getter is not provided.", entry->name); } entry->index = i; g_hash_table_insert(meta->data, entry->name, entry); meta->list = g_list_insert_sorted(meta->list, entry, entries_sort_func); i++; } return meta; } void meta_close(Meta* meta) { g_hash_table_destroy(meta->data); g_list_free(meta->list); g_free(meta); } unsigned meta_size(Meta* meta) { return g_hash_table_size(meta->data); } MetaEntry* meta_get_entry(Meta* meta, const char* key) { MetaEntry* entry = (MetaEntry*)g_hash_table_lookup(meta->data, key); if (!entry) { return NULL; } MetaEntryType t = entry->type; if (t == META_ENTRY_TYPE_STRING || t == META_ENTRY_TYPE_INTEGER || t == META_ENTRY_TYPE_BOOLEAN) { return entry; } dd("WARN: tried to get META_ENTRY_TYPE_NONE entry [%s]", key); return NULL; } static void* new_empty_bool() { return g_new0(gboolean, 1); } static void* new_empty_str() { return g_new0(char*, 1); } static void* new_empty_int() { return g_new0(int, 1); } GOptionEntry* meta_get_option_entries(Meta* meta) { GOptionEntry app_options[] = { { .long_name = "version", .short_name = 'v', .arg = G_OPTION_ARG_NONE, .arg_data = new_empty_bool(), .description = "Show version", .arg_description = NULL, }, { .long_name = "daemon", .arg = G_OPTION_ARG_NONE, .arg_data = new_empty_bool(), .description = "Launch as daemon process", .arg_description = NULL, }, { .long_name = "use", .short_name = 'u', .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = " to config file. Set '" TYM_SYMBOL_NONE "' to start without loading config", .arg_description = "", }, { .long_name = "theme", .short_name = 't', .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = " to theme file. Set '" TYM_SYMBOL_NONE "' to start without loading theme", .arg_description = "", }, { .long_name = "id", .short_name = 'i', .arg = G_OPTION_ARG_INT, .arg_data = new_empty_int(), .description = " to use in the new instance", .arg_description = "", }, { .long_name = "signal", .short_name = 's', .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = " to send via DBus", .arg_description = "", }, { .long_name = "call", .short_name = 'c', .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = " to call via DBus", .arg_description = "", }, { .long_name = "param", .short_name = 'p', .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = "param with which is called method via DBus", .arg_description = "", }, { .long_name = "dest", .short_name = 'd', .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = " to send signal/call method($TYM_ID is default)", .arg_description = "", }, { .long_name = "isolated", .arg = G_OPTION_ARG_NONE, .arg_data = new_empty_bool(), .description = "Start as an isolated instance", .arg_description = NULL, }, { .long_name = "cwd", .arg = G_OPTION_ARG_STRING, .arg_data = new_empty_str(), .description = "Set the terminal's working directory. Must be an absolute path.", .arg_description = "", } }; GOptionEntry* options_entries = (GOptionEntry*)g_new0( GOptionEntry, sizeof(app_options) / sizeof(GOptionEntry) + meta_size(meta) + 1 ); memmove(options_entries, app_options, sizeof(app_options)); unsigned i = sizeof(app_options) / sizeof(GOptionEntry); for (GList* li = meta->list; li != NULL; li = li->next) { MetaEntry* me = (MetaEntry*)li->data; GOptionEntry* e = &options_entries[i]; i += 1; e->long_name = me->name; e->short_name = me->short_name; e->flags = me->option_flag; e->arg_description = me->arg_desc; e->description = me->desc; switch (me->type) { case META_ENTRY_TYPE_STRING: e->arg = G_OPTION_ARG_STRING; e->arg_data = new_empty_str(); break; case META_ENTRY_TYPE_INTEGER: e->arg = G_OPTION_ARG_INT; e->arg_data = new_empty_int(); break; case META_ENTRY_TYPE_BOOLEAN: e->arg = G_OPTION_ARG_NONE; e->arg_data = new_empty_bool(); break; case META_ENTRY_TYPE_NONE: // used for help text e->arg = G_OPTION_ARG_INT; e->arg_data = new_empty_int(); break; } } return options_entries; } ================================================ FILE: src/option.c ================================================ /** * option.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "option.h" Option* option_init(GOptionEntry* entries) { df(); Option* option = g_new0(Option, 1); option->entries = entries; option->option_context = g_option_context_new("tym command line"); g_option_context_add_main_entries(option->option_context, option->entries, NULL); /* g_option_context_set_help_enabled(option->option_context, false); */ /* g_option_context_add_group(option->option_context, gtk_get_option_group(TRUE)); */ option->entries_as_table = g_hash_table_new_full( g_str_hash, g_str_equal, NULL, NULL ); return option; } void option_close(Option* option) { g_option_context_free(option->option_context); if (option->entries) { GOptionEntry* e = &option->entries[0]; while (e->long_name) { if (e->arg_data) { g_free(e->arg_data); } e++; }; g_free(option->entries); } if (option->entries_as_table) { g_hash_table_destroy(option->entries_as_table); } g_free(option); } bool option_parse(Option* option, int argc, char** argv) { df(); g_assert(option->entries); g_assert(option->entries_as_table); GError* error = NULL; char** argv_strv = g_new0(char*, argc + 1); int i = 0; while (i < argc) { argv_strv[i] = g_strdup(argv[i]); i++; } g_option_context_parse_strv(option->option_context, &argv_strv, &error); /* Rest value containes un-parsed parts that is followed by "--" double dash */ option->rest_argv = argv_strv; if (error) { g_warning("%s", error->message); return false; } GOptionEntry* e = &option->entries[0]; while (e->long_name) { g_hash_table_insert(option->entries_as_table, (void*)e->long_name, e); e++; }; return true; } void* option_get_pointer(Option* option, const char* key) { g_assert(option->entries); GOptionEntry* e = (GOptionEntry*)g_hash_table_lookup(option->entries_as_table, key); g_assert(e); return e->arg_data; } char* option_get_str(Option* option, const char* key) { char** p = (char**)option_get_pointer(option, key); if (!p) { return false; } return *p; } int option_get_int(Option* option, const char* key) { int* p = (int*)option_get_pointer(option, key); if (!p) { return false; } return *p; } bool option_get_bool(Option* option, const char* key) { gboolean* p = (gboolean*)option_get_pointer(option, key); if (!p) { return false; } return *p; } ================================================ FILE: src/option_test.c ================================================ /** * option_test.c * * Copyright (c) 2022 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "tym_test.h" #include "option.h" #include "app.h" static void test_parse() { Meta* meta = meta_init(); Option* option = option_init(meta_get_option_entries(meta)); char* argv_base[] = { "tym", "-u", "config.lua", "--width", "123", "--autohide", NULL }; int argc = sizeof(argv_base) / sizeof(char*) - 1; g_assert(option_parse(option, argc, argv_base)); g_assert(is_equal("config.lua", option_get_str(option, "use"))); g_assert(option_get_int(option, "width") == 123); g_assert(option_get_bool(option, "autohide") == true); g_assert(option_get_str(option, "theme") == NULL); g_assert(option_get_int(option, "height") == 0); g_assert_false(option_get_bool(option, "silent")); char** a = &argv_base[0]; while (*a) { dd("ARG %s", *a); a++; } dd("ARGC: %d", argc); option_close(option); } void test_option() { test_parse(); } ================================================ FILE: src/property.c ================================================ /** * property.c * * Copyright (c) 2019 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "common.h" #include "property.h" #include "regex.h" typedef enum { VTE_CJK_WIDTH_NARROW = 1, VTE_CJK_WIDTH_WIDE = 2 } VteCjkWidth; typedef void (*VteSetColorFunc)(VteTerminal*, const GdkRGBA*); // STR void setter_shell(Context* context, const char* key, const char* value) { if (!is_equal(context_get_str(context, key), value) && context->initialized) { context_log_message(context, false, "To override `%s`, you need to set value before terminal finish initialization.`", key); return; } config_set_str(context->config, key, value); } void setter_term(Context* context, const char* key, const char* value) { if (!is_equal(context_get_str(context, key), value) && context->initialized) { context_log_message(context, false, "To override `%s`, you need to set value before the terminal finish initialization.`", key); return; } config_set_str(context->config, key, value); } const char* getter_title(Context* context, const char* key) { return gtk_window_get_title(context->layout.window); } void setter_title(Context* context, const char* key, const char* value) { gtk_window_set_title(context->layout.window, value); } void setter_font(Context* context, const char* key, const char* value) { PangoFontDescription* font_desc = pango_font_description_from_string(value); vte_terminal_set_font(context->layout.vte, font_desc); pango_font_description_free(font_desc); config_set_str(context->config, key, value); } const char* getter_icon(Context* context, const char* key) { return gtk_window_get_icon_name(context->layout.window); } void setter_icon(Context* context, const char* key, const char* value) { gtk_window_set_icon_name(context->layout.window, value); } const char* getter_role(Context* context, const char* key) { const char* role = gtk_window_get_role(context->layout.window); return role ? role : ""; } void setter_role(Context* context, const char* key, const char* value) { gtk_window_set_role(context->layout.window, is_none(value) ? NULL : value); } const char* getter_cursor_shape(Context* context, const char* key) { VteCursorShape cursor_shape = vte_terminal_get_cursor_shape(context->layout.vte); switch (cursor_shape) { case VTE_CURSOR_SHAPE_IBEAM: return TYM_CURSOR_SHAPE_IBEAM; case VTE_CURSOR_SHAPE_UNDERLINE: return TYM_CURSOR_SHAPE_UNDERLINE; case VTE_CURSOR_SHAPE_BLOCK: return TYM_CURSOR_SHAPE_BLOCK; default: dw("Invalid cursor shape `%d` is detected.", cursor_shape); return TYM_CURSOR_SHAPE_BLOCK; } } void setter_cursor_shape(Context* context, const char* key, const char* value) { VteCursorShape cursor_shape = VTE_CURSOR_SHAPE_BLOCK; if (is_equal(value, TYM_CURSOR_SHAPE_BLOCK)) { } else if (is_equal(value, TYM_CURSOR_SHAPE_UNDERLINE)) { cursor_shape = VTE_CURSOR_SHAPE_UNDERLINE; } else if (is_equal(value, TYM_CURSOR_SHAPE_IBEAM)) { cursor_shape = VTE_CURSOR_SHAPE_IBEAM; } else { context_log_message(context, true, "Invalid `cursor_shape` value. (`%s` is provided). '" \ TYM_CURSOR_SHAPE_BLOCK "', '" TYM_CURSOR_SHAPE_IBEAM "' or '" \ TYM_CURSOR_SHAPE_UNDERLINE "' is available.", value); return; } vte_terminal_set_cursor_shape(context->layout.vte, cursor_shape); } const char* getter_cursor_blink_mode(Context* context, const char* key) { VteCursorBlinkMode mode = vte_terminal_get_cursor_blink_mode(context->layout.vte); switch (mode) { case VTE_CURSOR_BLINK_SYSTEM: return TYM_CURSOR_BLINK_MODE_SYSTEM; case VTE_CURSOR_BLINK_ON: return TYM_CURSOR_BLINK_MODE_ON; case VTE_CURSOR_BLINK_OFF: return TYM_CURSOR_BLINK_MODE_OFF; default: dw("Invalid cursor blink `%d` is detected.", mode); return TYM_CURSOR_BLINK_MODE_SYSTEM; } } void setter_cursor_blink_mode(Context* context, const char* key, const char* value) { VteCursorBlinkMode mode = VTE_CURSOR_BLINK_SYSTEM; if (is_equal(value, TYM_CURSOR_BLINK_MODE_SYSTEM)) { } else if (is_equal(value, TYM_CURSOR_BLINK_MODE_ON)) { mode = VTE_CURSOR_BLINK_ON; } else if (is_equal(value, TYM_CURSOR_BLINK_MODE_OFF)) { mode = VTE_CURSOR_BLINK_OFF; } else { context_log_message(context, true, "Invalid `cursor_blink_mode` value. (`%s` is provided). '" \ TYM_CURSOR_BLINK_MODE_SYSTEM "', '" TYM_CURSOR_BLINK_MODE_ON "' or '" \ TYM_CURSOR_BLINK_MODE_OFF "' is available.", value); return; } vte_terminal_set_cursor_blink_mode(context->layout.vte, mode); } const char* getter_cjk_width(Context* context, const char* key) { VteCjkWidth cjk = vte_terminal_get_cjk_ambiguous_width(context->layout.vte); switch (cjk) { case VTE_CJK_WIDTH_NARROW: return TYM_CJK_WIDTH_NARROW; case VTE_CJK_WIDTH_WIDE: return TYM_CJK_WIDTH_WIDE; default: dw("Invalid `cjk_width` `%d` is detected.", cjk); return TYM_CJK_WIDTH_NARROW; } } void setter_cjk_width(Context* context, const char* key, const char* value) { VteCjkWidth cjk = VTE_CJK_WIDTH_NARROW; if (is_equal(value, TYM_CJK_WIDTH_NARROW)) { } else if (is_equal(value, TYM_CURSOR_BLINK_MODE_ON)) { cjk = VTE_CJK_WIDTH_WIDE; } else { context_log_message(context, true, "Invalid `cjk_width` value. (`%s` is provided). '" \ TYM_CJK_WIDTH_NARROW "' or '" TYM_CJK_WIDTH_WIDE "' is available.", value); return; } vte_terminal_set_cjk_ambiguous_width(context->layout.vte, cjk); } void setter_background_image(Context* context, const char* key, const char* value) { char* css; if (is_empty(value)) { css = g_strdup("window { background-image: none; }"); } else { char* path; if (g_path_is_absolute(value)) { path = g_strdup(value); } else { char* cwd = g_get_current_dir(); path = g_build_path(G_DIR_SEPARATOR_S, cwd, value, NULL); g_free(cwd); } if (!g_file_test(path, G_FILE_TEST_EXISTS)) { context_log_message(context, true, "`%s`: `%s` does not exist.", key, path); g_free(path); return; } css = g_strdup_printf("window { background-image: url('%s'); background-size: cover; background-position: center; }", path); g_free(path); } GtkCssProvider* css_provider = gtk_css_provider_new(); GError* error = NULL; gtk_css_provider_load_from_data(css_provider, css, -1, &error); g_free(css); if (error) { context_log_message(context, true, "`%s`: Error in css: %s", key, error->message); g_error_free(error); return; } GtkStyleContext* style_context = gtk_widget_get_style_context(GTK_WIDGET(context->layout.window)); gtk_style_context_add_provider(style_context, GTK_STYLE_PROVIDER(css_provider), GTK_STYLE_PROVIDER_PRIORITY_USER); config_set_str(context->config, key, value); g_object_unref(css_provider); } void setter_uri_schemes(Context* context, const char* key, const char* value) { gchar* uri_pattern; if (g_strcmp0(TYM_SYMBOL_WILDCARD, value) == 0) { uri_pattern = g_strconcat(SCHEME, SCHEMELESS_URI, NULL); } else { int errorcode; PCRE2_SIZE erroroffset; pcre2_code* code = pcre2_compile( SCHEME_LIST, PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED | PCRE2_CASELESS | PCRE2_ENDANCHORED, &errorcode, &erroroffset, NULL ); if (!code) { g_warning("pcre2_compile failed for errorcode `%d` at offset `%d`\n", errorcode, (int)erroroffset); return; } // repetitivelly get all schemes in the list, one by one. // TODO: handle ill-formatted inputs GSList* schemes = NULL; int scheme_length_sum = 0; const char* v = value; while (true) { pcre2_match_data* match_data = pcre2_match_data_create_from_pattern(code, NULL); int res = pcre2_match( code, v, PCRE2_ZERO_TERMINATED, 0, PCRE2_ANCHORED | PCRE2_ENDANCHORED | PCRE2_NOTEMPTY, match_data, NULL ); if (res <= 0) { switch (res) { case 0: g_warning("Ovector was not big enough. This should not happen."); break; case PCRE2_ERROR_NOMATCH: g_warning("No match\n"); break; default: g_warning("PCRE2 match error %d\n", res); break; } pcre2_match_data_free(match_data); pcre2_code_free(code); return; } PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data); int length = ovector[3] - ovector[2]; if (length > 0) { schemes = g_slist_prepend(schemes, g_strndup(v + ovector[2], length)); // get first scheme scheme_length_sum += length + 1; // 1 for separater `|` or terminal null char } if (ovector[1] > ovector[3]) { // there is at least one more scheme in the list, so move the pointer forward v = &v[ovector[3] + 1]; } else { break; } pcre2_match_data_free(match_data); } pcre2_code_free(code); // if no schemes specified, remove current regex and return immediately if (scheme_length_sum == 0) { if (context->layout.uri_tag >= 0) { vte_terminal_match_remove(context->layout.vte, context->layout.uri_tag); context->layout.uri_tag = -1; config_set_str(context->config, key, value); } return; } gchar scheme_pattern[scheme_length_sum]; gchar* p = scheme_pattern; for (GSList* scheme = schemes; scheme; scheme = scheme->next) { p = g_stpcpy(p, scheme->data); *p = '|'; ++p; } scheme_pattern[scheme_length_sum - 1] = '\0'; // replace last `|` with null char uri_pattern = g_strconcat("(?:", scheme_pattern, ")", SCHEMELESS_URI, NULL); g_slist_free_full(schemes, g_free); } GError* error = NULL; VteRegex* regex = vte_regex_new_for_match(uri_pattern, -1, PCRE2_UTF | PCRE2_MULTILINE | PCRE2_CASELESS, &error); g_free(uri_pattern); if (error) { g_warning("Error when adding regex to VTE: %s", error->message); g_error_free(error); } else { if (context->layout.uri_tag >= 0) { vte_terminal_match_remove(context->layout.vte, context->layout.uri_tag); } int tag = vte_terminal_match_add_regex(context->layout.vte, regex, 0); context->layout.uri_tag = tag; vte_terminal_match_set_cursor_name(context->layout.vte, tag, "hand"); vte_regex_unref(regex); config_set_str(context->config, key, value); } } // INT int getter_width(Context* context, const char* key) { return vte_terminal_get_column_count(context->layout.vte); } void setter_width(Context* context, const char* key, int value) { context_resize(context, value, context_get_int(context, "height")); } int getter_height(Context* context, const char* key) { return vte_terminal_get_row_count(context->layout.vte); } void setter_height(Context* context, const char* key, int value) { context_resize(context, context_get_int(context, "width"), value); } int getter_scale(Context* context, const char* key) { return roundup(vte_terminal_get_font_scale(context->layout.vte) * 100); } void setter_scale(Context* context, const char* key, int value) { vte_terminal_set_font_scale(context->layout.vte, (double)value / 100); } int getter_cell_width(Context* context, const char* key) { return roundup(vte_terminal_get_cell_width_scale(context->layout.vte) * 100); } void setter_cell_width(Context* context, const char* key, int value) { vte_terminal_set_cell_width_scale(context->layout.vte, (double)value / 100); } int getter_cell_height(Context* context, const char* key) { return roundup(vte_terminal_get_cell_height_scale(context->layout.vte) * 100); } void setter_cell_height(Context* context, const char* key, int value) { vte_terminal_set_cell_height_scale(context->layout.vte, (double)value / 100); } /* DEPRECATED START */ void setter_padding_horizontal(Context* context, const char* key, int value) { gtk_box_set_child_packing(context->layout.hbox, GTK_WIDGET(context->layout.vte), true, true, value, GTK_PACK_START); config_set_int(context->config, key, value); if (value != 0) { const char* s = "Proprty `padding_horizontal` is deprecated. Use `padding_top` and `padding_bottom` instead."; g_warning("%s", s); context_notify(context, s, "Deprecation warning"); } } void setter_padding_vertical(Context* context, const char* key, int value) { gtk_box_set_child_packing(context->layout.vbox, GTK_WIDGET(context->layout.hbox), true, true, value, GTK_PACK_START); config_set_int(context->config, key, value); if (value != 0) { const char* s = "Proprty `padding_vertical` is deprecated. Use `padding_top` and `padding_bottom` instead."; g_warning("%s", s); context_notify(context, s, "Deprecation warning"); } } /* DEPRECATED END */ int getter_padding_top(Context* context, const char* key) { return gtk_widget_get_margin_top(GTK_WIDGET(context->layout.vte)); } void setter_padding_top(Context* context, const char* key, int value) { gtk_widget_set_margin_top(GTK_WIDGET(context->layout.vte), value); } int getter_padding_bottom(Context* context, const char* key) { return gtk_widget_get_margin_bottom(GTK_WIDGET(context->layout.vte)); } void setter_padding_bottom(Context* context, const char* key, int value) { gtk_widget_set_margin_bottom(GTK_WIDGET(context->layout.vte), value); } int getter_padding_left(Context* context, const char* key) { return gtk_widget_get_margin_start(GTK_WIDGET(context->layout.vte)); } void setter_padding_left(Context* context, const char* key, int value) { gtk_widget_set_margin_start(GTK_WIDGET(context->layout.vte), value); } int getter_padding_right(Context* context, const char* key) { return gtk_widget_get_margin_end(GTK_WIDGET(context->layout.vte)); } void setter_padding_right(Context* context, const char* key, int value) { gtk_widget_set_margin_end(GTK_WIDGET(context->layout.vte), value); } int getter_scrollback_length(Context* context, const char* key) { return vte_terminal_get_scrollback_lines(context->layout.vte); } void setter_scrollback_length(Context* context, const char* key, int value) { vte_terminal_set_scrollback_lines(context->layout.vte, value); } // BOOL bool getter_scroll_on_output(Context* context, const char* key) { return vte_terminal_get_scroll_on_output(context->layout.vte); } void setter_scroll_on_output(Context* context, const char* key, bool value) { vte_terminal_set_scroll_on_output(context->layout.vte, value); } bool getter_silent(Context* context, const char* key) { return !vte_terminal_get_audible_bell(context->layout.vte); } void setter_silent(Context* context, const char* key, bool value) { vte_terminal_set_audible_bell(context->layout.vte, !value); } bool getter_autohide(Context* context, const char* key) { return vte_terminal_get_mouse_autohide(context->layout.vte); } void setter_autohide(Context* context, const char* key, bool value) { vte_terminal_set_mouse_autohide(context->layout.vte, value); } bool gettter_bold_is_bright(Context* context, const char* key) { return vte_terminal_get_bold_is_bright(context->layout.vte); } void setter_bold_is_bright(Context* context, const char* key, bool value) { vte_terminal_set_bold_is_bright(context->layout.vte, value); } // COLOR static void setter_color_special(Context* context, const char* key, const char* value, VteSetColorFunc color_func) { GdkRGBA color = {}; bool valid = gdk_rgba_parse(&color, value); if (!valid) { context_log_message(context, true, "Invalid color string for '%s': %s", key, value); return; } color_func(context->layout.vte, &color); config_set_str(context->config, key, value); } void setter_color_normal(Context* context, const char* key, const char* value) { assert(value); if (is_equal(value, context_get_str(context, key))) { return; } char* target = NULL; int index = g_ascii_strtoull(&key[6], &target, 10); GdkRGBA* palette = g_new0(GdkRGBA, 16); assert(&key[6] != target || index < 0 || index > 15); char s[10] = {}; unsigned i = 0; while (i < 16) { const char* v; if (i == index) { v = value; } else { g_snprintf(s, 10, "color_%d", i); v = context_get_str(context, s); } bool valid = gdk_rgba_parse(&palette[i], v); if (!valid) { context_log_message(context, true, "Invalid color string for '%s': %s", key, value); return; } i += 1; } vte_terminal_set_colors(context->layout.vte, NULL, NULL, palette, 16); config_set_str(context->config, key, value); g_free(palette); } void setter_color_window_background(Context* context, const char* key, const char* value) { if (is_empty(value)) { gtk_widget_set_app_paintable(GTK_WIDGET(context->layout.window), false); config_set_str(context->config, key, value); return; } if (!is_none(value)) { GdkRGBA color = {}; bool valid = gdk_rgba_parse(&color, value); if (!valid) { context_log_message(context, true, "Invalid color string for '%s': %s", key, value); return; } } else { gtk_widget_queue_draw(GTK_WIDGET(context->layout.window)); } gtk_widget_set_app_paintable(GTK_WIDGET(context->layout.window), true); config_set_str(context->config, key, value); } void setter_color_background(Context* context, const char* key, const char* value) { if (is_none(value)) { #ifdef TYM_USE_TRANSPARENT vte_terminal_set_clear_background(context->layout.vte, false); config_set_str(context->config, key, value); #else context_log_message(context, true, "`NONE` for `color_background` is supported on VTE version>=0.52 (your VTE version is %s)", TYM_VTE_VERSION); #endif return; } vte_terminal_set_clear_background(context->layout.vte, true); setter_color_special(context, key, value, vte_terminal_set_color_background); } void setter_color_foreground(Context* context, const char* key, const char* value) { setter_color_special(context, key, value, vte_terminal_set_color_foreground); } void setter_color_bold(Context* context, const char* key, const char* value) { setter_color_special(context, key, value, vte_terminal_set_color_bold); } void setter_color_cursor(Context* context, const char* key, const char* value) { setter_color_special(context, key, value, vte_terminal_set_color_cursor); } void setter_color_cursor_foreground(Context* context, const char* key, const char* value) { #ifdef TYM_USE_VTE_COLOR_CURSOR_FOREGROUND setter_color_special(context, key, value, vte_terminal_set_color_cursor_foreground); #else context_log_message(context, true, "`%s` is supported on VTE version>=0.46 (your VTE version is %s)", key, TYM_VTE_VERSION); #endif } void setter_color_highlight(Context* context, const char* key, const char* value) { setter_color_special(context, key, value, vte_terminal_set_color_highlight); } void setter_color_highlight_foreground(Context* context, const char* key, const char* value) { setter_color_special(context, key, value, vte_terminal_set_color_highlight_foreground); } ================================================ FILE: src/regex_test.c ================================================ /** * regex_test.c * * Copyright (c) 2019 endaaman, iTakeshi * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "tym_test.h" #include "regex.h" #define URI "(?:http|https|file|mailto)" SCHEMELESS_URI static int check_match(int anchored, const char* pattern, const char* subject, const char* expected, int invert) { if (expected == NULL) expected = subject; int errorcode; PCRE2_SIZE erroroffset; pcre2_code* code = pcre2_compile( pattern, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_NO_UTF_CHECK | PCRE2_MULTILINE | PCRE2_CASELESS | PCRE2_NEVER_BACKSLASH_C | PCRE2_USE_OFFSET_LIMIT | (anchored ? PCRE2_ANCHORED : 0), &errorcode, &erroroffset, NULL ); if (!code) { printf("pcre2_compile failed for errorcode `%d` at offset `%d`\n", errorcode, (int)erroroffset); return 1; } pcre2_match_data_8 *match_data = pcre2_match_data_create_8(256, NULL); pcre2_match_context_8 *match_context = pcre2_match_context_create_8(NULL); pcre2_set_recursion_limit_8(match_context, 64); int res = pcre2_match( code, subject, PCRE2_ZERO_TERMINATED, 0, PCRE2_NO_UTF_CHECK | PCRE2_NOTEMPTY | (anchored ? PCRE2_ANCHORED : 0), match_data, match_context ); if (res > 0) { PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(match_data); int offset = ovector[0]; int length = ovector[1] - ovector[0]; char matched[256] = { 0 }; strncpy(matched, &subject[offset], length); if (length == strlen(expected) && strncmp(matched, expected, length) == 0) { if (invert) { printf(" UNEXPECTED MATCH: matched=\"%s\", expected=fail\n", matched); return 0; } else { printf(" MATCH SUCCESS: %s\n", matched); return 1; } } else { if (invert) { printf(" EXPECTED UNMATCH: %s\n", subject); return 1; } else { printf(" UNEXPECTED MATCH: matched=\"%s\", expected=\"%s\"\n", matched, expected); return 0; } } } else { if (invert && res == PCRE2_ERROR_NOMATCH) { printf(" EXPECTED UNMATCH: %s\n", subject); return 1; } else { char mes[256] = {}; pcre2_get_error_message(res, mes, 256); printf(" PCRE2_MATCH ERROR: code=%d, message=\"%s\"\n", res, mes); return 0; } } pcre2_match_data_free(match_data); pcre2_code_free(code); } void test_regex() { printf("Testing HOST\n"); g_assert(check_match(1, SCHEME , "http" , NULL , 0)); g_assert(check_match(1, SCHEME , "HTTP" , NULL , 0)); g_assert(check_match(1, SCHEME , "foo0.-+" , NULL , 0)); g_assert(check_match(1, SCHEME , "0foo" , NULL , 1)); // disallow non-alphabet character at the beginning printf("Testing USERINFO\n"); g_assert(check_match(1, USERINFO , "foo.bar-baz" , NULL , 0)); g_assert(check_match(1, USERINFO , "user:pass!$&'*+,;=" , NULL , 0)); g_assert(check_match(1, USERINFO , "user@" , NULL , 1)); // disallow `@` g_assert(check_match(1, USERINFO , "user:pass@" , NULL , 1)); // disallow `@` printf("Testing HOST\n"); g_assert(check_match(1 , HOST , "localhost" , NULL , 0)); g_assert(check_match(1 , HOST , "example.com" , NULL , 0)); g_assert(check_match(1 , HOST , "a-abc_d;.e.012" , NULL , 0)); g_assert(check_match(1 , HOST , "172.0.0.1" , NULL , 0)); g_assert(check_match(1 , HOST , "[2001:db8::1234:0:0:9abc]" , NULL , 0)); g_assert(check_match(1 , HOST , "あいう.example.com" , NULL , 1)); // disallow non-ascii g_assert(check_match(1 , HOST , "172.0.0.300" , NULL , 1)); // check ip-v4 range g_assert(check_match(1 , HOST , "example.co/m" , NULL , 1)); // disallow `/` printf("Testing QUERY\n"); g_assert(check_match(1 , QUERY , "foo0=bar0" , NULL , 0)); g_assert(check_match(1 , QUERY , "foo0=bar0&foo1=bar1" , NULL , 0)); g_assert(check_match(1 , QUERY , "foo0=bar0&path=baz/qux?quux" , NULL , 0)); // cases are cited from RFC3987 and RFC6068 printf("Integrated tests\n"); g_assert(check_match(0 , URI , "http://localhost:3000/index.html" , "http://localhost:3000/index.html" , 0)); g_assert(check_match(0 , URI , "http://www.example.org/D%C3%BCrst" , "http://www.example.org/D%C3%BCrst" , 0)); g_assert(check_match(0 , URI , "http://www.example.org/Dürst" , "http://www.example.org/Dürst" , 0)); g_assert(check_match(0 , URI , "http://www.example.org/D%FCrst" , "http://www.example.org/D%FCrst" , 0)); g_assert(check_match(0 , URI , "http://xn--99zt52a.example.org/%e2%80%ae" , "http://xn--99zt52a.example.org/%e2%80%ae" , 0)); g_assert(check_match(0 , URI , "\"http://ab.CDEFGH.ij/kl/mn/op.html\"" , "http://ab.CDEFGH.ij/kl/mn/op.html" , 0)); 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)); 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)); g_assert(check_match(0 , URI , "\"http://AB.CD.ef/gh/IJ/KL.html\"" , "http://AB.CD.ef/gh/IJ/KL.html" , 0)); g_assert(check_match(0 , URI , "" , "mailto:addr1@an.example,addr2@an.example" , 0)); g_assert(check_match(0 , URI , "" , "mailto:chris@example.com" , 0)); g_assert(check_match(0 , URI , "" , "mailto:infobot@example.com?subject=current-issue" , 0)); g_assert(check_match(0 , URI , "" , "mailto:infobot@example.com?body=send%20current-issue" , 0)); g_assert(check_match(0 , URI , "" , "mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index" , 0)); g_assert(check_match(0 , URI , "" , "mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E" , 0)); g_assert(check_match(0 , URI , "" , "mailto:majordomo@example.com?body=subscribe%20bamboo-l" , 0)); g_assert(check_match(0 , URI , "" , "mailto:joe@example.com?cc=bob@example.com&body=hello" , 0)); g_assert(check_match(0 , URI , "" , "mailto:addr1@an.example?to=addr2@an.example" , 0)); g_assert(check_match(0 , URI , "" , "mailto:gorby%25kremvax@example.com" , 0)); g_assert(check_match(0 , URI , "" , "mailto:unlikely%3Faddress@example.com?blat=foop" , 0)); g_assert(check_match(0 , URI , "" , "mailto:joe@an.example?cc=bob@an.example&body=hello" , 0)); g_assert(check_match(0 , URI , "." , "mailto:Mike%26family@example.org" , 0)); g_assert(check_match(0 , URI , "." , "mailto:%22not%40me%22@example.org" , 0)); g_assert(check_match(0 , URI , "." , "mailto:%22oh%5C%5Cno%22@example.org" , 0)); g_assert(check_match(0 , URI , "." , "mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org" , 0)); g_assert(check_match(0 , URI , "" , "mailto:user@example.org?subject=caf%C3%A9" , 0)); g_assert(check_match(0 , URI , "" , "mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D" , 0)); g_assert(check_match(0 , URI , "" , "mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D" , 0)); g_assert(check_match(0 , URI , "" , "mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9" , 0)); g_assert(check_match(0 , URI , "" , "mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO" , 0)); g_assert(check_match(0 , URI , "file:///" , "file:///" , 0)); g_assert(check_match(0 , URI , "file:///home/user/example.txt" , "file:///home/user/example.txt" , 0)); g_assert(check_match(0 , URI, "[link](https://example.com)" , "https://example.com" , 0)); g_assert(check_match(0 , URI, "[link](https://example.com/path)" , "https://example.com/path" , 0)); g_assert(check_match(0 , URI, "[link](https://example.com/path?query)" , "https://example.com/path?query" , 0)); g_assert(check_match(0 , URI, "[link](https://example.com/path?query#fragment)" , "https://example.com/path?query#fragment" , 0)); 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)); g_assert(check_match(0 , URI, "[link](https://example.com/pat)h?q(uer)y#fr(ag)ment)" , "https://example.com/pat" , 0)); 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)); 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)); // NOT match g_assert(check_match(0 , URI , "foo:" , NULL , 1)); // only scheme-like part printf("regex tests complete!\n"); } ================================================ FILE: src/tym.c ================================================ /** * tym.c * * Copyright (c) 2017 endaaman * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "tym.h" int main(int argc, char* argv[]) { dd("start"); app_init(); GOptionEntry* entries = meta_get_option_entries(app->meta); Option* option = option_init(entries); if (!option_parse(option, argc, argv)) { return 1; } if (option_get_bool(option, "version")) { g_print("version %s\n", PACKAGE_VERSION); return 0; } int exit_code = app_start(option, argc, argv); app_close(); return exit_code; } ================================================ FILE: src/tym_test.c ================================================ /** * tym_test.c * * Copyright (c) 2019 endaaman, iTakeshi * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ #include "tym_test.h" #include "app.h" int main(int argc, char* argv[]) { g_test_init(&argc, &argv, NULL); g_test_add_func("/tym/config", test_config); g_test_add_func("/tym/regex", test_regex); g_test_add_func("/tym/option", test_option); return g_test_run(); } ================================================ FILE: tym-daemon.desktop ================================================ [Desktop Entry] Categories=System;TerminalEmulator; Comment=Daemon process for tym Exec=tym --daemon GenericName=Terminal Icon=utilities-terminal Name=tym(daemon) StartupNotify=true Terminal=false TryExec=tym Type=Application X-GNOME-SingleWindow=false ================================================ FILE: tym-daemon.service.in ================================================ [Unit] Description=tym daemon [Service] Type=simple ExecStart=@prefix@/bin/tym --daemon [Install] WantedBy=graphical.target ================================================ FILE: tym.1.in ================================================ .TH tym 1 "@DATE@" "@VERSION@" "tym" .SH DESCRIPTION \fBtym\fR is a tiny VTE-based terminal emulator, which configurable by Lua. .SH SYNOPSIS \fBtym\fR [OPTIONS] .SH OPTIONS .IP "\fB\-h\fR, \fB\-\-help\fR" Show help message. .IP "\fB\-v\fR, \fB\-\-version\fR" Show version. .IP "\fB\-u\fR, \fB\-\-use\fR=\fI\fR" Use instead of default config file. .IP "\fB\-t\fR, \fB\-\-theme\fR=\fI\fR" Use instead of default theme file. .IP "\fB\-\-cwd\fR=\fI\fR" Use as the terminal's working directory. Must be an absolute path. .IP "\fB\-\-\fR\fI