pax_global_header00006660000000000000000000000064144541700120014510gustar00rootroot0000000000000052 comment=f9793b209461278fe7910e00d20115c7b58271ec fuzzel/000077500000000000000000000000001445417001200123735ustar00rootroot00000000000000fuzzel/.builds/000077500000000000000000000000001445417001200137335ustar00rootroot00000000000000fuzzel/.builds/alpine-x64.yml000066400000000000000000000020631445417001200163460ustar00rootroot00000000000000image: alpine/latest packages: - musl-dev - linux-headers - meson - ninja - gcc - scdoc - libxkbcommon-dev - cairo-dev - yaml-dev - librsvg-dev - wayland-dev - wayland-protocols - wlroots-dev - python3 - py3-pip sources: - https://git.sr.ht/~dnkl/fuzzel # triggers: # - action: email # condition: failure # to: tasks: - fcft: | cd fuzzel/subprojects git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | mkdir -p bld/debug meson --buildtype=debug fuzzel bld/debug ninja -C bld/debug -k0 - release: | mkdir -p bld/release meson --buildtype=release fuzzel bld/release ninja -C bld/release -k0 - no-cairo-no-icons: | mkdir -p bld/no-cairo-no-icons meson --buildtype=release -Denable-cairo=disabled -Dpng-backend=none -Dsvg-backend=none fuzzel bld/no-cairo-no-icons ninja -C bld/no-cairo-no-icons -k0 - codespell: | pip install codespell cd fuzzel ~/.local/bin/codespell README.md CHANGELOG.md *.c *.h doc/*.scd fuzzel/.builds/freebsd-x64.yml000066400000000000000000000016711445417001200165140ustar00rootroot00000000000000image: freebsd/latest packages: - meson - ninja - pkgconf - scdoc - libxkbcommon - cairo - libyaml - librsvg2 - wayland - wayland-protocols - wlroots sources: - https://git.sr.ht/~dnkl/fuzzel tasks: - fcft: | cd fuzzel/subprojects git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | mkdir -p bld/debug meson --buildtype=debug fuzzel bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release meson --buildtype=release fuzzel bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs - no-cairo-no-icons: | mkdir -p bld/no-cairo-no-icons meson --buildtype=release -Denable-cairo=disabled -Dpng-backend=none -Dsvg-backend=none fuzzel bld/no-cairo-no-icons ninja -C bld/no-cairo-no-icons -k0 meson test -C bld/no-cairo-no-icons --print-errorlogs fuzzel/.gitignore000066400000000000000000000000771445417001200143670ustar00rootroot00000000000000/bld/ /pkg/ /src/ /subprojects/* !/subprojects/*.wrap /.cache/ fuzzel/.gitmodules000066400000000000000000000002101445417001200145410ustar00rootroot00000000000000[submodule "external/wlr-protocols"] path = external/wlr-protocols url = https://github.com/swaywm/wlr-protocols.git branch = master fuzzel/.woodpecker.yml000066400000000000000000000052231445417001200153400ustar00rootroot00000000000000pipeline: codespell: when: branch: - master - releases/* image: alpine:latest commands: - apk add python3 - apk add py3-pip - pip install codespell - codespell README.md CHANGELOG.md *.c *.h doc/*.scd subprojects: when: branch: - master - releases/* image: alpine:latest commands: - apk add git - mkdir -p subprojects && cd subprojects - git clone https://codeberg.org/dnkl/tllist.git - git clone https://codeberg.org/dnkl/fcft.git - cd .. x64: when: branch: - master - releases/* group: build image: alpine:latest commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc - apk add libxkbcommon-dev cairo-dev yaml-dev librsvg-dev - apk add wayland-dev wayland-protocols wlroots-dev - apk add git # Debug - mkdir -p bld/debug-x64 - meson --buildtype=debug . bld/debug-x64 - ninja -C bld/debug-x64 -k0 - bld/debug-x64/fuzzel --version # Release (gcc) - mkdir -p bld/release-x64 - meson --buildtype=release . bld/release-x64 - ninja -C bld/release-x64 -k0 - bld/release-x64/fuzzel --version # Release (clang) - mkdir -p bld/release-x64-clang - CC=clang meson --buildtype=release . bld/release-x64-clang - ninja -C bld/release-x64-clang -k0 - bld/release-x64-clang/fuzzel --version # No cairo, no icons - mkdir -p bld/no-cairo-no-icons - meson --buildtype=release -Denable-cairo=disabled -Dpng-backend=none -Dsvg-backend=none . bld/no-cairo-no-icons - ninja -C bld/no-cairo-no-icons -k0 - bld/no-cairo-no-icons/fuzzel --version x86: when: branch: - master - releases/* group: build image: i386/alpine:latest commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc - apk add libxkbcommon-dev cairo-dev yaml-dev librsvg-dev - apk add wayland-dev wayland-protocols wlroots-dev - apk add git # Debug - mkdir -p bld/debug-x86 - meson --buildtype=debug . bld/debug-x86 - ninja -C bld/debug-x86 -k0 - bld/debug-x86/fuzzel --version # Release (gcc) - mkdir -p bld/release-x86 - meson --buildtype=release . bld/release-x86 - ninja -C bld/release-x86 -k0 - bld/release-x86/fuzzel --version # Release (clang) - mkdir -p bld/release-x86-clang - CC=clang meson --buildtype=release . bld/release-x86-clang - ninja -C bld/release-x86-clang -k0 - bld/release-x86-clang/fuzzel --version fuzzel/3rd-party/000077500000000000000000000000001445417001200142205ustar00rootroot00000000000000fuzzel/3rd-party/nanosvg/000077500000000000000000000000001445417001200156735ustar00rootroot00000000000000fuzzel/3rd-party/nanosvg/CMakeLists.txt000066400000000000000000000037631445417001200204440ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.10) project(NanoSVG C) # CMake needs *.c files to do something useful configure_file(src/nanosvg.h ${CMAKE_CURRENT_BINARY_DIR}/nanosvg.c) configure_file(src/nanosvgrast.h ${CMAKE_CURRENT_BINARY_DIR}/nanosvgrast.c) add_library(nanosvg ${CMAKE_CURRENT_BINARY_DIR}/nanosvg.c) find_library(MATH_LIBRARY m) # Business as usual if(MATH_LIBRARY) target_link_libraries(nanosvg PUBLIC ${MATH_LIBRARY}) endif() target_include_directories(nanosvg PUBLIC $) target_compile_definitions(nanosvg PRIVATE NANOSVG_IMPLEMENTATION) # Same for nanosvgrast add_library(nanosvgrast ${CMAKE_CURRENT_BINARY_DIR}/nanosvgrast.c) target_link_libraries(nanosvgrast PUBLIC nanosvg) target_include_directories(nanosvgrast PRIVATE src) target_compile_definitions(nanosvgrast PRIVATE NANOSVGRAST_IMPLEMENTATION) # Installation and export: include(CMakePackageConfigHelpers) write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" VERSION 1.0 COMPATIBILITY AnyNewerVersion ) install(TARGETS nanosvg nanosvgrast EXPORT ${PROJECT_NAME}Targets ) export(EXPORT ${PROJECT_NAME}Targets FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Targets.cmake" NAMESPACE ${PROJECT_NAME}:: ) set(ConfigPackageLocation lib/cmake/${PROJECT_NAME}) configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" INSTALL_DESTINATION ${ConfigPackageLocation} NO_CHECK_REQUIRED_COMPONENTS_MACRO ) install( FILES src/nanosvg.h src/nanosvgrast.h DESTINATION include/nanosvg ) install(EXPORT ${PROJECT_NAME}Targets FILE ${PROJECT_NAME}Targets.cmake NAMESPACE ${PROJECT_NAME}:: DESTINATION ${ConfigPackageLocation} ) install( FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" DESTINATION ${ConfigPackageLocation} )fuzzel/3rd-party/nanosvg/Config.cmake.in000066400000000000000000000002211445417001200205020ustar00rootroot00000000000000@PACKAGE_INIT@ if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/NanoSVGTargets.cmake) include("${CMAKE_CURRENT_LIST_DIR}/NanoSVGTargets.cmake") endif ()fuzzel/3rd-party/nanosvg/LICENSE.txt000066400000000000000000000015601445417001200175200ustar00rootroot00000000000000Copyright (c) 2013-14 Mikko Mononen memon@inside.org This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. fuzzel/3rd-party/nanosvg/README.md000066400000000000000000000100441445417001200171510ustar00rootroot00000000000000*This project is not actively maintained.* Nano SVG ========== ## Parser ![screenshot of some splines rendered with the sample program](/example/screenshot-1.png?raw=true) NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! The shapes in the SVG images are transformed by the viewBox and converted to specified units. That is, you should get the same looking data as your designed in your favorite app. NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. DPI (dots-per-inch) controls how the unit conversion is done. If you don't know or care about the units stuff, "px" and 96 should get you going. ## Rasterizer ![screenshot of tiger.svg rendered with NanoSVG rasterizer](/example/screenshot-2.png?raw=true) The parser library is accompanied with really simpler SVG rasterizer. Currently it only renders flat filled shapes. The intended usage for the rasterizer is to for example bake icons of different size into a texture. The rasterizer is not particular fast or accurate, but it's small and packed in one header file. ## Example Usage ``` C // Load struct NSVGimage* image; image = nsvgParseFromFile("test.svg", "px", 96); printf("size: %f x %f\n", image->width, image->height); // Use... for (shape = image->shapes; shape != NULL; shape = shape->next) { for (path = shape->paths; path != NULL; path = path->next) { for (i = 0; i < path->npts-1; i += 3) { float* p = &path->pts[i*2]; drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); } } } // Delete nsvgDelete(image); ``` ## Using NanoSVG in your project In order to use NanoSVG in your own project, just copy nanosvg.h to your project. In one C/C++ define `NANOSVG_IMPLEMENTATION` before including the library to expand the NanoSVG implementation in that file. NanoSVG depends on `stdio.h` ,`string.h` and `math.h`, they should be included where the implementation is expanded before including NanoSVG. ``` C #include #include #include #define NANOSVG_IMPLEMENTATION // Expands implementation #include "nanosvg.h" ``` By default, NanoSVG parses only the most common colors. In order to get support for full list of [SVG color keywords](http://www.w3.org/TR/SVG11/types.html#ColorKeywords), define `NANOSVG_ALL_COLOR_KEYWORDS` before expanding the implementation. ``` C #include #include #include #define NANOSVG_ALL_COLOR_KEYWORDS // Include full list of color keywords. #define NANOSVG_IMPLEMENTATION // Expands implementation #include "nanosvg.h" ``` Alternatively, you can install the library using CMake and import it into your project using the standard CMake `find_package` command. ```CMake add_executable(myexe main.c) find_package(NanoSVG REQUIRED) target_link_libraries(myexe NanoSVG::nanosvg NanoSVG::nanosvgrast) ``` ## Compiling Example Project In order to compile the demo project, your will need to install [GLFW](http://www.glfw.org/) to compile. NanoSVG demo project uses [premake4](http://industriousone.com/premake) to build platform specific projects, now is good time to install it if you don't have it already. To build the example, navigate into the root folder in your favorite terminal, then: - *OS X*: `premake4 xcode4` - *Windows*: `premake4 vs2010` - *Linux*: `premake4 gmake` See premake4 documentation for full list of supported build file types. The projects will be created in `build` folder. An example of building and running the example on OS X: ```bash $ premake4 gmake $ cd build/ $ make $ ./example ``` # License The library is licensed under [zlib license](LICENSE.txt) fuzzel/3rd-party/nanosvg/example/000077500000000000000000000000001445417001200173265ustar00rootroot00000000000000fuzzel/3rd-party/nanosvg/example/23.svg000066400000000000000000002747171445417001200203150ustar00rootroot00000000000000 fuzzel/3rd-party/nanosvg/example/drawing.svg000066400000000000000000000066171445417001200215140ustar00rootroot00000000000000 image/svg+xml fuzzel/3rd-party/nanosvg/example/example1.c000066400000000000000000000140631445417001200212120ustar00rootroot00000000000000// // Copyright (c) 2013 Mikko Mononen memon@inside.org // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. // #include #include #include #include #define NANOSVG_IMPLEMENTATION #include "nanosvg.h" NSVGimage* g_image = NULL; static unsigned char bgColor[4] = {205,202,200,255}; static unsigned char lineColor[4] = {0,160,192,255}; static float distPtSeg(float x, float y, float px, float py, float qx, float qy) { float pqx, pqy, dx, dy, d, t; pqx = qx-px; pqy = qy-py; dx = x-px; dy = y-py; d = pqx*pqx + pqy*pqy; t = pqx*dx + pqy*dy; if (d > 0) t /= d; if (t < 0) t = 0; else if (t > 1) t = 1; dx = px + t*pqx - x; dy = py + t*pqy - y; return dx*dx + dy*dy; } static void cubicBez(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tol, int level) { float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; float d; if (level > 12) return; x12 = (x1+x2)*0.5f; y12 = (y1+y2)*0.5f; x23 = (x2+x3)*0.5f; y23 = (y2+y3)*0.5f; x34 = (x3+x4)*0.5f; y34 = (y3+y4)*0.5f; x123 = (x12+x23)*0.5f; y123 = (y12+y23)*0.5f; x234 = (x23+x34)*0.5f; y234 = (y23+y34)*0.5f; x1234 = (x123+x234)*0.5f; y1234 = (y123+y234)*0.5f; d = distPtSeg(x1234, y1234, x1,y1, x4,y4); if (d > tol*tol) { cubicBez(x1,y1, x12,y12, x123,y123, x1234,y1234, tol, level+1); cubicBez(x1234,y1234, x234,y234, x34,y34, x4,y4, tol, level+1); } else { glVertex2f(x4, y4); } } void drawPath(float* pts, int npts, char closed, float tol) { int i; glBegin(GL_LINE_STRIP); glColor4ubv(lineColor); glVertex2f(pts[0], pts[1]); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; cubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7], tol, 0); } if (closed) { glVertex2f(pts[0], pts[1]); } glEnd(); } void drawControlPts(float* pts, int npts) { int i; // Control lines glColor4ubv(lineColor); glBegin(GL_LINES); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; glVertex2f(p[0],p[1]); glVertex2f(p[2],p[3]); glVertex2f(p[4],p[5]); glVertex2f(p[6],p[7]); } glEnd(); // Points glPointSize(6.0f); glColor4ubv(lineColor); glBegin(GL_POINTS); glVertex2f(pts[0],pts[1]); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; glVertex2f(p[6],p[7]); } glEnd(); // Points glPointSize(3.0f); glBegin(GL_POINTS); glColor4ubv(bgColor); glVertex2f(pts[0],pts[1]); for (i = 0; i < npts-1; i += 3) { float* p = &pts[i*2]; glColor4ubv(lineColor); glVertex2f(p[2],p[3]); glVertex2f(p[4],p[5]); glColor4ubv(bgColor); glVertex2f(p[6],p[7]); } glEnd(); } void drawframe(GLFWwindow* window) { int width = 0, height = 0; float view[4], cx, cy, hw, hh, aspect, px; NSVGshape* shape; NSVGpath* path; glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); glfwGetFramebufferSize(window, &width, &height); glViewport(0, 0, width, height); glClearColor(220.0f/255.0f, 220.0f/255.0f, 220.0f/255.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_TEXTURE_2D); glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Fit view to bounds cx = g_image->width*0.5f; cy = g_image->height*0.5f; hw = g_image->width*0.5f; hh = g_image->height*0.5f; if (width/hw < height/hh) { aspect = (float)height / (float)width; view[0] = cx - hw * 1.2f; view[2] = cx + hw * 1.2f; view[1] = cy - hw * 1.2f * aspect; view[3] = cy + hw * 1.2f * aspect; } else { aspect = (float)width / (float)height; view[0] = cx - hh * 1.2f * aspect; view[2] = cx + hh * 1.2f * aspect; view[1] = cy - hh * 1.2f; view[3] = cy + hh * 1.2f; } // Size of one pixel. px = (view[2] - view[1]) / (float)width; glOrtho(view[0], view[2], view[3], view[1], -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glDisable(GL_DEPTH_TEST); glColor4ub(255,255,255,255); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); // Draw bounds glColor4ub(0,0,0,64); glBegin(GL_LINE_LOOP); glVertex2f(0, 0); glVertex2f(g_image->width, 0); glVertex2f(g_image->width, g_image->height); glVertex2f(0, g_image->height); glEnd(); for (shape = g_image->shapes; shape != NULL; shape = shape->next) { for (path = shape->paths; path != NULL; path = path->next) { drawPath(path->pts, path->npts, path->closed, px * 1.5f); drawControlPts(path->pts, path->npts); } } glfwSwapBuffers(window); } void resizecb(GLFWwindow* window, int width, int height) { // Update and render NSVG_NOTUSED(width); NSVG_NOTUSED(height); drawframe(window); } int main() { GLFWwindow* window; const GLFWvidmode* mode; if (!glfwInit()) return -1; mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); window = glfwCreateWindow(mode->width - 40, mode->height - 80, "Nano SVG", NULL, NULL); if (!window) { printf("Could not open window\n"); glfwTerminate(); return -1; } glfwSetFramebufferSizeCallback(window, resizecb); glfwMakeContextCurrent(window); glEnable(GL_POINT_SMOOTH); glEnable(GL_LINE_SMOOTH); g_image = nsvgParseFromFile("../example/nano.svg", "px", 96.0f); if (g_image == NULL) { printf("Could not open SVG image.\n"); glfwTerminate(); return -1; } while (!glfwWindowShouldClose(window)) { drawframe(window); glfwPollEvents(); } nsvgDelete(g_image); glfwTerminate(); return 0; } fuzzel/3rd-party/nanosvg/example/example2.c000066400000000000000000000037121445417001200212120ustar00rootroot00000000000000// // Copyright (c) 2013 Mikko Mononen memon@inside.org // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. // #include #include #include #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" #define NANOSVG_IMPLEMENTATION #include "nanosvg.h" #define NANOSVGRAST_IMPLEMENTATION #include "nanosvgrast.h" int main() { NSVGimage *image = NULL; NSVGrasterizer *rast = NULL; unsigned char* img = NULL; int w, h; const char* filename = "../example/23.svg"; printf("parsing %s\n", filename); image = nsvgParseFromFile(filename, "px", 96.0f); if (image == NULL) { printf("Could not open SVG image.\n"); goto error; } w = (int)image->width; h = (int)image->height; rast = nsvgCreateRasterizer(); if (rast == NULL) { printf("Could not init rasterizer.\n"); goto error; } img = malloc(w*h*4); if (img == NULL) { printf("Could not alloc image buffer.\n"); goto error; } printf("rasterizing image %d x %d\n", w, h); nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); printf("writing svg.png\n"); stbi_write_png("svg.png", w, h, 4, img, w*4); error: nsvgDeleteRasterizer(rast); nsvgDelete(image); return 0; } fuzzel/3rd-party/nanosvg/example/nano.svg000066400000000000000000000055421445417001200210100ustar00rootroot00000000000000 fuzzel/3rd-party/nanosvg/example/screenshot-1.png000066400000000000000000001666451445417001200223710ustar00rootroot00000000000000PNG  IHDRTziCCPICC ProfileX YuX3sCw HtwwJph݊J` %"**  "*&!H|Wソ/yf=k{ڳg:p4Ãa:BB6FN.i@`@cmmcm@;$vt7mAq3Hw89 >بb&2j kw_m{GqlP *GTꡡvEwO'$$lX_zI$uHD &y F؅^"l; 8o-Pkb[~6^6ר0/J/Hoόq8}1'E!CHrDZ/҃!HmןQHd+;f}A_/EFFؠBAZQ:~kw<~ۅBh~F}ap <U  ‡H ʈb ~IA"iD:еA>"?08 ##FcD`R092yL &fRcXU 돍f`gױ<]p,8aڝqqi\7[xq OG3|!~@I! .PBPO&<$%P𡈧ȣ蠸O1OHO&j%F 9qr7 ^@%))g)P1PQQQESRzBBMM-DMBEK}zwFITr4i)hiuh&^ONNDBWNNn^ފ>>6;<C:C #Ϩxqq $d´)=Effyf8r.Eń%%*$:+7/k&k#CollllYlMllA[٧90b{9b9p r|dT JZm]=q.]wսkG'$O{^f^`ޛ|\|||U|~m;mwi~"2I~E $O) 9 jz'&l" |QHHȸ(NTY4H,  V.v_W?-`vʞ={KPIHH\d4Ll,% "u\jHjKZA:XV LL̲l\\ܒ)F # JdF%%OSJsU****?TUT~QP RW{.^>[Q1ɫY9ŧEҪzͯ}V~ϺҺdTz}#,{ e/ w^4\4R0J457~lmmrdT4Y+s1sylajq⹥ee2:a5m-la}c/noldlllm=lmtًG;:9\pX8$tǙ9йreuM-m]=>} o4 k_ ( XxoAVA炶B!! A7vŅ=P(X$FBmQLGhH͘BF3&&%b&&$W@)^)?H<tntZAC:ҹ6:|1&ڑGeefneddKgeox9Vrl;/^bޙ|\~hq  NXh9{2BEERҍrS\2O};s3땁SUFU-BE57Cuur>y.y7/(]PUw}qaFƪ&rWϚ.z˦řYWy}xzc> zjtǙK9U1n5ڵWg{UOSowѤArLe+lWn]zOKmkF[Pu Nί]c=}} <6xiH}mx;ţw=ߗ|{壞񊉤IRS'%O1?},dK"bߒrʞ_V6~+m=_7e ' [))^jo> ҌL5oYe8Y{x50o p]_8Od],HdL|¢򐪔Z֠nޔaєiY%LFv_ŝ =]]j[#}|q~4t BhCapCWc\cUX.''%[Ȥ2xv/ܡ^GDѽq6'Tvb1\ܟy=ǫ :\h\PWB,Y.*,8v^@%rjzlٹ\H`}IQi+vUc7J;껋{[ hT%:$:,q[~D]{d=|D3Lx<8uIϢO+`~r|l9R.w![}LY^\6^|uvg~ם:nll??tvF葫W,ێ#0E>чҐJQɂr!;ġUݷ/ 6@OTKUDyvg6(9DT T[kXhkiiR~{aPjgdo,mB46m2K3wY>7V׎ca)EΕ[{ɾ0CO.EҠWw/w¯?*@/1pnsPFShFpXvq&U06+N;n-!Țx?hnfʍ*vIGPX9rh@`\VMOhy6wk$5EŢKKDޖ_8}:M|%sJXu}Z:7Py%F&\WW֮6\wpKXk\[n{mG݄^>؁7/0~t$}-ߓ76QIǹSkO}}y4U7'5h'Np `Ao DSO&EphIXl[ȣ9pA38psiaьQ ݠH()h`#>W]sxF4Or,b1hyaeLl vnj bqi<o!| Մ  Ojy(1FItlB\iɣJJ{N>~!5?'X4c)`gma3gcO .dz;W;?ZSPSh0$Dr~ϕ0bz9u(dƛrhUO:A*[lUYszRz.<}}BÛ&+[o uI.?0`d*,|B%k׍~>ؤߒf@hh$p4~0 >564Fhu *.Ca4p| ~fRT9 Z `1X^6{ pqE(_N$N^RPR\A#g()s)ߡq5DM=L#ESBנoec(ʤ4l<Jdf`{;6w.]?xy3\vKS&T*&""f"GLb$AHoRqK*)(E)4QASۭ`nhem!"2 >1!0Urr*ہig,M̭̂ʷ<}0X}Yө3lUjn՜˾wRu+צocPt>lz;d70MpA#Έh!!).`*1OLؽ|#+WWA*ABa"b(Kx BBQWI|)يK55u\zž _E}7_p`ppɈ̝ލE=x:5ۓoM?xi0 3}7?B}_|Z>-Xxu?8~{o,hTڒݪfى@Th6ooެAO-!γ&@IDATx GuK]-YlK 6mc'.!N!N@OdeY]=[Z=33;=}^zU7uؽ{wprGpGp+-õ^oGpGpG pGpG`X#n~#8#8~8#8# k wGpGpGpGpaú#8#8Ae0,K;#8#09~DnxA[ᢎ#8#0,nuSng+8#8C@3 oS%!,GpG ԉF7 nU7W8#8M@U F3 \k*8#8!PcDY0k6}ǣGpG%2rT>q JJ[-={)#8#8U@"#D~)[>oq. 1X=Q#8#ب&)G&Q㸮q r rd4]8#8#P'6NS+']2eYOønqp`SzwGpG3 Rqql|Q<q Rj84+>7~ i8pGpj#Пa_V,^T@CmAiQt:$?N0n,SO\ͨ&#%F@bi~p1~'-CxrcҦyl,ˑ8#8@HY2_1O~ K_:$U7KT.ˀLc~?y/W ؟S|1^~GpGp ,2K)[̋qrU'ibzΒ鋫Q\UưT!OOn1|(K.OSt`92}qGpG A!a_|/+xr˟v'¸"ɅxPYa<˥i:,b|E ?ʉWwGpGi2ļد~"ƅ$'7%xUsjqᴡYn8K]N~Y8.+>OS:#8#8E`Wa1>qrUn^q)K4p"W-*qؠLY<8\/.OawGpG5᩼Ҽ8W8ƺ/=xŗNdar۵W0˟6ZqiN)b|rcno/]ZÎ#8#B m@"+;$O8JFi%_q&G1?$=*OZP0tXqU8-xL,&O{1#8#T bd̗$SӮ q",y+tee ׌=et *S0qqYn,WV-_n9cR: 8m_L{{>MW^y՟g1`/ΊyBq:/^:LÓpPą;P*q/4s|Ǐ?8}`RDU< Fٗ|!K BX2e.饇xʁ<%Tq:$ttMV'7Kx'WiT.61zH_@G8O)x;$Zx 8>"q~b^ˆ|y&\ ʑ/yQ^U&%CE/┏*RzҁtgT7ށ)M|!OXHGat]>u{;`p!c:2̘1-|/Q7fﰃ$9sz_Tgʂ ]*:KWC/nU'*eK1!o_}~Ier)/ kҨJOe~+.$}JCb"]\Wb_tbldtF:K=~s e+9ÌY+[BXze8lկ8=,Y|Pb@1}W]wSI}ge%2IY%݄ko,%%[$iI/|N:0T/A 'Oс^xSI.GrqXuIIKN#,\._$?P<:,ī-YA~\H@o8#χi2G7C;zǿtR8woYoxxӱտ }Ϝokpwi{P8m o=y~_y&g ˟f|ACyE/K22&ڧS~\H:Nwr4B\JxE \ iɍu(7lhׅspf#ǖ뮼 ĉ3^hs]o&ӷ>d&MNzzѦ wTiS #~~0FyمO$_r5b_y_i]12KZ\giȊxJF~($ˆJD|W.yÅQ+^yIqc>6?Xxtۋ 1 vA^r٘/tR}ҋL9X׀vSЕd/}_r,dxnzDn|$xnM8~ [w{>imϛ^r)pi''qD=?\BE(N\.+[ySQtĤ8$ O#+UXqz)K'/SH1\L!YI <+}nxK~%YJ.^/|F?,?ڞo}-|EZ[&c  i Bw*oǦ''_|կ]+Ím|5ar箰Cu?B?^wG3y[hߴ6zϳ<?z=[EÄ)eZ2`Y0~.S:EP'(3*6QAV?JNqC# 8Nnpu\uc-0c6:zas;vtcF=w$q {ƩNܬEg%=L8FE=pfo9?Ey! 0!YT.0!dC.U.54*.$%#~G/īmVҧ|bW>*8⹤SyH'qa$?MI/qW#r"ۿ X#7lҷoSCnИWƏóۑ a>17𵿱 o ײaƉY8tr6>~CƿOX7~$5GQa_w0y{'O Mi{n'iCSzMدpφ^{@Ү wZY渹vԳᆟ=FNy}/Thm[oi3]8P꘾u=IJӅ<~ݗ {|q"aG/`p K+AnA)L|4W02\]- X.N'YSbq)"cWX^<\p GrS腮D[XN֜ݽmn<Ϙ^xviΤ^WSԹPFѹ~Ӗ0geOeƧz࡛<1Sqƒx+YPaTWU91ů!WW:/FD W\_/U(Pڟ8H8K'iWu^˫> |\й%,:i1\zϥ9$v3'iΝ2\pA[#\t^6ߞagMq t pZ?Xvn;?|ЬQ{ZA y_ud`)cZW~~CXw=] - Kƴ΍[v.:вnk/t_r_ G?x=wxE) .w݄I4e쵔L76{ ))F2onHPߧ _IrdU!dcd `x/}#z e0li cƍ Ə ǍI F.CY`ؠK:a>* QXT֑mt\P\VM"zp#-,2␁'.{'YC.YtB#z 萌Ґ_|ċ?ǼdbG~%J˵sξߟGW?/;JYַģ/q<~2/ޠDž*X̋q 04 2[L((NIΙvut1i=ʌaT[.,f7E6sfOOn^QdqG\}IަB: tDe p\F>7FcďpRCV|`p&8UyЇ,qJ4ēCA L<Y+??@8A<<(>\t Ox /3S̓Ne2*?q= |!C:sQn nk[ٵ5|H__~s8jf.+n,wN0;u\{a ^#wNhӫaö CWk 5F:FO=ttv4][ֆMc {ÜY?.zE'+yK&ۓzlZtonUpcxY_&Zyu  _w=U;۷C3ND0  )[ߡ3.0D(eD3#)#zބ~2bYу”x.HR~ /?q| Y"=J2R~uQ᮲K~Y>y+ :U#8U:ˌ^J>!䄁Up!tOqztz )N`jKFgG_3f4\w鵡.}f/~~ឤiF#b~43iQpҰ%aFowvm`JhcV=995o}8|Cw=-e`s{~;9<=amr5z~8~aK? p=#Y6(>g?Wn;n߄IDwXBA^~ƺVF_kb ǖŋF&Kd 7CA>i~xP#,>$۽S:x/%P4_d"C.ōCЃ Ə OXa݀LlD$',% ->hAR&l@z!SIHtG>p < x(/&.GF<;l=G'c}Sun)Xx~.8_<⥋KZ\d' /=*ʢJ?:$2 'UVxI'NW\gĩܸ\tCۿxc]M dXb{{L߾*<]_NxwbB}da'lwtuuGbC [O.vch9a=_ G 6o W~yr+0~fFRmN7[~/ o :F/3xϿ=&pd\d1߂}a$0i ܂D? =3_E#))@Fů@i$̔ :4 H~nѣGc<<{ir3]>(!=BF'ny._U"6 o[,O`]ÓLF,~]q!]w` `D`a ^~Ǐ&q%32iӦ]2`x` "!16N(f˖->ҩ" bt01AaToJO+rå078QQ,$4<%8I^JiTN(\nڟ|Iʭ+?DZ."/wdA tT.1b9 ɋ[B? $Yc7T:ҞɤEE+8H~/d_n̏y~1 a`E ?)-id`|a!.ŏqEZC?|=㲐 9\C\OAx%oS6䠸l#^BHyB~\AdN*Sl0|2T\OF0e"2`/ O e7!:Dď\H+,\dT*.-1b~!.f_K ? ,GO*yy#)6:+㈰~1+]\/lҊ0F*".G: +W|p@7%C}%/#-e LZ"H/RYQdҠ[:Ce$ UntS |Ջ4;K8 'tOp?h1sų{&~>G{w }ErW?d!&S pϢ}KrċB6!è/^[,i^Js2.b^ˏ.Q7UQq2Yu!ǡA(HM 9(]C䩴2U=HE/eQp'@*$O\"'Ĕ.r^U#EȨn/y\ʡ:%W8N!+"?U}$ʤ s)9_!'RZOĩ{p'N'/Ɲ(=GX^{{?{{z/ ?Wz_3#N<翀 2"K5p 9O:7V&RP ر'N.~]a gIϡ˖-:9#8#TŋoI #7`bĮ"!~|WXo?t0A"nX%zesrGpG ؞_l.+q)]2Pڍy@MK_n(B~ԔŲi9j؏N'GpGpmmt |V^:vaBKRR8.O|荍^noUPwGpGpA mW6W]A?adc\;6.>Jdc rq\GNŗ8syzpGpG<x4a\l"W*.Wq b?:0~~*>K75#8#8@Ⱦm+pE??gK7(Zq\@Y4/ׅbq)׍~GpGp(ʒS.AL'Uz+,Z1+r%W:uGpG Jl.ɒCW8Ιɧ唦_e44̪HI)^,7ҽ#8#8 e[_2[AUJ > 8tƲI6xeGpGp S. +¸%37KgQ=E"+DV<<]Rp,aI/?#8#8HTqX~4ˏ+*˟ƺīA.L—~%8qpGpG|b*Ic/\JoS4Z*H)9/N''^#8#8!e_vVk?XGO˕K+++~r/bV@ɂT["G9#8#@R,=Q b 6$Q~YnՀrE#8# sl4;M/T TYSjP|\#8#8͊PSYbMPK"b?8#8#sRfibhÕl+OypGpG:TbN ԒfH<#8#8]JS.F0 `a#8#A\U;#8#4:n7z zGpGpă;#8#4:n7z zGpGpă;#8#4:n7z zGpGpă;#8#4:n7z zGpGpă;#8#4:n7z zGpGpă;#8#4:^/i_~fK}D '7W::#8 ]0)7z0F#M<Ytkmvdž>Ć#P 3F W/JLNP,w^2kJ|ҍ/lOx]4%Lka=k L)=giGʭm{#0XnJܿ#t>^;w٩S>$g eF++6Y?+|vƽkz])v/?dE ?5E?_t^0zHFnUǮpN@شk=𶮰y݄N@XsWFA*"lJmʛv# bW(jpxEV9+#n~>cv&1-#7 ǎk w ai>1ujaWe y5y֔ Fwҗҧ759mLr-^Ɛ5EGf(0O-?Onڏ8I9bٲe2w6I0uNS ^9Gp"OGoX>kTK߀SY6gLΜ:&4 ,^"a0&u G2+,y-va*.x5V" J] >Bmf`0L:g|4vOӞU^VGpz W@wj;N;wW2 eoK0L3Gx@`R"5ן\'/4FGg4AN=8#8;p:hfOH6o"O o/N fόΛ7vQf3‹'jZx1Gp@gMkso鋱AY :2~}bmć엨9oکpFɄ}6wrGp!@M_M8A8m<`/_LwrGp!@M_Q8AbJh;nߣ7r^ /#8#0Lm 9zmos8Ǎ1|km浍ۻ{V^qh#8EW6N02lÏw8w9n7wf_dtߵGpG \;2K뻿-B^"p8/#\nѼh#8Gu%#s{n\F;76ɣêM{GpG >-n[wfKwxp\@k~%{q Wmo;9#8@ދg& .XgDS;!o$%㤛quM΋sGp-4/|Iq ;7>It(pG=pa FT h$/#8#8@pvغfGpGp@ h$/#8#8@pvغfGpGp@ h$/#8#8@pvغfGpGp@ h$/#8#8@pvغfGpGp@ h$/#8#8@pvغfGpGp@ h$/#8#8@pvغfGpGp@ h$/#8#8@pvغfGpGp@Etnnߴ30eLIa#n^ G>ut/?Շ5: #@ G`ЁaOlm_؞ttL_LWZÎ#x|k;-g2v}}1q#P:$Fz8k>rtX+-~FC_bs6"\dt;v\Kppʔщl`s[w6|ّ|49ƽ@#++T[K;Vu,Z1q3@A\.R.7l-rَ]{V'X`~$(@IDAT>\ޕt`06c#»v=!l :I#[˜#ؖa]ҙS,EqљFz8G!΅wu]6-'v;b0nwX{g};Z2[:$zgw w/ ǎoI>Wj#571h n'Y됞۹+cW9~lɸpQɨ~[ÏlWXZF{0sg&͖])ӃQ=%05dfeOFM_~zsҹ2 cܻ h3-OĵNs) j2/lj᢬!хYH]Y?,0ʙAZځrRG(kKf{PpIk F\Lyztcጉ#… pUWbph#e;Y{XsO ~TW.Gwߖȗ{[׮}tdR+0Ny pk"{n4:l$=mx]x3ib&j[yg1 F {_e_ًŠw6~x[0^jA-10Yŋ_dՙGt\N;@ zyf1raFE+6IĈJηjSQQㅹvo5">bxc4's\?ז;?nr }f&`5j'_=d;?Ě>]򡬍@۞beY:opщ<E.Xtf_vMNPʴvpr7~ tȐ [m6lF##pc4}cM3^5}l㏎ Ѫ|lCy|vGQ,ócHv*r 8xcN-4F~n5  JqG_ũ?2ջMUgolwo/L#\ȣ9_l]Rڒ3|`wo wN~\LI*9yF <*+РmquhJxE1 3e>/7trkK?3瘅y+|̿-鯎!lVnp_nӶ7|aF8_{ \CK~j?F޻߄!%>PCҭvtoS{-lvfnL`Mm6?d112W6""6|J6^ቀóݛ֚NdG" k}l=e!׊q>:N1!=mwVm %4/DԼڼryOo0zXxn3H7!fb?-[Pg) 4?~R7}!6q?ŨlT)eۙo\~D7^D l4/r6}ړc& /X'&MjNxf3DQs3Ѷc_ 4˖7aR<3x]-/lF^=Yhg>eEv`tB6ghSDgIfrc\O~T8ީΦl[L5 gv-ud}.\U>ɶF)cjDeD,+ۗ艣[ Nϡ=wG[Gz/ʹЉب ړy)`˱vОU|a7Ee5VSF_|`뱷͙ KS7{?LZhQN@3 ߋe!g;Fvthfuc8J}&}`y)iT4mTE?$]NyH.6 :㴲s[l"U*${QNgqq9kBLa>8s`Bw_lRԓVb1g0'Nx=ӌknƌ lݹtkkBlϜ[iTvr!C0l.3}M{-,OkA7Zdz ߛaIdǑcAyF29sK8g0{_d|p*~wfux3A//K:A Z=l[%O!1~T` t4UUc wN/y RA|tmvk|<6B!vXn,ff?b'ͱy-oJPOb-! 9'#@b!3saѶ.;m={nm7q j=#0A}h񅓪Ű֕w،b_bdǎJzdmv E~Ύ 7l8J u\PǢ>6u6*z e``*?<آ<;qTgR u8檭/;ӝSmK̀ kLulJ[1ߙqb?bd֮fzATU]l3sIhdk lݱ6~^cӭpǚV_fCΨO <ኀõ因{XN5A`ͱԁS-~dl6m ۢ +N+u5Q-)N7hl搗LpP?И56ym=ʉNE6Hh1[} b~k#Y;7k2 /3fG6͕[lMT{ئw|=>N*ujOl__;oLoyt3 >U~i3`hOяcReHivO>kbb#>+t*D o  l3[ldF|>`9migؚjJØBUxz9W~LO$]0-e(u\W,\3w2_i7cF+ne\ p-zVGl,FNS蜆EI=]kg7~gM4*q0?@L@y Cs@?fcxqHCԣ -7'iztw4"%tjsm]W-X~݌ K@rPQv+LGmݐ5>:XηE1έ|܆ -axvz;ʾjQ]EkpDҨta햑#1&]N/ Zo/ME[Y8gPdyd ݟ_)m۵?= Uw {x!#6@NFj\FcۖqU׎. t#35cvӑaN+`' F8|,ujwEvR6JE|\W qeSՄ? 柭bjtb6_cχm׀/v0{Tj uvh1篽`9Sn|Lkzئpfog`f",XN|m{ďA tr@jYLދ.hQ-0溿>i9Й[zs;Negž,=dkGׇz|CXg;lʟfXkylTU1}f99V~3v5Lh?^0Y.F7w>7}3x2X@D<;&ӿQa4>B4`. w!|IkGhsn򬀧ٓf8r[j g`[ +?Qt{E,-vKe䁘ϜL]?MňSOiKn nNԡ638{74,`eiVwN^1aNԾ鼓DUGâfަA۰E; i(6=E飃bK8@p$JKVwGQ7cz#;aC{v{r@=Dg*霾kb;%ٓ.>}Дd4I3&ˎ:e'M&Pg!]6wMNۚv*QN.ۯjG~7ɟ0%=8{f`j3cvXL^F/_">W=6ʌUmn>:0bN<*1:';í;\jra .[-MYp |Ἇ w7c4ܟ^L-k+9-Os$s70Uɪ6\ŧ/Z<>"sD#xV-Nm{ d{mҏ^X.91öEt +u1 rk)6TWZ>ۯa1#jmHX1= /͉A|<~ 綆NY!ϼovy]dp,msܫž,]gߍ5s9="+6ٽWٔ-@@4@ EѼD3/2^11"tr:̡,Fpqa\q;Fl|OQɷv-cQβ%7YE),⡞490Yu5~h^fhrž _UJ*N_/(li{JZKY~^b?zفBL!YgSoGOx;\bpҐ鼿n{G0?w߳vkcxRx[Ge7k o|DsK131P1cL OM7QܩWM7ۯ1'7'{5լwtIYRIYyF\_㣗ۉ|?ydmB1<)m/0Ao?,AWv$l3oז./ow?>t@tz7n׻~\9`ŷ|,fcFGy(7R_kl.t^+zLY_ ܸΨ7Ir]`'yaScvXƧNi頻eh䇠񥧶$?y}C=R6 >B,-$`M:>H81B1U3Th-95Mϟf(c &F6-|4[mf2æi۔G+axo:&qbjR|P?-"Xn0_&37Ӿ.\`0?Jδ]J~6qv~gF/?Pﴝ<|q>2Hme-7ɥ:a(ǰn0 ( \`Tm~ cGF  αg!=NQoقDV}[PRN^ڙ0aOվ{Q C~f8zlqmܽ}wXo-)֋YiΛnb"ݲMN$âvHks*Go15B߄8p3@L!`:5*Xk-鶽&Œ5SXFAl"1 9䡌12y~FiZL`A MɦJ`כjď4FV;mc} 8\ fTtptx\r/i(ۯqcvR`^fѿ_N|kx8PMbݞ=xf؎3B:V-%!]wryzYv.|7鳨FR[w$G.׻\͟}_KQIv +=h,J׷ۂ͙VclTϼ{v$ku@Le徹vW2y/G:R'~ԕP--.TC\M4]נ`4j>y4OB'R-hĔ}FYhQ:V$аIfܾ0BNygl O1̮ ijl286bVŜZ.=XT]XxA$ΡjnWE1h45yyn~7tF(pL2 QaU\.Jf]Ncn+u@œkly3ˎ Pohd?"Vԕכئ082"^rpRXcYDSuqjk2b1Q̨3XRuy~#m.B!XoMߘaTkq?F$[}8/?зۖ^xXdDRϖ[Jꏀ?o~KQR&hPljpm@g}1j^vu|Rm~yG,,;(rn{8n;v"Б#0!}1y@ qdԃO[e>ޕuV.7yυmO3k-_+omf99ƙh?S9ese?{'h^GnW HW3x9$ڮFa兘|ljWWWOnIm뉚}J'oۯ'0J\i+lBbۨuA%ǶTnN3׋0|Kmjѯ=8 X0H=}`MgeǮdfu'GhJ nfmJ 1/q-:<[CA|3|wp햂ᾮ;s M8VM\>ǸkKaY>2nF#q=Qp\OKvݸySGU {FdAqUuL0{zz85om7Fu#qg8-6~0g3 |B͝dtx<ԍ^by)3{ÞC-]JFe!ofa3Iq?l[wS@){縎99@!qiC׈v-mD^;vF?i7[p&JGF%#L5fӼ'o7 ώeU?M3F&SpMSCǵIm#}hӢg9XPG89nbQO U=u+nזi.?jV[>[ydU0.g1;eL3-Q ǎ+Ir.$q rpH\/$򩃦1~G &իT}NX2}#ghxfs6 Ma͕Ne{v`c+X\~0̏+GZm_ 0^O:j&8ƙ]'01ƙQOځc&oQϵtGUL0㙛iuA\b$͵0ه*k|u+wӅ=gyJ 8Pץ^-6#T&mqj<cc^h'U~z{hSdRMԭw=.forkKb#477w6dY=iڸc֜*٩⡓ wǚ00K|(1#y #결[vk n rBLҞv#  Udj EN2C; `;.-s&$ p! —ف%=>Mg_cF$\jM:™‚+]mbE-WjcG:TWuZ>VmjoM-zy3u'!s aNLbd2** ϧ*Ǡ( @fQIB ! t'yo{wW93Us~~jׯ9^W,fN]nWn6~z`vfiOx/%È  ք})d윊MmcrxR"K׆7l_E7g3sma¦#&ENaֻL]a@}~u> CId B;f؅q\ [=ؐ䃱amQ'kj8 du7nߗc-7{YYa}Ntey*qS"}-?;cJb#sF=f?>bx`2\U6g/>aOaݙ~mjb!Wyݘ4ձYJ7YDF ď뱇+L?ʾy8>uV֋hc/]LFeyo\ <`N+'D /K 2{bxE:76xg__.ސA}&/$8<ڇ oC/dBx3Bƫ >,QN;0ԩG̸S'D>M^+ ȣRG )6{nS%ǃQc_y|X7+ -givat`:_cҁ^#=06F[~;1B˭!W,,|/hX/NF`KձveWċv6X6k҃mRR'}uF܆(A M+?ck>rEɩV7\!tA^yOcCjE_+:_;3HܯC1W,^>.`"4{ƌlIfcSso'g!Ƽ"|m٢#u"7X;ouBT_+T qwo Ӂ?<@ !hr~63 ^ Q wG~joTFn{-rvJ!T}+H+Oc=貊B^e:>]ДC/k,|qi&w(~Xl-NFr1Ѡ`$͉:iU'@Gw+~ܱgc6t'7L*a/xpV…a2D|oc>oAqV'kIl{u/!9Rح>nAA/^ ur2̎zm%K=I=OuԮҝn3[+Nafwrg\=tݞNn=:@7˃Et7 lp鼜0wv#6,l:{<}lvmZl0pYE޼U}ݏ!,,K 8Xk}܊a2zx?&+z뵩>?~gm J/Oc:GݤnzN rVݭq*2͑8إNg<.M2! }E1&|ll?lxXF|W/7Po !hup1쇫^P'ý>ڄ_ 7wWe^^< 6L]#P&U!v 1S .R;fxS.† ~Xv391z~gCG(pvcŽ-6R [ m?!U/8?:zB|i^b¸B^}˦9iux1q0Exl=BWjKZZ,}Rk#8j1J9r+7bG/Mq]Hq~ R%|CX{aGG6tW%[[3N#޵-*3^;bq ʦ}f0`{hԽ}-Y \#P &ĕyH;6gd#Če|8nI9wRGXUi=扁 |qyvmgXe̙3'g'zqVڤ_n֑~볿&kVpp#0T77h'-^tҢmac,2T~~{[>?wnΓަ׎}4$]\XrØK`; ν^]rae񒹳4* fu8ǗB*?>#`B<7.}UDn}xTEfoqǁ3^g}bQ1΢:Dܙ3>Ņ r^-) K3̼bQ5ֵҧ|Ȫ,C~XͲ#`B} S$~8޿v{~;^A{Jpc'/ ÓzIܯb70pF OߋQ^:0~u'_$sS,6 _}9v\#ĿM:c&( ,k8c==l57[f@ %[0xٯY4~5¥.DnJ?< +9yݿ虛b*){+n7(DF%Cн#0g#VG[[?;{B,xdF|o^q쮽NTISk/;*vgQ\ ZvK&ψXFwg=png(q_2^[0qe^: xdT)̟ [I;sgf4:[[qn b~RxX`4rFO[0+R|r#0M%S˒vi޽( n}0{b2bA|+bkٓ&v4ňf$ݱ:ko7 %>x 1] xכ~*`@"cNVe ga|훳熇 ?83FUN 2Gg-=|-sUc 5ڕŊ)z쇫qsy}?=W_ ܗǍ@?q?P9H1O[ 7G}'d?$"gOnA<*KaMl3?1o◄uZkO3^Ӂ|KAOYal[B')L"|^M]W~Z^s,F#`B\F`JVX֮G\{onF Svx6yyȆU 5`qUXpخ\DhiTCdxeZSFy~jȰ5[/7oߛh$Ne/KK{?:ޱݼҲ ²kG|#]kFE.pl.c;= b4 ϘS\l^~@W}nxwj~߆9!:_sb-L&Tt 1b},櫓G W^2>!ݶ/p//] kwɯEE+kߜ~GOν.wPT qC~ԯ.M>0`nސFb]LVjFܰk,[o^օcb};skgqb|?v(/R<[c?1xT!|&ٮūWq>ѷc‹@IDAT "ZUx\|l%96m}#-Bر_@CҪ~ U[Q%5G7 D2{ūs+ֿS5ْ5an0λ !3s]y!ÌZUc#*߷==&ğ۸;}܊j^(XL qk.^*;A+IH,/h0wN[vo/ߌBv?~A]?!mn׃/|ǫxu3 +v4'eE^}l͜q™6r %QCgB>~0`T#ӕolۛ-vUv >?1HF:v슗E>݁Μ937|݁|}%ȫ.Z} .GT{lSAn?G+&u#P#C FYEܚ <_o~͕uQNlx‡mlqs,Î*1]X~a/MpͰuh2k!KPT)ㄸ&0+>UWy?|n#0j#S_ ?z>>eixpwa3E틙}x+edIݯԓ{rc |M*_Y1ӫZ?0۔W)cL|G5tl[CFj{}>8 US>*]q4:đt*ND?}6v!x;cĵ*aDilpw30U-u2bGÏtcuacEwNB=6`6qWGSkUx iU~Qц̊`R Lg,ҙ?>}-F /&{o}e6r»IZXa2.cHնΘ |hʈS4k ٵn*rAǓ*?̋bwLu=guBh՛(>F%F!ʃ&&ڣ܃v~"`BO}.#0  ,c݇~|_eO.8F)]{"WŎqUr.gN%L/U~ub}G[W9bfecއձKFoS:CyUƿkꌟ6:ν0g#V>sn>bvuS=2\ƭ3+ U D-_ͪs;ZXԊ^L wd] uYXp̔ݍ~\ V5pj'Z9o]Ol51}F@#;9=Su`ׁʻ·~:îono3wfjܭ9%\UUoUg |_@[7NngՋ\ PtzF``HF@QoݗmׅsՂ_]ܤ y|x 9#oo?.[eĂG,v1LL<1󁷉3+B_:fp ~#`#oHclcjgf'2LȆ"UwQ׮ |'FF W08dB|^1k[;=Lvg{NuEuռĔ]&Exxﭯl2bŢ5%_Tf @ e#_?avU-^x1Jdy}-Fw5vݝ{YEpbTdq~lCE#eb&/o [NLj費#|`I{wL7wٺ: S /i J{E~)v>V!;ݳjh,\_#B g_bIˆsVG@lƥ体CxewG;vW"x9н-=~ׇm5;ƎWSVbܫkJ&XpEQr}gcNW73=>J\Gh*vߊPh=ZA[>+oo-C7tzg4}f˪vp8\xa79=+vC<%U);aQ6'H̗*e  ZAWB]˞ >x̌~={b![ܘ. r^fqv/~v4 q#djb*â*Җ,1RQyI56 ڣ7}ntMvR|6a!`X8awM1[9ǪEVlAv ,2}}r\ck{ Ff u~sgm7ڋpW?=4!."Rô< (a%"ek7GL-U \z kxdyGMꪰN U_Uם%أyGeX~x%ُhd. ,WjӨN @\`Z胞 :&ĭc5@wegfOU֠=F Yh{|-4X,`;cjklɼsMLټwZOHbv3MpUřZxTo{:(I?vH  }q?@_='&u0R4r2. _4`F7=bt(Z:Gמߘ#>lqSw05n {~X7p ‹8$ eꤍN#?SNr"`B_}.!plӁvJm5tZ5Xԁ ӏƔ,7U<"U_ F7OmS[bQ 츆H1fdU-|~E}cO(1fZu7ˆ$U^0=EՋe"`B<~ X@ ~1x]sӁ0Ͷƨo`S7vN% x\ֶٖh]ȷcQ'$|$ߏoV 1I7Ο(1[9%ߋMU0zkonAExl ?ٷݽ=ߥt{_]m;cx߾uSLHؘCRiL&~5L9R3aT.ݗr~X2N|4?Q0O6&0bkyw}LaY=&_ bIBCBgvhTckng=mQp1}ue1͹f#Gu|Įpg,]d^K*?!~9+&U_etwadw?QZ6O" 0a5. ?b z Rpq!wXo߾%VJe/[$8%a|Kclho\)WՉ bO^Q 20:|\skLtǷHs<+>sSS+^\6m>c31JHHn a՜VmaCzus0'l /?ky+e빱;pgV_7?AUݹ57H d?̨k#b 5T?c[v Fyq`C<1s;  0Jǽ'~+f ¬_`-v,#0%g6&ȮhuƴEӗKM_x{(F2E˱)־ٝR/Tݼ8ܴ@vS\_uܯቀ,g5voP]a7By9'bl׿~^&J~aKn{wl>/#`c*3), Uj[ȀwĢ3[؏uQ.;fBgE3%O{W:ȓ/Wy"H2 xs})Kkaք_^-! &uuCnw9ce*8\cxBpeĎUƧ<5:*_!>9UM֋Ww*_X,S4//HY1/XxsXƲp#>1k|\lqA qFQ/ u!W=Mae/rޢM2BG @#lά |~#`Bo}@ywaF[^}i3$QXFK^zX0{u3f yc}Xo,ʽS?ʹ4ŘƜ'̫5暪p N^NPᅱu*>y`g<^X7W)0b-ב~jP=!~# "7w&N>!xba󩉅~PKFi{]C#*&ĭ"r=EW7-xM!+pMדo/!Rs۪si4*9ʍ}ٍ i]ÍĬbh]fvb" [@5au0lc0!>k97ޟ?;*e7 )<0`daqF daeAu3}+ώEoF ]j+'A  pY twCvG1Fxw0~Xl{V wݳ#MBwI< ]!`O B䕋WLHHK)6ިs`_{uE{ ^SŸ2a v6[ES1x\02:'y}fv(~B^^61v)~6]F+B;}nWyCl#?|nᦒ!|7!fJgFT}_QCnΌ|ڽ})N e7"D]Tk=5pњ|݁I/<T~*\pwz|n]0x)ڝ{nݰ='F$}|"wpX>)38L+ؕ3;aj^,ΌogffRR%fo 9C#`E`)&k.Q ~bLi٨ 6,Ohal[yD(KNu/ R &&ՠzH~%H&u#ÂeӐUK,,R]gPBmz]r38vNu*ܯU|VY9'4h]}r}$|g0d-!x/R'0cybcKzmR;侘3PӖf+]Ҭx#,ج؞R_ f0ɰMCoB.2wk!vgҕ =pؼIK=x=۳<6D-F. Y]pv? xh~aAݸc_D//^ j U+|1:#w0Txa쒅e0 0_O,.AGT<|В|䴎Gi{omkOXO( /?>c0!ne~b鼜omfWfxؒ[ VS;'I,~d"Go1ݻ^eܯ]yګO^?/5;eO{6Z Ӭ񒍏ln2=L\o߸;Gfn VE]\k0:ӗMngs?9{E eR-Nցoa\U1 UwNELW: .'. ^QX0mVw _;uIxͺ4C͗Ã/w9ܡl|4C.SL{ o;5:vbl7؟P`fa>Lg"ra2PGAw#i;֩%9)f.7l)I{gpM{3! q^~܄woϏw7sF` L"pf<eكb#aF--~a' ;~`|6\V5ucsץm^Ӹ_{v{fG/Z.Vڏy1R)feo R}Ŗ">SH/fv/b#&9 WCLkxSܥcxX|[[+-ƄgޭiE{sa=[24O 3\Z~_dB̋3؞/͐+žxY`;k\m=x({v{x,!/?A<6NbAK$>dѬl{~-vLAГ5\?aO0Cbܜ40&Qaqvmr 8 rsJI̫bDR<^xzx<`vR8ȉpெ_}S A.#1u@=h~ơ;#Đ&Wbbca0v%R7ƈ 폑D$LWm>GYp#{E@7)eǦ@W {:eq0li ?Df(G~?l8:WqM` ̊4#nΰd[P5bea1iU "3_9Fcs]t1%1PgY o_0xq wuMxxh]|~ Zycq|׍}ȰpCOQrA xA]4n.[‡Ba O busnc|YL=q͉b28w0}[a# C 6ߏ"% 񥴎>2F!øCV)1 tAx2ii\y|swkĎCa^_XSWd:b3eN]>L>/_.Z=-[gH1 |bXNj5 x15j,v9oRl;92cW̋AyKeÔDk4}F@ $j$$,8'FqϦ4ߖrRA/z wR\Yh#pJa{U-h㖘@eϴvD>Q}AN_4'Oyc& lCʧ:0] s'KGh^;ߑ6H)t YF7%|Vsό#|!/_w {SU=-~GUn0!k!?p,`pc<>vG2VT"C"/HPFSڝ^?MyA}03#蚝zm%e<Sytt{,=3aʾmżĿ}Wr?rrljt_Zmom%.8uI c#C1`1 (VRŏsǝE"_3h{"¼`N|h:3M tBF|VD}ӑ7Ďzv^mJYro޵Axx`ʏ"mOLcB>j#?,CX"al$x92:jQ]^Q&q™y3dHe[͜$A$߶`(O䈟>a~:`&7~)ow=dU)1ƛκ ߌge7-2˫YpӇ~ul]ء{e1 37_xxs8eշ¬['~,"aTeܞ8gux{ݜ0iO̖?c*[SA#20d bs<=o f5a Q%F߆_lq '.Gk]nG"> [v{Ht,!| @K;9fc32BF!oߗbtyM|S|[,=gems,\yoﮘm]~ptl!§Ƨ2eDs 4 Kg )N:n'7!IPg}1-a xWb2gh9kkw@_C:σʔ޺)\xBVY 7`J PgÄATq}9^CB2Z#nѐQd QίQ~f/GE> vDM#{㇅Q=M-һ-ɞtx0 gyDMجEo}6f.N^ڦs}.{ayv}ocOTFqU^ <G\:*/K)Y]K{@G|B"?#<.l ޟANj_ FHjFn#ja|lziBp*w~Zibf]1|c=oUi{lĢfR4 `B| $VH g?ԑ#FLS 18 e.L8[Swh@@tzbs!F%َ2G0h,:#` rE@V.{uզ\8igXV:mշ#M0WAS1+w}(i#0J V> ࡸ@_1[0F#`"`B<n0F#&#`0F4&#}}F#`0& #`0F4&#}}F#`0& #`0F4&#}}F#`0& #`0F4&#}}F#`0& #`0F4&#}}F#`0& #`0F4&pw(費h0F`3[H1oB\۶dO~}KB#`0F]{ӨgeX_=W>;we]hfvîuK˞w;#`0G!gQp L#5Umۗ3#lB#`0F~]{JL[A2;p8?-F#`@~d**×yn)inOk0F` xFN<=B\ hv G쉋Eff/:iQb0F8j-Q#Z#tJsA9ݺ/r}ߕA>,{۲4i绒&2F#0!BN {:mYvvc2ל44fl- C`y* z#7fsfȞtfUsv_K˞7__#u]2*][1d˩+؝?+/[>+po>]l~5A͟VԆG>uw^;Lޱ@vJ|AeaėΘy5sfd|o5]~ǖQc&[eΛL?<2knڱ? wgbe<3/_3'oE3,aȳqͮhu7 q.Y>?[;912_Z_h1$xMaɚyyGp8_Ry>e҃2qtb\eUcA+XǼ8TP.MﴡxYyKҸt0F#)iS|l!ҍCI9(>Mn_7 ?q+UFʥ!QKriJ+^fw)P OSSfK/FqlO)9VZyeaTā| X0F#  4))9UŲ:⹥/ uTjqo).!)kdQhzE姺b~uҺ&unGn;]#`0B #!VVʷr6K|9mkO)Lf'/]W<͓s*^/?ȬFxUWy0F#oR>xovmy\iI'BʤS]OHxZJ\D1ntqyҥNy^q{`΅թܴBkZ&-6ȉ!HdzZN.BLQ=HPWS›QPm($_*#`0EJ&tJ,C).Vu*-K|_+JYb&e4\Q+/GXDYitSiDm}S PVQ+:UGiNʛT8b0F6E:4N9R}>%ʗN!zJGZb\"i:)//4QNVCjrF"BOB_W^چLH*:{bIWԎF#zi\mIUsIi\0F#)J㴥4a'O\B)N#|+1]U,|DSG+شq X#"NS0:D &.=eCZ})sQYyf*N>RLk7K9n0F2$yk\P|IeHWUVy*8R^ʒ.JtQaZ؛H!4.iCMIl,!)GF,utPO4PB>3#`0]F K+ԩ&,IupbyЋ;)i\uԾIOY$ Ҹʤ4N q)4Nyu|EO"R$ҩ=Pҥu!:Ő2#`0F\JbB(o%=*'~%q:Md1> |l""ԑyiF'lQpNtH0 &evh0FvQ4xY+M;JROqB0O@QӐx*eiŻAHbzr鋡ʔ]2R,G\P( ɣҍ((_BY0F#`z@G)]JG+x1L)O+M+N|2tj[ K!p\{h6!nqa]tTD1\مPP!i\uGHs+zji?H렜℈#2ixcF#`.N i=lWYZPzʨ=U>ƕ0?IOJI.iƧ̡"SuW|vPDVqʨEq4.L)Rq$mO4T+Lu-F#`/Kq)eХG/iWY*OZ4ӫlGa1bZL"WHˤFusS}f1^,2}1Oe#`0F`n)WWL2Ő:EiDQ~˕eX7G4ETW2Jz׹Ҵt#=qjK U_;n0Fn!P$j77SVybt,,fy NKϙ:V4Ruqor|qW S9tPQ^eUPSTơ0F#`ZA^y*W 퐯2)^/+ӥ(rJJ5]if:94F#`@25˦knW{euW{AintAPX$ezti9Qʓ.'_eiC^$iCCPICC Profile8UoT>oR? XGůUS[IJ*$:7鶪O{7@Hkk?<kktq݋m6nƶد-mR;`zv x#=\% oYRڱ#&?>ҹЪn_;j;$}*}+(}'}/LtY"$].9⦅%{_a݊]hk5'SN{<_ t jM{-4%TńtY۟R6#v\喊x:'HO3^&0::m,L%3:qVE t]~Iv6Wٯ) |ʸ2]G4(6w‹$"AEv m[D;Vh[}چN|3HS:KtxU'D;77;_"e?Yqxl+ pHYs  @IDATx}|*TP9r` r@cSR9r@W*TP909 1xS)P9r@ʁ*T!D·[vY(30PRQr`88 tfu P4얤 "#Ow)64$Ocݟil^WpÜaHlib޾Hd͎#lp įxgWo%ۇ[rϩE9 xX@r$̣B5׀9}Ymaޤ*\T H|voe7 W{zenGWwjW;B]w/xFjQ9QޣT;S90*_*$nu[+ugV!Vh/ڻ AVN_mߟfGh5jl ǁsʂxt6TGNC@ȱrMuO\Tlo:W$`h3C X﷨9,:g qC.$pK{Wh[ѷ8 xߺ*5*qŽMB4wv,e(AKZCKT/ ˛68`Yo9 zHXc*ot!xPP& tZMw6wM2{yAߋ[:3:{3ںz2pjjqER+US*P a@ x] smH dm[hNj(ݿ%z^zeQU[>9>/<$![hǗw%Ivln:S.ة޽dZ|{ T?V:QGtL uqr^5D4v oeMTFp"\Q4z \A*U6:8 |年 _kkH eGq!TM;d}[75`nDF(-]O8Ǟc5:QFjEqcU6+68P4;*[[!7T+)~LNu2t ]+ߋGR”}suNյNjV3s"uвb"]C飣U:X*UplS\Xں9lVSPd|i0[;":\)C1:ׄϫGR~];mUmrwQÈ @!"CH08V>0wǸY͹ƃ M?MJrVjEh\o& hwYȯ5P[g).<Cg{v+ T?fn:_8Yp- hR=2I|^,t ooi+mWnjjd."I uxX! S?QFy"y^Cfl(M }.?pUL1iH]pMWO/lVT/z~Bu#?l3xkmkjRu]!v>:Q+!9%9B^%ńPRw>|@UW "BqbNr.iLIamrgq"dDoI

qOzѥNGQ#_SsD kU+F ~ Ґ/M͛)o_,9ݿ._nQ$"pbzR=pd6Hp@k>FXK2"(C#*H8^IWN r?Z'5vBcmFpg7-̈́. hSۡKHG%|Xazz}ܫXj4Qyy9S=<4 x_;*m#B3bt[ k SG CbŊ~ǎO?4Y x} xR;XTwEiD8{Y0@CV%>35V#ͳ%H Xւbvoqݹ߯+VO!hȰ{#Je@,:M Uq.A?OKt1ONԱ"''#f444+J^ x\w=W_5utO`}WYRn *..[~|eCpe_,Ż4Ȋ uA//ʊv].ozH[3*T#<-Npx֝ gdaV#!Rbr\Nt.Njkq0Oyan`>0L ՎU; |ϗS9YDYV!swe֢[:>Nm6ny]ͲEFFٳ]wwEsXNnoƍ|/vSW+h"%*X 900z[bD0"_Fˮ~PLJ qZ-Z f9f;^S{KGV E8Múw0^;P%6pX;کq:XfC'J;e}8y1b겏ZE֮wW<1Sط5:-g?͠|i?P+N 'ŨȠ Ks̡+V\闔4{[nE?~.%7 GNGnJs \/OӋo-p.f\=pP+= HM%qzPĚ4.lo`E%/tW2c=lgōlAF9[N{&=\9N H T_^uhgb}Jppںt2,,L3f`3g `?&yP7Kmrk?u;?2ZC0s0*MpR/K۔#%NQq.VSwsK݁ߞ^3 HrVҦFAu8oN68ortH["M.¯Q;m41oϟ/oFh".>Dgɓ'} u=L U Q 93$I^9‘5tw@4I"Ww5.X0z#Ř>džHV7y.#y ĽEbyNSڽy#_ÿ*kp%)lQ"^bP"Cq64)oN(j*[B4y'+o˟'tnw߃*h/Ja/jrS'JysSRj^ ~wC DA4[1(iLъL@%4==c-2Œ(ڌ8M}O!92K!,/E2Lcr־mgS N[ 4_FΦniC 綾^{F~'uC@gCX&+ȩ8d3\n%d(TZ{!#^Cp0,wC҉'d GS' s"!㭦uoS[֮8T-+fM)9x܍y6`sfιކuO !n~4ޠ~_=S`P4KGA'tϫ} YS^wC;|YsX^'07lR(ZyN׶|he0z8SXM F䍗@ ztm/*u&8eK'e 6߿ A&|.+ϫ4Y bp)@U&'8-L /F;1v[&yxJ_gވyKУ ܿ7E8(>""r`tsLd],*}U~xƦ&*++==噵<z՞v*ؖ47+9L^ a_:h-;JEQbsn=zXꌧnsE!C뚿q hgAh"4%VJq{-Kkc?A޻M~c쿽+k5~A!mV7;7k,ꪫ߆jY^Q9gR a,U3<vNz !ح&Fv8*/tŔ-Spl6Ʊ+iLJaa;吺/lxAOH҉nN_g8kU4S^'-gFurӘXG.IeM o$Q`]_;KiGqoq }sVV15~|KEH|r;k巤m[QlnO^ĔU"F; /D=Rkώ Is"%JkՋ/ 8pWza+e.2MD}:=cs,wtR|}g>0 O4KsBA0qvzd> f>w /RRRY}6s.:%*4RuT?RWul -ٌ Sxp9;5Lq,X%u`7(onc 7%kdz1w0ܔpxR?Ra'kD17@GŬ s]72X";ƙ֤xN&:1"[&z8r*/hy<4p8BX ##`n+獸&Lu bc%`-[PGGcW(~8 ܯ@Bť9]fNƫ7׊4yۥԏfFNd՞?7'cJ (޲f:Tn+ yuo ALu0 ^wN 23>\gu :8t ;#ֵuY(:#m˞$X2k*M5ڝk&y\ΝKlG8sÇ#G xo0XS5@Gj:gYNdխ]F(P8)' g'/ebgI 7!om?D0]fu]'NQ~ANNڞ_DGȩkapNմSYs.Lv3|kة%)}P8Y_ 5}ɸH9=aeWIjyumoഢ6E--Afc:6̅!٣Oǐ9ټԱ[Vb,] ?wWNZ*˕+WҟgNcaa!|ͽ[ne}sꀠtD83bU^BBWM~ٛ~ k:kuk"'y0l]K 'kfxZv3de a{ ?3/IA'j|xI苆NzpX)ଅ|1/575p2Z5AN3m0(?/H2"đjm:S7ZfXpkϋpA2:gqz޿4ugwI˨{IG]TT$oNlRFڪ~#BH F@ۖS)6 YhAY[5)F^0UbpX9S;w~zp$d0%M6uR{w@RFiޚ5r ,8oˁcs;|8| M8Q>dE;ŇMV#b{Q#U/=H{g:EX5x;Q\<1U32,G/hzn34綕Y5~^=7Ac ײ6up@akۖ׽y|| 4+^>8\OuNy#pY`7!Cngz՟ + ȥvFB4ħy Ç,8ph﫧`)`+n4x1ד2**{s J{WdJvRV录jZ{"2ݵ(ŘüO~m:DՋ<đ(7\mnF*4f rJ3),||_[@s&8 ߉x}jR4FtUkw(u vL2cpl, bavE[K81G\<Qe <.*@Dq\:Ciow%N}ɛg% K׹w'x6$qF)MXUX.J L_n)1jD]Z{ 7}X ]LEy1=!T<*>'Ǚg#A#N ZarR<|&neEEҩ ' xT˕9:#?o)*Zl9g-MƵuvHpG%-"l$PRkWȈ l>!B Xwڲ3<.Gޙz.ȯ ZKb^,͊"E5ւW [RdQXXSf@n?.yW2Ǚث x'LgjnR.Z{rI5v$YptƬZ;&)_1$h찕B3q9^3t / 뽺 ۉf=o(>8<+)gqcjJ[/Rqz8Eɂ.?? wU#e,0ix$#V]!‰e֬Y#9;s<_ۆVOjڇUDFd>j/^8 0:-eb뙺A/UFbOtwcOML8^lуw!X.'#Zᔱ*&|3 wVPxwF:W1dVd6iHtl#0pܰvZwo~TܦIg%Wѷ[_4!c/vAqe͝ =˩49jNQPnbS{֡W>ZL%!)=H=sM.}ѿV ^5q;s?q͔q.waZ5jP+<#r֠d:U]]]}z뭷$ y0:jL* j8eEEu\%Pf;$X\f EvQÂ:;6{q7 OIGx5%ȋ#u5D !)RyMĹ7KN#^uw #NF֜,.$ky@E6/LvyX@ċwA@۱rzR7=A>Z[[E=zT]`wUzgTwfUO9æA)X ǹ8pbp>Ӿ>3^F}ԧQX8_C=joM+6 NdsTwDMZGT9=k ?/ϒoT\ja]Hz.TCď1drX}yVBh"$(`4 x]rqoe]]}h}1\5饎U%ƞO<7~­J^n.m2'IrSAoMjc3:ؾVS"jڠeΦpg@[^]jB~K[E}S~{N47f'=6 G)j;&oN7=5m*;FyIH,>@sNv3M=x\n3;e?y~LHf:9t J9 %멡/gI@sD_w QRWىSߎ@ >R w\eƕc͛53l'[D_4 $@S_.Q'wiJ_6B8K̫nXMg\ovd{: ޸;*y>VKWƥ;b3K8_РM)M@wX1'>9ut@"ulCپ=P_pq p1-_k،y_?PMkV8kx\|xk옐LԚc5rBNn,XwzВ +?Sb4~4kMMMMܿMπ*͹;́~U}GɀYs?"C!T8T%i3y2Šg?{0&ژ'qk̈'E[㳹Fhۡs/O(him;G"7  mgYLLizqd4'7."e9SsNsHʮ&cKofS/;>UZ9 Pn |xyzeYY;}};t[UGFkS(iꔿXdg'ɇ.NG l?%>X9d]Z_9H'4l/nl,_sB\Ӝ)8%d;䏗?m-s5W3Ӭߛ? %l-07%eYΡ<yxӷc >TqR^mi`Abs8~ٌYb^y啾͛7󺾫2 DKHWiJ})B\gG(H"C'ҡH_*٤쨰1nC#|hs_n U-moVvt⇑TpU4kںE`t!ͪjnuO 0+r;䒩S{dMm@,> vr .w4ƿc?_:,37-6Gc7h$LuD3D;?t4_W!o~zuPd<[NM_/𷧾(U0Oj)eu#GS åMv޲Va?|9XX#`0H6Ӊ3̹R /7[C#ztG>u_~'UV)opP|yWNW(cf?X5r}?6Nӏ D LtƎ_Cfen4^RnC/rO[cPXĹh ?ң4ngʳ5<LNѕXǞF]}${fﺴPfȐ1BߑV t͔$~fDBCTQhխs1ԲcQRzo$3X-*ڨ@g';T~c\8~{P\.h;l5Ϋ~ݱ7<\Dv^@W7Y7oNL .ΐ+1oO8CD\;Ƌ5>2>n?: ݂Q^LP± #}aSVgIxsbu6L|ak"Ho-ߋ!{5Զ!VuvUT@{㘮iw'LwvgpkGvY9TXq1a0y$Gʳ_#BKЈ%QVp99v f?U_^^N~|wX]v hæ+`^Z!##C2[GC'bb2_BJX6"Y맬^Y!^Q*ʀd6Z8/Ί' jqwE,*41uY+sYNO@.q4VGO{w!AbYAiYȳM?Ue9tETg 29QT1atBt7rN!+F#өS{ۿvZZY[x}_A{M}&s7 :$$Β/ -nkSH$iɋ_L8G9"e-2ɜB@m<@ͣ5jh݆hKr)1d6'o/0f 2JJySDf(P[->*8_ig~&FG8j^>Pՙjl%5DcS`C2s}jgbPM=k"}w;?73֊;‚Rmuo9=M[o^H" aber4V0M~kN-+nM-~L6{C6vFa{WnEXpM ʎpGyilĥ%; *|oAg>uV)𒟿NcUmx΃ZŃNW!sgy*X3555FY^sgMŸas롽XQ'a^M:tl5E$2v^3^rei 9įIT&kH,wmY&00g'j获O?m-We;;Ϥp LJJ^m͗Ci^'  WMo0hۇkI*&Jןkpdv9b5 Mu- jZhNmٟ+BQ~uGe5D?*o0+HFxP5$ND '+L/JϫOc?^VLy؟u;JXEYIJQHE*P ЦP`Y+{kb@IDATOI`t9e eb(ERx*1Փe@.fXs R Q>`7V[J ?c =&ѧw(},Kh5$ּC{?0Ff*ucnHm ~ "Gk[3pinyI<" r}iXy9;󒍐q04|R,M BfOQƦhr_^v< ^!yFr(}Q,%IaA}5IqZ0?|kX.xGٜ!Lw^ yilr4|̬8OB;7(捈*g j,^z819ޜ꾑-J{)߳NO qKӭCc EYV;gjY0_<.(MN;H11FoG+rc~bհau: Nevއ$ C-b32gC̵Κ<{s\ۊ`:iqB9pԁ39X3bo^ <0?h1)]4<@}āvG{iarBΣnZYaR#M+ƽcNOx 4?5)(q NIʩ\^l5Ok#UP|qxVԷQj$G I0A8C[k"NdÉOR* Ki'vSbπWc;W~Mdۏ,]U96g'>/'*0#w 򠺹]ceTx9-I.w>o_4Q3!>-ikK{k̓~ekc>>9ce[ǁXyv< KlU;3l=Ec+o!]]0v"7#1 Xvؑ&KC EّFamzi?Ljw6>&DNNbC:sDbfs/ů\sF`"f# 88}Bg12<J[sghybB׊^y8rmoɯ/1;8O~QOYV/p?OS\}*9`SiVP&u10ESP"6JbÖ>~E' ;{u^ HB= oږ ctXܲe:?ÛW00f-.K sNAC=79@ D7//OTf2oT-Jm24Wapj?gj[~}ydXY9#}-2N"_Lσ@xs`sRzJ};f}KR />$$/L)u佋S wπ8)[`2g\'b)Y*y3"\^Vi᥎;zlÂ8zILB TsScH6RF{xY jcQ漭|~ @rnb{"py}Ç iC0 v"O!U!$u=DTXbPs(O}zc+'腟]6וǕiN9`q6:| :#8\Bӽʔ־S`AI_Wf_#-NǐJ !cso'~I` gF;^Fkm>og3 ֙ucuIw4 e(݄QBN n~>}jrXei8;h'|/lƆ-Lw~7NI_7/;|hH-8Bɧ6F6-ZGH#4/5kz9"C Kk@--aԤ# Б+||_2IM'f9qTR^R;SuCZ:{cusln/2B|v?^+嬐7]!ޞ>qvTo+9򵩵pg2魅Ӕӕlex Kh+Ve֮[i)c 0?C6Eu Ie8`?͸#)ߔI]ݲ Yz{;P߳ހy!$(;uZ9`fdeT-K/HRܦ[hUqnƸXǩ`5ȍ8V;W1\17!PV10sénɫ6;31sXapLaPۺcj VY/Lx!s͓}P u: Vpu "jv*?{v:.Q^q%C ')DH;)S~u*{Z~{k8G)aϥdohG6f CGlG/ | 7F~~ %\z(oh#$ {GqfU~g~\9!z9[XZӢՉ`8?>L~h6tCeTNS SRDMLLE~(_ɉ %$yHpӤ 渏jjߞ3d(n`}{={:Z{Wp5emYf}=ntw > A^B˾. 0mA6` ycRxoyij>ߚ*/>]=5lNJìAد*t vA *-쵿N׼6\!&g @U_T' <'5g/u3o{zhM~4-ӊJ}`|k_Up'XnBl;ڀ m/0 bnϋJ:YaM΅ɖrZ;B 1Q1KdAIbzl&!\Yg~Ǫv)qLގom},ag->"{ ;[)60к:QU_+ĉxi;\ƒD*6iƅCJWM+RuamNBd ]ѝ6<˲]هciF?[}%.2Y~viNrcD~uLg+sNM,;PG/9/p?~'>^-nM;*À~kNgOW`m7=1ư7hSEAqȀhIAq" Ļ? /!F&06fmߺ;DogC7S_G誫~`P'S K*ewϹ^[꥽C +_RH(noН~g8oNBq;+/ɉAf<@I 1͞SG 1eh.3b w{AO޾d\̏6}~H4ȠyNXpWU9 aalepCIkha Xˀph#c(8.CFI]B\))):ї!;u*[hΜ96ya(@NDpL ޻Z}Okn7,X+HĒ@IeM-~WipyQaRnNUq@XSb(>Tٽosryp|v6[b$+B}ґ'ޘwM^>i77W41>L,Lfit|U߼}M _\~_|5٫q~]`m9ςX.Y^_IV0{"~emhQiH.kAzDǑX4ĦB@}*<6}K|//K_}>"/Fi&9|bm?(<7E?8Tt֗I$Xq/ !u'2%Z;zB1[~C5=D777Dt/=Xr6 zSV;$9wD3Bzn6;^t!#g O.8݌dzћ=p8'&x2ϻK~pN0Xz,ɉLY/lt15=/ءȄh8K>PGx t=uí?~\w}3eegX@mՅ:kD{QUJUbh s ×oVbY%Lv@S*hf 9 n= W'۳ȭ 5^co#3gۚ+~ysIJg|~Gُ5] [ШyB%bɌG>.:.>f ^wkKMDli'Mh6x˾9 gCL݅BhffKIZ+5ω-='H.c}/'< ҮzS[?fa=X̅{t {l2Li5ܐLtHL;jKے^vZJ ܈!z{;yTo~|)ůL22o vw?NOVEkN:oNOpvsp+z@R *Z׷< 8-"ZSںig4mg~It_hu[?PZpDzyx=-kh]ZDMM| qd9FHjX`^<1n#T#&NQqu`t NȦД\lW\@{bc4ld(:B%'iF(^sSN5pvG:}IW3ƺ7 ^lZ|7vR;?RB7õ>qExQbOx 3)W$;}z>;(Jt}US OO)Bd7 u/iـ7LsSGf0=ƩG̲EZz``of{@KZ w0#Q('%yghZj/nO3 }mFkkf6C܈AruT\g[4w "7b;`Gχ5hb5r>s-Gvw<-ˉp=ZgI* ړ\"NWx[#Gjhә%rPK#Ί׏f[5Lcov4))i [#3.ȉ Y+&k//*p;A09gjzםt8LeHY]k$Ő1 _?dA&pV^M@3 |[ocǎɧ~AkhrvW_ 5Qe-^BApAZ&:ɲ&v'B8o911kgIeȅxwe3߶[}KgkKdg6ojdP o-Ȋ}T{u ~8?L>"I&^D_6> Mu2&>YtEO=pZYpA>xyUR#CqYXQRx)q}^I:5wvK0˻K=믻:L:?\Z޽>K 9yQk S]i>UsAVfz:p a`cB,z2e}c!VY5 qޞsFj$]7-FNd5`E|vk"g0;[9u'_cId:3D@]UӇkȜİwygaar#FɥK}2̿n(d#vMglzB " ]zoG* "6@齥@d~fvwvwfK{s̜9mwW<~a惿0LWTNhϷY^~msM'-ٞY ׊|~{,hc[ȉ pI((x$gzo~;m x`rZ} Op6>>9N<ö 6mM:_uU5\^uJ9cG % aHDj-orF#'eq(j_GC8g5-: B؀8ui2Unr|;Y2|-_--@ J]V"00"ؙeqqq,??l`ҥVߊ,wiLkCo|]FF@.+*- y‚}"JqΚk?Y%GWG*jkm׺S81yɺ/g> t#0Mz^4--?+бYY svg0;,ܱ2Ç;]-b"^~>㍍P@:vgM}>HpqqLMSo.KRr?wzXy~ '~}_.snc(&#T ÐdC㖖ǖ-[[o% rASZZnVMBMO<8P>vɒ% F\$.Ch\16#մt07puNŎr]L]?zꑯG S,K&GwOt6O:ͫ=>M1R7^_lIO2g09#iMrLL?pbA[:xAk Uja6w\iݺuwޑjR:tR[\_gV%91eGwiXI] .n9->T"d/ r5EkvW q#X)Ű1㜜~f>ignUN3Z̙vv/ a05U>>!lc!QBTwWD%rhĺ_P(TpmiLB? *{SirKg'5zEqwkxz#ܭIiYMx{.q%Ut)w} "_`E3gVTajhmW=O'[dRItN -lS{Gt߬@LsWvM~1),(1'es m`&j7+bϓ=>)yr{y#F;;\sfBDp`0g:%prs5}9/ گ;>ߪdANݙx)&/5$INyTWexx ̉)mЅz 7X egQh( G], #2 DܤWvGJ-𬉐ɂ`(MُVu`Tp OJPY9m * dKM-춷308E cs-(ݪx  6C]wݥ_{5ˇnmI5q_O)4C18|$Л HJVT^ 'S'HVviwN `szSVwG u:v4OaqS-e/?wDsʼcisEUe`JĬ0 g –,t=L6wQyt+o!JXY-mNu$`f._$x  8:ȏ=!O$ m$:2t;0 -.SeM-{e쾧g[o]N˱|~.Ǝ!d^& u?zSRc14@&ug_nѿҦh GZ%;w/ӃNL۞6׳5 3uw'h Ƥ;R+]ٕɃV9 A_sn)yXwe/W|^y߁ asd"b 9Y~c B(UTTŋKٓ@[}^ /gZSJ¦~=(GgDg@wD: \R+@sbv=j ~{N֞c[msOdw.ץħ$pp7<Fx A{HzV/x@Ad;P9r|\ァ,)B? d^*5a o,4q|U՘U}e ʿ!a[}~ꃍne#~||l{$<0Nz~2|<;4nIs P&{m?c URP٫U;:ur(<ȧh%YU k?ɽiJ s9g;/vYq֟ g'[&`b+:~'Mڔ3fMe؆u;86ڔymM ʯ׮2@>|CX@`0q&HMO#(\ɺhgwV޳c3R`DZۧ@`Jx:}`Bt{jb]mQ/U`sTF9zG^dd`޺,#ywɡ3>CxPbv> (Ieʕ+E-u;>`ATUn}]< $qAVW(w+u%)f꺚GB>jNqYs]BQ }$hnՎᩯ^CHS|BOP8 a~A,JL !'ŇX;Lӏ7UJRu]gsaw8^5 c7n WF;̟?_L6MY>$_ie0V؇BB7 N.qu/~YZu [O*i;㡺-gDn{?hdܻ! zZ1_{C8YG{vc۽C&;52*g3FtŭX ֫W/-w}gQʐ>M*Дl+cg-njN00i}UIӸvzRӒG?ՓIvоq!j@RX5Q"w{=6C7V2qs[͛7 qYԧ2ƈ@7k[Ģ?9@- /~ƀt~aH$6YǹsJ9ljO@P~Ut9!#L|U]9=Eh(++\ψqYW {9EGwD/K!ZXٺu+1m.ZY4;4@\-ݷ+(d+6}[sb'HoxdԓϺI|#JdϜG;=$,-~OkHt8mU)ҚF'9̟!"=rbx⼡qKvT4vָ\G|뎭a~1/Vʄ<塴OlUJP^%#em d|눸DDJJdw{hFZC @oR՜uġMB sS&%6DŽBy߈Ȁ]jz3e;wC>A`Ψ̳V{7x)sުUMLZj+~N1}VP'Mʍ_aŤ(LA`\~pxjK3V0̞JDȉY"(LHt ]/v*Ifd,ygSw0s0gVqj=?.#lƄ+t;zz]LX| ”8Ådm=d>6?<1zľA^%!NK6[iY;j`@4O bia+V=2O~Zd)`Ġz- s'pg4Frs`RqVD:OuZT14AӧO焒X] jEfG&!75=0^tZovUaS:%N=I= tFo(HЮ+!^~W{\ƍ+5}džUݿ%X65_;>_G+>^>w "'&05G0yIBZXwy{]ڜ f Iu;~m$?>Y J gg Mg !-rM/cwr J*-||hWxք]D0Ԯ飏>2O8h㏛ZX{,I#CRZZjb=z4O@6%ڑz~jNs8<-];1M}q(KӒf-N}Ǣ@7xI9|Ije7I㲡ɾ/5qػV½\g\>^32M=u)&XC0ڷo_w !n Y+isL%[2> W_mͪXCc^u,l4^jOE*Y|~Θݑ8) v IzkrLX<{lW'ad:6Oy`Z~[2?FO[ ߵO\:mJV-_jܵP')4Ƀ#|]1H P5+KtQ,88X z즛nY{{^[nYS`IEcv%J&7յšf&yB'`[CQ)J=xaq3i{'~K;VpPxLvԮI #~(jRr0~WOxW\7cSz^T1Oԥsk0z$ ! *XŎRSWVV5k0 )J'EUϐV{"܅p#Rˡp$՘dxZ#FpOl.xB ~ܯ)OK󏊾(@;iP ގnjԡ;!.9`9B6-̲,uwi[!FȆ7o}ڭ믿^ 7P0ְjU| [ZȸթʤyBL4^u-|gI˳ $a5\ćF#oMK"zG~ȕy#li`bG)fO:r KgGwTBҸhr`e_0 ;*8"%d>17ZwzqrbؠяM:1;J(nXFe#~g!qbU(C;G( 1R6ƚC\^ ^WX;RKT_qFNiSK$o&:TJ[!Zu+*P[e3alJdz z>sL wLF 7 Oq)c+Gz@IDAT?:EH Xu`^$0m{;4pdfp!;pϬ>YSye"e{*5*۴O!x2-6vW>y?̖+myhQDjY VD_,cR_h 0wǀVe~f#1L,6be#Sb•D J2y+W/zȌǵ, :XU8„] 3C%PwM۵>mmڼMtJw0g<ܿ;IC̲'9AOā`r,l*̘2e {QU& H!U+ȣ^g/>ؗNf_\:-<7 O׷t'|Dx2x"aMaN5O zNTT/SmT珈}Fo=Ws\iG! !λw\Z& `Gݏ#1wcjhD!D-Gtm?϶՞ yz\XebBV$KQ V}NMsԹBWWia>䉴@<9.6h0"Ԃ|>q>AϾ kQI'|R,]TYfi믋kATґA|+V0X\D7r߰hCB9k. |6CDqh襂{+2d/ =br&r2V+(j LyVN| V?iDw+}8óG&LKcp3Q5%8'{<(2&ۚFYvڥS'FI®۵:'7t͘ɦ?OjwRRxZkXޞ^Yl0 ~'Qi]=bZ59Q>Ai2 -vwV+Dmu]|#/@IJ;Oٗ vTQ 'rӒHۚY;0iW;kDūfƌN?@wqGأZ3a*,v& m ~X1h vȍ5{TԒ5ZoT61%~RIYBI> ido<̯d}%@$֛%v>O iwĽUk#Џy'^張5_' nVƎIݓ>~%:qD3>}.!! vX~fa/_:ː3߾0 / P% G6–N q4؎V5dN}'#bO%Y-bGxSK##ۋPgM6zaw}nFf@JT(e'd&¡,`D0(Wdk5 CA~~Xryi}+_r[kک(^DDq@41#Bٍ@9{pD^εFCCrD12YrP{.9eLJMWR:Z9zkmG*?klionkPt)pb.;ѪnzWs̑ԧol&N(nKd"rucqZ:}PPm5Mu.'B^hK zL*ƥ9\ɪW*ze_RнdI[sk k⤚iy ^"zҬY$- =?xK+wzΔ@3vm**|A7ofV]Y}\HfGmFu-u,ܩ*]pPt GM&Pi_*eF}-7@wT+q_;W*F 2K}fڜb[o4(#Cnѷ[^bz#ָ_~Q<=g<@k)R-K: 09p#g;HwdQt_™Be5WuHռD_p[9KRv5˿pꎾ痏I>ݬ]ݕx2eGM8G6f y@B9$ eA-5,a_uUcXsbP扰D ' JHG|OOP18,6x;XC .%K*޶1#S€íڥT  &NB`"@7@ko7tM[n6ov׉J*VdGGt~$Xc&!J>: k|LN} R섔:FS(^}9V _BAáp%'R:';5,a<]O[jw1!PvrN<7,+M322VR3lI%*VVb}YdHuy5&G4bsɞ^@^aNca43w;AnL N3@Vhm7!GĹ{G@sR}Vz^{-۱c? u}ޘDi; 霜OKx F& \ yxѨMxB~Xᰩ^2ך4w(o+N'O(Ts5O˸e)ޑ)t͚[Y_~mg %cK@m%PYү |{܈a/9l*y*xOIaX=W@]Z@ՠ~OHH++++I<(11_ yg=VUUEaeAcL'pYnmvį6 "P^,UU؛0{trYy+Z\F>OZɉVNoA:7}[C-kd_pn rVV'''իW <N: Nfn_~;#K/lp㢲rcz3rk3$uD]8~|m3A-gDizF/-XjiC.Cy~]_\m:ȧ0S#dK{bJ %g>s2-Uyv *ji4L)]yma L H6ii|mwQGYSUASMeb18(і&6TΉ0Ĥ;Txf=BxZB#KLVPfii_%'م:;W/@N-^[xV_M`7J.kU %W,ܭz +&ጭ򐅼O$_#Ճܗ%sw&Y_LjSEu_1\89,9׽^j _YzA{3Eu蟹kO):ć)Ƥ9vUӺP>O%-ʃ=x35pAf,=dlφ43ї'mǖC+@a&B:G qUIّbb ޖl3%+X>kp.f->O ɋDsZT# ZAEOc''mkmMu?P 2A#:Վ^CK+u P2ƻ $Z}g};_\+J`m`%Ef`hrdGi=Us+Z=D.ʬ97O.kwpvك/::G͟Vv8*O)eKyXA>8bf;qmuiOӌ ;zVuIg𥸉NK92/1p@j.e ,;Mt40r>11IzUf=,7uT[ozNuR@6jd)>.uXK)38VY#~"]|h gh>gl/U5%GcldI_tTs=;ǎ";鞓 <u/;eg;_gq)"u,X4ёV^1駟J\wzWKFY 7`"Wd_ŵ xr2Z{Of#^9J<2P8='R@ZnioX77 ,ISH%p 8yHš&#Y*]䲊TO]e.oʭ'|ŞrFqr"'Dn$F1񆐰Sr"k6rKţwBEg~xw)z[E/ڃAov oܑM9n47 n- Zvt4(תTZR&؎v-~YBXcB:hXWpNt*ӟ~ų%0Xs6XJCO̎}eyz\jYOwӞ;qj﨏v6<^mm|Wo=?l*[ 'e]f9TSOk>|sNf\r 'R}r#š~JvTY2 l=lw%-Ӛ\OMlnvౌomwY LxiHag~V'44%IhӖQ"aOI!í; @ \,66_~$¡kvNIN͈}xy^Rn{k\sTft Nd _VWr^5: Fw6yp06Ńm4UiB-mZٿm3߿ZQё^|E ^>-"ebu-S|\w~ͧ վaܯ[=#2?yR;*Db[?ڧBľ =YLZ[cN*Dx^ &9-7I36}".bX؀*' QPIB[v퉺|"6BʎpAhԖ+ǫkAϷ]jqq6⠀߆$xm"UuANDf‡}ΰDAxJ |Xb1ohO^rdȀ;R=--m]SW\a±f$R{Tѧ6B V-팉WV(Sj?6϶sb񊇳r+ K)ᢴ{VtD OZ[GK?P7 bd{F"MxZvw #d'css@5RG4oljGIBJdvXr%8d“y~9[WKF$إ hxџ_]ȵ}Pg"ӳ0z+(vZWI |* ӤIkc9#?{ᢑIg"V粑lUG*D͞ "2' Ot^YIpEa۶m&rLKK3 }SOAvx3!VtE$hI tlqO =Pv%,Ch E 0=ػc~ǽrUk@R˾DX9T,p:;7Nѵͅg2X_a/.H8cXY}cQժ8 ҳ'ZYD&,0I {'m9Gm /uX)co>}Ɩy HelwVlu0zϳ=ݙs$:\T`['k>hڦMrkj v'Hn9)33&:07皢" 5K6rrC%M&.Vd_Ǫie&/Dh#N?tFZ#{X/eMey,ͯ?13C|-}uVqÝ]!ºpDxkM8XҌ_ͦ3D.z_}'9GqB^A#Z&(X]VH=ZK]sۗFFfwYw禉zǰe;T~ςi1tT"՜ Y1W"UE{t 뿿Đ @b6$iV$JcEȓ%3<<1YZ&Ɵ1J4꽛C<9g=4c:l_5Wj2qS[ ֢K2+`A+V6fCqbhnjB"WX-XiA`g#ۻgM91 uK_]z?4 hb@ӛ԰݉%Hշ$v%;ʸ#‘URYC͔ĝ4}S_oݘsmST+ۖ+I;QuMJ` # cя@}tdXſE ZK2֋HU{z 0p.ig_աVy5k*͗#T-R*s};fk}ٵFb~FJ˅̓q/~6`}ν6pv|P}1?V`&XR?ø9aK}GkH'`XiK n1&{4?~].ˈ f7ONwIS;U.A 4 wRmQL bôi Ljw4'>I&|9 S^߻4!'o`UMn?e`.UFs^FT ;M(lYmG<=߾uм/ͫY`sݻV&tG<B,DbY˳Dmٲfo?pM<`5vEtЎ1Gκ!M[Zիŀ,a]N&'gu tjR;y0qcŋ0}|`3W*ll.n00<_&ԴϜ4.6Y56؏?4ڂLP蒇{Kcf bمj'DIj';UTiÝZݿCCcoK=(|6lo@B$  ༀE q~T18E=7~xNvdVG@iӦSn*aslw~F0vy ;$9>w_A,;Xrv >-B䩫ot-1v>U4#P>+*/d>(NsO:՜"n{p6"1$'P$D5(N{WVTV&OZ?Q N {Γ'ޅg=_xMe[;:@RBwMgٿO'8XEJ),6WYy!b8fg9&̚s"KS5c-xCe) 4}ל)Ui<~U58sDo߳xlb\0coޒ}p@ Kd}Y`.ީ J@ڭk1VvՖm??>w[(p̊DY,6yعZ!b}yme;+]kncݳ>Kx .D0STT$>O#n8R_ <p#CUKC`&Ph~>~";*-WmI*25;u]w߾!74#ĪmWcZ5_{]0 H[ aS" H_b!܃)t4gdD~xo_pfb5{UC_ 1W=B:>sI)a3geepW_}54~;nW2dd'ê y[CKߔz!܉^R:E7p$ }||fqr{X} qt}l»AMO B:}rambMц9ϰ @oLBAdnos[A[ϰupOY;>\y ~B(^:Rg0ow+vm SPv4Pa7 Ӹ e7H ] g P< B͸ Y$Pѧ_9:E zE0*-Z:|qLMm5|E=oYl9sLssA';x̥a-7‚z,[|UTǢ/s ;/~YQ ;tX `U|Ìcr Խ mCSi}.="@YڽvxzGM yū>9>;6CH aɒ(aC;E~ wSكӞV{ڽF OA#2X wXdi%E%C5| Fټ~h|WxpD1 z~~aO_iO7}نuj122Fəٱ`i :r>c`-SxwCd;fΦc^ vT;iGOGG+F'eVN|zD,s u>YjZH5o1j_in h_| >3i?O(m47fϤpjAd-g8G~f=;vluE͢r!U9$)3T|JWv5+}[7zNᮬڳMZ \ԅTʟ~XhYx eW7c؟+Jy;cNa;fgA,W55k4ڴ4TG׋'x;yZ:^_J Z S%GkNrHnH-jOGSxRt$;Xj9Fr dU͢xW$R%4v~e3v4 *).Dj,|(SMw+)w82<92Qs^?NÞӡ}0]v9]fnV7qtyeGDNG_iGkK,fLI+lwCu>VQͯ'/Ϋ5Դ@_'ܩvUN-! r2;9De1|svHO"7+@HX pqڞ! %182`s铆1x |vUdg'S>MnԳѩaOŷ=H|chL l#baynb>flcp`m ,1/#-l>\!;f rMeP׳*Nog]oPBw,a԰;0Y^:3ay=lR\kwM4 x_}ֲջ*!VuVnϣ/ x8iqf%? Á9pXd w '*B@LqAD@6;" WNٌ>4w}Wk7̀&= V-X*ܫAkIl}f ص2h+\{H0=뎣<$aYBm]2b5@e+_mZUcsR[dݓ vܧN ﳗ^z+;?!U|%:]ve?R{c  |z_U+.pi3$;y ;TVK8HdMPջ?KOy{K3&vG "'MͶVW>|+(@jgܷwԽJkMlY R=!`='ln,E cZ8KV N2ߨ%")Z'YY} #r^UC}\^xwڮ:tT|X߿뮻?Sn?fx(ܾ߀ly! |pq>4vv̗&>=Is³$*XحU{gni; m??tj?$oogS m^Jn]֚rss Tav "/$`tC@Jfw "%%aXsuTsp'XE1x6Tp0Iccw^t ;TʾcFr\Hz˜ >k?#u/LQ":;b?`œ1 OHw qxr[׹/Q~!o/aVozxC=@^d1_\z!mKVѹ-p4m6mڴň9s\R~6UW7<eT >w9T&%* E xx B<^OtK*>tϟOhq5UaMrRh*:>vXվh2B_~@vp۔3-9Nl ޲6 O<_xᅬw6}^(P5-4sl(FaT0GErR z. B^D`CaC7^~}r-;s/<ƀ?V~@kas&- ˪MOW]Ĥ_`j.G$/>ax.U<Omwh|:l@IDATs1#cyZKZIUl_Y% ?nppL ?ߔ5j1Tvjܐ|}Vێ W;0G8Bt8Rl[w ';V[la~".5C_8B@z R<_|93f Err2{W7|n}ߝp8ϫnO=2ٮm> y](>S:oSF07_Ő _bbb,駟ؗ_~V\)m!o-_Io[¢|M:*T~+~)YAsus;GY>|2hJw_}`I|][\~ŨD(M\E*7()DJkjO76nv&h9(gk+Ma9‡dzH'P 2_+SbMΦʊ#>_><'68ٙ_\p "sfz7 ǎVVPw̾P3͇w\'"9twG9g\͛'!`'B>i~cmZ>k? 'NK}|DҰq@ÓBN6 ƤxC#~9< !߶,彵v pSyQj>.6f9T/7ѳU3Bl FN:Ap4l޼Qݸ V?~y*?T~TF֕E^y.Wf Jt,3TV#FY:ԶDj֚Bsnv\p5״x, ]K n3*-"wPzIcbB@n`+do oCՁ6hӉ,oҥ+y#IѨYWTӔhg>- i3X;f@f<vk4믿S{׽j~ yłDZisynYC]G%.'O=`NPn廙Zmsm)f2~Ë =pWH+{aIv$;cy]y#l.DO6oY%.;w7TThm-,*ALѻGM MT|bYm;w_v ,y;Qd6 od1cPQoյDgK[0I.<߉ZfgaQNX KNJ1~`yU/KYϚ2W+dpcKS1.d1TTU l!s=\d7^ zr' <<]-!2/vFx}34f)] !$I c \Z 3A;`}>ϑ\ɻxCʦ{47iCKMf*Q[|p"ZmũD,3ɢg7pqHhw[_ƉvLZ/N2od )ڏ,\3 ,h/oel b;4m8F½U؁w4&W+Cv59L>v=}iiFqUp8#Dqa݃hBWT&&*ͻ˹)ӣBF ^'>HaYi)lؠ,1rp'>[ ~b ?ן Y ++`^(xܙġܫKle[zIO6/&`> 4!s-vLjj*Z|kKE|[!aK܍vUEX} ͗CIU"F͈{ +]P#I%EB1 8XCV\pf-ݺ??nرto^ N03]]N Ь#+tu#C9ıL8i>rnJŻhw?dMĚ;ul߶I7Xk!U1fb6'X qF[ ֱ|̠ OҲӇZDRR%ūWY%L zZW^۰=ewؙJ3kź⺀tJV&#BxE7{>5W-߼"Ws/p^outPn34箯bu 3IȹoL# ?FZ\ Ĺy?.Maf s6k,~a1i}tq8Gܨr76 ="4s &'uM-}uD#tlX&WaRtc-[q@+ - (Dq9bOM3DXw,[xy7Ftr'$ /twI] t=\#2c%X'!U1g7l&; /9uDQklx9_3 $Iiy'-v⯅|NovAiUSmg{;  -U V(msR4-|hj~2덁^ ֶP~‚d s=49Bؒx ua4ˇIkMA?s%K𒲲fIIItIc ^d$Yd_쮾 t\)f\Z%8р$KrF65{B,<19*lxHO/m(g)g Ceۭq ^?|999ÑtGrĮ]J pW'~VUUu҃x7We ֺ(K~)eM0aТ(U*;)3q5ɼT71I8r2@/P_īiOFǚJ UAkj4!v\94^pwWS٩|i5!g9H0%2{{eP(vp@vJڒRGqܥ2%];u߃+ir_![!lm3)Gj[q6hƭM.\_4IaJm)ӳU/8e9oGf~Bwe_}Xjo_k4`_!<Q723Io͍ i 6|8,u߾ipaWMe} T`KN`ۊ q:2ΟUv l\J4;oˉ]2\aӍ+tk(U})b ?Y" dCAYMBQœNY6fn*SBqFvtYXώ+.NBHtB7p޾T8y͟f_-]>k$14fŋə}kkAϪR gw]YǎGU'{Muelzy+UQ!~ՌlQXӝSETh=:W[B xdf4S.%G䆤lcKa[8Ӊ?1$`$ԉԺbӦMa jxK ]Nhٌ%-np17b"L=P`enx˜Bk AF{פAIœx/(4x*}ɾ i:~ktXdۡPn|{?fxv![Kv/L7M__ȇ5LWzrS?AWуu*_}կΝ{.ft Fչ<3kA bJA4K8o{SGTP6R"A=x<M;@@3bhUQS& țwm4 5Qo* ߒ[)V+^~%j_!S8[{+5(iS-m5l}bBXT~o#N*%bݗoXTE~/;Cޛ`d6 `%:Zh1X"dU2aI A|~sU1|)=SCu/0 ­ ɳz9YYuz]מܝ D]-Y1H bZPlc-aBSn1]خohPGf3\ f"%լ&/;ĐS>$׸/ w?jݬElvqBR7}7 k 6V$wu?:d/ . Α`mz_n!ID!ݏZ**}8E5bor"T)e;6N%NRb;sG}٣{a 91]p#^uaŲ2j\L*0 6 "||Ϸ;nӁ1ի.wyꐱ:ׅs6M'_~ל|/S5lBs_3=MUi] _ x4f4=;Sp)u8S<,~N۞q[ůdEq_ 4u~lEk~Ě5ٴFe0LH6H.v{kYqO=bM]S1= a̖F=|kˏaC]+yG>h.J"AWMdF8Z<|TZLͱvW8`Cq1{t `?СC9xQp^K{bs | Zjd{)Eփ uC$W)S8;.ͶxP&\Db&GZNj'  |%!$U ̎mC^ {^q4'[n;i$n!%.6/?^`T M$$$71Yv{PQ&~܄ Jn-)$=eTi>_:!5~"yG/h@{EMۆ ̐ɈgqDf<7wtrË֖o7Vr'Dkа9qbGKMJԳyVQ[ȶ } SӃ*}E{Ic_]ג7aBb0kK;&BWUd Auͭb̗tC/zxġNӧ3)=SX}!DDeY;hsȄAZHC<b& af-L$IҜ9!pIcoUrgpjBK RAǨ>9Ď+>}CǾd!GǎkWRcgE/2x<o3# XOH3LHaa{sr6O-&FܳF&y8Bߚ(T>ulPMPI A^lHkGt:MHf 3i azXsTVs8!(VV9*O˵1L4B4jyH(R.ZӋ΀P? px1Oزv֝2z8@3ujEsǦSF'5\wی.\#y@*f3持s>PCk9wF7HjB S<'AإA49e6v a'?= 1aoIJ=H&)b2b+LoT#sy_xlx/'f7 i(rPR0:efN0GJ/GO:mSM3rt!?-/~;&T#9~ \x6<J@̝_ F۵6p4m+n0(8RK`//sɋafXI5~X?\xxN^uUg"&PcuM/V>m#FsNÎF-KYX4ȅBo1g򼓮4g.՛<[7K&jad=&F a$<0LZDm,8xKhSPh0yt:믿U&8hN)(8~wE%A9 -*ps-b:#w{>*+uBksBSXM2,ҭ$*Tj?a'MQqz Z|y+4s d0$=kh{d.b@+9@yϳV4X#H~ M }ezXOTHPk jAeeoذgRSFa߼qlE4q ޷!lEO?|;y?6%nYc*NUom$ x^E47H /d!jET^o*yT@`Mtjq=deS=gNAchwЇfwfBXY"3h;hP5L][Qׄegg?'ԆnB588!&+VH5감/UV( 1{;m#o2ƧN=vՈ!Eٞ=<LuܕJ oC UM,>E ^a] {Tilb2M+b͏`sw@ٛ+ܔ4Acd9F鬝u&7+-ϷȰH\ P2iӦc>}\l]EYwKiwŪ?OV0rZ?a;ΠjOJ}iOq =0 '󁇝M m (W"<.Z,4J'=F,ԠŌ)!IНXhGr3{G'" v:=pVf{84Քƪ@awC !b[/v۲Zlׯb"p5  \ wTGBcJBo r$5$c=L*r4 @-BiLQw4zCZׯ_r'5FovqUFGwIIeXQ ƿ 1b .hIk7Eqv^YbG w47) D'>Yz5/` i #&$";SܢU ޑVV-A̿GWOz{LXUEcD(44:Ji %^@:xǞq ϙusWϊ + ˓F_&IIazM\%2'y'^KznJV eDdkSC6[|W8dsԛ:-]rrr{Z^'&M4'`_I57u[H' c(3$d͟?ac1h ";C)4+w 3tzapA[ F<m;()bG#wFlBzN׏)BȣIrw~s2+7t=`R9hqyh,rFQ |DHwݛwXxꝵv<#A䢋.:vMA[ Tdml3mX3|/f-Rn\WCjx6'=6@D$+xeEcX;AЬ#o]=`=ж]u5S |)VrQ\r_qOA r†&ck yCxy'L;F=t>\x⭷tص.AAkߝB8oxMe3kDɪbJL;5')\ݻֲD^H#W DyIxu{ Bᢵ&p hc`3{_.i`BiC y?uT6g?kbhpAƛGֻۄ"])ɗ; &a8û3Ξ%E:͞vqP?Єd򏛂h{ﵫ5L|XRP X.'32S ƭZP5كQxL 48gӆj3*a{>PN*sʶ? |X3ĴPiԄO]uɉ 윃a^%ƾ?-}vq+PTs:,%-Y(rR-EZ9]3 yY1?13(3z=O:$6|O>a twfvCjbx̚ }C``7m? o8XbU0f>r q:7ⷅ W0\!Kpzr/f9cE eKCFD:=?6Oջj>=aL}MHſ]1&[Y?~CaPmʱ{j<ɓ>~[orҾ\G̙3'!0EvO@[uM7]ܝ1&YCof3̿#₂WT]6"?O_lGsI-Aw4ixϛƐ> I[D3_u>=mZ *њEL|SFGkrv囗̳S^fƛSE~LpmBOUU Mui(Ch̵b!!/ \elW/%Ҫ{D*8'AtF6ĵ@>_HeYFL,L[0&gOuP*g̘Ȑy+h\/TK/h$fh$0S<&uE'áf?sдi:M!$ԕӝaYe7< ; V WAp4;G'sź˜ݝ;a}7/n>:nTLL]ݖ_uʶGB8,d|gq?0 #ituߍ]uQ=0}чb#38)?W:iF5Md6rVWd rzc:{ɪC嫗?CK?dsMK`~8d &82}Fe >}|V05ɀOP'E:ij]O?I%GyR֥h"YL^y~ּbւD{x6}4vWKvҡ`$~zz/[yfܥ0 FP,ɩ.J{7`lp@%wBC yz$ul_W8v#&ϝ;n .mr_ ֮70.F=ׁPi ~6qk{`⻙禙g]9$y>ԚY&#c>V Sbq5Y5>)4t v Hҗ~&9'^79Ec  :VO>/$&Foށ0)VΧY ⭵7(W&\d٘6fw&u{eYq x/&d\Bj Vŏ_nuȉIu ujcJ0mOU¿)k7"KNH{xQC< U:K1\Bk2%)?ސf/9 t'P=#^{5J7!, ,UЃ,;ߩu6w]{whu C|j{i3o|?ߨ{v xE{-g#h8.zIC-K1B?aZt7裏މ[QO_/x^4m l_E; ڿ0AΌ;ѩ#Pm=HMﮢud^:;`}p-u\ uKF$ooF:ZG|>&=F#S;M@uHqu8W|M/ÞqNN;}Ƣ?ز3 ,YJߗLN7̳ Vu\ܒ|Uqz81=K2ǞyTa,<{c+8}Ͼގ5gz|)Y9_lUOmaIN^p*M1#w<yw<}kbMYܰWɬa؎kL̏BۀqTyQBq}[LGA[F9v6s,6c˲)bLpᲣ9|qph cنMb+$%$= @ZOރ,sF-ql\Piю8m% ?cu<ж#|q3rO7o;ꠋ`˅f-&/o.30".Lp%R_9*6j|Dć_ZLlP=eBmϏ%T{{?L>SRMRl-~^ yU auNz2A88aqLg`> \,ޝ:4hyρmg9؉я-X' R@#}>~;,g&6vA+~ذDvѤte.c.я[ofs`o>[sl٬$Ճ9Ko<==Sh3nA}鷣] p%#Ҋ`z9=4(sEQOZ6%Gq": i$ͩCqbҿYu-&)eeebeh!lʕnF湽Q` E\p DGEHwᇉ'GOſ->xLjY ?yM<z CT:-ZXc?8&رPBOYC~ /5z`0߁HgHK.Za2<퍘> xeêJT E7 w 5n!+jd\Q)R@LcEJFtLfd /.au >{5G|41[ z,d!< waMhzpT7O?SU頸M6h.G3+r4#_+ bι':7[~\ɶ"͇ddĽml2[]lgˁψ jE<eロ=}|pRĺ6ϑ@XW]^-xwԹꁳhv-LlV{IN&:FzHl HG@`&*@4#MKg2|fL~uDwJ"c/=#e]&ΠǕ~% ןQL^)iceqo"koGl4ٳgRޙ߃Jdtp0XD@mH/zhOF)IϝY'+.Uň-J <6]+CR\19 ?}Y/Ko_@1M й4sdGc5'ZD&4\f-ru[{ mM0Zk@~--- <̰W_}!W;&`[\s5}2[C='}wux `dpɊ$Po!.Fy;;4f c<>:.|KVӹ*#}3g>+ɾOT;O??:&ߎ`NO)$Õ]0+0h$iM^ToN@*aD @u: vC7B_[iaƧ>2XGubBa6)-K̺]U+z5Afl6!gv:G~*gOHՆ$ʢfC- # dHmU!""HۉAxRY@+3z/k 0&ȴDB̈%* N+X%M{̒@5ur^y`; M.g# K&ABÿcСCŋ/!;#aZrT@J},-v8eKt|lNWVh$أ5y yUW4r;k>G<uEjWtMH|oj|_Ekk߽[l3Fv%>2QhJX?oOtacqSB;ů`+VZGylаN-ؘTgN=1Pc}ώﴪ1]ix5u ޒ8LD@]L|0s[ThቁVSU1snEXUF, 1S<'aOhs_vٌɁgCZ^` K>!G(K4h q`]>4Ҭ͙=bEJJ-[$ 4x ]vAWR|-vI1,בVh`^w^kٮ`@3=hg|߀n-4"Ѽ_ ;@58gavSBHzp'544T(}mBUe.)DN{ m;%uJ"LN͊_K9vX9U,ot 2`|> bx; Ł8e;E\ 1In/v4Og&F o WXD&}=h)g]z<n*{~ͷN;F}UZZh-,H/*h:I<˜Y sB ]Sݻ &5em+I%nBNE1 ]͍kY܈*˰I\gܳ9y+a>oqZ1@!V wrAAcS]OʕB Y 5bgm=9UbsN k`7L̀df}nE3n¯9,]Ed, ͘(@,c3{+y94{7Zr$C;`w70@~+K^֗_~9HpL˕c]G5 IS"C0׷<}Ȋ +*5 ʂ`Ȏ "jTlXQ+fFQd p 1娛| cb=OݼxFQOˊ6IQіKbN Z~\G1> ߉_=SiCuXuȲ@ޯv>^9LHE[Y ҄&b k^m}(Pݪ˱R=WN %X԰"_hMjK>^k_|!>& ^Uc J`Cг#=u}S%$ЮJHmQ vہ'|+xj C_LC#>:_oGoeϞ=EMl%Kјړ3Y)aX~`B+c>#vHUS w"J6, JĬ|o^z\Phe+V'QÆې-KnVY SΠ9fBw= 5bܛW|͂ w#qn^pIV񞧖;V>9ܹlj3yCQV=յL0a/VWP?-mک mQ&M>*m*W%?|n݀9CK&y1LKZ&@*FOSFNS%]%!z6$CQ)iX⡨^g! dFSӉ:8bp.ߦvqll6 L4Lozvѭ ^C@1$&C03ʰ`Dx!t5 5zhO#|' Ƚ`Ä}Y2.i`~oPaV1Raov^2ᇹD`׿BO>XeI|ZZZr|t 㢒`Fd=C|h1Fq„NaccF)а`Cht Ke %3xTM'_q7=A?RxD d2G/:Sq<S*DQp}*v -NvD+!9FI/meO.XFh!)VqĬCr}Ryїr|kOﷺhbø\&LGHMCR+@S9]mH;*k=A5M 10Ӓ ؅btN2k11<ĵFKoq}?2rGJX4˧=f,d>}Y^z%7d A~<-oZ$@K=D$"JzI xO)#J1Ow7œ… D!ywo('<0"O%5xg|6QjkOs\RW6RAaO{"be.@5m\#"hhؔ؆hk@^0tȜr|[|^4#CaGy8?kylJ;S̾RWLׯfM"֛kAI R dăFYcx;C8f@'DEC`}e1:igͺE n(V"M= vWOoS* `M!ZPBaG38нT[,4n |%,pA 5 'ܠSk?Zo Wh? LWBu!3qw'}Wcw_x]Uq!5G!ҁwRx*!p7Q1k B N5Ew:5=/oz?UvPٻjB2bta;!OK@|Gq.u-B[!$'~|`fxT\$\g9)aOK@ /K"!dHht]$ 64?Bۿ[C_lb#7.=9Ҿ؀8{$rU-}H6&ޠf#޶-yq~?Ԁ!¯qm=Gf! ڽYW+¦ֈ;³s'K[TG5$-^~tY'? \mKb87 RdWhݼq\^Ĥ IqxCc w("^0@ ƱЎР` v<75̞wD -^aHP# ^;5>kG_( MKUef JSvPdi'gʐxg]txptJK%*{ =[dii醨v@?4@V{`[h "#vԩ<̈́ws3ΕҰ\/J.&|$&pa:tn,e5ga߼\[b=Nxg>`}tS^=P 58O37/<Ũa*U8Z{q8˗5Qz aU-B( ~1eV' Y& L)9`Hn1I *iPd͹TZ!P49Pn b}ʾP;CRBc_~uY&HFZpբz <`;3DB {Sh"I1푉q*&3Ria1%Fڵb~{Λj&J'-=4(-BpC1.DH[6ӕN%oXl5_%4۟u;t:Dgt5vZ;z¯\ox޼`XқJ<{2:gPuLh{oo;6Ő/d`ƘyDΓY+7/eڻ'\ʚO۹쵞ƚ>pqlw}EEElԩ?K,N<|@a0/#wZ6dߌԹMB47/[C_zN>d?@gH %P H-;<]2)~mL}8/rғb$F *(TM!z[Rҁeb2`i tjv/c&Em8`$uEG.&]>./ӦNy#*YKC ՈPخ,ٳu}X~~v5<g9>Ѐ @·pS:W7& PfObo?×SWf c`6,RC;}r!'c,!\d &):KUZ 9Ȝ泩j)ޔm .}[3 5롻ܼgD[K3~U1!Uy+< {>ļ <İ৷ŚGP?#ٸ1d Nb-zDDFzP7w<j, /o=_X|ΎjqD0έ`;jG0sPv?m5F3\qk-+6(W̼C)4wX޳+y 1fv79٪7n{کn椙lO"do,V97 WS^jGAOtHn=QC'cYl`\q '0v8}CQޒPCb`md4iRcDY B{ܿb!>}6lWQ~ &q\4x$޺Hb#N_}$^;xSC%;4yg-+X(߼>\YT֥MP D^tO? =/xI.x|1fd'6=J;=1{\A2K(Twl)Rͷ$p?+:fÐa.>auC #[S:]%Q"2˜xM)>'PV O h&тׅ;l_ wzwA~x^dIps/9_y„ 0RWz`a$G٩/D p5&qm|̒Ѯ5v c2̳ص4m̜9D AGxZc`:s_:o* k"W/袛w OU\eBf֑;ԭ]ܰ}w(?}{VW5;es >=>6i]6{V69URTZvC I&^ hnIz;=E~t$81$϶R庲Sϋ~Q{#L}Hv/X0{|gƤ ̓)B=wǀ@r_Г ̍Ca P &>.w_kK;R#4ܧFυ%H~p `91)ʮ4'rwq ;>%p+$u6͹µ%< ivntKchk6Y_lDx;vT||Ԃß8&~G/?P Jhp%Eܓp1mW=@%$B.֢U\v~'1G!H|_* Z ipFJ`fwzkR J#vK>s{"ps3>:T2Ѧ,;{'M>.ǘƒu\csFQBỷ)[( 5A!snG?A_\q wu 15㛀FY)B&&-~xԤt\-(G3*n=}>9Ri 3xܰl $ :>4&œ Hq F$Ѕ"co`2 j`5g͹C,ƿ J _!Y|۴q2w~AI H8EPa/DU r/%;]R#r7[uZlH)u˞*g5{f\;:sFk}SF?XQ\ʜ6D޲o> d\.2K*⟻]Od0/Zǧp1s32M. ַ_ko| tq]w;wFῗO999o'_n$xws{ }&8o\_ ,DP=&}R[H|~(bct$JڱQCJW>|~:(tifD7{$:D*)ILFr l~Ĭt[xUWLH\3_)op:fa :,]'ABV.-4ǐ7mjYEU{E^Cews(AK?(:Py1Z,쯇/1X6/ˑ*_O^.kFw+%8RR~ŷ2$=j h-f39=mFѝ(ω{ᇑϋ@ h79yGjO+ҵ6o `Eݪ! smRTJ SQ{O 4KJN{>}};QEH\&OУ@D+8|9ʮ.ɊC=JsGu ȇ\<`!?UeE!ܓC0`d}2|ʒ.~yRȲI 2p^)&U9^XӴ6D,aIŘ&@la夘xm-H94Dta? Х=PN9?BbT.$+Ņ4ĺI>VY]{̉Q.)cȾJ8_hEj/Fc0v؍H^y;M;2iw Nz`.+]-}%yȽQn0BVwprUewfk6B¦I(BC"RUb* ^D쯂 ""HEzM!Bz;sߙ9wl6ds{9{yyMsx.oXۖ[6m$UzĻqmw aǟ_#j-U7c#>O<~W^3wR~wZCy)ijڐ!W;# 5$S:T>e/:uBv]54% K`"RyNR\ f́^xv8Fٹ?876#߈NNniX/WmB-`$v$7düʶa ]Q{ i{܁Dv(Snhv;P{<^E/mޚ[e/DM͞=;RxHx|G\2D-mG~ܼ֌52?)@lgkU++Az;ka#[$bURRj(K].f^YĠÔ'E0`5'_7߳ak˂LcJYj ?–҂yaej c=,nǣ2:.rjbB֝]}Hvo@8u3< ^fWs3s_H߅}Z_P>gnewh9ftε5HJrvrKm6HTGjTUezi%W!k3[&ex1|ؒ%iۢaԐc|z%v%(qP:9$x˽~cZOmdǡ2զ~{ERt;a7 Lf&{MDLfi"ߨI( kݾvnޓ52̏nݞuFe/tqҖ9Ҷݛb,`Dp]6KH|O?%D WE2F#ACfGv_)$8lU >.3\" p]w 9g\R iȖ,YM:okn#G:Q9fQ-mVz|/|/(>/{pi;\N=5ɣ>N:w=6 ;EJtBw}C;M.;*[r쓦֚#HIw^۷]CVǽG|JoX7!UA I!M6ڦ&@yaWW 2i9y%7*}%IVƞPM\wx(D*c&}ۃOߗMpe'޲N- Zjǝhle-7s}qFg70vn>^Y6|򾶖Mo6nH] @ ?ClX;O~;Wم=7=ߒyNX2-Qi_X+}!{ȝl%f1vz5-[VviQ0"L%{Y;͍撯|[fZ{"6.ݚ'1+ϱWp<&/`85~=-\Oqؐ~d~Ŀ {vзCD^⥋zI돘S6sM_腟1|&,"/tО=3js8F.#j(P ?i[#x͹Ry֙[?? x,C̔{0S@p;j_@6} WmܼO0|z-/a:[} S8ЗIϑh{,1|Lui{/gP]=wg{QWj:ں.k2]m-|3dϙCMMrBV|*ݘQ v臑[KBG%GIBm63ݎ{PnhVn٢j%)nB{?6q.㘤vWV)cV MTlqSVe&w B'J_ϛLʅ)wIx#'EGMrm f/ki$&Ǭ̐^:;[Zd[kٮepq}'Dfղ~Fw2W띊Z aϋ3- D@/ttՍ`&3{[=n .ڑx5OOV**?%뮔Y@<1=0R\% @^ObQZ}w7I$ru]w!1?˺ à;oO=ooLD*b'|\ R2޼W׹׾$@{u%Ƈ( b4';Praʖ*?Uv$[;y1 JO٭!tRoPD-q6z\S4<ѩA6q`-CD#^qsO֦)&rԽnG Zo?!c+AM$I+Cw1)wHڬHU ;4vnk_x@H{߫A~͘DIRoڰԌ^ggnH'MG"=th@PC@ۑsxd?Ao~i_mX#u>ut]m%_; ٘YEǼcњ18['!dwpiW^yDd47pC(%ZSDr) $wDhW$ (\z LG2t_) (B=-7HwLJ -[hv.J\R D>cn=wKchp*F*`GĢmέ7oI͓=qJm< #п hLF:ы4o') ji#$F QےE#Rۗ%l3tbZXeWH6d>=m;qS*( . i=Z[c6# L) 0HHeЬM͈݂>n{L{~Ʈsۖ3[7<=`l]i^ض!=*wy%@Im+e,:),q-VKrzG  3ѷKz?Ş_bthL8^ _P|E8˧`>tɃ>KႥΤ Z_.z{# s0hKDݰ&y+ޘ/1<f yFbdWb|lk:udJƤ,^A5@cqrNr`#>su|/ כHg1IyҊG0PG5;a^B >fk*N 7Yg CB7Eoo=xDj/>.* n={~誔ӽCBr^aIW:ۺoZ-)CzP[C b #UCC<rLƪ%Uj 3z{+k_βe ޵M?4"BBRKrRFH1h+e;2|.DV`g/F@>u U5#Yuet 7$b!mm]2GTQ~egLKQՃ'Ӏ~pL'6f Z^9j7bQՍC_{,ZVK;"wuh}rE_xvi6'Ta0Pij-^ؓbYؔwxA!oUe )jqIN@D/rVRhlJ|^A*^{o 45 gqVB&\;_q#m7o8w#b C!N |r(gsC=5܏ڝdh:0 h ;aIJRC 'swo[TQ q˳^IRzlX;>hqC8wYI be$;ˇGƙ;lkB60{5kND4 HɱoZx@3k{u,J ⻵aYm! hэ["{f-oMQcVk]BBıC-a94Aړt3RR5A0ۚ2usMHU-\2 䊵O}k7x?ƧYD"By׊xrk^KiaԎtB '3~;>X"ۄTѪAu/&Apfja_ Aܛ6vxѪZzҖ, qkm4oolؚ SWWc v>7˕WLdG?ͯnjn{"㩠9bRJMo١DbyiI|L.n'RR 0@Y QobKznbQ$^nkŃe1tOr6գl(O:`q"C[i\ 2+sX*!HxP ƯQej/ f}Spٛ:`"jIgE'J\gZ ƭj$ycnUStڦ}"[՗mol^^5(aϥh *Av7 MEG~t*2~ QT,m2,KbO'|;IjDeU г/# f nݹ z_y4-œ[?ƍO_&ۄ!A@qe ƫɵ L.t` ղ+_{!#/oi:8߁1SFX{anokgw֬Yohn[}b˻lx˖lc茅Kpu!yE1|G!IRRy1qs{ն ҍ >1麺:vB WsWE 2ڸAm ˴D!7ϽL{4,bxbwޯl|j^#ϚuwW+AL"K̇0p" a8GF٠,uӟ s9ozduM'Kv8ԫOnǛL{vo D~Ccٴ_1:m!"Jy[uq8HDA]%(n-+%]QԱ5"\RC[@]&.|Lw$}+xw/~4-=yy_y۶- ;("ʝꤔ.LiZo$CW!BP(&a1k'όHPSGP6JL r=.FC$5zЗ4RG;lJFPJoEw*O<0~3gS;=6y'B dGdDL%sb @6OSr#Ȧ _X! "Xʱ?q@Sۀ=u:A}Dz3lo1zqk_m_w07]];%'V_+/ 7UG3g{Rd$א4Ad֮\&!~|%vH%d1XYlr$PKKǖ>s@rGAnkB7n%VC_yY[lHy$b~6|vy &DފEЇvI֮.DB!R|]JTQ)VN-Uuă![wʍO?a>u#~j&mmkj4m6zKi id% JW!˧]}:T;w/U@sQQR!t?LB!^ـz۱CXjaoi\7j߸yWaMmAei%ac(#s,4:hb[^XHCG;vJGL$aOBfˮ+MJFd? w50h#;ˎ5:žw%99SleN X/=zbYϭ;dOD@xYh,=k[YR-%KJY[oadzZT s=t wA d3p(mnSd_7~/$ɾsG1K 7yo%֦#=&'D:;f#&ʛM;ffѪMʦ9yek1h@wZqӁf&I@Ue 4ܬtT lA $!fBb'-b(;$c;;4P,CV`gb M*^ak!$-1 Tس߸ }E{6zIB) f׳֚=5j:w@rNu.ƥ?9Nxנw.]{@>H=^;f n G^,cok5eb޸iak5a-x W*D?~MzM7yMMMa,vJc%rU0_ g%oڴћb]HՋ/Cû2M.%x!9}cg}gJg;Jl)F#1hw\}}w>P~q4n$1S 0X> zzB2U%-Njq4x3~Qi -h} n- ^ wi^셮TROԦqmLb !3鹾 #x~ڔ)6D$yӭeۭY|+]]_R:fa~> F;ޓG})Bu$BB˽ .|5Sۓf^> 6E?`~ ]j[*h;~ f΋+&Nؾ!/X;{k|ڴWdb!ث4slSSiͤ. v n/Ibڡ#`4rS}gspWs,Sr}k1DI"e"Xv]Uǹ/<%iԎ) ӀG-)wF#<һkmٰcF`L&˘)@M9uva"Ht0՜O\IBZFę,r1x`JJylR!ߞ(K+ 6I4ȮPL]sChxhZp=G?+y7;1Hպhn5? -"vd/&$'{2/c;ֻ-:>_11x' 6uuu)MHʟFAa^ )g}ƻ۸-'ShAB S~㾐KzM:>$838X?F+W2r;p6?emOۿ9V3s1m o$jz=5=b\_o<(! ;7 ?䰩I.dDHݵ◼u6"hU$*(J9°&|gk~#*-"a)K28 ˉzFAR|<<̏l]2ɝɋ?}?q܏}TLT$Fv$і[f /.7)^ae"1"R0"x[S^=0P[ G?QEPxS9lWzuFZ B_Fط`2)sPۭ`jwy6>(#8f#!\sjw;'W^lob@w]w`d?%6eސ%PjOeٳnp1u TxB[\Z;ٍOڊP>˳?(dR<҇Sȥ_l` r jUpzݎ:JY\5f)hJhߛ3}0admo7'HdV?w˥g'0id7 T  !I,1v͛7v~~{! e $M'Vf|C;<8ξAQXh_M'ZyKI`EooWj ZޫSLaxV.  (|gŕ$at?P' }_|=B& =#JN9U 0~E.~c=7J眥FnQR Gn _^!Lp7DSPMrvطεy^a٧b(zC۳za|5[#Kk>]\=t!{;v6>r)U.ZaD[>%eD(|07͡b#'W|rwxmq XNjS{xHAW&Rw B=ʲf?Y=$bFVLdE#Gs!:E@XE"**LmtLh&l1d"FyYT'VQpT#"VDVqV޴! {K%3 # \WSc"rjǦl`Rzw*{7%+߷B&uTE AmلԎ aʰ լ1nNqx2 Q~J9BgQ|ѻϟ'&;L>XSAomh DAg1i|D8$ }4LFtGo_TKѹ Ԋk@^@M5uX薗`̔ fL6m}L/M$\?!O1C/c>so߾ 7T0LVLB o/$}KQك2Њ@%p"hu nmՎ)0 ɗҶBl{YRzK,u8/;guMGmS } uދ(Ov`b^1T%);MFWojL:h.~"PB 9N*`s$$*NeDsg12^&TܮX`Jږy=cWeYt-¡sżE;餓Rw炚{w[mgswt-`xo:SaG51_4,WÅ:(V>GJ8N)$>8 ï 5=!,@2<ʀ(Zŗ1^k7^]]rjj*n1hC +͡l*$cm.@W{ɷ4m5\}cIJp?KQ6ߜ7loP(XsAC jCoCC٪ |dTr`lu}F}6j,gI=e#Fnj{7;,h Y;u9fmJ& Hr7K5 sжl[vY k-w1i|A-cJE,k.U霆37RJ*~#>?6!ӛt񶺈a7!t޵W3O{OJd KD6+Y]0x?ywD.M 4e U@}imo,INnj '.5. :kC>WItmpPsN-N2[lY`}4p Kb5K,7=̱2u06ъ"h|l 2ݵy},?f]˘^j?9}CvKKvQ!Rҝg-ړ B//}8g6Տ'PGE`սˠe1i˃'{a+\Hx{LY~ w]A=/+K/Ǝ{PJ|gCg@$k8~͒oj믮9D$E\YK_AsDo-/V$z iBbHD~y t3wBHTU;.gIi>{ۂڰ~tߟ9pN;2oi!Yb>rENR .Q *qCQR=jba [V}6{<#z1rƊFۀID)qWp DĕO9_ "@׳Eh=,Ə#=!Kq k1΁Dj^I[W(̫V>=)ﻟ K| PZe{Euh&-옚| 1?W cT B׎$nZ-L5db'jFz7߉x@'%$Cs[֎텝okGnm_[_|dM}dA#VI=,`y̩6tWEno[_kdnOK*a3b颟*7[N!Pyp[$1ފMMuD2#\rIз#<}("ZekB f-Zd@8e}l"$PAo33*$J*؟3׳u\]C;6CjN o hԍx|3L֨ W-m6cwFIKNjUE~ӑ^ы#0 V^3[:FtV \ZbmYskyyf@+}jԂ"?b#AP~@_y{muHk7Mm1oM አ޾&o;%kxPG.M㥹 Gosʉ'! &,Kr(V" @Yh~c[=|L$ּSܬY[Un\6 ]SpZ6-P` NRr}%N 1eC;OB ЗVŎYsU,A^:]*RNM#Xދb!̅Q骭.*n1= P]5r.څER$b7nL"49kϔf|0>H-{3[<_sD {hJ3<# >Pp&M`kX  a/KP =Mm5Da{eEt Um=vYan_>h8ԌFpko(5ue:Ӻe=xƭM\Ú~|g7Ɓb/BEN`PYy򒕑ח> ~֌ lD3Y#'{#rr.׽7XB774{Θk(o1q_.u|OsCuWXS#7ٝE9D*RQ!cQ_ p w!D]Uҋ|E%07&Kj "ײe#d@%qF"1EW7f5tePnZI~kоٯ KcQ)O#$BzzI?׆[Qr3Z DԵO1WDHó[B<ԯq{$:/$ۜZ#rq0f= O1ǽQB[!Rʢ%b.{@>߬}:)kKݮ?qIBR<ԦZ UzL=> k߽!YOY*zR]BNJg<~P=%٘\,qWz~0g|#>^K}=4FtO n-hf%uGUК|fDTY4_s5_j|3ϗDGyv$G C,VTn\7;d>ciR]dxƖ.]W3>򰳀'lbdW6'z#5K/f= e;<ޘL!1vE:>›8rQތGnv۟c|"$`鮙_IZE¬űF$$ > aD\I/esw 0[n\n_"~G*z8 ׾N[p"d;j_?k[vլWbR,帕*0w c+xY240ùCM4_y;$e>u/ Hn !0J~՟oWpdj]ޚ܅, KK ?(G`1Hjͅh#Aq<>"O:E; \2y9ש!q?mէTeqЩqO63UZH:?aD}9{t?lmpbv)؊q2CP]剐j/A5'ءZ:90$qXG]x!aw"|}HֲaqӘ*  /x`7B>$/CD]`n1.=*}P/9g%nRFR)[6.˜ D6.U6}3w+_ML>b֬zȥ|o&V9yOH0jO -PR܈nX~rNӭ{CusuBm 3Rg~{-#h "ІzWAH B+ wqTd*ev @>j.]fd?|-awW.~{5;t4RZ-އ3u%&D=6W+6W0ĥ.|طB~Z`).Һ/ĹS;Bs%{oS:Iabݼ𧓴 eڲ)ucq%wL6T3~#-݄{J:2enE„M̄dpڗB8\ 'Luw-fvWE̡46(2,YČ%rZ")~AwА`!g8 =/]עIw4U0?ϯJ; ozL<qoK/^?n4N\O;S^x<$wl{э7- ^|AqȝeXIrq8b$>ڋX )iيlD}Q>~$cT,w`Ԯbh=]1wפoY3J˜1 AE+ܖ< }\Wj1I0sjƞFP` UyaWRfow[d>Zwzt^31o]'-රz5gz~hCXw.c-"6.n::;qFp O7WwO0qZ4vwMI'Z]=\o;\I)h`q0qi<"(D @T&OYE"q~EfR7/1"nf/ͽW~F`V ;ՖE+H}٧ h^8NH{?HN~9סuF \G5Rus7Q8`쒚B_tpD;ܵ"=HrwǴPG]+ j{f+[R3;}ARQ+ 0yحJF wG>ۚuYN9xDs4L ڈJ?O>\,d_yטЖd:ɤq콆w}2}N[n]J/) *R롁ݓ+&i^k[LS_, ,@e uLȻ[G1Bf%7.I^Ɔ%ᘳ @)2YA*zJ}‡D~y 1,q@/bZ)KK1r]Kjru!ah1T %/\ǜt7{"{Iyz*K@* ;6Θ"Om᱉9@f QXEWLGdjDR``md/ŠRHGZѯz랤 EN?p3wqw(zq6oH\)ѿy$rO\ 8=)p),7X׸iкDӳ*lqq_y;!8KKV: o]8+qWF=R8PA>te{/Kso s_kܕH(aA].#T*+$)I,P_jKkKMNR{UvK@(k+fGl"jrBej1Bʋs&tN !Ϲr E&rdRBTq[1hc+L.5s]^Fra땹元P:P12o|{ט"M:],vq~,Nĝ ;G"a;Cb6ZUAs_:ˏWʟ8g'"0!B:^!Rˇ;nx"|q$wwԸr7|IRO@b*`n,BO޾+P"e*NG׃ އN 'W:14%T@MՎHwdS97"BJhUHF1- Tw(1r(%o/ L h_ ij w $|'?jnM(Iկ>.eQq LeE<|;W4B~"2eAC*;lDcHiIXd~G{:b^X).sn'0I -L IPo&|DF ҾrB% 4:e;}hJT=c3E$S1CRq5L1zU:$񂞃V\ mlnyk牏||/^ /"rDdUQqҾd;Uo}w R^wbwZHK1P@H JL8m#B 4"DqᙿL<ћ9gvLH"0͝n9[.eH/| "{yg)=޻߅w2Eq w_ B./5ba3bJ!ʝRZBךȎ"<ѷ ]p9wgEߧXw*O`nuiƻ㠒RpUW,[Sz܇UY` AT.^d^BG\2Srd)AR@`,[l<(DN2<|Y@1 N܂:AWVV@86T,G&4R}rq1WڙD0c il4;[=yƼ2/P* `'IgnkG+ ڷ2eԮ\< nOno\mF|o=7{mz*﫯H m +ҥݳ`UZ[wW2ܔ{ygOTZǎٵ/ށg ?o=/l"ӦO;8>ҝnDj^<"S(ɽOw1xʥN_ݤ$JNbC%uYED]T4.".a3|<߅nCRJzׅt=zFS0Ȟ|3YtH*Paz5j$j)aP,w svc!h)S$ =Ԥ)2Q /w'RR3EǺz6:_Bq۶l:ͱ)/=|i(VS_+h1t8qӱӜGuznOo8|=^{ΉzjeevC%bӇ~v3zt>i**Cbgl_n-VڢV\gO zk+i]/-3YzLPBgx3Oo1[?"(UD SOg02{X=!q+>+8kd I;/ m[Y…DֽFY`IK약OE*n\r'fvVd։0B÷>H:U!7۱ ]>@IDAT_ڳ9˧ ip1V|.Kw3U'L!g d'Ew SlV+ ZE"\ʷYo#G.NA/\ [͉wo*(h؟TI$=;"y>*^MfU "-x5lOP%c D(QNcaJZe\9$dD(yB_>%#ǿp"R$dvZET Iw [l,L}ӧHR$wuyo+,E]$^.VIwL#oh3XK=G ϛTzY[r?~<KjUPKڰA@͇06iH ΧG(k%KjfK)I ?;|~dz)(aC'M)|Y#-[L_S?<{۔;f~+#SށB15z(G;ywz 5l0̯Muq.c]>L}XrJJ% L6Laj8sd:>Oy;rwNJr ~pZC70R|wٞT~P^`sVE.?\d]{{S7|m7nf%6tL>IKC!I]M3]W6n73fDc:Dٌ"e).;%ז<䢛| |V6L,z*3mny 4`1Zv)y#ŷykzP|ߛ9P~۸)GQf#=iF|L/ڔ9D9>`ATZe>a.I`h 1'q,CŴ?齕Bf҉@x!BʃYwerJ/of{aQ7/5ar,B>\xd?3]##Ǝ("z0-~Ss?kYd#>NĹvX U gXs9SAIDS)!&MAi Bh.TѣF[\ reG)aƠL.o@ VLsVd{ְws)vf'T_N'< %9ó4h)lTAu#/1+:g+WUVv=B,P\//i E%Gau/-WO̕VQmi*2&P&B^s8`Wp‚P˿ q?pu/ijY'!QK/f^$"J>&"?u5?nhb$7 -^>b CH~UoXd;HP }"19$f^|E?/I9Z* U ŭacvyg]jt#[ooB{^3-#IH蝕!`#b_W@;ɲe}GJ:i{T SS-s!bFϼ6W Xg~ѾGvX9KlYMqA%cWG-;1>a {bG_BȮb 6NAॎG}w?`%f =|mE%޽Mݵ(0GM4hMjpƂ>Z8!Y=n~co5'yG\Ы۽s}\߰ >&j`\0X?v<0lN#7â`_RB1KE8e쫄]9BR$mi/dzȾböQТ,"FR4KE444Z LPKT>Qb\*磌 8*Qm% !u}o| )VX8#} g& ˇRH\#3\#e!>=7Iy? 1 4TϣYlǾ3>oyCBkPY@3ϊ{󭨐Z%S/aÕ[v_-bm6|nl֟_Fҁ,l-6ob3,\6 "Bn0\G@mGD%J*iC6b[`)}EZ$6'#tZ0;" 2t&Mz\-⎪\0H<!Qmi¨iqeI^v*o@H1Ii1EX`ҹLctUWإ69.t^Z iM:+lΝBӧ/T&d w 9< kTZϘun\S'i2=ȭ.:"dA/>MA~P r +Hryqǟ$e ȶ0?@Gg)IwW]>S\v1xcssA'gcx#pȓ5&{IZ>߮knEEaB&;&9\wmr4mB\{[aqHw\HI#!Cgt&rDA^Da2@-[b+dow }Bo΅ RT[ IJ} Z5I%Ekq#$ p?ȍ= 5:e厹ƛ$6ISӽh^otG6(+RMwo[nEl)qJ0̵\qtN;.;=fQ S+܊p[='wF$/Hv&ߛe%T/"įgR_CE>\!}xĘta< "?DV4"_ {'՜VKE-mjdμA/UGfdߕb{Hv><Ōinhq2W{Zvd'LNhJ{J-B@EP,Y[y' _V"%a C;ofU%ܧ#9OO-{EM'*|xU}:SUhjq [61߳{lʊ\9+TޕT{z?׏Uqﱞ>%;c6ZbnކLַ; bcDg&}E@a\9k3xj_F_Hw&uw ['ݜZ|Ҥc2zjzI9!lyۋ*ɫ'ysY_(Xzᓒ%yi1ÖvĨ,%s[h՗2_BWO; {K)WtuBokcrh˭RD?ن^P!#Hf 8wh^A6/iyhLn1Nˏ|]՚~Ϲ~8B~-%Qgy`U]O9cމ>Q}y.h=*쌪kj L2u(uIQ]rZrI t*;oጧ39gB0b)9`A$;oq,N=ntŮ~a^4sĐQ=Hc 7p eLq0;ffZ\l;)L|Lܿ툗N! |"#A/g> \d W_t4Bwn#cb7r4a#rz L 1ֱӲۤ0'>uCލq،9Hⰵ 5U3&iS$o2DY|`q"qO5Ӈ ktXq>CZs_xih6*|Hd$,ѧj~%HtX*Ab#FU "%n|g|s{NnQjWUYF#s.tL_4d'0`$_ ZsNwB: LE?  DUBR$n)?(a滍<'.^c-]\>)3sU]_G]B& Mó"41Y7C_ny wܙ\vM5q9c֑-aʳLqz~+#M젺#X }b =zƳ|BD,$S< 4 SHsAn@R|=&m-13:j:.\rsg1 @?]05lZ$0~L!Mݎˬ{"43O7mf~IF$#7nIї^@ܮ7%GKq7 $xmR[/U97Q]ZZԼEU|[yplyV$3 D޼۾ dLK l e]?.sۃ58p >+ֿSy{ 33֭JjDe3W$9g#aHE٨q#qkc¶`sY\ m4XMn2*4ԡ>RzfpZ`"w(wxu|n1uFJc0{{GQ=0͜-=k9%Ʈ'LDܵ5qa>Hyn{榎~!ˍm*hWD~` YeBڭ nJ(ڧ(HK^2/pߡ 7bUzu-DPzT'=q) liv$D~M౓bRx Gi>E) Irv–O}Xuį:XSMIE앗/~YgИh_E@h$,n"spl"YZz֯f-g]S6G;OO!$Dc0%=I1upA3zB=1 ~MyfCv$HDDtr$$Ț(c>m]R ^G"Gc-`%<څҩ? g[~!iO<_<%A B9u-/[V!x]4DS:όP޿lϗy7n[.UaK36wrG|5cW".hLXXa"BMa_ ;Nz yj^+sBg%Y~)ǣZ<sEA!׫VXlZ$C#.!)'wnaS&.Ra93fceR|/ƽ=NG0aB )TsDrmM-)RG&[o% fЗ9en]Po1sfL9BE ]s@Ƙ'߷8˄i׭ U:4 {]hKl' PTi/ npeqӧjwJIPξsn6_ܬ;|<qI4  Y7%pʻrWEq7%E'@uۤ]#*{MEbuܮʁ:0t 8Ag}ln5Zg]D؏x[Aue-mp89xzIDiln+z[bV5Rdmlc՛[45~{-bD$n" !0DŽ:eh)|#ב ܚ(O EM*}3G?nH۲Sζ)n;oj+>a|:Y7C&GSg#ڧmU9Z L&Y9AYa6t:㳥EqukCcVz 'M95y$Ꮖ8x8pP6~>]n:N\ŽG^x`ٓ?ip甲 tW"q]Ӌ/Be$֛YϭeΝG.ٰfTn%RccIEI6mZoL;j7ΓI)st2@iz]Һn tsϊcjAC?LNvk=&*؄+T lMJH/Y,*+f;Ajj&|ƜGY`E܊'J r냲6cc=t44f̘l`ECG(TW0B7j~j"1zi5o^mS\69FJ[&lҏ؆m'j9汮V䇛lT1}ˉq j| ܫL#]t p`iuS^=XZK'Y$=z-ֱIc"^KG>ebԿzߚZznzhͷֳu,\W}up'|RbŝMnYo놅lra6>&TFBNڳ\EPQ tK74$ \r>atْ8, c;f*J0~GiڈZzM)"m9o3'9ml:|Mi)HIBZ%!~{66WtY1;Rɯ<!v(H&W<Բ}gϞ=(nUcDwt{kx!?bS=,4X5t;6.4t"\(v7<[|[Yknܮ_0_=AXgI\2ѦkNn:%}}} %3MCMn~aAc%"9GM(gI{\op\2j5Q K{JH8i{@>!?&k|Ѣz?~"Q)tFü#,q2-*l%3Pyޘ0-lf"Ar-@cmM/7J-mIK|+.Ixj„ \wҍZY5p 79v]~gpaeTx x,4 Y/'3f-Yj)"m㎲U P$ +u?L]àr% 6Yg]1ύzJC`-9E99c :}+aeR BO> Лbg #I杳dzq̈́#5IazUT;7/+HNTWTz2tKY2 Yg>\zQ0MN;PzR]H( o.|RWC,շ02,O6煋:7RA8(:OֲfG9]Dhuk'S8q)US/K]|ڟ 7 uc\0G!T2 ]ּ|G6p&FnQFtWJ6D0(H67A릌1)ޙР6ncQ7 5|Av%^CfX}% t *)?Dj,0 CX ,f=q;Ȓq4D&$-`[%1s?~H$FtPt&;Ԗ{niJuQ*x_:5o嘳a"|I#=gX ӕ,f"ܭʇQG $WL|H=)n A]i4h5MM*C 6یU!ig@]8=#(i Gˍ sK0 a% p\4tPO4@~QnV_?q 1bsHYk/{3uOa6:B)$jS&F7wg;h"[9C(q`",o5f ;eXp]PLDҪ|t .q`Y`CF'r1~8ċ:DZƆ攝;y5am_1q\f|vfǒZ~DSO;U Ƿ'~w0 Y>H0ݬ@S8Hnh@I"L}Ɂ6lY"gE7^ )U~{~c5A]fX[͚=Knp t)3} NVUw>d3~C  u5$MXxb %\oryLt!8ӵ_5 ʫ3kesu1y8F12Pq̏xdcLl,h޴"7. %J u'kvԅ3w LB_a:QNLVZ+J0 zV\{B…郦TJHdNbg+T =jQo"%@m7&61/ڒR WrbS9 |򴴈Aٌ)ʑbwl"$mcUiO P[6$58!jLS߯b$tZkC.X9$Ym֙鎯@ӝ9Ƿ+^ć7=$_LCLב$bCTl\w;R n5xPڨk(;XU:mp~8X,Aeʶ2f&ԥ~HĉN:栁OvqQ!nZ@(i@7q/sQ_/8c8I"ǖl$Albs4z[iڵ.%A.4]K׾MS{țy4&v۴"sĢM>Y|?i\DM$(Wb| 1$TE< zՙG(nc/\ E uV[۞~ngV ">] ~^M&X(1=+Q Ѡr޿&\;Wƚg-/ky빵M9X%8$.NBe-oέmS wa/+aS]T:8X=LYesWSnzb9yS+.pȼM/l"_بÇk3# Gegzpqٳi'l s$,1 huY$ -)yN,CTy4EWtIgx /tg. w8T;-U'||ΓLm|bmk|_y<{B{ <1ffvKcՒF q:0XpyZt_LL}d3WIq/ռ1FVAw]7Q-A%24>ۧݍk9g^;9InϑmH$$0qN?G~rTyhmXy{$@:`IQF<n _%Uii Oc^B͔ BXcz\zrsӎۑx?"3ԍo@ h&3pم^]׷ـ6/9_Iߊo~PE_Q(xͦ3 ϳ;0VTp[ҢtHb$Ft' x)m0wpI͜v̑6 \DYT sŴ#d?3!e!׈뜙1;#Zh6f#ڴNf`U5-fߐθ >Yq?ย 6vI/E 6(aV9ky`S$X̵׺/J_N`-Z/;VPUvzlzh??QJEnn{/tеG򵕐˿ uS)9I&~FlY_%_w>[ZmSONУNe /=+M@WNJ ۆN;_yY9pO| n ّG)a=)x t7-rfF h;U,n͗bSGb˦.T|X# 3}B*~|UZU}~,JE9j2$}ȹm;uQ6ξ6˯Q_r*/+N"b&C^A_!!cSqQE7QQ]W;ob{_L(6Hi7e;/~ |::ܟ ׭[[~LEDԪ] ћaA 3K1Z5(^?D̫,S'Hhm̙QտCnT{Čw>DxXuMrd9z!PĐD(89^y&y[=.N4|dlWuX;ɃR=yͲQ"-{6_ܰ{iGs &yX7)m~_i`՛|Ao8wzrH7\R_¢`-:yt!MQǟV8s|Zе2J"~%nԥ"LUDYnЦxoI>h4w89f 5Hx]VD?F FT_ QYG**ǿ/~s;c o\8ڵ /w_,f^*>`xĉg Uv]6wcd+EI,oFҦ˲2H6W Tz#E-_E:QYDV;ߏ/7OnTX6wïbμ5ԕzl8 DtPM?@ԙ$nP@'utJ6IDy (Q'_)oַ([\|ym￷ ڪsl1~q4&gbc&Aje &[oeڂ@6"3"31λh[[u&= &7w䈉O鋵9 -`ls+B3Дƀd4#L;5 'aU4` [>a#`v\1GZ sS9zuuw#X`agDNV@+ApZl|D _lM:`'8LU8$"\hn:% $B %>5DOv-)iu8H._ZvoXz1?@?Q=buf9ä҇_1 0dؤ@IDATcʓN?1"Lc厗^,U׫K60~ab+6%-c^MKg|T3sR[EeHG e3>o__#8ׯ %:V}ZHjr$: ZE0ԭ{7ѶM[3#bXQ(L?з=/:F~^bA-{mO?m0n\|+$27˗~fR__*[\)ffÇLu)X/E>mj+ŬUݳF?O7nGY 3nSe/;̱@R59}Nٛ[(:wfDty֒3Ja/.*靋Կ/5juo.]V .ꋺڣKqھѦvX `8[ZV1r+.t7{c  Awmu쩪 AN:4`߆**m A+=oI6ݮ.9gĔGGXmVlKD;(;A%;%rcCꫯVlBuVvQodzj.xvqxc-X(W3?ruAP3K ~Ki'i+ζߕ HesE8p&;y@g A@(m澃=J?Q[ %JAqC|w*(q[D >DUG1|4wƍe\vN?m1_ %ﷳxg}ӏ+z [m۶TkV' C.贽[9%fjK=)#.|%{OyrޫjX03PO#g[n@Pn e]\JU7X8:ϙKoE:J[ꮊb էFlꮻ_"^}TޤIϼb|wx3X/9٦M3ڸxw`˩нoG(O<@k}".A5kva3P}"cOzvGQ{__zhGůn%D [~ڱ.p0(+%p%Tq=|Ukgv_&FmC)k+xLL9%3ڲvX=}zf;V8M|xծoN}N5o%[}DI_}_w˃ s%]nZ1z.Y.=~zcWؤMxSvhx Ϙŋ K,HHIhM]{ ΁X Gv kRx[N07[ o OՖ˞ɳD!ZgUs 3ik!B.`&Dž ~Λy߷M^{a_g$/|CWZ)(.SN>XLx\=O+CG%fL^8q٥NjN1Jkji4՚oJE ӾI#k*u5j(HA~RnibٷSD&ĝ  %g<w~UJT|d=%4$ڼwjeibw jl]NZߵ8;p=bҶ,II` M$WnQOulJYt`56Dl<2ɠs?$-Ϥu)F-lU^zv? r¸q޺tr kTTt\ܔ#0eϟ951?/ oKxg6mZ/*n ~PR\dD ܱhQ b9Rlͫ_1 S]uNR V/8L:ub_*^\9Ɲ*}ۛ!a;;3pL:JEl%JmBXdg]V͘)J{#5٬G75i'QajC )t 0%"' c"!q#X5D9]LCwW(k:/W{KC^%NɆTݖG[LepH[L䟔l\#7`'wnRƜ<>~ŔwM6E~}oCv !-+.S_XQCC|ΜE)TVY\x=W>L+h}cRVK -T!@| Oc2tdӒbˊrC^ n}>L']ҰX4q1a wHy/XmeJEMMu[4& [*ZL #]/0XiRI2< U '!r:88U'T73ibF#`b HQ7ǔiZ~J<+̳δ}?C |/N`9ώP3g2Ȑ!#u5[PJg3}^ D۶D Flp, ߴi3Ɣ4:W؋ wgnj?vqݰx^hUѪ^rMj̘my,asqR\ؽ?flll@Չb/K 9bqVkvi/ׂW ?I|> o 808ŀ4gK dI]=ַVs4^z2X~M9+Х^s GiD箕rtF$S`{0pG'IDQP%|H5Z1C So*Uz$g yk@:T cy4T oi$Qu$͛7VZ@ogkѢYb3@굪"NWJx\5\w%ѯջ]2OރWo9`dq>SEHϼY-]JAD%/*jqePEE{ JK}9x ^}aWwhmEnł+ײ~a wTeWm´A :=|ռyS.۔mMkȷET,\Irm:p=x`kNByV5gD7wIb ׃g`\A 8Xyf̀:*|J&z!A(pncnם 9#yƛ_zR}9 ӛn, uj{6,?8VɏF=j#(<n !>'E|\N6[qVμ _O1'[yMZlVO/g5kC4Tk% ˩CoR-^}ةW5W.Uftl|شO.C}p.OûETޭ%zwݸ޾Y@FRjM# ʸU.)SHftat1&Z?12έ}sX7g {zV(BՆ&pyT D}VҐβX}~+7aK*5oڍ9 XLi(VoXħD JˈIii}裷TY(vG@#l~vG_CJ/#z9i$'|{oQ&r$Dt˯k+Bcf;uDimLC\Ac,pA3'|1㜨ÔF7sLtq۳@ f7=,kϾ~;_Jf ]]O{r_EΝd-dzvpE$Fr\~TTM޺[BuR90atcׇY>!l߿α̛7O]62[gˍ]?#Dt>6K.$쑨xÆ yknB4?$c4k<>/X{Ԡ믽SNhCވ/Zd%wx%ǰ^弮s᥻Cg鉂4`]o5ƺMc藢qŻitvޣ_N*r]۶Wm@k[ve?LM}ӟrzѪXZDiR&!cGy. "rK[aZˀ,{>M ֽ<̳(93{4RR'x&q.H9? &X ~S~~i(CExDTUN{TU-E E AB޽jݬc3TQixε\A} Uo^zWo/7ϪqI]sƄT'$ܠQl޳N>o4#mM3f̰^9%, Ym36*&Dl@~A~ >,Ր2V[D܎zx+uB0$]p+%UvCkQL/}aLf}`ڪlѤ0!tA#+7_!B}[n(eOE=9gԯS3NB3)բ*)HDYJoZ0ά;{]t)O[ !SxSՖU ?2X;87w I,˹!sN.4q5M-MoX\N!SQX\M3g ."a. ˲r˯ QrP6aKE^5KznǖmYrwkmfLgZ$Θw!M$Cq=C Uff >,Ր2dN2D*^r-|#A%7aI #uF0 nmCkXXU>waQSp9Lbrػi [RÂY&C+sgGcDE-P)MF],ZP_~L!I1H7ޘwqs2m"C-\z룂 !?x >,Հ2HZ6.boX_ q 8"*p+ D4iF%$[aLu׼Mg. PTn=D~iId 㦙D,u[Or~|Ѯy}s/_V/ChͶfty24c=C=DUw.+VA#0wiDN?qD 4y|HˏPkQ0>uEEgY DP "P$tp[WUI"T $l{DYj.8W/[m5N.ڋL#e[s5_p(B|HD4[?9Oa}8{\&HR/tIF!qS+IÔ)eg I̗1Xw}_T1]U/WH}.)x@Y n _Kr{,*j" ֙zs\sb1v_1cG4:ѠA~Т{ZUݰVsu&ڂ[J_X\ȕcAUg!n :bC4ö6/r}~M[(r7dȐ%yB(^T,gugp'3f@4 5!q#]B'1c̯:ox:yD - #DKޫ~'|vgZ,*j㭛8rRwlvi'c^M '4A)HdvNح)ƃe @s Ik-#UTMYE^-/N;܃,YUvJ;їYkqz Z1@K=l^00L͍ "38)v /}=$2Q5xvx2('ZAFZ,W 5gK7˸ӕ3%Pl_.u!W~UyAr ]BO%5UC5^ns[q}~FW]㋛*$<]JCUC GX|G"G//^Pc՘O?9" 8 $?NS6>پ5n<āp9T5@lRRNX`5Gcf /wY:[U߭ OԗMq<5wuNdE` Z,iBp>ݴhD]8ߝ[&*,PD Y\n%"mHY8<٠f$QE/o{/Gbie̙SK~F0R^{\|J|`S3h\4C5$o0C-* ڣRe3x,.%0teLܥK8? Bֶ!@Ο\;]ϧV_7o3VE&׿3MUw+&70kXNl,|hv1a"q\/q*JIb/dc,Z[yy W&#_tPj9x3b{J㻭jXbJ'&ѭ GDCOgy}$k7mh=ע/\>b)7U[/2Q;Ӳ*6}kw*.CQ8 K⮻ z%U9қvؼ;O LbŊﵳ\wTs=>QRiaOE@ۤ0kpmSz+u1t;v.D1vprƦM2I"'fk ްVkc9,dMƪMebpAy'b`e(G$ծZԿss%hs";0e-]?$jc{J}˞w;^{bX`rpQ7|p} [fğ2uva٧ đҋkrH3-ߘX.zm.n߫<^{Yߊ["Oq@l +Vuͤg}6sg$՝#Z 2.ְJV03<@1 7vm5#%aM75Ņ^#KgOT+uա[1ah$qy]vUzM=pZ]4;:\x&)m,UiWC@FÄ$% *{#0EACmP/f"ȑ#Jb%,+/~15QOg॔'.=$nu4!}(rQݴjT*:m(,xK1|)!C-/^6FlA(69-<#D?Tr7Fr}ML Ё+228pSzO $Tv}"i;"SL6-~=ȣ>CqfHo:Qa,aMe-Bu1,8*$ks즏5NHZV:Mɭ_^ĔL^v*2YQW3X#l #_=ET8GL2#v8HUkv$7Q;:4mԶoYh<"a?}8tL"ŋǫ/\Ʃ-a[HOf$틒s@OJ:B@'x7Z,| [O=Կc-nљ^NFr]롂D?ꢋ.s4B"QA]>JļƙRfEzMn$箔\mi(kϤ|fK朩iy4!|a:R' ,NBI):>fr + .C{1r?Dy77" /lDG)elSV0565bz&R="aqT'ⰲRV:ћ4PK ;+X"ܑƷz H zc&%;L[d9gqg' Md*lÅrU;нJ}A]\=6ZaڧDVG5B9hRUM3m ] ~H\;u=!S 1QneltC>()@T*J1ED_ZCJwk7Er[g-Y6{,32}:SG܋TCy֙=[qu|QL \gK.W_^3+\7 >(''O h7g P kyr|os}7-ZO2[yX;edݻ\CWvڢvfW fe:tC&L m0vJd8n ȱD?\M:a} IJBסk&0&yKw=Xq~SP}'pAgTO(ZQ\-pRu Un$`g}&nP+gfvSz饗4=C= 4$&'O]}!>^!5>|<-T KJS&M4j6bQ:rb5*[X#m˥yQv]ݔ9]Y'ޛ:KmشYvmI48G&ԴS^O?ȡ2Q^ !9%u0L EyZpzaц&pH"H=c <"Ё'1kN>am0".t!a'n 1СCڄݴӯE{Q%)h9Z@Ҩ~ҘLĴe "7 o&{X>U$>CjSU!Sl h{w#w>4S d3n!oI4$-kox ƨ> }u Uϛ{Lg0`vNQ"|#N3dafIul nue펶k "~>66 oE߮1ՙ6t9OWבTܐx$5~ZDOѣG~3 Z>['25ȓYj#;\j`k~7;5 f"xVOu<^I-m 4 K$O>dt  0hK=|Y!Vqڿryu\"w3ԴK xNT:vuKԱ-߸N| :94mD.vKm0*ד6/'fs>WMCiR$Oak2D>Lt>kC|ͣ#ȍD7Q#[X2_Ʌ1,7ʱ|nm0 JN9V'ʹ[})2ۥjX~V~i}2EՕ8*T^z矫 ./[^ns O 6{$GL 84vK cfIt0|Y2X'}0C S> IUOq?=\Eq>v)X!:Ջ ݕhX/ C=re+>o9o"kNeis?s_-U^`Ͽ=C^*̃(qљx= $H]1#r6QE!:w qGPDDѵ#׳&wviDZ[;޵zA/kx>DaecO? zA,v矘mPZ\Q $/B|s3V-N*SBuG5JT=L{'yDsQ hgt dAN!D(V϶M`i=;26z XͧBTJJx*)/%4m,'u $)5p`Lb㰪JܱضBXe;bc7ꎈSOVJb*{=Hs)2G!Ҡ`$3fLIUn+r[YsmJ |DprUJnDiݔ{:yVkz_Z U(wuFG,\/C+9eJܭ<7^) OXZk{<hZ ,@uq<2IĴ:+ѸN^]G?կLNỲ{ !v:"$K &Db36 eXWp,My9){SYR՘&~bPC̯#a# }Y[; 3[[e׬RI,^Q&=ۀZ^l}eE=լacѡeW-_ &/{Q qByVN#ᝡ# )D,)qs !,"Ғ[$upF 3-X= Qzϱ,_M:F q_.@fުr ;_ܡF8MCF~ʵ[*8՝ R~!akiEQ뒋izHrLڤI̩ػy;Ѳn8+yKo~*/p[(-ύ+mw-*%#_}N3euik<nx&o;wo(y*t 325y]ي: $7.4~%Kkl.#|N /Lo?/kA\?<#hD"=5sy|f`]9T/Ӝ, ȣ[*+@IDATfΜl/wk"Pֵ .t,iYLѓcTN }ҟ|I펉W_^%sO lIdӁߝ0G2^1dUW]ԏ-~>ID? &kPm 2}s30 ۣ <qׁ,?/ M5$HT,`僰 /k#+%\5ipN*Y;RXp tlס=;O>)>ƿUrA3 q)יL]^Gr;uZS ,[&TTHO[LB:6m(թ%h`(+֖=xdrӔSٛKVWek|fMJmfS49NհL6 <4Sw P l#vuuFNΎGyAB= {/"MmǫۧIm۴ x9s4k;<% ׾l"(PjCu_~Y}W_};@߮ b0)n mVj5KO๣rkgC.4zK**CZ|~',n1[0+z]T7M% )}.4SޡhRAu<7 %`U&0JX0q ]?V ěnQ;n 1!+%rXLe.!hvfHOUOI楘^#x |馛An֤q?"^4Af؀o>D Ic>1Qu}3pbUaM$nZ/6mے0 ^-E7M2;cs jк)P/+ʔ+#Z~1F|a/0[|llP^t3ox@Xl7'@:X xAI?^STTI 8 c9y6"s6Ƈފen czVI'8!UU?30ՍHUio#"tB-5JiOΛFšKe=$c'lj'~ ({7ѩrNu]vCUqd2S]Ed%\`Bu)D<ʡ6 {Ԙ;}QRqsT*_SO=E2WUjXl!`2ݒ[HT8GdH9+g_w`w*$X[sM]QxIr`wU2 #;$uP Xi mdw^Z&cɒUbuF ։2{ͤuZHG36:Ƅ+_`[xb?{v767o;v խu@- C'})GS܀^ajM %}/gAYR߷>Q᧴[nrP;4 LkXBy -;Bc I|m7Dd?͊*ڀ!u?gU$pVc?HK~b{Yqz@%~gw4c/EW4Su H=t99 soM/244'勍3enFvޛӆ,WC7J,Oc,ף* lm*/:5w)*ɂMs'Vm^\K^D1{1\sKr*{6-[([Iyx*,W^Z}"~On'xl i(S+..qѦ5?{r~}\= v|_~e5hXpHJlkucRk-%72Gx?xHT{.4ljpJwfAi_@"B|L(5w2mU0OA l7p%<'rdQ=ju@_-STŀ5t/YVNN%oy"FUvldk}X3y8}eZnc7l,j_Ӝc}nc rDmpLӴCP{ּh۶m+a dMm}狪CDV3ھxQ~ڨoתټ~48hspK6ԪVèڢp@PW0o#Pt`4ۚng5EΏg)lALUb/pAg'LrgL2=63u1ЋK7sHD Jcr9$E4x$o_/ȵD[|HPw}3Π}{'53Hi lB! >t"ƍzehGPVnj?N… (ЫZ?&?FhBu?|u!Bۚu2iN; 9ZH-s[kƭWCdA|йvVEQ$ aܽ:G>O.6q]_]DfRG+b>SS"xĠ@@|nrn!=={-Uty56_=@xVkZ7҉~Q./psZ|$s9n{ȏŽHX6X rZOɩ w]G9_uaf4?h.;mq~rY#UnXM˙Nd^侉[vnwܳ8"x$2'OG"E$/`Iݖ9EHs"4rY`V-me@0[4' Kg v~Nqc0>@G-_1b&}ƭ?6!y,ҽS< Hs5l= eS0nxرn5bR:m/;^AG3("w!ov\Gva:zWt5?$󅽮վ0.t'JԨ5:œd2-1^#Ї0Yx8* rHo$]>K" lgPY#Ҿ x+{#ׂ {)4{ )s<\:}]րqI0Nva+=tR\9yfZnSO^]^9 )8Ex,y̼nc2Y%vܒuZ-~{}J C5'^"2H.&?Ž):袋B?O{VP'Տ[fzۼys.Nq1i1'B|7O^9BU%5 Jn{6>!뮻\/QgV `n8vr-!r$E#HIQ!/c%~HG3xBBs6JvQAh֬M1DZ9Juӏ>!n)y ]i "~路*xs)*D-ScD[rV70_J٫^m3 s=\y8Zۇ)aЌ'J RfGyĄ?u˵&Jo36n%-o/4a_`|W^"Ozg0`D(3f(\A tdBLɟs`ݷW?འmЅ_CKn^.o!8> ^^5hVX>~unF~0|v!zYe)gEN2}O4V}$QᬺƗ"jElnTWEcK>kLT8u4 F5p)?ޚeXYxGg: +^*Vo)W1u=d_.ÝП_bWzçi?4ncLj\I 5 aNb7rBj]S^M>1Be?V>{N~/ k/<٦Mm_DԴݩٱ\<~ӶWJ X 9&De,H1k!@]"X6Az[8 Z:N6n>-^x~ЭUO?cAQEiMN9|UR/ȱzyey\=aP*|뢍b|Tj͏+E5D:4qݛČ{Ǚ=FKCq^|s寮{:s3Cc?_=I!"w~uxYfqq;< Sw:ZAӺי.m3z,~0G) =UҲE4|T(Wqì(:jL"RtH`!w$WO?4mA5RYT"wC!awN/'t7^a U@YD7EAv69g.ŎweuNn(vmL$6!^vŖś\U X3i٠Knd ȥsCsYpƐrxt S9<Qp e^%ɀ)$H S ;9RT1PMr0`<鼍 70Fhst/9BY5P4 =POA tKcx!n L_pA:j ,x??[M>R:/S}Qԍ"aArqtQ;"M8ΛqlnG<=(k ,҇SeR=-6$sH%G|Q|/VV\9rdPda |[dN+gIσ,lAsAĊw=\"wr*^P@fAiS{cU{_5n{ qQvmD޺ٜ'k4jP_\]>fk[cֿ7h{N'|1I-DcؾeM.94eĶ`Q")Ă )l/}zh,&x$dҢ8nJB bȄqInY2:0 E@EIB?8Cs{^ d~-@x bYHhּ_lr<>v`|yY(_ܩn ! >sL."َw2@a18:x':"^xmP+Uݷl!L; ABje~';A*U]n\z1v}}lN1 xv@QКF ٶtqxmQF#!yi;Xިme+%7׍_fThPլUskлR,:'.e8Y~0w,hWvPXwu}(Yg.; &$|gLBDUC]d,vnݺ'tRO?#l:6i4Sxx&r3oKecˁiڬ!#3}SP]x/P^n)T ?{R,~;HP~xsPm[L͟b+hkP/pMuoc SVGnŸja _/B7oVc5T6)Sfzo 'V73~ʭbrtGkf 56[/6]+6_onc{4ھY=#q؂UE43s2굃 ]ǮO:֫ p NN]}y/eT.h>:aݳo?{p)̀aUO݁{]9)B" ҃Xy%"b%睱 (ZfF=_ƎtG>pvZ>p(;0cJW˞b1FvQ,!!.Lj! _}!|珸(l TeY8dzFꋖ'Ovś], @`[iε*Vg#r^1lX|EE9!oQ|ؽj5;̼!9|A-q6{*qAyt ^>(<N 512lGԯ0qڏ"ܦ&/E4K"̭4)ktb &0}"^dG]s~@P!q BcE$2ak=[ dި>ƌ,v1=@[6|HF\ v_#x.G8OI=u~\%AJ-W`UUl9~5E!5jM.ݼVlڝW8'e6۱Ue^U*TBDʖl_-bRN9EU_Ԫەs0yc>8ɶG"}qZ9N}Y5&q;uWgE#:xH=z\ûƩuڔKP7k)migA% PKNPe<7ٻ\R/xB6[h \'* ׃ak{;F=<=*X@LZzyx|6 }F@ LAe`oz,]MFՏ{7b@/̝#V,_aŅw[Ak@2p~I3$b$?7gL4bflO<12H.ZA >& aʘ! #[ 0XgqP}o{ =fJl}!e,| -;֩*ȑYtrTuQ2#GHL'ܙ)ꡃ[ ^|0[3@Ga#20x .}} #ϼ dTb׸OPg>B'P|Y?ͭtM)eV/i N7Rέ~g_>O'6"mBѨ hCq3l0\1Gz.N{A( w־^{6~\|{t @9eoÚ!ugp?6FǠOZzL7ZC5s^t:?gyH{W*4J薰!ow=x\Z*A@B>4|)jćw(7~hV?9L~L$;JȎX3;6N1?ԩS-$CӀ4O& WLA ~}$jϳ/xZKd.+W^xڦ^®]v"NuBm`RMe߶{AB]o b$|4L~Ma%{,1Q#͗@)(r3-^8jFj-H>iʔ/ [Qz_  T?RUVnP0|_s5 [:bmr;a3#kA!ϊ̊mMd50?~/:uev."x= "ccݛ}E;Fӥh ' .WJ"M3HdM 0aɽ VɃ7|Sj=KZTS(aq=-j ! SP\B0H~0qb Evg %b2dP`S(*:FvBË['QV) N8%xګT"}ƌcf \dWNsҢlaqoÏkH"R5-B18=1C6`7 kǰL5i%n؄=ퟢBM|(p.iY9!<"S8]~I+{rV5ϼ$n?Eه^3095#^:g(Vu)/y ijw3xF;ˤz~wo, KcVKa9r<:NVE ;~ ڦWy|ONlU :b(-%a4nPEpޥO%D?5(0^=G?L|6g &q+ qA;@ݫrg?,.jxdz9388 x iM\?(7 L~Eq'&4gHsn7& 2Dw#a@I ҿ#w![r~W^& k B >-YD1$la?>dI, ^ SQ=v#0h7JdƗ[@../#z reF %B,%`-0_ĨU*-@!q8Z:Xe)9D U#$5 PvWEyǸ ;B+} 5`\ ]$qDZ$2 Əii ߞǻF*{Ӵ~$pgؙ+؏w(. /F4MzfH3ɓ\I`4;^^b83,h"wrdC#w&gDYȨg#b'P{\DGr_7GڸVB6,Nf­rFH/Gx㏪4LJ)ٳS6ǒQ}C)lx>7>hS{P-i:AHh铪>)A zb+3^QQ4o<ŲoRo/|\b"J~$b0[} Iw0 "Tg8q\(|D aqNPۈ_)m_8/_d{|{&weݾs_0iҤ|yYg`gxu| $W+/.wy~|{DQ y':u| :5HkpE1!BtDa hȽu}"o*cH~'d' r;q@(Y6kjqXyMW9~c kaF@1I0(ϒ<^q/׏!Irp|] |1C7|?Q#D=SH|xig .Ax3ϑrA /FoѠ6Z˯O65{Z%/x y6lEl8b26躬!jي]B_Wލ`KBDf?U6#ƺRZ^}"7xImOõҠ89 Y)y! !&EQc_y^Ty|ޡ{+ϳQ#FDDܧ| N( 7a`pO3C TI*z,MpӤjA)[ȝȈ֢an[..lcj9J=2ZiXuK".-#:rn6It^\ĭ/'!@I,ts*D#9\{ ,Hvc||O!;`/0J -ט{w!w-J6 yaș#{?ArXERF%FiEWppFs۴bfPg1zJ1 ts8]&ө]䩥ϦfNlM<ڛ㮹Qm2@jG dArNDkxkNGY^:JjΣ@h"p`GQ &G,$Z\;tt.@1d:`B}e(̓X6iVpgb{U^N8fHb!L^;x"i7v>? 7c2O"xp #B$'ǪG/^s> j)~NwAt5ahhٛ}܇W!_K=zUQdY[iлw~A!g-xÊR g e~OHPHq=XH G#t@9 v2CFUGDansXFÌ җ\_!X^F9zeO@SwVh= x[ulKs ~H + HEJr>.A`ƌc-3 A4ͱ&-jew%5?AvH4hT>Q]~9ɫ?98ոwG6]V3-Ɖ8 H{W9&xOjDM2FwrQ|ܻ{(lWQc7/œDg~by7H@ $-ic$Q(ic2d#$;Fץ5ܹ ^A;ձ|ieA#tȍH$ 1(Ooߧ??n\^cBBJ<`,Rm߷k_DߎCU4FH.vr֙QV12)Kr@z{\@b!x_ɩj4|h/K#PoLDqg &y?!1އG+g|}r:Í7롇z`[K<ԧ+i iEƊ!Rgf f(K!3QA{"=ý+"):W?mРA}@l6^{$8!xΏO#2SjI!{L%p't9x4@`t3 B"& ݻ"mR}h# Tʥ'{zSxϋ'|r; gyV~T=3 Ğt{qIB5\wy+anw O#x }< Ҷ)h=m6kC4.htR0rNZRôHsEI#teat:r$2 ϺU;8o|&)'QF_x.{ & W՞ݩSźmG͔C-6773!eIuk))㹽 [IXO^CMO/C97cғF"xغ$$Awc qp>B.2tbqEv}Фʩݯ\9l,M.XB$r͛5qaΉyPa(Je˖2مLИeϧ(k,H%FrM/ҥ˱~fqP^Nyѣ#E+w)kcq=P'^g\#L?,X;~FbTۣ;uԵ7.ts2Q NzsiQ"bIv>[^ _^>$o?a{5(wU;*jf>욘v$IDATp<^5!3 xe[65槓Agȝ&%3SΜ#Ɋ)+3C#>ъF@)G\Dk~'I c2$zz>CJ ٞ#_=ZX<][o/xLtgtM7=` "fÕzp\(:r!!H]w\ `BS6%p:wai,_f7Oϴr H1SȺ\tAx +p mnKcrO={-l Q&HSN9W_}^[˾צ^z_wu·y-ې/R%ȑ#C75rxGP@;v-p^zH LzŢ|*ZWppLOCIv 7w6>ZI`(%xG_`H7k-FWg2; YhP( tH\ʼn@Iݷ\+qڴisa4 N3lذ@UWqFTC bO?G?Sw"by OE$(QC $l'lAs3ՉԿ-WvʲFY@2Ǐ?>Lg=,$wi.ӡYƢ,֑mY`&(”J+ALKL/eosd|d@Vt'NKvZդIktC-Mē?瞣=D7|;O[DQn}LEfuBTC(@Fxxɺl?jO( ۵}pQ"^}Hm"w!mQ?cۇYޗˑh-LaD.qQ޾A"qȤKL@D]\"it= ;o'؏L*r%tgY9byӿW7Xsio01uTrAևA]/&ٸIcbn/vHPHÊ9 Fs zuSA4_ٯWQ9NLdy/<߁!D;שo'x7U/^CSA\rr@*T^gQ#lBi8u͓KK KuQD04c 78֢,I5G]0Q̙3O$Lt DІƒ sA1b|֙G{) lЊ?AT!jTsغz^$JpyODft@!;lq>Őm>1$.4[e:" } ^l<#-?RnuZEk,[l"| a\W~1|w>1=N=`[pssM .#{E -X)ö=QO@_gm "#UMzѭIC_~A0/Oz >@%rM wCV4 R(k>Wx)z65 U 4.6 w#l}JO(%HE9%yiX {_5vANMv.=NCHWvDoͶvd~3'V)BCTAL2י-}mV31n‘a!r'{i`[6NrkKu=  mT/Ͽ+q,]qFIqA<Đ2P,fg(T ȒrG>j]J?paaB' ]֭[C^+<3 iP-J!2e;4'!kiq0΋C28l7\m {N3]䲹?_էҭ[3${ܻ,'jL=R.k.)|q|j3 i.y{;E7}>kO?Ꟊ6ZAx>ּk/1"566XOA\Oϡ!P |IziU}@iƠ.$Gc:nahe혆x@҄Ȥv$ʍ"b]l EK{(O\ "lƸxbE`:'>O[_ PmX7s@HRyl o6#1_9ê1r Bb@Al ) ֢scfKfr 4;l*6{~Wºynb =@qSi q+  / J$Vȭ3CU"Mr黍[n˺a:]RUH0Z3 xD ;܈8ӎשװ 7/mj;ϹD5sƇ),tSP҈fڸim ѦjHS]fCҘN,D2ʨEk RKɃN6 a$8 F` ND*=ZMye[450dxytSg\pUFw;{쉠9/גma @,"Hq4ulTJŒP #`:7p7u>t Bid!4‡ ['@/jvh_GH |o55"0}xr LƗg+b؃˨o^v(9P 9^:99d t >A ҇c*Nm۶6 0ijҫW`)D]TNk:  !|+#!~gmw#?Vwm%v+v;Rz"RYl'`;~\}F u u^EJPoxNJXx ׄpskns!i?m ns54C(^7Dt"zBSe;,r ^v"zrJ8-(%l#>Tb>Uܳ {P;,; &ym?6iҤN&q Ay\釀Fy_k׮n?!rnI_<\r.rAD@@ y$cXw?ր .=WrND'_X=뮻k뜮@Nct@>Zxـlw/8i/ƾ̰&ݎܻ'RI"h by/]b Q"Rg.%QqPTL <#w}줏Y1M8 D,%.׬Uӱl"v#F<4lƔ`:79s/yI(d)!as/:wqwMp{UuZC@C ;!|v>b+XljyNHf+V㤉<֩{R<~VDA yCՠN;}ۉX#Nx>q {YHG]vsss[w풯uO@W5g)n[Su;> ".tI>dȐazш<[ޓ[|6簷v!X9qRx$U6Pq:?v/rNA2P{y嗟=eʔrϠD7{\r8]!%<4#FL[Z` í.'%UXFEj_t= _Կ;I (sSy9'  (GU2@‰*'^ P'l<쳻2~7x"d?}^hϦXνs2"9sv&qi.qDI[+WQM=j'n_sA#1y x~๽GPk~$IίyiEr̹H?1!!yhgPw@-;}ռtϟ-10ۋ@)WaXЕ=H:t ]_.sJ4h 6kJB‚7#۞nLe,{_Шzc8-y׷~Ր^PA@V@vC@#~>bwK.%u4.uŋNN;-\9G}R<'grhpsݫi$g*U웰pbZM/-ã1m|ɳ`I4<)ᠤ75֝7khd4ϞgQ,wBwrDn͛7733$9g}Hz)o Ig,= @K˼QjX8{bx4D{[oѲeK ҥOO.RZۋZއHH^5>D*!&uʐPz:tOv"Q>_ZNya!1E* IEdd={+qz@!|A9s"9+_7Y"ޘzpV7io}|wGd|k kRl?~)8s=zů{y$ Nܻ $Sۻ'1ʞl4iLS^HqM'GEN*9w駟~3Î5423 (A]\pW_}&G4ANjp H>!o'UZ)I `o9(oڂ5NvUNѽ˾{?9J @`0TpVcT=?9#ҿO.h]ϣX P^4!^9kt L;e8TH^QNa׊*E@ A\ "J+&L֩uk{B|ka+Q-F_kd94;3N>ݕ3(j/LSRD^M}}>iQ)"/(5t40d2mڴ{DG?G$L*OQp&cǎ@@7b+;ΈT znP֦54gs]}Dr! p;y~sSߜ vK n~ȑQn^eo.cqVo?E\ώM41?xܰI9Tq=^<{N97]?\hh[fA|.?e!5 *&ګ{챱Oihd4ϞgQwӾ}#0%pk#8Y)-ӝX$=OwPEkXHG\^y'o裏^8ꨣp3lJ.իW?KLIVÆ "`1AD@ږa}< 5Pby gĄ9o<+A{֭a'ܦO>`?|'w^ՓckuHܲ ߘ+S@ ƉØ Q[o1\'$X{{zߝdB ӆv!P< |xN%bKDd ok}>q*njln]ݘ{!u1{[ߟiT!QB u42">|;V!.~sٗuH `)5mY'PLb='d?:5i.}!>NI[ /<(2U ޘ:uD 5k.`yb]v<3UYN_ɐ׾zjBx̠g0<#3۞,нĜ1zy|vo D\&KLIDͫ0:{nw37D @_?t=۩wzgrf:B$?mZ8Sa Mmwy7 ,N?{s2gΜ d8 B5Y̗Fm^{~ƌh "ǓR`.54g)y#"H(x zCU=q y w#v*<ѣGKr2. 0$FZvFܦy]xQWfS;Aߨ[n:}k#3O=Կ̚ or^?zCMR٫]ihd4Ϯw-T~Bgs,Z!]#zw-\p}K/&ig+DY\>GVZY}%Wm^p$ MC |=` bx)@ԩS"xC$.`6Cb o_hr?ݻ{޿A4>%<}ч`߫,eߑVp FNu |<,[}p(n~SzmNt sO,u.p 9fQũ'|:e]vSAP}.gOxKSd|R^z_Gc'!D L\VX/S8"]{袋bG>oe07|iRPg\6 VT@vC@#~>zw>Knx [gfeV|zt7p}n{oqy'9gI(3gΜ4iʔ80Cb%<n} Rk  쁀F,N=?C^m^;ӘFwWuFW#FGyxus=TeOoﯯ542 3 zQ[޽QXzx2b!eU{7f=' f;QC@C ;!|v>x{-9/*g?wYO:]ûʱ^-r)"aÆ ep؆Cqз&!!ᡯ1 _C;])@t1n>y;vxD5/[cƌ@_룆@vB@#|.zWI@g]g}tILa A <r\׮]-ې('N d-lݫޗ@"&V  ]*sW_}59ۓs@?h!& Cc9~РA XqӦMKˋZDˠid_|ɮ7|yv0 uC 5O?#30I@@#$d? __]" ?i`2ߕp=rX=jfh_wW?Sƀ WrAv #꣆@vA@#zz7%իW ׽7+GU(Gpq%[(0 ,7oRC 9!۟^;ZhqlFHEQΫH-}jg/x(ȝij)ŏQߺ@vԭ[7g>44J&4/Uߕ@)6+/}    Ksw!!!!P!|)khhhLh_2+    RK o_C@C@C@CdB@#\]ihhhrh__}%~WxAHBIENDB`fuzzel/3rd-party/nanosvg/example/stb_image_write.h000066400000000000000000000442471445417001200226560ustar00rootroot00000000000000/* stbiw-0.92 - public domain - http://nothings.org/stb/stb_image_write.h writes out PNG/BMP/TGA images to C stdio - Sean Barrett 2010 no warranty implied; use at your own risk Before including, #define STB_IMAGE_WRITE_IMPLEMENTATION in the file that you want to have the implementation. ABOUT: This header file is a library for writing images to C stdio. It could be adapted to write to memory or a general streaming interface; let me know. The PNG output is not optimal; it is 20-50% larger than the file written by a decent optimizing implementation. This library is designed for source code compactness and simplicitly, not optimal image file size or run-time performance. USAGE: There are three functions, one for each image file format: int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); Each function returns 0 on failure and non-0 on success. The functions create an image file defined by the parameters. The image is a rectangle of pixels stored from left-to-right, top-to-bottom. Each pixel contains 'comp' channels of data stored interleaved with 8-bits per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. The *data pointer points to the first byte of the top-left-most pixel. For PNG, "stride_in_bytes" is the distance in bytes from the first byte of a row of pixels to the first byte of the next row of pixels. PNG creates output files with the same number of components as the input. The BMP and TGA formats expand Y to RGB in the file format. BMP does not output alpha. PNG supports writing rectangles of data even when the bytes storing rows of data are not consecutive in memory (e.g. sub-rectangles of a larger image), by supplying the stride between the beginning of adjacent rows. The other formats do not. (Thus you cannot write a native-format BMP through the BMP writer, both because it is in BGR order and because it may have padding at the end of the line.) */ #ifndef INCLUDE_STB_IMAGE_WRITE_H #define INCLUDE_STB_IMAGE_WRITE_H #ifdef __cplusplus extern "C" { #endif extern int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); extern int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); extern int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); #ifdef __cplusplus } #endif #endif//INCLUDE_STB_IMAGE_WRITE_H #ifdef STB_IMAGE_WRITE_IMPLEMENTATION #include #include #include #include #include typedef unsigned int stbiw_uint32; typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; static void writefv(FILE *f, const char *fmt, va_list v) { while (*fmt) { switch (*fmt++) { case ' ': break; case '1': { unsigned char x = (unsigned char) va_arg(v, int); fputc(x,f); break; } case '2': { int x = va_arg(v,int); unsigned char b[2]; b[0] = (unsigned char) x; b[1] = (unsigned char) (x>>8); fwrite(b,2,1,f); break; } case '4': { stbiw_uint32 x = va_arg(v,int); unsigned char b[4]; b[0]=(unsigned char)x; b[1]=(unsigned char)(x>>8); b[2]=(unsigned char)(x>>16); b[3]=(unsigned char)(x>>24); fwrite(b,4,1,f); break; } default: assert(0); return; } } } static void write3(FILE *f, unsigned char a, unsigned char b, unsigned char c) { unsigned char arr[3]; arr[0] = a, arr[1] = b, arr[2] = c; fwrite(arr, 3, 1, f); } static void write_pixels(FILE *f, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad) { unsigned char bg[3] = { 255, 0, 255}, px[3]; stbiw_uint32 zero = 0; int i,j,k, j_end; if (y <= 0) return; if (vdir < 0) j_end = -1, j = y-1; else j_end = y, j = 0; for (; j != j_end; j += vdir) { for (i=0; i < x; ++i) { unsigned char *d = (unsigned char *) data + (j*x+i)*comp; if (write_alpha < 0) fwrite(&d[comp-1], 1, 1, f); switch (comp) { case 1: case 2: write3(f, d[0],d[0],d[0]); break; case 4: if (!write_alpha) { // composite against pink background for (k=0; k < 3; ++k) px[k] = bg[k] + ((d[k] - bg[k]) * d[3])/255; write3(f, px[1-rgb_dir],px[1],px[1+rgb_dir]); break; } /* FALLTHROUGH */ case 3: write3(f, d[1-rgb_dir],d[1],d[1+rgb_dir]); break; } if (write_alpha > 0) fwrite(&d[comp-1], 1, 1, f); } fwrite(&zero,scanline_pad,1,f); } } static int outfile(char const *filename, int rgb_dir, int vdir, int x, int y, int comp, void *data, int alpha, int pad, const char *fmt, ...) { FILE *f; if (y < 0 || x < 0) return 0; f = fopen(filename, "wb"); if (f) { va_list v; va_start(v, fmt); writefv(f, fmt, v); va_end(v); write_pixels(f,rgb_dir,vdir,x,y,comp,data,alpha,pad); fclose(f); } return f != NULL; } int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) { int pad = (-x*3) & 3; return outfile(filename,-1,-1,x,y,comp,(void *) data,0,pad, "11 4 22 4" "4 44 22 444444", 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header } int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) { int has_alpha = !(comp & 1); return outfile(filename, -1,-1, x, y, comp, (void *) data, has_alpha, 0, "111 221 2222 11", 0,0,2, 0,0,0, 0,0,x,y, 24+8*has_alpha, 8*has_alpha); } // stretchy buffer; stbi__sbpush() == vector<>::push_back() -- stbi__sbcount() == vector<>::size() #define stbi__sbraw(a) ((int *) (a) - 2) #define stbi__sbm(a) stbi__sbraw(a)[0] #define stbi__sbn(a) stbi__sbraw(a)[1] #define stbi__sbneedgrow(a,n) ((a)==0 || stbi__sbn(a)+n >= stbi__sbm(a)) #define stbi__sbmaybegrow(a,n) (stbi__sbneedgrow(a,(n)) ? stbi__sbgrow(a,n) : 0) #define stbi__sbgrow(a,n) stbi__sbgrowf((void **) &(a), (n), sizeof(*(a))) #define stbi__sbpush(a, v) (stbi__sbmaybegrow(a,1), (a)[stbi__sbn(a)++] = (v)) #define stbi__sbcount(a) ((a) ? stbi__sbn(a) : 0) #define stbi__sbfree(a) ((a) ? free(stbi__sbraw(a)),0 : 0) static void *stbi__sbgrowf(void **arr, int increment, int itemsize) { int m = *arr ? 2*stbi__sbm(*arr)+increment : increment+1; void *p = realloc(*arr ? stbi__sbraw(*arr) : 0, itemsize * m + sizeof(int)*2); assert(p); if (p) { if (!*arr) ((int *) p)[1] = 0; *arr = (void *) ((int *) p + 2); stbi__sbm(*arr) = m; } return *arr; } static unsigned char *stbi__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) { while (*bitcount >= 8) { stbi__sbpush(data, (unsigned char) *bitbuffer); *bitbuffer >>= 8; *bitcount -= 8; } return data; } static int stbi__zlib_bitrev(int code, int codebits) { int res=0; while (codebits--) { res = (res << 1) | (code & 1); code >>= 1; } return res; } static unsigned int stbi__zlib_countm(unsigned char *a, unsigned char *b, int limit) { int i; for (i=0; i < limit && i < 258; ++i) if (a[i] != b[i]) break; return i; } static unsigned int stbi__zhash(unsigned char *data) { stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); hash ^= hash << 3; hash += hash >> 5; hash ^= hash << 4; hash += hash >> 17; hash ^= hash << 25; hash += hash >> 6; return hash; } #define stbi__zlib_flush() (out = stbi__zlib_flushf(out, &bitbuf, &bitcount)) #define stbi__zlib_add(code,codebits) \ (bitbuf |= (code) << bitcount, bitcount += (codebits), stbi__zlib_flush()) #define stbi__zlib_huffa(b,c) stbi__zlib_add(stbi__zlib_bitrev(b,c),c) // default huffman tables #define stbi__zlib_huff1(n) stbi__zlib_huffa(0x30 + (n), 8) #define stbi__zlib_huff2(n) stbi__zlib_huffa(0x190 + (n)-144, 9) #define stbi__zlib_huff3(n) stbi__zlib_huffa(0 + (n)-256,7) #define stbi__zlib_huff4(n) stbi__zlib_huffa(0xc0 + (n)-280,8) #define stbi__zlib_huff(n) ((n) <= 143 ? stbi__zlib_huff1(n) : (n) <= 255 ? stbi__zlib_huff2(n) : (n) <= 279 ? stbi__zlib_huff3(n) : stbi__zlib_huff4(n)) #define stbi__zlib_huffb(n) ((n) <= 143 ? stbi__zlib_huff1(n) : stbi__zlib_huff2(n)) #define stbi__ZHASH 16384 unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) { static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; unsigned int bitbuf=0; int i,j, bitcount=0; unsigned char *out = NULL; unsigned char **hash_table[stbi__ZHASH]; // 64KB on the stack! if (quality < 5) quality = 5; stbi__sbpush(out, 0x78); // DEFLATE 32K window stbi__sbpush(out, 0x5e); // FLEVEL = 1 stbi__zlib_add(1,1); // BFINAL = 1 stbi__zlib_add(1,2); // BTYPE = 1 -- fixed huffman for (i=0; i < stbi__ZHASH; ++i) hash_table[i] = NULL; i=0; while (i < data_len-3) { // hash next 3 bytes of data to be compressed int h = stbi__zhash(data+i)&(stbi__ZHASH-1), best=3; unsigned char *bestloc = 0; unsigned char **hlist = hash_table[h]; int n = stbi__sbcount(hlist); for (j=0; j < n; ++j) { if (hlist[j]-data > i-32768) { // if entry lies within window int d = stbi__zlib_countm(hlist[j], data+i, data_len-i); if (d >= best) best=d,bestloc=hlist[j]; } } // when hash table entry is too long, delete half the entries if (hash_table[h] && stbi__sbn(hash_table[h]) == 2*quality) { memcpy(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); stbi__sbn(hash_table[h]) = quality; } stbi__sbpush(hash_table[h],data+i); if (bestloc) { // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal h = stbi__zhash(data+i+1)&(stbi__ZHASH-1); hlist = hash_table[h]; n = stbi__sbcount(hlist); for (j=0; j < n; ++j) { if (hlist[j]-data > i-32767) { int e = stbi__zlib_countm(hlist[j], data+i+1, data_len-i-1); if (e > best) { // if next match is better, bail on current match bestloc = NULL; break; } } } } if (bestloc) { int d = data+i - bestloc; // distance back assert(d <= 32767 && best <= 258); for (j=0; best > lengthc[j+1]-1; ++j); stbi__zlib_huff(j+257); if (lengtheb[j]) stbi__zlib_add(best - lengthc[j], lengtheb[j]); for (j=0; d > distc[j+1]-1; ++j); stbi__zlib_add(stbi__zlib_bitrev(j,5),5); if (disteb[j]) stbi__zlib_add(d - distc[j], disteb[j]); i += best; } else { stbi__zlib_huffb(data[i]); ++i; } } // write out final bytes for (;i < data_len; ++i) stbi__zlib_huffb(data[i]); stbi__zlib_huff(256); // end of block // pad with 0 bits to byte boundary while (bitcount) stbi__zlib_add(0,1); for (i=0; i < stbi__ZHASH; ++i) (void) stbi__sbfree(hash_table[i]); { // compute adler32 on input unsigned int i=0, s1=1, s2=0, blocklen = data_len % 5552; int j=0; while (j < data_len) { for (i=0; i < blocklen; ++i) s1 += data[j+i], s2 += s1; s1 %= 65521, s2 %= 65521; j += blocklen; blocklen = 5552; } stbi__sbpush(out, (unsigned char) (s2 >> 8)); stbi__sbpush(out, (unsigned char) s2); stbi__sbpush(out, (unsigned char) (s1 >> 8)); stbi__sbpush(out, (unsigned char) s1); } *out_len = stbi__sbn(out); // make returned pointer freeable memmove(stbi__sbraw(out), out, *out_len); return (unsigned char *) stbi__sbraw(out); } unsigned int stbi__crc32(unsigned char *buffer, int len) { static unsigned int crc_table[256]; unsigned int crc = ~0u; int i,j; if (crc_table[1] == 0) for(i=0; i < 256; i++) for (crc_table[i]=i, j=0; j < 8; ++j) crc_table[i] = (crc_table[i] >> 1) ^ (crc_table[i] & 1 ? 0xedb88320 : 0); for (i=0; i < len; ++i) crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; return ~crc; } #define stbi__wpng4(o,a,b,c,d) ((o)[0]=(unsigned char)(a),(o)[1]=(unsigned char)(b),(o)[2]=(unsigned char)(c),(o)[3]=(unsigned char)(d),(o)+=4) #define stbi__wp32(data,v) stbi__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); #define stbi__wptag(data,s) stbi__wpng4(data, s[0],s[1],s[2],s[3]) static void stbi__wpcrc(unsigned char **data, int len) { unsigned int crc = stbi__crc32(*data - len - 4, len+4); stbi__wp32(*data, crc); } static unsigned char stbi__paeth(int a, int b, int c) { int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); if (pa <= pb && pa <= pc) return (unsigned char) a; if (pb <= pc) return (unsigned char) b; return (unsigned char) c; } unsigned char *stbi_write_png_to_mem(unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) { int ctype[5] = { -1, 0, 4, 2, 6 }; unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; unsigned char *out,*o, *filt, *zlib; signed char *line_buffer; int i,j,k,p,zlen; if (stride_bytes == 0) stride_bytes = x * n; filt = (unsigned char *) malloc((x*n+1) * y); if (!filt) return 0; line_buffer = (signed char *) malloc(x * n); if (!line_buffer) { free(filt); return 0; } for (j=0; j < y; ++j) { static int mapping[] = { 0,1,2,3,4 }; static int firstmap[] = { 0,1,0,5,6 }; int *mymap = j ? mapping : firstmap; int best = 0, bestval = 0x7fffffff; for (p=0; p < 2; ++p) { for (k= p?best:0; k < 5; ++k) { int type = mymap[k],est=0; unsigned char *z = pixels + stride_bytes*j; for (i=0; i < n; ++i) switch (type) { case 0: line_buffer[i] = z[i]; break; case 1: line_buffer[i] = z[i]; break; case 2: line_buffer[i] = z[i] - z[i-stride_bytes]; break; case 3: line_buffer[i] = z[i] - (z[i-stride_bytes]>>1); break; case 4: line_buffer[i] = (signed char) (z[i] - stbi__paeth(0,z[i-stride_bytes],0)); break; case 5: line_buffer[i] = z[i]; break; case 6: line_buffer[i] = z[i]; break; } for (i=n; i < x*n; ++i) { switch (type) { case 0: line_buffer[i] = z[i]; break; case 1: line_buffer[i] = z[i] - z[i-n]; break; case 2: line_buffer[i] = z[i] - z[i-stride_bytes]; break; case 3: line_buffer[i] = z[i] - ((z[i-n] + z[i-stride_bytes])>>1); break; case 4: line_buffer[i] = z[i] - stbi__paeth(z[i-n], z[i-stride_bytes], z[i-stride_bytes-n]); break; case 5: line_buffer[i] = z[i] - (z[i-n]>>1); break; case 6: line_buffer[i] = z[i] - stbi__paeth(z[i-n], 0,0); break; } } if (p) break; for (i=0; i < x*n; ++i) est += abs((signed char) line_buffer[i]); if (est < bestval) { bestval = est; best = k; } } } // when we get here, best contains the filter type, and line_buffer contains the data filt[j*(x*n+1)] = (unsigned char) best; memcpy(filt+j*(x*n+1)+1, line_buffer, x*n); } free(line_buffer); zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, 8); // increase 8 to get smaller but use more memory free(filt); if (!zlib) return 0; // each tag requires 12 bytes of overhead out = (unsigned char *) malloc(8 + 12+13 + 12+zlen + 12); if (!out) return 0; *out_len = 8 + 12+13 + 12+zlen + 12; o=out; memcpy(o,sig,8); o+= 8; stbi__wp32(o, 13); // header length stbi__wptag(o, "IHDR"); stbi__wp32(o, x); stbi__wp32(o, y); *o++ = 8; *o++ = (unsigned char) ctype[n]; *o++ = 0; *o++ = 0; *o++ = 0; stbi__wpcrc(&o,13); stbi__wp32(o, zlen); stbi__wptag(o, "IDAT"); memcpy(o, zlib, zlen); o += zlen; free(zlib); stbi__wpcrc(&o, zlen); stbi__wp32(o,0); stbi__wptag(o, "IEND"); stbi__wpcrc(&o,0); assert(o == out + *out_len); return out; } int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) { FILE *f; int len; unsigned char *png = stbi_write_png_to_mem((unsigned char *) data, stride_bytes, x, y, comp, &len); if (!png) return 0; f = fopen(filename, "wb"); if (!f) { free(png); return 0; } fwrite(png, 1, len, f); fclose(f); free(png); return 1; } #endif // STB_IMAGE_WRITE_IMPLEMENTATION /* Revision history 0.92 (2010-08-01) casts to unsigned char to fix warnings 0.91 (2010-07-17) first public release 0.90 first internal release */ fuzzel/3rd-party/nanosvg/premake4.lua000066400000000000000000000026271445417001200201150ustar00rootroot00000000000000 local action = _ACTION or "" solution "nanosvg" location ( "build" ) configurations { "Debug", "Release" } platforms {"native", "x64", "x32"} project "example1" kind "ConsoleApp" language "C++" files { "example/example1.c", "example/*.h", "src/*.h" } includedirs { "example", "src" } targetdir("build") configuration { "linux" } links { "X11","Xrandr", "rt", "GL", "GLU", "pthread", "glfw" } configuration { "windows" } links { "glu32","opengl32", "gdi32", "winmm", "user32" } configuration { "macosx" } links { "glfw3" } linkoptions { "-framework OpenGL", "-framework Cocoa", "-framework IOKit", "-framework CoreVideo" } configuration "Debug" defines { "DEBUG" } flags { "Symbols", "ExtraWarnings"} configuration "Release" defines { "NDEBUG" } flags { "Optimize", "ExtraWarnings"} project "example2" kind "ConsoleApp" language "C++" files { "example/example2.c", "example/*.h", "src/*.h" } includedirs { "example", "src" } targetdir("build") configuration { "linux" } links { "X11","Xrandr", "rt", "pthread" } configuration { "windows" } links { "winmm", "user32" } configuration { "macosx" } linkoptions { "-framework Cocoa", "-framework IOKit" } configuration "Debug" defines { "DEBUG" } flags { "Symbols", "ExtraWarnings"} configuration "Release" defines { "NDEBUG" } flags { "Optimize", "ExtraWarnings"} fuzzel/3rd-party/nanosvg/src/000077500000000000000000000000001445417001200164625ustar00rootroot00000000000000fuzzel/3rd-party/nanosvg/src/nanosvg.h000066400000000000000000002423751445417001200203230ustar00rootroot00000000000000/* * Copyright (c) 2013-14 Mikko Mononen memon@inside.org * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. * * The SVG parser is based on Anti-Grain Geometry 2.4 SVG example * Copyright (C) 2002-2004 Maxim Shemanarev (McSeem) (http://www.antigrain.com/) * * Arc calculation code based on canvg (https://code.google.com/p/canvg/) * * Bounding box calculation based on http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html * */ #ifndef NANOSVG_H #define NANOSVG_H #ifndef NANOSVG_CPLUSPLUS #ifdef __cplusplus extern "C" { #endif #endif // NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. // // The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. // // NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! // // The shapes in the SVG images are transformed by the viewBox and converted to specified units. // That is, you should get the same looking data as your designed in your favorite app. // // NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose // to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. // // The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. // DPI (dots-per-inch) controls how the unit conversion is done. // // If you don't know or care about the units stuff, "px" and 96 should get you going. /* Example Usage: // Load SVG NSVGimage* image; image = nsvgParseFromFile("test.svg", "px", 96); printf("size: %f x %f\n", image->width, image->height); // Use... for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { for (int i = 0; i < path->npts-1; i += 3) { float* p = &path->pts[i*2]; drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); } } } // Delete nsvgDelete(image); */ enum NSVGpaintType { NSVG_PAINT_UNDEF = -1, NSVG_PAINT_NONE = 0, NSVG_PAINT_COLOR = 1, NSVG_PAINT_LINEAR_GRADIENT = 2, NSVG_PAINT_RADIAL_GRADIENT = 3 }; enum NSVGspreadType { NSVG_SPREAD_PAD = 0, NSVG_SPREAD_REFLECT = 1, NSVG_SPREAD_REPEAT = 2 }; enum NSVGlineJoin { NSVG_JOIN_MITER = 0, NSVG_JOIN_ROUND = 1, NSVG_JOIN_BEVEL = 2 }; enum NSVGlineCap { NSVG_CAP_BUTT = 0, NSVG_CAP_ROUND = 1, NSVG_CAP_SQUARE = 2 }; enum NSVGfillRule { NSVG_FILLRULE_NONZERO = 0, NSVG_FILLRULE_EVENODD = 1 }; enum NSVGflags { NSVG_FLAGS_VISIBLE = 0x01 }; typedef struct NSVGgradientStop { unsigned int color; float offset; } NSVGgradientStop; typedef struct NSVGgradient { float xform[6]; char spread; float fx, fy; int nstops; NSVGgradientStop stops[1]; } NSVGgradient; typedef struct NSVGpaint { signed char type; union { unsigned int color; NSVGgradient* gradient; }; } NSVGpaint; typedef struct NSVGpath { float* pts; // Cubic bezier points: x0,y0, [cpx1,cpx1,cpx2,cpy2,x1,y1], ... int npts; // Total number of bezier points. char closed; // Flag indicating if shapes should be treated as closed. float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. struct NSVGpath* next; // Pointer to next path, or NULL if last element. } NSVGpath; typedef struct NSVGshape { char id[64]; // Optional 'id' attr of the shape or its group NSVGpaint fill; // Fill paint NSVGpaint stroke; // Stroke paint float opacity; // Opacity of the shape. float strokeWidth; // Stroke width (scaled). float strokeDashOffset; // Stroke dash offset (scaled). float strokeDashArray[8]; // Stroke dash array (scaled). char strokeDashCount; // Number of dash values in dash array. char strokeLineJoin; // Stroke join type. char strokeLineCap; // Stroke cap type. float miterLimit; // Miter limit char fillRule; // Fill rule, see NSVGfillRule. unsigned char flags; // Logical or of NSVG_FLAGS_* flags float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. char fillGradient[64]; // Optional 'id' of fill gradient char strokeGradient[64]; // Optional 'id' of stroke gradient float xform[6]; // Root transformation for fill/stroke gradient NSVGpath* paths; // Linked list of paths in the image. struct NSVGshape* next; // Pointer to next shape, or NULL if last element. } NSVGshape; typedef struct NSVGimage { float width; // Width of the image. float height; // Height of the image. NSVGshape* shapes; // Linked list of shapes in the image. } NSVGimage; // Parses SVG file from a file, returns SVG image as paths. NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi); // Parses SVG file from a null terminated string, returns SVG image as paths. // Important note: changes the string. NSVGimage* nsvgParse(char* input, const char* units, float dpi); // Duplicates a path. NSVGpath* nsvgDuplicatePath(NSVGpath* p); // Deletes an image. void nsvgDelete(NSVGimage* image); #ifndef NANOSVG_CPLUSPLUS #ifdef __cplusplus } #endif #endif #ifdef NANOSVG_IMPLEMENTATION #include #include #include #include #define NSVG_PI (3.14159265358979323846264338327f) #define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. #define NSVG_ALIGN_MIN 0 #define NSVG_ALIGN_MID 1 #define NSVG_ALIGN_MAX 2 #define NSVG_ALIGN_NONE 0 #define NSVG_ALIGN_MEET 1 #define NSVG_ALIGN_SLICE 2 #define NSVG_NOTUSED(v) do { (void)(1 ? (void)0 : ( (void)(v) ) ); } while(0) #define NSVG_RGB(r, g, b) (((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16)) #ifdef _MSC_VER #pragma warning (disable: 4996) // Switch off security warnings #pragma warning (disable: 4100) // Switch off unreferenced formal parameter warnings #ifdef __cplusplus #define NSVG_INLINE inline #else #define NSVG_INLINE #endif #else #define NSVG_INLINE inline #endif static int nsvg__isspace(char c) { return strchr(" \t\n\v\f\r", c) != 0; } static int nsvg__isdigit(char c) { return c >= '0' && c <= '9'; } static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } // Simple XML parser #define NSVG_XML_TAG 1 #define NSVG_XML_CONTENT 2 #define NSVG_XML_MAX_ATTRIBS 256 static void nsvg__parseContent(char* s, void (*contentCb)(void* ud, const char* s), void* ud) { // Trim start white spaces while (*s && nsvg__isspace(*s)) s++; if (!*s) return; if (contentCb) (*contentCb)(ud, s); } static void nsvg__parseElement(char* s, void (*startelCb)(void* ud, const char* el, const char** attr), void (*endelCb)(void* ud, const char* el), void* ud) { const char* attr[NSVG_XML_MAX_ATTRIBS]; int nattr = 0; char* name; int start = 0; int end = 0; char quote; // Skip white space after the '<' while (*s && nsvg__isspace(*s)) s++; // Check if the tag is end tag if (*s == '/') { s++; end = 1; } else { start = 1; } // Skip comments, data and preprocessor stuff. if (!*s || *s == '?' || *s == '!') return; // Get tag name name = s; while (*s && !nsvg__isspace(*s)) s++; if (*s) { *s++ = '\0'; } // Get attribs while (!end && *s && nattr < NSVG_XML_MAX_ATTRIBS-3) { char* name = NULL; char* value = NULL; // Skip white space before the attrib name while (*s && nsvg__isspace(*s)) s++; if (!*s) break; if (*s == '/') { end = 1; break; } name = s; // Find end of the attrib name. while (*s && !nsvg__isspace(*s) && *s != '=') s++; if (*s) { *s++ = '\0'; } // Skip until the beginning of the value. while (*s && *s != '\"' && *s != '\'') s++; if (!*s) break; quote = *s; s++; // Store value and find the end of it. value = s; while (*s && *s != quote) s++; if (*s) { *s++ = '\0'; } // Store only well formed attributes if (name && value) { attr[nattr++] = name; attr[nattr++] = value; } } // List terminator attr[nattr++] = 0; attr[nattr++] = 0; // Call callbacks. if (start && startelCb) (*startelCb)(ud, name, attr); if (end && endelCb) (*endelCb)(ud, name); } int nsvg__parseXML(char* input, void (*startelCb)(void* ud, const char* el, const char** attr), void (*endelCb)(void* ud, const char* el), void (*contentCb)(void* ud, const char* s), void* ud) { char* s = input; char* mark = s; int state = NSVG_XML_CONTENT; while (*s) { if (*s == '<' && state == NSVG_XML_CONTENT) { // Start of a tag *s++ = '\0'; nsvg__parseContent(mark, contentCb, ud); mark = s; state = NSVG_XML_TAG; } else if (*s == '>' && state == NSVG_XML_TAG) { // Start of a content or new tag. *s++ = '\0'; nsvg__parseElement(mark, startelCb, endelCb, ud); mark = s; state = NSVG_XML_CONTENT; } else { s++; } } return 1; } /* Simple SVG parser. */ #define NSVG_MAX_ATTR 128 enum NSVGgradientUnits { NSVG_USER_SPACE = 0, NSVG_OBJECT_SPACE = 1 }; #define NSVG_MAX_DASHES 8 enum NSVGunits { NSVG_UNITS_USER, NSVG_UNITS_PX, NSVG_UNITS_PT, NSVG_UNITS_PC, NSVG_UNITS_MM, NSVG_UNITS_CM, NSVG_UNITS_IN, NSVG_UNITS_PERCENT, NSVG_UNITS_EM, NSVG_UNITS_EX }; typedef struct NSVGcoordinate { float value; int units; } NSVGcoordinate; typedef struct NSVGlinearData { NSVGcoordinate x1, y1, x2, y2; } NSVGlinearData; typedef struct NSVGradialData { NSVGcoordinate cx, cy, r, fx, fy; } NSVGradialData; typedef struct NSVGgradientData { char id[64]; char ref[64]; signed char type; union { NSVGlinearData linear; NSVGradialData radial; }; char spread; char units; float xform[6]; int nstops; NSVGgradientStop* stops; struct NSVGgradientData* next; } NSVGgradientData; typedef struct NSVGattrib { char id[64]; float xform[6]; unsigned int fillColor; unsigned int strokeColor; float opacity; float fillOpacity; float strokeOpacity; char fillGradient[64]; char strokeGradient[64]; float strokeWidth; float strokeDashOffset; float strokeDashArray[NSVG_MAX_DASHES]; int strokeDashCount; char strokeLineJoin; char strokeLineCap; float miterLimit; char fillRule; float fontSize; unsigned int stopColor; float stopOpacity; float stopOffset; char hasFill; char hasStroke; char visible; } NSVGattrib; typedef struct NSVGparser { NSVGattrib attr[NSVG_MAX_ATTR]; int attrHead; float* pts; int npts; int cpts; NSVGpath* plist; NSVGimage* image; NSVGgradientData* gradients; NSVGshape* shapesTail; float viewMinx, viewMiny, viewWidth, viewHeight; int alignX, alignY, alignType; float dpi; char pathFlag; char defsFlag; } NSVGparser; static void nsvg__xformIdentity(float* t) { t[0] = 1.0f; t[1] = 0.0f; t[2] = 0.0f; t[3] = 1.0f; t[4] = 0.0f; t[5] = 0.0f; } static void nsvg__xformSetTranslation(float* t, float tx, float ty) { t[0] = 1.0f; t[1] = 0.0f; t[2] = 0.0f; t[3] = 1.0f; t[4] = tx; t[5] = ty; } static void nsvg__xformSetScale(float* t, float sx, float sy) { t[0] = sx; t[1] = 0.0f; t[2] = 0.0f; t[3] = sy; t[4] = 0.0f; t[5] = 0.0f; } static void nsvg__xformSetSkewX(float* t, float a) { t[0] = 1.0f; t[1] = 0.0f; t[2] = tanf(a); t[3] = 1.0f; t[4] = 0.0f; t[5] = 0.0f; } static void nsvg__xformSetSkewY(float* t, float a) { t[0] = 1.0f; t[1] = tanf(a); t[2] = 0.0f; t[3] = 1.0f; t[4] = 0.0f; t[5] = 0.0f; } static void nsvg__xformSetRotation(float* t, float a) { float cs = cosf(a), sn = sinf(a); t[0] = cs; t[1] = sn; t[2] = -sn; t[3] = cs; t[4] = 0.0f; t[5] = 0.0f; } static void nsvg__xformMultiply(float* t, float* s) { float t0 = t[0] * s[0] + t[1] * s[2]; float t2 = t[2] * s[0] + t[3] * s[2]; float t4 = t[4] * s[0] + t[5] * s[2] + s[4]; t[1] = t[0] * s[1] + t[1] * s[3]; t[3] = t[2] * s[1] + t[3] * s[3]; t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; t[0] = t0; t[2] = t2; t[4] = t4; } static void nsvg__xformInverse(float* inv, float* t) { double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1]; if (det > -1e-6 && det < 1e-6) { nsvg__xformIdentity(t); return; } invdet = 1.0 / det; inv[0] = (float)(t[3] * invdet); inv[2] = (float)(-t[2] * invdet); inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet); inv[1] = (float)(-t[1] * invdet); inv[3] = (float)(t[0] * invdet); inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet); } static void nsvg__xformPremultiply(float* t, float* s) { float s2[6]; memcpy(s2, s, sizeof(float)*6); nsvg__xformMultiply(s2, t); memcpy(t, s2, sizeof(float)*6); } static void nsvg__xformPoint(float* dx, float* dy, float x, float y, float* t) { *dx = x*t[0] + y*t[2] + t[4]; *dy = x*t[1] + y*t[3] + t[5]; } static void nsvg__xformVec(float* dx, float* dy, float x, float y, float* t) { *dx = x*t[0] + y*t[2]; *dy = x*t[1] + y*t[3]; } #define NSVG_EPSILON (1e-12) static int nsvg__ptInBounds(float* pt, float* bounds) { return pt[0] >= bounds[0] && pt[0] <= bounds[2] && pt[1] >= bounds[1] && pt[1] <= bounds[3]; } static double nsvg__evalBezier(double t, double p0, double p1, double p2, double p3) { double it = 1.0-t; return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; } static void nsvg__curveBounds(float* bounds, float* curve) { int i, j, count; double roots[2], a, b, c, b2ac, t, v; float* v0 = &curve[0]; float* v1 = &curve[2]; float* v2 = &curve[4]; float* v3 = &curve[6]; // Start the bounding box by end points bounds[0] = nsvg__minf(v0[0], v3[0]); bounds[1] = nsvg__minf(v0[1], v3[1]); bounds[2] = nsvg__maxf(v0[0], v3[0]); bounds[3] = nsvg__maxf(v0[1], v3[1]); // Bezier curve fits inside the convex hull of it's control points. // If control points are inside the bounds, we're done. if (nsvg__ptInBounds(v1, bounds) && nsvg__ptInBounds(v2, bounds)) return; // Add bezier curve inflection points in X and Y. for (i = 0; i < 2; i++) { a = -3.0 * v0[i] + 9.0 * v1[i] - 9.0 * v2[i] + 3.0 * v3[i]; b = 6.0 * v0[i] - 12.0 * v1[i] + 6.0 * v2[i]; c = 3.0 * v1[i] - 3.0 * v0[i]; count = 0; if (fabs(a) < NSVG_EPSILON) { if (fabs(b) > NSVG_EPSILON) { t = -c / b; if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) roots[count++] = t; } } else { b2ac = b*b - 4.0*c*a; if (b2ac > NSVG_EPSILON) { t = (-b + sqrt(b2ac)) / (2.0 * a); if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) roots[count++] = t; t = (-b - sqrt(b2ac)) / (2.0 * a); if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) roots[count++] = t; } } for (j = 0; j < count; j++) { v = nsvg__evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); bounds[0+i] = nsvg__minf(bounds[0+i], (float)v); bounds[2+i] = nsvg__maxf(bounds[2+i], (float)v); } } } static NSVGparser* nsvg__createParser(void) { NSVGparser* p; p = (NSVGparser*)malloc(sizeof(NSVGparser)); if (p == NULL) goto error; memset(p, 0, sizeof(NSVGparser)); p->image = (NSVGimage*)malloc(sizeof(NSVGimage)); if (p->image == NULL) goto error; memset(p->image, 0, sizeof(NSVGimage)); // Init style nsvg__xformIdentity(p->attr[0].xform); memset(p->attr[0].id, 0, sizeof p->attr[0].id); p->attr[0].fillColor = NSVG_RGB(0,0,0); p->attr[0].strokeColor = NSVG_RGB(0,0,0); p->attr[0].opacity = 1; p->attr[0].fillOpacity = 1; p->attr[0].strokeOpacity = 1; p->attr[0].stopOpacity = 1; p->attr[0].strokeWidth = 1; p->attr[0].strokeLineJoin = NSVG_JOIN_MITER; p->attr[0].strokeLineCap = NSVG_CAP_BUTT; p->attr[0].miterLimit = 4; p->attr[0].fillRule = NSVG_FILLRULE_NONZERO; p->attr[0].hasFill = 1; p->attr[0].visible = 1; return p; error: if (p) { if (p->image) free(p->image); free(p); } return NULL; } static void nsvg__deletePaths(NSVGpath* path) { while (path) { NSVGpath *next = path->next; if (path->pts != NULL) free(path->pts); free(path); path = next; } } static void nsvg__deletePaint(NSVGpaint* paint) { if (paint->type == NSVG_PAINT_LINEAR_GRADIENT || paint->type == NSVG_PAINT_RADIAL_GRADIENT) free(paint->gradient); } static void nsvg__deleteGradientData(NSVGgradientData* grad) { NSVGgradientData* next; while (grad != NULL) { next = grad->next; free(grad->stops); free(grad); grad = next; } } static void nsvg__deleteParser(NSVGparser* p) { if (p != NULL) { nsvg__deletePaths(p->plist); nsvg__deleteGradientData(p->gradients); nsvgDelete(p->image); free(p->pts); free(p); } } static void nsvg__resetPath(NSVGparser* p) { p->npts = 0; } static void nsvg__addPoint(NSVGparser* p, float x, float y) { if (p->npts+1 > p->cpts) { p->cpts = p->cpts ? p->cpts*2 : 8; p->pts = (float*)realloc(p->pts, p->cpts*2*sizeof(float)); if (!p->pts) return; } p->pts[p->npts*2+0] = x; p->pts[p->npts*2+1] = y; p->npts++; } static void nsvg__moveTo(NSVGparser* p, float x, float y) { if (p->npts > 0) { p->pts[(p->npts-1)*2+0] = x; p->pts[(p->npts-1)*2+1] = y; } else { nsvg__addPoint(p, x, y); } } static void nsvg__lineTo(NSVGparser* p, float x, float y) { float px,py, dx,dy; if (p->npts > 0) { px = p->pts[(p->npts-1)*2+0]; py = p->pts[(p->npts-1)*2+1]; dx = x - px; dy = y - py; nsvg__addPoint(p, px + dx/3.0f, py + dy/3.0f); nsvg__addPoint(p, x - dx/3.0f, y - dy/3.0f); nsvg__addPoint(p, x, y); } } static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) { if (p->npts > 0) { nsvg__addPoint(p, cpx1, cpy1); nsvg__addPoint(p, cpx2, cpy2); nsvg__addPoint(p, x, y); } } static NSVGattrib* nsvg__getAttr(NSVGparser* p) { return &p->attr[p->attrHead]; } static void nsvg__pushAttr(NSVGparser* p) { if (p->attrHead < NSVG_MAX_ATTR-1) { p->attrHead++; memcpy(&p->attr[p->attrHead], &p->attr[p->attrHead-1], sizeof(NSVGattrib)); } } static void nsvg__popAttr(NSVGparser* p) { if (p->attrHead > 0) p->attrHead--; } static float nsvg__actualOrigX(NSVGparser* p) { return p->viewMinx; } static float nsvg__actualOrigY(NSVGparser* p) { return p->viewMiny; } static float nsvg__actualWidth(NSVGparser* p) { return p->viewWidth; } static float nsvg__actualHeight(NSVGparser* p) { return p->viewHeight; } static float nsvg__actualLength(NSVGparser* p) { float w = nsvg__actualWidth(p), h = nsvg__actualHeight(p); return sqrtf(w*w + h*h) / sqrtf(2.0f); } static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length) { NSVGattrib* attr = nsvg__getAttr(p); switch (c.units) { case NSVG_UNITS_USER: return c.value; case NSVG_UNITS_PX: return c.value; case NSVG_UNITS_PT: return c.value / 72.0f * p->dpi; case NSVG_UNITS_PC: return c.value / 6.0f * p->dpi; case NSVG_UNITS_MM: return c.value / 25.4f * p->dpi; case NSVG_UNITS_CM: return c.value / 2.54f * p->dpi; case NSVG_UNITS_IN: return c.value * p->dpi; case NSVG_UNITS_EM: return c.value * attr->fontSize; case NSVG_UNITS_EX: return c.value * attr->fontSize * 0.52f; // x-height of Helvetica. case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length; default: return c.value; } return c.value; } static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) { NSVGgradientData* grad = p->gradients; if (id == NULL || *id == '\0') return NULL; while (grad != NULL) { if (strcmp(grad->id, id) == 0) return grad; grad = grad->next; } return NULL; } static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, float *xform, signed char* paintType) { NSVGgradientData* data = NULL; NSVGgradientData* ref = NULL; NSVGgradientStop* stops = NULL; NSVGgradient* grad; float ox, oy, sw, sh, sl; int nstops = 0; int refIter; data = nsvg__findGradientData(p, id); if (data == NULL) return NULL; // TODO: use ref to fill in all unset values too. ref = data; refIter = 0; while (ref != NULL) { NSVGgradientData* nextRef = NULL; if (stops == NULL && ref->stops != NULL) { stops = ref->stops; nstops = ref->nstops; break; } nextRef = nsvg__findGradientData(p, ref->ref); if (nextRef == ref) break; // prevent infite loops on malformed data ref = nextRef; refIter++; if (refIter > 32) break; // prevent infite loops on malformed data } if (stops == NULL) return NULL; grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); if (grad == NULL) return NULL; // The shape width and height. if (data->units == NSVG_OBJECT_SPACE) { ox = localBounds[0]; oy = localBounds[1]; sw = localBounds[2] - localBounds[0]; sh = localBounds[3] - localBounds[1]; } else { ox = nsvg__actualOrigX(p); oy = nsvg__actualOrigY(p); sw = nsvg__actualWidth(p); sh = nsvg__actualHeight(p); } sl = sqrtf(sw*sw + sh*sh) / sqrtf(2.0f); if (data->type == NSVG_PAINT_LINEAR_GRADIENT) { float x1, y1, x2, y2, dx, dy; x1 = nsvg__convertToPixels(p, data->linear.x1, ox, sw); y1 = nsvg__convertToPixels(p, data->linear.y1, oy, sh); x2 = nsvg__convertToPixels(p, data->linear.x2, ox, sw); y2 = nsvg__convertToPixels(p, data->linear.y2, oy, sh); // Calculate transform aligned to the line dx = x2 - x1; dy = y2 - y1; grad->xform[0] = dy; grad->xform[1] = -dx; grad->xform[2] = dx; grad->xform[3] = dy; grad->xform[4] = x1; grad->xform[5] = y1; } else { float cx, cy, fx, fy, r; cx = nsvg__convertToPixels(p, data->radial.cx, ox, sw); cy = nsvg__convertToPixels(p, data->radial.cy, oy, sh); fx = nsvg__convertToPixels(p, data->radial.fx, ox, sw); fy = nsvg__convertToPixels(p, data->radial.fy, oy, sh); r = nsvg__convertToPixels(p, data->radial.r, 0, sl); // Calculate transform aligned to the circle grad->xform[0] = r; grad->xform[1] = 0; grad->xform[2] = 0; grad->xform[3] = r; grad->xform[4] = cx; grad->xform[5] = cy; grad->fx = fx / r; grad->fy = fy / r; } nsvg__xformMultiply(grad->xform, data->xform); nsvg__xformMultiply(grad->xform, xform); grad->spread = data->spread; memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); grad->nstops = nstops; *paintType = data->type; return grad; } static float nsvg__getAverageScale(float* t) { float sx = sqrtf(t[0]*t[0] + t[2]*t[2]); float sy = sqrtf(t[1]*t[1] + t[3]*t[3]); return (sx + sy) * 0.5f; } static void nsvg__getLocalBounds(float* bounds, NSVGshape *shape, float* xform) { NSVGpath* path; float curve[4*2], curveBounds[4]; int i, first = 1; for (path = shape->paths; path != NULL; path = path->next) { nsvg__xformPoint(&curve[0], &curve[1], path->pts[0], path->pts[1], xform); for (i = 0; i < path->npts-1; i += 3) { nsvg__xformPoint(&curve[2], &curve[3], path->pts[(i+1)*2], path->pts[(i+1)*2+1], xform); nsvg__xformPoint(&curve[4], &curve[5], path->pts[(i+2)*2], path->pts[(i+2)*2+1], xform); nsvg__xformPoint(&curve[6], &curve[7], path->pts[(i+3)*2], path->pts[(i+3)*2+1], xform); nsvg__curveBounds(curveBounds, curve); if (first) { bounds[0] = curveBounds[0]; bounds[1] = curveBounds[1]; bounds[2] = curveBounds[2]; bounds[3] = curveBounds[3]; first = 0; } else { bounds[0] = nsvg__minf(bounds[0], curveBounds[0]); bounds[1] = nsvg__minf(bounds[1], curveBounds[1]); bounds[2] = nsvg__maxf(bounds[2], curveBounds[2]); bounds[3] = nsvg__maxf(bounds[3], curveBounds[3]); } curve[0] = curve[6]; curve[1] = curve[7]; } } } static void nsvg__addShape(NSVGparser* p) { NSVGattrib* attr = nsvg__getAttr(p); float scale = 1.0f; NSVGshape* shape; NSVGpath* path; int i; if (p->plist == NULL) return; shape = (NSVGshape*)malloc(sizeof(NSVGshape)); if (shape == NULL) goto error; memset(shape, 0, sizeof(NSVGshape)); memcpy(shape->id, attr->id, sizeof shape->id); memcpy(shape->fillGradient, attr->fillGradient, sizeof shape->fillGradient); memcpy(shape->strokeGradient, attr->strokeGradient, sizeof shape->strokeGradient); memcpy(shape->xform, attr->xform, sizeof shape->xform); scale = nsvg__getAverageScale(attr->xform); shape->strokeWidth = attr->strokeWidth * scale; shape->strokeDashOffset = attr->strokeDashOffset * scale; shape->strokeDashCount = (char)attr->strokeDashCount; for (i = 0; i < attr->strokeDashCount; i++) shape->strokeDashArray[i] = attr->strokeDashArray[i] * scale; shape->strokeLineJoin = attr->strokeLineJoin; shape->strokeLineCap = attr->strokeLineCap; shape->miterLimit = attr->miterLimit; shape->fillRule = attr->fillRule; shape->opacity = attr->opacity; shape->paths = p->plist; p->plist = NULL; // Calculate shape bounds shape->bounds[0] = shape->paths->bounds[0]; shape->bounds[1] = shape->paths->bounds[1]; shape->bounds[2] = shape->paths->bounds[2]; shape->bounds[3] = shape->paths->bounds[3]; for (path = shape->paths->next; path != NULL; path = path->next) { shape->bounds[0] = nsvg__minf(shape->bounds[0], path->bounds[0]); shape->bounds[1] = nsvg__minf(shape->bounds[1], path->bounds[1]); shape->bounds[2] = nsvg__maxf(shape->bounds[2], path->bounds[2]); shape->bounds[3] = nsvg__maxf(shape->bounds[3], path->bounds[3]); } // Set fill if (attr->hasFill == 0) { shape->fill.type = NSVG_PAINT_NONE; } else if (attr->hasFill == 1) { shape->fill.type = NSVG_PAINT_COLOR; shape->fill.color = attr->fillColor; shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; } else if (attr->hasFill == 2) { shape->fill.type = NSVG_PAINT_UNDEF; } // Set stroke if (attr->hasStroke == 0) { shape->stroke.type = NSVG_PAINT_NONE; } else if (attr->hasStroke == 1) { shape->stroke.type = NSVG_PAINT_COLOR; shape->stroke.color = attr->strokeColor; shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; } else if (attr->hasStroke == 2) { shape->stroke.type = NSVG_PAINT_UNDEF; } // Set flags shape->flags = (attr->visible ? NSVG_FLAGS_VISIBLE : 0x00); // Add to tail if (p->image->shapes == NULL) p->image->shapes = shape; else p->shapesTail->next = shape; p->shapesTail = shape; return; error: if (shape) free(shape); } static void nsvg__addPath(NSVGparser* p, char closed) { NSVGattrib* attr = nsvg__getAttr(p); NSVGpath* path = NULL; float bounds[4]; float* curve; int i; if (p->npts < 4) return; if (closed) nsvg__lineTo(p, p->pts[0], p->pts[1]); // Expect 1 + N*3 points (N = number of cubic bezier segments). if ((p->npts % 3) != 1) return; path = (NSVGpath*)malloc(sizeof(NSVGpath)); if (path == NULL) goto error; memset(path, 0, sizeof(NSVGpath)); path->pts = (float*)malloc(p->npts*2*sizeof(float)); if (path->pts == NULL) goto error; path->closed = closed; path->npts = p->npts; // Transform path. for (i = 0; i < p->npts; ++i) nsvg__xformPoint(&path->pts[i*2], &path->pts[i*2+1], p->pts[i*2], p->pts[i*2+1], attr->xform); // Find bounds for (i = 0; i < path->npts-1; i += 3) { curve = &path->pts[i*2]; nsvg__curveBounds(bounds, curve); if (i == 0) { path->bounds[0] = bounds[0]; path->bounds[1] = bounds[1]; path->bounds[2] = bounds[2]; path->bounds[3] = bounds[3]; } else { path->bounds[0] = nsvg__minf(path->bounds[0], bounds[0]); path->bounds[1] = nsvg__minf(path->bounds[1], bounds[1]); path->bounds[2] = nsvg__maxf(path->bounds[2], bounds[2]); path->bounds[3] = nsvg__maxf(path->bounds[3], bounds[3]); } } path->next = p->plist; p->plist = path; return; error: if (path != NULL) { if (path->pts != NULL) free(path->pts); free(path); } } // We roll our own string to float because the std library one uses locale and messes things up. static double nsvg__atof(const char* s) { char* cur = (char*)s; char* end = NULL; double res = 0.0, sign = 1.0; long long intPart = 0, fracPart = 0; char hasIntPart = 0, hasFracPart = 0; // Parse optional sign if (*cur == '+') { cur++; } else if (*cur == '-') { sign = -1; cur++; } // Parse integer part if (nsvg__isdigit(*cur)) { // Parse digit sequence intPart = strtoll(cur, &end, 10); if (cur != end) { res = (double)intPart; hasIntPart = 1; cur = end; } } // Parse fractional part. if (*cur == '.') { cur++; // Skip '.' if (nsvg__isdigit(*cur)) { // Parse digit sequence fracPart = strtoll(cur, &end, 10); if (cur != end) { res += (double)fracPart / pow(10.0, (double)(end - cur)); hasFracPart = 1; cur = end; } } } // A valid number should have integer or fractional part. if (!hasIntPart && !hasFracPart) return 0.0; // Parse optional exponent if (*cur == 'e' || *cur == 'E') { long expPart = 0; cur++; // skip 'E' expPart = strtol(cur, &end, 10); // Parse digit sequence with sign if (cur != end) { res *= pow(10.0, (double)expPart); } } return res * sign; } static const char* nsvg__parseNumber(const char* s, char* it, const int size) { const int last = size-1; int i = 0; // sign if (*s == '-' || *s == '+') { if (i < last) it[i++] = *s; s++; } // integer part while (*s && nsvg__isdigit(*s)) { if (i < last) it[i++] = *s; s++; } if (*s == '.') { // decimal point if (i < last) it[i++] = *s; s++; // fraction part while (*s && nsvg__isdigit(*s)) { if (i < last) it[i++] = *s; s++; } } // exponent if ((*s == 'e' || *s == 'E') && (s[1] != 'm' && s[1] != 'x')) { if (i < last) it[i++] = *s; s++; if (*s == '-' || *s == '+') { if (i < last) it[i++] = *s; s++; } while (*s && nsvg__isdigit(*s)) { if (i < last) it[i++] = *s; s++; } } it[i] = '\0'; return s; } static const char* nsvg__getNextPathItemWhenArcFlag(const char* s, char* it) { it[0] = '\0'; while (*s && (nsvg__isspace(*s) || *s == ',')) s++; if (!*s) return s; if (*s == '0' || *s == '1') { it[0] = *s++; it[1] = '\0'; return s; } return s; } static const char* nsvg__getNextPathItem(const char* s, char* it) { it[0] = '\0'; // Skip white spaces and commas while (*s && (nsvg__isspace(*s) || *s == ',')) s++; if (!*s) return s; if (*s == '-' || *s == '+' || *s == '.' || nsvg__isdigit(*s)) { s = nsvg__parseNumber(s, it, 64); } else { // Parse command it[0] = *s++; it[1] = '\0'; return s; } return s; } static unsigned int nsvg__parseColorHex(const char* str) { unsigned int r=0, g=0, b=0; if (sscanf(str, "#%2x%2x%2x", &r, &g, &b) == 3 ) // 2 digit hex return NSVG_RGB(r, g, b); if (sscanf(str, "#%1x%1x%1x", &r, &g, &b) == 3 ) // 1 digit hex, e.g. #abc -> 0xccbbaa return NSVG_RGB(r*17, g*17, b*17); // same effect as (r<<4|r), (g<<4|g), .. return NSVG_RGB(128, 128, 128); } // Parse rgb color. The pointer 'str' must point at "rgb(" (4+ characters). // This function returns gray (rgb(128, 128, 128) == '#808080') on parse errors // for backwards compatibility. Note: other image viewers return black instead. static unsigned int nsvg__parseColorRGB(const char* str) { int i; unsigned int rgbi[3]; float rgbf[3]; // try decimal integers first if (sscanf(str, "rgb(%u, %u, %u)", &rgbi[0], &rgbi[1], &rgbi[2]) != 3) { // integers failed, try percent values (float, locale independent) const char delimiter[3] = {',', ',', ')'}; str += 4; // skip "rgb(" for (i = 0; i < 3; i++) { while (*str && (nsvg__isspace(*str))) str++; // skip leading spaces if (*str == '+') str++; // skip '+' (don't allow '-') if (!*str) break; rgbf[i] = nsvg__atof(str); // Note 1: it would be great if nsvg__atof() returned how many // bytes it consumed but it doesn't. We need to skip the number, // the '%' character, spaces, and the delimiter ',' or ')'. // Note 2: The following code does not allow values like "33.%", // i.e. a decimal point w/o fractional part, but this is consistent // with other image viewers, e.g. firefox, chrome, eog, gimp. while (*str && nsvg__isdigit(*str)) str++; // skip integer part if (*str == '.') { str++; if (!nsvg__isdigit(*str)) break; // error: no digit after '.' while (*str && nsvg__isdigit(*str)) str++; // skip fractional part } if (*str == '%') str++; else break; while (nsvg__isspace(*str)) str++; if (*str == delimiter[i]) str++; else break; } if (i == 3) { rgbi[0] = roundf(rgbf[0] * 2.55f); rgbi[1] = roundf(rgbf[1] * 2.55f); rgbi[2] = roundf(rgbf[2] * 2.55f); } else { rgbi[0] = rgbi[1] = rgbi[2] = 128; } } // clip values as the CSS spec requires for (i = 0; i < 3; i++) { if (rgbi[i] > 255) rgbi[i] = 255; } return NSVG_RGB(rgbi[0], rgbi[1], rgbi[2]); } typedef struct NSVGNamedColor { const char* name; unsigned int color; } NSVGNamedColor; NSVGNamedColor nsvg__colors[] = { { "red", NSVG_RGB(255, 0, 0) }, { "green", NSVG_RGB( 0, 128, 0) }, { "blue", NSVG_RGB( 0, 0, 255) }, { "yellow", NSVG_RGB(255, 255, 0) }, { "cyan", NSVG_RGB( 0, 255, 255) }, { "magenta", NSVG_RGB(255, 0, 255) }, { "black", NSVG_RGB( 0, 0, 0) }, { "grey", NSVG_RGB(128, 128, 128) }, { "gray", NSVG_RGB(128, 128, 128) }, { "white", NSVG_RGB(255, 255, 255) }, #ifdef NANOSVG_ALL_COLOR_KEYWORDS { "aliceblue", NSVG_RGB(240, 248, 255) }, { "antiquewhite", NSVG_RGB(250, 235, 215) }, { "aqua", NSVG_RGB( 0, 255, 255) }, { "aquamarine", NSVG_RGB(127, 255, 212) }, { "azure", NSVG_RGB(240, 255, 255) }, { "beige", NSVG_RGB(245, 245, 220) }, { "bisque", NSVG_RGB(255, 228, 196) }, { "blanchedalmond", NSVG_RGB(255, 235, 205) }, { "blueviolet", NSVG_RGB(138, 43, 226) }, { "brown", NSVG_RGB(165, 42, 42) }, { "burlywood", NSVG_RGB(222, 184, 135) }, { "cadetblue", NSVG_RGB( 95, 158, 160) }, { "chartreuse", NSVG_RGB(127, 255, 0) }, { "chocolate", NSVG_RGB(210, 105, 30) }, { "coral", NSVG_RGB(255, 127, 80) }, { "cornflowerblue", NSVG_RGB(100, 149, 237) }, { "cornsilk", NSVG_RGB(255, 248, 220) }, { "crimson", NSVG_RGB(220, 20, 60) }, { "darkblue", NSVG_RGB( 0, 0, 139) }, { "darkcyan", NSVG_RGB( 0, 139, 139) }, { "darkgoldenrod", NSVG_RGB(184, 134, 11) }, { "darkgray", NSVG_RGB(169, 169, 169) }, { "darkgreen", NSVG_RGB( 0, 100, 0) }, { "darkgrey", NSVG_RGB(169, 169, 169) }, { "darkkhaki", NSVG_RGB(189, 183, 107) }, { "darkmagenta", NSVG_RGB(139, 0, 139) }, { "darkolivegreen", NSVG_RGB( 85, 107, 47) }, { "darkorange", NSVG_RGB(255, 140, 0) }, { "darkorchid", NSVG_RGB(153, 50, 204) }, { "darkred", NSVG_RGB(139, 0, 0) }, { "darksalmon", NSVG_RGB(233, 150, 122) }, { "darkseagreen", NSVG_RGB(143, 188, 143) }, { "darkslateblue", NSVG_RGB( 72, 61, 139) }, { "darkslategray", NSVG_RGB( 47, 79, 79) }, { "darkslategrey", NSVG_RGB( 47, 79, 79) }, { "darkturquoise", NSVG_RGB( 0, 206, 209) }, { "darkviolet", NSVG_RGB(148, 0, 211) }, { "deeppink", NSVG_RGB(255, 20, 147) }, { "deepskyblue", NSVG_RGB( 0, 191, 255) }, { "dimgray", NSVG_RGB(105, 105, 105) }, { "dimgrey", NSVG_RGB(105, 105, 105) }, { "dodgerblue", NSVG_RGB( 30, 144, 255) }, { "firebrick", NSVG_RGB(178, 34, 34) }, { "floralwhite", NSVG_RGB(255, 250, 240) }, { "forestgreen", NSVG_RGB( 34, 139, 34) }, { "fuchsia", NSVG_RGB(255, 0, 255) }, { "gainsboro", NSVG_RGB(220, 220, 220) }, { "ghostwhite", NSVG_RGB(248, 248, 255) }, { "gold", NSVG_RGB(255, 215, 0) }, { "goldenrod", NSVG_RGB(218, 165, 32) }, { "greenyellow", NSVG_RGB(173, 255, 47) }, { "honeydew", NSVG_RGB(240, 255, 240) }, { "hotpink", NSVG_RGB(255, 105, 180) }, { "indianred", NSVG_RGB(205, 92, 92) }, { "indigo", NSVG_RGB( 75, 0, 130) }, { "ivory", NSVG_RGB(255, 255, 240) }, { "khaki", NSVG_RGB(240, 230, 140) }, { "lavender", NSVG_RGB(230, 230, 250) }, { "lavenderblush", NSVG_RGB(255, 240, 245) }, { "lawngreen", NSVG_RGB(124, 252, 0) }, { "lemonchiffon", NSVG_RGB(255, 250, 205) }, { "lightblue", NSVG_RGB(173, 216, 230) }, { "lightcoral", NSVG_RGB(240, 128, 128) }, { "lightcyan", NSVG_RGB(224, 255, 255) }, { "lightgoldenrodyellow", NSVG_RGB(250, 250, 210) }, { "lightgray", NSVG_RGB(211, 211, 211) }, { "lightgreen", NSVG_RGB(144, 238, 144) }, { "lightgrey", NSVG_RGB(211, 211, 211) }, { "lightpink", NSVG_RGB(255, 182, 193) }, { "lightsalmon", NSVG_RGB(255, 160, 122) }, { "lightseagreen", NSVG_RGB( 32, 178, 170) }, { "lightskyblue", NSVG_RGB(135, 206, 250) }, { "lightslategray", NSVG_RGB(119, 136, 153) }, { "lightslategrey", NSVG_RGB(119, 136, 153) }, { "lightsteelblue", NSVG_RGB(176, 196, 222) }, { "lightyellow", NSVG_RGB(255, 255, 224) }, { "lime", NSVG_RGB( 0, 255, 0) }, { "limegreen", NSVG_RGB( 50, 205, 50) }, { "linen", NSVG_RGB(250, 240, 230) }, { "maroon", NSVG_RGB(128, 0, 0) }, { "mediumaquamarine", NSVG_RGB(102, 205, 170) }, { "mediumblue", NSVG_RGB( 0, 0, 205) }, { "mediumorchid", NSVG_RGB(186, 85, 211) }, { "mediumpurple", NSVG_RGB(147, 112, 219) }, { "mediumseagreen", NSVG_RGB( 60, 179, 113) }, { "mediumslateblue", NSVG_RGB(123, 104, 238) }, { "mediumspringgreen", NSVG_RGB( 0, 250, 154) }, { "mediumturquoise", NSVG_RGB( 72, 209, 204) }, { "mediumvioletred", NSVG_RGB(199, 21, 133) }, { "midnightblue", NSVG_RGB( 25, 25, 112) }, { "mintcream", NSVG_RGB(245, 255, 250) }, { "mistyrose", NSVG_RGB(255, 228, 225) }, { "moccasin", NSVG_RGB(255, 228, 181) }, { "navajowhite", NSVG_RGB(255, 222, 173) }, { "navy", NSVG_RGB( 0, 0, 128) }, { "oldlace", NSVG_RGB(253, 245, 230) }, { "olive", NSVG_RGB(128, 128, 0) }, { "olivedrab", NSVG_RGB(107, 142, 35) }, { "orange", NSVG_RGB(255, 165, 0) }, { "orangered", NSVG_RGB(255, 69, 0) }, { "orchid", NSVG_RGB(218, 112, 214) }, { "palegoldenrod", NSVG_RGB(238, 232, 170) }, { "palegreen", NSVG_RGB(152, 251, 152) }, { "paleturquoise", NSVG_RGB(175, 238, 238) }, { "palevioletred", NSVG_RGB(219, 112, 147) }, { "papayawhip", NSVG_RGB(255, 239, 213) }, { "peachpuff", NSVG_RGB(255, 218, 185) }, { "peru", NSVG_RGB(205, 133, 63) }, { "pink", NSVG_RGB(255, 192, 203) }, { "plum", NSVG_RGB(221, 160, 221) }, { "powderblue", NSVG_RGB(176, 224, 230) }, { "purple", NSVG_RGB(128, 0, 128) }, { "rosybrown", NSVG_RGB(188, 143, 143) }, { "royalblue", NSVG_RGB( 65, 105, 225) }, { "saddlebrown", NSVG_RGB(139, 69, 19) }, { "salmon", NSVG_RGB(250, 128, 114) }, { "sandybrown", NSVG_RGB(244, 164, 96) }, { "seagreen", NSVG_RGB( 46, 139, 87) }, { "seashell", NSVG_RGB(255, 245, 238) }, { "sienna", NSVG_RGB(160, 82, 45) }, { "silver", NSVG_RGB(192, 192, 192) }, { "skyblue", NSVG_RGB(135, 206, 235) }, { "slateblue", NSVG_RGB(106, 90, 205) }, { "slategray", NSVG_RGB(112, 128, 144) }, { "slategrey", NSVG_RGB(112, 128, 144) }, { "snow", NSVG_RGB(255, 250, 250) }, { "springgreen", NSVG_RGB( 0, 255, 127) }, { "steelblue", NSVG_RGB( 70, 130, 180) }, { "tan", NSVG_RGB(210, 180, 140) }, { "teal", NSVG_RGB( 0, 128, 128) }, { "thistle", NSVG_RGB(216, 191, 216) }, { "tomato", NSVG_RGB(255, 99, 71) }, { "turquoise", NSVG_RGB( 64, 224, 208) }, { "violet", NSVG_RGB(238, 130, 238) }, { "wheat", NSVG_RGB(245, 222, 179) }, { "whitesmoke", NSVG_RGB(245, 245, 245) }, { "yellowgreen", NSVG_RGB(154, 205, 50) }, #endif }; static unsigned int nsvg__parseColorName(const char* str) { int i, ncolors = sizeof(nsvg__colors) / sizeof(NSVGNamedColor); for (i = 0; i < ncolors; i++) { if (strcmp(nsvg__colors[i].name, str) == 0) { return nsvg__colors[i].color; } } return NSVG_RGB(128, 128, 128); } static unsigned int nsvg__parseColor(const char* str) { size_t len = 0; while(*str == ' ') ++str; len = strlen(str); if (len >= 1 && *str == '#') return nsvg__parseColorHex(str); else if (len >= 4 && str[0] == 'r' && str[1] == 'g' && str[2] == 'b' && str[3] == '(') return nsvg__parseColorRGB(str); return nsvg__parseColorName(str); } static float nsvg__parseOpacity(const char* str) { float val = nsvg__atof(str); if (val < 0.0f) val = 0.0f; if (val > 1.0f) val = 1.0f; return val; } static float nsvg__parseMiterLimit(const char* str) { float val = nsvg__atof(str); if (val < 0.0f) val = 0.0f; return val; } static int nsvg__parseUnits(const char* units) { if (units[0] == 'p' && units[1] == 'x') return NSVG_UNITS_PX; else if (units[0] == 'p' && units[1] == 't') return NSVG_UNITS_PT; else if (units[0] == 'p' && units[1] == 'c') return NSVG_UNITS_PC; else if (units[0] == 'm' && units[1] == 'm') return NSVG_UNITS_MM; else if (units[0] == 'c' && units[1] == 'm') return NSVG_UNITS_CM; else if (units[0] == 'i' && units[1] == 'n') return NSVG_UNITS_IN; else if (units[0] == '%') return NSVG_UNITS_PERCENT; else if (units[0] == 'e' && units[1] == 'm') return NSVG_UNITS_EM; else if (units[0] == 'e' && units[1] == 'x') return NSVG_UNITS_EX; return NSVG_UNITS_USER; } static int nsvg__isCoordinate(const char* s) { // optional sign if (*s == '-' || *s == '+') s++; // must have at least one digit, or start by a dot return (nsvg__isdigit(*s) || *s == '.'); } static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) { NSVGcoordinate coord = {0, NSVG_UNITS_USER}; char buf[64]; coord.units = nsvg__parseUnits(nsvg__parseNumber(str, buf, 64)); coord.value = nsvg__atof(buf); return coord; } static NSVGcoordinate nsvg__coord(float v, int units) { NSVGcoordinate coord = {v, units}; return coord; } static float nsvg__parseCoordinate(NSVGparser* p, const char* str, float orig, float length) { NSVGcoordinate coord = nsvg__parseCoordinateRaw(str); return nsvg__convertToPixels(p, coord, orig, length); } static int nsvg__parseTransformArgs(const char* str, float* args, int maxNa, int* na) { const char* end; const char* ptr; char it[64]; *na = 0; ptr = str; while (*ptr && *ptr != '(') ++ptr; if (*ptr == 0) return 1; end = ptr; while (*end && *end != ')') ++end; if (*end == 0) return 1; while (ptr < end) { if (*ptr == '-' || *ptr == '+' || *ptr == '.' || nsvg__isdigit(*ptr)) { if (*na >= maxNa) return 0; ptr = nsvg__parseNumber(ptr, it, 64); args[(*na)++] = (float)nsvg__atof(it); } else { ++ptr; } } return (int)(end - str); } static int nsvg__parseMatrix(float* xform, const char* str) { float t[6]; int na = 0; int len = nsvg__parseTransformArgs(str, t, 6, &na); if (na != 6) return len; memcpy(xform, t, sizeof(float)*6); return len; } static int nsvg__parseTranslate(float* xform, const char* str) { float args[2]; float t[6]; int na = 0; int len = nsvg__parseTransformArgs(str, args, 2, &na); if (na == 1) args[1] = 0.0; nsvg__xformSetTranslation(t, args[0], args[1]); memcpy(xform, t, sizeof(float)*6); return len; } static int nsvg__parseScale(float* xform, const char* str) { float args[2]; int na = 0; float t[6]; int len = nsvg__parseTransformArgs(str, args, 2, &na); if (na == 1) args[1] = args[0]; nsvg__xformSetScale(t, args[0], args[1]); memcpy(xform, t, sizeof(float)*6); return len; } static int nsvg__parseSkewX(float* xform, const char* str) { float args[1]; int na = 0; float t[6]; int len = nsvg__parseTransformArgs(str, args, 1, &na); nsvg__xformSetSkewX(t, args[0]/180.0f*NSVG_PI); memcpy(xform, t, sizeof(float)*6); return len; } static int nsvg__parseSkewY(float* xform, const char* str) { float args[1]; int na = 0; float t[6]; int len = nsvg__parseTransformArgs(str, args, 1, &na); nsvg__xformSetSkewY(t, args[0]/180.0f*NSVG_PI); memcpy(xform, t, sizeof(float)*6); return len; } static int nsvg__parseRotate(float* xform, const char* str) { float args[3]; int na = 0; float m[6]; float t[6]; int len = nsvg__parseTransformArgs(str, args, 3, &na); if (na == 1) args[1] = args[2] = 0.0f; nsvg__xformIdentity(m); if (na > 1) { nsvg__xformSetTranslation(t, -args[1], -args[2]); nsvg__xformMultiply(m, t); } nsvg__xformSetRotation(t, args[0]/180.0f*NSVG_PI); nsvg__xformMultiply(m, t); if (na > 1) { nsvg__xformSetTranslation(t, args[1], args[2]); nsvg__xformMultiply(m, t); } memcpy(xform, m, sizeof(float)*6); return len; } static void nsvg__parseTransform(float* xform, const char* str) { float t[6]; int len; nsvg__xformIdentity(xform); while (*str) { if (strncmp(str, "matrix", 6) == 0) len = nsvg__parseMatrix(t, str); else if (strncmp(str, "translate", 9) == 0) len = nsvg__parseTranslate(t, str); else if (strncmp(str, "scale", 5) == 0) len = nsvg__parseScale(t, str); else if (strncmp(str, "rotate", 6) == 0) len = nsvg__parseRotate(t, str); else if (strncmp(str, "skewX", 5) == 0) len = nsvg__parseSkewX(t, str); else if (strncmp(str, "skewY", 5) == 0) len = nsvg__parseSkewY(t, str); else{ ++str; continue; } if (len != 0) { str += len; } else { ++str; continue; } nsvg__xformPremultiply(xform, t); } } static void nsvg__parseUrl(char* id, const char* str) { int i = 0; str += 4; // "url("; if (*str && *str == '#') str++; while (i < 63 && *str && *str != ')') { id[i] = *str++; i++; } id[i] = '\0'; } static char nsvg__parseLineCap(const char* str) { if (strcmp(str, "butt") == 0) return NSVG_CAP_BUTT; else if (strcmp(str, "round") == 0) return NSVG_CAP_ROUND; else if (strcmp(str, "square") == 0) return NSVG_CAP_SQUARE; // TODO: handle inherit. return NSVG_CAP_BUTT; } static char nsvg__parseLineJoin(const char* str) { if (strcmp(str, "miter") == 0) return NSVG_JOIN_MITER; else if (strcmp(str, "round") == 0) return NSVG_JOIN_ROUND; else if (strcmp(str, "bevel") == 0) return NSVG_JOIN_BEVEL; // TODO: handle inherit. return NSVG_JOIN_MITER; } static char nsvg__parseFillRule(const char* str) { if (strcmp(str, "nonzero") == 0) return NSVG_FILLRULE_NONZERO; else if (strcmp(str, "evenodd") == 0) return NSVG_FILLRULE_EVENODD; // TODO: handle inherit. return NSVG_FILLRULE_NONZERO; } static const char* nsvg__getNextDashItem(const char* s, char* it) { int n = 0; it[0] = '\0'; // Skip white spaces and commas while (*s && (nsvg__isspace(*s) || *s == ',')) s++; // Advance until whitespace, comma or end. while (*s && (!nsvg__isspace(*s) && *s != ',')) { if (n < 63) it[n++] = *s; s++; } it[n++] = '\0'; return s; } static int nsvg__parseStrokeDashArray(NSVGparser* p, const char* str, float* strokeDashArray) { char item[64]; int count = 0, i; float sum = 0.0f; // Handle "none" if (str[0] == 'n') return 0; // Parse dashes while (*str) { str = nsvg__getNextDashItem(str, item); if (!*item) break; if (count < NSVG_MAX_DASHES) strokeDashArray[count++] = fabsf(nsvg__parseCoordinate(p, item, 0.0f, nsvg__actualLength(p))); } for (i = 0; i < count; i++) sum += strokeDashArray[i]; if (sum <= 1e-6f) count = 0; return count; } static void nsvg__parseStyle(NSVGparser* p, const char* str); static int nsvg__parseAttr(NSVGparser* p, const char* name, const char* value) { float xform[6]; NSVGattrib* attr = nsvg__getAttr(p); if (!attr) return 0; if (strcmp(name, "style") == 0) { nsvg__parseStyle(p, value); } else if (strcmp(name, "display") == 0) { if (strcmp(value, "none") == 0) attr->visible = 0; // Don't reset ->visible on display:inline, one display:none hides the whole subtree } else if (strcmp(name, "fill") == 0) { if (strcmp(value, "none") == 0) { attr->hasFill = 0; } else if (strncmp(value, "url(", 4) == 0) { attr->hasFill = 2; nsvg__parseUrl(attr->fillGradient, value); } else { attr->hasFill = 1; attr->fillColor = nsvg__parseColor(value); } } else if (strcmp(name, "opacity") == 0) { attr->opacity = nsvg__parseOpacity(value); } else if (strcmp(name, "fill-opacity") == 0) { attr->fillOpacity = nsvg__parseOpacity(value); } else if (strcmp(name, "stroke") == 0) { if (strcmp(value, "none") == 0) { attr->hasStroke = 0; } else if (strncmp(value, "url(", 4) == 0) { attr->hasStroke = 2; nsvg__parseUrl(attr->strokeGradient, value); } else { attr->hasStroke = 1; attr->strokeColor = nsvg__parseColor(value); } } else if (strcmp(name, "stroke-width") == 0) { attr->strokeWidth = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); } else if (strcmp(name, "stroke-dasharray") == 0) { attr->strokeDashCount = nsvg__parseStrokeDashArray(p, value, attr->strokeDashArray); } else if (strcmp(name, "stroke-dashoffset") == 0) { attr->strokeDashOffset = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); } else if (strcmp(name, "stroke-opacity") == 0) { attr->strokeOpacity = nsvg__parseOpacity(value); } else if (strcmp(name, "stroke-linecap") == 0) { attr->strokeLineCap = nsvg__parseLineCap(value); } else if (strcmp(name, "stroke-linejoin") == 0) { attr->strokeLineJoin = nsvg__parseLineJoin(value); } else if (strcmp(name, "stroke-miterlimit") == 0) { attr->miterLimit = nsvg__parseMiterLimit(value); } else if (strcmp(name, "fill-rule") == 0) { attr->fillRule = nsvg__parseFillRule(value); } else if (strcmp(name, "font-size") == 0) { attr->fontSize = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); } else if (strcmp(name, "transform") == 0) { nsvg__parseTransform(xform, value); nsvg__xformPremultiply(attr->xform, xform); } else if (strcmp(name, "stop-color") == 0) { attr->stopColor = nsvg__parseColor(value); } else if (strcmp(name, "stop-opacity") == 0) { attr->stopOpacity = nsvg__parseOpacity(value); } else if (strcmp(name, "offset") == 0) { attr->stopOffset = nsvg__parseCoordinate(p, value, 0.0f, 1.0f); } else if (strcmp(name, "id") == 0) { strncpy(attr->id, value, 63); attr->id[63] = '\0'; } else { return 0; } return 1; } static int nsvg__parseNameValue(NSVGparser* p, const char* start, const char* end) { const char* str; const char* val; char name[512]; char value[512]; int n; str = start; while (str < end && *str != ':') ++str; val = str; // Right Trim while (str > start && (*str == ':' || nsvg__isspace(*str))) --str; ++str; n = (int)(str - start); if (n > 511) n = 511; if (n) memcpy(name, start, n); name[n] = 0; while (val < end && (*val == ':' || nsvg__isspace(*val))) ++val; n = (int)(end - val); if (n > 511) n = 511; if (n) memcpy(value, val, n); value[n] = 0; return nsvg__parseAttr(p, name, value); } static void nsvg__parseStyle(NSVGparser* p, const char* str) { const char* start; const char* end; while (*str) { // Left Trim while(*str && nsvg__isspace(*str)) ++str; start = str; while(*str && *str != ';') ++str; end = str; // Right Trim while (end > start && (*end == ';' || nsvg__isspace(*end))) --end; ++end; nsvg__parseNameValue(p, start, end); if (*str) ++str; } } static void nsvg__parseAttribs(NSVGparser* p, const char** attr) { int i; for (i = 0; attr[i]; i += 2) { if (strcmp(attr[i], "style") == 0) nsvg__parseStyle(p, attr[i + 1]); else nsvg__parseAttr(p, attr[i], attr[i + 1]); } } static int nsvg__getArgsPerElement(char cmd) { switch (cmd) { case 'v': case 'V': case 'h': case 'H': return 1; case 'm': case 'M': case 'l': case 'L': case 't': case 'T': return 2; case 'q': case 'Q': case 's': case 'S': return 4; case 'c': case 'C': return 6; case 'a': case 'A': return 7; case 'z': case 'Z': return 0; } return -1; } static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) { if (rel) { *cpx += args[0]; *cpy += args[1]; } else { *cpx = args[0]; *cpy = args[1]; } nsvg__moveTo(p, *cpx, *cpy); } static void nsvg__pathLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) { if (rel) { *cpx += args[0]; *cpy += args[1]; } else { *cpx = args[0]; *cpy = args[1]; } nsvg__lineTo(p, *cpx, *cpy); } static void nsvg__pathHLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) { if (rel) *cpx += args[0]; else *cpx = args[0]; nsvg__lineTo(p, *cpx, *cpy); } static void nsvg__pathVLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) { if (rel) *cpy += args[0]; else *cpy = args[0]; nsvg__lineTo(p, *cpx, *cpy); } static void nsvg__pathCubicBezTo(NSVGparser* p, float* cpx, float* cpy, float* cpx2, float* cpy2, float* args, int rel) { float x2, y2, cx1, cy1, cx2, cy2; if (rel) { cx1 = *cpx + args[0]; cy1 = *cpy + args[1]; cx2 = *cpx + args[2]; cy2 = *cpy + args[3]; x2 = *cpx + args[4]; y2 = *cpy + args[5]; } else { cx1 = args[0]; cy1 = args[1]; cx2 = args[2]; cy2 = args[3]; x2 = args[4]; y2 = args[5]; } nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); *cpx2 = cx2; *cpy2 = cy2; *cpx = x2; *cpy = y2; } static void nsvg__pathCubicBezShortTo(NSVGparser* p, float* cpx, float* cpy, float* cpx2, float* cpy2, float* args, int rel) { float x1, y1, x2, y2, cx1, cy1, cx2, cy2; x1 = *cpx; y1 = *cpy; if (rel) { cx2 = *cpx + args[0]; cy2 = *cpy + args[1]; x2 = *cpx + args[2]; y2 = *cpy + args[3]; } else { cx2 = args[0]; cy2 = args[1]; x2 = args[2]; y2 = args[3]; } cx1 = 2*x1 - *cpx2; cy1 = 2*y1 - *cpy2; nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); *cpx2 = cx2; *cpy2 = cy2; *cpx = x2; *cpy = y2; } static void nsvg__pathQuadBezTo(NSVGparser* p, float* cpx, float* cpy, float* cpx2, float* cpy2, float* args, int rel) { float x1, y1, x2, y2, cx, cy; float cx1, cy1, cx2, cy2; x1 = *cpx; y1 = *cpy; if (rel) { cx = *cpx + args[0]; cy = *cpy + args[1]; x2 = *cpx + args[2]; y2 = *cpy + args[3]; } else { cx = args[0]; cy = args[1]; x2 = args[2]; y2 = args[3]; } // Convert to cubic bezier cx1 = x1 + 2.0f/3.0f*(cx - x1); cy1 = y1 + 2.0f/3.0f*(cy - y1); cx2 = x2 + 2.0f/3.0f*(cx - x2); cy2 = y2 + 2.0f/3.0f*(cy - y2); nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); *cpx2 = cx; *cpy2 = cy; *cpx = x2; *cpy = y2; } static void nsvg__pathQuadBezShortTo(NSVGparser* p, float* cpx, float* cpy, float* cpx2, float* cpy2, float* args, int rel) { float x1, y1, x2, y2, cx, cy; float cx1, cy1, cx2, cy2; x1 = *cpx; y1 = *cpy; if (rel) { x2 = *cpx + args[0]; y2 = *cpy + args[1]; } else { x2 = args[0]; y2 = args[1]; } cx = 2*x1 - *cpx2; cy = 2*y1 - *cpy2; // Convert to cubix bezier cx1 = x1 + 2.0f/3.0f*(cx - x1); cy1 = y1 + 2.0f/3.0f*(cy - y1); cx2 = x2 + 2.0f/3.0f*(cx - x2); cy2 = y2 + 2.0f/3.0f*(cy - y2); nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); *cpx2 = cx; *cpy2 = cy; *cpx = x2; *cpy = y2; } static float nsvg__sqr(float x) { return x*x; } static float nsvg__vmag(float x, float y) { return sqrtf(x*x + y*y); } static float nsvg__vecrat(float ux, float uy, float vx, float vy) { return (ux*vx + uy*vy) / (nsvg__vmag(ux,uy) * nsvg__vmag(vx,vy)); } static float nsvg__vecang(float ux, float uy, float vx, float vy) { float r = nsvg__vecrat(ux,uy, vx,vy); if (r < -1.0f) r = -1.0f; if (r > 1.0f) r = 1.0f; return ((ux*vy < uy*vx) ? -1.0f : 1.0f) * acosf(r); } static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) { // Ported from canvg (https://code.google.com/p/canvg/) float rx, ry, rotx; float x1, y1, x2, y2, cx, cy, dx, dy, d; float x1p, y1p, cxp, cyp, s, sa, sb; float ux, uy, vx, vy, a1, da; float x, y, tanx, tany, a, px = 0, py = 0, ptanx = 0, ptany = 0, t[6]; float sinrx, cosrx; int fa, fs; int i, ndivs; float hda, kappa; rx = fabsf(args[0]); // y radius ry = fabsf(args[1]); // x radius rotx = args[2] / 180.0f * NSVG_PI; // x rotation angle fa = fabsf(args[3]) > 1e-6 ? 1 : 0; // Large arc fs = fabsf(args[4]) > 1e-6 ? 1 : 0; // Sweep direction x1 = *cpx; // start point y1 = *cpy; if (rel) { // end point x2 = *cpx + args[5]; y2 = *cpy + args[6]; } else { x2 = args[5]; y2 = args[6]; } dx = x1 - x2; dy = y1 - y2; d = sqrtf(dx*dx + dy*dy); if (d < 1e-6f || rx < 1e-6f || ry < 1e-6f) { // The arc degenerates to a line nsvg__lineTo(p, x2, y2); *cpx = x2; *cpy = y2; return; } sinrx = sinf(rotx); cosrx = cosf(rotx); // Convert to center point parameterization. // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes // 1) Compute x1', y1' x1p = cosrx * dx / 2.0f + sinrx * dy / 2.0f; y1p = -sinrx * dx / 2.0f + cosrx * dy / 2.0f; d = nsvg__sqr(x1p)/nsvg__sqr(rx) + nsvg__sqr(y1p)/nsvg__sqr(ry); if (d > 1) { d = sqrtf(d); rx *= d; ry *= d; } // 2) Compute cx', cy' s = 0.0f; sa = nsvg__sqr(rx)*nsvg__sqr(ry) - nsvg__sqr(rx)*nsvg__sqr(y1p) - nsvg__sqr(ry)*nsvg__sqr(x1p); sb = nsvg__sqr(rx)*nsvg__sqr(y1p) + nsvg__sqr(ry)*nsvg__sqr(x1p); if (sa < 0.0f) sa = 0.0f; if (sb > 0.0f) s = sqrtf(sa / sb); if (fa == fs) s = -s; cxp = s * rx * y1p / ry; cyp = s * -ry * x1p / rx; // 3) Compute cx,cy from cx',cy' cx = (x1 + x2)/2.0f + cosrx*cxp - sinrx*cyp; cy = (y1 + y2)/2.0f + sinrx*cxp + cosrx*cyp; // 4) Calculate theta1, and delta theta. ux = (x1p - cxp) / rx; uy = (y1p - cyp) / ry; vx = (-x1p - cxp) / rx; vy = (-y1p - cyp) / ry; a1 = nsvg__vecang(1.0f,0.0f, ux,uy); // Initial angle da = nsvg__vecang(ux,uy, vx,vy); // Delta angle // if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; // if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; if (fs == 0 && da > 0) da -= 2 * NSVG_PI; else if (fs == 1 && da < 0) da += 2 * NSVG_PI; // Approximate the arc using cubic spline segments. t[0] = cosrx; t[1] = sinrx; t[2] = -sinrx; t[3] = cosrx; t[4] = cx; t[5] = cy; // Split arc into max 90 degree segments. // The loop assumes an iteration per end point (including start and end), this +1. ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); hda = (da / (float)ndivs) / 2.0f; // Fix for ticket #179: division by 0: avoid cotangens around 0 (infinite) if ((hda < 1e-3f) && (hda > -1e-3f)) hda *= 0.5f; else hda = (1.0f - cosf(hda)) / sinf(hda); kappa = fabsf(4.0f / 3.0f * hda); if (da < 0.0f) kappa = -kappa; for (i = 0; i <= ndivs; i++) { a = a1 + da * ((float)i/(float)ndivs); dx = cosf(a); dy = sinf(a); nsvg__xformPoint(&x, &y, dx*rx, dy*ry, t); // position nsvg__xformVec(&tanx, &tany, -dy*rx * kappa, dx*ry * kappa, t); // tangent if (i > 0) nsvg__cubicBezTo(p, px+ptanx,py+ptany, x-tanx, y-tany, x, y); px = x; py = y; ptanx = tanx; ptany = tany; } *cpx = x2; *cpy = y2; } static void nsvg__parsePath(NSVGparser* p, const char** attr) { const char* s = NULL; char cmd = '\0'; float args[10]; int nargs; int rargs = 0; char initPoint; float cpx, cpy, cpx2, cpy2; const char* tmp[4]; char closedFlag; int i; char item[64]; for (i = 0; attr[i]; i += 2) { if (strcmp(attr[i], "d") == 0) { s = attr[i + 1]; } else { tmp[0] = attr[i]; tmp[1] = attr[i + 1]; tmp[2] = 0; tmp[3] = 0; nsvg__parseAttribs(p, tmp); } } if (s) { nsvg__resetPath(p); cpx = 0; cpy = 0; cpx2 = 0; cpy2 = 0; initPoint = 0; closedFlag = 0; nargs = 0; while (*s) { item[0] = '\0'; if ((cmd == 'A' || cmd == 'a') && (nargs == 3 || nargs == 4)) s = nsvg__getNextPathItemWhenArcFlag(s, item); if (!*item) s = nsvg__getNextPathItem(s, item); if (!*item) break; if (cmd != '\0' && nsvg__isCoordinate(item)) { if (nargs < 10) args[nargs++] = (float)nsvg__atof(item); if (nargs >= rargs) { switch (cmd) { case 'm': case 'M': nsvg__pathMoveTo(p, &cpx, &cpy, args, cmd == 'm' ? 1 : 0); // Moveto can be followed by multiple coordinate pairs, // which should be treated as linetos. cmd = (cmd == 'm') ? 'l' : 'L'; rargs = nsvg__getArgsPerElement(cmd); cpx2 = cpx; cpy2 = cpy; initPoint = 1; break; case 'l': case 'L': nsvg__pathLineTo(p, &cpx, &cpy, args, cmd == 'l' ? 1 : 0); cpx2 = cpx; cpy2 = cpy; break; case 'H': case 'h': nsvg__pathHLineTo(p, &cpx, &cpy, args, cmd == 'h' ? 1 : 0); cpx2 = cpx; cpy2 = cpy; break; case 'V': case 'v': nsvg__pathVLineTo(p, &cpx, &cpy, args, cmd == 'v' ? 1 : 0); cpx2 = cpx; cpy2 = cpy; break; case 'C': case 'c': nsvg__pathCubicBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'c' ? 1 : 0); break; case 'S': case 's': nsvg__pathCubicBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 's' ? 1 : 0); break; case 'Q': case 'q': nsvg__pathQuadBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'q' ? 1 : 0); break; case 'T': case 't': nsvg__pathQuadBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 't' ? 1 : 0); break; case 'A': case 'a': nsvg__pathArcTo(p, &cpx, &cpy, args, cmd == 'a' ? 1 : 0); cpx2 = cpx; cpy2 = cpy; break; default: if (nargs >= 2) { cpx = args[nargs-2]; cpy = args[nargs-1]; cpx2 = cpx; cpy2 = cpy; } break; } nargs = 0; } } else { cmd = item[0]; if (cmd == 'M' || cmd == 'm') { // Commit path. if (p->npts > 0) nsvg__addPath(p, closedFlag); // Start new subpath. nsvg__resetPath(p); closedFlag = 0; nargs = 0; } else if (initPoint == 0) { // Do not allow other commands until initial point has been set (moveTo called once). cmd = '\0'; } if (cmd == 'Z' || cmd == 'z') { closedFlag = 1; // Commit path. if (p->npts > 0) { // Move current point to first point cpx = p->pts[0]; cpy = p->pts[1]; cpx2 = cpx; cpy2 = cpy; nsvg__addPath(p, closedFlag); } // Start new subpath. nsvg__resetPath(p); nsvg__moveTo(p, cpx, cpy); closedFlag = 0; nargs = 0; } rargs = nsvg__getArgsPerElement(cmd); if (rargs == -1) { // Command not recognized cmd = '\0'; rargs = 0; } } } // Commit path. if (p->npts) nsvg__addPath(p, closedFlag); } nsvg__addShape(p); } static void nsvg__parseRect(NSVGparser* p, const char** attr) { float x = 0.0f; float y = 0.0f; float w = 0.0f; float h = 0.0f; float rx = -1.0f; // marks not set float ry = -1.0f; int i; for (i = 0; attr[i]; i += 2) { if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "x") == 0) x = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); if (strcmp(attr[i], "y") == 0) y = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); if (strcmp(attr[i], "width") == 0) w = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p)); if (strcmp(attr[i], "height") == 0) h = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p)); if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); } } if (rx < 0.0f && ry > 0.0f) rx = ry; if (ry < 0.0f && rx > 0.0f) ry = rx; if (rx < 0.0f) rx = 0.0f; if (ry < 0.0f) ry = 0.0f; if (rx > w/2.0f) rx = w/2.0f; if (ry > h/2.0f) ry = h/2.0f; if (w != 0.0f && h != 0.0f) { nsvg__resetPath(p); if (rx < 0.00001f || ry < 0.0001f) { nsvg__moveTo(p, x, y); nsvg__lineTo(p, x+w, y); nsvg__lineTo(p, x+w, y+h); nsvg__lineTo(p, x, y+h); } else { // Rounded rectangle nsvg__moveTo(p, x+rx, y); nsvg__lineTo(p, x+w-rx, y); nsvg__cubicBezTo(p, x+w-rx*(1-NSVG_KAPPA90), y, x+w, y+ry*(1-NSVG_KAPPA90), x+w, y+ry); nsvg__lineTo(p, x+w, y+h-ry); nsvg__cubicBezTo(p, x+w, y+h-ry*(1-NSVG_KAPPA90), x+w-rx*(1-NSVG_KAPPA90), y+h, x+w-rx, y+h); nsvg__lineTo(p, x+rx, y+h); nsvg__cubicBezTo(p, x+rx*(1-NSVG_KAPPA90), y+h, x, y+h-ry*(1-NSVG_KAPPA90), x, y+h-ry); nsvg__lineTo(p, x, y+ry); nsvg__cubicBezTo(p, x, y+ry*(1-NSVG_KAPPA90), x+rx*(1-NSVG_KAPPA90), y, x+rx, y); } nsvg__addPath(p, 1); nsvg__addShape(p); } } static void nsvg__parseCircle(NSVGparser* p, const char** attr) { float cx = 0.0f; float cy = 0.0f; float r = 0.0f; int i; for (i = 0; attr[i]; i += 2) { if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); if (strcmp(attr[i], "r") == 0) r = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualLength(p))); } } if (r > 0.0f) { nsvg__resetPath(p); nsvg__moveTo(p, cx+r, cy); nsvg__cubicBezTo(p, cx+r, cy+r*NSVG_KAPPA90, cx+r*NSVG_KAPPA90, cy+r, cx, cy+r); nsvg__cubicBezTo(p, cx-r*NSVG_KAPPA90, cy+r, cx-r, cy+r*NSVG_KAPPA90, cx-r, cy); nsvg__cubicBezTo(p, cx-r, cy-r*NSVG_KAPPA90, cx-r*NSVG_KAPPA90, cy-r, cx, cy-r); nsvg__cubicBezTo(p, cx+r*NSVG_KAPPA90, cy-r, cx+r, cy-r*NSVG_KAPPA90, cx+r, cy); nsvg__addPath(p, 1); nsvg__addShape(p); } } static void nsvg__parseEllipse(NSVGparser* p, const char** attr) { float cx = 0.0f; float cy = 0.0f; float rx = 0.0f; float ry = 0.0f; int i; for (i = 0; attr[i]; i += 2) { if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); } } if (rx > 0.0f && ry > 0.0f) { nsvg__resetPath(p); nsvg__moveTo(p, cx+rx, cy); nsvg__cubicBezTo(p, cx+rx, cy+ry*NSVG_KAPPA90, cx+rx*NSVG_KAPPA90, cy+ry, cx, cy+ry); nsvg__cubicBezTo(p, cx-rx*NSVG_KAPPA90, cy+ry, cx-rx, cy+ry*NSVG_KAPPA90, cx-rx, cy); nsvg__cubicBezTo(p, cx-rx, cy-ry*NSVG_KAPPA90, cx-rx*NSVG_KAPPA90, cy-ry, cx, cy-ry); nsvg__cubicBezTo(p, cx+rx*NSVG_KAPPA90, cy-ry, cx+rx, cy-ry*NSVG_KAPPA90, cx+rx, cy); nsvg__addPath(p, 1); nsvg__addShape(p); } } static void nsvg__parseLine(NSVGparser* p, const char** attr) { float x1 = 0.0; float y1 = 0.0; float x2 = 0.0; float y2 = 0.0; int i; for (i = 0; attr[i]; i += 2) { if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "x1") == 0) x1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); if (strcmp(attr[i], "y1") == 0) y1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); if (strcmp(attr[i], "x2") == 0) x2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); if (strcmp(attr[i], "y2") == 0) y2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); } } nsvg__resetPath(p); nsvg__moveTo(p, x1, y1); nsvg__lineTo(p, x2, y2); nsvg__addPath(p, 0); nsvg__addShape(p); } static void nsvg__parsePoly(NSVGparser* p, const char** attr, int closeFlag) { int i; const char* s; float args[2]; int nargs, npts = 0; char item[64]; nsvg__resetPath(p); for (i = 0; attr[i]; i += 2) { if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "points") == 0) { s = attr[i + 1]; nargs = 0; while (*s) { s = nsvg__getNextPathItem(s, item); args[nargs++] = (float)nsvg__atof(item); if (nargs >= 2) { if (npts == 0) nsvg__moveTo(p, args[0], args[1]); else nsvg__lineTo(p, args[0], args[1]); nargs = 0; npts++; } } } } } nsvg__addPath(p, (char)closeFlag); nsvg__addShape(p); } static void nsvg__parseSVG(NSVGparser* p, const char** attr) { int i; for (i = 0; attr[i]; i += 2) { if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "width") == 0) { p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); } else if (strcmp(attr[i], "height") == 0) { p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); } else if (strcmp(attr[i], "viewBox") == 0) { const char *s = attr[i + 1]; char buf[64]; s = nsvg__parseNumber(s, buf, 64); p->viewMinx = nsvg__atof(buf); while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; if (!*s) return; s = nsvg__parseNumber(s, buf, 64); p->viewMiny = nsvg__atof(buf); while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; if (!*s) return; s = nsvg__parseNumber(s, buf, 64); p->viewWidth = nsvg__atof(buf); while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; if (!*s) return; s = nsvg__parseNumber(s, buf, 64); p->viewHeight = nsvg__atof(buf); } else if (strcmp(attr[i], "preserveAspectRatio") == 0) { if (strstr(attr[i + 1], "none") != 0) { // No uniform scaling p->alignType = NSVG_ALIGN_NONE; } else { // Parse X align if (strstr(attr[i + 1], "xMin") != 0) p->alignX = NSVG_ALIGN_MIN; else if (strstr(attr[i + 1], "xMid") != 0) p->alignX = NSVG_ALIGN_MID; else if (strstr(attr[i + 1], "xMax") != 0) p->alignX = NSVG_ALIGN_MAX; // Parse X align if (strstr(attr[i + 1], "yMin") != 0) p->alignY = NSVG_ALIGN_MIN; else if (strstr(attr[i + 1], "yMid") != 0) p->alignY = NSVG_ALIGN_MID; else if (strstr(attr[i + 1], "yMax") != 0) p->alignY = NSVG_ALIGN_MAX; // Parse meet/slice p->alignType = NSVG_ALIGN_MEET; if (strstr(attr[i + 1], "slice") != 0) p->alignType = NSVG_ALIGN_SLICE; } } } } } static void nsvg__parseGradient(NSVGparser* p, const char** attr, signed char type) { int i; NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); if (grad == NULL) return; memset(grad, 0, sizeof(NSVGgradientData)); grad->units = NSVG_OBJECT_SPACE; grad->type = type; if (grad->type == NSVG_PAINT_LINEAR_GRADIENT) { grad->linear.x1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); grad->linear.y1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); grad->linear.x2 = nsvg__coord(100.0f, NSVG_UNITS_PERCENT); grad->linear.y2 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); } else if (grad->type == NSVG_PAINT_RADIAL_GRADIENT) { grad->radial.cx = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); grad->radial.cy = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); grad->radial.r = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); } nsvg__xformIdentity(grad->xform); for (i = 0; attr[i]; i += 2) { if (strcmp(attr[i], "id") == 0) { strncpy(grad->id, attr[i+1], 63); grad->id[63] = '\0'; } else if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { if (strcmp(attr[i], "gradientUnits") == 0) { if (strcmp(attr[i+1], "objectBoundingBox") == 0) grad->units = NSVG_OBJECT_SPACE; else grad->units = NSVG_USER_SPACE; } else if (strcmp(attr[i], "gradientTransform") == 0) { nsvg__parseTransform(grad->xform, attr[i + 1]); } else if (strcmp(attr[i], "cx") == 0) { grad->radial.cx = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "cy") == 0) { grad->radial.cy = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "r") == 0) { grad->radial.r = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "fx") == 0) { grad->radial.fx = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "fy") == 0) { grad->radial.fy = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "x1") == 0) { grad->linear.x1 = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "y1") == 0) { grad->linear.y1 = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "x2") == 0) { grad->linear.x2 = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "y2") == 0) { grad->linear.y2 = nsvg__parseCoordinateRaw(attr[i + 1]); } else if (strcmp(attr[i], "spreadMethod") == 0) { if (strcmp(attr[i+1], "pad") == 0) grad->spread = NSVG_SPREAD_PAD; else if (strcmp(attr[i+1], "reflect") == 0) grad->spread = NSVG_SPREAD_REFLECT; else if (strcmp(attr[i+1], "repeat") == 0) grad->spread = NSVG_SPREAD_REPEAT; } else if (strcmp(attr[i], "xlink:href") == 0) { const char *href = attr[i+1]; strncpy(grad->ref, href+1, 62); grad->ref[62] = '\0'; } } } grad->next = p->gradients; p->gradients = grad; } static void nsvg__parseGradientStop(NSVGparser* p, const char** attr) { NSVGattrib* curAttr = nsvg__getAttr(p); NSVGgradientData* grad; NSVGgradientStop* stop; int i, idx; curAttr->stopOffset = 0; curAttr->stopColor = 0; curAttr->stopOpacity = 1.0f; for (i = 0; attr[i]; i += 2) { nsvg__parseAttr(p, attr[i], attr[i + 1]); } // Add stop to the last gradient. grad = p->gradients; if (grad == NULL) return; grad->nstops++; grad->stops = (NSVGgradientStop*)realloc(grad->stops, sizeof(NSVGgradientStop)*grad->nstops); if (grad->stops == NULL) return; // Insert idx = grad->nstops-1; for (i = 0; i < grad->nstops-1; i++) { if (curAttr->stopOffset < grad->stops[i].offset) { idx = i; break; } } if (idx != grad->nstops-1) { for (i = grad->nstops-1; i > idx; i--) grad->stops[i] = grad->stops[i-1]; } stop = &grad->stops[idx]; stop->color = curAttr->stopColor; stop->color |= (unsigned int)(curAttr->stopOpacity*255) << 24; stop->offset = curAttr->stopOffset; } static void nsvg__startElement(void* ud, const char* el, const char** attr) { NSVGparser* p = (NSVGparser*)ud; if (p->defsFlag) { // Skip everything but gradients in defs if (strcmp(el, "linearGradient") == 0) { nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); } else if (strcmp(el, "radialGradient") == 0) { nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); } else if (strcmp(el, "stop") == 0) { nsvg__parseGradientStop(p, attr); } return; } if (strcmp(el, "g") == 0) { nsvg__pushAttr(p); nsvg__parseAttribs(p, attr); } else if (strcmp(el, "path") == 0) { if (p->pathFlag) // Do not allow nested paths. return; nsvg__pushAttr(p); nsvg__parsePath(p, attr); nsvg__popAttr(p); } else if (strcmp(el, "rect") == 0) { nsvg__pushAttr(p); nsvg__parseRect(p, attr); nsvg__popAttr(p); } else if (strcmp(el, "circle") == 0) { nsvg__pushAttr(p); nsvg__parseCircle(p, attr); nsvg__popAttr(p); } else if (strcmp(el, "ellipse") == 0) { nsvg__pushAttr(p); nsvg__parseEllipse(p, attr); nsvg__popAttr(p); } else if (strcmp(el, "line") == 0) { nsvg__pushAttr(p); nsvg__parseLine(p, attr); nsvg__popAttr(p); } else if (strcmp(el, "polyline") == 0) { nsvg__pushAttr(p); nsvg__parsePoly(p, attr, 0); nsvg__popAttr(p); } else if (strcmp(el, "polygon") == 0) { nsvg__pushAttr(p); nsvg__parsePoly(p, attr, 1); nsvg__popAttr(p); } else if (strcmp(el, "linearGradient") == 0) { nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); } else if (strcmp(el, "radialGradient") == 0) { nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); } else if (strcmp(el, "stop") == 0) { nsvg__parseGradientStop(p, attr); } else if (strcmp(el, "defs") == 0) { p->defsFlag = 1; } else if (strcmp(el, "svg") == 0) { nsvg__parseSVG(p, attr); } } static void nsvg__endElement(void* ud, const char* el) { NSVGparser* p = (NSVGparser*)ud; if (strcmp(el, "g") == 0) { nsvg__popAttr(p); } else if (strcmp(el, "path") == 0) { p->pathFlag = 0; } else if (strcmp(el, "defs") == 0) { p->defsFlag = 0; } } static void nsvg__content(void* ud, const char* s) { NSVG_NOTUSED(ud); NSVG_NOTUSED(s); // empty } static void nsvg__imageBounds(NSVGparser* p, float* bounds) { NSVGshape* shape; shape = p->image->shapes; if (shape == NULL) { bounds[0] = bounds[1] = bounds[2] = bounds[3] = 0.0; return; } bounds[0] = shape->bounds[0]; bounds[1] = shape->bounds[1]; bounds[2] = shape->bounds[2]; bounds[3] = shape->bounds[3]; for (shape = shape->next; shape != NULL; shape = shape->next) { bounds[0] = nsvg__minf(bounds[0], shape->bounds[0]); bounds[1] = nsvg__minf(bounds[1], shape->bounds[1]); bounds[2] = nsvg__maxf(bounds[2], shape->bounds[2]); bounds[3] = nsvg__maxf(bounds[3], shape->bounds[3]); } } static float nsvg__viewAlign(float content, float container, int type) { if (type == NSVG_ALIGN_MIN) return 0; else if (type == NSVG_ALIGN_MAX) return container - content; // mid return (container - content) * 0.5f; } static void nsvg__scaleGradient(NSVGgradient* grad, float tx, float ty, float sx, float sy) { float t[6]; nsvg__xformSetTranslation(t, tx, ty); nsvg__xformMultiply (grad->xform, t); nsvg__xformSetScale(t, sx, sy); nsvg__xformMultiply (grad->xform, t); } static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) { NSVGshape* shape; NSVGpath* path; float tx, ty, sx, sy, us, bounds[4], t[6], avgs; int i; float* pt; // Guess image size if not set completely. nsvg__imageBounds(p, bounds); if (p->viewWidth == 0) { if (p->image->width > 0) { p->viewWidth = p->image->width; } else { p->viewMinx = bounds[0]; p->viewWidth = bounds[2] - bounds[0]; } } if (p->viewHeight == 0) { if (p->image->height > 0) { p->viewHeight = p->image->height; } else { p->viewMiny = bounds[1]; p->viewHeight = bounds[3] - bounds[1]; } } if (p->image->width == 0) p->image->width = p->viewWidth; if (p->image->height == 0) p->image->height = p->viewHeight; tx = -p->viewMinx; ty = -p->viewMiny; sx = p->viewWidth > 0 ? p->image->width / p->viewWidth : 0; sy = p->viewHeight > 0 ? p->image->height / p->viewHeight : 0; // Unit scaling us = 1.0f / nsvg__convertToPixels(p, nsvg__coord(1.0f, nsvg__parseUnits(units)), 0.0f, 1.0f); // Fix aspect ratio if (p->alignType == NSVG_ALIGN_MEET) { // fit whole image into viewbox sx = sy = nsvg__minf(sx, sy); tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; } else if (p->alignType == NSVG_ALIGN_SLICE) { // fill whole viewbox with image sx = sy = nsvg__maxf(sx, sy); tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; } // Transform sx *= us; sy *= us; avgs = (sx+sy) / 2.0f; for (shape = p->image->shapes; shape != NULL; shape = shape->next) { shape->bounds[0] = (shape->bounds[0] + tx) * sx; shape->bounds[1] = (shape->bounds[1] + ty) * sy; shape->bounds[2] = (shape->bounds[2] + tx) * sx; shape->bounds[3] = (shape->bounds[3] + ty) * sy; for (path = shape->paths; path != NULL; path = path->next) { path->bounds[0] = (path->bounds[0] + tx) * sx; path->bounds[1] = (path->bounds[1] + ty) * sy; path->bounds[2] = (path->bounds[2] + tx) * sx; path->bounds[3] = (path->bounds[3] + ty) * sy; for (i =0; i < path->npts; i++) { pt = &path->pts[i*2]; pt[0] = (pt[0] + tx) * sx; pt[1] = (pt[1] + ty) * sy; } } if (shape->fill.type == NSVG_PAINT_LINEAR_GRADIENT || shape->fill.type == NSVG_PAINT_RADIAL_GRADIENT) { nsvg__scaleGradient(shape->fill.gradient, tx,ty, sx,sy); memcpy(t, shape->fill.gradient->xform, sizeof(float)*6); nsvg__xformInverse(shape->fill.gradient->xform, t); } if (shape->stroke.type == NSVG_PAINT_LINEAR_GRADIENT || shape->stroke.type == NSVG_PAINT_RADIAL_GRADIENT) { nsvg__scaleGradient(shape->stroke.gradient, tx,ty, sx,sy); memcpy(t, shape->stroke.gradient->xform, sizeof(float)*6); nsvg__xformInverse(shape->stroke.gradient->xform, t); } shape->strokeWidth *= avgs; shape->strokeDashOffset *= avgs; for (i = 0; i < shape->strokeDashCount; i++) shape->strokeDashArray[i] *= avgs; } } static void nsvg__createGradients(NSVGparser* p) { NSVGshape* shape; for (shape = p->image->shapes; shape != NULL; shape = shape->next) { if (shape->fill.type == NSVG_PAINT_UNDEF) { if (shape->fillGradient[0] != '\0') { float inv[6], localBounds[4]; nsvg__xformInverse(inv, shape->xform); nsvg__getLocalBounds(localBounds, shape, inv); shape->fill.gradient = nsvg__createGradient(p, shape->fillGradient, localBounds, shape->xform, &shape->fill.type); } if (shape->fill.type == NSVG_PAINT_UNDEF) { shape->fill.type = NSVG_PAINT_NONE; } } if (shape->stroke.type == NSVG_PAINT_UNDEF) { if (shape->strokeGradient[0] != '\0') { float inv[6], localBounds[4]; nsvg__xformInverse(inv, shape->xform); nsvg__getLocalBounds(localBounds, shape, inv); shape->stroke.gradient = nsvg__createGradient(p, shape->strokeGradient, localBounds, shape->xform, &shape->stroke.type); } if (shape->stroke.type == NSVG_PAINT_UNDEF) { shape->stroke.type = NSVG_PAINT_NONE; } } } } NSVGimage* nsvgParse(char* input, const char* units, float dpi) { NSVGparser* p; NSVGimage* ret = 0; p = nsvg__createParser(); if (p == NULL) { return NULL; } p->dpi = dpi; nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); // Create gradients after all definitions have been parsed nsvg__createGradients(p); // Scale to viewBox nsvg__scaleToViewbox(p, units); ret = p->image; p->image = NULL; nsvg__deleteParser(p); return ret; } NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) { FILE* fp = NULL; size_t size; char* data = NULL; NSVGimage* image = NULL; fp = fopen(filename, "rb"); if (!fp) goto error; fseek(fp, 0, SEEK_END); size = ftell(fp); fseek(fp, 0, SEEK_SET); data = (char*)malloc(size+1); if (data == NULL) goto error; if (fread(data, 1, size, fp) != size) goto error; data[size] = '\0'; // Must be null terminated. fclose(fp); image = nsvgParse(data, units, dpi); free(data); return image; error: if (fp) fclose(fp); if (data) free(data); if (image) nsvgDelete(image); return NULL; } NSVGpath* nsvgDuplicatePath(NSVGpath* p) { NSVGpath* res = NULL; if (p == NULL) return NULL; res = (NSVGpath*)malloc(sizeof(NSVGpath)); if (res == NULL) goto error; memset(res, 0, sizeof(NSVGpath)); res->pts = (float*)malloc(p->npts*2*sizeof(float)); if (res->pts == NULL) goto error; memcpy(res->pts, p->pts, p->npts * sizeof(float) * 2); res->npts = p->npts; memcpy(res->bounds, p->bounds, sizeof(p->bounds)); res->closed = p->closed; return res; error: if (res != NULL) { free(res->pts); free(res); } return NULL; } void nsvgDelete(NSVGimage* image) { NSVGshape *snext, *shape; if (image == NULL) return; shape = image->shapes; while (shape != NULL) { snext = shape->next; nsvg__deletePaths(shape->paths); nsvg__deletePaint(&shape->fill); nsvg__deletePaint(&shape->stroke); free(shape); shape = snext; } free(image); } #endif // NANOSVG_IMPLEMENTATION #endif // NANOSVG_H fuzzel/3rd-party/nanosvg/src/nanosvgrast.h000066400000000000000000001130061445417001200212010ustar00rootroot00000000000000/* * Copyright (c) 2013-14 Mikko Mononen memon@inside.org * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. * * The polygon rasterization is heavily based on stb_truetype rasterizer * by Sean Barrett - http://nothings.org/ * */ #ifndef NANOSVGRAST_H #define NANOSVGRAST_H #include "nanosvg.h" #ifndef NANOSVGRAST_CPLUSPLUS #ifdef __cplusplus extern "C" { #endif #endif typedef struct NSVGrasterizer NSVGrasterizer; /* Example Usage: // Load SVG NSVGimage* image; image = nsvgParseFromFile("test.svg", "px", 96); // Create rasterizer (can be used to render multiple images). struct NSVGrasterizer* rast = nsvgCreateRasterizer(); // Allocate memory for image unsigned char* img = malloc(w*h*4); // Rasterize nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); */ // Allocated rasterizer context. NSVGrasterizer* nsvgCreateRasterizer(void); // Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) // r - pointer to rasterizer context // image - pointer to image to rasterize // tx,ty - image offset (applied after scaling) // scale - image scale // dst - pointer to destination image data, 4 bytes per pixel (RGBA) // w - width of the image to render // h - height of the image to render // stride - number of bytes per scaleline in the destination buffer void nsvgRasterize(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, unsigned char* dst, int w, int h, int stride); // Deletes rasterizer context. void nsvgDeleteRasterizer(NSVGrasterizer*); #ifndef NANOSVGRAST_CPLUSPLUS #ifdef __cplusplus } #endif #endif #ifdef NANOSVGRAST_IMPLEMENTATION #include #include #include #define NSVG__SUBSAMPLES 5 #define NSVG__FIXSHIFT 10 #define NSVG__FIX (1 << NSVG__FIXSHIFT) #define NSVG__FIXMASK (NSVG__FIX-1) #define NSVG__MEMPAGE_SIZE 1024 typedef struct NSVGedge { float x0,y0, x1,y1; int dir; struct NSVGedge* next; } NSVGedge; typedef struct NSVGpoint { float x, y; float dx, dy; float len; float dmx, dmy; unsigned char flags; } NSVGpoint; typedef struct NSVGactiveEdge { int x,dx; float ey; int dir; struct NSVGactiveEdge *next; } NSVGactiveEdge; typedef struct NSVGmemPage { unsigned char mem[NSVG__MEMPAGE_SIZE]; int size; struct NSVGmemPage* next; } NSVGmemPage; typedef struct NSVGcachedPaint { signed char type; char spread; float xform[6]; unsigned int colors[256]; } NSVGcachedPaint; struct NSVGrasterizer { float px, py; float tessTol; float distTol; NSVGedge* edges; int nedges; int cedges; NSVGpoint* points; int npoints; int cpoints; NSVGpoint* points2; int npoints2; int cpoints2; NSVGactiveEdge* freelist; NSVGmemPage* pages; NSVGmemPage* curpage; unsigned char* scanline; int cscanline; unsigned char* bitmap; int width, height, stride; }; NSVGrasterizer* nsvgCreateRasterizer(void) { NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); if (r == NULL) goto error; memset(r, 0, sizeof(NSVGrasterizer)); r->tessTol = 0.25f; r->distTol = 0.01f; return r; error: nsvgDeleteRasterizer(r); return NULL; } void nsvgDeleteRasterizer(NSVGrasterizer* r) { NSVGmemPage* p; if (r == NULL) return; p = r->pages; while (p != NULL) { NSVGmemPage* next = p->next; free(p); p = next; } if (r->edges) free(r->edges); if (r->points) free(r->points); if (r->points2) free(r->points2); if (r->scanline) free(r->scanline); free(r); } static NSVGmemPage* nsvg__nextPage(NSVGrasterizer* r, NSVGmemPage* cur) { NSVGmemPage *newp; // If using existing chain, return the next page in chain if (cur != NULL && cur->next != NULL) { return cur->next; } // Alloc new page newp = (NSVGmemPage*)malloc(sizeof(NSVGmemPage)); if (newp == NULL) return NULL; memset(newp, 0, sizeof(NSVGmemPage)); // Add to linked list if (cur != NULL) cur->next = newp; else r->pages = newp; return newp; } static void nsvg__resetPool(NSVGrasterizer* r) { NSVGmemPage* p = r->pages; while (p != NULL) { p->size = 0; p = p->next; } r->curpage = r->pages; } static unsigned char* nsvg__alloc(NSVGrasterizer* r, int size) { unsigned char* buf; if (size > NSVG__MEMPAGE_SIZE) return NULL; if (r->curpage == NULL || r->curpage->size+size > NSVG__MEMPAGE_SIZE) { r->curpage = nsvg__nextPage(r, r->curpage); } buf = &r->curpage->mem[r->curpage->size]; r->curpage->size += size; return buf; } static int nsvg__ptEquals(float x1, float y1, float x2, float y2, float tol) { float dx = x2 - x1; float dy = y2 - y1; return dx*dx + dy*dy < tol*tol; } static void nsvg__addPathPoint(NSVGrasterizer* r, float x, float y, int flags) { NSVGpoint* pt; if (r->npoints > 0) { pt = &r->points[r->npoints-1]; if (nsvg__ptEquals(pt->x,pt->y, x,y, r->distTol)) { pt->flags = (unsigned char)(pt->flags | flags); return; } } if (r->npoints+1 > r->cpoints) { r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); if (r->points == NULL) return; } pt = &r->points[r->npoints]; pt->x = x; pt->y = y; pt->flags = (unsigned char)flags; r->npoints++; } static void nsvg__appendPathPoint(NSVGrasterizer* r, NSVGpoint pt) { if (r->npoints+1 > r->cpoints) { r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); if (r->points == NULL) return; } r->points[r->npoints] = pt; r->npoints++; } static void nsvg__duplicatePoints(NSVGrasterizer* r) { if (r->npoints > r->cpoints2) { r->cpoints2 = r->npoints; r->points2 = (NSVGpoint*)realloc(r->points2, sizeof(NSVGpoint) * r->cpoints2); if (r->points2 == NULL) return; } memcpy(r->points2, r->points, sizeof(NSVGpoint) * r->npoints); r->npoints2 = r->npoints; } static void nsvg__addEdge(NSVGrasterizer* r, float x0, float y0, float x1, float y1) { NSVGedge* e; // Skip horizontal edges if (y0 == y1) return; if (r->nedges+1 > r->cedges) { r->cedges = r->cedges > 0 ? r->cedges * 2 : 64; r->edges = (NSVGedge*)realloc(r->edges, sizeof(NSVGedge) * r->cedges); if (r->edges == NULL) return; } e = &r->edges[r->nedges]; r->nedges++; if (y0 < y1) { e->x0 = x0; e->y0 = y0; e->x1 = x1; e->y1 = y1; e->dir = 1; } else { e->x0 = x1; e->y0 = y1; e->x1 = x0; e->y1 = y0; e->dir = -1; } } static float nsvg__normalize(float *x, float* y) { float d = sqrtf((*x)*(*x) + (*y)*(*y)); if (d > 1e-6f) { float id = 1.0f / d; *x *= id; *y *= id; } return d; } static float nsvg__absf(float x) { return x < 0 ? -x : x; } static void nsvg__flattenCubicBez(NSVGrasterizer* r, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, int level, int type) { float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; float dx,dy,d2,d3; if (level > 10) return; x12 = (x1+x2)*0.5f; y12 = (y1+y2)*0.5f; x23 = (x2+x3)*0.5f; y23 = (y2+y3)*0.5f; x34 = (x3+x4)*0.5f; y34 = (y3+y4)*0.5f; x123 = (x12+x23)*0.5f; y123 = (y12+y23)*0.5f; dx = x4 - x1; dy = y4 - y1; d2 = nsvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); d3 = nsvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { nsvg__addPathPoint(r, x4, y4, type); return; } x234 = (x23+x34)*0.5f; y234 = (y23+y34)*0.5f; x1234 = (x123+x234)*0.5f; y1234 = (y123+y234)*0.5f; nsvg__flattenCubicBez(r, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, 0); nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); } static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) { int i, j; NSVGpath* path; for (path = shape->paths; path != NULL; path = path->next) { r->npoints = 0; // Flatten path nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); for (i = 0; i < path->npts-1; i += 3) { float* p = &path->pts[i*2]; nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); } // Close path nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); // Build edges for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); } } enum NSVGpointFlags { NSVG_PT_CORNER = 0x01, NSVG_PT_BEVEL = 0x02, NSVG_PT_LEFT = 0x04 }; static void nsvg__initClosed(NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) { float w = lineWidth * 0.5f; float dx = p1->x - p0->x; float dy = p1->y - p0->y; float len = nsvg__normalize(&dx, &dy); float px = p0->x + dx*len*0.5f, py = p0->y + dy*len*0.5f; float dlx = dy, dly = -dx; float lx = px - dlx*w, ly = py - dly*w; float rx = px + dlx*w, ry = py + dly*w; left->x = lx; left->y = ly; right->x = rx; right->y = ry; } static void nsvg__buttCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) { float w = lineWidth * 0.5f; float px = p->x, py = p->y; float dlx = dy, dly = -dx; float lx = px - dlx*w, ly = py - dly*w; float rx = px + dlx*w, ry = py + dly*w; nsvg__addEdge(r, lx, ly, rx, ry); if (connect) { nsvg__addEdge(r, left->x, left->y, lx, ly); nsvg__addEdge(r, rx, ry, right->x, right->y); } left->x = lx; left->y = ly; right->x = rx; right->y = ry; } static void nsvg__squareCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) { float w = lineWidth * 0.5f; float px = p->x - dx*w, py = p->y - dy*w; float dlx = dy, dly = -dx; float lx = px - dlx*w, ly = py - dly*w; float rx = px + dlx*w, ry = py + dly*w; nsvg__addEdge(r, lx, ly, rx, ry); if (connect) { nsvg__addEdge(r, left->x, left->y, lx, ly); nsvg__addEdge(r, rx, ry, right->x, right->y); } left->x = lx; left->y = ly; right->x = rx; right->y = ry; } #ifndef NSVG_PI #define NSVG_PI (3.14159265358979323846264338327f) #endif static void nsvg__roundCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int ncap, int connect) { int i; float w = lineWidth * 0.5f; float px = p->x, py = p->y; float dlx = dy, dly = -dx; float lx = 0, ly = 0, rx = 0, ry = 0, prevx = 0, prevy = 0; for (i = 0; i < ncap; i++) { float a = (float)i/(float)(ncap-1)*NSVG_PI; float ax = cosf(a) * w, ay = sinf(a) * w; float x = px - dlx*ax - dx*ay; float y = py - dly*ax - dy*ay; if (i > 0) nsvg__addEdge(r, prevx, prevy, x, y); prevx = x; prevy = y; if (i == 0) { lx = x; ly = y; } else if (i == ncap-1) { rx = x; ry = y; } } if (connect) { nsvg__addEdge(r, left->x, left->y, lx, ly); nsvg__addEdge(r, rx, ry, right->x, right->y); } left->x = lx; left->y = ly; right->x = rx; right->y = ry; } static void nsvg__bevelJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) { float w = lineWidth * 0.5f; float dlx0 = p0->dy, dly0 = -p0->dx; float dlx1 = p1->dy, dly1 = -p1->dx; float lx0 = p1->x - (dlx0 * w), ly0 = p1->y - (dly0 * w); float rx0 = p1->x + (dlx0 * w), ry0 = p1->y + (dly0 * w); float lx1 = p1->x - (dlx1 * w), ly1 = p1->y - (dly1 * w); float rx1 = p1->x + (dlx1 * w), ry1 = p1->y + (dly1 * w); nsvg__addEdge(r, lx0, ly0, left->x, left->y); nsvg__addEdge(r, lx1, ly1, lx0, ly0); nsvg__addEdge(r, right->x, right->y, rx0, ry0); nsvg__addEdge(r, rx0, ry0, rx1, ry1); left->x = lx1; left->y = ly1; right->x = rx1; right->y = ry1; } static void nsvg__miterJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) { float w = lineWidth * 0.5f; float dlx0 = p0->dy, dly0 = -p0->dx; float dlx1 = p1->dy, dly1 = -p1->dx; float lx0, rx0, lx1, rx1; float ly0, ry0, ly1, ry1; if (p1->flags & NSVG_PT_LEFT) { lx0 = lx1 = p1->x - p1->dmx * w; ly0 = ly1 = p1->y - p1->dmy * w; nsvg__addEdge(r, lx1, ly1, left->x, left->y); rx0 = p1->x + (dlx0 * w); ry0 = p1->y + (dly0 * w); rx1 = p1->x + (dlx1 * w); ry1 = p1->y + (dly1 * w); nsvg__addEdge(r, right->x, right->y, rx0, ry0); nsvg__addEdge(r, rx0, ry0, rx1, ry1); } else { lx0 = p1->x - (dlx0 * w); ly0 = p1->y - (dly0 * w); lx1 = p1->x - (dlx1 * w); ly1 = p1->y - (dly1 * w); nsvg__addEdge(r, lx0, ly0, left->x, left->y); nsvg__addEdge(r, lx1, ly1, lx0, ly0); rx0 = rx1 = p1->x + p1->dmx * w; ry0 = ry1 = p1->y + p1->dmy * w; nsvg__addEdge(r, right->x, right->y, rx1, ry1); } left->x = lx1; left->y = ly1; right->x = rx1; right->y = ry1; } static void nsvg__roundJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth, int ncap) { int i, n; float w = lineWidth * 0.5f; float dlx0 = p0->dy, dly0 = -p0->dx; float dlx1 = p1->dy, dly1 = -p1->dx; float a0 = atan2f(dly0, dlx0); float a1 = atan2f(dly1, dlx1); float da = a1 - a0; float lx, ly, rx, ry; if (da < NSVG_PI) da += NSVG_PI*2; if (da > NSVG_PI) da -= NSVG_PI*2; n = (int)ceilf((nsvg__absf(da) / NSVG_PI) * (float)ncap); if (n < 2) n = 2; if (n > ncap) n = ncap; lx = left->x; ly = left->y; rx = right->x; ry = right->y; for (i = 0; i < n; i++) { float u = (float)i/(float)(n-1); float a = a0 + u*da; float ax = cosf(a) * w, ay = sinf(a) * w; float lx1 = p1->x - ax, ly1 = p1->y - ay; float rx1 = p1->x + ax, ry1 = p1->y + ay; nsvg__addEdge(r, lx1, ly1, lx, ly); nsvg__addEdge(r, rx, ry, rx1, ry1); lx = lx1; ly = ly1; rx = rx1; ry = ry1; } left->x = lx; left->y = ly; right->x = rx; right->y = ry; } static void nsvg__straightJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p1, float lineWidth) { float w = lineWidth * 0.5f; float lx = p1->x - (p1->dmx * w), ly = p1->y - (p1->dmy * w); float rx = p1->x + (p1->dmx * w), ry = p1->y + (p1->dmy * w); nsvg__addEdge(r, lx, ly, left->x, left->y); nsvg__addEdge(r, right->x, right->y, rx, ry); left->x = lx; left->y = ly; right->x = rx; right->y = ry; } static int nsvg__curveDivs(float r, float arc, float tol) { float da = acosf(r / (r + tol)) * 2.0f; int divs = (int)ceilf(arc / da); if (divs < 2) divs = 2; return divs; } static void nsvg__expandStroke(NSVGrasterizer* r, NSVGpoint* points, int npoints, int closed, int lineJoin, int lineCap, float lineWidth) { int ncap = nsvg__curveDivs(lineWidth*0.5f, NSVG_PI, r->tessTol); // Calculate divisions per half circle. NSVGpoint left = {0,0,0,0,0,0,0,0}, right = {0,0,0,0,0,0,0,0}, firstLeft = {0,0,0,0,0,0,0,0}, firstRight = {0,0,0,0,0,0,0,0}; NSVGpoint* p0, *p1; int j, s, e; // Build stroke edges if (closed) { // Looping p0 = &points[npoints-1]; p1 = &points[0]; s = 0; e = npoints; } else { // Add cap p0 = &points[0]; p1 = &points[1]; s = 1; e = npoints-1; } if (closed) { nsvg__initClosed(&left, &right, p0, p1, lineWidth); firstLeft = left; firstRight = right; } else { // Add cap float dx = p1->x - p0->x; float dy = p1->y - p0->y; nsvg__normalize(&dx, &dy); if (lineCap == NSVG_CAP_BUTT) nsvg__buttCap(r, &left, &right, p0, dx, dy, lineWidth, 0); else if (lineCap == NSVG_CAP_SQUARE) nsvg__squareCap(r, &left, &right, p0, dx, dy, lineWidth, 0); else if (lineCap == NSVG_CAP_ROUND) nsvg__roundCap(r, &left, &right, p0, dx, dy, lineWidth, ncap, 0); } for (j = s; j < e; ++j) { if (p1->flags & NSVG_PT_CORNER) { if (lineJoin == NSVG_JOIN_ROUND) nsvg__roundJoin(r, &left, &right, p0, p1, lineWidth, ncap); else if (lineJoin == NSVG_JOIN_BEVEL || (p1->flags & NSVG_PT_BEVEL)) nsvg__bevelJoin(r, &left, &right, p0, p1, lineWidth); else nsvg__miterJoin(r, &left, &right, p0, p1, lineWidth); } else { nsvg__straightJoin(r, &left, &right, p1, lineWidth); } p0 = p1++; } if (closed) { // Loop it nsvg__addEdge(r, firstLeft.x, firstLeft.y, left.x, left.y); nsvg__addEdge(r, right.x, right.y, firstRight.x, firstRight.y); } else { // Add cap float dx = p1->x - p0->x; float dy = p1->y - p0->y; nsvg__normalize(&dx, &dy); if (lineCap == NSVG_CAP_BUTT) nsvg__buttCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); else if (lineCap == NSVG_CAP_SQUARE) nsvg__squareCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); else if (lineCap == NSVG_CAP_ROUND) nsvg__roundCap(r, &right, &left, p1, -dx, -dy, lineWidth, ncap, 1); } } static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoin) { int i, j; NSVGpoint* p0, *p1; p0 = &r->points[r->npoints-1]; p1 = &r->points[0]; for (i = 0; i < r->npoints; i++) { // Calculate segment direction and length p0->dx = p1->x - p0->x; p0->dy = p1->y - p0->y; p0->len = nsvg__normalize(&p0->dx, &p0->dy); // Advance p0 = p1++; } // calculate joins p0 = &r->points[r->npoints-1]; p1 = &r->points[0]; for (j = 0; j < r->npoints; j++) { float dlx0, dly0, dlx1, dly1, dmr2, cross; dlx0 = p0->dy; dly0 = -p0->dx; dlx1 = p1->dy; dly1 = -p1->dx; // Calculate extrusions p1->dmx = (dlx0 + dlx1) * 0.5f; p1->dmy = (dly0 + dly1) * 0.5f; dmr2 = p1->dmx*p1->dmx + p1->dmy*p1->dmy; if (dmr2 > 0.000001f) { float s2 = 1.0f / dmr2; if (s2 > 600.0f) { s2 = 600.0f; } p1->dmx *= s2; p1->dmy *= s2; } // Clear flags, but keep the corner. p1->flags = (p1->flags & NSVG_PT_CORNER) ? NSVG_PT_CORNER : 0; // Keep track of left turns. cross = p1->dx * p0->dy - p0->dx * p1->dy; if (cross > 0.0f) p1->flags |= NSVG_PT_LEFT; // Check to see if the corner needs to be beveled. if (p1->flags & NSVG_PT_CORNER) { if ((dmr2 * miterLimit*miterLimit) < 1.0f || lineJoin == NSVG_JOIN_BEVEL || lineJoin == NSVG_JOIN_ROUND) { p1->flags |= NSVG_PT_BEVEL; } } p0 = p1++; } } static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) { int i, j, closed; NSVGpath* path; NSVGpoint* p0, *p1; float miterLimit = shape->miterLimit; int lineJoin = shape->strokeLineJoin; int lineCap = shape->strokeLineCap; float lineWidth = shape->strokeWidth * scale; for (path = shape->paths; path != NULL; path = path->next) { // Flatten path r->npoints = 0; nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); for (i = 0; i < path->npts-1; i += 3) { float* p = &path->pts[i*2]; nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); } if (r->npoints < 2) continue; closed = path->closed; // If the first and last points are the same, remove the last, mark as closed path. p0 = &r->points[r->npoints-1]; p1 = &r->points[0]; if (nsvg__ptEquals(p0->x,p0->y, p1->x,p1->y, r->distTol)) { r->npoints--; p0 = &r->points[r->npoints-1]; closed = 1; } if (shape->strokeDashCount > 0) { int idash = 0, dashState = 1; float totalDist = 0, dashLen, allDashLen, dashOffset; NSVGpoint cur; if (closed) nsvg__appendPathPoint(r, r->points[0]); // Duplicate points -> points2. nsvg__duplicatePoints(r); r->npoints = 0; cur = r->points2[0]; nsvg__appendPathPoint(r, cur); // Figure out dash offset. allDashLen = 0; for (j = 0; j < shape->strokeDashCount; j++) allDashLen += shape->strokeDashArray[j]; if (shape->strokeDashCount & 1) allDashLen *= 2.0f; // Find location inside pattern dashOffset = fmodf(shape->strokeDashOffset, allDashLen); if (dashOffset < 0.0f) dashOffset += allDashLen; while (dashOffset > shape->strokeDashArray[idash]) { dashOffset -= shape->strokeDashArray[idash]; idash = (idash + 1) % shape->strokeDashCount; } dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; for (j = 1; j < r->npoints2; ) { float dx = r->points2[j].x - cur.x; float dy = r->points2[j].y - cur.y; float dist = sqrtf(dx*dx + dy*dy); if ((totalDist + dist) > dashLen) { // Calculate intermediate point float d = (dashLen - totalDist) / dist; float x = cur.x + dx * d; float y = cur.y + dy * d; nsvg__addPathPoint(r, x, y, NSVG_PT_CORNER); // Stroke if (r->npoints > 1 && dashState) { nsvg__prepareStroke(r, miterLimit, lineJoin); nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); } // Advance dash pattern dashState = !dashState; idash = (idash+1) % shape->strokeDashCount; dashLen = shape->strokeDashArray[idash] * scale; // Restart cur.x = x; cur.y = y; cur.flags = NSVG_PT_CORNER; totalDist = 0.0f; r->npoints = 0; nsvg__appendPathPoint(r, cur); } else { totalDist += dist; cur = r->points2[j]; nsvg__appendPathPoint(r, cur); j++; } } // Stroke any leftover path if (r->npoints > 1 && dashState) nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); } else { nsvg__prepareStroke(r, miterLimit, lineJoin); nsvg__expandStroke(r, r->points, r->npoints, closed, lineJoin, lineCap, lineWidth); } } } static int nsvg__cmpEdge(const void *p, const void *q) { const NSVGedge* a = (const NSVGedge*)p; const NSVGedge* b = (const NSVGedge*)q; if (a->y0 < b->y0) return -1; if (a->y0 > b->y0) return 1; return 0; } static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float startPoint) { NSVGactiveEdge* z; if (r->freelist != NULL) { // Restore from freelist. z = r->freelist; r->freelist = z->next; } else { // Alloc new edge. z = (NSVGactiveEdge*)nsvg__alloc(r, sizeof(NSVGactiveEdge)); if (z == NULL) return NULL; } float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); // STBTT_assert(e->y0 <= start_point); // round dx down to avoid going too far if (dxdy < 0) z->dx = (int)(-floorf(NSVG__FIX * -dxdy)); else z->dx = (int)floorf(NSVG__FIX * dxdy); z->x = (int)floorf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); // z->x -= off_x * FIX; z->ey = e->y1; z->next = 0; z->dir = e->dir; return z; } static void nsvg__freeActive(NSVGrasterizer* r, NSVGactiveEdge* z) { z->next = r->freelist; r->freelist = z; } static void nsvg__fillScanline(unsigned char* scanline, int len, int x0, int x1, int maxWeight, int* xmin, int* xmax) { int i = x0 >> NSVG__FIXSHIFT; int j = x1 >> NSVG__FIXSHIFT; if (i < *xmin) *xmin = i; if (j > *xmax) *xmax = j; if (i < len && j >= 0) { if (i == j) { // x0,x1 are the same pixel, so compute combined coverage scanline[i] = (unsigned char)(scanline[i] + ((x1 - x0) * maxWeight >> NSVG__FIXSHIFT)); } else { if (i >= 0) // add antialiasing for x0 scanline[i] = (unsigned char)(scanline[i] + (((NSVG__FIX - (x0 & NSVG__FIXMASK)) * maxWeight) >> NSVG__FIXSHIFT)); else i = -1; // clip if (j < len) // add antialiasing for x1 scanline[j] = (unsigned char)(scanline[j] + (((x1 & NSVG__FIXMASK) * maxWeight) >> NSVG__FIXSHIFT)); else j = len; // clip for (++i; i < j; ++i) // fill pixels between x0 and x1 scanline[i] = (unsigned char)(scanline[i] + maxWeight); } } } // note: this routine clips fills that extend off the edges... ideally this // wouldn't happen, but it could happen if the truetype glyph bounding boxes // are wrong, or if the user supplies a too-small bitmap static void nsvg__fillActiveEdges(unsigned char* scanline, int len, NSVGactiveEdge* e, int maxWeight, int* xmin, int* xmax, char fillRule) { // non-zero winding fill int x0 = 0, w = 0; if (fillRule == NSVG_FILLRULE_NONZERO) { // Non-zero while (e != NULL) { if (w == 0) { // if we're currently at zero, we need to record the edge start point x0 = e->x; w += e->dir; } else { int x1 = e->x; w += e->dir; // if we went to zero, we need to draw if (w == 0) nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); } e = e->next; } } else if (fillRule == NSVG_FILLRULE_EVENODD) { // Even-odd while (e != NULL) { if (w == 0) { // if we're currently at zero, we need to record the edge start point x0 = e->x; w = 1; } else { int x1 = e->x; w = 0; nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); } e = e->next; } } } static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a > mx ? mx : a); } static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); } static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) { int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); int r = (((c0) & 0xff)*(256-iu) + (((c1) & 0xff)*iu)) >> 8; int g = (((c0>>8) & 0xff)*(256-iu) + (((c1>>8) & 0xff)*iu)) >> 8; int b = (((c0>>16) & 0xff)*(256-iu) + (((c1>>16) & 0xff)*iu)) >> 8; int a = (((c0>>24) & 0xff)*(256-iu) + (((c1>>24) & 0xff)*iu)) >> 8; return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); } static unsigned int nsvg__applyOpacity(unsigned int c, float u) { int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); int r = (c) & 0xff; int g = (c>>8) & 0xff; int b = (c>>16) & 0xff; int a = (((c>>24) & 0xff)*iu) >> 8; return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); } static inline int nsvg__div255(int x) { return ((x+1) * 257) >> 16; } static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, float tx, float ty, float scale, NSVGcachedPaint* cache) { if (cache->type == NSVG_PAINT_COLOR) { int i, cr, cg, cb, ca; cr = cache->colors[0] & 0xff; cg = (cache->colors[0] >> 8) & 0xff; cb = (cache->colors[0] >> 16) & 0xff; ca = (cache->colors[0] >> 24) & 0xff; for (i = 0; i < count; i++) { int r,g,b; int a = nsvg__div255((int)cover[0] * ca); int ia = 255 - a; // Premultiply r = nsvg__div255(cr * a); g = nsvg__div255(cg * a); b = nsvg__div255(cb * a); // Blend over r += nsvg__div255(ia * (int)dst[0]); g += nsvg__div255(ia * (int)dst[1]); b += nsvg__div255(ia * (int)dst[2]); a += nsvg__div255(ia * (int)dst[3]); dst[0] = (unsigned char)r; dst[1] = (unsigned char)g; dst[2] = (unsigned char)b; dst[3] = (unsigned char)a; cover++; dst += 4; } } else if (cache->type == NSVG_PAINT_LINEAR_GRADIENT) { // TODO: spread modes. // TODO: plenty of opportunities to optimize. float fx, fy, dx, gy; float* t = cache->xform; int i, cr, cg, cb, ca; unsigned int c; fx = ((float)x - tx) / scale; fy = ((float)y - ty) / scale; dx = 1.0f / scale; for (i = 0; i < count; i++) { int r,g,b,a,ia; gy = fx*t[1] + fy*t[3] + t[5]; c = cache->colors[(int)nsvg__clampf(gy*255.0f, 0, 255.0f)]; cr = (c) & 0xff; cg = (c >> 8) & 0xff; cb = (c >> 16) & 0xff; ca = (c >> 24) & 0xff; a = nsvg__div255((int)cover[0] * ca); ia = 255 - a; // Premultiply r = nsvg__div255(cr * a); g = nsvg__div255(cg * a); b = nsvg__div255(cb * a); // Blend over r += nsvg__div255(ia * (int)dst[0]); g += nsvg__div255(ia * (int)dst[1]); b += nsvg__div255(ia * (int)dst[2]); a += nsvg__div255(ia * (int)dst[3]); dst[0] = (unsigned char)r; dst[1] = (unsigned char)g; dst[2] = (unsigned char)b; dst[3] = (unsigned char)a; cover++; dst += 4; fx += dx; } } else if (cache->type == NSVG_PAINT_RADIAL_GRADIENT) { // TODO: spread modes. // TODO: plenty of opportunities to optimize. // TODO: focus (fx,fy) float fx, fy, dx, gx, gy, gd; float* t = cache->xform; int i, cr, cg, cb, ca; unsigned int c; fx = ((float)x - tx) / scale; fy = ((float)y - ty) / scale; dx = 1.0f / scale; for (i = 0; i < count; i++) { int r,g,b,a,ia; gx = fx*t[0] + fy*t[2] + t[4]; gy = fx*t[1] + fy*t[3] + t[5]; gd = sqrtf(gx*gx + gy*gy); c = cache->colors[(int)nsvg__clampf(gd*255.0f, 0, 255.0f)]; cr = (c) & 0xff; cg = (c >> 8) & 0xff; cb = (c >> 16) & 0xff; ca = (c >> 24) & 0xff; a = nsvg__div255((int)cover[0] * ca); ia = 255 - a; // Premultiply r = nsvg__div255(cr * a); g = nsvg__div255(cg * a); b = nsvg__div255(cb * a); // Blend over r += nsvg__div255(ia * (int)dst[0]); g += nsvg__div255(ia * (int)dst[1]); b += nsvg__div255(ia * (int)dst[2]); a += nsvg__div255(ia * (int)dst[3]); dst[0] = (unsigned char)r; dst[1] = (unsigned char)g; dst[2] = (unsigned char)b; dst[3] = (unsigned char)a; cover++; dst += 4; fx += dx; } } } static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) { NSVGactiveEdge *active = NULL; int y, s; int e = 0; int maxWeight = (255 / NSVG__SUBSAMPLES); // weight per vertical scanline int xmin, xmax; for (y = 0; y < r->height; y++) { memset(r->scanline, 0, r->width); xmin = r->width; xmax = 0; for (s = 0; s < NSVG__SUBSAMPLES; ++s) { // find center of pixel for this scanline float scany = (float)(y*NSVG__SUBSAMPLES + s) + 0.5f; NSVGactiveEdge **step = &active; // update all active edges; // remove all active edges that terminate before the center of this scanline while (*step) { NSVGactiveEdge *z = *step; if (z->ey <= scany) { *step = z->next; // delete from list // NSVG__assert(z->valid); nsvg__freeActive(r, z); } else { z->x += z->dx; // advance to position for current scanline step = &((*step)->next); // advance through list } } // resort the list if needed for (;;) { int changed = 0; step = &active; while (*step && (*step)->next) { if ((*step)->x > (*step)->next->x) { NSVGactiveEdge* t = *step; NSVGactiveEdge* q = t->next; t->next = q->next; q->next = t; *step = q; changed = 1; } step = &(*step)->next; } if (!changed) break; } // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline while (e < r->nedges && r->edges[e].y0 <= scany) { if (r->edges[e].y1 > scany) { NSVGactiveEdge* z = nsvg__addActive(r, &r->edges[e], scany); if (z == NULL) break; // find insertion point if (active == NULL) { active = z; } else if (z->x < active->x) { // insert at front z->next = active; active = z; } else { // find thing to insert AFTER NSVGactiveEdge* p = active; while (p->next && p->next->x < z->x) p = p->next; // at this point, p->next->x is NOT < z->x z->next = p->next; p->next = z; } } e++; } // now process all active edges in non-zero fashion if (active != NULL) nsvg__fillActiveEdges(r->scanline, r->width, active, maxWeight, &xmin, &xmax, fillRule); } // Blit if (xmin < 0) xmin = 0; if (xmax > r->width-1) xmax = r->width-1; if (xmin <= xmax) { nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); } } } static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) { int x,y; // Unpremultiply for (y = 0; y < h; y++) { unsigned char *row = &image[y*stride]; for (x = 0; x < w; x++) { int r = row[0], g = row[1], b = row[2], a = row[3]; if (a != 0) { row[0] = (unsigned char)(r*255/a); row[1] = (unsigned char)(g*255/a); row[2] = (unsigned char)(b*255/a); } row += 4; } } // Defringe for (y = 0; y < h; y++) { unsigned char *row = &image[y*stride]; for (x = 0; x < w; x++) { int r = 0, g = 0, b = 0, a = row[3], n = 0; if (a == 0) { if (x-1 > 0 && row[-1] != 0) { r += row[-4]; g += row[-3]; b += row[-2]; n++; } if (x+1 < w && row[7] != 0) { r += row[4]; g += row[5]; b += row[6]; n++; } if (y-1 > 0 && row[-stride+3] != 0) { r += row[-stride]; g += row[-stride+1]; b += row[-stride+2]; n++; } if (y+1 < h && row[stride+3] != 0) { r += row[stride]; g += row[stride+1]; b += row[stride+2]; n++; } if (n > 0) { row[0] = (unsigned char)(r/n); row[1] = (unsigned char)(g/n); row[2] = (unsigned char)(b/n); } } row += 4; } } } static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opacity) { int i, j; NSVGgradient* grad; cache->type = paint->type; if (paint->type == NSVG_PAINT_COLOR) { cache->colors[0] = nsvg__applyOpacity(paint->color, opacity); return; } grad = paint->gradient; cache->spread = grad->spread; memcpy(cache->xform, grad->xform, sizeof(float)*6); if (grad->nstops == 0) { for (i = 0; i < 256; i++) cache->colors[i] = 0; } if (grad->nstops == 1) { for (i = 0; i < 256; i++) cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); } else { unsigned int ca, cb = 0; float ua, ub, du, u; int ia, ib, count; ca = nsvg__applyOpacity(grad->stops[0].color, opacity); ua = nsvg__clampf(grad->stops[0].offset, 0, 1); ub = nsvg__clampf(grad->stops[grad->nstops-1].offset, ua, 1); ia = (int)(ua * 255.0f); ib = (int)(ub * 255.0f); for (i = 0; i < ia; i++) { cache->colors[i] = ca; } for (i = 0; i < grad->nstops-1; i++) { ca = nsvg__applyOpacity(grad->stops[i].color, opacity); cb = nsvg__applyOpacity(grad->stops[i+1].color, opacity); ua = nsvg__clampf(grad->stops[i].offset, 0, 1); ub = nsvg__clampf(grad->stops[i+1].offset, 0, 1); ia = (int)(ua * 255.0f); ib = (int)(ub * 255.0f); count = ib - ia; if (count <= 0) continue; u = 0; du = 1.0f / (float)count; for (j = 0; j < count; j++) { cache->colors[ia+j] = nsvg__lerpRGBA(ca,cb,u); u += du; } } for (i = ib; i < 256; i++) cache->colors[i] = cb; } } /* static void dumpEdges(NSVGrasterizer* r, const char* name) { float xmin = 0, xmax = 0, ymin = 0, ymax = 0; NSVGedge *e = NULL; int i; if (r->nedges == 0) return; FILE* fp = fopen(name, "w"); if (fp == NULL) return; xmin = xmax = r->edges[0].x0; ymin = ymax = r->edges[0].y0; for (i = 0; i < r->nedges; i++) { e = &r->edges[i]; xmin = nsvg__minf(xmin, e->x0); xmin = nsvg__minf(xmin, e->x1); xmax = nsvg__maxf(xmax, e->x0); xmax = nsvg__maxf(xmax, e->x1); ymin = nsvg__minf(ymin, e->y0); ymin = nsvg__minf(ymin, e->y1); ymax = nsvg__maxf(ymax, e->y0); ymax = nsvg__maxf(ymax, e->y1); } fprintf(fp, "", xmin, ymin, (xmax - xmin), (ymax - ymin)); for (i = 0; i < r->nedges; i++) { e = &r->edges[i]; fprintf(fp ,"", e->x0,e->y0, e->x1,e->y1); } for (i = 0; i < r->npoints; i++) { if (i+1 < r->npoints) fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i+1].x, r->points[i+1].y); fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i].flags == 0 ? "#f00" : "#0f0"); } fprintf(fp, ""); fclose(fp); } */ void nsvgRasterize(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, unsigned char* dst, int w, int h, int stride) { NSVGshape *shape = NULL; NSVGedge *e = NULL; NSVGcachedPaint cache; int i; r->bitmap = dst; r->width = w; r->height = h; r->stride = stride; if (w > r->cscanline) { r->cscanline = w; r->scanline = (unsigned char*)realloc(r->scanline, w); if (r->scanline == NULL) return; } for (i = 0; i < h; i++) memset(&dst[i*stride], 0, w*4); for (shape = image->shapes; shape != NULL; shape = shape->next) { if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; if (shape->fill.type != NSVG_PAINT_NONE) { nsvg__resetPool(r); r->freelist = NULL; r->nedges = 0; nsvg__flattenShape(r, shape, scale); // Scale and translate edges for (i = 0; i < r->nedges; i++) { e = &r->edges[i]; e->x0 = tx + e->x0; e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; e->x1 = tx + e->x1; e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; } // Rasterize edges if (r->nedges != 0) qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule nsvg__initPaint(&cache, &shape->fill, shape->opacity); nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); } if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { nsvg__resetPool(r); r->freelist = NULL; r->nedges = 0; nsvg__flattenShapeStroke(r, shape, scale); // dumpEdges(r, "edge.svg"); // Scale and translate edges for (i = 0; i < r->nedges; i++) { e = &r->edges[i]; e->x0 = tx + e->x0; e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; e->x1 = tx + e->x1; e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; } // Rasterize edges if (r->nedges != 0) qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule nsvg__initPaint(&cache, &shape->stroke, shape->opacity); nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); } } nsvg__unpremultiplyAlpha(dst, w, h, stride); r->bitmap = NULL; r->width = 0; r->height = 0; r->stride = 0; } #endif // NANOSVGRAST_IMPLEMENTATION #endif // NANOSVGRAST_H fuzzel/CHANGELOG.md000066400000000000000000000400431445417001200142050ustar00rootroot00000000000000# Changelog * [1.9.2](#1-9-2) * [1.9.1](#1-9-1) * [1.9.0](#1-9-0) * [1.8.2](#1-8-2) * [1.8.1](#1-8-1) * [1.8.0](#1-8-0) * [1.7.0](#1-7-0) * [1.6.5](#1-6-5) * [1.6.4](#1-6-4) * [1.6.3](#1-6-3) * [1.6.2](#1-6-2) * [1.6.1](#1-6-1) * [1.6.0](#1-6-0) * [1.5.4](#1-5-4) * [1.5.3](#1-5-3) * [1.5.2](#1-5-2) * [1.5.1](#1-5-1) * [1.5.0](#1-5-0) * [1.4.2](#1-4-2) * [1.4.1](#1-4-1) ## 1.9.2 ### Added * Added a new option `--filter-desktop` which toggles filtering of desktop entries based on the OnlyShowIn and NotShowIn keys. Filtering is based on the value of $XDG\_CURRENT\_DESKTOP according to desktop-entry spec. Filtering is off by default. To disable filtering set in the config from the command line, use --filter-desktop=no ### Changed * Output scaling is now applied to the border radius ([#236][236]). [236]: https://codeberg.org/dnkl/fuzzel/issues/236 ### Fixed * Last line sometimes not being rendered ([#234][234]). * `key-bindings.cursor-right-word` not being recognized as a valid action. * `password-character` being set in `fuzzel.ini` incorrectly enabling password mode ([#241][241]). * Added missing zsh+fish completions for `--password`. [234]: https://codeberg.org/dnkl/fuzzel/issues/234 [241]: https://codeberg.org/dnkl/fuzzel/issues/241 ### Contributors * complex2liu * Mark Stosberg * Ronan Pigott ## 1.9.1 ### Fixed * Regression: default font size was unintentionally changed from 12pt in 1.8.2, to 8pt in 1.9.0. The old default of 12pt has now been restored. * Regression: crash when pressing Enter and the match list is empty (e.g. when trying to execute a command line) ([#222][222]). [222]: https://codeberg.org/dnkl/fuzzel/issues/222 ## 1.9.0 ### Added * Add support for startup notifications via xdg activation ([#195][195]) * Convert tabs to spaces when rendering ([#137][137]). * `--dmenu0` command line option. Like `--dmenu`, but input is NUL separated instead of newline separated ([#197][197]). * Support for localized strings. If you want the old behavior, run `fuzzel` with `LC_MESSAGES=C` ([#199][199]). * Export `FUZZEL_DESKTOP_FILE_ID` environment variable when setting the `--launch-prefix` in order to pass the Desktop File ID to the launch prefix ([#110][110]). * New key bindings: `[key-bindings].first` and `[key-bindings].last`, bound to `Control+Home` and `Control+End` by default ([#210][210]). * New key binding: `[key-bindings].insert-selected`, bound to `Control+Tab` by default. It replaces the current prompt (filter) with the selected item ([#212][212]). [195]: https://codeberg.org/dnkl/fuzzel/pulls/195 [137]: https://codeberg.org/dnkl/fuzzel/issues/137 [197]: https://codeberg.org/dnkl/fuzzel/issues/197 [199]: https://codeberg.org/dnkl/fuzzel/issues/199 [110]: https://codeberg.org/dnkl/fuzzel/issues/110 [210]: https://codeberg.org/dnkl/fuzzel/issues/210 [212]: https://codeberg.org/dnkl/fuzzel/issues/212 ### Changed * Better verification of color values specified on the command line ([#194][194]). * When determining initial font size, do FontConfig config substitution if the user-provided font pattern has no {pixel}size option ([#1287][foot-1287]). [194]: https://codeberg.org/dnkl/fuzzel/issues/194 [foot-1287]: https://codeberg.org/dnkl/foot/issues/1287 ### Fixed * Update nanosvg to f0a3e10. Fixes rendering of certain SVG icons ([#190][190]). * Not being able to input numbers using the keypad ([#192][192]). * Absolute path PNG icons not being loaded ([#214][214]). [190]: https://codeberg.org/dnkl/fuzzel/issues/190 [192]: https://codeberg.org/dnkl/fuzzel/issues/192 [214]: https://codeberg.org/dnkl/fuzzel/issues/214 ### Contributors * Mark Stosberg * Max Gautier * Ronan Pigott ## 1.8.2 ### Added * Fish completions ([#176][176]) [176]: https://codeberg.org/dnkl/fuzzel/issues/176 ### Fixed * Unsupported icon formats not being skipped when loading application icons. * Wrong size of PNG icons selected ([#182][182]) [182]: https://codeberg.org/dnkl/fuzzel/issues/182 ## 1.8.1 ### Fixed * Regression: not able to input text with modifiers (e.g. Shift) pressed ([#177][177]). [177]: https://codeberg.org/dnkl/fuzzel/issues/177 ## 1.8.0 ### Added * Support for file based configuration ([#3][3]). * Customizable key bindings ([#117][117]). * "Custom" key bindings (like Rofi’s `kb-custom-N` key bindings). * If `argv[0]` is _dmenu_, fuzzel now starts in dmenu mode ([#107][107]). * `--password=[CHARACTER]` command line option. Intended to be used with “password input”; all typed text is rendered as _CHARACTER_, defaulting to `*` if _CHARACTER_ is omitted ([#108][108]). * `Ctrl+y` binding to execute selected entry. * `Ctrl+j`/`Ctrl+k` binding to move to the next/previous item ([#120][120]). * Escape sequences in `Exec` arguments are now supported. * Quoted environment variables in `Exec` arguments are now supported ([#143][143]). * Multiple space-separated search words can now be entered at the prompt. * `-M,--selection-match-color`, that lets you configure the color of matched substrings of the currently selected item * New config option `image-size-ratio`, allowing you to control the size of the large image displayed when there are only a “few” matches. * Support for icons in dmenu mode, using Rofi’s extended dmenu protocol ([#166][166]). * `--layer` command line option, allowing you to choose which layer to render the fuzzel window on (`top` or `overlay`) ([#81][81]). * `--no-exit-on-keyboard-focus-loss` command line option (`exit-on-keyboard-focus-loss` config option) ([#128][128]). [3]: https://codeberg.org/dnkl/fuzzel/issues/3 [117]: https://codeberg.org/dnkl/fuzzel/issues/117 [107]: https://codeberg.org/dnkl/fuzzel/issues/107 [108]: https://codeberg.org/dnkl/fuzzel/issues/108 [120]: https://codeberg.org/dnkl/fuzzel/issues/120 [143]: https://codeberg.org/dnkl/fuzzel/issues/143 [166]: https://codeberg.org/dnkl/fuzzel/issues/166 [81]: https://codeberg.org/dnkl/fuzzel/issues/81 [128]: https://codeberg.org/dnkl/fuzzel/issues/128 ### Changed * `-i` is now **ignored**. This is to increase compatibility with other similar utilities. To set the icon theme, either use the long option (`--icon-theme=THEME`), or set it in the configuration file (default: `$XDG_CONFIG_HOME/fuzzel/fuzzel.ini`) ([#149][149]). * Minimum required meson version is now 0.58. * libpng warnings are now routed through fuzzel’s logging ([#101][101]). * Nanosvg is now the default SVG backend. librsvg is still supported, and can be used by setting the `-Dsvg-backend=librsvg` meson option. * It is no longer necessary to close stdin when using fuzzel in dmenu mode, as long as `--no-run-if-empty` is **not** being used ([#106][106]). * Improved performance of initial rendering of icons ([#124][124]). * `--terminal` now defaults to `$TERMINAL -e`. * Font shaping is now applied to the prompt * The large image displayed when there are only a “few” matches is now smaller by default. * Swapped meaning of the command line options `-p` and `-P`; `-p` is now the short option for `--prompt` ([#146][146]). * Do not add icon-sized padding on the left size in dmenu mode ([#158][158]). * Color config values are now allowed to be prefixed with `#` ([#160][160]). [149]: https://codeberg.org/dnkl/fuzzel/issues/149 [101]: https://codeberg.org/dnkl/fuzzel/issues/101 [106]: https://codeberg.org/dnkl/fuzzel/issues/106 [124]: https://codeberg.org/dnkl/fuzzel/issues/124 [146]: https://codeberg.org/dnkl/fuzzel/issues/146 [158]: https://codeberg.org/dnkl/fuzzel/issues/158 [160]: https://codeberg.org/dnkl/fuzzel/issues/160 ### Fixed * User `.desktop` entries with `NoDisplay=true` not overriding system entries ([#114][114]). * Icon lookup is now better at following the XDG specification. * Backspace removes not only the previous character, but also everything **after** the cursor. * Crash on exit in dmenu mode when selection list is empty. * Keypad `enter` not executing the selected entry ([#138][138]) [114]: https://codeberg.org/dnkl/fuzzel/issues/114 [138]: https://codeberg.org/dnkl/fuzzel/issues/138 ### Contributors * Chinmay Dalal * Matthew Toohey * Michael Yang * Eyeoglu ## 1.7.0 ### Added * `-F,--fields=FIELDS` command line option, allowing you to select which XDG Desktop Entry fields to match against ([#63](https://codeberg.org/dnkl/fuzzel/issues/63)). * Support for desktop entry actions ([#71](https://codeberg.org/dnkl/fuzzel/issues/71)). * Fuzzy matching. This is enabled by default, but can be disabled with `--no-fuzzy`. When enabled, the fuzziness can be adjusted with `--fuzzy-max-length-discrepancy` and `--fuzzy-max-distance` ([#56](https://codeberg.org/dnkl/fuzzel/issues/56)). * `--index` (dmenu mode only): print selected entry’s index instead of its text ([#88](https://codeberg.org/dnkl/fuzzel/issues/88)). * `--log-level=info|warning|error|none` command line option ([#34](https://codeberg.org/dnkl/fuzzel/issues/34)). * `--log-no-syslog` command line option. * `--log-colorize=auto|never|always` command line option. ### Changed * Fuzzel now refuses to start if there is another fuzzel instance running ([#57](https://codeberg.org/dnkl/fuzzel/issues/57)). * Treat "Apps" as valid context for applications to support more icon themes (for example, Faenza) * The `Name` entry of the desktop files are no longer used as unique identifiers. Instead, we now generate the “desktop file ID” according to the XDG desktop entry specification, and use that as ID ([#68](https://codeberg.org/dnkl/fuzzel/issues/68)). * All XDG data directories are now searched when loading an icon. Previously, only XDG data directories where the theme directory contained an `index.theme` file were searched ([#62](https://codeberg.org/dnkl/fuzzel/issues/62)). * Pressing Tab when there is a single match now executes it ([#77](https://codeberg.org/dnkl/fuzzel/issues/77)). * Use a lock file instead of parsing `/proc` to prevent multiple fuzzel instances from running at the same time ([#84](https://codeberg.org/dnkl/fuzzel/issues/84)). * The application list is now populated in a separate thread, in parallel to initializing the GUI. This reduces the risk of missing keyboard input ([#82](https://codeberg.org/dnkl/fuzzel/issues/82)). * Icons are now loaded in a thread. This allows us to display the application list quickly (initially without icons, if loading them takes “too” long). * Fuzzel now exits with exit code 0 when the non-dmenu launcher is aborted (no application has been launched) by the user ([#98](https://codeberg.org/dnkl/fuzzel/issues/98)). ### Fixed * Long entries overrunning the right side padding ([#80](https://codeberg.org/dnkl/fuzzel/issues/80)). * Tab and Shift+Tab not wrapping around ([#78](https://codeberg.org/dnkl/fuzzel/issues/78)). * Visual glitches in the corners, when using rounded corners ([#90](https://codeberg.org/dnkl/fuzzel/issues/90)). * Regression: `--dmenu --lines=0` crashing ([#92](https://codeberg.org/dnkl/fuzzel/issues/92)). ### Contributors * yangyingchao * ReplayCoding ## 1.6.5 ### Added * `--dpi-aware=no|yes|auto` command line option. * Multi-page view ([#42](https://codeberg.org/dnkl/fuzzel/issues/42)). ### Removed * Misleading error message about a non-existing cache file ([#59](https://codeberg.org/dnkl/fuzzel/issues/59)). ### Fixed * Window quickly resized when launched ([#21](https://codeberg.org/dnkl/fuzzel/issues/21)). * Layer surface being committed before configure event has been ack:ed. ## 1.6.4 ### Added * Support for [nanosvg](https://github.com/memononen/nanosvg) as an alternative SVG backend. Nanosvg is bundled with fuzzel and has **no** additional dependencies. This means you can now have SVGs without depending on Cairo. ### Changed * Meson option `-Denable-png` replaced with `-Dpng-backend=none|libpng`. * Meson option `-Denable-svg` replaced with `-Dsvg-backend=none|librsvg|nanosvg * fcft >= 3.0 is now required. * `-f,--font` now supports explicit font fallbacks. ### Fixed * Graphical corruption triggered by the “gerbview” SVG icon, from KiCAD ([#47](https://codeberg.org/dnkl/fuzzel/issues/47)). * SVG icons containing multiple icons not being limited to the main icon ([#48](https://codeberg.org/dnkl/fuzzel/issues/48)). ## 1.6.3 ### Added * `-P,--prompt` command line option, allowing you to set a custom prompt. ### Changed * `-f,--font` now supports explicit font fallbacks. ### Fixed * Removed usage of deprecated function `rsvg_handle_get_dimensions()` when building against recent versions of librsvg ([#45](https://codeberg.org/dnkl/fuzzel/issues/45)). ### Contributors * [bapt](https://codeberg.org/bapt) ## 1.6.2 ### Added * `-s,--selection-text-color` command line option, that lets you configure the foreground/text color of the currently selected item ([#37](https://codeberg.org/dnkl/fuzzel/issues/37)). ### Changed * Use `rsvg_handle_render_document()` instead of `rsvg_handle_render_cairo()` on libsrvg >= 2.46, since the latter has been deprecated ([#32](https://codeberg.org/dnkl/fuzzel/issues/32)). ### Fixed * Icons not being searched for in all icon theme instances * Crash when XKB compose file is missing ([#35](https://codeberg.org/dnkl/fuzzel/issues/35)). ## 1.6.1 ### Fixed * Wrong font being used for some entries if guessing monitor fuzzel will appear on, and guessing wrong ([#31](https://codeberg.org/dnkl/fuzzel/issues/31)). ## 1.6.0 ### Added * Text shaping support ([#20](https://codeberg.org/dnkl/fuzzel/issues/20)). * Option for vertical padding between prompt and match list. ### Changed * fcft >= 2.4.0 is now required. * In dmenu mode, fuzzel now prints the keyboard input as is, if it does not match any of the items ([#23](https://codeberg.org/dnkl/fuzzel/issues/23)). * The `.desktop` filename is now also matched against ([#25](https://codeberg.org/dnkl/fuzzel/issues/25)). ### Fixed * Set initial subpixel mode correctly when there is only one monitor. * Crash when `~/.cache/fuzzel` contained invalid/corrupt entries. ### Contributors * [loserMcloser](https://codeberg.org/loserMcloser) ## 1.5.4 ### Fixed * Icon size calculation with scaling factors > 1 ## 1.5.3 ### Fixed * Compilation when both PNGs and SVGs have been disabled. ## 1.5.2 ### Changed * Maximum icon height reduced, from the `line height`, to the `line height` minus the font's `descent`. This ensures a margin between icons. ### Fixed * Crash when compositor provided bad monitor geometry data ([#17](https://codeberg.org/dnkl/fuzzel/issues/17)). ## 1.5.1 ### Fixed * Regression: border not being rendered when `--border-radius=0`, or if fuzzel was built without cairo ([#15](https://codeberg.org/dnkl/fuzzel/issues/15)). ## 1.5.0 ### Added * meson option `-Denable-svg=[auto|enabled|disabled]`. When disabled, _librsvg_ is no longer a dependency and SVG icons are disabled. Default: `auto`. * meson option `-Denable-png=[auto|enabled|disabled]`. When disabled, _libpng_ is no longer a dependency and PNG icons are disabled. Default: `auto`. * meson option `-Denable-cairo=[auto|enabled|disabled]`. When disabled, fuzzel will not be able to draw rounded corners, nor support SVGs (regardless of what `-Denable-svg` is set to) ([#10](https://codeberg.org/dnkl/fuzzel/issues/10)). * `-I,--no-icons` command line option; disables all icons ([#12](https://codeberg.org/dnkl/fuzzel/issues/12)) * FreeBSD port. * `-x,--horizontal-pad` and `-y,--vertical-pad` command line options ([#12](https://codeberg.org/dnkl/fuzzel/issues/12)). * `--line-height` and `-letter-spacing` command line options ([#12](https://codeberg.org/dnkl/fuzzel/issues/12)). ### Changed * PNGs are now loaded and rendered with _libpng_ instead of _cairo_. ### Fixed * Wrong colors when not fully opaque. * Rendering of SVGs containing multiple icons. * Font being incorrectly scaled on rotated monitors. * PPI being calculated incorrectly. * Crash on keyboard input when repeat rate was zero (i.e. no repeat). ### Contributors * [magenbluten](https://codeberg.org/magenbluten) * jbeich ## 1.4.2 ### Fixed * Subpixel antialiasing was not applied correctly on opaque backgrounds. ## 1.4.1 ### Fixed * Incorrect extension for man pages. fuzzel/LICENSE000066400000000000000000000020561445417001200134030ustar00rootroot00000000000000MIT License Copyright (c) 2019 Daniel Eklöf 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. fuzzel/PKGBUILD000066400000000000000000000023171445417001200135220ustar00rootroot00000000000000CAIRO=enabled # disabled|enabled PNG_BACKEND=libpng # none|libpng SVG_BACKEND=librsvg # none|librsvg|nanosvg (librsvg force-enables cairo, nanosvg is bundled) pkgname=fuzzel pkgver=1.9.2 pkgrel=1 pkgdesc="Simplistic application launcher for wayland" arch=('x86_64' 'aarch64') url=https://codeberg.org/dnkl/fuzzel license=(mit) makedepends=('meson' 'ninja' 'scdoc' 'wayland-protocols' 'tllist>=1.0.1') depends=('libxkbcommon' 'wayland' 'pixman' 'fcft>=3.0.0' 'fcft<4.0.0') source=() changelog=CHANGELOG.md if [[ ${PNG_BACKEND} == libpng ]]; then depends+=( 'libpng' ) fi if [[ ${SVG_BACKEND} == librsvg ]]; then depends+=( 'librsvg' ) CAIRO=enabled fi if [[ ${CAIRO} == enabled ]]; then depends+=( 'cairo' ) fi pkgver() { cd ../.git &> /dev/null && git describe --tags --long | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || head -3 ../meson.build | grep version | cut -d "'" -f 2 } build() { meson \ --prefix=/usr \ --buildtype=release \ --wrap-mode=nofallback \ -Denable-cairo=${CAIRO} \ -Dpng-backend=${PNG_BACKEND} \ -Dsvg-backend=${SVG_BACKEND} \ .. ninja } package() { DESTDIR="${pkgdir}/" ninja install } fuzzel/README.md000066400000000000000000000045471445417001200136640ustar00rootroot00000000000000[![CI status](https://ci.codeberg.org/api/badges/dnkl/fuzzel/status.svg)](https://ci.codeberg.org/dnkl/fuzzel) # Fuzzel Fuzzel is a Wayland-native application launcher, similar to rofi's _drun_ mode. [![Packaging status](https://repology.org/badge/vertical-allrepos/fuzzel.svg)](https://repology.org/project/fuzzel/versions) ## Screenshot ![Screenshot](doc/screenshot.png) _Fuzzel, with transparency, on top of a browser window showing a diff of a fuzzel commit_ ## Features: - Wayland native - Rofi drun-like mode of operation - dmenu mode where newline separated entries are read from stdin - Emacs key bindings - Icons! - Remembers frequently launched applications ## Limitations: - No themes (but you **can** configure font and colors) ## Requirements ### Runtime * pixman * wayland (_client_ and _cursor_ libraries) * xkbcommon * cairo (optional) * libpng (optional) * librsvg (optional) * [fcft](https://codeberg.org/dnkl/fcft) [^1] [^1]: can also be built as subprojects, in which case they are statically linked. ### Building * meson * ninja * wayland protocols * scdoc * [tllist](https://codeberg.org/dnkl/tllist) [^1] ## Installation To build, first, create a build directory, and switch to it: ```sh mkdir -p bld/release && cd bld/release ``` Second, configure the build (if you intend to install it globally, you might also want `--prefix=/usr`): ```sh meson --buildtype=release \ -Denable-cairo=disabled|enabled|auto \ -Dpng-backend=none|libpng \ -Dsvg-backend=none|librsvg|nanosvg \ ../.. ``` `-D{png,svg}-backend` can be used to force-enable or force-disable a specific png and/or svg backend. Note that _nanosvg_ is builtin (i.e. it needs to external dependencies). `-Denable-cairo` can be used to force-enable or force-disable cairo support. When disabled, fuzzel will not be able to draw rounded corners, nor will it support SVGs using the _librsvg_ backend. Three, build it: ```sh ninja ``` You can now run it directly from the build directory: ```sh ./fuzzel ``` Use command line arguments to configure the look-and-feel: ```sh ./fuzzel --help ``` Optionally, install it: ```sh ninja install ``` For more detailed configuration information, see the man page: ```sh man fuzzel ``` ## License Fuzzel is released under the [MIT license](LICENSE). Fuzzel uses nanosvg, released under the [Zlib license](3rd-party/nanosvg/LICENSE.txt). fuzzel/application.c000066400000000000000000000232721445417001200150500ustar00rootroot00000000000000#include "application.h" #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "application" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" static bool push_argv(char ***argv, size_t *size, char *arg, size_t *argc) { if (arg != NULL && arg[0] == '%') return true; if (*argc >= *size) { size_t new_size = *size > 0 ? 2 * *size : 10; char **new_argv = realloc(*argv, new_size * sizeof(new_argv[0])); if (new_argv == NULL) return false; *argv = new_argv; *size = new_size; } (*argv)[(*argc)++] = arg; return true; } static bool tokenize_cmdline(char *cmdline, char ***argv) { *argv = NULL; size_t argv_size = 0; char *p = cmdline; char *search_start = p; /* points to start of current arg */ char open_quote = 0; /* the current opening quote character, 0 means none open */ size_t idx = 0; while (*p != '\0') { if (*p == '\\') { /* lookahead to handle escaped chars * only escape if not within single quotes */ if (open_quote != '\'') { /* within double quotes, only \$, \`, \", \\ should be * escaped, others should be literal */ if ((open_quote != '"' && (*(p + 1) == '\'' || *(p + 1) == ' ')) || *(p + 1) == '$' || *(p + 1) == '"' || *(p + 1) == '`' || *(p + 1) == '\\') { /* essentially delete the (first) backslash */ memmove(p, p + 1, strlen(p + 1) + 1); } } /* ignore the other cases */ } else { if (open_quote == 0 && (*p == '\'' || *p == '"')) { /* open a quote, delete the opening character but * remember that we have one open */ open_quote = *p; memmove(p, p + 1, strlen(p + 1) + 1); continue; /* don't increment p */ } else if (*p == open_quote) { /* close the quote, delete the closing character */ open_quote = 0; memmove(p, p + 1, strlen(p + 1) + 1); continue; /* don't increment p */ } else if (*p == ' ' && open_quote == 0) { /* we must not be in an argument rn * check if we can close the arg at p (exclusive) * note: passing empty quotes doesn't count as an argument */ if (p > search_start) { *p = '\0'; if (!push_argv(argv, &argv_size, search_start, &idx)) goto err; } search_start = p + 1; } } p++; } if (open_quote != 0) { LOG_ERR("unterminated %s quote\n", open_quote == '"' ? "double" : "single"); goto err; } /* edge case: argument terminated by \0 */ if (p > search_start) { if (!push_argv(argv, &argv_size, search_start, &idx)) goto err; } if (!push_argv(argv, &argv_size, NULL, &idx)) goto err; return true; err: free(*argv); return false; } bool application_execute(const struct application *app, const struct prompt *prompt, const char *launch_prefix, const char *xdg_activation_token) { const char32_t *ptext = prompt_text(prompt); const size_t c32len = c32tombs(NULL, ptext, 0); char cprompt[c32len + 1]; c32tombs(cprompt, ptext, c32len + 1); const char *execute = app != NULL ? app->exec : cprompt; const char *path = app != NULL ? app->path : NULL; const char *id = app != NULL ? app->id : NULL; LOG_DBG("exec(%s)", execute); /* Tokenize the command */ char *unescaped; char *execute_dest; size_t execute_len = strlen(execute); if (launch_prefix != NULL) { size_t launch_len = strlen(launch_prefix); unescaped = malloc(launch_len + execute_len + 2 /* whitespace + null terminator */); sprintf(unescaped, "%s ", launch_prefix); execute_dest = unescaped + launch_len + 1; if (id != NULL) { setenv("FUZZEL_DESKTOP_FILE_ID", id, 1); } else { LOG_WARN("No Desktop File ID, not setting FUZZEL_DESKTOP_FILE_ID"); } } else { unescaped = malloc(execute_len + 1); execute_dest = unescaped; } /* Substitute escape sequences for their literal character values */ for (size_t i = 0; i <= execute_len /* so null terminator is copied */; i++, execute_dest++) { if (execute[i] != '\\') { *execute_dest = execute[i]; } else { i++; switch(execute[i]) { case 's': *execute_dest = ' '; break; case 'n': *execute_dest = '\n'; break; case 't': *execute_dest = '\t'; break; case 'r': *execute_dest = '\r'; break; case ';': *execute_dest = ';'; break; case '\\': *execute_dest = '\\'; break; default: free(unescaped); LOG_ERR("invalid escaped exec argument character: %c", execute[i]); return false; } } } char **argv; if (!tokenize_cmdline(unescaped, &argv)) { free(unescaped); return false; } LOG_DBG("argv:"); for (size_t i = 0; argv[i] != NULL; i++) LOG_DBG(" %zu: \"%s\"", i, argv[i]); int pipe_fds[2]; if (pipe2(pipe_fds, O_CLOEXEC) == -1) { LOG_ERRNO("failed to create pipe"); free(unescaped); free(argv); return false; } pid_t pid = fork(); if (pid == -1) { close(pipe_fds[0]); close(pipe_fds[1]); free(unescaped); free(argv); LOG_ERRNO("failed to fork"); return false; } if (pid == 0) { /* Child */ /* Close read end */ close(pipe_fds[0]); if (path != NULL) { if (chdir(path) == -1) LOG_ERRNO("failed to chdir to %s", path); } if (xdg_activation_token != NULL) { /* Prepare client for xdg activation startup */ setenv("XDG_ACTIVATION_TOKEN", xdg_activation_token, 1); /* And X11 startup notifications */ setenv("DESKTOP_STARTUP_ID", xdg_activation_token, 1); } /* Redirect stdin/stdout/stderr -> /dev/null */ int devnull_r = open("/dev/null", O_RDONLY | O_CLOEXEC); int devnull_w = open("/dev/null", O_WRONLY | O_CLOEXEC); if (devnull_r == -1 || devnull_w == -1) goto child_err; if (dup2(devnull_r, STDIN_FILENO) == -1 || dup2(devnull_w, STDOUT_FILENO) == -1 || dup2(devnull_w, STDERR_FILENO) == -1) { goto child_err; } execvp(argv[0], argv); child_err: /* Signal error back to parent process */ (void)!write(pipe_fds[1], &errno, sizeof(errno)); _exit(1); } else { /* Parent */ free(unescaped); free(argv); /* Close write end */ close(pipe_fds[1]); int _errno; static_assert(sizeof(_errno) == sizeof(errno), "errno size mismatch"); ssize_t ret = read(pipe_fds[0], &_errno, sizeof(_errno)); if (ret == -1) LOG_ERRNO("failed to read from pipe"); else if (ret == sizeof(_errno)) LOG_ERRNO_P("%s: failed to execute", _errno, execute); else { LOG_DBG("%s: fork+exec succeeded", execute); } close(pipe_fds[0]); return ret == 0; } } struct application_list * applications_init(void) { return calloc(1, sizeof(struct application_list)); } void applications_destroy(struct application_list *apps) { if (apps == NULL) return; for (size_t i = 0; i < apps->count; i++) { struct application *app = &apps->v[i]; free(app->id); free(app->path); free(app->exec); free(app->basename); free(app->wexec); free(app->app_id); free(app->title); if (app->render_title != app->title) free(app->render_title); free(app->generic_name); free(app->comment); free(app->icon.name); tll_free_and_free(app->keywords, free); tll_free_and_free(app->categories, free); switch (app->icon.type) { case ICON_NONE: break; case ICON_PNG: if (app->icon.png != NULL) { #if defined(FUZZEL_ENABLE_PNG_LIBPNG) free(pixman_image_get_data(app->icon.png)); pixman_image_unref(app->icon.png); #endif } break; case ICON_SVG: if (app->icon.svg != NULL) { #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) g_object_unref(app->icon.svg); #elif defined(FUZZEL_ENABLE_SVG_NANOSVG) nsvgDelete(app->icon.svg); #endif } break; } tll_foreach(app->icon.rasterized, it) { struct rasterized *rast = &it->item; free(pixman_image_get_data(rast->pix)); pixman_image_unref(rast->pix); tll_remove(app->icon.rasterized, it); } free(app->icon.path); fcft_text_run_destroy(app->shaped); } free(apps->v); free(apps); } void applications_flush_text_run_cache(struct application_list *apps) { for (size_t i = 0; i < apps->count; i++) { fcft_text_run_destroy(apps->v[i].shaped); apps->v[i].shaped = NULL; } } fuzzel/application.h000066400000000000000000000036641445417001200150600ustar00rootroot00000000000000#pragma once #include #include #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) #include #endif #if defined(FUZZEL_ENABLE_SVG_NANOSVG) #include #endif #include #include #include "prompt.h" enum icon_type { ICON_NONE, ICON_PNG, ICON_SVG }; struct rasterized { pixman_image_t *pix; int size; }; typedef tll(struct rasterized) rasterized_list_t; struct icon { char *name; char *path; enum icon_type type; union { #if defined(FUZZEL_ENABLE_PNG_LIBPNG) pixman_image_t *png; #else void *png; #endif #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) RsvgHandle *svg; #elif defined(FUZZEL_ENABLE_SVG_NANOSVG) NSVGimage *svg; #else void *svg; #endif }; /* List of cached rasterizations (used with SVGs) */ rasterized_list_t rasterized; }; typedef tll(char32_t *) char32_list_t; typedef tll(char *) char_list_t; struct application { char *id; /* Desktop File ID, as defined in the Desktop Entry specicication https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html */ char *path; char *exec; char32_t *basename; char32_t *wexec; /* Same as ‘exec’, but for matching purposes */ char32_t *title; char32_t *render_title; char32_t *generic_name; char *app_id; char32_t *comment; char32_list_t keywords; char32_list_t categories; struct icon icon; bool visible; unsigned count; struct fcft_text_run *shaped; }; bool application_execute( const struct application *app, const struct prompt *prompt, const char *launch_prefix, const char *xdg_activation_token); struct application_list { struct application *v; size_t count; }; struct application_list *applications_init(void); void applications_destroy(struct application_list *apps); void applications_flush_text_run_cache(struct application_list *apps); fuzzel/char32.c000066400000000000000000000120361445417001200136230ustar00rootroot00000000000000#include "char32.h" #include #include #include #include #include #if defined __has_include #if __has_include () #include #endif #endif #define LOG_MODULE "char32" #define LOG_ENABLE_DBG 0 #include "log.h" /* * For now, assume we can map directly to the corresponding wchar_t * functions. This is true if: * * - both data types have the same size * - both use the same encoding (though we require that encoding to be UTF-32) */ _Static_assert( sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch"); #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif #if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) #error "wchar_t does not use UTF-32" #endif size_t c32len(const char32_t *s) { return wcslen((const wchar_t *)s); } int c32cmp(const char32_t *s1, const char32_t *s2) { return wcscmp((const wchar_t *)s1, (const wchar_t *)s2); } char32_t * c32cpy(char32_t *dest, const char32_t *src) { return (char32_t *)wcscpy((wchar_t *)dest, (const wchar_t *)src); } char32_t * c32cat(char32_t *dest, const char32_t *src) { return (char32_t *)wcscat((wchar_t *)dest, (const wchar_t *)src); } char32_t * c32dup(const char32_t *s) { return (char32_t *)wcsdup((const wchar_t *)s); } char32_t * c32chr(const char32_t *s, char32_t c) { return (char32_t *)wcschr((const wchar_t *)s, (wchar_t)c); } size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len) { mbstate_t ps = {0}; char32_t *out = dst; const char *in = src; size_t consumed = 0; size_t chars = 0; size_t rc; while ((out == NULL || chars < len) && consumed < nms && (rc = mbrtoc32(out, in, nms - consumed, &ps)) != 0) { switch (rc) { case 0: goto done; case (size_t)-1: case (size_t)-2: case (size_t)-3: goto err; } in += rc; consumed += rc; chars++; if (out != NULL) out++; } done: return chars; err: return (char32_t)-1; } size_t mbstoc32(char32_t *dst, const char *src, size_t len) { return mbsntoc32(dst, src, strlen(src) + 1, len); } size_t c32ntombs(char *dst, const char32_t *src, size_t nwc, size_t len) { mbstate_t ps = {0}; char *out = dst; const char32_t *in = src; size_t consumed = 0; size_t bytes = 0; size_t rc; char mb[MB_CUR_MAX]; while ((out == NULL || bytes < len) && consumed < nwc && (rc = c32rtomb(mb, *in, &ps)) != 0) { switch (rc) { case 0: goto done; case (size_t)-1: goto err; } if (out != NULL) { for (size_t i = 0; i < rc; i++, out++) *out = mb[i]; } if (*in == U'\0') break; in++; consumed++; bytes += rc; } done: return bytes; err: return (char32_t)-1; } size_t c32tombs(char *dst, const char32_t *src, size_t len) { return c32ntombs(dst, src, c32len(src) + 1, len); } char32_t * ambstoc32(const char *src) { if (src == NULL) return NULL; const size_t src_len = strlen(src); char32_t *ret = malloc((src_len + 1) * sizeof(ret[0])); if (ret == NULL) return NULL; mbstate_t ps = {0}; char32_t *out = ret; const char *in = src; const char *const end = src + src_len + 1; size_t chars = 0; size_t rc; while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) { switch (rc) { case (size_t)-1: case (size_t)-2: case (size_t)-3: goto err; } in += rc; out++; chars++; } *out = U'\0'; ret = realloc(ret, (chars + 1) * sizeof(ret[0])); return ret; err: free(ret); return NULL; } char * ac32tombs(const char32_t *src) { if (src == NULL) return NULL; const size_t src_len = c32len(src); size_t allocated = src_len + 1; char *ret = malloc(allocated); if (ret == NULL) return NULL; mbstate_t ps = {0}; char *out = ret; const char32_t *const end = src + src_len + 1; size_t bytes = 0; char mb[MB_CUR_MAX]; for (const char32_t *in = src; in < end; in++) { size_t rc = c32rtomb(mb, *in, &ps); switch (rc) { case (size_t)-1: goto err; } if (bytes + rc > allocated) { allocated *= 2; ret = realloc(ret, allocated); out = &ret[bytes]; } for (size_t i = 0; i < rc; i++, out++) *out = mb[i]; bytes += rc; } assert(ret[bytes - 1] == '\0'); ret = realloc(ret, bytes); return ret; err: free(ret); return NULL; } char32_t toc32lower(char32_t c) { return (char32_t)towlower((wint_t)c); } char32_t toc32upper(char32_t c) { return (char32_t)towupper((wint_t)c); } bool isc32space(char32_t c32) { return iswspace((wint_t)c32); } fuzzel/char32.h000066400000000000000000000014671445417001200136360ustar00rootroot00000000000000#pragma once #include #include #include #include size_t c32len(const char32_t *s); int c32cmp(const char32_t *s1, const char32_t *s2); char32_t *c32cpy(char32_t *dest, const char32_t *src); char32_t *c32cat(char32_t *dest, const char32_t *src); char32_t *c32dup(const char32_t *s); char32_t *c32chr(const char32_t *s, char32_t c); size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len); size_t mbstoc32(char32_t *dst, const char *src, size_t len); size_t c32ntombs(char *dst, const char32_t *src, size_t nwc, size_t len); size_t c32tombs(char *dst, const char32_t *src, size_t len); char32_t *ambstoc32(const char *src); char *ac32tombs(const char32_t *src); char32_t toc32lower(char32_t c); char32_t toc32upper(char32_t c); bool isc32space(char32_t c32); fuzzel/completions/000077500000000000000000000000001445417001200147275ustar00rootroot00000000000000fuzzel/completions/fish/000077500000000000000000000000001445417001200156605ustar00rootroot00000000000000fuzzel/completions/fish/fuzzel.fish000066400000000000000000000157301445417001200200600ustar00rootroot00000000000000function __fish_complete_fuzzel_output if type -q wlr-randr; wlr-randr | grep -e '^[^[:space:]]\+' | cut -d ' ' -f 1 else if type -q swaymsg; swaymsg -t get_outputs --raw|grep name|cut -d '"' -f 4 end end complete -c fuzzel complete -c fuzzel -f complete -c fuzzel -r -s c -l config -d "path to configuration file (XDG_CONFIG_HOME/fuzzel/fuzzel.ini)" complete -c fuzzel -x -s f -l font -a "(fc-list : family | sed 's/,/\n/g' | sort | uniq)" -d "font name and style in fontconfig format (monospace)" complete -c fuzzel -x -s o -l output -a "(__fish_complete_fuzzel_output)" -d "output (monitor) do display on (none)" complete -c fuzzel -x -s D -l dpi-aware -a "no yes auto" -d "scale fonts using the monitor's DPI (auto)" complete -c fuzzel -x -l icon-theme -a "(find /usr/share/icons -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 basename | sort)" -d "icon theme name (hicolor)" complete -c fuzzel -s I -l no-icons -d "do not render any icons" complete -c fuzzel -x -s F -l fields -a "filename name generic exec categories keywords comment" -d "comma separated list of XDG Desktop entry fields to match" complete -c fuzzel -x -s p -l prompt -d "string to use as input prompt (\"> \")" complete -c fuzzel -l password -a "" -d "render all input using either '*', or the specified character" # TODO: this currently doesn’t quote the completed argument complete -c fuzzel -x -s T -l terminal -a "(__fish_complete_subcommand)" -d "terminal command, with arguments ($TERMINAL -e)" complete -c fuzzel -x -s l -l lines -d "maximum number of matches to displayh (15)" complete -c fuzzel -x -s w -l width -d "window width, in characters (30)" complete -c fuzzel -x -s x -l horizontal-pad -d "horizontal padding, in pixels (40)" complete -c fuzzel -x -s y -l vertical-pad -d "vertical padding, in pixels (8)" complete -c fuzzel -x -s P -l inner-pad -d "vertical padding between prompt and matches, in pixels (0)" complete -c fuzzel -x -s b -l background -d "background color (fdf6e3dd)" complete -c fuzzel -x -s t -l text-color -d "text color (657b83ff)" complete -c fuzzel -x -s m -l match-color -d "color of matched substring (cb4b16ff)" complete -c fuzzel -x -s s -l selection-color -d "background color of selected item (eee8d5dd)" complete -c fuzzel -x -s S -l selection-text-color -d "text color of selected item (657b83ff)" complete -c fuzzel -x -s M -l selection-match-color -d "color of matched substring of selected item (cb4b16ff)" complete -c fuzzel -x -s B -l border-width -d "width of border, in pixels (1)" complete -c fuzzel -x -s r -l border-radius -d "amount of corner \"roundness\" (10)" complete -c fuzzel -x -s C -l border-color -d "border color (002b36ff)" complete -c fuzzel -l show-actions -d "include desktop actions (e.g. \"New Window\") in the list" complete -c fuzzel -l no-fuzzy -d "disable fuzzy matching" complete -c fuzzel -l filter-desktop -d "filter desktop entries based on XDG_CURRENT_DESKTOP" complete -c fuzzel -x -l fuzzy-min-length -d "search strings shorter than this will not be fuzzy matched" complete -c fuzzel -x -l fuzzy-max-length-discrepancy -d "maximum allowed length difference between the search string and a fuzzy match (2)" complete -c fuzzel -x -l fuzzy-max-distance -d "maximum allowed levenshtein distance between the search string and a fuzzy match (1)" complete -c fuzzel -x -l line-height -d "override the line height from font metrics, in points or pixels" complete -c fuzzel -x -l letter-spacing -d "additional letter spacing, in points or pixels" complete -c fuzzel -x -l layer -a "top overlay" -d "which layer to render the fuzzel window on (top)" complete -c fuzzel -s d -l dmenu -d "dmenu compatibility mode; entries are read from stdin, newline separated" complete -c fuzzel -l dmenu0 -d "dmenu compatibility mode; entries are read from stdin, NUL separated" complete -c fuzzel -l index -d "print selected entry's index instead of its text (dmenu mode only)" complete -c fuzzel -s R -l no-run-if-empty -d "exit immediately without showing the UI if stdin is empty (dmenu mode only)" complete -c fuzzel -x -s d -l log-level -a "info warning error none" -d "log-level (info)" complete -c fuzzel -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" complete -c fuzzel -s S -l log-no-syslog -d "disable syslog logging" complete -c fuzzel -s v -l version -d "show the version number and quit" complete -c fuzzel -s h -l help -d "show help message and quit" fuzzel/completions/meson.build000066400000000000000000000004311445417001200170670ustar00rootroot00000000000000zsh_install_dir = join_paths(get_option('datadir'), 'zsh/site-functions') fish_install_dir = join_paths(get_option('datadir'), 'fish', 'vendor_completions.d') install_data('zsh/_fuzzel', install_dir: zsh_install_dir) install_data('fish/fuzzel.fish', install_dir: fish_install_dir) fuzzel/completions/zsh/000077500000000000000000000000001445417001200155335ustar00rootroot00000000000000fuzzel/completions/zsh/_fuzzel000066400000000000000000000111101445417001200171260ustar00rootroot00000000000000#compdef fuzzel _arguments \ -s \ '--config[path to configuration file (XDG_CONFIG_HOME/fuzzel/fuzzel.ini)]:config:_files' \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ '(-h --help)'{-h,--help}'[show help message and quit]' \ '(-o --output)'{-o,--output}'[output (monitor) to display on (none)]:output:->outputs' \ '(-f --font)'{-f,--font}'[font name and style in fontconfig format (monospace)]:font:->fonts' \ '(-D --dpi-aware)'{-D,--dpi-aware}"[scale fonts using the monitor's DPI (auto)]:dpi_aware:(no yes auto)" \ '--icon-theme[icon theme name (hicolor)]:theme:->icon_theme' \ '(-I --no-icons)'{-I,--no-icons}'[do not render any icons]' \ '(-F --fields)'{-F,--fields}'[comma separated list of XDG Desktop entry fields to match ]:fields:->fields' \ '(-p --prompt)'{-p,--prompt}'[string to use as input prompt ("> ")]:()' \ '--password[render all input using either "*", or the specified character]:()' \ '(-T --terminal)'{-T,--terminal}'[terminal command, with arguments ($TERMINAL -e)]:terminal:_command_names -e' \ '(-l --lines)'{-l,--lines}'[maximum number of matches to display (15)]:()' \ '(-w --width)'{-w,--width}'[window width, in characters (30)]:()' \ '(-x --horizontal-pad)'{-x,--horizontal-pad}'[horizontal padding, in pixels (40)]:()' \ '(-y --vertical-pad)'{-y,--vertical-pad}'[vertical padding, in pixels (8)]:()' \ '(-P --inner-pad)'{-P,--inner-pad}'[vertical padding between prompt and matches, in pixels (0)]:()' \ '(-b --background)'{-b,--background}'[background color (fdf6e3dd)]:background:()' \ '(-t --text-color)'{-t,--text-color}'[text color (657b83ff)]:text-color:()' \ '(-m --match-color)'{-m,--match-color}'[color of matched substring (cb4b16ff)]:match-color:()' \ '(-s --selection-color)'{-s,--selection-color}'[background color of selected item (eee8d5dd)]:selection-color:()' \ '(-S --selection-text-color)'{-S,--selection-text-color}'[text color of selected item (657b83ff)]:selection-text-color:()' \ '(-M --selection-match-color)'{-M,--selection-match-color}'[color of matched substring of selected item (cb4b16ff)]:match-color:()' \ '(-B --border-width)'{-B,--border-width}'[width of border, in pixels (1)]:border-width:()' \ '(-r --border-radius)'{-r,--border-radius}'[amount of corner "roundness" (10)]:border-radius:()' \ '(-C --border-color)'{-C,--border-color}'[border color (002b36ff)]:border-color:()' \ '--show-actions[include desktop actions (e.g "New Window") in the list]' \ '--no-fuzzy[disable fuzzy matching]' \ '--filter-desktop=-[filter desktop entries based on XDG_CURRENT_DESKTOP]: :(no)' \ '--fuzzy-min-length[search strings shorter than this will not be fuzzy matched (3)]:()' \ '--fuzzy-max-length-discrepancy[maximum allowed length difference between the search string and a fuzzy match (2)]:()' \ '--fuzzy-max-distance[maximum allowed levenshtein distance between the search string and a fuzzy match (1)]:()' \ '--line-height[override line height from font metrics, in points or pixels]:()' \ '--letter-spacing[additional letter spacing, in points or pixels]:()' \ '--layer[which layer to render the fuzzel window on (top)]:layer:(top overlay)' \ '(-d --dmenu)'{-d,--dmenu}'[dmenu compatibility mode; entries are read from stdin, newline separated]' \ '--dmenu0[dmenu compatibility mode; entries are read from stdin, NUL separated]' \ "--index[print selected entry's index instead of its text (dmenu mode only)]" \ '(-R --no-run-if-empty)'{-R,--no-run-if-empty}'[exit immediately without showing the UI if stdin is empty (dmenu mode only)]' \ '--log-level[log level (info)]:loglevel:(info warning error none)' \ '--log-colorize[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '--log-no-syslog[disable syslog logging]' case "${state}" in fonts) if command -v fc-list > /dev/null; then _values 'font families' $(fc-list : family | tr -d ' ') fi ;; icon_theme) _values 'icon themes' $(find /usr/share/icons -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 basename | sort) ;; outputs) if command -v wlr-randr > /dev/null; then _values 'outputs' $(wlr-randr | grep -e '^[^[:space:]]\+' | cut -d ' ' -f 1) elif command -v swaymsg > /dev/null; then _values 'outputs' $(swaymsg -t get_outputs --raw|grep name|cut -d '"' -f 4) fi ;; fields) _values -s , 'Desktop entry fields' filename name generic exec categories keywords comment ;; esac fuzzel/config.c000066400000000000000000001252561445417001200140170ustar00rootroot00000000000000#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "config" #define LOG_ENABLE_DBG 0 #include "log.h" #include "key-binding.h" #define ALEN(v) (sizeof(v) / sizeof((v)[0])) static const char *const binding_action_map[] = { [BIND_ACTION_NONE] = NULL, [BIND_ACTION_CANCEL] = "cancel", [BIND_ACTION_CURSOR_HOME] = "cursor-home", [BIND_ACTION_CURSOR_END] = "cursor-end", [BIND_ACTION_CURSOR_LEFT] = "cursor-left", [BIND_ACTION_CURSOR_LEFT_WORD] = "cursor-left-word", [BIND_ACTION_CURSOR_RIGHT] = "cursor-right", [BIND_ACTION_CURSOR_RIGHT_WORD] = "cursor-right-word", [BIND_ACTION_DELETE_PREV] = "delete-prev", [BIND_ACTION_DELETE_PREV_WORD] = "delete-prev-word", [BIND_ACTION_DELETE_NEXT] = "delete-next", [BIND_ACTION_DELETE_NEXT_WORD] = "delete-next-word", [BIND_ACTION_DELETE_LINE] = "delete-line", [BIND_ACTION_INSERT_SELECTED] = "insert-selected", [BIND_ACTION_MATCHES_EXECUTE] = "execute", [BIND_ACTION_MATCHES_EXECUTE_OR_NEXT] = "execute-or-next", [BIND_ACTION_MATCHES_PREV] = "prev", [BIND_ACTION_MATCHES_PREV_WITH_WRAP] = "prev-with-wrap", [BIND_ACTION_MATCHES_PREV_PAGE] = "prev-page", [BIND_ACTION_MATCHES_NEXT] = "next", [BIND_ACTION_MATCHES_NEXT_WITH_WRAP] = "next-with-wrap", [BIND_ACTION_MATCHES_NEXT_PAGE] = "next-page", [BIND_ACTION_MATCHES_FIRST] = "first", [BIND_ACTION_MATCHES_LAST] = "last", [BIND_ACTION_CUSTOM_1] = "custom-1", [BIND_ACTION_CUSTOM_2] = "custom-2", [BIND_ACTION_CUSTOM_3] = "custom-3", [BIND_ACTION_CUSTOM_4] = "custom-4", [BIND_ACTION_CUSTOM_5] = "custom-5", [BIND_ACTION_CUSTOM_6] = "custom-6", [BIND_ACTION_CUSTOM_7] = "custom-7", [BIND_ACTION_CUSTOM_8] = "custom-8", [BIND_ACTION_CUSTOM_9] = "custom-9", [BIND_ACTION_CUSTOM_10] = "custom-10", [BIND_ACTION_CUSTOM_11] = "custom-11", [BIND_ACTION_CUSTOM_12] = "custom-12", [BIND_ACTION_CUSTOM_13] = "custom-13", [BIND_ACTION_CUSTOM_14] = "custom-14", [BIND_ACTION_CUSTOM_15] = "custom-15", [BIND_ACTION_CUSTOM_16] = "custom-16", [BIND_ACTION_CUSTOM_17] = "custom-17", [BIND_ACTION_CUSTOM_18] = "custom-18", [BIND_ACTION_CUSTOM_19] = "custom-19", }; struct context { struct config *conf; const char *section; const char *key; const char *value; const char *path; unsigned lineno; bool errors_are_fatal; }; #define LOG_CONTEXTUAL_ERR(...) \ log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) #define LOG_CONTEXTUAL_WARN(...) \ log_contextual(ctx, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) #define LOG_CONTEXTUAL_ERRNO(...) \ log_contextual_errno(ctx, __FILE__, __LINE__, __VA_ARGS__) static void __attribute__((format(printf, 5, 6))) log_contextual(struct context *ctx, enum log_class log_class, const char *file, int lineno, const char *fmt, ...) { char *formatted_msg = NULL; va_list va; va_start(va, fmt); if (vasprintf(&formatted_msg, fmt, va) < 0) { va_end(va); return; } va_end(va); bool print_dot = ctx->key != NULL; bool print_colon = ctx->value != NULL; if (!print_dot) ctx->key = ""; if (!print_colon) ctx->value = ""; log_msg( log_class, LOG_MODULE, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); free(formatted_msg); } struct config_file { char *path; /* Full, absolute, path */ int fd; /* FD of file, O_RDONLY */ }; static const char * get_user_home_dir(void) { const struct passwd *passwd = getpwuid(getuid()); if (passwd == NULL) return NULL; return passwd->pw_dir; } static struct config_file open_config(void) { char *path = NULL; struct config_file ret = {.path = NULL, .fd = -1}; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); const char *home_dir = get_user_home_dir(); char *xdg_config_dirs_copy = NULL; /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') { if (asprintf(&path, "%s/fuzzel/fuzzel.ini", xdg_config_home) < 0) { LOG_ERRNO("failed to build fuzzel.ini path"); goto done; } } else if (home_dir != NULL) { if (asprintf(&path, "%s/.config/fuzzel/fuzzel.ini", home_dir) < 0) { LOG_ERRNO("failed to build fuzzel.ini path"); goto done; } } if (path != NULL) { int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd >= 0) { ret = (struct config_file) {.path = path, .fd = fd}; path = NULL; goto done; } } xdg_config_dirs_copy = xdg_config_dirs != NULL && xdg_config_dirs[0] != '\0' ? strdup(xdg_config_dirs) : strdup("/etc/xdg"); if (xdg_config_dirs_copy == NULL || xdg_config_dirs_copy[0] == '\0') goto done; for (const char *conf_dir = strtok(xdg_config_dirs_copy, ":"); conf_dir != NULL; conf_dir = strtok(NULL, ":")) { free(path); path = NULL; if (asprintf(&path, "%s/fuzzel/fuzzel.ini", conf_dir) < 0) { LOG_ERRNO("failed to build fuzzel.ini path"); goto done; } int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd >= 0) { ret = (struct config_file){.path = path, .fd = fd}; path = NULL; goto done; } } done: free(xdg_config_dirs_copy); free(path); return ret; } static void free_key_binding_list(struct config_key_binding_list *bindings) { free(bindings->arr); bindings->arr = NULL; bindings->count = 0; } static bool parse_modifiers(struct context *ctx, const char *text, size_t len, struct config_key_modifiers *modifiers) { bool ret = false; *modifiers = (struct config_key_modifiers){0}; /* Handle "none" separately because e.g. none+shift is nonsense */ if (strncmp(text, "none", len) == 0) return true; char *copy = strndup(text, len); for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); key != NULL; key = strtok_r(NULL, "+", &tok_ctx)) { if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0) modifiers->shift = true; else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0) modifiers->ctrl = true; else if (strcmp(key, XKB_MOD_NAME_ALT) == 0) modifiers->alt = true; else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0) modifiers->super = true; else { LOG_CONTEXTUAL_ERR("not a valid modifier name: %s", key); goto out; } } ret = true; out: free(copy); return ret; } static void remove_from_key_bindings_list(struct config_key_binding_list *bindings, int action) { size_t remove_first_idx = 0; size_t remove_count = 0; for (size_t i = 0; i < bindings->count; i++) { struct config_key_binding *binding = &bindings->arr[i]; if (binding->action != action) continue; if (remove_count++ == 0) remove_first_idx = i; assert(remove_first_idx + remove_count - 1 == i); //free_key_binding(binding); } if (remove_count == 0) return; size_t move_count = bindings->count - (remove_first_idx + remove_count); memmove( &bindings->arr[remove_first_idx], &bindings->arr[remove_first_idx + remove_count], move_count * sizeof(bindings->arr[0])); bindings->count -= remove_count; } static bool value_to_key_combos(struct context *ctx, int action, struct config_key_binding_list *bindings) { if (strcasecmp(ctx->value, "none") == 0) { remove_from_key_bindings_list(bindings, action); return true; } /* Count number of combinations */ size_t combo_count = 1; for (const char *p = strchr(ctx->value, ' '); p != NULL; p = strchr(p + 1, ' ')) { combo_count++; } struct config_key_binding new_combos[combo_count]; char *copy = strdup(ctx->value); size_t idx = 0; for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx), idx++) { struct config_key_binding *new_combo = &new_combos[idx]; new_combo->action = action; new_combo->path = ctx->path; new_combo->lineno = ctx->lineno; char *key = strrchr(combo, '+'); if (key == NULL) { /* No modifiers */ key = combo; new_combo->modifiers = (struct config_key_modifiers){0}; } else { *key = '\0'; if (!parse_modifiers(ctx, combo, key - combo, &new_combo->modifiers)) goto err; key++; /* Skip past the '+' */ } /* Translate key name to symbol */ new_combo->k.sym = xkb_keysym_from_name(key, 0); if (new_combo->k.sym == XKB_KEY_NoSymbol) { LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key); goto err; } } if (idx == 0) { LOG_CONTEXTUAL_ERR( "empty binding not allowed (set to 'none' to unmap)"); goto err; } remove_from_key_bindings_list(bindings, action); bindings->arr = realloc( bindings->arr, (bindings->count + combo_count) * sizeof(bindings->arr[0])); memcpy(&bindings->arr[bindings->count], new_combos, combo_count * sizeof(bindings->arr[0])); bindings->count += combo_count; free(copy); return true; err: free(copy); return false; } static bool modifiers_equal(const struct config_key_modifiers *mods1, const struct config_key_modifiers *mods2) { bool shift = mods1->shift == mods2->shift; bool alt = mods1->alt == mods2->alt; bool ctrl = mods1->ctrl == mods2->ctrl; bool super = mods1->super == mods2->super; return shift && alt && ctrl && super; } static char * modifiers_to_str(const struct config_key_modifiers *mods) { char *ret = NULL; if (asprintf(&ret, "%s%s%s%s", mods->ctrl ? XKB_MOD_NAME_CTRL "+" : "", mods->alt ? XKB_MOD_NAME_ALT "+": "", mods->super ? XKB_MOD_NAME_LOGO "+": "", mods->shift ? XKB_MOD_NAME_SHIFT "+": "") < 0) return NULL; return ret; } static bool resolve_key_binding_collisions(struct config *conf, const char *section_name, const char *const action_map[], struct config_key_binding_list *bindings) { bool ret = true; for (size_t i = 1; i < bindings->count; i++) { enum { COLLISION_NONE, COLLISION_BINDING, } collision_type = COLLISION_NONE; const struct config_key_binding *collision_binding = NULL; struct config_key_binding *binding1 = &bindings->arr[i]; assert(binding1->action != BIND_ACTION_NONE); const struct config_key_modifiers *mods1 = &binding1->modifiers; /* Does our binding collide with another binding? */ for (ssize_t j = i - 1; collision_type == COLLISION_NONE && j >= 0; j--) { const struct config_key_binding *binding2 = &bindings->arr[j]; assert(binding2->action != BIND_ACTION_NONE); if (binding2->action == binding1->action) continue; const struct config_key_modifiers *mods2 = &binding2->modifiers; bool mods_equal = modifiers_equal(mods1, mods2); bool sym_equal; sym_equal = binding1->k.sym == binding2->k.sym; if (!mods_equal || !sym_equal) continue; collision_binding = binding2; collision_type = COLLISION_BINDING; break; } if (collision_type != COLLISION_NONE) { char *modifier_names = modifiers_to_str(mods1); char sym_name[64]; xkb_keysym_get_name(binding1->k.sym, sym_name, sizeof(sym_name)); switch (collision_type) { case COLLISION_NONE: break; case COLLISION_BINDING: { LOG_ERR( "%s:%d: [%s].%s: %s%s already mapped to '%s'", binding1->path, binding1->lineno, section_name, action_map[binding1->action], modifier_names, sym_name, action_map[collision_binding->action]); ret = false; break; } } free(modifier_names); //free_key_binding(binding1); /* Remove the most recent binding */ size_t move_count = bindings->count - (i + 1); memmove(&bindings->arr[i], &bindings->arr[i + 1], move_count * sizeof(bindings->arr[0])); bindings->count--; i--; } } return ret; } static bool str_to_ulong(const char *s, int base, unsigned long *res) { if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtoul(s, &end, base); return errno == 0 && *end == '\0'; } static bool str_to_uint32(const char *s, int base, uint32_t *res) { unsigned long v; bool ret = str_to_ulong(s, base, &v); if (!ret) return false; if (v > UINT32_MAX) return false; *res = v; return true; } static bool value_to_str(struct context *ctx, char **res) { free(*res); *res = strdup(ctx->value); return true; } static bool value_to_wchars(struct context *ctx, char32_t **res) { char32_t *s = ambstoc32(ctx->value); if (s == NULL) { LOG_CONTEXTUAL_ERR("not a valie string value"); return false; } free(*res); *res = s; return true; } static bool value_to_bool(struct context *ctx, bool *res) { static const char *const yes[] = {"on", "true", "yes", "1"}; static const char *const no[] = {"off", "false", "no", "0"}; for (size_t i = 0; i < ALEN(yes); i++) { if (strcasecmp(ctx->value, yes[i]) == 0) { *res = true; return true; } } for (size_t i = 0; i < ALEN(no); i++) { if (strcasecmp(ctx->value, no[i]) == 0) { *res = false; return true; } } LOG_CONTEXTUAL_ERR("invalid boolean value"); return false; } static bool value_to_uint32(struct context *ctx, int base, uint32_t *res) { if (!str_to_uint32(ctx->value, base, res)){ LOG_CONTEXTUAL_ERR( "invalid integer value, or outside range 0-%u", UINT32_MAX); return false; } return true; } static bool value_to_color(struct context *ctx, bool allow_alpha, struct rgba *color) { const char *clr_start = ctx->value; if (clr_start[0] == '#') clr_start++; if (strlen(clr_start) != 8) { LOG_CONTEXTUAL_ERR("not a valid color value"); return false; } uint32_t v; if (!str_to_uint32(clr_start, 16, &v)) { LOG_CONTEXTUAL_ERR("not a valid color value"); return false; } if (!allow_alpha && (v & 0x000000ff) != 0) { LOG_CONTEXTUAL_ERR("color value must not have an alpha component"); return false; } *color = conf_hex_to_rgba(v); return true; } static bool value_to_double(struct context *ctx, float *res) { const char *s = ctx->value; if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtof(s, &end); if (!(errno == 0 && *end == '\0')) { LOG_CONTEXTUAL_ERR("invalid decimal value"); return false; } return true; } static bool value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) { const char *s = ctx->value; size_t len = s != NULL ? strlen(s) : 0; if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') { errno = 0; char *end = NULL; long value = strtol(s, &end, 10); if (!(errno == 0 && end == s + len - 2)) { LOG_CONTEXTUAL_ERR("invalid px value (must be on the form 12px)"); return false; } res->pt = 0; res->px = value; } else { float value; if (!value_to_double(ctx, &value)) return false; res->pt = value; res->px = 0; } return true; } static bool value_to_enum(struct context *ctx, const char **value_map, int *res) { size_t str_len = 0; size_t count = 0; for (; value_map[count] != NULL; count++) { if (strcasecmp(value_map[count], ctx->value) == 0) { *res = count; return true; } str_len += strlen(value_map[count]); } const size_t size = str_len + count * 4 + 1; char valid_values[512]; size_t idx = 0; assert(size < sizeof(valid_values)); for (size_t i = 0; i < count; i++) idx += snprintf(&valid_values[idx], size - idx, "'%s', ", value_map[i]); if (count > 0) valid_values[idx - 2] = '\0'; LOG_CONTEXTUAL_ERR("not one of %s", valid_values); *res = -1; return false; } static bool parse_config_file( FILE *f, struct config *conf, const char *path, bool errors_are_fatal); static bool parse_section_main(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; const char *value = ctx->value; if (strcmp(key, "output") == 0) return value_to_str(ctx, &conf->output); else if (strcmp(key, "font") == 0) return value_to_str(ctx, &conf->font); else if (strcmp(key, "dpi-aware") == 0) { if (strcmp(value, "auto") == 0) conf->dpi_aware = DPI_AWARE_AUTO; else { bool value; if (!value_to_bool(ctx, &value)) return false; conf->dpi_aware = value ? DPI_AWARE_YES : DPI_AWARE_NO; } return true; } else if (strcmp(key, "prompt") == 0) return value_to_wchars(ctx, &conf->prompt); else if (strcmp(key, "icon-theme") == 0) return value_to_str(ctx, &conf->icon_theme); else if (strcmp(key, "icons-enabled") == 0) return value_to_bool(ctx, &conf->icons_enabled); else if (strcmp(key, "fields") == 0) { _Static_assert(sizeof(conf->match_fields) == sizeof(int), "enum is not 32-bit"); enum match_fields match_fields = 0; char *copy = strdup(value); for (const char *field = strtok(copy, ","); field != NULL; field = strtok(NULL, ",")) { static const struct { const char *name; enum match_fields value; } map[] = { {"filename", MATCH_FILENAME}, {"name", MATCH_NAME}, {"generic", MATCH_GENERIC}, {"exec", MATCH_EXEC}, {"categories", MATCH_CATEGORIES}, {"keywords", MATCH_KEYWORDS}, {"comment", MATCH_COMMENT}, }; enum match_fields field_value = 0; for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { if (strcmp(field, map[i].name) == 0) { field_value = map[i].value; break; } } if (field_value == 0) { LOG_CONTEXTUAL_ERR( "invalid field name \"%s\", " "must be one of: " "\"filename\", \"name\", \"generic\", \"exec\", " "\"categories\", \"keywords\", \"comment\"", field); free(copy); return false; } match_fields |= field_value; } conf->match_fields = match_fields; free(copy); return true; } else if (strcmp(key, "password-character") == 0) { char32_t *password_chars = ambstoc32(value); if (password_chars == NULL || c32len(password_chars) != 1) { LOG_CONTEXTUAL_ERR("invalid password character"); free(password_chars); return false; } conf->password_mode.character = password_chars[0]; free(password_chars); return true; } else if (strcmp(key, "filter-desktop") == 0) return value_to_bool(ctx, &conf->filter_desktop); else if (strcmp(key, "fuzzy") == 0) return value_to_bool(ctx, &conf->fuzzy.enabled); else if (strcmp(key, "show-actions") == 0) return value_to_bool(ctx, &conf->actions_enabled); else if (strcmp(key, "terminal") == 0) return value_to_str(ctx, &conf->terminal); else if (strcmp(key, "launch-prefix") == 0) return value_to_str(ctx, &conf->launch_prefix); else if (strcmp(key, "lines") == 0) return value_to_uint32(ctx, 10, &conf->lines); else if (strcmp(key, "width") == 0) return value_to_uint32(ctx, 10, &conf->chars); else if (strcmp(key, "tabs") == 0) return value_to_uint32(ctx, 10, &conf->tabs); else if (strcmp(key, "horizontal-pad") == 0) return value_to_uint32(ctx, 10, &conf->pad.x); else if (strcmp(key, "vertical-pad") == 0) return value_to_uint32(ctx, 10, &conf->pad.y); else if (strcmp(key, "inner-pad") == 0) return value_to_uint32(ctx, 10, &conf->pad.inner); else if (strcmp(key, "line-height") == 0) return value_to_pt_or_px(ctx, &conf->line_height); else if (strcmp(key, "letter-spacing") == 0) return value_to_pt_or_px(ctx, &conf->letter_spacing); else if (strcmp(key, "image-size-ratio") == 0) { float ratio; if (!value_to_double(ctx, &ratio)) return false; if (ratio < 0. || ratio > 1.) { LOG_CONTEXTUAL_ERR("not in range 0.0 - 1.0"); return false; } conf->image_size_ratio = ratio; return true; } else if (strcmp(key, "layer") == 0) { if (strcasecmp(value, "top") == 0) { conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; return true; } else if (strcasecmp(value, "overlay") == 0) { conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; return true; } LOG_CONTEXTUAL_ERR("not one of 'top', 'overay'"); return false; } else if (strcmp(key, "exit-on-keyboard-focus-loss") == 0) return value_to_bool(ctx, &conf->exit_on_kb_focus_loss); else LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } static bool parse_section_colors(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (strcmp(key, "background") == 0) return value_to_color(ctx, true, &conf->colors.background); else if (strcmp(key, "text") == 0) return value_to_color(ctx, true, &conf->colors.text); else if (strcmp(key, "match") == 0) return value_to_color(ctx, true, &conf->colors.match); else if (strcmp(key, "selection") == 0) return value_to_color(ctx, true, &conf->colors.selection); else if (strcmp(key, "selection-text") == 0) return value_to_color(ctx, true, &conf->colors.selection_text); else if (strcmp(key, "selection-match") == 0) return value_to_color(ctx, true, &conf->colors.selection_match); else if (strcmp(key, "border") == 0) return value_to_color(ctx, true, &conf->colors.border); else LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } static bool parse_section_border(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (strcmp(key, "width") == 0) return value_to_uint32(ctx, 10, &conf->border.size); else if (strcmp(key, "radius") == 0) #if defined(FUZZEL_ENABLE_CAIRO) return value_to_uint32(ctx, 10, &conf->border.radius); #else { LOG_CONTEXTUAL_WARN( "fuzzel compiled without cairo support; ignoring"); return true; } #endif else LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } static bool parse_section_dmenu(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (strcmp(key, "mode") == 0) { _Static_assert(sizeof(conf->dmenu.mode) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"text", "index", NULL}, (int *)&conf->dmenu.mode); } else if (strcmp(key, "exit-immediately-if-empty") == 0) return value_to_bool(ctx, &conf->dmenu.exit_immediately_if_empty); else LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } static bool parse_section_key_bindings(struct context *ctx) { for (int action = 0; action < ALEN(binding_action_map); action++) { if (binding_action_map[action] == NULL) continue; if (strcmp(ctx->key, binding_action_map[action]) != 0) continue; if (!value_to_key_combos(ctx, action, &ctx->conf->key_bindings)) return false; return true; } LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); return false; } enum section { SECTION_MAIN, SECTION_COLORS, SECTION_BORDER, SECTION_DMENU, SECTION_KEY_BINDINGS, SECTION_COUNT, }; /* Function pointer, called for each key/value line */ typedef bool (*parser_fun_t)(struct context *ctx); static const struct { parser_fun_t fun; const char *name; } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, [SECTION_BORDER] = {&parse_section_border, "border"}, [SECTION_DMENU] = {&parse_section_dmenu, "dmenu"}, [SECTION_KEY_BINDINGS] = {&parse_section_key_bindings, "key-bindings"}, }; static enum section str_to_section(const char *str) { for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { if (strcmp(str, section_info[section].name) == 0) return section; } return SECTION_COUNT; } static bool parse_key_value(char *kv, const char **section, const char **key, const char **value) { bool section_is_needed = section != NULL; /* Strip leading whitespace */ while (isspace(kv[0])) ++kv; if (section_is_needed) *section = "main"; if (kv[0] == '=') return false; *key = kv; *value = NULL; size_t kvlen = strlen(kv); /* Strip trailing whitespace */ while (isspace(kv[kvlen - 1])) kvlen--; kv[kvlen] = '\0'; for (size_t i = 0; i < kvlen; ++i) { if (kv[i] == '.' && section_is_needed) { section_is_needed = false; *section = kv; kv[i] = '\0'; if (i == kvlen - 1 || kv[i + 1] == '=') { *key = NULL; return false; } *key = &kv[i + 1]; } else if (kv[i] == '=') { kv[i] = '\0'; if (i != kvlen - 1) *value = &kv[i + 1]; break; } } if (*value == NULL) return false; /* Strip trailing whitespace from key (leading stripped earlier) */ { assert(!isspace(*key[0])); char *end = (char *)*key + strlen(*key) - 1; while (isspace(end[0])) end--; end[1] = '\0'; } /* Strip leading whitespace from value (trailing stripped earlier) */ while (isspace(*value[0])) ++*value; /* Un-quote * * Note: this is very simple; we only support the *entire* value * being quoted. That is, no mid-value quotes. Both double and * single quotes are supported. * * - key="value" OK * - key=abc "quote" def NOT OK * - key=’value’ OK * * Finally, we support escaping the quote character, and the * escape character itself: * * - key="value \"quotes\"" * - key="backslash: \\" * * ONLY the "current" quote character can be escaped: * * key="value \'" NOt OK (both backslash and single quote is kept) */ { char *end = (char *)*value + strlen(*value) - 1; if ((*value[0] == '"' && *end == '"') || (*value[0] == '\'' && *end == '\'')) { const char quote = (*value)[0]; (*value)++; *end = '\0'; /* Un-escape */ for (char *p = (char *)*value; *p != '\0'; p++) { if (p[0] == '\\' && (p[1] == '\\' || p[1] == quote)) { memmove(p, p + 1, end - p); } } } } return true; } static bool parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal) { enum section section = SECTION_MAIN; char *_line = NULL; size_t count = 0; bool ret = true; #define error_or_continue() \ { \ if (errors_are_fatal) { \ ret = false; \ goto done; \ } else \ continue; \ } char *section_name = strdup("main"); struct context context = { .conf = conf, .section = section_name, .path = path, .lineno = 0, .errors_are_fatal = errors_are_fatal, }; struct context *ctx = &context; /* For LOG_AND_*() */ errno = 0; ssize_t len; while ((len = getline(&_line, &count, f)) != -1) { context.key = NULL; context.value = NULL; context.lineno++; char *line = _line; /* Strip leading whitespace */ while (isspace(line[0])) { line++; len--; } /* Empty line, or comment */ if (line[0] == '\0' || line[0] == '#') continue; /* Strip the trailing newline - may be absent on the last line */ if (line[len - 1] == '\n') line[--len] = '\0'; /* Split up into key/value pair + trailing comment separated by blank */ char *key_value = line; char *kv_trailing = &line[len - 1]; char *comment = &line[1]; while (comment[1] != '\0') { if (isblank(comment[0]) && comment[1] == '#') { comment[1] = '\0'; /* Terminate key/value pair */ kv_trailing = comment++; break; } comment++; } comment++; /* Strip trailing whitespace */ while (isspace(kv_trailing[0])) kv_trailing--; kv_trailing[1] = '\0'; /* Check for new section */ if (key_value[0] == '[') { key_value++; if (key_value[0] == ']') { LOG_CONTEXTUAL_ERR("empty section name"); section = SECTION_COUNT; error_or_continue(); } char *end = strchr(key_value, ']'); if (end == NULL) { context.section = key_value; LOG_CONTEXTUAL_ERR("syntax error: no closing ']'"); context.section = section_name; section = SECTION_COUNT; error_or_continue(); } end[0] = '\0'; if (end[1] != '\0') { context.section = key_value; LOG_CONTEXTUAL_ERR("section declaration contains trailing " "characters"); context.section = section_name; section = SECTION_COUNT; error_or_continue(); } section = str_to_section(key_value); if (section == SECTION_COUNT) { context.section = key_value; LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value); context.section = section_name; error_or_continue(); } free(section_name); section_name = strdup(key_value); context.section = section_name; /* Process next line */ continue; } if (section >= SECTION_COUNT) { /* Last section name was invalid; ignore all keys in it */ continue; } if (!parse_key_value(key_value, NULL, &context.key, &context.value)) { LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", context.key == NULL ? "key" : "value"); error_or_continue(); } LOG_DBG("section=%s, key='%s', value='%s', comment='%s'", section_info[section].name, context.key, context.value, comment); assert(section >= 0 && section < SECTION_COUNT); parser_fun_t section_parser = section_info[section].fun; assert(section_parser != NULL); if (!section_parser(ctx)) error_or_continue(); } if (errno != 0) { LOG_ERRNO("failed to read from configuration"); if (errors_are_fatal) ret = false; } done: free(section_name); free(_line); return ret; } static bool overrides_apply(struct config *conf, const config_override_t *overrides, bool errors_are_fatal) { if (overrides == NULL) goto resolve_key_bindings; struct context context = { .conf = conf, .path = "override", .lineno = 0, .errors_are_fatal = errors_are_fatal, }; struct context *ctx = &context; tll_foreach(*overrides, it) { context.lineno++; if (!parse_key_value( it->item, &context.section, &context.key, &context.value)) { LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", context.key == NULL ? "key" : "value"); if (errors_are_fatal) return false; continue; } if (context.section[0] == '\0') { LOG_CONTEXTUAL_ERR("empty section name"); if (errors_are_fatal) return false; continue; } enum section section = str_to_section(context.section); if (section == SECTION_COUNT) { LOG_CONTEXTUAL_ERR("invalid section name: %s", context.section); if (errors_are_fatal) return false; continue; } parser_fun_t section_parser = section_info[section].fun; assert(section_parser != NULL); if (!section_parser(ctx)) { if (errors_are_fatal) return false; continue; } } resolve_key_bindings: return resolve_key_binding_collisions( conf, section_info[SECTION_KEY_BINDINGS].name, binding_action_map, &conf->key_bindings); } #define m_none {0} #define m_alt {.alt = true} #define m_ctrl {.ctrl = true} #define m_shift {.shift = true} #define m_ctrl_shift {.ctrl = true, .shift = true} static void add_default_key_bindings(struct config *conf) { static const struct config_key_binding bindings[] = { {BIND_ACTION_CANCEL, m_none, {{XKB_KEY_Escape}}}, {BIND_ACTION_CANCEL, m_ctrl, {{XKB_KEY_g}}}, {BIND_ACTION_CURSOR_HOME, m_none, {{XKB_KEY_Home}}}, {BIND_ACTION_CURSOR_HOME, m_ctrl, {{XKB_KEY_a}}}, {BIND_ACTION_CURSOR_END, m_none, {{XKB_KEY_End}}}, {BIND_ACTION_CURSOR_END, m_ctrl, {{XKB_KEY_e}}}, {BIND_ACTION_CURSOR_LEFT, m_ctrl, {{XKB_KEY_b}}}, {BIND_ACTION_CURSOR_LEFT, m_none, {{XKB_KEY_Left}}}, {BIND_ACTION_CURSOR_LEFT_WORD, m_alt, {{XKB_KEY_b}}}, {BIND_ACTION_CURSOR_LEFT_WORD, m_ctrl, {{XKB_KEY_Left}}}, {BIND_ACTION_CURSOR_RIGHT, m_ctrl, {{XKB_KEY_f}}}, {BIND_ACTION_CURSOR_RIGHT, m_none, {{XKB_KEY_Right}}}, {BIND_ACTION_CURSOR_RIGHT_WORD, m_alt, {{XKB_KEY_f}}}, {BIND_ACTION_CURSOR_RIGHT_WORD, m_ctrl, {{XKB_KEY_Right}}}, {BIND_ACTION_DELETE_PREV, m_none, {{XKB_KEY_BackSpace}}}, {BIND_ACTION_DELETE_PREV_WORD, m_ctrl, {{XKB_KEY_BackSpace}}}, {BIND_ACTION_DELETE_PREV_WORD, m_alt, {{XKB_KEY_BackSpace}}}, {BIND_ACTION_DELETE_NEXT, m_none, {{XKB_KEY_Delete}}}, {BIND_ACTION_DELETE_NEXT, m_none, {{XKB_KEY_KP_Delete}}}, {BIND_ACTION_DELETE_NEXT, m_ctrl, {{XKB_KEY_d}}}, {BIND_ACTION_DELETE_NEXT_WORD, m_alt, {{XKB_KEY_d}}}, {BIND_ACTION_DELETE_NEXT_WORD, m_ctrl, {{XKB_KEY_Delete}}}, {BIND_ACTION_DELETE_NEXT_WORD, m_ctrl, {{XKB_KEY_KP_Delete}}}, {BIND_ACTION_DELETE_LINE, m_ctrl, {{XKB_KEY_k}}}, {BIND_ACTION_INSERT_SELECTED, m_ctrl, {{XKB_KEY_Tab}}}, {BIND_ACTION_MATCHES_EXECUTE, m_none, {{XKB_KEY_Return}}}, {BIND_ACTION_MATCHES_EXECUTE, m_none, {{XKB_KEY_KP_Enter}}}, {BIND_ACTION_MATCHES_EXECUTE, m_ctrl, {{XKB_KEY_y}}}, {BIND_ACTION_MATCHES_EXECUTE_OR_NEXT, m_none, {{XKB_KEY_Tab}}}, {BIND_ACTION_MATCHES_PREV, m_none, {{XKB_KEY_Up}}}, {BIND_ACTION_MATCHES_PREV, m_ctrl, {{XKB_KEY_p}}}, {BIND_ACTION_MATCHES_PREV_WITH_WRAP, m_none, {{XKB_KEY_ISO_Left_Tab}}}, {BIND_ACTION_MATCHES_PREV_PAGE, m_none, {{XKB_KEY_Page_Up}}}, {BIND_ACTION_MATCHES_PREV_PAGE, m_none, {{XKB_KEY_KP_Page_Up}}}, {BIND_ACTION_MATCHES_NEXT, m_none, {{XKB_KEY_Down}}}, {BIND_ACTION_MATCHES_NEXT, m_ctrl, {{XKB_KEY_n}}}, {BIND_ACTION_MATCHES_NEXT_PAGE, m_none, {{XKB_KEY_Page_Down}}}, {BIND_ACTION_MATCHES_NEXT_PAGE, m_none, {{XKB_KEY_KP_Page_Down}}}, {BIND_ACTION_MATCHES_FIRST, m_ctrl, {{XKB_KEY_Home}}}, {BIND_ACTION_MATCHES_LAST, m_ctrl, {{XKB_KEY_End}}}, {BIND_ACTION_CUSTOM_1, m_alt, {{XKB_KEY_1}}}, {BIND_ACTION_CUSTOM_2, m_alt, {{XKB_KEY_2}}}, {BIND_ACTION_CUSTOM_3, m_alt, {{XKB_KEY_3}}}, {BIND_ACTION_CUSTOM_4, m_alt, {{XKB_KEY_4}}}, {BIND_ACTION_CUSTOM_5, m_alt, {{XKB_KEY_5}}}, {BIND_ACTION_CUSTOM_6, m_alt, {{XKB_KEY_6}}}, {BIND_ACTION_CUSTOM_7, m_alt, {{XKB_KEY_7}}}, {BIND_ACTION_CUSTOM_8, m_alt, {{XKB_KEY_8}}}, {BIND_ACTION_CUSTOM_9, m_alt, {{XKB_KEY_9}}}, {BIND_ACTION_CUSTOM_10, m_alt, {{XKB_KEY_0}}}, {BIND_ACTION_CUSTOM_11, m_alt, {{XKB_KEY_exclam}}}, {BIND_ACTION_CUSTOM_12, m_alt, {{XKB_KEY_at}}}, {BIND_ACTION_CUSTOM_13, m_alt, {{XKB_KEY_numbersign}}}, {BIND_ACTION_CUSTOM_14, m_alt, {{XKB_KEY_dollar}}}, {BIND_ACTION_CUSTOM_15, m_alt, {{XKB_KEY_percent}}}, {BIND_ACTION_CUSTOM_16, m_alt, {{XKB_KEY_dead_circumflex}}}, {BIND_ACTION_CUSTOM_17, m_alt, {{XKB_KEY_ampersand}}}, {BIND_ACTION_CUSTOM_18, m_alt, {{XKB_KEY_asterisk}}}, {BIND_ACTION_CUSTOM_19, m_alt, {{XKB_KEY_parenleft}}}, }; conf->key_bindings.count = ALEN(bindings); conf->key_bindings.arr = malloc(sizeof(bindings)); memcpy(conf->key_bindings.arr, bindings, sizeof(bindings)); } bool config_load(struct config *conf, const char *conf_path, const config_override_t *overrides, bool errors_are_fatal) { bool ret = !errors_are_fatal; *conf = (struct config) { .output = NULL, .prompt = c32dup(U"> "), .match_fields = MATCH_FILENAME | MATCH_NAME | MATCH_GENERIC, .password_mode = { .character = U'\0', .enabled = false, }, .terminal = NULL, .launch_prefix = NULL, .font = strdup("monospace"), .dpi_aware = DPI_AWARE_AUTO, .icons_enabled = true, .icon_theme = strdup("hicolor"), .actions_enabled = false, .fuzzy = { .min_length = 3, .max_length_discrepancy = 2, .max_distance = 1, .enabled = true, }, .dmenu = { .enabled = false, .mode = DMENU_MODE_TEXT, .exit_immediately_if_empty = false, .delim = '\n', }, .lines = 15, .chars = 30, .tabs = 8, .pad = { .x = 40, .y = 8, .inner = 0, }, .colors = { .background = conf_hex_to_rgba(0xfdf6e3dd), .border = conf_hex_to_rgba(0x002b36ff), .text = conf_hex_to_rgba(0x657b83ff), .match = conf_hex_to_rgba(0xcb4b16ff), .selection = conf_hex_to_rgba(0xeee8d5dd), .selection_text = conf_hex_to_rgba(0x657b83ff), .selection_match = conf_hex_to_rgba(0xcb4b16ff), }, .border = { .size = 1u, .radius = 10u, }, .image_size_ratio = 0.5, .line_height = {-1, 0.0}, .letter_spacing = {0}, .layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP, .exit_on_kb_focus_loss = true, }; add_default_key_bindings(conf); struct config_file conf_file = {.path = NULL, .fd = -1}; const char *terminal = getenv("TERMINAL"); if (terminal != NULL) { if (asprintf(&conf->terminal, "%s -e", terminal) < 0) { LOG_ERRNO("failed to build default terminal command line"); goto out; } } if (conf_path != NULL) { int fd = open(conf_path, O_RDONLY); if (fd < 0) { LOG_ERRNO("%s: failed to open", conf_path); goto out; } conf_file.path = strdup(conf_path); conf_file.fd = fd; } else { conf_file = open_config(); if (conf_file.fd < 0) { LOG_WARN("no configuration found, using defaults"); ret = true; goto out; } } LOG_INFO("loading configuration from %s", conf_file.path); FILE *f = fdopen(conf_file.fd, "r"); if (f == NULL) { LOG_ERRNO("%s: failed to open", conf_file.path); goto out; } if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal) || !overrides_apply(conf, overrides, errors_are_fatal)) { ret = !errors_are_fatal; } else ret = true; fclose(f); out: free(conf_file.path); if (conf_file.fd >= 0) close(conf_file.fd); return ret; } void config_free(struct config *conf) { free(conf->output); free(conf->prompt); free(conf->terminal); free(conf->launch_prefix); free(conf->font); free(conf->icon_theme); free_key_binding_list(&conf->key_bindings); } struct rgba conf_hex_to_rgba(uint32_t color) { return (struct rgba){ .r = (double)((color >> 24) & 0xff) / 255.0, .g = (double)((color >> 16) & 0xff) / 255.0, .b = (double)((color >> 8) & 0xff) / 255.0, .a = (double)((color >> 0) & 0xff) / 255.0, }; } fuzzel/config.h000066400000000000000000000056661445417001200140260ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #define DEFINE_LIST(type) \ type##_list { \ size_t count; \ type *arr; \ } struct rgba {double r; double g; double b; double a;}; struct pt_or_px {int px; float pt;}; enum dpi_aware { DPI_AWARE_AUTO, DPI_AWARE_YES, DPI_AWARE_NO, }; enum dmenu_mode { DMENU_MODE_TEXT, DMENU_MODE_INDEX, }; enum match_fields { MATCH_FILENAME = 0x01, MATCH_NAME = 0x02, MATCH_GENERIC = 0x04, MATCH_EXEC = 0x08, MATCH_CATEGORIES = 0x10, MATCH_KEYWORDS = 0x20, MATCH_COMMENT = 0x40, }; struct config_key_modifiers { bool shift; bool alt; bool ctrl; bool super; }; struct config_key_binding { int action; /* One of the varios bind_action_* enums from wayland.h */ struct config_key_modifiers modifiers; union { /* Key bindings */ struct { xkb_keysym_t sym; } k; #if 0 /* Mouse bindings */ struct { int button; int count; } m; #endif }; /* For error messages in collision handling */ const char *path; int lineno; }; DEFINE_LIST(struct config_key_binding); struct config { char *output; char32_t *prompt; enum match_fields match_fields; struct { char32_t character; bool enabled; } password_mode; char *terminal; char *launch_prefix; char *font; enum dpi_aware dpi_aware; bool filter_desktop; bool icons_enabled; char *icon_theme; bool actions_enabled; struct config_key_binding_list key_bindings; struct { size_t min_length; size_t max_length_discrepancy; size_t max_distance; bool enabled; } fuzzy; struct { bool enabled; enum dmenu_mode mode; bool exit_immediately_if_empty; char delim; } dmenu; unsigned lines; unsigned chars; unsigned tabs; /* Tab stop every number of #spaces */ struct { unsigned x; unsigned y; unsigned inner; } pad; struct { struct rgba background; struct rgba border; struct rgba text; struct rgba match; struct rgba selection; struct rgba selection_text; struct rgba selection_match; } colors; struct { unsigned size; unsigned radius; } border; float image_size_ratio; struct pt_or_px line_height; struct pt_or_px letter_spacing; enum zwlr_layer_shell_v1_layer layer; bool exit_on_kb_focus_loss; }; typedef tll(char *) config_override_t; bool config_load( struct config *conf, const char *path, const config_override_t *overrides, bool errors_are_fatal); void config_free(struct config *conf); struct rgba conf_hex_to_rgba(uint32_t color); fuzzel/dmenu.c000066400000000000000000000063541445417001200136570ustar00rootroot00000000000000#include "dmenu.h" #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "dmenu" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" void dmenu_load_entries(struct application_list *applications, char delim, int abort_fd) { tll(struct application) entries = tll_init(); char *line = NULL; size_t alloc_size = 0; int flags; if ((flags = fcntl(STDIN_FILENO, F_GETFL)) < 0 || fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) < 0) { LOG_ERRNO("failed to set O_NONBLOCK on stdin"); goto out; } errno = 0; while (true) { struct pollfd fds[] = { {.fd = STDIN_FILENO, .events = POLLIN}, /* Must be last */ {.fd = abort_fd, .events = POLLIN}, }; size_t count = sizeof(fds) / sizeof(fds[0]); if (abort_fd < 0) count--; int ret = poll(fds, count, -1); if (ret < 0) { LOG_ERRNO("failed to poll stdin"); break; } if (fds[1].revents & (POLLIN | POLLHUP)) break; ssize_t len = getdelim(&line, &alloc_size, delim, stdin); if (len < 0) { if (errno != 0) LOG_ERRNO("failed to read from stdin"); break; } while (len > 0 && line[len - 1] == '\n') line[--len] = '\0'; /* * Support Rofi’s extended dmenu protocol. One can specify an * icon by appending ‘\0icon\x1f’ to the entry: * * “hello world\0icon\x1ffirefox” */ const char *icon_name = NULL; const char *extra = strchr(line, '\0'); if (extra != NULL && strncmp(extra + 1, "icon", 4) == 0) { char *separator = strchr(extra + 1, '\x1f'); if (separator != NULL) { *separator = '\0'; icon_name = separator + 1; } } LOG_DBG("%s (icon=%s)", line, icon_name); char32_t *wline = ambstoc32(line); if (wline == NULL) continue; struct application app = { .title = wline, .icon = {.name = icon_name != NULL ? strdup(icon_name) : NULL}, .visible = true, }; tll_push_back(entries, app); } free(line); out: applications->v = calloc(tll_length(entries), sizeof(applications->v[0])); applications->count = tll_length(entries); /* Convert linked-list to regular array */ size_t i = 0; tll_foreach(entries, it) { applications->v[i++] = it->item; tll_remove(entries, it); } tll_free(entries); } bool dmenu_execute(const struct application *app, ssize_t index, const struct prompt *prompt, enum dmenu_mode format) { switch (format) { case DMENU_MODE_TEXT: { char *text = ac32tombs(app != NULL ? app->title : prompt_text(prompt)); if (text != NULL) printf("%s\n", text); free(text); break; } case DMENU_MODE_INDEX: printf("%zd\n", index); break; } return true; } fuzzel/dmenu.h000066400000000000000000000005141445417001200136540ustar00rootroot00000000000000#pragma once #include #include "application.h" #include "config.h" #include "prompt.h" void dmenu_load_entries(struct application_list *applications, char delim, int abort_fd); bool dmenu_execute(const struct application *app, ssize_t index, const struct prompt *prompt, enum dmenu_mode format); fuzzel/doc/000077500000000000000000000000001445417001200131405ustar00rootroot00000000000000fuzzel/doc/fuzzel.1.scd000066400000000000000000000246761445417001200153300ustar00rootroot00000000000000fuzzel(1) # NAME fuzzel - display XDG applications in a searchable Wayland window # SYNOPSIS *fuzzel* [_OPTIONS_]... # DESCRIPTION *fuzzel* lists all available XDG applications in a searchable Window. The search box supports Emacs-like key bindings. Fuzzel uses the localized strings from the .desktop files by default. To disable this, run fuzzel with *LC_MESSAGES=C*. The window size, font and colors can be configured with *fuzzel.ini*(5) or command line options: *--config*=_PATH_ Path to configuration file, see *fuzzel.ini*(5) for details. *-o*,*--output*=_OUTPUT_ Specifies the monitor to display the window on. Autocompletion is available for zsh and fish, or you can list the available outputs with *wlr-randr* or with Sway using *swaymsg -t get_outputs*. Example: _DP-1_ Default: Let the compositor choose output. *-f*,*--font*=_FONT_[,_FALLBACK1_,_FALLBACK2_,...] Comma separated list of primary font, and fallback fonts, in FontConfig format. See *FONT FORMAT*. Default: _monospace_. *-D*,*--dpi-aware*=_no|yes|auto_ When set to *yes*, fonts are sized using the monitor's DPI, making a font of a given point size have the same physical size, regardless of monitor. In this mode, the monitor's scaling factor is ignored; doubling the scaling factor will *not* double the font size. When set to *no*, the monitor's DPI is ignored. The font is instead sized using the monitor's scaling factor; doubling the scaling factor *does* double the font size. Finally, if set to *auto*, fonts will be sized using the monitor's DPI if _all_ monitors have a scaling factor of 1. If at least one monitor as a scaling factor larger than 1 (regardless of whether the fuzzel window is mapped on that monitor or not), fonts will be scaled using the scaling factor. Note that this option typically does not work with bitmap fonts, which only contains a pre-defined set of sizes, and cannot be dynamically scaled. Whichever size (of the available ones) that best matches the DPI or scaling factor, will be used. Also note that if the font size has been specified in pixels (*:pixelsize=*_N_, instead of *:size=*_N_), DPI scaling (*dpi-aware=yes*) will have no effect (the specified pixel size will be used as is). But, if the monitor's scaling factor is used to size the font (*dpi-aware=no*), the font's pixel size will be multiplied with the scaling factor. Default: _auto_ *-p*,*--prompt*=_PROMPT_ Prompt to use. Default: _> _. *-i* Ignored; for compatibility with other, similar utilities (where *-i* means "case insensitive search"). *--icon-theme*=_NAME_ Icon theme to use. Note that this option is case sensitive; the name must match the theme's folder name. Example: _Adwaita_. Default: _hicolor_. *-I*,*--no-icons* Do not render any icons. *-F*,*--fields*=_FIELDS_ Comma separated list of XDG Desktop entry fields to match against: - *filename*: the .desktop file's filename - *name*: the application's name (title) - *generic*: the application's generic name - *exec*: the applications's executable, as specified in the desktop file. Note: may include command line options as well. - *keywords*: the application's keywords - *categories*: the application's categories - *comment*: the application's comment Default: _filename,name,generic_ *--password*=[_CHARACTER_] Password input. Render all typed text as _CHARACTER_. If _CHARACTER_ is omitted, a *\** will be used. *-T*,*--terminal*=_TERMINAL ARGS_ Command to launch XDG applications with the property *Terminal=true* (_htop_, for example). Example: _xterm -e_. Default: _not set_. *-l*,*--lines*=COUNT The (maximum) number of matches to display. This dictates the window height. Default: _15_. *-w*,*--width* Window width, in characters. Margins and borders not included. Default: _30_. *--tabs*=_COUNT_ Number of spaces a tab is expanded to. Default: _8_. *-x*,*--horizontal-pad*=_PAD_ Horizontal padding between border and icons and text. In pixels, subject to output scaling. Default: _40_. *-y*,*--vertical-pad*=_PAD_ Vertical padding between border and text. In pixels, subject to output scaling. Default: _8_. *-P*,*--inner-pad*=_PAD_ Vertical padding between prompt and match list. In pixels, subject to output scaling. Default: _0_. *-b*,*--background*=_HEX_ Background color. See *COLORS*. Default: _fdf6e3dd_. *-t*,*--text-color*=_HEX_ Text color. See *COLORS*. Default: _657b83ff_. *-m*,*--match-color*=_HEX_ The color of matching substring(s). As you start typing in the search box, the matching part in each application's name is highlighted with this color. See *COLORS*. Default: _cb4b16ff_. *-s*,*--selection-color*=_HEX_ The color to use as background of the currently selected application. See *COLORS*. Default: _eee8d5dd_. *-S*,*--selection-text-color*=_HEX_ The text color of the currently selected application. See *COLORS*. Default: _657b83ff_. *-M*,*--selection-match-color*=_HEX_ The color of matching substring(s) of the currently selected application. As you start typing in the search box, the matching part in each application's name is highlighted with this color. See *COLORS*. Default: _cb4b16ff_. *-B*,*--border-width*=_INT_ The width of the surrounding border, in pixels (subject to output scaling). Default: _1_. *-r*,*--border-radius*=_INT_ The corner curvature, subject to output scaling. Larger means more rounded corners. 0 disables rounded corners. Default: _10_. *-C*,*--border-color*=_HEX_ The color of the border. See *COLORS*. Default: _002b36ff_. *--show-actions* Include desktop actions in the list. Desktop actions are alternative actions some desktop entries have. Examples include "New Window", "New Document", etc. *--filter-desktop*=[_no_] Filter the visible desktop entries based on the value of XDG_CURRENT_DESKTOP. If the optional parameter is "no", explicitly disables filtering. *--no-fuzzy* Disables fuzzy matching. When disabled, only exact (case insensitive) substring matches are considered. *--fuzzy-min-length*=_VALUE_ Search strings shorter than this will not by fuzzy matched. Default: _3_. *--fuzzy-max-length-discrepancy*=_VALUE_ Maximum allowed length difference between the search string, and a fuzzy match. Larger values result in more fuzzy matches. Default: _2_. *--fuzzy-max-distance*=_VALUE_ Maximum allowed levenshtein distance between the search string, and a fuzzy match. Larger values result in more fuzzy matches. Default: _1_. *--line-height*=_HEIGHT_ Override line height from font metrics. In points by default, but can be specified as pixels by appending 'px' (i.e. *--line-height=16px*). Default: _not set_. *--letter-spacing*=_AMOUNT_ Additional space between letters. In points by default, but can be specified as pixels by appending 'px' (i.e. *letter-spacing=5px*). Negative values are supported. Default: _0_. *--layer*=_top_|_overlay_ Which layer to render the fuzzel window on. Valid values are *top* and *overlay*. *top* renders above normal windows, but typically below fullscreen windows and lock screens. *overlay* renders on top of both normal windows and fullscreen windows. Note that the order is undefined if several windows use the same layer. Since e.g. lock screens typically use *overlay*, that means fuzzel may or may not appear on top of a lock screen. Default: _top_ *--no-exit-on-keyboard-focus-loss* Do not exit when losing keyboard focus. This can be useful on compositors where enabling "focus-follows-mouse" causes fuzzel to exit as soon as the mouse is moved over another window. Sway (at least up to, and including 1.7) exhibits this behavior. *--launch-prefix*=_COMMAND_ Command to launch XDG applications with. If set, fuzzel will pass the Desktop File ID of the chosen application (see the Desktop Entry specification) in the FUZZEL_DESKTOP_FILE_ID environment variables. Example: _swaymsg exec --_. Default: _not set_. *-d*,*--dmenu* dmenu compatibility mode. In this mode, the list entries are read from stdin (newline separated). The selected entry is printed to stdout. If the input string does not match any of the entries, the input string is printed as is on stdout. Alternatively, you can symlink the fuzzel binary to *dmenu*. Fuzzel will then start in dmenu mode, without the *--dmenu* argument. Fuzzel also supports icons, using Rofi's extended dmenu protocol. To set an icon for an entry, append *\\0icon\\x1f*. Example: echo -en "Firefox\\0icon\\x1ffirefox" | fuzzel --dmenu *--dmenu0* Like *--dmenu*, but input is NUL separated instead of newline separated. Note that in this mode, icons are not supported. *--index* Print selected entry's index instead of its text. **dmenu** mode only. *-R*,*--no-run-if-empty* Exit immediately, without showing the UI, if stdin is empty. **dmenu** mode only. *--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as syslog. Default: _info_. *--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. *--log-no-syslog* Disables syslog logging. Logging is only done on stderr. *-v*,*--version* Show the version number and quit # CONFIGURATION fuzzel will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/fuzzel/fuzzel.ini* (defaulting to *~/.config/fuzzel/fuzzel.ini* if unset) - *XDG_CONFIG_DIRS/fuzzel/fuzzel.ini* (defaulting to */etc/xdg/fuzzel/fuzzel.ini* if unset) An example configuration file containing all options with their default value commented out will usually be installed to */etc/xdg/fuzzel/fuzzel.ini*. For more information, see *fuzzel.ini*(5). # FONT FORMAT The font is specified in FontConfig syntax. That is, a colon-separated list of font name and font options. _Examples_: - Dina:weight=bold:slant=italic - Arial:size=12 # COLORS All colors must be specified as a RGBA quadruple, in hex format, without a leading '0x'. _EXAMPLES_: - white: *ffffffff* (no transparency) - black: *000000ff* (no transparency) - black: *00000010* (semi-transparent) - red: *ff0000ff* (no transparency) The default color scheme is _Solarized_. # FILES _$XDG_CACHE_HOME/fuzzel_ Stores a list of applications and their launch count. This allows fuzzel to sort frequently launched applications at the top. _$XDG_RUNTIME_DIR/fuzzel-$WAYLAND_DISPLAY.lock_ Lock file, used to prevent multiple fuzzel instances from running at the same time. # SEE ALSO *fuzzel.ini*(5), *https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html* fuzzel/doc/fuzzel.ini.5.scd000066400000000000000000000261501445417001200160770ustar00rootroot00000000000000fuzzel.ini(5) # NAME fuzzel.ini - configuration file for *fuzzel*(1) # DESCRIPTION *fuzzel* uses the standard _unix configuration format_, with section based key/value pairs. The default section is usually unnamed, i.e. not prefixed with a _[section]_. However it can also be explicitly named _[main]_, say if it needs to be reopened after any of the other sections. fuzzel will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/fuzzel/fuzzel.ini* (defaulting to *~/.config/fuzzel/fuzzel.ini* if unset) - *XDG_CONFIG_DIRS/fuzzel/fuzzel.ini* (defaulting to */etc/xdg/fuzzel/fuzzel.ini* if unset) An example configuration file containing all options with their default value commented out will usually be installed to */etc/xdg/fuzzel/fuzzel.ini*. # SECTION: main *output* Output (monitor) to display on. You can list the available outputs with *wlr-randr* or with Sway using *swaymsg -t get_outputs*. Example: _DP-1_ Default: Not set-- let the compositor choose output. *font* Comma separated list of fonts to use, in fontconfig format. That is, a font name followed by a list of colon-separated options. Most noteworthy is *:size=n*, which is used to set the font size. Note that the font size is also affected by the *dpi-aware* option. Examples: - Dina:weight=bold:slant=italic - Courier New:size=12 - Fantasque Sans Mono:fontfeatures=ss01 Default: _monospace_. *dpi-aware* *auto*, *yes*, or *no*. When set to *yes*, fonts are sized using the monitor's DPI, making a font of a given size have the same physical size, regardless of monitor. In this mode, the monitor's scaling factor is ignored; doubling the scaling factor will *not* double the font size. When set to *no*, the monitor's DPI is ignored. The font is instead sized using the monitor's scaling factor; doubling the scaling factor *does* double the font size. Finally, if set to *auto*, fonts will be sized using the monitor's DPI if _all_ monitors have a scaling factor of 1. If at least one monitor as a scaling factor larger than 1 (regardless of whether the fuzzel window is mapped on that monitor or not), fonts will be scaled using the scaling factor. Note that this option typically does not work with bitmap fonts, which only contains a pre-defined set of sizes, and cannot be dynamically scaled. Whichever size (of the available ones) that best matches the DPI or scaling factor, will be used. Also note that if the font size has been specified in pixels (*:pixelsize=*_N_, instead of *:size=*_N_), DPI scaling (*dpi-aware=yes*) will have no effect (the specified pixel size will be used as is). But, if the monitor's scaling factor is used to size the font (*dpi-aware=no*), the font's pixel size will be multiplied with the scaling factor. Default: _auto_. *prompt*= String to use as input prompt. Note that trailing spaces are trimmed, unless the string is quoted. Default: _"> "_. *icon-theme* Icon theme. Note that the name is case sensitive. Default: _hicolor_. *icons-enabled* Boolean. When enabled, application icons (from the selected *icon-theme*) will be rendered. Default: _yes_. *fields* Comma separated list of XDG Desktop entry fields to match against. Even though fuzzel only displays the application names, matching can (and by default is) be done against other fields as well. Supported fields are: - filename - name - generic - exec - categories - keywords - comment Default: _filename,name,generic_. *password-character* Default character to use with the command line option *--password*. Note that this can be overridden on the command line, by explicitly specifying the character with *--password=X*. Default: _\*_. *filter-desktop* Boolean. Filter desktop files based on the value of XDG_CURRENT_DESKTOP. *fuzzy* Boolean. Enables or disables fuzzy matching. Default: _yes_. *show-actions* Boolean. Some desktop files define "actions", in addition to the application itself. Examples are "new window", "preferences", etc. When this option is enabled, those actions will be listed by fuzzel. Default: _no_. *terminal* terminal command to use when launching 'terminal' programs, e.g. \"xterm -e\". Default: _$TERMINAL -e_. *launch-prefix* Prefix to add before argv of executed program. If set, fuzzel will pass the Desktop File ID of the chosen application (see the Desktop Entry specification) in the FUZZEL_DESKTOP_FILE_ID environment variables. Default: _not set_. *lines* Number of matches to show. Default: _15_. *width* Window width, in characters. Border and margins are not included in this. Default: _30_. *tabs* Number of spaces a tab is expanded to. Default: _8_. *horizontal-pad* Horizontal padding, in pixels, between border and content. Default: _40_. *vertical-pad* Vertical padding, in pixels, between border and content. Default: _8_. *inner-pad* Vertical padding between prompt and match list, in pixels. Default: _0_. *image-size-ratio* The ratio of the large image displayed when there are only a "few" matches, compared to the full window size. Default: _0.5_. *line-height* Override line height from font metrics. Default: use font metrics. *letter-spacing* Additional letter spacing. Negative values are allowed. Default: _0_. *layer* Which layer to render the fuzzel window on. Valid values are *top* and *overlay*. *top* renders above normal windows, but typically below fullscreen windows and lock screens. *overlay* renders on top of both normal windows and fullscreen windows. Note that the order is undefined if several windows use the same layer. Since e.g. lock screens typically use *overlay*, that means fuzzel may or may not appear on top of a lock screen. Default: _top_ *exit-on-keyboard-focus-loss* Boolean. If true, exit when the fuzzel window loses keyboard focus. Setting this to false can be useful on compositors where enabling "focus-follows-mouse" causes fuzzel to exit as soon as the mouse is moved over another window. Sway (<= 1.7) exhibits this behavior, for example. Default: _yes_ # SECTION: colors All color values are in RGBA. *background* Background color. Default: _fdf6e3dd_. *text* Text (foreground) color of unselected entries. Default: _657b83ff_. *match* Text (foreground) color of the matched substring. Default: _cb4b16ff_. *selection* Background color of the selected entry. Default: _eee8d5dd_. *selection-text* Text (foreground) color of the selected entry. Default: _657b83ff_. *selection-match* Text (foreground) color of the matched substring of the selected entry. Default: _cb4b16ff_. *border* Border color. Default: _002b36ff_. # SECTION: border *width* Width of the border, in pixels. Default: _1_. *radius* Amount of corner "roundness". Default: _10_. # SECTION: dmenu *mode* One of *text* or *index*. Determines what fuzzel prints on stdout when an entry is selected: *text* prints the entry itself, *index* prints the index of the selected entry. Default: _text_. *exit-immediately-if-empty* Boolean. If enabled, fuzzel will not run at all (i.e. it will not open a window, and will not print anything on stdout) if there's nothing on stdin. Default: _no_. # SECTION: key-bindings This section lets you override the default key bindings. The general format is _action=combo1...comboN_. That is, each action may have one or more key combinations, space separated. Each combination is in the form _mod1+mod2+key_. The names of the modifiers and the key *must* be valid XKB key names. Note that if *Shift* is one of the modifiers, the _key_ *must not* be in upper case. For example, *Control+Shift+V* will never trigger, but *Control+Shift+v* will. Note that *Alt* is usually called *Mod1*. *xkbcli interactive-wayland* can be useful for finding keysym names. A key combination can only be mapped to *one* action. Lets say you want to bind *Control+k* to *next*. Since this is the default shortcut for *delete-line*, you must unmap the default binding. This can be done by setting _action=none_; e.g. *delete-line=none*. *cancel* Quite fuzzel without executing anythingDefault: _Control+g Control+c Escape_. *execute* Execute the currently selected entry. Or, in dmenu mode, print the selected entry on stdout. Default: _Return KP_Enter Control+y_. *execute-or-next* If there is a single match, execute it. If there are more than one match, select the next entry. Wraps around when the last entry has been reached. Default: _Tab_. *cursor-left* Moves the cursor one **character** to the left. Default: _Left Control+b_. *cursor-left-word* Moves the cursor one **word** to the left. Default: _Control+Left Mod1+b_. *cursor-right* Moves the cursor one **character** to the right. Default: _Right Control+f_. *cursor-right-word* Moves the cursor one **word** to the right. Default: _Control+Right Mod1+f_. *cursor-home* Moves the cursor to the beginning of the input. Default: _Home Control+a_. *cursor-end* Moves the cursor to the end of the input. Default: _End Control+e_. *delete-prev* Deletes the **character before** the cursor. Default: _BackSpace_. *delete-prev-word* Deletes the **word before** the cursor. Default: _Mod1+BackSpace Control+BackSpace_. *delete-next* Deletes the **character after** the cursor. Default: _Delete_. *delete-next-word* Deletes the **word after** the cursor. Default: _Mod1+d Control+Delete_. *delete-line* Deletes everything after the cursor. Default: _Control+k_. *insert-selected* Copies the selected entry to the prompt, replacing the current prompt. In application mode, the *Exec* line is inserted, and in dmenu mode the entry itself is used. Default: _Control+Tab_. *prev* Select the previous entry. Does *not* wrap around when the first entry has been reached. Default: _Up Control+p_. *prev-with-wrap* Select the previous entry. Wraps around when the first entry has been reached. Default: _ISO_Left_Tab_. *prev-page* Switch to the previous page. Default: _PageUp KP_PageUp_. *next* Select the next entry. Does *not* wrap around when the last entry has been reached. Default: _Down Control+n_. *next-with-wrap* Select the previous entry. Wraps around when the first entry has been reached. Default: _not bound_. *next-page* Switch to the next page. Default: _Page_Down KP_Page_Down_. *first* Select the first entry, on the first page. Default: _Control+Home_. *last* Select the last entry, on the last page. Default: _Control+End_. *custom-1*, *custom-2*, ..., *custom-19* Execute the currently selected entry, then exit with a non-zero exit code. *custom-1* exits with exit code 10, *custom-2* with 11, *custom-3* with 12, and so on. Default: - custom-1: _Mod1+1_ - custom-2: _Mod1+2_ - custom-3: _Mod1+3_ - custom-4: _Mod1+4_ - custom-5: _Mod1+5_ - custom-6: _Mod1+6_ - custom-7: _Mod1+7_ - custom-8: _Mod1+8_ - custom-9: _Mod1+9_ - custom-10: _Mod1+10_ - custom-11: _Mod1+11_ - custom-12: _Mod1+12_ - custom-13: _Mod1+13_ - custom-14: _Mod1+14_ - custom-15: _Mod1+15_ - custom-16: _Mod1+16_ - custom-17: _Mod1+17_ - custom-18: _Mod1+18_ - custom-19: _Mod1+19_ # SEE ALSO *fuzzel*(1), *https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html* fuzzel/doc/meson.build000066400000000000000000000012071445417001200153020ustar00rootroot00000000000000sh = find_program('sh', native: true) scdoc = dependency('scdoc', native: true) scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) foreach man_src : [{'name': 'fuzzel', 'section': 1}, {'name': 'fuzzel.ini', 'section': 5}] name = man_src['name'] section = man_src['section'] out = '@0@.@1@'.format(name, section) custom_target( out, output: out, input: '@0@.@1@.scd'.format(name, section), command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.full_path())], capture: true, install: true, install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) endforeach fuzzel/doc/screenshot.png000066400000000000000000001110441445417001200160240ustar00rootroot00000000000000PNG  IHDRu bKGD IDATxy0_Ut=.7ADA_x$&&cyI(PkgZawfvvw`}?~TwWU,몮"PB!TR1u@[O:ľE[;w~׵@XB!PBWB0"PZv_'}[766\;vXpg}\~w +)[n%1tQFݲeK:O6l|>h2&NXYYu/+SL5j~uYO>ɓw/[~Elmذaĉ{?x<7n{Wg|p8Ç7ͅ ZnW_O|Gy0iҤc644}E-^{?gKO׿'~gϾ|L9YP3gΜ^zis̙3gƍ`W^ye}}}}}o]W_};n9sw}] ̙Ν;/%K,\0S\s5]tٳWX10P_K_/Eill|xT*b  yLK/&??>sm=쳙۶m ck׮e-_cRSرdEQco[r#N[z[0͛j*؞={Ə Փ16y]vuP.gO3߷7/3ƚf͚ecg566N4i,c ۯI&}gk׮siL۶mt8RH$'>r4bĈ4443fڵ{9r$<3gc߿$Ik/_#FݩN<[neժU~O>ɳr8^s:$QJwرxb9r7msXt) 6fxÇϘ1ϟO|/]ccݻϟ?O<7j׳3i6sr8/kkk_uBHSSӎ;,Xuuu7n:t(cPuޛ6mZee(or:|g}:,N2p݂ e#G֭y睺:SOXnb10W^y̙3<}%iӦ'L{]fkf2yt#su/ΤR)"TP(dx$Q;}AnYϒ&Ϝ??l2J߾xbr_ycPyH%t:4 ?aƌٳgcl6/Ϝp ^~O~[nEUɓ'C>7랆3../:wgL3rHmtC2%Jt:_L&QcD$I JcRY3[3TU$$:544XV !~?rl0Dh4D(vS}DMt8õkEQ !jFQw\j~Yv{&EUU> n7o'bp8L!S"Kr4~[zͺ墦u]y 7,\9?\bŏ~O?4_ׯ衇A4+`߾}w}w0k]hcoO pw|?묳;d֙ε' Ѳ*%%tN ÚUUUuEJC귥>G?Ɔ'ӫQ={uW{~u}Էn/O}}_!)}]QpIpr#XUS(b5M/>}ٲe Tm`|p}pEuJou;u ɷ(w}wK)=.u&WB},?oK{*hXr+[f ml۷ꭏz*'<|.w7ك_\6|INO(Ͼoo-qڬ6! }1+o/9O3P$eO?\c?gI+3L4B\9p,yvXgM="uRRIrT϶,+)RWu]UUZE>z(c|~jީa]]466i2iU7o߷[WxE3'+*K$ eJWwJ)Jfe"Id%IQ !v Pj-Sr2Y"B k xމG\W^mS%} ƟS>S`G.9 T 6_ODcn)6Ga|rotqc(\0ԗdPMi"g_MRMϾ~iOeYQUUcٽ fc<& _r=$IPy(MϙdF4zo͚b›E mSZVe- ߙ*ѴRp8ZZZ|>_MMEN:`|L[Bc/:6!_P !8H)wg7Xys7 Vl0[7C)1\y'p&QuJ)ja|U4SP9+BeKT*_gnJ31NiB,wB_*[ dnfdF)a#P/ZO}}_WcI$\>oB,UӴdRZ 4_QU7Y,72-8~!*=!Pۭt݀b%2&xB6~+YLo(Ν=CzV!_ ~uʕϙuǛwBg|ⵔx!J<+RRPSSq6llm*Ih_o;mڠ=X:#*'_9EE&Q|57 !6i˵lס7>zv9B_9D2-sf~lW!JXB* HJ;cW_d4zM6aś{?/>gŒK_+뺪ufa]t&13O@$' 4{*-'+<{Пcwm=(ڋ8v@꧍U*!-JId6fs:拼R)Y-lVU1W_ku*aQJK9KB=6rUxFY]`UD ڭfثKl!_Y5jVkҰBbɬU*r,J*#|gL8$P~d'rakjVUvY,I4 &]ʁ{m I,+ٽa(9B=kcs`N7}O !oE4y05jwe 6$D,ߦTRl6SJy0P)VGYłk"9O<}$@5s GS-ߦ'BGݿ<B(adSJAuX_ ot$IRd2 |B݇A-[/3͖S&{Kjutjߙ1qSO@L&1 l!<+btb`b&&ew-31bs.ԦUw}cAuC!t,*J)w,fXÝvۼ.sBPyW8!G^#e_訉-WB%6B0"B'f*9,]I`JBY߭ ~X+T0JJ[1BWByE>#R!Jl;_zuM:Ss/<}P+P1BiM@;[s7jyl 3֬\Yg|yg/Txq#q!Tf :0`(&'jN*RQ$2uϮGVkjjWƅTẶ}[:'32)%55ϘkoÖv*I%`ƪI猼wQ4~-H$._;Ⴉcq)Y&*PX:f ,LSRJ&J"ajR"rxHVj$aeӆ[|D2K SH0rUL)ߞH,OpeZE]Ymyk|7359D Eet Ϛ[kI%I|Ϯw'7,yǟ]{sO6|&:y#] PP6 MLS&j)L4wgA65k譮.{vFQkw tƣdځF_[/7k'^|Y 0 &=\0uȀN<h绫܎QCt*nwD*P˔0?)*P"C71**0hߢQRWv` F2}mh E*<U5UT`Œr)`YQ yfRjF1noo/%/~zc|mt3ab̈,<ڼcPJ5zob9OrO3\/cSi]7S*̈́Of0B c,OMf03]w'⟯3Ip`J)c,Od6{$h$| Dp|WRSiPJۂ WAd<}Cʹn({ϟ1@QR?g %$&ijb?LyNݞ<;ߦbJ?züȗwoz-+~xᴱ~PRꩣgv1c/6`VY7jئm{3<ScZZ㝙$ :'2AILzhp9Bv=3űaC*j$-foWˡj8棅iZ]5UEbfr`8 d;Gx$.[cW>_ g$9ٛfwyvU¡p4N>R90>ys']ɅБ?m4yp5hwɄT| 9m܉/ѫn5N)KKdvk @OG5S%v*t,mm$vul6vkC>wVWJ̀|{shja3!Q+Lţq~3OQQO"XjT _KpM>}L9-C-|:,.qf L;m{><~0XzԱ'>x/O1n5}fٺ-ԩ|BݑL$1J7Q:N`|"샪Z,b=qlMmΝ+N>aK/P38AJlj Fj*]TR駜}N;d< bnqkQI[pu?[>t{j[P`X_otحuUG7YZxg~ӯL;eKkomVSz(QN͕N۩3Owxm߻ƭ6n0zYnmv!BD-6k+b;u›Λ|ʗ{f)GfN=SnmJyc.8sqblۛcQV^>wZ8|ҵW!Pt:Js; /bsEd-)#FkPIKnG/ZWJf3È1n[0oWB%TNSOBQ!P_NQB0"B!tRJx|6y/`J#Fxuūo֯-z6IG/;-B_W&kµO[MQ DB^]WmYxl,eYcxj+*gBTUZnzwSumOfݚ%2󯘿eXS/^|J+BwA!0N3*P"6cIr&QuMO))îUMiZ$TTTe%N)X-PDZ3̎9sj*}$Q3RpuW#ֻ p3*^O|o;at?[l|go.ŮCCFiܬ*Oykz&4IО@.y h2lΨ<{,pxRARJ dm 11OfP͎-;%QpUEQ2/v۫ߞ}lI:z@wD͢ -%ȂƉ‰]fPjY!f֟p0̇e Fbf;5 p$j@uMbj-'Ŭ$*w8G#e2@].&v!_ӞaC*:Gx$.[}֜gTd=m|/U pl1SJ:RYf|0iu >aj6k\=]ݏ VU4.&3&|O^2{7tp}|kU]86q‰0Ƽފ. BuҮOt·,_Bc#TT9fECVGDqUBPumQP>E~7λv䱟t3__P8 !*=!Pa|E!J+B!Tz_BBWB0"B7!X4M&wgDe+Be =&`xjʟT0IJx4n[A1jv@)BFl*Hb2MZZ{j&ʢdLf5McZJt\"1+*?~ElBe` H6 ca T]z@zWʖ( YȄRC7TE%@D `b븳RJ*F/6V(n6sEe +B婵+,R2ԄJhk7,l d1=nff` xuŮ Ed;E#?=BGuRacLMD C5t[p+!wC7aG*ۯ`ސ-"錘 4 НSKfI].~ +B"E-D$ <$b.m0"T@%&QLk=QBf> WP_"8_oBe+B]v׵@İC!*=!Pa|E!J+B!Tz_BBWB0"B!T|;ʼ725|- -- -x"m&^Fӡ&]+ͪ!HSԯtX e膞DA0 <1:)_*GM`՚ŋGV_{P(YD+BeKUTקK'T@ȢK'TRO~Xjv?nbEQwڬ Qb;;=@vZ8f.;AݹޛM3ËEac1 @"$`A6J|(ٿԚYmVMպppZ]-,BBay[N-T|(FQI<<+lvf5dI?hw-6jf_̀*PBյ՚|6M4/(%ݖ۝7]P8vf9pOiɳU/N{,EPy=lܬ*OT k$w}pxt@ө4( %ӗ4s4) O>OwFJ~gw#e=4}`PUSgX!V?֕}o so0cD͢xן 2_Mi(5|0g_u=Ozxv8!]鎄"@Pp# ``wyvWÁp"`{[ F⑸lEWyrx4N8~qI*w4- iʯ]. F(E<ͼlw W8fRZ=+!;](O7MwՏؿי34lPo.}$SEUEO?o_YS`#bHWDZ۫Gkif~>\dُtYrI'ۯ!:'H#ݕ~\K?z}0",9pEwpf{b|EQ< D2Vup(Aa""E2lfj ~+_xCS tQ}T~ccZHP4y)h5K**ݶFx2/Hu+:.%}v4lp+ ;W/m;0m_}]:W}] t0sV]ށWa bQWԗԞ-ŏ fbX,L&{Z}?lXvn+aJb8K_Q_gM m۷o?tp;񕣔\US9 j:p9a6fu~Ȍh8ڵ]_Q PZf0F3!MƘaH"ժ!ZDB 6sɷC8piVUS—6)h=ʔ%>:7kc`X:f03 IQe-@a~`Lmyl0DA4JBUUuMd AD2;ϜuMTEEbfkޛk/?4Bh @dA7H$h)-Hg'vMgva[n>}ZQ$Mӂ 躮"Wk 0;`cwC! SpWp<*7v=#q"⑟B|>0nR/x-2#۶m**Pv0 ض1A1۷M|w3DQ6s+ "˖ÛvkeMbUMiK9 ,m::ux1ATۅW_0n(IV*y3:ۻc=1~ X,VA O9iM ۿ׬>gF<ۏ2%t<7+{ oBE,~&,qӲ+l[̝= uW_np^wx !AST_Q_%ItѹXq ¬2!”)_񻼂@gwg >xK!J+*ÆԚ0q:u >J]?3NS&:{ RaT&\;sgOWnHB%L(ōyI&S,!WT&N묥%d!s̼S[Splapq+*Ptڶc"HRaٛL&,Q_ CUzyx噏ɛfso<ϾPx%vw KF gj< c 7>wޜ3J٣\Ot|w?d膞DA0 Cf Tz*i?hYU?ė%Z][]驌b#Y OE4U5UJOکEQLƓyxj0hFWQju]PnM%S@NgG:^IYiiVUS5ʷMW{U+"CFi8puDQ+[.˃`};@:^r_QD ι©746VyKg[~-7.0n!{}9?T]_\sP IDATKoHI)@L4tCUTD4J k|v 뺷 6氕<:At-ftMU'&Q4t7zxL4| qQ %JBE$piJ pV8W\/:E$B!d2 E/"OSӪ ΞOsFn<вew=rCFGW>w8ԓ^x%믮_5>hU|f5K*56-fR}{5k%;?S1#] P@el膦h&)uQolk[]WW-` %mJoH)5آ$G8h8{>.;-^Dè:|u)R<9M/rrs2r{khGs/MյCO />⚕kvk"H3.\e/>W]1U̠7K~ˉI9۽n3WV7x[;B YTD nT0Awxxv@jhF6:UrUҩtͲ _ GMÙ󌺩SH,V7+MpvJKc\[l|gϿsUUǞ>`x|˫k7{[۲6m悙 gʐ$6p*YfD2PRP%$sxF6>i:̝==p垟썓*) Wf0B oȊR tFL}LgD^; nGw (&1HZmV]}MʚJ$D%XlUUuM?ykݼ7h n.Zk3*>|RH>mJ/IPNE ٱeǼ%b A.|UQ\/v53Yl$+*+5W,9}8%?9M7|vغsǟ~7iIdɎH4sy?}"E-2Ae dE&QևUp0 CQ&+.GgZv=յy:v=#q"gn<,)@4M z뺦jHl5;\!wS$eɧ OZz[i|u轏z`[E8yH(Mo\9q^~_֝<:>r`s=ϙ6_yFϺXтRFSCS_6lJ~. !'-ٺ=Z{ӡ;3WT!}tF |=_|$RsEC.!T^t]qލ'$;+*\9 5 |[\{E/ٰzvnֽ| ov8P/}:Wt;.:$Y˗\ܷ?Ϭmjd6zEj=͟*Ir6-7.*Ö&QI=W6<3RU2.^p,{q?͟B8.,GDaϞ6OL>c͟r~WRyY/@u{R|1JLj&cbnb3eJN]W׾#y&O !dJ @bmGR^9S-0-N)]s-.܉X0!F+ϔfJtT:%b1h{ݞMrsŜ^WvWJIQ\)~\/ㅽPk]ֽW)& j4qi4Q4:f!˔g+ҩNNMRJ'Jٲ 9u{"lGc/oähO:4]9wd܋E "3D,C]n6Vr \!8Kgٖ t6}cPK,泥R6;ʶmB!IJ,ƘJI%cz.pss*ɔCV'D)+L"J%(, 8 s. 0Q,`Bb895g))/jX R:sh3a/l֛b89=Φ+0-R)aubuW4&n׾̱R7ۄ@e}eNXzJA^[ߩS/LNN9>lz^K[Aӧ.-x+nqh(>WR%mQZ//Jnmhh?/Xul@qҡ!YDcZ۾~!h<)n|_mS~F" W40}j~Ri$['Bh`|Ela7d,Urr h?7! k ֆW_-_71}sIUc_{g>ͤj{+Bh03׿ywR]؇~,zB +:#!q=[' c'^oaV+oT"e^PjZ?jZ 9 -win6ц7;=|lsa|Eg[GTq/{qԋd,#?2D$Nm-oT[;nwZ7kDa0+!v\!|Çnj%7dPztF򞏾繿-Gtm|Dt#uA&z$8-עj#?J2zߨm,:FuqmFخmPJ)+ZRĢYol>̟r~WRyY/@FZ]O'Ň^%`4+ 'MQܪD޲-^?=@Zu{ nvQHXa/,c/!J߾~ڷ_~X}k_q5W~_sqnM׿钗]b}kC^Ͽy{؇7O}SJ)۱omqWr_љB)57_b%?p"[+MBMUJ РAt&ڜS҉DyZۡQmҩt0QhT&/!$tiw?p_|/Ǻ7 o/&jU~o>?}{΃g;O[>w13H%NA?txR2q/`  %w9!ˑ )ek'O. !*sœv7UٳfדMĂ[04h򶦼ps06J7LJ)!r` +v7W&L )?>![.7Ͳ- !lo`Rò,ssgomB,1RRX$ѥW^ HS vW*9~LgӵJM׿)ضm0:\g+?ßtZSCZ~L(1Q',,B w26<8츎I}ZfJIGJ]7Ҡ[4,'Q2 J)b/-0ST1׶=BV怗*sv;tfT[Ezc2j?X[qJ*qG1VB)Z]t6{tjɩt6=:[<d'|Ƴ1zyiP(`s董+:S<9t~JC.0+f3ӗeeFPB wtYX xsSn ̜e_&iThiVivRT&q% fw| :͌OszRꤜUs45Tu*!M c,qܬ7)3r{}= fiޱboܻnX[<[\WW^}m'o:![Z2ي͝&!<&4(M;/ _јkzl)] +Bhc0qW/h{ߏ!s`!4οD5]l/}{[!tƐؿ;.^}Ai0q3U*㭯yʹK_jA0r7~wO"U!4_U%jwnIՆ:a4&.MW'\T}IՆV49^PjyLxi~dGN# |eyin7^;=|lp _88CodS~-TIָT2A3ΘuZSr/E(ۜߦt]/_Omw.h si/jJs%U0 Bϧ-۱9Zc0}Ѝ;>jhpZ7kboņNsYdG({)y~u|RskĖkQFM6:y/,,tY(g0?95XZX2ah^g;v&i7A/0ǫ8kK5vmR:t"!'cm t]uy8h֛&Gl&1O 9+fVk'߲~\1ϖ^80}h:Ve6'm&&V<͠t]ss:}鯿O>,O{؇~ڷ_~X}k_q5W~_sqnM׿钗]b}kC^Ͽy`|Ec߽%S|uXLΝGJkH?ͧ%崟U[+ L`-5M:rSJT]z)q ~nɩɅ&JJ-^-חkvm;vylֺ^gT:enf(8K%t&MYA/s !UĂqFi5Z&~wv=QHSf;c˲ֽnone~P ^ؑ>r7_/?Lݧ<)^W]kxwn-B2z#7}GO^{Wc_~u'nY줵W{Ρ_dbɿzu pɸ@0+J+bM ; ڹOBT*'MM4lSFG%tmw (OH%waT3CBJ\45_)W#b1qgRH)tt4-B!$MmLBٍ³,+ϖfJOOOySlNgl0+Tژ/;5融SJ -n:L* BƘ-ﶻnPbT /U*Yv]GY<<4Q0c.e|]woQJjTB?۝+Նyݟd3OOvwml RFbF}5Z,_ƙYP|Հ1qQQ~9|N/JwYdZiB lrjJ%J(x|ͿB6S'XB|v\슡{Dyqsg@RPC->[\Ib1(JB(ZkΦ{~/Nx]_ 9zg<{OW^u f}_~E)}sx~rK_nuBw1y0PH_j>4]Q&6j 3/^P]NNO@&iT`%Ni[f'Ner70Yh֛v& #N7]d3긎X%)2_ae8YlO_io>[>O)}ǟcgoϽ Ź#o}e/xx5z[gVq>?Y,{oߎxGď=я~l5ϝs &$)Mt.:7!TΏ ?to_sY[(!~ٻ4ֺ2W)]dm!E58^lM+گ{YkڈPƐ^[ہ7ji%;+_~3<)ڎ[~ss_$Ւ_?>&ut4N!>i)ϔ7!74؅# vYW=j|/>BR!`|E ߼A"U!PW_%7A0~~|{3;W+گo #&6JJ7Sڥ !"4 f6RZ"&#,L#*BmnmrGkw_cZ+MTƒrϟC!@LG!_["29̌*""cI(ڮSZ ~Q@_OZkrʨ:cB !DFE(Lי:0Jia|Eh<_(Q_JԚhu܋4m<# C#Bx2cJ*JJ(/ f!; \+Ah `|Eh<Bb`Oe63}iPHFf Y? or7#Bc+Bc2jվJSBLE!!JWB(y_Ba|E!!JWB(y_Ba|E!!J>!4*L6J[(n֚J)3^{F4}%ګ/-2ݐqWƖ*q/z෴Q7藌oܜnJ (O8'TI LVF ř̈w(J+_+B RF BRDk^ | }:@%(=sRֈ.2tU)e L6nM%O5I+ћ>Ц`|Eh1C7@=+Bc2jVTqԟ?l<p{:+2^ݍta͗Dz}N}'ͮU(3fksgZI9/L`be C7R@' 'Zc/<(c5kt647!Vr\<3=T:Ngҫm:nOJ/GgUkiРOSѪΥQ-W~"o2.PJ)+Gzk|P__ߺ7/?'voMC /Jg__y WK%FhlBLp4_d,)+BЍ=lyഹ^7T:U.& jC)S҉Dyl$q\.M':j 8K3ܔejպJ3dQ]ޭN79%UR.V u˻ϔ&&ڍ\<[^ONetX.m$~gv-VgJ3T:eHV3Ѭ7IiuV_ڿ:>S}#ue96".cA.#b0sJqC \xS7Z?jLJOZkr3>1"#]7qSPfJ78̠Ҡ[4l8݊: Jy/}NgV%R Bh13<K lrj ݸf acqH)}l?28۾3n c)zB395ΦWW0uJ!E, 6%;6%3kٖͦfUJBZiBTkwoqPJcaVojڋ~E| _W.R6ޢ"4!"l*Dٲ3AaЬ5Ag;4 Be4]L>Ӫr̴66춻@0Yhr5%TGJ :folc@C&1uOd Y˲nʭWc|6f_5検VivRT&F,* DZ-~<7 Hx Cv+^'=i>WwSw h{ӟM@h]0=Kj#Kҧ= o2;!ɉg>w1h'&&N> ݅·]rɿϢ,z y跶CmW;\g_W\X_|~xFvvG[5=EƠy{>kfgC>B~TEbQs{ы^+# s~8vGs<礔^vًgf3e+x?cMBh0]s`.(g]p9u׷N q6>]+~kE!vx[!O?/~QP]LqBh;`|EfΤN7Q&ϽeW/=h/b˵L~ȏ(&iZ¼6&w|/RJC?bqb>!}0t`GSkeĜx?Wktݎv;/91QN"gn!tXM5FV+?NeRpqT:dߗ'6z vWҞ}Γ~:SNBL6;ΙvJ*e9qf6XZ%Bяa'$af=a$X3Aج5 ukJtv{J8,hC'j h"fWk1"#OYRf^PѱbuLB'|;hNzXEi[MVL$JԚhu܋4aPL}Jg1=+&i?=Q;3J*J^%j\r7y&>$ ++`_[Jǽ8E2JGHD8uQ7%7E~@)u\߬55vjVE)5wl/Wm1:np3Ph۱sRJ$[wd+#4 \䧋2jiE(Йjyhr~2^" u.L̗^ Hw}7frJΦfiRf9\xlarj[|uuqmFخ/`|6u{Jt.Τ7{ǭZKF;0j՗3y˶nӓfcZ& Nb>NRJ)]3twzAP Osgm0uWrc?}w!zs o{}x=;~᪛>{'/O>|J)e;m-bS+/|oju׾k1BVZS&@ %CŊ8!ު)w^m޿K{"k:8w ZzfR锈~/ S -L&L4!qfJB%7宖Vafz)/%-Yd[ vlO'R^hK!gZВe2LRN;!sjzW]g˔n`ؕsnm99{o\q镗{[ۏԍꋮ:u<'-߼vmS'eGnM'k+lawd܋E ̥VIf֩˙Zk(#mf|8 œ'nؙlֺdsOGq~LgӃJ*Ks[[_}ůYuLgӵJM׿)˘:K>JU+BJk-]nƇc?&Bd$;b{rIF6bqSPfJ^NDu\gc71[Y?*4oRK)k +Ͷ43^zՁn%Zix| #Nut6{Qߡ%oO&Y4TR&Z86 )u/e@Ѓ[?w'3kbBZ{1+md$#Ziط^R~#nN&i7&R2ʴZVXO5RޝR/|FJqBV5u`1fi)YkX4]QrF~4\>F nV;Q`} Y83cnzkaƓYdZiB lrJ-m3)$hԃK6*[Ȯ]h;uZ[u>ol֠{88Bvy]$c,cI(R] )ba׿lkDJB(Zkw'n-2ӊ2GnNS?wgq IDATI)ѮQkT*X>Q8~o@ڸe{i|ߣ|GM+B]."a.Vf#|D("?ZFYR^W-oL6S_ׅB춻@0Y3z:)ĭ!!2j@YZXRJe Ysݳlc@C&Y&[M c,qܬ7 䔗Ujl^&_rRN4;t*M=֚2jN3W spL.S,4kMkЙlGAg6ׯ}럼wtgR'_ϲd}^|^tE4w3G61ϗ޾5#4Bn{Ug8h5RȥS돯΅W=x-٨!p|!~ъ(MxB{n>_ۼa!JWB(y_Ba|E!!JWB(y_Ba|E!!tjTxn;BoBhl)Lt qJǽ9oY)kӏﶻ^Ϯ3v&FaԪL\1ghfl>_I+Bc+b˵L~ȏ(&ZdV7T{Uzv n.ϔg"zӤ3泎@uDZFnI 3uΟr~WRyY/ mֺT/La(ϖ !fGk=u`JJY[9)R: \|lX.nz"!_[Z+MASƒrOpg.w~}9o7۶c0f66 /yiOĢX-ϖ)S -L&W3NBHǥ针R:3RJrqBHŶc]bj9:0{-pKfJ*4wW*L2M+ f R\t:IlD)+BMҔnqy&mN rTYc@ Bu8R A#hhB:cl7_OZkr3>1"#25q UJ33J^ZXoѰ: JR%!8Yl[`W bBYJ4d;Ec+B (!PDZͤnsPJ̼+;e)Mo8ڡun:ٲp"̅i`Ƙe[Buh? 4LTRZ+M(aٞmQNaS^KMz3[n1Ƹ{΢R|ˌ@0cq =T[ZX 8, \;iRfjGR~"A;'Bw2XJ'27& Zt&fFafK%3z:)g|&dVf& fck֚g-q((f $׾۷f[BvrѬ7c@ z[v8>B{~m#^79=M@WB(y_Ba|E!!JWB(y_Ba|E!!J>_!;K KZk˲ }a|Ehl)d$@f1q/Qr, !2,-QOјaVfVʲ\KDdz=^W+ &چPJ1)?_[rW x<JP;wxFhlq@X03fJsP.!a6kMRV*ΦzYOˊxZ@pSFֱJ!2khs\gT*҆"t:.VK)WƓC!PDk^ SfEɝ;j7)/gwm?'32R42` mn(f BT:#"7a|Eh<BE$L?PRr=ܔ}'_[Q;JYr)pe?_BcMk7;7!4pL!JWB(y_urBh]Z&v"/ڶUBɥF튯~6UB}?|ͧ\ܴ튯ۿm!Z/~Çw_?r]sT?B!GNw^ޮz|V.~rG!g=eϹ3?;B!z}]96>I[˻OQJ5/߾!RRH8c֩%ZSrsovxЎP}{0{+ #K^~t'nB \d2jiE(I+D ,t;;6'g{n|jhq|feⳞ/M2&BhPI} #f1ʩVz0jHB_zv{ޅRUJiW:~pbq>oo s?vjD|5}RI ~!쬳ﻯٕM+\|E+O&UFIï}rz2C9|=f=6~4'xE/FSOVV٪k/CZ6YOx ۡV:=˵ܜ|ʩđ;@܋ !Z뙟Ng{y~4FG +BZnLP<* `TvȒRԦ{\+ٓ15h}ljԜ}WBi_8k+} 8ޒk?20:B!<!P0"BB%+B!<!PeY)ҹsw+>onB _Ba|E!!JWО{{?VJ^~~{bā?hڛ|:묳=\ַc_t:?|ߧ>Ϝݖ9rd%?eZ;w0"4/Q}_MQ=y矐>SÇ|;]v z;)ww/bxk^k,}.{h)7xSJ|ͷ~ɓ'oᆗ@ǎ{;߹z_UW mϧ?;[o5_p mLBi8c' DqCԃAdDE<<9E \"KNnHBDCgQ, A'_n]_ z]MFFFzL&#I~d2srr! h8<<~KKK:v湹9$^Rt:ev}ff0TGf;;;hX,E1gggϕuuu4MOMMI$ߕ ÙKٜk4lfiWPPGQi<;;nL4T*E";8D{pp|z5ZV̛{xfgg- u\0Jurrr~~233F㻲X,Zj߾mdddIIZ<٬kjj@$bV7j^\\@MMnmmm8volld$Ij4Pȴ955 f_.X,TRPFDDQ,VU XoNONNJ$EAOB 1l6yODD`!|\pw:\.v4͌\1aL&+3L YN{\z&p4] I2..sL]\\@ff&aO5>"" ^/݅u/ @DAA'J\̄bJ:h4Y__7J@ GGǩ9j"d2SbҾ777Zrfggƾ/ϟ?>s666Ƃ[ZZi\fCRt?!!xhhwdjooK 0 2 v=88٘nssS/~DaaaUUU BT&%%}!  BUIENDB`fuzzel/external/000077500000000000000000000000001445417001200142155ustar00rootroot00000000000000fuzzel/external/wlr-layer-shell-unstable-v1.xml000066400000000000000000000440361445417001200221300ustar00rootroot00000000000000 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. fuzzel/external/wlr-protocols/000077500000000000000000000000001445417001200170435ustar00rootroot00000000000000fuzzel/fdm.c000066400000000000000000000116561445417001200133160ustar00rootroot00000000000000#include "fdm.h" #include #include #include #include #include #include #include #define LOG_MODULE "fdm" #define LOG_ENABLE_DBG 0 #include "log.h" struct handler { int fd; int events; fdm_handler_t callback; void *callback_data; bool deleted; }; struct fdm { int epoll_fd; bool is_polling; tll(struct handler *) fds; tll(struct handler *) deferred_delete; }; struct fdm * fdm_init(void) { int epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_fd == -1) { LOG_ERRNO("failed to create epoll FD"); return NULL; } struct fdm *fdm = malloc(sizeof(*fdm)); *fdm = (struct fdm){ .epoll_fd = epoll_fd, .is_polling = false, .fds = tll_init(), .deferred_delete = tll_init(), }; return fdm; } void fdm_destroy(struct fdm *fdm) { if (fdm == NULL) return; if (tll_length(fdm->fds) > 0) LOG_WARN("FD list not empty"); assert(tll_length(fdm->fds) == 0); assert(tll_length(fdm->deferred_delete) == 0); tll_free(fdm->fds); tll_free(fdm->deferred_delete); close(fdm->epoll_fd); free(fdm); } bool fdm_add(struct fdm *fdm, int fd, int events, fdm_handler_t handler, void *data) { #if defined(_DEBUG) tll_foreach(fdm->fds, it) { if (it->item->fd == fd) { LOG_ERR("FD=%d already registered", fd); return false; } } #endif struct handler *fd_data = malloc(sizeof(*fd_data)); *fd_data = (struct handler) { .fd = fd, .events = events, .callback = handler, .callback_data = data, .deleted = false, }; tll_push_back(fdm->fds, fd_data); struct epoll_event ev = { .events = events, .data = {.ptr = fd_data}, }; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { LOG_ERRNO("failed to register FD=%d with epoll", fd); free(fd_data); tll_pop_back(fdm->fds); return false; } return true; } static bool fdm_del_internal(struct fdm *fdm, int fd, bool close_fd) { if (fd == -1) return true; tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0) LOG_ERRNO("failed to unregister FD=%d from epoll", fd); if (close_fd) close(it->item->fd); it->item->deleted = true; if (fdm->is_polling) tll_push_back(fdm->deferred_delete, it->item); else free(it->item); tll_remove(fdm->fds, it); return true; } LOG_ERR("no such FD: %d", fd); return false; } bool fdm_del(struct fdm *fdm, int fd) { return fdm_del_internal(fdm, fd, true); } bool fdm_del_no_close(struct fdm *fdm, int fd) { return fdm_del_internal(fdm, fd, false); } static bool event_modify(struct fdm *fdm, struct handler *fd, int new_events) { if (new_events == fd->events) return true; struct epoll_event ev = { .events = new_events, .data = {.ptr = fd}, }; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) { LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)", fd->fd, fd->events, new_events); return false; } fd->events = new_events; return true; } bool fdm_event_add(struct fdm *fdm, int fd, int events) { tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; return event_modify(fdm, it->item, it->item->events | events); } LOG_ERR("FD=%d not registered with the FDM", fd); return false; } bool fdm_event_del(struct fdm *fdm, int fd, int events) { tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; return event_modify(fdm, it->item, it->item->events & ~events); } LOG_ERR("FD=%d not registered with the FDM", fd); return false; } bool fdm_poll(struct fdm *fdm) { assert(!fdm->is_polling && "nested calls to fdm_poll() not allowed"); if (fdm->is_polling) { LOG_ERR("nested calls to fdm_poll() not allowed"); return false; } struct epoll_event events[tll_length(fdm->fds)]; int r = epoll_wait(fdm->epoll_fd, events, tll_length(fdm->fds), -1); if (r == -1) { if (errno == EINTR) return true; LOG_ERRNO("failed to epoll"); return false; } bool ret = true; fdm->is_polling = true; for (int i = 0; i < r; i++) { struct handler *fd = events[i].data.ptr; if (fd->deleted) continue; if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) { ret = false; break; } } fdm->is_polling = false; tll_foreach(fdm->deferred_delete, it) { free(it->item); tll_remove(fdm->deferred_delete, it); } return ret; } fuzzel/fdm.h000066400000000000000000000010031445417001200133040ustar00rootroot00000000000000#pragma once #include struct fdm; typedef bool (*fdm_handler_t)(struct fdm *fdm, int fd, int events, void *data); struct fdm *fdm_init(void); void fdm_destroy(struct fdm *fdm); bool fdm_add(struct fdm *fdm, int fd, int events, fdm_handler_t handler, void *data); bool fdm_del(struct fdm *fdm, int fd); bool fdm_del_no_close(struct fdm *fdm, int fd); bool fdm_event_add(struct fdm *fdm, int fd, int events); bool fdm_event_del(struct fdm *fdm, int fd, int events); bool fdm_poll(struct fdm *fdm); fuzzel/fuzzel.ini000066400000000000000000000037041445417001200144170ustar00rootroot00000000000000# output= # font=monospace # dpi-aware=auto # prompt=> # icon-theme=hicolor # icons-enabled=yes # fields=filename,name,generic # password-character=* # filter-desktop=no # fuzzy=yes # show-actions=no # terminal=$TERMINAL -e # Note: you cannot actually use environment variables here # launch-prefix= # lines=15 # width=30 # tabs=8 # horizontal-pad=40 # vertical-pad=8 # inner-pad=0 # image-size-ratio=0.5 # line-height= # letter-spacing=0 # layer = top # exit-on-keyboard-focus-loss = yes [colors] # background=fdf6e3dd # text=657b83ff # match=cb4b16ff # selection=eee8d5dd # selection-text=657b83ff # border=002b36ff [border] # width=1 # radius=10 [dmenu] # mode=text # text|index # exit-immediately-if-empty=no [key-bindings] # cancel=Escape Control+g # execute=Return KP_Enter Control+y # execute-or-next=Tab # cursor-left=Left Control+b # cursor-left-word=Control+Left Mod1+b # cursor-right=Right Control+f # cursor-right-word=Control+Right Mod1+f # cursor-home=Home Control+a # cursor-end=End Control+e # delete-prev=BackSpace # delete-prev-word=Mod1+BackSpace Control+BackSpace # delete-next=Delete KP_Delete Control+d # delete-next-word=Mod1+d Control+Delete Control+KP_Delete # delete-line=Control+k # prev=Up Control+p # prev-with-wrap=ISO_Left_Tab # prev-page=Page_Up KP_Page_Up # next=Down Control+n # next-with-wrap=none # next-page=Page_Down KP_Page_Down # custom-N: *dmenu mode only*. Like execute, but with a non-zero # exit-code; custom-1 exits with code 10, custom-2 with 11, custom-3 # with 12, and so on. # custom-1=Mod1+1 # custom-2=Mod1+2 # custom-3=Mod1+3 # custom-4=Mod1+4 # custom-5=Mod1+5 # custom-6=Mod1+6 # custom-7=Mod1+7 # custom-8=Mod1+8 # custom-9=Mod1+9 # custom-10=Mod1+0 # custom-11=Mod1+exclam # custom-12=Mod1+at # custom-13=Mod1+numbersign # custom-14=Mod1+dollar # custom-15=Mod1+percent # custom-16=Mod1+dead_circumflex # custom-17=Mod1+ampersand # custom-18=Mod1+asterix # custom-19=Mod1+parentleft fuzzel/generate-version.sh000077500000000000000000000020061445417001200162050ustar00rootroot00000000000000#!/bin/sh set -e default_version=${1} src_dir=${2} out_file=${3} # echo "default version: ${default_version}" # echo "source directory: ${src_dir}" # echo "output file: ${out_file}" if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then workdir=$(pwd) cd "${src_dir}" if git describe --tags > /dev/null 2>&1; then git_version=$(git describe --always --tags) else # No tags available, happens in e.g. CI builds git_version="${default_version}" fi git_branch=$(git rev-parse --abbrev-ref HEAD) cd "${workdir}" new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')" else new_version="${default_version}" fi new_version="#define FUZZEL_VERSION \"${new_version}\"" if [ -f "${out_file}" ]; then old_version=$(cat "${out_file}") else old_version="" fi # echo "old version: ${old_version}" # echo "new version: ${new_version}" if [ "${old_version}" != "${new_version}" ]; then echo "${new_version}" > "${out_file}" fi fuzzel/icon.c000066400000000000000000000531451445417001200134770ustar00rootroot00000000000000#include "icon.h" #include #include #include #include #include #include #include #include #include #if defined(FUZZEL_ENABLE_PNG_LIBPNG) #include "png-fuzzel.h" #endif #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) #include #endif #if defined(FUZZEL_ENABLE_SVG_NANOSVG) #include #endif #include #define LOG_MODULE "icon" #define LOG_ENABLE_DBG 0 #include "log.h" #include "xdg.h" typedef tll(char *) theme_names_t; static void parse_theme(FILE *index, struct icon_theme *theme, theme_names_t *themes_to_load) { char *section = NULL; int size = -1; int min_size = -1; int max_size = -1; int scale = 1; int threshold = 2; char *context = NULL; enum icon_dir_type type = ICON_DIR_THRESHOLD; while (true) { char *line = NULL; size_t sz = 0; ssize_t len = getline(&line, &sz, index); if (len == -1) { free(line); break; } if (len == 0) { free(line); continue; } if (line[len - 1] == '\n') { line[len - 1] = '\0'; len--; } if (len == 0) { free(line); continue; } if (line[0] == '[' && line[len - 1] == ']') { tll_foreach(theme->dirs, it) { struct icon_dir *d = &it->item; if (section == NULL || strcmp(d->path, section) != 0) continue; d->size = size; d->min_size = min_size >= 0 ? min_size : size; d->max_size = max_size >= 0 ? max_size : size; d->scale = scale; d->threshold = threshold; d->type = type; } free(section); free(context); size = min_size = max_size = -1; scale = 1; section = NULL; context = NULL; type = ICON_DIR_THRESHOLD; threshold = 2; section = malloc(len - 2 + 1); memcpy(section, &line[1], len - 2); section[len - 2] = '\0'; free(line); continue; } char *tok_ctx = NULL; const char *key = strtok_r(line, "=", &tok_ctx); char *value = strtok_r(NULL, "=", &tok_ctx); if (strcasecmp(key, "inherits") == 0) { char *ctx = NULL; for (const char *theme_name = strtok_r(value, ",", &ctx); theme_name != NULL; theme_name = strtok_r(NULL, ",", &ctx)) { tll_push_back(*themes_to_load, strdup(theme_name)); } } if (strcasecmp(key, "directories") == 0) { char *save = NULL; for (const char *d = strtok_r(value, ",", &save); d != NULL; d = strtok_r(NULL, ",", &save)) { struct icon_dir dir = {.path = strdup(d)}; tll_push_back(theme->dirs, dir); } } else if (strcasecmp(key, "size") == 0) sscanf(value, "%d", &size); else if (strcasecmp(key, "minsize") == 0) sscanf(value, "%d", &min_size); else if (strcasecmp(key, "maxsize") == 0) sscanf(value, "%d", &max_size); else if (strcasecmp(key, "scale") == 0) sscanf(value, "%d", &scale); else if (strcasecmp(key, "context") == 0) context = strdup(value); else if (strcasecmp(key, "threshold") == 0) sscanf(value, "%d", &threshold); else if (strcasecmp(key, "type") == 0) { if (strcasecmp(value, "fixed") == 0) type = ICON_DIR_FIXED; else if (strcasecmp(value, "scalable") == 0) type = ICON_DIR_SCALABLE; else if (strcasecmp(value, "threshold") == 0) type = ICON_DIR_THRESHOLD; else { LOG_WARN( "ignoring unrecognized icon theme directory type: %s", value); } } free(line); } tll_foreach(theme->dirs, it) { struct icon_dir *d = &it->item; if (section == NULL || strcmp(d->path, section) != 0) continue; d->size = size; d->min_size = min_size >= 0 ? min_size : size; d->max_size = max_size >= 0 ? max_size : size; d->scale = scale; d->threshold = threshold; d->type = type; } tll_foreach(theme->dirs, it) { if (it->item.size == 0) { free(it->item.path); tll_remove(theme->dirs, it); } } free(section); free(context); } static bool load_theme_in(const char *dir, struct icon_theme *theme, theme_names_t *themes_to_load) { char path[PATH_MAX]; snprintf(path, sizeof(path), "%s/index.theme", dir); FILE *index = fopen(path, "r"); if (index == NULL) return false; parse_theme(index, theme, themes_to_load); fclose(index); return true; } icon_theme_list_t icon_load_theme(const char *name) { /* List of themes; first item is the primary theme, subsequent * items are inherited items (i.e. fallback themes) */ icon_theme_list_t themes = tll_init(); /* List of themes to try to load. This list will be appended to as * we go, and find 'Inherits' values in the theme index files. */ theme_names_t themes_to_load = tll_init(); tll_push_back(themes_to_load, strdup(name)); xdg_data_dirs_t dirs = xdg_data_dirs(); while (tll_length(themes_to_load) > 0) { char *theme_name = tll_pop_front(themes_to_load); /* * Check if we've already loaded this theme. Example: * "Arc" inherits "Moka,Faba,elementary,Adwaita,ghome,hicolor * "Moka" inherits "Faba" * "Faba" inherits "elementary,gnome,hicolor" */ bool theme_already_loaded = false; tll_foreach(themes, it) { if (strcasecmp(it->item.name, theme_name) == 0) { theme_already_loaded = true; break; } } if (theme_already_loaded) { free(theme_name); continue; } tll_foreach(dirs, dir_it) { char path[strlen(dir_it->item.path) + 1 + strlen("icons") + 1 + strlen(theme_name) + 1]; sprintf(path, "%s/icons/%s", dir_it->item.path, theme_name); struct icon_theme theme = {0}; if (load_theme_in(path, &theme, &themes_to_load)) { theme.name = strdup(theme_name); tll_push_back(themes, theme); } } free(theme_name); } xdg_data_dirs_destroy(dirs); return themes; } static void theme_destroy(struct icon_theme theme) { free(theme.name); tll_foreach(theme.dirs, it) { free(it->item.path); tll_remove(theme.dirs, it); } } void icon_themes_destroy(icon_theme_list_t themes) { tll_foreach(themes, it) { theme_destroy(it->item); tll_remove(themes, it); } } #if defined(FUZZEL_ENABLE_PNG_LIBPNG) static bool icon_from_png_libpng(struct icon *icon, const char *file_name) { pixman_image_t *png = png_load(file_name); if (png == NULL) return false; icon->type = ICON_PNG; icon->png = png; return true; } #endif bool icon_from_png(struct icon *icon, const char *name) { #if defined(FUZZEL_ENABLE_PNG_LIBPNG) return icon_from_png_libpng(icon, name); #else return false; #endif } #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) static bool icon_from_svg_librsvg(struct icon *icon, const char *file_name) { RsvgHandle *svg = rsvg_handle_new_from_file(file_name, NULL); if (svg == NULL) return false; icon->type = ICON_SVG; icon->svg = svg; return true; } #endif #if defined(FUZZEL_ENABLE_SVG_NANOSVG) static bool icon_from_svg_nanosvg(struct icon *icon, const char *file_name) { /* TODO: DPI */ NSVGimage *svg = nsvgParseFromFile(file_name, "px", 96); if (svg == NULL) return false; if (svg->width == 0 || svg->height == 0) { nsvgDelete(svg); return false; } icon->type = ICON_SVG; icon->svg = svg; return true; } #endif bool icon_from_svg(struct icon *icon, const char *name) { #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) return icon_from_svg_librsvg(icon, name); #elif defined(FUZZEL_ENABLE_SVG_NANOSVG) return icon_from_svg_nanosvg(icon, name); #else return false; #endif } static bool svg(struct icon *icon, const char *path) { icon->path = strdup(path); icon->type = ICON_SVG; icon->svg = NULL; return true; } static bool png(struct icon *icon, const char *path) { icon->path = strdup(path); icon->type = ICON_PNG; icon->png = NULL; return true; } static void icon_reset(struct icon *icon) { free(icon->path); icon->path = NULL; switch (icon->type) { case ICON_NONE: break; case ICON_PNG: if (icon->png != NULL) { #if defined(FUZZEL_ENABLE_PNG_LIBPNG) free(pixman_image_get_data(icon->png)); pixman_image_unref(icon->png); icon->png = NULL; #endif } break; case ICON_SVG: if (icon->svg != NULL) { #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) g_object_unref(icon->svg); #elif defined(FUZZEL_ENABLE_SVG_NANOSVG) nsvgDelete(icon->svg); #endif icon->svg = NULL; } break; } tll_foreach(icon->rasterized, it) { struct rasterized *rast = &it->item; free(pixman_image_get_data(rast->pix)); pixman_image_unref(rast->pix); tll_remove(icon->rasterized, it); } icon->type = ICON_NONE; } /* * Path is expected to contain the icon’s basename. It doesn’t have to * have the extension filled in; it will be filled in by this * function. * * Note that only supported image types are searched for. That is, if * PNGs have been disabled, we only search for SVGs. * * Also note that we only check for the existence of a file; we don’t * validate it. * * Returns true if there exist an icon file (of the specified name) in * . In this case, path has been updated with the extension * (.png or .svg). */ static bool icon_file_exists(int dir_fd, char *path, size_t path_len) { #if defined(FUZZEL_ENABLE_PNG_LIBPNG) path[path_len - 3] = 'p'; path[path_len - 2] = 'n'; path[path_len - 1] = 'g'; if (faccessat(dir_fd, path, R_OK, 0) < 0) { #if defined(FUZZEL_ENABLE_SVG_NANOSVG) || defined(FUZZEL_ENABLE_SVG_LIBRSVG) path[path_len - 3] = 's'; path[path_len - 2] = 'v'; path[path_len - 1] = 'g'; return faccessat(dir_fd, path, R_OK, 0) == 0; #else return false; #endif } return true; #elif defined(FUZZEL_ENABLE_SVG_NANOSVG) || defined(FUZZEL_ENABLE_SVG_LIBRSVG) path[path_len - 3] = 's'; path[path_len - 2] = 'v'; path[path_len - 1] = 'g'; return faccessat(dir_fd, path, R_OK, 0) == 0; #else return false; #endif } static bool lookup_icons(const icon_theme_list_t *themes, int icon_size, struct application_list *applications, const xdg_data_dirs_t *xdg_dirs) { struct icon_data { const char *name; struct application *app; char *file_name; size_t file_name_len; struct { int diff; const struct xdg_data_dir *xdg_dir; const struct icon_theme *theme; const struct icon_dir *icon_dir; enum icon_type type; } min_diff; }; tll(struct icon_data) icons = tll_init(); for (size_t i = 0; i < applications->count; i++) { struct application *app = &applications->v[i]; icon_reset(&app->icon); if (app->icon.name == NULL) continue; if (app->icon.name[0] == '/') { const size_t name_len = strlen(app->icon.name); if (app->icon.name[name_len - 3] == 's' && app->icon.name[name_len - 2] == 'v' && app->icon.name[name_len - 1] == 'g') { if (svg(&app->icon, app->icon.name)) LOG_DBG("%s: absolute path SVG", app->icon.name); } else if (app->icon.name[name_len - 3] == 'p' && app->icon.name[name_len - 2] == 'n' && app->icon.name[name_len - 1] == 'g') { if (png(&app->icon, app->icon.name)) LOG_DBG("%s: abslute path PNG", app->icon.name); } } else { size_t file_name_len = strlen(app->icon.name) + 4; char *file_name = malloc(file_name_len + 1); strcpy(file_name, app->icon.name); strcat(file_name, ".xxx"); struct icon_data data = { .name = app->icon.name, .app = app, .file_name = file_name, .file_name_len = file_name_len, .min_diff = {.diff = INT_MAX}, }; tll_push_back(icons, data); } } /* For details, see * https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup */ tll_foreach(*themes, theme_it) { const struct icon_theme *theme = &theme_it->item; /* Fallback icon to use if there aren’t any exact matches */ /* Assume sorted */ tll_foreach(theme->dirs, icon_dir_it) { const struct icon_dir *icon_dir = &icon_dir_it->item; char theme_relative_path[ 5 + 1 + /* “icons” */ strlen(theme->name) + 1 + strlen(icon_dir->path) + 1]; sprintf(theme_relative_path, "icons/%s/%s", theme->name, icon_dir->path); tll_foreach(*xdg_dirs, xdg_dir_it) { const struct xdg_data_dir *xdg_dir = &xdg_dir_it->item; const int scale = icon_dir->scale; const int size = icon_dir->size * scale; const int min_size = icon_dir->min_size * scale; const int max_size = icon_dir->max_size * scale; const int threshold = icon_dir->threshold * scale; const enum icon_dir_type type = icon_dir->type; bool is_exact_match = false; int diff = INT_MAX; /* See if this directory is usable for the requested icon size */ switch (type) { case ICON_DIR_FIXED: is_exact_match = size == icon_size; diff = abs(size - icon_size); LOG_DBG( "%s/%s (fixed): " "icon-size=%d, size=%d, exact=%d, diff=%d", xdg_dir->path, theme_relative_path, icon_size, size, is_exact_match, diff); break; case ICON_DIR_THRESHOLD: is_exact_match = (size - threshold) <= icon_size && (size + threshold) >= icon_size; diff = icon_size < (size - threshold) ? min_size - icon_size : (icon_size > (size + threshold) ? icon_size - max_size : 0); LOG_DBG( "%s/%s (threshold): " "icon-size=%d, threshold=%d, exact=%d, diff=%d", xdg_dir->path, theme_relative_path, icon_size, threshold, is_exact_match, diff); break; case ICON_DIR_SCALABLE: is_exact_match = min_size <= icon_size && max_size >= icon_size; diff = icon_size < min_size ? min_size - icon_size : (icon_size > max_size ? icon_size - max_size : 0); LOG_DBG("%s/%s (scalable): " "icon-size=%d, min=%d, max=%d, exact=%d, diff=%d", xdg_dir->path, theme_relative_path, icon_size, min_size, max_size, is_exact_match, diff); break; } int dir_fd = openat( xdg_dir->fd, theme_relative_path, O_RDONLY | O_DIRECTORY); if (dir_fd < 0) continue; tll_foreach(icons, icon_it) { struct icon_data *icon = &icon_it->item; if (!is_exact_match && icon->min_diff.diff <= diff) continue; size_t len = icon->file_name_len; char *path = icon->file_name; path[len - 4] = '.'; if (!icon_file_exists(dir_fd, path, len)) continue; if (!is_exact_match) { assert(diff < icon->min_diff.diff); icon->min_diff.diff = diff; icon->min_diff.xdg_dir = xdg_dir; icon->min_diff.theme = theme; icon->min_diff.icon_dir = icon_dir; icon->min_diff.type = path[len - 3] == 's' ? ICON_SVG : ICON_PNG; continue; } char *full_path = malloc( strlen(xdg_dir->path) + 1 + 5 + 1 + /* “icons” */ strlen(theme->name) + 1 + strlen(icon_dir->path) + 1 + len + 1); sprintf(full_path, "%s/icons/%s/%s/%s", xdg_dir->path, theme->name, icon_dir->path, path); if ((path[len - 3] == 's' && svg(&icon->app->icon, full_path)) || (path[len - 3] == 'p' && png(&icon->app->icon, full_path))) { LOG_DBG("%s: %s", icon->name, full_path); free(icon->file_name); tll_remove(icons, icon_it); } free(full_path); } close(dir_fd); } } /* Try loading fallbacks for those icons we didn’t find an * exact match */ tll_foreach(icons, icon_it) { struct icon_data *icon = &icon_it->item; if (icon->min_diff.type == ICON_NONE) { assert(icon->min_diff.xdg_dir == NULL); continue; } size_t path_len = strlen(icon->min_diff.xdg_dir->path) + 1 + 5 + 1 + /* “icons” */ strlen(icon->min_diff.theme->name) + 1 + strlen(icon->min_diff.icon_dir->path) + 1 + strlen(icon->name) + 4; char full_path[path_len + 1]; sprintf(full_path, "%s/icons/%s/%s/%s.%s", icon->min_diff.xdg_dir->path, icon->min_diff.theme->name, icon->min_diff.icon_dir->path, icon->name, icon->min_diff.type == ICON_SVG ? "svg" : "png"); if ((icon->min_diff.type == ICON_SVG && svg(&icon->app->icon, full_path)) || (icon->min_diff.type == ICON_PNG && png(&icon->app->icon, full_path))) { LOG_DBG("%s: %s (fallback)", icon->name, full_path); free(icon->file_name); tll_remove(icons, icon_it); } else { /* Reset diff data, before checking the parent theme(s) */ icon->min_diff.diff = INT_MAX; icon->min_diff.xdg_dir = NULL; icon->min_diff.theme = NULL; icon->min_diff.icon_dir = NULL; icon->min_diff.type = ICON_NONE; } } } /* Finally, look in XDG_DATA_DIRS/pixmaps */ tll_foreach(icons, icon_it) { const struct icon_data *icon = &icon_it->item; tll_foreach(*xdg_dirs, it) { int pixmaps_fd = openat(it->item.fd, "pixmaps", O_RDONLY); if (pixmaps_fd < 0) continue; size_t len = icon->file_name_len; char *path = icon->file_name; if (!icon_file_exists(pixmaps_fd, path, len)) { close(pixmaps_fd); continue; } close(pixmaps_fd); char full_path[strlen(it->item.path) + 1 + strlen("pixmaps") + 1 + len + 1]; /* Try SVG variant first */ sprintf(full_path, "%s/pixmaps/%s", it->item.path, path); if (path[len - 3] == 's' && svg(&icon->app->icon, full_path)) { LOG_DBG("%s: %s (pixmaps)", icon->name, full_path); break; } /* No SVG, look for PNG instead */ if (path[len - 3] == 'p' && png(&icon->app->icon, full_path)) { LOG_DBG("%s: %s (pixmaps)", icon->name, full_path); break; } } free(icon->file_name); tll_remove(icons, icon_it); } return true; } bool icon_lookup_application_icons(icon_theme_list_t themes, int icon_size, struct application_list *applications) { xdg_data_dirs_t xdg_dirs = xdg_data_dirs(); lookup_icons(&themes, icon_size, applications, &xdg_dirs); xdg_data_dirs_destroy(xdg_dirs); return true; } fuzzel/icon.h000066400000000000000000000014761445417001200135040ustar00rootroot00000000000000#pragma once #include #include "application.h" #include "tllist.h" enum icon_dir_type { ICON_DIR_FIXED, ICON_DIR_SCALABLE, ICON_DIR_THRESHOLD, }; struct icon_dir { char *path; /* Relative to theme's base path */ int size; int min_size; int max_size; int scale; int threshold; enum icon_dir_type type; }; struct icon_theme { char *name; tll(struct icon_dir) dirs; }; typedef tll(struct icon_theme) icon_theme_list_t; icon_theme_list_t icon_load_theme(const char *name); void icon_themes_destroy(icon_theme_list_t themes); bool icon_lookup_application_icons( icon_theme_list_t themes, int icon_size, struct application_list *applications); bool icon_from_png(struct icon *icon, const char *name); bool icon_from_svg(struct icon *icon, const char *name); fuzzel/key-binding.c000066400000000000000000000260161445417001200147440ustar00rootroot00000000000000#include "key-binding.h" #include #include #define LOG_MODULE "key-binding" #define LOG_ENABLE_DBG 0 #include "log.h" #define ALEN(v) (sizeof(v) / sizeof((v)[0])) struct key_set { struct key_binding_set public; const struct config *conf; const struct seat *seat; }; typedef tll(struct key_set) bind_set_list_t; struct kb_manager { struct key_set *last_used_set; bind_set_list_t binding_sets; }; static void load_keymap( struct key_set *set, struct xkb_state *xkb, struct xkb_keymap *keymap); static void unload_keymap(struct key_set *set); struct kb_manager * kb_manager_new(void) { struct kb_manager *mgr = calloc(1, sizeof(*mgr)); return mgr; } void kb_manager_destroy(struct kb_manager *mgr) { assert(tll_length(mgr->binding_sets) == 0); free(mgr); } void kb_new_for_seat(struct kb_manager *mgr, const struct config *conf, const struct seat *seat) { #if defined(_DEBUG) tll_foreach(mgr->binding_sets, it) assert(it->item.seat != seat); #endif struct key_set set = { .public = { .key = tll_init(), //.mouse = tll_init(), }, .conf = conf, .seat = seat, }; tll_push_back(mgr->binding_sets, set); LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), (void *)set.seat, (void *)set.conf); //load_keymap(&tll_back(mgr->binding_sets)); LOG_DBG("new (seat): total number of sets: %zu", tll_length(mgr->binding_sets)); } static void kb_set_destroy(struct kb_manager *mgr, struct key_set *set) { unload_keymap(set); if (mgr->last_used_set == set) mgr->last_used_set = NULL; /* Note: caller must remove from binding_sets */ } void kb_remove_seat(struct kb_manager *mgr, const struct seat *seat) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat != seat) continue; kb_set_destroy(mgr, set); tll_remove(mgr->binding_sets, it); LOG_DBG("remove seat: set=%p, seat=%p, total number of sets: %zu", (void *)set, (void *)seat, tll_length(mgr->binding_sets)); } LOG_DBG("remove seat: total number of sets: %zu", tll_length(mgr->binding_sets)); } static xkb_keycode_list_t key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) { xkb_keycode_list_t key_codes = tll_init(); /* * Find all key codes that map to this symbol. * * This allows us to match bindings in other layouts * too. */ struct xkb_state *state = xkb_state_new(keymap); for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); code <= xkb_keymap_max_keycode(keymap); code++) { if (xkb_state_key_get_one_sym(state, code) == sym) tll_push_back(key_codes, code); } xkb_state_unref(state); return key_codes; } static xkb_keysym_t maybe_repair_key_combo(struct xkb_state *xkb, struct xkb_keymap *keymap, xkb_keysym_t sym, xkb_mod_mask_t mods) { /* * Detect combos containing a shifted symbol and the corresponding * modifier, and replace the shifted symbol with its unshifted * variant. * * For example, the combo is “Control+Shift+U”. In this case, * Shift is the modifier used to “shift” ‘u’ to ‘U’, after which * ‘Shift’ will have been “consumed”. Since we filter out consumed * modifiers when matching key combos, this key combo will never * trigger (we will never be able to match the ‘Shift’ modifier). * * There are two correct variants of the above key combo: * - “Control+U” (upper case ‘U’) * - “Control+Shift+u” (lower case ‘u’) * * What we do here is, for each key *code*, check if there are any * (shifted) levels where it produces ‘sym’. If there are, check * *which* sets of modifiers are needed to produce it, and compare * with ‘mods’. * * If there is at least one common modifier, it means ‘sym’ is a * “shifted” symbol, with the corresponding shifting modifier * explicitly included in the key combo. I.e. the key combo will * never trigger. * * We then proceed and “repair” the key combo by replacing ‘sym’ * with the corresponding unshifted symbol. * * To reduce the noise, we ignore all key codes where the shifted * symbol is the same as the unshifted symbol. */ for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); code <= xkb_keymap_max_keycode(keymap); code++) { xkb_layout_index_t layout_idx = xkb_state_key_get_layout(xkb, code); /* Get all unshifted symbols for this key */ const xkb_keysym_t *base_syms = NULL; size_t base_count = xkb_keymap_key_get_syms_by_level( keymap, code, layout_idx, 0, &base_syms); if (base_count == 0 || sym == base_syms[0]) { /* No unshifted symbols, or unshifted symbol is same as ‘sym’ */ continue; } /* Name of the unshifted symbol, for logging */ char base_name[100]; xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name)); /* Iterate all shift levels */ for (xkb_level_index_t level_idx = 1; level_idx < xkb_keymap_num_levels_for_key( keymap, code, layout_idx); level_idx++) { /* Get all symbols for current shift level */ const xkb_keysym_t *shifted_syms = NULL; size_t shifted_count = xkb_keymap_key_get_syms_by_level( keymap, code, layout_idx, level_idx, &shifted_syms); for (size_t i = 0; i < shifted_count; i++) { if (shifted_syms[i] != sym) continue; /* Get modifier sets that produces the current shift level */ xkb_mod_mask_t mod_masks[16]; size_t mod_mask_count = xkb_keymap_key_get_mods_for_level( keymap, code, layout_idx, level_idx, mod_masks, ALEN(mod_masks)); /* Check if key combo’s modifier set intersects */ for (size_t j = 0; j < mod_mask_count; j++) { if ((mod_masks[j] & mods) != mod_masks[j]) continue; char combo[64] = {0}; for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) { if (!(mods & (1u << k))) continue; const char *mod_name = xkb_keymap_mod_get_name( keymap, k); strcat(combo, mod_name); strcat(combo, "+"); } size_t len = strlen(combo); xkb_keysym_get_name( sym, &combo[len], sizeof(combo) - len); LOG_WARN( "%s: combo with both explicit modifier and shifted symbol " "(level=%d, mod-mask=0x%08x), " "replacing with %s", combo, level_idx, mod_masks[j], base_name); /* Replace with unshifted symbol */ return base_syms[0]; } } } } return sym; } static xkb_mod_mask_t conf_modifiers_to_mask(struct xkb_keymap *keymap, const struct config_key_modifiers *modifiers) { xkb_mod_index_t shift = xkb_keymap_mod_get_index( keymap, XKB_MOD_NAME_SHIFT); xkb_mod_index_t alt = xkb_keymap_mod_get_index( keymap, XKB_MOD_NAME_ALT); xkb_mod_index_t ctrl = xkb_keymap_mod_get_index( keymap, XKB_MOD_NAME_CTRL); xkb_mod_index_t super = xkb_keymap_mod_get_index( keymap, XKB_MOD_NAME_LOGO); xkb_mod_mask_t mods = 0; if (shift != XKB_MOD_INVALID) mods |= modifiers->shift << shift; if (ctrl != XKB_MOD_INVALID) mods |= modifiers->ctrl << ctrl; if (alt != XKB_MOD_INVALID) mods |= modifiers->alt << alt; if (super != XKB_MOD_INVALID) mods |= modifiers->super << super; return mods; } static void convert_key_binding(struct xkb_state *xkb, struct xkb_keymap *keymap, const struct config_key_binding *conf_binding, key_binding_list_t *bindings) { xkb_mod_mask_t mods = conf_modifiers_to_mask( keymap, &conf_binding->modifiers); xkb_keysym_t sym = maybe_repair_key_combo( xkb, keymap, conf_binding->k.sym, mods); struct key_binding binding = { .action = conf_binding->action, .mods = mods, .k = { .sym = sym, .key_codes = key_codes_for_xkb_sym(keymap, sym), }, }; tll_push_back(*bindings, binding); } static void convert_key_bindings(struct key_set *set, struct xkb_state *xkb, struct xkb_keymap *keymap) { const struct config *conf = set->conf; for (size_t i = 0; i < conf->key_bindings.count; i++) { const struct config_key_binding *binding = &conf->key_bindings.arr[i]; convert_key_binding(xkb, keymap, binding, &set->public.key); } } static void load_keymap(struct key_set *set, struct xkb_state *xkb, struct xkb_keymap *keymap) { LOG_DBG("load keymap: set=%p, seat=%p, conf=%p", (void *)set, (void *)set->seat, (void *)set->conf); if (keymap == NULL) { LOG_DBG("no XKB keymap"); return; } convert_key_bindings(set, xkb, keymap); //convert_mouse_bindings(set); } void kb_load_keymap(struct kb_manager *mgr, const struct seat *seat, struct xkb_state *xkb, struct xkb_keymap *keymap) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat == seat) load_keymap(set, xkb, keymap); } } static void kb_destroy(key_binding_list_t *bindings) { tll_foreach(*bindings, it) { struct key_binding *bind = &it->item; tll_free(bind->k.key_codes); tll_remove(*bindings, it); } } static void unload_keymap(struct key_set *set) { kb_destroy(&set->public.key); //kb_destroy(&set->public.mouse); } void kb_unload_keymap(struct kb_manager *mgr, const struct seat *seat) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat != seat) continue; LOG_DBG("unload keymap: set=%p, seat=%p, conf=%p", (void *)set, (void *)seat, (void *)set->conf); unload_keymap(set); } } struct key_binding_set * key_binding_for(struct kb_manager *mgr, const struct seat *seat) { struct key_set *last_used = mgr->last_used_set; if (last_used != NULL && last_used->seat == seat) return &last_used->public; tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat != seat) continue; mgr->last_used_set = set; return &set->public; } return NULL; } fuzzel/key-binding.h000066400000000000000000000047351445417001200147550ustar00rootroot00000000000000#pragma once #include #include #include "config.h" enum bind_action { BIND_ACTION_NONE, BIND_ACTION_CANCEL, BIND_ACTION_CURSOR_HOME, BIND_ACTION_CURSOR_END, BIND_ACTION_CURSOR_LEFT, BIND_ACTION_CURSOR_LEFT_WORD, BIND_ACTION_CURSOR_RIGHT, BIND_ACTION_CURSOR_RIGHT_WORD, BIND_ACTION_DELETE_PREV, BIND_ACTION_DELETE_PREV_WORD, BIND_ACTION_DELETE_NEXT, BIND_ACTION_DELETE_NEXT_WORD, BIND_ACTION_DELETE_LINE, BIND_ACTION_INSERT_SELECTED, BIND_ACTION_MATCHES_EXECUTE, BIND_ACTION_MATCHES_EXECUTE_OR_NEXT, BIND_ACTION_MATCHES_PREV, BIND_ACTION_MATCHES_PREV_WITH_WRAP, BIND_ACTION_MATCHES_PREV_PAGE, BIND_ACTION_MATCHES_NEXT, BIND_ACTION_MATCHES_NEXT_WITH_WRAP, BIND_ACTION_MATCHES_NEXT_PAGE, BIND_ACTION_MATCHES_FIRST, BIND_ACTION_MATCHES_LAST, BIND_ACTION_CUSTOM_1, BIND_ACTION_CUSTOM_2, BIND_ACTION_CUSTOM_3, BIND_ACTION_CUSTOM_4, BIND_ACTION_CUSTOM_5, BIND_ACTION_CUSTOM_6, BIND_ACTION_CUSTOM_7, BIND_ACTION_CUSTOM_8, BIND_ACTION_CUSTOM_9, BIND_ACTION_CUSTOM_10, BIND_ACTION_CUSTOM_11, BIND_ACTION_CUSTOM_12, BIND_ACTION_CUSTOM_13, BIND_ACTION_CUSTOM_14, BIND_ACTION_CUSTOM_15, BIND_ACTION_CUSTOM_16, BIND_ACTION_CUSTOM_17, BIND_ACTION_CUSTOM_18, BIND_ACTION_CUSTOM_19, BIND_ACTION_COUNT, }; typedef tll(xkb_keycode_t) xkb_keycode_list_t; struct key_binding { int action; /* enum bind_action_* */ xkb_mod_mask_t mods; union { struct { xkb_keysym_t sym; xkb_keycode_list_t key_codes; } k; #if 0 struct { uint32_t button; int count; } m; #endif }; }; typedef tll(struct key_binding) key_binding_list_t; struct key_binding_set { key_binding_list_t key; //key_binding_list_t mouse; }; struct seat; struct kb_manager; struct kb_manager *kb_manager_new(void); void kb_manager_destroy(struct kb_manager *mgr); void kb_new_for_seat(struct kb_manager *mgr, const struct config *conf, const struct seat *seat); void kb_remove_seat(struct kb_manager *mgr, const struct seat *seat); void kb_load_keymap(struct kb_manager *mgr, const struct seat *seat, struct xkb_state *xkb, struct xkb_keymap *keymap); void kb_unload_keymap(struct kb_manager *mgr, const struct seat *seat); struct key_binding_set *key_binding_for( struct kb_manager *mgr, const struct seat *seat); fuzzel/log.c000066400000000000000000000127761445417001200133350ustar00rootroot00000000000000#include "log.h" #include #include #include #include #include #include #include #include #include #include #define ALEN(v) (sizeof(v) / sizeof((v)[0])) #define UNUSED __attribute__((unused)) static bool colorize = false; static bool do_syslog = true; static enum log_class log_level = LOG_CLASS_NONE; static const struct { const char name[8]; const char log_prefix[7]; uint8_t color; int syslog_equivalent; } log_level_map[] = { [LOG_CLASS_NONE] = {"none", "none", 5, -1}, [LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR}, [LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING}, [LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO}, [LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG}, }; void log_init(enum log_colorize _colorize, bool _do_syslog, enum log_facility syslog_facility, enum log_class _log_level) { static const int facility_map[] = { [LOG_FACILITY_USER] = LOG_USER, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; colorize = _colorize == LOG_COLORIZE_NEVER ? false : _colorize == LOG_COLORIZE_ALWAYS ? true : isatty(STDERR_FILENO); do_syslog = _do_syslog; log_level = _log_level; int slvl = log_level_map[_log_level].syslog_equivalent; if (do_syslog && slvl != -1) { openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); setlogmask(LOG_UPTO(slvl)); } } void log_deinit(void) { if (do_syslog) closelog(); } static void _log(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, int sys_errno, va_list va) { assert(log_class > LOG_CLASS_NONE); assert(log_class < ALEN(log_level_map)); if (log_class > log_level) return; const char *prefix = log_level_map[log_class].log_prefix; unsigned int class_clr = log_level_map[log_class].color; char clr[16]; snprintf(clr, sizeof(clr), "\033[%um", class_clr); fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : ""); if (colorize) fputs("\033[2m", stderr); fprintf(stderr, "%s:%d: ", file, lineno); if (colorize) fputs("\033[0m", stderr); vfprintf(stderr, fmt, va); if (sys_errno != 0) fprintf(stderr, ": %s (%d)", strerror(sys_errno), sys_errno); fputc('\n', stderr); } static void _sys_log(enum log_class log_class, const char *module, const char UNUSED *file, int UNUSED lineno, const char *fmt, int sys_errno, va_list va) { assert(log_class > LOG_CLASS_NONE); assert(log_class < ALEN(log_level_map)); if (!do_syslog) return; /* Map our log level to syslog's level */ int level = log_level_map[log_class].syslog_equivalent; char msg[4096]; int n = vsnprintf(msg, sizeof(msg), fmt, va); assert(n >= 0); if (sys_errno != 0 && (size_t)n < sizeof(msg)) snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno)); syslog(level, "%s: %s", module, msg); } void log_msg_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) { va_list va2; va_copy(va2, va); _log(log_class, module, file, lineno, fmt, 0, va); _sys_log(log_class, module, file, lineno, fmt, 0, va2); va_end(va2); } void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); log_msg_va(log_class, module, file, lineno, fmt, va); va_end(va); } void log_errno_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) { log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va); } void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); log_errno_va(log_class, module, file, lineno, fmt, va); va_end(va); } void log_errno_provided_va(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy, const char *fmt, va_list va) { va_list va2; va_copy(va2, va); _log(log_class, module, file, lineno, fmt, errno_copy, va); _sys_log(log_class, module, file, lineno, fmt, errno_copy, va2); va_end(va2); } void log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy, const char *fmt, ...) { va_list va; va_start(va, fmt); log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va); va_end(va); } static size_t map_len(void) { size_t len = ALEN(log_level_map); #ifndef _DEBUG /* Exclude "debug" entry for non-debug builds */ len--; #endif return len; } int log_level_from_string(const char *str) { if (str[0] == '\0') return -1; for (int i = 0, n = map_len(); i < n; i++) if (strcmp(str, log_level_map[i].name) == 0) return i; return -1; } const char * log_level_string_hint(void) { static char buf[64]; if (buf[0] != '\0') return buf; for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) { const char *entry = log_level_map[i].name; const char *delim = (i + 1 < n) ? ", " : ""; pos += snprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim); } return buf; } fuzzel/log.h000066400000000000000000000045271445417001200133350ustar00rootroot00000000000000#pragma once #include #include enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; enum log_class { LOG_CLASS_NONE, LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG, }; void log_init(enum log_colorize colorize, bool do_syslog, enum log_facility syslog_facility, enum log_class log_level); void log_deinit(void); void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) __attribute__((format (printf, 5, 6))); void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) __attribute__((format (printf, 5, 6))); void log_errno_provided( enum log_class log_class, const char *module, const char *file, int lineno, int _errno, const char *fmt, ...) __attribute__((format (printf, 6, 7))); void log_errno_va( enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) __attribute__((format (printf, 5, 0))); void log_errno_provided_va( enum log_class log_class, const char *module, const char *file, int lineno, int _errno, const char *fmt, va_list va) __attribute__((format (printf, 6, 0))); int log_level_from_string(const char *str); const char *log_level_string_hint(void); #define LOG_ERR(fmt, ...) \ log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #define LOG_ERRNO(fmt, ...) \ log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #define LOG_ERRNO_P(fmt, _errno, ...) \ log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ _errno, fmt, ## __VA_ARGS__) #define LOG_WARN(fmt, ...) \ log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #define LOG_INFO(fmt, ...) \ log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG #define LOG_DBG(fmt, ...) \ log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #else #define LOG_DBG(fmt, ...) #endif fuzzel/main.c000066400000000000000000001441241445417001200134710ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "fuzzel" #define LOG_ENABLE_DBG 0 #include "log.h" #include "application.h" #include "char32.h" #include "config.h" #include "dmenu.h" #include "fdm.h" #include "key-binding.h" #include "match.h" #include "render.h" #include "shm.h" #include "version.h" #include "wayland.h" #include "xdg.h" #define max(x, y) ((x) > (y) ? (x) : (y)) struct context { const struct config *conf; struct wayland *wayl; struct render *render; struct matches *matches; struct prompt *prompt; struct application_list *apps; icon_theme_list_t *themes; int icon_size; mtx_t *icon_lock; #if 0 /* TODO: can we use a struct config ptr instead? */ struct { const char *icon_theme; const char *terminal; bool icons_enabled; bool actions_enabled; bool dmenu_mode; } options; #endif int event_fd; int dmenu_abort_fd; }; static void read_cache(struct application_list *apps) { const char *path = xdg_cache_dir(); if (path == NULL) { LOG_WARN("failed to get cache directory: not saving popularity cache"); return; } int cache_dir_fd = open(path, O_DIRECTORY); if (cache_dir_fd == -1) { LOG_ERRNO("%s: failed to open", path); return; } struct stat st; int fd = -1; if (fstatat(cache_dir_fd, "fuzzel", &st, 0) == -1 || (fd = openat(cache_dir_fd, "fuzzel", O_RDONLY)) == -1) { close(cache_dir_fd); if (errno != ENOENT) LOG_ERRNO("%s/fuzzel: failed to open", path); return; } close(cache_dir_fd); char *text = mmap( NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); close(fd); if (text == MAP_FAILED) { LOG_ERRNO("%s/fuzzel: failed to mmap", path); return; } size_t app_idx = 0; /* Loop lines */ char *lineptr = NULL; for (char *line = strtok_r(text, "\n", &lineptr); line != NULL; line = strtok_r(NULL, "\n", &lineptr)) { /* Parse each line ("|<count>") */ char *ptr = NULL; const char *title = strtok_r(line, "|", &ptr); const char *count_str = strtok_r(NULL, "|", &ptr); if (title == NULL || count_str == NULL) { LOG_ERR("invalid cache entry (cache corrupt?): %s", line); continue; } int count; sscanf(count_str, "%u", &count); size_t wlen = mbstoc32(NULL, title, 0); if (wlen == (size_t)-1) continue; char32_t wtitle[wlen + 1]; mbstoc32(wtitle, title, wlen + 1); for (; app_idx < apps->count; app_idx++) { int cmp = c32cmp(apps->v[app_idx].title, wtitle); if (cmp == 0) { apps->v[app_idx].count = count; app_idx++; break; } else if (cmp > 0) break; } } munmap(text, st.st_size); } static void write_cache(const struct application_list *apps) { const char *path = xdg_cache_dir(); if (path == NULL) { LOG_WARN("failed to get cache directory: not saving popularity cache"); return; } int cache_dir_fd = open(path, O_DIRECTORY); if (cache_dir_fd == -1) { LOG_ERRNO("%s: failed to open", path); return; } int fd = openat(cache_dir_fd, "fuzzel", O_WRONLY | O_CREAT | O_TRUNC, 0644); close(cache_dir_fd); if (fd == -1) { LOG_ERRNO("%s/fuzzel: failed to open", path); return; } for (size_t i = 0; i < apps->count; i++) { if (apps->v[i].count == 0) continue; char count_as_str[11]; sprintf(count_as_str, "%u", apps->v[i].count); const size_t count_len = strlen(count_as_str); size_t c32len = c32tombs(NULL, apps->v[i].title, 0); char ctitle[c32len + 1]; c32tombs(ctitle, apps->v[i].title, c32len + 1); if (write(fd, ctitle, c32len) != c32len || write(fd, "|", 1) != 1 || write(fd, count_as_str, count_len) != count_len || write(fd, "\n", 1) != 1) { LOG_ERRNO("failed to write cache"); break; } } close(fd); } static const char * version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), "version: %s %ccairo %cpng %csvg%s %cassertions", FUZZEL_VERSION, #if defined(FUZZEL_ENABLE_CAIRO) '+', #else '-', #endif #if defined(FUZZEL_ENABLE_PNG_LIBPNG) '+', #else '-', #endif #if defined(FUZZEL_ENABLE_SVG_NANOSVG) '+', "(nanosvg)", #elif defined(FUZZEL_ENABLE_SVG_LIBRSVG) '+', "(librsvg)", #else '-', "", #endif #if !defined(NDEBUG) '+' #else '-' #endif ); return buf; } static void print_usage(const char *prog_name) { printf("Usage: %s [OPTION]...\n", prog_name); printf("\n"); printf("Options:\n"); printf(" --config=PATH load configuration from PATH (XDG_CONFIG_HOME/fuzzel/fuzzel.ini)\n" " -o,--output=OUTPUT output (monitor) to display on (none)\n" " -f,--font=FONT font name and style, in FontConfig format\n" " (monospace)\n" " -D,--dpi-aware=no|yes|auto enable or disable DPI aware rendering (auto)\n" " -i,--icon-theme=NAME icon theme name (\"hicolor\")\n" " -I,--no-icons do not render any icons\n" " -F,--fields=FIELDS comma separated list of XDG Desktop entry\n" " fields to match\n" " -p,--prompt=PROMPT string to use as input prompt (\"> \")\n" " --password=[CHARACTER] render all input as CHARACTER ('*' by default)\n" " -T,--terminal terminal command to use when launching\n" " 'terminal' programs, e.g. \"xterm -e\".\n" " Not used in dmenu mode (not set)\n" " -l,--lines number of matches to show\n" " -w,--width window width, in characters (margins and\n" " borders not included)\n" " --tabs=INT number of spaces a tab is expanded to (8)\n" " -x,--horizontal-pad=PAD horizontal padding, in pixels (40)\n" " -y,--vertical-pad=PAD vertical padding, in pixels (8)\n" " -P,--inner-pad=PAD vertical padding between prompt and match list,\n" " in pixels (0)\n" " -b,--background-color=HEX background color (000000ff)\n" " -t,--text-color=HEX text color (ffffffff)\n" " -m,--match-color=HEX color of matched substring (cc9393ff)\n" " -s,--selection-color=HEX background color of selected item (333333ff)\n" " -S,--selection-text-color=HEX text color of selected item (ffffffff)\n" " -M,--selection-match-color=HEX color of matched substring in selection (cb4b16ff)\n" " -B,--border-width=INT width of border, in pixels (1)\n" " -r,--border-radius=INT amount of corner \"roundness\" (10)\n" " -C,--border-color=HEX border color (ffffffff)\n" " --show-actions include desktop actions in the list\n" " --no-fuzzy disable fuzzy matching\n" " --fuzzy-min-length=VALUE search strings shorter than this will not be\n" " fuzzy matched (3)\n" " --fuzzy-max-length-discrepancy=VALUE maximum allowed length discrepancy\n" " between a fuzzy match and the search\n" " criteria. Larger values mean more\n" " fuzzy matches (2)\n" " --fuzzy-max-distance=VALUE maximum levenshtein distance between a fuzzy (1)\n" " match and the search criteria. Larger values\n" " mean more fuzzy matches\n" " --line-height=HEIGHT override line height from font metrics\n" " --letter-spacing=AMOUNT additional letter spacing\n" " --layer=top|overlay which layer to render the fuzzel window on (top)\n" " --no-exit-on-keyboard-focus-loss do not exit when losing keyboard focus\n" " --launch-prefix=COMMAND prefix to add before argv of executed program\n" " -d,--dmenu dmenu compatibility mode\n" " --dmenu0 like --dmenu, but input is NUL separated\n" " instead of newline separated\n" " --index print selected entry's index instead of of the \n" " entry's text (dmenu mode only)\n" " -R,--no-run-if-empty exit immediately without showing UI if stdin\n" " is empty (dmenu mode only)\n" " --log-level={info|warning|error|none}\n" " log level (info)\n" " --log-colorize=[never|always|auto]\n" " enable/disable colorization of log output on\n" " stderr\n" " --log-no-syslog disable syslog logging\n" " -v,--version show the version number and quit\n"); printf("\n"); printf("All colors are RGBA - i.e. 8-digit hex values, without prefix.\n"); } static void font_reloaded(struct wayland *wayl, struct fcft_font *font, void *data) { struct context *ctx = data; const struct config *conf = ctx->conf; applications_flush_text_run_cache(ctx->apps); mtx_lock(ctx->icon_lock); { ctx->icon_size = render_icon_size(ctx->render); if (conf->icons_enabled) { icon_lookup_application_icons( *ctx->themes, ctx->icon_size, ctx->apps); } } mtx_unlock(ctx->icon_lock); } static bool pt_or_px_from_string(const char *s, struct pt_or_px *res) { const size_t len = strlen(s); if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') { errno = 0; char *end = NULL; long value = strtol(s, &end, 10); if (!(errno == 0 && end == s + len - 2)) { fprintf(stderr, "%s: not a valid pixel value\n", s); return false; } res->pt = 0; res->px = value; } else { errno = 0; char *end = NULL; float value = strtof(s, &end); if (!(errno == 0 && *end == '\0')) { fprintf(stderr, "%s: not a valid point value\n", s); return false; } res->pt = value; res->px = 0; } return true; } static char * lock_file_name(void) { const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime_dir == NULL) xdg_runtime_dir = "/tmp"; const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) return NULL; #define path_fmt "%s/fuzzel-%s.lock" int chars = snprintf(NULL, 0, path_fmt, xdg_runtime_dir, wayland_display); char *path = malloc(chars + 1); snprintf(path, chars + 1, path_fmt, xdg_runtime_dir, wayland_display); #undef path_fmt LOG_DBG("lock file: %s", path); return path; } static bool acquire_file_lock(const char *path, int *fd) { *fd = open(path, O_WRONLY | O_CREAT | O_CLOEXEC, 0644); if (*fd < 0) { /* Warn, but allow running anyway */ LOG_WARN("%s: failed to create lock file", path); return true; } if (flock(*fd, LOCK_EX | LOCK_NB) < 0) { if (errno == EWOULDBLOCK) { /* The file is locked and the LOCK_NB flag was selected */ LOG_ERR("%s: failed to acquire lock: fuzzel already running?", path); return false; } } return true; } enum event_type { EVENT_APPS_LOADED = 1, EVENT_ICONS_LOADED = 2 }; static int send_event(int fd, enum event_type event) { ssize_t bytes = write(fd, &(uint64_t){event}, sizeof(uint64_t)); if (bytes < 0) return -errno; else if (bytes != (ssize_t)sizeof(uint64_t)) return 1; return 0; } /* THREAD */ static int populate_apps(void *_ctx) { struct context *ctx = _ctx; struct application_list *apps = ctx->apps; const struct config *conf = ctx->conf; const char *icon_theme = conf->icon_theme; const char *terminal = conf->terminal; bool actions_enabled = conf->actions_enabled; bool dmenu_enabled = conf->dmenu.enabled; bool icons_enabled = conf->icons_enabled; char dmenu_delim = conf->dmenu.delim; bool filter_desktop = conf->filter_desktop; char_list_t desktops = tll_init(); char *saveptr = NULL; if (filter_desktop) { char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); if (xdg_current_desktop && strlen(xdg_current_desktop) != 0) { xdg_current_desktop = strdup(xdg_current_desktop); for (char *desktop = strtok_r(xdg_current_desktop, ":", &saveptr); desktop != NULL; desktop = strtok_r(NULL, ":", &saveptr)) { tll_push_back(desktops, strdup(desktop)); } free(xdg_current_desktop); } } if (dmenu_enabled) dmenu_load_entries(apps, dmenu_delim, ctx->dmenu_abort_fd); else { xdg_find_programs(terminal, actions_enabled, filter_desktop, &desktops, apps); read_cache(apps); } tll_free_and_free(desktops, free); int r = send_event(ctx->event_fd, EVENT_APPS_LOADED); if (r != 0) return r; if (icons_enabled) { icon_theme_list_t icon_themes = icon_load_theme(icon_theme); if (tll_length(icon_themes) > 0) LOG_INFO("theme: %s", tll_front(icon_themes).name); else LOG_WARN("%s: icon theme not found", icon_theme); mtx_lock(ctx->icon_lock); { *ctx->themes = icon_themes; if (ctx->icon_size > 0) { icon_lookup_application_icons( *ctx->themes, ctx->icon_size, apps); } } mtx_unlock(ctx->icon_lock); r = send_event(ctx->event_fd, EVENT_ICONS_LOADED); if (r != 0) return r; } return 0; } /* * Called when the application list has been populated */ static bool fdm_apps_populated(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; uint64_t event; ssize_t bytes = read(fd, &event, sizeof(event)); if (bytes != (ssize_t)sizeof(event)) { if (bytes < 0) LOG_ERRNO("failed to read event FD"); else LOG_ERR("partial read from event FD"); return false; } struct context *ctx = data; struct wayland *wayl = ctx->wayl; struct application_list *apps = ctx->apps; struct matches *matches = ctx->matches; struct prompt *prompt = ctx->prompt; switch (event) { case EVENT_APPS_LOADED: /* Update matches list, then refresh the GUI */ matches_set_applications(matches, apps); matches_update(matches, prompt); break; case EVENT_ICONS_LOADED: /* Just need to refresh the GUI */ break; default: LOG_ERR("unknown event: %llx", (long long)event); return false; } wayl_refresh(wayl); return true; } int main(int argc, char *const *argv) { #define OPT_LETTER_SPACING 256 #define OPT_LAUNCH_PREFIX 257 #define OPT_SHOW_ACTIONS 258 #define OPT_NO_FUZZY 259 #define OPT_FUZZY_MIN_LENGTH 260 #define OPT_FUZZY_MAX_LENGTH_DISCREPANCY 261 #define OPT_FUZZY_MAX_DISTANCE 262 #define OPT_DMENU_INDEX 263 #define OPT_LOG_LEVEL 264 #define OPT_LOG_COLORIZE 265 #define OPT_LOG_NO_SYSLOG 266 #define OPT_PASSWORD 267 #define OPT_CONFIG 268 #define OPT_LAYER 269 #define OPT_ICON_THEME 270 #define OPT_NO_EXIT_ON_KB_LOSS 271 #define OPT_TABS 272 #define OPT_DMENU_NULL 273 #define OPT_FILTER_DESKTOP 274 static const struct option longopts[] = { {"config", required_argument, 0, OPT_CONFIG}, {"output" , required_argument, 0, 'o'}, {"font", required_argument, 0, 'f'}, {"dpi-aware", required_argument, 0, 'D'}, {"icon-theme", required_argument, 0, OPT_ICON_THEME}, {"no-icons", no_argument, 0, 'I'}, {"fields", required_argument, 0, 'F'}, {"password", optional_argument, 0, OPT_PASSWORD}, {"lines", required_argument, 0, 'l'}, {"width", required_argument, 0, 'w'}, {"tabs", required_argument, 0, OPT_TABS}, {"horizontal-pad", required_argument, 0, 'x'}, {"vertical-pad", required_argument, 0, 'y'}, {"inner-pad", required_argument, 0, 'P'}, {"background-color", required_argument, 0, 'b'}, {"text-color", required_argument, 0, 't'}, {"match-color", required_argument, 0, 'm'}, {"selection-color", required_argument, 0, 's'}, {"selection-text-color", required_argument, 0, 'S'}, {"selection-match-color",required_argument, 0, 'M'}, {"border-width", required_argument, 0, 'B'}, {"border-radius", required_argument, 0, 'r'}, {"border-color", required_argument, 0, 'C'}, {"prompt", required_argument, 0, 'p'}, {"terminal", required_argument, 0, 'T'}, {"show-actions", no_argument, 0, OPT_SHOW_ACTIONS}, {"filter-desktop", optional_argument, 0, OPT_FILTER_DESKTOP}, {"no-fuzzy", no_argument, 0, OPT_NO_FUZZY}, {"fuzzy-min-length", required_argument, 0, OPT_FUZZY_MIN_LENGTH}, {"fuzzy-max-length-discrepancy", required_argument, 0, OPT_FUZZY_MAX_LENGTH_DISCREPANCY}, {"fuzzy-max-distance", required_argument, 0, OPT_FUZZY_MAX_DISTANCE}, {"line-height", required_argument, 0, 'H'}, {"letter-spacing", required_argument, 0, OPT_LETTER_SPACING}, {"launch-prefix", required_argument, 0, OPT_LAUNCH_PREFIX}, {"layer", required_argument, 0, OPT_LAYER}, {"no-exit-on-keyboard-focus-loss", no_argument, 0, OPT_NO_EXIT_ON_KB_LOSS}, /* dmenu mode */ {"dmenu", no_argument, 0, 'd'}, {"dmenu0", no_argument, 0, OPT_DMENU_NULL}, {"no-run-if-empty", no_argument, 0, 'R'}, {"index", no_argument, 0, OPT_DMENU_INDEX}, /* Misc */ {"log-level", required_argument, 0, OPT_LOG_LEVEL}, {"log-colorize", optional_argument, 0, OPT_LOG_COLORIZE}, {"log-no-syslog", no_argument, 0, OPT_LOG_NO_SYSLOG}, {"version", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, {NULL, no_argument, 0, 0}, }; const char *config_path = NULL; enum log_class log_level = LOG_CLASS_INFO; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool log_syslog = true; struct { struct config conf; bool dpi_aware_set:1; bool match_fields_set:1; bool icons_disabled_set:1; bool lines_set:1; bool chars_set:1; bool tabs_set:1; bool pad_x_set:1; bool pad_y_set:1; bool pad_inner_set:1; bool background_color_set:1; bool text_color_set:1; bool match_color_set:1; bool selection_color_set:1; bool selection_text_color_set:1; bool selection_match_color_set:1; bool border_color_set:1; bool border_size_set:1; bool border_radius_set:1; bool actions_enabled_set:1; bool filter_desktop_set:1; bool fuzzy_set:1; bool fuzzy_min_length_set:1; bool fuzzy_max_length_discrepancy_set:1; bool fuzzy_max_distance_set:1; bool line_height_set:1; bool letter_spacing_set:1; bool dmenu_enabled_set:1; bool dmenu_mode_set:1; bool dmenu_exit_immediately_if_empty_set:1; bool dmenu_delim_set:1; bool layer_set:1; bool no_exit_on_keyboard_focus_loss_set:1; } cmdline_overrides = {{0}}; setlocale(LC_CTYPE, ""); setlocale(LC_MESSAGES, ""); /* Auto-enable dmenu mode if invoked through a ‘dmenu’ symlink */ if (argv[0] != NULL) { char *copy = strdup(argv[0]); if (copy != NULL) { const char *name = basename(copy); if (name != NULL && strcmp(name, "dmenu") == 0) { cmdline_overrides.conf.dmenu.enabled = true; cmdline_overrides.dmenu_enabled_set = true; } free(copy); } } while (true) { int c = getopt_long(argc, argv, ":o:f:D:IF:il:w:x:y:p:P:b:t:m:s:S:M:B:r:C:T:dRvh", longopts, NULL); if (c == -1) break; switch (c) { case OPT_CONFIG: config_path = optarg; break; case 'o': cmdline_overrides.conf.output = optarg; break; case 'f': cmdline_overrides.conf.font = optarg; break; case 'D': if (strcmp(optarg, "auto") == 0) cmdline_overrides.conf.dpi_aware = DPI_AWARE_AUTO; else if (strcmp(optarg, "no") == 0) cmdline_overrides.conf.dpi_aware = DPI_AWARE_NO; else if (strcmp(optarg, "yes") == 0) cmdline_overrides.conf.dpi_aware = DPI_AWARE_YES; else { fprintf( stderr, "%s: invalid value for dpi-aware: " "must be one of 'auto', 'no', or 'yes'\n", optarg); return EXIT_FAILURE; } cmdline_overrides.dpi_aware_set = true; break; case 'i': /* Ignore (this flag means case insensitive search in * other fuzzel-like utilities) */ break; case OPT_ICON_THEME: cmdline_overrides.conf.icon_theme = optarg; break; case 'I': cmdline_overrides.conf.icons_enabled = false; cmdline_overrides.icons_disabled_set = true; break; case 'F': { static const struct { const char *name; enum match_fields value; } map[] = { {"filename", MATCH_FILENAME}, {"name", MATCH_NAME}, {"generic", MATCH_GENERIC}, {"exec", MATCH_EXEC}, {"categories", MATCH_CATEGORIES}, {"keywords", MATCH_KEYWORDS}, {"comment", MATCH_COMMENT}, }; cmdline_overrides.conf.match_fields = 0; for (const char *f = strtok(optarg, ", "); f != NULL; f = strtok(NULL, ", ")) { enum match_fields field = 0; for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { if (strcmp(f, map[i].name) == 0) { field = map[i].value; break; } } if (field > 0) cmdline_overrides.conf.match_fields |= field; else { char valid_names[128] = {0}; int idx = 0; for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { idx += snprintf( &valid_names[idx], sizeof(valid_names) - idx, "'%s', ", map[i].name); } valid_names[idx - 2] = '\0'; fprintf( stderr, "%s: invalid field name: must be one of %s\n", f, valid_names); return EXIT_FAILURE; } } cmdline_overrides.match_fields_set = true; break; } case OPT_PASSWORD: { char32_t password_char = U'\0'; if (optarg != NULL) { char32_t *wide_optarg = ambstoc32(optarg); if (c32len(wide_optarg) != 1) { fprintf( stderr, "%s: password character must be a single character\n", optarg); free(wide_optarg); return EXIT_FAILURE; } password_char = wide_optarg[0]; free(wide_optarg); } cmdline_overrides.conf.password_mode.enabled = true; cmdline_overrides.conf.password_mode.character = password_char; break; } case 'T': cmdline_overrides.conf.terminal = optarg; break; case 'l': if (sscanf(optarg, "%u", &cmdline_overrides.conf.lines) != 1) { fprintf(stderr, "%s: invalid line count\n", optarg); return EXIT_FAILURE; } cmdline_overrides.lines_set = true; break; case 'w': if (sscanf(optarg, "%u", &cmdline_overrides.conf.chars) != 1) { fprintf(stderr, "%s: invalid width\n", optarg); return EXIT_FAILURE; } cmdline_overrides.chars_set = true; break; case OPT_TABS: if (sscanf(optarg, "%u", &cmdline_overrides.conf.tabs) != 1) { fprintf(stderr, "%s: invalid tab count\n", optarg); return EXIT_FAILURE; } cmdline_overrides.tabs_set = true; break; case 'x': if (sscanf(optarg, "%u", &cmdline_overrides.conf.pad.x) != 1) { fprintf(stderr, "%s: invalid padding\n", optarg); return EXIT_FAILURE; } cmdline_overrides.pad_x_set = true; break; case 'y': if (sscanf(optarg, "%u", &cmdline_overrides.conf.pad.y) != 1) { fprintf(stderr, "%s: invalid padding\n", optarg); return EXIT_FAILURE; } cmdline_overrides.pad_y_set = true; break; case 'P': if (sscanf(optarg, "%u", &cmdline_overrides.conf.pad.inner) != 1) { fprintf(stderr, "%s: invalid padding\n", optarg); return EXIT_FAILURE; } cmdline_overrides.pad_inner_set = true; break; case 'p': free(cmdline_overrides.conf.prompt); cmdline_overrides.conf.prompt = ambstoc32(optarg); if (cmdline_overrides.conf.prompt == NULL) { fprintf(stderr, "%s: invalid prompt\n", optarg); return EXIT_FAILURE; } break; case 'b': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t background = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "background-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.background = conf_hex_to_rgba(background); cmdline_overrides.background_color_set = true; break; } case 't': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t text_color = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "text-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.text = conf_hex_to_rgba(text_color); cmdline_overrides.text_color_set = true; break; } case 'm': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t match_color = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "match-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.match = conf_hex_to_rgba(match_color); cmdline_overrides.match_color_set = true; break; } case 's': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t selection_color = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "selection-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.selection = conf_hex_to_rgba(selection_color); cmdline_overrides.selection_color_set = true; break; } case 'S': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t selection_text_color = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "selection-text-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.selection_text = conf_hex_to_rgba(selection_text_color); cmdline_overrides.selection_text_color_set = true; break; } case 'M': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t selection_match_color = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "selection-match-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.selection_match = conf_hex_to_rgba(selection_match_color); cmdline_overrides.selection_match_color_set = true; break; } case 'B': if (sscanf(optarg, "%u", &cmdline_overrides.conf.border.size) != 1) { fprintf( stderr, "%s: invalid border width (must be an integer)\n", optarg); return EXIT_FAILURE; } cmdline_overrides.border_size_set = true; break; case 'r': if (sscanf(optarg, "%u", &cmdline_overrides.conf.border.radius) != 1) { fprintf(stderr, "%s: invalid border radius (must be an integer)\n", optarg); return EXIT_FAILURE; } cmdline_overrides.border_radius_set = true; break; case 'C': { const char *clr_start = optarg; if (clr_start[0] == '#') clr_start++; errno = 0; char *end = NULL; uint32_t border_color = strtoul(clr_start, &end, 16); if (errno != 0 || end == NULL || *end != '\0' || (end - clr_start) != 8) { fprintf(stderr, "border-color: %s: invalid color\n", optarg); return EXIT_FAILURE; } cmdline_overrides.conf.colors.border = conf_hex_to_rgba(border_color); cmdline_overrides.border_color_set = true; break; } case OPT_SHOW_ACTIONS: cmdline_overrides.actions_enabled_set = true; cmdline_overrides.conf.actions_enabled = true; break; case OPT_FILTER_DESKTOP: cmdline_overrides.filter_desktop_set = true; if (optarg != NULL && strcasecmp(optarg, "no") == 0) cmdline_overrides.conf.filter_desktop = false; else if (optarg != NULL) { fprintf(stderr, "%s: invalid filter-desktop option\n", optarg); return EXIT_FAILURE; } else cmdline_overrides.conf.filter_desktop = true; break; case OPT_NO_FUZZY: cmdline_overrides.conf.fuzzy.enabled = false; cmdline_overrides.fuzzy_set = true; break; case OPT_FUZZY_MIN_LENGTH: if (sscanf(optarg, "%zu", &cmdline_overrides.conf.fuzzy.min_length) != 1) { fprintf( stderr, "%s: invalid fuzzy min length (must be an integer)\n", optarg); return EXIT_FAILURE; } cmdline_overrides.fuzzy_min_length_set = true; break; case OPT_FUZZY_MAX_LENGTH_DISCREPANCY: if (sscanf(optarg, "%zu", &cmdline_overrides.conf.fuzzy.max_length_discrepancy) != 1) { fprintf( stderr, "%s: invalid fuzzy max length discrepancy " "(must be an integer)\n", optarg); return EXIT_FAILURE; } cmdline_overrides.fuzzy_max_length_discrepancy_set = true; break; case OPT_FUZZY_MAX_DISTANCE: if (sscanf(optarg, "%zu", &cmdline_overrides.conf.fuzzy.max_distance) != 1) { fprintf( stderr, "%s: invalid fuzzy max distance (must be an integer)\n", optarg); return EXIT_FAILURE; } cmdline_overrides.fuzzy_max_distance_set = true; break; case 'H': { /* line-height */ if (!pt_or_px_from_string(optarg, &cmdline_overrides.conf.line_height)) return EXIT_FAILURE; cmdline_overrides.line_height_set = true; break; } case OPT_LETTER_SPACING: { if (!pt_or_px_from_string(optarg, &cmdline_overrides.conf.letter_spacing)) return EXIT_FAILURE; cmdline_overrides.letter_spacing_set = true; break; } case OPT_LAUNCH_PREFIX: { cmdline_overrides.conf.launch_prefix = optarg; break; } case OPT_LAYER: if (strcasecmp(optarg, "top") == 0) cmdline_overrides.conf.layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; else if (strcasecmp(optarg, "overlay") == 0) cmdline_overrides.conf.layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; else { fprintf( stderr, "%s: invalid layer. Must be one of 'top', 'overlay'\n", optarg); return EXIT_FAILURE; } cmdline_overrides.layer_set = true; break; case OPT_NO_EXIT_ON_KB_LOSS: cmdline_overrides.conf.exit_on_kb_focus_loss = false; cmdline_overrides.no_exit_on_keyboard_focus_loss_set = true; break; case 'd': cmdline_overrides.conf.dmenu.enabled = true; cmdline_overrides.dmenu_enabled_set = true; break; case OPT_DMENU_NULL: cmdline_overrides.conf.dmenu.enabled = true; cmdline_overrides.conf.dmenu.delim = '\0'; cmdline_overrides.dmenu_enabled_set = true; cmdline_overrides.dmenu_delim_set = true; break; case 'R': cmdline_overrides.conf.dmenu.exit_immediately_if_empty = true; cmdline_overrides.dmenu_exit_immediately_if_empty_set = true; break; case OPT_DMENU_INDEX: cmdline_overrides.conf.dmenu.mode = DMENU_MODE_INDEX; cmdline_overrides.dmenu_mode_set = true; break; case OPT_LOG_LEVEL: { int lvl = log_level_from_string(optarg); if (lvl < 0) { fprintf( stderr, "--log-level: %s: argument must be one of %s\n", optarg, log_level_string_hint()); return EXIT_FAILURE; } log_level = lvl; break; } case OPT_LOG_COLORIZE: if (optarg == NULL || strcmp(optarg, "auto") == 0) log_colorize = LOG_COLORIZE_AUTO; else if (strcmp(optarg, "never") == 0) log_colorize = LOG_COLORIZE_NEVER; else if (strcmp(optarg, "always") == 0) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "--log-colorize: %s: argument must be one of 'never', 'always' or 'auto'\n", optarg); return EXIT_FAILURE; } break; case OPT_LOG_NO_SYSLOG: log_syslog = false; break; case 'v': printf("fuzzel %s\n", version_and_features()); return EXIT_SUCCESS; case 'h': print_usage(argv[0]); return EXIT_SUCCESS; case ':': fprintf(stderr, "error: %s: missing required argument\n", argv[optind-1]); return EXIT_FAILURE; case '?': fprintf(stderr, "error: %s: invalid option\n", argv[optind-1]); return EXIT_FAILURE; } } log_init(log_colorize, log_syslog, LOG_FACILITY_USER, log_level); LOG_INFO("%s", version_and_features()); int ret = EXIT_FAILURE; struct config conf = {0}; bool conf_successful = config_load(&conf, config_path, NULL, true); if (!conf_successful) { config_free(&conf); return ret; } /* Apply command line overrides */ if (cmdline_overrides.conf.output != NULL) { free(conf.output); conf.output = strdup(cmdline_overrides.conf.output); } if (cmdline_overrides.conf.prompt != NULL) { free(conf.prompt); conf.prompt = cmdline_overrides.conf.prompt; } if (cmdline_overrides.conf.password_mode.enabled) { conf.password_mode.enabled = true; conf.password_mode.character = cmdline_overrides.conf.password_mode.character != U'\0' ? cmdline_overrides.conf.password_mode.character : conf.password_mode.character != U'\0' ? conf.password_mode.character : U'*'; } if (cmdline_overrides.conf.terminal != NULL) { free(conf.terminal); conf.terminal = strdup(cmdline_overrides.conf.terminal); } if (cmdline_overrides.conf.launch_prefix != NULL) { free(conf.launch_prefix); conf.launch_prefix = strdup(cmdline_overrides.conf.launch_prefix); } if (cmdline_overrides.conf.font != NULL) { free(conf.font); conf.font = strdup(cmdline_overrides.conf.font); } if (cmdline_overrides.conf.icon_theme != NULL) { free(conf.icon_theme); conf.icon_theme = strdup(cmdline_overrides.conf.icon_theme); } if (cmdline_overrides.dpi_aware_set) conf.dpi_aware = cmdline_overrides.conf.dpi_aware; if (cmdline_overrides.match_fields_set) conf.match_fields = cmdline_overrides.conf.match_fields; if (cmdline_overrides.icons_disabled_set) conf.icons_enabled = cmdline_overrides.conf.icons_enabled; if (cmdline_overrides.lines_set) conf.lines = cmdline_overrides.conf.lines; if (cmdline_overrides.chars_set) conf.chars = cmdline_overrides.conf.chars; if (cmdline_overrides.tabs_set) conf.tabs = cmdline_overrides.conf.tabs; if (cmdline_overrides.pad_x_set) conf.pad.x = cmdline_overrides.conf.pad.x; if (cmdline_overrides.pad_y_set) conf.pad.y = cmdline_overrides.conf.pad.y; if (cmdline_overrides.pad_inner_set) conf.pad.inner = cmdline_overrides.conf.pad.inner; if (cmdline_overrides.background_color_set) conf.colors.background = cmdline_overrides.conf.colors.background; if (cmdline_overrides.text_color_set) conf.colors.text = cmdline_overrides.conf.colors.text; if (cmdline_overrides.match_color_set) conf.colors.match = cmdline_overrides.conf.colors.match; if (cmdline_overrides.selection_color_set) conf.colors.selection = cmdline_overrides.conf.colors.selection; if (cmdline_overrides.selection_text_color_set) conf.colors.selection_text = cmdline_overrides.conf.colors.selection_text; if (cmdline_overrides.selection_match_color_set) conf.colors.selection_match = cmdline_overrides.conf.colors.selection_match; if (cmdline_overrides.border_color_set) conf.colors.border = cmdline_overrides.conf.colors.border; if (cmdline_overrides.border_size_set) conf.border.size = cmdline_overrides.conf.border.size; if (cmdline_overrides.border_radius_set) { #if defined(FUZZEL_ENABLE_CAIRO) conf.border.radius = cmdline_overrides.conf.border.radius; #else LOG_WARN("fuzzel compiled without cairo support, " "ignoring --border-radius=%d", cmdline_overrides.conf.border.radius); #endif } if (cmdline_overrides.filter_desktop_set) conf.filter_desktop = cmdline_overrides.conf.filter_desktop; if (cmdline_overrides.actions_enabled_set) conf.actions_enabled = cmdline_overrides.conf.actions_enabled; if (cmdline_overrides.fuzzy_set) conf.fuzzy.enabled = cmdline_overrides.conf.fuzzy.enabled; if (cmdline_overrides.fuzzy_min_length_set) conf.fuzzy.min_length = cmdline_overrides.conf.fuzzy.min_length; if (cmdline_overrides.fuzzy_max_length_discrepancy_set) conf.fuzzy.max_length_discrepancy = cmdline_overrides.conf.fuzzy.max_length_discrepancy; if (cmdline_overrides.fuzzy_max_distance_set) conf.fuzzy.max_distance = cmdline_overrides.conf.fuzzy.max_distance; if (cmdline_overrides.line_height_set) conf.line_height = cmdline_overrides.conf.line_height; if (cmdline_overrides.letter_spacing_set) conf.letter_spacing = cmdline_overrides.conf.letter_spacing; if (cmdline_overrides.layer_set) conf.layer = cmdline_overrides.conf.layer; if (cmdline_overrides.no_exit_on_keyboard_focus_loss_set) conf.exit_on_kb_focus_loss = cmdline_overrides.conf.exit_on_kb_focus_loss; if (cmdline_overrides.dmenu_enabled_set) conf.dmenu.enabled = cmdline_overrides.conf.dmenu.enabled; if (cmdline_overrides.dmenu_delim_set) conf.dmenu.delim = cmdline_overrides.conf.dmenu.delim; if (cmdline_overrides.dmenu_mode_set) conf.dmenu.mode = cmdline_overrides.conf.dmenu.mode; if (cmdline_overrides.dmenu_exit_immediately_if_empty_set) conf.dmenu.exit_immediately_if_empty = cmdline_overrides.conf.dmenu.exit_immediately_if_empty; _Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR, "fcft log level enum offset"); _Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, "fcft colorize enum mismatch"); fcft_init((enum fcft_log_colorize)log_colorize, log_syslog, (enum fcft_log_class)log_level); #if !defined(FUZZEL_ENABLE_SVG_LIBRSVG) /* Skip fcft cleanup if we’re using the librsvg backend * (https://codeberg.org/dnkl/fuzzel/issues/87) */ atexit(&fcft_fini); #endif mtx_t icon_lock; if (mtx_init(&icon_lock, mtx_plain) != thrd_success) { LOG_ERR("failed to create icon lock"); return EXIT_FAILURE; } struct application_list *apps = NULL; struct fdm *fdm = NULL; struct prompt *prompt = NULL; struct matches *matches = NULL; struct render *render = NULL; struct wayland *wayl = NULL; struct kb_manager *kb_manager = NULL; thrd_t app_thread_id; bool join_app_thread = false; int event_pipe[2] = {-1, -1}; int dmenu_abort_fd = -1; char *lock_file = NULL; int file_lock_fd = -1; bool unlink_lock_file = true; icon_theme_list_t themes = tll_init(); /* Don’t allow multiple instances (in the same Wayland session) */ lock_file = lock_file_name(); if (lock_file != NULL) { if (!acquire_file_lock(lock_file, &file_lock_fd)) { unlink_lock_file = false; goto out; } } if ((fdm = fdm_init()) == NULL) goto out; if ((render = render_init(&conf, &icon_lock)) == NULL) goto out; if ((prompt = prompt_init(conf.prompt)) == NULL) goto out; if ((matches = matches_init( conf.match_fields, conf.fuzzy.enabled, conf.fuzzy.min_length, conf.fuzzy.max_length_discrepancy, conf.fuzzy.max_distance)) == NULL) goto out; matches_max_matches_per_page_set(matches, conf.lines); if ((apps = applications_init()) == NULL) goto out; if (conf.dmenu.enabled) { if (conf.dmenu.exit_immediately_if_empty) { /* * If no_run_if_empty is set, we *must* load the entries * *before displaying the window. */ dmenu_load_entries(apps, conf.dmenu.delim, -1); if (apps->count == 0) goto out; if (conf.icons_enabled) { themes = icon_load_theme(conf.icon_theme); if (tll_length(themes) > 0) LOG_INFO("theme: %s", tll_front(themes).name); else LOG_WARN("%s: icon theme not found", conf.icon_theme); } matches_set_applications(matches, apps); matches_update(matches, prompt); } else { dmenu_abort_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); if (dmenu_abort_fd < 0) { LOG_ERRNO("failed to create event FD for dmenu mode"); goto out; } } } struct context ctx = { .conf = &conf, .render = render, .matches = matches, .prompt = prompt, .apps = apps, .themes = &themes, .icon_lock = &icon_lock, .event_fd = -1, .dmenu_abort_fd = dmenu_abort_fd, }; if ((kb_manager = kb_manager_new()) == NULL) goto out; if ((wayl = wayl_init( &conf, fdm, kb_manager, render, prompt, matches, &font_reloaded, &ctx)) == NULL) goto out; ctx.wayl = wayl; /* Create thread that will populate the application list */ if (!conf.dmenu.enabled || !conf.dmenu.exit_immediately_if_empty) { if (pipe2(event_pipe, O_CLOEXEC | O_NONBLOCK) < 0) { LOG_ERRNO("failed to create event pipe"); goto out; } ctx.event_fd = event_pipe[1]; if (!fdm_add(fdm, event_pipe[0], EPOLLIN, &fdm_apps_populated, &ctx)) goto out; if (thrd_create(&app_thread_id, &populate_apps, &ctx) != thrd_success) { LOG_ERR("failed to create thread"); goto out; } join_app_thread = true; } wayl_refresh(wayl); while (true) { wayl_flush(wayl); if (!fdm_poll(fdm)) break; } if (wayl_update_cache(wayl)) write_cache(apps); ret = wayl_exit_code(wayl); out: if (join_app_thread) { if (dmenu_abort_fd >= 0) { if (write(dmenu_abort_fd, &(uint64_t){1}, sizeof(uint64_t)) < 0) LOG_ERRNO("failed to signal abort"); } int res; thrd_join(app_thread_id, &res); if (res != 0) { if (res < 0) LOG_ERRNO_P("populate application list thread failed", res); else LOG_ERRNO("populate application list thread failed: " "failed to signal done event"); } } if (event_pipe[0] >= 0) { fdm_del_no_close(fdm, event_pipe[0]); close(event_pipe[0]); } if (event_pipe[1] >= 0) close(event_pipe[1]); if (dmenu_abort_fd >= 0) close(dmenu_abort_fd); mtx_destroy(&icon_lock); shm_fini(); wayl_destroy(wayl); kb_manager_destroy(kb_manager); render_destroy(render); matches_destroy(matches); prompt_destroy(prompt); fdm_destroy(fdm); applications_destroy(apps); icon_themes_destroy(themes); //free(prompt_allocated); config_free(&conf); #if defined(FUZZEL_ENABLE_CAIRO) && defined(_DEBUG) cairo_debug_reset_static_data(); #endif log_deinit(); if (file_lock_fd >= 0) close(file_lock_fd); if (lock_file != NULL) { if (unlink_lock_file) unlink(lock_file); free(lock_file); } return ret; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/match.c��������������������������������������������������������������������������������������0000664�0000000�0000000�00000052254�14454170012�0013643�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "match.h" #include <stdlib.h> #include <string.h> #include <wctype.h> #include <assert.h> #define LOG_MODULE "match" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #define min(x, y) ((x < y) ? (x) : (y)) #define max(x, y) ((x > y) ? (x) : (y)) struct matches { const struct application_list *applications; enum match_fields fields; struct match *matches; bool fuzzy; size_t page_count; size_t match_count; size_t selected; size_t max_matches_per_page; size_t fuzzy_min_length; size_t fuzzy_max_length_discrepancy; size_t fuzzy_max_distance; }; struct levenshtein_matrix { size_t distance; enum choice { UNSET, FIRST, SECOND, THIRD } choice; }; static void levenshtein_distance(const char32_t *a, size_t alen, const char32_t *b, size_t blen, struct levenshtein_matrix **m) { for (size_t i = 1; i <= blen; i++) { m[i][0].distance = i; m[i][0].choice = FIRST; } for (size_t i = 1; i <= blen; i++) { for (size_t j = 1; j <= alen; j++) { const size_t cost = towlower(a[j - 1]) == towlower(b[i - 1]) ? 0 : 1; const size_t first = m[i - 1][j].distance + 1; const size_t second = m[i][j - 1].distance + 1; const size_t third = m[i - 1][j - 1].distance + cost; const size_t shortest = min(min(first, second), third); m[i][j].distance = shortest; if (shortest == first) m[i][j].choice = FIRST; else if (shortest == second) m[i][j].choice = SECOND; else m[i][j].choice = THIRD; } } } static const char32_t * match_levenshtein(struct matches *matches, const char32_t *src, const char32_t *pat, size_t *_match_len) { if (!matches->fuzzy) return NULL; const size_t src_len = c32len(src); const size_t pat_len = c32len(pat); if (pat_len < matches->fuzzy_min_length) return NULL; if (src_len < pat_len) return NULL; struct levenshtein_matrix **m = calloc(pat_len + 1, sizeof(m[0])); for (size_t i = 0; i < pat_len + 1; i++) m[i] = calloc(src_len + 1, sizeof(m[0][0])); levenshtein_distance(src, src_len, pat, pat_len, m); #if 0 /* Dump levenshtein table */ printf(" "); for (size_t j = 0; j < src_len; j++) printf("%lc ", src[j]); printf("\n"); for (size_t i = 0; i < pat_len + 1; i++) { if (i > 0) printf("%lc ", pat[i - 1]); else printf(" "); for (size_t j = 0; j < src_len + 1; j++) printf("%zu%c ", m[i][j].distance, m[i][j].choice == FIRST ? 'f' : m[i][j].choice == SECOND ? 's' : m[i][j].choice == THIRD ? 't' : 'u'); printf("\n"); } #endif size_t c = 0; size_t best = m[pat_len][0].distance; for (ssize_t j = src_len; j >= 0; j--) { if (m[pat_len][j].distance < best) { best = m[pat_len][j].distance; c = j; } } const size_t end = c; size_t r = pat_len; // printf("starting at r=%zu, c=%zu\n", r, c); while (r > 0) { switch (m[r][c].choice) { case UNSET: assert(false); break; case FIRST: r--; break; case SECOND: c--; break; case THIRD: r--; c--; break; } } #if 0 *match = (struct levenshtein_match){ .start = c, .len = end - c, .distance = m[pat_len][end].distance, }; #endif const size_t match_ofs = c; const size_t match_distance = m[pat_len][end].distance; const size_t match_len = end - c; LOG_DBG("%ls vs. %ls: sub-string: %.*ls, (distance=%zu, row=%zu, col=%zu)", (const wchar_t *)src, (const wchar_t *)pat, (int)match_len, &src[match_ofs], match_distance, r, c); for (size_t i = 0; i < pat_len + 1; i++) free(m[i]); free(m); const size_t len_diff = match_len > pat_len ? match_len - pat_len : pat_len - match_len; if (len_diff <= matches->fuzzy_max_length_discrepancy && match_distance <= matches->fuzzy_max_distance) { if (_match_len != NULL) *_match_len = match_len; return &src[match_ofs]; } return NULL; } struct matches * matches_init(enum match_fields fields, bool fuzzy, size_t fuzzy_min_length, size_t fuzzy_max_length_discrepancy, size_t fuzzy_max_distance) { struct matches *matches = malloc(sizeof(*matches)); *matches = (struct matches) { .applications = NULL, .fields = fields, .matches = NULL, .fuzzy = fuzzy, .page_count = 0, .match_count = 0, .selected = 0, .max_matches_per_page = 0, .fuzzy_min_length = fuzzy_min_length, .fuzzy_max_length_discrepancy = fuzzy_max_length_discrepancy, .fuzzy_max_distance = fuzzy_max_distance, }; return matches; } void matches_destroy(struct matches *matches) { if (matches == NULL) return; if (matches->applications != NULL) { for (size_t i = 0; i < matches->applications->count; i++) free(matches->matches[i].pos); } free(matches->matches); free(matches); } void matches_set_applications(struct matches *matches, const struct application_list *applications) { assert(matches->applications == NULL); assert(matches->matches == NULL); matches->applications = applications; matches->matches = calloc( applications->count, sizeof(matches->matches[0])); } bool matches_have_icons(const struct matches *matches) { if (matches->applications == NULL) return false; for (size_t i = 0; i < matches->applications->count; i++) { if (matches->applications->v[i].icon.name != NULL) return true; } return false; } size_t matches_max_matches_per_page(const struct matches *matches) { return matches->max_matches_per_page; } void matches_max_matches_per_page_set(struct matches *matches, size_t max_matches) { matches->max_matches_per_page = max_matches; } size_t matches_get_page_count(const struct matches *matches) { return matches->max_matches_per_page != 0 ? matches->match_count / matches->max_matches_per_page : matches->match_count; } size_t matches_get_page(const struct matches *matches) { return matches->max_matches_per_page != 0 ? matches->selected / matches->max_matches_per_page : matches->selected; } const struct match * matches_get(const struct matches *matches, size_t idx) { const size_t page_no = matches_get_page(matches); const size_t items_on_page __attribute__((unused)) = matches_get_count(matches); LOG_DBG( "page-count: %zu, page-no: %zu, items-on-page: %zu, idx: %zu, max: %zu, " "match-count: %zu", matches->page_count, page_no, items_on_page, idx, matches->max_matches_per_page, matches->match_count); assert(idx < items_on_page); idx += page_no * matches->max_matches_per_page; assert(idx < matches->match_count); return &matches->matches[idx]; } const struct match * matches_get_match(const struct matches *matches) { return matches->match_count > 0 ? &matches->matches[matches->selected] : NULL; } size_t matches_get_count(const struct matches *matches) { const size_t total = matches->match_count; const size_t page_no = matches_get_page(matches); if (matches->max_matches_per_page == 0) return 0; if (total == 0) return 0; else if (page_no + 1 >= matches->page_count) { size_t remainder = total % matches->max_matches_per_page; return remainder == 0 ? matches->max_matches_per_page : remainder; } else return matches->max_matches_per_page; } size_t matches_get_total_count(const struct matches *matches) { return matches->match_count; } size_t matches_get_match_index(const struct matches *matches) { return matches->max_matches_per_page != 0 ? matches->selected % matches->max_matches_per_page : 0; } bool matches_selected_first(struct matches *matches) { if (matches->match_count <= 0 || matches->selected <= 0) return false; matches->selected = 0; return true; } bool matches_selected_last(struct matches *matches) { if (matches->match_count <= 0 || matches->selected >= matches->match_count - 1) { return false; } matches->selected = matches->match_count - 1; return true; } bool matches_selected_prev(struct matches *matches, bool wrap) { if (matches->selected > 0) { matches->selected--; return true; } else if (wrap && matches->match_count > 1) { matches->selected = matches->match_count - 1; return true; } return false; } bool matches_selected_next(struct matches *matches, bool wrap) { if (matches->selected + 1 < matches->match_count) { matches->selected++; return true; } else if (wrap && matches->match_count > 1) { matches->selected = 0; return true; } return false; } bool matches_selected_prev_page(struct matches *matches) { const size_t page_no = matches_get_page(matches); if (page_no > 0) { assert(matches->selected >= matches->max_matches_per_page); matches->selected -= matches->max_matches_per_page; return true; } else if (matches->selected > 0) { matches->selected = 0; return true; } return false; } bool matches_selected_next_page(struct matches *matches) { const size_t page_no = matches_get_page(matches); if (page_no + 1 < matches->page_count) { matches->selected = min( matches->selected + matches->max_matches_per_page, matches->match_count - 1); return true; } else if (matches->selected < matches->match_count - 1) { matches->selected = matches->match_count - 1; return true; } return false; } static int match_compar(const void *_a, const void *_b) { const struct match *a = _a; const struct match *b = _b; /* * Exact matches are preferred over fuzzy matches. I.e. an exact * match is considered to be “less” than a fuzzy match. * * If the two matches have the same match type, prefer the one * with the highest launch count. That is, a *high* launch count * is considered to be “less” than a low launch count. */ if (a->matched_type == MATCHED_FUZZY && b->matched_type == MATCHED_EXACT) return 1; else if (a->matched_type == MATCHED_EXACT && b->matched_type == MATCHED_FUZZY) return -1; else if (a->application->count > b->application->count) return -1; else if (a->application->count < b->application->count) return 1; else return 0; } static char32_t * c32casestr(const char32_t *haystack, const char32_t *needle) { const size_t hay_len = c32len(haystack); const size_t needle_len = c32len(needle); if (needle_len > hay_len) return NULL; for (size_t i = 0; i < hay_len - needle_len + 1; i++) { bool matched = true; for (size_t j = 0; j < needle_len; j++) { if (toc32lower(haystack[i + j]) != toc32lower(needle[j])) { matched = false; break; } } if (matched) return (char32_t *)&haystack[i]; } return NULL; } void matches_update(struct matches *matches, const struct prompt *prompt) { if (matches->applications == NULL) return; const char32_t *ptext = prompt_text(prompt); /* Nothing entered; all programs found matches */ if (c32len(ptext) == 0) { matches->match_count = 0; for (size_t i = 0; i < matches->applications->count; i++) { if (!matches->applications->v[i].visible) continue; free(matches->matches[matches->match_count].pos); matches->matches[matches->match_count++] = (struct match){ .matched_type = MATCHED_NONE, .application = &matches->applications->v[i], .pos = NULL, .pos_count = 0, .index = i, }; } /* Sort */ qsort(matches->matches, matches->match_count, sizeof(matches->matches[0]), &match_compar); if (matches->selected >= matches->match_count && matches->selected > 0) matches->selected = matches->match_count - 1; matches->page_count = matches->max_matches_per_page > 0 ? ((matches->match_count + (matches->max_matches_per_page - 1)) / matches->max_matches_per_page) : 1; return; } const enum match_fields fields = matches->fields; const bool match_filename = fields & MATCH_FILENAME; const bool match_name = fields & MATCH_NAME; const bool match_generic = fields & MATCH_GENERIC; const bool match_exec = fields & MATCH_EXEC; const bool match_categories = fields & MATCH_CATEGORIES; const bool match_keywords = fields & MATCH_KEYWORDS; const bool match_comment = fields & MATCH_COMMENT; LOG_DBG( "matching: filename=%s, name=%s, generic=%s, exec=%s, categories=%s, " "keywords=%s, comment=%s", match_filename ? "yes" : "no", match_name ? "yes" : "no", match_generic ? "yes" : "no", match_exec ? "yes" : "no", match_categories ? "yes" : "no", match_keywords ? "yes" : "no", match_comment ? "yes" : "no"); matches->match_count = 0; LOG_DBG("match update begin"); char32_t *copy = c32dup(ptext); size_t tok_count = 1; const char32_t **tokens = malloc(tok_count * sizeof(tokens[0])); tokens[0] = copy; for (char32_t *p = copy; *p != U'\0'; p++) { if (*p != U' ') continue; *p = U'\0'; if ((ptrdiff_t)(p - tokens[tok_count - 1]) == 0) { /* Collapse multiple spaces */ tokens[tok_count - 1] = p + 1; } else { tok_count++; tokens = realloc(tokens, tok_count * sizeof(tokens[0])); tokens[tok_count - 1] = p + 1; } } if (tokens[tok_count - 1][0] == U'\0') { /* Don’t count trailing spaces as a token */ tok_count--; } for (size_t i = 0; i < matches->applications->count; i++) { struct application *app = &matches->applications->v[i]; if (!app->visible) continue; enum matched_type app_match_type = MATCHED_NONE; size_t pos_count = 0; struct match *match = &matches->matches[matches->match_count]; struct match_substring *pos = match->pos; for (size_t j = 0; j < tok_count; j++) { const char32_t *token = tokens[j]; enum matched_type token_match_type = MATCHED_NONE; if (token_match_type == MATCHED_NONE && match_name) { const char32_t *m = c32casestr(app->title, token); size_t match_len = 0; if (m != NULL) { token_match_type = MATCHED_EXACT; match_len = c32len(token); LOG_DBG("%ls: exact title", (const wchar_t *)app->title); } else if ((m = match_levenshtein(matches, app->title, token, &match_len)) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy title", (const wchar_t *)app->title); } if (match_len > 0) { size_t new_pos_count = pos_count + 1; struct match_substring *new_pos = realloc( pos, new_pos_count * sizeof(new_pos[0])); if (new_pos != NULL) { pos = new_pos; pos_count = new_pos_count; pos[pos_count - 1].start = m - app->title; pos[pos_count - 1].len = match_len; match->pos = pos; } } } if (token_match_type == MATCHED_NONE && match_filename && app->basename != NULL) { if (c32casestr(app->basename, token) != NULL) { token_match_type = MATCHED_EXACT; LOG_DBG("%ls: exact filename", (const wchar_t *)app->title); } else if (match_levenshtein(matches, app->basename, token, NULL) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy filename", (const wchar_t *)app->title); } } if (token_match_type == MATCHED_NONE && match_generic && app->generic_name != NULL) { if (c32casestr(app->generic_name, token) != NULL) { token_match_type = MATCHED_EXACT; LOG_DBG("%ls: exact generic", (const wchar_t *)app->title); } else if (match_levenshtein(matches, app->generic_name, token, NULL) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy generic", (const wchar_t *)app->title); } } if (token_match_type == MATCHED_NONE && match_exec && app->wexec != NULL) { if (c32casestr(app->wexec, token) != NULL) { token_match_type = MATCHED_EXACT; LOG_DBG("%ls: exact exec", (const wchar_t *)app->title); } else if (match_levenshtein(matches, app->wexec, token, NULL) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy exec", (const wchar_t *)app->title); } } if (token_match_type == MATCHED_NONE && match_comment && app->comment != NULL) { if (c32casestr(app->comment, token) != NULL) { token_match_type = MATCHED_EXACT; LOG_DBG("%ls: exact comment", (const wchar_t *)app->title); } else if (match_levenshtein(matches, app->comment, token, NULL) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy comment", (const wchar_t *)app->title); } } if (token_match_type == MATCHED_NONE && match_keywords) { tll_foreach(app->keywords, it) { if (c32casestr(it->item, token) != NULL) { token_match_type = MATCHED_EXACT; LOG_DBG("%ls: exact keyword (%ls)", (const wchar_t *)app->title, (const wchar_t *)it->item); break; } else if (match_levenshtein(matches, it->item, token, NULL) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy keyword (%ls)", (const wchar_t *)app->title, (const wchar_t *)it->item); break; } } } if (token_match_type == MATCHED_NONE && match_categories) { tll_foreach(app->categories, it) { if (c32casestr(it->item, token) != NULL) { token_match_type = MATCHED_EXACT; LOG_DBG("%ls: exact category (%ls)", (const wchar_t *)app->title, (const wchar_t *)it->item); break; } else if (match_levenshtein(matches, it->item, token, NULL) != NULL) { token_match_type = MATCHED_FUZZY; LOG_DBG("%ls: fuzzy category (%ls)", (const wchar_t *)app->title, (const wchar_t *)it->item); break; } } } /* * When propagating a token match type to the “top-level” * application match type, we use the following rules: * * - all tokens must match: that is MATCHED_NONE -> MATCHED_NONE * - the application is an exact match *iff* there’s a * single token, *and* that token is an exact match. */ if (token_match_type == MATCHED_NONE) { app_match_type = MATCHED_NONE; break; } else { if (app_match_type != MATCHED_FUZZY) app_match_type = token_match_type; } } if (app_match_type == MATCHED_NONE) continue; *match = (struct match){ .matched_type = app_match_type, .application = app, .pos = pos, .pos_count = pos_count, .index = i, }; matches->match_count++; } free(copy); free(tokens); LOG_DBG("match update done"); /* Sort */ qsort(matches->matches, matches->match_count, sizeof(matches->matches[0]), &match_compar); matches->page_count = matches->max_matches_per_page ? ((matches->match_count + (matches->max_matches_per_page - 1)) / matches->max_matches_per_page) : 1; if (matches->selected >= matches->match_count && matches->selected > 0) matches->selected = matches->match_count - 1; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/match.h��������������������������������������������������������������������������������������0000664�0000000�0000000�00000003660�14454170012�0013645�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stdbool.h> #include <unistd.h> #include "application.h" #include "config.h" #include "prompt.h" enum matched_type { MATCHED_NONE, MATCHED_EXACT, MATCHED_FUZZY, }; struct match_substring { ssize_t start; size_t len; }; struct match { enum matched_type matched_type; struct application *application; struct match_substring *pos; size_t pos_count; size_t index; }; struct matches; struct matches *matches_init(enum match_fields fields, bool fuzzy, size_t fuzzy_min_length, size_t fuzzy_max_length_discrepancy, size_t fuzzy_max_distance); void matches_destroy(struct matches *matches); void matches_set_applications( struct matches *matches, const struct application_list *applications); bool matches_have_icons(const struct matches *matches); size_t matches_max_matches_per_page(const struct matches *matches); void matches_max_matches_per_page_set( struct matches *matches, size_t max_matches); void matches_update(struct matches *matches, const struct prompt *prompt); size_t matches_get_page_count(const struct matches *matches); size_t matches_get_page(const struct matches *matches); const struct match *matches_get(const struct matches *matches, size_t idx); const struct match *matches_get_match(const struct matches *matches); size_t matches_get_count(const struct matches *matches); /* Matches on current page */ size_t matches_get_total_count(const struct matches *matches); size_t matches_get_match_index(const struct matches *matches); bool matches_selected_first(struct matches *matches); bool matches_selected_last(struct matches *matches); bool matches_selected_prev(struct matches *matches, bool wrap); bool matches_selected_next(struct matches *matches, bool wrap); bool matches_selected_prev_page(struct matches *matches); bool matches_selected_next_page(struct matches *matches); ��������������������������������������������������������������������������������fuzzel/meson.build����������������������������������������������������������������������������������0000664�0000000�0000000�00000012530�14454170012�0014536�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������project('fuzzel', 'c', version: '1.9.2', license: 'MIT', meson_version: '>=0.58.0', default_options: [ 'c_std=c18', 'warning_level=1', 'werror=true', 'b_ndebug=if-release']) is_debug_build = get_option('buildtype').startswith('debug') add_project_arguments( ['-D_GNU_SOURCE'] + (is_debug_build ? ['-D_DEBUG'] : []), language: 'c', ) cc = meson.get_compiler('c') if cc.has_function('memfd_create') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif # Compute the relative path used by compiler invocations. source_root = meson.current_source_dir().split('/') build_root = meson.global_build_root().split('/') relative_dir_parts = [] i = 0 in_prefix = true foreach p : build_root if i >= source_root.length() or not in_prefix or p != source_root[i] in_prefix = false relative_dir_parts += '..' endif i += 1 endforeach i = 0 in_prefix = true foreach p : source_root if i >= build_root.length() or not in_prefix or build_root[i] != p in_prefix = false relative_dir_parts += p endif i += 1 endforeach relative_dir = join_paths(relative_dir_parts) + '/' if cc.has_argument('-fmacro-prefix-map=/foo=') add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c') endif math = cc.find_library('m') threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] fontconfig = dependency('fontconfig') pixman = dependency('pixman-1') cairo = dependency('cairo', required: get_option('enable-cairo')) libepoll = dependency('epoll-shim', required: false) wayland_protocols = dependency('wayland-protocols') wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') xkb = dependency('xkbcommon') tllist = dependency('tllist', version: '>=1.0.1', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.0.0', '<4.0.0'], fallback: 'fcft') if cairo.found() add_project_arguments('-DFUZZEL_ENABLE_CAIRO=1', language: 'c') endif if get_option('png-backend') == 'libpng' libpng = dependency('libpng') add_project_arguments('-DFUZZEL_ENABLE_PNG_LIBPNG=1', language: 'c') else libpng = declare_dependency() endif if cairo.found() and get_option('svg-backend') == 'librsvg' librsvg = dependency('librsvg-2.0') nanosvg = declare_dependency() svg_backend = 'librsvg' add_project_arguments('-DFUZZEL_ENABLE_SVG_LIBRSVG=1', language: 'c') elif get_option('svg-backend') == 'nanosvg' librsvg = declare_dependency() nanosvg = declare_dependency( sources: ['nanosvg.c', '3rd-party/nanosvg/src/nanosvg.h', 'nanosvgrast.c', '3rd-party/nanosvg/src/nanosvgrast.h'], include_directories: '3rd-party/nanosvg/src', dependencies: math) svg_backend = 'nanosvg' add_project_arguments('-DFUZZEL_ENABLE_SVG_NANOSVG=1', language: 'c') else svg_backend = 'none' librsvg = declare_dependency() nanosvg = declare_dependency() endif wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') wscanner = dependency('wayland-scanner', native: true) wscanner_prog = find_program( wscanner.get_variable('wayland_scanner'), native: true) wl_proto_headers = [] wl_proto_src = [] foreach prot : [ 'external/wlr-layer-shell-unstable-v1.xml', wayland_protocols_datadir + '/stable/xdg-shell/xdg-shell.xml', wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml', wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml'] wl_proto_headers += custom_target( prot.underscorify() + '-client-header', output: '@BASENAME@.h', input: prot, command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) wl_proto_src += custom_target( prot.underscorify() + '-private-code', output: '@BASENAME@.c', input: prot, command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) endforeach env = find_program('env', native: true) generate_version_sh = files('generate-version.sh') version = custom_target( 'generate_version', build_always_stale: true, output: 'version.h', command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) executable( 'fuzzel', 'application.c', 'application.h', 'config.c', 'config.h', 'char32.c', 'char32.h', 'dmenu.c', 'dmenu.h', 'fdm.c', 'fdm.h', 'icon.c', 'icon.h', 'key-binding.c', 'key-binding.h', 'log.c', 'log.h', 'main.c', 'match.c', 'match.h', 'png.c', 'png-fuzzel.h', 'prompt.c', 'prompt.h', 'render.c', 'render.h', 'shm.c', 'shm.h', 'stride.h', 'wayland.c', 'wayland.h', 'xdg.c', 'xdg.h', wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, pixman, fontconfig, cairo, libepoll, libpng, librsvg, nanosvg, wayland_client, wayland_cursor, xkb, tllist, fcft], install: true) install_data( 'fuzzel.ini', install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'fuzzel')) install_data( 'LICENSE', 'README.md', 'CHANGELOG.md', install_dir: join_paths(get_option('datadir'), 'doc', 'fuzzel')) subdir('completions') subdir('doc') summary( { 'Cairo': cairo.found(), 'PNG icons': get_option('png-backend'), 'SVG icons': svg_backend, }, bool_yn: true ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/meson_options.txt����������������������������������������������������������������������������0000664�0000000�0000000�00000000677�14454170012�0016042�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������option('enable-cairo', type: 'feature', description: 'use cairo; required for rounded corners and SVG icons (librsvg backend)') option('png-backend', type: 'combo', choices: ['none', 'libpng'], value: 'libpng', description: 'enable support for PNG icons') option('svg-backend', type: 'combo', choices: ['none', 'librsvg', 'nanosvg'], value: 'nanosvg', description: 'enables support for SVG icons (librsvg: also requires cairo, nanosvg: bundled)') �����������������������������������������������������������������fuzzel/nanosvg.c������������������������������������������������������������������������������������0000664�0000000�0000000�00000000220�14454170012�0014204�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include <stdio.h> #include <string.h> #include <math.h> #define NANOSVG_ALL_COLOR_KEYWORDS #define NANOSVG_IMPLEMENTATION #include <nanosvg.h> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/nanosvgrast.c��������������������������������������������������������������������������������0000664�0000000�0000000�00000000172�14454170012�0015104�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include <stdlib.h> #include <string.h> #include <nanosvg.h> #define NANOSVGRAST_IMPLEMENTATION #include <nanosvgrast.h> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/png-fuzzel.h���������������������������������������������������������������������������������0000664�0000000�0000000�00000000235�14454170012�0014645�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #if defined(FUZZEL_ENABLE_PNG_LIBPNG) #include <pixman.h> pixman_image_t *png_load(const char *path); #endif /* FUZZEL_ENABLE_PNG_LIBPNG */ �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/png.c����������������������������������������������������������������������������������������0000664�0000000�0000000�00000010574�14454170012�0013332�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#if defined(FUZZEL_ENABLE_PNG_LIBPNG) #include "png-fuzzel.h" #include <stdlib.h> #include <stdio.h> #include <stdbool.h> #include <string.h> #include <assert.h> #include <png.h> #include <pixman.h> #define LOG_MODULE "png" #define LOG_ENABLE_DBG 0 #include "log.h" #include "stride.h" static void png_warning_cb(png_structp png_ptr, png_const_charp warning_msg) { LOG_WARN("libpng: %s", warning_msg); } pixman_image_t * png_load(const char *path) { pixman_image_t *pix = NULL; FILE *fp = NULL; png_structp png_ptr = NULL; png_infop info_ptr = NULL; png_bytepp row_pointers = NULL; uint8_t *image_data = NULL; /* open file and test for it being a png */ if ((fp = fopen(path, "rb")) == NULL) { //LOG_ERRNO("%s: failed to open", path); goto err; } /* Verify PNG header */ uint8_t header[8] = {0}; if (fread(header, 1, 8, fp) != 8 || png_sig_cmp(header, 0, 8)) { // LOG_ERR("%s: not a PNG", path); goto err; } /* Prepare for reading the PNG */ if ((png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL || (info_ptr = png_create_info_struct(png_ptr)) == NULL) { LOG_ERR("%s: failed to initialize libpng", path); goto err; } if (setjmp(png_jmpbuf(png_ptr))) { LOG_ERR("%s: libpng error", path); goto err; } /* Set custom “warning” function */ png_set_error_fn( png_ptr, png_get_error_ptr(png_ptr), NULL, &png_warning_cb); png_init_io(png_ptr, fp); png_set_sig_bytes(png_ptr, 8); /* Get meta data */ png_read_info(png_ptr, info_ptr); int width = png_get_image_width(png_ptr, info_ptr); int height = png_get_image_height(png_ptr, info_ptr); png_byte color_type = png_get_color_type(png_ptr, info_ptr); png_byte bit_depth __attribute__((unused)) = png_get_bit_depth(png_ptr, info_ptr); int channels __attribute__((unused)) = png_get_channels(png_ptr, info_ptr); LOG_DBG("%s: %dx%d@%hhubpp, %d channels", path, width, height, bit_depth, channels); png_set_packing(png_ptr); png_set_interlace_handling(png_ptr); png_set_strip_16(png_ptr); /* "pack" 16-bit colors to 8-bit */ png_set_bgr(png_ptr); /* pixman expects pre-multiplied alpha */ png_set_alpha_mode(png_ptr, PNG_ALPHA_PREMULTIPLIED, 1.0); /* Tell libpng to expand to RGB(A) when necessary, and tell pixman * whether we have alpha or not */ pixman_format_code_t format; switch (color_type) { case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY_ALPHA: LOG_DBG("%d-bit gray%s", bit_depth, color_type == PNG_COLOR_TYPE_GRAY_ALPHA ? "+alpha" : ""); if (bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png_ptr); png_set_gray_to_rgb(png_ptr); format = color_type == PNG_COLOR_TYPE_GRAY ? PIXMAN_r8g8b8 : PIXMAN_a8r8g8b8; break; case PNG_COLOR_TYPE_PALETTE: LOG_DBG("%d-bit colormap%s", bit_depth, png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ? "+tRNS" : ""); png_set_palette_to_rgb(png_ptr); if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png_ptr); format = PIXMAN_a8r8g8b8; } else format = PIXMAN_r8g8b8; break; case PNG_COLOR_TYPE_RGB: LOG_DBG("RGB"); format = PIXMAN_r8g8b8; break; case PNG_COLOR_TYPE_RGBA: LOG_DBG("RGBA"); format = PIXMAN_a8r8g8b8; break; } png_read_update_info(png_ptr, info_ptr); size_t row_bytes __attribute__((unused)) = png_get_rowbytes(png_ptr, info_ptr); int stride = stride_for_format_and_width(format, width); image_data = malloc(height * stride); LOG_DBG("stride=%d, row-bytes=%zu", stride, row_bytes); assert(stride >= row_bytes); row_pointers = malloc(height * sizeof(png_bytep)); for (int i = 0; i < height; i++) row_pointers[i] = &image_data[i * stride]; png_read_image(png_ptr, row_pointers); pix = pixman_image_create_bits_no_clear( format, width, height, (uint32_t *)image_data, stride); err: if (pix == NULL) free(image_data); free(row_pointers); if (png_ptr != NULL) png_destroy_read_struct(&png_ptr, &info_ptr, NULL); if (fp != NULL) fclose(fp); return pix; } #endif /* FUZZEL_ENABLE_PNG_LIBPNG */ ������������������������������������������������������������������������������������������������������������������������������������fuzzel/prompt.c�������������������������������������������������������������������������������������0000664�0000000�0000000�00000012561�14454170012�0014065�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "prompt.h" #include <stdlib.h> #include <string.h> #include <wctype.h> #define LOG_MODULE "prompt" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" struct prompt { char32_t *prompt; char32_t *text; size_t cursor; }; struct prompt * prompt_init(const char32_t *prompt_text) { struct prompt *prompt = malloc(sizeof(*prompt)); *prompt = (struct prompt) { .prompt = c32dup(prompt_text), .text = calloc(1, sizeof(char32_t)), .cursor = 0, }; return prompt; } void prompt_destroy(struct prompt *prompt) { if (prompt == NULL) return; free(prompt->prompt); free(prompt->text); free(prompt); } bool prompt_insert_chars(struct prompt *prompt, const char *text, size_t len) { size_t wlen = mbsntoc32(NULL, text, len, 0); const size_t new_len = c32len(prompt->text) + wlen + 1; char32_t *new_text = realloc(prompt->text, new_len * sizeof(char32_t)); if (new_text == NULL) return false; memmove(&new_text[prompt->cursor + wlen], &new_text[prompt->cursor], (c32len(new_text) - prompt->cursor + 1) * sizeof(char32_t)); mbsntoc32(&new_text[prompt->cursor], text, len, wlen + 1); prompt->text = new_text; prompt->cursor += wlen; return true; } const char32_t * prompt_prompt(const struct prompt *prompt) { return prompt->prompt; } const char32_t * prompt_text(const struct prompt *prompt) { return prompt->text; } size_t prompt_cursor(const struct prompt *prompt) { return prompt->cursor; } static size_t idx_next_char(struct prompt *prompt) { if (prompt->text[prompt->cursor] == U'\0') return prompt->cursor; return prompt->cursor + 1; } static size_t idx_prev_char(const struct prompt *prompt) { if (prompt->cursor == 0) return 0; return prompt->cursor - 1; } static size_t idx_prev_word(const struct prompt *prompt) { size_t prev_char = idx_prev_char(prompt); const char32_t *space = &prompt->text[prev_char]; /* Ignore initial spaces */ while (space >= prompt->text && iswspace(*space)) space--; /* Skip non-spaces */ while (space >= prompt->text && !iswspace(*space)) space--; return space - prompt->text + 1; } static size_t idx_next_word(const struct prompt *prompt) { const char32_t *end = prompt->text + c32len(prompt->text); const char32_t *space = &prompt->text[prompt->cursor]; /* Ignore initial non-spaces */ while (space < end && !iswspace(*space)) space++; /* Skip spaces */ while (space < end && iswspace(*space)) space++; return space - prompt->text; } bool prompt_cursor_home(struct prompt *prompt) { if (prompt->cursor == 0) return false; prompt->cursor = 0; return true; } bool prompt_cursor_end(struct prompt *prompt) { size_t text_len = c32len(prompt->text); if (prompt->cursor >= text_len) return false; prompt->cursor = text_len; return true; } bool prompt_cursor_next_char(struct prompt *prompt) { size_t idx = idx_next_char(prompt); if (idx == prompt->cursor) return false; prompt->cursor = idx; return true; } bool prompt_cursor_prev_char(struct prompt *prompt) { size_t idx = idx_prev_char(prompt); if (idx == prompt->cursor) return false; prompt->cursor = idx; return true; } bool prompt_cursor_prev_word(struct prompt *prompt) { size_t idx = idx_prev_word(prompt); if (idx == prompt->cursor) return false; prompt->cursor = idx; return true; } bool prompt_cursor_next_word(struct prompt *prompt) { size_t idx = idx_next_word(prompt); if (idx == prompt->cursor) return false; prompt->cursor = idx; return true; } bool prompt_erase_all(struct prompt *prompt) { free(prompt->text); prompt->text = calloc(1, sizeof(char32_t)); prompt->cursor = 0; return true; } bool prompt_erase_next_char(struct prompt *prompt) { if (prompt->cursor >= c32len(prompt->text)) return false; size_t next_char = idx_next_char(prompt); memmove(&prompt->text[prompt->cursor], &prompt->text[next_char], (c32len(prompt->text) - next_char + 1) * sizeof(char32_t)); return true; } bool prompt_erase_prev_char(struct prompt *prompt) { if (prompt->cursor == 0) return false; size_t prev_char = idx_prev_char(prompt); memmove(&prompt->text[prev_char], &prompt->text[prompt->cursor], (c32len(prompt->text) - prompt->cursor + 1) * sizeof(char32_t)); prompt->cursor = prev_char; return true; } bool prompt_erase_next_word(struct prompt *prompt) { size_t next_word = idx_next_word(prompt); if (next_word == prompt->cursor) return false; memmove(&prompt->text[prompt->cursor], &prompt->text[next_word], (c32len(prompt->text) - next_word + 1) * sizeof(char32_t)); return true; } bool prompt_erase_prev_word(struct prompt *prompt) { size_t new_cursor = idx_prev_word(prompt); if (new_cursor == prompt->cursor) return false; memmove(&prompt->text[new_cursor], &prompt->text[prompt->cursor], (c32len(prompt->text) - prompt->cursor + 1) * sizeof(char32_t)); prompt->cursor = new_cursor; return true; } bool prompt_erase_after_cursor(struct prompt *prompt) { prompt->text[prompt->cursor] = U'\0'; return true; } �����������������������������������������������������������������������������������������������������������������������������������������������fuzzel/prompt.h�������������������������������������������������������������������������������������0000664�0000000�0000000�00000002043�14454170012�0014064�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stddef.h> #include <stdbool.h> #include <uchar.h> struct prompt; struct prompt *prompt_init(const char32_t *prompt_text); void prompt_destroy(struct prompt *prompt); bool prompt_insert_chars(struct prompt *prompt, const char *text, size_t len); const char32_t *prompt_prompt(const struct prompt *prompt); const char32_t *prompt_text(const struct prompt *prompt); size_t prompt_cursor(const struct prompt *prompt); bool prompt_cursor_home(struct prompt *prompt); bool prompt_cursor_end(struct prompt *prompt); bool prompt_cursor_next_char(struct prompt *prompt); bool prompt_cursor_prev_char(struct prompt *prompt); bool prompt_cursor_prev_word(struct prompt *prompt); bool prompt_cursor_next_word(struct prompt *prompt); bool prompt_erase_all(struct prompt *prompt); bool prompt_erase_next_char(struct prompt *prompt); bool prompt_erase_prev_char(struct prompt *prompt); bool prompt_erase_next_word(struct prompt *prompt); bool prompt_erase_prev_word(struct prompt *prompt); bool prompt_erase_after_cursor(struct prompt *prompt); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/render.c�������������������������������������������������������������������������������������0000664�0000000�0000000�00000065322�14454170012�0014026�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "render.h" #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcft/fcft.h> #if defined(FUZZEL_ENABLE_SVG_NANOSVG) #include <nanosvgrast.h> #endif #define LOG_MODULE "render" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "icon.h" #include "stride.h" #include "wayland.h" #define min(x, y) ((x) < (y) ? (x) : (y)) #define max(x, y) ((x) > (y) ? (x) : (y)) struct render { const struct config *conf; struct fcft_font *font; enum fcft_subpixel subpixel; int scale; float dpi; bool size_font_by_dpi; pixman_color_t pix_background_color; pixman_color_t pix_border_color; pixman_color_t pix_text_color; pixman_color_t pix_match_color; pixman_color_t pix_selection_color; pixman_color_t pix_selection_text_color; pixman_color_t pix_selection_match_color; unsigned x_margin; unsigned y_margin; unsigned inner_pad; unsigned border_size; unsigned row_height; unsigned icon_height; mtx_t *icon_lock; }; static pixman_color_t rgba2pixman(struct rgba rgba) { uint16_t r = rgba.r * 65535.0; uint16_t g = rgba.g * 65535.0; uint16_t b = rgba.b * 65535.0; uint16_t a = rgba.a * 65535.0; return (pixman_color_t){ .red = (uint32_t)r * a / 0xffff, .green = (uint32_t)g * a / 0xffff, .blue = (uint32_t)b * a / 0xffff, .alpha = a, }; } static int pt_or_px_as_pixels(const struct render *render, const struct pt_or_px *pt_or_px) { double scale = !render->size_font_by_dpi ? render->scale : 1.; double dpi = render->size_font_by_dpi ? render->dpi : 96.; return pt_or_px->px == 0 ? round(pt_or_px->pt * scale * dpi / 72.) : pt_or_px->px; } void render_background(const struct render *render, struct buffer *buf) { bool use_pixman = #if defined(FUZZEL_ENABLE_CAIRO) render->conf->border.radius == 0 #else true #endif ; if (use_pixman) { unsigned bw = render->conf->border.size; pixman_color_t bg = rgba2pixman(render->conf->colors.background); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &bg, 1, &(pixman_rectangle16_t){ bw, bw, buf->width - 2 * bw, buf->height - 2 * bw}); pixman_color_t border_color = rgba2pixman(render->conf->colors.border); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &border_color, 4, (pixman_rectangle16_t[]){ {0, 0, buf->width, bw}, /* top */ {0, bw, bw, buf->height - 2 * bw}, /* left */ {buf->width - bw, bw, bw, buf->height - 2 * bw}, /* right */ {0, buf->height - bw, buf->width, bw} /* bottom */ }); } else { #if defined(FUZZEL_ENABLE_CAIRO) /* Erase */ cairo_set_operator(buf->cairo, CAIRO_OPERATOR_CLEAR); cairo_rectangle(buf->cairo, 0, 0, buf->width, buf->height); cairo_fill(buf->cairo); /* * Lines in cairo are *between* pixels. * * To get a sharp 1px line, we need to draw it with * line-width=2. * * Thus, we need to draw the path offset:ed with half that * (=actual border width). */ const double b = render->border_size; const double w = max(buf->width - 2 * b, 0.); const double h = max(buf->height - 2 * b, 0.); const double from_degree = M_PI / 180; const double radius = render->conf->border.radius * render->scale; /* Path describing an arc:ed rectangle */ cairo_new_path(buf->cairo); cairo_arc(buf->cairo, b + w - radius, b + h - radius, radius, 0.0 * from_degree, 90.0 * from_degree); cairo_arc(buf->cairo, b + radius, b + h - radius, radius, 90.0 * from_degree, 180.0 * from_degree); cairo_arc(buf->cairo, b + radius, b + radius, radius, 180.0 * from_degree, 270.0 * from_degree); cairo_arc(buf->cairo, b + w - radius, b + radius, radius, 270.0 * from_degree, 360.0 * from_degree); cairo_close_path(buf->cairo); /* Border */ const struct rgba *bc = &render->conf->colors.border; cairo_set_operator(buf->cairo, CAIRO_OPERATOR_SOURCE); cairo_set_line_width(buf->cairo, 2 * b); cairo_set_source_rgba(buf->cairo, bc->r, bc->g, bc->b, bc->a); cairo_stroke_preserve(buf->cairo); /* Background */ const struct rgba *bg = &render->conf->colors.background; cairo_set_source_rgba(buf->cairo, bg->r, bg->g, bg->b, bg->a); cairo_fill(buf->cairo); #else assert(false); #endif } } static void render_glyph(pixman_image_t *pix, const struct fcft_glyph *glyph, int x, int y, const pixman_color_t *color) { if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { /* Glyph surface is a pre-rendered image (typically a color emoji...) */ pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, x + glyph->x, y - glyph->y, glyph->width, glyph->height); } else { /* Glyph surface is an alpha mask */ pixman_image_t *src = pixman_image_create_solid_fill(color); pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, pix, 0, 0, 0, 0, x + glyph->x, y - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } } void render_prompt(const struct render *render, struct buffer *buf, const struct prompt *prompt) { struct fcft_font *font = render->font; assert(font != NULL); const struct config *conf = render->conf; const char32_t *pprompt = prompt_prompt(prompt); size_t prompt_len = c32len(pprompt); const char32_t *ptext = prompt_text(prompt); const size_t text_len = c32len(ptext); const enum fcft_subpixel subpixel = (render->conf->colors.background.a == 1. && render->conf->colors.selection.a == 1.) ? render->subpixel : FCFT_SUBPIXEL_NONE; int x = render->border_size + render->x_margin; int y = render->border_size + render->y_margin + (render->row_height + font->height) / 2 - font->descent; if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { struct fcft_text_run *run = fcft_rasterize_text_run_utf32( font, prompt_len, pprompt, subpixel); if (run != NULL) { for (size_t i = 0; i < run->count; i++) { const struct fcft_glyph *glyph = run->glyphs[i]; render_glyph(buf->pix, glyph, x, y, &render->pix_text_color); x += glyph->advance.x; } fcft_text_run_destroy(run); /* Cursor, if right after the prompt. In all other cases, * the cursor will be rendered by the loop below */ if (prompt_cursor(prompt) == 0) { pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &render->pix_text_color, 1, &(pixman_rectangle16_t){ x, y - font->ascent, font->underline.thickness, font->ascent + font->descent}); } /* Prevent loop below from rendering the prompt */; prompt_len = 0; } } char32_t prev = 0; for (size_t i = 0; i < prompt_len + text_len; i++) { char32_t wc = i < prompt_len ? pprompt[i] : (conf->password_mode.enabled ? conf->password_mode.character : ptext[i - prompt_len]); const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( font, wc, subpixel); if (glyph == NULL) { prev = wc; continue; } long x_kern; fcft_kerning(font, prev, wc, &x_kern, NULL); x += x_kern; render_glyph(buf->pix, glyph, x, y, &render->pix_text_color); x += glyph->advance.x; if (i >= prompt_len) x += pt_or_px_as_pixels(render, &render->conf->letter_spacing); /* Cursor */ if (prompt_cursor(prompt) + prompt_len - 1 == i) { pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &render->pix_text_color, 1, &(pixman_rectangle16_t){ x, y - font->ascent, font->underline.thickness, font->ascent + font->descent}); } prev = wc; } } static void render_match_text(struct buffer *buf, double *_x, double _y, double max_x, const char32_t *text, size_t match_count, const struct match_substring matches[static match_count], struct fcft_font *font, enum fcft_subpixel subpixel, int letter_spacing, int tabs, pixman_color_t regular_color, pixman_color_t match_color, struct fcft_text_run **run) { int x = *_x; int y = _y; const struct fcft_glyph **glyphs = NULL; int *clusters = NULL; long *kern = NULL; size_t count = 0; if (*run == NULL && (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING)) { *run = fcft_rasterize_text_run_utf32( font, c32len(text), text, subpixel); } if (*run != NULL) { glyphs = (*run)->glyphs; clusters = (*run)->cluster; count = (*run)->count; } else { count = c32len(text); glyphs = malloc(count * sizeof(glyphs[0])); clusters = malloc(count * sizeof(clusters[0])); kern = malloc(count * sizeof(kern[0])); for (size_t i = 0; i < count; i++) { const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( font, text[i], subpixel); if (glyph == NULL) { glyphs[i] = NULL; continue; } if (i > 0) fcft_kerning(font, text[i - 1], text[i], &kern[i], NULL); else kern[i] = 0; glyphs[i] = glyph; clusters[i] = i; } } for (size_t i = 0; i < count; i++) { if (text[clusters[i]] == U'\t') { const struct fcft_glyph *space = fcft_rasterize_char_utf32(font, U' ', subpixel); if (space != NULL) { const size_t chars_to_next_tab_stop = tabs - (clusters[i] % tabs); x += chars_to_next_tab_stop * space->advance.x; } continue; } bool is_match = false; for (size_t j = 0; j < match_count; j++) { const struct match_substring *match = &matches[j]; assert(match->start >= 0); if (clusters[i] >= match->start && clusters[i] < match->start + match->len) { is_match = true; break; } } if (x + (kern != NULL ? kern[i] : 0) + glyphs[i]->advance.x >= max_x) { const struct fcft_glyph *ellipses = fcft_rasterize_char_utf32(font, U'…', subpixel); if (ellipses != NULL) render_glyph(buf->pix, ellipses, x, y, ®ular_color); break; } x += kern != NULL ? kern[i] : 0; render_glyph(buf->pix, glyphs[i], x, y, is_match ? &match_color : ®ular_color); x += glyphs[i]->advance.x + letter_spacing; y += glyphs[i]->advance.y; } if (*run == NULL) { free(kern); free(clusters); free(glyphs); } *_x = x; } #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) static void render_svg_librsvg(const struct icon *icon, int x, int y, int size, struct buffer *buf) { RsvgHandle *svg = icon->svg; cairo_save(buf->cairo); cairo_set_operator(buf->cairo, CAIRO_OPERATOR_ATOP); #if LIBRSVG_CHECK_VERSION(2, 46, 0) if (cairo_status(buf->cairo) == CAIRO_STATUS_SUCCESS) { const RsvgRectangle viewport = { .x = x, .y = y, .width = size, .height = size, }; cairo_rectangle(buf->cairo, x, y, size, size); cairo_clip(buf->cairo); rsvg_handle_render_document(svg, buf->cairo, &viewport, NULL); } #else RsvgDimensionData dim; rsvg_handle_get_dimensions(svg, &dim); const double scale_x = size / dim.width; const double scale_y = size / dim.height; const double scale = scale_x < scale_y ? scale_x : scale_y; const double height = dim.height * scale; const double width = dim.width * scale; cairo_rectangle(buf->cairo, x, y, height, width); cairo_clip(buf->cairo); /* Translate + scale. Note: order matters! */ cairo_translate(buf->cairo, x, y); cairo_scale(buf->cairo, scale, scale); if (cairo_status(buf->cairo) == CAIRO_STATUS_SUCCESS) rsvg_handle_render_cairo(svg, buf->cairo); #endif cairo_restore(buf->cairo); } #endif /* FUZZEL_ENABLE_SVG_LIBRSVG */ #if defined(FUZZEL_ENABLE_SVG_NANOSVG) static void render_svg_nanosvg(struct icon *icon, int x, int y, int size, struct buffer *buf) { #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_flush(buf->cairo_surface); #endif pixman_image_t *img = NULL; /* Look for a cached image, at the correct size */ tll_foreach(icon->rasterized, it) { if (it->item.size == size) { img = it->item.pix; break; } } if (img == NULL) { NSVGimage *svg = icon->svg; struct NSVGrasterizer *rast = nsvgCreateRasterizer(); if (rast == NULL) return; float scale = svg->width > svg->height ? size / svg->width : size / svg->height; uint8_t *data = malloc(size * size * 4); nsvgRasterize(rast, svg, 0, 0, scale, data, size, size, size * 4); img = pixman_image_create_bits_no_clear( PIXMAN_a8b8g8r8, size, size, (uint32_t *)data, size * 4); /* Nanosvg produces non-premultiplied ABGR, while pixman expects * premultiplied */ for (uint32_t *abgr = (uint32_t *)data; abgr < (uint32_t *)(data + size * size * 4); abgr++) { uint8_t alpha = (*abgr >> 24) & 0xff; uint8_t blue = (*abgr >> 16) & 0xff; uint8_t green = (*abgr >> 8) & 0xff; uint8_t red = (*abgr >> 0) & 0xff; if (alpha == 0xff) continue; if (alpha == 0x00) blue = green = red = 0x00; else { blue = blue * alpha / 0xff; green = green * alpha / 0xff; red = red * alpha / 0xff; } *abgr = (uint32_t)alpha << 24 | blue << 16 | green << 8 | red; } nsvgDeleteRasterizer(rast); tll_push_back(icon->rasterized, ((struct rasterized){img, size})); } pixman_image_composite32( PIXMAN_OP_OVER, img, NULL, buf->pix, 0, 0, 0, 0, x, y, size, size); #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_mark_dirty(buf->cairo_surface); #endif } #endif /* FUZZEL_ENABLE_SVG_NANOSVG */ static void render_svg(struct icon *icon, int x, int y, int size, struct buffer *buf) { assert(icon->type == ICON_SVG); if (icon->svg == NULL) { if (!icon_from_svg(icon, icon->path)) return; LOG_DBG("%s", icon->path); } #if defined(FUZZEL_ENABLE_SVG_LIBRSVG) render_svg_librsvg(icon, x, y, size, buf); #elif defined(FUZZEL_ENABLE_SVG_NANOSVG) render_svg_nanosvg(icon, x, y, size, buf); #endif } #if defined(FUZZEL_ENABLE_PNG_LIBPNG) static void render_png_libpng(struct icon *icon, int x, int y, int size, struct buffer *buf) { #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_flush(buf->cairo_surface); #endif pixman_image_t *png = icon->png; pixman_format_code_t fmt = pixman_image_get_format(png); int height = pixman_image_get_height(png); int width = pixman_image_get_width(png); if (height > size) { double scale = (double)size / height; pixman_f_transform_t _scale_transform; pixman_f_transform_init_scale(&_scale_transform, 1. / scale, 1. / scale); pixman_transform_t scale_transform; pixman_transform_from_pixman_f_transform( &scale_transform, &_scale_transform); pixman_image_set_transform(png, &scale_transform); int param_count = 0; pixman_kernel_t kernel = PIXMAN_KERNEL_LANCZOS3; pixman_fixed_t *params = pixman_filter_create_separable_convolution( ¶m_count, pixman_double_to_fixed(1. / scale), pixman_double_to_fixed(1. / scale), kernel, kernel, kernel, kernel, pixman_int_to_fixed(1), pixman_int_to_fixed(1)); if (params != NULL || param_count == 0) { pixman_image_set_filter( png, PIXMAN_FILTER_SEPARABLE_CONVOLUTION, params, param_count); } free(params); width *= scale; height *= scale; int stride = stride_for_format_and_width(fmt, width); uint8_t *data = malloc(height * stride); pixman_image_t *scaled_png = pixman_image_create_bits_no_clear( fmt, width, height, (uint32_t *)data, stride); pixman_image_composite32( PIXMAN_OP_SRC, png, NULL, scaled_png, 0, 0, 0, 0, 0, 0, width, height); free(pixman_image_get_data(png)); pixman_image_unref(png); png = scaled_png; icon->png = png; } pixman_image_composite32( PIXMAN_OP_OVER, png, NULL, buf->pix, 0, 0, 0, 0, x, y, width, height); #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_mark_dirty(buf->cairo_surface); #endif } #endif /* FUZZEL_ENABLE_PNG_LIBPNG */ static void render_png(struct icon *icon, int x, int y, int size, struct buffer *buf) { assert(icon->type == ICON_PNG); if (icon->png == NULL) { if (!icon_from_png(icon, icon->path)) return; LOG_DBG("%s", icon->path); } #if defined(FUZZEL_ENABLE_PNG_LIBPNG) render_png_libpng(icon, x, y, size, buf); #endif } void render_match_list(const struct render *render, struct buffer *buf, const struct prompt *prompt, const struct matches *matches) { struct fcft_font *font = render->font; assert(font != NULL); const int x_margin = render->x_margin; const int y_margin = render->y_margin; const int inner_pad = render->inner_pad; const int border_size = render->border_size; const size_t match_count = matches_get_count(matches); const size_t selected = matches_get_match_index(matches); const enum fcft_subpixel subpixel = (render->conf->colors.background.a == 1. && render->conf->colors.selection.a == 1.) ? render->subpixel : FCFT_SUBPIXEL_NONE; const struct fcft_glyph *ellipses = fcft_rasterize_char_utf32(font, U'…', subpixel); assert(match_count == 0 || selected < match_count); const int row_height = render->row_height; const int first_row = 1 * border_size + y_margin + row_height + inner_pad; const int sel_margin = x_margin / 3; int y = first_row + (row_height + font->height) / 2 - font->descent; bool render_icons = mtx_trylock(render->icon_lock) == thrd_success; for (size_t i = 0; i < match_count; i++) { const struct match *match = matches_get(matches, i); if (i == selected) { /* If currently selected item has a scalable icon, and if * there's "enough" free space, render a large * representation of the icon */ const double ratio = render->conf->image_size_ratio; const double size = min(buf->height * ratio, buf->width * ratio); const double img_x = (buf->width - size) / 2.; const double img_y_bottom = max(buf->height - first_row, 0.); const double img_y = max(img_y_bottom - size, 0.); const double list_end = first_row + match_count * row_height; LOG_DBG("img_y=%f, list_end=%f", img_y, list_end); if (render_icons && match->application->icon.type == ICON_SVG && img_y > list_end + row_height) { render_svg(&match->application->icon, img_x, img_y, size, buf); } pixman_color_t sc = rgba2pixman(render->conf->colors.selection); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &sc, 1, &(pixman_rectangle16_t){ x_margin - sel_margin, first_row + i * row_height, buf->width - 2 * (x_margin - sel_margin), row_height} ); } double cur_x = border_size + x_margin; double max_x = buf->width - border_size - x_margin; #if 0 /* Render the icon+text bounding box */ pixman_color_t sc = rgba2pixman(render->conf->colors.match); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &sc, 1, &(pixman_rectangle16_t){ cur_x, first_row + i * row_height, max_x - cur_x, row_height} ); #endif if (render_icons) { struct icon *icon = &match->application->icon; const int size = render->icon_height; const int img_x = cur_x; const int img_y = first_row + i * row_height + (row_height - size) / 2; switch (icon->type) { case ICON_NONE: break; case ICON_PNG: render_png(icon, img_x, img_y, size, buf); break; case ICON_SVG: render_svg(icon, img_x, img_y, size, buf); break; } } const struct fcft_glyph *space = fcft_rasterize_char_utf32( font, U' ', subpixel); cur_x += (render->conf->icons_enabled && matches_have_icons(matches) ? (row_height + (space != NULL ? space->advance.x : font->max_advance.x)) : 0) + pt_or_px_as_pixels(render, &render->conf->letter_spacing); /* Replace newlines in title, with spaces (basic support for * multiline entries) */ if (match->application->render_title == NULL) { char32_t *newline = c32chr(match->application->title, U'\n'); if (newline != NULL) { char32_t *render_title = c32dup(match->application->title); newline = render_title + (newline - match->application->title); *newline = U' '; while ((newline = c32chr(newline, U'\n')) != NULL) *newline = U' '; match->application->render_title = render_title; } else { /* No newlines, use title as-is */ match->application->render_title = match->application->title; } } /* Application title */ render_match_text( buf, &cur_x, y, max_x - (ellipses != NULL ? ellipses->width : 0), match->application->render_title, match->pos_count, match->pos, font, subpixel, pt_or_px_as_pixels(render, &render->conf->letter_spacing), render->conf->tabs, (i == selected ? render->pix_selection_text_color : render->pix_text_color), (i == selected ? render->pix_selection_match_color : render->pix_match_color), &match->application->shaped); y += row_height; } if (render_icons) mtx_unlock(render->icon_lock); } struct render * render_init(const struct config *conf, mtx_t *icon_lock) { struct render *render = calloc(1, sizeof(*render)); *render = (struct render){ .conf = conf, .icon_lock = icon_lock, }; render->pix_background_color = rgba2pixman(render->conf->colors.background); render->pix_border_color = rgba2pixman(render->conf->colors.border); render->pix_text_color = rgba2pixman(render->conf->colors.text); render->pix_match_color = rgba2pixman(render->conf->colors.match); render->pix_selection_color = rgba2pixman(render->conf->colors.selection); render->pix_selection_text_color = rgba2pixman(render->conf->colors.selection_text); render->pix_selection_match_color = rgba2pixman(render->conf->colors.selection_match); return render; } void render_set_subpixel(struct render *render, enum fcft_subpixel subpixel) { render->subpixel = subpixel; } bool render_set_font(struct render *render, struct fcft_font *font, int scale, float dpi, bool size_font_by_dpi, int *new_width, int *new_height) { if (font != NULL) { fcft_destroy(render->font); render->font = font; } else { assert(render->font != NULL); font = render->font; } render->scale = scale; render->dpi = dpi; render->size_font_by_dpi = size_font_by_dpi; const struct fcft_glyph *W = fcft_rasterize_char_utf32( font, U'W', render->subpixel); const unsigned x_margin = render->conf->pad.x * scale; const unsigned y_margin = render->conf->pad.y * scale; const unsigned inner_pad = render->conf->pad.inner * scale; const unsigned border_size = render->conf->border.size * scale; const unsigned row_height = render->conf->line_height.px >= 0 ? pt_or_px_as_pixels(render, &render->conf->line_height) : font->height; const unsigned icon_height = max(0, row_height - font->descent); const unsigned height = border_size + /* Top border */ y_margin + row_height + /* The prompt */ inner_pad + /* Padding between prompt and matches */ render->conf->lines * row_height + /* Matches */ y_margin + border_size; /* Bottom border */ const unsigned width = border_size + x_margin + (max((W->advance.x + pt_or_px_as_pixels( render, &render->conf->letter_spacing)), 0) * render->conf->chars) + x_margin + border_size; LOG_DBG("x-margin: %d, y-margin: %d, border: %d, row-height: %d, " "icon-height: %d, height: %d, width: %d, scale: %d", x_margin, y_margin, border_size, row_height, icon_height, height, width, scale); render->y_margin = y_margin; render->x_margin = x_margin; render->inner_pad = inner_pad; render->border_size = border_size; render->row_height = row_height; render->icon_height = icon_height; if (new_width != NULL) *new_width = width; if (new_height != NULL) *new_height = height; return true; } int render_icon_size(const struct render *render) { return render->icon_height; } void render_destroy(struct render *render) { if (render == NULL) return; fcft_destroy(render->font); free(render); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/render.h�������������������������������������������������������������������������������������0000664�0000000�0000000�00000001646�14454170012�0014032�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <threads.h> #include <pixman.h> #include <fcft/fcft.h> #include "config.h" #include "match.h" #include "shm.h" #include "tllist.h" struct render; struct render *render_init(const struct config *conf, mtx_t *icon_lock); void render_destroy(struct render *render); void render_set_subpixel(struct render *render, enum fcft_subpixel subpixel); bool render_set_font(struct render *render, struct fcft_font *font, int scale, float dpi, bool size_font_by_dpi, int *new_width, int *new_height); void render_background(const struct render *render, struct buffer *buf); void render_prompt( const struct render *render, struct buffer *buf, const struct prompt *prompt); void render_match_list( const struct render *render, struct buffer *buf, const struct prompt *prompt, const struct matches *matches); int render_icon_size(const struct render *render); ������������������������������������������������������������������������������������������fuzzel/shm.c����������������������������������������������������������������������������������������0000664�0000000�0000000�00000012312�14454170012�0013325�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "shm.h" #include <unistd.h> #include <assert.h> #include <sys/types.h> #include <sys/mman.h> #include <fcntl.h> #include <tllist.h> #define LOG_MODULE "shm" #include "log.h" #include "stride.h" static tll(struct buffer) buffers; static void buffer_release(void *data, struct wl_buffer *wl_buffer) { //printf("buffer release\n"); struct buffer *buffer = data; assert(buffer->wl_buf == wl_buffer); assert(buffer->busy); buffer->busy = false; } static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; struct buffer * shm_get_buffer(struct wl_shm *shm, int width, int height) { tll_foreach(buffers, it) { if (!it->item.busy && it->item.width == width && it->item.height == height) { it->item.busy = true; return &it->item; } } /* * No existing buffer available. Create a new one by: * * 1. open a memory backed "file" with memfd_create() * 2. mmap() the memory file, to be used by the cairo surface * 3. create a wayland shm buffer for the same memory file * * The cairo surface and the wayland buffer are now sharing * memory. */ int pool_fd = -1; void *mmapped = NULL; size_t size = 0; struct wl_shm_pool *pool = NULL; struct wl_buffer *buf = NULL; pixman_image_t *pix = NULL; #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_t *cairo_surface = NULL; cairo_t *cairo = NULL; #endif /* Backing memory for SHM */ #if defined(MEMFD_CREATE) pool_fd = memfd_create("fuzzel-wayland-shm-buffer-pool", MFD_CLOEXEC); #elif defined(__FreeBSD__) // memfd_create on FreeBSD 13 is SHM_ANON without sealing support pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); #else char name[] = "/tmp/fuzzel-wayland-shm-buffer-pool-XXXXXX"; pool_fd = mkostemp(name, O_CLOEXEC); unlink(name); #endif if (pool_fd == -1) { LOG_ERRNO("failed to create SHM backing memory file"); goto err; } /* Total size */ const uint32_t stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); size = stride * height; if (ftruncate(pool_fd, size) == -1) { LOG_ERRNO("failed to truncate SHM pool"); goto err; } mmapped = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, pool_fd, 0); if (mmapped == MAP_FAILED) { LOG_ERR("failed to mmap SHM backing memory file"); goto err; } pool = wl_shm_create_pool(shm, pool_fd, size); if (pool == NULL) { LOG_ERR("failed to create SHM pool"); goto err; } buf = wl_shm_pool_create_buffer( pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); if (buf == NULL) { LOG_ERR("failed to create SHM buffer"); goto err; } /* We use the entire pool for our single buffer */ wl_shm_pool_destroy(pool); pool = NULL; close(pool_fd); pool_fd = -1; pix = pixman_image_create_bits_no_clear( PIXMAN_a8r8g8b8, width, height, mmapped, stride); if (pix == NULL) { LOG_ERR("failed to create pixman image"); goto err; } #if defined(FUZZEL_ENABLE_CAIRO) /* Create a cairo surface around the mmapped memory */ cairo_surface = cairo_image_surface_create_for_data( mmapped, CAIRO_FORMAT_ARGB32, width, height, stride); if (cairo_surface_status(cairo_surface) != CAIRO_STATUS_SUCCESS) { LOG_ERR("failed to create cairo surface: %s", cairo_status_to_string(cairo_surface_status(cairo_surface))); goto err; } cairo = cairo_create(cairo_surface); if (cairo_status(cairo) != CAIRO_STATUS_SUCCESS) { LOG_ERR("failed to create cairo context: %s", cairo_status_to_string(cairo_status(cairo))); goto err; } #endif /* Push to list of available buffers, but marked as 'busy' */ tll_push_back( buffers, ((struct buffer){ .width = width, .height = height, .stride = stride, .busy = true, .size = size, .mmapped = mmapped, .wl_buf = buf, .pix = pix, #if defined(FUZZEL_ENABLE_CAIRO) .cairo_surface = cairo_surface, .cairo = cairo #endif }) ); struct buffer *ret = &tll_back(buffers); wl_buffer_add_listener(ret->wl_buf, &buffer_listener, ret); return ret; err: #if defined(FUZZEL_ENABLE_CAIRO) if (cairo != NULL) cairo_destroy(cairo); if (cairo_surface != NULL) cairo_surface_destroy(cairo_surface); #endif if (pix != NULL) pixman_image_unref(pix); if (buf != NULL) wl_buffer_destroy(buf); if (pool != NULL) wl_shm_pool_destroy(pool); if (pool_fd != -1) close(pool_fd); if (mmapped != NULL) munmap(mmapped, size); return NULL; } void shm_fini(void) { tll_foreach(buffers, it) { struct buffer *buf = &it->item; #if defined(FUZZEL_ENABLE_CAIRO) cairo_destroy(buf->cairo); cairo_surface_destroy(buf->cairo_surface); #endif pixman_image_unref(buf->pix); wl_buffer_destroy(buf->wl_buf); munmap(buf->mmapped, buf->size); tll_remove(buffers, it); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/shm.h����������������������������������������������������������������������������������������0000664�0000000�0000000�00000001025�14454170012�0013331�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stdbool.h> #include <stddef.h> #include <pixman.h> #if defined(FUZZEL_ENABLE_CAIRO) #include <cairo.h> #endif #include <wayland-client.h> struct buffer { int width; int height; int stride; bool busy; size_t size; void *mmapped; struct wl_buffer *wl_buf; pixman_image_t *pix; #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_t *cairo_surface; cairo_t *cairo; #endif }; struct buffer *shm_get_buffer(struct wl_shm *shm, int width, int height); void shm_fini(void); �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/stride.h�������������������������������������������������������������������������������������0000664�0000000�0000000�00000000306�14454170012�0014035�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <pixman.h> static inline int stride_for_format_and_width(pixman_format_code_t format, int width) { return (((PIXMAN_FORMAT_BPP(format) * width + 7) / 8 + 4 - 1) & -4); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/subprojects/���������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14454170012�0014736�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/subprojects/fcft.wrap������������������������������������������������������������������������0000664�0000000�0000000�00000000106�14454170012�0016550�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[wrap-git] url = https://codeberg.org/dnkl/fcft.git revision = master ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/subprojects/tllist.wrap����������������������������������������������������������������������0000664�0000000�0000000�00000000110�14454170012�0017134�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[wrap-git] url = https://codeberg.org/dnkl/tllist.git revision = master ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/wayland.c������������������������������������������������������������������������������������0000664�0000000�0000000�00000167076�14454170012�0014217�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "wayland.h" #include <stdlib.h> #include <string.h> #include <ctype.h> #include <wctype.h> #include <unistd.h> #include <errno.h> #include <locale.h> #include <sys/mman.h> #include <sys/epoll.h> #include <sys/timerfd.h> #include <wayland-client.h> #include <wayland-cursor.h> #include <wayland-util.h> #include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon-keysyms.h> #include <xkbcommon/xkbcommon-compose.h> #include <fontconfig/fontconfig.h> #include <xdg-output-unstable-v1.h> #include <wlr-layer-shell-unstable-v1.h> #include <xdg-activation-v1.h> #include <tllist.h> #define LOG_MODULE "wayland" #define LOG_ENABLE_DBG 0 #include "log.h" #include "dmenu.h" #include "icon.h" #include "key-binding.h" #include "prompt.h" #include "render.h" #include "shm.h" struct font_spec { char *pattern; double pt_size; int px_size; }; struct monitor { struct wayland *wayl; struct wl_output *output; struct zxdg_output_v1 *xdg; uint32_t wl_name; int x; int y; struct { /* Physical size, in mm */ struct { int width; int height; } mm; /* Physical size, in pixels */ struct { int width; int height; } px_real; /* Scaled size, in pixels */ struct { int width; int height; } px_scaled; } dim; struct { /* PPI, based on physical size */ struct { int x; int y; } real; /* PPI, logical, based on scaled size */ struct { int x; int y; } scaled; } ppi; float dpi; enum wl_output_transform transform; /* From wl_output */ char *make; char *model; /* From xdg_output */ char *name; char *description; float inch; /* e.g. 24" */ float refresh; int scale; enum fcft_subpixel subpixel; }; struct repeat { int fd; int32_t delay; int32_t rate; bool dont_re_repeat; uint32_t key; }; struct seat { struct wayland *wayl; struct wl_seat *wl_seat; uint32_t wl_name; char *name; struct wl_keyboard *wl_keyboard; struct { uint32_t serial; struct xkb_context *xkb; struct xkb_keymap *xkb_keymap; struct xkb_state *xkb_state; struct xkb_compose_table *xkb_compose_table; struct xkb_compose_state *xkb_compose_state; struct repeat repeat; } kbd; struct wl_pointer *wl_pointer; struct { uint32_t serial; int x; int y; struct wl_surface *surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; int scale; } pointer; }; struct wayland { const struct config *conf; struct fdm *fdm; struct kb_manager *kb_manager; struct render *render; struct prompt *prompt; struct matches *matches; struct font_spec *fonts; size_t font_count; struct { font_reloaded_t cb; void *data; } font_reloaded; bool render_first_frame_transparent; int width; int height; int scale; float dpi; enum fcft_subpixel subpixel; bool font_is_sized_by_dpi; enum { KEEP_RUNNING, EXIT_UPDATE_CACHE, EXIT} status; int exit_code; struct wl_callback *frame_cb; struct buffer *pending; struct wl_display *display; struct wl_registry *registry; struct wl_compositor *compositor; struct wl_surface *surface; struct zwlr_layer_shell_v1 *layer_shell; struct zwlr_layer_surface_v1 *layer_surface; struct wl_shm *shm; struct zxdg_output_manager_v1 *xdg_output_manager; struct xdg_activation_v1 *xdg_activation_v1; tll(struct monitor) monitors; const struct monitor *monitor; tll(struct seat) seats; }; bool repeat_start(struct repeat *repeat, uint32_t key) { if (repeat->dont_re_repeat) return true; if (repeat->rate == 0) return true; struct itimerspec t = { .it_value = {.tv_sec = 0, .tv_nsec = repeat->delay * 1000000}, .it_interval = {.tv_sec = 0, .tv_nsec = 1000000000 / repeat->rate}, }; if (t.it_value.tv_nsec >= 1000000000) { t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000; t.it_value.tv_nsec %= 1000000000; } if (t.it_interval.tv_nsec >= 1000000000) { t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000; t.it_interval.tv_nsec %= 1000000000; } if (timerfd_settime(repeat->fd, 0, &t, NULL) < 0) { LOG_ERRNO("failed to arm keyboard repeat timer"); return false; } repeat->key = key; return true; } bool repeat_stop(struct repeat *repeat, uint32_t key) { if (key != (uint32_t)-1 && key != repeat->key) return true; if (timerfd_settime(repeat->fd, 0, &(struct itimerspec){{0}}, NULL) < 0) { LOG_ERRNO("failed to disarm keyboard repeat timer"); return false; } return true; } static void seat_destroy(struct seat *seat) { if (seat == NULL) return; kb_remove_seat(seat->wayl->kb_manager, seat); if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_unref(seat->kbd.xkb_compose_state); if (seat->kbd.xkb_compose_table != NULL) xkb_compose_table_unref(seat->kbd.xkb_compose_table); if (seat->kbd.xkb_state != NULL) xkb_state_unref(seat->kbd.xkb_state); if (seat->kbd.xkb_keymap != NULL) xkb_keymap_unref(seat->kbd.xkb_keymap); if (seat->kbd.xkb != NULL) xkb_context_unref(seat->kbd.xkb); if (seat->kbd.repeat.fd > 0) fdm_del(seat->wayl->fdm, seat->kbd.repeat.fd); if (seat->wl_keyboard != NULL) wl_keyboard_release(seat->wl_keyboard); if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->pointer.surface != NULL) wl_surface_destroy(seat->pointer.surface); if (seat->wl_pointer != NULL) wl_pointer_release(seat->wl_pointer); if (seat->wl_seat != NULL) wl_seat_release(seat->wl_seat); free(seat->name); } static void update_cursor_surface(struct seat *seat) { if (seat->pointer.surface == NULL || seat->pointer.theme == NULL) return; if (seat->pointer.cursor == NULL) { seat->pointer.cursor = wl_cursor_theme_get_cursor( seat->pointer.theme, "left_ptr"); if (seat->pointer.cursor == NULL) { LOG_ERR("%s: failed to load cursor 'left_ptr'", seat->name); return; } } struct wl_cursor_image *image = seat->pointer.cursor->images[0]; const int scale = seat->pointer.scale; wl_surface_set_buffer_scale(seat->pointer.surface, scale); wl_surface_attach( seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, seat->pointer.surface, image->hotspot_x / scale, image->hotspot_y / scale); wl_surface_damage_buffer(seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_commit(seat->pointer.surface); wl_display_flush(seat->wayl->display); } static bool reload_cursor_theme(struct seat *seat, int new_scale) { assert(seat->pointer.surface != NULL); if (seat->pointer.theme != NULL && seat->pointer.scale == new_scale) return true; if (seat->pointer.theme != NULL) { wl_cursor_theme_destroy(seat->pointer.theme); seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } const char *xcursor_theme = getenv("XCURSOR_THEME"); int xcursor_size = 24; { const char *env_cursor_size = getenv("XCURSOR_SIZE"); if (env_cursor_size != NULL) { unsigned size; if (sscanf(env_cursor_size, "%u", &size) == 1) xcursor_size = size; } } LOG_INFO("cursor theme: %s, size: %u, scale: %d", xcursor_theme, xcursor_size, new_scale); seat->pointer.theme = wl_cursor_theme_load( xcursor_theme, xcursor_size * new_scale, seat->wayl->shm); if (seat->pointer.theme == NULL) { LOG_ERR("failed to load cursor theme"); return false; } seat->pointer.scale = new_scale; return true; } static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { } static const struct wl_shm_listener shm_listener = { .format = &shm_format, }; static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size) { struct seat *seat = data; if (seat->kbd.xkb_compose_state != NULL) { xkb_compose_state_unref(seat->kbd.xkb_compose_state); seat->kbd.xkb_compose_state = NULL; } if (seat->kbd.xkb_compose_table != NULL) { xkb_compose_table_unref(seat->kbd.xkb_compose_table); seat->kbd.xkb_compose_table = NULL; } if (seat->kbd.xkb_keymap != NULL) { xkb_keymap_unref(seat->kbd.xkb_keymap); seat->kbd.xkb_keymap = NULL; } if (seat->kbd.xkb_state != NULL) { xkb_state_unref(seat->kbd.xkb_state); seat->kbd.xkb_state = NULL; } if (seat->kbd.xkb != NULL) { xkb_context_unref(seat->kbd.xkb); seat->kbd.xkb = NULL; } kb_unload_keymap(seat->wayl->kb_manager, seat); /* Verify keymap is in a format we understand */ switch ((enum wl_keyboard_keymap_format)format) { case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: return; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); return; } char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); if (map_str == MAP_FAILED) { LOG_ERRNO("failed to mmap keyboard keymap"); close(fd); return; } while (map_str[size - 1] == '\0') size--; seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (seat->kbd.xkb != NULL) { seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); /* Compose (dead keys) */ seat->kbd.xkb_compose_table = xkb_compose_table_new_from_locale( seat->kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); if (seat->kbd.xkb_compose_table == NULL) { LOG_WARN("failed to instantiate compose table; dead keys will not work"); } else { seat->kbd.xkb_compose_state = xkb_compose_state_new( seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); } } if (seat->kbd.xkb_keymap != NULL) { /* TODO: initialize in enter? */ seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); } munmap(map_str, size); close(fd); if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { kb_load_keymap(seat->wayl->kb_manager, seat, seat->kbd.xkb_state, seat->kbd.xkb_keymap); } } static void keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { struct seat *seat = data; seat->kbd.serial = serial; LOG_DBG("keyboard enter"); #if 0 uint32_t *key; wl_array_for_each(key, keys) xkb_state_update_key(xkb_state, *key, 1); #endif } static void keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; repeat_stop(&seat->kbd.repeat, -1); seat->kbd.serial = 0; if (seat->wayl->conf->exit_on_kb_focus_loss) seat->wayl->status = EXIT; } static void token_done(void *data, struct xdg_activation_token_v1 *token, const char *name) { char **xdg_activation_token = data; *xdg_activation_token = strdup(name); } static const struct xdg_activation_token_v1_listener token_listener = { .done = token_done }; /* Synchronously grab a token from the compositor. The compositor may refuse to * grant a token by not sending the done event. In this case, the * xdg_activation_token return parameter will not be modified. */ static void get_xdg_activation_token(struct seat *seat, const char *app_id, char **xdg_activation_token) { struct wayland *wayl = seat->wayl; struct xdg_activation_token_v1 *token = xdg_activation_v1_get_activation_token(wayl->xdg_activation_v1); xdg_activation_token_v1_set_serial(token, seat->kbd.serial, seat->wl_seat); xdg_activation_token_v1_set_surface(token, wayl->surface); if (app_id != NULL) xdg_activation_token_v1_set_app_id(token, app_id); xdg_activation_token_v1_add_listener( token, &token_listener, xdg_activation_token); xdg_activation_token_v1_commit(token); wl_display_roundtrip(wayl->display); xdg_activation_token_v1_destroy(token); } static void execute_selected(struct seat *seat, int custom_success_exit_code) { struct wayland *wayl = seat->wayl; wayl->status = EXIT; const struct match *match = matches_get_match(wayl->matches); struct application *app = match != NULL ? match->application : NULL; ssize_t index = match != NULL ? match->index : -1; if (wayl->conf->dmenu.enabled) { dmenu_execute(app, index, wayl->prompt, wayl->conf->dmenu.mode); wayl->exit_code = custom_success_exit_code >= 0 ? custom_success_exit_code : EXIT_SUCCESS; } else { char *xdg_activation_token = NULL; if (wayl->xdg_activation_v1 != NULL && app != NULL) get_xdg_activation_token(seat, app->app_id, &xdg_activation_token); bool success = application_execute( app, wayl->prompt, wayl->conf->launch_prefix, xdg_activation_token); free(xdg_activation_token); wayl->exit_code = success ? (custom_success_exit_code >= 0 ? custom_success_exit_code : EXIT_SUCCESS) : EXIT_FAILURE; if (success && match != NULL) { wayl->status = EXIT_UPDATE_CACHE; app->count++; } } } static bool execute_binding(struct seat *seat, const struct key_binding *binding, bool *refresh) { const enum bind_action action = binding->action; struct wayland *wayl = seat->wayl; *refresh = false; switch (action) { case BIND_ACTION_NONE: return true; case BIND_ACTION_CANCEL: wayl->status = EXIT; return true; case BIND_ACTION_CURSOR_HOME: *refresh = prompt_cursor_home(wayl->prompt); return true; case BIND_ACTION_CURSOR_END: *refresh = prompt_cursor_end(wayl->prompt); return true; case BIND_ACTION_CURSOR_LEFT: *refresh = prompt_cursor_prev_char(wayl->prompt); return true; case BIND_ACTION_CURSOR_LEFT_WORD: *refresh = prompt_cursor_prev_word(wayl->prompt); return true; case BIND_ACTION_CURSOR_RIGHT: *refresh = prompt_cursor_next_char(wayl->prompt); return true; case BIND_ACTION_CURSOR_RIGHT_WORD: *refresh = prompt_cursor_next_word(wayl->prompt); return true; case BIND_ACTION_DELETE_PREV: *refresh = prompt_erase_prev_char(wayl->prompt); if (*refresh) matches_update(wayl->matches, wayl->prompt); return true; case BIND_ACTION_DELETE_PREV_WORD: *refresh = prompt_erase_prev_word(wayl->prompt); if (*refresh) matches_update(wayl->matches, wayl->prompt); return true; case BIND_ACTION_DELETE_NEXT: *refresh = prompt_erase_next_char(wayl->prompt); if (*refresh) matches_update(wayl->matches, wayl->prompt); return true; case BIND_ACTION_DELETE_NEXT_WORD: *refresh = prompt_erase_next_word(wayl->prompt); if (*refresh) matches_update(wayl->matches, wayl->prompt); return true; case BIND_ACTION_DELETE_LINE: *refresh = prompt_erase_after_cursor(wayl->prompt); if (*refresh) matches_update(wayl->matches, wayl->prompt); return true; case BIND_ACTION_INSERT_SELECTED: { const struct match *match = matches_get_match(wayl->matches); if (match == NULL) return true; prompt_erase_all(wayl->prompt); if (wayl->conf->dmenu.enabled) { char *chars = ac32tombs(match->application->title); if (chars != NULL) { *refresh = prompt_insert_chars( wayl->prompt, chars, strlen(chars)); free(chars); } } else { *refresh = prompt_insert_chars( wayl->prompt, match->application->exec, strlen(match->application->exec)); } if (*refresh) matches_update(wayl->matches, wayl->prompt); return true; } case BIND_ACTION_MATCHES_EXECUTE: execute_selected(seat, -1); return true; case BIND_ACTION_MATCHES_EXECUTE_OR_NEXT: if (matches_get_total_count(wayl->matches) == 1) execute_selected(seat, -1); else *refresh = matches_selected_next(wayl->matches, true); return true; case BIND_ACTION_MATCHES_PREV: *refresh = matches_selected_prev(wayl->matches, false); return true; case BIND_ACTION_MATCHES_PREV_WITH_WRAP: *refresh = matches_selected_prev(wayl->matches, true); return true; case BIND_ACTION_MATCHES_PREV_PAGE: *refresh = matches_selected_prev_page(wayl->matches); return true; case BIND_ACTION_MATCHES_NEXT: *refresh = matches_selected_next(wayl->matches, false); return true; case BIND_ACTION_MATCHES_NEXT_WITH_WRAP: *refresh = matches_selected_next(wayl->matches, true); return true; case BIND_ACTION_MATCHES_NEXT_PAGE: *refresh = matches_selected_next_page(wayl->matches); return true; case BIND_ACTION_MATCHES_FIRST: *refresh = matches_selected_first(wayl->matches); return true; case BIND_ACTION_MATCHES_LAST: *refresh = matches_selected_last(wayl->matches); return true; case BIND_ACTION_CUSTOM_1: case BIND_ACTION_CUSTOM_2: case BIND_ACTION_CUSTOM_3: case BIND_ACTION_CUSTOM_4: case BIND_ACTION_CUSTOM_5: case BIND_ACTION_CUSTOM_6: case BIND_ACTION_CUSTOM_7: case BIND_ACTION_CUSTOM_8: case BIND_ACTION_CUSTOM_9: case BIND_ACTION_CUSTOM_10: case BIND_ACTION_CUSTOM_11: case BIND_ACTION_CUSTOM_12: case BIND_ACTION_CUSTOM_13: case BIND_ACTION_CUSTOM_14: case BIND_ACTION_CUSTOM_15: case BIND_ACTION_CUSTOM_16: case BIND_ACTION_CUSTOM_17: case BIND_ACTION_CUSTOM_18: case BIND_ACTION_CUSTOM_19: { const size_t idx = action - BIND_ACTION_CUSTOM_1; execute_selected(seat, 10 + idx); return true; } case BIND_ACTION_COUNT: assert(false); return false; } return false; } static bool symbol_is_keypad(xkb_keysym_t sym) { switch (sym) { default: return false; case XKB_KEY_KP_Space: case XKB_KEY_KP_Tab: case XKB_KEY_KP_Enter: case XKB_KEY_KP_F1: case XKB_KEY_KP_F2: case XKB_KEY_KP_F3: case XKB_KEY_KP_F4: case XKB_KEY_KP_Home: case XKB_KEY_KP_Left: case XKB_KEY_KP_Up: case XKB_KEY_KP_Right: case XKB_KEY_KP_Down: case XKB_KEY_KP_Prior: case XKB_KEY_KP_Next: case XKB_KEY_KP_End: case XKB_KEY_KP_Begin: case XKB_KEY_KP_Insert: case XKB_KEY_KP_Delete: case XKB_KEY_KP_Equal: case XKB_KEY_KP_Multiply: case XKB_KEY_KP_Add: case XKB_KEY_KP_Separator: case XKB_KEY_KP_Subtract: case XKB_KEY_KP_Decimal: case XKB_KEY_KP_Divide: case XKB_KEY_KP_0: case XKB_KEY_KP_1: case XKB_KEY_KP_2: case XKB_KEY_KP_3: case XKB_KEY_KP_4: case XKB_KEY_KP_5: case XKB_KEY_KP_6: case XKB_KEY_KP_7: case XKB_KEY_KP_8: case XKB_KEY_KP_9: return true; } } static void keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { /* TODO: these can’t be static - need to reload on keymap changes */ static bool mod_masks_initialized = false; static xkb_mod_mask_t shift = -1; static xkb_mod_mask_t ctrl = -1; static xkb_mod_mask_t alt = -1; static xkb_mod_mask_t super = -1; struct seat *seat = data; struct wayland *wayl = seat->wayl; if (!mod_masks_initialized) { mod_masks_initialized = true; xkb_mod_index_t shift_idx = xkb_keymap_mod_get_index( seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); xkb_mod_index_t ctrl_idx = xkb_keymap_mod_get_index( seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL); xkb_mod_index_t alt_idx = xkb_keymap_mod_get_index( seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT); xkb_mod_index_t super_idx = xkb_keymap_mod_get_index( seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); shift = shift_idx != XKB_MOD_INVALID ? 1 << shift_idx : 0; ctrl = ctrl_idx != XKB_MOD_INVALID ? 1 << ctrl_idx : 0; alt = alt_idx != XKB_MOD_INVALID ? 1 << alt_idx : 0; super = super_idx != XKB_MOD_INVALID ? 1 << super_idx : 0; } if (state == XKB_KEY_UP) { repeat_stop(&seat->kbd.repeat, key); return; } key += 8; bool should_repeat = xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key); enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING; if (seat->kbd.xkb_compose_state != NULL) { xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); compose_status = xkb_compose_state_get_status( seat->kbd.xkb_compose_state); } if (compose_status == XKB_COMPOSE_COMPOSING) { /* TODO: goto maybe_repeat? */ return; } xkb_mod_mask_t mods = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); const xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); const xkb_mod_mask_t locked = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); const xkb_mod_mask_t significant = shift | ctrl | alt | super; const xkb_mod_mask_t bind_mods = mods & significant & ~locked; const xkb_mod_mask_t bind_consumed = consumed & significant & ~locked; const xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, key); const xkb_keysym_t *raw_syms = NULL; const size_t raw_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); #if 0 for (size_t i = 0; i < 32; i++) { if (mods & (1 << i)) { LOG_DBG("%s", xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); } } #endif LOG_DBG("sym=%u, mod=0x%08x, consumed=0x%08x, significant=0x%08x, " "effective=0x%08x", sym, mods, consumed, significant, effective_mods); const struct key_binding_set *bindings = key_binding_for(wayl->kb_manager, seat); assert(bindings != NULL); bool refresh = false; seat->kbd.serial = serial; tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; if (bind->k.sym == sym && bind->mods == (bind_mods & ~bind_consumed) && execute_binding(seat, bind, &refresh)) { if (refresh) wayl_refresh(wayl); goto maybe_repeat; } if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) continue; /* * Skip raw key codes for keypad keys. * * The keypad is usually unaffected by the layout. Instead, * mapping raw key codes effectively means we’ll ignore * NumLock. That is, if we have a key binding for KP_PageUp, * it’ll trigger regardless of NumLock setting, and thus * making it impossible to use the keypad to enter numericals. * * See https://codeberg.org/dnkl/fuzzel/issues/192 */ if (symbol_is_keypad(bind->k.sym)) continue; for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && execute_binding(seat, bind, &refresh)) { if (refresh) wayl_refresh(wayl); goto maybe_repeat; } } tll_foreach(bind->k.key_codes, code) { if (code->item == key && execute_binding(seat, bind, &refresh)) { if (refresh) wayl_refresh(wayl); goto maybe_repeat; } } } if ((bind_mods & ~bind_consumed) != 0) goto maybe_repeat; /* * Compose, and maybe emit "normal" character */ char buf[64] = {0}; int count = 0; if (compose_status == XKB_COMPOSE_COMPOSED) { count = xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, buf, sizeof(buf)); xkb_compose_state_reset(seat->kbd.xkb_compose_state); } else if (compose_status == XKB_COMPOSE_CANCELLED) { goto maybe_repeat; } else { count = xkb_state_key_get_utf8( seat->kbd.xkb_state, key, buf, sizeof(buf)); } if (count == 0) return; if (!prompt_insert_chars(wayl->prompt, buf, count)) return; matches_update(wayl->matches, wayl->prompt); wayl_refresh(wayl); maybe_repeat: if (should_repeat) repeat_start(&seat->kbd.repeat, key - 8); } static void 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 seat *seat = data; LOG_DBG("modifiers: depressed=0x%x, latched=0x%x, locked=0x%x, group=%u", mods_depressed, mods_latched, mods_locked, group); xkb_state_update_mask( seat->kbd.xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); } static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) { struct seat *seat = data; LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay); seat->kbd.repeat.delay = delay; seat->kbd.repeat.rate = rate; } static const struct wl_keyboard_listener keyboard_listener = { .keymap = &keyboard_keymap, .enter = &keyboard_enter, .leave = &keyboard_leave, .key = &keyboard_key, .modifiers = &keyboard_modifiers, .repeat_info = &keyboard_repeat_info, }; static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; seat->pointer.serial = serial; reload_cursor_theme(seat, seat->wayl->scale); update_cursor_surface(seat); } static void wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; seat->pointer.serial = serial; } static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { } static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { } static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { } static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { } static void wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source) { } static void wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { } static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { } static const struct wl_pointer_listener 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 seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) { struct seat *seat = data; assert(seat->wl_seat == wl_seat); if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { if (seat->wl_keyboard == NULL) { seat->wl_keyboard = wl_seat_get_keyboard(wl_seat); wl_keyboard_add_listener(seat->wl_keyboard, &keyboard_listener, seat); } } else { if (seat->wl_keyboard != NULL) { wl_keyboard_release(seat->wl_keyboard); seat->wl_keyboard = NULL; } } if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { assert(seat->pointer.surface == NULL); seat->pointer.surface = wl_compositor_create_surface(seat->wayl->compositor); if (seat->pointer.surface == NULL) { LOG_ERR("%s: failed to create pointer surface", seat->name); return; } seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); } } else { if (seat->wl_pointer != NULL) { wl_pointer_release(seat->wl_pointer); wl_surface_destroy(seat->pointer.surface); if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); seat->wl_pointer = NULL; seat->pointer.surface = NULL; seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } } } static void seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name) { struct seat *seat = data; free(seat->name); seat->name = strdup(name); } static const struct wl_seat_listener seat_listener = { .capabilities = seat_handle_capabilities, .name = seat_handle_name, }; static int guess_scale(const struct wayland *wayl) { if (tll_length(wayl->monitors) == 0) return 1; bool all_have_same_scale = true; int last_scale = -1; tll_foreach(wayl->monitors, it) { if (last_scale == -1) last_scale = it->item.scale; else if (last_scale != it->item.scale) { all_have_same_scale = false; break; } } if (all_have_same_scale) { assert(last_scale >= 1); return last_scale; } return 1; } static enum fcft_subpixel guess_subpixel(const struct wayland *wayl) { if (tll_length(wayl->monitors) == 0) return FCFT_SUBPIXEL_DEFAULT; return tll_front(wayl->monitors).subpixel; } static bool size_font_using_dpi(const struct wayland *wayl) { switch (wayl->conf->dpi_aware) { case DPI_AWARE_NO: return false; case DPI_AWARE_YES: return true; case DPI_AWARE_AUTO: tll_foreach(wayl->monitors, it) { const struct monitor *mon = &it->item; if (mon->scale > 1) return false; } return true; } assert(false); return false; } static bool reload_font(struct wayland *wayl, float new_dpi, unsigned new_scale) { struct fcft_font *font = NULL; bool was_sized_using_dpi = wayl->font_is_sized_by_dpi; bool will_size_using_dpi = size_font_using_dpi(wayl); bool need_font_reload = was_sized_using_dpi != will_size_using_dpi || (will_size_using_dpi ? wayl->dpi != new_dpi : wayl->scale != new_scale); LOG_DBG( "font reload: scale: %u -> %u, dpi: %.2f -> %.2f size method: %s -> %s", wayl->scale, new_scale, wayl->dpi, new_dpi, was_sized_using_dpi ? "DPI" : "scaling factor", will_size_using_dpi ? "DPI" : "scaling factor"); wayl->font_is_sized_by_dpi = will_size_using_dpi; if (need_font_reload) { char **names = malloc(wayl->font_count * sizeof(names[0])); /* Apply font size, possibly scaled using the scaling factor */ for (size_t i = 0; i < wayl->font_count; i++) { const struct font_spec *spec = &wayl->fonts[i]; const bool use_px_size = spec->px_size > 0; const int scale = wayl->font_is_sized_by_dpi ? 1 : new_scale; char size[64]; size_t size_len; if (use_px_size) { size_len = snprintf( size, sizeof(size), ":pixelsize=%d", spec->px_size * scale); } else { size_len = snprintf( size, sizeof(size), ":size=%.2f", spec->pt_size * scale); } size_t len = strlen(spec->pattern) + size_len + 1; names[i] = malloc(len); strcpy(names[i], spec->pattern); strcat(names[i], size); } /* Font’s DPI */ float dpi = will_size_using_dpi ? new_dpi : 96.; char attrs[256]; snprintf(attrs, sizeof(attrs), "dpi=%.2f", dpi); font = fcft_from_name(wayl->font_count, (const char **)names, attrs); for (size_t i = 0; i < wayl->font_count; i++) free(names[i]); free(names); if (font == NULL) return false; bool ret = render_set_font( wayl->render, font, new_scale, new_dpi, wayl->font_is_sized_by_dpi, &wayl->width, &wayl->height); if (wayl->font_reloaded.cb != NULL) wayl->font_reloaded.cb(wayl, font, wayl->font_reloaded.data); return ret; } return true; } static float wayl_ppi(const struct wayland *wayl) { /* Use user configured output, if available, otherwise use the * "first" output */ const struct monitor *mon = wayl->monitor != NULL ? wayl->monitor : (tll_length(wayl->monitors) > 0 ? &tll_front(wayl->monitors) : NULL); if (mon != NULL && mon->dpi != 0.) return mon->dpi; /* No outputs available, return "something" */ return 96.; } static void update_size(struct wayland *wayl) { const struct monitor *mon = wayl->monitor; const int scale = mon != NULL ? mon->scale : guess_scale(wayl); const float dpi = wayl_ppi(wayl); if (scale == wayl->scale && dpi == wayl->dpi) return; reload_font(wayl, dpi, scale); wayl->scale = scale; wayl->dpi = dpi; wayl->width /= scale; wayl->width *= scale; wayl->height /= scale; wayl->height *= scale; zwlr_layer_surface_v1_set_size( wayl->layer_surface, wayl->width / scale, wayl->height / scale); /* Trigger a 'configure' event, after which we'll have the actual width+height */ wl_surface_commit(wayl->surface); wl_display_roundtrip(wayl->display); } static void output_update_ppi(struct monitor *mon) { if (mon->dim.mm.width == 0 || mon->dim.mm.height == 0) return; int x_inches = mon->dim.mm.width * 0.03937008; int y_inches = mon->dim.mm.height * 0.03937008; mon->ppi.real.x = mon->dim.px_real.width / x_inches; mon->ppi.real.y = mon->dim.px_real.height / y_inches; /* The *logical* size is affected by the transform */ switch (mon->transform) { case WL_OUTPUT_TRANSFORM_90: case WL_OUTPUT_TRANSFORM_270: case WL_OUTPUT_TRANSFORM_FLIPPED_90: case WL_OUTPUT_TRANSFORM_FLIPPED_270: { int swap = x_inches; x_inches = y_inches; y_inches = swap; break; } case WL_OUTPUT_TRANSFORM_NORMAL: case WL_OUTPUT_TRANSFORM_180: case WL_OUTPUT_TRANSFORM_FLIPPED: case WL_OUTPUT_TRANSFORM_FLIPPED_180: break; } mon->ppi.scaled.x = mon->dim.px_scaled.width / x_inches; mon->ppi.scaled.y = mon->dim.px_scaled.height / y_inches; float px_diag = sqrt( pow(mon->dim.px_scaled.width, 2) + pow(mon->dim.px_scaled.height, 2)); mon->dpi = px_diag / mon->inch * mon->scale; } 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 monitor *mon = data; free(mon->make); free(mon->model); mon->dim.mm.width = physical_width; mon->dim.mm.height = physical_height; mon->inch = sqrt(pow(mon->dim.mm.width, 2) + pow(mon->dim.mm.height, 2)) * 0.03937008; mon->make = make != NULL ? strdup(make) : NULL; mon->model = model != NULL ? strdup(model) : NULL; mon->subpixel = subpixel; mon->transform = transform; output_update_ppi(mon); } static void output_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) return; struct monitor *mon = data; mon->refresh = (float)refresh / 1000; mon->dim.px_real.width = width; mon->dim.px_real.height = height; output_update_ppi(mon); } static void output_done(void *data, struct wl_output *wl_output) { } static void output_scale(void *data, struct wl_output *wl_output, int32_t factor) { struct monitor *mon = data; mon->scale = factor; if (mon->wayl->monitor == mon) { int old_scale = mon->wayl->scale; float old_dpi = mon->wayl->dpi; update_size(mon->wayl); if (mon->wayl->scale != old_scale || mon->wayl->dpi != old_dpi) wayl_refresh(mon->wayl); } } static const struct wl_output_listener output_listener = { .geometry = &output_geometry, .mode = &output_mode, .done = &output_done, .scale = &output_scale, }; static void xdg_output_handle_logical_position( void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { struct monitor *mon = data; mon->x = x; mon->y = y; } static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { struct monitor *mon = data; mon->dim.px_scaled.width = width; mon->dim.px_scaled.height = height; output_update_ppi(mon); } static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { } static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { struct monitor *mon = data; struct wayland *wayl = mon->wayl; free(mon->name); mon->name = name != NULL ? strdup(name) : NULL; if (wayl->conf->output != NULL && mon->name != NULL && strcmp(wayl->conf->output, mon->name) == 0) { wayl->monitor = mon; } } static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { struct monitor *mon = data; free(mon->description); mon->description = description != NULL ? strdup(description) : NULL; } static struct zxdg_output_v1_listener xdg_output_listener = { .logical_position = xdg_output_handle_logical_position, .logical_size = xdg_output_handle_logical_size, .done = xdg_output_handle_done, .name = xdg_output_handle_name, .description = xdg_output_handle_description, }; static bool verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) { if (version >= wanted) return true; LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version); return false; } static bool fdm_repeat(struct fdm *fdm, int fd, int events, void *data) { struct seat *seat = data; struct repeat *repeat = &seat->kbd.repeat; uint64_t expiration_count; ssize_t ret = read( repeat->fd, &expiration_count, sizeof(expiration_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read key repeat count from timer fd"); return false; } repeat->dont_re_repeat = true; for (size_t i = 0; i < expiration_count; i++) keyboard_key(seat, NULL, 0, 0, repeat->key, XKB_KEY_DOWN); repeat->dont_re_repeat = false; if (events & EPOLLHUP) { LOG_ERR("keyboard repeater timer FD closed unexpectedly"); return false; } return true; } static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland *wayl = data; if (strcmp(interface, wl_compositor_interface.name) == 0) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; wayl->compositor = wl_registry_bind( wayl->registry, name, &wl_compositor_interface, required); } else if (strcmp(interface, wl_shm_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->shm = wl_registry_bind( wayl->registry, name, &wl_shm_interface, required); wl_shm_add_listener(wayl->shm, &shm_listener, wayl); } else if (strcmp(interface, wl_output_interface.name) == 0) { const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; struct wl_output *output = wl_registry_bind( wayl->registry, name, &wl_output_interface, required); tll_push_back(wayl->monitors, ((struct monitor){ .output = output, .wayl = wayl, .wl_name = name, })); struct monitor *mon = &tll_back(wayl->monitors); wl_output_add_listener(output, &output_listener, mon); /* * The "output" interface doesn't give us the monitors' * identifiers (e.g. "LVDS-1"). Use the XDG output interface * for that. */ assert(wayl->xdg_output_manager != NULL); if (wayl->xdg_output_manager != NULL) { mon->xdg = zxdg_output_manager_v1_get_xdg_output( wayl->xdg_output_manager, mon->output); zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); } } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->layer_shell = wl_registry_bind( wayl->registry, name, &zwlr_layer_shell_v1_interface, required); } else if (strcmp(interface, wl_seat_interface.name) == 0) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; struct wl_seat *wl_seat = wl_registry_bind( wayl->registry, name, &wl_seat_interface, required); int repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (repeat_fd == -1) { LOG_ERRNO("failed to create keyboard repeat timer FD"); wl_seat_destroy(wl_seat); return; } tll_push_back(wayl->seats, ((struct seat){ .wayl = wayl, .wl_seat = wl_seat, .wl_name = name, .kbd = { .repeat = { .fd = repeat_fd, }, }})); struct seat *seat = &tll_back(wayl->seats); if (!fdm_add(wayl->fdm, repeat_fd, EPOLLIN, &fdm_repeat, seat)) { LOG_ERR("failed to register keyboard repeat timer FD with FDM"); close(seat->kbd.repeat.fd); wl_seat_destroy(seat->wl_seat); tll_pop_back(wayl->seats); return; } kb_new_for_seat(wayl->kb_manager, wayl->conf, seat); wl_seat_add_listener(wl_seat, &seat_listener, seat); } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; wayl->xdg_output_manager = wl_registry_bind( wayl->registry, name, &zxdg_output_manager_v1_interface, required); } else if (strcmp(interface, xdg_activation_v1_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->xdg_activation_v1 = wl_registry_bind( wayl->registry, name, &xdg_activation_v1_interface, required); } } static void monitor_destroy(struct monitor *mon) { if (mon->xdg != NULL) zxdg_output_v1_destroy(mon->xdg); if (mon->output != NULL) wl_output_release(mon->output); free(mon->name); free(mon->make); free(mon->model); free(mon->description); } static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { LOG_DBG("global removed: 0x%08x", name); struct wayland *wayl = data; tll_foreach(wayl->monitors, it) { struct monitor *mon = &it->item; if (mon->wl_name != name) continue; LOG_INFO("monitor disabled: %s", mon->name); if (wayl->monitor == mon) wayl->monitor = NULL; monitor_destroy(mon); tll_remove(wayl->monitors, it); return; } tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; if (seat->wl_name != name) continue; LOG_INFO("seat removed: %s", seat->name); seat_destroy(seat); tll_remove(wayl->seats, it); return; } LOG_WARN("unknown global removed: 0x%08x", name); } static const struct wl_registry_listener registry_listener = { .global = &handle_global, .global_remove = &handle_global_remove, }; static void layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t w, uint32_t h) { struct wayland *wayl = data; zwlr_layer_surface_v1_ack_configure(surface, serial); if (w > 0 && h > 0) { if (w * wayl->scale != wayl->width || h * wayl->scale != wayl->height) { wayl->width = w * wayl->scale; wayl->height = h * wayl->scale; wayl_refresh(wayl); } } } static void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) { struct wayland *wayl = data;; wayl->status = EXIT; } static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { .configure = &layer_surface_configure, .closed = &layer_surface_closed, }; static void frame_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener frame_listener = { .done = &frame_callback, }; static void commit_buffer(struct wayland *wayl, struct buffer *buf) { assert(buf->busy); assert(wayl->scale >= 1); wl_surface_set_buffer_scale(wayl->surface, wayl->scale); wl_surface_attach(wayl->surface, buf->wl_buf, 0, 0); wl_surface_damage_buffer(wayl->surface, 0, 0, buf->width, buf->height); assert(wayl->frame_cb == NULL); wayl->frame_cb = wl_surface_frame(wayl->surface); wl_callback_add_listener(wayl->frame_cb, &frame_listener, wayl); wl_surface_commit(wayl->surface); } static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { struct wayland *wayl = data; assert(wayl->frame_cb == wl_callback); wl_callback_destroy(wayl->frame_cb); wayl->frame_cb = NULL; if (wayl->pending != NULL) { commit_buffer(wayl, wayl->pending); wayl->pending = NULL; } if (wayl->render_first_frame_transparent) { wayl->render_first_frame_transparent = false; wayl_refresh(wayl); } } void wayl_refresh(struct wayland *wayl) { struct buffer *buf = shm_get_buffer(wayl->shm, wayl->width, wayl->height); pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); pixman_image_set_clip_region32(buf->pix, &clip); pixman_region32_fini(&clip); if (wayl->render_first_frame_transparent) { pixman_color_t transparent = {0}; pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &transparent, 1, &(pixman_rectangle16_t){0, 0, wayl->width, wayl->height}); goto commit; } render_set_subpixel(wayl->render, wayl->subpixel); /* Background + border */ render_background(wayl->render, buf); /* Window content */ render_prompt(wayl->render, buf, wayl->prompt); render_match_list(wayl->render, buf, wayl->prompt, wayl->matches); #if defined(FUZZEL_ENABLE_CAIRO) cairo_surface_flush(buf->cairo_surface); #endif commit: if (wayl->frame_cb != NULL) { /* There's already a frame being drawn - delay current frame * (overwriting any previous pending frame) */ if (wayl->pending != NULL) wayl->pending->busy = false; wayl->pending = buf; } else { /* No pending frames - render immediately */ assert(wayl->pending == NULL); commit_buffer(wayl, buf); } } static bool fdm_handler(struct fdm *fdm, int fd, int events, void *data) { struct wayland *wayl = data; int event_count = 0; if (events & EPOLLIN) { if (wl_display_read_events(wayl->display) < 0) { LOG_ERRNO("failed to read events from the Wayland socket"); return false; } while (wl_display_prepare_read(wayl->display) != 0) if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); return false; } } if (events & EPOLLHUP) { LOG_WARN("disconnected from Wayland"); return false; } wl_display_flush(wayl->display); return event_count != -1 && wayl->status == KEEP_RUNNING; } static void surface_enter(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct wayland *wayl = data; tll_foreach(wayl->monitors, it) { if (it->item.output != wl_output) continue; enum fcft_subpixel old_subpixel = wayl->subpixel; int old_scale = wayl->scale; float old_dpi = wayl->dpi; wayl->monitor = &it->item; wayl->subpixel = wayl->monitor->subpixel; update_size(wayl); if (wayl->scale != old_scale || wayl->dpi != old_dpi || wayl->subpixel != old_subpixel) { wayl_refresh(wayl); } break; } } static void surface_leave(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct wayland *wayl = data; wayl->monitor = NULL; } static const struct wl_surface_listener surface_listener = { .enter = &surface_enter, .leave = &surface_leave, }; static bool font_pattern_to_spec(const char *pattern, struct font_spec *spec) { FcPattern *pat = FcNameParse((const FcChar8 *)pattern); if (pat == NULL) return false; /* * First look for user specified {pixel}size option * e.g. “font-name:size=12” */ double pt_size = -1.0; FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); int px_size = -1; FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { /* * Apply fontconfig config. Can’t do that until we’ve first * checked for a user provided size, since we may end up with * both “size” and “pixelsize” being set, and we don’t know * which one takes priority. */ FcPattern *pat_copy = FcPatternDuplicate(pat); if (pat_copy == NULL || !FcConfigSubstitute(NULL, pat_copy, FcMatchPattern)) { LOG_WARN("%s: failed to do config substitution", pattern); } else { have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size); have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size); } FcPatternDestroy(pat_copy); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) pt_size = 12.0; } FcPatternRemove(pat, FC_SIZE, 0); FcPatternRemove(pat, FC_PIXEL_SIZE, 0); char *stripped_pattern = (char *)FcNameUnparse(pat); FcPatternDestroy(pat); LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); *spec = (struct font_spec){ .pattern = stripped_pattern, .pt_size = pt_size, .px_size = px_size }; return true; } static void parse_font_spec(const char *font_spec, size_t *count, struct font_spec **specs) { tll(struct font_spec) fonts = tll_init(); char *copy = strdup(font_spec); for (char *font = strtok(copy, ","); font != NULL; font = strtok(NULL, ",")) { while (*font != '\0' && isspace(*font)) font++; size_t len = strlen(font); while (len > 0 && isspace(font[len - 1])) font[--len] = '\0'; if (font[0] == '\0') continue; struct font_spec spec; if (font_pattern_to_spec(font, &spec)) tll_push_back(fonts, spec); } free(copy); *count = tll_length(fonts); *specs = malloc(*count * sizeof((*specs)[0])); struct font_spec *s = *specs; tll_foreach(fonts, it) { *s = it->item; tll_remove(fonts, it); s++; } } struct wayland * wayl_init(const struct config *conf, struct fdm *fdm, struct kb_manager *kb_manager, struct render *render, struct prompt *prompt, struct matches *matches, font_reloaded_t font_reloaded_cb, void *data) { struct wayland *wayl = malloc(sizeof(*wayl)); *wayl = (struct wayland){ .conf = conf, .fdm = fdm, .kb_manager = kb_manager, .render = render, .prompt = prompt, .matches = matches, .status = KEEP_RUNNING, .exit_code = !conf->dmenu.enabled ? EXIT_SUCCESS : EXIT_FAILURE, .font_reloaded = { .cb = font_reloaded_cb, .data = data, }, }; parse_font_spec(conf->font, &wayl->font_count, &wayl->fonts); wayl->display = wl_display_connect(NULL); if (wayl->display == NULL) { LOG_ERR("failed to connect to wayland; no compositor running?"); goto out; } wayl->registry = wl_display_get_registry(wayl->display); if (wayl->registry == NULL) { LOG_ERR("failed to get wayland registry"); goto out; } wl_registry_add_listener(wayl->registry, ®istry_listener, wayl); wl_display_roundtrip(wayl->display); if (wayl->compositor == NULL) { LOG_ERR("no compositor"); goto out; } if (wayl->layer_shell == NULL) { LOG_ERR("no layer shell interface"); goto out; } if (wayl->shm == NULL) { LOG_ERR("no shared memory buffers interface"); goto out; } wl_display_roundtrip(wayl->display); if (tll_length(wayl->monitors) == 0) { LOG_ERR("no monitors"); goto out; } tll_foreach(wayl->monitors, it) { const struct monitor *mon = &it->item; LOG_INFO( "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d PPI=%dx%d (physical) PPI=%dx%d (logical), DPI=%.2f", mon->name, mon->dim.px_real.width, mon->dim.px_real.height, mon->x, mon->y, (int)round(mon->refresh), mon->model != NULL ? mon->model : mon->description, mon->inch, mon->scale, mon->ppi.real.x, mon->ppi.real.y, mon->ppi.scaled.x, mon->ppi.scaled.y, it->item.dpi); } LOG_DBG("using output: %s", wayl->monitor != NULL ? wayl->monitor->name : NULL); /* * Only do the “first frame is transparent” trick if * needed. I.e. if: * * - we have more than one monitor (in which case there’s a * chance we guess the scaling factor, or DPI, wrong). * * and * * - the user hasn’t selected a specific output. */ wayl->render_first_frame_transparent = tll_length(wayl->monitors) > 1 && wayl->monitor == NULL; LOG_DBG("using the first-frame-is-transparent trick: %s", wayl->render_first_frame_transparent ? "yes" :"no"); wayl->surface = wl_compositor_create_surface(wayl->compositor); if (wayl->surface == NULL) { LOG_ERR("failed to create panel surface"); goto out; } wl_surface_add_listener(wayl->surface, &surface_listener, wayl); #if 0 wayl->pointer.surface = wl_compositor_create_surface(wayl->compositor); if (wayl->pointer.surface == NULL) { LOG_ERR("failed to create cursor surface"); goto out; } wayl->pointer.theme = wl_cursor_theme_load(NULL, 24, wayl->shm); if (wayl->pointer.theme == NULL) { LOG_ERR("failed to load cursor theme"); goto out; } #endif wayl->layer_surface = zwlr_layer_shell_v1_get_layer_surface( wayl->layer_shell, wayl->surface, wayl->monitor != NULL ? wayl->monitor->output : NULL, conf->layer, "launcher"); if (wayl->layer_surface == NULL) { LOG_ERR("failed to create layer shell surface"); goto out; } zwlr_layer_surface_v1_set_keyboard_interactivity(wayl->layer_surface, 1); zwlr_layer_surface_v1_add_listener( wayl->layer_surface, &layer_surface_listener, wayl); wayl->subpixel = wayl->monitor != NULL ? (enum fcft_subpixel)wayl->monitor->subpixel : guess_subpixel(wayl); update_size(wayl); if (wl_display_prepare_read(wayl->display) != 0) { LOG_ERRNO("failed to prepare for reading wayland events"); goto out; } if (!fdm_add(wayl->fdm, wl_display_get_fd(wayl->display), EPOLLIN, &fdm_handler, wayl)) { LOG_ERR("failed to register Wayland socket with FDM"); goto out; } return wayl; out: wayl_destroy(wayl); return NULL; } void wayl_destroy(struct wayland *wayl) { if (wayl == NULL) return; if (wayl->frame_cb != NULL) wl_callback_destroy(wayl->frame_cb); if (wayl->display != NULL) fdm_del_no_close(wayl->fdm, wl_display_get_fd(wayl->display)); tll_foreach(wayl->seats, it) seat_destroy(&it->item); tll_free(wayl->seats); tll_foreach(wayl->monitors, it) monitor_destroy(&it->item); tll_free(wayl->monitors); if (wayl->xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); if (wayl->xdg_activation_v1 != NULL) xdg_activation_v1_destroy(wayl->xdg_activation_v1); if (wayl->layer_surface != NULL) zwlr_layer_surface_v1_destroy(wayl->layer_surface); if (wayl->layer_shell != NULL) zwlr_layer_shell_v1_destroy(wayl->layer_shell); if (wayl->surface != NULL) wl_surface_destroy(wayl->surface); if (wayl->compositor != NULL) wl_compositor_destroy(wayl->compositor); if (wayl->shm != NULL) wl_shm_destroy(wayl->shm); if (wayl->registry != NULL) wl_registry_destroy(wayl->registry); if (wayl->display != NULL) { wayl_flush(wayl); wl_display_disconnect(wayl->display); } for (size_t i = 0; i < wayl->font_count; i++) free(wayl->fonts[i].pattern); free(wayl->fonts); free(wayl); } void wayl_flush(struct wayland *wayl) { wl_display_flush(wayl->display); } int wayl_exit_code(const struct wayland *wayl) { return wayl->exit_code; } bool wayl_update_cache(const struct wayland *wayl) { return wayl->status == EXIT_UPDATE_CACHE; } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/wayland.h������������������������������������������������������������������������������������0000664�0000000�0000000�00000001361�14454170012�0014204�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <fcft/fcft.h> #include "config.h" #include "dmenu.h" #include "fdm.h" #include "key-binding.h" #include "match.h" #include "prompt.h" #include "render.h" struct wayland; typedef void (*font_reloaded_t)( struct wayland *wayl, struct fcft_font *font, void *data); struct wayland *wayl_init( const struct config *conf, struct fdm *fdm, struct kb_manager *kb_manager, struct render *render, struct prompt *prompt, struct matches *matches, font_reloaded_t font_reloaded_cb, void *data); void wayl_destroy(struct wayland *wayl); void wayl_refresh(struct wayland *wayl); void wayl_flush(struct wayland *wayl); int wayl_exit_code(const struct wayland *wayl); bool wayl_update_cache(const struct wayland *wayl); �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/xdg.c����������������������������������������������������������������������������������������0000664�0000000�0000000�00000060102�14454170012�0013320�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "xdg.h" #include <errno.h> #include <limits.h> #include <locale.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <dirent.h> #include <fcntl.h> #include <pwd.h> #define LOG_MODULE "xdg" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "icon.h" typedef tll(struct application) application_llist_t; struct locale_variants { char *lang_country_modifier; char *lang_country; char *lang_modifier; char *lang; }; struct action { char32_t *name; char32_t *generic_name; char *app_id; char32_t *comment; char32_list_t keywords; char32_list_t categories; char_list_t onlyshowin; char_list_t notshowin; int name_locale_score; int generic_name_locale_score; int comment_locale_score; int keywords_locale_score; int categories_locale_score; char *icon; char *exec; char32_t *wexec; char *path; bool visible; bool use_terminal; }; static bool filter_desktop_entry(const struct action *act, const char_list_t *desktops) { /* If a matching entry is found in OnlyShowIn then the desktop file is * shown. If an entry is found in NotShowIn then the desktop file is not * shown. */ tll_foreach(*desktops, current) { tll_foreach(act->onlyshowin, desktop) { if (strcmp(current->item, desktop->item) == 0) return true; } tll_foreach(act->notshowin, desktop) { if (strcmp(current->item, desktop->item) == 0) return false; } } /* By default, a desktop file should be shown, unless an OnlyShowIn key is * present, in which case, the default is for the file not to be shown. */ return tll_length(act->onlyshowin) == 0; } static void parse_desktop_file(int fd, char *id, const char32_t *file_basename, const char *terminal, bool include_actions, bool filter_desktops, char_list_t *desktops, const struct locale_variants *lc_messages, application_llist_t *applications) { FILE *f = fdopen(fd, "r"); if (f == NULL) return; bool is_desktop_entry = false; tll(char *) action_names = tll_init(); tll(struct action) actions = tll_init(); tll_push_back(actions, ((struct action){.visible = true})); struct action *action = &tll_back(actions); struct action *default_action = action; while (true) { char *line = NULL; size_t sz = 0; ssize_t len = getline(&line, &sz, f); if (len == -1) { free(line); break; } if (len == 0) { free(line); continue; } if (line[len - 1] == '\n') { line[len - 1] = '\0'; len--; } if (len == 0) { free(line); continue; } if (line[0] == '[' && line[len - 1] == ']') { if (strncasecmp(&line[1], "desktop entry", len - 2) == 0) { is_desktop_entry = true; free(line); continue; } else if (include_actions && strncasecmp(&line[1], "desktop action ", 15) == 0) { const char *action_name = &line[16]; const size_t name_len = len - 1 - 16; bool action_is_valid = false; tll_foreach(action_names, it) { if (strlen(it->item) == name_len && strncmp(it->item, action_name, name_len) == 0) { tll_push_back(actions, ((struct action){0})); action = &tll_back(actions); action->generic_name = default_action->generic_name != NULL ? c32dup(default_action->generic_name) : NULL; action->comment = default_action->comment != NULL ? c32dup(default_action->comment) : NULL; action->path = default_action->path != NULL ? strdup(default_action->path) : NULL; action->icon = default_action->icon != NULL ? strdup(default_action->icon) : NULL; action->visible = default_action->visible; action->use_terminal = default_action->use_terminal; tll_foreach(default_action->keywords, it) tll_push_back(action->keywords, c32dup(it->item)); tll_foreach(default_action->categories, it) tll_push_back(action->categories, c32dup(it->item)); action_is_valid = true; break; } } free(line); if (action_is_valid) continue; else break; } else { free(line); break; } } char *ctx; char *key = strtok_r(line, "=", &ctx); char *value = strtok_r(NULL, "\n", &ctx); int locale_score = 1; /* Default, locale not specified in key */ if (key != NULL && value != NULL) { const size_t key_len = strlen(key); if (key[key_len - 1] == ']') { char *locale = strchr(key, '['); if (locale != NULL) { /* NULL terminate key */ *locale = '\0'; /* NULL terminate locale */ key[key_len - 1] = '\0'; locale++; /* Skip past ‘[’ */ if (lc_messages->lang_country_modifier != NULL && strcmp(locale, lc_messages->lang_country_modifier) == 0) { locale_score = 5; } else if (lc_messages->lang_country != NULL && strcmp(locale, lc_messages->lang_country) == 0) { locale_score = 4; } else if (lc_messages->lang_modifier != NULL && strcmp(locale, lc_messages->lang_modifier) == 0) { locale_score = 3; } else if (lc_messages->lang != NULL && strcmp(locale, lc_messages->lang) == 0) { locale_score = 2; } else { /* Key has locale, but didn’t match. Make sure * we don’t use its value */ locale_score = -1; } } } if (strcmp(key, "Name") == 0) { if (locale_score > action->name_locale_score) { free(action->name); action->name = ambstoc32(value); action->name_locale_score = locale_score; } } else if (strcmp(key, "Exec") == 0) { free(action->exec); free(action->wexec); action->exec = strdup(value); action->wexec = ambstoc32(value); } else if (strcmp(key, "Path") == 0) { free(action->path); action->path = strdup(value); } else if (strcmp(key, "GenericName") == 0) { if (locale_score > action->generic_name_locale_score) { free(action->generic_name); action->generic_name = ambstoc32(value); action->generic_name_locale_score = locale_score; } } else if (strcmp(key, "StartupWMClass") == 0) { free(action->app_id); action->app_id = strdup(value); } else if (strcmp(key, "Comment") == 0) { if (locale_score > action->comment_locale_score) { free(action->comment); action->comment = ambstoc32(value); action->comment_locale_score = locale_score; } } else if (strcmp(key, "Keywords") == 0) { if (locale_score > action->keywords_locale_score) { for (const char *kw = strtok_r(value, ";", &ctx); kw != NULL; kw = strtok_r(NULL, ";", &ctx)) { char32_t *wide_kw = ambstoc32(kw); if (wide_kw != NULL) tll_push_back(action->keywords, wide_kw); } action->keywords_locale_score = locale_score; } } else if (strcmp(key, "Categories") == 0) { if (locale_score > action->categories_locale_score) { for (const char *category = strtok_r(value, ";", &ctx); category != NULL; category = strtok_r(NULL, ";", &ctx)) { char32_t *wide_category = ambstoc32(category); if (wide_category != NULL) tll_push_back(action->categories, wide_category); } action->categories_locale_score = locale_score; } } else if (strcmp(key, "Actions") == 0) { for (const char *action = strtok_r(value, ";", &ctx); action != NULL; action = strtok_r(NULL, ";", &ctx)) { tll_push_back(action_names, strdup(action)); } } else if (strcmp(key, "OnlyShowIn") == 0) { for (const char *desktop = strtok_r(value, ";", &ctx); desktop != NULL; desktop = strtok_r(NULL, ";", &ctx)) { tll_push_back(action->onlyshowin, strdup(desktop)); } } else if (strcmp(key, "NotShowIn") == 0) { for (const char *desktop = strtok_r(value, ";", &ctx); desktop != NULL; desktop = strtok_r(NULL, ";", &ctx)) { tll_push_back(action->notshowin, strdup(desktop)); } } else if (strcmp(key, "Icon") == 0) { free(action->icon); action->icon = strdup(value); } else if (strcmp(key, "Hidden") == 0 || strcmp(key, "NoDisplay") == 0) { if (strcmp(value, "true") == 0) action->visible = false; } else if (strcmp(key, "Terminal") == 0) { if (strcmp(value, "true") == 0) action->use_terminal = true; } } free(line); } fclose(f); tll_foreach(actions, it) { struct action *a = &it->item; if (!(is_desktop_entry && a->name != NULL && a->exec != NULL)) { free(a->name); free(a->generic_name); free(a->app_id); free(a->comment); free(a->icon); free(a->exec); free(a->wexec); free(a->path); tll_free_and_free(a->keywords, free); tll_free_and_free(a->categories, free); tll_free_and_free(a->onlyshowin, free); tll_free_and_free(a->notshowin, free); continue; } if (a->use_terminal && terminal != NULL) { char *exec_with_terminal = malloc( strlen(terminal) + 1 + strlen(a->exec) + 1); strcpy(exec_with_terminal, terminal); strcat(exec_with_terminal, " "); strcat(exec_with_terminal, a->exec); free(a->exec); a->exec = exec_with_terminal; } char32_t *title = a->name; if (a != default_action) { size_t title_len = c32len(default_action->name) + 3 + /* “ - “ */ c32len(a->name) + 1; title = malloc(title_len * sizeof(char32_t)); c32cpy(title, default_action->name); c32cat(title, U" - "); c32cat(title, a->name); free(a->name); } tll_push_back( *applications, ((struct application){ .id = strdup(id), .path = a->path, .exec = a->exec, .basename = c32dup(file_basename), .wexec = a->wexec, .title = title, .app_id = a->app_id, .generic_name = a->generic_name, .comment = a->comment, .keywords = a->keywords, .categories = a->categories, .icon = {.name = a->icon}, .visible = a->visible && (!filter_desktops || filter_desktop_entry(a, desktops)), .count = 0})); } free(id); tll_free(actions); tll_free_and_free(action_names, free); } static char * new_id(const char *base_id, const char *new_part) { if (base_id == NULL) return strdup(new_part); size_t len = strlen(base_id) + 1 + strlen(new_part) + 1; char *id = malloc(len); strcpy(id, base_id); strcat(id, "-"); strcat(id, new_part); return id; } static void scan_dir(int base_fd, const char *terminal, bool include_actions, bool filter_desktop, char_list_t *desktops, application_llist_t *applications, const char *base_id) { DIR *d = fdopendir(base_fd); if (d == NULL) { LOG_ERRNO("failed to open directory"); return; } const char *locale = setlocale(LC_MESSAGES, NULL); struct locale_variants lc_messages = {NULL}; if (locale != NULL) { char *copy = strdup(locale); char *lang = copy; char *country_start = strchr(copy, '_'); char *encoding_start = strchr(copy, '.'); char *modifier_start = strchr(copy, '@'); if (country_start != NULL) *country_start = '\0'; if (encoding_start != NULL) *encoding_start = '\0'; if (modifier_start != NULL) *modifier_start = '\0'; lc_messages.lang = copy; if (country_start != NULL) { if (asprintf( &lc_messages.lang_country, "%s_%s", lang, country_start + 1) < 0) { LOG_WARN( "failed to construct lang_country string from %s and %s: %s", lang, country_start + 1, strerror(errno)); } } if (modifier_start != NULL) { if (asprintf( &lc_messages.lang_modifier, "%s@%s", lang, modifier_start + 1) < 0) { LOG_WARN( "failed to construct lang@modifier string from %s and %s: %s", lang, modifier_start + 1, strerror(errno)); } } if (country_start != NULL && modifier_start != NULL) { if (asprintf( &lc_messages.lang_country_modifier, "%s_%s@%s", lang, country_start + 1, modifier_start + 1) < 0) { LOG_WARN( "failed to construct lang_country@modifier string from %s, %s and %s: %s", lang, country_start + 1, modifier_start + 1, strerror(errno)); } } LOG_DBG("lang=%s, lang_country=%s, lang@modifier=%s, lang_country@modifier=%s", lc_messages.lang, lc_messages.lang_country, lc_messages.lang_modifier, lc_messages.lang_country_modifier); } for (const struct dirent *e = readdir(d); e != NULL; e = readdir(d)) { if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue; struct stat st; if (fstatat(base_fd, e->d_name, &st, 0) == -1) { LOG_WARN("%s: failed to stat", e->d_name); continue; } if (S_ISDIR(st.st_mode)) { int dir_fd = openat(base_fd, e->d_name, O_RDONLY); if (dir_fd == -1) { LOG_ERRNO("%s: failed to open", e->d_name); continue; } char *nested_base_id = new_id(base_id, e->d_name); scan_dir(dir_fd, terminal, include_actions, filter_desktop, desktops, applications, nested_base_id); free(nested_base_id); close(dir_fd); } else if (S_ISREG(st.st_mode)) { /* Skip files not ending with ".desktop" */ const size_t desktop_len = strlen(".desktop"); const size_t name_len = strlen(e->d_name); if (name_len < desktop_len) continue; if (strcmp(&e->d_name[name_len - desktop_len], ".desktop") != 0) continue; //LOG_DBG("%s", e->d_name); int fd = openat(base_fd, e->d_name, O_RDONLY); if (fd == -1) LOG_WARN("%s: failed to open", e->d_name); else { const char *file_basename = strrchr(e->d_name, '/'); if (file_basename == NULL) file_basename = e->d_name; else file_basename++; const char *extension = strchr(file_basename, '.'); if (extension == NULL) extension = file_basename + strlen(file_basename); int chars = mbsntoc32(NULL, file_basename, extension - file_basename, 0); assert(chars >= 0); char32_t wfile_basename[chars + 1]; mbsntoc32(wfile_basename, file_basename, extension - file_basename, chars); wfile_basename[chars] = U'\0'; char *id = new_id(base_id, e->d_name); bool already_added = false; tll_foreach(*applications, it) { if (strcmp(it->item.id, id) == 0) { already_added = true; break; } } if (!already_added) { parse_desktop_file( fd, id, wfile_basename, terminal, include_actions, filter_desktop, desktops, &lc_messages, applications); } else free(id); close(fd); } } } free(lc_messages.lang); free(lc_messages.lang_country); free(lc_messages.lang_modifier); free(lc_messages.lang_country_modifier); closedir(d); } static int sort_application_by_title(const void *_a, const void *_b) { const struct application *a = _a; const struct application *b = _b; return c32cmp(a->title, b->title); } void xdg_find_programs(const char *terminal, bool include_actions, bool filter_desktop, char_list_t *desktops, struct application_list *applications) { application_llist_t apps = tll_init(); xdg_data_dirs_t dirs = xdg_data_dirs(); tll_foreach(dirs, it) { char path[strlen(it->item.path) + 1 + strlen("applications") + 1]; sprintf(path, "%s/applications", it->item.path); int fd = open(path, O_RDONLY); if (fd != -1) { scan_dir(fd, terminal, include_actions, filter_desktop, desktops, &apps, NULL); close(fd); } } applications->count = tll_length(apps); applications->v = malloc(tll_length(apps) * sizeof(applications->v[0])); size_t i = 0; tll_foreach(apps, it) { applications->v[i++] = it->item; tll_remove(apps, it); } tll_free(apps); qsort(applications->v, applications->count, sizeof(applications->v[0]), &sort_application_by_title); xdg_data_dirs_destroy(dirs); #if defined(_DEBUG) && LOG_ENABLE_DBG && 0 for (size_t i = 0; i < applications->count; i++) { const struct application *app = &applications->v[i]; char32_t keywords[1024]; char32_t categories[1024]; int idx = 0; tll_foreach(app->keywords, it) { idx += swprintf(&keywords[idx], (sizeof(keywords) / sizeof(keywords[0])) - idx, L"%ls, ", (const wchar_t *)it->item); } if (idx > 0) keywords[idx - 2] = U'\0'; idx = 0; tll_foreach(app->categories, it) { idx += swprintf(&categories[idx], (sizeof(categories) / sizeof(categories[0])) - idx, L"%ls, ", (const wchar_t *)it->item); } if (idx > 0) categories[idx - 2] = U'\0'; LOG_DBG("%s:\n" " name/title: %ls\n" " path: %s\n" " exec: %s\n" " basename: %ls\n" " generic-name: %ls\n" " comment: %ls\n" " keywords: %ls\n" " categories: %ls\n" " icon:\n" " name: %s\n" " type: %s", app->id, (const wchar_t *)app->title, app->path, app->exec, (const wchar_t *)app->basename, (const wchar_t *)app->generic_name, (const wchar_t *)app->comment, (const wchar_t *)keywords, (const wchar_t *)categories, app->icon.name, app->icon.type == ICON_PNG ? "PNG" : app->icon.type == ICON_SVG ? "SVG" : "<none>"); } #endif } xdg_data_dirs_t xdg_data_dirs(void) { xdg_data_dirs_t ret = tll_init(); const char *xdg_data_home = getenv("XDG_DATA_HOME"); if (xdg_data_home != NULL && xdg_data_home[0] != '\0') { int fd = open(xdg_data_home, O_RDONLY | O_DIRECTORY); if (fd >= 0) { struct xdg_data_dir d = {.fd = fd, .path = strdup(xdg_data_home)}; tll_push_back(ret, d); } } else { static const char *const local = ".local/share"; const struct passwd *pw = getpwuid(getuid()); char *path = malloc(strlen(pw->pw_dir) + 1 + strlen(local) + 1); sprintf(path, "%s/%s", pw->pw_dir, local); int fd = open(path, O_RDONLY | O_DIRECTORY); if (fd >= 0) { struct xdg_data_dir d = {.fd = fd, .path = path}; tll_push_back(ret, d); } else free(path); } const char *_xdg_data_dirs = getenv("XDG_DATA_DIRS"); if (_xdg_data_dirs != NULL) { char *ctx = NULL; char *copy = strdup(_xdg_data_dirs); for (const char *tok = strtok_r(copy, ":", &ctx); tok != NULL; tok = strtok_r(NULL, ":", &ctx)) { int fd = open(tok, O_RDONLY | O_DIRECTORY); if (fd >= 0) { struct xdg_data_dir d = {.fd = fd, .path = strdup(tok)}; tll_push_back(ret, d); } } free(copy); } else { int fd1 = open("/usr/local/share", O_RDONLY | O_DIRECTORY); int fd2 = open("/usr/share", O_RDONLY | O_DIRECTORY); if (fd1 >= 0) { struct xdg_data_dir d = {.fd = fd1, .path = strdup("/usr/local/share")}; tll_push_back(ret, d); } if (fd2 >= 0) { struct xdg_data_dir d = {.fd = fd2, .path = strdup("/usr/share")}; tll_push_back(ret, d);; } } return ret; } void xdg_data_dirs_destroy(xdg_data_dirs_t dirs) { tll_foreach(dirs, it) { close(it->item.fd); free(it->item.path); tll_remove(dirs, it); } } const char * xdg_cache_dir(void) { const char *xdg_cache_home = getenv("XDG_CACHE_HOME"); if (xdg_cache_home != NULL && xdg_cache_home[0] != '\0') return xdg_cache_home; static char path[PATH_MAX]; const struct passwd *pw = getpwuid(getuid()); snprintf(path, sizeof(path), "%s/.cache", pw->pw_dir); return path; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������fuzzel/xdg.h����������������������������������������������������������������������������������������0000664�0000000�0000000�00000000725�14454170012�0013332�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include "application.h" #include "icon.h" #include "tllist.h" struct xdg_data_dir { char *path; int fd; }; typedef tll(struct xdg_data_dir) xdg_data_dirs_t; xdg_data_dirs_t xdg_data_dirs(void); void xdg_data_dirs_destroy(xdg_data_dirs_t dirs); const char *xdg_cache_dir(void); void xdg_find_programs( const char *terminal, bool include_actions, bool filter_desktop, char_list_t *desktops, struct application_list *applications); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������