diff --git a/config/assets/config.json b/config/assets/config.json index 2f0a891..a93e26d 100644 --- a/config/assets/config.json +++ b/config/assets/config.json @@ -1 +1 @@ -{"prefix_restart": "SHIFT ALT", "suffix_restart": "T", "prefix_bluetooth": "ALT", "suffix_bluetooth": "B", "prefix_emoji": "SUPER", "suffix_emoji": "E", "prefix_cliphist": "SUPER", "suffix_cliphist": "V", "prefix_todo": "SUPER", "suffix_todo": "T", "prefix_sh": "SUPER", "suffix_sh": "S", "prefix_walls": "SUPER", "suffix_walls": "W", "prefix_screenshotregion": "SUPER", "suffix_screenshotregion": "Z", "prefix_screenshot": "", "suffix_screenshot": "Print", "prefix_screencast": "SUPER", "suffix_screencast": "R", "prefix_launcher": "SUPER", "suffix_launcher": "D", "prefix_power": "SUPER", "suffix_power": "X", "prefix_toggle": "SUPER", "suffix_toggle": "H", "prefix_randomwallpaper": "ALT SHIFT", "suffix_randomwallpaper": "W", "wallpapers_dir": "/home/sankalp/Pictures/wallpapers"} \ No newline at end of file +{"prefix_restart": "SHIFT ALT", "suffix_restart": "T", "prefix_bluetooth": "ALT", "suffix_bluetooth": "B", "prefix_emoji": "SUPER", "suffix_emoji": "E", "prefix_cliphist": "SUPER", "suffix_cliphist": "V", "prefix_todo": "SUPER", "suffix_todo": "T", "prefix_sh": "SUPER", "suffix_sh": "S", "prefix_walls": "SUPER", "suffix_walls": "W", "prefix_screenshotregion": "SUPER", "suffix_screenshotregion": "Z", "prefix_screenshot": "", "suffix_screenshot": "Print", "prefix_screencast": "SUPER", "suffix_screencast": "R", "prefix_launcher": "SUPER", "suffix_launcher": "D", "prefix_power": "SUPER", "suffix_power": "X", "prefix_wifi": "SUPER", "suffix_wifi": "N", "prefix_toggle": "SUPER", "suffix_toggle": "H", "prefix_randomwallpaper": "ALT SHIFT", "suffix_randomwallpaper": "W", "wallpapers_dir": "/home/sankalp/Pictures/wallpapers"} \ No newline at end of file diff --git a/config/assets/launcher.json b/config/assets/modus.json similarity index 68% rename from config/assets/launcher.json rename to config/assets/modus.json index c2cfc5b..12e8657 100644 --- a/config/assets/launcher.json +++ b/config/assets/modus.json @@ -9,5 +9,16 @@ ":pw": "launcher.open(\"power\")", ":nw": "launcher.open(\"wifi\")", ":cl": "launcher.open(\"calendar\")" - } + }, + "pinned_apps": [ + "kitty", + "code", + "materialgram", + "vesktop", + "spotify", + "libreoffice", + "wasistlos", + "pavucontrol", + "zen-browser" + ] } diff --git a/config/assets/settings.json b/config/assets/settings.json index 65e8d2b..d11f7c8 100644 --- a/config/assets/settings.json +++ b/config/assets/settings.json @@ -2,4 +2,4 @@ "color-scheme": "dark", "custom-color": "none", "generation-scheme": "tonalSpot" -} +} \ No newline at end of file diff --git a/config/config.py b/config/config.py index 46cd4c7..c280ec1 100644 --- a/config/config.py +++ b/config/config.py @@ -117,7 +117,7 @@ def copy_wallpapers(): subprocess.run( [ "python", - os.path.expanduser("~/Modus/config/wallpaper.py"), + os.path.expanduser("~/Modus/config/scripts/wallpaper.py"), "-I", os.path.join(dest_wallpaper_dir, "example-1.jpg"), ] @@ -150,7 +150,7 @@ exec-once = wl-paste --type text --watch cliphist store exec-once = wl-paste --type image --watch cliphist store $fabricSend = fabric-cli exec modus -$scriptsDir = $HOME/Modus/config +$scriptsDir = $HOME/Modus/config/scripts/ bind = {bind_vars["prefix_restart"]}, {bind_vars["suffix_restart"]}, exec, killall modus; python {home}/Modus/main.py # Reload Modus | Default: SUPER ALT + T bind = {bind_vars["prefix_bluetooth"]}, {bind_vars["suffix_bluetooth"]}, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth | Default: ALT + B diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf index cd7dc0d..2eee02b 100644 --- a/config/hypr/modus.conf +++ b/config/hypr/modus.conf @@ -5,10 +5,11 @@ exec-once = wl-paste --type text --watch cliphist store exec-once = wl-paste --type image --watch cliphist store $fabricSend = fabric-cli exec modus -$scriptsDir = $HOME/Modus/config +$scriptsDir = $HOME/Modus/config/scripts/ bind = SHIFT ALT, T, exec, killall modus; python /home/sankalp/Modus/main.py # Reload Modus | Default: SUPER ALT + T bind = ALT, B, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth | Default: ALT + B +bind = SUPER, N, exec, $fabricSend 'launcher.open("wifi")' # Wifi | Default: SUPER + N bind = SUPER, E, exec, $fabricSend 'launcher.open("emoji")' # Emoji | Default: SUPER + E bind = SUPER, V, exec, $fabricSend 'launcher.open("cliphist")' # Cliphist | Default: SUPER + V bind = SUPER, T, exec, $fabricSend 'launcher.open("todo")' # Todo | Default: SUPER + T @@ -20,7 +21,7 @@ bind = SUPER, X, exec, $fabricSend 'launcher.open("power")' # Power Menu | Defau bind = SUPER, H, exec, $fabricSend 'bar.toggle_hidden()' # Toggle Bar | Default: SUPER + H bind = SUPER, Z, exec, $fabricSend 'sc.screenshot()' # Screenshot Region | Default: SUPER + Z bind = , Print, exec, $fabricSend 'sc.screenshot(True)' # ScreenshotFullScreen | Default: Print -bind = SUPER, R, exec, $fabricSend 'sc.screencast()' # Screencast | Default: SUPER + R +bind = SUPER, R, exec, $fabricSend 'sc.screencast_start()' # Screencast | Default: SUPER + R # Wallpapers directory: /home/sankalp/Pictures/wallpapers diff --git a/config/material-colors/generate.py b/config/material-colors/generate.py index 32504e0..6fa3446 100644 --- a/config/material-colors/generate.py +++ b/config/material-colors/generate.py @@ -7,23 +7,22 @@ import argparse from materialyoucolor.quantize import QuantizeCelebi # type: ignore from materialyoucolor.score.score import Score # type: ignore from materialyoucolor.hct import Hct # type: ignore -from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors # type: ignore # noqa -from materialyoucolor.scheme.dynamic_scheme import DynamicScheme # type: ignore # noqa -from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot # type: ignore # noqa -from materialyoucolor.scheme.scheme_expressive import SchemeExpressive # type: ignore # noqa -from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad # type: ignore # noqa -from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome # type: ignore # noqa -from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow # type: ignore # noqa -from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant # type: ignore # noqa -from materialyoucolor.scheme.scheme_neutral import SchemeNeutral # type: ignore # noqa -from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity # type: ignore # noqa -from materialyoucolor.scheme.scheme_content import SchemeContent # type: ignore # noqa +from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors # type: ignore # noqa +from materialyoucolor.scheme.dynamic_scheme import DynamicScheme # type: ignore # noqa +from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot # type: ignore # noqa +from materialyoucolor.scheme.scheme_expressive import SchemeExpressive # type: ignore # noqa +from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad # type: ignore # noqa +from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome # type: ignore # noqa +from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow # type: ignore # noqa +from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant # type: ignore # noqa +from materialyoucolor.scheme.scheme_neutral import SchemeNeutral # type: ignore # noqa +from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity # type: ignore # noqa +from materialyoucolor.scheme.scheme_content import SchemeContent # type: ignore # noqa import hashlib import pickle import numpy as np import re - schemes = { "tonalSpot": SchemeTonalSpot, "expressive": SchemeExpressive, @@ -41,22 +40,21 @@ def join(*args): return os.path.join(*args) -home = os.path.expanduser('~') - +home = os.path.expanduser("~") script_dir = os.path.dirname(os.path.abspath(__file__)) cache_path = f"{home}/.cache/material/" os.makedirs(cache_path, exist_ok=True) def rgb_to_hex(rgb): - return '#{:02x}{:02x}{:02x}'.format(*rgb[:3]) + return "#{:02x}{:02x}{:02x}".format(*rgb[:3]) def rgba_to_rgb(rgba): - return f'{rgba[0]}, {rgba[1]}, {rgba[2]}' + return f"{rgba[0]}, {rgba[1]}, {rgba[2]}" -class Color(): +class Color: def __init__(self, scheme) -> None: self.scheme = scheme @@ -70,8 +68,12 @@ class Color(): class ColorsCache: def __init__( - self, colors: DynamicScheme | dict, wallpaper: str, - original_color: int, contrast_level: int, scheme_name: str + self, + colors: DynamicScheme | dict, + wallpaper: str, + original_color: int, + contrast_level: int, + scheme_name: str, ) -> None: self.colors: dict[str, str] = {} self.wallpaper = wallpaper @@ -90,14 +92,13 @@ class ColorsCache: def colors_dict(cache: ColorsCache): - dict = { + return { "wallpaper": cache.wallpaper, "colors": cache.colors, "original_color": cache.original_color, "contrast_level": cache.contrast_level, - "scheme_name": cache.scheme_name + "scheme_name": cache.scheme_name, } - return dict def get_cache_object(object: dict | str): @@ -108,10 +109,7 @@ def get_cache_object(object: dict | str): original_color = object["original_color"] contrast_level = object.get("contrast_level", 0) scheme_name = object.get("scheme_name", "tonalSpot") - return ColorsCache( - colors, wallpaper, original_color, - contrast_level, scheme_name - ) + return ColorsCache(colors, wallpaper, original_color, contrast_level, scheme_name) def get_file_list(folder_path): @@ -124,21 +122,18 @@ def get_file_list(folder_path): ready_templates = { "colors.css": "@define-color {name} {hex};\n", - "colors.scss": "${name}: {hex};\n" -} -additional = { - "onBackground": "foreground" + "colors.scss": "${name}: {hex};\n", } +additional = {"onBackground": "foreground"} -def generate_color_map(scheme: DynamicScheme): - color_map: dict[str, str] = {} - for color in vars(MaterialDynamicColors).keys(): - color_name = getattr(MaterialDynamicColors, color) - if hasattr(color_name, "get_hct"): - rgba = color_name.get_hct(scheme).to_rgba() - color_map[color.strip()] = rgb_to_hex(rgba) - return color_map +def generate_color_map(scheme): + colors = {} + for color_name in vars(MaterialDynamicColors).keys(): + color_obj = getattr(MaterialDynamicColors, color_name) + if hasattr(color_obj, "get_hct"): + colors[color_name] = rgb_to_hex(color_obj.get_hct(scheme).to_rgba()) + return colors class ColorFormatter: @@ -147,25 +142,23 @@ class ColorFormatter: def apply_transformations(self, value, transformations): intermediate_transforms = [ - t for t in transformations - if not (t.startswith("strip") or t == "rgb") + t for t in transformations if not (t.startswith("strip") or t == "rgb") ] final_transforms = [ - t for t in transformations - if t.startswith("strip") or t == "rgb" + t for t in transformations if t.startswith("strip") or t == "rgb" ] for transform in intermediate_transforms: if transform.startswith("lighten"): - percent = int(re.search(r'\d+', transform).group()) + percent = int(re.search(r"\d+", transform).group()) value = self.adjust_brightness(value, percent) elif transform.startswith("darken"): - percent = int(re.search(r'\d+', transform).group()) + percent = int(re.search(r"\d+", transform).group()) value = self.adjust_brightness(value, -percent) for transform in final_transforms: if transform.startswith("strip"): - value = value.lstrip('#') + value = value.lstrip("#") elif transform == "rgb": value = self.hex_to_rgb(value) @@ -175,24 +168,20 @@ class ColorFormatter: def min_max(v): return min(255, max(0, v)) - hex_color = hex_color.lstrip('#') - r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - + hex_color = hex_color.lstrip("#") + r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) r = min_max(int(r * (1 + factor / 100))) g = min_max(int(g * (1 + factor / 100))) b = min_max(int(b * (1 + factor / 100))) - - return f'#{r:02X}{g:02X}{b:02X}'.lower() + return f"#{r:02X}{g:02X}{b:02X}".lower() def hex_to_rgb(self, hex_color): - hex_color = hex_color.lstrip('#') - r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - return f'{r},{g},{b}' + hex_color = hex_color.lstrip("#") + r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + return f"{r},{g},{b}" def parse_transformations(self, transformations_str): - transformations = re.findall( - r'(\w+)(?:\((\d*)\))?', transformations_str - ) + transformations = re.findall(r"(\w+)(?:\((\d*)\))?", transformations_str) result = [] for command, arg in transformations: if command: @@ -203,7 +192,7 @@ class ColorFormatter: return result def format(self, text): - pattern = r'(<(\w+)(?:\.(.+))?>)' + pattern = r"(<(\w+)(?:\.(.+))?>)" matches = re.findall(pattern, text) result = [] last_end = 0 @@ -211,27 +200,28 @@ class ColorFormatter: for match in matches: full_match, key, transformations_str = match if key in self.color_map: - result.append(text[last_end:text.find(full_match, last_end)]) + result.append(text[last_end : text.find(full_match, last_end)]) value = self.color_map[key] if transformations_str: - transformations = self.parse_transformations( - transformations_str - ) + transformations = self.parse_transformations(transformations_str) value = self.apply_transformations(value, transformations) result.append(value) last_end = text.find(full_match, last_end) + len(full_match) result.append(text[last_end:]) - result = ''.join(result) - pattern = r'<\\\\([^>]+)>' - - result = re.sub(pattern, r'<\1>', result) - + result = "".join(result) + pattern = r"<\\\\([^>]+)>" + result = re.sub(pattern, r"<\1>", result) return result -def generate_templates(folder: str, output_folder: str, scheme: DynamicScheme, - color_scheme: str, wallpaper: str = ''): +def generate_templates( + folder: str, + output_folder: str, + scheme: DynamicScheme, + color_scheme: str, + wallpaper: str = "", +): if not os.path.exists(output_folder): os.makedirs(output_folder) if not os.path.exists(folder): @@ -244,13 +234,12 @@ def generate_templates(folder: str, output_folder: str, scheme: DynamicScheme, formatter = ColorFormatter(scheme) template = formatter.format(template) template = ( - template - .replace("", color_scheme) + template.replace("", color_scheme) .replace("", output_folder) .replace("", wallpaper) ) new_path = join(output_folder, os.path.basename(file_path)) - with open(new_path, 'w') as f: + with open(new_path, "w") as f: f.write(template) for file in ready_templates: @@ -262,21 +251,17 @@ def generate_templates(folder: str, output_folder: str, scheme: DynamicScheme, hex_color = rgb_to_hex(rgba) rgb_color = rgba_to_rgb(rgba) new_line = ready_templates[file].format( - name=color, - hex=hex_color, - rgb=rgb_color + name=color, hex=hex_color, rgb=rgb_color ) _template += new_line if color in additional: new_line = ready_templates[file].format( - name=additional[color], - hex=hex_color, - rgb=rgb_color + name=additional[color], hex=hex_color, rgb=rgb_color ) _template += new_line new_path = join(output_folder, os.path.basename(file)) - with open(new_path, 'w') as f: + with open(new_path, "w") as f: f.write(_template) @@ -286,10 +271,10 @@ def run_hooks(folder: str): file_list = get_file_list(folder) for file_path in file_list: file_name = os.path.basename(file_path) - if str(file_name).endswith(".py"): - subprocess.run(['python', '-O', file_path]) - elif str(file_name).endswith(".sh"): - subprocess.run(['sh', file_path]) + if file_name.endswith(".py"): + subprocess.run(["python", "-O", file_path]) + elif file_name.endswith(".sh"): + subprocess.run(["sh", file_path]) def process_image(image_path, quality=2, num_colors=128): @@ -297,81 +282,69 @@ def process_image(image_path, quality=2, num_colors=128): global cache_path _cache_path = join(cache_path, "cached_colors") hash_object = hashlib.md5(image_path.encode()) - cache_filename = hash_object.hexdigest() + '.pkl' + cache_filename = hash_object.hexdigest() + ".pkl" os.makedirs(_cache_path, exist_ok=True) return join(_cache_path, cache_filename) def load_from_cache(cache_path): if os.path.exists(cache_path): - with open(cache_path, 'rb') as f: + with open(cache_path, "rb") as f: return pickle.load(f) return None def save_to_cache(cache_path, data): - with open(cache_path, 'wb') as f: + with open(cache_path, "wb") as f: pickle.dump(data, f) - cache_path = get_cache_path(image_path) - - cached_result = load_from_cache(cache_path) + cache_file = get_cache_path(image_path) + cached_result = load_from_cache(cache_file) if cached_result is not None: return cached_result - image = Image.open(image_path).convert('RGB') - + image = Image.open(image_path).convert("RGB") image_data = np.array(image) pixel_array = image_data[::quality, ::quality].reshape(-1, 3) - result = QuantizeCelebi(pixel_array, num_colors) - color = Score.score(result)[0] - - save_to_cache(cache_path, color) - + save_to_cache(cache_file, color) return color def main( - color_scheme: str, image_path: str, use_color: int | None = None, - scheme_name: str | None = None, contrast_level: int = 0 + color_scheme: str, + image_path: str, + use_color: int | None = None, + scheme_name: str | None = None, + contrast_level: int = 0, ): scheme_name = scheme_name or "tonalSpot" scheme = schemes[scheme_name] is_dark = True if color_scheme == "dark" else False if use_color is None: color = process_image(image_path, 4, 1024) - else: color = use_color - scheme = scheme( - Hct.from_int(color), - is_dark, - contrast_level - ) + scheme = scheme(Hct.from_int(color), is_dark, contrast_level) # for color in vars(MaterialDynamicColors).keys(): # color_name = getattr(MaterialDynamicColors, color) # if hasattr(color_name, "get_hct"): # print(color, rgb_to_hex(color_name.get_hct(scheme).to_rgba())) - with open(join(cache_path, "colors.json"), 'w') as f: - object = ColorsCache( - scheme, image_path, color, contrast_level, scheme_name + with open(join(cache_path, "colors.json"), "w") as f: + cache_obj = ColorsCache( + generate_color_map(scheme), image_path, color, contrast_level, scheme_name ) - json.dump(colors_dict(object), f, indent=2) + json.dump(colors_dict(cache_obj), f, indent=2) generate_templates( f"{script_dir}/templates", cache_path, scheme, color_scheme, image_path ) - generate_templates( - f"{script_dir}/templates/svg", join(cache_path, "svg/"), scheme, - color_scheme, image_path - ) if is_dark: - subprocess.run(['sh', join(script_dir, 'gtk-material.sh')]) + subprocess.run(["sh", join(script_dir, "gtk-material.sh")]) else: - subprocess.run(['sh', join(script_dir, 'gtk-material.sh'), '--light']) + subprocess.run(["sh", join(script_dir, "gtk-material.sh"), "--light"]) run_hooks(join(script_dir, "hooks")) @@ -379,30 +352,29 @@ def main( def _argparse() -> tuple[bool, bool, str, str, str, str]: parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("-R", help="Restore previous colorscheme.", action="store_true") group.add_argument( - "-R", help="Restore previous colorscheme.", action='store_true' - ) - group.add_argument( - "-w", help="Use last used wallpaper for color generation.", - action='store_true' - ) - group.add_argument( - "--image", type=str, help="Image for color scheme generation" + "-w", help="Use last used wallpaper for color generation.", action="store_true" ) + group.add_argument("--image", type=str, help="Image for color scheme generation") group.add_argument( "--color", type=str, help="Main color for color scheme generation" ) parser.add_argument( - '--color-scheme', type=str, default='dark', choices=['dark', 'light'], - help="Color scheme variant" + "--color-scheme", + type=str, + default="dark", + choices=["dark", "light"], + help="Color scheme variant", ) parser.add_argument( - '--scheme', type=str, default="", - choices=list(schemes.keys()), help="Scheme for color generation" + "--scheme", + type=str, + default="", + choices=list(schemes.keys()), + help="Scheme for color generation", ) - args = parser.parse_args() - restore_palette = args.R use_last_used_wallpaper = args.w image = args.image @@ -410,16 +382,19 @@ def _argparse() -> tuple[bool, bool, str, str, str, str]: use_color = args.color scheme = args.scheme return ( - restore_palette, use_last_used_wallpaper, image, - color_scheme, use_color, scheme + restore_palette, + use_last_used_wallpaper, + image, + color_scheme, + use_color, + scheme, ) def _main(): - ( - restore_palette, use_last_used_wallpaper, image, - color_scheme, use_color, scheme - ) = _argparse() + restore_palette, use_last_used_wallpaper, image, color_scheme, use_color, scheme = ( + _argparse() + ) if not scheme: scheme = None if use_color is not None: @@ -427,15 +402,18 @@ def _main(): with open(join(cache_path, "colors.json")) as f: content = get_cache_object(f.read()) main( - color_scheme, image_path=content.wallpaper, - use_color=int(use_color.lstrip('#'), 16), scheme_name=scheme + color_scheme, + image_path=content.wallpaper, + use_color=int(use_color.lstrip("#"), 16), + scheme_name=scheme, ) except Exception: main( - color_scheme, image_path="/dev/null", - use_color=int(use_color.lstrip('#'), 16), scheme_name=scheme + color_scheme, + image_path="/dev/null", + use_color=int(use_color.lstrip("#"), 16), + scheme_name=scheme, ) - elif restore_palette: with open(join(cache_path, "colors.json")) as f: content = get_cache_object(f.read()) @@ -444,7 +422,7 @@ def _main(): image_path=content.wallpaper, use_color=content.original_color, scheme_name=scheme or content.scheme_name, - contrast_level=content.contrast_level + contrast_level=content.contrast_level, ) elif use_last_used_wallpaper: with open(join(cache_path, "colors.json")) as f: @@ -453,13 +431,12 @@ def _main(): color_scheme, image_path=content.wallpaper, scheme_name=scheme or content.scheme_name, - contrast_level=content.contrast_level + contrast_level=content.contrast_level, ) else: if os.path.isdir(image): files = [ - f for f in os.listdir(image) - if f.endswith(('.png', '.jpg', '.jpeg')) + f for f in os.listdir(image) if f.endswith((".png", ".jpg", ".jpeg")) ] if files: image = join(image, random.choice(files)) diff --git a/config/material-colors/templates/colors.tdesktop-theme b/config/material-colors/templates/colors.tdesktop-theme index 1dc6a05..7b4db51 100644 --- a/config/material-colors/templates/colors.tdesktop-theme +++ b/config/material-colors/templates/colors.tdesktop-theme @@ -392,7 +392,7 @@ msgFileOutBgSelected: ACCENT; // Outgoing selected audio file msgFile1Bg: #72b1df; // Blue shared links / files without image square thumbnail msgFile1BgDark: #5c9ece; // Blue shared files without image download circle background msgFile1BgOver: #5294c4; // Blue shared files without image download circle background with mouse over -msgFile1BgSelected: #5099d0; // Blue shared files without image download circle background if file is selected +msgFile1BgSelected: #50100d0; // Blue shared files without image download circle background if file is selected msgFile2Bg: #61b96e; // Green shared links / shared files without image square thumbnail msgFile2BgDark: #4da859; // Green shared files without image download circle background msgFile2BgOver: #44a050; // Green shared files without image download circle background with mouse over @@ -514,7 +514,7 @@ mediaviewFileSizeFg: ON_BACKGROUND_DARKER; // File size text in file rectan mediaviewFileRedCornerFg: #d55959; // Red file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .pdf) mediaviewFileYellowCornerFg: #e8a659; // Yellow file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .zip) mediaviewFileGreenCornerFg: #49a957; // Green file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .exe) -mediaviewFileBlueCornerFg: #599dcf; // Blue file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .dmg) +mediaviewFileBlueCornerFg: #5100dcf; // Blue file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .dmg) mediaviewFileExtFg: activeButtonFg; // File extension text in file thumbnail placeholder in file rectangle mediaviewMenuBg: BACKGROUND; // Context menu in Media Viewer background diff --git a/config/material-colors/templates/material-discord.scss b/config/material-colors/templates/material-discord.scss index 607ee99..f302ab7 100644 --- a/config/material-colors/templates/material-discord.scss +++ b/config/material-colors/templates/material-discord.scss @@ -1,14 +1,14 @@ @use "sass:color"; +@use "sass:math"; +@use "colors.scss" as *; @import url(https://capnkitten.github.io/BetterDiscord/Themes/Material-Discord/css/source.css); -@import "/colors.scss"; - $color-scheme: ; -$hue: round(color.channel($primary, "hue", $space: hsl)); -$saturation: round(color.channel($primary, "saturation", $space: hsl)); -$lightness: round(color.channel($primary, "lightness", $space: hsl)); +$hue: math.round(color.channel($primary, "hue", $space: hsl)); +$saturation: math.round(color.channel($primary, "saturation", $space: hsl)); +$lightness: math.round(color.channel($primary, "lightness", $space: hsl)); * { transition: color 0.2s ease-in-out; diff --git a/services/__init__.py b/services/__init__.py index b464ce6..3d7d12d 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -1,7 +1,7 @@ from fabric.audio import Audio from .brightness import Brightness from .screen_record import ScreenRecorder -from .wifi import * +from .network import * brightness = Brightness() diff --git a/services/mpris.py b/services/mpris.py new file mode 100644 index 0000000..9c5afb5 --- /dev/null +++ b/services/mpris.py @@ -0,0 +1,279 @@ +import contextlib + +import gi +from fabric.core.service import Property, Service, Signal +from fabric.utils import bulk_connect +from gi.repository import GLib # type: ignore +from loguru import logger + + +class PlayerctlImportError(ImportError): + """An error to raise when playerctl is not installed.""" + + def __init__(self, *args): + super().__init__( + "Playerctl is not installed, please install it first", + *args, + ) + + +try: + gi.require_version("Playerctl", "2.0") + from gi.repository import Playerctl + + # from gi.repository import Playerctl # type: ignore +except ValueError: + raise PlayerctlImportError + + +class MprisPlayer(Service): + """A service to manage a mpris player.""" + + @Signal + def exit(self, value: bool) -> bool: ... + + @Signal + def changed(self) -> None: ... + + def __init__( + self, + player: Playerctl.Player, + **kwargs, + ): + self._signal_connectors: dict = {} + self._player: Playerctl.Player = player + super().__init__(**kwargs) + for sn in ["playback-status", "loop-status", "shuffle", "volume", "seeked"]: + self._signal_connectors[sn] = self._player.connect( + sn, + lambda *args, sn=sn: self.notifier(sn, args), + ) + + self._signal_connectors["exit"] = self._player.connect( + "exit", + self.on_player_exit, + ) + self._signal_connectors["metadata"] = self._player.connect( + "metadata", + lambda *args: self.update_status(), + ) + GLib.idle_add(lambda *args: self.update_status_once()) + + def update_status(self): + # schedule each notifier asynchronously. + def notify_property(prop): + if self.get_property(prop) is not None: + self.notifier(prop) + + for prop in [ + "metadata", + "title", + "artist", + "arturl", + "length", + ]: + GLib.idle_add(lambda p=prop: (notify_property(p), False)) + for prop in [ + "can-seek", + "can-pause", + "can-shuffle", + "can-go-next", + "can-go-previous", + ]: + GLib.idle_add(lambda p=prop: (self.notifier(p), False)) + + def update_status_once(self): + # schedule notifier calls for each property + def notify_all(): + for prop in self.list_properties(): # type: ignore + self.notifier(prop.name) + return False + + GLib.idle_add(notify_all, priority=GLib.PRIORITY_DEFAULT_IDLE) + + def notifier(self, name: str, args=None): + def notify_and_emit(): + self.notify(name) + self.emit("changed") + return False + + GLib.idle_add(notify_and_emit, priority=GLib.PRIORITY_DEFAULT_IDLE) + + def on_player_exit(self, player): + for id in list(self._signal_connectors.values()): + with contextlib.suppress(Exception): + self._player.disconnect(id) + del self._signal_connectors + GLib.idle_add(lambda: (self.emit("exit", True), False)) + del self._player + + def toggle_shuffle(self): + if self.can_shuffle: + # schedule the shuffle toggle in the GLib idle loop + GLib.idle_add(lambda: (setattr(self, "shuffle", not self.shuffle), False)) + # else do nothing + + def play_pause(self): + if self.can_pause: + GLib.idle_add(lambda: (self._player.play_pause(), False)) + + def next(self): + if self.can_go_next: + GLib.idle_add(lambda: (self._player.next(), False)) + + def previous(self): + if self.can_go_previous: + GLib.idle_add(lambda: (self._player.previous(), False)) + + # Properties + @Property(str, "readable") + def player_name(self) -> int: + return self._player.get_property("player-name") # type: ignore + + @Property(int, "read-write", default_value=0) + def position(self) -> int: + return self._player.get_property("position") # type: ignore + + @position.setter + def position(self, new_pos: int): + self._player.set_position(new_pos) + + @Property(object, "readable") + def metadata(self) -> dict: + return self._player.get_property("metadata") # type: ignore + + @Property(str or None, "readable") + def arturl(self) -> str | None: + if "mpris:artUrl" in self.metadata.keys(): # type: ignore # noqa: SIM118 + return self.metadata["mpris:artUrl"] # type: ignore + return None + + @Property(str or None, "readable") + def length(self) -> str | None: + if "mpris:length" in self.metadata.keys(): # type: ignore # noqa: SIM118 + return self.metadata["mpris:length"] # type: ignore + return None + + @Property(str, "readable") + def artist(self) -> str: + artist = self._player.get_artist() # type: ignore + if isinstance(artist, (list, tuple)): + return ", ".join(artist) + return artist + + @Property(str, "readable") + def album(self) -> str: + return self._player.get_album() # type: ignore + + @Property(str, "readable") + def title(self): + return self._player.get_title() + + @Property(bool, "read-write", default_value=False) + def shuffle(self) -> bool: + return self._player.get_property("shuffle") # type: ignore + + @shuffle.setter + def shuffle(self, do_shuffle: bool): + self.notifier("shuffle") + return self._player.set_shuffle(do_shuffle) + + @Property(str, "readable") + def playback_status(self) -> str: + return { + Playerctl.PlaybackStatus.PAUSED: "paused", + Playerctl.PlaybackStatus.PLAYING: "playing", + Playerctl.PlaybackStatus.STOPPED: "stopped", + }.get(self._player.get_property("playback_status"), "unknown") # type: ignore + + @Property(str, "read-write") + def loop_status(self) -> str: + return { + Playerctl.LoopStatus.NONE: "none", + Playerctl.LoopStatus.TRACK: "track", + Playerctl.LoopStatus.PLAYLIST: "playlist", + }.get(self._player.get_property("loop_status"), "unknown") # type: ignore + + @loop_status.setter + def loop_status(self, status: str): + loop_status = { + "none": Playerctl.LoopStatus.NONE, + "track": Playerctl.LoopStatus.TRACK, + "playlist": Playerctl.LoopStatus.PLAYLIST, + }.get(status) + self._player.set_loop_status(loop_status) if loop_status else None + + @Property(bool, "readable", default_value=False) + def can_go_next(self) -> bool: + return self._player.get_property("can_go_next") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_go_previous(self) -> bool: + return self._player.get_property("can_go_previous") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_seek(self) -> bool: + return self._player.get_property("can_seek") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_pause(self) -> bool: + return self._player.get_property("can_pause") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_shuffle(self) -> bool: + try: + self._player.set_shuffle(self._player.get_property("shuffle")) + return True + except Exception: + return False + + @Property(bool, "readable", default_value=False) + def can_loop(self) -> bool: + try: + self._player.set_shuffle(self._player.get_property("shuffle")) + return True + except Exception: + return False + + +class MprisPlayerManager(Service): + """A service to manage mpris players.""" + + @Signal + def player_appeared(self, player: Playerctl.Player) -> Playerctl.Player: ... + + @Signal + def player_vanished(self, player_name: str) -> str: ... + + def __init__( + self, + **kwargs, + ): + self._manager = Playerctl.PlayerManager.new() + bulk_connect( + self._manager, + { + "name-appeared": self.on_name_appeard, + "name-vanished": self.on_name_vanished, + }, + ) + self.add_players() + super().__init__(**kwargs) + + def on_name_appeard(self, manager, player_name: Playerctl.PlayerName): + logger.info(f"[MprisPlayer] {player_name.name} appeared") + new_player = Playerctl.Player.new_from_name(player_name) + manager.manage_player(new_player) + self.emit("player-appeared", new_player) # type: ignore + + def on_name_vanished(self, manager, player_name: Playerctl.PlayerName): + logger.info(f"[MprisPlayer] {player_name.name} vanished") + self.emit("player-vanished", player_name.name) # type: ignore + + def add_players(self): + for player in self._manager.get_property("player-names"): # type: ignore + self._manager.manage_player(Playerctl.Player.new_from_name(player)) # type: ignore + + @Property(object, "readable") + def players(self): + return self._manager.get_property("players") # type: ignore diff --git a/services/wifi.py b/services/network.py similarity index 100% rename from services/wifi.py rename to services/network.py