pax_global_header00006660000000000000000000000064144147415140014517gustar00rootroot0000000000000052 comment=71a4801d20d8904cfcfa5e92c96d53ee06a2c69f tofi-0.9.1/000077500000000000000000000000001441474151400124675ustar00rootroot00000000000000tofi-0.9.1/.github/000077500000000000000000000000001441474151400140275ustar00rootroot00000000000000tofi-0.9.1/.github/workflows/000077500000000000000000000000001441474151400160645ustar00rootroot00000000000000tofi-0.9.1/.github/workflows/build-test.yml000066400000000000000000000050251441474151400206650ustar00rootroot00000000000000name: Test build process on: push: paths-ignore: - 'README.md' - 'doc' - 'themes' jobs: ubuntu-20_04: runs-on: ubuntu-20.04 steps: - name: Checkout repo uses: actions/checkout@v3 - name: Install dependencies run: | sudo apt-get update sudo apt-get install build-essential llvm clang meson scdoc \ ninja-build libfreetype-dev libharfbuzz-dev libcairo-dev \ libpango1.0-dev libwayland-dev wayland-protocols libxkbcommon-dev \ python3-pip sudo pip3 install --upgrade meson # The version of GCC on Ubuntu 20.04 is too old to recognise # [[attribute]] syntax, so skip the build there. # # Additionally, clang will raise unknown attribute errors, so don't # enable werror. - name: Set clang as default compiler run: sudo update-alternatives --set cc $(which clang) - name: Clang LTO build run: | meson build ninja -C build test rm -rf build - name: Clang non-LTO build run: | meson build meson configure build -Db_lto=false ninja -C build test rm -rf build ubuntu-22_04: runs-on: ubuntu-22.04 steps: - name: Checkout repo uses: actions/checkout@v3 - name: Install dependencies run: | sudo apt-get update sudo apt-get install build-essential llvm clang meson scdoc \ ninja-build libfreetype-dev libharfbuzz-dev libcairo-dev \ libpango1.0-dev libwayland-dev wayland-protocols libxkbcommon-dev \ python3-pip sudo pip3 install --upgrade meson - name: GCC LTO build run: | meson build meson configure build -Dwerror=true ninja -C build test rm -rf build - name: GCC non-LTO build run: | meson build meson configure build -Dwerror=true meson configure build -Db_lto=false ninja -C build test rm -rf build - name: Set clang as default compiler run: sudo update-alternatives --set cc $(which clang) - name: Clang LTO build run: | meson build meson configure build -Dwerror=true ninja -C build test rm -rf build - name: Clang non-LTO build run: | meson build meson configure build -Dwerror=true meson configure build -Db_lto=false ninja -C build test rm -rf build tofi-0.9.1/.gitignore000066400000000000000000000010201441474151400144500ustar00rootroot00000000000000# Prerequisites *.d # Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ *.su *.idb *.pdb # Kernel Module Compile Results *.mod* *.cmd .tmp_versions/ modules.order Module.symvers Mkfile.old dkms.conf # Vim files *.swp *.taghl tags # Mac OS files .DS_Store # Project specific files build/ .cache tofi-0.9.1/CHANGELOG.md000066400000000000000000000267671441474151400143220ustar00rootroot00000000000000# Changelog ## [0.9.1] - 2023-04-10 ### Fixed - Fixed broken line spacing for some fonts with the HarfBuzz backend. ## [0.9.0] - 2023-04-09 ### Added - Added support for a text cursor. This can be enabled with the `text-cursor` option, the style of cursor chosen with `text-cursor-style`, and themed similarly to other text. - Added support for fractional scaling, correcting the behaviour of percentage sizes when fractional scaling is used. - Added Ctrl-n, Ctrl-p, Page-Up and Page-Down keybindings. - Added `auto-accept-single` option, to automatically accept the last remaining result when there is only one. ### Changed - The `font` option now performs home path substitution for paths starting with `~/`. ### Fixed - Fixed some more potential errors from malformed config files. - Fixed some potential memory leaks when generating caches. - Fixed rounded corners when a background padding of -1 is specified. - Fixed broken text rendering with some versions of Harfbuzz. - Fixed some man page typos. ## [0.8.1] - 2022-12-01 ### Fixed - Stop debug logs printing in release builds. ## [0.8.0] - 2022-12-01 ### Deprecated Text styling has been overhauled in this update, and as a result the `selection-padding` option has been replaced with `selection-background-padding`, to avoid ambiguity and match the other available options. `selection-padding` is therefore deprecated, and will be removed in a future version of tofi, so please update your configs. ### Added - Added `placeholder-text` option. - Overhaul text styling. Each piece of text in tofi is now styleable in a similar way, with foreground and background colours. The pieces of text that can be individually styled are: - `prompt` - `placeholder` - `input` - `default-result` - `alternate-result` - `selection` Each of these pieces of text now has the following options available: - `-color` - `-background` - `-background-padding` - `-background-corner-radius` See `man 5 tofi` or the example config file for more information. - Added ability to paste from the clipboard with `ctrl-v`. - Added `history-file` option. This both allows changing the history file location, and when combined with `history=true` (the default), enables history sorting in plain `tofi` mode. - Added `font-features` option, allowing the specification of OpenType font features in a similar way to CSS `font-feature-settings`. - Added `font-variations` option, allowing customisation of variable fonts in a similar way to CSS `font-variation-settings`. - Added `clip-to-padding` option, to allow drawing text outside the specified padding. ### Changed - Due to the number of available options, the usage info now only contains a short list of the most important ones and directs the user to `man 5 tofi`. - If `-h` was passed, print usage info to `stdout` rather than `stderr`. - Improved performance of most text handling operations. - Improved performance of `selection-background` and others, so drawing backgrounds is no longer an expensive operation. ### Fixed - Invalid values in config options no longer set the option to a default value. - Fix various potential errors due to malformed config files. - Fixed a compilation error on FreeBSD (from [@jbeich](https://github.com/jbeich)). - Fixed a compilation error with musl libc (from [@akdjka](https://github.com/akdjka)). ## [0.7.0] - 2022-11-01 ### Added - Added `include` option, allowing config files to include other files. - Added `hide-input` and `hidden-character` options for sensitive input. - Added `exclusive-zone` option, to control interaction with menu bars etc. - Added `terminal` option to allow `tofi-drun` to launch terminal apps. - Added a couple of extra keybindings. ### Changed - Searching is now Unicode aware, so case-insensitive matching of non-Latin characters should work. - Fuzzy matching will now use a simpler algorithm when matching lines more than 100 characters in length to avoid slowdowns. - By default, tofi will now refuse to start if another instance is already running, preventing accidental double launches. This can be changed with the `multi-instance` option. - Tofi will now show up on top of fullscreen windows. ### Fixed - Keyboard shortcuts are now bound to physical keys rather than characters, so should not change places when changing keyboard layouts. - Fix crash when attempting to change the selection while no results are displayed. - Fixed a rare issue where input could become out of sync with the display. ## [0.6.0] - 2022-09-08 ### Warning - HiDPI config change In the [0.5.0] release, the `scale` option was added to enable scaling of pixel values by the display's scale factor. In this release, the default value of `scale` has changed to be `true`, and fonts are no longer scaled if `scale` is set to `false`. This makes tofi's behaviour match that of e.g. Sway, and makes configs work more reliably on monitors with different scale factors. If you use tofi on a HiDPI display, you may need to change your config's pixel values to make things look right again. ### Added - Added `require-match` option, to allow printing of input even when there are no matching results. - Added `prompt-padding` option for more flexible spacing between the prompt and other text. - Added a new example theme, dark-paper. ### Changed - The `scale` option now defaults to `true`, as noted above. The example themes have been updated to account for this change. - Spaces are now allowed as part of normal input. Similarly to dmenu, tofi will split the input into words, and only show results for which every word matches individually. - Split `tofi(5)` manpage into behaviour and style options to make finding options easier. ### Fixed - Fixed build failure when link-time optimisation is disabled. ## [0.5.0] - 2022-08-21 ### Warning - HiDPI config change In previous versions of tofi, pixel values were always treated as device pixels, ignoring the display's scale factor. This allows pixel-perfect sizes, but means you have to make different configs for differently scaled displays, and isn't how e.g. Sway does things. Additionally, fonts currently *are* scaled by the scale factor, making things a little complex. This release adds a `scale` boolean option, which currently defaults to `false`. Setting this to `true` will make pixel values scale with the display's scale factor. In the next version of tofi, `scale` will default to `true` (but still be around if you want the old behaviour). Setting `scale` to `false` will also start causing fonts to not be scaled by the scale factor. If you use tofi on a HiDPI display, you should explicitly set `scale` to your desired setting now (or at least be aware that you'll need to change some theme dimensions in the next version). ### Added - Fuzzy matching can now be enabled with the `fuzzy_match` option. - Added `scale` option, as described above. - Added Ctrl-u and Ctrl-w readline keybindings. - Added this changelog. ### Changed - Improved performance when neither `selection-match-color` or `selection-background-color` are specified. - Improved performance on systems with Transparent HugePages enabled for shared memory (none that I know of for now, but may be relevant in the future). ## [0.4.0] - 2022-08-07 ### Deprecated In the [0.3.0] release, the `drun-print-exec` option was added to enable fixed `tofi-drun` behaviour. This release changes this to be the default, as this is how it should have been done from the start. Consequently, the `drun-print-exec` option is now obsolete, and may be removed in the future, so you can safely delete it from your configs. ### Added - A full example config file is included in `doc/config`, and is installed to `/etc/xdg/tofi/config` - Added `selection-padding` option, to make the selected item background wider. - Added `selection-match-color` option, to highlight the matching portion of the selected result. - Added key-repeat. - Added result pagination. ### Removed - `tofi-compgen` is no longer installed, as it's really just a debugging utility and not needed. ### Changed - `drun-print-exec` is now always set to true, and the option is deprecated. - The `output` and `late-keyboard-init` options are no longer command-line only, and so can be specified in the config file. ### Fixed - Fixed slanted fonts being cut off if they extend back towards the prompt. - The selection background now correctly wraps slanted fonts. - Enable compilation with some older Wayland versions (specifically that found on Ubuntu 20.04). ## [0.3.1] - 2022-07-28 ### Fixed - Fix some program arguments not working in drun mode. ## [0.3.0] - 2022-07-27 ### Deprecated Previously, tofi-drun would print the filename of the selected .desktop file to stdout. This could then be passed to `xargs swaymsg exec gio launch` to be executed. The problem is that this ends up defeating the purpose of passing the command to swaymsg exec, and the workspace the command was selected on may not be the one that it starts up on, if for example it takes a long time and the user switches workspaces in the meantime. The solution is to instead print the Exec= line from the .desktop file, and pass that directly to `xargs swaymsg exec --` for execution. To avoid too much breaking of configs for the few people who use tofi currently, this release adds a new option, `--drun-print-exec`, to enable the fixed behaviour. The next release will change this to be the default, as this is how it should have been done from the start. ### Added - Tofi will now automatically detect how many results can be displayed if `--num-results=0` is set (the new default). - When running in drun mode, keywords will also be matched along with the name (so e.g. searching for "web" will return "firefox") - Add `--drun-print-exec` option, as noted above. ### Fixed - Fixed percentage values passed to margin options not behaving correctly when output scaling is enabled. - Fix tofi not grabbing keyboard focus on River. - Correct `--font` option name in man page. - Fix percentage values on vertical monitors. - Fix drun mode weirdness when history is enabled ## [0.2.0] - 2022-07-25 ### Added - `tofi-drun` mode for launching apps from `.desktop` files. - Navigation keybindings for `Ctrl-j`, `Ctrl-k` and `Tab`. ### Changed - Search results now prioritise matches early in a word, e.g. a search for `fire` yields `firefox` before `aafire`. ### Fixed - Fixed input / display sometimes going out of sync. - Add dependency on `librt` for systems that require that (from [@sktt](https://github.com/sktt)). - Allow for a count of more than 128 program launches in the history file. ## [0.1.1] - 2022-06-28 ### Fixed - Fix typo in `meson.build`. ## [0.1.0] - 2022-06-27 Initial release. Good enough to use, but still some jank. [0.9.1]: https://github.com/philj56/tofi/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/philj56/tofi/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/philj56/tofi/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/philj56/tofi/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/philj56/tofi/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/philj56/tofi/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/philj56/tofi/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/philj56/tofi/compare/v0.3.1...v0.4.0 [0.3.1]: https://github.com/philj56/tofi/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/philj56/tofi/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/philj56/tofi/compare/v0.2.0...v0.1.1 [0.1.1]: https://github.com/philj56/tofi/compare/v0.1.1...v0.1.0 [0.1.0]: https://github.com/philj56/tofi/releases/tag/v0.1.0 tofi-0.9.1/LICENSE000066400000000000000000000020451441474151400134750ustar00rootroot00000000000000Copyright (C) 2021-2022 Philip Jones Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tofi-0.9.1/README.md000066400000000000000000000326261441474151400137570ustar00rootroot00000000000000# Tofi An extremely fast and simple [dmenu](https://tools.suckless.org/dmenu/) / [rofi](https://github.com/davatorium/rofi) replacement for [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots)-based [Wayland](https://wayland.freedesktop.org/) compositors such as [Sway](https://github.com/swaywm/sway/). The aim is to do just what I want it to as quick as possible. When [configured correctly](#performance), tofi can get on screen within a single frame. ![](screenshot_fullscreen.png) ## Table of Contents * [Install](#install) * [Building](#building) * [Arch](#arch) * [Usage](#usage) * [Theming](#theming) * [Performance](#performance) * [Options](#options) * [Benchmarks](#benchmarks) * [Where is the time spent?](#where-is-the-time-spent) ## Install ### Building Install the necessary dependencies. #### For Arch: ```sh # Runtime dependencies sudo pacman -S freetype2 harfbuzz cairo pango wayland libxkbcommon # Build-time dependencies sudo pacman -S meson scdoc wayland-protocols ``` #### For Fedora ```sh # Runtime dependencies sudo dnf install freetype-devel cairo-devel pango-devel wayland-devel libxkbcommon-devel harfbuzz # Build-time dependencies sudo dnf install meson scdoc wayland-protocols-devel ``` #### For Debian/Ubuntu ```sh # Runtime dependencies sudo apt install libfreetype-dev libcairo2-dev libpango1.0-dev libwayland-dev libxkbcommon-dev libharfbuzz-dev # Build-time dependencies sudo apt install meson scdoc wayland-protocols ``` Then build: ```sh meson build && ninja -C build install ``` ### Arch Tofi is available in the [AUR](https://aur.archlinux.org/packages/tofi): ```sh paru -S tofi ``` ## Usage By default, running `tofi` causes it to act like dmenu, accepting options on `stdin` and printing the selection to `stdout`. `tofi-run` is a symlink to `tofi`, which will cause tofi to display a list of executables under the user's `$PATH`. `tofi-drun` is also a symlink to `tofi`, which will cause tofi to display a list of applications found in desktop files as described by the [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html). To use as a launcher for Sway, add something similar to the following to your Sway config file: ``` set $menu tofi-run | xargs swaymsg exec -- bindsym $mod+d exec $menu ``` For `tofi-drun`, there are two possible methods: ``` # Launch via Sway set $drun tofi-drun | xargs swaymsg exec -- bindsym $mod+Shift+d exec $drun # Launch directly set $drun tofi-drun --drun-launch=true bindsym $mod+Shift+d exec $drun ``` See the main [manpage](doc/tofi.1.md) for more info. ### Theming Tofi supports a fair number of theming options - see the default [config file](doc/config) config file or the config file [manpage](doc/tofi.5.md) for a complete description. Theming is based on the box model shown below: ![Default theme screenshot](screenshot_default.png) This consists of a box with a border, border outlines and optionally rounded corners. Text inside the box can either be laid out vertically: ``` ╔═══════════════════╗ ║ prompt input ║ ║ result 1 ║ ║ result 2 ║ ║ ... ║ ╚═══════════════════╝ ``` or horizontally: ``` ╔═══════════════════════════════════════════╗ ║ prompt input result 1 result 2 ... ║ ╚═══════════════════════════════════════════╝ ``` Each piece of text can have its colour customised, and be surrounded by a box with optionally rounded corners, A few example themes are included and shown below. Note that you may need to tweak them to look correct on your display. [`themes/fullscreen`](themes/fullscreen) ![Fullscreen theme screenshot](screenshot_fullscreen.png) [`themes/dmenu`](themes/dmenu) ![dmenu theme screenshot](screenshot_dmenu.png) [`themes/dos`](themes/dos) ![DOS theme screenshot](screenshot_dos.png) [`themes/dark-paper`](themes/dark-paper) ![Dark paper theme screenshot](screenshot_dark_paper.png) [`themes/soy-milk`](themes/soy-milk) ![Soy milk theme screenshot](screenshot_soy_milk.png) ## Performance By default, tofi isn't really any faster than its alternatives. However, when configured correctly, it can startup and get on screen within a single frame, or about 2ms in the ideal case. ### Options In roughly descending order, the most important options for performance are: * `--font` - This is *by far* the most important option. By default, tofi uses [Pango](https://pango.gnome.org/) for font rendering, which (on Linux) looks up fonts via [Fontconfig](https://www.freedesktop.org/wiki/Software/fontconfig/). Unfortunately, this font lookup is about as slow as wading through treacle (relatively speaking). On battery power on my laptop (Arch linux, AMD Ryzen 5 5600U), with ~10000 fonts as the output of `fc-list`, loading a single font with Pango & Fontconfig takes ~120ms. The solution is to pass a path to a font file to `--font`, e.g. `--font /usr/share/fonts/noto/NotoSansMono-Regular.ttf`. Tofi will then skip any font searching, and use [Harfbuzz](https://harfbuzz.github.io/) and [Cairo](https://www.cairographics.org/) directly to load the font and display text. This massively speeds up startup (font loading takes <1ms). The (minor for me) downside is that any character not in the specified font won't render correctly, but unless you have commands (or items) with CJK characters or emojis in their names, that shouldn't be an issue. * `--width`, `--height` - Larger windows take longer to draw (mostly just for the first frame). Again, on battery power on my laptop, drawing a fullscreen window (2880px × 1800px) takes ~20ms on the first frame, whereas a dmenu-like ribbon (2880px × 60px) takes ~1ms. * `--num-results` - By default, tofi auto-detects how many results will fit in the window. This is quite tricky when `--horizontal=true` is passed, and leads to a few ms slowdown (only in this case). Setting a fixed number of results will speed this up, but since this likely only applies to dmenu-like themes (which are already very quick) it's probably not worth setting this. * `--*-background` - Drawing background boxes around text effectively requires drawing the text twice, so specifying a lot of these options can lead to a couple of ms slowdown. * `--hint-font` - Getting really into it now, one of the remaining slow points is hinting fonts. For the dmenu theme on battery power on my laptop, with a specific font file chosen, the initial text render with the default font hinting takes ~4-6ms. Specifying `--hint-font false` drops this to ~1ms. For hidpi screens or large font sizes, this doesn't noticeably impact font sharpness, but your mileage may vary. This option has no effect if a path to a font file hasn't been passed to `--font`. * `--ascii-input` - Proper Unicode handling is slower than plain ASCII - on the order of a few ms for ~40 kB of input. Specifying `--ascii-input true` will disable some of this handling, speeding up tofi's startup, but searching for non-ASCII characters may not work properly. * `--late-keyboard-init` - The last avoidable thing that slows down startup is initialisation of the keyboard. This only takes 1-2ms on my laptop, but up to 60ms on a Raspberry Pi Zero 2 W. Passing this option will delay keyboard initialisation until after the first draw to screen, meaning that *keypresses will be missed* until then, so it's disabled by default. ### Benchmarks Below are some rough benchmarks of the included themes on different machines. These were generated with version 0.1.0 of tofi. The time shown is measured from program launch to Sway reporting that the window has entered the screen. Results are the mean and standard deviation of 10 runs. All tests were performed with `--font /path/to/font/file.ttf`, `--hint-font false` and the equivalent of `--ascii-input true` (as tofi 0.1.0 didn't support Unicode text).
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
The table below additionally includes `--late-keyboard-init` in the arguments.
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
#### Bonus Round: Transparent HugePages It turns out that it's possible to speed up fullscreen windows somewhat with some advanced memory tweaks. See [this Stack Overflow question](https://stackoverflow.com/questions/73278608/can-mmaps-performance-be-improved-for-shared-memory) if you want full details, but basically by setting `/sys/kernel/mm/transparent_hugepage/shmem_enabled` to `advise`, we can tell the kernel we're going to be working with large memory areas. This results in fewer page faults when first allocating memory, speeding up tofi. Note that I don't recommend you play with this unless you know what you're doing (I don't), but I've included it just in case, and to show that the slowdown on large screens is partially due to factors beyond tofi's control. The table below shows the effects of additionally enabling hugepages from the table above. The dmenu theme has been skipped, as the window it creates is too small to benefit from them. The Raspberry Pi is also omitted, as it doesn't support hugepages.
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
### Where is the time spent? For those who are interested in how much time there is even left to save, I've plotted the startup performance of version 0.8.0 of `tofi-run` below, alongside the corresponding debug output. This is the data from 1000 runs of the dmenu theme on a Ryzen 7 3700X machine, with all performance options set as mentioned above, along with `--num-results 10`. I've highlighted some points of interest, most of which are out of tofi's control. [![Startup performance plot](startup_performance.svg)](https://raw.githubusercontent.com/philj56/tofi/master/startup_performance.svg) (You may want to click the image to see it at full size). Note that this is slightly faster than shown in previous benchmarks (with some runs under 1.5ms!), due to some string handling improvements made in version 0.8.0. Also note that the real performance is slightly better still, as the performance logging used slows down the code by roughly 10%. As you can see, there's not a huge amount of time that could even theoretically be saved. Somewhere around 50% of the startup time is simply spent waiting, and most of the code isn't parallelisable, as many steps depend on the result of previous steps. One idea would be to daemonize tofi, skipping much of this startup. I don't want to do this, however, for two main reasons: complexity, and I think it's probably about fast enough already! tofi-0.9.1/completions/000077500000000000000000000000001441474151400150235ustar00rootroot00000000000000tofi-0.9.1/completions/tofi000066400000000000000000000042241441474151400157110ustar00rootroot00000000000000# vi: ft=bash _tofi() { local cur prev words cword _init_completion || return words=( --help --config --include --output --scale --anchor --background-color --corner-radius --font --font-size --font-features --font-variations --num-results --selection-color --selection-match-color --selection-background --selection-background-padding --selection-background-corner-radius --outline-width --outline-color --text-cursor --text-cursor-style --text-cursor-color --text-cursor-background --text-cursor-corner-radius --text-cursor-thickness --prompt-text --prompt-padding --prompt-color --prompt-background --prompt-background-padding --prompt-background-corner-radius --placeholder-text --placeholder-color --placeholder-background --placeholder-background-padding --placeholder-background-corner-radius --input-color --input-background --input-background-padding --input-background-corner-radius --default-result-color --default-result-background --default-result-background-padding --default-result-background-corner-radius --alternate-result-color --alternate-result-background --alternate-result-background-padding --alternate-result-background-corner-radius --result-spacing --min-input-width --border-width --border-color --text-color --width --height --exclusive-zone --margin-top --margin-bottom --margin-left --margin-right --padding-top --padding-bottom --padding-left --padding-right --clip-to-padding --horizontal --hide-cursor --history --history-file --fuzzy-match --require-match --auto-accept-single --hide-input --hidden-character --drun-launch --terminal --hint-font --late-keyboard-init --multi-instance --ascii-input ) case "${prev}" in --font) ;& --history-file) ;& --include) ;& --config|-c) _filedir return 0 ;; --help|-h) ;; --*) return 0 ;; esac case "${cur}" in -[ch]) COMPREPLY=($cur) ;; *) COMPREPLY=($(compgen -W "${words[*]}" -- ${cur})) return 0 ;; esac } complete -F _tofi tofi complete -F _tofi tofi-run complete -F _tofi tofi-drun tofi-0.9.1/doc/000077500000000000000000000000001441474151400132345ustar00rootroot00000000000000tofi-0.9.1/doc/config000066400000000000000000000202741441474151400144310ustar00rootroot00000000000000# Default config for tofi # # Copy this file to ~/.config/tofi/config and get customising! # # A complete reference of available options can be found in `man 5 tofi`. # ### Fonts # # Font to use, either a path to a font file or a name. # # If a path is given, tofi will startup much quicker, but any # characters not in the chosen font will fail to render. # # Otherwise, fonts are interpreted in Pango format. font = "Sans" # Point size of text. font-size = 24 # Comma separated list of OpenType font feature settings to apply, # if supported by the chosen font. The format is similar to the CSS # "font-feature-settings" property. # # Examples: # # font-features = "smcp, c2sc" (all small caps) # font-features = "liga 0" (disable ligatures) font-features = "" # Comma separated list of OpenType font variation settings to apply # to variable fonts. The format is similar to the CSS # "font-variation-settings" property. # # Examples: # # font-variations = "wght 900" (Extra bold) # font-variations = "wdth 25, slnt -10" (Narrow and slanted) font-variations = "" # Perform font hinting. Only applies when a path to a font has been # specified via `font`. Disabling font hinting speeds up text # rendering appreciably, but will likely look poor at small font pixel # sizes. hint-font = true # ### Text theming # # Default text color # # All text defaults to this color if not otherwise specified. text-color = #FFFFFF # All pieces of text have the same theming attributes available: # # *-color # Foreground color # # *-background # Background color # # *-background-padding # Background padding in pixels (comma-delimited, CSS-style list). # See "DIRECTIONAL VALUES" under `man 5 tofi` for more info. # # *-background-corner-radius # Radius of background box corners in pixels # Prompt text theme # prompt-color = #FFFFFF prompt-background = #00000000 prompt-background-padding = 0 prompt-background-corner-radius = 0 # Placeholder text theme placeholder-color = #FFFFFFA8 placeholder-background = #00000000 placeholder-background-padding = 0 placeholder-background-corner-radius = 0 # Input text theme # input-color = #FFFFFF input-background = #00000000 input-background-padding = 0 input-background-corner-radius = 0 # Default result text theme # default-result-color = #FFFFFF default-result-background = #00000000 default-result-background-padding = 0 default-result-background-corner-radius = 0 # Alternate (even-numbered) result text theme # # If unspecified, these all default to the corresponding # default-result-* attribute. # # alternate-result-color = #FFFFFF # alternate-result-background = #00000000 # alternate-result-background-padding = 0 # alternate-result-background-corner-radius = 0 # Selection text selection-color = #F92672 selection-background = #00000000 selection-background-padding = 0 selection-background-corner-radius = 0 # Matching portion of selection text selection-match-color = #00000000 # ### Text cursor theme # # Style of the optional text cursor. # # Supported values: bar, block, underscore text-cursor-style = bar # Color of the text cursor # # If unspecified, defaults to the same as input-color # text-cursor-color = #FFFFFF # Color of text behind the text cursor when text-cursor-style = block # # If unspecified, defaults to the same as background-color # text-cursor-background = #000000 # Corner radius of the text cursor text-cursor-corner-radius = 0 # Thickness of the bar and underscore text cursors. # # If unspecified, defaults to a font-dependent value when # text-cursor-style = underscore, or to 2 otherwise. # text-cursor-thickness = 2 # ### Text layout # # Prompt to display. prompt-text = "run: " # Extra horizontal padding between prompt and input. prompt-padding = 0 # Placeholder input text. placeholder-text = "" # Maximum number of results to display. # If 0, tofi will draw as many results as it can fit in the window. num-results = 0 # Spacing between results in pixels. Can be negative. result-spacing = 0 # List results horizontally. horizontal = false # Minimum width of input in horizontal mode. min-input-width = 0 # ### Window theming # # Width and height of the window. Can be pixels or a percentage. width = 1280 height = 720 # Window background color background-color = #1B1D1E # Width of the border outlines in pixels. outline-width = 4 # Border outline color outline-color = #080800 # Width of the border in pixels. border-width = 12 # Border color border-color = #F92672 # Radius of window corners in pixels. corner-radius = 0 # Padding between borders and text. Can be pixels or a percentage. padding-top = 8 padding-bottom = 8 padding-left = 8 padding-right = 8 # Whether to clip text drawing to be within the specified padding. This # is mostly important for allowing text to be inset from the border, # while still allowing text backgrounds to reach right to the edge. clip-to-padding = true # Whether to scale the window by the output's scale factor. scale = true # ### Window positioning # # The name of the output to appear on. An empty string will use the # default output chosen by the compositor. output = "" # Location on screen to anchor the window to. # # Supported values: top-left, top, top-right, right, bottom-right, # bottom, bottom-left, left, center. anchor = center # Set the size of the exclusive zone. # # A value of -1 means ignore exclusive zones completely. # A value of 0 will move tofi out of the way of other windows' zones. # A value greater than 0 will set that much space as an exclusive zone. # # Values greater than 0 are only meaningful when tofi is anchored to a # single edge. exclusive-zone = -1 # Window offset from edge of screen. Only has an effect when anchored # to the relevant edge. Can be pixels or a percentage. margin-top = 0 margin-bottom = 0 margin-left = 0 margin-right = 0 # ### Behaviour # # Hide the mouse cursor. hide-cursor = false # Show a text cursor in the input field. text-cursor = false # Sort results by number of usages in run and drun modes. history = true # Specify an alternate file to read and store history information # from / to. This shouldn't normally be needed, and is intended to # facilitate the creation of custom modes. # history-file = /path/to/histfile # Use fuzzy matching for searches. fuzzy-match = false # If true, require a match to allow a selection to be made. If false, # making a selection with no matches will print input to stdout. # In drun mode, this is always true. require-match = true # If true, automatically accept a result if it is the only one # remaining. If there's only one result on startup, window creation is # skipped altogether. auto-accept-single = false # If true, typed input will be hidden, and what is displayed (if # anything) is determined by the hidden-character option. hide-input = false # Replace displayed input characters with a character. If the empty # string is given, input will be completely hidden. # This option only has an effect when hide-input is set to true. hidden-character = "*" # If true, directly launch applications on selection when in drun mode. # Otherwise, just print the command line to stdout. drun-launch = false # The terminal to run terminal programs in when in drun mode. # This option has no effect if drun-launch is set to true. # Defaults to the value of the TERMINAL environment variable. # terminal = foot # Delay keyboard initialisation until after the first draw to screen. # This option is experimental, and will cause tofi to miss keypresses # for a short time after launch. The only reason to use this option is # performance on slow systems. late-keyboard-init = false # If true, allow multiple simultaneous processes. # If false, create a lock file on startup to prevent multiple instances # from running simultaneously. multi-instance = false # Assume input is plain ASCII, and disable some Unicode handling # functions. This is faster, but means e.g. a search for "e" will not # match "é". ascii-input = false # ### Inclusion # # Configs can be split between multiple files, and then included # within each other. # include = /path/to/config tofi-0.9.1/doc/tofi.1.md000066400000000000000000000041741441474151400146640ustar00rootroot00000000000000# NAME tofi - Tiny dynamic menu for Wayland, inspired by **rofi**(1) and **dmenu**(1). # SYNOPSIS **tofi** \[options...\] **tofi-run** \[options...\] **tofi-drun** \[options...\] # DESCRIPTION **tofi** is a tiny dynamic menu for Wayland compositors supporting the layer-shell protocol. It reads newline-separated items from stdin, and displays a graphical selection menu. When a selection is made, it is printed to stdout. When invoked via the name **tofi-run**, **tofi** will not accept items on stdin, instead presenting a list of executables in the user's \$PATH. When invoked via the name **tofi-drun**, **tofi** will not accept items on stdin, and will generate a list of applications from desktop files as described in the Desktop Entry Specification. # OPTIONS **-h, --help** > Print help and exit. **-c, --config** \ > Specify path to custom config file. All config file options described in **tofi**(5) are also accepted, in the form **--key=value**. # KEYS \ \| \ \| \-k \| \-p > Move the selection back one entry. \ \| \ \| \-j \| \-n \| \ > Move the selection forward one entry. \ > Move the selection back one page. \ > Move the selection forward one page. \-u > Delete line. \-w \| \-\ > Delete word. \ > Confirm the current selection and quit. \ > Quit without making a selection. # FILES */etc/xdg/tofi/config* > Example configuration file. *\$XDG_CONFIG_HOME/tofi/config* > The default configuration file location. *\$XDG_CACHE_HOME/tofi-compgen* > Cached list of executables under \$PATH, regenerated as necessary. *\$XDG_CACHE_HOME/tofi-drun* > Cached list of desktop applications, regenerated as necessary. *\$XDG_STATE_HOME/tofi-history* > Numeric count of commands selected in **tofi-run**, to enable sorting > results by run count. *\$XDG_STATE_HOME/tofi-drun-history* > Numeric count of commands selected in **tofi-drun**, to enable sorting > results by run count. # AUTHORS Philip Jones \ # SEE ALSO **tofi**(5), **dmenu**(1) **rofi**(1) tofi-0.9.1/doc/tofi.1.scd000066400000000000000000000040021441474151400150230ustar00rootroot00000000000000tofi(1) # NAME tofi - Tiny dynamic menu for Wayland, inspired by *rofi*(1) and *dmenu*(1). # SYNOPSIS *tofi* [options...] *tofi-run* [options...] *tofi-drun* [options...] # DESCRIPTION *tofi* is a tiny dynamic menu for Wayland compositors supporting the layer-shell protocol. It reads newline-separated items from stdin, and displays a graphical selection menu. When a selection is made, it is printed to stdout. When invoked via the name *tofi-run*, *tofi* will not accept items on stdin, instead presenting a list of executables in the user's $PATH. When invoked via the name *tofi-drun*, *tofi* will not accept items on stdin, and will generate a list of applications from desktop files as described in the Desktop Entry Specification. # OPTIONS *-h, --help* Print help and exit. *-c, --config* Specify path to custom config file. All config file options described in *tofi*(5) are also accepted, in the form *--key=value*. # KEYS | | -k | -p Move the selection back one entry. | | -j | -n | Move the selection forward one entry. Move the selection back one page. Move the selection forward one page. -u Delete line. -w | - Delete word. Confirm the current selection and quit. Quit without making a selection. # FILES _/etc/xdg/tofi/config_ Example configuration file. _$XDG_CONFIG_HOME/tofi/config_ The default configuration file location. _$XDG_CACHE_HOME/tofi-compgen_ Cached list of executables under $PATH, regenerated as necessary. _$XDG_CACHE_HOME/tofi-drun_ Cached list of desktop applications, regenerated as necessary. _$XDG_STATE_HOME/tofi-history_ Numeric count of commands selected in *tofi-run*, to enable sorting results by run count. _$XDG_STATE_HOME/tofi-drun-history_ Numeric count of commands selected in *tofi-drun*, to enable sorting results by run count. # AUTHORS Philip Jones # SEE ALSO *tofi*(5), *dmenu*(1) *rofi*(1) tofi-0.9.1/doc/tofi.5.md000066400000000000000000000352141441474151400146670ustar00rootroot00000000000000# NAME tofi - configuration file # DESCRIPTION The config file format is basic .ini/.cfg style. Options are set one per line, with the syntax: > option = value Whitespace is ignored. Values starting or ending with whitespace can be given by enclosing them in double quotes like so: > option = " value " Lines beginning with \# or ; are treated as comments. Section headers of the form \[header\] are currently ignored. All options and values are case-insensitive, except where not possible (e.g. paths). Later options override earlier options, and command line options override config file options. # SPECIAL OPTIONS **include**=*path* > Include the contents of another config file. If *path* is a relative > path, it is interpreted as relative to this config file's path (or the > current directory if **--include** is passed on the command line). > Inclusion happens immediately, before the rest of the current file's > contents are parsed. # BEHAVIOUR OPTIONS **hide-cursor**=*true\|false* > Hide the mouse cursor. > > Default: false **text-cursor**=*true\|false* > Show a text cursor in the input field. > > Default: false **history**=*true\|false* > Sort results by number of usages. By default, this is only effective > in the run and drun modes - see the **history-file** option for more > information. > > Default: true **history-file**=*path* > Specify an alternate file to read and store history information from / > to. This shouldn't normally be needed, and is intended to facilitate > the creation of custom modes. The default value depends on the current > mode. > > Defaults: > > > > · > > > > > > tofi: None (no history file) > > > > > · > > > > > > tofi-run: *\$XDG_STATE_HOME/tofi-history* > > > > > · > > > > > > tofi-drun: *\$XDG_STATE_HOME/tofi-drun-history* **fuzzy-match**=*true\|false* > If true, searching is performed via a simple fuzzy matching algorithm. > If false, substring matching is used, weighted to favour matches > closer to the beginning of the string. > > Default: false **require-match**=*true\|false* > If true, require a match to allow a selection to be made. If false, > making a selection with no matches will print input to stdout. In drun > mode, this is always true. > > Default: true **auto-accept-single**=*true\|false* > If true, automatically accept a result if it is the only one > remaining. If there's only one result on startup, window creation is > skipped altogether. > > Default: false **hide-input**=*true\|false* > If true, typed input will be hidden, and what is displayed (if > anything) is determined by the **hidden-character** option. > > Default: false **hidden-character**=*char* > Replace displayed input characters with *char*. If *char* is set to > the empty string, input will be completely hidden. This option only > has an effect when **hide-input** is set to true. > > Default: \* **drun-launch**=*true\|false* > If true, directly launch applications on selection when in drun mode. > Otherwise, just print the Exec line of the .desktop file to stdout. > > Default: false **terminal**=*command* > The terminal to run terminal programs in when in drun mode. *command* > will be prepended to the the application's command line. This option > has no effect if **drun-launch** is set to true. > > Default: the value of the TERMINAL environment variable **drun-print-exec**=*true\|false* > **WARNING**: This option does nothing, and may be removed in a future > version of tofi. > > Default: true **late-keyboard-init**=*true\|false* > Delay keyboard initialisation until after the first draw to screen. > This option is experimental, and will cause tofi to miss keypresses > for a short time after launch. The only reason to use this option is > performance on slow systems. > > Default: false **multi-instance**=*true\|false* > If true, allow multiple simultaneous processes. If false, create a > lock file on startup to prevent multiple instances from running > simultaneously. > > Default: false **ascii-input**=*true\|false* > Assume input is plain ASCII, and disable some Unicode handling > functions. This is faster, but means e.g. a search for "e" will not > match "é". > > Default: false # STYLE OPTIONS **font**=*font* > Font to use. If *font* is a path to a font file, **tofi** will not > have to use Pango or Fontconfig. This greatly speeds up startup, but > any characters not in the chosen font will fail to render. > > If a path is not given, *font* is interpreted as a font name in Pango > format. > > Default: "Sans" **font-size**=*pt* > Point size of text. > > Default: 24 **font-features**=*features* > Comma separated list of OpenType font feature settings to apply. The > format is similar to the CSS "font-feature-settings" property. For > example, "smcp, c2sc" will turn all text into small caps (if supported > by the chosen font). > > Default: "" **font-variations**=*variations* > Comma separated list of OpenType font variation settings to apply. The > format is similar to the CSS "font-variation-settings" property. For > example, "wght 900" will set the weight of a variable font to 900 (if > supported by the chosen font). > > Default: "" **background-color**=*color* > Color of the background. See **COLORS** for more information. > > Default: \#1B1D1E **outline-width**=*px* > Width of the border outlines. > > Default: 4 **outline-color**=*color* > Color of the border outlines. See **COLORS** for more information. > > Default: \#080800 **border-width**=*px* > Width of the border. > > Default: 12 **border-color**=*color* > Color of the border. See **COLORS** for more information. > > Default: \#F92672 **text-color**=*color* > Color of text. See **COLORS** for more information. > > Default: \#FFFFFF **prompt-text**=*string* > Prompt text. > > Default: "run: " **prompt-padding**=*px* > Extra horizontal padding between prompt and input. > > Default: 0 **prompt-color**=*color* > Color of prompt text. See **COLORS** for more information. > > Default: Same as **text-color** **prompt-background**=*color* > Background color of prompt. See **COLORS** for more information. > > Default: \#00000000 **prompt-background-padding**=*directional* > Extra padding of the prompt background. See **DIRECTIONAL VALUES** for > more information. > > Default: 0 **prompt-background-corner-radius**=*px* > Corner radius of the prompt background. > > Default: 0 **placeholder-text**=*string* > Placeholder input text. > > Default: "" **placeholder-color**=*color* > Color of placeholder input text. See **COLORS** for more information. > > Default: \#FFFFFFA8 **placeholder-background**=*color* > Background color of placeholder input text. See **COLORS** for more > information. > > Default: \#00000000 **placeholder-background-padding**=*directional* > Extra padding of the placeholder input text background. See > **DIRECTIONAL VALUES** for more information. > > Default: 0 **placeholder-background-corner-radius**=*px* > Corner radius of the placeholder input text background. > > Default: 0 **input-color**=*color* > Color of input text. See **COLORS** for more information. > > Default: Same as **text-color** **input-background**=*color* > Background color of input. See **COLORS** for more information. > > Default: \#00000000 **input-background-padding**=*directional* > Extra padding of the input background. See **DIRECTIONAL VALUES** for > more information. > > Default: 0 **input-background-corner-radius**=*px* > Corner radius of the input background. > > Default: 0 **text-cursor-style**=*bar\|block\|underscore* > Style of the text cursor (if shown). > > Default: bar **text-cursor-color**=*color* > Color of the text cursor. > > Default: same as **input-color** **text-cursor-background**=*color* > Color of text behind the text cursor when **text-cursor-style**=block. > > Default: same as **background-color** **text-cursor-corner-radius**=*px* > Corner radius of the text cursor. > > Default: 0 **text-cursor-thickness**=*px* > Thickness of the bar and underscore text cursors. > > Default: font-dependent when **text-cursor-style**=underscore, 2 > otherwise. **default-result-color**=*color* > Default color of result text. See **COLORS** for more information. > > Default: Same as **text-color** **default-result-background**=*color* > Default background color of results. See **COLORS** for more > information. > > Default: \#00000000 **default-result-background-padding**=*directional* > Default extra padding of result backgrounds. See **DIRECTIONAL > VALUES** for more information. > > Default: 0 **default-result-background-corner-radius**=*px* > Default corner radius of result backgrounds. > > Default: 0 **alternate-result-color**=*color* > Color of alternate (even-numbered) result text. See **COLORS** for > more information. > > Default: same as **default-result-color** **alternate-result-background**=*color* > Background color of alternate (even-numbered) results. See **COLORS** > for more information. > > Default: same as **default-result-background** **alternate-result-background-padding**=*directional* > Extra padding of alternate (even-numbered) result backgrounds. See > **DIRECTIONAL VALUES** for more information. > > Default: same as **default-result-background-padding** **alternate-result-background-corner-radius**=*px* > Corner radius of alternate (even-numbered) result backgrounds. > > Default: same as **default-result-background-corner-radius** **num-results**=*n* > Maximum number of results to display. If *n* = 0, tofi will draw as > many results as it can fit in the window. > > Default: 0 **selection-color**=*color* > Color of selected result. See **COLORS** for more information. > > Default: \#F92672 **selection-match-color**=*color* > Color of the matching portion of the selected result. This will not > always be shown if the **fuzzy-match** option is set to true. Any > color that is fully transparent (alpha = 0) will disable this > highlighting. See **COLORS** for more information. > > Default: \#00000000 **selection-padding**=*px* > **WARNING**: This option is deprecated, and will be removed in a > future version of tofi. You should use the > **selection-background-padding** option instead. > > Extra horizontal padding of the selection background. If *px* = -1, > the padding will fill the whole window width. > > Default: 0 **selection-background**=*color* > Background color of selected result. See **COLORS** for more > information. > > Default: \#00000000 **selection-background-padding**=*directional* > Extra padding of the selected result background. See **DIRECTIONAL** > VALUES for more information. > > Default: 0 **selection-background-corner-radius**=*px* > Corner radius of the selected result background. Default: 0 **result-spacing**=*px* > Spacing between results. Can be negative. > > Default: 0 **min-input-width**=*px* > Minimum width of input in horizontal mode. > > Default: 0 **width**=*px\|%* > Width of the window. See **PERCENTAGE VALUES** for more information. > > Default: 1280 **height**=*px\|%* > Height of the window. See **PERCENTAGE VALUES** for more information. > > Default: 720 **corner-radius**=*px* > Radius of the window corners. > > Default: 0 **anchor**=*position* > Location on screen to anchor the window. Supported values are > *top-left*, *top*, *top-right*, *right*, *bottom-right*, *bottom*, > *bottom-left*, *left*, and *center*. > > Default: center **exclusive-zone**=*-1\|px\|%* > Set the size of the exclusive zone. A value of -1 means ignore > exclusive zones completely. A value of 0 will move tofi out of the way > of other windows' exclusive zones. A value greater than 0 will set > that much space as an exclusive zone. Values greater than 0 are only > meaningful when tofi is anchored to a single edge. > > Default: -1 **output**=*name* > The name of the output to appear on, if multiple outputs are present. > If empty, the compositor will choose which output to display the > window on (usually the currently focused output). > > Default: "" **scale**=*true\|false* > Scale the window by the output's scale factor. > > Default: true **margin-top**=*px\|%* > Offset from top of screen. See **PERCENTAGE VALUES** for more > information. Only has an effect when anchored to the top of the > screen. > > Default: 0 **margin-bottom**=*px\|%* > Offset from bottom of screen. See **PERCENTAGE VALUES** for more > information. Only has an effect when anchored to the bottom of the > screen. > > Default: 0 **margin-left**=*px\|%* > Offset from left of screen. See **PERCENTAGE VALUES** for more > information. Only has an effect when anchored to the left of the > screen. > > Default: 0 **margin-right**=*px\|%* > Offset from right of screen. See **PERCENTAGE VALUES** for more > information. Only has an effect when anchored to the right of the > screen. > > Default: 0 **padding-top**=*px\|%* > Padding between top border and text. See **PERCENTAGE VALUES** for > more information. > > Default: 8 **padding-bottom**=*px\|%* > Padding between bottom border and text. See **PERCENTAGE VALUES** for > more information. > > Default: 8 **padding-left**=*px\|%* > Padding between left border and text. See **PERCENTAGE VALUES** for > more information. > > Default: 8 **padding-right**=*px\|%* > Padding between right border and text. See **PERCENTAGE VALUES** for > more information. > > Default: 8 **clip-to-padding**=*true\|false* > Whether to clip text drawing to be within the specified padding. This > is mostly important for allowing text to be inset from the border, > while still allowing text backgrounds to reach right to the edge. > > Default: true **horizontal**=*true\|false* > List results horizontally. > > Default: false **hint-font**=*true\|false* > Perform font hinting. Only applies when a path to a font has been > specified via **font**. Disabling font hinting speeds up text > rendering appreciably, but will likely look poor at small font pixel > sizes. > > Default: true # COLORS Colors can be specified in the form *RGB*, *RGBA*, *RRGGBB* or *RRGGBBAA*, optionally prefixed with a hash (#). # PERCENTAGE VALUES Some pixel values can optionally have a % suffix, like so: > width = 50% This will be interpreted as a percentage of the screen resolution in the relevant direction. # DIRECTIONAL VALUES The background box padding of a type of text can be specified by one to four comma separated values, with meanings similar to the CSS padding property: > · > > One value sets all edges. > · > > Two values set (top & bottom), (left & right) edges. > · > > Three values set (top), (left & right), (bottom) edges. > · > > Four values set (top), (right), (bottom), (left) edges. Specifying -1 for any of the values will pad as far as possible in that direction. # AUTHORS Philip Jones \ # SEE ALSO **tofi**(1), **dmenu**(1) **rofi**(1) tofi-0.9.1/doc/tofi.5.scd000066400000000000000000000335361441474151400150450ustar00rootroot00000000000000tofi(5) # NAME tofi - configuration file # DESCRIPTION The config file format is basic .ini/.cfg style. Options are set one per line, with the syntax: option = value Whitespace is ignored. Values starting or ending with whitespace can be given by enclosing them in double quotes like so: option = " value " Lines beginning with # or ; are treated as comments. Section headers of the form [header] are currently ignored. All options and values are case-insensitive, except where not possible (e.g. paths). Later options override earlier options, and command line options override config file options. # SPECIAL OPTIONS *include*=_path_ Include the contents of another config file. If _path_ is a relative path, it is interpreted as relative to this config file's path (or the current directory if *--include* is passed on the command line). Inclusion happens immediately, before the rest of the current file's contents are parsed. # BEHAVIOUR OPTIONS *hide-cursor*=_true|false_ Hide the mouse cursor. Default: false *text-cursor*=_true|false_ Show a text cursor in the input field. Default: false *history*=_true|false_ Sort results by number of usages. By default, this is only effective in the run and drun modes - see the *history-file* option for more information. Default: true *history-file*=_path_ Specify an alternate file to read and store history information from / to. This shouldn't normally be needed, and is intended to facilitate the creation of custom modes. The default value depends on the current mode. Defaults: - tofi: None (no history file) - tofi-run: _$XDG_STATE_HOME/tofi-history_ - tofi-drun: _$XDG_STATE_HOME/tofi-drun-history_ *fuzzy-match*=_true|false_ If true, searching is performed via a simple fuzzy matching algorithm. If false, substring matching is used, weighted to favour matches closer to the beginning of the string. Default: false *require-match*=_true|false_ If true, require a match to allow a selection to be made. If false, making a selection with no matches will print input to stdout. In drun mode, this is always true. Default: true *auto-accept-single*=_true|false_ If true, automatically accept a result if it is the only one remaining. If there's only one result on startup, window creation is skipped altogether. Default: false *hide-input*=_true|false_ If true, typed input will be hidden, and what is displayed (if anything) is determined by the *hidden-character* option. Default: false *hidden-character*=_char_ Replace displayed input characters with _char_. If _char_ is set to the empty string, input will be completely hidden. This option only has an effect when *hide-input* is set to true. Default: \* *drun-launch*=_true|false_ If true, directly launch applications on selection when in drun mode. Otherwise, just print the Exec line of the .desktop file to stdout. Default: false *terminal*=_command_ The terminal to run terminal programs in when in drun mode. _command_ will be prepended to the the application's command line. This option has no effect if *drun-launch* is set to true. Default: the value of the TERMINAL environment variable *drun-print-exec*=_true|false_ *WARNING*: This option does nothing, and may be removed in a future version of tofi. Default: true *late-keyboard-init*=_true|false_ Delay keyboard initialisation until after the first draw to screen. This option is experimental, and will cause tofi to miss keypresses for a short time after launch. The only reason to use this option is performance on slow systems. Default: false *multi-instance*=_true|false_ If true, allow multiple simultaneous processes. If false, create a lock file on startup to prevent multiple instances from running simultaneously. Default: false *ascii-input*=_true|false_ Assume input is plain ASCII, and disable some Unicode handling functions. This is faster, but means e.g. a search for "e" will not match "é". Default: false # STYLE OPTIONS *font*=_font_ Font to use. If _font_ is a path to a font file, *tofi* will not have to use Pango or Fontconfig. This greatly speeds up startup, but any characters not in the chosen font will fail to render. If a path is not given, _font_ is interpreted as a font name in Pango format. Default: "Sans" *font-size*=_pt_ Point size of text. Default: 24 *font-features*=_features_ Comma separated list of OpenType font feature settings to apply. The format is similar to the CSS "font-feature-settings" property. For example, "smcp, c2sc" will turn all text into small caps (if supported by the chosen font). Default: "" *font-variations*=_variations_ Comma separated list of OpenType font variation settings to apply. The format is similar to the CSS "font-variation-settings" property. For example, "wght 900" will set the weight of a variable font to 900 (if supported by the chosen font). Default: "" *background-color*=_color_ Color of the background. See *COLORS* for more information. Default: #1B1D1E *outline-width*=_px_ Width of the border outlines. Default: 4 *outline-color*=_color_ Color of the border outlines. See *COLORS* for more information. Default: #080800 *border-width*=_px_ Width of the border. Default: 12 *border-color*=_color_ Color of the border. See *COLORS* for more information. Default: #F92672 *text-color*=_color_ Color of text. See *COLORS* for more information. Default: #FFFFFF *prompt-text*=_string_ Prompt text. Default: "run: " *prompt-padding*=_px_ Extra horizontal padding between prompt and input. Default: 0 *prompt-color*=_color_ Color of prompt text. See *COLORS* for more information. Default: Same as *text-color* *prompt-background*=_color_ Background color of prompt. See *COLORS* for more information. Default: #00000000 *prompt-background-padding*=_directional_ Extra padding of the prompt background. See *DIRECTIONAL VALUES* for more information. Default: 0 *prompt-background-corner-radius*=_px_ Corner radius of the prompt background. Default: 0 *placeholder-text*=_string_ Placeholder input text. Default: "" *placeholder-color*=_color_ Color of placeholder input text. See *COLORS* for more information. Default: #FFFFFFA8 *placeholder-background*=_color_ Background color of placeholder input text. See *COLORS* for more information. Default: #00000000 *placeholder-background-padding*=_directional_ Extra padding of the placeholder input text background. See *DIRECTIONAL VALUES* for more information. Default: 0 *placeholder-background-corner-radius*=_px_ Corner radius of the placeholder input text background. Default: 0 *input-color*=_color_ Color of input text. See *COLORS* for more information. Default: Same as *text-color* *input-background*=_color_ Background color of input. See *COLORS* for more information. Default: #00000000 *input-background-padding*=_directional_ Extra padding of the input background. See *DIRECTIONAL VALUES* for more information. Default: 0 *input-background-corner-radius*=_px_ Corner radius of the input background. Default: 0 *text-cursor-style*=_bar|block|underscore_ Style of the text cursor (if shown). Default: bar *text-cursor-color*=_color_ Color of the text cursor. Default: same as *input-color* *text-cursor-background*=_color_ Color of text behind the text cursor when *text-cursor-style*=block. Default: same as *background-color* *text-cursor-corner-radius*=_px_ Corner radius of the text cursor. Default: 0 *text-cursor-thickness*=_px_ Thickness of the bar and underscore text cursors. Default: font-dependent when *text-cursor-style*=underscore, 2 otherwise. *default-result-color*=_color_ Default color of result text. See *COLORS* for more information. Default: Same as *text-color* *default-result-background*=_color_ Default background color of results. See *COLORS* for more information. Default: #00000000 *default-result-background-padding*=_directional_ Default extra padding of result backgrounds. See *DIRECTIONAL VALUES* for more information. Default: 0 *default-result-background-corner-radius*=_px_ Default corner radius of result backgrounds. Default: 0 *alternate-result-color*=_color_ Color of alternate (even-numbered) result text. See *COLORS* for more information. Default: same as *default-result-color* *alternate-result-background*=_color_ Background color of alternate (even-numbered) results. See *COLORS* for more information. Default: same as *default-result-background* *alternate-result-background-padding*=_directional_ Extra padding of alternate (even-numbered) result backgrounds. See *DIRECTIONAL VALUES* for more information. Default: same as *default-result-background-padding* *alternate-result-background-corner-radius*=_px_ Corner radius of alternate (even-numbered) result backgrounds. Default: same as *default-result-background-corner-radius* *num-results*=_n_ Maximum number of results to display. If _n_ = 0, tofi will draw as many results as it can fit in the window. Default: 0 *selection-color*=_color_ Color of selected result. See *COLORS* for more information. Default: #F92672 *selection-match-color*=_color_ Color of the matching portion of the selected result. This will not always be shown if the *fuzzy-match* option is set to true. Any color that is fully transparent (alpha = 0) will disable this highlighting. See *COLORS* for more information. Default: #00000000 *selection-padding*=_px_ *WARNING*: This option is deprecated, and will be removed in a future version of tofi. You should use the *selection-background-padding* option instead. Extra horizontal padding of the selection background. If _px_ = -1, the padding will fill the whole window width. Default: 0 *selection-background*=_color_ Background color of selected result. See *COLORS* for more information. Default: #00000000 *selection-background-padding*=_directional_ Extra padding of the selected result background. See *DIRECTIONAL VALUES* for more information. Default: 0 *selection-background-corner-radius*=_px_ Corner radius of the selected result background. Default: 0 *result-spacing*=_px_ Spacing between results. Can be negative. Default: 0 *min-input-width*=_px_ Minimum width of input in horizontal mode. Default: 0 *width*=_px|%_ Width of the window. See *PERCENTAGE VALUES* for more information. Default: 1280 *height*=_px|%_ Height of the window. See *PERCENTAGE VALUES* for more information. Default: 720 *corner-radius*=_px_ Radius of the window corners. Default: 0 *anchor*=_position_ Location on screen to anchor the window. Supported values are _top-left_, _top_, _top-right_, _right_, _bottom-right_, _bottom_, _bottom-left_, _left_, and _center_. Default: center *exclusive-zone*=_-1|px|%_ Set the size of the exclusive zone. A value of -1 means ignore exclusive zones completely. A value of 0 will move tofi out of the way of other windows' exclusive zones. A value greater than 0 will set that much space as an exclusive zone. Values greater than 0 are only meaningful when tofi is anchored to a single edge. Default: -1 *output*=_name_ The name of the output to appear on, if multiple outputs are present. If empty, the compositor will choose which output to display the window on (usually the currently focused output). Default: "" *scale*=_true|false_ Scale the window by the output's scale factor. Default: true *margin-top*=_px|%_ Offset from top of screen. See *PERCENTAGE VALUES* for more information. Only has an effect when anchored to the top of the screen. Default: 0 *margin-bottom*=_px|%_ Offset from bottom of screen. See *PERCENTAGE VALUES* for more information. Only has an effect when anchored to the bottom of the screen. Default: 0 *margin-left*=_px|%_ Offset from left of screen. See *PERCENTAGE VALUES* for more information. Only has an effect when anchored to the left of the screen. Default: 0 *margin-right*=_px|%_ Offset from right of screen. See *PERCENTAGE VALUES* for more information. Only has an effect when anchored to the right of the screen. Default: 0 *padding-top*=_px|%_ Padding between top border and text. See *PERCENTAGE VALUES* for more information. Default: 8 *padding-bottom*=_px|%_ Padding between bottom border and text. See *PERCENTAGE VALUES* for more information. Default: 8 *padding-left*=_px|%_ Padding between left border and text. See *PERCENTAGE VALUES* for more information. Default: 8 *padding-right*=_px|%_ Padding between right border and text. See *PERCENTAGE VALUES* for more information. Default: 8 *clip-to-padding*=_true|false_ Whether to clip text drawing to be within the specified padding. This is mostly important for allowing text to be inset from the border, while still allowing text backgrounds to reach right to the edge. Default: true *horizontal*=_true|false_ List results horizontally. Default: false *hint-font*=_true|false_ Perform font hinting. Only applies when a path to a font has been specified via *font*. Disabling font hinting speeds up text rendering appreciably, but will likely look poor at small font pixel sizes. Default: true # COLORS Colors can be specified in the form _RGB_, _RGBA_, _RRGGBB_ or _RRGGBBAA_, optionally prefixed with a hash (#). # PERCENTAGE VALUES Some pixel values can optionally have a % suffix, like so: width = 50% This will be interpreted as a percentage of the screen resolution in the relevant direction. # DIRECTIONAL VALUES The background box padding of a type of text can be specified by one to four comma separated values, with meanings similar to the CSS padding property: - One value sets all edges. - Two values set (top & bottom), (left & right) edges. - Three values set (top), (left & right), (bottom) edges. - Four values set (top), (right), (bottom), (left) edges. Specifying -1 for any of the values will pad as far as possible in that direction. # AUTHORS Philip Jones # SEE ALSO *tofi*(1), *dmenu*(1) *rofi*(1) tofi-0.9.1/meson.build000066400000000000000000000127221441474151400146350ustar00rootroot00000000000000project( 'tofi', 'c', version: '0.9.1', license: 'MIT', meson_version: '>=0.61.0', default_options: [ 'c_std=c2x', 'optimization=3', 'buildtype=debugoptimized', 'warning_level=3', 'b_lto=true', 'b_lto_threads=-1', 'b_pie=true', 'prefix=/usr' ], ) debug = get_option('buildtype').startswith('debug') if debug add_project_arguments('-DDEBUG', language : 'c') endif config_location = join_paths( get_option('sysconfdir'), 'xdg', 'tofi' ) install_data( 'doc/config', install_dir: config_location ) license_dir = join_paths( get_option('datadir'), 'licenses', 'tofi' ) install_data( 'LICENSE', install_dir: license_dir ) completion_location = join_paths( get_option('prefix'), get_option('datadir'), 'bash-completion', 'completions' ) install_data( 'completions/tofi', install_dir: completion_location ) install_symlink( 'tofi-run', install_dir: completion_location, pointing_to: 'tofi', ) install_symlink( 'tofi-drun', install_dir: completion_location, pointing_to: 'tofi', ) install_symlink( 'tofi-run', install_dir: get_option('bindir'), pointing_to: 'tofi', ) install_symlink( 'tofi-drun', install_dir: get_option('bindir'), pointing_to: 'tofi', ) add_project_arguments( [ '-pedantic', #'-Wconversion', '-Wshadow', '-Wno-unused-parameter', '-D_GNU_SOURCE', '-D_FORTIFY_SOURCE=2', # Disable these unwind tables for binary size, as we don't use exceptions # or anything else that requires them. '-fno-asynchronous-unwind-tables', ], language: 'c' ) common_sources = files( 'src/clipboard.c', 'src/color.c', 'src/compgen.c', 'src/config.c', 'src/desktop_vec.c', 'src/drun.c', 'src/entry.c', 'src/entry_backend/pango.c', 'src/entry_backend/harfbuzz.c', 'src/fuzzy_match.c', 'src/history.c', 'src/input.c', 'src/lock.c', 'src/log.c', 'src/mkdirp.c', 'src/scale.c', 'src/shm.c', 'src/string_vec.c', 'src/surface.c', 'src/unicode.c', 'src/xmalloc.c', ) compgen_sources = files( 'src/main_compgen.c', 'src/compgen.c', 'src/fuzzy_match.c', 'src/log.c', 'src/mkdirp.c', 'src/string_vec.c', 'src/unicode.c', 'src/xmalloc.c' ) cc = meson.get_compiler('c') librt = cc.find_library('rt', required: false) libm = cc.find_library('m', required: false) # On systems where libc doesn't provide fts (i.e. musl) we require libfts libfts = cc.find_library('fts', required: not cc.has_function('fts_read')) freetype = dependency('freetype2') harfbuzz = dependency('harfbuzz') cairo = dependency('cairo') pangocairo = dependency('pangocairo') wayland_client = dependency('wayland-client') wayland_protocols = dependency('wayland-protocols', native: true) wayland_scanner_dep = dependency('wayland-scanner', native: true) xkbcommon = dependency('xkbcommon') glib = dependency('glib-2.0') gio_unix = dependency('gio-unix-2.0') if wayland_client.version().version_compare('<1.20.0') add_project_arguments( ['-DNO_WL_OUTPUT_NAME=1'], language: 'c' ) endif if harfbuzz.version().version_compare('<4.0.0') add_project_arguments( ['-DNO_HARFBUZZ_METRIC_FALLBACK=1'], language: 'c' ) endif if harfbuzz.version().version_compare('<4.4.0') add_project_arguments( ['-DNO_HARFBUZZ_FONT_CHANGED=1'], language: 'c' ) endif # Generate the necessary Wayland headers / sources with wayland-scanner wayland_scanner = find_program( wayland_scanner_dep.get_variable(pkgconfig: 'wayland_scanner'), native: true ) wayland_protocols_dir = wayland_protocols.get_variable(pkgconfig: 'pkgdatadir') wl_proto_headers = [] wl_proto_src = [] wl_proto_xml = [ wayland_protocols_dir + '/stable/xdg-shell/xdg-shell.xml', wayland_protocols_dir + '/stable/viewporter/viewporter.xml', 'protocols/wlr-layer-shell-unstable-v1.xml', 'protocols/fractional-scale-v1.xml' ] foreach proto : wl_proto_xml wl_proto_headers += custom_target( proto.underscorify() + '_client_header', output: '@BASENAME@.h', input: proto, command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@']) wl_proto_src += custom_target( proto.underscorify() + '_private_code', output: '@BASENAME@.c', input: proto, command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@']) endforeach subdir('test') executable( 'tofi', files('src/main.c'), common_sources, wl_proto_src, wl_proto_headers, dependencies: [librt, libm, libfts, freetype, harfbuzz, cairo, pangocairo, wayland_client, xkbcommon, glib, gio_unix], install: true ) executable( 'tofi-compgen', compgen_sources, dependencies: [glib], install: false ) scdoc = find_program('scdoc', required: get_option('man-pages')) if scdoc.found() sed = find_program('sed') sh = find_program('sh') mandir = get_option('mandir') output = 'tofi.1' custom_target( output, input: 'doc/tofi.1.scd', output: output, command: [ sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.full_path(), output) ], install: true, install_dir: '@0@/man1'.format(mandir) ) install_symlink( 'tofi-run.1', install_dir: '@0@/man1'.format(mandir), pointing_to: 'tofi.1' ) install_symlink( 'tofi-drun.1', install_dir: '@0@/man1'.format(mandir), pointing_to: 'tofi.1' ) output = 'tofi.5' custom_target( output, input: 'doc/tofi.5.scd', output: output, command: [ sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.full_path(), output) ], install: true, install_dir: '@0@/man5'.format(mandir) ) endif tofi-0.9.1/meson_options.txt000066400000000000000000000001271441474151400161240ustar00rootroot00000000000000option('man-pages', type: 'feature', value: 'auto', description: 'Install man pages.') tofi-0.9.1/protocols/000077500000000000000000000000001441474151400145135ustar00rootroot00000000000000tofi-0.9.1/protocols/fractional-scale-v1.xml000066400000000000000000000110341441474151400207670ustar00rootroot00000000000000 Copyright © 2022 Kenny Levinsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This protocol allows a compositor to suggest for surfaces to render at fractional scales. A client can submit scaled content by utilizing wp_viewport. This is done by creating a wp_viewport object for the surface and setting the destination rectangle to the surface size before the scale factor is applied. The buffer size is calculated by multiplying the surface size by the intended scale. The wl_surface buffer scale should remain set to 1. If a surface has a surface-local size of 100 px by 50 px and wishes to submit buffers with a scale of 1.5, then a buffer of 150px by 75 px should be used and the wp_viewport destination rectangle should be 100 px by 50 px. For toplevel surfaces, the size is rounded halfway away from zero. The rounding algorithm for subsurface position and size is not defined. A global interface for requesting surfaces to use fractional scales. Informs the server that the client will not be using this protocol object anymore. This does not affect any other objects, wp_fractional_scale_v1 objects included. Create an add-on object for the the wl_surface to let the compositor request fractional scales. If the given wl_surface already has a wp_fractional_scale_v1 object associated, the fractional_scale_exists protocol error is raised. An additional interface to a wl_surface object which allows the compositor to inform the client of the preferred scale. Destroy the fractional scale object. When this object is destroyed, preferred_scale events will no longer be sent. Notification of a new preferred scale for this surface that the compositor suggests that the client should use. The sent scale is the numerator of a fraction with a denominator of 120. tofi-0.9.1/protocols/wlr-layer-shell-unstable-v1.xml000066400000000000000000000440361441474151400224260ustar00rootroot00000000000000 Copyright © 2017 Drew DeVault Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and rendered with a defined z-depth respective to each other. They may also be anchored to the edges and corners of a screen and specify input handling semantics. This interface should be suitable for the implementation of many desktop shell components, and a broad number of other applications that interact with the desktop. Create a layer surface for an existing surface. This assigns the role of layer_surface, or raises a protocol error if another role is already assigned. Creating a layer surface from a wl_surface which has a buffer attached or committed is a client error, and any attempts by a client to attach or manipulate a buffer prior to the first layer_surface.configure call must also be treated as errors. After creating a layer_surface object and setting it up, the client must perform an initial commit without any buffer attached. The compositor will reply with a layer_surface.configure event. The client must acknowledge it and is then allowed to attach a buffer to map the surface. You may pass NULL for output to allow the compositor to decide which output to use. Generally this will be the one that the user most recently interacted with. Clients can specify a namespace that defines the purpose of the layer surface. These values indicate which layers a surface can be rendered in. They are ordered by z depth, bottom-most first. Traditional shell surfaces will typically be rendered between the bottom and top layers. Fullscreen shell surfaces are typically rendered at the top layer. Multiple surfaces can share a single layer, and ordering within a single layer is undefined. This request indicates that the client will not use the layer_shell object any more. Objects that have been created through this instance are not affected. An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like environment. Layer surface state (layer, size, anchor, exclusive zone, margin, interactivity) is double-buffered, and will be applied at the time wl_surface.commit of the corresponding wl_surface is called. Attaching a null buffer to a layer surface unmaps it. Unmapping a layer_surface means that the surface cannot be shown by the compositor until it is explicitly mapped again. The layer_surface returns to the state it had right after layer_shell.get_layer_surface. The client can re-map the surface by performing a commit without any buffer attached, waiting for a configure event and handling it as usual. Sets the size of the surface in surface-local coordinates. The compositor will display the surface centered with respect to its anchors. If you pass 0 for either value, the compositor will assign it and inform you of the assignment in the configure event. You must set your anchor to opposite edges in the dimensions you omit; not doing so is a protocol error. Both values are 0 by default. Size is double-buffered, see wl_surface.commit. Requests that the compositor anchor the surface to the specified edges and corners. If two orthogonal edges are specified (e.g. 'top' and 'left'), then the anchor point will be the intersection of the edges (e.g. the top left corner of the output); otherwise the anchor point will be centered on that edge, or in the center if none is specified. Anchor is double-buffered, see wl_surface.commit. Requests that the compositor avoids occluding an area with other surfaces. The compositor's use of this information is implementation-dependent - do not assume that this region will not actually be occluded. A positive value is only meaningful if the surface is anchored to one edge or an edge and both perpendicular edges. If the surface is not anchored, anchored to only two perpendicular edges (a corner), anchored to only two parallel edges or anchored to all edges, a positive value will be treated the same as zero. A positive zone is the distance from the edge in surface-local coordinates to consider exclusive. Surfaces that do not wish to have an exclusive zone may instead specify how they should interact with surfaces that do. If set to zero, the surface indicates that it would like to be moved to avoid occluding surfaces with a positive exclusive zone. If set to -1, the surface indicates that it would not like to be moved to accommodate for other surfaces, and the compositor should extend it all the way to the edges it is anchored to. For example, a panel might set its exclusive zone to 10, so that maximized shell surfaces are not shown on top of it. A notification might set its exclusive zone to 0, so that it is moved to avoid occluding the panel, but shell surfaces are shown underneath it. A wallpaper or lock screen might set their exclusive zone to -1, so that they stretch below or over the panel. The default value is 0. Exclusive zone is double-buffered, see wl_surface.commit. Requests that the surface be placed some distance away from the anchor point on the output, in surface-local coordinates. Setting this value for edges you are not anchored to has no effect. The exclusive zone includes the margin. Margin is double-buffered, see wl_surface.commit. Types of keyboard interaction possible for layer shell surfaces. The rationale for this is twofold: (1) some applications are not interested in keyboard events and not allowing them to be focused can improve the desktop experience; (2) some applications will want to take exclusive keyboard focus. This value indicates that this surface is not interested in keyboard events and the compositor should never assign it the keyboard focus. This is the default value, set for newly created layer shell surfaces. This is useful for e.g. desktop widgets that display information or only have interaction with non-keyboard input devices. Request exclusive keyboard focus if this surface is above the shell surface layer. For the top and overlay layers, the seat will always give exclusive keyboard focus to the top-most layer which has keyboard interactivity set to exclusive. If this layer contains multiple surfaces with keyboard interactivity set to exclusive, the compositor determines the one receiving keyboard events in an implementation- defined manner. In this case, no guarantee is made when this surface will receive keyboard focus (if ever). For the bottom and background layers, the compositor is allowed to use normal focus semantics. This setting is mainly intended for applications that need to ensure they receive all keyboard events, such as a lock screen or a password prompt. This requests the compositor to allow this surface to be focused and unfocused by the user in an implementation-defined manner. The user should be able to unfocus this surface even regardless of the layer it is on. Typically, the compositor will want to use its normal mechanism to manage keyboard focus between layer shell surfaces with this setting and regular toplevels on the desktop layer (e.g. click to focus). Nevertheless, it is possible for a compositor to require a special interaction to focus or unfocus layer shell surfaces (e.g. requiring a click even if focus follows the mouse normally, or providing a keybinding to switch focus between layers). This setting is mainly intended for desktop shell components (e.g. panels) that allow keyboard interaction. Using this option can allow implementing a desktop shell that can be fully usable without the mouse. Set how keyboard events are delivered to this surface. By default, layer shell surfaces do not receive keyboard events; this request can be used to change this. This setting is inherited by child surfaces set by the get_popup request. Layer surfaces receive pointer, touch, and tablet events normally. If you do not want to receive them, set the input region on your surface to an empty region. Keyboard interactivity is double-buffered, see wl_surface.commit. This assigns an xdg_popup's parent to this layer_surface. This popup should have been created via xdg_surface::get_popup with the parent set to NULL, and this request must be invoked before committing the popup's initial state. See the documentation of xdg_popup for more details about what an xdg_popup is and how it is used. When a configure event is received, if a client commits the surface in response to the configure event, then the client must make an ack_configure request sometime before the commit request, passing along the serial of the configure event. If the client receives multiple configure events before it can respond to one, it only has to ack the last configure event. A client is not required to commit immediately after sending an ack_configure request - it may even ack_configure several times before its next surface commit. A client may send multiple ack_configure requests before committing, but only the last request sent before a commit indicates which configure event the client really is responding to. This request destroys the layer surface. The configure event asks the client to resize its surface. Clients should arrange their surface for the new states, and then send an ack_configure request with the serial sent in this configure event at some point before committing the new surface. The client is free to dismiss all but the last configure event it received. The width and height arguments specify the size of the window in surface-local coordinates. The size is a hint, in the sense that the client is free to ignore it if it doesn't resize, pick a smaller size (to satisfy aspect ratio or resize in steps of NxM pixels). If the client picks a smaller size and is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the surface will be centered on this axis. If the width or height arguments are zero, it means the client should decide its own window dimension. The closed event is sent by the compositor when the surface will no longer be shown. The output may have been destroyed or the user may have asked for it to be removed. Further changes to the surface will be ignored. The client should destroy the resource after receiving this event, and create a new surface if they so choose. Change the layer that the surface is rendered on. Layer is double-buffered, see wl_surface.commit. tofi-0.9.1/screenshot_dark_paper.png000066400000000000000000002203051441474151400175440ustar00rootroot00000000000000PNG  IHDR/ IDATx  ̟˚Cb 8@p@p 8 8@p@p 8 8@p@p 8 8@p@pm}ٻ ( %'$lB$Vp'!qwY a&!KTӷK{ߋEDDDDDDDĀ߄_~=]<&O6zXK1bHPUUk J`"{9mԨQ&M5𹁪Pn\F N| """"""b|D7FjдiSMM-Zjii}m۶m߾C۶'nf7n+(+OLٴe.rֲeC{x>rSN1cւW\avn޲k |us7~ig̜pђUnٶcç^8g|_ihjj믿ڵ吡N.^#GC.&AGG#; ߮[A@=آӐwu6.]dU&P2(سw?r H cpj2++#3;{O\40pCi/<%#GE΄JW3s1»cuOVXADDDDDD 8>6mZT4Ho|Vҕ弊(=}E#}CwbI_xYׯC]]}#b JPyqIff۵o?kܨx Nb1cǣSJPdrtՈpZB=)\ܝ]\9<2V(X|%zj="/_g%!8n m׮=55!e@ϳCՃZ'%z'Mr9\}J9{"pTb㓇L.&,*[X%>IxOdak sΫ$pQ ] O^~񄉓0M9@p&z """""""7m;n?UaֻV\ ?=Y 8k6frIBDDDDDD 8~ær! pDSS蘄z嚟ʬs+ 8\ncђeV,mAw|V1X~Pgg,011ht8[ cђ Աu#&اV 8.+p̑??%Apђ:WADDDDDDĀcZ1KH17PUd(1xYb["|gƌY s“X P hdqLqFpd8:)p| tp|&DDDDDDDoPs.7jHT* 82?S$ *s 8wQy۠g_BQ8 8^ 8'Li1S6nRTk':>NQZ˙s8Tgϙ'Aap|#p3DDDDDDD 86o.p֮AUpT{\{ǚGs;vV8CY2Z 8Μ?`QAp;!pyy 3%m縧,1u 6ܳ@qOүOFU@R^M% ʤGxckX S+9yE܊/NOOsg./)Y5><"Ш.111Z[c+WW8p^cƌY:nFJoEl޲ݬweee5"""""""w">~M0559;ݾ@8PackT{p[lAրz lłNk Wd" $w=rtr/$""""""bQ`5!HL~fe% jk޼嫾xK`_z!}`aX6/^3] n^ɴU$P]h bUzi2#q nG,dZ++;d_H?fnݻ#Ui $|ĚMM͡Cmb⒄7nXβ"S'eĬZ\'fΚB,(Y|"""""""򿺫AKK+8*WWE+VU GH{Z2PŐ{cOaq'xy6rS!ASt$}-$ 8|wT7&M &JJJUOݸڵgPy 8HYY}#4o5?2 UXtҲcXxl-70sW-'O=M KssP36dEGIOAs qWG=444ڶmgdl­2݇y&NsRV($V>715Я_D-BTś#Fa`lbڢEK!hٲ%J%*DDDDDDĀ|KHIHJÑ\VhɉoÐӘ2+O y6^yW@_Ѝ[wѡ#"2vР!ntU!RzD~ū( Mzzl#9'19 ({^JN= d3egonn!E"""""""5ZңGODxV@d[HL4JF+1{tuh׮=֘Ȟ)daEdoZ M5#XW$%y#f^0 Lv+#N:oBlʫFDDDDDDĀڴc(@LcP 4ѓpÓ"cqNbRZrꋰ(lt94p~STṔ࣠'`mT{ڦMbyHY7EGW8p~NϝI9?&&8QL\b|b*|kպCGdPbUaaKTL iHwb s횵+IG]oշXaOV==^pѽG|C c40s,pBMH+:wyv,< ~%7bE+S=}3_8x$=3kI8dq& d13ݶm[ 欸I+ғz⃾:w`Bku"2Oɘ[bFDAtttX}q%ZVY54mV\iV=-[wQs'h EqE~}e|h֬knѢdX[ Ⴃ3}v ;yLƲ?HgUIܱ ! @.!zIoEo8w@v& p p p pF8@8@88@@8@8@88@@g.GGխT$iB!xºojüYFN=zH*/H͘eֶk{[b/rBc8@E :z/7Gպ1J6Ԙ4}+>Nכ4g"Uni8.OE{eDQ89ucn\#wF7 p=ý;чk;z#F.}M%'`>ESә(@@Ȭu7ln-ӹ26DDk_k#y~pcw_\0Fݢ;a|VYW>᪇@iWZg ܮfݳ>[^#s_98\DVst]k?t& W?ZHTrOEq_@(DN_q]Rt8PpxZESe,1F;1sIW[-oYI(u4>_po.Ghg.;d{p%/ݕ$JXwG_h6= `Iu#p7*}$oloG j7^*?Ýdy<.",)p\N]!bw<~ٴ"GpܒV}+R}Cu ې{8*F#\֬"Zg8, ߦhtekCjc,u:5c q8t$u pJ0\.4};~=97dbE^.?>=nC riް>}o2@ pK n*A0F/M#eWEQ ja3{oMvT3gZ-_hH͘ucJߗ/6@k"ou<~2ME-‡_ ?|&p`H|NEqG(Ł}dnQ4O6636b+f,{~R j/zﭭ}./ٍ:+T49l6 ȻQpߔBR ܈ %'B!BHbqdܱ#й <ϝK%~ML$f+4XD-E8V_,.ǀ^p~pn.Tdu? 3Gh'| NG YUT*EvהԓeÊ>jm&_w1[[88\jcR;a#|8"!; |pm`϶ v~gt{E!B!!DR0|{Ym~JH "E gwg18▃ *7݇0'áLZkDO>/76]Tcg7G+įU(H&Vz`͟vͻ ZuMeۿߚ-\qS'd܆vJ"hs/fy!B!$pv/O¡& FTL (q#߅j.I0:S_Q*!^䜈@bE/Z%#*;qƬű\2%x8,<}g5CnY*^[-6m!PNsp_~}{]cs&' ˢ!c1w!B!$p}7np$G\,i*ק8ױN3FZG fbKj ?řaf\ L"Z5`x]1p=up F*Gau#og8'Z׃@3j cv%' ҂(B%TX{6Δb"EL^MBm"B!BH" `^Xu-9Kx{ ÑSD}Ĥ<] R$lfh{=aynV 2 A&KTIp|6=}$4cʁQ"g' F-!B!!1C8HӠ<܎$vq%<3ǭ=) `sXA^^v< ꏗB`jđWVl2"TFF0)6aeUՌB!B |%ޟOz95h .WIP+X?8`kR ,L`(22/6 JOW ؄~mGkkx7&/$pp֥<F?eR&wM n2kwR73?aĿ1~<N3 8إT *,E6Kar &B!8>$pzf{"pp۞^/8F*^䯞nb"u  k5^^g#!B!q/8>puC@0 R™rp-.h}r~|گNݠ|O?BaiY!B!$p (p6?ommB/X/o77 -a.pts9;=)t]T}T RR/2M^j,k !B!qR<5/:pXK-pbf8V!5ryi.1sA)T0{CЯ\~ոJi)H6jYf%vO*\ B!8Ir_: 8[vpuKނ*b&瘼ZL 67ᰑZ Ԍ58r٫ %02bu2ؖ_\,zrE>/ 8ؙr"LSN?a/v]UX!B!qtz97_7S w926;b[>]G++ <!D&MQt} O(!m| ΂VT&nlƖĦɰöKB!BH$ݽޞc@5 #Yc^a8>4iʩu4=;#3=or䳐_ÆgRrX#p|F1 $XW]`P`)Pϕ~1DkcM 4ECŽ**|[2B!BtUT{cF )!LOQJRx @<[[X(;Ȕ$' )q kF݀sqMb&7d8NE!yIBfnkxxFMSDJ>2k.,B!!P>F&prVJ%Yדdm@B^@- qI8Α\ TFc--q0^ͶVA y|TQC *qr:ab^ϋf1)ɑs{Z@?bënnf<w1`;DJl; >l5;-vk  a =kkoWeJB!BPU=Uy>qjLL?Y`|/uEMOl|FWp)7p+:z*?rE1lV>i *v]JS\1eF6dGhY;{#pAEIB!8X=18HgIJt;an+p#]HA J }ŠDr6x_\s52%GٟVM5r1F+7/+A, ƍYX!B!!*S.+d[`Ѹ^7 5p$W* 8X>9N :ݑ|/2f|T{+7˝nzLa&íYqhd'Դg|:|'JN+\F6Ǔ6~7'l,1nD < -B0ѴFN=WNx;J%]w%0{hveC=9^Ywnw؊p["F!B!C2:} ]= K+gW// ]G^3/&>V 0h`993fyxzчfllBš_zcƌ6m…׬]?cM41\?v^~y@pTLBn憦.i!4S {SXZUP[DooSKGk_;F5ф/#fh544}-_?8uQ陹ťv!C*YY?v KLN{U\ZQ]}~@/CϿ)(ա]E%9y/+j/1n8)^87 G褉+nu7$d03dCS?DE@6L>}޼,usptt޳w?n*n{:>15$,GOӳK,pS7~:Јizty'u@8h28b:a#Buo韊}1_|b* 8}LLL|c%e &6LMM=!)huqW_GBâ&L(@t$Vc-"WMs-g"O:9a DU ?7gwuBã'M#p@oܼÜ6ZBr~B:*j&z 6s;( z e&M̫GN: xbeM-=G}hAYrbssw}|s6_ҮA-,fu"6p po =o,\$ŵ_ ps,mak78Չ>cFtU ]B2c8 +ه s_JI oA ._to^FV:1 3gB@`|-=G  fN}wK.6ێ|Zں_L;8444SҲՉM5{688V\]3p5yyz.aiiUPTʌ)i*÷3.:pHKI)&$1$/bXd)=}s7=T[E0|RC !pL<%,"YtU55<@4 8ki[)==|f 4i@ĉ.\^мpb+G`88,1aĐ(fuS35:G'g :zYy1nxAغu{eU]GKy}|h+M ~~sGڱDp7A5hFށ[:96zѣ1m}@{="omENv'DFMkNZz6pSyemN6 :rN6URVy<~ZG'J }х5cn!999Z fu3s(K!p p֮ YPTfie-+CZ/ Դ,f~<|+!++w.(67!:By@:Yyt @T1eu!_[#7ͽ4w<%ŊCqwxB_>e>;+ɖlwն3%K˗?w<ٲe˜93_r_@@3?m4ZZPHB/_u˚5?1#EI>Gu`"W|ɛ7GѴ)NhI~ۢK>fϞ5wew"ԭvLOX+m\|=@ Dop98F}E|ӼE{I9dݤ&Mqqi#cE;vpђg/\w$UO-X{ʕgH 2A! }5jL}VN"˭Rj`pB/]O?Y1gΜhYf? 8-\bݬ 5züJ*fAXƌsM$i=A+M4 jځP2w:X{@D9j WJYf-m{%=}~䡤MB,6cLeFQReY:}2g' ̄֓X:l՚ Eu"#&ű#a탦7o> ʬB qiӎ'0_{x)]uR@aTFtݙsoB9a_ 6Ķ4d<i;^4`"C !D}8 f@<! i+ҳ؂ >fs\ v8x B^x/ xAxޢ1;u"!acǍ'CFQpq[woQ!x}rA+F5o܌jcPA!mڶ8@ G|oށXHR Haș+F\`c;k!)+T yb\\ӥKoVBºG'SWV2rݾCG(o;wJCޣ 0e(b6mަ18B[޻ kYn0x ODT,c Qz{ƯCdX,LLZRc2rhr.>_dd'M#CeODQ?` UaŐW+ֺujѢ!t2RK.G3q7WXD :I#_BŊH?]E1Ak @ @Z^a NDb̖2`ZAcCk+\ &Zm |LͻSz,q*0HUl?4O!a(Ǩc!n!߲m߶j,?xzWL"؄mw׭E3ǒ!CzNݧD1(DXr%v|6mѦ^-\ v'Af1 YHրH:| h7n+ocjCF@%ymS./_U%Ok[_!0q6yl/).li3R#pLU5MY6nXǓ/ǪF~Y5k㣡,2 *{RUP.9NMR/L(ځaTUTUc Ǐ'|x ӦIӧO?MjJ@ @ @J |V[B_ MO Phoho7m+yiQ-sEud`)8)SͭXɉx}N`X+SgJebIHztE K3+fJ %Q 5j"ID͗HV4@ @zZ;fTuU*ԱSgk`(qN@z 0T /ĤJU-ZbYӭ{O$ XlҒDp8@ 8jy;qZoؾC'8ZcƌC #=~HZ嚫'w)8Odj kͦ, ԄfE9jtcPj…xyT A즨[$,[jHWwڳ#O>wՋ_rUWj\A?fGY֙$@ !)FQJ<.zm\uSvbt ~C<~DRAvM?oyC%NU㚻.t~"pJo /_&=|x5k7͛/5[8XF.CmCnOlW锉cUR&p?ƍ6j0en/Yj51U+I̽}O[`€pC?OYeNP\%Kң΄@ @hdH⦩8( ZqIfb;ttJW7B!D)u;7oђ[t=g_ u%S(Jg͞ …K.pp/e+or8fAR{@$%Fe)8qTpţD VZ, %4P2qϘ)@ ؼe!8GT\:u ZͪժsH)F;~fW|ƍp1#q#gΜ\uABĹC΂>G*0+TfMNH > wpf "ϜiN](\td+́L -sTWFʒRWKrI, =Epݗ@!}Q[8@ 8@0! 9s19iTBQ/\0@ @  Q \|RTի'4bAɉSg_zY<<㧈ȕ+7;̇VbK<8.]q6Y  T\¥R#"p fv)qUN8`x7m%!"aћvּ_ЫWD;eb8Tc+ U;o|ֳԀP=@I8ڵ{/|'@ D{uȤPX7^$:EM?9zfr(bInQp8B"5PB .ph"Aa#RlԨY`u[T"۲Uk&n 0ym;+WJh(Uధ(G!aQJpdCN(g܅ؚX[.A=z}e%EuUNfsN @ !w8'V0[]M/j|8E۠A#9ԕi2>1 j2qR¶RbE /T2?!phG~)E W۫d,Cjߡ_@)ði6$ Ӱ u8jԨmcT?,Ūk.a={#pXKQM+"ĆC @ C 0R(YvKpa\yOQ޻`' S6nNĂ[9?/XX,#'gQgPN EEUW\*/p33EHRl2cbNjʖmj|-J2#4{ذ0|'EL l"cA%+~Re <\l"%}ж]{#8-P=8T *޼@KEQZuFȐ!=Es>6qdDp@ "p]i\}=#Ccp\xU=R&W)]?z}UGW7y#+{[±ٳrԳWoyDY֭nm;~Q[wP X)‚)ӰpTiMڮH 2~P4QtKBv^XzޑX/G+-V̑f̐Mv>7ns/q}Ep&@ "p0C;wLb?i [GC/p\s9gN"`S ,]}ܹ8M_[^AM AÇtsSm_b, U}mdv(ʹmE': mkMWxƂ'"1 jeHKnQ$~^b* %_-P*4eʖ3nCYf``pS(<أgo#oB% aRe;*lb>u5a|ѯԚG @ C 2u篠m8/aGRD󏀳@ dDɒ5R`B!dAP·~}C]+Wҵ[;0As*wT >!%K2 c&BVR2e 6aʖ 0 %1AE\tcO,\rӦψ#(,6D(RΚm;nr# B.|o1?* FoQ[[*r$# 1D>W 5K{4IM&Mɩ0kn(2p%?@ !ϿqV=XjkmZ^jժ׬YTW<} #iӠq hfSI)DūV#`}u @PDV S.Lrxaph$ "9YA կߐXUF;,/eY4ԬS.(sؒK)VY,Y}EDWV%r'GJ( A&m71RⰛL, yW1:BãӘǢ mڼqD @>~²/%,>`i wE@ !h NddH~Ќ͕$3kJ_P_[ٰq&8*cst\gAwhN>gQ4 IW{3}Ƭ &͜5g]L1lc궽z1% 9@ D4E9ETt<:BXD4 )|$#GOM$5tڝ틗,:m &Qaa Y8p0I(<"Ǫs9ri].#!&.B y\5}x~k5m۶ljICUpN)Ѵi32 |7t%U;pkOsV, ¼~ٵnz ".[Y[y9+֘@@ e}4m2eLBb dPo,ƌ AH} /ZZX(V xYVҘr8w']}dt'!Pum@bH  q&/K$/#8 By Q  Q|Cx~XDLtLfx`ǠDp0JDLXx4Ov < e4 `! 6QcˮZh6)1g]?a b/_aæ-wp]Bb5 6&CБu >}4" P/rj'ۄ|jRx;&jGzƤ}-MꇶzBHlqlDHh$U d!XD&'ܧAn=lxj^0hg\Xzl`C% @ !rӦ/Z$6.>9%55-}rz8@1z̘̫λygy[r KJwwȱS^7`@C^oIK|^{de߷Sg+[:Zl?fzasݎϺz^9kx}gwٝ'6Qť;ݟ۝=2ph _`(bھMe]Y@zQ=^8P8BBB_xy׬W׮hsK5,Bo &],'h{v-۷lwVu} ڿ``۟Z:T}EUEP.cNI Z>-mht~].8!+jT}UMKECicAgϙ5{aýtbE6ptB3@eѓ񾦶qe3p\!Dhh؍7\T.ܽ_2a$EQӦM?qj:^wkmGs>x_[߼l EQ~W }[|(Ik 11t8Əp mUWFߟW=Cq 77h"khWE+0rԨ}G_x-A񾱹M(߬1s꡵[1t]{k8XsT}SKGlld7fϙ{BCK[gF|m_\#p-д [\| 4G|pi#**l&p34װ@o?8C[ ZRK{Jcs}<]H#"KvhW 9%M+giSH ]}K5>Dxx|GhhXaqv5+K<==m$|}}S2>YNRTR%$$nhͪf+8P"bq6yzOH9"""ýMnnfWPۻ0.Zz{{@αfׇ~^?|ƕ_3b"+Wű !F-+-,* siVLHJ#/XVAM˖X8-b{o1אy$ λ\?ʈ_k֪D}+plg/͗_@ 5N0Q$I-ĐŎJj._rWo8h38U<?UԹr.\(H!1sk4eк7lԄ?k+W![1p}4,oAQb]|dh̄C jEq11l+D_ݸI8N:3Wn&_8P@ D/͜%KxCXfm;m߽yN3&LܨqSx>&-8-{[vh-6^sn0Lǜo3?H>CHh!֮۴g]tg_:p(]i^Re?N]$֭ۮ\̹Kxy~c-_ݲU,YASd04PÖƍ냧CD}{ŜXXM6oݦ]f-p,TH„"[#C? SHhX߂fϑ3Q*wb_CM:Ӌ?̚}}!EQ͔.}YM WCNf66hГ(Q"~wT|||[!HQ6-g\S吻@`8x$fBQՂV ^ė:ܚdɒ!@ C B 2l?ͼrO9?|9rza}7ū? z%i/P hG޻x'Z;n}d*UA :u:;pU„54 uQɳWS>W?p"7бs~;u̅Yqæ6oGQ2mϐvlٶ\7Oxue!!amՓ1S:uM1{?`9gmܽG ) -}i8Hoa'N:wނ+"BQ|m;rr ?4nҴ]5zWT4!vɓ39m,6)SfE޾}{?ؔp ݅OTCX&-@ 8U^y]_ym߹w}זJ 3lܤٝ{ɚ/_s\?q,5 ^q&M׬yBAܵuLeeݨ֣WR??&7lm[ R3y$ZN<ꁷXv-r=Z?&qRLSm%X,"K$IRYh)G9_kv6'J!RDw{x8dBvߵ{OFI٫jE<Ay pdk)[<3/'o^RuPg@.ÿYgcTU /܋%K8˜>;>󗇏EOoڄI.&p2~$BN+l@ ;uҀF<~ nܺ7n +eȐ? YdK+wVCMoy0}ʕH"-fŋ6|k7^GaWrCsCJC3mڶhz E(% #muRQ"'(Y 1wBR- &n_@r2ɍrvO-x 6l܊bcޫo{ } 5e˖sbV @ C \L 7/a4=;~Up~+!4x6! #FU݀AE $GuC /㢞cOS z2OJx'Lӧvr"Nx(:s % qbJPFAVlv8p-fJ۟2 6\%Ԡ.QȠ>,VpC*qi%?C"2dDWz×P1V#1U³T2hdPցSuBGtEhmddN7YI3:c~c; Fkܼ}֝#GAuByaf*Vh2ytRKDT_HZwȬ9۶kON cŷbLHQ[JD^}б&p4n4 0-28pAȞ#T l񠒨oS͸rۺEV^תu-ZRRƓ ϙ?Q87Mڭi,[=;|A @ C =ShA7C,"2rN}ǒ+= l|Z%IAG>N(BǶBc&-.7hkvm u9S?Ƨn~1 n'c@ @I$^Ga֤ .Pp׭[?!d"(Y 5jr^;׵2eH @quZi`6i=ZTu?+ǏOQA\j א2D07EygYWSӖq.FcNJ, ?WXE.4!DŽ&]DPisCE-UZC RJ2X - ~X;:kSf͞->r$!Q?@ "p\){0eT^-9luiɑ'x ОC8(`1djٲ@dZi]#jI <$r@^$YS(0!B.a@;҆Џ#$hE}Y `/@ *pyVSh!6a[|0ett:IcQM!$I80Eq.rF¥kx|uC @ C If1mA ,䝝rb |k ~ tRV))׭vQj(~{H>Tq0Y&k 6U/UJQS_<t8) !T EÆ9j@ѴYsR6[̆bo;vi@3`jEȗGPj&plܼ7](Q$<~>h٪e@ @@?x蘕2(d2:|ܵ xaͿYZ5HL)(TB5ԝô!Ʊa$A|kAײ dటp:d07 6p{ >)/+k1Xfُ;4ͥE1AK*SYh%b?AO&p@ "pD a=ZrWkծKj,nБj֪DŽַj6)悰/x&Nޢ%oq+ޱ}LI7`P>Jq!0Uu{/_Ѱʣu8W%=>bV_C>SOvs,\B[uSX (6/~A!fmf̢+@ Dl3ޕkuBG] bDLwY22(jWg64 hf3Pyg5}Ne֜yZQ(ţp5%T&E]ךuxt=Ď ڍ;oY1{zl@ A4AWw;e™iz3c#kl$8"EJD ÐO>TR@J_o 4;Dau 6€&H"n(y@ߖ%K1g8V ]W'W uշ v\:}&/GATwkVw58ؽWy!vHbc/U̻8@ 't3ťJCr<|vIϐߺ8z;s/I.ҹ Wu!N?Hj~P+Dy7ﴉ]vCBc"p& 0mBq5:~V*WaE" Pe+VSc/c8:wjL45 QnTi,-4TB"0 >x +te;wCXaU8X򃈧@<}EF=ڭ6n$rp!pw8(;4cwm ٛ4yҏ yt9?E@ 8R@l*ioihٲ&G\N:JP 2#`D߾@++?ypRH&S3*QD$e4oj>~&PX4sr<G ?|NUVr՞`&p8"pt\=f'8j+QsA=gdxCƌxv^r|/2%x'͕;\ p^QD(n޼ V^wU"mܼuv㧲fˎr*ܽm8^jV.*qjԬYxЃưˬ¨c%pu8FI QZ .28! a>x @ !SQ\ 6n`ZB 88eT^X47O|\p-p:`.2Cӷy5 TKTS:|Q -xb.pDZJZu|8$BHÆ4ն̝@D1sϢ̙+u:@ @+QԐa#K:M;@(.g_28  OC>L ?ޚJ۰J`Y$eTEt27nSP@G{/-9ZP(d\zI)=g>^6 @ "p1 0c1nBt u'-;$MsFɒ,Zhl!$Ǯ}G6#pP@vkfoS& zyjf.8<._b4ľFQITxtYsæ56iR?u @ @ ơZ/Ppvѭw.Nhan p;C ?vWo/Q2&eOaٲPZmn+GF ƕk !1 0cacVbeny]NӜ' DSE)Bc@ 8ףW{i<+rݵ/_s'pm۱Qz 'M(,* t믣ZeN 8+Nk#$4̮P`  7q0SOU\~.nKT=$CKЖͻ4B@ lbR(qQ!1h/ tE_rÝ`JP8klz* Z;'*plڲ=~G_r/p朂1­A5hY*7q8jDULFՍmT?LN0p%$$L @ D>RJ&_>'/p9jGGXWycKx7-]ko77%uMd+W>Ck4|2uwמɓ`[*plܴx(Ӗ38D5_ 58ptQ2!֧qL7MxR8diQh[&p5nҔCDH!a.] GY:rPlu=./]V-D/1R?a{-Y9K̶:r"s,. Qfw=z~ڽA/7X@hߑC:&ۯ? Dsg21vDst>4l&7AӆJ8kV,oo}C{1Ӵ57o˓7y@ NPkש3.,Y>(9}B|ݑ{_qG#} a RmK~-[|,cW؅N> L7+_HEqI8xv G(ui1-R޾:^ {֯Pӧo ^]0q3v>hQVZ |Wd蜿xծݼ}eJ H6@ @'=vĚ<~"[p=+E/Y2KH:uLjaZXh BɉFP۫w_&8q8WAV|9z[X ꃇx#7t ?Q{PYjXTAC È%g_uq s޹ljfǣΡd(#^fmR27aދ*g⤩eCPrf 71fYa,u ϊ*Dx>vIT'L|ImK/@ℱ') [.]^"59lڼi;y)ʗ`"r^V%ؗf-֭G)=" ƌSG٢s؇ZNʕ۪q 4*j* zǂEK8BJA%5}py(:Q4}`˴]c ;7K)R @ "pvhiROr\Jqo}iv[jC2@T >rQ|@1Cؽ /S̎sBP)fAa9ub5ٱs/4PȊ/~ш VMשS/0Y2HXH*SPn#{Xe,8᭐t!w! \qKQD+,߀AO W9bʈ.Y]媧 ~?ў-vr=%~#{QdƂEKI1=DwK$ 2MR\ɏ^`6uiaGZXVH 1@ @ q5n FeYYi*;)@࣒@ OSʈ 6($88ft:U 63*ڍ۰\iK~ޥș ;o~K R/3suҧgND6jspYv@-"p셾* %B>>3_K͂g 3w2_ԀxIlQcC}+3`B;Gϥ/zаpJH@ 'l-~OqR`Q}xx=t-ZAkWI w'pEKUcT6 ƴiӑ(9J{?bid`ebW|(0%L0mbVgkԏa#?ʕ`en0iʨn' \`Z&N?ep1FG7,EXfѳW_Fiɸ 9ߗp+4iE+3SeCEN? *~SrUO (e& 0Wj*~-P1y4~ LX{5Q0?W҄M|zJl,e%wǕ5jԢEmZvYNnؖ3f׻ﻁ1*_ZJ8KS]fjtƗnIU,٤D @ "pD@e/ ʔ:`G/DurZƏ?g\WZ-wPZoT@^Öi\|.֬HKu7Ϝ=ZBk(=qO, &3f΁֩[KzZ~`A ߶cH׮kuل{~l/Onn0#35)AAb1x?GU`ޖo"= m "n+sK[ , !@ C PR@Ÿ {2ջrO*P|_*Fʅ8jPwl2!g*O0ѻyرcAs]@p 8 8@p@p 8 8 V+hu׶}\>FTQ] ׶ms5u;mP38@@8@8@8:pGQAIr]r3dT1*FFZhѦM>}7~oժ p@Ή8 yy%%e>vW+N<;e0  p@Niذ᫯yȉ%5U5_}Z/]QZVA})M6=wAeT~,4iw?8r{WٰpYslڲ=$DQԻOCvnK /|Fnj\ \q0CJ=q3J2 qif%%e._O/?Mlp)^LNr? 9(?|D*p:s~ш; 'N+.q4xێ=qę¢(wmq( p@zݺw_~S*p>z2/@8 S+WO'pѮ]e+֤ǁCGnj'pѪU%VǾGF~ScC#G388[nYhi*p{pĈq@ײey Ǯ ͚7;Q*pصoaдi9{8( ի\6h GQkmܸu#T ^$'?O߲wqq5"Vcj۶SVX۶4SwywfVg{^󇩩Y&L/nٺ]{,]>e=zLhkܻX8۵oOoa< F[[m ++.Z~O7ރǗ._۹{ .HCm8;L1s1Y%xeGƞ:s~)x?DDDDDD԰"|hi֘$|yQ?%e ([LH:̛TTOKKxPѡc'\#H?~JS9!Btv׵,[fv>> _\Zw.*PRҲ9.]4ADH 0_c„I_?e_tu^;v;~הD۷Ej 8Ν,>9{![hE |'Nr'^z#+'̹=zRSSӨf|r,B> ܹQ9"8|i8""""""tC__ٲ+&aNpp=~hkgi6wbIPvGD!G)1aieĤc(tzJjPVÆ *?`ӦvΜ+*)ӗigȔ>>W1vDzĻ/tߨ""""""Dn7ϩt}"iaaYͷzDuێJul!ѨD՛ÇjE"!$bO%;"*U56uz\v##:h{z#7QGgΜ."""""" 8dA/<$q5, zG[ z:!D5+$&.MźlQ0^"<| "P~{HX4񧟫@ʠCP@MJtlbFDDDDDD 8HuSlll.\"r ,\TT !@FҋBIШRՊO/_!MphӬYM1>>P'O*|+`Mo`bꇏo޾zZvaADDDDDq)y32s;ñ E&*#yQUľ^B:g.cmq`ƄC1QoYqY eЀT8}tL=x;wN#<+Vo}{ qXjY6ҫw %E#TP(}4O=|0kCpPC.M8)-#rEa 9'>|AwbܵG8"b[vSNU17M"njUWw+ѣ6j45hXzJ"ڵȉ&]AR)(,Pmpl߱[H#cZj]-($B(@u&:CZ`臟Ж"zD$`S. """"""@S\Z * =711*Sax"?qZKK[mw GXDL˖j 8a6nRp W|E+x?9Pj""""""԰y@mE=V%;uNHz%!aQb !زuGqIE5Wֶť^(_X%)oPi_Ŋ/0O~zzzVCDDDDDDp!P(,*}zlEE8C 8 GF:99)LиRZQxɲM q !9wP߾{aT> ;(|GAǼ ѦQkDT$xӹKBT׀1ĩYS9(S[dj}tzN^0T(cI~f˶w@,"܇T( ¼"3f>JhZAoHmpz ^޾i~U*v F~٨xTsdFFF­HU">=~/azipgGN I/ :6ãMfbUiG`pC]fMڽ5J6K[S:u޸iKdt<2i!YRkHT0 ڷ JnjzDdw> A`p=b5~JF9sUh7ע Ϟewc܂=<-,,TW/[B40{zTp 9jLVv255kk3g?y %vET% VpۏB$wpv톢F(عG8Bpd! aģ!>s p+bADDDDDD*pYXX@tF]|MH}5iX9JP ySnF!U)*zZ[[mn칋DDDDDDRpZF&ݧ}:>vu VܼuOJ/ػϳL8,RvUOpuAo/Pcn,ADDDDDD 8HպTuݮŽUԩC&S|u1GӪ",=uvVޢlee]( nˏ8mo\|C¥8NlܴP8Kh`QAa?K %DDDDDD*dM!BE8>kk={=H:?ya̧2 aʋWz&'qm٪=3s+e/^qw&&?^%%}&&K{v(RyDT6 BՃVfI2x2vxEEb K>[ M7B\ ,[}0>[c_ܫw7,;Cg̜uR Qʁ77w\\U*>bptjz>OOipQ~_%yyl!o1 fVV6o$NUq`߁Ol׮=R;=v]ѱFQܽhШ_%d I&̝NF;|(tL>sPiǵ6 Ղ+EBWKph vvn#F=%%!|'ќ"z.""""""Č)̙/R،Ovh%$#h@@4|ӧM5.(= ()P4d!'0#+7+ DqĔLL>|$ָ"{Ry˿XsjxdL`PXDT 7 ?߱sS&HOGyбV@)t@1rx`\ŘԲs:. xP{?KRw0#1&Ldaidf!޼fko\_5 `m~O}c a [A`ѷ__~KϞG$$|GL\*Gs`y]tET1242ٳע~<|OGEO.\hZQC+yN=zpq&aĈQ{tvvAziiichl1]]+++w6cڥK6mafhijbF˖pJKttt?1&&?cj>Ig@%gݣk;=gŧE6VXZލk۶m6rd@@8@8@88@@8@8@88@@8@8@88@@8@8GId8 a駟/_|UEEeE2Y#''/?pM[:}y lզX8 A?_՛/_Ykt+^J5kըYj%J̐1oŲM(=܈#O%Ν;OƚjBJyK$O?>,hB!BH­q/Qs,w'Ϟ8u+Vܥ[޼~2H$ѣG_blj\zʵ[3f)X1o:KDR}_qˡ#'߼/߲={{Gܸy[J*2X QdȘ9s\o߽g_uu6܅+~[ШqL2%s!B B8'NҰQk7\y?eFջSg>[Vʒ"[b.]pq]wxoÇ?vqT7ԭ߾C'ёL6cᢥkoڲm׶wݴe;]WD>Ȟ#rƪN: xνGēݺ;ŋ_L9>sgV 2u 2P) 9HE#mޢУgA7aƬ9KM۶n5gB kV6m:F˭=x'0Q}.[S. AB&LrnGWT'Wjuw`?~oĒ+T3 %KV3y.Oz@O>uQraЅB!!p\g58nݾܥ=z";pYbXa9$eP@Hj+a$1$qȗ8Ԅ1 0Mk?ręɜ#RNp`YpHAMP%tE5KXF)IXn ߾݇,&$ )kyI^0TlXxu 9?D˗;sn} KW/Xb1c l$)Rvs܋}l<Wo4jܔ[!ani7og%GϋWo/pֽ&Fq!9, }OV~rڌY$eP!B? o_^GLQ?X/ÅOL& 9 |FH]iڴqQab%KQ46 O>M>{!{MZq2fDJ9rPa 6} 1j䨱+V9v3OrU2Vٲwe̝{I,:lLիdcAd{ǖm;RD3G1x0kNP<;w;ժ]qF;7$z(ӷ HB!8?nďWE8G5A>{U@"y"\rb=G~?ٞ,Uqĭ_![$zS0C#vؔp*=ja7 [ȡ0ts{BVQQ~RF7޻w_"vDyc}Ibؼe_ 5QA=}W{C< uQkmrV 0g!"[w3*/@$V{ $n#ȱSz$L @Bz1Lw,Y/T!BJUX?w !(0~F4FobὂU*~ Ӕ/_4N(qq'ٵ7 Gl1Et>/]adPd 8B!8Q+%J.!BCPS<#7CN-@+%?Ɖ K GvCĉ!Y<gϭ;?9f+@=w FM> 5ȭ#LIApGS%I$I5˶cJWXb1d&MFv ;};vv%Ag!Bn۴űS}!CGp]jHx⸨2{ܔ1`RW'q$agmɈcAy}II6i3;p'3'(w+Vlk$f:^k`RiB!BHrEk d;|p@'<#w̝EV[_q"pdI'߁sNoBH; sN/J]yeD7; %܏'F^,yzRCe ?FʔPm_~8ͬ9ȸ p"Pb=qꌐ0mv@1b0Xqedjũ'Ƌ-HSR&ehIvYN$LJ+ԙ e˕$k7nۧsj"sT!BH2%-7,E!|DбIAWK2dJCMB_WWWCP:sCS/64B/A /[0.j_6۴s.O4pP~ 4aaI[ B!o0IDMm‰SQrpr )'L]:411WaɃuhbFNAC<+T$ŽBd Ih(,c]D g.]n4N.}.JAGBgA5}UmbN˔E 'l5j܄UE.;uƳ_(8HSGKĆM[%K\A%.F t !B!C|P2KǍ @|Zp+nz*pD)R֕#'u"TQ CtIHtߨR!X#0s*qa2ņsɀ[qsHݪuЫY`i3E%r8 /\~(>.=ɒ'I.~nWFBu°HhѢ׊;tz_}WB!!)R<}3N1'zcŪ?=G1gWūcfƎp̤}`\ VFN>&LD(D#q8 |17mf'VMp_1ܣ)p##^x7o} @_{*250Б2g 0>vęٲ8x$XhӳW+D!B ⻃pbʏ;YКfΣ>NҪUOc AԶXGz2} ɤ.lL+k r?ccll"YDf #6"[.R0*Q ,tuñ'n߃{)Qw'μp ISH2\Cͬ0gl፾M!B ;vEKW2q ^w_%&<-46Dx ĸ:m_(%GSgcĐ=G>u!7l$3LMFL ;%~If89vVkXⴇ\H*?CD؉3d_goHTZً7{wZvNQ 2؛rHH@x229")!B!7Cb… q-SLdDFWS\l%!w]kk8(- 'D8L;A&/YfݸyR#Fѣ7'MO{T\m>:Ɗ?1Je0kNt?N!B!CѽOl?}d{F7TR-HR)R;pc#O˭wy,pPڰM}~zGVǛhѲyKj.SxpEŌ@~ r:{|W[+ѻoo>8#B!w#pAe/_bw<}Jj0B8~n݇<{5yI_y.p>o|/\Tjx.pċ^p¤NIsQf,n[aGw* [-Y[u㏿>=w"(_Aٲe'ˌ9&LDΎ᧋/vH <&\r~o?`/]8' 6~Y y{d=SX&Lb&SfHJ!BXj۠as/8ΓA;B; ,5oqM+\744IV|c!NF/_ O&L8|#8򩓺JX$3g. }asK,;WHNGa#@'jܸif%{TVW >`.@}+$*:qy 9AL)б3ug]ThM`sy'KP8ɒ%s$p}C=82km4Sxv1p0^9n@q}ƀMsIg7.rEs|j qh4ldIQ]0Q"wCVN󁃇ƏSϒ>}[^~hbh$IJqp D~ZP>ftEm>c6 !B!C1jqqUO/Qc']a kt ,9ăZZl?x/xxFNGiX7 88ʗѳoԪ]ν<{{anp,eʮ]@PhRPaVSYV(#d ("OP: *wj L١35l'OpXxrˌBJtdhhبqxP$ˢzۆջV-!B!CH݈1"?eVЀx ]_H *SX.x]6|aoA  ^n۾-7vN!C#NO(C\ >XѦ {:H >T0l:s jn)?)Sܕ7- ,YVƷѳ2)J&Hn.ZP`JA+W>]BXD'q .z s/ d #gOa){iܤ*+vlXQ3eLӦi{&@Je$!B!_CH ."̞=#F޶¥k`Lb1h0:ڟ<]*wڬV]`hj\}ʎʑ#3j% .A3]PUDG,VaFbN [f2bkݺ-6:Z/]7m:.qg%8:|4fAcdRXBޝ{9N`2zH)) SȺKWnx1 8^iּ,1wًרE8p]|=~tczèn޾,$FB!w,pA0Э{u78u@I(\' (f~p ?S,N*#]owIG?Ao߽n2{qp2c A1' q_#p<128zMBSGUz=&Q7«רJ(bQÚ4mn-u/_NY~ժħzѓ3d`-#}x@{шPXeai/x5p|+Qd8py4ǚjL2f " :a:&$EP'{(HErf|7Ppٸv?a9I7_6XЇSA13ЩsWņ9T؍!ewH]w?,AHf1R*p?4xb;GuOSj9wdD ր5~w/X̞3)43ceM6S+tbBǢqIS=^FGc]p]>>yjv~9C_|՛ϜͧlFʛ/?u.p1ѣgoL^ y638\Ct/LO,QDHXb )]n|s>cڇ#$z}0ߠ!-^iv _ Ah'HJ,E C[r~H!B {hGr0"?H.ʉL <`É 2p/}nMS+!²;秡n6d(Xb W >:Q~Q][LRe߼\hI\ԁacdYt9~ 5iР؋zsE]1ȑc2gbab1*Bv…!l8IH$+n"RxceswGϟ sV}AG3:XV7".j]Q0ڷMd`3#0`!a!B!$p|H }tԅ~ Kw@@ K,nց!%xUNfVd[֥V '%X6$-qt X:Dzt8)vc܄Ih3p-]:﫟=w)_Ĝ$K?}iXQuqȣxi0Evc!لLlX6%8i[Ho߸CGuL΂EKhg>VPoHQn0p?A([{Y'"B!at<%X"T%fÀ[w#>{ l@ҙӷaƥ˔&/R1uL2)FOAC@Ez}*Qc}V:H V 3b 8q8¿{^l2pPJ`Ǝd:Em۶'K9p qQr~s}u03ӵM٢e+5Kxs03fs \ g}>~$C.*QBtAl"C/.(F޾_hպ E ̷*LtFFntQ֯ڠK {+lSBeA޺}߰PEH>"ʬB!B>iEgnf919Rȝ{>kdBŊcW`El)O7׿%00`uLFpO+##] (5"S)Sa8JH+URdi1%JDU4 E1*CevB :0Y \cSD0P.!<7 B+qB!BHR!q;Dz[KZ:2! $tB~ h "6 a2Jmu4GF~ '9b%0B'`I,Ye$ !B!Chb$z7qHO 8)i!ʜw.pA%pBL=oC1)Ǐ WyHAѰ'wTXɟB!8YANznc'`L /&N7 OSJ=fxJENH\CUݰԻoaR&rIڴm!B!H;C3fΡɳ-ZN QA}E>w qƝbJiU ݇?Lm&,EeWQe@O>sv BVt Rr5{!اs7 !B!CD%u/㽴Ay-U!m8I'K}>* FWm欹p`Jܠ,5YO!ƭI81O _`μ?Cp޺}W2e#%B!B^^^i`ae:su#|0|pJ\sfZG#)p`e2o" ߪY2U! 5gQ77h%rQ'NB#'/K!gYdUe!B!CO"g\vosWz{B3фeAH/ea<8p7q霹 /jYbŊ]j=|V?ժuq~1sD!B B7Ν;9o!0_lޢbm4Q"|=c~u+VfV鴚+W8tEkEVK2IDqL֝nD~΃3f%cB!BH"v8wx*76 m|C *VuqfMܡzia9Ə$Iaxqd͖=Bh+klmڶߴe_K^zZ8B!BADYɒ%߳+퇽b5z+1o)S!UTFǙ[vHU OΓ}N7m%i{)j;qzd3YRHc|*W:iԣN=r# Z$JQQB!8$I"=c'`C<Ӓrc}%urO2-vϡqE-x$ /\ًT,ؓ&=y뷿G9rĈBB {ϽibV^pʍy WV=B!BH_~]aK nڬE`P׮]{Wv%:4{#Rڳo Vѣ{E`xѸqS @-ח,[٠aiҰPI y)[|lPC-_<B!BPAyrܹ'S,bH~?8иLOjժg-ut7l޺{^pkh'MĎG/=9cZNE"B!!ﴳe˾.Ys "EAFl$KW`ًzg ;w U>C25(ZrM.݊/(2_40*Zݳ&L]^̙)GRʆB!!DȄM6y[ztbJϙqcz=TQͥn`2uNp;ѠFZ6|x]1Iyy`q+eTqO?!B!C (٫ϭ;\yϟ;yŊ,q a?"eJ QI\)Y1Lп#~H/^F 'Ǒ'sڍЏB*@ BB!BH 6ڢecOyU`r%K͛,Y4۴>s6>+:ǿJO1SHMs„ OweXf0OmޥWeϞC>B!B BV N:ʵ[TiGW_r~޺sפiXç6nS807:&ʃժtT#B!8LĈ쌱&?x$q px)<}ɳ/]n$I#T" )S{~-e)|nPAovfbpҌ"B!!ggҥOO^ YcSB~DžWO9бA5kա Eߠaz Pj֪]F-2VQJT\9OG) ;s>| !B!X4zt/L.鯙'OBG/_9rI6N8ѣGg?݁1 (S>XqxţVϼ3 pwI @88@@8A?{'cpMsm۶wcۼƶڶm۶;㤂s./o I p p p@AEz' (He W^iԨqv{=t~{?} _׾P:s8 }\a{| p(jܤɼ*j|EUISS9hdqވUT\o}ç柖W?8 VZ<5jL"o~s OʪoFQpUU5qޣcA^*@yb,㸗nRn,03333331;dffff;Lߨgw4RS[>M]IӦM!CTR?1I$B `$!oȴiK @W"#~ /$$xC?_.2͇~`jjR ]G}1zGtkW6odz6jQշDP( BB+\y Nwzxd4_ԮSoڼm߁8g[v?MvJ# mL37ڸsFL- k #ߺZ/IZ;hp/I&>hEΐ1cժ{3wŸww u0xASNg *Lq,0 ݤ\ -[ׯر:k\!y%JJ:5!uruh(_j\De_DȆ̿|KGҀP%8%JlӶ=׺uۢŊT׈/2Ve5oѧ7nj>c֬9&L2`V۔-W)\ϵ6>/^mܸix8 BP(*p(kJ2e4i!Yd͝;ld*VQV 4mְac!f^7ow:OÇ}}^6m^DITfʔS.D߹(o8xͱc-Zcyܥ+;:vB!-ZmסCΝtޣg}۷ଡ଼V:z2]ZiѢUmwH˴ߠa#M Zd)VH=zQc&N2}왳8egEmJQԨQѳ׷ QcM:cū֬۾c7> -ev{"߸u?C,,NM<| ml~?=p \*n`0/^Ξ~ΝfϞ:~,YryXٳ(XRám֬E۶[i5[6jQ``jjgS͜1sן [IBA}{+Sxo,<g e\PdI[+3]&*p( BPP:?pЮ}yĉSg]|;=yN3fC V%kVEq1exS4Hgs7lҵkw$^\ %I5 , 5`\ރnܾ{Q& _!:mC#/_Y>_ϫ7|>P̲i;`BǸfCʔ)uȷ* .\)[DTʅK׸{?/r(=JrW)g8uGB\޼h/u(Y4W;|VOEЌx; 8zza5<|hGϐFbŊ1stQO0'˖jԨ 8QϞ$r1056d3>o|,TH!Kܲm'==#Iҡ0vhXPJ=VPq_QL|mN`B#'NOwm=aot.]:njnɲVIf/p( BP(TPhr3H^/Uckms= PyއivtUڶkODJk֪M [?}a6WZn5ɒqAם|xNh8s/{T ,'۵[\UB%Ox/a|,q+[hԵ&`bLk\lڲ֝k-{XcRHѩs']n⤩'LZrMph2 #5z,mG4^p#GO]/Q 1ZQbe4F\Y4n"&FW9YVf`Hw-W<b8vFfQ*paCã֮4yt҅/Ychԧ0_PXh Hi|  BP( 8 )[ϯ|Bxuo%NURoaPF쀑Y`&@r3EϞ;ձ"2?  5>'N[>!TRhDÁcɿ%kF8J=q[hԑ6>W?[_Ԍ71hZ X"#Za]\L<2{%p0 O:b Dz'Q-KWFMIJũE?ywP}l v@0͢t z K߇qsM Zc授+anoFHˆU^t:BE .Je<@ȹ y8D=P?bƙ!CF2ci/|CCs-Z"peR4DݨS^. =s!Pb[%u~ޫO_O*} f-t *.'N Ҡȗ1wa#+ -8nc΅kJvɤ}Ql+%DdD0qTvL*#&L* [0ZE$Jo)E8KO4i_$FY^cB -g_xx]m*ΌX.ACPLzKTaHL3=Vl=3Zl%bsCP( BB< ׻tauA*> 1 y.‹.Zj@#+Vu/\W֞1㧄o OO@t jzz&弲&,\oq` 63gc^ &X;+.pSJȣԤA8 : ر3Pd61#wa9dwfMB]̱w`Dv@HA;hkoC:Z̀yYjDD9NҚ q*q+ҁ1.S 4VZ#< *,r1g $Jr*HBôfWY!Bs/"0` .=6*Rigc/a*Bu Z!*p E%P( BPC{x!֒,&qWh*$qPV7zx x{o>EE/=s! N`t*bq|"<{&ex ƢEVp |w℟?W"p).n]?XlݶL8 (7:Y[^Լx_~+gy9?DLºqV2Mb][;wA*EOY1#ϣϭ"#G"փ )0B|nD![$q;DŽ.Zo~,(≐TbnOa^kah%ag:;7[Gq!Z6 tbM!FdEJqT b`8 !p( BP(TP(^W1O޼A'!!Yz8'-J*nA\z7ƢdžxSUo0jղ2n0^ܴpxMW+gK6-&q8(JqFU90`^Z㆖ aB[}XQ;ѤPBl;iԸpW]/[/F[fmLܾ+ !czw8 &9s_% I׌Vm:#T'vڮSӆdC4)X"#ڸT``u KP$$J" UhfXX*P( BB.&}a+DRODk 0+jݦK\@U&A0VX+'VYT!_^3XJ*}(p0zy` g D)L؂lҤrvv\U{ƒcE-DYk)'X4&\?oPЬ,Wu)cܡ::}L^("#G?ɈGtf8 BP(TP$T(&ХbH|+jŎe=!T]y Q8_ |`HI<1*8+v2͸ 8 7W^\y~1uν}8T=r$ K>}qݧW>d;!{)I~+I&e1wB/ F[O$p??7f+TH~$bGHZy詰4CnMڶcx^Hb==UAf*p( BPP$P] ;q7C z%Ǚ.mDC@PIOzSVqX޷vb ^ دO|C1º` #p`k>'==UX[j~eN G2^QR7uJ_p`(Vb86 +5'DV'U(8-ak7Œm،Յئ ?B#lwy c;o1)Rԁ8BP+X(:{]P,!poP( B"Gy"U"B_kY(n1|"l35陳EJ.b ޢEQ7Tk;Wm(whO2EI35b(r(a)WAl;6nfC}v<߈z#0Moϙ;߫~*Ps@' T\#ʧo!~Gh٪UD꫿z:5q,ev BP(*p(fN {ZT8Yppt8H(eDDž܅+AЉvs. Dg:ŪvDŽ8nY\G]\sGFb(㷺03[ >֬yJӇ۰Ҵsq[ ͚3z}هZICck޸ 쳔ajjp.k!UH^)I8a8K)4xUࠞ  BP(*p( Ø0$4GC\]E7yr4,Yڵ8m,2d |G1PTY¢ ʗ@QF$AMKZFL;ջ׬U;~I&*Ft GBq K00.F>\-4;~~'zz/#uhbΜ/RCGKn OZa( Rx9eM [sQP( BBw(C.pe=DÆti|*Uٳ7Upm6YJZF(axB/ /&\t;w8HwVSl9(Ü *U";WK$@jmTGʋxӔz80 _2M0Fү2B]48r&HG]|z:-ٚ 8t r c*p( BPH ps[Vsb%kuBՂ '_?kf͜텃|>aG3fyկ> ժU%pDn áS9XPӵ͗ȱSbH$aCIͷVS*p( BPP$2dȈp/Giw&pȨ| |ӯE4u7l:pzpƖzո+Wo[@[Z#3xD=Q`!x] koDžKWK{8СSxYdR'N/6n7$tD!aQ8(+/_ P !ÑG=+j8?*:H v|(X%O5V3*p( BPP$ҥy%> <*z'm9^C#` y㎝{[d)83A-V AQ&|DHw?lR4B jFPyeI8mv B'OGXA.ߒAF a+cL&ʖ-1`4#x8{9.EK}4DD;]'E^S鶇  BP(*p( Y2&9s抈<M1HlrV~9҆}a@ .HUxD'={QCx1N3T\Xԩ3i⭲yq~Ubd%Kf XvN$K ̒%/q̑Ǜ{  BP(*p(  QB+nB=8)p=%aG\&NΝ;Oĉ{JFN:'T('O^W4?Qk?/AJ$)p;7{lfΚ9]Hm۵'z un])D燁d Sٗ,ߠѓg~**$MG!X=>}I=~7|pIY@ѣP=>P( BPCHOL~#Hx⍠dZ_D Pqn$=v<2 7TAE6mEbW( 4>xK;pqdR|ʻӘ(C={߷V Z0@V}#Η22gwGibva^/gpď\$"E(tH>IN2׉GfE2zޣUݻo* BP(TP(==c18VKmAzgNAv Vp.!Mv;m)R  axdn"p=qf͚ oN/V%!( ;?hy%U-cE*oIS8ʐ$Hq~!&&\7ũ3w㡵~杌s$ptFyTBP( 8 8Ń!cF^}# | j 1c56¾ěĒ s|G9.L8 55,HN 47)R=NozzQMç!C ⠝y[A'mY(Qp DŽS8XRT87occ3 i_Gx/pH_na湠]>Tɱ'ƒr*Mxp}Z FRCP( B" KT"p@ĭ;')YB-ؘ^qm7z+pPr;f͞+,$@KQ^ĩh'zƬ{Ī,ލFPBI]1͘)m8?]z(exr]F; c^ e15UkX&MţPWjðGS<|z7_>J#\mwZsBP(  EB$SQ͚3+b" lAaGL֩SQpa.Y TꞐ RO)S7A-_O@Ɗ'Wѓď{|ePUP`KѹK7"q ?,eB7~i+\zCh+ /%k&8ZjW>f:a)"s y^IEBb6{|FIb"c:C~DBP( 87N ʩ#F[7l;tCʑc76sMZiH5өVU邊0Pns:cƎ8&f35v%&v.DLX{L:͢%ͪt)Sfnqr@@sM[S'ئ;Rg1vDS*ә9{.&p4M}8e.O#cR6f܄I"e;j_͒e+zء.Yd -Z֤#ʕhdgBN-g̜8E!>gJV&uF/`q# WK.t]R= ;lٶbJwJ۠ԭ[vU]ӽG/ugs7o#>~ +ZU dXJ{fRa_[J_#6 W3xkJVxQagL=^c`iň:NƊ{!Bxy S #0S&bB/аQ#G;pg\&bXXqwVΜ)CɝGqMa5R;b}gi,z!Ndtmb+VV2Yjev$cO:t8l5AȚ-? $sl4#k ۙZׄ9f̂5U&I0+ $Iy 12*!.qc4D#OzbHu?|bV ,&mڶc+WYGHŃGOJ@zVMD˜C]lrLȴ;A qL2ūg7D0e42:eb5( BP(TP(L |MjՁ,Qzxop~7^Η)[nEg x֨Y fkã těP3f`ђ]A?aJ4h-,#fFB"!XP*OxϦX$ԊKӲig]pH8s)hc0'9#5d͚6*V(_-I ˆ©aGqx\*k?6{SICD ϤSDDp K 'V/ >/gXQ.ʕ+-[v&6$:r  }e~Nm|E .\&V;vl\c|~c ⤩OL6n?`Ƶ#FyOY zl%$B{Zlx>|%i\;BϦBP( B"\~km 72!<}6u?F/ >Yh)z"ސ1?wB>.T[]H Hw^ Q }F3gɲnfń;qz.fBLIٱ3M(Bx'GJ3 *,yrq?.!$gi$P;6s,D^u>n#t%h.Q XMF Ч .\I2aFS\vӸGUe3t:r{vqϞ>p0mɒ%74ėaܾˬGo]pR xTe,B] '2gk)%KD+؆ s8TGdwV[Y˟붝 5<)Q( 0ѣq( BPP$;CBeH9x8 "[KGjѮ}9H0, V :y+OR9YA{ׯNUDIk)oU>Ze5sHǛy~ESSXvPƓK(֭m۶Ge(1T͛ωc(C%SgΛ+8'dg;q&$42, g6՛Jx|IҤVA`fUb {!>X!YKC!&<iiێ]O%%q*7i*C6}xY('Nibb0$!8[O>'1{n*@EzFe%BP(  E _%G`d@@2!*ngXݦM;Dsqx՗X5I!}QاeIe9E؁ "Uk$~b7Pj7D[@a WjP|pQ&eL(پc76$l ֥K@"aVsZ )7A:2pqaT YhiZ#UbZah%sN gʾ`Z\u•K/e5 \yU¥kԚ4;A!jO/]vڴy>Lsq+̈aUeKC#r" i2`K `r5 dZ1 r#1s2YfDd .3Dph0H4F̑ex6;ș5n8nڴ@,fGP̹bk1&ϰ#CDO}م?+ BP(TP(L࠶.ϲS箼`_r i;~:a'wp.m8~~[\d9l;w޹_0 p!!0|%ͬ0(Xo#v-Mڴ5ނi$VHfJ+!+u^H-a>?QEIYa| 5oĮjǜ8yP;Y|;hUj5wԩ e)S,SF6H]A)mC'z bξuCg=%"5 Ga[ʗƔY<+StYoȳ(XP|qP"DUwE`pQ\*,b2H:芾2rRoH!0%K4^sC.#zah*Z8OS葨:?@\X ])V8@no ;‡h [=%84cɝaHY-5eAReʕ@bfAiP( BPCHP 2BE ܐ4 _%ˆ0w=.*,n8 kԨ/+x@}iӶ=$apxȬxܯ_x௷FW,ӧO9sf/UHI O}=M0q"׆{ 2%#F m"GIhе}q=#_@8 v5G\ߺNk#Ϋ'$_g.0d^lFN@_x79ēO?s{GS>˒w‰'m3JW^G߻_ǧ$;7߽‹b8V͝ϾeFKm߇H\.q|g.m۝Zk޲fwYY5y/^ ;Xn lw[pdj>8 rottlxx64TV+J\.}'kÆZrxdddlllbb", p p p p p p p pK8@8@88@@8@8@88@@8@8@88@@8@8@88@@8@8@88["GrxIENDB`tofi-0.9.1/screenshot_default.png000066400000000000000000000677001441474151400170700ustar00rootroot00000000000000PNG  IHDR@JoIDATxl9n N ww+Zy~.-O kR}5s%ɾ3`3C/?H} @ Ϸ)C @ 0`\0ph_0$@@+$ 0`"@ 8 @` @0` @'0`   0` p @>|s]dYGܵ[貈KeAf^|ŒKNŭqUW_ڴ  ~M2۵woki:.ݧG\]1~%%%5/σZQnzP/be={8J7ӷ_گ߯s٬H{4k'x'=W7mn}޹swlٲ军nns6kԩS˻aQ' |bݢ3gmA[QZZo}k]aY=uxʴi6ٴiS.hm޽-ns @ k'_nq_yŗ^8epe4x˖-s\w]dIθqݧ1k֮۸qc[T\sfe:zZ:?y5ݷ/-[Id8+V6mޢ{:vyozwz { cb7߯\zu(tC"N >;vLK?qDhgip84uplDY+~F޴_=rNo#}u7ܘL@/\(teϿ}wZcZzc ;wI ** d;;gܔ8?%u:t(fdf } pJ;v '{ -m,@Y.x -2a$;8ex)} \Dͷܚ `޹K p `R ; HϿ4@ pߟ78ɓ'k.e˖xWhڼEEE4|K9z4 H6mTUUUZᩧQ _~jFix=bXUMv#v.xվƏKppCʂ˃ަOY5dXpQcf ÓȑG6oaߧ8@mZj}^^-yT{iGɰS sj<>3+񞽂ӷ_FfV7nYpozZ>8ʫV__PЭG AEAh ߟ߬PRR~WZb}pUM痗72}=:lǃ~~7iwh$wbߧ6@uuսϟǎ }p'z:uKKKGeG   V.^d=H |bPƍbeIC՝p %@  @ @` @N80` @'0`  0`  0@ p?9n?rlɘ4aƛ)c8e0p0|uf;XW [uɈSt3@gZ[ p3-g*Xsp:@Ou+ue`L^UczU^;KM*wK}dVbiyk@z_ZiO{ȶTS*Y2zt=k cRr@S7]%6Ɠu. cb4wgn $m GKvx$ɑ([pPdg=AJpO׭UCDesy_lWI )I>QW-g#ݺT3ZW4^~ɅB@Frmߔe|4zR;Z5< '9gPα}t,Scr,m9".&jk8,1T3-'_`G" +&8^g`Y罯ZN|b`ĭ[ט?aNy|B%P?q3 %ba`ߺN`8߻9J ׵l-K5N4-,< c0p)UpN|~N(/8ҙ!j]mK:ɜ_ >:Xٖwi%vv.n8i2LfkZώBU$~ ,1ub >g&n}8_|YBwAh]u~VW9@Oe"G+U^;x{_xvirb(ߛE0p@,F}pH<6^x>™9HsĝFiVn͕ es/+ljKmK]㵚gR[8LU{61@(íp"[:gG2>z;w[dG}l<ߝoqҕmt6uD7Ru+mVŬ1p3SW)ѼUej]C*vV:Q}l].dvMnyy|O.vQ_=m^O1b%xI gs8i2O](:y_=f>pN e)Wfg;h50]-R6 g̴E3ݺOζA-g'Y8W߾_xj;4Is*Y98 v׏rmT=u&+\LA/;5<3,KR0.w w{]պ*ߺa| X<8 g1'vZZěow1e;''軠Ҟ"LgצsKov`SmKlsy7+hsp+'S7[88_~ݚܥD˼E)ubֵ=<"^ng+n# z|V@ЭkZyK8,uh~3#uO{;Vr.w1l5> O.XQacةr9)6L{fͮ9+.kaiI+T0)T<[6yKe.vޚs[o*[蛃 ;',E:NKE`n!Nk O80[sKm`b[zgwūUiR=<&Rs7NYベv{3-}#/nRIO ^`}%<>-l%M-k U8 lk<#v>,e?N'TkU7ao⇨+l3ES"z_ij@ ]rmqM+1قc)~lRe5-ev2;&$l6rOpfR$}ɢp WuĊ x)lh54 dw u=n]ӮJgB2*ܻ}xK84,m/4 UҌe;?|rr{w:N76&+mXy]bm]}l|/ $m/읃\ضFm۶m;bW싘hs=g79yw*swW:o>So3oSN;w\ZKfΚ[og(G8tǏ۷ooٲ~̖#'Fܘ2ej۶ms{{uԩgϞk׮M1kf@ W"m^R?gp0bcx>%Kٳwq̕VjCN> 'dzf_[bwܙA|bbV~k^+Wʛ޾jK\ lܴ)h(Vr@C 4 ɓM5o2Gwc𐡢nxty-c$*{_ SPȕ[FEC|2`=Teoep?~zNHlITVsD:Go⁚3w^FQzezS\:)_}ZL-X֭[.kMw5R](.dʘvuum(JD߻8bE 1Oݻ*hRaFNڽ;6݋-, ]*%%Ex+t }ik zw%[-`?… ={%KS͹aML@CIlܨ 货nh0/0y5ʕRK{V( R6hĎ;k?x@DoˑZ0u5kނ `p*KǎCsC^JϽԻkQ&(꠭T@/M޿ K=flFrcNϚ= `)SingTs+!UW^ϝ;ǟD'PT ."?}\BZ;ڷo?;@4* ͞3W.X0%}Q8͛7|o `K7od#ymvpV,5(ԥQ#֯390s֬teŋ'M,W_׭W~CWZ&ޛ=YNEb*׮_7UN҅{Dy#E3~cG'9 !hҫh6י3g7he{=|ƍRefFTu;mtGnj1bg^<:X㎌Cʓ7׮[w4-{B˖/a9ݺaQI3gvNonsFM~g+@Cydxҿ JNN6j-ҶF 2nlܴ)啪TMR`M-tHPu.>jѣQu T d(pYsb] `oS5kFۓz))VR;v|qV'H޺'N5"Q4.]! !zVR#fϞcnTlto|blzN$/2q$-^/A={sM= +^˝7_4ݻwaS}&Va_[G.[ɓF8Sl ` mڶ3'%%(ܖafٳm~2&D\ 72t2Uz] x^UGj `"qr=yĖFE7o,ӷڰq)Vt@+,j.ThKvɥ6ʹC?fl`_wNlVQ^}ءc'߯dRcMPYkՊ͛?߽1!׮]jQ{0M? !8|ŗ^+fVanƚyfF4ldR芪36&YL͇|s cPѣG};(t\y9HEGŋguӽ1!ۊ/θ}%6n@{gt v]lGgR>nh oO>5?i,W^?]{ld'ٶ}wct4-i-:?u4c@~ZOlI?~kzZ11 62۵kw:t3gѣs g?yG!Abō^.Zhyj { chIII)Ww#.!3fX5߼yRwpzO>5FC@F498+ftt|YJt9sWw#\j5ZUeӪT2eOşV tL^L<|vGcƍ:xPqbXBoٹɓ&Sm3w l'Ο?ZX.b o\l,yRXnxQm9lƉ #UXG̨ўvo=+*<&UR;Q-W^C` */wzqtՃi=jrrg5VZe|e˗kAA?uCoFs.HnA!Fzet͖^re/uifЗfӈo>qĴ3TWMF]1 Jŗѥk,#pܳ{r*{h`x @st>oVۤfUYUSjШqݺϜ5{NR=))_Kf}~jvu6~u먨EJP<իG۷_V_N5fZ"VM]|HD1O TUMe]۳gһ2t*ΖĨpMZ f4vqaGY:CG1;HC(Uڃ?Mw~d xm Ғ*e<3ͩ]w=lEJMjUE=3i=ՋVY f͕ haÇ3hD1M)k:TޒxU#5߲e뛇uzh?S;6W.:v%f6Qm]| #``s޽~R`C5oBP  ʐm*P jЩKT.!a`@)(^ݻo#ׯ+Vp"0 ܵ?,_B/Z 0 n.Q`0 go(W'O@l۶݋\$X㦑=fll\q۶k_RrLG)\Yt蘘SbO.:wZj|#cXb3j8_\zX_i3":z)&N|n񒥦MF"5Е.[vqOnܸ$>^ ymE .Go۾yjljѲՆ _g'N?^5<\\vm [M^U <JVc#KP8h3wΚ=[eǎS@%Gܚ}k֬֨IS(G.;UD41Kkg9ⲯ=F`m=/|ٳ#G͍ljԪ?xMM>|8X/CM2m]hvߋ楓}=xQsiGDt\{үAkٷxU&%%{9zThNOsS>cޗ6Z75}3.f{qZiּPM1\mcנoBlk;.Rz/uX;oޕ+Wg-6nyT^iyrGo͙;WV: ہ i}㚵z*:c˗/UC8Srɫ@Ղ*aenqhNvӘbUK'j@ܗU/z6믕ĜƘ<T8Qs=QcM+krxڡc']ւ.r) z|ף1rn 7n6~ϝ;g\A3Y_u4ZJ.c,``c.j֋ g(\[|~&'zpQ&JǁX^*Vd\y%:3~5k| ~yQF*ԣrWEA oެT g?06mvfFP`Oh "*l\D5yc';vzzn9Nua\DU`*lVzT`6z:lxPӍƪ@F;n߹^`*?8~@/٬q=zz7.Aڐ){\ly{د2s 4lY>:(X{h@i+0.='>]z=XswFZӶzlNMel`zG[,XEBEv,wqf`=J]a [|c}>c͚z݋$2҈ ځJֆR[&R*L{IMfj[:nsBz@>*M+Wƪ2s Ng4Ny'>˦XHlr0B0רU9yԩEBe>ȑ367nƉT+fo;Qnݫר{4dHF?`>*0B.8߽{FQhw"yeֽGO̜5]kvvmcةQґu붚k}npU[ztVTvG}ٳʴns7u>?~bֵ o5fm>k18nݾv] z%[U&NR,Tf3ʕ,^TNtU:ݏk2{AU-nBPO5zʉko!-2(`3j%:&F y3m @ 0`- @ 0` @ >0` @@ 00l} /^9 @J-six=>*}Xr 7(oܼymUIO|JQm/zSb'L4":oU* #38yСC/o *|SU=rӧOW^qwX << `f1Fj|o.]O>|z~B`p}9?$pfΝ;lѲU60 F!CXΙ;9 VDk&9 `Ǎۅ ^McX+{%$/]f[;a\'LrO~޻zTX]OyֽG_Uz?x߳]TF&w"X+T;wVVgMe諹\Mf4 0YHTve4?, 0)8%%%R>5nĸ lȘQ>3j4`XeUzG^11SbƎrSM5K5?ȑk׌On&K˔-׶]{)=hȐ]VZdz:.(9366.Nt*U 0.ҬEV6l|lj'MqXx.۪u>Y.ɋ,5mtm8}-[#5w^غzZp?|>={O?)(Uڋ9ޟ~&G=rxcO~yqY>Lz]5{]c*VG ckV={9^~=pР`9nԤ&'LzT}*fѯRsuX52 ެfn[~ؚ9}Qvp=߼y?cԗ3>*}^_-d2b]=κĚ:?sLl\\@Λg\#GRιm mM#=vƶIdl]Bx(+W2޽{Oy@e`yp9s2]u~b+WQӃlgl{k4ju^2t/`0c$-"PuV.S>hꏲ~ƙ`G7~|f[-V.Q̃lgl՗y:H{ ߼yja 2I33;ݷa [H>nݾmn0"kx W>|2!펭#3-O>0_~1d@E`7.mbT*Z;ʄg{wUѣGn [-Z2tOGLEvǶuT׊+@|nԤgf4Џm۵[ >p2eGt&Mb́K*V\߼ժm.3h㫲-aX4իWyn U%ni[}zi\X}G^z `D֢ܻw :q۲e3#;v,`VVij&%>~ɓ'UV[ L18ŋ̻qSի{93 T)ݶ}ץKIz*307 0!+vu4(u)OZG]u^ ǗsG tt5k׶mZUgSuKe>ȑ3Xʕ|x}bř֭یT?,}V֞7:+m;w7qd;/oب_3ݺX|fij?Z`24,WqN-Ν7_:I,cƎ{W|+ՠO~t?GDGMq㷪d:*etqhHՅ⚵dU7l߱C/>~hl;[Çڑm>k1M^Ο?pW u-a XG53KxzQՂg[X?~8ʕ+@`$ gfmmlC%[՚~fxÇƹ[ 1tr 4;)-Z8Xl-K@=pVv;\t)^47X,:ur5z `XA6 #3VƑPIw-k6x v}o`Sq)=ºfZ*ne<0GmԴ1쬒aF6K@ݟ{oа`"Uբ=zFYƍ=F|;wد(Vb%GL;u4}6QmUBe6X֜&NS4dH(-QP[  @ 0`  @ 0` @ `0` @ `0` >b 2,?ȑaA(i4rܹ9r'OzÇ/]ھcǔظ4;I,^FdZy(]>ao0Gܡ1@N8{߱z_l9p7ֽG7nÆӧuTTUBoa቉RRR͛7۶kשӦ?4coﺄ7o[nu؉@FV|蘘={ܺ}C.]Jo믹PxN?^R'OR;nܸڿNڝ; =00yJݡEʽ:E}9/^TZ@FΝ;n\D!0 dߘQ _n_5je {`@~8n?yʕ_vӦ')}ٌoUC5 g1!kzW?w~ܾ-^L#v6]$2hV4X=?<~8q…'0fZΟ NXBA\mq<뤇x}~ǣGU\-2s͐ #~|qiիc}bU.[aF-6!z&\E;C#Ւ {`@JM#OyE"1,wڵP"iTThV~Yǎ !3  ~y5j B;8rh:OHW{.83{` 믍׎֮/__*0HO;诛* ۠a>}ڳw8 `xYưq?<|r9DΜ9;Tvl` jgJȕ ͚T3˄fΚMv;oB;X`pDc/߿`_`Ld?.^fmK&, `g|~wo0BjY9sۀLVM#oޢeFƛWqvU&g0g!7oVM f {.2k?}ŗ zܞey%a~E!MKek eG2嗽{?ʗOAҜH zP󘔃r:I{=,XI^hro-[8F%|- :mwr(dY/P5"Drl`]p!M[~} dБbbtٲB Ar #`0QNZBV2_8Q(YtR+VG;wÏ>m2q9bHr;nݾ]Zth4B!RM8 {m۶H Sp3vx0ٳg;w邵!<It~ϞA۶$`[DAyǟ| 'z7=}=5&)^[tؿ|1YKfGp~$I0 dlĉv'֌3b<s1 F0 p.1p79Cy}W6vHN4=iFټiB_x0lܪ;>8w߽r劺>j! 7 k|nf|QAcyimʭܘ`#8 ᅦ&B'þz$'ǗBفYU\? #F DI~!L 0Z9K.ԹsaRәeH*kWYCtQr~w1}9k۸iӨ eނg>S 9H@E-;+6|DsQޖX<_d?"}k԰:MVG-4MJ.9֠5t2H~qz9q ID]R8b_yX!1<>owDږݻ0HWS)vJPBrҡ p\:/%*MSO۵@\0M==F'aQ&KWǍ7PX~Jl _Kls̬`12 -[x&͚nC^ _FC$'t A^-O F=8 #F b7f!(:.LnTUf%&Wـ_p!`by'^F"޶0DJ !(k\9|/٭kE$VDM>ʗ/RcƎ(F `#λ% .^ &)xX~ÆͺPQ?|(CҊfYV\OtIn`P"^Oj~dD#" S|X ?$ w:񃟹տxdĶsloFԓnP挢]R9goL< 5, /I 78 0U:qswU/ya.&L{н^$G+~w"_]6֭_IMQr\w1 F!*6LE,v3Q 9dCX$*;b3S[nsՌK`lrrF S?^! L+^8U+b`ߺ)]'@5`q9gXEuNr? #F nf"ժÒ⍽3g.{V{Q1! B.YLd`R/XAfEV3s$biW֨Y+x%%8%tv,A=~KN ٩m+[V@C5} a`06lps4;ラ{,5r$Oy("EH%`8pf:L[X_wV/7?׵8+'>tny={~\p{s|7>Y\=")$[9q/à޷_6/7MF `#w딃 Dz&Qj4oR:㞧+!7ʫeбs׮Wck/DSH3F4P ڂ`:ܥ㖯X'uO.|R}e{:vꬖt2!-v0b06E={'r,D)!-6m BR5p޽We">nikq`."H 0 !,Te\`:)[es`'G"Opse8ޛŒ3 1 FB2֭? 0M7']H={c]&M`AiIA6XݴdQ+X#uJx _u N8yvޝڐLͪ!tL)mث q0aA#o}3BDL v_{sd ڻc۶m۶=õm۶mۻu_Ng3S9サtMݧ|>#R&cH.5%FLPb.QgR%Y4??G&&S~ת:+8\Ϙ9K{l~0Խ݇v/˥# p.q\ONT\j+uY/lbŹE,3O6-\2|@~Fy0U$+?e1cF8slC]\Ǵ=x܄ PJeu;v:}i2li&:\X7\.뮻^Xj^pQxz7k!%(8x׸t*b0qjVFDVkZV/ >˽0 [\w2r-Cdt#借98Gn_Ͽ(u&WcHr]щ]ny\{ϞU]믿!#%ٛ6o)5 .?CE_:!48w܅h=<; \S)bA>ȵT>OM={1~)͊оDsşxIszVC$/dp܍\.``~? \Z֛ȪfS-";_xEEV$Ie`V.@y; _}5U9D)n= '?(tѣ@k[4"1?Oy_{uSx?Vm9 v2xڞў={9@^uӾ}r^m sap!݈rvv)0 VOw#~Rf|捛6 ”Sc;(Suaߟ/J6B6m׽88e1s=G`"oX^`|[np,;ٍ\.``TL=TkYe?PX5``>CݲԜw˯B,-c<'82>穧A9}]9zBFed$8j`)5nK[n FxX8L #^NJV<9&N t]ɟ^u\駟i8_| 9CDVr6.R G8 u '2?i;Ƿ=;-Iro_ʇO~)+gm»rvv5mBܶJ T Cھc'm\{W0cD_~/RG}tڌ5`>L6`v2rhP]:X)5K{;E%Û_d /RR'U`ADܞҵA{H\YLoVxwnʥrCVO>l"mqLƯCǺyҥtcyW^uUrF \c9WѲUS\W5q/юC0M}gCx0H8Wsލ_.Ax}ZEm/'\^#<ia5k$ QeլM=;WJeHiR!Ā%s]tqS_B&"+YNVnsyWzi\͇ЩER7uHr$l`e=;*oza:!L-zUp.)^.Ut#)N`"F֯KゔT ѳWMSOBk,>r$%uc0:P8-_*>,%>-O8~w›fHCh@7}i%p O>㤡%yGT~7̗8;_t)9mt `tef{.[R÷lrL}eG;*8kdi6DMWF) բKJp ]ۺ1a,5`V>Ƚ, C =?6_IW͝2 FX# !˲ɩ[ݗi30XV/iD`XlgO{Vnffl|~Oo?|kdtliyh\l8?o5`@5w|/,+$-NOOp4yBx +Jg5`  /[ܽg5` :nvx.XаUbpz߼07yT?88jF^˕&ݮ@  0` @ @0` @ 0` @ 0` @ 0 @ @0` @ 0` @N  0` @ 810` @ 0 @ @0` @ 0` @N  0` @ 810` @ 0 @ @0` @ 0` @N  0` @ 800` ` @ 0 ` ?= )H \IENDB`tofi-0.9.1/screenshot_dmenu.png000066400000000000000000000237631441474151400165550ustar00rootroot00000000000000PNG  IHDR/'IDATxFp"@A{fc0d`00v3 97ث}4!}xM|gF]0gg-4Me)IhxJ?k3;BUUYE?m[YG;;|m8|fVź] qY@qdkXb,F8hvIffd03sbffffff:3O wTWWݪ_}夤aÆ͚5k„ :u ~̙SBRJ͜9S;!ρ1c*TUVo3F?w>n޽{?>`ӦM\(P婤~z^ziܸq͚5턨=Gt:U,V:vSP"'ԙhkҍ ,Q;( vc}.;};h8d[' =Ë9Lڥ?M,hWOeL{u`[iFRr햒ة,ĘC[D.]|駓'OW T@ p™lƲUoWxΝkժŧ?3gΰ[vmA/;AAAcW DGGQFaݻCVZvv6<"Q µrssh(O5m۶ТE ۰aCvV!olCNtg!LLEąm:j +4ㆧ Wvm:^|UOVzhB@@nLcsI _t LV )I z⧟~6<P+5j@b17 0֭[/ 0@Pzul{FY!==]ea)p!a 7qC @ -Zt׮]'-]Ѿ!7ov\…+/RRR իW/rȑ~/b722[n_56co߾)SOlٲ`GqYɦ`D=ŠjOV+p,Iiipw~;LHjxpZ-]C8#g.Ko4 |̔aTM*U;vɊ+2f`[)*C/ZpZdƍhǿH!@ g6mreCVR%z;zh2eJF@9N=%e" `!C ,}vx.^{}1嬬,Y, ǜ8#IhMA [(!rb"#6w@x@㪩_}gF7LWGZh QޒpkkF(}ÐC Oh5QQQugEqO ~ ?z,)be˿H@ 88O?~5 _s֭*FBݺu9Q h[֊a1)Fa`;lU xݹ]w`0Զ0φD*eM!d&pS|X!nDP^2_%s6] ~t + A#ΞE8 !ڷop&eɒ%T؉kݰalnGН/"@ DPMQm7gaرc[?("/"ꘄY?Ow`0T&ql#pGk_1*q9(C _qۿ}XA\?"1c<8HJՐCuHdHñcY\SН"@ DШQ˖-J5<000rϞ=#VVI0?%$$Ol`05GmLVV. rG)pij+ڴPs A&;k މl0c7!Som۶A8|2Z{\ (,4@ DzḅPΝ;(~YA64e#""IKK>}:6I(uL%J@l_ԝ: I r|pTS,)pP&7 WڔGVLǃm0๸*;3a3M4,*`گ_?]n$0Z|9S D{2Ѯ]; ֯_t:U ~; #."@ t$b;Ç'ȟs}޼yG&qҥK.PUV%%%NH9 301lXf +>tD xO#Q`84 rbArO.Om};maoBЍ=sMKneFrr $CEw ޳M>I2 qO޵|̔Vl9&e)G5xQ.L %?+>?(]_z5_~0޲/R@ @(Y$,n—w'۶%Jо Or>dz)Vd7]N?p?Jd>l!eJ" pڌ1B1 F+0_)fAbW`D .NiyƝ&]vxqdE)-?κr-P[ fy*"eRƞre2(l|!(#<}:{.KmEAċK@ !M8p$y:ⶌN\9+R/ 40cx2YTϟنQcEY!ϸKଁ myBډNJ'،W‡~? -"@ A~jժ< 8';R_ޤ02pr/@ªURsN:[5k$v*"PN^^/Xy^fA0ɒ?5*Tq8'$m9vP>8J bu>5~8ƍ܀zQ^#X#||Gdv5X>ȼOT]€|GDӗ 'Ws plf{/8~@JXz<_pG~VH hbE@RC'Xs81z9.#ذN1;ͻlsosژ0.^#]WGi`QMaQ9d}"pp>i..8$ 'VNVp;w KlfH?Q4|IZ p[Oͅ{̷~8v 7 zᎡ%g~Zje/9mpL4rx{9p#`rP|fY'LR`us@}~qV~%1-T>3m-bB\C Ɓx3Mn(oQ EF~qu‹|ԣ>l0N>YL{ppQ+>QS:!unSԷr..$pX|P[\7v9[xT?QKӘ/YͰ_-20s /A'f|a,^3'і\IÔR9|'y⼅ ɧ: 86 pJ#s"_I s:'$plOȿX~S.w0#/T8v%pP/WpMjG Ǘ=8^_ aAvo1|伹(~ר$8X8=e]ZX6]ȟ#pMFzX0t> Ϗqͬn5 0ذ0 ?T8ڣ!p:|*p[r`]8OT6"y| p|MIvducƑ O8`w⨷lMF@np+|cb_33 GCcF cc =@ @X8qST@8~RȟF88@XL88@8@88@@8@8@88@@8@X8@@8@8@88@@8@8@88@@8;@Կ!)@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p@p 8 8@p@p 8 8x 8 8@p@p 8 8@p@p 8 8`A }H 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8Np 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8kjo%F @p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@pD 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8@p@p 8 8`Pt$IENDB`tofi-0.9.1/screenshot_dos.png000066400000000000000000000716551441474151400162350ustar00rootroot00000000000000PNG  IHDRjystIDATxckwm۳m]sm۶}'knZ{1:ktWA1z=yz5?EKٲ%2RdR3YJLfCR*c ʫf3r(avir9Ў];tdTQ1y kW(th Zd7;pܸQ;f\QoZZ7W&qRl^;7?M L]:RʽKqQIo:e;nhciMj\Hl[R~-+>#+ÖϘ1Ye ` X?AnX~`>Gj{de_0,nmF4m\Bu427\Qthe]ڵz3;dt,Vpxp5AZx|.#X걬s*#Ye:uH?k$MDJ*IڻkWO?Ay"k Q,3>j_,KmX?@o q- Ɗ;Oy_i/UrՓ$SHmT>s'L>i>Ǒckg32R10k( $ - #h"$,[0u)˲帎ǮG0$I(jQ0 H=f? ~Gɢ=ǽeHvmNjw0a-J'~Q[mD֘ BCH`ŭWO4ሉYs]y'qk M( 0ܣ"6jÆ[d3)# ҐgMt¨{WGWE08(mN=oH顄@ ?OI۹S'?g1=^eN*y\YO_%-˨;l˖&zaJ8]JW/&N%s?fm+q~PRE+W>ٛ|pe@죊w_#zi۶乮Qr!>^n}_8gco i۶l[lVnPrYIXx՟`V: J@_2XN4kT͟=]LG|fsYe3n<*oږV?>T%z/Vl֠|^V T'ݾ1+Ӳ+^ǿKG*lzKu\syc4K @]yՊvTǝw\3{aO͚5U]d<W@\QIߠ$᧯f H3f+?|} 2 ${0m7NiiHҎܧIbRTs9nK)̌wgH)9?,zIK( ]}ofBKUōiZ{?^p7C7=O:;;E&RF"133uUE-q̉_w}xGAi&FQ>C?py(^ׯO/{ÃA}a:*?ZC_)?fu~/ ƿPkjꝭ[mX'~x_ENQo۶jQ5?M:G>~ol2Q4M4u֮l2w,n-`7yG8G۷m>X`ZmW8m{}o=kv6xŮ]z'ƣqu6xԻgر{뫺ɷm-`7Zh.=Au"rEZS7fO.ß]|ʹ2z h6iցGapbc;_`wx1 L+AxlM 4m ?`0X텻?{`K'RTuf(8}oMٶcE[DN*˵ĚQeΎc诺ՙ\.^t)(6[8ROZ8v(b^1r GzGRRb4~soMœ_=&UU{_=ũ +-6S7umh vQ7L;uXe(n'0"eܱzCc~b*`wpϙw]mw"bfEֽynr7t\=[|`kp;Jewsv6;o߮ȩ*ڦ mU]E}WOmؽ{Rƣ"Fq3y7w<*^ݾ}W{GQE "EFss_&W3Ǐidf 38x#X-a<0 zWO|| ~k8Dp0HF6.~nr{^wq>2ꪎm4mTU99{r}oI&Wx0L\2QTϬ?_wj2PMD 75v`070ؿoϷwhn?}޽;#g8\?X=tS`7_,nxmF9|xW?2~;֙ٳk{wMY8r#*˨:z@4S ?\qKq" rvl'UzdUuUŵ^kn9w~j}vEΰ ڶ"ϹpvvRdeU]uS9A+ޫ$жm,.y?MjdTDS7qMDJ)Ƀ|č4M9wujgK5;75MSGh8X.>+_uUmHFԷ{ə0wk@S7snnru&2R<AUUTu~;w)s"#EGF]5Q"5પ"+/}`7Z>г[p4M9O۹|p>hvvl6 (M5G}<Qߍ׀*Ӟ4ؾ6`ֹ^7vǎ=9uUpu9'G7y rgSJ2RJWD,?Q0U]qsC{[GF]VlUUF-|#GNYl|C `~eYK)#Eۭ5Gyb\xT|h KX"=^8|ho==UYE΁}75;w9U*x2EIj7RQl@E=RHبʲr^بR_+'2- "={vFNJ)R7v&N!H={vjKqibl\E*bǛj&"HM")8/N/(R;S"EVGRŁȘ%(Ҿ+"# S;"?S HA3pMlI3<HElG S ("mcSHUUTuS&XN/Oix@~q17M`奔"i@@@fĿk @@@@@p1/@@@@@@@E">jyF|+b&M<&@ O0H fJfyA-s۱Hwʩ3*tX@'!Ǵ`} nAzrA{tMR^KWH޼M{iM0޵CDŽ'_)  H?Iצvr1u2{|Zӿ/gE_m%PT[l'/>'sDǴyE6] tYBϼڭuu]7)USҡv,/=kxfY;+5+}ZS@NyV㿑ҶY+V4]ƶo3 V:T ZE)! m  mON|qRa Fɏ# PmX.)?T kzB@lkWb`n/eSy OY:-t":b=yYuJC}qRl>;:Wz9 eKWH@nܣǞXg C@cgH헳 J# @`cQZQm~<$GRwʹW_sIeZOn^ |@"* iU^%4;I30'}r-xဴmVVI8;ڀ}1bsIJX+PoJ;g%${ X:NDo>~Ӻ!#f<<9e r(H%ԡ~LX[!"{d?2 ԑKmM^-ZZkn֚iط;4yELΫ3YjW UNk˿>flC(˰m}=\$0" H+ԬLm+rԓ[m7HH7^UDPTm<r~pfjo Kcl^UVfX޶8^zm`z%Oҧ.Ӳ?&$T4th;7Y+=d:J%²?;\m %8IBX6\;a?&4"h9S| m9k8E)?YW{? X{ELA` h;zPgܺӻ灲@`6*LCѺhM4}$ߜq9֩q\jR뤢oagtKU{3(Iv)bGd'wI*O͹cGd_6a 7M$᯶J.vqHͨt H8X1jx}Nw_qyN9@u/K_X\S7l>a/f:{1a;_*N x. x&פiԴ*o?uRhf^ݘsEfmjku|)tǍf{"qʾt̜kn6_jfYNX5IW* &5Y)ZvޖW|쵗sʌ/8˲-auu@9 z;sߖOr؏;ߚZEyrTluz8 @`~JSN* Y56w0״ylnJ>cV׿Xg 2 @`!]=e^IJ;[I:8''UY 2u3R;sSvWz%/JR(19o:0Z|'14f>+<键nL$3.fMF rD vU 훓vw?(FйKnL#OIޭLh=7+FjjuE)e[-Zҳϐ߯qLzveõ ҝowlZj@N,Ko@OfP! u}^P) @Џz ז'U$m[GY23 33333,0G˼1yf$RZo2*uKʾREտ_DEXY7{*^ pS|ԅΘ70yt=~ h h h h4 @@4 @@|fၛw~Lg=qnw&_?o|'_?;L ,uʝԌze_$YK++jnfa}s}m7H+eZ/Mѓ<ʳ̓g`:a[wLȑ4zwGP\g @8%p$;ܶd0 ;19a `wq3Er=2@0x\9ˇ! y%OM/&ki38%Iگݕ]Ljq3)zdqOTĩMf2M#k~) @ݠ;֕T)&FE_ES"\  @堆8kϫɡ0"UqNym Rta+$hTj({=^ls1 yrǙ}>* $7ȘrrS3^8{o;c/&o'ݤٹ$D?0Lao=޾9~:XwJ{'+(n'9?g'+NG?̷) @ 63хwq5rA60|*c]l[cX2 .x]rV:W3h|輩zBƜMU>/߭x=-Symq]>y~gpZ[[KE_+Gܕ]+9L[gBvxZr/??S~\ߣM-e;oHkwj9[mW{g7)_P Z4xb|9Na-:x-ܴ1KsC@f|g@76`ֆ9ؠ+ rm l)_w0FUZ=^[/)ky8p*'7Jn+ uQoO-xks|Cܓ M=D͚Rֻrk'<|?1O;dO} krt%WSsoQ~!m=~8)O?ѳ_xd-9OB7y薌yˋ0;md̛^L>|ك'6?lGI@`^|ߵ%>_?q(yGd[a8;+wbG isӮav?Sisɝ?^y0c^u0UOyVGͿQw{??$?G)zf] Θvk_j2`^8n?˺a}vkׯWy[)\* 3oQ8жN׿a)9 V Tf\3!˳) 򩨇ŹLJ7^a \ӽ#Nt{w9kLlTA>{8ۛpnOm\Us?ټi1Wl8:|k14ӳ :h1)QϮQR2N_}|{r.6 B°@oa VWWsbmm-%_ \/}0wJ4G7}dK~C~HZ'F~{3nv#~7'gпI7gO}Lޞ[TgwLįxST q'== 0Z)k͘rvrfY$ueA}ct@wuί&حΡ"1fOy,tbw* p~3oִ֕p_qOSX_Y}>1c5,_ g2ѓg kZi? ؍uPOKQ``~ h h h@7e h T h h g^EHNۗsEy<&ɧx^>߻-;u|#E'65-^T򯯃cWrd>N]N{` @pbN}6qsRVgu& 8YU[Q?d[&h?,?>$A}.5uqfOٰ ǯVo]m {sXQoSܣ  (:/+Sx'~0ms8Yn&Ǚ~>5nsZ'{ktb<*뭾N?! wgs2wꡔִxgɘnO:Ͽ.7<Sъ8ΧQw|v8UO%7>%a=oH#}!23Δ4ˎ.4ƱѧE(7( Kz=˺mp*i/Ǻ!#nB'V\sxF>d5cnZH\O0|PއRo)ۮFܹ+7kræ qZ]QE):Tn'!sm)(^"~1T.t;fO± irŐB{Xssu]? @oPsymޠ! 3U[ 7|M?|kLu}}P.q|b`AR±'aTU>e7ǦPc8Nlf{;>GܖK$'~웒W.'?@ WxϹ[DGqGύk|&$?~ip4'M}>FءrìVsTTB=d9u]S3t7?,k.X(]|*F9 cfO-kܼj7W7 TQoSzT~G!k;`a:NܪgR\,t*7|jra~y)3>NaV ']ݱa=׬)e"NdL7S RQoSiq'{cC+Z[JZμV^MSqn*7zԘA>e uq~+jm_;V_ǩx!o's[a7%$H߸|Y<צSӸm)i&O:?<1yn#WJ>޶S&䙋W7x/fO9TMƅvye.yl=OfO{uCkjhݺY䷎QB]ok.Tp|e׏>?t'I.?a_o{$[ÉC5l GOGm>mf|STi4tԛa>ݘٵTm.+srk/+PvXPcW?,7-k+kkܮ,4\9,tRov|vt+ss)ҽTū54hjÌdwrݭ/vRcu |'7W? Va C'm]?oN#rëְz )xxNaru s"NM]^@˥a c  sΕ{&i{d}kyn_J.yBZso526Fy^u{ 8~~t~ pbA ׭.\Z3&M Ӎ)'U禅͋-ί=`n8Hy]x\gz#W4,|ϯ3i_ocqf1LU4.2Ơb]@TL?ͧ>V}ޠ}/ǩW=V8N^maϮ٬zW}>h>ގJ緋Sgϭ:``' h h h h h P4 @@4L }9WǓx:'2{eG%H^EHN0 LˑŴrt1H9ߣK:Ʊ\_ @NnZYf_Y* ުq`4I6i?Ⱦ1([  ~ź}x/c^! W=ine+ptp}7^s @vX]H[O:`: 0\tC@0RC@ /a0@Nz?e1Dy<ܛ|jZGϹ>՚$?~\'E^L]Oxet$;Ǜ_ܒ|]O<|5C@mk|[ <7` @_c,覅rk6-ѥrN9׽\G RoMMܼ=7`6R~@Rwqc5q*TX&ItG79Nm6qg @i1~d[ˁ(úk5 3[I118u6 Q֩~8C;eS4=|N'szKWֵY_o[3s aNݘ|+ƧWX_I=IXjO': qli|;냌>_?s.7OgA}8ib){w'6;|ILCIiζ>/I.7e̅QִxgɘnO:Ͽr7<<>ދICV˃,\OB:5k=[o޶1.]y(ȡr){\I5sG<% gϰ Z.O~>0qH8 0\t;-T.XBeLTr1kr\חYξ=R_#ǯn 0E\Be \?,+*ǘޖ`P\RZW>vu\?A}MMnG&9kL09 '6Z\^"Y <ꥼfVup79vyo?k_O/N:Q;'&{\oJ_}o:hJL.^|mq\F0^(HW+y~ߕ̺~ן$^3N36[DGqG+ s @cKcSp"V8P?/֜3:r>M2fS; .(/Րupplύsz)?:( ))}3h s+;ȶYsϺÊ_N KVd8B8efƬoN[kf_krs_Oh;Ѵ޿z*9<+Л{O+=\L/$d'ݦ5:f\'k%c Wnn='Kƭɟ9i|]0yl5qhHɿqtmRj;:;%vS27.ߘl,ϵih4n[Jmg#~s3xn>kܼX8kDk`\9w${#Ed{=a>ד)kYnKw'>\2I.sex[}ki/l޴},7-+Brzsu޻~l<>y~q;ߏxρdk8q LsOhe.YRsT-7s;}*ލ7A_hwcwg`6:)v^>In&?≯Av3UNwm[nR/Q?`??>ml C@fq/ɰV".>17 +\^sn`lZ퇨lڷBwL[pkm%yb:ή=s1$/lk|l) gTi<ݠO%:} kBqX7̮>צ)? revSnsMRrɓf`4:ɍ[ I>-Ơ*ή3wxuW_KSm߭F/I{a#.lsko+sSBY}{<}!w'_pr+?~ѧwn5:W?Ld~٧@`c+U6UqcU8Xյ` |"AE>SNj3>g6wߌ&ӏSP1,*VkKѩYMV:f}. ^(|W|{uPO}556{Coa VWWsbmm-%_ q't 0?{w$Ǖ f4hwgldžy/ ]fffG5-YjSKݥю.wdv)״'B<9u@4 @@4@4/ h h h h ,W%_pKgϱwJwߛBûWí ,W8vqZ  @`-eup6H؝ǎ34i:M4rf/ntz7 ܨy@ `CN$_4r[Q^n4,f l&wl+Ƀ;4 0aZu@@hR {xk9/W*[;.$?p_)7$vkY]&w}=n%ox4 y&Af٧ޘ|ksw8+SO_/'=.$y_~5ܿΜ/nZ=Qsܰ, huf{]R7Z/Ɗ\ܮ[IG]\7י|jpdd t rIj=gCV9A:ؿFzkי|4@ddoR[M:9TzξޭݻN9Qϳy|4ѳFiybvklvm:I]qvVU{*ϫu:O9Ól:sw]H~TxZw$ӻV;4 2i7 kg I>򶧓?h}'ߐN=)wO&oy,-K/.A9𓧒vR/LےѴGìhN[CI|_9@ÿvF9L95c'V_[ X]r^p0}Nn0{sNg} ucrٽBGmTEC>,rnb8gf]Α~{p_ oɁXɷ_?W'wZӲN+Ogz=;w{t5~da'A7}`:iBYgo9_ܼzt(}zqXq}}]wSԚs{̧(x=?Xs G^a{\>LgչNQ[_tvkY|+Χ0̠EFGG]g2uIi:uScZ}MߜŴ\|8<3̜K=~ks~vsGsnN߫tj;${7zӻnjۭ)N?usץ-OSO%o|{:NnI>dfg4;44:[֒߸3?O6#G^ 9Ju!yn^П张6[f_Mnwari*[~o.n%-׮ prZZ΁=aZ6g6]}qYK\?Vo>Xe8ȁryӴMrtRJ L2Ag< @:P8?)Ɗk&iybZX^]箟WR_ks\yN{c<2 @wAUnqmqN}oܬ'~fd2͑N=5Ã0<ٯwFxnܼy-ׇ&ڿy<9Ng[5A]g{qwp0-zkosnTJT__nHw^Yt}u }w|k/yEZ=[8ܾG(Gg_ϻyxpl|~Άf^\/>S#SI%c2_ޘ;R]_w >Qש>kVzlt8{KQgP@`eA>>,t8^Xq^X?g:r<;Iޤߝ5ݏ{W[{zqf͑! y3FxBq}e>7+O^4ؿN=sϢ4G8qD{=7O=y @@4 @@4 @@4 @@4 @4 @@4 @4 @@4w_ n!??;yz^?JhH~| @ܸzı ثPY[J s5HNVuOhNcs4MrqNF`vݯ[{i9;7qB`k7G:! As>D%HW/|)eA1Yl&wl+Ƀ;4== ! aZ31 X KMB@ @̡{xw W*[;.$?p_)7$vkY]&w}=n%ox4 y&AfI9_Mg|~d'RPgSoL9҃;OB@z)=u/nZ=~Psܰ, uf{]R4ZmW/ͿZ]u n5X}^=m8H[Iah x W!N C@.^|>kP?C=uhZ שmhA4ޤ<ts^[]yjߝ@`Vת[λziˁsg(OڮuN~R]kɋEйFakGaݶv;ܯ8٭Na򶧓?hwk[CI|_䮫_x2M3o+Z8`&|Cw&m;-3t~ܽN>屴l,%d8HOJY̱pu#& X}v僱+-kOs]\9w~!i|FIA!gi]H}0ugf97tݻoV!#UǺ 1LR}Ru2X-Ϲs\ bj݊=>pnS4 iZy-Qc{s/ b؛`{&s ][t_hl_<2@c~T=18#!grz/=y~\4tzJL[ޓ붵W׮Z=^שo =*˱ꜙi^weW?8G:Y9SO'_?ƏL>D2HL'MH}r+_|-9Kֻ^89yW'wZӲN+Ogz=;w4nb ͫGa7{wW߫>ߍ=Y}0uivt GaϹ2[yZBu۳.3x @ifPM9զu}' v@J Ӳs9̸k3sv/(ϙѤ{d;sXz۾>7gGsX7`qB@^ yn=[vk=gݜ|uinKS7ü{3\&쫢'7 xڍ 3dzo;gϻ%kiy7&sևE-koܙ'𑣿 K%ﺐsh ຕ\u9҉y{`'9pFCż8׼:5d|kș%Ͻ9Gj^߽{Qg޴zã?ܪ?fIU:Kd<-CiI(_f9-W-]Ǟ0-3K~ Uõ+=U}0uvg=V__~44HpKX=i4_眃=>bcn+K]IA.d 820+m\婋iybkU54ٛnk[\}0u+f2홋o"ak8> r.ٛ~n8o`̑:_]~GpyP&~mtW b؝X(ִ >zj+a -;ޯ g+k?5o^K޻Y<~4p0-zkosnTqTE__nH#w^Y3}up\W7J_mw~VֹdmS&o{,_|+ߚĩ7?~+s'V!#:=.I2hs%u1ǎ>w>z0h eeA>>,t:ޡNףV_sϧ,IZ&M~>~Gnzr<׭~| @`l=z y8WB͜%ijKO^x_гN=˺}>=m{ xy4G8qD{=O=+;o= @@ @@ @@@@@@ @@ @@ @  @ @؁ A.@@ @@ @@ @@@@@@ @@ @ @   @ @@ @@ @@ @@ @@ @@@@@@ @@ @ @   @ @@ @@ @@ @@ @@ @@{,=F^xol۶8udݩm۶mϔҨH[S綻72\a,g               ꖀ L  ܞg&  Ы:R&+2(Xua`  e2&R7ybx2]`p:ͣ<FJ̿n"eeyx:]Eqќ`c`kD/z"_4]/h`s9=E,`jY uU&vb;q`u-.ۋPA 0;w<#zYS}¥7_@ΜmŃ>)\@HysŃʪ?;\&[M_"T]@_@xxAo +Wi Mns{K^Ξ)e]gG͕7c]#(2RN:77[,Qe$5?s\\H("Pv~/1inf@PU4u];KK';Jb(;nw:|<).^zʲWWnV*F~`QX^MbcuU%v^ng^ؑ#;f3.^^w(#e௢[MsC E4M#i(V?|\| 84k-qwtuyܻr$E(:_жDLX-nG ;^nzH-xp&o`H(b=րߢzş%׀xid(uUEd2yy5g^x4&#`2Dʹs#ۣ0oʏ;#^%ɋi<6GFIYYSם4Fi-YZ_kׁ:2HE<)EQY7p)b/<|ʧUUչ ܎3oܜF6?mns[m{[$uCW`f8vu^:+?3RELt:{و—Ek`1:}uݱ <nFUUe'O^e0mw޵o"pMpSip]{e_'qb5,GJ]U1 n&s;v"e0o2?d4t8nwf0NgM,e4`@{wVͽv0$vxM&ko(˶|w< Mӎ=n@{,w4꾉qdoUԋ+?R7M h\дq^0]x.:  ۣU)WlV{8UUuA;@[E#_w5Ћ5e[Zt|_d2k]]w/d/Fzot;$ilD0Ngw kkm`,x2zMIϬ.'$IAeieK\x bn9xK9d?c  ^.@VUZzQ+VӶ[,R/e˕wWo{{4H+ʪSq;fWuLxڵԋ9;?>m,u#Y4M/4Mc{92m3CJR~q@ hjHJE6N賎U >8m٦gUT!k3uU{{*n߮vM;\#օǜ%+h x$xw8B8V[{$Qw-[ O <>ƨ*ohRMoשP(qf`/2J[m9ꐋ yu| %``=wYkՍz `axÌ҃7 2hQGxBA Gf{$QO-YPgO礱Q.+/kժ&kUi-#FL|&k 1FA>P>(RZͬznfmذ1g\Q?T*2X~?c'C`.}UŪVkJTRG9gR 2oḮr/=qV*IS5b5jxOscև[<;Mn~@/cժ+;DQ*>Ii7u(9XZMiYf5m rWi0`JC(A #vW",S[T‰8ae<^=I(EXZ5>w 5 `rѓ=i&L:W2^~Q+zzo6oݮk7!m>?,i2BנN4qhow9yZ)cEq8N$Y퍭[wh D :4 !&76y8-c|ϓ*NQ$eڵqӶ;v\/ @v:o*UMoj#'{\eY=%qN_LҨ[ao J -6:ǑO>e]OZ"(:z9a65 drs?CV]&$Ͻ?,.RIG}@29WN=9SeV+V֝o~/, t S* Ç }ANpA MS-[Vv힦,{ Gc c>0y⸦Qr#oӦmZas2Cޯo@׎T{\^]1^‹+ݖ_2ݬ99,f$?"ϙH4zaBDL6(`VE{;(ٜak[բj<%wa-,5<]EA6d8 xrn؇Q8(d7dWG>y)qS#?.y+M9N k#_l_?o0)魳8"cS:]Bxw ܭd2(KQv,>IeE&_nBIV)ie^g7 *N*L] y ex#yլdzlm5ŇYe2_,FfxBk0w[0DlGb2U&MRP7(#G}d@'tZյc$-TṇY#tOUCWɍ.1JM?8NfŢߪWgz!k͕*޹jJ8,PjD5٬U4sGVMg^G`@hAahW JE:(Q)F|iy}!Pbf@WfԌ.d4,?9PCɹ\~܎K _Z_:y+ ўf2bѝ280T%O|ثC诳D>z]/"&ˉj3whjj!;Ar?i%g7V/:yƄ87 (Te˛ӓ4* r_oIR+^U KAN6q| V)U|UƕA~zC}Da!Bny^rBU? p@^ۗ[ׇ( w*ۓ?i j'/\%mnw  rZ+=]Mk W4uEp|V$ㅹ!q"y jhthz BFPWC{$ jG8vg;?H̩WM"*4^.ٗd@5;ho7mIl;ShP~~?_kL<;^Qg jT$OY o@r&&H!K@AՆʀy%V9%OQY'ElS32P>A?Q>)^W8hF_NIҴ=E2E {Q, )wNg+8n Q/i7AqhaߟO :6*HJi溁E/)"OhQRˢx8R`KuĐR$cF^oj6dQ}tIVv+xpuL˯V6huf}o">p8 $ǰ ) W qcW3\s4&18ڮ{iik?2?FA^}q> K^Ͷ˲IZv{\k- qUm_ڒg}D^Tz%e%>P11B%(YN2}'S$e*ލ~+pOSO] 󴼵(4$CV,?<.%4lWJk2C2tN%,B4X٦~O1)<3ZSAvHFL6Фqt WQ 3ܔ%+[3G:}nc0 Uܞ(Êӥ"YRL$0z%d iI1eYIEK:~#bb^|v o8 M?g <ۛfJe*qn;lD8JjP%T`0B+Aυg2O^dp\.ZJ? p,Yd&pbA9k~{>mF`Vo&IxXR,?m"$B?2&d/1 r{(5B]?*b5ta{rl1[Hrw \;6^ >΁\9}BɁƢd LOlu)%ppZ -3O] ED<pX E~9݂ͣ,Ph:"G(F)pCgpJ<3ޤN.@a`nA*)NU r%1# 8l&T FਢI}q(D3,_Eu(s/Y00000000DeM74aiLC;@?+ &( RM(|K$;^eI~:!C,;YS+Xe`:TteK`6ԑ2uFjA|22EE[B# "'*=UrKMؓaEG1ȋk6a$K8iVGٰBLE[\, D-MzJ}'6,G HZr5w;HNΑVŘ%Og.n>D&Z}sr%-p MRNg j:RE'ϘO8E JB1" mE R^D2߈\A;( Y yC/}eO8Q8޼!iN Ok0ZDJ ~9۬g` K[}UTv!W;mLS9r?EF5?n6; tTR;\2 ~MG&[$( df{#bgfw_a5t'hX՞=ߢR!WJ}h%EI+!)gӸO[,9,9"9$Nm'jI-eB_5Ȗ; h9 8;$\7YQȌ;q ^?"=JZ;Z%]%nhލP}0000000028*ǯ_$[F},+P6Oz_V yZ^Odll9n֓^*A\.qsRAn$Hil/X2Ga} HÕJeTfY|:$~MWV:}H45,Ie-|=Z]ڵ$/1|)(itbtc4S2U}֗ͅ ~ExJcx5?ե`5ON2;( {emA $wf"'q Z-6#+Cף[V~k+ϔvvRS3MEl{ Z{txP 0?7 Kó;+/b7R>I4hdB.w[E7e\2N2z^l~__%;(A/$i*ݞ  WUrE~:춛d2vG~p#YY74ckVj or4W[xNqѴq|pGﴞf>DptPsXz$uÀd ̗J}jr<O竳 jW> ]7ךϦ=qVYi# ou|ܝ0Gquك͟Y=pC˵Vҝ| ̝mA덡D H#Dj2-/IJf>Rus!BAIK-CYoJ(x}*FƁTI+!6(ڌ[DA]$9q])jx<m7gGsM%dsgFNVgz%,W`nyM*u;]-pՂ7aoUTCzAlvn2%ArxM4Ѽfx% E9L'l~ og3x<vƫjE9\JOgbMizJHPJw2%}^(:;>KQ?~t}("_0ȩ7#{dH2ㅷIӼ~ilpd3Lju=E23'l~dAb~$Z|ԍf=2873qcIs,'rHl.]ݵ, gXxIJz${nP@qIZxA\Qr@=J қ\. &=5fإFh8XYZDEwfrģQK/3zUm0\ mXKQlOT_5 yQp*G$VG]ϓ<P:]B pJɆ] rB.CY/b04Go:E_4d>{:d]Z4 ꁔ"2ɳ'l {xɜ@DU+ 13YA3HHfJ«~۪׸d2ﷱHEtZ{;(ٜ$bZT W-800000000000g~ +C, ,F<(%G!Hvٵ@MI:olasӂg<|YTBւw>=*wnЛCQ\ @nw=}nO)g׼r؈a #+ @a4C)JZQ74.gꂱa8={^s>w< |GsTWO3σҝvYH3L̓|afϏ?Z HCX5T#ƫ-ԅXZNTOU o߽p '8[d` : js:P`;Bo`8zhQ? & N!4+NuՅps.#yᮻ,@.ƲzB =@sD1|'9=x-B~" = E>J꤇`yZv:>c4`-yY 0d3L]~.⇨fܖoFڨr> Q79Xǜ 䶝žFp&؇d* ~y 7Ph./5ѲnuF-"E఻ y%LX"qH4zNB"o_1ΤC8XN/t1KռJ'ji^ ,P7S^xXA'pNBzi`^hXL$X rn_< PUX(?`0C()#@9B|`Va0PLaQx9/Ze5ܢ9q|pg}a0Qvj a[iFdw_%pnU'L\UQQҦ]FʶG>oYǿ.po{>ģ|!۸3*K8f{1R xnXQCiaAha[ KpNxhQ}7<%8"=+(ehm k`G rEd)RQX_]okGG>i#`{"P!3*p>@G(7f{ R|?<;_ iKl&#F:. -dYA) ۺX|D`Gm: 7Õ qzoQ98>7F:8V@@X63==7 Oe_z""8PZ([TDD[V'+^|~AC$s_ѧ-PLds}Q63`!=+%77v|xCQl>BeW ^A|{F]#PI7f_ QPG43|cks>B)G|QqQ/o, ">}E-h?Y?i L|vI2?[>xw;M3Hv3}+3M2ib9 @M#C ! &/BoEndQ?,8B?^""O8x㽑HibSO"˪?z<dNq3Ӗ&66M,b_D&4w-f5TrϜ T_zNX߼()q&cձp;MlN'?JnʷYT&ug9X_s(*4]dIMK4fc&B}ђ(6X)Q!K'?d/+8 fɨ~̉۱ŋeG(W!E(pL]K$J0S/--`> `y79f -س{f6o0%8YєIU*i$3|YqcsaCsQtȖ+zR. (Ixd.rXn'm ܸ0߈(pG&k}L!p/^3mY p"U\pcƉȬ;n/w0!N^nQIp/r2%Nٱ-*D8DrCxFeH9s4ss2>:]x)p+ (pAF378 2ʐ}-䷇}s{ͼ1 AF$C};AF@#e2ig*d<~F`:y[9oI6NNdt+_ ADCDDDLJ |%w&P"fG4ۦM4qQlͤlf32>&kgҝg-|՛-$ib5 ~+" """?p/5ާ@RDDDCDDD8D#v&l~(pq aC 3wJnQ)9ێ7G=HMo& XtQEΊބOFD82`C,,"""1h8X@bXB&*h>!(J2$$vG3FzԒ2E)zSH5LHvhbezV~&"an$eZ>u+Ky LzMhibDq=|DAלj~ -|Z`Փ):SbaՒ*k;$HBQsy'bvdÝ+N2-< drYLjRU,*`2G*BuC&(Xo& 9,6<mIS:?QX7f[뺕?ֿkƝ:"""" ( L8Iu,th|(3ޑǓz}8yZ B8(.g-)-pDdfrUÔْYC·õM)d9',"'6!1tDD/6(p.LK$J0=--  `y_!1rrޞ2쨓W 7a)K<(p\)_y\rZN'Uf"t> z8LW¦͕~5G|~ QXƺm{Qr7DuCDD&nQ39Gý@fyW 2.L|D`GmdB 7qzqR;c5=qa |bpM;Շcf#N a8?8DDcG+8RZZ '2>e9B aFߌp;ş 2o2P foEcpP b;RlEA:h*fsf|;I lH3 1D#ǯ{Jd4IńشPibCibYKO˞`_@M,*l[=P:Pؾ=̈́""Np6L+"""wLϟݞrmUPx:"""! ]Q*Zd (pܧ]>ܮ/巏@DDܢr>Z= ;nQ-*TҖ-D/olR ለ a:2NayU0D2>O8DD 0&1v`[ QcXOM7M|`XJPCD-tT Ȗ<5~%cS+ixFJ/DDD# "9 17>ĜcQ]sZ:v|M,8l Ut'I/?""!vf'X`.nX§qG@EGVXխ`yGyc|*$!pЍvKz=y]硭@:yQ;MKNZXJPgT:Mjo&H$Qr>LW>؉{LTD8%p%UDg9c{A8C82e=fmR bC *}4P^DD82_ʣg$ \gyShW ލX|D`Gmq Eexr!N^OmQG`oUjBv"""nQ ,DܟRE z)f.![Tb|A_ݴ_?F=.R1h`̙3È_qLI*F`3J^o2ʐ` 2<ۂZ;r:%UE ܋BŇ뱙}]ȏB2@V2$3^UL&%I&_Y,l斚U?oD&6pJ?|ܔs,* ddFﯜNI2`ӌ6GܪH3c/2Y}G&rksMEJ?fj}2]Gˆ3V%kN*%e4͌v;jʤTq"p,!ƁT~H("2.GQar}Z U01͌͜r(k0p(p|8`&)V \3S;-,LULx wݜ N/}'7a)K<(p\/)p#sT40(A] FjPd9gd7e;֞(p8yۚ 9 ̷7".Xr@{5#|LK˔xA"{7N`^8pcȑv5r(pcq="Q}`U9{)"!"""?6(@{Fk@2.AFF2ffv%QHdqZAF烃CF!2ۊ:ZQ4ֲ3>n :0G`3iLQ4*sNΨ$F4.}`3ãIJ&? 弽$?0P|ȿ< nzEUfWDDCDDD88Q"bmd d~@Bj&?Q! ^ey}zWw7+2'r({"TN*R\55[kL?ϷBB9AF@%h82 1m b4G| {b:!(J"ne>:cz(@hS`؞c>'UC38}u>{2&áB*#Q07ҹKLksr=7 }oU8ba[?ΰ*V۱0 ~ݦX%" #E$]N^D'f,b L2}!b>dC^ɜ%hdTq;MlRN~()Wy|4IQz-YX_s"iq - &9hdp6" I2K%1KNޓk%j)@ "?uswn#sӛq-ג}{V/bF)s}2sL/^4W߿""" b1_f/pgp{IB mU¹ƑjfqU͙wDlfQS&XNc0KFG9'u!b~4&'ާRC֒fIV7ЇYF׻Jf"K/>vm<~UFlF^ǵkL+#7FDDDc&?q{fh Y}@`C6 m3˪C_ #{X Wh?\Ȅ* i&'cY.Wm+zJ| 8YΙ V:cJIdGB.^GVlڲă^"=cˁ%طDDDDc'| B^o XU!Pd,m 377dɲ 'κs,9RݮFPV~D7,p#o$@3}i?ݛDMqdpxmcsRD~𣹧-*]S8٢YCDD<`@$3d;YyIO}`3(o2ʸ- AF!#GLmLyD>ZvfчэAgS<G?Q C ,3ibaL_d4X0hAFt9PzXH]Gi&6_@] dJŽ4.}`3ãIJ&? 弽$?0P|ȿ< nz"'=`3&VDD~, oo_G8b7ɏX9(p)+7tǏke-#ݳ:< |1XyϠ/W>O٢R[T:H6s-*1~EEDD~$ 3mY(]ߟ- D7T`(r#(؍*n[`m?_<B"9'o㋙~4ը"HMyh+,D AF{8bUfRAFp냌"^#J"Cw4MSC+/DD8"a1 ^پRfyOC ӊ?.mG+LI:JO2a`}$==hh]Wh7·MC9Ki<'UQZ[qބM(Fx4ս;Oڨ"UM^)t`ƿm-UZcc`S I@.Wɗ9 dN۫<*nM)ze?H{=Ln)Yu"" yGs~:y1%O5Y1v  2rK+ac geҍE4{xx8Xߨ;ɣt ã wnks_|6Ҋp؊çϨpEYō[(^TWwZ89ߣӧ.̗ \gyg2SGL)*t?Dɽf}fF;sc|K#b78̹Q7'p PD(9Kow80.8d]Cgll<-p bGs`+t7X7 e~qs2XoX$}`|>hHv؎· qV7l*lE8iʼnI vx* vχpp>psrGSCcμX)@-F&dq`CbPG5 `lf, ^7 eMbϷ܈=}2lV# AtAx%""?OI_¢ -G2hŝ~䤌Ŝh!|Y`M yZG7V&{?Bq|GZQt@n"G2 n`#o Ơ/pV},ߊsoatC_Ǟ{~)>h'Xrp>{+"f􄇛d6c">! 7wdr882o:tЇ AF0h\y t>>]I7-U* fHWw1~;EED {418 1hP?/)ĵ!7ckZ <,Y(̭}8\P|`gfd;aVܷ!?wQ vؤ+LU Xk`u9ߦLihl>؊OŠ"" """b^:Zw?Qi$8 mU¹fN-)d`lfQ0RʼnȇXREG^#l}][ZSRskwSp]Zf)"!"""KLGF&d `Cbm9'S&L&!+88%!pdKYAcAYVZRtUHz2uNҘG*~nX[fRtc3aYDDCDDD8.k|  Bo XU!W $oWC>87̴XD%'pP&`^8 OKD&ƦڏG bkf?'k62$j+ȗ=(κKQ:$A}fv%QHd%b' P҄2I|{=%x,sob0@F "i*LQ4Ly41#/F4.}`3ãĦLO,*lBii?=+x@C liE43_RuCD8DDDD/"F&ONey XEY(p(p<_O" 9̉E@{[T H6s-*1~Eioyd(p= ~2ýAF@F4NV1p=hB!(R"79N|S4r:XBrʥ8HE읈(p~iԒC̏Z!""Į)+ E'ܓ3zV1pSc>dCᲙ̣^Tq;MlRN~(w)O΢fޔ.$qr>/NÞflU93edJfEDD|!"eDDD~ ֌ ,0fCv8۲`lH,n;>+l7LFcsE%,E9X+N>G +*]qXG' สrX"=?@+8 0܈1xQ>O5cADD2YV!!{Pu t/Hs& BJ !(:pSn ,I ~z&6-m2;UoIJ?u;MlRզ量rǓDzqB_9=S nQ➒gN~Di/"!""" kfSt|ke"w]Йk3/gο2u ɘ^'}'ғa~:8p>).=SbŴxN.́^+0ʚ‘'m)G洿Q;.Әxo8m•,Le̩I?E"B my:Zq8=,Se”~e, ŇhI,\Ex>䜌\nQ|q@DD8DDD-*kqȉ*pD(uD^esYA8C,( ;j#Ab,,&>|-*](k[An Ӓ+8>lyZ)>r4ED8DDD 'W%hB&AF)p 273n|xCC ϧeOSbV|tflZ>ls9DQ4Lz&vu&1*ajfxrؔibEM`O/Q6t&bK=OJ-*"!""" L~ 16?۴Q [%W"" ;x{ xC-Nr]WϿ<ΐI7mi>-ʷoJ[Thvس- I l$glQ-*Va 9tT) "X{2nOxV`+`Oqd}Ncr,I^޷"|O-I$y~|mƴ(NAF0~'#_ AF2d@3iakJ҇+{O~uQ:ɰ 2uJ@% kT#Hp=i,PvGsu[""o3RxXoe ؊s'73Ij__~@M#G`8<ܤw5(⾓EEF^лD+uQ<$iH糞| 8aCZwe>-Jb;ibW/bPbv>Ot%ʖ 50?|^jIW)qkE:j>Ї&9懩vtuZQ!fQTJ&uP#'HX`NŜ,t3((i&[~w""uOgt_2w*,lAjA'C}'>U|#ǧ nzUʱdQu]'  6H+ί ,nUcI/OrN\&5HyINrjvaW+Ӣ|8rr9gU <X*>b᣻I:!TX+9NRY@J/pQ KY ';2M)p#Iv\ʣy(QJ+xN j(`3y>eG>Qʰ P(;|Q<xh л),dQu*].Xa+`VNf.`Gp vT*-\OړsKoM?~#St5u:aXÍ+iQOoGʶLW[i!l2bUTZUA ^$7,{+7, xR=j(Yd\0> 呈Uk2VpN,DOmqpwDA$cN~4PΟ3BCvf>䁡nvHjSГ`][|Gx”V`+6>=3GCiX{'m9^VL'>ܼ9 s^ō+ c;a8-z1WRעdm#- %FX7Ym>V!pPN8;s4_W#+8+G7}Ė}W9f~`W<es> I.Oo&C|@N{ A}.}'9Mr]"ӤqaB`+6L\88wV*ny d+%KrÍ+O'Fo6Kkσ 1'qC*I:YZ1(dl>~F)VHNX RN@jYi!fǚ=[~x!"Mn>9y{{8x9!6~@y$pGC-(6cpI.M' ӯ Vc.rڗV1[plkyJ~M6ŧ~)fpH~} pGӢibyaWc! _Ы(lXsPϳVƣIJ&VЍR `(ȇGܡsK?ߏibp`hiJ>?'luZFs'pߛp>B%؄9q{WΒF?KlE8>OޡOIwЪ(-]Yb+)w/O?CO޾we[o8Ф/!gw*DDD/pI*bs.} |#!""Fn4{D.SbZ 0S|߾ey]@ ijMhYWpn .'XQWi?Gc3#\ ,@xNNNdǢЗW/qGc'""دyE-( dE햍ytao)p,?"p0Eڐ9s!pP25 OijavƲ+8􁇛Db>(pA0{9]!""WDBЃ懽7c.!hZ1e *tQΨ)õ0EB 0EQD:x]nXd-"qw^9O &&vw@*JzTM-4M{N(|a?g!X}\UQxg L/"7zXS7T!\""!"""HC8R^>ED8DDD-*-!t [`+-"-*toQka7m>䘮ެS.xV߃#"OACDDD 2z;1ͫndZ  LLd4'dN2~' +-b#{!6`aϓr8-oÊ՟J뺓r>vp &vM<-*_SirXfkQz.?3g-Hˇ0ZV$cjdV1rD&6rӊ u%|7In'Ik&:Y&bӒ\qӎi?'|4x>|zuwَW7/^VnDDCDDD82GZSdSWL)-XiR!8r*pBL5ׇn9 L[XEofzr\;*UtUΨ8 ϜQa#ZR&"\#_ֿ}>UP2&_r4LwЯN ^⭈(ppLmb{],\„ y._ŮӰ-B n3=VfGfeV4۳&@9 V/|$ea3ٓx[vfw2 }I@Cըoدwy|Mx (BgY̷88 Yw!pP'WpmiC&\ɒ2+ц6aȤ?XqCş~ЇI]7VplW7/~ `J -4, p2 i%"S@)h0EcV> Ѥt?2>Ѝ4(Y! 8DDD4Ya~7Mr`U(9X-4t_/CnbG!|wh-aSνPv4VD8DDDP<+R}dzf/k+`Uѳ/Qd+KDZ8z4G胈<XM Z 1HR`PpkBԜwHuC"" ognF鎑<y@cDXfS>d*/aҭ8V7E+\AF˴D܎CKDIEWXhN#+Gj"L@KDDܿg:@z<0y"/X(ޏHI&kNH$>L0^šOLPT1 50`sJYB]>ćdp_t4tqز|9r4~W$=j`eQi``QeA4e(WLhak&}`rB>X=0R|}'1Ǚ#K2[0=8c<ΰLɲ?>\XhrcP9ҍ)V|ZbawJ7`g^|Wy[_-$Gs4Lzci%0npLxVoL"" χu#>JWý|0w-AcqADDwf\ڐ5@Ì&3XOÏ,(ϺWp\;=,eVWGC !iEVt!8@iI67Dc`υ"t8B_A:|60םX,"CP``9A $ =L@ZQXbqucsE%"I|!zև+8>: MheiQ^+EEyd, :*6 [3>~܂Cy)I`\` 'c 2iZPoη( 2> 2MU[1_uh?d4=9e2*ij%ez4Vi֓q`4Rlz4*-\*'#Ea3?t SFDD~W,s^$Sڧ?SZQքπXEibS(pp 2XAF,X%hq!#~3x AFي8?dzH#%HG LviK]Й#8h!gu5}{ED8DDDDcːkV}lgb7ûTdLMSdHHHp0!fmXߦ̆;>qv騤}^q\qE%U޸f TaX`Y~*@$(5AN~~ Mk@6(ptzq&EEׇO-KBkIz҇C_ć5 M+X`|( U@:!8y"p,jaŇ8#NTqժ TaX H$YA=k;DK>ޟ<9a8pUlb?";_DDCDDDnLf\P,0et+*8`Lxx}c!`Nh NoQxPXC"} #U\;3$ pEاYElj14V"pE9lfQ._: @b!/ù`[``._2;v.LlFfSA"ẃtZ =+8\f^lzZhE,\E+|`O.xp,"" """bQٻѣo@,XL^Q" 돥|xCQFYU F/,䧱'wF`,թmQ筈 cp|\42"" """"L;@?MnB *X&L*·NZzظwL8ew)ZQ;WoELp,Ϋ.U(p(py2oSW_gRmΫ[D8zZ @+Hh?J!BQ4(@Ǜ훌KRY_0&5x-a.=oQ~GT"afCwQ@/E; r K.xW# 0opH,B> t$}; (M8Շ珘_IgעaX>f .%t~ɨ)$/=\Exoe" %t@p>p((sQrhM<%p s9NEa Pۃ$M)L{a;P9q Mc̈ ,v{9O)8+CNcM"Âyp;FbmRnq)#REG5 NDmyi{6Hd/59r:Dhw叀3К,Ź6+ {ޛ{UfG\POes%): G2l4ɣniVxPT70`LlP[tp8HF 92ØA9k驥S;rK УJ7TxioC0w ؝eFs/,,1}RLFy`$\%9Lͷ6zC@1l)0Q bcudEQju2t_ɀ=!K񋾔)4 tt֚4;mk,R`}d/48"˿Eꓴ&P[Z]kƂkpD41"48m~p \ q.Nx0,%73٧DR/ Xɤ78\H |߲MY#,inЅ)B%w(p .]=PBu5Y kotDE\^E)iMaaa b %BAr?J<$ BF>J^o#2yp(b24G )0QEz _d<wrwM)h $ŤOz֤(o4=?h{#%78QB:U.叠5O^}-J*DkXfw9⎇]!Rwpp 0־^@{(ŕ/#G 5kX$U ~j!šO2e8 B!]q(S OJ<$5~g1d*($+@߲X!+-yVĬiu%)[)s}}"T%Tt{O4)s.Ě4EI7Y J)QBZ2npٮHDCभ#>d4d`+H)h,Z+`Ȥ 24kp󀺈xdKIV\[(IJ؃N[{թYJ[ G@cۓz%nU{!MA_YRQjPzokKF)sSXD pKF;8ލGI "dEkɬmp,,TĽzb ³yCK|#wphao%79t HB[rRQz5RҐnR 'M']LrB&%1 R>y(/u&j/ZWnAM{79nX/h1yPh .HSS|L,R%ST%(!>/^iggjy.Ukr/,,,sLl,&`9Hxl>)*|X%n/K៞"xeu!ǫYIf2r1 ">&=J4I)XH-IFȫq-[-ƺ~>(-5f5< xnz68-^ M8CXca3Fconsaaa6{cq5 ?sQxbrb??B!s,ag5(|JGTfcGB9qaaaaab_KLLz zUbNW鿠:|~xܽ_QdKFH..D8ޞ~O4KF{xɨf-D`bUD>[SyxgQ68 p9GN09o wH1?ӡnQ Y. ~usϼ=Rscqܚ FaV@W'~4|15a7XݳGbIԌwH(N䡕b7cLl@sib1P0z.wr'rQ MBF:LĆkk2I)ڹm7qi#Wr م!BiȀ?9{CWFaaaaaaHQQ\M|f1W3,#PQ Jm'Ie`GfRIy3.ZU;~bݣ쟳*OULbMi՜zm J;cb;zqۥ7X㥗% R$_j; $dM񼹊rYn*-+pMV"8p4'!.&TTj.Q5d9C#Z lR W"9[El-B<>3?2uvXXXX>0GiBIsdAeAc=DsY݅Ҳ1w <,[[RY n]LI(ޣ6ZXw(3SPs5)&{5V\}|OWGC0h"/=zpi)0yI4yp)IcO |FH4nL*TA Ibl:lI!MKCA48rgl`#x0ijmoIL]@2y|n.fRH1^9$ dRwVHsN"W2arҳ#HK@@ wz d;8(؂LkAw$H "|uWW{u'>GQ̖[^Mh$* az1!ŰVw=(-)&D CMJ(,A/8K9H@ VJdo(˕&]] J&_:frEQQȷ풢4IRr(q.eێ^;8!gZC ')d >(=G$q/f}y ^(ZyPx{L>½ڿ O)y8vD&g;8h֚]){552 jQ9A/xpG :JLA&H(GMvmpmb{5.uC5in_zy@1$ip`b7Ll欵VsD ?&O1LK6qݚG @չXXXX)* |@ D( qi\xy"Cx`$k6spC&ݚWͨ7KL)iqi9q|L¡ܫYK;|pzѧåzp'{¤,<\I|3s_8Xqq# Ptߩ\}!z b諞Vt'fϴe6}Fp̬}5;[}yi@7 uO%6aOx6S_@ʧ p :m1!ֿX Si$/sC z޿Q]tt!}bM Gl qD80 e xj\wTm <ԗ+dh{}۸Gnzu% WS}FKGkr̠}8|p?.,jW0%J#` t@~PM& %: TqWTAMԐ͝0u, or-wgԡ'@ÿMڅ.zd *֭om5KH@=ET4p@.j5-:&x}P?}Q2%^P ԭV8(cIGInzɉCb/O9$r(aqSpIH wz,.| Jҫ}%V?腢35RFg +C}EB`Z+U=Fym{]/\?"H\ݡ-]_A QT?!$GdK >Z &[ETyC ceV 7x׋%g v[f5;8?px/jj)Nr28Ÿj[ ?j^ʡ ǝnйL.S_XR2?bCv\E:mV®J*jdBGm{!k*FCYp mʪ +mrN(8NMѥVʡIl.qmp^3LQ'q E J OݾdY,۬GfRb((76*fb0G1Df?ʇs῕}.j6Yex@CT:; SWt'Po?WgWaEV^T/ * @0ľ u &਽۶E682@npdwʷt,pMDf@}Q3@8"Yh68(ҊG.hL 8뺖. X 2GsfTs8>sή tL-a3Mp< ?γyzO7Axz M -8 .* #7Fb(͒}uR}̱GC q!I|ϯk~\*|ͯ:Q9M7j@'EsRg6'YC?M8-D^Ov4) ӝ wgb_R6/b#52"rl`%rr!c7r [OMrѷ|a l/{StN\ ><8o WF6mj9Ge~P*~{6J7JYqN٦z¹ ԐMo9IuOR>,3gYg:W5ti +L70OB7> bΧ;s]P͒`8n.TeA))'kAqd<$럭נP,x= .v)p9_Hr!rp7dӽ3dlC_$OG}YSJpg6t =B\6jo)3= TVAK؞9Mʾ*Q?*;~ D~p4|հ4RC6Z+STmp8_'\Xx&5!-3{p\ a>;6(zuPCHwͥX&体Պ_f bGxWST$*e2uzzMeu%Q )pt8|8BD}8Tu3x0 Wѥׯ ҃c/D41 MะaFy,|'ݦ:\6Q7 ؃cXG3 !TЦllxJ\}%*McgJ-iU P>%g'2ǜ~eKf\ԐMz@Dˌ AF/3 > "{EW$KTi6wR4C2 ):㭏,7.dzwWD 2ZT#쪗E%,*\`M$ib3Є  e~ibS] +ݻfs,*ΟRɣwRNĒ#ޓsU3^ p3~w͘};{L Þ \I?x7]./\‘T%^ffDns,Ȩk\z,l0wr+?*樃t$):Kq%GEv*4H.-4 !ΰJ7s[Ty;0lbf3e}2rz7*mά;|"=,蛛/L:$K99X24ȫ(ep M:D5訃g_~א(X!6] yF\Zuw^G%A"In-Is*eUD0H  ]5e0GLmb2-5Id &B!<-G,sS,*J4KIjX,LIM*6́O4 Q |@4D롎_Ly@VֱcABcLSD@)DQ^ʢR/[?^NR-C>A QdK}CYZTAX )MB1p (˷/`Ş.K^hҿ~* E]EGRQeIJ;>]ɃB!CXy[3TqY򭤮knGYD.6ԡ6Kh`5pR(@;9Í 4ptOj~ ez(BXokFSF2@u-bh *ql ?zpzWu(`}\Ee")muQ~$!GG25ehvk?лb)u]{ԪW)zWhQB".*.2XZC!qKܓ:M-2ua H5ʿ.4zp,EH,p.˅.$67p4 [hS]S'tȴ;`+ }gXLI;Pߕ  $K^>,XX\<B1phs..W5zhۿۚ!xЃņ!MtBIzp)"" jHǾ6p0`ƢUG?P C4q@G yp,'KF5Gt1(XJ駁4ptoB C247D"ΨQ` M ']Hfa!:%<Aap) Ep_m*(l&4 )sl(cXa- uͼh wZ IRJ-aߪ Q <}#dh18]]H5Bl|8ff$sYhB!YT$E ȳ, @U1<,js4`g0,*"4(ɢ 0aC.PL &I4PZħϟۛՅF]]@iB!7A x{h !8/{2wBO) ]3ƾ5bpx<B6p 6֟7(A=Yw *<~)V{7bȿw=yz~N/*gG qi'ң̵ #*FÏ#*( ƅ#B yxr1KMk'm:In.AW@nTn]/ſV_7!F {G8783q=ZM_cp8*54 7CtUg ñ%S{L%8~޹iL%,d pLq XhX/e?|ɺ$)Œ$B  tÊCQnYyNǜeZl^TeGD_`IKk7>VάPjWׁw_sݼ'y?p8&o9 7@iiE`:jX{)e0 c*a!MvŎGԁt % v&J_֑yl1AZ/EM9:s;* HY&h{o5if?=M}`.&JF)ǒEiV4B!SOٔ^}ႤOyu` PZ:4ʯ͘v6`oӓP2~ [aA]@QV.A|)V>{Gsqa8 71h;VԵii!e?x >~Tԅu:_`ԡhO`Y`_uqS]2ж:%&q"4u"M}"hElKfl(R%M~qibnldZuJgZ;Ek ȃ#!^T3YQv p졡kzT0jb F[ / 0+Z?oJ>'zWn/Bw6AKPT=wJlhrؓ^\n9{GhJ#~HU/]5Ų րR$ hyOXOJPBi- a5ya0I-EH5Z-&8&Ȩ?%/F5fQI!88p+ tH0Y84뇄uw)a91YI?[jo%kWM> iE]J#$o,5줴s3 ,Zp9ʣH^{dXQ4_:_N6zg|k]t­֯!bZlf *zߴ,?1pֆY7LwmsτB !v|Lӷo،_m^~+e|,ܲg&GzxI3號v ^?f΋`)'b}NUɷ÷+a+Q"~Vھ"|+;俥Ӊ<iHEra qo`+[w"*4̶qu`%oS$֡CQfJ@L(#!pCC;d0@"M1;uc%/a%,XVwY4Nzh9_Y~) ;]^zҋw_ &>C6pajS@k%yU P}^kVJ|n*f$j٭L+T22OW hl^ɏ/uibT,B}h/ysFCQTPk꩛*!p,* ҘbK/M1p!+3> `"\]"dYVÕZa:5tϒ`N^zȲ|3iA%-]%hC_c}{ yE_3%UޗG>&|X4 B2 ,F_ ` x/nM;dfG c6y3[EY#~"ݝP[ X!܀8DsQG.3N~L'"RI0)wMWX~]Z0`.ʊ`Jj R5X ]mVlyG"`y *w5K ! X| ^=8|o*aU}hX]9\}3)Vŷ(?B `S?]" ǜ'@1 Dz˅ jPu01_0 f jzu`O^jkuSCy󕰩(%8n IFB#+:|4 \2ij !qsS3_0{nx(]JU),YoB~+Bx{ba-ah/v`+Y:bnI(@ǟ~<ƺ__ #P~??oo~r !8B!AZY`cP 8j@ ,3:2dWBӍ=|HO B+D6,R2>"zU 9c*2^B.ŧ|SƋȫO] ))o}PaBx(1pB!<-MeUWc21Y ,۩a)">'Sf$gHsU'l(Vкm`1g\x4s鬖*D [eoM땢\3G}l2pt)2p0UX!1pB!&B'{c [?X] h!ǖrQ46?O}DT ѣ2Թ7B!B!$GJ8n1O0ObxB Ө "6`6X5L~KͤRp@mQv5@JS)ԅу.18o| B1pB!YT$E ȳlP81EH`#?wOKv`,\W_.u )]>. HB1pBoO godC-,B !B]35bt!bJф6tog<}C1p_+A bC@l$"%(d dQE m$F%B1pD7 #q+S)ooo)>^}G7oWyCC5+7'uf ,IK$OC27!BlS!<۔G[!ꋐ٪iPG1Tj+g  ڧiƒZr XM7(Х~i3 wzz׷,]V,1=f- "xG-,a.^,wJu莪]_~_?,ZûZy$xpw?Eഈ]}{h`԰o^%Bvb+|GYhdȾ޿rP4puB'88r̵.L=\<_]k>}3>(۸ (+٭.Z+mi4̊@ ŸPe`Gzo-X$ Y\?ͤEA6ݷ7p:^@@t׽e% L6ld8z-|PGT]tRD!Q\A2 %)r0j&,l%{d|PL eZP|'bo\+DFmQ  w#=8|G òg?8u\]mMć '~&ۇt@ ATz.Κ1p <:ȎFߤ>$@`YG1#L7 ~|+8kVfɡ5azF0Q7WTpW/#4ɢ$)M5x:.;~IPG0u0dX4VIqQ&`)ù$L=}aKMΩV s Xv]]@i?QjE;JP]}m; G(r}@?L7_$;GTB wfoPB!8&ЛrIyO!8s׌5<7(B1pdȪ&C"8fU91oQf3hg7%2g7 _a;j*b!G~'Qi4AZ5鵸J>bky37݉rzGٌksB!B!kb\KU"(@ c*nGe`)qAO싽4LUu{~~V% KMo9jH54K6rj5,U0QuC!B!^{Z;=tY@J> -|X4f =#c\KFc"~=,ث!]D-pȱcŌt[RPvmC#¨t?b*uGi(B1pB!zph^Ln0!|܎Jln(i6 `4|[) Q ,?JNuGIN"ti$ NMuۿV !8B!vxO0w*b_vmyp< z7\*\@S8nLAہ*.<0% U63̈́B#Bɧ1 .Ec1@%?7-c.2 `%g\ 2ʱfJJ& P>)#B#BɢbR _>N )Uevϝ6fydK/b:jr"i>X|3YT|%F+ɖR eB!B!}ǝp3y{?O!G!B s HB1oB6`_p3Bc?Hg=~P< N"RR2JyDED/0%#*s?</ !8$Z ~oqG۟͵.wj!ܓІso8}hmAC2K[e+L9МF!5 &iүe+| #eWR55jL'b05qͣ\]zEzszwYLXڬSnB!'NoJљ2o )$viO\y(RĚD5a=7ʡ]T«AZM9n.NaX/ªIJ&Y̫a-Cۗ֏j@%M3UsQXB!d篷Z|d\ԧE]-{pHCA)Y)&!`Ю^3\Oҽ;NBe)\PAEk^oX4m1ϝ8K@gՌ2QH KW~4U5P HQDY?k!g118,ƍ:LBYT|B _<8RUaaG|o*@|zv9fHOUK S<v*Gs~WIfN_?ńB5pܒ} n,ޞB !B1p]3#}-B#B!8 Ҡh}*Ae/8Hrv7BXoC5EL 5<{(b!6 <p"a TVDL̒Ta>$S?JS]19XjJFfc?) 0|4d[aJa!غ LB#Ba'9;MtTd AjtŖ;^LbKD-QMT_Im˻bj,5mp3zuZ\ҽ&}YfOh j`#!8B!vv;ڡi{r],$z+X|` Z)8 ,> ߱D-p F@Cn) /?*BA~(yA ׵ !8B!{3̭ 4$`M"v=8uߎ -L 1 P؄yvCX\uuGo}gXN#MӾE<$ih" " 8B!B!ᆙ[eaX>|oG0p踐iC.%`#Q :GCvY@J&;ʩ~4OOB !B< Ӌx`dWUD` j`:ڣRLsDoQT`z !B[X,Fk`%QUJ+B1pB!YTy/b_ 8a *UevOV:*1e`>I^OJyX,*xB!B!qwĦ"B#B1p9w͠ .#sxBC^x.]ZB!'5bD * W8+Ĉ@)G ?17HxqDerEOB1pt61DrLDNԿEU!B Jf3zch)ᤆfCtbRǕ|c0Wv"%s"dd!V ȔcS&ȨXJMn_?W(p\B1p(RugW4-FLm)tI8jBO!ݦvN특^vt-<>f! 9Pd>eލ%Q }@4]SkQ$u!]FM}zc9&X=M̩`.ju˙o駿/{?MoT+B70p5wLw3W|KYV)P{ZVՋV[ϗ"T(T BadЖX)V nK渡-VRz nQ" 5YTTypWxǢEdK8R7;f Ж0.0k׭I[}7ʊQ挲b?ro~Qz-ʮG7B k֦~wŞ^U\e/MZBRRͫ+ M!VڞihjJTG]h"7M"N} A ;hS]S'tpc2q,I[(F8~V/R%BC JtwuJ{pRZ@ʅ44Sk TZ!M\{fzp9Kǩu0pЃLXpL=Fh<24pLU㚁Fl9oC(J8}B4pHg%s*jx9ktG\&.fN BI [q?c` FDhR~Qj`W I ,&hfR`zTpM,^{AF2aȃcp q!ܘUzWI,9a) ÄyR- B!A&I,*k0,&)N\X0 &8TI~,|?"!d ![84OՅ`~IPa)Zħ}㗇J-ycC!8=ɖNtƄB  =B1p]3ÎB!8b(k}&B㣛0p\P7p,X}@CЖ Y&fY~WpQ4K^C7/zX*\4toQQ!b0FD@[_HA!E-yRI3S#B<84i<10,v:GPQ5z&rpYuQE+\G a%eqY{qԯ]V1!~_ȊٖkX\".D6hB<8qw*g㲸5v w=S;|s3}twNNNǎ:vغuk8+WqԬY{˖-Gయ [{F&zTE(;<`_nV˒dA@p87sSg2>4|GRÐ '4Əx8G`4=˥$**oR$,#/` Q!;r|t۽{enj ؐ>l޽7nKACqVzr{P\B5`L2La(t  ޜb;HX$9iMﴝStl g=_r6 i7}a5)rrH<UrD"~챜柗Уxt JXAqWZG?x )[~&,iޢ 4ܡҚ F{P{9gW0I3y0ZA[EE_EŇ?DrP vbztBz)lh2~؜b䟦{!Ei;P!O8pQb֫WԩBg9rd+W_>~)cٲev 4!ϹS1K͝**CE X% Gه $ueVB,G-A/r )V{}jCիW"33sfASO=  Hp 2 __t )s'NȨT.1eq5/O(RjÆ4hPjhذaqG9 K3ڵk7jԸN:S}BpL2EaIpAA$"8C*ZXTbTi7|ozm"&UA[Eebl[yN9eb3bXi<(4 D٨>[+vIVvr@jn\ ʣ-As}%ADh=@\8S!xA 6l!|k :t„ ͛;w)S^z-NE+1Tڵkה=_ze8 gyFsuǏ"| Ͽsί)2y2rF\pqp曳gF͜\c # $3f֭[A4XhQ\vQX38yI3gY`|뭷{yJ[nGiؙ3gf ݶmUV6+++//O9L  Npg =a8 Cs% w:GAvIBpKF)'3.zV%)|-d=X؍zD [?3Iu3Ͼ0 o~Ű J D) :j׮o#K~(ϼR(1wzd c{Ux~eP+W2$Ps\tE7nĢaP,uE Rm׿uϞ=)-'MHpAA?Vlw_̅Y|p,9a6YT'4'8g)l 3LRۃqm}%8Bp`@|(E5kܹ~5\⅋=c D>|x͚pAvs=& Od *nurs[;31[3*V6IЧO͛7ѭ@0u۴iSv۵k>8} Nz˜kfIHpF)S|VbSzW^yLi$8*?֯_H*q:(KE jj}SNQ~W{i~&T*єcd%s 0@͔ͷ܂2Ipԯ_ىCpUz K;]BXPL>Yٟw=GAQ ] v[$P D ͦ4(rk`{3-sYG[E;:O8Dl-[5C/>8G2rI`N_HHpt횔x衇`Cw>2ybkaQeJ+AT$>TS_}$E2~{ (wzv'P__7ʀmա ">utzC HUmn D #%YPYLʙm*w0.((hݺuJ͛/[LtM a rņ+?A]v'G[   P'@hv9D2G,FLpۃ?OgDߡ D zG,BV`_+1. î?ts}bzsP6G&J$822*/l 6l`Xbɸ'N_7Fŋ yqT*߿]$8! y^d!ꍒ  #C7ru&3 OWp$'8#SзFQFTVAH( x$cQXX4 yg}j9(M#G:C={v:u Vz? xӽ8~3N:Тũ`76nCZ ~ K"I' $ (욕Pe˖Qc0}=z v:`ꫯ$ }z22p կ_aÆ gPZ&?b`rt1sp8d  %[EŇ?DZKr'NAKm Pz&/-nTq*NDR寒3_ kJ8ga8Ur05Rw {AO;w. >lڴ 1&&&s fffΚ5֮] S-_~k*!Ia&/곰 rުիQm˖-}]w۶m0FΟ?PٳGx駥Uroo!(sUصk' :mX&N8G' AA(Hk+a6~ICIK/ߠҥk-1npqHm۶6lH1dLL5/ǁ qeرKիל9s~+nݺ; L29oo2wA  Kp$Waw9_; (Y8{)R !jۣv 4 [X6zhI$aȔ{w&̟`̙\wu'trZ<8괏?a'e^x!FDÑ#G"oC[jU2+ȔC~":16f̘]  "ǿz-P U!'H/{g8ss}_w*?b4*J*10;KK-$wMK&ޯx1&g_?n:%Oǯ?sTJڳ|?ݲ/@śE7&bY)io@t2m/U)-% S H #eq"Cٕ"dJקY֋AdO5n~t/t#RwGF г]ŕ*'OxaC,}MLz QGwɝT<{ ˬ`mLZo#Y#vgy/J 1ȁULK)qRݚ 릍d1RL8t+ pXX=>(f8$pYLDadQmYfC.G(}n0f!mGF^蘊Bs`"ȍ Y=dۈ.t`#dzBPT$ecI in8Ru3GҌTdlˏ }aU/ ;88t/:u5ctjh?}=sxH/Ta&;8?z/&ϝG x\Ϸo 0sP7o8]k{8WݲJDjh@*S/wj_1|chU3V.R^H(O. p WWL-2n %pZ{q L2<ǁ"{ւ ZXvpR^HMS־^ k᠀xX%{LTef< 8ޮ0-ƆK <\jR.ƭ}ZHy3@* o*ʩR++6t(O߯bT#)kpݣQռTU! x;I}wIo^ةOOeG0gt; s8~ǿ/q,9&Q&z,eS+o7ץk7wLuyI&&S\Sr@5X"Q-ef7tz.V'>J g plnZ)|SaZY .t9(FJ.73ETAyzzZz࿥"X_\5 o˔m=g8RĐj8حH+1;w6ِ-@U/$YS_2ܯ~swsd(jb3J`]H@*/C[ c\kK*4YkNSk\ja0V)3 G<:ñMm n b96~jyI d7Gʨsv9*d/j/F蠧nF ދEgd;8vVxb'"wߌ+-lS9iBg`6( =b aD>L:-vLap%jp\@_1_Hj_Fn@dUઑzQ9g3L#l|rf uYoVX= )@.Dzs ̞S8tPې;8tG #= (zVx QԤwp(;A؇ڏ8HnZCCazQvp }IFk2rl ƾrucuzd(4"6Or~NenC(Y(Z3++ ћ~)ؐ@ɯE)czT#(Yao+8>1 ͞2h\2!iJR%͇*l0&.Oy2!!!HzZ՚sWgF*ċ+onxK[&OK#N6="Y݆[O0MH;#z4Wʁ*K҆6VGlx1pdNtTm>W:kCE^%3gM4ϵk*׫U*Lz`w#% W"VWx3\Z~ ?[0G|MtH+6 pXy=8"¢3XiR0yZAcU]*,0,ԅavL mPg_(R`z jH jVVMw'=[uճ2(yuUa%;nΈ<8^;TG5l ?u{ݬB[) oHi1WOw71\Ȇ_e6d7#<@i*Ё5KsAO/R7 3p<-~~ ;~fHBˏ&+ލ0pІIsG yUXLlstz|ul rJ5HK- 122%7h6rYq8J+|0q&Q#s2iCQa2Ooa Ļty:|e6d7͆pdw8@$ݶCu_$@^h$'L@%Γ*$uPx  ѣQۯ֑=OFWϫ|tYqmsp8ZJX19zG)^*cߌ쟎O_~?ͫչWG8pmj8Nup5qpb(usCsgoe7l#YRw[(p?pvphKauEWHn@+uv@ьM\ϝ%$i[ҟHJRa۰M< [$XRp2}_Xdnr]߆TXV>sUTQɥ׈N v5.p'n='<@PSӷx 8 0phN?>=>I"pr*t4ұ~fb@E%E;eA].-zH-;3:BUQ(KNMҢc_SdŶ0٠qHtaowB-]\ ĬJd"Lݬn)c*R`+X3s|"sq T,gY>#W_o{?/7O`xXqF?Պ&7VhPEv&2f`Oe0b.pd5`QB![Qe%<H$T]&A*~Q;AQ$$,+4S,Ӫ߅vUK$sb ~` CBHG԰iyG;ki_xIFtW;/Y}I8_xjSQ<`(pr 3 mIgWӼD ^)[(pB!TSƘj&Ap 1rV #WebL,?Gv j5HtZǀ)$}u$aeoK͔B}5M)pT;B!Pd5s%g6! @efmZN! GȵFm [qiksHE|*ȲE%yۢb+ߢ0"̑3H< P4#cE[T?ُ:8B7sV9o$Et6_'BJ2jdN2IFNJm$ IFuIF1(AƀIFs27 ,F'Jc ^2X67'3L q eJCl_-x nB4+ο9"^ i G@OI %.@^7{? !L̯2Cd+iRЃzH51h u12G X , jcP4):&s譶Af.c2Rթ]OGO؍2qj2eb'+L*BM kV^ Ɔ!-'ujf>f@lK8,k'_NZh$lQkL`Ξ-j+3} F?U6i[;D̃B~˱8rV_E0}$?M!>%Ph?tЃz.t63^$ k1ȟoڨsPLAVc xP;D%H&?ÞahHE.>OPKo!P'xqJaRAꢟt-%=xJrS:/7haVZy~p%(p]נ|7Mz_*+h8~ȟbv!AP4qm "8 CB 3NtB;O펁zP}Age}]DmEzE.ӏ@x soL4𷨘M%8zT0$ƀ`G E0IL qRz_'-}RP ~ԵR=Gc+,#@F ~a?B!8)βrdjDlsAAXAj(p*I8C0FEO$J$f v䱒?/5$B0R6`e)uO0vB7}shyWg&F5U87>yGAdy%*uE =ma_2F47m Uฤ{B @N*qbE6"8,"G 38nb16!0ɨ+zeX$*+d8'Ndjd6fj ¹Vnq>xj3^981 fAV,ѿA5s!C{(U`/^~M6}5Lt`K;_ջz/wuۂUfcSowӮ 鶾 O n;7ʋrJ qD'߫r,)0eM06ǚtA[MN.ԃJ43qꌃY<UspB!,fj&AV&V X.5Lld?XaJUT@.2? !W_B8T80d:Ta5={ ߸l>Y϶fÓ[%΢lp  Ds1hGˍ˱r ! p*JZU_F;}hFSh|7;νgŵk J+V-eKӜot{EJ+?Z;-GS4YCt%ۺWC~w.T#p ZFG]BB#D\^E??6ST=Pe3j(G=QcnB,)pd6e3qW''@K&8xX81MR[,-;j3`㈊8Q|OX|=ꈊ>*Q<}X{Z>{~ 8T$a7*(f.f(_zK 7.$b;h'-$$d=tuY{ h&ԘJ2zp9Q6FɾpB^>hI.݅dٝNj$E H2btr`2b\f(3bwL{4WI(WmF.oĮе-B5P *[HeÔP]1ty4]y5LwyB.;}zzVT#=&J2MqJ5o*7煮mYه*U] +y.w\hwePN59V[ѰA] _qS);Qϻ,!Ɔ.cO.2,Afݲk(9 BRȉc*-tB4v11xZ1Rkc H;5mfBTBШ&pI8l4zl:󖮵#x-/O]2}iFu#QB28٥ j5,]oCN/½pٯY(!&c0.d1 blBfAx Eʌ7 /J=ocn!I ~ҵPUs D- E<ʸ6-)pxV9:2dN̓ 4(+g)qlYKiL; NԨO颟*ٰ'p?=}H$;GT$pL˝ 5^+0ld/('U7vrQ)-l݅Na2Qc )B~]w~3t#cOy (\mV89z+Joف?w(G=b(f{eb@q(eb%O(|'2j[Ee#)zGJv ӔSMvŻ 2QwQn7LlVbA[͟PkPQbqGѫw%'gc~v$ɽMC. pQGT:i8#*2~>0mI dTl'dt{%^$3LtY$̶$'ఉe#o짋;n>5PĪfri.ddʀmX$ ,rN75*:]&v2Kzinh$T2F}5I*JM ֤ 'je8C&!`}vqc*-tet1Ĵٝ*?8f9o6c\.pdY{YfBLc>88llÔTJueXoKI\@;Ź}j0f7O˻$-عK -cNh}GZ8 ˸+ܡL49Q66Ҡ6 fdTiI'v.  p1zL=h $.QvVp"Y۾!uCA$V$#*8M/ఉ(0M&}YZȂD7sQNEi;?}8$Zem'%N2*YaΧ'ՋV`!>_7m6`Ҳ/޴ѿcA2GIFs8[&v*;@M¢4(}F)_\Ī21&Z &݅?]chy,]xH;@@8F }`XQGT~%oQu`qDeUO_tӈDVdf IFvNZIF VCdԪ$e$Z&Jw]J2z9'*,uQ6RF}P}SļN^MKj큇ڬh~'Q"TFAzp#a T/SR$-\6>LɌVPݍRt[i485\%sTeb*zIY U?UT4QiSɨLl~ozه5bH&aWS.e{LEmﳞ%R_- d@P!R`}vqc[Het1Ĵu1Nl:o6c\.pna u2xDg 6Qj =9LS2󖮵3 GRjz\ p< pX,'"A:Bb GsYqi0&n!I ҅"f& vC;8†agXE_epG9lASqz쳗>$%@ ?y) pI:1x $.QF]! {7nH;HD r&zwpDieI+{wAm>-?2gC.Q񓹋'2iwpq *n'%N2цiIFӅIF$yw&j6 ri0iYo>rpؙ&އ6!&!p< pPbP(`|Lo baQ><+k~q!*6Ī{!+IwWEvp/Ujs~sU5G ΌK8_<> Z:iaC#M(G8(O^4wm $I F!;EYvr2Ӆ9G.JRT86Zg zR-$i!Ev)p ҅%e(K ;94Qkl4 &N⌋.D٠$}wpŝm&:Ӵ~ ɨ$w‚IFGV8Iq!zQx ɨ:9mjH/jzBV~5OTAȈ> a98EObHJ78nyK|L$e0Q@ Ҡa T qTXwՁn% 9 ynI#ԋ76r"qJ]q@@.|w Eg?}K pDEQcG9pD^IOr ]ߓ ;{C1K'֟vAIFvNZ{IFWE=KQKٓ*IF?eߜ>DeyZ 2Az7WQbywl.~֋~ס\ j0d]>MZXi6eDޏrAY R?V&vſȭ *R)iAw1yLW&JY.F>dQj0Ul urLA?^:~jܯ. ŚgI˺+{H):Υձb1=Lh+(Ê k`lg_~9g 4a tП(*pf!yuY@km~Z+E)Ǧz '`[q 8D-sKaBX˓mHRTa*쨚nw% _ 'q!H>\-C\,A9k{v7/[=uZ3-G. ~;qE-X82_f4N)$JHQ܁=Q2F1&8CQ T21 &~pPt4[yU6m Bi M85`Kk!KPUB݋m7px 69#sTjJ.+}[%aQLpsP@dHR!â :Qx?Ep:Ho}%mFUX*GphI8bl7r8QBū{H(6UV[`_EǏL:?;qbj1ѹsN7vaYj PrAǤ#8P"8#8p8%Ȩ^"K]dThGPa;\rW< v/σ `//7f6*+%$ $U <:bH/arK)=3k[qח-[VO*UJ FݺugϞddyY2^#YB22tLPC6!;WlC0jpd6gl%62,uXV-4mb%5}XXEe-fXUaML셰[lUYcɘl[cs$Ep\xE??|+ JAplذN4N;SNg}ʻmPW`a&)i : 8ŨR*UYVʕ)5\#Ǽy)l.TLkԨQ| l*]ReK<() UUlg"j(Y;?,;:n˳Pӑ0իW?蠃y.:/V? STiRT G9EJ*vE,STrzƀB(c9Xp8ow@Qς~m 8֮]?P"S#8uIgΜdRy]t)ӷQ<=J.޽zmh dLwy}zm'M2dEz]r%< JXx3fct g^ffrz1gΜ#G*Uws΅YO0aٲe˗/'X7ӧ7j{ܸq \)=z J~2ME_ԅŪk7o>nvŊkTT9cF]T]ųICpdp*}arX\DO68/2H]dKP.2%xU(2tijb(2+82T"9RS0ך0LPЏ&J9cxAX %f 6em??8_۷Ő3s, @KH+FTV-0nvFL2|v:Ѹp"$$Z?$B@#Č@)w'нX~ATz衇\7-.03e}0fa4^Gbޤ85,IR{W!0k< l74@{xSd)bsQe)ZE`v[# i<@~RWOD,!:Uou2D;'J{i̡~Lp蛐=ЩVG1@('KMTmJ6~fHPdqi](6.m0N;zρ5x:x 8~\G d+C۶m!N5:$/lڴiR,U\\]JId"%8ڴi}r_{hڂ}%//qƸkxɒ%<9so_Z|rpza t "nCC,F"# 87ԧO\T2e"fY Jںuw}WWb%ZV .܍+4}T@ dĈ4k޼QFP8aÇ'8[nТ#Bi" i7~+y #\ۜJ9V}8нؤ$] o6 .U/iPr ɉf~J!X?Kue)Z?KpO21 Lύ-e^$/ 6,*7FKHApex0dY5Cf)y\eاEN A{v5uTĤ !.Hu,ZռZ"XڤR>Ï|"]5"8ȞWs^F a 4?9^dT+2JO"J+(EFH *E_dj.PvLp]r˘LV)*)LFպ Vg Fr1|6%:W%h:{!uY~8ϗ]vn'8@1G&| H0=9Jp`6(+_)$ȇ͛h|jJXLx*$A:|f+-Z'/FEF7ސR sΣc k/bT*I_MEt)WӮص'+>N؅x%(F v"U+2Q!vRC C c& y/gY'P*NK2&Vn+ &,H&VXBkmbu&6ǻAx:@2&^n 4 wٗf Æ(o).(^.ZPT2 ֭V)sÇ;w" O #̈tv"bi:K~[CGmͰ{MOVFvXjb#XY0 sσ:T{!JxTlŇRJ}[p8#G i- <@8qDKX2߻GuV'Ji̡P# Y'.= B`D0da.Uhw!d-͐f2I'(Bs$Y5Q#3<#N2Z+WB.OA:@ { Rpq PyfTȔcLΉ'$19Hp@aD@vA⌆.]> jja2d?#Г 8 >s Uشi nݺjjպ[p7nir/^ܩSg{r ;qu=1!8[lYw-di 0 paFP[JYAj-GŊf͚%gCX$@e1E肑\ cpx \#uy ٖ` AXAT*!]dPc2Q$RT4w#9Q:Qd{VpIaB#8dms*`M%㻰pQի%`m;;zk̙ 7Uyā wW^dC <W qeϘ1в| ̡Z ,Ā)SnݦPd}{khT$_~5m]~̓19Hp͚5Z;8q"[] ,>+!`(x}P jҥ'OFMҥȝ t/3w\' KeZhƛn js@"^G{,&78zhddFp8R@'rߋ!J (t"7U)W|5 VXfZUTw?%nM jjf):w qDA˄ƀbeq؊NI-A{0}aDО|cx%o~fF?| *6A`ؕ~0(qeʥqwRE΃mmll ,ʤc188];3D&v ?wMP% ]'>cq iNQ$74g +STfaD} I8ncJ"8EO ~f ch\d4G=mEFʎIѤ, s6"o:1L.ITmâ r5J/t ?*Z5ۋIch .0kk_# x|<:SVyO_a,NT\c" эs+g*UJAQjouLa6h^2hƢAm)O r t8 D{T61-_hɅU(GʏX@h1c䠕g/`}19+*8Zxxx`hO++}ݻe^`4kʋIYMp1Ƙ4޷[r2 UAQ=&(Lf[2G8hC၃PJ4&b*p98t/TK/&JPxC,${{x"Kg/} ^L͒1`-|1{DX4EW-|>??D pQ h, Nϓ|u8c̲`gʼnA,+`?szLdz+*):&~C=4Òث4&*}h?GB@=6N^)p$RQyW46HDZl'ǹ~c뷷RX]]FE~=F 1zS.# dABсCȯc̲ a">FDGE=2|C Je=IK =p`jOx3)*zB'+xHjpt1t. vWx G f-  j g }ppptttxxɪb'R(6+8 ȨDٶ:U;q^bo?wZ.<i 8x(+ )),xHd-+lK!IFyqCjR68M/!I/N<ߵɺ3dy&. G\m0ɨ\2"#<:rICxTXl2B/ӱnՉw7@Q/Z&vDDcj3?'K**PA-<_{{!~ oZ""" """! ?_8|"" """[?Ws)4x_ܢv8שAJ6D7#pIFa;Lzے)2Ye?$]2IFIvCLKɁbRM'],uRԤAI]!S<$F~ED-pDeVu¢ ID4fa =d:]yaޒc_+DhCք%.K6x^442+2ŷezZ2+E[K* $OwIVg3=#}9H{X}H5u.8 Az8$8"RQ ġN&?-f>\<>5G(pO/Y;fIzXH{7r~$LHv rQzՋݢ'u~rGRH.Wpf. lD,9d aG~,"!"""&ed4VFYvі6$!Xl)p̧^`C*^=1~:ݑ#3sz⭡ADD-*Ee&=$6Oۢ[T)+xN[W?e׼ps̫7E@ }vљIFgfL$K9(-oH2J>H^dJ>9GpgCANw2( j<І( 1 ھm게7).=4 #T>2⡋:ZAy{0SvfC`X/?>n!>2`>Pṁi p̨ A#S` ًԒzJ Wpŭ^׋֋[A o][3>\_elم=,OzQ`r1={Hòrp>dr3CyGz8QK #M»m|w8/,C̍\XBz@Sk/KD>۞c^ѿ?*湂 $C.\\<_D 4@\!I˲)pa{+htdKd{ W p^W-*˺8Yǁ =e/~l\XBzm#+8P?jMGJ0K-؁}McCDD$IF)}Dm&mYaCXJ !`N2ʋy"Y͞,p {I?ѩLkNrp,gsxjE~ ⴑ G2C -2bX)޿n'bI } 982Gj([98DDD,2dPi'^'!Qeb> OGnq*!bx3m.9W.sH'NdIfifDDD~79\Md牋P8GfDD8"/""! WгCL~{SDD86Vt CqL,ۢ[T3ooQ`1/_ߢ2QfBiJzvaǰwoIpC/b|dQN2:`/LXsH2Z$ܯ"sgfyif3wɠ!BTY2^d#gUNDDpe@g ɐ(j!GXb~C'K7'"""1fz_V!I`)pjmr'*{+8D݊Vc.2Gj]&@E#0R^Rn;|A ^*Gp 2͠㜆|lE0CtOs5,"RQx:130ry?V3J}]}tDh1<0H@t3CPX`l!icd*=̤08\/q-$YߚT,74r8 yC=c 5*e Ӎn!aePԋ AOs5 -!FaQ."pzY{.y?bxttArE2zu҆(nfCDD8XwB_ ~={HCtCz/ g;WQƼ#=l KCfTXnHP. rYzՋ.w$Wp=S,z ɡ!pR-t +8>=A ]b8r['!rXc16<=(^XfIF2?esQ^ܐdF LZ-s@yڠsDn^ A ,w$ksl3I!tcŬWr"ih@t3BX&[eb)g2x( L7R9! 1@dJ9P&Ca,a,"p&p~cCDD8DDDDC8"-}ym-*Ee&=;[T6w;/E׷Ds[T m=p0ya ً授$dt&=vљaH2z%g' IF/&spлYlJ(mP\j]cC%y$.PX'Oy n"E%\.S#s-_{{x{{;:: mAA!{gǵb2A~V،a2333Gvkl_K^}_&Z溧rmv{ϜQ4:6NQy׼ys\pq 7XQ2&Tep$6ŀDD'n6QAn+c6gq[=\m6:6mZQQўcӦMstI![d6P]/N_r{d<$ 8JYG9ӵkׅ 9r_ߋxc$tݻ޽{Yg7lqq8¼Y˛xՋ,l {jv^+xx'~igp.V8S8G8 #AF1!1 llbN}!|\;4}GCg@w_? 3o߾cܹM4qqq8d8! aBoPboP{YI-**p`NLЉNFĦ@##)pP`><Xnz 7*8Y88Dp/\ ,ޔ7L;w"-g-]tjţ=VfMZjn( 44h6`K/[T"88G"A]u.2+]?EFE9\ނlCB/$b `QJbP;428 lÃd1!oYr .C4RoQ k) ~a8FMa3c=.''7;;O:999FqqCp\HT2cL qW^ur(Ps!k֬c̙#j%Umڴ)&LD׉'{;6\{ss=ְ&L@]v-mѢWPP'???Y|ժU뮻>u)S=jԨ1ΪRa=zL*ptW^YGaÆ-j.w}w2jlJ3Afg8oJ6clQ9[Ty-*T&d#`8_H8859{T1k֬ J``;3hݺ+PtA*"`(AvqI-G ]N:O?MMiӧOI@6 5Bn8LٳTdKkHv!}@A'2ы8?g-{a#6?d-!Aۥ6QAn+c6,+gyDx`-c?P- Z^W[F#%ܑnu91T5aIbAQ6{mhxСKtͧXl92rݻzjDǥ^?9zҬYDب"58o׮TRds;S0 +U{ia$ ڸiӺu0fѣG^i5zy5`le"p@A k`j֬W}=H7߄}9b~̉ ~{'}q(~@(a_6 q{H=;ſb6 J !„Ğ]&~"(cxߞ(Ai$zk dC-pTQ%U6 @\^"iC=t۳@ЙĿ@ }x!8J垪*rσ8*>@&ȑ?l##Y<>?@ŊT}gS'p؍M7W>!8pS{)'~cNS%=,pqq#;ŹihZ~`{`CAR^bH`#1q=* w Y^ $ P4!؃}&E/K¿"ppc8!C: MvH ,i RI\pc_|1۔7~Ҳ)8QvԫW/-?JR." Ar[q3dq|*.W=pB yAٞ"(a>$HbmQQ#bjNvE-*!q0bA T((yOͰ 2ݲ4s':,%MgϞ,oƴ *WR+VoU\9Ԯdy@` CJzvz@)O" x^8XxQ}+2JyUw""+8z]IquסmdۼIMI =Sq=SC8*l2q_̞:m:&ZYc/]pyGM =zY㩧#p88[T@-*e^E%w@mQQѷ&*LuCwFqQ9|ԆoV8.p⋶^k.o>MÆ Ν+EFo GI*pÏP1 p88EFEFm؃h-]ߌ"\(2eʡ(ΔZuf`OYx?T5PA\,K*t2N Yisq1b(ylctH5kIHʶm.G@OGRW~m8rrrO8q)6XrcG]HȀ骛=R x R\;nHyŧN_$o,2L2vXOEVz&<.*E`@бC$S:ep"gӰ6F^@(2u㤑^ ƍ_V-E+D\3gN6mO֭E?4A!ңG_Is[oS88.p`y p.S=ذ6(5b{RtQDDL\v`O؈s4P=െA!'b98?bLYz(0VvwEqnvY|xax!ԇ+WbM`S3<38A'N%W_nbSѩS' ݻHz̙31%1Ho /_qq/2D"A].2*B #hqbP""EFiZ**Gh8J+R&| |CكL&{8116H`g]w믿b{acVa?a$q  ٳC 1p@!r#"CE`W6X $g裏bЀMAju]W 5kD+׈/lժ"ď>[\88&mbh 05tؿBɨ6n"fjf_}Cn(~sX{AqoRV휜\%BZ'xbn:rj*U}AɎ+&п;q\k,.=YtO<je<2$%B!88d=|z6P%m}QD!8 !884Ml!!!馡SBvu1 t!rD6|p8 (vDGT u2Jq =Rn5Ągso'6< KGYB!AFa;ȨG5nydtăAFvC}r m(g  #{Ȿ%GHEԢ\u [~Bqpto{:ҫfD@P_rkp ޚB 8z-] \I< h ޻hx2rJ"5 @g='0r;Ml@80UrSYY/roTd|4K2mj3 jn*諗 vc~.ЫIh)hh/"H%]1ܛq<ߍBqpä=B0٪E5u=HG,8Bu(P-р!>"0RkGwtF=kqO]TshІ#Ԇ$%P_KE҆ca΀Cя~mu{@,z͒5p/sCӸ5`C@hK[={`foK!88z}Ÿ[iנqnⲈ t 6j0[<17pAG╌vݍjf4qV>)B\imv1e=BgHbcv2iHaޞBvptvp|ց\JK! 2{b;Ȩ߮XDmŭBj*h%p-A(YP@c16W,+7`ޓ6 ~m(${981}8x8b183ΗΥG!K&$ UC6`_G"QibYOy'EWw/B"L-hw۠~ݷv!C`\B!vpܓ7Fk0 /"RY!;>B!88BПC[lBC 0o}u !#*qD僣=OQGTPxDl8b <7uݏ+ėh&! 0;ࠬ@k*ͱ&Eh}Gϋ ! dԣ`;Bſ 2:B- t!uJ>8M}l2/.~&\^,.q3l{7ML0R'UБTD\߈'J,?/8?ɏ ýb/kU٪YϸT@1Lw#ɬ}!ibSS<1I5(R@mTgscWK9t۳{#Ķ ,WMu{r~`Ɣ;h\QԇEFַ4M]9ۀ= |V= &/2!s$͇;}zwWklA6A뫣h^MY+{pek)v oT@QݭF΋ yBw88xjtCXlU^.~(Ǘ#$]G 2chHP tBWsmb6z@ԍJІBonFl0^9ao/\c:bf̖ ȄOqp<S&-~ū;,2l Z^ؠc1Eh5768Th0TyuphN/-POZh0~4;w1RƼHl"+ByH0FtG+BѰ_gMk*P 'Cl(QM;x,:8~._wv6lhHxBGɩE Af,S͕Ql,/rpt)qtBGkoww!Y2Zʵ Z(B88p^k$!sנ Pv-9@èuqD\: U6F,AD)Xk+7jgoOGT0!𣎨lwpOZAC Xj1A-b-nuy H3h1d2!dTǧ ~7jdvDq 2FQHo2'$5zccR V*0ZLF0qV >]ȌӪD6mI4d:BHQ pIYz):,0$j>*ѫjMEl)`F |V|IEj)v}ibKK1H(H{BVר~S<*M,2ib1Qh-Xߊ~(S s]p.mxՒW!4B/)m@foϢc1ol]#ˇ—5ݽEŷnr~Yi!;8nKڄxӏ93YOA_TrpB!Ǿ[ʿڢ.#__߇!B/6lO :#*QD0}{VCņB!B_dzTlHW,MQ d IuiQyA5\:zE=lJE Cga>n@lѠݡB!B&VXtd!Q0^.Ԇ\xoWUD'ASJYl N68\#5]Ϣ24 _&b"|V6j10}_. bx[~[au[&!§Gl6U[^ ERe6E&FTZ#CڣźTi7QEP?huh{_-aCT]).w4JȽ>)5rpOlPA !88B!;8N7 0]1Wݢa[tհ b-88ؘTqU*\EPj ʨ04 zӆB!B^Dכ^k!s^ƶ"x7ɋx_H66LtS uF~h6 X+T=8O4zӜeBtZ Cڠ}BqpB3IQ{AFeg Xmm†c81E2ʇШj 2* |u/dzFZ ڒذ !88B!&#^&7Q \ڀ{:JzXTvXZG' m\0j!T\}qM!B!88t=6Pm}A?AB#Bqp88Ыv|o !8^!#*qD# O`@#*(_?4i_l0‡ōBK[x#~!Bw08FnV# B 2 AF=zTſ 2:A mji!ehLgQ!QˊR % j/B[ 4E˶kͫ驘1a#ȠZM㥻쳮2ķVAm!hL"4 V2 j'蕃4(Z X^MOGNm!9};e za_J|;64d,ևb exel`>PStcV#t !t{tݮ8aUj"z2ۍ„| |Zf088ԇ]LUupF:gF)uӠ3Wg=-S FC Z·E EwuwvPv/02E_U<}=vpS5ޥvE !88> 6644XzNņe@)<9uVS0`wppBW8Gy)؋(AQ r!nѰ2ѱ)zjpp4[( ӍO5"Lg"0Ug!^~Ɔ jw} !B!88ؘP3zq>(΅v@V5 TVl(D  1`EqD85'2d)taO5qxm88px Hsa YP{G;! 2 SQ3B1T+j55Ȩ)7ՠ BvF6h%hDZֆ:k0184*n"gQVScpGLkqFgiS C+!B!w%XOKkPc ~S<*M,2ib1OQ ;'UeP^Vs#)Ծ/hE!88B!Z "F&G!B!% ׏Р߭bC#+3OXBqp0e7!;nud4_%BHT dt{][YyCQ)Q'%Ȩ_u)Z0 eVhMr]EZDYH;"W0dF"0ځEq#Km^=JuIFVBZ./a=@B!YUuR*pNEY5jP:7wpR*=6Jb{vO`vضܥq`$0RSEEEk째i圻t8Y^d??(2(mׯ6@w_g"$C.6B!~! ،;뱙 P?KQo(Vw+-hyA,Mԥ1~!`Qݛ*}{=8JjRA5\ =G6|u{uԅ=P-р>"0Rlpp|w898f)-.oԗ)r]۫4hÆR!Ǘ³ƹPq]WK|sPWyycP A\*rS1B!00Fm}ܴ%uS0M0C*篷 tjpppޛv!rD6||8QGQAg92k"_U!!2#a8JrgyIЯd|!0nn!~hT dTgo>(q%o2*&4QGϹ%i=jŃFpĤhA@A〬"AFoBH }=W8?/janX5ozӖ#p t>vhP(Lw~JF>gV!](VFLuR*Cv hBPjP']g9͓L#-=,gFnmXǁqԤ}OY*:A9pyXJešW2h- e%L߱ß?Bc1y*C zD0_bbqcݒӽL[VWƏ`6B3z *vhj1'Ɔř7!C{tP'op)Fse>>!:2 e腑>1QF88ՠ6GP.@y3>?X!!!_*Y+u?~nfJ {`kѥEpS jnEm_ȒY)7 XBnŹK8z(էР@^PueV;qD h80򽎨{ J#6 +S4 n}Q5Z`^3OoB\R_NZt6Pʈ`h=fGx+w ^i=x#hw 3< AF7y *%NdTTSAdTa dd-aX;5pIJ>4hLaUЛLo/ƸcU ' ptpIYzOjAOLZgAGl/t|;,(L{\sW=JٛYwH^XTѰ&uT)&&YT|-|6B?йagU}^40͠Bqpx\72&Y1r} Ӓ4T%j}_lgQ1m YuV͙ !q[^(~p;ieZW:k!88B!#T%5wC!B!䈊⏇}ue9TiGTPxDH- ;aDnP%Gϝ.w1A|B#B 2 AF5 -YL55F 2J^$cp`f8Qo?vՑ8 g4!B!oc8XTg V DFRBZbU|( ibq`fZRMbX$7NMc@WQ5 adrHM3SՆB!B׊焚!Ы^7qKSTå Y{L-X"71(0 S{98M,40jOfE.{saëQ?kJ&!BsUh 商6"&oׂjaalP80 j'S0 hI jE4PЫFj;a!G!rDQuƤ#(1ՠV^ i6\Fm`EqD5t󪀾ҧȇ< }Tsq^9m Gk+LCB#B 2{b;ȨXm hRF5 AF5È=ALʇ;8|T=A;x1F_F zGikwPhgj_/!w黎@FUp`no=N5aibYOyAjMue@3"\ [tOxQ3~ !88B!oXyX/(&+pjBqpB!{p-tSB!xQ,B4ui&2͊Akan(x# ],1R4#}BqpXON?@]?Z<g##L>$~BAnWnM}`XLvF,s]y71(]rU]mFZM^GAeUЦV q=Yu/ gu{jH pz# g< Z\O!3Cx;8TϽ_eaw%BJ<5{ef64(4F@f N",jsoհ C j';8pu;;hSQu;k԰nYF,6Ԙ@C)T08&RI->B\ TL'`Fo3axKB!¾6(B_# T*(%ՠ8Uװ}CYh XmGTxֈ:#bnm`AdG1<渐;P z˱6;8*ppR/t? w͕Y#Bs}(=f,KQx@!𭂌2nKd[@z6 >(n2jCTZz7}+KI bc#*9nl?Ah|QCeփ~]BHQyrBb3،!?.B!|4%ށmzB!p٨&<-M,2>McMW FvAm*G`olX5c=MVib}Bqpvc0͍,*x'B!Wxfo=!7E_hنw/~B!B#qp-t VB#B9;GT gX>Xf#*(_6632|,W j9]{\݉!W:fc4OOo9]_ KqpB!]A8 h4uY+Uå@D탯(4'NcyvONM&m iq9jRj,*gRA󏐢o%.ˇZ1MR/IWYTGLmh;iˆ"7Ҡ?qˍ[#Ah!B!88zu4 * g/0P՛8)] {ϥZ &P4"yG4#uy!Md_C{aimjՀۓUh_Wg70~z/+"G!`=`[{(z:mhP^i(+R~>X~9޷P~jppplPGi 4(siɪ[o6nYF,6T]*cPC)U߅y> *qJ:!B,kw/!4&*/PbAQ l+0DcjӼ8;wtq;8tk Z&;800Dž^}0EVd}dž7zaȥAmѣ P#H !88B! 2{b;Ȩ_]x5Rz6 >(n2jCTZzW3]zFs.%6hB;Ԃz?Uo`jY2!B!wrYOK& 9zj"&‹MW N吴Ee|P!Xؠ-&V&VK)G!%Wxfo=!7E_hنwBqpB!6"n;!t !rD6\#ϰ|_iGTP~DwnեU7EE rxʋ) Bqp0nw]~s !dzT#Yjp>h6!ȨkȗdoISa$*𭨫Wu,v6E rZĢ_ !88SϓdpQ>F;Q+EpK>>I)oM!n)8gdQ]W%op)Pj;&d*"J?8$ i,ЩIĶ $ =#GbQ]#/ fθi?u8YQFG3h~ "v|4Q)DNB!=z:="saCCLg}]w;њг-F(F1M=i5dB!o`Q]9`״7}z<},Gۭ@-ta7bQᬡźTGk1[~ 8wfJ| \ðAK-"=$5rFa8Ω^n !q\#BU=%64ԷZY_g΃]ʱEu>FFҥ^ LJ9FL! ;8o7l+`ExD商蕊dʂZoLaCZP˯:8!'% 5[k*"E2~?ؠިF!88!׃]vp y0X(U}G~a{I9aKqpB ^𦚑T4Z xoC_`IEqD :8uRsp; OG j-B̈́3MsBvp0"B040C@Uvp *tB!dvQT5\P#ujب& |!(FjTP;8Cf?-ݣj(%:"0u%Zr uA! e!پePsQ21 tp!OrYOK2R<XSMxXXdĪVD6".먇}TLkSL!df0dj+\2g):")zCGydq-{l!3I+I]ZDފBYTߥ f E}=B!(fLq4^B/7ix>_B! lQ(qDe9XiGTP~Dų؛u!&`N2CehRx1h![$ CkEAy0cA:Lt+^}wp ]Cc(WRQ%Rm(-Bo9d1" *PK߿17FV;4!!cD`L>k(w0|!d1r;ȨtHB٨& |!ȨƞШ A7AF"**ߡzGMij0 ZMZøt;JZhg~wO h01eѰA!+2(!p G!KIJq+Ml;Y`/΋8;ՄR\X_hCAlDkaҬF⋾A4"u)C"B@ P~t_͠&p!88DB!88Bg̶&}|ܯl!t !~8b5QA"ma=#NǹG`7!B#B 2 AF= h t,M55 2JЯJ1:)AFfIcg6K@CrHXc|I \0{ ~rh5Hu5+w5 G4n!B!|c\Z gx^ ŠI==RԒMeJO-ti$f1FbSĶ WMl& hS%1aZï~uWibEccc_9aÎ}E}xס~Z/7m=%J #(GO$88B!G/zvET/QP՛8) * MǗS ]+; E#+wp| fHup>]Z$5{ \ge|Il/ڡ04QXF^ƝbwpYM5_ɬ9B~H_ d~(!B!88zr"aV^#W #O*Ӌu 4jkE^eP=jpp`6[<16ZgWQC5YV\eUOEi_pOWwߊSӚ!O4+FU}_֨,B#B¾BC"0^E0AP l+0# Dcd g7T@7&Yq`QRAg$FFwpZ4M<88d!Fڹ0|jڛ}0j%+FAYB!BAFYl@UQH=d7H v]cc ́ lg tswV oHܥ?AA~qpG!&vdJ[N[7Ezj"&ŨZW9d~Qos^Ð1F%Lhz5G!CW8_*K'c8~(!!B!<ͱ_oWKBqp c018UȓrVn\U)dPWJ/*{H4BqpX#A~:P0FP]B!k5<9;YTg P vd|{;8(lCˏsU. s;Ml@:80r%.WH5R=",fQsW4BdAҧ櫩}O_FFVWMԟ!88t,r z +!X۠C6x?B! ^ ]@} NT&zij0Wy/pޟK?hFоF@CW1(]UupFhĎjJkT8`FW؀@_M{0745J>BC A44_G>Ͱa c("BxWG/[$ zC5jPf Z`e,Ki(C5l888bxc ?AXLҘi-'L; ;q.gaoLl &T F5\NnV&eb  At? a8{v8p0L,9h ZDDO80}86w|}QQࠢtf2.(pp=WTvnWT6H_F:/y~vO\(pIFqz Ct1= w_d }҆}C$ R2(JEm,!PK&CI:S碳Ā{/z8?f "!"""o[  X/-7յ~,/[r;jmۿZI?H ,Oy\&cH Y~GAߗ$DzHu!pEԯaRc${LCrFbE=݌AxR%A5Q̺k4֏}W XpF*vO/:;h@e;](졶D:8F>;"0`q hĦ Tsؚ9w$(b O YwDb1Ȳ^C;m䕇b1H<\'w/ ò,1P&:y޻ 1[Q4@g2CJJE*E Υ >'UDDCDDD8rc_mrCN<-ϓ$D(3z e.p +E҅/"p\\x<+*U l",ofХw|'^0Ql܆'3w,; {l|CTYu I25x+๛ M}6֏CB _{(g[Fn Կ;jjMl?s6QF6h^""" \w<+/t%Y+APSxxMW8 y pe lo.p<ߓGiMJ(A\5N}x61~3 2[EDT#%(pKh+89l[|o,"p]0Vp3@jFd$=$XV@<0(Oad+x_8 r@~D˲YnC_Q"Gjn\э lYjmGϏU,plȗmxe} i'P\CDD%-n|K=(ﺗdKR}1d4ߐdyZIFm2Bf!m&NFO_ec]^8b ?[ ߘ#MnƦ+p!GHC5O_ MOlۘ?GDD2u`;1aOX"k v!ܽLllMxbta9-ƫK KOUT q?J6 r O+?;< GMLM)"""o?GY`j?QëgpsC8,k_s>F"?~9I8;\x>݋_Q  4=,Mp_Qa+*7SB ?oJ.5$8!@m$˒(g!" wǯI7Ls1b46WV""r$!{dt14&2\uh3$ IF s9$w͞0%D~""qגmqgSsXHA!,9DX6E&RET?,O2=ڕj# ǕMyhNjԯOcXb3ǂk6"b9`7Ib(Z밤mZYFV[EwF pDD#Zk_諸q]ħNlgNs'z/"Lsb&JL5 )""$pdb`֙ecpOdHi6l]q^>;" 2Gc c9'_ih\؍srHM29mƊ>~^ec]^8b ?i&̀ 㜤A3N'ϗ˛:'ED߽?h63xɱ|^5^ ?a9X&Jƀr$X4'"r2u`Я/r^ W kCaY&z؄w+F7!Ӳ.nJİKO l$ٻvqR;g/{""ݿc{밄3_Oagg¶Hr% ?>|pQqmpoj<50'DD8D+; AQ#|= "+*~\xwc&ί0 ""!"""& 4`DVL;=,MZ&MoH2y]KTD~i""!"""e+[KaPX/m'5P^4M# B<C;e U`YrLeb{Rx^ګ꧸oeb/!-AOBDDCDDD8e."G vdKDEȯߎC#JlRMe/TEzQM,CM?HH7"2zrU HE8C FLDD8DDDy$K0W ~)3<ၫY@e9 ޖ޷S"Fw09 ╇g#󕈈(pG,Ռ oay%aTϲMQo dxF% ,8xxdtI-1j=E uկ |DDDCDDD_+]a %^õ txbw.LϭrPf%K]8P8mwqu ߇RQR}Q'oEDD|E%rO.{IF Ctp=biI%/޺D {b%hoH2.$=3|FKA19eY&o xg+1 ŞjQϛ_)VEe 'KNUXUHh;*Q'ٹC(Y r'&DzxH/F A4>PIl?&8Lk# UTr,/ׯ{E8|8 Ú aVc/A7?W_UϜbFU1/X%""" OtMYY0A?,-ab\mlL.C#Jl/eŁH/곣](rE{)p8x'c-{^[Qqr4/ ~k&^v&=GDDsq}0f//z"""oc 'yM8iCƛ2x36?t }o )p,+`Cm82V!6FX4kHE/8ԛ`/Mnw;LMf*JXЁW3tғHą< 2agFRr@ ):tg1__E%98_D&0ѭr<E bts?'ODf bsw$ew\$I]_M2'Ej dFs[##9X9%$6G"[QLPQ$`7!"ߍIF 3i6\/}(=DDDZ:0蜑%^õ txbw.L쭫 KO9"" """! ~8+z2"" """+*M\xwc&_Qa+*wp䅻-5M1hs'] " "f=,M&m7$eLϙ$nvb7U}\N' z\W2A#(pleb{J 4ώZ);)6 i,x؉׿H˲o~ߟ}'qT?}<cmDCaRޖߺ zRTRi_d`ޑ 0GR&cɒ7,""!"""r"p䦺Ǟ]hPoYg7B1*V\Iaؤ.}"ώ&vƳ8d1+! H~,,sNa8I{Šx-'L3t!"!"""r.pdAߜl1N'Y 2VӀ7e2<~m"PБWux>Ju*4#\$ݠ6 TD8DDDD~gQeɘd!sIt rB{ &7$eZfFsggK\7Iխb,9eFOD8`$ADDDCDDD䯕ͮ9n +Vp-/txbw.eb}HlOpv?E!cc8݀=Q:Ņ;k (p H/ %ݠ"" :0ȳiOFDD|E%\=\-c +*8;rWT+*7$}mb @b?nnLED82#$wQsDDDL2:`ΑmaȞd4MKtsO2~Jn&mspt]Xdzґs^&㰜â%%]ELJFf|v wh@>S+> 3;[,C\Ѐ}6)?e,CWɳc(` ' zcvi8uIde[Ǎkpnj#6I < cXdyVXp͌d6,#](!GRȚ"p0B =\C}DnMd xY ]`0fucDo@0„~155a_>8+8]WwU˃~V8,6DY=hIIjjCNUMV$#o󀈈#7ڮwj4Ȯ`f|,{]+8Fp }9ǽLi0f(pD79wq!ϙ=!C<\8d8An3Cs4GmWTv#`] 8r{yʍHY]1?GgǨy{FK hq,1 +89~ IF)O&jI%0lk!¾fuflڀNA"oH2q`\!1hmDHk} h{#s l)|{98F <ޑf7;b[H|˳gF0AFl㋇V+gB&9\IfWsLl; > = pc7Zw8Eeb\E]H^?-Ygrnᡧf ư"" ] '8=8r/}]ԫ0Zy5jWƀko&?A`DDDC1QH+cu(?,ĔFrcuMD|E쯇οC䢇v&WTvS`|: 2vq8h?Q8 Gs`Yf;oKđdm oEo'={b؜1$ '|< ೥dBM (wI%x2%!zA(AlLd3 iba ^9@U*qݖqq(pp& 5ʐske-_8-I0Qn K7afLK'|Ҡ oymuFk6)_1 "o6A%)E__K(heغ\9I&Pj#M!%-&:gy"M4^t0Vϱ**JRɥK +\1L1I[ƗPnJ&{qǜI1ٜۥ]$s|?qg0&U.#[Jׯ}K'FCd/+2뗵5'h\\z]MçtE4{? ÕKP(p۪)w[uTnjky 1W< _i =cJo"I G.#sd|?jT4o $$ -^=I;jQĖ^dg!")xf4ȕU qQgx;A&rtωr0IC}E E*b \Eq>E}xٓsV+{? nƀ&xDD2WĞHb蹧= >-ɛ2W<@rSp pd[xPwr?iA>Qb@=d8DD8/K%2I){NMz$aIt1p>!p2n1z \q Y# gaCD8"(m|/B .= Ѷ06G!L~QqGi829.#' =d/uE蓼_8b79Ꟈ.sA_Ns͉='M S{h~xuvu8ǯ,cG. <^g*ug<1lMFCd$y x4`x@&T,DoH2^ odK A/2)O`!f iCf $E/8?_1(r `Av^4Ls'ʽ =K/^O&S/IΒfUȞ7~9f3yaS6_s/= ebcvV&6o9 2en[ݍm޶L,ZYb$^0`ɒg3Xc~ގW=`G#"bާ^d"\2{e Eebd5b~ɽUTޏmafkiqW'oIz""" """! jEiLDDCDDD;v/\bX(rWT+*;llIU 1|(pIFs<\Mİ,[$Kę$spCK>ޭ3*1?5$ x8Đ Ā !x{(ppTbWJS =&a)|x"pdAߜlyzz{| ϻÕu(㞙 +.xn@p,Qk z EhXjo"w_l&AcH+ I$ccxu,#QvP#qzB< j/=zȲR8XDāA"kE|< :q "-+8 $4vV 侂xH$NTDDCDDD'LxSc\A2uRMc ǓmL2Zǿ(K `0ElhA€Anc"nv,2xEe/ mz1*n5\b{7VebV&6g ā oF>3{!,1 @jo>AuOoB(p(pܕtd[y(pd}0_Q9xfzݛrE۾W^Qka3:rrL7+,)3glz0鈈%O) 1_%IFs\,MȞ*L Q$LoH2$b/7ռfjRlLrq9a`M>Tzkadx8?"xN_(3'3z M rvUC ܅^?Q$]iK9 <ÌzMyX[>xQf.Q/{"a>ޗ.yNDD82I]Jm/Ϻ3G}O:vYfn!z8x [`7C.&r56pXܯ:1^82y QrF%A䪾,C/]) ,jTC !/p\s~Ͷك\@_bwuMOMtDDc"dٿ\,Ivl(,a\ՌZK>axCDD8ocgcaB&ǀBypeJ.G:Wp<7q pzYAc, E#kWib= x@tW]x\&Fr?^ˁ܃wLsuiٲ^7Hk2Vk嚤~K^z%'Kd3Pͮ&c`{NjWŵ7]=0> |e88a!%;rS]M?Эnb7MƳNG \҃%(8U~FuR`B8}6 c0X&M&c̆ހ&Bi[ 8Y{V"Ij'_%:{ ESѥ:Xh? Cd|P>tsYM쨔9JIǘcp]/m l?r/~SmE<_bC^2H n$=gy"v ë꧸\&ЀGHEY·MCKT%z #콈e _rJ'勵uk"Y/'_BQRsk#a_f~ %B4Qtꗵ+=mCZ݋CtH(YGDD8b8zOA;_;v{ŝaym"I.&ٝsVf/ZwH *{D<\8l<{4(p3#MD=TD T+8g!:Nl?BqrN lNڌދM61<DV#"YĈ[zG}{,i~Aވ]= =7=?,V6=;M%G$zh&2QfW2„̩}e5}Itx %my ;B$/Rz@G^XxP藇dOF=_7&EŢDh"ҋZ6Q,t^rXD8~; V=7 ?ghS ʈd̪rG?40<zಂvJ}`Ga[~QDF98_Dุ x |PDgG*fD^ëW&H`rXD>GƳ#sJ&e}-sq|/_RK#+8DD$(pIFO3CbdAS88h6!(3G$Ȭ઄ 9 0{ Jf6̑q@ڄ``M(pܟ,3HP=OnK$""" q/s7 T}:%y(|!a$""+*!Wޛ+*; 1G^Q9bfO^Kz (p?JY7^}^H|-$ߙdtO䚹gn\ٓYS!N؋$mV!b{MΨbWmz:IϏB+M(x̃g9U}Ab¬\\ {oz^zq+^0 sdtΚwnr%[pz 6+'b!E+e[bxu9:v ëeWQI735P >GjjHF-3Q"~DG(S#Mu=xd ?1}]9%M)}aj&/{+4I5‘P-7?ZIґY3>hBDD. Ifٖ9j;~9`v/%9C)6J 29Ϩ2@$3 =?%apL4 9MƐ#"A{Ap1bbT{0YC 1LWc ϼ&Pup,F h^^0g枘qaBz7DzVsd<,L*Wo'48 T ,Nl@Bek^\u4q1Q8Ezqx~E%BO05jQf0F/~VF1spdgwAfrj&k*Aokxuǂ1 x%W}WluWd`n^7(p $2eö|#1vr&pPx~(viC>P+8"x j+K(#X\\^0<+n(p`l+8~%(`dp2=ZIq}GA{1ࢅE$E,m&֋ "bQJ8 (-L k%=U*` l,h6!(䋠L28 !c4"Ds^Ħ\-b6F2D{ -/'•WG7C<6BDD ir{دI砢^d98%o7~c~#Ƈ`)OnЄ'GSDDC860;[Ӓ&DD8~!MJ&bC~K: nJ_El,7<:ȹ %1H{&Y [IFõ$4x L - e8hH}CQ&b,&,Clo6殏Po, &?^ B-'s͞Q9GANuv>ב]Ƣ/Ц#QH F1 ˦;vv9Y2dm'_\.qjTah0D~97AST4 @JBaŒ}&(6ׯcS#"!""" U]eV2(w}O`m`; v$y[h!1M^@x4(p3#M<8YBE"aWCt>R9$[NڌދW?#8ȹe];f{.0ЀHL ]("/"p\\wxx,pT{4&+8 .TI7#=pT= 61< c!"!"""rdw'@tpdtI$$ '^& x=xCD8dzQ:ʞMd#NF 6"" """";Llv()E pLl2tPwK7&2 l{a{:6^,B&q` Y)"!""" q=]kʂۄ(pǭIw>'{}@c!"E~i>No=,`G_Q9b:zߝ (piD`mED.""bQ{σi;lzʽC>Y6+ߐd0s2=+2’ 3ؕz;IsɏP `n2 ^"" ).W~6IS0 pr$""ѓhrǻh4KB_6]zѽ)vz>lڸkj$jl<eb;N83^E%8@ׯQ%#ϨԤ - ?2&bUat1̊\Imb=Ms1'<8pQȍ\d̲1g˾̘y F:[NǼTW1tO8do+GxqNG \҃ܣ1RVarF ˁjqYHy =pGSkE1t\,vAat1hfCDD`.$NRbN8N/DDD802#Hb Y/ E<-—qxyS[8Wp#,K<(p=ȟvIDDCDDDIFs: {̓\Ylb,CၹT&m7$};3@QY'ؤ>Bi0xA'k/ ErcF "!"""2Ow놢ky]c[T[GCL rPwjBDDdd8 ODDWTF `s~wދJ_ELÂfXuC"" >3g# 22ߋ"='سo2 )<xIFӑxY6+ߐdUIFYqHQŮ h׳~NKH6&c%"i,d#3H~m o<ꍞ z? Rg̤c aU?S/3#ud߆B_t:ƮIxI]+ <=^t2H n$=gy"Ħ"oL z _rFdԢ> R'XLebUat1̊G҆(3Ev |'v" d d}axhe6z&7d$hz,tjodmf"""ѓhr\k˵+ގÒl^|[ ؋j9MpE(졶Db5ǗE*c(9Qa*ׂDʋPiEN2[Qb DS`lbxd 9Q( ٚxOd=AgbA,=#&N{#2h=1\tm{3ڠqXf?Yހٸ岂~Ԑ{Ä͞aƐy^ơKɾMyeYA#x%0||i&c98 L^lby9\z` 涜CX7q2le-{XDȮe]G2{ KN:blbx`Tx;(tB +Yu֋g7Ϟhzmg b,""86_s/>҄Do.v&DD8M܍2Fˆ-Q,n"l-q>(p` bσgS??xn_|/a8ED|E%q@}a1+*q~J|6ĞOdx!""hl)G'MyA7:n JD${zο}3I%w!'݉ œ39K2ēߐdUIFgClbS `nEi[ :Y{V"Ij`Ѵ#f4, ̃REg5>F yDXyv=0 .N͗t>R*c.)qCi1tM\\PN:#gx<Dz)c6yëY62Gu< k;A&WO1O_.q!ЀG:g>d=%PzE<,]Ŧʫ]` 7`/"yYK_H? eccs >|'m0Y M cvz k>Yn\z X=}|4O3\}C٤ |ߑ}%6#p ? q׍IMtOëY6{D<\8lWz/}:x&p b04v>χ8Z0\Y28xڈ3lbx8#8- `EE{0` zMyspa)ǁ5{1$W:<+'|}\/^"i80DX2aB\f]Ø~aq8 ^ +8āKeYxy}g<0Hp~cnRd^,:NorN o{]&倎؋UQ͝`> ΃&xU,˿&A+CF=2/bL^XdI!""Y߂-p.6-VoF 6G@6^ҵ]sO;+s-/>\bM$Kز \Ŧoz$qT'ezfA S$Nj8aR FDo$pGn&LsSe;k;w}˝V۲:ص98u1 6"" """D0<, 2yMΜCCɒ!$+A}42Vד2{?>Fݬ_Wd/ %*{yF2Pr{ sI^>kIFED,+"""eŠ0.+8 a< gI }IJ6o)<#8Fr/B؋q  f*3aln5kx\<~?)rGM֙3QT [_Қ\G~rT85,?;[*iՍB@N p p p88@88@8nYIENDB`tofi-0.9.1/screenshot_soy_milk.png000066400000000000000000000764161441474151400172760ustar00rootroot00000000000000PNG  IHDR @`d|IDATx1A(İEs?%w@siǟ9|m*U^mkemg[3t'PX;=ݓjʚ9:{"9tyolj%.;u^TeRcDaXz4׈J$G]mi.VMDLS7ᜉNsAaa&ZRdn^EHFGn)ǫY-2\o0y5Yʶ~#M3u^ 0a`jpQ9\={~͇7\'& LnP 0ڹU.]dՓK& e*"== fRL=[,UZrT/Sţ%\ؒwn)^x#ù0@1(`3lgm^&0SON 9Եm'[LJT+ ϭ,Ԅ"Wl{G'S ?ZJ@P,>^hih @ݠ$0PM[֑n0AP;wqF&K|K51˪K Yj.$[NlƁOL=T*^/yB 0mnH);dBN[ٚWPO;0{<#',Dqݹɡv?tcGvgDP@#k^7L\vO&!/K65)50 0cv'e,&kpMkQ\oFk\}3`a tb8N|džc;[;?{2#LP;hy`6l$F#ziP.S9 8,= D) `bu0\ْRм߹ڙr(oҽSeRvF_}Ch}R ׽dyO"@7(2\ݖ0m99%̤7R7O8XhqO,oT60(' Q߶FlӢ^fqmIIޒ \[Pw>^J~3v6Sd^`6mclK[V SMݏ:)^͕\_95WTMso$r|j?uh00@1Lra"f&΁gk<>[KX5y$?:jJ:8ؑ2ί.!;LnP px'`$-Y)N];R" "ϥ. wyw@_&9H6ԫ=}/Ef{)+qөEpjVFM)Ng|j;?hG&>em0ցN. F],`}Y5.u.m@Sd^GJf])N>h&K%R 6#l%u)BZ);g DP<0GG|?J)LhiDP<0ϬG`僡AOWǧ+zK]гP2'mfCrab (a tb#Rj{rkm8~=;d2Vv۟m˅׶m6O;gڤi2o߲?t3_3?9ѿ?X+.ͦPzv )XKrO6F9'VjRa,%l;Fo{awvWq|&~,Z  /|s'RcɌL!lֻYo~͵_в&ʂ14O[c c:a1AQ =%"w棸[ 1[[Sv,yF1xglO$\B4~?h6,u`m#0TmcI3I4? Xt5!:I%L7+`lqYUH!ފhf#/g50V1vc cÀ1Ʈ3J#3YT֟GTVUJ0zi}y|Ԡy#ٗ|j_J$)kꬿ~Gj ޸}]cA0RCp98sycKj[ߔ$.%"Ӯ +W݃E(`qwp9}v˿ÄE44y_!,p!?Xy{W"֬y`(V3SVYj Ӷ(h7x&tRL6[hin^ձl}KU뾞=Hk&~yGW:` h}]7[Q%`M¾X"fT]'l6u=lld24]1W٫;Z[֮\鰈%*5惓58VVXU ̕c#5,s*L<3*W(ʦ&϶r(,QX kbfI8?ڡy,=~B0_ byp& XZKc:u]a}gRX3 F@w?(@Yq/j,*UB(Ǿb0eY?Դ҄cp`==ؤ7KygM*YZVy љS݃=8aurÖ{4T cܗ7ato̩ܺMdQ 27𗿼ExL+SޠGǺ&I!5}w[n0jFc]Z*}omY8}04? m8m``29?㎥%Hm7n|ϺZSޮ3b0;{˦ma8 8 *3܄oV;DoZ=i^\a)( AN=WUBJ9~gm}#i",AtΟ.'(扠Q-#MGY B@G]Hd0Fd֤D^ױ8i(tMuI(c _8{;K_x/?'O 4(L`,L a1cv|YndyڌcxיSDpEOxϜ<=#)izp7ނ(Gq{,|0Q#<9wC<-v|Q!aPϙUbj;͜8%@(Ld5}As1@hPnX 2wB1:*1c1d(LOf Kj{jم1u,Zv*t5J8@"0՟9I('(C+%M ?/76E0OEcO% -vukw(zΗ0`*?ܡL44Pй$Zp`HRG)(>}"= cnW_8 ǘc3(3aU*1c1FON1Ѹ'W01-|,d9*y5 D2„!0WGXHTʋ x+%Z0Q:&*{c Xr{tH[d&%]rcb=^ & +Kc |mJMĖ&K#*1c1Ƙk|^0(95؝ B9Pg2F;(LV4*+ƘxaiN ?8x]fvh@(D|Wo3K퍨i%'c:ʄ<@YߠO%Aϕ? c ?۟?{ +Kcf\wRq,;C Uv4C)I-P@%1c1ʍ^occ aYb7Z& yDQZp'FQ#3Phh4cc:l1P9V68go& I#o݌ׄF}Y&"LB6Wkfs2?yq"--f4Эm+bYLmqY-гѰwdڛ%BAz=cn:Jun[Xoz6 %% "7vnq;enp1fdvA2JL ނd$LXof_mZc1c]qe?wH̲ر؉ M]ˌ[z}ŔҦMh0;f&bgƉ빎l٪Y3sA:g4W#=RRpd'c AEw]A\iuǠ6*^\v#TE[JF}v1%u\ +ˠBj-0vs{ LDl]XX  T*`!`Ptٵo\QWP_kg2%|p4K8&1(Clq+4\f+X`?#CQ %!O3_TJLJKbGbRQŗ8La+MpRN*qhPoLQ,jvTx4,H0" I0ey $ I$_H<՟xv kD8F%8gJ6i[HI*jCHO ߟ?}|X"Xh`hŖ͍OuA^   Z4wx 7kizٱPQ=_hifm~Mͫ Eqmo`5fB&[+֭(70,qEZGJMm\@e>2+.wmx#8&qyYJ-*"Uؐ%PW|Xڟ"w &`@eM?˔Ӓ~z)%|ȊmYS[@\ n+ s1b֯zc nӐ2g(! 5%Fz|lKIYy^0P옅-9]d~T8"LBk|Z@"lGlM% '^Oy,"d3*[o.3KK`:50󵷚) Fό1) Luz.76*N JIYDQyzʮs _?ܼd|]kc;\20.x$mOp_pV[AA$߅0K✆HP"Q${Yqj#\FvKssdogL\z1JSWBEfM~P#EhdhG)hˢ*{jv;1oG!q֔YvT٭0aMpAHUd1KLnd8:202ra Vgi~~MiA鐣}m#@,6[Qn^u"lo;=ܫ\1ܼ%Zc=g;=J1\UKs76Dx(Z7.-/S0 )|;I$$Qh)(,]US5nwW@4"QZYRpAgTkO"Vpi܋2>t™pLսW^O Ad %j UIJua֦ 0/N <s'`:lejnB8)=B_gȱ| O+*\62 8zD'"]`XS7Q:d ۛ<OeABݒM5҄Q10E ]Uf HK$pT7va `vƢXE|{O4N3e9\qԟ#B@pϕ,PTA~Y&@'e_q'AYi,,R@ =~c0qaDLܢ6)2qAB^gteTWg/es|G'Lʀ(N۳\cniKź=n+6b0 ĿL ({3% W2DūiہFNtwzÁDɒ_]VPh7q{xO6{a~l`.Q)Ρ/ᐐehV tv#Z#8KV-ZYU_5ָ.o&\~a}0:x'$SٟY` ,bwӤ7~͋ʹ^7HrcE#++ FEEϏk yR\A?| *6S.O I--"Uz8:>scx4 9F,Z[`6)|L4>w!pĘM*RRVcZAeCu %N@\Q]}MS/ntiИ1NzD ELL5Z- `fl$@2P vH>9_>Aq_3`vwn3k.ɿ:o&-1ު1O˭|H o4藀t'a13D \ ҂ߞ=E\2(67tX rV>Ǻwc\|3n_T`"/w}}[ҡaRi;J )vMiE>4CыZ!%H U^mc.̬,dc^a/-/4 CaS"_8FaR@#~MuyRBHXX9{^c]5 MHk+Z|qC0?ٴL`Bp,T4`otB,XvKKX ~}E{d(Ygkuc&+җ>pGC)>S,\cYբ6$Bӄ>>mqn,)B!;C1C8< H'72#Y[J!6yIKGql]amgT)*~Z7X\b{L O0gn7.6SFz[na\[~Wބ8xhe^0nO^u{'$9ctK!䏟$+r&jbp1̈`Ԇ {v(誸yuMW T]7ݶ u~.mɃ,+@ QPY_I];<ܿ@=ʁGOt?.Be?Pْ8=V!MΒGY!~VV JWTt\ uuU(Eil4l)r@dSXy7bQm'V-+Ԅ/'>\A_y;}UP~?B?>7@ G.D+G%Ѭ-Aen :N?T O :scˋeY3JoN5{uB~jJn 7u} #90:X׌/]oDF'NT.εz{ mF$wV,X6KX?v;F8?n{RۻݧoOgɌhۆ%uVʺx4ZS H}~[$ȏ/z3U@ *pre8,O7Ցlw( '[$+0F>>" GLB:؁8͡@lWcokbG|+n)>1! ^C*DW8a'no| |&$|R|9WbhMl؋E&-Ɛ*L}sQ)5W#{9XAdTgW8!0wG'Nz!511q0a/(ě0GCpc}/u(Փ`,_}OJyg}/j9{OEe'@W_tJ Ϟ^d^͑qϑa>pvo|g@L)DtU?D{pBqPېol0%ꉦ4+Fۆ>a~Ӗڿ4 E]tS7l/e E\_˹<}+b6Xo.t  R:$f. ŻDsBRXU u)BvP픫Ut= '}ϵ( cT|;p۱}f_i\JsGL09-9|ܜ9-B׋hYڪCWRbos2 dKRrX֬)5UZLi`YB}uDKM5z&˶l q%b%K'6) eny+"?C# $4Me(>z~ L^+% 7n]1 &#g91ɮo>^zGڎ>uûF= =Ѷ 0Lu]lDG }@ĶH$f\ذ U,|'^vw4<J`ܐepA Hd!J)!e?o>䩂ӕ;ؾ3'^A3|s~QkTEN,aH7}JӒB,!C@(NϡĿDtdB'=wfK.988I`9y7!sh>Cv5fDOHQ4T4+@S?>S/$^#ӱ^DX~~n?<7'ܙ3,"$ebD1huXIah2ξRh2'@Agem!]mz9+*ѦM m@- @- f%qZHcpϷ3\ P@-TOB&ot,=ݛxFY %'GF#&25.Lj徤&!Ovb 5R\"HaH9Fؙ$DII0 po1T7:+xlComry_{n8<88(4ijsYrQHˇR:ZLp-,x8Ko f½wY$Gltl#= (y 1KjY<:PGHsVKt4~~s:2X8y|g#q ʳD]¸C̻ED?]H-tPǡtu4LSSs52Tf@W$mL<C% Cpw4SX"gu0dzN\_{|:#L1iCMT$ה]C}T<eVj@f{B/.f7/W7u $$$4‡}3_#L WL~ c C0!tHؼCJ髽?<| VkԻj4(5m.Xrwb6LXdg0P 0P YIxM^=QSd)6-,0L+/@ށC &E,(}cB<Í֏qf1s-Eɝa '}Ӕ#^L4>9|΃LLl&ƀ/,;\> T6 ħWgYvY;¼=4E(bU0;pA܃c珝1Ѹ۴9q< S p|W/Q,ADL#DXƼ1V/FcdŃ:Ŭ330[0;o/^!a (e?1eB v> 7:yE,6i‰#U{&(T^r{NM\Y8$̑t>[q|/Ԩ. y-ت}l 4?9;x rʫ ,hxKX,4pΗ= FGL5{:uK3?7nq)#U۰ %-([]G0=*0F- fZ,$1x}ysk۴^Εr%s@-T^b IW>:pÑ'25.ٺ 7x79B87HXʉ&pEƈNǚoq|n&Gʏf/1mm̶EQ@bt]M0k$l %p")>r>|W3wo}`Uu]ʥpd*\}:~aBe20Fo0#p N]w=W L+yMZP;5|_ |ߏCLwo(Rh\na@Qx87<i{/~o%z˻ۚ§^@5F$5E{6E%;}~uwoIY`?r0!݂BQ(qc:elpc !ssPs'_GaVwUEh->uۀptw\3K;4 l0Oa]j?um1'O?fR2ۛ_}$!s ʮÝM0A(JSɿ:=azp"ƨ,P 0ĭFh+7~*EL!DZ%/(zHE{"32.sw@iă'mK0ļD[@n0ck7| ؗl'Z%Nsb[ cͿ\)s|^D5 YG,QޅAaTh/((DGKfowߢ@֪5@0 YF(OH\2I`_zTo {Y[4"UbǛ[{#An%sH閑s8`ϙirXRq70A1w"#y c-z:N1.F߸x~4ʰ&JכK^tbJ9 JZg%A ɳVdsjʭTҗh2GV e 6iWYXޡOzdG^0VVEg*44?χld7fdzN(GgOzpu@F0z''vx5N H㺣@͗ci^yύy:9Np, ƨE `>9-h:l9}rv,P 0=ʕPwh 0cdqGa2&%O"\7!T`(}H{-a'߅|'0?8LtpKt.KԸ9)D}}~ ɻQ uZ`Ah wc-E4追"^7ng5?b Bޣ[fG2Үp6 *O˘4RdNAA`w/\\b}hА'<: [Go^gCYn|97~?dqv˧ћWm._W`@s/_TzyyKKMz> !srUwo\w}6GXf]z_uu;,0r8JP]yXjߕXv7w&0Xێ~b<N"eh(ơ]hrhx[@k̰!0 -Q"49,e=8ӛhJ|j3?ݓh""AM=0 ,ZXfeE^B0pҫ{^ @e8?*ْylq t:r .*i z1dzNh=& .Wopڲ+ xP%qys]fyh!ez=AB<-WSsw'19JʮCcpgaf`!x7Z< ׃ 6(Sq20vd[%Wz_|x$ 6V.W8g!$h" dzNq bGf zݶ[N@>/vi;1SwV;uy$K&o WI_?p1'fkhln~ %E3Vv̊Ƚ|x՟~ro2<KBzԙԊަÌH Xjg@- fZnΈʕjRRcIH~ A/1&  @oa] Q ./\(j#6Lخԏ37.VX>|/ىN *me1)$!W;q,MC|L'OiTϷrCO~Sn NnӗhEnݠ΅{kdT^SMR46]#ov<~PSu±X*M Kr $}Ɠ~gSn57ߜ_tXbF+ϭ/v` $ ^y1vڏ󳷭Xjg;!@q 6fk O XC#Msp;Ʋx6jq@ŋͼ di\BY_c6,L޿{NIo~p4\FZqBⱓcP1 $z%?yeUvvpkV'zz=SY]x"#\|HD|-E9ci,Zbr/Q6^f$LP)9w땟\ְfw;ٕ+Sqps[n{ZBS޿reuЄ |y=^ܦyMVȈ vw[3$* ̍7v޻kK%Wn&Xqͽ/يA 6Hm*[[rWetpbAJ<9D8s>z_y=_o-@}mZLR YLi\@-T|+03F7İ3Za(08fAm 3yLv=&Xnr%aq)ݢ%F1? x/G2Z}4k|xH;B|ÌlM'+S9ߖ%R=&xJT7j{,oq/gǒԏ BR\Ϋ XjtGM&y(gR H '~_2-^(=c-s@ $8K` l @AJ1򗥸ܺb J9)Y8/+W8(b1.h "(g?>VE*,ߖ6CдU  (C!Cxشjx%2ȃn4jj$) oM hE:Cś=1 QmTh3_-fe3/Ad(rYf4M˄ȳǐ 9="PfӶ߻|š\)! ~z{/xRiiqH0S=cYBD(+޾|`fhʑ6#!6}0۱ RJ=&ܹt"RG ̍«*Hr0Efф7X27Xm q? õ}aIŚ[nZn[_EoJ$݃=2MGFcJeج;'F.x+BM>a²4:"'$K´@D[* J HDQYL?14">G3pσTre< {{G4S؃,m7+ʯ{@ғ* }_!X`QA0p #Py! #|ak\Jʜ_ж*Ho2ć 4Є9 i `j,2C=@j)һ`ZЙף9yHi[a0@ ,Хϱ:@ CTWZ2`&oo̍~Xܐ(Đ6"CZ0?WX0i HXRk6Hy@s/!bqAȐ&gex|,&J+E(*-q v,@- ҧIiT@-T:Y- Lme HCp-+BB+a>jdr\M%*Ł? qbA[qleoXYgb405dңRO}|-8ŗ P19޷M5w޶q H+|}^i19LO|ãVs"1˱tԑ`(b`j/g%cTp[0dN @mGZ&kg(,IS?qA.gVX4[ԄSXr @a"L E痟xC ;4&!h B8ˠ%-\ [Nk1Dl :NP/Fh.j,ÜF ށ V^&@ :M . B ͱ@SAw2VdP ,vZpSO䯺A9JgB8ܱWړ2q(|? )-fQ&Dži# @RU"XYX:9t]uКw L݆͠:T*/ͷ6,g9,: קb\?| s,B*,6&1GP3vl[dŞ5$Q}q]< JX5($wיd˨ 7.OY?e*S2AO eJksiLmЖiIO*,eQ0k68JMmoAx~&'E1L8ƞRoj%>.2Z!Kg~LURՈAp哯 #,p8cρHUy_|`eQp=޸/wGv_8(&CD]-44  2 ȼ3dMܠPa"u~]DNCdUjmI M DhP&߻c./@aI!g0 ʄ@OJ- ҧR $4d\Q #hIWOGaowc8ͨ{GbΏFN1xMp,IۣO1,츠| L(@Mo Iթ{-$j]5kɢ{6tģ O΅O4pKtyƠ(ǡMqI˜KZmF/ 3: #s( iD¿瓠@]ͩTF[ՋXX P`֮/ӾoY?{IaA0 hwWxPRY(WhZp_P#͊K,{.*?㓀c1m657c?廨Pw=;C13Lurbd{d8J5A8tap:`vTWW7d("(%7DX,J}KӍ'Lb!zQr N "bw!Kj c!r0t!t ď?֠Ց~xp8)ҙ)ڈhgMLs N0=8vDBl?^- َ?fK WSMU+8"eP_`d}g)!5L&'u˛h'L,?8#)ƙW.0겺}J} ol\PZ%m#aүش.ͳ47Vkp.鍮P66 "m7N χ9xZVKLDx 肼ӎM&4I>ih<^/9i|ACz@YHs\1J w=K1E â[T]DjPJ5!M.9 $ݩ#G_>N?v$__ H 7OBp:P%ɍP0F`yߔ9:)JaN6&D2`ZT*Óp+\?;?stwp0:?nlP*2yahy"P42::||{A ;}=J7]N<(҅.AgՕ(dd&Hhhkeͭi`&Kcf+\ƙYPgwހ@dc?'^|!1[Yg&R+mQfҭ+洠#Aڟ7q!^FTUd y4 AaY+ApU*xJ*"]m d2u9C1 &z[ VѕN,RCE,CTVUoMb(knRiͽ56m`<l{[S[/`Ȕ`_x}e=|ʉdž@cT]z\|RO6Cݿ>aaH,G|az̖M*swöw"0è=N`a`[nHW:kscX݂U3a F=(D&@|Bz*Ⱥ/|g&dX~[1l jaf7tR(CZdP 8(zS:ݩF1Um8>QcXR!mA49&B:zN (`a3Bg"5jͦ)HIBƘedngv&$q˼,{5 M@2S* J^ei$p >tYee( hDdh+a3Ӑĕ-*f(D.&y"MS0іe6Yiע.w mi2Eq+/n553qG MRzS۴,߈ 40| 0}8}"I0E EL~Opgfb b*Ad,ƞÒ;^p+"|Q ˘A&GdzR0Lv6]˪4o+A(L֏2h,E̲3[2p)lD594 MS c9E?`K5f ¨a𚼞y08OwnDZ (>/^3xo͍1 |ERmZZB! \3X zGr =qI(Z]:=Cso޲rVpgnlGV*Ğ@- @- f+q,M쫌+ӝjRpPf*qaCNf)?ߡcvzbb3#1;M̩(Ch85>8EQ9K(G(HMjrL.r bd24.dQ)B=Oo/ѕ H*4u9W@t٬R7 ^ S~QkJM$t,v`0m4c}h@_#SMhZ6DƍʈؘBB-)oQHRaZ-}%S iCجyt0V;jE8WYGVYFfŖݶzmY;8.Z+r p-,_, rhb4m3"0ʔoypyYA)g-\S&ε[n~duE.eMU>DC*sݚ>H;oY2+@8²r#(-#URK$PX\Z[-60@{MAZ!= _c~;18e}_k/JxBS"Vs4t! $ӡ!eqE<'xwc}"LERh<7=ѡDMH_1J&B=F&1xd@$o<8(vhK/?]XF w ;"G~Q(YNf*"e('DQ ,5nCSȟ6104c`6R`Ũ@F` w{f,LrXBV]mܟ0 `AlDXDQ%fv{vt>O⩫B&*PzvaI:+2$yo,\VEJN<\ N&YKH#~?~'p W_Yh=(|Rv{GZq 2e@Z aYD~w3kgMSSb'ŁK3hJt2Ϋ^PiaPON b$#Z7gg(ԢY-H͇ۇIUfZq׃G: 윂*Jʱ=#P<&ꍖ5..ST=  ];Ý#P9߀G -e"@Ɇڂ]t7aITVi~`IӞSH=sq`;O86\`o_hBoghn@wsm8x7k. ڴ(ٱKsc$D^HVjKkal^95$-ͧ?ءi86GF &%Nla PJ`E17Fm@ FvxI ZrT,.rh"niBk-yy 3O- €@- f1qpکδtzc8,n(]Q:2Z>@.t (dx$&QL i2#mAR) uG !F 1R ,`XXY iK _7`F3rE(i$J/P2OΖLsYRҴHB -=*wrN'tzyblhp8%yId,.ۚ΍O 2OC#p0HC3Zb2g[ ZjG~w(I$"(&<1 333'0 .̛03cfE-uEa 5]m s\uU' aX*W[Хŀf9Gzw-8c־&؎W~xˮ, 1ϵ,m~@{È6ۅ) [ȉ$a%UHGՏdg xhl&:Q.$7Q6 O{\ {}ٙ*N%$@7(0I\2[0n1W:{Iҹq&kE}?\EVN/c+'qr/KEj'dn}G7)3{!`00 +;OU3>o$9npI3A7;Yk߰5ϩryZy®$tٹʗG6LnbbdFEJVP~,qD}S-\&VR[CM7^ߓL$4q${ŋ$$@7(pUf]]GN+=P)TL|0HuL77 KfsmĒ+P)䈶)L$!R {1AkJU)A5ʏ)Ph0N 3\ $C>0_{D)LsM=ၦ\I:Ux#x'[))MI/両8XVG =D& P Fo8PdS麌W~gy?*Rg!)U>W* n4kKҪ<88-@]Jm L``$)Sg1 ԘAQ1Q'hJM(͗: uT5W69|訞l֡ZtY=jPCLO6a @'c)LԮ^'ɉV7MZګdwVRs;9Ƞf;R_.e7SNT{M0;= LnP Pz=˝XYjLКʵĨLoD'>gmWRF}(XQGdzK7wfq`UGB2jeBЮ}g_vˮq#:D0y) `]˽{N =EzК2+Fk_6-эʣԭvn~hGp v`bk+_~#ٕ{c~)6`S8aZyuّ][(ʮd[6n ax?bsg7ETVmXe[^voKP ZK-]zm"LnP p`w{q?[0)F8s0p9\Pϕ=7ݫ N-veWQG5T^ø-.9ҏ͉y[CJlUR0j93(8êh)pݾwY,qoC3@]Sʻl3G߷k4  E3ޙh7m1 `s wJ @ @ `ޭbIENDB`tofi-0.9.1/src/000077500000000000000000000000001441474151400132565ustar00rootroot00000000000000tofi-0.9.1/src/clipboard.c000066400000000000000000000007241441474151400153640ustar00rootroot00000000000000#include #include "clipboard.h" void clipboard_finish_paste(struct clipboard *clipboard) { if (clipboard->fd > 0) { close(clipboard->fd); clipboard->fd = 0; } } void clipboard_reset(struct clipboard *clipboard) { if (clipboard->wl_data_offer != NULL) { wl_data_offer_destroy(clipboard->wl_data_offer); clipboard->wl_data_offer = NULL; } if (clipboard->fd > 0) { close(clipboard->fd); clipboard->fd = 0; } clipboard->mime_type = NULL; } tofi-0.9.1/src/clipboard.h000066400000000000000000000004531441474151400153700ustar00rootroot00000000000000#ifndef CLIPBOARD_H #define CLIPBOARD_H #include struct clipboard { struct wl_data_offer *wl_data_offer; const char *mime_type; int fd; }; void clipboard_finish_paste(struct clipboard *clipboard); void clipboard_reset(struct clipboard *clipboard); #endif /* CLIPBOARD_H */ tofi-0.9.1/src/color.c000066400000000000000000000027441441474151400145470ustar00rootroot00000000000000#include #include #include #include "color.h" #include "log.h" struct color hex_to_color(const char *hex) { if (hex[0] == '#') { hex++; } uint32_t val = 0; int64_t tmp; size_t len = strlen(hex); errno = 0; if (len == 3) { char str[] = { hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], '\0'}; char *endptr; tmp = strtol(str, &endptr, 16); if (errno || *endptr != '\0' || tmp < 0) { return (struct color) { -1, -1, -1, -1 }; } val = tmp; val <<= 8; val |= 0xFFu; } else if (len == 4) { char str[] = { hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], hex[3], hex[3], '\0'}; char *endptr; tmp = strtol(str, &endptr, 16); if (errno || *endptr != '\0' || tmp < 0) { return (struct color) { -1, -1, -1, -1 }; } val = tmp; } else if (len == 6) { char *endptr; tmp = strtol(hex, &endptr, 16); if (errno || *endptr != '\0' || tmp < 0) { return (struct color) { -1, -1, -1, -1 }; } val = tmp; val <<= 8; val |= 0xFFu; } else if (len == 8) { char *endptr; tmp = strtol(hex, &endptr, 16); if (errno || *endptr != '\0' || tmp < 0) { return (struct color) { -1, -1, -1, -1 }; } val = tmp; } else { return (struct color) { -1, -1, -1, -1 }; } return (struct color) { .r = (float)((val & 0xFF000000u) >> 24) / 255.0f, .g = (float)((val & 0x00FF0000u) >> 16) / 255.0f, .b = (float)((val & 0x0000FF00u) >> 8) / 255.0f, .a = (float)((val & 0x000000FFu) >> 0) / 255.0f, }; } tofi-0.9.1/src/color.h000066400000000000000000000003101441474151400145370ustar00rootroot00000000000000#ifndef COLOR_H #define COLOR_H #include #include struct color { float r; float g; float b; float a; }; struct color hex_to_color(const char *hex); #endif /* COLOR_H */ tofi-0.9.1/src/compgen.c000066400000000000000000000150551441474151400150600ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "compgen.h" #include "history.h" #include "log.h" #include "mkdirp.h" #include "string_vec.h" #include "xmalloc.h" static const char *default_cache_dir = ".cache"; static const char *cache_basename = "tofi-compgen"; [[nodiscard("memory leaked")]] static char *get_cache_path() { char *cache_name = NULL; const char *state_path = getenv("XDG_CACHE_HOME"); if (state_path == NULL) { const char *home = getenv("HOME"); if (home == NULL) { log_error("Couldn't retrieve HOME from environment.\n"); return NULL; } size_t len = strlen(home) + 1 + strlen(default_cache_dir) + 1 + strlen(cache_basename) + 1; cache_name = xmalloc(len); snprintf( cache_name, len, "%s/%s/%s", home, default_cache_dir, cache_basename); } else { size_t len = strlen(state_path) + 1 + strlen(cache_basename) + 1; cache_name = xmalloc(len); snprintf( cache_name, len, "%s/%s", state_path, cache_basename); } return cache_name; } static void write_cache(const char *buffer, const char *filename) { errno = 0; FILE *fp = fopen(filename, "wb"); if (!fp) { log_error("Failed to open cache file \"%s\": %s\n", filename, strerror(errno)); return; } size_t len = strlen(buffer); errno = 0; if (fwrite(buffer, 1, len, fp) != len) { log_error("Error writing cache file \"%s\": %s\n", filename, strerror(errno)); } fclose(fp); } static char *read_cache(const char *filename) { errno = 0; FILE *fp = fopen(filename, "rb"); if (!fp) { log_error("Failed to open cache file \"%s\": %s\n", filename, strerror(errno)); return NULL; } if (fseek(fp, 0, SEEK_END)) { log_error("Failed to seek in cache file: %s\n", strerror(errno)); fclose(fp); return NULL; } size_t size; { long ssize = ftell(fp); if (ssize < 0) { log_error("Failed to determine cache file size: %s\n", strerror(errno)); fclose(fp); return NULL; } size = (size_t)ssize; } char *cache = xmalloc(size + 1); rewind(fp); if (fread(cache, 1, size, fp) != size) { log_error("Failed to read cache file: %s\n", strerror(errno)); free(cache); fclose(fp); return NULL; } fclose(fp); cache[size] = '\0'; return cache; } char *compgen_cached() { log_debug("Retrieving PATH.\n"); const char *env_path = getenv("PATH"); if (env_path == NULL) { log_error("Couldn't retrieve PATH from environment.\n"); exit(EXIT_FAILURE); } log_debug("Retrieving cache location.\n"); char *cache_path = get_cache_path(); struct stat sb; if (cache_path == NULL) { return compgen(); } /* If the cache doesn't exist, create it and return */ errno = 0; if (stat(cache_path, &sb) == -1) { if (errno == ENOENT) { char *commands = compgen(); if (mkdirp(cache_path)) { write_cache(commands, cache_path); } free(cache_path); return commands; } free(cache_path); return compgen(); } /* The cache exists, so check if it's still in date */ char *path = xstrdup(env_path); char *saveptr = NULL; char *path_entry = strtok_r(path, ":", &saveptr); bool out_of_date = false; while (path_entry != NULL) { struct stat path_sb; if (stat(path_entry, &path_sb) == 0) { if (path_sb.st_mtim.tv_sec > sb.st_mtim.tv_sec) { out_of_date = true; break; } } path_entry = strtok_r(NULL, ":", &saveptr); } free(path); char *commands; if (out_of_date) { log_debug("Cache out of date, updating.\n"); log_indent(); commands = compgen(); log_unindent(); write_cache(commands, cache_path); } else { log_debug("Cache up to date, loading.\n"); commands = read_cache(cache_path); } free(cache_path); return commands; } char *compgen() { log_debug("Retrieving PATH.\n"); const char *env_path = getenv("PATH"); if (env_path == NULL) { log_error("Couldn't retrieve PATH from environment.\n"); exit(EXIT_FAILURE); } struct string_vec programs = string_vec_create(); char *path = xstrdup(env_path); char *saveptr = NULL; char *path_entry = strtok_r(path, ":", &saveptr); log_debug("Scanning PATH for binaries.\n"); while (path_entry != NULL) { DIR *dir = opendir(path_entry); if (dir != NULL) { int fd = dirfd(dir); struct dirent *d; while ((d = readdir(dir)) != NULL) { struct stat sb; if (fstatat(fd, d->d_name, &sb, 0) == -1) { continue; } if (!(sb.st_mode & S_IXUSR)) { continue; } if (!S_ISREG(sb.st_mode)) { continue; } string_vec_add(&programs, d->d_name); } closedir(dir); } path_entry = strtok_r(NULL, ":", &saveptr); } free(path); log_debug("Sorting results.\n"); string_vec_sort(&programs); log_debug("Making unique.\n"); string_vec_uniq(&programs); size_t buf_len = 0; for (size_t i = 0; i < programs.count; i++) { buf_len += strlen(programs.buf[i].string) + 1; } char *buf = xmalloc(buf_len + 1); size_t bytes_written = 0; for (size_t i = 0; i < programs.count; i++) { bytes_written += sprintf(&buf[bytes_written], "%s\n", programs.buf[i].string); } buf[bytes_written] = '\0'; string_vec_destroy(&programs); return buf; } static int cmpscorep(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; return str2->history_score - str1->history_score; } struct string_ref_vec compgen_history_sort(struct string_ref_vec *programs, struct history *history) { log_debug("Moving already known programs to the front.\n"); for (size_t i = 0; i < history->count; i++) { struct scored_string_ref *res = string_ref_vec_find_sorted(programs, history->buf[i].name); if (res == NULL) { log_debug("History entry \"%s\" not found.\n", history->buf[i].name); continue; } res->history_score = history->buf[i].run_count; } /* * For compgen, we expect there to be many more commands than history * entries. For speed, we therefore create a copy of the command * vector with all of the non-zero history score items pushed to the * front. We can then call qsort() on just the first few items of the * new vector, rather than on the entire original vector. */ struct string_ref_vec vec = { .count = programs->count, .size = programs->size, .buf = xcalloc(programs->size, sizeof(*vec.buf)) }; size_t n_hist = 0; for (ssize_t i = programs->count - 1; i >= 0; i--) { if (programs->buf[i].history_score == 0) { vec.buf[i + n_hist] = programs->buf[i]; } else { vec.buf[n_hist] = programs->buf[i]; n_hist++; } } qsort(vec.buf, n_hist, sizeof(vec.buf[0]), cmpscorep); return vec; } tofi-0.9.1/src/compgen.h000066400000000000000000000005411441474151400150570ustar00rootroot00000000000000#ifndef COMPGEN_H #define COMPGEN_H #include "history.h" #include "string_vec.h" [[nodiscard("memory leaked")]] char *compgen(void); [[nodiscard("memory leaked")]] char *compgen_cached(void); [[nodiscard("memory leaked")]] struct string_ref_vec compgen_history_sort(struct string_ref_vec *programs, struct history *history); #endif /* COMPGEN_H */ tofi-0.9.1/src/config.c000066400000000000000000001071361441474151400146770ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "tofi.h" #include "color.h" #include "config.h" #include "log.h" #include "nelem.h" #include "scale.h" #include "unicode.h" #include "xmalloc.h" /* Maximum number of config file errors before we give up */ #define MAX_ERRORS 5 /* Maximum inclusion recursion depth before we give up */ #define MAX_RECURSION 32 /* Anyone with a 10M config file is doing something very wrong */ #define MAX_CONFIG_SIZE (10*1024*1024) /* Convenience macros for anchor combinations */ #define ANCHOR_TOP_LEFT (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \ ) #define ANCHOR_TOP (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \ ) #define ANCHOR_TOP_RIGHT (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \ ) #define ANCHOR_RIGHT (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \ ) #define ANCHOR_BOTTOM_RIGHT (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \ ) #define ANCHOR_BOTTOM (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \ ) #define ANCHOR_BOTTOM_LEFT (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \ ) #define ANCHOR_LEFT (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \ ) #define ANCHOR_CENTER (\ ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \ | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \ ) struct uint32_percent { uint32_t value; bool percent; }; static char *strip(const char *str); static bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const char *option, const char *value); static char *get_config_path(void); static uint32_t fixup_percentage(uint32_t value, uint32_t base, bool is_percent); static uint32_t parse_anchor(const char *filename, size_t lineno, const char *str, bool *err); static enum cursor_style parse_cursor_style(const char *filename, size_t lineno, const char *str, bool *err); static bool parse_bool(const char *filename, size_t lineno, const char *str, bool *err); static uint32_t parse_char(const char *filename, size_t lineno, const char *str, bool *err); static struct color parse_color(const char *filename, size_t lineno, const char *str, bool *err); static uint32_t parse_uint32(const char *filename, size_t lineno, const char *str, bool *err); static int32_t parse_int32(const char *filename, size_t lineno, const char *str, bool *err); static struct uint32_percent parse_uint32_percent(const char *filename, size_t lineno, const char *str, bool *err); static struct directional parse_directional(const char *filename, size_t lineno, const char *str, bool *err); /* * Function-like macro. Yuck. */ #define PARSE_ERROR_NO_ARGS(filename, lineno, fmt) \ if ((lineno) > 0) {\ log_error("%s: line %zu: ", (filename), (lineno));\ log_append_error((fmt)); \ } else {\ log_error((fmt)); \ } #define PARSE_ERROR(filename, lineno, fmt, ...) \ if ((lineno) > 0) {\ log_error("%s: line %zu: ", (filename), (lineno));\ log_append_error((fmt), __VA_ARGS__); \ } else {\ log_error((fmt), __VA_ARGS__); \ } void config_load(struct tofi *tofi, const char *filename) { char *default_filename = NULL; if (!filename) { default_filename = get_config_path(); if (!default_filename) { return; } filename = default_filename; } /* * Track and limit recursion depth, so we don't overflow the stack if * a config file loop is created. */ static uint8_t recursion_depth = 0; recursion_depth++; if (recursion_depth > MAX_RECURSION) { log_error("Refusing to load %s, recursion too deep (>%u layers).\n", filename, MAX_RECURSION); recursion_depth--; return; } char *config; FILE *fp = fopen(filename, "rb"); if (!fp) { if (!default_filename || errno != ENOENT) { log_error("Failed to open config file %s: %s\n", filename, strerror(errno)); } goto CLEANUP_FILENAME; } if (fseek(fp, 0, SEEK_END)) { log_error("Failed to seek in config file: %s\n", strerror(errno)); fclose(fp); goto CLEANUP_FILENAME; } size_t size; { long ssize = ftell(fp); if (ssize < 0) { log_error("Failed to determine config file size: %s\n", strerror(errno)); fclose(fp); goto CLEANUP_FILENAME; } if (ssize > MAX_CONFIG_SIZE) { log_error("Config file too big (> %d MiB)! Are you sure it's a file?\n", MAX_CONFIG_SIZE / 1024 / 1024); fclose(fp); goto CLEANUP_FILENAME; } size = (size_t)ssize; } config = xmalloc(size + 1); if (!config) { log_error("Failed to malloc buffer for config file.\n"); fclose(fp); goto CLEANUP_FILENAME; } rewind(fp); if (fread(config, 1, size, fp) != size) { log_error("Failed to read config file: %s\n", strerror(errno)); fclose(fp); goto CLEANUP_CONFIG; } fclose(fp); config[size] = '\0'; char *config_copy = xstrdup(config); if (!config_copy) { log_error("Failed to malloc second buffer for config file.\n"); goto CLEANUP_ALL; } log_debug("Loading config file %s.\n", filename); char *saveptr1 = NULL; char *saveptr2 = NULL; char *copy_pos = config_copy; size_t lineno = 1; size_t num_errs = 0; for (char *str1 = config; ; str1 = NULL, saveptr2 = NULL) { if (num_errs > MAX_ERRORS) { log_error("Too many config file errors (>%u), giving up.\n", MAX_ERRORS); break; } char *line = strtok_r(str1, "\r\n", &saveptr1); if (!line) { /* We're done here */ break; } while ((copy_pos - config_copy) < (line - config)) { if (*copy_pos == '\n') { lineno++; } copy_pos++; } { /* Grab first non-space character on the line. */ char c = '\0'; for (char *tmp = line; *tmp != '\0'; tmp++) { c = *tmp; if (!isspace(c)) { break; } } /* * Comment characters. * N.B. treating section headers as comments for now. */ switch (c) { case '#': case ';': case '[': continue; } } if (line[0] == '=') { PARSE_ERROR_NO_ARGS(filename, lineno, "Missing option.\n"); num_errs++; continue; } char *option = strtok_r(line, "=", &saveptr2); if (!option) { char *tmp = strip(line); PARSE_ERROR(filename, lineno, "Config option \"%s\" missing value.\n", tmp); num_errs++; free(tmp); continue; } char *option_stripped = strip(option); if (!option_stripped) { PARSE_ERROR_NO_ARGS(filename, lineno, "Missing option.\n"); num_errs++; continue; } char *value = strtok_r(NULL, "\r\n", &saveptr2); if (!value) { PARSE_ERROR(filename, lineno, "Config option \"%s\" missing value.\n", option_stripped); num_errs++; free(option_stripped); continue; } char *value_stripped = strip(value); if (!value_stripped) { PARSE_ERROR(filename, lineno, "Config option \"%s\" missing value.\n", option_stripped); num_errs++; free(option_stripped); continue; } if (!parse_option(tofi, filename, lineno, option_stripped, value_stripped)) { num_errs++; } /* Cleanup */ free(value_stripped); free(option_stripped); } CLEANUP_ALL: free(config_copy); CLEANUP_CONFIG: free(config); CLEANUP_FILENAME: if (default_filename) { free(default_filename); } recursion_depth--; } char *strip(const char *str) { size_t start = 0; size_t end = strlen(str); while (start <= end && isspace(str[start])) { start++; } if (start == end) { return NULL; } while (end > start && (isspace(str[end]) || str[end] == '\0')) { end--; } if (end < start) { return NULL; } if (str[start] == '"' && str[end] == '"' && end > start) { start++; end--; } size_t len = end - start + 1; char *buf = xcalloc(len + 1, 1); strncpy(buf, str + start, len); buf[len] = '\0'; return buf; } bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const char *option, const char *value) { bool err = false; struct uint32_percent percent; if (strcasecmp(option, "include") == 0) { if (value[0] == '/') { config_load(tofi, value); } else { char *tmp = xstrdup(filename); char *dir = dirname(tmp); size_t len = strlen(dir) + 1 + strlen(value) + 1; char *config = xcalloc(len, 1); snprintf(config, len, "%s/%s", dir, value); config_load(tofi, config); free(config); free(tmp); } } else if (strcasecmp(option, "anchor") == 0) { uint32_t val = parse_anchor(filename, lineno, value, &err); if (!err) { tofi->anchor = val; } } else if (strcasecmp(option, "background-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.background_color = val; } } else if (strcasecmp(option, "corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.corner_radius = val; } } else if (strcasecmp(option, "output") == 0) { snprintf(tofi->target_output_name, N_ELEM(tofi->target_output_name), "%s", value); } else if (strcasecmp(option, "font") == 0) { if ((strlen(value) > 2) && (value[0] == '~') && (value[1] == '/')) { snprintf(tofi->window.entry.font_name, N_ELEM(tofi->window.entry.font_name), "%s%s", getenv("HOME"), &(value[1])); } else { snprintf(tofi->window.entry.font_name, N_ELEM(tofi->window.entry.font_name), "%s", value); } } else if (strcasecmp(option, "font-size") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (val == 0) { err = true; PARSE_ERROR(filename, lineno, "Option \"%s\" must be greater than 0.\n", option); } else { tofi->window.entry.font_size = val; } } else if (strcasecmp(option, "font-features") == 0) { snprintf(tofi->window.entry.font_features, N_ELEM(tofi->window.entry.font_features), "%s", value); } else if (strcasecmp(option, "font-variations") == 0) { snprintf(tofi->window.entry.font_variations, N_ELEM(tofi->window.entry.font_variations), "%s", value); } else if (strcasecmp(option, "num-results") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.num_results = val; } } else if (strcasecmp(option, "outline-width") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.outline_width = val; } } else if (strcasecmp(option, "outline-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.outline_color = val; } } else if (strcasecmp(option, "text-cursor") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->window.entry.cursor_theme.show = val; } } else if (strcasecmp(option, "text-cursor-style") == 0) { enum cursor_style val = parse_cursor_style(filename, lineno, value, &err); if (!err) { tofi->window.entry.cursor_theme.style = val; } } else if (strcasecmp(option, "text-cursor-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.cursor_theme.color = val; tofi->window.entry.cursor_theme.color_specified = true; } } else if (strcasecmp(option, "text-cursor-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.cursor_theme.text_color = val; tofi->window.entry.cursor_theme.text_color_specified = true; } } else if (strcasecmp(option, "text-cursor-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.cursor_theme.corner_radius = val; } } else if (strcasecmp(option, "text-cursor-thickness") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.cursor_theme.thickness = val; tofi->window.entry.cursor_theme.thickness_specified = true; } } else if (strcasecmp(option, "prompt-text") == 0) { snprintf(tofi->window.entry.prompt_text, N_ELEM(tofi->window.entry.prompt_text), "%s", value); } else if (strcasecmp(option, "prompt-padding") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.prompt_padding = val; } } else if (strcasecmp(option, "prompt-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.prompt_theme.foreground_color = val; tofi->window.entry.prompt_theme.foreground_specified = true; } } else if (strcasecmp(option, "prompt-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.prompt_theme.background_color = val; tofi->window.entry.prompt_theme.background_specified = true; } } else if (strcasecmp(option, "prompt-background-padding") == 0) { struct directional val = parse_directional(filename, lineno, value, &err); if (!err) { tofi->window.entry.prompt_theme.padding = val; tofi->window.entry.prompt_theme.padding_specified = true; } } else if (strcasecmp(option, "prompt-background-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.prompt_theme.background_corner_radius = val; tofi->window.entry.prompt_theme.radius_specified = true; } } else if (strcasecmp(option, "placeholder-text") == 0) { snprintf(tofi->window.entry.placeholder_text, N_ELEM(tofi->window.entry.placeholder_text), "%s", value); } else if (strcasecmp(option, "placeholder-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.placeholder_theme.foreground_color = val; tofi->window.entry.placeholder_theme.foreground_specified = true; } } else if (strcasecmp(option, "placeholder-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.placeholder_theme.background_color = val; tofi->window.entry.placeholder_theme.background_specified = true; } } else if (strcasecmp(option, "placeholder-background-padding") == 0) { struct directional val = parse_directional(filename, lineno, value, &err); if (!err) { tofi->window.entry.placeholder_theme.padding = val; tofi->window.entry.placeholder_theme.padding_specified = true; } } else if (strcasecmp(option, "placeholder-background-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.placeholder_theme.background_corner_radius = val; tofi->window.entry.placeholder_theme.radius_specified = true; } } else if (strcasecmp(option, "input-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.input_theme.foreground_color = val; tofi->window.entry.input_theme.foreground_specified = true; } } else if (strcasecmp(option, "input-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.input_theme.background_color = val; tofi->window.entry.input_theme.background_specified = true; } } else if (strcasecmp(option, "input-background-padding") == 0) { struct directional val = parse_directional(filename, lineno, value, &err); if (!err) { tofi->window.entry.input_theme.padding = val; tofi->window.entry.input_theme.padding_specified = true; } } else if (strcasecmp(option, "input-background-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.input_theme.background_corner_radius = val; tofi->window.entry.input_theme.radius_specified = true; } } else if (strcasecmp(option, "default-result-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.default_result_theme.foreground_color = val; tofi->window.entry.default_result_theme.foreground_specified = true; } } else if (strcasecmp(option, "default-result-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.default_result_theme.background_color = val; tofi->window.entry.default_result_theme.background_specified = true; } } else if (strcasecmp(option, "default-result-background-padding") == 0) { struct directional val = parse_directional(filename, lineno, value, &err); if (!err) { tofi->window.entry.default_result_theme.padding = val; tofi->window.entry.default_result_theme.padding_specified = true; } } else if (strcasecmp(option, "default-result-background-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.default_result_theme.background_corner_radius = val; tofi->window.entry.default_result_theme.radius_specified = true; } } else if (strcasecmp(option, "alternate-result-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.alternate_result_theme.foreground_color = val; tofi->window.entry.alternate_result_theme.foreground_specified = true; } } else if (strcasecmp(option, "alternate-result-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.alternate_result_theme.background_color = val; tofi->window.entry.alternate_result_theme.background_specified = true; } } else if (strcasecmp(option, "alternate-result-background-padding") == 0) { struct directional val = parse_directional(filename, lineno, value, &err); if (!err) { tofi->window.entry.alternate_result_theme.padding = val; tofi->window.entry.alternate_result_theme.padding_specified = true; } } else if (strcasecmp(option, "alternate-result-background-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.alternate_result_theme.background_corner_radius = val; tofi->window.entry.alternate_result_theme.radius_specified = true; } } else if (strcasecmp(option, "min-input-width") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.input_width = val; } } else if (strcasecmp(option, "result-spacing") == 0) { int32_t val = parse_int32(filename, lineno, value, &err); if (!err) { tofi->window.entry.result_spacing = val; } } else if (strcasecmp(option, "border-width") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.border_width = val; } } else if (strcasecmp(option, "border-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.border_color = val; } } else if (strcasecmp(option, "text-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.foreground_color = val; } } else if (strcasecmp(option, "selection-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.selection_theme.foreground_color = val; tofi->window.entry.selection_theme.foreground_specified = true; } } else if (strcasecmp(option, "selection-match-color") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.selection_highlight_color = val; } } else if (strcasecmp(option, "selection-padding") == 0) { log_warning("The \"selection-padding\" option is deprecated, and will be removed in future. Please switch to \"selection-background-padding\".\n"); int32_t val = parse_int32(filename, lineno, value, &err); if (!err) { tofi->window.entry.selection_theme.padding.left = val; tofi->window.entry.selection_theme.padding.right = val; tofi->window.entry.selection_theme.padding_specified = true; } } else if (strcasecmp(option, "selection-background") == 0) { struct color val = parse_color(filename, lineno, value, &err); if (!err) { tofi->window.entry.selection_theme.background_color = val; tofi->window.entry.selection_theme.background_specified = true; } } else if (strcasecmp(option, "selection-background-padding") == 0) { struct directional val = parse_directional(filename, lineno, value, &err); if (!err) { tofi->window.entry.selection_theme.padding = val; tofi->window.entry.selection_theme.padding_specified = true; } } else if (strcasecmp(option, "selection-background-corner-radius") == 0) { uint32_t val = parse_uint32(filename, lineno, value, &err); if (!err) { tofi->window.entry.selection_theme.background_corner_radius = val; tofi->window.entry.selection_theme.radius_specified = true; } } else if (strcasecmp(option, "exclusive-zone") == 0) { if (strcmp(value, "-1") == 0) { tofi->window.exclusive_zone = -1; } else { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.exclusive_zone = percent.value; tofi->window.exclusive_zone_is_percent = percent.percent; } } } else if (strcasecmp(option, "width") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.width = percent.value; tofi->window.width_is_percent = percent.percent; } } else if (strcasecmp(option, "height") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.height = percent.value; tofi->window.height_is_percent = percent.percent; } } else if (strcasecmp(option, "margin-top") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.margin_top = percent.value; tofi->window.margin_top_is_percent = percent.percent; } } else if (strcasecmp(option, "margin-bottom") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.margin_bottom = percent.value; tofi->window.margin_bottom_is_percent = percent.percent; } } else if (strcasecmp(option, "margin-left") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.margin_left = percent.value; tofi->window.margin_left_is_percent = percent.percent; } } else if (strcasecmp(option, "margin-right") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.margin_right = percent.value; tofi->window.margin_right_is_percent = percent.percent; } } else if (strcasecmp(option, "padding-top") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.entry.padding_top = percent.value; tofi->window.entry.padding_top_is_percent = percent.percent; } } else if (strcasecmp(option, "padding-bottom") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.entry.padding_bottom = percent.value; tofi->window.entry.padding_bottom_is_percent = percent.percent; } } else if (strcasecmp(option, "padding-left") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.entry.padding_left = percent.value; tofi->window.entry.padding_left_is_percent = percent.percent; } } else if (strcasecmp(option, "padding-right") == 0) { percent = parse_uint32_percent(filename, lineno, value, &err); if (!err) { tofi->window.entry.padding_right = percent.value; tofi->window.entry.padding_right_is_percent = percent.percent; } } else if (strcasecmp(option, "clip-to-padding") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->window.entry.clip_to_padding = val; } } else if (strcasecmp(option, "horizontal") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->window.entry.horizontal = val; } } else if (strcasecmp(option, "hide-cursor") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->hide_cursor = val; } } else if (strcasecmp(option, "history") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->use_history = val; } } else if (strcasecmp(option, "history-file") == 0) { snprintf(tofi->history_file, N_ELEM(tofi->history_file), "%s", value); } else if (strcasecmp(option, "fuzzy-match") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->fuzzy_match = val; } } else if (strcasecmp(option, "require-match") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->require_match = val; } } else if (strcasecmp(option, "auto-accept-single") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->auto_accept_single = val; } } else if (strcasecmp(option, "hide-input") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->window.entry.hide_input = val; } } else if (strcasecmp(option, "hidden-character") == 0) { uint32_t ch = parse_char(filename, lineno, value, &err); if (!err) { tofi->window.entry.hidden_character_utf8_length = utf32_to_utf8(ch, tofi->window.entry.hidden_character_utf8); } } else if (strcasecmp(option, "drun-launch") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->drun_launch = val; } } else if (strcasecmp(option, "drun-print-exec") == 0) { log_warning("drun-print-exec is deprecated, as it is now always true.\n" " This option may be removed in a future version of tofi.\n"); } else if (strcasecmp(option, "terminal") == 0) { snprintf(tofi->default_terminal, N_ELEM(tofi->default_terminal), "%s", value); } else if (strcasecmp(option, "hint-font") == 0) { bool val = !parse_bool(filename, lineno, value, &err); if (!err) { tofi->window.entry.harfbuzz.disable_hinting = val; } } else if (strcasecmp(option, "multi-instance") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->multiple_instance = val; } } else if (strcasecmp(option, "ascii-input") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->ascii_input = val; } } else if (strcasecmp(option, "late-keyboard-init") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->late_keyboard_init = val; } } else if (strcasecmp(option, "output") == 0) { snprintf(tofi->target_output_name, N_ELEM(tofi->target_output_name), "%s", value); } else if (strcasecmp(option, "scale") == 0) { bool val = parse_bool(filename, lineno, value, &err); if (!err) { tofi->use_scale = val; } } else { PARSE_ERROR(filename, lineno, "Unknown option \"%s\"\n", option); err = true; } return !err; } bool config_apply(struct tofi *tofi, const char *option, const char *value) { return parse_option(tofi, "", 0, option, value); } uint32_t fixup_percentage(uint32_t value, uint32_t base, bool is_percent) { if (is_percent) { return value * base / 100; } return value; } void config_fixup_values(struct tofi *tofi) { uint32_t base_width = tofi->output_width; uint32_t base_height = tofi->output_height; uint32_t scale; if (tofi->window.fractional_scale != 0) { scale = tofi->window.fractional_scale; } else { scale = tofi->window.scale * 120; } /* * If we're going to be scaling these values in Cairo, * we need to apply the inverse scale here. */ if (tofi->use_scale) { base_width = scale_apply_inverse(base_width, scale); base_height = scale_apply_inverse(base_height, scale); } tofi->window.margin_top = fixup_percentage( tofi->window.margin_top, base_height, tofi->window.margin_top_is_percent); tofi->window.margin_bottom = fixup_percentage( tofi->window.margin_bottom, base_height, tofi->window.margin_bottom_is_percent); tofi->window.margin_left = fixup_percentage( tofi->window.margin_left, base_width, tofi->window.margin_left_is_percent); tofi->window.margin_right = fixup_percentage( tofi->window.margin_right, base_width, tofi->window.margin_right_is_percent); tofi->window.entry.padding_top = fixup_percentage( tofi->window.entry.padding_top, base_height, tofi->window.entry.padding_top_is_percent); tofi->window.entry.padding_bottom = fixup_percentage( tofi->window.entry.padding_bottom, base_height, tofi->window.entry.padding_bottom_is_percent); tofi->window.entry.padding_left = fixup_percentage( tofi->window.entry.padding_left, base_width, tofi->window.entry.padding_left_is_percent); tofi->window.entry.padding_right = fixup_percentage( tofi->window.entry.padding_right, base_width, tofi->window.entry.padding_right_is_percent); /* * Window width and height are a little special. We're only going to be * using them to specify sizes to Wayland, which always wants scaled * pixels, so always scale them here (unless we've directly specified a * scaled size). */ tofi->window.width = fixup_percentage( tofi->window.width, tofi->output_width, tofi->window.width_is_percent); tofi->window.height = fixup_percentage( tofi->window.height, tofi->output_height, tofi->window.height_is_percent); if (tofi->window.width_is_percent || !tofi->use_scale) { tofi->window.width = scale_apply_inverse(tofi->window.width, scale); } if (tofi->window.height_is_percent || !tofi->use_scale) { tofi->window.height = scale_apply_inverse(tofi->window.height, scale); } /* Don't attempt percentage handling if exclusive_zone is set to -1. */ if (tofi->window.exclusive_zone > 0) { /* Exclusive zone base depends on anchor. */ switch (tofi->anchor) { case ANCHOR_TOP: case ANCHOR_BOTTOM: tofi->window.exclusive_zone = fixup_percentage( tofi->window.exclusive_zone, base_height, tofi->window.exclusive_zone_is_percent); break; case ANCHOR_LEFT: case ANCHOR_RIGHT: tofi->window.exclusive_zone = fixup_percentage( tofi->window.exclusive_zone, base_width, tofi->window.exclusive_zone_is_percent); break; default: /* * Exclusive zone >0 is meaningless for other * anchor positions. */ tofi->window.exclusive_zone = MIN(tofi->window.exclusive_zone, 0); break; } } } char *get_config_path() { char *base_dir = getenv("XDG_CONFIG_HOME"); char *ext = ""; size_t len = strlen("/tofi/config") + 1; if (!base_dir) { base_dir = getenv("HOME"); ext = "/.config"; if (!base_dir) { log_error("Couldn't find XDG_CONFIG_HOME or HOME envvars\n"); return NULL; } } len += strlen(base_dir) + strlen(ext) + 2; char *name = xcalloc(len, sizeof(*name)); snprintf(name, len, "%s%s%s", base_dir, ext, "/tofi/config"); return name; } bool parse_bool(const char *filename, size_t lineno, const char *str, bool *err) { if (strcasecmp(str, "true") == 0) { return true; } else if (strcasecmp(str, "false") == 0) { return false; } PARSE_ERROR(filename, lineno, "Invalid boolean value \"%s\".\n", str); if (err) { *err = true; } return false; } uint32_t parse_char(const char *filename, size_t lineno, const char *str, bool *err) { uint32_t ch = U'\0'; if (*str == '\0') { return ch; } if (!utf8_validate(str)) { PARSE_ERROR(filename, lineno, "Invalid UTF-8 string \"%s\".\n", str); if (err) { *err = true; } return ch; } char *tmp = utf8_compose(str); ch = utf8_to_utf32_validate(tmp); if (ch == (uint32_t)-2 || ch == (uint32_t)-1 || *utf8_next_char(tmp) != '\0') { PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as a character.\n", str); if (err) { *err = true; } } free(tmp); return ch; } uint32_t parse_anchor(const char *filename, size_t lineno, const char *str, bool *err) { if(strcasecmp(str, "top-left") == 0) { return ANCHOR_TOP_LEFT; } if (strcasecmp(str, "top") == 0) { return ANCHOR_TOP; } if (strcasecmp(str, "top-right") == 0) { return ANCHOR_TOP_RIGHT; } if (strcasecmp(str, "right") == 0) { return ANCHOR_RIGHT; } if (strcasecmp(str, "bottom-right") == 0) { return ANCHOR_BOTTOM_RIGHT; } if (strcasecmp(str, "bottom") == 0) { return ANCHOR_BOTTOM; } if (strcasecmp(str, "bottom-left") == 0) { return ANCHOR_BOTTOM_LEFT; } if (strcasecmp(str, "left") == 0) { return ANCHOR_LEFT; } if (strcasecmp(str, "center") == 0) { return ANCHOR_CENTER; } PARSE_ERROR(filename, lineno, "Invalid anchor \"%s\".\n", str); if (err) { *err = true; } return 0; } enum cursor_style parse_cursor_style(const char *filename, size_t lineno, const char *str, bool *err) { if(strcasecmp(str, "bar") == 0) { return CURSOR_STYLE_BAR; } if(strcasecmp(str, "block") == 0) { return CURSOR_STYLE_BLOCK; } if(strcasecmp(str, "underscore") == 0) { return CURSOR_STYLE_UNDERSCORE; } PARSE_ERROR(filename, lineno, "Invalid cursor style \"%s\".\n", str); if (err) { *err = true; } return 0; } struct color parse_color(const char *filename, size_t lineno, const char *str, bool *err) { struct color color = hex_to_color(str); if (color.r == -1) { PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as a color.\n", str); if (err) { *err = true; } } return color; } uint32_t parse_uint32(const char *filename, size_t lineno, const char *str, bool *err) { errno = 0; char *endptr; int64_t ret = strtoul(str, &endptr, 0); if (endptr == str || *endptr != '\0') { PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as unsigned int.\n", str); if (err) { *err = true; } } else if (errno || ret < 0 || ret > UINT32_MAX) { PARSE_ERROR(filename, lineno, "Unsigned int value \"%s\" out of range.\n", str); if (err) { *err = true; } } return ret; } int32_t parse_int32(const char *filename, size_t lineno, const char *str, bool *err) { errno = 0; char *endptr; int64_t ret = strtol(str, &endptr, 0); if (endptr == str || *endptr != '\0') { PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as int.\n", str); if (err) { *err = true; } } else if (errno || ret < INT32_MIN || ret > INT32_MAX) { PARSE_ERROR(filename, lineno, "Int value \"%s\" out of range.\n", str); if (err) { *err = true; } } return ret; } struct uint32_percent parse_uint32_percent(const char *filename, size_t lineno, const char *str, bool *err) { errno = 0; char *endptr; int64_t val = strtoul(str, &endptr, 0); bool percent = false; if (endptr == str || (*endptr != '\0' && *endptr != '%')) { PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as unsigned int.\n", str); if (err) { *err = true; } } else if (errno || val < 0 || val > UINT32_MAX) { PARSE_ERROR(filename, lineno, "Unsigned int value \"%s\" out of range.\n", str); if (err) { *err = true; } } if (!err || !*err) { if (*endptr == '%') { percent = true; } } return (struct uint32_percent){ val, percent }; } struct directional parse_directional(const char *filename, size_t lineno, const char *str, bool *err) { /* One extra value to act as a sentinel for too many values in str. */ int32_t values[5]; char *saveptr = NULL; char *tmp = xstrdup(str); char *val = strtok_r(tmp, ",", &saveptr); size_t n; for (n = 0; n < N_ELEM(values) && val != NULL; n++) { values[n] = parse_int32(filename, lineno, val, err); if (err && *err) { break; } val = strtok_r(NULL, ",", &saveptr); } free(tmp); struct directional ret = {0}; if (err && *err) { return ret; } switch (n) { case 0: break; case 1: ret = (struct directional) { .top = values[0], .right = values[0], .bottom = values[0], .left = values[0], }; break; case 2: ret = (struct directional) { .top = values[0], .right = values[1], .bottom = values[0], .left = values[1], }; break; case 3: ret = (struct directional) { .top = values[0], .right = values[1], .bottom = values[2], .left = values[1], }; break; case 4: ret = (struct directional) { .top = values[0], .right = values[1], .bottom = values[2], .left = values[3], }; break; default: PARSE_ERROR(filename, lineno, "Too many values in \"%s\" for directional.\n", str); if (err) { *err = true; } break; }; return ret; } tofi-0.9.1/src/config.h000066400000000000000000000004461441474151400147000ustar00rootroot00000000000000#ifndef TOFI_CONFIG_H #define TOFI_CONFIG_H #include #include "tofi.h" void config_load(struct tofi *tofi, const char *filename); bool config_apply(struct tofi *tofi, const char *option, const char *value); void config_fixup_values(struct tofi *tofi); #endif /* TOFI_CONFIG_H */ tofi-0.9.1/src/desktop_vec.c000066400000000000000000000271041441474151400157340ustar00rootroot00000000000000#include #include #include "desktop_vec.h" #include "fuzzy_match.h" #include "log.h" #include "string_vec.h" #include "unicode.h" #include "xmalloc.h" static bool match_current_desktop(char * const *desktop_list, gsize length); [[nodiscard("memory leaked")]] struct desktop_vec desktop_vec_create(void) { struct desktop_vec vec = { .count = 0, .size = 128, .buf = xcalloc(128, sizeof(*vec.buf)), }; return vec; } void desktop_vec_destroy(struct desktop_vec *restrict vec) { for (size_t i = 0; i < vec->count; i++) { free(vec->buf[i].id); free(vec->buf[i].name); free(vec->buf[i].path); free(vec->buf[i].keywords); } free(vec->buf); } void desktop_vec_add( struct desktop_vec *restrict vec, const char *restrict id, const char *restrict name, const char *restrict path, const char *restrict keywords) { if (vec->count == vec->size) { vec->size *= 2; vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); } vec->buf[vec->count].id = xstrdup(id); vec->buf[vec->count].name = utf8_normalize(name); if (vec->buf[vec->count].name == NULL) { vec->buf[vec->count].name = xstrdup(name); } vec->buf[vec->count].path = xstrdup(path); vec->buf[vec->count].keywords = xstrdup(keywords); vec->buf[vec->count].search_score = 0; vec->buf[vec->count].history_score = 0; vec->count++; } void desktop_vec_add_file(struct desktop_vec *vec, const char *id, const char *path) { GKeyFile *file = g_key_file_new(); if (!g_key_file_load_from_file(file, path, G_KEY_FILE_NONE, NULL)) { log_error("Failed to open %s.\n", path); return; } const char *group = "Desktop Entry"; if (g_key_file_get_boolean(file, group, "Hidden", NULL) || g_key_file_get_boolean(file, group, "NoDisplay", NULL)) { goto cleanup_file; } char *name = g_key_file_get_locale_string(file, group, "Name", NULL, NULL); if (name == NULL) { log_error("%s: No name found.\n", path); goto cleanup_file; } /* * This is really a list rather than a string, but for the purposes of * matching against user input it's easier to just keep it as a string. */ char *keywords = g_key_file_get_locale_string(file, group, "Keywords", NULL, NULL); if (keywords == NULL) { keywords = xmalloc(1); *keywords = '\0'; } gsize length; gchar **list = g_key_file_get_string_list(file, group, "OnlyShowIn", &length, NULL); if (list) { bool match = match_current_desktop(list, length); g_strfreev(list); list = NULL; if (!match) { goto cleanup_all; } } list = g_key_file_get_string_list(file, group, "NotShowIn", &length, NULL); if (list) { bool match = match_current_desktop(list, length); g_strfreev(list); list = NULL; if (match) { goto cleanup_all; } } desktop_vec_add(vec, id, name, path, keywords); cleanup_all: free(keywords); free(name); cleanup_file: g_key_file_unref(file); } static int cmpdesktopp(const void *restrict a, const void *restrict b) { struct desktop_entry *restrict d1 = (struct desktop_entry *)a; struct desktop_entry *restrict d2 = (struct desktop_entry *)b; return strcmp(d1->name, d2->name); } static int cmpscorep(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; int hist_diff = str2->history_score - str1->history_score; int search_diff = str2->search_score - str1->search_score; return hist_diff + search_diff; } void desktop_vec_sort(struct desktop_vec *restrict vec) { qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmpdesktopp); } struct desktop_entry *desktop_vec_find_sorted(struct desktop_vec *restrict vec, const char *name) { /* * Explicitly cast away const-ness, as even though we won't modify the * name, the compiler rightly complains that we might. */ struct desktop_entry tmp = { .name = (char *)name }; return bsearch(&tmp, vec->buf, vec->count, sizeof(vec->buf[0]), cmpdesktopp); } struct string_ref_vec desktop_vec_filter( const struct desktop_vec *restrict vec, const char *restrict substr, bool fuzzy) { struct string_ref_vec filt = string_ref_vec_create(); for (size_t i = 0; i < vec->count; i++) { int32_t search_score; if (fuzzy) { search_score = fuzzy_match_words(substr, vec->buf[i].name); } else { search_score = fuzzy_match_simple_words(substr, vec->buf[i].name); } if (search_score != INT32_MIN) { string_ref_vec_add(&filt, vec->buf[i].name); /* * Store the position of the match in the string as * its search_score, for later sorting. */ filt.buf[filt.count - 1].search_score = search_score; filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; } else { /* If we didn't match the name, check the keywords. */ if (fuzzy) { search_score = fuzzy_match_words(substr, vec->buf[i].keywords); } else { search_score = fuzzy_match_simple_words(substr, vec->buf[i].keywords); } if (search_score != INT32_MIN) { string_ref_vec_add(&filt, vec->buf[i].name); /* * Arbitrary score addition to make name * matches preferred over keyword matches. */ filt.buf[filt.count - 1].search_score = search_score - 20; filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; } } } /* * Sort the results by this search_score. This moves matches at the beginnings * of words to the front of the result list. */ qsort(filt.buf, filt.count, sizeof(filt.buf[0]), cmpscorep); return filt; } struct desktop_vec desktop_vec_load(FILE *file) { struct desktop_vec vec = desktop_vec_create(); if (file == NULL) { return vec; } ssize_t bytes_read; char *line = NULL; size_t len; while ((bytes_read = getline(&line, &len, file)) != -1) { if (line[bytes_read - 1] == '\n') { line[bytes_read - 1] = '\0'; } char *id = line; size_t sublen = strlen(line); char *name = &line[sublen + 1]; sublen = strlen(name); char *path = &name[sublen + 1]; sublen = strlen(path); char *keywords = &path[sublen + 1]; desktop_vec_add(&vec, id, name, path, keywords); } free(line); return vec; } void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file) { /* * Using null bytes for field separators is a bit odd, but it makes * parsing very quick and easy. */ for (size_t i = 0; i < vec->count; i++) { fputs(vec->buf[i].id, file); fputc('\0', file); fputs(vec->buf[i].name, file); fputc('\0', file); fputs(vec->buf[i].path, file); fputc('\0', file); fputs(vec->buf[i].keywords, file); fputc('\n', file); } } bool match_current_desktop(char * const *desktop_list, gsize length) { const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); if (xdg_current_desktop == NULL) { return false; } struct string_vec desktops = string_vec_create(); char *saveptr = NULL; char *tmp = xstrdup(xdg_current_desktop); char *desktop = strtok_r(tmp, ":", &saveptr); while (desktop != NULL) { string_vec_add(&desktops, desktop); desktop = strtok_r(NULL, ":", &saveptr); } string_vec_sort(&desktops); for (gsize i = 0; i < length; i++) { if (string_vec_find_sorted(&desktops, desktop_list[i])) { return true; } } string_vec_destroy(&desktops); free(tmp); return false; } /* * Checking-in commented-out code is generally bad practice, but this may be * needed in the near future. Using the various GKeyFile functions above * ensures correct behaviour, but is relatively slow (~3-4 ms for 60 desktop * files). Below are some quick and dirty replacement functions, which work * correctly except for name localisation, and are ~4x faster. If we go a while * without needing these, they should be deleted. */ // static char *strip(const char *str) // { // size_t start = 0; // size_t end = strlen(str); // while (start <= end && isspace(str[start])) { // start++; // } // if (start == end) { // return NULL; // } // while (end > start && (isspace(str[end]) || str[end] == '\0')) { // end--; // } // if (end < start) { // return NULL; // } // if (str[start] == '"' && str[end] == '"' && end > start) { // start++; // end--; // } // size_t len = end - start + 1; // char *buf = xcalloc(len + 1, 1); // strncpy(buf, str + start, len); // buf[len] = '\0'; // return buf; // } // // static char *get_option(const char *line) // { // size_t index = 0; // while (line[index] != '=' && index < strlen(line)) { // index++; // } // if (index >= strlen(line)) { // return NULL; // } // index++; // while (isspace(line[index]) && index < strlen(line)) { // index++; // } // if (index >= strlen(line)) { // return NULL; // } // return strip(&line[index]); // } // static bool match_current_desktop2(const char *desktop_list) // { // const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); // if (xdg_current_desktop == NULL) { // return false; // } // // struct string_vec desktops = string_vec_create(); // // char *saveptr = NULL; // char *tmp = xstrdup(xdg_current_desktop); // char *desktop = strtok_r(tmp, ":", &saveptr); // while (desktop != NULL) { // string_vec_add(&desktops, desktop); // desktop = strtok_r(NULL, ":", &saveptr); // } // free(tmp); // // /* // * Technically this will fail if the desktop list contains an escaped // * \;, but I don't know of any desktops with semicolons in their names. // */ // saveptr = NULL; // tmp = xstrdup(desktop_list); // desktop = strtok_r(tmp, ";", &saveptr); // while (desktop != NULL) { // if (string_vec_find_sorted(&desktops, desktop)) { // return true; // } // desktop = strtok_r(NULL, ";", &saveptr); // } // free(tmp); // // string_vec_destroy(&desktops); // return false; // } // // static void desktop_vec_add_file2(struct desktop_vec *desktop, const char *id, const char *path) // { // FILE *file = fopen(path, "rb"); // if (!file) { // log_error("Failed to open %s.\n", path); // return; // } // // char *line = NULL; // size_t len; // bool found = false; // while(getline(&line, &len, file) > 0) { // if (!strncmp(line, "[Desktop Entry]", strlen("[Desktop Entry]"))) { // found = true; // break; // } // } // if (!found) { // log_error("%s: No [Desktop Entry] section found.\n", path); // goto cleanup_file; // } // // /* Please forgive the macro usage. */ // #define OPTION(key) (!strncmp(line, (key), strlen((key)))) // char *name = NULL; // found = false; // while(getline(&line, &len, file) > 0) { // /* We've left the [Desktop Entry] section, stop parsing. */ // if (line[0] == '[') { // break; // } // if (OPTION("Name")) { // if (line[4] == ' ' || line[4] == '=') { // found = true; // name = get_option(line); // } // } else if (OPTION("Hidden") // || OPTION("NoDisplay")) { // char *option = get_option(line); // if (option != NULL) { // bool match = !strcmp(option, "true"); // free(option); // if (match) { // goto cleanup_file; // } // } // } else if (OPTION("OnlyShowIn")) { // char *option = get_option(line); // if (option != NULL) { // bool match = match_current_desktop2(option); // free(option); // if (!match) { // goto cleanup_file; // } // } // } else if (OPTION("NotShowIn")) { // char *option = get_option(line); // if (option != NULL) { // bool match = match_current_desktop2(option); // free(option); // if (match) { // goto cleanup_file; // } // } // } // } // if (!found) { // log_error("%s: No name found.\n", path); // goto cleanup_name; // } // if (name == NULL) { // log_error("%s: Malformed name key.\n", path); // goto cleanup_file; // } // // desktop_vec_add(desktop, id, name, path); // // cleanup_name: // free(name); // cleanup_file: // free(line); // fclose(file); // } tofi-0.9.1/src/desktop_vec.h000066400000000000000000000022411441474151400157340ustar00rootroot00000000000000#ifndef DESKTOP_VEC_H #define DESKTOP_VEC_H #include #include #include #include struct desktop_entry { char *id; char *name; char *path; char *keywords; uint32_t search_score; uint32_t history_score; }; struct desktop_vec { size_t count; size_t size; struct desktop_entry *buf; }; [[nodiscard("memory leaked")]] struct desktop_vec desktop_vec_create(void); void desktop_vec_destroy(struct desktop_vec *restrict vec); void desktop_vec_add( struct desktop_vec *restrict vec, const char *restrict id, const char *restrict name, const char *restrict path, const char *restrict keywords); void desktop_vec_add_file(struct desktop_vec *desktop, const char *id, const char *path); void desktop_vec_sort(struct desktop_vec *restrict vec); struct desktop_entry *desktop_vec_find_sorted(struct desktop_vec *restrict vec, const char *name); struct string_ref_vec desktop_vec_filter( const struct desktop_vec *restrict vec, const char *restrict substr, bool fuzzy); struct desktop_vec desktop_vec_load(FILE *file); void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file); #endif /* DESKTOP_VEC_H */ tofi-0.9.1/src/drun.c000066400000000000000000000247011441474151400143760ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include "drun.h" #include "history.h" #include "log.h" #include "mkdirp.h" #include "string_vec.h" #include "xmalloc.h" static const char *default_data_dir = ".local/share/"; static const char *default_cache_dir = ".cache/"; static const char *cache_basename = "tofi-drun"; [[nodiscard("memory leaked")]] static char *get_cache_path() { char *cache_name = NULL; const char *state_path = getenv("XDG_CACHE_HOME"); if (state_path == NULL) { const char *home = getenv("HOME"); if (home == NULL) { log_error("Couldn't retrieve HOME from environment.\n"); return NULL; } size_t len = strlen(home) + 1 + strlen(default_cache_dir) + 1 + strlen(cache_basename) + 1; cache_name = xmalloc(len); snprintf( cache_name, len, "%s/%s/%s", home, default_cache_dir, cache_basename); } else { size_t len = strlen(state_path) + 1 + strlen(cache_basename) + 1; cache_name = xmalloc(len); snprintf( cache_name, len, "%s/%s", state_path, cache_basename); } return cache_name; } [[nodiscard("memory leaked")]] static struct string_vec get_application_paths() { char *base_paths = NULL; const char *xdg_data_dirs = getenv("XDG_DATA_DIRS"); if (xdg_data_dirs == NULL) { xdg_data_dirs = "/usr/local/share/:/usr/share/"; } const char *xdg_data_home = getenv("XDG_DATA_HOME"); if (xdg_data_home == NULL) { const char *home = getenv("HOME"); if (home == NULL) { log_error("Couldn't retrieve HOME from environment.\n"); exit(EXIT_FAILURE); } size_t len = strlen(home) + 1 + strlen(default_data_dir) + 1 + strlen(xdg_data_dirs) + 1; base_paths = xmalloc(len); snprintf( base_paths, len, "%s/%s:%s", home, default_data_dir, xdg_data_dirs); } else { size_t len = strlen(xdg_data_home) + 1 + strlen(xdg_data_dirs) + 1; base_paths = xmalloc(len); snprintf( base_paths, len, "%s:%s", xdg_data_home, xdg_data_dirs); } /* Append /applications/ to each entry. */ struct string_vec paths = string_vec_create(); char *saveptr = NULL; char *path_entry = strtok_r(base_paths, ":", &saveptr); while (path_entry != NULL) { const char *subdir = "applications/"; size_t len = strlen(path_entry) + 1 + strlen(subdir) + 1; char *apps = xmalloc(len); snprintf(apps, len, "%s/%s", path_entry, subdir); string_vec_add(&paths, apps); free(apps); path_entry = strtok_r(NULL, ":", &saveptr); } free(base_paths); return paths; } static void parse_desktop_file(gpointer key, gpointer value, void *data) { const char *id = key; const char *path = value; struct desktop_vec *apps = data; desktop_vec_add_file(apps, id, path); } struct desktop_vec drun_generate(void) { /* * Note for the future: this custom logic could be replaced with * g_app_info_get_all(), but that's slower. Worth remembering * though if this runs into issues. */ log_debug("Retrieving application dirs.\n"); struct string_vec paths = get_application_paths(); struct string_vec desktop_files = string_vec_create(); log_debug("Scanning for .desktop files.\n"); for (size_t i = 0; i < paths.count; i++) { const char *path_entry = paths.buf[i].string; DIR *dir = opendir(path_entry); if (dir != NULL) { struct dirent *d; while ((d = readdir(dir)) != NULL) { const char *extension = strrchr(d->d_name, '.'); if (extension == NULL) { continue; } if (strcmp(extension, ".desktop")) { continue; } string_vec_add(&desktop_files, d->d_name); } closedir(dir); } } log_debug("Found %zu files.\n", desktop_files.count); log_debug("Parsing .desktop files.\n"); /* * The Desktop Entry Specification says that only the highest * precedence application file with a given ID should be used, so store * the id / path pairs into a hash table to enforce uniqueness. */ GHashTable *id_hash = g_hash_table_new_full(g_str_hash, g_str_equal, free, free); struct desktop_vec apps = desktop_vec_create(); for (size_t i = 0; i < paths.count; i++) { char *path_entry = paths.buf[i].string; char *tree[2] = { path_entry, NULL }; size_t prefix_len = strlen(path_entry); FTS *fts = fts_open(tree, FTS_LOGICAL, NULL); FTSENT *entry = fts_read(fts); for (; entry != NULL; entry = fts_read(fts)) { const char *extension = strrchr(entry->fts_name, '.'); if (extension == NULL) { continue; } if (strcmp(extension, ".desktop")) { continue; } char *id = xstrdup(&entry->fts_path[prefix_len]); char *slash = strchr(id, '/'); while (slash != NULL) { *slash = '-'; slash = strchr(slash, '/'); } /* * We're iterating from highest to lowest precedence, * so only the first file with a given ID should be * stored. */ if (!g_hash_table_contains(id_hash, id)) { char *path = xstrdup(entry->fts_path); g_hash_table_insert(id_hash, id, path); } else { free(id); } } fts_close(fts); } /* Parse the remaining files into our desktop_vec. */ g_hash_table_foreach(id_hash, parse_desktop_file, &apps); g_hash_table_unref(id_hash); log_debug("Found %zu apps.\n", apps.count); /* * It's now safe to sort the desktop file vector, as the rules about * file precedence have been taken care of. */ log_debug("Sorting results.\n"); desktop_vec_sort(&apps); string_vec_destroy(&desktop_files); string_vec_destroy(&paths); return apps; } struct desktop_vec drun_generate_cached() { log_debug("Retrieving cache location.\n"); char *cache_path = get_cache_path(); struct stat sb; if (cache_path == NULL) { return drun_generate(); } /* If the cache doesn't exist, create it and return */ errno = 0; if (stat(cache_path, &sb) == -1) { if (errno == ENOENT) { struct desktop_vec apps = drun_generate(); if (!mkdirp(cache_path)) { free(cache_path); return apps; } FILE *cache = fopen(cache_path, "wb"); desktop_vec_save(&apps, cache); fclose(cache); free(cache_path); return apps; } free(cache_path); return drun_generate(); } log_debug("Retrieving application dirs.\n"); struct string_vec application_path = get_application_paths();; /* The cache exists, so check if it's still in date */ bool out_of_date = false; for (size_t i = 0; i < application_path.count; i++) { struct stat path_sb; if (stat(application_path.buf[i].string, &path_sb) == 0) { if (path_sb.st_mtim.tv_sec > sb.st_mtim.tv_sec) { out_of_date = true; break; } } } string_vec_destroy(&application_path); struct desktop_vec apps; if (out_of_date) { log_debug("Cache out of date, updating.\n"); log_indent(); apps = drun_generate(); log_unindent(); FILE *cache = fopen(cache_path, "wb"); desktop_vec_save(&apps, cache); fclose(cache); } else { log_debug("Cache up to date, loading.\n"); FILE *cache = fopen(cache_path, "rb"); apps = desktop_vec_load(cache); fclose(cache); } free(cache_path); return apps; } void drun_print(const char *filename, const char *terminal_command) { GKeyFile *file = g_key_file_new(); if (!g_key_file_load_from_file(file, filename, G_KEY_FILE_NONE, NULL)) { log_error("Failed to open %s.\n", filename); return; } const char *group = "Desktop Entry"; char *exec = g_key_file_get_string(file, group, "Exec", NULL); if (exec == NULL) { log_error("Failed to get Exec key from %s.\n", filename); g_key_file_unref(file); return; } /* * Build a string vector from the command line, replacing % field codes * with the appropriate values. */ struct string_vec pieces = string_vec_create(); char *search = exec; char *last = search; while ((search = strchr(search, '%')) != NULL) { /* Add the string up to here to our vector. */ search[0] = '\0'; string_vec_add(&pieces, last); switch (search[1]) { case 'i': if (g_key_file_has_key(file, group, "Icon", NULL)) { string_vec_add(&pieces, "--icon "); string_vec_add(&pieces, g_key_file_get_string(file, group, "Icon", NULL)); } break; case 'c': string_vec_add(&pieces, g_key_file_get_locale_string(file, group, "Name", NULL, NULL)); break; case 'k': string_vec_add(&pieces, filename); break; } search += 2; last = search; } string_vec_add(&pieces, last); /* * If this is a terminal application, the command line needs to be * preceded by the terminal command. */ bool terminal = g_key_file_get_boolean(file, group, "Terminal", NULL); if (terminal) { if (terminal_command[0] == '\0') { log_warning("Terminal application launched, but no terminal is set.\n"); log_warning("This probably isn't what you want.\n"); log_warning("See the --terminal option documentation in the man page.\n"); } else { fputs(terminal_command, stdout); fputc(' ', stdout); } } /* Build the command line from our vector. */ for (size_t i = 0; i < pieces.count; i++) { fputs(pieces.buf[i].string, stdout); } fputc('\n', stdout); string_vec_destroy(&pieces); free(exec); g_key_file_unref(file); } void drun_launch(const char *filename) { GDesktopAppInfo *info = g_desktop_app_info_new_from_filename(filename); GAppLaunchContext *context = g_app_launch_context_new(); GError *err = NULL; if (!g_app_info_launch((GAppInfo *)info, NULL, context, &err)) { log_error("Failed to launch %s.\n", filename); log_error("%s.\n", err->message); log_error( "If this is a terminal issue, you can use `--drun-launch=false`,\n" " and pass your preferred terminal command to `--terminal`.\n" " For more information, see https://gitlab.gnome.org/GNOME/glib/-/issues/338\n" " and https://github.com/philj56/tofi/issues/46.\n"); } g_clear_error(&err); g_object_unref(context); g_object_unref(info); } static int cmpscorep(const void *restrict a, const void *restrict b) { struct desktop_entry *restrict app1 = (struct desktop_entry *)a; struct desktop_entry *restrict app2 = (struct desktop_entry *)b; return app2->history_score - app1->history_score; } void drun_history_sort(struct desktop_vec *apps, struct history *history) { log_debug("Moving already known apps to the front.\n"); for (size_t i = 0; i < history->count; i++) { struct desktop_entry *res = desktop_vec_find_sorted(apps, history->buf[i].name); if (res == NULL) { continue; } res->history_score = history->buf[i].run_count; } qsort(apps->buf, apps->count, sizeof(apps->buf[0]), cmpscorep); } tofi-0.9.1/src/drun.h000066400000000000000000000006121441474151400143760ustar00rootroot00000000000000#ifndef DRUN_H #define DRUN_H #include "desktop_vec.h" #include "history.h" #include "string_vec.h" struct desktop_vec drun_generate(void); struct desktop_vec drun_generate_cached(void); void drun_history_sort(struct desktop_vec *apps, struct history *history); void drun_print(const char *filename, const char *terminal_command); void drun_launch(const char *filename); #endif /* DRUN_H */ tofi-0.9.1/src/entry.c000066400000000000000000000216441441474151400145720ustar00rootroot00000000000000#include #include #include #include "entry.h" #include "log.h" #include "nelem.h" #include "scale.h" #undef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint32_t r) { cairo_new_path(cr); /* Top-left */ cairo_arc(cr, r, r, r, -M_PI, -M_PI_2); /* Top-right */ cairo_arc(cr, width - r, r, r, -M_PI_2, 0); /* Bottom-right */ cairo_arc(cr, width - r, height - r, r, 0, M_PI_2); /* Bottom-left */ cairo_arc(cr, r, height - r, r, M_PI_2, M_PI); cairo_close_path(cr); } static void apply_text_theme_fallback(struct text_theme *theme, const struct text_theme *fallback) { if (!theme->foreground_specified) { theme->foreground_color = fallback->foreground_color; } if (!theme->background_specified) { theme->background_color = fallback->background_color; } if (!theme->padding_specified) { theme->padding = fallback->padding; } if (!theme->radius_specified) { theme->background_corner_radius = fallback->background_corner_radius; } } void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height, uint32_t fractional_scale_numerator) { double scale = fractional_scale_numerator / 120.; /* * Create the cairo surfaces and contexts we'll be using. * * In order to avoid an unnecessary copy when passing the image to the * Wayland server, we accept a pointer to the mmap-ed file that our * Wayland buffers are created from. This is assumed to be * (width * height * (sizeof(uint32_t) == 4) * 2) bytes, * to allow for double buffering. */ log_debug("Creating %u x %u Cairo surface with scale factor %.3lf.\n", width, height, fractional_scale_numerator / 120.); cairo_surface_t *surface = cairo_image_surface_create_for_data( buffer, CAIRO_FORMAT_ARGB32, width, height, width * sizeof(uint32_t) ); cairo_surface_set_device_scale(surface, scale, scale); cairo_t *cr = cairo_create(surface); entry->cairo[0].surface = surface; entry->cairo[0].cr = cr; entry->cairo[1].surface = cairo_image_surface_create_for_data( &buffer[width * height * sizeof(uint32_t)], CAIRO_FORMAT_ARGB32, width, height, width * sizeof(uint32_t) ); cairo_surface_set_device_scale(entry->cairo[1].surface, scale, scale); entry->cairo[1].cr = cairo_create(entry->cairo[1].surface); /* If we're scaling with Cairo, remember to account for that here. */ width = scale_apply_inverse(width, fractional_scale_numerator); height = scale_apply_inverse(height, fractional_scale_numerator); log_debug("Drawing window.\n"); /* Draw the background */ struct color color = entry->background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_paint(cr); /* Draw the border with outlines */ cairo_set_line_width(cr, 4 * entry->outline_width + 2 * entry->border_width); rounded_rectangle(cr, width, height, entry->corner_radius); color = entry->outline_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_stroke_preserve(cr); color = entry->border_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_set_line_width(cr, 2 * entry->outline_width + 2 * entry->border_width); cairo_stroke_preserve(cr); color = entry->outline_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_set_line_width(cr, 2 * entry->outline_width); cairo_stroke_preserve(cr); /* Clear the overdrawn bits outside of the rounded corners */ /* * N.B. the +1's shouldn't be required, but certain fractional scale * factors can otherwise cause 1-pixel artifacts on the edges * (presumably because Cairo is performing rounding differently to us * at some point). */ cairo_rectangle(cr, 0, 0, width + 1, height + 1); cairo_set_source_rgba(cr, 0, 0, 0, 1); cairo_save(cr); cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR); cairo_fill(cr); cairo_restore(cr); cairo_set_operator(cr, CAIRO_OPERATOR_OVER); /* Move and clip following draws to be within this outline */ double dx = 2.0 * entry->outline_width + entry->border_width; cairo_translate(cr, dx, dx); width -= 2 * dx; height -= 2 * dx; /* If we're clipping to the padding, account for that as well here */ if (entry->clip_to_padding) { cairo_translate(cr, entry->padding_left, entry->padding_top); width -= entry->padding_left + entry->padding_right; height -= entry->padding_top + entry->padding_bottom; } /* Account for rounded corners */ double inner_radius = (double)entry->corner_radius - dx; inner_radius = MAX(inner_radius, 0); dx = ceil(inner_radius * (1.0 - 1.0 / M_SQRT2)); cairo_translate(cr, dx, dx); width -= 2 * dx; height -= 2 * dx; cairo_rectangle(cr, 0, 0, width, height); cairo_clip(cr); /* Store the clip rectangle width and height. */ cairo_matrix_t mat; cairo_get_matrix(cr, &mat); entry->clip_x = mat.x0; entry->clip_y = mat.y0; entry->clip_width = width; entry->clip_height = height; /* * If we're not clipping to the padding, we didn't account for it * before. */ if (!entry->clip_to_padding) { cairo_translate(cr, entry->padding_left, entry->padding_top); } /* Setup the backend. */ if (access(entry->font_name, R_OK) != 0) { /* * We've been given a font name rather than path, * so fallback to Pango */ entry->use_pango = true; } if (entry->use_pango) { entry_backend_pango_init(entry, &width, &height); } else { entry_backend_harfbuzz_init(entry, &width, &height); } /* * Before we render any text, ensure all text themes are fully * specified. */ const struct text_theme default_theme = { .foreground_color = entry->foreground_color, .background_color = (struct color) { .a = 0 }, .padding = (struct directional) {0}, .background_corner_radius = 0 }; apply_text_theme_fallback(&entry->prompt_theme, &default_theme); apply_text_theme_fallback(&entry->input_theme, &default_theme); apply_text_theme_fallback(&entry->placeholder_theme, &default_theme); apply_text_theme_fallback(&entry->default_result_theme, &default_theme); apply_text_theme_fallback(&entry->alternate_result_theme, &entry->default_result_theme); apply_text_theme_fallback(&entry->selection_theme, &default_theme); /* The cursor is a special case, as it just needs the input colours. */ if (!entry->cursor_theme.color_specified) { entry->cursor_theme.color = entry->input_theme.foreground_color; } if (!entry->cursor_theme.text_color_specified) { entry->cursor_theme.text_color = entry->background_color; } /* * Perform an initial render of the text. * This is done here rather than by calling entry_update to avoid the * unnecessary cairo_paint() of the background for the first frame, * which can be slow for large (e.g. fullscreen) windows. */ log_debug("Initial text render.\n"); if (entry->use_pango) { entry_backend_pango_update(entry); } else { entry_backend_harfbuzz_update(entry); } entry->index = !entry->index; /* * To avoid performing all this drawing twice, we take a small * shortcut. After performing all the drawing as normal on our first * Cairo context, we can copy over just the important state (the * transformation matrix and clip rectangle) and perform a memcpy() * to initialise the other context. * * This memcpy can pretty expensive however, and isn't needed until * we need to draw our second buffer (i.e. when the user presses a * key). In order to minimise startup time, the memcpy() isn't * performed here, but instead happens later, just after the first * frame has been displayed on screen (and while the user is unlikely * to press another key for the <10ms it takes to memcpy). */ cairo_set_matrix(entry->cairo[1].cr, &mat); cairo_rectangle(entry->cairo[1].cr, 0, 0, width, height); cairo_clip(entry->cairo[1].cr); /* * If we're not clipping to the padding, the transformation matrix * didn't include it, so account for it here. */ if (!entry->clip_to_padding) { cairo_translate(entry->cairo[1].cr, entry->padding_left, entry->padding_top); } } void entry_destroy(struct entry *entry) { if (entry->use_pango) { entry_backend_pango_destroy(entry); } else { entry_backend_harfbuzz_destroy(entry); } cairo_destroy(entry->cairo[0].cr); cairo_destroy(entry->cairo[1].cr); cairo_surface_destroy(entry->cairo[0].surface); cairo_surface_destroy(entry->cairo[1].surface); } void entry_update(struct entry *entry) { log_debug("Start rendering entry.\n"); cairo_t *cr = entry->cairo[entry->index].cr; /* Clear the image. */ struct color color = entry->background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_save(cr); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_paint(cr); cairo_restore(cr); /* Draw our text. */ if (entry->use_pango) { entry_backend_pango_update(entry); } else { entry_backend_harfbuzz_update(entry); } log_debug("Finish rendering entry.\n"); entry->index = !entry->index; } tofi-0.9.1/src/entry.h000066400000000000000000000063171441474151400145770ustar00rootroot00000000000000#ifndef ENTRY_H #define ENTRY_H #include "entry_backend/pango.h" #include "entry_backend/harfbuzz.h" #include #include #include "color.h" #include "desktop_vec.h" #include "history.h" #include "surface.h" #include "string_vec.h" #define MAX_INPUT_LENGTH 256 #define MAX_PROMPT_LENGTH 256 #define MAX_FONT_NAME_LENGTH 256 #define MAX_FONT_FEATURES_LENGTH 128 #define MAX_FONT_VARIATIONS_LENGTH 128 enum cursor_style { CURSOR_STYLE_BAR, CURSOR_STYLE_BLOCK, CURSOR_STYLE_UNDERSCORE }; struct directional { int32_t top; int32_t right; int32_t bottom; int32_t left; }; struct text_theme { struct color foreground_color; struct color background_color; struct directional padding; uint32_t background_corner_radius; bool foreground_specified; bool background_specified; bool padding_specified; bool radius_specified; }; struct cursor_theme { struct color color; struct color text_color; enum cursor_style style; uint32_t corner_radius; uint32_t thickness; double underline_depth; double em_width; bool color_specified; bool text_color_specified; bool thickness_specified; bool show; }; struct entry { struct entry_backend_harfbuzz harfbuzz; struct entry_backend_pango pango; struct { cairo_surface_t *surface; cairo_t *cr; } cairo[2]; int index; uint32_t input_utf32[MAX_INPUT_LENGTH]; char input_utf8[4*MAX_INPUT_LENGTH]; uint32_t input_utf32_length; uint32_t input_utf8_length; uint32_t cursor_position; uint32_t selection; uint32_t first_result; char *command_buffer; struct string_ref_vec results; struct string_ref_vec commands; struct desktop_vec apps; struct history history; bool use_pango; uint32_t clip_x; uint32_t clip_y; uint32_t clip_width; uint32_t clip_height; /* Options */ bool drun; bool horizontal; bool hide_input; char hidden_character_utf8[6]; uint8_t hidden_character_utf8_length; uint32_t num_results; uint32_t num_results_drawn; uint32_t last_num_results_drawn; int32_t result_spacing; uint32_t font_size; char font_name[MAX_FONT_NAME_LENGTH]; char font_features[MAX_FONT_FEATURES_LENGTH]; char font_variations[MAX_FONT_VARIATIONS_LENGTH]; char prompt_text[MAX_PROMPT_LENGTH]; char placeholder_text[MAX_PROMPT_LENGTH]; uint32_t prompt_padding; uint32_t corner_radius; uint32_t padding_top; uint32_t padding_bottom; uint32_t padding_left; uint32_t padding_right; bool padding_top_is_percent; bool padding_bottom_is_percent; bool padding_left_is_percent; bool padding_right_is_percent; bool clip_to_padding; uint32_t input_width; uint32_t border_width; uint32_t outline_width; struct color foreground_color; struct color background_color; struct color selection_highlight_color; struct color border_color; struct color outline_color; struct cursor_theme cursor_theme; struct text_theme prompt_theme; struct text_theme input_theme; struct text_theme placeholder_theme; struct text_theme default_result_theme; struct text_theme alternate_result_theme; struct text_theme selection_theme; }; void entry_init(struct entry *entry, uint8_t *restrict buffer, uint32_t width, uint32_t height, uint32_t fractional_scale_numerator); void entry_destroy(struct entry *entry); void entry_update(struct entry *entry); #endif /* ENTRY_H */ tofi-0.9.1/src/entry_backend/000077500000000000000000000000001441474151400160665ustar00rootroot00000000000000tofi-0.9.1/src/entry_backend/harfbuzz.c000066400000000000000000000670701441474151400200770ustar00rootroot00000000000000#include #include #include #include #include "harfbuzz.h" #include "../entry.h" #include "../log.h" #include "../nelem.h" #include "../unicode.h" #include "../xmalloc.h" /* * FreeType is normally compiled without error strings, so we have to do this * funky macro trick to get them. See for more details. */ #undef FTERRORS_H_ #define FT_ERRORDEF( e, v, s ) { e, s }, #define FT_ERROR_START_LIST { #define FT_ERROR_END_LIST { 0, NULL } }; const struct { int err_code; const char *err_msg; } ft_errors[] = #include #undef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) #undef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint32_t r) { cairo_new_path(cr); /* Top-left */ cairo_arc(cr, r, r, r, -M_PI, -M_PI_2); /* Top-right */ cairo_arc(cr, width - r, r, r, -M_PI_2, 0); /* Bottom-right */ cairo_arc(cr, width - r, height - r, r, 0, M_PI_2); /* Bottom-left */ cairo_arc(cr, r, height - r, r, M_PI_2, M_PI); cairo_close_path(cr); } static const char *get_ft_error_string(int err_code) { for (size_t i = 0; i < N_ELEM(ft_errors); i++) { if (ft_errors[i].err_code == err_code) { return ft_errors[i].err_msg; } } return "Unknown FT error"; } /* * Cairo / FreeType use 72 Pts per inch, but Pango uses 96 DPI, so we have to * rescale for consistency. */ #define PT_TO_DPI (96.0 / 72.0) /* * hb_buffer_clear_contents also clears some basic script information, so group * them here for convenience. */ static void setup_hb_buffer(hb_buffer_t *buffer) { hb_buffer_set_direction(buffer, HB_DIRECTION_LTR); hb_buffer_set_script(buffer, HB_SCRIPT_LATIN); hb_buffer_set_language(buffer, hb_language_from_string("en", -1)); hb_buffer_set_cluster_level(buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); } /* * Render a hb_buffer with Cairo, and return the extents of the rendered text * in Cairo units. */ static cairo_text_extents_t render_hb_buffer(cairo_t *cr, hb_font_extents_t *font_extents, hb_buffer_t *buffer, double scale) { cairo_save(cr); /* * Cairo uses y-down coordinates, but HarfBuzz uses y-up, so we * shift the text down by its ascent height to compensate. */ cairo_translate(cr, 0, font_extents->ascender / 64.0); unsigned int glyph_count; hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buffer, &glyph_count); hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buffer, &glyph_count); cairo_glyph_t *cairo_glyphs = xmalloc(sizeof(cairo_glyph_t) * glyph_count); double x = 0; double y = 0; for (unsigned int i=0; i < glyph_count; i++) { /* * The coordinates returned by HarfBuzz are in 26.6 fixed-point * format, so we divide by 64.0 (2^6) to get floats. * * For whatever reason, the coordinates are also scaled by * Cairo's scale factor, so we have to also divide by the scale * factor to account for this. */ cairo_glyphs[i].index = glyph_info[i].codepoint; cairo_glyphs[i].x = x + glyph_pos[i].x_offset / 64.0 / scale; cairo_glyphs[i].y = y - glyph_pos[i].y_offset / 64.0 / scale; x += glyph_pos[i].x_advance / 64.0 / scale; y -= glyph_pos[i].y_advance / 64.0 / scale; } cairo_show_glyphs(cr, cairo_glyphs, glyph_count); cairo_text_extents_t extents; cairo_glyph_extents(cr, cairo_glyphs, glyph_count, &extents); /* Account for the shifted baseline in our returned text extents. */ extents.y_bearing += font_extents->ascender / 64.0; free(cairo_glyphs); cairo_restore(cr); return extents; } /* * Clear the harfbuzz buffer, shape some text and render it with Cairo, * returning the extents of the rendered text in Cairo units. */ static cairo_text_extents_t render_text( cairo_t *cr, struct entry_backend_harfbuzz *hb, const char *text) { hb_buffer_clear_contents(hb->hb_buffer); setup_hb_buffer(hb->hb_buffer); hb_buffer_add_utf8(hb->hb_buffer, text, -1, 0, -1); hb_shape(hb->hb_font, hb->hb_buffer, hb->hb_features, hb->num_features); return render_hb_buffer(cr, &hb->hb_font_extents, hb->hb_buffer, hb->scale); } /* * Render the background box for a piece of text with the given theme and text * extents. */ static void render_text_background( cairo_t *cr, struct entry *entry, cairo_text_extents_t extents, const struct text_theme *theme) { struct directional padding = theme->padding; cairo_font_extents_t font_extents = entry->harfbuzz.cairo_font_extents; /* * If any of the padding values are negative, make them just big enough * to fit the available area. This is needed over just making them * bigger than the clip area in order to make rounded corners line up * with the edges nicely. */ cairo_matrix_t mat; cairo_get_matrix(cr, &mat); double base_x = mat.x0 - entry->clip_x + extents.x_bearing; double base_y = mat.y0 - entry->clip_y; /* Need to use doubles to end up on nice pixel boundaries. */ double padding_left = padding.left; double padding_right = padding.right; double padding_top = padding.top; double padding_bottom = padding.bottom; if (padding.left < 0) { padding_left = base_x; } if (padding.right < 0) { padding_right = entry->clip_width - extents.width - base_x; } if (padding.top < 0) { padding_top = base_y; } if (padding.bottom < 0) { padding_bottom = entry->clip_height - font_extents.height - base_y; } cairo_save(cr); struct color color = theme->background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate( cr, -padding_left + extents.x_bearing, -padding_top); rounded_rectangle( cr, ceil(extents.width + padding_left + padding_right), ceil(font_extents.height + padding_top + padding_bottom), theme->background_corner_radius ); cairo_fill(cr); cairo_restore(cr); } /* * Render some text with an optional background box, using settings from the * given theme. */ static cairo_text_extents_t render_text_themed( cairo_t *cr, struct entry *entry, const char *text, const struct text_theme *theme) { struct entry_backend_harfbuzz *hb = &entry->harfbuzz; /* * I previously thought rendering the text to a group, measuring it, * drawing the box on the main canvas and then drawing the group would * be the most efficient way of doing this. I was wrong. * * It turns out to be much quicker to just draw the text to the canvas, * paint over it with the box, and then draw the text again. This is * fine, as long as the box is always bigger than the text (which it is * unless the user sets some extreme values for the corner radius). */ struct color color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_text_extents_t extents = render_text(cr, hb, text); if (theme->background_color.a == 0) { /* No background to draw, we're done. */ return extents; } render_text_background(cr, entry, extents, theme); color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); render_text(cr, hb, text); return extents; } /* * Rendering the input is more complicated when a cursor is involved. * * Firstly, we need to use UTF-32 strings in order to properly position the * cursor when ligatures / combining diacritics are involved. * * Next, we need to do some calculations on the shaped hb_buffer, to work out * where to draw the cursor and how wide it needs to be. We may also need to * make the background wider to account for the cursor. * * Finally, if we're drawing a block-style cursor, we may need to render the * text again to draw the highlighted character in a different colour. */ static cairo_text_extents_t render_input( cairo_t *cr, struct entry_backend_harfbuzz *hb, const uint32_t *text, uint32_t text_length, const struct text_theme *theme, uint32_t cursor_position, const struct cursor_theme *cursor_theme) { cairo_font_extents_t font_extents; cairo_font_extents(cr, &font_extents); struct directional padding = theme->padding; struct color color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); hb_buffer_clear_contents(hb->hb_buffer); setup_hb_buffer(hb->hb_buffer); hb_buffer_add_utf32(hb->hb_buffer, text, -1, 0, -1); hb_shape(hb->hb_font, hb->hb_buffer, hb->hb_features, hb->num_features); cairo_text_extents_t extents = render_hb_buffer(cr, &hb->hb_font_extents, hb->hb_buffer, hb->scale); /* * If the cursor is at the end of text, we need to account for it in * both the size of the background and the returned extents.x_advance. */ double extra_cursor_advance = 0; if (cursor_position == text_length && cursor_theme->show) { switch (cursor_theme->style) { case CURSOR_STYLE_BAR: extra_cursor_advance = cursor_theme->thickness; break; case CURSOR_STYLE_BLOCK: extra_cursor_advance = cursor_theme->em_width; break; case CURSOR_STYLE_UNDERSCORE: extra_cursor_advance = cursor_theme->em_width; break; } extra_cursor_advance += extents.x_advance - extents.x_bearing - extents.width; } /* Draw the background if required. */ if (theme->background_color.a != 0) { cairo_save(cr); color = theme->background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate( cr, floor(-padding.left + extents.x_bearing), -padding.top); rounded_rectangle( cr, ceil(extents.width + extra_cursor_advance + padding.left + padding.right), ceil(font_extents.height + padding.top + padding.bottom), theme->background_corner_radius ); cairo_fill(cr); cairo_restore(cr); color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); render_hb_buffer(cr, &hb->hb_font_extents, hb->hb_buffer, hb->scale); } if (!cursor_theme->show) { /* No cursor to draw, we're done. */ return extents; } double cursor_x; double cursor_width; if (cursor_position == text_length) { /* Cursor is at the end of text, no calculations to be done. */ cursor_x = extents.x_advance; cursor_width = cursor_theme->em_width; } else { /* * If the cursor is within the text, there's a bit more to do. * * We need to walk through the drawn glyphs, advancing the * cursor by the appropriate amount as we go. This is * complicated by the potential presence of ligatures, which * mean the cursor could be located part way through a single * glyph. * * To determine the appropriate width for the block and * underscore cursors, we then do the same thing again, this * time stopping at the character after the cursor. The width * is then the difference between these two positions. */ unsigned int glyph_count; hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(hb->hb_buffer, &glyph_count); hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(hb->hb_buffer, &glyph_count); int32_t cursor_start = 0; int32_t cursor_end = 0; for (size_t i = 0; i < glyph_count; i++) { uint32_t cluster = glyph_info[i].cluster; int32_t x_advance = glyph_pos[i].x_advance; uint32_t next_cluster = text_length; for (size_t j = i + 1; j < glyph_count; j++) { /* * This needs to be a loop to account for * multiple glyphs sharing the same cluster * (e.g. diacritics). */ if (glyph_info[j].cluster > cluster) { next_cluster = glyph_info[j].cluster; break; } } if (next_cluster > cursor_position) { size_t glyph_clusters = next_cluster - cluster; if (glyph_clusters > 1) { uint32_t diff = cursor_position - cluster; cursor_start += diff * x_advance / glyph_clusters; } break; } cursor_start += x_advance; } for (size_t i = 0; i < glyph_count; i++) { uint32_t cluster = glyph_info[i].cluster; int32_t x_advance = glyph_pos[i].x_advance; uint32_t next_cluster = text_length; for (size_t j = i + 1; j < glyph_count; j++) { if (glyph_info[j].cluster > cluster) { next_cluster = glyph_info[j].cluster; break; } } if (next_cluster > cursor_position + 1) { size_t glyph_clusters = next_cluster - cluster; if (glyph_clusters > 1) { uint32_t diff = cursor_position + 1 - cluster; cursor_end += diff * x_advance / glyph_clusters; } break; } cursor_end += x_advance; } /* Convert from HarfBuzz 26.6 fixed-point to float. */ cursor_x = cursor_start / 64.0 / hb->scale; cursor_width = (cursor_end - cursor_start) / 64.0 / hb->scale; } cairo_save(cr); color = cursor_theme->color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate(cr, cursor_x, 0); switch (cursor_theme->style) { case CURSOR_STYLE_BAR: rounded_rectangle(cr, cursor_theme->thickness, font_extents.height, cursor_theme->corner_radius); cairo_fill(cr); break; case CURSOR_STYLE_BLOCK: rounded_rectangle(cr, cursor_width, font_extents.height, cursor_theme->corner_radius); cairo_fill_preserve(cr); cairo_clip(cr); cairo_translate(cr, -cursor_x, 0); color = cursor_theme->text_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); render_hb_buffer(cr, &hb->hb_font_extents, hb->hb_buffer, hb->scale); break; case CURSOR_STYLE_UNDERSCORE: cairo_translate(cr, 0, cursor_theme->underline_depth); rounded_rectangle(cr, cursor_width, cursor_theme->thickness, cursor_theme->corner_radius); cairo_fill(cr); break; } cairo_restore(cr); extents.x_advance += extra_cursor_advance; return extents; } static bool size_overflows(struct entry *entry, uint32_t width, uint32_t height) { cairo_t *cr = entry->cairo[entry->index].cr; cairo_matrix_t mat; cairo_get_matrix(cr, &mat); if (entry->horizontal) { if (mat.x0 + width > entry->clip_x + entry->clip_width) { return true; } } else { if (mat.y0 + height > entry->clip_y + entry->clip_height) { return true; } } return false; } void entry_backend_harfbuzz_init( struct entry *entry, uint32_t *width, uint32_t *height) { struct entry_backend_harfbuzz *hb = &entry->harfbuzz; cairo_t *cr = entry->cairo[0].cr; uint32_t font_size = floor(entry->font_size * PT_TO_DPI); cairo_surface_get_device_scale(entry->cairo[0].surface, &hb->scale, NULL); /* * Setting up our font has three main steps: * * 1. Load the font face with FreeType. * 2. Create a HarfBuzz font referencing the FreeType font. * 3. Create a Cairo font referencing the FreeType font. * * The simultaneous interaction of Cairo and HarfBuzz with FreeType is * a little finicky, so the order of the last two steps is important. * We use HarfBuzz to set font variation settings (such as weight), if * any. This modifies the underlying FreeType font, so we must create * the Cairo font *after* this point for the changes to take effect. * * This doesn't seem like it should be necessary, as both HarfBuzz and * Cairo reference the same FreeType font, but it is. */ /* Setup FreeType. */ log_debug("Creating FreeType library.\n"); int err; err = FT_Init_FreeType(&hb->ft_library); if (err) { log_error("Error initialising FreeType: %s\n", get_ft_error_string(err)); exit(EXIT_FAILURE); } log_debug("Loading FreeType font.\n"); err = FT_New_Face( hb->ft_library, entry->font_name, 0, &hb->ft_face); if (err) { log_error("Error loading font: %s\n", get_ft_error_string(err)); exit(EXIT_FAILURE); } err = FT_Set_Char_Size( hb->ft_face, font_size * 64, font_size * 64, 0, 0); if (err) { log_error("Error setting font size: %s\n", get_ft_error_string(err)); } log_debug("Creating Harfbuzz font.\n"); hb->hb_font = hb_ft_font_create_referenced(hb->ft_face); if (entry->font_variations[0] != 0) { log_debug("Parsing font variations.\n"); } char *saveptr = NULL; char *variation = strtok_r(entry->font_variations, ",", &saveptr); while (variation != NULL && hb->num_variations < N_ELEM(hb->hb_variations)) { if (hb_variation_from_string(variation, -1, &hb->hb_variations[hb->num_variations])) { hb->num_variations++; } else { log_error("Failed to parse font variation \"%s\".\n", variation); } variation = strtok_r(NULL, ",", &saveptr); } /* * We need to set variations now and update the underlying FreeType * font, as Cairo will then use the FreeType font for drawing. */ hb_font_set_variations(hb->hb_font, hb->hb_variations, hb->num_variations); #ifndef NO_HARFBUZZ_FONT_CHANGED hb_ft_hb_font_changed(hb->hb_font); #endif if (entry->font_features[0] != 0) { log_debug("Parsing font features.\n"); } saveptr = NULL; char *feature = strtok_r(entry->font_features, ",", &saveptr); while (feature != NULL && hb->num_features < N_ELEM(hb->hb_features)) { if (hb_feature_from_string(feature, -1, &hb->hb_features[hb->num_features])) { hb->num_features++; } else { log_error("Failed to parse font feature \"%s\".\n", feature); } feature = strtok_r(NULL, ",", &saveptr); } /* Get some font metrics used for rendering the cursor. */ uint32_t m_codepoint; if (hb_font_get_glyph_from_name(hb->hb_font, "m", -1, &m_codepoint)) { entry->cursor_theme.em_width = hb_font_get_glyph_h_advance(hb->hb_font, m_codepoint) / 64.0; } else { /* If we somehow fail to get an m from the font, just guess. */ entry->cursor_theme.em_width = font_size * 5.0 / 8.0; } hb_font_get_h_extents(hb->hb_font, &hb->hb_font_extents); int32_t underline_depth; #ifdef NO_HARFBUZZ_METRIC_FALLBACK if (!hb_ot_metrics_get_position( hb->hb_font, HB_OT_METRICS_TAG_UNDERLINE_OFFSET, &underline_depth)) { underline_depth = -font_size * 64.0 / 18; } #else hb_ot_metrics_get_position_with_fallback( hb->hb_font, HB_OT_METRICS_TAG_UNDERLINE_OFFSET, &underline_depth); #endif entry->cursor_theme.underline_depth = (hb->hb_font_extents.ascender - underline_depth) / 64.0; if (entry->cursor_theme.style == CURSOR_STYLE_UNDERSCORE && !entry->cursor_theme.thickness_specified) { int32_t thickness; #ifdef NO_HARFBUZZ_METRIC_FALLBACK if (!hb_ot_metrics_get_position( hb->hb_font, HB_OT_METRICS_TAG_UNDERLINE_SIZE, &thickness)) { thickness = font_size * 64.0 / 18; } #else hb_ot_metrics_get_position_with_fallback( hb->hb_font, HB_OT_METRICS_TAG_UNDERLINE_SIZE, &thickness); #endif entry->cursor_theme.thickness = thickness / 64.0; } hb->line_spacing = hb->hb_font_extents.ascender - hb->hb_font_extents.descender + hb->hb_font_extents.line_gap; log_debug("Creating Harfbuzz buffer.\n"); hb->hb_buffer = hb_buffer_create(); log_debug("Creating Cairo font.\n"); hb->cairo_face = cairo_ft_font_face_create_for_ft_face(hb->ft_face, 0); struct color color = entry->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_set_font_face(cr, hb->cairo_face); cairo_set_font_size(cr, font_size); cairo_font_options_t *opts = cairo_font_options_create(); if (hb->disable_hinting) { cairo_font_options_set_hint_style(opts, CAIRO_HINT_STYLE_NONE); } else { cairo_font_options_set_hint_metrics(opts, CAIRO_HINT_METRICS_ON); } cairo_set_font_options(cr, opts); /* We also need to set up the font for our other Cairo context. */ cairo_set_font_face(entry->cairo[1].cr, hb->cairo_face); cairo_set_font_size(entry->cairo[1].cr, font_size); cairo_set_font_options(entry->cairo[1].cr, opts); cairo_font_options_destroy(opts); /* * This is really dumb, but if we don't call cairo_font_extents (or * presumably some similar function) before rendering the text for the * first time, font spacing is all messed up. * * This whole Harfbuzz - Cairo - FreeType thing is a bit finicky to * set up. */ cairo_font_extents(cr, &hb->cairo_font_extents); /* * Cairo changes the size of the font, which sometimes causes rendering * of 'm' characters to mess up (as Harfbuzz has already it when we * measured it). We therefore have to notify Harfbuzz of any potential * changes here. * * In future, the recently-added hb-cairo interface would probably * solve this issue. */ hb_ft_font_changed(hb->hb_font); } void entry_backend_harfbuzz_destroy(struct entry *entry) { hb_buffer_destroy(entry->harfbuzz.hb_buffer); hb_font_destroy(entry->harfbuzz.hb_font); cairo_font_face_destroy(entry->harfbuzz.cairo_face); FT_Done_Face(entry->harfbuzz.ft_face); FT_Done_FreeType(entry->harfbuzz.ft_library); } void entry_backend_harfbuzz_update(struct entry *entry) { cairo_t *cr = entry->cairo[entry->index].cr; cairo_text_extents_t extents; cairo_save(cr); /* Render the prompt */ extents = render_text_themed(cr, entry, entry->prompt_text, &entry->prompt_theme); { struct entry_backend_harfbuzz *hb = &entry->harfbuzz; hb_buffer_clear_contents(hb->hb_buffer); setup_hb_buffer(hb->hb_buffer); hb_buffer_add_utf8(hb->hb_buffer, "test", -1, 0, -1); hb_shape(hb->hb_font, hb->hb_buffer, hb->hb_features, hb->num_features); } cairo_translate(cr, extents.x_advance, 0); cairo_translate(cr, entry->prompt_padding, 0); /* Render the entry text */ if (entry->input_utf32_length == 0) { uint32_t *tmp = utf8_string_to_utf32_string(entry->placeholder_text); extents = render_input( cr, &entry->harfbuzz, tmp, utf32_strlen(tmp), &entry->placeholder_theme, 0, &entry->cursor_theme); free(tmp); } else if (entry->hide_input) { size_t nchars = entry->input_utf32_length; uint32_t *buf = xcalloc(nchars + 1, sizeof(*entry->input_utf32)); uint32_t ch = utf8_to_utf32(entry->hidden_character_utf8); for (size_t i = 0; i < nchars; i++) { buf[i] = ch; } buf[nchars] = U'\0'; extents = render_input( cr, &entry->harfbuzz, buf, entry->input_utf32_length, &entry->input_theme, entry->cursor_position, &entry->cursor_theme); free(buf); } else { extents = render_input( cr, &entry->harfbuzz, entry->input_utf32, entry->input_utf32_length, &entry->input_theme, entry->cursor_position, &entry->cursor_theme); } extents.x_advance = MAX(extents.x_advance, entry->input_width); uint32_t num_results; if (entry->num_results == 0) { num_results = entry->results.count; } else { num_results = MIN(entry->num_results, entry->results.count); } /* Render our results */ size_t i; for (i = 0; i < num_results; i++) { if (entry->horizontal) { cairo_translate(cr, extents.x_advance + entry->result_spacing, 0); } else { cairo_translate(cr, 0, entry->harfbuzz.line_spacing / 64.0 + entry->result_spacing); } if (entry->num_results == 0) { if (size_overflows(entry, 0, 0)) { break; } } else if (i >= entry->num_results) { break; } size_t index = i + entry->first_result; /* * We may be on the last page, which could have fewer results * than expected, so check and break if necessary. */ if (index >= entry->results.count) { break; } const char *result = entry->results.buf[index].string; /* * If this isn't the selected result, or it is but we're not * doing any fancy match-highlighting, just print as normal. */ if (i != entry->selection || (entry->selection_highlight_color.a == 0)) { const struct text_theme *theme; if (i == entry->selection) { theme = &entry->selection_theme; } else if (index % 2) { theme = &entry->alternate_result_theme;; } else { theme = &entry->default_result_theme;; } if (entry->num_results > 0) { /* * We're not auto-detecting how many results we * can fit, so just render the text. */ extents = render_text_themed(cr, entry, result, theme); } else if (!entry->horizontal) { /* * The height of the text doesn't change, so * we don't need to re-measure it each time. */ if (size_overflows(entry, 0, entry->harfbuzz.line_spacing / 64.0)) { break; } else { extents = render_text_themed(cr, entry, result, theme); } } else { /* * The difficult case: we're auto-detecting how * many results to draw, but we can't know * whether this result will fit without * drawing it! To solve this, draw to a * temporary group, measure that, then copy it * to the main canvas only if it will fit. */ cairo_push_group(cr); extents = render_text_themed(cr, entry, result, theme); cairo_pattern_t *group = cairo_pop_group(cr); if (size_overflows(entry, extents.x_advance, 0)) { cairo_pattern_destroy(group); break; } else { cairo_save(cr); cairo_set_source(cr, group); cairo_paint(cr); cairo_restore(cr); cairo_pattern_destroy(group); } } } else { /* * For match highlighting, there's a bit more to do. * * We need to split the text into prematch, match and * postmatch chunks, and draw each separately. * * However, we only want one background box around them * all (if we're drawing one). To do this, we have to * do the rendering part of render_text_themed() * manually, with the same method of: * - Draw the text and measure it * - Draw the box * - Draw the text again * * N.B. The size_overflows check isn't necessary here, * as it's currently not possible for the selection to * do so. */ size_t prematch_len; size_t postmatch_len; char *prematch = xstrdup(result); char *match = NULL; char *postmatch = NULL; if (entry->input_utf8_length > 0 && entry->selection_highlight_color.a != 0) { char *match_pos = utf8_strcasestr(prematch, entry->input_utf8); if (match_pos != NULL) { match = xstrdup(result); prematch_len = (match_pos - prematch); prematch[prematch_len] = '\0'; postmatch_len = strlen(result) - prematch_len - entry->input_utf8_length; if (postmatch_len > 0) { postmatch = xstrdup(result); } match[entry->input_utf8_length + prematch_len] = '\0'; } } for (int pass = 0; pass < 2; pass++) { cairo_save(cr); struct color color = entry->selection_theme.foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_text_extents_t subextents = render_text(cr, &entry->harfbuzz, prematch); extents = subextents; if (match != NULL) { cairo_translate(cr, subextents.x_advance, 0); color = entry->selection_highlight_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); subextents = render_text(cr, &entry->harfbuzz, &match[prematch_len]); if (prematch_len == 0) { extents = subextents; } else { /* * This calculation is a little * complex, but it's basically: * * (distance from leftmost pixel of * prematch to logical end of prematch) * * + * * (distance from logical start of match * to rightmost pixel of match). */ extents.width = extents.x_advance - extents.x_bearing + subextents.x_bearing + subextents.width; extents.x_advance += subextents.x_advance; } } if (postmatch != NULL) { cairo_translate(cr, subextents.x_advance, 0); color = entry->selection_theme.foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); subextents = render_text( cr, &entry->harfbuzz, &postmatch[entry->input_utf8_length + prematch_len]); extents.width = extents.x_advance - extents.x_bearing + subextents.x_bearing + subextents.width; extents.x_advance += subextents.x_advance; } cairo_restore(cr); if (entry->selection_theme.background_color.a == 0) { /* No background box, we're done. */ break; } else if (pass == 0) { /* * First pass, paint over the text with * our background box. */ render_text_background(cr, entry, extents, &entry->selection_theme); } } free(prematch); if (match != NULL) { free(match); } if (postmatch != NULL) { free(postmatch); } } } entry->num_results_drawn = i; log_debug("Drew %zu results.\n", i); cairo_restore(cr); } tofi-0.9.1/src/entry_backend/harfbuzz.h000066400000000000000000000016671441474151400201040ustar00rootroot00000000000000#ifndef ENTRY_BACKEND_HARFBUZZ_H #define ENTRY_BACKEND_HARFBUZZ_H #include #include #include #include FT_FREETYPE_H #include #define MAX_FONT_VARIATIONS 16 #define MAX_FONT_FEATURES 16 struct entry; struct entry_backend_harfbuzz { FT_Library ft_library; FT_Face ft_face; cairo_font_face_t *cairo_face; cairo_font_extents_t cairo_font_extents; hb_font_t *hb_font; hb_font_extents_t hb_font_extents; hb_buffer_t *hb_buffer; hb_variation_t hb_variations[MAX_FONT_VARIATIONS]; hb_feature_t hb_features[MAX_FONT_FEATURES]; uint8_t num_variations; uint8_t num_features; double line_spacing; double scale; bool disable_hinting; }; void entry_backend_harfbuzz_init(struct entry *entry, uint32_t *width, uint32_t *height); void entry_backend_harfbuzz_destroy(struct entry *entry); void entry_backend_harfbuzz_update(struct entry *entry); #endif /* ENTRY_BACKEND_HARFBUZZ_H */ tofi-0.9.1/src/entry_backend/pango.c000066400000000000000000000375471441474151400173560ustar00rootroot00000000000000#include #include #include #include "../entry.h" #include "../log.h" #include "../nelem.h" #include "../unicode.h" #include "../xmalloc.h" #undef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) #undef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) static void rounded_rectangle(cairo_t *cr, uint32_t width, uint32_t height, uint32_t r) { cairo_new_path(cr); /* Top-left */ cairo_arc(cr, r, r, r, -M_PI, -M_PI_2); /* Top-right */ cairo_arc(cr, width - r, r, r, -M_PI_2, 0); /* Bottom-right */ cairo_arc(cr, width - r, height - r, r, 0, M_PI_2); /* Bottom-left */ cairo_arc(cr, r, height - r, r, M_PI_2, M_PI); cairo_close_path(cr); } static void render_text_themed( cairo_t *cr, struct entry *entry, const char *text, const struct text_theme *theme, PangoRectangle *ink_rect, PangoRectangle *logical_rect) { PangoLayout *layout = entry->pango.layout; struct color color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_layout_set_text(layout, text, -1); pango_cairo_update_layout(cr, layout); pango_cairo_show_layout(cr, layout); pango_layout_get_pixel_extents(layout, ink_rect, logical_rect); if (theme->background_color.a == 0) { /* No background to draw, we're done. */ return; } struct directional padding = theme->padding; cairo_matrix_t mat; cairo_get_matrix(cr, &mat); int32_t base_x = mat.x0 - entry->clip_x + ink_rect->x; int32_t base_y = mat.y0 - entry->clip_y; double padding_left = padding.left; double padding_right = padding.right; double padding_top = padding.top; double padding_bottom = padding.bottom; if (padding_left < 0) { padding_left = base_x; } if (padding_right < 0) { padding_right = entry->clip_width - ink_rect->width - base_x; } if (padding_top < 0) { padding_top = base_y; } if (padding_bottom < 0) { padding_bottom = entry->clip_height - logical_rect->height - base_y; } cairo_save(cr); color = theme->background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate( cr, -padding_left + ink_rect->x, -padding_top); rounded_rectangle( cr, ceil(ink_rect->width + padding_left + padding_right), ceil(logical_rect->height + padding_top + padding_bottom), theme->background_corner_radius ); cairo_fill(cr); cairo_restore(cr); color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_cairo_show_layout(cr, layout); } static void render_input( cairo_t *cr, PangoLayout *layout, const char *text, uint32_t text_length, const struct text_theme *theme, uint32_t cursor_position, const struct cursor_theme *cursor_theme, PangoRectangle *ink_rect, PangoRectangle *logical_rect) { struct directional padding = theme->padding; struct color color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_layout_set_text(layout, text, -1); pango_cairo_update_layout(cr, layout); pango_cairo_show_layout(cr, layout); pango_layout_get_pixel_extents(layout, ink_rect, logical_rect); double extra_cursor_advance = 0; if (cursor_position == text_length && cursor_theme->show) { switch (cursor_theme->style) { case CURSOR_STYLE_BAR: extra_cursor_advance = cursor_theme->thickness; break; case CURSOR_STYLE_BLOCK: extra_cursor_advance = cursor_theme->em_width; break; case CURSOR_STYLE_UNDERSCORE: extra_cursor_advance = cursor_theme->em_width; break; } extra_cursor_advance += logical_rect->width - logical_rect->x - ink_rect->width; } if (theme->background_color.a != 0) { cairo_save(cr); color = theme->background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate( cr, floor(-padding.left + ink_rect->x), -padding.top); rounded_rectangle( cr, ceil(ink_rect->width + extra_cursor_advance + padding.left + padding.right), ceil(logical_rect->height + padding.top + padding.bottom), theme->background_corner_radius ); cairo_fill(cr); cairo_restore(cr); color = theme->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_cairo_show_layout(cr, layout); } if (!cursor_theme->show) { /* No cursor to draw, we're done. */ return; } double cursor_x; double cursor_width; if (cursor_position == text_length) { cursor_x = logical_rect->width + logical_rect->x; cursor_width = cursor_theme->em_width; } else { /* * Pango wants a byte index rather than a character index for * the cursor position, so we have to calculate that here. */ const char *tmp = text; for (size_t i = 0; i < cursor_position; i++) { tmp = utf8_next_char(tmp); } uint32_t start_byte_index = tmp - text; uint32_t end_byte_index = utf8_next_char(tmp) - text; PangoRectangle start_pos; PangoRectangle end_pos; pango_layout_get_cursor_pos(layout, start_byte_index, &start_pos, NULL); pango_layout_get_cursor_pos(layout, end_byte_index, &end_pos, NULL); cursor_x = (double)start_pos.x / PANGO_SCALE; cursor_width = (double)(end_pos.x - start_pos.x) / PANGO_SCALE;; } cairo_save(cr); color = cursor_theme->color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate(cr, cursor_x, 0); switch (cursor_theme->style) { case CURSOR_STYLE_BAR: rounded_rectangle(cr, cursor_theme->thickness, logical_rect->height, cursor_theme->corner_radius); cairo_fill(cr); break; case CURSOR_STYLE_BLOCK: rounded_rectangle(cr, cursor_width, logical_rect->height, cursor_theme->corner_radius); cairo_fill_preserve(cr); cairo_clip(cr); cairo_translate(cr, -cursor_x, 0); color = cursor_theme->text_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_cairo_show_layout(cr, layout); break; case CURSOR_STYLE_UNDERSCORE: cairo_translate(cr, 0, cursor_theme->underline_depth); rounded_rectangle(cr, cursor_width, cursor_theme->thickness, cursor_theme->corner_radius); cairo_fill(cr); break; } logical_rect->width += extra_cursor_advance; cairo_restore(cr); } void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *height) { cairo_t *cr = entry->cairo[0].cr; /* Setup Pango. */ log_debug("Creating Pango context.\n"); PangoContext *context = pango_cairo_create_context(cr); log_debug("Creating Pango font description.\n"); PangoFontDescription *font_description = pango_font_description_from_string(entry->font_name); pango_font_description_set_size( font_description, entry->font_size * PANGO_SCALE); if (entry->font_variations[0] != 0) { pango_font_description_set_variations( font_description, entry->font_variations); } pango_context_set_font_description(context, font_description); entry->pango.layout = pango_layout_new(context); if (entry->font_features[0] != 0) { log_debug("Setting font features.\n"); PangoAttribute *attr = pango_attr_font_features_new(entry->font_features); PangoAttrList *attr_list = pango_attr_list_new(); pango_attr_list_insert(attr_list, attr); pango_layout_set_attributes(entry->pango.layout, attr_list); } log_debug("Loading Pango font.\n"); PangoFontMap *map = pango_cairo_font_map_get_default(); PangoFont *font = pango_font_map_load_font(map, context, font_description); PangoFontMetrics *metrics = pango_font_get_metrics(font, NULL); hb_font_t *hb_font = pango_font_get_hb_font(font); uint32_t m_codepoint; if (hb_font_get_glyph_from_name(hb_font, "m", -1, &m_codepoint)) { entry->cursor_theme.em_width = (double)hb_font_get_glyph_h_advance(hb_font, m_codepoint) / PANGO_SCALE; } else { entry->cursor_theme.em_width = (double)pango_font_metrics_get_approximate_char_width(metrics) / PANGO_SCALE; } entry->cursor_theme.underline_depth = (double) ( pango_font_metrics_get_ascent(metrics) - pango_font_metrics_get_underline_position(metrics) ) / PANGO_SCALE; if (entry->cursor_theme.style == CURSOR_STYLE_UNDERSCORE && !entry->cursor_theme.thickness_specified) { entry->cursor_theme.thickness = pango_font_metrics_get_underline_thickness(metrics) / PANGO_SCALE; } pango_font_metrics_unref(metrics); g_object_unref(font); log_debug("Loaded.\n"); pango_font_description_free(font_description); entry->pango.context = context; } void entry_backend_pango_destroy(struct entry *entry) { g_object_unref(entry->pango.layout); g_object_unref(entry->pango.context); } static bool size_overflows(struct entry *entry, uint32_t width, uint32_t height) { cairo_t *cr = entry->cairo[entry->index].cr; cairo_matrix_t mat; cairo_get_matrix(cr, &mat); if (entry->horizontal) { if (mat.x0 + width > entry->clip_x + entry->clip_width) { return true; } } else { if (mat.y0 + height > entry->clip_y + entry->clip_height) { return true; } } return false; } /* * This is pretty much a direct translation of the corresponding function in * the harfbuzz backend. As that's the one that I care about most, there are * more explanatory comments than there are here, so go look at that if you * want to understand how tofi's text rendering works. */ void entry_backend_pango_update(struct entry *entry) { cairo_t *cr = entry->cairo[entry->index].cr; PangoLayout *layout = entry->pango.layout; cairo_save(cr); /* Render the prompt */ PangoRectangle ink_rect; PangoRectangle logical_rect; render_text_themed(cr, entry, entry->prompt_text, &entry->prompt_theme, &ink_rect, &logical_rect); cairo_translate(cr, logical_rect.width + logical_rect.x, 0); cairo_translate(cr, entry->prompt_padding, 0); /* Render the entry text */ if (entry->input_utf8_length == 0) { render_input( cr, layout, entry->placeholder_text, utf8_strlen(entry->placeholder_text), &entry->placeholder_theme, 0, &entry->cursor_theme, &ink_rect, &logical_rect); } else if (entry->hide_input) { size_t nchars = entry->input_utf32_length; size_t char_size = entry->hidden_character_utf8_length; char *buf = xmalloc(1 + nchars * char_size); for (size_t i = 0; i < nchars; i++) { for (size_t j = 0; j < char_size; j++) { buf[i * char_size + j] = entry->hidden_character_utf8[j]; } } buf[char_size * nchars] = '\0'; render_input( cr, layout, buf, entry->input_utf32_length, &entry->input_theme, entry->cursor_position, &entry->cursor_theme, &ink_rect, &logical_rect); free(buf); } else { render_input( cr, layout, entry->input_utf8, entry->input_utf32_length, &entry->input_theme, entry->cursor_position, &entry->cursor_theme, &ink_rect, &logical_rect); } logical_rect.width = MAX(logical_rect.width, (int)entry->input_width); struct color color = entry->foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); uint32_t num_results; if (entry->num_results == 0) { num_results = entry->results.count; } else { num_results = MIN(entry->num_results, entry->results.count); } /* Render our results */ size_t i; for (i = 0; i < num_results; i++) { if (entry->horizontal) { cairo_translate(cr, logical_rect.x + logical_rect.width + entry->result_spacing, 0); } else { cairo_translate(cr, 0, logical_rect.height + entry->result_spacing); } if (entry->num_results == 0) { if (size_overflows(entry, 0, 0)) { break; } } else if (i >= entry->num_results) { break; } size_t index = i + entry->first_result; /* * We may be on the last page, which could have fewer results * than expected, so check and break if necessary. */ if (index >= entry->results.count) { break; } const char *str; if (i < entry->results.count) { str = entry->results.buf[index].string; } else { str = ""; } if (i != entry->selection || (entry->selection_highlight_color.a == 0)) { const struct text_theme *theme; if (i == entry->selection) { theme = &entry->selection_theme; } else if (index % 2) { theme = &entry->alternate_result_theme;; } else { theme = &entry->default_result_theme;; } if (entry->num_results > 0) { render_text_themed(cr, entry, str, theme, &ink_rect, &logical_rect); } else if (!entry->horizontal) { if (size_overflows(entry, 0, logical_rect.height)) { entry->num_results_drawn = i; break; } else { render_text_themed(cr, entry, str, theme, &ink_rect, &logical_rect); } } else { cairo_push_group(cr); render_text_themed(cr, entry, str, theme, &ink_rect, &logical_rect); cairo_pattern_t *group = cairo_pop_group(cr); if (size_overflows(entry, logical_rect.width, 0)) { entry->num_results_drawn = i; cairo_pattern_destroy(group); break; } else { cairo_save(cr); cairo_set_source(cr, group); cairo_paint(cr); cairo_restore(cr); cairo_pattern_destroy(group); } } } else { ssize_t prematch_len = -1; ssize_t postmatch_len = -1; size_t match_len = entry->input_utf8_length; PangoRectangle ink_subrect; PangoRectangle logical_subrect; if (entry->input_utf8_length > 0 && entry->selection_highlight_color.a != 0) { char *match_pos = utf8_strcasestr(str, entry->input_utf8); if (match_pos != NULL) { prematch_len = (match_pos - str); postmatch_len = strlen(str) - prematch_len - match_len; if (postmatch_len <= 0) { postmatch_len = -1; } } } for (int pass = 0; pass < 2; pass++) { cairo_save(cr); color = entry->selection_theme.foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_layout_set_text(layout, str, prematch_len); pango_cairo_update_layout(cr, layout); pango_cairo_show_layout(cr, layout); pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); ink_rect = ink_subrect; logical_rect = logical_subrect; if (prematch_len != -1) { cairo_translate(cr, logical_subrect.x + logical_subrect.width, 0); color = entry->selection_highlight_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_layout_set_text(layout, &str[prematch_len], match_len); pango_cairo_update_layout(cr, layout); pango_cairo_show_layout(cr, layout); pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); if (prematch_len == 0) { ink_rect = ink_subrect; logical_rect = logical_subrect; } else { ink_rect.width = logical_rect.width - ink_rect.x + ink_subrect.x + ink_subrect.width; logical_rect.width += logical_subrect.x + logical_subrect.width; } } if (postmatch_len != -1) { cairo_translate(cr, logical_subrect.x + logical_subrect.width, 0); color = entry->selection_theme.foreground_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); pango_layout_set_text(layout, &str[prematch_len + match_len], -1); pango_cairo_update_layout(cr, layout); pango_cairo_show_layout(cr, layout); pango_layout_get_pixel_extents(entry->pango.layout, &ink_subrect, &logical_subrect); ink_rect.width = logical_rect.width - ink_rect.x + ink_subrect.x + ink_subrect.width; logical_rect.width += logical_subrect.x + logical_subrect.width; } cairo_restore(cr); if (entry->selection_theme.background_color.a == 0) { break; } else if (pass == 0) { struct directional padding = entry->selection_theme.padding; cairo_save(cr); color = entry->selection_theme.background_color; cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); cairo_translate( cr, floor(-padding.left + ink_rect.x), -padding.top); rounded_rectangle( cr, ceil(ink_rect.width + padding.left + padding.right), ceil(logical_rect.height + padding.top + padding.bottom), entry->selection_theme.background_corner_radius ); cairo_fill(cr); cairo_restore(cr); } } } } entry->num_results_drawn = i; log_debug("Drew %zu results.\n", i); cairo_restore(cr); } tofi-0.9.1/src/entry_backend/pango.h000066400000000000000000000006421441474151400173450ustar00rootroot00000000000000#ifndef ENTRY_BACKEND_PANGO_H #define ENTRY_BACKEND_PANGO_H #include struct entry; struct entry_backend_pango { PangoContext *context; PangoLayout *layout; }; void entry_backend_pango_init(struct entry *entry, uint32_t *width, uint32_t *height); void entry_backend_pango_destroy(struct entry *entry); void entry_backend_pango_update(struct entry *entry); #endif /* ENTRY_BACKEND_PANGO_H */ tofi-0.9.1/src/fuzzy_match.c000066400000000000000000000145471441474151400160000ustar00rootroot00000000000000#include #include #include #include #include #include "fuzzy_match.h" #include "unicode.h" #include "xmalloc.h" #undef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) static int32_t compute_score( int32_t jump, bool first_char, const char *restrict match); static int32_t fuzzy_match_recurse( const char *restrict pattern, const char *restrict str, int32_t score, bool first_match_only, bool first_char); /* * Split patterns into words, and perform simple matching against str for each. * Returns the sum of substring distances from the start of str. * If a word is not found, returns INT32_MIN. */ int32_t fuzzy_match_simple_words(const char *restrict patterns, const char *restrict str) { int32_t score = 0; char *saveptr = NULL; char *tmp = utf8_normalize(patterns); char *pattern = strtok_r(tmp, " ", &saveptr); while (pattern != NULL) { char *c = utf8_strcasestr(str, pattern); if (c == NULL) { score = INT32_MIN; break; } else { score += str - c; } pattern = strtok_r(NULL, " ", &saveptr); } free(tmp); return score; } /* * Split patterns into words, and return the sum of fuzzy_match(word, str). * If a word is not found, returns INT32_MIN. */ int32_t fuzzy_match_words(const char *restrict patterns, const char *restrict str) { int32_t score = 0; char *saveptr = NULL; char *tmp = utf8_normalize(patterns); char *pattern = strtok_r(tmp, " ", &saveptr); while (pattern != NULL) { int32_t word_score = fuzzy_match(pattern, str); if (word_score == INT32_MIN) { score = INT32_MIN; break; } else { score += word_score; } pattern = strtok_r(NULL, " ", &saveptr); } free(tmp); return score; } /* * Returns score if each character in pattern is found sequentially within str. * Returns INT32_MIN otherwise. */ int32_t fuzzy_match(const char *restrict pattern, const char *restrict str) { const int unmatched_letter_penalty = -1; const size_t slen = utf8_strlen(str); const size_t plen = utf8_strlen(pattern); int32_t score = 0; if (*pattern == '\0') { return score; } if (slen < plen) { return INT32_MIN; } /* We can already penalise any unused letters. */ score += unmatched_letter_penalty * (int32_t)(slen - plen); /* * If the string is more than 100 characters, just find the first fuzzy * match rather than the best. * * This is required as the number of possible matches (for patterns and * strings all consisting of one letter) scales something like: * * slen! / (plen! (slen - plen)!) ~ slen^plen for plen << slen * * This quickly grinds everything to a halt. 100 is chosen fairly * arbitrarily from the following logic: * * - e is the most common character in English, at around 13% of * letters. Depending on the context, let's say this be up to 20%. * - 100 * 0.20 = 20 repeats of the same character. * - In the worst case here, 20! / (10! 10!) ~200,000 possible matches, * which is "slow but not frozen" for my machine. * * In reality, this worst case shouldn't be hit, and finding the "best" * fuzzy match in lines of text > 100 characters isn't really in scope * for a dmenu clone. */ bool first_match_only = slen > 100; /* Perform the match. */ score = fuzzy_match_recurse(pattern, str, score, first_match_only, true); return score; } /* * Recursively match the whole of pattern against str. * The score parameter is the score of the previously matched character. * * This reaches a maximum recursion depth of strlen(pattern) + 1. However, the * stack usage is small (the maximum I've seen on x86_64 is 144 bytes with * gcc -O3), so this shouldn't matter unless pattern contains thousands of * characters. */ int32_t fuzzy_match_recurse( const char *restrict pattern, const char *restrict str, int32_t score, bool first_match_only, bool first_char) { if (*pattern == '\0') { /* We've matched the full pattern. */ return score; } const char *match = str; uint32_t search = utf8_to_utf32(pattern); int32_t best_score = INT32_MIN; /* * Find all occurrences of the next pattern character in str, and * recurse on them. */ while ((match = utf8_strcasechr(match, search)) != NULL) { int32_t jump = 0; for (const char *tmp = str; tmp != match; tmp = utf8_next_char(tmp)) { jump++; } int32_t subscore = fuzzy_match_recurse( utf8_next_char(pattern), utf8_next_char(match), compute_score(jump, first_char, match), first_match_only, false); best_score = MAX(best_score, subscore); match = utf8_next_char(match); if (first_match_only) { break; } } if (best_score == INT32_MIN) { /* We couldn't match the rest of the pattern. */ return INT32_MIN; } else { return score + best_score; } } /* * Calculate the score for a single matching letter. * The scoring system is taken from fts_fuzzy_match v0.2.0 by Forrest Smith, * which is licensed to the public domain. * * The factors affecting score are: * - Bonuses: * - If there are multiple adjacent matches. * - If a match occurs after a separator character. * - If a match is uppercase, and the previous character is lowercase. * * - Penalties: * - If there are letters before the first match. * - If there are superfluous characters in str (already accounted for). */ int32_t compute_score(int32_t jump, bool first_char, const char *restrict match) { const int adjacency_bonus = 15; const int separator_bonus = 30; const int camel_bonus = 30; const int first_letter_bonus = 15; const int leading_letter_penalty = -5; const int max_leading_letter_penalty = -15; int32_t score = 0; const uint32_t cur = utf8_to_utf32(match); /* Apply bonuses. */ if (!first_char && jump == 0) { score += adjacency_bonus; } if (!first_char || jump > 0) { const uint32_t prev = utf8_to_utf32(utf8_prev_char(match)); if (utf32_isupper(cur) && utf32_islower(prev)) { score += camel_bonus; } if (utf32_isalnum(cur) && !utf32_isalnum(prev)) { score += separator_bonus; } } if (first_char && jump == 0) { /* Match at start of string gets separator bonus. */ score += first_letter_bonus; } /* Apply penalties. */ if (first_char) { score += MAX(leading_letter_penalty * jump, max_leading_letter_penalty); } return score; } tofi-0.9.1/src/fuzzy_match.h000066400000000000000000000005321441474151400157720ustar00rootroot00000000000000#ifndef FUZZY_MATCH_H #define FUZZY_MATCH_H #include int32_t fuzzy_match_simple_words(const char *restrict patterns, const char *restrict str); int32_t fuzzy_match_words(const char *restrict patterns, const char *restrict str); int32_t fuzzy_match(const char *restrict pattern, const char *restrict str); #endif /* FUZZY_MATCH_H */ tofi-0.9.1/src/history.c000066400000000000000000000122421441474151400151240ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include "history.h" #include "log.h" #include "mkdirp.h" #include "xmalloc.h" #define MAX_HISTFILE_SIZE (10*1024*1024) static const char *default_state_dir = ".local/state"; static const char *histfile_basename = "tofi-history"; static const char *drun_histfile_basename = "tofi-drun-history"; [[nodiscard("memory leaked")]] static struct history history_create(void); static char *get_histfile_path(bool drun) { const char *basename; if (drun) { basename = drun_histfile_basename; } else { basename = histfile_basename; } char *histfile_name = NULL; const char *state_path = getenv("XDG_STATE_HOME"); if (state_path == NULL) { const char *home = getenv("HOME"); if (home == NULL) { log_error("Couldn't retrieve HOME from environment.\n"); return NULL; } size_t len = strlen(home) + 1 + strlen(default_state_dir) + 1 + strlen(basename) + 1; histfile_name = xmalloc(len); snprintf( histfile_name, len, "%s/%s/%s", home, default_state_dir, basename); } else { size_t len = strlen(state_path) + 1 + strlen(basename) + 1; histfile_name = xmalloc(len); snprintf( histfile_name, len, "%s/%s", state_path, basename); } return histfile_name; } struct history history_load(const char *path) { struct history vec = history_create(); FILE *histfile = fopen(path, "rb"); if (histfile == NULL) { return vec; } errno = 0; if (fseek(histfile, 0, SEEK_END) != 0) { log_error("Error seeking in history file: %s.\n", strerror(errno)); fclose(histfile); return vec; } errno = 0; size_t len = ftell(histfile); if (len > MAX_HISTFILE_SIZE) { log_error("History file too big (> %d MiB)! Are you sure it's a file?\n", MAX_HISTFILE_SIZE / 1024 / 1024); fclose(histfile); return vec; } errno = 0; if (fseek(histfile, 0, SEEK_SET) != 0) { log_error("Error seeking in history file: %s.\n", strerror(errno)); fclose(histfile); return vec; } errno = 0; char *buf = xmalloc(len + 1); if (fread(buf, 1, len, histfile) != len) { log_error("Error reading history file: %s.\n", strerror(errno)); fclose(histfile); return vec; } fclose(histfile); buf[len] = '\0'; char *saveptr = NULL; char *tok = strtok_r(buf, " ", &saveptr); while (tok != NULL) { size_t run_count = strtoull(tok, NULL, 10); tok = strtok_r(NULL, "\n", &saveptr); if (tok == NULL) { break; } history_add(&vec, tok); vec.buf[vec.count - 1].run_count = run_count; tok = strtok_r(NULL, " ", &saveptr); } free(buf); return vec; } void history_save(const struct history *history, const char *path) { /* Create the path if necessary. */ if (!mkdirp(path)) { return; } /* Use open rather than fopen to ensure the proper permissions. */ int histfd = open(path, O_WRONLY | O_CREAT, 0600); FILE *histfile = fdopen(histfd, "wb"); if (histfile == NULL) { return; } for (size_t i = 0; i < history->count; i++) { fprintf(histfile, "%zu %s\n", history->buf[i].run_count, history->buf[i].name); } fclose(histfile); } struct history history_load_default_file(bool drun) { char *histfile_name = get_histfile_path(drun); if (histfile_name == NULL) { return history_create(); } struct history vec = history_load(histfile_name); free(histfile_name); return vec; } void history_save_default_file(const struct history *history, bool drun) { char *histfile_name = get_histfile_path(drun); if (histfile_name == NULL) { return; } history_save(history, histfile_name); free(histfile_name); } struct history history_create(void) { struct history vec = { .count = 0, .size = 16, .buf = xcalloc(16, sizeof(struct program)) }; return vec; } void history_destroy(struct history *restrict vec) { for (size_t i = 0; i < vec->count; i++) { free(vec->buf[i].name); } free(vec->buf); } void history_add(struct history *restrict vec, const char *restrict str) { /* * If the program's already in our vector, just increment the count and * move the program up if needed. */ for (size_t i = 0; i < vec->count; i++) { if (!strcmp(vec->buf[i].name, str)) { vec->buf[i].run_count++; size_t count = vec->buf[i].run_count; if (i > 0 && count <= vec->buf[i-1].run_count) { return; } /* We need to move the program up the list */ size_t j = i; while (j > 0 && count > vec->buf[j-1].run_count) { j--; } struct program tmp = vec->buf[i]; memmove(&vec->buf[j+1], &vec->buf[j], (i - j) * sizeof(struct program)); vec->buf[j] = tmp; return; } } /* Otherwise add it to the end with a run count of 1 */ if (vec->count == vec->size) { vec->size *= 2; vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); } vec->buf[vec->count].name = xstrdup(str); vec->buf[vec->count].run_count = 1; vec->count++; } void history_remove(struct history *restrict vec, const char *restrict str) { for (size_t i = 0; i < vec->count; i++) { if (!strcmp(vec->buf[i].name, str)) { free(vec->buf[i].name); if (i < vec->count - 1) { memmove(&vec->buf[i], &vec->buf[i+1], (vec->count - i) * sizeof(struct program)); } vec->count--; return; } } } tofi-0.9.1/src/history.h000066400000000000000000000014451441474151400151340ustar00rootroot00000000000000#ifndef HISTORY_H #define HISTORY_H #include #include struct program { char *restrict name; size_t run_count; }; struct history { size_t count; size_t size; struct program *buf; }; [[gnu::nonnull]] void history_destroy(struct history *restrict vec); [[gnu::nonnull]] void history_add(struct history *restrict vec, const char *restrict str); //[[gnu::nonnull]] //void history_remove(struct history *restrict vec, const char *restrict str); [[nodiscard("memory leaked")]] struct history history_load(const char *path); void history_save(const struct history *history, const char *path); [[nodiscard("memory leaked")]] struct history history_load_default_file(bool drun); void history_save_default_file(const struct history *history, bool drun); #endif /* HISTORY_H */ tofi-0.9.1/src/input.c000066400000000000000000000234251441474151400145670ustar00rootroot00000000000000#include #include #include #include #include "input.h" #include "log.h" #include "nelem.h" #include "tofi.h" #include "unicode.h" static void add_character(struct tofi *tofi, xkb_keycode_t keycode); static void delete_character(struct tofi *tofi); static void delete_word(struct tofi *tofi); static void clear_input(struct tofi *tofi); static void paste(struct tofi *tofi); static void select_previous_result(struct tofi *tofi); static void select_next_result(struct tofi *tofi); static void select_previous_page(struct tofi *tofi); static void select_next_page(struct tofi *tofi); static void next_cursor_or_result(struct tofi *tofi); static void previous_cursor_or_result(struct tofi *tofi); static void reset_selection(struct tofi *tofi); void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) { if (tofi->xkb_state == NULL) { return; } /* * Use physical key code for shortcuts, ignoring layout changes. * Linux keycodes are 8 less than XKB keycodes. */ const uint32_t key = keycode - 8; xkb_keysym_t sym = xkb_state_key_get_one_sym(tofi->xkb_state, keycode); uint32_t ch = xkb_state_key_get_utf32( tofi->xkb_state, keycode); if (utf32_isprint(ch)) { add_character(tofi, keycode); } else if ((sym == XKB_KEY_BackSpace || key == KEY_W) && xkb_state_mod_name_is_active( tofi->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE)) { delete_word(tofi); } else if (sym == XKB_KEY_BackSpace) { delete_character(tofi); } else if (key == KEY_U && xkb_state_mod_name_is_active( tofi->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) ) { clear_input(tofi); } else if (key == KEY_V && xkb_state_mod_name_is_active( tofi->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) ) { paste(tofi); } else if (sym == XKB_KEY_Left) { previous_cursor_or_result(tofi); } else if (sym == XKB_KEY_Right) { next_cursor_or_result(tofi); } else if (sym == XKB_KEY_Up || sym == XKB_KEY_Left || sym == XKB_KEY_ISO_Left_Tab || ((key == KEY_K || key == KEY_P) && xkb_state_mod_name_is_active( tofi->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) ) ) { select_previous_result(tofi); } else if (sym == XKB_KEY_Down || sym == XKB_KEY_Right || sym == XKB_KEY_Tab || ((key == KEY_J || key == KEY_N) && xkb_state_mod_name_is_active( tofi->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) ) ) { select_next_result(tofi); } else if (sym == XKB_KEY_Home) { reset_selection(tofi); } else if (sym == XKB_KEY_Page_Up) { select_previous_page(tofi); } else if (sym == XKB_KEY_Page_Down) { select_next_page(tofi); } else if (sym == XKB_KEY_Escape || (key == KEY_C && xkb_state_mod_name_is_active( tofi->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) ) ) { tofi->closed = true; return; } else if (sym == XKB_KEY_Return || sym == XKB_KEY_KP_Enter) { tofi->submit = true; return; } if (tofi->auto_accept_single && tofi->window.entry.results.count == 1) { tofi->submit = true; } tofi->window.surface.redraw = true; } void reset_selection(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; entry->selection = 0; entry->first_result = 0; } void add_character(struct tofi *tofi, xkb_keycode_t keycode) { struct entry *entry = &tofi->window.entry; if (entry->input_utf32_length >= N_ELEM(entry->input_utf32) - 1) { /* No more room for input */ return; } char buf[5]; /* 4 UTF-8 bytes plus null terminator. */ int len = xkb_state_key_get_utf8( tofi->xkb_state, keycode, buf, sizeof(buf)); if (entry->cursor_position == entry->input_utf32_length) { entry->input_utf32[entry->input_utf32_length] = utf8_to_utf32(buf); entry->input_utf32_length++; entry->input_utf32[entry->input_utf32_length] = U'\0'; memcpy(&entry->input_utf8[entry->input_utf8_length], buf, N_ELEM(buf)); entry->input_utf8_length += len; if (entry->drun) { struct string_ref_vec results = desktop_vec_filter(&entry->apps, entry->input_utf8, tofi->fuzzy_match); string_ref_vec_destroy(&entry->results); entry->results = results; } else { struct string_ref_vec tmp = entry->results; entry->results = string_ref_vec_filter(&entry->results, entry->input_utf8, tofi->fuzzy_match); string_ref_vec_destroy(&tmp); } reset_selection(tofi); } else { for (size_t i = entry->input_utf32_length; i > entry->cursor_position; i--) { entry->input_utf32[i] = entry->input_utf32[i - 1]; } entry->input_utf32[entry->cursor_position] = utf8_to_utf32(buf); entry->input_utf32_length++; entry->input_utf32[entry->input_utf32_length] = U'\0'; input_refresh_results(tofi); } entry->cursor_position++; } void input_refresh_results(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; size_t bytes_written = 0; for (size_t i = 0; i < entry->input_utf32_length; i++) { bytes_written += utf32_to_utf8( entry->input_utf32[i], &entry->input_utf8[bytes_written]); } entry->input_utf8[bytes_written] = '\0'; entry->input_utf8_length = bytes_written; string_ref_vec_destroy(&entry->results); if (entry->drun) { entry->results = desktop_vec_filter(&entry->apps, entry->input_utf8, tofi->fuzzy_match); } else { entry->results = string_ref_vec_filter(&entry->commands, entry->input_utf8, tofi->fuzzy_match); } reset_selection(tofi); } void delete_character(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; if (entry->input_utf32_length == 0) { /* No input to delete. */ return; } if (entry->cursor_position == 0) { return; } else if (entry->cursor_position == entry->input_utf32_length) { entry->cursor_position--; entry->input_utf32_length--; entry->input_utf32[entry->input_utf32_length] = U'\0'; } else { for (size_t i = entry->cursor_position - 1; i < entry->input_utf32_length - 1; i++) { entry->input_utf32[i] = entry->input_utf32[i + 1]; } entry->cursor_position--; entry->input_utf32_length--; entry->input_utf32[entry->input_utf32_length] = U'\0'; } input_refresh_results(tofi); } void delete_word(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; if (entry->cursor_position == 0) { /* No input to delete. */ return; } uint32_t new_cursor_pos = entry->cursor_position; while (new_cursor_pos > 0 && utf32_isspace(entry->input_utf32[new_cursor_pos - 1])) { new_cursor_pos--; } while (new_cursor_pos > 0 && !utf32_isspace(entry->input_utf32[new_cursor_pos - 1])) { new_cursor_pos--; } uint32_t new_length = entry->input_utf32_length - (entry->cursor_position - new_cursor_pos); for (size_t i = 0; i < new_length; i++) { entry->input_utf32[new_cursor_pos + i] = entry->input_utf32[entry->cursor_position + i]; } entry->input_utf32_length = new_length; entry->input_utf32[entry->input_utf32_length] = U'\0'; entry->cursor_position = new_cursor_pos; input_refresh_results(tofi); } void clear_input(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; entry->cursor_position = 0; entry->input_utf32_length = 0; entry->input_utf32[0] = U'\0'; input_refresh_results(tofi); } void paste(struct tofi *tofi) { if (tofi->clipboard.wl_data_offer == NULL || tofi->clipboard.mime_type == NULL) { return; } /* * Create a pipe, and give the write end to the compositor to give to * the clipboard manager. */ errno = 0; int fildes[2]; if (pipe2(fildes, O_CLOEXEC | O_NONBLOCK) == -1) { log_error("Failed to open pipe for clipboard: %s\n", strerror(errno)); return; } wl_data_offer_receive(tofi->clipboard.wl_data_offer, tofi->clipboard.mime_type, fildes[1]); close(fildes[1]); /* Keep the read end for reading in the main loop. */ tofi->clipboard.fd = fildes[0]; } void select_previous_result(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; if (entry->selection > 0) { entry->selection--; return; } uint32_t nsel = MAX(MIN(entry->num_results_drawn, entry->results.count), 1); if (entry->first_result > nsel) { entry->first_result -= entry->last_num_results_drawn; entry->selection = entry->last_num_results_drawn - 1; } else if (entry->first_result > 0) { entry->selection = entry->first_result - 1; entry->first_result = 0; } } void select_next_result(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; uint32_t nsel = MAX(MIN(entry->num_results_drawn, entry->results.count), 1); entry->selection++; if (entry->selection >= nsel) { entry->selection -= nsel; if (entry->results.count > 0) { entry->first_result += nsel; entry->first_result %= entry->results.count; } else { entry->first_result = 0; } entry->last_num_results_drawn = entry->num_results_drawn; } } void previous_cursor_or_result(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; if (entry->cursor_theme.show && entry->selection == 0 && entry->cursor_position > 0) { entry->cursor_position--; } else { select_previous_result(tofi); } } void next_cursor_or_result(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; if (entry->cursor_theme.show && entry->cursor_position < entry->input_utf32_length) { entry->cursor_position++; } else { select_next_result(tofi); } } void select_previous_page(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; if (entry->first_result >= entry->last_num_results_drawn) { entry->first_result -= entry->last_num_results_drawn; } else { entry->first_result = 0; } entry->selection = 0; entry->last_num_results_drawn = entry->num_results_drawn; } void select_next_page(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; entry->first_result += entry->num_results_drawn; if (entry->first_result >= entry->results.count) { entry->first_result = 0; } entry->selection = 0; entry->last_num_results_drawn = entry->num_results_drawn; } tofi-0.9.1/src/input.h000066400000000000000000000003401441474151400145630ustar00rootroot00000000000000#ifndef INPUT_H #define INPUT_H #include #include "tofi.h" void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode); void input_refresh_results(struct tofi *tofi); #endif /* INPUT_H */ tofi-0.9.1/src/lock.c000066400000000000000000000030661441474151400143570ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "log.h" #include "xmalloc.h" static const char *default_cache_dir = ".cache/"; static const char *lock_filename = "tofi.lock"; [[nodiscard("memory leaked")]] static char *get_lock_path() { char *lock_name = NULL; const char *runtime_path = getenv("XDG_RUNTIME_DIR"); if (runtime_path == NULL) { runtime_path = getenv("XDG_CACHE_HOME"); } if (runtime_path == NULL) { const char *home = getenv("HOME"); if (home == NULL) { log_error("Couldn't retrieve HOME from environment.\n"); return NULL; } size_t len = strlen(home) + 1 + strlen(default_cache_dir) + 1 + strlen(lock_filename) + 1; lock_name = xmalloc(len); snprintf( lock_name, len, "%s/%s/%s", home, default_cache_dir, lock_filename); } else { size_t len = strlen(runtime_path) + 1 + strlen(lock_filename) + 1; lock_name = xmalloc(len); snprintf( lock_name, len, "%s/%s", runtime_path, lock_filename); } return lock_name; } bool lock_check(void) { bool ret = false; char *filename = get_lock_path(); errno = 0; int fd = open(filename, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) { log_error("Failed to open lock file %s: %s.\n", filename, strerror(errno)); } else if (flock(fd, LOCK_EX | LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* * We can't lock the file because another tofi process * already has. */ ret = true; } } free(filename); return ret; } tofi-0.9.1/src/lock.h000066400000000000000000000001411441474151400143530ustar00rootroot00000000000000#ifndef LOCK_H #define LOCK_H #include bool lock_check(void); #endif /* LOCK_H */ tofi-0.9.1/src/log.c000066400000000000000000000051001441474151400141770ustar00rootroot00000000000000#include #include #include #include #include #define SECOND 1000000000ul static struct timespec time_diff( struct timespec cur, struct timespec old); static int indent = 0; static void print_indent(FILE *file) { for (int i = 0; i < indent; i++) { fprintf(file, " "); } } void log_indent(void) { indent++; } void log_unindent(void) { if (indent > 0) { indent--; } } void log_error(const char *const fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "[ERROR]: "); vfprintf(stderr, fmt, args); va_end(args); } void log_warning(const char *const fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "[WARNING]: "); vfprintf(stderr, fmt, args); va_end(args); } void log_debug(const char *const fmt, ...) { #ifndef DEBUG return; #endif static struct timespec start_time; if (start_time.tv_nsec == 0) { fprintf(stderr, "[ real, cpu, maxRSS]\n"); clock_gettime(CLOCK_REALTIME, &start_time); } struct timespec real_time; struct timespec cpu_time; clock_gettime(CLOCK_REALTIME, &real_time); clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &cpu_time); real_time = time_diff(real_time, start_time); struct rusage usage; getrusage(RUSAGE_SELF, &usage); va_list args; va_start(args, fmt); fprintf( stderr, "[%ld.%06ld, %ld.%06ld, %5ld KB][DEBUG]: ", real_time.tv_sec, real_time.tv_nsec / 1000, cpu_time.tv_sec, cpu_time.tv_nsec / 1000, usage.ru_maxrss ); print_indent(stderr); vfprintf(stderr, fmt, args); va_end(args); } void log_info(const char *const fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "[INFO]: "); print_indent(stderr); vfprintf(stderr, fmt, args); va_end(args); } void log_append_error(const char *const fmt, ...) { va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); } void log_append_warning(const char *const fmt, ...) { va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); } void log_append_debug(const char *const fmt, ...) { #ifndef DEBUG return; #endif va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); } void log_append_info(const char *const fmt, ...) { va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); } struct timespec time_diff(struct timespec cur, struct timespec old) { struct timespec diff; diff.tv_sec = cur.tv_sec - old.tv_sec; if (cur.tv_nsec > old.tv_nsec) { diff.tv_nsec = cur.tv_nsec - old.tv_nsec; } else { diff.tv_nsec = SECOND + cur.tv_nsec - old.tv_nsec; diff.tv_sec -= 1; } return diff; } tofi-0.9.1/src/log.h000066400000000000000000000007371441474151400142170ustar00rootroot00000000000000#ifndef LOG_H #define LOG_H void log_indent(void); void log_unindent(void); void log_error(const char *const fmt, ...); void log_warning(const char *const fmt, ...); void log_debug(const char *const fmt, ...); void log_info(const char *const fmt, ...); void log_append_error(const char *const fmt, ...); void log_append_warning(const char *const fmt, ...); void log_append_debug(const char *const fmt, ...); void log_append_info(const char *const fmt, ...); #endif /* LOG_H */ tofi-0.9.1/src/main.c000066400000000000000000001516151441474151400143570ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tofi.h" #include "compgen.h" #include "drun.h" #include "config.h" #include "entry.h" #include "input.h" #include "log.h" #include "nelem.h" #include "lock.h" #include "scale.h" #include "shm.h" #include "string_vec.h" #include "string_vec.h" #include "unicode.h" #include "viewporter.h" #include "xmalloc.h" #undef MAX #undef MIN #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) static const char *mime_type_text_plain = "text/plain"; static const char *mime_type_text_plain_utf8 = "text/plain;charset=utf-8"; static uint32_t gettime_ms() { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); uint32_t ms = t.tv_sec * 1000; ms += t.tv_nsec / 1000000; return ms; } /* Read all of stdin into a buffer. */ static char *read_stdin(bool normalize) { const size_t block_size = BUFSIZ; size_t num_blocks = 1; size_t buf_size = block_size; char *buf = xmalloc(buf_size); for (size_t block = 0; ; block++) { if (block == num_blocks) { num_blocks *= 2; buf = xrealloc(buf, num_blocks * block_size); } size_t bytes_read = fread( &buf[block * block_size], 1, block_size, stdin); if (bytes_read != block_size) { if (!feof(stdin) && ferror(stdin)) { log_error("Error reading stdin.\n"); } buf[block * block_size + bytes_read] = '\0'; break; } } if (normalize) { if (utf8_validate(buf)) { char *tmp = utf8_normalize(buf); free(buf); buf = tmp; } else { log_error("Invalid UTF-8 in stdin.\n"); } } return buf; } static void zwlr_layer_surface_configure( void *data, struct zwlr_layer_surface_v1 *zwlr_layer_surface, uint32_t serial, uint32_t width, uint32_t height) { struct tofi *tofi = data; if (width == 0 || height == 0) { /* Compositor is deferring to us, so don't do anything. */ log_debug("Layer surface configure with no width or height.\n"); return; } log_debug("Layer surface configure, %u x %u.\n", width, height); /* * Resize the main window. * We want actual pixel width / height, so we have to scale the * values provided by Wayland. */ if (tofi->window.fractional_scale != 0) { tofi->window.surface.width = scale_apply(width, tofi->window.fractional_scale); tofi->window.surface.height = scale_apply(height, tofi->window.fractional_scale); } else { tofi->window.surface.width = width * tofi->window.scale; tofi->window.surface.height = height * tofi->window.scale; } zwlr_layer_surface_v1_ack_configure( tofi->window.zwlr_layer_surface, serial); } static void zwlr_layer_surface_close( void *data, struct zwlr_layer_surface_v1 *zwlr_layer_surface) { struct tofi *tofi = data; tofi->closed = true; log_debug("Layer surface close.\n"); } static const struct zwlr_layer_surface_v1_listener zwlr_layer_surface_listener = { .configure = zwlr_layer_surface_configure, .closed = zwlr_layer_surface_close }; static void wl_keyboard_keymap( void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size) { struct tofi *tofi = data; assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1); char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); assert(map_shm != MAP_FAILED); if (tofi->late_keyboard_init) { log_debug("Delaying keyboard configuration.\n"); tofi->xkb_keymap_string = xstrdup(map_shm); } else { log_debug("Configuring keyboard.\n"); struct xkb_keymap *xkb_keymap = xkb_keymap_new_from_string( tofi->xkb_context, map_shm, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); munmap(map_shm, size); close(fd); struct xkb_state *xkb_state = xkb_state_new(xkb_keymap); xkb_keymap_unref(tofi->xkb_keymap); xkb_state_unref(tofi->xkb_state); tofi->xkb_keymap = xkb_keymap; tofi->xkb_state = xkb_state; log_debug("Keyboard configured.\n"); } munmap(map_shm, size); close(fd); } static void wl_keyboard_enter( void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { /* Deliberately left blank */ } static void wl_keyboard_leave( void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface) { /* Deliberately left blank */ } static void wl_keyboard_key( void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { struct tofi *tofi = data; /* * If this wasn't a keypress (i.e. was a key release), just update key * repeat info and return. */ uint32_t keycode = key + 8; if (state != WL_KEYBOARD_KEY_STATE_PRESSED) { if (keycode == tofi->repeat.keycode) { tofi->repeat.active = false; } else { tofi->repeat.next = gettime_ms() + tofi->repeat.delay; } return; } /* A rate of 0 disables key repeat */ if (xkb_keymap_key_repeats(tofi->xkb_keymap, keycode) && tofi->repeat.rate != 0) { tofi->repeat.active = true; tofi->repeat.keycode = keycode; tofi->repeat.next = gettime_ms() + tofi->repeat.delay; } input_handle_keypress(tofi, keycode); } static void wl_keyboard_modifiers( void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { struct tofi *tofi = data; if (tofi->xkb_state == NULL) { return; } xkb_state_update_mask( tofi->xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); } static void wl_keyboard_repeat_info( void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) { struct tofi *tofi = data; tofi->repeat.rate = rate; tofi->repeat.delay = delay; if (rate > 0) { log_debug("Key repeat every %u ms after %u ms.\n", 1000 / rate, delay); } else { log_debug("Key repeat disabled.\n"); } } static const struct wl_keyboard_listener wl_keyboard_listener = { .keymap = wl_keyboard_keymap, .enter = wl_keyboard_enter, .leave = wl_keyboard_leave, .key = wl_keyboard_key, .modifiers = wl_keyboard_modifiers, .repeat_info = wl_keyboard_repeat_info, }; static void wl_pointer_enter( void *data, struct wl_pointer *pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct tofi *tofi = data; if (tofi->hide_cursor) { /* Hide the cursor by setting its surface to NULL. */ wl_pointer_set_cursor(tofi->wl_pointer, serial, NULL, 0, 0); } } static void wl_pointer_leave( void *data, struct wl_pointer *pointer, uint32_t serial, struct wl_surface *surface) { /* Deliberately left blank */ } static void wl_pointer_motion( void *data, struct wl_pointer *pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { /* Deliberately left blank */ } static void wl_pointer_button( void *data, struct wl_pointer *pointer, uint32_t serial, uint32_t time, uint32_t button, enum wl_pointer_button_state state) { /* Deliberately left blank */ } static void wl_pointer_axis( void *data, struct wl_pointer *pointer, uint32_t time, enum wl_pointer_axis axis, wl_fixed_t value) { /* Deliberately left blank */ } static void wl_pointer_frame(void *data, struct wl_pointer *pointer) { /* Deliberately left blank */ } static void wl_pointer_axis_source( void *data, struct wl_pointer *pointer, enum wl_pointer_axis_source axis_source) { /* Deliberately left blank */ } static void wl_pointer_axis_stop( void *data, struct wl_pointer *pointer, uint32_t time, enum wl_pointer_axis axis) { /* Deliberately left blank */ } static void wl_pointer_axis_discrete( void *data, struct wl_pointer *pointer, enum wl_pointer_axis axis, int32_t discrete) { /* Deliberately left blank */ } static const struct wl_pointer_listener wl_pointer_listener = { .enter = wl_pointer_enter, .leave = wl_pointer_leave, .motion = wl_pointer_motion, .button = wl_pointer_button, .axis = wl_pointer_axis, .frame = wl_pointer_frame, .axis_source = wl_pointer_axis_source, .axis_stop = wl_pointer_axis_stop, .axis_discrete = wl_pointer_axis_discrete }; static void wl_seat_capabilities( void *data, struct wl_seat *wl_seat, uint32_t capabilities) { struct tofi *tofi = data; bool have_keyboard = capabilities & WL_SEAT_CAPABILITY_KEYBOARD; bool have_pointer = capabilities & WL_SEAT_CAPABILITY_POINTER; if (have_keyboard && tofi->wl_keyboard == NULL) { tofi->wl_keyboard = wl_seat_get_keyboard(tofi->wl_seat); wl_keyboard_add_listener( tofi->wl_keyboard, &wl_keyboard_listener, tofi); log_debug("Got keyboard from seat.\n"); } else if (!have_keyboard && tofi->wl_keyboard != NULL) { wl_keyboard_release(tofi->wl_keyboard); tofi->wl_keyboard = NULL; log_debug("Released keyboard.\n"); } if (have_pointer && tofi->wl_pointer == NULL) { tofi->wl_pointer = wl_seat_get_pointer(tofi->wl_seat); wl_pointer_add_listener( tofi->wl_pointer, &wl_pointer_listener, tofi); log_debug("Got pointer from seat.\n"); } else if (!have_pointer && tofi->wl_pointer != NULL) { wl_pointer_release(tofi->wl_pointer); tofi->wl_pointer = NULL; log_debug("Released pointer.\n"); } } static void wl_seat_name(void *data, struct wl_seat *wl_seat, const char *name) { /* Deliberately left blank */ } static const struct wl_seat_listener wl_seat_listener = { .capabilities = wl_seat_capabilities, .name = wl_seat_name, }; static void wl_data_offer_offer( void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) { struct clipboard *clipboard = data; /* Only accept plain text, and prefer utf-8. */ if (!strcmp(mime_type, mime_type_text_plain)) { if (clipboard->mime_type != NULL) { clipboard->mime_type = mime_type_text_plain; } } else if (!strcmp(mime_type, mime_type_text_plain_utf8)) { clipboard->mime_type = mime_type_text_plain_utf8; } } static void wl_data_offer_source_actions( void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions) { /* Deliberately left blank */ } static void wl_data_offer_action( void *data, struct wl_data_offer *wl_data_offer, uint32_t action) { /* Deliberately left blank */ } static const struct wl_data_offer_listener wl_data_offer_listener = { .offer = wl_data_offer_offer, .source_actions = wl_data_offer_source_actions, .action = wl_data_offer_action }; static void wl_data_device_data_offer( void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *wl_data_offer) { struct clipboard *clipboard = data; clipboard_reset(clipboard); clipboard->wl_data_offer = wl_data_offer; wl_data_offer_add_listener( wl_data_offer, &wl_data_offer_listener, clipboard); } static void wl_data_device_enter( void *data, struct wl_data_device *wl_data_device, uint32_t serial, struct wl_surface *wl_surface, int32_t x, int32_t y, struct wl_data_offer *wl_data_offer) { /* Drag-and-drop is just ignored for now. */ wl_data_offer_accept( wl_data_offer, serial, NULL); wl_data_offer_set_actions( wl_data_offer, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); } static void wl_data_device_leave( void *data, struct wl_data_device *wl_data_device) { /* Deliberately left blank */ } static void wl_data_device_motion( void *data, struct wl_data_device *wl_data_device, uint32_t time, int32_t x, int32_t y) { /* Deliberately left blank */ } static void wl_data_device_drop( void *data, struct wl_data_device *wl_data_device) { /* Deliberately left blank */ } static void wl_data_device_selection( void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *wl_data_offer) { struct clipboard *clipboard = data; if (wl_data_offer == NULL) { clipboard_reset(clipboard); } } static const struct wl_data_device_listener wl_data_device_listener = { .data_offer = wl_data_device_data_offer, .enter = wl_data_device_enter, .leave = wl_data_device_leave, .motion = wl_data_device_motion, .drop = wl_data_device_drop, .selection = wl_data_device_selection }; static void output_geometry( void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { struct tofi *tofi = data; struct output_list_element *el; wl_list_for_each(el, &tofi->output_list, link) { if (el->wl_output == wl_output) { el->transform = transform; } } } static void output_mode( void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { struct tofi *tofi = data; struct output_list_element *el; wl_list_for_each(el, &tofi->output_list, link) { if (el->wl_output == wl_output) { if (flags & WL_OUTPUT_MODE_CURRENT) { el->width = width; el->height = height; } } } } static void output_scale( void *data, struct wl_output *wl_output, int32_t factor) { struct tofi *tofi = data; struct output_list_element *el; wl_list_for_each(el, &tofi->output_list, link) { if (el->wl_output == wl_output) { el->scale = factor; } } } static void output_name( void *data, struct wl_output *wl_output, const char *name) { struct tofi *tofi = data; struct output_list_element *el; wl_list_for_each(el, &tofi->output_list, link) { if (el->wl_output == wl_output) { el->name = xstrdup(name); } } } static void output_description( void *data, struct wl_output *wl_output, const char *description) { /* Deliberately left blank */ } static void output_done(void *data, struct wl_output *wl_output) { log_debug("Output configuration done.\n"); } static const struct wl_output_listener wl_output_listener = { .geometry = output_geometry, .mode = output_mode, .done = output_done, .scale = output_scale, #ifndef NO_WL_OUTPUT_NAME .name = output_name, .description = output_description, #endif }; static void registry_global( void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version) { struct tofi *tofi = data; //log_debug("Registry %u: %s v%u.\n", name, interface, version); if (!strcmp(interface, wl_compositor_interface.name)) { tofi->wl_compositor = wl_registry_bind( wl_registry, name, &wl_compositor_interface, 4); log_debug("Bound to compositor %u.\n", name); } else if (!strcmp(interface, wl_seat_interface.name)) { tofi->wl_seat = wl_registry_bind( wl_registry, name, &wl_seat_interface, 7); wl_seat_add_listener( tofi->wl_seat, &wl_seat_listener, tofi); log_debug("Bound to seat %u.\n", name); } else if (!strcmp(interface, wl_output_interface.name)) { struct output_list_element *el = xmalloc(sizeof(*el)); if (version < 4) { el->name = xstrdup(""); log_warning("Using an outdated compositor, " "output selection will not work.\n"); } else { version = 4; } el->wl_output = wl_registry_bind( wl_registry, name, &wl_output_interface, version); wl_output_add_listener( el->wl_output, &wl_output_listener, tofi); wl_list_insert(&tofi->output_list, &el->link); log_debug("Bound to output %u.\n", name); } else if (!strcmp(interface, wl_shm_interface.name)) { tofi->wl_shm = wl_registry_bind( wl_registry, name, &wl_shm_interface, 1); log_debug("Bound to shm %u.\n", name); } else if (!strcmp(interface, wl_data_device_manager_interface.name)) { tofi->wl_data_device_manager = wl_registry_bind( wl_registry, name, &wl_data_device_manager_interface, 3); log_debug("Bound to data device manager %u.\n", name); } else if (!strcmp(interface, zwlr_layer_shell_v1_interface.name)) { if (version < 3) { log_warning("Using an outdated compositor, " "screen anchoring may not work.\n"); } else { version = 3; } tofi->zwlr_layer_shell = wl_registry_bind( wl_registry, name, &zwlr_layer_shell_v1_interface, version); log_debug("Bound to zwlr_layer_shell_v1 %u.\n", name); } else if (!strcmp(interface, wp_viewporter_interface.name)) { tofi->wp_viewporter = wl_registry_bind( wl_registry, name, &wp_viewporter_interface, 1); log_debug("Bound to wp_viewporter %u.\n", name); } else if (!strcmp(interface, wp_fractional_scale_manager_v1_interface.name)) { tofi->wp_fractional_scale_manager = wl_registry_bind( wl_registry, name, &wp_fractional_scale_manager_v1_interface, 1); log_debug("Bound to wp_fractional_scale_manager_v1 %u.\n", name); } } static void registry_global_remove( void *data, struct wl_registry *wl_registry, uint32_t name) { /* Deliberately left blank */ } static const struct wl_registry_listener wl_registry_listener = { .global = registry_global, .global_remove = registry_global_remove, }; static void surface_enter( void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { log_debug("Surface entered output.\n"); } static void surface_leave( void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { /* Deliberately left blank */ } static const struct wl_surface_listener wl_surface_listener = { .enter = surface_enter, .leave = surface_leave }; /* * These "dummy_*" functions are callbacks just for the dummy surface used to * select the default output if there's more than one. */ static void dummy_layer_surface_configure( void *data, struct zwlr_layer_surface_v1 *zwlr_layer_surface, uint32_t serial, uint32_t width, uint32_t height) { zwlr_layer_surface_v1_ack_configure( zwlr_layer_surface, serial); } static void dummy_layer_surface_close( void *data, struct zwlr_layer_surface_v1 *zwlr_layer_surface) { } static const struct zwlr_layer_surface_v1_listener dummy_layer_surface_listener = { .configure = dummy_layer_surface_configure, .closed = dummy_layer_surface_close }; static void dummy_fractional_scale_preferred_scale( void *data, struct wp_fractional_scale_v1 *wp_fractional_scale, uint32_t scale) { struct tofi *tofi = data; tofi->window.fractional_scale = scale; } static const struct wp_fractional_scale_v1_listener dummy_fractional_scale_listener = { .preferred_scale = dummy_fractional_scale_preferred_scale }; static void dummy_surface_enter( void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct tofi *tofi = data; struct output_list_element *el; wl_list_for_each(el, &tofi->output_list, link) { if (el->wl_output == wl_output) { tofi->default_output = el; break; } } } static void dummy_surface_leave( void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { /* Deliberately left blank */ } static const struct wl_surface_listener dummy_surface_listener = { .enter = dummy_surface_enter, .leave = dummy_surface_leave }; static void usage(bool err) { fprintf(err ? stderr : stdout, "%s", "Usage: tofi [options]\n" "\n" "Basic options:\n" " -h, --help Print this message and exit.\n" " -c, --config Specify a config file.\n" " --prompt-text Prompt text.\n" " --width Width of the window.\n" " --height Height of the window.\n" " --output Name of output to display window on.\n" " --anchor Location on screen to anchor window.\n" " --horizontal List results horizontally.\n" " --fuzzy-match Use fuzzy matching for searching.\n" "\n" "All options listed in \"man 5 tofi\" are also accpted in the form \"--key=value\".\n" ); } /* Option parsing with getopt. */ const struct option long_options[] = { {"help", no_argument, NULL, 'h'}, {"config", required_argument, NULL, 'c'}, {"include", required_argument, NULL, 0}, {"anchor", required_argument, NULL, 0}, {"exclusive-zone", required_argument, NULL, 0}, {"background-color", required_argument, NULL, 0}, {"corner-radius", required_argument, NULL, 0}, {"font", required_argument, NULL, 0}, {"font-size", required_argument, NULL, 0}, {"font-features", required_argument, NULL, 0}, {"font-variations", required_argument, NULL, 0}, {"num-results", required_argument, NULL, 0}, {"selection-color", required_argument, NULL, 0}, {"selection-match-color", required_argument, NULL, 0}, {"selection-padding", required_argument, NULL, 0}, {"selection-background", required_argument, NULL, 0}, {"selection-background-padding", required_argument, NULL, 0}, {"selection-background-corner-radius", required_argument, NULL, 0}, {"outline-width", required_argument, NULL, 0}, {"outline-color", required_argument, NULL, 0}, {"text-cursor", required_argument, NULL, 0}, {"text-cursor-style", required_argument, NULL, 0}, {"text-cursor-color", required_argument, NULL, 0}, {"text-cursor-background", required_argument, NULL, 0}, {"text-cursor-corner-radius", required_argument, NULL, 0}, {"text-cursor-thickness", required_argument, NULL, 0}, {"prompt-text", required_argument, NULL, 0}, {"prompt-padding", required_argument, NULL, 0}, {"prompt-color", required_argument, NULL, 0}, {"prompt-background", required_argument, NULL, 0}, {"prompt-background-padding", required_argument, NULL, 0}, {"prompt-background-corner-radius", required_argument, NULL, 0}, {"placeholder-text", required_argument, NULL, 0}, {"placeholder-color", required_argument, NULL, 0}, {"placeholder-background", required_argument, NULL, 0}, {"placeholder-background-padding", required_argument, NULL, 0}, {"placeholder-background-corner-radius", required_argument, NULL, 0}, {"input-color", required_argument, NULL, 0}, {"input-background", required_argument, NULL, 0}, {"input-background-padding", required_argument, NULL, 0}, {"input-background-corner-radius", required_argument, NULL, 0}, {"default-result-color", required_argument, NULL, 0}, {"default-result-background", required_argument, NULL, 0}, {"default-result-background-padding", required_argument, NULL, 0}, {"default-result-background-corner-radius", required_argument, NULL, 0}, {"alternate-result-color", required_argument, NULL, 0}, {"alternate-result-background", required_argument, NULL, 0}, {"alternate-result-background-padding", required_argument, NULL, 0}, {"alternate-result-background-corner-radius", required_argument, NULL, 0}, {"result-spacing", required_argument, NULL, 0}, {"min-input-width", required_argument, NULL, 0}, {"border-width", required_argument, NULL, 0}, {"border-color", required_argument, NULL, 0}, {"text-color", required_argument, NULL, 0}, {"width", required_argument, NULL, 0}, {"height", required_argument, NULL, 0}, {"margin-top", required_argument, NULL, 0}, {"margin-bottom", required_argument, NULL, 0}, {"margin-left", required_argument, NULL, 0}, {"margin-right", required_argument, NULL, 0}, {"padding-top", required_argument, NULL, 0}, {"padding-bottom", required_argument, NULL, 0}, {"padding-left", required_argument, NULL, 0}, {"padding-right", required_argument, NULL, 0}, {"clip-to-padding", required_argument, NULL, 0}, {"horizontal", required_argument, NULL, 0}, {"hide-cursor", required_argument, NULL, 0}, {"history", required_argument, NULL, 0}, {"history-file", required_argument, NULL, 0}, {"fuzzy-match", required_argument, NULL, 0}, {"require-match", required_argument, NULL, 0}, {"auto-accept-single", required_argument, NULL, 0}, {"hide-input", required_argument, NULL, 0}, {"hidden-character", required_argument, NULL, 0}, {"drun-launch", required_argument, NULL, 0}, {"drun-print-exec", required_argument, NULL, 0}, {"terminal", required_argument, NULL, 0}, {"hint-font", required_argument, NULL, 0}, {"multi-instance", required_argument, NULL, 0}, {"ascii-input", required_argument, NULL, 0}, {"output", required_argument, NULL, 0}, {"scale", required_argument, NULL, 0}, {"late-keyboard-init", optional_argument, NULL, 'k'}, {NULL, 0, NULL, 0} }; const char *short_options = ":hc:"; static void parse_args(struct tofi *tofi, int argc, char *argv[]) { bool load_default_config = true; int option_index = 0; /* Handle errors ourselves. */ opterr = 0; /* First pass, just check for config file, help, and errors. */ optind = 1; int opt = getopt_long(argc, argv, short_options, long_options, &option_index); while (opt != -1) { if (opt == 'h') { usage(false); exit(EXIT_SUCCESS); } else if (opt == 'c') { config_load(tofi, optarg); load_default_config = false; } else if (opt == ':') { log_error("Option %s requires an argument.\n", argv[optind - 1]); usage(true); exit(EXIT_FAILURE); } else if (opt == '?') { if (optopt) { log_error("Unknown option -%c.\n", optopt); } else { log_error("Unknown option %s.\n", argv[optind - 1]); } usage(true); exit(EXIT_FAILURE); } opt = getopt_long(argc, argv, short_options, long_options, &option_index); } if (load_default_config) { config_load(tofi, NULL); } /* Second pass, parse everything else. */ optind = 1; opt = getopt_long(argc, argv, short_options, long_options, &option_index); while (opt != -1) { if (opt == 0) { if (!config_apply(tofi, long_options[option_index].name, optarg)) { exit(EXIT_FAILURE); } } else if (opt == 'k') { /* * Backwards compatibility for --late-keyboard-init not * taking an argument. */ if (optarg) { if (!config_apply(tofi, long_options[option_index].name, optarg)) { exit(EXIT_FAILURE); } } else { tofi->late_keyboard_init = true; } } opt = getopt_long(argc, argv, short_options, long_options, &option_index); } if (optind < argc) { log_error("Unexpected non-option argument '%s'.\n", argv[optind]); usage(true); exit(EXIT_FAILURE); } } static bool do_submit(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; uint32_t selection = entry->selection + entry->first_result; char *res = entry->results.buf[selection].string; if (tofi->window.entry.results.count == 0) { /* Always require a match in drun mode. */ if (tofi->require_match || entry->drun) { return false; } else { printf("%s\n", entry->input_utf8); return true; } } if (entry->drun) { /* * At this point, the list of apps is history sorted rather * than alphabetically sorted, so we can't use * desktop_vec_find_sorted(). */ struct desktop_entry *app = NULL; for (size_t i = 0; i < entry->apps.count; i++) { if (!strcmp(res, entry->apps.buf[i].name)) { app = &entry->apps.buf[i]; break; } } if (app == NULL) { log_error("Couldn't find application file! This shouldn't happen.\n"); return false; } char *path = app->path; if (tofi->drun_launch) { drun_launch(path); } else { drun_print(path, tofi->default_terminal); } } else { printf("%s\n", res); } if (tofi->use_history) { history_add( &entry->history, entry->results.buf[selection].string); if (tofi->history_file[0] == 0) { history_save_default_file(&entry->history, entry->drun); } else { history_save(&entry->history, tofi->history_file); } } return true; } static void read_clipboard(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; /* Make a copy of any text after the cursor. */ uint32_t *end_text = NULL; size_t end_text_length = entry->input_utf32_length - entry->cursor_position; if (end_text_length > 0) { end_text = xcalloc(end_text_length, sizeof(*entry->input_utf32)); memcpy(end_text, &entry->input_utf32[entry->cursor_position], end_text_length * sizeof(*entry->input_utf32)); } /* Buffer for 4 UTF-8 bytes plus a null terminator. */ char buffer[5]; memset(buffer, 0, N_ELEM(buffer)); errno = 0; bool eof = false; while (entry->cursor_position < N_ELEM(entry->input_utf32)) { for (size_t i = 0; i < 4; i++) { /* * Read input 1 byte at a time. This is slow, but easy, * and speed of pasting shouldn't really matter. */ int res = read(tofi->clipboard.fd, &buffer[i], 1); if (res == 0) { eof = true; break; } else if (res == -1) { if (errno == EAGAIN) { /* * There was no more data to be read, * but EOF hasn't been reached yet. * * This could mean more than a pipe's * capacity (64k) of data was sent, in * which case we'd potentially skip * a character, but we should hit the * input length limit long before that. */ input_refresh_results(tofi); tofi->window.surface.redraw = true; return; } log_error("Failed to read clipboard: %s\n", strerror(errno)); clipboard_finish_paste(&tofi->clipboard); return; } uint32_t unichar = utf8_to_utf32_validate(buffer); if (unichar == (uint32_t)-2) { /* The current character isn't complete yet. */ continue; } else if (unichar == (uint32_t)-1) { log_error("Invalid UTF-8 character in clipboard: %s\n", buffer); break; } else { entry->input_utf32[entry->cursor_position] = unichar; entry->cursor_position++; break; } } memset(buffer, 0, N_ELEM(buffer)); if (eof) { break; } } entry->input_utf32_length = entry->cursor_position; /* If there was any text after the cursor, re-insert it now. */ if (end_text != NULL) { for (size_t i = 0; i < end_text_length; i++) { if (entry->input_utf32_length == N_ELEM(entry->input_utf32)) { break; } entry->input_utf32[entry->input_utf32_length] = end_text[i]; entry->input_utf32_length++; } free(end_text); } entry->input_utf32[MIN(entry->input_utf32_length, N_ELEM(entry->input_utf32) - 1)] = U'\0'; clipboard_finish_paste(&tofi->clipboard); input_refresh_results(tofi); tofi->window.surface.redraw = true; } int main(int argc, char *argv[]) { /* Call log_debug to initialise the timers we use for perf checking. */ log_debug("This is tofi.\n"); /* * Set the locale to the user's default, so we can deal with non-ASCII * characters. */ setlocale(LC_ALL, ""); /* Default options. */ struct tofi tofi = { .window = { .scale = 1, .width = 1280, .height = 720, .exclusive_zone = -1, .entry = { .font_name = "Sans", .font_size = 24, .prompt_text = "run: ", .hidden_character_utf8 = u8"*", .padding_top = 8, .padding_bottom = 8, .padding_left = 8, .padding_right = 8, .clip_to_padding = true, .border_width = 12, .outline_width = 4, .background_color = {0.106f, 0.114f, 0.118f, 1.0f}, .foreground_color = {1.0f, 1.0f, 1.0f, 1.0f}, .border_color = {0.976f, 0.149f, 0.447f, 1.0f}, .outline_color = {0.031f, 0.031f, 0.0f, 1.0f}, .placeholder_theme.foreground_color = {1.0f, 1.0f, 1.0f, 0.66f}, .placeholder_theme.foreground_specified = true, .selection_theme.foreground_color = {0.976f, 0.149f, 0.447f, 1.0f}, .selection_theme.foreground_specified = true, .cursor_theme.thickness = 2 } }, .anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, .use_history = true, .require_match = true, .use_scale = true, }; wl_list_init(&tofi.output_list); if (getenv("TERMINAL") != NULL) { snprintf( tofi.default_terminal, N_ELEM(tofi.default_terminal), "%s", getenv("TERMINAL")); } parse_args(&tofi, argc, argv); log_debug("Config done.\n"); if (!tofi.multiple_instance && lock_check()) { log_error("Another instance of tofi is already running.\n"); exit(EXIT_FAILURE); } /* * Initial Wayland & XKB setup. * The first thing to do is connect a listener to the global registry, * so that we can bind to the various global objects and start talking * to Wayland. */ log_debug("Connecting to Wayland display.\n"); tofi.wl_display = wl_display_connect(NULL); if (tofi.wl_display == NULL) { log_error("Couldn't connect to Wayland display.\n"); exit(EXIT_FAILURE); } tofi.wl_registry = wl_display_get_registry(tofi.wl_display); if (!tofi.late_keyboard_init) { log_debug("Creating xkb context.\n"); tofi.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (tofi.xkb_context == NULL) { log_error("Couldn't create an XKB context.\n"); exit(EXIT_FAILURE); } } wl_registry_add_listener( tofi.wl_registry, &wl_registry_listener, &tofi); /* * After this first roundtrip, the only thing that should have happened * is our registry_global() function being called and setting up the * various global object bindings. */ log_debug("First roundtrip start.\n"); log_indent(); wl_display_roundtrip(tofi.wl_display); log_unindent(); log_debug("First roundtrip done.\n"); /* * The next roundtrip causes the listeners we set up in * registry_global() to be called. Notably, the output should be * configured, telling us the scale factor and size. */ log_debug("Second roundtrip start.\n"); log_indent(); wl_display_roundtrip(tofi.wl_display); log_unindent(); log_debug("Second roundtrip done.\n"); { /* * Determine the output we're going to appear on, and get its * fractional scale if supported. * * This seems like an ugly solution, but as far as I know * there's no way to determine the default output other than to * call get_layer_surface with NULL as the output and see which * output our surface turns up on. * * Additionally, determining fractional scale factors can * currently only be done by attaching a wp_fractional_scale to * a surface and displaying it. * * Here we set up a single pixel surface, perform the required * two roundtrips, then tear it down. tofi.default_output * should then contain the output our surface was assigned to, * and tofi.window.fractional_scale should have the scale * factor. */ log_debug("Determining output.\n"); log_indent(); struct surface surface = { .width = 1, .height = 1 }; surface.wl_surface = wl_compositor_create_surface(tofi.wl_compositor); wl_surface_add_listener( surface.wl_surface, &dummy_surface_listener, &tofi); struct wp_fractional_scale_v1 *wp_fractional_scale = NULL; if (tofi.wp_fractional_scale_manager != NULL) { wp_fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale( tofi.wp_fractional_scale_manager, surface.wl_surface); wp_fractional_scale_v1_add_listener( wp_fractional_scale, &dummy_fractional_scale_listener, &tofi); } /* * If we have a desired output, make sure we appear on it so we * can determine the correct fractional scale. */ struct wl_output *wl_output = NULL; if (tofi.target_output_name[0] != '\0') { struct output_list_element *el; wl_list_for_each(el, &tofi.output_list, link) { if (!strcmp(tofi.target_output_name, el->name)) { wl_output = el->wl_output; break; } } } struct zwlr_layer_surface_v1 *zwlr_layer_surface = zwlr_layer_shell_v1_get_layer_surface( tofi.zwlr_layer_shell, surface.wl_surface, wl_output, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "dummy"); /* * Workaround for Hyprland, where if this is not set the dummy * surface never enters an output for some reason. */ zwlr_layer_surface_v1_set_keyboard_interactivity( zwlr_layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE ); zwlr_layer_surface_v1_add_listener( zwlr_layer_surface, &dummy_layer_surface_listener, &tofi); zwlr_layer_surface_v1_set_size( zwlr_layer_surface, 1, 1); wl_surface_commit(surface.wl_surface); log_debug("First dummy roundtrip start.\n"); log_indent(); wl_display_roundtrip(tofi.wl_display); log_unindent(); log_debug("First dummy roundtrip done.\n"); log_debug("Initialising dummy surface.\n"); log_indent(); surface_init(&surface, tofi.wl_shm); surface_draw(&surface); log_unindent(); log_debug("Dummy surface initialised.\n"); log_debug("Second dummy roundtrip start.\n"); log_indent(); wl_display_roundtrip(tofi.wl_display); log_unindent(); log_debug("Second dummy roundtrip done.\n"); surface_destroy(&surface); zwlr_layer_surface_v1_destroy(zwlr_layer_surface); if (wp_fractional_scale != NULL) { wp_fractional_scale_v1_destroy(wp_fractional_scale); } wl_surface_destroy(surface.wl_surface); /* * Walk through our output list and select the one we want if * the user's asked for a specific one, otherwise just get the * default one. */ bool found_target = false; struct output_list_element *head; head = wl_container_of(tofi.output_list.next, head, link); struct output_list_element *el; struct output_list_element *tmp; if (tofi.target_output_name[0] != 0) { log_debug("Looking for output %s.\n", tofi.target_output_name); } else if (tofi.default_output != NULL) { snprintf( tofi.target_output_name, N_ELEM(tofi.target_output_name), "%s", tofi.default_output->name); /* We don't need this anymore. */ tofi.default_output = NULL; } wl_list_for_each_reverse_safe(el, tmp, &tofi.output_list, link) { if (!strcmp(tofi.target_output_name, el->name)) { found_target = true; continue; } /* * If we've already found the output we're looking for * or this isn't the first output in the list, remove * it. */ if (found_target || el != head) { wl_list_remove(&el->link); wl_output_release(el->wl_output); free(el->name); free(el); } } /* * The only output left should either be the one we want, or * the first that was advertised. */ el = wl_container_of(tofi.output_list.next, el, link); /* * If we're rotated 90 degrees, we need to swap width and * height to calculate percentages. */ switch (el->transform) { case WL_OUTPUT_TRANSFORM_90: case WL_OUTPUT_TRANSFORM_270: case WL_OUTPUT_TRANSFORM_FLIPPED_90: case WL_OUTPUT_TRANSFORM_FLIPPED_270: tofi.output_width = el->height; tofi.output_height = el->width; break; default: tofi.output_width = el->width; tofi.output_height = el->height; } tofi.window.scale = el->scale; tofi.window.transform = el->transform; log_unindent(); log_debug("Selected output %s.\n", el->name); } /* * We can now scale values and calculate any percentages, as we know * the output size and scale. */ config_fixup_values(&tofi); /* * If we were invoked as tofi-run, generate the command list. * If we were invoked as tofi-drun, generate the desktop app list. * Otherwise, just read standard input. */ if (strstr(argv[0], "-run")) { log_debug("Generating command list.\n"); log_indent(); tofi.window.entry.command_buffer = compgen_cached(); struct string_ref_vec commands = string_ref_vec_from_buffer(tofi.window.entry.command_buffer); if (tofi.use_history) { if (tofi.history_file[0] == 0) { tofi.window.entry.history = history_load_default_file(tofi.window.entry.drun); } else { tofi.window.entry.history = history_load(tofi.history_file); } tofi.window.entry.commands = compgen_history_sort(&commands, &tofi.window.entry.history); string_ref_vec_destroy(&commands); } else { tofi.window.entry.commands = commands; } log_unindent(); log_debug("Command list generated.\n"); } else if (strstr(argv[0], "-drun")) { log_debug("Generating desktop app list.\n"); log_indent(); tofi.window.entry.drun = true; struct desktop_vec apps = drun_generate_cached(); if (tofi.use_history) { if (tofi.history_file[0] == 0) { tofi.window.entry.history = history_load_default_file(tofi.window.entry.drun); } else { tofi.window.entry.history = history_load(tofi.history_file); } if (tofi.window.entry.drun) { drun_history_sort(&apps, &tofi.window.entry.history); } } struct string_ref_vec commands = string_ref_vec_create(); for (size_t i = 0; i < apps.count; i++) { string_ref_vec_add(&commands, apps.buf[i].name); } tofi.window.entry.commands = commands; tofi.window.entry.apps = apps; log_unindent(); log_debug("App list generated.\n"); } else { log_debug("Reading stdin.\n"); char *buf = read_stdin(!tofi.ascii_input); tofi.window.entry.command_buffer = buf; tofi.window.entry.commands = string_ref_vec_from_buffer(buf); if (tofi.use_history) { if (tofi.history_file[0] == 0) { tofi.use_history = false; } else { tofi.window.entry.history = history_load(tofi.history_file); string_ref_vec_history_sort(&tofi.window.entry.commands, &tofi.window.entry.history); } } log_debug("Result list generated.\n"); } tofi.window.entry.results = string_ref_vec_copy(&tofi.window.entry.commands); if (tofi.auto_accept_single && tofi.window.entry.results.count == 1) { log_debug("Only one result, exiting.\n"); do_submit(&tofi); return EXIT_SUCCESS; } /* * Next, we create the Wayland surface, which takes on the * layer shell role. */ log_debug("Creating main window surface.\n"); tofi.window.surface.wl_surface = wl_compositor_create_surface(tofi.wl_compositor); wl_surface_add_listener( tofi.window.surface.wl_surface, &wl_surface_listener, &tofi); if (tofi.window.width == 0 || tofi.window.height == 0) { /* * Workaround for compatibility with legacy behaviour. * * Before the fractional_scale protocol was released, there was * no way for a client to know whether a fractional scale * factor had been set, meaning percentage-based dimensions * were incorrect. As a workaround for full-size windows, we * allowed specifying 0 for the width / height, which caused * zwlr_layer_shell to tell us the correct size to use. * * To make fractional scaling work, we have to use * wp_viewporter, and no longer need to set the buffer scale. * However, viewporter doesn't allow specifying 0 for * destination width or height. As a workaround, if 0 size is * set, don't use viewporter, warn the user and set the buffer * scale here. */ log_warning("Width or height set to 0, disabling fractional scaling support.\n"); log_warning("If your compositor supports the fractional scale protocol, percentages are preferred.\n"); tofi.window.fractional_scale = 0; wl_surface_set_buffer_scale( tofi.window.surface.wl_surface, tofi.window.scale); } else if (tofi.wp_viewporter == NULL) { /* * We also could be running on a Wayland compositor which * doesn't support wp_viewporter, in which case we need to use * the old scaling method. */ log_warning("Using an outdated compositor, " "fractional scaling will not work properly.\n"); tofi.window.fractional_scale = 0; wl_surface_set_buffer_scale( tofi.window.surface.wl_surface, tofi.window.scale); } /* Grab the first (and only remaining) output from our list. */ struct wl_output *wl_output; { struct output_list_element *el; el = wl_container_of(tofi.output_list.next, el, link); wl_output = el->wl_output; } tofi.window.zwlr_layer_surface = zwlr_layer_shell_v1_get_layer_surface( tofi.zwlr_layer_shell, tofi.window.surface.wl_surface, wl_output, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "launcher"); zwlr_layer_surface_v1_set_keyboard_interactivity( tofi.window.zwlr_layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE); zwlr_layer_surface_v1_add_listener( tofi.window.zwlr_layer_surface, &zwlr_layer_surface_listener, &tofi); zwlr_layer_surface_v1_set_anchor( tofi.window.zwlr_layer_surface, tofi.anchor); zwlr_layer_surface_v1_set_exclusive_zone( tofi.window.zwlr_layer_surface, tofi.window.exclusive_zone); zwlr_layer_surface_v1_set_margin( tofi.window.zwlr_layer_surface, tofi.window.margin_top, tofi.window.margin_right, tofi.window.margin_bottom, tofi.window.margin_left); /* * No matter whether we're scaling via Cairo or not, we're presenting a * scaled buffer to Wayland, so scale the window size here if we * haven't already done so. */ zwlr_layer_surface_v1_set_size( tofi.window.zwlr_layer_surface, tofi.window.width, tofi.window.height); /* * Set up a viewport for our surface, necessary for fractional scaling. */ if (tofi.wp_viewporter != NULL) { tofi.window.wp_viewport = wp_viewporter_get_viewport( tofi.wp_viewporter, tofi.window.surface.wl_surface); if (tofi.window.width > 0 && tofi.window.height > 0) { wp_viewport_set_destination( tofi.window.wp_viewport, tofi.window.width, tofi.window.height); } } /* Commit the surface to finalise setup. */ wl_surface_commit(tofi.window.surface.wl_surface); /* * Create a data device and setup a listener for data offers. This is * required for clipboard support. */ tofi.wl_data_device = wl_data_device_manager_get_data_device( tofi.wl_data_device_manager, tofi.wl_seat); wl_data_device_add_listener( tofi.wl_data_device, &wl_data_device_listener, &tofi.clipboard); /* * Now that we've done all our Wayland-related setup, we do another * roundtrip. This should cause the layer surface window to be * configured, after which we're ready to start drawing to the screen. */ log_debug("Third roundtrip start.\n"); log_indent(); wl_display_roundtrip(tofi.wl_display); log_unindent(); log_debug("Third roundtrip done.\n"); /* * Create the various structures for our window surface. This needs to * be done before calling entry_init as that performs some initial * drawing, and surface_init allocates the buffers we'll be drawing to. */ log_debug("Initialising window surface.\n"); log_indent(); surface_init(&tofi.window.surface, tofi.wl_shm); log_unindent(); log_debug("Window surface initialised.\n"); /* * Initialise the structures for rendering the entry. * Cairo needs to know the size of the surface it's creating, and * there's no way to resize it aside from tearing everything down and * starting again, so we make sure to do this after we've determined * our output's scale factor. This stops us being able to change the * scale factor after startup, but this is just a launcher, which * shouldn't be moving between outputs while running. */ log_debug("Initialising renderer.\n"); log_indent(); { /* * No matter how we're scaling (with fractions, integers or not * at all), we pass a fractional scale factor (the numerator of * a fraction with denominator 120) to our setup function for * ease. */ uint32_t scale = 120; if (tofi.use_scale) { if (tofi.window.fractional_scale != 0) { scale = tofi.window.fractional_scale; } else { scale = tofi.window.scale * 120; } } entry_init( &tofi.window.entry, tofi.window.surface.shm_pool_data, tofi.window.surface.width, tofi.window.surface.height, scale); } log_unindent(); log_debug("Renderer initialised.\n"); /* Perform an initial render. */ surface_draw(&tofi.window.surface); /* * entry_init() left the second of the two buffers we use for * double-buffering unpainted to lower startup time, as described * there. Here, we flush our first, finished buffer to the screen, then * copy over the image to the second buffer before we need to use it in * the main loop. This ensures we paint to the screen as quickly as * possible after startup. */ wl_display_roundtrip(tofi.wl_display); log_debug("Initialising second buffer.\n"); memcpy( cairo_image_surface_get_data(tofi.window.entry.cairo[1].surface), cairo_image_surface_get_data(tofi.window.entry.cairo[0].surface), tofi.window.surface.width * tofi.window.surface.height * sizeof(uint32_t) ); log_debug("Second buffer initialised.\n"); /* We've just rendered, so we don't need to do it again right now. */ tofi.window.surface.redraw = false; /* If we delayed keyboard initialisation, do it now */ if (tofi.late_keyboard_init) { log_debug("Creating xkb context.\n"); tofi.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (tofi.xkb_context == NULL) { log_error("Couldn't create an XKB context.\n"); exit(EXIT_FAILURE); } log_debug("Configuring keyboard.\n"); struct xkb_keymap *xkb_keymap = xkb_keymap_new_from_string( tofi.xkb_context, tofi.xkb_keymap_string, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); struct xkb_state *xkb_state = xkb_state_new(xkb_keymap); xkb_keymap_unref(tofi.xkb_keymap); xkb_state_unref(tofi.xkb_state); tofi.xkb_keymap = xkb_keymap; tofi.xkb_state = xkb_state; free(tofi.xkb_keymap_string); tofi.late_keyboard_init = false; log_debug("Keyboard configured.\n"); } /* * Main event loop. * See the wl_display(3) man page for an explanation of the * order of the various functions called here. */ while (!tofi.closed) { struct pollfd pollfds[2] = {{0}, {0}}; pollfds[0].fd = wl_display_get_fd(tofi.wl_display); /* Make sure we're ready to receive events on the main queue. */ while (wl_display_prepare_read(tofi.wl_display) != 0) { wl_display_dispatch_pending(tofi.wl_display); } /* Make sure all our requests have been sent to the server. */ while (wl_display_flush(tofi.wl_display) != 0) { pollfds[0].events = POLLOUT; poll(&pollfds[0], 1, -1); } /* * Set time to wait for poll() to -1 (unlimited), unless * there's some key repeating going on. */ int timeout = -1; if (tofi.repeat.active) { int64_t wait = (int64_t)tofi.repeat.next - (int64_t)gettime_ms(); if (wait >= 0) { timeout = wait; } else { timeout = 0; } } pollfds[0].events = POLLIN | POLLPRI; int res; if (tofi.clipboard.fd == 0) { res = poll(&pollfds[0], 1, timeout); } else { /* * We're trying to paste from the clipboard, which is * done by reading from a pipe, so poll that file * descriptor as well. */ pollfds[1].fd = tofi.clipboard.fd; pollfds[1].events = POLLIN | POLLPRI; res = poll(pollfds, 2, timeout); } if (res == 0) { /* * No events to process and no error - we presumably * have a key repeat to handle. */ wl_display_cancel_read(tofi.wl_display); if (tofi.repeat.active) { int64_t wait = (int64_t)tofi.repeat.next - (int64_t)gettime_ms(); if (wait <= 0) { input_handle_keypress(&tofi, tofi.repeat.keycode); tofi.repeat.next += 1000 / tofi.repeat.rate; } } } else if (res < 0) { /* There was an error polling the display. */ wl_display_cancel_read(tofi.wl_display); } else { if (pollfds[0].revents & (POLLIN | POLLPRI)) { /* Events to read, so put them on the queue. */ wl_display_read_events(tofi.wl_display); } else { /* * No events to read - we were woken up to * handle clipboard data. */ wl_display_cancel_read(tofi.wl_display); } if (pollfds[1].revents & (POLLIN | POLLPRI)) { /* Read clipboard data. */ if (tofi.clipboard.fd > 0) { read_clipboard(&tofi); } } if (pollfds[1].revents & POLLHUP) { /* * The other end of the clipboard pipe has * closed, cleanup. */ clipboard_finish_paste(&tofi.clipboard); } } /* Handle any events we read. */ wl_display_dispatch_pending(tofi.wl_display); if (tofi.window.surface.redraw) { entry_update(&tofi.window.entry); surface_draw(&tofi.window.surface); tofi.window.surface.redraw = false; } if (tofi.submit) { tofi.submit = false; if (do_submit(&tofi)) { break; } } } log_debug("Window closed, performing cleanup.\n"); #ifdef DEBUG /* * For debug builds, try to cleanup as much as possible, to make using * e.g. Valgrind easier. There's still a few unavoidable leaks though, * mostly from Pango, and Cairo holds onto quite a bit of cached data * (without leaking it) */ surface_destroy(&tofi.window.surface); entry_destroy(&tofi.window.entry); if (tofi.window.wp_viewport != NULL) { wp_viewport_destroy(tofi.window.wp_viewport); } zwlr_layer_surface_v1_destroy(tofi.window.zwlr_layer_surface); wl_surface_destroy(tofi.window.surface.wl_surface); if (tofi.wl_keyboard != NULL) { wl_keyboard_release(tofi.wl_keyboard); } if (tofi.wl_pointer != NULL) { wl_pointer_release(tofi.wl_pointer); } wl_compositor_destroy(tofi.wl_compositor); if (tofi.clipboard.wl_data_offer != NULL) { wl_data_offer_destroy(tofi.clipboard.wl_data_offer); } wl_data_device_release(tofi.wl_data_device); wl_data_device_manager_destroy(tofi.wl_data_device_manager); wl_seat_release(tofi.wl_seat); { struct output_list_element *el; struct output_list_element *tmp; wl_list_for_each_safe(el, tmp, &tofi.output_list, link) { wl_list_remove(&el->link); wl_output_release(el->wl_output); free(el->name); free(el); } } wl_shm_destroy(tofi.wl_shm); if (tofi.wp_fractional_scale_manager != NULL) { wp_fractional_scale_manager_v1_destroy(tofi.wp_fractional_scale_manager); } if (tofi.wp_viewporter != NULL) { wp_viewporter_destroy(tofi.wp_viewporter); } zwlr_layer_shell_v1_destroy(tofi.zwlr_layer_shell); xkb_state_unref(tofi.xkb_state); xkb_keymap_unref(tofi.xkb_keymap); xkb_context_unref(tofi.xkb_context); wl_registry_destroy(tofi.wl_registry); if (tofi.window.entry.drun) { desktop_vec_destroy(&tofi.window.entry.apps); } if (tofi.window.entry.command_buffer != NULL) { free(tofi.window.entry.command_buffer); } string_ref_vec_destroy(&tofi.window.entry.commands); string_ref_vec_destroy(&tofi.window.entry.results); if (tofi.use_history) { history_destroy(&tofi.window.entry.history); } #endif /* * For release builds, skip straight to display disconnection and quit. */ wl_display_roundtrip(tofi.wl_display); wl_display_disconnect(tofi.wl_display); log_debug("Finished, exiting.\n"); return EXIT_SUCCESS; } tofi-0.9.1/src/main_compgen.c000066400000000000000000000005501441474151400160560ustar00rootroot00000000000000#include #include #include "compgen.h" #include "string_vec.h" int main() { char *buf = compgen_cached(); struct string_ref_vec commands = string_ref_vec_from_buffer(buf); for (size_t i = 0; i < commands.count; i++) { fputs(commands.buf[i].string, stdout); fputc('\n', stdout); } string_ref_vec_destroy(&commands); free(buf); } tofi-0.9.1/src/mkdirp.c000066400000000000000000000015321441474151400147110ustar00rootroot00000000000000#include #include #include #include #include "log.h" #include "mkdirp.h" #include "xmalloc.h" bool mkdirp(const char *path) { struct stat statbuf; if (stat(path, &statbuf) == 0) { /* If the file exists, we don't need to do anything. */ return true; } /* * Walk down the path, creating directories as we go. * This works by repeatedly finding the next / in path, then calling * mkdir() on the string up to that point. */ char *tmp = xstrdup(path); char *cursor = tmp; while ((cursor = strchr(cursor + 1, '/')) != NULL) { *cursor = '\0'; log_debug("Creating directory %s\n", tmp); if (mkdir(tmp, 0700) != 0 && errno != EEXIST) { log_error( "Error creating file path: %s.\n", strerror(errno)); free(tmp); return false; } *cursor = '/'; } free(tmp); return true; } tofi-0.9.1/src/mkdirp.h000066400000000000000000000001571441474151400147200ustar00rootroot00000000000000#ifndef MKDIRP_H #define MKDIRP_H #include bool mkdirp(const char *path); #endif /* MKDIRP_H */ tofi-0.9.1/src/nelem.h000066400000000000000000000003461441474151400145320ustar00rootroot00000000000000/* * Copyright (C) 2021 Philip Jones * * Licensed under the MIT License. * See either the LICENSE file, or: * * https://opensource.org/licenses/MIT * */ #ifndef N_ELEM #define N_ELEM(x) (sizeof(x) / sizeof(*(x))) #endif tofi-0.9.1/src/scale.c000066400000000000000000000006061441474151400145130ustar00rootroot00000000000000#include #include /* * In order to correctly scale by fractions of 120 (used by * wp_fractional_scale_v1), we need to bias the result before rounding. */ uint32_t scale_apply(uint32_t base, uint32_t scale) { return round(base * (scale / 120.) + 1e-6); } uint32_t scale_apply_inverse(uint32_t base, uint32_t scale) { return round(base * (120. / scale) + 1e-6); } tofi-0.9.1/src/scale.h000066400000000000000000000002761441474151400145230ustar00rootroot00000000000000#ifndef SCALE_H #define SCALE_H #include uint32_t scale_apply(uint32_t base, uint32_t scale); uint32_t scale_apply_inverse(uint32_t base, uint32_t scale); #endif /* SCALE_H */ tofi-0.9.1/src/shm.c000066400000000000000000000024031441474151400142100ustar00rootroot00000000000000#include #include #include #include #include #include "shm.h" /* These two functions aren't used on linux. */ #ifndef __linux__ static void randname(char *buf) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); long r = ts.tv_nsec; for (int i = 0; i < 6; ++i) { buf[i] = 'A'+(r&15)+(r&16)*2; r >>= 5; } } static int create_shm_file(void) { int retries = 100; do { char name[] = "/wl_shm-XXXXXX"; randname(name + sizeof(name) - 7); --retries; int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); if (fd >= 0) { shm_unlink(name); return fd; } } while (retries > 0 && errno == EEXIST); return -1; } #endif int shm_allocate_file(size_t size) { #ifdef __linux__ /* * On linux, we can just use memfd_create(). This is both simpler and * potentially allows usage of Transparent HugePages, which speed up * the first paint of a large screen buffer. * * This isn't available on *BSD, which we could conceivably be running * on. */ int fd = memfd_create("wl_shm", 0); #else int fd = create_shm_file(); #endif if (fd < 0) return -1; int ret; do { ret = ftruncate(fd, size); } while (ret < 0 && errno == EINTR); if (ret < 0) { close(fd); return -1; } return fd; } tofi-0.9.1/src/shm.h000066400000000000000000000001521441474151400142140ustar00rootroot00000000000000#ifndef SHM_H #define SHM_H #include int shm_allocate_file(size_t size); #endif /* SHM_H */ tofi-0.9.1/src/string_vec.c000066400000000000000000000132631441474151400155720ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "fuzzy_match.h" #include "history.h" #include "string_vec.h" #include "unicode.h" #include "xmalloc.h" static int cmpstringp(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; /* * Ensure any NULL strings are shoved to the end. */ if (str1->string == NULL) { return 1; } if (str2->string == NULL) { return -1; } return strcmp(str1->string, str2->string); } static int cmpscorep(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; int hist_diff = str2->history_score - str1->history_score; int search_diff = str2->search_score - str1->search_score; return hist_diff + search_diff; } static int cmphistoryp(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; return str2->history_score - str1->history_score; } struct string_vec string_vec_create(void) { struct string_vec vec = { .count = 0, .size = 128, .buf = xcalloc(128, sizeof(*vec.buf)), }; return vec; } struct string_ref_vec string_ref_vec_create(void) { struct string_ref_vec vec = { .count = 0, .size = 128, .buf = xcalloc(128, sizeof(*vec.buf)), }; return vec; } void string_vec_destroy(struct string_vec *restrict vec) { for (size_t i = 0; i < vec->count; i++) { free(vec->buf[i].string); } free(vec->buf); } void string_ref_vec_destroy(struct string_ref_vec *restrict vec) { free(vec->buf); } struct string_ref_vec string_ref_vec_copy(const struct string_ref_vec *restrict vec) { struct string_ref_vec copy = { .count = vec->count, .size = vec->size, .buf = xcalloc(vec->size, sizeof(*copy.buf)), }; for (size_t i = 0; i < vec->count; i++) { copy.buf[i].string = vec->buf[i].string; copy.buf[i].search_score = vec->buf[i].search_score; copy.buf[i].history_score = vec->buf[i].history_score; } return copy; } void string_vec_add(struct string_vec *restrict vec, const char *restrict str) { if (!utf8_validate(str)) { return; } if (vec->count == vec->size) { vec->size *= 2; vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); } vec->buf[vec->count].string = utf8_normalize(str); if (vec->buf[vec->count].string == NULL) { vec->buf[vec->count].string = xstrdup(str); } vec->buf[vec->count].search_score = 0; vec->buf[vec->count].history_score = 0; vec->count++; } void string_ref_vec_add(struct string_ref_vec *restrict vec, char *restrict str) { if (vec->count == vec->size) { vec->size *= 2; vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); } vec->buf[vec->count].string = str; vec->buf[vec->count].search_score = 0; vec->buf[vec->count].history_score = 0; vec->count++; } void string_vec_sort(struct string_vec *restrict vec) { qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmpstringp); } void string_ref_vec_history_sort(struct string_ref_vec *restrict vec, struct history *history) { /* * To find elements without assuming the vector is pre-sorted, we use a * hash table, which results in O(N+M) work (rather than O(N*M) for * linear search. */ GHashTable *hash = g_hash_table_new(g_str_hash, g_str_equal); for (size_t i = 0; i < vec->count; i++) { g_hash_table_insert(hash, vec->buf[i].string, &vec->buf[i]); } for (size_t i = 0; i < history->count; i++) { struct scored_string_ref *res = g_hash_table_lookup(hash, history->buf[i].name); if (res == NULL) { continue; } res->history_score = history->buf[i].run_count; } g_hash_table_unref(hash); qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmphistoryp); } void string_vec_uniq(struct string_vec *restrict vec) { size_t count = vec->count; for (size_t i = 1; i < vec->count; i++) { if (!strcmp(vec->buf[i].string, vec->buf[i-1].string)) { free(vec->buf[i-1].string); vec->buf[i-1].string = NULL; count--; } } string_vec_sort(vec); vec->count = count; } struct scored_string *string_vec_find_sorted(struct string_vec *restrict vec, const char * str) { return bsearch(&str, vec->buf, vec->count, sizeof(vec->buf[0]), cmpstringp); } struct scored_string_ref *string_ref_vec_find_sorted(struct string_ref_vec *restrict vec, const char * str) { return bsearch(&str, vec->buf, vec->count, sizeof(vec->buf[0]), cmpstringp); } struct string_ref_vec string_ref_vec_filter( const struct string_ref_vec *restrict vec, const char *restrict substr, bool fuzzy) { if (substr[0] == '\0') { return string_ref_vec_copy(vec); } struct string_ref_vec filt = string_ref_vec_create(); for (size_t i = 0; i < vec->count; i++) { int32_t search_score; if (fuzzy) { search_score = fuzzy_match_words(substr, vec->buf[i].string); } else { search_score = fuzzy_match_simple_words(substr, vec->buf[i].string); } if (search_score != INT32_MIN) { string_ref_vec_add(&filt, vec->buf[i].string); filt.buf[filt.count - 1].search_score = search_score; filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; } } /* Sort the results by their search score. */ qsort(filt.buf, filt.count, sizeof(filt.buf[0]), cmpscorep); return filt; } struct string_ref_vec string_ref_vec_from_buffer(char *buffer) { struct string_ref_vec vec = string_ref_vec_create(); char *saveptr = NULL; char *line = strtok_r(buffer, "\n", &saveptr); while (line != NULL) { string_ref_vec_add(&vec, line); line = strtok_r(NULL, "\n", &saveptr); } return vec; } tofi-0.9.1/src/string_vec.h000066400000000000000000000041701441474151400155740ustar00rootroot00000000000000#ifndef STRING_VEC_H #define STRING_VEC_H #include #include #include #include #include "history.h" struct scored_string { char *string; int32_t search_score; int32_t history_score; }; struct string_vec { size_t count; size_t size; struct scored_string *buf; }; [[nodiscard("memory leaked")]] struct string_vec string_vec_create(void); void string_vec_destroy(struct string_vec *restrict vec); void string_vec_add(struct string_vec *restrict vec, const char *restrict str); void string_vec_sort(struct string_vec *restrict vec); struct scored_string *string_vec_find_sorted(struct string_vec *restrict vec, const char *str); /* * Like a string_vec, but only store a reference to the corresponding string * rather than copying it. Although compatible with the string_vec struct, we * create a new struct to make the compiler complain if we mix them up. */ struct scored_string_ref { char *string; int32_t search_score; int32_t history_score; }; struct string_ref_vec { size_t count; size_t size; struct scored_string_ref *buf; }; /* * Although some of these functions are identical to the corresponding * string_vec ones, we create new functions to avoid potentially mixing up * the two. */ [[nodiscard("memory leaked")]] struct string_ref_vec string_ref_vec_create(void); void string_ref_vec_destroy(struct string_ref_vec *restrict vec); [[nodiscard("memory leaked")]] struct string_ref_vec string_ref_vec_copy(const struct string_ref_vec *restrict vec); void string_ref_vec_add(struct string_ref_vec *restrict vec, char *restrict str); void string_ref_vec_history_sort(struct string_ref_vec *restrict vec, struct history *history); void string_vec_uniq(struct string_vec *restrict vec); struct scored_string_ref *string_ref_vec_find_sorted(struct string_ref_vec *restrict vec, const char *str); [[nodiscard("memory leaked")]] struct string_ref_vec string_ref_vec_filter( const struct string_ref_vec *restrict vec, const char *restrict substr, bool fuzzy); [[nodiscard("memory leaked")]] struct string_ref_vec string_ref_vec_from_buffer(char *buffer); #endif /* STRING_VEC_H */ tofi-0.9.1/src/surface.c000066400000000000000000000043171441474151400150570ustar00rootroot00000000000000#include #include #include #include "log.h" #include "shm.h" #include "surface.h" #undef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) void surface_init( struct surface *surface, struct wl_shm *wl_shm) { const int height = surface->height; const int width = surface->width; /* Assume 4 bytes per pixel for WL_SHM_FORMAT_ARGB8888 */ const int stride = width * 4; surface->stride = stride; /* Double-buffered pool, so allocate space for two windows */ surface->shm_pool_size = height * stride * 2; surface->shm_pool_fd = shm_allocate_file(surface->shm_pool_size); surface->shm_pool_data = mmap( NULL, surface->shm_pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, surface->shm_pool_fd, 0); #ifdef __linux__ /* * On linux, ask for Transparent HugePages if available and our * buffer's at least 2MiB. This can greatly speed up the first * cairo_paint() by reducing page faults, but unfortunately is disabled * for shared memory at the time of writing. * * MADV_HUGEPAGE isn't available on *BSD, which we could conceivably be * running on. */ if (surface->shm_pool_size >= (2 << 20)) { madvise(surface->shm_pool_data, surface->shm_pool_size, MADV_HUGEPAGE); } #endif surface->wl_shm_pool = wl_shm_create_pool( wl_shm, surface->shm_pool_fd, surface->shm_pool_size); for (int i = 0; i < 2; i++) { int offset = height * stride * i; surface->buffers[i] = wl_shm_pool_create_buffer( surface->wl_shm_pool, offset, width, height, stride, WL_SHM_FORMAT_ARGB8888); } log_debug("Created shm file with size %d KiB.\n", surface->shm_pool_size / 1024); } void surface_destroy(struct surface *surface) { wl_shm_pool_destroy(surface->wl_shm_pool); munmap(surface->shm_pool_data, surface->shm_pool_size); surface->shm_pool_data = NULL; close(surface->shm_pool_fd); wl_buffer_destroy(surface->buffers[0]); wl_buffer_destroy(surface->buffers[1]); } void surface_draw(struct surface *surface) { wl_surface_attach(surface->wl_surface, surface->buffers[surface->index], 0, 0); wl_surface_damage_buffer(surface->wl_surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_commit(surface->wl_surface); surface->index = !surface->index; } tofi-0.9.1/src/surface.h000066400000000000000000000010731441474151400150600ustar00rootroot00000000000000#ifndef SURFACE_H #define SURFACE_H #include #include #include #include "color.h" struct surface { struct wl_surface *wl_surface; struct wl_shm_pool *wl_shm_pool; int32_t width; int32_t height; int32_t stride; int index; struct wl_buffer *buffers[2]; int shm_pool_size; int shm_pool_fd; uint8_t *shm_pool_data; bool redraw; }; void surface_init( struct surface *surface, struct wl_shm *wl_shm); void surface_destroy(struct surface *surface); void surface_draw(struct surface *surface); #endif /* SURFACE_H */ tofi-0.9.1/src/tofi.h000066400000000000000000000050061441474151400143710ustar00rootroot00000000000000#ifndef TOFI_H #define TOFI_H #include #include #include #include #include "clipboard.h" #include "color.h" #include "entry.h" #include "surface.h" #include "wlr-layer-shell-unstable-v1.h" #include "fractional-scale-v1.h" #define MAX_OUTPUT_NAME_LEN 256 #define MAX_TERMINAL_NAME_LEN 256 #define MAX_HISTORY_FILE_NAME_LEN 256 struct output_list_element { struct wl_list link; struct wl_output *wl_output; char *name; uint32_t width; uint32_t height; int32_t scale; int32_t transform; }; struct tofi { /* Wayland globals */ struct wl_display *wl_display; struct wl_registry *wl_registry; struct wl_compositor *wl_compositor; struct wl_seat *wl_seat; struct wl_shm *wl_shm; struct wl_data_device_manager *wl_data_device_manager; struct wl_data_device *wl_data_device; struct wp_viewporter *wp_viewporter; struct wp_fractional_scale_manager_v1 *wp_fractional_scale_manager; struct zwlr_layer_shell_v1 *zwlr_layer_shell; struct wl_list output_list; struct output_list_element *default_output; /* Wayland objects */ struct wl_keyboard *wl_keyboard; struct wl_pointer *wl_pointer; /* Keyboard objects */ char *xkb_keymap_string; struct xkb_state *xkb_state; struct xkb_context *xkb_context; struct xkb_keymap *xkb_keymap; /* State */ bool submit; bool closed; int32_t output_width; int32_t output_height; struct clipboard clipboard; struct { struct surface surface; struct wp_viewport *wp_viewport; struct zwlr_layer_surface_v1 *zwlr_layer_surface; struct entry entry; uint32_t width; uint32_t height; uint32_t scale; uint32_t fractional_scale; int32_t transform; int32_t exclusive_zone; int32_t margin_top; int32_t margin_bottom; int32_t margin_left; int32_t margin_right; bool width_is_percent; bool height_is_percent; bool exclusive_zone_is_percent; bool margin_top_is_percent; bool margin_bottom_is_percent; bool margin_left_is_percent; bool margin_right_is_percent; } window; struct { uint32_t rate; uint32_t delay; uint32_t keycode; uint32_t next; bool active; } repeat; /* Options */ uint32_t anchor; bool ascii_input; bool hide_cursor; bool use_history; bool use_scale; bool late_keyboard_init; bool drun_launch; bool drun_print_exec; bool fuzzy_match; bool require_match; bool auto_accept_single; bool multiple_instance; char target_output_name[MAX_OUTPUT_NAME_LEN]; char default_terminal[MAX_TERMINAL_NAME_LEN]; char history_file[MAX_HISTORY_FILE_NAME_LEN]; }; #endif /* TOFI_H */ tofi-0.9.1/src/unicode.c000066400000000000000000000041711441474151400150530ustar00rootroot00000000000000#include #include #include "unicode.h" uint8_t utf32_to_utf8(uint32_t c, char *buf) { return g_unichar_to_utf8(c, buf); } uint32_t utf8_to_utf32(const char *s) { return g_utf8_get_char(s); } uint32_t utf8_to_utf32_validate(const char *s) { return g_utf8_get_char_validated(s, -1); } uint32_t *utf8_string_to_utf32_string(const char *s) { return g_utf8_to_ucs4_fast(s, -1, NULL); } uint32_t utf32_isprint(uint32_t c) { return g_unichar_isprint(c); } uint32_t utf32_isspace(uint32_t c) { return g_unichar_isspace(c); } uint32_t utf32_isupper(uint32_t c) { return g_unichar_isupper(c); } uint32_t utf32_islower(uint32_t c) { return g_unichar_islower(c); } uint32_t utf32_isalnum(uint32_t c) { return g_unichar_isalnum(c); } uint32_t utf32_toupper(uint32_t c) { return g_unichar_toupper(c); } uint32_t utf32_tolower(uint32_t c) { return g_unichar_tolower(c); } size_t utf32_strlen(const uint32_t *s) { size_t len = 0; while (s[len] != U'\0') { len++; } return len; } char *utf8_next_char(const char *s) { return g_utf8_next_char(s); } char *utf8_prev_char(const char *s) { return g_utf8_prev_char(s); } char *utf8_strchr(const char *s, uint32_t c) { return g_utf8_strchr(s, -1, c); } char *utf8_strcasechr(const char *s, uint32_t c) { c = g_unichar_tolower(c); const char *p = s; while (*p != '\0' && g_unichar_tolower(g_utf8_get_char(p)) != c) { p = g_utf8_next_char(p); } if (*p == '\0') { return NULL; } return (char *)p; } size_t utf8_strlen(const char *s) { return g_utf8_strlen(s, -1); } char *utf8_strcasestr(const char * restrict haystack, const char * restrict needle) { char *h = g_utf8_casefold(haystack, -1); char *n = g_utf8_casefold(needle, -1); char *cmp = strstr(h, n); char *ret; if (cmp == NULL) { ret = NULL; } else { ret = (char *)haystack + (cmp - h); } free(h); free(n); return ret; } char *utf8_normalize(const char *s) { return g_utf8_normalize(s, -1, G_NORMALIZE_DEFAULT); } char *utf8_compose(const char *s) { return g_utf8_normalize(s, -1, G_NORMALIZE_DEFAULT_COMPOSE); } bool utf8_validate(const char *s) { return g_utf8_validate(s, -1, NULL); } tofi-0.9.1/src/unicode.h000066400000000000000000000017471441474151400150660ustar00rootroot00000000000000#ifndef UNICODE_H #define UNICODE_H #include #include #include uint8_t utf32_to_utf8(uint32_t c, char *buf); uint32_t utf8_to_utf32(const char *s); uint32_t utf8_to_utf32_validate(const char *s); uint32_t *utf8_string_to_utf32_string(const char *s); uint32_t utf32_isprint(uint32_t c); uint32_t utf32_isspace(uint32_t c); uint32_t utf32_isupper(uint32_t c); uint32_t utf32_islower(uint32_t c); uint32_t utf32_isalnum(uint32_t c); uint32_t utf32_toupper(uint32_t c); uint32_t utf32_tolower(uint32_t c); size_t utf32_strlen(const uint32_t *s); char *utf8_next_char(const char *s); char *utf8_prev_char(const char *s); char *utf8_strchr(const char *s, uint32_t c); char *utf8_strcasechr(const char *s, uint32_t c); size_t utf8_strlen(const char *s); char *utf8_strcasestr(const char * restrict haystack, const char * restrict needle); char *utf8_normalize(const char *s); char *utf8_compose(const char *s); bool utf8_validate(const char *s); #endif /* UNICODE_H */ tofi-0.9.1/src/xmalloc.c000066400000000000000000000016661441474151400150720ustar00rootroot00000000000000#include #include #include "log.h" #include "xmalloc.h" void *xmalloc(size_t size) { void *ptr = malloc(size); if (ptr != NULL) { //log_debug("Allocated %zu bytes.\n", size); return ptr; } else { log_error("Out of memory, exiting.\n"); exit(EXIT_FAILURE); } } void *xcalloc(size_t nmemb, size_t size) { void *ptr = calloc(nmemb, size); if (ptr != NULL) { //log_debug("Allocated %zux%zu bytes.\n", nmemb, size); return ptr; } else { log_error("Out of memory, exiting.\n"); exit(EXIT_FAILURE); } } void *xrealloc(void *ptr, size_t size) { ptr = realloc(ptr, size); if (ptr != NULL) { //log_debug("Reallocated to %zu bytes.\n", size); return ptr; } else { log_error("Out of memory, exiting.\n"); exit(EXIT_FAILURE); } } char *xstrdup(const char *s) { char *ptr = strdup(s); if (ptr != NULL) { return ptr; } else { log_error("Out of memory, exiting.\n"); exit(EXIT_FAILURE); } } tofi-0.9.1/src/xmalloc.h000066400000000000000000000006151441474151400150700ustar00rootroot00000000000000#ifndef XMALLOC_H #define XMALLOC_H #include [[nodiscard("memory leaked")]] [[gnu::malloc]] void *xmalloc(size_t size); [[nodiscard("memory leaked")]] [[gnu::malloc]] void *xcalloc(size_t nmemb, size_t size); [[nodiscard("memory leaked")]] void *xrealloc(void *ptr, size_t size); [[nodiscard("memory leaked")]] [[gnu::malloc]] char *xstrdup(const char *s); #endif /* XMALLOC_H */ tofi-0.9.1/startup_performance.svg000066400000000000000000022326121441474151400173030ustar00rootroot00000000000000 This is tofi.Loading config file themes/dmenu.Config doneConnecting to Wayland display.Creating xkb context.First roundtrip start. Bound to shm 1. Bound to compositor 4. Bound to data device manager 6. Bound to zwlr_layer_shell_v1 12. Bound to seat 43. Bound to output 46.First roundtrip done.Second roundtrip start. Got keyboard from seat. Got pointer from seat. Output configuration done.Second roundtrip done.Selected output DP-1.Generating command list. Retrieving PATH. Retrieving cache location. Cache up to date, loading. Moving already known programs to the front.Command list generated.Creating main window surface.Third roundtrip start. Layer surface configure, 2560 x 30. Layer surface configure, 2560 x 30.Third roundtrip done.Initialising window surface. Created shm file with size 600 KiB.Window surface initialised.Initialising renderer. Drawing window. Creating FreeType library. Loading FreeType font. Creating Harfbuzz font. Creating Harfbuzz buffer. Creating Cairo font. Initial text render. Drew 10 results.Renderer initialised.Surface entered output.Layer surface configure, 2560 x 30. Waiting for Sway to tell us about theavailable interfaces and creating thecorresponding objects. Waiting for Sway to set up some of theobjects we made in the first roundtrip. Reading the cache file (a single fread()). Waiting for Sway to configure the surfacewe just created. Initial Cairo paint - mostly waiting formemory from the kernel. Waiting for Sway to display our window. Not waiting for anyone - this is all tofi. tofi-0.9.1/test/000077500000000000000000000000001441474151400134465ustar00rootroot00000000000000tofi-0.9.1/test/config.c000066400000000000000000000122211441474151400150550ustar00rootroot00000000000000#include #include #include #include #include #include "config.h" #include "tofi.h" #include "tap.h" void is_valid(const char *option, const char *value, const char *message) { struct tofi tofi; bool res = config_apply(&tofi, option, value); tap_is(res, true, message); } void isnt_valid(const char *option, const char *value, const char *message) { struct tofi tofi; bool res = config_apply(&tofi, option, value); tap_is(res, false, message); } int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); tap_version(14); /* Anchors */ is_valid("anchor", "top-left", "Anchor top-left"); is_valid("anchor", "top", "Anchor top"); is_valid("anchor", "top-right", "Anchor top-right"); is_valid("anchor", "right", "Anchor right"); is_valid("anchor", "bottom-right", "Anchor bottom-right"); is_valid("anchor", "bottom", "Anchor bottom"); is_valid("anchor", "bottom-left", "Anchor bottom-left"); is_valid("anchor", "left", "Anchor left"); is_valid("anchor", "center", "Anchor center"); isnt_valid("anchor", "left-bottom", "Invalid anchor"); /* Cursor styles */ is_valid("text-cursor-style", "bar", "Text cursor bar"); is_valid("text-cursor-style", "block", "Text cursor block"); is_valid("text-cursor-style", "underscore", "Text cursor underscore"); isnt_valid("text-cursor-style", "blocky", "Invalid text cursor style"); /* Bools */ is_valid("horizontal", "tRuE", "Boolean true"); is_valid("horizontal", "fAlSe", "Boolean false"); isnt_valid("horizontal", "truefalse", "Invalid boolean"); /* Password characters */ is_valid("hidden-character", "O", "Single Latin character"); is_valid("hidden-character", "Д", "Single Cyrillic character"); is_valid("hidden-character", "Ξ", "Single Greek character"); is_valid("hidden-character", "ọ", "Single character with decomposed diacritic"); is_valid("hidden-character", "漢", "Single CJK character"); isnt_valid("hidden-character", "ae", "Multiple characters"); /* Colours */ is_valid("text-color", "46B", "Three character color without hash"); is_valid("text-color", "#46B", "Three character color with hash"); is_valid("text-color", "46BA", "Four character color without hash"); is_valid("text-color", "#46BA", "Four character color with hash"); is_valid("text-color", "4466BB", "Six character color without hash"); is_valid("text-color", "#4466BB", "Six character color with hash"); is_valid("text-color", "4466BBAA", "Eight character color without hash"); is_valid("text-color", "#4466BBAA", "Eight character color with hash"); isnt_valid("text-color", "4466BBA", "Five character color without hash"); isnt_valid("text-color", "#4466BBA", "Five character color with hash"); isnt_valid("text-color", "9GB", "Three character color with invalid characters"); isnt_valid("text-color", "95GB", "Four character color with invalid characters"); isnt_valid("text-color", "95XGUB", "Six character color with invalid characters"); isnt_valid("text-color", "950-4GBY", "Eight character color with invalid characters"); isnt_valid("text-color", "-99", "Negative two character color"); isnt_valid("text-color", "-999", "Negative three character color"); isnt_valid("text-color", "-9999", "Negative four character color"); isnt_valid("text-color", "-99999", "Negative five character color"); isnt_valid("text-color", "-999999", "Negative six character color"); isnt_valid("text-color", "-9999999", "Negative seven character color"); isnt_valid("text-color", "-99999999", "Negative eight character color"); /* Signed values */ is_valid("result-spacing", "-2147483648", "INT32 Min"); is_valid("result-spacing", "2147483647", "INT32 Max"); isnt_valid("result-spacing", "-2147483649", "INT32 Min - 1"); isnt_valid("result-spacing", "2147483648", "INT32 Max + 1"); isnt_valid("result-spacing", "6A", "INT32 invalid character"); /* Unsigned values */ is_valid("corner-radius", "0", "UINT32 0"); is_valid("corner-radius", "4294967295", "UINT32 Max"); isnt_valid("corner-radius", "4294967296", "UINT32 Max + 1"); isnt_valid("corner-radius", "-1", "UINT32 -1"); isnt_valid("corner-radius", "6A", "UINT32 invalid character"); /* Unsigned percentages */ is_valid("width", "0", "UINT32 0 percent without sign"); is_valid("width", "0%", "UINT32 0 percent with sign"); is_valid("width", "4294967295", "UINT32 Max percent without sign"); is_valid("width", "4294967295%", "UINT32 Max percent with sign"); isnt_valid("width", "4294967296", "UINT32 Max + 1 percent without sign"); isnt_valid("width", "4294967296%", "UINT32 Max + 1 percent with sign"); isnt_valid("width", "-1", "UINT32 -1 percent without sign"); isnt_valid("width", "-1%", "UINT32 -1 percent with sign"); /* Directional values */ is_valid("prompt-background-padding", "0", "Single directional value"); is_valid("prompt-background-padding", "0,1", "Two directional values"); is_valid("prompt-background-padding", "0,1,-2", "Three directional values"); is_valid("prompt-background-padding", "0,1,-2,3", "Four directional values"); isnt_valid("prompt-background-padding", "0,1,-2,3,-4", "Five directional values"); isnt_valid("prompt-background-padding", "0,1,-2,3,-4,5", "Six directional values"); tap_plan(); return EXIT_SUCCESS; } tofi-0.9.1/test/meson.build000066400000000000000000000006261441474151400156140ustar00rootroot00000000000000tests = [ 'config', 'utf8' ] foreach test_file : tests t = executable( test_file, files(test_file + '.c', 'tap.c'), common_sources, wl_proto_src, wl_proto_headers, include_directories: ['../src'], dependencies: [librt, libm, freetype, harfbuzz, cairo, pangocairo, wayland_client, xkbcommon, glib, gio_unix], install: false ) test(test_file, t, protocol: 'tap') endforeach tofi-0.9.1/test/tap.c000066400000000000000000000015011441474151400143730ustar00rootroot00000000000000#include #include #include #include static size_t test = 0; static char *todo = NULL; void tap_version(size_t version) { printf("TAP version %zu\n", version); } void tap_plan() { printf("1..%zu\n", test); } void tap_ok(const char *message, ...) { va_list args; va_start(args, message); printf("ok %zu - ", ++test); vprintf(message, args); if (todo != NULL) { printf(" # TODO %s", todo); free(todo); todo = NULL; } printf("\n"); va_end(args); } void tap_not_ok(const char *message, ...) { va_list args; va_start(args, message); printf("not ok %zu - ", ++test); vprintf(message, args); if (todo != NULL) { printf(" # TODO %s", todo); free(todo); todo = NULL; } printf("\n"); va_end(args); } void tap_todo(const char *message) { todo = strdup(message); } tofi-0.9.1/test/tap.h000066400000000000000000000006441441474151400144070ustar00rootroot00000000000000#ifndef TAP_H #define TAP_H #include #define tap_is(a, b, message) ((a) == (b) ? tap_ok((message)) : tap_not_ok((message))) #define tap_isnt(a, b, message) ((a) != (b) ? tap_ok((message)) : tap_not_ok((message))) void tap_version(size_t version); void tap_plan(void); void tap_ok(const char *message, ...); void tap_not_ok(const char *message, ...); void tap_todo(const char *message); #endif /* TAP_H */ tofi-0.9.1/test/utf8.c000066400000000000000000000035201441474151400145000ustar00rootroot00000000000000#include #include #include #include #include #include "fuzzy_match.h" #include "tap.h" void is_simple_match(const char *pattern, const char *str, const char *message) { int32_t res = fuzzy_match_simple_words(pattern, str); tap_isnt(res, INT32_MIN, message); } void isnt_simple_match(const char *pattern, const char *str, const char *message) { int32_t res = fuzzy_match_simple_words(pattern, str); tap_is(res, INT32_MIN, message); } void is_fuzzy_match(const char *pattern, const char *str, const char *message) { int32_t res = fuzzy_match_words(pattern, str); tap_isnt(res, INT32_MIN, message); } void isnt_fuzzy_match(const char *pattern, const char *str, const char *message) { int32_t res = fuzzy_match_words(pattern, str); tap_is(res, INT32_MIN, message); } void is_match(const char *pattern, const char *str, const char *message) { is_simple_match(pattern, str, message); is_fuzzy_match(pattern, str, message); } void isnt_match(const char *pattern, const char *str, const char *message) { isnt_simple_match(pattern, str, message); isnt_fuzzy_match(pattern, str, message); } int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); tap_version(14); /* Case insensitivity. */ is_match("o", "O", "Single Latin character, different case"); is_match("д", "Д", "Single Cyrillic character, different case"); is_match("ξ", "Ξ", "Single Greek character, different case"); is_match("o", "ọ", "Single character with decomposed diacritic"); /* Combining diacritics. */ isnt_match("o", "ọ", "Single character with composed diacritic"); isnt_simple_match("ạ", "aọ", "Decomposed diacritics, character mismatch"); tap_todo("Needs composed character comparison"); isnt_fuzzy_match("ạ", "aọ", "Decomposed diacritics, character mismatch"); tap_plan(); return EXIT_SUCCESS; } tofi-0.9.1/themes/000077500000000000000000000000001441474151400137545ustar00rootroot00000000000000tofi-0.9.1/themes/dark-paper000066400000000000000000000004041441474151400157230ustar00rootroot00000000000000font = Fanwood Text font-size = 64 outline-width = 0 border-width = 0 padding-left = 4% padding-top = 2% padding-right = 0 padding-bottom = 0 background-color = #111 text-color = #f9fbff selection-color = #933 width = 100% height = 100% hide-cursor = true tofi-0.9.1/themes/dmenu000066400000000000000000000004351441474151400150110ustar00rootroot00000000000000anchor = top width = 100% height = 30 horizontal = true font-size = 14 prompt-text = " run: " font = monospace outline-width = 0 border-width = 0 background-color = #000000 min-input-width = 120 result-spacing = 15 padding-top = 0 padding-bottom = 0 padding-left = 0 padding-right = 0 tofi-0.9.1/themes/dos000066400000000000000000000004111441474151400144600ustar00rootroot00000000000000font = VT323 corner-radius = 60 outline-color = #D3D1B9 outline-width = 3 border-color = #E3E1C9 border-width = 60 background-color = #000000 text-color = #0A3 selection-color = #0F6 prompt-text = "C:\> " num-results = 9 hide-cursor = true width = 640 height = 480 tofi-0.9.1/themes/fullscreen000066400000000000000000000002611441474151400160400ustar00rootroot00000000000000width = 100% height = 100% border-width = 0 outline-width = 0 padding-left = 35% padding-top = 35% result-spacing = 25 num-results = 5 font = monospace background-color = #000A tofi-0.9.1/themes/soy-milk000066400000000000000000000016201441474151400154420ustar00rootroot00000000000000# Font font = Fredoka One font-size = 20 # Window Style horizontal = true anchor = top width = 100% height = 48 outline-width = 0 border-width = 0 min-input-width = 120 result-spacing = 30 padding-top = 8 padding-bottom = 0 padding-left = 20 padding-right = 0 # Text style prompt-text = "Can I have a" prompt-padding = 30 background-color = #fff0dc text-color = #4280a0 prompt-background = #eebab1 prompt-background-padding = 4, 10 prompt-background-corner-radius = 12 input-color = #e1666a input-background = #f4cf42 input-background-padding = 4, 10 input-background-corner-radius = 12 alternate-result-background = #b8daf3 alternate-result-background-padding = 4, 10 alternate-result-background-corner-radius = 12 selection-color = #f0d2af selection-background = #da5d64 selection-background-padding = 4, 10 selection-background-corner-radius = 12 selection-match-color = #fff clip-to-padding = false