├── .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 | 
14 |
15 | 
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 | 
48 |
49 | - `#mammal` - Limit to emojis that have `mammal` inside of its Subgroup, e.g.
50 | `Animals & Nature » Animal-mammal`.
51 |
52 | 
53 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------