commit 601c0b5cc55d709a10bcd4a6a024684830f8c317 Author: stabosa87 Date: Mon Jun 8 02:40:00 2026 -0500 INIT! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9287c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +history.txt \ No newline at end of file diff --git a/config/.stow-local-ignore b/config/.stow-local-ignore new file mode 100644 index 0000000..597cd34 --- /dev/null +++ b/config/.stow-local-ignore @@ -0,0 +1 @@ +nushell/history.txt \ No newline at end of file diff --git a/config/kitty/kitty.conf b/config/kitty/kitty.conf new file mode 100644 index 0000000..bfaa626 --- /dev/null +++ b/config/kitty/kitty.conf @@ -0,0 +1 @@ +paste_actions filter diff --git a/config/mako/config b/config/mako/config new file mode 100644 index 0000000..acbec7f --- /dev/null +++ b/config/mako/config @@ -0,0 +1,10 @@ +background-color=#000000 +text-color=#FFFFFF +border-size=0 +border-radius=0 +padding=10 +margin=10 +default-timeout=5000 +anchor=top-right +width=300 +height=100 diff --git a/config/nushell/config.nu b/config/nushell/config.nu new file mode 100644 index 0000000..20dc2de --- /dev/null +++ b/config/nushell/config.nu @@ -0,0 +1,30 @@ +$env.PROMPT_COMMAND = { + let user = (whoami) + let host = (sys host | get hostname) + let cwd = ($env.PWD | path expand | str replace $env.HOME "~") + + $"(ansi white)($user)@($host)\n(ansi white)($cwd)(ansi reset)" +} + +$env.PROMPT_INDICATOR = { $"(ansi white)$ (ansi reset)" } +$env.PROMPT_INDICATOR_VI_INSERT = { $"(ansi white)$ (ansi reset)" } +$env.PROMPT_INDICATOR_VI_NORMAL = { $"(ansi white)$ (ansi reset)" } +$env.LANG = "en_US.UTF-8" +$env.LC_ALL = "en_US.UTF-8" +$env.LC_CTYPE = "en_US.UTF-8" +$env.config.color_config = { + text: light_white + shape_keyword: light_white + shape_string: light_green + shape_int: light_white + shape_float: light_white + shape_bool: light_yellow + shape_operator: light_white + shape_variable: light_white + shape_flag: light_white + shape_external: light_white + shape_path: light_white + shape_error: light_red +} +$env.config.show_banner = false +$env.PROMPT_COMMAND_RIGHT = { "" } diff --git a/config/nushell/env.nu b/config/nushell/env.nu new file mode 100644 index 0000000..349bee3 --- /dev/null +++ b/config/nushell/env.nu @@ -0,0 +1,33 @@ +# env.nu +# +# Installed by: +# version = "0.109.1" +# +# Previously, environment variables were typically configured in `env.nu`. +# In general, most configuration can and should be performed in `config.nu` +# or one of the autoload directories. +# +# This file is generated for backwards compatibility for now. +# It is loaded before config.nu and login.nu +# +# See https://www.nushell.sh/book/configuration.html +# +# Also see `help config env` for more options. +# +# You can remove these comments if you want or leave +# them for future reference. +let base_xdg = [ + "/usr/local/share" + "/usr/share" +] + +let flatpak_xdg = [ + "/var/lib/flatpak/exports/share" + ($env.HOME | path join ".local/share/flatpak/exports/share") +] + +$env.XDG_DATA_DIRS = ($base_xdg | append $flatpak_xdg | str join ":") + +$env.PYENV_ROOT = ($env.HOME | path join ".pyenv") +$env.PATH = ($env.PATH | split row (char env_sep) | prepend $'($env.PYENV_ROOT)/bin' | prepend $'($env.PYENV_ROOT)/shims') +alias sway = dbus-run-session sway diff --git a/config/sway/config b/config/sway/config new file mode 100755 index 0000000..ed0bc1c --- /dev/null +++ b/config/sway/config @@ -0,0 +1,6 @@ +set $base ~/.config/sway + +xwayland enable +include ~/.config/mute/monitors.conf + +exec_always sh -c 'BASE_SWAY="$base" /usr/bin/python $base/main/boot.py' \ No newline at end of file diff --git a/config/sway/main/boot.py b/config/sway/main/boot.py new file mode 100644 index 0000000..68b2143 --- /dev/null +++ b/config/sway/main/boot.py @@ -0,0 +1,50 @@ +import os +import subprocess +from swayipc import Swayipc + + +def get_base(): + return os.environ.get("BASE_SWAY", os.path.expanduser("~/.config/sway")) + + +Logs = "/tmp/sway_boot.log" + + +def apply_safe_config(sway): + try: + sway.cmd("floating_modifier Mod4") + sway.cmd("bindsym Mod4+Shift+grave reload") + sway.cmd("bindsym Mod4+Shift+escape exit") + sway.cmd("bindsym Mod4+q exec kitty") + sway.cmd("bindsym Mod4+c kill") + sway.cmd("exec notify-send 'Sway FUCKED!' 'Fallback active'") + sway.cmd('for_window [app_id="xpad"] floating enable') + except Exception: + print("truely fucked") + pass + + +def boot(base): + base = os.path.expanduser(base) if base else "" + + with open(Logs, "w") as f: + return subprocess.run( + ["python3", os.path.join(base, "main/swayconfig.py")], + stdout=f, + stderr=f + ).returncode + + +def main(): + sway = Swayipc() + base = get_base() + + rc = boot(base) + + if rc != 0: + apply_safe_config(sway) + subprocess.Popen(["xpad", "-f", Logs]) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config/sway/main/moves.py b/config/sway/main/moves.py new file mode 100644 index 0000000..ffdd0cc --- /dev/null +++ b/config/sway/main/moves.py @@ -0,0 +1,198 @@ +import subprocess +import json +import sys +import os +import time +import tempfile +import base64 +from datetime import datetime +from pathlib import Path + +Recording_flag = "/tmp/screenshot_active" +Recordings = Path.home() / "records" +Tmp = tempfile.gettempdir() + +def rand12(): + return base64.urlsafe_b64encode(os.urandom(9)).decode()[:12] + + +def run(cmd): + return subprocess.run(cmd) + +def find_focused_pid(node): + if isinstance(node, dict): + if node.get("focused") is True: + return node.get("pid") + + for key in ("nodes", "floating_nodes"): + for child in node.get(key, []): + pid = find_focused_pid(child) + if pid: + return pid + + elif isinstance(node, list): + for item in node: + pid = find_focused_pid(item) + if pid: + return pid + + return None + +def force_kill(): + tree = json.loads(subprocess.check_output( + ["swaymsg", "-t", "get_tree"], + text=True + )) + pid = find_focused_pid(tree) + + if not pid: + print("No focused window PID found") + return + + os.kill(pid, 9) + +def is_recording(): + return subprocess.run( + ["pgrep", "-x", "wf-recorder"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ).returncode == 0 + + +def record(fullscreen=False): + Recordings.mkdir(parents=True, exist_ok=True) + + if is_recording(): + subprocess.run(["killall", "wf-recorder"]) + try: + os.remove(Recording_flag) + except FileNotFoundError: + pass + return + + Path(Recording_flag).write_text("1") + + slurp_cmd = ["slurp"] + if fullscreen: + slurp_cmd.append("-o") + + selection = subprocess.getoutput(" ".join(slurp_cmd)) + + if not selection.strip(): + return + + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + output = Recordings / f"{timestamp}.mp4" + + proc = subprocess.Popen( + ["wf-recorder", "-g", selection, "-f", str(output)] + ) + proc.wait() + + if not output.exists(): + return + + compressed = output.with_name(output.stem + "_comp720p.mp4") + + subprocess.run([ + "ffmpeg", + "-i", str(output), + "-vf", "scale=-2:720", + "-vcodec", "libx264", + "-crf", "32", + "-preset", "fast", + str(compressed) + ]) + + try: + output.unlink() + except FileNotFoundError: + pass + +def resolve_output(out): + if not out: + out = Tmp + + out = os.path.expanduser(out) + + if os.path.isdir(out) or out.endswith("/"): + os.makedirs(out, exist_ok=True) + return os.path.join(out, f"{rand12()}.png") + + parent = os.path.dirname(out) + if parent: + os.makedirs(parent, exist_ok=True) + + if not out.endswith(".png"): + out += ".png" + + return out + + +def take_screenshot(output_path): + Path(Recording_flag).write_text("1") + + try: + time.sleep(0.2) + + freeze = subprocess.Popen(["wayfreeze"]) + time.sleep(0.1) + + try: + region = subprocess.check_output(["slurp"]).decode().strip() + + subprocess.run( + ["grim", "-g", region, output_path], + check=True + ) + + except subprocess.CalledProcessError: + return False + + finally: + freeze.terminate() + + if os.path.exists(output_path): + with open(output_path, "rb") as f: + subprocess.run( + ["wl-copy", "--type", "image/png"], + input=f.read() + ) + + subprocess.run(["notify-send", "screenshot", f"Saved: {output_path}"]) + return True + + finally: + try: + os.remove(Recording_flag) + except FileNotFoundError: + pass + + return False + +def main(): + if len(sys.argv) < 2: + sys.exit(1) + + cmd = sys.argv[1] + + if cmd == "kill": + force_kill() + + elif cmd == "record": + fullscreen = "--fullscreen" in sys.argv + record(fullscreen) + + elif cmd == "screenshot": + out = None + + if "--out" in sys.argv: + idx = sys.argv.index("--out") + if idx + 1 < len(sys.argv): + out = sys.argv[idx + 1] + + output = resolve_output(out) + take_screenshot(output) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config/sway/main/sway_status.py b/config/sway/main/sway_status.py new file mode 100755 index 0000000..d3070a1 --- /dev/null +++ b/config/sway/main/sway_status.py @@ -0,0 +1,156 @@ +import os +import time +import json +import socket +import struct +import subprocess +import urllib.request +from pathlib import Path + + +vpncache = Path("/tmp/vpn_country.cache") +screenshotflag = Path("/tmp/screenshot_active") + + +class Cached: + def __init__(self, ttl): + self.ttl = ttl + self.last = 0 + self.value = "" + + def get(self, fn): + now = time.time() + if now - self.last >= self.ttl: + try: + self.value = fn() + except Exception: + self.value = "" + self.last = now + return self.value + + +def http_get(url, timeout=2): + try: + with urllib.request.urlopen(url, timeout=timeout) as r: + return r.read().decode().strip() + except Exception: + return "" + + +class StatusBar: + def __init__(self): + self.cpu = Cached(5) + self.bat = Cached(5) + self.vpn = Cached(60) + self.rec = Cached(1) + + def run(self, cmd): + return subprocess.run(cmd, capture_output=True, text=True).stdout.strip() + + def get_volume(self): + return self.run(["pamixer", "--get-volume-human"]) + + def get_ram(self): + out = self.run(["free", "-m"]).splitlines() + mem = next((l for l in out if l.startswith("Mem:")), "") + parts = mem.split() + if len(parts) < 7: + return "?" + total = int(parts[1]) + used = int(parts[2]) + return f"{used//1000}GB/{total//1000}GB" + + def get_cpu(self): + def compute(): + try: + out = self.run(["grep", "cpu ", "/proc/stat"]) + p = out.split() + if len(p) < 8: + return "0.0%" + user, nice, system, idle = map(int, p[1:5]) + total = user + nice + system + idle + busy = total - idle + return f"{(busy / total) * 100:.1f}%" + except Exception: + return "0.0%" + + return self.cpu.get(compute) + + def get_battery(self): + def compute(): + bat = Path("/sys/class/power_supply/BAT0") + if not bat.exists(): + return "" + try: + cap = (bat / "capacity").read_text().strip() + status = (bat / "status").read_text().strip() + return f"{cap}% ({status})" + except Exception: + return "" + + return self.bat.get(compute) + + def get_vpn(self): + def compute(): + if not self.run(["wg", "show", "interfaces"]): + if vpncache.exists(): + vpncache.unlink() + return "" + + country = "" + if vpncache.exists(): + country = vpncache.read_text().strip() + + if not country: + ip = http_get("https://ifconfig.me") + geo = http_get(f"https://iplookup.stab.ing/api/v1/lookup?ip={ip}") + + if '"country"' in geo: + try: + country = geo.split('"country":"')[1].split('"')[0] + except Exception: + country = "" + + if country: + vpncache.write_text(country) + + return f"VPN: {country or 'Unknown'}" + + return self.vpn.get(compute) + + def get_recording(self): + def compute(): + return "RECORDING!" if self.run(["pgrep", "-x", "wf-recorder"]) else "" + + return self.rec.get(compute) + + def get_time(self): + if screenshotflag.exists(): + return "REDACTED" + return self.run(["date", "+%Y-%m-%d %I:%M %p"]) + + def render(self): + vol = self.get_volume() + ram = self.get_ram() + cpu = self.get_cpu() + bat = self.get_battery() + vpn = self.get_vpn() + rec = self.get_recording() + t = self.get_time() + + extra = " / ".join([x for x in [rec, vpn, bat] if x]) + + if extra: + return f"CPU: {cpu} / RAM: {ram} / VOL: {vol} / {extra} / {t}" + return f"CPU: {cpu} / RAM: {ram} / VOL: {vol} / {t}" + + +def main(): + bar = StatusBar() + while True: + print(bar.render(), flush=True) + time.sleep(0.1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config/sway/main/swayconfig.py b/config/sway/main/swayconfig.py new file mode 100755 index 0000000..79b490f --- /dev/null +++ b/config/sway/main/swayconfig.py @@ -0,0 +1,163 @@ +import os +import subprocess +import sys +from swayipc import Swayipc +import swayworkspace +from pathlib import Path + +def get_base(): + return os.environ.get("BASE_SWAY") + +def run_sh(c): + subprocess.Popen(c, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def apply_ui_and_environment(sway): + base = os.path.expanduser(get_base()) + + sway.cmd("floating_modifier Mod4") + sway.cmd("default_border pixel 0") + + sway.cmd("client.focused #ffffff #ffffff #ffffff #ffffff") + sway.cmd("client.focused_inactive #000000 #000000 #000000 #000000") + sway.cmd("client.unfocused #000000 #000000 #000000 #000000") + sway.cmd("client.urgent #ffffff #ffffff #ffffff #ffffff") + sway.cmd("client.placeholder #000000 #000000 #000000 #000000") + + sway.cmd('for_window [app_id="xpad"] floating enable') + + sway.cmd("exec_always gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'") + sway.cmd("exec_always gsettings set org.gnome.desktop.interface gtk-theme 'MonoThemeDark'") + sway.cmd("exec_always gsettings set org.gnome.desktop.interface.wm.preferences theme 'MonoThemeDark'") + sway.cmd("exec_always gsettings set org.gnome.desktop.interface cursor-size '24'") + sway.cmd("exec_always gsettings set org.gnome.desktop.interface font-name 'Noto Sans Mono 10'") + sway.cmd("exec_always gsettings set org.gnome.desktop.interface.desktop-app-info-default-handler 'librewolf.desktop'") + + run_sh("/usr/lib/xdg-desktop-portal -r") + os.environ["GTK_USE_PORTAL"] = "0" + os.environ["XDG_CURRENT_DESKTOP"] = "sway" + os.environ["XDG_SESSION_TYPE"] = "wayland" + os.environ["XDG_PORTAL_BACKEND"] = "wlroots" + + run_sh(f"swaybg -i {base}/wallpaper/metrowaymaka.png -m fill") + run_sh("blueman-applet") + run_sh("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") + swayworkspace.SwaySome().init(1) + run_sh("dbus-update-activation-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP SWAYSOCK") + +def bind_keys(sway): + base = get_base() + mod = "Mod4" + + binding_actions = {} + script_path = os.path.abspath(__file__) + + def assign_key(key_combo): + def decorator(func): + result = func() + sway.cmd(f"bindsym {key_combo} {result}") + return func + return decorator + + @assign_key(f"{mod}+h") + def focus_left(): return "focus left" + + @assign_key(f"{mod}+j") + def focus_down(): return "focus down" + + @assign_key(f"{mod}+k") + def focus_up(): return "focus up" + + @assign_key(f"{mod}+l") + def focus_right(): return "focus right" + + @assign_key(f"{mod}+Shift+h") + def move_left(): return "move left" + + @assign_key(f"{mod}+Shift+j") + def move_down(): return "move down" + + @assign_key(f"{mod}+Shift+k") + def move_up(): return "move up" + + @assign_key(f"{mod}+Shift+l") + def move_right(): return "move right" + + @assign_key(f"{mod}+bracketleft") + def workspace_prev(): return "workspace prev" + + @assign_key(f"{mod}+bracketright") + def workspace_next(): return "workspace next" + + @assign_key(f"{mod}+Q") + def open_terminal(): return "exec kitty" + + @assign_key(f"{mod}+c") + def kill_window(): return "kill" + + @assign_key(f"{mod}+Shift+s") + def take_screenshot(): return f"exec python3 {base}/main/moves.py screenshot --out ~/screenshots/" + + @assign_key(f"{mod}+Shift+escape") + def exit_sway(): return "exit" + + @assign_key(f"{mod}+Up") + def volume_up(): return "exec pamixer --increase 5" + + @assign_key(f"{mod}+Down") + def volume_down(): return "exec pamixer --decrease 5" + + @assign_key(f"{mod}+Shift+grave") + def reload_sway(): return "reload" + + @assign_key(f"{mod}+Shift+f") + def record_screen(): return f"exec python {base}/main/moves.py record --fullscreen" + + @assign_key(f"{mod}+r") + def open_menu(): return "exec wofi --show drun --width 400 --height 500 --layer overlay --location 1" + + @assign_key("Ctrl+Shift+Alt+Mod4+L") + def open_linkedin(): return "exec flatpak run io.github.ungoogled_software.ungoogled_chromium https://www.linkedin.com" + + @assign_key(f"{mod}+Shift+c") + def force_kill(): return f"exec python {base}/main/moves.py kill" + + @assign_key(f"{mod}+n") + def open_notes(): return "exec xpad" + + @assign_key(f"{mod}+v") + def toggle_floating(): return "floating toggle" + + @assign_key("XF86AudioPlay") + def media_play(): return "exec playerctl play-pause" + + @assign_key("XF86AudioPause") + def media_pause(): return "exec playerctl play-pause" + + for i in range(1, 11): + num = 0 if i == 10 else i + sway.cmd(f"bindsym {mod}+{num} exec python {base}/main/swayworkspace.py focus {i}") + sway.cmd(f"bindsym {mod}+Shift+{num} exec python {base}/main/swayworkspace.py move {i}") + + return binding_actions + +def build_bar(sway): + base = os.path.expanduser(get_base()) + + sway.cmd("bar bar-main position top") + sway.cmd(f"bar bar-main status_command python3 {base}/main/sway_status.py") + sway.cmd("bar bar-main font pango:Noto Sans Mono 10") + sway.cmd("bar bar-main mode dock") + + sway.cmd("bar bar-main colors background #000000") + sway.cmd("bar bar-main colors statusline #ffffff") + sway.cmd("bar bar-main colors separator #ffffff") + sway.cmd("bar bar-main colors focused_workspace #000000 #ffffff #000000") + sway.cmd("bar bar-main colors active_workspace #000000 #ffffff #000000") + sway.cmd("bar bar-main colors inactive_workspace #000000 #000000 #ffffff") + sway.cmd("bar bar-main colors urgent_workspace #ffffff #ffffff #ffffff") + +if __name__ == "__main__": + sway = Swayipc() + apply_ui_and_environment(sway) + bind_keys(sway) + build_bar(sway) diff --git a/config/sway/main/swayipc.py b/config/sway/main/swayipc.py new file mode 100644 index 0000000..39c6f1b --- /dev/null +++ b/config/sway/main/swayipc.py @@ -0,0 +1,92 @@ +#sway socket working for working the socket for sending commands +import json +import os +import socket +import struct +import sys +from dataclasses import dataclass + +ipcmag = b"i3-ipc" + +@dataclass +class Output: + name: str + focused: bool = False + active: bool = False + +@dataclass +class Workspace: + num: int + output: str + visible: bool = False + +class Swayipc: + def __init__(self): + self.socket = None + sock_path = None + for var in ("SWAYSOCK", "I3SOCK"): + sock_path = os.environ.get(var) + if sock_path: + break + + if not sock_path: + raise RuntimeError("No socket found!") + + self._connect(sock_path) + + def _connect(self, path): + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.socket.connect(path) + + def _send(self, cmd_type: int, payload: str): + data = payload.encode() + header = ( + ipcmag + + struct.pack("=I", len(data)) + + struct.pack("=I", cmd_type) + ) + self.socket.sendall(header + data) + + def _read(self): + header = b"" + while len(header) < 14: + chunk = self.socket.recv(14 - len(header)) + if not chunk: + raise RuntimeError("Socket closed") + header += chunk + + if header[:6] != ipcmag: + raise RuntimeError("Bad ipc response") + + length = struct.unpack("=I", header[6:10])[0] + + payload = b"" + while len(payload) < length: + chunk = self.socket.recv(length - len(payload)) + if not chunk: + raise RuntimeError("Socket closed during message") + payload += chunk + + return payload.decode() + + def cmd(self, command_string): + try: + self._send(0, command_string) + resp = json.loads(self._read()) + if not resp[0].get("success", False): + print(f"Sway ipc error (skipped): '{command_string}' -> {resp[0].get('error')}", file=sys.stderr) + except Exception as e: + print(f"Exception execute command '{command_string}': {e}", file=sys.stderr) + + def outputs(self): + self._send(3, "") + data = json.loads(self._read()) + return [ + Output(name=o["name"], focused=o.get("focused", False), active=o.get("active", False)) + for o in data if o.get("active", False) + ] + + def workspaces(self): + self._send(1, "") + data = json.loads(self._read()) + return [Workspace(num=w["num"], output=w["output"], visible=w.get("visible", False)) for w in data] \ No newline at end of file diff --git a/config/sway/main/swayworkspace.py b/config/sway/main/swayworkspace.py new file mode 100755 index 0000000..bd09b2f --- /dev/null +++ b/config/sway/main/swayworkspace.py @@ -0,0 +1,81 @@ +# implementation of swaysome in python +import sys +from dataclasses import dataclass +from swayipc import Swayipc, Output, Workspace +import pathlib +class SwaySome(Swayipc): + def __init__(self): + super().__init__() + + def _cmd(self, cmd_str): + self.cmd(cmd_str) + + def ordered_outputs(self): + outs = self.outputs() + outs.sort(key=lambda o: o.name) + return outs + + def current_workspace(self): + outs = self.outputs() + wss = self.workspaces() + + focused_output = next((o.name for o in outs if o.focused), None) + if not focused_output: + raise RuntimeError("No focused output") + + for ws in wss: + if ws.visible and ws.output == focused_output: + return ws.num + + raise RuntimeError("No active workspace found") + + def group_base(self): + return (self.current_workspace() // 10) * 10 + + def resolve(self, n: int): + return self.group_base() + n + + def focus(self, n: int): + self._cmd(f"workspace number {self.resolve(n)}") + + def move(self, n: int): + self._cmd(f"move container to workspace number {self.resolve(n)}") + + def focus_abs(self, n: int): + self._cmd(f"workspace number {n}") + + def move_abs(self, n: int): + self._cmd(f"move container to workspace number {n}") + + def init(self, n: int): + outputs = self.ordered_outputs() + current = next((o.name for o in outputs if o.focused), None) + + for i, out in enumerate(outputs): + target = ((i + 1) * 10) + n + self._cmd(f"focus output {out.name}") + self._cmd(f"workspace number {target}") + + if current: + self._cmd(f"focus output {current}") + +def main(): + cmd = sys.argv[1] + n = int(sys.argv[2]) + + sway = SwaySome() + + if cmd == "focus": + sway.focus(n) + elif cmd == "move": + sway.move(n) + elif cmd == "focus-abs": + sway.focus_abs(n) + elif cmd == "move-abs": + sway.move_abs(n) + else: + print(f"Unknown command: {cmd}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config/sway/wallpaper/metrowaymaka.png b/config/sway/wallpaper/metrowaymaka.png new file mode 100644 index 0000000..5d252c3 Binary files /dev/null and b/config/sway/wallpaper/metrowaymaka.png differ diff --git a/config/wofi/config b/config/wofi/config new file mode 100755 index 0000000..5465740 --- /dev/null +++ b/config/wofi/config @@ -0,0 +1,2 @@ +insensitive=true +no_actions=true diff --git a/config/wofi/style.css b/config/wofi/style.css new file mode 100755 index 0000000..b395178 --- /dev/null +++ b/config/wofi/style.css @@ -0,0 +1,19 @@ +* { + all: unset; + font-family: monospace; + font-size: 13px; +} + +window { + background-color: #000000; +} + +#input, #entry { + background-color: #000000; + color: #ffffff; +} + +#entry:selected { + background-color: #ffffff; + color: #000000; +} diff --git a/init/swaysetupmonitors.py b/init/swaysetupmonitors.py new file mode 100644 index 0000000..0260817 --- /dev/null +++ b/init/swaysetupmonitors.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +import json +import subprocess +from pathlib import Path + +out = Path("./mute/monitors.conf") + +DIRECT = { + "left": (-1, 0), + "right": (1, 0), + "above": (0, -1), + "below": (0, 1), + "primary": (0, 0) +} + +def get_sway_data(target_type): + cmd = ["swaymsg", "-t", f"get_{target_type}", "-r"] + return json.loads(subprocess.check_output(cmd, text=True)) + +def get_best_mode(output): + modes = output.get("modes", []) + if not modes: + return (1920, 1080) + + for m in modes: + if m.get("preferred"): + return m["width"], m["height"] + + best = max(modes, key=lambda m: m["width"] * m["height"]) + return best["width"], best["height"] + +def ask(prompt, default=None): + prompt_str = f"{prompt} [{default}]: " if default else f"{prompt}: " + val = input(prompt_str).strip() + return val if val else default + +def compute_positions(monitors, layout, outputs): + positions = {} + output_map = {o["name"]: o for o in outputs} + + primary_name = next((name for name, dist in layout.items() if dist == "primary"), None) + if not primary_name and monitors: + primary_name = monitors[0]["name"] + layout[primary_name] = "primary" + + logical_sizes = {} + + for m in monitors: + name = m["name"] + + try: + res_w, res_h = map(int, m["res"].lower().split("x")) + except ValueError: + res_w, res_h = get_best_mode(output_map[name]) + + scale = float(m["scale"]) + + logical_sizes[name] = { + "width": res_w / scale, + "height": res_h / scale + } + + for m in monitors: + name = m["name"] + direction = layout[name] + + if direction == "primary": + positions[name] = (0, 0) + continue + + dx, dy = DIRECT.get(direction, (1, 0)) + + ref_w = logical_sizes[name]["width"] if dx < 0 else logical_sizes[primary_name]["width"] + ref_h = logical_sizes[name]["height"] if dy < 0 else logical_sizes[primary_name]["height"] + + positions[name] = (int(ref_w * dx), int(ref_h * dy)) + + return positions + +def build_config(monitors, positions): + lines = ["# generated config\n"] + + for m in monitors: + name = m["name"] + mode = f"{m['res']}@{m['refresh']}" if m["refresh"] else m["res"] + x, y = positions[name] + + lines.append( + f"output {name} mode {mode}\n" + f"output {name} scale {m['scale']}\n" + f"output {name} position {x} {y}\n" + ) + + return "\n".join(lines) + +def main(): + Path("mute").mkdir(exist_ok=True) + + outputs = get_sway_data("outputs") + workspaces = get_sway_data("workspaces") + + monitors = [] + layout_map = {} + + for o in outputs: + name = o["name"] + ws = [str(w["name"]) for w in workspaces if w.get("output") == name] + + w, h = get_best_mode(o) + best_res = f"{w}x{h}" + + print(f"\nMONITOR: {name}\nWORKSPACES: {ws}") + + layout_map[name] = ask( + "Position relative to primary (primary/left/right/above/below)", + "right" + ) + + res_input = ask("Resolution (optional press enter for best)", "") + + monitors.append({ + "name": name, + "res": res_input if res_input else best_res, + "refresh": ask("Refresh rate (optional)", ""), + "scale": ask("Scale", "1") + }) + + positions = compute_positions(monitors, layout_map, outputs) + out.write_text(build_config(monitors, positions)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..ea0b2de --- /dev/null +++ b/justfile @@ -0,0 +1,29 @@ +default: + just --list + +init-arch: install-aur install-arch stow + +install-arch: + @echo "installing pkgs" + sudo pacman -S --needed --noconfirm - < packages-arch + @echo "Done. Moving onto aur..." + yay -S --noconfirm wayfreeze + +install-aur: + sudo pacman -S --noconfirm debugedit fakeroot go make gcc + git clone https://aur.archlinux.org/yay.git /tmp/yay + cd /tmp/yay && makepkg -si + +stow: + stow -t ~/.config config + @echo "Stoweed" + +unstow: + stow -D -t ~/.config config + +setup-displays: + python init/swaysetupmonitors.py + mv mute ~/.config/ + +restow: + stow -R -t ~/.config config \ No newline at end of file diff --git a/packages-arch b/packages-arch new file mode 100644 index 0000000..d3f6b7d --- /dev/null +++ b/packages-arch @@ -0,0 +1,32 @@ +sway +kitty +pamixer +grim +slurp +wl-clipboard +wf-recorder +gnome-keyring +glib2 +curl +jq +procps-ng +darkman +xpad +thunar-archive-plugin +thunar +noto-fonts +blueman +python-pyqt5 +xdg-desktop-portal-gnome +xdg-desktop-portal-wlr +base-devel +git +dbus-python +wofi +nushell +swaybg +stow +python +just +git +vim \ No newline at end of file