├── .envrc ├── .gitignore ├── Changelog.md ├── LICENSE ├── Makefile.am ├── README.md ├── all_emojis.txt ├── clipboard-adapter.sh ├── configure.ac ├── m4 └── pkg.m4 ├── run-development.sh ├── screenshots ├── 1_main.png ├── 2_menu.png ├── custom_format.png ├── group_search.png ├── subgroup_search_1.png └── subgroup_search_2.png ├── shell.nix ├── src ├── actions.c ├── actions.h ├── emoji.c ├── emoji.h ├── formatter.c ├── formatter.h ├── loader.c ├── loader.h ├── menu.c ├── menu.h ├── plugin.c ├── plugin.h ├── search.c ├── search.h ├── utils.c └── utils.h └── tests ├── check_emoji.c ├── check_loader.c └── check_utils.c /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.m4 3 | configure 4 | config.* 5 | Makefile.in 6 | ar-lib 7 | autom4te.cache/ 8 | compile 9 | depcomp 10 | install-sh 11 | ltmain.sh 12 | missing 13 | test-driver 14 | .deps 15 | 16 | compile_commands.json 17 | .ccls-cache/ 18 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Development Version 2 | 3 | - Nothing yet. 4 | 5 | # Version 4.1.0 (2005-04-04) 6 | 7 | ## Changed 8 | 9 | - Updated [`emoji-data`][emoji-data] to [version 2.7][emoji-data-2.7], adding more emojis (Emoji 16.0) and better keywords. 10 | 11 | # Version 4.0.0 (2024-07-23) 12 | 13 | This version breaks away from Rofi 1.7 and starts to work against the as of yet 14 | unreleased Rofi 1.8. This is because this version of Rofi contains breaking 15 | changes to the Plugin ABI, and a lot of Rofi forks have started to appear that 16 | are based on the 1.8 branch. 17 | 18 | If you want to use Rofi 1.7, stick to the 3.x branch. 19 | 20 | ## Breaking changes 21 | 22 | - Support + require unreleased version of Rofi in order to compile. 23 | - Using new Plugin interface in Rofi `next`. ([LordMZTE](https://mzte.de/)) 24 | 25 | ## Added 26 | 27 | - Updated [`emoji-data`][emoji-data] to [version 2.6][emoji-data-2.6], adding 28 | more emojis (Emoji 15.1) and better keywords. 29 | 30 | - `copy_no_insert` mode for inserting without using the clipboard. May not work 31 | everywhere. ([jones-josh](https://github.com/jones-josh)) 32 | 33 | # Version 3.4.1 (2024-07-23) 34 | 35 | ## Fixed 36 | 37 | - Support `copy_no_insert` mode through CLI flags. ([jones-josh][jones-josh]) 38 | 39 | # Version 3.4.0 (2024-07-18) 40 | 41 | ## Added 42 | 43 | - `copy_no_insert` mode for inserting without using the clipboard. May not work 44 | everywhere. ([jones-josh][jones-josh]) 45 | 46 | # Version 3.3.0 (2024-02-27) 47 | 48 | ## Changed 49 | 50 | - Updated [`emoji-data`][emoji-data] to [version 2.6][emoji-data-2.6], adding 51 | more emojis (Emoji 15.1) and better keywords. 52 | 53 | # Version 3.2.0 (2023-04-17) 54 | 55 | ## Changed 56 | 57 | - Updated [`emoji-data`][emoji-data] to [version 2.5][emoji-data-2.5], adding 58 | more emojis (Emoji 15) and better keywords. 59 | 60 | # Version 3.2.0 (2023-04-17) 61 | 62 | ## Changed 63 | 64 | - Updated [`emoji-data`][emoji-data] to [version 2.5][emoji-data-2.5], adding 65 | more emojis (Emoji 15) and better keywords. 66 | 67 | # Version 3.1.0 (2022-09-12) 68 | 69 | ## Added 70 | 71 | - Added menu option to insert emoji no matter which mode is currently active. 72 | ([Alexander Schulz (hlfbt)](https://github.com/hlfbt)) 73 | - Change default menu item between Copy and Insert based on the current mode 74 | such that the default is the opposite of the mode. ([Alexander Schulz 75 | (hlfbt)](https://github.com/hlfbt)) 76 | 77 | # Version 3.0.1 (2022-07-24) 78 | 79 | ## Fixed 80 | 81 | - Make project build without `pkgconf` dependency; only `pkg-config` binary 82 | and other listed dependencies should be required. 83 | 84 | # Version 3.0.0 (2022-07-05) 85 | 86 | ## Breaking changes 87 | 88 | - Adapter script has a new call signature. Read the `--help` output to see it. 89 | 90 | ## Added 91 | 92 | - Insert mode (that tries) to insert emoji directly into foreground app. 93 | - Menu mode with options on what to do with the emoji. 94 | - Stdout mode that emits the selected emoji to stdout. 95 | - Group and subgroup filter for searches using `@groupname` or `#subgroup`. 96 | - The `-emoji-mode` option to set default selection mode. 97 | - Quick shortcut to open menu, no matter what the default mode is. 98 | - Quick shortcut to copy emoji, no matter what the default mode is. 99 | - The `-emoji-file` option to read custom emoji databases. 100 | - Documentation about the format of the Emoji database. 101 | - The `-emoji-format` option to set custom rendering of lines. 102 | 103 | ## Changed 104 | 105 | - New default selection mode: Insert. 106 | - The default rendering of Emoji entries. 107 | 108 | - No longer showing group and subgroup. 109 | - No empty parenthesis for entries without keywords. 110 | - Names are capitalized. 111 | 112 | **Before:** 113 | 114 | > ☺️ **smiling face** () [Smileys & Emotion / face-affection] 115 | 116 | **After:** 117 | 118 | > ☺️ **Smiling face** 119 | 120 | # Version 2.3.0 (2022-02-02) 121 | 122 | ## Added 123 | 124 | - Support for `copyq` X11 clipboard adapter. ([Muhammad Mabrouk 125 | (M-Mabrouk1)](https://github.com/M-Mabrouk1)) 126 | - Emoji 14.0 emojis and latest keywords from CLDR 127 | 128 | ## Changed 129 | 130 | - Allow clipboard-adapter.sh script to be replaced and run by a different 131 | interpreter than `/bin/sh`. 132 | 133 | # Version 2.2.0 (2021-05-19) 134 | 135 | ## Added 136 | 137 | - Full `LICENSE` file, detailing the MIT license mentioned in the README. 138 | 139 | ## Changed 140 | 141 | - Updated [`emoji-data`][emoji-data] to version 2.3, adding more emojis and 142 | better keywords. 143 | 144 | # Version 2.1.2 (2020-03-30) 145 | 146 | ## Fixed 147 | 148 | - Build configuration now includes undocumented Cairo dependency. 149 | 150 | # Version 2.1.1 (2020-03-23) 151 | 152 | ## Fixed 153 | 154 | - Wayland detection under Sway. 155 | 156 | # Version 2.1.0 (2019-10-06) 157 | 158 | Change clipboard adapter to use arguments instead of STDIN, which should 159 | prevent some issues from occurring regarding subprocesses getting stuck in a 160 | blocking read. 161 | 162 | ## Changed 163 | 164 | - Clipboard adapter script now accepts emoji bytes as an argument instead of 165 | standard input. 166 | 167 | # Version 2.0 (2019-07-23) 168 | 169 | Due to a lot of issues with the "insert" action on many environments, and 170 | inconsistent support for the primary selection, this feature has now been 171 | dropped and the plugin is again only doing clipboard copying. 172 | 173 | ## Removed 174 | 175 | - Direct insert via Enter; now this key also copies the emoji to the 176 | clipboard to let you paste it manually. 177 | - `xdotool` as a supported adapter. 178 | 179 | # Version 1.2 (2019-06-16) 180 | 181 | This is a large upgrade to the emoji data, which restores a few things that 182 | went missing in version 1.1. 183 | 184 | ## Fixed 185 | 186 | - Named country flags are back! 187 | - Emoji names are present again (from 1.0), together with all the keywords from 188 | 1.1. 189 | 190 | ## Changed 191 | 192 | - Updated to [`emoji-data`][emoji-data] version 2.0. 193 | 194 | # Version 1.1 (2019-06-02) 195 | 196 | First new feature release! This release does a lot of improvements and adds 197 | some new features. 198 | 199 | ## Fixed 200 | 201 | - Rofi plugin directory is now detected automatically via `pkg-config`. 202 | 203 | ## Added 204 | 205 | - Changed default action to insert emoji via `xdotool`. 206 | - Hold Shift to copy it like before. 207 | - Support for `xclip`. 208 | - Experimental, untested support for Wayland via `wl-clipboard`. 209 | - Adapter script for adding support for other clipboard manager. 210 | 211 | ## Changed 212 | 213 | - More emojis: Unicode 12.0. 214 | - Emojis have multiple keywords now (for example, 😎 is now also _cool_). 215 | 216 | ## Known issues 217 | 218 | - Country flags are no longer searchable via country names. See 219 | [Mange/emoji-data][emoji-data]. 220 | 221 | # Version 1.0 (2018-05-11) 222 | 223 | Initial release with Unicode 11.0. 224 | 225 | [emoji-data]: https://github.com/Mange/emoji-data 226 | [emoji-data-2.5]: https://github.com/Mange/emoji-data/releases/tag/v2.5 227 | [emoji-data-2.6]: https://github.com/Mange/emoji-data/releases/tag/v2.6 228 | [emoji-data-2.7]: https://github.com/Mange/emoji-data/releases/tag/v2.7 229 | [jones-josh]: https://github.com/jones-josh 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Magnus Bergmark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | ACLOCAL_AMFLAGS=-I m4 2 | plugindir=${rofi_PLUGIN_INSTALL_DIR}/ 3 | 4 | plugin_LTLIBRARIES = emoji.la 5 | 6 | dist_pkgdata_DATA = all_emojis.txt README.md LICENSE 7 | dist_pkgdata_SCRIPTS = clipboard-adapter.sh 8 | 9 | emoji_la_SOURCES=\ 10 | src/emoji.c \ 11 | src/utils.c \ 12 | src/loader.c \ 13 | src/formatter.c \ 14 | src/menu.c \ 15 | src/search.c \ 16 | src/actions.c \ 17 | src/plugin.c 18 | 19 | emoji_la_CFLAGS= @glib_CFLAGS@ @rofi_CFLAGS@ @cairo_CFLAGS@ 20 | emoji_la_LIBADD= @glib_LIBS@ @rofi_LIBS@ @cairo_LIBS@ 21 | emoji_la_LDFLAGS= -module -avoid-version 22 | 23 | if HAVE_CHECK 24 | check_PROGRAMS = tests/check_utils tests/check_emoji tests/check_loader 25 | TESTS = tests/check_utils tests/check_emoji tests/check_loader 26 | 27 | tests_check_utils_SOURCES = tests/check_utils.c src/utils.c 28 | tests_check_utils_CFLAGS = $(CFLAGS) $(CHECK_CFLAGS) @glib_CFLAGS@ @rofi_CFLAGS@ @cairo_CFLAGS@ 29 | tests_check_utils_LDADD = $(LDFLAGS) $(CHECK_LIBS) @glib_LIBS@ @rofi_LIBS@ @cairo_LIBS@ 30 | 31 | tests_check_emoji_SOURCES = tests/check_emoji.c src/emoji.c 32 | tests_check_emoji_CFLAGS = $(CFLAGS) $(CHECK_CFLAGS) @glib_CFLAGS@ @rofi_CFLAGS@ @cairo_CFLAGS@ 33 | tests_check_emoji_LDADD = $(LDFLAGS) $(CHECK_LIBS) @glib_LIBS@ @rofi_LIBS@ @cairo_LIBS@ 34 | 35 | tests_check_loader_SOURCES = tests/check_loader.c src/loader.c src/emoji.c src/utils.c 36 | tests_check_loader_CFLAGS = $(CFLAGS) $(CHECK_CFLAGS) @glib_CFLAGS@ @rofi_CFLAGS@ @cairo_CFLAGS@ 37 | tests_check_loader_LDADD = $(LDFLAGS) $(CHECK_LIBS) @glib_LIBS@ @rofi_LIBS@ @cairo_LIBS@ 38 | else 39 | check_PROGRAMS = 40 | TESTS = 41 | endif 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rofi emoji plugin 2 | 3 | An emoji selector plugin for Rofi that copies the selected emoji to the 4 | clipboard, among other things. 5 | 6 | > [!Note] 7 | > See the [`3-x-stable` branch][stable-branch] for documentation of 3.x branch, 8 | > which is the version that is meant for Rofi ≤ 1.7.5. 9 | 10 | ## Screenshots 11 | 12 | ![Screenshot showing a Rofi window searching for emojis containing "uni", the 13 | emoji for "Unicorn face" being selected](screenshots/1_main.png) 14 | 15 | ![Screenshot of the Emoji menu for the Unicorn emoji](screenshots/2_menu.png) 16 | 17 | ## Usage 18 | 19 | Run Rofi like: 20 | 21 | ```bash 22 | rofi -modi emoji -show emoji 23 | ``` 24 | 25 | ### Keybindings 26 | 27 | | Keymap | Default key in Rofi | Effect | 28 | | ----------------- | --------------------------------- | ---------------------------------------------- | 29 | | `kb-accept-entry` | Enter | Select emoji (see **Mode** below). | 30 | | `kb-accept-alt` | Shift+Enter | Opens a menu for the Emoji with other actions. | 31 | | `kb-custom-1` | Alt+1 | Copy emoji. | 32 | 33 | > 💡 **Tip:** Change your `kb-custom-1` to Ctrl+C. 34 | > 35 | > ``` 36 | > rofi -modi emoji -show emoji -kb-secondary-copy "" -kb-custom-1 Ctrl+c 37 | > ``` 38 | 39 | ### Search patterns 40 | 41 | You can type parts of the Emojis name or keywords to find it. If you want to 42 | limit your search to particular groups or subgroups you can use prefix 43 | searches: 44 | 45 | - `@sym` - Limit to emojis that have `sym` inside of its Group, like `Symbols`. 46 | 47 | ![](screenshots/group_search.png) 48 | 49 | - `#mammal` - Limit to emojis that have `mammal` inside of its Subgroup, e.g. 50 | `Animals & Nature » Animal-mammal`. 51 | 52 | ![](screenshots/subgroup_search_1.png) 53 | ![](screenshots/subgroup_search_2.png) 54 | 55 | You can only use one instance inside of each prefix. The latest one wins: 56 | 57 | - `@foo bar @baz` - Searches for `bar` on all emojis in a group including `baz`. 58 | 59 | If you want to know which group and subgroup a particular emoji has, you can 60 | open the menu on it. See **Menu** below. 61 | 62 | ### Menu 63 | 64 | By pressing the `kb-accept-alt` binding on an emoji the plugin will open a menu 65 | for that particular emoji. The menu will provide you with alternative actions, 66 | like copying the Emojis name or codepoint. Metadata about the emoji will also 67 | be shown inside the menu in case you want to know what group it belongs to in 68 | order to find it faster in the future. 69 | 70 | ### Command line arguments 71 | 72 | Due to a limitation in Rofi's plugin system, this plugin cannot append 73 | additional options to the output of `rofi -help`. 74 | 75 | The plugin adds the following command line arguments to `rofi`: 76 | 77 | | Name | Description | 78 | | --------------- | -------------------------------------------------------- | 79 | | `-emoji-mode` | Default action when selecting an emoji in the search. | 80 | | `-emoji-file` | Path to custom emoji database file. | 81 | | `-emoji-format` | Custom formatting string for rendering lines. See below. | 82 | 83 | #### Mode 84 | 85 | The plugin supports five modes: 86 | 87 | 1. `insert` (default) - Copies the selected emoji, and then tries to insert it 88 | directly in the focused window. 89 | 2. `copy` - Only copies the selected emoji to your clipboard without trying to 90 | insert anything. 91 | 3. `insert_no_copy` - Tries to insert the emoji in the focused window, but 92 | without copying anything. 93 | 4. `menu` - Open the menu. Useful if you prefer to always get options when just 94 | pressing Enter. 95 | 5. `stdout` - Write selected emoji to standard output. This is useful if you 96 | want to use the emoji selector inside of a shell pipeline, like Rofi's 97 | `-dmenu` mode. It will use the `-format` argument to customize the outputted 98 | text, just like `-dmenu`. 99 | 100 | Inserting is not very reliable under X11 since different toolkits respond 101 | differently to the X11 events that are emitted when trying to write unicode 102 | characters. If inserting does not work for you, you can still paste the emoji 103 | as before. 104 | 105 | In case you have any issues with insertion mode, you can override the default 106 | mode using a `-emoji-mode copy` command line argument to Rofi. 107 | 108 | The `copy` mode is also always available on `kb-custom-1`. 109 | 110 | #### Format 111 | 112 | The formatting string should be valid [Pango markup][pango] with placeholders 113 | for the Emoji values found in the database. 114 | 115 | The logic of this follows the same rule as Rofi's `-ssh-command` option, 116 | quickly summarized as such: 117 | 118 | - Items between curly braces (`{}`) are replaced with [Pango][pango]-escaped text. 119 | - Wrapping an item inside brackets (`[]`) will hide the entire section if the 120 | value is empty. 121 | 122 | The default format string is this: 123 | 124 | ```html 125 | {emoji} {name}[ 126 | ({keywords})] 127 | ``` 128 | 129 | This will render the emoji with its name next to it in bold, and if the emoji 130 | has any keywords they will be shown in a parenthesised list with a smaller font 131 | size. 132 | 133 | | Item | Example | 134 | | ----------- | ----------------------------------------------------------------------- | 135 | | `emoji` | 🤣 | 136 | | `name` | Rolling on the floor laughing | 137 | | `group` | Smileys & Emotion | 138 | | `subgroup` | Face-smiling | 139 | | `keywords` | Face, Floor, Laugh, Rofl, Rolling, Rolling on the floor laughing, Rotfl | 140 | | `codepoint` | U+1F923 | 141 | 142 | | | | 143 | | ------------ | ------------------------------------------------------------------------------------------------ | 144 | | ⚠️ **NOTE:** | Rofi does not have a way to escape brackets, so you may not use literal `[]` inside your output. | 145 | 146 | ##### Example 147 | 148 | ![](screenshots/custom_format.png) 149 | 150 | ```bash 151 | rofi -modi emoji -show emoji -emoji-format '{emoji}' 152 | ``` 153 | 154 | ## Dependencies 155 | 156 | | rofi-emoji version | Rofi version | 157 | | -------------------: | -----------: | 158 | | [3.x][stable-branch] | ≤ 1.7.5 | 159 | | [4.x][master-branch] | ≥ 1.7.6 | 160 | 161 | ### Optional dependencies 162 | 163 | In order to actually use rofi-emoji an "adapter" need to be installed, as 164 | appropriate for your environment. 165 | 166 | | Kind | Dependency | Environment | 167 | | ------ | ------------ | ----------- | 168 | | Copy | xsel | X11 | 169 | | Copy | xclip | X11 | 170 | | Copy | copyq | X11 | 171 | | Copy | wl-clipboard | Wayland | 172 | | | | | 173 | | Insert | xdotool | X11 | 174 | | Insert | wtype | Wayland | 175 | 176 | You only need to install the ones required for your environment and usage. Note 177 | that in order to use `insert` mode you must also install a `copy` adapter as 178 | `insert` also copies as a fallback. 179 | 180 | ## Installation 181 | 182 | 183 | Packaging status 184 | 185 | 186 | ### Arch Linux 187 | 188 | ```bash 189 | pacman -S rofi-emoji 190 | ``` 191 | 192 | There's also a community-managed AUR package called 193 | [`rofi-emoji-git`](https://aur.archlinux.org/packages/rofi-emoji-git). 194 | 195 | ```bash 196 | paru -S rofi-emoji-git 197 | ``` 198 | 199 | ### Manjaro 200 | 201 | ```bash 202 | pacman -S rofi-emoji 203 | ``` 204 | 205 | ### Void Linux 206 | 207 | ```bash 208 | xbps-install -S rofi-emoji 209 | ``` 210 | 211 | ### NixOS or Home Manager 212 | 213 | If you are using `home-manager` you should set up this as a plugin to your Rofi 214 | install: 215 | 216 | ```nix 217 | programs.rofi = { 218 | enable = true; 219 | plugins = [pkgs.rofi-emoji]; 220 | # ... 221 | } 222 | ``` 223 | 224 | If you are using plain NixOS, then you might need to set up your own plugin 225 | path to the command. 226 | 227 | ```nix 228 | environment.systemPackages = [ 229 | # ... 230 | ( 231 | pkgs.rofi.override (old: { 232 | plugins = old.plugins ++ [pkgs.rofi-emoji]; 233 | }) 234 | ) 235 | # ... 236 | ]; 237 | ``` 238 | 239 | ### Compile from source 240 | 241 | `rofi-emoji` uses autotools as its build system. On Debian/Ubuntu based systems 242 | you will need to install the packages first: 243 | 244 | - `rofi-dev` 245 | - `autoconf` 246 | - `automake` 247 | - `libtool-bin` 248 | - `libtool` 249 | 250 | Download the source and run the following to install it: 251 | 252 | ```bash 253 | autoreconf -i 254 | mkdir build 255 | cd build/ 256 | ../configure 257 | make 258 | sudo make install 259 | ``` 260 | 261 | If you plan on developing the code and want to test the plugin, you can also 262 | run `./run-development.sh`, which will do all setup steps for you and then 263 | start Rofi using the locally compiled plugin and clipboard adapter script. This 264 | will not affect your system and does not require root. 265 | 266 | > [!Note] 267 | > Don't forget to also install the appropriate [optional 268 | > dependencies](#optional-dependencies) in order for the plugin to work. 269 | 270 | ### Running tests 271 | 272 | Also install `check` and run the following commands after doing the **Compile 273 | from source** steps above. 274 | 275 | ```bash 276 | # In project root 277 | automake -a 278 | cd build 279 | ../configure --with-check 280 | make check VERBOSE=true 281 | ``` 282 | 283 | There is not a lot of things to test here since Rofi doesn't expose any of its 284 | internal methods as a library to link the test binaries against, which means 285 | it's not possible to compile and link any tests for any files where a Rofi 286 | dependency is used. 287 | 288 | ## Emoji database 289 | 290 | When installing, the emoji database is installed in 291 | `$PREFIX/share/rofi-emoji/all_emojis.txt`. 292 | 293 | The plugin will search `$XDG_DATA_DIRS` for a directory where 294 | `rofi-emoji/all_emojis.txt` exists in if no `-emoji-file` option is set. 295 | 296 | If the plugin cannot find the file, make sure `$XDG_DATA_DIRS` is set 297 | correctly. If it is unset it should default to `/usr/local/share:/usr/share`, 298 | which works with the most common prefixes. 299 | 300 | ### Custom database 301 | 302 | The emoji database is a plain-text file that lists one emoji per line. It has 303 | the following format: 304 | 305 | ``` 306 | EMOJI_BYTES - The bytes of the emoji, for example "🤣". This is what is acted on. 307 | \t - Tab character 308 | GROUP_NAME - The name of the group, for example "Smileys & Emotion". 309 | \t - Tab character 310 | SUBGROUP - The name of the subgroup, for example "face-smiling". 311 | \t - Tab character 312 | NAME - Name of emoji, for example "rolling on the floor laughing". 313 | \t - Tab character 314 | KEYWORD_1 - Keyword of the emoji, for example "rofl". 315 | (" | " KEYWORD_n)… - Additional keywords are added with pipes and spaces between them. 316 | \n - Newline ends the current record. 317 | ``` 318 | 319 | **Example rows:** 320 | 321 | ``` 322 | 🤣 Smileys & Emotion face-smiling rolling on the floor laughing face | floor | laugh | rofl | rolling | rolling on the floor laughing | rotfl 323 | 😂 Smileys & Emotion face-smiling face with tears of joy face | face with tears of joy | joy | laugh | tear 324 | 🙂 Smileys & Emotion face-smiling slightly smiling face face | slightly smiling face | smile 325 | 🙃 Smileys & Emotion face-smiling upside-down face face | upside-down | upside down | upside-down face 326 | ``` 327 | 328 | ### Updating default database to a newer version 329 | 330 | The list is copied from the [Mange/emoji-data][emoji-data] repo. 331 | 332 | ## License 333 | 334 | This plugin is released under the MIT license. See `LICENSE` for more details. 335 | 336 | [emoji-data]: https://github.com/Mange/emoji-data 337 | [pango]: https://docs.gtk.org/Pango/pango_markup.html 338 | [master-branch]: https://github.com/Mange/rofi-emoji/tree/master 339 | [stable-branch]: https://github.com/Mange/rofi-emoji/tree/3-x-stable 340 | -------------------------------------------------------------------------------- /clipboard-adapter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage() { 4 | cat <&2 75 | exit 1 76 | ;; 77 | esac 78 | } 79 | 80 | stderr_is_null() { 81 | test /proc/self/fd/2 -ef /dev/null 82 | } 83 | 84 | show_error() { 85 | if stderr_is_null && hash notify-send 2>/dev/null; then 86 | notify-send rofi-emoji "$@" 87 | else 88 | echo "$@" >&2 89 | fi 90 | } 91 | 92 | perform_copy() { 93 | tool=$(find_copy_tool) 94 | 95 | case "$tool" in 96 | xsel) 97 | xsel --clipboard --input 98 | ;; 99 | xclip) 100 | xclip -selection clipboard -in 101 | ;; 102 | copyq) 103 | copyq copy - 104 | ;; 105 | wl-copy) 106 | wl-copy 107 | ;; 108 | "") 109 | show_error "Could not find any tool to handle copying. Please install a clipboard handler." 110 | exit 1 111 | ;; 112 | *) 113 | show_error "$tool has no implementation for copying yet" 114 | exit 2 115 | ;; 116 | esac 117 | } 118 | 119 | perform_insert() { 120 | tool=$(find_insert_tool) 121 | 122 | case "$tool" in 123 | xdotool) 124 | xdotool type --clearmodifiers --file - 125 | ;; 126 | wtype) 127 | wtype - 128 | ;; 129 | "") 130 | show_error "Could not find any tool to handle insertion. Please install xdotool or wtype." 131 | exit 1 132 | ;; 133 | *) 134 | show_error "$tool has no implementation for insertion yet" 135 | exit 2 136 | ;; 137 | esac 138 | } 139 | 140 | # Print out the first argument and return true if that argument is an installed 141 | # command. Prints nothing and returns false if the argument is not an installed 142 | # command. 143 | try_tool() { 144 | if hash "$1" 2>/dev/null; then 145 | echo "$1" 146 | return 0 147 | else 148 | return 1 149 | fi 150 | } 151 | 152 | # Find the best clipboard tool to use. 153 | find_copy_tool() { 154 | if [ "$XDG_SESSION_TYPE" = wayland ] || [ -n "$WAYLAND_DISPLAY" ]; then 155 | try_tool wl-copy || return 1 156 | else 157 | try_tool xsel || try_tool xclip || try_tool copyq || return 1 158 | fi 159 | } 160 | 161 | # Find the best insertion tool to use. 162 | find_insert_tool() { 163 | if [ "$XDG_SESSION_TYPE" = wayland ] || [ -n "$WAYLAND_DISPLAY" ]; then 164 | try_tool wtype || return 1 165 | else 166 | try_tool xdotool || return 1 167 | fi 168 | } 169 | 170 | main "$@" 171 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_INIT([rofi-emoji], [4.1.0], [https://github.com/Mange/rofi-emoji],[],[https://github.com/Mange/rofi-emoji/issues]) 2 | 3 | AC_CONFIG_HEADERS([config.h]) 4 | 5 | 6 | AC_CONFIG_MACRO_DIRS([m4]) 7 | dnl --------------------------------------------------------------------- 8 | dnl Setup automake to be silent and in foreign mode. 9 | dnl We want xz distribution 10 | dnl --------------------------------------------------------------------- 11 | AM_INIT_AUTOMAKE([-Wall -Werror foreign subdir-objects dist-xz]) 12 | AM_SILENT_RULES([yes]) 13 | 14 | dnl --------------------------------------------------------------------- 15 | dnl Check for compiler 16 | dnl --------------------------------------------------------------------- 17 | AC_PROG_CC([clang gcc cc]) 18 | 19 | dnl --------------------------------------------------------------------- 20 | dnl C to Object rules. 21 | dnl --------------------------------------------------------------------- 22 | AM_PROG_CC_C_O 23 | 24 | dnl --------------------------------------------------------------------- 25 | dnl System extensions 26 | dnl --------------------------------------------------------------------- 27 | AC_USE_SYSTEM_EXTENSIONS 28 | 29 | dnl --------------------------------------------------------------------- 30 | dnl Static libraries programs 31 | dnl --------------------------------------------------------------------- 32 | AM_PROG_AR 33 | 34 | dnl --------------------------------------------------------------------- 35 | dnl Base CFLAGS 36 | dnl --------------------------------------------------------------------- 37 | AM_CFLAGS="-Wall -Wextra -Wparentheses -Winline -pedantic -Wunreachable-code" 38 | 39 | 40 | dnl --------------------------------------------------------------------- 41 | dnl Check dependencies 42 | dnl --------------------------------------------------------------------- 43 | PKG_PROG_PKG_CONFIG 44 | 45 | 46 | dnl --------------------------------------------------------------------- 47 | dnl PKG_CONFIG based dependencies 48 | dnl --------------------------------------------------------------------- 49 | PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.40 gio-unix-2.0 gmodule-2.0 ]) 50 | PKG_CHECK_MODULES([cairo], [cairo]) 51 | PKG_CHECK_MODULES([rofi], [rofi]) 52 | 53 | dnl --------------------------------------------------------------------- 54 | dnl Testing 55 | dnl --------------------------------------------------------------------- 56 | PKG_HAVE_WITH_MODULES([CHECK], [check]) 57 | 58 | [rofi_PLUGIN_INSTALL_DIR]="`$PKG_CONFIG --variable=pluginsdir rofi`" 59 | AC_SUBST([rofi_PLUGIN_INSTALL_DIR]) 60 | 61 | LT_INIT([disable-static]) 62 | 63 | dnl --------------------------------------------------------------------- 64 | dnl Add extra compiler flags 65 | dnl --------------------------------------------------------------------- 66 | AC_SUBST([AM_CFLAGS]) 67 | 68 | AC_CONFIG_FILES([Makefile ]) 69 | AC_OUTPUT 70 | -------------------------------------------------------------------------------- /m4/pkg.m4: -------------------------------------------------------------------------------- 1 | # pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*- 2 | # serial 11 (pkg-config-0.29.1) 3 | 4 | dnl Copyright © 2004 Scott James Remnant . 5 | dnl Copyright © 2012-2015 Dan Nicholson 6 | dnl 7 | dnl This program is free software; you can redistribute it and/or modify 8 | dnl it under the terms of the GNU General Public License as published by 9 | dnl the Free Software Foundation; either version 2 of the License, or 10 | dnl (at your option) any later version. 11 | dnl 12 | dnl This program is distributed in the hope that it will be useful, but 13 | dnl WITHOUT ANY WARRANTY; without even the implied warranty of 14 | dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | dnl General Public License for more details. 16 | dnl 17 | dnl You should have received a copy of the GNU General Public License 18 | dnl along with this program; if not, write to the Free Software 19 | dnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 20 | dnl 02111-1307, USA. 21 | dnl 22 | dnl As a special exception to the GNU General Public License, if you 23 | dnl distribute this file as part of a program that contains a 24 | dnl configuration script generated by Autoconf, you may include it under 25 | dnl the same distribution terms that you use for the rest of that 26 | dnl program. 27 | 28 | dnl PKG_PREREQ(MIN-VERSION) 29 | dnl ----------------------- 30 | dnl Since: 0.29 31 | dnl 32 | dnl Verify that the version of the pkg-config macros are at least 33 | dnl MIN-VERSION. Unlike PKG_PROG_PKG_CONFIG, which checks the user's 34 | dnl installed version of pkg-config, this checks the developer's version 35 | dnl of pkg.m4 when generating configure. 36 | dnl 37 | dnl To ensure that this macro is defined, also add: 38 | dnl m4_ifndef([PKG_PREREQ], 39 | dnl [m4_fatal([must install pkg-config 0.29 or later before running autoconf/autogen])]) 40 | dnl 41 | dnl See the "Since" comment for each macro you use to see what version 42 | dnl of the macros you require. 43 | m4_defun([PKG_PREREQ], 44 | [m4_define([PKG_MACROS_VERSION], [0.29.1]) 45 | m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1, 46 | [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])]) 47 | ])dnl PKG_PREREQ 48 | 49 | dnl PKG_PROG_PKG_CONFIG([MIN-VERSION]) 50 | dnl ---------------------------------- 51 | dnl Since: 0.16 52 | dnl 53 | dnl Search for the pkg-config tool and set the PKG_CONFIG variable to 54 | dnl first found in the path. Checks that the version of pkg-config found 55 | dnl is at least MIN-VERSION. If MIN-VERSION is not specified, 0.9.0 is 56 | dnl used since that's the first version where most current features of 57 | dnl pkg-config existed. 58 | AC_DEFUN([PKG_PROG_PKG_CONFIG], 59 | [m4_pattern_forbid([^_?PKG_[A-Z_]+$]) 60 | m4_pattern_allow([^PKG_CONFIG(_(PATH|LIBDIR|SYSROOT_DIR|ALLOW_SYSTEM_(CFLAGS|LIBS)))?$]) 61 | m4_pattern_allow([^PKG_CONFIG_(DISABLE_UNINSTALLED|TOP_BUILD_DIR|DEBUG_SPEW)$]) 62 | AC_ARG_VAR([PKG_CONFIG], [path to pkg-config utility]) 63 | AC_ARG_VAR([PKG_CONFIG_PATH], [directories to add to pkg-config's search path]) 64 | AC_ARG_VAR([PKG_CONFIG_LIBDIR], [path overriding pkg-config's built-in search path]) 65 | 66 | if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then 67 | AC_PATH_TOOL([PKG_CONFIG], [pkg-config]) 68 | fi 69 | if test -n "$PKG_CONFIG"; then 70 | _pkg_min_version=m4_default([$1], [0.9.0]) 71 | AC_MSG_CHECKING([pkg-config is at least version $_pkg_min_version]) 72 | if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then 73 | AC_MSG_RESULT([yes]) 74 | else 75 | AC_MSG_RESULT([no]) 76 | PKG_CONFIG="" 77 | fi 78 | fi[]dnl 79 | ])dnl PKG_PROG_PKG_CONFIG 80 | 81 | dnl PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 82 | dnl ------------------------------------------------------------------- 83 | dnl Since: 0.18 84 | dnl 85 | dnl Check to see whether a particular set of modules exists. Similar to 86 | dnl PKG_CHECK_MODULES(), but does not set variables or print errors. 87 | dnl 88 | dnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG]) 89 | dnl only at the first occurence in configure.ac, so if the first place 90 | dnl it's called might be skipped (such as if it is within an "if", you 91 | dnl have to call PKG_CHECK_EXISTS manually 92 | AC_DEFUN([PKG_CHECK_EXISTS], 93 | [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl 94 | if test -n "$PKG_CONFIG" && \ 95 | AC_RUN_LOG([$PKG_CONFIG --exists --print-errors "$1"]); then 96 | m4_default([$2], [:]) 97 | m4_ifvaln([$3], [else 98 | $3])dnl 99 | fi]) 100 | 101 | dnl _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES]) 102 | dnl --------------------------------------------- 103 | dnl Internal wrapper calling pkg-config via PKG_CONFIG and setting 104 | dnl pkg_failed based on the result. 105 | m4_define([_PKG_CONFIG], 106 | [if test -n "$$1"; then 107 | pkg_cv_[]$1="$$1" 108 | elif test -n "$PKG_CONFIG"; then 109 | PKG_CHECK_EXISTS([$3], 110 | [pkg_cv_[]$1=`$PKG_CONFIG --[]$2 "$3" 2>/dev/null` 111 | test "x$?" != "x0" && pkg_failed=yes ], 112 | [pkg_failed=yes]) 113 | else 114 | pkg_failed=untried 115 | fi[]dnl 116 | ])dnl _PKG_CONFIG 117 | 118 | dnl _PKG_SHORT_ERRORS_SUPPORTED 119 | dnl --------------------------- 120 | dnl Internal check to see if pkg-config supports short errors. 121 | AC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED], 122 | [AC_REQUIRE([PKG_PROG_PKG_CONFIG]) 123 | if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then 124 | _pkg_short_errors_supported=yes 125 | else 126 | _pkg_short_errors_supported=no 127 | fi[]dnl 128 | ])dnl _PKG_SHORT_ERRORS_SUPPORTED 129 | 130 | 131 | dnl PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], 132 | dnl [ACTION-IF-NOT-FOUND]) 133 | dnl -------------------------------------------------------------- 134 | dnl Since: 0.4.0 135 | dnl 136 | dnl Note that if there is a possibility the first call to 137 | dnl PKG_CHECK_MODULES might not happen, you should be sure to include an 138 | dnl explicit call to PKG_PROG_PKG_CONFIG in your configure.ac 139 | AC_DEFUN([PKG_CHECK_MODULES], 140 | [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl 141 | AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl 142 | AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl 143 | 144 | pkg_failed=no 145 | AC_MSG_CHECKING([for $1]) 146 | 147 | _PKG_CONFIG([$1][_CFLAGS], [cflags], [$2]) 148 | _PKG_CONFIG([$1][_LIBS], [libs], [$2]) 149 | 150 | m4_define([_PKG_TEXT], [Alternatively, you may set the environment variables $1[]_CFLAGS 151 | and $1[]_LIBS to avoid the need to call pkg-config. 152 | See the pkg-config man page for more details.]) 153 | 154 | if test $pkg_failed = yes; then 155 | AC_MSG_RESULT([no]) 156 | _PKG_SHORT_ERRORS_SUPPORTED 157 | if test $_pkg_short_errors_supported = yes; then 158 | $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` 159 | else 160 | $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` 161 | fi 162 | # Put the nasty error message in config.log where it belongs 163 | echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD 164 | 165 | m4_default([$4], [AC_MSG_ERROR( 166 | [Package requirements ($2) were not met: 167 | 168 | $$1_PKG_ERRORS 169 | 170 | Consider adjusting the PKG_CONFIG_PATH environment variable if you 171 | installed software in a non-standard prefix. 172 | 173 | _PKG_TEXT])[]dnl 174 | ]) 175 | elif test $pkg_failed = untried; then 176 | AC_MSG_RESULT([no]) 177 | m4_default([$4], [AC_MSG_FAILURE( 178 | [The pkg-config script could not be found or is too old. Make sure it 179 | is in your PATH or set the PKG_CONFIG environment variable to the full 180 | path to pkg-config. 181 | 182 | _PKG_TEXT 183 | 184 | To get pkg-config, see .])[]dnl 185 | ]) 186 | else 187 | $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS 188 | $1[]_LIBS=$pkg_cv_[]$1[]_LIBS 189 | AC_MSG_RESULT([yes]) 190 | $3 191 | fi[]dnl 192 | ])dnl PKG_CHECK_MODULES 193 | 194 | 195 | dnl PKG_CHECK_MODULES_STATIC(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], 196 | dnl [ACTION-IF-NOT-FOUND]) 197 | dnl --------------------------------------------------------------------- 198 | dnl Since: 0.29 199 | dnl 200 | dnl Checks for existence of MODULES and gathers its build flags with 201 | dnl static libraries enabled. Sets VARIABLE-PREFIX_CFLAGS from --cflags 202 | dnl and VARIABLE-PREFIX_LIBS from --libs. 203 | dnl 204 | dnl Note that if there is a possibility the first call to 205 | dnl PKG_CHECK_MODULES_STATIC might not happen, you should be sure to 206 | dnl include an explicit call to PKG_PROG_PKG_CONFIG in your 207 | dnl configure.ac. 208 | AC_DEFUN([PKG_CHECK_MODULES_STATIC], 209 | [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl 210 | _save_PKG_CONFIG=$PKG_CONFIG 211 | PKG_CONFIG="$PKG_CONFIG --static" 212 | PKG_CHECK_MODULES($@) 213 | PKG_CONFIG=$_save_PKG_CONFIG[]dnl 214 | ])dnl PKG_CHECK_MODULES_STATIC 215 | 216 | 217 | dnl PKG_INSTALLDIR([DIRECTORY]) 218 | dnl ------------------------- 219 | dnl Since: 0.27 220 | dnl 221 | dnl Substitutes the variable pkgconfigdir as the location where a module 222 | dnl should install pkg-config .pc files. By default the directory is 223 | dnl $libdir/pkgconfig, but the default can be changed by passing 224 | dnl DIRECTORY. The user can override through the --with-pkgconfigdir 225 | dnl parameter. 226 | AC_DEFUN([PKG_INSTALLDIR], 227 | [m4_pushdef([pkg_default], [m4_default([$1], ['${libdir}/pkgconfig'])]) 228 | m4_pushdef([pkg_description], 229 | [pkg-config installation directory @<:@]pkg_default[@:>@]) 230 | AC_ARG_WITH([pkgconfigdir], 231 | [AS_HELP_STRING([--with-pkgconfigdir], pkg_description)],, 232 | [with_pkgconfigdir=]pkg_default) 233 | AC_SUBST([pkgconfigdir], [$with_pkgconfigdir]) 234 | m4_popdef([pkg_default]) 235 | m4_popdef([pkg_description]) 236 | ])dnl PKG_INSTALLDIR 237 | 238 | 239 | dnl PKG_NOARCH_INSTALLDIR([DIRECTORY]) 240 | dnl -------------------------------- 241 | dnl Since: 0.27 242 | dnl 243 | dnl Substitutes the variable noarch_pkgconfigdir as the location where a 244 | dnl module should install arch-independent pkg-config .pc files. By 245 | dnl default the directory is $datadir/pkgconfig, but the default can be 246 | dnl changed by passing DIRECTORY. The user can override through the 247 | dnl --with-noarch-pkgconfigdir parameter. 248 | AC_DEFUN([PKG_NOARCH_INSTALLDIR], 249 | [m4_pushdef([pkg_default], [m4_default([$1], ['${datadir}/pkgconfig'])]) 250 | m4_pushdef([pkg_description], 251 | [pkg-config arch-independent installation directory @<:@]pkg_default[@:>@]) 252 | AC_ARG_WITH([noarch-pkgconfigdir], 253 | [AS_HELP_STRING([--with-noarch-pkgconfigdir], pkg_description)],, 254 | [with_noarch_pkgconfigdir=]pkg_default) 255 | AC_SUBST([noarch_pkgconfigdir], [$with_noarch_pkgconfigdir]) 256 | m4_popdef([pkg_default]) 257 | m4_popdef([pkg_description]) 258 | ])dnl PKG_NOARCH_INSTALLDIR 259 | 260 | 261 | dnl PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE, 262 | dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) 263 | dnl ------------------------------------------- 264 | dnl Since: 0.28 265 | dnl 266 | dnl Retrieves the value of the pkg-config variable for the given module. 267 | AC_DEFUN([PKG_CHECK_VAR], 268 | [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl 269 | AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl 270 | 271 | _PKG_CONFIG([$1], [variable="][$3]["], [$2]) 272 | AS_VAR_COPY([$1], [pkg_cv_][$1]) 273 | 274 | AS_VAR_IF([$1], [""], [$5], [$4])dnl 275 | ])dnl PKG_CHECK_VAR 276 | 277 | dnl PKG_WITH_MODULES(VARIABLE-PREFIX, MODULES, 278 | dnl [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND], 279 | dnl [DESCRIPTION], [DEFAULT]) 280 | dnl ------------------------------------------ 281 | dnl 282 | dnl Prepare a "--with-" configure option using the lowercase 283 | dnl [VARIABLE-PREFIX] name, merging the behaviour of AC_ARG_WITH and 284 | dnl PKG_CHECK_MODULES in a single macro. 285 | AC_DEFUN([PKG_WITH_MODULES], 286 | [ 287 | m4_pushdef([with_arg], m4_tolower([$1])) 288 | 289 | m4_pushdef([description], 290 | [m4_default([$5], [build with ]with_arg[ support])]) 291 | 292 | m4_pushdef([def_arg], [m4_default([$6], [auto])]) 293 | m4_pushdef([def_action_if_found], [AS_TR_SH([with_]with_arg)=yes]) 294 | m4_pushdef([def_action_if_not_found], [AS_TR_SH([with_]with_arg)=no]) 295 | 296 | m4_case(def_arg, 297 | [yes],[m4_pushdef([with_without], [--without-]with_arg)], 298 | [m4_pushdef([with_without],[--with-]with_arg)]) 299 | 300 | AC_ARG_WITH(with_arg, 301 | AS_HELP_STRING(with_without, description[ @<:@default=]def_arg[@:>@]),, 302 | [AS_TR_SH([with_]with_arg)=def_arg]) 303 | 304 | AS_CASE([$AS_TR_SH([with_]with_arg)], 305 | [yes],[PKG_CHECK_MODULES([$1],[$2],$3,$4)], 306 | [auto],[PKG_CHECK_MODULES([$1],[$2], 307 | [m4_n([def_action_if_found]) $3], 308 | [m4_n([def_action_if_not_found]) $4])]) 309 | 310 | m4_popdef([with_arg]) 311 | m4_popdef([description]) 312 | m4_popdef([def_arg]) 313 | 314 | ])dnl PKG_WITH_MODULES 315 | 316 | dnl PKG_HAVE_WITH_MODULES(VARIABLE-PREFIX, MODULES, 317 | dnl [DESCRIPTION], [DEFAULT]) 318 | dnl ----------------------------------------------- 319 | dnl 320 | dnl Convenience macro to trigger AM_CONDITIONAL after PKG_WITH_MODULES 321 | dnl check._[VARIABLE-PREFIX] is exported as make variable. 322 | AC_DEFUN([PKG_HAVE_WITH_MODULES], 323 | [ 324 | PKG_WITH_MODULES([$1],[$2],,,[$3],[$4]) 325 | 326 | AM_CONDITIONAL([HAVE_][$1], 327 | [test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"]) 328 | ])dnl PKG_HAVE_WITH_MODULES 329 | 330 | dnl PKG_HAVE_DEFINE_WITH_MODULES(VARIABLE-PREFIX, MODULES, 331 | dnl [DESCRIPTION], [DEFAULT]) 332 | dnl ------------------------------------------------------ 333 | dnl 334 | dnl Convenience macro to run AM_CONDITIONAL and AC_DEFINE after 335 | dnl PKG_WITH_MODULES check. HAVE_[VARIABLE-PREFIX] is exported as make 336 | dnl and preprocessor variable. 337 | AC_DEFUN([PKG_HAVE_DEFINE_WITH_MODULES], 338 | [ 339 | PKG_HAVE_WITH_MODULES([$1],[$2],[$3],[$4]) 340 | 341 | AS_IF([test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"], 342 | [AC_DEFINE([HAVE_][$1], 1, [Enable ]m4_tolower([$1])[ support])]) 343 | ])dnl PKG_HAVE_DEFINE_WITH_MODULES 344 | -------------------------------------------------------------------------------- /run-development.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run this script in order to compile the plugin and run it in Rofi. This 3 | # allows you to test this plugin while developing without having to `make 4 | # install` it on every change, which would also clobber any OS-managed 5 | # files if you have it installed that way. 6 | set -eu 7 | 8 | [ ! -f configure ] && autoreconf -i 9 | [ ! -d build ] && mkdir build 10 | 11 | cd build 12 | [ ! -e .xdg ] && mkdir .xdg && ln -s ../.. .xdg/rofi-emoji 13 | [ ! -f config.h ] && ../configure 14 | 15 | make 16 | 17 | if [ -z "${XDG_DATA_DIRS:-}" ]; then 18 | xdg_data_dirs="$(pwd)/.xdg" 19 | else 20 | xdg_data_dirs="$(pwd)/.xdg:${XDG_DATA_DIRS}" 21 | fi 22 | 23 | XDG_DATA_DIRS="$xdg_data_dirs" \ 24 | rofi \ 25 | -plugin-path "$(pwd)/.libs" \ 26 | -modi emoji \ 27 | -show emoji \ 28 | "$@" 29 | -------------------------------------------------------------------------------- /screenshots/1_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mange/rofi-emoji/794fecfbb14a53016208793da527f69fbdff6ad5/screenshots/1_main.png -------------------------------------------------------------------------------- /screenshots/2_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mange/rofi-emoji/794fecfbb14a53016208793da527f69fbdff6ad5/screenshots/2_menu.png -------------------------------------------------------------------------------- /screenshots/custom_format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mange/rofi-emoji/794fecfbb14a53016208793da527f69fbdff6ad5/screenshots/custom_format.png -------------------------------------------------------------------------------- /screenshots/group_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mange/rofi-emoji/794fecfbb14a53016208793da527f69fbdff6ad5/screenshots/group_search.png -------------------------------------------------------------------------------- /screenshots/subgroup_search_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mange/rofi-emoji/794fecfbb14a53016208793da527f69fbdff6ad5/screenshots/subgroup_search_1.png -------------------------------------------------------------------------------- /screenshots/subgroup_search_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mange/rofi-emoji/794fecfbb14a53016208793da527f69fbdff6ad5/screenshots/subgroup_search_2.png -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: pkgs.mkShell { 2 | name = "rofi-emoji"; 3 | 4 | buildInputs = with pkgs; [ 5 | # Build tools 6 | gnumake 7 | autoconf 8 | automake 9 | libtool 10 | pkg-config 11 | clang 12 | 13 | # Dependencies 14 | rofi 15 | glib 16 | cairo 17 | 18 | # Clipboard adapters 19 | wl-clipboard 20 | wtype 21 | xclip 22 | xdotool 23 | xsel 24 | ]; 25 | } 26 | -------------------------------------------------------------------------------- /src/actions.c: -------------------------------------------------------------------------------- 1 | #include "actions.h" 2 | #include "menu.h" 3 | #include "utils.h" 4 | 5 | #include 6 | 7 | Emoji *get_selected_emoji(EmojiModePrivateData *pd, unsigned int line) { 8 | if (pd->selected_emoji != NULL) { 9 | return pd->selected_emoji; 10 | } 11 | 12 | if (line >= pd->emojis->len) { 13 | return NULL; 14 | } 15 | 16 | return g_ptr_array_index(pd->emojis, line); 17 | } 18 | 19 | ModeMode text_adapter_action(const char *action, EmojiModePrivateData *pd, 20 | const char *text) { 21 | if (run_clipboard_adapter(action, text, &(pd->message))) { 22 | return MODE_EXIT; 23 | } else { 24 | // Copying failed, reload dialog to show error message in pd->message. 25 | return RELOAD_DIALOG; 26 | } 27 | } 28 | 29 | ModeMode copy_emoji(EmojiModePrivateData *pd, unsigned int line) { 30 | const Emoji *emoji = get_selected_emoji(pd, line); 31 | if (emoji == NULL) { 32 | return MODE_EXIT; 33 | } 34 | 35 | return text_adapter_action("copy", pd, emoji->bytes); 36 | } 37 | 38 | ModeMode insert_emoji(EmojiModePrivateData *pd, unsigned int line, bool copy) { 39 | const Emoji *emoji = get_selected_emoji(pd, line); 40 | if (emoji == NULL) { 41 | return MODE_EXIT; 42 | } 43 | 44 | // Must hide window and give back focus to whatever app should receive the 45 | // insert action. 46 | rofi_view_hide(); 47 | const char *action = copy ? "insert" : "insert_no_copy"; 48 | text_adapter_action(action, pd, emoji->bytes); 49 | 50 | // View is hidden and we cannot get it back again. We must exit at this point. 51 | return MODE_EXIT; 52 | } 53 | 54 | ModeMode output_emoji(EmojiModePrivateData *pd, unsigned int line) { 55 | const Emoji *emoji = get_selected_emoji(pd, line); 56 | if (emoji == NULL) { 57 | return MODE_EXIT; 58 | } 59 | 60 | // Reuse Rofi's dmenu format settings and semantics. 61 | char *format = "s"; 62 | find_arg_str("-format", &format); 63 | rofi_output_formatted_line(format, emoji->bytes, line, ""); 64 | 65 | return MODE_EXIT; 66 | } 67 | 68 | ModeMode copy_codepoint(EmojiModePrivateData *pd, unsigned int line) { 69 | const Emoji *emoji = get_selected_emoji(pd, line); 70 | if (emoji == NULL) { 71 | return MODE_EXIT; 72 | } 73 | 74 | return text_adapter_action("copy", pd, codepoint(emoji->bytes)); 75 | } 76 | 77 | ModeMode copy_name(EmojiModePrivateData *pd, unsigned int line) { 78 | const Emoji *emoji = get_selected_emoji(pd, line); 79 | if (emoji == NULL) { 80 | return MODE_EXIT; 81 | } 82 | 83 | return text_adapter_action("copy", pd, emoji->name); 84 | } 85 | 86 | ModeMode open_menu(EmojiModePrivateData *pd, unsigned int line) { 87 | if (line >= pd->emojis->len) { 88 | return MODE_EXIT; 89 | } 90 | 91 | Emoji *emoji = g_ptr_array_index(pd->emojis, line); 92 | if (emoji == NULL) { 93 | return MODE_EXIT; 94 | } 95 | 96 | pd->selected_emoji = emoji; 97 | emoji_menu_init(pd); 98 | 99 | return RESET_DIALOG; 100 | } 101 | 102 | ModeMode exit_menu(EmojiModePrivateData *pd, unsigned int line) { 103 | emoji_menu_destroy(pd); 104 | pd->selected_emoji = NULL; 105 | return RESET_DIALOG; 106 | } 107 | 108 | ModeMode exit_search(EmojiModePrivateData *pd, unsigned int line) { 109 | return MODE_EXIT; 110 | } 111 | 112 | ModeMode perform_action(EmojiModePrivateData *pd, const Action action, 113 | unsigned int line) { 114 | switch (action) { 115 | case NOOP: 116 | return RELOAD_DIALOG; 117 | case INSERT_EMOJI: 118 | return insert_emoji(pd, line, true); 119 | case INSERT_NO_COPY_EMOJI: 120 | return insert_emoji(pd, line, false); 121 | case COPY_EMOJI: 122 | return copy_emoji(pd, line); 123 | case OUTPUT_EMOJI: 124 | return output_emoji(pd, line); 125 | case COPY_NAME: 126 | return copy_name(pd, line); 127 | case COPY_CODEPOINT: 128 | return copy_codepoint(pd, line); 129 | case OPEN_MENU: 130 | return open_menu(pd, line); 131 | case EXIT_MENU: 132 | return exit_menu(pd, line); 133 | case EXIT_SEARCH: 134 | return exit_search(pd, line); 135 | default: 136 | g_assert_not_reached(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/actions.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTIONS_H 2 | #define ACTIONS_H 3 | 4 | #include 5 | 6 | typedef enum { 7 | NOOP, 8 | INSERT_EMOJI, 9 | INSERT_NO_COPY_EMOJI, 10 | COPY_EMOJI, 11 | OUTPUT_EMOJI, 12 | COPY_NAME, 13 | COPY_CODEPOINT, 14 | OPEN_MENU, 15 | EXIT_MENU, 16 | EXIT_SEARCH, 17 | } Action; 18 | 19 | #include "plugin.h" 20 | 21 | ModeMode perform_action(EmojiModePrivateData *pd, const Action action, 22 | unsigned int line); 23 | 24 | #endif // ACTIONS_H 25 | -------------------------------------------------------------------------------- /src/emoji.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "emoji.h" 4 | 5 | Emoji *emoji_new(char *bytes, char *name, char *group, char *subgroup, 6 | char **keywords) { 7 | Emoji *emoji = g_new(Emoji, 1); 8 | emoji->bytes = bytes; 9 | emoji->name = name; 10 | emoji->group = group; 11 | emoji->subgroup = subgroup; 12 | emoji->keywords = keywords; 13 | return emoji; 14 | } 15 | 16 | void emoji_free(Emoji *emoji) { 17 | g_free(emoji->bytes); 18 | g_free(emoji->name); 19 | g_free(emoji->group); 20 | g_free(emoji->subgroup); 21 | g_strfreev(emoji->keywords); 22 | g_free(emoji); 23 | } 24 | -------------------------------------------------------------------------------- /src/emoji.h: -------------------------------------------------------------------------------- 1 | #ifndef EMOJI_H 2 | #define EMOJI_H 3 | 4 | typedef struct Emoji { 5 | char *bytes; 6 | char *name; 7 | char *group; 8 | char *subgroup; 9 | 10 | char **keywords; 11 | } Emoji; 12 | 13 | Emoji *emoji_new(char *bytes, char *name, char *group, char *subgroup, 14 | char **keywords); 15 | void emoji_free(Emoji *emoji); 16 | 17 | #endif // EMOJI_H 18 | -------------------------------------------------------------------------------- /src/formatter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "emoji.h" 5 | #include "utils.h" 6 | 7 | char *new_format_entry(const char *text) { 8 | if (text == NULL) { 9 | return NULL; 10 | } 11 | 12 | if (strlen(text) == 0) { 13 | return NULL; 14 | } 15 | 16 | char *escaped = g_markup_escape_text(text, -1); 17 | 18 | return escaped; 19 | } 20 | 21 | char *format_emoji(const Emoji *emoji, const char *format) { 22 | char *bytes = new_format_entry(emoji->bytes); 23 | char *name = new_format_entry(emoji->name); 24 | char *group = new_format_entry(emoji->group); 25 | char *subgroup = new_format_entry(emoji->subgroup); 26 | 27 | char *keywords_str = g_strjoinv(", ", emoji->keywords); 28 | char *keywords_entry = new_format_entry(keywords_str); 29 | g_free(keywords_str); 30 | 31 | char *cp = codepoint(emoji->bytes); 32 | 33 | // clang-format off 34 | char *formatted = helper_string_replace_if_exists( 35 | (char *) format, // LOL C. "trust me bro" 36 | "{emoji}", bytes, 37 | "{name}", name, 38 | "{group}", group, 39 | "{subgroup}", subgroup, 40 | "{keywords}", keywords_entry, 41 | "{codepoint}", cp, 42 | NULL 43 | ); 44 | // clang-format on 45 | 46 | g_free(bytes); 47 | g_free(name); 48 | g_free(group); 49 | g_free(subgroup); 50 | g_free(keywords_entry); 51 | g_free(cp); 52 | 53 | return formatted; 54 | } 55 | -------------------------------------------------------------------------------- /src/formatter.h: -------------------------------------------------------------------------------- 1 | #ifndef FORMATTER_H 2 | #define FORMATTER_H 3 | 4 | #include "emoji.h" 5 | 6 | char *format_emoji(const Emoji *emoji, const char *format); 7 | 8 | #endif // FORMATTER_H 9 | -------------------------------------------------------------------------------- /src/loader.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "loader.h" 7 | #include "utils.h" 8 | 9 | #define MAX_LINE_LENGTH 1024 10 | 11 | // Copies the text from the `input` string up until (but not including) the 12 | // next `until` character into a newly allocated buffer at `result`. You need 13 | // to free `result` when you are done with it. 14 | // 15 | // Returns the position in the input string after the `until` character. 16 | // 17 | // If `until` could not be found in `string`, it will not advance and `result` 18 | // will be set to `NULL`. 19 | const char *scan_until(const char until, const char *input, char **result) { 20 | char *index = strchr(input, until); 21 | 22 | if (index == NULL) { 23 | *result = NULL; 24 | return input; 25 | } 26 | 27 | int length = (index - input); 28 | *result = g_strndup(input, length); 29 | 30 | // Advance input to character after the `until` character. 31 | return index + 1; 32 | } 33 | 34 | void array_emoji_free_item(gpointer item) { emoji_free(item); } 35 | 36 | GPtrArray *read_emojis_from_file(const char *path) { 37 | FILE *file = fopen(path, "r"); 38 | if (!file) { 39 | return NULL; 40 | } 41 | 42 | char line[MAX_LINE_LENGTH]; 43 | 44 | GPtrArray *list = g_ptr_array_sized_new(512); 45 | g_ptr_array_set_free_func(list, array_emoji_free_item); 46 | 47 | while (fgets(line, MAX_LINE_LENGTH, file) != NULL) { 48 | Emoji *emoji = parse_emoji_from_line(line); 49 | if (emoji == NULL) { 50 | break; 51 | } 52 | g_ptr_array_add(list, emoji); 53 | } 54 | 55 | fclose(file); 56 | 57 | return list; 58 | } 59 | 60 | void cleanup(char *str) { 61 | g_strstrip(str); 62 | capitalize(str); 63 | } 64 | 65 | int scan_line(const char *line, char **bytes, char **name, char **group, 66 | char **subgroup, char **keywords) { 67 | *bytes = NULL; 68 | *group = NULL; 69 | *subgroup = NULL; 70 | *name = NULL; 71 | *keywords = NULL; 72 | 73 | const char *cursor = line; 74 | 75 | // Each line in the file has this format: 76 | // [bytes]\t[group]\t[subgroup]\t[keywords_str] 77 | 78 | cursor = scan_until('\t', cursor, bytes); 79 | if (*bytes == NULL) { 80 | return 0; 81 | } 82 | cursor = scan_until('\t', cursor, group); 83 | if (*group == NULL) { 84 | g_free(*bytes); 85 | return 0; 86 | } 87 | cursor = scan_until('\t', cursor, subgroup); 88 | if (*subgroup == NULL) { 89 | g_free(*bytes); 90 | g_free(*group); 91 | return 0; 92 | } 93 | cursor = scan_until('\t', cursor, name); 94 | if (*name == NULL) { 95 | g_free(*bytes); 96 | g_free(*group); 97 | g_free(*subgroup); 98 | return 0; 99 | } 100 | cursor = scan_until('\n', cursor, keywords); 101 | if (*keywords == NULL) { 102 | g_free(*bytes); 103 | g_free(*group); 104 | g_free(*subgroup); 105 | g_free(*name); 106 | return 0; 107 | } 108 | 109 | return 1; 110 | } 111 | 112 | char **build_keyword_list(const char *keywords_str, const char *name) { 113 | // Build keyword list. Skip entries that are identical to the name as they 114 | // will just be redundant. 115 | char *name_casefold = g_utf8_casefold(name, -1); 116 | GPtrArray *kw_array = g_ptr_array_new(); 117 | char **keywords; 118 | 119 | keywords = g_strsplit(keywords_str, "|", -1); 120 | 121 | for (int i = 0; keywords[i] != NULL; i++) { 122 | char *keyword = keywords[i]; 123 | cleanup(keyword); 124 | char *keyword_casefold = g_utf8_casefold(keyword, -1); 125 | 126 | if (strcmp(name_casefold, keyword_casefold) != 0) { 127 | g_ptr_array_add(kw_array, g_strdup(keyword)); 128 | } 129 | 130 | g_free(keyword_casefold); 131 | } 132 | 133 | // Original keywords can now be freed and replaced with the built list (which 134 | // is cleaned up and has no keywords equal to the name). 135 | g_strfreev(keywords); 136 | 137 | keywords = g_new(char *, kw_array->len + 1); 138 | for (int i = 0; i < kw_array->len; i++) { 139 | keywords[i] = g_strdup(g_ptr_array_index(kw_array, i)); 140 | } 141 | keywords[kw_array->len] = NULL; 142 | 143 | g_ptr_array_free(kw_array, TRUE); 144 | g_free(name_casefold); 145 | 146 | return keywords; 147 | } 148 | 149 | Emoji *parse_emoji_from_line(const char *line) { 150 | const char *cursor = line; 151 | 152 | char *bytes = NULL; 153 | char *group = NULL; 154 | char *subgroup = NULL; 155 | char *name = NULL; 156 | char *keywords_str = NULL; 157 | 158 | if (!scan_line(cursor, &bytes, &name, &group, &subgroup, &keywords_str)) { 159 | return NULL; 160 | } 161 | 162 | g_strstrip(bytes); 163 | cleanup(name); 164 | cleanup(group); 165 | cleanup(subgroup); 166 | 167 | char **keywords = build_keyword_list(keywords_str, name); 168 | 169 | Emoji *emoji = emoji_new(bytes, name, group, subgroup, keywords); 170 | return emoji; 171 | } 172 | -------------------------------------------------------------------------------- /src/loader.h: -------------------------------------------------------------------------------- 1 | #ifndef LOADER_H 2 | #define LOADER_H 3 | 4 | #include 5 | 6 | #include "emoji.h" 7 | 8 | GPtrArray *read_emojis_from_file(const char *path); 9 | Emoji *parse_emoji_from_line(const char *line); 10 | 11 | const char *scan_until(const char until, const char *input, char **result); 12 | 13 | #endif // LOADER_H 14 | -------------------------------------------------------------------------------- /src/menu.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "formatter.h" 4 | #include "menu.h" 5 | 6 | const int NUM_MENU_ITEMS = 5; 7 | typedef enum { 8 | EMOJI_MENU_PRIMARY = 0, 9 | EMOJI_MENU_SECONDARY = 1, 10 | EMOJI_MENU_INSERT_NO_COPY = 2, 11 | EMOJI_MENU_NAME = 3, 12 | EMOJI_MENU_CODEPOINT = 4, 13 | EMOJI_MENU_BACK = 5, 14 | } MenuItem; 15 | 16 | char *emoji_menu_get_display_value(const EmojiModePrivateData *pd, 17 | unsigned int line) { 18 | switch (line) { 19 | case EMOJI_MENU_BACK: 20 | return g_strdup("⬅ Back to search"); 21 | case EMOJI_MENU_PRIMARY: 22 | return format_emoji(pd->selected_emoji, 23 | pd->search_default_action == INSERT_EMOJI ? 24 | "Copy emoji ({emoji})" : "Insert emoji ({emoji})"); 25 | case EMOJI_MENU_SECONDARY: 26 | return format_emoji(pd->selected_emoji, 27 | pd->search_default_action == INSERT_EMOJI ? 28 | "Insert emoji ({emoji})" : "Copy emoji ({emoji})"); 29 | case EMOJI_MENU_INSERT_NO_COPY: 30 | return format_emoji(pd->selected_emoji, "Insert (without copying) emoji ({emoji})"); 31 | case EMOJI_MENU_NAME: 32 | return format_emoji(pd->selected_emoji, "Copy name ({name})"); 33 | case EMOJI_MENU_CODEPOINT: 34 | return format_emoji(pd->selected_emoji, 35 | "Copy codepoint ({codepoint})"); 36 | default: 37 | return g_strdup(""); 38 | } 39 | } 40 | 41 | void emoji_menu_init(EmojiModePrivateData *pd) { 42 | if (pd->menu_matcher_strings != NULL) { 43 | emoji_menu_destroy(pd); 44 | } 45 | 46 | if (pd->selected_emoji != NULL) { 47 | char **items = g_new(char *, NUM_MENU_ITEMS + 1); 48 | for (int i = 0; i < NUM_MENU_ITEMS; ++i) { 49 | items[i] = emoji_menu_get_display_value(pd, i); 50 | } 51 | items[NUM_MENU_ITEMS] = NULL; 52 | 53 | pd->menu_matcher_strings = items; 54 | } 55 | } 56 | 57 | void emoji_menu_destroy(EmojiModePrivateData *pd) { 58 | if (pd->menu_matcher_strings != NULL) { 59 | g_strfreev(pd->menu_matcher_strings); 60 | pd->menu_matcher_strings = NULL; 61 | } 62 | } 63 | 64 | unsigned int emoji_menu_get_num_entries(const EmojiModePrivateData *pd) { 65 | return NUM_MENU_ITEMS; 66 | } 67 | 68 | char *emoji_menu_get_message(const EmojiModePrivateData *pd) { 69 | Emoji *emoji = pd->selected_emoji; 70 | if (emoji == NULL) { 71 | return NULL; 72 | } 73 | 74 | return format_emoji(emoji, "{emoji} {name}\n" 75 | "{group} [» {subgroup}]\n" 76 | "[Keywords: {keywords}]"); 78 | } 79 | 80 | char *emoji_menu_preprocess_input(EmojiModePrivateData *pd, const char *input) { 81 | return g_strdup(input); 82 | } 83 | 84 | int emoji_menu_token_match(const EmojiModePrivateData *pd, 85 | rofi_int_matcher **tokens, unsigned int line) { 86 | return line < NUM_MENU_ITEMS && 87 | helper_token_match(tokens, pd->menu_matcher_strings[line]); 88 | } 89 | 90 | Action emoji_menu_select_item(EmojiModePrivateData *pd, unsigned int line) { 91 | if (line >= NUM_MENU_ITEMS) { 92 | return NOOP; 93 | } 94 | 95 | switch (line) { 96 | case EMOJI_MENU_BACK: 97 | return EXIT_MENU; 98 | case EMOJI_MENU_PRIMARY: 99 | return pd->search_default_action == INSERT_EMOJI ? COPY_EMOJI : INSERT_EMOJI; 100 | case EMOJI_MENU_SECONDARY: 101 | return pd->search_default_action == INSERT_EMOJI ? INSERT_EMOJI : COPY_EMOJI; 102 | case EMOJI_MENU_INSERT_NO_COPY: 103 | return INSERT_NO_COPY_EMOJI; 104 | case EMOJI_MENU_NAME: 105 | return COPY_NAME; 106 | case EMOJI_MENU_CODEPOINT: 107 | return COPY_CODEPOINT; 108 | default: 109 | g_assert_not_reached(); 110 | } 111 | } 112 | 113 | Action emoji_menu_on_event(EmojiModePrivateData *pd, const Event event, 114 | unsigned int line) { 115 | switch (event) { 116 | case SELECT_DEFAULT: 117 | return emoji_menu_select_item(pd, line); 118 | case EXIT: 119 | return EXIT_MENU; 120 | default: 121 | return NOOP; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/menu.h: -------------------------------------------------------------------------------- 1 | #ifndef MENU_H 2 | #define MENU_H 3 | 4 | #include "actions.h" 5 | #include "plugin.h" 6 | 7 | void emoji_menu_init(EmojiModePrivateData *pd); 8 | void emoji_menu_destroy(EmojiModePrivateData *pd); 9 | 10 | unsigned int emoji_menu_get_num_entries(const EmojiModePrivateData *pd); 11 | char *emoji_menu_get_message(const EmojiModePrivateData *pd); 12 | char *emoji_menu_get_display_value(const EmojiModePrivateData *pd, 13 | unsigned int line); 14 | 15 | int emoji_menu_token_match(const EmojiModePrivateData *pd, 16 | rofi_int_matcher **tokens, unsigned int line); 17 | 18 | char *emoji_menu_preprocess_input(EmojiModePrivateData *pd, const char *input); 19 | 20 | Action emoji_menu_on_event(EmojiModePrivateData *pd, const Event event, 21 | unsigned int line); 22 | 23 | #endif // MENU_H 24 | -------------------------------------------------------------------------------- /src/plugin.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Must be included before other rofi includes. 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "actions.h" 10 | #include "emoji.h" 11 | #include "formatter.h" 12 | #include "loader.h" 13 | #include "menu.h" 14 | #include "plugin.h" 15 | #include "search.h" 16 | #include "utils.h" 17 | 18 | G_MODULE_EXPORT Mode mode; 19 | 20 | /* 21 | * Try to find the location of the emoji file by looking at command line 22 | * arguments and then falling back to the default filename in the XDG data 23 | * directories. 24 | */ 25 | FindDataFileResult find_emoji_file(char **path) { 26 | if (find_arg("-emoji-file") >= 0) { 27 | if (find_arg_str("-emoji-file", path)) { 28 | if (g_file_test(*path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { 29 | return SUCCESS; 30 | } else { 31 | return NOT_A_FILE; 32 | } 33 | } else { 34 | (*path) = NULL; 35 | return CANNOT_DETERMINE_PATH; 36 | } 37 | } else { 38 | return find_data_file("all_emojis.txt", path); 39 | } 40 | } 41 | 42 | static void get_emoji(EmojiModePrivateData *pd) { 43 | char *path; 44 | 45 | FindDataFileResult result = find_emoji_file(&path); 46 | if (result == SUCCESS) { 47 | pd->emojis = read_emojis_from_file(path); 48 | } else { 49 | if (result == CANNOT_DETERMINE_PATH) { 50 | pd->message = g_strdup( 51 | "Failed to load emoji file: The path could not be determined"); 52 | } else if (result == NOT_A_FILE) { 53 | pd->message = g_markup_printf_escaped( 54 | "Failed to load emoji file: %s is not a file", path); 55 | } 56 | pd->emojis = NULL; 57 | } 58 | } 59 | 60 | /** 61 | * Initialize mode 62 | * 63 | * @param mode The mode to initialize 64 | * 65 | * @returns FALSE if there was a failure, TRUE if successful 66 | */ 67 | static int emoji_mode_init(Mode *sw) { 68 | if (mode_get_private_data(sw) == NULL) { 69 | EmojiModePrivateData *pd = g_malloc0(sizeof(*pd)); 70 | 71 | pd->emojis = NULL; 72 | pd->selected_emoji = NULL; 73 | pd->message = NULL; 74 | 75 | // Search 76 | pd->search_default_action = INSERT_EMOJI; 77 | pd->search_matcher_strings = NULL; 78 | pd->format = NULL; 79 | pd->group_matchers = NULL; 80 | pd->subgroup_matchers = NULL; 81 | 82 | // Menu 83 | pd->menu_matcher_strings = NULL; 84 | 85 | if (find_arg("-emoji-format")) { 86 | char *format; 87 | if (find_arg_str("-emoji-format", &format)) { 88 | // We want ownership of this data and not rely on a reference to global 89 | // data. 90 | pd->format = g_strdup(format); 91 | } 92 | } 93 | 94 | if (find_arg("-emoji-mode")) { 95 | char *format; 96 | if (find_arg_str("-emoji-mode", &format)) { 97 | if (strcmp(format, "insert") == 0) { 98 | pd->search_default_action = INSERT_EMOJI; 99 | } else if (strcmp(format, "copy") == 0) { 100 | pd->search_default_action = COPY_EMOJI; 101 | } else if (strcmp(format, "insert_no_copy") == 0) { 102 | pd->search_default_action = INSERT_NO_COPY_EMOJI; 103 | } else if (strcmp(format, "menu") == 0) { 104 | pd->search_default_action = OPEN_MENU; 105 | } else if (strcmp(format, "stdout") == 0) { 106 | pd->search_default_action = OUTPUT_EMOJI; 107 | } else { 108 | g_critical("Invalid emoji-mode: %s. Falling back to insert.", format); 109 | pd->search_default_action = INSERT_EMOJI; 110 | } 111 | } 112 | } 113 | 114 | get_emoji(pd); 115 | if (pd->emojis == NULL) { 116 | return FALSE; 117 | } 118 | 119 | emoji_search_init(pd); 120 | emoji_menu_init(pd); 121 | mode_set_private_data(sw, (void *)pd); 122 | } 123 | return TRUE; 124 | } 125 | 126 | /** 127 | * Get the number of entries in the mode. 128 | * 129 | * @param sw The mode to query 130 | * 131 | * @returns an unsigned in with the number of entries. 132 | */ 133 | static unsigned int emoji_mode_get_num_entries(const Mode *sw) { 134 | const EmojiModePrivateData *pd = 135 | (const EmojiModePrivateData *)mode_get_private_data(sw); 136 | if (pd->selected_emoji == NULL) { 137 | return emoji_search_get_num_entries(pd); 138 | } else { 139 | return emoji_menu_get_num_entries(pd); 140 | } 141 | } 142 | 143 | /** 144 | * Acts on the user interaction. 145 | * 146 | * @param sw The mode to query 147 | * @param menu_retv The menu return value. 148 | * @param input Pointer to the user input string. [in][out] 149 | * @param selected_line the line selected by the user. 150 | * 151 | * @returns the next #ModeMode. 152 | */ 153 | static ModeMode emoji_mode_result(Mode *sw, int mretv, char **input, 154 | unsigned int selected_line) { 155 | EmojiModePrivateData *pd = (EmojiModePrivateData *)mode_get_private_data(sw); 156 | Event event = EXIT; 157 | 158 | if (mretv & MENU_NEXT) { 159 | return NEXT_DIALOG; 160 | } else if (mretv & MENU_PREVIOUS) { 161 | return PREVIOUS_DIALOG; 162 | } else if (mretv & MENU_QUICK_SWITCH) { 163 | return (mretv & MENU_LOWER_MASK); 164 | } else if ((mretv & MENU_ENTRY_DELETE) == MENU_ENTRY_DELETE) { 165 | return RESET_DIALOG; 166 | } else if (mretv & MENU_CANCEL) { 167 | event = EXIT; 168 | } else if (mretv & MENU_CUSTOM_COMMAND) { 169 | if ((mretv & MENU_LOWER_MASK) == 0) { 170 | event = SELECT_CUSTOM_1; 171 | } else { 172 | return RELOAD_DIALOG; 173 | } 174 | } else if ((mretv & MENU_OK)) { 175 | if ((mretv & MENU_CUSTOM_ACTION) == MENU_CUSTOM_ACTION) { 176 | event = SELECT_ALTERNATIVE; 177 | } else { 178 | event = SELECT_DEFAULT; 179 | } 180 | } 181 | 182 | Action action = EXIT_SEARCH; 183 | if (pd->selected_emoji == NULL) { 184 | action = emoji_search_on_event(pd, event, selected_line); 185 | } else { 186 | action = emoji_menu_on_event(pd, event, selected_line); 187 | } 188 | 189 | return perform_action(pd, action, selected_line); 190 | } 191 | 192 | /** 193 | * Destroy the mode 194 | * 195 | * @param sw The mode to destroy 196 | * 197 | */ 198 | static void emoji_mode_destroy(Mode *sw) { 199 | EmojiModePrivateData *pd = (EmojiModePrivateData *)mode_get_private_data(sw); 200 | if (pd != NULL) { 201 | emoji_search_destroy(pd); 202 | emoji_menu_destroy(pd); 203 | 204 | pd->selected_emoji = NULL; // Freed via the emojis list 205 | g_ptr_array_free(pd->emojis, TRUE); 206 | 207 | g_free(pd->message); 208 | g_free(pd->format); 209 | g_free(pd); 210 | mode_set_private_data(sw, NULL); 211 | } 212 | } 213 | 214 | /** 215 | * Query the mode for a user display. 216 | * 217 | * @param sw The mode to query 218 | * 219 | * @return a new allocated (valid pango markup) message to display (user should 220 | * free). 221 | */ 222 | static char *emoji_get_message(const Mode *sw) { 223 | EmojiModePrivateData *pd = (EmojiModePrivateData *)mode_get_private_data(sw); 224 | 225 | if (pd->message != NULL) { 226 | return g_strdup(pd->message); 227 | } 228 | 229 | if (pd->selected_emoji == NULL) { 230 | return emoji_search_get_message(pd); 231 | } else { 232 | return emoji_menu_get_message(pd); 233 | } 234 | } 235 | 236 | /** 237 | * Returns the string as it should be displayed for the entry and the state of 238 | * how it should be displayed. 239 | * @param sw The mode to query 240 | * @param selected_line The entry to query 241 | * @param state The state of the entry [out] 242 | * @param attribute_list List of extra (pango) attribute to apply when 243 | * displaying. [out][null] 244 | * @param get_entry If the should be returned. 245 | * 246 | * @returns allocated new string and state when get_entry is TRUE otherwise just 247 | * the state. 248 | */ 249 | static char *emoji_get_display_value(const Mode *sw, unsigned int selected_line, 250 | G_GNUC_UNUSED int *state, 251 | G_GNUC_UNUSED GList **attr_list, 252 | int get_entry) { 253 | EmojiModePrivateData *pd = (EmojiModePrivateData *)mode_get_private_data(sw); 254 | 255 | *state |= STATE_MARKUP; 256 | 257 | // Only return the string if requested, otherwise only set state. 258 | if (!get_entry) { 259 | return NULL; 260 | } 261 | 262 | if (pd->selected_emoji == NULL) { 263 | return emoji_search_get_display_value(pd, selected_line); 264 | } else { 265 | return emoji_menu_get_display_value(pd, selected_line); 266 | } 267 | } 268 | 269 | /** 270 | * @param sw The mode object. 271 | * @param tokens The tokens to match against. 272 | * @param index The index in this plugin to match against. 273 | * 274 | * Match the entry. 275 | * 276 | * @param returns try when a match. 277 | */ 278 | static int emoji_token_match(const Mode *sw, rofi_int_matcher **tokens, 279 | unsigned int index) { 280 | EmojiModePrivateData *pd = (EmojiModePrivateData *)mode_get_private_data(sw); 281 | 282 | if (pd->selected_emoji == NULL) { 283 | return emoji_search_token_match(pd, tokens, index); 284 | } else { 285 | return emoji_menu_token_match(pd, tokens, index); 286 | } 287 | } 288 | 289 | static char *emoji_preprocess_input(Mode *sw, const char *input) { 290 | EmojiModePrivateData *pd = (EmojiModePrivateData *)mode_get_private_data(sw); 291 | if (pd->selected_emoji == NULL) { 292 | return emoji_search_preprocess_input(pd, input); 293 | } else { 294 | return emoji_menu_preprocess_input(pd, input); 295 | } 296 | } 297 | 298 | Mode mode = { 299 | .abi_version = ABI_VERSION, 300 | .name = "emoji", 301 | .cfg_name_key = "emoji", 302 | .type = MODE_TYPE_SWITCHER, 303 | ._init = emoji_mode_init, 304 | ._get_num_entries = emoji_mode_get_num_entries, 305 | ._result = emoji_mode_result, 306 | ._destroy = emoji_mode_destroy, 307 | ._token_match = emoji_token_match, 308 | ._get_display_value = emoji_get_display_value, 309 | ._get_message = emoji_get_message, 310 | ._get_completion = NULL, 311 | ._preprocess_input = emoji_preprocess_input, 312 | ._completer_result = NULL, 313 | .private_data = NULL, 314 | .free = NULL, 315 | }; 316 | -------------------------------------------------------------------------------- /src/plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef PLUGIN_H 2 | #define PLUGIN_H 3 | 4 | #include 5 | 6 | // Must be included before other rofi includes. 7 | #include 8 | 9 | #include "actions.h" 10 | #include "emoji.h" 11 | 12 | typedef enum { 13 | SELECT_DEFAULT, 14 | SELECT_ALTERNATIVE, 15 | SELECT_CUSTOM_1, 16 | EXIT, 17 | } Event; 18 | 19 | typedef struct { 20 | GPtrArray *emojis; 21 | Emoji *selected_emoji; 22 | char *message; 23 | 24 | // For search 25 | Action search_default_action; 26 | char **search_matcher_strings; 27 | char *format; 28 | rofi_int_matcher **group_matchers; 29 | rofi_int_matcher **subgroup_matchers; 30 | 31 | // For menu 32 | char **menu_matcher_strings; 33 | } EmojiModePrivateData; 34 | 35 | #endif // PLUGIN_H 36 | -------------------------------------------------------------------------------- /src/search.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "actions.h" 4 | #include "formatter.h" 5 | #include "search.h" 6 | #include "utils.h" 7 | 8 | const char *DEFAULT_FORMAT = "{emoji} {name}" 9 | "[ ({keywords})]"; 10 | 11 | char **generate_matcher_strings(GPtrArray *list); 12 | 13 | void emoji_search_init(EmojiModePrivateData *pd) { 14 | pd->search_matcher_strings = generate_matcher_strings(pd->emojis); 15 | } 16 | 17 | void emoji_search_destroy(EmojiModePrivateData *pd) { 18 | g_strfreev(pd->search_matcher_strings); 19 | helper_tokenize_free(pd->group_matchers); 20 | helper_tokenize_free(pd->subgroup_matchers); 21 | } 22 | 23 | unsigned int emoji_search_get_num_entries(const EmojiModePrivateData *pd) { 24 | return pd->emojis->len; 25 | } 26 | 27 | char *emoji_search_get_message(const EmojiModePrivateData *pd) { return NULL; } 28 | 29 | char *emoji_search_get_display_value(const EmojiModePrivateData *pd, 30 | unsigned int line) { 31 | if (line >= pd->emojis->len) { 32 | return g_strdup(""); 33 | } 34 | 35 | Emoji *emoji = g_ptr_array_index(pd->emojis, line); 36 | const char *format = pd->format; 37 | if (format == NULL || format[0] == '\0') { 38 | format = DEFAULT_FORMAT; 39 | } 40 | 41 | if (emoji == NULL) { 42 | return g_strdup("n/a"); 43 | } else { 44 | 45 | return format_emoji(emoji, format); 46 | } 47 | } 48 | 49 | char *emoji_search_preprocess_input(EmojiModePrivateData *pd, 50 | const char *input) { 51 | char *query; 52 | char *group_query; 53 | char *subgroup_query; 54 | 55 | if (pd->group_matchers != NULL) { 56 | helper_tokenize_free(pd->group_matchers); 57 | pd->group_matchers = NULL; 58 | } 59 | if (pd->subgroup_matchers != NULL) { 60 | helper_tokenize_free(pd->subgroup_matchers); 61 | pd->subgroup_matchers = NULL; 62 | } 63 | 64 | tokenize_search(input, &query, &group_query, &subgroup_query); 65 | 66 | if (group_query != NULL) { 67 | pd->group_matchers = helper_tokenize(group_query, FALSE); 68 | } 69 | 70 | if (subgroup_query != NULL) { 71 | pd->subgroup_matchers = helper_tokenize(subgroup_query, FALSE); 72 | } 73 | 74 | return query; 75 | } 76 | 77 | int emoji_search_token_match(const EmojiModePrivateData *pd, 78 | rofi_int_matcher **tokens, unsigned int line) { 79 | if (line >= pd->emojis->len) { 80 | return FALSE; 81 | } 82 | 83 | if (pd->group_matchers != NULL || pd->subgroup_matchers != NULL) { 84 | Emoji *emoji = g_ptr_array_index(pd->emojis, line); 85 | 86 | if (pd->group_matchers != NULL) { 87 | if (!helper_token_match(pd->group_matchers, emoji->group)) { 88 | return FALSE; 89 | } 90 | } 91 | 92 | if (pd->subgroup_matchers != NULL) { 93 | if (!helper_token_match(pd->subgroup_matchers, emoji->subgroup)) { 94 | return FALSE; 95 | } 96 | } 97 | } 98 | 99 | return helper_token_match(tokens, pd->search_matcher_strings[line]); 100 | } 101 | 102 | Action emoji_search_on_event(EmojiModePrivateData *pd, const Event event, 103 | unsigned int line) { 104 | switch (event) { 105 | case SELECT_DEFAULT: 106 | if (line >= pd->emojis->len) { 107 | return NOOP; 108 | } 109 | return pd->search_default_action; 110 | case SELECT_ALTERNATIVE: 111 | if (line >= pd->emojis->len) { 112 | return NOOP; 113 | } 114 | return OPEN_MENU; 115 | case SELECT_CUSTOM_1: 116 | return COPY_EMOJI; 117 | case EXIT: 118 | return EXIT_SEARCH; 119 | default: 120 | return NOOP; 121 | } 122 | } 123 | 124 | char **generate_matcher_strings(GPtrArray *list) { 125 | char **strings = g_new(char *, list->len + 1); 126 | for (int i = 0; i < list->len; ++i) { 127 | Emoji *emoji = g_ptr_array_index(list, i); 128 | 129 | strings[i] = format_emoji(emoji, "{emoji} {name} {keywords}"); 130 | } 131 | strings[list->len] = NULL; 132 | return strings; 133 | } 134 | -------------------------------------------------------------------------------- /src/search.h: -------------------------------------------------------------------------------- 1 | #ifndef SEARCH_H 2 | #define SEARCH_H 3 | 4 | #include "actions.h" 5 | #include "plugin.h" 6 | 7 | void emoji_search_init(EmojiModePrivateData *pd); 8 | void emoji_search_destroy(EmojiModePrivateData *pd); 9 | 10 | unsigned int emoji_search_get_num_entries(const EmojiModePrivateData *pd); 11 | char *emoji_search_get_message(const EmojiModePrivateData *pd); 12 | char *emoji_search_get_display_value(const EmojiModePrivateData *pd, 13 | unsigned int line); 14 | 15 | int emoji_search_token_match(const EmojiModePrivateData *pd, 16 | rofi_int_matcher **tokens, unsigned int line); 17 | 18 | char *emoji_search_preprocess_input(EmojiModePrivateData *pd, 19 | const char *input); 20 | 21 | Action emoji_search_on_event(EmojiModePrivateData *pd, const Event event, 22 | unsigned int line); 23 | 24 | #endif // SEARCH_H 25 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "loader.h" 9 | #include "utils.h" 10 | 11 | FindDataFileResult find_data_file(const char *basename, char **path) { 12 | const char *const *data_dirs = g_get_system_data_dirs(); 13 | if (data_dirs == NULL) { 14 | return CANNOT_DETERMINE_PATH; 15 | } 16 | 17 | // Store first path in case file cannot be found; this path will then be the 18 | // path reported to the user in the error message. 19 | char *first_path = NULL; 20 | 21 | int index = 0; 22 | char const *data_dir = data_dirs[index]; 23 | while (1) { 24 | char *current_path = 25 | g_build_filename(data_dir, "rofi-emoji", basename, NULL); 26 | if (current_path == NULL) { 27 | return CANNOT_DETERMINE_PATH; 28 | } 29 | 30 | if (g_file_test(current_path, 31 | (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))) { 32 | *path = current_path; 33 | g_free(first_path); 34 | return SUCCESS; 35 | } 36 | 37 | if (first_path == NULL) { 38 | first_path = current_path; 39 | } else { 40 | g_free(current_path); 41 | } 42 | 43 | index += 1; 44 | data_dir = data_dirs[index]; 45 | if (data_dir == NULL) { 46 | break; 47 | } 48 | } 49 | 50 | *path = first_path; 51 | return NOT_A_FILE; 52 | } 53 | 54 | int find_clipboard_adapter(char **adapter, char **error) { 55 | FindDataFileResult result = find_data_file("clipboard-adapter.sh", adapter); 56 | 57 | if (result == SUCCESS) { 58 | return TRUE; 59 | } else if (result == CANNOT_DETERMINE_PATH) { 60 | *error = g_strdup("Failed to load clipboard-adapter file: The path could " 61 | "not be determined"); 62 | } else if (result == NOT_A_FILE) { 63 | *error = 64 | g_markup_printf_escaped("Failed to load clipboard-adapter file: " 65 | "%s is not a file\nAlso " 66 | "searched in every path in $XDG_DATA_DIRS.", 67 | *adapter); 68 | } else { 69 | *error = g_strdup("Unexpected error"); 70 | } 71 | 72 | return FALSE; 73 | } 74 | 75 | int run_clipboard_adapter(const char *action, const char *text, char **error) { 76 | char *adapter; 77 | int ca_result = find_clipboard_adapter(&adapter, error); 78 | if (ca_result != TRUE) { 79 | return FALSE; 80 | } 81 | 82 | GPid child_pid; 83 | gint child_stdin; 84 | int exit_status = -1; 85 | g_autoptr(GError) child_error = NULL; 86 | 87 | g_spawn_async_with_pipes( 88 | /* working_directory */ NULL, 89 | /* argv */ (char *[]){adapter, (char *)action, NULL}, 90 | /* envp */ NULL, 91 | 92 | // G_SPAWN_DO_NOT_REAP_CHILD allows us to call waitpid and get the staus 93 | // code. 94 | /* flags */ (G_SPAWN_DEFAULT | G_SPAWN_DO_NOT_REAP_CHILD), 95 | 96 | /* child_setup */ NULL, 97 | /* user_data */ NULL, 98 | /* child_pid */ &child_pid, 99 | /* standard_input */ &child_stdin, 100 | /* standard_output */ NULL, 101 | /* standard_error */ NULL, 102 | /* error */ &child_error); 103 | 104 | if (child_error == NULL) { 105 | FILE *stdin; 106 | if (!(stdin = fdopen(child_stdin, "ab"))) { 107 | *error = g_strdup_printf("Failed to open child's stdin"); 108 | return FALSE; 109 | } 110 | fprintf(stdin, "%s", text); 111 | fclose(stdin); 112 | 113 | pid_t res = waitpid(child_pid, &exit_status, WUNTRACED); 114 | if (res < 0) { 115 | *error = g_strdup_printf( 116 | "Could not wait for child process (PID %i) to close", child_pid); 117 | g_spawn_close_pid(child_pid); 118 | return FALSE; 119 | } 120 | g_spawn_close_pid(child_pid); 121 | } 122 | 123 | if (child_error != NULL) { 124 | *error = g_strdup_printf("Failed to run clipboard-adapter: %s", 125 | child_error->message); 126 | return FALSE; 127 | } 128 | 129 | if (exit_status == 0) { 130 | *error = NULL; 131 | return TRUE; 132 | } else { 133 | *error = g_strdup_printf("clipboard-adapter exited with %d", exit_status); 134 | return FALSE; 135 | } 136 | } 137 | 138 | /* 139 | * Strips each string inside of a null-terminated list of char*. 140 | * 141 | * The list is modified in-place. 142 | */ 143 | void strip_strv(char **in) { 144 | if (in == NULL) { 145 | return; 146 | } 147 | 148 | int i = 0; 149 | char *str = in[i]; 150 | 151 | while (str != NULL) { 152 | g_strstrip(str); 153 | str = in[++i]; 154 | } 155 | } 156 | 157 | /* 158 | * Makes the first ASCII character in the string uppercase by modifying it 159 | * in-place. 160 | * 161 | * Does nothing on NULL values or empty strings. 162 | */ 163 | void capitalize(char *text) { 164 | if (text == NULL || *text == '\0') { 165 | return; 166 | } 167 | 168 | text[0] = g_ascii_toupper(text[0]); 169 | } 170 | 171 | void append(char **dest, const char *addition) { 172 | char *tmp; 173 | if (*dest == NULL) { 174 | tmp = g_strdup(addition); 175 | } else { 176 | tmp = g_strconcat(*dest, addition, NULL); 177 | } 178 | g_free(*dest); 179 | *dest = tmp; 180 | } 181 | 182 | void appendn(char **dest, const char *addition, int n) { 183 | char *tmp; 184 | if (*dest == NULL) { 185 | tmp = g_strndup(addition, n); 186 | } else { 187 | char *copy = g_strndup(addition, n); 188 | tmp = g_strconcat(*dest, copy, NULL); 189 | g_free(copy); 190 | } 191 | g_free(*dest); 192 | *dest = tmp; 193 | } 194 | 195 | void replace(char **dest, const char *replacement) { 196 | g_free(*dest); 197 | if (replacement != NULL) { 198 | *dest = g_strdup(replacement); 199 | } else { 200 | *dest = NULL; 201 | } 202 | } 203 | 204 | void replacen(char **dest, const char *replacement, int n) { 205 | g_free(*dest); 206 | if (replacement != NULL) { 207 | *dest = g_strndup(replacement, n); 208 | } else { 209 | *dest = NULL; 210 | } 211 | } 212 | 213 | void tokenize_search(const char *input, char **query, char **group_query, 214 | char **subgroup_query) { 215 | *query = NULL; 216 | *group_query = NULL; 217 | *subgroup_query = NULL; 218 | 219 | const char *current = input; 220 | 221 | while (*current != '\0') { 222 | char *index = strchr(current, ' '); 223 | 224 | if (index == NULL) { 225 | // No more spaces, so rest of input is a single word. 226 | switch (current[0]) { 227 | case '@': 228 | if (strlen(current) > 1) { 229 | replace(group_query, current + 1); 230 | } else { 231 | replace(group_query, NULL); 232 | } 233 | break; 234 | case '#': 235 | if (strlen(current) > 1) { 236 | replace(subgroup_query, current + 1); 237 | } else { 238 | replace(subgroup_query, NULL); 239 | } 240 | break; 241 | default: 242 | append(query, current); 243 | } 244 | break; 245 | } 246 | 247 | int length = (index - current); 248 | 249 | switch (current[0]) { 250 | case '@': 251 | if (length > 1) { 252 | replacen(group_query, current + 1, length - 1); 253 | } else { 254 | replace(group_query, NULL); 255 | } 256 | break; 257 | case '#': 258 | if (length > 1) { 259 | replacen(subgroup_query, current + 1, length - 1); 260 | } else { 261 | replace(subgroup_query, NULL); 262 | } 263 | break; 264 | default: 265 | // Add one extra length for the space 266 | appendn(query, current, length + 1); 267 | } 268 | 269 | // Skip ahead to after the space 270 | current = index + 1; 271 | } 272 | 273 | // Query must always be something 274 | if (*query == NULL) { 275 | *query = g_strdup(""); 276 | } 277 | 278 | g_strstrip(*query); 279 | } 280 | 281 | char *codepoint(char *bytes) { 282 | int added = 0; 283 | GString *str = g_string_new(""); 284 | 285 | while (bytes[0] != '\0') { 286 | if (added > 0) { 287 | g_string_append(str, " "); 288 | } 289 | 290 | gunichar c = g_utf8_get_char_validated(bytes, -1); 291 | if (c == -1) { // Not valid 292 | g_string_append(str, "U+INVALID"); 293 | } else if (c == -2) { // Incomplete 294 | g_string_append(str, "U+INCOMPLETE"); 295 | } else { 296 | char *formatted = g_strdup_printf("U+%04X", c); 297 | g_string_append(str, formatted); 298 | g_free(formatted); 299 | } 300 | added++; 301 | bytes = g_utf8_find_next_char(bytes, NULL); 302 | } 303 | 304 | return g_string_free(str, FALSE); 305 | } 306 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | // Rofi is not yet exporting these constants in their headers 5 | // https://github.com/DaveDavenport/rofi/blob/79adae77d72be3de96d1c4e6d53b6bae4cb7e00e/include/widgets/textbox.h#L104 6 | #define STATE_MARKUP 8 7 | 8 | // Not exported by Rofi 9 | void rofi_view_hide(); 10 | 11 | #include "emoji.h" 12 | 13 | typedef enum { 14 | SUCCESS = 1, 15 | NOT_A_FILE = 0, 16 | CANNOT_DETERMINE_PATH = -1 17 | } FindDataFileResult; 18 | 19 | FindDataFileResult find_data_file(const char *basename, char **path); 20 | int find_clipboard_adapter(char **adapter, char **error); 21 | int run_clipboard_adapter(const char *action, const char *text, char **error); 22 | void capitalize(char *text); 23 | 24 | void tokenize_search(const char *input, char **query, char **group_query, 25 | char **subgroup_query); 26 | 27 | char *codepoint(char *bytes); 28 | 29 | #endif // UTILS_H 30 | -------------------------------------------------------------------------------- /tests/check_emoji.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "../src/emoji.h" 6 | 7 | START_TEST(test_new_and_free) { 8 | char *keywords[4] = {"kw1", "kw2 - electric buggaloo", NULL}; 9 | 10 | // Accepts owned data 11 | // clang-format off 12 | Emoji *emoji = emoji_new( 13 | g_strdup("😀"), 14 | g_strdup("smiling"), 15 | g_strdup("people"), 16 | g_strdup("faces"), 17 | g_strdupv(keywords) 18 | ); 19 | // clang-format on 20 | 21 | ck_assert_str_eq(emoji->bytes, "😀"); 22 | ck_assert_str_eq(emoji->name, "smiling"); 23 | ck_assert_str_eq(emoji->group, "people"); 24 | ck_assert_str_eq(emoji->subgroup, "faces"); 25 | ck_assert_str_eq(emoji->keywords[0], "kw1"); 26 | ck_assert_str_eq(emoji->keywords[1], "kw2 - electric buggaloo"); 27 | ck_assert_ptr_eq(emoji->keywords[2], NULL); 28 | 29 | emoji_free(emoji); 30 | } 31 | END_TEST 32 | 33 | Suite *emoji_suite(void) { 34 | Suite *s; 35 | TCase *tc_model; 36 | 37 | s = suite_create("Emoji"); 38 | tc_model = tcase_create("Model"); 39 | 40 | tcase_add_test(tc_model, test_new_and_free); 41 | suite_add_tcase(s, tc_model); 42 | 43 | return s; 44 | } 45 | 46 | int main(void) { 47 | int number_failed; 48 | Suite *s; 49 | SRunner *sr; 50 | 51 | s = emoji_suite(); 52 | sr = srunner_create(s); 53 | 54 | srunner_run_all(sr, CK_VERBOSE); 55 | number_failed = srunner_ntests_failed(sr); 56 | srunner_free(sr); 57 | 58 | return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; 59 | } 60 | -------------------------------------------------------------------------------- /tests/check_loader.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "../src/loader.h" 6 | 7 | START_TEST(test_scan_until) { 8 | const char *input = "this is an example"; 9 | 10 | const char *cursor = input; 11 | char *result = NULL; 12 | cursor = scan_until(' ', cursor, &result); 13 | 14 | ck_assert_str_eq(result, "this"); 15 | ck_assert_str_eq(cursor, "is an example"); 16 | 17 | // Result is a copy! 18 | result[0] = 'X'; 19 | ck_assert_str_eq(result, "Xhis"); 20 | ck_assert_str_eq(input, "this is an example"); 21 | g_free(result); 22 | 23 | // Keep scanning! 24 | cursor = scan_until('x', cursor, &result); 25 | ck_assert_str_eq(result, "is an e"); 26 | ck_assert_str_eq(cursor, "ample"); 27 | g_free(result); 28 | 29 | // Sets NULL result on no match without advancing cursor. 30 | cursor = scan_until('Z', cursor, &result); 31 | ck_assert_ptr_eq(result, NULL); 32 | ck_assert_str_eq(cursor, "ample"); 33 | } 34 | END_TEST 35 | 36 | START_TEST(test_emoji_parse_line) { 37 | const char *line = "😀 Smileys & Emotion face-smiling " 38 | " grinning face face | grin \n"; 39 | Emoji *emoji = parse_emoji_from_line(line); 40 | 41 | ck_assert_str_eq(emoji->bytes, "😀"); 42 | ck_assert_str_eq(emoji->group, "Smileys & Emotion"); 43 | ck_assert_str_eq(emoji->subgroup, "Face-smiling"); 44 | ck_assert_str_eq(emoji->name, "Grinning face"); 45 | ck_assert_int_eq(g_strv_length(emoji->keywords), 2); 46 | ck_assert_str_eq(emoji->keywords[0], "Face"); 47 | ck_assert_str_eq(emoji->keywords[1], "Grin"); 48 | ck_assert_ptr_eq(emoji->keywords[2], NULL); 49 | 50 | emoji_free(emoji); 51 | } 52 | END_TEST 53 | 54 | START_TEST(test_emoji_parse_skip_redundant_keywords) { 55 | const char *line = 56 | "😀 X X grinning face face|grinning face |grin \n"; 57 | Emoji *emoji = parse_emoji_from_line(line); 58 | 59 | // The "grinning face" keyword is removed since its the same 60 | // as the name. 61 | ck_assert_int_eq(g_strv_length(emoji->keywords), 2); 62 | ck_assert_str_eq(emoji->keywords[0], "Face"); 63 | ck_assert_str_eq(emoji->keywords[1], "Grin"); 64 | ck_assert_ptr_eq(emoji->keywords[2], NULL); 65 | 66 | emoji_free(emoji); 67 | } 68 | END_TEST 69 | 70 | Suite *loader_suite(void) { 71 | Suite *s; 72 | TCase *tc_core; 73 | 74 | s = suite_create("Loader"); 75 | tc_core = tcase_create("Core"); 76 | 77 | tcase_add_test(tc_core, test_scan_until); 78 | tcase_add_test(tc_core, test_emoji_parse_line); 79 | tcase_add_test(tc_core, test_emoji_parse_skip_redundant_keywords); 80 | suite_add_tcase(s, tc_core); 81 | 82 | return s; 83 | } 84 | 85 | int main(void) { 86 | int number_failed; 87 | Suite *s; 88 | SRunner *sr; 89 | 90 | s = loader_suite(); 91 | sr = srunner_create(s); 92 | 93 | srunner_run_all(sr, CK_VERBOSE); 94 | number_failed = srunner_ntests_failed(sr); 95 | srunner_free(sr); 96 | 97 | return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; 98 | } 99 | -------------------------------------------------------------------------------- /tests/check_utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "../src/utils.h" 6 | 7 | START_TEST(test_capitalize) { 8 | char *str = g_strdup("hello world"); 9 | char *upper = g_strdup("HELLO WORLD"); 10 | 11 | capitalize(str); 12 | capitalize(upper); 13 | 14 | ck_assert_str_eq(str, "Hello world"); 15 | ck_assert_str_eq(upper, "HELLO WORLD"); 16 | 17 | g_free(str); 18 | g_free(upper); 19 | } 20 | END_TEST 21 | 22 | START_TEST(test_tokenize_search_simple_query) { 23 | char *query = NULL; 24 | char *group_query = NULL; 25 | char *subgroup_query = NULL; 26 | 27 | char *input = "hello world "; 28 | tokenize_search(input, &query, &group_query, &subgroup_query); 29 | 30 | ck_assert_str_eq(query, "hello world"); 31 | ck_assert_ptr_eq(group_query, NULL); 32 | ck_assert_ptr_eq(subgroup_query, NULL); 33 | 34 | // query is a copy of the input 35 | query[0] = 'b'; 36 | ck_assert_str_eq(query, "bello world"); 37 | ck_assert_str_eq(input, "hello world "); 38 | 39 | g_free(query); 40 | g_free(group_query); 41 | g_free(subgroup_query); 42 | } 43 | END_TEST 44 | 45 | START_TEST(test_tokenize_search_empty_query) { 46 | char *query = NULL; 47 | char *group_query = NULL; 48 | char *subgroup_query = NULL; 49 | 50 | char *input = ""; 51 | tokenize_search(input, &query, &group_query, &subgroup_query); 52 | 53 | ck_assert_str_eq(query, ""); 54 | ck_assert_ptr_eq(group_query, NULL); 55 | ck_assert_ptr_eq(subgroup_query, NULL); 56 | 57 | // query is a copy of the input 58 | ck_assert_ptr_ne(query, input); 59 | 60 | g_free(query); 61 | g_free(group_query); 62 | g_free(subgroup_query); 63 | } 64 | END_TEST 65 | 66 | START_TEST(test_tokenize_search_group_query) { 67 | char *query = NULL; 68 | char *group_query = NULL; 69 | char *subgroup_query = NULL; 70 | 71 | char *input = "hello @group world"; 72 | tokenize_search(input, &query, &group_query, &subgroup_query); 73 | 74 | ck_assert_str_eq(query, "hello world"); 75 | ck_assert_str_eq(group_query, "group"); 76 | ck_assert_ptr_eq(subgroup_query, NULL); 77 | 78 | // query is a copy of the input 79 | ck_assert_ptr_ne(query, input); 80 | 81 | g_free(query); 82 | g_free(group_query); 83 | g_free(subgroup_query); 84 | } 85 | END_TEST 86 | 87 | START_TEST(test_tokenize_search_subgroup_query) { 88 | char *query = NULL; 89 | char *group_query = NULL; 90 | char *subgroup_query = NULL; 91 | 92 | char *input = "hello #sub world"; 93 | tokenize_search(input, &query, &group_query, &subgroup_query); 94 | 95 | ck_assert_str_eq(query, "hello world"); 96 | ck_assert_ptr_eq(group_query, NULL); 97 | ck_assert_str_eq(subgroup_query, "sub"); 98 | 99 | // query is a copy of the input 100 | ck_assert_ptr_ne(query, input); 101 | 102 | g_free(query); 103 | g_free(group_query); 104 | g_free(subgroup_query); 105 | } 106 | END_TEST 107 | 108 | START_TEST(test_tokenize_search_complex_query) { 109 | char *query = NULL; 110 | char *group_query = NULL; 111 | char *subgroup_query = NULL; 112 | 113 | char *input = "@group unicorn #animal"; 114 | tokenize_search(input, &query, &group_query, &subgroup_query); 115 | 116 | ck_assert_str_eq(query, "unicorn"); 117 | ck_assert_str_eq(group_query, "group"); 118 | ck_assert_str_eq(subgroup_query, "animal"); 119 | 120 | // query is a copy of the input 121 | ck_assert_ptr_ne(query, input); 122 | 123 | g_free(query); 124 | g_free(group_query); 125 | g_free(subgroup_query); 126 | } 127 | END_TEST 128 | 129 | START_TEST(test_tokenize_search_empty_filters) { 130 | char *query = NULL; 131 | char *group_query = NULL; 132 | char *subgroup_query = NULL; 133 | 134 | char *input = "@ #"; 135 | tokenize_search(input, &query, &group_query, &subgroup_query); 136 | 137 | ck_assert_str_eq(query, ""); 138 | ck_assert_ptr_eq(group_query, NULL); 139 | ck_assert_ptr_eq(subgroup_query, NULL); 140 | 141 | // query is a copy of the input 142 | ck_assert_ptr_ne(query, input); 143 | 144 | g_free(query); 145 | g_free(group_query); 146 | g_free(subgroup_query); 147 | } 148 | END_TEST 149 | 150 | START_TEST(test_tokenize_search_only_group) { 151 | char *query = NULL; 152 | char *group_query = NULL; 153 | char *subgroup_query = NULL; 154 | 155 | char *input = "@hello"; 156 | tokenize_search(input, &query, &group_query, &subgroup_query); 157 | 158 | ck_assert_str_eq(query, ""); 159 | ck_assert_str_eq(group_query, "hello"); 160 | ck_assert_ptr_eq(subgroup_query, NULL); 161 | 162 | // query is a copy of the input 163 | ck_assert_ptr_ne(query, input); 164 | 165 | g_free(query); 166 | g_free(group_query); 167 | g_free(subgroup_query); 168 | } 169 | END_TEST 170 | 171 | START_TEST(test_tokenize_search_repeated_filters) { 172 | char *query = NULL; 173 | char *group_query = NULL; 174 | char *subgroup_query = NULL; 175 | 176 | char *input = "1 @a #x 2 #y @b 3"; 177 | tokenize_search(input, &query, &group_query, &subgroup_query); 178 | 179 | ck_assert_str_eq(query, "1 2 3"); 180 | ck_assert_str_eq(group_query, "b"); 181 | ck_assert_str_eq(subgroup_query, "y"); 182 | 183 | // query is a copy of the input 184 | ck_assert_ptr_ne(query, input); 185 | 186 | g_free(query); 187 | g_free(group_query); 188 | g_free(subgroup_query); 189 | } 190 | END_TEST 191 | 192 | START_TEST(test_codepoint) { 193 | ck_assert_str_eq(codepoint("A"), "U+0041"); 194 | ck_assert_str_eq(codepoint("🙃"), "U+1F643"); 195 | ck_assert_str_eq(codepoint("🇸🇪"), "U+1F1F8 U+1F1EA"); 196 | } 197 | END_TEST 198 | 199 | Suite *utils_suite(void) { 200 | Suite *s; 201 | TCase *tc_core; 202 | TCase *tc_tokenize; 203 | TCase *tc_codepoint; 204 | 205 | s = suite_create("Utils"); 206 | 207 | tc_core = tcase_create("Core"); 208 | tcase_add_test(tc_core, test_capitalize); 209 | 210 | tc_tokenize = tcase_create("Tokenize"); 211 | tcase_add_test(tc_tokenize, test_tokenize_search_simple_query); 212 | tcase_add_test(tc_tokenize, test_tokenize_search_empty_query); 213 | tcase_add_test(tc_tokenize, test_tokenize_search_group_query); 214 | tcase_add_test(tc_tokenize, test_tokenize_search_subgroup_query); 215 | tcase_add_test(tc_tokenize, test_tokenize_search_complex_query); 216 | tcase_add_test(tc_tokenize, test_tokenize_search_empty_filters); 217 | tcase_add_test(tc_tokenize, test_tokenize_search_only_group); 218 | tcase_add_test(tc_tokenize, test_tokenize_search_repeated_filters); 219 | 220 | tc_codepoint = tcase_create("Codepoint"); 221 | tcase_add_test(tc_codepoint, test_codepoint); 222 | 223 | suite_add_tcase(s, tc_core); 224 | suite_add_tcase(s, tc_tokenize); 225 | suite_add_tcase(s, tc_codepoint); 226 | 227 | return s; 228 | } 229 | 230 | int main(void) { 231 | int number_failed; 232 | Suite *s; 233 | SRunner *sr; 234 | 235 | s = utils_suite(); 236 | sr = srunner_create(s); 237 | 238 | srunner_run_all(sr, CK_VERBOSE); 239 | number_failed = srunner_ntests_failed(sr); 240 | srunner_free(sr); 241 | 242 | return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; 243 | } 244 | --------------------------------------------------------------------------------