├── .clang-tidy ├── icon-missing.png ├── subprojects ├── nlohmann_json.wrap └── gtk-layer-shell-0.wrap ├── nwgconfig.h.in ├── dmenu ├── style.css ├── meson.build ├── help.h.in ├── dmenu.h ├── dmenu_tools.cc ├── dmenu.cc └── dmenu_classes.cc ├── common ├── meson.build ├── filesystem-compat.h ├── nwg_exceptions.cc ├── log.h ├── charconv-compat.h ├── nwg_exceptions.h ├── time_report.h ├── nwg_tools.h ├── nwg_classes.h ├── nwg_classes.cc └── nwg_tools.cc ├── bar ├── bar.json ├── meson.build ├── style.css ├── bar_tools.cc ├── help.h.in ├── bar.h ├── bar_classes.cc └── bar.cc ├── examples ├── nwgbar │ ├── exit-ob.json │ ├── exit-fvwm.json │ └── exit-i3.json └── icons │ ├── system-log-out.svg │ ├── system-shutdown.svg │ ├── system-reboot.svg │ └── system-lock-screen.svg ├── .gitignore ├── meson_options.txt ├── grid ├── grid.conf ├── meson.build ├── style.css ├── help.h.in ├── grid_tools.cc ├── grid_client.cc ├── on_desktop_entry.h ├── grid_entries.h ├── grid.cc ├── grid_entries.cc └── grid.h ├── .github └── workflows │ ├── freebsd.yml │ └── ubuntu.yml ├── make_readme.py ├── CONTRIBUTING.md ├── meson.build ├── icon-missing.svg ├── README.md.in └── README.md /.clang-tidy: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon-missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwg-piotr/nwg-launchers/HEAD/icon-missing.png -------------------------------------------------------------------------------- /subprojects/nlohmann_json.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | url = https://github.com/nlohmann/json.git 3 | revision = develop 4 | depth = 1 5 | -------------------------------------------------------------------------------- /subprojects/gtk-layer-shell-0.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | url = https://github.com/wmww/gtk-layer-shell.git 3 | revision = v0.6.0 4 | depth = 1 5 | -------------------------------------------------------------------------------- /nwgconfig.h.in: -------------------------------------------------------------------------------- 1 | #define VERSION_STR "@version@" 2 | #define INSTALL_PREFIX_STR "@prefix@" 3 | #define DATA_DIR_STR "@datadir@" 4 | #mesondefine HAVE_MODERN_NLOHMANN_JSON 5 | -------------------------------------------------------------------------------- /dmenu/style.css: -------------------------------------------------------------------------------- 1 | 2 | box { 3 | /* Uncomment to set vertical margin 4 | margin-top: 30px; 5 | margin-bottom: 30px; 6 | */ 7 | } 8 | 9 | #searchbox { 10 | /* Adjust to your taste */ 11 | } 12 | 13 | /* Menu items */ 14 | #commands { 15 | padding-left: 5px 16 | } 17 | -------------------------------------------------------------------------------- /common/meson.build: -------------------------------------------------------------------------------- 1 | sources = files( 2 | 'nwg_tools.cc', 3 | 'nwg_classes.cc', 4 | 'nwg_exceptions.cc' 5 | ) 6 | 7 | nwg_inc = include_directories('.') 8 | 9 | nwg = static_library( 10 | 'nwg', 11 | sources, 12 | dependencies: [json, gdk_x11, gtkmm, gtk_layer_shell], 13 | include_directories: [nwg_conf_inc], 14 | install: false 15 | ) 16 | -------------------------------------------------------------------------------- /common/filesystem-compat.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(__cpp_lib_filesystem) || __has_include() 4 | #include 5 | namespace fs = std::filesystem; 6 | #elif defined(__cpp_lib_experimental_filesystem) || __has_include() 7 | #include 8 | namespace fs = std::experimental::filesystem; 9 | #else 10 | #error "No filesystem support" 11 | #endif 12 | -------------------------------------------------------------------------------- /bar/bar.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Lock screen", 4 | "exec": "swaylock -f -c 000000", 5 | "icon": "system-lock-screen" 6 | }, 7 | { 8 | "name": "Logout", 9 | "exec": "swaymsg exit", 10 | "icon": "system-log-out" 11 | }, 12 | { 13 | "name": "Reboot", 14 | "exec": "systemctl reboot", 15 | "icon": "system-reboot" 16 | }, 17 | { 18 | "name": "Shutdown", 19 | "exec": "systemctl -i poweroff", 20 | "icon": "system-shutdown" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /examples/nwgbar/exit-ob.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Lock", 4 | "exec": "i3lock -c 000000", 5 | "icon": "system-lock-screen" 6 | }, 7 | { 8 | "name": "Logout", 9 | "exec": "openbox --exit", 10 | "icon": "system-log-out" 11 | }, 12 | { 13 | "name": "Reboot", 14 | "exec": "systemctl reboot", 15 | "icon": "system-reboot" 16 | }, 17 | { 18 | "name": "Shutdown", 19 | "exec": "systemctl -i poweroff", 20 | "icon": "system-shutdown" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /examples/nwgbar/exit-fvwm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Lock", 4 | "exec": "i3lock-fancy-rapid 5 3", 5 | "icon": "system-lock-screen" 6 | }, 7 | { 8 | "name": "Logout", 9 | "exec": "killall fvwm", 10 | "icon": "system-log-out" 11 | }, 12 | { 13 | "name": "Reboot", 14 | "exec": "systemctl reboot", 15 | "icon": "system-reboot" 16 | }, 17 | { 18 | "name": "Shutdown", 19 | "exec": "systemctl -i poweroff", 20 | "icon": "system-shutdown" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /examples/nwgbar/exit-i3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Lock screen", 4 | "exec": "i3lock -c 000000", 5 | "icon": "system-lock-screen" 6 | }, 7 | { 8 | "name": "Logout", 9 | "exec": "i3-msg exit", 10 | "icon": "system-log-out" 11 | }, 12 | { 13 | "name": "Reboot", 14 | "exec": "systemctl reboot", 15 | "icon": "system-reboot" 16 | }, 17 | { 18 | "name": "Shutdown", 19 | "exec": "systemctl -i poweroff", 20 | "icon": "system-shutdown" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /bar/meson.build: -------------------------------------------------------------------------------- 1 | 2 | configure_file(input: 'help.h.in', output: 'help.h', configuration: conf_data) 3 | 4 | sources = files( 5 | 'bar.cc', 6 | 'bar_classes.cc', 7 | 'bar_tools.cc' 8 | ) 9 | 10 | bar_exe = executable( 11 | 'nwgbar', 12 | sources, 13 | dependencies: [json, gtkmm, gtk_layer_shell], 14 | link_with: nwg, 15 | include_directories: [nwg_inc, nwg_conf_inc], 16 | install: true 17 | ) 18 | 19 | install_data( 20 | ['style.css', 'bar.json'], 21 | install_dir: conf_data.get('datadir', '') / 'nwgbar' 22 | ) 23 | -------------------------------------------------------------------------------- /dmenu/meson.build: -------------------------------------------------------------------------------- 1 | configure_file(input: 'help.h.in', output: 'help.h', configuration: conf_data) 2 | 3 | sources = files( 4 | 'dmenu.cc', 5 | 'dmenu_classes.cc', 6 | 'dmenu_tools.cc' 7 | ) 8 | 9 | dmenu_exe = executable( 10 | 'nwgdmenu', 11 | sources, 12 | dependencies: [json, gtkmm, gtk_layer_shell], 13 | link_with: nwg, 14 | include_directories: [nwg_inc, nwg_conf_inc], 15 | install: true 16 | ) 17 | 18 | install_data( 19 | ['style.css'], 20 | install_dir: conf_data.get('datadir', '') / 'nwgdmenu' 21 | ) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Project files 35 | *.geany 36 | *.kdev4 37 | 38 | subprojects/nlohmann_json 39 | subprojects/gtk-layer-shell-0 40 | builddir 41 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('bar', type: 'boolean', value: true, description: 'Build the bar app.') 2 | option('dmenu', type: 'boolean', value: true, description: 'Build the dmenu app.') 3 | option('grid', type: 'boolean', value: true, description: 'Build the grid app.') 4 | option('layer-shell', type: 'feature', value: 'auto', description: 'Enable layer-shell support') 5 | option('gdk-x11', type: 'feature', value: 'auto', description: 'Use Gdk X11 API') 6 | option('generate-readme', type: 'boolean', value: false, description: 'Generate fresh README.md in build directory') 7 | -------------------------------------------------------------------------------- /bar/style.css: -------------------------------------------------------------------------------- 1 | #bar { 2 | margin: 30px /* affects top/bottom & left/right alignment */ 3 | } 4 | 5 | button, image { 6 | background: none; 7 | border-style: none; 8 | box-shadow: none; 9 | color: #999; 10 | text-shadow: none; 11 | } 12 | 13 | button { 14 | padding-left: 10px; 15 | padding-right: 10px; 16 | margin: 5px; 17 | -gtk-icon-shadow: none; 18 | } 19 | 20 | button:hover { 21 | background-color: rgba (255, 255, 255, 0.1) 22 | } 23 | 24 | button:focus { 25 | box-shadow: 0 0 2px; 26 | } 27 | 28 | grid { 29 | /* e.g. for common background to all buttons */ 30 | } 31 | -------------------------------------------------------------------------------- /grid/grid.conf: -------------------------------------------------------------------------------- 1 | { 2 | "categories": { 3 | "AudioVideo" : "Multimedia 📀", 4 | "Development" : "Development 💻", 5 | "Education" : "Education 🎓", 6 | "Game" : "Games 🎮", 7 | "Graphics" : "Graphics 🎨", 8 | "Network" : "Network 🌎", 9 | "Office" : "Office 💼", 10 | "Science" : "Science 🔬", 11 | "Settings" : "Settings ⚙️", 12 | "System" : "System 🖥️", 13 | "Utility" : "Utility 🛠️" 14 | }, 15 | "favorites" : false, 16 | "pins" : false, 17 | "columns" : 6, 18 | "icon-size" : 72, 19 | "no-categories": false 20 | } 21 | 22 | -------------------------------------------------------------------------------- /common/nwg_exceptions.cc: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Exception classes for nwg-launchers 4 | * Copyright (c) 2021 Piotr Miller 5 | * e-mail: nwg.piotr@gmail.com 6 | * Website: http://nwg.pl 7 | * Project: https://github.com/nwg-piotr/nwg-launchers 8 | * License: GPL3 9 | * */ 10 | 11 | 12 | #include 13 | #include 14 | #include "nwg_exceptions.h" 15 | 16 | std::string error_description(int err) { 17 | errno = 0; 18 | auto cstr = std::strerror(err); 19 | if (!cstr || errno) { 20 | throw std::runtime_error{ "failed to retrieve errno description: strerror return NULL" }; 21 | } 22 | return { cstr }; 23 | } 24 | -------------------------------------------------------------------------------- /common/log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace Log { 5 | template 6 | void write(std::ostream& out, Ts && ... ts) { 7 | ((out << ts), ...); 8 | out << '\n'; 9 | } 10 | 11 | template 12 | void info(Ts && ... ts) { write(std::cerr, "INFO: ", std::forward(ts)...); } 13 | template 14 | void warn(Ts && ... ts) { write(std::cerr, "WARN: ", std::forward(ts)...); } 15 | template 16 | void error(Ts && ... ts) { write(std::cerr, "ERROR: ", std::forward(ts)...); } 17 | template 18 | void plain(Ts && ... ts) { write(std::cerr, std::forward(ts)...); } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/freebsd.yml: -------------------------------------------------------------------------------- 1 | name: freebsd 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.cpp" 7 | - "**.h" 8 | - "**.yml" 9 | - "**.build" 10 | pull_request: 11 | paths: 12 | - "**.cpp" 13 | - "**.h" 14 | - "**.yml" 15 | - "**.build" 16 | 17 | jobs: 18 | clang: 19 | runs-on: macos-12 # until https://github.com/actions/runner/issues/385 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Test in FreeBSD VM 23 | uses: vmactions/freebsd-vm@v0 24 | with: 25 | usesh: true 26 | prepare: | 27 | pkg install -y meson pkgconf gtkmm30 nlohmann-json 28 | pkg install -y gtk-layer-shell 29 | run: | 30 | meson setup _build 31 | meson compile -C _build 32 | -------------------------------------------------------------------------------- /common/charconv-compat.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #if defined (__cpp_lib_to_chars) || __has_include() 5 | #include 6 | 7 | #define HAVE_TO_CHARS 8 | 9 | template 10 | bool parse_number(std::string_view source, T& number) { 11 | auto from = source.data(); 12 | auto to = from + source.size(); 13 | if (auto [p, ec] = std::from_chars(from, to, number); ec == std::errc()) { 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | #else 20 | #include 21 | 22 | template 23 | bool parse_number(std::string_view source, T& number) { 24 | std::stringstream stream; 25 | stream << source; 26 | if (stream >> number) { 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /grid/meson.build: -------------------------------------------------------------------------------- 1 | 2 | configure_file(input: 'help.h.in', output: 'help.h', configuration: conf_data) 3 | 4 | sources = files( 5 | 'grid.cc', 6 | 'grid_classes.cc', 7 | 'grid_tools.cc', 8 | 'grid_entries.cc' 9 | ) 10 | 11 | grid_server_exe = executable( 12 | 'nwggrid', 13 | files('grid_client.cc', 'grid_classes.cc', 'grid_tools.cc'), 14 | dependencies: [json, gtkmm, gtk_layer_shell], 15 | link_with: nwg, 16 | include_directories: [nwg_inc, nwg_conf_inc], 17 | install: true 18 | ) 19 | 20 | grid_client_exe = executable( 21 | 'nwggrid-server', 22 | sources, 23 | dependencies: [json, gtkmm, gtk_layer_shell], 24 | link_with: nwg, 25 | include_directories: [nwg_inc, nwg_conf_inc], 26 | install: true 27 | ) 28 | 29 | install_data( 30 | ['style.css', 'grid.conf'], 31 | install_dir: conf_data.get('datadir', '') / 'nwggrid' 32 | ) 33 | -------------------------------------------------------------------------------- /bar/bar_tools.cc: -------------------------------------------------------------------------------- 1 | /* GTK-based application grid 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | 9 | #include "bar.h" 10 | 11 | /* 12 | * Returns a vector of BarEntry data structs 13 | * */ 14 | std::vector get_bar_entries(ns::json&& bar_json) { 15 | // read from json object 16 | std::vector entries {}; 17 | for (auto&& json : bar_json) { 18 | auto && entry = entries.emplace_back( 19 | std::move(json.at("name")), 20 | std::move(json.at("exec")), 21 | std::move(json.at("icon")) 22 | ); 23 | if (auto iter = json.find("class"); iter != json.end()) { 24 | entry.css_class = *iter; 25 | } 26 | } 27 | return entries; 28 | } 29 | -------------------------------------------------------------------------------- /grid/style.css: -------------------------------------------------------------------------------- 1 | button, label, image { 2 | background: none; 3 | border-style: none; 4 | box-shadow: none; 5 | color: #999; 6 | } 7 | 8 | button { 9 | padding: 5px; 10 | margin: 5px; 11 | text-shadow: none; 12 | } 13 | 14 | button:hover { 15 | background-color: rgba (255, 255, 255, 0.1); 16 | } 17 | 18 | button:focus { 19 | box-shadow: 0 0 10px; 20 | } 21 | 22 | button:checked { 23 | background-color: rgba (255, 255, 255, 0.1); 24 | } 25 | 26 | #searchbox { 27 | background: none; 28 | border-color: #999; 29 | color: #ccc; 30 | margin-top: 20px; 31 | margin-bottom: 20px 32 | } 33 | 34 | #separator { 35 | background-color: rgba(200, 200, 200, 0.5); 36 | margin-left: 500px; 37 | margin-right: 500px; 38 | margin-top: 10px; 39 | margin-bottom: 10px 40 | } 41 | 42 | #description { 43 | margin-bottom: 20px 44 | } 45 | -------------------------------------------------------------------------------- /make_readme.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | 6 | src = sys.argv[1] 7 | tgt = sys.argv[2] 8 | 9 | exes = sys.argv[3:] 10 | 11 | def get_help(exe): 12 | result = subprocess.run([exe, '-h'], capture_output=True) 13 | assert result.returncode == 0 14 | return result.stderr.decode(sys.stdout.encoding) 15 | 16 | 17 | data = { 18 | 'BAR': get_help(exes[0]), 19 | 'DMENU': get_help(exes[1]), 20 | 'GRID_CLIENT': get_help(exes[2]), 21 | 'GRID_SERVER': get_help(exes[3]) 22 | } 23 | 24 | with open(src) as readme_in, open(tgt, 'w') as readme: 25 | for line in readme_in: 26 | for k,v in data.items(): 27 | placeholder = 'HELP_OUTPUT_FOR_' + k 28 | line = line.replace(placeholder, v) 29 | readme.write(line) 30 | 31 | print('Successfully generated README, do not forget to copy it to the source root') 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to nwg-launchers 2 | 3 | ## Pull requests 4 | 5 | This project has already been significantly improved by skilled programmers, and I do appreciate the fact. To keep things in the proper order, however, 6 | please consider the priority list: 7 | 8 | 1. bug fixes 9 | 2. new features 10 | 3. optimization, refactoring, e.t.c. 11 | 12 | Please test your changes not in sway only, but also in i3 and Openbox. 13 | 14 | Before submitting a major PR, it makes sense to open an issue, and discuss the changes you're planning on. 15 | 16 | ### Code formatting 17 | 18 | Please use the [Google Style Guide](https://google.github.io/styleguide/cppguide.html#Formatting), especially if it comes to braces in Conditionals, 19 | Loops and Switch statements. 80 characters in a line is all right, up to 120 is acceptable. 20 | 21 | ## Issues and Feature requests 22 | 23 | ...are the only feedback we have. Do not hesitate to submit. 24 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: ubuntu 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.cpp" 7 | - "**.h" 8 | - "**.yml" 9 | - "**.build" 10 | pull_request: 11 | paths: 12 | - "**.cpp" 13 | - "**.h" 14 | - "**.yml" 15 | - "**.build" 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Update repositories 23 | run: sudo apt update 24 | - name: Install dependencies 25 | run: sudo apt install meson pkgconf libgtkmm-3.0-dev nlohmann-json3-dev libgtk-layer-shell-dev clang gcc gobject-introspection libgirepository1.0-dev libwayland-dev 26 | - name: Setup clang build 27 | env: 28 | CXX: clang++ 29 | run: meson setup clang_build 30 | - name: Setup gcc build 31 | env: 32 | CXX: g++ 33 | run: meson setup gcc_build 34 | - name: Build with clang 35 | run: ninja -C clang_build 36 | - name: Build with gcc 37 | run: ninja -C gcc_build 38 | -------------------------------------------------------------------------------- /bar/help.h.in: -------------------------------------------------------------------------------- 1 | 2 | namespace bar { 3 | 4 | const char* const HELP_MESSAGE = 5 | "GTK button bar: nwgbar @version@ (c) Piotr Miller & Contributors 2022\n\n\ 6 | Options:\n\ 7 | -h show this help message and exit\n\ 8 | -v arrange buttons vertically\n\ 9 | -ha | horizontal alignment left/right (default: center)\n\ 10 | -va | vertical alignment top/bottom (default: middle)\n\ 11 | -t template file name (default: bar.json)\n\ 12 | -c css file name (default: style.css)\n\ 13 | -o background opacity (0.0 - 1.0, default 0.9)\n\ 14 | -b background colour in RRGGBB or RRGGBBAA format (RRGGBBAA alpha overrides )\n\ 15 | -s button image size (default: 72)\n\ 16 | -g GTK theme name\n\ 17 | -wm window manager name (if can not be detected)\n\n\ 18 | [requires layer-shell]:\n\ 19 | -layer-shell-layer {BACKGROUND,BOTTOM,TOP,OVERLAY}, default: OVERLAY\n\ 20 | -layer-shell-exclusive-zone {auto, valid integer (usually -1 or 0)}, default: auto\n"; 21 | 22 | } // namespace bar 23 | -------------------------------------------------------------------------------- /common/nwg_exceptions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* 4 | * Exception classes for nwg-launchers 5 | * Copyright (c) 2021 Piotr Miller 6 | * e-mail: nwg.piotr@gmail.com 7 | * Website: http://nwg.pl 8 | * Project: https://github.com/nwg-piotr/nwg-launchers 9 | * License: GPL3 10 | * */ 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | // wraps strerror into std::string 17 | std::string error_description(int err); 18 | 19 | struct ErrnoException: public std::runtime_error { 20 | static inline std::string concat(std::string_view a, std::string b) { 21 | b.insert(0, a.data(), a.size()); 22 | return b; 23 | } 24 | /* Make sure to use this constructor as follows: 25 | * 26 | * int err = errno; 27 | * ErrnoException{ "desc", err } 28 | * 29 | * and NOT 30 | * 31 | * ErrnoException{ "desc", errno } 32 | * 33 | * because creating `string_view` can potentially change `errno` value, 34 | * and the initialization order is not guaranteed */ 35 | ErrnoException(std::string_view desc, int err): 36 | std::runtime_error{ concat(desc, error_description(err)) } 37 | { 38 | // intentionally left blank 39 | } 40 | ErrnoException(int err): std::runtime_error{ error_description(err) } { 41 | // intentionally left blank 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /dmenu/help.h.in: -------------------------------------------------------------------------------- 1 | 2 | #define STR_EXPAND(x) #x 3 | #define STR(x) STR_EXPAND(x) 4 | 5 | namespace dmenu { 6 | 7 | const char* const HELP_MESSAGE = 8 | "GTK dynamic menu: nwgdmenu @version@ (c) Piotr Miller & Contributors 2022\n\n\ 9 | | nwgdmenu - displays newline-separated stdin input as a GTK menu\n\ 10 | nwgdmenu - creates a GTK menu out of commands found in $PATH\n\n\ 11 | Options:\n\ 12 | -h show this help message and exit\n\ 13 | -n no search box\n\ 14 | -ha | horizontal alignment left/right (default: center)\n\ 15 | -va | vertical alignment top/bottom (default: middle)\n\ 16 | -r number of rows (default: " STR(ROWS_DEFAULT) ")\n\ 17 | -c css file name (default: style.css)\n\ 18 | -o background opacity (0.0 - 1.0, default 0.3)\n\ 19 | -b background colour in RRGGBB or RRGGBBAA format (RRGGBBAA alpha overrides )\n\ 20 | -g GTK theme name\n\ 21 | -wm window manager name (if can not be detected)\n\ 22 | -run ignore stdin, always build from commands in $PATH\n\n\ 23 | [requires layer-shell]:\n\ 24 | -layer-shell-layer {BACKGROUND,BOTTOM,TOP,OVERLAY}, default: OVERLAY\n\ 25 | -layer-shell-exclusive-zone {auto, valid integer (usually -1 or 0)}, default: auto\n\n\ 26 | Hotkeys:\n\ 27 | Delete clear search box\n\ 28 | Insert switch case sensitivity\n"; 29 | 30 | } // namespace dmenu 31 | -------------------------------------------------------------------------------- /dmenu/dmenu.h: -------------------------------------------------------------------------------- 1 | /* GTK-based dmenu 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | 9 | #pragma once 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include "filesystem-compat.h" 16 | #include "nwgconfig.h" 17 | #include "nwg_classes.h" 18 | 19 | #ifndef ROWS_DEFAULT 20 | #define ROWS_DEFAULT 20 // used in dmenu.cc/HELP_MESSAGE, don't turn into variable 21 | #endif 22 | 23 | struct DmenuConfig: public Config { 24 | DmenuConfig(const InputParser& parser, const Glib::RefPtr& screen); 25 | 26 | fs::path settings_file; 27 | int rows{ ROWS_DEFAULT }; // number of menu items to display 28 | bool dmenu_run{ true }; 29 | bool show_searchbox{ true }; 30 | bool case_sensitive{ true }; 31 | }; 32 | 33 | class DmenuWindow : public PlatformWindow { 34 | public: 35 | DmenuWindow(DmenuConfig&, std::vector&); 36 | ~DmenuWindow(); 37 | void emplace_back(const Glib::ustring&); 38 | 39 | int get_height() override; 40 | private: 41 | void filter_view(); 42 | void select_first_item(); 43 | void switch_case_sensitivity(); 44 | 45 | bool on_key_press_event(GdkEventKey*) override; 46 | 47 | Gtk::SearchEntry searchbox; 48 | Gtk::ListViewText commands; 49 | Gtk::VBox vbox; 50 | std::vector& commands_source; 51 | bool case_sensitivity_changed = false; 52 | DmenuConfig& config; 53 | }; 54 | 55 | /* 56 | * Function declarations 57 | * */ 58 | std::vector get_commands_list(const DmenuConfig& config); 59 | fs::path get_settings_path(); 60 | -------------------------------------------------------------------------------- /bar/bar.h: -------------------------------------------------------------------------------- 1 | /* GTK-based button bar 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include // nlohmann-json package 14 | 15 | #include "nwgconfig.h" 16 | #include "nwg_classes.h" 17 | 18 | namespace ns = nlohmann; 19 | 20 | enum class Orientation: unsigned int { Horizontal = 0, Vertical }; 21 | 22 | struct BarConfig: public Config { 23 | int icon_size{ 72 }; 24 | Orientation orientation{ Orientation::Horizontal }; 25 | fs::path definition_file{ "bar.json" }; 26 | BarConfig(const InputParser& parser, const Glib::RefPtr& screen); 27 | }; 28 | 29 | class BarBox : public AppBox { 30 | public: 31 | BarBox(Glib::ustring, Glib::ustring, Glib::ustring); 32 | bool on_button_press_event(GdkEventButton*) override; 33 | void on_activate() override; 34 | }; 35 | 36 | class BarWindow : public PlatformWindow { 37 | public: 38 | BarWindow(Config&); 39 | 40 | Gtk::ScrolledWindow scrolled_window; 41 | Gtk::VBox outer_box; 42 | Gtk::HBox inner_hbox; 43 | Gtk::Grid grid; // Buttons grid 44 | Gtk::Separator separator; // between favs and all apps 45 | std::vector boxes {}; // attached to favs_grid 46 | 47 | private: 48 | //Override default signal handler: 49 | bool on_button_press_event(GdkEventButton* button) override; 50 | bool on_key_press_event(GdkEventKey* event) override; 51 | }; 52 | 53 | struct BarEntry { 54 | std::string name; 55 | std::string exec; 56 | std::string icon; 57 | std::string css_class; 58 | BarEntry(std::string, std::string, std::string); 59 | }; 60 | 61 | /* 62 | * Function declarations 63 | * */ 64 | std::vector get_bar_entries(ns::json&&); 65 | -------------------------------------------------------------------------------- /grid/help.h.in: -------------------------------------------------------------------------------- 1 | 2 | namespace grid { 3 | 4 | namespace server { 5 | 6 | const char* const HELP_MESSAGE = 7 | "GTK application grid: nwggrid @version@ (c) 2022 Piotr Miller, Sergey Smirnykh & Contributors \n\n\ 8 | \ 9 | Options:\n\ 10 | -h show this help message and exit\n\ 11 | -f display favourites (most used entries); does not work with -d\n\ 12 | -p display pinned entries; does not work with -d \n\ 13 | -d look for .desktop files in custom paths (-d '/my/path1:/my/another path:/third/path') \n\ 14 | -o default (black) background opacity (0.0 - 1.0, default 0.9)\n\ 15 | -b background colour in RRGGBB or RRGGBBAA format (RRGGBBAA alpha overrides )\n\ 16 | -n number of grid columns (default: 6)\n\ 17 | -s button image size (default: 72)\n\ 18 | -c css file name (default: style.css)\n\ 19 | -l force use of language\n\ 20 | -g GTK theme name\n\ 21 | -wm window manager name (if can not be detected)\n\ 22 | -no-categories disable categories display\n\ 23 | -oneshot run in the foreground, exit when window is closed\n\ 24 | generally you should not use this option, use simply `nwggrid` instead\n\ 25 | [requires layer-shell]:\n\ 26 | -layer-shell-layer {BACKGROUND,BOTTOM,TOP,OVERLAY}, default: OVERLAY\n\ 27 | -layer-shell-exclusive-zone {auto, valid integer (usually -1 or 0)}, default: auto\n"; 28 | 29 | } // namespace server 30 | 31 | namespace client { 32 | 33 | const char* const HELP_MESSAGE = "\ 34 | GTK application grid: nwggrid " VERSION_STR " (c) 2021 Piotr Miller, Sergey Smirnykh & Contributors \n\n\ 35 | Usage:\n\ 36 | nwggrid -client sends -SIGUSR1 to nwggrid-server, requires nwggrid-server running\n\ 37 | nwggrid [ARGS...] launches nwggrid-server -oneshot ARGS...\n\n\ 38 | \ 39 | See also:\n\ 40 | nwggrid-server -h\n"; 41 | 42 | } // namespace client 43 | 44 | } // namespace grid 45 | -------------------------------------------------------------------------------- /examples/icons/system-log-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 56 | 61 | 62 | -------------------------------------------------------------------------------- /examples/icons/system-shutdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 56 | 60 | 64 | 65 | -------------------------------------------------------------------------------- /common/time_report.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Classes for nwg-launchers 3 | * Copyright (c) 2021 Érico Nogueira 4 | * e-mail: ericonr@disroot.org 5 | * Copyright (c) 2021 Piotr Miller 6 | * e-mail: nwg.piotr@gmail.com 7 | * Website: http://nwg.pl 8 | * Project: https://github.com/nwg-piotr/nwg-launchers 9 | * License: GPL3 10 | * */ 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | namespace ntime { 17 | 18 | struct Time { 19 | std::string_view name; 20 | timespec time; 21 | Time* next = nullptr; 22 | 23 | Time( std::string_view name ): name{ name } { 24 | if (clock_gettime(CLOCK_MONOTONIC, &time)) { 25 | throw std::runtime_error{ "clock_gettime(CLOCK_MONOTONIC, ...) failed" }; 26 | } 27 | } 28 | 29 | Time( std::string_view name, Time& prev ): Time{ name } { 30 | if (prev.next) { 31 | throw std::logic_error{ "Time::Time(name, prev)" }; 32 | } 33 | prev.next = this; 34 | } 35 | 36 | Time(Time&&) = delete; 37 | Time(const Time&) = delete; 38 | }; 39 | 40 | namespace detail { 41 | 42 | inline size_t to_ms(timespec t) { 43 | return t.tv_sec * 1000 + t.tv_nsec / 1000000; 44 | } 45 | 46 | inline size_t diff_ms(const Time& t1, const Time& t2) { 47 | return to_ms(t2.time) - to_ms(t1.time); 48 | } 49 | 50 | const Time& report(const Time& t1, const Time& t2) { 51 | auto diff = diff_ms(t1, t2); 52 | 53 | Log::plain(t2.name, ": ", diff, "ms"); 54 | if (t2.next) { 55 | return report(t2, *t2.next); 56 | } 57 | return t2; 58 | } 59 | }; // namespace detail 60 | 61 | void report(const Time& initial) { 62 | const Time* last = initial.next; 63 | if (!last) { 64 | throw std::logic_error{ "time::report(initial): inital.next is empty" }; 65 | } 66 | 67 | if (last->next) { 68 | last = &detail::report(initial, *last); 69 | } 70 | 71 | auto diff = detail::diff_ms(initial, *last); 72 | Log::plain("Total: ", diff, "ms"); 73 | } 74 | 75 | } // namespace time_report 76 | -------------------------------------------------------------------------------- /examples/icons/system-reboot.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 56 | 67 | 68 | -------------------------------------------------------------------------------- /dmenu/dmenu_tools.cc: -------------------------------------------------------------------------------- 1 | /* GTK-based dmenu 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "filesystem-compat.h" 14 | #include "nwg_tools.h" 15 | #include "dmenu.h" 16 | #include "log.h" 17 | 18 | /* 19 | * Returns settings cache file path 20 | * */ 21 | fs::path get_settings_path() { 22 | auto full_path = get_cache_home(); 23 | full_path /= "nwg-dmenu-case"; 24 | return full_path; 25 | } 26 | 27 | /* 28 | * Returns all commands paths 29 | * */ 30 | static std::vector list_commands() { 31 | std::vector commands; 32 | if (auto command_dirs_ = getenv("PATH")) { 33 | std::string command_dirs{ command_dirs_ }; 34 | auto paths = split_string(command_dirs, ":"); 35 | std::error_code ec; 36 | for (auto && dir: paths) { 37 | if (fs::is_directory(dir, ec) && !ec) { 38 | for (auto && entry: fs::directory_iterator(dir)) { 39 | auto cmd = take_last_by(entry.path().native(), "/"); 40 | if (cmd.size() > 1 && cmd[0] != '.') { 41 | commands.emplace_back(cmd.data(), cmd.size()); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | return commands; 48 | } 49 | 50 | /* 51 | * Returns list of commands loaded according to config 52 | * */ 53 | std::vector get_commands_list(const DmenuConfig& config) { 54 | std::vector all_commands; 55 | if (config.dmenu_run) { 56 | /* get a list of paths to all commands from all application dirs */ 57 | all_commands = list_commands(); 58 | Log::info(all_commands.size(), " commands found"); 59 | 60 | /* Sort case insensitive */ 61 | std::sort(all_commands.begin(), all_commands.end(), [](auto& a, auto& b) { 62 | return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](auto a, auto b) { 63 | return std::tolower(a) < std::tolower(b); 64 | }); 65 | }); 66 | } else { 67 | for (std::string line; std::getline(std::cin, line);) { 68 | all_commands.emplace_back(std::move(line)); 69 | } 70 | } 71 | return all_commands; 72 | } 73 | -------------------------------------------------------------------------------- /examples/icons/system-lock-screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('nwg-launchers', 'cpp', 2 | default_options: [ 3 | 'cpp_std=c++17', 4 | 'warning_level=3' 5 | ], 6 | license: 'GPL-3.0-or-later', 7 | version: '0.7.1.1' 8 | ) 9 | 10 | compiler = meson.get_compiler('cpp') 11 | 12 | if compiler.has_link_argument('-lc++fs') 13 | add_global_link_arguments('-lc++fs', language: 'cpp') 14 | elif compiler.has_link_argument('-lstdc++fs') 15 | add_global_link_arguments('-lstdc++fs', language: 'cpp') 16 | endif 17 | 18 | # Dependencies 19 | gdk_x11 = dependency('gdk-x11-3.0', required: get_option('gdk-x11')) 20 | 21 | ## gtkmm 22 | gtkmm = dependency('gtkmm-3.0', required: true) 23 | 24 | ## gtk-layer-shell 25 | gtk_layer_shell = dependency( 26 | 'gtk-layer-shell-0', 27 | version: ['>= 0.5.0'], 28 | fallback: ['gtk-layer-shell-0', 'gtk-layer-shell'], 29 | required: get_option('layer-shell') 30 | ) 31 | if gtk_layer_shell.found() 32 | add_project_arguments('-DHAVE_GTK_LAYER_SHELL', language: 'cpp') 33 | endif 34 | 35 | ## nlohmann-json 36 | json = dependency( 37 | 'nlohmann_json', 38 | fallback: ['nlohmann_json', 'nlohmann_json_dep'], 39 | required: true 40 | ) 41 | 42 | # Generate configuration header 43 | conf_data = configuration_data() 44 | conf_data.set('version', meson.project_version()) 45 | conf_data.set('prefix', get_option('prefix')) 46 | conf_data.set('datadir', get_option('prefix') / get_option('datadir') / 'nwg-launchers') 47 | conf_data.set('HAVE_MODERN_NLOHMANN_JSON', json.version().version_compare('>=3.11.0')) 48 | configure_file( 49 | input : 'nwgconfig.h.in', 50 | output : 'nwgconfig.h', 51 | configuration : conf_data 52 | ) 53 | # Include nwgconfig.h 54 | nwg_conf_inc = include_directories('.') 55 | 56 | subdir('common') 57 | 58 | if get_option('bar') 59 | subdir('bar') 60 | endif 61 | 62 | if get_option('dmenu') 63 | subdir('dmenu') 64 | endif 65 | 66 | if get_option('grid') 67 | subdir('grid') 68 | endif 69 | 70 | if get_option('generate-readme') 71 | python = find_program('python3', required: false) 72 | if not python.found() 73 | message('python3 not found in PATH, trying python...') 74 | python = find_program('python', required: true) 75 | endif 76 | 77 | # generate README.md from template 78 | # make sure to copy it to the source directory! 79 | readme = custom_target('readme', 80 | output: [ 'README.md' ], 81 | input: [ 'README.md.in' ], 82 | command: [ 83 | python, '@SOURCE_ROOT@/make_readme.py', 84 | '@INPUT@', '@OUTPUT@', 85 | bar_exe.full_path(), 86 | dmenu_exe.full_path(), 87 | grid_client_exe.full_path(), 88 | grid_server_exe.full_path() 89 | ], 90 | depends: [bar_exe, dmenu_exe, grid_client_exe, grid_server_exe], 91 | install_dir: conf_data.get('datadir'), 92 | install: true 93 | ) 94 | endif 95 | 96 | install_data( 97 | ['icon-missing.svg', 'icon-missing.png'], 98 | install_dir: conf_data.get('datadir') 99 | ) 100 | -------------------------------------------------------------------------------- /dmenu/dmenu.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * GTK-based dmenu 3 | * Copyright (c) 2021 Piotr Miller 4 | * e-mail: nwg.piotr@gmail.com 5 | * Website: http://nwg.pl 6 | * Project: https://github.com/nwg-piotr/nwg-launchers 7 | * License: GPL3 8 | * */ 9 | 10 | #include 11 | 12 | #include "nwg_tools.h" 13 | #include "nwg_classes.h" 14 | #include "dmenu.h" 15 | #include "log.h" 16 | 17 | #include 18 | 19 | int main(int argc, char *argv[]) { 20 | try { 21 | 22 | InputParser input(argc, argv); 23 | if (input.cmdOptionExists("-h")){ 24 | Log::plain(dmenu::HELP_MESSAGE); 25 | return 0; 26 | } 27 | 28 | auto background_color = input.get_background_color(0.3); 29 | 30 | auto config_dir = get_config_dir("nwgdmenu"); 31 | if (!fs::is_directory(config_dir)) { 32 | Log::info("Config dir not found, creating..."); 33 | fs::create_directories(config_dir); 34 | } 35 | 36 | auto app = Gtk::Application::create(); 37 | 38 | auto provider = Gtk::CssProvider::create(); 39 | auto display = Gdk::Display::get_default(); 40 | auto screen = display->get_default_screen(); 41 | auto settings = Gtk::Settings::get_for_screen(screen); 42 | if (!provider || !display || !settings || !screen) { 43 | Log::error("Failed to initialize GTK"); 44 | return EXIT_FAILURE; 45 | } 46 | DmenuConfig config { 47 | input, 48 | screen 49 | }; 50 | 51 | settings->property_gtk_theme_name() = config.theme; 52 | 53 | Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_USER); 54 | { 55 | auto css_file = setup_css_file("nwgdmenu", config_dir, config.css_filename); 56 | Log::info("Using css file \'", css_file, "\'"); 57 | provider->load_from_path(css_file); 58 | } 59 | 60 | auto all_commands = get_commands_list(config); 61 | DmenuWindow window{ config, all_commands }; 62 | window.set_background_color(background_color); 63 | window.show_all_children(); 64 | switch (2 * (config.valign == VAlign::NotSpecified) + (config.halign == HAlign::NotSpecified )) { 65 | case 0: 66 | window.show(hint::Sides{ { config.halign == HAlign::Right, 50 }, { config.valign == VAlign::Bottom, 50 } }); break; 67 | case 1: 68 | window.show(hint::Side{ config.valign == VAlign::Bottom, 50 }); break; 69 | case 2: 70 | window.show(hint::Side{ config.halign == HAlign::Right, 50 }); break; 71 | case 3: 72 | window.show(hint::Center); break; 73 | } 74 | return app->run(window); 75 | } catch (const Glib::FileError& error) { 76 | Log::error(error.what()); 77 | } catch (const std::runtime_error& error) { 78 | Log::error(error.what()); 79 | } 80 | return EXIT_FAILURE; 81 | } 82 | -------------------------------------------------------------------------------- /icon-missing.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 50 | 60 | 70 | 74 | 78 | 82 | 83 | -------------------------------------------------------------------------------- /common/nwg_tools.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Tools for nwg-launchers 3 | * Copyright (c) 2021 Érico Nogueira 4 | * e-mail: ericonr@disroot.org 5 | * Copyright (c) 2021 Piotr Miller 6 | * e-mail: nwg.piotr@gmail.com 7 | * Website: http://nwg.pl 8 | * Project: https://github.com/nwg-piotr/nwg-launchers 9 | * License: GPL3 10 | * */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | #include "filesystem-compat.h" 23 | #include "nwg_classes.h" 24 | 25 | namespace ns = nlohmann; 26 | 27 | std::string_view get_home_dir(); 28 | fs::path get_cache_home(); 29 | fs::path get_config_dir(std::string_view); 30 | 31 | /// @brief Get path to application's config file by looking up across all config directories 32 | /// in the following order: 33 | /// 1. $XDG_CONFIG_HOME/nwg-launchers// 34 | /// 2. $HOME/.config/nwg-launchers// 35 | /// 3. DATA_DIR/nwg-launchers// where DATA_DIR is build option, usually set to /usr or /usr/share 36 | /// @param app application 37 | /// @param file configuration file name 38 | fs::path get_config_file(std::string_view app, std::string_view file); 39 | 40 | fs::path get_runtime_dir(); 41 | // returns path to pid file 42 | fs::path get_pid_file(std::string_view name); 43 | // parses icon size from `arg`, saturating to [16, 2048] if needed 44 | int parse_icon_size(std::string_view arg); 45 | // returns saved instance pid or nullopt if the file does not exist 46 | // throws std::runtime_error 47 | std::optional get_instance_pid(const char* pid_file_path); 48 | void write_instance_pid(const char* path, pid_t pid); 49 | 50 | std::string detect_wm(const Glib::RefPtr&, const Glib::RefPtr&); 51 | 52 | std::string get_term(std::string_view); 53 | std::string get_locale(void); 54 | 55 | namespace category { 56 | 57 | // TODO: avoid globals, single config file 58 | std::vector get_known_categories(std::string_view app); 59 | std::string_view localize(const ns::json& j, std::string_view category); 60 | 61 | } // namespace category 62 | 63 | std::string read_file_to_string(const fs::path&); 64 | void save_string_to_file(std::string_view, const fs::path&); 65 | std::vector split_string(std::string_view, std::string_view); 66 | std::string_view take_last_by(std::string_view, std::string_view); 67 | 68 | ns::json::reference json_at(ns::json& j, std::string_view key); 69 | ns::json::const_reference json_at(const ns::json& j, std::string_view key); 70 | 71 | ns::json json_from_file(const fs::path&); 72 | ns::json string_to_json(std::string_view); 73 | void save_json(const ns::json&, const fs::path&); 74 | void decode_color(std::string_view, RGBA& color); 75 | 76 | std::string get_output(const std::string&); 77 | fs::path setup_css_file(std::string_view name, const fs::path& config_dir, const fs::path& custom_css_file); 78 | Geometry display_geometry(std::string_view, Glib::RefPtr, Glib::RefPtr); 79 | 80 | // Glibmm does not provide C++ wrappers over glibmm-unix extensions 81 | // so, to handle a signal, we define following plain functions 82 | // taking pointer to Instance as userdata and calling respective methods 83 | // on the said pointer 84 | // These functions always return G_SOURCE_CONTINUE 85 | int instance_on_sigterm(void*); 86 | int instance_on_sigusr1(void*); 87 | int instance_on_sighup(void*); 88 | int instance_on_sigint(void*); 89 | 90 | constexpr auto concat = [](auto&& ... xs) { std::string r; ((r += xs), ...); return r; }; 91 | -------------------------------------------------------------------------------- /grid/grid_tools.cc: -------------------------------------------------------------------------------- 1 | /* GTK-based application grid 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "filesystem-compat.h" 14 | #include "nwg_tools.h" 15 | #include "grid.h" 16 | #include "log.h" 17 | 18 | CacheEntry::CacheEntry(std::string desktop_id, int clicks): desktop_id(std::move(desktop_id)), clicks(clicks) { } 19 | 20 | /* 21 | * Returns locations of .desktop files 22 | * */ 23 | std::vector get_app_dirs() { 24 | std::vector result; 25 | result.reserve(8); 26 | 27 | fs::path home; 28 | if (auto home_ = getenv("HOME")) { 29 | home = home_; 30 | } 31 | 32 | if (auto env_ = getenv("XDG_DATA_HOME")) { 33 | std::string xdg_data_home = env_; // getenv result is not guaranteed to live long enough 34 | for (auto&& dir : split_string(xdg_data_home, ":")) { 35 | result.emplace_back(dir) /= "applications"; 36 | } 37 | } else { 38 | if (!home.empty()) { 39 | result.emplace_back(home) /= ".local/share/applications"; 40 | } 41 | } 42 | std::string xdg_data_dirs; 43 | if (auto env_ = getenv("XDG_DATA_DIRS")) { 44 | xdg_data_dirs = env_; 45 | } else { 46 | xdg_data_dirs = "/usr/local/share/:/usr/share/"; 47 | } 48 | for (auto&& dir: split_string(xdg_data_dirs, ":")) { 49 | result.emplace_back(dir) /= "applications"; 50 | } 51 | 52 | // Add flatpak dirs if not found in XDG_DATA_DIRS 53 | auto suffix = "flatpak/exports/share/applications"; 54 | std::array flatpak_data_dirs { 55 | home / suffix, 56 | fs::path{"/var/lib"} / suffix 57 | }; 58 | for (auto&& fp_dir : flatpak_data_dirs) { 59 | if (std::find(result.begin(), result.end(), fp_dir) == result.end()) { 60 | result.emplace_back(fp_dir); 61 | } 62 | } 63 | 64 | return result; 65 | } 66 | 67 | /* 68 | * Returns vector of strings out of the pinned cache file content 69 | * */ 70 | std::vector get_pinned(const fs::path& pinned_file) { 71 | std::vector lines; 72 | if (std::ifstream in{ pinned_file }) { 73 | for (std::string str; std::getline(in, str);) { 74 | // add non-empty lines to the vector 75 | if (!str.empty()) { 76 | lines.emplace_back(std::move(str)); 77 | } 78 | } 79 | } else { 80 | Log::info("Could not find ", pinned_file, ", creating!"); 81 | save_string_to_file("", pinned_file); 82 | } 83 | return lines; 84 | } 85 | 86 | /* 87 | * Returns n cache items sorted by clicks; n should be the number of grid columns 88 | * */ 89 | std::vector get_favourites(ns::json&& cache, int number) { 90 | // read from json object 91 | std::vector sorted_cache {}; // not yet sorted 92 | for (auto it : cache.items()) { 93 | sorted_cache.emplace_back(it.key(), it.value()); 94 | } 95 | // actually sort by the number of clicks 96 | std::sort(sorted_cache.begin(), sorted_cache.end(), [](const CacheEntry& lhs, const CacheEntry& rhs) { 97 | return lhs.clicks > rhs.clicks; 98 | }); 99 | // Trim to the number of columns, as we need just 1 row of favourites 100 | auto from = sorted_cache.begin() + number; 101 | auto to = sorted_cache.end(); 102 | sorted_cache.erase(from, to); 103 | return sorted_cache; 104 | } 105 | -------------------------------------------------------------------------------- /bar/bar_classes.cc: -------------------------------------------------------------------------------- 1 | /* GTK-based button bar 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * * 8 | * Credits for window transparency go to AthanasiusOfAlex at https://stackoverflow.com/a/21460337 9 | * transparent.cpp 10 | * Code adapted from 'alphademo.c' by Mike 11 | * (http://plan99.net/~mike/blog--now a dead link--unable to find it.) 12 | * as modified by karlphillip for StackExchange: 13 | * (https://stackoverflow.com/questions/3908565/how-to-make-gtk-window-background-transparent) 14 | * Re-worked for Gtkmm 3.0 by Louis Melahn, L.C. January 31, 2014. 15 | * */ 16 | 17 | #include "charconv-compat.h" 18 | #include "nwg_tools.h" 19 | #include "bar.h" 20 | #include "log.h" 21 | 22 | BarConfig::BarConfig(const InputParser& parser, const Glib::RefPtr& screen): 23 | Config{ parser, "~nwgbar", "~nwgbar", screen } 24 | { 25 | if (parser.cmdOptionExists("-v")) { 26 | orientation = Orientation::Vertical; 27 | } 28 | if (auto tname = parser.getCmdOption("-t"); !tname.empty()) { 29 | definition_file = tname; 30 | } 31 | if (auto i_size = parser.getCmdOption("-s"); !i_size.empty()) { 32 | icon_size = parse_icon_size(i_size); 33 | } 34 | } 35 | 36 | BarWindow::BarWindow(Config& config): PlatformWindow(config) { 37 | // scrolled_window -> outer_box -> inner_hbox -> grid 38 | grid.set_column_spacing(5); 39 | grid.set_row_spacing(5); 40 | grid.set_column_homogeneous(true); 41 | outer_box.set_spacing(15); 42 | inner_hbox.set_name("bar"); 43 | switch (config.halign) { 44 | case HAlign::Left: inner_hbox.pack_start(grid, false, false); break; 45 | case HAlign::Right: inner_hbox.pack_end(grid, false, false); break; 46 | default: inner_hbox.pack_start(grid, true, false); 47 | } 48 | switch (config.valign) { 49 | case VAlign::Top: outer_box.pack_start(inner_hbox, false, false); break; 50 | case VAlign::Bottom: outer_box.pack_end(inner_hbox, false, false); break; 51 | default: outer_box.pack_start(inner_hbox, Gtk::PACK_EXPAND_PADDING); 52 | } 53 | scrolled_window.add(outer_box); 54 | add(scrolled_window); 55 | show_all_children(); 56 | } 57 | 58 | bool BarWindow::on_button_press_event(GdkEventButton* button) { 59 | (void)button; 60 | this->close(); 61 | return true; 62 | } 63 | 64 | bool BarWindow::on_key_press_event(GdkEventKey* key_event) { 65 | if (key_event -> keyval == GDK_KEY_Escape) { 66 | this->close(); 67 | } 68 | //if the event has not been handled, call the base class 69 | return CommonWindow::on_key_press_event(key_event); 70 | } 71 | 72 | /* 73 | * Constructor is required for std::vector::emplace_back to work 74 | * It is not needed when compiling with C++20 and greater 75 | * */ 76 | BarEntry::BarEntry(std::string name, std::string exec, std::string icon) 77 | : name(std::move(name)), exec(std::move(exec)), icon(std::move(icon)) {} 78 | 79 | BarBox::BarBox(Glib::ustring name, Glib::ustring exec, Glib::ustring comment) 80 | : AppBox(std::move(name), std::move(exec), std::move(comment)) {} 81 | 82 | bool BarBox::on_button_press_event(GdkEventButton* event) { 83 | (void)event; // suppress warning 84 | 85 | this->activate(); 86 | return false; 87 | } 88 | 89 | void BarBox::on_activate() { 90 | try { 91 | Glib::spawn_command_line_async(exec); 92 | } catch (const Glib::SpawnError& error) { 93 | Log::error("Failed to run command: ", error.what()); 94 | } catch (const Glib::ShellError& error) { 95 | Log::error("Failed to run command: ", error.what()); 96 | } 97 | dynamic_cast(this->get_toplevel())->close(); 98 | } 99 | -------------------------------------------------------------------------------- /grid/grid_client.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * GTK-based application grid 3 | * Copyright (c) 2021 Piotr Miller 4 | * e-mail: nwg.piotr@gmail.com 5 | * Website: http://nwg.pl 6 | * Project: https://github.com/nwg-piotr/nwg-launchers 7 | * License: GPL3 8 | * */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "nwg_tools.h" 15 | #include "nwg_exceptions.h" 16 | #include "nwgconfig.h" 17 | #include "log.h" 18 | 19 | #include 20 | 21 | const char* const HELP_MESSAGE = "\ 22 | GTK application grid: nwggrid " VERSION_STR " (c) 2021 Piotr Miller, Sergey Smirnykh & Contributors \n\n\ 23 | Usage:\n\ 24 | nwggrid -client sends -SIGUSR1 to nwggrid-server, requires nwggrid-server running\n\ 25 | nwggrid [ARGS...] launches nwggrid-server -oneshot ARGS...\n\n\ 26 | \ 27 | See also:\n\ 28 | nwggrid-server -h\n"; 29 | 30 | int main(int argc, char* argv[]) { 31 | try { 32 | using namespace std::string_view_literals; 33 | 34 | if (argc >= 2) { 35 | std::string_view argv1{ argv[1] }; 36 | 37 | if (argv1 == "-h"sv) { 38 | Log::plain(grid::client::HELP_MESSAGE); 39 | return EXIT_SUCCESS; 40 | } 41 | 42 | if (argv1 == "-client"sv) { 43 | auto pid_file = get_pid_file("nwggrid-server.pid"); 44 | Log::info("Using pid file ", pid_file); 45 | Log::info("Running in client mode"); 46 | if (argc != 2) { 47 | Log::warn("Arguments after '-client' must be passed to nwggrid-server"); 48 | } 49 | auto pid = get_instance_pid(pid_file.c_str()); 50 | if (!pid) { 51 | throw std::runtime_error{ "nwggrid-server is not running" }; 52 | } 53 | if (kill(*pid, SIGUSR1) != 0) { 54 | throw std::runtime_error{ "failed to send SIGUSR1 to the pid" }; 55 | } 56 | Log::plain("Success"); 57 | return EXIT_SUCCESS; 58 | } 59 | } 60 | char path[] = INSTALL_PREFIX_STR "/bin/nwggrid-server"; 61 | char oneshot[] = "-oneshot"; 62 | auto arguments = new char*[argc + 2]; 63 | arguments[0] = path; 64 | for (int i = 1; i < argc; ++i) { 65 | arguments[i] = strdup(argv[i]); 66 | if (!arguments[i]) { 67 | int err = errno; 68 | // totally unnecessary cleanup, but why not? 69 | for (int j = 0; j < i; ++j) { 70 | free(arguments[j]); 71 | } 72 | throw std::runtime_error{ error_description(err) }; 73 | } 74 | } 75 | arguments[argc] = oneshot; 76 | arguments[argc + 1] = (char*)NULL; 77 | 78 | auto r = execv( 79 | INSTALL_PREFIX_STR "/bin/nwggrid-server", 80 | arguments 81 | ); 82 | if (r == -1) { 83 | throw ErrnoException{ errno }; 84 | } 85 | return EXIT_SUCCESS; 86 | } catch (const Glib::Error& err) { 87 | // Glib::ustring performs conversion with respect to locale settings 88 | // it might throw (and it does [on my machine]) 89 | // so let's try our best 90 | auto ustr = err.what(); 91 | try { 92 | Log::error(ustr); 93 | } catch (const Glib::ConvertError& err) { 94 | Log::plain("[message conversion failed]"); 95 | Log::error(std::string_view{ ustr.data(), ustr.bytes() }); 96 | } catch (...) { 97 | Log::error("Failed to print error message due to unknown error"); 98 | } 99 | } catch (const std::exception& err) { 100 | Log::error(err.what()); 101 | } 102 | return EXIT_FAILURE; 103 | } 104 | -------------------------------------------------------------------------------- /bar/bar.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * GTK-based button bar 3 | * Copyright (c) 2021 Piotr Miller 4 | * e-mail: nwg.piotr@gmail.com 5 | * Website: http://nwg.pl 6 | * Project: https://github.com/nwg-piotr/nwg-launchers 7 | * License: GPL3 8 | * */ 9 | 10 | #include 11 | 12 | #include "nwg_classes.h" 13 | #include "nwg_tools.h" 14 | #include "bar.h" 15 | #include "log.h" 16 | #include "time_report.h" 17 | 18 | #include 19 | 20 | int main(int argc, char *argv[]) { 21 | try { 22 | ntime::Time start_time{ "start" }; 23 | 24 | InputParser input(argc, argv); 25 | if(input.cmdOptionExists("-h")){ 26 | Log::plain(bar::HELP_MESSAGE); 27 | return 0; 28 | } 29 | 30 | auto background_color = input.get_background_color(0.9); 31 | 32 | auto config_dir = get_config_dir("nwgbar"); 33 | if (!fs::is_directory(config_dir)) { 34 | Log::info("Config dir not found, creating..."); 35 | fs::create_directories(config_dir); 36 | } 37 | 38 | auto app = Gtk::Application::create(); 39 | 40 | auto provider = Gtk::CssProvider::create(); 41 | auto display = Gdk::Display::get_default(); 42 | auto screen = display->get_default_screen(); 43 | auto settings = Gtk::Settings::get_for_screen(screen); 44 | if (!provider || !display || !settings || !screen) { 45 | Log::error("Failed to initialize GTK"); 46 | return EXIT_FAILURE; 47 | } 48 | 49 | BarConfig config { 50 | input, 51 | screen 52 | }; 53 | 54 | settings->property_gtk_theme_name() = config.theme; 55 | 56 | // default or custom template 57 | auto default_bar_file = config_dir / "bar.json"; 58 | auto custom_bar_file = config_dir / config.definition_file; 59 | // copy default anyway if not found 60 | if (!fs::exists(default_bar_file)) { 61 | try { 62 | fs::copy_file(DATA_DIR_STR "/nwgbar/bar.json", default_bar_file, fs::copy_options::overwrite_existing); 63 | } catch (...) { 64 | Log::error("Failed copying default template"); 65 | } 66 | } 67 | 68 | ns::json bar_json; 69 | try { 70 | bar_json = json_from_file(custom_bar_file); 71 | } catch (...) { 72 | Log::error("Template file not found, using default"); 73 | bar_json = json_from_file(default_bar_file); 74 | } 75 | Log::info(bar_json.size(), " bar entries loaded"); 76 | 77 | std::vector bar_entries {}; 78 | if (bar_json.size() > 0) { 79 | bar_entries = get_bar_entries(std::move(bar_json)); 80 | } 81 | 82 | Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_USER); 83 | { 84 | auto css_file = setup_css_file("nwgbar", config_dir, config.css_filename); 85 | provider->load_from_path(css_file); 86 | Log::info("Using css file \'", css_file, "\'"); 87 | } 88 | IconProvider icon_provider { 89 | Gtk::IconTheme::get_for_screen(screen), 90 | config.icon_size 91 | }; 92 | 93 | BarWindow window{ config }; 94 | window.set_background_color(background_color); 95 | 96 | /* Create buttons */ 97 | for (auto& entry : bar_entries) { 98 | auto image = Gtk::make_managed(icon_provider.load_icon(entry.icon)); 99 | auto& ab = window.boxes.emplace_back(std::move(entry.name), 100 | std::move(entry.exec), 101 | std::move(entry.icon)); 102 | ab.set_image_position(Gtk::POS_TOP); 103 | ab.set_image(*image); 104 | if (!entry.css_class.empty()) { 105 | auto && style_context = ab.get_style_context(); 106 | style_context->add_class(entry.css_class); 107 | } 108 | } 109 | 110 | int column = 0; 111 | int row = 0; 112 | 113 | window.grid.freeze_child_notify(); 114 | for (auto& box : window.boxes) { 115 | window.grid.attach(box, column, row, 1, 1); 116 | if (config.orientation == Orientation::Vertical) { 117 | row++; 118 | } else { 119 | column++; 120 | } 121 | } 122 | window.grid.thaw_child_notify(); 123 | Instance instance{ *app.get(), "nwgbar" }; 124 | 125 | window.show_all_children(); 126 | window.show(hint::Fullscreen); 127 | 128 | ntime::Time end_time{ "end", start_time }; 129 | ntime::report(start_time); 130 | 131 | return app->run(window); 132 | } catch (const Glib::Error& e) { 133 | Log::error(e.what()); 134 | } catch (const std::exception& error) { 135 | Log::error(error.what()); 136 | } 137 | return EXIT_FAILURE; 138 | } 139 | -------------------------------------------------------------------------------- /grid/on_desktop_entry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef ON_DESKTOP_ENTRY_H 4 | #define ON_DESKTOP_ENTRY_H 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "nwg_classes.h" 12 | #include "nwg_tools.h" 13 | #include "filesystem-compat.h" 14 | 15 | #include "grid_entries.h" 16 | 17 | namespace entry_parse { 18 | struct Error{}; 19 | struct Hidden{}; 20 | } 21 | 22 | struct FieldParser { 23 | virtual ~FieldParser() = default; 24 | virtual void parse(std::string_view) = 0; 25 | }; 26 | 27 | struct PlainFieldParser: public FieldParser { 28 | std::string& dest; 29 | 30 | PlainFieldParser(std::string& dest): dest{ dest } { 31 | // intentionally left blank 32 | } 33 | virtual ~PlainFieldParser() = default; 34 | virtual void parse(std::string_view str) override { 35 | dest = str; 36 | } 37 | }; 38 | 39 | struct ExecParser: public PlainFieldParser { 40 | using PlainFieldParser::PlainFieldParser; 41 | void parse(std::string_view str) override { 42 | std::string_view home{ "~/" }; 43 | if (str.substr(0, home.size()) == home) { 44 | str.remove_prefix(home.size()); 45 | dest += get_home_dir(); 46 | dest.push_back('/'); 47 | } 48 | 49 | auto idx = str.find(" %"); 50 | if (idx == std::string_view::npos) { 51 | idx = std::size(str); 52 | } 53 | dest += str.substr(0, idx); 54 | } 55 | }; 56 | 57 | struct CategoryParser: public FieldParser { 58 | using CategoriesType = decltype(DesktopEntry{}.categories); 59 | CategoriesType& categories; 60 | const ns::json& source; 61 | const std::vector& known_categories; 62 | 63 | CategoryParser(CategoriesType& categories, const DesktopEntryConfig& config): 64 | categories{ categories }, 65 | source{ config.config_source }, 66 | known_categories{ config.known_categories } 67 | { 68 | // intentionally left blank 69 | } 70 | 71 | void parse(std::string_view str) override { 72 | auto is_known_category = [&](auto && c){ 73 | return std::find(known_categories.begin(), known_categories.end(), c) != known_categories.end(); 74 | }; 75 | auto parts = split_string(str, ";"); 76 | for (auto && part: parts) { 77 | if (!part.empty() && is_known_category(part)) { 78 | categories.emplace_back(category::localize(source, part)); 79 | } 80 | } 81 | } 82 | }; 83 | 84 | /* 85 | * Parses .desktop file to DesktopEntry struct, 86 | * throwing entry_parse::{Error,Hidden} or io errors 87 | * */ 88 | DesktopEntry parse_desktop_entry(const fs::path& path, const DesktopEntryConfig& config) { 89 | using namespace std::literals::string_view_literals; 90 | 91 | DesktopEntry entry; 92 | entry.terminal = false; 93 | 94 | std::ifstream file{ path }; 95 | if (!file) { 96 | throw entry_parse::Error{}; 97 | } 98 | std::string str; // buffer to read into 99 | 100 | std::string name_ln {}; // localized: Name[ln]= 101 | std::string comment_ln {}; // localized: Comment[ln]= 102 | 103 | // if line starts with `prefix`, call `parser.parse()` 104 | struct Match { 105 | std::string_view prefix; 106 | FieldParser& parser; 107 | }; 108 | struct Result { 109 | bool ok; 110 | size_t pos; 111 | }; 112 | PlainFieldParser name_parser{ entry.name }; 113 | PlainFieldParser name_ln_parser{ name_ln }; 114 | ExecParser exec_parser{ entry.exec }; 115 | PlainFieldParser icon_parser{ entry.icon }; 116 | PlainFieldParser comment_parser{ entry.comment }; 117 | PlainFieldParser comment_ln_parser{ comment_ln }; 118 | PlainFieldParser mime_type_parser{ entry.mime_type }; 119 | CategoryParser categories_parser{ entry.categories, config }; 120 | 121 | Match matches[] = { 122 | { "Name="sv, name_parser }, 123 | { config.name_ln, name_ln_parser }, 124 | { "Exec="sv, exec_parser }, 125 | { "Icon="sv, icon_parser }, 126 | { "Comment="sv, comment_parser }, 127 | { config.comment_ln, comment_ln_parser }, 128 | { "MimeType="sv, mime_type_parser }, 129 | { "Categories="sv, categories_parser } 130 | }; 131 | std::bitset parsed; 132 | 133 | // Skip everything not related 134 | constexpr auto header = "[Desktop Entry]"sv; 135 | while (std::getline(file, str)) { 136 | str.resize(header.size()); 137 | if (str == header) { 138 | break; 139 | } 140 | } 141 | // Repeat until the next section 142 | constexpr auto nodisplay = "NoDisplay=true"sv; 143 | constexpr auto terminal = "Terminal=true"sv; 144 | while (std::getline(file, str) && !parsed.all()) { 145 | if (!str.empty() && str[0] == '[') { // new section begins, break 146 | break; 147 | } 148 | auto view = std::string_view{str}; 149 | auto view_len = std::size(view); 150 | if (view == nodisplay) { 151 | throw entry_parse::Hidden{}; 152 | } 153 | if (view == terminal) { 154 | entry.terminal = true; 155 | } 156 | auto try_strip_prefix = [&view, view_len](auto& prefix) { 157 | auto len = std::min(view_len, std::size(prefix)); 158 | return Result { 159 | prefix == view.substr(0, len), 160 | len 161 | }; 162 | }; 163 | std::size_t index{ 0 }; 164 | for (auto& [prefix, parser] : matches) { 165 | if (parsed[index]) { 166 | continue; 167 | } 168 | if (auto [ok, pos] = try_strip_prefix(prefix); ok) { 169 | parser.parse(view.substr(pos)); 170 | break; 171 | } 172 | ++index; 173 | } 174 | } 175 | 176 | if (!name_ln.empty()) { 177 | entry.name = std::move(name_ln); 178 | } 179 | if (!comment_ln.empty()) { 180 | entry.comment = std::move(comment_ln); 181 | } 182 | if (entry.name.empty() || entry.exec.empty()) { 183 | throw entry_parse::Error{}; 184 | } 185 | if (entry.terminal) { 186 | entry.exec = concat(config.term, " ", entry.exec); 187 | } 188 | 189 | return entry; 190 | } 191 | 192 | #endif 193 | -------------------------------------------------------------------------------- /grid/grid_entries.h: -------------------------------------------------------------------------------- 1 | /* GTK-based application grid 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | #include "nwg_classes.h" 15 | #include "filesystem-compat.h" 16 | #include "grid.h" 17 | 18 | /* Stores pre-processed assets useful when parsing DesktopEntry struct */ 19 | struct DesktopEntryConfig { 20 | std::string_view term; // user-preferred terminal 21 | std::string name_ln; // localized prefix: Name[ln]= 22 | std::string comment_ln; // localized prefix: Comment[ln]= 23 | std::string_view home; 24 | 25 | const ns::json& config_source; 26 | std::vector known_categories; 27 | 28 | DesktopEntryConfig(const GridConfig& config); 29 | 30 | DesktopEntryConfig(DesktopEntryConfig&&) = delete; 31 | }; 32 | 33 | // Table containing entries 34 | // internally is a thin wrapper over list 35 | struct EntriesModel { 36 | GridConfig& config; 37 | GridWindow& window; 38 | 39 | // TODO: think of saner way to load icons 40 | IconProvider& icons; 41 | 42 | Span pins; 43 | Span favs; 44 | 45 | // list because entries should not get invalidated when inserting/erasing 46 | std::list entries; 47 | using Index = typename decltype(entries)::iterator; 48 | 49 | EntriesModel(GridConfig& config, GridWindow& window, IconProvider& icons, Span pins, Span favs): 50 | config{ config }, window{ window }, icons{ icons }, pins{ pins }, favs{ favs } 51 | { 52 | // intentionally left blank 53 | } 54 | 55 | template 56 | Index emplace_entry(Ts && ... args) { 57 | auto & entry = entries.emplace_front(std::forward(args)...); 58 | set_entry_stats(entry); 59 | auto && box = window.emplace_box( 60 | entry.desktop_entry().name, 61 | entry.desktop_entry().comment, 62 | entry 63 | ); 64 | // boxing is necessary 65 | // for some reason the icons are not shown if the images are not boxed 66 | auto image = Gtk::make_managed(icons.load_icon(entry.desktop_entry().icon)); 67 | box.set_image(*image); 68 | box.set_always_show_image(true); 69 | window.build_grids(); 70 | 71 | return entries.begin(); 72 | } 73 | template 74 | Index update_entry(Index index, Ts && ... args) { 75 | // TODO: merge entries 76 | //! ЗДЕСЬ ПРОИСХОДИТ ОБСЕР 77 | auto new_index = entries.emplace(index, std::forward(args)...); 78 | auto& entry = *new_index; 79 | 80 | decltype(entries) preserve; 81 | preserve.splice(index, entries); 82 | 83 | set_entry_stats(entry); 84 | GridBox new_box { 85 | entry.desktop_entry().name, 86 | entry.desktop_entry().comment, 87 | entry 88 | }; 89 | // boxing is necessary 90 | // for some reason the icons are not shown if the images are not boxed 91 | auto image = Gtk::make_managed(icons.load_icon(entry.desktop_entry().icon)); 92 | new_box.set_image(*image); 93 | window.update_box_by_id(entry.desktop_id, std::move(new_box)); 94 | 95 | return new_index; 96 | } 97 | void erase_entry(Index index) { 98 | auto && entry = *index; 99 | window.remove_box_by_desktop_id(entry.desktop_id); 100 | entries.erase(index); 101 | window.build_grids(); 102 | } 103 | auto & row(Index index) { 104 | return *index; 105 | } 106 | private: 107 | void set_entry_stats(Entry& entry) { 108 | if (auto result = std::find(pins.begin(), pins.end(), entry.desktop_id); result != pins.end()) { 109 | entry.stats.pinned = Stats::Pinned; 110 | // temporary fix for #176 111 | // see comments to PinnedBoxes class 112 | entry.stats.position = (result - pins.begin()) - pins.size() - 1; 113 | } 114 | auto cmp = [&entry](auto && fav){ return entry.desktop_id == fav.desktop_id; }; 115 | if (auto result = std::find_if(favs.begin(), favs.end(), cmp); result != favs.end()) { 116 | entry.stats.favorite = Stats::Favorite; 117 | entry.stats.clicks = result->clicks; 118 | } 119 | } 120 | }; 121 | 122 | /* EntriesManager handles loading/updating entries. 123 | * For each directory in `dirs` it sets a monitor and loads all .desktop files in it. 124 | * It also supports "overwriting" files: if two files have the same desktop id, 125 | * it will work with the file stored in the directory listed first, i.e. having more precedence. 126 | * The "desktop id" mechanism it uses is a bit different than the mechanism described in 127 | * the Freedesktop standard, but it works roughly the same; if two files have conflicting desktop ids, 128 | * the "desktop id"s will conflict too, and vice versa. */ 129 | struct EntriesManager { 130 | struct Metadata { 131 | using Index = EntriesModel::Index; 132 | enum FileState: unsigned short { 133 | Ok = 0, 134 | Invalid, 135 | Hidden 136 | }; 137 | Index index; // index in table; index is invalid if state is not Ok 138 | FileState state; 139 | int priority; // the lower the value, the bigger the priority 140 | // i.e. if file1.priority > file2.priority, the file2 wins 141 | 142 | Metadata(Index index, FileState state, int priority): 143 | index{ index }, state{ state }, priority{ priority } 144 | { 145 | // intentionally left blank 146 | } 147 | }; 148 | 149 | // stores "desktop id"s 150 | // list because insertions/removals should not invalidate the store 151 | std::list desktop_ids_store; 152 | // maps "desktop id" to Metadata 153 | std::unordered_map desktop_ids_info; 154 | // stored monitors 155 | // just to keep them alive 156 | std::vector> monitors; 157 | 158 | EntriesModel& table; 159 | GridConfig& config; 160 | 161 | DesktopEntryConfig desktop_entry_config; 162 | 163 | EntriesManager(Span dirs, EntriesModel& table, GridConfig& config); 164 | void on_file_changed(std::string id, const Glib::RefPtr& file, int priority); 165 | void on_file_deleted(std::string id, int priority); 166 | private: 167 | // tries to load & insert entry with `id` from `file` 168 | void try_load_entry_(std::string id, const fs::path& file, int priority); 169 | }; 170 | -------------------------------------------------------------------------------- /grid/grid.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * GTK-based application grid 3 | * Copyright (c) 2021 Piotr Miller 4 | * e-mail: nwg.piotr@gmail.com 5 | * Website: http://nwg.pl 6 | * Project: https://github.com/nwg-piotr/nwg-launchers 7 | * License: GPL3 8 | * */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "nwg_tools.h" 15 | #include "nwg_classes.h" 16 | #include "grid.h" 17 | #include "grid_entries.h" 18 | #include "time_report.h" 19 | 20 | #include 21 | 22 | /* Base class for application drivers, simply calls Application::run */ 23 | struct ApplicationDriver { 24 | Glib::RefPtr app; 25 | 26 | ApplicationDriver(const Glib::RefPtr& app): app{ app } { 27 | // intentionally left blank 28 | } 29 | virtual ~ApplicationDriver() = default; 30 | virtual int run() { return app->run(); } 31 | }; 32 | 33 | /* Keeps the application alive when the window is closed, registers & deregisters */ 34 | struct ServerDriver: public ApplicationDriver { 35 | GridInstance instance; 36 | 37 | ServerDriver(const Glib::RefPtr& app, GridWindow& window): 38 | ApplicationDriver{ app }, 39 | instance{ *app.get(), window, "nwggrid-server" } 40 | { 41 | app->hold(); 42 | } 43 | }; 44 | 45 | /* Does not register application instance, exits once the window is closed */ 46 | struct OneshotDriver: public ApplicationDriver { 47 | GridWindow& window; 48 | GridInstance instance; 49 | 50 | OneshotDriver(const Glib::RefPtr& app, GridWindow& window): 51 | ApplicationDriver{ app }, 52 | window{ window }, 53 | instance{ *app.get(), window, "nwggrid" } 54 | { 55 | app->hold(); 56 | } 57 | int run() override { 58 | window.show(hint::Fullscreen); 59 | window.signal_hide().connect([this](){ 60 | this->app->release(); 61 | }); 62 | return ApplicationDriver::run(); 63 | } 64 | }; 65 | 66 | int main(int argc, char *argv[]) { 67 | try { 68 | ntime::Time start{ "start" }; 69 | 70 | InputParser input{ argc, argv }; 71 | if (input.cmdOptionExists("-h")){ 72 | Log::plain(grid::server::HELP_MESSAGE); 73 | return 0; 74 | } 75 | 76 | auto config_dir = get_config_dir("nwggrid"); 77 | if (!fs::is_directory(config_dir)) { 78 | Log::info("Config dir not found, creating..."); 79 | fs::create_directories(config_dir); 80 | } 81 | 82 | auto app = Gtk::Application::create(); 83 | 84 | auto provider = Gtk::CssProvider::create(); 85 | auto display = Gdk::Display::get_default(); 86 | auto screen = display->get_default_screen(); 87 | auto settings = Gtk::Settings::get_for_screen(screen); 88 | if (!provider || !display || !settings || !screen) { 89 | Log::error("Failed to initialize GTK"); 90 | return EXIT_FAILURE; 91 | } 92 | 93 | GridConfig config { 94 | input, 95 | screen, 96 | config_dir 97 | }; 98 | Log::info("Locale: ", config.lang); 99 | 100 | settings->property_gtk_theme_name() = config.theme; 101 | 102 | Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_USER); 103 | { 104 | auto css_file = setup_css_file("nwggrid", config_dir, config.css_filename); 105 | provider->load_from_path(css_file); 106 | Log::info("Using css file \'", css_file, "\'"); 107 | } 108 | IconProvider icon_provider { 109 | Gtk::IconTheme::get_for_screen(screen), 110 | config.icon_size 111 | }; 112 | 113 | // This will be read-only, to find n most clicked items (n = number of grid columns) 114 | std::vector favourites; 115 | if (config.favs) { 116 | try { 117 | auto cache = json_from_file(config.cached_file); 118 | if (cache.size() > 0) { 119 | Log::info(cache.size(), " cache entries loaded"); 120 | } else { 121 | Log::info("No cache entries loaded"); 122 | } 123 | auto n = std::min(config.num_col, cache.size()); 124 | favourites = get_favourites(std::move(cache), n); 125 | } catch (...) { 126 | // TODO: only save cache if favs were changed 127 | Log::error("Failed to read cache file '", config.cached_file, "'"); 128 | } 129 | } 130 | 131 | std::vector pinned; 132 | if (config.pins) { 133 | pinned = get_pinned(config.pinned_file); 134 | if (pinned.size() > 0) { 135 | Log::info(pinned.size(), " pinned entries loaded"); 136 | } else { 137 | Log::info("No pinned entries found"); 138 | } 139 | } 140 | 141 | std::vector dirs; 142 | if (!config.special_dirs.empty()) { 143 | using namespace std::string_view_literals; 144 | // use special dirs specified with -d argument (feature request #122) 145 | auto dirs_ = split_string(config.special_dirs, ":"); 146 | Log::info("Using custom .desktop files path(s):\n"); 147 | std::array status { "' [INVALID]\n"sv, "' [OK]\n"sv }; 148 | for (auto && dir: dirs_) { 149 | std::error_code ec; 150 | auto is_dir = fs::is_directory(dir, ec) && !ec; 151 | Log::plain('\'', dir, status[is_dir]); 152 | if (is_dir) { 153 | dirs.emplace_back(dir); 154 | } 155 | } 156 | } else { 157 | // get all applications dirs 158 | dirs = get_app_dirs(); 159 | } 160 | 161 | ntime::Time commons{ "common", start }; 162 | 163 | GridWindow window{ config }; 164 | 165 | ntime::Time window_time{ "window", commons }; 166 | 167 | EntriesModel table{ config, window, icon_provider, pinned, favourites }; 168 | EntriesManager entries_provider{ dirs, table, config }; 169 | 170 | ntime::Time model_time{ "models", window_time }; 171 | ntime::report(start); 172 | 173 | std::unique_ptr driver; 174 | if (config.oneshot) { 175 | driver.reset(new OneshotDriver{ app, window }); 176 | } else { 177 | driver.reset(new ServerDriver{ app, window }); 178 | } 179 | return driver->run(); 180 | } catch (const Glib::Error& err) { 181 | // Glib::ustring performs conversion with respect to locale settings 182 | // it might throw (and it does [on my machine]) 183 | // so let's try our best 184 | auto ustr = err.what(); 185 | try { 186 | Log::error(ustr); 187 | } catch (const Glib::ConvertError& err) { 188 | Log::plain("[message conversion failed]"); 189 | Log::error(std::string_view{ ustr.data(), ustr.bytes() }); 190 | } catch (...) { 191 | Log::error("Failed to print error message due to unknown error"); 192 | } 193 | } catch (const std::exception& err) { 194 | Log::error(err.what()); 195 | } 196 | return EXIT_FAILURE; 197 | } 198 | -------------------------------------------------------------------------------- /dmenu/dmenu_classes.cc: -------------------------------------------------------------------------------- 1 | /* GTK-based dmenu 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * * 8 | * Credits for window transparency go to AthanasiusOfAlex at https://stackoverflow.com/a/21460337 9 | * transparent.cpp 10 | * Code adapted from 'alphademo.c' by Mike 11 | * (http://plan99.net/~mike/blog--now a dead link--unable to find it.) 12 | * as modified by karlphillip for StackExchange: 13 | * (https://stackoverflow.com/questions/3908565/how-to-make-gtk-window-background-transparent) 14 | * Re-worked for Gtkmm 3.0 by Louis Melahn, L.C. January 31, 2014. 15 | * */ 16 | 17 | #include // isatty 18 | #include 19 | 20 | #include "charconv-compat.h" 21 | #include "nwg_tools.h" 22 | #include "dmenu.h" 23 | #include "log.h" 24 | 25 | DmenuConfig::DmenuConfig(const InputParser& parser, const Glib::RefPtr& screen): 26 | Config{ parser, "~nwgdmenu", "~nwgdmenu", screen }, 27 | settings_file{ get_settings_path() } 28 | { 29 | // For now the settings file only determines if case_sensitive was turned on. 30 | if (std::ifstream settings{ settings_file }) { 31 | std::string sensitivity; 32 | settings >> sensitivity; 33 | case_sensitive = sensitivity == "case_sensitive"; 34 | } 35 | 36 | // We will build dmenu out of commands found in $PATH if nothing has been passed by stdin 37 | dmenu_run = parser.cmdOptionExists("-run") || isatty(STDIN_FILENO) == 1; 38 | show_searchbox = !parser.cmdOptionExists("-n"); 39 | 40 | if (auto rw = parser.getCmdOption("-r"); !rw.empty()){ 41 | int r; 42 | if (parse_number(rw, r)) { 43 | if (r > 0 && r <= 100) { 44 | rows = r; 45 | } else { 46 | Log::error("Number of rows must be in range 1 - 100"); 47 | } 48 | } else { 49 | Log::error("Invalid rows number"); 50 | } 51 | } 52 | } 53 | 54 | inline auto set_searchbox_placeholder = [](auto && searchbox, auto case_sensitive) { 55 | constexpr std::array placeholders { "TYPE TO SEARCH", "Type to Search" }; 56 | searchbox.set_placeholder_text(placeholders[case_sensitive]); 57 | }; 58 | 59 | inline auto build_commands_list = [](auto && dmenu, auto && commands, auto max) { 60 | decltype(max) count{ 0 }; 61 | for (auto && command: commands) { 62 | dmenu.emplace_back(command); 63 | count++; 64 | if (count == max) { 65 | break; 66 | } 67 | } 68 | }; 69 | 70 | DmenuWindow::DmenuWindow(DmenuConfig& config, std::vector& src): 71 | PlatformWindow{ config }, 72 | commands{ 1, false, Gtk::SELECTION_SINGLE }, 73 | commands_source{ src }, 74 | config{ config } 75 | { 76 | // different shells emit different events 77 | auto display_name = this->get_screen()->get_display()->get_name(); 78 | auto is_wayland = display_name.find("wayland") == 0; 79 | // non-void lambdas are broken in gtkmm, thus the need for bind_return 80 | signal_leave_notify_event().connect(sigc::bind_return([this,is_wayland](auto* event) { 81 | constexpr std::array windowing { 82 | std::array { GDK_NOTIFY_ANCESTOR, GDK_NOTIFY_VIRTUAL }, // X11 (i3 and openbox atleast) 83 | std::array { GDK_NOTIFY_NONLINEAR, GDK_NOTIFY_NONLINEAR_VIRTUAL } // Wayland (wlr-layer-shell) 84 | }; 85 | for (auto detail: windowing[is_wayland]) { 86 | if (event->detail == detail) { 87 | this->close(); 88 | } 89 | } 90 | }, true)); 91 | commands.set_name("commands"); 92 | commands.set_reorderable(false); 93 | commands.set_headers_visible(false); 94 | commands.set_enable_search(false); 95 | commands.set_hover_selection(true); 96 | commands.set_activate_on_single_click(true); 97 | commands.signal_row_activated().connect([this](auto && path, auto*) { 98 | // this is ****** 99 | auto model = this->commands.get_model(); 100 | auto iter = model->get_iter(path); 101 | Glib::ustring item; 102 | iter->get_value(0, item); 103 | try { 104 | Glib::spawn_command_line_async(item); 105 | } catch (const Glib::SpawnError& error) { 106 | Log::error("Failed to run command: ", error.what()); 107 | } catch (const Glib::ShellError& error) { 108 | Log::error("Failed to run command: ", error.what()); 109 | } 110 | this->close(); 111 | }); 112 | searchbox.set_name("searchbox"); 113 | if (config.show_searchbox) { 114 | set_searchbox_placeholder(searchbox, config.case_sensitive); 115 | searchbox.signal_search_changed().connect(sigc::mem_fun(*this, &DmenuWindow::filter_view)); 116 | vbox.pack_start(searchbox, false, false); 117 | } 118 | vbox.pack_start(commands); 119 | 120 | add(vbox); 121 | 122 | build_commands_list(*this, commands_source, config.rows); 123 | } 124 | 125 | DmenuWindow::~DmenuWindow() { 126 | using namespace std::string_view_literals; 127 | if (case_sensitivity_changed) { 128 | std::ofstream file{ config.settings_file, std::ios::trunc }; 129 | constexpr std::array values { "case_insensitive"sv, "case_sensitive"sv }; 130 | file << values[config.case_sensitive]; 131 | } 132 | } 133 | 134 | void DmenuWindow::emplace_back(const Glib::ustring& command) { 135 | this->commands.append(command); 136 | } 137 | 138 | void DmenuWindow::filter_view() { 139 | auto model_refptr = commands.get_model(); 140 | auto& model = dynamic_cast(*model_refptr.get()); 141 | model.clear(); 142 | auto search_phrase = searchbox.get_text(); 143 | if (search_phrase.length() > 0) { 144 | // append at most `max` entries satisfying `matches` predicate, return count 145 | auto fill_matches = [this](auto && source, auto && matches, auto max) { 146 | decltype(max) count = 0; 147 | auto begin = source.begin(); auto end = source.end(); 148 | while (begin < end && count < max) { 149 | auto && command = *begin++; 150 | if (matches(command)) { 151 | this->emplace_back(command); 152 | count++; 153 | } 154 | } 155 | return count; 156 | }; 157 | // append entries matching `exact`, then entries matching `almost` (at most `max` entries) 158 | auto fill_all = [this,fill_matches,rows=config.rows](auto && exact, auto && almost) { 159 | auto count = fill_matches(this->commands_source, exact, rows); 160 | if (count < rows) { 161 | fill_matches(this->commands_source, almost, rows - count); 162 | } 163 | }; 164 | if (config.case_sensitive) { 165 | fill_all([&a=search_phrase](auto && b){ return b.find(a) == 0; }, 166 | [&a=search_phrase](auto && b){ auto r = b.find(a); return r > 0 && r != a.npos; }); 167 | } else { 168 | auto sf = search_phrase.casefold(); 169 | fill_all([&a=sf](auto && b){ return b.casefold().find(a) == 0; }, 170 | [&a=sf](auto && b){ auto r = b.casefold().find(a); return r > 0 && r != a.npos; }); 171 | } 172 | } else { 173 | // searchentry is clear, show all options 174 | build_commands_list(*this, commands_source, config.rows); 175 | } 176 | select_first_item(); 177 | } 178 | 179 | void DmenuWindow::select_first_item() { 180 | Gtk::ListStore::Path path{1}; 181 | commands.set_cursor(path); 182 | commands.grab_focus(); 183 | } 184 | 185 | void DmenuWindow::switch_case_sensitivity() { 186 | case_sensitivity_changed = true; 187 | config.case_sensitive = !config.case_sensitive; 188 | searchbox.set_text(""); 189 | set_searchbox_placeholder(searchbox, config.case_sensitive); 190 | } 191 | 192 | bool DmenuWindow::on_key_press_event(GdkEventKey* key_event) { 193 | switch (key_event->keyval) { 194 | case GDK_KEY_Escape: 195 | close(); 196 | break; 197 | case GDK_KEY_Delete: 198 | if (config.show_searchbox) { 199 | searchbox.set_text(""); 200 | } 201 | break; 202 | case GDK_KEY_Insert: 203 | if (config.show_searchbox) { 204 | switch_case_sensitivity(); 205 | searchbox.set_text(""); 206 | } 207 | break; 208 | case GDK_KEY_Left: 209 | case GDK_KEY_Right: 210 | case GDK_KEY_Up: 211 | case GDK_KEY_Down: 212 | break; 213 | case GDK_KEY_Return: 214 | // handling code assigned in constructor 215 | break; 216 | default: 217 | if (config.show_searchbox) { 218 | searchbox.grab_focus(); 219 | searchbox.select_region(0, 0); 220 | searchbox.set_position(-1); 221 | } 222 | } 223 | 224 | //if the event has not been handled, call the base class 225 | return CommonWindow::on_key_press_event(key_event); 226 | } 227 | 228 | // TreeView will have height of 1 until it's actually shown 229 | // in this hack we do our best to calculate actual window size 230 | // we assume all cells have same height (which is true for ListViewText) and all cells are filled 231 | int DmenuWindow::get_height() { 232 | auto model = commands.get_model(); 233 | // Gtk::TreeModel::iter_n_root_children is protected, so ... 234 | auto rows = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(model->gobj()), nullptr); 235 | auto base_height = CommonWindow::get_height(); 236 | int off_x = -1, off_y = -1, cell_width = -1, cell_height = -1; 237 | Gdk::Rectangle rect; 238 | auto* column = commands.get_column(0); 239 | column->cell_get_size(rect, off_x, off_y, cell_width, cell_height); 240 | auto cell_spacing = column->get_spacing(); 241 | return base_height + cell_height * (rows + 1) + cell_spacing * rows; 242 | } 243 | -------------------------------------------------------------------------------- /grid/grid_entries.cc: -------------------------------------------------------------------------------- 1 | /* GTK-based application grid 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | #include "grid_entries.h" 9 | #include "on_desktop_entry.h" 10 | #include "log.h" 11 | 12 | DesktopEntryConfig::DesktopEntryConfig(const GridConfig& config): 13 | term{ config.term }, 14 | name_ln{ concat("Name[", config.lang, "]=") }, 15 | comment_ln{ concat("Comment[", config.lang, "]=") }, 16 | home{ get_home_dir() }, 17 | config_source{ config.config_source } 18 | { 19 | if (config.categories) { 20 | for (auto & [k, _] : config.config_source["categories"].items()) { 21 | known_categories.push_back(k); 22 | } 23 | } 24 | } 25 | 26 | 27 | inline bool looks_like_desktop_file(const Glib::RefPtr& file) { 28 | fs::path path{ file->get_path() }; 29 | return path.extension() == ".desktop"; 30 | } 31 | inline bool looks_like_desktop_file(const fs::directory_entry& entry) { 32 | auto && path = entry.path(); 33 | return path.extension() == ".desktop"; 34 | } 35 | inline bool can_be_loaded(const Glib::RefPtr& file) { 36 | auto file_type = file->query_file_type(); 37 | return file_type == Gio::FILE_TYPE_REGULAR; 38 | } 39 | inline bool can_be_loaded(const fs::directory_entry& entry) { 40 | return entry.is_regular_file(); 41 | } 42 | inline auto desktop_id(const Glib::RefPtr& file, const Glib::RefPtr& dir) { 43 | return dir->get_relative_path(file); 44 | } 45 | inline auto desktop_id(const fs::path& file, const fs::path& dir) { 46 | return file.lexically_relative(dir); 47 | } 48 | 49 | EntriesManager::EntriesManager(Span dirs, EntriesModel& table, GridConfig& config): 50 | table{ table }, config{ config }, desktop_entry_config{ config } 51 | { 52 | // set monitors 53 | monitors.reserve(dirs.size()); 54 | for (auto && dir: dirs) { 55 | auto dir_index = monitors.size(); 56 | auto monitored_dir = Gio::File::create_for_path(dir); 57 | auto && monitor = monitors.emplace_back(monitored_dir->monitor_directory()); 58 | // dir_index and monitored_dir are captured by value 59 | // TODO: should I disconnect on exit to make sure there is no dangling reference to `this`? 60 | monitor->signal_changed().connect([this,monitored_dir,dir_index](auto && file1, auto && file2, auto event) { 61 | (void)file2; // silence warning 62 | if (looks_like_desktop_file(file1)) { 63 | auto && id = desktop_id(file1, monitored_dir); 64 | switch (event) { 65 | // ignored in favor of CHANGES_DONE_HINT 66 | case Gio::FILE_MONITOR_EVENT_CHANGED: break; 67 | case Gio::FILE_MONITOR_EVENT_CHANGES_DONE_HINT: 68 | if (can_be_loaded(file1)) { 69 | on_file_changed(id, file1, dir_index); 70 | } 71 | break; 72 | case Gio::FILE_MONITOR_EVENT_DELETED: 73 | on_file_deleted(id, dir_index); 74 | break; 75 | // ignore because CREATED is emitted when the file is created but not written to 76 | // copying/moving emit two signals: CREATED and then CHANGED 77 | case Gio::FILE_MONITOR_EVENT_CREATED: 78 | // TODO: it seems we can safely ignored but I guess we should doublecheck 79 | case Gio::FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: break; 80 | // TODO: should we set WATCH_MOVES? 81 | // we don't set WATCH_MOVES so these three should not be emitted 82 | case Gio::FILE_MONITOR_EVENT_RENAMED: 83 | case Gio::FILE_MONITOR_EVENT_MOVED_IN: 84 | case Gio::FILE_MONITOR_EVENT_MOVED_OUT: Log::warn("WATCH_MOVES flag is set but not handled"); break; 85 | // we don't set SEND_MOVED (deprecated) 86 | case Gio::FILE_MONITOR_EVENT_MOVED: Log::warn("SEND_MOVED flag is deprecated and thus shouldn't be used"); break; 87 | // TODO: handle unmounting, e.g. for all files in directory when pre-unmounting erase their entries 88 | case Gio::FILE_MONITOR_EVENT_PRE_UNMOUNT: 89 | case Gio::FILE_MONITOR_EVENT_UNMOUNTED: Log::warn("Unmounting is not supported yet"); break; 90 | // no default statement so we could see a compiler warning if new flag is added in the future 91 | }; 92 | } 93 | }); 94 | } 95 | // dir_index is used as priority 96 | std::size_t dir_index{ 0 }; 97 | for (auto && dir: dirs) { 98 | std::error_code ec; 99 | // TODO: shouldn't it be recursive_directory_iterator? 100 | fs::directory_iterator dir_iter{ dir, ec }; 101 | for (auto& entry : dir_iter) { 102 | if (ec) { 103 | Log::error(ec.message()); 104 | ec.clear(); 105 | continue; 106 | } 107 | if (looks_like_desktop_file(entry) && can_be_loaded(entry)) { 108 | auto && path = entry.path(); 109 | auto && id = desktop_id(path, dir); 110 | try_load_entry_(id, path, dir_index); 111 | } 112 | } 113 | ++dir_index; 114 | } 115 | } 116 | 117 | // tries to load & insert entry with `id` from `file` 118 | void EntriesManager::try_load_entry_(std::string id, const fs::path& file, int priority) { 119 | // node with id 120 | std::list id_node; 121 | // desktop_ids_store stores string_views. 122 | // If we just insert id, there will be dangling reference when id is freed. 123 | // To avoid this, we store id in the node and then take a view of it. 124 | auto && id_ = id_node.emplace_front(std::move(id)); 125 | 126 | auto [iter, inserted] = desktop_ids_info.try_emplace( 127 | id_, 128 | EntriesModel::Index{}, 129 | Metadata::Hidden, 130 | priority 131 | ); 132 | if (inserted) { 133 | // the entry was inserted, therefore we need to add the node to the store 134 | // to keep the view valid 135 | desktop_ids_store.splice(desktop_ids_store.begin(), id_node); 136 | // load it 137 | try { 138 | std::unique_ptr desktop_entry{ 139 | new DesktopEntry{ parse_desktop_entry(file, desktop_entry_config) } 140 | }; 141 | auto && meta = iter->second; 142 | meta.state = Metadata::Ok; 143 | meta.index = table.emplace_entry( 144 | id_, 145 | Stats{}, 146 | std::move(desktop_entry) 147 | ); 148 | } catch (entry_parse::Hidden) { 149 | // do nothing 150 | } catch (entry_parse::Error) { 151 | Log::error("Failed to load desktop file '", file, "'"); 152 | } 153 | } else { 154 | Log::info(".desktop file '", file, "' with id '", id_, "' overridden, ignored"); 155 | } 156 | } 157 | 158 | void EntriesManager::on_file_deleted(std::string id, int priority) { 159 | if (auto result = desktop_ids_info.find(id); result != desktop_ids_info.end()) { 160 | if (result->second.priority < priority) { 161 | return; 162 | } 163 | if (result->second.state == Metadata::Ok) { 164 | table.erase_entry(result->second.index); 165 | } 166 | desktop_ids_info.erase(result); 167 | auto iter = std::find(desktop_ids_store.begin(), desktop_ids_store.end(), id); 168 | desktop_ids_store.erase(iter); 169 | } else { 170 | Log::error("on_file_deleted: no entry with id '", id, "'"); 171 | } 172 | } 173 | 174 | void EntriesManager::on_file_changed(std::string id, const Glib::RefPtr& file, int priority) { 175 | auto && path = file->get_path(); 176 | if (auto result = desktop_ids_info.find(id); result != desktop_ids_info.end()) { 177 | auto && meta = result->second; 178 | if (meta.priority < priority) { 179 | // changed file is overridden, no need to do anything 180 | return; 181 | } 182 | meta.priority = priority; 183 | 184 | try { 185 | std::unique_ptr desktop_entry{ 186 | new DesktopEntry{ parse_desktop_entry(path, desktop_entry_config) } 187 | }; 188 | 189 | if (meta.state == Metadata::Ok) { 190 | // entry was ok, now ok -> update contents 191 | auto new_index = table.update_entry( 192 | result->second.index, 193 | result->first, 194 | Stats{}, 195 | std::move(desktop_entry) 196 | ); 197 | result->second.index = new_index; 198 | } else { 199 | // entry wasn't ok, but now ok -> add it to table it 200 | meta.index = table.emplace_entry( 201 | result->first, 202 | Stats{}, 203 | std::move(desktop_entry) 204 | ); 205 | meta.state = Metadata::Ok; 206 | } 207 | } catch (entry_parse::Hidden) { 208 | if (meta.state == Metadata::Ok) { 209 | table.erase_entry(meta.index); 210 | } 211 | meta.state = Metadata::Hidden; 212 | } catch (entry_parse::Error) { 213 | Log::error("Failed to load desktop file'", path, "'"); 214 | if (meta.state == Metadata::Ok) { 215 | table.erase_entry(meta.index); 216 | } 217 | meta.state = Metadata::Invalid; 218 | } 219 | } else { 220 | // there was not such entry, add it 221 | try_load_entry_(std::move(id), path, priority); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /common/nwg_classes.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Classes for nwg-launchers 3 | * Copyright (c) 2021 Érico Nogueira 4 | * e-mail: ericonr@disroot.org 5 | * Copyright (c) 2021 Piotr Miller 6 | * e-mail: nwg.piotr@gmail.com 7 | * Website: http://nwg.pl 8 | * Project: https://github.com/nwg-piotr/nwg-launchers 9 | * License: GPL3 10 | * */ 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #ifdef HAVE_GTK_LAYER_SHELL 23 | #include 24 | #endif 25 | 26 | #include "filesystem-compat.h" 27 | 28 | template 29 | struct Overloaded: Os... { using Os::operator()...; }; 30 | template Overloaded(Os ...) -> Overloaded; 31 | 32 | struct RGBA { 33 | double red; 34 | double green; 35 | double blue; 36 | double alpha; 37 | }; 38 | 39 | /* 40 | * Argument parser 41 | * Credits for this cool class go to iain at https://stackoverflow.com/a/868894 42 | * */ 43 | class InputParser{ 44 | public: 45 | InputParser (int, char **); 46 | /// @author iain 47 | std::string_view getCmdOption(std::string_view) const; 48 | /// @author iain 49 | bool cmdOptionExists(std::string_view) const; 50 | RGBA get_background_color(double default_opacity) const; 51 | private: 52 | std::vector tokens; 53 | }; 54 | 55 | #ifdef HAVE_GTK_LAYER_SHELL 56 | struct LayerShellArgs { 57 | GtkLayerShellLayer layer = GTK_LAYER_SHELL_LAYER_OVERLAY; 58 | int exclusive_zone = -1; 59 | bool exclusive_zone_is_auto = true; 60 | 61 | LayerShellArgs(const InputParser& parser); 62 | }; 63 | #endif 64 | 65 | enum class HAlign: unsigned int { NotSpecified = 0, Left, Right }; 66 | enum class VAlign: unsigned int { NotSpecified = 0, Top, Bottom }; 67 | 68 | /* 69 | * Stores configuration data 70 | */ 71 | struct Config { 72 | const InputParser& parser; 73 | std::string wm; 74 | std::string theme; 75 | std::string_view title; 76 | std::string_view role; 77 | HAlign halign{ HAlign::NotSpecified }; 78 | VAlign valign{ VAlign::NotSpecified }; 79 | fs::path css_filename{ "style.css" }; // filename relative to config dir 80 | 81 | #ifdef HAVE_GTK_LAYER_SHELL 82 | LayerShellArgs layer_shell_args; 83 | #endif 84 | 85 | Config(const InputParser&, std::string_view, std::string_view, const Glib::RefPtr&); 86 | }; 87 | 88 | class CommonWindow : public Gtk::Window { 89 | public: 90 | CommonWindow(Config&); 91 | virtual ~CommonWindow() = default; 92 | 93 | void check_screen(); 94 | void set_background_color(RGBA color); 95 | 96 | virtual int get_height(); // we need to override get_height for dmenu to work 97 | 98 | std::string_view title_view(); 99 | protected: 100 | bool on_draw(const ::Cairo::RefPtr< ::Cairo::Context>& cr) override; 101 | void on_screen_changed(const Glib::RefPtr& previous_screen) override; 102 | private: 103 | std::string_view title; 104 | RGBA background_color; 105 | bool _SUPPORTS_ALPHA; 106 | }; 107 | 108 | class AppBox : public Gtk::Button { 109 | public: 110 | AppBox(); 111 | AppBox(Glib::ustring, Glib::ustring, Glib::ustring); 112 | AppBox(AppBox&&) = default; 113 | AppBox(const AppBox&) = delete; 114 | 115 | Glib::ustring name; 116 | Glib::ustring exec; 117 | Glib::ustring comment; 118 | 119 | virtual ~AppBox() = default; 120 | }; 121 | 122 | /* 123 | * Stores x, y, width, height 124 | * */ 125 | struct Geometry { 126 | int x; 127 | int y; 128 | int width; 129 | int height; 130 | }; 131 | 132 | struct DesktopEntry { 133 | std::string name; 134 | std::string exec; 135 | std::string icon; 136 | std::string comment; 137 | std::string mime_type; 138 | std::vector categories; 139 | bool terminal; 140 | }; 141 | 142 | struct Instance { 143 | Gtk::Application& app; 144 | fs::path pid_file; 145 | int pid_lock_fd; 146 | 147 | Instance(Gtk::Application& app, std::string_view name); 148 | virtual ~Instance(); 149 | // note: the provided implementation of on_{sigterm,sigint} handlers 150 | // calls Gtk::Application::quit, which does NOT call any destructors 151 | virtual void on_sigterm(); 152 | virtual void on_sigusr1(); 153 | virtual void on_sighup(); 154 | virtual void on_sigint(); 155 | }; 156 | 157 | struct IconProvider { 158 | Glib::RefPtr icon_theme; 159 | Glib::RefPtr fallback; 160 | int icon_size; 161 | 162 | IconProvider(const Glib::RefPtr& theme, int icon_size); 163 | // Returns Gtk::Image out of the icon name of file path 164 | // the returned image is scaled to icon_size x icon_size 165 | Gtk::Image load_icon(const std::string& icon) const; 166 | }; 167 | 168 | enum class SwayError { 169 | ConnectFailed, 170 | EnvNotSet, 171 | OpenFailed, 172 | RecvHeaderFailed, 173 | RecvBodyFailed, 174 | SendHeaderFailed, 175 | SendBodyFailed 176 | }; 177 | 178 | struct SwaySock { 179 | SwaySock(); 180 | SwaySock(const SwaySock&) = delete; 181 | ~SwaySock(); 182 | // pass the command to sway via socket 183 | template 184 | void run(Ts ... ts) { 185 | auto body_size = (ts.size() + ...); 186 | send_header_(body_size, Commands::Run); 187 | // should we send it as one message? 1 write() is better than N 188 | (send_body_(ts), ...); 189 | // should we recv the response? 190 | // suppress warning 191 | (void)recv_response_(); 192 | } 193 | // swaymsg -t get_outputs 194 | std::string get_outputs(); 195 | std::string get_workspaces(); 196 | 197 | // see sway-ipc (7) 198 | enum class Commands: std::uint32_t { 199 | Run = 0, 200 | GetWorkspaces = 1, 201 | GetOutputs = 3 202 | }; 203 | static constexpr std::array MAGIC { 'i', '3', '-', 'i', 'p', 'c' }; 204 | static constexpr auto MAGIC_SIZE = MAGIC.size(); 205 | // magic + body length (u32) + type (u32) 206 | static constexpr auto HEADER_SIZE = MAGIC_SIZE + 2 * sizeof(std::uint32_t); 207 | 208 | int sock_; 209 | std::array header; 210 | 211 | void send_header_(std::uint32_t, Commands); 212 | void send_body_(std::string_view); 213 | std::string recv_response_(); 214 | }; 215 | 216 | /* 217 | * This namespace defines types that can be passed to PlatformWindow::show 218 | * The rationale behind this design is as follows: 219 | * 1) Logic implementing all the positioning is collected in one place, 220 | * not scattered across classes / spagetti functions 221 | * 2) It is resolved at compile time, allowing compiler to optimize it better 222 | * 3) It can be tweaked and expanded with ease and safety (everything is checked at compile-time) 223 | */ 224 | namespace hint { 225 | constexpr struct Fullscreen_ {} Fullscreen; 226 | constexpr struct Center_ {} Center; 227 | struct Horizontal{}; 228 | struct Vertical{}; 229 | template 230 | struct Side { bool side; int margin; constexpr static S side_type{}; }; 231 | struct Sides { Side h; Side v; }; 232 | } 233 | 234 | /* 235 | * Each shell defined by which means window is positioned. They do not share common base class, 236 | * but instead wrapped in a variant to avoid unecessary dynamic allocations 237 | * Shell::show method receives a window reference and a templated parameter, 238 | * it's job is to position window according to the parameter type 239 | * GenericShell only uses common Gtk functions and is best used on X11 or as a fallback 240 | * SwayShell uses IPC connection to Sway/i3 241 | * LayerShell uses wlr-layer-shell (or rather gtk-layer-shell library built on top of it) 242 | */ 243 | struct GenericShell { 244 | GenericShell(Config& config); 245 | Geometry geometry(CommonWindow& window); 246 | template void show(CommonWindow&, S); 247 | // some window managers (openbox, notably) do not open window in fullscreen 248 | // when requested 249 | bool respects_fullscreen = true; 250 | }; 251 | 252 | struct SwayShell: GenericShell { 253 | SwayShell(CommonWindow& window, Config& config); 254 | // use GenericShell::show unless called with Fullscreen 255 | using GenericShell::show; 256 | void show(CommonWindow& window, hint::Fullscreen_); 257 | 258 | SwaySock sock_; 259 | }; 260 | 261 | #ifdef HAVE_GTK_LAYER_SHELL 262 | struct LayerShell { 263 | LayerShell(CommonWindow& window, LayerShellArgs args); 264 | template void show(CommonWindow& window, S); 265 | 266 | LayerShellArgs args; 267 | }; 268 | #endif 269 | 270 | struct PlatformWindow: public CommonWindow { 271 | public: 272 | PlatformWindow(Config& config); 273 | void fullscreen(); 274 | template void show(S); 275 | private: 276 | std::variant< 277 | #ifdef HAVE_GTK_LAYER_SHELL 278 | LayerShell, 279 | #endif 280 | SwayShell, GenericShell> shell; 281 | }; 282 | 283 | template 284 | void GenericShell::show(CommonWindow& window, Hint hint) { 285 | window.show(); 286 | window.set_type_hint(Gdk::WINDOW_TYPE_HINT_SPLASHSCREEN); 287 | window.set_decorated(false); 288 | auto display = geometry(window); 289 | auto window_coord_at_side = [](auto d_size, auto w_size, auto side, auto margin) { 290 | std::array map { margin, d_size - w_size - margin }; 291 | return map[side]; 292 | }; 293 | Overloaded place_window { 294 | [&](hint::Center_) { 295 | auto x = display.x + (display.width - window.get_width()) / 2; 296 | auto y = display.y + (display.height - window.get_height()) / 2; 297 | window.move(x, y); 298 | }, 299 | [&](hint::Fullscreen_) { 300 | if (this->respects_fullscreen) { 301 | window.fullscreen(); 302 | } else { 303 | window.resize(display.width, display.height); 304 | window.move(display.x, display.y); 305 | } 306 | }, 307 | [&](hint::Side hint) { 308 | auto w_x = window_coord_at_side(display.width, window.get_width(), hint.side, hint.margin); 309 | window.move(display.x + w_x, display.y + (display.height - window.get_height()) / 2); 310 | }, 311 | [&](hint::Side hint) { 312 | auto w_y = window_coord_at_side(display.height, window.get_height(), hint.side, hint.margin); 313 | window.move(display.x + (display.width - window.get_width()) / 2, display.y + w_y); 314 | }, 315 | [&,display](hint::Sides hint) { 316 | auto w_x = window_coord_at_side(display.width, window.get_width(), hint.h.side, hint.h.margin); 317 | auto w_y = window_coord_at_side(display.height, window.get_height(), hint.v.side, hint.v.margin); 318 | window.move(display.x + w_x, display.y + w_y); 319 | } 320 | }; 321 | place_window(hint); 322 | window.present(); // grab focus 323 | } 324 | 325 | #ifdef HAVE_GTK_LAYER_SHELL 326 | template 327 | void LayerShell::show(CommonWindow& window, Hint hint) { 328 | std::array edges{ 0, 0, 0, 0 }; 329 | std::array margins{ 0, 0, 0, 0 }; 330 | constexpr Overloaded index { [](hint::Horizontal){ return 0; }, [](hint::Vertical) { return 2; } }; 331 | auto account_side = [&](auto side) { 332 | constexpr auto i = index(side.side_type); 333 | edges[i + side.side] = true; 334 | margins[i + side.side] = side.margin; 335 | }; 336 | Overloaded set_edges_margins { 337 | [&](hint::Center_) { /* nothing to do */ }, 338 | [&](hint::Fullscreen_) { edges = { 1, 1, 1, 1 }; }, 339 | [&](hint::Sides hint) { account_side(hint.h); account_side(hint.v); }, 340 | account_side 341 | }; 342 | set_edges_margins(hint); 343 | window.show(); 344 | auto gtk_win = window.gobj(); 345 | std::array edges_ { 346 | GTK_LAYER_SHELL_EDGE_LEFT, 347 | GTK_LAYER_SHELL_EDGE_RIGHT, 348 | GTK_LAYER_SHELL_EDGE_TOP, 349 | GTK_LAYER_SHELL_EDGE_BOTTOM 350 | }; 351 | for (size_t i = 0; i < 4; i++) { 352 | gtk_layer_set_anchor(gtk_win, edges_[i], edges[i]); 353 | gtk_layer_set_margin(gtk_win, edges_[i], margins[i]); 354 | } 355 | gtk_layer_set_layer(gtk_win, args.layer); 356 | gtk_layer_set_keyboard_interactivity(gtk_win, true); 357 | gtk_layer_set_namespace(gtk_win, window.title_view().data()); 358 | if (args.exclusive_zone_is_auto) { 359 | gtk_layer_auto_exclusive_zone_enable (gtk_win); 360 | } else { 361 | gtk_layer_set_exclusive_zone(gtk_win, args.exclusive_zone); 362 | } 363 | } 364 | #endif 365 | 366 | template 367 | void PlatformWindow::show(Hint h) { 368 | std::visit([&](auto& shell){ shell.show(*this, h); }, shell); 369 | } 370 | 371 | 372 | -------------------------------------------------------------------------------- /README.md.in: -------------------------------------------------------------------------------- 1 | # nwg-launchers 2 | 3 | [![Build Status](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/ubuntu.yml) 4 | [![Build Status](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/freebsd.yml/badge.svg)](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/freebsd.yml) 5 | 6 | ## This project is community-driven 7 | 8 | As it seems I'm not going to live long enough to learn C++ properly, I decided to develop my launchers from scratch in Go. You'll find them in the 9 | [nwg-shell](https://github.com/nwg-piotr/nwg-shell) project. They only support sway, and partially other wlroots-based compositors. Nwg-launchers 10 | is a community-driven project from now on. The main developer is [Siborgium](https://github.com/Siborgium). 11 | ## Description 12 | 13 | It's damned difficult to make all the stuff behave properly on all window managers. My priorities are: 14 | 15 | 1. it **must work well** on sway; 16 | 2. it **should work as well as possible** on Wayfire, i3, dwm and Openbox. 17 | 18 | Feel free to report issues you encounter on other window managers / desktop environments, but they may or may not be resolved. 19 | 20 | ## Packages 21 | 22 | The latest released version is [available](https://aur.archlinux.org/packages/nwg-launchers) in Arch User Repository. 23 | Current development version (`master` branch) may be installed as the `nwg-launchers-git` AUR package. 24 | For other Linux distributions see the table below. 25 | 26 | [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-launchers.svg)](https://repology.org/project/nwg-launchers/versions) 27 | 28 | ## Building and installing 29 | 30 | To build nwg-launchers from source, you need a copy of the source code, which 31 | can be obtained by cloning the repository or by downloading and unpacking [the 32 | latest release](https://github.com/nwg-piotr/nwg-launchers/releases/latest). 33 | 34 | ### Dependencies 35 | 36 | ##### Build dependencies 37 | - `meson` and `ninja` 38 | - `nlohmann-json` - will be downloaded as a subproject if not found on the system 39 | 40 | ##### Runtime dependencies 41 | - `gtkmm3` (`libgtkmm-3.0-dev`) 42 | - `gtk-layer-shell` - optional, set to `auto` by default; will be downloaded as a subproject if explicitly enabled, but not found on the system 43 | - `librsvg` - optional, required to support SVG icons 44 | 45 | ### Building 46 | 47 | This project uses the Meson build system for building and installing the 48 | executables and the necessary data. The options that can be passed to the 49 | `meson` command can be found in the `meson_options.txt` file, and can be used to 50 | disable building some of the available programs. 51 | 52 | ``` 53 | $ git clone https://github.com/nwg-piotr/nwg-launchers.git 54 | $ cd nwg-launchers 55 | $ meson builddir -Dbuildtype=release 56 | $ ninja -C builddir 57 | ``` 58 | 59 | ### Installation 60 | 61 | To install: 62 | 63 | ``` 64 | $ sudo ninja -C builddir install 65 | ``` 66 | 67 | To uninstall: 68 | 69 | ``` 70 | $ sudo ninja -C builddir uninstall 71 | ``` 72 | 73 | **Note: the descriptions below apply to the `master` branch. Certain features may or may not be available in the latest 74 | release, as well as in the current package for your Linux distribution.** 75 | 76 | ## nwggrid 77 | 78 | This command creates a GNOME-like application grid, with the search box, optionally prepended with a row of `-f` favourites 79 | (most frequently used apps) or `-p` pinned program icons. 80 | 81 | This only works with the `-p` argument: 82 | 83 | - to pin up a program icon, right click its icon in the applications grid; 84 | - to unpin a program, right click its icon in the pinned programs grid. 85 | 86 | *Hit "Delete" key to clear the search box.* 87 | 88 | [![Swappshot-Mon-Mar-23-205030-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-205030-2020.th.png)](https://scrot.cloud/image/jb3k) [![Swappshot-Mon-Mar-23-205157-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-205157-2020.th.png)](https://scrot.cloud/image/jOWg) [![Swappshot-Mon-Mar-23-205248-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-205248-2020.th.png)](https://scrot.cloud/image/joh5) 89 | 90 | Starting with version 0.6.0 nwggrid can be run in server mode which drastically improves responsiveness. 91 | First, start a server with `nwggrid-server` command. 92 | When it's up and running, run `nwggrid -client` to show the grid. 93 | 94 | Starting with version 0.7.0 nwggrid has limited support for XDG Desktop Menu Categories. 95 | A list of toggles is displayed between pinned/favorite grids and ordinary one. 96 | Clicking on a button displays entries of said category, 97 | and holding Ctrl allows to select multiple categories at the same time. 98 | Only non-empty categories from the list of "known" categories are shown. 99 | The list can be customized via `/your/config/dir/nwg-launchers/nwggrid/grid.conf`, a JSON configuration file. 100 | Sample file is provided along with other nwg-launchers sample configuration files. 101 | I plan to move all customization points to JSON config in the future. 102 | If the file is not present, a fixed list of categories is used. 103 | Additionally, you may use `-no-categories` to disable categories, or set `no-categories: false` in configuration file. 104 | 105 | ### Usage 106 | 107 | ``` 108 | $ nwggrid -h 109 | HELP_OUTPUT_FOR_GRID_CLIENT 110 | ``` 111 | 112 | ``` 113 | $ nwggrid-server -h 114 | HELP_OUTPUT_FOR_GRID_SERVER 115 | ``` 116 | 117 | ### Terminal applications 118 | 119 | `.desktop` files with the `Terminal=true` line should be started in a terminal emulator. There's no common method 120 | to determine which terminal to use. The `nwggrid` command since v0.4.1 at the first run will look for installed 121 | terminals in the following order: alacritty, kitty, urxvt, foot, lxterminal, sakura, st, termite, terminator, xfce4-terminal, 122 | gnome-terminal. The name of the first one found will be saved to the `~/.config/nwg-launchers/nwggrid/terminal` file. 123 | If none of above is found, the fallback `xterm` value will be saved, regardless of whether xterm is installed or not. 124 | You may edit the `term` file to use another terminal. 125 | 126 | ### Custom background 127 | 128 | Use -b | argument (w/o #) to define custom background colour. If alpha value given, it overrides 129 | the opacity, as well default, as defined with the -o argument. 130 | 131 | ### Custom styling 132 | 133 | On first run the program creates the `nwg-launchers/nwggrid` folder in your .config directory. You'll find the `style.css` files inside. 134 | You may edit the style sheet to your liking. 135 | 136 | ### Customization 137 | 138 | On first run the program creates the `nwg-launchers/nwggrid` folder in your .config directory. You'll find a sample template `grid.conf` file inside. 139 | 140 | Below lists the keys currently available to be set in the `grid.conf` file. Not all keys shown below are set by in the sample template, such as the `"custom-path"` and `"language"` keys. 141 | 142 | It should be noted that the `"favorites"` and `"pins"` keys should not be set to `true` if the `"custom-path"` key is set, as those options will not work. 143 | 144 | ```json 145 | { 146 | "categories": { 147 | "AudioVideo" : "Multimedia 📀", 148 | "Development" : "Development 💻", 149 | "Education" : "Education 🎓", 150 | "Game" : "Games 🎮", 151 | "Graphics" : "Graphics 🎨", 152 | "Network" : "Network 🌎", 153 | "Office" : "Office 💼", 154 | "Science" : "Science 🔬", 155 | "Settings" : "Settings ⚙️", 156 | "System" : "System 🖥️", 157 | "Utility" : "Utility 🛠️" 158 | }, 159 | "custom-path" : "/my/path1:/my/another path:/third/path", 160 | "favorites" : false, 161 | "pins" : false, 162 | "columns" : 6, 163 | "icon-size" : 72, 164 | "language" : "en", 165 | "no-categories": false, 166 | "oneshot" : false 167 | } 168 | ``` 169 | 170 | ## nwgbar 171 | 172 | This command creates a horizontal or vertical button bar, out of a template file. 173 | 174 | [![Swappshot-Mon-Mar-23-210713-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-210713-2020.th.png)](https://scrot.cloud/image/jRPQ) [![Swappshot-Mon-Mar-23-210652-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-210652-2020.th.png)](https://scrot.cloud/image/j8LU) 175 | 176 | ### Usage 177 | 178 | ``` 179 | $ nwgbar -h 180 | HELP_OUTPUT_FOR_BAR 181 | ``` 182 | 183 | ### Custom background 184 | 185 | Use -b | argument (w/o #) to define custom background colour. If alpha value given, it overrides 186 | the opacity, as well default, as defined with the -o argument. 187 | 188 | ### Customization 189 | 190 | On first run the program creates the `nwg-launchers/nwgbar` folder in your .config directory. You'll find a sample 191 | template `bar.json` and the `style.css` files inside. 192 | 193 | Templates use json format. The default one defines an example Exit menu for sway window manager on Arch Linux: 194 | 195 | ```json 196 | [ 197 | { 198 | "name": "Lock screen", 199 | "exec": "swaylock -f -c 000000", 200 | "icon": "system-lock-screen" 201 | }, 202 | { 203 | "name": "Logout", 204 | "exec": "swaymsg exit", 205 | "icon": "system-log-out" 206 | }, 207 | { 208 | "name": "Reboot", 209 | "exec": "systemctl reboot", 210 | "icon": "system-reboot" 211 | }, 212 | { 213 | "name": "Shutdown", 214 | "exec": "systemctl -i poweroff", 215 | "icon": "system-shutdown" 216 | } 217 | ] 218 | ``` 219 | 220 | To set a keyboard shortcut (using Alt+KEY) for an entry, you can add an underscore before the letter you want to use. 221 | Example to set `s` as the shortcut: 222 | 223 | ``` 224 | [ 225 | ... 226 | { 227 | "name": "Lock _screen", 228 | "exec": "swaylock -f -c 000000", 229 | "icon": "system-lock-screen" 230 | } 231 | ... 232 | ] 233 | ``` 234 | 235 | **Note for underscore ("_")** 236 | 237 | If you want to use an underscore in the name, you have to double it ("__"). 238 | 239 | **Wayfire note** 240 | 241 | For the Logout button, as in the bar above, you may use [wayland-logout](https://github.com/soreau/wayland-logout) by @soreau. 242 | 243 | You may use as many templates as you need, with the `-t` argument. All of them must be placed in the config directory. 244 | You may use own icon files instead of icon names, like `/path/to/the/file/my_icon.svg`. 245 | 246 | The style sheet makes the buttons look similar to `nwggrid`. You can customize them as well. 247 | 248 | ## nwgdmenu 249 | 250 | This program provides 2 commands: 251 | 252 | - ` | nwgdmenu` - displays newline-separated stdin input as a GTK menu 253 | - `nwgdmenu` - creates a GTK menu out of commands found in $PATH 254 | 255 | *Hit "Delete" to clear the search box.* 256 | *Hit "Insert" to switch case sensitivity.* 257 | 258 | [![Swappshot-Mon-Mar-23-211702-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-211702-2020.th.png)](https://scrot.cloud/image/jfHK) [![Swappshot-Mon-Mar-23-211911-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-211911-2020.th.png)](https://scrot.cloud/image/j3MG) [![Swappshot-Mon-Mar-23-211736-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-211736-2020.th.png)](https://scrot.cloud/image/jvOi) 259 | 260 | ### Usage 261 | 262 | ``` 263 | $ nwgdmenu -h 264 | HELP_OUTPUT_FOR_DMENU 265 | ``` 266 | 267 | (1) _The program should auto-detect if something has been passed in `stdin`, and build the menu out of the `stdin` content 268 | or from commands found in `$PATH` accordingly. However, in some specific cases (e.g. if you use gdm and start nwgdmenu 269 | from a key binding) the `stdin` content detection may be false-positive, which results in displaying an empty menu. 270 | In such case use the `nwgdmenu -run` instead, to force building the menu out of commands in `$PATH`._ 271 | 272 | Notice: if you start your WM from a script (w/o DM), only sway and i3 will be auto-detected. You may need to pass the WM name as the argument: 273 | 274 | `nwgdmenu -wm dwm` 275 | 276 | The generic name `tiling` will be accepted as well. 277 | 278 | ### Custom background 279 | 280 | Use -b | argument (w/o #) to define custom background colour. If alpha value given, it overrides 281 | the opacity, as well default, as defined with the -o argument. 282 | 283 | ### Custom styling 284 | 285 | On first run the program creates the `nwg-launchers/nwgdmenu` folder in your .config directory. You'll find the 286 | default `style.css` files inside. Use it to adjust styling and a vertical margin to the menu, if needed. 287 | 288 | ## i3 note 289 | 290 | In case you use default window borders, an exclusion like this may be necessary: 291 | 292 | ``` 293 | for_window [title="~nwg"] border none 294 | ``` 295 | 296 | ### Openbox Note 297 | 298 | To start nwgdmenu from a key binding, use the `-run` argument, e.g.: 299 | 300 | ```xml 301 | 302 | 303 | nwgdmenu -run 304 | 305 | 306 | ``` 307 | 308 | ### wlr-layer-shell protocol 309 | 310 | From 0.5.0 the nwg-launchers support wlr-layer-shell protocol (via gtk-layer-shell), and use it where preferred. The default layer is `OVERLAY` and the default exclusive zone is `auto`, but you can change it using command line arguments. Notably, you may want to set exclusive zone to `-1` to show nwggrid or nwgbar on top of panels (waybar, wf-panel, etc). 311 | 312 | ### Tips & tricks 313 | 314 | ### Hide unwanted icons in nwggrid 315 | 316 | See: [https://wiki.archlinux.org/index.php/desktop_entries#Hide_desktop_entries](https://wiki.archlinux.org/index.php/desktop_entries#Hide_desktop_entries) 317 | -------------------------------------------------------------------------------- /common/nwg_classes.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Classes for nwg-launchers 3 | * Copyright (c) 2021 Érico Nogueira 4 | * e-mail: ericonr@disroot.org 5 | * Copyright (c) 2021 Piotr Miller 6 | * e-mail: nwg.piotr@gmail.com 7 | * Website: http://nwg.pl 8 | * Project: https://github.com/nwg-piotr/nwg-launchers 9 | * License: GPL3 10 | * */ 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include "charconv-compat.h" 20 | #include "nwgconfig.h" 21 | #include "nwg_classes.h" 22 | #include "nwg_exceptions.h" 23 | #include "nwg_tools.h" 24 | #include "log.h" 25 | 26 | InputParser::InputParser (int argc, char **argv) { 27 | tokens.reserve(argc - 1); 28 | for (int i = 1; i < argc; ++i) { 29 | tokens.emplace_back(argv[i]); 30 | } 31 | } 32 | 33 | std::string_view InputParser::getCmdOption(std::string_view option) const { 34 | auto itr = std::find(this->tokens.begin(), this->tokens.end(), option); 35 | if (itr != this->tokens.end() && ++itr != this->tokens.end()){ 36 | return *itr; 37 | } 38 | return {}; 39 | } 40 | 41 | bool InputParser::cmdOptionExists(std::string_view option) const { 42 | return std::find(this->tokens.begin(), this->tokens.end(), option) 43 | != this->tokens.end(); 44 | } 45 | 46 | RGBA InputParser::get_background_color(double default_opacity) const { 47 | RGBA color{ 0.0, 0.0, 0.0, default_opacity }; 48 | if (auto opacity_str = getCmdOption("-o"); !opacity_str.empty()) { 49 | auto opacity = std::stod(std::string{opacity_str}); 50 | if (opacity >= 0.0 && opacity <= 1.0) { 51 | color.alpha = opacity; 52 | } else { 53 | Log::error("Opacity must be in range 0.0 to 1.0"); 54 | } 55 | } 56 | if (auto color_str = getCmdOption("-b"); !color_str.empty()) { 57 | decode_color(color_str, color); 58 | } 59 | return color; 60 | } 61 | 62 | Config::Config(const InputParser& parser, std::string_view title, std::string_view role, const Glib::RefPtr& screen): 63 | parser{parser}, 64 | title{title}, 65 | role{role} 66 | #ifdef HAVE_GTK_LAYER_SHELL 67 | ,layer_shell_args{parser} 68 | #endif 69 | { 70 | if (auto wm_name = parser.getCmdOption("-wm"); !wm_name.empty()){ 71 | this->wm = wm_name; 72 | } else { 73 | this->wm = detect_wm(screen->get_display(), screen); 74 | } 75 | Log::info("wm: ", this->wm); 76 | 77 | auto halign_ = parser.getCmdOption("-ha"); 78 | if (halign_ == "l" || halign_ == "left") { halign = HAlign::Left; } 79 | if (halign_ == "r" || halign_ == "right") { halign = HAlign::Right; } 80 | auto valign_ = parser.getCmdOption("-va"); 81 | if (valign_ == "t" || valign_ == "top") { valign = VAlign::Top; } 82 | if (valign_ == "b" || valign_ == "bottom") { valign = VAlign::Bottom; } 83 | 84 | if (auto css_name = parser.getCmdOption("-c"); !css_name.empty()) { 85 | css_filename = css_name; 86 | } 87 | 88 | if (auto theme = parser.getCmdOption("-g"); !theme.empty()) { 89 | this->theme = theme; 90 | } 91 | } 92 | 93 | CommonWindow::CommonWindow(Config& config): title{config.title} { 94 | set_title({config.title.data(), config.title.size()}); 95 | set_role({config.role.data(), config.role.size()}); 96 | set_skip_pager_hint(true); 97 | add_events(Gdk::KEY_PRESS_MASK | Gdk::KEY_RELEASE_MASK); 98 | set_app_paintable(true); 99 | check_screen(); 100 | } 101 | 102 | std::string_view CommonWindow::title_view() { return title; } 103 | 104 | bool CommonWindow::on_draw(const Cairo::RefPtr& cr) { 105 | cr->save(); 106 | auto [r, g, b, a] = this->background_color; 107 | if (_SUPPORTS_ALPHA) { 108 | cr->set_source_rgba(r, g, b, a); 109 | } else { 110 | cr->set_source_rgb(r, g, b); 111 | } 112 | cr->set_operator(Cairo::OPERATOR_SOURCE); 113 | cr->paint(); 114 | cr->restore(); 115 | return Gtk::Window::on_draw(cr); 116 | } 117 | 118 | void CommonWindow::on_screen_changed(const Glib::RefPtr& previous_screen) { 119 | (void) previous_screen; // suppress warning 120 | this->check_screen(); 121 | } 122 | 123 | void CommonWindow::check_screen() { 124 | auto screen = get_screen(); 125 | auto visual = screen -> get_rgba_visual(); 126 | 127 | if (!visual) { 128 | Log::warn("Your screen does not support alpha channels!"); 129 | } 130 | _SUPPORTS_ALPHA = (bool)visual; 131 | gtk_widget_set_visual(GTK_WIDGET(gobj()), visual->gobj()); 132 | } 133 | 134 | void CommonWindow::set_background_color(RGBA color) { 135 | this->background_color = color; 136 | } 137 | 138 | int CommonWindow::get_height() { return Gtk::Window::get_height(); } 139 | 140 | AppBox::AppBox() { 141 | this -> set_always_show_image(true); 142 | } 143 | 144 | AppBox::AppBox(Glib::ustring name, Glib::ustring exec, Glib::ustring comment): 145 | Gtk::Button(name, true), 146 | name{std::move(name)}, 147 | exec{std::move(exec)}, 148 | comment{std::move(comment)} 149 | { 150 | if (this->name.length() > 25) { 151 | this->name.resize(22); 152 | this->name.append("..."); 153 | } 154 | this -> set_always_show_image(true); 155 | } 156 | 157 | Instance::Instance(Gtk::Application& app, std::string_view name): app{ app } { 158 | // TODO: maybe use dbus if it is present? 159 | pid_file = get_pid_file(name); 160 | pid_file += ".pid"; 161 | auto lock_file = pid_file; 162 | lock_file += ".lock"; 163 | 164 | // we'll need this lock file to synchronize us & running instance 165 | // note: it doesn't get unlinked when the program exits 166 | // so the other instance can safely wait on this file 167 | pid_lock_fd = open(lock_file.c_str(), O_CLOEXEC | O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR); 168 | if (pid_lock_fd < 0) { 169 | int err = errno; 170 | throw ErrnoException{ "failed to open pid lock: ", err }; 171 | } 172 | 173 | // let's try to read pid file 174 | if (auto pid = get_instance_pid(pid_file.c_str())) { 175 | Log::info("Another instance is running, trying to terminate it..."); 176 | if (kill(*pid, SIGTERM) != 0) { 177 | throw std::runtime_error{ "failed to send SIGTERM to pid" }; 178 | } 179 | Log::plain("Success"); 180 | } 181 | 182 | // acquire lock 183 | // we'll hold this lock until the very exit 184 | if (lockf(pid_lock_fd, F_LOCK, 0)) { 185 | int err = errno; 186 | throw ErrnoException{ "failed to lock the pid lock: ", err }; 187 | } 188 | 189 | // write instance pid 190 | write_instance_pid(pid_file.c_str(), getpid()); 191 | 192 | // using glib unix extensions instead of plain signals allows for arbitrary functions to be used 193 | // when handling signals 194 | g_unix_signal_add(SIGHUP, instance_on_sighup, this); 195 | g_unix_signal_add(SIGINT, instance_on_sigint, this); 196 | g_unix_signal_add(SIGUSR1, instance_on_sigusr1, this); 197 | g_unix_signal_add(SIGTERM, instance_on_sigterm, this); 198 | } 199 | 200 | void Instance::on_sighup(){} 201 | void Instance::on_sigint(){ app.quit(); } 202 | void Instance::on_sigusr1() {} 203 | void Instance::on_sigterm(){ app.quit(); } 204 | 205 | Instance::~Instance() { 206 | // it is important to delete pid file BEFORE releasing the lock 207 | // otherwise other instance may overwrite it just before we delete it 208 | if (std::error_code err; !fs::remove(pid_file, err) && err) { 209 | Log::error("Failed to remove pid file '", pid_file, "': ", err.message()); 210 | } 211 | if (lockf(pid_lock_fd, F_ULOCK, 0)) { 212 | int err = errno; 213 | Log::error("Failed to unlock pid lock: ", error_description(err)); 214 | } 215 | close(pid_lock_fd); 216 | } 217 | 218 | IconProvider::IconProvider(const Glib::RefPtr& theme, int icon_size): 219 | icon_theme{ theme }, 220 | icon_size{ icon_size } 221 | { 222 | constexpr std::array fallback_icons { 223 | DATA_DIR_STR "/icon-missing.svg", 224 | DATA_DIR_STR "/icon-missing.png" 225 | }; 226 | for (auto && icon: fallback_icons) { 227 | try { 228 | fallback = Gdk::Pixbuf::create_from_file( 229 | icon, 230 | icon_size, 231 | icon_size, 232 | true 233 | ); 234 | break; 235 | } catch (const Glib::Error& e) { 236 | Log::error("Failed to load fallback icon '", icon, "'"); 237 | } 238 | } 239 | if (!fallback) { 240 | throw std::runtime_error{ "No fallback icon available" }; 241 | } 242 | } 243 | 244 | Gtk::Image IconProvider::load_icon(const std::string& icon) const { 245 | if (icon.empty()) { 246 | return Gtk::Image{ fallback }; 247 | } 248 | try { 249 | if (icon.find_first_of("/") == icon.npos) { 250 | return Gtk::Image{ icon_theme->load_icon(icon, icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE) }; 251 | } else { 252 | return Gtk::Image{ Gdk::Pixbuf::create_from_file(icon, icon_size, icon_size, true) }; 253 | } 254 | } catch (const Glib::Error& error) { 255 | Log::error("Failed to load icon '", icon, "': ", error.what()); 256 | } 257 | try { 258 | return Gtk::Image{ Gdk::Pixbuf::create_from_file("/usr/share/pixmaps/" + icon, icon_size, icon_size, true) }; 259 | } catch (const Glib::Error& error) { 260 | Log::error("Failed to load icon '", icon, "': ", error.what()); 261 | Log::plain("falling back to placeholder"); 262 | } 263 | return Gtk::Image{ fallback }; 264 | } 265 | 266 | GenericShell::GenericShell(Config& config) { 267 | // respects_fullscreen is default initialized to true 268 | using namespace std::string_view_literals; 269 | constexpr std::array wms { "openbox"sv, "i3"sv, "sway"sv }; 270 | for (auto && wm: wms) { 271 | if (config.wm == wm) { 272 | respects_fullscreen = false; 273 | break; 274 | } 275 | } 276 | } 277 | 278 | Geometry GenericShell::geometry(CommonWindow& window) { 279 | Geometry geo; 280 | auto get_geo = [&](auto && monitor) { 281 | Gdk::Rectangle rect; 282 | monitor->get_geometry(rect); 283 | geo.x = rect.get_x(); 284 | geo.y = rect.get_y(); 285 | geo.width = rect.get_width(); 286 | geo.height = rect.get_height(); 287 | }; 288 | auto display = window.get_display(); 289 | 290 | #ifdef GDK_WINDOWING_X11 291 | // only works on X11, reports 0,0 on wayland 292 | auto device_mgr = display->get_device_manager(); 293 | auto device = device_mgr->get_client_pointer(); 294 | int x, y; 295 | device->get_position(x, y); 296 | if (auto monitor = display->get_monitor_at_point(x, y)) { 297 | get_geo(monitor); 298 | } else 299 | #endif 300 | if (auto monitor = display->get_monitor_at_window(window.get_window())) { 301 | get_geo(monitor); 302 | } else { 303 | throw std::logic_error{ "No monitor at window" }; 304 | } 305 | return geo; 306 | } 307 | 308 | SwayShell::SwayShell(CommonWindow& window, Config& config): 309 | GenericShell{config} 310 | { 311 | window.set_type_hint(Gdk::WINDOW_TYPE_HINT_SPLASHSCREEN); 312 | window.set_decorated(false); 313 | using namespace std::string_view_literals; 314 | sock_.run("for_window [title="sv, window.title_view(), "*] floating enable"sv); 315 | sock_.run("for_window [title="sv, window.title_view(), "*] border none"sv); 316 | } 317 | 318 | void SwayShell::show(CommonWindow& window, hint::Fullscreen_) { 319 | // We can not go fullscreen() here: 320 | // On sway the window would become opaque - we don't want it 321 | // On i3 all windows below will be hidden - we don't want it as well 322 | window.show(); 323 | // works just fine on Sway/i3 as far as I could test 324 | // thus, no need to use ipc (I hope) 325 | auto [x, y, w, h] = geometry(window); 326 | window.resize(w, h); 327 | window.move(x, y); 328 | } 329 | 330 | #ifdef HAVE_GTK_LAYER_SHELL 331 | LayerShellArgs::LayerShellArgs(const InputParser& parser) { 332 | using namespace std::string_view_literals; 333 | if (auto layer = parser.getCmdOption("-layer-shell-layer"); !layer.empty()) { 334 | constexpr std::array map { 335 | std::pair{ "BACKGROUND"sv, GTK_LAYER_SHELL_LAYER_BACKGROUND }, 336 | std::pair{ "BOTTOM"sv, GTK_LAYER_SHELL_LAYER_BOTTOM }, 337 | std::pair{ "TOP"sv, GTK_LAYER_SHELL_LAYER_TOP }, 338 | std::pair{ "OVERLAY"sv, GTK_LAYER_SHELL_LAYER_OVERLAY } 339 | }; 340 | bool found = false; 341 | for (auto && [s, l]: map) { 342 | if (layer == s) { 343 | this->layer = l; 344 | found = true; 345 | break; 346 | } 347 | } 348 | if (!found) { 349 | Log::error("Incorrect layer-shell-layer value"); 350 | std::exit(EXIT_FAILURE); 351 | } 352 | } 353 | if (auto zone = parser.getCmdOption("-layer-shell-exclusive-zone"); !zone.empty()) { 354 | this->exclusive_zone_is_auto = zone == "auto"sv; 355 | if (!this->exclusive_zone_is_auto) { 356 | if (!parse_number(zone, this->exclusive_zone)) { 357 | Log::error("Unable to decode layer-shell-exclusive-zone value"); 358 | std::exit(EXIT_FAILURE); 359 | } 360 | } 361 | } 362 | } 363 | 364 | LayerShell::LayerShell(CommonWindow& window, LayerShellArgs args): args{args} { 365 | // this has to be called before the window is realized 366 | gtk_layer_init_for_window(window.gobj()); 367 | } 368 | #endif 369 | 370 | PlatformWindow::PlatformWindow(Config& config): 371 | CommonWindow{config}, 372 | shell{std::in_place_type, config} 373 | { 374 | #ifdef HAVE_GTK_LAYER_SHELL 375 | if (gtk_layer_is_supported()) { 376 | shell.emplace(*this, config.layer_shell_args); 377 | return; 378 | } 379 | #endif 380 | if (config.wm == "sway" || config.wm == "i3") { 381 | shell.emplace(*this, config); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nwg-launchers 2 | 3 | [![Build Status](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/ubuntu.yml) 4 | [![Build Status](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/freebsd.yml/badge.svg)](https://github.com/nwg-piotr/nwg-launchers/actions/workflows/freebsd.yml) 5 | 6 | ## This project is community-driven 7 | 8 | As it seems I'm not going to live long enough to learn C++ properly, I decided to develop my launchers from scratch in Go. You'll find them in the 9 | [nwg-shell](https://github.com/nwg-piotr/nwg-shell) project. They only support sway, and partially other wlroots-based compositors. Nwg-launchers 10 | is a community-driven project from now on. The main developer is [Siborgium](https://github.com/Siborgium). 11 | ## Description 12 | 13 | It's damned difficult to make all the stuff behave properly on all window managers. My priorities are: 14 | 15 | 1. it **must work well** on sway; 16 | 2. it **should work as well as possible** on Wayfire, i3, dwm and Openbox. 17 | 18 | Feel free to report issues you encounter on other window managers / desktop environments, but they may or may not be resolved. 19 | 20 | ## Packages 21 | 22 | The latest released version is [available](https://aur.archlinux.org/packages/nwg-launchers) in Arch User Repository. 23 | Current development version (`master` branch) may be installed as the `nwg-launchers-git` AUR package. 24 | For other Linux distributions see the table below. 25 | 26 | [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-launchers.svg)](https://repology.org/project/nwg-launchers/versions) 27 | 28 | ## Building and installing 29 | 30 | To build nwg-launchers from source, you need a copy of the source code, which 31 | can be obtained by cloning the repository or by downloading and unpacking [the 32 | latest release](https://github.com/nwg-piotr/nwg-launchers/releases/latest). 33 | 34 | ### Dependencies 35 | 36 | ##### Build dependencies 37 | - `meson` and `ninja` 38 | - `nlohmann-json` - will be downloaded as a subproject if not found on the system 39 | 40 | ##### Runtime dependencies 41 | - `gtkmm3` (`libgtkmm-3.0-dev`) 42 | - `gtk-layer-shell` - optional, set to `auto` by default; will be downloaded as a subproject if explicitly enabled, but not found on the system 43 | - `librsvg` - optional, required to support SVG icons 44 | 45 | ### Building 46 | 47 | This project uses the Meson build system for building and installing the 48 | executables and the necessary data. The options that can be passed to the 49 | `meson` command can be found in the `meson_options.txt` file, and can be used to 50 | disable building some of the available programs. 51 | 52 | ``` 53 | $ git clone https://github.com/nwg-piotr/nwg-launchers.git 54 | $ cd nwg-launchers 55 | $ meson builddir -Dbuildtype=release 56 | $ ninja -C builddir 57 | ``` 58 | 59 | ### Installation 60 | 61 | To install: 62 | 63 | ``` 64 | $ sudo ninja -C builddir install 65 | ``` 66 | 67 | To uninstall: 68 | 69 | ``` 70 | $ sudo ninja -C builddir uninstall 71 | ``` 72 | 73 | **Note: the descriptions below apply to the `master` branch. Certain features may or may not be available in the latest 74 | release, as well as in the current package for your Linux distribution.** 75 | 76 | ## nwggrid 77 | 78 | This command creates a GNOME-like application grid, with the search box, optionally prepended with a row of `-f` favourites 79 | (most frequently used apps) or `-p` pinned program icons. 80 | 81 | This only works with the `-p` argument: 82 | 83 | - to pin up a program icon, right click its icon in the applications grid; 84 | - to unpin a program, right click its icon in the pinned programs grid. 85 | 86 | *Hit "Delete" key to clear the search box.* 87 | 88 | [![Swappshot-Mon-Mar-23-205030-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-205030-2020.th.png)](https://scrot.cloud/image/jb3k) [![Swappshot-Mon-Mar-23-205157-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-205157-2020.th.png)](https://scrot.cloud/image/jOWg) [![Swappshot-Mon-Mar-23-205248-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-205248-2020.th.png)](https://scrot.cloud/image/joh5) 89 | 90 | Starting with version 0.6.0 nwggrid can be run in server mode which drastically improves responsiveness. 91 | First, start a server with `nwggrid-server` command. 92 | When it's up and running, run `nwggrid -client` to show the grid. 93 | 94 | Starting with version 0.7.0 nwggrid has limited support for XDG Desktop Menu Categories. 95 | A list of toggles is displayed between pinned/favorite grids and ordinary one. 96 | Clicking on a button displays entries of said category, 97 | and holding Ctrl allows to select multiple categories at the same time. 98 | Only non-empty categories from the list of "known" categories are shown. 99 | The list can be customized via `/your/config/dir/nwg-launchers/nwggrid/grid.conf`, a JSON configuration file. 100 | Sample file is provided along with other nwg-launchers sample configuration files. 101 | I plan to move all customization points to JSON config in the future. 102 | If the file is not present, a fixed list of categories is used. 103 | Additionally, you may use `-no-categories` to disable categories, or set `no-categories: false` in configuration file. 104 | 105 | ### Usage 106 | 107 | ``` 108 | $ nwggrid -h 109 | GTK application grid: nwggrid 0.7.1.1 (c) 2022 Piotr Miller, Sergey Smirnykh & Contributors 110 | 111 | Options: 112 | -h show this help message and exit 113 | -f display favourites (most used entries); does not work with -d 114 | -p display pinned entries; does not work with -d 115 | -d look for .desktop files in custom paths (-d '/my/path1:/my/another path:/third/path') 116 | -o default (black) background opacity (0.0 - 1.0, default 0.9) 117 | -b background colour in RRGGBB or RRGGBBAA format (RRGGBBAA alpha overrides ) 118 | -n number of grid columns (default: 6) 119 | -s button image size (default: 72) 120 | -c css file name (default: style.css) 121 | -l force use of language 122 | -g GTK theme name 123 | -wm window manager name (if can not be detected) 124 | -no-categories disable categories display 125 | -oneshot run in the foreground, exit when window is closed 126 | generally you should not use this option, use simply `nwggrid` instead 127 | [requires layer-shell]: 128 | -layer-shell-layer {BACKGROUND,BOTTOM,TOP,OVERLAY}, default: OVERLAY 129 | -layer-shell-exclusive-zone {auto, valid integer (usually -1 or 0)}, default: auto 130 | 131 | 132 | ``` 133 | 134 | ``` 135 | $ nwggrid-server -h 136 | GTK application grid: nwggrid 0.7.1.1 (c) 2021 Piotr Miller, Sergey Smirnykh & Contributors 137 | 138 | Usage: 139 | nwggrid -client sends -SIGUSR1 to nwggrid-server, requires nwggrid-server running 140 | nwggrid [ARGS...] launches nwggrid-server -oneshot ARGS... 141 | 142 | See also: 143 | nwggrid-server -h 144 | 145 | 146 | ``` 147 | 148 | ### Terminal applications 149 | 150 | `.desktop` files with the `Terminal=true` line should be started in a terminal emulator. There's no common method 151 | to determine which terminal to use. The `nwggrid` command since v0.4.1 at the first run will look for installed 152 | terminals in the following order: alacritty, kitty, urxvt, foot, lxterminal, sakura, st, termite, terminator, xfce4-terminal, 153 | gnome-terminal. The name of the first one found will be saved to the `~/.config/nwg-launchers/nwggrid/terminal` file. 154 | If none of above is found, the fallback `xterm` value will be saved, regardless of whether xterm is installed or not. 155 | You may edit the `term` file to use another terminal. 156 | 157 | ### Custom background 158 | 159 | Use -b | argument (w/o #) to define custom background colour. If alpha value given, it overrides 160 | the opacity, as well default, as defined with the -o argument. 161 | 162 | ### Custom styling 163 | 164 | On first run the program creates the `nwg-launchers/nwggrid` folder in your .config directory. You'll find the `style.css` files inside. 165 | You may edit the style sheet to your liking. 166 | 167 | ### Customization 168 | 169 | On first run the program creates the `nwg-launchers/nwggrid` folder in your .config directory. You'll find a sample template `grid.conf` file inside. 170 | 171 | Below lists the keys currently available to be set in the `grid.conf` file. Not all keys shown below are set by in the sample template, such as the `"custom-path"` and `"language"` keys. 172 | 173 | It should be noted that the `"favorites"` and `"pins"` keys should not be set to `true` if the `"custom-path"` key is set, as those options will not work. 174 | 175 | ```json 176 | { 177 | "categories": { 178 | "AudioVideo" : "Multimedia 📀", 179 | "Development" : "Development 💻", 180 | "Education" : "Education 🎓", 181 | "Game" : "Games 🎮", 182 | "Graphics" : "Graphics 🎨", 183 | "Network" : "Network 🌎", 184 | "Office" : "Office 💼", 185 | "Science" : "Science 🔬", 186 | "Settings" : "Settings ⚙️", 187 | "System" : "System 🖥️", 188 | "Utility" : "Utility 🛠️" 189 | }, 190 | "custom-path" : "/my/path1:/my/another path:/third/path", 191 | "favorites" : false, 192 | "pins" : false, 193 | "columns" : 6, 194 | "icon-size" : 72, 195 | "language" : "en", 196 | "no-categories": false, 197 | "oneshot" : false 198 | } 199 | ``` 200 | 201 | ## nwgbar 202 | 203 | This command creates a horizontal or vertical button bar, out of a template file. 204 | 205 | [![Swappshot-Mon-Mar-23-210713-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-210713-2020.th.png)](https://scrot.cloud/image/jRPQ) [![Swappshot-Mon-Mar-23-210652-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-210652-2020.th.png)](https://scrot.cloud/image/j8LU) 206 | 207 | ### Usage 208 | 209 | ``` 210 | $ nwgbar -h 211 | GTK button bar: nwgbar 0.7.1.1 (c) Piotr Miller & Contributors 2022 212 | 213 | Options: 214 | -h show this help message and exit 215 | -v arrange buttons vertically 216 | -ha | horizontal alignment left/right (default: center) 217 | -va | vertical alignment top/bottom (default: middle) 218 | -t template file name (default: bar.json) 219 | -c css file name (default: style.css) 220 | -o background opacity (0.0 - 1.0, default 0.9) 221 | -b background colour in RRGGBB or RRGGBBAA format (RRGGBBAA alpha overrides ) 222 | -s button image size (default: 72) 223 | -g GTK theme name 224 | -wm window manager name (if can not be detected) 225 | 226 | [requires layer-shell]: 227 | -layer-shell-layer {BACKGROUND,BOTTOM,TOP,OVERLAY}, default: OVERLAY 228 | -layer-shell-exclusive-zone {auto, valid integer (usually -1 or 0)}, default: auto 229 | 230 | 231 | ``` 232 | 233 | ### Custom background 234 | 235 | Use -b | argument (w/o #) to define custom background colour. If alpha value given, it overrides 236 | the opacity, as well default, as defined with the -o argument. 237 | 238 | ### Customization 239 | 240 | On first run the program creates the `nwg-launchers/nwgbar` folder in your .config directory. You'll find a sample 241 | template `bar.json` and the `style.css` files inside. 242 | 243 | Templates use json format. The default one defines an example Exit menu for sway window manager on Arch Linux: 244 | 245 | ```json 246 | [ 247 | { 248 | "name": "Lock screen", 249 | "exec": "swaylock -f -c 000000", 250 | "icon": "system-lock-screen" 251 | }, 252 | { 253 | "name": "Logout", 254 | "exec": "swaymsg exit", 255 | "icon": "system-log-out" 256 | }, 257 | { 258 | "name": "Reboot", 259 | "exec": "systemctl reboot", 260 | "icon": "system-reboot" 261 | }, 262 | { 263 | "name": "Shutdown", 264 | "exec": "systemctl -i poweroff", 265 | "icon": "system-shutdown" 266 | } 267 | ] 268 | ``` 269 | 270 | To set a keyboard shortcut (using Alt+KEY) for an entry, you can add an underscore before the letter you want to use. 271 | Example to set `s` as the shortcut: 272 | 273 | ``` 274 | [ 275 | ... 276 | { 277 | "name": "Lock _screen", 278 | "exec": "swaylock -f -c 000000", 279 | "icon": "system-lock-screen" 280 | } 281 | ... 282 | ] 283 | ``` 284 | 285 | **Note for underscore ("_")** 286 | 287 | If you want to use an underscore in the name, you have to double it ("__"). 288 | 289 | **Wayfire note** 290 | 291 | For the Logout button, as in the bar above, you may use [wayland-logout](https://github.com/soreau/wayland-logout) by @soreau. 292 | 293 | You may use as many templates as you need, with the `-t` argument. All of them must be placed in the config directory. 294 | You may use own icon files instead of icon names, like `/path/to/the/file/my_icon.svg`. 295 | 296 | The style sheet makes the buttons look similar to `nwggrid`. You can customize them as well. 297 | 298 | ## nwgdmenu 299 | 300 | This program provides 2 commands: 301 | 302 | - ` | nwgdmenu` - displays newline-separated stdin input as a GTK menu 303 | - `nwgdmenu` - creates a GTK menu out of commands found in $PATH 304 | 305 | *Hit "Delete" to clear the search box.* 306 | *Hit "Insert" to switch case sensitivity.* 307 | 308 | [![Swappshot-Mon-Mar-23-211702-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-211702-2020.th.png)](https://scrot.cloud/image/jfHK) [![Swappshot-Mon-Mar-23-211911-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-211911-2020.th.png)](https://scrot.cloud/image/j3MG) [![Swappshot-Mon-Mar-23-211736-2020.th.png](https://scrot.cloud/images/2020/03/23/Swappshot-Mon-Mar-23-211736-2020.th.png)](https://scrot.cloud/image/jvOi) 309 | 310 | ### Usage 311 | 312 | ``` 313 | $ nwgdmenu -h 314 | GTK dynamic menu: nwgdmenu 0.7.1.1 (c) Piotr Miller & Contributors 2022 315 | 316 | | nwgdmenu - displays newline-separated stdin input as a GTK menu 317 | nwgdmenu - creates a GTK menu out of commands found in $PATH 318 | 319 | Options: 320 | -h show this help message and exit 321 | -n no search box 322 | -ha | horizontal alignment left/right (default: center) 323 | -va | vertical alignment top/bottom (default: middle) 324 | -r number of rows (default: 20) 325 | -c css file name (default: style.css) 326 | -o background opacity (0.0 - 1.0, default 0.3) 327 | -b background colour in RRGGBB or RRGGBBAA format (RRGGBBAA alpha overrides ) 328 | -g GTK theme name 329 | -wm window manager name (if can not be detected) 330 | -run ignore stdin, always build from commands in $PATH 331 | 332 | [requires layer-shell]: 333 | -layer-shell-layer {BACKGROUND,BOTTOM,TOP,OVERLAY}, default: OVERLAY 334 | -layer-shell-exclusive-zone {auto, valid integer (usually -1 or 0)}, default: auto 335 | 336 | Hotkeys: 337 | Delete clear search box 338 | Insert switch case sensitivity 339 | 340 | 341 | ``` 342 | 343 | (1) _The program should auto-detect if something has been passed in `stdin`, and build the menu out of the `stdin` content 344 | or from commands found in `$PATH` accordingly. However, in some specific cases (e.g. if you use gdm and start nwgdmenu 345 | from a key binding) the `stdin` content detection may be false-positive, which results in displaying an empty menu. 346 | In such case use the `nwgdmenu -run` instead, to force building the menu out of commands in `$PATH`._ 347 | 348 | Notice: if you start your WM from a script (w/o DM), only sway and i3 will be auto-detected. You may need to pass the WM name as the argument: 349 | 350 | `nwgdmenu -wm dwm` 351 | 352 | The generic name `tiling` will be accepted as well. 353 | 354 | ### Custom background 355 | 356 | Use -b | argument (w/o #) to define custom background colour. If alpha value given, it overrides 357 | the opacity, as well default, as defined with the -o argument. 358 | 359 | ### Custom styling 360 | 361 | On first run the program creates the `nwg-launchers/nwgdmenu` folder in your .config directory. You'll find the 362 | default `style.css` files inside. Use it to adjust styling and a vertical margin to the menu, if needed. 363 | 364 | ## i3 note 365 | 366 | In case you use default window borders, an exclusion like this may be necessary: 367 | 368 | ``` 369 | for_window [title="~nwg"] border none 370 | ``` 371 | 372 | ### Openbox Note 373 | 374 | To start nwgdmenu from a key binding, use the `-run` argument, e.g.: 375 | 376 | ```xml 377 | 378 | 379 | nwgdmenu -run 380 | 381 | 382 | ``` 383 | 384 | ### wlr-layer-shell protocol 385 | 386 | From 0.5.0 the nwg-launchers support wlr-layer-shell protocol (via gtk-layer-shell), and use it where preferred. The default layer is `OVERLAY` and the default exclusive zone is `auto`, but you can change it using command line arguments. Notably, you may want to set exclusive zone to `-1` to show nwggrid or nwgbar on top of panels (waybar, wf-panel, etc). 387 | 388 | ### Tips & tricks 389 | 390 | ### Hide unwanted icons in nwggrid 391 | 392 | See: [https://wiki.archlinux.org/index.php/desktop_entries#Hide_desktop_entries](https://wiki.archlinux.org/index.php/desktop_entries#Hide_desktop_entries) 393 | -------------------------------------------------------------------------------- /grid/grid.h: -------------------------------------------------------------------------------- 1 | /* GTK-based application grid 2 | * Copyright (c) 2021 Piotr Miller 3 | * e-mail: nwg.piotr@gmail.com 4 | * Website: http://nwg.pl 5 | * Project: https://github.com/nwg-piotr/nwg-launchers 6 | * License: GPL3 7 | * */ 8 | #pragma once 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | #include "nwgconfig.h" 18 | #include "filesystem-compat.h" 19 | #include "nwg_classes.h" 20 | 21 | namespace ns = nlohmann; 22 | 23 | /* Primitive version of C++20's std::span */ 24 | template 25 | struct Span { 26 | Span(): _ref{ nullptr }, _n{ 0 } {} 27 | Span(const Span& s) = default; 28 | Span(std::vector& t): _ref{ t.data() }, _n{ t.size() } { } 29 | T& operator [](std::size_t n) { return _ref[n]; } 30 | T* _ref; 31 | std::size_t _n; 32 | 33 | T* begin() { return _ref; } 34 | T* end() { return _ref + _n; } 35 | std::size_t size() { return _n; } 36 | }; 37 | 38 | struct Stats { 39 | enum FavTag: bool { 40 | Common = 0, 41 | Favorite = 1, 42 | }; 43 | enum PinTag: bool { 44 | Unpinned = 0, 45 | Pinned = 1, 46 | }; 47 | int clicks{ 0 }; 48 | int position{ 0 }; 49 | FavTag favorite{ Common }; 50 | PinTag pinned{ Unpinned }; 51 | Stats(int c, int i, FavTag f, PinTag p) 52 | : clicks(c), position(i), favorite(f), pinned(p) { } 53 | Stats() = default; 54 | }; 55 | 56 | struct Entry { 57 | std::string_view desktop_id; 58 | // no point making it string_view as Glib::spawn_command_async takes const string& 59 | // making it string& however breaks move ctors/assignments 60 | std::string* exec; 61 | Stats stats; 62 | 63 | // TODO: should we store it separately? 64 | std::unique_ptr desktop_entry_; 65 | 66 | Entry(std::string_view id, Stats stats, std::unique_ptr entry): 67 | desktop_id{ id }, exec{ &entry->exec }, stats{ stats }, desktop_entry_{ std::move(entry) } 68 | { 69 | // intentionally left blank 70 | } 71 | auto & desktop_entry() { 72 | return *desktop_entry_; 73 | } 74 | }; 75 | 76 | class GridWindow; 77 | 78 | class GridBox : public Gtk::Button { 79 | public: 80 | /* name, comment, desktop-id, index */ 81 | GridBox(Glib::ustring, Glib::ustring, Entry& entry); 82 | GridBox(GridBox&&) = default; 83 | ~GridBox() = default; 84 | 85 | GridWindow& get_toplevel(); 86 | 87 | bool on_button_press_event(GdkEventButton*) override; 88 | bool on_focus_in_event(GdkEventFocus*) override; 89 | void on_enter() override; 90 | void on_activate() override; 91 | 92 | Glib::ustring name; 93 | Glib::ustring comment; 94 | 95 | Entry* entry; 96 | }; 97 | 98 | struct GridConfig: public Config { 99 | GridConfig(const InputParser& parser, const Glib::RefPtr& screen, const fs::path& config_dir); 100 | 101 | bool pins; // whether to display pinned 102 | bool favs; // whether to display favorites 103 | std::string term; // user-preferred terminal 104 | std::string lang; // user-preferred language 105 | std::string special_dirs; // user-preferred path to desktop files 106 | std::size_t num_col{ 6 }; // number of grid columns 107 | fs::path pinned_file; // file with pins 108 | fs::path cached_file; // file with favs 109 | int icon_size{ 72 }; 110 | RGBA background_color; 111 | bool oneshot{ false }; // run in foreground, exit when window is closed 112 | bool categories{ false }; // enable categories 113 | ns::json config_source; 114 | }; 115 | 116 | 117 | struct CategoryButton; 118 | 119 | struct CategoriesSet { 120 | struct Category { 121 | std::string category; 122 | std::size_t refs; 123 | CategoryButton* button; 124 | Category(std::string_view category): category{ category }, refs{ 1 } {} 125 | }; 126 | 127 | std::list categories_store; 128 | std::unordered_map categories; 129 | std::unordered_set active_categories; 130 | bool all_enabled{ true }; 131 | 132 | using Index = typename decltype(categories_store)::iterator; 133 | 134 | CategoriesSet() = default; 135 | bool toggle(std::string_view category); 136 | std::pair ref(std::string_view category); 137 | std::pair unref(std::string_view category); 138 | bool enabled(std::string_view category) const; 139 | bool enabled(const GridBox& box) const; 140 | 141 | void delete_by_index(Index index); 142 | }; 143 | 144 | struct CategoryButton: public Gtk::ToggleButton { 145 | Gdk::ModifierType modifiers; 146 | bool mod_pressed{ false }; 147 | 148 | CategoriesSet& categories; 149 | CategoriesSet::Index index; 150 | 151 | CategoryButton(const std::string& name, CategoriesSet& set, CategoriesSet::Index index); 152 | ~CategoryButton(); 153 | 154 | bool on_button_press_event(GdkEventButton* key) override { 155 | mod_pressed = (key->state & modifiers) == Gdk::CONTROL_MASK; 156 | // if Ctrl is not pressed, disable all other categories 157 | if (!mod_pressed) { 158 | auto& fboxchild = *dynamic_cast(get_parent()); 159 | auto& fbox = dynamic_cast(*fboxchild.get_parent()); 160 | fbox.foreach([this](Gtk::Widget& widget) { 161 | auto& fboxchild = static_cast(widget); 162 | auto* button = dynamic_cast(fboxchild.get_child()); 163 | // button is not CategoryButton 164 | if (button && this != button) { 165 | button->set_active(false); 166 | } 167 | }); 168 | } 169 | return Gtk::ToggleButton::on_button_press_event(key); 170 | } 171 | bool on_button_release_event(GdkEventButton* key) override { 172 | return Gtk::ToggleButton::on_button_release_event(key); 173 | } 174 | }; 175 | 176 | 177 | class AbstractBoxes { 178 | protected: 179 | std::vector boxes; 180 | public: 181 | virtual ~AbstractBoxes() = default; 182 | 183 | decltype(auto) begin() { return boxes.begin(); } 184 | decltype(auto) end() { return boxes.end(); } 185 | auto & front() { return boxes.front(); } 186 | auto size() const { return boxes.size(); } 187 | auto empty() const { return boxes.empty(); } 188 | 189 | virtual void add(GridBox& box) = 0; 190 | virtual void erase(GridBox& box) = 0; 191 | }; 192 | 193 | class BoxesModel: public AbstractBoxes, public Gio::ListModel, public Glib::Object { 194 | public: 195 | virtual ~BoxesModel() = default; 196 | virtual void erase(GridBox& box) override { 197 | if (auto iter = std::find(boxes.begin(), boxes.end(), &box); iter != boxes.end()) { 198 | auto pos = std::distance(boxes.begin(), iter); 199 | boxes.erase(iter); 200 | box.reference(); 201 | items_changed(pos, 1, 0); 202 | } 203 | } 204 | virtual void update(GridBox& from, GridBox& to) { 205 | if (auto iter = std::find(boxes.begin(), boxes.end(), &from); iter != boxes.end()) { 206 | auto pos = std::distance(boxes.begin(), iter); 207 | // two references required, but why? 208 | to.reference(); 209 | to.reference(); 210 | *iter = &to; 211 | items_changed(pos, 1, 1); 212 | } 213 | } 214 | protected: 215 | BoxesModel(): Glib::ObjectBase(typeid(BoxesModel)), Gio::ListModel() {} 216 | GType get_item_type_vfunc() override { 217 | return GridBox::get_type(); 218 | } 219 | guint get_n_items_vfunc() override { 220 | return boxes.size(); 221 | } 222 | gpointer get_item_vfunc(guint position) override { 223 | if (position < boxes.size()) { 224 | return boxes[position]->gobj(); 225 | } 226 | return nullptr; 227 | } 228 | }; 229 | 230 | /* CRTP class providing T::create() -> RefPtr */ 231 | template struct Create { 232 | template 233 | static auto create(Ts && ... ts) { 234 | auto* ptr = new T{ std::forward(ts)... }; 235 | // refptr(ptr) constructor does not increase reference count, but ~refptr does decrease 236 | // resulting in refcount < 0 237 | ptr->reference(); 238 | return Glib::RefPtr{ ptr }; 239 | } 240 | }; 241 | 242 | inline auto container_add_sorted = [](auto && container, auto && elem, auto && cmp_less) { 243 | auto iter = std::find_if(container.begin(), container.end(), [&](auto & other) { 244 | return !cmp_less(elem, other); 245 | }); 246 | auto pos = std::distance(container.begin(), iter); 247 | container.insert(iter, elem); 248 | return pos; 249 | }; 250 | 251 | class PinnedBoxes: public BoxesModel, public Create { 252 | friend struct Create; // permit Create to access a protected constructor 253 | protected: 254 | int monotonic_index{ 0 }; 255 | PinnedBoxes(): Glib::ObjectBase(typeid(PinnedBoxes)) {} 256 | public: 257 | void add(GridBox& box) override { 258 | box.entry->stats.pinned = Stats::Pinned; 259 | // temporary fix for #176 260 | // initial indices are set to < 0 so they are not reordered 261 | // but we reset them to 0 when erasing, so that when the entry is unpinned & pinned again 262 | // it receives proper position 263 | if (box.entry->stats.position >= 0) { 264 | box.entry->stats.position = monotonic_index; 265 | ++monotonic_index; 266 | } 267 | auto pos = container_add_sorted(boxes, &box, [](auto* a, auto* b) { 268 | return a->entry->stats.position > b->entry->stats.position; 269 | }); 270 | 271 | // monotonic index increases each time an entry is pinned 272 | // ensuring it will appear last 273 | items_changed(pos, 0, 1); 274 | } 275 | void erase(GridBox& box) override { 276 | box.entry->stats.position = 0; 277 | BoxesModel::erase(box); 278 | } 279 | }; 280 | 281 | class FavBoxes: public BoxesModel, public Create { 282 | friend struct Create; // permit Create to access a protected constructor 283 | protected: 284 | FavBoxes(): Glib::ObjectBase(typeid(FavBoxes)) {} 285 | public: 286 | void add(GridBox& box) override { 287 | box.entry->stats.favorite = Stats::Favorite; 288 | box.entry->stats.clicks = 1; 289 | auto pos = container_add_sorted(boxes, &box, [](auto* a, auto* b) { 290 | return a->entry->stats.clicks < b->entry->stats.clicks; 291 | }); 292 | items_changed(pos, 0, 1); 293 | } 294 | }; 295 | 296 | class AppBoxes: public BoxesModel, public Create { 297 | friend struct Create; // permit Create to access a protected constructor 298 | private: 299 | std::vector all_boxes; // unsorted & unfiltered boxes 300 | Glib::ustring search_criteria; 301 | CategoriesSet& categories; 302 | protected: 303 | AppBoxes(CategoriesSet& set): Glib::ObjectBase(typeid(AppBoxes)), categories{ set } {} 304 | void add_if_matches(GridBox* box) { 305 | auto enabled = categories.enabled(*box); 306 | if (enabled && (box->name.casefold().find(search_criteria) != Glib::ustring::npos)) { 307 | container_add_sorted(boxes, box, [](auto* a, auto* b) { 308 | return a->name.compare(b->name) > 0; 309 | }); 310 | } 311 | } 312 | void filter_impl(bool restore) { 313 | // TODO: only update actually removed/inserted entries 314 | auto old_size = boxes.size(); 315 | if (!restore) { 316 | for (auto && box: boxes) { 317 | box->reference(); 318 | } 319 | boxes.clear(); 320 | for (auto && box: all_boxes) { 321 | box->reference(); 322 | add_if_matches(box); 323 | } 324 | } else { 325 | boxes = all_boxes; 326 | std::sort(boxes.begin(), boxes.end(), [](auto* a, auto* b) { 327 | return a->name.compare(b->name) < 0; 328 | }); 329 | for (auto && box: all_boxes) { 330 | box->reference(); 331 | box->reference(); 332 | } 333 | } 334 | items_changed(0, old_size, boxes.size()); 335 | } 336 | public: 337 | void add(GridBox& box) override { 338 | // TODO: ensure the box does not exist before insertion for all *Boxes classes 339 | all_boxes.push_back(&box); 340 | auto ok = categories.enabled(box) && ( 341 | search_criteria.length() == 0 342 | || 343 | box.name.casefold().find(search_criteria) != Glib::ustring::npos 344 | ); 345 | if (ok) { 346 | auto pos = container_add_sorted(boxes, &box, [](auto* a, auto* b) { 347 | return a->name.compare(b->name) > 0; 348 | }); 349 | items_changed(pos, 0, 1); 350 | } 351 | } 352 | void erase(GridBox& box) override { 353 | // erasing from filtered boxes will decrease reference count by 1, destroying object 354 | // but we want it alive to remove it from all_boxes and then to destroy it ourselves 355 | box.reference(); 356 | // erase from filtered boxes 357 | BoxesModel::erase(box); 358 | // erase from all boxes 359 | if (auto to_erase_2 = std::remove(all_boxes.begin(), all_boxes.end(), &box); to_erase_2 != all_boxes.end()) { 360 | all_boxes.erase(to_erase_2); 361 | } 362 | } 363 | void update(GridBox& from, GridBox& to) override { 364 | all_boxes.push_back(&to); 365 | auto removed = std::remove(all_boxes.begin(), all_boxes.end(), &from); 366 | if (removed != all_boxes.end()) { 367 | all_boxes.erase(removed, all_boxes.end()); 368 | } 369 | return BoxesModel::update(from, to); 370 | } 371 | void filter(const Glib::ustring& criteria) { 372 | auto criteria_ = criteria.casefold(); 373 | if (search_criteria != criteria_) { 374 | search_criteria = criteria_; 375 | filter_impl(search_criteria.length() == 0); 376 | } 377 | } 378 | void on_category_toggled() { 379 | filter_impl(false); 380 | } 381 | bool is_filtered() { 382 | return search_criteria.length() > 0; 383 | } 384 | }; 385 | 386 | class GridWindow : public PlatformWindow { 387 | public: 388 | GridWindow(GridConfig& config); 389 | GridWindow(const GridWindow&) = delete; 390 | ~GridWindow(); 391 | 392 | Gtk::SearchEntry searchbox; // Search apps 393 | Gtk::FlowBox categories_box; 394 | Gtk::ToggleButton categories_all; 395 | Gtk::Label description; // To display .desktop entry Comment field at the bottom 396 | Gtk::FlowBox apps_grid; // All application buttons grid 397 | Gtk::FlowBox favs_grid; // Favourites grid above 398 | Gtk::FlowBox pinned_grid; // Pinned entries grid above 399 | Gtk::Separator separator; // between favs and all apps 400 | Gtk::Separator separator1; // below pinned 401 | 402 | struct HVBoxes; 403 | std::unique_ptr hvboxes; 404 | Gtk::HBox hbox_header; 405 | Gtk::HBox pinned_hbox; 406 | Gtk::HBox favs_hbox; 407 | Gtk::HBox apps_hbox; 408 | Gtk::HBox categories_hbox; 409 | Gtk::ScrolledWindow scrolled_window; 410 | GridConfig& config; 411 | 412 | template 413 | GridBox& emplace_box(Args&& ... args); // emplace box 414 | void update_box_by_id(std::string_view desktop_id, GridBox&&); 415 | void remove_box_by_desktop_id(std::string_view desktop_id); 416 | 417 | void build_grids(); 418 | void toggle_pinned(GridBox& box); 419 | void set_description(const Glib::ustring&); 420 | void save_cache(); 421 | void run_box(GridBox& box); 422 | 423 | std::string& exec_of(const GridBox& box) { 424 | return *box.entry->exec; 425 | } 426 | Stats& stats_of(const GridBox& box) { 427 | return box.entry->stats; 428 | } 429 | protected: 430 | //Override default signal handler: 431 | bool on_key_press_event(GdkEventKey*) override; 432 | void on_show() override; 433 | bool on_delete_event(GdkEventAny*) override; 434 | bool on_button_press_event(GdkEventButton*) override; 435 | private: 436 | void ref_categories(const GridBox& box); 437 | void unref_categories(GridBox& box); 438 | 439 | std::list all_boxes {}; // stores all applications buttons 440 | Glib::RefPtr apps_boxes; // common boxes (possibly filtered) 441 | Glib::RefPtr fav_boxes; // favourites (most clicked) 442 | Glib::RefPtr pinned_boxes; // boxes pinned by user 443 | 444 | CategoriesSet categories; 445 | 446 | bool pins_changed = false; 447 | bool favs_changed = false; 448 | 449 | void focus_first_box(); 450 | void filter_view(); 451 | void refresh_separators(); 452 | }; 453 | 454 | template 455 | GridBox& GridWindow::emplace_box(Args&& ... args) { 456 | auto& ab = this -> all_boxes.emplace_back(std::forward(args)...); 457 | ref_categories(ab); 458 | ab.reference(); 459 | ab.reference(); 460 | AbstractBoxes* boxes = apps_boxes.get(); 461 | auto& stats = this -> stats_of(ab); 462 | if (stats.pinned) { 463 | boxes = pinned_boxes.get(); 464 | } else if (stats.favorite) { 465 | boxes = fav_boxes.get(); 466 | } 467 | boxes->add(ab); 468 | return ab; 469 | } 470 | 471 | struct CacheEntry { 472 | std::string desktop_id; 473 | int clicks; 474 | CacheEntry(std::string, int); 475 | }; 476 | 477 | struct GridInstance: public Instance { 478 | GridWindow& window; 479 | 480 | GridInstance(Gtk::Application& app, GridWindow& window, std::string_view name): 481 | Instance{ app, name }, window{ window } 482 | { 483 | // intentionally left blank 484 | } 485 | /* Instance on_* handlers call Application::quit 486 | * which internally calls _exit, destructors are not called 487 | * To handle this problem GridInstance overrides handlers 488 | * to call Application::release 489 | */ 490 | void on_sighup() override; // reload 491 | void on_sigint() override; // save & exit 492 | void on_sigterm() override; // save & exit 493 | void on_sigusr1() override; // show 494 | ~GridInstance() { 495 | window.save_cache(); 496 | } 497 | }; 498 | 499 | /* 500 | * Function declarations 501 | * */ 502 | std::vector get_app_dirs(void); 503 | std::vector get_pinned(const fs::path& pinned_file); 504 | std::vector get_favourites(ns::json&&, int); 505 | -------------------------------------------------------------------------------- /common/nwg_tools.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Tools for nwg-launchers 3 | * Copyright (c) 2021 Érico Nogueira 4 | * e-mail: ericonr@disroot.org 5 | * Copyright (c) 2021 Piotr Miller 6 | * e-mail: nwg.piotr@gmail.com 7 | * Website: http://nwg.pl 8 | * Project: https://github.com/nwg-piotr/nwg-launchers 9 | * License: GPL3 10 | * */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "charconv-compat.h" 25 | #include "filesystem-compat.h" 26 | #include "nwgconfig.h" 27 | #include "nwg_exceptions.h" 28 | #include "nwg_tools.h" 29 | #include "log.h" 30 | 31 | 32 | #ifdef GDK_WINDOWING_X11 33 | #include 34 | #endif 35 | 36 | // stores the name of the pid_file, for use in atexit 37 | static fs::path pid_file{}; 38 | 39 | std::string_view get_home_dir() { 40 | static fs::path home_dir; 41 | if (home_dir.empty()) { 42 | const char* val = getenv("HOME"); 43 | if (!val) { 44 | Log::error("$HOME not set"); 45 | throw std::runtime_error{ "get_home_dir: $HOME not set" }; 46 | } 47 | home_dir = val; 48 | } 49 | return { home_dir.native() }; 50 | } 51 | 52 | fs::path get_config_file(std::string_view app, std::string_view file) { 53 | auto try_ = [=](fs::path& base) { 54 | base /= "nwg-launchers"; 55 | base /= app; 56 | base /= file; 57 | Log::info("trying ", base.native()); 58 | if (fs::exists(base)) { 59 | return true; 60 | } 61 | base.clear(); 62 | return false; 63 | }; 64 | 65 | fs::path result; 66 | 67 | // try xdg-config-home 68 | char* val = getenv("XDG_CONFIG_HOME"); 69 | if (val) { 70 | result = val; 71 | if (try_(result)) { 72 | return result; 73 | } 74 | } 75 | 76 | // try home 77 | result = get_home_dir(); 78 | result /= ".config"; 79 | if (try_(result)) { 80 | return result; 81 | } 82 | 83 | // DATA_DIR_STR already contains "nwg-launchers" 84 | result = DATA_DIR_STR; 85 | result /= app; 86 | result /= file; 87 | if (fs::exists(result)) { 88 | return result; 89 | } 90 | 91 | throw std::runtime_error{ concat("Unable to locate '", app, '/', file, "' configuration file") }; 92 | } 93 | 94 | /* 95 | * Returns config dir 96 | * */ 97 | fs::path get_config_dir(std::string_view app) { 98 | fs::path path; 99 | char* val = getenv("XDG_CONFIG_HOME"); 100 | if (!val) { 101 | path = get_home_dir(); 102 | path /= ".config"; 103 | } else { 104 | path = val; 105 | } 106 | path /= "nwg-launchers"; 107 | path /= app; 108 | return path; 109 | } 110 | 111 | /* 112 | * Returns path to cache directory 113 | * */ 114 | fs::path get_cache_home() { 115 | char* home_ = getenv("XDG_CACHE_HOME"); 116 | fs::path home; 117 | if (home_) { 118 | home = home_; 119 | } else { 120 | home = get_home_dir(); 121 | home /= ".cache"; 122 | } 123 | return home; 124 | } 125 | 126 | /* 127 | * Return runtime dir 128 | * */ 129 | fs::path get_runtime_dir() { 130 | if (auto* xdg_runtime_dir = getenv("XDG_RUNTIME_DIR")) { 131 | return xdg_runtime_dir; 132 | } 133 | std::error_code ec; 134 | { 135 | std::array myuid; 136 | auto myuid_ = getuid(); 137 | #ifdef HAVE_TO_CHARS 138 | if (auto [p, e] = std::to_chars(myuid.data(), myuid.data() + myuid.size(), myuid_); e == std::errc()) { 139 | std::string_view myuid_view{ myuid.data(), std::size_t(p - myuid.data()) }; 140 | #else 141 | if (auto n = std::snprintf(myuid.data(), 64, "%u", myuid_); n > 0) { 142 | std::string_view myuid_view{ myuid.data(), static_cast(n) }; 143 | #endif 144 | if (fs::path path{ "/run/user/" }; fs::exists(path, ec) && !ec) { 145 | // let's try /run/user/ first 146 | path /= myuid_view; 147 | return path; 148 | } 149 | } else { 150 | throw std::runtime_error{ "Failed to convert UID to chars" }; 151 | } 152 | ec.clear(); 153 | } 154 | if (fs::path tmp{ Glib::get_tmp_dir() }; fs::exists(tmp, ec) && !ec) { 155 | return tmp; 156 | } 157 | throw std::runtime_error{ "Failed to determine user runtime directory" }; 158 | } 159 | 160 | fs::path get_pid_file(std::string_view name) { 161 | auto dir = get_runtime_dir(); 162 | dir /= name; 163 | return dir; 164 | } 165 | 166 | int parse_icon_size(std::string_view arg) { 167 | int i_s; 168 | if (parse_number(arg, i_s)) { 169 | switch (2 * (i_s > 2048) + (i_s < 16)) { 170 | case 0: return i_s; 171 | case 1: Log::error("Icon size is too small (<16), setting to 16"); 172 | return 16; 173 | case 2: 174 | Log::error("Icon size is too large (>2048), setting to 2048"); 175 | return 2048; 176 | default: throw std::logic_error{ 177 | "parse_icon_size: unexpected switch variant triggered" 178 | }; 179 | } 180 | } else { 181 | // -s argument couldn't be parsed, therefore something's really wrong 182 | throw std::runtime_error{ "Image size should be valid integer in range 16 - 2048\n" }; 183 | } 184 | } 185 | 186 | /* RAII wrappers to reduce the amount of bookkeeping */ 187 | struct FdGuard { 188 | int fd; 189 | ~FdGuard() { close(fd); } 190 | }; 191 | struct LockfGuard { 192 | int fd; 193 | off_t len; 194 | LockfGuard(int fd, int cmd, off_t len): fd{ fd }, len{ len } { 195 | if (lockf(fd, cmd, len)) { 196 | int err = errno; 197 | throw ErrnoException{ "Failed to lock file: ", err }; 198 | } 199 | } 200 | ~LockfGuard() { 201 | if (lockf(fd, F_ULOCK, len)) { 202 | int err = errno; 203 | Log::error("Failed to unlock file: ", error_description(err)); 204 | } 205 | } 206 | }; 207 | /* private helpers handling partial reads/writes (see {read,write}(2)) */ 208 | static inline size_t read_buf(int fd, char* buf, size_t n) { 209 | size_t total{ 0 }; 210 | while (total < n) { 211 | auto bytes = read(fd, buf + total, n - total); 212 | if (bytes == 0) { 213 | break; 214 | } 215 | if (bytes < 0) { 216 | int err = errno; 217 | throw ErrnoException{ "read(2) failed: ", err }; 218 | } 219 | total += bytes; 220 | } 221 | return total; 222 | } 223 | static inline void write_buf(int fd, const char* buf, size_t n) { 224 | size_t total{ 0 }; 225 | while (total < n) { 226 | auto bytes = write(fd, buf + total, n - total); 227 | if (bytes == 0) { 228 | break; 229 | } 230 | if (bytes < 0) { 231 | int err = errno; 232 | throw ErrnoException{ "write(2) failed: ", err }; 233 | } 234 | total += bytes; 235 | } 236 | } 237 | 238 | std::optional get_instance_pid(const char* path) { 239 | // we need write capability to be able to lockf(3) file 240 | auto fd = open(path, O_RDWR | O_CLOEXEC, 0); 241 | if (fd == -1) { 242 | int err = errno; 243 | if (err == ENOENT) { 244 | return std::nullopt; 245 | } 246 | throw ErrnoException{ "failed to open pid file: ", err }; 247 | } 248 | FdGuard fd_guard{ fd }; 249 | LockfGuard guard{ fd, F_LOCK, 0 }; 250 | 251 | // we read at most 64 bytes which is more than enough for pid 252 | char buf[64]{}; 253 | pid_t pid{ -1 }; 254 | auto bytes = read_buf(fd, buf, 64); 255 | if (bytes == 0) { 256 | Log::warn("the pid file is empty"); 257 | return std::nullopt; 258 | } 259 | std::string_view view{ buf, size_t(bytes) }; 260 | if (!parse_number(view, pid)) { 261 | throw std::runtime_error{ "Failed to read pid from file" }; 262 | } 263 | if (pid < 0) { 264 | Log::warn("the saved pid is negative"); 265 | return std::nullopt; 266 | } 267 | if (kill(pid, 0) != 0) { 268 | Log::warn("the saved pid is stale"); 269 | return std::nullopt; 270 | } 271 | return pid; 272 | } 273 | 274 | void write_instance_pid(const char* path, pid_t pid) { 275 | auto fd = open(path, O_WRONLY | O_CLOEXEC | O_CREAT, S_IWUSR | S_IRUSR); 276 | if (fd == -1) { 277 | int err = errno; 278 | throw ErrnoException{ "failed to open the pid file: ", err }; 279 | } 280 | FdGuard fd_guard{ fd }; 281 | LockfGuard guard{ fd, F_LOCK, 0 }; 282 | auto str = std::to_string(pid); 283 | write_buf(fd, str.data(), str.size()); 284 | } 285 | 286 | /* 287 | * Returns window manager name 288 | * */ 289 | std::string detect_wm(const Glib::RefPtr& display, const Glib::RefPtr& screen) { 290 | /* Actually we only need to check if we're on sway, i3 or other WM, 291 | * but let's try to find a WM name if possible. If not, let it be just "other" */ 292 | std::string wm_name{"other"}; 293 | 294 | #ifdef GDK_WINDOWING_X11 295 | { 296 | auto* g_display = display->gobj(); 297 | auto* g_screen = screen->gobj(); 298 | if (GDK_IS_X11_DISPLAY (g_display)) { 299 | auto* str_ = gdk_x11_screen_get_window_manager_name (g_screen); 300 | if (str_) { 301 | Glib::ustring str = str_; 302 | wm_name = str.lowercase(); 303 | return wm_name; 304 | } 305 | } 306 | } 307 | #else 308 | (void)display; 309 | (void)screen; 310 | #endif 311 | 312 | for(auto env : {"DESKTOP_SESSION", "SWAYSOCK", "I3SOCK"}) { 313 | // get environment values 314 | if (auto env_val = getenv(env)) { 315 | std::string_view s = env_val; 316 | if (s.find("sway") != s.npos) { 317 | wm_name = "sway"; 318 | break; 319 | } else if (s.find("i3") != s.npos) { 320 | wm_name = "i3"; 321 | break; 322 | } else { 323 | // is the value a full path or just a name? 324 | if (s.find('/') == s.npos) { 325 | // full value is the name 326 | wm_name = s; 327 | break; 328 | } else { 329 | // path given 330 | int idx = s.find_last_of("/") + 1; 331 | wm_name = s.substr(idx); 332 | break; 333 | } 334 | } 335 | } 336 | } 337 | return wm_name; 338 | } 339 | 340 | /* 341 | * Detect installed terminal emulator, save the command to txt file for further use. 342 | * */ 343 | std::string get_term(std::string_view config_dir) { 344 | using namespace std::string_view_literals; 345 | 346 | auto term_file = concat(config_dir, "/term"sv); 347 | auto terminal_file = concat(config_dir, "/terminal"sv); 348 | 349 | std::error_code ec; 350 | auto term_file_exists = fs::is_regular_file(term_file, ec) && !ec; 351 | ec.clear(); 352 | auto terminal_file_exists = fs::is_regular_file(terminal_file, ec) && !ec; 353 | 354 | if (term_file_exists) { 355 | if (!terminal_file_exists) { 356 | fs::rename(term_file, terminal_file); 357 | terminal_file_exists = true; 358 | } else { 359 | fs::remove(term_file); 360 | } 361 | } 362 | 363 | std::string term; 364 | auto check_env_vars = [&]() { 365 | // TODO: TERMINAL is usually just term name, we don't know if it supports '-e' 366 | constexpr std::array known_term_vars { /*"TERMINAL",*/ "TERMCMD"}; 367 | for (auto var: known_term_vars) { 368 | if (auto e = getenv(var)) { 369 | term = e; 370 | return 0; 371 | } 372 | } 373 | return -1; 374 | }; 375 | auto check_terms = [&]() { 376 | constexpr std::array term_flags { " -e"sv, ""sv }; 377 | constexpr std::array terms { 378 | std::pair{ "alacritty"sv, 0 }, 379 | std::pair{ "kitty"sv, 0 }, 380 | std::pair{ "urxvt"sv, 0 }, 381 | std::pair{ "lxterminal"sv, 0 }, 382 | std::pair{ "sakura"sv, 0 }, 383 | std::pair{ "st"sv, 0 }, 384 | std::pair{ "termite"sv, 0 }, 385 | std::pair{ "terminator"sv, 0 }, 386 | std::pair{ "xfce4-terminal"sv, 0 }, 387 | std::pair{ "gnome-terminal"sv, 0 }, 388 | std::pair{ "foot"sv, 1 } 389 | }; 390 | for (auto&& [term_, flag_]: terms) { 391 | // TODO: use exec/similar from glib to avoid spawning shells 392 | auto command = concat("command -v "sv, term, " > /dev/null 2>&1"sv); 393 | if (std::system(command.data()) == 0) { 394 | term = concat(term_, term_flags[flag_]); 395 | return 0; 396 | } 397 | } 398 | return -1; 399 | }; 400 | 401 | bool needs_save = true; 402 | if (check_env_vars()) { 403 | if (terminal_file_exists) { 404 | term = read_file_to_string(terminal_file); 405 | // do NOT append ' -e' as it breaks non-standard terminals 406 | term.erase(remove(term.begin(), term.end(), '\n'), term.end()); 407 | needs_save = false; 408 | } else if (check_terms()) { 409 | // nothing worked, fallback to xterm 410 | term = "xterm -e"sv; 411 | } 412 | } 413 | if (needs_save) { 414 | save_string_to_file(term, terminal_file); 415 | } 416 | return term; 417 | } 418 | 419 | /* 420 | * Connects to Sway socket, loading socket info from $SWAYSOCK or $I3SOCK 421 | * Throws SwayError 422 | * - sway --get-socketpath is not supported (yet?) 423 | * */ 424 | SwaySock::SwaySock() { 425 | auto path = getenv("SWAYSOCK"); 426 | if (!path) { 427 | path = getenv("I3SOCK"); 428 | if (!path) { 429 | throw SwayError::EnvNotSet; 430 | } 431 | } 432 | path = strdup(path); 433 | 434 | sock_ = socket(AF_UNIX, SOCK_STREAM, 0); 435 | if (sock_ == -1) { 436 | free(path); 437 | throw SwayError::OpenFailed; 438 | } 439 | 440 | sockaddr_un addr; 441 | addr.sun_family = AF_UNIX; 442 | strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); 443 | addr.sun_path[sizeof(addr.sun_path) - 1] = 0; 444 | if (connect(sock_, reinterpret_cast(&addr), sizeof(addr)) == -1) { 445 | free(path); 446 | throw SwayError::ConnectFailed; 447 | } 448 | free(path); 449 | } 450 | 451 | SwaySock::~SwaySock() { 452 | if (close(sock_)) { 453 | Log::error("Unable to close socket"); 454 | } 455 | } 456 | 457 | /* 458 | * Returns `swaymsg -t get_outputs` 459 | * Throws `SwayError` 460 | * */ 461 | std::string SwaySock::get_outputs() { 462 | send_header_(0, Commands::GetOutputs); 463 | return recv_response_(); 464 | } 465 | 466 | /* 467 | * Returns `swaymsg -t get_workspaces` 468 | * Throws `SwayError` 469 | * */ 470 | std::string SwaySock::get_workspaces() { 471 | send_header_(0, Commands::GetWorkspaces); 472 | return recv_response_(); 473 | } 474 | 475 | /* 476 | * Returns output of previously issued command 477 | * Throws `SwayError::Recv{Header,Body}Failed` 478 | */ 479 | std::string SwaySock::recv_response_() { 480 | std::size_t total = 0; 481 | while (total < HEADER_SIZE) { 482 | auto received = recv(sock_, header.data(), HEADER_SIZE - total, 0); 483 | if (received < 0) { 484 | throw SwayError::RecvHeaderFailed; 485 | } 486 | total += received; 487 | } 488 | auto payload_size = *reinterpret_cast(header.data() + MAGIC_SIZE); 489 | std::string buffer(payload_size + 1, '\0'); 490 | auto payload = buffer.data(); 491 | total = 0; 492 | while (total < payload_size) { 493 | auto received = recv(sock_, payload + total, payload_size - total, 0); 494 | if (received < 0) { 495 | throw SwayError::RecvBodyFailed; 496 | } 497 | total += received; 498 | } 499 | return buffer; 500 | } 501 | 502 | void SwaySock::send_header_(std::uint32_t message_len, Commands command) { 503 | memcpy(header.data(), MAGIC.data(), MAGIC_SIZE); 504 | memcpy(header.data() + MAGIC_SIZE, &message_len, sizeof(message_len)); 505 | memcpy(header.data() + MAGIC_SIZE + sizeof(message_len), &command, sizeof(command)); 506 | if (write(sock_, header.data(), HEADER_SIZE) == -1) { 507 | throw SwayError::SendHeaderFailed; 508 | } 509 | } 510 | void SwaySock::send_body_(std::string_view cmd) { 511 | if (write(sock_, cmd.data(), cmd.size()) == -1) { 512 | throw SwayError::SendBodyFailed; 513 | } 514 | } 515 | 516 | /* 517 | * Returns x, y, width, hight of focused display 518 | * */ 519 | Geometry display_geometry(std::string_view wm, Glib::RefPtr display, Glib::RefPtr window) { 520 | Geometry geo = {0, 0, 0, 0}; 521 | if (wm == "sway" || wm == "i3") { 522 | try { 523 | // TODO: Maybe there is a way to make it more uniform? 524 | SwaySock sock; 525 | auto jsonString = wm == "sway" ? sock.get_outputs() : sock.get_workspaces(); 526 | auto jsonObj = string_to_json(jsonString); 527 | for (auto&& entry : jsonObj) { 528 | if (entry.at("focused")) { 529 | auto&& rect = entry.at("rect"); 530 | geo.x = rect.at("x"); 531 | geo.y = rect.at("y"); 532 | geo.width = rect.at("width"); 533 | geo.height = rect.at("height"); 534 | break; 535 | } 536 | } 537 | return geo; 538 | } 539 | catch (...) { } 540 | } 541 | 542 | // it's going to fail until the window is actually open 543 | int retry = 0; 544 | while (geo.width == 0 || geo.height == 0) { 545 | Gdk::Rectangle rect; 546 | auto monitor = display->get_monitor_at_window(window); 547 | if (monitor) { 548 | monitor->get_geometry(rect); 549 | geo.x = rect.get_x(); 550 | geo.y = rect.get_y(); 551 | geo.width = rect.get_width(); 552 | geo.height = rect.get_height(); 553 | } 554 | retry++; 555 | if (retry > 100) { 556 | Log::error("Failed checking display geometry, tries: ", retry); 557 | break; 558 | } 559 | } 560 | return geo; 561 | } 562 | 563 | /* 564 | * Returns current locale 565 | * */ 566 | std::string get_locale() { 567 | std::string locale = "en"; 568 | // avoid crash when LANG unset at all (regressed by #83 in v0.3.3) #114 569 | if (auto env = getenv("LANG")) { 570 | std::string_view loc = env; 571 | if (!loc.empty()) { 572 | auto idx = loc.find_first_of('_'); 573 | if (idx != loc.npos) { 574 | loc.remove_suffix(loc.size() - idx); 575 | } 576 | locale = loc; 577 | } 578 | } 579 | return locale; 580 | } 581 | 582 | 583 | namespace category { 584 | 585 | using namespace std::string_view_literals; 586 | 587 | std::string_view localize(const ns::json& source, std::string_view category) { 588 | auto& map = json_at(source, "categories"sv); 589 | if (category == "All"sv) { 590 | if (auto iter = map.find(category); iter != map.end()) { 591 | return iter.value().get_ref(); 592 | } 593 | return "All"sv; 594 | } 595 | auto item = map.find(category); 596 | if (item == map.end()) { 597 | throw std::logic_error{ "Trying to localize unknown category" }; 598 | } 599 | 600 | return item.value().get_ref(); 601 | } 602 | 603 | } // namespace category 604 | 605 | 606 | /* 607 | * Returns file content as a string 608 | * */ 609 | std::string read_file_to_string(const fs::path& filename) { 610 | std::ifstream input(filename); 611 | std::stringstream sstr; 612 | 613 | while(input >> sstr.rdbuf()); 614 | 615 | return sstr.str(); 616 | } 617 | 618 | /* 619 | * Saves a string to a file 620 | * */ 621 | void save_string_to_file(std::string_view s, const fs::path& filename) { 622 | std::ofstream file(filename); 623 | file << s; 624 | } 625 | 626 | /* 627 | * Splits string into vector of strings by delimiter 628 | * */ 629 | std::vector split_string(std::string_view str, std::string_view delimiter) { 630 | std::vector result; 631 | std::size_t current, previous = 0; 632 | current = str.find_first_of(delimiter); 633 | while (current != str.npos) { 634 | result.emplace_back(str.substr(previous, current - previous)); 635 | previous = current + 1; 636 | current = str.find_first_of(delimiter, previous); 637 | } 638 | result.emplace_back(str.substr(previous, current - previous)); 639 | 640 | return result; 641 | } 642 | 643 | /* 644 | * Splits string by delimiter and takes the last piece 645 | * */ 646 | std::string_view take_last_by(std::string_view str, std::string_view delimiter) { 647 | auto pos = str.find_last_of(delimiter); 648 | if (pos != str.npos) { 649 | return str.substr(pos + 1); 650 | } 651 | return str; 652 | } 653 | 654 | 655 | ns::json::reference json_at(ns::json& j, std::string_view key) { 656 | #ifdef HAVE_MODERN_NLOHMANN_JSON 657 | auto& k = key; 658 | #else 659 | // pray to SSO 660 | std::string k{ key }; 661 | #endif // HAVE_MODERN_NLOHMANN_JSON 662 | 663 | return j[k]; 664 | } 665 | 666 | ns::json::const_reference json_at(const ns::json& j, std::string_view key) { 667 | #ifdef HAVE_MODERN_NLOHMANN_JSON 668 | auto& k = key; 669 | #else 670 | // pray to SSO 671 | std::string k{ key }; 672 | #endif // HAVE_MODERN_NLOHMANN_JSON 673 | 674 | return j[k]; 675 | } 676 | 677 | 678 | /* 679 | * Reads json from file 680 | * */ 681 | ns::json json_from_file(const fs::path& path) { 682 | ns::json json; 683 | std::ifstream{path} >> json; 684 | return json; 685 | } 686 | 687 | /* 688 | * Converts json string into a json object 689 | * */ 690 | ns::json string_to_json(std::string_view jsonString) { 691 | return ns::json::parse(jsonString.begin(), jsonString.end()); 692 | } 693 | 694 | /* 695 | * Saves json into file 696 | * */ 697 | void save_json(const ns::json& json_obj, const fs::path& filename) { 698 | std::ofstream o(filename); 699 | o << std::setw(2) << json_obj << std::endl; 700 | } 701 | 702 | /* 703 | * Sets RGBA background according to hex strings 704 | * Note: if `string` is #RRGGBB, alpha will not be changed 705 | * */ 706 | void decode_color(std::string_view string, RGBA& color) { 707 | std::string hex_string {"0x"}; 708 | unsigned long int rgba; 709 | std::stringstream ss; 710 | try { 711 | if (string.find("#") == 0) { 712 | hex_string += string.substr(1); 713 | } else { 714 | hex_string += string; 715 | } 716 | ss << std::hex << hex_string; 717 | ss >> rgba; 718 | if (hex_string.size() == 8) { 719 | color.red = ((rgba >> 16) & 0xff) / 255.0; 720 | color.green = ((rgba >> 8) & 0xff) / 255.0; 721 | color.blue = ((rgba) & 0xff) / 255.0; 722 | } else if (hex_string.size() == 10) { 723 | color.red = ((rgba >> 24) & 0xff) / 255.0; 724 | color.green = ((rgba >> 16) & 0xff) / 255.0; 725 | color.blue = ((rgba >> 8) & 0xff) / 255.0; 726 | color.alpha = ((rgba) & 0xff) / 255.0; 727 | } else { 728 | Log::error("invalid color value. Should be RRGGBB or RRGGBBAA"); 729 | } 730 | } 731 | catch (...) { 732 | Log::error("Unable to parse RGB(A) value"); 733 | } 734 | } 735 | 736 | /* 737 | * Returns output of a command as string 738 | * */ 739 | std::string get_output(const std::string& cmd) { 740 | const char *command = cmd.c_str(); 741 | std::array buffer; 742 | std::string result; 743 | std::unique_ptr pipe(popen(command, "r"), pclose); 744 | if (!pipe) { 745 | throw std::runtime_error("popen() failed!"); 746 | } 747 | while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { 748 | result += buffer.data(); 749 | } 750 | return result; 751 | } 752 | 753 | /* Prepares CSS file for app `name` using config directory `config_dir` 754 | * if default css file (config_dir / style.css) does not exist, it is copied from DATA directory to config_dir 755 | * TODO: explain better 756 | */ 757 | fs::path setup_css_file(std::string_view name, const fs::path& config_dir, const fs::path& custom_css_file) { 758 | // default and custom style sheet 759 | auto default_css_file = config_dir / "style.css"; 760 | // css file to be used 761 | auto css_file = config_dir / custom_css_file; 762 | // copy default file if not found 763 | if (!fs::exists(default_css_file)) { 764 | try { 765 | fs::path sample_css_file { DATA_DIR_STR }; 766 | sample_css_file /= name; 767 | sample_css_file /= "style.css"; 768 | fs::copy_file(sample_css_file, default_css_file, fs::copy_options::overwrite_existing); 769 | } catch (const fs::filesystem_error& error) { 770 | Log::error("Failed copying default style.css: \'", error.what(), "\'"); 771 | } 772 | } 773 | 774 | if (!fs::is_regular_file(css_file)) { 775 | Log::error("Unable to open user-specified css file '", css_file, "', using default"); 776 | css_file = default_css_file; 777 | } 778 | return css_file; 779 | } 780 | 781 | int instance_on_sigterm(void* userdata) { 782 | static_cast(userdata)->on_sigterm(); 783 | return G_SOURCE_CONTINUE; 784 | } 785 | int instance_on_sigusr1(void* userdata) { 786 | static_cast(userdata)->on_sigusr1(); 787 | return G_SOURCE_CONTINUE; 788 | } 789 | int instance_on_sighup(void* userdata) { 790 | static_cast(userdata)->on_sighup(); 791 | return G_SOURCE_CONTINUE; 792 | } 793 | int instance_on_sigint(void* userdata) { 794 | static_cast(userdata)->on_sigint(); 795 | return G_SOURCE_CONTINUE; 796 | } 797 | --------------------------------------------------------------------------------