diff --git a/.gitignore b/.gitignore index b7a9284..94dd2dc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ __dummy.html *.obj dub.selections.json dwin-bar -.vscode/ \ No newline at end of file +.vscode/ +/config.json +/nohup.out +/spotify-auth.json diff --git a/config.json.sample b/config.json.sample new file mode 100644 index 0000000..ef40413 --- /dev/null +++ b/config.json.sample @@ -0,0 +1,41 @@ +{ + "fontPrimary": "Roboto:Medium", + "fontSecondary": "Roboto:Light", + "height": 32, + "spotify": { // or omit this block + "clientId": "", + "clientSecret": "", + "redirectURL": "http://127.0.0.1:3007/spotify/auth" + }, + "panels": [ + { + "screen": "First", + "dock": "Bottom", + "widgets": [ + "dwinbar.widgets.clock.ClockWidget", + "dwinbar.widgets.volume.VolumeWidget", + { + "type": "dwinbar.widgets.mediaplayer.MprisMediaPlayerWidget", + "dest": "org.mpris.MediaPlayer2.spotify", + "spotify": true + }, + "dwinbar.widgets.phone_battery.PhoneBatteryWidget", + { + "type": "dwinbar.widgets.workspace.WorkspaceWidget", + "display": "DisplayPort-0" + } + ] + }, + { + "screen": "Second", + "dock": "Bottom", + "widgets": [ + "dwinbar.widgets.clock.ClockWidget", + { + "type": "dwinbar.widgets.workspace.WorkspaceWidget", + "display": "DisplayPort-1" + } + ] + } + ] +} \ No newline at end of file diff --git a/dub.json b/dub.json index ae85802..dbe391b 100644 --- a/dub.json +++ b/dub.json @@ -10,7 +10,8 @@ "imageformats": "~>7.0.0", "derelict-ft": "~>2.0.0-beta.4", "eventsystem": "~>1.2.0", - "ddbus": "~>2.3.0" + "ddbus": "~>2.3.0", + "vibe-d": "~>0.8.5-rc.1" }, "libs": [ "Xinerama" diff --git a/source/app.d b/source/app.d index 86ae2d5..41b237f 100644 --- a/source/app.d +++ b/source/app.d @@ -6,7 +6,10 @@ import dwinbar.widgets.notifications; import dwinbar.widgets.volume; import dwinbar.widgets.workspace; +import dwinbar.widget; + import dwinbar.kdeconnect; +import dwinbar.webserver; /*import dwinbar.widgets.volume; import dwinbar.widgets.tray; @@ -14,8 +17,71 @@ import dwinbar.widgets.workspace;*/ import dwinbar.bar; +import std.conv; +import std.string; +import std.meta; + +import vibe.core.file; +import vibe.core.log; +import vibe.data.json; + +bool applyBarConfig(string setting, Json value, ref BarConfiguration config) +{ + switch (setting) + { + static foreach (member; AliasSeq!("fontPrimary", "fontSecondary", + "fontNeutral", "fontFallback")) + { + case member: + __traits(getMember, config, member) = value.to!string; + return true; + } + case "displayName": + config.displayName = cast(char*) value.to!string.dup.toStringz; + return true; + case "spotify": + clientId = value["clientId"].to!string; + clientSecret = value["clientSecret"].to!string; + redirectURL = value["redirectURL"].to!string; + return true; + default: + return false; + } +} + +bool applyPanelConfig(string setting, Json value, ref PanelConfiguration config, + ref Screen screen, ref Dock dock) +{ + switch (setting) + { + static foreach (member; AliasSeq!("height", "barBaselinePadding", "appIconPadding", + "appBaselineMargin", "widgetBaselineMargin", "focusStripeHeight", "offsetX", "offsetY")) + { + case member: + __traits(getMember, config, member) = value.to!int; + return true; + } + case "enableAppList": + config.enableAppList = value.to!bool; + return true; + case "screen": + screen = value.to!string + .to!Screen; + return true; + case "dock": + dock = value.to!string + .to!Dock; + return true; + default: + return false; + } +} + void main(string[] args) { + Json[string] userConfig = parseJsonString(readFileUTF8("config.json"), "config.json").get!( + Json[string]); + BarConfiguration config; config.fontPrimary = "Roboto:Medium"; config.fontSecondary = "Roboto:Light"; @@ -23,37 +89,119 @@ void main(string[] args) PanelConfiguration panelConfig; panelConfig.height = 32; + Json[] panels; + + foreach (k, v; userConfig) + { + if (k == "panels") + { + panels = v.get!(Json[]); + continue; + } + + if (applyBarConfig(k, v, config)) + continue; + + Screen screen; + Dock dock; + if (applyPanelConfig(k, v, panelConfig, screen, dock)) + continue; + + logInfo("Unknown json setting: %s: %s", k, v); + } + Bar bar = loadBar(config); + startSpotifyWebServer(); + scope (exit) + { + import std.concurrency : send; - string left = args.length > 1 ? args[1] : null; - string right = args.length > 2 ? args[2] : null; - - auto phones = KDEConnectDevice.listDevices(); - - //dfmt off - auto panel1 = bar.addPanel(Screen.First, Dock.Bottom, panelConfig) - .add(new ClockWidget()) - // find using `dbus-send --print-reply --system --dest=org.freedesktop.UPower /org/freedesktop/UPower org.freedesktop.UPower.EnumerateDevices` - //.add(new BatteryWidget(bar.fontFamily, "/org/freedesktop/UPower/devices/battery_BAT1")) - //.add(new NotificationsWidget(&bar)) - .add(new VolumeWidget()) - .add(new MprisMediaPlayerWidget(bar.fontFamily, "org.mpris.MediaPlayer2.spotify")) - //.add(new WorkspaceWidget(bar.x, "DisplayPort-1")) - ; - if (phones.length) - panel1.add(new PhoneBatteryWidget(bar.fontFamily, phones[0])); - - if (left.length) - panel1.add(new WorkspaceWidget(bar.x, left)); - - if (right.length) - bar.addPanel(Screen.Second, Dock.Bottom, panelConfig) - .add(new ClockWidget()) - .add(new WorkspaceWidget(bar.x, right)) - //.add(new WorkspaceWidget(bar.x, "DisplayPort-0")) - ; - //dfmt on + send(webServerThread, DoExit()); + } + + foreach (panel; panels) + { + bar.loadPanel(panel.get!(Json[string]), panelConfig); + } //bar.tray = panel1; bar.start(); } + +void loadPanel(ref Bar bar, Json[string] settings, PanelConfiguration panelConfig) +{ + Screen screen = Screen.First; + Dock dock = Dock.Bottom; + + Json[] widgets; + foreach (k, v; settings) + { + if (k == "widgets") + { + widgets = v.get!(Json[]); + continue; + } + + if (applyPanelConfig(k, v, panelConfig, screen, dock)) + continue; + + logInfo("Unknown panel setting: %s: %s", k, v); + } + + auto panel = bar.addPanel(screen, dock, panelConfig); + + foreach (widget; widgets) + { + panel.addWidget(widget, bar, panelConfig); + } + + // .add(new ClockWidget()) + // // find using `dbus-send --print-reply --system --dest=org.freedesktop.UPower /org/freedesktop/UPower org.freedesktop.UPower.EnumerateDevices` + // //.add(new BatteryWidget(bar.fontFamily, "/org/freedesktop/UPower/devices/battery_BAT1")) + // //.add(new NotificationsWidget(&bar)) + // .add(new VolumeWidget()) + // .add(new MprisMediaPlayerWidget(bar.fontFamily, "org.mpris.MediaPlayer2.spotify")) + // //.add(new WorkspaceWidget(bar.x, "DisplayPort-1")) + // ; + // if (phones.length) + // panel1.add(new PhoneBatteryWidget(bar.fontFamily, phones[0])); + + // if (left.length) + // panel1.add(new WorkspaceWidget(bar.x, left)); + + // if (right.length) + // bar.addPanel(Screen.Second, Dock.Bottom, panelConfig) + // .add(new ClockWidget()) + // .add(new WorkspaceWidget(bar.x, right)) + // //.add(new WorkspaceWidget(bar.x, "DisplayPort-0")) + // ; + // //dfmt on +} + +void addWidget(Panel panel, Json widget, ref Bar bar, PanelConfiguration panelConfig) +{ + string className = widget.type == Json.Type.string ? widget.to!string : widget["type"].to!string; + + auto ret = cast(Widget) Object.factory(className); + if (!ret) + { + logError("Could not find widget %s", className); + return; + } + ret.loadBase(WidgetConfig(&bar, panelConfig)); + + if (widget.type == Json.Type.object) + { + foreach (k, v; widget.get!(Json[string])) + { + if (k == "type") + continue; + if (ret.setProperty(k, v)) + continue; + + logInfo("Unknown %s widget config: %s %s", className, k, v); + } + } + + panel.add(ret); +} diff --git a/source/dwinbar/webserver.d b/source/dwinbar/webserver.d new file mode 100644 index 0000000..1ba2781 --- /dev/null +++ b/source/dwinbar/webserver.d @@ -0,0 +1,191 @@ +module dwinbar.webserver; + +import std.base64; +import std.concurrency; +import std.conv; +import std.format; +import std.typecons; +import std.uri; + +import vibe.vibe; + +import core.thread; + +__gshared string clientId, clientSecret, redirectURL; +__gshared AuthToken spotifyToken; + +Tid webServerThread; + +struct DoExit +{ +} + +void vibeMain() +{ + auto settings = new HTTPServerSettings; + settings.options = HTTPServerOption.reusePort; + settings.bindAddresses = ["::1", "127.0.0.1"]; + settings.port = 3007; + + auto router = new URLRouter; + router.get("/spotify/auth", &getSpotifyAuth); + router.get("/spotify", &getSpotify); + + if (existsFile("spotify-auth.json")) + { + spotifyToken = deserializeJson!AuthToken( + parseJsonString(readFileUTF8("spotify-auth.json"), "spotify-auth.json")); + refreshSpotify(); + } + + runTask({ receiveOnly!DoExit(); exitEventLoop(true); }); + + listenHTTP(settings, router); + + runEventLoop(); +} + +void startSpotifyWebServer() +{ + if (clientId.length && clientSecret.length && redirectURL.length) + webServerThread = spawn(&vibeMain); +} + +void getSpotify(scope HTTPServerRequest req, scope HTTPServerResponse res) +{ + res.redirect(format!"https://accounts.spotify.com/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=user-read-playback-state"( + clientId, encodeComponent(redirectURL))); +} + +struct AuthToken +{ + @name("access_token") + string accessToken; + @name("refresh_token") + string refreshToken; + + string scope_; + @name("expires_in") + int expiresIn; +} + +void refreshSpotify() @trusted +{ + int status; + Json ret; + + requestHTTP("https://accounts.spotify.com/api/token", (scope req) { + req.method = HTTPMethod.POST; + req.writeFormBody([ + "grant_type": "refresh_token", + "refresh_token": spotifyToken.refreshToken, + "client_id": clientId, + "client_secret": clientSecret + ]); + }, (scope res) { status = res.statusCode; ret = res.readJson; }); + if (status != 200) + { + logError("Failed refreshing spotify token: %s", ret); + return; + } + spotifyToken.accessToken = ret["access_token"].get!string; + spotifyToken.scope_ = ret["scope"].get!string; + spotifyToken.expiresIn = ret["expires_in"].get!int; + rearmRefresh(); +} + +void rearmRefresh() @trusted +{ + setTimer((spotifyToken.expiresIn * 2 / 3).seconds, () @safe nothrow{ + try + { + refreshSpotify(); + } + catch (Exception e) + { + logError("Failed to refresh spotify: %s", e.msg); + } + }); +} + +void getSpotifyAuth(scope HTTPServerRequest req, scope HTTPServerResponse res) +{ + string code = req.query.get("code", ""); + string error = req.query.get("error", ""); + + if (!code.length && error.length) + { + res.writeBody("You rejected spotify authentication. Song progress will not be displayed on the bar.", + "text/plain"); + } + else + { + int status; + Json data; + requestHTTP("https://accounts.spotify.com/api/token", (scope req) { + req.method = HTTPMethod.POST; + req.writeFormBody([ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirectURL, + "client_id": clientId, + "client_secret": clientSecret + ]); + }, (scope res) { status = res.statusCode; data = res.readJson(); }); + + if (status != 200) + { + res.writeBody("Failed to authenticate spotify. Please try again. Check the console for details.", + "text/plain"); + logError("Spotify failure: %s", data); + return; + } + + spotifyToken = deserializeJson!AuthToken(data); + logInfo("Token: %s", spotifyToken); + + writeFileUTF8(NativePath("spotify-auth.json"), serializeToJsonString(spotifyToken)); + + res.writeBody("SuccessSuccess. You can close the window now.", + "text/html"); + + rearmRefresh(); + } +} + +struct SpotifyUserStatus +{ +@optional: + long timestamp; + Nullable!long progress_ms; + bool is_playing; + @name("currently_playing_type") + string currentlyPlayingType; +} + +SpotifyUserStatus getSpotifyCurrentlyPlaying() +{ + if (!spotifyToken.accessToken.length) + return SpotifyUserStatus.init; + + try + { + int status; + Json ret; + requestHTTP("https://api.spotify.com/v1/me/player", (scope req) { + req.headers.addField("Authorization", "Bearer " ~ spotifyToken.accessToken); + }, (scope res) { status = res.statusCode; ret = res.readJson; }); + + if (status != 200) + { + logError("Failed reading spotify status: %s", ret); + return SpotifyUserStatus.init; + } + return deserializeJson!SpotifyUserStatus(ret); + } + catch (Exception e) + { + logError("Exception reading spotify status: %s", e); + return SpotifyUserStatus.init; + } +} diff --git a/source/dwinbar/widget.d b/source/dwinbar/widget.d index 65ea662..47fd363 100644 --- a/source/dwinbar/widget.d +++ b/source/dwinbar/widget.d @@ -16,6 +16,8 @@ import std.traits; import std.utf; import std.uni; +public import vibe.data.json; + public import imageformats; ubyte[n] mix(ubyte n, F)(ubyte[n] a, ubyte[n] b, F fac) @@ -392,6 +394,12 @@ struct ImageRange } } +struct WidgetConfig +{ + Bar* bar; + PanelConfiguration panel; +} + abstract class Widget { abstract int width(bool vertical) const; @@ -415,6 +423,15 @@ abstract class Widget return _queueRedraw; } + void loadBase(WidgetConfig config) + { + } + + bool setProperty(string property, Json value) + { + return false; + } + private: bool _queueRedraw; } diff --git a/source/dwinbar/widgets/battery.d b/source/dwinbar/widgets/battery.d index 59f9219..f497840 100644 --- a/source/dwinbar/widgets/battery.d +++ b/source/dwinbar/widgets/battery.d @@ -21,6 +21,10 @@ import tinyevent; class BatteryWidget : Widget { + this() + { + } + this(FontFamily font) { this.font = font; @@ -29,18 +33,47 @@ class BatteryWidget : Widget this(FontFamily font, string batteryDevice) { this.font = font; - systemBus.attach(); - batteryInterface = new PathIface(systemBus.conn, "org.freedesktop.UPower", - batteryDevice, "org.freedesktop.DBus.Properties"); + loadIcons(); + setDevice(batteryDevice); + updateClock.start(); + } + + void loadIcons() + { batteryFullIcon = read_image("res/icon/battery-charging-full.png").premultiply; batteryIcon.loadAll("res/icon/battery-"); chargingIcon.loadAll("res/icon/battery-charging-"); if (!batteryIcon.images.length) throw new Exception("No battery icons found"); unknownIcon = batteryIcon.imageFor(0); + } + + void setDevice(string device) + { + systemBus.attach(); + batteryInterface = new PathIface(systemBus.conn, "org.freedesktop.UPower", + device, "org.freedesktop.DBus.Properties"); + } + + override void loadBase(WidgetConfig config) + { + this.font = config.bar.fontFamily; updateClock.start(); } + override bool setProperty(string property, Json value) + { + switch (property) + { + case "batteryDevice": + case "device": + setDevice(value.to!string); + return true; + default: + return false; + } + } + override int width(bool) const { return 18 + cast(int) ceil(measureText(cast() font, 1, batteryLevel.to!string)[0]); diff --git a/source/dwinbar/widgets/clock.d b/source/dwinbar/widgets/clock.d index 970f8d4..b6821c7 100644 --- a/source/dwinbar/widgets/clock.d +++ b/source/dwinbar/widgets/clock.d @@ -9,7 +9,14 @@ import std.conv; class ClockWidget : Widget { - this(bool showSeconds = true, bool secondsColon = false) + this() + { + this.showSeconds = true; + this.secondsColon = true; + clockIcon = read_image("res/icon/clock.png").premultiply; + } + + this(bool showSeconds, bool secondsColon = false) { this.showSeconds = showSeconds; this.secondsColon = secondsColon; @@ -48,8 +55,8 @@ class ClockWidget : Widget auto pos = ret.drawText(bar.fontFamily, 0, clockMajor, 0, 14, cast(ubyte[4])[0xFF, 0xFF, 0xFF, 0xFF]); if (showSeconds) - ret.drawText(bar.fontFamily, 1, clockMinor, pos[0] + (secondsColon ? 0 : 2), 14, - cast(ubyte[4])[0xFF, 0xFF, 0xFF, 0xFF]); + ret.drawText(bar.fontFamily, 1, clockMinor, pos[0] + (secondsColon ? 0 : 2), + 14, cast(ubyte[4])[0xFF, 0xFF, 0xFF, 0xFF]); ret.draw(clockIcon, ret.w - 16, 0); diff --git a/source/dwinbar/widgets/mediaplayer.d b/source/dwinbar/widgets/mediaplayer.d index b6f69d0..48a9104 100644 --- a/source/dwinbar/widgets/mediaplayer.d +++ b/source/dwinbar/widgets/mediaplayer.d @@ -23,6 +23,12 @@ import tinyevent; class MprisMediaPlayerWidget : Widget, IMouseWatch { + this() + { + path = "/org/mpris/MediaPlayer2"; + iface = "org.mpris.MediaPlayer2.Player"; + } + this(FontFamily font, string dest, string path = "/org/mpris/MediaPlayer2", string iface = "org.mpris.MediaPlayer2.Player") { @@ -38,6 +44,33 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch updateClock.start(); } + override void loadBase(WidgetConfig config) + { + this.font = config.bar.fontFamily; + + sessionBus.attach(); + prevIcon = read_image("res/icon/skip-previous.png").premultiply; + pauseIcon = read_image("res/icon/pause.png").premultiply; + playIcon = read_image("res/icon/play.png").premultiply; + nextIcon = read_image("res/icon/skip-next.png").premultiply; + updateClock.start(); + } + + override bool setProperty(string property, Json value) + { + switch (property) + { + case "dest": + dest = value.to!string; + return true; + case "spotify": + spotify = value.to!bool; + return true; + default: + return false; + } + } + override int width(bool) const { if (!mpInterface || !playerInterface) @@ -47,7 +80,7 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch override int height(bool) const { - return 16; + return 32; } override bool hasHover() @property @@ -67,11 +100,30 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch enum xOffset = (24 - 16) / 2; - canvas.draw(prevIcon, xOffset, 0, 0, 0, canPrev ? 255 : 90); - canvas.draw(isPlaying ? pauseIcon : playIcon, 24 + xOffset, 0, 0, 0, + if (songLength != Duration.zero) + { + float progress = this.progress.peek.total!"usecs" / cast(float) songLength.total!"usecs"; + stderr.writeln("Progress: ", progress); + int width = cast(int)(canvas.w * progress); + if (width >= 0 && width <= canvas.w) + { + if (width > 1) + canvas.fillRect(1, canvas.h - 3, width - 1, 1, cast(ubyte[4])[ + 0xFF, 0x65, 0x00, 0xFF + ]); + canvas.fillRect(0, canvas.h - 2, width, 2, cast(ubyte[4])[ + 0xFF, 0x65, 0x00, 0xFF + ]); + } + } + + canvas.draw(prevIcon, xOffset, 8, 0, 0, canPrev ? 255 : 90); + canvas.draw(isPlaying ? pauseIcon : playIcon, 24 + xOffset, 8, 0, 0, (isPlaying ? canPause : canPlay) ? 255 : 90); - canvas.draw(nextIcon, 24 * 2 + xOffset, 0, 0, 0, canNext ? 255 : 90); - canvas.drawText(font, 1, label, 24 * 3 + 8, 14, cast(ubyte[4])[0xFF, 0xFF, 0xFF, 0xFF]); + canvas.draw(nextIcon, 24 * 2 + xOffset, 8, 0, 0, canNext ? 255 : 90); + canvas.drawText(font, 1, label, 24 * 3 + 8, 14 + 8, cast(ubyte[4])[ + 0xFF, 0xFF, 0xFF, 0xFF + ]); return canvas; } @@ -97,6 +149,8 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch if (!ensureConnection) return; playerInterface.Play(); + if (!progress.running) + progress.start(); } void pause() @@ -104,6 +158,8 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch if (!ensureConnection) return; playerInterface.Pause(); + if (progress.running) + progress.stop(); } void playPause() @@ -118,11 +174,13 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch if (!ensureConnection) return; playerInterface.Previous(); + progress.reset(); } void next() { playerInterface.Next(); + progress.reset(); } override void update(Bar) @@ -130,8 +188,30 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch if ((updateClock.peek <= 400.msecs && !force) || !ensureConnection) return; force = false; + tick++; updateClock.reset(); updateDBus(); + + if (tick > 12 && isPlaying && spotify) + { + import dwinbar.webserver : getSpotifyCurrentlyPlaying; + + auto status = getSpotifyCurrentlyPlaying(); + if (!status.progress_ms.isNull) + progress.setTimeElapsed(status.progress_ms.get.msecs); + + if (status.is_playing && !progress.running) + progress.start(); + else if (progress.running) + progress.stop(); + tick = 0; + } + else if (!isPlaying && progress.running) + progress.stop(); + + if (songLength != Duration.zero && isPlaying) + queueRedraw(); + try { auto song = mpInterface.Get(iface, "Metadata").to!(Variant!DBusAny[string]); @@ -140,6 +220,22 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch const newCanPlay = mpInterface.Get(iface, "CanPlay").to!bool; const newCanPause = mpInterface.Get(iface, "CanPause").to!bool; const newIsPlaying = mpInterface.Get(iface, "PlaybackStatus").to!string == "Playing"; + const newLocation = mpInterface.Get(iface, "Position").to!long; + + if (newLocation != 0) + { + if (!progress.running) + progress.start(); + progress.setTimeElapsed(newLocation.usecs); + } + + if (isPlaying) + { + if (auto length = "mpris:length" in song) + songLength = length.data.to!long.usecs; + else + songLength = Duration.zero; + } if (newCanNext != canNext || newCanPrev != canPrev || newCanPause != canPause || newCanPlay != canPlay || newIsPlaying != isPlaying || updateLabel(song)) @@ -181,12 +277,14 @@ class MprisMediaPlayerWidget : Widget, IMouseWatch return false; label = song; + progress.reset(); return true; } void mouseDown(bool vertical, int x, int y, int button) { - if (button != 1) return; + if (button != 1) + return; try { if (x < 24) @@ -224,6 +322,10 @@ private: string dest, path, iface; string label; bool isPlaying, canPlay, canPause, canPrev, canNext, force; + StopWatch progress; + Duration songLength; + bool spotify; IFImage prevIcon, pauseIcon, playIcon, nextIcon; StopWatch updateClock; + int tick; } diff --git a/source/dwinbar/widgets/notifications.d b/source/dwinbar/widgets/notifications.d index 008b95e..fc29f3f 100644 --- a/source/dwinbar/widgets/notifications.d +++ b/source/dwinbar/widgets/notifications.d @@ -148,8 +148,9 @@ struct Notification XSelectInput(x.display, window, ExposureMask | ButtonPressMask); - XSetWMProtocols(x.display, window, [XAtom[AtomName.WM_DELETE_WINDOW], - XAtom[AtomName._NET_WM_PING]].ptr, 2); + XSetWMProtocols(x.display, window, [ + XAtom[AtomName.WM_DELETE_WINDOW], XAtom[AtomName._NET_WM_PING] + ].ptr, 2); XSetWindowBackground(x.display, window, 0); @@ -275,8 +276,10 @@ class NotificationServer string[] GetCapabilities() { - return ["action-icons", "actions", "body", "body-hyperlinks", "body-images", - "body-markup", "icon-multi", "icon-static", "persistence", "sound"]; + return [ + "action-icons", "actions", "body", "body-hyperlinks", "body-images", + "body-markup", "icon-multi", "icon-static", "persistence", "sound" + ]; } uint Notify(string app_name, uint replaces_id, string app_icon, string summary, @@ -366,7 +369,21 @@ private: class NotificationsWidget : Widget, IWindowManager { + this() + { + } + this(Bar* bar) + { + loadBar(bar); + } + + override void loadBase(WidgetConfig config) + { + loadBar(config.bar); + } + + private void loadBar(Bar* bar) { this.bar = bar; x = bar.x; diff --git a/source/dwinbar/widgets/phone_battery.d b/source/dwinbar/widgets/phone_battery.d index 4845132..e3f34ae 100644 --- a/source/dwinbar/widgets/phone_battery.d +++ b/source/dwinbar/widgets/phone_battery.d @@ -23,17 +23,48 @@ import tinyevent; class PhoneBatteryWidget : BatteryWidget { + this() + { + } + this(FontFamily font, KDEConnectDevice device) { super(font); this.device = device; + loadIcons(); + updateClock.start(); + } + + override void loadBase(WidgetConfig config) + { + this.font = config.bar.fontFamily; + devices = KDEConnectDevice.listDevices; + if (devices.length) + device = devices[0]; + loadIcons(); + updateClock.start(); + } + + override bool setProperty(string property, Json value) + { + switch (property) + { + case "device": + device = devices[value.to!int]; + return true; + default: + return false; + } + } + + override void loadIcons() + { unknownIcon = read_image("res/icon/cellphone-erase.png").premultiply; batteryFullIcon = read_image("res/icon/battery-charging-full.png").premultiply; batteryIcon.loadAll("res/icon/battery-"); chargingIcon.loadAll("res/icon/battery-charging-"); if (!batteryIcon.images.length) throw new Exception("No battery icons found"); - updateClock.start(); } override void update(Bar bar) @@ -105,5 +136,6 @@ class PhoneBatteryWidget : BatteryWidget private: int tick = 25; + KDEConnectDevice[] devices; KDEConnectDevice device; } diff --git a/source/dwinbar/widgets/workspace.d b/source/dwinbar/widgets/workspace.d index 5a6c9d9..6381a3a 100644 --- a/source/dwinbar/widgets/workspace.d +++ b/source/dwinbar/widgets/workspace.d @@ -11,6 +11,10 @@ import std.conv; class WorkspaceWidget : Widget, IPropertyWatch, IMouseWatch { + this() + { + } + this(XBackend x, string limitDesktops = null) { this.x = x; @@ -18,6 +22,25 @@ class WorkspaceWidget : Widget, IPropertyWatch, IMouseWatch refreshDesktops(); } + override void loadBase(WidgetConfig config) + { + this.x = config.bar.x; + refreshDesktops(); + } + + override bool setProperty(string property, Json value) + { + switch (property) + { + case "limitDesktops": + case "display": + limitDesktops = value.to!string; + return true; + default: + return false; + } + } + override int width(bool) const { return cast(int) desktops.length * 32;