From af9d9e3451d788fc0c6b59107ed2aa101fb688f0 Mon Sep 17 00:00:00 2001 From: gnat Date: Fri, 6 Sep 2024 16:55:55 -0700 Subject: [PATCH] initial commit, you are welcome odette --- config.js | 64 ++++ icons/expansion-card.svg | 1 + icons/fan.svg | 1 + icons/memory-solid.svg | 1 + icons/microchip-solid.svg | 1 + icons/thermometer.svg | 1 + services/brightness.js | 44 +++ services/mpd.js | 361 ++++++++++++++++++++++ style/colors.scss | 31 ++ style/components/bar-control-center.scss | 68 ++++ style/components/bar.scss | 75 +++++ style/components/launcher.scss | 43 +++ style/components/lock.scss | 28 ++ style/components/notification-popups.scss | 89 ++++++ style/components/top-bar.scss | 126 ++++++++ style/css | 212 +++++++++++++ style/mixins.scss | 27 ++ style/style.css | 315 +++++++++++++++++++ style/style.scss | 14 + utils/PopupWindow.js | 29 ++ widgets/battery.js | 38 +++ widgets/brightness.js | 63 ++++ widgets/clock.js | 62 ++++ widgets/hypractive.js | 10 + widgets/hyprworkspaces.js | 59 ++++ widgets/marqueeLabel.js | 87 ++++++ widgets/mpd.js | 182 +++++++++++ widgets/mpris.js | 114 +++++++ widgets/resourceDial.js | 24 ++ widgets/systray.js | 14 + widgets/volume.js | 70 +++++ windows/bar.js | 141 +++++++++ windows/launcher.js | 93 ++++++ windows/lock.js | 76 +++++ windows/menu.js | 49 +++ windows/notifications.js | 175 +++++++++++ windows/top-bar.js | 70 +++++ 37 files changed, 2858 insertions(+) create mode 100644 config.js create mode 100644 icons/expansion-card.svg create mode 100644 icons/fan.svg create mode 100644 icons/memory-solid.svg create mode 100644 icons/microchip-solid.svg create mode 100644 icons/thermometer.svg create mode 100644 services/brightness.js create mode 100644 services/mpd.js create mode 100644 style/colors.scss create mode 100644 style/components/bar-control-center.scss create mode 100644 style/components/bar.scss create mode 100644 style/components/launcher.scss create mode 100644 style/components/lock.scss create mode 100644 style/components/notification-popups.scss create mode 100644 style/components/top-bar.scss create mode 100644 style/css create mode 100644 style/mixins.scss create mode 100644 style/style.css create mode 100644 style/style.scss create mode 100644 utils/PopupWindow.js create mode 100644 widgets/battery.js create mode 100644 widgets/brightness.js create mode 100644 widgets/clock.js create mode 100644 widgets/hypractive.js create mode 100644 widgets/hyprworkspaces.js create mode 100644 widgets/marqueeLabel.js create mode 100644 widgets/mpd.js create mode 100644 widgets/mpris.js create mode 100644 widgets/resourceDial.js create mode 100644 widgets/systray.js create mode 100644 widgets/volume.js create mode 100644 windows/bar.js create mode 100644 windows/launcher.js create mode 100644 windows/lock.js create mode 100644 windows/menu.js create mode 100644 windows/notifications.js create mode 100644 windows/top-bar.js diff --git a/config.js b/config.js new file mode 100644 index 0000000..67a415f --- /dev/null +++ b/config.js @@ -0,0 +1,64 @@ +const { execAsync } = Utils + +// import { reveal_menu, reveal_launcher, Bar, FakeBar } from './windows/bar.js' +import { Bar, FakeBar } from './windows/top-bar.js' +import { NotificationPopups } from './windows/notifications.js' +import { Lock, show_lock } from './windows/lock.js' + +execAsync('mpDris2') + +Utils.monitorFile( + `./style/style.scss`, + + function() { + const scss = `./style/style.scss` + + const css = `./style/style.css` + + Utils.exec(`sassc ${scss} ${css}`) + App.resetCss() + App.applyCss(css) + }, +) + +App.addIcons('/usr/share/icons/Papirus/symbolic/status') + +App.config({ + windows: [ + Lock, + FakeBar, + Bar, + NotificationPopups, + ], + style: './style/style.css', + icons: './icons' +}) + +Object.defineProperty(globalThis, "lock", { + get() { + show_lock.value = true + } +}) + + + +/* +Object.defineProperty(globalThis, "swipeRight", { + get() { + if (reveal_menu.value) { + reveal_launcher.value = true + } else { + reveal_menu.value = true + } + } +}) + +Object.defineProperty(globalThis, "swipeLeft", { + get() { + if (reveal_launcher.value) { + reveal_launcher.value = false + } else { + reveal_menu.value = false + } + } +})*/ diff --git a/icons/expansion-card.svg b/icons/expansion-card.svg new file mode 100644 index 0000000..8b1ca84 --- /dev/null +++ b/icons/expansion-card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/fan.svg b/icons/fan.svg new file mode 100644 index 0000000..5e07b31 --- /dev/null +++ b/icons/fan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/memory-solid.svg b/icons/memory-solid.svg new file mode 100644 index 0000000..f7160d7 --- /dev/null +++ b/icons/memory-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/microchip-solid.svg b/icons/microchip-solid.svg new file mode 100644 index 0000000..5e4b242 --- /dev/null +++ b/icons/microchip-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/thermometer.svg b/icons/thermometer.svg new file mode 100644 index 0000000..c9a5f5a --- /dev/null +++ b/icons/thermometer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/brightness.js b/services/brightness.js new file mode 100644 index 0000000..09e5057 --- /dev/null +++ b/services/brightness.js @@ -0,0 +1,44 @@ +class Brightness extends Service { + static { + Service.register( + this, + {}, + { + screen: ["float", "rw"], + }, + ); + } + + _screen = 0; + + get screen() { + return this._screen; + } + + set screen(percent) { + if (percent < 0) percent = 0; + + if (percent > 1) percent = 1; + + Utils.execAsync(`brightnessctl s ${percent * 100}% -q`) + .then(() => { + this._screen = percent; + this.changed("screen"); + }) + .catch(console.error); + } + + constructor() { + super(); + try { + this._screen = + Number(Utils.exec("brightnessctl g")) / + Number(Utils.exec("brightnessctl m")); + } catch (error) { + console.error("missing dependancy: brightnessctl"); + } + } +} + +const service = new Brightness(); +export default service; diff --git a/services/mpd.js b/services/mpd.js new file mode 100644 index 0000000..30c3da9 --- /dev/null +++ b/services/mpd.js @@ -0,0 +1,361 @@ +import Gio from "gi://Gio"; + +Gio._promisify(Gio.DataInputStream.prototype, "read_line_async"); + +class Mpd extends Service { + static { + Service.register( + this, + {}, + { + //TODO: parse some properties like duration into number? + partition: ["string", "r"], + volume: ["string", "r"], + repeat: ["string", "r"], + random: ["string", "r"], + single: ["string", "r"], + consume: ["string", "r"], + playlist: ["string", "r"], + playlistlength: ["string", "r"], + state: ["string", "r"], + song: ["string", "r"], + songid: ["string", "r"], + nextsong: ["string", "r"], + nextsongid: ["string", "r"], + elapsed: ["string", "r"], + duration: ["string", "r"], + bitrate: ["string", "r"], + mixrampdb: ["string", "r"], + audio: ["string", "r"], + + file: ["string", "r"], + "Last-Modified": ["string", "r"], + Artist: ["string", "r"], + Title: ["string", "r"], + Album: ["string", "r"], + Pos: ["string", "r"], + Id: ["string", "r"], + }, + ); + } + + #socket; + + #inputStream; + #outputStream; + + _decoder = new TextDecoder(); + _encoder = new TextEncoder(); + _messageHandlerQueue = []; + + //TODO: more properties? + + // Status + _partition; + _volume; + _repeat; + _random; + _single; + _consume; + _playlist; + _playlistlength; + _state; + _song; + _songid; + _nextsong; + _nextsongid; + _elapsed; + _duration; + _bitrate; + _mixrampdb; + _audio; + + _file; + _LastModified; + _Artist; + _Title; + _Album; + _Pos; + _Id; + + get partition() { + return this._partition; + } + + get volume() { + return this._volume; + } + + get repeat() { + return this._repeat; + } + + get random() { + return this._random; + } + + get single() { + return this._single; + } + + get consume() { + return this._consume; + } + + get playlist() { + return this._playlist; + } + + get playlistlength() { + return this._playlistlength; + } + + get state() { + return this._state; + } + + get song() { + return this._song; + } + + get songid() { + return this._songid; + } + + get nextsong() { + return this._nextsong; + } + + get nextsongid() { + return this._nextsongid; + } + + get elapsed() { + return this._elapsed; + } + + get duration() { + return this._duration; + } + + get bitrate() { + return this._bitrate; + } + + get mixrampdb() { + return this._mixrampdb; + } + + get audio() { + return this._audio; + } + + get file() { + return this._file; + } + + get Last_Modified() { + return this._LastModified; + } + + get Artist() { + return this._Artist; + } + + get Title() { + return this._Title; + } + + get Album() { + return this._Album; + } + + get Pos() { + return this._Pos; + } + + get Id() { + return this._Id; + } + + constructor() { + super(); + this._initSocket(); + } + + async _initSocket() { + try { + this.#socket = new Gio.SocketClient().connect_to_host( + "localhost", + 6600, + null, + ); + + this.#inputStream = new Gio.DataInputStream({ + base_stream: this.#socket.get_input_stream(), + }); + + this.#outputStream = new Gio.DataOutputStream({ + base_stream: this.#socket.get_output_stream(), + }); + + this._watchSocket(); + + //init properties + //[TODO): init more properties? + + this.send("status") + .then(this._updateProperties.bind(this)) + .catch(logError); + this.send("currentsong") + .then(this._updateProperties.bind(this)) + .catch(logError); + } catch (e) { + logError(e); + } + } + + async _watchSocket() { + let bufferedLines = []; + while (true) { + const [rawData] = await this.#inputStream.read_line_async(0, null); + const data = this._decoder.decode(rawData); + if (data == null) continue; + bufferedLines.push(data); + const { response, remain } = this._parseResponse(bufferedLines); + bufferedLines = remain; + + if (!response) continue; + switch (response.type) { + case "version": + console.log(`MPD Server Version ${response.payload}`); + break; + case "error": + this._handleMessage(new Error(response.payload), null); + break; + case "data": + this._handleMessage(null, response.payload); + break; + } + } + } + + _parseResponse(lines) { + let response; + let beginLine = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + const version = line.match(/^OK MPD (.+)/); + const error = line.match(/^ACK \[.*] {.*} (.+)/); + + if (version) { + response = { type: "version", payload: version[1] }; + beginLine = i + 1; + } else if (error) { + response = { type: "error", payload: error[1] }; + beginLine = i + 1; + } else if (line === "OK") { + response = { + type: "data", + payload: lines.slice(beginLine, i).join("\n"), + }; + beginLine = i + 1; + } + } + return { response, remain: lines.slice(beginLine) }; + } + + _handleMessage(err, msg) { + const { func } = this._messageHandlerQueue.shift(); + func(err, msg); + if (this._messageHandlerQueue.length === 0) { + this._idle(); + } + } + + async send(data) { + data = data.trim(); + const isIdle = data === "idle"; + + if (this._messageHandlerQueue[0]?.isIdle) { + this.#outputStream.write(this._encoder.encode("noidle\n"), null); + } + this.#outputStream.write(this._encoder.encode(`${data}\n`), null); + + return new Promise((resolve, reject) => { + this._messageHandlerQueue.push({ + isIdle, + func: (err, msg) => { + if (err != null) reject(err); + resolve(msg); + }, + }); + }); + } + + _idle() { + this.send("idle") + .then((msg) => { + for (const line of msg.split("\n")) { + const subsystem = /changed: (\w+)/.exec(line); + if (subsystem == null) continue; + + //TODO: only update those things that could have + //changed using a switch over the subsystems + this.send("status") + .then(this._updateProperties.bind(this)) + .catch(logError); + this.send("currentsong") + .then(this._updateProperties.bind(this)) + .catch(logError); + /* + switch(subsystem[1]) { + case "player": + break; + }*/ + } + }) + .catch(logError); + } + + _updateProperties(msg) { + for (const line of msg.split("\n")) { + const keyValue = line.match(/(.*): (.*)/); + if (keyValue == null) continue; + const deprecatedKeys = [ + "time", + "Time", //deprecated + "Format", //same as audio + ]; + if (deprecatedKeys.includes(keyValue[1])) continue; + if (!this.hasOwnProperty(`_${keyValue[1]}`)) continue; + this.updateProperty(keyValue[1], keyValue[2]); + this.emit("changed"); + } + } + + setCrossfade = (seconds) => this.send(`crossfade ${seconds}`); + setVolume = (volume) => this.send(`setvol ${volume}`); + + toggleShuffle = () => this.send(`random ${+this._random ? "0" : "1"}`); + toggleRepeat = () => this.send(`repeat ${+this._repeat ? "0" : "1"}`); + + next = () => this.send("next"); + playPause = () => this.send(`pause ${this._state === "pause" ? "0" : "1"}`); + pause = () => this.send("pause 1"); + play = () => this.send("pause 0"); + playSong = (songpos) => this.send(`play ${songpos}`); + playSongId = (songid) => this.send(`playid ${songid}`); + seekSong = (songpos, time) => this.send(`seek ${songpos} ${time}`); + seekSongId = (songid, time) => this.send(`seekid ${songid} ${time}`); + seekCur = (time) => this.send(`seekcur ${time}`); + previous = () => this.send("previous"); + stop = () => this.send("stop"); + + clearQueue = () => this.send("clear"); +} + +const service = new Mpd; +export default service; diff --git a/style/colors.scss b/style/colors.scss new file mode 100644 index 0000000..54c4e92 --- /dev/null +++ b/style/colors.scss @@ -0,0 +1,31 @@ +$bg: #161616; +$bg-alt-1: #262626; +$bg-alt-2: #393939; + +$fg: #f2f4f8; + +$hl: #FF7EB6; +$hl-alt-1: #33B1FF; +$hl-alt-2: #42be65; + +$icon-color: #f2f4f8; +$button-hover-color: #FF7EB6; + +$battery-dial-bg: #42be65; +$battery-dial-fg: #161616; + +$volume-dial-bg: #33B1FF; +$volume-dial-fg: #161616; + +$brightness-dial-bg: #33B1FF; +$brightness-dial-fg: #161616; + +$ws-active: #FF7EB6; +$ws-inactive: $bg; + +$mpd-progress-primary: #FF7Eb6; + +$resource-dial-fg-cpu: #FF7eb6; +$resource-dial-fg-mem: #FF7eb6; +$resource-dial-fg-tmp: #FF7eb6; +$resource-dial-fg-fan: #FF7eb6; diff --git a/style/components/bar-control-center.scss b/style/components/bar-control-center.scss new file mode 100644 index 0000000..5d7dde3 --- /dev/null +++ b/style/components/bar-control-center.scss @@ -0,0 +1,68 @@ +#status-bar .Menu { + background-color: $bg-alt-1; + min-width: 640px; + + .mpd-controls { + background-color: $bg; + border-radius: 10px; + margin: 10px; + + .button { + font-size: 24px; + padding: 10px; + margin: 10px; + background-color: $bg-alt-1; + border-color: $bg-alt-1; + border-radius: 10px; + } + + .cover { + min-width: 250px; + min-height: 250px; + border-radius: 10px; + margin: 10px; + background: $bg; + background-size: cover; + } + + .title { + padding-top: 10px; + font-size: 20px; + font-weight: bold; + } + + .position { + margin: 10px; + margin-right: 20px; + min-height: 10px; + border-radius: 5px; + + trough { + background: $bg-alt-1; + border-radius: 5px; + min-height: 10px; + } + + slider { + min-height: 10px; + border-radius: 5px; + background: $mpd-progress-primary; + } + } + + .position-label { + margin-top: 75px; + } + } + + .dial-parent { + @include panel-dial($resource-dial-fg-cpu, $bg); + border-radius: 10px; + + + .dial-icon { + color: $fg; + } + } +} + diff --git a/style/components/bar.scss b/style/components/bar.scss new file mode 100644 index 0000000..de73da7 --- /dev/null +++ b/style/components/bar.scss @@ -0,0 +1,75 @@ +* :not(selection) :not(tooltip) { + all: unset; +} + +#status-bar { + background-color: $bg; + + border-radius: 0 10px 10px 0; + + .dial-container { + margin-top: 10px; + + .battery-dial { + @include bar-dial($battery-dial-bg, $battery-dial-fg) + } + + .volume-dial { + @include bar-dial($volume-dial-bg, $volume-dial-fg) + } + + .brightness-dial { + @include bar-dial($brightness-dial-bg, $brightness-dial-fg) + } + } + + .workspace-container { + background-color: $bg-alt-1; + border-radius: 10px; + padding: 6px 0px; + margin: 0px 12px; + border: none; + + .ws-norm { + min-width: 15px; + border-radius: 10px; + margin: 2px 5px; + padding: 6px 10px; + background-color: $ws-inactive; + color: $fg; + } + + .ws-active { + color: $bg; + background-color: $ws-active; + } + } + + .menu-visibility-button { + border-radius: 0px 10px 10px 0px; + padding: 10px 0px 10px 2px; + background-color: $bg-alt-1 + } + + .clock { + background-color: $bg-alt-1; + margin: 8px; + margin-bottom: 10px; + padding: 6px 2px; + border-radius: 10px; + } + + .mpd-controls { + .button { + font-size: 14px; + margin: 2px; + margin-bottom: 8px; + font-family: 'Symbols Nerd Font Mono'; + + &:hover { + color: $button-hover-color; + } + } + } +} + diff --git a/style/components/launcher.scss b/style/components/launcher.scss new file mode 100644 index 0000000..5b93533 --- /dev/null +++ b/style/components/launcher.scss @@ -0,0 +1,43 @@ +.launcher { + min-width: 640px; + background: $bg-alt-1; + + .search { + > image { + margin-left: 10px; + } + + > entry { + padding: 5px; + margin: 10px; + background: $bg; + border: 5px solid $bg; + border-radius: 10px; + font-size: 32px; + } + } + + .entry { + margin: 0 10px 0 10px; + border: 10px solid $bg; + border-radius: 10px; + background: $bg; + + > box { + > image { + padding: 5px; + margin: 5px 10px 5px 10px; + background: $bg-alt-1; + border-radius: 5px; + } + + > label { + + } + } + } + + > scrolledwindow > * > box:last-child { + margin-bottom: 10px; + } +} diff --git a/style/components/lock.scss b/style/components/lock.scss new file mode 100644 index 0000000..14003cb --- /dev/null +++ b/style/components/lock.scss @@ -0,0 +1,28 @@ +.lock-ready > box{ + box { + background: $bg; + padding: 20px; + border: 10px solid $hl-alt-1; + min-width: 300px; + margin: 15px; + } + + .img { + background: url('/home/catalie/.config/wallpaper'); + min-width: 300px; + min-height: 300px; + background-position: 25% 12.5%; + background-size: cover; + } + + box > entry { + background: $bg-alt-1; + font-size: 14px; + padding: 5px; + margin: 10px; + } +} + +.lock { + background: transparent; +} diff --git a/style/components/notification-popups.scss b/style/components/notification-popups.scss new file mode 100644 index 0000000..14045f2 --- /dev/null +++ b/style/components/notification-popups.scss @@ -0,0 +1,89 @@ +#notifications { + background: transparent; +} + +.notifications { + opacity: 1; + min-width: 24rem; + .revealer { + + .revealer { + >*>box { + margin-top: 0; + } + } + + .notification { + margin-bottom: 25px; + min-width: 24rem; + padding: 0px; + border: 2px solid $fg; + border-right: 25px solid $hl-alt-2; + background: $bg; + + &.critical { + border-right: 25px solid $hl; + } + } + + .body { + margin-right: 1em; + } + + .timeout-bar { + margin: 5px 0px 0; + margin-top: 5px; + background: $bg-alt-1; + + > trough > progress { + background-image: none; + background-color: $hl-alt-1; + } + } + + .button { + font-size: 20px; + margin-left: 5px; + margin-right: 5px; + + &:hover { + color: $hl; + } + } + + .icon { + min-width: 68px; + min-height: 68px; + margin-right: 1em; + margin-left: 1em; + } + + .icon image { + font-size: 58px; + margin: 5px; + color: $fg; + } + + .icon box { + min-width: 68px; + min-height: 68px; + } + + .title { + font-size: 28px; + } + + .actions .action-button { + margin: 0 .4em; + margin-top: .8em; + border: 2px solid $fg; + } + + .actions .action-button:first-child { + margin-left: .5em; + } + + .actions .action-button:last-child { + margin-right: .5em; + } + } +} diff --git a/style/components/top-bar.scss b/style/components/top-bar.scss new file mode 100644 index 0000000..e8b6c5d --- /dev/null +++ b/style/components/top-bar.scss @@ -0,0 +1,126 @@ +$height: 50px; + +#status-bar { + background: transparent; + + .workspaces { + min-height: $height; + background: $bg; + padding: 5px; + padding-top: 15px; + + .workspace { + min-width: 10px; + min-height: 10px; + margin: 5px; + background-color: $bg-alt-1; + } + + .occupied { + background-color: $bg-alt-2; + } + + .focused { + background-color: $hl; + } + } + + .media-controls { + margin-top: 20px; + min-width: 220px; + + .button { + font-size: 20px; + } + } + + .progress-bar { + margin-top: 20px; + min-width: 200px; + min-height: 2px; + background: transparent; + + trough { + min-height: 2px; + background: $bg-alt-1; + } + + highlight { + min-height: 2px; + background: $mpd-progress-primary; + } + } + + .right { + min-height: $height; + background: $bg; + + + .battery-container { + padding-left: 10px; + padding-right: 10px; + .battery-dial { + @include bar-dial($battery-dial-bg, $battery-dial-fg); + } + } + + .sliderbox { + margin: 5px; + .volume { + .slider { + trough { + min-height: 10px; + min-width: 120px; + background: $bg-alt-1; + } + + highlight { + background: $hl-alt-1; + } + } + + button { + font-size: 20px; + margin: 5px; + } + } + + .brightness { + .slider { + trough { + min-height: 10px; + min-width: 120px; + background: $bg-alt-1; + } + + highlight { + background: $hl-alt-1; + } + } + + button { + font-size: 20px; + margin: 5px; + } + } + } + + separator { + background: $bg-alt-1; + padding: 1px; + margin-top: 10px; + margin-bottom: 10px; + } + + .clock { + background: $bg; + + .datetime { + margin: 1px; + font-size: 10pt; + } + } + + } +} + diff --git a/style/css b/style/css new file mode 100644 index 0000000..c6186c5 --- /dev/null +++ b/style/css @@ -0,0 +1,212 @@ +* :not(selection) :not(tooltip) { + all: unset; } + +#status-bar { + background-color: #161616; + border-radius: 0 10px 10px 0; } + #status-bar .dial-container { + margin-top: 10px; } + #status-bar .dial-container .battery-dial { + color: #33B1FF; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .dial-container .battery-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .dial-container .volume-dial { + color: #33B1FF; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .dial-container .volume-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .dial-container .brightness-dial { + color: #33B1FF; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .dial-container .brightness-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .workspace-container { + background-color: #262626; + border-radius: 10px; + padding: 6px 0px; + margin: 0px 12px; + border: none; } + #status-bar .workspace-container .ws-norm { + min-width: 15px; + border-radius: 10px; + margin: 2px 5px; + padding: 6px 10px; + background-color: #161616; + color: #f2f4f8; } + #status-bar .workspace-container .ws-active { + color: #161616; + background-color: #FF7EB6; } + #status-bar .menu-visibility-button { + border-radius: 0px 10px 10px 0px; + padding: 10px 0px 10px 2px; + background-color: #262626; } + #status-bar .clock { + background-color: #262626; + margin: 8px; + margin-bottom: 10px; + padding: 6px 2px; + border-radius: 10px; } + #status-bar .mpd-controls .button { + font-size: 14px; + margin: 2px; + margin-bottom: 8px; + font-family: 'Symbols Nerd Font Mono'; } + #status-bar .mpd-controls .button:hover { + color: #FF7EB6; } + +#status-bar .Menu { + background-color: #262626; + min-width: 640px; } + #status-bar .Menu .mpd-controls { + background-color: #161616; + border-radius: 10px; + margin: 10px; } + #status-bar .Menu .mpd-controls .button { + font-size: 24px; + padding: 10px; + margin: 10px; + background-color: #262626; + border-color: #262626; + border-radius: 10px; } + #status-bar .Menu .mpd-controls .cover { + min-width: 250px; + min-height: 250px; + border-radius: 10px; + margin: 10px; + background: #161616; + background-size: cover; } + #status-bar .Menu .mpd-controls .title { + padding-top: 10px; + font-size: 20px; + font-weight: bold; } + #status-bar .Menu .mpd-controls .position { + margin: 10px; + margin-right: 20px; + min-height: 10px; + border-radius: 5px; } + #status-bar .Menu .mpd-controls .position trough { + background: #262626; + border-radius: 5px; + min-height: 10px; } + #status-bar .Menu .mpd-controls .position highlight { + min-height: 10px; + border-radius: 5px; + background: #FF7Eb6; } + #status-bar .Menu .mpd-controls .position-label { + margin-top: 75px; } + #status-bar .Menu .dial-parent { + color: #FF7eb6; + background-color: #161616; + margin: 0px; + padding: 10px; + font-size: 8px; + margin-left: 10px; + border-radius: 10px; + border-radius: 10px; } + #status-bar .Menu .dial-parent .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 32px; + color: #f2f4f8; } + #status-bar .Menu .dial-parent .resource-dial { + min-width: 96px; + min-height: 96px; } + #status-bar .Menu .dial-parent .dial-icon { + color: #f2f4f8; } + +#notifications { + background: transparent; } + +.notifications { + opacity: 1; + min-width: 24rem; } + .notifications .revealer + .revealer > * > box { + margin-top: 0; } + .notifications .revealer .notification { + margin: 25px; + min-width: 24rem; + padding: 0px; + border: 2px solid #f2f4f8; + background: #161616; } + .notifications .revealer .notification.critical { + border: 2px solid #FF7EB6; } + .notifications .revealer .body { + margin-right: 1em; } + .notifications .revealer .timeout-bar { + margin: 5px 0px 0; + margin-top: 5px; + background: #262626; } + .notifications .revealer .timeout-bar > trough > progress { + background-image: none; + background-color: #33B1FF; } + .notifications .revealer .button { + font-size: 20px; + margin-left: 5px; + margin-right: 5px; } + .notifications .revealer .button:hover { + color: #FF7EB6; } + .notifications .revealer .icon { + min-width: 68px; + min-height: 68px; + margin-right: 1em; + margin-left: 1em; } + .notifications .revealer .icon image { + font-size: 58px; + margin: 5px; + color: #f2f4f8; } + .notifications .revealer .icon box { + min-width: 68px; + min-height: 68px; } + .notifications .revealer .title { + font-size: 24px; } + .notifications .revealer .actions .action-button { + margin: 0 .4em; + margin-top: .8em; + border: 2px solid #f2f4f8; } + .notifications .revealer .actions .action-button:first-child { + margin-left: .5em; } + .notifications .revealer .actions .action-button:last-child { + margin-right: .5em; } + +.launcher { + min-width: 640px; + background: #262626; } + .launcher .search > image { + margin-left: 10px; } + .launcher .search > entry { + padding: 5px; + margin: 10px; + background: #161616; + border: 5px solid #161616; + border-radius: 10px; + font-size: 32px; } + .launcher .entry { + margin: 0 10px 0 10px; + border: 10px solid #161616; + border-radius: 10px; + background: #161616; } + .launcher .entry > box > image { + padding: 5px; + margin: 5px 10px 5px 10px; + background: #262626; + border-radius: 5px; } + .launcher > scrolledwindow > * > box:last-child { + margin-bottom: 10px; } + +* :not(selection) :not(tooltip) { + all: unset; } diff --git a/style/mixins.scss b/style/mixins.scss new file mode 100644 index 0000000..4679390 --- /dev/null +++ b/style/mixins.scss @@ -0,0 +1,27 @@ +@mixin dial($_fg, $_bg, $margin, $padding, $font-size, $icon-font-size) { + color: $_fg; + background-color: $_bg; + margin: $margin; + padding: $padding; + font-size: $font-size; + + .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: $icon-font-size; + color: $icon-color; + } +} + +@mixin bar-dial($_fg, $_bg) { + @include dial($_fg, $_bg, 0px, 0px, 5px, 16px) +} + +@mixin panel-dial($_fg, $_bg) { + @include dial($_fg, $_bg, 0px, 10px, 8px, 32px); + margin-left: 10px; + border-radius: 10px; + .resource-dial { + min-width: 96px; + min-height: 96px; + } +} diff --git a/style/style.css b/style/style.css new file mode 100644 index 0000000..300e49b --- /dev/null +++ b/style/style.css @@ -0,0 +1,315 @@ +.lock-ready > box box { + background: #161616; + padding: 20px; + border: 10px solid #33B1FF; + min-width: 300px; + margin: 15px; } + +.lock-ready > box .img { + background: url("/home/catalie/.config/wallpaper"); + min-width: 300px; + min-height: 300px; + background-position: 25% 12.5%; + background-size: cover; } + +.lock-ready > box box > entry { + background: #262626; + font-size: 14px; + padding: 5px; + margin: 10px; } + +.lock { + background: transparent; } + +* :not(selection) :not(tooltip) { + all: unset; } + +#status-bar { + background-color: #161616; + border-radius: 0 10px 10px 0; } + #status-bar .dial-container { + margin-top: 10px; } + #status-bar .dial-container .battery-dial { + color: #42be65; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .dial-container .battery-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .dial-container .volume-dial { + color: #33B1FF; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .dial-container .volume-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .dial-container .brightness-dial { + color: #33B1FF; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .dial-container .brightness-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .workspace-container { + background-color: #262626; + border-radius: 10px; + padding: 6px 0px; + margin: 0px 12px; + border: none; } + #status-bar .workspace-container .ws-norm { + min-width: 15px; + border-radius: 10px; + margin: 2px 5px; + padding: 6px 10px; + background-color: #161616; + color: #f2f4f8; } + #status-bar .workspace-container .ws-active { + color: #161616; + background-color: #FF7EB6; } + #status-bar .menu-visibility-button { + border-radius: 0px 10px 10px 0px; + padding: 10px 0px 10px 2px; + background-color: #262626; } + #status-bar .clock { + background-color: #262626; + margin: 8px; + margin-bottom: 10px; + padding: 6px 2px; + border-radius: 10px; } + #status-bar .mpd-controls .button { + font-size: 14px; + margin: 2px; + margin-bottom: 8px; + font-family: 'Symbols Nerd Font Mono'; } + #status-bar .mpd-controls .button:hover { + color: #FF7EB6; } + +#status-bar { + background: transparent; } + #status-bar .workspaces { + min-height: 50px; + background: #161616; + padding: 5px; + padding-top: 15px; } + #status-bar .workspaces .workspace { + min-width: 10px; + min-height: 10px; + margin: 5px; + background-color: #262626; } + #status-bar .workspaces .occupied { + background-color: #393939; } + #status-bar .workspaces .focused { + background-color: #FF7EB6; } + #status-bar .media-controls { + margin-top: 20px; + min-width: 220px; } + #status-bar .media-controls .button { + font-size: 20px; } + #status-bar .progress-bar { + margin-top: 20px; + min-width: 200px; + min-height: 2px; + background: transparent; } + #status-bar .progress-bar trough { + min-height: 2px; + background: #262626; } + #status-bar .progress-bar highlight { + min-height: 2px; + background: #FF7Eb6; } + #status-bar .right { + min-height: 50px; + background: #161616; } + #status-bar .right .battery-container { + padding-left: 10px; + padding-right: 10px; } + #status-bar .right .battery-container .battery-dial { + color: #42be65; + background-color: #161616; + margin: 0px; + padding: 0px; + font-size: 5px; } + #status-bar .right .battery-container .battery-dial .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 16px; + color: #f2f4f8; } + #status-bar .right .sliderbox { + margin: 5px; } + #status-bar .right .sliderbox .volume .slider trough { + min-height: 10px; + min-width: 120px; + background: #262626; } + #status-bar .right .sliderbox .volume .slider highlight { + background: #33B1FF; } + #status-bar .right .sliderbox .volume button { + font-size: 20px; + margin: 5px; } + #status-bar .right .sliderbox .brightness .slider trough { + min-height: 10px; + min-width: 120px; + background: #262626; } + #status-bar .right .sliderbox .brightness .slider highlight { + background: #33B1FF; } + #status-bar .right .sliderbox .brightness button { + font-size: 20px; + margin: 5px; } + #status-bar .right separator { + background: #262626; + padding: 1px; + margin-top: 10px; + margin-bottom: 10px; } + #status-bar .right .clock { + background: #161616; } + #status-bar .right .clock .datetime { + margin: 1px; + font-size: 10pt; } + +#status-bar .Menu { + background-color: #262626; + min-width: 640px; } + #status-bar .Menu .mpd-controls { + background-color: #161616; + border-radius: 10px; + margin: 10px; } + #status-bar .Menu .mpd-controls .button { + font-size: 24px; + padding: 10px; + margin: 10px; + background-color: #262626; + border-color: #262626; + border-radius: 10px; } + #status-bar .Menu .mpd-controls .cover { + min-width: 250px; + min-height: 250px; + border-radius: 10px; + margin: 10px; + background: #161616; + background-size: cover; } + #status-bar .Menu .mpd-controls .title { + padding-top: 10px; + font-size: 20px; + font-weight: bold; } + #status-bar .Menu .mpd-controls .position { + margin: 10px; + margin-right: 20px; + min-height: 10px; + border-radius: 5px; } + #status-bar .Menu .mpd-controls .position trough { + background: #262626; + border-radius: 5px; + min-height: 10px; } + #status-bar .Menu .mpd-controls .position slider { + min-height: 10px; + border-radius: 5px; + background: #FF7Eb6; } + #status-bar .Menu .mpd-controls .position-label { + margin-top: 75px; } + #status-bar .Menu .dial-parent { + color: #FF7eb6; + background-color: #161616; + margin: 0px; + padding: 10px; + font-size: 8px; + margin-left: 10px; + border-radius: 10px; + border-radius: 10px; } + #status-bar .Menu .dial-parent .dial-icon { + font-family: 'Symbols Nerd Font Mono'; + font-size: 32px; + color: #f2f4f8; } + #status-bar .Menu .dial-parent .resource-dial { + min-width: 96px; + min-height: 96px; } + #status-bar .Menu .dial-parent .dial-icon { + color: #f2f4f8; } + +#notifications { + background: transparent; } + +.notifications { + opacity: 1; + min-width: 24rem; } + .notifications .revealer + .revealer > * > box { + margin-top: 0; } + .notifications .revealer .notification { + margin-bottom: 25px; + min-width: 24rem; + padding: 0px; + border: 2px solid #f2f4f8; + border-right: 25px solid #42be65; + background: #161616; } + .notifications .revealer .notification.critical { + border-right: 25px solid #FF7EB6; } + .notifications .revealer .body { + margin-right: 1em; } + .notifications .revealer .timeout-bar { + margin: 5px 0px 0; + margin-top: 5px; + background: #262626; } + .notifications .revealer .timeout-bar > trough > progress { + background-image: none; + background-color: #33B1FF; } + .notifications .revealer .button { + font-size: 20px; + margin-left: 5px; + margin-right: 5px; } + .notifications .revealer .button:hover { + color: #FF7EB6; } + .notifications .revealer .icon { + min-width: 68px; + min-height: 68px; + margin-right: 1em; + margin-left: 1em; } + .notifications .revealer .icon image { + font-size: 58px; + margin: 5px; + color: #f2f4f8; } + .notifications .revealer .icon box { + min-width: 68px; + min-height: 68px; } + .notifications .revealer .title { + font-size: 28px; } + .notifications .revealer .actions .action-button { + margin: 0 .4em; + margin-top: .8em; + border: 2px solid #f2f4f8; } + .notifications .revealer .actions .action-button:first-child { + margin-left: .5em; } + .notifications .revealer .actions .action-button:last-child { + margin-right: .5em; } + +.launcher { + min-width: 640px; + background: #262626; } + .launcher .search > image { + margin-left: 10px; } + .launcher .search > entry { + padding: 5px; + margin: 10px; + background: #161616; + border: 5px solid #161616; + border-radius: 10px; + font-size: 32px; } + .launcher .entry { + margin: 0 10px 0 10px; + border: 10px solid #161616; + border-radius: 10px; + background: #161616; } + .launcher .entry > box > image { + padding: 5px; + margin: 5px 10px 5px 10px; + background: #262626; + border-radius: 5px; } + .launcher > scrolledwindow > * > box:last-child { + margin-bottom: 10px; } + +* :not(selection) :not(tooltip) { + all: unset; } diff --git a/style/style.scss b/style/style.scss new file mode 100644 index 0000000..259aa96 --- /dev/null +++ b/style/style.scss @@ -0,0 +1,14 @@ +@import 'colors.scss'; +@import 'mixins.scss'; + +// components +@import './components/lock.scss'; +@import './components/bar.scss'; +@import './components/top-bar.scss'; +@import './components/bar-control-center.scss'; +@import './components/notification-popups.scss'; +@import './components/launcher.scss'; + +* :not(selection) :not(tooltip) { + all: unset; +} diff --git a/utils/PopupWindow.js b/utils/PopupWindow.js new file mode 100644 index 0000000..015ea0d --- /dev/null +++ b/utils/PopupWindow.js @@ -0,0 +1,29 @@ +export default ({ + name, + child, + transition = "slide_up", + transitionDuration = 250, + ...props +}) => { + const reveal = Variable(false) + const window = Widget.Window({ + name, + visible: false, + ...props, + + child: Widget.Box({ + css: `min-height: 2px; + min-width: 2px;`, + child: Widget.Revealer({ + transition, + transitionDuration, + hexpand: true, + vexpand: true, + child: child, + revealChild: reveal.bind() + }), + }), + }); + + return window, reveal; +} diff --git a/widgets/battery.js b/widgets/battery.js new file mode 100644 index 0000000..8fda125 --- /dev/null +++ b/widgets/battery.js @@ -0,0 +1,38 @@ +const battery = await Service.import('battery') + +const battery_dial = Widget.CircularProgress({ + className: 'battery-dial', + rounded: false, + inverted: false, + startAt: 0.75, + value: battery.bind('percent').as(p => p / 100), + child: Widget.Label({ + className: "dial-icon", + hexpand: true, + setup: (self) => { + self.hook(battery, (self) => { + console.log(battery) + const icons = [ + ["󰂎", "󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"], + ["󰢟", "󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"], + ]; + self.label = icons[Number(battery.charging)][Math.floor(battery.percent / 10)]; + self.tooltip_text = 'Battery ' + String(battery.percent) + '%'; + }); + } + }), + setup: (self) => { + self.hook(battery, (self) => { + if (battery.percent <= 30 && battery.charging === false) { + self.toggleClassName("battery-low", true); + } else { + self.toggleClassName("battery-low", false); + } + }); + } +}); + +export { + battery_dial +} + diff --git a/widgets/brightness.js b/widgets/brightness.js new file mode 100644 index 0000000..d2f067b --- /dev/null +++ b/widgets/brightness.js @@ -0,0 +1,63 @@ +const { exec, execAsync } = Utils; + +import Brightness from '../services/brightness.js' + +const brightness_dial = Widget.EventBox({ + className: 'eventbox-hide-pointer', + 'on-primary-click': () => {execAsync('hyprshade toggle blue-light-filter')}, + 'on-scroll-up': () => {Brightness.screen += 0.01}, + 'on-scroll-down': () => {Brightness.screen -= 0.01}, + child: Widget.CircularProgress({ + rounded: false, + className: 'brightness-dial', + inverted: false, + startAt: 0.75, + value: Brightness.bind('screen'), + child: Widget.Label({ + className: "dial-icon", + hexpand: true, + hpack: 'center', + setup: (self) => { + self.hook(Brightness, (self => { + const brightness = Brightness.screen * 100; + + self.label = ["󰃚", "󰃛", "󰃜", "󰃝", "󰃞", "󰃟", "󰃠"][Math.floor(brightness/15)] + self.tooltip_text = `Brightness ${Math.floor(brightness)}%`; + })) + } + }) + }) +}) + +const brightness_slider = Widget.Box({ + className: 'brightness', + children: [ + Widget.Button({ + on_clicked: () => execAsync('hyprshade toggle blue-light-filter'), + child: Widget.Icon().hook(Brightness, self => { + const brightness = Brightness.screen * 100; + const icon = [ + [80, 'display-brightness-high-symbolic'], + [50, 'display-brightness-medium-symbolic'], + [20, 'display-brightness-low-symbolic'], + [0, 'display-brightness-off-symbolic'] + ].find(([threshold]) => brightness >= threshold)?.[1]; + + self.icon = icon; + self.tooltip_text = `Brightness ${Math.floor(brightness)}%`; + }), + }), + Widget.Slider({ + className: 'slider', + hexpand: true, + drawValue: false, + onChange: ({ value }) => Brightness.screen = value, + value: Brightness.bind('screen'), + }) + ] +}); + +export { + brightness_dial, + brightness_slider +} diff --git a/widgets/clock.js b/widgets/clock.js new file mode 100644 index 0000000..cbd1eee --- /dev/null +++ b/widgets/clock.js @@ -0,0 +1,62 @@ +const { exec, execAsync } = Utils; + +const bar_clock = Widget.Box({ + className: 'clock', + vpack: 'end', + vertical: true, + setup: (self) => { + var month_and_date, hours_and_minutes, seconds; + self.poll(1000, self => { + execAsync("date +'%m/%d %H:%M %S'").then((time) => { + [month_and_date, hours_and_minutes, seconds] = time.split(' '); + }); + self.children = [ + Widget.Label({ + className: 'datetime', + label: month_and_date + }), + Widget.Label({ + className: 'datetime', + label: hours_and_minutes + }), + Widget.Label({ + className: 'datetime', + label: seconds + }), + ] + }) + } +}) + + +const horizontal_clock = Widget.Box({ + className: 'clock', + vpack: 'center', + vertical: true, + setup: (self) => { + var date_time, unix_seconds; + self.poll(1000, self => { + execAsync("date +'%d %b %H:%M:%S %s'").then((time) => { + let parts = time.split(' '); + date_time = `${parts[0]} ${parts[1]} ${parts[2]}`; + unix_seconds = parts[3]; + }); + self.children = [ + Widget.Label({ + className: 'datetime', + label: date_time + }), + Widget.Label({ + hpack: 'start', + className: 'datetime', + label: unix_seconds + }) + ]; + }); + } +}); + +export { + bar_clock, + horizontal_clock +} diff --git a/widgets/hypractive.js b/widgets/hypractive.js new file mode 100644 index 0000000..5980acd --- /dev/null +++ b/widgets/hypractive.js @@ -0,0 +1,10 @@ +const hyprland = await Service.import('hyprland') + +const active_window = Widget.Label({ + className: 'active-window', + label: '',//hyprland.bind('active').as(c => c.client.title), +}) + +export { + active_window +} diff --git a/widgets/hyprworkspaces.js b/widgets/hyprworkspaces.js new file mode 100644 index 0000000..58d5d5d --- /dev/null +++ b/widgets/hyprworkspaces.js @@ -0,0 +1,59 @@ +const hyprland = await Service.import('hyprland') + + +const goto_workspace = (ws) => hyprland.messageAsync(`dispatch workspace ${ws}`) + +const hyprworkspaces = Widget.EventBox({ + onScrollUp: () => goto_workspace('+1'), + onScrollDown: () => goto_workspace('-1'), + child: Widget.Box({ + vertical: true, + className: 'workspace-container', + children: Array.from({ length: 10 }, (_, i) => i + 1).map(i => Widget.Button({ + className: 'ws-norm', + attribute: i, + child: Widget.Label(String(i)), + onClicked: () => goto_workspace(i), + setup: self => self.hook(hyprland, self => self.attribute == hyprland.active.workspace.id ? + self.toggleClassName('ws-active', true) + : self.toggleClassName('ws-active', false)) + })), + + setup: self => self.hook(hyprland, () => self.children.forEach(btn => { + btn.visible = hyprland.workspaces.some(ws => ws.id === btn.attribute); + })), + }), +}) + +function workspace_row(start, length) { + return Widget.Box({ + vpack: 'center', + hpack: 'center', + className: 'workspace-row', + children: Array.from({ length: length }, (_, i) => i + 1 + start).map(i => Widget.Button({ + className: 'workspace', + attribute: i, + onClicked: () => goto_workspace(i), + setup: self => { + self.hook(hyprland, self => { + self.attribute == hyprland.active.workspace.id ? self.toggleClassName('focused', true) : self.toggleClassName('focused', false) + hyprland.workspaces.map(w => w.id).includes(self.attribute) ? self.toggleClassName('occupied', true) : self.toggleClassName('occupied', false) + }) + } + })) + }) +} + +const hyprworkspaces_grid = Widget.Box({ + className: 'workspaces', + vertical: true, + children: [ + workspace_row(0, 5), + workspace_row(5, 5) + ] +}) + +export { + hyprworkspaces, + hyprworkspaces_grid +} diff --git a/widgets/marqueeLabel.js b/widgets/marqueeLabel.js new file mode 100644 index 0000000..e0423a4 --- /dev/null +++ b/widgets/marqueeLabel.js @@ -0,0 +1,87 @@ +const { Gtk, cairo } = imports.gi; +const register = Widget.register; + +class MarqueeLabel extends Gtk.DrawingArea { + static { + register(this, { + properties: { + label: ["string", "rw"], + "scroll-speed": ["int", "rw"], + }, + }); + } + + #xOffset; + #scrollDirection; + + get label() { + return this._label; + } + + set label(label) { + this._label = label; + this.queue_draw(); + this.notify("label"); + } + + get scroll_speed() { + return this._scrollSpeed; + } + + set scroll_speed(speed) { + this._scrollSpeed = speed; + this.notify("scroll-speed"); + } + + constructor(props) { + super(props); + + this._reset(); + + this.poll(this._scrollSpeed * 11, () => this.queue_draw()); + + this.on("size-allocate", () => this._reset()); + this.on("notify::label", () => this._reset()); + } + + _reset() { + this.#xOffset = 1; + this.#scrollDirection = 0; + } + + vfunc_draw(cr) { + const allocation = this.get_allocation(); + const styles = this.get_style_context(); + const width = allocation.width; + const height = allocation.height; + const color = styles.get_color(Gtk.StateFlags.NORMAL); + const [fontFamily] = styles.get_property( + "font-family", + Gtk.StateFlags.NORMAL, + ); + const fontSize = Math.floor( + styles.get_property("font-size", Gtk.StateFlags.NORMAL), + ); + + cr.setSourceRGB(color.red, color.green, color.blue); + cr.selectFontFace(fontFamily, null, null); + cr.setFontSize(fontSize); + + const labelWidth = cr.textExtents(this._label).width; + + if (labelWidth > width) { + this.#xOffset += this.#scrollDirection * this._scrollSpeed; + + if (this.#xOffset >= 1 || this.#xOffset <= width - labelWidth) { + this.#scrollDirection *= 0; + } + } else { + this.#xOffset = (width - labelWidth) / 3; + } + + cr.moveTo(this.#xOffset*1.475, fontSize); + cr.showText(this._label); + } +} + +export default MarqueeLabel; diff --git a/widgets/mpd.js b/widgets/mpd.js new file mode 100644 index 0000000..01bf9ae --- /dev/null +++ b/widgets/mpd.js @@ -0,0 +1,182 @@ +const { Gtk } = imports.gi; + +import Mpd from '../services/mpd.js'; +import MarqueeLabel from './marqueeLabel.js' + +const Mpris = await Service.import("mpris"); + +const AspectFrame = Widget.subclass(Gtk.AspectFrame); + +function lengthString(length) { + return ( + `${Math.floor(length / 60) + .toString() + .padStart(2, "0")}:` + + `${Math.floor(length % 60) + .toString() + .padStart(2, "0")}` + ); +} + +const albumCover = Widget.Box({ + className: "cover", + setup: (self) => self.hook(Mpris, () => { + const mpd = Mpris.getPlayer("mpd"); + self.css = `background-image: url("${mpd?.coverPath}");`; + }), +}); + +const positionLabel = Widget.Label({ + className: 'position-label', + setup: (self) => self.poll(500, () => { + Mpd.send("status") + .then((msg) => { + const elapsed = msg?.match(/elapsed: (\d+\.\d+)/)?.[1]; + self.label = `${lengthString(elapsed || 0)} / ${lengthString(Mpd.duration || 0)}`; + }) + .catch((error) => logError(error)); + }), +}); + +const positionSlider = Widget.Slider({ + className: 'position', + vpack: 'end', + drawValue: false, + onChange: ({ value }) => { + Mpd.seekCur(value * Mpd.duration); + }, + setup: (self) => { + self.poll(500, () => { + Mpd.send("status") + .then((msg) => { + const elapsed = msg?.match(/elapsed: (\d+\.\d+)/)?.[1]; + self.value = elapsed / Mpd.duration || 0; + }) + .catch((error) => logError(error)); + }); + }, +}); + +const songTitle = Widget.Box({ + className: 'title', + children: [ + new MarqueeLabel({ + heightRequest: 30, + widthRequest: 350, + scrollSpeed: 1, + label: 'No Title', + setup: (self) => { + self.hook(Mpd, () => { + self.label = `${Mpd.Title || "No Title"}`; + }); + }, + }) + ] +}) + +const songArtist = Widget.Box({ + className: 'artist', + children: [ + new MarqueeLabel({ + heightRequest: 30, + widthRequest: 350, + scrollSpeed: 1, + label: 'No Artist', + setup: (self) => { + self.hook(Mpd, () => { + self.label = `${Mpd.Artist || "No Artist"}`; + }); + }, + }) + ] +}) + +const mediaControls = Widget.Box() + +const mpd_controls = () => Widget.CenterBox({ + className: 'mpd-controls', + hpack: 'center', + hexpand: true, + startWidget: Widget.Button({ + hpack: 'start', + className: 'button', + onClicked: () => Mpd.previous(), + child: Widget.Icon('media-skip-backward-symbolic') + }), + centerWidget: Widget.Button({ + hpack: 'center', + className: 'button', + onClicked: () => Mpd.playPause(), + child: Widget.Icon({ + setup: self => self.hook(Mpd, () => (Mpd.state === 'play') + ? self.icon = 'media-playback-pause-symbolic' + : self.icon = 'media-playback-start-symbolic') + }) + }), + endWidget: Widget.Button({ + hpack: 'end', + className: 'button', + onClicked: () => Mpd.next(), + child: Widget.Icon('media-skip-forward-symbolic') + }) +}) + +export const mpd_bar_controls = mpd_controls() + +export const mpd_menu_controls = Widget.Box({ + className: 'mpd-controls', + children: [ + albumCover, + Widget.Box({ + vertical: true, + hpack: 'end', + children: [ + Widget.Box({ + vertical: true, + children: [ + songTitle, + songArtist + ] + }), + mpd_controls(), + positionLabel, + positionSlider + ] + }) + ] +}) + +export const cover_with_controls = Widget.Box({ + className: 'cover', + hexpand: true, + vexpand: true, + child: Widget.Box({ + className: "cover", + vertical: true, + children: [ + mpd_controls(), + Widget.Slider({ + vpack: 'end', + className: 'progress-bar', + drawValue: false, + hexpand: false, + onChange: ({ value }) => Mpd.seekCur(value * Mpd.duration), + setup: (self) => { + self.poll(500, () => { + Mpd.send("status") + .then((msg) => { + const elapsed = msg?.match(/elapsed: (\d+\.\d+)/)?.[1]; + self.value = elapsed / Mpd.duration || 0; + }) + .catch((error) => logError(error)); + }); + }, + }) + ], + setup: (self) => + self.hook(Mpris, () => { + const mpd = Mpris.getPlayer("mpd"); + self.css = `background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url("${mpd?.coverPath}"); min-height: 60px; min-width: 250px; background-position: 50% 50%; background-size: cover; border: 5px solid #161616`; + }) + }) +}) diff --git a/widgets/mpris.js b/widgets/mpris.js new file mode 100644 index 0000000..07b24a9 --- /dev/null +++ b/widgets/mpris.js @@ -0,0 +1,114 @@ +const { Gtk, Gdk } = imports.gi; +const Mpris = await Service.import('mpris') + +const media_controls = (player) => Widget.CenterBox({ + className: 'media-controls', + hpack: 'center', + hexpand: true, + startWidget: Widget.Button({ + hpack: 'start', + className: 'button', + onClicked: () => player.previous(), + child: Widget.Icon('media-skip-backward-symbolic') + }), + centerWidget: Widget.Button({ + hpack: 'center', + className: 'button', + onClicked: () => player.playPause(), + child: Widget.Icon({ + setup: self => self.hook(Mpris, () => (player.playBackStatus === 'Playing') + ? self.icon = 'media-playback-pause-symbolic' + : self.icon = 'media-playback-start-symbolic') + }) + }), + endWidget: Widget.Button({ + hpack: 'end', + className: 'button', + onClicked: () => player.next(), + child: Widget.Icon('media-skip-forward-symbolic') + }) +}) + +const cover_with_controls = (player) => Widget.Box({ + className: "media", + children: [ + Widget.Box({ + vertical: true, + children: [ + media_controls(player), + Widget.Slider({ + vpack: 'end', + className: 'progress-bar', + drawValue: false, + hexpand: false, + onChange: ({ value }) => {player.position = (value * player.length)}, + setup: self => self.poll(500, self => { + if (!player) return + self.value = player.position/player.length + }) + }) + ], + }), + ], + setup: (self) => { + self.hook(Mpris, () => { + self.queue_draw() + self.css = `background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url("${player.coverPath}"); min-height: 60px; min-width: 250px; background-position: 50% 50%; background-size: cover; border: 5px solid #161616`; + }) + }, +}); + + +export const players = Widget.Stack({ + transition: "slide_up_down", + transitionDuration: 125, + children: {}, + setup: (self) => { + self.add_events(Gdk.EventMask.SCROLL_MASK); + self.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); + + let currentDeltaY = 0; + + self.on("scroll-event", (_, event) => { + const childNames = Object.keys(self.children); + + const length = Object.keys(self.children).length + + const prevChild = childNames[((n)=>n>=0?n:length-1)((childNames.indexOf(self.get_visible_child_name()) - 1) % length)]; + const nextChild = childNames[(childNames.indexOf(self.get_visible_child_name()) + 1) % length]; + + const deltaY = event.get_scroll_deltas()[2]; + + currentDeltaY += deltaY; + + if (currentDeltaY > 10 && prevChild) { + self.set_visible_child_name(prevChild); + currentDeltaY = 0; + } + if (currentDeltaY < -10 && nextChild) { + self.set_visible_child_name(nextChild); + currentDeltaY = 0; + } + console.log(self.children) + }); + + self.hook(Mpris, (_, name) => { + if (!name) return; + self.add_named(cover_with_controls(Mpris.getPlayer(name)), name); + }, "player-added"); + self.hook(Mpris, (_, name) => { + if (!name) return; + + self.get_child_by_name(name).destroy(); + delete children[name] + console.log('destroyed') + }, "player-closed"); + }, +}); + +export const media = Widget.Revealer({ + revealChild: Mpris.bind("players").as((players) => players.length > 0), + transition: "slide_up", + transitionDuration: 125, + child: players, +}); diff --git a/widgets/resourceDial.js b/widgets/resourceDial.js new file mode 100644 index 0000000..9b2c424 --- /dev/null +++ b/widgets/resourceDial.js @@ -0,0 +1,24 @@ +const { Gio, GioUnix } = imports.gi; + +export const resource_dial = (label, icon, timeout, command, transformer) => { + let poll = Variable(100, { + poll: [timeout, command, v => Number(v)] + }) + + return Widget.Box({ + className: 'dial-parent', + children: [ + Widget.CircularProgress({ + startAt: 0.75, + className: 'resource-dial', + value: poll.bind().as(v => transformer(v)), + tooltipText: poll.bind().as(v => label(v)), + child: Widget.Icon({ + className: 'dial-icon', + icon: icon + }), + }) + ] + }) +} + diff --git a/widgets/systray.js b/widgets/systray.js new file mode 100644 index 0000000..193b83f --- /dev/null +++ b/widgets/systray.js @@ -0,0 +1,14 @@ +const systemtray = await Service.import('systemtray') + +const SysTrayItem = item => Widget.Button({ + child: Widget.Icon().bind('icon', item, 'icon'), + tooltipMarkup: item.bind('tooltip_markup'), + onPrimaryClick: (_, event) => item.activate(event), + onSecondaryClick: (_, event) => item.openMenu(event), +}); + +export const systray = Widget.Box({ + className: 'systray', + vertical: true, + children: systemtray.bind('items').as(i => i.map(SysTrayItem)) +}) diff --git a/widgets/volume.js b/widgets/volume.js new file mode 100644 index 0000000..4347880 --- /dev/null +++ b/widgets/volume.js @@ -0,0 +1,70 @@ +const { exec, execAsync } = Utils + +const audio = await Service.import('audio') + +const volume_dial = Widget.EventBox({ + className: 'eventbox-hide-pointer', + 'on-scroll-up': () => {audio.speaker.volume += 0.01}, + 'on-scroll-down': () => {audio.speaker.volume -= 0.01}, + 'on-primary-click': () => {audio.speaker.is_muted = !audio.speaker.is_muted}, + child: Widget.CircularProgress({ + className: 'volume-dial', + rounded: false, + inverted: false, + startAt: 0.75, + value: audio.speaker.bind('volume'), + child: Widget.Icon({ + className: "dial-icon", + hexpand: true, + setup: (self) => { + self.hook(audio, (self => { + const vol = audio.speaker.volume * 100; + const icon = [ + [101, 'overamplified'], + [67, 'high'], + [34, 'medium'], + [1, 'low'], + [0, 'muted'], + ].find(([threshold]) => threshold <= vol)?.[1]; + + self.icon = `audio-volume-${icon}-symbolic`; + self.tooltip_text = `Volume ${Math.floor(vol)}%`; + })) + } + }) + }) +}) + +const volume_slider = Widget.Box({ + className: 'volume', + children: [ + Widget.Button({ + on_clicked: () => audio.speaker.is_muted = !audio.speaker.is_muted, + child: Widget.Icon().hook(audio.speaker, self => { + const vol = audio.speaker.volume * 100; + const icon = [ + [101, 'overamplified'], + [67, 'high'], + [34, 'medium'], + [1, 'low'], + [0, 'muted'], + ].find(([threshold]) => threshold <= vol)?.[1]; + + self.icon = `audio-volume-${icon}-symbolic`; + self.tooltip_text = `Volume ${Math.floor(vol)}%`; + }), + }), + Widget.Slider({ + className: 'slider', + hexpand: true, + drawValue: false, + onChange: ({ value }) => audio['speaker'].volume = value, + value: audio['speaker'].bind('volume'), + }) + ] +}) + +export { + volume_dial, + volume_slider +} diff --git a/windows/bar.js b/windows/bar.js new file mode 100644 index 0000000..9fadf99 --- /dev/null +++ b/windows/bar.js @@ -0,0 +1,141 @@ +import { battery_dial } from '../widgets/battery.js' +import { volume_dial } from '../widgets/volume.js' +import { brightness_dial } from '../widgets/brightness.js' +import { hyprworkspaces } from '../widgets/hyprworkspaces.js' +import { mpd_bar_controls } from '../widgets/mpd.js' +// import { systray } from '../widgets/systray.js' + +import { bar_clock} from '../widgets/clock.js' + +import { MenuWidget } from './menu.js' +import { reveal_launcher, launcher } from './launcher.js' + +export const FakeBar = Widget.Window({ + name: 'FakeBar', + exclusivity: 'exclusive', + anchor: ['left'], + margins: [0, 35], + child: Widget.Box({css: 'min-width: 1px;'}) +}) + +let reveal_menu = Variable(false) +let reveal_menu_button = Variable(false) + +const MenuRevealButton = Widget.Box({ + children: [ + Widget.Button({ + vexpand: false, + vpack: 'center', + className: 'menu-visibility-button', + 'on-primary-click': (self) => reveal_menu.value = !reveal_menu.value, + child: Widget.Icon({ + icon: reveal_menu.bind().as(reveal => !reveal ? 'go-next-symbolic' : 'go-previous-symbolic') + }) + }) + ] +}) + +const BarTopWidget = Widget.Box({ + vertical: true, + vpack: 'start', + children: [ + Widget.Box({ + className: 'dial-container', + vertical: true, + spacing: 8, + children: [ + battery_dial, + volume_dial, + brightness_dial + ] + }), + Widget.Box({ + css: 'margin-left: 35px; margin-right: 35px' + }) + ] +}) + +const BarMiddleWidget = Widget.Box({ + children: [ + Widget.EventBox({ + 'on-hover': () => reveal_menu_button.value = true, + child: Widget.Box({ + css: 'min-width: 1px; min-height: 40px;' + }) + }), + hyprworkspaces + ] +}) + +const BarEndWidget = Widget.Box({ + vertical: true, + vpack: 'end', + children: [ + // systray, + mpd_bar_controls, + bar_clock + ] +}) + +const BarWidget = Widget.CenterBox({ + homogeneous: false, + vertical: true, + spacing: 10, + startWidget: BarTopWidget, + centerWidget: BarMiddleWidget, + endWidget: BarEndWidget +}) + +const MenuButtonRevealer = Widget.Revealer({ + revealChild: reveal_menu_button.bind(), + transition: 'slide_right', + child: MenuRevealButton +}) + +const MenuRevealer = Widget.Revealer({ + revealChild: reveal_menu.bind(), + transition: 'slide_right', + transitionDuration: 500, + child: MenuWidget, +}) + +const LauncherRevealer = Widget.Revealer({ + revealChild: reveal_launcher.bind(), + transition: 'slide_right', + transitionDuration: 500, + child: launcher, +}) + +export const Bar = Widget.Window({ + name: 'status-bar', + exclusivity: 'ignore', + keymode: reveal_launcher.bind().as(v => v ? 'exclusive' : 'on-demand'), + anchor: ['top', 'left', 'bottom'], + child: Widget.Overlay({ + 'pass-through': true, + overlay: Widget.Box({ + children: [ + Widget.EventBox({ + 'on-hover': () => reveal_menu_button.value = true, + 'on-hover-lost': () => reveal_menu_button.value = false, + child: Widget.Box({ + css: 'margin-right: 4px;', + children: [MenuButtonRevealer] + }) + }), + ] + }), + child: Widget.Box({ + children: [ + LauncherRevealer, + MenuRevealer, + BarWidget + ] + }) + }) +}) + +export { + reveal_menu, + reveal_launcher +} diff --git a/windows/launcher.js b/windows/launcher.js new file mode 100644 index 0000000..f5bf4eb --- /dev/null +++ b/windows/launcher.js @@ -0,0 +1,93 @@ +const Applications = await Service.import("applications") + +const reveal_launcher = Variable(false) + +function appItem(app) { + return Widget.Button({ + className: 'entry', + onClicked: () => { + reveal_launcher.value = false + app.launch() + }, + attribute: { app }, + child: Widget.Box([ + Widget.Icon({ + icon: app.icon_name || '', + size: 42, + }), + Widget.Label({ + className: 'app-title', + label: app.name, + xalign: 0, + vpack: 'center', + truncate: 'end' + }) + ]) + }) +} + +function _launcher() { + let applications = Applications.query('').map(appItem) + + const list = Widget.Box({ + vertical: true, + children: applications, + spacing: 12 + }) + + function repopulate() { + applications = Applications.query('').map(appItem) + list.children = applications + } + + const entry = Widget.Box({ + className: 'search', + children: [ + Widget.Icon({ + icon: 'edit-find-symbolic', + size: 42, + }), + Widget.Entry({ + hexpand: true, + className: 'search', + on_accept: () => { + applications.filter((item) => item.visible)[0]?.attribute.app.launch() + reveal_launcher.value = false + }, + on_change: ({ text }) => applications.forEach(item => { + item.visible = item.attribute.app.match(text ?? '') + }) + }) + ] + }) + + return Widget.Box({ + vertical: true, + className: 'launcher', + children: [ + entry, + Widget.Scrollable({ + hscroll: 'never', + vexpand: true, + hexpand: true, + child: list + }) + ], + setup: self => self.hook(reveal_launcher, () => { + entry.text = '' + if (reveal_launcher.value) { + repopulate() + console.log('nya') + entry.text = '' + entry.grab_focus() + } + }, "changed") + }) +} + +const launcher = _launcher() + +export { + reveal_launcher, + launcher +} diff --git a/windows/lock.js b/windows/lock.js new file mode 100644 index 0000000..0e4974b --- /dev/null +++ b/windows/lock.js @@ -0,0 +1,76 @@ +import { show_notification_popups } from './notifications.js' + +// TODO: const { Gdk, GtkSessionLock } = imports.gi; +const { authenticateUser, exec } = Utils + +export const show_lock = Variable(false) +const lock_ready = Variable(false) + +const username = Variable() + +const password_entry = Widget.Entry({ + visibility: false, + xalign: 0.5, + onAccept: (self) => { + authenticateUser(username.value, self.text) + .then(() => { + show_lock.value = false + exec('rm /tmp/lock-pixelated.png') + show_notification_popups.value = true + }) + .catch(() => self.text = '') + self.text = '' + username_entry.text = '' + username_entry.grab_focus() + }, + setup: self => self.text = '' +}) + +const username_entry = Widget.Entry({ + xalign: 0.5, + onAccept: (self) => { + username.value = self.text + password_entry.grab_focus() + }, +}) + +const login_container = Widget.Box({ + child: Widget.Box({ + className: lock_ready.bind().as(b => b ? 'lock-ready' : 'lock'), + vpack: 'center', + hpack: 'center', + hexpand: 'true', + vertical: true, + child: Widget.Box({ + vertical: true, + children: [ + Widget.Box({className: 'img'}), + Widget.Box({ + vertical: true, + children: [ + username_entry, + password_entry + ] + }) + ] + }) + }), + setup: self => self.hook(show_lock, () => { + self.css = 'min-width: 2560px; min-height: 1600px; background: url("/tmp/lock-pixelated.png");' + lock_ready.value = true + }) +}) + +export const Lock = Widget.Window({ + name: 'lock', + visible: show_lock.bind().as(b => { + if (b) { + exec(`bash -c "grim - | convert - -scale 12.5% -scale 800% -filter point /tmp/lock-pixelated.png"`) + show_notification_popups.value = false + } + return b + }), + exclusivity: 'ignore', + keymode: 'exclusive', + child: login_container, +}) diff --git a/windows/menu.js b/windows/menu.js new file mode 100644 index 0000000..5701ebf --- /dev/null +++ b/windows/menu.js @@ -0,0 +1,49 @@ +import { mpd_menu_controls } from '../widgets/mpd.js' +import { resource_dial } from '../widgets/resourceDial.js' + +export const MenuWidget = Widget.Box({ + vertical: true, + className: 'Menu', + children: [ + mpd_menu_controls, + Widget.Box({ + children: [ + resource_dial( + (v) => `cpu: ${v}%`, + 'microchip-solid', + 1000, + "bash -c \"top -bn1 | grep 'Cpu(s)' | awk '{print \$2 + \$4}'\"", + v => v/100 + ), + resource_dial( + (v) => `mem: ${Math.round(100*v/31396)}%`, + 'memory-solid', + 1000, + "bash -c \"free -m | grep Mem | awk '{print $3}'\"", + v => v/31396 + ), + resource_dial( + (v) => `igpu: ${v}%`, + 'expansion-card', + 1000, + "bash -c \"cat /sys/class/drm/card1/device/gpu_busy_percent\"", + v => v/100 + ), + resource_dial( + (v) => `fan: ${v}rpm`, + 'fan', + 1000, + "bash -c \"sudo ectool pwmgetfanrpm | awk '{a+=\$4} END {print a/2}'\"", + v => v/5000 + ), + resource_dial( + (v) => `temp (cpu): ${v}`, + 'thermometer', + 1000, + "bash -c \"sudo ectool temps all | grep -E 'cpu' | awk '{print \$5}'\"", + v => v/100 + ), + ] + }) + ] +}) diff --git a/windows/notifications.js b/windows/notifications.js new file mode 100644 index 0000000..43e32dc --- /dev/null +++ b/windows/notifications.js @@ -0,0 +1,175 @@ +const Notifications = await Service.import('notifications') +const { Pango } = imports.gi; + +Notifications.popupTimeout = 3000; + + + +function notification_icon({ app_entry, app_icon, image }) { + if (image) { + return Widget.Box({ + css: `background-image: url("${image}");` + + 'background-size: contain;' + + 'background-repeat: no-repeat;' + + 'background-position: center;', + }) + } + + let icon = 'dialog-information-symbolic' + if (Utils.lookUpIcon(app_icon)) + icon = app_icon + + if (app_entry && Utils.lookUpIcon(app_entry)) + icon = app_entry + + return Widget.Box({ + child: Widget.Icon(icon), + }) +} + +const notification = (n) => { + const icon = Widget.Box({ + vpack: 'start', + class_name: 'icon', + child: notification_icon(n), + }) + + const title = Widget.Label({ + className: 'title', + label: n.summary, + xalign: 0, + justify: 'left', + }) + + const body = Widget.Label({ + className: 'body', + label: n.body, + xalign: 0, + justification: 'left', + maxWidthChars: 26, + wrap: true, + wrapMode: Pango.WrapMode.WORD_CHAR, + useMarkup: true, + }) + + const actions = Widget.Box({ + class_name: "actions", + children: n.actions.map(({ id, label }) => Widget.Button({ + class_name: "action-button", + on_clicked: () => { + n.invoke(id) + n.dismiss() + }, + hexpand: true, + child: Widget.Label(label), + })), + }) + + const buttons = Widget.Box({ + hpack: 'end', + children: [ + Widget.Button({ + className: 'button', + onClicked: () => n.dismiss(), + child: Widget.Label('') + }) + ] + }) + + const timeout_progress = Widget.ProgressBar({ + className: 'timeout-bar', + hexpand: true, + vpack: 'end', + value: 1, + setup: (self) => { + self.poll(n.timeout/100, () => { + if (self.value > 0.01) { + self.value = self.value - .01 + } + else { n.dismiss() } // Notifications.forceTimeout doesn't work for notifications that are bugged. + }) + } + }) + + const layout = Widget.Box({ + className: `notification ${n.urgency}`, + vertical: true, + children: [ + buttons, + Widget.Box([ + icon, + Widget.Box({ + vertical: true, + children: [ + title, body + ] + }) + ]), + actions, + timeout_progress + ] + }) + + return Widget.Revealer({ + className: 'revealer', + attribute: { id: n.id }, + vpack: 'start', + transition: 'slide_down', + transitionDuration: 250, + setup: (self) => { + Utils.timeout(1, () => self.set_reveal_child(true)); + self.on('notify::reveal-child', () => { + if (self.reveal_child) Utils.timeout(125, () => self.child.set_reveal_child(true)) + }) + }, + child: Widget.Revealer({ + className: 'revealer', + hpack: 'end', + transition: 'slide_left', + transitionDuration: 250, + setup: (self) => self.on('notify::reveal-child', () => { + if (!self.reveal_child) Utils.timeout(250, () => self.parent.set_reveal_child(false)) + }), + child: layout + }) + }) +} + +export const show_notification_popups = Variable(true) +export const NotificationPopups = Widget.Window({ + visible: show_notification_popups.bind(), + name: 'notifications', + anchor: ['top', 'right'], + layer: 'overlay', + margins: [5, 0, 0, 0], + child: Widget.Box({ + className: 'notifications', + vertical: true, + widthRequest: 2, + heightRequest: 2, + children: Notifications.popups.map(notification), + setup: (self) => self.hook(Notifications, (_, id) => { + if (!id || Notifications.dnd) return; + + const n = Notifications.getNotification(id) + + if (!n) return; + + self.children = [...self.children, notification(n)] + }, 'notified') + .hook(Notifications, (_, id) => { + if (!id) return; + + const n = self.children.find( + (child) => child.attribute.id === id, + ) + + if (!n) return; + + n.child.set_reveal_child(false) + + Utils.timeout(n.child.transition_duration, () => n.destroy()) + }, 'dismissed') + }) +}) + diff --git a/windows/top-bar.js b/windows/top-bar.js new file mode 100644 index 0000000..b036b92 --- /dev/null +++ b/windows/top-bar.js @@ -0,0 +1,70 @@ +import { hyprworkspaces_grid } from '../widgets/hyprworkspaces.js' +import { volume_slider } from '../widgets/volume.js' +import { brightness_slider } from '../widgets/brightness.js' +import { battery_dial } from '../widgets/battery.js' +import { horizontal_clock } from '../widgets/clock.js' +import { active_window } from '../widgets/hypractive.js' +import { media } from '../widgets/mpris.js' + +export const FakeBar = Widget.Window({ + name: 'FakeBar', + exclusivity: 'exclusive', + anchor: ['left', 'top', 'right'], + margins: [35, 0], + child: Widget.Box({css: 'min-height: 1px;'}) +}) + +const left_box = Widget.Box({ + className: 'left', + hpack: 'start', + children: [ + hyprworkspaces_grid, + media + ] +}) + +const middle_box = Widget.Box({ + className: 'middle', + hpack: 'center', + children: [ + active_window + ] +}) + +const right_box = Widget.Box({ + className: 'right', + hpack: 'end', + children: [ + Widget.Box({ + className: 'sliderbox', + vertical: true, + children: [ + volume_slider, + brightness_slider + ] + }), + Widget.Box({ + className: 'battery-container', + children: [battery_dial] + }), + Widget.Separator({vertical: true}), + horizontal_clock + ] +}) + +const bar = Widget.Box({ + className: 'bar', + children: [ + left_box, + middle_box, + right_box + ] +}) + +export const Bar = Widget.Window({ + name: 'status-bar', + exclusivity: 'ignore', + // margins: [5, 5, 5, 5], + anchor: ['left', 'top', 'right'], + child: bar +})