├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── completions └── tofi ├── doc ├── config ├── scd2gfm.sh ├── tofi.1.md ├── tofi.1.scd ├── tofi.5.md └── tofi.5.scd ├── meson.build ├── meson_options.txt ├── protocols ├── fractional-scale-v1.xml └── wlr-layer-shell-unstable-v1.xml ├── screenshot_dark_paper.png ├── screenshot_default.png ├── screenshot_dmenu.png ├── screenshot_dos.png ├── screenshot_fullscreen.png ├── screenshot_soy_milk.png ├── src ├── clipboard.c ├── clipboard.h ├── color.c ├── color.h ├── compgen.c ├── compgen.h ├── config.c ├── config.h ├── desktop_vec.c ├── desktop_vec.h ├── drun.c ├── drun.h ├── entry.c ├── entry.h ├── entry_backend │ ├── harfbuzz.c │ ├── harfbuzz.h │ ├── pango.c │ └── pango.h ├── history.c ├── history.h ├── input.c ├── input.h ├── lock.c ├── lock.h ├── log.c ├── log.h ├── main.c ├── main_compgen.c ├── matching.c ├── matching.h ├── mkdirp.c ├── mkdirp.h ├── nelem.h ├── scale.c ├── scale.h ├── shm.c ├── shm.h ├── string_vec.c ├── string_vec.h ├── surface.c ├── surface.h ├── tofi.h ├── unicode.c ├── unicode.h ├── xmalloc.c └── xmalloc.h ├── startup_performance.svg ├── test ├── config.c ├── meson.build ├── tap.c ├── tap.h └── utf8.c └── themes ├── dark-paper ├── dmenu ├── dos ├── fullscreen └── soy-milk /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Test build process 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | - 'doc' 8 | - 'themes' 9 | 10 | jobs: 11 | ubuntu-20_04: 12 | 13 | runs-on: ubuntu-20.04 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Install dependencies 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install build-essential llvm clang meson scdoc \ 23 | ninja-build libfreetype-dev libharfbuzz-dev libcairo-dev \ 24 | libpango1.0-dev libwayland-dev wayland-protocols libxkbcommon-dev \ 25 | python3-pip 26 | 27 | sudo pip3 install --upgrade meson 28 | 29 | # The version of GCC on Ubuntu 20.04 is too old to recognise 30 | # [[attribute]] syntax, so skip the build there. 31 | # 32 | # Additionally, clang will raise unknown attribute errors, so don't 33 | # enable werror. 34 | 35 | - name: Set clang as default compiler 36 | run: sudo update-alternatives --set cc $(which clang) 37 | 38 | - name: Clang LTO build 39 | run: | 40 | meson build 41 | ninja -C build test 42 | rm -rf build 43 | 44 | - name: Clang non-LTO build 45 | run: | 46 | meson build 47 | meson configure build -Db_lto=false 48 | ninja -C build test 49 | rm -rf build 50 | 51 | ubuntu-22_04: 52 | 53 | runs-on: ubuntu-22.04 54 | 55 | steps: 56 | - name: Checkout repo 57 | uses: actions/checkout@v3 58 | 59 | - name: Install dependencies 60 | run: | 61 | sudo apt-get update 62 | sudo apt-get install build-essential llvm clang meson scdoc \ 63 | ninja-build libfreetype-dev libharfbuzz-dev libcairo-dev \ 64 | libpango1.0-dev libwayland-dev wayland-protocols libxkbcommon-dev \ 65 | python3-pip 66 | 67 | sudo pip3 install --upgrade meson 68 | 69 | - name: GCC LTO build 70 | run: | 71 | meson build 72 | meson configure build -Dwerror=true 73 | ninja -C build test 74 | rm -rf build 75 | 76 | - name: GCC non-LTO build 77 | run: | 78 | meson build 79 | meson configure build -Dwerror=true 80 | meson configure build -Db_lto=false 81 | ninja -C build test 82 | rm -rf build 83 | 84 | - name: Set clang as default compiler 85 | run: sudo update-alternatives --set cc $(which clang) 86 | 87 | - name: Clang LTO build 88 | run: | 89 | meson build 90 | meson configure build -Dwerror=true 91 | ninja -C build test 92 | rm -rf build 93 | 94 | - name: Clang non-LTO build 95 | run: | 96 | meson build 97 | meson configure build -Dwerror=true 98 | meson configure build -Db_lto=false 99 | ninja -C build test 100 | rm -rf build 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # Vim files 55 | *.swp 56 | *.taghl 57 | tags 58 | 59 | # Mac OS files 60 | .DS_Store 61 | 62 | # Project specific files 63 | build/ 64 | .cache 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.9.1] - 2023-04-10 4 | ### Fixed 5 | - Fixed broken line spacing for some fonts with the HarfBuzz backend. 6 | 7 | ## [0.9.0] - 2023-04-09 8 | ### Added 9 | - Added support for a text cursor. This can be enabled with the `text-cursor` 10 | option, the style of cursor chosen with `text-cursor-style`, and themed 11 | similarly to other text. 12 | - Added support for fractional scaling, correcting the behaviour of percentage 13 | sizes when fractional scaling is used. 14 | - Added Ctrl-n, Ctrl-p, Page-Up and Page-Down keybindings. 15 | - Added `auto-accept-single` option, to automatically accept the last remaining 16 | result when there is only one. 17 | 18 | ### Changed 19 | - The `font` option now performs home path substitution for paths starting with 20 | `~/`. 21 | 22 | ### Fixed 23 | - Fixed some more potential errors from malformed config files. 24 | - Fixed some potential memory leaks when generating caches. 25 | - Fixed rounded corners when a background padding of -1 is specified. 26 | - Fixed broken text rendering with some versions of Harfbuzz. 27 | - Fixed some man page typos. 28 | 29 | 30 | ## [0.8.1] - 2022-12-01 31 | ### Fixed 32 | - Stop debug logs printing in release builds. 33 | 34 | 35 | ## [0.8.0] - 2022-12-01 36 | ### Deprecated 37 | Text styling has been overhauled in this update, and as a result the 38 | `selection-padding` option has been replaced with 39 | `selection-background-padding`, to avoid ambiguity and match the other 40 | available options. `selection-padding` is therefore deprecated, and will be 41 | removed in a future version of tofi, so please update your configs. 42 | 43 | ### Added 44 | - Added `placeholder-text` option. 45 | - Overhaul text styling. Each piece of text in tofi is now styleable in a 46 | similar way, with foreground and background colours. The pieces of text 47 | that can be individually styled are: 48 | 49 | - `prompt` 50 | - `placeholder` 51 | - `input` 52 | - `default-result` 53 | - `alternate-result` 54 | - `selection` 55 | 56 | Each of these pieces of text now has the following options available: 57 | 58 | - `-color` 59 | - `-background` 60 | - `-background-padding` 61 | - `-background-corner-radius` 62 | 63 | See `man 5 tofi` or the example config file for more information. 64 | - Added ability to paste from the clipboard with `ctrl-v`. 65 | - Added `history-file` option. This both allows changing the history file 66 | location, and when combined with `history=true` (the default), enables 67 | history sorting in plain `tofi` mode. 68 | - Added `font-features` option, allowing the specification of OpenType font 69 | features in a similar way to CSS `font-feature-settings`. 70 | - Added `font-variations` option, allowing customisation of variable fonts in a 71 | similar way to CSS `font-variation-settings`. 72 | - Added `clip-to-padding` option, to allow drawing text outside the specified 73 | padding. 74 | 75 | ### Changed 76 | - Due to the number of available options, the usage info now only contains a 77 | short list of the most important ones and directs the user to `man 5 tofi`. 78 | - If `-h` was passed, print usage info to `stdout` rather than `stderr`. 79 | - Improved performance of most text handling operations. 80 | - Improved performance of `selection-background` and others, so drawing 81 | backgrounds is no longer an expensive operation. 82 | 83 | ### Fixed 84 | - Invalid values in config options no longer set the option to a default value. 85 | - Fix various potential errors due to malformed config files. 86 | - Fixed a compilation error on FreeBSD (from [@jbeich](https://github.com/jbeich)). 87 | - Fixed a compilation error with musl libc (from [@akdjka](https://github.com/akdjka)). 88 | 89 | 90 | ## [0.7.0] - 2022-11-01 91 | ### Added 92 | - Added `include` option, allowing config files to include other files. 93 | - Added `hide-input` and `hidden-character` options for sensitive input. 94 | - Added `exclusive-zone` option, to control interaction with menu bars etc. 95 | - Added `terminal` option to allow `tofi-drun` to launch terminal apps. 96 | - Added a couple of extra keybindings. 97 | 98 | ### Changed 99 | - Searching is now Unicode aware, so case-insensitive matching of non-Latin 100 | characters should work. 101 | - Fuzzy matching will now use a simpler algorithm when matching lines more than 102 | 100 characters in length to avoid slowdowns. 103 | - By default, tofi will now refuse to start if another instance is already 104 | running, preventing accidental double launches. This can be changed 105 | with the `multi-instance` option. 106 | - Tofi will now show up on top of fullscreen windows. 107 | 108 | ### Fixed 109 | - Keyboard shortcuts are now bound to physical keys rather than characters, so 110 | should not change places when changing keyboard layouts. 111 | - Fix crash when attempting to change the selection while no results are 112 | displayed. 113 | - Fixed a rare issue where input could become out of sync with the display. 114 | 115 | 116 | ## [0.6.0] - 2022-09-08 117 | ### Warning - HiDPI config change 118 | In the [0.5.0] release, the `scale` option was added to enable scaling of pixel 119 | values by the display's scale factor. In this release, the default value of 120 | `scale` has changed to be `true`, and fonts are no longer scaled if `scale` is 121 | set to `false`. This makes tofi's behaviour match that of e.g. Sway, and makes 122 | configs work more reliably on monitors with different scale factors. 123 | 124 | If you use tofi on a HiDPI display, you may need to change your config's pixel 125 | values to make things look right again. 126 | 127 | ### Added 128 | - Added `require-match` option, to allow printing of input even when there are 129 | no matching results. 130 | - Added `prompt-padding` option for more flexible spacing between the prompt 131 | and other text. 132 | - Added a new example theme, dark-paper. 133 | 134 | ### Changed 135 | - The `scale` option now defaults to `true`, as noted above. The example themes 136 | have been updated to account for this change. 137 | - Spaces are now allowed as part of normal input. Similarly to dmenu, tofi will 138 | split the input into words, and only show results for which every word 139 | matches individually. 140 | - Split `tofi(5)` manpage into behaviour and style options to make finding 141 | options easier. 142 | 143 | ### Fixed 144 | - Fixed build failure when link-time optimisation is disabled. 145 | 146 | ## [0.5.0] - 2022-08-21 147 | ### Warning - HiDPI config change 148 | In previous versions of tofi, pixel values were always treated as device 149 | pixels, ignoring the display's scale factor. This allows pixel-perfect sizes, 150 | but means you have to make different configs for differently scaled displays, 151 | and isn't how e.g. Sway does things. Additionally, fonts currently *are* scaled 152 | by the scale factor, making things a little complex. 153 | 154 | This release adds a `scale` boolean option, which currently defaults to 155 | `false`. Setting this to `true` will make pixel values scale with the display's 156 | scale factor. 157 | 158 | In the next version of tofi, `scale` will default to `true` (but still be 159 | around if you want the old behaviour). Setting `scale` to `false` will also 160 | start causing fonts to not be scaled by the scale factor. 161 | 162 | If you use tofi on a HiDPI display, you should explicitly set `scale` to your 163 | desired setting now (or at least be aware that you'll need to change some theme 164 | dimensions in the next version). 165 | 166 | ### Added 167 | - Fuzzy matching can now be enabled with the `fuzzy_match` option. 168 | - Added `scale` option, as described above. 169 | - Added Ctrl-u and Ctrl-w readline keybindings. 170 | - Added this changelog. 171 | 172 | ### Changed 173 | - Improved performance when neither `selection-match-color` or 174 | `selection-background-color` are specified. 175 | - Improved performance on systems with Transparent HugePages enabled for shared 176 | memory (none that I know of for now, but may be relevant in the future). 177 | 178 | 179 | ## [0.4.0] - 2022-08-07 180 | ### Deprecated 181 | In the [0.3.0] release, the `drun-print-exec` option was added to enable fixed 182 | `tofi-drun` behaviour. This release changes this to be the default, as this is 183 | how it should have been done from the start. Consequently, the 184 | `drun-print-exec` option is now obsolete, and may be removed in the future, so 185 | you can safely delete it from your configs. 186 | 187 | ### Added 188 | - A full example config file is included in `doc/config`, and is installed to 189 | `/etc/xdg/tofi/config` 190 | - Added `selection-padding` option, to make the selected item background wider. 191 | - Added `selection-match-color` option, to highlight the matching portion of 192 | the selected result. 193 | - Added key-repeat. 194 | - Added result pagination. 195 | 196 | ### Removed 197 | - `tofi-compgen` is no longer installed, as it's really just a debugging 198 | utility and not needed. 199 | 200 | ### Changed 201 | - `drun-print-exec` is now always set to true, and the option is deprecated. 202 | - The `output` and `late-keyboard-init` options are no longer command-line 203 | only, and so can be specified in the config file. 204 | 205 | ### Fixed 206 | - Fixed slanted fonts being cut off if they extend back towards the prompt. 207 | - The selection background now correctly wraps slanted fonts. 208 | - Enable compilation with some older Wayland versions (specifically that found 209 | on Ubuntu 20.04). 210 | 211 | 212 | ## [0.3.1] - 2022-07-28 213 | ### Fixed 214 | - Fix some program arguments not working in drun mode. 215 | 216 | 217 | ## [0.3.0] - 2022-07-27 218 | ### Deprecated 219 | Previously, tofi-drun would print the filename of the selected .desktop file to 220 | stdout. This could then be passed to `xargs swaymsg exec gio launch` to be 221 | executed. 222 | 223 | The problem is that this ends up defeating the purpose of passing the command 224 | to swaymsg exec, and the workspace the command was selected on may not be the 225 | one that it starts up on, if for example it takes a long time and the user 226 | switches workspaces in the meantime. 227 | 228 | The solution is to instead print the Exec= line from the .desktop file, and 229 | pass that directly to `xargs swaymsg exec --` for execution. 230 | 231 | To avoid too much breaking of configs for the few people who use tofi 232 | currently, this release adds a new option, `--drun-print-exec`, to enable the 233 | fixed behaviour. The next release will change this to be the default, as this 234 | is how it should have been done from the start. 235 | 236 | ### Added 237 | - Tofi will now automatically detect how many results can be displayed if 238 | `--num-results=0` is set (the new default). 239 | - When running in drun mode, keywords will also be matched along with the name 240 | (so e.g. searching for "web" will return "firefox") 241 | - Add `--drun-print-exec` option, as noted above. 242 | 243 | ### Fixed 244 | - Fixed percentage values passed to margin options not behaving correctly when 245 | output scaling is enabled. 246 | - Fix tofi not grabbing keyboard focus on River. 247 | - Correct `--font` option name in man page. 248 | - Fix percentage values on vertical monitors. 249 | - Fix drun mode weirdness when history is enabled 250 | 251 | 252 | ## [0.2.0] - 2022-07-25 253 | ### Added 254 | - `tofi-drun` mode for launching apps from `.desktop` files. 255 | - Navigation keybindings for `Ctrl-j`, `Ctrl-k` and `Tab`. 256 | 257 | ### Changed 258 | - Search results now prioritise matches early in a word, e.g. a search for 259 | `fire` yields `firefox` before `aafire`. 260 | 261 | ### Fixed 262 | - Fixed input / display sometimes going out of sync. 263 | - Add dependency on `librt` for systems that require that (from [@sktt](https://github.com/sktt)). 264 | - Allow for a count of more than 128 program launches in the history file. 265 | 266 | 267 | ## [0.1.1] - 2022-06-28 268 | ### Fixed 269 | - Fix typo in `meson.build`. 270 | 271 | ## [0.1.0] - 2022-06-27 272 | Initial release. Good enough to use, but still some jank. 273 | 274 | [0.9.1]: https://github.com/philj56/tofi/compare/v0.9.0...v0.9.1 275 | [0.9.0]: https://github.com/philj56/tofi/compare/v0.8.1...v0.9.0 276 | [0.8.1]: https://github.com/philj56/tofi/compare/v0.8.0...v0.8.1 277 | [0.8.0]: https://github.com/philj56/tofi/compare/v0.7.0...v0.8.0 278 | [0.7.0]: https://github.com/philj56/tofi/compare/v0.6.0...v0.7.0 279 | [0.6.0]: https://github.com/philj56/tofi/compare/v0.5.0...v0.6.0 280 | [0.5.0]: https://github.com/philj56/tofi/compare/v0.4.0...v0.5.0 281 | [0.4.0]: https://github.com/philj56/tofi/compare/v0.3.1...v0.4.0 282 | [0.3.1]: https://github.com/philj56/tofi/compare/v0.3.0...v0.3.1 283 | [0.3.0]: https://github.com/philj56/tofi/compare/v0.2.0...v0.3.0 284 | [0.2.0]: https://github.com/philj56/tofi/compare/v0.2.0...v0.1.1 285 | [0.1.1]: https://github.com/philj56/tofi/compare/v0.1.1...v0.1.0 286 | [0.1.0]: https://github.com/philj56/tofi/releases/tag/v0.1.0 287 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-2022 Philip Jones 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tofi 2 | 3 | An extremely fast and simple [dmenu](https://tools.suckless.org/dmenu/) / 4 | [rofi](https://github.com/davatorium/rofi) replacement for 5 | [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots)-based 6 | [Wayland](https://wayland.freedesktop.org/) compositors such as 7 | [Sway](https://github.com/swaywm/sway/). 8 | 9 | The aim is to do just what I want it to as quick as possible. 10 | 11 | When [configured correctly](#performance), tofi can get on screen within a 12 | single frame. 13 | 14 | ![](screenshot_fullscreen.png) 15 | 16 | ## Table of Contents 17 | * [Install](#install) 18 | * [Building](#building) 19 | * [Arch](#arch) 20 | * [Usage](#usage) 21 | * [Theming](#theming) 22 | * [Performance](#performance) 23 | * [Options](#options) 24 | * [Benchmarks](#benchmarks) 25 | * [Where is the time spent?](#where-is-the-time-spent) 26 | 27 | ## Install 28 | ### Building 29 | 30 | Install the necessary dependencies. 31 | 32 | #### For Arch: 33 | ```sh 34 | # Runtime dependencies 35 | sudo pacman -S freetype2 harfbuzz cairo pango wayland libxkbcommon 36 | 37 | # Build-time dependencies 38 | sudo pacman -S meson scdoc wayland-protocols 39 | ``` 40 | 41 | #### For Fedora 42 | ```sh 43 | # Runtime dependencies 44 | sudo dnf install freetype-devel cairo-devel pango-devel wayland-devel libxkbcommon-devel harfbuzz 45 | 46 | # Build-time dependencies 47 | sudo dnf install meson scdoc wayland-protocols-devel 48 | ``` 49 | 50 | #### For Debian/Ubuntu 51 | 52 | ```sh 53 | # Runtime dependencies 54 | sudo apt install libfreetype-dev libcairo2-dev libpango1.0-dev libwayland-dev libxkbcommon-dev libharfbuzz-dev 55 | 56 | # Build-time dependencies 57 | sudo apt install meson scdoc wayland-protocols 58 | ``` 59 | 60 | Then build: 61 | ```sh 62 | meson build && ninja -C build install 63 | ``` 64 | 65 | ### Arch 66 | Tofi is available in the [AUR](https://aur.archlinux.org/packages/tofi): 67 | ```sh 68 | paru -S tofi 69 | ``` 70 | 71 | ## Usage 72 | 73 | By default, running `tofi` causes it to act like dmenu, accepting options on 74 | `stdin` and printing the selection to `stdout`. 75 | 76 | `tofi-run` is a symlink to `tofi`, which will cause tofi to display a list of 77 | executables under the user's `$PATH`. 78 | 79 | `tofi-drun` is also a symlink to `tofi`, which will cause tofi to display a 80 | list of applications found in desktop files as described by the [Desktop Entry 81 | Specification](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html). 82 | 83 | To use as a launcher for Sway, add something similar to the following to your 84 | Sway config file: 85 | ``` 86 | set $menu tofi-run | xargs swaymsg exec -- 87 | bindsym $mod+d exec $menu 88 | ``` 89 | 90 | For `tofi-drun`, there are two possible methods: 91 | ``` 92 | # Launch via Sway 93 | set $drun tofi-drun | xargs swaymsg exec -- 94 | bindsym $mod+Shift+d exec $drun 95 | 96 | # Launch directly 97 | set $drun tofi-drun --drun-launch=true 98 | bindsym $mod+Shift+d exec $drun 99 | ``` 100 | 101 | See the main [manpage](doc/tofi.1.md) for more info. 102 | 103 | ### Theming 104 | 105 | Tofi supports a fair number of theming options - see the default [config 106 | file](doc/config) or the config file [manpage](doc/tofi.5.md) for a complete 107 | description. Theming is based on the box model shown below: 108 | 109 | ![Default theme screenshot](screenshot_default.png) 110 | 111 | This consists of a box with a border, border outlines and optionally rounded 112 | corners. Text inside the box can either be laid out vertically: 113 | ``` 114 | ╔═══════════════════╗ 115 | ║ prompt input ║ 116 | ║ result 1 ║ 117 | ║ result 2 ║ 118 | ║ ... ║ 119 | ╚═══════════════════╝ 120 | ``` 121 | or horizontally: 122 | ``` 123 | ╔═══════════════════════════════════════════╗ 124 | ║ prompt input result 1 result 2 ... ║ 125 | ╚═══════════════════════════════════════════╝ 126 | ``` 127 | Each piece of text can have its colour customised, and be surrounded by a box 128 | with optionally rounded corners, 129 | 130 | A few example themes are included and shown below. Note that you may need to 131 | tweak them to look correct on your display. 132 | 133 | [`themes/fullscreen`](themes/fullscreen) 134 | ![Fullscreen theme screenshot](screenshot_fullscreen.png) 135 | 136 | [`themes/dmenu`](themes/dmenu) 137 | ![dmenu theme screenshot](screenshot_dmenu.png) 138 | 139 | [`themes/dos`](themes/dos) 140 | ![DOS theme screenshot](screenshot_dos.png) 141 | 142 | [`themes/dark-paper`](themes/dark-paper) 143 | ![Dark paper theme screenshot](screenshot_dark_paper.png) 144 | 145 | [`themes/soy-milk`](themes/soy-milk) 146 | ![Soy milk theme screenshot](screenshot_soy_milk.png) 147 | 148 | ## Performance 149 | 150 | By default, tofi isn't really any faster than its alternatives. However, when 151 | configured correctly, it can startup and get on screen within a single frame, 152 | or about 2ms in the ideal case. 153 | 154 | ### Options 155 | In roughly descending order, the most important options for performance are: 156 | 157 | * `--font` - This is *by far* the most important option. By default, tofi uses 158 | [Pango](https://pango.gnome.org/) for font rendering, which (on Linux) looks 159 | up fonts via 160 | [Fontconfig](https://www.freedesktop.org/wiki/Software/fontconfig/). 161 | Unfortunately, this font lookup is about as slow as wading through treacle 162 | (relatively speaking). On battery power on my laptop (Arch linux, AMD Ryzen 5 163 | 5600U), with ~10000 fonts as the output of `fc-list`, loading a single font 164 | with Pango & Fontconfig takes ~120ms. 165 | 166 | The solution is to pass a path to a font file to `--font`, e.g. `--font 167 | /usr/share/fonts/noto/NotoSansMono-Regular.ttf`. Tofi will then skip any font 168 | searching, and use [Harfbuzz](https://harfbuzz.github.io/) and 169 | [Cairo](https://www.cairographics.org/) directly to load the font and display 170 | text. This massively speeds up startup (font loading takes <1ms). The (minor 171 | for me) downside is that any character not in the specified font won't render 172 | correctly, but unless you have commands (or items) with CJK characters or 173 | emojis in their names, that shouldn't be an issue. 174 | 175 | * `--width`, `--height` - Larger windows take longer to draw (mostly just for 176 | the first frame). Again, on battery power on my laptop, drawing a fullscreen 177 | window (2880px × 1800px) takes ~20ms on the first frame, whereas a dmenu-like 178 | ribbon (2880px × 60px) takes ~1ms. 179 | 180 | * `--num-results` - By default, tofi auto-detects how many results will fit in 181 | the window. This is quite tricky when `--horizontal=true` is passed, and 182 | leads to a few ms slowdown (only in this case). Setting a fixed number of 183 | results will speed this up, but since this likely only applies to dmenu-like 184 | themes (which are already very quick) it's probably not worth setting this. 185 | 186 | * `--*-background` - Drawing background boxes around text effectively requires 187 | drawing the text twice, so specifying a lot of these options can lead to a 188 | couple of ms slowdown. 189 | 190 | * `--hint-font` - Getting really into it now, one of the remaining slow points 191 | is hinting fonts. For the dmenu theme on battery power on my laptop, with a 192 | specific font file chosen, the initial text render with the default font 193 | hinting takes ~4-6ms. Specifying `--hint-font false` drops this to ~1ms. For 194 | hidpi screens or large font sizes, this doesn't noticeably impact font 195 | sharpness, but your mileage may vary. This option has no effect if a path to 196 | a font file hasn't been passed to `--font`. 197 | 198 | * `--ascii-input` - Proper Unicode handling is slower than plain ASCII - on the 199 | order of a few ms for ~40 kB of input. Specifying `--ascii-input true` will 200 | disable some of this handling, speeding up tofi's startup, but searching for 201 | non-ASCII characters may not work properly. 202 | 203 | * `--late-keyboard-init` - The last avoidable thing that slows down startup is 204 | initialisation of the keyboard. This only takes 1-2ms on my laptop, but up 205 | to 60ms on a Raspberry Pi Zero 2 W. Passing this option will delay keyboard 206 | initialisation until after the first draw to screen, meaning that *keypresses 207 | will be missed* until then, so it's disabled by default. 208 | 209 | ### Benchmarks 210 | 211 | Below are some rough benchmarks of the included themes on different machines. 212 | These were generated with version 0.1.0 of tofi. The time shown is measured 213 | from program launch to Sway reporting that the window has entered the screen. 214 | Results are the mean and standard deviation of 10 runs. All tests were 215 | performed with `--font /path/to/font/file.ttf`, `--hint-font false` and the 216 | equivalent of `--ascii-input true` (as tofi 0.1.0 didn't support Unicode text). 217 | 218 | 219 | 220 | 221 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 |
222 | Theme
fullscreen dmenu dos
Machine Ryzen 7 3700X
2560px × 1440px
9.5ms ± 1.8ms 5.2ms ± 1.5ms 6.1ms ± 1.3ms
Ryzen 5 5600U (AC)
2880px × 1800px
17.1ms ± 1.4ms 4.0ms ± 0.5ms 6.7ms ± 1.1ms
Ryzen 5 5600U (battery)
2880px × 1800px
28.1ms ± 3.7ms 6.0ms ± 1.6ms 12.3ms ± 3.4ms
Raspberry Pi Zero 2 W
1920px × 1080px
119.0ms ± 5.9ms 67.3ms ± 10.2ms 110.0ms ± 10.3ms
257 | 258 | The table below additionally includes `--late-keyboard-init` in the arguments. 259 | 260 | 261 | 262 | 263 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 |
264 | Theme
fullscreen dmenu dos
Machine Ryzen 7 3700X
2560px × 1440px
7.9ms ± 1.0ms 2.3ms ± 0.8ms 3.8ms ± 0.8ms
Ryzen 5 5600U (AC)
2880px × 1800px
13.4ms ± 0.8ms 2.6ms ± 0.5ms 5.5ms ± 0.51ms
Ryzen 5 5600U (battery)
2880px × 1800px
21.8ms ± 1.8ms 3.6ms ± 0.7ms 8.1ms ± 0.7ms
Raspberry Pi Zero 2 W
1920px × 1080px
98.3ms ± 5.7ms 44.8ms ± 16.3ms 87.4ms ± 9.9ms
299 | 300 | #### Bonus Round: Transparent HugePages 301 | 302 | It turns out that it's possible to speed up fullscreen windows somewhat with 303 | some advanced memory tweaks. See [this Stack Overflow 304 | question](https://stackoverflow.com/questions/73278608/can-mmaps-performance-be-improved-for-shared-memory) 305 | if you want full details, but basically by setting 306 | `/sys/kernel/mm/transparent_hugepage/shmem_enabled` to `advise`, we can tell 307 | the kernel we're going to be working with large memory areas. This results in 308 | fewer page faults when first allocating memory, speeding up tofi. 309 | 310 | Note that I don't recommend you play with this unless you know what you're 311 | doing (I don't), but I've included it just in case, and to show that the 312 | slowdown on large screens is partially due to factors beyond tofi's control. 313 | 314 | The table below shows the effects of additionally enabling hugepages from the 315 | table above. The dmenu theme has been skipped, as the window it creates is too 316 | small to benefit from them. The Raspberry Pi is also omitted, as it doesn't 317 | support hugepages. 318 | 319 | 320 | 321 | 322 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 |
323 | Theme
fullscreen dos
Machine Ryzen 7 3700X
2560px × 1440px
6.9ms ± 1.1ms 3.2ms ± 0.4ms
Ryzen 5 5600U (AC)
2880px × 1800px
7.9ms ± 1.2ms 3.4ms ± 1.0ms
Ryzen 5 5600U (battery)
2880px × 1800px
13.7ms ± 0.9ms 5.6ms ± 0.8ms
348 | 349 | ### Where is the time spent? 350 | 351 | For those who are interested in how much time there is even left to save, I've 352 | plotted the startup performance of version 0.8.0 of `tofi-run` below, alongside 353 | the corresponding debug output. This is the data from 1000 runs of the dmenu 354 | theme on a Ryzen 7 3700X machine, with all performance options set as mentioned 355 | above, along with `--num-results 10`. I've highlighted some points of interest, 356 | most of which are out of tofi's control. 357 | 358 | [![Startup performance plot](startup_performance.svg)](https://raw.githubusercontent.com/philj56/tofi/master/startup_performance.svg) 359 | 360 | (You may want to click the image to see it at full size). 361 | 362 | Note that this is slightly faster than shown in previous benchmarks (with some 363 | runs under 1.5ms!), due to some string handling improvements made in version 364 | 0.8.0. Also note that the real performance is slightly better still, as the 365 | performance logging used slows down the code by roughly 10%. 366 | 367 | As you can see, there's not a huge amount of time that could even theoretically 368 | be saved. Somewhere around 50% of the startup time is simply spent waiting, and 369 | most of the code isn't parallelisable, as many steps depend on the result of 370 | previous steps. One idea would be to daemonize tofi, skipping much of this 371 | startup. I don't want to do this, however, for two main reasons: complexity, 372 | and I think it's probably about fast enough already! 373 | -------------------------------------------------------------------------------- /completions/tofi: -------------------------------------------------------------------------------- 1 | # vi: ft=bash 2 | 3 | _tofi() 4 | { 5 | local cur prev words cword 6 | _init_completion || return 7 | 8 | words=( 9 | --help 10 | --config 11 | --include 12 | --output 13 | --scale 14 | --anchor 15 | --background-color 16 | --corner-radius 17 | --font 18 | --font-size 19 | --font-features 20 | --font-variations 21 | --num-results 22 | --selection-color 23 | --selection-match-color 24 | --selection-background 25 | --selection-background-padding 26 | --selection-background-corner-radius 27 | --outline-width 28 | --outline-color 29 | --text-cursor 30 | --text-cursor-style 31 | --text-cursor-color 32 | --text-cursor-background 33 | --text-cursor-corner-radius 34 | --text-cursor-thickness 35 | --prompt-text 36 | --prompt-padding 37 | --prompt-color 38 | --prompt-background 39 | --prompt-background-padding 40 | --prompt-background-corner-radius 41 | --placeholder-text 42 | --placeholder-color 43 | --placeholder-background 44 | --placeholder-background-padding 45 | --placeholder-background-corner-radius 46 | --input-color 47 | --input-background 48 | --input-background-padding 49 | --input-background-corner-radius 50 | --default-result-color 51 | --default-result-background 52 | --default-result-background-padding 53 | --default-result-background-corner-radius 54 | --alternate-result-color 55 | --alternate-result-background 56 | --alternate-result-background-padding 57 | --alternate-result-background-corner-radius 58 | --result-spacing 59 | --min-input-width 60 | --border-width 61 | --border-color 62 | --text-color 63 | --width 64 | --height 65 | --exclusive-zone 66 | --margin-top 67 | --margin-bottom 68 | --margin-left 69 | --margin-right 70 | --padding-top 71 | --padding-bottom 72 | --padding-left 73 | --padding-right 74 | --clip-to-padding 75 | --horizontal 76 | --hide-cursor 77 | --history 78 | --history-file 79 | --matching-algorithm 80 | --fuzzy-match 81 | --require-match 82 | --auto-accept-single 83 | --print-index 84 | --hide-input 85 | --hidden-character 86 | --physical-keybindings 87 | --drun-launch 88 | --terminal 89 | --hint-font 90 | --late-keyboard-init 91 | --multi-instance 92 | --ascii-input 93 | ) 94 | 95 | case "${prev}" in 96 | --font) 97 | ;& 98 | --history-file) 99 | ;& 100 | --include) 101 | ;& 102 | --config|-c) 103 | _filedir 104 | return 0 105 | ;; 106 | --help|-h) 107 | ;; 108 | --*) 109 | return 0 110 | ;; 111 | esac 112 | case "${cur}" in 113 | -[ch]) 114 | COMPREPLY=($cur) 115 | ;; 116 | *) 117 | COMPREPLY=($(compgen -W "${words[*]}" -- ${cur})) 118 | return 0 119 | ;; 120 | esac 121 | } 122 | complete -F _tofi tofi 123 | complete -F _tofi tofi-run 124 | complete -F _tofi tofi-drun 125 | -------------------------------------------------------------------------------- /doc/config: -------------------------------------------------------------------------------- 1 | # Default config for tofi 2 | # 3 | # Copy this file to ~/.config/tofi/config and get customising! 4 | # 5 | # A complete reference of available options can be found in `man 5 tofi`. 6 | 7 | # 8 | ### Fonts 9 | # 10 | # Font to use, either a path to a font file or a name. 11 | # 12 | # If a path is given, tofi will startup much quicker, but any 13 | # characters not in the chosen font will fail to render. 14 | # 15 | # Otherwise, fonts are interpreted in Pango format. 16 | font = "Sans" 17 | 18 | # Point size of text. 19 | font-size = 24 20 | 21 | # Comma separated list of OpenType font feature settings to apply, 22 | # if supported by the chosen font. The format is similar to the CSS 23 | # "font-feature-settings" property. 24 | # 25 | # Examples: 26 | # 27 | # font-features = "smcp, c2sc" (all small caps) 28 | # font-features = "liga 0" (disable ligatures) 29 | font-features = "" 30 | 31 | # Comma separated list of OpenType font variation settings to apply 32 | # to variable fonts. The format is similar to the CSS 33 | # "font-variation-settings" property. 34 | # 35 | # Examples: 36 | # 37 | # font-variations = "wght 900" (Extra bold) 38 | # font-variations = "wdth 25, slnt -10" (Narrow and slanted) 39 | font-variations = "" 40 | 41 | # Perform font hinting. Only applies when a path to a font has been 42 | # specified via `font`. Disabling font hinting speeds up text 43 | # rendering appreciably, but will likely look poor at small font pixel 44 | # sizes. 45 | hint-font = true 46 | 47 | # 48 | ### Text theming 49 | # 50 | # Default text color 51 | # 52 | # All text defaults to this color if not otherwise specified. 53 | text-color = #FFFFFF 54 | 55 | # All pieces of text have the same theming attributes available: 56 | # 57 | # *-color 58 | # Foreground color 59 | # 60 | # *-background 61 | # Background color 62 | # 63 | # *-background-padding 64 | # Background padding in pixels (comma-delimited, CSS-style list). 65 | # See "DIRECTIONAL VALUES" under `man 5 tofi` for more info. 66 | # 67 | # *-background-corner-radius 68 | # Radius of background box corners in pixels 69 | 70 | # Prompt text theme 71 | # prompt-color = #FFFFFF 72 | prompt-background = #00000000 73 | prompt-background-padding = 0 74 | prompt-background-corner-radius = 0 75 | 76 | # Placeholder text theme 77 | placeholder-color = #FFFFFFA8 78 | placeholder-background = #00000000 79 | placeholder-background-padding = 0 80 | placeholder-background-corner-radius = 0 81 | 82 | # Input text theme 83 | # input-color = #FFFFFF 84 | input-background = #00000000 85 | input-background-padding = 0 86 | input-background-corner-radius = 0 87 | 88 | # Default result text theme 89 | # default-result-color = #FFFFFF 90 | default-result-background = #00000000 91 | default-result-background-padding = 0 92 | default-result-background-corner-radius = 0 93 | 94 | # Alternate (even-numbered) result text theme 95 | # 96 | # If unspecified, these all default to the corresponding 97 | # default-result-* attribute. 98 | # 99 | # alternate-result-color = #FFFFFF 100 | # alternate-result-background = #00000000 101 | # alternate-result-background-padding = 0 102 | # alternate-result-background-corner-radius = 0 103 | 104 | # Selection text 105 | selection-color = #F92672 106 | selection-background = #00000000 107 | selection-background-padding = 0 108 | selection-background-corner-radius = 0 109 | 110 | # Matching portion of selection text 111 | selection-match-color = #00000000 112 | 113 | 114 | # 115 | ### Text cursor theme 116 | # 117 | # Style of the optional text cursor. 118 | # 119 | # Supported values: bar, block, underscore 120 | text-cursor-style = bar 121 | 122 | # Color of the text cursor 123 | # 124 | # If unspecified, defaults to the same as input-color 125 | # text-cursor-color = #FFFFFF 126 | 127 | # Color of text behind the text cursor when text-cursor-style = block 128 | # 129 | # If unspecified, defaults to the same as background-color 130 | # text-cursor-background = #000000 131 | 132 | # Corner radius of the text cursor 133 | text-cursor-corner-radius = 0 134 | 135 | # Thickness of the bar and underscore text cursors. 136 | # 137 | # If unspecified, defaults to a font-dependent value when 138 | # text-cursor-style = underscore, or to 2 otherwise. 139 | # text-cursor-thickness = 2 140 | 141 | # 142 | ### Text layout 143 | # 144 | # Prompt to display. 145 | prompt-text = "run: " 146 | 147 | # Extra horizontal padding between prompt and input. 148 | prompt-padding = 0 149 | 150 | # Placeholder input text. 151 | placeholder-text = "" 152 | 153 | # Maximum number of results to display. 154 | # If 0, tofi will draw as many results as it can fit in the window. 155 | num-results = 0 156 | 157 | # Spacing between results in pixels. Can be negative. 158 | result-spacing = 0 159 | 160 | # List results horizontally. 161 | horizontal = false 162 | 163 | # Minimum width of input in horizontal mode. 164 | min-input-width = 0 165 | 166 | # 167 | ### Window theming 168 | # 169 | # Width and height of the window. Can be pixels or a percentage. 170 | width = 1280 171 | height = 720 172 | 173 | # Window background color 174 | background-color = #1B1D1E 175 | 176 | # Width of the border outlines in pixels. 177 | outline-width = 4 178 | 179 | # Border outline color 180 | outline-color = #080800 181 | 182 | # Width of the border in pixels. 183 | border-width = 12 184 | 185 | # Border color 186 | border-color = #F92672 187 | 188 | # Radius of window corners in pixels. 189 | corner-radius = 0 190 | 191 | # Padding between borders and text. Can be pixels or a percentage. 192 | padding-top = 8 193 | padding-bottom = 8 194 | padding-left = 8 195 | padding-right = 8 196 | 197 | # Whether to clip text drawing to be within the specified padding. This 198 | # is mostly important for allowing text to be inset from the border, 199 | # while still allowing text backgrounds to reach right to the edge. 200 | clip-to-padding = true 201 | 202 | # Whether to scale the window by the output's scale factor. 203 | scale = true 204 | 205 | # 206 | ### Window positioning 207 | # 208 | # The name of the output to appear on. An empty string will use the 209 | # default output chosen by the compositor. 210 | output = "" 211 | 212 | # Location on screen to anchor the window to. 213 | # 214 | # Supported values: top-left, top, top-right, right, bottom-right, 215 | # bottom, bottom-left, left, center. 216 | anchor = center 217 | 218 | # Set the size of the exclusive zone. 219 | # 220 | # A value of -1 means ignore exclusive zones completely. 221 | # A value of 0 will move tofi out of the way of other windows' zones. 222 | # A value greater than 0 will set that much space as an exclusive zone. 223 | # 224 | # Values greater than 0 are only meaningful when tofi is anchored to a 225 | # single edge. 226 | exclusive-zone = -1 227 | 228 | # Window offset from edge of screen. Only has an effect when anchored 229 | # to the relevant edge. Can be pixels or a percentage. 230 | margin-top = 0 231 | margin-bottom = 0 232 | margin-left = 0 233 | margin-right = 0 234 | 235 | # 236 | ### Behaviour 237 | # 238 | # Hide the mouse cursor. 239 | hide-cursor = false 240 | 241 | # Show a text cursor in the input field. 242 | text-cursor = false 243 | 244 | # Sort results by number of usages in run and drun modes. 245 | history = true 246 | 247 | # Specify an alternate file to read and store history information 248 | # from / to. This shouldn't normally be needed, and is intended to 249 | # facilitate the creation of custom modes. 250 | # history-file = /path/to/histfile 251 | 252 | # Select the matching algorithm used. If normal, substring matching is 253 | # used, weighted to favour matches closer to the beginning of the 254 | # string. If prefix, only substrings at the beginning of the string are 255 | # matched. If fuzzy, searching is performed via a simple fuzzy matching 256 | # algorithm. 257 | # 258 | # Supported values: normal, prefix, fuzzy 259 | matching-algorithm = normal 260 | 261 | # If true, require a match to allow a selection to be made. If false, 262 | # making a selection with no matches will print input to stdout. 263 | # In drun mode, this is always true. 264 | require-match = true 265 | 266 | # If true, automatically accept a result if it is the only one 267 | # remaining. If there's only one result on startup, window creation is 268 | # skipped altogether. 269 | auto-accept-single = false 270 | 271 | # If true, typed input will be hidden, and what is displayed (if 272 | # anything) is determined by the hidden-character option. 273 | hide-input = false 274 | 275 | # Replace displayed input characters with a character. If the empty 276 | # string is given, input will be completely hidden. 277 | # This option only has an effect when hide-input is set to true. 278 | hidden-character = "*" 279 | 280 | # If true, use physical keys for shortcuts, regardless of the current 281 | # keyboard layout. If false, use the current layout's keys. 282 | physical-keybindings = true 283 | 284 | # Instead of printing the selected entry, print the 1-based index of 285 | # the selection. This option has no effect in run or drun mode. If 286 | # require-match is set to false, non-matching input will still result 287 | # in the input being printed. 288 | print-index = false 289 | 290 | # If true, directly launch applications on selection when in drun mode. 291 | # Otherwise, just print the command line to stdout. 292 | drun-launch = false 293 | 294 | # The terminal to run terminal programs in when in drun mode. 295 | # This option has no effect if drun-launch is set to true. 296 | # Defaults to the value of the TERMINAL environment variable. 297 | # terminal = foot 298 | 299 | # Delay keyboard initialisation until after the first draw to screen. 300 | # This option is experimental, and will cause tofi to miss keypresses 301 | # for a short time after launch. The only reason to use this option is 302 | # performance on slow systems. 303 | late-keyboard-init = false 304 | 305 | # If true, allow multiple simultaneous processes. 306 | # If false, create a lock file on startup to prevent multiple instances 307 | # from running simultaneously. 308 | multi-instance = false 309 | 310 | # Assume input is plain ASCII, and disable some Unicode handling 311 | # functions. This is faster, but means e.g. a search for "e" will not 312 | # match "é". 313 | ascii-input = false 314 | 315 | # 316 | ### Inclusion 317 | # 318 | # Configs can be split between multiple files, and then included 319 | # within each other. 320 | # include = /path/to/config 321 | -------------------------------------------------------------------------------- /doc/scd2gfm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Convert an scdoc file to GitHub markdown, with some custom tweaks to work 4 | # around pandoc's poor formatting. 5 | # 6 | # Usage: 7 | # ./scd2gfm < file.scd > file.md 8 | 9 | set -eu 10 | 11 | html=$(scd2html -f) 12 | no_sections=$(echo "$html" | sed -e "s|\([^<]*\)|\1|g") 14 | no_header=$(echo "$italics" | awk ' 15 | /
/ { 16 | header = 1 17 | } 18 | 19 | /<\/header>/ { 20 | header = 0 21 | } 22 | 23 | !/header/ { 24 | if (header == 0) { 25 | print 26 | } 27 | } 28 | ') 29 | 30 | combine_blockquotes=$(echo "$no_header" | awk ' 31 | /\/blockquote/ { 32 | if (endblock > 0) { 33 | print buffer 34 | } 35 | endblock = NR 36 | buffer = $0 37 | } 38 | 39 | /

