Compare commits

...

7 Commits

3 changed files with 232 additions and 2 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__
*~

View File

@@ -29,10 +29,11 @@ from libqtile.config import Click, Drag, Group, Key, Match, hook, Screen, KeyCho
from libqtile.lazy import lazy
from libqtile.utils import guess_terminal
from libqtile.dgroups import simple_key_binder
from spotify import Spotify
mod = "mod4" #aka Windows key
terminal = "alacritty" #This is an example on how flexible Qtile is, you create variables then use them in a keybind for example (see below)
terminal = "kitty" #This is an example on how flexible Qtile is, you create variables then use them in a keybind for example (see below)
mod1 = "mod1" #alt key
filemanager = "thunar"
@@ -69,6 +70,7 @@ def auto_sticky_windows(window):
and info['name'] == 'Picture-in-Picture'):
sticky_windows.append(window)
# █▄▀ █▀▀ █▄█ █▄▄ █ █▄░█ █▀▄ █▀
# █░█ ██▄ ░█░ █▄█ █ █░▀█ █▄▀ ▄█
@@ -100,6 +102,7 @@ keys = [
Key([mod], "n", lazy.layout.normalize(), desc="Reset all window sizes"),
Key([mod], "f", lazy.window.toggle_fullscreen(), desc="Toggle focused window to fullscreen"),
Key([mod], "v", lazy.window.toggle_floating(), desc="Toggle focused window to floating"),
Key([mod1], "l", lazy.spawn("betterlockscreen -l"), desc="Lock the screen"),
# Toggle between split and unsplit sides of stack.
# Split = all windows displayed
# Unsplit = 1 window displayed, like Max layout, but still with
@@ -230,7 +233,6 @@ def open_btop():
def open_pavucontrol():
qtile.cmd_spawn("pavucontrol")
# █▄▄ ▄▀█ █▀█
# █▄█ █▀█ █▀▄
@@ -313,6 +315,17 @@ screens = [
background = '#52548D',
),
Spotify(),
widget.Image(
filename = '~/.config/qtile/Assets/5.png',
),
widget.Image(
filename = '~/.config/qtile/Assets/1.png',
background = '#52548D',
),
widget.CPU(
font = "IBM Plex Mono Medium",
format='CPU:({load_percent:.1f}%/{freq_current}GHz)',
@@ -521,6 +534,17 @@ screens = [
background = '#52548D',
),
Spotify(),
widget.Image(
filename = '~/.config/qtile/Assets/5.png',
),
widget.Image(
filename = '~/.config/qtile/Assets/1.png',
background = '#52548D',
),
widget.CPU(
font = "IBM Plex Mono Medium",
format='CPU:({load_percent:.1f}%/{freq_current}GHz)',

204
spotify.py Normal file
View File

@@ -0,0 +1,204 @@
# The MIT License (MIT)
# Copyright(c) 2022 Ben Hunt
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from subprocess import CompletedProcess, run
from typing import List
from libqtile.group import _Group
from libqtile.config import Screen
from libqtile.widget import base
from libqtile.log_utils import logger
SPOTIFY = "Spotify"
class Spotify(base.ThreadPoolText):
"""
A widget to interact with spotify via dbus.
"""
defaults = [
("play_icon", "", "icon to display when playing music"),
("pause_icon", "", "icon to display when music paused"),
("update_interval", 0.5, "polling rate in seconds"),
("format", "{icon} {artist}: {album} - {track}", "Spotify display format"),
]
def __init__(self, **config) -> None:
# init base class
super().__init__(text="", **config)
self.add_defaults(Spotify.defaults)
self.add_callbacks(
{
"Button1": self.toggle_music,
}
)
def _is_proc_running(self, proc_name: str) -> bool:
# create regex pattern to search for to avoid similar named processes
pattern = f"{proc_name}$"
# pgrep will return a string of pids for matching processes
cmd = ["pgrep", "-fli", pattern]
proc_out = run(cmd, capture_output=True).stdout.decode(
"utf-8"
)
return proc_out != ""
def toggle_between_groups(self) -> None:
"""
remember which group you were on before you switched to spotify
so you can toggle between the 2 groups
"""
current_screen: Screen = self.qtile.current_screen
current_group_info = self.qtile.current_group.info()
logger.warning(f"current group info: {current_group_info}")
windows = current_group_info["windows"]
if SPOTIFY in windows:
# go to previous group
logger.warning("going to previous group")
current_screen.group.get_previous_group().toscreen()
logger.warning("went to previous group")
else:
self.go_to_spotify()
def go_to_spotify(self) -> None:
"""
Switch to whichever group has the current spotify instance
if none exists then we will spawn an instance on the current group
"""
# spawn spotify if not already running
if not self._is_proc_running("spotify"):
self.qtile.spawn("spotify", shell=True)
return
all_groups: List[_Group] = self.qtile.groups
# we need to find the group that has spotify in it
for group in all_groups:
info = group.info()
# get a list of windows for the group. We look for 'Spotify here'
windows = info["windows"]
if SPOTIFY in windows:
name = group.name
# switch to 'name' group
spotify_group = self.qtile.groups_map[name]
spotify_group.toscreen()
break
def poll(self) -> str: # type: ignore
"""Poll content for the text box"""
vars = {
"icon": self.play_icon if self.playing else self.pause_icon,
"artist": self.artist,
"track": self.song_title,
"album": self.album,
}
return self.format.format(**vars) # type: ignore
def toggle_music(self) -> None:
cmd = """
dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause
"""
run(cmd, shell=True)
def get_proc_output(self, proc: CompletedProcess) -> str:
stdout = proc.stdout.decode("utf-8")
no_spotify = "Error" in stdout
return (
""
if no_spotify
else stdout.rstrip()
)
@property
def _meta(self) -> str:
cmd = """dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 \
org.freedesktop.DBus.Properties.Get \
string:'org.mpris.MediaPlayer2.Player' \
string:'Metadata'
"""
proc = run( cmd, shell=True, capture_output=True)
output: str = proc.stdout.decode("utf-8").replace("'", "ʼ").rstrip()
return "" if ("org.mpris.MediaPlayer2.spotify" in output) else output
@property
def artist(self) -> str:
cmd ="""
dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 \
org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' \
string:'Metadata' | grep -m1 'xesam:artist' -b2 | tail -n 1 | grep -o '\".*\"' | \
sed 's/\"//g' | sed -e 's/&/and/g'
"""
proc: CompletedProcess = run(cmd,
shell=True,
capture_output=True,
)
return self.get_proc_output(proc)
@property
def song_title(self) -> str:
cmd = f"""
echo '{self._meta}' | grep -m1 'xesam:title' -b1 | tail -n1 | grep -o '\".*\"' | \
sed 's/\"//g' | sed -e 's/&/and/g'
"""
proc: CompletedProcess = run(cmd,
shell=True,
capture_output=True
)
return self.get_proc_output(proc)
@property
def album(self) -> str:
cmd = f"""
echo '{self._meta}' | grep -m1 'xesam:album' -b1 | tail -n1 | grep -o '\".*\"' | \
sed 's/\"//g' | sed -e 's/&/and/g'
"""
proc = run(cmd,
shell=True,
capture_output=True,
)
return self.get_proc_output(proc)
@property
def playing(self) -> bool:
cmd = """
dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify \
/org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get \
string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus' | grep -o Playing
"""
play = run(cmd,
shell=True,
capture_output=True,
).stdout.decode("utf-8")
return play != ""