diff --git a/waybar/config b/waybar/config new file mode 100644 index 0000000..bc7d6f7 --- /dev/null +++ b/waybar/config @@ -0,0 +1,171 @@ +{ + "layer": "top", // Waybar at top layer + "height": 30, // Waybar height (to be removed for auto height) + "spacing": 4, // Gaps between modules (4px) + // Choose the order of the modules + "modules-left": ["custom/os", "hyprland/workspaces"], + "modules-right": ["custom/spotify", "network", "bluetooth", "cpu", + "memory", "disk", "temperature", "keyboard-state", "battery", "clock" ], + "keyboard-state": { + "numlock": true, + "capslock": true, + "format": "{name} {icon}", + "format-icons": { + "locked": "", + "unlocked": "" + } + }, + "mpd": { + "format": "{stateIcon} {consumeIcon}{randomIcon}{repeatIcon}{singleIcon}{artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S}) ⸨{songPosition}|{queueLength}⸩ {volume}% ", + "format-disconnected": "Disconnected ", + "format-stopped": "{consumeIcon}{randomIcon}{repeatIcon}{singleIcon}Stopped ", + "unknown-tag": "N/A", + "interval": 2, + "consume-icons": { + "on": " " + }, + "random-icons": { + "off": " ", + "on": " " + }, + "repeat-icons": { + "on": " " + }, + "single-icons": { + "on": "1 " + }, + "state-icons": { + "paused": "", + "playing": "" + }, + "tooltip-format": "MPD (connected)", + "tooltip-format-disconnected": "MPD (disconnected)" + }, + "idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + } + }, + "tray": { + // "icon-size": 21, + "spacing": 10 + }, + "clock": { + // "timezone": "America/New_York", + "format": "{: %H:%M  %d/%m/%Y}", + "tooltip-format": "{:%Y %B}\n{calendar}", + "format-alt": "{:%Y-%m-%d}" + }, + "cpu": { + "format": " {usage}%", + "tooltip": false + }, + "memory": { + "format": " {}%" + }, + "temperature": { + // "thermal-zone": 2, + // "hwmon-path": "/sys/class/hwmon/hwmon2/temp1_input", + "critical-threshold": 80, + // "format-critical": "{temperatureC}°C {icon}", + "format": "{icon} {temperatureC}°C", + "format-icons": ["", "", ""] + }, + "backlight": { + "device": "acpi_video1", + "format": "{percent}% {icon}", + "format-icons": ["", "", "", "", "", "", "", "", ""] + }, + "battery": { + "states": { + // "good": 95, + "warning": 30, + "critical": 15 + }, + "format": "{icon} {capacity}%", + "format-charging": "{capacity}%", + "format-plugged": "{capacity}%", + "format-alt": "{time} {icon}", + // "format-good": "", // An empty format will hide the module + // "format-full": "", + "format-icons": ["", "", "", "", ""] + }, + "battery#bat2": { + "bat": "BAT2" + }, + "network": { + // "interface": "wlp2*", // (Optional) To force the use of this interface + "format-wifi": " {essid} ({signalStrength}%)", + "format-ethernet": "{ipaddr}/{cidr} ", + "tooltip-format": "{ifname} via {gwaddr} ", + "format-linked": "{ifname} (No IP) ", + "format-disconnected": "⚠ Disconnected", + "format-alt": "{ifname}: {ipaddr}/{cidr}" + }, + "pulseaudio": { + // "scroll-step": 1, // %, can be a float + "format": "{icon} {volume}%", + "format-bluetooth": "{icon} {volume}% {format_source}", + "format-bluetooth-muted": " {icon} {format_source}", + "format-muted": " {format_source}", + "format-source": " {volume}%", + "format-source-muted": "", + "format-icons": { + "headphone": "", + "hands-free": "", + "headset": "", + "phone": "", + "portable": "", + "car": "", + "default": ["", "", ""] + }, + "on-click": "pavucontrol" + }, + "custom/media": { + "format": "{icon} {}", + "return-type": "json", + "max-length": 40, + "format-icons": { + "spotify": "", + "default": "🎜" + }, + "escape": true, + "exec": "$HOME/.config/waybar/mediaplayer.py 2> /dev/null" // Script in resources folder + // "exec": "$HOME/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name + }, + "custom/os": { + "format": " archbish", + }, + +"disk": { + "interval": 30, + "format": " {percentage_used}%", + "path": "/home", +}, + +"custom/spotify": { + "exec": "/usr/bin/python3 $HOME/.config/waybar/resources/custom_modules/mediaplayer.py --player spotify", + "format": "{} ", + "return-type": "json", + "on-click": "playerctl play-pause", + "on-scroll-up": "playerctl next", + "on-scroll-down": "playerctl previous" +}, + +"bluetooth": { + "controller": "bluetoothctl", // specify the alias of the controller if there are more than 1 on the system + "format": " {status}", + "format-connected": " {device_alias}", + "format-connected-battery": " {device_alias} {device_battery_percentage}% ", + // "format-device-preference": [ "device1", "device2" ], // preference list deciding the displayed device + "tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected", + "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}", + "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}", + "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%" +} + + +} + diff --git a/waybar/resources/custom_modules/mediaplayer.py b/waybar/resources/custom_modules/mediaplayer.py new file mode 100644 index 0000000..c66c50f --- /dev/null +++ b/waybar/resources/custom_modules/mediaplayer.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version("Playerctl", "2.0") +from gi.repository import Playerctl, GLib +from gi.repository.Playerctl import Player +import argparse +import logging +import sys +import signal +import gi +import json +import os +from typing import List + +logger = logging.getLogger(__name__) + + +def signal_handler(sig, frame): + logger.info("Received signal to stop, exiting") + sys.stdout.write("\n") + sys.stdout.flush() + # loop.quit() + sys.exit(0) + + +class PlayerManager: + def __init__(self, selected_player=None): + self.manager = Playerctl.PlayerManager() + self.loop = GLib.MainLoop() + self.manager.connect( + "name-appeared", lambda *args: self.on_player_appeared(*args) + ) + self.manager.connect( + "player-vanished", lambda *args: self.on_player_vanished(*args) + ) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + self.selected_player = selected_player + + self.init_players() + + def init_players(self): + for player in self.manager.props.player_names: + if self.selected_player is not None and self.selected_player != player.name: + logger.debug(f"{player.name} is not the filtered player, skipping it") + continue + self.init_player(player) + + def run(self): + logger.info("Starting main loop") + self.loop.run() + + def init_player(self, player): + logger.info(f"Initialize new player: {player.name}") + player = Playerctl.Player.new_from_name(player) + player.connect("playback-status", self.on_playback_status_changed, None) + player.connect("metadata", self.on_metadata_changed, None) + self.manager.manage_player(player) + self.on_metadata_changed(player, player.props.metadata) + + def get_players(self) -> List[Player]: + return self.manager.props.players + + def write_output(self, text, player): + logger.debug(f"Writing output: {text}") + + output = { + "text": text, + "class": "custom-" + player.props.player_name, + "alt": player.props.player_name, + } + + sys.stdout.write(json.dumps(output) + "\n") + sys.stdout.flush() + + def clear_output(self): + sys.stdout.write("\n") + sys.stdout.flush() + + def on_playback_status_changed(self, player, status, _=None): + logger.debug( + f"Playback status changed for player {player.props.player_name}: {status}" + ) + self.on_metadata_changed(player, player.props.metadata) + + def get_first_playing_player(self): + players = self.get_players() + logger.debug(f"Getting first playing player from {len(players)} players") + if len(players) > 0: + # if any are playing, show the first one that is playing + # reverse order, so that the most recently added ones are preferred + for player in players[::-1]: + if player.props.status == "Playing": + return player + # if none are playing, show the first one + return players[0] + else: + logger.debug("No players found") + return None + + def show_most_important_player(self): + logger.debug("Showing most important player") + # show the currently playing player + # or else show the first paused player + # or else show nothing + current_player = self.get_first_playing_player() + if current_player is not None: + self.on_metadata_changed(current_player, current_player.props.metadata) + else: + self.clear_output() + + def on_metadata_changed(self, player, metadata, _=None): + logger.debug(f"Metadata changed for player {player.props.player_name}") + player_name = player.props.player_name + artist = player.get_artist() + title = player.get_title() + + track_info = "" + if ( + player_name == "spotify" + and "mpris:trackid" in metadata.keys() + and ":ad:" in player.props.metadata["mpris:trackid"] + ): + track_info = "Advertisement" + elif artist is not None and title is not None: + track_info = f"{artist} - {title}" + else: + track_info = title + + if track_info: + if player.props.status == "Playing": + track_info = " " + track_info + else: + track_info = " " + track_info + # only print output if no other player is playing + current_playing = self.get_first_playing_player() + if ( + current_playing is None + or current_playing.props.player_name == player.props.player_name + ): + self.write_output(track_info, player) + else: + logger.debug( + f"Other player {current_playing.props.player_name} is playing, skipping" + ) + + def on_player_appeared(self, _, player): + logger.info(f"Player has appeared: {player.name}") + if player is not None and ( + self.selected_player is None or player.name == self.selected_player + ): + self.init_player(player) + else: + logger.debug( + "New player appeared, but it's not the selected player, skipping" + ) + + def on_player_vanished(self, _, player): + logger.info(f"Player {player.props.player_name} has vanished") + self.show_most_important_player() + + +def parse_arguments(): + parser = argparse.ArgumentParser() + + # Increase verbosity with every occurrence of -v + parser.add_argument("-v", "--verbose", action="count", default=0) + + # Define for which player we"re listening + parser.add_argument("--player") + + parser.add_argument("--enable-logging", action="store_true") + + return parser.parse_args() + + +def main(): + arguments = parse_arguments() + + # Initialize logging + if arguments.enable_logging: + logfile = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "media-player.log" + ) + logging.basicConfig( + filename=logfile, + level=logging.DEBUG, + format="%(asctime)s %(name)s %(levelname)s:%(lineno)d %(message)s", + ) + + # Logging is set by default to WARN and higher. + # With every occurrence of -v it's lowered by one + logger.setLevel(max((3 - arguments.verbose) * 10, 0)) + + logger.info("Creating player manager") + if arguments.player: + logger.info(f"Filtering for player: {arguments.player}") + player = PlayerManager(arguments.player) + player.run() + + +if __name__ == "__main__": + main() diff --git a/waybar/style.css b/waybar/style.css new file mode 100644 index 0000000..4a3e7fe --- /dev/null +++ b/waybar/style.css @@ -0,0 +1,250 @@ +* { + /* `otf-font-awesome` is required to be installed for icons */ + font-family: FontAwesome, "PragmataPro Mono Liga", Roboto, Helvetica, Arial, + sans-serif; + font-size: 16px; + font-weight: 600; +} + +window#waybar { + background-color: transparent; + border-bottom: none; + color: #ffffff; + transition-property: background-color; + transition-duration: 0.5s; +} + +window#waybar.hidden { + opacity: 0.2; +} + +/* +window#waybar.empty { + background-color: transparent; +} +window#waybar.solo { + background-color: #FFFFFF; +} +*/ + +window#waybar.termite { + background-color: #3f3f3f; +} + +window#waybar.chromium { + background-color: #000000; + border: none; +} + +button { + /* Use box-shadow instead of border so the text isn't offset */ + box-shadow: inset 0 -3px transparent; + /* Avoid rounded borders under each button name */ + border: none; + border-radius: 0; +} + +/* https://github.com/Alexays/Waybar/wiki/FAQ#the-workspace-buttons-have-a-strange-hover-effect */ + +#workspaces button.active { + background-color: #c678dd; + color: #2d3436; +} + +#workspaces button { + padding: 0 6px; + color: #2d3436; + background-color: #abb2bf; + border-radius: 4px; +} + +#workspaces button.urgent { + background-color: #eb4d4b; +} + +#mode { + background-color: #64727d; + border-bottom: 3px solid #ffffff; +} + +#clock, +#battery, +#cpu, +#memory, +#disk, +#temperature, +#backlight, +#network, +#pulseaudio, +#wireplumber, +#custom-media, +#tray, +#custom-spotify, +#mode, +#idle_inhibitor, +#scratchpad, +#mpd, +#user, +#bluetooth, +#custom-os { + padding: 3px 12px; + margin: 3px 3px 0; + color: #ffffff; + border-radius: 4px; +} + +#window, +#workspaces { + margin: 3px 3px 0; +} + +#window { + color: #abb2bf; +} + +/* If workspaces is the leftmost module, omit left margin */ +.modules-left > widget:first-child > #workspaces { + margin-left: 0; +} + +/* If workspaces is the rightmost module, omit right margin */ +.modules-right > widget:last-child > #workspaces { + margin-right: 0; +} + +#clock { + background-color: #abb2bf; + color: #2d3436; +} + +#bluetooth { + background-color: #abb2bf; + color: #2d3436; +} + +#battery { + background-color: #98c379; + color: #282c34; +} + +#battery.charging, +#battery.plugged { + color: #282c34; + background-color: #98c379; +} + +@keyframes blink { + to { + background-color: #ffffff; + color: #282c34; + } +} + +#battery.critical:not(.charging) { + background-color: #be5046; + color: #ffffff; + animation-name: blink; + animation-duration: 0.5s; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-direction: alternate; +} + +label:focus { + background-color: #000000; +} + +#cpu { + background-color: #98c379; + color: #282c34; +} + +#memory { + color: #282c34; + background-color: #c678dd; +} + +#disk { + color: #282c34; + background-color: #e5c07b; +} + +#backlight { + background-color: #61afef; + color: #282c34; +} + +#network { + background-color: #61afef; + color: #282c34; +} + +#network.disconnected { + background-color: #e06c75; +} + +#user { + background-color: #e06c75; + color: #282c34; +} + +#pulseaudio { + background-color: #e5c07b; + color: #282c34; +} + +#pulseaudio.muted { + background-color: #90b1b1; + color: #e5c07b; +} + +#wireplumber { + background-color: #fff0f5; + color: #000000; +} + +#wireplumber.muted { + background-color: #e06c75; +} + +#custom-spotify { + background-color: #98c379; + color: #282c34; +} + +#temperature { + color: #282c34; + background-color: #56b6c2; +} + +#temperature.critical { + color: #282c34; + background-color: #e06c75; +} + +#tray { + background-color: #2980b9; +} + +#tray > .passive { + -gtk-icon-effect: dim; +} + +#tray > .needs-attention { + -gtk-icon-effect: highlight; + background-color: #e06c75; +} + +#idle_inhibitor { + background-color: #2d3436; +} + +#idle_inhibitor.activated { + background-color: #abb2bf; + color: #2d3436; +} + +#custom-os { + background-color: #61afef; + font-size: 16px; +}