├── .gitattributes ├── .gitignore ├── HYPRLAND_BLUR_CONFIGURATION.md ├── LICENSE ├── PKGBUILD ├── README.md ├── build.sh ├── cleanup.sh ├── data ├── hyprmenu.conf ├── icons │ └── hyprmenu.svg └── org.hyprmenu.desktop ├── flake.lock ├── flake.nix ├── meson.build └── src ├── app_entry.c ├── app_entry.h ├── app_grid.c ├── app_grid.h ├── category_list.c ├── category_list.h ├── config.c ├── config.h ├── list_view.c ├── list_view.h ├── main.c ├── window.c └── window.h /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | # Build directories 35 | build/ 36 | _build/ 37 | dist/ 38 | *.o 39 | *.so 40 | *.a 41 | *.la 42 | *.lo 43 | *.obj 44 | *.exe 45 | *.out 46 | *.app 47 | 48 | # IDE and editor files 49 | .vscode/ 50 | .idea/ 51 | *.swp 52 | *~ 53 | .DS_Store 54 | 55 | # Dependencies 56 | deps/ 57 | vendor/ 58 | 59 | # Generated files 60 | *.log 61 | *.pid 62 | *.seed 63 | *.pid.lock 64 | 65 | # System files 66 | # *.conf # Commented out because we want to keep hyprmenu.conf 67 | # *.desktop # Commented out because we want to keep org.hyprmenu.desktop 68 | *.service 69 | 70 | # Debug files 71 | debug_*.log 72 | app_debug.log 73 | 74 | # Temporary files 75 | *.tmp 76 | *.temp 77 | *.bak 78 | *.backup 79 | 80 | # Package files 81 | *.pkg.tar.zst 82 | *.deb 83 | *.rpm 84 | -------------------------------------------------------------------------------- /HYPRLAND_BLUR_CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # HyprMenu Blur Configuration 2 | 3 | To enable native blur for HyprMenu in Hyprland, add these lines to your Hyprland configuration file (`~/.config/hypr/hyprland.conf`): 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mattscreative 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Matt 2 | 3 | pkgname=hyprmenu 4 | pkgver=0.1.0 5 | pkgrel=1 6 | pkgdesc="A modern application launcher for Hyprland" 7 | arch=('x86_64') 8 | url="https://github.com/hyprland-community/hyprmenu" 9 | license=('MIT') 10 | depends=( 11 | 'gtk4' 12 | 'gtk4-layer-shell' 13 | 'hicolor-icon-theme' 14 | 'glib2' 15 | ) 16 | makedepends=( 17 | 'meson' 18 | 'ninja' 19 | 'gcc' 20 | 'pkgconf' 21 | ) 22 | optdepends=( 23 | 'ttf-font-awesome: for icon support' 24 | ) 25 | backup=( 26 | "etc/hyprmenu/hyprmenu.conf" 27 | ) 28 | source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") 29 | sha256sums=('SKIP') 30 | 31 | build() { 32 | cd "$pkgname-$pkgver" 33 | meson setup build \ 34 | --prefix=/usr \ 35 | --buildtype=release \ 36 | --wrap-mode=nofallback 37 | meson compile -C build 38 | } 39 | 40 | package() { 41 | cd "$pkgname-$pkgver" 42 | meson install -C build --destdir "$pkgdir" 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyprMenu 2 | 3 | A highly customizable, modern application launcher for Wayland compositors (Hyprland, Sway, etc.) 4 | 5 | ## Features 6 | - **Fully customizable appearance** via `hyprmenu.conf` 7 | - **Outer and inner borders**: independently style the menu's outer and inner borders 8 | - **Grid and list views**: switchable, with configurable columns, spacing, and item size 9 | - **Flat, icon-only system buttons** (power, lock, etc.) 10 | - **Alphabetically organized config** for easy navigation 11 | - **Per-section options** for Window, Grid, List, AppEntry, Category, Search, SystemButton, Behavior, Style, Layout 12 | - **Live reload**: changes to config are applied instantly 13 | - **Dark mode** and transparency support 14 | - **Extensive color and spacing options** 15 | 16 | ## Configuration 17 | All options are set in `~/.config/hyprmenu/hyprmenu.conf`. Sections and keys are alphabetical for clarity. 18 | 19 | ### Example: Borders 20 | ```ini 21 | [Window] 22 | outer_border_width=3 23 | outer_border_color=#888888 24 | outer_border_radius=16 25 | inner_border_width=2 26 | inner_border_color=#444444 27 | inner_border_radius=12 28 | ``` 29 | 30 | ### Major Sections and Options 31 | - **[Window]**: Outer/inner borders, background, padding, shadow, alignment 32 | - **[Grid]**: Grid columns, item size, spacing, margins, alignment 33 | - **[List]**: List item size, spacing, margins, alignment 34 | - **[AppEntry]**: Icon size, font sizes, colors, padding, hover/active colors 35 | - **[Category]**: Background, text color, font, padding, separators 36 | - **[Search]**: Bar color, font, padding, icon, placeholder, focus border 37 | - **[SystemButton]**: Icon color, hover/active color, size, spacing, flat style 38 | - **[Behavior]**: Close on click, super key, app launch, focus, show/hide UI elements 39 | - **[Style]**: Global background, blur, transparency, AGS effects 40 | - **[Layout]**: Window size, margins, position, offsets 41 | 42 | ### System Buttons: Flat/Icon-Only 43 | System buttons are styled to be flat and icon-only by default. You can further customize their color and hover effect in the `[SystemButton]` section. 44 | 45 | ### Example Config Snippet 46 | ```ini 47 | [Window] 48 | outer_border_width=3 49 | outer_border_color=#888888 50 | outer_border_radius=16 51 | inner_border_width=2 52 | inner_border_color=#444444 53 | inner_border_radius=12 54 | background_opacity=0.85 55 | background_blur=5.0 56 | shadow_color=rgba(0,0,0,0.3) 57 | shadow_radius=20 58 | 59 | [Grid] 60 | grid_columns=5 61 | grid_item_size=100 62 | row_spacing=12 63 | column_spacing=12 64 | 65 | [SystemButton] 66 | icon_color=#fff 67 | hover_color=rgba(255,255,255,0.08) 68 | background_color=transparent 69 | corner_radius=0 70 | ``` 71 | 72 | ## Customization Tips 73 | - **Change border colors/thickness**: Edit `outer_border_*` and `inner_border_*` in `[Window]`. 74 | - **Switch between grid/list**: Use the toggle button or set defaults in `[Grid]`/`[List]`. 75 | - **Make system buttons flat**: Use the default config or set `background_color=transparent` and `corner_radius=0` in `[SystemButton]`. 76 | - **Adjust spacing and padding**: All major sections have `padding`, `margin`, or `spacing` options. 77 | - **Colors**: All color values support hex, rgb(), or rgba(). 78 | 79 | ## Getting Started 80 | 1. Copy or generate a config: `~/.config/hyprmenu/hyprmenu.conf` 81 | 2. Edit the config to your liking (see above for options) 82 | 3. Run HyprMenu and enjoy your custom launcher! 83 | 84 | --- 85 | 86 | For more details, see the comments in the config file or open an issue for help. 87 | 88 | ## Requirements 89 | 90 | - Wayland compositor (specifically designed for Hyprland) 91 | - GTK4 92 | - gtk4-layer-shell 93 | 94 | ## Important Note 95 | 96 | **This application is WAYLAND-ONLY**. It will not work on X11. The application requires a Wayland compositor (specifically Hyprland) and GTK Layer Shell to function properly. 97 | 98 | ## Features 99 | 100 | - Modern, clean interface 101 | - System control buttons (logout, shutdown, reboot, etc.) 102 | - Configurable positioning 103 | - Search functionality 104 | - Dark theme by default 105 | - Support for both grid and list view modes 106 | - **Full color theming:** Every part of the menu (background, border, search, categories, app entries, system buttons, highlights, separators, scrollbar, shadow, etc.) can be themed via the config file. 107 | - **Pywal & AGS color support:** Optionally auto-theme the menu using pywal or AGS color scripts, or override any color manually. 108 | - **Config auto-update:** The config file is auto-regenerated to include all new options when the app is updated. 109 | - **VSCode color picker friendly:** All color options are compatible with color pickers in editors like VSCode. 110 | - **Dynamic grid columns:** Set the number of app columns in grid view (`grid_columns`), and the menu will auto-resize. 111 | - **Grid/List item sizing:** Control the size of app items in both grid (`grid_item_size`) and list (`list_item_size`) views. 112 | - **Proportional icon sizing:** Icons scale automatically with item size in both views. 113 | - **All settings exposed:** All theming and layout options are exposed in the config and auto-documented. 114 | - **No breaking changes:** All new features are backward compatible. 115 | 116 | ## Building 117 | 118 | ```bash 119 | sudo ./build.sh --install 120 | ``` 121 | 122 | ## Installation 123 | 124 | After building, the application will be installed automatically. 125 | 126 | ## License 127 | 128 | This project is licensed under the MIT License. 129 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Colors 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | NC='\033[0m' 10 | 11 | # Print with color 12 | print_status() { 13 | echo -e "${GREEN}==>${NC} $1" 14 | } 15 | 16 | print_warning() { 17 | echo -e "${YELLOW}Warning:${NC} $1" 18 | } 19 | 20 | print_error() { 21 | echo -e "${RED}Error:${NC} $1" 22 | } 23 | 24 | # Check dependencies 25 | check_dependencies() { 26 | local deps=("meson" "ninja" "gcc" "pkg-config") 27 | local pkgconfig_deps=("gtk4" "gtk4-layer-shell-0" "glib-2.0" "gio-2.0" "gio-unix-2.0") 28 | local missing=() 29 | 30 | # Check for command dependencies 31 | for dep in "${deps[@]}"; do 32 | if ! command -v "$dep" &> /dev/null; then 33 | missing+=("$dep") 34 | fi 35 | done 36 | 37 | # Check for pkg-config dependencies 38 | for dep in "${pkgconfig_deps[@]}"; do 39 | if ! pkg-config --exists "$dep" &> /dev/null; then 40 | missing+=("$dep") 41 | fi 42 | done 43 | 44 | if [ ${#missing[@]} -ne 0 ]; then 45 | print_error "Missing dependencies: ${missing[*]}" 46 | exit 1 47 | fi 48 | } 49 | 50 | # Parse arguments 51 | BUILD_TYPE="release" 52 | INSTALL=0 53 | PREFIX="/usr" 54 | CLEAN=0 55 | 56 | while [[ $# -gt 0 ]]; do 57 | case $1 in 58 | --debug) 59 | BUILD_TYPE="debug" 60 | shift 61 | ;; 62 | --install) 63 | INSTALL=1 64 | shift 65 | ;; 66 | --prefix) 67 | PREFIX="$2" 68 | shift 2 69 | ;; 70 | --clean) 71 | CLEAN=1 72 | shift 73 | ;; 74 | *) 75 | print_error "Unknown option: $1" 76 | exit 1 77 | ;; 78 | esac 79 | done 80 | 81 | # Check dependencies 82 | print_status "Checking dependencies..." 83 | check_dependencies 84 | 85 | # Clean build directory if requested 86 | if [ $CLEAN -eq 1 ]; then 87 | print_status "Cleaning build directory..." 88 | rm -rf build/ 89 | fi 90 | 91 | # Configure 92 | print_status "Configuring build..." 93 | meson setup build \ 94 | --prefix="$PREFIX" \ 95 | --buildtype="$BUILD_TYPE" \ 96 | --wrap-mode=nofallback 97 | 98 | # Build 99 | print_status "Building HyprMenu..." 100 | meson compile -C build 101 | 102 | # Install if requested 103 | if [ $INSTALL -eq 1 ]; then 104 | if [ "$PREFIX" = "/usr" ]; then 105 | if [ "$EUID" -ne 0 ]; then 106 | print_error "Installation to /usr requires root privileges" 107 | exit 1 108 | fi 109 | fi 110 | 111 | print_status "Installing HyprMenu..." 112 | meson install -C build 113 | 114 | # Update icon cache if installing system-wide 115 | if [ "$PREFIX" = "/usr" ]; then 116 | print_status "Updating icon cache..." 117 | gtk-update-icon-cache -f -t /usr/share/icons/hicolor 118 | fi 119 | fi 120 | 121 | print_status "Build completed successfully!" 122 | 123 | if [ $INSTALL -eq 0 ]; then 124 | print_status "To install, run: $0 --install" 125 | fi -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set colors for terminal output 4 | GREEN='\033[0;32m' 5 | YELLOW='\033[1;33m' 6 | RED='\033[0;31m' 7 | NC='\033[0m' # No Color 8 | 9 | echo -e "${GREEN}==> Cleaning up HyprMenu project for git...${NC}" 10 | 11 | # Remove debug log files 12 | echo -e "${YELLOW}Removing debug log files...${NC}" 13 | rm -f debug_*.log app_debug.log 14 | 15 | # Remove backup files 16 | echo -e "${YELLOW}Removing backup files...${NC}" 17 | find . -name "*.bak" -o -name "*~" -o -name "*.backup" -delete 18 | 19 | # Clean build directory if requested 20 | if [ "$1" == "--clean-build" ] || [ "$1" == "--full" ]; then 21 | echo -e "${YELLOW}Cleaning build directory...${NC}" 22 | rm -rf build/ 23 | fi 24 | 25 | # Full cleanup 26 | if [ "$1" == "--full" ]; then 27 | echo -e "${YELLOW}Performing full cleanup for git...${NC}" 28 | 29 | # Remove any object files 30 | find . -name "*.o" -delete 31 | 32 | # Remove any temporary files 33 | find . -name "*.tmp" -o -name "*.temp" -delete 34 | 35 | # Remove executable 36 | rm -f hyprmenu 37 | 38 | # Check for any leftover large files (>1MB) 39 | echo -e "${YELLOW}Checking for large files (>1MB) that might be accidentally included:${NC}" 40 | find . -type f -size +1M -not -path "./.git/*" | while read file; do 41 | echo -e "${RED} - $file${NC}" 42 | done 43 | fi 44 | 45 | # Show the current status 46 | echo -e "${GREEN}==> Cleanup complete!${NC}" 47 | echo -e "${YELLOW}Current status:${NC}" 48 | git status -s 49 | 50 | echo -e "${GREEN}==> Files are now ready for git commit${NC}" 51 | echo -e "Run the following commands to commit:" 52 | echo -e " git add ." 53 | echo -e " git commit -m \"Your commit message\"" -------------------------------------------------------------------------------- /data/hyprmenu.conf: -------------------------------------------------------------------------------- 1 | # HyprMenu Configuration File 2 | # This file controls the appearance and behavior of HyprMenu 3 | # All color values support #RRGGBB, #RRGGBBAA, rgb(), or rgba() formats 4 | 5 | [Layout] 6 | # Main window dimensions and positioning 7 | window_width=800 # Width of the menu window in pixels 8 | window_height=600 # Height of the menu window in pixels 9 | top_margin=48 # Margin from the top of the screen 10 | left_margin=8 # Margin from the left of the screen 11 | center_window=false # Whether to center the window on screen 12 | bottom_offset=55 # Offset from bottom for dock/panel (0 to respect reserved space) 13 | top_offset=48 # Offset from top for panel 14 | window_padding=8 # Internal padding of the main window 15 | 16 | [Window] 17 | # Main window appearance 18 | background_color= # Window background color (empty for transparent) 19 | background_opacity=1.0 # Overall window opacity (0.0 to 1.0) 20 | background_blur=5.0 # Background blur strength 21 | corner_radius=12 # Window corner radius 22 | halign=center # Horizontal alignment (start, center, end) 23 | valign=center # Vertical alignment (start, center, end) 24 | shadow_color=rgba(0,0,0,0.3) # Window shadow color 25 | shadow_radius=20 # Window shadow radius 26 | opacity=1.0 # Overall window opacity 27 | 28 | [Border] 29 | # Border appearance settings 30 | inner_border_color=#444444 # Color of the inner border 31 | inner_border_radius=12 # Radius of inner border corners 32 | inner_border_width=2 # Width of inner border 33 | outer_border_color=#888888 # Color of the outer border 34 | outer_border_radius=16 # Radius of outer border corners 35 | outer_border_width=3 # Width of outer border 36 | 37 | [Grid] 38 | # Grid view settings (when showing apps in grid mode) 39 | columns=5 # Number of columns in grid 40 | item_size=100 # Size of each grid item 41 | item_corner_radius=8 # Corner radius of grid items 42 | item_border_width=1 # Border width of grid items 43 | item_border_color=rgba(255,255,255,0.08) # Border color of grid items 44 | item_background_color=rgba(50,50,60,0.7) # Background color of grid items 45 | row_spacing=12 # Vertical spacing between rows 46 | column_spacing=12 # Horizontal spacing between columns 47 | margin_start=12 # Left margin 48 | margin_end=12 # Right margin 49 | margin_top=12 # Top margin 50 | margin_bottom=12 # Bottom margin 51 | halign=center # Horizontal alignment of grid 52 | valign=center # Vertical alignment of grid 53 | hexpand=true # Whether grid expands horizontally 54 | vexpand=false # Whether grid expands vertically 55 | opacity=1.0 # Overall grid opacity 56 | item_opacity=1.0 # Individual item opacity 57 | 58 | [List] 59 | # List view settings (when showing apps in list mode) 60 | item_size=48 # Height of each list item 61 | item_corner_radius=6 # Corner radius of list items 62 | item_border_width=1 # Border width of list items 63 | item_border_color=rgba(255,255,255,0.05) # Border color of list items 64 | item_background_color=rgba(60,60,70,0.6) # Background color of list items 65 | row_spacing=8 # Vertical spacing between items 66 | margin_start=12 # Left margin 67 | margin_end=12 # Right margin 68 | margin_top=12 # Top margin 69 | margin_bottom=12 # Bottom margin 70 | halign=fill # Horizontal alignment of list 71 | valign=center # Vertical alignment of list 72 | hexpand=true # Whether list expands horizontally 73 | vexpand=false # Whether list expands vertically 74 | opacity=1.0 # Overall list opacity 75 | item_opacity=1.0 # Individual item opacity 76 | 77 | [AppEntry] 78 | # Individual application entry appearance 79 | icon_size=32 # Size of application icons 80 | icon_corner_radius=6 # Corner radius of icons 81 | icon_background_color=rgba(60,60,70,0.6) # Background color behind icons 82 | name_font_size=12 # Font size of application names 83 | name_color=#ffffff # Color of application names 84 | desc_font_size=10 # Font size of application descriptions 85 | desc_color=rgba(255,255,255,0.7) # Color of application descriptions 86 | padding=6 # Internal padding of entries 87 | hover_color=rgba(100,100,100,0.8) # Background color on hover 88 | active_color=rgba(100,100,100,0.9) # Background color when clicked 89 | opacity=1.0 # Overall entry opacity 90 | icon_opacity=1.0 # Icon opacity 91 | name_opacity=1.0 # Name text opacity 92 | desc_opacity=1.0 # Description text opacity 93 | 94 | [Category] 95 | # Category header appearance 96 | background_color=#2d2d2d # Category background color 97 | background_opacity=1.0 # Category background opacity 98 | corner_radius=10 # Corner radius of category headers 99 | text_color=rgba(255,255,255,0.9) # Category text color 100 | font_size=13 # Category text size 101 | font_family=Sans Bold # Category font family 102 | padding=6 # Internal padding of categories 103 | show_separators=true # Whether to show separators between categories 104 | separator_color=rgba(255,255,255,0.1) # Color of category separators 105 | opacity=1.0 # Overall category opacity 106 | title_opacity=1.0 # Category title opacity 107 | 108 | [Search] 109 | # Search bar appearance and behavior 110 | background_color=rgba(34,34,34,0.3) # Search bar background color 111 | background_opacity=1.0 # Search bar background opacity 112 | corner_radius=8 # Corner radius of search bar 113 | text_color=#ffffff # Search text color 114 | font_size=14 # Search text size 115 | font_family=Sans # Search text font 116 | padding=8 # Internal padding of search bar 117 | min_height=20 # Minimum height of search bar 118 | left_padding=2 # Left padding of search text 119 | length=0 # Maximum search text length (0 for unlimited) 120 | placeholder_text=Search apps... # Placeholder text when empty 121 | icon_size=18 # Size of search icon 122 | icon_color=#ffffff # Color of search icon 123 | focus_border_color=rgba(255,255,255,0.5) # Border color when focused 124 | focus_shadow_color=rgba(255,255,255,0.2) # Shadow color when focused 125 | opacity=1.0 # Overall search bar opacity 126 | text_opacity=1.0 # Search text opacity 127 | icon_opacity=1.0 # Search icon opacity 128 | 129 | [SystemButton] 130 | # System button appearance (power, settings, etc.) 131 | background_color=rgba(40,42,54,0.7) # Button background color 132 | icon_color=rgba(255,255,255,0.85) # Button icon color 133 | hover_color=rgba(80,85,100,0.5) # Background color on hover 134 | active_color=rgba(90,95,120,0.6) # Background color when clicked 135 | corner_radius=6 # Corner radius of buttons 136 | size=24 # Size of buttons 137 | spacing=8 # Space between buttons 138 | opacity=1.0 # Overall button opacity 139 | icon_opacity=1.0 # Button icon opacity 140 | 141 | [Behavior] 142 | # Program behavior settings 143 | close_on_click_outside=true # Close when clicking outside the menu 144 | close_on_super_key=true # Close when pressing Super key 145 | close_on_app_launch=true # Close when launching an application 146 | focus_search_on_open=true # Focus search bar when opening 147 | close_on_escape=true # Close when pressing Escape 148 | close_on_focus_out=true # Close when losing focus 149 | show_categories=true # Show application categories 150 | show_descriptions=true # Show application descriptions 151 | show_icons=true # Show application icons 152 | show_search=true # Show search bar 153 | show_scrollbar=true # Show scrollbar 154 | show_border=true # Show window border 155 | show_shadow=true # Show window shadow 156 | blur_background=true # Enable background blur 157 | blur_strength=5 # Background blur strength 158 | max_recent_apps=5 # Maximum number of recent apps to show 159 | 160 | [Transparency] 161 | # Global transparency settings 162 | enabled=true # Enable transparency effects 163 | alpha=1.0 # Global alpha value (0.0 to 1.0) 164 | blur=true # Enable blur effects 165 | shadow=true # Enable shadow effects 166 | shadow_color=rgba(0,0,0,0.3) # Shadow color 167 | shadow_radius=20 # Shadow radius -------------------------------------------------------------------------------- /data/icons/hyprmenu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /data/org.hyprmenu.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=HyprMenu 4 | GenericName=Application Launcher 5 | Comment=A modern application launcher for Hyprland 6 | Exec=hyprmenu 7 | Icon=hyprmenu 8 | Terminal=false 9 | Categories=Utility; 10 | Keywords=launcher;menu;application;program; 11 | StartupNotify=false -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1746904237, 24 | "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "HyprMenu - A GTK4 application menu for Hyprland"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | in 14 | { 15 | packages.default = pkgs.stdenv.mkDerivation { 16 | pname = "hyprmenu"; 17 | version = "0.1"; 18 | src = builtins.path { 19 | path = ./.; 20 | name = "hyprmenu-source"; 21 | filter = path: type: 22 | type != "directory" || !(builtins.elem (builtins.baseNameOf path) ["build" "result"]); 23 | }; 24 | 25 | nativeBuildInputs = [ 26 | pkgs.meson 27 | pkgs.ninja 28 | pkgs.pkg-config 29 | pkgs.wrapGAppsHook4 30 | ]; 31 | 32 | buildInputs = [ 33 | pkgs.gtk4 34 | pkgs.gtk4-layer-shell 35 | pkgs.glib 36 | pkgs.gobject-introspection 37 | ]; 38 | 39 | dontWrapGApps = true; 40 | 41 | preFixup = '' 42 | gappsWrapperArgs+=( 43 | --prefix XDG_DATA_DIRS : "${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}" 44 | ) 45 | ''; 46 | 47 | mesonFlags = [ 48 | "--prefix=${placeholder "out"}" 49 | "--buildtype=release" 50 | ]; 51 | }; 52 | 53 | devShells.default = pkgs.mkShell { 54 | packages = with pkgs; [ 55 | meson 56 | ninja 57 | pkg-config 58 | gtk4 59 | gtk4-layer-shell 60 | glib 61 | gobject-introspection 62 | wrapGAppsHook4 63 | ]; 64 | 65 | shellHook = '' 66 | export XDG_DATA_DIRS=${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}:$XDG_DATA_DIRS 67 | ''; 68 | }; 69 | 70 | apps.default = { 71 | type = "app"; 72 | program = "${self.packages.${system}.default}/bin/hyprmenu"; 73 | }; 74 | }); 75 | } -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('hyprmenu', 'c', 2 | version: '0.1.0', 3 | license: 'MIT', 4 | default_options: [ 5 | 'c_std=c11', 6 | 'warning_level=2', 7 | 'prefix=/usr', 8 | ], 9 | ) 10 | 11 | # Dependencies 12 | gtk_dep = dependency('gtk4') 13 | layer_shell_dep = dependency('gtk4-layer-shell-0') 14 | glib_dep = dependency('glib-2.0') 15 | gio_dep = dependency('gio-2.0') 16 | gio_unix_dep = dependency('gio-unix-2.0') 17 | 18 | # Installation directories 19 | prefix = get_option('prefix') 20 | bindir = join_paths(prefix, get_option('bindir')) 21 | datadir = join_paths(prefix, get_option('datadir')) 22 | sysconfdir = get_option('sysconfdir') 23 | icondir = join_paths(datadir, 'icons/hicolor') 24 | 25 | # Source files 26 | sources = [ 27 | 'src/main.c', 28 | 'src/window.c', 29 | 'src/app_grid.c', 30 | 'src/category_list.c', 31 | 'src/app_entry.c', 32 | 'src/config.c', 33 | 'src/list_view.c', 34 | ] 35 | 36 | # Header files for installation 37 | headers = [ 38 | 'src/window.h', 39 | 'src/app_grid.h', 40 | 'src/category_list.h', 41 | 'src/app_entry.h', 42 | 'src/config.h', 43 | 'src/list_view.h', 44 | ] 45 | 46 | # Build configuration 47 | conf = configuration_data() 48 | conf.set_quoted('PACKAGE_VERSION', meson.project_version()) 49 | conf.set_quoted('SYSCONFDIR', sysconfdir) 50 | conf.set_quoted('DATADIR', datadir) 51 | 52 | configure_file( 53 | output: 'config.h', 54 | configuration: conf 55 | ) 56 | 57 | # Build the executable 58 | executable('hyprmenu', 59 | sources, 60 | dependencies: [ 61 | gtk_dep, 62 | layer_shell_dep, 63 | glib_dep, 64 | gio_dep, 65 | gio_unix_dep, 66 | ], 67 | install: true, 68 | install_dir: bindir 69 | ) 70 | 71 | # Install header files 72 | install_headers(headers) 73 | 74 | # Install desktop file 75 | install_data('data/org.hyprmenu.desktop', 76 | install_dir: join_paths(datadir, 'applications') 77 | ) 78 | 79 | # Install icons 80 | install_data('data/icons/hyprmenu.svg', 81 | install_dir: join_paths(icondir, 'scalable/apps') 82 | ) 83 | 84 | # Install license and documentation 85 | install_data('LICENSE', install_dir: join_paths(datadir, 'licenses', 'hyprmenu')) 86 | install_data('README.md', install_dir: join_paths(datadir, 'doc', 'hyprmenu')) -------------------------------------------------------------------------------- /src/app_entry.c: -------------------------------------------------------------------------------- 1 | #include "app_entry.h" 2 | #include "config.h" 3 | #include "window.h" 4 | #include 5 | 6 | // Function declarations 7 | static void on_clicked(GtkGestureClick *gesture, gint n_press, double x, double y, gpointer user_data); 8 | static void launch_application(GDesktopAppInfo *app_info, GtkWidget *widget); 9 | 10 | struct _HyprMenuAppEntry 11 | { 12 | GtkButton parent_instance; 13 | 14 | GDesktopAppInfo *app_info; 15 | char *app_id; 16 | char *app_name; 17 | char **categories; 18 | 19 | GtkWidget *main_box; 20 | GtkWidget *icon; 21 | GtkWidget *name_label; 22 | 23 | gboolean is_grid_layout; // Whether we're using grid layout (vertical) 24 | int icon_size; 25 | }; 26 | 27 | G_DEFINE_TYPE (HyprMenuAppEntry, hyprmenu_app_entry, GTK_TYPE_BUTTON) 28 | 29 | static void 30 | show_context_menu(HyprMenuAppEntry *self, double x, double y) 31 | { 32 | g_print("CONTEXT MENU: Creating menu for app: %s\n", self->app_name ? self->app_name : "Unknown"); 33 | 34 | GtkWidget *popover = gtk_popover_new(); 35 | gtk_widget_set_halign(popover, GTK_ALIGN_START); 36 | gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE); 37 | 38 | // Create a container for the popover content 39 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); 40 | gtk_widget_set_margin_start(box, 8); 41 | gtk_widget_set_margin_end(box, 8); 42 | gtk_widget_set_margin_top(box, 8); 43 | gtk_widget_set_margin_bottom(box, 8); 44 | 45 | // Create launch button 46 | GtkWidget *launch_button = gtk_button_new_with_label("Launch"); 47 | gtk_widget_add_css_class(launch_button, "menu-button"); 48 | gtk_widget_set_can_focus(launch_button, TRUE); 49 | gtk_button_set_has_frame(GTK_BUTTON(launch_button), TRUE); 50 | g_signal_connect(launch_button, "clicked", G_CALLBACK(on_clicked), self); 51 | 52 | gtk_box_append(GTK_BOX(box), launch_button); 53 | 54 | // Set the popover content 55 | gtk_popover_set_child(GTK_POPOVER(popover), box); 56 | 57 | // Position the popover relative to the click coordinates rather than the widget 58 | GdkRectangle rect = { x, y, 1, 1 }; 59 | gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect); 60 | 61 | // Set the parent, position and display 62 | gtk_widget_set_parent(popover, GTK_WIDGET(self)); 63 | gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_RIGHT); 64 | gtk_popover_popup(GTK_POPOVER(popover)); 65 | 66 | g_print("CONTEXT MENU: Popover displayed\n"); 67 | } 68 | 69 | static void 70 | on_right_click(GtkGestureClick *gesture, 71 | gint n_press, 72 | double x, 73 | double y, 74 | gpointer user_data) 75 | { 76 | g_print("RIGHT CLICK DETECTED! x=%.1f, y=%.1f, n_press=%d\n", x, y, n_press); 77 | 78 | HyprMenuAppEntry *self = HYPRMENU_APP_ENTRY(user_data); 79 | 80 | if (!self) { 81 | g_warning("RIGHT CLICK ERROR: self is NULL"); 82 | return; 83 | } 84 | 85 | g_print("RIGHT CLICK DEBUG: App entry is for: %s\n", self->app_name ? self->app_name : "Unknown"); 86 | 87 | // Make sure the gesture is recognized and claimed 88 | GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); 89 | if (sequence) { 90 | gtk_gesture_set_sequence_state(GTK_GESTURE(gesture), sequence, GTK_EVENT_SEQUENCE_CLAIMED); 91 | g_print("RIGHT CLICK DEBUG: Claimed event sequence\n"); 92 | } else { 93 | g_print("RIGHT CLICK DEBUG: No event sequence to claim\n"); 94 | } 95 | 96 | show_context_menu(self, x, y); 97 | g_print("RIGHT CLICK DEBUG: Context menu displayed\n"); 98 | } 99 | 100 | static void 101 | on_clicked(GtkGestureClick *gesture, 102 | gint n_press, 103 | double x, 104 | double y, 105 | gpointer user_data) 106 | { 107 | // Suppress unused parameter warnings 108 | (void)gesture; 109 | (void)n_press; 110 | (void)x; 111 | (void)y; 112 | 113 | g_print("DEBUG: on_clicked() handler called\n"); 114 | 115 | HyprMenuAppEntry *self = HYPRMENU_APP_ENTRY(user_data); 116 | 117 | if (!self) { 118 | g_warning("LAUNCH ERROR: App entry is NULL in on_clicked"); 119 | return; 120 | } 121 | 122 | if (!self->app_info) { 123 | g_warning("LAUNCH ERROR: App info is NULL for entry %s", 124 | self->app_name ? self->app_name : "(unknown)"); 125 | return; 126 | } 127 | 128 | g_print("DEBUG: Launching application: %s\n", self->app_name ? self->app_name : "(unknown)"); 129 | 130 | // Launch the application 131 | launch_application(self->app_info, GTK_WIDGET(self)); 132 | } 133 | 134 | static void 135 | hyprmenu_app_entry_dispose (GObject *object) 136 | { 137 | HyprMenuAppEntry *self = HYPRMENU_APP_ENTRY (object); 138 | 139 | // First remove all children from main_box 140 | if (self->main_box) { 141 | GtkWidget *child = gtk_widget_get_first_child(self->main_box); 142 | while (child) { 143 | GtkWidget *next = gtk_widget_get_next_sibling(child); 144 | gtk_widget_unparent(child); 145 | child = next; 146 | } 147 | // Then unparent main_box itself 148 | gtk_widget_unparent(self->main_box); 149 | self->main_box = NULL; 150 | } 151 | 152 | // Clear references to child widgets since they're owned by main_box 153 | self->icon = NULL; 154 | self->name_label = NULL; 155 | 156 | G_OBJECT_CLASS (hyprmenu_app_entry_parent_class)->dispose (object); 157 | } 158 | 159 | static void 160 | hyprmenu_app_entry_finalize (GObject *object) 161 | { 162 | HyprMenuAppEntry *self = HYPRMENU_APP_ENTRY (object); 163 | 164 | g_clear_object (&self->app_info); 165 | g_free (self->app_id); 166 | g_free (self->app_name); 167 | g_strfreev (self->categories); 168 | 169 | G_OBJECT_CLASS (hyprmenu_app_entry_parent_class)->finalize (object); 170 | } 171 | 172 | /* Create a horizontal box for list view */ 173 | static GtkWidget* 174 | create_list_layout(HyprMenuAppEntry *self) 175 | { 176 | /* Make a simple box with 10px spacing between elements */ 177 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); 178 | gtk_widget_add_css_class(box, "hyprmenu-list-row"); 179 | gtk_widget_set_hexpand(box, TRUE); 180 | gtk_widget_set_margin_start(box, 4); 181 | gtk_widget_set_margin_end(box, 4); 182 | gtk_widget_set_margin_top(box, 2); 183 | gtk_widget_set_margin_bottom(box, 2); 184 | 185 | // Set the height of the list item and ensure it's respected 186 | gtk_widget_set_size_request(box, -1, config->list_item_size); 187 | gtk_widget_set_valign(box, GTK_ALIGN_CENTER); 188 | 189 | /* Create a fixed size icon */ 190 | GtkWidget *icon_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 191 | gtk_widget_set_halign(icon_box, GTK_ALIGN_CENTER); 192 | gtk_widget_set_valign(icon_box, GTK_ALIGN_CENTER); 193 | 194 | // Set icon box size to 75% of list item height 195 | int icon_size = config->list_item_size * 0.75; 196 | gtk_widget_set_size_request(icon_box, icon_size, icon_size); 197 | gtk_box_append(GTK_BOX(box), icon_box); 198 | 199 | // Create the icon 200 | GtkWidget *icon = gtk_image_new_from_icon_name("application-x-executable"); 201 | gtk_image_set_pixel_size(GTK_IMAGE(icon), icon_size * 0.8); // Icon slightly smaller than box 202 | gtk_widget_set_margin_start(icon, 4); 203 | gtk_widget_set_margin_end(icon, 4); 204 | gtk_box_append(GTK_BOX(icon_box), icon); 205 | 206 | // Try to replace with actual app icon if available 207 | if (self->app_info) { 208 | GIcon *app_icon = g_app_info_get_icon(G_APP_INFO(self->app_info)); 209 | if (app_icon) { 210 | gtk_image_set_from_gicon(GTK_IMAGE(icon), app_icon); 211 | } 212 | } 213 | 214 | self->icon = icon; 215 | 216 | /* Create a label box for vertical alignment */ 217 | GtkWidget *label_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 218 | gtk_widget_set_hexpand(label_box, TRUE); 219 | gtk_widget_set_valign(label_box, GTK_ALIGN_CENTER); 220 | gtk_box_append(GTK_BOX(box), label_box); 221 | 222 | // Create label with app name 223 | GtkWidget *name_label = gtk_label_new(NULL); 224 | char *markup = g_markup_printf_escaped("%s", 225 | self->app_name ? self->app_name : "Unknown"); 226 | gtk_label_set_markup(GTK_LABEL(name_label), markup); 227 | g_free(markup); 228 | 229 | gtk_label_set_xalign(GTK_LABEL(name_label), 0); 230 | gtk_widget_set_hexpand(name_label, TRUE); 231 | gtk_widget_set_valign(name_label, GTK_ALIGN_CENTER); 232 | gtk_box_append(GTK_BOX(label_box), name_label); 233 | 234 | self->name_label = name_label; 235 | 236 | return box; 237 | } 238 | 239 | /* Create a tile-style layout for grid view */ 240 | static GtkWidget* 241 | create_grid_layout(HyprMenuAppEntry *self) 242 | { 243 | /* Create box for tile */ 244 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); 245 | gtk_widget_add_css_class(box, "hyprmenu-app-entry"); 246 | gtk_widget_add_css_class(box, "grid-item"); 247 | 248 | /* Set fixed size for the box */ 249 | gtk_widget_set_size_request(box, config->grid_item_size, config->grid_item_size); 250 | gtk_widget_set_margin_start(box, 2); 251 | gtk_widget_set_margin_end(box, 2); 252 | gtk_widget_set_margin_top(box, 2); 253 | gtk_widget_set_margin_bottom(box, 2); 254 | 255 | /* Create a box for the icon to center it */ 256 | GtkWidget *icon_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 257 | gtk_widget_set_hexpand(icon_box, TRUE); 258 | gtk_widget_set_vexpand(icon_box, TRUE); 259 | gtk_widget_set_halign(icon_box, GTK_ALIGN_CENTER); 260 | gtk_widget_set_valign(icon_box, GTK_ALIGN_CENTER); 261 | gtk_widget_set_margin_top(icon_box, 8); 262 | gtk_box_append(GTK_BOX(box), icon_box); 263 | 264 | /* DIRECT ICON APPROACH - similar to list view */ 265 | // Create the icon - use a built-in gtk icon first for guaranteed visibility 266 | GtkWidget *icon = gtk_image_new_from_icon_name("application-x-executable"); 267 | gtk_image_set_pixel_size(GTK_IMAGE(icon), config->app_icon_size); // Use configured icon size 268 | gtk_box_append(GTK_BOX(icon_box), icon); 269 | 270 | // Try to replace with actual app icon if available 271 | if (self->app_info) { 272 | GIcon *app_icon = g_app_info_get_icon(G_APP_INFO(self->app_info)); 273 | if (app_icon) { 274 | gtk_image_set_from_gicon(GTK_IMAGE(icon), app_icon); 275 | } 276 | } 277 | 278 | self->icon = icon; 279 | 280 | /* Create a label container for the name */ 281 | GtkWidget *label_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 282 | gtk_widget_set_hexpand(label_container, TRUE); 283 | gtk_widget_set_halign(label_container, GTK_ALIGN_CENTER); 284 | gtk_widget_set_margin_top(label_container, 4); 285 | gtk_widget_set_margin_start(label_container, 4); 286 | gtk_widget_set_margin_end(label_container, 4); 287 | gtk_widget_set_margin_bottom(label_container, 8); 288 | gtk_box_append(GTK_BOX(box), label_container); 289 | 290 | /* Add name label with markup */ 291 | GtkWidget *name_label = gtk_label_new(NULL); 292 | char *markup = g_markup_printf_escaped("%s", 293 | self->app_name ? self->app_name : "Unknown"); 294 | gtk_label_set_markup(GTK_LABEL(name_label), markup); 295 | g_free(markup); 296 | 297 | gtk_label_set_ellipsize(GTK_LABEL(name_label), PANGO_ELLIPSIZE_END); 298 | gtk_label_set_lines(GTK_LABEL(name_label), 2); 299 | gtk_label_set_max_width_chars(GTK_LABEL(name_label), 12); 300 | gtk_label_set_wrap(GTK_LABEL(name_label), TRUE); 301 | gtk_label_set_justify(GTK_LABEL(name_label), GTK_JUSTIFY_CENTER); 302 | gtk_widget_set_halign(name_label, GTK_ALIGN_CENTER); 303 | gtk_box_append(GTK_BOX(label_container), name_label); 304 | 305 | self->name_label = name_label; 306 | 307 | return box; 308 | } 309 | 310 | static void 311 | hyprmenu_app_entry_init (HyprMenuAppEntry *self) 312 | { 313 | g_print("INIT: Creating app entry\n"); 314 | 315 | /* Initialize layout mode - default to horizontal */ 316 | self->is_grid_layout = FALSE; 317 | self->icon_size = 48; // Default icon size 318 | 319 | /* Create main box - horizontal for list view by default */ 320 | self->main_box = create_list_layout(self); 321 | 322 | /* Add main box to self */ 323 | gtk_widget_set_parent(self->main_box, GTK_WIDGET(self)); 324 | 325 | /* Add context menu styles */ 326 | static gboolean menu_styles_added = FALSE; 327 | if (!menu_styles_added) { 328 | GtkCssProvider *provider = gtk_css_provider_new(); 329 | gtk_css_provider_load_from_string(provider, 330 | ".menu-button {" 331 | " padding: 8px 12px;" 332 | " margin: 2px;" 333 | " border-radius: 4px;" 334 | "}" 335 | ".menu-button:hover {" 336 | " background-color: alpha(@theme_selected_bg_color, 0.1);" 337 | "}" 338 | ".menu-button:active {" 339 | " background-color: alpha(@theme_selected_bg_color, 0.2);" 340 | "}"); 341 | 342 | GdkDisplay *display = gdk_display_get_default(); 343 | gtk_style_context_add_provider_for_display(display, 344 | GTK_STYLE_PROVIDER(provider), 345 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); 346 | g_object_unref(provider); 347 | menu_styles_added = TRUE; 348 | g_print("INIT: Added context menu styles\n"); 349 | } 350 | 351 | g_print("INIT: Adding click gestures\n"); 352 | 353 | /* Make sure button is clickable */ 354 | gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE); 355 | gtk_widget_set_focusable(GTK_WIDGET(self), TRUE); 356 | 357 | /* Add left-click gesture */ 358 | GtkGesture *left_click = gtk_gesture_click_new(); 359 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(left_click), GDK_BUTTON_PRIMARY); 360 | g_signal_connect(left_click, "released", G_CALLBACK(on_clicked), self); 361 | g_signal_connect(left_click, "pressed", G_CALLBACK(on_clicked), self); // Try both signals 362 | gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(left_click)); 363 | 364 | /* Connect the clicked signal for GtkButton base class functionality */ 365 | g_signal_connect(self, "clicked", G_CALLBACK(on_clicked), self); 366 | 367 | /* Add right-click gesture */ 368 | GtkGesture *right_click = gtk_gesture_click_new(); 369 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(right_click), GDK_BUTTON_SECONDARY); 370 | 371 | /* Connect to both released and pressed signals to increase chances of capturing */ 372 | g_signal_connect(right_click, "released", G_CALLBACK(on_right_click), self); 373 | g_signal_connect(right_click, "pressed", G_CALLBACK(on_right_click), self); 374 | 375 | /* Add controller in capture phase to catch events early */ 376 | gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(right_click), 377 | GTK_PHASE_CAPTURE); 378 | 379 | gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(right_click)); 380 | 381 | g_print("INIT: App entry created successfully\n"); 382 | } 383 | 384 | static void 385 | hyprmenu_app_entry_class_init (HyprMenuAppEntryClass *klass) 386 | { 387 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 388 | 389 | object_class->dispose = hyprmenu_app_entry_dispose; 390 | object_class->finalize = hyprmenu_app_entry_finalize; 391 | } 392 | 393 | HyprMenuAppEntry * 394 | hyprmenu_app_entry_new (GDesktopAppInfo *app_info) 395 | { 396 | if (!app_info) { 397 | g_warning("app_entry_new: Attempted to create entry with NULL app_info"); 398 | return NULL; 399 | } 400 | 401 | const char *app_name = g_app_info_get_name(G_APP_INFO(app_info)); 402 | if (!app_name || *app_name == '\0') { 403 | g_warning("app_entry_new: App has no name or empty name, skipping"); 404 | return NULL; 405 | } 406 | 407 | HyprMenuAppEntry *self = g_object_new (HYPRMENU_TYPE_APP_ENTRY, NULL); 408 | 409 | /* Store app info */ 410 | self->app_info = g_object_ref (app_info); 411 | self->app_id = g_strdup (g_app_info_get_id (G_APP_INFO (app_info))); 412 | self->app_name = g_strdup (app_name); 413 | 414 | /* Get categories */ 415 | self->categories = NULL; 416 | const char *categories_str = g_desktop_app_info_get_categories(app_info); 417 | 418 | if (categories_str && g_utf8_validate(categories_str, -1, NULL)) { 419 | // Parse only if valid UTF-8 420 | self->categories = g_strsplit(categories_str, ";", -1); 421 | 422 | // Verify we got valid categories 423 | if (!self->categories || !self->categories[0] || !self->categories[0][0]) { 424 | // Invalid or empty categories, free and set to default 425 | if (self->categories) { 426 | g_strfreev(self->categories); 427 | } 428 | self->categories = g_strsplit("Other", ";", -1); 429 | } 430 | } else { 431 | // Default category 432 | self->categories = g_strsplit("Other", ";", -1); 433 | } 434 | 435 | // Note: Icon and label text are now set directly in the layout functions 436 | 437 | return self; 438 | } 439 | 440 | int 441 | hyprmenu_app_entry_compare_by_name(HyprMenuAppEntry *a, HyprMenuAppEntry *b) 442 | { 443 | if (!a || !b) return 0; 444 | 445 | const char *name_a = hyprmenu_app_entry_get_app_name(a); 446 | const char *name_b = hyprmenu_app_entry_get_app_name(b); 447 | 448 | if (!name_a) name_a = ""; 449 | if (!name_b) name_b = ""; 450 | 451 | return g_ascii_strcasecmp(name_a, name_b); 452 | } 453 | 454 | const char * 455 | hyprmenu_app_entry_get_app_name (HyprMenuAppEntry *self) 456 | { 457 | return self->app_name; 458 | } 459 | 460 | const char * 461 | hyprmenu_app_entry_get_app_id (HyprMenuAppEntry *self) 462 | { 463 | return self->app_id; 464 | } 465 | 466 | const char ** 467 | hyprmenu_app_entry_get_categories (HyprMenuAppEntry *self) 468 | { 469 | if (!self) { 470 | g_warning("hyprmenu_app_entry_get_categories: NULL self pointer"); 471 | return NULL; 472 | } 473 | 474 | if (!self->categories) { 475 | g_warning("hyprmenu_app_entry_get_categories: NULL categories for app %s", 476 | self->app_name ? self->app_name : "(unknown)"); 477 | return NULL; 478 | } 479 | 480 | return (const char **) self->categories; 481 | } 482 | 483 | /** 484 | * hyprmenu_app_entry_set_grid_layout: 485 | * @self: A #HyprMenuAppEntry 486 | * @is_grid: TRUE for grid/vertical layout, FALSE for list/horizontal layout 487 | * 488 | * Sets the layout of the app entry to be either vertical (for grid view) or 489 | * horizontal (for list view). 490 | */ 491 | void 492 | hyprmenu_app_entry_set_grid_layout (HyprMenuAppEntry *self, gboolean is_grid) 493 | { 494 | if (!self) return; 495 | 496 | /* Skip if already in the right layout */ 497 | if (self->is_grid_layout == is_grid) { 498 | return; 499 | } 500 | 501 | /* Update flag */ 502 | self->is_grid_layout = is_grid; 503 | 504 | /* Clear old children */ 505 | if (self->main_box) { 506 | // Unparent will handle destroying the widget and its children 507 | gtk_widget_unparent(self->main_box); 508 | self->main_box = NULL; 509 | self->icon = NULL; 510 | self->name_label = NULL; 511 | } 512 | 513 | /* Create appropriate layout */ 514 | if (is_grid) { 515 | self->main_box = create_grid_layout(self); 516 | } else { 517 | self->main_box = create_list_layout(self); 518 | } 519 | 520 | /* Set parent */ 521 | if (self->main_box) { 522 | gtk_widget_set_parent(self->main_box, GTK_WIDGET(self)); 523 | } 524 | } 525 | 526 | GDesktopAppInfo* 527 | hyprmenu_app_entry_get_app_info (HyprMenuAppEntry *self) 528 | { 529 | if (!self) { 530 | g_warning("hyprmenu_app_entry_get_app_info: NULL self pointer"); 531 | return NULL; 532 | } 533 | 534 | return self->app_info; 535 | } 536 | 537 | void 538 | hyprmenu_app_entry_launch (HyprMenuAppEntry *self) 539 | { 540 | if (!self) { 541 | g_warning("hyprmenu_app_entry_launch: NULL self pointer"); 542 | return; 543 | } 544 | 545 | launch_application(self->app_info, GTK_WIDGET(self)); 546 | } 547 | 548 | GIcon* 549 | hyprmenu_app_entry_get_icon (HyprMenuAppEntry *self) 550 | { 551 | g_return_val_if_fail(HYPRMENU_IS_APP_ENTRY(self), NULL); 552 | 553 | if (self->app_info) { 554 | return g_app_info_get_icon(G_APP_INFO(self->app_info)); 555 | } 556 | 557 | return NULL; 558 | } 559 | 560 | void 561 | hyprmenu_app_entry_set_icon_size(HyprMenuAppEntry *self, int size) 562 | { 563 | g_return_if_fail(HYPRMENU_IS_APP_ENTRY(self)); 564 | 565 | self->icon_size = size; 566 | if (GTK_IS_IMAGE(self->icon)) { 567 | gtk_image_set_pixel_size(GTK_IMAGE(self->icon), size); 568 | } 569 | } 570 | 571 | static void 572 | update_layout(HyprMenuAppEntry *self) 573 | { 574 | if (config->grid_hexpand) { 575 | // Create grid layout 576 | GtkWidget *new_box = create_grid_layout(self); 577 | gtk_widget_set_parent(new_box, GTK_WIDGET(self)); 578 | gtk_widget_unparent(self->main_box); 579 | self->main_box = new_box; 580 | self->is_grid_layout = TRUE; 581 | } else { 582 | // Create list layout 583 | GtkWidget *new_box = create_list_layout(self); 584 | gtk_widget_set_parent(new_box, GTK_WIDGET(self)); 585 | gtk_widget_unparent(self->main_box); 586 | self->main_box = new_box; 587 | self->is_grid_layout = FALSE; 588 | } 589 | } 590 | 591 | // Add the launch_application function back 592 | static void 593 | launch_application(GDesktopAppInfo *app_info, GtkWidget *widget) 594 | { 595 | g_print("DEBUG: launch_application() called\n"); 596 | 597 | if (!app_info) { 598 | g_warning("LAUNCH ERROR: app_info is NULL"); 599 | return; 600 | } 601 | 602 | if (!widget) { 603 | g_warning("LAUNCH ERROR: widget is NULL"); 604 | return; 605 | } 606 | 607 | const char *app_name = g_app_info_get_name(G_APP_INFO(app_info)); 608 | const char *app_cmd = g_app_info_get_commandline(G_APP_INFO(app_info)); 609 | g_print("DEBUG: Launching app '%s' with command: %s\n", 610 | app_name ? app_name : "(unknown)", 611 | app_cmd ? app_cmd : "(unknown)"); 612 | 613 | GError *error = NULL; 614 | 615 | if (!g_app_info_launch(G_APP_INFO(app_info), NULL, NULL, &error)) { 616 | g_warning("Failed to launch application: %s", error->message); 617 | g_error_free(error); 618 | return; 619 | } 620 | 621 | g_print("DEBUG: App launch successful\n"); 622 | 623 | // Close the window if configured to do so 624 | if (config->close_on_app_launch) { 625 | g_print("DEBUG: Configured to close on app launch, closing window\n"); 626 | // Get the parent window and close it 627 | GtkRoot *root = gtk_widget_get_root(widget); 628 | if (GTK_IS_WINDOW(root)) { 629 | gtk_window_close(GTK_WINDOW(root)); 630 | } 631 | } 632 | } 633 | 634 | void 635 | hyprmenu_app_entry_activate(HyprMenuAppEntry *self) 636 | { 637 | g_return_if_fail(HYPRMENU_IS_APP_ENTRY(self)); 638 | 639 | launch_application(self->app_info, GTK_WIDGET(self)); 640 | } -------------------------------------------------------------------------------- /src/app_entry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | G_BEGIN_DECLS 7 | 8 | #define HYPRMENU_TYPE_APP_ENTRY (hyprmenu_app_entry_get_type()) 9 | G_DECLARE_FINAL_TYPE (HyprMenuAppEntry, hyprmenu_app_entry, HYPRMENU, APP_ENTRY, GtkButton) 10 | 11 | HyprMenuAppEntry* hyprmenu_app_entry_new (GDesktopAppInfo *app_info); 12 | const char* hyprmenu_app_entry_get_app_name (HyprMenuAppEntry *self); 13 | const char* hyprmenu_app_entry_get_app_id (HyprMenuAppEntry *self); 14 | const char** hyprmenu_app_entry_get_categories (HyprMenuAppEntry *self); 15 | void hyprmenu_app_entry_set_grid_layout (HyprMenuAppEntry *self, gboolean is_grid); 16 | void hyprmenu_app_entry_set_icon_size(HyprMenuAppEntry *self, int size); 17 | GIcon* hyprmenu_app_entry_get_icon (HyprMenuAppEntry *self); 18 | 19 | GDesktopAppInfo* hyprmenu_app_entry_get_app_info (HyprMenuAppEntry *self); 20 | void hyprmenu_app_entry_launch (HyprMenuAppEntry *self); 21 | 22 | int hyprmenu_app_entry_compare_by_name(HyprMenuAppEntry *a, HyprMenuAppEntry *b); 23 | 24 | G_END_DECLS -------------------------------------------------------------------------------- /src/app_grid.c: -------------------------------------------------------------------------------- 1 | #include "app_grid.h" 2 | #include "category_list.h" 3 | #include "app_entry.h" 4 | #include "config.h" 5 | #include 6 | #include 7 | #include // For sync() function 8 | 9 | struct _HyprMenuAppGrid 10 | { 11 | GtkBox parent_instance; 12 | 13 | GtkWidget *category_list; // Grid view 14 | GtkWidget *list_view; // List view 15 | GtkWidget *scrolled_window; 16 | GtkWidget *toggle_button; // Toggle button for grid/list view 17 | GtkWidget *current_view; // Points to either category_list or list_view 18 | 19 | GArray *app_entries; 20 | char *filter_text; 21 | 22 | // Add event controller for key events 23 | GtkEventController *key_controller; 24 | }; 25 | 26 | G_DEFINE_TYPE (HyprMenuAppGrid, hyprmenu_app_grid, GTK_TYPE_BOX) 27 | 28 | static void 29 | close_window(GtkWidget *widget) 30 | { 31 | GtkRoot *root = gtk_widget_get_root(widget); 32 | if (GTK_IS_WINDOW(root)) { 33 | gtk_window_close(GTK_WINDOW(root)); 34 | } 35 | } 36 | 37 | static gboolean 38 | on_key_pressed(GtkEventController *controller, 39 | guint keyval, 40 | guint keycode, 41 | GdkModifierType state, 42 | gpointer user_data) 43 | { 44 | // Check if Super key was pressed and if configured to close on Super key 45 | if ((keyval == GDK_KEY_Super_L || keyval == GDK_KEY_Super_R) && 46 | config->close_on_super_key) { 47 | HyprMenuAppGrid *self = HYPRMENU_APP_GRID(user_data); 48 | close_window(GTK_WIDGET(self)); 49 | return TRUE; 50 | } 51 | return FALSE; 52 | } 53 | 54 | static void 55 | on_click_outside(GtkGestureClick *gesture, 56 | gint n_press, 57 | double x, 58 | double y, 59 | gpointer user_data) 60 | { 61 | // Only proceed if configured to close on click outside 62 | if (!config->close_on_click_outside) { 63 | return; 64 | } 65 | 66 | HyprMenuAppGrid *self = HYPRMENU_APP_GRID(user_data); 67 | GtkWidget *widget = GTK_WIDGET(self); 68 | graphene_rect_t bounds; 69 | 70 | // Get the widget's bounds 71 | if (!gtk_widget_compute_bounds(widget, widget, &bounds)) { 72 | return; // Failed to compute bounds 73 | } 74 | 75 | // Check if the click is outside the widget's bounds 76 | if (x < bounds.origin.x || y < bounds.origin.y || 77 | x > bounds.origin.x + bounds.size.width || 78 | y > bounds.origin.y + bounds.size.height) { 79 | close_window(widget); 80 | } 81 | } 82 | 83 | static void 84 | on_toggle_view_clicked(GtkButton *button, gpointer user_data) 85 | { 86 | HyprMenuAppGrid *self = HYPRMENU_APP_GRID(user_data); 87 | g_print("Toggle button clicked! Current mode: %s\n", config->grid_hexpand ? "grid" : "list"); 88 | hyprmenu_app_grid_toggle_view(self); 89 | } 90 | 91 | static void 92 | hyprmenu_app_grid_finalize (GObject *object) 93 | { 94 | HyprMenuAppGrid *self = HYPRMENU_APP_GRID (object); 95 | 96 | g_free (self->filter_text); 97 | 98 | if (self->app_entries) { 99 | for (guint i = 0; i < self->app_entries->len; i++) { 100 | HyprMenuAppEntry *entry = g_array_index(self->app_entries, HyprMenuAppEntry*, i); 101 | if (entry) { 102 | g_object_unref(entry); 103 | } 104 | } 105 | g_array_unref (self->app_entries); 106 | } 107 | 108 | G_OBJECT_CLASS (hyprmenu_app_grid_parent_class)->finalize (object); 109 | } 110 | 111 | static void 112 | hyprmenu_app_grid_init (HyprMenuAppGrid *self) 113 | { 114 | /* Initialize data */ 115 | self->app_entries = g_array_new (FALSE, FALSE, sizeof (HyprMenuAppEntry *)); 116 | g_array_set_clear_func (self->app_entries, (GDestroyNotify) g_object_unref); 117 | self->filter_text = NULL; 118 | 119 | /* Create UI */ 120 | gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL); 121 | 122 | /* Create toggle button */ 123 | self->toggle_button = gtk_button_new_from_icon_name( 124 | config->grid_hexpand ? "view-list-symbolic" : "view-grid-symbolic"); 125 | gtk_widget_add_css_class(self->toggle_button, "flat"); 126 | gtk_widget_set_tooltip_text(self->toggle_button, 127 | config->grid_hexpand ? "Switch to List View" : "Switch to Grid View"); 128 | 129 | // Make toggle button more visible with a distinct icon size 130 | GtkWidget *icon = gtk_button_get_child(GTK_BUTTON(self->toggle_button)); 131 | if (GTK_IS_IMAGE(icon)) { 132 | gtk_image_set_pixel_size(GTK_IMAGE(icon), 20); // Make icon slightly larger 133 | } 134 | 135 | g_signal_connect(self->toggle_button, "clicked", G_CALLBACK(on_toggle_view_clicked), self); 136 | g_print("Toggle button created with icon %s for mode: %s\n", 137 | config->grid_hexpand ? "view-list-symbolic" : "view-grid-symbolic", 138 | config->grid_hexpand ? "grid" : "list"); 139 | 140 | /* Create scrolled window */ 141 | self->scrolled_window = gtk_scrolled_window_new (); 142 | gtk_widget_set_vexpand (self->scrolled_window, TRUE); 143 | gtk_widget_set_hexpand(self->scrolled_window, FALSE); 144 | gtk_widget_set_halign(self->scrolled_window, GTK_ALIGN_CENTER); 145 | gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (self->scrolled_window), 146 | GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); 147 | 148 | /* Create both views */ 149 | self->category_list = GTK_WIDGET(hyprmenu_category_list_new()); 150 | gtk_widget_set_hexpand(self->category_list, FALSE); 151 | gtk_widget_set_halign(self->category_list, GTK_ALIGN_CENTER); 152 | self->list_view = hyprmenu_list_view_new(); 153 | 154 | /* Set initial view based on config */ 155 | if (config->grid_hexpand) { 156 | self->current_view = self->category_list; 157 | hyprmenu_category_list_set_grid_view(HYPRMENU_CATEGORY_LIST(self->category_list), TRUE); 158 | } else { 159 | self->current_view = self->list_view; 160 | } 161 | 162 | gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scrolled_window), 163 | self->current_view); 164 | 165 | /* Add scrolled window to self */ 166 | gtk_box_append(GTK_BOX(self), self->scrolled_window); 167 | 168 | // Add key controller for Super key 169 | self->key_controller = gtk_event_controller_key_new(); 170 | g_signal_connect(self->key_controller, "key-pressed", G_CALLBACK(on_key_pressed), self); 171 | gtk_widget_add_controller(GTK_WIDGET(self), self->key_controller); 172 | 173 | // Add click gesture for click-outside 174 | GtkGesture *click_gesture = gtk_gesture_click_new(); 175 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_gesture), GDK_BUTTON_PRIMARY); 176 | g_signal_connect(click_gesture, "pressed", G_CALLBACK(on_click_outside), self); 177 | gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(click_gesture)); 178 | } 179 | 180 | static void 181 | hyprmenu_app_grid_class_init (HyprMenuAppGridClass *klass) 182 | { 183 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 184 | 185 | object_class->finalize = hyprmenu_app_grid_finalize; 186 | } 187 | 188 | HyprMenuAppGrid * 189 | hyprmenu_app_grid_new (void) 190 | { 191 | return g_object_new (HYPRMENU_TYPE_APP_GRID, NULL); 192 | } 193 | 194 | void 195 | hyprmenu_app_grid_refresh (HyprMenuAppGrid *self) 196 | { 197 | g_print("hyprmenu_app_grid_refresh: Starting\n"); 198 | 199 | if (!self) { 200 | g_warning("hyprmenu_app_grid_refresh: NULL self pointer"); 201 | return; 202 | } 203 | 204 | /* Clear existing entries */ 205 | if (self->app_entries) { 206 | for (guint i = 0; i < self->app_entries->len; i++) { 207 | HyprMenuAppEntry *entry = g_array_index(self->app_entries, HyprMenuAppEntry*, i); 208 | if (entry) { 209 | g_object_unref(entry); 210 | } 211 | } 212 | g_array_unref(self->app_entries); 213 | } 214 | 215 | /* Create a new array */ 216 | self->app_entries = g_array_new(FALSE, FALSE, sizeof(HyprMenuAppEntry *)); 217 | g_array_set_clear_func(self->app_entries, (GDestroyNotify)g_object_unref); 218 | 219 | /* Clear both views */ 220 | hyprmenu_category_list_clear(HYPRMENU_CATEGORY_LIST(self->category_list)); 221 | hyprmenu_list_view_clear(HYPRMENU_LIST_VIEW(self->list_view)); 222 | 223 | /* Get all desktop apps */ 224 | GList *all_apps = g_app_info_get_all(); 225 | 226 | for (GList *l = all_apps; l != NULL; l = l->next) { 227 | GAppInfo *app_info = G_APP_INFO(l->data); 228 | 229 | if (!G_IS_DESKTOP_APP_INFO(app_info) || !g_app_info_should_show(app_info)) { 230 | continue; 231 | } 232 | 233 | const char *app_name = g_app_info_get_name(app_info); 234 | const char *app_id = g_app_info_get_id(app_info); 235 | 236 | if (!app_name || !app_id || app_name[0] == '\0' || app_id[0] == '\0') { 237 | continue; 238 | } 239 | 240 | /* Create app entry for the array */ 241 | HyprMenuAppEntry *entry = hyprmenu_app_entry_new(G_DESKTOP_APP_INFO(app_info)); 242 | if (!entry) continue; 243 | 244 | /* Add to both views */ 245 | gboolean category_added = hyprmenu_category_list_add_app( 246 | HYPRMENU_CATEGORY_LIST(self->category_list), 247 | G_DESKTOP_APP_INFO(app_info) 248 | ); 249 | 250 | gboolean list_added = hyprmenu_list_view_add_app( 251 | HYPRMENU_LIST_VIEW(self->list_view), 252 | G_DESKTOP_APP_INFO(app_info) 253 | ); 254 | 255 | if (!category_added || !list_added) { 256 | g_warning("Failed to add app to one or both views: %s", app_id); 257 | g_object_unref(entry); 258 | continue; 259 | } 260 | 261 | /* Store in array */ 262 | g_array_append_val(self->app_entries, entry); 263 | } 264 | 265 | g_list_free(all_apps); 266 | 267 | /* Apply current filter if any */ 268 | if (self->filter_text) { 269 | hyprmenu_app_grid_filter(self, self->filter_text); 270 | } 271 | } 272 | 273 | void 274 | hyprmenu_app_grid_filter (HyprMenuAppGrid *self, const char *search_text) 275 | { 276 | g_return_if_fail(HYPRMENU_IS_APP_GRID(self)); 277 | 278 | g_free(self->filter_text); 279 | self->filter_text = g_strdup(search_text); 280 | 281 | /* Apply filter to both views */ 282 | gboolean category_filtered = hyprmenu_category_list_filter( 283 | HYPRMENU_CATEGORY_LIST(self->category_list), 284 | search_text 285 | ); 286 | 287 | gboolean list_filtered = hyprmenu_list_view_filter( 288 | HYPRMENU_LIST_VIEW(self->list_view), 289 | search_text 290 | ); 291 | 292 | if (!category_filtered || !list_filtered) { 293 | g_warning("Failed to apply filter to one or both views"); 294 | } 295 | } 296 | 297 | void 298 | hyprmenu_app_grid_toggle_view (HyprMenuAppGrid *self) 299 | { 300 | g_return_if_fail(HYPRMENU_IS_APP_GRID(self)); 301 | 302 | gboolean old_mode = config->grid_hexpand; 303 | 304 | g_print("Toggle view button clicked - changing from %s to %s\n", 305 | old_mode ? "grid" : "list", 306 | !old_mode ? "grid" : "list"); 307 | 308 | /* Update config */ 309 | config->grid_hexpand = !config->grid_hexpand; 310 | 311 | g_print("Config updated: grid_hexpand now = %s\n", config->grid_hexpand ? "true" : "false"); 312 | 313 | /* Save configuration immediately */ 314 | GError *error = NULL; 315 | gboolean saved = hyprmenu_config_save_with_error(&error); 316 | if (!saved) { 317 | g_warning("Failed to save configuration: %s", error ? error->message : "Unknown error"); 318 | g_clear_error(&error); 319 | } else { 320 | g_print("Configuration saved successfully with view mode: %s\n", 321 | config->grid_hexpand ? "grid" : "list"); 322 | 323 | // Make absolutely sure changes are written to disk 324 | fsync(0); // Use fsync on stdout instead of sync() 325 | } 326 | 327 | /* Verify the config value was actually changed */ 328 | g_print("Double-checking config->grid_hexpand = %s\n", config->grid_hexpand ? "true" : "false"); 329 | 330 | /* Update toggle button */ 331 | gtk_button_set_icon_name(GTK_BUTTON(self->toggle_button), 332 | config->grid_hexpand ? "view-list-symbolic" : "view-grid-symbolic"); 333 | gtk_widget_set_tooltip_text(self->toggle_button, 334 | config->grid_hexpand ? "Switch to List View" : "Switch to Grid View"); 335 | 336 | /* Switch views */ 337 | GtkWidget *new_view; 338 | if (config->grid_hexpand) { 339 | g_print("Setting view to grid\n"); 340 | new_view = self->category_list; 341 | hyprmenu_category_list_set_grid_view(HYPRMENU_CATEGORY_LIST(self->category_list), TRUE); 342 | } else { 343 | // Validate list view before switching 344 | if (!hyprmenu_list_view_is_valid(HYPRMENU_LIST_VIEW(self->list_view))) { 345 | g_warning("List view is not valid, falling back to grid view"); 346 | config->grid_hexpand = TRUE; 347 | new_view = self->category_list; 348 | hyprmenu_category_list_set_grid_view(HYPRMENU_CATEGORY_LIST(self->category_list), TRUE); 349 | 350 | // Save the config again if we had to revert 351 | hyprmenu_config_save(); 352 | } else { 353 | g_print("Setting view to list\n"); 354 | new_view = self->list_view; 355 | } 356 | } 357 | 358 | if (new_view != self->current_view) { 359 | g_print("Switching current view\n"); 360 | gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scrolled_window), new_view); 361 | self->current_view = new_view; 362 | 363 | /* Apply current filter to new view */ 364 | if (self->filter_text) { 365 | hyprmenu_app_grid_filter(self, self->filter_text); 366 | } 367 | } else { 368 | g_print("View didn't change (new_view == current_view)\n"); 369 | } 370 | } 371 | 372 | GtkWidget* hyprmenu_app_grid_get_toggle_button(HyprMenuAppGrid *self) { 373 | return self->toggle_button; 374 | } -------------------------------------------------------------------------------- /src/app_grid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "list_view.h" 5 | 6 | G_BEGIN_DECLS 7 | 8 | #define HYPRMENU_TYPE_APP_GRID (hyprmenu_app_grid_get_type()) 9 | G_DECLARE_FINAL_TYPE (HyprMenuAppGrid, hyprmenu_app_grid, HYPRMENU, APP_GRID, GtkBox) 10 | 11 | HyprMenuAppGrid* hyprmenu_app_grid_new (void); 12 | void hyprmenu_app_grid_refresh (HyprMenuAppGrid *self); 13 | void hyprmenu_app_grid_filter (HyprMenuAppGrid *self, const char *search_text); 14 | void hyprmenu_app_grid_toggle_view (HyprMenuAppGrid *self); 15 | GtkWidget* hyprmenu_app_grid_get_toggle_button(HyprMenuAppGrid *self); 16 | 17 | G_END_DECLS -------------------------------------------------------------------------------- /src/category_list.c: -------------------------------------------------------------------------------- 1 | #include "category_list.h" 2 | #include "config.h" 3 | #include "app_entry.h" 4 | #include 5 | 6 | // Forward declaration of the comparison function 7 | static gint compare_rows_by_app_name(GtkListBoxRow *row1, GtkListBoxRow *row2, gpointer user_data); 8 | 9 | // Helper function to extract app name from a list box row 10 | static const char* get_app_name_from_row(GtkListBoxRow *row) { 11 | if (!row) return ""; 12 | 13 | GtkWidget *child = gtk_list_box_row_get_child(row); 14 | if (!child) return ""; 15 | 16 | // The child should be a box containing a label with the app name 17 | GtkWidget *label = NULL; 18 | 19 | // Look for the label in the children 20 | GtkWidget *box = GTK_WIDGET(child); 21 | GtkWidget *first_child = gtk_widget_get_first_child(box); 22 | if (first_child) { 23 | // Skip the icon, find the name label 24 | GtkWidget *child = gtk_widget_get_next_sibling(first_child); 25 | while (child) { 26 | if (GTK_IS_LABEL(child)) { 27 | label = child; 28 | break; 29 | } 30 | child = gtk_widget_get_next_sibling(child); 31 | } 32 | } 33 | 34 | if (!GTK_IS_LABEL(label)) return ""; 35 | 36 | return gtk_label_get_text(GTK_LABEL(label)); 37 | } 38 | 39 | // Compare function for sorting by app name - using proper GtkListBoxSortFunc signature 40 | static gint compare_rows_by_app_name(GtkListBoxRow *row1, GtkListBoxRow *row2, gpointer user_data) { 41 | (void)user_data; // Unused parameter 42 | 43 | const char *name_a = get_app_name_from_row(row1); 44 | const char *name_b = get_app_name_from_row(row2); 45 | 46 | g_print("Comparing: '%s' vs '%s'\n", name_a, name_b); 47 | 48 | return g_utf8_collate(name_a, name_b); 49 | } 50 | 51 | // Function to set up alphabetical sorting for any list box 52 | static void setup_alphabetical_sorting(GtkListBox *list_box) { 53 | if (!list_box) return; 54 | gtk_list_box_set_sort_func(list_box, compare_rows_by_app_name, NULL, NULL); 55 | g_print("List box sort function set to alphabetical order\n"); 56 | } 57 | 58 | static void 59 | on_list_row_clicked(GtkGestureClick *gesture, 60 | gint n_press, 61 | double x, 62 | double y, 63 | gpointer user_data) 64 | { 65 | (void)gesture; // Silence unused parameter warning 66 | (void)n_press; // Silence unused parameter warning 67 | (void)x; // Silence unused parameter warning 68 | (void)y; // Silence unused parameter warning 69 | 70 | // Launch the app using the stored reference 71 | HyprMenuAppEntry *app_entry = HYPRMENU_APP_ENTRY(user_data); 72 | hyprmenu_app_entry_launch(app_entry); 73 | } 74 | 75 | static void 76 | on_grid_clicked(GtkGestureClick *gesture, 77 | gint n_press, 78 | double x, 79 | double y, 80 | gpointer user_data) 81 | { 82 | (void)gesture; // Silence unused parameter warning 83 | (void)n_press; // Silence unused parameter warning 84 | 85 | g_print("DEBUG: on_grid_clicked() called at x=%.1f, y=%.1f\n", x, y); 86 | 87 | HyprMenuCategoryList *cat_list = HYPRMENU_CATEGORY_LIST(user_data); 88 | if (!cat_list) { 89 | g_warning("LAUNCH ERROR: Invalid category list in on_grid_clicked"); 90 | return; 91 | } 92 | 93 | // Access the flowbox through the parent widget's children 94 | GtkWidget *grid = NULL; 95 | GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(cat_list)); 96 | while (child) { 97 | if (GTK_IS_FLOW_BOX(child)) { 98 | grid = child; 99 | break; 100 | } 101 | child = gtk_widget_get_next_sibling(child); 102 | } 103 | 104 | if (!grid) { 105 | g_warning("LAUNCH ERROR: Could not find flowbox grid in on_grid_clicked"); 106 | return; 107 | } 108 | 109 | // Get the child at the click position 110 | GtkFlowBoxChild *flowbox_child = gtk_flow_box_get_child_at_pos(GTK_FLOW_BOX(grid), x, y); 111 | 112 | if (flowbox_child) { 113 | g_print("DEBUG: Found flow box child at position\n"); 114 | 115 | // Manually activate the child 116 | GtkWidget *app_widget = gtk_flow_box_child_get_child(flowbox_child); 117 | if (app_widget && HYPRMENU_IS_APP_ENTRY(app_widget)) { 118 | HyprMenuAppEntry *entry = HYPRMENU_APP_ENTRY(app_widget); 119 | g_print("DEBUG: Manually launching app: %s\n", 120 | hyprmenu_app_entry_get_app_name(entry) ? hyprmenu_app_entry_get_app_name(entry) : "(unknown)"); 121 | hyprmenu_app_entry_launch(entry); 122 | } 123 | } else { 124 | g_print("DEBUG: No flow box child found at position\n"); 125 | } 126 | } 127 | 128 | static void 129 | on_flowbox_child_activated(GtkFlowBox *flowbox, 130 | GtkFlowBoxChild *child, 131 | gpointer user_data) 132 | { 133 | (void)flowbox; // Silence unused parameter warning 134 | (void)user_data; // Silence unused parameter warning 135 | 136 | g_print("DEBUG: on_flowbox_child_activated() called\n"); 137 | 138 | if (!child) { 139 | g_warning("LAUNCH ERROR: child is NULL in on_flowbox_child_activated"); 140 | return; 141 | } 142 | 143 | GtkWidget *app_widget = gtk_flow_box_child_get_child(child); 144 | 145 | if (!app_widget) { 146 | g_warning("LAUNCH ERROR: app_widget is NULL in on_flowbox_child_activated"); 147 | return; 148 | } 149 | 150 | if (HYPRMENU_IS_APP_ENTRY(app_widget)) { 151 | HyprMenuAppEntry *entry = HYPRMENU_APP_ENTRY(app_widget); 152 | g_print("DEBUG: Launching app entry: %s\n", 153 | hyprmenu_app_entry_get_app_name(entry) ? hyprmenu_app_entry_get_app_name(entry) : "(unknown)"); 154 | hyprmenu_app_entry_launch(entry); 155 | } else { 156 | g_warning("LAUNCH ERROR: app_widget is not an HyprMenuAppEntry"); 157 | } 158 | } 159 | 160 | struct _HyprMenuCategoryList 161 | { 162 | GtkBox parent_instance; 163 | 164 | GHashTable *category_boxes; 165 | GtkWidget *main_box; 166 | GtkWidget *all_apps_grid; // Grid for grid view mode 167 | 168 | gboolean grid_view_mode; // Whether we're in grid view mode 169 | }; 170 | 171 | G_DEFINE_TYPE (HyprMenuCategoryList, hyprmenu_category_list, GTK_TYPE_BOX) 172 | 173 | static void 174 | hyprmenu_category_list_finalize (GObject *object) 175 | { 176 | HyprMenuCategoryList *self = HYPRMENU_CATEGORY_LIST (object); 177 | 178 | if (self->category_boxes) { 179 | g_hash_table_unref (self->category_boxes); 180 | } 181 | 182 | G_OBJECT_CLASS (hyprmenu_category_list_parent_class)->finalize (object); 183 | } 184 | 185 | static gint 186 | grid_sort_func(GtkFlowBoxChild *child1, 187 | GtkFlowBoxChild *child2, 188 | gpointer user_data) 189 | { 190 | GtkWidget *widget1 = gtk_flow_box_child_get_child(child1); 191 | GtkWidget *widget2 = gtk_flow_box_child_get_child(child2); 192 | 193 | if (!HYPRMENU_IS_APP_ENTRY(widget1) || !HYPRMENU_IS_APP_ENTRY(widget2)) { 194 | return 0; 195 | } 196 | 197 | return hyprmenu_app_entry_compare_by_name(HYPRMENU_APP_ENTRY(widget1), 198 | HYPRMENU_APP_ENTRY(widget2)); 199 | } 200 | 201 | static void 202 | hyprmenu_category_list_init (HyprMenuCategoryList *self) 203 | { 204 | /* Initialize properties */ 205 | self->grid_view_mode = FALSE; 206 | 207 | /* Create main box for list view */ 208 | self->main_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); 209 | gtk_widget_add_css_class (self->main_box, "hyprmenu-category-list"); 210 | gtk_widget_set_hexpand (self->main_box, TRUE); 211 | gtk_widget_set_vexpand (self->main_box, TRUE); 212 | gtk_widget_set_halign (self->main_box, GTK_ALIGN_FILL); 213 | gtk_widget_set_margin_start(self->main_box, 12); 214 | gtk_widget_set_margin_end(self->main_box, 12); 215 | 216 | /* Add main box to self */ 217 | gtk_widget_set_parent (self->main_box, GTK_WIDGET (self)); 218 | 219 | /* Create flow box for grid view */ 220 | self->all_apps_grid = gtk_flow_box_new(); 221 | gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->all_apps_grid), GTK_SELECTION_NONE); 222 | gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->all_apps_grid), config->grid_columns); 223 | 224 | // Use config values for spacing 225 | g_object_set(self->all_apps_grid, 226 | "row-spacing", config->grid_row_spacing, 227 | "column-spacing", config->grid_column_spacing, 228 | NULL); 229 | 230 | /* Set up sorting for grid view */ 231 | gtk_flow_box_set_sort_func(GTK_FLOW_BOX(self->all_apps_grid), 232 | grid_sort_func, 233 | NULL, NULL); 234 | 235 | /* Force items to their native size rather than trying to make them homogeneous */ 236 | gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->all_apps_grid), FALSE); 237 | 238 | /* Set spacing for grid-like appearance */ 239 | gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(self->all_apps_grid), config->grid_column_spacing); 240 | gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(self->all_apps_grid), config->grid_row_spacing); 241 | 242 | /* Make sure we can activate on single click */ 243 | gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->all_apps_grid), TRUE); 244 | 245 | g_print("DEBUG: Setting up flowbox child-activated signal\n"); 246 | 247 | /* Connect child-activated signal to launch apps */ 248 | g_signal_connect(self->all_apps_grid, "child-activated", G_CALLBACK(on_flowbox_child_activated), NULL); 249 | 250 | /* Add direct click handler for flowbox items */ 251 | GtkGesture *click_gesture = gtk_gesture_click_new(); 252 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_gesture), GDK_BUTTON_PRIMARY); 253 | g_signal_connect(click_gesture, "pressed", G_CALLBACK(on_grid_clicked), self); 254 | gtk_widget_add_controller(self->all_apps_grid, GTK_EVENT_CONTROLLER(click_gesture)); 255 | 256 | gtk_widget_add_css_class(self->all_apps_grid, "hyprmenu-app-grid"); 257 | gtk_widget_set_hexpand(self->all_apps_grid, config->grid_hexpand); 258 | gtk_widget_set_vexpand(self->all_apps_grid, config->grid_vexpand); 259 | gtk_widget_set_visible(self->all_apps_grid, FALSE); // Hide by default (list view is default) 260 | 261 | // Set alignment from config 262 | GtkAlign halign = GTK_ALIGN_CENTER; 263 | GtkAlign valign = GTK_ALIGN_CENTER; 264 | if (g_strcmp0(config->grid_halign, "fill") == 0) halign = GTK_ALIGN_FILL; 265 | else if (g_strcmp0(config->grid_halign, "start") == 0) halign = GTK_ALIGN_START; 266 | else if (g_strcmp0(config->grid_halign, "end") == 0) halign = GTK_ALIGN_END; 267 | gtk_widget_set_halign(self->all_apps_grid, halign); 268 | if (g_strcmp0(config->grid_valign, "fill") == 0) valign = GTK_ALIGN_FILL; 269 | else if (g_strcmp0(config->grid_valign, "start") == 0) valign = GTK_ALIGN_START; 270 | else if (g_strcmp0(config->grid_valign, "end") == 0) valign = GTK_ALIGN_END; 271 | gtk_widget_set_valign(self->all_apps_grid, valign); 272 | 273 | // Set margins from config 274 | gtk_widget_set_margin_start(self->all_apps_grid, config->grid_margin_start); 275 | gtk_widget_set_margin_end(self->all_apps_grid, config->grid_margin_end); 276 | gtk_widget_set_margin_top(self->all_apps_grid, config->grid_margin_top); 277 | gtk_widget_set_margin_bottom(self->all_apps_grid, config->grid_margin_bottom); 278 | 279 | /* Initialize category hash table */ 280 | self->category_boxes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); 281 | } 282 | 283 | static void 284 | hyprmenu_category_list_dispose (GObject *object) 285 | { 286 | HyprMenuCategoryList *self = HYPRMENU_CATEGORY_LIST(object); 287 | 288 | // Clear all widgets 289 | hyprmenu_category_list_clear(self); 290 | 291 | // Clear hash table 292 | g_clear_pointer(&self->category_boxes, g_hash_table_unref); 293 | 294 | G_OBJECT_CLASS(hyprmenu_category_list_parent_class)->dispose(object); 295 | } 296 | 297 | static void 298 | hyprmenu_category_list_class_init (HyprMenuCategoryListClass *klass) 299 | { 300 | GObjectClass *object_class = G_OBJECT_CLASS(klass); 301 | object_class->dispose = hyprmenu_category_list_dispose; 302 | } 303 | 304 | HyprMenuCategoryList * 305 | hyprmenu_category_list_new (void) 306 | { 307 | return g_object_new (HYPRMENU_TYPE_CATEGORY_LIST, NULL); 308 | } 309 | 310 | void 311 | hyprmenu_category_list_clear (HyprMenuCategoryList *self) 312 | { 313 | if (!self) return; 314 | 315 | // First, clear all widgets from the grid view if it exists 316 | if (self->all_apps_grid) { 317 | GtkWidget *child = gtk_widget_get_first_child(self->all_apps_grid); 318 | while (child) { 319 | GtkWidget *next = gtk_widget_get_next_sibling(child); 320 | gtk_widget_unparent(child); 321 | child = next; 322 | } 323 | } 324 | 325 | // Clear all widgets from the main box 326 | if (self->main_box) { 327 | GtkWidget *category_box = gtk_widget_get_first_child(self->main_box); 328 | while (category_box) { 329 | GtkWidget *next_category = gtk_widget_get_next_sibling(category_box); 330 | // For each category box, first clear its children 331 | GtkWidget *child = gtk_widget_get_first_child(category_box); 332 | while (child) { 333 | GtkWidget *next = gtk_widget_get_next_sibling(child); 334 | gtk_widget_unparent(child); 335 | child = next; 336 | } 337 | // Then remove the category box itself 338 | gtk_widget_unparent(category_box); 339 | category_box = next_category; 340 | } 341 | } 342 | 343 | // Now clear the hash table 344 | if (self->category_boxes) { 345 | g_hash_table_remove_all(self->category_boxes); 346 | } 347 | } 348 | 349 | void 350 | hyprmenu_category_list_add_category (HyprMenuCategoryList *self, 351 | const char *category_name, 352 | GtkWidget *app_widget) 353 | { 354 | if (!self || !category_name || !app_widget) return; 355 | 356 | /* If we're in grid view mode, just add to the grid */ 357 | if (self->grid_view_mode) { 358 | /* Set the app widget to grid layout mode */ 359 | hyprmenu_app_entry_set_grid_layout(HYPRMENU_APP_ENTRY(app_widget), TRUE); 360 | 361 | /* Set size request for the app widget */ 362 | gtk_widget_set_size_request(app_widget, config->grid_item_size, config->grid_item_size); 363 | 364 | /* Set the icon size proportionally to the grid item size */ 365 | hyprmenu_app_entry_set_icon_size(HYPRMENU_APP_ENTRY(app_widget), config->grid_item_size * 0.6); 366 | 367 | /* Add to flow box */ 368 | gtk_flow_box_append(GTK_FLOW_BOX(self->all_apps_grid), app_widget); 369 | return; 370 | } 371 | 372 | /* LIST VIEW IMPLEMENTATION */ 373 | HyprMenuAppEntry *entry = HYPRMENU_APP_ENTRY(app_widget); 374 | 375 | /* Find existing category box */ 376 | GtkWidget *category_box = g_hash_table_lookup(self->category_boxes, category_name); 377 | 378 | /* Create new category box if not found */ 379 | if (!category_box) { 380 | category_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8); 381 | gtk_widget_add_css_class (category_box, "hyprmenu-category"); 382 | gtk_widget_set_hexpand (category_box, TRUE); 383 | 384 | /* Add category title */ 385 | GtkWidget *title = gtk_label_new (category_name); 386 | gtk_widget_add_css_class (title, "hyprmenu-category-title"); 387 | gtk_label_set_xalign (GTK_LABEL (title), 0); 388 | gtk_box_append (GTK_BOX (category_box), title); 389 | 390 | /* Create a list box for this category to enable sorting */ 391 | GtkWidget *list_box = gtk_list_box_new(); 392 | gtk_list_box_set_selection_mode(GTK_LIST_BOX(list_box), GTK_SELECTION_NONE); 393 | gtk_widget_add_css_class(list_box, "hyprmenu-category-list-box"); 394 | gtk_box_append(GTK_BOX(category_box), list_box); 395 | 396 | /* Set up alphabetical sorting */ 397 | setup_alphabetical_sorting(GTK_LIST_BOX(list_box)); 398 | 399 | /* Store category name */ 400 | g_object_set_data_full (G_OBJECT (category_box), "category-name", 401 | g_strdup (category_name), g_free); 402 | g_object_set_data (G_OBJECT (category_box), "list-box", list_box); 403 | 404 | /* Add to main box */ 405 | gtk_box_append (GTK_BOX (self->main_box), category_box); 406 | 407 | /* Store in hash table */ 408 | g_hash_table_insert (self->category_boxes, g_strdup(category_name), category_box); 409 | } 410 | 411 | /* Get the list box from the category box */ 412 | GtkWidget *list_box = g_object_get_data(G_OBJECT(category_box), "list-box"); 413 | if (!list_box) { 414 | g_warning("Category box doesn't have a list box"); 415 | return; 416 | } 417 | 418 | /* Create a custom row for this app */ 419 | GtkWidget *row_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); 420 | gtk_widget_add_css_class(row_content, "hyprmenu-list-row-content"); 421 | gtk_widget_set_hexpand(row_content, TRUE); 422 | 423 | /* Add app icon */ 424 | GIcon *icon = hyprmenu_app_entry_get_icon(entry); 425 | if (icon) { 426 | GtkWidget *image = gtk_image_new_from_gicon(icon); 427 | gtk_image_set_pixel_size(GTK_IMAGE(image), 24); 428 | gtk_box_append(GTK_BOX(row_content), image); 429 | } 430 | 431 | /* Add app name */ 432 | const char *app_name = hyprmenu_app_entry_get_app_name(entry); 433 | GtkWidget *name_label = gtk_label_new(app_name); 434 | gtk_label_set_ellipsize(GTK_LABEL(name_label), PANGO_ELLIPSIZE_END); 435 | gtk_widget_set_hexpand(name_label, TRUE); 436 | gtk_label_set_xalign(GTK_LABEL(name_label), 0); 437 | gtk_box_append(GTK_BOX(row_content), name_label); 438 | 439 | /* Create a new list box row and add the content */ 440 | GtkWidget *list_row = gtk_list_box_row_new(); 441 | gtk_widget_add_css_class(list_row, "hyprmenu-list-row"); 442 | gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(list_row), row_content); 443 | 444 | /* Store the app widget reference in the row */ 445 | g_object_set_data_full(G_OBJECT(list_row), "app-widget", g_object_ref(app_widget), g_object_unref); 446 | 447 | /* Add click handler */ 448 | GtkGesture *click_gesture = gtk_gesture_click_new(); 449 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_gesture), GDK_BUTTON_PRIMARY); 450 | g_signal_connect(click_gesture, "pressed", G_CALLBACK(on_list_row_clicked), app_widget); 451 | gtk_widget_add_controller(list_row, GTK_EVENT_CONTROLLER(click_gesture)); 452 | 453 | /* Add the row to the list box */ 454 | gtk_list_box_append(GTK_LIST_BOX(list_box), list_row); 455 | } 456 | 457 | void 458 | hyprmenu_category_list_set_grid_view (HyprMenuCategoryList *self, gboolean use_grid_view) 459 | { 460 | if (!self) return; 461 | 462 | /* Skip if already in the requested mode */ 463 | if (self->grid_view_mode == use_grid_view) return; 464 | 465 | g_print("hyprmenu_category_list_set_grid_view: Changing from %s to %s\n", 466 | self->grid_view_mode ? "grid" : "list", 467 | use_grid_view ? "grid" : "list"); 468 | 469 | /* Update the mode */ 470 | self->grid_view_mode = use_grid_view; 471 | 472 | /* Update the flow box column count (in case config changed) */ 473 | if (use_grid_view) { 474 | gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->all_apps_grid), config->grid_columns); 475 | gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(self->all_apps_grid), config->grid_columns); 476 | gtk_widget_set_halign(self->all_apps_grid, GTK_ALIGN_CENTER); 477 | 478 | // Set width to ensure consistent alignment 479 | gtk_widget_set_size_request(self->all_apps_grid, -1, -1); 480 | } 481 | 482 | /* Show/hide the appropriate view */ 483 | if (use_grid_view) { 484 | /* Grid view - collect all app entries from list rows and move them to the grid */ 485 | GList *app_widgets = NULL; 486 | 487 | /* First, collect all app widgets from category rows */ 488 | if (self->category_boxes) { 489 | g_print("Moving from list view to grid view\n"); 490 | 491 | GHashTableIter iter; 492 | gpointer key, value; 493 | 494 | g_hash_table_iter_init(&iter, self->category_boxes); 495 | while (g_hash_table_iter_next(&iter, &key, &value)) { 496 | GtkWidget *category_box = GTK_WIDGET(value); 497 | if (category_box) { 498 | // Skip first child (category title) 499 | GtkWidget *child = gtk_widget_get_first_child(category_box); 500 | if (child) child = gtk_widget_get_next_sibling(child); 501 | 502 | while (child) { 503 | GtkWidget *next = gtk_widget_get_next_sibling(child); 504 | 505 | // Get the app_widget from the list row 506 | GtkWidget *app_widget = g_object_get_data(G_OBJECT(child), "app-widget"); 507 | if (app_widget) { 508 | // Ensure we ref the widget before adding it to our list to prevent it from being freed 509 | g_object_ref(app_widget); 510 | 511 | // Prepare for grid view 512 | hyprmenu_app_entry_set_grid_layout(HYPRMENU_APP_ENTRY(app_widget), TRUE); 513 | app_widgets = g_list_append(app_widgets, app_widget); 514 | } 515 | 516 | // Remove the row from category 517 | gtk_box_remove(GTK_BOX(category_box), child); 518 | child = next; 519 | } 520 | } 521 | } 522 | } 523 | 524 | /* Add all app entries to the grid */ 525 | for (GList *l = app_widgets; l != NULL; l = l->next) { 526 | GtkWidget *app_widget = GTK_WIDGET(l->data); 527 | gtk_widget_set_size_request(app_widget, config->grid_item_size, config->grid_item_size); 528 | hyprmenu_app_entry_set_grid_layout(HYPRMENU_APP_ENTRY(app_widget), TRUE); 529 | hyprmenu_app_entry_set_icon_size(HYPRMENU_APP_ENTRY(app_widget), config->grid_item_size * 0.6); 530 | gtk_flow_box_append(GTK_FLOW_BOX(self->all_apps_grid), app_widget); 531 | g_object_unref(GTK_WIDGET(l->data)); // Balance the ref from above 532 | } 533 | g_list_free(app_widgets); 534 | 535 | /* Invalidate sort to ensure proper ordering */ 536 | gtk_flow_box_invalidate_sort(GTK_FLOW_BOX(self->all_apps_grid)); 537 | 538 | /* Set visibility */ 539 | if (!gtk_widget_get_parent(self->all_apps_grid)) { 540 | gtk_widget_set_parent(self->all_apps_grid, GTK_WIDGET(self)); 541 | } 542 | gtk_widget_set_visible(self->main_box, FALSE); 543 | gtk_widget_set_visible(self->all_apps_grid, TRUE); 544 | } else { 545 | /* List view - move app entries from grid back to categories */ 546 | g_print("Moving from grid view to list view\n"); 547 | 548 | // Ensure we have a valid hash table 549 | if (!self->category_boxes) { 550 | self->category_boxes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); 551 | } 552 | 553 | /* Collect all app entries from the grid */ 554 | GHashTable *category_app_entries = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, 555 | (GDestroyNotify)g_slist_free); 556 | 557 | // Safe way to iterate through children while removing them 558 | GList *entries_to_process = NULL; 559 | GtkWidget *child = gtk_widget_get_first_child(self->all_apps_grid); 560 | while (child) { 561 | GtkWidget *next = gtk_widget_get_next_sibling(child); 562 | 563 | if (HYPRMENU_IS_APP_ENTRY(gtk_flow_box_child_get_child(GTK_FLOW_BOX_CHILD(child)))) { 564 | // Ref the entry while we store it to prevent premature deletion 565 | GtkWidget *entry_widget = gtk_flow_box_child_get_child(GTK_FLOW_BOX_CHILD(child)); 566 | g_object_ref(entry_widget); 567 | entries_to_process = g_list_append(entries_to_process, entry_widget); 568 | } 569 | 570 | child = next; 571 | } 572 | 573 | // Process the entries outside the loop to avoid reference issues 574 | for (GList *l = entries_to_process; l != NULL; l = l->next) { 575 | HyprMenuAppEntry *entry = HYPRMENU_APP_ENTRY(l->data); 576 | 577 | /* Get the first category */ 578 | const char **categories = hyprmenu_app_entry_get_categories(entry); 579 | const char *category = categories && categories[0] ? categories[0] : "Other"; 580 | 581 | /* Get or create list for this category */ 582 | GSList *category_entries = g_hash_table_lookup(category_app_entries, category); 583 | category_entries = g_slist_append(category_entries, entry); 584 | g_hash_table_insert(category_app_entries, g_strdup(category), category_entries); 585 | 586 | /* No need to remove from grid at this stage */ 587 | } 588 | 589 | // Now remove all entries from grid 590 | child = gtk_widget_get_first_child(self->all_apps_grid); 591 | while (child) { 592 | GtkWidget *next = gtk_widget_get_next_sibling(child); 593 | gtk_flow_box_remove(GTK_FLOW_BOX(self->all_apps_grid), child); 594 | child = next; 595 | } 596 | 597 | /* Create category lists and add app entries */ 598 | GList *categories = g_hash_table_get_keys(category_app_entries); 599 | categories = g_list_sort(categories, (GCompareFunc)g_ascii_strcasecmp); 600 | 601 | for (GList *l = categories; l != NULL; l = l->next) { 602 | const char *category_name = l->data; 603 | GSList *entries = g_hash_table_lookup(category_app_entries, category_name); 604 | 605 | // Sort entries by name 606 | entries = g_slist_sort(entries, (GCompareFunc)hyprmenu_app_entry_compare_by_name); 607 | 608 | // Add each entry to its category 609 | for (GSList *entry_item = entries; entry_item != NULL; entry_item = entry_item->next) { 610 | HyprMenuAppEntry *entry = HYPRMENU_APP_ENTRY(entry_item->data); 611 | 612 | // Set list layout before adding 613 | hyprmenu_app_entry_set_grid_layout(entry, FALSE); 614 | 615 | // Add to category 616 | hyprmenu_category_list_add_category(self, category_name, GTK_WIDGET(entry)); 617 | 618 | // Now we can unref the entry since it's now owned by its new parent 619 | g_object_unref(entry); 620 | } 621 | } 622 | 623 | g_list_free(categories); 624 | g_hash_table_destroy(category_app_entries); 625 | g_list_free(entries_to_process); 626 | 627 | /* Set visibility */ 628 | gtk_widget_set_visible(self->all_apps_grid, FALSE); 629 | gtk_widget_set_visible(self->main_box, TRUE); 630 | } 631 | } 632 | 633 | gboolean 634 | hyprmenu_category_list_add_app (HyprMenuCategoryList *self, 635 | GDesktopAppInfo *app_info) 636 | { 637 | g_return_val_if_fail(HYPRMENU_IS_CATEGORY_LIST(self), FALSE); 638 | g_return_val_if_fail(G_IS_DESKTOP_APP_INFO(app_info), FALSE); 639 | 640 | HyprMenuAppEntry *entry = hyprmenu_app_entry_new(app_info); 641 | if (!entry) return FALSE; 642 | 643 | const char **categories = hyprmenu_app_entry_get_categories(entry); 644 | const char *category = categories && categories[0] ? categories[0] : "Other"; 645 | 646 | hyprmenu_category_list_add_category(self, category, GTK_WIDGET(entry)); 647 | return TRUE; 648 | } 649 | 650 | gboolean 651 | hyprmenu_category_list_filter (HyprMenuCategoryList *self, 652 | const char *search_text) 653 | { 654 | g_return_val_if_fail(HYPRMENU_IS_CATEGORY_LIST(self), FALSE); 655 | 656 | g_print("Filtering with search text: '%s'\n", search_text ? search_text : "(null)"); 657 | 658 | if (self->grid_view_mode) { 659 | // Grid view filtering 660 | GtkWidget *child = gtk_widget_get_first_child(self->all_apps_grid); 661 | while (child) { 662 | GtkWidget *next = gtk_widget_get_next_sibling(child); 663 | if (GTK_IS_FLOW_BOX_CHILD(child)) { 664 | GtkWidget *app = gtk_flow_box_child_get_child(GTK_FLOW_BOX_CHILD(child)); 665 | if (HYPRMENU_IS_APP_ENTRY(app)) { 666 | gboolean visible = TRUE; 667 | if (search_text && *search_text) { 668 | const char *app_name = hyprmenu_app_entry_get_app_name(HYPRMENU_APP_ENTRY(app)); 669 | char *name_lower = g_utf8_strdown(app_name, -1); 670 | char *search_lower = g_utf8_strdown(search_text, -1); 671 | 672 | visible = (strstr(name_lower, search_lower) != NULL); 673 | 674 | g_free(name_lower); 675 | g_free(search_lower); 676 | } 677 | 678 | gtk_widget_set_visible(child, visible); 679 | } 680 | } 681 | child = next; 682 | } 683 | 684 | return TRUE; 685 | } 686 | 687 | // List view filtering 688 | GHashTableIter iter; 689 | gpointer key, value; 690 | 691 | g_hash_table_iter_init(&iter, self->category_boxes); 692 | while (g_hash_table_iter_next(&iter, &key, &value)) { 693 | GtkWidget *category_box = GTK_WIDGET(value); 694 | GtkWidget *list_box = g_object_get_data(G_OBJECT(category_box), "list-box"); 695 | gboolean has_visible_items = FALSE; 696 | 697 | if (GTK_IS_LIST_BOX(list_box)) { 698 | GtkWidget *child = gtk_widget_get_first_child(list_box); 699 | while (child) { 700 | GtkWidget *next = gtk_widget_get_next_sibling(child); 701 | if (GTK_IS_LIST_BOX_ROW(child)) { 702 | // Get the app widget from the row's data 703 | GtkWidget *app = g_object_get_data(G_OBJECT(child), "app-widget"); 704 | if (HYPRMENU_IS_APP_ENTRY(app)) { 705 | gboolean visible = TRUE; 706 | if (search_text && *search_text) { 707 | const char *app_name = hyprmenu_app_entry_get_app_name(HYPRMENU_APP_ENTRY(app)); 708 | char *name_lower = g_utf8_strdown(app_name, -1); 709 | char *search_lower = g_utf8_strdown(search_text, -1); 710 | 711 | visible = (strstr(name_lower, search_lower) != NULL); 712 | 713 | g_free(name_lower); 714 | g_free(search_lower); 715 | } 716 | 717 | gtk_widget_set_visible(child, visible); 718 | if (visible) has_visible_items = TRUE; 719 | } 720 | } 721 | child = next; 722 | } 723 | } 724 | 725 | gtk_widget_set_visible(category_box, has_visible_items); 726 | } 727 | 728 | return TRUE; 729 | } -------------------------------------------------------------------------------- /src/category_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | G_BEGIN_DECLS 7 | 8 | #define HYPRMENU_TYPE_CATEGORY_LIST (hyprmenu_category_list_get_type()) 9 | G_DECLARE_FINAL_TYPE (HyprMenuCategoryList, hyprmenu_category_list, HYPRMENU, CATEGORY_LIST, GtkBox) 10 | 11 | HyprMenuCategoryList* hyprmenu_category_list_new (void); 12 | void hyprmenu_category_list_add_category (HyprMenuCategoryList *self, const char *category_name, GtkWidget *app_widget); 13 | GtkWidget* hyprmenu_category_list_get_category_content (HyprMenuCategoryList *self, const char *category_name); 14 | void hyprmenu_category_list_clear (HyprMenuCategoryList *self); 15 | void hyprmenu_category_list_set_grid_view (HyprMenuCategoryList *self, gboolean use_grid_view); 16 | 17 | /* New functions */ 18 | gboolean hyprmenu_category_list_add_app (HyprMenuCategoryList *self, GDesktopAppInfo *app_info); 19 | gboolean hyprmenu_category_list_filter (HyprMenuCategoryList *self, const char *search_text); 20 | 21 | G_END_DECLS -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /* Menu position enum */ 7 | typedef enum { 8 | POSITION_TOP_LEFT = 0, 9 | POSITION_TOP_CENTER, 10 | POSITION_TOP_RIGHT, 11 | POSITION_BOTTOM_LEFT, 12 | POSITION_BOTTOM_CENTER, 13 | POSITION_BOTTOM_RIGHT, 14 | POSITION_CENTER 15 | } HyprMenuPosition; 16 | 17 | typedef struct _HyprMenuConfig { 18 | // Window layout 19 | int window_width; 20 | int window_height; 21 | int top_margin; 22 | int left_margin; 23 | gboolean center_window; 24 | HyprMenuPosition menu_position; // Position of the menu 25 | int bottom_offset; // Offset from bottom for dock/panel 26 | int top_offset; // Offset from top for panel 27 | 28 | // File paths 29 | char *config_dir; 30 | char *config_file; 31 | char *css_file; 32 | 33 | // Window options 34 | int window_corner_radius; 35 | int window_border_width; 36 | char *window_border_color; 37 | double window_background_opacity; 38 | double window_background_blur; 39 | char *window_background_color; 40 | int window_padding; 41 | char *window_halign; 42 | char *window_valign; 43 | char *window_shadow_color; 44 | int window_shadow_radius; 45 | 46 | // Inner border (main box) 47 | int inner_border_width; 48 | char *inner_border_color; 49 | int inner_border_radius; 50 | 51 | // Outer border (window) 52 | int outer_border_width; 53 | char *outer_border_color; 54 | int outer_border_radius; 55 | 56 | // Grid options 57 | int grid_margin_start; 58 | int grid_margin_end; 59 | int grid_margin_top; 60 | int grid_margin_bottom; 61 | int grid_row_spacing; 62 | int grid_column_spacing; 63 | char *grid_halign; 64 | char *grid_valign; 65 | gboolean grid_hexpand; 66 | gboolean grid_vexpand; 67 | int grid_columns; 68 | int grid_item_size; 69 | int grid_item_corner_radius; 70 | int grid_item_border_width; 71 | char *grid_item_border_color; 72 | char *grid_item_background_color; 73 | double grid_opacity; 74 | double grid_item_opacity; 75 | 76 | // List options 77 | int list_item_size; 78 | int list_item_corner_radius; 79 | int list_item_border_width; 80 | char *list_item_border_color; 81 | char *list_item_background_color; 82 | int list_row_spacing; 83 | char *list_halign; 84 | char *list_valign; 85 | gboolean list_hexpand; 86 | gboolean list_vexpand; 87 | int list_margin_start; 88 | int list_margin_end; 89 | int list_margin_top; 90 | int list_margin_bottom; 91 | double list_opacity; 92 | double list_item_opacity; 93 | 94 | // AppEntry options 95 | int app_icon_size; 96 | int app_icon_corner_radius; 97 | char *app_icon_background_color; 98 | int app_name_font_size; 99 | char *app_name_color; 100 | int app_desc_font_size; 101 | char *app_desc_color; 102 | int app_entry_padding; 103 | char *app_entry_hover_color; 104 | char *app_entry_active_color; 105 | double app_entry_opacity; 106 | double app_icon_opacity; 107 | double app_name_opacity; 108 | double app_desc_opacity; 109 | 110 | // Category options 111 | char *category_background_color; 112 | double category_background_opacity; 113 | int category_corner_radius; 114 | char *category_text_color; 115 | int category_font_size; 116 | char *category_font_family; 117 | int category_padding; 118 | gboolean category_show_separators; 119 | char *category_separator_color; 120 | double category_opacity; 121 | double category_title_opacity; 122 | 123 | // Search options 124 | char *search_background_color; 125 | double search_background_opacity; 126 | int search_corner_radius; 127 | char *search_text_color; 128 | int search_font_size; 129 | char *search_font_family; 130 | int search_padding; 131 | int search_min_height; 132 | int search_left_padding; 133 | int search_length; 134 | char *search_placeholder_text; 135 | int search_icon_size; 136 | char *search_icon_color; 137 | char *search_focus_border_color; 138 | char *search_focus_shadow_color; 139 | double search_opacity; 140 | double search_text_opacity; 141 | double search_icon_opacity; 142 | 143 | // SystemButton options 144 | char *system_button_background_color; 145 | char *system_button_icon_color; 146 | char *system_button_hover_color; 147 | char *system_button_active_color; 148 | int system_button_corner_radius; 149 | int system_button_size; 150 | int system_button_spacing; 151 | double system_button_opacity; 152 | double system_button_icon_opacity; 153 | 154 | // Behavior options 155 | gboolean close_on_click_outside; 156 | gboolean close_on_super_key; 157 | gboolean close_on_app_launch; 158 | gboolean focus_search_on_open; 159 | gboolean close_on_escape; 160 | gboolean close_on_focus_out; 161 | gboolean show_categories; 162 | gboolean show_descriptions; 163 | gboolean show_icons; 164 | gboolean show_search; 165 | gboolean show_scrollbar; 166 | gboolean show_border; 167 | gboolean show_shadow; 168 | gboolean blur_background; 169 | int blur_strength; 170 | double opacity; 171 | int max_recent_apps; // Maximum number of recent apps to show 172 | 173 | // Hyprland-specific settings 174 | gboolean use_hyprland_corner_fix; 175 | int hyprland_corner_radius; 176 | } HyprMenuConfig; 177 | 178 | // Global config instance 179 | extern HyprMenuConfig *config; 180 | 181 | // Function declarations 182 | gboolean hyprmenu_config_init(); 183 | void hyprmenu_config_free(); 184 | gboolean hyprmenu_config_load(); 185 | gboolean hyprmenu_config_save(); 186 | gboolean hyprmenu_config_save_with_error(GError **error); 187 | void hyprmenu_config_apply_css(); 188 | 189 | // Position utility functions 190 | const gchar* hyprmenu_position_to_string(HyprMenuPosition position); 191 | HyprMenuPosition hyprmenu_position_from_string(const gchar *position_str); 192 | 193 | // Pywal color struct 194 | #define PYWAL_COLOR_COUNT 16 195 | 196 | typedef struct { 197 | char *colors[PYWAL_COLOR_COUNT]; // colors[0] = color0, ... 198 | char *special_background; 199 | char *special_foreground; 200 | char *special_cursor; 201 | } PywalColors; 202 | 203 | // Global pywal color instance 204 | extern PywalColors pywal_colors; -------------------------------------------------------------------------------- /src/list_view.c: -------------------------------------------------------------------------------- 1 | #include "list_view.h" 2 | #include "config.h" 3 | #include 4 | 5 | struct _HyprMenuListView { 6 | GtkWidget parent_instance; 7 | 8 | // Main containers 9 | GtkWidget* scroll_window; // Scrolled window container 10 | GtkWidget* main_box; // Main vertical box 11 | GtkWidget* categories_box; // Box containing category boxes 12 | 13 | // Data storage 14 | GHashTable* category_boxes; // Maps category names to their GtkBox widgets 15 | GHashTable* app_entries; // Maps app IDs to AppEntry structs 16 | GList* categories; // Ordered list of category names 17 | 18 | // Settings 19 | gboolean show_descriptions; 20 | char* filter_text; 21 | 22 | // State tracking 23 | gboolean initialized; 24 | guint visible_apps_count; 25 | GError* last_error; 26 | }; 27 | 28 | typedef struct { 29 | char* id; // Application ID 30 | char* name; // Display name 31 | char* description; // Description or comment 32 | char* primary_category; // Main category 33 | GDesktopAppInfo* app_info; // Application info 34 | GtkWidget* row; // The row widget containing the app entry 35 | GtkWidget* icon; // Icon widget 36 | GtkWidget* label_box; // Box containing name and description labels 37 | GtkWidget* name_label; // Name label 38 | GtkWidget* desc_label; // Description label (may be NULL) 39 | HyprMenuListView* view; // Back reference to containing view 40 | gboolean visible; // Visibility state 41 | } AppEntry; 42 | 43 | G_DEFINE_TYPE(HyprMenuListView, hyprmenu_list_view, GTK_TYPE_WIDGET) 44 | 45 | static void 46 | app_entry_free(AppEntry* entry) 47 | { 48 | if (!entry) return; 49 | 50 | LIST_VIEW_DEBUG("Freeing app entry: %s", entry->name ? entry->name : "(null)"); 51 | 52 | g_free(entry->id); 53 | g_free(entry->name); 54 | g_free(entry->description); 55 | g_free(entry->primary_category); 56 | 57 | if (entry->app_info) { 58 | g_object_unref(entry->app_info); 59 | } 60 | 61 | if (entry->row) { 62 | gtk_widget_unparent(entry->row); 63 | } 64 | 65 | g_free(entry); 66 | } 67 | 68 | static void 69 | on_app_activated(GtkGestureClick* gesture, 70 | gint n_press, 71 | gdouble x, 72 | gdouble y, 73 | gpointer user_data) 74 | { 75 | AppEntry* entry = (AppEntry*)user_data; 76 | 77 | if (!entry || !entry->app_info) { 78 | LIST_VIEW_WARNING("App activation failed: Invalid entry or app_info"); 79 | return; 80 | } 81 | 82 | LIST_VIEW_DEBUG("Launching app: %s", entry->name); 83 | 84 | GError* error = NULL; 85 | if (!g_app_info_launch(G_APP_INFO(entry->app_info), NULL, NULL, &error)) { 86 | LIST_VIEW_ERROR("Failed to launch application %s: %s", 87 | entry->name, error ? error->message : "Unknown error"); 88 | if (error) g_error_free(error); 89 | return; 90 | } 91 | 92 | LIST_VIEW_DEBUG("Successfully launched app: %s", entry->name); 93 | 94 | // Close the menu window if configured to do so 95 | if (config->close_on_app_launch) { 96 | GtkRoot *root = gtk_widget_get_root(entry->row); 97 | if (GTK_IS_WINDOW(root)) { 98 | gtk_window_close(GTK_WINDOW(root)); 99 | } 100 | } 101 | } 102 | 103 | static GtkWidget* 104 | create_category_label(const char* category) 105 | { 106 | g_return_val_if_fail(category != NULL, NULL); 107 | 108 | LIST_VIEW_DEBUG("Creating category label: %s", category); 109 | 110 | GtkWidget* label = gtk_label_new(NULL); 111 | char* markup = g_markup_printf_escaped("%s", category); 112 | gtk_label_set_markup(GTK_LABEL(label), markup); 113 | g_free(markup); 114 | 115 | gtk_widget_add_css_class(label, "hyprmenu-category-title"); 116 | gtk_label_set_xalign(GTK_LABEL(label), 0); 117 | gtk_widget_set_margin_start(label, config->category_padding); 118 | gtk_widget_set_margin_top(label, 12); 119 | gtk_widget_set_margin_bottom(label, 6); 120 | 121 | return label; 122 | } 123 | 124 | static GtkWidget* 125 | get_or_create_category_box(HyprMenuListView* self, const char* category) 126 | { 127 | g_return_val_if_fail(HYPRMENU_IS_LIST_VIEW(self), NULL); 128 | g_return_val_if_fail(category != NULL, NULL); 129 | 130 | GtkWidget* category_box = g_hash_table_lookup(self->category_boxes, category); 131 | if (category_box) { 132 | LIST_VIEW_DEBUG("Found existing category box for: %s", category); 133 | return category_box; 134 | } 135 | 136 | LIST_VIEW_DEBUG("Creating new category box for: %s", category); 137 | 138 | // Create new category box 139 | category_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, config->category_padding); 140 | gtk_widget_add_css_class(category_box, "hyprmenu-category"); 141 | 142 | // Add category label 143 | GtkWidget* label = create_category_label(category); 144 | if (!label) { 145 | LIST_VIEW_ERROR("Failed to create category label for: %s", category); 146 | return NULL; 147 | } 148 | gtk_box_append(GTK_BOX(category_box), label); 149 | 150 | // Add to main container and store 151 | gtk_box_append(GTK_BOX(self->categories_box), category_box); 152 | g_hash_table_insert(self->category_boxes, g_strdup(category), category_box); 153 | 154 | // Add to ordered category list 155 | self->categories = g_list_insert_sorted(self->categories, g_strdup(category), 156 | (GCompareFunc)g_ascii_strcasecmp); 157 | 158 | return category_box; 159 | } 160 | 161 | static AppEntry* 162 | create_app_entry(HyprMenuListView* self, GDesktopAppInfo* app_info) 163 | { 164 | g_return_val_if_fail(HYPRMENU_IS_LIST_VIEW(self), NULL); 165 | g_return_val_if_fail(G_IS_DESKTOP_APP_INFO(app_info), NULL); 166 | 167 | const char* app_id = g_app_info_get_id(G_APP_INFO(app_info)); 168 | const char* app_name = g_app_info_get_display_name(G_APP_INFO(app_info)); 169 | 170 | LIST_VIEW_DEBUG("Creating app entry - ID: %s, Name: %s", 171 | app_id ? app_id : "(null)", 172 | app_name ? app_name : "(null)"); 173 | 174 | AppEntry* entry = g_new0(AppEntry, 1); 175 | entry->view = self; 176 | entry->app_info = g_object_ref(app_info); 177 | entry->id = g_strdup(app_id); 178 | entry->name = g_strdup(app_name); 179 | entry->description = g_strdup(g_app_info_get_description(G_APP_INFO(app_info))); 180 | entry->visible = TRUE; 181 | 182 | // Get primary category 183 | const char* categories = g_desktop_app_info_get_categories(app_info); 184 | if (categories) { 185 | char** cats = g_strsplit(categories, ";", -1); 186 | entry->primary_category = g_strdup(cats[0] ? cats[0] : "Other"); 187 | g_strfreev(cats); 188 | } else { 189 | entry->primary_category = g_strdup("Other"); 190 | } 191 | 192 | LIST_VIEW_DEBUG("App category: %s", entry->primary_category); 193 | 194 | // Create row widget 195 | entry->row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); 196 | gtk_widget_add_css_class(entry->row, "hyprmenu-app-entry"); 197 | gtk_widget_set_margin_start(entry->row, 4); 198 | gtk_widget_set_margin_end(entry->row, 4); 199 | gtk_widget_set_margin_top(entry->row, 2); 200 | gtk_widget_set_margin_bottom(entry->row, 2); 201 | // Set the row height from config 202 | gtk_widget_set_size_request(entry->row, -1, config->list_item_size); 203 | 204 | // Create icon 205 | entry->icon = gtk_image_new(); 206 | GIcon* icon = g_app_info_get_icon(G_APP_INFO(app_info)); 207 | if (icon) { 208 | gtk_image_set_from_gicon(GTK_IMAGE(entry->icon), icon); 209 | } else { 210 | gtk_image_set_from_icon_name(GTK_IMAGE(entry->icon), "application-x-executable"); 211 | } 212 | // Set icon size proportional to list_item_size 213 | int icon_size = config->list_item_size * 0.75; 214 | gtk_image_set_pixel_size(GTK_IMAGE(entry->icon), icon_size); 215 | gtk_widget_set_margin_start(entry->icon, config->app_entry_padding); 216 | gtk_widget_add_css_class(entry->icon, "hyprmenu-app-icon"); 217 | 218 | // Create label box 219 | entry->label_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); 220 | gtk_widget_set_hexpand(entry->label_box, TRUE); 221 | gtk_widget_set_valign(entry->label_box, GTK_ALIGN_CENTER); 222 | gtk_widget_set_margin_start(entry->label_box, config->app_entry_padding); 223 | gtk_widget_set_margin_end(entry->label_box, config->app_entry_padding); 224 | 225 | // Calculate scaling for font sizes 226 | double scale = (double)config->list_item_size / 48.0; 227 | int name_font_size = (int)(config->app_name_font_size * scale); 228 | int desc_font_size = (int)(config->app_desc_font_size * scale); 229 | if (desc_font_size < 8) desc_font_size = 8; 230 | if (name_font_size < 8) name_font_size = 8; 231 | 232 | // Create name label with scaled font size 233 | char *name_markup = g_markup_printf_escaped("%s", name_font_size * PANGO_SCALE, entry->name); 234 | entry->name_label = gtk_label_new(NULL); 235 | gtk_label_set_markup(GTK_LABEL(entry->name_label), name_markup); 236 | g_free(name_markup); 237 | gtk_label_set_xalign(GTK_LABEL(entry->name_label), 0); 238 | gtk_widget_set_valign(entry->name_label, GTK_ALIGN_CENTER); 239 | gtk_widget_add_css_class(entry->name_label, "app-name"); 240 | 241 | // Create description label if needed, with scaled font size and centered vertically 242 | if (self->show_descriptions && entry->description) { 243 | char *desc_markup = g_markup_printf_escaped("%s", desc_font_size * PANGO_SCALE, entry->description); 244 | entry->desc_label = gtk_label_new(NULL); 245 | gtk_label_set_markup(GTK_LABEL(entry->desc_label), desc_markup); 246 | g_free(desc_markup); 247 | gtk_label_set_xalign(GTK_LABEL(entry->desc_label), 0); 248 | gtk_widget_set_valign(entry->desc_label, GTK_ALIGN_CENTER); 249 | gtk_label_set_wrap(GTK_LABEL(entry->desc_label), TRUE); 250 | gtk_widget_add_css_class(entry->desc_label, "app-description"); 251 | } 252 | 253 | // Add widgets to containers 254 | gtk_box_append(GTK_BOX(entry->label_box), entry->name_label); 255 | if (entry->desc_label) { 256 | gtk_box_append(GTK_BOX(entry->label_box), entry->desc_label); 257 | } 258 | 259 | gtk_box_append(GTK_BOX(entry->row), entry->icon); 260 | gtk_box_append(GTK_BOX(entry->row), entry->label_box); 261 | 262 | // Add click handler 263 | GtkGesture* gesture = gtk_gesture_click_new(); 264 | g_signal_connect(gesture, "pressed", G_CALLBACK(on_app_activated), entry); 265 | gtk_widget_add_controller(entry->row, GTK_EVENT_CONTROLLER(gesture)); 266 | 267 | return entry; 268 | } 269 | 270 | static void 271 | update_entry_visibility(gpointer key, gpointer value, gpointer user_data) 272 | { 273 | AppEntry* entry = (AppEntry*)value; 274 | HyprMenuListView* self = entry->view; 275 | 276 | gboolean visible = TRUE; 277 | 278 | if (self->filter_text && *self->filter_text) { 279 | char* name_down = g_utf8_strdown(entry->name, -1); 280 | char* filter_down = g_utf8_strdown(self->filter_text, -1); 281 | 282 | visible = strstr(name_down, filter_down) != NULL; 283 | 284 | if (!visible && entry->description) { 285 | char* desc_down = g_utf8_strdown(entry->description, -1); 286 | visible = strstr(desc_down, filter_down) != NULL; 287 | g_free(desc_down); 288 | } 289 | 290 | g_free(name_down); 291 | g_free(filter_down); 292 | } 293 | 294 | if (entry->visible != visible) { 295 | entry->visible = visible; 296 | gtk_widget_set_visible(entry->row, visible); 297 | 298 | if (visible) { 299 | self->visible_apps_count++; 300 | } else { 301 | self->visible_apps_count--; 302 | } 303 | } 304 | } 305 | 306 | static void 307 | update_category_visibility(gpointer key, gpointer value, gpointer user_data) 308 | { 309 | GtkWidget* category_box = (GtkWidget*)value; 310 | gboolean has_visible_children = FALSE; 311 | 312 | GtkWidget* child = gtk_widget_get_first_child(category_box); 313 | while (child) { 314 | if (gtk_widget_get_visible(child)) { 315 | if (GTK_IS_BOX(child)) { // Skip the category label 316 | has_visible_children = TRUE; 317 | break; 318 | } 319 | } 320 | child = gtk_widget_get_next_sibling(child); 321 | } 322 | 323 | gtk_widget_set_visible(category_box, has_visible_children); 324 | } 325 | 326 | static void 327 | hyprmenu_list_view_dispose(GObject* object) 328 | { 329 | HyprMenuListView* self = HYPRMENU_LIST_VIEW(object); 330 | 331 | LIST_VIEW_DEBUG("Disposing list view"); 332 | 333 | g_clear_pointer(&self->filter_text, g_free); 334 | 335 | // First clear all app entries 336 | if (self->app_entries) { 337 | GHashTableIter iter; 338 | gpointer key, value; 339 | g_hash_table_iter_init(&iter, self->app_entries); 340 | while (g_hash_table_iter_next(&iter, &key, &value)) { 341 | AppEntry* entry = (AppEntry*)value; 342 | if (entry->row) { 343 | gtk_widget_unparent(entry->row); 344 | entry->row = NULL; 345 | } 346 | } 347 | g_hash_table_destroy(self->app_entries); 348 | self->app_entries = NULL; 349 | } 350 | 351 | // Clear category boxes 352 | if (self->category_boxes) { 353 | GHashTableIter iter; 354 | gpointer key, value; 355 | g_hash_table_iter_init(&iter, self->category_boxes); 356 | while (g_hash_table_iter_next(&iter, &key, &value)) { 357 | GtkWidget* box = GTK_WIDGET(value); 358 | if (box) { 359 | GtkWidget* child = gtk_widget_get_first_child(box); 360 | while (child) { 361 | GtkWidget* next = gtk_widget_get_next_sibling(child); 362 | gtk_widget_unparent(child); 363 | child = next; 364 | } 365 | gtk_widget_unparent(box); 366 | } 367 | } 368 | g_hash_table_destroy(self->category_boxes); 369 | self->category_boxes = NULL; 370 | } 371 | 372 | // Clear categories list 373 | if (self->categories) { 374 | g_list_free_full(self->categories, g_free); 375 | self->categories = NULL; 376 | } 377 | 378 | // Finally unparent the scroll window 379 | if (self->scroll_window) { 380 | gtk_widget_unparent(self->scroll_window); 381 | self->scroll_window = NULL; 382 | } 383 | 384 | if (self->last_error) { 385 | g_error_free(self->last_error); 386 | self->last_error = NULL; 387 | } 388 | 389 | G_OBJECT_CLASS(hyprmenu_list_view_parent_class)->dispose(object); 390 | } 391 | 392 | static void 393 | hyprmenu_list_view_class_init(HyprMenuListViewClass* class) 394 | { 395 | GObjectClass* object_class = G_OBJECT_CLASS(class); 396 | GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(class); 397 | 398 | object_class->dispose = hyprmenu_list_view_dispose; 399 | 400 | gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT); 401 | 402 | LIST_VIEW_DEBUG("List view class initialized"); 403 | } 404 | 405 | static void 406 | hyprmenu_list_view_init(HyprMenuListView* self) 407 | { 408 | LIST_VIEW_DEBUG("Initializing list view"); 409 | 410 | self->app_entries = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, 411 | (GDestroyNotify)app_entry_free); 412 | self->category_boxes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); 413 | self->categories = NULL; 414 | self->show_descriptions = TRUE; 415 | self->filter_text = NULL; 416 | self->visible_apps_count = 0; 417 | self->initialized = FALSE; 418 | self->last_error = NULL; 419 | 420 | // Create scroll window 421 | self->scroll_window = gtk_scrolled_window_new(); 422 | gtk_widget_set_parent(self->scroll_window, GTK_WIDGET(self)); 423 | gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->scroll_window), 424 | GTK_POLICY_NEVER, 425 | GTK_POLICY_AUTOMATIC); 426 | 427 | // Create main box 428 | self->main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 429 | gtk_widget_add_css_class(self->main_box, "hyprmenu-main-box"); 430 | gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scroll_window), self->main_box); 431 | 432 | // Create categories box 433 | self->categories_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); 434 | gtk_widget_add_css_class(self->categories_box, "hyprmenu-categories"); 435 | gtk_box_append(GTK_BOX(self->main_box), self->categories_box); 436 | 437 | self->initialized = TRUE; 438 | LIST_VIEW_DEBUG("List view initialization complete"); 439 | } 440 | 441 | GtkWidget* 442 | hyprmenu_list_view_new(void) 443 | { 444 | LIST_VIEW_DEBUG("Creating new list view"); 445 | return GTK_WIDGET(g_object_new(HYPRMENU_TYPE_LIST_VIEW, NULL)); 446 | } 447 | 448 | gboolean 449 | hyprmenu_list_view_add_app(HyprMenuListView* self, GDesktopAppInfo* app_info) 450 | { 451 | g_return_val_if_fail(HYPRMENU_IS_LIST_VIEW(self), FALSE); 452 | g_return_val_if_fail(G_IS_DESKTOP_APP_INFO(app_info), FALSE); 453 | 454 | if (!self->initialized) { 455 | LIST_VIEW_ERROR("Cannot add app: List view not properly initialized"); 456 | return FALSE; 457 | } 458 | 459 | const char* app_id = g_app_info_get_id(G_APP_INFO(app_info)); 460 | if (!app_id) { 461 | LIST_VIEW_WARNING("Cannot add app: Missing app ID"); 462 | return FALSE; 463 | } 464 | 465 | // Check if app already exists 466 | if (g_hash_table_contains(self->app_entries, app_id)) { 467 | LIST_VIEW_DEBUG("App already exists: %s", app_id); 468 | return TRUE; 469 | } 470 | 471 | // Create new entry 472 | AppEntry* entry = create_app_entry(self, app_info); 473 | if (!entry) { 474 | LIST_VIEW_ERROR("Failed to create app entry for: %s", app_id); 475 | return FALSE; 476 | } 477 | 478 | // Add to category 479 | GtkWidget* category_box = get_or_create_category_box(self, entry->primary_category); 480 | if (!category_box) { 481 | LIST_VIEW_ERROR("Failed to get/create category box for: %s", entry->primary_category); 482 | app_entry_free(entry); 483 | return FALSE; 484 | } 485 | 486 | gtk_box_append(GTK_BOX(category_box), entry->row); 487 | 488 | // Store entry 489 | g_hash_table_insert(self->app_entries, entry->id, entry); 490 | self->visible_apps_count++; 491 | 492 | LIST_VIEW_DEBUG("Successfully added app: %s", app_id); 493 | return TRUE; 494 | } 495 | 496 | void 497 | hyprmenu_list_view_clear(HyprMenuListView* self) 498 | { 499 | g_return_if_fail(HYPRMENU_IS_LIST_VIEW(self)); 500 | 501 | LIST_VIEW_DEBUG("Clearing list view"); 502 | 503 | if (self->app_entries) { 504 | g_hash_table_remove_all(self->app_entries); 505 | } 506 | 507 | if (self->category_boxes) { 508 | g_hash_table_remove_all(self->category_boxes); 509 | } 510 | 511 | if (self->categories) { 512 | g_list_free_full(self->categories, g_free); 513 | self->categories = NULL; 514 | } 515 | 516 | self->visible_apps_count = 0; 517 | 518 | LIST_VIEW_DEBUG("List view cleared"); 519 | } 520 | 521 | gboolean 522 | hyprmenu_list_view_filter(HyprMenuListView* self, const char* text) 523 | { 524 | g_return_val_if_fail(HYPRMENU_IS_LIST_VIEW(self), FALSE); 525 | 526 | LIST_VIEW_DEBUG("Applying filter: %s", text ? text : "(null)"); 527 | 528 | g_free(self->filter_text); 529 | self->filter_text = text ? g_strdup(text) : NULL; 530 | 531 | self->visible_apps_count = 0; 532 | 533 | // Update visibility of entries 534 | g_hash_table_foreach(self->app_entries, update_entry_visibility, NULL); 535 | 536 | // Update visibility of categories 537 | g_hash_table_foreach(self->category_boxes, update_category_visibility, NULL); 538 | 539 | LIST_VIEW_DEBUG("Filter applied. Visible apps: %u", self->visible_apps_count); 540 | return TRUE; 541 | } 542 | 543 | void 544 | hyprmenu_list_view_set_show_descriptions(HyprMenuListView* self, gboolean show_descriptions) 545 | { 546 | g_return_if_fail(HYPRMENU_IS_LIST_VIEW(self)); 547 | 548 | if (self->show_descriptions == show_descriptions) 549 | return; 550 | 551 | LIST_VIEW_DEBUG("Setting show descriptions: %d", show_descriptions); 552 | 553 | self->show_descriptions = show_descriptions; 554 | 555 | // Rebuild all entries 556 | GList* entries = g_hash_table_get_values(self->app_entries); 557 | for (GList* l = entries; l; l = l->next) { 558 | AppEntry* entry = l->data; 559 | 560 | if (show_descriptions && entry->description && !entry->desc_label) { 561 | entry->desc_label = gtk_label_new(entry->description); 562 | gtk_label_set_xalign(GTK_LABEL(entry->desc_label), 0); 563 | gtk_label_set_wrap(GTK_LABEL(entry->desc_label), TRUE); 564 | gtk_widget_add_css_class(entry->desc_label, "app-description"); 565 | gtk_box_append(GTK_BOX(entry->label_box), entry->desc_label); 566 | } else if (!show_descriptions && entry->desc_label) { 567 | gtk_widget_unparent(entry->desc_label); 568 | entry->desc_label = NULL; 569 | } 570 | } 571 | g_list_free(entries); 572 | 573 | LIST_VIEW_DEBUG("Show descriptions updated"); 574 | } 575 | 576 | guint 577 | hyprmenu_list_view_get_visible_count(HyprMenuListView* self) 578 | { 579 | g_return_val_if_fail(HYPRMENU_IS_LIST_VIEW(self), 0); 580 | return self->visible_apps_count; 581 | } 582 | 583 | gboolean 584 | hyprmenu_list_view_is_valid(HyprMenuListView* self) 585 | { 586 | if (!HYPRMENU_IS_LIST_VIEW(self)) { 587 | return FALSE; 588 | } 589 | 590 | if (!self->initialized) { 591 | if (self->last_error) { 592 | g_error_free(self->last_error); 593 | } 594 | self->last_error = g_error_new(G_IO_ERROR, G_IO_ERROR_FAILED, 595 | "List view not properly initialized"); 596 | return FALSE; 597 | } 598 | 599 | if (!self->scroll_window || !self->main_box || !self->categories_box) { 600 | if (self->last_error) { 601 | g_error_free(self->last_error); 602 | } 603 | self->last_error = g_error_new(G_IO_ERROR, G_IO_ERROR_FAILED, 604 | "Essential widgets are missing"); 605 | return FALSE; 606 | } 607 | 608 | if (!self->app_entries || !self->category_boxes) { 609 | if (self->last_error) { 610 | g_error_free(self->last_error); 611 | } 612 | self->last_error = g_error_new(G_IO_ERROR, G_IO_ERROR_FAILED, 613 | "Data storage containers are missing"); 614 | return FALSE; 615 | } 616 | 617 | return TRUE; 618 | } -------------------------------------------------------------------------------- /src/list_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | G_BEGIN_DECLS 7 | 8 | /* Debug macros */ 9 | #define HYPRMENU_DEBUG_ENV "HYPRMENU_DEBUG" 10 | 11 | #ifdef G_ENABLE_DEBUG 12 | #define LIST_VIEW_DEBUG(fmt, ...) g_message("DEBUG: [ListView] " fmt, ##__VA_ARGS__) 13 | #define LIST_VIEW_ERROR(fmt, ...) g_critical("ERROR: [ListView] " fmt, ##__VA_ARGS__) 14 | #define LIST_VIEW_WARNING(fmt, ...) g_warning("WARNING: [ListView] " fmt, ##__VA_ARGS__) 15 | #define LIST_VIEW_INFO(fmt, ...) g_info("INFO: [ListView] " fmt, ##__VA_ARGS__) 16 | #else 17 | #define LIST_VIEW_DEBUG(fmt, ...) 18 | #define LIST_VIEW_ERROR(fmt, ...) 19 | #define LIST_VIEW_WARNING(fmt, ...) 20 | #define LIST_VIEW_INFO(fmt, ...) 21 | #endif 22 | 23 | #define HYPRMENU_TYPE_LIST_VIEW (hyprmenu_list_view_get_type()) 24 | G_DECLARE_FINAL_TYPE(HyprMenuListView, hyprmenu_list_view, HYPRMENU, LIST_VIEW, GtkWidget) 25 | 26 | /** 27 | * Create a new list view widget 28 | */ 29 | GtkWidget* hyprmenu_list_view_new(void); 30 | 31 | /** 32 | * Add an application to the list view 33 | * @param self The list view instance 34 | * @param app_info The application info to add 35 | * @return TRUE if the app was added successfully, FALSE otherwise 36 | */ 37 | gboolean hyprmenu_list_view_add_app(HyprMenuListView* self, GDesktopAppInfo* app_info); 38 | 39 | /** 40 | * Clear all applications from the list view 41 | * @param self The list view instance 42 | */ 43 | void hyprmenu_list_view_clear(HyprMenuListView* self); 44 | 45 | /** 46 | * Filter applications based on search text 47 | * @param self The list view instance 48 | * @param text The search text to filter by 49 | * @return TRUE if the filter was applied successfully, FALSE otherwise 50 | */ 51 | gboolean hyprmenu_list_view_filter(HyprMenuListView* self, const char* text); 52 | 53 | /** 54 | * Set whether to show application descriptions 55 | * @param self The list view instance 56 | * @param show_descriptions Whether to show descriptions 57 | */ 58 | void hyprmenu_list_view_set_show_descriptions(HyprMenuListView* self, gboolean show_descriptions); 59 | 60 | /** 61 | * Get the number of visible applications 62 | * @param self The list view instance 63 | * @return The number of visible applications 64 | */ 65 | guint hyprmenu_list_view_get_visible_count(HyprMenuListView* self); 66 | 67 | /** 68 | * Check if the list view is valid and ready 69 | * @param self The list view instance 70 | * @return TRUE if the list view is valid, FALSE otherwise 71 | */ 72 | gboolean hyprmenu_list_view_is_valid(HyprMenuListView* self); 73 | 74 | G_END_DECLS -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "window.h" 5 | #include "config.h" 6 | 7 | static void 8 | setup_debug_logging(void) 9 | { 10 | const char* debug_env = g_getenv("HYPRMENU_DEBUG"); 11 | if (debug_env && *debug_env) { 12 | g_setenv("G_MESSAGES_DEBUG", "all", TRUE); 13 | g_message("Debug logging enabled"); 14 | } 15 | } 16 | 17 | static void 18 | on_activate(GtkApplication *app) 19 | { 20 | g_message("Activating application"); 21 | 22 | // Load config first 23 | if (!hyprmenu_config_load()) { 24 | g_critical("Failed to load configuration"); 25 | return; 26 | } 27 | g_message("Configuration loaded successfully"); 28 | 29 | // Create window 30 | HyprMenuWindow *window = hyprmenu_window_new(app); 31 | if (!window) { 32 | g_critical("Failed to create window"); 33 | return; 34 | } 35 | g_message("Window created successfully"); 36 | 37 | // Show window 38 | hyprmenu_window_show(window); 39 | g_message("Window shown"); 40 | } 41 | 42 | static void 43 | on_shutdown(GApplication *app, gpointer user_data) 44 | { 45 | (void)app; // Silence unused parameter warning 46 | (void)user_data; // Silence unused parameter warning 47 | 48 | g_message("Shutting down application"); 49 | hyprmenu_config_save(); 50 | g_message("Configuration saved"); 51 | } 52 | 53 | int 54 | main(int argc, char *argv[]) 55 | { 56 | // Enable debug logging if requested 57 | setup_debug_logging(); 58 | 59 | g_message("Starting HyprMenu"); 60 | 61 | /* Force Wayland backend */ 62 | g_setenv("GDK_BACKEND", "wayland", TRUE); 63 | g_message("Set Wayland backend"); 64 | 65 | /* Initialize GTK */ 66 | if (!gtk_init_check()) { 67 | g_critical("Failed to initialize GTK"); 68 | return 1; 69 | } 70 | g_message("GTK initialized"); 71 | 72 | /* Verify we're running under Wayland */ 73 | GdkDisplay *display = gdk_display_get_default(); 74 | if (!display) { 75 | g_critical("No display found"); 76 | return 1; 77 | } 78 | 79 | if (!GDK_IS_WAYLAND_DISPLAY(display)) { 80 | g_critical("Not running under Wayland"); 81 | return 1; 82 | } 83 | g_message("Confirmed running under Wayland"); 84 | 85 | /* Check GTK Layer Shell */ 86 | if (!gtk_layer_is_supported()) { 87 | g_critical("GTK Layer Shell is not supported by this compositor"); 88 | return 1; 89 | } 90 | g_message("GTK Layer Shell support confirmed"); 91 | 92 | // Create application 93 | GtkApplication *app = gtk_application_new("org.hyprmenu.app", G_APPLICATION_DEFAULT_FLAGS); 94 | g_signal_connect(app, "activate", G_CALLBACK(on_activate), NULL); 95 | g_signal_connect(app, "shutdown", G_CALLBACK(on_shutdown), NULL); 96 | g_message("Application created"); 97 | 98 | /* Initialize configuration */ 99 | if (!hyprmenu_config_init()) { 100 | g_critical("Failed to initialize configuration"); 101 | return 1; 102 | } 103 | g_message("Configuration initialized"); 104 | 105 | // Run application 106 | g_message("Running application"); 107 | int status = g_application_run(G_APPLICATION(app), argc, argv); 108 | 109 | // Cleanup 110 | g_object_unref(app); 111 | g_message("Application cleanup complete"); 112 | 113 | return status; 114 | } -------------------------------------------------------------------------------- /src/window.c: -------------------------------------------------------------------------------- 1 | #include "window.h" 2 | #include "config.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "app_grid.h" 8 | 9 | // Add this struct definition at the top of the file, after the includes 10 | typedef struct { 11 | HyprMenuWindow *window; 12 | char *command; 13 | char *error_message; 14 | } DialogData; 15 | 16 | // Function declarations 17 | static void on_dialog_yes_clicked(GtkButton *button, gpointer user_data); 18 | static void on_dialog_cancel_clicked(GtkButton *button, gpointer user_data); 19 | static void show_confirmation_dialog(HyprMenuWindow *self, 20 | const char *title, 21 | const char *message, 22 | const char *command, 23 | const char *error_message); 24 | static void execute_system_action(HyprMenuWindow *self, 25 | const char *command, 26 | const char *error_message); 27 | static void on_logout_clicked(GtkButton *button, gpointer user_data); 28 | static void on_shutdown_clicked(GtkButton *button, gpointer user_data); 29 | static void on_reboot_clicked(GtkButton *button, gpointer user_data); 30 | static void on_hibernate_clicked(GtkButton *button, gpointer user_data); 31 | static void on_sleep_clicked(GtkButton *button, gpointer user_data); 32 | static void on_lock_clicked(GtkButton *button, gpointer user_data); 33 | 34 | G_DEFINE_TYPE (HyprMenuWindow, hyprmenu_window, GTK_TYPE_APPLICATION_WINDOW) 35 | 36 | static void 37 | on_search_changed (GtkSearchEntry *entry, 38 | HyprMenuWindow *self) 39 | { 40 | g_print("on_search_changed: Search text changed\n"); 41 | 42 | if (!entry) { 43 | g_warning("on_search_changed: Search entry is NULL"); 44 | return; 45 | } 46 | 47 | if (!self) { 48 | g_warning("on_search_changed: Window is NULL"); 49 | return; 50 | } 51 | 52 | const char *text = gtk_editable_get_text (GTK_EDITABLE (entry)); 53 | g_print("on_search_changed: Search text is '%s'\n", text ? text : "NULL"); 54 | 55 | if (!self->app_grid) { 56 | g_warning("on_search_changed: App grid is NULL"); 57 | return; 58 | } 59 | 60 | hyprmenu_app_grid_filter (HYPRMENU_APP_GRID (self->app_grid), text); 61 | g_print("on_search_changed: Filter applied successfully\n"); 62 | } 63 | 64 | static void 65 | on_search_activate (GtkSearchEntry *entry, 66 | HyprMenuWindow *self) 67 | { 68 | // Handle search activation if needed 69 | (void)entry; 70 | (void)self; 71 | } 72 | 73 | static void 74 | on_search_stop (GtkSearchEntry *entry, 75 | HyprMenuWindow *self) 76 | { 77 | // Handle search stop if needed 78 | (void)entry; 79 | (void)self; 80 | } 81 | 82 | static void 83 | on_focus_out (GtkWindow *window, 84 | GParamSpec *pspec, 85 | gpointer user_data) 86 | { 87 | (void)pspec; 88 | (void)user_data; 89 | 90 | // Close the window when it loses focus 91 | if (config->close_on_focus_out) { 92 | GtkApplication *app = GTK_APPLICATION(gtk_window_get_application(window)); 93 | gtk_window_close(window); 94 | if (app) { 95 | g_application_quit(G_APPLICATION(app)); 96 | } 97 | } 98 | } 99 | 100 | static gboolean 101 | on_key_press (GtkEventControllerKey *controller, 102 | guint keyval, 103 | guint keycode, 104 | GdkModifierType state, 105 | HyprMenuWindow *self) 106 | { 107 | (void)controller; 108 | (void)keycode; 109 | (void)state; 110 | 111 | // Close window on Escape key press 112 | if (keyval == GDK_KEY_Escape) { 113 | if (config->close_on_escape) { 114 | GtkApplication *app = GTK_APPLICATION(gtk_window_get_application(GTK_WINDOW(self))); 115 | gtk_window_close(GTK_WINDOW(self)); 116 | if (app) { 117 | g_application_quit(G_APPLICATION(app)); 118 | } 119 | return TRUE; 120 | } 121 | } 122 | 123 | // Close window on Super key press if configured 124 | if ((keyval == GDK_KEY_Super_L || keyval == GDK_KEY_Super_R) && 125 | config->close_on_super_key) { 126 | GtkApplication *app = GTK_APPLICATION(gtk_window_get_application(GTK_WINDOW(self))); 127 | gtk_window_close(GTK_WINDOW(self)); 128 | if (app) { 129 | g_application_quit(G_APPLICATION(app)); 130 | } 131 | return TRUE; 132 | } 133 | 134 | return FALSE; 135 | } 136 | 137 | static gboolean 138 | on_key_release (GtkEventControllerKey *controller, 139 | guint keyval, 140 | guint keycode, 141 | GdkModifierType state, 142 | HyprMenuWindow *self) 143 | { 144 | (void)controller; 145 | (void)keycode; 146 | (void)state; 147 | 148 | // Close window on Super key release if configured 149 | if ((keyval == GDK_KEY_Super_L || keyval == GDK_KEY_Super_R) && 150 | config->close_on_super_key) { 151 | gtk_window_close (GTK_WINDOW (self)); 152 | return TRUE; 153 | } 154 | 155 | return FALSE; 156 | } 157 | 158 | static void 159 | on_click_outside(GtkGestureClick *gesture, 160 | gint n_press, 161 | double x, 162 | double y, 163 | gpointer user_data) 164 | { 165 | // Only proceed if configured to close on click outside 166 | if (!config->close_on_click_outside) { 167 | return; 168 | } 169 | 170 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 171 | GtkWidget *widget = GTK_WIDGET(self); 172 | GtkWidget *clicked_widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture)); 173 | 174 | // Get the widget under the pointer 175 | GtkWidget *target = gtk_widget_pick(widget, x, y, GTK_PICK_DEFAULT); 176 | g_print("Click outside - target widget: %s\n", target ? gtk_widget_get_name(target) : "NULL"); 177 | 178 | // If the click is on a system button or its child (icon), don't close 179 | GtkWidget *ancestor = target; 180 | while (ancestor) { 181 | if (GTK_IS_BUTTON(ancestor) && 182 | gtk_widget_has_css_class(ancestor, "system-button")) { 183 | g_print("Click on system button - not closing\n"); 184 | return; 185 | } 186 | ancestor = gtk_widget_get_parent(ancestor); 187 | } 188 | 189 | // Get the main box bounds 190 | graphene_rect_t bounds; 191 | if (!gtk_widget_compute_bounds(self->main_box, widget, &bounds)) { 192 | return; // Failed to compute bounds 193 | } 194 | 195 | // Convert coordinates to widget space 196 | graphene_point_t point = GRAPHENE_POINT_INIT(x, y); 197 | graphene_point_t transformed; 198 | if (!gtk_widget_compute_point(clicked_widget, self->main_box, &point, &transformed)) { 199 | return; // Failed to compute point 200 | } 201 | 202 | // Check if the click is outside the main box 203 | if (transformed.x < bounds.origin.x || 204 | transformed.y < bounds.origin.y || 205 | transformed.x > bounds.origin.x + bounds.size.width || 206 | transformed.y > bounds.origin.y + bounds.size.height) { 207 | g_print("Click outside main box - closing window\n"); 208 | GtkApplication *app = gtk_window_get_application(GTK_WINDOW(self)); 209 | gtk_window_close(GTK_WINDOW(self)); 210 | if (app) { 211 | g_application_quit(G_APPLICATION(app)); 212 | } 213 | } 214 | } 215 | 216 | static void 217 | on_dialog_yes_clicked(GtkButton *button, gpointer user_data) 218 | { 219 | DialogData *data = (DialogData *)user_data; 220 | g_print("Executing command: %s\n", data->command); 221 | 222 | // Execute the command 223 | execute_system_action(data->window, data->command, data->error_message); 224 | 225 | // Get dialog window and destroy it 226 | GtkWidget *dialog = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_WINDOW); 227 | if (dialog) { 228 | gtk_window_destroy(GTK_WINDOW(dialog)); 229 | } 230 | 231 | // Free data 232 | g_free(data->command); 233 | g_free(data->error_message); 234 | g_free(data); 235 | } 236 | 237 | static void 238 | on_dialog_cancel_clicked(GtkButton *button, gpointer user_data) 239 | { 240 | DialogData *data = (DialogData *)user_data; 241 | 242 | // Get dialog window and destroy it 243 | GtkWidget *dialog = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_WINDOW); 244 | if (dialog) { 245 | gtk_window_destroy(GTK_WINDOW(dialog)); 246 | } 247 | 248 | // Free data 249 | g_free(data->command); 250 | g_free(data->error_message); 251 | g_free(data); 252 | } 253 | 254 | static void 255 | show_confirmation_dialog(HyprMenuWindow *self, 256 | const char *title, 257 | const char *message, 258 | const char *command, 259 | const char *error_message) 260 | { 261 | g_print("Showing confirmation dialog: %s\n", title); 262 | 263 | // Create a new window for the dialog 264 | GtkWidget *dialog = gtk_window_new(); 265 | gtk_window_set_title(GTK_WINDOW(dialog), title); 266 | gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); 267 | gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(self)); 268 | gtk_window_set_destroy_with_parent(GTK_WINDOW(dialog), TRUE); 269 | 270 | // Initialize layer shell for dialog 271 | gtk_layer_init_for_window(GTK_WINDOW(dialog)); 272 | gtk_layer_set_layer(GTK_WINDOW(dialog), GTK_LAYER_SHELL_LAYER_OVERLAY); 273 | gtk_layer_set_anchor(GTK_WINDOW(dialog), GTK_LAYER_SHELL_EDGE_TOP, TRUE); 274 | gtk_layer_set_anchor(GTK_WINDOW(dialog), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); 275 | gtk_layer_set_anchor(GTK_WINDOW(dialog), GTK_LAYER_SHELL_EDGE_LEFT, TRUE); 276 | gtk_layer_set_anchor(GTK_WINDOW(dialog), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); 277 | 278 | // Create main container 279 | GtkWidget *main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 280 | gtk_widget_add_css_class(main_box, "session-bg"); 281 | 282 | // Add click-outside handler using a gesture controller 283 | GtkWidget *overlay_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 284 | GtkGesture *click = gtk_gesture_click_new(); 285 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), GDK_BUTTON_PRIMARY); 286 | g_signal_connect(click, "pressed", G_CALLBACK(on_click_outside), dialog); 287 | gtk_widget_add_controller(overlay_box, GTK_EVENT_CONTROLLER(click)); 288 | gtk_box_append(GTK_BOX(main_box), overlay_box); 289 | 290 | // Create content box 291 | GtkWidget *content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 15); 292 | gtk_widget_set_vexpand(content_box, TRUE); 293 | gtk_widget_set_valign(content_box, GTK_ALIGN_CENTER); 294 | gtk_widget_set_halign(content_box, GTK_ALIGN_CENTER); 295 | gtk_widget_add_css_class(content_box, "spacing-v-15"); 296 | 297 | // Add title and description 298 | GtkWidget *title_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 299 | gtk_widget_set_margin_bottom(title_box, 10); 300 | 301 | GtkWidget *title_label = gtk_label_new(title); 302 | gtk_widget_add_css_class(title_label, "txt-title"); 303 | gtk_box_append(GTK_BOX(title_box), title_label); 304 | 305 | GtkWidget *desc_label = gtk_label_new(message); 306 | gtk_widget_add_css_class(desc_label, "txt-small"); 307 | gtk_label_set_justify(GTK_LABEL(desc_label), GTK_JUSTIFY_CENTER); 308 | gtk_box_append(GTK_BOX(title_box), desc_label); 309 | 310 | gtk_box_append(GTK_BOX(content_box), title_box); 311 | 312 | // Create button grid 313 | GtkWidget *button_grid = gtk_grid_new(); 314 | gtk_grid_set_row_spacing(GTK_GRID(button_grid), 15); 315 | gtk_grid_set_column_spacing(GTK_GRID(button_grid), 15); 316 | gtk_widget_set_halign(button_grid, GTK_ALIGN_CENTER); 317 | 318 | // Create Yes button 319 | GtkWidget *yes_button = gtk_button_new(); 320 | gtk_widget_add_css_class(yes_button, "session-button"); 321 | gtk_widget_add_css_class(yes_button, "session-color-5"); // Use red color for power actions 322 | 323 | GtkWidget *yes_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 324 | GtkWidget *yes_icon = gtk_image_new_from_icon_name("system-shutdown-symbolic"); 325 | gtk_image_set_pixel_size(GTK_IMAGE(yes_icon), 48); 326 | gtk_box_append(GTK_BOX(yes_box), yes_icon); 327 | 328 | GtkWidget *yes_label = gtk_label_new("Yes"); 329 | gtk_widget_add_css_class(yes_label, "session-button-desc"); 330 | gtk_widget_set_margin_top(yes_label, 5); 331 | gtk_box_append(GTK_BOX(yes_box), yes_label); 332 | 333 | gtk_button_set_child(GTK_BUTTON(yes_button), yes_box); 334 | 335 | // Store command and error message for the callback 336 | DialogData *data = g_new(DialogData, 1); 337 | data->window = self; 338 | data->command = g_strdup(command); 339 | data->error_message = g_strdup(error_message); 340 | 341 | g_signal_connect(yes_button, "clicked", G_CALLBACK(on_dialog_yes_clicked), data); 342 | 343 | // Create No button 344 | GtkWidget *no_button = gtk_button_new(); 345 | gtk_widget_add_css_class(no_button, "session-button"); 346 | gtk_widget_add_css_class(no_button, "session-color-7"); // Use neutral color for cancel 347 | 348 | GtkWidget *no_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 349 | GtkWidget *no_icon = gtk_image_new_from_icon_name("window-close-symbolic"); 350 | gtk_image_set_pixel_size(GTK_IMAGE(no_icon), 48); 351 | gtk_box_append(GTK_BOX(no_box), no_icon); 352 | 353 | GtkWidget *no_label = gtk_label_new("No"); 354 | gtk_widget_add_css_class(no_label, "session-button-desc"); 355 | gtk_widget_set_margin_top(no_label, 5); 356 | gtk_box_append(GTK_BOX(no_box), no_label); 357 | 358 | gtk_button_set_child(GTK_BUTTON(no_button), no_box); 359 | g_signal_connect(no_button, "clicked", G_CALLBACK(on_dialog_cancel_clicked), data); 360 | 361 | // Add buttons to grid 362 | gtk_grid_attach(GTK_GRID(button_grid), yes_button, 0, 0, 1, 1); 363 | gtk_grid_attach(GTK_GRID(button_grid), no_button, 1, 0, 1, 1); 364 | 365 | gtk_box_append(GTK_BOX(content_box), button_grid); 366 | gtk_box_append(GTK_BOX(main_box), content_box); 367 | 368 | gtk_window_set_child(GTK_WINDOW(dialog), main_box); 369 | gtk_window_present(GTK_WINDOW(dialog)); 370 | } 371 | 372 | static void 373 | on_logout_clicked (GtkButton *button, gpointer user_data) 374 | { 375 | g_print("Logout button clicked\n"); 376 | (void)button; 377 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 378 | 379 | show_confirmation_dialog(self, 380 | "Logout", 381 | "Are you sure you want to logout?", 382 | "bash -c 'pkill Hyprland || pkill sway || pkill niri || loginctl terminate-user $USER'", 383 | "Failed to execute logout command"); 384 | } 385 | 386 | static void 387 | on_shutdown_clicked (GtkButton *button, gpointer user_data) 388 | { 389 | g_print("Shutdown button clicked\n"); 390 | (void)button; 391 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 392 | 393 | show_confirmation_dialog(self, 394 | "Shutdown", 395 | "Are you sure you want to shutdown the system?", 396 | "bash -c 'systemctl poweroff || loginctl poweroff'", 397 | "Failed to execute shutdown command"); 398 | } 399 | 400 | static void 401 | on_reboot_clicked (GtkButton *button, gpointer user_data) 402 | { 403 | g_print("Reboot button clicked\n"); 404 | (void)button; 405 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 406 | 407 | show_confirmation_dialog(self, 408 | "Reboot", 409 | "Are you sure you want to reboot the system?", 410 | "bash -c 'systemctl reboot || loginctl reboot'", 411 | "Failed to execute reboot command"); 412 | } 413 | 414 | static void 415 | on_hibernate_clicked (GtkButton *button, gpointer user_data) 416 | { 417 | g_print("Hibernate button clicked\n"); 418 | (void)button; 419 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 420 | 421 | show_confirmation_dialog(self, 422 | "Hibernate", 423 | "Are you sure you want to hibernate the system?", 424 | "bash -c 'systemctl hibernate || loginctl hibernate'", 425 | "Failed to execute hibernate command"); 426 | } 427 | 428 | static void 429 | on_sleep_clicked (GtkButton *button, gpointer user_data) 430 | { 431 | g_print("Sleep button clicked\n"); 432 | (void)button; 433 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 434 | 435 | show_confirmation_dialog(self, 436 | "Sleep", 437 | "Are you sure you want to put the system to sleep?", 438 | "bash -c 'systemctl suspend || loginctl suspend'", 439 | "Failed to execute sleep command"); 440 | } 441 | 442 | static void 443 | on_lock_clicked (GtkButton *button, gpointer user_data) 444 | { 445 | g_print("Lock button clicked\n"); 446 | (void)button; 447 | HyprMenuWindow *self = HYPRMENU_WINDOW(user_data); 448 | 449 | show_confirmation_dialog(self, 450 | "Lock", 451 | "Are you sure you want to lock the screen?", 452 | "loginctl lock-session", 453 | "Failed to execute lock command"); 454 | } 455 | 456 | static GtkWidget* 457 | create_system_button (const char *icon_name, const char *label, GCallback callback, gpointer user_data) 458 | { 459 | g_print("Creating system button: %s\n", label); 460 | 461 | GtkWidget *button = gtk_button_new(); 462 | 463 | // Set a unique name for debugging 464 | gtk_widget_set_name(button, g_strdup_printf("system-button-%s", label)); 465 | 466 | // Create icon 467 | GtkWidget *icon = gtk_image_new_from_icon_name(icon_name); 468 | gtk_image_set_pixel_size(GTK_IMAGE(icon), 16); 469 | 470 | // Set button properties 471 | gtk_button_set_child(GTK_BUTTON(button), icon); 472 | gtk_widget_add_css_class(button, "system-button"); 473 | gtk_widget_set_margin_top(button, 0); 474 | gtk_widget_set_margin_bottom(button, 0); 475 | 476 | // Set tooltip text (shown on hover) 477 | gtk_widget_set_tooltip_text(button, label); 478 | 479 | // Make button activatable and focusable 480 | gtk_widget_set_can_focus(button, TRUE); 481 | gtk_widget_set_focusable(button, TRUE); 482 | 483 | // Connect signal with debug print 484 | if (callback) { 485 | g_print("Connecting signal for button: %s\n", label); 486 | gulong handler_id = g_signal_connect(button, "clicked", callback, user_data); 487 | g_print("Signal handler ID for %s: %lu\n", label, handler_id); 488 | } 489 | 490 | // Add click controller explicitly 491 | GtkGesture *click = gtk_gesture_click_new(); 492 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), GDK_BUTTON_PRIMARY); 493 | gtk_widget_add_controller(button, GTK_EVENT_CONTROLLER(click)); 494 | 495 | return button; 496 | } 497 | 498 | static void 499 | hyprmenu_window_init (HyprMenuWindow *self) 500 | { 501 | /* Set window properties */ 502 | gtk_window_set_default_size(GTK_WINDOW(self), config->window_width, config->window_height); 503 | gtk_window_set_resizable(GTK_WINDOW(self), FALSE); 504 | gtk_window_set_decorated(GTK_WINDOW(self), FALSE); 505 | 506 | /* Initialize layer shell */ 507 | GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(self)); 508 | if (!GDK_IS_WAYLAND_DISPLAY(display)) { 509 | g_error("HyprMenu requires Wayland"); 510 | return; 511 | } 512 | 513 | if (!gtk_layer_is_supported()) { 514 | g_error("GTK Layer Shell is required but not supported"); 515 | return; 516 | } 517 | 518 | g_message("Initializing GTK Layer Shell for window"); 519 | gtk_layer_init_for_window(GTK_WINDOW(self)); 520 | 521 | g_message("Setting layer shell properties"); 522 | gtk_layer_set_layer(GTK_WINDOW(self), GTK_LAYER_SHELL_LAYER_OVERLAY); 523 | gtk_layer_set_keyboard_mode(GTK_WINDOW(self), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); 524 | gtk_layer_set_exclusive_zone(GTK_WINDOW(self), -1); 525 | 526 | /* Set namespace for blur and other effects */ 527 | gtk_layer_set_namespace(GTK_WINDOW(self), "hyprmenu"); 528 | 529 | /* Enable blur for Hyprland */ 530 | GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(self)); 531 | if (GDK_IS_WAYLAND_SURFACE(surface)) { 532 | // Set window background to transparent to allow blur 533 | GtkStyleContext *style_context = gtk_widget_get_style_context(GTK_WIDGET(self)); 534 | GtkCssProvider *provider = gtk_css_provider_new(); 535 | gtk_css_provider_load_from_data(provider, 536 | ".hyprmenu-window { " 537 | " background-color: rgba(0, 0, 0, 0.0); " 538 | " border-radius: 16px; " 539 | " overflow: hidden; " 540 | "}\n" 541 | ".hyprmenu-main-box { " 542 | " border-radius: 12px; " 543 | " overflow: hidden; " 544 | "}\n" 545 | "window, .background { " 546 | " border-radius: 16px; " 547 | " overflow: hidden; " 548 | "}" 549 | , 550 | -1); 551 | gtk_style_context_add_provider(style_context, 552 | GTK_STYLE_PROVIDER(provider), 553 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); 554 | g_object_unref(provider); 555 | 556 | g_message("Enabled native Hyprland blur support with improved corner radius"); 557 | } 558 | 559 | /* Position window based on menu_position config */ 560 | g_message("Setting window position to: %d", config->menu_position); 561 | 562 | int bottom_margin = (config->bottom_offset == 0) ? 2 : (config->bottom_offset + 2); 563 | int top_margin = (config->top_offset == 0) ? 2 : (config->top_offset + 2); 564 | switch (config->menu_position) { 565 | case POSITION_TOP_LEFT: 566 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, TRUE); 567 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, TRUE); 568 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); 569 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE); 570 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, top_margin); 571 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, config->left_margin); 572 | break; 573 | 574 | case POSITION_TOP_CENTER: 575 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, TRUE); 576 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, FALSE); 577 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); 578 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE); 579 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, top_margin); 580 | break; 581 | 582 | case POSITION_TOP_RIGHT: 583 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); 584 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, TRUE); 585 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, FALSE); 586 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE); 587 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, top_margin); 588 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, config->left_margin); 589 | break; 590 | 591 | case POSITION_BOTTOM_LEFT: 592 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, TRUE); 593 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); 594 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); 595 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, FALSE); 596 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin); 597 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, config->left_margin); 598 | break; 599 | 600 | case POSITION_BOTTOM_CENTER: 601 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); 602 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, FALSE); 603 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); 604 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, FALSE); 605 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin); 606 | break; 607 | 608 | case POSITION_BOTTOM_RIGHT: 609 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE); 610 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); 611 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, FALSE); 612 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, FALSE); 613 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin); 614 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, config->left_margin); 615 | break; 616 | 617 | case POSITION_CENTER: 618 | // Center-center: do not anchor any edge, let the window float and center manually 619 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, FALSE); 620 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); 621 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, FALSE); 622 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE); 623 | // No margin needed, centering will be handled elsewhere if needed 624 | break; 625 | 626 | default: 627 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, TRUE); 628 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, TRUE); 629 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_RIGHT, FALSE); 630 | gtk_layer_set_anchor(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_BOTTOM, FALSE); 631 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_TOP, top_margin); 632 | gtk_layer_set_margin(GTK_WINDOW(self), GTK_LAYER_SHELL_EDGE_LEFT, config->left_margin); 633 | break; 634 | } 635 | 636 | /* Add CSS classes for styling */ 637 | gtk_widget_add_css_class (GTK_WIDGET (self), "hyprmenu-window"); 638 | 639 | /* Create main container (vertical box) */ 640 | GtkWidget *v_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 641 | gtk_widget_set_margin_start(v_box, config->window_padding); 642 | gtk_widget_set_margin_end(v_box, config->window_padding); 643 | gtk_widget_set_margin_top(v_box, config->window_padding); 644 | gtk_widget_set_margin_bottom(v_box, config->window_padding); 645 | gtk_widget_set_hexpand(v_box, TRUE); 646 | gtk_widget_set_vexpand(v_box, TRUE); 647 | gtk_widget_set_halign(v_box, GTK_ALIGN_FILL); 648 | gtk_widget_set_valign(v_box, GTK_ALIGN_FILL); 649 | gtk_window_set_child(GTK_WINDOW(self), v_box); 650 | 651 | /* Create content area */ 652 | GtkWidget *content_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 653 | gtk_widget_set_hexpand(content_box, TRUE); 654 | gtk_widget_set_vexpand(content_box, TRUE); 655 | gtk_widget_set_halign(content_box, GTK_ALIGN_FILL); 656 | gtk_box_append(GTK_BOX(v_box), content_box); 657 | 658 | /* Create main box */ 659 | self->main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 660 | gtk_widget_add_css_class(self->main_box, "hyprmenu-main-box"); 661 | gtk_widget_set_hexpand(self->main_box, TRUE); 662 | gtk_widget_set_halign(self->main_box, GTK_ALIGN_FILL); 663 | gtk_box_append(GTK_BOX(content_box), self->main_box); 664 | 665 | // Search bar 666 | self->search_entry = gtk_search_entry_new(); 667 | gtk_widget_add_css_class(self->search_entry, "hyprmenu-search"); 668 | gtk_widget_set_hexpand(self->search_entry, TRUE); 669 | gtk_widget_set_halign(self->search_entry, GTK_ALIGN_FILL); 670 | int search_extra_pad = 8; 671 | gtk_widget_set_margin_start(self->search_entry, config->window_padding + search_extra_pad); 672 | gtk_widget_set_margin_end(self->search_entry, config->window_padding + search_extra_pad); 673 | gtk_widget_set_margin_top(self->search_entry, search_extra_pad); 674 | gtk_widget_set_margin_bottom(self->search_entry, search_extra_pad); 675 | gtk_box_append(GTK_BOX(self->main_box), self->search_entry); 676 | 677 | // Content container with border 678 | GtkWidget *content_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 679 | gtk_widget_add_css_class(content_container, "hyprmenu-content-container"); 680 | gtk_widget_set_hexpand(content_container, TRUE); 681 | gtk_widget_set_vexpand(content_container, TRUE); 682 | gtk_widget_set_halign(content_container, GTK_ALIGN_FILL); 683 | gtk_widget_set_margin_start(content_container, config->window_padding + 10); 684 | gtk_widget_set_margin_end(content_container, config->window_padding + 10); 685 | gtk_widget_set_margin_top(content_container, 4); 686 | gtk_widget_set_margin_bottom(content_container, 12); // Increased space between grid and system buttons 687 | gtk_box_append(GTK_BOX(self->main_box), content_container); 688 | 689 | // App grid 690 | self->app_grid = GTK_WIDGET(hyprmenu_app_grid_new()); 691 | gtk_widget_add_css_class(self->app_grid, "hyprmenu-app-grid"); 692 | gtk_widget_set_hexpand(self->app_grid, TRUE); 693 | gtk_widget_set_halign(self->app_grid, GTK_ALIGN_FILL); 694 | gtk_widget_set_vexpand(self->app_grid, TRUE); 695 | gtk_box_append(GTK_BOX(content_container), self->app_grid); 696 | 697 | // Set grid columns if in grid view 698 | if (config->grid_hexpand) { 699 | GtkWidget *category_list = NULL; 700 | GObject *grid_obj = NULL; 701 | // Try to get the category list from the app_grid 702 | g_object_get(self->app_grid, "category_list", &category_list, NULL); 703 | if (category_list) { 704 | // Set columns for the flow box 705 | GtkWidget *all_apps_grid = NULL; 706 | g_object_get(category_list, "all_apps_grid", &all_apps_grid, NULL); 707 | if (all_apps_grid) { 708 | gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(all_apps_grid), config->grid_columns); 709 | gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(all_apps_grid), config->grid_columns); 710 | } 711 | } 712 | } 713 | 714 | /* Create system buttons box at the bottom */ 715 | self->system_buttons_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); 716 | gtk_widget_add_css_class(self->system_buttons_box, "hyprmenu-system-buttons"); 717 | gtk_widget_set_halign(self->system_buttons_box, GTK_ALIGN_CENTER); 718 | gtk_widget_set_margin_top(self->system_buttons_box, 4); 719 | gtk_widget_set_margin_bottom(self->system_buttons_box, 8); 720 | gtk_widget_set_margin_start(self->system_buttons_box, 4); 721 | gtk_widget_set_margin_end(self->system_buttons_box, 4); 722 | 723 | /* Add system buttons */ 724 | GtkWidget *logout_button = create_system_button("system-log-out-symbolic", "Logout", G_CALLBACK(on_logout_clicked), self); 725 | GtkWidget *shutdown_button = create_system_button("system-shutdown-symbolic", "Shutdown", G_CALLBACK(on_shutdown_clicked), self); 726 | GtkWidget *reboot_button = create_system_button("system-reboot-symbolic", "Reboot", G_CALLBACK(on_reboot_clicked), self); 727 | GtkWidget *hibernate_button = create_system_button("system-suspend-hibernate-symbolic", "Hibernate", G_CALLBACK(on_hibernate_clicked), self); 728 | GtkWidget *sleep_button = create_system_button("system-suspend-symbolic", "Sleep", G_CALLBACK(on_sleep_clicked), self); 729 | GtkWidget *lock_button = create_system_button("system-lock-screen-symbolic", "Lock", G_CALLBACK(on_lock_clicked), self); 730 | 731 | gtk_box_append(GTK_BOX(self->system_buttons_box), logout_button); 732 | gtk_box_append(GTK_BOX(self->system_buttons_box), shutdown_button); 733 | gtk_box_append(GTK_BOX(self->system_buttons_box), reboot_button); 734 | gtk_box_append(GTK_BOX(self->system_buttons_box), hibernate_button); 735 | gtk_box_append(GTK_BOX(self->system_buttons_box), sleep_button); 736 | gtk_box_append(GTK_BOX(self->system_buttons_box), lock_button); 737 | 738 | /* Add system buttons to bottom of main container */ 739 | gtk_box_append(GTK_BOX(v_box), self->system_buttons_box); 740 | 741 | /* Connect signals */ 742 | g_signal_connect (self->search_entry, "search-changed", 743 | G_CALLBACK (on_search_changed), self); 744 | g_signal_connect (self->search_entry, "activate", 745 | G_CALLBACK (on_search_activate), self); 746 | g_signal_connect (self->search_entry, "stop-search", 747 | G_CALLBACK (on_search_stop), self); 748 | 749 | /* Add key controller for Escape key and Super key */ 750 | self->key_controller = gtk_event_controller_key_new(); 751 | g_signal_connect(self->key_controller, "key-pressed", G_CALLBACK(on_key_press), self); 752 | g_signal_connect(self->key_controller, "key-released", G_CALLBACK(on_key_release), self); 753 | gtk_widget_add_controller(GTK_WIDGET(self), self->key_controller); 754 | 755 | /* Add click gesture for click-outside */ 756 | GtkGesture *gesture = gtk_gesture_click_new(); 757 | self->click_gesture = GTK_GESTURE_CLICK(gesture); 758 | gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->click_gesture), GDK_BUTTON_PRIMARY); 759 | gtk_gesture_single_set_exclusive(GTK_GESTURE_SINGLE(self->click_gesture), TRUE); 760 | g_signal_connect(self->click_gesture, "pressed", G_CALLBACK(on_click_outside), self); 761 | gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(gesture)); 762 | 763 | /* Connect focus-out signal */ 764 | g_signal_connect(self, "notify::has-focus", G_CALLBACK(on_focus_out), NULL); 765 | 766 | /* Apply custom CSS from configuration */ 767 | hyprmenu_config_apply_css(); 768 | 769 | /* Set dark color scheme */ 770 | GtkSettings *settings = gtk_settings_get_default(); 771 | g_object_set(settings, "gtk-application-prefer-dark-theme", TRUE, NULL); 772 | 773 | /* Focus search entry if configured */ 774 | if (config->focus_search_on_open) { 775 | g_signal_connect(self, "map", G_CALLBACK(gtk_widget_grab_focus), self->search_entry); 776 | } 777 | 778 | if (config->search_length > 0) { 779 | gtk_widget_set_size_request(self->search_entry, config->search_length, -1); 780 | } 781 | } 782 | 783 | static void 784 | hyprmenu_window_dispose(GObject *object) 785 | { 786 | HyprMenuWindow *self = HYPRMENU_WINDOW(object); 787 | 788 | // Remove controllers before disposing 789 | if (self->key_controller) { 790 | gtk_widget_remove_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->key_controller)); 791 | self->key_controller = NULL; 792 | } 793 | 794 | if (self->click_gesture) { 795 | gtk_widget_remove_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->click_gesture)); 796 | self->click_gesture = NULL; 797 | } 798 | 799 | // Properly unparent child widgets 800 | if (self->main_box) { 801 | GtkWidget *child = gtk_widget_get_first_child(self->main_box); 802 | while (child) { 803 | GtkWidget *next = gtk_widget_get_next_sibling(child); 804 | gtk_widget_unparent(child); 805 | child = next; 806 | } 807 | gtk_widget_unparent(self->main_box); 808 | self->main_box = NULL; 809 | } 810 | 811 | // Clear references to child widgets 812 | self->app_grid = NULL; 813 | self->search_entry = NULL; 814 | self->system_buttons_box = NULL; 815 | 816 | // Chain up 817 | G_OBJECT_CLASS(hyprmenu_window_parent_class)->dispose(object); 818 | } 819 | 820 | static void 821 | hyprmenu_window_finalize(GObject *object) 822 | { 823 | // Chain up 824 | G_OBJECT_CLASS(hyprmenu_window_parent_class)->finalize(object); 825 | } 826 | 827 | static void 828 | hyprmenu_window_class_init(HyprMenuWindowClass *class) 829 | { 830 | GObjectClass *object_class = G_OBJECT_CLASS(class); 831 | 832 | object_class->dispose = hyprmenu_window_dispose; 833 | object_class->finalize = hyprmenu_window_finalize; 834 | } 835 | 836 | HyprMenuWindow * 837 | hyprmenu_window_new (GtkApplication *app) 838 | { 839 | return g_object_new (HYPRMENU_TYPE_WINDOW, 840 | "application", app, 841 | NULL); 842 | } 843 | 844 | void 845 | hyprmenu_window_show (HyprMenuWindow *self) 846 | { 847 | hyprmenu_app_grid_refresh (HYPRMENU_APP_GRID (self->app_grid)); 848 | gtk_window_present (GTK_WINDOW (self)); 849 | 850 | // Directly focus the search entry when showing the window 851 | if (config->focus_search_on_open) { 852 | // Use a short delay to ensure the window is fully mapped before focusing 853 | g_timeout_add(50, (GSourceFunc)gtk_widget_grab_focus, self->search_entry); 854 | } 855 | } 856 | 857 | static void 858 | execute_system_action(HyprMenuWindow *self, 859 | const char *command, 860 | const char *error_message) 861 | { 862 | g_print("Executing system action: %s\n", command); 863 | 864 | GError *error = NULL; 865 | 866 | // First close the menu 867 | GtkApplication *app = gtk_window_get_application(GTK_WINDOW(self)); 868 | if (app) { 869 | g_application_quit(G_APPLICATION(app)); 870 | } 871 | 872 | // Execute the command asynchronously 873 | if (!g_spawn_command_line_async(command, &error)) { 874 | g_warning("%s: %s", error_message, error->message); 875 | g_error_free(error); 876 | } 877 | } -------------------------------------------------------------------------------- /src/window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "config.h" // For position enum 8 | 9 | G_BEGIN_DECLS 10 | 11 | #define HYPRMENU_TYPE_WINDOW (hyprmenu_window_get_type()) 12 | G_DECLARE_FINAL_TYPE (HyprMenuWindow, hyprmenu_window, HYPRMENU, WINDOW, GtkApplicationWindow) 13 | 14 | typedef struct _HyprMenuWindow { 15 | GtkApplicationWindow parent_instance; 16 | 17 | GtkWidget *main_box; 18 | GtkWidget *search_entry; 19 | GtkWidget *app_grid; 20 | GtkWidget *system_buttons_box; 21 | 22 | GtkEventController *key_controller; 23 | GtkGestureClick *click_gesture; 24 | } HyprMenuWindow; 25 | 26 | HyprMenuWindow *hyprmenu_window_new (GtkApplication *app); 27 | void hyprmenu_window_show (HyprMenuWindow *self); 28 | 29 | G_END_DECLS --------------------------------------------------------------------------------