diff --git a/flake.nix b/flake.nix index 27c5237..2d6c595 100644 --- a/flake.nix +++ b/flake.nix @@ -12,9 +12,6 @@ ecsls.url = "github:Sigmapitech-meta/ecsls"; - qtile.url = "github:Sigmanificient/QtileConfig"; - qtile.inputs.dotfiles.follows = "/"; - home-manager = { url = "github:nix-community/home-manager/release-23.05"; inputs.nixpkgs.follows = "nixpkgs"; diff --git a/home/qtile/src/autostart.sh b/home/qtile/src/autostart.sh new file mode 100755 index 0000000..e9cd4e7 --- /dev/null +++ b/home/qtile/src/autostart.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +[[ "$(hostname)" =~ 'Sigmachine' ]] \ + && xrandr --output HDMI-0 --right-of DP-1 --primary + +notify-send "Welcome, $USER!" & diff --git a/home/qtile/src/config.py b/home/qtile/src/config.py new file mode 100644 index 0000000..c2fee90 --- /dev/null +++ b/home/qtile/src/config.py @@ -0,0 +1,47 @@ +from core import autostart, floating_layout, groups, keys, layouts, mouse, screens +from widgets import widget_defaults + +extension_defaults = widget_defaults.copy() + +dgroups_key_binder = None +dgroups_app_rules = [] +follow_mouse_focus = True +bring_front_click = False +cursor_warp = False + +auto_fullscreen = True +focus_on_window_activation = "smart" +reconfigure_screens = True + +# If things like steam games want to auto-minimize themselves when losing +# focus, should we respect this or not? +auto_minimize = False + +# XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this +# string besides java UI toolkits; you can see several discussions on the +# mailing lists, GitHub issues, and other WM documentation that suggest setting +# this string if your java app doesn't work correctly. We may as well just lie +# and say that we're a working one by default. +# +# We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in +# java that happens to be on java's whitelist. +wmname = "LG3D" + +__all__ = ( + # Hooks + "autostart", + # Keybindings + "keys", + # Mouse + "mouse", + # Workspaces groups + "groups", + # Layouts + "layouts", + "floating_layout", + # Screens + "screens", + # Widgets + "widget_defaults", + "extension_defaults", +) diff --git a/home/qtile/src/core/__init__.py b/home/qtile/src/core/__init__.py new file mode 100644 index 0000000..d4403a1 --- /dev/null +++ b/home/qtile/src/core/__init__.py @@ -0,0 +1,23 @@ +from .groups import groups +from .hooks import autostart +from .keys import keys, mod +from .layouts import floating_layout, layouts +from .mouse import mouse +from .screens import screens + +__all__ = ( + # Keybindings + "keys", + "mod", + # Hooks + "autostart", + # Mouse + "mouse", + # Workspaces groups + "groups", + # Layouts + "layouts", + "floating_layout", + # Screens + "screens", +) diff --git a/home/qtile/src/core/bar.py b/home/qtile/src/core/bar.py new file mode 100644 index 0000000..e9ec4db --- /dev/null +++ b/home/qtile/src/core/bar.py @@ -0,0 +1,59 @@ +import os + +from libqtile import bar, widget + +from utils import Color +from widgets import ( + Battery, + Clock, + CPUGraph, + GroupBox, + Memory, + Prompt, + QuickExit, + Separator, + TaskList, + Wakatime, +) + + +class Bar(bar.Bar): + instance_count: int = 0 + + widgets_checks = { + Battery: lambda _: os.uname().nodename == "Bacon", + } + + _widgets = [ + GroupBox, + Separator, + TaskList, + Separator, + Prompt, + Wakatime, + Battery, + Memory, + CPUGraph, + Separator, + widget.Volume, + Clock, + Separator, + QuickExit, + ] + + def __init__(self, id_): + self.id = id_ + + super().__init__( + widgets=self._build_widgets(), + size=24, + background=Color.BG_DARK.with_alpha(0.7), + margin=[0, 0, 8, 0], + ) + + def _build_widgets(self): + return [ + widget_builder() + for widget_builder in self._widgets + if self.widgets_checks.get(widget_builder, bool)(self) + ] diff --git a/home/qtile/src/core/groups.py b/home/qtile/src/core/groups.py new file mode 100644 index 0000000..aa5ba6c --- /dev/null +++ b/home/qtile/src/core/groups.py @@ -0,0 +1,62 @@ +from libqtile.config import DropDown, Group, Key, ScratchPad +from libqtile.lazy import lazy + +from .keys import keys, mod + +groups = [Group(f"{i}") for i in "ζδωχλξπσς"] +group_keys = [ + "ampersand", + "eacute", + "quotedbl", + "apostrophe", + "parenleft", + "minus", + "egrave", + "underscore", + "agrave", +] + +for g, key in zip(groups, group_keys): + keys.extend( + [ + # mod1 + letter of group = switch to group + Key( + [mod], + key, + lazy.group[g.name].toscreen(), + desc="Switch to group {}".format(g.name), + ), + Key( + [mod, "shift"], + key, + lazy.window.togroup(g.name, switch_group=True), + desc="Switch to & move focused window to group {}".format( + g.name + ), + ), + Key( + [mod], + "space", + lazy.group["scratchpad"].dropdown_toggle("term"), + ), + ] + ) + + +groups.append( + ScratchPad( + "scratchpad", + [ + DropDown( + "term", + "kitty", + x=0.05, + y=0.05, + opacity=0.95, + height=0.9, + width=0.9, + on_focus_lost_hide=False, + ) + ], + ) +) diff --git a/home/qtile/src/core/hooks.py b/home/qtile/src/core/hooks.py new file mode 100644 index 0000000..316d324 --- /dev/null +++ b/home/qtile/src/core/hooks.py @@ -0,0 +1,13 @@ +import pathlib +import os +import subprocess + +from libqtile import hook + + +@hook.subscribe.startup_once +def autostart(): + cwd = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + autostart_path = str((cwd / ".." / "autostart.sh").absolute()) + + subprocess.call([autostart_path]) diff --git a/home/qtile/src/core/keys.py b/home/qtile/src/core/keys.py new file mode 100644 index 0000000..8c204fb --- /dev/null +++ b/home/qtile/src/core/keys.py @@ -0,0 +1,88 @@ +from libqtile.config import Key +from libqtile.lazy import lazy + +mod = "mod4" + +keys = [ + Key([mod], "e", lazy.spawn("thunar")), + Key([mod], "v", lazy.spawn("kitty -e pulsemixer")), + Key([mod], "h", lazy.spawn("kitty -e nmtui")), + Key([mod, "shift"], "v", lazy.spawn("pavucontrol")), + Key([mod], "l", lazy.spawn("betterlockscreen -l")), + Key([mod], "f", lazy.window.toggle_floating(), desc="Toggle floating"), + Key([mod], "b", lazy.spawn("firefox")), + Key([], "Print", lazy.spawn("flameshot gui --clipboard")), + Key( + [mod], + "space", + lazy.layout.next(), + desc="Move window focus to other window", + ), + Key( + [mod, "shift"], + "h", + lazy.layout.shuffle_left(), + desc="Move window to the left", + ), + Key([mod], "n", lazy.layout.normalize(), desc="Reset all window sizes"), + Key( + [mod, "shift"], + "Return", + lazy.layout.toggle_split(), + desc="Toggle between split and unsplit sides of stack", + ), + Key([mod], "Return", lazy.spawn("kitty"), desc="Launch terminal"), + Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"), + Key([mod], "w", lazy.window.kill(), desc="Kill focused window"), + Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"), + Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"), + Key( + [mod], + "r", + lazy.spawncmd(), + desc="Spawn a command using a prompt widget", + ), + # Backlight + Key([], "XF86MonBrightnessUp", lazy.spawn("brightnessctl set +5%")), + Key([], "XF86MonBrightnessDown", lazy.spawn("brightnessctl set 5%-")), + # Volume + Key([], "XF86AudioMute", lazy.spawn("pamixer --toggle-mute")), + Key([], "XF86AudioLowerVolume", lazy.spawn("pamixer --decrease 5")), + Key([], "XF86AudioRaiseVolume", lazy.spawn("pamixer --increase 5")), + Key( + [mod, "control"], + "Right", + lazy.layout.grow_right(), + lazy.layout.grow(), + lazy.layout.increase_ratio(), + lazy.layout.delete(), + desc="Increase active window size.", + ), + Key( + [mod, "control"], + "Left", + lazy.layout.grow_left(), + lazy.layout.shrink(), + lazy.layout.decrease_ratio(), + lazy.layout.add(), + desc="Decrease active window size.", + ), + Key( + [mod, "control"], + "Up", + lazy.layout.grow_up(), + lazy.layout.shrink(), + lazy.layout.decrease_ratio(), + lazy.layout.add(), + desc="Decrease active window size.", + ), + Key( + [mod, "control"], + "Down", + lazy.layout.grow_down(), + lazy.layout.shrink(), + lazy.layout.decrease_ratio(), + lazy.layout.add(), + desc="Decrease active window size.", + ), +] diff --git a/home/qtile/src/core/layouts.py b/home/qtile/src/core/layouts.py new file mode 100644 index 0000000..7d2d634 --- /dev/null +++ b/home/qtile/src/core/layouts.py @@ -0,0 +1,31 @@ +from libqtile import layout +from libqtile.config import Match + +from utils import Color + +layouts = [ + layout.Columns( + border_width=2, + margin=4, + border_focus=Color.BLUE_DARK, + border_normal=Color.BG_DARK, + ) +] + +floating_layout = layout.Floating( + border_width=2, + border_focus=Color.BLUE_DARK, + border_normal=Color.BG_DARK, + float_rules=[ + *layout.Floating.default_float_rules, + Match(wm_class="pavucontrol"), # gitk + Match(wm_class="confirmreset"), # gitk + Match(wm_class="makebranch"), # gitk + Match(wm_class="maketag"), # gitk + Match(wm_class="ssh-askpass"), # ssh-askpass + Match(title="branchdialog"), # gitk + Match(title="pinentry"), # GPG key password entry + Match(title="splash"), # .Idea loader + Match(title="kitty"), + ], +) diff --git a/home/qtile/src/core/mouse.py b/home/qtile/src/core/mouse.py new file mode 100644 index 0000000..a85d9c2 --- /dev/null +++ b/home/qtile/src/core/mouse.py @@ -0,0 +1,24 @@ +from libqtile.config import Click, Drag +from libqtile.lazy import lazy + +from core import mod + +mouse = [ + Drag( + [mod], + "Button1", + lazy.window.set_position_floating(), + start=lazy.window.get_position(), + ), + Drag( + [mod], + "Button3", + lazy.window.set_size_floating(), + start=lazy.window.get_size(), + ), + Click( + [mod], "Button3", + lazy.spawn("jgmenu_run"), + ), + Click([mod], "Button2", lazy.window.bring_to_front()), +] diff --git a/home/qtile/src/core/screens.py b/home/qtile/src/core/screens.py new file mode 100644 index 0000000..2ce08a4 --- /dev/null +++ b/home/qtile/src/core/screens.py @@ -0,0 +1,28 @@ +import pathlib +import os + +from libqtile.bar import Gap +from libqtile.config import Screen + +from core.bar import Bar + +gap = Gap(4) + +_cwd = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + +wallpaper_path = os.path.expanduser("~/assets/wallpaper.png") + +if not os.path.exists(wallpaper_path): + wallpaper_path = str((_cwd / ".." / ".." / "wallpaper.png").absolute()) + +screens = [ + Screen( + top=Bar(i), + bottom=gap, + left=gap, + right=gap, + wallpaper=wallpaper_path, + wallpaper_mode="fill", + ) + for i in range(2) +] diff --git a/home/qtile/src/utils/__init__.py b/home/qtile/src/utils/__init__.py new file mode 100644 index 0000000..54f1c74 --- /dev/null +++ b/home/qtile/src/utils/__init__.py @@ -0,0 +1,3 @@ +from .colors import Color + +__all__ = ("Color",) diff --git a/home/qtile/src/utils/colors.py b/home/qtile/src/utils/colors.py new file mode 100644 index 0000000..90a00cd --- /dev/null +++ b/home/qtile/src/utils/colors.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +AlphaColor = str + + +class _Color(AlphaColor): + def with_alpha(self, alpha: float) -> AlphaColor: + return AlphaColor(f"{self}{int(alpha * 255):02x}") + + +class Color(AlphaColor): + BG_DARK = _Color("#0F0F1C") + BG_LIGHT = _Color("#1A1C31") + BG_LIGHTER = _Color("#22263F") + + RED_DARK = _Color("#D22942") + RED_LIGHT = _Color("#DE4259") + + GREEN_DARK = _Color("#17B67C") + GREEN_LIGHT = _Color("#3FD7A0") + + YELLOW_DARK = _Color("#F2A174") + YELLOW_LIGHT = _Color("#EED49F") + + BLUE_VERY_DARK = _Color("#3F3D9E") + BLUE_DARK = _Color("#8B8AF1") + BLUE_LIGHT = _Color("#A7A5FB") + + PURPLE_DARK = _Color("#D78AF1") + PURPLE_LIGHT = _Color("#E5A5FB") + + CYAN_DARK = _Color("#8ADEF1") + CYAN_LIGHT = _Color("#A5EBFB") + + TEXT_INACTIVE = _Color("#292C39") + TEXT_DARK = _Color("#A2B1E8") + TEXT_LIGHT = _Color("#CAD3F5") diff --git a/home/qtile/src/widgets/__init__.py b/home/qtile/src/widgets/__init__.py new file mode 100644 index 0000000..2a51e8b --- /dev/null +++ b/home/qtile/src/widgets/__init__.py @@ -0,0 +1,27 @@ +from .defaults import widget_defaults +from .overides import ( + Battery, + Clock, + CPUGraph, + GroupBox, + Memory, + Prompt, + QuickExit, + Separator, + TaskList, +) +from .wakatime import Wakatime + +__all__ = ( + "widget_defaults", + "Battery", + "Clock", + "CPUGraph", + "GroupBox", + "Memory", + "Prompt", + "QuickExit", + "Separator", + "TaskList", + "Wakatime", +) diff --git a/home/qtile/src/widgets/defaults.py b/home/qtile/src/widgets/defaults.py new file mode 100644 index 0000000..99d218e --- /dev/null +++ b/home/qtile/src/widgets/defaults.py @@ -0,0 +1,9 @@ +from utils import Color + +widget_defaults = dict( + font="JetBrainsMono Nerd Font", + fontsize=12, + padding=12, + background=Color.BG_DARK.with_alpha(0.9), + foreground=Color.TEXT_LIGHT, +) diff --git a/home/qtile/src/widgets/overides.py b/home/qtile/src/widgets/overides.py new file mode 100644 index 0000000..f634018 --- /dev/null +++ b/home/qtile/src/widgets/overides.py @@ -0,0 +1,85 @@ +from functools import partialmethod + +from libqtile import widget +from libqtile.lazy import lazy + +from utils import Color + + +def mk_overrides(cls, **conf): + init_method = partialmethod(cls.__init__, **conf) + return type(cls.__name__, (cls,), {"__init__": init_method}) + + +Battery = mk_overrides( + widget.Battery, + format="⚡{percent:2.0%}", + background=Color.BG_DARK.with_alpha(0.7), + foreground=Color.TEXT_LIGHT, + low_background=Color.RED_DARK.with_alpha(0.7), + low_percentage=0.1, +) + +CPUGraph = mk_overrides( + widget.CPUGraph, type="line", line_width=1, border_width=0 +) + +GroupBox = mk_overrides( + widget.GroupBox, + highlight_method="line", + disable_drag=True, + other_screen_border=Color.BLUE_VERY_DARK, + other_current_screen_border=Color.BLUE_VERY_DARK, + this_screen_border=Color.BLUE_DARK, + this_current_screen_border=Color.BLUE_DARK, + block_highlight_text_color=Color.TEXT_LIGHT, + highlight_color=[Color.BG_LIGHT, Color.BG_LIGHT], + inactive=Color.TEXT_INACTIVE, + active=Color.TEXT_LIGHT, +) + +Memory = mk_overrides( + widget.Memory, + format="{MemUsed: .3f}Mb", + mouse_callbacks={ + "Button1": lazy.spawn( + "kitty" + " -o initial_window_width=1720" + " -o initial_window_height=860" + " -e btop" + ) + }, +) + +TaskList = mk_overrides( + widget.TaskList, + icon_size=0, + fontsize=12, + borderwidth=0, + margin=0, + padding=4, + txt_floating="", + highlight_method="block", + title_width_method="uniform", + spacing=8, + foreground=Color.TEXT_LIGHT, + background=Color.BG_DARK.with_alpha(0.8), + border=Color.BG_DARK.with_alpha(0.9), +) + +Separator = mk_overrides(widget.Spacer, length=4) +Clock = mk_overrides(widget.Clock, format="%A, %b %-d %H:%M") + + +QuickExit = mk_overrides( + widget.QuickExit, default_text="⏻", countdown_format="{}" +) + +Prompt = mk_overrides( + widget.Prompt, + prompt=">", + bell_style="visual", + background=Color.BG_DARK, + foreground=Color.TEXT_LIGHT, + padding=8, +) diff --git a/home/qtile/src/widgets/wakatime.py b/home/qtile/src/widgets/wakatime.py new file mode 100644 index 0000000..819c7f8 --- /dev/null +++ b/home/qtile/src/widgets/wakatime.py @@ -0,0 +1,59 @@ +"""Wakatime widget by Drawu.""" +import subprocess +import sys + +from libqtile import qtile +from libqtile.widget import base + + +class Wakatime(base.InLoopPollText): + def __init__(self, **config): + self.name = "Wakatime widget" + super().__init__( + update_interval=600, qtile=qtile, **config + ) + + self.default_string = "[:<]" + + def poll(self): + try: + proc = subprocess.Popen( + ["wakatime-cli", "--today"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + except FileNotFoundError: + return self.default_string + + stdout, stderr = proc.communicate() + if stderr is not None: + return self.default_string + + raw = stdout.decode("utf-8").strip() + + if raw is not None: + return self.process_string(raw) + + return self.default_string + + @staticmethod + def process_string(raw) -> str: + activities = raw.split(', ') + out = [] + + for ac in activities: + if ac.count(' ') == 2: + m, _, ac_name = ac.split(' ') + + elif ac.count(' ') == 4: + h, _, m, _, ac_name = ac.split(' ') + m = str(int(m) + (int(h) * 60)) + + else: + continue + + out.append(f"{ac_name[0]}:{m}") + + print("=>", ' '.join(out)) + return ' '.join(out) diff --git a/wallpaper.png b/wallpaper.png new file mode 100644 index 0000000..4a48ff7 Binary files /dev/null and b/wallpaper.png differ