├── .gitattributes ├── .gitignore ├── DOCUMENTATION.md ├── LICENSE ├── Makefile ├── README.md ├── calendar.conf ├── config.conf ├── events.css ├── preview.jpg ├── proto └── wlr-foreign-toplevel-management-unstable-v1.xml ├── src ├── config.hpp ├── config_parser.cpp ├── config_parser.hpp ├── main.cpp ├── main.hpp ├── module.cpp ├── module.hpp ├── modules │ ├── backlight.cpp │ ├── backlight.hpp │ ├── battery.cpp │ ├── battery.hpp │ ├── bluetooth.cpp │ ├── bluetooth.hpp │ ├── cellular.cpp │ ├── cellular.hpp │ ├── clock.cpp │ ├── clock.hpp │ ├── controls.cpp │ ├── controls.hpp │ ├── hyprland.cpp │ ├── hyprland.hpp │ ├── menu.cpp │ ├── menu.hpp │ ├── mpris.cpp │ ├── mpris.hpp │ ├── network.cpp │ ├── network.hpp │ ├── notifications.cpp │ ├── notifications.hpp │ ├── performance.cpp │ ├── performance.hpp │ ├── taskbar.cpp │ ├── taskbar.hpp │ ├── tray.cpp │ ├── tray.hpp │ ├── volume.cpp │ ├── volume.hpp │ ├── weather.cpp │ └── weather.hpp ├── overlay.cpp ├── window.cpp ├── window.hpp ├── wireless_network.cpp ├── wireless_network.hpp ├── wireplumber.cpp └── wireplumber.hpp └── style.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build/ 3 | src/git_info.hpp 4 | src/wlr-foreign-toplevel-management-unstable-v1.c 5 | src/wlr-foreign-toplevel-management-unstable-v1.h -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Features 2 | Sysbar offers modules which could be displayed on the bar to provide real time information about various things.
3 | It also offers sidepanels which contain widgets that show additional information from the loaded modules.
4 | 5 | Sidebars can be accessed by either clicking or tapping and dragging on either end of the bar.
6 | (Touch gestures are supported)
7 | 8 |
9 | List of supported features 10 | 11 | * clock 12 | * Module: Show time and date 13 | * Widget: Clendar (Planned: Show events and holidays) 14 | 15 | * weather 16 | * Module: Show weather 17 | * Widget: (Planned: Show more detailed weather info) 18 | 19 | * tray 20 | * Module: Show running tray items 21 | 22 | * hyprland 23 | * Module: Show window title (Planned: Workspace indicator) 24 | 25 | * volume 26 | * Module: Show audio output volume level 27 | * Widget: Same as the module (Planned: Set volume level) 28 | 29 | * network 30 | * Module: Show network type and status (Ethernet, Wireless, Cellular) 31 | * Control: (Planned: Show nearby wireless networks) 32 | 33 | * battery 34 | * Module: Show battery level and status (Charging, Discharging) 35 | * Control: (Planned: Show additional power stuff, Set power plan) 36 | 37 | * notification 38 | * Widget: Show notifications 39 | 40 | * taskbar 41 | * Module: Show running toplevels 42 | 43 | * backlight 44 | * Module: Show backlight level 45 | * Widget: Show and set backlight brightness levels 46 | 47 | * menu 48 | * Module: Shows a simple button to open or close an app launcher 49 | 50 | * mpris 51 | * Module: Show currently playing song 52 | * Widget: Same as the above but with controls and album art 53 | 54 | * bluetooth 55 | * Module: Show bluetooth status (Connected, Disconnected) 56 | * Control: (Planned: Show nearby bluetooth devices) 57 | 58 | * cellular 59 | * Module: Show cellular signal strength 60 | * Control: (Planned: Ability to connect/disconnect to/from cellular networks) 61 |
62 | 63 | # Configuration 64 | Sysbar offers compile time and runtime configuration options.
65 | Undesired features can be disabled by ommiting them from `src/config.hpp`
66 | The config system is INI based and can be configured by editing `~/.config/sys64/bar/config.conf`
67 | 68 |
69 | Runtime file based config 70 | 71 | | section | default | description | 72 | |----------------------|-----------------------------|----------------------------------------------------| 73 | | [main] | | Primary configuration | 74 | | position | 0 | 0 = top 1 = right 2 = bottom 3 = left | 75 | | size | 40 | Height or width depending on position | 76 | | layer | 2 | Background = 0, Bottom = 1, Top = 2, Overlay = 3 | 77 | | exclusive | true | Exclude part of the screen for the bar | 78 | | verbose | false | Verbose output (For debugging) | 79 | | main-monitor | HDMI-A-1 | Monitor output name (DP-1, HDMI-A-1, ect..) | 80 | | modules-start | clock,weather,tray | Modules shown at the start of the bar (Left/Top) | 81 | | modules-center | hyprland | Modules shown in the middle of the bar | 82 | | modules-end | volume,network,notification | Modules shown at the end of the bar (Right/Bottom) | 83 | | sidepanel-start-size | 350 | Start (Left/Top) sidepanel (Width/Height) | 84 | | sidepanel-end-size | 350 | End (Right/Bottom) sidepanel (Width/Height) | 85 | |  | | | 86 | | [clock] | | Clock module configuration | 87 | | interval | 1000 | How long (in ms) to refresh the time | 88 | | label-format | %H:%M | [Label format](https://www.man7.org/linux/man-pages/man1/date.1.html) | 89 | | tooltip-format | %Y/%m/%d | Same as the above but for the tooltip | 90 | | widget-layout | 0044 | [XYWH (Single digit values to position the widget)](https://gnome.pages.gitlab.gnome.org/gtkmm/classGtk_1_1Grid.html#a2f3d5ceb9a1c2f491b541aa56ebdc1e8) | 91 | |  | | | 92 | | [weather] | | Weather module configuration | 93 | | url | https://wttr.in/?format=j1 | [wttr.in](https://github.com/chubin/wttr.in) Cool project, Consider supporting the dev out | 94 | | unit | f | Temperature unit **C**elsius or **F**ahrenheit | 95 | |  | | | 96 | | [hyprland] | | Hyprland module configuration | 97 | | character-limit | 128 | Label character limit so the text won't get funky | 98 | |  | | | 99 | | [volume] | | Volume module configuration | 100 | | show-label | false | Show the volume level as text | 101 | | widget-layout | 0441 | [XYWH (Single digit values to position the widget)](https://gnome.pages.gitlab.gnome.org/gtkmm/classGtk_1_1Grid.html#a2f3d5ceb9a1c2f491b541aa56ebdc1e8) | 102 | |  | | | 103 | | [network] | | Network module configuration | 104 | | show-label | false | Show signal strength as text | 105 | |  | | | 106 | | [battery] | | Battery module configuration | 107 | | show-label | false | Show charge level as text | 108 | |  | | | 109 | | [notification] | | Notification widget configuration | 110 | | command | ffplay /usr/share/.. | Command to run whenever you recieve a notification | 111 | |  | | | 112 | | [backlight] | | Backlight module configuration | 113 | | path | | Path to backlight (/sys/class/backlight/panel) | 114 | | show-icon | true | Show brightness level as an icon | 115 | | show-label | true | Show brightness level as text | 116 | | widget-layout | 0341 | [XYWH (Single digit values to position the widget)](https://gnome.pages.gitlab.gnome.org/gtkmm/classGtk_1_1Grid.html#a2f3d5ceb9a1c2f491b541aa56ebdc1e8) | 117 | |  | | | 118 | | [menu] | | Menu module configuration | 119 | | show-icon | true | Show the icon | 120 | | show-label | false | Show the label | 121 | | icon-name | start-here | Icon name from your GTK icon theme | 122 | | label-text | Applications | Text to show on the menu button | 123 | |  | | | 124 | | [taskbar] | | Taskbar module configuration | 125 | | text-length | 14 | Window title length | 126 | | icon-size | 32 | Size of the icons | 127 | |  | | | 128 | | [mpris] | | Mpris module configuration | 129 | | show-icon | true | Show player status as an icon | 130 | | show-label | true | Show album name as text | 131 | | widget-layout | 0142 | [XYWH (Single digit values to position the widget)](https://gnome.pages.gitlab.gnome.org/gtkmm/classGtk_1_1Grid.html#a2f3d5ceb9a1c2f491b541aa56ebdc1e8) | 132 | |  | | | 133 | | [cellular] | | Cellular module configuration | 134 | | show-icon | true | Show signal strength as an icon | 135 | | show-label | false | Show signal strength as text | 136 |
137 | 138 |
139 | Runtime launch arguments 140 | 141 | ``` 142 | -p Set position 143 | -s Set start modules (modules on the left side) 144 | -c Set center modules (modules in the middle) 145 | -e Set end modules (modules on the right side) 146 | -S Set bar size (Height or Width depending on position) 147 | -V Be more verbose 148 | -v Prints version info 149 | ``` 150 |
151 | 152 | # Styling 153 | By default sysbar will follow your GTK4 theme.
154 | However it can also load Custom Style Sheets from `~/.config/sys64/bar/style.css` 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINS = sysbar 2 | LIBS = libsysbar.so 3 | PKGS = gtkmm-4.0 gtk4-layer-shell-0 4 | SRCS = $(wildcard src/*.cpp) 5 | 6 | PREFIX ?= /usr/local 7 | BINDIR ?= $(PREFIX)/bin 8 | LIBDIR ?= $(PREFIX)/lib 9 | DATADIR ?= $(PREFIX)/share 10 | BUILDDIR = build 11 | 12 | # Include enabled modules from config.hpp 13 | ifneq (, $(shell grep -E '^#define MODULE_CLOCK' src/config.hpp)) 14 | SRCS += src/modules/clock.cpp 15 | endif 16 | ifneq (, $(shell grep -E '^#define MODULE_WEATHER' src/config.hpp)) 17 | SRCS += src/modules/weather.cpp 18 | PKGS += libcurl jsoncpp 19 | endif 20 | ifneq (, $(shell grep -E '^#define MODULE_TRAY' src/config.hpp)) 21 | SRCS += src/modules/tray.cpp 22 | PKGS += dbus-1 23 | endif 24 | ifneq (, $(shell grep -E '^#define MODULE_HYPRLAND' src/config.hpp)) 25 | SRCS += src/modules/hyprland.cpp 26 | endif 27 | ifneq (, $(shell grep -E '^#define MODULE_VOLUME' src/config.hpp)) 28 | SRCS += src/modules/volume.cpp 29 | PKGS += wireplumber-0.5 30 | else 31 | SRCS := $(filter-out src/wireplumber.cpp, $(SRCS)) 32 | endif 33 | ifneq (, $(shell grep -E '^#define MODULE_NETWORK' src/config.hpp)) 34 | SRCS += src/modules/network.cpp 35 | CXXFLAGS += -I /usr/include/libnl3/ -lnl-3 -lnl-genl-3 36 | endif 37 | ifneq (, $(shell grep -E '^#define MODULE_BATTERY' src/config.hpp)) 38 | SRCS += src/modules/battery.cpp 39 | endif 40 | ifneq (, $(shell grep -E '^#define MODULE_NOTIFICATION' src/config.hpp)) 41 | SRCS += src/modules/notifications.cpp 42 | endif 43 | ifneq (, $(shell grep -E '^#define MODULE_PERFORMANCE' src/config.hpp)) 44 | SRCS += src/modules/performance.cpp 45 | endif 46 | ifneq (, $(shell grep -E '^#define MODULE_TASKBAR' src/config.hpp)) 47 | SRCS += src/modules/taskbar.cpp 48 | endif 49 | ifneq (, $(shell grep -E '^#define MODULE_BACKLIGHT' src/config.hpp)) 50 | SRCS += src/modules/backlight.cpp 51 | endif 52 | ifneq (, $(shell grep -E '^#define MODULE_MPRIS' src/config.hpp)) 53 | SRCS += src/modules/mpris.cpp 54 | PKGS += playerctl libcurl 55 | endif 56 | ifneq (, $(shell grep -E '^#define MODULE_BLUETOOTH' src/config.hpp)) 57 | SRCS += src/modules/bluetooth.cpp 58 | endif 59 | ifneq (, $(shell grep -E '^#define MODULE_CONTROLS' src/config.hpp)) 60 | SRCS += src/modules/controls.cpp 61 | endif 62 | ifneq (, $(shell grep -E '^#define MODULE_CELLULAR' src/config.hpp)) 63 | SRCS += src/modules/cellular.cpp 64 | endif 65 | ifneq (, $(shell grep -E '^#define MODULE_MENU' src/config.hpp)) 66 | SRCS += src/modules/menu.cpp 67 | endif 68 | ifeq (, $(shell grep -E '^#define FEATURE_WIRELESS' src/config.hpp)) 69 | SRCS := $(filter-out src/wireless_network.cpp, $(SRCS)) 70 | endif 71 | 72 | VPATH = src src/modules 73 | OBJS = $(patsubst src/%, $(BUILDDIR)/%, $(patsubst src/modules/%, $(BUILDDIR)/%, $(SRCS:.cpp=.o))) 74 | 75 | CXXFLAGS += $(CFLAGS) 76 | LDFLAGS += -Wl,--as-needed,-z,now,-z,pack-relative-relocs 77 | 78 | CXXFLAGS += $(shell pkg-config --cflags $(PKGS)) 79 | LDFLAGS += $(shell pkg-config --libs $(PKGS)) 80 | 81 | PROTOS = $(wildcard proto/*.xml) 82 | PROTO_HDRS = $(patsubst proto/%.xml, src/%.h, $(PROTOS)) 83 | PROTO_SRCS = $(patsubst proto/%.xml, src/%.c, $(PROTOS)) 84 | PROTO_OBJS = $(patsubst src/%,$(BUILDDIR)/%,$(PROTO_SRCS:.c=.o)) 85 | 86 | $(shell mkdir -p $(BUILDDIR)) 87 | JOB_COUNT := $(BINS) $(LIBS) $(PROTO_HDRS) $(PROTO_SRCS) $(PROTO_OBJS) $(OBJS) src/git_info.hpp 88 | JOBS_DONE := $(shell ls -l $(JOB_COUNT) 2> /dev/null | wc -l) 89 | 90 | define progress 91 | $(eval JOBS_DONE := $(shell echo $$(($(JOBS_DONE) + 1)))) 92 | @printf "[$(JOBS_DONE)/$(shell echo $(JOB_COUNT) | wc -w)] %s %s\n" $(1) $(2) 93 | endef 94 | 95 | all: release 96 | 97 | release: CFLAGS += -Oz -s -flto -fPIC -fomit-frame-pointer -Wl,--gc-sections -ffunction-sections -fdata-sections -ffast-math 98 | release: $(BINS) $(LIBS) 99 | 100 | debug: CFLAGS += -O0 -g -Wall -fPIC -DDEBUG 101 | debug: $(BINS) $(LIBS) 102 | 103 | install: $(BINS) 104 | @echo "Installing..." 105 | @install -D -t $(DESTDIR)$(BINDIR) $(BUILDDIR)/$(BINS) 106 | @install -D -t $(DESTDIR)$(LIBDIR) $(BUILDDIR)/$(LIBS) 107 | @install -D -t $(DESTDIR)$(DATADIR)/sys64/bar config.conf style.css events.css calendar.conf 108 | 109 | clean: 110 | @echo "Cleaning up" 111 | @rm -r $(BUILDDIR) src/git_info.hpp $(PROTO_HDRS) $(PROTO_SRCS) 112 | 113 | uninstall: 114 | @echo "Uninstalling..." 115 | @rm -f $(DESTDIR)$(BINDIR)/$(BINS) 116 | @rm -f $(DESTDIR)$(LIBDIR)/$(LIBS) 117 | @rm -f $(DESTDIR)$(DATADIR)/sys64/bar/config.conf 118 | @rm -f $(DESTDIR)$(DATADIR)/sys64/bar/style.css 119 | @rm -f $(DESTDIR)$(DATADIR)/sys64/bar/events.css 120 | @rm -f $(DESTDIR)$(DATADIR)/sys64/bar/calendar.conf 121 | 122 | $(BINS): src/git_info.hpp $(BUILDDIR)/main.o $(BUILDDIR)/config_parser.o 123 | $(call progress, Linking $@) 124 | @$(CXX) -o \ 125 | $(BUILDDIR)/$(BINS) \ 126 | $(BUILDDIR)/main.o \ 127 | $(BUILDDIR)/config_parser.o \ 128 | $(CXXFLAGS) \ 129 | -lgtkmm-4.0 -lglibmm-2.68 -lgiomm-2.68 -lgtk4-layer-shell 130 | 131 | $(LIBS): $(PROTO_HDRS) $(PROTO_SRCS) $(PROTO_OBJS) $(OBJS) 132 | $(call progress, Linking $@) 133 | @$(CXX) -o \ 134 | $(BUILDDIR)/$(LIBS) \ 135 | $(filter-out $(BUILDDIR)/main.o, $(OBJS)) \ 136 | $(PROTO_OBJS) \ 137 | $(CXXFLAGS) \ 138 | $(LDFLAGS) \ 139 | -shared 140 | 141 | $(BUILDDIR)/%.o: %.cpp 142 | $(call progress, Compiling $@) 143 | @$(CXX) -c $< -o $@ $(CXXFLAGS) 144 | 145 | $(BUILDDIR)/%.o: %.c 146 | $(call progress, Compiling $@) 147 | @$(CC) -c $< -o $@ $(CFLAGS) 148 | 149 | $(PROTO_HDRS): src/%.h : proto/%.xml 150 | $(call progress, Creating $@) 151 | @wayland-scanner client-header $< $@ 152 | 153 | $(PROTO_SRCS): src/%.c : proto/%.xml 154 | $(call progress, Creating $@) 155 | @wayland-scanner public-code $< $@ 156 | 157 | src/git_info.hpp: 158 | $(call progress, Creating $@) 159 | @commit_hash=$$(git rev-parse HEAD); \ 160 | commit_date=$$(git show -s --format=%cd --date=short $$commit_hash); \ 161 | commit_message=$$(git show -s --format="%s" $$commit_hash | sed 's/"/\\\"/g'); \ 162 | echo "#define GIT_COMMIT_MESSAGE \"$$commit_message\"" > src/git_info.hpp; \ 163 | echo "#define GIT_COMMIT_DATE \"$$commit_date\"" >> src/git_info.hpp 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sysbar 2 | Sysbar is a modular status bar for wayland written in gtkmm4
3 | ![preview](https://github.com/System64fumo/sysbar/blob/main/preview.jpg "preview") 4 | 5 | [![Packaging status](https://repology.org/badge/vertical-allrepos/sysbar.svg)](https://repology.org/project/sysbar/versions) 6 | 7 | # Documentation 8 | Read [this](https://github.com/System64fumo/sysbar/blob/main/DOCUMENTATION.md "documentation") to get started.
9 | 10 | # Credits 11 | [wttr.in](https://github.com/chubin/wttr.in) for their weather service
12 | [waybar](https://github.com/Alexays/Waybar) for showing how to write wireplumber stuff
13 | [wf-shell](https://github.com/WayfireWM/wf-shell) for showing how to do system tray stuff
14 | -------------------------------------------------------------------------------- /calendar.conf: -------------------------------------------------------------------------------- 1 | [events] 2 | *-1-1="New Year's Day!" 3 | *-2-14="Valentine's Day" 4 | *-4-1="April Fool's Day" 5 | *-7-4="Independence Day" 6 | *-10-31="Halloween" 7 | *-12-25="Christmas" 8 | -------------------------------------------------------------------------------- /config.conf: -------------------------------------------------------------------------------- 1 | [main] 2 | position=0 3 | size=40 4 | layer=2 5 | exclusive=true 6 | verbose=false 7 | main-monitor=HDMI-A-1 8 | modules-start=clock,weather,tray 9 | modules-center=hyprland 10 | modules-end=volume,network,notification 11 | sidepanel-start-size=350 12 | sidepanel-end-size=350 13 | 14 | [clock] 15 | interval=1000 16 | label-format=%H:%M 17 | tooltip-format=%Y/%m/%d 18 | widget-layout=0044 19 | 20 | [weather] 21 | url=https://wttr.in/?format=j1 22 | unit=f 23 | 24 | [hyprland] 25 | character-limit=128 26 | 27 | [volume] 28 | show-label=false 29 | widget-layout=0441 30 | 31 | [network] 32 | show-label=false 33 | 34 | [battery] 35 | show-label=false 36 | 37 | [notification] 38 | command=ffplay -nodisp -autoexit /usr/share/sounds/freedesktop/stereo/message.oga &>/dev/null 39 | 40 | [backlight] 41 | path= 42 | show-icon=true 43 | show-label=true 44 | widget-layout=0341 45 | 46 | [menu] 47 | show-icon=true 48 | show-label=false 49 | icon-name=start-here 50 | label-text=Applications 51 | 52 | [taskbar] 53 | text-length=14 54 | icon-size=32 55 | 56 | [mpris] 57 | show-icon=true 58 | show-label=true 59 | album-rounding=10 60 | widget-layout=0142 61 | 62 | [cellular] 63 | show-icon=true 64 | show-label=false 65 | -------------------------------------------------------------------------------- /events.css: -------------------------------------------------------------------------------- 1 | #sysbar_overlay .event_christmas calendar > grid { 2 | box-shadow: 0px 10px 10px -10px inset black; 3 | background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 1px, transparent 1px) 0px 0px; 4 | background-color: rgba(20, 20, 23, 1); 5 | background-repeat: repeat; 6 | background-size: 32px 32px; 7 | animation: snow 10s linear infinite; 8 | border-bottom: 1px solid @borders; 9 | } 10 | 11 | #sysbar_overlay .event_christmas calendar .day-number { 12 | color: rgba(255, 255, 255, 0.85); 13 | } 14 | #sysbar_overlay .event_christmas calendar .other-month { 15 | color: rgba(255, 255, 255, 0.3); 16 | } 17 | 18 | @keyframes snow { 19 | 0% { 20 | background-position: 5px 5px; 21 | } 22 | 23 | 100% { 24 | background-position: 50px 100px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/System64fumo/sysbar/7040925b06617aabd9f8837fd4f9370bd45ae4c8/preview.jpg -------------------------------------------------------------------------------- /proto/wlr-foreign-toplevel-management-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Ilia Bozhinov 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | The purpose of this protocol is to enable the creation of taskbars 31 | and docks by providing them with a list of opened applications and 32 | letting them request certain actions on them, like maximizing, etc. 33 | 34 | After a client binds the zwlr_foreign_toplevel_manager_v1, each opened 35 | toplevel window will be sent via the toplevel event 36 | 37 | 38 | 39 | 40 | This event is emitted whenever a new toplevel window is created. It 41 | is emitted for all toplevels, regardless of the app that has created 42 | them. 43 | 44 | All initial details of the toplevel(title, app_id, states, etc.) will 45 | be sent immediately after this event via the corresponding events in 46 | zwlr_foreign_toplevel_handle_v1. 47 | 48 | 49 | 50 | 51 | 52 | 53 | Indicates the client no longer wishes to receive events for new toplevels. 54 | However the compositor may emit further toplevel_created events, until 55 | the finished event is emitted. 56 | 57 | The client must not send any more requests after this one. 58 | 59 | 60 | 61 | 62 | 63 | This event indicates that the compositor is done sending events to the 64 | zwlr_foreign_toplevel_manager_v1. The server will destroy the object 65 | immediately after sending this request, so it will become invalid and 66 | the client should free any resources associated with it. 67 | 68 | 69 | 70 | 71 | 72 | 73 | A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel 74 | window. Each app may have multiple opened toplevels. 75 | 76 | Each toplevel has a list of outputs it is visible on, conveyed to the 77 | client with the output_enter and output_leave events. 78 | 79 | 80 | 81 | 82 | This event is emitted whenever the title of the toplevel changes. 83 | 84 | 85 | 86 | 87 | 88 | 89 | This event is emitted whenever the app-id of the toplevel changes. 90 | 91 | 92 | 93 | 94 | 95 | 96 | This event is emitted whenever the toplevel becomes visible on 97 | the given output. A toplevel may be visible on multiple outputs. 98 | 99 | 100 | 101 | 102 | 103 | 104 | This event is emitted whenever the toplevel stops being visible on 105 | the given output. It is guaranteed that an entered-output event 106 | with the same output has been emitted before this event. 107 | 108 | 109 | 110 | 111 | 112 | 113 | Requests that the toplevel be maximized. If the maximized state actually 114 | changes, this will be indicated by the state event. 115 | 116 | 117 | 118 | 119 | 120 | Requests that the toplevel be unmaximized. If the maximized state actually 121 | changes, this will be indicated by the state event. 122 | 123 | 124 | 125 | 126 | 127 | Requests that the toplevel be minimized. If the minimized state actually 128 | changes, this will be indicated by the state event. 129 | 130 | 131 | 132 | 133 | 134 | Requests that the toplevel be unminimized. If the minimized state actually 135 | changes, this will be indicated by the state event. 136 | 137 | 138 | 139 | 140 | 141 | Request that this toplevel be activated on the given seat. 142 | There is no guarantee the toplevel will be actually activated. 143 | 144 | 145 | 146 | 147 | 148 | 149 | The different states that a toplevel can have. These have the same meaning 150 | as the states with the same names defined in xdg-toplevel 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 162 | is created and each time the toplevel state changes, either because of a 163 | compositor action or because of a request in this protocol. 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | This event is sent after all changes in the toplevel state have been 172 | sent. 173 | 174 | This allows changes to the zwlr_foreign_toplevel_handle_v1 properties 175 | to be seen as atomic, even if they happen via multiple events. 176 | 177 | 178 | 179 | 180 | 181 | Send a request to the toplevel to close itself. The compositor would 182 | typically use a shell-specific method to carry out this request, for 183 | example by sending the xdg_toplevel.close event. However, this gives 184 | no guarantees the toplevel will actually be destroyed. If and when 185 | this happens, the zwlr_foreign_toplevel_handle_v1.closed event will 186 | be emitted. 187 | 188 | 189 | 190 | 191 | 192 | The rectangle of the surface specified in this request corresponds to 193 | the place where the app using this protocol represents the given toplevel. 194 | It can be used by the compositor as a hint for some operations, e.g 195 | minimizing. The client is however not required to set this, in which 196 | case the compositor is free to decide some default value. 197 | 198 | If the client specifies more than one rectangle, only the last one is 199 | considered. 200 | 201 | The dimensions are given in surface-local coordinates. 202 | Setting width=height=0 removes the already-set rectangle. 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 215 | 216 | 217 | 218 | 219 | This event means the toplevel has been destroyed. It is guaranteed there 220 | won't be any more events for this zwlr_foreign_toplevel_handle_v1. The 221 | toplevel itself becomes inert so any requests will be ignored except the 222 | destroy request. 223 | 224 | 225 | 226 | 227 | 228 | Destroys the zwlr_foreign_toplevel_handle_v1 object. 229 | 230 | This request should be called either when the client does not want to 231 | use the toplevel anymore or after the closed event to finalize the 232 | destruction of the object. 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | Requests that the toplevel be fullscreened on the given output. If the 241 | fullscreen state and/or the outputs the toplevel is visible on actually 242 | change, this will be indicated by the state and output_enter/leave 243 | events. 244 | 245 | The output parameter is only a hint to the compositor. Also, if output 246 | is NULL, the compositor should decide which output the toplevel will be 247 | fullscreened on, if at all. 248 | 249 | 250 | 251 | 252 | 253 | 254 | Requests that the toplevel be unfullscreened. If the fullscreen state 255 | actually changes, this will be indicated by the state event. 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | This event is emitted whenever the parent of the toplevel changes. 264 | 265 | No event is emitted when the parent handle is destroyed by the client. 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Build time configuration Description 4 | #define CONFIG_RUNTIME // Allow the use of runtime arguments 5 | #define CONFIG_FILE // Allow the use of a config file 6 | #define MODULE_CLOCK // Include the clock module 7 | #define MODULE_WEATHER // Include the weather module 8 | #define MODULE_TRAY // Include the tray module 9 | #define MODULE_HYPRLAND // Include the tray module 10 | #define MODULE_VOLUME // Include the volume module 11 | #define MODULE_NETWORK // Include the network module 12 | #define MODULE_BATTERY // Include the battery module 13 | #define MODULE_NOTIFICATION // Include the notifications module 14 | #define MODULE_PERFORMANCE // Include the performance module 15 | #define MODULE_TASKBAR // Include the taskbar module 16 | #define MODULE_BACKLIGHT // Include the backlight module 17 | #define MODULE_MPRIS // Include the mpris module 18 | #define MODULE_BLUETOOTH // Include the bluetooth module 19 | #define MODULE_CONTROLS // Include the controls module (technically widget only) 20 | #define MODULE_CELLULAR // Include the cellular module 21 | #define MODULE_MENU // Include the menu module 22 | #define FEATURE_WIRELESS // Support for wireless networks 23 | 24 | // TODO: Re-Add a fallback/user defined config 25 | // Either that or add a check that requires one of the config options 26 | -------------------------------------------------------------------------------- /src/config_parser.cpp: -------------------------------------------------------------------------------- 1 | #include "config_parser.hpp" 2 | #include 3 | #include 4 | 5 | config_parser::config_parser(const std::string& filename) { 6 | std::ifstream file(filename); 7 | std::string line; 8 | std::string current_section; 9 | 10 | available = file.is_open(); 11 | 12 | if (available) { 13 | while (std::getline(file, line)) { 14 | line = trim(line); 15 | 16 | if (line.empty() || line[0] == ';' || line[0] == '#') { 17 | continue; 18 | } 19 | else if (line[0] == '[' && line[line.size() - 1] == ']') { 20 | current_section = line.substr(1, line.size() - 2); 21 | } 22 | else { 23 | size_t delimiter_pos = line.find('='); 24 | if (delimiter_pos != std::string::npos) { 25 | std::string key = trim(line.substr(0, delimiter_pos)); 26 | std::string value = trim(line.substr(delimiter_pos + 1)); 27 | data[current_section][key] = value; 28 | } 29 | } 30 | } 31 | file.close(); 32 | } 33 | else { 34 | std::fprintf(stderr, "Unable to open file: %s\n", filename.c_str()); 35 | } 36 | } 37 | 38 | std::string config_parser::trim(const std::string& str) { 39 | const size_t first = str.find_first_not_of(' '); 40 | if (std::string::npos == first) { 41 | return str; 42 | } 43 | const size_t last = str.find_last_not_of(' '); 44 | return str.substr(first, (last - first + 1)); 45 | } 46 | -------------------------------------------------------------------------------- /src/config_parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // INI parser 6 | class config_parser { 7 | public: 8 | config_parser(const std::string&); 9 | std::map> data; 10 | bool available; 11 | 12 | private: 13 | std::string trim(const std::string&); 14 | }; 15 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main.hpp" 2 | #include "config_parser.hpp" 3 | #include "git_info.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | void load_libsysbar() { 11 | void* handle = dlopen("libsysbar.so", RTLD_LAZY); 12 | if (!handle) { 13 | std::fprintf(stderr, "Cannot open library: %s\n", dlerror()); 14 | exit(1); 15 | } 16 | 17 | sysbar_create_ptr = (sysbar_create_func)dlsym(handle, "sysbar_create"); 18 | sysbar_handle_signal_ptr = (sysbar_handle_signal_func)dlsym(handle, "sysbar_signal"); 19 | 20 | if (!sysbar_create_ptr) { 21 | std::fprintf(stderr, "Cannot load symbols: %s\n", dlerror()); 22 | dlclose(handle); 23 | exit(1); 24 | } 25 | } 26 | 27 | void handle_signal(int signum) { 28 | sysbar_handle_signal_ptr(win, signum); 29 | } 30 | 31 | int main(int argc, char* argv[]) { 32 | #ifdef CONFIG_FILE 33 | std::string config_path; 34 | std::map> config; 35 | std::map> config_usr; 36 | 37 | bool cfg_sys = std::filesystem::exists("/usr/share/sys64/bar/config.conf"); 38 | bool cfg_sys_local = std::filesystem::exists("/usr/local/share/sys64/bar/config.conf"); 39 | bool cfg_usr = std::filesystem::exists(std::string(getenv("HOME")) + "/.config/sys64/bar/config.conf"); 40 | 41 | // Load default config 42 | if (cfg_sys) 43 | config_path = "/usr/share/sys64/bar/config.conf"; 44 | else if (cfg_sys_local) 45 | config_path = "/usr/local/share/sys64/bar/config.conf"; 46 | else 47 | std::fprintf(stderr, "No default config found, Things will get funky!\n"); 48 | 49 | config = config_parser(config_path).data; 50 | 51 | // Load user config 52 | if (cfg_usr) 53 | config_path = std::string(getenv("HOME")) + "/.config/sys64/bar/config.conf"; 54 | else 55 | std::fprintf(stderr, "No user config found\n"); 56 | 57 | config_usr = config_parser(config_path).data; 58 | 59 | // Merge configs 60 | for (const auto& [key, nested_map] : config_usr) 61 | for (const auto& [inner_key, inner_value] : nested_map) 62 | config[key][inner_key] = inner_value; 63 | 64 | // Sanity check 65 | if (!(cfg_sys || cfg_sys_local || cfg_usr)) { 66 | std::fprintf(stderr, "No config available, Something ain't right here."); 67 | return 1; 68 | } 69 | #endif 70 | 71 | // TODO: Consider using -- arguments 72 | // TODO: Add support for literally every config via runtime 73 | // TODO: Add more help options 74 | 75 | // Read launch arguments 76 | #ifdef CONFIG_RUNTIME 77 | while (true) { 78 | switch(getopt(argc, argv, "p:s:c:e:S:Vm:d:vh")) { 79 | case 'p': 80 | config["main"]["position"] = optarg; 81 | continue; 82 | 83 | case 's': 84 | config["main"]["modules-start"] = optarg; 85 | continue; 86 | 87 | case 'c': 88 | config["main"]["modules-center"] = optarg; 89 | continue; 90 | 91 | case 'e': 92 | config["main"]["modules-end"] = optarg; 93 | continue; 94 | 95 | case 'S': 96 | config["main"]["size"] = optarg; 97 | continue; 98 | 99 | case 'V': 100 | config["main"]["verbose"] = "true"; 101 | continue; 102 | 103 | case 'm': 104 | config["main"]["main-monitor"] = optarg; 105 | continue; 106 | 107 | case 'v': 108 | std::printf("Commit: %s\n", GIT_COMMIT_MESSAGE); 109 | std::printf("Date: %s\n", GIT_COMMIT_DATE); 110 | return 0; 111 | 112 | case 'h': 113 | default : 114 | std::printf("usage:\n"); 115 | std::printf(" sysbar [argument...]:\n\n"); 116 | std::printf("arguments:\n"); 117 | std::printf(" -p Set position\n"); 118 | std::printf(" -s Set start modules\n"); 119 | std::printf(" -c Set center modules\n"); 120 | std::printf(" -e Set end modules\n"); 121 | std::printf(" -S Set bar size\n"); 122 | std::printf(" -V Be more verbose\n"); 123 | std::printf(" -m Set primary monitor\n"); 124 | std::printf(" -v Prints version info\n"); 125 | std::printf(" -h Show this help message\n"); 126 | return 0; 127 | 128 | case -1: 129 | break; 130 | } 131 | 132 | break; 133 | } 134 | #endif 135 | 136 | // Load the application 137 | Glib::RefPtr app = Gtk::Application::create("funky.sys64.sysbar"); 138 | app->hold(); 139 | 140 | load_libsysbar(); 141 | win = sysbar_create_ptr(config); 142 | 143 | // Catch signals 144 | signal(SIGUSR1, handle_signal); 145 | signal(SIGUSR2, handle_signal); 146 | signal(SIGRTMIN, handle_signal); 147 | 148 | return app->run(); 149 | } 150 | -------------------------------------------------------------------------------- /src/main.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.hpp" 3 | #include 4 | #include 5 | 6 | class sysbar {}; 7 | sysbar* win; 8 | 9 | typedef sysbar* (*sysbar_create_func)(const std::map>&); 10 | sysbar_create_func sysbar_create_ptr; 11 | typedef void (*sysbar_handle_signal_func)(sysbar*, int); 12 | sysbar_handle_signal_func sysbar_handle_signal_ptr; 13 | -------------------------------------------------------------------------------- /src/module.cpp: -------------------------------------------------------------------------------- 1 | #include "module.hpp" 2 | 3 | #include 4 | #include 5 | 6 | module::module(sysbar* window, const bool& icon_on_start) : win(window) { 7 | // TODO: Read config to see if the icon should appear before or after 8 | // the label. 9 | 10 | // TODO: Read config to see if the label should be displayed for the module. 11 | 12 | // Initialization 13 | get_style_context()->add_class("module"); 14 | append(label_info); 15 | set_cursor(Gdk::Cursor::create("pointer")); 16 | 17 | // Set orientation 18 | if (win->position % 2) { 19 | Gtk::Orientation orientation = Gtk::Orientation::VERTICAL; 20 | set_orientation(orientation); 21 | } 22 | 23 | if (icon_on_start) { 24 | prepend(image_icon); 25 | if (win->position % 2) 26 | label_info.set_margin_bottom(win->size / 3); 27 | else 28 | label_info.set_margin_end(win->size / 3); 29 | } 30 | else { 31 | append(image_icon); 32 | if (win->position % 2) 33 | label_info.set_margin_top(win->size / 3); 34 | else 35 | label_info.set_margin_start(win->size / 3); 36 | } 37 | 38 | // TODO: add user customizable margins 39 | image_icon.set_size_request(win->size, win->size); 40 | } 41 | -------------------------------------------------------------------------------- /src/module.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.hpp" 3 | #include "window.hpp" 4 | 5 | #include 6 | #include 7 | 8 | class module : public Gtk::Box { 9 | public: 10 | module(sysbar*, const bool& = true); 11 | Gtk::Label label_info; 12 | Gtk::Image image_icon; 13 | sysbar* win; 14 | 15 | private: 16 | void on_dispatcher(); 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/backlight.cpp: -------------------------------------------------------------------------------- 1 | #include "backlight.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | module_backlight::module_backlight(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 9 | get_style_context()->add_class("module_backlight"); 10 | image_icon.set_from_icon_name("brightness-display-symbolic"); 11 | brightness_icons[0] = "display-brightness-low-symbolic"; 12 | brightness_icons[1] = "display-brightness-medium-symbolic"; 13 | brightness_icons[2] = "display-brightness-high-symbolic"; 14 | 15 | std::string cfg_bl_path = win->config_main["backlight"]["path"]; 16 | if (!cfg_bl_path.empty()) 17 | backlight_path = cfg_bl_path; 18 | 19 | if (win->config_main["backlight"]["show-icon"] != "true") { 20 | image_icon.hide(); 21 | label_info.set_margin_end(win->size / 3); 22 | } 23 | 24 | if (win->config_main["backlight"]["show-label"] != "true") 25 | label_info.hide(); 26 | 27 | std::string cfg_layout = win->config_main["backlight"]["widget-layout"]; 28 | if (!cfg_layout.empty()) { 29 | widget_layout.clear(); 30 | for (char c : cfg_layout) { 31 | widget_layout.push_back(c - '0'); 32 | } 33 | } 34 | 35 | // Setup 36 | get_backlight_path(backlight_path); 37 | if (backlight_path.empty()) // TODO: Maybe replace this with a test function? 38 | return; 39 | 40 | brightness = get_brightness(); 41 | update_info(); 42 | setup_widget(); 43 | 44 | // Listen for changes 45 | dispatcher_callback.connect(sigc::mem_fun(*this, &module_backlight::update_info)); 46 | 47 | std::thread([&]() { 48 | int inotify_fd = inotify_init(); 49 | inotify_add_watch(inotify_fd, backlight_path.c_str(), IN_MODIFY); 50 | 51 | int last_brightness = get_brightness(); 52 | char buffer[1024]; 53 | 54 | while (true) { 55 | ssize_t ret = read(inotify_fd, buffer, 1024); 56 | (void)ret; // Return value does not matter 57 | 58 | brightness = get_brightness(); 59 | if (brightness == last_brightness) 60 | break; 61 | 62 | last_brightness = brightness; 63 | dispatcher_callback.emit(); 64 | } 65 | }).detach(); 66 | } 67 | 68 | void module_backlight::update_info() { 69 | label_info.set_text(std::to_string(brightness)); 70 | image_widget_icon.set_from_icon_name(brightness_icons[brightness / 35]); 71 | // TODO: Prevent this from changing if currently being dragged 72 | //scale_backlight.set_value(brightness); 73 | } 74 | 75 | void module_backlight::on_scale_brightness_change() { 76 | double scale_val_db = scale_backlight.get_value(); 77 | int scale_val = (int)scale_val_db; 78 | if (scale_val == brightness) 79 | return; 80 | 81 | // Probably not ideal to open and close the file every time.. 82 | FILE* backlight_file = fopen((backlight_path + "/brightness").c_str(), "w"); 83 | fprintf(backlight_file, "%d\n", scale_val); 84 | fclose(backlight_file); 85 | 86 | image_widget_icon.set_from_icon_name(brightness_icons[(scale_val_db / max_brightness) * 100.0 / 35]); 87 | } 88 | 89 | void module_backlight::setup_widget() { 90 | Gtk::Box *box_widget = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); 91 | 92 | box_widget->get_style_context()->add_class("widget_backlight"); 93 | image_widget_icon.set_from_icon_name("brightness-display-symbolic"); 94 | image_widget_icon.set_pixel_size(16); 95 | 96 | scale_backlight.set_hexpand(true); 97 | scale_backlight.set_vexpand(true); 98 | scale_backlight.set_value(brightness_literal); 99 | 100 | scale_backlight.signal_value_changed().connect(sigc::mem_fun(*this, &module_backlight::on_scale_brightness_change)); 101 | 102 | if (widget_layout[2] < widget_layout[3]) { // Vertical layout 103 | box_widget->set_orientation(Gtk::Orientation::VERTICAL); 104 | scale_backlight.set_orientation(Gtk::Orientation::VERTICAL); 105 | box_widget->append(scale_backlight); 106 | box_widget->append(image_widget_icon); 107 | scale_backlight.set_inverted(true); 108 | } 109 | else { // Horizontal layout 110 | box_widget->append(image_widget_icon); 111 | box_widget->append(scale_backlight); 112 | } 113 | 114 | win->grid_widgets_end.attach(*box_widget, widget_layout[0], widget_layout[1], widget_layout[2], widget_layout[3]); 115 | } 116 | 117 | void module_backlight::get_backlight_path(const std::string& custom_backlight_path) { 118 | if (custom_backlight_path != "") { 119 | backlight_path = custom_backlight_path; 120 | return; 121 | } 122 | std::string path = "/sys/class/backlight/"; 123 | for (const auto& entry : std::filesystem::directory_iterator(path)) { 124 | if (std::filesystem::is_directory(entry.path())) { 125 | backlight_path = entry.path(); 126 | return; 127 | } 128 | } 129 | } 130 | 131 | int module_backlight::get_brightness() { 132 | std::lock_guard lock(brightness_mutex); 133 | std::ifstream brightness_file(backlight_path + "/brightness"); 134 | std::ifstream max_brightness_file(backlight_path + "/max_brightness"); 135 | brightness_file >> brightness_literal; 136 | max_brightness_file >> max_brightness; 137 | 138 | scale_backlight.set_range(0, max_brightness); 139 | return (brightness_literal / max_brightness) * 100; 140 | } 141 | -------------------------------------------------------------------------------- /src/modules/backlight.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_BACKLIGHT 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class module_backlight : public module { 11 | public: 12 | module_backlight(sysbar*, const bool&); 13 | 14 | private: 15 | std::ofstream brightness_file; 16 | std::ifstream max_brightness_file; 17 | int brightness; 18 | int max_brightness; 19 | double brightness_literal; 20 | int inotify_fd; 21 | std::string backlight_path; 22 | std::mutex brightness_mutex; 23 | std::map brightness_icons; 24 | std::vector widget_layout = {0, 3, 4, 1}; 25 | 26 | Gtk::Scale scale_backlight; 27 | Gtk::Image image_widget_icon; 28 | Glib::Dispatcher dispatcher_callback; 29 | 30 | void update_info(); 31 | void on_scale_brightness_change(); 32 | void get_backlight_path(const std::string&); 33 | int get_brightness(); 34 | void setup_widget(); 35 | }; 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /src/modules/battery.cpp: -------------------------------------------------------------------------------- 1 | #include "battery.hpp" 2 | 3 | #include 4 | 5 | module_battery::module_battery(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 6 | get_style_context()->add_class("module_battery"); 7 | image_icon.set_from_icon_name("battery-missing-symbolic"); // Fallback 8 | label_info.set_text("0"); // Fallback 9 | 10 | if (win->config_main["battery"]["show-label"] != "true") 11 | label_info.hide(); 12 | 13 | setup(); 14 | setup_control(); 15 | } 16 | 17 | void module_battery::setup() { 18 | proxy = Gio::DBus::Proxy::create_sync( 19 | Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM), 20 | "org.freedesktop.UPower", 21 | "/org/freedesktop/UPower/devices/DisplayDevice", 22 | "org.freedesktop.UPower.Device"); 23 | 24 | proxy->signal_properties_changed().connect( 25 | sigc::mem_fun(*this, &module_battery::on_properties_changed)); 26 | 27 | // Trigger a manual update 28 | update_info("IconName"); 29 | update_info("UpdateTime"); 30 | } 31 | 32 | void module_battery::on_properties_changed( 33 | const Gio::DBus::Proxy::MapChangedProperties &properties, 34 | const std::vector &invalidated) { 35 | 36 | for (const auto& prop : properties) { 37 | update_info(prop.first); 38 | } 39 | } 40 | 41 | void module_battery::update_info(const std::string& property) { 42 | // TODO: Check for more stuff 43 | 44 | if (property == "IconName") { 45 | Glib::Variant icon_name; 46 | proxy->get_cached_property(icon_name, "IconName"); 47 | image_icon.set_from_icon_name(icon_name.get()); 48 | } 49 | 50 | else if (property == "UpdateTime") { 51 | Glib::Variant charge; 52 | proxy->get_cached_property(charge, "Percentage"); 53 | label_info.set_text(std::to_string((int)charge.get())); 54 | } 55 | } 56 | 57 | #ifdef MODULE_CONTROLS 58 | void module_battery::setup_control() { 59 | if (!win->box_controls) 60 | return; 61 | 62 | auto container = static_cast(win->box_controls); 63 | control_battery = Gtk::make_managed(win, "battery-level-100-symbolic", true, "battery"); 64 | container->flowbox_controls.append(*control_battery); 65 | 66 | // TODO: Clicking on the main button should trigger low power mode 67 | // Additionaly controls for battery management (For devices that support it)- 68 | // should be added. 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /src/modules/battery.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_BATTERY 4 | #include "controls.hpp" 5 | 6 | #include 7 | 8 | class module_battery : public module { 9 | public: 10 | module_battery(sysbar*, const bool&); 11 | 12 | private: 13 | Glib::RefPtr proxy; 14 | 15 | #ifdef MODULE_CONTROLS 16 | control* control_battery; 17 | void setup_control(); 18 | #endif 19 | 20 | void setup(); 21 | void on_properties_changed( 22 | const Gio::DBus::Proxy::MapChangedProperties&, 23 | const std::vector&); 24 | void update_info(const std::string&); 25 | }; 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /src/modules/bluetooth.cpp: -------------------------------------------------------------------------------- 1 | #include "bluetooth.hpp" 2 | #include 3 | #include 4 | 5 | module_bluetooth::module_bluetooth(sysbar *window, const bool &icon_on_start) : module(window, icon_on_start) { 6 | get_style_context()->add_class("module_bluetooth"); 7 | label_info.hide(); 8 | 9 | if (!test()) { 10 | // TODO: Consider adding error codes or error messages, This is too vague. 11 | std::printf("Bluetooth: Some errors were found, disabling..\n"); 12 | image_icon.hide(); 13 | return; 14 | } 15 | 16 | auto connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM); 17 | 18 | // Get initial data 19 | auto om_proxy = Gio::DBus::Proxy::create_sync( 20 | connection, 21 | "org.bluez", 22 | "/", 23 | "org.freedesktop.DBus.ObjectManager"); 24 | 25 | std::vector args_vector; 26 | auto args = Glib::VariantContainerBase::create_tuple(args_vector); 27 | auto result = om_proxy->call_sync("GetManagedObjects", args); 28 | auto result_cb = Glib::VariantBase::cast_dynamic(result); 29 | auto result_base = result_cb.get_child(0); 30 | 31 | extract_data(result_base); 32 | default_adapter = adapters.front(); 33 | 34 | // TODO: This should probably use the first adapter's path- 35 | // rather than using a hardcoded path.. 36 | 37 | // Setup change detection 38 | prop_proxy = Gio::DBus::Proxy::create_sync( 39 | connection, 40 | "org.bluez", 41 | "/org/bluez/hci0", 42 | "org.freedesktop.DBus.Properties"); 43 | 44 | prop_proxy->signal_signal().connect( 45 | sigc::mem_fun(*this, &module_bluetooth::on_properties_changed)); 46 | 47 | #ifdef MODULE_CONTROLS 48 | setup_control(); 49 | #endif 50 | update_info("PowerState"); 51 | } 52 | 53 | bool module_bluetooth::test() { 54 | bool bluetoothd_running = false; 55 | bool adapter_available = false; 56 | 57 | // Check if bluetooth service is running 58 | DIR* dir = opendir("/proc"); 59 | struct dirent* entry; 60 | while ((entry = readdir(dir)) != nullptr) { 61 | if (entry->d_type == DT_DIR && std::all_of(entry->d_name, entry->d_name + strlen(entry->d_name), ::isdigit)) { 62 | std::string pid = entry->d_name; 63 | std::string comm_file = "/proc/" + pid + "/comm"; 64 | 65 | std::ifstream comm_stream(comm_file); 66 | if (comm_stream.is_open()) { 67 | std::string comm; 68 | comm_stream >> comm; 69 | 70 | if (comm == "bluetoothd") { 71 | bluetoothd_running = true; 72 | break; 73 | } 74 | 75 | } 76 | } 77 | } 78 | closedir(dir); 79 | 80 | // Check if an adapter is available 81 | std::filesystem::path bluetooth_path("/sys/class/bluetooth/"); 82 | for (const auto& entry : std::filesystem::directory_iterator(bluetooth_path)) { 83 | if (std::filesystem::is_directory(entry.status())) 84 | adapter_available = true; 85 | } 86 | 87 | return bluetoothd_running && adapter_available; 88 | } 89 | 90 | void module_bluetooth::on_properties_changed( 91 | const Glib::ustring& sender_name, 92 | const Glib::ustring& signal_name, 93 | const Glib::VariantContainerBase& parameters) { 94 | 95 | std::printf("Bluetooth properties updated\n"); 96 | Glib::VariantIter iter(parameters); 97 | Glib::VariantBase child; 98 | iter.next_value(child); 99 | Glib::ustring adp_interface = Glib::VariantBase::cast_dynamic>(child).get(); 100 | iter.next_value(child); 101 | 102 | // Other adapters do not matter 103 | if (std::string(adp_interface.c_str()) != default_adapter.interface) 104 | return; 105 | 106 | auto variant_dict = Glib::VariantBase::cast_dynamic>>(child); 107 | std::map map_hints = variant_dict.get(); 108 | for (const auto& [key, value] : map_hints) { 109 | std::printf("Property changed: %s\n", key.c_str()); 110 | if (key == "PowerState") { 111 | std::string powered = Glib::VariantBase::cast_dynamic>(value).get(); 112 | default_adapter.powered = (powered == "on"); 113 | } 114 | update_info(key); 115 | } 116 | } 117 | 118 | void module_bluetooth::extract_data(const Glib::VariantBase& variant_base) { 119 | auto variant = Glib::VariantBase::cast_dynamic>>>>(variant_base); 120 | auto data_map = variant.get(); 121 | 122 | for (const auto& [object_path, interface_map] : data_map) { 123 | if (win->verbose) 124 | std::printf("Object Path: %s\n", object_path.c_str()); 125 | 126 | for (const auto& [interface_name, property_map] : interface_map) { 127 | if (win->verbose) 128 | std::printf(" Interface: %s\n", interface_name.c_str()); 129 | 130 | if (interface_name.find("org.bluez.Adapter") == 0) { 131 | adapter adp; 132 | adp.interface = interface_name; 133 | adp.path = object_path; 134 | 135 | for (const auto& [property_name, value] : property_map) { 136 | if (property_name == "Alias") 137 | adp.alias = value.print(); 138 | else if (property_name == "Discoverable") 139 | adp.discoverable = (value.print() == "true"); 140 | else if (property_name == "Discovering") 141 | adp.discovering = (value.print() == "true"); 142 | else if (property_name == "Name") 143 | adp.name = value.print(); 144 | else if (property_name == "Pairable") 145 | adp.pairable = (value.print() == "true"); 146 | else if (property_name == "Powered") 147 | adp.powered = (value.print() == "true"); 148 | } 149 | adapters.push_back(adp); 150 | } 151 | else if (interface_name.find("org.bluez.Device") == 0) { 152 | device dev; 153 | dev.path = object_path; 154 | 155 | for (const auto& [property_name, value] : property_map) { 156 | if (property_name == "Blocked") 157 | dev.blocked = (value.print() == "true"); 158 | else if (property_name == "Connected") 159 | dev.connected = (value.print() == "true"); 160 | else if (property_name == "Icon") 161 | dev.icon = value.print(); 162 | else if (property_name == "Name") 163 | dev.name = value.print(); 164 | else if (property_name == "Paired") 165 | dev.paired = (value.print() == "true"); 166 | else if (property_name == "Trusted") 167 | dev.trusted = (value.print() == "true"); 168 | } 169 | devices.push_back(dev); 170 | } 171 | 172 | if (win->verbose) 173 | for (const auto& [property_name, value] : property_map) { 174 | std::printf(" Property: %s = %s\n", property_name.c_str(), value.print().c_str()); 175 | } 176 | } 177 | } 178 | } 179 | 180 | void module_bluetooth::update_info(const std::string& property) { 181 | if (property == "PowerState") { 182 | std::string icon; 183 | if (default_adapter.powered) 184 | icon = "bluetooth-active-symbolic"; 185 | else 186 | icon = "bluetooth-disabled-symbolic"; 187 | 188 | image_icon.set_from_icon_name(icon); 189 | 190 | if (!win->box_controls) 191 | return; 192 | 193 | control_bluetooth->button_action.set_icon_name(icon); 194 | } 195 | } 196 | 197 | #ifdef MODULE_CONTROLS 198 | void module_bluetooth::setup_control() { 199 | if (!win->box_controls) 200 | return; 201 | 202 | auto container = static_cast(win->box_controls); 203 | control_bluetooth = Gtk::make_managed(win, "bluetooth-active-symbolic", true, "bluetooth"); 204 | container->flowbox_controls.append(*control_bluetooth); 205 | } 206 | #endif 207 | -------------------------------------------------------------------------------- /src/modules/bluetooth.hpp: -------------------------------------------------------------------------------- 1 | #include "../module.hpp" 2 | #include "controls.hpp" 3 | 4 | #include 5 | #include 6 | 7 | struct device { 8 | std::string path; 9 | bool blocked; 10 | bool connected; 11 | std::string icon; 12 | std::string name; 13 | bool paired; 14 | bool trusted; 15 | }; 16 | 17 | struct adapter { 18 | std::string interface; 19 | std::string path; 20 | std::string alias; 21 | bool discoverable; 22 | bool discovering; 23 | std::string name; 24 | bool pairable; 25 | bool powered; 26 | }; 27 | 28 | class module_bluetooth : public module { 29 | public: 30 | module_bluetooth(sysbar*, const bool&); 31 | bool test(); 32 | 33 | private: 34 | Glib::RefPtr prop_proxy; 35 | 36 | std::vector adapters; 37 | std::vector devices; 38 | adapter default_adapter; 39 | 40 | #ifdef MODULE_CONTROLS 41 | control* control_bluetooth; 42 | void setup_control(); 43 | #endif 44 | 45 | void on_properties_changed( 46 | const Glib::ustring&, 47 | const Glib::ustring&, 48 | const Glib::VariantContainerBase&); 49 | void extract_data(const Glib::VariantBase&); 50 | void update_info(const std::string&); 51 | }; 52 | -------------------------------------------------------------------------------- /src/modules/cellular.cpp: -------------------------------------------------------------------------------- 1 | #include "cellular.hpp" 2 | 3 | #include 4 | 5 | module_cellular::module_cellular(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 6 | get_style_context()->add_class("module_cellular"); 7 | image_icon.set_from_icon_name("network-cellular-acquiring-symbolic"); 8 | label_info.set_text("0"); 9 | 10 | 11 | if (win->config_main["cellular"]["show-icon"] != "true") { 12 | image_icon.hide(); 13 | label_info.set_margin_end(win->size / 3); 14 | } 15 | 16 | if (win->config_main["cellular"]["show-label"] != "true") 17 | label_info.hide(); 18 | 19 | setup(); 20 | } 21 | 22 | void module_cellular::setup() { 23 | // Fetch initial data 24 | auto om_proxy = Gio::DBus::Proxy::create_sync( 25 | Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM), 26 | "org.freedesktop.ModemManager1", 27 | "/org/freedesktop/ModemManager1", 28 | "org.freedesktop.DBus.ObjectManager"); 29 | 30 | std::vector args_vector; 31 | auto args = Glib::VariantContainerBase::create_tuple(args_vector); 32 | auto result = om_proxy->call_sync("GetManagedObjects", args); 33 | auto result_cb = Glib::VariantBase::cast_dynamic(result); 34 | auto result_base = result_cb.get_child(0); 35 | extract_data(result_base); 36 | 37 | // Monitor changes 38 | proxy = Gio::DBus::Proxy::create_sync( 39 | Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM), 40 | "org.freedesktop.ModemManager1", 41 | "/org/freedesktop/ModemManager1/Modem/0", 42 | "org.freedesktop.DBus.Properties"); 43 | 44 | proxy->signal_signal().connect( 45 | sigc::mem_fun(*this, &module_cellular::on_properties_changed)); 46 | } 47 | 48 | void module_cellular::on_properties_changed( 49 | const Glib::ustring& sender_name, 50 | const Glib::ustring& signal_name, 51 | const Glib::VariantContainerBase& parameters) { 52 | 53 | Glib::VariantIter iter(parameters); 54 | Glib::VariantBase child; 55 | iter.next_value(child); // Ignore this for now "org.freedesktop.ModemManager1.Modem" 56 | iter.next_value(child); 57 | 58 | auto variant_dict = Glib::VariantBase::cast_dynamic>>(child); 59 | std::map map_hints = variant_dict.get(); 60 | for (const auto& [key, value] : map_hints) { 61 | if (key == "SignalQuality") { 62 | auto tuple = Glib::VariantBase::cast_dynamic(value); 63 | signal = Glib::VariantBase::cast_dynamic>(tuple.get_child(0)).get(); 64 | //uint32_t boolean = Glib::VariantBase::cast_dynamic>(tuple.get_child(1)).get(); 65 | update_info(); 66 | } 67 | } 68 | } 69 | 70 | void module_cellular::update_info() { 71 | label_info.set_text(std::to_string(signal)); 72 | std::string icon; 73 | if (signal > 80) 74 | icon = "network-cellular-signal-excellent-symbolic"; 75 | else if (signal > 60) 76 | icon = "network-cellular-signal-good-symbolic"; 77 | else if (signal > 40) 78 | icon = "network-cellular-signal-ok-symbolic"; 79 | else if (signal > 20) 80 | icon = "network-cellular-signal-weak-symbolic"; 81 | else 82 | icon = "network-cellular-signal-none-symbolic"; 83 | image_icon.set_from_icon_name(icon); 84 | } 85 | 86 | void module_cellular::extract_data(const Glib::VariantBase& variant_base) { 87 | auto variant = Glib::VariantBase::cast_dynamic>>>>(variant_base); 88 | auto data_map = variant.get(); 89 | 90 | for (const auto& [object_path, interface_map] : data_map) { 91 | //std::printf("Object Path: %s\n", object_path.c_str()); 92 | 93 | for (const auto& [interface_name, property_map] : interface_map) { 94 | //std::printf(" Interface: %s\n", interface_name.c_str()); 95 | 96 | if (interface_name == "org.freedesktop.ModemManager1.Modem") { 97 | for (const auto& [property_name, value] : property_map) { 98 | if (property_name == "SignalQuality") { 99 | std::string signal_str; 100 | for (char ch : value.print()) { 101 | if (isdigit(ch)) 102 | signal_str += ch; 103 | } 104 | signal = std::stoi(signal_str); 105 | update_info(); 106 | } 107 | else if (property_name == "AccessTechnologies") { 108 | // TODO: Set network module icon using this 109 | std::string icon = tech_to_icon(std::stoi(value.print())); 110 | } 111 | //std::printf(" Property: %s = %s\n", property_name.c_str(), value.print().c_str()); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | std::string module_cellular::tech_to_icon(const int& tech) { 119 | // 2G 120 | bool GSM = tech & MM_MODEM_ACCESS_TECHNOLOGY_GSM; 121 | bool GPRS = tech & MM_MODEM_ACCESS_TECHNOLOGY_GPRS; 122 | bool EDGE = tech & MM_MODEM_ACCESS_TECHNOLOGY_EDGE; 123 | 124 | // 3G 125 | bool UMTS = tech & MM_MODEM_ACCESS_TECHNOLOGY_UMTS; 126 | bool HSPA = tech & MM_MODEM_ACCESS_TECHNOLOGY_HSPA; 127 | 128 | // 4G 129 | bool LTE = tech & MM_MODEM_ACCESS_TECHNOLOGY_LTE; 130 | 131 | // 5G 132 | bool NR = tech & MM_MODEM_ACCESS_TECHNOLOGY_5GNR; 133 | 134 | // For now the specifics are irrelevant 135 | if (GSM || GPRS || EDGE) // 2G 136 | return "network-cellular-2g-symbolic"; 137 | else if (UMTS || HSPA) // 3G 138 | return "network-cellular-3g-symbolic"; 139 | else if (LTE) // 4G 140 | return "network-cellular-4g-symbolic"; 141 | else if (NR) // 5G 142 | return "network-cellular-5g-symbolic"; 143 | 144 | return "network-cellular-gprs-symbolic"; // Fallback 145 | } 146 | -------------------------------------------------------------------------------- /src/modules/cellular.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_CELLULAR 4 | 5 | #include 6 | 7 | // Pulled from modem manager 8 | enum MMModemAccessTechnology { 9 | MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN = 0, 10 | MM_MODEM_ACCESS_TECHNOLOGY_POTS = 1 << 0, 11 | MM_MODEM_ACCESS_TECHNOLOGY_GSM = 1 << 1, 12 | MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT = 1 << 2, 13 | MM_MODEM_ACCESS_TECHNOLOGY_GPRS = 1 << 3, 14 | MM_MODEM_ACCESS_TECHNOLOGY_EDGE = 1 << 4, 15 | MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5, 16 | MM_MODEM_ACCESS_TECHNOLOGY_HSDPA = 1 << 6, 17 | MM_MODEM_ACCESS_TECHNOLOGY_HSUPA = 1 << 7, 18 | MM_MODEM_ACCESS_TECHNOLOGY_HSPA = 1 << 8, 19 | MM_MODEM_ACCESS_TECHNOLOGY_HSPA_PLUS = 1 << 9, 20 | MM_MODEM_ACCESS_TECHNOLOGY_1XRTT = 1 << 10, 21 | MM_MODEM_ACCESS_TECHNOLOGY_EVDO0 = 1 << 11, 22 | MM_MODEM_ACCESS_TECHNOLOGY_EVDOA = 1 << 12, 23 | MM_MODEM_ACCESS_TECHNOLOGY_EVDOB = 1 << 13, 24 | MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14, 25 | MM_MODEM_ACCESS_TECHNOLOGY_5GNR = 1 << 15, 26 | MM_MODEM_ACCESS_TECHNOLOGY_LTE_CAT_M = 1 << 16, 27 | MM_MODEM_ACCESS_TECHNOLOGY_LTE_NB_IOT = 1 << 17, 28 | MM_MODEM_ACCESS_TECHNOLOGY_ANY = 0xFFFFFFFF, 29 | }; 30 | 31 | class module_cellular : public module { 32 | public: 33 | module_cellular(sysbar*, const bool&); 34 | 35 | private: 36 | Glib::RefPtr proxy; 37 | 38 | uint32_t signal; 39 | 40 | void setup(); 41 | void on_properties_changed( 42 | const Glib::ustring&, 43 | const Glib::ustring&, 44 | const Glib::VariantContainerBase&); 45 | void update_info(); 46 | void extract_data(const Glib::VariantBase&); 47 | std::string tech_to_icon(const int&); 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /src/modules/clock.cpp: -------------------------------------------------------------------------------- 1 | #include "clock.hpp" 2 | #include "../config_parser.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | module_clock::module_clock(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 9 | get_style_context()->add_class("module_clock"); 10 | image_icon.set_from_icon_name("preferences-system-time-symbolic"); 11 | 12 | std::string cfg_label_format = win->config_main["clock"]["label-format"]; 13 | if (!cfg_label_format.empty()) 14 | tooltip_format = cfg_label_format; 15 | 16 | std::string cfg_tooltip_format = win->config_main["clock"]["tooltip-format"]; 17 | if (!cfg_tooltip_format.empty()) 18 | tooltip_format = cfg_tooltip_format; 19 | 20 | std::string cfg_interval = win->config_main["clock"]["interval"]; 21 | if (!cfg_interval.empty()) 22 | interval = std::stoi(cfg_interval); 23 | 24 | std::string cfg_layout = win->config_main["clock"]["widget-layout"]; 25 | if (!cfg_layout.empty()) { 26 | widget_layout.clear(); 27 | for (char c : cfg_layout) { 28 | widget_layout.push_back(c - '0'); 29 | } 30 | } 31 | 32 | update_info(); 33 | Glib::signal_timeout().connect(sigc::mem_fun(*this, &module_clock::update_info), interval); 34 | window->overlay_window.signal_show().connect(sigc::mem_fun(*this, &module_clock::on_overlay_change)); 35 | setup_widget(); 36 | } 37 | 38 | bool module_clock::update_info() { 39 | std::time_t now = std::time(nullptr); 40 | std::tm* local_time = std::localtime(&now); 41 | 42 | char label_buffer[32]; 43 | std::strftime(label_buffer, sizeof(label_buffer), label_format.c_str(), local_time); 44 | label_info.set_text(label_buffer); 45 | 46 | char tooltip_buffer[32]; 47 | std::strftime(tooltip_buffer, sizeof(tooltip_buffer), tooltip_format.c_str() , local_time); 48 | set_tooltip_text(tooltip_buffer); 49 | 50 | return true; 51 | } 52 | 53 | void module_clock::setup_widget() { 54 | Gtk::Box* box_widget = Gtk::make_managed(Gtk::Orientation::VERTICAL); 55 | box_widget->append(calendar); 56 | box_widget->append(revealer_events); 57 | calendar.set_hexpand(true); 58 | label_event.set_xalign(0); 59 | label_event.get_style_context()->add_class("event_label"); 60 | revealer_events.set_child(label_event); 61 | revealer_events.set_transition_type(Gtk::RevealerTransitionType::SLIDE_DOWN); 62 | revealer_events.set_transition_duration(500); 63 | win->grid_widgets_start.attach(*box_widget, widget_layout[0], widget_layout[1], widget_layout[2], widget_layout[3]); 64 | 65 | std::string events_path; 66 | const std::string user_events = std::string(getenv("HOME")) + "/.config/sys64/bar/calendar.conf"; 67 | const std::string system_events = "/usr/share/sys64/bar/calendar.conf"; 68 | 69 | if (std::filesystem::exists(user_events)) 70 | events_path = user_events; 71 | else 72 | events_path = system_events; 73 | 74 | config_parser events(events_path); 75 | calendar_events = events.data; 76 | 77 | calendar.signal_day_selected().connect(sigc::mem_fun(*this, &module_clock::check_for_events)); 78 | calendar.signal_prev_month().connect(sigc::mem_fun(*this, &module_clock::on_calendar_change)); 79 | calendar.signal_next_month().connect(sigc::mem_fun(*this, &module_clock::on_calendar_change)); 80 | calendar.signal_prev_year().connect(sigc::mem_fun(*this, &module_clock::on_calendar_change)); 81 | calendar.signal_next_year().connect(sigc::mem_fun(*this, &module_clock::on_calendar_change)); 82 | on_calendar_change(); 83 | } 84 | 85 | void module_clock::on_calendar_change() { 86 | // TODO: Add event types 87 | auto it = calendar_events.find("events"); 88 | if (it == calendar_events.end()) 89 | return; 90 | 91 | calendar.clear_marks(); 92 | for (const auto& [event_date, event_name] : it->second) { 93 | module_clock::date_time date = parse_date_time(event_date); 94 | 95 | const bool& same_year = date.year == calendar.get_year() || date.year == 0; 96 | const bool& same_month = date.month - 1 == calendar.get_month() || date.month == 0; 97 | if (same_year && same_month) { 98 | calendar.mark_day(date.day); 99 | } 100 | } 101 | 102 | check_for_events(); 103 | } 104 | 105 | void module_clock::check_for_events() { 106 | revealer_events.set_reveal_child(false); 107 | if (!event_class.empty()) { 108 | win->get_style_context()->remove_class(event_class); 109 | win->grid_widgets_start.get_style_context()->remove_class(event_class); 110 | win->grid_widgets_end.get_style_context()->remove_class(event_class); 111 | event_class = ""; 112 | } 113 | 114 | if (!calendar.get_day_is_marked(calendar.get_day())) 115 | return; 116 | auto datetime = calendar.get_date(); 117 | std::string date_str = datetime.format("%Y-%m-%d"); 118 | std::string event = calendar_events["events"][date_str]; 119 | if (event.empty()) 120 | event = calendar_events["events"]["*" + date_str.substr(4)]; 121 | 122 | // What? 123 | if (event.empty()) 124 | return; 125 | 126 | label_event.set_text(event); 127 | revealer_events.set_reveal_child(true); 128 | 129 | // TODO: Check if today is the day of the event 130 | for (char c : event) { 131 | if (std::isalpha(c)) 132 | event_class.push_back(std::tolower(c)); 133 | } 134 | event_class = "event_" + event_class; 135 | win->get_style_context()->add_class(event_class); 136 | win->grid_widgets_start.get_style_context()->add_class(event_class); 137 | win->grid_widgets_end.get_style_context()->add_class(event_class); 138 | } 139 | 140 | module_clock::date_time module_clock::parse_date_time(const std::string& date_str) { 141 | date_time date; 142 | 143 | std::istringstream ss(date_str); 144 | std::string year_str, month_str, day_str; 145 | std::getline(ss, year_str, '-'); 146 | std::getline(ss, month_str, '-'); 147 | std::getline(ss, day_str, ' '); 148 | 149 | if (year_str != "*") 150 | date.year = std::stoi(year_str); 151 | if (month_str != "*") 152 | date.month = std::stoi(month_str); 153 | if (day_str != "*") 154 | date.day = std::stoi(day_str); 155 | 156 | return date; 157 | } 158 | 159 | void module_clock::on_overlay_change() { 160 | // Focus calendar to current day when the overlay is shown 161 | calendar.select_day(Glib::DateTime::create_now_local()); 162 | } 163 | -------------------------------------------------------------------------------- /src/modules/clock.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_CLOCK 4 | 5 | #include 6 | #include 7 | 8 | class module_clock : public module { 9 | public: 10 | module_clock(sysbar*, const bool&); 11 | 12 | private: 13 | struct date_time { 14 | int year = 0; 15 | int month = 0; 16 | int day = 0; 17 | }; 18 | 19 | Gtk::Calendar calendar; 20 | Gtk::Revealer revealer_events; 21 | Gtk::Label label_event; 22 | 23 | int interval = 1000; 24 | std::string label_format = "%H:%M"; 25 | std::string tooltip_format = "%Y/%m/%d"; 26 | std::vector widget_layout = {0, 0, 4, 4}; 27 | std::map> calendar_events; 28 | std::string event_class; 29 | 30 | bool update_info(); 31 | void setup_widget(); 32 | void on_calendar_change(); 33 | void on_overlay_change(); 34 | void check_for_events(); 35 | date_time parse_date_time(const std::string& date_str); 36 | }; 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /src/modules/controls.cpp: -------------------------------------------------------------------------------- 1 | #include "controls.hpp" 2 | 3 | module_controls::module_controls(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 4 | get_style_context()->add_class("widget_controls"); 5 | image_icon.hide(); 6 | label_info.hide(); 7 | append(flowbox_controls); 8 | flowbox_controls.set_orientation(Gtk::Orientation::VERTICAL); 9 | flowbox_controls.set_homogeneous(true); 10 | flowbox_controls.set_hexpand(true); 11 | flowbox_controls.set_max_children_per_line(1); 12 | flowbox_controls.set_selection_mode(Gtk::SelectionMode::NONE); 13 | 14 | win->grid_widgets_end.attach(*this, 0, 0, 4, 1); 15 | } 16 | 17 | control_page::control_page(sysbar* window, const std::string& name) : Gtk::Box(Gtk::Orientation::VERTICAL), box_body(Gtk::Orientation::VERTICAL) { 18 | append(box_header); 19 | append(box_body); 20 | 21 | box_header.append(button_return); 22 | button_return.signal_clicked().connect([window]() { 23 | window->stack_end.set_visible_child("main"); 24 | }); 25 | 26 | // TODO: Detect position 27 | window->stack_end.add(*this, name); 28 | } 29 | 30 | control::control(sysbar* window, const std::string& icon, const bool& extra, const std::string& name) { 31 | get_style_context()->add_class("control"); 32 | append(button_action); 33 | button_action.set_icon_name(icon); 34 | button_action.get_style_context()->add_class("button_action"); 35 | button_action.set_focusable(false); 36 | button_action.set_has_frame(false); 37 | button_action.set_hexpand(true); 38 | 39 | // This is for sub menus 40 | if (extra) { 41 | append(button_expand); 42 | button_action.get_style_context()->add_class("button_expand"); 43 | button_expand.set_focusable(false); 44 | button_expand.set_has_frame(false); 45 | button_expand.set_icon_name("arrow-right"); 46 | page = Gtk::make_managed(window, name); 47 | } 48 | } -------------------------------------------------------------------------------- /src/modules/controls.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_CONTROLS 4 | 5 | #include 6 | #include 7 | 8 | class control_page : public Gtk::Box { 9 | public: 10 | control_page(sysbar* window, const std::string& name); 11 | 12 | Gtk::Box box_header; 13 | Gtk::Box box_body; 14 | Gtk::Button button_return; 15 | }; 16 | 17 | class control : public Gtk::Box { 18 | public: 19 | control(sysbar* window, const std::string& icon, const bool& extra, const std::string& name); 20 | Gtk::Button button_action; 21 | Gtk::Button button_expand; 22 | control_page* page; 23 | }; 24 | 25 | class module_controls : public module { 26 | public: 27 | module_controls(sysbar*, const bool&); 28 | Gtk::FlowBox flowbox_controls; 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/modules/hyprland.cpp: -------------------------------------------------------------------------------- 1 | #include "hyprland.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | module_hyprland::module_hyprland(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start), new_workspace(0), character_limit(128) { 10 | get_style_context()->add_class("module_hyprland"); 11 | image_icon.hide(); 12 | label_info.set_margin_end(win->size / 3); 13 | 14 | 15 | std::string cfg_char_limit = win->config_main["hyprland"]["character-limit"]; 16 | if (!cfg_char_limit.empty()) 17 | character_limit = std::stoi(cfg_char_limit); 18 | 19 | 20 | if (win->position %2 == 0) { 21 | dispatcher.connect(sigc::mem_fun(*this, &module_hyprland::update_info)); 22 | std::thread socket_thread(&module_hyprland::socket_listener, this); 23 | socket_thread.detach(); 24 | } 25 | } 26 | 27 | void module_hyprland::update_info() { 28 | std::lock_guard lock(mutex); 29 | std::string data = data_queue.front(); 30 | data_queue.pop(); 31 | 32 | //std::printf("Data: %s\n", data.c_str()); 33 | 34 | // Create new workspace 35 | if (data.find("createworkspace>>") != std::string::npos) { 36 | new_workspace = std::stoi(data.substr(17,data.find(',') - 12)); 37 | } 38 | 39 | // Delete workspace 40 | if (data.find("destroyworkspace>>") != std::string::npos) { 41 | std::string workspace_id = data.substr(18,data.find(',') - 12); 42 | // TODO: Add delete code 43 | } 44 | 45 | if (data.find("activewindow>>") != std::string::npos) { 46 | std::string active_window_data = data.substr(14); 47 | int pos = active_window_data.find(','); 48 | 49 | std::string active_window = active_window_data.substr(0, pos); 50 | Glib::ustring active_window_title = active_window_data.substr(pos + 1); 51 | 52 | if ((int)active_window_title.size() > character_limit) 53 | active_window_title = active_window_title.substr(0, character_limit + 3) + "..."; 54 | 55 | label_info.set_text(active_window_title); 56 | } 57 | else if (data.find("focusedmon>>") != std::string::npos) { 58 | std::string focused_monitor = data.substr(12,data.find(',') - 12); 59 | int focused_workspace = std::stoi(data.substr(data.find(',') + 1)); 60 | auto mon_it = std::find_if(monitors.begin(), monitors.end(), [focused_monitor](const monitor& mon) { 61 | return mon.name == focused_monitor; 62 | }); 63 | if (mon_it == monitors.end()) { 64 | monitor mon; 65 | mon.name = focused_monitor; 66 | mon.active = true; 67 | monitors.push_back(mon); 68 | 69 | workspace ws; 70 | ws.id = focused_workspace; 71 | ws.active = false; 72 | ws.fullscreen = false; 73 | mon.workspaces.push_back(ws); 74 | } 75 | 76 | for (monitor mon : monitors) { 77 | mon.active = false; 78 | if (mon.name != focused_monitor) 79 | continue; 80 | 81 | // Create a new workspace if needed 82 | if (new_workspace != 0) { 83 | workspace ws; 84 | ws.id = new_workspace; 85 | ws.active = false; 86 | ws.fullscreen = false; 87 | mon.workspaces.push_back(ws); 88 | new_workspace = 0; 89 | } 90 | 91 | for (workspace ws : mon.workspaces) 92 | ws.active = (ws.id == focused_workspace); 93 | } 94 | } 95 | } 96 | 97 | void module_hyprland::socket_listener() { 98 | std::filesystem::path xdg_runtime_dir = std::filesystem::path(getenv("XDG_RUNTIME_DIR")); 99 | std::filesystem::path hyprland_socket = xdg_runtime_dir / "hypr"; 100 | 101 | if (!std::filesystem::exists(hyprland_socket)) { 102 | std::fprintf(stderr, "Socket not found at: %s\n", hyprland_socket.c_str()); 103 | return; 104 | } 105 | 106 | const char* is = getenv("HYPRLAND_INSTANCE_SIGNATURE"); 107 | 108 | if (is == nullptr) { 109 | std::fprintf(stderr, "Hyprland instance signature not found, Is hyprland running?\n"); 110 | return; 111 | } 112 | 113 | std::string instance_signature = is; 114 | 115 | std::filesystem::path socket_path = hyprland_socket / instance_signature / ".socket2.sock"; 116 | 117 | struct sockaddr_un addr; 118 | std::vector buffer(1024); 119 | int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); 120 | memset(&addr, 0, sizeof(addr)); 121 | addr.sun_family = AF_UNIX; 122 | strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1); 123 | 124 | if (connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { 125 | std::fprintf(stderr, "socket connection failed\n"); 126 | close(sockfd); 127 | return; 128 | } 129 | 130 | std::string temp_buff; 131 | 132 | while (true) { 133 | ssize_t numBytes = read(sockfd, buffer.data(), buffer.size()); 134 | if (numBytes < 0) { 135 | std::fprintf(stderr, "Read error\n"); 136 | close(sockfd); 137 | return; 138 | } else if (numBytes == 0) { 139 | std::fprintf(stderr, "Connection closed by peer\n"); 140 | break; 141 | } 142 | 143 | temp_buff.append(buffer.data(), numBytes); 144 | size_t pos = 0; 145 | 146 | // On new line 147 | while ((pos = temp_buff.find('\n')) != std::string::npos) { 148 | std::lock_guard lock(mutex); 149 | data_queue.push(temp_buff.substr(0, pos)); 150 | temp_buff.erase(0, pos + 1); 151 | 152 | dispatcher.emit(); 153 | } 154 | } 155 | 156 | close(sockfd); 157 | } 158 | -------------------------------------------------------------------------------- /src/modules/hyprland.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_HYPRLAND 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | class module_hyprland : public module { 10 | public: 11 | module_hyprland(sysbar*, const bool&); 12 | 13 | private: 14 | struct workspace { 15 | int id; 16 | bool active; 17 | bool fullscreen; 18 | }; 19 | struct monitor { 20 | std::string name; 21 | bool active; 22 | std::vector workspaces; 23 | }; 24 | 25 | int new_workspace; 26 | 27 | Glib::Dispatcher dispatcher; 28 | 29 | int character_limit; 30 | std::queue data_queue; 31 | std::mutex mutex; 32 | std::vector monitors; 33 | 34 | void update_info(); 35 | void socket_listener(); 36 | }; 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /src/modules/menu.cpp: -------------------------------------------------------------------------------- 1 | #include "menu.hpp" 2 | #include "../config_parser.hpp" 3 | 4 | module_menu::module_menu(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 5 | get_style_context()->add_class("module_menu"); 6 | 7 | // TODO: Add option to set other icons maybe even images? 8 | if (win->config_main["menu"]["show-icon"] == "true") 9 | image_icon.set_from_icon_name(win->config_main["menu"]["icon-name"]); 10 | else 11 | image_icon.hide(); 12 | 13 | if (win->config_main["menu"]["show-label"] == "true") 14 | label_info.set_text(win->config_main["menu"]["label-text"]); 15 | else 16 | label_info.hide(); 17 | 18 | // Custom on_clicked handle 19 | gesture_click = Gtk::GestureClick::create(); 20 | gesture_click->set_button(GDK_BUTTON_PRIMARY); 21 | gesture_click->signal_pressed().connect(sigc::mem_fun(*this, &module_menu::on_clicked)); 22 | add_controller(gesture_click); 23 | } 24 | 25 | void module_menu::on_clicked(const int& n_press, const double& x, const double& y) { 26 | // TODO: Add native sysmenu integration if no command is specified 27 | // TODO: Add custom command support 28 | 29 | system("pkill -34 sysmenu || pkill -34 sysshell"); 30 | 31 | // Prevent gestures bellow from triggering 32 | gesture_click->reset(); 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/menu.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_MENU 4 | 5 | #include 6 | 7 | class module_menu : public module { 8 | public: 9 | module_menu(sysbar*, const bool&); 10 | 11 | private: 12 | Glib::RefPtr gesture_click; 13 | void on_clicked(const int&, const double&, const double&); 14 | }; 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /src/modules/mpris.cpp: -------------------------------------------------------------------------------- 1 | #include "mpris.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | Glib::RefPtr create_rounded_pixbuf(const Glib::RefPtr& src_pixbuf, const int& size, const int& rounding_radius) { 10 | // Limit to 50% rounding otherwise funky stuff happens 11 | int rounding = std::clamp(rounding_radius, 0, size / 2); 12 | 13 | int width = src_pixbuf->get_width(); 14 | int height = src_pixbuf->get_height(); 15 | 16 | auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, size, size); 17 | auto cr = Cairo::Context::create(surface); 18 | 19 | cr->begin_new_path(); 20 | cr->arc(rounding, rounding, rounding, M_PI, 3.0 * M_PI / 2.0); 21 | cr->arc(width - rounding, rounding, rounding, 3.0 * M_PI / 2.0, 0.0); 22 | cr->arc(width - rounding, height - rounding, rounding, 0.0, M_PI / 2.0); 23 | cr->arc(rounding, height - rounding, rounding, M_PI / 2.0, M_PI); 24 | cr->close_path(); 25 | cr->clip(); 26 | 27 | Gdk::Cairo::set_source_pixbuf(cr, src_pixbuf, 0, 0); 28 | cr->paint(); 29 | 30 | return Gdk::Pixbuf::create(surface, 0, 0, size, size); 31 | } 32 | 33 | static void playback_status(PlayerctlPlayer *player, PlayerctlPlaybackStatus status, gpointer user_data) { 34 | module_mpris* self = static_cast(user_data); 35 | self->status = status; 36 | self->dispatcher_callback.emit(); 37 | } 38 | 39 | // Generate image from URL 40 | size_t write_data(void* ptr, size_t size, size_t nmemb, std::vector* data) { 41 | data->insert(data->end(), static_cast(ptr), static_cast(ptr) + size * nmemb); 42 | return size * nmemb; 43 | } 44 | 45 | static void metadata(PlayerctlPlayer* player, GVariant* metadata, gpointer user_data) { 46 | module_mpris* self = static_cast(user_data); 47 | 48 | // Initial cleanup 49 | self->artist.clear(); 50 | self->album.clear(); 51 | self->title.clear(); 52 | self->length.clear(); 53 | self->album_art_url.clear(); 54 | self->album_pixbuf = nullptr; 55 | self->dispatcher_callback.emit(); 56 | 57 | // Gather metadata 58 | if (auto artist = playerctl_player_get_artist(player, nullptr)) { 59 | self->artist = artist; 60 | g_free(artist); 61 | } 62 | if (auto album = playerctl_player_get_album(player, nullptr)) { 63 | self->album = album; 64 | g_free(album); 65 | } 66 | if (auto title = playerctl_player_get_title(player, nullptr)) { 67 | if (title[0] == '\0') 68 | self->title = "Not playing"; 69 | else 70 | self->title = title; 71 | g_free(title); 72 | } 73 | if (auto length = playerctl_player_print_metadata_prop(player, "mpris:length", nullptr)) { 74 | self->length = length; 75 | g_free(length); 76 | } 77 | if (auto album_art_url = playerctl_player_print_metadata_prop(player, "mpris:artUrl", nullptr)) { 78 | self->album_art_url = album_art_url; 79 | g_free(album_art_url); 80 | } 81 | 82 | // Load album art 83 | if (self->album_art_url.find("file://") == 0) { 84 | self->album_art_url.erase(0, 7); 85 | if (self->album_art_url == "" || !std::filesystem::exists(self->album_art_url)) 86 | return; 87 | 88 | try { 89 | Glib::RefPtr pixbuf = Gdk::Pixbuf::create_from_file(self->album_art_url); 90 | int width = pixbuf->get_width(); 91 | int height = pixbuf->get_height(); 92 | 93 | int square_size = std::min(width, height); 94 | int offset_x = (width - square_size) / 2; 95 | int offset_y = (height - square_size) / 2; 96 | self->album_pixbuf = Gdk::Pixbuf::create_subpixbuf(pixbuf, offset_x, offset_y, square_size, square_size); 97 | if (std::stoi(self->win->config_main["mpris"]["album-rounding"]) > 0) 98 | self->album_pixbuf = create_rounded_pixbuf(self->album_pixbuf, square_size, std::stoi(self->win->config_main["mpris"]["album-rounding"])); 99 | self->dispatcher_callback.emit(); 100 | } 101 | catch (...) { 102 | return; 103 | } 104 | } 105 | else if (self->album_art_url.find("https://") == 0) { 106 | CURL* curl = curl_easy_init(); 107 | std::thread([&, curl, self]() { 108 | std::vector image_data; 109 | curl_easy_setopt(curl, CURLOPT_URL, self->album_art_url.c_str()); 110 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); 111 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &image_data); 112 | (void)curl_easy_perform(curl); 113 | 114 | auto pixbuf_loader = Gdk::PixbufLoader::create(); 115 | pixbuf_loader->write(image_data.data(), image_data.size()); 116 | pixbuf_loader->close(); 117 | self->album_pixbuf = pixbuf_loader->get_pixbuf(); 118 | int square_size = std::min(self->album_pixbuf->get_width(), self->album_pixbuf->get_height()); 119 | self->album_pixbuf = create_rounded_pixbuf(self->album_pixbuf, square_size, 20); 120 | curl_easy_cleanup(curl); 121 | self->dispatcher_callback.emit(); 122 | }).detach(); 123 | } 124 | else 125 | self->dispatcher_callback.emit(); 126 | } 127 | 128 | static void player_appeared(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, gpointer user_data) { 129 | module_mpris* self = static_cast(user_data); 130 | self->player = playerctl_player_new_from_name(player_name, nullptr); 131 | 132 | g_object_get(self->player, "playback-status", &self->status, nullptr); 133 | g_signal_connect(self->player, "playback-status", G_CALLBACK(playback_status), self); 134 | g_signal_connect(self->player, "metadata", G_CALLBACK(metadata), self); 135 | metadata(self->player, nullptr, self); 136 | 137 | self->update_info(); 138 | } 139 | 140 | static void player_vanished(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, gpointer user_data) { 141 | module_mpris* self = static_cast(user_data); 142 | self->player = nullptr; 143 | 144 | // Itterate over all players 145 | GList* players = playerctl_list_players(nullptr); 146 | for (auto plr = players; plr != nullptr; plr = plr->next) { 147 | auto plr_name = static_cast(plr->data); 148 | player_appeared(nullptr, plr_name, self); 149 | return; 150 | } 151 | } 152 | 153 | module_mpris::module_mpris(sysbar *window, const bool &icon_on_start) : module(window, icon_on_start), player(nullptr) { 154 | get_style_context()->add_class("module_mpris"); 155 | 156 | if (win->config_main["mpris"]["show-icon"] != "true") 157 | image_icon.hide(); 158 | 159 | if (win->config_main["mpris"]["show-label"] != "true") 160 | label_info.hide(); 161 | 162 | std::string cfg_layout = win->config_main["mpris"]["widget-layout"]; 163 | if (!cfg_layout.empty()) { 164 | widget_layout.clear(); 165 | for (char c : cfg_layout) { 166 | widget_layout.push_back(c - '0'); 167 | } 168 | } 169 | 170 | dispatcher_callback.connect(sigc::mem_fun(*this, &module_mpris::update_info)); 171 | 172 | // Setup 173 | auto manager = playerctl_player_manager_new(nullptr); 174 | g_object_connect(manager, "signal::name-appeared", G_CALLBACK(player_appeared), this, nullptr); 175 | g_object_connect(manager, "signal::name-vanished", G_CALLBACK(player_vanished), this, nullptr); 176 | 177 | setup_widget(); 178 | 179 | // Reuse code to get available players 180 | player_vanished(nullptr, nullptr, this); 181 | } 182 | 183 | void module_mpris::update_info() { 184 | bool sensitivity = (player != nullptr); 185 | button_previous.set_sensitive(sensitivity); 186 | button_play_pause.set_sensitive(sensitivity); 187 | button_next.set_sensitive(sensitivity); 188 | 189 | std::string status_icon = status ? "player_play" : "player_pause"; 190 | image_icon.set_from_icon_name(status_icon); 191 | button_play_pause.set_icon_name(status_icon); 192 | label_info.set_text(title); 193 | 194 | label_title.set_text(title); 195 | label_artist.set_text(artist); 196 | 197 | if (album_pixbuf != nullptr) 198 | image_album_art.set(album_pixbuf); 199 | else if (album_art_url.empty()) 200 | image_album_art.set_from_icon_name("music-app-symbolic"); 201 | else 202 | image_album_art.set_from_icon_name("process-working-symbolic"); 203 | 204 | // TODO: Add a progress slider 205 | } 206 | 207 | void module_mpris::setup_widget() { 208 | win->grid_widgets_end.attach(box_player, widget_layout[0], widget_layout[1], widget_layout[2], widget_layout[3]); 209 | 210 | box_player.get_style_context()->add_class("widget_mpris"); 211 | image_album_art.get_style_context()->add_class("image_album_art"); 212 | image_album_art.set_from_icon_name("music-app-symbolic"); 213 | image_album_art.set_size_request(96 ,96); 214 | box_player.append(image_album_art); 215 | 216 | box_player.append(box_right); 217 | box_right.set_orientation(Gtk::Orientation::VERTICAL); 218 | 219 | label_title.get_style_context()->add_class("label_title"); 220 | label_title.set_ellipsize(Pango::EllipsizeMode::END); 221 | label_title.set_max_width_chars(0); 222 | label_title.set_text("Not playing"); 223 | box_right.append(label_title); 224 | 225 | label_artist.get_style_context()->add_class("label_artist"); 226 | label_artist.set_ellipsize(Pango::EllipsizeMode::END); 227 | label_artist.set_max_width_chars(0); 228 | box_right.append(label_artist); 229 | 230 | button_previous.set_icon_name("media-skip-backward"); 231 | button_play_pause.set_icon_name("player_play"); 232 | button_next.set_icon_name("media-skip-forward"); 233 | 234 | button_previous.set_focusable(false); 235 | button_play_pause.set_focusable(false); 236 | button_next.set_focusable(false); 237 | 238 | button_previous.set_has_frame(false); 239 | button_play_pause.set_has_frame(false); 240 | button_next.set_has_frame(false); 241 | 242 | button_previous.set_sensitive(false); 243 | button_play_pause.set_sensitive(false); 244 | button_next.set_sensitive(false); 245 | 246 | button_previous.signal_clicked().connect([&]() { 247 | playerctl_player_previous(player, nullptr); 248 | }); 249 | 250 | button_play_pause.signal_clicked().connect([&]() { 251 | playerctl_player_play_pause(player, nullptr); 252 | }); 253 | 254 | button_next.signal_clicked().connect([&]() { 255 | playerctl_player_next(player, nullptr); 256 | }); 257 | 258 | box_right.append(box_controls); 259 | box_controls.get_style_context()->add_class("box_controls"); 260 | box_controls.set_vexpand(true); 261 | box_controls.set_hexpand(true); 262 | box_controls.set_halign(Gtk::Align::CENTER); 263 | box_controls.set_valign(Gtk::Align::END); 264 | 265 | box_controls.append(button_previous); 266 | box_controls.append(button_play_pause); 267 | box_controls.append(button_next); 268 | } 269 | -------------------------------------------------------------------------------- /src/modules/mpris.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_MPRIS 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | class module_mpris : public module { 10 | public: 11 | module_mpris(sysbar*, const bool&); 12 | 13 | Glib::Dispatcher dispatcher_callback; 14 | PlayerctlPlayer* player; 15 | Glib::RefPtr album_pixbuf; 16 | 17 | int status = 0; 18 | std::string artist = ""; 19 | std::string album = ""; 20 | std::string title = ""; 21 | std::string length = ""; 22 | std::string album_art_url = ""; 23 | std::vector widget_layout = {0, 1, 4, 2}; 24 | 25 | void update_info(); 26 | 27 | private: 28 | Gtk::Box box_player; 29 | Gtk::Box box_right; 30 | Gtk::Image image_album_art; 31 | Gtk::Label label_title; 32 | Gtk::Label label_album; 33 | Gtk::Label label_artist; 34 | 35 | Gtk::Box box_controls; 36 | Gtk::Button button_previous; 37 | Gtk::Button button_play_pause; 38 | Gtk::Button button_next; 39 | 40 | void setup_widget(); 41 | }; 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /src/modules/network.cpp: -------------------------------------------------------------------------------- 1 | #include "network.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | module_network::module_network(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start), default_if_index(0) { 11 | get_style_context()->add_class("module_network"); 12 | 13 | if (win->config_main["network"]["show-label"] != "true") 14 | label_info.hide(); 15 | 16 | // Set up networking stuff 17 | if (!setup_netlink()) 18 | return; 19 | 20 | dispatcher.connect(sigc::mem_fun(*this, &module_network::update_info)); 21 | std::thread thread_network(&module_network::interface_thread, this); 22 | thread_network.detach(); 23 | 24 | Glib::signal_timeout().connect([&]() { 25 | update_info(); 26 | return true; 27 | }, 10000); 28 | 29 | #ifdef MODULE_CONTROLS 30 | setup_control(); 31 | #endif 32 | } 33 | 34 | module_network::~module_network() { 35 | close(nl_socket); 36 | } 37 | 38 | void module_network::interface_thread() { 39 | while (true) { 40 | int len = recv(nl_socket, buffer, sizeof(buffer), 0); 41 | if (len < 0) { 42 | std::fprintf(stderr, "Error receiving netlink message"); 43 | return; 44 | } 45 | 46 | for (struct nlmsghdr *nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { 47 | if (nlh->nlmsg_type == NLMSG_DONE) { 48 | break; 49 | } 50 | if (nlh->nlmsg_type == RTM_NEWADDR || nlh->nlmsg_type == RTM_DELADDR || nlh->nlmsg_type == RTM_GETADDR) { 51 | process_message(nlh); 52 | } 53 | } 54 | dispatcher.emit(); 55 | } 56 | } 57 | 58 | void module_network::update_info() { 59 | // Get primary interface 60 | uint if_index = default_if_index; 61 | auto default_if = std::find_if(adapters.begin(), adapters.end(), [if_index](const network_adapter& a) { return a.index == if_index; }); 62 | if (default_if == adapters.end()) { 63 | std::fprintf(stderr, "No interface found\n"); 64 | image_icon.set_from_icon_name("network-error-symbolic"); 65 | return; 66 | } 67 | 68 | if (win->verbose) 69 | std::printf("Default interface is %s\n", default_if->interface.c_str()); 70 | 71 | if (default_if->type == "Ethernet") { 72 | image_icon.set_from_icon_name("network-wired-symbolic"); 73 | label_info.set_text(""); 74 | } 75 | #ifdef FEATURE_WIRELESS 76 | else if (default_if->type == "Wireless") { 77 | auto info = manager.get_wireless_info(default_if->interface); 78 | label_info.set_text(std::to_string(info->signal_percentage)); 79 | 80 | std::string icon; 81 | if (info->signal_percentage > 80) 82 | icon = "network-wireless-signal-excellent-symbolic"; 83 | else if (info->signal_percentage > 60) 84 | icon = "network-wireless-signal-good-symbolic"; 85 | else if (info->signal_percentage > 40) 86 | icon = "network-wireless-signal-ok-symbolic"; 87 | else if (info->signal_percentage > 20) 88 | icon = "network-wireless-signal-weak-symbolic"; 89 | else 90 | icon = "network-wireless-signal-none-symbolic"; 91 | 92 | image_icon.set_from_icon_name(icon); 93 | } 94 | else if (default_if->type == "Cellular") 95 | image_icon.set_from_icon_name("network-cellular-connected-symbolic"); 96 | #endif 97 | else 98 | image_icon.set_from_icon_name("network-error-symbolic"); 99 | 100 | set_tooltip_text("Adapter: " + default_if->interface + "\nAddress: " + default_if->ipv4); 101 | } 102 | 103 | bool module_network::setup_netlink() { 104 | nl_socket = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); 105 | if (nl_socket < 0) { 106 | std::fprintf(stderr, "Failed to open netlink socket\n"); 107 | return false; 108 | } 109 | 110 | struct sockaddr_nl sa; 111 | memset(&sa, 0, sizeof(sa)); 112 | sa.nl_family = AF_NETLINK; 113 | sa.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV4_ROUTE; 114 | 115 | if (bind(nl_socket, (struct sockaddr*)&sa, sizeof(sa)) < 0) { 116 | std::fprintf(stderr, "Failed to bind netlink socket\n"); 117 | close(nl_socket); 118 | return false; 119 | } 120 | 121 | // Request current addresses 122 | request_dump(RTM_GETADDR); 123 | return true; 124 | } 125 | 126 | void module_network::request_dump(const int& type) { 127 | struct nlmsghdr nlh; 128 | struct rtgenmsg rtg; 129 | 130 | memset(&nlh, 0, sizeof(nlh)); 131 | nlh.nlmsg_len = NLMSG_LENGTH(sizeof(rtg)); 132 | nlh.nlmsg_type = type; 133 | nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; 134 | nlh.nlmsg_seq = 1; 135 | nlh.nlmsg_pid = getpid(); 136 | 137 | memset(&rtg, 0, sizeof(rtg)); 138 | rtg.rtgen_family = AF_INET; 139 | 140 | struct iovec iov = { &nlh, nlh.nlmsg_len }; 141 | struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; 142 | 143 | struct msghdr msg = { 144 | .msg_name = &sa, 145 | .msg_namelen = sizeof(sa), 146 | .msg_iov = &iov, 147 | .msg_iovlen = 1, 148 | }; 149 | 150 | sendmsg(nl_socket, &msg, 0); 151 | } 152 | 153 | void module_network::process_message(struct nlmsghdr* nlh) { 154 | struct ifaddrmsg* ifa = (struct ifaddrmsg*)NLMSG_DATA(nlh); 155 | struct rtattr* rth = IFA_RTA(ifa); 156 | int rtl = IFA_PAYLOAD(nlh); 157 | char if_name[IF_NAMESIZE]; 158 | char if_addr[INET_ADDRSTRLEN]; 159 | std::string if_type; 160 | unsigned int if_index = 0; 161 | 162 | for (; RTA_OK(rth, rtl); rth = RTA_NEXT(rth, rtl)) { 163 | // Get interface type 164 | if (if_indextoname(ifa->ifa_index, if_name) != nullptr) { 165 | std::string interface_path = "/sys/class/net/"; 166 | std::ifstream file(interface_path + if_name + "/type"); 167 | bool wireless = std::filesystem::exists(interface_path + if_name + "/wireless"); 168 | std::ostringstream type_buffer; 169 | type_buffer << file.rdbuf(); 170 | std::string type = type_buffer.str(); 171 | type.pop_back(); 172 | 173 | // TODO: Add more types 174 | // Hotspot and VPN types are left 175 | if (wireless && (type == "1")) 176 | if_type = "Wireless"; 177 | else if (type == "519") 178 | if_type = "Cellular"; 179 | else 180 | if_type = "Ethernet"; 181 | } 182 | 183 | // Get interface index 184 | if_index = if_nametoindex(if_name); 185 | 186 | // Skip anything we don't need 187 | if (rth->rta_type == RTA_OIF) 188 | default_if_index = if_index; 189 | else if (rth->rta_type != IFA_LOCAL) 190 | continue; 191 | else if (strcmp(if_name, "lo") == 0) 192 | continue; 193 | 194 | // Has an interface been updated? 195 | if (nlh->nlmsg_type == RTM_NEWADDR) 196 | inet_ntop(AF_INET, RTA_DATA(rth), if_addr, sizeof(if_addr)); 197 | else 198 | strcpy(if_addr, "null"); 199 | 200 | // Add the interface to the list 201 | if (rth->rta_type == IFA_LOCAL) { 202 | 203 | /// Find the interface if it already exists 204 | auto it = std::find_if(adapters.begin(), adapters.end(), [if_index](const network_adapter& s) { 205 | return s.index == if_index; 206 | }); 207 | if (it != adapters.end()) { 208 | it->ipv4 = if_addr; 209 | continue; 210 | } 211 | 212 | // Create a new interface if needed 213 | if (win->verbose) 214 | std::printf("%s has been added to the list\n", if_name); 215 | 216 | network_adapter adapter; 217 | adapter.interface = if_name; 218 | adapter.type = if_type; 219 | adapter.ipv4 = if_addr; 220 | adapter.index = if_index; 221 | adapters.push_back(adapter); 222 | } 223 | } 224 | } 225 | 226 | #ifdef MODULE_CONTROLS 227 | void module_network::setup_control() { 228 | if (!win->box_controls) 229 | return; 230 | 231 | auto container = static_cast(win->box_controls); 232 | control_network = Gtk::make_managed(win, "network-wireless-symbolic", true, "network"); 233 | container->flowbox_controls.append(*control_network); 234 | control_network->button_expand.signal_clicked().connect([&]() { 235 | win->stack_end.set_visible_child("network"); 236 | }); 237 | } 238 | #endif 239 | -------------------------------------------------------------------------------- /src/modules/network.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_NETWORK 4 | #include "../wireless_network.hpp" 5 | #include "controls.hpp" 6 | 7 | #include 8 | 9 | struct network_adapter { 10 | std::string interface; 11 | std::string type; 12 | std::string ipv4; 13 | uint index; 14 | }; 15 | 16 | class module_network : public module { 17 | public: 18 | module_network(sysbar*, const bool&); 19 | ~module_network(); 20 | 21 | private: 22 | Glib::Dispatcher dispatcher; 23 | #ifdef FEATURE_WIRELESS 24 | wireless_manager manager; 25 | #endif 26 | 27 | #ifdef MODULE_CONTROLS 28 | control* control_network; 29 | void setup_control(); 30 | #endif 31 | 32 | int nl_socket; 33 | char buffer[4096]; 34 | uint default_if_index; 35 | std::vector adapters; 36 | 37 | bool setup_netlink(); 38 | void interface_thread(); 39 | void update_info(); 40 | void process_message(struct nlmsghdr*); 41 | void request_dump(const int&); 42 | }; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/modules/notifications.cpp: -------------------------------------------------------------------------------- 1 | #include "notifications.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | const auto introspection_data = Gio::DBus::NodeInfo::create_for_xml( 8 | "" 9 | "" 10 | " " 11 | " " 12 | " " 13 | " " 14 | " " 15 | " " 16 | " " 17 | " " 18 | " " 19 | " " 20 | " " 21 | " " 22 | " " 23 | " " 24 | " " 25 | " " 26 | " " 27 | " " 28 | " " 29 | " " 30 | " " 31 | " " 32 | " " 33 | " " 34 | " " 35 | " " 36 | " " 37 | " " 38 | " " 39 | " " 40 | " " 41 | " " 42 | " " 43 | "")->lookup_interface(); 44 | 45 | module_notifications::module_notifications(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start), notif_count(0) { 46 | get_style_context()->add_class("module_notifications"); 47 | image_icon.set_from_icon_name("notification-symbolic"); 48 | set_tooltip_text("No new notifications"); 49 | label_info.hide(); 50 | 51 | std::string cfg_command = win->config_main["notification"]["command"]; 52 | if (!cfg_command.empty()) 53 | command = cfg_command; 54 | 55 | setup_widget(); 56 | setup_daemon(); 57 | } 58 | 59 | void module_notifications::setup_widget() { 60 | box_notifications.set_orientation(Gtk::Orientation::VERTICAL); 61 | 62 | scrolledwindow_notifications.set_child(box_notifications); 63 | scrolledwindow_notifications.set_vexpand(); 64 | scrolledwindow_notifications.set_propagate_natural_height(); 65 | 66 | if (win->position / 2) { 67 | win->box_widgets_end.prepend(box_header); 68 | win->box_widgets_end.prepend(scrolledwindow_notifications); 69 | } 70 | else { 71 | win->box_widgets_end.append(box_header); 72 | win->box_widgets_end.append(scrolledwindow_notifications); 73 | } 74 | 75 | box_header.get_style_context()->add_class("notifications_header"); 76 | box_header.set_visible(false); 77 | box_header.append(label_notif_count); 78 | label_notif_count.set_halign(Gtk::Align::START); 79 | label_notif_count.set_hexpand(true); 80 | scrolledwindow_notifications.set_visible(false); 81 | 82 | box_header.append(button_clear); 83 | button_clear.set_halign(Gtk::Align::END); 84 | button_clear.set_image_from_icon_name("application-exit-symbolic"); 85 | button_clear.signal_clicked().connect([&]() { 86 | for (auto n_child : box_notifications.get_children()) { 87 | auto n = static_cast(n_child); 88 | box_notifications.remove(*n); 89 | } 90 | box_header.set_visible(false); 91 | label_notif_count.set_text(""); 92 | image_icon.set_from_icon_name("notification-symbolic"); 93 | scrolledwindow_notifications.set_visible(false); 94 | }); 95 | 96 | // TODO: Support other orientations 97 | popover_alert.get_style_context()->add_class("popover_notifications"); 98 | popover_alert.set_parent(*win); 99 | popover_alert.set_child(scrolledwindow_alert); 100 | popover_alert.set_autohide(false); 101 | popover_alert.set_has_arrow(false); 102 | scrolledwindow_alert.set_size_request(350, -1); 103 | scrolledwindow_alert.set_child(flowbox_alert); 104 | scrolledwindow_alert.set_propagate_natural_height(true); 105 | flowbox_alert.set_max_children_per_line(1); 106 | 107 | win->overlay_window.signal_show().connect(sigc::mem_fun(*this, &module_notifications::on_overlay_change)); 108 | } 109 | 110 | 111 | void module_notifications::on_overlay_change() { 112 | popover_alert.popdown(); 113 | } 114 | 115 | void module_notifications::setup_daemon() { 116 | Gio::DBus::own_name(Gio::DBus::BusType::SESSION, 117 | "org.freedesktop.Notifications", 118 | sigc::mem_fun(*this, &module_notifications::on_bus_acquired), 119 | {}, 120 | {}, 121 | Gio::DBus::BusNameOwnerFlags::REPLACE); 122 | } 123 | 124 | void module_notifications::on_bus_acquired(const Glib::RefPtr &connection, const Glib::ustring &name) { 125 | object_id = connection->register_object( 126 | "/org/freedesktop/Notifications", 127 | introspection_data, 128 | interface_vtable); 129 | 130 | daemon_connection = connection; 131 | } 132 | 133 | void module_notifications::on_interface_method_call( 134 | const Glib::RefPtr& connection, 135 | const Glib::ustring &sender, const Glib::ustring& object_path, 136 | const Glib::ustring &interface_name, const Glib::ustring& method_name, 137 | const Glib::VariantContainerBase& parameters, 138 | const Glib::RefPtr& invocation) { 139 | 140 | if (method_name == "GetServerInformation") { 141 | static const auto info = 142 | Glib::Variant>::create( 143 | {"sysbar", "funky.sys64", "0.9.0", "1.0"}); 144 | invocation->return_value(info); 145 | } 146 | else if (method_name == "GetCapabilities") { 147 | static const auto value = Glib::Variant>>::create( 148 | {{"action-icons", "actions", "body", "body-hyperlinks", "body-markup", "body-images", "persistance"}}); 149 | invocation->return_value(value); 150 | } 151 | else if (method_name == "Notify") { 152 | box_header.set_visible(true); 153 | image_icon.set_from_icon_name("notification-new-symbolic"); 154 | label_notif_count.set_text(std::to_string(box_notifications.get_children().size() + 1) + " Unread Notifications"); 155 | scrolledwindow_notifications.set_visible(true); 156 | 157 | // TODO: This is worse 158 | notification *notif = Gtk::make_managed(box_notifications, sender, parameters, command); 159 | 160 | if (notif->replaces_id != 0) { 161 | for (auto n_child : box_notifications.get_children()) { 162 | auto n = static_cast(n_child); 163 | if (n->notif_id == notif->replaces_id) { 164 | // TODO: Alert notifications don't get replaced yet 165 | box_notifications.remove(*n); 166 | notif->notif_id = notif->replaces_id; 167 | } 168 | } 169 | } 170 | 171 | auto id_var = Glib::VariantContainerBase::create_tuple( 172 | Glib::Variant::create(notif->notif_id)); 173 | 174 | notif->signal_clicked().connect([&, notif]() { 175 | // TODO: Make this switch focus to the program that sent the notification 176 | box_notifications.remove(*notif); 177 | label_notif_count.set_text(std::to_string(box_notifications.get_children().size()) + " Unread Notifications"); 178 | if (box_notifications.get_children().size() == 0) { 179 | box_header.set_visible(false); 180 | image_icon.set_from_icon_name("notification-symbolic"); 181 | set_tooltip_text("No new notifications"); 182 | scrolledwindow_notifications.set_visible(false); 183 | } 184 | }); 185 | 186 | if (!win->overlay_window.is_visible()) { 187 | notification *notif_alert = Gtk::make_managed(box_notifications, sender, parameters, ""); 188 | notif_alert->signal_clicked().connect([&, notif_alert]() { 189 | // TODO: Make this switch focus to the program that sent the notification 190 | for (auto n_child : box_notifications.get_children()) { 191 | auto n = static_cast(n_child); 192 | if (n->notif_id == notif_alert->notif_id) 193 | box_notifications.remove(*n); 194 | } 195 | 196 | notif_alert->timeout_connection.disconnect(); 197 | flowbox_alert.remove(*notif_alert); 198 | label_notif_count.set_text(std::to_string(box_notifications.get_children().size()) + " Unread Notifications"); 199 | 200 | if (box_notifications.get_children().size() == 0) { 201 | box_header.set_visible(false); 202 | image_icon.set_from_icon_name("notification-symbolic"); 203 | timeout_connection.disconnect(); 204 | popover_alert.popdown(); 205 | set_tooltip_text("No new notifications"); 206 | scrolledwindow_notifications.set_visible(false); 207 | } 208 | }); 209 | flowbox_alert.prepend(*notif_alert); 210 | 211 | notif_alert->timeout_connection = Glib::signal_timeout().connect([&, notif_alert]() { 212 | flowbox_alert.remove(*notif_alert); 213 | return false; 214 | }, 5000); 215 | popover_alert.popup(); 216 | } 217 | 218 | box_notifications.prepend(*notif); 219 | set_tooltip_text(std::to_string(flowbox_alert.get_children().size()) + " unread notifications\n"); 220 | 221 | invocation->return_value(id_var); 222 | 223 | timeout_connection.disconnect(); 224 | timeout_connection = Glib::signal_timeout().connect([&]() { 225 | popover_alert.popdown(); 226 | return false; 227 | }, 5000); 228 | } 229 | } 230 | 231 | notification::notification(const Gtk::Box& box_notifications, const Glib::ustring& sender, const Glib::VariantContainerBase& parameters, const std::string& command) { 232 | get_style_context()->add_class("notification"); 233 | if (!parameters.is_of_type(Glib::VariantType("(susssasa{sv}i)"))) 234 | return; 235 | 236 | Glib::VariantIter iter(parameters); 237 | Glib::VariantBase child; 238 | 239 | iter.next_value(child); 240 | app_name = Glib::VariantBase::cast_dynamic>(child).get(); 241 | 242 | iter.next_value(child); 243 | replaces_id = Glib::VariantBase::cast_dynamic>(child).get(); 244 | notif_id = box_notifications.get_children().size() + 1; 245 | 246 | iter.next_value(child); 247 | app_icon = Glib::VariantBase::cast_dynamic>(child).get(); 248 | 249 | iter.next_value(child); 250 | summary = Glib::VariantBase::cast_dynamic>(child).get(); 251 | 252 | iter.next_value(child); 253 | body = Glib::VariantBase::cast_dynamic>(child).get(); 254 | 255 | // TODO: Fix this? doesn't seem to work? 256 | iter.next_value(child); 257 | auto variant_array = Glib::VariantBase::cast_dynamic>>(child); 258 | std::vector actions = variant_array.get(); 259 | 260 | iter.next_value(child); 261 | auto variant_dict = Glib::VariantBase::cast_dynamic>>(child); 262 | std::map map_hints = variant_dict.get(); 263 | 264 | // Expiration is not currently supported 265 | iter.next_value(child); 266 | int32_t expires = Glib::VariantBase::cast_dynamic>(child).get(); 267 | (void)expires; // Make the compiler shut up 268 | 269 | // Actions are currently unsupported 270 | /*for (const auto& action : actions) { 271 | }*/ 272 | for (const auto& [key, value] : map_hints) { 273 | handle_hint(key, value); 274 | } 275 | 276 | Gtk::Box box_notification; 277 | Gtk::Label label_headerbar, label_body; 278 | Gtk::Image image_icon; 279 | 280 | // Load image data from path 281 | if (app_icon != "") 282 | image_data = Gdk::Pixbuf::create_from_file(app_icon); 283 | 284 | // Load image from pixbuf if applicable 285 | if (image_data != nullptr) { 286 | image_icon.get_style_context()->add_class("image"); 287 | image_icon.set(image_data); 288 | image_data = image_data->scale_simple(64, 64, Gdk::InterpType::BILINEAR); 289 | image_icon.set_size_request(64, 64); 290 | box_main.append(image_icon); 291 | } 292 | 293 | label_headerbar.get_style_context()->add_class("summary"); 294 | label_headerbar.set_text(summary); 295 | label_headerbar.set_halign(Gtk::Align::START); 296 | label_headerbar.set_max_width_chars(0); 297 | label_headerbar.set_wrap(true); 298 | label_headerbar.set_ellipsize(Pango::EllipsizeMode::END); 299 | label_headerbar.set_wrap_mode(Pango::WrapMode::WORD_CHAR); 300 | label_headerbar.set_margin_start(5); 301 | label_headerbar.set_hexpand(true); 302 | box_notification.append(label_headerbar); 303 | 304 | label_body.get_style_context()->add_class("body"); 305 | label_body.set_text(body); 306 | label_body.set_max_width_chars(0); 307 | label_body.set_halign(Gtk::Align::START); 308 | label_body.set_wrap(true); 309 | label_body.set_wrap_mode(Pango::WrapMode::WORD_CHAR); 310 | label_body.set_lines(3); 311 | label_body.set_margin_start(5); 312 | label_body.set_hexpand(true); 313 | 314 | // TODO: Add button to expand (Aka change these 2 values) 315 | label_body.set_single_line_mode(true); 316 | label_body.set_ellipsize(Pango::EllipsizeMode::END); 317 | 318 | box_notification.append(label_body); 319 | 320 | box_main.append(box_notification); 321 | set_child(box_main); 322 | set_focusable(false); 323 | 324 | box_notification.set_orientation(Gtk::Orientation::VERTICAL); 325 | 326 | if (!command.empty()) { 327 | std::thread([command]() { 328 | (void)system(command.c_str()); 329 | }).detach(); 330 | } 331 | } 332 | 333 | void notification::handle_hint(const Glib::ustring& key, const Glib::VariantBase& value) { 334 | if (key == "icon_data") { 335 | auto container = Glib::VariantBase::cast_dynamic(value); 336 | int width = Glib::VariantBase::cast_dynamic>(container.get_child(0)).get(); 337 | int height = Glib::VariantBase::cast_dynamic>(container.get_child(1)).get(); 338 | int rowstride = Glib::VariantBase::cast_dynamic>(container.get_child(2)).get(); 339 | bool has_alpha = Glib::VariantBase::cast_dynamic>(container.get_child(3)).get(); 340 | int bits_per_sample = Glib::VariantBase::cast_dynamic>(container.get_child(4)).get(); 341 | //int channels = Glib::VariantBase::cast_dynamic>(container.get_child(5)).get(); // Unused 342 | auto pixel_data = Glib::VariantBase::cast_dynamic>>(container.get_child(6)).get(); 343 | 344 | image_data = Gdk::Pixbuf::create_from_data( 345 | pixel_data.data(), 346 | Gdk::Colorspace::RGB, 347 | has_alpha, 348 | bits_per_sample, 349 | width, 350 | height, 351 | rowstride 352 | )->copy(); 353 | } 354 | } -------------------------------------------------------------------------------- /src/modules/notifications.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_NOTIFICATION 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class notification : public Gtk::Button { 12 | public: 13 | notification(const Gtk::Box&, const Glib::ustring&, const Glib::VariantContainerBase&, const std::string&); 14 | 15 | sigc::connection timeout_connection; 16 | 17 | guint32 notif_id; 18 | guint32 replaces_id; 19 | 20 | private: 21 | Gtk::Box box_main; 22 | 23 | Glib::ustring app_name; 24 | Glib::ustring app_icon; 25 | Glib::ustring summary; 26 | Glib::ustring body; 27 | Glib::RefPtr image_data; 28 | 29 | void handle_hint(const Glib::ustring&, const Glib::VariantBase&); 30 | }; 31 | 32 | class module_notifications : public module { 33 | public: 34 | module_notifications(sysbar*, const bool&); 35 | 36 | int notif_count; 37 | Gtk::Box box_notifications; 38 | Gtk::ScrolledWindow scrolledwindow_notifications; 39 | 40 | private: 41 | Gtk::Box box_header; 42 | Gtk::Label label_notif_count; 43 | Gtk::Button button_clear; 44 | Gtk::Popover popover_alert; 45 | Gtk::FlowBox flowbox_alert; 46 | Gtk::ScrolledWindow scrolledwindow_alert; 47 | sigc::connection timeout_connection; 48 | Glib::RefPtr daemon_connection; 49 | const Gio::DBus::InterfaceVTable interface_vtable{sigc::mem_fun(*this, &module_notifications::on_interface_method_call)}; 50 | 51 | std::string command; 52 | guint object_id; 53 | 54 | void setup_widget(); 55 | void on_overlay_change(); 56 | void setup_daemon(); 57 | void on_bus_acquired(const Glib::RefPtr &connection, const Glib::ustring &name); 58 | void on_interface_method_call( 59 | const Glib::RefPtr &connection, 60 | const Glib::ustring &sender, const Glib::ustring &object_path, 61 | const Glib::ustring &interface_name, const Glib::ustring &method_name, 62 | const Glib::VariantContainerBase ¶meters, 63 | const Glib::RefPtr &invocation); 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /src/modules/performance.cpp: -------------------------------------------------------------------------------- 1 | #include "performance.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | module_performance::module_performance(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start), precision(0), interval(1000) { 8 | get_style_context()->add_class("module_performance"); 9 | image_icon.set_from_icon_name("cpu-symbolic"); 10 | 11 | // TODO: Add config support 12 | // TODO: Add more metrics (Ram, Disk, Temp, Ect..) 13 | 14 | prev_stats = get_cpu_stats(); 15 | label_info.set_text("0"); 16 | Glib::signal_timeout().connect(sigc::mem_fun(*this, &module_performance::update_info), interval); 17 | } 18 | 19 | bool module_performance::update_info() { 20 | cpu_stats curr_stats = get_cpu_stats(); 21 | double cpu_load = calculate_cpu_load(prev_stats, curr_stats); 22 | prev_stats = curr_stats; 23 | 24 | std::ostringstream out; 25 | out << std::fixed << std::setprecision(precision) << cpu_load; 26 | label_info.set_text(out.str()); 27 | return true; 28 | } 29 | 30 | cpu_stats module_performance::get_cpu_stats() { 31 | std::ifstream file("/proc/stat"); 32 | std::string line; 33 | cpu_stats stats = {}; 34 | 35 | if (file.is_open()) { 36 | std::getline(file, line); 37 | std::sscanf(line.c_str(), "cpu %llu %llu %llu %llu %llu %llu %llu %llu", 38 | &stats.user, &stats.nice, &stats.system, &stats.idle, 39 | &stats.iowait, &stats.irq, &stats.softirq, &stats.steal); 40 | file.close(); 41 | } 42 | 43 | return stats; 44 | } 45 | 46 | double module_performance::calculate_cpu_load(const cpu_stats& prev, const cpu_stats& curr) { 47 | unsigned long long prev_idle = prev.idle + prev.iowait; 48 | unsigned long long curr_idle = curr.idle + curr.iowait; 49 | 50 | unsigned long long prev_non_idle = prev.user + prev.nice + prev.system + prev.irq + prev.softirq + prev.steal; 51 | unsigned long long curr_non_idle = curr.user + curr.nice + curr.system + curr.irq + curr.softirq + curr.steal; 52 | 53 | unsigned long long prev_total = prev_idle + prev_non_idle; 54 | unsigned long long curr_total = curr_idle + curr_non_idle; 55 | 56 | unsigned long long totald = curr_total - prev_total; 57 | unsigned long long idled = curr_idle - prev_idle; 58 | 59 | double cpu_load = (totald - idled) / static_cast(totald); 60 | return cpu_load * 100.0; 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/performance.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_PERFORMANCE 4 | 5 | struct cpu_stats { 6 | unsigned long long user, nice, system, idle, iowait, irq, softirq, steal; 7 | }; 8 | 9 | class module_performance : public module { 10 | public: 11 | module_performance(sysbar*, const bool&); 12 | 13 | private: 14 | int precision; 15 | int interval; 16 | cpu_stats prev_stats; 17 | bool update_info(); 18 | cpu_stats get_cpu_stats(); 19 | double calculate_cpu_load(const cpu_stats&, const cpu_stats&); 20 | }; 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /src/modules/taskbar.cpp: -------------------------------------------------------------------------------- 1 | #include "taskbar.hpp" 2 | 3 | #include 4 | #include 5 | 6 | std::vector> app_list; 7 | 8 | std::string cleanup_string(const std::string &str) { 9 | std::string result; 10 | 11 | // Convert to lowercase and remove separators 12 | for (char c : str) { 13 | if (c != ' ' && c != '-') { 14 | result += std::tolower(c); 15 | } 16 | } 17 | 18 | return result; 19 | } 20 | 21 | // Placeholder functions 22 | void handle_toplevel_title(void* data, zwlr_foreign_toplevel_handle_v1* handle, const char* title) { 23 | auto toplevel_entry = static_cast(data); 24 | toplevel_entry->handle = handle; 25 | 26 | std::string text = title; 27 | 28 | if (text == "") 29 | text = "Untitled Window"; 30 | 31 | if (text.length() > toplevel_entry->text_length) 32 | text = text.substr(0, toplevel_entry->text_length - 2) + ".."; 33 | 34 | toplevel_entry->toplevel_label.set_text(text); 35 | } 36 | 37 | void handle_toplevel_app_id(void* data, zwlr_foreign_toplevel_handle_v1*, const char* app_id) { 38 | auto toplevel_entry = static_cast(data); 39 | Glib::RefPtr app_info; 40 | 41 | std::string appid = cleanup_string(app_id); 42 | toplevel_entry->set_tooltip_markup(appid); 43 | for (auto app : app_list) { 44 | std::string app_name = cleanup_string(app->get_name()); 45 | std::string app_executable = cleanup_string(app->get_executable()); 46 | 47 | if (appid == app_name || appid == app_executable ) { 48 | app_info = app; 49 | break; 50 | } 51 | } 52 | 53 | // Did not find the app in the list 54 | if (app_info == nullptr) 55 | toplevel_entry->image_icon.set_from_icon_name("binary"); 56 | else 57 | toplevel_entry->image_icon.set(app_info->get_icon()); 58 | } 59 | 60 | void handle_toplevel_output_enter(void* data, zwlr_foreign_toplevel_handle_v1*, wl_output* output) {} 61 | 62 | void handle_toplevel_output_leave(void* data, zwlr_foreign_toplevel_handle_v1*, wl_output* output) {} 63 | 64 | void handle_toplevel_state(void* data, zwlr_foreign_toplevel_handle_v1*, wl_array* state) { 65 | auto toplevel_entry = static_cast(data); 66 | auto flowbox_child = static_cast(toplevel_entry->get_parent()); 67 | auto flowbox = static_cast(flowbox_child->get_parent()); 68 | 69 | uint32_t* state_array = (uint32_t* )state->data; 70 | size_t count = state->size / sizeof(uint32_t); 71 | 72 | for (size_t i = 0; i < count; ++i) { 73 | switch (state_array[i]) { 74 | case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: 75 | break; 76 | case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: 77 | break; 78 | case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: 79 | flowbox->select_child(*flowbox_child); 80 | break; 81 | case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: 82 | break; 83 | } 84 | } 85 | } 86 | 87 | void handle_toplevel_done(void* data, zwlr_foreign_toplevel_handle_v1*) {} 88 | 89 | void handle_toplevel_closed(void* data, zwlr_foreign_toplevel_handle_v1* handle) { 90 | auto toplevel_entry = static_cast(data); 91 | auto flowbox_child = static_cast(toplevel_entry->get_parent()); 92 | auto flowbox = static_cast(flowbox_child->get_parent()); 93 | flowbox->remove(*toplevel_entry); 94 | 95 | // Does this cause a memory leak? 96 | // Future me will find out! 97 | } 98 | 99 | void handle_toplevel_parent(void* data, zwlr_foreign_toplevel_handle_v1* handle, zwlr_foreign_toplevel_handle_v1* parent) {} 100 | 101 | zwlr_foreign_toplevel_handle_v1_listener toplevel_handle_v1_impl = { 102 | .title = handle_toplevel_title, 103 | .app_id = handle_toplevel_app_id, 104 | .output_enter = handle_toplevel_output_enter, 105 | .output_leave = handle_toplevel_output_leave, 106 | .state = handle_toplevel_state, 107 | .done = handle_toplevel_done, 108 | .closed = handle_toplevel_closed, 109 | .parent = handle_toplevel_parent 110 | }; 111 | 112 | void handle_manager_toplevel(void* data, zwlr_foreign_toplevel_manager_v1* manager, 113 | zwlr_foreign_toplevel_handle_v1* toplevel) { 114 | auto self = static_cast(data); 115 | 116 | taskbar_item* toplevel_entry = Gtk::make_managed(self, self->cfg); 117 | 118 | zwlr_foreign_toplevel_handle_v1_add_listener(toplevel, 119 | &toplevel_handle_v1_impl, toplevel_entry); 120 | 121 | self->flowbox_main.append(*toplevel_entry); 122 | } 123 | 124 | zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_v1_impl = { 125 | .toplevel = handle_manager_toplevel, 126 | }; 127 | 128 | void registry_handler(void* data, struct wl_registry* registry, 129 | uint32_t id, const char* interface, uint32_t version) { 130 | 131 | auto self = static_cast(data); 132 | if (strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { 133 | auto zwlr_toplevel_manager = (zwlr_foreign_toplevel_manager_v1*) 134 | wl_registry_bind(registry, id, 135 | &zwlr_foreign_toplevel_manager_v1_interface, 136 | std::min(version, 3u)); 137 | zwlr_foreign_toplevel_manager_v1_add_listener(zwlr_toplevel_manager, 138 | &toplevel_manager_v1_impl, self); 139 | } 140 | } 141 | 142 | wl_registry_listener registry_listener = { 143 | ®istry_handler 144 | }; 145 | 146 | module_taskbar::module_taskbar(sysbar* window, const bool &icon_on_start) : module(window, icon_on_start) { 147 | // Undo normal widget stuff 148 | get_style_context()->remove_class("module"); 149 | set_cursor(Gdk::Cursor::create("default")); 150 | image_icon.unparent(); 151 | label_info.unparent(); 152 | 153 | std::string cfg_text_length = win->config_main["taskbar"]["text-length"]; 154 | if (!cfg_text_length.empty()) 155 | cfg.text_length = std::stoi(cfg_text_length); 156 | 157 | std::string cfg_icon_size = win->config_main["taskbar"]["icon-size"]; 158 | if (!cfg_icon_size.empty()) 159 | cfg.icon_size = std::stoi(cfg_icon_size); 160 | 161 | cfg.show_icon = (win->config_main["taskbar"]["show-icon"] == "true"); 162 | 163 | cfg.show_label = (win->config_main["taskbar"]["show-label"] == "true"); 164 | 165 | 166 | if (win->position %2 == 0) { 167 | box_container.set_halign(Gtk::Align::CENTER); 168 | flowbox_main.set_min_children_per_line(25); 169 | flowbox_main.set_max_children_per_line(25); 170 | } 171 | else 172 | box_container.set_valign(Gtk::Align::CENTER); 173 | 174 | flowbox_main.signal_child_activated().connect([this](Gtk::FlowBoxChild* child) { 175 | auto toplevel_entry = static_cast(child->get_child()); 176 | auto gseat = Gdk::Display::get_default()->get_default_seat(); 177 | auto seat = gdk_wayland_seat_get_wl_seat(gseat->gobj()); 178 | zwlr_foreign_toplevel_handle_v1_activate(toplevel_entry->handle, seat); 179 | }); 180 | 181 | box_container.get_style_context()->add_class("module_taskbar"); 182 | box_container.append(flowbox_main); 183 | 184 | scrolledwindow.set_child(box_container); 185 | scrolledwindow.set_policy(Gtk::PolicyType::NEVER,Gtk::PolicyType::NEVER); 186 | scrolledwindow.set_hexpand_set(true); 187 | scrolledwindow.set_vexpand_set(true); 188 | append(scrolledwindow); 189 | 190 | // Cleanup applist 191 | app_list = Gio::AppInfo::get_all(); 192 | app_list.erase( 193 | std::remove_if(app_list.begin(), app_list.end(), 194 | [](const Glib::RefPtr& app_info) { 195 | return !app_info->should_show(); 196 | }), 197 | app_list.end() 198 | ); 199 | 200 | setup_proto(); 201 | } 202 | 203 | void module_taskbar::setup_proto() { 204 | auto gdk_display = gdk_display_get_default(); 205 | auto display = gdk_wayland_display_get_wl_display(gdk_display); 206 | auto registry = wl_display_get_registry(display); 207 | wl_registry_add_listener(registry, ®istry_listener, this); 208 | wl_display_roundtrip(display); 209 | } 210 | 211 | taskbar_item::taskbar_item(module_taskbar* self, const module_taskbar::config_tb& cfg) { 212 | text_length = cfg.text_length; 213 | image_icon.set_pixel_size(cfg.icon_size); 214 | auto gesture_drag = Gtk::GestureDrag::create(); 215 | 216 | // There's probably a better way of doing this 217 | gesture_drag->signal_drag_begin().connect([&, self](const double& x, const double& y) { 218 | self->win->gesture_drag->set_propagation_phase(Gtk::PropagationPhase::NONE); 219 | }); 220 | gesture_drag->signal_drag_update().connect([&](const double& x, const double& y) { 221 | }); 222 | gesture_drag->signal_drag_end().connect([&, self](const double& x, const double& y) { 223 | self->win->gesture_drag->set_propagation_phase(Gtk::PropagationPhase::BUBBLE); 224 | }); 225 | 226 | add_controller(gesture_drag); 227 | append(image_icon); 228 | append(toplevel_label); 229 | 230 | image_icon.set_visible(cfg.show_icon); 231 | toplevel_label.set_visible(cfg.show_label); 232 | 233 | if (!cfg.show_label) { 234 | image_icon.set_hexpand(true); 235 | image_icon.set_vexpand(true); 236 | return; 237 | } 238 | 239 | if (self->flowbox_main.get_min_children_per_line() == 25) 240 | set_size_request(100, -1); 241 | else 242 | set_size_request(-1, 100); 243 | 244 | toplevel_label.set_margin_start(3); 245 | } 246 | -------------------------------------------------------------------------------- /src/modules/taskbar.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_TASKBAR 4 | 5 | #include "../wlr-foreign-toplevel-management-unstable-v1.h" 6 | 7 | #include 8 | #include 9 | 10 | class module_taskbar : public module { 11 | public: 12 | module_taskbar(sysbar*, const bool&); 13 | 14 | struct config_tb { 15 | int text_length = 14; 16 | int icon_size = 34; 17 | bool show_icon = true; 18 | bool show_label = true; 19 | } cfg; 20 | 21 | Gtk::FlowBox flowbox_main; 22 | 23 | private: 24 | Gtk::Box box_container; 25 | Gtk::ScrolledWindow scrolledwindow; 26 | 27 | void setup_proto(); 28 | }; 29 | 30 | class taskbar_item : public Gtk::Box { 31 | public: 32 | taskbar_item(module_taskbar*, const module_taskbar::config_tb&); 33 | 34 | Gtk::Label toplevel_label; 35 | Gtk::Image image_icon; 36 | zwlr_foreign_toplevel_handle_v1* handle; 37 | 38 | uint text_length; 39 | }; 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /src/modules/tray.cpp: -------------------------------------------------------------------------------- 1 | #include "tray.hpp" 2 | 3 | #include 4 | #include 5 | 6 | // Tray module 7 | module_tray::module_tray(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start), m_icon_on_start(icon_on_start) { 8 | get_style_context()->add_class("module_tray"); 9 | label_info.hide(); 10 | 11 | box_container.get_style_context()->add_class("tray_container"); 12 | 13 | // TODO: Add an option to disable the revealer 14 | // TODO: Add an option to set the revealer's transition duration 15 | // TODO: Set custom transition types based on bar position or icon_on_start 16 | 17 | Gtk::RevealerTransitionType transition_type; 18 | 19 | // Set orientation 20 | if (win->position % 2) { 21 | image_icon.set_from_icon_name(icon_on_start ? "arrow-down" : "arrow-up"); 22 | transition_type = icon_on_start ? Gtk::RevealerTransitionType::SLIDE_DOWN : Gtk::RevealerTransitionType::SLIDE_UP; 23 | box_container.set_orientation(Gtk::Orientation::VERTICAL); 24 | } 25 | else { 26 | image_icon.set_from_icon_name(icon_on_start ? "arrow-right" : "arrow-left"); 27 | transition_type = icon_on_start ? Gtk::RevealerTransitionType::SLIDE_RIGHT : Gtk::RevealerTransitionType::SLIDE_LEFT; 28 | } 29 | 30 | revealer_box.set_child(box_container); 31 | revealer_box.set_transition_type(transition_type); 32 | revealer_box.set_transition_duration(250); 33 | revealer_box.set_reveal_child(false); 34 | 35 | if (icon_on_start) 36 | append(revealer_box); 37 | else 38 | prepend(revealer_box); 39 | 40 | // Custom on_clicked handle 41 | gesture_click = Gtk::GestureClick::create(); 42 | gesture_click->set_button(GDK_BUTTON_PRIMARY); 43 | gesture_click->signal_pressed().connect(sigc::mem_fun(*this, &module_tray::on_clicked)); 44 | add_controller(gesture_click); 45 | } 46 | 47 | void module_tray::on_clicked(const int& n_press, const double& x, const double& y) { 48 | bool revealed = revealer_box.get_reveal_child(); 49 | 50 | revealer_box.set_reveal_child(!revealed); 51 | if (win->position % 2) 52 | image_icon.set_from_icon_name(m_icon_on_start == revealed ? "arrow-down" : "arrow-up"); 53 | else 54 | image_icon.set_from_icon_name(m_icon_on_start == revealed ? "arrow-right" : "arrow-left"); 55 | 56 | // Prevent gestures bellow from triggering 57 | gesture_click->reset(); 58 | } 59 | 60 | // Tray watcher 61 | tray_watcher::tray_watcher(Gtk::Box* box) : hosts_counter(0), watcher_id(0) { 62 | box_container = box; 63 | auto pid = std::to_string(getpid()); 64 | auto host_id = "org.kde.StatusNotifierHost-" + pid + "-" + std::to_string(++hosts_counter); 65 | auto watcher_name = "org.kde.StatusNotifierWatcher"; 66 | Gio::DBus::own_name(Gio::DBus::BusType::SESSION, watcher_name, sigc::mem_fun(*this, &tray_watcher::on_bus_acquired_watcher)); 67 | Gio::DBus::own_name(Gio::DBus::BusType::SESSION, host_id, sigc::mem_fun(*this, &tray_watcher::on_bus_acquired_host)); 68 | } 69 | 70 | void tray_watcher::on_bus_acquired_host(const Glib::RefPtr& conn, const Glib::ustring& name) { 71 | watcher_id = Gio::DBus::watch_name(conn,"org.kde.StatusNotifierWatcher", 72 | sigc::mem_fun(*this, &tray_watcher::on_name_appeared), 73 | sigc::mem_fun(*this, &tray_watcher::on_name_vanished)); 74 | } 75 | 76 | void tray_watcher::on_bus_acquired_watcher(const Glib::RefPtr& conn, const Glib::ustring& name) { 77 | const auto introspection_data = Gio::DBus::NodeInfo::create_for_xml( 78 | R"( 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | )")->lookup_interface(); 103 | 104 | conn->register_object("/StatusNotifierWatcher", introspection_data, interface_table); 105 | watcher_connection = conn; 106 | } 107 | 108 | void tray_watcher::on_interface_method_call( 109 | const Glib::RefPtr& connection, 110 | const Glib::ustring& sender, 111 | const Glib::ustring& object_path, 112 | const Glib::ustring& interface_name, 113 | const Glib::ustring& method_name, 114 | const Glib::VariantContainerBase& parameters, 115 | const Glib::RefPtr& invocation) { 116 | 117 | handle_signal(sender, method_name, parameters); 118 | 119 | invocation->return_value(Glib::VariantContainerBase()); 120 | } 121 | 122 | void tray_watcher::on_interface_get_property(Glib::VariantBase& property, 123 | const Glib::RefPtr& connection, 124 | const Glib::ustring & sender, const Glib::ustring& object_path, 125 | const Glib::ustring & interface_name, const Glib::ustring& property_name) { 126 | 127 | // TODO: This sucks, Use real data!! 128 | // Write actual code to get stuff 129 | if (property_name == "RegisteredStatusNotifierItems") { 130 | std::vector sn_items_names; 131 | sn_items_names.reserve(1); 132 | property = Glib::Variant>::create(sn_items_names); 133 | } 134 | else if (property_name == "IsStatusNotifierHostRegistered") { 135 | property = Glib::Variant::create(0); 136 | } 137 | else if (property_name == "ProtocolVersion") { 138 | property = Glib::Variant::create(0); 139 | } 140 | else { 141 | std::fprintf(stderr, "Unknown property: %s\n", property_name.c_str()); 142 | } 143 | } 144 | 145 | void tray_watcher::on_name_appeared(const Glib::RefPtr& conn, const Glib::ustring& name, const Glib::ustring& owner) { 146 | Gio::DBus::Proxy::create(conn, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher", 147 | [this, host_name = name](const Glib::RefPtr &result) { 148 | watcher_proxy = Gio::DBus::Proxy::create_finish(result); 149 | watcher_proxy->call("RegisterStatusNotifierHost", Glib::Variant>::create({ host_name })); 150 | watcher_proxy->signal_signal().connect(sigc::mem_fun(*this, &tray_watcher::handle_signal)); 151 | 152 | Glib::Variant> registered_items; 153 | watcher_proxy->get_cached_property(registered_items, "RegisteredStatusNotifierItems"); 154 | 155 | // Add existing items 156 | for (const auto& service : registered_items.get()) { 157 | items.emplace(service, service); 158 | auto it = items.find(service); 159 | tray_item& item = it->second; 160 | box_container->prepend(item); 161 | } 162 | }); 163 | } 164 | 165 | void tray_watcher::on_name_vanished(const Glib::RefPtr& conn, const Glib::ustring& name) { 166 | Gio::DBus::unwatch_name(watcher_id); 167 | } 168 | 169 | void tray_watcher::handle_signal(const Glib::ustring& sender, const Glib::ustring& signal, const Glib::VariantContainerBase& params) { 170 | if (!params.is_of_type(Glib::VariantType("(s)"))) 171 | return; 172 | 173 | Glib::Variant item_path; 174 | params.get_child(item_path); 175 | Glib::ustring service = item_path.get(); 176 | 177 | if (signal == "RegisterStatusNotifierItem") { 178 | auto dbus_name = ((service[0] == '/') ? sender : service); 179 | auto dbus_path = ((service[0] == '/') ? service : "/StatusNotifierItem"); 180 | 181 | items.emplace(sender, dbus_name + dbus_path); 182 | auto it = items.find(sender); 183 | tray_item& item = it->second; 184 | box_container->prepend(item); 185 | 186 | // TODO: Add cleanup code 187 | Gio::DBus::watch_name( 188 | Gio::DBus::BusType::SESSION, 189 | sender, {}, [this, sender] (const Glib::RefPtr &conn, const Glib::ustring &name) { 190 | watcher_connection->emit_signal( 191 | "/StatusNotifierWatcher", 192 | "org.kde.StatusNotifierWatcher", 193 | "StatusNotifierItemUnregistered", 194 | {}, 195 | Glib::Variant>::create(std::tuple(sender))); 196 | }); 197 | } 198 | else if (signal == "StatusNotifierItemUnregistered") { 199 | auto it = items.find(service); 200 | tray_item& item = it->second; 201 | box_container->remove(item); 202 | items.erase(service); 203 | } 204 | } 205 | 206 | // Tray item 207 | tray_item::tray_item(const Glib::ustring& service) { 208 | get_style_context()->add_class("tray_item"); 209 | const auto slash_ind = service.find('/'); 210 | dbus_name = service.substr(0, slash_ind); 211 | dbus_path = (slash_ind != Glib::ustring::npos) ? service.substr(slash_ind) : "/StatusNotifierItem"; 212 | 213 | Gio::DBus::Proxy::create_for_bus( 214 | Gio::DBus::BusType::SESSION, 215 | dbus_name, 216 | dbus_path, 217 | "org.kde.StatusNotifierItem", 218 | [this](const Glib::RefPtr &result) { 219 | item_proxy = Gio::DBus::Proxy::create_for_bus_finish(result); 220 | 221 | item_proxy->signal_signal().connect([this](const Glib::ustring&, 222 | const Glib::ustring& signal, 223 | const Glib::VariantContainerBase&) { 224 | if (signal.size() >= 3) { 225 | std::string_view property(signal.c_str() + 3, signal.size() - 3); 226 | update_properties(); 227 | } 228 | }); 229 | 230 | update_properties(); 231 | } 232 | ); 233 | 234 | // TODO: add option to use custom icon sizes 235 | set_size_request(22,22); 236 | 237 | // Right click menu 238 | gesture_right_click = Gtk::GestureClick::create(); 239 | gesture_right_click->set_button(GDK_BUTTON_SECONDARY); 240 | gesture_right_click->signal_pressed().connect(sigc::mem_fun(*this, &tray_item::on_right_clicked)); 241 | add_controller(gesture_right_click); 242 | 243 | flowbox_context.signal_child_activated().connect(sigc::mem_fun(*this, &tray_item::on_menu_item_click)); 244 | flowbox_context.set_selection_mode(Gtk::SelectionMode::NONE); 245 | flowbox_context.set_max_children_per_line(1); 246 | 247 | popover_context.get_style_context()->add_class("context_menu"); 248 | popover_context.set_child(flowbox_context); 249 | popover_context.set_offset(0, 5); 250 | popover_context.set_has_arrow(false); 251 | popover_context.set_parent(*this); 252 | } 253 | 254 | tray_item::~tray_item() { 255 | popover_context.unparent(); 256 | } 257 | 258 | namespace { 259 | const Glib::RefPtr extract_pixbuf(std::vector>> pixbuf_data) { 260 | if (pixbuf_data.empty()) 261 | return {}; 262 | 263 | auto chosen_image = std::max_element(pixbuf_data.begin(), pixbuf_data.end()); 264 | auto &[width, height, data] = *chosen_image; 265 | 266 | // Convert ARGB to RGBA 267 | for (size_t i = 0; i + 3 < data.size(); i += 4) { 268 | const auto alpha = data[i]; 269 | data[i] = data[i + 1]; 270 | data[i + 1] = data[i + 2]; 271 | data[i + 2] = data[i + 3]; 272 | data[i + 3] = alpha; 273 | } 274 | 275 | auto *data_ptr = new auto(std::move(data)); 276 | 277 | return Gdk::Pixbuf::create_from_data( 278 | data_ptr->data(), 279 | Gdk::Colorspace::RGB, 280 | true, 281 | 8, 282 | width, 283 | height, 284 | 4 * width, 285 | [data_ptr] (auto*) { delete data_ptr; } 286 | ); 287 | } 288 | } 289 | 290 | void tray_item::build_menu(const Glib::VariantBase& layout) { 291 | auto layout_tuple = Glib::VariantBase::cast_dynamic(layout); 292 | 293 | // This mess has to get cleaned up one day.. 294 | int id = Glib::VariantBase::cast_dynamic>(layout_tuple.get_child(0)).get(); 295 | auto properties_dict = Glib::VariantBase::cast_dynamic>>(layout_tuple.get_child(1)); 296 | auto children = Glib::VariantBase::cast_dynamic>>(layout_tuple.get_child(2)); 297 | 298 | for (const auto& key_value : properties_dict.get()) { 299 | if (key_value.first == "label") { 300 | std::string label_str = key_value.second.print(); 301 | label_str = label_str.substr(1, label_str.length() - 2); 302 | 303 | Gtk::Label* label = Gtk::make_managed(label_str); 304 | label->set_name(std::to_string(id)); 305 | label->set_use_underline(true); 306 | flowbox_context.append(*label); 307 | } 308 | else if (key_value.first == "type") { 309 | std::string type = key_value.second.print(); 310 | type = type.substr(1, type.size() - 2); 311 | 312 | if (type == "separator") { 313 | Gtk::FlowBoxChild* child = Gtk::make_managed(); 314 | child->set_child(*Gtk::make_managed()); 315 | child->set_sensitive(false); 316 | flowbox_context.append(*child); 317 | } 318 | } 319 | } 320 | 321 | for (const auto& child : children.get()) { 322 | build_menu(child); 323 | } 324 | } 325 | 326 | void tray_item::update_properties() { 327 | Glib::VariantBase variant; 328 | item_proxy->get_cached_property(variant, "Title"); 329 | auto label = Glib::VariantBase::cast_dynamic>(variant).get(); 330 | 331 | item_proxy->get_cached_property(variant, "ToolTip"); 332 | auto [tooltip_icon_name, tooltip_icon_data, tooltip_title, tooltip_text] = (variant) ? 333 | Glib::VariantBase::cast_dynamic>>, Glib::ustring, Glib::ustring>>>(variant).get() : 334 | std::make_tuple(Glib::ustring{}, std::vector>>{}, Glib::ustring{}, Glib::ustring{}); 335 | 336 | item_proxy->get_cached_property(variant, "IconThemePath"); 337 | auto icon_theme_path = (variant) ? Glib::VariantBase::cast_dynamic>(variant).get() : ""; 338 | 339 | item_proxy->get_cached_property(variant, "Status"); 340 | auto status = Glib::VariantBase::cast_dynamic>(variant).get(); 341 | Glib::ustring icon_type_name = status == "NeedsAttention" ? "AttentionIcon" : "Icon"; 342 | 343 | item_proxy->get_cached_property(variant, icon_type_name + "Name"); 344 | auto icon_name = Glib::VariantBase::cast_dynamic>(variant).get(); 345 | 346 | item_proxy->get_cached_property(variant, "Menu"); 347 | menu_path = Glib::VariantBase::cast_dynamic>(variant).get(); 348 | 349 | if (!tooltip_title.empty()) 350 | set_tooltip_text(tooltip_title); 351 | else 352 | set_tooltip_text(label); 353 | 354 | const std::string icon_path = icon_theme_path + "/" + icon_name + ".png"; 355 | 356 | if (std::filesystem::exists(icon_path)) { 357 | set(icon_path); 358 | } 359 | else { 360 | Glib::VariantBase variant; 361 | item_proxy->get_cached_property(variant, icon_type_name + "Pixmap"); 362 | 363 | if (variant) { 364 | auto pixmap = Glib::VariantBase::cast_dynamic>>>>(variant).get(); 365 | set(extract_pixbuf(pixmap)); 366 | } 367 | } 368 | 369 | if (menu_path.empty()) 370 | return; 371 | 372 | // TODO: Maybe don't rebuild the menu on EVERY update? 373 | // This is very horrible 374 | 375 | auto proxy = Gio::DBus::Proxy::create_sync( 376 | Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SESSION), 377 | dbus_name, 378 | menu_path, 379 | "com.canonical.dbusmenu" // Does this change? 380 | ); 381 | 382 | std::vector args_vector; 383 | args_vector.push_back(Glib::Variant::create(0)); 384 | args_vector.push_back(Glib::Variant::create(1)); 385 | args_vector.push_back(Glib::Variant>::create({})); 386 | 387 | auto args = Glib::VariantContainerBase::create_tuple(args_vector); 388 | 389 | auto result = proxy->call_sync("GetLayout", args); 390 | auto result_tuple = Glib::VariantBase::cast_dynamic(result); 391 | 392 | build_menu(result_tuple.get_child(1)); 393 | } 394 | 395 | void tray_item::on_right_clicked(const int& n_press, const double& x, const double& y) { 396 | popover_context.popup(); 397 | } 398 | 399 | void tray_item::on_menu_item_click(Gtk::FlowBoxChild* child) { 400 | Gtk::Label* label = dynamic_cast(child->get_child()); 401 | 402 | auto connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SESSION); 403 | 404 | auto now = std::chrono::system_clock::now(); 405 | auto duration = now.time_since_epoch(); 406 | auto timestamp = std::chrono::duration_cast(duration).count(); 407 | 408 | auto parameters_variant = Glib::Variant>::create( 409 | std::make_tuple( 410 | std::stoi(label->get_name()), 411 | "clicked", 412 | Glib::Variant::create(0), // No clue what this is 413 | timestamp 414 | ) 415 | ); 416 | 417 | auto message = Gio::DBus::Message::create_method_call( 418 | dbus_name, 419 | menu_path, 420 | "com.canonical.dbusmenu", // Does this change? (Part 2) 421 | "Event" 422 | ); 423 | 424 | message->set_body(parameters_variant); 425 | auto response = connection->send_message_with_reply_sync(message, -1); 426 | popover_context.popdown(); 427 | } 428 | -------------------------------------------------------------------------------- /src/modules/tray.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_TRAY 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class tray_item : public Gtk::Image { 13 | public: 14 | tray_item(const Glib::ustring &service); 15 | ~tray_item(); 16 | 17 | private: 18 | Gtk::Popover popover_context; 19 | Gtk::FlowBox flowbox_context; 20 | Glib::ustring dbus_name; 21 | Glib::ustring dbus_path; 22 | Glib::DBusObjectPathString menu_path; 23 | Glib::RefPtr item_proxy; 24 | Glib::RefPtr gesture_right_click; 25 | 26 | void on_right_clicked(const int&, const double&, const double&); 27 | void on_menu_item_click(Gtk::FlowBoxChild*); 28 | void update_properties(); 29 | void build_menu(const Glib::VariantBase&); 30 | }; 31 | 32 | class tray_watcher { 33 | public: 34 | tray_watcher(Gtk::Box*); 35 | 36 | private: 37 | Gtk::Box* box_container; 38 | std::map items; 39 | int hosts_counter; 40 | 41 | guint watcher_id; 42 | Glib::RefPtr watcher_connection; 43 | Glib::RefPtr watcher_proxy; 44 | 45 | const Gio::DBus::InterfaceVTable interface_table = 46 | Gio::DBus::InterfaceVTable( 47 | sigc::mem_fun(*this, &tray_watcher::on_interface_method_call), 48 | sigc::mem_fun(*this, &tray_watcher::on_interface_get_property)); 49 | 50 | void on_interface_method_call(const Glib::RefPtr&, 51 | const Glib::ustring&, 52 | const Glib::ustring&, const Glib::ustring&, 53 | const Glib::ustring&, const Glib::VariantContainerBase&, 54 | const Glib::RefPtr&); 55 | 56 | void on_interface_get_property(Glib::VariantBase&, 57 | const Glib::RefPtr&, 58 | const Glib::ustring&, const Glib::ustring&, 59 | const Glib::ustring&, const Glib::ustring&); 60 | 61 | void on_bus_acquired_host(const Glib::RefPtr&, const Glib::ustring&); 62 | void on_bus_acquired_watcher(const Glib::RefPtr&, const Glib::ustring&); 63 | void on_name_appeared(const Glib::RefPtr&, const Glib::ustring &name, const Glib::ustring&); 64 | void on_name_vanished(const Glib::RefPtr&, const Glib::ustring&); 65 | void handle_signal(const Glib::ustring&, const Glib::ustring&, const Glib::VariantContainerBase&); 66 | }; 67 | 68 | class module_tray : public module { 69 | public: 70 | module_tray(sysbar*, const bool& = true); 71 | 72 | private: 73 | bool m_icon_on_start; 74 | Gtk::Revealer revealer_box; 75 | Gtk::Box box_container; 76 | 77 | tray_watcher watcher = tray_watcher(&box_container); 78 | Glib::RefPtr gesture_click; 79 | void on_clicked(const int&, const double&, const double&); 80 | }; 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /src/modules/volume.cpp: -------------------------------------------------------------------------------- 1 | #include "volume.hpp" 2 | 3 | #include 4 | 5 | module_volume::module_volume(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 6 | get_style_context()->add_class("module_volume"); 7 | volume_icons[0] = "audio-volume-low-symbolic"; 8 | volume_icons[1] = "audio-volume-medium-symbolic"; 9 | volume_icons[2] = "audio-volume-high-symbolic"; 10 | label_info.hide(); 11 | 12 | 13 | if (win->config_main["volume"]["show-label"] == "true") 14 | label_info.show(); 15 | 16 | std::string cfg_layout = win->config_main["volume"]["widget-layout"]; 17 | if (!cfg_layout.empty()) { 18 | widget_layout.clear(); 19 | for (char c : cfg_layout) { 20 | widget_layout.push_back(c - '0'); 21 | } 22 | } 23 | 24 | dispatcher_callback.connect(sigc::mem_fun(*this, &module_volume::update_info)); 25 | dispatcher_load.connect(sigc::mem_fun(*this, &module_volume::load_wireplumber)); 26 | dispatcher_load.emit(); 27 | 28 | setup_widget(); 29 | } 30 | 31 | void module_volume::setup_widget() { 32 | box_widget.get_style_context()->add_class("widget_volume"); 33 | 34 | image_widget_icon.set_pixel_size(24); 35 | 36 | scale_volume.set_hexpand(true); 37 | scale_volume.set_vexpand(true); 38 | scale_volume.set_range(0, 100); 39 | scale_volume.signal_change_value().connect([&](Gtk::ScrollType scroll_type, double val) { 40 | sys_wp->set_volume(true, val); 41 | return true; 42 | }, true); 43 | 44 | if (widget_layout[2] < widget_layout[3]) { // Vertical layout 45 | box_widget.set_orientation(Gtk::Orientation::VERTICAL); 46 | scale_volume.set_orientation(Gtk::Orientation::VERTICAL); 47 | box_widget.append(scale_volume); 48 | box_widget.append(image_widget_icon); 49 | scale_volume.set_inverted(true); 50 | } 51 | else { // Horizontal layout 52 | box_widget.append(image_widget_icon); 53 | box_widget.append(scale_volume); 54 | } 55 | 56 | win->grid_widgets_end.attach(box_widget, widget_layout[0], widget_layout[1], widget_layout[2], widget_layout[3]); 57 | } 58 | 59 | void module_volume::update_info() { 60 | label_info.set_text(std::to_string(sys_wp->volume)); 61 | if (sys_wp->muted) 62 | image_icon.set_from_icon_name("audio-volume-muted-blocking-symbolic"); 63 | else 64 | image_icon.set_from_icon_name(volume_icons[sys_wp->volume / 35]); 65 | 66 | // Widget 67 | image_widget_icon.set_from_icon_name(image_icon.get_icon_name()); 68 | scale_volume.set_value(sys_wp->volume); 69 | set_tooltip_text(std::to_string(sys_wp->volume) + "%"); 70 | } 71 | 72 | void module_volume::load_wireplumber() { 73 | sys_wp = new syshud_wireplumber(nullptr, &dispatcher_callback); 74 | } -------------------------------------------------------------------------------- /src/modules/volume.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_VOLUME 4 | 5 | #include "../wireplumber.hpp" 6 | #include 7 | 8 | class module_volume : public module { 9 | public: 10 | module_volume(sysbar*, const bool&); 11 | 12 | private: 13 | Glib::Dispatcher dispatcher_callback; 14 | Glib::Dispatcher dispatcher_load; 15 | Gtk::Scale scale_volume; 16 | Gtk::Image image_widget_icon; 17 | Gtk::Box box_widget; 18 | syshud_wireplumber *sys_wp; 19 | 20 | std::map volume_icons; 21 | std::vector widget_layout = {0, 4, 4, 1}; 22 | 23 | void update_info(); 24 | void setup_widget(); 25 | void load_wireplumber(); 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /src/modules/weather.cpp: -------------------------------------------------------------------------------- 1 | #include "weather.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | module_weather::module_weather(sysbar* window, const bool& icon_on_start) : module(window, icon_on_start) { 12 | get_style_context()->add_class("module_weather"); 13 | image_icon.set_from_icon_name("content-loading-symbolic"); 14 | label_info.hide(); 15 | 16 | std::string cfg_url = win->config_main["weather"]["url"]; 17 | if (!cfg_url.empty()) 18 | weather_file_url = cfg_url; 19 | 20 | std::string cfg_unit = win->config_main["weather"]["unit"]; 21 | if (!cfg_unit.empty()) 22 | unit = cfg_unit[0]; 23 | 24 | 25 | std::thread update_thread(&module_weather::update_info, this); 26 | update_thread.detach(); 27 | 28 | Glib::signal_timeout().connect(sigc::mem_fun(*this, &module_weather::update_info), 60 * 60 * 1000); // Update every hour 29 | } 30 | 31 | bool module_weather::update_info() { 32 | // TODO: Ideally this should run on another thread, 33 | // You know.. Juuuust in case it blocks ui updataes 34 | 35 | std::string home_dir = getenv("HOME"); 36 | weather_file = std::move(home_dir) + "/.cache/sysbar-weather.json"; 37 | 38 | std::ifstream file(weather_file, std::ios::ate); 39 | 40 | // Check if the file is Ok 41 | if (file.tellg() < 10) { 42 | file.close(); 43 | download_file(); 44 | file.open(weather_file, std::ios::ate); 45 | } 46 | 47 | // The file is not okay 48 | if (!file.is_open() || file.tellg() < 10) { 49 | image_icon.set_from_icon_name("weather-none-available-symbolic"); 50 | std::fprintf(stderr, "Failed to parse weather data\n"); 51 | return true; 52 | } 53 | 54 | // Sanity check 55 | file.seekg(0); 56 | Json::Value root; 57 | Json::CharReaderBuilder builder; 58 | std::string errs; 59 | 60 | if (!Json::parseFromStream(builder, file, &root, &errs)) { 61 | image_icon.set_from_icon_name("weather-none-available-symbolic"); 62 | std::fprintf(stderr, "The weather file does not seem to be valid: %s\n", errs.c_str()); 63 | return false; 64 | } 65 | 66 | // Reset the file pointer and load the data 67 | file.seekg(0, std::ios::beg); 68 | file >> json_data; 69 | file.close(); 70 | 71 | // Get time and date 72 | const std::time_t& t = std::time(nullptr); 73 | std::tm* now = std::localtime(&t); 74 | 75 | std::ostringstream date_stream; 76 | date_stream << std::put_time(now, "%Y-%m-%d"); 77 | 78 | const std::string& date = date_stream.str(); 79 | const std::string& time = std::to_string((now->tm_hour / 3) * 300); 80 | 81 | get_weather_data(date, time); 82 | 83 | if (unit == 'c') 84 | label_info.set_text(weather_info_current.temp_C);\ 85 | else if (unit == 'f') 86 | label_info.set_text(weather_info_current.temp_F); 87 | else 88 | std::fprintf(stderr, "Unknown unit: %c\n", unit); 89 | 90 | // Add more cases, Snow, Storms, ect ect 91 | std::map icon_from_desc = { 92 | {"sunny", "weather-clear-symbolic"}, 93 | {"clear", "weather-clear-symbolic"}, 94 | {"partly cloudy", "weather-few-clouds-symbolic"}, 95 | {"cloudy", "weather-clouds-symbolic"}, 96 | {"overcast", "weather-overcast-symbolic"}, 97 | {"patchy rain nearby", "weather-showers-scattered-symbolic"}, 98 | {"patchy rain possible", "weather-showers-scattered-symbolic"}, 99 | {"patchy light rain", "weather-showers-scattered-symbolic"}, 100 | {"light rain", "weather-showers-scattered-symbolic"}, 101 | {"light rain shower", "weather-showers-scattered-symbolic"}, 102 | }; 103 | 104 | // Set icon according to weather description 105 | if (icon_from_desc.find(weather_info_current.weatherDesc) != icon_from_desc.end()) 106 | image_icon.set_from_icon_name(icon_from_desc[weather_info_current.weatherDesc]); 107 | else 108 | image_icon.set_from_icon_name("weather-none-available-symbolic"); 109 | 110 | set_tooltip_text(weather_info_current.weatherDesc); 111 | 112 | return true; 113 | } 114 | 115 | void module_weather::download_file() { 116 | CURL *curl; 117 | FILE *fp; 118 | CURLcode res; 119 | curl = curl_easy_init(); 120 | if (!curl) { 121 | image_icon.set_from_icon_name("weather-none-available-symbolic"); 122 | std::fprintf(stderr, "Error: unable to initialize curl.\n"); 123 | return; 124 | } 125 | 126 | fp = fopen(weather_file.c_str(), "wb"); 127 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); 128 | curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); 129 | curl_easy_setopt(curl, CURLOPT_URL, weather_file_url.c_str()); 130 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); 131 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); 132 | res = curl_easy_perform(curl); 133 | curl_easy_cleanup(curl); 134 | fclose(fp); 135 | 136 | if (res != CURLE_OK) { 137 | image_icon.set_from_icon_name("weather-none-available-symbolic"); 138 | std::fprintf(stderr, "Error: curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); 139 | return; 140 | } 141 | } 142 | 143 | void module_weather::get_weather_data(const std::string& date, const std::string& time) { 144 | Json::Value weatherArray = json_data["weather"]; 145 | 146 | // Iterate over each date in the weather array 147 | for (const auto& dailyWeather : weatherArray) { 148 | if (dailyWeather["date"].asString() == date) { 149 | Json::Value hourlyArray = dailyWeather["hourly"]; 150 | 151 | // Iterate over each hourly section 152 | for (const auto& hourly : hourlyArray) { 153 | if (hourly["time"].asString() == time) { 154 | weather_info_current.feels_like_C = hourly["FeelsLikeC"].asString(); 155 | weather_info_current.feels_like_F = hourly["FeelsLikeF"].asString(); 156 | weather_info_current.temp_C = hourly["tempC"].asString(); 157 | weather_info_current.temp_F = hourly["tempF"].asString(); 158 | weather_info_current.humidity = hourly["humidity"].asString(); 159 | weather_info_current.weatherDesc = hourly["weatherDesc"][0]["value"].asString(); 160 | std::transform(weather_info_current.weatherDesc.begin(), 161 | weather_info_current.weatherDesc.end(), 162 | weather_info_current.weatherDesc.begin(), 163 | [](unsigned char c) { return std::tolower(c); }); 164 | 165 | // For whatever reason, sometimes the last character is a space 166 | if (weather_info_current.weatherDesc.back() == ' ') 167 | weather_info_current.weatherDesc.pop_back(); 168 | label_info.show(); 169 | return; 170 | } 171 | } 172 | } 173 | } 174 | 175 | // If we reach this point then the file is probably out of date 176 | download_file(); 177 | } 178 | -------------------------------------------------------------------------------- /src/modules/weather.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../module.hpp" 3 | #ifdef MODULE_WEATHER 4 | 5 | #include 6 | 7 | class module_weather : public module { 8 | public: 9 | module_weather(sysbar*, const bool&); 10 | 11 | private: 12 | struct weather_info { 13 | std::string feels_like_C; 14 | std::string feels_like_F; 15 | std::string temp_C; 16 | std::string temp_F; 17 | std::string humidity; 18 | std::string weatherDesc; 19 | } weather_info_current; 20 | 21 | Json::Value json_data; 22 | char unit = 'c'; 23 | std::string weather_file; 24 | std::string weather_file_url = "https://wttr.in/?format=j1"; 25 | 26 | bool update_info(); 27 | void download_file(); 28 | void get_weather_data(const std::string&, const std::string&); 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/overlay.cpp: -------------------------------------------------------------------------------- 1 | #include "window.hpp" 2 | 3 | #include 4 | 5 | /* 6 | Just as a heads up to whomever decides to work on this. 7 | You are about to see some horrible & nonsensical code. 8 | YOU HAVE BEEN WARNED!! 9 | */ 10 | 11 | void sysbar::setup_overlay() { 12 | overlay_window.set_name("sysbar_overlay"); 13 | gtk_layer_init_for_window(overlay_window.gobj()); 14 | gtk_layer_set_namespace(overlay_window.gobj(), "sysbar-overlay"); 15 | gtk_layer_set_layer(overlay_window.gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY); 16 | gtk_layer_set_keyboard_mode(overlay_window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); 17 | gtk_layer_set_monitor(overlay_window.gobj(), monitor); 18 | 19 | gtk_layer_set_anchor(overlay_window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); 20 | gtk_layer_set_anchor(overlay_window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); 21 | gtk_layer_set_anchor(overlay_window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); 22 | gtk_layer_set_anchor(overlay_window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); 23 | 24 | overlay_window.set_child(box_overlay); 25 | } 26 | 27 | void sysbar::setup_overlay_widgets() { 28 | int default_size_start = 350; 29 | int default_size_end = 350; 30 | 31 | 32 | std::string cfg_sidepanel_start_size = config_main["main"]["sidepanel-start-size"]; 33 | if (!cfg_sidepanel_start_size.empty()) 34 | default_size_start = std::stoi(cfg_sidepanel_start_size); 35 | 36 | std::string cfg_sidepanel_end_size = config_main["main"]["sidepanel-end-size"]; 37 | if (!cfg_sidepanel_end_size.empty()) 38 | default_size_end = std::stoi(cfg_sidepanel_start_size); 39 | 40 | 41 | int size = (position % 2) ? monitor_geometry.height : monitor_geometry.width; 42 | 43 | stack_start.get_style_context()->add_class("widgets_start"); 44 | stack_end.get_style_context()->add_class("widgets_end"); 45 | 46 | box_widgets_start.get_style_context()->add_class("sidepanel_start"); 47 | box_widgets_end.get_style_context()->add_class("sidepanel_end"); 48 | 49 | scrolled_Window_start.set_child(box_widgets_start); 50 | stack_start.add(grid_widgets_start, "main"); 51 | stack_start.set_vexpand_set(); 52 | stack_start.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 53 | box_widgets_start.append(stack_start); 54 | 55 | scrolled_Window_end.set_child(box_widgets_end); 56 | stack_end.add(grid_widgets_end, "main"); 57 | stack_end.set_vexpand_set(); 58 | stack_end.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT); 59 | box_widgets_end.append(stack_end); 60 | 61 | scrolled_Window_start.set_kinetic_scrolling(false); 62 | scrolled_Window_end.set_kinetic_scrolling(false); 63 | 64 | if (position == 0) { 65 | overlay_window.get_style_context()->add_class("position_top"); 66 | scrolled_Window_start.set_valign(Gtk::Align::START); 67 | scrolled_Window_end.set_valign(Gtk::Align::START); 68 | box_widgets_start.set_orientation(Gtk::Orientation::VERTICAL); 69 | box_widgets_end.set_orientation(Gtk::Orientation::VERTICAL); 70 | grid_widgets_start.set_valign(Gtk::Align::START); 71 | grid_widgets_end.set_valign(Gtk::Align::START); 72 | box_widgets_start.set_valign(Gtk::Align::START); 73 | box_widgets_end.set_valign(Gtk::Align::START); 74 | } 75 | else if (position == 1) { 76 | overlay_window.get_style_context()->add_class("position_right"); 77 | scrolled_Window_start.set_valign(Gtk::Align::START); 78 | scrolled_Window_end.set_valign(Gtk::Align::END); 79 | scrolled_Window_start.set_halign(Gtk::Align::END); 80 | scrolled_Window_end.set_halign(Gtk::Align::END); 81 | grid_widgets_start.set_halign(Gtk::Align::END); 82 | grid_widgets_end.set_halign(Gtk::Align::END); 83 | box_widgets_start.set_valign(Gtk::Align::START); 84 | box_widgets_end.set_valign(Gtk::Align::START); 85 | } 86 | else if (position == 2) { 87 | overlay_window.get_style_context()->add_class("position_bottom"); 88 | scrolled_Window_start.set_valign(Gtk::Align::END); 89 | scrolled_Window_end.set_valign(Gtk::Align::END); 90 | box_widgets_start.set_orientation(Gtk::Orientation::VERTICAL); 91 | box_widgets_end.set_orientation(Gtk::Orientation::VERTICAL); 92 | grid_widgets_start.set_valign(Gtk::Align::END); 93 | grid_widgets_end.set_valign(Gtk::Align::END); 94 | box_widgets_start.set_valign(Gtk::Align::END); 95 | box_widgets_end.set_valign(Gtk::Align::END); 96 | } 97 | else if (position == 3) { 98 | overlay_window.get_style_context()->add_class("position_left"); 99 | scrolled_Window_start.set_valign(Gtk::Align::START); 100 | scrolled_Window_end.set_valign(Gtk::Align::END); 101 | grid_widgets_start.set_valign(Gtk::Align::START); 102 | grid_widgets_end.set_valign(Gtk::Align::START); 103 | grid_widgets_start.set_halign(Gtk::Align::START); 104 | grid_widgets_end.set_halign(Gtk::Align::START); 105 | box_widgets_start.set_valign(Gtk::Align::END); 106 | box_widgets_end.set_valign(Gtk::Align::END); 107 | } 108 | 109 | // Common 110 | bool vertical_layout = position % 2; 111 | if (vertical_layout) { // Vertical 112 | if (size / 2 > default_size_start) { 113 | grid_widgets_start.set_size_request(-1, default_size_start); 114 | scrolled_Window_start.set_halign(Gtk::Align::START); 115 | } 116 | else 117 | scrolled_Window_start.set_hexpand(true); 118 | 119 | if (size / 2 > default_size_end) { 120 | grid_widgets_end.set_size_request(-1, default_size_end); 121 | scrolled_Window_end.set_halign(Gtk::Align::END); 122 | } 123 | else 124 | scrolled_Window_end.set_hexpand(true); 125 | 126 | box_overlay.set_orientation(Gtk::Orientation::VERTICAL); 127 | } 128 | else { // Horizontal 129 | if (size / 2 > default_size_start) { 130 | grid_widgets_start.set_size_request(default_size_start, -1); 131 | scrolled_Window_start.set_halign(Gtk::Align::START); 132 | } 133 | else { 134 | scrolled_Window_start.set_hexpand(true); 135 | scrolled_Window_start.set_halign(Gtk::Align::FILL); 136 | } 137 | 138 | if (size / 2 > default_size_end) { 139 | grid_widgets_end.set_size_request(default_size_end, -1); 140 | scrolled_Window_end.set_halign(Gtk::Align::END); 141 | } 142 | else { 143 | scrolled_Window_end.set_hexpand(true); 144 | scrolled_Window_end.set_halign(Gtk::Align::FILL); 145 | } 146 | 147 | box_overlay.set_orientation(Gtk::Orientation::HORIZONTAL); 148 | } 149 | 150 | scrolled_Window_start.set_policy( 151 | vertical_layout ? Gtk::PolicyType::EXTERNAL : Gtk::PolicyType::NEVER, 152 | !vertical_layout ? Gtk::PolicyType::EXTERNAL : Gtk::PolicyType::NEVER); 153 | scrolled_Window_end.set_policy( 154 | vertical_layout ? Gtk::PolicyType::EXTERNAL : Gtk::PolicyType::NEVER, 155 | !vertical_layout ? Gtk::PolicyType::EXTERNAL : Gtk::PolicyType::NEVER); 156 | 157 | scrolled_Window_start.set_visible(false); 158 | scrolled_Window_end.set_visible(false); 159 | 160 | box_overlay.append(scrolled_Window_start); 161 | box_overlay.append(scrolled_Window_end); 162 | } 163 | 164 | // TODO: The whole gesture system needs a rework 165 | // This is a horrible mess.. 166 | void sysbar::setup_gestures() { 167 | gesture_drag = Gtk::GestureDrag::create(); 168 | gesture_drag->signal_drag_begin().connect([&](const double& x, const double& y) { 169 | gesture_touch = gesture_drag->get_current_event()->get_pointer_emulated(); 170 | on_drag_start(x, y); 171 | 172 | if (!gesture_touch) 173 | gesture_drag->reset(); 174 | }); 175 | gesture_drag->signal_drag_update().connect(sigc::mem_fun(*this, &sysbar::on_drag_update)); 176 | gesture_drag->signal_drag_end().connect(sigc::mem_fun(*this, &sysbar::on_drag_stop)); 177 | add_controller(gesture_drag); 178 | 179 | gesture_drag_start = Gtk::GestureDrag::create(); 180 | gesture_drag_start->signal_drag_begin().connect([&](const double& x, const double& y) { 181 | gesture_touch = gesture_drag_start->get_current_event()->get_pointer_emulated(); 182 | if (!gesture_touch) { 183 | gesture_drag_start->reset(); 184 | return; 185 | } 186 | 187 | on_drag_start(0, 0); 188 | on_drag_update(0, 0); 189 | }); 190 | gesture_drag_start->signal_drag_update().connect(sigc::mem_fun(*this, &sysbar::on_drag_update)); 191 | gesture_drag_start->signal_drag_end().connect(sigc::mem_fun(*this, &sysbar::on_drag_stop)); 192 | scrolled_Window_start.add_controller(gesture_drag_start); 193 | 194 | gesture_drag_end = Gtk::GestureDrag::create(); 195 | gesture_drag_end->signal_drag_begin().connect([&](const double& x, const double& y) { 196 | gesture_touch = gesture_drag_end->get_current_event()->get_pointer_emulated(); 197 | if (!gesture_touch) { 198 | gesture_drag_end->reset(); 199 | return; 200 | } 201 | 202 | on_drag_start(monitor_geometry.height, monitor_geometry.width); 203 | on_drag_update(0, 0); 204 | }); 205 | gesture_drag_end->signal_drag_update().connect(sigc::mem_fun(*this, &sysbar::on_drag_update)); 206 | gesture_drag_end->signal_drag_end().connect(sigc::mem_fun(*this, &sysbar::on_drag_stop)); 207 | scrolled_Window_end.add_controller(gesture_drag_end); 208 | } 209 | 210 | void sysbar::on_drag_start(const double& x, const double& y) { 211 | overlay_window.show(); 212 | 213 | scrolled_Window_start.set_valign(Gtk::Align::START); 214 | scrolled_Window_end.set_valign(Gtk::Align::START); 215 | 216 | if (position == 0) { 217 | scrolled_Window_start.set_valign(Gtk::Align::START); 218 | scrolled_Window_end.set_valign(Gtk::Align::START); 219 | } 220 | else if (position == 1) { 221 | scrolled_Window_start.set_halign(Gtk::Align::END); 222 | scrolled_Window_end.set_halign(Gtk::Align::END); 223 | } 224 | else if (position == 2) { 225 | scrolled_Window_start.set_valign(Gtk::Align::END); 226 | scrolled_Window_end.set_valign(Gtk::Align::END); 227 | } 228 | else if (position == 3) { 229 | scrolled_Window_start.set_halign(Gtk::Align::START); 230 | scrolled_Window_end.set_halign(Gtk::Align::START); 231 | } 232 | 233 | if (position % 2) { 234 | sliding_start_widget = y < monitor_geometry.height / 2; 235 | initial_size_start = scrolled_Window_start.get_width(); 236 | initial_size_end = scrolled_Window_end.get_width(); 237 | } 238 | else { 239 | sliding_start_widget = x < monitor_geometry.width / 2; 240 | initial_size_start = scrolled_Window_start.get_height(); 241 | initial_size_end = scrolled_Window_end.get_height(); 242 | } 243 | 244 | if (initial_size_start != 0 || initial_size_end != 0) { 245 | if (position % 2) { 246 | initial_size_start = grid_widgets_start.get_allocated_width(); 247 | initial_size_end = grid_widgets_end.get_allocated_width(); 248 | } 249 | else { 250 | initial_size_start = grid_widgets_start.get_allocated_height(); 251 | initial_size_end = grid_widgets_end.get_allocated_height(); 252 | } 253 | } 254 | 255 | scrolled_Window_start.set_visible(sliding_start_widget); 256 | scrolled_Window_end.set_visible(!sliding_start_widget); 257 | } 258 | 259 | void sysbar::on_drag_update(const double& x, const double& y) { 260 | double drag_width = -1; 261 | double drag_height = -1; 262 | double initial_size = sliding_start_widget ? initial_size_start : initial_size_end; 263 | Gtk::ScrolledWindow* scrolled_Window = sliding_start_widget ? &scrolled_Window_start : &scrolled_Window_end; 264 | 265 | if (position == 0) 266 | drag_height = y + initial_size; 267 | else if (position == 1) 268 | drag_width = -x + initial_size; // This is buggy (As usual) 269 | else if (position == 2) 270 | drag_height = -y + initial_size; // So is this 271 | else if (position == 3) 272 | drag_width = x + initial_size; 273 | 274 | // This clamp ensures the values are not bellow 0 275 | drag_width = std::max(0.0, drag_width); 276 | drag_height = std::max(0.0, drag_height); 277 | 278 | // And this ensures we don't go outside of the screen 279 | drag_width = std::min((double)monitor_geometry.width - bar_width, drag_width); 280 | drag_height = std::min((double)monitor_geometry.height - bar_height, drag_height); 281 | 282 | scrolled_Window->set_size_request(drag_width, drag_height); 283 | } 284 | 285 | void sysbar::on_drag_stop(const double& x, const double& y) { 286 | double size = 0; 287 | double initial_size = sliding_start_widget ? initial_size_start : initial_size_end; 288 | double size_threshold = sliding_start_widget ? grid_widgets_start.get_allocated_height() : grid_widgets_end.get_allocated_height();; 289 | Gtk::ScrolledWindow* scrolled_Window = sliding_start_widget ? &scrolled_Window_start : &scrolled_Window_end; 290 | 291 | if (position == 0) 292 | size = y + initial_size; 293 | else if (position == 1) 294 | size = -x + initial_size; 295 | else if (position == 2) 296 | size = -y + initial_size; 297 | else if (position == 3) 298 | size = x + initial_size; 299 | 300 | // Ensure size is not negative 301 | size = std::max(0.0, size); 302 | bool passed_threshold = size > (size_threshold * 0.75); 303 | 304 | if (!((passed_threshold && gesture_touch) || (size == 0 && !gesture_touch))) 305 | scrolled_Window->hide(); 306 | 307 | if (position % 2) 308 | scrolled_Window->set_halign(Gtk::Align::FILL); 309 | else 310 | scrolled_Window->set_valign(Gtk::Align::FILL); 311 | 312 | scrolled_Window->set_size_request(-1, -1); 313 | 314 | if (!scrolled_Window_start.get_visible() && !scrolled_Window_end.get_visible()) 315 | overlay_window.hide(); 316 | } 317 | -------------------------------------------------------------------------------- /src/window.cpp: -------------------------------------------------------------------------------- 1 | #include "window.hpp" 2 | 3 | #include "modules/backlight.hpp" 4 | #include "modules/battery.hpp" 5 | #include "modules/bluetooth.hpp" 6 | #include "modules/cellular.hpp" 7 | #include "modules/clock.hpp" 8 | #include "modules/controls.hpp" 9 | #include "modules/hyprland.hpp" 10 | #include "modules/menu.hpp" 11 | #include "modules/mpris.hpp" 12 | #include "modules/network.hpp" 13 | #include "modules/notifications.hpp" 14 | #include "modules/performance.hpp" 15 | #include "modules/taskbar.hpp" 16 | #include "modules/tray.hpp" 17 | #include "modules/volume.hpp" 18 | #include "modules/weather.hpp" 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | sysbar::sysbar(const std::map>& cfg) { 25 | config_main = cfg; 26 | 27 | // Only load commonly used non string configs 28 | position = std::stoi(config_main["main"]["position"]); 29 | size = std::stoi(config_main["main"]["size"]); 30 | layer = std::stoi(config_main["main"]["layer"]); 31 | verbose = config_main["main"]["verbose"] == "true"; 32 | 33 | // Get main monitor 34 | GdkDisplay* display = gdk_display_get_default(); 35 | GListModel* monitors = gdk_display_get_monitors(display); 36 | 37 | for (guint i = 0; i < g_list_model_get_n_items(monitors); ++i) { 38 | monitor = static_cast(g_list_model_get_item(monitors, i)); 39 | const gchar* monitor_name = gdk_monitor_get_connector(monitor); 40 | 41 | if (verbose) 42 | std::printf("Monitor: %s\n", monitor_name); 43 | 44 | if (config_main["main"]["main-monitor"] == monitor_name) 45 | break; 46 | else 47 | monitor = GDK_MONITOR(g_list_model_get_item(monitors, 0)); 48 | } 49 | 50 | gdk_monitor_get_geometry(monitor, &monitor_geometry); 51 | 52 | // Initialize layer shell 53 | gtk_layer_init_for_window(gobj()); 54 | gtk_layer_set_namespace(gobj(), "sysbar"); 55 | gtk_layer_set_layer(gobj(), static_cast(layer)); 56 | if (config_main["main"]["exclusive"] == "true") 57 | gtk_layer_set_exclusive_zone(gobj(), size); 58 | gtk_layer_set_monitor(gobj(), monitor); 59 | 60 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); 61 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); 62 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); 63 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); 64 | 65 | Gtk::RevealerTransitionType transition_type = Gtk::RevealerTransitionType::SLIDE_DOWN; 66 | 67 | switch (position) { 68 | case 0: 69 | transition_type = Gtk::RevealerTransitionType::SLIDE_DOWN; 70 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, false); 71 | bar_width = -1; 72 | bar_height = size; 73 | get_style_context()->add_class("position_bottom"); 74 | break; 75 | case 1: 76 | transition_type = Gtk::RevealerTransitionType::SLIDE_LEFT; 77 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_LEFT, false); 78 | bar_width = size; 79 | bar_height = -1; 80 | get_style_context()->add_class("position_left"); 81 | break; 82 | case 2: 83 | transition_type = Gtk::RevealerTransitionType::SLIDE_UP; 84 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_TOP, false); 85 | bar_width = -1; 86 | bar_height = size; 87 | get_style_context()->add_class("position_top"); 88 | break; 89 | case 3: 90 | transition_type = Gtk::RevealerTransitionType::SLIDE_RIGHT; 91 | gtk_layer_set_anchor(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, false); 92 | bar_width = size; 93 | bar_height = -1; 94 | get_style_context()->add_class("position_right"); 95 | break; 96 | } 97 | 98 | // Initialize 99 | set_name("sysbar"); 100 | set_hide_on_close(true); 101 | set_default_size(bar_width, bar_height); 102 | set_child(revealer_box); 103 | revealer_box.set_child(centerbox_main); 104 | revealer_box.set_transition_type(transition_type); 105 | revealer_box.set_transition_duration(1000); 106 | centerbox_main.get_style_context()->add_class("centerbox_main"); 107 | centerbox_main.set_start_widget(box_start); 108 | centerbox_main.set_center_widget(box_center); 109 | centerbox_main.set_end_widget(box_end); 110 | show(); 111 | revealer_box.set_reveal_child(true); 112 | 113 | // Set orientation 114 | if (position % 2) { 115 | Gtk::Orientation orientation = Gtk::Orientation::VERTICAL; 116 | centerbox_main.set_orientation(orientation); 117 | box_start.set_orientation(orientation); 118 | box_center.set_orientation(orientation); 119 | box_end.set_orientation(orientation); 120 | } 121 | 122 | const std::string& style_path = "/usr/share/sys64/bar/style.css"; 123 | const std::string& style_path_usr = std::string(getenv("HOME")) + "/.config/sys64/bar/style.css"; 124 | 125 | // Load base style 126 | if (std::filesystem::exists(style_path)) { 127 | auto css = Gtk::CssProvider::create(); 128 | css->load_from_path(style_path); 129 | get_style_context()->add_provider_for_display(property_display(), css, GTK_STYLE_PROVIDER_PRIORITY_USER); 130 | } 131 | // Load user style 132 | if (std::filesystem::exists(style_path_usr)) { 133 | auto css = Gtk::CssProvider::create(); 134 | css->load_from_path(style_path_usr); 135 | get_style_context()->add_provider_for_display(property_display(), css, GTK_STYLE_PROVIDER_PRIORITY_USER); 136 | } 137 | 138 | 139 | // Overlay 140 | setup_overlay(); 141 | setup_overlay_widgets(); 142 | setup_gestures(); 143 | 144 | // Load modules 145 | load_modules(config_main["main"]["modules-start"], box_start); 146 | load_modules(config_main["main"]["modules-center"], box_center); 147 | load_modules(config_main["main"]["modules-end"], box_end); 148 | } 149 | 150 | void sysbar::load_modules(const std::string& modules, Gtk::Box& box) { 151 | std::istringstream iss(modules); 152 | std::string module_name; 153 | 154 | // TODO: add a config to force either left or right alignment 155 | // Right now this is auto 156 | bool icon_on_start = (&box == &box_start); 157 | 158 | while (std::getline(iss, module_name, ',')) { 159 | module* new_module; 160 | 161 | if (verbose) 162 | std::printf("Loading module: %s\n", module_name.c_str()); 163 | 164 | if (false) 165 | std::printf("You're not supposed to see this\n"); 166 | 167 | #ifdef MODULE_CLOCK 168 | else if (module_name == "clock") 169 | new_module = Gtk::make_managed(this, icon_on_start); 170 | #endif 171 | 172 | #ifdef MODULE_WEATHER 173 | else if (module_name == "weather") 174 | new_module = Gtk::make_managed(this, icon_on_start); 175 | #endif 176 | 177 | #ifdef MODULE_TRAY 178 | else if (module_name == "tray") 179 | new_module = Gtk::make_managed(this, icon_on_start); 180 | #endif 181 | 182 | #ifdef MODULE_HYPRLAND 183 | else if (module_name == "hyprland") 184 | new_module = Gtk::make_managed(this, icon_on_start); 185 | #endif 186 | 187 | #ifdef MODULE_VOLUME 188 | else if (module_name == "volume") 189 | new_module = Gtk::make_managed(this, icon_on_start); 190 | #endif 191 | 192 | #ifdef MODULE_NETWORK 193 | else if (module_name == "network") 194 | new_module = Gtk::make_managed(this, icon_on_start); 195 | #endif 196 | 197 | #ifdef MODULE_BATTERY 198 | else if (module_name == "battery") 199 | new_module = Gtk::make_managed(this, icon_on_start); 200 | #endif 201 | 202 | #ifdef MODULE_NOTIFICATION 203 | else if (module_name == "notification") 204 | new_module = Gtk::make_managed(this, icon_on_start); 205 | #endif 206 | 207 | #ifdef MODULE_PERFORMANCE 208 | else if (module_name == "performance") 209 | new_module = Gtk::make_managed(this, icon_on_start); 210 | #endif 211 | 212 | #ifdef MODULE_TASKBAR 213 | else if (module_name == "taskbar") 214 | new_module = Gtk::make_managed(this, icon_on_start); 215 | #endif 216 | 217 | #ifdef MODULE_BACKLIGHT 218 | else if (module_name == "backlight") 219 | new_module = Gtk::make_managed(this, icon_on_start); 220 | #endif 221 | 222 | #ifdef MODULE_MPRIS 223 | else if (module_name == "mpris") 224 | new_module = Gtk::make_managed(this, icon_on_start); 225 | #endif 226 | 227 | #ifdef MODULE_BLUETOOTH 228 | else if (module_name == "bluetooth") 229 | new_module = Gtk::make_managed(this, icon_on_start); 230 | #endif 231 | 232 | #ifdef MODULE_CELLULAR 233 | else if (module_name == "cellular") 234 | new_module = Gtk::make_managed(this, icon_on_start); 235 | #endif 236 | 237 | #ifdef MODULE_CONTROLS 238 | else if (module_name == "controls" && &box != &box_center) { 239 | box_controls = Gtk::make_managed(this, icon_on_start); 240 | continue; 241 | } 242 | #endif 243 | 244 | #ifdef MODULE_MENU 245 | else if (module_name == "menu") 246 | new_module = Gtk::make_managed(this, icon_on_start); 247 | #endif 248 | 249 | else { 250 | std::fprintf(stderr, "Unknown module: %s\n", module_name.c_str()); 251 | continue; 252 | } 253 | 254 | box.append(*new_module); 255 | } 256 | } 257 | 258 | void sysbar::handle_signal(const int& signum) { 259 | Glib::signal_idle().connect([this, signum]() { 260 | switch (signum) { 261 | case 10: // Show 262 | revealer_box.set_reveal_child(true); 263 | gtk_layer_set_exclusive_zone(gobj(), size); 264 | set_default_size(bar_width, bar_height); 265 | break; 266 | 267 | case 12: // Hide 268 | revealer_box.set_reveal_child(false); 269 | gtk_layer_set_exclusive_zone(gobj(), 0); 270 | set_default_size(1, 1); 271 | break; 272 | 273 | case 34: // Toggle 274 | if (revealer_box.get_reveal_child()) 275 | handle_signal(12); 276 | else 277 | handle_signal(10); 278 | break; 279 | } 280 | return false; 281 | }); 282 | } 283 | 284 | extern "C" { 285 | sysbar* sysbar_create(const std::map>& cfg) { 286 | return new sysbar(cfg); 287 | } 288 | void sysbar_signal(sysbar* window, int signal) { 289 | window->handle_signal(signal); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/window.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class sysbar : public Gtk::Window { 14 | public: 15 | sysbar(const std::map>&); 16 | void handle_signal(const int&); 17 | 18 | std::map> config_main; 19 | Gtk::Window overlay_window; 20 | Glib::RefPtr gesture_drag; 21 | Gtk::Box* box_controls = nullptr; 22 | Gtk::Box box_widgets_start; 23 | Gtk::Stack stack_start; 24 | Gtk::Stack stack_end; 25 | Gtk::Grid grid_widgets_start; 26 | Gtk::Box box_widgets_end; 27 | Gtk::Grid grid_widgets_end; 28 | 29 | // Main config 30 | int position; 31 | int size; 32 | int layer; 33 | bool verbose; 34 | 35 | private: 36 | Gtk::Revealer revealer_box; 37 | Gtk::CenterBox centerbox_main; 38 | Gtk::Box box_overlay; 39 | Gtk::Box box_start; 40 | Gtk::Box box_center; 41 | Gtk::Box box_end; 42 | 43 | Gtk::ScrolledWindow scrolled_Window_start; 44 | Gtk::ScrolledWindow scrolled_Window_end; 45 | Glib::RefPtr gesture_drag_start; 46 | Glib::RefPtr gesture_drag_end; 47 | 48 | GdkMonitor* monitor; 49 | GdkRectangle monitor_geometry; 50 | double initial_size_start, initial_size_end; 51 | int bar_width, bar_height; 52 | bool sliding_start_widget; 53 | bool gesture_touch; 54 | 55 | void load_modules(const std::string&, Gtk::Box&); 56 | void setup_overlay(); 57 | void setup_overlay_widgets(); 58 | void setup_gestures(); 59 | 60 | void on_drag_start(const double&, const double&); 61 | void on_drag_update(const double&, const double&); 62 | void on_drag_stop(const double&, const double&); 63 | }; 64 | 65 | extern "C" { 66 | sysbar* sysbar_create(const std::map>&); 67 | void sysbar_signal(sysbar*, int); 68 | } 69 | -------------------------------------------------------------------------------- /src/wireless_network.cpp: -------------------------------------------------------------------------------- 1 | #include "wireless_network.hpp" 2 | #ifdef FEATURE_WIRELESS 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | wireless_manager::wireless_manager() { 10 | state.socket = nl_socket_alloc(); 11 | if (!state.socket) { 12 | std::fprintf(stderr, "Failed to allocate netlink socket\n"); 13 | return; 14 | } 15 | 16 | if (genl_connect(state.socket)) { 17 | std::fprintf(stderr, "Failed to connect to generic netlink\n"); 18 | nl_socket_free(state.socket); 19 | return; 20 | } 21 | 22 | state.nl80211_id = genl_ctrl_resolve(state.socket, "nl80211"); 23 | if (state.nl80211_id < 0) { 24 | std::fprintf(stderr, "nl80211 not found\n"); 25 | nl_socket_free(state.socket); 26 | return; 27 | } 28 | } 29 | 30 | wireless_manager::~wireless_manager() { 31 | if (state.socket) { 32 | nl_socket_free(state.socket); 33 | } 34 | } 35 | 36 | int wireless_manager::convert_signal_strength(const int& signal_strength_dbm) { 37 | const int min_dbm = -90; 38 | const int max_dbm = -45; 39 | 40 | if (signal_strength_dbm <= min_dbm) 41 | return 0; 42 | else if (signal_strength_dbm >= max_dbm) 43 | return 100; 44 | else 45 | return std::round((signal_strength_dbm - min_dbm) * 100.0 / (max_dbm - min_dbm)); 46 | } 47 | 48 | int wireless_manager::nl_socket_modify_cb(struct nl_msg* msg, void* arg) { 49 | wireless_manager* manager = static_cast(arg); 50 | struct nlattr* tb[NL80211_ATTR_MAX + 1]; 51 | struct nlattr* bss[NL80211_BSS_MAX + 1]; 52 | auto* gnlh = static_cast(nlmsg_data(nlmsg_hdr(msg))); 53 | 54 | nla_parse(tb, NL80211_ATTR_MAX, (struct nlattr*)genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), nullptr); 55 | 56 | if (!tb[NL80211_ATTR_BSS]) { 57 | std::fprintf(stderr, "No BSS information found\n"); 58 | return NL_SKIP; 59 | } 60 | 61 | nla_parse_nested(bss, NL80211_BSS_MAX, tb[NL80211_ATTR_BSS], nullptr); 62 | 63 | if (bss[NL80211_BSS_BSSID]) { 64 | auto ies = static_cast(nla_data(bss[NL80211_BSS_INFORMATION_ELEMENTS])); 65 | auto ies_len = nla_len(bss[NL80211_BSS_INFORMATION_ELEMENTS]); 66 | int hdr_len = 2; 67 | 68 | while (ies_len > hdr_len) { 69 | uint8_t element_id = static_cast(ies[0]); 70 | uint8_t element_len = static_cast(ies[1]); 71 | 72 | if (ies_len < element_len + hdr_len) 73 | break; 74 | 75 | if (element_id == 0x00) { 76 | if (element_len > 0) 77 | manager->info.bssid.assign(ies + hdr_len, ies + hdr_len + element_len); 78 | else 79 | manager->info.bssid = ""; 80 | break; 81 | } 82 | 83 | ies_len -= element_len + hdr_len; 84 | ies += element_len + hdr_len; 85 | } 86 | } 87 | 88 | if (bss[NL80211_BSS_SIGNAL_MBM]) { 89 | manager->info.signal_dbm = (int)nla_get_u32(bss[NL80211_BSS_SIGNAL_MBM]) / 100; 90 | manager->info.signal_percentage = manager->convert_signal_strength(manager->info.signal_dbm); 91 | } 92 | 93 | if (bss[NL80211_BSS_FREQUENCY]) { 94 | double frequency = nla_get_u32(bss[NL80211_BSS_FREQUENCY]); 95 | manager->info.frequency = frequency / 1000.0; 96 | } 97 | 98 | return NL_SKIP; 99 | } 100 | 101 | wireless_manager::wireless_info* wireless_manager::get_wireless_info(const std::string& interface_name) { 102 | auto* msg = nlmsg_alloc(); 103 | genlmsg_put(msg, 0, 0, state.nl80211_id, 0, NLM_F_DUMP, NL80211_CMD_GET_SCAN, 0); 104 | 105 | int if_index = if_nametoindex(interface_name.c_str()); 106 | if (if_index == 0) { 107 | std::fprintf(stderr, "Interface %s not found\n", interface_name.c_str()); 108 | nlmsg_free(msg); 109 | return nullptr; 110 | } 111 | 112 | nla_put_u32(msg, NL80211_ATTR_IFINDEX, if_index); 113 | 114 | nl_cb_set(nl_socket_get_cb(state.socket), NL_CB_VALID, NL_CB_CUSTOM, wireless_manager::nl_socket_modify_cb, this); 115 | 116 | if (nl_send_auto(state.socket, msg) < 0) { 117 | std::fprintf(stderr, "Failed to send message to nl80211\n"); 118 | nlmsg_free(msg); 119 | return nullptr; 120 | } 121 | 122 | nl_recvmsgs_default(state.socket); 123 | nlmsg_free(msg); 124 | 125 | return &info; 126 | } 127 | #endif 128 | -------------------------------------------------------------------------------- /src/wireless_network.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "config.hpp" 3 | #ifdef MODULE_NETWORK 4 | 5 | #include 6 | 7 | class wireless_manager { 8 | public: 9 | wireless_manager(); 10 | ~wireless_manager(); 11 | 12 | struct wireless_info { 13 | std::string bssid; 14 | int signal_dbm; 15 | int signal_percentage; 16 | double frequency; 17 | }; 18 | 19 | wireless_info* get_wireless_info(const std::string&); 20 | 21 | private: 22 | struct nl80211_state { 23 | struct nl_sock *socket; 24 | int nl80211_id; 25 | }; 26 | 27 | nl80211_state state; 28 | wireless_info info; 29 | 30 | int convert_signal_strength(const int&); 31 | static int nl_socket_modify_cb(struct nl_msg*, void*); 32 | }; 33 | #endif 34 | -------------------------------------------------------------------------------- /src/wireplumber.cpp: -------------------------------------------------------------------------------- 1 | #include "wireplumber.hpp" 2 | 3 | bool syshud_wireplumber::is_valid_node_id(uint32_t id) { 4 | return id > 0 && id < G_MAXUINT32; 5 | } 6 | 7 | void syshud_wireplumber::on_mixer_changed(syshud_wireplumber* self, uint32_t id) { 8 | GVariant* variant = nullptr; 9 | 10 | if (!is_valid_node_id(id)) 11 | return; 12 | 13 | g_autoptr(WpNode) node = static_cast(wp_object_manager_lookup( 14 | self->om, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", 15 | "=u", id, nullptr)); 16 | 17 | if (node == nullptr) 18 | return; 19 | 20 | double temp_volume; 21 | g_signal_emit_by_name(self->mixer_api, "get-volume", id, &variant); 22 | g_variant_lookup(variant, "volume", "d", &temp_volume); 23 | g_variant_lookup(variant, "mute", "b", &self->muted); 24 | g_clear_pointer(&variant, g_variant_unref); 25 | 26 | // Figure out if the change came from an input or output device 27 | const std::string media_class = std::string( 28 | wp_pipewire_object_get_property( 29 | WP_PIPEWIRE_OBJECT(node), "media.class")); 30 | 31 | // Set values and trigger a callback 32 | self->volume = (temp_volume + 0.0001) * 100.0; 33 | if (media_class == "Audio/Source") { 34 | if (self->input_callback != nullptr) 35 | self->input_callback->emit(); 36 | } 37 | else { 38 | if (self->output_callback != nullptr) 39 | self->output_callback->emit(); 40 | } 41 | } 42 | 43 | void syshud_wireplumber::on_default_nodes_api_changed(syshud_wireplumber* self) { 44 | g_signal_emit_by_name(self->def_nodes_api, "get-default-node", "Audio/Sink", &self->output_id); 45 | g_signal_emit_by_name(self->def_nodes_api, "get-default-node", "Audio/Source", &self->input_id); 46 | 47 | if (!is_valid_node_id(self->output_id) || !is_valid_node_id(self->input_id)) 48 | return; 49 | 50 | g_autoptr(WpNode) output_node = static_cast( 51 | wp_object_manager_lookup(self->om, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, 52 | "bound-id", "=u", self->output_id, nullptr)); 53 | 54 | g_autoptr(WpNode) input_node = static_cast( 55 | wp_object_manager_lookup(self->om, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, 56 | "bound-id", "=u", self->input_id, nullptr)); 57 | 58 | self->output_name = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(output_node), "node.name"); 59 | self->input_name = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(input_node), "node.name"); 60 | } 61 | 62 | void syshud_wireplumber::on_plugin_activated(WpObject* p, GAsyncResult* res, syshud_wireplumber* self) { 63 | if (wp_object_activate_finish(p, res, nullptr) == 0) 64 | return; 65 | 66 | if (--self->pending_plugins == 0) { 67 | wp_core_install_object_manager(self->core, self->om); 68 | } 69 | } 70 | 71 | void syshud_wireplumber::activatePlugins() { 72 | for (uint16_t i = 0; i < apis->len; i++) { 73 | WpPlugin* plugin = static_cast(g_ptr_array_index(apis, i)); 74 | pending_plugins++; 75 | wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, nullptr, 76 | (GAsyncReadyCallback)on_plugin_activated, this); 77 | } 78 | } 79 | 80 | void syshud_wireplumber::on_mixer_api_loaded(WpObject* p, GAsyncResult* res, syshud_wireplumber* self) { 81 | if (!wp_core_load_component_finish(self->core, res, nullptr)) 82 | return; 83 | 84 | g_ptr_array_add(self->apis, ({ 85 | WpPlugin* p = wp_plugin_find(self->core, "mixer-api"); 86 | g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, nullptr); 87 | p; 88 | })); 89 | 90 | self->activatePlugins(); 91 | } 92 | 93 | void syshud_wireplumber::on_default_nodes_api_loaded(WpObject* p, GAsyncResult* res, syshud_wireplumber* self) { 94 | if (!wp_core_load_component_finish(self->core, res, nullptr)) 95 | return; 96 | 97 | g_ptr_array_add(self->apis, wp_plugin_find(self->core, "default-nodes-api")); 98 | 99 | wp_core_load_component(self->core, "libwireplumber-module-mixer-api", "module", nullptr, 100 | "mixer-api", nullptr, (GAsyncReadyCallback)on_mixer_api_loaded, self); 101 | } 102 | 103 | void syshud_wireplumber::on_object_manager_installed(syshud_wireplumber* self) { 104 | self->def_nodes_api = wp_plugin_find(self->core, "default-nodes-api"); 105 | if (self->def_nodes_api == nullptr) 106 | return; 107 | 108 | self->mixer_api = wp_plugin_find(self->core, "mixer-api"); 109 | if (self->mixer_api == nullptr) 110 | return; 111 | 112 | g_signal_emit_by_name(self->def_nodes_api, "get-default-node", "Audio/Sink", &self->output_id); 113 | g_signal_emit_by_name(self->def_nodes_api, "get-default-node", "Audio/Source", &self->input_id); 114 | 115 | on_mixer_changed(self, self->output_id); 116 | 117 | g_signal_connect_swapped(self->mixer_api, "changed", (GCallback)on_mixer_changed, self); 118 | g_signal_connect_swapped(self->def_nodes_api, "changed", (GCallback)on_default_nodes_api_changed, 119 | self); 120 | 121 | on_default_nodes_api_changed(self); 122 | } 123 | 124 | void syshud_wireplumber::set_volume(bool type, double value) { 125 | gboolean res = FALSE; 126 | const uint32_t node_id = (type) ? output_id : input_id; 127 | g_signal_emit_by_name(mixer_api, "set-volume", node_id, g_variant_new_double(value / 100), &res); 128 | } 129 | 130 | syshud_wireplumber::syshud_wireplumber(Glib::Dispatcher* input_callback, Glib::Dispatcher* output_callback) { 131 | this->input_callback = input_callback; 132 | this->output_callback = output_callback; 133 | 134 | wp_init(WP_INIT_PIPEWIRE); 135 | core = wp_core_new(nullptr, nullptr, nullptr); 136 | apis = g_ptr_array_new_with_free_func(g_object_unref); 137 | om = wp_object_manager_new(); 138 | 139 | // This has to be done from the main thread 140 | g_main_context_invoke(NULL, [](gpointer user_data) -> gboolean { 141 | if (wp_core_connect(static_cast(user_data)) == 0) 142 | std::fprintf(stderr, "Could not connect to wireplumber\n"); 143 | return G_SOURCE_REMOVE; 144 | }, core); 145 | 146 | g_signal_connect_swapped( 147 | om, 148 | "installed", 149 | (GCallback)on_object_manager_installed, 150 | this); 151 | 152 | wp_object_manager_add_interest( 153 | om, 154 | WP_TYPE_NODE,WP_CONSTRAINT_TYPE_PW_PROPERTY, 155 | "media.class", 156 | "=s", 157 | "Audio/Sink", 158 | nullptr); 159 | 160 | wp_object_manager_add_interest( 161 | om, 162 | WP_TYPE_NODE,WP_CONSTRAINT_TYPE_PW_PROPERTY, 163 | "media.class", 164 | "=s", 165 | "Audio/Source", 166 | nullptr); 167 | 168 | wp_core_load_component( 169 | core, 170 | "libwireplumber-module-default-nodes-api", 171 | "module", 172 | nullptr, 173 | "default-nodes-api", 174 | nullptr, 175 | (GAsyncReadyCallback)on_default_nodes_api_loaded, 176 | this); 177 | } 178 | 179 | syshud_wireplumber::~syshud_wireplumber() { 180 | wp_core_disconnect(core); 181 | g_clear_pointer(&apis, g_ptr_array_unref); 182 | g_clear_object(&om); 183 | g_clear_object(&core); 184 | g_clear_object(&mixer_api); 185 | g_clear_object(&def_nodes_api); 186 | } 187 | -------------------------------------------------------------------------------- /src/wireplumber.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | class syshud_wireplumber { 6 | public: 7 | syshud_wireplumber(Glib::Dispatcher*, Glib::Dispatcher*); 8 | virtual ~syshud_wireplumber(); 9 | 10 | int volume; 11 | bool muted; 12 | const char* output_name; 13 | const char* input_name; 14 | 15 | void set_volume(bool type, double value); 16 | 17 | private: 18 | Glib::Dispatcher* input_callback; 19 | Glib::Dispatcher* output_callback; 20 | 21 | GPtrArray* apis; 22 | WpCore* core; 23 | WpObjectManager* om; 24 | int pending_plugins; 25 | 26 | uint32_t output_id = 0; 27 | uint32_t input_id = 0; 28 | const gchar* node_name; 29 | WpPlugin *mixer_api; 30 | WpPlugin *def_nodes_api; 31 | 32 | void activatePlugins(); 33 | static bool is_valid_node_id(uint32_t); 34 | static void on_mixer_changed(syshud_wireplumber*, uint32_t); 35 | static void on_default_nodes_api_changed(syshud_wireplumber* self); 36 | static void on_plugin_activated(WpObject* p, GAsyncResult* res, syshud_wireplumber* self); 37 | static void on_mixer_api_loaded(WpObject* p, GAsyncResult* res, syshud_wireplumber* self); 38 | static void on_default_nodes_api_loaded(WpObject* p, GAsyncResult* res, syshud_wireplumber* self); 39 | static void on_object_manager_installed(syshud_wireplumber* self); 40 | }; 41 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url('events.css'); 2 | 3 | #sysbar { 4 | background: @theme_bg_color; 5 | border-bottom: 1px solid @borders; 6 | } 7 | 8 | #sysbar .centerbox_main { 9 | } 10 | 11 | #sysbar_overlay { 12 | background: transparent; 13 | } 14 | 15 | #sysbar_overlay .grid_widgets_start, 16 | #sysbar_overlay .grid_widgets_end { 17 | background: @theme_base_color; 18 | margin: 10px; 19 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5); 20 | border: 1px solid @borders; 21 | border-radius: 10px; 22 | } 23 | 24 | #sysbar_overlay.position_top .sidepanel_end >:first-child:not(:only-child), 25 | #sysbar_overlay.position_right .sidepanel_end >:first-child:not(:only-child) { 26 | border-radius: 10px 10px 0px 0px; 27 | border-bottom: 0px; 28 | margin-bottom: 0px; 29 | } 30 | 31 | #sysbar_overlay.position_bottom .sidepanel_end >:last-child:not(:only-child), 32 | #sysbar_overlay.position_left .sidepanel_end >:last-child:not(:only-child) { 33 | border-radius: 0px 0px 10px 10px; 34 | border-top: 0px; 35 | margin-top: 0px; 36 | } 37 | 38 | #sysbar .module { 39 | border-radius: 4px; 40 | } 41 | 42 | #sysbar .module:hover { 43 | background: alpha(CurrentColor, 0.1); 44 | } 45 | 46 | #sysbar .tray_container { 47 | } 48 | 49 | #sysbar .tray_item { 50 | padding: 10px; 51 | } 52 | #sysbar .tray_item:hover { 53 | background: alpha(currentcolor, 0.1); 54 | border-radius: 4px; 55 | } 56 | 57 | #sysbar .context_menu contents { 58 | background: @insensitive_bg_color; 59 | min-width: 100px; 60 | padding: 5px; 61 | } 62 | #sysbar .context_menu flowboxchild { 63 | padding: 10px; 64 | } 65 | #sysbar .context_menu flowboxchild:hover { 66 | background: alpha(currentcolor, 0.1); 67 | } 68 | 69 | #sysbar .module_taskbar flowboxchild { 70 | color: alpha(@theme_fg_color, 0.75); 71 | margin: 0px; 72 | padding: 0px; 73 | } 74 | #sysbar .module_taskbar flowboxchild:selected { 75 | background: alpha(@theme_fg_color, 0.1); 76 | color: unset; 77 | } 78 | #sysbar .module_taskbar flowboxchild:hover { 79 | background: alpha(@theme_fg_color, 0.05); 80 | } 81 | 82 | #sysbar_overlay .notifications_header { 83 | background: alpha(@theme_base_color, 0.9); 84 | margin: 10px; 85 | padding: 5px; 86 | border: 1px solid @borders; 87 | } 88 | 89 | #sysbar_overlay.position_top .notifications_header, 90 | #sysbar_overlay.position_right .notifications_header { 91 | margin-top: 0px; 92 | padding-top: 0px; 93 | border-top: unset; 94 | border-radius: 0px 0px 10px 10px; 95 | box-shadow: 0px -2px 5px 0px rgba(0, 0, 0, 0.5); 96 | } 97 | 98 | #sysbar_overlay.position_bottom .notifications_header, 99 | #sysbar_overlay.position_left .notifications_header { 100 | margin-bottom: 0px; 101 | padding-bottom: 0px; 102 | border-bottom: unset; 103 | border-radius: 10px 10px 0px 0px; 104 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.5); 105 | } 106 | 107 | #sysbar_overlay .notifications_header > label { 108 | margin-left: 10px; 109 | } 110 | 111 | #sysbar_overlay .notification { 112 | background: alpha(currentcolor, 0.1); 113 | border-radius: 4px; 114 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.25); 115 | margin: 5px; 116 | padding: 5px; 117 | min-height: 70px; 118 | } 119 | #sysbar_overlay .notification .summary { 120 | font-size: 14px; 121 | font-weight: 500; 122 | } 123 | #sysbar_overlay .notification .body { 124 | font-size: 13px; 125 | } 126 | #sysbar .popover_notifications > * { 127 | background: transparent; 128 | border: unset; 129 | box-shadow: unset; 130 | margin: unset; 131 | padding: unset; 132 | } 133 | #sysbar .popover_notifications .notification { 134 | background: alpha(@insensitive_bg_color, 0.9); 135 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); 136 | margin: 10px 10px 0px 10px; 137 | } 138 | 139 | #sysbar_overlay calendar { 140 | border-radius: 10px; 141 | border: unset; 142 | } 143 | #sysbar_overlay calendar > grid { 144 | margin: 0px; 145 | } 146 | #sysbar_overlay calendar > header { 147 | border-radius: 10px 10px 0px 0px; 148 | } 149 | #sysbar_overlay calendar > header { 150 | background: alpha(@insensitive_bg_color, 0.5); 151 | } 152 | #sysbar_overlay .day-number:checked { 153 | box-shadow: 0px -2px inset rgb(255, 0, 100); 154 | border-radius: 4px 4px 0px 0px; 155 | } 156 | 157 | #sysbar_overlay .widget_controls { 158 | } 159 | 160 | #sysbar_overlay .widget_mpris { 161 | background: alpha(currentcolor, 0.05); 162 | border-radius: 4px; 163 | margin: 10px; 164 | } 165 | 166 | #sysbar_overlay .widget_mpris .image_album_art { 167 | background: alpha(currentcolor, 0.05); 168 | margin: 10px; 169 | border-radius: 10px; 170 | } 171 | 172 | #sysbar_overlay .widget_mpris .label_title { 173 | margin-top: 10px; 174 | } 175 | 176 | #sysbar_overlay .widget_mpris .box_controls { 177 | margin-bottom: 10px; 178 | } 179 | #sysbar_overlay .widget_controls .control { 180 | border: 1px solid @borders; 181 | border-radius: 10px; 182 | } 183 | --------------------------------------------------------------------------------