Compare commits

..

9 Commits

78 changed files with 3191 additions and 1213 deletions

34
.clang-tidy Normal file
View File

@@ -0,0 +1,34 @@
Checks: 'clang-analyzer-*,cppcoreguidelines-*,modernize-*,-modernize-use-trailing-return-type'
WarningsAsErrors: true
HeaderFilterRegex: ''
FormatStyle: google
CheckOptions:
- key: cert-dcl16-c.NewSuffixes
value: 'L;LL;LU;LLU'
- key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField
value: '0'
- key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors
value: '1'
- key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
value: '1'
- key: google-readability-braces-around-statements.ShortStatementLines
value: '1'
- key: google-readability-function-size.StatementThreshold
value: '800'
- key: google-readability-namespace-comments.ShortNamespaceLines
value: '10'
- key: google-readability-namespace-comments.SpacesBeforeComments
value: '2'
- key: modernize-loop-convert.MaxCopySize
value: '16'
- key: modernize-loop-convert.MinConfidence
value: reasonable
- key: modernize-loop-convert.NamingStyle
value: CamelCase
- key: modernize-pass-by-value.IncludeStyle
value: llvm
- key: modernize-replace-auto-ptr.IncludeStyle
value: llvm
- key: modernize-use-nullptr.NullMacros
value: 'NULL'

View File

