├── .gitmodules ├── assets ├── bt.png ├── bar.png └── audioflyin.png ├── .gitignore ├── src ├── Bar.h ├── BluetoothDevices.h ├── SNI.h ├── CSS.h ├── Plugin.h ├── AudioFlyin.h ├── Log.cpp ├── Log.h ├── Plugin.cpp ├── Workspaces.h ├── Window.h ├── Wayland.h ├── AMDGPU.h ├── System.h ├── CSS.cpp ├── NvidiaGPU.h ├── AudioFlyin.cpp ├── gBar.cpp ├── Config.h ├── Common.h ├── Window.cpp ├── PulseAudio.h ├── Widget.h ├── Workspaces.cpp └── BluetoothDevices.cpp ├── example ├── main.cpp └── meson.build ├── meson_options.txt ├── data ├── update.sh └── config ├── LICENSE ├── flake.nix ├── css ├── dracula │ └── LICENSE ├── style.css └── style.scss ├── protocols ├── sni-watcher.xml ├── sni-item.xml ├── wlr-foreign-toplevel-management-unstable-v1.xml └── ext-workspace-unstable-v1.xml ├── .clang-format ├── .github ├── workflows │ └── build.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── flake.lock ├── meson.build ├── README.md └── module.nix /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/bt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scorpion-26/gBar/HEAD/assets/bt.png -------------------------------------------------------------------------------- /assets/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scorpion-26/gBar/HEAD/assets/bar.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .cache 3 | build 4 | compile_commands.json 5 | result 6 | .idea 7 | -------------------------------------------------------------------------------- /assets/audioflyin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scorpion-26/gBar/HEAD/assets/audioflyin.png -------------------------------------------------------------------------------- /src/Bar.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Widget.h" 3 | #include "Window.h" 4 | 5 | namespace Bar 6 | { 7 | void Create(Window& window, const std::string& monitor); 8 | } 9 | -------------------------------------------------------------------------------- /src/BluetoothDevices.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Widget.h" 3 | #include "Window.h" 4 | 5 | namespace BluetoothDevices 6 | { 7 | void Create(Window& window, const std::string& monitor); 8 | } 9 | -------------------------------------------------------------------------------- /src/SNI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef WITH_SNI 3 | class Widget; 4 | namespace SNI 5 | { 6 | void Init(); 7 | void WidgetSNI(Widget& parent); 8 | void Shutdown(); 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /src/CSS.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace CSS 6 | { 7 | void Load(const std::string& overrideConfigLocation); 8 | GtkCssProvider* GetProvider(); 9 | } 10 | -------------------------------------------------------------------------------- /src/Plugin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "Window.h" 4 | 5 | namespace Plugin 6 | { 7 | void LoadWidgetFromPlugin(const std::string& pluginName, Window& window, const std::string& monitor); 8 | } 9 | -------------------------------------------------------------------------------- /src/AudioFlyin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Widget.h" 3 | #include "Window.h" 4 | 5 | namespace AudioFlyin 6 | { 7 | enum class Type 8 | { 9 | Speaker, 10 | Microphone 11 | }; 12 | void Create(Window& window, const std::string& monitor, Type type); 13 | } 14 | -------------------------------------------------------------------------------- /example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void Create(Window& window, const std::string& monitor) 5 | { 6 | auto mainWidget = Widget::Create(); 7 | mainWidget->SetText("Hello, World!"); 8 | 9 | window.SetMainWidget(std::move(mainWidget)); 10 | } 11 | 12 | DEFINE_PLUGIN(Create); 13 | -------------------------------------------------------------------------------- /example/meson.build: -------------------------------------------------------------------------------- 1 | project('gBarHelloWorld', 2 | ['cpp'], 3 | version: '0.0.1', 4 | license: 'MIT', 5 | meson_version: '>=0.45.1', 6 | default_options: ['c_std=c++17', 'warning_level=3']) 7 | 8 | gBar = dependency('gBar') 9 | 10 | add_global_arguments('-DUSE_LOGFILE', language: 'cpp') 11 | 12 | library( 13 | 'gBarHelloWorld', 14 | ['main.cpp'], 15 | dependencies: [gBar], 16 | install: true, 17 | install_dir: 'lib/gBar/') 18 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | # Hyprland IPC 2 | option('WithHyprland', type: 'boolean', value : true) 3 | 4 | # Workspaces general, enables Wayland protocol 5 | option('WithWorkspaces', type: 'boolean', value : true) 6 | 7 | # Support for loading SCSS directly 8 | option('WithLibSass', type: 'boolean', value : true) 9 | 10 | # Tray icons 11 | option('WithSNI', type: 'boolean', value : true) 12 | 13 | option('WithNvidia', type: 'boolean', value : true) 14 | option('WithAMD', type: 'boolean', value : true) 15 | option('WithBlueZ', type: 'boolean', value : true) 16 | -------------------------------------------------------------------------------- /src/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include "Common.h" 3 | 4 | #include 5 | 6 | namespace Logging 7 | { 8 | static std::ofstream logFile; 9 | 10 | void Init() 11 | { 12 | pid_t pid = getpid(); 13 | logFile = std::ofstream("/tmp/gBar-" + std::to_string(pid) + ".log"); 14 | if (!logFile.is_open()) 15 | { 16 | LOG("Cannot open logfile(/tmp/gBar-" << pid << ".log)"); 17 | } 18 | } 19 | 20 | void Log(const std::string& str) 21 | { 22 | if (logFile.is_open()) 23 | logFile << str << std::endl; 24 | } 25 | 26 | void Shutdown() 27 | { 28 | logFile.close(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /data/update.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # This script is not used by default. It is a human-readable version of the default Arch-applicable 4 | # package command with explanations 5 | 6 | updates="$(checkupdates)"; 7 | exitCode=$?; 8 | 9 | if [ $exitCode -eq 127 ] ; then 10 | # checkupdates wasn't found. 11 | # Forward the error to gBar, so gBar can shut down the widget 12 | # This is done, so we don't bother non-Arch systems with update checking 13 | exit 127; 14 | fi 15 | 16 | if [ $exitCode -eq 2 ] ; then 17 | # Zero packages out-of-date. We need to handle this case, since 'echo "$updates" | wc -l' would return 1 18 | echo "0" && exit 0 19 | fi 20 | 21 | # We need the extra newline (-n option omitted), since 'echo -n $"updates" | wc -l' is off by one, 22 | # since 'echo -n $"updates"' has a \0 at the end 23 | echo "$updates" | wc -l 24 | -------------------------------------------------------------------------------- /src/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #ifdef USE_LOGFILE 6 | #define LOG(x) \ 7 | std::cout << x << '\n'; \ 8 | { \ 9 | std::stringstream str; \ 10 | str << x; \ 11 | Logging::Log(str.str()); \ 12 | } 13 | #else 14 | #define LOG(x) std::cout << x << '\n' 15 | #endif 16 | #define ASSERT(x, log) \ 17 | if (!(x)) \ 18 | { \ 19 | LOG(log << "\n[Exiting due to assert failed]"); \ 20 | exit(-1); \ 21 | } 22 | namespace Logging 23 | { 24 | void Init(); 25 | 26 | void Log(const std::string& str); 27 | 28 | void Shutdown(); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 scorpion_26 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | 7 | outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachSystem ["x86_64-linux"] (system: 8 | let 9 | pkgs = import nixpkgs { 10 | inherit system; 11 | }; 12 | 13 | gbar = (with pkgs; stdenv.mkDerivation { 14 | 15 | name = "gbar"; 16 | src = ./.; 17 | 18 | nativeBuildInputs = [ 19 | pkg-config 20 | meson 21 | cmake 22 | ninja 23 | ]; 24 | buildInputs = [ 25 | wayland 26 | wayland-protocols 27 | wayland-scanner 28 | bluez 29 | gtk3 30 | gtk-layer-shell 31 | libpulseaudio 32 | libdbusmenu-gtk3 33 | libsass 34 | ]; 35 | }); 36 | in { 37 | defaultPackage = gbar; 38 | devShell = pkgs.mkShell { 39 | buildInputs = [ 40 | gbar 41 | ]; 42 | }; 43 | homeManagerModules.default = import ./module.nix self; 44 | } 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /css/dracula/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dracula Theme 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /protocols/sni-watcher.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Microsoft 2 | 3 | AlignAfterOpenBracket: Align 4 | AlignArrayOfStructures: Left 5 | AlignEscapedNewlines: Left 6 | AlignOperands: Align 7 | PenaltyBreakAssignment: 100 8 | PenaltyBreakBeforeFirstCallParameter: 20 9 | AllowShortBlocksOnASingleLine: Empty 10 | AllowShortCaseLabelsOnASingleLine: true 11 | AllowShortEnumsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: Inline 13 | AllowShortIfStatementsOnASingleLine: Never 14 | AllowShortLambdasOnASingleLine: Empty 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakTemplateDeclarations: Yes 17 | SpaceAfterTemplateKeyword: false 18 | Cpp11BracedListStyle: true 19 | IncludeBlocks: Preserve 20 | SortIncludes: false 21 | IndentAccessModifiers: false 22 | AccessModifierOffset: -4 # Stop indenting my access modifiers wierdly! I want them the same level as the class god damn! 23 | NamespaceIndentation: All 24 | FixNamespaceComments: false 25 | PointerAlignment: Left 26 | ColumnLimit: 150 27 | KeepEmptyLinesAtTheStartOfBlocks: false 28 | BreakBeforeBraces: Custom 29 | BraceWrapping: 30 | AfterClass: true 31 | AfterCaseLabel: true 32 | AfterControlStatement: Always 33 | AfterEnum: true 34 | AfterFunction: true 35 | AfterNamespace: true 36 | BeforeLambdaBody: true 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | manual: 11 | name: Build gBar manually 12 | runs-on: ubuntu-latest 13 | container: 14 | image: archlinux 15 | steps: 16 | - name: Setup Arch Keyring 17 | run: | 18 | pacman-key --init 19 | pacman-key --populate archlinux 20 | - name: Download pacman packages 21 | run: | 22 | pacman -Syu --noconfirm base-devel gcc git ninja meson gtk-layer-shell pulseaudio wayland libdbusmenu-gtk3 libsass python-packaging glib2-devel 23 | 24 | - name: Download gBar 25 | uses: actions/checkout@v4.1.2 26 | 27 | - name: Run meson 28 | run: | 29 | meson setup build 30 | 31 | - name: Build gBar 32 | run: | 33 | ninja -C build 34 | nix: 35 | name: Build using Nix 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Install Nix 39 | uses: cachix/install-nix-action@v20 40 | 41 | - name: Download gBar 42 | uses: actions/checkout@v3.3.0 43 | with: 44 | submodules: recursive 45 | 46 | - name: Build Nix flake 47 | run: | 48 | nix build --print-build-logs 49 | -------------------------------------------------------------------------------- /src/Plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "Plugin.h" 2 | #include "Common.h" 3 | #include "Window.h" 4 | #include "src/Log.h" 5 | 6 | #include 7 | 8 | void Plugin::LoadWidgetFromPlugin(const std::string& pluginName, Window& window, const std::string& monitor) 9 | { 10 | std::string home = std::getenv("HOME"); 11 | std::array paths = {home + "/.local/lib/gBar", "/usr/local/lib/gBar", "/usr/lib/gBar"}; 12 | // 1. Try and load plugin 13 | void* dl; 14 | for (auto& path : paths) 15 | { 16 | std::string soPath = path + "/lib" + pluginName + ".so"; 17 | dl = dlopen(soPath.c_str(), RTLD_NOW); 18 | if (dl) 19 | break; 20 | } 21 | ASSERT(dl, "Error: Cannot find plugin \"" << pluginName 22 | << "\"!\n" 23 | "Note: Did you mean to run \"gBar bar \" instead?"); 24 | 25 | typedef void (*PFN_InvokeCreateFun)(void*, void*); 26 | typedef int32_t (*PFN_GetVersion)(); 27 | auto getVersion = (PFN_GetVersion)dlsym(dl, "Plugin_GetVersion"); 28 | ASSERT(getVersion, "DL is not a valid gBar plugin!"); 29 | ASSERT(getVersion() == DL_VERSION, "Mismatching version, please recompile your plugin!"); 30 | 31 | auto invokeCreateFun = (PFN_InvokeCreateFun)dlsym(dl, "Plugin_InvokeCreateFun"); 32 | ASSERT(invokeCreateFun, "DL is not a valid gBar plugin!"); 33 | 34 | // Execute 35 | invokeCreateFun(&window, (void*)&monitor); 36 | } 37 | -------------------------------------------------------------------------------- /src/Workspaces.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Common.h" 3 | #include "System.h" 4 | #include "Config.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #ifdef WITH_WORKSPACES 15 | namespace Workspaces 16 | { 17 | void Init(); 18 | 19 | void PollStatus(const std::string& monitor, uint32_t numWorkspaces); 20 | 21 | System::WorkspaceStatus GetStatus(uint32_t workspaceId); 22 | 23 | uint32_t GetMaxUsedWorkspace(); 24 | 25 | void Shutdown(); 26 | 27 | // TODO: Use ext_workspaces for this, if applicable 28 | inline void Goto(uint32_t workspace) 29 | { 30 | if (RuntimeConfig::Get().hasWorkspaces == false) 31 | { 32 | LOG("Error: Called Go to workspace, but Workspaces isn't open!"); 33 | return; 34 | } 35 | LOG("Switching workspace: hyprctl dispatch workspace " << workspace); 36 | system(("hyprctl dispatch workspace " + std::to_string(workspace)).c_str()); 37 | } 38 | 39 | // direction: + or - 40 | inline void GotoNext(char direction) 41 | { 42 | char scrollOp = 'e'; 43 | if (Config::Get().workspaceScrollOnMonitor) 44 | { 45 | scrollOp = 'm'; 46 | } 47 | std::string cmd = std::string("hyprctl dispatch workspace ") + scrollOp + direction + "1"; 48 | LOG("Switching workspace: " << cmd.c_str()); 49 | system(cmd.c_str()); 50 | } 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1701040486, 24 | "narHash": "sha256-vawYwoHA5CwvjfqaT3A5CT9V36Eq43gxdwpux32Qkjw=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "45827faa2132b8eade424f6bdd48d8828754341a", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Found an issue with gBar? 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Please fill out this form and delete the defaults(everything not in bold font), so I can help you better* 11 | **Describe the bug** 12 | 13 | What doesn't work? Please be as precise as possible. 14 | e.g.: "gBar crashes on hovering the RAM sensor module. No other modules are affected." 15 | 16 | **Steps to Reproduce** 17 | 18 | What did you do, that triggered the issue? 19 | e.g.: 20 | 1. Open the bar 21 | 2. Hover over the RAM sensor module 22 | 3. gBar crashes 23 | 24 | **Expected behavior** 25 | 26 | What do you expect should happen? 27 | e.g.: gBar shouldn't crash, but stay open 28 | 29 | **Screenshots/Error logs** 30 | 31 | Please include as much information about the crash/bug as possible. 32 | - If it is a visual glitch, please include a screenshot/video as well as any modifications to the css (if any). 33 | - If it is a *crash* (e.g. ```Segmentation fault (Core dumped)``` in the console), please include the log (```/tmp/gBar-.log``` or from the console) and a crash stack trace. They can e.g. be found via journalctl (```journalctl --boot=0 --reverse```) if systemd-coredump is used. 34 | - If it is an *assert failed* (Last line in the log reads ```[Exiting due to assert failed]```) please include the log (```/tmp/gBar-.log``` or from the console). 35 | - The config (```~/.config/gBar/config```) used is always valuable to diagnose an issue, so please include it. 36 | 37 | **Information about your system and gBar** 38 | - OS: [e.g. Arch, Ubuntu, ...] 39 | - Desktop environment [e.g. Hyprland, Sway, ...] 40 | - commit sha256 if possible [e.g. 16e7d8aa50ddebeffb2509d7c0428b38cad565f8] 41 | -------------------------------------------------------------------------------- /protocols/sni-item.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 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 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "Widget.h" 4 | #include "Common.h" 5 | 6 | enum class Anchor 7 | { 8 | Top = BIT(0), 9 | Bottom = BIT(1), 10 | Left = BIT(2), 11 | Right = BIT(3) 12 | }; 13 | DEFINE_ENUM_FLAGS(Anchor); 14 | 15 | enum class Layer 16 | { 17 | Top, 18 | Overlay 19 | }; 20 | 21 | class Window 22 | { 23 | public: 24 | Window() = default; 25 | Window(int32_t monitor); 26 | Window(const std::string& monitor); 27 | Window(Window&& window) noexcept = default; 28 | Window& operator=(Window&& other) noexcept = default; 29 | ~Window(); 30 | 31 | void Init(const std::string& overrideConfigLocation); 32 | void Run(); 33 | 34 | void Close(); 35 | 36 | void SetAnchor(Anchor anchor) { m_Anchor = anchor; } 37 | void SetMargin(Anchor anchor, int32_t margin); 38 | void SetExclusive(bool exclusive) { m_Exclusive = exclusive; } 39 | void SetLayer(Layer layer) { m_Layer = layer; } 40 | void SetLayerNamespace(const std::string& layerNamespace) { m_LayerNamespace = layerNamespace; } 41 | 42 | void SetMainWidget(std::unique_ptr&& mainWidget); 43 | 44 | int GetWidth() const; 45 | int GetHeight() const; 46 | 47 | // Returns the connector name of the currnet monitor 48 | std::string GetName() const { return m_MonitorName; } 49 | 50 | // Callback when the widget should be recreated 51 | std::function OnWidget; 52 | 53 | private: 54 | void Create(); 55 | void Destroy(); 56 | 57 | void UpdateMargin(); 58 | 59 | void LoadCSS(GtkCssProvider* provider); 60 | 61 | void MonitorAdded(GdkDisplay* display, GdkMonitor* monitor); 62 | void MonitorRemoved(GdkDisplay* display, GdkMonitor* monitor); 63 | 64 | GtkWindow* m_Window = nullptr; 65 | GtkApplication* m_App = nullptr; 66 | 67 | std::unique_ptr m_MainWidget; 68 | 69 | Anchor m_Anchor; 70 | std::array, 4> m_Margin; 71 | bool m_Exclusive = true; 72 | Layer m_Layer = Layer::Top; 73 | std::string m_LayerNamespace = "gbar"; 74 | 75 | // The monitor we are currently on. 76 | std::string m_MonitorName; 77 | 78 | // The monitor we want to be on. 79 | std::string m_TargetMonitor; 80 | 81 | GdkMonitor* m_Monitor = nullptr; 82 | 83 | bool bShouldQuit = false; 84 | bool bHandleMonitorChanges = false; 85 | }; 86 | -------------------------------------------------------------------------------- /src/Wayland.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Common.h" 3 | 4 | struct wl_output; 5 | struct zext_workspace_group_handle_v1; 6 | struct zext_workspace_handle_v1; 7 | namespace Wayland 8 | { 9 | struct Monitor 10 | { 11 | std::string name; 12 | uint32_t wlName; 13 | int32_t width; 14 | int32_t height; 15 | int32_t scale; // TODO: Handle fractional scaling 16 | uint32_t rotation; 17 | zext_workspace_group_handle_v1* workspaceGroup; 18 | // The Gdk monitor index. This is only a hacky approximation, since there is no way to get the wl_output from a GdkMonitor 19 | uint32_t ID; 20 | }; 21 | 22 | struct Workspace 23 | { 24 | zext_workspace_group_handle_v1* parent; 25 | uint32_t id; 26 | bool active; 27 | }; 28 | struct WorkspaceGroup 29 | { 30 | std::vector workspaces; 31 | zext_workspace_handle_v1* lastActiveWorkspace; 32 | }; 33 | 34 | struct Window 35 | { 36 | std::string title; 37 | bool activated; 38 | }; 39 | 40 | void Init(); 41 | void PollEvents(); 42 | 43 | const std::unordered_map& GetMonitors(); 44 | const std::unordered_map& GetWorkspaceGroups(); 45 | const std::unordered_map& GetWorkspaces(); 46 | 47 | // Returns the connector name of the monitor 48 | std::string GtkMonitorIDToName(int32_t monitorID); 49 | int32_t NameToGtkMonitorID(const std::string& name); 50 | 51 | template 52 | inline const Monitor* FindMonitor(Predicate&& pred) 53 | { 54 | auto& mons = GetMonitors(); 55 | auto it = std::find_if(mons.begin(), mons.end(), 56 | [&](const std::pair& mon) 57 | { 58 | return pred(mon.second); 59 | }); 60 | return it != mons.end() ? &it->second : nullptr; 61 | } 62 | 63 | inline const Monitor* FindMonitorByName(const std::string& name) 64 | { 65 | return FindMonitor( 66 | [&](const Monitor& mon) 67 | { 68 | return mon.name == name; 69 | }); 70 | } 71 | 72 | const Window* GetActiveWindow(); 73 | 74 | void Shutdown(); 75 | } 76 | -------------------------------------------------------------------------------- /src/AMDGPU.h: -------------------------------------------------------------------------------- 1 | #include "Common.h" 2 | #include "Config.h" 3 | 4 | #include 5 | 6 | #ifdef WITH_AMD 7 | namespace AMDGPU 8 | { 9 | static const char* drmCardPrefix = "/sys/class/drm/"; 10 | static const char* utilizationFile = "/device/gpu_busy_percent"; 11 | static const char* vramTotalFile = "/device/mem_info_vram_total"; 12 | static const char* vramUsedFile = "/device/mem_info_vram_used"; 13 | 14 | inline void Init() 15 | { 16 | // Test for drm device files 17 | std::ifstream test(drmCardPrefix + Config::Get().drmAmdCard + utilizationFile); 18 | if (!test.is_open()) 19 | { 20 | LOG("AMD GPU not found, disabling AMD GPU"); 21 | RuntimeConfig::Get().hasAMD = false; 22 | } 23 | } 24 | 25 | inline uint32_t GetUtilization() 26 | { 27 | if (!RuntimeConfig::Get().hasAMD) 28 | { 29 | LOG("Error: Called AMD GetUtilization, but AMD GPU wasn't found!"); 30 | return {}; 31 | } 32 | 33 | std::ifstream file(drmCardPrefix + Config::Get().drmAmdCard + utilizationFile); 34 | std::string line; 35 | std::getline(file, line); 36 | return atoi(line.c_str()); 37 | } 38 | 39 | inline uint32_t GetTemperature() 40 | { 41 | if (!RuntimeConfig::Get().hasAMD) 42 | { 43 | LOG("Error: Called AMD GetTemperature, but AMD GPU wasn't found!"); 44 | return {}; 45 | } 46 | 47 | std::ifstream file(drmCardPrefix + Config::Get().drmAmdCard + Config::Get().amdGpuThermalZone); 48 | 49 | std::string line; 50 | std::getline(file, line); 51 | return atoi(line.c_str()) / 1000; 52 | } 53 | 54 | struct VRAM 55 | { 56 | uint64_t totalB; 57 | uint64_t usedB; 58 | }; 59 | 60 | inline VRAM GetVRAM() 61 | { 62 | if (!RuntimeConfig::Get().hasAMD) 63 | { 64 | LOG("Error: Called AMD GetVRAM, but AMD GPU wasn't found!"); 65 | return {}; 66 | } 67 | VRAM mem{}; 68 | 69 | std::ifstream file(drmCardPrefix + Config::Get().drmAmdCard + vramTotalFile); 70 | std::string line; 71 | std::getline(file, line); 72 | mem.totalB = strtoul(line.c_str(), nullptr, 10); 73 | 74 | file = std::ifstream(drmCardPrefix + Config::Get().drmAmdCard + vramUsedFile); 75 | std::getline(file, line); 76 | mem.usedB = strtoul(line.c_str(), nullptr, 10); 77 | 78 | return mem; 79 | } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /src/System.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace System 8 | { 9 | // From 0-1, all cores 10 | double GetCPUUsage(); 11 | // Tctl 12 | double GetCPUTemp(); 13 | 14 | bool IsBatteryCharging(); 15 | double GetBatteryPercentage(); 16 | 17 | struct RAMInfo 18 | { 19 | double totalGiB; 20 | double freeGiB; 21 | }; 22 | RAMInfo GetRAMInfo(); 23 | 24 | #if defined WITH_NVIDIA || defined WITH_AMD 25 | struct GPUInfo 26 | { 27 | double utilisation; 28 | double coreTemp; 29 | }; 30 | GPUInfo GetGPUInfo(); 31 | 32 | struct VRAMInfo 33 | { 34 | double totalGiB; 35 | double usedGiB; 36 | }; 37 | VRAMInfo GetVRAMInfo(); 38 | #endif 39 | 40 | struct DiskInfo 41 | { 42 | std::string partition; 43 | double totalGiB; 44 | double usedGiB; 45 | }; 46 | DiskInfo GetDiskInfo(); 47 | 48 | #ifdef WITH_BLUEZ 49 | struct BluetoothDevice 50 | { 51 | bool connected; 52 | bool paired; 53 | std::string mac; 54 | std::string name; 55 | // Known types: input-[keyboard,mouse]; audio-headset 56 | std::string type; 57 | }; 58 | 59 | struct BluetoothInfo 60 | { 61 | std::string defaultController; 62 | std::vector devices; 63 | }; 64 | BluetoothInfo GetBluetoothInfo(); 65 | void StartBTScan(); 66 | void StopBTScan(); 67 | 68 | // MT functions, callback, is from different thread 69 | void ConnectBTDevice(BluetoothDevice& device, std::function onFinish); 70 | void DisconnectBTDevice(BluetoothDevice& device, std::function onFinish); 71 | 72 | void OpenBTWidget(); 73 | 74 | std::string BTTypeToIcon(const BluetoothDevice& dev); 75 | #endif 76 | 77 | struct AudioInfo 78 | { 79 | double sinkVolume; 80 | bool sinkMuted; 81 | 82 | double sourceVolume; 83 | bool sourceMuted; 84 | }; 85 | AudioInfo GetAudioInfo(); 86 | void SetVolumeSink(double volume); 87 | void SetVolumeSource(double volume); 88 | void SetMutedSink(bool muted); 89 | void SetMutedSource(bool muted); 90 | 91 | #ifdef WITH_WORKSPACES 92 | enum class WorkspaceStatus 93 | { 94 | Dead, 95 | Inactive, 96 | Visible, 97 | Current, 98 | Active 99 | }; 100 | void PollWorkspaces(const std::string& monitor, uint32_t numWorkspaces); 101 | WorkspaceStatus GetWorkspaceStatus(uint32_t workspace); 102 | uint32_t GetMaxUsedWorkspace(); 103 | void GotoWorkspace(uint32_t workspace); 104 | // direction: + or - 105 | void GotoNextWorkspace(char direction); 106 | std::string GetWorkspaceSymbol(int index); 107 | #endif 108 | 109 | // Bytes per second upload. dx is time since last call. Will always return 0 on first run 110 | double GetNetworkBpsUpload(double dt); 111 | // Bytes per second download. dx is time since last call. Will always return 0 on first run 112 | double GetNetworkBpsDownload(double dt); 113 | 114 | // This can only be called one at a time. If it is already running it is assumed, that the old handler is no longer valid. 115 | void GetOutdatedPackagesAsync(std::function&& returnVal); 116 | 117 | std::string GetTime(); 118 | 119 | std::string GetActiveWindowTitle(); 120 | 121 | void Shutdown(); 122 | void Reboot(); 123 | void ExitWM(); 124 | void Lock(); 125 | void Suspend(); 126 | 127 | void Init(const std::string& overrideConfigLocation); 128 | void FreeResources(); 129 | } 130 | -------------------------------------------------------------------------------- /src/CSS.cpp: -------------------------------------------------------------------------------- 1 | #include "CSS.h" 2 | #include "Common.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef WITH_LIBSASS 9 | #include 10 | #endif 11 | 12 | namespace CSS 13 | { 14 | static GtkCssProvider* sProvider; 15 | 16 | #ifdef WITH_LIBSASS 17 | bool CompileAndLoadSCSS(const std::string& scssFile) 18 | { 19 | if (!std::ifstream(scssFile).is_open()) 20 | { 21 | LOG("Warning: Couldn't open " << scssFile); 22 | return false; 23 | } 24 | 25 | LOG("Info: Compiling " << scssFile); 26 | Sass_File_Context* ctx = sass_make_file_context(scssFile.c_str()); 27 | Sass_Context* ctxout = sass_file_context_get_context(ctx); 28 | sass_compile_file_context(ctx); 29 | if (sass_context_get_error_status(ctxout)) 30 | { 31 | LOG("Error compiling SCSS: " << sass_context_get_error_message(ctxout)); 32 | return false; 33 | } 34 | 35 | std::string data = sass_context_get_output_string(ctxout); 36 | GError* err = nullptr; 37 | gtk_css_provider_load_from_data(sProvider, data.c_str(), data.length(), &err); 38 | if (err != nullptr) 39 | { 40 | LOG("Error loading compiled SCSS: " << err->message); 41 | g_error_free(err); 42 | err = nullptr; 43 | return false; 44 | } 45 | 46 | sass_delete_file_context(ctx); 47 | return true; 48 | } 49 | #endif 50 | 51 | bool LoadCSS(const std::string& cssFile) 52 | { 53 | if (!std::ifstream(cssFile).is_open()) 54 | { 55 | LOG("Warning: Couldn't open " << cssFile); 56 | return false; 57 | } 58 | 59 | LOG("Info: Loading " << cssFile); 60 | GError* err = nullptr; 61 | gtk_css_provider_load_from_path(sProvider, cssFile.c_str(), &err); 62 | if (err != nullptr) 63 | { 64 | LOG("Error loading CSS: " << err->message); 65 | g_error_free(err); 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | void Load(const std::string& overrideConfigLocation) 72 | { 73 | sProvider = gtk_css_provider_new(); 74 | 75 | std::vector locations; 76 | const char* home = std::getenv("HOME"); 77 | 78 | if (overrideConfigLocation != "") 79 | { 80 | locations.push_back(overrideConfigLocation); 81 | } 82 | 83 | const char* configHome = std::getenv("XDG_CONFIG_HOME"); 84 | if (configHome && strlen(configHome) != 0) 85 | { 86 | locations.push_back(std::string(configHome) + "/gBar"); 87 | } 88 | else if (home) 89 | { 90 | locations.push_back(std::string(home) + "/.config/gBar"); 91 | } 92 | 93 | const char* dataHome = std::getenv("XDG_DATA_HOME"); 94 | if (dataHome && strlen(dataHome) != 0) 95 | { 96 | locations.push_back(std::string(dataHome) + "/gBar"); 97 | } 98 | else if (home) 99 | { 100 | locations.push_back(std::string(home) + "/.local/share/gBar"); 101 | } 102 | 103 | const char* dataDirs = std::getenv("XDG_DATA_DIRS"); 104 | if (dataDirs && strlen(dataDirs) != 0) 105 | { 106 | std::stringstream ss(dataDirs); 107 | std::string dir; 108 | while (std::getline(ss, dir, ':')) 109 | locations.push_back(dir + "/gBar"); 110 | } 111 | else 112 | { 113 | locations.push_back("/usr/local/share/gBar"); 114 | locations.push_back("/usr/share/gBar"); 115 | } 116 | 117 | for (auto& dir : locations) 118 | { 119 | #ifdef WITH_LIBSASS 120 | if (!Config::Get().forceCSS) 121 | { 122 | if (CompileAndLoadSCSS(dir + "/style.scss")) 123 | { 124 | LOG("SCSS found and loaded successfully!"); 125 | return; 126 | } 127 | else 128 | { 129 | LOG("Warning: Failed loading SCSS, falling back to CSS!"); 130 | } 131 | } 132 | #endif 133 | 134 | if (LoadCSS(dir + "/style.css")) 135 | { 136 | LOG("CSS found and loaded successfully!"); 137 | return; 138 | } 139 | } 140 | ASSERT(false, "No CSS file found!"); 141 | } 142 | 143 | GtkCssProvider* GetProvider() 144 | { 145 | return sProvider; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/NvidiaGPU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Common.h" 3 | #include "Config.h" 4 | 5 | #include 6 | 7 | #ifdef WITH_NVIDIA 8 | namespace NvidiaGPU 9 | { 10 | struct GPUUtilization 11 | { 12 | uint32_t gpu; 13 | uint32_t vram; 14 | }; 15 | struct VRAM 16 | { 17 | uint64_t totalB; 18 | uint64_t freeB; 19 | uint64_t usedB; 20 | }; 21 | 22 | static void* nvmldl; 23 | 24 | static void* nvmlGPUHandle; 25 | 26 | // Preloaded Query functions 27 | typedef int (*PFN_nvmlDeviceGetUtilizationRates)(void*, struct GPUUtilization*); 28 | static PFN_nvmlDeviceGetUtilizationRates nvmlDeviceGetUtilizationRates; 29 | typedef int (*PFN_nvmlDeviceGetTemperature)(void*, uint32_t, uint32_t*); 30 | static PFN_nvmlDeviceGetTemperature nvmlDeviceGetTemperature; 31 | typedef int (*PFN_nvmlDeviceGetMemoryInfo)(void*, struct VRAM*); 32 | static PFN_nvmlDeviceGetMemoryInfo nvmlDeviceGetMemoryInfo; 33 | 34 | inline void Init() 35 | { 36 | if (nvmldl || !RuntimeConfig::Get().hasNvidia) 37 | return; 38 | 39 | nvmldl = dlopen("libnvidia-ml.so", RTLD_NOW); 40 | // nvmldl not found. Nvidia probably not installed 41 | if (!nvmldl) 42 | { 43 | LOG("NVML not found, disabling Nvidia GPU"); 44 | RuntimeConfig::Get().hasNvidia = false; 45 | return; 46 | } 47 | 48 | typedef int (*PFN_nvmlInit)(); 49 | auto nvmlInit = (PFN_nvmlInit)dlsym(nvmldl, "nvmlInit"); 50 | int res = nvmlInit(); 51 | if (res != 0) 52 | { 53 | LOG("Failed initializing nvml (Error: " << res << "), disabling Nvidia GPU"); 54 | RuntimeConfig::Get().hasNvidia = false; 55 | return; 56 | } 57 | 58 | // Get GPU handle 59 | typedef int (*PFN_nvmlDeviceGetHandle)(uint32_t, void**); 60 | auto nvmlDeviceGetHandle = (PFN_nvmlDeviceGetHandle)dlsym(nvmldl, "nvmlDeviceGetHandleByIndex"); 61 | res = nvmlDeviceGetHandle(0, &nvmlGPUHandle); 62 | ASSERT(res == 0, "Failed getting device (Error: " << res << ")!"); 63 | 64 | // Dynamically load functions 65 | nvmlDeviceGetUtilizationRates = (PFN_nvmlDeviceGetUtilizationRates)dlsym(nvmldl, "nvmlDeviceGetUtilizationRates"); 66 | nvmlDeviceGetTemperature = (PFN_nvmlDeviceGetTemperature)dlsym(nvmldl, "nvmlDeviceGetTemperature"); 67 | nvmlDeviceGetMemoryInfo = (PFN_nvmlDeviceGetMemoryInfo)dlsym(nvmldl, "nvmlDeviceGetMemoryInfo"); 68 | 69 | // Check if all information is available 70 | GPUUtilization util; 71 | res = nvmlDeviceGetUtilizationRates(nvmlGPUHandle, &util); 72 | if (res != 0) 73 | { 74 | LOG("Failed querying utilization rates (Error: " << res << "), disabling Nvidia GPU!"); 75 | RuntimeConfig::Get().hasNvidia = false; 76 | return; 77 | } 78 | uint32_t temp; 79 | res = nvmlDeviceGetTemperature(nvmlGPUHandle, 0, &temp); 80 | if (res != 0) 81 | { 82 | LOG("Failed querying temperature (Error: " << res << "), disabling Nvidia GPU!"); 83 | RuntimeConfig::Get().hasNvidia = false; 84 | return; 85 | } 86 | VRAM mem; 87 | res = nvmlDeviceGetMemoryInfo(nvmlGPUHandle, &mem); 88 | if (res != 0) 89 | { 90 | LOG("Failed querying VRAM (Error: " << res << "), disabling Nvidia GPU!"); 91 | RuntimeConfig::Get().hasNvidia = false; 92 | return; 93 | } 94 | } 95 | 96 | inline void Shutdown() 97 | { 98 | if (nvmldl) 99 | dlclose(nvmldl); 100 | } 101 | 102 | inline GPUUtilization GetUtilization() 103 | { 104 | if (!RuntimeConfig::Get().hasNvidia) 105 | { 106 | LOG("Error: Called Nvidia GetUtilization, but nvml wasn't found!"); 107 | return {}; 108 | } 109 | 110 | GPUUtilization util; 111 | int res = nvmlDeviceGetUtilizationRates(nvmlGPUHandle, &util); 112 | ASSERT(res == 0, "Failed getting utilization (Error: " << res << ")!"); 113 | return util; 114 | } 115 | 116 | inline uint32_t GetTemperature() 117 | { 118 | if (!RuntimeConfig::Get().hasNvidia) 119 | { 120 | LOG("Error: Called Nvidia GetTemperature, but nvml wasn't found!"); 121 | return {}; 122 | } 123 | 124 | uint32_t temp; 125 | int res = nvmlDeviceGetTemperature(nvmlGPUHandle, 0, &temp); 126 | ASSERT(res == 0, "Failed getting temperature (Error: " << res << ")!"); 127 | return temp; 128 | } 129 | 130 | inline VRAM GetVRAM() 131 | { 132 | if (!RuntimeConfig::Get().hasNvidia) 133 | { 134 | LOG("Error: Called Nvidia GetVRAM, but nvml wasn't found!"); 135 | return {}; 136 | } 137 | 138 | VRAM mem; 139 | int res = nvmlDeviceGetMemoryInfo(nvmlGPUHandle, &mem); 140 | ASSERT(res == 0, "Failed getting memory (Error: " << res << ")!"); 141 | return mem; 142 | } 143 | } 144 | #endif 145 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('gBar', 2 | ['c', 'cpp'], 3 | version: '0.0.1', 4 | license: 'MIT', 5 | meson_version: '>=0.53.0', 6 | default_options: ['cpp_std=c++17', 7 | 'warning_level=3', 8 | 'default_library=static', 9 | 'buildtype=release'], 10 | ) 11 | 12 | # Wayland protocols 13 | wayland_client = dependency('wayland-client') 14 | wayland_scanner = find_program('wayland-scanner') 15 | 16 | ext_workspace_src = custom_target('generate-ext-workspace-src', 17 | input: ['protocols/ext-workspace-unstable-v1.xml'], 18 | output: ['ext-workspace-unstable-v1.c'], 19 | command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@']) 20 | 21 | ext_workspace_header = custom_target('generate-ext-workspace-header', 22 | input: ['protocols/ext-workspace-unstable-v1.xml'], 23 | output: ['ext-workspace-unstable-v1.h'], 24 | command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@']) 25 | 26 | wlr_foreign_toplevel_src = custom_target('generate-wlr-foreign-toplevel-src', 27 | input: ['protocols/wlr-foreign-toplevel-management-unstable-v1.xml'], 28 | output: ['wlr-foreign-toplevel-management-unstable-v1.c'], 29 | command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@']) 30 | 31 | wlr_foreign_toplevel_header = custom_target('generate-wlr-foreign-toplevel-header', 32 | input: ['protocols/wlr-foreign-toplevel-management-unstable-v1.xml'], 33 | output: ['wlr-foreign-toplevel-management-unstable-v1.h'], 34 | command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@']) 35 | gtk = dependency('gtk+-3.0') 36 | gtk_layer_shell = dependency('gtk-layer-shell-0') 37 | 38 | pulse = dependency('libpulse') 39 | 40 | 41 | headers = [ 42 | 'src/Common.h', 43 | 'src/Log.h', 44 | 'src/System.h', 45 | 'src/PulseAudio.h', 46 | 'src/Widget.h', 47 | 'src/Window.h', 48 | 'src/Wayland.h', 49 | 'src/Config.h', 50 | 'src/CSS.h' 51 | ] 52 | 53 | sources = [ 54 | ext_workspace_src, 55 | ext_workspace_header, 56 | wlr_foreign_toplevel_src, 57 | wlr_foreign_toplevel_header, 58 | 'src/Window.cpp', 59 | 'src/Widget.cpp', 60 | 'src/Wayland.cpp', 61 | 'src/System.cpp', 62 | 'src/Bar.cpp', 63 | 'src/Workspaces.cpp', 64 | 'src/AudioFlyin.cpp', 65 | 'src/BluetoothDevices.cpp', 66 | 'src/Plugin.cpp', 67 | 'src/Config.cpp', 68 | 'src/CSS.cpp', 69 | 'src/Log.cpp', 70 | 'src/SNI.cpp', 71 | ] 72 | 73 | dependencies = [gtk, gtk_layer_shell, pulse, wayland_client] 74 | 75 | if get_option('WithHyprland') 76 | add_global_arguments('-DWITH_HYPRLAND', language: 'cpp') 77 | headers += 'src/Workspaces.h' 78 | endif 79 | if get_option('WithWorkspaces') 80 | add_global_arguments('-DWITH_WORKSPACES', language: 'cpp') 81 | headers += 'src/Workspaces.h' 82 | endif 83 | if get_option('WithLibSass') 84 | add_global_arguments('-DWITH_LIBSASS', language: 'cpp') 85 | dependencies += dependency('libsass') 86 | endif 87 | if get_option('WithNvidia') 88 | add_global_arguments('-DWITH_NVIDIA', language: 'cpp') 89 | headers += 'src/NvidiaGPU.h' 90 | endif 91 | if get_option('WithAMD') 92 | add_global_arguments('-DWITH_AMD', language: 'cpp') 93 | headers += 'src/AMDGPU.h' 94 | endif 95 | if get_option('WithBlueZ') 96 | add_global_arguments('-DWITH_BLUEZ', language: 'cpp') 97 | endif 98 | if get_option('WithSNI') 99 | add_global_arguments('-DWITH_SNI', language: 'cpp') 100 | 101 | # DBus protocols 102 | dbus_gen = find_program('gdbus-codegen') 103 | sni_item_src = custom_target('generate-sni-item-src', 104 | input: ['protocols/sni-item.xml'], 105 | output: ['sni-item.c'], 106 | command: [dbus_gen, '--c-namespace', 'sni', '--body', '--output', '@OUTPUT@', '@INPUT@']) 107 | 108 | sni_item_header = custom_target('generate-sni-item-header', 109 | input: ['protocols/sni-item.xml'], 110 | output: ['sni-item.h'], 111 | command: [dbus_gen, '--c-namespace', 'sni', '--header', '--output', '@OUTPUT@', '@INPUT@']) 112 | 113 | sni_watcher_src = custom_target('generate-sni-watcher-src', 114 | input: ['protocols/sni-watcher.xml'], 115 | output: ['sni-watcher.c'], 116 | command: [dbus_gen, '--c-namespace', 'sni', '--body', '--output', '@OUTPUT@', '@INPUT@']) 117 | 118 | sni_watcher_header = custom_target('generate-sni-watcher-header', 119 | input: ['protocols/sni-watcher.xml'], 120 | output: ['sni-watcher.h'], 121 | command: [dbus_gen, '--c-namespace', 'sni', '--header', '--output', '@OUTPUT@', '@INPUT@']) 122 | libdbusmenu = dependency('dbusmenu-gtk3-0.4') 123 | 124 | sources += sni_item_src 125 | sources += sni_item_header 126 | sources += sni_watcher_src 127 | sources += sni_watcher_header 128 | 129 | dependencies += libdbusmenu 130 | endif 131 | 132 | add_global_arguments('-DUSE_LOGFILE', language: 'cpp') 133 | 134 | libgBar = library('gBar', 135 | sources, 136 | dependencies: dependencies, 137 | install: true) 138 | 139 | pkg = import('pkgconfig') 140 | pkg.generate(libgBar) 141 | 142 | executable( 143 | 'gBar', 144 | ['src/gBar.cpp'], 145 | dependencies: [gtk], 146 | link_with: libgBar, 147 | install_rpath: '$ORIGIN/', 148 | install: true 149 | ) 150 | 151 | install_headers( 152 | headers, 153 | subdir: 'gBar' 154 | ) 155 | 156 | install_data( 157 | ['css/style.css', 158 | 'css/style.scss'], 159 | install_dir: get_option('datadir') / 'gBar' 160 | ) 161 | -------------------------------------------------------------------------------- /src/AudioFlyin.cpp: -------------------------------------------------------------------------------- 1 | #include "AudioFlyin.h" 2 | #include "System.h" 3 | 4 | namespace AudioFlyin 5 | { 6 | namespace DynCtx 7 | { 8 | Type type; 9 | 10 | Window* win; 11 | Slider* slider; 12 | Text* icon; 13 | double sliderVal; 14 | bool muted = false; 15 | 16 | int32_t msOpen = 0; 17 | constexpr int32_t closeTime = 2000; 18 | constexpr int32_t height = 50; 19 | int32_t curCloseTime = closeTime; 20 | int32_t transitionTime = 50; 21 | 22 | void OnChangeVolume(Slider&, double value) 23 | { 24 | if (type == Type::Speaker) 25 | { 26 | System::SetVolumeSink(value); 27 | } 28 | else if (type == Type::Microphone) 29 | { 30 | System::SetVolumeSource(value); 31 | } 32 | } 33 | 34 | TimerResult Main(Box&) 35 | { 36 | System::AudioInfo info = System::GetAudioInfo(); 37 | if (type == Type::Speaker) 38 | { 39 | if (sliderVal != info.sinkVolume || muted != info.sinkMuted) 40 | { 41 | // Extend timer 42 | curCloseTime = msOpen + closeTime; 43 | 44 | sliderVal = info.sinkVolume; 45 | slider->SetValue(info.sinkVolume); 46 | 47 | muted = info.sinkMuted; 48 | if (info.sinkMuted) 49 | { 50 | icon->SetText(Config::Get().speakerMutedIcon); 51 | } 52 | else 53 | { 54 | icon->SetText(Config::Get().speakerHighIcon); 55 | } 56 | } 57 | } 58 | else if (type == Type::Microphone) 59 | { 60 | if (sliderVal != info.sourceVolume || muted != info.sourceMuted) 61 | { 62 | // Extend timer 63 | curCloseTime = msOpen + closeTime; 64 | 65 | sliderVal = info.sourceVolume; 66 | slider->SetValue(info.sourceVolume); 67 | 68 | muted = info.sourceMuted; 69 | if (info.sourceMuted) 70 | { 71 | icon->SetText(Config::Get().micMutedIcon); 72 | } 73 | else 74 | { 75 | icon->SetText(Config::Get().micHighIcon); 76 | } 77 | } 78 | } 79 | 80 | msOpen++; 81 | 82 | if (Config::Get().manualFlyinAnimation) 83 | { 84 | auto marginFunction = [](int32_t x) -> int32_t 85 | { 86 | // A inverted, cutoff 'V' shape 87 | // Fly in -> hover -> fly out 88 | double steepness = (double)height / (double)transitionTime; 89 | return (int32_t)std::min(-std::abs((double)x - (double)curCloseTime / 2) * steepness + (double)curCloseTime / 2, (double)height); 90 | }; 91 | win->SetMargin(Anchor::Bottom, marginFunction(msOpen)); 92 | } 93 | 94 | if (msOpen >= curCloseTime) 95 | { 96 | win->Close(); 97 | } 98 | return TimerResult::Ok; 99 | } 100 | } 101 | void WidgetAudio(Widget& parent) 102 | { 103 | auto slider = Widget::Create(); 104 | slider->SetOrientation(Orientation::Horizontal); 105 | slider->SetHorizontalTransform({100, true, Alignment::Fill}); 106 | slider->SetInverted(true); 107 | if (DynCtx::type == Type::Speaker) 108 | { 109 | slider->SetClass("audio-volume"); 110 | } 111 | else if (DynCtx::type == Type::Microphone) 112 | { 113 | slider->SetClass("mic-volume"); 114 | } 115 | slider->OnValueChange(DynCtx::OnChangeVolume); 116 | slider->SetRange({0, 1, 0.01}); 117 | DynCtx::slider = slider.get(); 118 | 119 | auto icon = Widget::Create(); 120 | icon->SetHorizontalTransform({-1, true, Alignment::Fill, 0, 8}); 121 | if (DynCtx::type == Type::Speaker) 122 | { 123 | icon->SetClass("audio-icon"); 124 | icon->SetText(Config::Get().speakerHighIcon); 125 | } 126 | else if (DynCtx::type == Type::Microphone) 127 | { 128 | icon->SetClass("mic-icon"); 129 | icon->SetText(Config::Get().speakerMutedIcon); 130 | } 131 | 132 | DynCtx::icon = icon.get(); 133 | 134 | parent.AddChild(std::move(slider)); 135 | parent.AddChild(std::move(icon)); 136 | } 137 | 138 | void Create(Window& window, UNUSED const std::string& monitor, Type type) 139 | { 140 | DynCtx::win = &window; 141 | DynCtx::type = type; 142 | auto mainWidget = Widget::Create(); 143 | mainWidget->SetSpacing({8, false}); 144 | mainWidget->SetVerticalTransform({16, true, Alignment::Fill}); 145 | mainWidget->SetClass("bar"); 146 | // We update the margin in the timer, so we need late dispatch. 147 | mainWidget->AddTimer(DynCtx::Main, 1, TimerDispatchBehaviour::LateDispatch); 148 | 149 | auto padding = Widget::Create(); 150 | padding->SetHorizontalTransform({8, true, Alignment::Fill}); 151 | mainWidget->AddChild(std::move(padding)); 152 | 153 | WidgetAudio(*mainWidget); 154 | 155 | padding = Widget::Create(); 156 | mainWidget->AddChild(std::move(padding)); 157 | 158 | // We want the audio flyin on top of fullscreen windows 159 | window.SetLayerNamespace("gbar-audio"); 160 | window.SetLayer(Layer::Overlay); 161 | window.SetExclusive(false); 162 | window.SetAnchor(Anchor::Bottom); 163 | window.SetMainWidget(std::move(mainWidget)); 164 | 165 | if (!Config::Get().manualFlyinAnimation) 166 | { 167 | // Do the margin here, so we don't need to update it every frame 168 | window.SetMargin(Anchor::Bottom, DynCtx::height); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/gBar.cpp: -------------------------------------------------------------------------------- 1 | #include "Window.h" 2 | #include "Common.h" 3 | #include "System.h" 4 | #include "Bar.h" 5 | #include "AudioFlyin.h" 6 | #include "BluetoothDevices.h" 7 | #include "Plugin.h" 8 | #include "Config.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | const char* audioTmpFilePath = "/tmp/gBar__audio"; 15 | const char* bluetoothTmpFilePath = "/tmp/gBar__bluetooth"; 16 | 17 | static bool tmpFileOpen = false; 18 | 19 | void OpenAudioFlyin(Window& window, const std::string& monitor, AudioFlyin::Type type) 20 | { 21 | tmpFileOpen = true; 22 | if (access(audioTmpFilePath, F_OK) != 0) 23 | { 24 | FILE* audioTempFile = fopen(audioTmpFilePath, "w"); 25 | AudioFlyin::Create(window, monitor, type); 26 | fclose(audioTempFile); 27 | } 28 | else 29 | { 30 | // Already open, close 31 | LOG("Audio flyin already open (/tmp/gBar__audio exists)! Exiting..."); 32 | exit(0); 33 | } 34 | } 35 | 36 | void CloseTmpFiles(int sig) 37 | { 38 | if (tmpFileOpen) 39 | { 40 | remove(audioTmpFilePath); 41 | remove(bluetoothTmpFilePath); 42 | } 43 | if (sig != 0) 44 | exit(1); 45 | } 46 | 47 | void PrintHelp() 48 | { 49 | LOG("==============================================\n" 50 | "| gBar |\n" 51 | "==============================================\n" 52 | "gBar, a fast status bar + widgets\n" 53 | "\n" 54 | "Basic usage: \n" 55 | "\tgBar [OPTIONS...] WIDGET [MONITOR]\n" 56 | "\n" 57 | "Sample usage:\n" 58 | "\tgBar bar DP-1 \tOpens the status bar on monitor \"DP-1\"\n" 59 | "\tgBar bar 0 \tOpens the status bar on monitor 0 (Legacy)\n" 60 | "\tgBar audio \tOpens the audio flyin on the current monitor\n" 61 | "\n" 62 | "All options:\n" 63 | "\t--help/-h \tPrints this help page and exits afterwards\n" 64 | "\t--config/-c DIR\tOverrides the config search path to DIR and appends DIR to the CSS search path.\n" 65 | "\t \t DIR cannot contain path shorthands like e.g. \"~\"" 66 | "\n" 67 | "All available widgets:\n" 68 | "\tbar \tThe main status bar\n" 69 | "\taudio \tAn audio volume slider flyin\n" 70 | "\tmic \tA microphone volume slider flyin\n" 71 | "\tbluetooth \tA bluetooth connection widget\n" 72 | "\t[plugin] \tTries to open and run the plugin lib[plugin].so\n"); 73 | } 74 | 75 | void CreateWidget(const std::string& widget, Window& window) 76 | { 77 | if (widget == "bar") 78 | { 79 | Bar::Create(window, window.GetName()); 80 | } 81 | else if (widget == "audio") 82 | { 83 | OpenAudioFlyin(window, window.GetName(), AudioFlyin::Type::Speaker); 84 | } 85 | else if (widget == "mic") 86 | { 87 | OpenAudioFlyin(window, window.GetName(), AudioFlyin::Type::Microphone); 88 | } 89 | #ifdef WITH_BLUEZ 90 | else if (widget == "bluetooth") 91 | { 92 | if (RuntimeConfig::Get().hasBlueZ) 93 | { 94 | if (access(bluetoothTmpFilePath, F_OK) != 0) 95 | { 96 | tmpFileOpen = true; 97 | FILE* bluetoothTmpFile = fopen(bluetoothTmpFilePath, "w"); 98 | BluetoothDevices::Create(window, window.GetName()); 99 | fclose(bluetoothTmpFile); 100 | } 101 | else 102 | { 103 | // Already open, close 104 | LOG("Bluetooth widget already open (/tmp/gBar__bluetooth exists)! Exiting..."); 105 | exit(0); 106 | } 107 | } 108 | else 109 | { 110 | LOG("Blutooth disabled, cannot open bluetooth widget!"); 111 | exit(1); 112 | } 113 | } 114 | #endif 115 | else 116 | { 117 | Plugin::LoadWidgetFromPlugin(widget, window, window.GetName()); 118 | } 119 | } 120 | 121 | int main(int argc, char** argv) 122 | { 123 | std::string widget; 124 | int32_t monitor = -1; 125 | std::string monitorName; 126 | std::string overrideConfigLocation = ""; 127 | 128 | // Arg parsing 129 | for (int i = 1; i < argc; i++) 130 | { 131 | std::string arg = argv[i]; 132 | if (arg.size() < 1 || arg[0] != '-') 133 | { 134 | // This must be the widget selection. 135 | widget = arg; 136 | if (i + 1 < argc) 137 | { 138 | std::string mon = argv[i + 1]; 139 | 140 | // Check if a monitor was supplied 141 | if (mon.empty() || mon[0] == '-') 142 | continue; 143 | 144 | if (std::isdigit(mon[0])) 145 | { 146 | // Monitor using ID 147 | monitor = std::stoi(mon); 148 | i += 1; 149 | } 150 | else 151 | { 152 | // Monitor using connector name 153 | monitorName = std::move(mon); 154 | i += 1; 155 | continue; 156 | } 157 | } 158 | } 159 | else if (arg == "-h" || arg == "--help") 160 | { 161 | PrintHelp(); 162 | return 0; 163 | } 164 | else if (arg == "-c" || arg == "--config") 165 | { 166 | ASSERT(i + 1 < argc, "Not enough arguments provided for -c/--config!"); 167 | overrideConfigLocation = argv[i + 1]; 168 | i += 1; 169 | } 170 | else 171 | { 172 | LOG("Warning: Unknown CLI option \"" << arg << "\"") 173 | } 174 | } 175 | if (widget == "") 176 | { 177 | LOG("Error: Widget to open not specified!\n"); 178 | PrintHelp(); 179 | return 0; 180 | } 181 | if (argc <= 1) 182 | { 183 | LOG("Error: Too little arguments\n"); 184 | PrintHelp(); 185 | return 0; 186 | } 187 | 188 | signal(SIGINT, CloseTmpFiles); 189 | System::Init(overrideConfigLocation); 190 | 191 | Window window; 192 | if (monitor != -1) 193 | { 194 | window = Window(monitor); 195 | } 196 | else 197 | { 198 | window = Window(monitorName); 199 | } 200 | 201 | window.Init(overrideConfigLocation); 202 | window.OnWidget = [&]() 203 | { 204 | CreateWidget(widget, window); 205 | }; 206 | window.Run(); 207 | 208 | System::FreeResources(); 209 | CloseTmpFiles(0); 210 | return 0; 211 | } 212 | -------------------------------------------------------------------------------- /src/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class Config 9 | { 10 | public: 11 | std::vector widgetsLeft = {"Workspaces"}; 12 | std::vector widgetsCenter = {"Time"}; 13 | std::vector widgetsRight = {"Tray", "Packages", "Audio", "Bluetooth", "Network", "Disk", 14 | "VRAM", "GPU", "RAM", "CPU", "Battery", "Power"}; 15 | 16 | std::string cpuThermalZone = ""; // idk, no standard way of doing this. 17 | std::string amdGpuThermalZone = "/device/hwmon/hwmon1/temp1_input"; 18 | std::string networkAdapter = "eno1"; // Is this standard? 19 | std::string drmAmdCard = "card0"; // The card to poll in AMDGPU. 20 | std::string suspendCommand = "systemctl suspend"; 21 | std::string lockCommand = ""; // idk, no standard way of doing this. 22 | std::string exitCommand = ""; // idk, no standard way of doing this. 23 | std::string batteryFolder = ""; // this can be BAT0, BAT1, etc. Usually in /sys/class/power_supply 24 | std::map workspaceSymbols; 25 | std::string defaultWorkspaceSymbol = ""; 26 | std::string dateTimeStyle = "%a %D - %H:%M:%S %Z"; // A sane default 27 | std::string dateTimeLocale = ""; // use system locale 28 | std::string diskPartition = "/"; // should be expectable on every linux system 29 | 30 | // Icons 31 | std::string shutdownIcon = " "; 32 | std::string rebootIcon = "󰑐"; 33 | std::string sleepIcon = "󰏤"; 34 | std::string lockIcon = ""; 35 | std::string exitIcon = "󰗼"; 36 | std::string btOffIcon = "󰂲"; 37 | std::string btOnIcon = "󰂯"; 38 | std::string btConnectedIcon = "󰂱"; 39 | std::string devKeyboardIcon = "󰌌 "; 40 | std::string devMouseIcon = "󰍽 "; 41 | std::string devHeadsetIcon = "󰋋 "; 42 | std::string devControllerIcon = "󰖺 "; 43 | std::string devUnknownIcon = " "; 44 | std::string speakerMutedIcon = "󰝟"; 45 | std::string speakerHighIcon = "󰕾"; 46 | std::string micMutedIcon = "󰍭"; 47 | std::string micHighIcon = "󰍬"; 48 | std::string packageOutOfDateIcon = "󰏔 "; 49 | // WS is handled by workspaceSymbols 50 | // TODO: Deprecate workspaceSymbols in favor of e.g. workspaceIcon-x 51 | // TODO: BluetoothDevices Refresh and close icons 52 | 53 | // Script that returns how many packages are out-of-date. The script should only print a number! 54 | // See data/update.sh for a human-readable version 55 | std::string checkPackagesCommand = 56 | "p=\"$(checkupdates)\"; e=$?; if [ $e -eq 127 ] ; then exit 127; fi; if [ $e -eq 2 ] ; then echo \"0\" && exit 0; fi; echo \"$p\" | wc -l"; 57 | 58 | bool forceCSS = false; // Whether to disable loading SCSS directly. 59 | bool centerWidgets = true; // Force the center widgets to be in the center. 60 | bool audioRevealer = false; 61 | bool audioInput = false; 62 | bool audioNumbers = false; // Affects both audio sliders 63 | bool manualFlyinAnimation = false; // Do the flyin animation via margin updating 64 | bool networkWidget = true; 65 | bool workspaceScrollOnMonitor = true; // Scroll through workspaces on monitor instead of all 66 | bool workspaceScrollInvert = false; // Up = +1, instead of Up = -1 67 | bool workspaceHideUnused = false; // Only display necessary workspaces (i.e. trailing empty workspaces) 68 | bool useHyprlandIPC = true; // Use Hyprland IPC instead of ext_workspaces protocol (Less buggy, but also less performant) 69 | bool enableSNI = true; // Enable tray icon 70 | bool sensorTooltips = false; // Use tooltips instead of sliders for the sensors 71 | bool iconsAlwaysUp = false; // Force icons to always point upwards in sidebar mode 72 | 73 | // Controls for color progression of the network widget 74 | uint32_t minUploadBytes = 0; // Bottom limit of the network widgets upload. Everything below it is considered "under" 75 | uint32_t maxUploadBytes = 4 * 1024 * 1024; // 4 MiB Top limit of the network widgets upload. Everything above it is considered "over" 76 | uint32_t minDownloadBytes = 0; // Bottom limit of the network widgets download. Everything above it is considered "under" 77 | uint32_t maxDownloadBytes = 10 * 1024 * 1024; // 10 MiB Top limit of the network widgets download. Everything above it is considered "over" 78 | 79 | uint32_t audioScrollSpeed = 5; // 5% each scroll 80 | uint32_t checkUpdateInterval = 5 * 60; // Interval to run the "checkPackagesCommand". In seconds 81 | uint32_t centerSpace = 300; // How much space should be reserved for the center widgets. 82 | uint32_t maxTitleLength = 30; // Maximum chars of the title widget. Longer titles will be shortened 83 | uint32_t numWorkspaces = 9; // How many workspaces to display 84 | uint32_t sensorSize = 24; // The size of the circular sensors 85 | uint32_t networkIconSize = 24; // The size of the two network arrows 86 | uint32_t batteryWarnThreshold = 20; // Threshold for color change when on battery 87 | 88 | char location = 'T'; // The Location of the bar. Can be L,R,T,B 89 | 90 | // SNIIconSize: ["Title String"], ["Size"] 91 | std::unordered_map sniIconSizes; 92 | std::unordered_map sniPaddingTop; 93 | std::unordered_map sniIconNames; 94 | std::unordered_map sniDisabled; 95 | 96 | // Only affects outputs (i.e.: speakers, not microphones). This remaps the range of the volume; In percent 97 | double audioMinVolume = 0.f; // Map the minimum volume to this value 98 | double audioMaxVolume = 100.f; // Map the maximum volume to this value 99 | 100 | static void Load(const std::string& overrideConfigLocation); 101 | static const Config& Get(); 102 | }; 103 | 104 | // Configs, that rely on specific files to be available(e.g. BlueZ running) 105 | class RuntimeConfig 106 | { 107 | public: 108 | #ifdef WITH_NVIDIA 109 | bool hasNvidia = true; 110 | #else 111 | bool hasNvidia = false; 112 | #endif 113 | #ifdef WITH_AMD 114 | bool hasAMD = true; 115 | #else 116 | bool hasAMD = false; 117 | #endif 118 | 119 | #ifdef WITH_WORKSPACES 120 | bool hasWorkspaces = true; 121 | #else 122 | bool hasWorkspaces = false; 123 | #endif 124 | 125 | #ifdef WITH_BLUEZ 126 | bool hasBlueZ = true; 127 | #else 128 | bool hasBlueZ = false; 129 | #endif 130 | 131 | #ifdef WITH_SNI 132 | bool hasSNI = true; 133 | #else 134 | bool hasSNI = false; 135 | #endif 136 | 137 | bool hasNet = true; 138 | 139 | bool hasPackagesScript = true; 140 | 141 | static RuntimeConfig& Get(); 142 | }; 143 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | all: unset; 3 | font-family: "CaskaydiaCove NF"; 4 | } 5 | 6 | .popup { 7 | background-color: #282a36; 8 | color: #50fa7b; 9 | border-radius: 16px; 10 | } 11 | 12 | menu { 13 | padding: 8px 8px 8px 8px; 14 | } 15 | 16 | .bar, tooltip { 17 | background-color: #282a36; 18 | border-radius: 16px; 19 | } 20 | 21 | .right { 22 | border-radius: 16px; 23 | } 24 | 25 | .time-text { 26 | font-size: 16px; 27 | } 28 | 29 | .title-text { 30 | font-size: 16px; 31 | } 32 | 33 | .reboot-button { 34 | font-size: 28px; 35 | color: #6272a4; 36 | } 37 | 38 | .sleep-button { 39 | font-size: 28px; 40 | color: #6272a4; 41 | } 42 | 43 | .lock-button { 44 | font-size: 22px; 45 | color: #6272a4; 46 | } 47 | 48 | .exit-button { 49 | font-size: 28px; 50 | color: #6272a4; 51 | } 52 | 53 | .power-button { 54 | font-size: 25px; 55 | color: #ff5555; 56 | } 57 | 58 | .system-confirm { 59 | color: #50fa7b; 60 | } 61 | 62 | trough { 63 | border-radius: 3px; 64 | border-width: 1px; 65 | border-style: none; 66 | background-color: #44475a; 67 | min-width: 4px; 68 | min-height: 4px; 69 | } 70 | 71 | slider { 72 | border-radius: 0%; 73 | border-width: 1px; 74 | border-style: none; 75 | margin: -9px -9px -9px -9px; 76 | min-width: 16px; 77 | min-height: 16px; 78 | background-color: transparent; 79 | } 80 | 81 | highlight { 82 | border-radius: 3px; 83 | border-width: 1px; 84 | border-style: none; 85 | min-width: 6px; 86 | min-height: 6px; 87 | } 88 | 89 | .audio-icon { 90 | font-size: 24px; 91 | color: #ffb86c; 92 | } 93 | 94 | .audio-volume { 95 | font-size: 16px; 96 | color: #ffb86c; 97 | } 98 | .audio-volume trough { 99 | background-color: #44475a; 100 | } 101 | .audio-volume slider { 102 | background-color: transparent; 103 | } 104 | .audio-volume highlight { 105 | background-color: #ffb86c; 106 | } 107 | 108 | .mic-icon { 109 | font-size: 24px; 110 | color: #bd93f9; 111 | } 112 | 113 | .mic-volume { 114 | font-size: 16px; 115 | color: #bd93f9; 116 | } 117 | .mic-volume trough { 118 | background-color: #44475a; 119 | } 120 | .mic-volume slider { 121 | background-color: transparent; 122 | } 123 | .mic-volume highlight { 124 | background-color: #bd93f9; 125 | } 126 | 127 | .package-outofdate { 128 | margin: -5px -5px -5px -5px; 129 | font-size: 24px; 130 | color: #ff5555; 131 | } 132 | 133 | .bt-num { 134 | font-size: 16px; 135 | color: #1793D1; 136 | } 137 | 138 | .bt-label-on { 139 | font-size: 20px; 140 | color: #1793D1; 141 | } 142 | 143 | .bt-label-off { 144 | font-size: 24px; 145 | color: #1793D1; 146 | } 147 | 148 | .bt-label-connected { 149 | font-size: 28px; 150 | color: #1793D1; 151 | } 152 | 153 | .disk-widget * { 154 | color: #bd93f9; 155 | font-size: 16px; 156 | } 157 | 158 | .disk-util-progress { 159 | background-color: #44475a; 160 | } 161 | 162 | .vram-widget * { 163 | color: #ffb86c; 164 | font-size: 16px; 165 | } 166 | 167 | .vram-util-progress { 168 | background-color: #44475a; 169 | } 170 | 171 | .ram-widget * { 172 | color: #f1fa8c; 173 | font-size: 16px; 174 | } 175 | 176 | .ram-util-progress { 177 | background-color: #44475a; 178 | } 179 | 180 | .gpu-widget * { 181 | color: #8be9fd; 182 | font-size: 16px; 183 | } 184 | 185 | .gpu-util-progress { 186 | background-color: #44475a; 187 | } 188 | 189 | .cpu-widget * { 190 | color: #50fa7b; 191 | font-size: 16px; 192 | } 193 | 194 | .cpu-util-progress { 195 | background-color: #44475a; 196 | } 197 | 198 | .battery-widget * { 199 | color: #ff79c6; 200 | font-size: 16px; 201 | } 202 | 203 | .battery-util-progress { 204 | background-color: #44475a; 205 | } 206 | 207 | .battery-charging { 208 | color: #ffb86c; 209 | } 210 | 211 | .battery-warning { 212 | color: #ff5555; 213 | } 214 | 215 | .network-data-text { 216 | color: #50fa7b; 217 | font-size: 16px; 218 | } 219 | 220 | .network-up-under { 221 | color: #44475a; 222 | } 223 | 224 | .network-up-low { 225 | color: #50fa7b; 226 | } 227 | 228 | .network-up-mid-low { 229 | color: #f1fa8c; 230 | } 231 | 232 | .network-up-mid-high { 233 | color: #ffb86c; 234 | } 235 | 236 | .network-up-high { 237 | color: #bd93f9; 238 | } 239 | 240 | .network-up-over { 241 | color: #ff5555; 242 | } 243 | 244 | .network-down-under { 245 | color: #44475a; 246 | } 247 | 248 | .network-down-low { 249 | color: #50fa7b; 250 | } 251 | 252 | .network-down-mid-low { 253 | color: #f1fa8c; 254 | } 255 | 256 | .network-down-mid-high { 257 | color: #ffb86c; 258 | } 259 | 260 | .network-down-high { 261 | color: #bd93f9; 262 | } 263 | 264 | .network-down-over { 265 | color: #ff5555; 266 | } 267 | 268 | .ws-dead { 269 | color: #44475a; 270 | font-size: 14px; 271 | } 272 | 273 | .ws-inactive { 274 | color: #6272a4; 275 | font-size: 14px; 276 | } 277 | 278 | .ws-visible { 279 | color: #8be9fd; 280 | font-size: 14px; 281 | } 282 | 283 | .ws-current { 284 | color: #f1fa8c; 285 | font-size: 14px; 286 | } 287 | 288 | .ws-active { 289 | color: #50fa7b; 290 | font-size: 14px; 291 | } 292 | 293 | @keyframes connectanim { 294 | from { 295 | background-image: radial-gradient(circle farthest-side at center, #1793D1 0%, transparent 0%, transparent 100%); 296 | } 297 | to { 298 | background-image: radial-gradient(circle farthest-side at center, #1793D1 0%, #1793D1 100%, transparent 100%); 299 | } 300 | } 301 | @keyframes disconnectanim { 302 | from { 303 | background-image: radial-gradient(circle farthest-side at center, transparent 0%, #1793D1 0%, #1793D1 100%); 304 | } 305 | to { 306 | background-image: radial-gradient(circle farthest-side at center, transparent 0%, transparent 100%, #1793D1 100%); 307 | } 308 | } 309 | @keyframes scanonanim { 310 | from { 311 | color: #f1fa8c; 312 | } 313 | to { 314 | color: #50fa7b; 315 | } 316 | } 317 | @keyframes scanoffanim { 318 | from { 319 | color: #50fa7b; 320 | } 321 | to { 322 | color: #f1fa8c; 323 | } 324 | } 325 | .bt-bg { 326 | background-color: #282a36; 327 | border-radius: 16px; 328 | } 329 | 330 | .bt-header-box { 331 | margin-top: 4px; 332 | margin-right: 8px; 333 | margin-left: 8px; 334 | font-size: 24px; 335 | color: #1793D1; 336 | } 337 | 338 | .bt-body-box { 339 | margin-right: 8px; 340 | margin-left: 8px; 341 | } 342 | 343 | .bt-button { 344 | border-radius: 16px; 345 | padding-left: 8px; 346 | padding-right: 8px; 347 | padding-top: 4px; 348 | padding-bottom: 4px; 349 | margin-bottom: 4px; 350 | margin-top: 4px; 351 | font-size: 16px; 352 | } 353 | .bt-button.active { 354 | animation-name: connectanim; 355 | animation-duration: 50ms; 356 | animation-timing-function: linear; 357 | animation-fill-mode: forwards; 358 | } 359 | .bt-button.inactive { 360 | animation-name: disconnectanim; 361 | animation-duration: 50ms; 362 | animation-timing-function: linear; 363 | animation-fill-mode: forwards; 364 | } 365 | .bt-button.failed { 366 | color: #ff5555; 367 | } 368 | 369 | .bt-close { 370 | color: #ff5555; 371 | background-color: #44475a; 372 | border-radius: 16px; 373 | padding: 0px 8px 0px 7px; 374 | margin: 0px 0px 0px 8px; 375 | } 376 | 377 | .bt-scan { 378 | color: #f1fa8c; 379 | background-color: #44475a; 380 | border-radius: 16px; 381 | padding: 2px 11px 0px 7px; 382 | margin: 0px 0px 0px 10px; 383 | font-size: 18px; 384 | } 385 | .bt-scan.active { 386 | animation-name: scanonanim; 387 | animation-duration: 50ms; 388 | animation-timing-function: linear; 389 | animation-fill-mode: forwards; 390 | } 391 | .bt-scan.inactive { 392 | animation-name: scanoffanim; 393 | animation-duration: 50ms; 394 | animation-timing-function: linear; 395 | animation-fill-mode: forwards; 396 | } 397 | 398 | /*# sourceMappingURL=style.css.map */ 399 | -------------------------------------------------------------------------------- /src/Common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "Log.h" 17 | 18 | #define UNUSED [[maybe_unused]] 19 | 20 | // Flag helper macros 21 | #define BIT(x) (1 << (x)) 22 | 23 | template, bool> = true> 24 | using EnumType = std::underlying_type_t; 25 | 26 | // Based on https://stackoverflow.com/questions/1448396/how-to-use-enums-as-flags-in-c 27 | // (Answer https://stackoverflow.com/a/23152590): Licensed under CC BY-SA 3.0 28 | #define DEFINE_ENUM_FLAGS(T) \ 29 | inline T operator~(T a) \ 30 | { \ 31 | return (T) ~(EnumType)a; \ 32 | } \ 33 | inline T operator|(T a, T b) \ 34 | { \ 35 | return (T)((EnumType)a | (EnumType)b); \ 36 | } \ 37 | inline T operator&(T a, T b) \ 38 | { \ 39 | return (T)((EnumType)a & (EnumType)b); \ 40 | } \ 41 | inline T operator^(T a, T b) \ 42 | { \ 43 | return (T)((EnumType)a ^ (EnumType)b); \ 44 | } \ 45 | inline T& operator|=(T& a, T b) \ 46 | { \ 47 | return (T&)((EnumType&)a |= (EnumType)b); \ 48 | } \ 49 | inline T& operator&=(T& a, T b) \ 50 | { \ 51 | return (T&)((EnumType&)a &= (EnumType)b); \ 52 | } \ 53 | inline T& operator^=(T& a, T b) \ 54 | { \ 55 | return (T&)((EnumType&)a ^= (EnumType)b); \ 56 | } 57 | 58 | #define FLAG_CHECK(val, check) ((int)(val & (check)) == (int)check) 59 | 60 | namespace Utils 61 | { 62 | template 63 | constexpr bool IsMapLike = false; 64 | 65 | template 66 | constexpr bool IsMapLike> = true; 67 | template 68 | constexpr bool IsMapLike> = true; 69 | 70 | inline std::string ToStringPrecision(double x, const char* fmt) 71 | { 72 | char buf[128]; 73 | snprintf(buf, sizeof(buf), fmt, x); 74 | return buf; 75 | } 76 | 77 | // Format must be something like %0.1f %s 78 | inline std::string StorageUnitDynamic(double bytes, const char* fmt) 79 | { 80 | constexpr double KiB = 1024; 81 | constexpr double MiB = 1024 * KiB; 82 | constexpr double GiB = 1024 * MiB; 83 | char buf[128]; 84 | if (bytes >= GiB) 85 | { 86 | snprintf(buf, sizeof(buf), fmt, bytes * (1 / GiB), "GiB"); 87 | return buf; 88 | } 89 | if (bytes >= MiB) 90 | { 91 | snprintf(buf, sizeof(buf), fmt, bytes * (1 / MiB), "MiB"); 92 | return buf; 93 | } 94 | if (bytes >= KiB) 95 | { 96 | snprintf(buf, sizeof(buf), fmt, bytes * (1 / KiB), "KiB"); 97 | return buf; 98 | } 99 | 100 | snprintf(buf, sizeof(buf), fmt, bytes, "B"); 101 | return buf; 102 | } 103 | 104 | template 105 | size_t RetrySocketOp(Func func, size_t retries, const char* socketOp) 106 | { 107 | ssize_t ret; 108 | size_t tries = 0; 109 | do 110 | { 111 | ret = func(); 112 | if (ret < 0) 113 | { 114 | // Error 115 | LOG("RetrySocketOp: " << socketOp << " failed with " << ret); 116 | } 117 | else 118 | { 119 | return ret; 120 | } 121 | tries++; 122 | } while (tries < retries); 123 | LOG("RetrySocketOp: Failed after " << retries << "tries"); 124 | return ret; 125 | } 126 | 127 | inline std::vector Split(const std::string& str, char delim) 128 | { 129 | std::stringstream strstr(str); 130 | std::string curElem; 131 | std::vector result; 132 | while (std::getline(strstr, curElem, delim)) 133 | { 134 | if (!curElem.empty()) 135 | { 136 | result.emplace_back(curElem); 137 | } 138 | } 139 | return result; 140 | } 141 | 142 | inline std::string FindFileWithName(const std::string& directory, const std::string& name) 143 | { 144 | if (!std::filesystem::exists(directory)) 145 | { 146 | return ""; 147 | } 148 | for (auto& path : std::filesystem::recursive_directory_iterator(directory)) 149 | { 150 | if (path.is_directory()) 151 | continue; 152 | if (path.path().filename().string().find(name) != std::string::npos) 153 | { 154 | return path.path().string(); 155 | } 156 | } 157 | return ""; 158 | } 159 | 160 | inline void Replace(std::string& str, const std::string& string, const std::string& replacement) 161 | { 162 | size_t curPos = 0; 163 | curPos = str.find(string); 164 | while (curPos != std::string::npos) 165 | { 166 | str.replace(curPos, string.length(), replacement); 167 | curPos += string.length(); 168 | curPos = str.find(string, curPos); 169 | } 170 | } 171 | } 172 | 173 | struct Process 174 | { 175 | pid_t pid; 176 | }; 177 | 178 | inline Process OpenProcess(const std::string& command) 179 | { 180 | pid_t child = fork(); 181 | ASSERT(child != -1, "fork error"); 182 | 183 | if (child == 0) 184 | { 185 | // Child 186 | system(command.c_str()); 187 | exit(0); 188 | } 189 | else 190 | { 191 | return {child}; 192 | } 193 | } 194 | 195 | template 196 | struct AsyncAtomicContext 197 | { 198 | std::atomic running = false; 199 | std::mutex queueLock; 200 | std::optional queue; 201 | }; 202 | 203 | // Executes the callback function asynchronously, but only one at a time. 204 | // Multiple requests at once are stored in a FIFO of size 1. 205 | // The context should point to a static reference. 206 | template 207 | inline void ExecuteAsyncAtomically(AsyncAtomicContext& context, const Callback& callback, const Data& data) 208 | { 209 | // Update the queue 210 | context.queueLock.lock(); 211 | context.queue = data; 212 | context.queueLock.unlock(); 213 | 214 | if (!context.running) 215 | { 216 | // Launch the thread 217 | context.running = true; 218 | std::thread( 219 | [&, callback]() 220 | { 221 | while (true) 222 | { 223 | context.queueLock.lock(); 224 | if (!context.queue.has_value()) 225 | { 226 | context.queueLock.unlock(); 227 | break; 228 | } 229 | Data data = std::move(*context.queue); 230 | context.queue = {}; 231 | context.queueLock.unlock(); 232 | 233 | // Execute 234 | callback(std::move(data)); 235 | } 236 | context.running = false; 237 | }) 238 | .detach(); 239 | } 240 | } 241 | 242 | // Plugins 243 | #include "Window.h" 244 | #define DL_VERSION 1 245 | 246 | #define DEFINE_PLUGIN(fun) \ 247 | extern "C" int32_t Plugin_GetVersion() \ 248 | { \ 249 | return DL_VERSION; \ 250 | }; \ 251 | extern "C" void Plugin_InvokeCreateFun(void* window, void* monitor) \ 252 | { \ 253 | fun(*(Window*)window, *(const std::string&*)monitor); \ 254 | } 255 | -------------------------------------------------------------------------------- /css/style.scss: -------------------------------------------------------------------------------- 1 | // colorscheme(dracula) 2 | // See https://github.com/dracula/dracula-theme/blob/master/LICENSE or dracula/LICENSE for the license 3 | $bg: #282a36; 4 | $fg: #f8f8f2; 5 | $inactive: #44475a; 6 | $darkblue: #6272a4; 7 | $cyan: #8be9fd; 8 | $green: #50fa7b; 9 | $orange: #ffb86c; 10 | $pink: #ff79c6; 11 | $purple: #bd93f9; 12 | $red: #ff5555; 13 | $yellow: #f1fa8c; 14 | 15 | 16 | $btblue: #1793D1; 17 | 18 | $textsize: 16px; 19 | 20 | *{ 21 | all: unset; 22 | font-family: "CaskaydiaCove NF"; 23 | } 24 | 25 | .popup { 26 | background-color: $bg; 27 | color: #50fa7b; 28 | border-radius: 16px; 29 | } 30 | 31 | menu { 32 | padding: 8px 8px 8px 8px; 33 | } 34 | 35 | // debug 36 | .cpu-box{ 37 | //background-color: #00cc00 38 | } 39 | 40 | .bar,tooltip{ 41 | background-color: $bg; 42 | border-radius: 16px; 43 | } 44 | 45 | .right { 46 | border-radius: 16px; 47 | } 48 | 49 | .time-text { 50 | font-size: $textsize; 51 | } 52 | 53 | .title-text { 54 | font-size: $textsize; 55 | } 56 | 57 | .reboot-button { 58 | font-size: 28px; 59 | 60 | color: $darkblue; 61 | } 62 | .sleep-button { 63 | font-size: 28px; 64 | 65 | color: $darkblue; 66 | } 67 | .lock-button { 68 | font-size: 22px; 69 | 70 | color: $darkblue; 71 | } 72 | .exit-button { 73 | font-size: 28px; 74 | 75 | color: $darkblue; 76 | } 77 | 78 | .power-button { 79 | font-size: 25px; 80 | 81 | color: $red; 82 | } 83 | 84 | .power-box { 85 | } 86 | 87 | .power-box-expand { 88 | } 89 | 90 | .system-confirm { 91 | color: $green; 92 | } 93 | 94 | // Common slider settings 95 | trough { 96 | border-radius: 3px; 97 | border-width: 1px; 98 | border-style: none; 99 | background-color: $inactive; 100 | // margin-top: 2px; 101 | min-width: 4px; 102 | min-height: 4px; 103 | } 104 | 105 | slider { 106 | // Controls the size of the control area (set border-style to solid to see) 107 | border-radius: 0%; 108 | border-width: 1px; 109 | border-style: none; 110 | margin: -9px -9px -9px -9px; 111 | min-width: 16px; 112 | min-height: 16px; 113 | background-color: transparent; 114 | } 115 | 116 | highlight { 117 | border-radius: 3px; 118 | border-width: 1px; 119 | border-style: none; 120 | // For vertical we need width, for horizontal we need height 121 | // Both can coexist though 122 | min-width: 6px; 123 | min-height: 6px; 124 | } 125 | 126 | .audio-icon { 127 | font-size: 24px; 128 | color: $orange; 129 | } 130 | 131 | .audio-volume { 132 | trough { 133 | background-color: $inactive; 134 | } 135 | 136 | slider { 137 | background-color: transparent; 138 | } 139 | 140 | highlight { 141 | background-color: $orange; 142 | } 143 | 144 | font-size: 16px; 145 | color: $orange; 146 | } 147 | 148 | .mic-icon { 149 | font-size: 24px; 150 | color: $purple; 151 | // margin-right: 0px; 152 | } 153 | 154 | .mic-volume { 155 | trough { 156 | background-color: $inactive; 157 | } 158 | 159 | slider { 160 | background-color: transparent; 161 | } 162 | 163 | highlight { 164 | background-color: $purple; 165 | } 166 | 167 | font-size: 16px; 168 | color: $purple; 169 | } 170 | 171 | .package-outofdate { 172 | margin: -5px -5px -5px -5px; 173 | font-size: 24px; 174 | color: $red; 175 | } 176 | 177 | .bt-num { 178 | font-size: $textsize; 179 | color: $btblue; 180 | } 181 | .bt-label-on { 182 | font-size: 20px; 183 | color: $btblue; 184 | } 185 | .bt-label-off { 186 | font-size: 24px; 187 | color: $btblue; 188 | } 189 | .bt-label-connected { 190 | font-size: 28px; 191 | color: $btblue; 192 | } 193 | 194 | .disk-widget * { 195 | color: $purple; 196 | font-size: $textsize; 197 | } 198 | .disk-util-progress { 199 | background-color: $inactive; 200 | } 201 | 202 | .vram-widget * { 203 | color: $orange; 204 | font-size: $textsize; 205 | } 206 | .vram-util-progress { 207 | background-color: $inactive; 208 | } 209 | 210 | .ram-widget * { 211 | color: $yellow; 212 | font-size: $textsize; 213 | } 214 | .ram-util-progress { 215 | background-color: $inactive; 216 | } 217 | 218 | .gpu-widget * { 219 | color: $cyan; 220 | font-size: $textsize; 221 | } 222 | .gpu-util-progress { 223 | background-color: $inactive; 224 | } 225 | 226 | .cpu-widget * { 227 | color: $green; 228 | font-size: $textsize; 229 | } 230 | .cpu-util-progress { 231 | background-color: $inactive; 232 | } 233 | 234 | .battery-widget * { 235 | color: $pink; 236 | font-size: $textsize; 237 | } 238 | .battery-util-progress { 239 | background-color: $inactive; 240 | } 241 | 242 | .battery-charging { 243 | color: $orange; 244 | } 245 | 246 | .battery-warning { 247 | color: $red; 248 | } 249 | 250 | .network-data-text { 251 | color: $green; 252 | font-size: $textsize; 253 | } 254 | 255 | // <= 0% (Below MinUploadBytes) 256 | .network-up-under { 257 | color: $inactive; 258 | } 259 | // <= 25% 260 | .network-up-low { 261 | color: $green; 262 | } 263 | // <= 50% 264 | .network-up-mid-low { 265 | color: $yellow; 266 | } 267 | // <= 75% 268 | .network-up-mid-high { 269 | color: $orange; 270 | } 271 | // <= 100% 272 | .network-up-high { 273 | color: $purple; 274 | } 275 | // > 100% (Above MaxUploadBytes) 276 | .network-up-over { 277 | color: $red; 278 | } 279 | 280 | // <= 0% (Below MinDownloadBytes) 281 | .network-down-under { 282 | color: $inactive; 283 | } 284 | // <= 25% 285 | .network-down-low { 286 | color: $green; 287 | } 288 | // <= 50% 289 | .network-down-mid-low { 290 | color: $yellow; 291 | } 292 | // <= 75% 293 | .network-down-mid-high { 294 | color: $orange; 295 | } 296 | // <= 100% 297 | .network-down-high { 298 | color: $purple; 299 | } 300 | // > 100% (Above MaxDownloadBytes) 301 | .network-down-over { 302 | color: $red; 303 | } 304 | 305 | .ws-dead { 306 | color: $inactive; 307 | font-size: 14px; 308 | } 309 | .ws-inactive { 310 | color: $darkblue; 311 | font-size: 14px; 312 | } 313 | .ws-visible { 314 | color: $cyan; 315 | font-size: 14px; 316 | } 317 | .ws-current { 318 | color: $yellow; 319 | font-size: 14px; 320 | } 321 | .ws-active { 322 | color: $green; 323 | font-size: 14px; 324 | } 325 | 326 | // Bluetooth Widget 327 | @keyframes connectanim { 328 | from { 329 | background-image: radial-gradient(circle farthest-side at center, $btblue 0%, transparent 0%, transparent 100%) 330 | } 331 | to { 332 | background-image: radial-gradient(circle farthest-side at center, $btblue 0%, $btblue 100%, transparent 100%) 333 | } 334 | } 335 | @keyframes disconnectanim { 336 | from { 337 | background-image: radial-gradient(circle farthest-side at center, transparent 0%, $btblue 0%, $btblue 100%) 338 | } 339 | to { 340 | background-image: radial-gradient(circle farthest-side at center, transparent 0%, transparent 100%, $btblue 100%) 341 | } 342 | } 343 | 344 | @keyframes scanonanim { 345 | from { 346 | color: $yellow; 347 | } 348 | to { 349 | color: $green; 350 | } 351 | } 352 | @keyframes scanoffanim { 353 | from { 354 | color: $green; 355 | } 356 | to { 357 | color: $yellow; 358 | } 359 | } 360 | 361 | .bt-bg { 362 | background-color: $bg; 363 | border-radius: 16px; 364 | } 365 | .bt-header-box { 366 | margin-top: 4px; 367 | margin-right: 8px; 368 | margin-left: 8px; 369 | font-size: 24px; 370 | color: $btblue; 371 | } 372 | .bt-body-box { 373 | margin-right: 8px; 374 | margin-left: 8px; 375 | } 376 | .bt-button { 377 | &.active { 378 | animation-name: connectanim; 379 | animation-duration: 50ms; 380 | animation-timing-function: linear; 381 | animation-fill-mode: forwards; 382 | } 383 | &.inactive { 384 | animation-name: disconnectanim; 385 | animation-duration: 50ms; 386 | animation-timing-function: linear; 387 | animation-fill-mode: forwards; 388 | } 389 | &.failed { 390 | color: $red; 391 | } 392 | border-radius: 16px; 393 | padding-left: 8px; 394 | padding-right: 8px; 395 | padding-top: 4px; 396 | padding-bottom: 4px; 397 | 398 | margin-bottom: 4px; 399 | margin-top: 4px; 400 | 401 | font-size: 16px; 402 | // color: $green; 403 | } 404 | .bt-close { 405 | color: $red; 406 | background-color: $inactive; 407 | border-radius: 16px; 408 | padding: 0px 8px 0px 7px; 409 | margin: 0px 0px 0px 8px; 410 | } 411 | .bt-scan { 412 | &.active { 413 | animation-name: scanonanim; 414 | animation-duration: 50ms; 415 | animation-timing-function: linear; 416 | animation-fill-mode: forwards; 417 | } 418 | &.inactive { 419 | animation-name: scanoffanim; 420 | animation-duration: 50ms; 421 | animation-timing-function: linear; 422 | animation-fill-mode: forwards; 423 | } 424 | color: $yellow; 425 | background-color: $inactive; 426 | border-radius: 16px; 427 | padding: 2px 11px 0px 7px; 428 | margin: 0px 0px 0px 10px; 429 | font-size: 18px; 430 | } 431 | -------------------------------------------------------------------------------- /data/config: -------------------------------------------------------------------------------- 1 | # Example configuration. 2 | # Everything after '#' is ignored 3 | # Format of the variables: 4 | # [variable]: [value] 5 | # Whitespaces are ignored in the following locations: 6 | # - Before the variable 7 | # - After the ':' 8 | # - After the value 9 | # 10 | # String variables can be escaped ([Notation in config] -> "Result"): 11 | # - foo\\bar -> "foobar" 12 | # - foo\nbar -> "foobar" 13 | # - foo\sbar -> "foo bar" 14 | 15 | # If true, gBar ignores *.scss files and only tries to load *.css files. 16 | # This is useful, if you don't want to SCSS, or if you want to use newer SCSS features, 17 | # that libsass (the backend gBar uses) doesn't support. 18 | ForceCSS: false 19 | 20 | # The following three options control the ordering of the widgets. 21 | # Reordering can cause slight margin inconsistencies, 22 | # so it is recommend to only make minor adjustments to the default layout. 23 | # Adding the same widget multiple times to the layout is *not* supported and will cause issues. 24 | 25 | # Widgets to show on the left side 26 | WidgetsLeft: [Workspaces] 27 | # Widgets to center 28 | WidgetsCenter: [Time] 29 | # Widgets to display on the right side 30 | WidgetsRight: [Tray, Packages, Audio, Bluetooth, Network, Disk, VRAM, GPU, RAM, CPU, Battery, Power] 31 | 32 | # The CPU sensor to use 33 | CPUThermalZone: /sys/devices/pci0000:00/0000:00:18.3/hwmon/hwmon2/temp1_input 34 | 35 | # The card to poll when using AMDGPU. If you don't have an AMD card, you can skip this config. 36 | # Possible values can be found by querying /sys/class/drm 37 | DrmAmdCard: card0 38 | 39 | # Relative path to AMD gpu thermal sensor, appended after /sys/class/drm/ 40 | AmdGPUThermalZone: /device/hwmon/hwmon1/temp1_input 41 | 42 | # The command to execute on suspend 43 | SuspendCommand: ~/.config/scripts/sys.sh suspend 44 | 45 | # The command to execute on lock 46 | LockCommand: ~/.config/scripts/sys.sh lock 47 | 48 | # The command to execute on exit 49 | ExitCommand: killall Hyprland 50 | 51 | # The folder, where the battery sensors reside 52 | BatteryFolder: /sys/class/power_supply/BAT1 53 | 54 | # Threshold, when the battery is considered low and a different color (as specified by the 'battery-warning' CSS property) is applied 55 | BatteryWarnThreshold: 20 56 | 57 | # The partition to monitor with disk sensor 58 | DiskPartition: / 59 | 60 | # Overrides the icon of the nth (in this case the first) workspace. 61 | # Please note the missing space between "," and the symbol. Adding a space here adds it to the bar too! 62 | #WorkspaceSymbol: 1, 63 | 64 | # The default symbol for the workspaces 65 | DefaultWorkspaceSymbol:  66 | 67 | # All of the icons that can be modified. 68 | # Please note that some icons require a space ("\s") (e.g. default ShutdownIcon) 69 | ShutdownIcon: \s 70 | RebootIcon: 󰑐 71 | SleepIcon: 󰏤 72 | LockIcon:  73 | ExitIcon: 󰗼 74 | BTOffIcon: 󰂲 75 | BTOnIcon: 󰂯 76 | BTConnectedIcon: 󰂱 77 | DevKeyboardIcon: 󰌌\s 78 | DevMouseIcon: 󰍽\s 79 | DevHeadsetIcon: 󰋋\s 80 | DevControllerIcon: 󰖺\s 81 | DevUnknownIcon: \s 82 | SpeakerMutedIcon: 󰝟 83 | SpeakerHighIcon: 󰕾 84 | MicMutedIcon: 󰍭 85 | MicHighIcon: 󰍬 86 | PackageOutOfDateIcon: 󰏔\s 87 | 88 | # Scroll through the workspaces of the current monitor instead of all workspaces 89 | WorkspaceScrollOnMonitor: true 90 | 91 | # When true: Scroll up -> Next workspace instead of previous workspace. Analogous with scroll down 92 | WorkspaceScrollInvert: false 93 | 94 | # Number of workspaces to display. Displayed workspace IDs are 1-n (Default: 1-9) 95 | NumWorkspaces: 9 96 | 97 | # Hide trailing unused workspaces (by ID). A workspace is considered unused, if it has no window on it 98 | WorkspaceHideUnused: true 99 | 100 | # Use Hyprland IPC instead of the ext_workspace protocol for workspace polling. 101 | # Hyprland IPC is *slightly* less performant (+0.1% one core), but way less bug prone, 102 | # since the protocol is not as feature complete as Hyprland IPC. 103 | # NOTE: Hyprland no longer supports ext-workspace-unstable-v1 as of commit bb09334. 104 | # Hyprland IPC is thus *required* for workspace support under Hyprland >=v0.30.0! 105 | UseHyprlandIPC: true 106 | 107 | # The location of the bar 108 | # Needs to be capitalized!! 109 | # Values are: L (Left), R (Right), T (Top), B (bottom) 110 | Location: T 111 | 112 | # When the location is set to side, this option forces everything (even text) to be right-side up. 113 | # *Always* make sure to enable SensorTooltips when enabling this option. Failure to do so *will* cause graphical issues. 114 | IconsAlwaysUp: false 115 | 116 | # Forces the widgets in the center to be centered. 117 | # This can cause the right widget to clip outside, if there is not enough space on screen (e.g. when opening the text) 118 | # Setting this to false will definitely fix this issue, but it won't look very good, since the widgets will be off-center. 119 | # So try to decrease "CenterSpace" first, before setting this configuration to false. 120 | # Legacy name: CenterTime 121 | CenterWidgets: true 122 | 123 | # How much space should be reserved for the center widgets. Setting this too high can cause the right widgets to clip outside. 124 | # Therefore try to set it as low as possible if you experience clipping. 125 | # Although keep in mind, that a value that is too low can cause the widget to be be off-center, 126 | # which can also cause clipping. 127 | # If you can't find an optimal value, consider setting 'CenterWidgets' to false 128 | # Legacy name: TimeSpace 129 | CenterSpace: 300 130 | 131 | # Set datetime style 132 | DateTimeStyle: %a %D - %H:%M:%S %Z 133 | 134 | # Set datetime locale (defaults to system locale if not set or set to empty string) 135 | #DateTimeLocale: de_DE.utf8 136 | 137 | # How many characters of the title can be displayed. Note that higher values *will* cause styling issues, especially when it is in the center. 138 | # If you have the title in the center, consider also increasing "CenterSpace" 139 | MaxTitleLength: 30 140 | 141 | # Adds a audio input(aka. microphone) widget 142 | AudioInput: false 143 | 144 | # Sets the audio slider to be on reveal (Just like the sensors) when true. Only affects the bar. 145 | AudioRevealer: false 146 | 147 | # Sets the rate of change of the slider on each scroll. In Percent 148 | AudioScrollSpeed: 5 149 | 150 | # Display numbers instead of a slider for the two audio widgets. Doesn't affect the audio flyin 151 | AudioNumbers: false 152 | 153 | # Manually perform the flyin animation for the audio widget. Enabling this can cause some graphical issues (Damage tracking issues after the flyin disappers) on Hyprland. 154 | # So it is recommended to disable this on Hyprland and configure the flyin animation there: 155 | # layerrule = animation slide, gbar-audio 156 | ManualFlyinAnimation: false 157 | 158 | # Command that is run to check if there are out-of-date packages. 159 | # The script should return *ONLY* a number. If it doesn't output a number, updates are no longer checked. 160 | # Default value is applicable for Arch Linux. (See data/update.sh for a human-readable version) 161 | CheckPackagesCommand: p="$(checkupdates)"; e=$?; if [ $e -eq 127 ] ; then exit 127; fi; if [ $e -eq 2 ] ; then echo "0" && exit 0; fi; echo "$p" | wc -l 162 | 163 | 164 | # How often to check for updates. In seconds 165 | CheckUpdateInterval: 300 166 | 167 | # Limits the range of the audio slider. Only works for audio output. 168 | # Slider "empty" is AudioMinVolume, Slider "full" is AudioMaxVolume 169 | # AudioMinVolume: 30 # Audio can't get below 30% 170 | # AudioMaxVolume: 120 # Audio can't get above 120% 171 | 172 | # The network adapter to use. You can query /sys/class/net for all possible values 173 | NetworkAdapter: eno1 174 | 175 | # Disables the network widget when set to false 176 | NetworkWidget: true 177 | 178 | # Use tooltips instead of sliders for the sensors 179 | SensorTooltips: false 180 | 181 | # The size of the of the circular sensors 182 | SensorSize: 24 183 | 184 | # The size of the network icon 185 | NetworkIconSize: 24 186 | 187 | # Enables tray icons 188 | EnableSNI: true 189 | 190 | # SNIIconSize sets the icon size for a SNI icon. 191 | # SNIPaddingTop Can be used to push the Icon down. Negative values are allowed 192 | # SNIIconName overrides what icon from an icon theme to display. 193 | # SNIDisabled prevents an icon from being registered. 194 | # For all SNI properties: The first parameter is a filter of the tooltip(The text that pops up, when the icon is hovered) of the icon. 195 | # The wildcard filter '*' does not work for SNIIconName and SNIDisabled 196 | 197 | # Scale everything down to 25 pixels ('*' as filter means everything) 198 | #SNIIconSize: *, 25 199 | # Explicitly make OBS a bit smaller than default 200 | #SNIIconSize: OBS, 23 201 | # Nudges the Discord icon a bit down 202 | #SNIPaddingTop: Discord, 5 203 | # Override the default icon given to gBar by discord to an icon theme supplied one (Example is from papirus theme) 204 | #SNIIconName: Discord, discord-tray 205 | # Prevents steam from displaying. Note: Steam doesn't have a tooltip, which means the object path is filtered instead. 206 | #SNIDisabled: steam, true 207 | 208 | # These set the range for the network widget. The widget changes colors at six intervals: 209 | # - Below Min...Bytes ("under") 210 | # - Between ]0%;25%]. 0% = Min...Bytes; 100% = Max...Bytes ("low") 211 | # - Between ]25%;50%]. 0% = Min...Bytes; 100% = Max...Bytes ("mid-low") 212 | # - Between ]50%;75%]. 0% = Min...Bytes; 100% = Max...Bytes ("mid-high") 213 | # - Between ]75%;100%]. 0% = Min...Bytes; 100% = Max...Bytes ("high") 214 | # - Above Max...Bytes ("over") 215 | MinDownloadBytes: 0 216 | MaxDownloadBytes: 10485760 # 10 * 1024 * 1024 = 10 MiB 217 | MinUploadBytes: 0 218 | MaxUploadBytes: 5242880 # 5 * 1024 * 1024 = 5 MiB 219 | -------------------------------------------------------------------------------- /src/Window.cpp: -------------------------------------------------------------------------------- 1 | #include "Window.h" 2 | #include "Common.h" 3 | #include "CSS.h" 4 | #include "Wayland.h" 5 | 6 | #include 7 | #include 8 | 9 | Window::Window(int32_t monitor) : m_MonitorName(Wayland::GtkMonitorIDToName(monitor)) {} 10 | Window::Window(const std::string& monitor) : m_MonitorName(monitor) {} 11 | 12 | Window::~Window() 13 | { 14 | if (m_App) 15 | { 16 | g_object_unref(m_App); 17 | m_App = nullptr; 18 | } 19 | } 20 | 21 | void Window::Init(const std::string& overideConfigLocation) 22 | { 23 | m_TargetMonitor = m_MonitorName; 24 | 25 | gtk_init(NULL, NULL); 26 | 27 | // Style 28 | CSS::Load(overideConfigLocation); 29 | 30 | gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), (GtkStyleProvider*)CSS::GetProvider(), GTK_STYLE_PROVIDER_PRIORITY_USER); 31 | 32 | GdkDisplay* defaultDisplay = gdk_display_get_default(); 33 | ASSERT(defaultDisplay != nullptr, "Cannot get display!"); 34 | if (!m_MonitorName.empty()) 35 | { 36 | m_Monitor = gdk_display_get_monitor(defaultDisplay, Wayland::NameToGtkMonitorID(m_MonitorName)); 37 | ASSERT(m_Monitor, "Cannot get monitor \"" << m_MonitorName << "\"!"); 38 | } 39 | else 40 | { 41 | LOG("Window: Requested monitor not found. Falling back to current monitor!") 42 | m_Monitor = gdk_display_get_primary_monitor(defaultDisplay); 43 | } 44 | 45 | // Register monitor added/removed callbacks 46 | auto monAdded = [](GdkDisplay* display, GdkMonitor* mon, void* window) 47 | { 48 | ((Window*)window)->MonitorAdded(display, mon); 49 | }; 50 | g_signal_connect(defaultDisplay, "monitor-added", G_CALLBACK(+monAdded), this); 51 | auto monRemoved = [](GdkDisplay* display, GdkMonitor* mon, void* window) 52 | { 53 | ((Window*)window)->MonitorRemoved(display, mon); 54 | }; 55 | g_signal_connect(defaultDisplay, "monitor-removed", G_CALLBACK(+monRemoved), this); 56 | } 57 | 58 | void Window::Run() 59 | { 60 | Create(); 61 | while (!bShouldQuit) 62 | { 63 | gtk_main_iteration(); 64 | if (bHandleMonitorChanges) 65 | { 66 | // Flush the event loop 67 | while (gtk_events_pending()) 68 | { 69 | if (!gtk_main_iteration()) 70 | break; 71 | } 72 | 73 | LOG("Window: Handling monitor changes"); 74 | bHandleMonitorChanges = false; 75 | 76 | if (m_MonitorName == m_TargetMonitor) 77 | { 78 | // Don't care 79 | continue; 80 | } 81 | // Process Wayland 82 | Wayland::PollEvents(); 83 | 84 | GdkDisplay* display = gdk_display_get_default(); 85 | 86 | // HACK: Discrepancies are mostly caused by the HEADLESS monitor. Assume that Gdk ID 0 is headless 87 | bool bGotHeadless = (size_t)gdk_display_get_n_monitors(display) != Wayland::GetMonitors().size(); 88 | if (bGotHeadless) 89 | { 90 | LOG("Window: Found discrepancy between GDK and Wayland!"); 91 | } 92 | 93 | // Try to find our target monitor 94 | const Wayland::Monitor* targetMonitor = Wayland::FindMonitorByName(m_TargetMonitor); 95 | if (targetMonitor) 96 | { 97 | // Found target monitor, snap back. 98 | if (m_MainWidget) 99 | Destroy(); 100 | m_MonitorName = m_TargetMonitor; 101 | m_Monitor = gdk_display_get_monitor(display, bGotHeadless ? targetMonitor->ID + 1 : targetMonitor->ID); 102 | Create(); 103 | continue; 104 | } 105 | 106 | // We haven't yet created, check if we can. 107 | if (m_MainWidget == nullptr) 108 | { 109 | // Find a non-headless monitor 110 | const Wayland::Monitor* replacementMonitor = Wayland::FindMonitor( 111 | [&](const Wayland::Monitor& mon) 112 | { 113 | return mon.name.find("HEADLESS") == std::string::npos; 114 | }); 115 | if (!replacementMonitor) 116 | continue; 117 | m_MonitorName = replacementMonitor->name; 118 | m_Monitor = gdk_display_get_monitor(display, bGotHeadless ? replacementMonitor->ID + 1 : replacementMonitor->ID); 119 | Create(); 120 | continue; 121 | } 122 | } 123 | } 124 | } 125 | 126 | void Window::Create() 127 | { 128 | LOG("Window: Create on monitor " << m_MonitorName); 129 | m_Window = (GtkWindow*)gtk_window_new(GTK_WINDOW_TOPLEVEL); 130 | gtk_layer_init_for_window(m_Window); 131 | 132 | // Notify our main method, that we want to init 133 | OnWidget(); 134 | 135 | ASSERT(m_MainWidget, "Main Widget not set!"); 136 | 137 | switch (m_Layer) 138 | { 139 | case Layer::Top: gtk_layer_set_layer(m_Window, GTK_LAYER_SHELL_LAYER_TOP); break; 140 | case Layer::Overlay: gtk_layer_set_layer(m_Window, GTK_LAYER_SHELL_LAYER_OVERLAY); break; 141 | } 142 | 143 | gtk_layer_set_namespace(m_Window, m_LayerNamespace.c_str()); 144 | 145 | if (m_Exclusive) 146 | gtk_layer_auto_exclusive_zone_enable(m_Window); 147 | 148 | gtk_layer_set_monitor(m_Window, m_Monitor); 149 | 150 | if (FLAG_CHECK(m_Anchor, Anchor::Left)) 151 | { 152 | gtk_layer_set_anchor(m_Window, GTK_LAYER_SHELL_EDGE_LEFT, true); 153 | } 154 | if (FLAG_CHECK(m_Anchor, Anchor::Right)) 155 | { 156 | gtk_layer_set_anchor(m_Window, GTK_LAYER_SHELL_EDGE_RIGHT, true); 157 | } 158 | if (FLAG_CHECK(m_Anchor, Anchor::Top)) 159 | { 160 | gtk_layer_set_anchor(m_Window, GTK_LAYER_SHELL_EDGE_TOP, true); 161 | } 162 | if (FLAG_CHECK(m_Anchor, Anchor::Bottom)) 163 | { 164 | gtk_layer_set_anchor(m_Window, GTK_LAYER_SHELL_EDGE_BOTTOM, true); 165 | } 166 | UpdateMargin(); 167 | 168 | // Create widgets 169 | Widget::CreateAndAddWidget(m_MainWidget.get(), (GtkWidget*)m_Window); 170 | 171 | gtk_widget_show_all((GtkWidget*)m_Window); 172 | } 173 | 174 | void Window::Destroy() 175 | { 176 | LOG("Window: Destroy"); 177 | m_MainWidget = nullptr; 178 | gtk_widget_destroy((GtkWidget*)m_Window); 179 | } 180 | 181 | void Window::Close() 182 | { 183 | Destroy(); 184 | bShouldQuit = true; 185 | } 186 | 187 | void Window::UpdateMargin() 188 | { 189 | for (auto [anchor, margin] : m_Margin) 190 | { 191 | if (FLAG_CHECK(anchor, Anchor::Left)) 192 | { 193 | gtk_layer_set_margin(m_Window, GTK_LAYER_SHELL_EDGE_LEFT, margin); 194 | } 195 | if (FLAG_CHECK(anchor, Anchor::Right)) 196 | { 197 | gtk_layer_set_margin(m_Window, GTK_LAYER_SHELL_EDGE_RIGHT, margin); 198 | } 199 | if (FLAG_CHECK(anchor, Anchor::Top)) 200 | { 201 | gtk_layer_set_margin(m_Window, GTK_LAYER_SHELL_EDGE_TOP, margin); 202 | } 203 | if (FLAG_CHECK(anchor, Anchor::Bottom)) 204 | { 205 | gtk_layer_set_margin(m_Window, GTK_LAYER_SHELL_EDGE_BOTTOM, margin); 206 | } 207 | } 208 | } 209 | 210 | void Window::SetMainWidget(std::unique_ptr&& mainWidget) 211 | { 212 | m_MainWidget = std::move(mainWidget); 213 | } 214 | 215 | void Window::SetMargin(Anchor anchor, int32_t margin) 216 | { 217 | if (FLAG_CHECK(anchor, Anchor::Left)) 218 | { 219 | m_Margin[0] = {Anchor::Left, margin}; 220 | } 221 | if (FLAG_CHECK(anchor, Anchor::Right)) 222 | { 223 | m_Margin[1] = {Anchor::Right, margin}; 224 | } 225 | if (FLAG_CHECK(anchor, Anchor::Top)) 226 | { 227 | m_Margin[2] = {Anchor::Top, margin}; 228 | } 229 | if (FLAG_CHECK(anchor, Anchor::Bottom)) 230 | { 231 | m_Margin[2] = {Anchor::Bottom, margin}; 232 | } 233 | 234 | if (m_Window) 235 | { 236 | UpdateMargin(); 237 | } 238 | } 239 | 240 | int Window::GetWidth() const 241 | { 242 | // gdk_monitor_get_geometry is really unreliable for some reason. 243 | // Use our wayland backend instead 244 | 245 | /*GdkRectangle rect{}; 246 | gdk_monitor_get_geometry(m_Monitor, &rect); 247 | return rect.width;*/ 248 | 249 | const Wayland::Monitor* mon = Wayland::FindMonitorByName(m_MonitorName); 250 | ASSERT(mon, "Window: Couldn't find monitor"); 251 | 252 | int viewedWidth = mon->width; 253 | 254 | // A 90/270 rotation should use the height for the viewed width 255 | switch (mon->rotation) 256 | { 257 | case 90: 258 | case 270: viewedWidth = mon->height; break; 259 | } 260 | return viewedWidth / mon->scale; 261 | } 262 | 263 | int Window::GetHeight() const 264 | { 265 | /*GdkRectangle rect{}; 266 | gdk_monitor_get_geometry(m_Monitor, &rect); 267 | return rect.height;*/ 268 | 269 | const Wayland::Monitor* mon = Wayland::FindMonitorByName(m_MonitorName); 270 | ASSERT(mon, "Window: Couldn't find monitor"); 271 | 272 | int viewedHeight = mon->height; 273 | 274 | // A 90/270 rotation should use the width for the viewed height 275 | switch (mon->rotation) 276 | { 277 | case 90: 278 | case 270: viewedHeight = mon->width; break; 279 | } 280 | return viewedHeight / mon->scale; 281 | } 282 | 283 | void Window::MonitorAdded(GdkDisplay*, GdkMonitor*) 284 | { 285 | bHandleMonitorChanges = true; 286 | } 287 | 288 | void Window::MonitorRemoved(GdkDisplay*, GdkMonitor* mon) 289 | { 290 | bHandleMonitorChanges = true; 291 | // Immediately react 292 | if (mon == m_Monitor) 293 | { 294 | LOG("Window: Current monitor removed!") 295 | m_Monitor = nullptr; 296 | m_MonitorName = ""; 297 | Destroy(); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/PulseAudio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "System.h" 3 | #include "Common.h" 4 | #include "Config.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace PulseAudio 13 | { 14 | 15 | static pa_mainloop* mainLoop; 16 | static pa_context* context; 17 | static std::list pendingOperations; 18 | 19 | static System::AudioInfo info; 20 | static bool queueUpdate = false; 21 | static bool blockUpdate = false; 22 | 23 | inline void FlushLoop() 24 | { 25 | while (pendingOperations.size() > 0) 26 | { 27 | pa_mainloop_iterate(mainLoop, 0, nullptr); 28 | // Remove done or cancelled operations 29 | pendingOperations.remove_if( 30 | [](pa_operation* op) 31 | { 32 | pa_operation_state_t state = pa_operation_get_state(op); 33 | if (state == PA_OPERATION_DONE) 34 | { 35 | pa_operation_unref(op); 36 | return true; 37 | } 38 | else if (state == PA_OPERATION_CANCELLED) 39 | { 40 | LOG("Warning: Cancelled pa_operation!"); 41 | pa_operation_unref(op); 42 | return true; 43 | } 44 | return false; 45 | }); 46 | } 47 | } 48 | 49 | inline double PAVolumeToDouble(const pa_cvolume* volume) 50 | { 51 | double vol = (double)pa_cvolume_avg(volume) / (double)PA_VOLUME_NORM; 52 | // Just round to 1% precision, should be enough 53 | constexpr double precision = 0.01; 54 | double volRounded = std::round(vol * 1 / precision) * precision; 55 | return volRounded; 56 | } 57 | 58 | inline double PAVolumeToDoubleWithMinMax(const pa_cvolume* volume) 59 | { 60 | double volRoundend = PAVolumeToDouble(volume); 61 | // Clamp it to min/max 62 | double minVolume = Config::Get().audioMinVolume / 100.; 63 | double maxVolume = Config::Get().audioMaxVolume / 100.; 64 | volRoundend = std::clamp(volRoundend, minVolume, maxVolume); 65 | // Remap min/max to 0/1 66 | double volRemapped = (volRoundend - minVolume) / (maxVolume - minVolume); 67 | return volRemapped; 68 | } 69 | 70 | inline double DoubleToVolumeWithMinMax(double value) 71 | { 72 | // Clamp to 0/1 73 | value = std::clamp(value, 0., 1.); 74 | // Remap 0/1 to min/max 75 | double minVolume = Config::Get().audioMinVolume / 100.; 76 | double maxVolume = Config::Get().audioMaxVolume / 100.; 77 | double volRemapped = value * (maxVolume - minVolume) + minVolume; 78 | return volRemapped; 79 | } 80 | 81 | inline void UpdateInfo() 82 | { 83 | LOG("PulseAudio: Update info"); 84 | struct ServerInfo 85 | { 86 | const char* defaultSink = nullptr; 87 | const char* defaultSource = nullptr; 88 | } serverInfo; 89 | 90 | // 1. Get default sink 91 | auto getServerInfo = [](pa_context*, const pa_server_info* paInfo, void* out) 92 | { 93 | if (!paInfo) 94 | return; 95 | 96 | ServerInfo* serverInfo = (ServerInfo*)out; 97 | serverInfo->defaultSink = paInfo->default_sink_name; 98 | serverInfo->defaultSource = paInfo->default_source_name; 99 | 100 | auto sinkInfo = [](pa_context*, const pa_sink_info* paInfo, int, void* audioInfo) 101 | { 102 | if (!paInfo) 103 | return; 104 | 105 | System::AudioInfo* out = (System::AudioInfo*)audioInfo; 106 | 107 | double vol = PAVolumeToDoubleWithMinMax(&paInfo->volume); 108 | out->sinkVolume = vol; 109 | out->sinkMuted = paInfo->mute; 110 | }; 111 | if (serverInfo->defaultSink) 112 | { 113 | pa_operation* op = pa_context_get_sink_info_by_name(context, serverInfo->defaultSink, +sinkInfo, &info); 114 | pa_operation_ref(op); 115 | pendingOperations.push_back(op); 116 | } 117 | 118 | auto sourceInfo = [](pa_context*, const pa_source_info* paInfo, int, void* audioInfo) 119 | { 120 | if (!paInfo) 121 | return; 122 | 123 | System::AudioInfo* out = (System::AudioInfo*)audioInfo; 124 | 125 | double vol = PAVolumeToDouble(&paInfo->volume); 126 | out->sourceVolume = vol; 127 | out->sourceMuted = paInfo->mute; 128 | }; 129 | if (serverInfo->defaultSource) 130 | { 131 | pa_operation* op = pa_context_get_source_info_by_name(context, serverInfo->defaultSource, +sourceInfo, &info); 132 | pa_operation_ref(op); 133 | pendingOperations.push_back(op); 134 | } 135 | }; 136 | 137 | pa_operation* op = pa_context_get_server_info(context, +getServerInfo, &serverInfo); 138 | pa_operation_ref(op); 139 | pendingOperations.push_back(op); 140 | FlushLoop(); 141 | } 142 | 143 | inline System::AudioInfo GetInfo() 144 | { 145 | pa_mainloop_iterate(mainLoop, 0, nullptr); 146 | if (queueUpdate && !blockUpdate) 147 | { 148 | UpdateInfo(); 149 | } 150 | queueUpdate = false; 151 | blockUpdate = false; 152 | return info; 153 | } 154 | 155 | inline void Init() 156 | { 157 | mainLoop = pa_mainloop_new(); 158 | pa_mainloop_api* api = pa_mainloop_get_api(mainLoop); 159 | 160 | context = pa_context_new(api, "gBar PA context"); 161 | int res = pa_context_connect(context, nullptr, PA_CONTEXT_NOAUTOSPAWN, nullptr); 162 | 163 | bool ready = false; 164 | auto stateCallback = [](pa_context* c, void* ready) 165 | { 166 | switch (pa_context_get_state(c)) 167 | { 168 | case PA_CONTEXT_TERMINATED: 169 | case PA_CONTEXT_FAILED: 170 | case PA_CONTEXT_UNCONNECTED: ASSERT(false, "PA Callback error!"); break; 171 | case PA_CONTEXT_AUTHORIZING: 172 | case PA_CONTEXT_SETTING_NAME: 173 | case PA_CONTEXT_CONNECTING: 174 | // Don't care 175 | break; 176 | case PA_CONTEXT_READY: 177 | LOG("PulseAudio: Context is ready!"); 178 | *(bool*)ready = true; 179 | break; 180 | } 181 | }; 182 | pa_context_set_state_callback(context, +stateCallback, &ready); 183 | 184 | while (!ready) 185 | { 186 | pa_mainloop_iterate(mainLoop, 0, nullptr); 187 | } 188 | 189 | // Subscribe to source and sink changes 190 | auto subscribeSuccess = [](pa_context*, int success, void*) 191 | { 192 | ASSERT(success >= 0, "Failed to subscribe to pulseaudio"); 193 | }; 194 | pa_operation* op = pa_context_subscribe(context, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE), 195 | +subscribeSuccess, nullptr); 196 | pa_operation_ref(op); 197 | pendingOperations.push_back(op); 198 | FlushLoop(); 199 | 200 | auto subscribeCallback = [](pa_context*, pa_subscription_event_type_t type, uint32_t, void*) 201 | { 202 | if (type & PA_SUBSCRIPTION_EVENT_CHANGE) 203 | queueUpdate = true; 204 | }; 205 | pa_context_set_subscribe_callback(context, +subscribeCallback, nullptr); 206 | 207 | // Initialise info 208 | UpdateInfo(); 209 | 210 | ASSERT(res >= 0, "pa_context_connect failed!"); 211 | } 212 | 213 | inline void SetVolumeSink(double value) 214 | { 215 | double valClamped = DoubleToVolumeWithMinMax(value); 216 | // I'm too lazy to implement the c api for this. Since it will only be called when needed and doesn't pipe, it shouldn't be a problem to 217 | // fallback for a command 218 | LOG("Audio: Set volume of sink: " << valClamped); 219 | std::string cmd = "pamixer --allow-boost --set-volume " + std::to_string((uint32_t)(valClamped * 100)); 220 | info.sinkVolume = std::clamp(value, 0., 1.); // We need to stay in 0/1 range 221 | blockUpdate = true; 222 | system(cmd.c_str()); 223 | } 224 | 225 | inline void SetVolumeSource(double value) 226 | { 227 | double valClamped = std::clamp(value, 0., 1.); 228 | // I'm too lazy to implement the c api for this. Since it will only be called when needed and doesn't pipe, it shouldn't be a problem to 229 | // fallback for a command 230 | LOG("Audio: Set volume of source: " << valClamped); 231 | std::string cmd = "pamixer --default-source --set-volume " + std::to_string((uint32_t)(valClamped * 100)); 232 | info.sourceVolume = valClamped; 233 | blockUpdate = true; 234 | system(cmd.c_str()); 235 | } 236 | 237 | inline void SetMutedSink(bool muted) 238 | { 239 | if (muted) 240 | { 241 | LOG("Audio: Mute sink"); 242 | system("pamixer --mute"); 243 | } 244 | else 245 | { 246 | LOG("Audio: Unmute sink"); 247 | system("pamixer --unmute"); 248 | } 249 | } 250 | 251 | inline void SetMutedSource(bool muted) 252 | { 253 | if (muted) 254 | { 255 | LOG("Audio: Mute source"); 256 | system("pamixer --default-source --mute"); 257 | } 258 | else 259 | { 260 | LOG("Audio: Unmute source"); 261 | system("pamixer --default-source --unmute"); 262 | } 263 | } 264 | 265 | inline void Shutdown() 266 | { 267 | pa_mainloop_free(mainLoop); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gBar 2 | My personal blazingly fast and efficient status bar + widgets, in case anyone finds a use for it. 3 | 4 | *gBar: **G**TK **Bar*** 5 | 6 | ## Prerequisites 7 | *If you don't have the optional dependencies, some features are not available.* 8 | - wayland 9 | - Hyprland(Optional -> For workspaces widget) 10 | - nvidia-utils(Optional -> For Nvidia GPU status) 11 | - bluez(Optional -> For Bluetooth status) 12 | - GTK 3.0 13 | - gtk-layer-shell 14 | - PulseAudio server (PipeWire works too!) 15 | - pamixer 16 | - libsass 17 | - meson, gcc/clang, ninja 18 | 19 | ## Building and installation (Manually) 20 | 1. Clone gBar 21 | ``` 22 | git clone https://github.com/scorpion-26/gBar 23 | ``` 24 | 2. Configure with meson 25 | 26 | *All optional dependencies enabled* 27 | ``` 28 | meson setup build 29 | ``` 30 | 3. Build and install 31 | ``` 32 | ninja -C build && sudo ninja -C build install 33 | ``` 34 | 35 | ## Building and installation (AUR) 36 | For Arch systems, gBar can be found on the AUR. 37 | You can install it e.g.: with yay 38 | ```yay -S gbar-git``` 39 | 40 | ## Building and installation (Nix) 41 | If you choose the Nix/NixOS installation there are a couple of ways to do it but they all require you to have flakes enabled. 42 | - Building it seperately and just running the binary, run nix build in the directory and use the binary from ./result/bin 43 | - Import the flake to inputs and then add `gBar.defaultPackage.x86_64-linux` to either environment.systemPackages or home.packages. 44 | - Use the home manager module. This is done by, as in the previous way, importing the flake and then adding `gBar.homeManagerModules.x86_64-linux.default` into your home-manager imorts section. This exposes the option programs.gBar to home-manager, use it like below. 45 | ```nix 46 | # Inputs section 47 | inputs.gBar.url = "github:scorpion-26/gBar"; 48 | ... 49 | # Inside home config 50 | home-manager.users.user = { 51 | ... 52 | imports = [ inputs.gBar.homeManagerModules.x86_64-linux.default ]; 53 | ... 54 | programs.gBar = { 55 | enable = true; 56 | config = { 57 | Location = "L"; 58 | EnableSNI = true; 59 | SNIIconSize = { 60 | Discord = 26; 61 | OBS = 23; 62 | }; 63 | WorkspaceSymbols = [ " " " " ]; 64 | }; 65 | }; 66 | }; 67 | ``` 68 | 69 | ## Running gBar 70 | *Open bar on monitor "DP-1"* 71 | ``` 72 | gBar bar DP-1 73 | ``` 74 | *Open bar on monitor 0 (Legacy way of specifying the monitor)* 75 | ``` 76 | gBar bar 0 77 | ``` 78 | *Open audio flyin (either on current monitor or on the specified monitor)* 79 | ``` 80 | gBar audio [monitor] 81 | ``` 82 | *Open microphone flyin, this is equivalent to the audio flyin* 83 | ``` 84 | gBar mic [monitor] 85 | ``` 86 | *Open bluetooth widget* 87 | ``` 88 | gBar bluetooth [monitor] 89 | ``` 90 | 91 | ## Gallery 92 | ![The bar with default css](/assets/bar.png) 93 | 94 | *Bar with default css* 95 | 96 | ![The audio flyin with default css](/assets/audioflyin.png) 97 | 98 | *Audio widget with default css* 99 | 100 | ![The bluetooth widget with default css](/assets/bt.png) 101 | 102 | *Bluetooth widget with default css* 103 | 104 | ## Features / Widgets 105 | Bar: 106 | - Workspaces (Hyprland only. Technically works on all compositors implementing ext_workspace when ```UseHyprlandIPC``` is false, though workspace control relies on Hyprland) 107 | - Time 108 | - Title of the focused Window 109 | - Bluetooth (BlueZ only) 110 | - Audio control 111 | - Microphone control 112 | - Power control 113 | - Shutdown 114 | - Restart 115 | - Suspend 116 | - Lock (Requires manual setup, see FAQ) 117 | - Exit/Logout (Hyprland only) 118 | - Battery: Capacity 119 | - CPU stats: Utilisation, temperature (Temperature requires manual setup, see FAQ) 120 | - RAM: Utilisation 121 | - GPU stats (Nvidia/AMD only): Utilisation, temperature, VRAM 122 | - Disk: Free/Total 123 | - Network: Current upload and download speed 124 | - Update checking (Non-Arch systems need to be configured manually) 125 | - Tray icons 126 | 127 | Bluetooth: 128 | - Scanning of nearby bluetooth devices 129 | - Pairing and connecting 130 | 131 | Audio Flyin: 132 | - Audio control 133 | - Microphone control 134 | 135 | ## Configuration for your system 136 | Copy the example config (found under data/config) into ~/.config/gBar/config and modify it to your needs. 137 | 138 | ## Plugins 139 | gBar utilizes a plugin system for custom widgets anyone can create without modifying the source code. 140 | Plugins are native shared-libraries, which need to be placed inside ```~/.local/lib/gBar```, ```/usr/lib/gBar``` or ```/usr/local/lib/gBar```. 141 | Inside example/ there is an example plugin setup. To build and run it, run the following commands inside the example directory: 142 | 143 | ``` 144 | meson setup build -Dprefix=~/.local 145 | ``` 146 | for the local user 147 | OR 148 | ``` 149 | meson setup build 150 | ``` 151 | for all users 152 | 153 | ``` 154 | ninja -C build install 155 | gBar gBarHelloWorld 156 | ``` 157 | The second argument is the name of the shared library (without 'lib' and '.so'). 158 | 159 | For more examples on how to use the gBar API, you can have a look at the built-in widgets (AudioFlyin.cpp, BluetoothDevices.cpp, Bar.cpp) as they use the same API. 160 | 161 | ## FAQ 162 | ### There are already many GTK bars out there, why not use them? 163 | - Waybar: 164 | Great performance, though limited styling(Almost no dynamic sliders, revealers, ...) and (at least for me) buggy css. 165 | - eww: 166 | Really solid project with many great customization options. There is one problem though: Performance.\ 167 | Due to the way eww configuration is set up, for each dynamic variable (the number of them quickly grows) you need a shell command which opens a process. 168 | This became quickly a bottleneck, where the bar took up 10% of the CPU-time due to the creation of many processes all the time (without even considering the workspace widget). 169 | gBar implements all of the information gathering(CPU, RAM, GPU, Disk, ...) in native C++ code, which is WAY faster. In fact, gBar was meant to be a fast replacement/alternative for eww for me. 170 | 171 | And lastly: Implementing it myself is fun and a great excuse to learn something new! 172 | 173 | ### Can you implement feature XYZ? / I've found a bug. Can you fix it? 174 | This project is meant to be for my personal use, though I want it to be easily used by others without bugs or a complicated setup. This means the following: 175 | - If you found a bug, please [open an issue](https://github.com/scorpion-26/gBar/issues/new/choose) and I'll try to fix it as quickly as I can. 176 | - If you're missing a particular feature, please [open an issue](https://github.com/scorpion-26/gBar/issues/new/choose) as well and I'll see what I can do, although I can't guarantee anything. Small requests or features I'll find useful too will probably be implemented in a timely fashion though. 177 | 178 | 179 | ### What scheme are you using? 180 | The colors are from the Dracula theme: https://draculatheme.com 181 | 182 | ### I want to customize the colors 183 | First, find where the data is located for gBar. Possible locations: 184 | - In a 'gBar' directory found in any of the directories listed by `echo $XDG_DATA_DIRS` 185 | - /usr/share/gBar 186 | - /usr/local/share/gBar 187 | - ~/.local/share/gBar 188 | - If you cloned this repository locally: Inside css/ 189 | 190 | Copy the scss and css files from within the data direction into ~/.config/gBar. e.g.: 191 | ``` 192 | mkdir ~/.config/gBar/ 193 | cp /usr/local/share/gBar/* ~/.config/gBar/ 194 | ``` 195 | This will override the default behaviour. If you have sass installed, you can modify the scss file and then regenerate the css file accordingly. Else modify the css file directly. 196 | 197 | ### The margins of the bar are inconsistent/messed up / Custom CSS broke. 198 | If you have a custom style.[s]css, make sure that the margins/names/... are the same as the ones found in ```style/style.[s]css```.\ 199 | If you've checked the css against upstream gBar and the issue persists, please [open an issue](https://github.com/scorpion-26/gBar/issues/new/choose).\ 200 | Major (breaking) changes to the css: 201 | - [f78758c](https://github.com/scorpion-26/gBar/commit/f78758c4eedb022ae49fbecf2f2505f9672d0b9d): Margins are no longer used in the default css. If you didn't play around with margins, you can safely remove them from your css.\ 202 | - [56c53c4](https://github.com/scorpion-26/gBar/commit/56c53c49cdbd7fac11726a5b7ab12f1e6490a211): The lock icon now has its own selector, causing wrong styling when using an outdated css. This can be fixed by including the default ```.lock-button``` section into your css. 203 | 204 | ### The Audio/Bluetooth widget doesn't open 205 | Delete ```/tmp/gBar__audio```/```/tmp/gBar__bluetooth```. 206 | This happens, when you kill the widget before it closes properly (Automatically after a few seconds for the audio widget, or the close button for the bluetooth widget). Ctrl-C in the terminal (SIGINT) is fine though. 207 | 208 | ### CPU Temperature is wrong / Lock doesn't work / Exiting WM does not work 209 | See *Configuration for your system* 210 | 211 | ### The icons are not showing! 212 | Please install a Nerd Font from https://www.nerdfonts.com (I use Caskaydia Cove NF), and change style.css/style.scss accordingly (Refer to 'I want to customize the colors' for that). You _will_ need a Nerd Font with version 2.3.0 or newer (For more details see [this comment](https://github.com/scorpion-26/gBar/issues/5#issuecomment-1442037005)) 213 | 214 | ### The tray doesn't show 215 | Some apps sometimes don't actively query for tray applications. A fix for this is to start gBar before the tray app 216 | If it still doesn't show, please open an issue with your application. 217 | The tray icons are confirmed to work with Discord, Telegram, OBS, Steam and KeePassXC 218 | 219 | ### Clicking on the tray opens a glitchy transparent menu 220 | ~This is semi-intentional and a known bug (See https://github.com/scorpion-26/gBar/pull/12#issuecomment-1529143790 for an explanation). You can make it opaque by setting the background-color property of .popup in style.css/style.scss~\ 221 | As of [bc0281c](https://github.com/scorpion-26/gBar/commit/bc0281ca5321cb6e72ab6d295c790ae10d7eec7e) this is now fixed! For things to look properly you may want to update your css (Specifically the selectors ```.popup``` and ```menu```) 222 | -------------------------------------------------------------------------------- /protocols/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/Widget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Config.h" 3 | #include "Log.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | enum class Alignment 12 | { 13 | Fill, 14 | Center, 15 | Left, 16 | Right, 17 | }; 18 | 19 | struct Transform 20 | { 21 | int size = -1; 22 | bool expand = true; 23 | Alignment alignment = Alignment::Fill; 24 | // Left/Top 25 | int marginBefore = 0; 26 | // Right/Bottom 27 | int marginAfter = 0; 28 | }; 29 | 30 | enum class Orientation 31 | { 32 | Vertical, 33 | Horizontal 34 | }; 35 | 36 | struct Spacing 37 | { 38 | uint32_t free = 0; 39 | bool evenly = false; 40 | }; 41 | 42 | enum class TransitionType 43 | { 44 | Fade, 45 | SlideLeft, 46 | SlideRight, 47 | SlideUp, 48 | SlideDown 49 | }; 50 | 51 | struct Transition 52 | { 53 | TransitionType type; 54 | uint32_t durationMS; 55 | }; 56 | 57 | struct Quad 58 | { 59 | double x, y, size; 60 | }; 61 | 62 | struct SensorStyle 63 | { 64 | double start = -90; // 0 = leftmost; -90 = topmost 65 | double strokeWidth = 4; 66 | }; 67 | 68 | struct Range 69 | { 70 | double min, max; 71 | }; 72 | 73 | struct SliderRange 74 | { 75 | double min, max; 76 | double step; 77 | }; 78 | 79 | enum class TimerDispatchBehaviour 80 | { 81 | ImmediateDispatch, // Call immediately after adding the timeout, then every x ms 82 | LateDispatch // Call for the first time after x ms. 83 | }; 84 | 85 | enum class TimerResult 86 | { 87 | Ok, 88 | Delete 89 | }; 90 | 91 | enum class ScrollDirection 92 | { 93 | Up, 94 | Down 95 | }; 96 | 97 | template 98 | using Callback = std::function; 99 | template 100 | using TimerCallback = std::function; 101 | 102 | class Widget 103 | { 104 | public: 105 | Widget() = default; 106 | virtual ~Widget(); 107 | 108 | template 109 | static std::unique_ptr Create() 110 | { 111 | return std::make_unique(); 112 | } 113 | 114 | static void CreateAndAddWidget(Widget* widget, GtkWidget* parentWidget); 115 | 116 | void SetClass(const std::string& cssClass); 117 | void AddClass(const std::string& cssClass); 118 | void RemoveClass(const std::string& cssClass); 119 | void SetVerticalTransform(const Transform& transform); 120 | void SetHorizontalTransform(const Transform& transform); 121 | void SetTooltip(const std::string& tooltip); 122 | 123 | virtual void Create() = 0; 124 | 125 | void AddChild(std::unique_ptr&& widget); 126 | void RemoveChild(size_t idx); 127 | void RemoveChild(Widget* widget); 128 | 129 | std::vector>& GetWidgets() { return m_Childs; } 130 | 131 | template 132 | void AddTimer(TimerCallback&& callback, uint32_t timeoutMS, TimerDispatchBehaviour dispatch = TimerDispatchBehaviour::ImmediateDispatch) 133 | { 134 | struct TimerPayload 135 | { 136 | TimerCallback timeoutFn; 137 | Widget* thisWidget; 138 | guint id; 139 | }; 140 | TimerPayload* payload = new TimerPayload(); 141 | payload->thisWidget = this; 142 | payload->timeoutFn = std::move(callback); 143 | auto fn = [](void* data) -> int 144 | { 145 | TimerPayload* payload = (TimerPayload*)data; 146 | TimerResult result = payload->timeoutFn(*(TWidget*)payload->thisWidget); 147 | if (result == TimerResult::Delete) 148 | { 149 | payload->thisWidget->m_Timeouts.erase(payload->id); 150 | delete payload; 151 | return false; 152 | } 153 | return true; 154 | }; 155 | if (dispatch == TimerDispatchBehaviour::ImmediateDispatch) 156 | { 157 | if (fn(payload) == false) 158 | { 159 | return; 160 | } 161 | } 162 | payload->id = g_timeout_add(timeoutMS, +fn, payload); 163 | m_Timeouts.insert(payload->id); 164 | } 165 | 166 | GtkWidget* Get() { return m_Widget; }; 167 | const std::vector>& GetChilds() const { return m_Childs; }; 168 | 169 | void SetVisible(bool visible); 170 | 171 | void SetOnCreate(Callback&& onCreate) { m_OnCreate = onCreate; } 172 | 173 | protected: 174 | void PropagateToParent(GdkEvent* event); 175 | void ApplyPropertiesToWidget(); 176 | 177 | GtkWidget* m_Widget = nullptr; 178 | 179 | std::vector> m_Childs; 180 | 181 | std::string m_CssClass; 182 | std::unordered_set m_AdditionalClasses; 183 | std::string m_Tooltip; 184 | Transform m_HorizontalTransform; // X 185 | Transform m_VerticalTransform; // Y 186 | 187 | Callback m_OnCreate; 188 | 189 | std::unordered_set m_Timeouts; 190 | }; 191 | 192 | class Box : public Widget 193 | { 194 | public: 195 | Box() = default; 196 | virtual ~Box() = default; 197 | 198 | void SetOrientation(Orientation orientation); 199 | void SetSpacing(Spacing spacing); 200 | 201 | virtual void Create() override; 202 | 203 | private: 204 | Orientation m_Orientation = Orientation::Horizontal; 205 | Spacing m_Spacing; 206 | }; 207 | 208 | class CenterBox : public Widget 209 | { 210 | public: 211 | void SetOrientation(Orientation orientation); 212 | 213 | virtual void Create() override; 214 | 215 | private: 216 | Orientation m_Orientation = Orientation::Horizontal; 217 | }; 218 | 219 | class EventBox : public Widget 220 | { 221 | public: 222 | void SetHoverFn(std::function&& fn); 223 | void SetScrollFn(std::function&& fn); 224 | virtual void Create() override; 225 | 226 | private: 227 | // If two hover events are sent, it needs also two close events for a close. 228 | // Somehow not doing that causes an issue. 229 | int32_t m_DiffHoverEvents = 0; 230 | std::function m_HoverFn; 231 | std::function m_ScrollFn; 232 | }; 233 | 234 | class CairoArea : public Widget 235 | { 236 | public: 237 | virtual void Create() override; 238 | 239 | protected: 240 | virtual void Draw(cairo_t* cr) = 0; 241 | 242 | Quad GetQuad(); 243 | }; 244 | 245 | class Sensor : public CairoArea 246 | { 247 | public: 248 | // Goes from 0-1 249 | void SetValue(double val); 250 | void SetStyle(SensorStyle style); 251 | 252 | private: 253 | void Draw(cairo_t* cr) override; 254 | 255 | double m_Val; 256 | SensorStyle m_Style{}; 257 | }; 258 | 259 | class NetworkSensor : public CairoArea 260 | { 261 | public: 262 | virtual void Create() override; 263 | 264 | void SetLimitUp(Range limit) { limitUp = limit; }; 265 | void SetLimitDown(Range limit) { limitDown = limit; }; 266 | 267 | void SetUp(double val); 268 | void SetDown(double val); 269 | 270 | void SetAngle(double angle) { m_Angle = angle; }; 271 | 272 | private: 273 | void Draw(cairo_t* cr) override; 274 | 275 | // These are in percent 276 | double up, down; 277 | 278 | Range limitUp; 279 | Range limitDown; 280 | 281 | double m_Angle; 282 | 283 | // What I do here is a little bit gross, but I need a working style context 284 | // Just manually creating a style context doesn't work for me. 285 | std::unique_ptr contextUp; 286 | std::unique_ptr contextDown; 287 | }; 288 | 289 | class Texture : public CairoArea 290 | { 291 | public: 292 | Texture() = default; 293 | virtual ~Texture(); 294 | 295 | // Non-Owning (Copies the pixbuf), ARGB32 296 | void SetBuf(GdkPixbuf* pixbuf, size_t width, size_t height); 297 | 298 | void ForceHeight(size_t height) { m_ForcedHeight = height; }; 299 | void AddPaddingTop(int32_t topPadding) { m_Padding = topPadding; }; 300 | void SetAngle(double angle) { m_Angle = angle; } 301 | 302 | private: 303 | void Draw(cairo_t* cr) override; 304 | 305 | size_t m_Width; 306 | size_t m_Height; 307 | size_t m_ForcedHeight = 0; 308 | double m_Angle; 309 | int32_t m_Padding = 0; 310 | GdkPixbuf* m_Pixbuf; 311 | }; 312 | 313 | class Revealer : public Widget 314 | { 315 | public: 316 | void SetTransition(Transition transition); 317 | 318 | void SetRevealed(bool revealed); 319 | 320 | virtual void Create() override; 321 | 322 | private: 323 | Transition m_Transition; 324 | }; 325 | 326 | class Text : public Widget 327 | { 328 | public: 329 | Text() = default; 330 | virtual ~Text() = default; 331 | 332 | void SetText(const std::string& text); 333 | void SetAngle(double angle); 334 | 335 | virtual void Create() override; 336 | 337 | private: 338 | std::string m_Text; 339 | double m_Angle; 340 | }; 341 | 342 | class Button : public Widget 343 | { 344 | public: 345 | Button() = default; 346 | virtual ~Button() = default; 347 | 348 | void SetText(const std::string& text); 349 | void SetAngle(double angle); 350 | 351 | virtual void Create() override; 352 | 353 | void OnClick(Callback