" 42 | gsub("
", "") 43 | buffer = "" 44 | } else { 45 | print buffer 46 | print 47 | buffer = "" 48 | } 49 | } 50 | 51 | !/blockquote/ { 52 | if (endblock == NR - 2) { 53 | print buffer 54 | buffer = "" 55 | } 56 | print 57 | } 58 | ') 59 | 60 | echo "$combine_blockquotes" | pandoc --from html --to gfm | sed "s/\s\+$//" 61 | -------------------------------------------------------------------------------- /doc/tofi.1.md: -------------------------------------------------------------------------------- 1 | ## NAME 2 | 3 | tofi - Tiny dynamic menu for Wayland, inspired by **rofi**(1) and 4 | **dmenu**(1). 5 | 6 | ## SYNOPSIS 7 | 8 | **tofi** \[options...\] 9 | 10 | **tofi-run** \[options...\] 11 | 12 | **tofi-drun** \[options...\] 13 | 14 | ## DESCRIPTION 15 | 16 | **tofi** is a tiny dynamic menu for Wayland compositors supporting the 17 | layer-shell protocol. It reads newline-separated items from stdin, and 18 | displays a graphical selection menu. When a selection is made, it is 19 | printed to stdout. 20 | 21 | When invoked via the name **tofi-run**, **tofi** will not accept items 22 | on stdin, instead presenting a list of executables in the user's \$PATH. 23 | 24 | When invoked via the name **tofi-drun**, **tofi** will not accept items 25 | on stdin, and will generate a list of applications from desktop files as 26 | described in the Desktop Entry Specification. 27 | 28 | ## OPTIONS 29 | 30 | **-h, --help** 31 | 32 | > Print help and exit. 33 | 34 | **-c, --config** \ 35 | 36 | > Specify path to custom config file. 37 | 38 | All config file options described in **tofi**(5) are also accepted, in 39 | the form **--key=value**. 40 | 41 | ## KEYS 42 | 43 | \ \| \ \| \-k \| \-p \| \-b \| 44 | \-k \| \-p \| \-h \| \-\ 45 | 46 | > Move the selection back one entry. 47 | 48 | \ \| \ \| \-j \| \-n \| \-f \| 49 | \-j \| \-n \| \-l \| \ 50 | 51 | > Move the selection forward one entry. 52 | 53 | \ 54 | 55 | > Move the selection back one page. 56 | 57 | \ 58 | 59 | > Move the selection forward one page. 60 | 61 | \ \| \-h 62 | 63 | > Delete character. 64 | 65 | \-u 66 | 67 | > Delete line. 68 | 69 | \-w \| \-\ 70 | 71 | > Delete word. 72 | 73 | \ \| \-m 74 | 75 | > Confirm the current selection and quit. 76 | 77 | \ \| \-c \| \-g \| \-\[ 78 | 79 | > Quit without making a selection. 80 | 81 | ## FILES 82 | 83 | */etc/xdg/tofi/config* 84 | 85 | > Example configuration file. 86 | 87 | *\$XDG_CONFIG_HOME/tofi/config* 88 | 89 | > The default configuration file location. 90 | 91 | *\$XDG_CACHE_HOME/tofi-compgen* 92 | 93 | > Cached list of executables under \$PATH, regenerated as necessary. 94 | 95 | *\$XDG_CACHE_HOME/tofi-drun* 96 | 97 | > Cached list of desktop applications, regenerated as necessary. 98 | 99 | *\$XDG_STATE_HOME/tofi-history* 100 | 101 | > Numeric count of commands selected in **tofi-run**, to enable sorting 102 | > results by run count. 103 | 104 | *\$XDG_STATE_HOME/tofi-drun-history* 105 | 106 | > Numeric count of commands selected in **tofi-drun**, to enable sorting 107 | > results by run count. 108 | 109 | ## EXIT STATUS 110 | 111 | **tofi** exits with one of the following values: 112 | 113 | 0 114 | 115 | > Success; a selection was made, or **tofi** was invoked with the **-h** 116 | > option. 117 | 118 | 1 119 | 120 | > An error occurred, or the user exited without making a selection. 121 | 122 | ## AUTHORS 123 | 124 | Philip Jones \<\> 125 | 126 | ## SEE ALSO 127 | 128 | **tofi**(5), **dmenu**(1), **rofi**(1) 129 | -------------------------------------------------------------------------------- /doc/tofi.1.scd: -------------------------------------------------------------------------------- 1 | tofi(1) 2 | 3 | # NAME 4 | 5 | tofi - Tiny dynamic menu for Wayland, inspired by *rofi*(1) and *dmenu*(1). 6 | 7 | # SYNOPSIS 8 | 9 | *tofi* [options...] 10 | 11 | *tofi-run* [options...] 12 | 13 | *tofi-drun* [options...] 14 | 15 | # DESCRIPTION 16 | 17 | *tofi* is a tiny dynamic menu for Wayland compositors supporting the 18 | layer-shell protocol. It reads newline-separated items from stdin, and displays 19 | a graphical selection menu. When a selection is made, it is printed to stdout. 20 | 21 | When invoked via the name *tofi-run*, *tofi* will not accept items on stdin, 22 | instead presenting a list of executables in the user's $PATH. 23 | 24 | When invoked via the name *tofi-drun*, *tofi* will not accept items on stdin, 25 | and will generate a list of applications from desktop files as described in the 26 | Desktop Entry Specification. 27 | 28 | # OPTIONS 29 | 30 | *-h, --help* 31 | Print help and exit. 32 | 33 | *-c, --config* 34 | Specify path to custom config file. 35 | 36 | All config file options described in *tofi*(5) are also accepted, in the form 37 | *--key=value*. 38 | 39 | # KEYS 40 | 41 | | | -k | -p | -b | -k | -p | -h | 42 | - 43 | Move the selection back one entry. 44 | 45 | | | -j | -n | -f | -j | -n | -l | 46 | 47 | Move the selection forward one entry. 48 | 49 | 50 | Move the selection back one page. 51 | 52 | 53 | Move the selection forward one page. 54 | 55 | | -h 56 | Delete character. 57 | 58 | -u 59 | Delete line. 60 | 61 | -w | - 62 | Delete word. 63 | 64 | | -m 65 | Confirm the current selection and quit. 66 | 67 | | -c | -g | -[ 68 | Quit without making a selection. 69 | 70 | # FILES 71 | 72 | _/etc/xdg/tofi/config_ 73 | Example configuration file. 74 | 75 | _$XDG_CONFIG_HOME/tofi/config_ 76 | The default configuration file location. 77 | 78 | _$XDG_CACHE_HOME/tofi-compgen_ 79 | Cached list of executables under $PATH, regenerated as necessary. 80 | 81 | _$XDG_CACHE_HOME/tofi-drun_ 82 | Cached list of desktop applications, regenerated as necessary. 83 | 84 | _$XDG_STATE_HOME/tofi-history_ 85 | Numeric count of commands selected in *tofi-run*, to enable sorting 86 | results by run count. 87 | 88 | _$XDG_STATE_HOME/tofi-drun-history_ 89 | Numeric count of commands selected in *tofi-drun*, to enable sorting 90 | results by run count. 91 | 92 | # EXIT STATUS 93 | 94 | *tofi* exits with one of the following values: 95 | 96 | 0 97 | Success; a selection was made, or *tofi* was invoked with the *-h* 98 | option. 99 | 100 | 1 101 | An error occurred, or the user exited without making a selection. 102 | 103 | # AUTHORS 104 | 105 | Philip Jones 106 | 107 | # SEE ALSO 108 | 109 | *tofi*(5), *dmenu*(1), *rofi*(1) 110 | -------------------------------------------------------------------------------- /doc/tofi.5.scd: -------------------------------------------------------------------------------- 1 | tofi(5) 2 | 3 | # NAME 4 | 5 | tofi - configuration file 6 | 7 | # DESCRIPTION 8 | 9 | The config file format is basic .ini/.cfg style. Options are set one per line, 10 | with the syntax: 11 | 12 | option = value 13 | 14 | Whitespace is ignored. Values starting or ending with whitespace can be given 15 | by enclosing them in double quotes like so: 16 | 17 | option = " value " 18 | 19 | Lines beginning with # or ; are treated as comments. Section headers of the 20 | form [header] are currently ignored. All options and values are 21 | case-insensitive, except where not possible (e.g. paths). Later options 22 | override earlier options, and command line options override config file 23 | options. 24 | 25 | # SPECIAL OPTIONS 26 | 27 | *include*=_path_ 28 | Include the contents of another config file. If _path_ is a relative 29 | path, it is interpreted as relative to this config file's path (or the 30 | current directory if *--include* is passed on the command line). 31 | Inclusion happens immediately, before the rest of the current file's 32 | contents are parsed. 33 | 34 | # BEHAVIOUR OPTIONS 35 | 36 | *hide-cursor*=_true|false_ 37 | Hide the mouse cursor. 38 | 39 | Default: false 40 | 41 | *text-cursor*=_true|false_ 42 | Show a text cursor in the input field. 43 | 44 | Default: false 45 | 46 | *history*=_true|false_ 47 | Sort results by number of usages. By default, this is only effective in 48 | the run and drun modes - see the *history-file* option for more 49 | information. 50 | 51 | Default: true 52 | 53 | *history-file*=_path_ 54 | Specify an alternate file to read and store history information from / 55 | to. This shouldn't normally be needed, and is intended to facilitate 56 | the creation of custom modes. The default value depends on the current 57 | mode. 58 | 59 | Defaults: 60 | - tofi: None (no history file) 61 | - tofi-run: _$XDG_STATE_HOME/tofi-history_ 62 | - tofi-drun: _$XDG_STATE_HOME/tofi-drun-history_ 63 | 64 | *matching-algorithm*=_normal|prefix|fuzzy_ 65 | Select the matching algorithm used. 66 | If _normal_, substring matching is used, weighted to favour matches 67 | closer to the beginning of the string. 68 | If _prefix_, only substrings at the beginning of the string are matched. 69 | If _fuzzy_, searching is performed via a simple fuzzy matching 70 | algorithm. 71 | 72 | Default: normal 73 | 74 | *fuzzy-match*=_true|false_ 75 | *WARNING*: This option is deprecated, and may be removed in a future 76 | version of tofi. You should use the *matching-algorithm* 77 | option instead. 78 | 79 | If true, searching is performed via a simple fuzzy matching algorithm. 80 | If false, substring matching is used, weighted to favour matches closer 81 | to the beginning of the string. 82 | 83 | Default: false 84 | 85 | *require-match*=_true|false_ 86 | If true, require a match to allow a selection to be made. If false, 87 | making a selection with no matches will print input to stdout. 88 | In drun mode, this is always true. 89 | 90 | Default: true 91 | 92 | *auto-accept-single*=_true|false_ 93 | If true, automatically accept a result if it is the only one remaining. 94 | If there's only one result on startup, window creation is skipped 95 | altogether. 96 | 97 | Default: false 98 | 99 | *hide-input*=_true|false_ 100 | If true, typed input will be hidden, and what is displayed (if 101 | anything) is determined by the *hidden-character* option. 102 | 103 | Default: false 104 | 105 | *hidden-character*=_char_ 106 | Replace displayed input characters with _char_. If _char_ is set to the 107 | empty string, input will be completely hidden. 108 | This option only has an effect when *hide-input* is set to true. 109 | 110 | Default: \* 111 | 112 | *physical-keybindings*=_true|false_ 113 | If true, use physical keys for shortcuts, regardless of the current 114 | keyboard layout. If false, use the current layout's keys. 115 | 116 | Default: true 117 | 118 | *print-index*=_true|false_ 119 | Instead of printing the selected entry, print the 1-based index of the 120 | selection. This option has no effect in run or drun mode. If 121 | *require-match* is set to false, non-matching input will still result in 122 | the input being printed. 123 | 124 | Default: false 125 | 126 | *drun-launch*=_true|false_ 127 | If true, directly launch applications on selection when in drun mode. 128 | Otherwise, just print the Exec line of the .desktop file to stdout. 129 | 130 | Default: false 131 | 132 | *terminal*=_command_ 133 | The terminal to run terminal programs in when in drun mode. _command_ 134 | will be prepended to the the application's command line. 135 | This option has no effect if *drun-launch* is set to true. 136 | 137 | Default: the value of the TERMINAL environment variable 138 | 139 | *drun-print-exec*=_true|false_ 140 | *WARNING*: This option does nothing, and may be removed in a future 141 | version of tofi. 142 | 143 | Default: true 144 | 145 | *late-keyboard-init*=_true|false_ 146 | Delay keyboard initialisation until after the first draw to screen. 147 | This option is experimental, and will cause tofi to miss keypresses 148 | for a short time after launch. The only reason to use this option is 149 | performance on slow systems. 150 | 151 | Default: false 152 | 153 | *multi-instance*=_true|false_ 154 | If true, allow multiple simultaneous processes. 155 | If false, create a lock file on startup to prevent multiple instances 156 | from running simultaneously. 157 | 158 | Default: false 159 | 160 | *ascii-input*=_true|false_ 161 | Assume input is plain ASCII, and disable some Unicode handling 162 | functions. This is faster, but means e.g. a search for "e" will not 163 | match "é". 164 | 165 | Default: false 166 | 167 | # STYLE OPTIONS 168 | 169 | *font*=_font_ 170 | Font to use. If _font_ is a path to a font file, *tofi* will not have 171 | to use Pango or Fontconfig. This greatly speeds up startup, but any 172 | characters not in the chosen font will fail to render. 173 | 174 | If a path is not given, _font_ is interpreted as a font name in Pango 175 | format. 176 | 177 | Default: "Sans" 178 | 179 | *font-size*=_pt_ 180 | Point size of text. 181 | 182 | Default: 24 183 | 184 | *font-features*=_features_ 185 | Comma separated list of OpenType font feature settings to apply. The 186 | format is similar to the CSS "font-feature-settings" property. 187 | For example, "smcp, c2sc" will turn all text into small caps (if 188 | supported by the chosen font). 189 | 190 | Default: "" 191 | 192 | *font-variations*=_variations_ 193 | Comma separated list of OpenType font variation settings to apply. The 194 | format is similar to the CSS "font-variation-settings" property. For 195 | example, "wght 900" will set the weight of a variable font to 900 (if 196 | supported by the chosen font). 197 | 198 | Default: "" 199 | 200 | *background-color*=_color_ 201 | Color of the background. See *COLORS* for more information. 202 | 203 | Default: #1B1D1E 204 | 205 | *outline-width*=_px_ 206 | Width of the border outlines. 207 | 208 | Default: 4 209 | 210 | *outline-color*=_color_ 211 | Color of the border outlines. See *COLORS* for more information. 212 | 213 | Default: #080800 214 | 215 | *border-width*=_px_ 216 | Width of the border. 217 | 218 | Default: 12 219 | 220 | *border-color*=_color_ 221 | Color of the border. See *COLORS* for more information. 222 | 223 | Default: #F92672 224 | 225 | *text-color*=_color_ 226 | Color of text. See *COLORS* for more information. 227 | 228 | Default: #FFFFFF 229 | 230 | *prompt-text*=_string_ 231 | Prompt text. 232 | 233 | Default: "run: " 234 | 235 | *prompt-padding*=_px_ 236 | Extra horizontal padding between prompt and input. 237 | 238 | Default: 0 239 | 240 | *prompt-color*=_color_ 241 | Color of prompt text. See *COLORS* for more information. 242 | 243 | Default: Same as *text-color* 244 | 245 | *prompt-background*=_color_ 246 | Background color of prompt. See *COLORS* for more information. 247 | 248 | Default: #00000000 249 | 250 | *prompt-background-padding*=_directional_ 251 | Extra padding of the prompt background. See *DIRECTIONAL VALUES* for 252 | more information. 253 | 254 | Default: 0 255 | 256 | *prompt-background-corner-radius*=_px_ 257 | Corner radius of the prompt background. 258 | 259 | Default: 0 260 | 261 | *placeholder-text*=_string_ 262 | Placeholder input text. 263 | 264 | Default: "" 265 | 266 | *placeholder-color*=_color_ 267 | Color of placeholder input text. See *COLORS* for more information. 268 | 269 | Default: #FFFFFFA8 270 | 271 | *placeholder-background*=_color_ 272 | Background color of placeholder input text. See *COLORS* for more 273 | information. 274 | 275 | Default: #00000000 276 | 277 | *placeholder-background-padding*=_directional_ 278 | Extra padding of the placeholder input text background. See 279 | *DIRECTIONAL VALUES* for more information. 280 | 281 | Default: 0 282 | 283 | *placeholder-background-corner-radius*=_px_ 284 | Corner radius of the placeholder input text background. 285 | 286 | Default: 0 287 | 288 | *input-color*=_color_ 289 | Color of input text. See *COLORS* for more information. 290 | 291 | Default: Same as *text-color* 292 | 293 | *input-background*=_color_ 294 | Background color of input. See *COLORS* for more information. 295 | 296 | Default: #00000000 297 | 298 | *input-background-padding*=_directional_ 299 | Extra padding of the input background. See *DIRECTIONAL VALUES* for 300 | more information. 301 | 302 | Default: 0 303 | 304 | *input-background-corner-radius*=_px_ 305 | Corner radius of the input background. 306 | 307 | Default: 0 308 | 309 | *text-cursor-style*=_bar|block|underscore_ 310 | Style of the text cursor (if shown). 311 | 312 | Default: bar 313 | 314 | *text-cursor-color*=_color_ 315 | Color of the text cursor. 316 | 317 | Default: same as *input-color* 318 | 319 | *text-cursor-background*=_color_ 320 | Color of text behind the text cursor when *text-cursor-style*=block. 321 | 322 | Default: same as *background-color* 323 | 324 | *text-cursor-corner-radius*=_px_ 325 | Corner radius of the text cursor. 326 | 327 | Default: 0 328 | 329 | *text-cursor-thickness*=_px_ 330 | Thickness of the bar and underscore text cursors. 331 | 332 | Default: font-dependent when *text-cursor-style*=underscore, 2 333 | otherwise. 334 | 335 | *default-result-color*=_color_ 336 | Default color of result text. See *COLORS* for more information. 337 | 338 | Default: Same as *text-color* 339 | 340 | *default-result-background*=_color_ 341 | Default background color of results. See *COLORS* for more information. 342 | 343 | Default: #00000000 344 | 345 | *default-result-background-padding*=_directional_ 346 | Default extra padding of result backgrounds. See *DIRECTIONAL VALUES* 347 | for more information. 348 | 349 | Default: 0 350 | 351 | *default-result-background-corner-radius*=_px_ 352 | Default corner radius of result backgrounds. 353 | 354 | Default: 0 355 | 356 | *alternate-result-color*=_color_ 357 | Color of alternate (even-numbered) result text. See *COLORS* for more 358 | information. 359 | 360 | Default: same as *default-result-color* 361 | 362 | *alternate-result-background*=_color_ 363 | Background color of alternate (even-numbered) results. See *COLORS* for 364 | more information. 365 | 366 | Default: same as *default-result-background* 367 | 368 | *alternate-result-background-padding*=_directional_ 369 | Extra padding of alternate (even-numbered) result backgrounds. See 370 | *DIRECTIONAL VALUES* for more information. 371 | 372 | Default: same as *default-result-background-padding* 373 | 374 | *alternate-result-background-corner-radius*=_px_ 375 | Corner radius of alternate (even-numbered) result backgrounds. 376 | 377 | Default: same as *default-result-background-corner-radius* 378 | 379 | *num-results*=_n_ 380 | Maximum number of results to display. If _n_ = 0, tofi will draw as 381 | many results as it can fit in the window. 382 | 383 | Default: 0 384 | 385 | *selection-color*=_color_ 386 | Color of selected result. See *COLORS* for more information. 387 | 388 | Default: #F92672 389 | 390 | *selection-match-color*=_color_ 391 | Color of the matching portion of the selected result. This will not 392 | always be shown if the *fuzzy-match* option is set to true. Any color 393 | that is fully transparent (alpha = 0) will disable this highlighting. 394 | See *COLORS* for more information. 395 | 396 | Default: #00000000 397 | 398 | *selection-padding*=_px_ 399 | *WARNING*: This option is deprecated, and will be removed in a future 400 | version of tofi. You should use the *selection-background-padding* 401 | option instead. 402 | 403 | Extra horizontal padding of the selection background. If _px_ = -1, 404 | the padding will fill the whole window width. 405 | 406 | Default: 0 407 | 408 | *selection-background*=_color_ 409 | Background color of selected result. See *COLORS* for more information. 410 | 411 | Default: #00000000 412 | 413 | *selection-background-padding*=_directional_ 414 | Extra padding of the selected result background. See *DIRECTIONAL 415 | VALUES* for more information. 416 | 417 | Default: 0 418 | 419 | *selection-background-corner-radius*=_px_ 420 | Corner radius of the selected result background. 421 | Default: 0 422 | 423 | *result-spacing*=_px_ 424 | Spacing between results. Can be negative. 425 | 426 | Default: 0 427 | 428 | *min-input-width*=_px_ 429 | Minimum width of input in horizontal mode. 430 | 431 | Default: 0 432 | 433 | *width*=_px|%_ 434 | Width of the window. See *PERCENTAGE VALUES* for more information. 435 | 436 | Default: 1280 437 | 438 | *height*=_px|%_ 439 | Height of the window. See *PERCENTAGE VALUES* for more information. 440 | 441 | Default: 720 442 | 443 | *corner-radius*=_px_ 444 | Radius of the window corners. 445 | 446 | Default: 0 447 | 448 | *anchor*=_position_ 449 | Location on screen to anchor the window. Supported values are 450 | _top-left_, _top_, _top-right_, _right_, _bottom-right_, _bottom_, 451 | _bottom-left_, _left_, and _center_. 452 | 453 | Default: center 454 | 455 | *exclusive-zone*=_-1|px|%_ 456 | Set the size of the exclusive zone. A value of -1 means ignore 457 | exclusive zones completely. A value of 0 will move tofi out of the way 458 | of other windows' exclusive zones. A value greater than 0 will set that 459 | much space as an exclusive zone. Values greater than 0 are only 460 | meaningful when tofi is anchored to a single edge. 461 | 462 | Default: -1 463 | 464 | *output*=_name_ 465 | The name of the output to appear on, if multiple outputs are present. 466 | If empty, the compositor will choose which output to display the window 467 | on (usually the currently focused output). 468 | 469 | Default: "" 470 | 471 | *scale*=_true|false_ 472 | Scale the window by the output's scale factor. 473 | 474 | Default: true 475 | 476 | *margin-top*=_px|%_ 477 | Offset from top of screen. See *PERCENTAGE VALUES* for more 478 | information. Only has an effect when anchored to the top of the screen. 479 | 480 | Default: 0 481 | 482 | *margin-bottom*=_px|%_ 483 | Offset from bottom of screen. See *PERCENTAGE VALUES* for more 484 | information. Only has an effect when anchored to the bottom of the 485 | screen. 486 | 487 | Default: 0 488 | 489 | *margin-left*=_px|%_ 490 | Offset from left of screen. See *PERCENTAGE VALUES* for more 491 | information. Only has an effect when anchored to the left of the 492 | screen. 493 | 494 | Default: 0 495 | 496 | *margin-right*=_px|%_ 497 | Offset from right of screen. See *PERCENTAGE VALUES* for more 498 | information. Only has an effect when anchored to the right of the 499 | screen. 500 | 501 | Default: 0 502 | 503 | *padding-top*=_px|%_ 504 | Padding between top border and text. See *PERCENTAGE VALUES* for more 505 | information. 506 | 507 | Default: 8 508 | 509 | *padding-bottom*=_px|%_ 510 | Padding between bottom border and text. See *PERCENTAGE VALUES* for 511 | more information. 512 | 513 | Default: 8 514 | 515 | *padding-left*=_px|%_ 516 | Padding between left border and text. See *PERCENTAGE VALUES* for more 517 | information. 518 | 519 | Default: 8 520 | 521 | *padding-right*=_px|%_ 522 | Padding between right border and text. See *PERCENTAGE VALUES* for more 523 | information. 524 | 525 | Default: 8 526 | 527 | *clip-to-padding*=_true|false_ 528 | Whether to clip text drawing to be within the specified padding. This 529 | is mostly important for allowing text to be inset from the border, 530 | while still allowing text backgrounds to reach right to the edge. 531 | 532 | Default: true 533 | 534 | *horizontal*=_true|false_ 535 | List results horizontally. 536 | 537 | Default: false 538 | 539 | *hint-font*=_true|false_ 540 | Perform font hinting. Only applies when a path to a font has been 541 | specified via *font*. Disabling font hinting speeds up text 542 | rendering appreciably, but will likely look poor at small font pixel 543 | sizes. 544 | 545 | Default: true 546 | 547 | # COLORS 548 | 549 | Colors can be specified in the form _RGB_, _RGBA_, _RRGGBB_ or _RRGGBBAA_, 550 | optionally prefixed with a hash (#). 551 | 552 | # PERCENTAGE VALUES 553 | 554 | Some pixel values can optionally have a % suffix, like so: 555 | 556 | width = 50% 557 | 558 | This will be interpreted as a percentage of the screen resolution in the 559 | relevant direction. 560 | 561 | # DIRECTIONAL VALUES 562 | 563 | The background box padding of a type of text can be specified by one to four 564 | comma separated values, with meanings similar to the CSS padding property: 565 | 566 | - One value sets all edges. 567 | - Two values set (top & bottom), (left & right) edges. 568 | - Three values set (top), (left & right), (bottom) edges. 569 | - Four values set (top), (right), (bottom), (left) edges. 570 | 571 | Specifying -1 for any of the values will pad as far as possible in that 572 | direction. 573 | 574 | # AUTHORS 575 | 576 | Philip Jones 577 | 578 | # SEE ALSO 579 | 580 | *tofi*(1), *dmenu*(1) *rofi*(1) 581 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'tofi', 3 | 'c', 4 | version: '0.9.1', 5 | license: 'MIT', 6 | meson_version: '>=0.61.0', 7 | default_options: [ 8 | 'c_std=c2x', 9 | 'optimization=3', 10 | 'buildtype=debugoptimized', 11 | 'warning_level=3', 12 | 'b_lto=true', 13 | 'b_lto_threads=-1', 14 | 'b_pie=true', 15 | 'prefix=/usr' 16 | ], 17 | ) 18 | 19 | debug = get_option('buildtype').startswith('debug') 20 | if debug 21 | add_project_arguments('-DDEBUG', language : 'c') 22 | endif 23 | 24 | config_location = join_paths( 25 | get_option('sysconfdir'), 26 | 'xdg', 27 | 'tofi' 28 | ) 29 | 30 | install_data( 31 | 'doc/config', 32 | install_dir: config_location 33 | ) 34 | 35 | license_dir = join_paths( 36 | get_option('datadir'), 37 | 'licenses', 38 | 'tofi' 39 | ) 40 | 41 | install_data( 42 | 'LICENSE', 43 | install_dir: license_dir 44 | ) 45 | 46 | completion_location = join_paths( 47 | get_option('prefix'), 48 | get_option('datadir'), 49 | 'bash-completion', 50 | 'completions' 51 | ) 52 | 53 | install_data( 54 | 'completions/tofi', 55 | install_dir: completion_location 56 | ) 57 | 58 | install_symlink( 59 | 'tofi-run', 60 | install_dir: completion_location, 61 | pointing_to: 'tofi', 62 | ) 63 | 64 | install_symlink( 65 | 'tofi-drun', 66 | install_dir: completion_location, 67 | pointing_to: 'tofi', 68 | ) 69 | 70 | install_symlink( 71 | 'tofi-run', 72 | install_dir: get_option('bindir'), 73 | pointing_to: 'tofi', 74 | ) 75 | 76 | install_symlink( 77 | 'tofi-drun', 78 | install_dir: get_option('bindir'), 79 | pointing_to: 'tofi', 80 | ) 81 | 82 | add_project_arguments( 83 | [ 84 | '-pedantic', 85 | #'-Wconversion', 86 | '-Wshadow', 87 | '-Wno-unused-parameter', 88 | '-D_GNU_SOURCE', 89 | '-D_FORTIFY_SOURCE=2', 90 | # Disable these unwind tables for binary size, as we don't use exceptions 91 | # or anything else that requires them. 92 | '-fno-asynchronous-unwind-tables', 93 | ], 94 | language: 'c' 95 | ) 96 | 97 | common_sources = files( 98 | 'src/clipboard.c', 99 | 'src/color.c', 100 | 'src/compgen.c', 101 | 'src/config.c', 102 | 'src/desktop_vec.c', 103 | 'src/drun.c', 104 | 'src/entry.c', 105 | 'src/entry_backend/pango.c', 106 | 'src/entry_backend/harfbuzz.c', 107 | 'src/matching.c', 108 | 'src/history.c', 109 | 'src/input.c', 110 | 'src/lock.c', 111 | 'src/log.c', 112 | 'src/mkdirp.c', 113 | 'src/scale.c', 114 | 'src/shm.c', 115 | 'src/string_vec.c', 116 | 'src/surface.c', 117 | 'src/unicode.c', 118 | 'src/xmalloc.c', 119 | ) 120 | 121 | compgen_sources = files( 122 | 'src/main_compgen.c', 123 | 'src/compgen.c', 124 | 'src/matching.c', 125 | 'src/log.c', 126 | 'src/mkdirp.c', 127 | 'src/string_vec.c', 128 | 'src/unicode.c', 129 | 'src/xmalloc.c' 130 | ) 131 | 132 | cc = meson.get_compiler('c') 133 | librt = cc.find_library('rt', required: false) 134 | libm = cc.find_library('m', required: false) 135 | # On systems where libc doesn't provide fts (i.e. musl) we require libfts 136 | libfts = cc.find_library('fts', required: not cc.has_function('fts_read')) 137 | freetype = dependency('freetype2') 138 | harfbuzz = dependency('harfbuzz') 139 | cairo = dependency('cairo') 140 | pangocairo = dependency('pangocairo') 141 | wayland_client = dependency('wayland-client') 142 | wayland_protocols = dependency('wayland-protocols', native: true) 143 | wayland_scanner_dep = dependency('wayland-scanner', native: true) 144 | xkbcommon = dependency('xkbcommon') 145 | glib = dependency('glib-2.0') 146 | gio_unix = dependency('gio-unix-2.0') 147 | 148 | if wayland_client.version().version_compare('<1.20.0') 149 | add_project_arguments( 150 | ['-DNO_WL_OUTPUT_NAME=1'], 151 | language: 'c' 152 | ) 153 | endif 154 | 155 | if harfbuzz.version().version_compare('<4.0.0') 156 | add_project_arguments( 157 | ['-DNO_HARFBUZZ_METRIC_FALLBACK=1'], 158 | language: 'c' 159 | ) 160 | endif 161 | 162 | if harfbuzz.version().version_compare('<4.4.0') 163 | add_project_arguments( 164 | ['-DNO_HARFBUZZ_FONT_CHANGED=1'], 165 | language: 'c' 166 | ) 167 | endif 168 | 169 | 170 | 171 | # Generate the necessary Wayland headers / sources with wayland-scanner 172 | wayland_scanner = find_program( 173 | wayland_scanner_dep.get_variable(pkgconfig: 'wayland_scanner'), 174 | native: true 175 | ) 176 | 177 | wayland_protocols_dir = wayland_protocols.get_variable(pkgconfig: 'pkgdatadir') 178 | 179 | wl_proto_headers = [] 180 | wl_proto_src = [] 181 | wl_proto_xml = [ 182 | wayland_protocols_dir + '/stable/xdg-shell/xdg-shell.xml', 183 | wayland_protocols_dir + '/stable/viewporter/viewporter.xml', 184 | 'protocols/wlr-layer-shell-unstable-v1.xml', 185 | 'protocols/fractional-scale-v1.xml' 186 | ] 187 | 188 | foreach proto : wl_proto_xml 189 | wl_proto_headers += custom_target( 190 | proto.underscorify() + '_client_header', 191 | output: '@BASENAME@.h', 192 | input: proto, 193 | command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@']) 194 | 195 | wl_proto_src += custom_target( 196 | proto.underscorify() + '_private_code', 197 | output: '@BASENAME@.c', 198 | input: proto, 199 | command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@']) 200 | endforeach 201 | 202 | subdir('test') 203 | 204 | executable( 205 | 'tofi', 206 | files('src/main.c'), common_sources, wl_proto_src, wl_proto_headers, 207 | dependencies: [librt, libm, libfts, freetype, harfbuzz, cairo, pangocairo, wayland_client, xkbcommon, glib, gio_unix], 208 | install: true 209 | ) 210 | 211 | executable( 212 | 'tofi-compgen', 213 | compgen_sources, 214 | dependencies: [glib], 215 | install: false 216 | ) 217 | 218 | scdoc = find_program('scdoc', required: get_option('man-pages')) 219 | if scdoc.found() 220 | sed = find_program('sed') 221 | sh = find_program('sh') 222 | mandir = get_option('mandir') 223 | 224 | output = 'tofi.1' 225 | custom_target( 226 | output, 227 | input: 'doc/tofi.1.scd', 228 | output: output, 229 | command: [ 230 | sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.full_path(), output) 231 | ], 232 | install: true, 233 | install_dir: '@0@/man1'.format(mandir) 234 | ) 235 | 236 | install_symlink( 237 | 'tofi-run.1', 238 | install_dir: '@0@/man1'.format(mandir), 239 | pointing_to: 'tofi.1' 240 | ) 241 | 242 | install_symlink( 243 | 'tofi-drun.1', 244 | install_dir: '@0@/man1'.format(mandir), 245 | pointing_to: 'tofi.1' 246 | ) 247 | 248 | 249 | output = 'tofi.5' 250 | custom_target( 251 | output, 252 | input: 'doc/tofi.5.scd', 253 | output: output, 254 | command: [ 255 | sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.full_path(), output) 256 | ], 257 | install: true, 258 | install_dir: '@0@/man5'.format(mandir) 259 | ) 260 | endif 261 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('man-pages', type: 'feature', value: 'auto', description: 'Install man pages.') 2 | -------------------------------------------------------------------------------- /protocols/fractional-scale-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2022 Kenny Levinsen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice (including the next 14 | paragraph) shall be included in all copies or substantial portions of the 15 | Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | 27 | This protocol allows a compositor to suggest for surfaces to render at 28 | fractional scales. 29 | 30 | A client can submit scaled content by utilizing wp_viewport. This is done by 31 | creating a wp_viewport object for the surface and setting the destination 32 | rectangle to the surface size before the scale factor is applied. 33 | 34 | The buffer size is calculated by multiplying the surface size by the 35 | intended scale. 36 | 37 | The wl_surface buffer scale should remain set to 1. 38 | 39 | If a surface has a surface-local size of 100 px by 50 px and wishes to 40 | submit buffers with a scale of 1.5, then a buffer of 150px by 75 px should 41 | be used and the wp_viewport destination rectangle should be 100 px by 50 px. 42 | 43 | For toplevel surfaces, the size is rounded halfway away from zero. The 44 | rounding algorithm for subsurface position and size is not defined. 45 | 46 | 47 | 48 | 49 | A global interface for requesting surfaces to use fractional scales. 50 | 51 | 52 | 53 | 54 | Informs the server that the client will not be using this protocol 55 | object anymore. This does not affect any other objects, 56 | wp_fractional_scale_v1 objects included. 57 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 67 | Create an add-on object for the the wl_surface to let the compositor 68 | request fractional scales. If the given wl_surface already has a 69 | wp_fractional_scale_v1 object associated, the fractional_scale_exists 70 | protocol error is raised. 71 | 72 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | An additional interface to a wl_surface object which allows the compositor 82 | to inform the client of the preferred scale. 83 | 84 | 85 | 86 | 87 | Destroy the fractional scale object. When this object is destroyed, 88 | preferred_scale events will no longer be sent. 89 | 90 | 91 | 92 | 93 | 94 | Notification of a new preferred scale for this surface that the 95 | compositor suggests that the client should use. 96 | 97 | The sent scale is the numerator of a fraction with a denominator of 120. 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /screenshot_dark_paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philj56/tofi/1eb6137572ab6c257ab6ab851d5d742167c18120/screenshot_dark_paper.png -------------------------------------------------------------------------------- /screenshot_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philj56/tofi/1eb6137572ab6c257ab6ab851d5d742167c18120/screenshot_default.png -------------------------------------------------------------------------------- /screenshot_dmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philj56/tofi/1eb6137572ab6c257ab6ab851d5d742167c18120/screenshot_dmenu.png -------------------------------------------------------------------------------- /screenshot_dos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philj56/tofi/1eb6137572ab6c257ab6ab851d5d742167c18120/screenshot_dos.png -------------------------------------------------------------------------------- /screenshot_fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philj56/tofi/1eb6137572ab6c257ab6ab851d5d742167c18120/screenshot_fullscreen.png -------------------------------------------------------------------------------- /screenshot_soy_milk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philj56/tofi/1eb6137572ab6c257ab6ab851d5d742167c18120/screenshot_soy_milk.png -------------------------------------------------------------------------------- /src/clipboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "clipboard.h" 3 | 4 | void clipboard_finish_paste(struct clipboard *clipboard) 5 | { 6 | if (clipboard->fd > 0) { 7 | close(clipboard->fd); 8 | clipboard->fd = 0; 9 | } 10 | } 11 | 12 | void clipboard_reset(struct clipboard *clipboard) 13 | { 14 | if (clipboard->wl_data_offer != NULL) { 15 | wl_data_offer_destroy(clipboard->wl_data_offer); 16 | clipboard->wl_data_offer = NULL; 17 | } 18 | if (clipboard->fd > 0) { 19 | close(clipboard->fd); 20 | clipboard->fd = 0; 21 | } 22 | clipboard->mime_type = NULL; 23 | } 24 | -------------------------------------------------------------------------------- /src/clipboard.h: -------------------------------------------------------------------------------- 1 | #ifndef CLIPBOARD_H 2 | #define CLIPBOARD_H 3 | 4 | #include 5 | 6 | struct clipboard { 7 | struct wl_data_offer *wl_data_offer; 8 | const char *mime_type; 9 | int fd; 10 | }; 11 | 12 | void clipboard_finish_paste(struct clipboard *clipboard); 13 | void clipboard_reset(struct clipboard *clipboard); 14 | 15 | #endif /* CLIPBOARD_H */ 16 | -------------------------------------------------------------------------------- /src/color.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "color.h" 5 | #include "log.h" 6 | 7 | struct color hex_to_color(const char *hex) 8 | { 9 | if (hex[0] == '#') { 10 | hex++; 11 | } 12 | 13 | uint32_t val = 0; 14 | int64_t tmp; 15 | size_t len = strlen(hex); 16 | 17 | errno = 0; 18 | if (len == 3) { 19 | char str[] = { 20 | hex[0], hex[0], 21 | hex[1], hex[1], 22 | hex[2], hex[2], 23 | '\0'}; 24 | char *endptr; 25 | tmp = strtoll(str, &endptr, 16); 26 | if (errno || *endptr != '\0' || tmp < 0) { 27 | return (struct color) { -1, -1, -1, -1 }; 28 | } 29 | val = tmp; 30 | val <<= 8; 31 | val |= 0xFFu; 32 | } else if (len == 4) { 33 | char str[] = { 34 | hex[0], hex[0], 35 | hex[1], hex[1], 36 | hex[2], hex[2], 37 | hex[3], hex[3], 38 | '\0'}; 39 | char *endptr; 40 | tmp = strtoll(str, &endptr, 16); 41 | if (errno || *endptr != '\0' || tmp < 0) { 42 | return (struct color) { -1, -1, -1, -1 }; 43 | } 44 | val = tmp; 45 | } else if (len == 6) { 46 | char *endptr; 47 | tmp = strtoll(hex, &endptr, 16); 48 | if (errno || *endptr != '\0' || tmp < 0) { 49 | return (struct color) { -1, -1, -1, -1 }; 50 | } 51 | val = tmp; 52 | val <<= 8; 53 | val |= 0xFFu; 54 | } else if (len == 8) { 55 | char *endptr; 56 | tmp = strtoll(hex, &endptr, 16); 57 | if (errno || *endptr != '\0' || tmp < 0) { 58 | return (struct color) { -1, -1, -1, -1 }; 59 | } 60 | val = tmp; 61 | } else { 62 | return (struct color) { -1, -1, -1, -1 }; 63 | } 64 | 65 | return (struct color) { 66 | .r = (float)((val & 0xFF000000u) >> 24) / 255.0f, 67 | .g = (float)((val & 0x00FF0000u) >> 16) / 255.0f, 68 | .b = (float)((val & 0x0000FF00u) >> 8) / 255.0f, 69 | .a = (float)((val & 0x000000FFu) >> 0) / 255.0f, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/color.h: -------------------------------------------------------------------------------- 1 | #ifndef COLOR_H 2 | #define COLOR_H 3 | 4 | #include 5 | #include 6 | 7 | struct color { 8 | float r; 9 | float g; 10 | float b; 11 | float a; 12 | }; 13 | 14 | struct color hex_to_color(const char *hex); 15 | 16 | #endif /* COLOR_H */ 17 | -------------------------------------------------------------------------------- /src/compgen.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "compgen.h" 10 | #include "history.h" 11 | #include "log.h" 12 | #include "mkdirp.h" 13 | #include "string_vec.h" 14 | #include "xmalloc.h" 15 | 16 | static const char *default_cache_dir = ".cache"; 17 | static const char *cache_basename = "tofi-compgen"; 18 | 19 | [[nodiscard("memory leaked")]] 20 | static char *get_cache_path() { 21 | char *cache_name = NULL; 22 | const char *state_path = getenv("XDG_CACHE_HOME"); 23 | if (state_path == NULL) { 24 | const char *home = getenv("HOME"); 25 | if (home == NULL) { 26 | log_error("Couldn't retrieve HOME from environment.\n"); 27 | return NULL; 28 | } 29 | size_t len = strlen(home) + 1 30 | + strlen(default_cache_dir) + 1 31 | + strlen(cache_basename) + 1; 32 | cache_name = xmalloc(len); 33 | snprintf( 34 | cache_name, 35 | len, 36 | "%s/%s/%s", 37 | home, 38 | default_cache_dir, 39 | cache_basename); 40 | } else { 41 | size_t len = strlen(state_path) + 1 42 | + strlen(cache_basename) + 1; 43 | cache_name = xmalloc(len); 44 | snprintf( 45 | cache_name, 46 | len, 47 | "%s/%s", 48 | state_path, 49 | cache_basename); 50 | } 51 | return cache_name; 52 | } 53 | 54 | static void write_cache(const char *buffer, const char *filename) 55 | { 56 | errno = 0; 57 | FILE *fp = fopen(filename, "wb"); 58 | if (!fp) { 59 | log_error("Failed to open cache file \"%s\": %s\n", filename, strerror(errno)); 60 | return; 61 | } 62 | size_t len = strlen(buffer); 63 | errno = 0; 64 | if (fwrite(buffer, 1, len, fp) != len) { 65 | log_error("Error writing cache file \"%s\": %s\n", filename, strerror(errno)); 66 | } 67 | fclose(fp); 68 | } 69 | 70 | static char *read_cache(const char *filename) 71 | { 72 | errno = 0; 73 | FILE *fp = fopen(filename, "rb"); 74 | if (!fp) { 75 | log_error("Failed to open cache file \"%s\": %s\n", filename, strerror(errno)); 76 | return NULL; 77 | } 78 | if (fseek(fp, 0, SEEK_END)) { 79 | log_error("Failed to seek in cache file: %s\n", strerror(errno)); 80 | fclose(fp); 81 | return NULL; 82 | } 83 | size_t size; 84 | { 85 | long ssize = ftell(fp); 86 | if (ssize < 0) { 87 | log_error("Failed to determine cache file size: %s\n", strerror(errno)); 88 | fclose(fp); 89 | return NULL; 90 | } 91 | size = (size_t)ssize; 92 | } 93 | char *cache = xmalloc(size + 1); 94 | rewind(fp); 95 | if (fread(cache, 1, size, fp) != size) { 96 | log_error("Failed to read cache file: %s\n", strerror(errno)); 97 | free(cache); 98 | fclose(fp); 99 | return NULL; 100 | } 101 | fclose(fp); 102 | cache[size] = '\0'; 103 | 104 | return cache; 105 | } 106 | 107 | char *compgen_cached() 108 | { 109 | log_debug("Retrieving PATH.\n"); 110 | const char *env_path = getenv("PATH"); 111 | if (env_path == NULL) { 112 | log_error("Couldn't retrieve PATH from environment.\n"); 113 | exit(EXIT_FAILURE); 114 | } 115 | 116 | log_debug("Retrieving cache location.\n"); 117 | char *cache_path = get_cache_path(); 118 | 119 | struct stat sb; 120 | if (cache_path == NULL) { 121 | return compgen(); 122 | } 123 | 124 | /* If the cache doesn't exist, create it and return */ 125 | errno = 0; 126 | if (stat(cache_path, &sb) == -1) { 127 | if (errno == ENOENT) { 128 | char *commands = compgen(); 129 | if (mkdirp(cache_path)) { 130 | write_cache(commands, cache_path); 131 | } 132 | free(cache_path); 133 | return commands; 134 | } 135 | free(cache_path); 136 | return compgen(); 137 | } 138 | 139 | /* The cache exists, so check if it's still in date */ 140 | char *path = xstrdup(env_path); 141 | char *saveptr = NULL; 142 | char *path_entry = strtok_r(path, ":", &saveptr); 143 | bool out_of_date = false; 144 | while (path_entry != NULL) { 145 | struct stat path_sb; 146 | if (stat(path_entry, &path_sb) == 0) { 147 | if (path_sb.st_mtim.tv_sec > sb.st_mtim.tv_sec) { 148 | out_of_date = true; 149 | break; 150 | } 151 | } 152 | path_entry = strtok_r(NULL, ":", &saveptr); 153 | } 154 | free(path); 155 | 156 | char *commands; 157 | if (out_of_date) { 158 | log_debug("Cache out of date, updating.\n"); 159 | log_indent(); 160 | commands = compgen(); 161 | log_unindent(); 162 | write_cache(commands, cache_path); 163 | } else { 164 | log_debug("Cache up to date, loading.\n"); 165 | commands = read_cache(cache_path); 166 | } 167 | free(cache_path); 168 | return commands; 169 | } 170 | 171 | char *compgen() 172 | { 173 | log_debug("Retrieving PATH.\n"); 174 | const char *env_path = getenv("PATH"); 175 | if (env_path == NULL) { 176 | log_error("Couldn't retrieve PATH from environment.\n"); 177 | exit(EXIT_FAILURE); 178 | } 179 | 180 | struct string_vec programs = string_vec_create(); 181 | char *path = xstrdup(env_path); 182 | char *saveptr = NULL; 183 | char *path_entry = strtok_r(path, ":", &saveptr); 184 | 185 | log_debug("Scanning PATH for binaries.\n"); 186 | while (path_entry != NULL) { 187 | DIR *dir = opendir(path_entry); 188 | if (dir != NULL) { 189 | int fd = dirfd(dir); 190 | struct dirent *d; 191 | while ((d = readdir(dir)) != NULL) { 192 | struct stat sb; 193 | if (fstatat(fd, d->d_name, &sb, 0) == -1) { 194 | continue; 195 | } 196 | if (faccessat(fd, d->d_name, X_OK, 0) == -1) { 197 | continue; 198 | } 199 | if (!S_ISREG(sb.st_mode)) { 200 | continue; 201 | } 202 | string_vec_add(&programs, d->d_name); 203 | } 204 | closedir(dir); 205 | } 206 | path_entry = strtok_r(NULL, ":", &saveptr); 207 | } 208 | free(path); 209 | 210 | log_debug("Sorting results.\n"); 211 | string_vec_sort(&programs); 212 | 213 | log_debug("Making unique.\n"); 214 | string_vec_uniq(&programs); 215 | 216 | size_t buf_len = 0; 217 | for (size_t i = 0; i < programs.count; i++) { 218 | buf_len += strlen(programs.buf[i].string) + 1; 219 | } 220 | char *buf = xmalloc(buf_len + 1); 221 | size_t bytes_written = 0; 222 | for (size_t i = 0; i < programs.count; i++) { 223 | bytes_written += sprintf(&buf[bytes_written], "%s\n", programs.buf[i].string); 224 | } 225 | buf[bytes_written] = '\0'; 226 | 227 | string_vec_destroy(&programs); 228 | 229 | return buf; 230 | } 231 | 232 | static int cmpscorep(const void *restrict a, const void *restrict b) 233 | { 234 | struct scored_string *restrict str1 = (struct scored_string *)a; 235 | struct scored_string *restrict str2 = (struct scored_string *)b; 236 | return str2->history_score - str1->history_score; 237 | } 238 | 239 | struct string_ref_vec compgen_history_sort(struct string_ref_vec *programs, struct history *history) 240 | { 241 | log_debug("Moving already known programs to the front.\n"); 242 | for (size_t i = 0; i < history->count; i++) { 243 | struct scored_string_ref *res = string_ref_vec_find_sorted(programs, history->buf[i].name); 244 | if (res == NULL) { 245 | log_debug("History entry \"%s\" not found.\n", history->buf[i].name); 246 | continue; 247 | } 248 | res->history_score = history->buf[i].run_count; 249 | } 250 | 251 | /* 252 | * For compgen, we expect there to be many more commands than history 253 | * entries. For speed, we therefore create a copy of the command 254 | * vector with all of the non-zero history score items pushed to the 255 | * front. We can then call qsort() on just the first few items of the 256 | * new vector, rather than on the entire original vector. 257 | */ 258 | struct string_ref_vec vec = { 259 | .count = programs->count, 260 | .size = programs->size, 261 | .buf = xcalloc(programs->size, sizeof(*vec.buf)) 262 | }; 263 | 264 | size_t n_hist = 0; 265 | for (ssize_t i = programs->count - 1; i >= 0; i--) { 266 | if (programs->buf[i].history_score == 0) { 267 | vec.buf[i + n_hist] = programs->buf[i]; 268 | } else { 269 | vec.buf[n_hist] = programs->buf[i]; 270 | n_hist++; 271 | } 272 | } 273 | qsort(vec.buf, n_hist, sizeof(vec.buf[0]), cmpscorep); 274 | return vec; 275 | } 276 | -------------------------------------------------------------------------------- /src/compgen.h: -------------------------------------------------------------------------------- 1 | #ifndef COMPGEN_H 2 | #define COMPGEN_H 3 | 4 | #include "history.h" 5 | #include "string_vec.h" 6 | 7 | [[nodiscard("memory leaked")]] 8 | char *compgen(void); 9 | 10 | [[nodiscard("memory leaked")]] 11 | char *compgen_cached(void); 12 | 13 | [[nodiscard("memory leaked")]] 14 | struct string_ref_vec compgen_history_sort(struct string_ref_vec *programs, struct history *history); 15 | 16 | #endif /* COMPGEN_H */ 17 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef TOFI_CONFIG_H 2 | #define TOFI_CONFIG_H 3 | 4 | #include 5 | #include "tofi.h" 6 | 7 | void config_load(struct tofi *tofi, const char *filename); 8 | bool config_apply(struct tofi *tofi, const char *option, const char *value); 9 | void config_fixup_values(struct tofi *tofi); 10 | 11 | #endif /* TOFI_CONFIG_H */ 12 | -------------------------------------------------------------------------------- /src/desktop_vec.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "desktop_vec.h" 4 | #include "matching.h" 5 | #include "log.h" 6 | #include "string_vec.h" 7 | #include "unicode.h" 8 | #include "xmalloc.h" 9 | 10 | static bool match_current_desktop(char * const *desktop_list, gsize length); 11 | 12 | [[nodiscard("memory leaked")]] 13 | struct desktop_vec desktop_vec_create(void) 14 | { 15 | struct desktop_vec vec = { 16 | .count = 0, 17 | .size = 128, 18 | .buf = xcalloc(128, sizeof(*vec.buf)), 19 | }; 20 | return vec; 21 | } 22 | 23 | void desktop_vec_destroy(struct desktop_vec *restrict vec) 24 | { 25 | for (size_t i = 0; i < vec->count; i++) { 26 | free(vec->buf[i].id); 27 | free(vec->buf[i].name); 28 | free(vec->buf[i].path); 29 | free(vec->buf[i].keywords); 30 | } 31 | free(vec->buf); 32 | } 33 | 34 | void desktop_vec_add( 35 | struct desktop_vec *restrict vec, 36 | const char *restrict id, 37 | const char *restrict name, 38 | const char *restrict path, 39 | const char *restrict keywords) 40 | { 41 | if (vec->count == vec->size) { 42 | vec->size *= 2; 43 | vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); 44 | } 45 | vec->buf[vec->count].id = xstrdup(id); 46 | vec->buf[vec->count].name = utf8_normalize(name); 47 | if (vec->buf[vec->count].name == NULL) { 48 | vec->buf[vec->count].name = xstrdup(name); 49 | } 50 | vec->buf[vec->count].path = xstrdup(path); 51 | vec->buf[vec->count].keywords = xstrdup(keywords); 52 | vec->buf[vec->count].search_score = 0; 53 | vec->buf[vec->count].history_score = 0; 54 | vec->count++; 55 | } 56 | 57 | void desktop_vec_add_file(struct desktop_vec *vec, const char *id, const char *path) 58 | { 59 | GKeyFile *file = g_key_file_new(); 60 | if (!g_key_file_load_from_file(file, path, G_KEY_FILE_NONE, NULL)) { 61 | log_error("Failed to open %s.\n", path); 62 | return; 63 | } 64 | 65 | const char *group = "Desktop Entry"; 66 | 67 | if (g_key_file_get_boolean(file, group, "Hidden", NULL) 68 | || g_key_file_get_boolean(file, group, "NoDisplay", NULL)) { 69 | goto cleanup_file; 70 | } 71 | 72 | char *name = g_key_file_get_locale_string(file, group, "Name", NULL, NULL); 73 | if (name == NULL) { 74 | log_error("%s: No name found.\n", path); 75 | goto cleanup_file; 76 | } 77 | 78 | /* 79 | * This is really a list rather than a string, but for the purposes of 80 | * matching against user input it's easier to just keep it as a string. 81 | */ 82 | char *keywords = g_key_file_get_locale_string(file, group, "Keywords", NULL, NULL); 83 | if (keywords == NULL) { 84 | keywords = xmalloc(1); 85 | *keywords = '\0'; 86 | } 87 | 88 | gsize length; 89 | gchar **list = g_key_file_get_string_list(file, group, "OnlyShowIn", &length, NULL); 90 | if (list) { 91 | bool match = match_current_desktop(list, length); 92 | g_strfreev(list); 93 | list = NULL; 94 | if (!match) { 95 | goto cleanup_all; 96 | } 97 | } 98 | 99 | list = g_key_file_get_string_list(file, group, "NotShowIn", &length, NULL); 100 | if (list) { 101 | bool match = match_current_desktop(list, length); 102 | g_strfreev(list); 103 | list = NULL; 104 | if (match) { 105 | goto cleanup_all; 106 | } 107 | } 108 | 109 | desktop_vec_add(vec, id, name, path, keywords); 110 | 111 | cleanup_all: 112 | free(keywords); 113 | free(name); 114 | cleanup_file: 115 | g_key_file_unref(file); 116 | } 117 | 118 | static int cmpdesktopp(const void *restrict a, const void *restrict b) 119 | { 120 | struct desktop_entry *restrict d1 = (struct desktop_entry *)a; 121 | struct desktop_entry *restrict d2 = (struct desktop_entry *)b; 122 | return strcmp(d1->name, d2->name); 123 | } 124 | 125 | static int cmpscorep(const void *restrict a, const void *restrict b) 126 | { 127 | struct scored_string *restrict str1 = (struct scored_string *)a; 128 | struct scored_string *restrict str2 = (struct scored_string *)b; 129 | 130 | int hist_diff = str2->history_score - str1->history_score; 131 | int search_diff = str2->search_score - str1->search_score; 132 | return hist_diff + search_diff; 133 | } 134 | 135 | void desktop_vec_sort(struct desktop_vec *restrict vec) 136 | { 137 | qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmpdesktopp); 138 | } 139 | 140 | struct desktop_entry *desktop_vec_find_sorted(struct desktop_vec *restrict vec, const char *name) 141 | { 142 | /* 143 | * Explicitly cast away const-ness, as even though we won't modify the 144 | * name, the compiler rightly complains that we might. 145 | */ 146 | struct desktop_entry tmp = { .name = (char *)name }; 147 | return bsearch(&tmp, vec->buf, vec->count, sizeof(vec->buf[0]), cmpdesktopp); 148 | } 149 | 150 | struct string_ref_vec desktop_vec_filter( 151 | const struct desktop_vec *restrict vec, 152 | const char *restrict substr, 153 | enum matching_algorithm algorithm) 154 | { 155 | struct string_ref_vec filt = string_ref_vec_create(); 156 | for (size_t i = 0; i < vec->count; i++) { 157 | int32_t search_score; 158 | search_score = match_words(algorithm, substr, vec->buf[i].name); 159 | if (search_score != INT32_MIN) { 160 | string_ref_vec_add(&filt, vec->buf[i].name); 161 | /* Store the score of the match for later sorting. */ 162 | filt.buf[filt.count - 1].search_score = search_score; 163 | filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; 164 | } else { 165 | /* If we didn't match the name, check the keywords. */ 166 | search_score = match_words(algorithm, substr, vec->buf[i].keywords); 167 | if (search_score != INT32_MIN) { 168 | string_ref_vec_add(&filt, vec->buf[i].name); 169 | /* 170 | * Arbitrary score addition to make name 171 | * matches preferred over keyword matches. 172 | */ 173 | filt.buf[filt.count - 1].search_score = search_score - 20; 174 | filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; 175 | } 176 | } 177 | } 178 | /* 179 | * Sort the results by this search_score. This moves matches at the beginnings 180 | * of words to the front of the result list. 181 | */ 182 | qsort(filt.buf, filt.count, sizeof(filt.buf[0]), cmpscorep); 183 | return filt; 184 | } 185 | 186 | struct desktop_vec desktop_vec_load(FILE *file) 187 | { 188 | struct desktop_vec vec = desktop_vec_create(); 189 | if (file == NULL) { 190 | return vec; 191 | } 192 | 193 | ssize_t bytes_read; 194 | char *line = NULL; 195 | size_t len; 196 | while ((bytes_read = getline(&line, &len, file)) != -1) { 197 | if (line[bytes_read - 1] == '\n') { 198 | line[bytes_read - 1] = '\0'; 199 | } 200 | char *id = line; 201 | size_t sublen = strlen(line); 202 | char *name = &line[sublen + 1]; 203 | sublen = strlen(name); 204 | char *path = &name[sublen + 1]; 205 | sublen = strlen(path); 206 | char *keywords = &path[sublen + 1]; 207 | desktop_vec_add(&vec, id, name, path, keywords); 208 | } 209 | free(line); 210 | 211 | return vec; 212 | } 213 | 214 | void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file) 215 | { 216 | /* 217 | * Using null bytes for field separators is a bit odd, but it makes 218 | * parsing very quick and easy. 219 | */ 220 | for (size_t i = 0; i < vec->count; i++) { 221 | fputs(vec->buf[i].id, file); 222 | fputc('\0', file); 223 | fputs(vec->buf[i].name, file); 224 | fputc('\0', file); 225 | fputs(vec->buf[i].path, file); 226 | fputc('\0', file); 227 | fputs(vec->buf[i].keywords, file); 228 | fputc('\n', file); 229 | } 230 | } 231 | 232 | bool match_current_desktop(char * const *desktop_list, gsize length) 233 | { 234 | const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); 235 | if (xdg_current_desktop == NULL) { 236 | return false; 237 | } 238 | 239 | struct string_vec desktops = string_vec_create(); 240 | 241 | char *saveptr = NULL; 242 | char *tmp = xstrdup(xdg_current_desktop); 243 | char *desktop = strtok_r(tmp, ":", &saveptr); 244 | while (desktop != NULL) { 245 | string_vec_add(&desktops, desktop); 246 | desktop = strtok_r(NULL, ":", &saveptr); 247 | } 248 | 249 | string_vec_sort(&desktops); 250 | for (gsize i = 0; i < length; i++) { 251 | if (string_vec_find_sorted(&desktops, desktop_list[i])) { 252 | return true; 253 | } 254 | } 255 | 256 | string_vec_destroy(&desktops); 257 | free(tmp); 258 | return false; 259 | } 260 | 261 | /* 262 | * Checking-in commented-out code is generally bad practice, but this may be 263 | * needed in the near future. Using the various GKeyFile functions above 264 | * ensures correct behaviour, but is relatively slow (~3-4 ms for 60 desktop 265 | * files). Below are some quick and dirty replacement functions, which work 266 | * correctly except for name localisation, and are ~4x faster. If we go a while 267 | * without needing these, they should be deleted. 268 | */ 269 | 270 | // static char *strip(const char *str) 271 | // { 272 | // size_t start = 0; 273 | // size_t end = strlen(str); 274 | // while (start <= end && isspace(str[start])) { 275 | // start++; 276 | // } 277 | // if (start == end) { 278 | // return NULL; 279 | // } 280 | // while (end > start && (isspace(str[end]) || str[end] == '\0')) { 281 | // end--; 282 | // } 283 | // if (end < start) { 284 | // return NULL; 285 | // } 286 | // if (str[start] == '"' && str[end] == '"' && end > start) { 287 | // start++; 288 | // end--; 289 | // } 290 | // size_t len = end - start + 1; 291 | // char *buf = xcalloc(len + 1, 1); 292 | // strncpy(buf, str + start, len); 293 | // buf[len] = '\0'; 294 | // return buf; 295 | // } 296 | // 297 | // static char *get_option(const char *line) 298 | // { 299 | // size_t index = 0; 300 | // while (line[index] != '=' && index < strlen(line)) { 301 | // index++; 302 | // } 303 | // if (index >= strlen(line)) { 304 | // return NULL; 305 | // } 306 | // index++; 307 | // while (isspace(line[index]) && index < strlen(line)) { 308 | // index++; 309 | // } 310 | // if (index >= strlen(line)) { 311 | // return NULL; 312 | // } 313 | // return strip(&line[index]); 314 | // } 315 | // static bool match_current_desktop2(const char *desktop_list) 316 | // { 317 | // const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); 318 | // if (xdg_current_desktop == NULL) { 319 | // return false; 320 | // } 321 | // 322 | // struct string_vec desktops = string_vec_create(); 323 | // 324 | // char *saveptr = NULL; 325 | // char *tmp = xstrdup(xdg_current_desktop); 326 | // char *desktop = strtok_r(tmp, ":", &saveptr); 327 | // while (desktop != NULL) { 328 | // string_vec_add(&desktops, desktop); 329 | // desktop = strtok_r(NULL, ":", &saveptr); 330 | // } 331 | // free(tmp); 332 | // 333 | // /* 334 | // * Technically this will fail if the desktop list contains an escaped 335 | // * \;, but I don't know of any desktops with semicolons in their names. 336 | // */ 337 | // saveptr = NULL; 338 | // tmp = xstrdup(desktop_list); 339 | // desktop = strtok_r(tmp, ";", &saveptr); 340 | // while (desktop != NULL) { 341 | // if (string_vec_find_sorted(&desktops, desktop)) { 342 | // return true; 343 | // } 344 | // desktop = strtok_r(NULL, ";", &saveptr); 345 | // } 346 | // free(tmp); 347 | // 348 | // string_vec_destroy(&desktops); 349 | // return false; 350 | // } 351 | // 352 | // static void desktop_vec_add_file2(struct desktop_vec *desktop, const char *id, const char *path) 353 | // { 354 | // FILE *file = fopen(path, "rb"); 355 | // if (!file) { 356 | // log_error("Failed to open %s.\n", path); 357 | // return; 358 | // } 359 | // 360 | // char *line = NULL; 361 | // size_t len; 362 | // bool found = false; 363 | // while(getline(&line, &len, file) > 0) { 364 | // if (!strncmp(line, "[Desktop Entry]", strlen("[Desktop Entry]"))) { 365 | // found = true; 366 | // break; 367 | // } 368 | // } 369 | // if (!found) { 370 | // log_error("%s: No [Desktop Entry] section found.\n", path); 371 | // goto cleanup_file; 372 | // } 373 | // 374 | // /* Please forgive the macro usage. */ 375 | // #define OPTION(key) (!strncmp(line, (key), strlen((key)))) 376 | // char *name = NULL; 377 | // found = false; 378 | // while(getline(&line, &len, file) > 0) { 379 | // /* We've left the [Desktop Entry] section, stop parsing. */ 380 | // if (line[0] == '[') { 381 | // break; 382 | // } 383 | // if (OPTION("Name")) { 384 | // if (line[4] == ' ' || line[4] == '=') { 385 | // found = true; 386 | // name = get_option(line); 387 | // } 388 | // } else if (OPTION("Hidden") 389 | // || OPTION("NoDisplay")) { 390 | // char *option = get_option(line); 391 | // if (option != NULL) { 392 | // bool match = !strcmp(option, "true"); 393 | // free(option); 394 | // if (match) { 395 | // goto cleanup_file; 396 | // } 397 | // } 398 | // } else if (OPTION("OnlyShowIn")) { 399 | // char *option = get_option(line); 400 | // if (option != NULL) { 401 | // bool match = match_current_desktop2(option); 402 | // free(option); 403 | // if (!match) { 404 | // goto cleanup_file; 405 | // } 406 | // } 407 | // } else if (OPTION("NotShowIn")) { 408 | // char *option = get_option(line); 409 | // if (option != NULL) { 410 | // bool match = match_current_desktop2(option); 411 | // free(option); 412 | // if (match) { 413 | // goto cleanup_file; 414 | // } 415 | // } 416 | // } 417 | // } 418 | // if (!found) { 419 | // log_error("%s: No name found.\n", path); 420 | // goto cleanup_name; 421 | // } 422 | // if (name == NULL) { 423 | // log_error("%s: Malformed name key.\n", path); 424 | // goto cleanup_file; 425 | // } 426 | // 427 | // desktop_vec_add(desktop, id, name, path); 428 | // 429 | // cleanup_name: 430 | // free(name); 431 | // cleanup_file: 432 | // free(line); 433 | // fclose(file); 434 | // } 435 | -------------------------------------------------------------------------------- /src/desktop_vec.h: -------------------------------------------------------------------------------- 1 | #ifndef DESKTOP_VEC_H 2 | #define DESKTOP_VEC_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "matching.h" 9 | 10 | struct desktop_entry { 11 | char *id; 12 | char *name; 13 | char *path; 14 | char *keywords; 15 | uint32_t search_score; 16 | uint32_t history_score; 17 | }; 18 | 19 | struct desktop_vec { 20 | size_t count; 21 | size_t size; 22 | struct desktop_entry *buf; 23 | }; 24 | 25 | [[nodiscard("memory leaked")]] 26 | struct desktop_vec desktop_vec_create(void); 27 | void desktop_vec_destroy(struct desktop_vec *restrict vec); 28 | void desktop_vec_add( 29 | struct desktop_vec *restrict vec, 30 | const char *restrict id, 31 | const char *restrict name, 32 | const char *restrict path, 33 | const char *restrict keywords); 34 | void desktop_vec_add_file(struct desktop_vec *desktop, const char *id, const char *path); 35 | 36 | void desktop_vec_sort(struct desktop_vec *restrict vec); 37 | struct desktop_entry *desktop_vec_find_sorted(struct desktop_vec *restrict vec, const char *name); 38 | struct string_ref_vec desktop_vec_filter( 39 | const struct desktop_vec *restrict vec, 40 | const char *restrict substr, 41 | enum matching_algorithm algorithm); 42 | 43 | struct desktop_vec desktop_vec_load(FILE *file); 44 | void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file); 45 | 46 | 47 | #endif /* DESKTOP_VEC_H */ 48 | -------------------------------------------------------------------------------- /src/drun.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "drun.h" 13 | #include "history.h" 14 | #include "log.h" 15 | #include "mkdirp.h" 16 | #include "string_vec.h" 17 | #include "xmalloc.h" 18 | 19 | static const char *default_data_dir = ".local/share/"; 20 | static const char *default_cache_dir = ".cache/"; 21 | static const char *cache_basename = "tofi-drun"; 22 | 23 | [[nodiscard("memory leaked")]] 24 | static char *get_cache_path() { 25 | char *cache_name = NULL; 26 | const char *state_path = getenv("XDG_CACHE_HOME"); 27 | if (state_path == NULL) { 28 | const char *home = getenv("HOME"); 29 | if (home == NULL) { 30 | log_error("Couldn't retrieve HOME from environment.\n"); 31 | return NULL; 32 | } 33 | size_t len = strlen(home) + 1 34 | + strlen(default_cache_dir) + 1 35 | + strlen(cache_basename) + 1; 36 | cache_name = xmalloc(len); 37 | snprintf( 38 | cache_name, 39 | len, 40 | "%s/%s/%s", 41 | home, 42 | default_cache_dir, 43 | cache_basename); 44 | } else { 45 | size_t len = strlen(state_path) + 1 46 | + strlen(cache_basename) + 1; 47 | cache_name = xmalloc(len); 48 | snprintf( 49 | cache_name, 50 | len, 51 | "%s/%s", 52 | state_path, 53 | cache_basename); 54 | } 55 | return cache_name; 56 | } 57 | 58 | [[nodiscard("memory leaked")]] 59 | static struct string_vec get_application_paths() { 60 | char *base_paths = NULL; 61 | const char *xdg_data_dirs = getenv("XDG_DATA_DIRS"); 62 | if (xdg_data_dirs == NULL) { 63 | xdg_data_dirs = "/usr/local/share/:/usr/share/"; 64 | } 65 | const char *xdg_data_home = getenv("XDG_DATA_HOME"); 66 | if (xdg_data_home == NULL) { 67 | const char *home = getenv("HOME"); 68 | if (home == NULL) { 69 | log_error("Couldn't retrieve HOME from environment.\n"); 70 | exit(EXIT_FAILURE); 71 | } 72 | size_t len = strlen(home) + 1 73 | + strlen(default_data_dir) + 1 74 | + strlen(xdg_data_dirs) + 1; 75 | base_paths = xmalloc(len); 76 | snprintf( 77 | base_paths, 78 | len, 79 | "%s/%s:%s", 80 | home, 81 | default_data_dir, 82 | xdg_data_dirs); 83 | } else { 84 | size_t len = strlen(xdg_data_home) + 1 85 | + strlen(xdg_data_dirs) + 1; 86 | base_paths = xmalloc(len); 87 | snprintf( 88 | base_paths, 89 | len, 90 | "%s:%s", 91 | xdg_data_home, 92 | xdg_data_dirs); 93 | } 94 | 95 | 96 | /* Append /applications/ to each entry. */ 97 | struct string_vec paths = string_vec_create(); 98 | char *saveptr = NULL; 99 | char *path_entry = strtok_r(base_paths, ":", &saveptr); 100 | while (path_entry != NULL) { 101 | const char *subdir = "applications/"; 102 | size_t len = strlen(path_entry) + 1 + strlen(subdir) + 1; 103 | char *apps = xmalloc(len); 104 | snprintf(apps, len, "%s/%s", path_entry, subdir); 105 | string_vec_add(&paths, apps); 106 | free(apps); 107 | path_entry = strtok_r(NULL, ":", &saveptr); 108 | } 109 | free(base_paths); 110 | 111 | return paths; 112 | } 113 | 114 | static void parse_desktop_file(gpointer key, gpointer value, void *data) 115 | { 116 | const char *id = key; 117 | const char *path = value; 118 | struct desktop_vec *apps = data; 119 | 120 | desktop_vec_add_file(apps, id, path); 121 | } 122 | 123 | struct desktop_vec drun_generate(void) 124 | { 125 | /* 126 | * Note for the future: this custom logic could be replaced with 127 | * g_app_info_get_all(), but that's slower. Worth remembering 128 | * though if this runs into issues. 129 | */ 130 | log_debug("Retrieving application dirs.\n"); 131 | struct string_vec paths = get_application_paths(); 132 | struct string_vec desktop_files = string_vec_create(); 133 | log_debug("Scanning for .desktop files.\n"); 134 | for (size_t i = 0; i < paths.count; i++) { 135 | const char *path_entry = paths.buf[i].string; 136 | DIR *dir = opendir(path_entry); 137 | if (dir != NULL) { 138 | struct dirent *d; 139 | while ((d = readdir(dir)) != NULL) { 140 | const char *extension = strrchr(d->d_name, '.'); 141 | if (extension == NULL) { 142 | continue; 143 | } 144 | if (strcmp(extension, ".desktop")) { 145 | continue; 146 | } 147 | string_vec_add(&desktop_files, d->d_name); 148 | } 149 | closedir(dir); 150 | } 151 | } 152 | log_debug("Found %zu files.\n", desktop_files.count); 153 | 154 | 155 | log_debug("Parsing .desktop files.\n"); 156 | /* 157 | * The Desktop Entry Specification says that only the highest 158 | * precedence application file with a given ID should be used, so store 159 | * the id / path pairs into a hash table to enforce uniqueness. 160 | */ 161 | GHashTable *id_hash = g_hash_table_new_full(g_str_hash, g_str_equal, free, free); 162 | struct desktop_vec apps = desktop_vec_create(); 163 | for (size_t i = 0; i < paths.count; i++) { 164 | char *path_entry = paths.buf[i].string; 165 | char *tree[2] = { path_entry, NULL }; 166 | size_t prefix_len = strlen(path_entry); 167 | FTS *fts = fts_open(tree, FTS_LOGICAL, NULL); 168 | FTSENT *entry = fts_read(fts); 169 | for (; entry != NULL; entry = fts_read(fts)) { 170 | const char *extension = strrchr(entry->fts_name, '.'); 171 | if (extension == NULL) { 172 | continue; 173 | } 174 | if (strcmp(extension, ".desktop")) { 175 | continue; 176 | } 177 | char *id = xstrdup(&entry->fts_path[prefix_len]); 178 | char *slash = strchr(id, '/'); 179 | while (slash != NULL) { 180 | *slash = '-'; 181 | slash = strchr(slash, '/'); 182 | } 183 | /* 184 | * We're iterating from highest to lowest precedence, 185 | * so only the first file with a given ID should be 186 | * stored. 187 | */ 188 | if (!g_hash_table_contains(id_hash, id)) { 189 | char *path = xstrdup(entry->fts_path); 190 | g_hash_table_insert(id_hash, id, path); 191 | } else { 192 | free(id); 193 | } 194 | 195 | } 196 | fts_close(fts); 197 | } 198 | 199 | /* Parse the remaining files into our desktop_vec. */ 200 | g_hash_table_foreach(id_hash, parse_desktop_file, &apps); 201 | g_hash_table_unref(id_hash); 202 | 203 | log_debug("Found %zu apps.\n", apps.count); 204 | 205 | /* 206 | * It's now safe to sort the desktop file vector, as the rules about 207 | * file precedence have been taken care of. 208 | */ 209 | log_debug("Sorting results.\n"); 210 | desktop_vec_sort(&apps); 211 | 212 | string_vec_destroy(&desktop_files); 213 | string_vec_destroy(&paths); 214 | return apps; 215 | } 216 | 217 | struct desktop_vec drun_generate_cached() 218 | { 219 | log_debug("Retrieving cache location.\n"); 220 | char *cache_path = get_cache_path(); 221 | 222 | struct stat sb; 223 | if (cache_path == NULL) { 224 | return drun_generate(); 225 | } 226 | 227 | /* If the cache doesn't exist, create it and return */ 228 | errno = 0; 229 | if (stat(cache_path, &sb) == -1) { 230 | if (errno == ENOENT) { 231 | struct desktop_vec apps = drun_generate(); 232 | if (!mkdirp(cache_path)) { 233 | free(cache_path); 234 | return apps; 235 | } 236 | errno = 0; 237 | FILE *cache = fopen(cache_path, "wb"); 238 | if (cache == NULL) { 239 | log_error("Error creating drun cache: %s.\n", strerror(errno)); 240 | free(cache_path); 241 | return apps; 242 | } 243 | desktop_vec_save(&apps, cache); 244 | fclose(cache); 245 | free(cache_path); 246 | return apps; 247 | } 248 | free(cache_path); 249 | return drun_generate(); 250 | } 251 | 252 | log_debug("Retrieving application dirs.\n"); 253 | struct string_vec application_path = get_application_paths();; 254 | 255 | /* The cache exists, so check if it's still in date */ 256 | bool out_of_date = false; 257 | for (size_t i = 0; i < application_path.count; i++) { 258 | struct stat path_sb; 259 | if (stat(application_path.buf[i].string, &path_sb) == 0) { 260 | if (path_sb.st_mtim.tv_sec > sb.st_mtim.tv_sec) { 261 | out_of_date = true; 262 | break; 263 | } 264 | } 265 | } 266 | string_vec_destroy(&application_path); 267 | 268 | struct desktop_vec apps; 269 | if (out_of_date) { 270 | log_debug("Cache out of date, updating.\n"); 271 | log_indent(); 272 | apps = drun_generate(); 273 | log_unindent(); 274 | errno = 0; 275 | FILE *cache = fopen(cache_path, "wb"); 276 | if (cache == NULL) { 277 | log_error("Failed to update cache: %s.\n", strerror(errno)); 278 | } else { 279 | desktop_vec_save(&apps, cache); 280 | fclose(cache); 281 | } 282 | } else { 283 | log_debug("Cache up to date, loading.\n"); 284 | errno = 0; 285 | FILE *cache = fopen(cache_path, "rb"); 286 | if (cache == NULL) { 287 | log_error("Failed to load cache: %s.\n", strerror(errno)); 288 | log_indent(); 289 | apps = drun_generate(); 290 | log_unindent(); 291 | } else { 292 | apps = desktop_vec_load(cache); 293 | fclose(cache); 294 | } 295 | } 296 | free(cache_path); 297 | return apps; 298 | } 299 | 300 | void drun_print(const char *filename, const char *terminal_command) 301 | { 302 | GKeyFile *file = g_key_file_new(); 303 | if (!g_key_file_load_from_file(file, filename, G_KEY_FILE_NONE, NULL)) { 304 | log_error("Failed to open %s.\n", filename); 305 | return; 306 | } 307 | const char *group = "Desktop Entry"; 308 | 309 | char *exec = g_key_file_get_string(file, group, "Exec", NULL); 310 | if (exec == NULL) { 311 | log_error("Failed to get Exec key from %s.\n", filename); 312 | g_key_file_unref(file); 313 | return; 314 | } 315 | 316 | /* 317 | * Build a string vector from the command line, replacing % field codes 318 | * with the appropriate values. 319 | */ 320 | struct string_vec pieces = string_vec_create(); 321 | char *search = exec; 322 | char *last = search; 323 | while ((search = strchr(search, '%')) != NULL) { 324 | /* Add the string up to here to our vector. */ 325 | search[0] = '\0'; 326 | string_vec_add(&pieces, last); 327 | 328 | switch (search[1]) { 329 | case 'i': 330 | if (g_key_file_has_key(file, group, "Icon", NULL)) { 331 | string_vec_add(&pieces, "--icon "); 332 | string_vec_add(&pieces, g_key_file_get_string(file, group, "Icon", NULL)); 333 | } 334 | break; 335 | case 'c': 336 | string_vec_add(&pieces, g_key_file_get_locale_string(file, group, "Name", NULL, NULL)); 337 | break; 338 | case 'k': 339 | string_vec_add(&pieces, filename); 340 | break; 341 | } 342 | 343 | search += 2; 344 | last = search; 345 | } 346 | string_vec_add(&pieces, last); 347 | 348 | /* 349 | * If this is a terminal application, the command line needs to be 350 | * preceded by the terminal command. 351 | */ 352 | bool terminal = g_key_file_get_boolean(file, group, "Terminal", NULL); 353 | if (terminal) { 354 | if (terminal_command[0] == '\0') { 355 | log_warning("Terminal application launched, but no terminal is set.\n"); 356 | log_warning("This probably isn't what you want.\n"); 357 | log_warning("See the --terminal option documentation in the man page.\n"); 358 | } else { 359 | fputs(terminal_command, stdout); 360 | fputc(' ', stdout); 361 | } 362 | } 363 | 364 | /* Build the command line from our vector. */ 365 | for (size_t i = 0; i < pieces.count; i++) { 366 | fputs(pieces.buf[i].string, stdout); 367 | } 368 | fputc('\n', stdout); 369 | 370 | string_vec_destroy(&pieces); 371 | free(exec); 372 | g_key_file_unref(file); 373 | } 374 | 375 | void drun_launch(const char *filename) 376 | { 377 | GDesktopAppInfo *info = g_desktop_app_info_new_from_filename(filename); 378 | GAppLaunchContext *context = g_app_launch_context_new(); 379 | GError *err = NULL; 380 | 381 | if (!g_app_info_launch((GAppInfo *)info, NULL, context, &err)) { 382 | log_error("Failed to launch %s.\n", filename); 383 | log_error("%s.\n", err->message); 384 | log_error( 385 | "If this is a terminal issue, you can use `--drun-launch=false`,\n" 386 | " and pass your preferred terminal command to `--terminal`.\n" 387 | " For more information, see https://gitlab.gnome.org/GNOME/glib/-/issues/338\n" 388 | " and https://github.com/philj56/tofi/issues/46.\n"); 389 | } 390 | 391 | g_clear_error(&err); 392 | g_object_unref(context); 393 | g_object_unref(info); 394 | } 395 | 396 | static int cmpscorep(const void *restrict a, const void *restrict b) 397 | { 398 | struct desktop_entry *restrict app1 = (struct desktop_entry *)a; 399 | struct desktop_entry *restrict app2 = (struct desktop_entry *)b; 400 | return app2->history_score - app1->history_score; 401 | } 402 | 403 | void drun_history_sort(struct desktop_vec *apps, struct history *history) 404 | { 405 | log_debug("Moving already known apps to the front.\n"); 406 | for (size_t i = 0; i < history->count; i++) { 407 | struct desktop_entry *res = desktop_vec_find_sorted(apps, history->buf[i].name); 408 | if (res == NULL) { 409 | continue; 410 | } 411 | res->history_score = history->buf[i].run_count; 412 | } 413 | qsort(apps->buf, apps->count, sizeof(apps->buf[0]), cmpscorep); 414 | } 415 | -------------------------------------------------------------------------------- /src/drun.h: -------------------------------------------------------------------------------- 1 | #ifndef DRUN_H 2 | #define DRUN_H 3 | 4 | #include "desktop_vec.h" 5 | #include "history.h" 6 | #include "string_vec.h" 7 | 8 | struct desktop_vec drun_generate(void); 9 | struct desktop_vec drun_generate_cached(void); 10 | void drun_history_sort(struct desktop_vec *apps, struct history *history); 11 | void drun_print(const char *filename, const char *terminal_command); 12 | void drun_launch(const char *filename); 13 | 14 | #endif /* DRUN_H */ 15 | -------------------------------------------------------------------------------- /src/entry.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "entry.h" 5 | #include "log.h" 6 | #include "nelem.h" 7 | #include "scale.h" 8 | 9 | #undef MAX 10 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 11 | 12 | static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint32_t r) 13 | { 14 | cairo_new_path(cr); 15 | 16 | /* Top-left */ 17 | cairo_arc(cr, r, r, r, -M_PI, -M_PI_2); 18 | 19 | /* Top-right */ 20 | cairo_arc(cr, width - r, r, r, -M_PI_2, 0); 21 | 22 | /* Bottom-right */ 23 | cairo_arc(cr, width - r, height - r, r, 0, M_PI_2); 24 | 25 | /* Bottom-left */ 26 | cairo_arc(cr, r, height - r, r, M_PI_2, M_PI); 27 | 28 | cairo_close_path(cr); 29 | } 30 | 31 | static void apply_text_theme_fallback(struct text_theme *theme, const struct text_theme *fallback) 32 | { 33 | if (!theme->foreground_specified) { 34 | theme->foreground_color = fallback->foreground_color; 35 | } 36 | if (!theme->background_specified) { 37 | theme->background_color = fallback->background_color; 38 | } 39 | if (!theme->padding_specified) { 40 | theme->padding = fallback->padding; 41 | } 42 | if (!theme->radius_specified) { 43 | theme->background_corner_radius = fallback->background_corner_radius; 44 | } 45 | } 46 | 47 | void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height, uint32_t fractional_scale_numerator) 48 | { 49 | double scale = fractional_scale_numerator / 120.; 50 | /* 51 | * Create the cairo surfaces and contexts we'll be using. 52 | * 53 | * In order to avoid an unnecessary copy when passing the image to the 54 | * Wayland server, we accept a pointer to the mmap-ed file that our 55 | * Wayland buffers are created from. This is assumed to be 56 | * (width * height * (sizeof(uint32_t) == 4) * 2) bytes, 57 | * to allow for double buffering. 58 | */ 59 | log_debug("Creating %u x %u Cairo surface with scale factor %.3lf.\n", 60 | width, 61 | height, 62 | fractional_scale_numerator / 120.); 63 | cairo_surface_t *surface = cairo_image_surface_create_for_data( 64 | buffer, 65 | CAIRO_FORMAT_ARGB32, 66 | width, 67 | height, 68 | width * sizeof(uint32_t) 69 | ); 70 | cairo_surface_set_device_scale(surface, scale, scale); 71 | cairo_t *cr = cairo_create(surface); 72 | 73 | entry->cairo[0].surface = surface; 74 | entry->cairo[0].cr = cr; 75 | 76 | entry->cairo[1].surface = cairo_image_surface_create_for_data( 77 | &buffer[width * height * sizeof(uint32_t)], 78 | CAIRO_FORMAT_ARGB32, 79 | width, 80 | height, 81 | width * sizeof(uint32_t) 82 | ); 83 | cairo_surface_set_device_scale(entry->cairo[1].surface, scale, scale); 84 | entry->cairo[1].cr = cairo_create(entry->cairo[1].surface); 85 | 86 | /* If we're scaling with Cairo, remember to account for that here. */ 87 | width = scale_apply_inverse(width, fractional_scale_numerator); 88 | height = scale_apply_inverse(height, fractional_scale_numerator); 89 | 90 | log_debug("Drawing window.\n"); 91 | /* Draw the background */ 92 | struct color color = entry->background_color; 93 | cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); 94 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); 95 | cairo_paint(cr); 96 | 97 | /* Draw the border with outlines */ 98 | cairo_set_line_width(cr, 4 * entry->outline_width + 2 * entry->border_width); 99 | rounded_rectangle(cr, width, height, entry->corner_radius); 100 | 101 | color = entry->outline_color; 102 | cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); 103 | cairo_stroke_preserve(cr); 104 | 105 | color = entry->border_color; 106 | cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); 107 | cairo_set_line_width(cr, 2 * entry->outline_width + 2 * entry->border_width); 108 | cairo_stroke_preserve(cr); 109 | 110 | color = entry->outline_color; 111 | cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); 112 | cairo_set_line_width(cr, 2 * entry->outline_width); 113 | cairo_stroke_preserve(cr); 114 | 115 | /* Clear the overdrawn bits outside of the rounded corners */ 116 | /* 117 | * N.B. the +1's shouldn't be required, but certain fractional scale 118 | * factors can otherwise cause 1-pixel artifacts on the edges 119 | * (presumably because Cairo is performing rounding differently to us 120 | * at some point). 121 | */ 122 | cairo_rectangle(cr, 0, 0, width + 1, height + 1); 123 | cairo_set_source_rgba(cr, 0, 0, 0, 1); 124 | cairo_save(cr); 125 | cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); 126 | cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR); 127 | cairo_fill(cr); 128 | cairo_restore(cr); 129 | 130 | cairo_set_operator(cr, CAIRO_OPERATOR_OVER); 131 | 132 | 133 | /* Move and clip following draws to be within this outline */ 134 | double dx = 2.0 * entry->outline_width + entry->border_width; 135 | cairo_translate(cr, dx, dx); 136 | width -= 2 * dx; 137 | height -= 2 * dx; 138 | 139 | /* If we're clipping to the padding, account for that as well here */ 140 | if (entry->clip_to_padding) { 141 | cairo_translate(cr, entry->padding_left, entry->padding_top); 142 | width -= entry->padding_left + entry->padding_right; 143 | height -= entry->padding_top + entry->padding_bottom; 144 | } 145 | 146 | /* Account for rounded corners */ 147 | double inner_radius = (double)entry->corner_radius - dx; 148 | inner_radius = MAX(inner_radius, 0); 149 | 150 | dx = ceil(inner_radius * (1.0 - 1.0 / M_SQRT2)); 151 | cairo_translate(cr, dx, dx); 152 | width -= 2 * dx; 153 | height -= 2 * dx; 154 | cairo_rectangle(cr, 0, 0, width, height); 155 | cairo_clip(cr); 156 | 157 | /* Store the clip rectangle width and height. */ 158 | cairo_matrix_t mat; 159 | cairo_get_matrix(cr, &mat); 160 | entry->clip_x = mat.x0; 161 | entry->clip_y = mat.y0; 162 | entry->clip_width = width; 163 | entry->clip_height = height; 164 | 165 | /* 166 | * If we're not clipping to the padding, we didn't account for it 167 | * before. 168 | */ 169 | if (!entry->clip_to_padding) { 170 | cairo_translate(cr, entry->padding_left, entry->padding_top); 171 | } 172 | 173 | /* Setup the backend. */ 174 | if (access(entry->font_name, R_OK) != 0) { 175 | /* 176 | * We've been given a font name rather than path, 177 | * so fallback to Pango 178 | */ 179 | entry->use_pango = true; 180 | } 181 | if (entry->use_pango) { 182 | entry_backend_pango_init(entry, &width, &height); 183 | } else { 184 | entry_backend_harfbuzz_init(entry, &width, &height); 185 | } 186 | 187 | /* 188 | * Before we render any text, ensure all text themes are fully 189 | * specified. 190 | */ 191 | const struct text_theme default_theme = { 192 | .foreground_color = entry->foreground_color, 193 | .background_color = (struct color) { .a = 0 }, 194 | .padding = (struct directional) {0}, 195 | .background_corner_radius = 0 196 | }; 197 | 198 | apply_text_theme_fallback(&entry->prompt_theme, &default_theme); 199 | apply_text_theme_fallback(&entry->input_theme, &default_theme); 200 | apply_text_theme_fallback(&entry->placeholder_theme, &default_theme); 201 | apply_text_theme_fallback(&entry->default_result_theme, &default_theme); 202 | apply_text_theme_fallback(&entry->alternate_result_theme, &entry->default_result_theme); 203 | apply_text_theme_fallback(&entry->selection_theme, &default_theme); 204 | 205 | /* The cursor is a special case, as it just needs the input colours. */ 206 | if (!entry->cursor_theme.color_specified) { 207 | entry->cursor_theme.color = entry->input_theme.foreground_color; 208 | } 209 | if (!entry->cursor_theme.text_color_specified) { 210 | entry->cursor_theme.text_color = entry->background_color; 211 | } 212 | 213 | /* 214 | * Perform an initial render of the text. 215 | * This is done here rather than by calling entry_update to avoid the 216 | * unnecessary cairo_paint() of the background for the first frame, 217 | * which can be slow for large (e.g. fullscreen) windows. 218 | */ 219 | log_debug("Initial text render.\n"); 220 | if (entry->use_pango) { 221 | entry_backend_pango_update(entry); 222 | } else { 223 | entry_backend_harfbuzz_update(entry); 224 | } 225 | entry->index = !entry->index; 226 | 227 | /* 228 | * To avoid performing all this drawing twice, we take a small 229 | * shortcut. After performing all the drawing as normal on our first 230 | * Cairo context, we can copy over just the important state (the 231 | * transformation matrix and clip rectangle) and perform a memcpy() 232 | * to initialise the other context. 233 | * 234 | * This memcpy can pretty expensive however, and isn't needed until 235 | * we need to draw our second buffer (i.e. when the user presses a 236 | * key). In order to minimise startup time, the memcpy() isn't 237 | * performed here, but instead happens later, just after the first 238 | * frame has been displayed on screen (and while the user is unlikely 239 | * to press another key for the <10ms it takes to memcpy). 240 | */ 241 | cairo_set_matrix(entry->cairo[1].cr, &mat); 242 | cairo_rectangle(entry->cairo[1].cr, 0, 0, width, height); 243 | cairo_clip(entry->cairo[1].cr); 244 | 245 | /* 246 | * If we're not clipping to the padding, the transformation matrix 247 | * didn't include it, so account for it here. 248 | */ 249 | if (!entry->clip_to_padding) { 250 | cairo_translate(entry->cairo[1].cr, entry->padding_left, entry->padding_top); 251 | } 252 | } 253 | 254 | void entry_destroy(struct entry *entry) 255 | { 256 | if (entry->use_pango) { 257 | entry_backend_pango_destroy(entry); 258 | } else { 259 | entry_backend_harfbuzz_destroy(entry); 260 | } 261 | cairo_destroy(entry->cairo[0].cr); 262 | cairo_destroy(entry->cairo[1].cr); 263 | cairo_surface_destroy(entry->cairo[0].surface); 264 | cairo_surface_destroy(entry->cairo[1].surface); 265 | } 266 | 267 | void entry_update(struct entry *entry) 268 | { 269 | log_debug("Start rendering entry.\n"); 270 | cairo_t *cr = entry->cairo[entry->index].cr; 271 | 272 | /* Clear the image. */ 273 | struct color color = entry->background_color; 274 | cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); 275 | cairo_save(cr); 276 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); 277 | cairo_paint(cr); 278 | cairo_restore(cr); 279 | 280 | /* Draw our text. */ 281 | if (entry->use_pango) { 282 | entry_backend_pango_update(entry); 283 | } else { 284 | entry_backend_harfbuzz_update(entry); 285 | } 286 | 287 | log_debug("Finish rendering entry.\n"); 288 | 289 | entry->index = !entry->index; 290 | } 291 | -------------------------------------------------------------------------------- /src/entry.h: -------------------------------------------------------------------------------- 1 | #ifndef ENTRY_H 2 | #define ENTRY_H 3 | 4 | #include "entry_backend/pango.h" 5 | #include "entry_backend/harfbuzz.h" 6 | 7 | #include 8 | #include 9 | #include "color.h" 10 | #include "desktop_vec.h" 11 | #include "history.h" 12 | #include "surface.h" 13 | #include "string_vec.h" 14 | 15 | #define MAX_INPUT_LENGTH 256 16 | #define MAX_PROMPT_LENGTH 256 17 | #define MAX_FONT_NAME_LENGTH 256 18 | #define MAX_FONT_FEATURES_LENGTH 128 19 | #define MAX_FONT_VARIATIONS_LENGTH 128 20 | 21 | enum tofi_mode { 22 | TOFI_MODE_PLAIN, 23 | TOFI_MODE_RUN, 24 | TOFI_MODE_DRUN 25 | }; 26 | 27 | enum cursor_style { 28 | CURSOR_STYLE_BAR, 29 | CURSOR_STYLE_BLOCK, 30 | CURSOR_STYLE_UNDERSCORE 31 | }; 32 | 33 | struct directional { 34 | int32_t top; 35 | int32_t right; 36 | int32_t bottom; 37 | int32_t left; 38 | }; 39 | 40 | struct text_theme { 41 | struct color foreground_color; 42 | struct color background_color; 43 | struct directional padding; 44 | uint32_t background_corner_radius; 45 | 46 | bool foreground_specified; 47 | bool background_specified; 48 | bool padding_specified; 49 | bool radius_specified; 50 | }; 51 | 52 | struct cursor_theme { 53 | struct color color; 54 | struct color text_color; 55 | enum cursor_style style; 56 | uint32_t corner_radius; 57 | uint32_t thickness; 58 | 59 | double underline_depth; 60 | double em_width; 61 | 62 | bool color_specified; 63 | bool text_color_specified; 64 | bool thickness_specified; 65 | 66 | bool show; 67 | }; 68 | 69 | struct entry { 70 | struct entry_backend_harfbuzz harfbuzz; 71 | struct entry_backend_pango pango; 72 | struct { 73 | cairo_surface_t *surface; 74 | cairo_t *cr; 75 | } cairo[2]; 76 | int index; 77 | 78 | uint32_t input_utf32[MAX_INPUT_LENGTH]; 79 | char input_utf8[4*MAX_INPUT_LENGTH]; 80 | uint32_t input_utf32_length; 81 | uint32_t input_utf8_length; 82 | uint32_t cursor_position; 83 | 84 | uint32_t selection; 85 | uint32_t first_result; 86 | char *command_buffer; 87 | struct string_ref_vec results; 88 | struct string_ref_vec commands; 89 | struct desktop_vec apps; 90 | struct history history; 91 | bool use_pango; 92 | 93 | uint32_t clip_x; 94 | uint32_t clip_y; 95 | uint32_t clip_width; 96 | uint32_t clip_height; 97 | 98 | /* Options */ 99 | enum tofi_mode mode; 100 | bool horizontal; 101 | bool hide_input; 102 | char hidden_character_utf8[6]; 103 | uint8_t hidden_character_utf8_length; 104 | uint32_t num_results; 105 | uint32_t num_results_drawn; 106 | uint32_t last_num_results_drawn; 107 | int32_t result_spacing; 108 | uint32_t font_size; 109 | char font_name[MAX_FONT_NAME_LENGTH]; 110 | char font_features[MAX_FONT_FEATURES_LENGTH]; 111 | char font_variations[MAX_FONT_VARIATIONS_LENGTH]; 112 | char prompt_text[MAX_PROMPT_LENGTH]; 113 | char placeholder_text[MAX_PROMPT_LENGTH]; 114 | uint32_t prompt_padding; 115 | uint32_t corner_radius; 116 | uint32_t padding_top; 117 | uint32_t padding_bottom; 118 | uint32_t padding_left; 119 | uint32_t padding_right; 120 | bool padding_top_is_percent; 121 | bool padding_bottom_is_percent; 122 | bool padding_left_is_percent; 123 | bool padding_right_is_percent; 124 | bool clip_to_padding; 125 | uint32_t input_width; 126 | uint32_t border_width; 127 | uint32_t outline_width; 128 | struct color foreground_color; 129 | struct color background_color; 130 | struct color selection_highlight_color; 131 | struct color border_color; 132 | struct color outline_color; 133 | 134 | struct cursor_theme cursor_theme; 135 | struct text_theme prompt_theme; 136 | struct text_theme input_theme; 137 | struct text_theme placeholder_theme; 138 | struct text_theme default_result_theme; 139 | struct text_theme alternate_result_theme; 140 | struct text_theme selection_theme; 141 | }; 142 | 143 | void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height, uint32_t fractional_scale_numerator); 144 | void entry_destroy(struct entry *entry); 145 | void entry_update(struct entry *entry); 146 | 147 | #endif /* ENTRY_H */ 148 | -------------------------------------------------------------------------------- /src/entry_backend/harfbuzz.h: -------------------------------------------------------------------------------- 1 | #ifndef ENTRY_BACKEND_HARFBUZZ_H 2 | #define ENTRY_BACKEND_HARFBUZZ_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include FT_FREETYPE_H 8 | #include 9 | 10 | #define MAX_FONT_VARIATIONS 16 11 | #define MAX_FONT_FEATURES 16 12 | 13 | struct entry; 14 | 15 | struct entry_backend_harfbuzz { 16 | FT_Library ft_library; 17 | FT_Face ft_face; 18 | 19 | cairo_font_face_t *cairo_face; 20 | cairo_font_extents_t cairo_font_extents; 21 | 22 | hb_font_t *hb_font; 23 | hb_font_extents_t hb_font_extents; 24 | hb_buffer_t *hb_buffer; 25 | hb_variation_t hb_variations[MAX_FONT_VARIATIONS]; 26 | hb_feature_t hb_features[MAX_FONT_FEATURES]; 27 | uint8_t num_variations; 28 | uint8_t num_features; 29 | 30 | double line_spacing; 31 | double scale; 32 | 33 | bool disable_hinting; 34 | }; 35 | 36 | void entry_backend_harfbuzz_init(struct entry *entry, uint32_t *width, uint32_t *height); 37 | void entry_backend_harfbuzz_destroy(struct entry *entry); 38 | void entry_backend_harfbuzz_update(struct entry *entry); 39 | 40 | #endif /* ENTRY_BACKEND_HARFBUZZ_H */ 41 | -------------------------------------------------------------------------------- /src/entry_backend/pango.h: -------------------------------------------------------------------------------- 1 | #ifndef ENTRY_BACKEND_PANGO_H 2 | #define ENTRY_BACKEND_PANGO_H 3 | 4 | #include 5 | 6 | struct entry; 7 | 8 | struct entry_backend_pango { 9 | PangoContext *context; 10 | PangoLayout *layout; 11 | }; 12 | 13 | void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *height); 14 | void entry_backend_pango_destroy(struct entry *entry); 15 | void entry_backend_pango_update(struct entry *entry); 16 | 17 | #endif /* ENTRY_BACKEND_PANGO_H */ 18 | -------------------------------------------------------------------------------- /src/history.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "history.h" 10 | #include "log.h" 11 | #include "mkdirp.h" 12 | #include "xmalloc.h" 13 | 14 | #define MAX_HISTFILE_SIZE (10*1024*1024) 15 | 16 | static const char *default_state_dir = ".local/state"; 17 | static const char *histfile_basename = "tofi-history"; 18 | static const char *drun_histfile_basename = "tofi-drun-history"; 19 | 20 | [[nodiscard("memory leaked")]] 21 | static struct history history_create(void); 22 | 23 | static char *get_histfile_path(bool drun) { 24 | const char *basename; 25 | if (drun) { 26 | basename = drun_histfile_basename; 27 | } else { 28 | basename = histfile_basename; 29 | } 30 | char *histfile_name = NULL; 31 | const char *state_path = getenv("XDG_STATE_HOME"); 32 | if (state_path == NULL) { 33 | const char *home = getenv("HOME"); 34 | if (home == NULL) { 35 | log_error("Couldn't retrieve HOME from environment.\n"); 36 | return NULL; 37 | } 38 | size_t len = strlen(home) + 1 39 | + strlen(default_state_dir) + 1 40 | + strlen(basename) + 1; 41 | histfile_name = xmalloc(len); 42 | snprintf( 43 | histfile_name, 44 | len, 45 | "%s/%s/%s", 46 | home, 47 | default_state_dir, 48 | basename); 49 | } else { 50 | size_t len = strlen(state_path) + 1 51 | + strlen(basename) + 1; 52 | histfile_name = xmalloc(len); 53 | snprintf( 54 | histfile_name, 55 | len, 56 | "%s/%s", 57 | state_path, 58 | basename); 59 | } 60 | return histfile_name; 61 | } 62 | 63 | struct history history_load(const char *path) 64 | { 65 | struct history vec = history_create(); 66 | 67 | FILE *histfile = fopen(path, "rb"); 68 | 69 | if (histfile == NULL) { 70 | return vec; 71 | } 72 | 73 | errno = 0; 74 | if (fseek(histfile, 0, SEEK_END) != 0) { 75 | log_error("Error seeking in history file: %s.\n", strerror(errno)); 76 | fclose(histfile); 77 | return vec; 78 | } 79 | 80 | errno = 0; 81 | size_t len = ftell(histfile); 82 | if (len > MAX_HISTFILE_SIZE) { 83 | log_error("History file too big (> %d MiB)! Are you sure it's a file?\n", MAX_HISTFILE_SIZE / 1024 / 1024); 84 | fclose(histfile); 85 | return vec; 86 | } 87 | 88 | errno = 0; 89 | if (fseek(histfile, 0, SEEK_SET) != 0) { 90 | log_error("Error seeking in history file: %s.\n", strerror(errno)); 91 | fclose(histfile); 92 | return vec; 93 | } 94 | 95 | errno = 0; 96 | char *buf = xmalloc(len + 1); 97 | if (fread(buf, 1, len, histfile) != len) { 98 | log_error("Error reading history file: %s.\n", strerror(errno)); 99 | fclose(histfile); 100 | return vec; 101 | } 102 | fclose(histfile); 103 | buf[len] = '\0'; 104 | 105 | char *saveptr = NULL; 106 | char *tok = strtok_r(buf, " ", &saveptr); 107 | while (tok != NULL) { 108 | size_t run_count = strtoull(tok, NULL, 10); 109 | tok = strtok_r(NULL, "\n", &saveptr); 110 | if (tok == NULL) { 111 | break; 112 | } 113 | history_add(&vec, tok); 114 | vec.buf[vec.count - 1].run_count = run_count; 115 | tok = strtok_r(NULL, " ", &saveptr); 116 | } 117 | 118 | free(buf); 119 | return vec; 120 | } 121 | 122 | void history_save(const struct history *history, const char *path) 123 | { 124 | /* Create the path if necessary. */ 125 | if (!mkdirp(path)) { 126 | return; 127 | } 128 | 129 | /* Use open rather than fopen to ensure the proper permissions. */ 130 | int histfd = open(path, O_WRONLY | O_CREAT, 0600); 131 | FILE *histfile = fdopen(histfd, "wb"); 132 | if (histfile == NULL) { 133 | return; 134 | } 135 | 136 | for (size_t i = 0; i < history->count; i++) { 137 | fprintf(histfile, "%zu %s\n", history->buf[i].run_count, history->buf[i].name); 138 | } 139 | 140 | fclose(histfile); 141 | } 142 | 143 | struct history history_load_default_file(bool drun) 144 | { 145 | char *histfile_name = get_histfile_path(drun); 146 | if (histfile_name == NULL) { 147 | return history_create(); 148 | } 149 | 150 | struct history vec = history_load(histfile_name); 151 | free(histfile_name); 152 | 153 | return vec; 154 | } 155 | 156 | void history_save_default_file(const struct history *history, bool drun) 157 | { 158 | char *histfile_name = get_histfile_path(drun); 159 | if (histfile_name == NULL) { 160 | return; 161 | } 162 | history_save(history, histfile_name); 163 | free(histfile_name); 164 | } 165 | 166 | struct history history_create(void) 167 | { 168 | struct history vec = { 169 | .count = 0, 170 | .size = 16, 171 | .buf = xcalloc(16, sizeof(struct program)) 172 | }; 173 | return vec; 174 | } 175 | 176 | void history_destroy(struct history *restrict vec) 177 | { 178 | for (size_t i = 0; i < vec->count; i++) { 179 | free(vec->buf[i].name); 180 | } 181 | free(vec->buf); 182 | } 183 | 184 | void history_add(struct history *restrict vec, const char *restrict str) 185 | { 186 | /* 187 | * If the program's already in our vector, just increment the count and 188 | * move the program up if needed. 189 | */ 190 | for (size_t i = 0; i < vec->count; i++) { 191 | if (!strcmp(vec->buf[i].name, str)) { 192 | vec->buf[i].run_count++; 193 | size_t count = vec->buf[i].run_count; 194 | if (i > 0 && count <= vec->buf[i-1].run_count) { 195 | return; 196 | } 197 | /* We need to move the program up the list */ 198 | size_t j = i; 199 | while (j > 0 && count > vec->buf[j-1].run_count) { 200 | j--; 201 | } 202 | struct program tmp = vec->buf[i]; 203 | memmove(&vec->buf[j+1], &vec->buf[j], (i - j) * sizeof(struct program)); 204 | vec->buf[j] = tmp; 205 | return; 206 | } 207 | } 208 | 209 | /* Otherwise add it to the end with a run count of 1 */ 210 | if (vec->count == vec->size) { 211 | vec->size *= 2; 212 | vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); 213 | } 214 | vec->buf[vec->count].name = xstrdup(str); 215 | vec->buf[vec->count].run_count = 1; 216 | vec->count++; 217 | } 218 | 219 | void history_remove(struct history *restrict vec, const char *restrict str) 220 | { 221 | for (size_t i = 0; i < vec->count; i++) { 222 | if (!strcmp(vec->buf[i].name, str)) { 223 | free(vec->buf[i].name); 224 | if (i < vec->count - 1) { 225 | memmove(&vec->buf[i], &vec->buf[i+1], (vec->count - i) * sizeof(struct program)); 226 | } 227 | vec->count--; 228 | return; 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/history.h: -------------------------------------------------------------------------------- 1 | #ifndef HISTORY_H 2 | #define HISTORY_H 3 | 4 | #include 5 | #include 6 | 7 | struct program { 8 | char *restrict name; 9 | size_t run_count; 10 | }; 11 | 12 | struct history { 13 | size_t count; 14 | size_t size; 15 | struct program *buf; 16 | }; 17 | 18 | [[gnu::nonnull]] 19 | void history_destroy(struct history *restrict vec); 20 | 21 | [[gnu::nonnull]] 22 | void history_add(struct history *restrict vec, const char *restrict str); 23 | 24 | //[[gnu::nonnull]] 25 | //void history_remove(struct history *restrict vec, const char *restrict str); 26 | 27 | [[nodiscard("memory leaked")]] 28 | struct history history_load(const char *path); 29 | 30 | void history_save(const struct history *history, const char *path); 31 | 32 | [[nodiscard("memory leaked")]] 33 | struct history history_load_default_file(bool drun); 34 | 35 | void history_save_default_file(const struct history *history, bool drun); 36 | 37 | #endif /* HISTORY_H */ 38 | -------------------------------------------------------------------------------- /src/input.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "input.h" 6 | #include "log.h" 7 | #include "nelem.h" 8 | #include "tofi.h" 9 | #include "unicode.h" 10 | 11 | 12 | static uint32_t keysym_to_key(xkb_keysym_t sym); 13 | static void add_character(struct tofi *tofi, xkb_keycode_t keycode); 14 | static void delete_character(struct tofi *tofi); 15 | static void delete_word(struct tofi *tofi); 16 | static void clear_input(struct tofi *tofi); 17 | static void paste(struct tofi *tofi); 18 | static void select_previous_result(struct tofi *tofi); 19 | static void select_next_result(struct tofi *tofi); 20 | static void select_previous_page(struct tofi *tofi); 21 | static void select_next_page(struct tofi *tofi); 22 | static void next_cursor_or_result(struct tofi *tofi); 23 | static void previous_cursor_or_result(struct tofi *tofi); 24 | static void reset_selection(struct tofi *tofi); 25 | 26 | void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) 27 | { 28 | if (tofi->xkb_state == NULL) { 29 | return; 30 | } 31 | 32 | bool ctrl = xkb_state_mod_name_is_active( 33 | tofi->xkb_state, 34 | XKB_MOD_NAME_CTRL, 35 | XKB_STATE_MODS_EFFECTIVE); 36 | bool alt = xkb_state_mod_name_is_active( 37 | tofi->xkb_state, 38 | XKB_MOD_NAME_ALT, 39 | XKB_STATE_MODS_EFFECTIVE); 40 | bool shift = xkb_state_mod_name_is_active( 41 | tofi->xkb_state, 42 | XKB_MOD_NAME_SHIFT, 43 | XKB_STATE_MODS_EFFECTIVE); 44 | 45 | uint32_t ch = xkb_state_key_get_utf32(tofi->xkb_state, keycode); 46 | 47 | /* 48 | * Use physical key code for shortcuts by default, ignoring layout 49 | * changes. Linux keycodes are 8 less than XKB keycodes. 50 | */ 51 | uint32_t key = keycode - 8; 52 | if (!tofi->physical_keybindings) { 53 | xkb_keysym_t sym = xkb_state_key_get_one_sym(tofi->xkb_state, keycode); 54 | key = keysym_to_key(sym); 55 | } 56 | 57 | /* 58 | * Alt does not affect which character is selected, so we have to check 59 | * for it explicitly. 60 | */ 61 | if (utf32_isprint(ch) && !ctrl && !alt) { 62 | add_character(tofi, keycode); 63 | } else if ((key == KEY_BACKSPACE || key == KEY_W) && ctrl) { 64 | delete_word(tofi); 65 | } else if (key == KEY_BACKSPACE 66 | || (key == KEY_H && ctrl)) { 67 | delete_character(tofi); 68 | } else if (key == KEY_U && ctrl) { 69 | clear_input(tofi); 70 | } else if (key == KEY_V && ctrl) { 71 | paste(tofi); 72 | } else if (key == KEY_LEFT) { 73 | previous_cursor_or_result(tofi); 74 | } else if (key == KEY_RIGHT) { 75 | next_cursor_or_result(tofi); 76 | } else if (key == KEY_UP 77 | || key == KEY_LEFT 78 | || (key == KEY_TAB && shift) 79 | || (key == KEY_H && alt) 80 | || ((key == KEY_K || key == KEY_P || key == KEY_B) && (ctrl || alt))) { 81 | select_previous_result(tofi); 82 | } else if (key == KEY_DOWN 83 | || key == KEY_RIGHT 84 | || key == KEY_TAB 85 | || (key == KEY_L && alt) 86 | || ((key == KEY_J || key == KEY_N || key == KEY_F) && (ctrl || alt))) { 87 | select_next_result(tofi); 88 | } else if (key == KEY_HOME) { 89 | reset_selection(tofi); 90 | } else if (key == KEY_PAGEUP) { 91 | select_previous_page(tofi); 92 | } else if (key == KEY_PAGEDOWN) { 93 | select_next_page(tofi); 94 | } else if (key == KEY_ESC 95 | || ((key == KEY_C || key == KEY_LEFTBRACE || key == KEY_G) && ctrl)) { 96 | tofi->closed = true; 97 | return; 98 | } else if (key == KEY_ENTER 99 | || key == KEY_KPENTER 100 | || (key == KEY_M && ctrl)) { 101 | tofi->submit = true; 102 | return; 103 | } 104 | 105 | if (tofi->auto_accept_single && tofi->window.entry.results.count == 1) { 106 | tofi->submit = true; 107 | } 108 | 109 | tofi->window.surface.redraw = true; 110 | } 111 | 112 | static uint32_t keysym_to_key(xkb_keysym_t sym) 113 | { 114 | switch (sym) { 115 | case XKB_KEY_BackSpace: 116 | return KEY_BACKSPACE; 117 | case XKB_KEY_w: 118 | return KEY_W; 119 | case XKB_KEY_u: 120 | return KEY_U; 121 | case XKB_KEY_v: 122 | return KEY_V; 123 | case XKB_KEY_Left: 124 | return KEY_LEFT; 125 | case XKB_KEY_Right: 126 | return KEY_RIGHT; 127 | case XKB_KEY_Up: 128 | return KEY_UP; 129 | case XKB_KEY_ISO_Left_Tab: 130 | return KEY_TAB; 131 | case XKB_KEY_h: 132 | return KEY_H; 133 | case XKB_KEY_k: 134 | return KEY_K; 135 | case XKB_KEY_p: 136 | return KEY_P; 137 | case XKB_KEY_Down: 138 | return KEY_DOWN; 139 | case XKB_KEY_Tab: 140 | return KEY_TAB; 141 | case XKB_KEY_l: 142 | return KEY_L; 143 | case XKB_KEY_j: 144 | return KEY_J; 145 | case XKB_KEY_n: 146 | return KEY_N; 147 | case XKB_KEY_Home: 148 | return KEY_HOME; 149 | case XKB_KEY_Page_Up: 150 | return KEY_PAGEUP; 151 | case XKB_KEY_Page_Down: 152 | return KEY_PAGEDOWN; 153 | case XKB_KEY_Escape: 154 | return KEY_ESC; 155 | case XKB_KEY_c: 156 | return KEY_C; 157 | case XKB_KEY_bracketleft: 158 | return KEY_LEFTBRACE; 159 | case XKB_KEY_Return: 160 | return KEY_ENTER; 161 | case XKB_KEY_KP_Enter: 162 | return KEY_KPENTER; 163 | case XKB_KEY_m: 164 | return KEY_M; 165 | } 166 | return (uint32_t)-1; 167 | } 168 | 169 | void reset_selection(struct tofi *tofi) 170 | { 171 | struct entry *entry = &tofi->window.entry; 172 | entry->selection = 0; 173 | entry->first_result = 0; 174 | } 175 | 176 | void add_character(struct tofi *tofi, xkb_keycode_t keycode) 177 | { 178 | struct entry *entry = &tofi->window.entry; 179 | 180 | if (entry->input_utf32_length >= N_ELEM(entry->input_utf32) - 1) { 181 | /* No more room for input */ 182 | return; 183 | } 184 | 185 | char buf[5]; /* 4 UTF-8 bytes plus null terminator. */ 186 | int len = xkb_state_key_get_utf8( 187 | tofi->xkb_state, 188 | keycode, 189 | buf, 190 | sizeof(buf)); 191 | if (entry->cursor_position == entry->input_utf32_length) { 192 | entry->input_utf32[entry->input_utf32_length] = utf8_to_utf32(buf); 193 | entry->input_utf32_length++; 194 | entry->input_utf32[entry->input_utf32_length] = U'\0'; 195 | memcpy(&entry->input_utf8[entry->input_utf8_length], 196 | buf, 197 | N_ELEM(buf)); 198 | entry->input_utf8_length += len; 199 | 200 | if (entry->mode == TOFI_MODE_DRUN) { 201 | struct string_ref_vec results = desktop_vec_filter(&entry->apps, entry->input_utf8, tofi->matching_algorithm); 202 | string_ref_vec_destroy(&entry->results); 203 | entry->results = results; 204 | } else { 205 | struct string_ref_vec tmp = entry->results; 206 | entry->results = string_ref_vec_filter(&entry->results, entry->input_utf8, tofi->matching_algorithm); 207 | string_ref_vec_destroy(&tmp); 208 | } 209 | 210 | reset_selection(tofi); 211 | } else { 212 | for (size_t i = entry->input_utf32_length; i > entry->cursor_position; i--) { 213 | entry->input_utf32[i] = entry->input_utf32[i - 1]; 214 | } 215 | entry->input_utf32[entry->cursor_position] = utf8_to_utf32(buf); 216 | entry->input_utf32_length++; 217 | entry->input_utf32[entry->input_utf32_length] = U'\0'; 218 | 219 | input_refresh_results(tofi); 220 | } 221 | 222 | entry->cursor_position++; 223 | } 224 | 225 | void input_refresh_results(struct tofi *tofi) 226 | { 227 | struct entry *entry = &tofi->window.entry; 228 | 229 | size_t bytes_written = 0; 230 | for (size_t i = 0; i < entry->input_utf32_length; i++) { 231 | bytes_written += utf32_to_utf8( 232 | entry->input_utf32[i], 233 | &entry->input_utf8[bytes_written]); 234 | } 235 | entry->input_utf8[bytes_written] = '\0'; 236 | entry->input_utf8_length = bytes_written; 237 | string_ref_vec_destroy(&entry->results); 238 | if (entry->mode == TOFI_MODE_DRUN) { 239 | entry->results = desktop_vec_filter(&entry->apps, entry->input_utf8, tofi->matching_algorithm); 240 | } else { 241 | entry->results = string_ref_vec_filter(&entry->commands, entry->input_utf8, tofi->matching_algorithm); 242 | } 243 | 244 | reset_selection(tofi); 245 | } 246 | 247 | void delete_character(struct tofi *tofi) 248 | { 249 | struct entry *entry = &tofi->window.entry; 250 | 251 | if (entry->input_utf32_length == 0) { 252 | /* No input to delete. */ 253 | return; 254 | } 255 | 256 | if (entry->cursor_position == 0) { 257 | return; 258 | } else if (entry->cursor_position == entry->input_utf32_length) { 259 | entry->cursor_position--; 260 | entry->input_utf32_length--; 261 | entry->input_utf32[entry->input_utf32_length] = U'\0'; 262 | } else { 263 | for (size_t i = entry->cursor_position - 1; i < entry->input_utf32_length - 1; i++) { 264 | entry->input_utf32[i] = entry->input_utf32[i + 1]; 265 | } 266 | entry->cursor_position--; 267 | entry->input_utf32_length--; 268 | entry->input_utf32[entry->input_utf32_length] = U'\0'; 269 | } 270 | 271 | input_refresh_results(tofi); 272 | } 273 | 274 | void delete_word(struct tofi *tofi) 275 | { 276 | struct entry *entry = &tofi->window.entry; 277 | 278 | if (entry->cursor_position == 0) { 279 | /* No input to delete. */ 280 | return; 281 | } 282 | 283 | uint32_t new_cursor_pos = entry->cursor_position; 284 | while (new_cursor_pos > 0 && utf32_isspace(entry->input_utf32[new_cursor_pos - 1])) { 285 | new_cursor_pos--; 286 | } 287 | while (new_cursor_pos > 0 && !utf32_isspace(entry->input_utf32[new_cursor_pos - 1])) { 288 | new_cursor_pos--; 289 | } 290 | uint32_t new_length = entry->input_utf32_length - (entry->cursor_position - new_cursor_pos); 291 | for (size_t i = 0; i < new_length; i++) { 292 | entry->input_utf32[new_cursor_pos + i] = entry->input_utf32[entry->cursor_position + i]; 293 | } 294 | entry->input_utf32_length = new_length; 295 | entry->input_utf32[entry->input_utf32_length] = U'\0'; 296 | 297 | entry->cursor_position = new_cursor_pos; 298 | input_refresh_results(tofi); 299 | } 300 | 301 | void clear_input(struct tofi *tofi) 302 | { 303 | struct entry *entry = &tofi->window.entry; 304 | 305 | entry->cursor_position = 0; 306 | entry->input_utf32_length = 0; 307 | entry->input_utf32[0] = U'\0'; 308 | 309 | input_refresh_results(tofi); 310 | } 311 | 312 | void paste(struct tofi *tofi) 313 | { 314 | if (tofi->clipboard.wl_data_offer == NULL || tofi->clipboard.mime_type == NULL) { 315 | return; 316 | } 317 | 318 | /* 319 | * Create a pipe, and give the write end to the compositor to give to 320 | * the clipboard manager. 321 | */ 322 | errno = 0; 323 | int fildes[2]; 324 | if (pipe2(fildes, O_CLOEXEC | O_NONBLOCK) == -1) { 325 | log_error("Failed to open pipe for clipboard: %s\n", strerror(errno)); 326 | return; 327 | } 328 | wl_data_offer_receive(tofi->clipboard.wl_data_offer, tofi->clipboard.mime_type, fildes[1]); 329 | close(fildes[1]); 330 | 331 | /* Keep the read end for reading in the main loop. */ 332 | tofi->clipboard.fd = fildes[0]; 333 | } 334 | 335 | void select_previous_result(struct tofi *tofi) 336 | { 337 | struct entry *entry = &tofi->window.entry; 338 | 339 | if (entry->selection > 0) { 340 | entry->selection--; 341 | return; 342 | } 343 | 344 | uint32_t nsel = MAX(MIN(entry->num_results_drawn, entry->results.count), 1); 345 | 346 | if (entry->first_result > nsel) { 347 | entry->first_result -= entry->last_num_results_drawn; 348 | entry->selection = entry->last_num_results_drawn - 1; 349 | } else if (entry->first_result > 0) { 350 | entry->selection = entry->first_result - 1; 351 | entry->first_result = 0; 352 | } 353 | } 354 | 355 | void select_next_result(struct tofi *tofi) 356 | { 357 | struct entry *entry = &tofi->window.entry; 358 | 359 | uint32_t nsel = MAX(MIN(entry->num_results_drawn, entry->results.count), 1); 360 | 361 | entry->selection++; 362 | if (entry->selection >= nsel) { 363 | entry->selection -= nsel; 364 | if (entry->results.count > 0) { 365 | entry->first_result += nsel; 366 | entry->first_result %= entry->results.count; 367 | } else { 368 | entry->first_result = 0; 369 | } 370 | entry->last_num_results_drawn = entry->num_results_drawn; 371 | } 372 | } 373 | 374 | void previous_cursor_or_result(struct tofi *tofi) 375 | { 376 | struct entry *entry = &tofi->window.entry; 377 | 378 | if (entry->cursor_theme.show 379 | && entry->selection == 0 380 | && entry->cursor_position > 0) { 381 | entry->cursor_position--; 382 | } else { 383 | select_previous_result(tofi); 384 | } 385 | } 386 | 387 | void next_cursor_or_result(struct tofi *tofi) 388 | { 389 | struct entry *entry = &tofi->window.entry; 390 | 391 | if (entry->cursor_theme.show 392 | && entry->cursor_position < entry->input_utf32_length) { 393 | entry->cursor_position++; 394 | } else { 395 | select_next_result(tofi); 396 | } 397 | } 398 | 399 | void select_previous_page(struct tofi *tofi) 400 | { 401 | struct entry *entry = &tofi->window.entry; 402 | 403 | if (entry->first_result >= entry->last_num_results_drawn) { 404 | entry->first_result -= entry->last_num_results_drawn; 405 | } else { 406 | entry->first_result = 0; 407 | } 408 | entry->selection = 0; 409 | entry->last_num_results_drawn = entry->num_results_drawn; 410 | } 411 | 412 | void select_next_page(struct tofi *tofi) 413 | { 414 | struct entry *entry = &tofi->window.entry; 415 | 416 | entry->first_result += entry->num_results_drawn; 417 | if (entry->first_result >= entry->results.count) { 418 | entry->first_result = 0; 419 | } 420 | entry->selection = 0; 421 | entry->last_num_results_drawn = entry->num_results_drawn; 422 | } 423 | -------------------------------------------------------------------------------- /src/input.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_H 2 | #define INPUT_H 3 | 4 | #include 5 | #include "tofi.h" 6 | 7 | void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode); 8 | void input_refresh_results(struct tofi *tofi); 9 | 10 | #endif /* INPUT_H */ 11 | -------------------------------------------------------------------------------- /src/lock.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "log.h" 9 | #include "xmalloc.h" 10 | 11 | static const char *default_cache_dir = ".cache/"; 12 | static const char *lock_filename = "tofi.lock"; 13 | 14 | [[nodiscard("memory leaked")]] 15 | static char *get_lock_path() { 16 | char *lock_name = NULL; 17 | const char *runtime_path = getenv("XDG_RUNTIME_DIR"); 18 | if (runtime_path == NULL) { 19 | runtime_path = getenv("XDG_CACHE_HOME"); 20 | } 21 | if (runtime_path == NULL) { 22 | const char *home = getenv("HOME"); 23 | if (home == NULL) { 24 | log_error("Couldn't retrieve HOME from environment.\n"); 25 | return NULL; 26 | } 27 | size_t len = strlen(home) + 1 28 | + strlen(default_cache_dir) + 1 29 | + strlen(lock_filename) + 1; 30 | lock_name = xmalloc(len); 31 | snprintf( 32 | lock_name, 33 | len, 34 | "%s/%s/%s", 35 | home, 36 | default_cache_dir, 37 | lock_filename); 38 | } else { 39 | size_t len = strlen(runtime_path) + 1 40 | + strlen(lock_filename) + 1; 41 | lock_name = xmalloc(len); 42 | snprintf( 43 | lock_name, 44 | len, 45 | "%s/%s", 46 | runtime_path, 47 | lock_filename); 48 | } 49 | return lock_name; 50 | } 51 | 52 | bool lock_check(void) 53 | { 54 | bool ret = false; 55 | char *filename = get_lock_path(); 56 | errno = 0; 57 | int fd = open(filename, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR); 58 | if (fd == -1) { 59 | log_error("Failed to open lock file %s: %s.\n", filename, strerror(errno)); 60 | } else if (flock(fd, LOCK_EX | LOCK_NB) == -1) { 61 | if (errno == EWOULDBLOCK) { 62 | /* 63 | * We can't lock the file because another tofi process 64 | * already has. 65 | */ 66 | ret = true; 67 | } 68 | } 69 | 70 | free(filename); 71 | return ret; 72 | } 73 | -------------------------------------------------------------------------------- /src/lock.h: -------------------------------------------------------------------------------- 1 | #ifndef LOCK_H 2 | #define LOCK_H 3 | 4 | #include 5 | 6 | bool lock_check(void); 7 | 8 | #endif /* LOCK_H */ 9 | -------------------------------------------------------------------------------- /src/log.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define SECOND 1000000000ul 8 | 9 | static struct timespec time_diff( 10 | struct timespec cur, 11 | struct timespec old); 12 | 13 | static int indent = 0; 14 | 15 | static void print_indent(FILE *file) 16 | { 17 | for (int i = 0; i < indent; i++) { 18 | fprintf(file, " "); 19 | } 20 | } 21 | 22 | void log_indent(void) 23 | { 24 | indent++; 25 | } 26 | 27 | void log_unindent(void) 28 | { 29 | if (indent > 0) { 30 | indent--; 31 | } 32 | } 33 | 34 | void log_error(const char *const fmt, ...) 35 | { 36 | va_list args; 37 | va_start(args, fmt); 38 | fprintf(stderr, "[ERROR]: "); 39 | vfprintf(stderr, fmt, args); 40 | va_end(args); 41 | } 42 | 43 | void log_warning(const char *const fmt, ...) 44 | { 45 | va_list args; 46 | va_start(args, fmt); 47 | fprintf(stderr, "[WARNING]: "); 48 | vfprintf(stderr, fmt, args); 49 | va_end(args); 50 | } 51 | 52 | void log_debug(const char *const fmt, ...) 53 | { 54 | #ifndef DEBUG 55 | return; 56 | #endif 57 | static struct timespec start_time; 58 | if (start_time.tv_nsec == 0) { 59 | fprintf(stderr, "[ real, cpu, maxRSS]\n"); 60 | clock_gettime(CLOCK_REALTIME, &start_time); 61 | } 62 | struct timespec real_time; 63 | struct timespec cpu_time; 64 | clock_gettime(CLOCK_REALTIME, &real_time); 65 | clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &cpu_time); 66 | real_time = time_diff(real_time, start_time); 67 | 68 | struct rusage usage; 69 | getrusage(RUSAGE_SELF, &usage); 70 | 71 | va_list args; 72 | va_start(args, fmt); 73 | fprintf( 74 | stderr, 75 | "[%ld.%06ld, %ld.%06ld, %5ld KB][DEBUG]: ", 76 | real_time.tv_sec, 77 | real_time.tv_nsec / 1000, 78 | cpu_time.tv_sec, 79 | cpu_time.tv_nsec / 1000, 80 | usage.ru_maxrss 81 | ); 82 | print_indent(stderr); 83 | vfprintf(stderr, fmt, args); 84 | va_end(args); 85 | } 86 | 87 | void log_info(const char *const fmt, ...) 88 | { 89 | va_list args; 90 | va_start(args, fmt); 91 | fprintf(stderr, "[INFO]: "); 92 | print_indent(stderr); 93 | vfprintf(stderr, fmt, args); 94 | va_end(args); 95 | } 96 | 97 | void log_append_error(const char *const fmt, ...) 98 | { 99 | va_list args; 100 | va_start(args, fmt); 101 | vfprintf(stderr, fmt, args); 102 | va_end(args); 103 | } 104 | 105 | void log_append_warning(const char *const fmt, ...) 106 | { 107 | va_list args; 108 | va_start(args, fmt); 109 | vfprintf(stderr, fmt, args); 110 | va_end(args); 111 | } 112 | 113 | void log_append_debug(const char *const fmt, ...) 114 | { 115 | #ifndef DEBUG 116 | return; 117 | #endif 118 | va_list args; 119 | va_start(args, fmt); 120 | vprintf(fmt, args); 121 | va_end(args); 122 | } 123 | 124 | void log_append_info(const char *const fmt, ...) 125 | { 126 | va_list args; 127 | va_start(args, fmt); 128 | vprintf(fmt, args); 129 | va_end(args); 130 | } 131 | 132 | struct timespec time_diff(struct timespec cur, 133 | struct timespec old) 134 | { 135 | struct timespec diff; 136 | diff.tv_sec = cur.tv_sec - old.tv_sec; 137 | if (cur.tv_nsec > old.tv_nsec) { 138 | diff.tv_nsec = cur.tv_nsec - old.tv_nsec; 139 | } else { 140 | diff.tv_nsec = SECOND + cur.tv_nsec - old.tv_nsec; 141 | diff.tv_sec -= 1; 142 | } 143 | return diff; 144 | } 145 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | void log_indent(void); 5 | void log_unindent(void); 6 | void log_error(const char *const fmt, ...); 7 | void log_warning(const char *const fmt, ...); 8 | void log_debug(const char *const fmt, ...); 9 | void log_info(const char *const fmt, ...); 10 | void log_append_error(const char *const fmt, ...); 11 | void log_append_warning(const char *const fmt, ...); 12 | void log_append_debug(const char *const fmt, ...); 13 | void log_append_info(const char *const fmt, ...); 14 | 15 | #endif /* LOG_H */ 16 | -------------------------------------------------------------------------------- /src/main_compgen.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "compgen.h" 4 | #include "string_vec.h" 5 | 6 | int main() 7 | { 8 | char *buf = compgen_cached(); 9 | struct string_ref_vec commands = string_ref_vec_from_buffer(buf); 10 | for (size_t i = 0; i < commands.count; i++) { 11 | fputs(commands.buf[i].string, stdout); 12 | fputc('\n', stdout); 13 | } 14 | string_ref_vec_destroy(&commands); 15 | free(buf); 16 | } 17 | -------------------------------------------------------------------------------- /src/matching.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "matching.h" 8 | #include "unicode.h" 9 | #include "xmalloc.h" 10 | 11 | #undef MAX 12 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 13 | 14 | static int32_t simple_match_words( 15 | const char *restrict patterns, 16 | const char *restrict str); 17 | 18 | static int32_t prefix_match_words( 19 | const char *restrict patterns, 20 | const char *restrict str); 21 | 22 | static int32_t fuzzy_match_words( 23 | const char *restrict patterns, 24 | const char *restrict str); 25 | 26 | static int32_t fuzzy_match( 27 | const char *restrict pattern, 28 | const char *restrict str); 29 | 30 | static int32_t fuzzy_match_recurse( 31 | const char *restrict pattern, 32 | const char *restrict str, 33 | int32_t score, 34 | bool first_match_only, 35 | bool first_char); 36 | 37 | static int32_t compute_score( 38 | int32_t jump, 39 | bool first_char, 40 | const char *restrict match); 41 | 42 | /* 43 | * Select the appropriate algorithm, and return its score. 44 | * Each algorithm returns larger scores for better matches, 45 | * and returns INT32_MIN if a word is not found. 46 | */ 47 | int32_t match_words( 48 | enum matching_algorithm algorithm, 49 | const char *restrict patterns, 50 | const char *restrict str) 51 | { 52 | switch (algorithm) { 53 | case MATCHING_ALGORITHM_NORMAL: 54 | return simple_match_words(patterns, str); 55 | case MATCHING_ALGORITHM_PREFIX: 56 | return prefix_match_words(patterns, str); 57 | case MATCHING_ALGORITHM_FUZZY: 58 | return fuzzy_match_words(patterns, str); 59 | default: 60 | return INT32_MIN; 61 | } 62 | } 63 | 64 | /* 65 | * Split patterns into words, and perform simple matching against str for each. 66 | * Returns the negative sum of substring distances from the start of str. 67 | * If a word is not found, returns INT32_MIN. 68 | */ 69 | int32_t simple_match_words(const char *restrict patterns, const char *restrict str) 70 | { 71 | int32_t score = 0; 72 | char *saveptr = NULL; 73 | char *tmp = utf8_normalize(patterns); 74 | char *pattern = strtok_r(tmp, " ", &saveptr); 75 | while (pattern != NULL) { 76 | char *c = utf8_strcasestr(str, pattern); 77 | if (c == NULL) { 78 | score = INT32_MIN; 79 | break; 80 | } else { 81 | score -= c - str; 82 | } 83 | pattern = strtok_r(NULL, " ", &saveptr); 84 | } 85 | free(tmp); 86 | return score; 87 | } 88 | 89 | /* 90 | * Split patterns into words, and perform prefix matching against str for each. 91 | * Returns the negative sum of remaining string suffix lengths. 92 | * If a word is not found, returns INT32_MIN. 93 | */ 94 | int32_t prefix_match_words(const char *restrict patterns, const char *restrict str) 95 | { 96 | int32_t score = 0; 97 | char *saveptr = NULL; 98 | char *tmp = utf8_normalize(patterns); 99 | char *pattern = strtok_r(tmp, " ", &saveptr); 100 | while (pattern != NULL) { 101 | char *c = utf8_strcasestr(str, pattern); 102 | if (c != str) { 103 | score = INT32_MIN; 104 | break; 105 | } else { 106 | score -= utf8_strlen(str) - utf8_strlen(pattern); 107 | } 108 | pattern = strtok_r(NULL, " ", &saveptr); 109 | } 110 | free(tmp); 111 | return score; 112 | } 113 | 114 | 115 | /* 116 | * Split patterns into words, and return the sum of fuzzy_match(word, str). 117 | * If a word is not found, returns INT32_MIN. 118 | */ 119 | int32_t fuzzy_match_words(const char *restrict patterns, const char *restrict str) 120 | { 121 | int32_t score = 0; 122 | char *saveptr = NULL; 123 | char *tmp = utf8_normalize(patterns); 124 | char *pattern = strtok_r(tmp, " ", &saveptr); 125 | while (pattern != NULL) { 126 | int32_t word_score = fuzzy_match(pattern, str); 127 | if (word_score == INT32_MIN) { 128 | score = INT32_MIN; 129 | break; 130 | } else { 131 | score += word_score; 132 | } 133 | pattern = strtok_r(NULL, " ", &saveptr); 134 | } 135 | free(tmp); 136 | return score; 137 | } 138 | 139 | /* 140 | * Returns score if each character in pattern is found sequentially within str. 141 | * Returns INT32_MIN otherwise. 142 | */ 143 | int32_t fuzzy_match(const char *restrict pattern, const char *restrict str) 144 | { 145 | const int unmatched_letter_penalty = -1; 146 | const size_t slen = utf8_strlen(str); 147 | const size_t plen = utf8_strlen(pattern); 148 | int32_t score = 0; 149 | 150 | if (*pattern == '\0') { 151 | return score; 152 | } 153 | if (slen < plen) { 154 | return INT32_MIN; 155 | } 156 | 157 | /* We can already penalise any unused letters. */ 158 | score += unmatched_letter_penalty * (int32_t)(slen - plen); 159 | 160 | /* 161 | * If the string is more than 100 characters, just find the first fuzzy 162 | * match rather than the best. 163 | * 164 | * This is required as the number of possible matches (for patterns and 165 | * strings all consisting of one letter) scales something like: 166 | * 167 | * slen! / (plen! (slen - plen)!) ~ slen^plen for plen << slen 168 | * 169 | * This quickly grinds everything to a halt. 100 is chosen fairly 170 | * arbitrarily from the following logic: 171 | * 172 | * - e is the most common character in English, at around 13% of 173 | * letters. Depending on the context, let's say this be up to 20%. 174 | * - 100 * 0.20 = 20 repeats of the same character. 175 | * - In the worst case here, 20! / (10! 10!) ~200,000 possible matches, 176 | * which is "slow but not frozen" for my machine. 177 | * 178 | * In reality, this worst case shouldn't be hit, and finding the "best" 179 | * fuzzy match in lines of text > 100 characters isn't really in scope 180 | * for a dmenu clone. 181 | */ 182 | bool first_match_only = slen > 100; 183 | 184 | /* Perform the match. */ 185 | score = fuzzy_match_recurse(pattern, str, score, first_match_only, true); 186 | 187 | return score; 188 | } 189 | 190 | /* 191 | * Recursively match the whole of pattern against str. 192 | * The score parameter is the score of the previously matched character. 193 | * 194 | * This reaches a maximum recursion depth of strlen(pattern) + 1. However, the 195 | * stack usage is small (the maximum I've seen on x86_64 is 144 bytes with 196 | * gcc -O3), so this shouldn't matter unless pattern contains thousands of 197 | * characters. 198 | */ 199 | int32_t fuzzy_match_recurse( 200 | const char *restrict pattern, 201 | const char *restrict str, 202 | int32_t score, 203 | bool first_match_only, 204 | bool first_char) 205 | { 206 | if (*pattern == '\0') { 207 | /* We've matched the full pattern. */ 208 | return score; 209 | } 210 | 211 | const char *match = str; 212 | uint32_t search = utf8_to_utf32(pattern); 213 | 214 | int32_t best_score = INT32_MIN; 215 | 216 | /* 217 | * Find all occurrences of the next pattern character in str, and 218 | * recurse on them. 219 | */ 220 | while ((match = utf8_strcasechr(match, search)) != NULL) { 221 | int32_t jump = 0; 222 | for (const char *tmp = str; tmp != match; tmp = utf8_next_char(tmp)) { 223 | jump++; 224 | } 225 | int32_t subscore = fuzzy_match_recurse( 226 | utf8_next_char(pattern), 227 | utf8_next_char(match), 228 | compute_score(jump, first_char, match), 229 | first_match_only, 230 | false); 231 | best_score = MAX(best_score, subscore); 232 | match = utf8_next_char(match); 233 | 234 | if (first_match_only) { 235 | break; 236 | } 237 | } 238 | 239 | if (best_score == INT32_MIN) { 240 | /* We couldn't match the rest of the pattern. */ 241 | return INT32_MIN; 242 | } else { 243 | return score + best_score; 244 | } 245 | } 246 | 247 | /* 248 | * Calculate the score for a single matching letter. 249 | * The scoring system is taken from fts_fuzzy_match v0.2.0 by Forrest Smith, 250 | * which is licensed to the public domain. 251 | * 252 | * The factors affecting score are: 253 | * - Bonuses: 254 | * - If there are multiple adjacent matches. 255 | * - If a match occurs after a separator character. 256 | * - If a match is uppercase, and the previous character is lowercase. 257 | * 258 | * - Penalties: 259 | * - If there are letters before the first match. 260 | * - If there are superfluous characters in str (already accounted for). 261 | */ 262 | int32_t compute_score(int32_t jump, bool first_char, const char *restrict match) 263 | { 264 | const int adjacency_bonus = 15; 265 | const int separator_bonus = 30; 266 | const int camel_bonus = 30; 267 | const int first_letter_bonus = 15; 268 | 269 | const int leading_letter_penalty = -5; 270 | const int max_leading_letter_penalty = -15; 271 | 272 | int32_t score = 0; 273 | 274 | const uint32_t cur = utf8_to_utf32(match); 275 | 276 | /* Apply bonuses. */ 277 | if (!first_char && jump == 0) { 278 | score += adjacency_bonus; 279 | } 280 | if (!first_char || jump > 0) { 281 | const uint32_t prev = utf8_to_utf32(utf8_prev_char(match)); 282 | if (utf32_isupper(cur) && utf32_islower(prev)) { 283 | score += camel_bonus; 284 | } 285 | if (utf32_isalnum(cur) && !utf32_isalnum(prev)) { 286 | score += separator_bonus; 287 | } 288 | } 289 | if (first_char && jump == 0) { 290 | /* Match at start of string gets separator bonus. */ 291 | score += first_letter_bonus; 292 | } 293 | 294 | /* Apply penalties. */ 295 | if (first_char) { 296 | score += MAX(leading_letter_penalty * jump, 297 | max_leading_letter_penalty); 298 | } 299 | 300 | return score; 301 | } 302 | -------------------------------------------------------------------------------- /src/matching.h: -------------------------------------------------------------------------------- 1 | #ifndef MATCHING_H 2 | #define MATCHING_H 3 | 4 | #include 5 | 6 | enum matching_algorithm { 7 | MATCHING_ALGORITHM_NORMAL, 8 | MATCHING_ALGORITHM_PREFIX, 9 | MATCHING_ALGORITHM_FUZZY 10 | }; 11 | 12 | int32_t match_words(enum matching_algorithm algorithm, const char *restrict patterns, const char *restrict str); 13 | 14 | #endif /* MATCHING_H */ 15 | -------------------------------------------------------------------------------- /src/mkdirp.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "log.h" 6 | #include "mkdirp.h" 7 | #include "xmalloc.h" 8 | 9 | bool mkdirp(const char *path) 10 | { 11 | struct stat statbuf; 12 | if (stat(path, &statbuf) == 0) { 13 | /* If the file exists, we don't need to do anything. */ 14 | return true; 15 | } 16 | 17 | /* 18 | * Walk down the path, creating directories as we go. 19 | * This works by repeatedly finding the next / in path, then calling 20 | * mkdir() on the string up to that point. 21 | */ 22 | char *tmp = xstrdup(path); 23 | char *cursor = tmp; 24 | while ((cursor = strchr(cursor + 1, '/')) != NULL) { 25 | *cursor = '\0'; 26 | log_debug("Creating directory %s\n", tmp); 27 | if (mkdir(tmp, 0700) != 0 && errno != EEXIST) { 28 | log_error( 29 | "Error creating file path: %s.\n", 30 | strerror(errno)); 31 | free(tmp); 32 | return false; 33 | } 34 | *cursor = '/'; 35 | } 36 | free(tmp); 37 | return true; 38 | } 39 | -------------------------------------------------------------------------------- /src/mkdirp.h: -------------------------------------------------------------------------------- 1 | #ifndef MKDIRP_H 2 | #define MKDIRP_H 3 | 4 | #include 5 | 6 | bool mkdirp(const char *path); 7 | 8 | #endif /* MKDIRP_H */ 9 | -------------------------------------------------------------------------------- /src/nelem.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Philip Jones 3 | * 4 | * Licensed under the MIT License. 5 | * See either the LICENSE file, or: 6 | * 7 | * https://opensource.org/licenses/MIT 8 | * 9 | */ 10 | 11 | #ifndef N_ELEM 12 | #define N_ELEM(x) (sizeof(x) / sizeof(*(x))) 13 | #endif 14 | -------------------------------------------------------------------------------- /src/scale.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* 5 | * In order to correctly scale by fractions of 120 (used by 6 | * wp_fractional_scale_v1), we need to bias the result before rounding. 7 | */ 8 | 9 | uint32_t scale_apply(uint32_t base, uint32_t scale) 10 | { 11 | return round(base * (scale / 120.) + 1e-6); 12 | } 13 | 14 | uint32_t scale_apply_inverse(uint32_t base, uint32_t scale) 15 | { 16 | return round(base * (120. / scale) + 1e-6); 17 | } 18 | -------------------------------------------------------------------------------- /src/scale.h: -------------------------------------------------------------------------------- 1 | #ifndef SCALE_H 2 | #define SCALE_H 3 | 4 | #include 5 | 6 | uint32_t scale_apply(uint32_t base, uint32_t scale); 7 | uint32_t scale_apply_inverse(uint32_t base, uint32_t scale); 8 | 9 | #endif /* SCALE_H */ 10 | -------------------------------------------------------------------------------- /src/shm.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "shm.h" 7 | 8 | /* These two functions aren't used on linux. */ 9 | #ifndef __linux__ 10 | static void randname(char *buf) 11 | { 12 | struct timespec ts; 13 | clock_gettime(CLOCK_REALTIME, &ts); 14 | long r = ts.tv_nsec; 15 | for (int i = 0; i < 6; ++i) { 16 | buf[i] = 'A'+(r&15)+(r&16)*2; 17 | r >>= 5; 18 | } 19 | } 20 | 21 | static int create_shm_file(void) 22 | { 23 | int retries = 100; 24 | do { 25 | char name[] = "/wl_shm-XXXXXX"; 26 | randname(name + sizeof(name) - 7); 27 | --retries; 28 | int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); 29 | if (fd >= 0) { 30 | shm_unlink(name); 31 | return fd; 32 | } 33 | } while (retries > 0 && errno == EEXIST); 34 | return -1; 35 | } 36 | #endif 37 | 38 | int shm_allocate_file(size_t size) 39 | { 40 | #ifdef __linux__ 41 | /* 42 | * On linux, we can just use memfd_create(). This is both simpler and 43 | * potentially allows usage of Transparent HugePages, which speed up 44 | * the first paint of a large screen buffer. 45 | * 46 | * This isn't available on *BSD, which we could conceivably be running 47 | * on. 48 | */ 49 | int fd = memfd_create("wl_shm", 0); 50 | #else 51 | int fd = create_shm_file(); 52 | #endif 53 | if (fd < 0) 54 | return -1; 55 | int ret; 56 | do { 57 | ret = ftruncate(fd, size); 58 | } while (ret < 0 && errno == EINTR); 59 | if (ret < 0) { 60 | close(fd); 61 | return -1; 62 | } 63 | return fd; 64 | } 65 | -------------------------------------------------------------------------------- /src/shm.h: -------------------------------------------------------------------------------- 1 | #ifndef SHM_H 2 | #define SHM_H 3 | 4 | #include 5 | 6 | int shm_allocate_file(size_t size); 7 | 8 | #endif /* SHM_H */ 9 | -------------------------------------------------------------------------------- /src/string_vec.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "history.h" 9 | #include "matching.h" 10 | #include "string_vec.h" 11 | #include "unicode.h" 12 | #include "xmalloc.h" 13 | 14 | static int cmpstringp(const void *restrict a, const void *restrict b) 15 | { 16 | struct scored_string *restrict str1 = (struct scored_string *)a; 17 | struct scored_string *restrict str2 = (struct scored_string *)b; 18 | 19 | /* 20 | * Ensure any NULL strings are shoved to the end. 21 | */ 22 | if (str1->string == NULL) { 23 | return 1; 24 | } 25 | if (str2->string == NULL) { 26 | return -1; 27 | } 28 | return strcmp(str1->string, str2->string); 29 | } 30 | 31 | static int cmpscorep(const void *restrict a, const void *restrict b) 32 | { 33 | struct scored_string *restrict str1 = (struct scored_string *)a; 34 | struct scored_string *restrict str2 = (struct scored_string *)b; 35 | 36 | int hist_diff = str2->history_score - str1->history_score; 37 | int search_diff = str2->search_score - str1->search_score; 38 | return hist_diff + search_diff; 39 | } 40 | 41 | static int cmphistoryp(const void *restrict a, const void *restrict b) 42 | { 43 | struct scored_string *restrict str1 = (struct scored_string *)a; 44 | struct scored_string *restrict str2 = (struct scored_string *)b; 45 | 46 | return str2->history_score - str1->history_score; 47 | } 48 | 49 | struct string_vec string_vec_create(void) 50 | { 51 | struct string_vec vec = { 52 | .count = 0, 53 | .size = 128, 54 | .buf = xcalloc(128, sizeof(*vec.buf)), 55 | }; 56 | return vec; 57 | } 58 | 59 | struct string_ref_vec string_ref_vec_create(void) 60 | { 61 | struct string_ref_vec vec = { 62 | .count = 0, 63 | .size = 128, 64 | .buf = xcalloc(128, sizeof(*vec.buf)), 65 | }; 66 | return vec; 67 | } 68 | 69 | void string_vec_destroy(struct string_vec *restrict vec) 70 | { 71 | for (size_t i = 0; i < vec->count; i++) { 72 | free(vec->buf[i].string); 73 | } 74 | free(vec->buf); 75 | } 76 | 77 | void string_ref_vec_destroy(struct string_ref_vec *restrict vec) 78 | { 79 | free(vec->buf); 80 | } 81 | 82 | struct string_ref_vec string_ref_vec_copy(const struct string_ref_vec *restrict vec) 83 | { 84 | struct string_ref_vec copy = { 85 | .count = vec->count, 86 | .size = vec->size, 87 | .buf = xcalloc(vec->size, sizeof(*copy.buf)), 88 | }; 89 | 90 | for (size_t i = 0; i < vec->count; i++) { 91 | copy.buf[i].string = vec->buf[i].string; 92 | copy.buf[i].search_score = vec->buf[i].search_score; 93 | copy.buf[i].history_score = vec->buf[i].history_score; 94 | } 95 | 96 | return copy; 97 | } 98 | 99 | void string_vec_add(struct string_vec *restrict vec, const char *restrict str) 100 | { 101 | if (!utf8_validate(str)) { 102 | return; 103 | } 104 | if (vec->count == vec->size) { 105 | vec->size *= 2; 106 | vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); 107 | } 108 | vec->buf[vec->count].string = utf8_normalize(str); 109 | if (vec->buf[vec->count].string == NULL) { 110 | vec->buf[vec->count].string = xstrdup(str); 111 | } 112 | vec->buf[vec->count].search_score = 0; 113 | vec->buf[vec->count].history_score = 0; 114 | vec->count++; 115 | } 116 | 117 | void string_ref_vec_add(struct string_ref_vec *restrict vec, char *restrict str) 118 | { 119 | if (vec->count == vec->size) { 120 | vec->size *= 2; 121 | vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); 122 | } 123 | vec->buf[vec->count].string = str; 124 | vec->buf[vec->count].search_score = 0; 125 | vec->buf[vec->count].history_score = 0; 126 | vec->count++; 127 | } 128 | 129 | void string_vec_sort(struct string_vec *restrict vec) 130 | { 131 | qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmpstringp); 132 | } 133 | 134 | void string_ref_vec_history_sort(struct string_ref_vec *restrict vec, struct history *history) 135 | { 136 | /* 137 | * To find elements without assuming the vector is pre-sorted, we use a 138 | * hash table, which results in O(N+M) work (rather than O(N*M) for 139 | * linear search). 140 | */ 141 | GHashTable *hash = g_hash_table_new(g_str_hash, g_str_equal); 142 | for (size_t i = 0; i < vec->count; i++) { 143 | g_hash_table_insert(hash, vec->buf[i].string, &vec->buf[i]); 144 | } 145 | for (size_t i = 0; i < history->count; i++) { 146 | struct scored_string_ref *res = g_hash_table_lookup(hash, history->buf[i].name); 147 | if (res == NULL) { 148 | continue; 149 | } 150 | res->history_score = history->buf[i].run_count; 151 | } 152 | g_hash_table_unref(hash); 153 | 154 | qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmphistoryp); 155 | } 156 | 157 | void string_vec_uniq(struct string_vec *restrict vec) 158 | { 159 | size_t count = vec->count; 160 | for (size_t i = 1; i < vec->count; i++) { 161 | if (!strcmp(vec->buf[i].string, vec->buf[i-1].string)) { 162 | free(vec->buf[i-1].string); 163 | vec->buf[i-1].string = NULL; 164 | count--; 165 | } 166 | } 167 | string_vec_sort(vec); 168 | vec->count = count; 169 | } 170 | 171 | struct scored_string *string_vec_find_sorted(struct string_vec *restrict vec, const char * str) 172 | { 173 | return bsearch(&str, vec->buf, vec->count, sizeof(vec->buf[0]), cmpstringp); 174 | } 175 | 176 | struct scored_string_ref *string_ref_vec_find_sorted(struct string_ref_vec *restrict vec, const char * str) 177 | { 178 | return bsearch(&str, vec->buf, vec->count, sizeof(vec->buf[0]), cmpstringp); 179 | } 180 | 181 | struct string_ref_vec string_ref_vec_filter( 182 | const struct string_ref_vec *restrict vec, 183 | const char *restrict substr, 184 | enum matching_algorithm algorithm) 185 | { 186 | if (substr[0] == '\0') { 187 | return string_ref_vec_copy(vec); 188 | } 189 | struct string_ref_vec filt = string_ref_vec_create(); 190 | for (size_t i = 0; i < vec->count; i++) { 191 | int32_t search_score; 192 | search_score = match_words(algorithm, substr, vec->buf[i].string); 193 | if (search_score != INT32_MIN) { 194 | string_ref_vec_add(&filt, vec->buf[i].string); 195 | filt.buf[filt.count - 1].search_score = search_score; 196 | filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; 197 | } 198 | } 199 | /* Sort the results by their search score. */ 200 | qsort(filt.buf, filt.count, sizeof(filt.buf[0]), cmpscorep); 201 | return filt; 202 | } 203 | 204 | struct string_ref_vec string_ref_vec_from_buffer(char *buffer) 205 | { 206 | struct string_ref_vec vec = string_ref_vec_create(); 207 | 208 | char *saveptr = NULL; 209 | char *line = strtok_r(buffer, "\n", &saveptr); 210 | while (line != NULL) { 211 | string_ref_vec_add(&vec, line); 212 | line = strtok_r(NULL, "\n", &saveptr); 213 | } 214 | return vec; 215 | } 216 | -------------------------------------------------------------------------------- /src/string_vec.h: -------------------------------------------------------------------------------- 1 | #ifndef STRING_VEC_H 2 | #define STRING_VEC_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "history.h" 9 | #include "matching.h" 10 | 11 | struct scored_string { 12 | char *string; 13 | int32_t search_score; 14 | int32_t history_score; 15 | }; 16 | 17 | struct string_vec { 18 | size_t count; 19 | size_t size; 20 | struct scored_string *buf; 21 | }; 22 | 23 | [[nodiscard("memory leaked")]] 24 | struct string_vec string_vec_create(void); 25 | 26 | void string_vec_destroy(struct string_vec *restrict vec); 27 | 28 | void string_vec_add(struct string_vec *restrict vec, const char *restrict str); 29 | 30 | void string_vec_sort(struct string_vec *restrict vec); 31 | 32 | struct scored_string *string_vec_find_sorted(struct string_vec *restrict vec, const char *str); 33 | 34 | 35 | /* 36 | * Like a string_vec, but only store a reference to the corresponding string 37 | * rather than copying it. Although compatible with the string_vec struct, we 38 | * create a new struct to make the compiler complain if we mix them up. 39 | */ 40 | struct scored_string_ref { 41 | char *string; 42 | int32_t search_score; 43 | int32_t history_score; 44 | }; 45 | 46 | struct string_ref_vec { 47 | size_t count; 48 | size_t size; 49 | struct scored_string_ref *buf; 50 | }; 51 | 52 | /* 53 | * Although some of these functions are identical to the corresponding 54 | * string_vec ones, we create new functions to avoid potentially mixing up 55 | * the two. 56 | */ 57 | [[nodiscard("memory leaked")]] 58 | struct string_ref_vec string_ref_vec_create(void); 59 | 60 | void string_ref_vec_destroy(struct string_ref_vec *restrict vec); 61 | 62 | [[nodiscard("memory leaked")]] 63 | struct string_ref_vec string_ref_vec_copy(const struct string_ref_vec *restrict vec); 64 | 65 | void string_ref_vec_add(struct string_ref_vec *restrict vec, char *restrict str); 66 | 67 | void string_ref_vec_history_sort(struct string_ref_vec *restrict vec, struct history *history); 68 | 69 | void string_vec_uniq(struct string_vec *restrict vec); 70 | 71 | struct scored_string_ref *string_ref_vec_find_sorted(struct string_ref_vec *restrict vec, const char *str); 72 | 73 | [[nodiscard("memory leaked")]] 74 | struct string_ref_vec string_ref_vec_filter( 75 | const struct string_ref_vec *restrict vec, 76 | const char *restrict substr, 77 | enum matching_algorithm algorithm); 78 | 79 | [[nodiscard("memory leaked")]] 80 | struct string_ref_vec string_ref_vec_from_buffer(char *buffer); 81 | 82 | #endif /* STRING_VEC_H */ 83 | -------------------------------------------------------------------------------- /src/surface.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "log.h" 5 | #include "shm.h" 6 | #include "surface.h" 7 | 8 | #undef MAX 9 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 10 | 11 | void surface_init( 12 | struct surface *surface, 13 | struct wl_shm *wl_shm) 14 | { 15 | const int height = surface->height; 16 | const int width = surface->width; 17 | 18 | /* Assume 4 bytes per pixel for WL_SHM_FORMAT_ARGB8888 */ 19 | const int stride = width * 4; 20 | surface->stride = stride; 21 | 22 | /* Double-buffered pool, so allocate space for two windows */ 23 | surface->shm_pool_size = 24 | height 25 | * stride 26 | * 2; 27 | surface->shm_pool_fd = shm_allocate_file(surface->shm_pool_size); 28 | surface->shm_pool_data = mmap( 29 | NULL, 30 | surface->shm_pool_size, 31 | PROT_READ | PROT_WRITE, 32 | MAP_SHARED, 33 | surface->shm_pool_fd, 34 | 0); 35 | #ifdef __linux__ 36 | /* 37 | * On linux, ask for Transparent HugePages if available and our 38 | * buffer's at least 2MiB. This can greatly speed up the first 39 | * cairo_paint() by reducing page faults, but unfortunately is disabled 40 | * for shared memory at the time of writing. 41 | * 42 | * MADV_HUGEPAGE isn't available on *BSD, which we could conceivably be 43 | * running on. 44 | */ 45 | if (surface->shm_pool_size >= (2 << 20)) { 46 | madvise(surface->shm_pool_data, surface->shm_pool_size, MADV_HUGEPAGE); 47 | } 48 | #endif 49 | surface->wl_shm_pool = wl_shm_create_pool( 50 | wl_shm, 51 | surface->shm_pool_fd, 52 | surface->shm_pool_size); 53 | 54 | for (int i = 0; i < 2; i++) { 55 | int offset = height * stride * i; 56 | surface->buffers[i] = wl_shm_pool_create_buffer( 57 | surface->wl_shm_pool, 58 | offset, 59 | width, 60 | height, 61 | stride, 62 | WL_SHM_FORMAT_ARGB8888); 63 | } 64 | 65 | log_debug("Created shm file with size %d KiB.\n", 66 | surface->shm_pool_size / 1024); 67 | } 68 | 69 | void surface_destroy(struct surface *surface) 70 | { 71 | wl_shm_pool_destroy(surface->wl_shm_pool); 72 | munmap(surface->shm_pool_data, surface->shm_pool_size); 73 | surface->shm_pool_data = NULL; 74 | close(surface->shm_pool_fd); 75 | wl_buffer_destroy(surface->buffers[0]); 76 | wl_buffer_destroy(surface->buffers[1]); 77 | } 78 | 79 | void surface_draw(struct surface *surface) 80 | { 81 | wl_surface_attach(surface->wl_surface, surface->buffers[surface->index], 0, 0); 82 | wl_surface_damage_buffer(surface->wl_surface, 0, 0, INT32_MAX, INT32_MAX); 83 | wl_surface_commit(surface->wl_surface); 84 | 85 | surface->index = !surface->index; 86 | } 87 | -------------------------------------------------------------------------------- /src/surface.h: -------------------------------------------------------------------------------- 1 | #ifndef SURFACE_H 2 | #define SURFACE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "color.h" 8 | 9 | struct surface { 10 | struct wl_surface *wl_surface; 11 | struct wl_shm_pool *wl_shm_pool; 12 | int32_t width; 13 | int32_t height; 14 | int32_t stride; 15 | int index; 16 | struct wl_buffer *buffers[2]; 17 | 18 | int shm_pool_size; 19 | int shm_pool_fd; 20 | uint8_t *shm_pool_data; 21 | bool redraw; 22 | }; 23 | 24 | void surface_init( 25 | struct surface *surface, 26 | struct wl_shm *wl_shm); 27 | void surface_destroy(struct surface *surface); 28 | void surface_draw(struct surface *surface); 29 | 30 | #endif /* SURFACE_H */ 31 | -------------------------------------------------------------------------------- /src/tofi.h: -------------------------------------------------------------------------------- 1 | #ifndef TOFI_H 2 | #define TOFI_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "clipboard.h" 9 | #include "color.h" 10 | #include "entry.h" 11 | #include "matching.h" 12 | #include "surface.h" 13 | #include "wlr-layer-shell-unstable-v1.h" 14 | #include "fractional-scale-v1.h" 15 | 16 | #define MAX_OUTPUT_NAME_LEN 256 17 | #define MAX_TERMINAL_NAME_LEN 256 18 | #define MAX_HISTORY_FILE_NAME_LEN 256 19 | 20 | struct output_list_element { 21 | struct wl_list link; 22 | struct wl_output *wl_output; 23 | char *name; 24 | uint32_t width; 25 | uint32_t height; 26 | int32_t scale; 27 | int32_t transform; 28 | }; 29 | 30 | struct tofi { 31 | /* Wayland globals */ 32 | struct wl_display *wl_display; 33 | struct wl_registry *wl_registry; 34 | struct wl_compositor *wl_compositor; 35 | struct wl_seat *wl_seat; 36 | struct wl_shm *wl_shm; 37 | struct wl_data_device_manager *wl_data_device_manager; 38 | struct wl_data_device *wl_data_device; 39 | struct wp_viewporter *wp_viewporter; 40 | struct wp_fractional_scale_manager_v1 *wp_fractional_scale_manager; 41 | struct zwlr_layer_shell_v1 *zwlr_layer_shell; 42 | struct wl_list output_list; 43 | struct output_list_element *default_output; 44 | 45 | /* Wayland objects */ 46 | struct wl_keyboard *wl_keyboard; 47 | struct wl_pointer *wl_pointer; 48 | 49 | /* Keyboard objects */ 50 | char *xkb_keymap_string; 51 | struct xkb_state *xkb_state; 52 | struct xkb_context *xkb_context; 53 | struct xkb_keymap *xkb_keymap; 54 | 55 | /* State */ 56 | bool submit; 57 | bool closed; 58 | int32_t output_width; 59 | int32_t output_height; 60 | struct clipboard clipboard; 61 | struct { 62 | struct surface surface; 63 | struct wp_viewport *wp_viewport; 64 | struct zwlr_layer_surface_v1 *zwlr_layer_surface; 65 | struct entry entry; 66 | uint32_t width; 67 | uint32_t height; 68 | uint32_t scale; 69 | uint32_t fractional_scale; 70 | int32_t transform; 71 | int32_t exclusive_zone; 72 | int32_t margin_top; 73 | int32_t margin_bottom; 74 | int32_t margin_left; 75 | int32_t margin_right; 76 | bool width_is_percent; 77 | bool height_is_percent; 78 | bool exclusive_zone_is_percent; 79 | bool margin_top_is_percent; 80 | bool margin_bottom_is_percent; 81 | bool margin_left_is_percent; 82 | bool margin_right_is_percent; 83 | } window; 84 | struct { 85 | uint32_t rate; 86 | uint32_t delay; 87 | uint32_t keycode; 88 | uint32_t next; 89 | bool active; 90 | } repeat; 91 | 92 | /* Options */ 93 | uint32_t anchor; 94 | enum matching_algorithm matching_algorithm; 95 | bool ascii_input; 96 | bool hide_cursor; 97 | bool use_history; 98 | bool use_scale; 99 | bool late_keyboard_init; 100 | bool drun_launch; 101 | bool drun_print_exec; 102 | bool require_match; 103 | bool auto_accept_single; 104 | bool print_index; 105 | bool multiple_instance; 106 | bool physical_keybindings; 107 | char target_output_name[MAX_OUTPUT_NAME_LEN]; 108 | char default_terminal[MAX_TERMINAL_NAME_LEN]; 109 | char history_file[MAX_HISTORY_FILE_NAME_LEN]; 110 | }; 111 | 112 | #endif /* TOFI_H */ 113 | -------------------------------------------------------------------------------- /src/unicode.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "unicode.h" 5 | 6 | uint8_t utf32_to_utf8(uint32_t c, char *buf) 7 | { 8 | return g_unichar_to_utf8(c, buf); 9 | } 10 | 11 | uint32_t utf8_to_utf32(const char *s) 12 | { 13 | return g_utf8_get_char(s); 14 | } 15 | 16 | uint32_t utf8_to_utf32_validate(const char *s) 17 | { 18 | return g_utf8_get_char_validated(s, -1); 19 | } 20 | 21 | uint32_t *utf8_string_to_utf32_string(const char *s) 22 | { 23 | return g_utf8_to_ucs4_fast(s, -1, NULL); 24 | } 25 | 26 | uint32_t utf32_isprint(uint32_t c) 27 | { 28 | return g_unichar_isprint(c); 29 | } 30 | 31 | uint32_t utf32_isspace(uint32_t c) 32 | { 33 | return g_unichar_isspace(c); 34 | } 35 | 36 | uint32_t utf32_isupper(uint32_t c) 37 | { 38 | return g_unichar_isupper(c); 39 | } 40 | 41 | uint32_t utf32_islower(uint32_t c) 42 | { 43 | return g_unichar_islower(c); 44 | } 45 | 46 | uint32_t utf32_isalnum(uint32_t c) 47 | { 48 | return g_unichar_isalnum(c); 49 | } 50 | 51 | uint32_t utf32_toupper(uint32_t c) 52 | { 53 | return g_unichar_toupper(c); 54 | } 55 | 56 | uint32_t utf32_tolower(uint32_t c) 57 | { 58 | return g_unichar_tolower(c); 59 | } 60 | 61 | size_t utf32_strlen(const uint32_t *s) 62 | { 63 | size_t len = 0; 64 | while (s[len] != U'\0') { 65 | len++; 66 | } 67 | return len; 68 | } 69 | 70 | char *utf8_next_char(const char *s) 71 | { 72 | return g_utf8_next_char(s); 73 | } 74 | 75 | char *utf8_prev_char(const char *s) 76 | { 77 | return g_utf8_prev_char(s); 78 | } 79 | 80 | char *utf8_strchr(const char *s, uint32_t c) 81 | { 82 | return g_utf8_strchr(s, -1, c); 83 | } 84 | 85 | char *utf8_strcasechr(const char *s, uint32_t c) 86 | { 87 | c = g_unichar_tolower(c); 88 | 89 | const char *p = s; 90 | while (*p != '\0' && g_unichar_tolower(g_utf8_get_char(p)) != c) { 91 | p = g_utf8_next_char(p); 92 | } 93 | if (*p == '\0') { 94 | return NULL; 95 | } 96 | return (char *)p; 97 | } 98 | 99 | size_t utf8_strlen(const char *s) 100 | { 101 | return g_utf8_strlen(s, -1); 102 | } 103 | 104 | char *utf8_strcasestr(const char * restrict haystack, const char * restrict needle) 105 | { 106 | char *h = g_utf8_casefold(haystack, -1); 107 | char *n = g_utf8_casefold(needle, -1); 108 | 109 | char *cmp = strstr(h, n); 110 | char *ret; 111 | 112 | if (cmp == NULL) { 113 | ret = NULL; 114 | } else { 115 | ret = (char *)haystack + (cmp - h); 116 | } 117 | 118 | free(h); 119 | free(n); 120 | 121 | return ret; 122 | } 123 | 124 | char *utf8_normalize(const char *s) 125 | { 126 | return g_utf8_normalize(s, -1, G_NORMALIZE_DEFAULT); 127 | } 128 | 129 | char *utf8_compose(const char *s) 130 | { 131 | return g_utf8_normalize(s, -1, G_NORMALIZE_DEFAULT_COMPOSE); 132 | } 133 | 134 | bool utf8_validate(const char *s) 135 | { 136 | return g_utf8_validate(s, -1, NULL); 137 | } 138 | -------------------------------------------------------------------------------- /src/unicode.h: -------------------------------------------------------------------------------- 1 | #ifndef UNICODE_H 2 | #define UNICODE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | uint8_t utf32_to_utf8(uint32_t c, char *buf); 9 | uint32_t utf8_to_utf32(const char *s); 10 | uint32_t utf8_to_utf32_validate(const char *s); 11 | uint32_t *utf8_string_to_utf32_string(const char *s); 12 | 13 | uint32_t utf32_isprint(uint32_t c); 14 | uint32_t utf32_isspace(uint32_t c); 15 | uint32_t utf32_isupper(uint32_t c); 16 | uint32_t utf32_islower(uint32_t c); 17 | uint32_t utf32_isalnum(uint32_t c); 18 | uint32_t utf32_toupper(uint32_t c); 19 | uint32_t utf32_tolower(uint32_t c); 20 | size_t utf32_strlen(const uint32_t *s); 21 | 22 | char *utf8_next_char(const char *s); 23 | char *utf8_prev_char(const char *s); 24 | char *utf8_strchr(const char *s, uint32_t c); 25 | char *utf8_strcasechr(const char *s, uint32_t c); 26 | size_t utf8_strlen(const char *s); 27 | char *utf8_strcasestr(const char * restrict haystack, const char * restrict needle); 28 | char *utf8_normalize(const char *s); 29 | char *utf8_compose(const char *s); 30 | bool utf8_validate(const char *s); 31 | 32 | #endif /* UNICODE_H */ 33 | -------------------------------------------------------------------------------- /src/xmalloc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "log.h" 4 | #include "xmalloc.h" 5 | 6 | void *xmalloc(size_t size) 7 | { 8 | void *ptr = malloc(size); 9 | 10 | if (ptr != NULL) { 11 | //log_debug("Allocated %zu bytes.\n", size); 12 | return ptr; 13 | } else { 14 | log_error("Out of memory, exiting.\n"); 15 | exit(EXIT_FAILURE); 16 | } 17 | } 18 | 19 | void *xcalloc(size_t nmemb, size_t size) 20 | { 21 | void *ptr = calloc(nmemb, size); 22 | 23 | if (ptr != NULL) { 24 | //log_debug("Allocated %zux%zu bytes.\n", nmemb, size); 25 | return ptr; 26 | } else { 27 | log_error("Out of memory, exiting.\n"); 28 | exit(EXIT_FAILURE); 29 | } 30 | } 31 | 32 | void *xrealloc(void *ptr, size_t size) 33 | { 34 | ptr = realloc(ptr, size); 35 | 36 | if (ptr != NULL) { 37 | //log_debug("Reallocated to %zu bytes.\n", size); 38 | return ptr; 39 | } else { 40 | log_error("Out of memory, exiting.\n"); 41 | exit(EXIT_FAILURE); 42 | } 43 | } 44 | 45 | char *xstrdup(const char *s) 46 | { 47 | char *ptr = strdup(s); 48 | 49 | if (ptr != NULL) { 50 | return ptr; 51 | } else { 52 | log_error("Out of memory, exiting.\n"); 53 | exit(EXIT_FAILURE); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/xmalloc.h: -------------------------------------------------------------------------------- 1 | #ifndef XMALLOC_H 2 | #define XMALLOC_H 3 | 4 | #include 5 | 6 | [[nodiscard("memory leaked")]] 7 | [[gnu::malloc]] 8 | void *xmalloc(size_t size); 9 | 10 | [[nodiscard("memory leaked")]] 11 | [[gnu::malloc]] 12 | void *xcalloc(size_t nmemb, size_t size); 13 | 14 | [[nodiscard("memory leaked")]] 15 | void *xrealloc(void *ptr, size_t size); 16 | 17 | [[nodiscard("memory leaked")]] 18 | [[gnu::malloc]] 19 | char *xstrdup(const char *s); 20 | 21 | #endif /* XMALLOC_H */ 22 | -------------------------------------------------------------------------------- /test/config.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "config.h" 7 | #include "tofi.h" 8 | #include "tap.h" 9 | 10 | void is_valid(const char *option, const char *value, const char *message) 11 | { 12 | struct tofi tofi; 13 | bool res = config_apply(&tofi, option, value); 14 | tap_is(res, true, message); 15 | } 16 | 17 | void isnt_valid(const char *option, const char *value, const char *message) 18 | { 19 | struct tofi tofi; 20 | bool res = config_apply(&tofi, option, value); 21 | tap_is(res, false, message); 22 | } 23 | 24 | int main(int argc, char *argv[]) 25 | { 26 | setlocale(LC_ALL, ""); 27 | 28 | tap_version(14); 29 | 30 | /* Anchors */ 31 | is_valid("anchor", "top-left", "Anchor top-left"); 32 | is_valid("anchor", "top", "Anchor top"); 33 | is_valid("anchor", "top-right", "Anchor top-right"); 34 | is_valid("anchor", "right", "Anchor right"); 35 | is_valid("anchor", "bottom-right", "Anchor bottom-right"); 36 | is_valid("anchor", "bottom", "Anchor bottom"); 37 | is_valid("anchor", "bottom-left", "Anchor bottom-left"); 38 | is_valid("anchor", "left", "Anchor left"); 39 | is_valid("anchor", "center", "Anchor center"); 40 | isnt_valid("anchor", "left-bottom", "Invalid anchor"); 41 | 42 | /* Cursor styles */ 43 | is_valid("text-cursor-style", "bar", "Text cursor bar"); 44 | is_valid("text-cursor-style", "block", "Text cursor block"); 45 | is_valid("text-cursor-style", "underscore", "Text cursor underscore"); 46 | isnt_valid("text-cursor-style", "blocky", "Invalid text cursor style"); 47 | 48 | /* Matching algorithms */ 49 | is_valid("matching-algorithm", "normal", "Normal matching"); 50 | is_valid("matching-algorithm", "fuzzy", "Fuzzy matching"); 51 | is_valid("matching-algorithm", "prefix", "Prefix matching"); 52 | isnt_valid("matching-algorithm", "regex", "Regex matching"); 53 | 54 | /* Bools */ 55 | is_valid("horizontal", "tRuE", "Boolean true"); 56 | is_valid("horizontal", "fAlSe", "Boolean false"); 57 | isnt_valid("horizontal", "truefalse", "Invalid boolean"); 58 | 59 | /* Password characters */ 60 | is_valid("hidden-character", "O", "Single Latin character"); 61 | is_valid("hidden-character", "Д", "Single Cyrillic character"); 62 | is_valid("hidden-character", "Ξ", "Single Greek character"); 63 | is_valid("hidden-character", "ọ", "Single character with decomposed diacritic"); 64 | is_valid("hidden-character", "漢", "Single CJK character"); 65 | isnt_valid("hidden-character", "ae", "Multiple characters"); 66 | 67 | /* Colours */ 68 | is_valid("text-color", "46B", "Three character color without hash"); 69 | is_valid("text-color", "#46B", "Three character color with hash"); 70 | is_valid("text-color", "46BA", "Four character color without hash"); 71 | is_valid("text-color", "#46BA", "Four character color with hash"); 72 | is_valid("text-color", "4466BB", "Six character color without hash"); 73 | is_valid("text-color", "#4466BB", "Six character color with hash"); 74 | is_valid("text-color", "4466BBAA", "Eight character color without hash"); 75 | is_valid("text-color", "#4466BBAA", "Eight character color with hash"); 76 | isnt_valid("text-color", "4466BBA", "Five character color without hash"); 77 | isnt_valid("text-color", "#4466BBA", "Five character color with hash"); 78 | isnt_valid("text-color", "9GB", "Three character color with invalid characters"); 79 | isnt_valid("text-color", "95GB", "Four character color with invalid characters"); 80 | isnt_valid("text-color", "95XGUB", "Six character color with invalid characters"); 81 | isnt_valid("text-color", "950-4GBY", "Eight character color with invalid characters"); 82 | isnt_valid("text-color", "-99", "Negative two character color"); 83 | isnt_valid("text-color", "-999", "Negative three character color"); 84 | isnt_valid("text-color", "-9999", "Negative four character color"); 85 | isnt_valid("text-color", "-99999", "Negative five character color"); 86 | isnt_valid("text-color", "-999999", "Negative six character color"); 87 | isnt_valid("text-color", "-9999999", "Negative seven character color"); 88 | isnt_valid("text-color", "-99999999", "Negative eight character color"); 89 | 90 | /* Signed values */ 91 | is_valid("result-spacing", "-2147483648", "INT32 Min"); 92 | is_valid("result-spacing", "2147483647", "INT32 Max"); 93 | isnt_valid("result-spacing", "-2147483649", "INT32 Min - 1"); 94 | isnt_valid("result-spacing", "2147483648", "INT32 Max + 1"); 95 | isnt_valid("result-spacing", "6A", "INT32 invalid character"); 96 | 97 | /* Unsigned values */ 98 | is_valid("corner-radius", "0", "UINT32 0"); 99 | is_valid("corner-radius", "4294967295", "UINT32 Max"); 100 | isnt_valid("corner-radius", "4294967296", "UINT32 Max + 1"); 101 | isnt_valid("corner-radius", "-1", "UINT32 -1"); 102 | isnt_valid("corner-radius", "6A", "UINT32 invalid character"); 103 | 104 | /* Unsigned percentages */ 105 | is_valid("width", "0", "UINT32 0 percent without sign"); 106 | is_valid("width", "0%", "UINT32 0 percent with sign"); 107 | is_valid("width", "4294967295", "UINT32 Max percent without sign"); 108 | is_valid("width", "4294967295%", "UINT32 Max percent with sign"); 109 | isnt_valid("width", "4294967296", "UINT32 Max + 1 percent without sign"); 110 | isnt_valid("width", "4294967296%", "UINT32 Max + 1 percent with sign"); 111 | isnt_valid("width", "-1", "UINT32 -1 percent without sign"); 112 | isnt_valid("width", "-1%", "UINT32 -1 percent with sign"); 113 | 114 | /* Directional values */ 115 | is_valid("prompt-background-padding", "0", "Single directional value"); 116 | is_valid("prompt-background-padding", "0,1", "Two directional values"); 117 | is_valid("prompt-background-padding", "0,1,-2", "Three directional values"); 118 | is_valid("prompt-background-padding", "0,1,-2,3", "Four directional values"); 119 | isnt_valid("prompt-background-padding", "0,1,-2,3,-4", "Five directional values"); 120 | isnt_valid("prompt-background-padding", "0,1,-2,3,-4,5", "Six directional values"); 121 | 122 | tap_plan(); 123 | 124 | return EXIT_SUCCESS; 125 | } 126 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | tests = [ 2 | 'config', 3 | 'utf8' 4 | ] 5 | 6 | foreach test_file : tests 7 | t = executable( 8 | test_file, 9 | files(test_file + '.c', 'tap.c'), common_sources, wl_proto_src, wl_proto_headers, 10 | include_directories: ['../src'], 11 | dependencies: [librt, libm, freetype, harfbuzz, cairo, pangocairo, wayland_client, xkbcommon, glib, gio_unix], 12 | install: false 13 | ) 14 | 15 | test(test_file, t, protocol: 'tap') 16 | endforeach 17 | -------------------------------------------------------------------------------- /test/tap.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | static size_t test = 0; 7 | static char *todo = NULL; 8 | 9 | void tap_version(size_t version) 10 | { 11 | printf("TAP version %zu\n", version); 12 | } 13 | 14 | void tap_plan() 15 | { 16 | printf("1..%zu\n", test); 17 | } 18 | 19 | void tap_ok(const char *message, ...) 20 | { 21 | va_list args; 22 | va_start(args, message); 23 | printf("ok %zu - ", ++test); 24 | vprintf(message, args); 25 | if (todo != NULL) { 26 | printf(" # TODO %s", todo); 27 | free(todo); 28 | todo = NULL; 29 | } 30 | printf("\n"); 31 | va_end(args); 32 | } 33 | 34 | void tap_not_ok(const char *message, ...) 35 | { 36 | va_list args; 37 | va_start(args, message); 38 | printf("not ok %zu - ", ++test); 39 | vprintf(message, args); 40 | if (todo != NULL) { 41 | printf(" # TODO %s", todo); 42 | free(todo); 43 | todo = NULL; 44 | } 45 | printf("\n"); 46 | va_end(args); 47 | } 48 | 49 | void tap_todo(const char *message) 50 | { 51 | todo = strdup(message); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /test/tap.h: -------------------------------------------------------------------------------- 1 | #ifndef TAP_H 2 | #define TAP_H 3 | 4 | #include 5 | 6 | #define tap_is(a, b, message) ((a) == (b) ? tap_ok((message)) : tap_not_ok((message))) 7 | #define tap_isnt(a, b, message) ((a) != (b) ? tap_ok((message)) : tap_not_ok((message))) 8 | 9 | void tap_version(size_t version); 10 | void tap_plan(void); 11 | void tap_ok(const char *message, ...); 12 | void tap_not_ok(const char *message, ...); 13 | void tap_todo(const char *message); 14 | 15 | #endif /* TAP_H */ 16 | -------------------------------------------------------------------------------- /test/utf8.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "matching.h" 7 | #include "tap.h" 8 | 9 | void is_single_match(enum matching_algorithm algorithm, const char *pattern, const char *str, const char *message) 10 | { 11 | int32_t res = match_words(algorithm, pattern, str); 12 | tap_isnt(res, INT32_MIN, message); 13 | } 14 | 15 | void isnt_single_match(enum matching_algorithm algorithm, const char *pattern, const char *str, const char *message) 16 | { 17 | int32_t res = match_words(algorithm, pattern, str); 18 | tap_is(res, INT32_MIN, message); 19 | } 20 | 21 | void is_match(const char *pattern, const char *str, const char *message) 22 | { 23 | is_single_match(MATCHING_ALGORITHM_NORMAL, pattern, str, message); 24 | is_single_match(MATCHING_ALGORITHM_PREFIX, pattern, str, message); 25 | is_single_match(MATCHING_ALGORITHM_FUZZY, pattern, str, message); 26 | } 27 | 28 | void isnt_match(const char *pattern, const char *str, const char *message) 29 | { 30 | isnt_single_match(MATCHING_ALGORITHM_NORMAL, pattern, str, message); 31 | isnt_single_match(MATCHING_ALGORITHM_PREFIX, pattern, str, message); 32 | isnt_single_match(MATCHING_ALGORITHM_FUZZY, pattern, str, message); 33 | } 34 | 35 | int main(int argc, char *argv[]) 36 | { 37 | setlocale(LC_ALL, ""); 38 | 39 | tap_version(14); 40 | 41 | /* Case insensitivity. */ 42 | is_match("o", "O", "Single Latin character, different case"); 43 | is_match("д", "Д", "Single Cyrillic character, different case"); 44 | is_match("ξ", "Ξ", "Single Greek character, different case"); 45 | is_match("o", "ọ", "Single character with decomposed diacritic"); 46 | 47 | /* Combining diacritics. */ 48 | isnt_match("o", "ọ", "Single character with composed diacritic"); 49 | isnt_single_match(MATCHING_ALGORITHM_NORMAL, "ạ", "aọ", "Decomposed diacritics, character mismatch"); 50 | tap_todo("Needs composed character comparison"); 51 | isnt_single_match(MATCHING_ALGORITHM_FUZZY, "ạ", "aọ", "Decomposed diacritics, character mismatch"); 52 | 53 | tap_plan(); 54 | 55 | return EXIT_SUCCESS; 56 | } 57 | -------------------------------------------------------------------------------- /themes/dark-paper: -------------------------------------------------------------------------------- 1 | font = Fanwood Text 2 | font-size = 64 3 | 4 | outline-width = 0 5 | border-width = 0 6 | padding-left = 4% 7 | padding-top = 2% 8 | padding-right = 0 9 | padding-bottom = 0 10 | 11 | background-color = #111 12 | text-color = #f9fbff 13 | selection-color = #933 14 | 15 | width = 100% 16 | height = 100% 17 | 18 | hide-cursor = true 19 | -------------------------------------------------------------------------------- /themes/dmenu: -------------------------------------------------------------------------------- 1 | anchor = top 2 | width = 100% 3 | height = 30 4 | horizontal = true 5 | font-size = 14 6 | prompt-text = " run: " 7 | font = monospace 8 | outline-width = 0 9 | border-width = 0 10 | background-color = #000000 11 | min-input-width = 120 12 | result-spacing = 15 13 | padding-top = 0 14 | padding-bottom = 0 15 | padding-left = 0 16 | padding-right = 0 17 | -------------------------------------------------------------------------------- /themes/dos: -------------------------------------------------------------------------------- 1 | font = VT323 2 | corner-radius = 60 3 | outline-color = #D3D1B9 4 | outline-width = 3 5 | border-color = #E3E1C9 6 | border-width = 60 7 | background-color = #000000 8 | text-color = #0A3 9 | selection-color = #0F6 10 | prompt-text = "C:\> " 11 | num-results = 9 12 | hide-cursor = true 13 | width = 640 14 | height = 480 15 | -------------------------------------------------------------------------------- /themes/fullscreen: -------------------------------------------------------------------------------- 1 | width = 100% 2 | height = 100% 3 | border-width = 0 4 | outline-width = 0 5 | padding-left = 35% 6 | padding-top = 35% 7 | result-spacing = 25 8 | num-results = 5 9 | font = monospace 10 | background-color = #000A 11 | -------------------------------------------------------------------------------- /themes/soy-milk: -------------------------------------------------------------------------------- 1 | # Font 2 | font = Fredoka One 3 | font-size = 20 4 | 5 | # Window Style 6 | horizontal = true 7 | anchor = top 8 | width = 100% 9 | height = 48 10 | 11 | outline-width = 0 12 | border-width = 0 13 | min-input-width = 120 14 | result-spacing = 30 15 | padding-top = 8 16 | padding-bottom = 0 17 | padding-left = 20 18 | padding-right = 0 19 | 20 | # Text style 21 | prompt-text = "Can I have a" 22 | prompt-padding = 30 23 | 24 | background-color = #fff0dc 25 | text-color = #4280a0 26 | 27 | prompt-background = #eebab1 28 | prompt-background-padding = 4, 10 29 | prompt-background-corner-radius = 12 30 | 31 | input-color = #e1666a 32 | input-background = #f4cf42 33 | input-background-padding = 4, 10 34 | input-background-corner-radius = 12 35 | 36 | alternate-result-background = #b8daf3 37 | alternate-result-background-padding = 4, 10 38 | alternate-result-background-corner-radius = 12 39 | 40 | selection-color = #f0d2af 41 | selection-background = #da5d64 42 | selection-background-padding = 4, 10 43 | selection-background-corner-radius = 12 44 | selection-match-color = #fff 45 | 46 | clip-to-padding = false 47 | --------------------------------------------------------------------------------