@@ -36,11 +36,18 @@ add_library(bar_lib)
target_sources(bar_lib target_sources(bar_lib
PUBLIC PUBLIC
src/app.cpp src/app.cpp
src/bar/bar.cpp src/bar/bar.cpp
src/components/mediaPlayer.cpp
src/components/button/iconButton.cpp
src/components/button/textButton.cpp
src/components/button/tabButton.cpp
src/connection/httpConnection.cpp src/connection/httpConnection.cpp
src/connection/dbus/notification.cpp src/connection/dbus/notification.cpp
src/connection/dbus/mpris.cpp src/connection/dbus/mpris.cpp
src/connection/dbus/bluetooth.cpp
src/connection/dbus/tray.cpp src/connection/dbus/tray.cpp
src/widgets/clock.cpp src/widgets/clock.cpp
@@ -49,22 +56,25 @@ target_sources(bar_lib
src/widgets/notification/copyNotification.cpp src/widgets/notification/copyNotification.cpp
src/widgets/notification/notificationWindow.cpp src/widgets/notification/notificationWindow.cpp
src/widgets/notification/spotifyNotification.cpp src/widgets/notification/spotifyNotification.cpp
src/widgets/controlCenter/controlCenter.cpp
src/widgets/controlCenter/mediaWidget.cpp
src/widgets/controlCenter/timer.cpp
src/widgets/volumeWidget.cpp src/widgets/volumeWidget.cpp
src/widgets/weather.cpp src/widgets/weather.cpp
src/widgets/webWidget.cpp src/widgets/webWidget.cpp
src/widgets/tray.cpp
src/widgets/controlCenter/bluetoothSettings.cpp
src/widgets/controlCenter/settings.cpp
src/widgets/controlCenter/bluetoothSettingsRow.cpp
src/services/hyprland.cpp src/services/hyprland.cpp
src/services/notificationController.cpp src/services/notificationController.cpp
src/services/textureCache.cpp src/services/textureCache.cpp
src/widgets/tray.cpp
src/widgets/controlCenter/controlCenter.cpp
src/widgets/controlCenter/mediaControl.cpp
src/components/popover.cpp src/components/popover.cpp
src/components/workspaceIndicator.cpp src/components/workspaceIndicator.cpp
src/components/base/button.cpp
) )
include_directories(bar_lib PRIVATE include_directories(bar_lib PRIVATE
include include
) )

View File

@@ -1,29 +1,38 @@
#pragma once #pragma once
#include <vector> #include <memory>
#include "bar/bar.hpp" #include "bar/bar.hpp"
#include "services/hyprland.hpp" #include "connection/dbus/bluetooth.hpp"
#include "connection/dbus/notification.hpp" #include "connection/dbus/notification.hpp"
#include "connection/dbus/tray.hpp" #include "connection/dbus/tray.hpp"
#include "services/hyprland.hpp"
#include "services/notificationController.hpp"
#include "gdkmm/monitor.h"
#include "glibmm/refptr.h" #include "glibmm/refptr.h"
#include "gtkmm/application.h" #include "gtkmm/application.h"
class MprisController;
class App { class App {
public: public:
struct BarMonitorPair {
std::shared_ptr<Bar> bar;
std::shared_ptr<Gdk::Monitor> window;
};
App(); App();
int run(); int run();
private: private:
Glib::RefPtr<Gtk::Application> app; Glib::RefPtr<Gtk::Application> app;
std::vector<std::shared_ptr<Bar>> bars; std::map<std::string, BarMonitorPair> bars;
std::shared_ptr<NotificationService> notificationService = nullptr;
std::shared_ptr<MprisController> mprisController = nullptr; std::shared_ptr<NotificationService> notificationService = NotificationService::getInstance();
HyprlandService *hyprlandService = nullptr; std::shared_ptr<NotificationController> notificationController = NotificationController::getInstance();
std::shared_ptr<BluetoothController> bluetoothController = BluetoothController::getInstance();
std::shared_ptr<MprisController> mprisController = MprisController::getInstance();
std::shared_ptr<HyprlandService> hyprlandService = HyprlandService::getInstance();
TrayService *trayService = TrayService::getInstance(); TrayService *trayService = TrayService::getInstance();
void setupServices(); void setupServices();

View File

@@ -4,12 +4,13 @@
#include <gtkmm.h> #include <gtkmm.h>
#include <memory> #include <memory>
#include "components/button/iconButton.hpp"
#include "widgets/clock.hpp" #include "widgets/clock.hpp"
#include "widgets/controlCenter/controlCenter.hpp"
#include "widgets/date.hpp" #include "widgets/date.hpp"
#include "widgets/tray.hpp" #include "widgets/tray.hpp"
#include "widgets/volumeWidget.hpp" #include "widgets/volumeWidget.hpp"
#include "widgets/webWidget.hpp" #include "widgets/webWidget.hpp"
#include "widgets/controlCenter/controlCenter.hpp"
class Bar : public Gtk::Window { class Bar : public Gtk::Window {
public: public:
@@ -27,12 +28,11 @@ class Bar : public Gtk::Window {
Clock clock; Clock clock;
Date date; Date date;
WebWidget homeAssistant{"\uf024", "Home Assistant", "https://home.rivercry.com"}; WebWidget homeAssistant{Icon::HOME_ASSISTANT, "Home Assistant", "https://home.rivercry.com"};
ControlCenter controlCenter{"\ue062", "Control Center"}; ControlCenter controlCenter{Icon::MENU, "Control Center"};
std::shared_ptr<TrayWidget> trayWidget = nullptr;
std::shared_ptr<VolumeWidget> volumeWidget = nullptr;
std::shared_ptr<TrayWidget> trayWidget = nullptr;
std::shared_ptr<VolumeWidget> volumeWidget = nullptr;
void setup_ui(); void setup_ui();
void setup_left_box(); void setup_left_box();

View File

@@ -1,18 +0,0 @@
#pragma once
#include <gtkmm/button.h>
#include "gtkmm/image.h"
#include "sigc++/signal.h"
class Button : public Gtk::Button {
public:
Button(const std::string label);
Button(Gtk::Image &image);
sigc::signal<void()> onClickedSignal;
private:
void on_clicked() {
onClickedSignal.emit();
}
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include "components/button/textButton.hpp"
#include "components/types/icon.hpp"
class IconButton : public TextButton {
public:
IconButton(Icon::Type icon, std::string fontFamilyCss = "material-icons");
void setIcon(Icon::Type icon);
};

View File

@@ -0,0 +1,9 @@
#pragma once
#include "components/button/iconButton.hpp"
class TabButton : public IconButton {
public:
TabButton(Icon::Type icon);
void setActive(bool active);
};

View File

@@ -0,0 +1,11 @@
#pragma once
#include <string>
#include "gtkmm/button.h"
class TextButton : public Gtk::Button {
public:
TextButton(const std::string &label);
void setText(const std::string &text);
};

View File

@@ -1,17 +1,17 @@
#pragma once #pragma once
#include "components/button/iconButton.hpp"
#include "connection/dbus/mpris.hpp"
#include "gtkmm/box.h" #include "gtkmm/box.h"
#include "gtkmm/button.h"
#include "gtkmm/label.h" #include "gtkmm/label.h"
#include "gtkmm/overlay.h" #include "gtkmm/overlay.h"
#include "gtkmm/picture.h" #include "gtkmm/picture.h"
#include "gtkmm/scale.h" #include "gtkmm/scale.h"
#include "gtkmm/scrolledwindow.h" #include "gtkmm/scrolledwindow.h"
#include "connection/dbus/mpris.hpp" class MediaPlayer : public Gtk::Box {
public:
class MediaControlWidget : public Gtk::Box { explicit MediaPlayer(std::shared_ptr<MprisController> controller);
public:
explicit MediaControlWidget(std::shared_ptr<MprisController> controller);
private: private:
std::shared_ptr<MprisController> mprisController; std::shared_ptr<MprisController> mprisController;
@@ -22,7 +22,7 @@ class MediaControlWidget : public Gtk::Box {
bool suppressSeekSignal = false; bool suppressSeekSignal = false;
std::string currentTrackId; std::string currentTrackId;
MprisController::PlaybackStatus playbackStatus = MprisController::PlaybackStatus::Stopped; MprisController::PlaybackStatus playbackStatus = MprisController::PlaybackStatus::Stopped;
bool canSeek = true; bool canSeek = true;
void setCurrentPosition(int64_t position_us); void setCurrentPosition(int64_t position_us);
void setTotalLength(int64_t length_us); void setTotalLength(int64_t length_us);
@@ -31,8 +31,6 @@ class MediaControlWidget : public Gtk::Box {
void schedulePauseAfterSeek(); void schedulePauseAfterSeek();
void setCanSeek(bool can_seek); void setCanSeek(bool can_seek);
Gtk::Box spotifyContainer;
// image as background, artist, title // image as background, artist, title
Gtk::Overlay topContainer; Gtk::Overlay topContainer;
Gtk::Picture backgroundImage; Gtk::Picture backgroundImage;
@@ -48,9 +46,9 @@ class MediaControlWidget : public Gtk::Box {
// playback controls // playback controls
Gtk::Box bottomContainer; Gtk::Box bottomContainer;
Gtk::Button previousButton; std::unique_ptr<IconButton> previousButton;
Gtk::Button playPauseButton; std::unique_ptr<IconButton> playPauseButton;
Gtk::Button nextButton; std::unique_ptr<IconButton> nextButton;
Gtk::ScrolledWindow imageWrapper; Gtk::ScrolledWindow imageWrapper;

View File

@@ -3,11 +3,13 @@
#include <gtkmm/button.h> #include <gtkmm/button.h>
#include <gtkmm/popover.h> #include <gtkmm/popover.h>
#include <string> #include <string>
#include "components/base/button.hpp"
class Popover : public Button { #include "components/button/iconButton.hpp"
#include "components/button/textButton.hpp"
class Popover : public IconButton {
public: public:
Popover(const std::string icon, std::string name); Popover(Icon::Type icon, std::string name);
~Popover() override; ~Popover() override;
protected: protected:

View File

@@ -0,0 +1 @@
#pragma once

View File

@@ -0,0 +1,105 @@
#pragma once
#include <iomanip>
#include <iostream>
#include <memory>
#include <sys/types.h>
#include "components/button/iconButton.hpp"
#include "services/timerService.hpp"
#include "gtkmm/box.h"
#include "gtkmm/label.h"
namespace {
std::string format_duration(const std::string &digits) {
std::string d = digits;
if (d.size() > 6) {
d = d.substr(d.size() - 6);
}
if (d.empty()) {
return "";
}
std::string padded = std::string(6 - d.size(), '0') + d;
int h = std::stoi(padded.substr(0, 2));
int m = std::stoi(padded.substr(2, 2));
int s = std::stoi(padded.substr(4, 2));
std::ostringstream out;
if (h > 0) {
out << h << "h " << std::setw(2) << std::setfill('0') << m << "m "
<< std::setw(2) << std::setfill('0') << s << "s";
} else if (m > 0) {
out << m << "m " << std::setw(2) << std::setfill('0') << s << "s";
} else {
out << s << "s";
}
return out.str();
}
} // namespace
class Timer : public Gtk::Box {
public:
Timer(const std::string &duration, uint64_t timerId) {
uint32_t totalTime = std::stoul(duration);
u_int64_t seconds = totalTime % 100;
u_int64_t minutes = (totalTime / 100) % 100;
u_int64_t hours = totalTime / 10000;
u_int64_t totalSeconds = hours * 3600 + minutes * 60 + seconds + 1; // +1 to account for the first tick happening after 1 second
this->timeLeft = totalSeconds;
this->timerLabel = std::make_shared<Gtk::Label>(format_duration(duration));
timerLabel->add_css_class("control-center-timer-label");
auto timerBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 5);
timerBox->append(*timerLabel);
auto cancelButton = Gtk::make_managed<IconButton>(Icon::Type::TOKEN);
cancelButton->set_tooltip_text("Cancel Timer");
cancelButton->signal_clicked().connect([this, timerId]() {
this->timerService->removeTimer(timerId);
});
timerBox->append(*cancelButton);
this->append(*timerBox);
}
void activateTimer() {
// std::cout << "Timer activated" << std::endl;
}
void updateTimeLeft(uint64_t timeValue) {
uint64_t s = timeValue % 100;
uint64_t m = (timeValue / 100) % 100;
uint64_t h = timeValue / 10000;
uint64_t totalSeconds = h * 3600 + m * 60 + s;
this->timeLeft = totalSeconds;
std::ostringstream out;
if (h > 0) {
out << h << "h " << std::setw(2) << std::setfill('0') << m << "m "
<< std::setw(2) << std::setfill('0') << s << "s";
} else if (m > 0) {
out << m << "m " << std::setw(2) << std::setfill('0') << s << "s";
} else {
out << s << "s";
}
this->timerLabel->set_text(out.str());
}
void tickDown() {
if (timeLeft > 0) {
this->updateTimeLeft(this->timeLeft - 1);
} else {
timeLeft = 0;
}
}
private:
std::shared_ptr<TimerService> timerService = TimerService::getInstance();
uint64_t timeLeft;
std::shared_ptr<Gtk::Label> timerLabel;
};

View File

@@ -0,0 +1,78 @@
#include <fcntl.h>
#include <map>
#include <string>
class Icon {
public:
enum Type {
HOME_ASSISTANT,
MENU,
SKIP_PREVIOUS,
SKIP_NEXT,
PLAY_ARROW,
PAUSE,
PLAY_CIRCLE,
EMPTY_DASHBOARD,
SAVE,
CONTENT_COPY,
TOKEN,
SETTINGS,
POWER_SETTINGS_NEW,
BLUETOOTH,
BLUETOOTH_CONNECTED,
BLUETOOTH_SEARCHING,
LINK,
LINK_OFF,
VERIFIED,
VERIFIED_OFF,
DONE_ALL,
REMOVE_DONE,
};
static const std::string toString(Type type) {
return typeToString[type];
}
private:
static inline std::map<Type, const std::string> typeToString = {
{HOME_ASSISTANT, "\uf024"},
{MENU, "\ue5d2"},
{SKIP_PREVIOUS, "\ue045"},
{SKIP_NEXT, "\ue044"},
{PLAY_ARROW, "\ue037"},
{PAUSE, "\ue034"},
{PLAY_CIRCLE, "\ue1c4"},
{EMPTY_DASHBOARD, "\uf844"},
{SAVE, "\ue161"},
{CONTENT_COPY, "\ue14d"},
{TOKEN, "\uea25"},
{SETTINGS, "\ue8b8"},
{POWER_SETTINGS_NEW, "\ue8ac"},
{BLUETOOTH, "\ue1a7"},
{BLUETOOTH_CONNECTED, "\ue1a8"},
{BLUETOOTH_SEARCHING, "\ue1aa"},
{LINK, "\ue157"},
{LINK_OFF, "\ue16f"},
{VERIFIED, "\uef76"},
{VERIFIED_OFF, "\uf30e"},
{DONE_ALL, "\ue877"},
{REMOVE_DONE, "\ue9d3"},
};
};

View File

@@ -8,7 +8,6 @@
class WorkspaceIndicator : public Gtk::Box { class WorkspaceIndicator : public Gtk::Box {
public: public:
enum InidicatorState { enum InidicatorState {
EMPTY, EMPTY,
ALIVE, ALIVE,

View File

@@ -0,0 +1,128 @@
#pragma once
#include <cstdint>
#include <giomm.h>
#include <map>
#include <memory>
#include <sigc++/sigc++.h>
#include <string>
#include <vector>
#include "connection/dbus/dbus.hpp"
struct BluetoothDevice {
std::string object_path;
std::string address;
std::string name;
std::string icon;
bool paired = false;
bool connected = false;
bool trusted = false;
int16_t rssi = 0;
};
class BluetoothController : public DbusConnection {
using PropertiesMap = std::map<Glib::ustring, Glib::VariantBase>;
public:
static std::shared_ptr<BluetoothController> getInstance() {
if (!instance) {
instance = std::shared_ptr<BluetoothController>(new BluetoothController());
}
return instance;
}
// Adapter control
void setPowered(bool powered);
bool isPowered() const;
// Discovery
void startDiscovery();
void stopDiscovery();
bool isDiscovering() const;
// Device actions (identified by object_path)
void pairDevice(const std::string &object_path);
void unpairDevice(const std::string &object_path);
void connectDevice(const std::string &object_path);
void disconnectDevice(const std::string &object_path);
void trustDevice(const std::string &object_path, bool trusted);
// Queries
std::vector<BluetoothDevice> getDevices() const;
std::vector<BluetoothDevice> getPairedDevices() const;
// Signals
sigc::signal<void(bool)> &signalPoweredChanged();
sigc::signal<void(bool)> &signalDiscoveringChanged();
sigc::signal<void(const BluetoothDevice &)> &signalDeviceAdded();
sigc::signal<void(const std::string &)> &signalDeviceRemoved();
sigc::signal<void(const BluetoothDevice &)> &signalDeviceChanged();
private:
BluetoothController();
inline static std::shared_ptr<BluetoothController> instance = nullptr;
bool m_powered = false;
bool m_discovering = false;
std::string m_adapter_path;
Glib::RefPtr<Gio::DBus::Proxy> m_adapter_proxy;
Glib::RefPtr<Gio::DBus::Proxy> m_object_manager_proxy;
std::map<std::string, BluetoothDevice> m_devices;
std::map<std::string, Glib::RefPtr<Gio::DBus::Proxy>> m_device_proxies;
sigc::signal<void(bool)> m_powered_signal;
sigc::signal<void(bool)> m_discovering_signal;
sigc::signal<void(const BluetoothDevice &)> m_device_added_signal;
sigc::signal<void(const std::string &)> m_device_removed_signal;
sigc::signal<void(const BluetoothDevice &)> m_device_changed_signal;
// Bus setup
void onBusConnected(const Glib::RefPtr<Gio::AsyncResult> &result);
void enumerateObjects();
void parseInterfaces(const std::string &object_path, const Glib::VariantBase &interfaces_var);
// ObjectManager signals
void onObjectManagerSignal(const Glib::ustring &sender_name,
const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters);
// Agent
guint m_agent_id = 0;
void registerAgent();
void on_agent_method_call(const Glib::RefPtr<Gio::DBus::Connection> &connection,
const Glib::ustring &sender,
const Glib::ustring &object_path,
const Glib::ustring &interface_name,
const Glib::ustring &method_name,
const Glib::VariantContainerBase &parameters,
const Glib::RefPtr<Gio::DBus::MethodInvocation> &invocation);
// Adapter
void setupAdapter(const std::string &path, const PropertiesMap &properties);
void onAdapterPropertiesChanged(const Gio::DBus::Proxy::MapChangedProperties &changed,
const std::vector<Glib::ustring> &invalidated);
// Devices
void addDevice(const std::string &path, const PropertiesMap &properties);
void removeDevice(const std::string &path);
void onDevicePropertiesChanged(const std::string &object_path,
const Gio::DBus::Proxy::MapChangedProperties &changed,
const std::vector<Glib::ustring> &invalidated);
// Helpers
static BluetoothDevice parseDeviceProperties(const std::string &path, const PropertiesMap &properties);
void setDbusProperty(const std::string &object_path,
const std::string &interface,
const std::string &property,
const Glib::VariantBase &value);
// HID Authorization handler
void authorizeHIDDevice(const std::string &object_path);
Glib::RefPtr<Gio::DBus::NodeInfo> m_node_info;
std::shared_ptr<Gio::DBus::InterfaceVTable> m_interface_vtable;
};

View File

@@ -5,20 +5,24 @@
class DbusConnection { class DbusConnection {
public: public:
virtual ~DbusConnection() = default; virtual ~DbusConnection() = default;
protected: protected:
Glib::RefPtr<Gio::DBus::Connection> connection; Glib::RefPtr<Gio::DBus::Connection> connection;
void connect_session_async(const sigc::slot<void(const Glib::RefPtr<Gio::AsyncResult> &)> &callback) { void connect_session_async(const sigc::slot<void(const Glib::RefPtr<Gio::AsyncResult> &)> &callback) {
Gio::DBus::Connection::get(Gio::DBus::BusType::SESSION, callback); Gio::DBus::Connection::get(Gio::DBus::BusType::SESSION, callback);
} }
static void ensure_gio_init() { void connect_system_async(const sigc::slot<void(const Glib::RefPtr<Gio::AsyncResult> &)> &callback) {
try { Gio::DBus::Connection::get(Gio::DBus::BusType::SYSTEM, callback);
Gio::init(); }
} catch (const Glib::Error &) {
// Already initialized. static void ensure_gio_init() {
} try {
} Gio::init();
} catch (const Glib::Error &) {
// Already initialized.
}
}
}; };

View File

@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <sys/types.h> #include <sys/types.h>
#include <vector> #include <vector>
#include "gdkmm/pixbuf.h" #include "gdkmm/pixbuf.h"
#include "glibmm/variant.h" #include "glibmm/variant.h"
@@ -23,8 +24,8 @@ struct MprisPlayer2Message {
}; };
enum NotificationUrgency { enum NotificationUrgency {
LOW = 0, LOW = 0,
NORMAL = 1, NORMAL = 1,
CRITICAL = 2 CRITICAL = 2
}; };
@@ -36,11 +37,15 @@ struct NotifyMessage {
std::string body; std::string body;
std::vector<std::string> actions; std::vector<std::string> actions;
NotificationUrgency urgency = NORMAL; NotificationUrgency urgency = NORMAL;
int32_t expire_timeout; int32_t expire_timeout = -1;
// Callback to invoke when an action is triggered // Callback to invoke when an action is triggered
std::function<void(const std::string& action_id)> on_action; std::function<void(const std::string &action_id)> on_action;
// Guard to prevent multiple action invocations across mirrors // Guard to prevent multiple action invocations across mirrors
std::shared_ptr<bool> actionInvoked; std::shared_ptr<bool> actionInvoked = std::make_shared<bool>(false);
// image data (if any) from dbus // image data (if any) from dbus
std::optional<Glib::RefPtr<Gdk::Pixbuf>> imageData; std::optional<Glib::RefPtr<Gdk::Pixbuf>> imageData;
bool has_input = false;
std::string input_placeholder;
std::function<void(const std::string &)> on_input;
}; };

View File

@@ -2,9 +2,10 @@
#include <cstdint> #include <cstdint>
#include <giomm.h> #include <giomm.h>
#include <sigc++/sigc++.h>
#include <set> #include <set>
#include <sigc++/sigc++.h>
#include <vector> #include <vector>
#include "connection/dbus/dbus.hpp" #include "connection/dbus/dbus.hpp"
#include "connection/dbus/messages.hpp" #include "connection/dbus/messages.hpp"
@@ -56,7 +57,7 @@ class MprisController : public DbusConnection {
Glib::RefPtr<Gio::DBus::Proxy> m_dbus_proxy; Glib::RefPtr<Gio::DBus::Proxy> m_dbus_proxy;
std::string m_player_bus_name = "org.mpris.MediaPlayer2.spotify"; std::string m_player_bus_name = "org.mpris.MediaPlayer2.spotify";
std::set<std::string> registeredPlayers; std::set<std::string> registeredPlayers;
sigc::signal<void(const MprisPlayer2Message &)> mprisUpdatedSignal; sigc::signal<void(const MprisPlayer2Message &)> mprisUpdatedSignal;
sigc::signal<void(PlaybackStatus)> playbackStatusChangedSignal; sigc::signal<void(PlaybackStatus)> playbackStatusChangedSignal;
sigc::signal<void(int64_t)> playbackPositionChangedSignal; sigc::signal<void(int64_t)> playbackPositionChangedSignal;
@@ -70,8 +71,8 @@ class MprisController : public DbusConnection {
void emit_cached_position(); void emit_cached_position();
void emit_cached_can_seek(); void emit_cached_can_seek();
void on_dbus_signal(const Glib::ustring &sender_name, void on_dbus_signal(const Glib::ustring &sender_name,
const Glib::ustring &signal_name, const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters); const Glib::VariantContainerBase &parameters);
void handle_player_registered(const std::string &bus_name); void handle_player_registered(const std::string &bus_name);
void handle_player_deregistered(const std::string &bus_name); void handle_player_deregistered(const std::string &bus_name);

View File

@@ -4,8 +4,10 @@
#include <giomm.h> #include <giomm.h>
#include <gtkmm.h> #include <gtkmm.h>
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
#include <sys/stat.h>
#include "connection/dbus/dbus.hpp" #include "connection/dbus/dbus.hpp"
#include "giomm/dbusconnection.h" #include "giomm/dbusconnection.h"
#include "giomm/dbusownname.h" #include "giomm/dbusownname.h"
#include "glib.h" #include "glib.h"
@@ -42,7 +44,17 @@ const Glib::ustring introspection_xml = R"(
class NotificationService : public DbusConnection { class NotificationService : public DbusConnection {
public: public:
NotificationService() : notificationIdCounter(1) { static std::shared_ptr<NotificationService> getInstance() {
if (NotificationService::instance == nullptr) {
NotificationService::instance = std::shared_ptr<NotificationService>(new NotificationService());
}
return NotificationService::instance;
}
void onBusAcquired(const Glib::RefPtr<Gio::DBus::Connection> &connection, const Glib::ustring &name);
private:
NotificationService() {
Gio::DBus::own_name( Gio::DBus::own_name(
Gio::DBus::BusType::SESSION, Gio::DBus::BusType::SESSION,
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@@ -52,10 +64,9 @@ class NotificationService : public DbusConnection {
Gio::DBus::BusNameOwnerFlags::REPLACE); Gio::DBus::BusNameOwnerFlags::REPLACE);
} }
void onBusAcquired(const Glib::RefPtr<Gio::DBus::Connection> &connection, const Glib::ustring &name); static inline std::shared_ptr<NotificationService> instance = nullptr;
private: guint notificationIdCounter = 0;
guint notificationIdCounter;
const Gio::DBus::InterfaceVTable &getMessageInterfaceVTable(); const Gio::DBus::InterfaceVTable &getMessageInterfaceVTable();
void on_method_call(const Glib::RefPtr<Gio::DBus::Connection> &connection, void on_method_call(const Glib::RefPtr<Gio::DBus::Connection> &connection,
const Glib::ustring &sender, const Glib::ustring &sender,

View File

@@ -19,6 +19,7 @@
class TrayService : public DbusConnection { class TrayService : public DbusConnection {
inline static TrayService *instance = nullptr; inline static TrayService *instance = nullptr;
public: public:
struct Item { struct Item {
std::string id; std::string id;
@@ -66,7 +67,7 @@ class TrayService : public DbusConnection {
if (TrayService::instance == nullptr) { if (TrayService::instance == nullptr) {
TrayService::instance = new TrayService(); TrayService::instance = new TrayService();
} }
return TrayService::instance; return TrayService::instance;
} }
@@ -137,7 +138,7 @@ class TrayService : public DbusConnection {
void begin_refresh(const std::string &id); void begin_refresh(const std::string &id);
static gboolean refresh_timeout_cb(gpointer user_data); static gboolean refresh_timeout_cb(gpointer user_data);
static void on_refresh_finished_static(GObject *source, GAsyncResult *res, static void on_refresh_finished_static(GObject *source, GAsyncResult *res,
gpointer user_data); gpointer user_data);
void emit_registered_items_changed(); void emit_registered_items_changed();
Glib::Variant<std::vector<Glib::ustring>> Glib::Variant<std::vector<Glib::ustring>>

View File

@@ -1,17 +1,17 @@
#pragma once #pragma once
#include <array> #include <array>
#include <cstdio>
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <cstdio>
class CommandHelper { class CommandHelper {
public: public:
static std::string exec(const char *cmd) { static std::string exec(const char *cmd) {
std::array<char, 128> buffer; std::array<char, 128> buffer;
std::string result; std::string result;
std::unique_ptr<FILE, int(*)(FILE*)> pipe(popen(cmd, "r"), pclose); std::unique_ptr<FILE, int (*)(FILE *)> pipe(popen(cmd, "r"), pclose);
if (!pipe) { if (!pipe) {
throw std::runtime_error("popen() failed!"); throw std::runtime_error("popen() failed!");
} }
@@ -23,7 +23,7 @@ class CommandHelper {
static void execNoOutput(std::string cmd) { static void execNoOutput(std::string cmd) {
std::string command = cmd + " > /dev/null 2>&1"; std::string command = cmd + " > /dev/null 2>&1";
int ret = std::system(command.c_str()); int ret = std::system(command.c_str());
if (ret != 0) { if (ret != 0) {
throw std::runtime_error("Command failed with return code: " + std::to_string(ret)); throw std::runtime_error("Command failed with return code: " + std::to_string(ret));
} }

View File

@@ -1,17 +1,101 @@
#pragma once #pragma once
#include <algorithm>
#include <array>
#include <cassert> #include <cassert>
#include <cstring>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <span>
#include <string> #include <string>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include "helpers/command.hpp" class HyprSocketHelper {
public:
static std::string getHyprlandSocketPath() {
const char *hyprlandInstanceSignature = std::getenv("HYPRLAND_INSTANCE_SIGNATURE");
const char *xdgRuntimeDir = std::getenv("XDG_RUNTIME_DIR");
if (!xdgRuntimeDir || !hyprlandInstanceSignature) {
return std::string();
}
std::string basePath = std::string(xdgRuntimeDir) + "/hypr/" + std::string(hyprlandInstanceSignature) + "/";
std::string sock1 = basePath + ".socket2.sock";
if (access(sock1.c_str(), F_OK) == 0) {
return sock1;
}
return std::string();
}
static std::string getHyprlandCommandSocketPath() {
const char *hyprlandInstanceSignature = std::getenv("HYPRLAND_INSTANCE_SIGNATURE");
const char *xdgRuntimeDir = std::getenv("XDG_RUNTIME_DIR");
if (!xdgRuntimeDir || !hyprlandInstanceSignature) {
return std::string();
}
std::string basePath = std::string(xdgRuntimeDir) + "/hypr/" + std::string(hyprlandInstanceSignature) + "/";
std::string sock1 = basePath + ".socket.sock";
if (access(sock1.c_str(), F_OK) == 0) {
return sock1;
}
return std::string();
}
};
class HyprctlHelper { class HyprctlHelper {
public: public:
static std::string sendCommand(const std::string &command) {
std::string socketPath = HyprSocketHelper::getHyprlandCommandSocketPath();
assert(!socketPath.empty() && "Failed to get Hyprland command socket path");
int socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
assert(socketFd != -1 && "Failed to create Hyprland command socket");
struct sockaddr_un addr{};
std::memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
auto sunPathSpan = std::span<char, sizeof(addr.sun_path)>(addr.sun_path);
std::ranges::fill(sunPathSpan, '\0');
constexpr size_t kMaxPathLength = sizeof(addr.sun_path) - 1;
const size_t copyLength = std::min(socketPath.size(), kMaxPathLength);
std::copy_n(socketPath.begin(), copyLength, sunPathSpan.begin());
auto *sockaddrPtr = static_cast<struct sockaddr *>(static_cast<void *>(&addr));
int result = connect(socketFd, sockaddrPtr, sizeof(addr));
assert(result != -1 && "Failed to connect to Hyprland command socket");
ssize_t bytesSent = send(socketFd, command.c_str(), command.size(), 0);
assert(bytesSent != -1 && "Failed to send command to Hyprland socket");
shutdown(socketFd, SHUT_WR);
std::string response;
constexpr size_t kBufferSize = 4096;
std::array<char, kBufferSize> buffer{};
while (true) {
ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), 0);
if (bytesRead <= 0) {
break;
}
response.append(buffer.data(), static_cast<size_t>(bytesRead));
}
close(socketFd);
return response;
}
static nlohmann::json getMonitorData() { static nlohmann::json getMonitorData() {
std::string result = CommandHelper::exec("hyprctl -j monitors"); std::string result = sendCommand("j/monitors");
assert(!result.empty() && "Failed to get monitor data from hyprctl"); assert(!result.empty() && "Failed to get monitor data from Hyprland socket");
auto json = nlohmann::json::parse(result); auto json = nlohmann::json::parse(result);
@@ -21,8 +105,8 @@ class HyprctlHelper {
} }
static nlohmann::json getWorkspaceData() { static nlohmann::json getWorkspaceData() {
std::string result = CommandHelper::exec("hyprctl -j workspaces"); std::string result = sendCommand("j/workspaces");
assert(!result.empty() && "Failed to get workspace data from hyprctl"); assert(!result.empty() && "Failed to get workspace data from Hyprland socket");
auto json = nlohmann::json::parse(result); auto json = nlohmann::json::parse(result);
@@ -32,8 +116,8 @@ class HyprctlHelper {
} }
static nlohmann::json getClientData() { static nlohmann::json getClientData() {
std::string result = CommandHelper::exec("hyprctl -j clients"); std::string result = sendCommand("j/clients");
assert(!result.empty() && "Failed to get client data from hyprctl"); assert(!result.empty() && "Failed to get client data from Hyprland socket");
auto json = nlohmann::json::parse(result); auto json = nlohmann::json::parse(result);
@@ -43,27 +127,7 @@ class HyprctlHelper {
} }
static void dispatchWorkspace(int workspaceNumber) { static void dispatchWorkspace(int workspaceNumber) {
std::string out = "hyprctl dispatch workspace " + std::to_string(workspaceNumber); std::string command = "dispatch workspace " + std::to_string(workspaceNumber);
CommandHelper::execNoOutput(out.c_str()); sendCommand(command);
}
};
class HyprSocketHelper {
public:
static std::string getHyprlandSocketPath() {
const char *hyprlandInstanceSignature = std::getenv("HYPRLAND_INSTANCE_SIGNATURE");
const char *xdgRuntimeDir = std::getenv("XDG_RUNTIME_DIR");
if (!xdgRuntimeDir || !hyprlandInstanceSignature) {
return std::string();
}
std::string basePath = std::string(xdgRuntimeDir) + "/hypr/" + std::string(hyprlandInstanceSignature) + "/";
std::string sock1 = basePath + ".socket2.sock";
if (access(sock1.c_str(), F_OK) == 0) {
return sock1;
}
return std::string();
} }
}; };

View File

@@ -3,9 +3,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
class StringHelper { class StringHelper {
public: public:
static std::vector<std::string> split(const std::string &input, char delimiter) { static std::vector<std::string> split(const std::string &input, char delimiter) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
std::string token; std::string token;
@@ -25,14 +24,14 @@ public:
return tokens; return tokens;
} }
static std::vector<std::string> split(const std::string &input, std::string delimiter) { static std::vector<std::string> split(const std::string &input, std::string delimiter) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
size_t start = 0; size_t start = 0;
size_t end = input.find(delimiter); size_t end = input.find(delimiter);
while (end != std::string::npos) { while (end != std::string::npos) {
tokens.push_back(input.substr(start, end - start)); tokens.push_back(input.substr(start, end - start));
start = end + delimiter.length(); start = end + delimiter.length();
end = input.find(delimiter, start); end = input.find(delimiter, start);
} }
tokens.push_back(input.substr(start)); tokens.push_back(input.substr(start));
return tokens; return tokens;
@@ -42,6 +41,12 @@ public:
if (input.length() <= maxSize) { if (input.length() <= maxSize) {
return input; return input;
} }
return input.substr(0, maxSize) + "...";
size_t len = maxSize;
while (len > 0 && (static_cast<unsigned char>(input[len]) & 0xC0) == 0x80) {
len--;
}
return input.substr(0, len) + "...";
} }
}; };

View File

@@ -10,7 +10,7 @@ class SystemHelper {
throw std::runtime_error("Could not open file: " + filePath); throw std::runtime_error("Could not open file: " + filePath);
} }
std::string content((std::istreambuf_iterator<char>(file)), std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>()); std::istreambuf_iterator<char>());
file.close(); file.close();
return content; return content;
} }

View File

@@ -1,49 +0,0 @@
#pragma once
#include <gio/gio.h>
#include <gtk/gtk.h>
#include <string>
#include <vector>
#include "sigc++/signal.h"
class BluetoothService {
inline static BluetoothService *instance = nullptr;
public:
sigc::signal<void(bool)> powerStateChangedSignal;
sigc::signal<void(bool)> isDiscoveringChangedSignal;
bool getPowerState();
bool getIsDiscovering();
void togglePowerState();
void toggleIsDiscovering();
static BluetoothService *getInstance() {
if (BluetoothService::instance == nullptr) {
BluetoothService::instance = new BluetoothService();
}
return BluetoothService::instance;
}
private:
BluetoothService();
GDBusProxy *adapter_proxy = nullptr;
std::vector<std::string> getDeviceObjectPaths();
bool powerState = false;
bool isDiscovering = false;
void onPropertyChanged(GDBusProxy *proxy,
GVariant *changed_properties,
const gchar *const *invalidated_properties,
gpointer user_data);
static void onPropertyChangedStatic(GDBusProxy *proxy,
GVariant *changed_properties,
const gchar *const *invalidated_properties,
gpointer user_data);
};

View File

@@ -17,7 +17,7 @@
#define NUM_WORKSPACES 7 #define NUM_WORKSPACES 7
class HyprlandService { class HyprlandService {
inline static HyprlandService *instance = nullptr; static inline std::shared_ptr<HyprlandService> instance;
public: public:
struct Client { struct Client {
@@ -49,9 +49,9 @@ class HyprlandService {
std::shared_ptr<Bar> bar; std::shared_ptr<Bar> bar;
}; };
static HyprlandService *getInstance() { static std::shared_ptr<HyprlandService> getInstance() {
if (!instance) { if (!instance) {
instance = new HyprlandService(); instance = std::shared_ptr<HyprlandService>(new HyprlandService());
} }
return instance; return instance;
} }
@@ -60,7 +60,12 @@ class HyprlandService {
void addBar(std::shared_ptr<Bar> bar, std::string monitorName); void addBar(std::shared_ptr<Bar> bar, std::string monitorName);
sigc::signal<void(std::string)> &signal_monitor_added() { return m_signal_monitor_added; }
sigc::signal<void(std::string)> &signal_monitor_removed() { return m_signal_monitor_removed; }
private: private:
HyprlandService();
enum SocketEventType { enum SocketEventType {
WORKSPACE_CHANGED, WORKSPACE_CHANGED,
@@ -72,6 +77,7 @@ class HyprlandService {
FOCUSED_MONITOR, FOCUSED_MONITOR,
MONITOR_REMOVED, MONITOR_REMOVED,
MONITOR_ADDED,
}; };
std::map<std::string, SocketEventType> socketEventTypeMap = { std::map<std::string, SocketEventType> socketEventTypeMap = {
@@ -83,7 +89,9 @@ class HyprlandService {
{"urgent", URGENT}, {"urgent", URGENT},
{"focusedmon", FOCUSED_MONITOR}, {"focusedmon", FOCUSED_MONITOR},
{"monitorremoved", MONITOR_REMOVED}, {"monitorremoved", MONITOR_REMOVED},
{"monitoradded", MONITOR_ADDED},
}; };
void onWorkspaceChanged(int workspaceId); void onWorkspaceChanged(int workspaceId);
void onFocusedMonitorChanged(std::string monitorData); void onFocusedMonitorChanged(std::string monitorData);
void onOpenWindow(std::string windowData); void onOpenWindow(std::string windowData);
@@ -92,16 +100,15 @@ class HyprlandService {
void onUrgent(std::string windowAddress); void onUrgent(std::string windowAddress);
void onActiveWindowChanged(std::string windowAddress); void onActiveWindowChanged(std::string windowAddress);
void onMonitorRemoved(std::string monitorName); void onMonitorRemoved(std::string monitorName);
// void onMonitorAdded(std::string monitorName); void onMonitorAdded(std::string monitorName);
HyprlandService();
std::map<std::string, std::shared_ptr<Monitor>> monitors; std::map<std::string, std::shared_ptr<Monitor>> monitors;
std::map<int, std::shared_ptr<Workspace>> workspaces; std::map<int, std::shared_ptr<Workspace>> workspaces;
std::map<std::string, std::shared_ptr<Client>> clients; std::map<std::string, std::shared_ptr<Client>> clients;
/// maybe refactor into reusable class /// maybe refactor into reusable class
std::string socketBuffer; std::string socketBuffer;
int socketFd; int socketFd = -1;
/// ///
void bindHyprlandSocket(); void bindHyprlandSocket();
@@ -112,4 +119,7 @@ class HyprlandService {
void refreshIndicator(std::shared_ptr<Workspace> workspace); void refreshIndicator(std::shared_ptr<Workspace> workspace);
void handleSocketMessage(SocketHelper::SocketMessage message); void handleSocketMessage(SocketHelper::SocketMessage message);
sigc::signal<void(std::string)> m_signal_monitor_added;
sigc::signal<void(std::string)> m_signal_monitor_removed;
}; };

View File

@@ -12,11 +12,11 @@
#include "gdkmm/monitor.h" #include "gdkmm/monitor.h"
class NotificationController { class NotificationController {
static std::shared_ptr<NotificationController> instance; inline static std::shared_ptr<NotificationController> instance = nullptr;
public: public:
static std::shared_ptr<NotificationController> getInstance() { static std::shared_ptr<NotificationController> getInstance() {
if (!NotificationController::instance) { if (NotificationController::instance == nullptr) {
NotificationController::instance = std::shared_ptr<NotificationController>(new NotificationController()); NotificationController::instance = std::shared_ptr<NotificationController>(new NotificationController());
} }
return NotificationController::instance; return NotificationController::instance;
@@ -26,12 +26,15 @@ class NotificationController {
void showNotificationOnAllMonitors(NotifyMessage notify); void showNotificationOnAllMonitors(NotifyMessage notify);
void showCopyNotification(NotifyMessage notify); void showCopyNotification(NotifyMessage notify);
void addMonitor(std::shared_ptr<Gdk::Monitor> monitor);
void removeMonitor(std::shared_ptr<Gdk::Monitor> monitor);
private: private:
uint64_t globalNotificationId = 1; uint64_t globalNotificationId = 1;
std::map<uint64_t, std::vector<std::shared_ptr<BaseNotification>>> activeNotifications; std::map<uint64_t, std::vector<std::shared_ptr<BaseNotification>>> activeNotifications;
std::map<uint64_t, int> hoverCounts; std::map<uint64_t, int> hoverCounts;
NotificationController(); NotificationController();
std::vector<std::shared_ptr<Gdk::Monitor>> activeMonitors; std::map<std::string, std::shared_ptr<Gdk::Monitor>> activeMonitors;
void updateHoverState(uint64_t notificationId, bool isHovered); void updateHoverState(uint64_t notificationId, bool isHovered);
void closeNotification(uint64_t notificationId); void closeNotification(uint64_t notificationId);
}; };

View File

@@ -12,9 +12,11 @@ class TextureCacheService {
Glib::RefPtr<Gdk::Texture> getTexture(const std::string &url); Glib::RefPtr<Gdk::Texture> getTexture(const std::string &url);
void pruneCache(); void pruneCache();
void clear();
private: private:
TextureCacheService() = default; TextureCacheService();
~TextureCacheService();
std::unordered_map<std::string, Glib::RefPtr<Gdk::Texture>> cache; std::unordered_map<std::string, Glib::RefPtr<Gdk::Texture>> cache;
}; };

View File

@@ -0,0 +1,129 @@
#pragma once
#include <map>
#include <memory>
#include <spdlog/spdlog.h>
#include <sys/types.h>
#include "glibmm/main.h"
#include "gtkmm/mediafile.h"
#include "gtkmm/mediastream.h"
#include "sigc++/signal.h"
class TimerService {
struct TimerData {
uint64_t duration;
uint64_t timerId;
};
public:
static std::shared_ptr<TimerService> getInstance() {
if (!instance) {
instance = std::shared_ptr<TimerService>(new TimerService());
// add a timer to tick every second
Glib::signal_timeout().connect([]() {
instance->tick();
return true; // continue calling every second
},
1000);
}
return instance;
}
void addTimer(uint64_t duration) {
this->timerAddedSignal.emit(std::to_string(duration), timerIdCounter);
this->activeTimers[timerIdCounter] = TimerData{.duration = duration, .timerId = timerIdCounter};
timerIdCounter++;
}
void expireTimer(uint64_t timerId) {
this->timerExpiredSignal.emit(timerId);
if (ringingTimerCounter == 0) {
mediaFile->set_volume(1.0);
mediaFile->set_loop(true);
mediaFile->play();
spdlog::info("Playing timer sound");
}
ringingTimerCounter++;
}
void removeTimer(uint64_t timerId) {
ringingTimerCounter--;
this->activeTimers.erase(timerId);
this->timerCancelledSignal.emit(timerId);
if (ringingTimerCounter <= 0) {
ringingTimerCounter = 0;
mediaFile->pause();
spdlog::info("Paused timer sound");
}
}
sigc::signal<void(const std::string &, u_int64_t)> &getSignalTimerSet() {
return this->timerAddedSignal;
}
sigc::signal<void(u_int64_t)> &getSignalTimerExpired() {
return this->timerExpiredSignal;
}
sigc::signal<void(u_int64_t)> &getSignalTimerCancelled() {
return this->timerCancelledSignal;
}
sigc::signal<void()> tickSignal;
private:
TimerService() {
this->mediaFile = Gtk::MediaFile::create();
if (this->mediaFile) {
this->mediaFile->set_filename("/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga");
// Connect to signals to debug playback issues
this->mediaFile->property_error().signal_changed().connect([]() {
if (auto err = mediaFile->get_error()) {
spdlog::error("Timer sound error: {}", err.what());
}
});
this->mediaFile->property_prepared().signal_changed().connect([]() {
if (mediaFile->is_prepared()) {
spdlog::info("Timer sound ready");
}
});
} else {
spdlog::error("Failed to create media file!");
}
}
inline static Glib::RefPtr<Gtk::MediaFile> mediaFile;
static inline uint64_t timerIdCounter = 0;
static inline u_int64_t ringingTimerCounter = 0;
sigc::signal<void(const std::string &, u_int64_t)> timerAddedSignal;
sigc::signal<void(u_int64_t)> timerCancelledSignal;
sigc::signal<void(u_int64_t)> timerExpiredSignal;
inline static std::shared_ptr<TimerService> instance = nullptr;
std::map<uint64_t, TimerData> activeTimers;
void tick() {
for (auto &[id, timer] : activeTimers) {
if (timer.duration > 0) {
timer.duration--;
} else if (timer.duration == 0) {
spdlog::info("Timer {} expired", timer.timerId);
expireTimer(timer.timerId);
timer.duration = -1; // Mark as expired to prevent multiple expirations
}
}
tickSignal.emit();
}
};

View File

@@ -6,8 +6,6 @@
#include "components/base/button.hpp" #include "components/base/button.hpp"
class BluetoothEntry : Gtk::Box { class BluetoothEntry : Gtk::Box {
public: public:
BluetoothEntry(std::string name, std::string address) { BluetoothEntry(std::string name, std::string address) {
@@ -38,7 +36,6 @@ class BluetoothEntry : Gtk::Box {
sigc::signal<void(std::string)> connect_clicked; sigc::signal<void(std::string)> connect_clicked;
}; };
class BluetoothWidget : public Gtk::Box { class BluetoothWidget : public Gtk::Box {
public: public:
BluetoothWidget(); BluetoothWidget();

View File

@@ -0,0 +1,40 @@
#pragma once
#include <spdlog/spdlog.h>
#include "components/button/iconButton.hpp"
#include "connection/dbus/bluetooth.hpp"
#include "gtkmm/box.h"
#include "gtkmm/scrolledwindow.h"
#include "widgets/controlCenter/bluetoothSettingsRow.hpp"
class BluetoothSettings : public Gtk::Box {
public:
BluetoothSettings();
private:
std::shared_ptr<BluetoothController> bluetoothController = BluetoothController::getInstance();
std::map<std::string, BluetoothDevice> activeBluetoothDevices;
std::map<std::string, std::shared_ptr<BluetoothSettingsRow>> deviceRows;
std::shared_ptr<IconButton> powerButton = std::make_shared<IconButton>(Icon::POWER_SETTINGS_NEW);
std::shared_ptr<IconButton> scanButton = std::make_shared<IconButton>(Icon::BLUETOOTH_SEARCHING);
Gtk::Box buttonRow;
Gtk::Box connectedDevicesBox;
Gtk::ScrolledWindow connectedDevicesScroll;
Gtk::Box availableDevicesBox;
Gtk::ScrolledWindow availableDevicesScroll;
bool bluetoothIsPowered = false;
bool bluetoothIsScanning = false;
void addBluetoothDevice(const BluetoothDevice &device);
void removeBluetoothDevice(const std::string &object_path);
void updateBluetoothDevice(const BluetoothDevice &device);
void setBluetoothPowered(bool powered);
void setScanning(bool scanning);
};

View File

@@ -0,0 +1,24 @@
#pragma once
#include <spdlog/spdlog.h>
#include "components/button/iconButton.hpp"
#include "connection/dbus/bluetooth.hpp"
#include "gtkmm/box.h"
#include "gtkmm/button.h"
#include "gtkmm/image.h"
#include "gtkmm/label.h"
class BluetoothSettingsRow : public Gtk::Box {
public:
BluetoothSettingsRow(const BluetoothDevice &device);
void updateDevice(const BluetoothDevice &device);
private:
BluetoothDevice device;
Gtk::Image icon;
Gtk::Label nameLabel;
Gtk::Label addressLabel;
IconButton pairButton;
IconButton connectButton;
IconButton trustButton;
};

View File

@@ -1,32 +1,41 @@
#pragma once #pragma once
#include <memory>
#include "components/button/iconButton.hpp"
#include "components/button/tabButton.hpp"
#include "components/popover.hpp" #include "components/popover.hpp"
#include "gtkmm/box.h" #include "widgets/controlCenter/mediaWidget.hpp"
#include "gtkmm/button.h" #include "widgets/controlCenter/settings.hpp"
#include "gtkmm/label.h" #include "widgets/controlCenter/timer.hpp"
#include "gtkmm/stack.h"
#include "widgets/controlCenter/mediaControl.hpp"
#include "widgets/weather.hpp" #include "widgets/weather.hpp"
#include <unordered_map> #include "gtkmm/box.h"
#include "gtkmm/scrolledwindow.h"
#include "gtkmm/stack.h"
class ControlCenter : public Popover { class ControlCenter : public Popover {
public: public:
ControlCenter(std::string icon, std::string name); ControlCenter(Icon::Type icon, std::string name);
private:
Gtk::Box container;
Gtk::Box tabRow;
Gtk::Stack contentStack;
Gtk::Box controlCenterContainer;
WeatherWidget weatherWidget;
Gtk::Button mediaControl;
Gtk::Button testTabButton;
std::shared_ptr<MprisController> mprisController = MprisController::getInstance();
std::unordered_map<std::string, MediaControlWidget*> mediaWidgets;
void addPlayerWidget(const std::string &bus_name); private:
void removePlayerWidget(const std::string &bus_name); Gtk::ScrolledWindow scrollview;
void setActiveTab(const std::string &tab_name);
Gtk::Box container;
Gtk::Box tabRow;
Gtk::Stack contentStack;
std::unique_ptr<TabButton> mediaTabButton;
std::unique_ptr<TabButton> infoTabButton;
std::unique_ptr<TabButton> timerButton;
std::unique_ptr<TabButton> settingsTabButton;
std::unique_ptr<WeatherWidget> weatherWidget;
std::unique_ptr<MediaWidget> mediaControlWidget;
std::unique_ptr<TimerWidget> timerWidget;
std::unique_ptr<SettingsWidget> settingsWidget;
void addPlayerWidget(const std::string &bus_name);
void removePlayerWidget(const std::string &bus_name);
void setActiveTab(const std::string &tab_name);
}; };

View File

@@ -0,0 +1,24 @@
#pragma once
#include <map>
#include <memory>
#include <string>
#include "components/mediaPlayer.hpp"
#include "connection/dbus/mpris.hpp"
#include "gtkmm/box.h"
class MediaWidget : public Gtk::Box {
public:
MediaWidget();
private:
Gtk::Box container;
std::shared_ptr<MprisController> mprisController = MprisController::getInstance();
void addPlayerWidget(const std::string &bus_name);
void removePlayerWidget(const std::string &bus_name);
std::map<std::string, std::unique_ptr<MediaPlayer>> mediaWidgets;
};

View File

@@ -0,0 +1,16 @@
#pragma once
#include <map>
#include "connection/dbus/bluetooth.hpp"
#include "widgets/controlCenter/bluetoothSettings.hpp"
#include "gtkmm/box.h"
class SettingsWidget : public Gtk::Box {
public:
SettingsWidget();
private:
BluetoothSettings bluetoothSettings;
};

View File

@@ -0,0 +1,29 @@
#pragma once
#include <memory>
#include <sigc++/connection.h>
#include <sys/types.h>
#include "components/timer.hpp"
#include "gtkmm/box.h"
class TimerWidget : public Gtk::Box {
public:
TimerWidget();
~TimerWidget();
void addTimer(const std::string &duration, uint64_t timerId);
void removeTimer(uint64_t timerId);
void activateTimer(uint64_t timerId);
private:
std::shared_ptr<TimerService> timerService = TimerService::getInstance();
bool updatingText = false;
std::string rawDigits;
std::map<uint64_t, std::unique_ptr<Timer>> activeTimers;
sigc::connection timerSetConnection;
sigc::connection timerCancelledConnection;
sigc::connection timerExpiredConnection;
sigc::connection tickConnection;
};

View File

@@ -1,17 +1,16 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <csignal>
#include <cstdint> #include <cstdint>
#include <memory>
#include <sigc++/connection.h> #include <sigc++/connection.h>
#include "gdkmm/monitor.h" #include "gdkmm/monitor.h"
#include "gtkmm/scrolledwindow.h"
#include "gtkmm/window.h" #include "gtkmm/window.h"
#define DEFAULT_NOTIFICATION_TIMEOUT 6700 #define DEFAULT_NOTIFICATION_TIMEOUT 6700
class BaseNotification : public Gtk::Window { class BaseNotification : public Gtk::Window, public std::enable_shared_from_this<BaseNotification> {
public: public:
BaseNotification(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor); BaseNotification(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor);
@@ -19,16 +18,29 @@ class BaseNotification : public Gtk::Window {
void resumeAutoClose(); void resumeAutoClose();
void startAutoClose(int timeoutMs); void startAutoClose(int timeoutMs);
sigc::signal<void(int)> signal_close;
sigc::signal<void(bool)> signal_hover_changed;
virtual ~BaseNotification() = default;
uint64_t getNotificationId() const { uint64_t getNotificationId() const {
return this->notificationId; return this->notificationId;
} }
sigc::signal<void(uint64_t)> getSignalClose() {
return this->signalClose;
}
sigc::signal<void(bool)> getSignalHoverChanged() {
return this->signalHoverChanged;
}
private: private:
sigc::signal<void(uint64_t)> signalClose;
sigc::signal<void(bool)> signalHoverChanged;
uint64_t notificationId;
bool autoClosePaused = false;
int autoCloseRemainingMs = 0;
std::chrono::steady_clock::time_point autoCloseDeadline;
sigc::connection autoCloseConnection;
void ensure_notification_css_loaded(); void ensure_notification_css_loaded();
void start_auto_close_timeout(int timeoutMs); void start_auto_close_timeout(int timeoutMs);
@@ -36,10 +48,15 @@ class BaseNotification : public Gtk::Window {
void resume_auto_close(); void resume_auto_close();
protected: protected:
uint64_t notificationId; bool getAutoClosePaused() const {
return this->autoClosePaused;
}
bool autoClosePaused = false; int getAutoCloseRemainingMs() const {
int autoCloseRemainingMs = 0; return this->autoCloseRemainingMs;
std::chrono::steady_clock::time_point autoCloseDeadline; }
sigc::connection autoCloseConnection;
std::chrono::steady_clock::time_point getAutoCloseDeadline() const {
return this->autoCloseDeadline;
}
}; };

View File

@@ -4,6 +4,7 @@
#include "connection/dbus/messages.hpp" #include "connection/dbus/messages.hpp"
#include "widgets/notification/baseNotification.hpp" #include "widgets/notification/baseNotification.hpp"
#include "gtkmm/box.h" #include "gtkmm/box.h"
class CopyNotification : public BaseNotification { class CopyNotification : public BaseNotification {

View File

@@ -2,6 +2,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include "connection/dbus/messages.hpp" #include "connection/dbus/messages.hpp"
#include "widgets/notification/baseNotification.hpp" #include "widgets/notification/baseNotification.hpp"
@@ -9,5 +10,4 @@ class NotificationWindow : public BaseNotification {
public: public:
NotificationWindow(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor, NotifyMessage message); NotificationWindow(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor, NotifyMessage message);
virtual ~NotificationWindow() = default; virtual ~NotificationWindow() = default;
}; };

View File

@@ -17,12 +17,12 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "components/button/iconButton.hpp"
#include "connection/dbus/tray.hpp" #include "connection/dbus/tray.hpp"
#include "components/base/button.hpp"
class TrayIconWidget : public Button { class TrayIconWidget : public IconButton {
public: public:
TrayIconWidget(std::string id); TrayIconWidget(Icon::Type icon, std::string id);
~TrayIconWidget() override; ~TrayIconWidget() override;
void update(const TrayService::Item &item); void update(const TrayService::Item &item);
@@ -39,12 +39,12 @@ class TrayIconWidget : public Button {
Glib::RefPtr<Gtk::PopoverMenu> menuPopover; Glib::RefPtr<Gtk::PopoverMenu> menuPopover;
Glib::RefPtr<Gio::SimpleActionGroup> menuActions; Glib::RefPtr<Gio::SimpleActionGroup> menuActions;
Glib::RefPtr<Gio::MenuModel> menuModel; Glib::RefPtr<Gio::MenuModel> menuModel;
bool menuPopupPending = false; bool menuPopupPending = false;
bool menuRequestInFlight = false; bool menuRequestInFlight = false;
bool hasRemoteMenu = false; bool hasRemoteMenu = false;
std::shared_ptr<bool> aliveFlag; std::shared_ptr<bool> aliveFlag;
double pendingX = 0.0; double pendingX = 0.0;
double pendingY = 0.0; double pendingY = 0.0;
void on_primary_released(int n_press, double x, double y); void on_primary_released(int n_press, double x, double y);
void on_middle_released(int n_press, double x, double y); void on_middle_released(int n_press, double x, double y);

View File

@@ -3,9 +3,10 @@
#include <gtkmm/button.h> #include <gtkmm/button.h>
#include <gtkmm/popover.h> #include <gtkmm/popover.h>
#include "components/button/iconButton.hpp"
#include "components/popover.hpp" #include "components/popover.hpp"
class WebWidget : public Popover { class WebWidget : public Popover {
public: public:
WebWidget(std::string icon, std::string title, std::string url); WebWidget(Icon::Type icon, std::string title, std::string url);
}; };

View File

@@ -1,7 +1,6 @@
#include "app.hpp" #include "app.hpp"
int main() int main() {
{
App app; App app;
return app.run(); return app.run();

View File

@@ -1,3 +1,27 @@
/* Custom Scrollbar Styling */
scrollbar, scrollbar * {
min-width: 8px;
background: transparent;
}
scrollbar slider {
background: rgba(255, 255, 255, 0.25);
border-radius: 6px;
min-width: 8px;
}
scrollbar slider:hover, scrollbar slider:active {
background: rgba(255, 255, 255, 0.45);
}
scrollbar trough {
background: rgba(255, 255, 255, 0.08);
border-radius: 6px;
}
scrollbar button {
background: transparent;
}
/** biome-ignore-all lint/correctness/noUnknownTypeSelector: gtk css has more valid identifiers */ /** biome-ignore-all lint/correctness/noUnknownTypeSelector: gtk css has more valid identifiers */
* { * {
all: unset; all: unset;
@@ -7,47 +31,48 @@
:root { :root {
--icon-font-material: "Material Icons", sans-serif; --icon-font-material: "Material Icons", sans-serif;
--icon-font-awesome: "Font Awesome 7 Free", sans-serif; --icon-font-awesome: "Font Awesome 7 Free", sans-serif;
--text-font: "Hack Nerd Font Mono", sans-serif; --text-font: "Hack Nerd Font", sans-serif;
--text-font-mono: "Hack Nerd Font Mono", monospace; --text-font-mono: "Hack Nerd Font Mono", monospace;
} }
window { window {
background-color: #191919c6; background-color: #191919c6;
color: #ffffff; color: #ffffff;
padding-left: 4px;
padding-right: 4px;
padding-bottom: 2px;
font-size: 14px; font-size: 14px;
font-family: var(--text-font); font-family: var(--text-font);
padding: 3px;
}
.text-area {
font-family: var(--text-font-mono);
background-color: rgba(25, 25, 25, 0.8);
border-radius: 8px;
padding: 4px 8px;
} }
.material-icons { .material-icons {
font-family: var(--icon-font-material); font-family: var(--icon-font-material);
font-size: 18px;
} }
.tab-icon { .power-button-on {
font-family: var(--icon-font-material); /* bright green */
font-size: 20px; background-color: rgba(0, 255, 0, 0.2);
margin-right: 6px;
border-radius: 4px;
border: 1px solid transparent;
padding: 2px 4px;
} }
.tab-icon:hover { .power-button-off {
background-color: rgba(255, 255, 255, 0.1); /* bright red */
border: 1px solid rgba(255, 255, 255, 0.2); background-color: rgba(255, 0, 0, 0.2);
color: #ffffff;
} }
.control-center-tab-row { .control-center-tab-row {
/* mordern and sleek */ background-color: rgba(255, 255, 255, 0.1);
background-color: rgba(50, 50, 50, 0.8); border-radius: 4px;
border-radius: 8px;
padding: 2px 4px;
margin-bottom: 4px;
} }
.active-button { .active-button {
background-color: #ffffff; background-color: #ffffff;
color: #1e1e1e; color: #1e1e1e;
@@ -56,30 +81,27 @@ window {
} }
popover { popover {
margin-top: 4px; font-family: var(--text-font);
font-family: var(--text-font); border-radius: 12px;
/* padding: 6px; TODO: create better padding*/
border-radius: 8px;
background: rgba(25, 25, 25, 0.8);
background: rgba(25, 25, 25, 0.9);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(57, 57, 57, 0.71); border: 1px solid rgba(57, 57, 57, 0.71);
font-size: 14px;
} }
.control-center-popover { .control-center-popover {
margin-top: 6px;
background-color: rgba(35, 35, 35, 0.95); background-color: rgba(35, 35, 35, 0.95);
padding: 12px; padding: 12px;
padding-top : 6px; padding-top : 6px;
border-radius: 8px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(57, 57, 57, 0.8); border: 1px solid rgba(57, 57, 57, 0.8);
} }
.control-center-player-container { .control-center-player-container {
border-radius: 8px; border-radius: 4px;
background: rgba(35, 35, 35, 0.95); background: rgba(35, 35, 35, 0.95);
margin-bottom: 6px;
} }
@@ -105,8 +127,6 @@ popover {
} }
.control-center-seek-bar { .control-center-seek-bar {
min-height: 6px;
min-width: 120px;
margin: 0 6px; margin: 0 6px;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
} }
@@ -138,34 +158,32 @@ tooltip {
font-size: 12px; font-size: 12px;
} }
button { .button {
font-size: 20px; font-size: 14px;
padding: 3px 6px; padding: 4px 8px;
border-radius: 4px;
transition:
background-color 0.2s,
color 0.2s,
border-color 0.2s;
} }
#spacer { #spacer {
font-weight: 900; font-weight: 900;
padding: 0 5px; padding: 0 5px;
text-shadow: 0 0 5px #ffffffaa;
border-radius: 4px; border-radius: 4px;
} }
.button:hover { .button:hover {
background-color: #111111; background-color: rgba(255, 255, 255, 0.1);
} box-shadow: none;
.flat-button {
background-color: #333333;
color: #ffffff;
padding: 2px 4px;
border-radius: 10px;
} }
.workspace-pill { .workspace-pill {
padding: 2px 5px; padding: 2px 4px;
margin-right: 6px; border-radius: 4px;
border-radius: 5px; margin: 0 2px;
text-shadow: 0 0 2px #646464; color: #cccccc;
transition: transition:
background-color 0.2s, background-color 0.2s,
color 0.2s, color 0.2s,
@@ -216,6 +234,17 @@ button {
margin-top: 4px; margin-top: 4px;
} }
.icon-button {
font-weight: 700;
font-size: 18px;
}
.tab-icon {
}
@keyframes workspace-updown { @keyframes workspace-updown {
0% { 0% {
transform: translateY(4px); transform: translateY(4px);
@@ -243,3 +272,27 @@ button {
opacity: 1; opacity: 1;
} }
} }
.bluetooth-settings {
font-family: var(--text-font);
font-size: 14px;
}
.bluetooth-device-address {
font-size: 10px;
color: #cccccce7;
}
.bluetooth-settings-row {
transition: background-color 0.2s;
}
.active-devices {
background-color: rgba(0, 255, 0, 0.1);
border-radius: 4px;
}
.available-devices {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}

View File

@@ -1,33 +1,96 @@
#include "app.hpp" #include "app.hpp"
#include <glibmm/main.h>
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
#include <vector>
#include "connection/dbus/notification.hpp"
#include "connection/dbus/mpris.hpp" #include "connection/dbus/mpris.hpp"
#include "connection/dbus/notification.hpp"
#include "services/notificationController.hpp" #include "services/notificationController.hpp"
#include "services/textureCache.hpp" #include "services/textureCache.hpp"
App::App() { App::App() : app(Gtk::Application::create("org.example.mybar")) {
this->app = Gtk::Application::create("org.example.mybar");
this->setupServices(); this->setupServices();
this->hyprlandService = HyprlandService::getInstance();
this->notificationService = std::make_shared<NotificationService>();
this->mprisController = MprisController::getInstance();
auto notificationController = NotificationController::getInstance();
this->mprisController->signal_mpris_updated().connect( this->mprisController->signal_mpris_updated().connect(
[notificationController](const MprisPlayer2Message &msg) { [this](const MprisPlayer2Message &msg) {
notificationController->showSpotifyNotification(msg); this->notificationController->showSpotifyNotification(msg);
}); });
this->hyprlandService->signal_monitor_added().connect([this](std::string monitorName) {
spdlog::info("[App] Monitor added: {}", monitorName);
auto tryCreateBar = [this](const std::string &name) -> bool {
if (bars.find(name) != bars.end()) {
return true;
}
auto display = Gdk::Display::get_default();
if (!display) {
return false;
}
auto monitors = display->get_monitors();
for (guint i = 0; i < monitors->get_n_items(); ++i) {
auto monitor = std::dynamic_pointer_cast<Gdk::Monitor>(
monitors->get_object(i));
if (monitor && monitor->get_connector() == name) {
auto bar = std::make_shared<Bar>(monitor->gobj());
bar->set_application(app);
bar->show();
std::string connectorName = monitor->get_connector();
bar->addLeftWidget(hyprlandService->getWorkspaceIndicatorsForMonitor(connectorName));
hyprlandService->addBar(bar, connectorName);
BarMonitorPair barMonitorPair{.bar = bar, .window = monitor};
bars[connectorName] = barMonitorPair;
if (this->notificationController) {
this->notificationController->addMonitor(monitor);
}
return true;
}
}
return false;
};
if (tryCreateBar(monitorName)) {
return;
}
auto attempts = std::make_shared<int>(0);
Glib::signal_timeout().connect(
[monitorName, attempts, tryCreateBar]() -> bool {
if (*attempts >= 10) {
spdlog::warn("[App] Monitor {} not ready after retries", monitorName);
return false;
}
(*attempts)++;
return !tryCreateBar(monitorName);
},
200);
});
this->hyprlandService->signal_monitor_removed().connect([this](std::string monitorName) {
spdlog::info("[App] Monitor removed: {}", monitorName);
auto bar = bars[monitorName].bar;
if (bar) {
bar->close();
}
bars.erase(monitorName);
});
app->signal_activate().connect([&]() { app->signal_activate().connect([&]() {
auto display = Gdk::Display::get_default(); auto display = Gdk::Display::get_default();
auto monitors = display->get_monitors(); auto monitors = display->get_monitors();
for (guint i = 0; i < monitors->get_n_items(); ++i) { for (guint i = 0; i < monitors->get_n_items(); ++i) {
auto monitor = std::dynamic_pointer_cast<Gdk::Monitor>( auto monitor = std::dynamic_pointer_cast<Gdk::Monitor>(
monitors->get_object(i) monitors->get_object(i));
);
if (monitor) { if (monitor) {
auto bar = std::make_shared<Bar>(monitor->gobj()); auto bar = std::make_shared<Bar>(monitor->gobj());
@@ -39,13 +102,18 @@ App::App() {
bar->addLeftWidget(hyprlandService->getWorkspaceIndicatorsForMonitor(monitorName)); bar->addLeftWidget(hyprlandService->getWorkspaceIndicatorsForMonitor(monitorName));
hyprlandService->addBar(bar, monitorName); hyprlandService->addBar(bar, monitorName);
bars.push_back(bar); BarMonitorPair barMonitorPair{.bar = bar, .window = monitor};
bars[monitorName] = barMonitorPair;
if (this->notificationController) {
this->notificationController->addMonitor(monitor);
}
} }
} }
}); });
app->signal_shutdown().connect([&]() { app->signal_shutdown().connect([&]() {
TextureCacheService::getInstance()->clear();
this->trayService->stop(); this->trayService->stop();
}); });
} }

View File

@@ -5,6 +5,8 @@
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <gtkmm/window.h> #include <gtkmm/window.h>
#include "components/button/textButton.hpp"
#include "connection/dbus/bluetooth.hpp"
#include "helpers/system.hpp" #include "helpers/system.hpp"
#include "widgets/date.hpp" #include "widgets/date.hpp"
#include "widgets/spacer.hpp" #include "widgets/spacer.hpp"
@@ -12,10 +14,12 @@
#include "glibmm/main.h" #include "glibmm/main.h"
#include "gtk/gtk.h" #include "gtk/gtk.h"
#include "sigc++/functors/mem_fun.h"
Bar::Bar(GdkMonitor *monitor) { Bar::Bar(GdkMonitor *monitor) {
set_name("bar-window"); set_name("bar-window");
add_css_class("bar");
auto bluetooh = BluetoothController::getInstance();
gtk_layer_init_for_window(this->gobj()); gtk_layer_init_for_window(this->gobj());
@@ -31,9 +35,10 @@ Bar::Bar(GdkMonitor *monitor) {
gtk_layer_auto_exclusive_zone_enable(this->gobj()); gtk_layer_auto_exclusive_zone_enable(this->gobj());
set_child(main_box); set_child(main_box);
set_valign(Gtk::Align::CENTER);
this->volumeWidget = std::make_shared<VolumeWidget>(); this->volumeWidget = std::make_shared<VolumeWidget>();
this->trayWidget = std::make_shared<TrayWidget>(); this->trayWidget = std::make_shared<TrayWidget>();
load_css(); load_css();
setup_ui(); setup_ui();

View File

@@ -1,14 +0,0 @@
#include "components/base/button.hpp"
#include "sigc++/functors/mem_fun.h"
Button::Button(const std::string label) : Gtk::Button(label) {
signal_clicked().connect(sigc::mem_fun(*this, &Button::on_clicked));
this->add_css_class("button");
}
Button::Button(Gtk::Image &image) : Gtk::Button() {
set_child(image);
signal_clicked().connect(sigc::mem_fun(*this, &Button::on_clicked));
this->add_css_class("button");
}

View File

@@ -0,0 +1,10 @@
#include "components/button/iconButton.hpp"
IconButton::IconButton(Icon::Type icon, std::string fontFamilyCss) : TextButton(Icon::toString(icon)) {
this->get_style_context()->add_class("icon-button");
this->get_style_context()->add_class(fontFamilyCss);
}
void IconButton::setIcon(Icon::Type icon) {
this->set_label(Icon::toString(icon));
}

View File

@@ -0,0 +1,13 @@
#include "components/button/tabButton.hpp"
TabButton::TabButton(Icon::Type icon) : IconButton(icon) {
add_css_class("tab-icon");
}
void TabButton::setActive(bool active) {
if (active) {
this->add_css_class("active-button");
} else {
this->remove_css_class("active-button");
}
}

View File

@@ -0,0 +1,9 @@
#include "components/button/textButton.hpp"
TextButton::TextButton(const std::string &label) : Gtk::Button(label) {
add_css_class("button");
}
void TextButton::setText(const std::string &text) {
this->set_label(text);
}

View File

@@ -1,15 +1,31 @@
#include "widgets/controlCenter/mediaControl.hpp" #include "components/button/iconButton.hpp"
#include "components/mediaPlayer.hpp"
#include "helpers/string.hpp" #include "helpers/string.hpp"
#include "services/textureCache.hpp" #include "services/textureCache.hpp"
MediaControlWidget::MediaControlWidget(std::shared_ptr<MprisController> controller) namespace {
: Gtk::Box(Gtk::Orientation::VERTICAL) { std::string formatTimeUs(int64_t time_us) {
if (time_us < 0) {
time_us = 0;
}
int64_t totalSeconds = time_us / 1000000;
int64_t hours = totalSeconds / 3600;
int64_t minutes = (totalSeconds / 60) % 60;
int64_t seconds = totalSeconds % 60;
this->mprisController = std::move(controller); if (hours > 0) {
return std::to_string(hours) + ":" +
(minutes < 10 ? "0" : "") + std::to_string(minutes) + ":" +
(seconds < 10 ? "0" : "") + std::to_string(seconds);
}
return std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds);
}
} // namespace
MediaPlayer::MediaPlayer(std::shared_ptr<MprisController> controller)
: Gtk::Box(Gtk::Orientation::VERTICAL), mprisController(controller) {
this->set_orientation(Gtk::Orientation::VERTICAL); this->set_orientation(Gtk::Orientation::VERTICAL);
this->set_size_request(200, -1);
this->set_hexpand(false); this->set_hexpand(false);
this->set_vexpand(false); this->set_vexpand(false);
this->add_css_class("control-center-player-container"); this->add_css_class("control-center-player-container");
@@ -88,6 +104,10 @@ MediaControlWidget::MediaControlWidget(std::shared_ptr<MprisController> controll
} }
}); });
this->previousButton = std::make_unique<IconButton>(Icon::SKIP_PREVIOUS);
this->playPauseButton = std::make_unique<IconButton>(Icon::PLAY_ARROW);
this->nextButton = std::make_unique<IconButton>(Icon::SKIP_NEXT);
this->bottomContainer.set_orientation(Gtk::Orientation::HORIZONTAL); this->bottomContainer.set_orientation(Gtk::Orientation::HORIZONTAL);
this->bottomContainer.set_vexpand(false); this->bottomContainer.set_vexpand(false);
this->bottomContainer.set_hexpand(false); this->bottomContainer.set_hexpand(false);
@@ -95,31 +115,22 @@ MediaControlWidget::MediaControlWidget(std::shared_ptr<MprisController> controll
this->bottomContainer.set_homogeneous(true); this->bottomContainer.set_homogeneous(true);
this->topContainer.set_vexpand(false); this->topContainer.set_vexpand(false);
this->topContainer.set_hexpand(true); this->topContainer.set_hexpand(true);
this->bottomContainer.append(this->previousButton); this->bottomContainer.append(*this->previousButton);
this->bottomContainer.append(this->playPauseButton); this->bottomContainer.append(*this->playPauseButton);
this->bottomContainer.append(this->nextButton); this->bottomContainer.append(*this->nextButton);
this->previousButton.set_label("\u23EE"); // Previous track symbol this->previousButton->signal_clicked().connect([this]() {
this->previousButton.add_css_class("button");
this->previousButton.add_css_class("material-icons");
this->playPauseButton.set_label("\u23EF"); // Play/Pause symbol
this->playPauseButton.add_css_class("button");
this->playPauseButton.add_css_class("material-icons");
this->nextButton.set_label("\u23ED"); // Next track symbol
this->nextButton.add_css_class("button");
this->nextButton.add_css_class("material-icons");
this->previousButton.signal_clicked().connect([this]() {
this->mprisController->previous_song(); this->mprisController->previous_song();
}); });
this->playPauseButton.signal_clicked().connect([this]() { this->playPauseButton->signal_clicked().connect([this]() {
this->mprisController->toggle_play(); this->mprisController->toggle_play();
}); });
this->nextButton.signal_clicked().connect([this]() { this->nextButton->signal_clicked().connect([this]() {
this->mprisController->next_song(); this->mprisController->next_song();
}); });
this->mprisController->signal_mpris_updated().connect( this->mprisController->signal_mpris_updated().connect(
sigc::mem_fun(*this, &MediaControlWidget::onSpotifyMprisUpdated)); sigc::mem_fun(*this, &MediaPlayer::onSpotifyMprisUpdated));
this->mprisController->signal_playback_status_changed().connect( this->mprisController->signal_playback_status_changed().connect(
[this](MprisController::PlaybackStatus status) { [this](MprisController::PlaybackStatus status) {
@@ -144,12 +155,12 @@ MediaControlWidget::MediaControlWidget(std::shared_ptr<MprisController> controll
this->resetSeekTimer(0); this->resetSeekTimer(0);
} }
void MediaControlWidget::setCanSeek(bool can_seek) { void MediaPlayer::setCanSeek(bool can_seek) {
this->canSeek = can_seek; this->canSeek = can_seek;
this->seekBarContainer.set_visible(can_seek); this->seekBarContainer.set_visible(can_seek);
} }
void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &message) { void MediaPlayer::onSpotifyMprisUpdated(const MprisPlayer2Message &message) {
std::string artistText = "Unknown Artist"; std::string artistText = "Unknown Artist";
if (!message.artist.empty()) { if (!message.artist.empty()) {
artistText = StringHelper::trimToSize(message.artist[0], 30); artistText = StringHelper::trimToSize(message.artist[0], 30);
@@ -157,7 +168,7 @@ void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &messag
this->artistLabel.set_text(artistText); this->artistLabel.set_text(artistText);
this->titleLabel.set_text(message.title); this->titleLabel.set_text(message.title);
const bool trackChanged = !this->currentTrackId.empty() && this->currentTrackId != message.track_id; const bool trackChanged = !this->currentTrackId.empty() && this->currentTrackId != message.track_id;
this->currentTrackId = message.track_id; this->currentTrackId = message.track_id;
if (trackChanged) { if (trackChanged) {
this->currentPositionUs = 0; this->currentPositionUs = 0;
@@ -177,29 +188,23 @@ void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &messag
} }
} }
void MediaControlWidget::setCurrentPosition(int64_t position_us) { void MediaPlayer::setCurrentPosition(int64_t position_us) {
this->currentPositionUs = position_us; this->currentPositionUs = position_us;
int64_t seconds = (position_us / 1000000) % 60; this->currentTimeLabel.set_text(formatTimeUs(position_us));
int64_t minutes = (position_us / (1000000 * 60)) % 60;
this->currentTimeLabel.set_text(
std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds));
if (totalLengthUs > 0) { if (totalLengthUs > 0) {
double fraction = static_cast<double>(currentPositionUs) / static_cast<double>(totalLengthUs); double fraction = static_cast<double>(currentPositionUs) / static_cast<double>(totalLengthUs);
this->suppressSeekSignal = true; this->suppressSeekSignal = true;
this->seekBar.set_value(fraction * 100); this->seekBar.set_value(fraction * 100);
this->suppressSeekSignal = false; this->suppressSeekSignal = false;
} }
} }
void MediaControlWidget::setTotalLength(int64_t length_us) { void MediaPlayer::setTotalLength(int64_t length_us) {
this->totalLengthUs = length_us; this->totalLengthUs = length_us;
int64_t seconds = (length_us / 1000000) % 60; this->totalTimeLabel.set_text(formatTimeUs(length_us));
int64_t minutes = (length_us / (1000000 * 60)) % 60;
this->totalTimeLabel.set_text(
std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds));
} }
void MediaControlWidget::resetSeekTimer(int64_t start_position_us) { void MediaPlayer::resetSeekTimer(int64_t start_position_us) {
if (seekTimerConnection.connected()) { if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect(); seekTimerConnection.disconnect();
} }
@@ -207,19 +212,20 @@ void MediaControlWidget::resetSeekTimer(int64_t start_position_us) {
setCurrentPosition(start_position_us); setCurrentPosition(start_position_us);
seekTimerConnection = Glib::signal_timeout().connect( seekTimerConnection = Glib::signal_timeout().connect(
sigc::mem_fun(*this, &MediaControlWidget::onSeekTick), sigc::mem_fun(*this, &MediaPlayer::onSeekTick),
1000); 1000);
} }
void MediaControlWidget::schedulePauseAfterSeek() { void MediaPlayer::schedulePauseAfterSeek() {
Glib::signal_timeout().connect_once([this]() { Glib::signal_timeout().connect_once([this]() {
if (this->playbackStatus != MprisController::PlaybackStatus::Playing) { if (this->playbackStatus != MprisController::PlaybackStatus::Playing) {
this->mprisController->pause(); this->mprisController->pause();
} }
}, 100); },
100);
} }
bool MediaControlWidget::onSeekTick() { bool MediaPlayer::onSeekTick() {
if (totalLengthUs <= 0) { if (totalLengthUs <= 0) {
return true; return true;
} }
@@ -232,7 +238,7 @@ bool MediaControlWidget::onSeekTick() {
return true; return true;
} }
void MediaControlWidget::onRunningStateChanged(MprisController::PlaybackStatus status) { void MediaPlayer::onRunningStateChanged(MprisController::PlaybackStatus status) {
this->playbackStatus = status; this->playbackStatus = status;
switch (status) { switch (status) {
case MprisController::PlaybackStatus::Playing: case MprisController::PlaybackStatus::Playing:
@@ -247,21 +253,22 @@ void MediaControlWidget::onRunningStateChanged(MprisController::PlaybackStatus s
} }
} }
void MediaControlWidget::onPlay() { void MediaPlayer::onPlay() {
this->playPauseButton.set_label("\u23F8"); // Pause symbol this->playPauseButton->setIcon(Icon::PAUSE);
// strart seek timer if not already running
this->resetSeekTimer(currentPositionUs); this->resetSeekTimer(currentPositionUs);
} }
void MediaControlWidget::onPause() { void MediaPlayer::onPause() {
this->playPauseButton.set_label("\u23F5"); // Play symbol this->playPauseButton->setIcon(Icon::PLAY_ARROW);
if (seekTimerConnection.connected()) { if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect(); seekTimerConnection.disconnect();
} }
} }
void MediaControlWidget::onStop() { void MediaPlayer::onStop() {
this->playPauseButton.set_label("\u23F9"); // stop symbol this->playPauseButton->setIcon(Icon::PLAY_ARROW);
if (seekTimerConnection.connected()) { if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect(); seekTimerConnection.disconnect();
} }

View File

@@ -0,0 +1,277 @@
#include "components/mediaPlayer.hpp"
#include "components/button/iconButton.hpp"
#include "helpers/string.hpp"
#include "services/textureCache.hpp"
namespace {
std::string formatTimeUs(int64_t time_us) {
if (time_us < 0) {
time_us = 0;
}
int64_t totalSeconds = time_us / 1000000;
int64_t hours = totalSeconds / 3600;
int64_t minutes = (totalSeconds / 60) % 60;
int64_t seconds = totalSeconds % 60;
if (hours > 0) {
return std::to_string(hours) + ":" +
(minutes < 10 ? "0" : "") + std::to_string(minutes) + ":" +
(seconds < 10 ? "0" : "") + std::to_string(seconds);
}
return std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds);
}
} // namespace
MediaPlayer::MediaPlayer(std::shared_ptr<MprisController> controller)
: Gtk::Box(Gtk::Orientation::VERTICAL), mprisController(controller) {
this->set_orientation(Gtk::Orientation::VERTICAL);
this->set_hexpand(false);
this->set_vexpand(false);
this->add_css_class("control-center-player-container");
this->append(this->topContainer);
this->append(this->seekBarContainer);
this->append(this->bottomContainer);
this->backgroundImage.set_content_fit(Gtk::ContentFit::COVER);
this->backgroundImage.set_can_shrink(true);
this->imageWrapper.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::NEVER);
this->imageWrapper.set_child(this->backgroundImage);
this->topContainer.set_child(this->imageWrapper);
this->topContainer.set_size_request(-1, 120);
this->topContainer.set_vexpand(false);
this->topContainer.set_hexpand(true);
this->infoContainer.set_orientation(Gtk::Orientation::VERTICAL);
this->infoContainer.set_valign(Gtk::Align::END);
this->infoContainer.set_halign(Gtk::Align::START);
this->infoContainer.append(this->artistLabel);
this->infoContainer.append(this->titleLabel);
this->topContainer.add_overlay(this->infoContainer);
this->artistLabel.set_halign(Gtk::Align::START);
this->titleLabel.set_halign(Gtk::Align::START);
this->seekBarContainer.set_orientation(Gtk::Orientation::HORIZONTAL);
this->seekBarContainer.set_vexpand(false);
this->seekBarContainer.set_hexpand(true);
this->seekBarContainer.set_halign(Gtk::Align::CENTER);
this->seekBarContainer.append(this->currentTimeLabel);
this->seekBarContainer.append(this->seekBar);
this->seekBarContainer.append(this->totalTimeLabel);
this->seekBarContainer.set_visible(true);
this->currentTimeLabel.set_text("0:00");
this->currentTimeLabel.set_halign(Gtk::Align::START);
this->totalTimeLabel.set_text("0:00");
this->totalTimeLabel.set_halign(Gtk::Align::END);
this->seekBar.set_range(0, 100);
this->seekBar.set_value(0);
this->seekBar.set_orientation(Gtk::Orientation::HORIZONTAL);
this->seekBar.set_draw_value(false);
this->seekBar.set_hexpand(true);
this->seekBar.set_halign(Gtk::Align::CENTER);
this->seekBar.add_css_class("control-center-seek-bar");
this->seekBar.signal_value_changed().connect([this]() {
if (this->suppressSeekSignal || this->totalLengthUs <= 0) {
return;
}
double fraction = this->seekBar.get_value() / 100.0;
int64_t new_position_us =
static_cast<int64_t>(fraction * static_cast<double>(this->totalLengthUs));
if (new_position_us == this->currentPositionUs) {
return;
}
if (!this->currentTrackId.empty()) {
this->mprisController->set_position(this->currentTrackId, new_position_us);
if (this->playbackStatus != MprisController::PlaybackStatus::Playing) {
this->schedulePauseAfterSeek();
}
} else if (this->playbackStatus == MprisController::PlaybackStatus::Playing) {
this->mprisController->emit_seeked(new_position_us - this->currentPositionUs); // in us
} else {
return;
}
if (this->playbackStatus == MprisController::PlaybackStatus::Playing) {
this->resetSeekTimer(new_position_us);
} else {
this->setCurrentPosition(new_position_us);
}
});
this->previousButton = std::make_unique<IconButton>(Icon::SKIP_PREVIOUS);
this->playPauseButton = std::make_unique<IconButton>(Icon::PLAY_ARROW);
this->nextButton = std::make_unique<IconButton>(Icon::SKIP_NEXT);
this->bottomContainer.set_orientation(Gtk::Orientation::HORIZONTAL);
this->bottomContainer.set_vexpand(false);
this->bottomContainer.set_hexpand(false);
this->bottomContainer.set_valign(Gtk::Align::START);
this->bottomContainer.set_homogeneous(true);
this->topContainer.set_vexpand(false);
this->topContainer.set_hexpand(true);
this->bottomContainer.append(*this->previousButton);
this->bottomContainer.append(*this->playPauseButton);
this->bottomContainer.append(*this->nextButton);
this->previousButton->signal_clicked().connect([this]() {
this->mprisController->previous_song();
});
this->playPauseButton->signal_clicked().connect([this]() {
this->mprisController->toggle_play();
});
this->nextButton->signal_clicked().connect([this]() {
this->mprisController->next_song();
});
this->mprisController->signal_mpris_updated().connect(
sigc::mem_fun(*this, &MediaPlayer::onSpotifyMprisUpdated));
this->mprisController->signal_playback_status_changed().connect(
[this](MprisController::PlaybackStatus status) {
this->onRunningStateChanged(status);
});
this->mprisController->signal_playback_position_changed().connect(
[this](int64_t position_us) {
this->setCurrentPosition(position_us);
});
this->mprisController->signal_can_seek_changed().connect(
[this](bool can_seek) {
this->setCanSeek(can_seek);
});
this->artistLabel.set_text("Artist Name");
this->artistLabel.add_css_class("control-center-player-artist-label");
this->titleLabel.set_text("Song Title");
this->titleLabel.add_css_class("control-center-player-title-label");
this->resetSeekTimer(0);
}
void MediaPlayer::setCanSeek(bool can_seek) {
this->canSeek = can_seek;
this->seekBarContainer.set_visible(can_seek);
}
void MediaPlayer::onSpotifyMprisUpdated(const MprisPlayer2Message &message) {
std::string artistText = "Unknown Artist";
if (!message.artist.empty()) {
artistText = StringHelper::trimToSize(message.artist[0], 30);
}
this->artistLabel.set_text(artistText);
this->titleLabel.set_text(message.title);
const bool trackChanged = !this->currentTrackId.empty() && this->currentTrackId != message.track_id;
this->currentTrackId = message.track_id;
if (trackChanged) {
this->currentPositionUs = 0;
}
if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) {
this->backgroundImage.set_paintable(texture);
}
this->setTotalLength(message.length_ms * 1000);
this->setCurrentPosition(this->currentPositionUs);
if (this->playbackStatus == MprisController::PlaybackStatus::Playing) {
this->resetSeekTimer(this->currentPositionUs);
} else if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect();
}
}
void MediaPlayer::setCurrentPosition(int64_t position_us) {
this->currentPositionUs = position_us;
this->currentTimeLabel.set_text(formatTimeUs(position_us));
if (totalLengthUs > 0) {
double fraction = static_cast<double>(currentPositionUs) / static_cast<double>(totalLengthUs);
this->suppressSeekSignal = true;
this->seekBar.set_value(fraction * 100);
this->suppressSeekSignal = false;
}
}
void MediaPlayer::setTotalLength(int64_t length_us) {
this->totalLengthUs = length_us;
this->totalTimeLabel.set_text(formatTimeUs(length_us));
}
void MediaPlayer::resetSeekTimer(int64_t start_position_us) {
if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect();
}
setCurrentPosition(start_position_us);
seekTimerConnection = Glib::signal_timeout().connect(
sigc::mem_fun(*this, &MediaPlayer::onSeekTick),
1000);
}
void MediaPlayer::schedulePauseAfterSeek() {
Glib::signal_timeout().connect_once([this]() {
if (this->playbackStatus != MprisController::PlaybackStatus::Playing) {
this->mprisController->pause();
}
},
100);
}
bool MediaPlayer::onSeekTick() {
if (totalLengthUs <= 0) {
return true;
}
int64_t nextPosition = currentPositionUs + 1000000;
if (nextPosition > totalLengthUs) {
nextPosition = totalLengthUs;
}
setCurrentPosition(nextPosition);
return true;
}
void MediaPlayer::onRunningStateChanged(MprisController::PlaybackStatus status) {
this->playbackStatus = status;
switch (status) {
case MprisController::PlaybackStatus::Playing:
this->onPlay();
break;
case MprisController::PlaybackStatus::Paused:
this->onPause();
break;
case MprisController::PlaybackStatus::Stopped:
this->onStop();
break;
}
}
void MediaPlayer::onPlay() {
this->playPauseButton->setIcon(Icon::PAUSE);
this->resetSeekTimer(currentPositionUs);
}
void MediaPlayer::onPause() {
this->playPauseButton->setIcon(Icon::PLAY_ARROW);
if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect();
}
}
void MediaPlayer::onStop() {
this->playPauseButton->setIcon(Icon::PLAY_ARROW);
if (seekTimerConnection.connected()) {
seekTimerConnection.disconnect();
}
this->setCurrentPosition(0);
}

View File

@@ -1,18 +1,23 @@
#include "components/popover.hpp" #include "components/popover.hpp"
Popover::Popover(const std::string icon, std::string name): Button(icon) { #include "components/button/iconButton.hpp"
Popover::Popover(Icon::Type icon, std::string name) : IconButton(icon) {
signal_clicked().connect(sigc::mem_fun(*this, &Popover::on_toggle_window)); signal_clicked().connect(sigc::mem_fun(*this, &Popover::on_toggle_window));
set_name(name); set_name(name);
this->add_css_class("material-icons");
popover = std::make_unique<Gtk::Popover>(); popover = std::make_unique<Gtk::Popover>();
popover->set_parent(*this); popover->set_parent(*this);
popover->set_autohide(true); popover->set_autohide(true);
} }
Popover::~Popover() = default; Popover::~Popover() {
if (popover) {
popover->popdown();
popover->unparent();
}
};
void Popover::on_toggle_window() { void Popover::on_toggle_window() {
if (popover->get_visible()) { if (popover->get_visible()) {
@@ -20,4 +25,4 @@ void Popover::on_toggle_window() {
} else { } else {
popover->popup(); popover->popup();
} }
} }

0
src/components/tab.cpp Normal file
View File

View File

@@ -6,7 +6,6 @@
WorkspaceIndicator::WorkspaceIndicator(int id, std::string label, sigc::slot<void(int)> onClick) WorkspaceIndicator::WorkspaceIndicator(int id, std::string label, sigc::slot<void(int)> onClick)
: Gtk::Box(Gtk::Orientation::HORIZONTAL) { : Gtk::Box(Gtk::Orientation::HORIZONTAL) {
overlay = std::make_shared<Gtk::Overlay>(); overlay = std::make_shared<Gtk::Overlay>();
auto numLabel = Gtk::make_managed<Gtk::Label>(label); auto numLabel = Gtk::make_managed<Gtk::Label>(label);
auto pillContainer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL); auto pillContainer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
@@ -41,7 +40,7 @@ WorkspaceIndicator::WorkspaceIndicator(int id, std::string label, sigc::slot<voi
void WorkspaceIndicator::setIndicatorState(InidicatorState state) { void WorkspaceIndicator::setIndicatorState(InidicatorState state) {
this->clearCssClass(); this->clearCssClass();
this->currentState = state; this->currentState = state;
auto cssClass = this->stateToCssClass[state]; auto cssClass = this->stateToCssClass[state];
this->overlay->add_css_class(cssClass); this->overlay->add_css_class(cssClass);
} }

View File

@@ -0,0 +1,768 @@
#include "connection/dbus/bluetooth.hpp"
#include "services/notificationController.hpp"
#include <spdlog/spdlog.h>
#include <iomanip>
#include <sstream>
const std::string agent_introspection_xml = R"(
<node>
<interface name="org.bluez.Agent1">
<method name="Release"/>
<method name="RequestPinCode">
<arg type="o" name="device" direction="in"/>
<arg type="s" name="pincode" direction="out"/>
</method>
<method name="DisplayPinCode">
<arg type="o" name="device" direction="in"/>
<arg type="s" name="pincode" direction="in"/>
</method>
<method name="RequestPasskey">
<arg type="o" name="device" direction="in"/>
<arg type="u" name="passkey" direction="out"/>
</method>
<method name="DisplayPasskey">
<arg type="o" name="device" direction="in"/>
<arg type="u" name="passkey" direction="in"/>
<arg type="q" name="entered" direction="in"/>
</method>
<method name="RequestConfirmation">
<arg type="o" name="device" direction="in"/>
<arg type="u" name="passkey" direction="in"/>
</method>
<method name="RequestAuthorization">
<arg type="o" name="device" direction="in"/>
</method>
<method name="AuthorizeService">
<arg type="o" name="device" direction="in"/>
<arg type="s" name="uuid" direction="in"/>
</method>
<method name="Cancel"/>
</interface>
</node>
)";
BluetoothController::BluetoothController() {
connect_system_async(sigc::mem_fun(*this, &BluetoothController::onBusConnected));
}
sigc::signal<void(bool)> &BluetoothController::signalPoweredChanged() { return m_powered_signal; }
sigc::signal<void(bool)> &BluetoothController::signalDiscoveringChanged() { return m_discovering_signal; }
sigc::signal<void(const BluetoothDevice &)> &BluetoothController::signalDeviceAdded() { return m_device_added_signal; }
sigc::signal<void(const std::string &)> &BluetoothController::signalDeviceRemoved() { return m_device_removed_signal; }
sigc::signal<void(const BluetoothDevice &)> &BluetoothController::signalDeviceChanged() { return m_device_changed_signal; }
bool BluetoothController::isPowered() const { return m_powered; }
bool BluetoothController::isDiscovering() const { return m_discovering; }
std::vector<BluetoothDevice> BluetoothController::getDevices() const {
std::vector<BluetoothDevice> result;
result.reserve(m_devices.size());
for (const auto &[_, device] : m_devices) {
result.push_back(device);
}
return result;
}
std::vector<BluetoothDevice> BluetoothController::getPairedDevices() const {
std::vector<BluetoothDevice> result;
for (const auto &[_, device] : m_devices) {
if (device.paired) {
result.push_back(device);
}
}
return result;
}
void BluetoothController::setPowered(bool powered) {
spdlog::info("Bluetooth: setting powered to {}", powered);
setDbusProperty(m_adapter_path, "org.bluez.Adapter1", "Powered",
Glib::Variant<bool>::create(powered));
}
void BluetoothController::startDiscovery() {
spdlog::info("Bluetooth: starting discovery");
if (m_adapter_proxy) {
m_adapter_proxy->call("StartDiscovery");
}
}
void BluetoothController::stopDiscovery() {
spdlog::info("Bluetooth: stopping discovery");
if (m_adapter_proxy) {
m_adapter_proxy->call("StopDiscovery");
}
}
void BluetoothController::pairDevice(const std::string &object_path) {
spdlog::info("Bluetooth: pairing device {}", object_path);
auto it = m_device_proxies.find(object_path);
if (it != m_device_proxies.end() && it->second) {
it->second->call("Pair");
}
}
void BluetoothController::unpairDevice(const std::string &object_path) {
spdlog::info("Bluetooth: unpairing device {}", object_path);
if (!m_adapter_proxy)
return;
auto params = Glib::VariantContainerBase::create_tuple(
Glib::Variant<Glib::DBusObjectPathString>::create(object_path));
m_adapter_proxy->call("RemoveDevice", params);
}
void BluetoothController::connectDevice(const std::string &object_path) {
spdlog::info("Bluetooth: connecting device {}", object_path);
auto it = m_device_proxies.find(object_path);
if (it != m_device_proxies.end() && it->second) {
it->second->call("Connect");
}
}
void BluetoothController::disconnectDevice(const std::string &object_path) {
spdlog::info("Bluetooth: disconnecting device {}", object_path);
auto it = m_device_proxies.find(object_path);
if (it != m_device_proxies.end() && it->second) {
it->second->call("Disconnect");
}
}
void BluetoothController::trustDevice(const std::string &object_path, bool trusted) {
spdlog::info("Bluetooth: setting trusted={} for device {}", trusted, object_path);
setDbusProperty(object_path, "org.bluez.Device1", "Trusted",
Glib::Variant<bool>::create(trusted));
}
// ---------------------------------------------------------------------------
// Bus connection
// ---------------------------------------------------------------------------
void BluetoothController::onBusConnected(const Glib::RefPtr<Gio::AsyncResult> &result) {
if (!result) {
spdlog::error("Bluetooth: null async result");
return;
}
try {
connection = Gio::DBus::Connection::get_finish(result);
m_object_manager_proxy = Gio::DBus::Proxy::create_sync(
connection,
"org.bluez",
"/",
"org.freedesktop.DBus.ObjectManager");
if (m_object_manager_proxy) {
m_object_manager_proxy->signal_signal().connect(
sigc::mem_fun(*this, &BluetoothController::onObjectManagerSignal));
enumerateObjects();
}
registerAgent();
} catch (const Glib::Error &ex) {
spdlog::error("Bluetooth DBus Connection Error: {}", ex.what());
}
}
// ---------------------------------------------------------------------------
// Enumerate existing BlueZ objects
// ---------------------------------------------------------------------------
void BluetoothController::enumerateObjects() {
if (!m_object_manager_proxy)
return;
try {
auto result = m_object_manager_proxy->call_sync("GetManagedObjects");
auto objects = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(
result.get_child(0));
for (gsize i = 0; i < objects.get_n_children(); i++) {
auto entry = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(
objects.get_child(i));
auto path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
entry.get_child(0))
.get());
parseInterfaces(path, entry.get_child(1));
}
} catch (const Glib::Error &ex) {
spdlog::error("Bluetooth: Error enumerating objects: {}", ex.what());
}
}
// ---------------------------------------------------------------------------
// Parse an interfaces-and-properties dict a{sa{sv}}
// ---------------------------------------------------------------------------
void BluetoothController::parseInterfaces(const std::string &object_path,
const Glib::VariantBase &interfaces_var) {
auto ifaces = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(interfaces_var);
for (gsize i = 0; i < ifaces.get_n_children(); i++) {
auto entry = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(
ifaces.get_child(i));
auto iface_name = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(
entry.get_child(0))
.get());
if (iface_name != "org.bluez.Device1" && iface_name != "org.bluez.Adapter1") {
continue;
}
auto props = Glib::VariantBase::cast_dynamic<Glib::Variant<PropertiesMap>>(
entry.get_child(1))
.get();
if (iface_name == "org.bluez.Device1") {
addDevice(object_path, props);
} else {
setupAdapter(object_path, props);
}
}
}
// ---------------------------------------------------------------------------
// ObjectManager signal handler (InterfacesAdded / InterfacesRemoved)
// ---------------------------------------------------------------------------
void BluetoothController::onObjectManagerSignal(
const Glib::ustring &,
const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters) {
if (signal_name == "InterfacesAdded") {
// signature: (oa{sa{sv}})
auto path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0))
.get());
parseInterfaces(path, parameters.get_child(1));
} else if (signal_name == "InterfacesRemoved") {
// signature: (oas)
auto path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0))
.get());
auto ifaces = Glib::VariantBase::cast_dynamic<
Glib::Variant<std::vector<Glib::ustring>>>(parameters.get_child(1))
.get();
for (const auto &iface : ifaces) {
if (iface == "org.bluez.Device1") {
removeDevice(path);
} else if (iface == "org.bluez.Adapter1" && path == m_adapter_path) {
m_adapter_proxy.reset();
m_adapter_path.clear();
m_powered = false;
m_discovering = false;
m_powered_signal.emit(false);
m_discovering_signal.emit(false);
spdlog::info("Bluetooth adapter removed");
}
}
}
}
// ---------------------------------------------------------------------------
// Adapter
// ---------------------------------------------------------------------------
void BluetoothController::setupAdapter(const std::string &path,
const PropertiesMap &properties) {
m_adapter_path = path;
spdlog::info("Bluetooth adapter found: {}", path);
auto it = properties.find("Powered");
if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) {
m_powered = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(it->second).get();
m_powered_signal.emit(m_powered);
}
it = properties.find("Discovering");
if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) {
m_discovering = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(it->second).get();
m_discovering_signal.emit(m_discovering);
}
try {
m_adapter_proxy = Gio::DBus::Proxy::create_sync(
connection, "org.bluez", path, "org.bluez.Adapter1");
if (m_adapter_proxy) {
m_adapter_proxy->signal_properties_changed().connect(
sigc::mem_fun(*this, &BluetoothController::onAdapterPropertiesChanged));
}
} catch (const Glib::Error &ex) {
spdlog::error("Bluetooth: Error creating adapter proxy: {}", ex.what());
}
}
void BluetoothController::onAdapterPropertiesChanged(
const Gio::DBus::Proxy::MapChangedProperties &changed,
const std::vector<Glib::ustring> &) {
auto it = changed.find("Powered");
if (it != changed.end() && it->second.is_of_type(Glib::VariantType("b"))) {
m_powered = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(it->second).get();
spdlog::info("Bluetooth powered: {}", m_powered);
m_powered_signal.emit(m_powered);
}
it = changed.find("Discovering");
if (it != changed.end() && it->second.is_of_type(Glib::VariantType("b"))) {
m_discovering = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(it->second).get();
spdlog::info("Bluetooth discovering: {}", m_discovering);
m_discovering_signal.emit(m_discovering);
}
}
// ---------------------------------------------------------------------------
// Device management
// ---------------------------------------------------------------------------
void BluetoothController::addDevice(const std::string &path,
const PropertiesMap &properties) {
auto device = parseDeviceProperties(path, properties);
m_devices[path] = device;
spdlog::info("Bluetooth device added: {} ({}) {}", device.name, device.address, device.icon);
try {
auto proxy = Gio::DBus::Proxy::create_sync(
connection, "org.bluez", path, "org.bluez.Device1");
if (proxy) {
proxy->signal_properties_changed().connect(
[this, path](const Gio::DBus::Proxy::MapChangedProperties &changed,
const std::vector<Glib::ustring> &invalidated) {
onDevicePropertiesChanged(path, changed, invalidated);
});
m_device_proxies[path] = proxy;
}
} catch (const Glib::Error &ex) {
spdlog::error("Bluetooth: Error creating device proxy for {}: {}", path, ex.what());
}
m_device_added_signal.emit(device);
}
void BluetoothController::removeDevice(const std::string &path) {
spdlog::info("Bluetooth device removed: {}", path);
m_devices.erase(path);
m_device_proxies.erase(path);
m_device_removed_signal.emit(path);
}
void BluetoothController::onDevicePropertiesChanged(
const std::string &object_path,
const Gio::DBus::Proxy::MapChangedProperties &changed,
const std::vector<Glib::ustring> &) {
auto it = m_devices.find(object_path);
if (it == m_devices.end())
return;
auto &device = it->second;
for (const auto &[key, value] : changed) {
if (key == "Name" && value.is_of_type(Glib::VariantType("s"))) {
device.name = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(value).get());
} else if (key == "Alias" && value.is_of_type(Glib::VariantType("s"))) {
// Only use Alias as fallback if Name is still the MAC address
auto alias = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(value).get());
if (device.name == device.address) {
device.name = alias;
}
} else if (key == "Paired" && value.is_of_type(Glib::VariantType("b"))) {
device.paired = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(value).get();
} else if (key == "Connected" && value.is_of_type(Glib::VariantType("b"))) {
device.connected = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(value).get();
} else if (key == "Trusted" && value.is_of_type(Glib::VariantType("b"))) {
device.trusted = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(value).get();
} else if (key == "RSSI" && value.is_of_type(Glib::VariantType("n"))) {
device.rssi = Glib::VariantBase::cast_dynamic<Glib::Variant<gint16>>(value).get();
} else if (key == "Icon" && value.is_of_type(Glib::VariantType("s"))) {
device.icon = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(value).get());
}
}
m_device_changed_signal.emit(device);
}
// ---------------------------------------------------------------------------
// Property parsing helper
// ---------------------------------------------------------------------------
BluetoothDevice BluetoothController::parseDeviceProperties(
const std::string &path,
const PropertiesMap &properties) {
BluetoothDevice device;
device.object_path = path;
auto getString = [&](const Glib::ustring &key) -> std::string {
auto it = properties.find(key);
if (it != properties.end() && it->second.is_of_type(Glib::VariantType("s"))) {
return static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(it->second).get());
}
return "";
};
auto getBool = [&](const Glib::ustring &key) -> bool {
auto it = properties.find(key);
if (it != properties.end() && it->second.is_of_type(Glib::VariantType("b"))) {
return Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(it->second).get();
}
return false;
};
device.address = getString("Address");
device.name = getString("Name");
if (device.name.empty())
device.name = getString("Alias");
if (device.name.empty())
device.name = device.address;
device.icon = getString("Icon");
device.paired = getBool("Paired");
device.connected = getBool("Connected");
device.trusted = getBool("Trusted");
auto rssi_it = properties.find("RSSI");
if (rssi_it != properties.end() && rssi_it->second.is_of_type(Glib::VariantType("n"))) {
device.rssi = Glib::VariantBase::cast_dynamic<Glib::Variant<gint16>>(rssi_it->second).get();
}
return device;
}
// ---------------------------------------------------------------------------
// D-Bus property setter (calls org.freedesktop.DBus.Properties.Set)
// ---------------------------------------------------------------------------
void BluetoothController::setDbusProperty(const std::string &object_path,
const std::string &interface,
const std::string &property,
const Glib::VariantBase &value) {
if (!connection || object_path.empty())
return;
try {
auto props_proxy = Gio::DBus::Proxy::create_sync(
connection, "org.bluez", object_path, "org.freedesktop.DBus.Properties");
auto wrapped = Glib::Variant<Glib::VariantBase>::create(value);
auto params = Glib::VariantContainerBase::create_tuple({Glib::Variant<Glib::ustring>::create(interface),
Glib::Variant<Glib::ustring>::create(property),
wrapped});
props_proxy->call_sync("Set", params);
} catch (const Glib::Error &ex) {
spdlog::error("Error setting {}.{}: {}", interface, property, ex.what());
}
}
// ---------------------------------------------------------------------------
// Agent
// ---------------------------------------------------------------------------
void BluetoothController::registerAgent() {
if (!connection)
return;
try {
m_node_info = Gio::DBus::NodeInfo::create_for_xml(agent_introspection_xml);
auto interface_info = m_node_info->lookup_interface("org.bluez.Agent1");
m_interface_vtable = std::make_shared<Gio::DBus::InterfaceVTable>(
sigc::mem_fun(*this, &BluetoothController::on_agent_method_call));
m_agent_id = connection->register_object("/org/bluez/bar_agent", interface_info, *m_interface_vtable);
auto agent_manager = Gio::DBus::Proxy::create_sync(
connection, "org.bluez", "/org/bluez", "org.bluez.AgentManager1");
if (agent_manager) {
agent_manager->call_sync(
"RegisterAgent",
Glib::VariantContainerBase::create_tuple(
{Glib::Variant<Glib::DBusObjectPathString>::create("/org/bluez/bar_agent"),
Glib::Variant<Glib::ustring>::create("DisplayYesNo")}));
agent_manager->call_sync(
"RequestDefaultAgent",
Glib::VariantContainerBase::create_tuple(
{Glib::Variant<Glib::DBusObjectPathString>::create("/org/bluez/bar_agent")}));
spdlog::info("Bluetooth Agent registered successfully");
}
} catch (const Glib::Error &ex) {
spdlog::error("Bluetooth: Failed to register agent: {}", ex.what());
}
}
void BluetoothController::on_agent_method_call(
const Glib::RefPtr<Gio::DBus::Connection> &,
const Glib::ustring &,
const Glib::ustring &,
const Glib::ustring &,
const Glib::ustring &method_name,
const Glib::VariantContainerBase &parameters,
const Glib::RefPtr<Gio::DBus::MethodInvocation> &invocation) {
spdlog::info("Bluetooth Agent method called: {}", method_name.c_str());
if (method_name == "RequestConfirmation") {
// Signature: (ou)
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
guint32 passkey = Glib::VariantBase::cast_dynamic<Glib::Variant<guint32>>(
parameters.get_child(1)).get();
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
std::ostringstream ss;
ss << std::setw(6) << std::setfill('0') << passkey;
std::string passkey_str = ss.str();
NotifyMessage msg;
msg.summary = "Bluetooth Pairing Request";
msg.body = "Device '" + device_name + "' wants to pair.\nPasskey: " + passkey_str;
msg.app_name = "Bar";
msg.actions = {"Confirm", "Confirm", "Cancel", "Cancel"};
msg.urgency = NotificationUrgency::CRITICAL;
msg.expire_timeout = -1; // No timeout, requires user interaction
// Capture invocation to respond later
// Note: invocation is a RefPtr, so copying it increases ref count
msg.on_action = [invocation](const std::string &action) {
if (action == "Confirm") {
invocation->return_value(Glib::VariantContainerBase());
} else {
invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user");
}
};
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
} else if (method_name == "RequestAuthorization") {
// Signature: (o)
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
NotifyMessage msg;
msg.summary = "Bluetooth Authorization Request";
msg.body = "Device '" + device_name + "' requests authorization to connect.";
msg.app_name = "Bar";
msg.actions = {"Allow", "Allow", "Deny", "Deny"};
msg.urgency = NotificationUrgency::LOW;
msg.expire_timeout = -1;
msg.on_action = [invocation](const std::string &action) {
if (action == "Allow") {
invocation->return_value(Glib::VariantContainerBase());
} else {
invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user");
}
};
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
} else if (method_name == "AuthorizeService") {
// Signature: (os)
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
std::string uuid = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(
parameters.get_child(1)).get());
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
NotifyMessage msg;
msg.summary = "Bluetooth Service Authorization";
msg.body = "Device '" + device_name + "' wants to access service: " + uuid;
msg.app_name = "Bar";
msg.actions = {"Allow", "Allow", "Deny", "Deny"};
msg.urgency = NotificationUrgency::CRITICAL;
msg.expire_timeout = -1;
msg.on_action = [invocation](const std::string &action) {
if (action == "Allow") {
invocation->return_value(Glib::VariantContainerBase());
} else {
invocation->return_dbus_error("org.bluez.Error.Rejected", "Rejected by user");
}
};
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
} else if (method_name == "DisplayPinCode") {
// Signature: (os)
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
std::string pincode = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(
parameters.get_child(1)).get());
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
NotifyMessage msg;
msg.summary = "Bluetooth Pin Code";
msg.body = "Pin code for device '" + device_name + "': " + pincode;
msg.app_name = "Bar";
msg.actions = {"Dismiss"};
msg.urgency = NotificationUrgency::NORMAL;
msg.expire_timeout = 0;
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
invocation->return_value(Glib::VariantContainerBase());
} else if (method_name == "DisplayPasskey") {
// Signature: (ouq)
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
guint32 passkey = Glib::VariantBase::cast_dynamic<Glib::Variant<guint32>>(
parameters.get_child(1)).get();
guint16 entered = Glib::VariantBase::cast_dynamic<Glib::Variant<guint16>>(
parameters.get_child(2)).get();
// Only show on first display (entered == 0) to avoid spam
if (entered == 0) {
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
std::ostringstream ss;
ss << std::setw(6) << std::setfill('0') << passkey;
std::string passkey_str = ss.str();
NotifyMessage msg;
msg.summary = "Bluetooth Passkey";
msg.body = "Type this passkey on '" + device_name + "': " + passkey_str;
msg.app_name = "Bar";
msg.actions = {"Dismiss"};
msg.urgency = NotificationUrgency::CRITICAL;
msg.expire_timeout = -1;
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
}
invocation->return_value(Glib::VariantContainerBase());
} else if (method_name == "RequestPinCode") {
// Signature: (o) -> s
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
NotifyMessage msg;
msg.summary = "Bluetooth PIN Request";
msg.body = "Enter PIN Code for '" + device_name + "'";
msg.app_name = "Bar";
msg.urgency = NotificationUrgency::CRITICAL;
msg.expire_timeout = -1;
msg.has_input = true;
msg.input_placeholder = "PIN Code";
msg.on_input = [invocation](const std::string &input) {
invocation->return_value(
Glib::VariantContainerBase::create_tuple(
{Glib::Variant<Glib::ustring>::create(input)}));
};
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
} else if (method_name == "RequestPasskey") {
// Signature: (o) -> u
std::string device_path = static_cast<std::string>(
Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::DBusObjectPathString>>(
parameters.get_child(0)).get());
std::string device_name = device_path;
if (m_devices.count(device_path)) {
device_name = m_devices[device_path].name;
if (device_name.empty())
device_name = m_devices[device_path].address;
}
NotifyMessage msg;
msg.summary = "Bluetooth Passkey Request";
msg.body = "Enter Passkey for '" + device_name + "'";
msg.app_name = "Bar";
msg.urgency = NotificationUrgency::CRITICAL;
msg.expire_timeout = -1;
msg.has_input = true;
msg.input_placeholder = "Passkey (0-999999)";
msg.on_input = [invocation](const std::string &input) {
try {
uint32_t passkey = std::stoul(input);
invocation->return_value(
Glib::VariantContainerBase::create_tuple(
{Glib::Variant<guint32>::create(passkey)}));
} catch (...) {
invocation->return_dbus_error("org.bluez.Error.Failed", "Invalid passkey format");
}
};
NotificationController::getInstance()->showNotificationOnAllMonitors(msg);
} else if (method_name == "Cancel" || method_name == "Release") {
// Just return
invocation->return_value(Glib::VariantContainerBase());
} else {
// Not implemented
invocation->return_dbus_error("org.bluez.Error.Rejected", "Not implemented");
}
}

View File

@@ -10,7 +10,10 @@
#include "giomm/dbusproxy.h" #include "giomm/dbusproxy.h"
std::shared_ptr<MprisController> MprisController::getInstance() { std::shared_ptr<MprisController> MprisController::getInstance() {
static std::shared_ptr<MprisController> instance = std::shared_ptr<MprisController>(new MprisController()); static std::shared_ptr<MprisController> instance;
if (!instance) {
instance = std::shared_ptr<MprisController>(new MprisController());
}
return instance; return instance;
} }
@@ -18,7 +21,7 @@ std::shared_ptr<MprisController> MprisController::createForPlayer(const std::str
return std::shared_ptr<MprisController>(new MprisController(bus_name)); return std::shared_ptr<MprisController>(new MprisController(bus_name));
} }
MprisController::MprisController() { MprisController::MprisController() : m_player_bus_name() {
connect_session_async(sigc::mem_fun(*this, &MprisController::on_bus_connected)); connect_session_async(sigc::mem_fun(*this, &MprisController::on_bus_connected));
} }
@@ -63,6 +66,33 @@ void MprisController::on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &res
try { try {
connection = Gio::DBus::Connection::get_finish(result); connection = Gio::DBus::Connection::get_finish(result);
// Per-player instances only need to create their own player proxy,
// not monitor all players on the bus.
if (!m_player_bus_name.empty()) {
if (!m_proxy) {
try {
m_proxy = Gio::DBus::Proxy::create_sync(
connection,
m_player_bus_name,
"/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player");
if (m_proxy) {
m_proxy->signal_properties_changed().connect(
sigc::mem_fun(*this, &MprisController::on_properties_changed));
signalNotification();
emit_cached_playback_status();
emit_cached_position();
emit_cached_can_seek();
}
} catch (const Glib::Error &ex) {
spdlog::error("DBus Connection Error for player {}: {}", m_player_bus_name, ex.what());
}
}
return;
}
// Singleton instance: monitor all MPRIS players on the bus.
if (!m_dbus_proxy) { if (!m_dbus_proxy) {
m_dbus_proxy = Gio::DBus::Proxy::create_sync( m_dbus_proxy = Gio::DBus::Proxy::create_sync(
connection, connection,
@@ -76,8 +106,8 @@ void MprisController::on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &res
try { try {
auto list_names_result = m_dbus_proxy->call_sync("ListNames"); auto list_names_result = m_dbus_proxy->call_sync("ListNames");
auto names_variant = Glib::VariantBase::cast_dynamic< auto names_variant = Glib::VariantBase::cast_dynamic<
Glib::Variant<std::vector<Glib::ustring>>>(list_names_result.get_child(0)); Glib::Variant<std::vector<Glib::ustring>>>(list_names_result.get_child(0));
for (const auto &name : names_variant.get()) { for (const auto &name : names_variant.get()) {
const std::string bus_name = name; const std::string bus_name = name;
@@ -103,7 +133,7 @@ void MprisController::on_dbus_signal(const Glib::ustring &,
return; return;
} }
auto name_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(0)); auto name_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(0));
auto old_owner_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(1)); auto old_owner_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(1));
auto new_owner_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(2)); auto new_owner_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(2));
@@ -270,7 +300,7 @@ void MprisController::emit_cached_playback_status() {
return; return;
} }
auto status = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(status_var).get(); auto status = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(status_var).get();
auto parsedStatusIt = playbackStatusMap.find(static_cast<std::string>(status)); auto parsedStatusIt = playbackStatusMap.find(static_cast<std::string>(status));
if (parsedStatusIt != playbackStatusMap.end()) { if (parsedStatusIt != playbackStatusMap.end()) {
currentPlaybackStatus = parsedStatusIt->second; currentPlaybackStatus = parsedStatusIt->second;
@@ -398,10 +428,8 @@ void MprisController::set_position(const std::string &track_id, int64_t position
} }
try { try {
Glib::VariantContainerBase params = Glib::VariantContainerBase::create_tuple({ Glib::VariantContainerBase params = Glib::VariantContainerBase::create_tuple({Glib::Variant<Glib::DBusObjectPathString>::create(track_id),
Glib::Variant<Glib::DBusObjectPathString>::create(track_id), Glib::Variant<gint64>::create(position_us)});
Glib::Variant<gint64>::create(position_us)
});
m_proxy->call("SetPosition", params); m_proxy->call("SetPosition", params);
} catch (const Glib::Error &ex) { } catch (const Glib::Error &ex) {

View File

@@ -160,11 +160,11 @@ void NotificationService::handle_notify(const Glib::VariantContainerBase &parame
notify.expire_timeout = expire_timeout; notify.expire_timeout = expire_timeout;
guint id = notificationIdCounter++; guint id = notificationIdCounter++;
if (app_name == "image-copy" ) { if (app_name == "image-copy") {
NotificationController::getInstance()->showCopyNotification(notify); NotificationController::getInstance()->showCopyNotification(notify);
invocation->return_value(Glib::VariantContainerBase::create_tuple( invocation->return_value(Glib::VariantContainerBase::create_tuple(
Glib::Variant<guint>::create(id))); Glib::Variant<guint>::create(id)));
return; return;
} }
@@ -173,7 +173,7 @@ void NotificationService::handle_notify(const Glib::VariantContainerBase &parame
invocation->return_value(Glib::VariantContainerBase::create_tuple( invocation->return_value(Glib::VariantContainerBase::create_tuple(
Glib::Variant<guint>::create(id))); Glib::Variant<guint>::create(id)));
return; return;
} }
if (app_name == "Thunderbird") { if (app_name == "Thunderbird") {
notify.expire_timeout = 10000; // 10 seconds for email notifications notify.expire_timeout = 10000; // 10 seconds for email notifications

View File

@@ -9,8 +9,8 @@
#include <giomm/dbusactiongroup.h> #include <giomm/dbusactiongroup.h>
#include <giomm/dbusownname.h> #include <giomm/dbusownname.h>
#include <giomm/menumodel.h> #include <giomm/menumodel.h>
#include <spdlog/spdlog.h>
#include <memory> #include <memory>
#include <spdlog/spdlog.h>
#include <tuple> #include <tuple>
#include <vector> #include <vector>
@@ -128,7 +128,7 @@ void on_simple_call_finished(GObject *source, GAsyncResult *res,
std::unique_ptr<SimpleCallData> data( std::unique_ptr<SimpleCallData> data(
static_cast<SimpleCallData *>(user_data)); static_cast<SimpleCallData *>(user_data));
GError *error = nullptr; GError *error = nullptr;
GVariant *reply = GVariant *reply =
g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error);
@@ -358,8 +358,8 @@ void TrayService::activate(const std::string &id, int32_t x, int32_t y) {
return; return;
} }
auto data = new SimpleCallData(); auto data = new SimpleCallData();
data->debugLabel = "Activate(" + id + ")"; data->debugLabel = "Activate(" + id + ")";
data->ignoreUnknownMethod = false; data->ignoreUnknownMethod = false;
g_dbus_connection_call( g_dbus_connection_call(
connection->gobj(), it->second->publicData.busName.c_str(), connection->gobj(), it->second->publicData.busName.c_str(),
@@ -375,8 +375,8 @@ void TrayService::secondaryActivate(const std::string &id, int32_t x,
return; return;
} }
auto data = new SimpleCallData(); auto data = new SimpleCallData();
data->debugLabel = "SecondaryActivate(" + id + ")"; data->debugLabel = "SecondaryActivate(" + id + ")";
data->ignoreUnknownMethod = false; data->ignoreUnknownMethod = false;
g_dbus_connection_call( g_dbus_connection_call(
connection->gobj(), it->second->publicData.busName.c_str(), connection->gobj(), it->second->publicData.busName.c_str(),
@@ -473,7 +473,7 @@ void on_menu_layout_finished(GObject *source, GAsyncResult *res,
return; return;
} }
GError *error = nullptr; GError *error = nullptr;
GVariant *reply = GVariant *reply =
g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error);
@@ -823,7 +823,7 @@ struct RefreshCallData {
}; };
void TrayService::on_refresh_finished_static(GObject *source, GAsyncResult *res, void TrayService::on_refresh_finished_static(GObject *source, GAsyncResult *res,
gpointer user_data) { gpointer user_data) {
std::unique_ptr<RefreshCallData> data( std::unique_ptr<RefreshCallData> data(
static_cast<RefreshCallData *>(user_data)); static_cast<RefreshCallData *>(user_data));
if (!data || !data->self) { if (!data || !data->self) {
@@ -837,7 +837,7 @@ void TrayService::on_refresh_finished_static(GObject *source, GAsyncResult *res,
auto &tracked = *it->second; auto &tracked = *it->second;
GError *error = nullptr; GError *error = nullptr;
GVariant *reply = GVariant *reply =
g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error);
if (!reply) { if (!reply) {
@@ -919,9 +919,9 @@ void TrayService::on_refresh_finished_static(GObject *source, GAsyncResult *res,
g_variant_unref(dictVariant); g_variant_unref(dictVariant);
const bool menuPathChanged = (tracked.publicData.menuPath != menuPath); const bool menuPathChanged = (tracked.publicData.menuPath != menuPath);
tracked.publicData.title = title; tracked.publicData.title = title;
tracked.publicData.status = status; tracked.publicData.status = status;
tracked.publicData.menuPath = menuPath; tracked.publicData.menuPath = menuPath;
tracked.publicData.menuAvailable = !menuPath.empty(); tracked.publicData.menuAvailable = !menuPath.empty();

View File

@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <curl/curl.h> #include <curl/curl.h>
#include <ranges>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -17,20 +18,20 @@ size_t write_to_string(void *contents, size_t size, size_t nmemb, void *userp) {
std::string trim(std::string value) { std::string trim(std::string value) {
auto not_space = [](unsigned char c) { return std::isspace(c) == 0; }; auto not_space = [](unsigned char c) { return std::isspace(c) == 0; };
value.erase(value.begin(), value.erase(value.begin(),
std::find_if(value.begin(), value.end(), not_space)); std::ranges::find_if(value, not_space));
value.erase(std::find_if(value.rbegin(), value.rend(), not_space).base(), value.erase(std::ranges::find_if(std::ranges::reverse_view(value), not_space).base(),
value.end()); value.end());
return value; return value;
} }
size_t header_to_map(char *buffer, size_t size, size_t nitems, void *userdata) { size_t header_to_map(char *buffer, size_t size, size_t nitems, void *userdata) {
size_t total = size * nitems; size_t total = size * nitems;
auto *header_map = static_cast<std::map<std::string, std::string> *>(userdata); auto *header_map = static_cast<std::map<std::string, std::string> *>(userdata);
std::string line(buffer, total); std::string line(buffer, total);
auto colon = line.find(':'); auto colon = line.find(':');
if (colon != std::string::npos) { if (colon != std::string::npos) {
auto key = trim(line.substr(0, colon)); auto key = trim(line.substr(0, colon));
auto value = trim(line.substr(colon + 1)); auto value = trim(line.substr(colon + 1));
if (!key.empty()) { if (!key.empty()) {
header_map->insert_or_assign(std::move(key), std::move(value)); header_map->insert_or_assign(std::move(key), std::move(value));
@@ -38,19 +39,19 @@ size_t header_to_map(char *buffer, size_t size, size_t nitems, void *userdata) {
} }
return total; return total;
} }
} } // namespace
HttpResponse HttpConnection::get(const std::string &url, HttpResponse HttpConnection::get(const std::string &url,
const std::map<std::string, std::string> &headers, const std::map<std::string, std::string> &headers,
long timeout_ms) { long timeout_ms) {
return performRequest("GET", url, std::string(), headers, std::string(), timeout_ms); return performRequest("GET", url, std::string(), headers, std::string(), timeout_ms);
} }
HttpResponse HttpConnection::post(const std::string &url, HttpResponse HttpConnection::post(const std::string &url,
const std::string &body, const std::string &body,
const std::map<std::string, std::string> &headers, const std::map<std::string, std::string> &headers,
const std::string &content_type, const std::string &content_type,
long timeout_ms) { long timeout_ms) {
return performRequest("POST", url, body, headers, content_type, timeout_ms); return performRequest("POST", url, body, headers, content_type, timeout_ms);
} }
@@ -91,12 +92,12 @@ HttpResponse HttpConnection::performRequest(const std::string &method,
struct curl_slist *header_list = nullptr; struct curl_slist *header_list = nullptr;
for (const auto &pair : headers) { for (const auto &pair : headers) {
std::string header = pair.first + ": " + pair.second; std::string header = pair.first + ": " + pair.second;
header_list = curl_slist_append(header_list, header.c_str()); header_list = curl_slist_append(header_list, header.c_str());
} }
if (method == "POST" && !content_type.empty()) { if (method == "POST" && !content_type.empty()) {
std::string content_header = "Content-Type: " + content_type; std::string content_header = "Content-Type: " + content_type;
header_list = curl_slist_append(header_list, content_header.c_str()); header_list = curl_slist_append(header_list, content_header.c_str());
} }
if (header_list) { if (header_list) {

View File

@@ -1,5 +1,6 @@
#include "services/hyprland.hpp" #include "services/hyprland.hpp"
#include <algorithm>
#include <cstring> #include <cstring>
#include <glib-unix.h> #include <glib-unix.h>
#include <glib.h> #include <glib.h>
@@ -14,10 +15,12 @@
#include "helpers/string.hpp" #include "helpers/string.hpp"
#include "gtkmm/box.h" #include "gtkmm/box.h"
#include "spdlog/spdlog.h" #include "spdlog/spdlog.h"
HyprlandService::HyprlandService() { HyprlandService::HyprlandService() {
if (instance) {
throw std::runtime_error("HyprlandService instance already exists");
}
init(); init();
this->bindHyprlandSocket(); this->bindHyprlandSocket();
} }
@@ -63,7 +66,11 @@ void HyprlandService::init() {
clientPtr->title = client["title"].get<std::string>(); clientPtr->title = client["title"].get<std::string>();
this->clients[clientPtr->address] = clientPtr; this->clients[clientPtr->address] = clientPtr;
auto workspacePtr = workspaces[clientPtr->workspaceId]; auto workspaceIt = workspaces.find(clientPtr->workspaceId);
if (workspaceIt == workspaces.end()) {
continue;
}
auto workspacePtr = workspaceIt->second;
workspacePtr->state->clients[clientPtr->address] = clientPtr; workspacePtr->state->clients[clientPtr->address] = clientPtr;
if (client.contains("urgent") && client["urgent"].get<bool>()) { if (client.contains("urgent") && client["urgent"].get<bool>()) {
@@ -74,7 +81,11 @@ void HyprlandService::init() {
auto workspaceDataJson = HyprctlHelper::getWorkspaceData(); auto workspaceDataJson = HyprctlHelper::getWorkspaceData();
for (const auto &workspace : workspaceDataJson) { for (const auto &workspace : workspaceDataJson) {
auto workspacePtr = workspaces[workspace["id"].get<int>()]; auto workspaceIt = workspaces.find(workspace["id"].get<int>());
if (workspaceIt == workspaces.end()) {
continue;
}
auto workspacePtr = workspaceIt->second;
auto state = workspacePtr->state; auto state = workspacePtr->state;
state->id = workspace["id"].get<int>(); state->id = workspace["id"].get<int>();
@@ -85,6 +96,9 @@ void HyprlandService::init() {
} }
void HyprlandService::bindHyprlandSocket() { void HyprlandService::bindHyprlandSocket() {
if (socketFd != -1) {
return;
}
std::string socketPath = HyprSocketHelper::getHyprlandSocketPath(); std::string socketPath = HyprSocketHelper::getHyprlandSocketPath();
socketFd = socket(AF_UNIX, SOCK_STREAM, 0); socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
@@ -93,7 +107,7 @@ void HyprlandService::bindHyprlandSocket() {
return; return;
} }
struct sockaddr_un addr; struct sockaddr_un addr{};
memset(&addr, 0, sizeof(addr)); memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX; addr.sun_family = AF_UNIX;
std::strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); std::strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
@@ -107,36 +121,49 @@ void HyprlandService::bindHyprlandSocket() {
} }
auto socket_conditions = static_cast<GIOCondition>(G_IO_IN | G_IO_HUP | G_IO_ERR); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) auto socket_conditions = static_cast<GIOCondition>(G_IO_IN | G_IO_HUP | G_IO_ERR); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange)
GSource *source = g_unix_fd_source_new(socketFd, socket_conditions); g_unix_fd_add(
socketFd,
socket_conditions,
[](gint fd, GIOCondition condition, gpointer user_data) -> gboolean {
auto *self = static_cast<HyprlandService *>(user_data);
if (condition & (G_IO_HUP | G_IO_ERR)) {
spdlog::error("[Hyprland] Socket read error or connection closed");
close(fd);
self->socketFd = -1;
return FALSE;
}
auto onSocketEvent = [](gint fd, GIOCondition, gpointer user_data) -> gboolean { auto messages = SocketHelper::parseSocketMessage(fd, ">>");
HyprlandService *self = static_cast<HyprlandService *>(user_data); for (const auto &message : messages) {
auto messages = SocketHelper::parseSocketMessage(fd, ">>"); self->handleSocketMessage(message);
}
for (const auto &message : messages) { return TRUE;
self->handleSocketMessage(message); },
} this);
return G_SOURCE_CONTINUE;
};
g_source_set_callback(source, reinterpret_cast<GSourceFunc>(reinterpret_cast<void *>(+onSocketEvent)), this, nullptr);
g_source_attach(source, g_main_context_default());
g_source_unref(source);
} }
void HyprlandService::onWorkspaceChanged(int workspaceId) { void HyprlandService::onWorkspaceChanged(int workspaceId) {
auto newActive = workspaces[workspaceId]; auto newActiveIt = workspaces.find(workspaceId);
if (newActiveIt == workspaces.end()) {
return;
}
auto newActive = newActiveIt->second;
auto state = newActive->state; auto state = newActive->state;
auto monitorPtr = monitors[state->monitorName]; auto monitorIt = monitors.find(state->monitorName);
if (monitorIt == monitors.end()) {
return;
}
auto monitorPtr = monitorIt->second;
int oldActiveWorkspaceId = monitorPtr->activeWorkspaceId; int oldActiveWorkspaceId = monitorPtr->activeWorkspaceId;
auto oldActive = workspaces[oldActiveWorkspaceId];
monitorPtr->activeWorkspaceId = workspaceId; monitorPtr->activeWorkspaceId = workspaceId;
refreshIndicator(newActive); refreshIndicator(newActive);
refreshIndicator(oldActive);
auto oldActiveIt = workspaces.find(oldActiveWorkspaceId);
if (oldActiveIt != workspaces.end()) {
refreshIndicator(oldActiveIt->second);
}
} }
void HyprlandService::onFocusedMonitorChanged(std::string monitorData) { void HyprlandService::onFocusedMonitorChanged(std::string monitorData) {
@@ -161,20 +188,6 @@ void HyprlandService::onFocusedMonitorChanged(std::string monitorData) {
} }
} }
void HyprlandService::onMonitorRemoved(std::string monitorName) {
auto monitorPtr = this->monitors[monitorName];
for (const auto &[wsId, wsPtr] : monitorPtr->monitorWorkspaces) {
this->workspaces.erase(wsId);
}
monitorPtr->monitorWorkspaces.clear();
monitorPtr->bar->close();
monitorPtr->bar = nullptr;
this->monitors.erase(monitorName);
}
void HyprlandService::onMoveWindow(std::string windowData) { void HyprlandService::onMoveWindow(std::string windowData) {
auto parts = StringHelper::split(windowData, ','); auto parts = StringHelper::split(windowData, ',');
std::string addr = "0x" + parts[0]; std::string addr = "0x" + parts[0];
@@ -188,19 +201,24 @@ void HyprlandService::onMoveWindow(std::string windowData) {
auto clientPtr = this->clients[addr]; auto clientPtr = this->clients[addr];
int oldWorkspaceId = clientPtr->workspaceId; int oldWorkspaceId = clientPtr->workspaceId;
auto oldWorkspacePtr = workspaces[oldWorkspaceId]; auto oldWorkspaceIt = workspaces.find(oldWorkspaceId);
oldWorkspacePtr->state->clients.erase(addr); bool wasUrgent = false;
bool wasUrgent = oldWorkspacePtr->state->urgentClients.erase(addr) > 0; if (oldWorkspaceIt != workspaces.end()) {
refreshIndicator(oldWorkspacePtr); oldWorkspaceIt->second->state->clients.erase(addr);
wasUrgent = oldWorkspaceIt->second->state->urgentClients.erase(addr) > 0;
refreshIndicator(oldWorkspaceIt->second);
}
clientPtr->workspaceId = newWorkspaceId; clientPtr->workspaceId = newWorkspaceId;
auto newWorkspacePtr = workspaces[newWorkspaceId]; auto newWorkspaceIt = workspaces.find(newWorkspaceId);
newWorkspacePtr->state->clients[addr] = clientPtr; if (newWorkspaceIt != workspaces.end()) {
if (wasUrgent) { newWorkspaceIt->second->state->clients[addr] = clientPtr;
newWorkspacePtr->state->urgentClients.insert(addr); if (wasUrgent) {
newWorkspaceIt->second->state->urgentClients.insert(addr);
}
refreshIndicator(newWorkspaceIt->second);
} }
refreshIndicator(newWorkspacePtr);
} }
void HyprlandService::onOpenWindow(std::string windowData) { void HyprlandService::onOpenWindow(std::string windowData) {
@@ -214,10 +232,11 @@ void HyprlandService::onOpenWindow(std::string windowData) {
clientPtr->workspaceId = workspaceId; clientPtr->workspaceId = workspaceId;
clientPtr->title = title; clientPtr->title = title;
this->clients[clientPtr->address] = clientPtr; this->clients[clientPtr->address] = clientPtr;
auto workspacePtr = workspaces[clientPtr->workspaceId]; auto workspaceIt = workspaces.find(clientPtr->workspaceId);
workspacePtr->state->clients[clientPtr->address] = clientPtr; if (workspaceIt != workspaces.end()) {
workspaceIt->second->state->clients[clientPtr->address] = clientPtr;
refreshIndicator(workspacePtr); refreshIndicator(workspaceIt->second);
}
} }
void HyprlandService::onCloseWindow(std::string windowData) { void HyprlandService::onCloseWindow(std::string windowData) {
@@ -230,12 +249,15 @@ void HyprlandService::onCloseWindow(std::string windowData) {
auto clientPtr = this->clients[addr]; auto clientPtr = this->clients[addr];
int workspaceId = clientPtr->workspaceId; int workspaceId = clientPtr->workspaceId;
auto workspacePtr = workspaces[workspaceId];
workspacePtr->state->clients.erase(addr);
this->clients.erase(addr); this->clients.erase(addr);
refreshIndicator(workspacePtr); auto workspaceIt = workspaces.find(workspaceId);
if (workspaceIt == workspaces.end()) {
return;
}
workspaceIt->second->state->clients.erase(addr);
refreshIndicator(workspaceIt->second);
} }
void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) { void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) {
@@ -271,6 +293,10 @@ void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) {
this->onActiveWindowChanged(eventData); this->onActiveWindowChanged(eventData);
break; break;
} }
case MONITOR_ADDED: {
this->onMonitorAdded(eventData);
break;
}
case MONITOR_REMOVED: { case MONITOR_REMOVED: {
this->onMonitorRemoved(eventData); this->onMonitorRemoved(eventData);
break; break;
@@ -281,6 +307,112 @@ void HyprlandService::handleSocketMessage(SocketHelper::SocketMessage message) {
} }
} }
} }
void HyprlandService::onMonitorAdded(std::string monitorName) {
spdlog::info("[Hyprland] Monitor added: {}", monitorName);
auto monitorDataJson = HyprctlHelper::getMonitorData();
auto monitorIt = std::ranges::find_if(monitorDataJson,
[&monitorName](const auto &item) {
return item.contains("name") && item["name"] == monitorName;
});
if (monitorIt == monitorDataJson.end()) {
spdlog::warn("[Hyprland] Monitor data not found for {}", monitorName);
return;
}
auto monitorPtr = std::make_shared<Monitor>();
monitorPtr->id = (*monitorIt)["id"];
monitorPtr->name = monitorName;
monitorPtr->activeWorkspaceId = (*monitorIt)["activeWorkspace"]["id"];
monitorPtr->focused = (*monitorIt)["focused"];
this->monitors[monitorName] = monitorPtr;
auto onClick = sigc::mem_fun(*this, &HyprlandService::switchToWorkspace);
for (int i = 1; i <= NUM_WORKSPACES; i++) {
auto state = std::make_shared<WorkspaceData>();
int workspaceId = i + (NUM_WORKSPACES * monitorPtr->id);
state->id = workspaceId;
state->monitorName = monitorName;
auto view = std::make_shared<WorkspaceIndicator>(workspaceId, std::to_string(i), onClick);
auto workSpace = std::make_shared<Workspace>();
workSpace->state = state;
workSpace->view = view;
monitorPtr->monitorWorkspaces[workspaceId] = workSpace;
this->workspaces[workspaceId] = workSpace;
}
auto workspaceDataJson = HyprctlHelper::getWorkspaceData();
for (const auto &workspace : workspaceDataJson) {
if (!workspace.contains("monitor") || workspace["monitor"].get<std::string>() != monitorName) {
continue;
}
auto workspaceIt = workspaces.find(workspace["id"].get<int>());
if (workspaceIt == workspaces.end()) {
continue;
}
auto workspacePtr = workspaceIt->second;
auto state = workspacePtr->state;
state->id = workspace["id"].get<int>();
state->monitorName = workspace["monitor"].get<std::string>();
refreshIndicator(workspacePtr);
}
auto clientsDataJson = HyprctlHelper::getClientData();
for (const auto &client : clientsDataJson) {
auto address = client["address"].get<std::string>();
if (this->clients.find(address) != this->clients.end()) {
continue;
}
int workspaceId = client["workspace"]["id"].get<int>();
auto workspaceIt = workspaces.find(workspaceId);
if (workspaceIt == workspaces.end()) {
continue;
}
if (workspaceIt->second->state->monitorName != monitorName) {
continue;
}
auto clientPtr = std::make_shared<Client>();
clientPtr->address = address;
clientPtr->workspaceId = workspaceId;
clientPtr->title = client["title"].get<std::string>();
this->clients[clientPtr->address] = clientPtr;
workspaceIt->second->state->clients[clientPtr->address] = clientPtr;
if (client.contains("urgent") && client["urgent"].get<bool>()) {
workspaceIt->second->state->urgentClients.insert(clientPtr->address);
}
refreshIndicator(workspaceIt->second);
}
m_signal_monitor_added.emit(monitorName);
}
void HyprlandService::onMonitorRemoved(std::string monitorName) {
spdlog::info("[Hyprland] Monitor removed: {}", monitorName);
auto monitorIt = monitors.find(monitorName);
if (monitorIt == monitors.end()) {
return;
}
auto monitorPtr = monitorIt->second;
for (const auto &[workspaceId, workspacePtr] : monitorPtr->monitorWorkspaces) {
for (const auto &[clientAddr, _] : workspacePtr->state->clients) {
clients.erase(clientAddr);
}
workspaces.erase(workspaceId);
}
this->monitors.erase(monitorIt);
m_signal_monitor_removed.emit(monitorName);
}
void HyprlandService::onUrgent(std::string windowAddress) { void HyprlandService::onUrgent(std::string windowAddress) {
std::string addr = "0x" + windowAddress; std::string addr = "0x" + windowAddress;
if (this->clients.find(addr) == this->clients.end()) { if (this->clients.find(addr) == this->clients.end()) {
@@ -290,10 +422,13 @@ void HyprlandService::onUrgent(std::string windowAddress) {
auto clientPtr = this->clients[addr]; auto clientPtr = this->clients[addr];
int workspaceId = clientPtr->workspaceId; int workspaceId = clientPtr->workspaceId;
auto workspacePtr = workspaces[workspaceId]; auto workspaceIt = workspaces.find(workspaceId);
workspacePtr->state->urgentClients.insert(addr); if (workspaceIt == workspaces.end()) {
return;
}
workspaceIt->second->state->urgentClients.insert(addr);
refreshIndicator(workspacePtr); refreshIndicator(workspaceIt->second);
} }
void HyprlandService::onActiveWindowChanged(std::string windowAddress) { void HyprlandService::onActiveWindowChanged(std::string windowAddress) {
@@ -305,15 +440,22 @@ void HyprlandService::onActiveWindowChanged(std::string windowAddress) {
auto clientPtr = this->clients[addr]; auto clientPtr = this->clients[addr];
int workspaceId = clientPtr->workspaceId; int workspaceId = clientPtr->workspaceId;
auto workspacePtr = workspaces[workspaceId]; auto workspaceIt = workspaces.find(workspaceId);
workspacePtr->state->urgentClients.erase(addr); if (workspaceIt == workspaces.end()) {
return;
}
workspaceIt->second->state->urgentClients.erase(addr);
refreshIndicator(workspacePtr); refreshIndicator(workspaceIt->second);
} }
std::shared_ptr<Gtk::Box> HyprlandService::getWorkspaceIndicatorsForMonitor(std::string monitorName) { std::shared_ptr<Gtk::Box> HyprlandService::getWorkspaceIndicatorsForMonitor(std::string monitorName) {
auto box = std::make_shared<Gtk::Box>(Gtk::Orientation::HORIZONTAL); auto box = std::make_shared<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
auto monitor = monitors[monitorName]; auto monitorIt = monitors.find(monitorName);
if (monitorIt == monitors.end()) {
return box;
}
auto monitor = monitorIt->second;
for (const auto &[wsId, wsPair] : monitor->monitorWorkspaces) { for (const auto &[wsId, wsPair] : monitor->monitorWorkspaces) {
box->append((Gtk::Box &)*wsPair->view); box->append((Gtk::Box &)*wsPair->view);
@@ -329,8 +471,12 @@ void HyprlandService::switchToWorkspace(int workspaceId) {
void HyprlandService::refreshIndicator(std::shared_ptr<Workspace> workspace) { void HyprlandService::refreshIndicator(std::shared_ptr<Workspace> workspace) {
auto view = workspace->view; auto view = workspace->view;
auto state = workspace->state; auto state = workspace->state;
auto monitorsPtr = monitors[state->monitorName]; auto monitorIt = monitors.find(state->monitorName);
if (monitorIt == monitors.end()) {
return;
}
auto monitorsPtr = monitorIt->second;
bool isUrgent = !state->urgentClients.empty(); bool isUrgent = !state->urgentClients.empty();
bool isEmpty = state->clients.empty(); bool isEmpty = state->clients.empty();
@@ -351,5 +497,9 @@ void HyprlandService::refreshIndicator(std::shared_ptr<Workspace> workspace) {
} }
void HyprlandService::addBar(std::shared_ptr<Bar> bar, std::string monitorName) { void HyprlandService::addBar(std::shared_ptr<Bar> bar, std::string monitorName) {
this->monitors[monitorName]->bar = bar; auto monitorIt = monitors.find(monitorName);
if (monitorIt == monitors.end()) {
return;
}
monitorIt->second->bar = bar;
} }

View File

@@ -12,8 +12,6 @@
#include "gdkmm/display.h" #include "gdkmm/display.h"
#include "sigc++/adaptors/bind.h" #include "sigc++/adaptors/bind.h"
std::shared_ptr<NotificationController> NotificationController::instance = nullptr;
NotificationController::NotificationController() { NotificationController::NotificationController() {
if (NotificationController::instance) { if (NotificationController::instance) {
throw std::runtime_error("use getInstance()!"); throw std::runtime_error("use getInstance()!");
@@ -32,8 +30,8 @@ NotificationController::NotificationController() {
for (guint i = 0; i < monitors->get_n_items(); ++i) { for (guint i = 0; i < monitors->get_n_items(); ++i) {
auto monitor = std::dynamic_pointer_cast<Gdk::Monitor>( auto monitor = std::dynamic_pointer_cast<Gdk::Monitor>(
monitors->get_object(i)); monitors->get_object(i));
auto name = monitor->get_connector();
this->activeMonitors.push_back(monitor); this->activeMonitors[name] = monitor;
} }
} }
@@ -41,7 +39,7 @@ void NotificationController::showNotificationOnAllMonitors(NotifyMessage notify)
uint64_t id = this->globalNotificationId++; uint64_t id = this->globalNotificationId++;
std::vector<std::shared_ptr<BaseNotification>> notifications; std::vector<std::shared_ptr<BaseNotification>> notifications;
for (const auto &monitor : this->activeMonitors) { for (const auto &[name, monitor] : this->activeMonitors) {
auto notification = std::make_shared<NotificationWindow>(id, monitor, notify); auto notification = std::make_shared<NotificationWindow>(id, monitor, notify);
notifications.push_back(notification); notifications.push_back(notification);
@@ -52,11 +50,11 @@ void NotificationController::showNotificationOnAllMonitors(NotifyMessage notify)
timeout = DEFAULT_NOTIFICATION_TIMEOUT; timeout = DEFAULT_NOTIFICATION_TIMEOUT;
} }
notification->signal_close.connect([this, id = notification->getNotificationId()](int) { notification->getSignalClose().connect([this, id = notification->getNotificationId()](int) {
closeNotification(id); closeNotification(id);
}); });
notification->signal_hover_changed.connect([this, id = notification->getNotificationId()](bool hovered) { notification->getSignalHoverChanged().connect([this, id = notification->getNotificationId()](bool hovered) {
updateHoverState(id, hovered); updateHoverState(id, hovered);
}); });
@@ -74,17 +72,17 @@ void NotificationController::showCopyNotification(NotifyMessage notify) {
std::vector<std::shared_ptr<BaseNotification>> notifications; std::vector<std::shared_ptr<BaseNotification>> notifications;
uint64_t id = this->globalNotificationId++; uint64_t id = this->globalNotificationId++;
for (const auto &monitor : this->activeMonitors) { for (const auto &[name, monitor] : this->activeMonitors) {
auto notification = std::make_shared<CopyNotification>(id, monitor, notify); auto notification = std::make_shared<CopyNotification>(id, monitor, notify);
notification->show(); notification->show();
notifications.push_back(notification); notifications.push_back(notification);
notification->signal_close.connect([this, id = notification->getNotificationId()](int) { notification->getSignalClose().connect([this, id = notification->getNotificationId()](int) {
closeNotification(id); closeNotification(id);
}); });
notification->signal_hover_changed.connect([this, id = notification->getNotificationId()](bool hovered) { notification->getSignalHoverChanged().connect([this, id = notification->getNotificationId()](bool hovered) {
updateHoverState(id, hovered); updateHoverState(id, hovered);
}); });
notification->startAutoClose(DEFAULT_NOTIFICATION_TIMEOUT); notification->startAutoClose(DEFAULT_NOTIFICATION_TIMEOUT);
@@ -97,17 +95,17 @@ void NotificationController::showSpotifyNotification(MprisPlayer2Message mpris)
std::vector<std::shared_ptr<BaseNotification>> notifications; std::vector<std::shared_ptr<BaseNotification>> notifications;
uint64_t id = this->globalNotificationId++; uint64_t id = this->globalNotificationId++;
for (const auto &monitor : this->activeMonitors) { for (const auto &[name, monitor] : this->activeMonitors) {
auto notification = std::make_shared<SpotifyNotification>(id, monitor, mpris); auto notification = std::make_shared<SpotifyNotification>(id, monitor, mpris);
notification->show(); notification->show();
notifications.push_back(notification); notifications.push_back(notification);
notification->signal_close.connect([this, id = notification->getNotificationId()](int) { notification->getSignalClose().connect([this, id = notification->getNotificationId()](int) {
closeNotification(id); closeNotification(id);
}); });
notification->signal_hover_changed.connect([this, id = notification->getNotificationId()](bool hovered) { notification->getSignalHoverChanged().connect([this, id = notification->getNotificationId()](bool hovered) {
updateHoverState(id, hovered); updateHoverState(id, hovered);
}); });
@@ -153,4 +151,14 @@ void NotificationController::closeNotification(uint64_t notificationId) {
this->activeNotifications.erase(notificationId); this->activeNotifications.erase(notificationId);
this->hoverCounts.erase(notificationId); this->hoverCounts.erase(notificationId);
}
void NotificationController::addMonitor(std::shared_ptr<Gdk::Monitor> monitor) {
auto name = monitor->get_connector();
this->activeMonitors[name] = monitor;
}
void NotificationController::removeMonitor(std::shared_ptr<Gdk::Monitor> monitor) {
auto name = monitor->get_connector();
this->activeMonitors.erase(name);
} }

View File

@@ -18,7 +18,7 @@ namespace {
#define CACHE_AGE 168 #define CACHE_AGE 168
constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ull; constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ull;
constexpr std::uint64_t kFnvPrime = 1099511628211ull; constexpr std::uint64_t kFnvPrime = 1099511628211ull;
std::string to_hex(std::uint64_t value) { std::string to_hex(std::uint64_t value) {
std::ostringstream stream; std::ostringstream stream;
@@ -53,7 +53,7 @@ std::filesystem::path get_cache_path_for_url(const std::string &url) {
auto filename = hash_url(url); auto filename = hash_url(url);
auto last_slash = url.find_last_of('/'); auto last_slash = url.find_last_of('/');
auto last_dot = url.find_last_of('.'); auto last_dot = url.find_last_of('.');
if (last_dot != std::string::npos && (last_slash == std::string::npos || last_dot > last_slash)) { if (last_dot != std::string::npos && (last_slash == std::string::npos || last_dot > last_slash)) {
auto ext = url.substr(last_dot); auto ext = url.substr(last_dot);
if (ext.size() <= 10) { if (ext.size() <= 10) {
@@ -66,14 +66,13 @@ std::filesystem::path get_cache_path_for_url(const std::string &url) {
std::chrono::system_clock::time_point to_system_clock(std::filesystem::file_time_type time) { std::chrono::system_clock::time_point to_system_clock(std::filesystem::file_time_type time) {
return std::chrono::time_point_cast<std::chrono::system_clock::duration>( return std::chrono::time_point_cast<std::chrono::system_clock::duration>(
time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now() time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
);
} }
size_t write_to_buffer(void *contents, size_t size, size_t nmemb, void *userp) { size_t write_to_buffer(void *contents, size_t size, size_t nmemb, void *userp) {
auto *buffer = static_cast<std::vector<unsigned char> *>(userp); auto *buffer = static_cast<std::vector<unsigned char> *>(userp);
auto total = size * nmemb; auto total = size * nmemb;
auto *bytes = static_cast<unsigned char *>(contents); auto *bytes = static_cast<unsigned char *>(contents);
buffer->insert(buffer->end(), bytes, bytes + total); buffer->insert(buffer->end(), bytes, bytes + total);
return total; return total;
} }
@@ -133,6 +132,15 @@ TextureCacheService *TextureCacheService::getInstance() {
return &instance; return &instance;
} }
TextureCacheService::TextureCacheService() {
curl_global_init(CURL_GLOBAL_DEFAULT);
}
TextureCacheService::~TextureCacheService() {
clear();
curl_global_cleanup();
}
Glib::RefPtr<Gdk::Texture> TextureCacheService::getTexture(const std::string &url) { Glib::RefPtr<Gdk::Texture> TextureCacheService::getTexture(const std::string &url) {
if (url.empty()) { if (url.empty()) {
return {}; return {};
@@ -144,7 +152,7 @@ Glib::RefPtr<Gdk::Texture> TextureCacheService::getTexture(const std::string &ur
} }
auto cache_path = get_cache_path_for_url(url); auto cache_path = get_cache_path_for_url(url);
auto texture = load_texture_from_file(cache_path); auto texture = load_texture_from_file(cache_path);
if (!texture) { if (!texture) {
texture = download_texture_from_url(url, cache_path); texture = download_texture_from_url(url, cache_path);
} }
@@ -166,7 +174,7 @@ void TextureCacheService::pruneCache() {
std::vector<std::pair<std::filesystem::path, std::uintmax_t>> files; std::vector<std::pair<std::filesystem::path, std::uintmax_t>> files;
std::uintmax_t total_size = 0; std::uintmax_t total_size = 0;
auto now = std::chrono::system_clock::now(); auto now = std::chrono::system_clock::now();
auto max_age = std::chrono::hours(CACHE_AGE); auto max_age = std::chrono::hours(CACHE_AGE);
for (const auto &entry : std::filesystem::directory_iterator(cache_dir, error)) { for (const auto &entry : std::filesystem::directory_iterator(cache_dir, error)) {
@@ -174,7 +182,7 @@ void TextureCacheService::pruneCache() {
continue; continue;
} }
auto path = entry.path(); auto path = entry.path();
auto last_write = entry.last_write_time(error); auto last_write = entry.last_write_time(error);
if (!error) { if (!error) {
auto age = now - to_system_clock(last_write); auto age = now - to_system_clock(last_write);
@@ -199,7 +207,7 @@ void TextureCacheService::pruneCache() {
std::sort(files.begin(), files.end(), [&](const auto &left, const auto &right) { std::sort(files.begin(), files.end(), [&](const auto &left, const auto &right) {
std::error_code left_error; std::error_code left_error;
std::error_code right_error; std::error_code right_error;
auto left_time = std::filesystem::last_write_time(left.first, left_error); auto left_time = std::filesystem::last_write_time(left.first, left_error);
auto right_time = std::filesystem::last_write_time(right.first, right_error); auto right_time = std::filesystem::last_write_time(right.first, right_error);
if (left_error || right_error) { if (left_error || right_error) {
return left.first.string() < right.first.string(); return left.first.string() < right.first.string();
@@ -221,3 +229,7 @@ void TextureCacheService::pruneCache() {
} }
} }
} }
void TextureCacheService::clear() {
cache.clear();
}

View File

View File

@@ -0,0 +1,145 @@
#include "widgets/controlCenter/bluetoothSettings.hpp"
#include <memory>
BluetoothSettings::BluetoothSettings() : bluetoothIsPowered(this->bluetoothController->isPowered()), bluetoothIsScanning(this->bluetoothController->isDiscovering()) {
set_orientation(Gtk::Orientation::VERTICAL);
set_spacing(12);
add_css_class("bluetooth-settings");
powerButton->add_css_class("power-button");
setBluetoothPowered(bluetoothIsPowered);
powerButton->set_tooltip_text(bluetoothIsPowered ? "Turn Bluetooth Off" : "Turn Bluetooth On");
powerButton->signal_clicked().connect([this]() {
bluetoothIsPowered = !bluetoothIsPowered;
this->bluetoothController->setPowered(bluetoothIsPowered);
});
this->buttonRow.append(*powerButton);
scanButton->add_css_class("power-button");
setScanning(bluetoothIsScanning);
scanButton->set_tooltip_text(bluetoothIsScanning ? "Stop Scanning" : "Start Scanning");
scanButton->signal_clicked().connect([this]() {
bluetoothIsScanning = !bluetoothIsScanning;
if (bluetoothIsScanning) {
this->bluetoothController->startDiscovery();
} else {
this->bluetoothController->stopDiscovery();
}
});
this->buttonRow.append(*scanButton);
this->buttonRow.set_spacing(12);
append(this->buttonRow);
connectedDevicesBox.add_css_class("active-devices");
connectedDevicesBox.set_orientation(Gtk::Orientation::VERTICAL);
connectedDevicesScroll.set_child(connectedDevicesBox);
connectedDevicesScroll.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
connectedDevicesScroll.set_propagate_natural_height(true);
connectedDevicesScroll.set_max_content_height(200);
append(connectedDevicesScroll);
availableDevicesBox.add_css_class("available-devices");
availableDevicesBox.set_orientation(Gtk::Orientation::VERTICAL);
availableDevicesScroll.set_child(availableDevicesBox);
availableDevicesScroll.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
availableDevicesScroll.set_propagate_natural_height(true);
availableDevicesScroll.set_max_content_height(200);
append(availableDevicesScroll);
this->bluetoothController->signalPoweredChanged().connect([this](bool powered) {
bluetoothIsPowered = powered;
setBluetoothPowered(powered);
});
this->bluetoothController->signalDiscoveringChanged().connect([this](bool scanning) {
bluetoothIsScanning = scanning;
setScanning(scanning);
});
auto devices = this->bluetoothController->getDevices();
for (const auto &device : devices) {
auto row = std::make_shared<BluetoothSettingsRow>(device);
deviceRows[device.object_path] = row;
if (device.connected) {
connectedDevicesBox.append(*row);
} else {
availableDevicesBox.append(*row);
}
}
this->bluetoothController->signalDeviceAdded().connect([this](const BluetoothDevice &device) {
auto row = std::make_shared<BluetoothSettingsRow>(device);
deviceRows[device.object_path] = row;
if (device.connected) {
connectedDevicesBox.append(*row);
} else {
availableDevicesBox.append(*row);
}
});
this->bluetoothController->signalDeviceRemoved().connect([this](const std::string &object_path) {
auto it = deviceRows.find(object_path);
if (it != deviceRows.end()) {
auto parent = it->second->get_parent();
if (parent) {
dynamic_cast<Gtk::Box *>(parent)->remove(*it->second);
}
deviceRows.erase(it);
}
});
this->bluetoothController->signalDeviceChanged().connect([this](const BluetoothDevice &device) {
auto it = deviceRows.find(device.object_path);
if (it != deviceRows.end()) {
it->second->updateDevice(device);
// Move between boxes if connection status changed
auto parent = it->second->get_parent();
Gtk::Box *targetBox = device.connected ? &connectedDevicesBox : &availableDevicesBox;
if (parent != targetBox) {
if (parent) {
dynamic_cast<Gtk::Box *>(parent)->remove(*it->second);
}
targetBox->append(*it->second);
}
}
});
}
void BluetoothSettings::setBluetoothPowered(bool powered) {
this->bluetoothIsPowered = powered;
if (powered) {
powerButton->remove_css_class("power-button-off");
powerButton->add_css_class("power-button-on");
powerButton->set_tooltip_text("Turn Bluetooth Off");
} else {
powerButton->remove_css_class("power-button-on");
powerButton->add_css_class("power-button-off");
powerButton->set_tooltip_text("Turn Bluetooth On");
}
}
void BluetoothSettings::setScanning(bool scanning) {
this->bluetoothIsScanning = scanning;
if (scanning) {
scanButton->add_css_class("power-button-on");
scanButton->remove_css_class("power-button-off");
scanButton->set_tooltip_text("Stop Scanning");
} else {
scanButton->remove_css_class("power-button-on");
scanButton->add_css_class("power-button-off");
scanButton->set_tooltip_text("Start Scanning");
}
}

View File

@@ -0,0 +1,88 @@
#include "widgets/controlCenter/bluetoothSettingsRow.hpp"
#include <spdlog/spdlog.h>
#include "components/button/iconButton.hpp"
#include "connection/dbus/bluetooth.hpp"
#include "helpers/string.hpp"
#include "services/notificationController.hpp"
BluetoothSettingsRow::BluetoothSettingsRow(const BluetoothDevice &device)
: device(device),
pairButton(device.paired ? Icon::BLUETOOTH_CONNECTED : Icon::BLUETOOTH),
connectButton(device.connected ? Icon::LINK : Icon::LINK_OFF),
trustButton(device.trusted ? Icon::DONE_ALL : Icon::REMOVE_DONE) {
set_orientation(Gtk::Orientation::HORIZONTAL);
set_spacing(10);
add_css_class("bluetooth-settings-row");
set_hexpand(true);
set_halign(Gtk::Align::FILL);
if (!device.icon.empty()) {
this->icon.set_from_icon_name(device.icon);
this->icon.set_pixel_size(24);
append(this->icon);
}
auto deviceInfoBox = Gtk::Box(Gtk::Orientation::VERTICAL);
deviceInfoBox.set_spacing(2);
append(deviceInfoBox);
nameLabel.set_ellipsize(Pango::EllipsizeMode::END);
nameLabel.set_text(device.name.empty() ? "Unknown Device" : device.name );
nameLabel.set_halign(Gtk::Align::START);
nameLabel.set_valign(Gtk::Align::CENTER);
deviceInfoBox.append(nameLabel);
addressLabel.set_text(device.address);
addressLabel.set_halign(Gtk::Align::START);
addressLabel.set_valign(Gtk::Align::CENTER);
addressLabel.add_css_class("bluetooth-device-address");
deviceInfoBox.append(addressLabel);
deviceInfoBox.set_size_request(140, -1);
auto buttonBox = Gtk::Box(Gtk::Orientation::HORIZONTAL);
buttonBox.set_hexpand(true);
buttonBox.set_halign(Gtk::Align::END);
append(buttonBox);
pairButton.signal_clicked().connect([this]() {
if (this->device.paired) {
BluetoothController::getInstance()->unpairDevice(this->device.object_path);
} else {
BluetoothController::getInstance()->pairDevice(this->device.object_path);
}
});
buttonBox.append(pairButton);
connectButton.signal_clicked().connect([this]() {
if (this->device.connected) {
BluetoothController::getInstance()->disconnectDevice(this->device.object_path);
} else {
BluetoothController::getInstance()->connectDevice(this->device.object_path);
}
});
buttonBox.append(connectButton);
trustButton.signal_clicked().connect([this]() {
BluetoothController::getInstance()->trustDevice(this->device.object_path, !this->device.trusted);
});
buttonBox.append(trustButton);
}
void BluetoothSettingsRow::updateDevice(const BluetoothDevice &device) {
this->device = device;
if (!device.icon.empty()) {
this->icon.set_from_icon_name(device.icon);
}
spdlog::info("Updating device {}: paired={}, connected={}, trusted={}", device.name, device.paired, device.connected, device.trusted);
nameLabel.set_text(device.name.empty() ? "Unknown Device" : device.name);
addressLabel.set_text(device.address);
pairButton.setIcon(device.paired ? Icon::BLUETOOTH_CONNECTED : Icon::BLUETOOTH);
connectButton.setIcon(device.connected ? Icon::LINK : Icon::LINK_OFF);
trustButton.setIcon(device.trusted ? Icon::DONE_ALL : Icon::REMOVE_DONE);
}

View File

@@ -1,101 +1,91 @@
#include "widgets/controlCenter/controlCenter.hpp" #include "widgets/controlCenter/controlCenter.hpp"
#include "components/button/iconButton.hpp"
#include "components/button/tabButton.hpp"
#include "components/mediaPlayer.hpp"
ControlCenter::ControlCenter(std::string icon, std::string name) ControlCenter::ControlCenter(Icon::Type icon, std::string name)
: Popover(icon, name) { : Popover(icon, name) {
this->popover->add_css_class("control-center-popover"); this->popover->add_css_class("control-center-popover");
this->container.set_orientation(Gtk::Orientation::VERTICAL); this->container.set_orientation(Gtk::Orientation::VERTICAL);
this->container.set_spacing(10); this->container.set_spacing(10);
this->popover->set_hexpand(false);
this->popover->set_size_request(240, -1);
set_popover_child(this->container); this->scrollview.set_child(this->container);
this->scrollview.set_policy(
Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC);
this->scrollview.set_hexpand(false);
this->scrollview.set_vexpand(true);
this->scrollview.set_propagate_natural_height(true);
set_popover_child(this->scrollview);
this->tabRow.set_orientation(Gtk::Orientation::HORIZONTAL); this->tabRow.set_orientation(Gtk::Orientation::HORIZONTAL);
this->tabRow.set_spacing(4); this->tabRow.set_spacing(4);
this->tabRow.set_margin_bottom(4);
this->tabRow.add_css_class("control-center-tab-row"); this->tabRow.add_css_class("control-center-tab-row");
this->mediaControl.set_label("\uf5d3"); // control icon this->mediaTabButton = std::make_unique<TabButton>(Icon::PLAY_CIRCLE);
this->mediaControl.add_css_class("tab-icon"); this->infoTabButton = std::make_unique<TabButton>(Icon::EMPTY_DASHBOARD);
this->testTabButton.set_label("\uE5CA"); // test icon this->timerButton = std::make_unique<TabButton>(Icon::TOKEN);
this->testTabButton.add_css_class("tab-icon"); this->settingsTabButton = std::make_unique<TabButton>(Icon::SETTINGS);
this->tabRow.append(this->mediaControl); this->tabRow.append(*this->mediaTabButton);
this->tabRow.append(this->testTabButton); this->tabRow.append(*this->infoTabButton);
this->tabRow.append(*this->timerButton);
this->tabRow.append(*this->settingsTabButton);
this->container.append(this->tabRow); this->container.append(this->tabRow);
this->contentStack.set_hhomogeneous(false); this->contentStack.set_hhomogeneous(true);
this->contentStack.set_vhomogeneous(false); this->contentStack.set_vhomogeneous(false);
this->contentStack.set_transition_type(Gtk::StackTransitionType::CROSSFADE); this->contentStack.set_transition_type(Gtk::StackTransitionType::CROSSFADE);
this->contentStack.set_transition_duration(150);
this->controlCenterContainer.set_orientation(Gtk::Orientation::VERTICAL); this->mediaControlWidget = std::make_unique<MediaWidget>();
this->controlCenterContainer.set_spacing(4); this->weatherWidget = std::make_unique<WeatherWidget>();
this->timerWidget = std::make_unique<TimerWidget>();
this->settingsWidget = std::make_unique<SettingsWidget>();
this->contentStack.add(*this->mediaControlWidget, "controls", "Controls");
this->contentStack.add(*this->weatherWidget, "info", "Info");
this->contentStack.add(*this->timerWidget, "timer", "Timer");
this->contentStack.add(*this->settingsWidget, "settings", "Settings");
this->contentStack.add(this->controlCenterContainer, "controls", "Controls");
this->contentStack.add(this->weatherWidget, "test", "Test");
this->contentStack.set_visible_child("controls"); this->contentStack.set_visible_child("controls");
this->setActiveTab("controls"); this->setActiveTab("controls");
this->container.append(this->contentStack); this->container.append(this->contentStack);
this->mediaControl.signal_clicked().connect([this]() { this->mediaTabButton->signal_clicked().connect([this]() {
this->setActiveTab("controls"); this->setActiveTab("controls");
}); });
this->testTabButton.signal_clicked().connect([this]() { this->infoTabButton->signal_clicked().connect([this]() {
this->setActiveTab("test"); this->setActiveTab("info");
}); });
this->mprisController->signal_player_registered().connect( this->timerButton->signal_clicked().connect([this]() {
[this](const std::string &bus_name) { this->setActiveTab("timer");
this->addPlayerWidget(bus_name); });
});
this->mprisController->signal_player_deregistered().connect(
[this](const std::string &bus_name) {
this->removePlayerWidget(bus_name);
});
for (const auto &bus_name : this->mprisController->get_registered_players()) {
this->addPlayerWidget(bus_name);
}
this->settingsTabButton->signal_clicked().connect([this]() {
this->setActiveTab("settings");
});
} }
void ControlCenter::setActiveTab(const std::string &tab_name) { void ControlCenter::setActiveTab(const std::string &tab_name) {
this->contentStack.set_visible_child(tab_name); this->contentStack.set_visible_child(tab_name);
this->mediaControl.remove_css_class("active-button"); this->mediaTabButton->setActive(false);
this->testTabButton.remove_css_class("active-button"); this->infoTabButton->setActive(false);
this->timerButton->setActive(false);
this->settingsTabButton->setActive(false);
if (tab_name == "controls") { if (tab_name == "controls") {
this->mediaControl.add_css_class("active-button"); this->mediaTabButton->setActive(true);
} else if (tab_name == "test") { } else if (tab_name == "info") {
this->testTabButton.add_css_class("active-button"); this->infoTabButton->setActive(true);
} else if (tab_name == "timer") {
this->timerButton->setActive(true);
} else if (tab_name == "settings") {
this->settingsTabButton->setActive(true);
} }
} }
void ControlCenter::addPlayerWidget(const std::string &bus_name) {
if (this->mediaWidgets.find(bus_name) != this->mediaWidgets.end()) {
return;
}
auto controller = MprisController::createForPlayer(bus_name);
auto widget = Gtk::make_managed<MediaControlWidget>(controller);
this->mediaWidgets.emplace(bus_name, widget);
this->controlCenterContainer.append(*widget);
}
void ControlCenter::removePlayerWidget(const std::string &bus_name) {
auto it = this->mediaWidgets.find(bus_name);
if (it == this->mediaWidgets.end()) {
return;
}
this->controlCenterContainer.remove(*it->second);
this->mediaWidgets.erase(it);
}

View File

@@ -0,0 +1,50 @@
#include "widgets/controlCenter/mediaWidget.hpp"
#include <memory>
MediaWidget::MediaWidget() : Gtk::Box(Gtk::Orientation::VERTICAL) {
this->set_hexpand(true);
this->set_vexpand(false);
this->add_css_class("control-center-media-widget");
this->container.set_orientation(Gtk::Orientation::VERTICAL);
this->container.set_hexpand(true);
this->container.set_vexpand(false);
this->append(this->container);
this->mprisController->signal_player_registered().connect(
[this](const std::string &bus_name) {
this->addPlayerWidget(bus_name);
});
this->mprisController->signal_player_deregistered().connect(
[this](const std::string &bus_name) {
this->removePlayerWidget(bus_name);
});
for (const auto &bus_name : this->mprisController->get_registered_players()) {
this->addPlayerWidget(bus_name);
}
}
void MediaWidget::addPlayerWidget(const std::string &bus_name) {
if (this->mediaWidgets.find(bus_name) != this->mediaWidgets.end()) {
return;
}
auto controller = MprisController::createForPlayer(bus_name);
auto widget = std::make_unique<MediaPlayer>(controller);
this->mediaWidgets.emplace(bus_name, std::move(widget));
this->container.append(*this->mediaWidgets[bus_name]);
}
void MediaWidget::removePlayerWidget(const std::string &bus_name) {
auto it = this->mediaWidgets.find(bus_name);
if (it == this->mediaWidgets.end()) {
return;
}
this->container.remove(*it->second);
this->mediaWidgets.erase(it);
}

View File

@@ -0,0 +1,8 @@
#include "widgets/controlCenter/settings.hpp"
SettingsWidget::SettingsWidget() {
set_orientation(Gtk::Orientation::VERTICAL);
set_spacing(12);
this->append(this->bluetoothSettings);
}

View File

@@ -0,0 +1,143 @@
#include "widgets/controlCenter/timer.hpp"
#include <cstdint>
#include <gdk/gdkkeysyms.h>
#include <memory>
#include <spdlog/spdlog.h>
#include <sys/types.h>
#include "gtkmm/entry.h"
#include "gtkmm/eventcontrollerkey.h"
#include "gtkmm/label.h"
TimerWidget::TimerWidget() {
set_orientation(Gtk::Orientation::VERTICAL);
set_spacing(6);
this->timerSetConnection = this->timerService->getSignalTimerSet().connect([this](const std::string &duration, uint64_t timerId) {
this->addTimer(duration, timerId);
});
this->timerCancelledConnection = this->timerService->getSignalTimerCancelled().connect([this](uint64_t timerId) {
this->removeTimer(timerId);
});
this->timerExpiredConnection = this->timerService->getSignalTimerExpired().connect([this](uint64_t timerId) {
this->activateTimer(timerId);
});
this->tickConnection = this->timerService->tickSignal.connect([this]() {
for (auto &[id, timer] : activeTimers) {
timer->tickDown();
}
});
auto label = Gtk::make_managed<Gtk::Label>("Set Timer");
label->add_css_class("control-center-timer-label");
auto entry = Gtk::make_managed<Gtk::Entry>();
entry->set_placeholder_text("0s");
entry->set_valign(Gtk::Align::CENTER);
entry->set_alignment(0.5);
entry->add_css_class("text-area");
entry->set_editable(false);
entry->set_focusable(true);
entry->set_position(-1);
set_focusable(false);
auto keyController = Gtk::EventControllerKey::create();
keyController->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
keyController->signal_key_pressed().connect([this, entry](guint keyval, guint, Gdk::ModifierType) -> bool {
if (updatingText) {
return true;
}
if (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9) {
rawDigits.push_back(static_cast<char>('0' + (keyval - GDK_KEY_0)));
} else if (keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9) {
rawDigits.push_back(static_cast<char>('0' + (keyval - GDK_KEY_KP_0)));
} else if (keyval == GDK_KEY_BackSpace || keyval == GDK_KEY_Delete ||
keyval == GDK_KEY_KP_Delete) {
if (!rawDigits.empty()) {
rawDigits.pop_back();
}
} else if (keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter ||
keyval == GDK_KEY_Tab || keyval == GDK_KEY_ISO_Left_Tab) {
return false;
} else {
return true;
}
if (rawDigits.size() > 6) {
rawDigits.erase(0, rawDigits.size() - 6);
}
updatingText = true;
entry->set_text(format_duration(rawDigits));
entry->set_position(-1);
updatingText = false;
return true;
},
false);
entry->add_controller(keyController);
entry->signal_activate().connect([this, entry]() {
if (rawDigits.empty()) {
return;
}
uint64_t rawValue = std::stoull(rawDigits);
uint64_t seconds = (rawValue / 10000) * 3600 + ((rawValue / 100) % 100) * 60 + (rawValue % 100);
spdlog::info("Timer set for {} seconds", seconds);
this->timerService->addTimer(seconds);
rawDigits.clear();
this->updatingText = true;
entry->set_text("");
entry->set_position(-1);
this->updatingText = false;
},
false);
append(*label);
append(*entry);
}
TimerWidget::~TimerWidget() {
if (this->timerSetConnection.connected()) {
this->timerSetConnection.disconnect();
}
if (this->timerCancelledConnection.connected()) {
this->timerCancelledConnection.disconnect();
}
if (this->timerExpiredConnection.connected()) {
this->timerExpiredConnection.disconnect();
}
if (this->tickConnection.connected()) {
this->tickConnection.disconnect();
}
};
void TimerWidget::addTimer(const std::string &duration, uint64_t timerId) {
std::unique_ptr<Timer> timer = std::make_unique<Timer>(duration, timerId);
append(*timer);
this->activeTimers[timerId] = std::move(timer);
}
void TimerWidget::removeTimer(uint64_t timerId) {
auto it = this->activeTimers.find(timerId);
if (it != this->activeTimers.end()) {
this->remove(*it->second);
this->activeTimers.erase(it);
}
}
void TimerWidget::activateTimer(uint64_t timerId) {
auto it = this->activeTimers.find(timerId);
if (it != this->activeTimers.end()) {
it->second->activateTimer();
}
}

View File

@@ -8,7 +8,6 @@
#include "gdkmm/monitor.h" #include "gdkmm/monitor.h"
#include "glibmm/main.h" #include "glibmm/main.h"
#include "glibmm/object.h"
#include "gtk4-layer-shell.h" #include "gtk4-layer-shell.h"
#include "gtkmm/button.h" #include "gtkmm/button.h"
#include "gtkmm/cssprovider.h" #include "gtkmm/cssprovider.h"
@@ -42,17 +41,17 @@ BaseNotification::BaseNotification(uint64_t notificationId, std::shared_ptr<Gdk:
return; return;
} }
} }
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->notificationId);
}); });
add_controller(window_click); add_controller(window_click);
auto window_motion = Gtk::EventControllerMotion::create(); auto window_motion = Gtk::EventControllerMotion::create();
window_motion->signal_enter().connect([this](double, double) { window_motion->signal_enter().connect([this](double, double) {
signal_hover_changed.emit(true); getSignalHoverChanged().emit(true);
pause_auto_close(); pause_auto_close();
}); });
window_motion->signal_leave().connect([this]() { window_motion->signal_leave().connect([this]() {
signal_hover_changed.emit(false); getSignalHoverChanged().emit(false);
resume_auto_close(); resume_auto_close();
}); });
add_controller(window_motion); add_controller(window_motion);
@@ -82,8 +81,8 @@ void BaseNotification::start_auto_close_timeout(int timeoutMs) {
} }
autoCloseDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); autoCloseDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
autoCloseConnection = Glib::signal_timeout().connect([this]() { autoCloseConnection = Glib::signal_timeout().connect([self = shared_from_this()]() {
this->signal_close.emit(this->notificationId); self->getSignalClose().emit(self->getNotificationId());
return false; // Don't repeat return false; // Don't repeat
}, },
timeoutMs); timeoutMs);
@@ -111,7 +110,7 @@ void BaseNotification::resume_auto_close() {
autoClosePaused = false; autoClosePaused = false;
if (autoCloseRemainingMs <= 0) { if (autoCloseRemainingMs <= 0) {
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->notificationId);
return; return;
} }

View File

@@ -3,6 +3,8 @@
#include <memory> #include <memory>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "components/button/iconButton.hpp"
#include "glibmm/datetime.h" #include "glibmm/datetime.h"
#include "glibmm/fileutils.h" #include "glibmm/fileutils.h"
#include "glibmm/miscutils.h" #include "glibmm/miscutils.h"
@@ -77,7 +79,6 @@ void CopyNotification::createImageNotification(NotifyMessage notify) {
auto contentBox = Gtk::make_managed<Gtk::Box>(); auto contentBox = Gtk::make_managed<Gtk::Box>();
contentBox->set_orientation(Gtk::Orientation::VERTICAL); contentBox->set_orientation(Gtk::Orientation::VERTICAL);
contentBox->set_margin(0);
auto imageWidget = Gtk::make_managed<Gtk::Image>(this->copiedImage); auto imageWidget = Gtk::make_managed<Gtk::Image>(this->copiedImage);
contentBox->append(*imageWidget); contentBox->append(*imageWidget);
imageWidget->set_pixel_size(300); imageWidget->set_pixel_size(300);
@@ -85,34 +86,27 @@ void CopyNotification::createImageNotification(NotifyMessage notify) {
auto buttonBox = Gtk::make_managed<Gtk::Box>(); auto buttonBox = Gtk::make_managed<Gtk::Box>();
buttonBox->set_spacing(10); buttonBox->set_spacing(10);
// material icons unicode auto saveToClipboardButton = Gtk::make_managed<IconButton>(Icon::CONTENT_COPY);
auto saveToClipboardLabel = Gtk::make_managed<Gtk::Label>("\uF0EA"); // content copy icon
auto saveToFileLabel = Gtk::make_managed<Gtk::Label>("\uF1C5"); // save icon
auto saveToClipboardButton = Gtk::make_managed<Gtk::Button>();
saveToClipboardButton->set_child(*saveToClipboardLabel);
saveToClipboardButton->signal_clicked().connect([this]() { saveToClipboardButton->signal_clicked().connect([this]() {
copyToClipboard(this->copiedImage); copyToClipboard(this->copiedImage);
spdlog::info("Copied image to clipboard"); spdlog::info("Copied image to clipboard");
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->getNotificationId());
}); });
saveToClipboardButton->set_tooltip_text("Copy to Clipboard"); saveToClipboardButton->set_tooltip_text("Copy to Clipboard");
saveToClipboardButton->add_css_class("notification-button"); saveToClipboardButton->add_css_class("notification-button");
saveToClipboardButton->add_css_class("notification-icon-button"); saveToClipboardButton->add_css_class("notification-icon-button");
auto saveToFileButton = Gtk::make_managed<Gtk::Button>(); auto saveToFileButton = Gtk::make_managed<IconButton>(Icon::SAVE);
saveToFileButton->set_child(*saveToFileLabel);
saveToFileButton->signal_clicked().connect([this]() { saveToFileButton->signal_clicked().connect([this]() {
// xdg-pic/screenshot // use env
auto xdgPicturesDir = Glib::get_user_special_dir(Glib::UserDirectory::PICTURES); auto xdgPicturesDir = Glib::get_user_special_dir(Glib::UserDirectory::PICTURES);
auto dateStamp = Glib::DateTime::create_now_local().format("%Y%m%d_%H%M%S"); auto dateStamp = Glib::DateTime::create_now_local().format("%Y%m%d_%H%M%S");
auto filepath = xdgPicturesDir + "/screenshot" ; auto filepath = xdgPicturesDir + "/screenshot";
auto filename = dateStamp + ".png"; auto filename = dateStamp + ".png";
saveImageToFile(this->copiedImage, filepath, filename); saveImageToFile(this->copiedImage, filepath, filename);
spdlog::info("Saved image to {}", filepath.c_str()); spdlog::info("Saved image to {}", filepath.c_str());
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->getNotificationId());
}); });
saveToFileButton->set_tooltip_text("Save to File"); saveToFileButton->set_tooltip_text("Save to File");
saveToFileButton->add_css_class("notification-button"); saveToFileButton->add_css_class("notification-button");
@@ -123,7 +117,6 @@ void CopyNotification::createImageNotification(NotifyMessage notify) {
contentBox->append(*buttonBox); contentBox->append(*buttonBox);
this->mainBox.append(*contentBox); this->mainBox.append(*contentBox);
} }
void CopyNotification::createTextNotification(NotifyMessage notify) { void CopyNotification::createTextNotification(NotifyMessage notify) {
@@ -136,12 +129,10 @@ void CopyNotification::createTextNotification(NotifyMessage notify) {
textLabel->set_margin_bottom(10); textLabel->set_margin_bottom(10);
contentBox->append(*textLabel); contentBox->append(*textLabel);
auto copyToClipboardButton = Gtk::make_managed<Gtk::Button>(); auto copyToClipboardButton = Gtk::make_managed<IconButton>(Icon::CONTENT_COPY);
auto copyToClipboardLabel = Gtk::make_managed<Gtk::Label>("\uF0EA"); // content copy icon
copyToClipboardButton->set_child(*copyToClipboardLabel);
copyToClipboardButton->signal_clicked().connect([this]() { copyToClipboardButton->signal_clicked().connect([this]() {
copyToClipboard(this->copiedText); copyToClipboard(this->copiedText);
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->getNotificationId());
}); });
copyToClipboardButton->set_tooltip_text("Copy to Clipboard"); copyToClipboardButton->set_tooltip_text("Copy to Clipboard");
copyToClipboardButton->add_css_class("notification-icon-button"); copyToClipboardButton->add_css_class("notification-icon-button");

View File

@@ -1,32 +1,33 @@
#include "widgets/notification/notificationWindow.hpp" #include "widgets/notification/notificationWindow.hpp"
#include <cstdint> #include <cstdint>
#include <sys/types.h> #include <sys/types.h>
#include "components/button/textButton.hpp"
#include "helpers/string.hpp" #include "helpers/string.hpp"
#include "gtkmm/box.h" #include "gtkmm/box.h"
#include "gtkmm/button.h" #include "gtkmm/entry.h"
#include "gtkmm/gestureclick.h"
#include "gtkmm/image.h" #include "gtkmm/image.h"
#include "gtkmm/label.h" #include "gtkmm/label.h"
NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor, NotifyMessage notify) : BaseNotification(notificationId, monitor) { NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor, NotifyMessage notify) : BaseNotification(notificationId, monitor) {
set_title(notify.summary); set_title(notify.summary);
// Main vertical box // Main vertical box
auto vbox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 8); auto vbox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 8);
vbox->set_halign(Gtk::Align::FILL); vbox->set_halign(Gtk::Align::FILL);
switch (notify.urgency) { switch (notify.urgency) {
case NotificationUrgency::CRITICAL: case NotificationUrgency::CRITICAL:
add_css_class("notification-critical"); add_css_class("notification-critical");
break; break;
case NotificationUrgency::NORMAL: case NotificationUrgency::NORMAL:
add_css_class("notification-normal"); add_css_class("notification-normal");
break; break;
case NotificationUrgency::LOW: case NotificationUrgency::LOW:
add_css_class("notification-low"); add_css_class("notification-low");
break; break;
} }
auto header_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL); auto header_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
@@ -44,26 +45,49 @@ NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr<
header_box->append(*img); header_box->append(*img);
} }
auto app_label = Gtk::make_managed<Gtk::Label>(StringHelper::trimToSize(notify.app_name, 24)); auto app_label = Gtk::make_managed<Gtk::Label>(notify.app_name);
app_label->set_halign(Gtk::Align::START); app_label->set_halign(Gtk::Align::START);
app_label->set_ellipsize(Pango::EllipsizeMode::END);
app_label->set_max_width_chars(24);
app_label->set_wrap(false); app_label->set_wrap(false);
app_label->add_css_class("notification-app-name"); app_label->add_css_class("notification-app-name");
header_box->append(*app_label); header_box->append(*app_label);
vbox->append(*header_box); vbox->append(*header_box);
// Summary label // Summary label
auto summary_label = Gtk::make_managed<Gtk::Label>("<b>" + StringHelper::trimToSize(notify.summary, 20) + "</b>"); auto summary_label = Gtk::make_managed<Gtk::Label>("<b>" + notify.summary + "</b>");
summary_label->set_use_markup(true); summary_label->set_use_markup(true);
summary_label->set_halign(Gtk::Align::START); summary_label->set_halign(Gtk::Align::START);
summary_label->set_ellipsize(Pango::EllipsizeMode::END);
summary_label->set_lines(1);
summary_label->set_wrap(true); summary_label->set_wrap(true);
vbox->append(*summary_label); vbox->append(*summary_label);
auto body_label = Gtk::make_managed<Gtk::Label>(StringHelper::trimToSize(notify.body, 100)); auto body_label = Gtk::make_managed<Gtk::Label>(notify.body);
body_label->set_use_markup(true); body_label->set_use_markup(true);
body_label->set_halign(Gtk::Align::START); body_label->set_halign(Gtk::Align::START);
body_label->set_ellipsize(Pango::EllipsizeMode::END);
body_label->set_lines(3);
body_label->set_wrap(true); body_label->set_wrap(true);
vbox->append(*body_label); vbox->append(*body_label);
if (notify.has_input) {
auto entry = Gtk::make_managed<Gtk::Entry>();
entry->set_placeholder_text(notify.input_placeholder);
entry->set_halign(Gtk::Align::FILL);
entry->set_hexpand(true);
entry->add_css_class("notification-entry");
vbox->append(*entry);
entry->grab_focus_without_selecting();
entry->signal_activate().connect([this, entry, cb = notify.on_input]() {
if (cb) {
cb(entry->get_text());
this->getSignalClose().emit(this->getNotificationId());
}
});
}
// If actions exist, add buttons // If actions exist, add buttons
if (!notify.actions.empty()) { if (!notify.actions.empty()) {
auto actions_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6); auto actions_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
@@ -71,28 +95,27 @@ NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr<
std::string action_id = notify.actions[i]; std::string action_id = notify.actions[i];
std::string action_label = notify.actions[i + 1]; std::string action_label = notify.actions[i + 1];
auto btn = Gtk::make_managed<Gtk::Button>(); auto btn = Gtk::make_managed<TextButton>(action_label);
btn->set_label(action_label);
btn->add_css_class("notification-button"); btn->add_css_class("notification-button");
switch (notify.urgency) { switch (notify.urgency) {
case NotificationUrgency::CRITICAL: case NotificationUrgency::CRITICAL:
btn->add_css_class("notification-critical"); btn->add_css_class("notification-critical");
break; break;
case NotificationUrgency::NORMAL: case NotificationUrgency::NORMAL:
btn->add_css_class("notification-normal"); btn->add_css_class("notification-normal");
break; break;
case NotificationUrgency::LOW: case NotificationUrgency::LOW:
btn->add_css_class("notification-low"); btn->add_css_class("notification-low");
break; break;
} }
btn->signal_clicked().connect([this, action_id, cb = notify.on_action, guard = notify.actionInvoked]() { btn->signal_clicked().connect([this, action_id, cb = notify.on_action, guard = notify.actionInvoked]() {
if (cb && guard && !*guard) { if (cb && guard && !*guard) {
*guard = true; *guard = true;
cb(action_id); cb(action_id);
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->getNotificationId());
} }
}); });
actions_box->append(*btn); actions_box->append(*btn);

View File

@@ -1,6 +1,8 @@
#include "widgets/notification/spotifyNotification.hpp" #include "widgets/notification/spotifyNotification.hpp"
#include <sys/types.h> #include <sys/types.h>
#include "components/button/iconButton.hpp"
#include "helpers/string.hpp" #include "helpers/string.hpp"
#include "services/textureCache.hpp" #include "services/textureCache.hpp"
@@ -34,10 +36,12 @@ SpotifyNotification::SpotifyNotification(uint64_t notificationId, std::shared_pt
auto artistLabel = Gtk::make_managed<Gtk::Label>(); auto artistLabel = Gtk::make_managed<Gtk::Label>();
if (!mpris.artist.empty()) { if (!mpris.artist.empty()) {
artistLabel->set_text(StringHelper::trimToSize(mpris.artist[0], 30)); artistLabel->set_text(mpris.artist[0]);
} else { } else {
artistLabel->set_text("Unknown Artist"); artistLabel->set_text("Unknown Artist");
} }
artistLabel->set_ellipsize(Pango::EllipsizeMode::END);
artistLabel->set_max_width_chars(30);
artistLabel->set_hexpand(true); artistLabel->set_hexpand(true);
artistLabel->set_halign(Gtk::Align::CENTER); artistLabel->set_halign(Gtk::Align::CENTER);
@@ -58,17 +62,17 @@ std::unique_ptr<Gtk::CenterBox> SpotifyNotification::createButtonBox(MprisPlayer
buttonBox->set_hexpand(true); buttonBox->set_hexpand(true);
buttonBox->set_halign(Gtk::Align::CENTER); buttonBox->set_halign(Gtk::Align::CENTER);
auto backButton = Gtk::make_managed<Gtk::Button>("\ue045"); auto backButton = Gtk::make_managed<IconButton>(Icon::SKIP_PREVIOUS);
backButton->add_css_class("notification-icon-button"); backButton->add_css_class("notification-icon-button");
backButton->add_css_class("notification-button"); backButton->add_css_class("notification-button");
backButton->signal_clicked().connect([this, mpris]() { backButton->signal_clicked().connect([this, mpris]() {
if (mpris.previous) { if (mpris.previous) {
mpris.previous(); mpris.previous();
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->getNotificationId());
} }
}); });
auto playPauseButton = Gtk::make_managed<Gtk::Button>("\ue037"); auto playPauseButton = Gtk::make_managed<IconButton>(Icon::PLAY_ARROW);
playPauseButton->add_css_class("notification-icon-button"); playPauseButton->add_css_class("notification-icon-button");
playPauseButton->add_css_class("notification-button"); playPauseButton->add_css_class("notification-button");
playPauseButton->signal_clicked().connect([playPauseButton, mpris]() { playPauseButton->signal_clicked().connect([playPauseButton, mpris]() {
@@ -77,21 +81,21 @@ std::unique_ptr<Gtk::CenterBox> SpotifyNotification::createButtonBox(MprisPlayer
static bool isPlaying = false; static bool isPlaying = false;
if (isPlaying) { if (isPlaying) {
playPauseButton->set_label("\ue037"); playPauseButton->setIcon(Icon::PLAY_ARROW);
} else { } else {
playPauseButton->set_label("\ue034"); playPauseButton->setIcon(Icon::PAUSE);
} }
isPlaying = !isPlaying; isPlaying = !isPlaying;
} }
}); });
auto nextButton = Gtk::make_managed<Gtk::Button>("\ue044"); auto nextButton = Gtk::make_managed<IconButton>(Icon::SKIP_NEXT);
nextButton->add_css_class("notification-icon-button"); nextButton->add_css_class("notification-icon-button");
nextButton->add_css_class("notification-button"); nextButton->add_css_class("notification-button");
nextButton->signal_clicked().connect([this, mpris]() { nextButton->signal_clicked().connect([this, mpris]() {
if (mpris.next) { if (mpris.next) {
mpris.next(); mpris.next();
this->signal_close.emit(this->notificationId); this->getSignalClose().emit(this->getNotificationId());
} }
}); });
buttonBox->set_start_widget(*backButton); buttonBox->set_start_widget(*backButton);

View File

@@ -1,13 +1,12 @@
#include "widgets/tray.hpp" #include "widgets/tray.hpp"
#include <cmath>
#include <gdkmm/rectangle.h> #include <gdkmm/rectangle.h>
#include <gio/gmenu.h> #include <gio/gmenu.h>
#include <gtk/gtk.h>
#include <cmath>
#include <graphene.h> #include <graphene.h>
#include <utility> #include <gtk/gtk.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "components/base/button.hpp" #include <utility>
namespace { namespace {
bool is_wayland_display(GtkWidget *widget) { bool is_wayland_display(GtkWidget *widget) {
@@ -182,9 +181,8 @@ void log_menu_tree(const std::vector<TrayService::MenuNode> &nodes,
} }
} // namespace } // namespace
TrayIconWidget::TrayIconWidget( std::string id) TrayIconWidget::TrayIconWidget(Icon::Type icon, std::string id) : IconButton(icon), id(std::move(id)),
: Button(id), id(std::move(id)), container(Gtk::Orientation::HORIZONTAL) {
container(Gtk::Orientation::HORIZONTAL) {
aliveFlag = std::make_shared<bool>(true); aliveFlag = std::make_shared<bool>(true);
set_has_frame(false); set_has_frame(false);
set_focusable(false); set_focusable(false);
@@ -245,8 +243,8 @@ TrayIconWidget::~TrayIconWidget() {
} }
void TrayIconWidget::update(const TrayService::Item &item) { void TrayIconWidget::update(const TrayService::Item &item) {
hasRemoteMenu = item.menuAvailable; hasRemoteMenu = item.menuAvailable;
menuPopupPending = false; menuPopupPending = false;
menuRequestInFlight = false; menuRequestInFlight = false;
if (!item.menuAvailable) { if (!item.menuAvailable) {
@@ -268,6 +266,7 @@ void TrayIconWidget::update(const TrayService::Item &item) {
picture.set_visible(true); picture.set_visible(true);
image.set_visible(false); image.set_visible(false);
} else if (!item.iconName.empty()) { } else if (!item.iconName.empty()) {
spdlog::info("App {} requesting icon name: {}", id, item.iconName); // Add this
image.set_from_icon_name(item.iconName); image.set_from_icon_name(item.iconName);
image.set_pixel_size(20); image.set_pixel_size(20);
image.set_visible(true); image.set_visible(true);
@@ -352,7 +351,7 @@ void TrayIconWidget::on_secondary_released(int /*n_press*/, double x,
} }
menuRequestInFlight = true; menuRequestInFlight = true;
auto weak = std::weak_ptr<bool>(aliveFlag); auto weak = std::weak_ptr<bool>(aliveFlag);
service.request_menu_layout( service.request_menu_layout(
id, [weak, this](std::optional<TrayService::MenuNode> layout) { id, [weak, this](std::optional<TrayService::MenuNode> layout) {
if (auto locked = weak.lock()) { if (auto locked = weak.lock()) {
@@ -404,8 +403,8 @@ void TrayIconWidget::on_menu_layout_ready(
const auto &layout = *layoutOpt; const auto &layout = *layoutOpt;
log_menu_tree(layout.children, 0); log_menu_tree(layout.children, 0);
auto menu = Gio::Menu::create(); auto menu = Gio::Menu::create();
auto actions = Gio::SimpleActionGroup::create(); auto actions = Gio::SimpleActionGroup::create();
populate_menu_items(layout.children, menu, actions); populate_menu_items(layout.children, menu, actions);
@@ -531,8 +530,8 @@ void TrayIconWidget::on_menu_action(const Glib::VariantBase & /*parameter*/,
} }
bool TrayIconWidget::try_get_pending_coords(int32_t &outX, int32_t &outY) const { bool TrayIconWidget::try_get_pending_coords(int32_t &outX, int32_t &outY) const {
outX = -1; outX = -1;
outY = -1; outY = -1;
int32_t sendX = static_cast<int32_t>(std::lround(pendingX)); int32_t sendX = static_cast<int32_t>(std::lround(pendingX));
int32_t sendY = static_cast<int32_t>(std::lround(pendingY)); int32_t sendY = static_cast<int32_t>(std::lround(pendingY));
if (!try_get_global_click_coords(GTK_WIDGET(gobj()), pendingX, pendingY, if (!try_get_global_click_coords(GTK_WIDGET(gobj()), pendingX, pendingY,
@@ -592,7 +591,7 @@ void TrayWidget::on_item_added(const TrayService::Item &item) {
return; return;
} }
auto icon = std::make_unique<TrayIconWidget>(item.id); auto icon = std::make_unique<TrayIconWidget>(Icon::Type::CONTENT_COPY, item.id);
icon->update(item); icon->update(item);
auto *raw = icon.get(); auto *raw = icon.get();
append(*raw); append(*raw);

View File

@@ -1,9 +1,9 @@
#include "widgets/volumeWidget.hpp" #include "widgets/volumeWidget.hpp"
#include <cmath> #include <cmath>
#include <spdlog/spdlog.h>
#include <regex> #include <regex>
#include <sigc++/functors/mem_fun.h> #include <sigc++/functors/mem_fun.h>
#include <spdlog/spdlog.h>
#include "helpers/command.hpp" #include "helpers/command.hpp"

View File

@@ -32,10 +32,6 @@ WeatherWidget::WeatherWidget()
: Gtk::Box(Gtk::Orientation::VERTICAL) { : Gtk::Box(Gtk::Orientation::VERTICAL) {
this->set_orientation(Gtk::Orientation::VERTICAL); this->set_orientation(Gtk::Orientation::VERTICAL);
this->set_spacing(6); this->set_spacing(6);
this->set_margin_top(4);
this->set_margin_bottom(4);
this->set_margin_start(4);
this->set_margin_end(4);
this->titleLabel.set_text("Weather"); this->titleLabel.set_text("Weather");
this->currentLabel.set_text("Now: --"); this->currentLabel.set_text("Now: --");

View File

@@ -3,7 +3,9 @@
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <webkit/webkit.h> #include <webkit/webkit.h>
WebWidget::WebWidget(std::string icon, std::string name, std::string url) : Popover(icon, name) { #include "components/button/iconButton.hpp"
WebWidget::WebWidget(Icon::Type icon, std::string name, std::string url) : Popover(icon, name) {
auto webview = webkit_web_view_new(); auto webview = webkit_web_view_new();
gtk_widget_set_hexpand(webview, true); gtk_widget_set_hexpand(webview, true);
gtk_widget_set_vexpand(webview, true); gtk_widget_set_vexpand(webview, true);

View File

@@ -1,580 +0,0 @@
{
"latitude": 49.0,
"longitude": 8.4,
"generationtime_ms": 0.23317337036132812,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"timezone_abbreviation": "GMT+1",
"elevation": 122.0,
"current_units": {
"time": "iso8601",
"interval": "seconds",
"temperature_2m": "°C"
},
"current": {
"time": "2026-02-03T23:00",
"interval": 900,
"temperature_2m": -0.7
},
"hourly_units": {
"time": "iso8601",
"temperature_2m": "°C",
"rain": "mm"
},
"hourly": {
"time": [
"2026-02-03T00:00",
"2026-02-03T01:00",
"2026-02-03T02:00",
"2026-02-03T03:00",
"2026-02-03T04:00",
"2026-02-03T05:00",
"2026-02-03T06:00",
"2026-02-03T07:00",
"2026-02-03T08:00",
"2026-02-03T09:00",
"2026-02-03T10:00",
"2026-02-03T11:00",
"2026-02-03T12:00",
"2026-02-03T13:00",
"2026-02-03T14:00",
"2026-02-03T15:00",
"2026-02-03T16:00",
"2026-02-03T17:00",
"2026-02-03T18:00",
"2026-02-03T19:00",
"2026-02-03T20:00",
"2026-02-03T21:00",
"2026-02-03T22:00",
"2026-02-03T23:00",
"2026-02-04T00:00",
"2026-02-04T01:00",
"2026-02-04T02:00",
"2026-02-04T03:00",
"2026-02-04T04:00",
"2026-02-04T05:00",
"2026-02-04T06:00",
"2026-02-04T07:00",
"2026-02-04T08:00",
"2026-02-04T09:00",
"2026-02-04T10:00",
"2026-02-04T11:00",
"2026-02-04T12:00",
"2026-02-04T13:00",
"2026-02-04T14:00",
"2026-02-04T15:00",
"2026-02-04T16:00",
"2026-02-04T17:00",
"2026-02-04T18:00",
"2026-02-04T19:00",
"2026-02-04T20:00",
"2026-02-04T21:00",
"2026-02-04T22:00",
"2026-02-04T23:00",
"2026-02-05T00:00",
"2026-02-05T01:00",
"2026-02-05T02:00",
"2026-02-05T03:00",
"2026-02-05T04:00",
"2026-02-05T05:00",
"2026-02-05T06:00",
"2026-02-05T07:00",
"2026-02-05T08:00",
"2026-02-05T09:00",
"2026-02-05T10:00",
"2026-02-05T11:00",
"2026-02-05T12:00",
"2026-02-05T13:00",
"2026-02-05T14:00",
"2026-02-05T15:00",
"2026-02-05T16:00",
"2026-02-05T17:00",
"2026-02-05T18:00",
"2026-02-05T19:00",
"2026-02-05T20:00",
"2026-02-05T21:00",
"2026-02-05T22:00",
"2026-02-05T23:00",
"2026-02-06T00:00",
"2026-02-06T01:00",
"2026-02-06T02:00",
"2026-02-06T03:00",
"2026-02-06T04:00",
"2026-02-06T05:00",
"2026-02-06T06:00",
"2026-02-06T07:00",
"2026-02-06T08:00",
"2026-02-06T09:00",
"2026-02-06T10:00",
"2026-02-06T11:00",
"2026-02-06T12:00",
"2026-02-06T13:00",
"2026-02-06T14:00",
"2026-02-06T15:00",
"2026-02-06T16:00",
"2026-02-06T17:00",
"2026-02-06T18:00",
"2026-02-06T19:00",
"2026-02-06T20:00",
"2026-02-06T21:00",
"2026-02-06T22:00",
"2026-02-06T23:00",
"2026-02-07T00:00",
"2026-02-07T01:00",
"2026-02-07T02:00",
"2026-02-07T03:00",
"2026-02-07T04:00",
"2026-02-07T05:00",
"2026-02-07T06:00",
"2026-02-07T07:00",
"2026-02-07T08:00",
"2026-02-07T09:00",
"2026-02-07T10:00",
"2026-02-07T11:00",
"2026-02-07T12:00",
"2026-02-07T13:00",
"2026-02-07T14:00",
"2026-02-07T15:00",
"2026-02-07T16:00",
"2026-02-07T17:00",
"2026-02-07T18:00",
"2026-02-07T19:00",
"2026-02-07T20:00",
"2026-02-07T21:00",
"2026-02-07T22:00",
"2026-02-07T23:00",
"2026-02-08T00:00",
"2026-02-08T01:00",
"2026-02-08T02:00",
"2026-02-08T03:00",
"2026-02-08T04:00",
"2026-02-08T05:00",
"2026-02-08T06:00",
"2026-02-08T07:00",
"2026-02-08T08:00",
"2026-02-08T09:00",
"2026-02-08T10:00",
"2026-02-08T11:00",
"2026-02-08T12:00",
"2026-02-08T13:00",
"2026-02-08T14:00",
"2026-02-08T15:00",
"2026-02-08T16:00",
"2026-02-08T17:00",
"2026-02-08T18:00",
"2026-02-08T19:00",
"2026-02-08T20:00",
"2026-02-08T21:00",
"2026-02-08T22:00",
"2026-02-08T23:00",
"2026-02-09T00:00",
"2026-02-09T01:00",
"2026-02-09T02:00",
"2026-02-09T03:00",
"2026-02-09T04:00",
"2026-02-09T05:00",
"2026-02-09T06:00",
"2026-02-09T07:00",
"2026-02-09T08:00",
"2026-02-09T09:00",
"2026-02-09T10:00",
"2026-02-09T11:00",
"2026-02-09T12:00",
"2026-02-09T13:00",
"2026-02-09T14:00",
"2026-02-09T15:00",
"2026-02-09T16:00",
"2026-02-09T17:00",
"2026-02-09T18:00",
"2026-02-09T19:00",
"2026-02-09T20:00",
"2026-02-09T21:00",
"2026-02-09T22:00",
"2026-02-09T23:00"
],
"temperature_2m": [
2.2,
2.2,
2.2,
2.2,
2.0,
1.9,
1.8,
1.5,
1.2,
1.4,
1.8,
2.2,
2.5,
1.9,
2.6,
3.0,
2.7,
3.0,
3.0,
2.4,
1.7,
1.3,
0.9,
-0.7,
-0.2,
-0.1,
-0.5,
-0.7,
-1.4,
-1.1,
-1.0,
-1.0,
-1.1,
-0.9,
1.7,
3.7,
5.0,
5.8,
6.5,
6.9,
6.6,
5.9,
4.8,
3.6,
2.7,
2.0,
1.5,
1.1,
0.5,
0.2,
0.4,
0.9,
1.2,
1.3,
1.5,
1.7,
1.8,
2.0,
2.2,
2.7,
3.4,
4.2,
5.2,
6.1,
6.4,
5.9,
4.5,
3.2,
2.4,
1.9,
1.4,
1.1,
1.0,
0.8,
0.5,
0.3,
0.1,
-0.1,
-0.2,
-0.3,
-0.3,
-0.1,
0.7,
1.7,
2.9,
4.1,
5.0,
5.2,
5.1,
4.8,
4.3,
4.1,
4.2,
4.1,
3.8,
3.5,
3.3,
3.4,
3.4,
3.5,
3.5,
3.5,
3.4,
3.4,
3.5,
3.6,
4.1,
5.2,
6.6,
7.8,
8.4,
8.8,
8.8,
8.1,
6.9,
5.9,
5.3,
4.9,
4.6,
4.3,
4.2,
4.0,
3.9,
3.9,
3.8,
3.8,
3.8,
3.9,
4.3,
4.7,
5.3,
6.0,
6.9,
7.5,
7.6,
7.5,
7.2,
6.6,
5.8,
5.0,
4.4,
3.9,
3.5,
3.1,
2.9,
2.8,
3.0,
3.3,
3.6,
3.5,
3.4,
3.4,
3.7,
4.2,
4.7,
5.3,
6.0,
6.5,
6.8,
6.8,
6.6,
5.9,
4.8,
3.9,
3.5,
3.3,
3.1,
2.9
],
"rain": [
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.10,
0.40,
0.00,
0.00,
0.20,
0.60,
0.30,
1.30,
0.00,
0.00,
0.40,
0.10,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.10,
0.20,
0.00,
0.00,
0.00,
0.10,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00,
0.00
]
},
"daily_units": {
"time": "iso8601",
"temperature_2m_max": "°C",
"temperature_2m_min": "°C",
"weather_code": "wmo code"
},
"daily": {
"time": [
"2026-02-03",
"2026-02-04",
"2026-02-05",
"2026-02-06",
"2026-02-07",
"2026-02-08",
"2026-02-09"
],
"temperature_2m_max": [
3.0,
6.9,
6.4,
5.2,
8.8,
7.6,
6.8
],
"temperature_2m_min": [
-0.7,
-1.4,
0.2,
-0.3,
3.3,
3.1,
2.8
],
"weather_code": [
61,
45,
45,
61,
61,
3,
3
]
}
}