diff --git a/README.md b/README.md index 394d3818a..edc39ae38 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,24 @@ - First of all, Install the newest [Hyprland](https://hyprland.org/) using this [guide](https://wiki.hyprland.org/Getting-Started/Installation/) depend on your Distro: ```zsh - yay -S hyprland-git + sudo pacman -S hyprland hyprlock hypridle slurp cliphist wl-clipboard wofi xdg-desktop-portal-hyprland ``` +- For all dependencies, install in Manual [(Manual Build Hyperland)](https://wiki.hyprland.org/Getting-Started/Installation/) + ### Base setups 💻: - Install waybar, Rofi, Dunst, kitty terminal, swaybg, swaylock-fancy, swayidle, pamixer, light, Brillo: ``` -yay -S waybar-hyprland rofi dunst kitty swaybg swaylock-fancy-git swayidle pamixer light brillo +yay -S waybar rofi dunst kitty swaybg swaylock-fancy-git swayidle pamixer light brillo brightnessctl ``` ### Necessary Font 🔑: + ``` + sudo pacman -S noto-fonts noto-fonts-emoji noto-fonts-cjk noto-fonts-extra + ``` + - [JetBrains Mono Nerd Font](https://github.com/ryanoasis/nerd-fonts/releases/download/v2.2.2/JetBrainsMono.zip) @@ -59,9 +65,9 @@ fc-cache -fv ## Copy Files 💾 ``` -git clone -b late-night-🌃 https://github.com/iamverysimp1e/dots -cd dots -cp -r ./configs/* ~/.config/ + git clone -b late-night-🌃 https://github.com/OverAlexander-MR/Hyperland.git + cd Hyperland + cp -r ./configs/* ~/.config/ ``` > Finally, now you can login with Late Night Hyprland Rice diff --git a/configs/hypr/hypridle.conf b/configs/hypr/hypridle.conf new file mode 100644 index 000000000..94bab9cf6 --- /dev/null +++ b/configs/hypr/hypridle.conf @@ -0,0 +1,35 @@ +general { + # Comando para bloquear cuando se debe bloquear + lock_cmd = hyprlock + + # Antes de dormir: bloquear + before_sleep_cmd = loginctl lock-session + + # Después de despertar: encender DPMS + after_sleep_cmd = hyprctl dispatch dpms on + + # Asegura que no suspenda antes de que el lock se lance: + inhibit_sleep = 3 +} + +# Listeners para diferentes acciones por inactividad + +listener { + timeout = 600 # 10 minutos + on-timeout = loginctl lock-session # bloquear por inactividad + on-resume = hyprctl dispatch dpms on +} + +listener { + timeout = 600 # 10 minutos + # on-timeout = gtklock # bloquear por inactividad + on-timeout = hyprctl dispatch dpms off # apagar pantalla (DPMS) + on-resume = hyprctl dispatch dpms on # encender cuando vuelva actividad +} + +listener { + timeout = 1800 # 30 minutos + on-timeout = systemctl suspend # suspender el sistema + on-resume = hyprctl dispatch dpms on # encender cuando vuelva actividad +} + diff --git a/configs/hypr/hyprland.conf b/configs/hypr/hyprland.conf index 1016feff9..9e41ab95a 100755 --- a/configs/hypr/hyprland.conf +++ b/configs/hypr/hyprland.conf @@ -1,36 +1,75 @@ -######################################################################################## - __ __ _ _ _ -| \/ (_)_ __ (_)_ __ ___ __ _| | -| |\/| | | '_ \| | '_ ` _ \ / _` | | -| | | | | | | | | | | | | | (_| | | -|_| |_|_|_| |_|_|_| |_| |_|\__,_|_| - - _ _ _ _ ____ __ _ -| | | |_ _ _ __ _ __| | __ _ _ __ __| | / ___|___ _ __ / _(_) __ _ ___ -| |_| | | | | '_ \| '__| |/ _` | '_ \ / _` | | | / _ \| '_ \| |_| |/ _` / __| -| _ | |_| | |_) | | | | (_| | | | | (_| | | |__| (_) | | | | _| | (_| \__ \ -|_| |_|\__, | .__/|_| |_|\__,_|_| |_|\__,_| \____\___/|_| |_|_| |_|\__, |___/ - |___/|_| |___/ ######################################################################################### +# __ __ _ _ _ +#| \/ (_)_ __ (_)_ __ ___ __ _| | +#| |\/| | | '_ \| | '_ ` _ \ / _` | | +#| | | | | | | | | | | | | | (_| | | +#|_| |_|_|_| |_|_|_| |_| |_|\__,_|_| +# +# _ _ _ _ ____ __ _ +#| | | |_ _ _ __ _ __| | __ _ _ __ __| | / ___|___ _ __ / _(_) __ _ ___ +#| |_| | | | | '_ \| '__| |/ _` | '_ \ / _` | | | / _ \| '_ \| |_| |/ _` / __| +#| _ | |_| | |_) | | | | (_| | | | | (_| | | |__| (_) | | | | _| | (_| \__ \ +#|_| |_|\__, | .__/|_| |_|\__,_|_| |_|\__,_| \____\___/|_| |_|_| |_|\__, |___/ +# |___/|_| |___/ +########################################################################################## + +# Monitors + +# Monitor configuration via HDMI Manager +monitor = eDP-1,1920x1080@60.00,0x1200,1 +monitor = HDMI-A-1,1920x1200@59.95,0x0,1 + + +# Home 1 +# monitor=,1920x1200@60,0x0,1 +# monitor=eDP-1,1920x1080@60,0x1200,1 + +# Home 2 +# monitor=HDMI-A-1,1920x1200@60,0x0,1 +# monitor=eDP-1,1920x1080@60,0x1200,1 + + +# External + +# Mirror +# monitor=eDP-1,1920x1080@60,0x1080,1 +# monitor = HDMI-A-1, preferred, auto, 1, mirror, eDP-1 + -# You have to change this based on your monitor -monitor=eDP-1,1920x1080@60,0x0,1 # Status bar :) # exec-once=eww open bar -exec-once=waybar +exec-once= waybar +exec-once = hypridle #Notification exec-once=dunst # Wallpaper -exec-once=swaybg -o \* -i ~/.config/hypr/wallpapers/night.jpg -m fill +# exec-once=swaybg -o \* -i ~/.config/hypr/wallpapers/night.jpg -m fill +exec-once=swaybg -o \* -i ~/.config/hypr/wallpapers/mo.png -m fill + # For screen sharing -exec-once=dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP +#exec-once=dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP # For keyboard exec-once=fcitx5 -D + # For lockscreen -exec-once=swayidle -w timeout 200 'swaylock-fancy' +# exec-once=swayidle -w timeout 200 'swaylock-fancy' +# exec-once = gtklock --daemonize +# exec-once=swayidle -w timeout 200 'gtklock' + +# Almacena el historial de texto +exec-once = wl-paste --type text --watch cliphist store +# Almacena imágenes +exec-once = wl-paste --type image --watch cliphist store + +#Disk mount +exec-once = lxqt-policykit-agent + +# Deslizar 3 dedos horizontalmente para cambiar de workspace +gesture = 3, horizontal, workspace + # Start Page -exec-once=~/.config/hypr/scripts/startpage.sh +# exec-once=~/.config/hypr/scripts/startpage.sh # Bluetooth exec-once=blueman-applet # Make sure you have installed blueman @@ -38,67 +77,58 @@ exec-once=blueman-applet # Make sure you have installed blueman # Screen Sharing exec-once=systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP exec-once=~/.config/hypr/scripts/screensharing.sh +exec-once=~/.config/hypr/scripts/hdmi-manager/hdmi-watcher.sh + +input { + kb_layout = latam, us + kb_options = grp:ctrl_space_toggle + follow_mouse = 1 + force_no_accel = 0 + + touchpad { + natural_scroll = 1 + } + sensitivity = 0.5 - -input { - kb_layout = us - follow_mouse = 1 - - touchpad { - natural_scroll = no - } - - sensitivity = 0 - force_no_accel = 1 } -# See https://wiki.hyprland.org/Configuring/Keywords/#executing - -device { - name = epic mouse V1 - sensitivity = -0.5 -} - -# See https://wiki.hyprland.org/Configuring/Variables/ - -gestures { - workspace_swipe = true - workspace_swipe_fingers = 3 +cursor { + no_hardware_cursors = true } +# Set cursor +exec-once = hyprctl setcursor Adwaita 24 +#source = ~/.config/hypr/input.conf +# See https://wiki.hyprland.org/Configuring/Keywords/#executing +# device { +# name = epic mouse V1 +# sensitivity = -0.5 +# } +# general { layout=dwindle - sensitivity=1.0 # for mouse cursor +# sensitivity=1.0 # for mouse cursor gaps_in=5 - gaps_out=20 + gaps_out=10 border_size=2 col.active_border=0xff5e81ac col.inactive_border=0x66333333 - apply_sens_to_raw=0 # whether to apply the sensitivity to raw input (e.g. used by games where you aim using your mouse) + # apply_sens_to_raw=0 # whether to apply the sensitivity to raw input (e.g. used by games where you aim using your mouse) } decoration { rounding=18 blur { enabled=1 - size=6.8 # minimum 1 + size=6 # minimum 1 passes=2 # minimum 1, more passes = more resource intensive. new_optimizations = true - - # Your blur "amount" is size * passes, but high size (over around 5-ish) - # will produce artifacts. - # if you want heavy blur, you need to up the passes. - # the more passes, the more you can up the size without noticing artifacts. } - drop_shadow=true - shadow_range=15 - col.shadow=0xffa7caff - col.shadow_inactive=0x50000000 } # Blur for waybar @@ -115,61 +145,53 @@ animations { animation=border,1,10,default } -dwindle { - pseudotile=1 # enable pseudotiling on dwindle - # force_split=2 - force_split=0 - no_gaps_when_only = true -} +# dwindle { +# # pseudotile=1 # enable pseudotiling on dwindle +# # force_split=2 +# force_split=0 +# #no_gaps_when_only = true +# } master { new_on_top=true - no_gaps_when_only = true + #no_gaps_when_only = true } misc { disable_hyprland_logo=true disable_splash_rendering=true mouse_move_enables_dpms=true - vfr = false + #vfr = false } - -######################################################################################## - -\ \ / (_) | | | __ \ | | - \ \ /\ / / _ _ __ __| | _____ _____ | |__) | _| | ___ ___ - \ \/ \/ / | | '_ \ / _` |/ _ \ \ /\ / / __| | _ / | | | |/ _ \/ __| - \ /\ / | | | | | (_| | (_) \ V V /\__ \ | | \ \ |_| | | __/\__ \ - \/ \/ |_|_| |_|\__,_|\___/ \_/\_/ |___/ |_| \_\__,_|_|\___||___/ - -######################################################################################## - - +# sources +source = ~/.config/hypr/keybindings.conf +######################################################################################### +# +#\ \ / (_) | | | __ \ | | +# \ \ /\ / / _ _ __ __| | _____ _____ | |__) | _| | ___ ___ +# \ \/ \/ / | | '_ \ / _` |/ _ \ \ /\ / / __| | _ / | | | |/ _ \/ __| +# \ /\ / | | | | | (_| | (_) \ V V /\__ \ | | \ \ |_| | | __/\__ \ +# \/ \/ |_|_| |_|\__,_|\___/ \_/\_/ |___/ |_| \_\__,_|_|\___||___/ +# +######################################################################################### # Float Necessary Windows -windowrule=float,Rofi -windowrule=float,pavucontrol -windowrulev2 = float,class:^()$,title:^(Picture in picture)$ -windowrulev2 = float,class:^(brave)$,title:^(Save File)$ -windowrulev2 = float,class:^(brave)$,title:^(Open File)$ -windowrulev2 = float,class:^(LibreWolf)$,title:^(Picture-in-Picture)$ -windowrulev2 = float,class:^(blueman-manager)$ -windowrulev2 = float,class:^(org.twosheds.iwgtk)$ -windowrulev2 = float,class:^(blueberry.py)$ -windowrulev2 = float,class:^(xdg-desktop-portal-gtk)$ -windowrulev2 = float,class:^(geeqie)$ - -# Increase the opacity -windowrule=opacity 0.92,Thunar -windowrule=opacity 0.96,discord -windowrule=opacity 0.9,VSCodium -windowrule=opacity 0.88,obsidian - -^.*nvim.*$ -windowrule=tile,librewolf -windowrule=tile,spotify -windowrule=opacity 1,neovim +#windowrule=float,Rofi +#windowrule=float,pavucontrol +# windowrulev2 = float,class:^()$,title:^(Picture in picture)$ +# windowrulev2 = float,class:^(brave)$,title:^(Save File)$ +# windowrulev2 = float,class:^(brave)$,title:^(Open File)$ +# windowrulev2 = float,class:^(LibreWolf)$,title:^(Picture-in-Picture)$ +# windowrulev2 = float,class:^(blueman-manager)$ +# windowrulev2 = float,class:^(org.twosheds.iwgtk)$ +# windowrulev2 = float,class:^(blueberry.py)$ +# windowrulev2 = float,class:^(xdg-desktop-portal-gtk)$ +# windowrulev2 = float,class:^(geeqie)$ bindm=SUPER,mouse:272,movewindow bindm=SUPER,mouse:273,resizewindow - -#sources -source = ~/.config/hypr/keybindings.conf +# +# Defaul Workspaces +# workspace = 1, persistent:true +# workspace = 2, persistent:true +# workspace = 3, persistent:true +# workspace = 4, persistent:true +# workspace = 5, persistent:true diff --git a/configs/hypr/hyprlock.conf b/configs/hypr/hyprlock.conf new file mode 100644 index 000000000..771577b3d --- /dev/null +++ b/configs/hypr/hyprlock.conf @@ -0,0 +1,78 @@ +# _ _ _ +# | |__ _ _ _ __ _ __| | ___ ___| | __ +# | '_ \| | | | '_ \| '__| |/ _ \ / __| |/ / +# | | | | |_| | |_) | | | | (_) | (__| < +# |_| |_|\__, | .__/|_| |_|\___/ \___|_|\_\ +# |___/|_| + +general { + no_fade_in = false + grace = 0 + disable_loading = true +} + +background { + monitor = + path = /home/arch/.config/hypr/wallpapers/mo.png # supports png, jpg, webp (or 'screenshot') + color = rgba(25, 20, 20, 1.0) + blur_passes = 2 # 0 disables blurring + blur_size = 7 + noise = 0.0117 + contrast = 0.8916 + brightness = 0.8172 + vibrancy = 0.1696 + vibrancy_darkness = 0.0 +} + +input-field { + monitor = + size = 100, 50 + outline_thickness = 3 + dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8 + dots_spacing = 0.2 # Scale of dots' absolute size, 0.0 - 1.0 + dots_center = true + dots_rounding = -1 # -1 default circle, -2 adapter to input-field height + outer_color = rgb(151515) + inner_color = rgba(200, 200, 200, 0) + font_color = rgb(255, 255, 255) + fade_on_empty = true + fade_timeout = 10 # Milliseconds before fade_on_empty is triggered. + #placeholder_text = Input Password... # Text rendered in the input box when it's empty. + hide_input = false + rounding = -1 # -1 means complete rounding (circle/rect shape) + check_color = rgb(204, 136, 34) + fail_color = rgb(204, 34, 34) # if authentication failed, changes outer_color and fail message color + fail_text = $FAIL ($ATTEMPTS) # can be set to empty + fail_transition = 300 # transition time in ms between normal outer_color and fail_color + capslock_color = -1 + numlock_color = -1 + bothlock_color = -1 # when both locks are active. -1 means don't change outer_color (default) + invert_numlock = false # change color if numlock is off + swap_font_color = false # see below + + position = 0, -50 + halign = center + valign = center +} + +label { + monitor = + text = $TIME + color = rgba(200, 200, 200, 1.0) + font_size = 64 + font_family = Noto Sans + position = 0, 80 + halign = center + valign = center +} + +label { + monitor = + text = cmd[update:1000] echo "$(date +'%A, %d %B')" + color = rgba(200, 200, 200, 1.0) + font_size = 24 + font_family = Noto Sans + position = 0, 10 + halign = center + valign = center +} diff --git a/configs/hypr/keybindings.conf b/configs/hypr/keybindings.conf new file mode 100755 index 000000000..8cf46ca1e --- /dev/null +++ b/configs/hypr/keybindings.conf @@ -0,0 +1,89 @@ +############################################ +# ____ _ _ _ +# | _ \(_) | (_) +# | |_) |_ _ __ __| |_ _ __ __ _ ___ +# | _ <| | '_ \ / _` | | '_ \ / _` / __| +# | |_) | | | | | (_| | | | | | (_| \__ \ +# |____/|_|_| |_|\__,_|_|_| |_|\__, |___/ +# __/ | +# |___/ +# +############################################ + +# example binds +bind=SUPER,Q,killactive +# exit sesion +bind = SUPER,M,exit +# lock screen +# bind = SUPER,L,exec,loginctl lock-session +bind = SUPER,B,exec,loginctl lock-session +#bind=SUPER,B,exec,librewolf +bind=SUPER,F,fullscreen,1 +bind=SUPERSHIFT,F,fullscreen,0 +bind=SUPER,RETURN,exec,kitty +bind=SUPER,C,killactive +bind=SUPERSHIFT,Q,exit +bind=SUPER,E,exec,thunar +bind=SUPER,D,exec,rofi -show drun +bind = ,XF86PowerOff, exec, ~/.config/waybar/scripts/power-menu/powermenu.sh +# Historial del portapapeles +bind = SUPER, V, exec, cliphist list | wofi --dmenu | cliphist decode | wl-copy + + +# Monitors +bind = SUPER, P, exec, python3 ~/.config/hypr/scripts/hdmi-manager/hdmi-gui.py + +bind=,XF86AudioMute,exec,~/.config/hypr/scripts/volume mute +bind=,XF86AudioLowerVolume,exec,~/.config/hypr/scripts/volume down +bind=,XF86AudioRaiseVolume,exec,~/.config/hypr/scripts/volume up +bind=,XF86AudioMicMute,exec,pactl set-source-mute @DEFAULT_SOURCE@ toggle + +bind=,XF86MonBrightnessUp,exec,~/.config/hypr/scripts/brightness up # increase screen brightness +bind=,XF86MonBrightnessDown,exec,~/.config/hypr/scripts/brightness down # decrease screen brightnes + +bind=SUPERSHIFT,C,exec,bash ~/.config/hypr/scripts/hyprPicker.sh +bind=SUPERSHIFT,E,exec,wlogout +bind = SUPER, T, togglefloating, + +## Screen shot +bind=SUPER SHIFT, S, exec, sh -c 'file=~/Pictures/Capturas/$(date +%Y-%m-%d_%H-%M-%S).png; grim -g "$(slurp)" "$file" && wl-copy < "$file"' + + +bind=SUPER,j,movefocus,d +bind=SUPER,k,movefocus,u + +bind=SUPER,h,movefocus,l +bind=SUPER,l,movefocus,r + +bind=SUPER,left,resizeactive,-40 0 +bind=SUPER,right,resizeactive,40 0 + +bind=SUPER,up,resizeactive,0 -40 +bind=SUPER,down,resizeactive,0 40 + +bind=SUPERSHIFT,h,movewindow,l +bind=SUPERSHIFT,l,movewindow,r +bind=SUPERSHIFT,k,movewindow,u +bind=SUPERSHIFT,j,movewindow,d + +bind=SUPER,1,workspace,1 +bind=SUPER,2,workspace,2 +bind=SUPER,3,workspace,3 +bind=SUPER,4,workspace,4 +bind=SUPER,5,workspace,5 +bind=SUPER,6,workspace,6 +bind=SUPER,7,workspace,7 +bind=SUPER,8,workspace,8 +bind=SUPER,9,workspace,9 +bind=SUPER,0,workspace,10 + +bind=SUPERSHIFT,1,movetoworkspacesilent,1 +bind=SUPERSHIFT,2,movetoworkspacesilent,2 +bind=SUPERSHIFT,3,movetoworkspacesilent,3 +bind=SUPERSHIFT,4,movetoworkspacesilent,4 +bind=SUPERSHIFT,5,movetoworkspacesilent,5 +bind=SUPERSHIFT,6,movetoworkspacesilent,6 +bind=SUPERSHIFT,7,movetoworkspacesilent,7 +bind=SUPERSHIFT,8,movetoworkspacesilent,8 +bind=SUPERSHIFT,9,movetoworkspacesilent,9 +bind=SUPERSHIFT,0,movetoworkspacesilent,10 diff --git a/configs/hypr/scripts/brightness b/configs/hypr/scripts/brightness index 542b91711..371d23fa9 100755 --- a/configs/hypr/scripts/brightness +++ b/configs/hypr/scripts/brightness @@ -1,18 +1,19 @@ #!/bin/sh - down() { -brillo -u 150000 -U 2 -brightness=$(light -g) -dunstify -a "BRIGHTNESS" "Decreasing to $brightness%" -h int:value:"$brightness" -i display-brightness-symbolic -r 2593 -u normal + brightnessctl set 5%- + brillo -u 150000 -U 2 + brightness=$(light -g) + dunstify -a "BRIGHTNESS" "Decreasing to $brightness%" -h int:value:"$brightness" -i display-brightness-symbolic -r 2593 -u normal } up() { -brillo -u 150000 -A 2 -brightness=$(light -g) -dunstify -a "BRIGHTNESS" "Increasing to $brightness%" -h int:value:"$brightness" -i display-brightness-symbolic -r 2593 -u normal + brightnessctl set +5% + brillo -u 150000 -A 2 + brightness=$(light -g) + dunstify -a "BRIGHTNESS" "Increasing to $brightness%" -h int:value:"$brightness" -i display-brightness-symbolic -r 2593 -u normal } case "$1" in - up) up;; - down) down;; + up) up ;; + down) down ;; esac diff --git a/configs/hypr/scripts/hdmi-manager/hdmi-gui.py b/configs/hypr/scripts/hdmi-manager/hdmi-gui.py new file mode 100755 index 000000000..517a5b8cd --- /dev/null +++ b/configs/hypr/scripts/hdmi-manager/hdmi-gui.py @@ -0,0 +1,830 @@ +#!/usr/bin/env python3 +# hdmi-gui.py +# Interfaz gráfica para gestión de monitor externo en Hyprland +# Requiere: python-gobject (gtk4 o gtk3), hyprctl + +import gi +import subprocess +import json +import os +import sys +import signal +import threading +import time + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk, GLib, Pango + +LOCK_FILE = '/tmp/hdmi-manager.lock' +CONFIG_DIR = os.path.expanduser('~/.config/hypr/scripts/hdmi-manager') +LOG_FILE = os.path.expanduser('~/.local/share/hdmi-manager/hdmi-manager.log') + +# ─── Colores y estilos CSS ─────────────────────────────────────────────────── +CSS = """ +* { + font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; +} + +window { + background-color: #0d0f14; +} + +.main-container { + background-color: #0d0f14; + padding: 32px; +} + +.header-title { + font-size: 22px; + font-weight: 700; + color: #e2e8f0; + letter-spacing: 2px; +} + +.header-subtitle { + font-size: 11px; + color: #4a5568; + letter-spacing: 3px; +} + +.monitor-badge { + background-color: #1a1d26; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 12px 16px; +} + +.monitor-name { + font-size: 13px; + font-weight: 600; + color: #63b3ed; +} + +.monitor-res { + font-size: 11px; + color: #718096; +} + +.section-label { + font-size: 10px; + font-weight: 700; + color: #4a5568; + letter-spacing: 3px; +} + +/* Botones de modo */ +.mode-btn { + background-color: #1a1d26; + border: 1px solid #2d3748; + border-radius: 10px; + padding: 20px 16px; + color: #a0aec0; + font-size: 12px; + font-weight: 600; + letter-spacing: 1px; + transition: all 200ms ease; + min-width: 140px; + min-height: 100px; +} + +.mode-btn:hover { + background-color: #1e2233; + border-color: #4299e1; + color: #e2e8f0; +} + +.mode-btn.selected { + background-color: #162032; + border: 2px solid #4299e1; + color: #63b3ed; +} + +.mode-icon { + font-size: 28px; +} + +/* Botones de dirección */ +.dir-btn { + background-color: #1a1d26; + border: 1px solid #2d3748; + border-radius: 8px; + color: #718096; + font-size: 18px; + min-width: 52px; + min-height: 52px; + transition: all 150ms ease; +} + +.dir-btn:hover { + background-color: #1e2233; + border-color: #4299e1; + color: #e2e8f0; +} + +.dir-btn.selected { + background-color: #162032; + border: 2px solid #4299e1; + color: #63b3ed; +} + +.dir-center { + background-color: #111318; + border: 1px solid #2d3748; + border-radius: 8px; + color: #2d3748; + font-size: 11px; + min-width: 52px; + min-height: 52px; +} + +/* Dropdown de resolución */ +.res-combo { + background-color: #1a1d26; + border: 1px solid #2d3748; + border-radius: 8px; + color: #e2e8f0; + font-size: 12px; + padding: 8px 12px; + min-width: 240px; +} + +/* Botón aplicar */ +.apply-btn { + background-color: #1a56a0; + border: none; + border-radius: 8px; + color: #e2e8f0; + font-size: 13px; + font-weight: 700; + letter-spacing: 2px; + padding: 14px 32px; + min-width: 200px; + transition: all 200ms ease; +} + +.apply-btn:hover { + background-color: #2563c4; +} + +.apply-btn:disabled { + background-color: #1a1d26; + color: #4a5568; +} + +.cancel-btn { + background-color: transparent; + border: 1px solid #2d3748; + border-radius: 8px; + color: #718096; + font-size: 12px; + font-weight: 600; + letter-spacing: 1px; + padding: 14px 24px; + transition: all 200ms ease; +} + +.cancel-btn:hover { + border-color: #e53e3e; + color: #fc8181; +} + +.status-bar { + background-color: #0a0c10; + border-top: 1px solid #1a1d26; + padding: 8px 16px; +} + +.status-text { + font-size: 10px; + color: #4a5568; + letter-spacing: 1px; +} + +.divider { + background-color: #1a1d26; + min-height: 1px; +} + +.save-check { + color: #718096; + font-size: 11px; +} + +.save-check check { + background-color: #1a1d26; + border-color: #2d3748; + border-radius: 4px; +} + +.save-check check:checked { + background-color: #1a56a0; + border-color: #4299e1; +} +""" + +def log(msg): + os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) + with open(LOG_FILE, 'a') as f: + f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] GUI: {msg}\n") + + +# ─── Helpers Hyprland ──────────────────────────────────────────────────────── + +def get_monitors(): + """Obtiene información de monitores via hyprctl.""" + try: + result = subprocess.run(['hyprctl', 'monitors', 'all','-j'], + capture_output=True, text=True, timeout=5) + return json.loads(result.stdout) + except Exception as e: + log(f"Error obteniendo monitores: {e}") + return [] + + +def get_connected_monitors(): + monitors = get_monitors() + return [ + m for m in monitors + if not m.get('disabled', False) + ] + +def get_external_monitors(monitors): + """Filtra monitores externos (cualquiera que no sea el principal).""" + primary = get_primary_monitor(monitors) + if not primary: + return monitors[1:] if len(monitors) > 1 else [] + return [m for m in monitors if m['name'] != primary['name']] + + +def get_primary_monitor(monitors): + """Obtiene el monitor principal (generalmente eDP/LVDS/built-in).""" + for m in monitors: + name = m.get('name', '').upper() + if any(k in name for k in ['EDP', 'LVDS', 'DSI', 'INTERNAL']): + return m + return monitors[0] if monitors else None + + +def get_available_resolutions(monitor_name): + """Obtiene resoluciones disponibles para un monitor.""" + try: + result = subprocess.run(['hyprctl', 'monitors', '-j'], + capture_output=True, text=True, timeout=5) + monitors = json.loads(result.stdout) + for m in monitors: + if m['name'] == monitor_name: + modes = m.get('availableModes', []) + if modes: + # Ordenar por resolución (mayor primero) + def res_key(mode): + try: + res = mode.split('@')[0] if '@' in mode else mode + w, h = res.split('x') + return int(w) * int(h) + except: + return 0 + return sorted(modes, key=res_key, reverse=True) + except Exception as e: + log(f"Error obteniendo resoluciones: {e}") + + # Resoluciones comunes como fallback + return ['3840x2160@60Hz', '2560x1440@144Hz', '2560x1440@60Hz', + '1920x1080@144Hz', '1920x1080@60Hz', '1280x720@60Hz'] + + +def apply_mirror(primary, external): + """Configura espejo de pantalla según ejemplo del usuario.""" + primary_name = primary['name'] + external_name = external['name'] + primary_res = f"{primary['width']}x{primary['height']}@{primary.get('refreshRate', 60):.2f}" + + # Ejemplo del usuario: + # monitor=eDP-1,1920x1080@60,0x1080,1 + # monitor = HDMI-A-1, preferred, auto, 1, mirror, eDP-1 + cmd1 = f"hyprctl keyword monitor {primary_name},{primary_res},0x1080,1" + cmd2 = f"hyprctl keyword monitor {external_name},preferred,auto,1,mirror,{primary_name}" + + log(f"Ejecutando: {cmd1}") + subprocess.run(cmd1.split(), capture_output=True, text=True) + log(f"Ejecutando: {cmd2}") + return subprocess.run(cmd2.split(), capture_output=True, text=True) + + +def apply_extend(primary, external, resolution, direction): + """Configura extensión de pantalla con Externo en 0x0.""" + primary_name = primary['name'] + external_name = external['name'] + + p_width = primary.get('width', 1920) + p_height = primary.get('height', 1080) + + # Parsear resolución seleccionada + res_clean = resolution.split('@')[0] if '@' in resolution else resolution + try: + ext_w, ext_h = map(int, res_clean.split('x')) + except: + ext_w, ext_h = 1920, 1080 + + # Seguir el ejemplo del usuario: Externo siempre en 0x0 + # Ejemplo Right: HDMI en 0x0, eDP-1 en -1920x0 + e_pos = "0x0" + + if direction == 'right': + p_pos = f"-{p_width}x0" + elif direction == 'left': + p_pos = f"{ext_w}x0" + elif direction == 'above': + p_pos = f"0x{ext_h}" + elif direction == 'below': + p_pos = f"0x-{p_height}" + else: + p_pos = f"-{p_width}x0" + + # Frecuencia de refresco para externo + rate = resolution.split('@')[1].replace('Hz', '') if '@' in resolution else '60' + ext_full_res = f"{res_clean}@{rate}" + + # Frecuencia de refresco para primario + p_rate = f"{primary.get('refreshRate', 60):.2f}" + p_full_res = f"{p_width}x{p_height}@{p_rate}" + + # Aplicar ambos + cmd_e = f"hyprctl keyword monitor {external_name},{ext_full_res},{e_pos},1" + cmd_p = f"hyprctl keyword monitor {primary_name},{p_full_res},{p_pos},1" + + log(f"Ejecutando: {cmd_e}") + subprocess.run(cmd_e.split(), capture_output=True, text=True) + log(f"Ejecutando: {cmd_p}") + return subprocess.run(cmd_p.split(), capture_output=True, text=True) + +def save_to_hyprland_conf(primary, external, mode, resolution=None, direction=None): + conf_path = os.path.expanduser('~/.config/hypr/hyprland.conf') + if not os.path.exists(conf_path): + log(f"No se encontró {conf_path}") + return False + + try: + with open(conf_path, 'r') as f: + lines = f.readlines() + + marker = "# Monitor configuration via HDMI Manager\n" + + # ── Generar nuevas líneas ── + p_width = primary.get('width', 1920) + p_height = primary.get('height', 1080) + p_rate = f"{primary.get('refreshRate', 60):.2f}" + p_full_res = f"{p_width}x{p_height}@{p_rate}" + + if mode == 'mirror': + p_conf = f"monitor = {primary['name']},{p_full_res},0x1080,1\n" + e_conf = f"monitor = {external['name']},preferred,auto,1,mirror,{primary['name']}\n" + else: + res_clean = resolution.split('@')[0] if resolution and '@' in resolution else (resolution or '1920x1080') + rate = resolution.split('@')[1].replace('Hz', '') if resolution and '@' in resolution else '60' + ext_full_res = f"{res_clean}@{rate}" + + try: + ext_w, ext_h = map(int, res_clean.split('x')) + except: + ext_w, ext_h = 1920, 1080 + + if direction == 'right': + p_pos = f"-{p_width}x0" + elif direction == 'left': + p_pos = f"{ext_w}x0" + elif direction == 'above': + p_pos = f"0x{ext_h}" + else: + p_pos = f"0x-{p_height}" + + e_conf = f"monitor = {external['name']},{ext_full_res},0x0,1\n" + p_conf = f"monitor = {primary['name']},{p_full_res},{p_pos},1\n" + + new_block = [marker, p_conf, e_conf] + + # ── Buscar si ya existe el bloque ── + if marker in lines: + idx = lines.index(marker) + + # Eliminar bloque existente (comentario + 2 líneas siguientes) + end = idx + 3 + lines = lines[:idx] + lines[end:] + + # Insertar nuevo bloque en la misma posición + lines[idx:idx] = new_block + else: + # Si no existe, añadir al final + lines.append("\n") + lines.extend(new_block) + + # ── Guardar ── + with open(conf_path, 'w') as f: + f.writelines(lines) + + log("Configuración actualizada.") + return True + + except Exception as e: + log(f"Error guardando configuración: {e}") + return False + + +# ─── Ventana principal ─────────────────────────────────────────────────────── + +class HdmiManagerWindow(Gtk.Window): + + def __init__(self): + super().__init__(title="HDMI Manager") + self.set_default_size(520, -1) + self.set_resizable(False) + self.set_decorated(False) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_keep_above(True) + + # Estado + self.selected_mode = None # 'mirror' | 'extend' + self.selected_direction = None # 'right' | 'left' | 'above' | 'below' + self.selected_resolution = None + + self.monitors = get_connected_monitors() #get_monitors() + self.external_monitors = get_external_monitors(self.monitors) + self.primary = get_primary_monitor(self.monitors) + self.external = self.external_monitors[0] if self.external_monitors else None + + # CSS + css_provider = Gtk.CssProvider() + css_provider.load_from_data(CSS) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + self._build_ui() + self.connect('destroy', self._on_destroy) + self.connect('key-press-event', self._on_key) + + # Cerrar con Escape + self.show_all() + log(f"Ventana creada. Monitores externos: {[m['name'] for m in self.external_monitors]}") + + def _build_ui(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.add(outer) + + main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24) + main.get_style_context().add_class('main-container') + outer.pack_start(main, True, True, 0) + + # ── Header ── + header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + title = Gtk.Label(label="HDMI MANAGER") + title.get_style_context().add_class('header-title') + title.set_halign(Gtk.Align.START) + subtitle = Gtk.Label(label="HYPRLAND DISPLAY CONTROLLER") + subtitle.get_style_context().add_class('header-subtitle') + subtitle.set_halign(Gtk.Align.START) + header.pack_start(title, False, False, 0) + header.pack_start(subtitle, False, False, 0) + main.pack_start(header, False, False, 0) + + # ── Info de monitores ── + monitors_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + + if self.primary: + p_box = self._make_monitor_badge("🖥", self.primary['name'], + f"{self.primary.get('width','?')}×{self.primary.get('height','?')} • PRINCIPAL") + monitors_box.pack_start(p_box, True, True, 0) + + arrow = Gtk.Label(label="⟷") + arrow.set_markup('') + monitors_box.pack_start(arrow, False, False, 0) + + if self.external: + e_box = self._make_monitor_badge("📺", self.external['name'], + f"{self.external.get('width','?')}×{self.external.get('height','?')} • HDMI") + monitors_box.pack_start(e_box, True, True, 0) + else: + no_ext = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + no_ext.get_style_context().add_class('monitor-badge') + lbl = Gtk.Label(label="⚠ Sin monitor externo detectado") + lbl.get_style_context().add_class('monitor-res') + no_ext.pack_start(lbl, True, True, 8) + monitors_box.pack_start(no_ext, True, True, 0) + + main.pack_start(monitors_box, False, False, 0) + + # ── Separador ── + sep1 = Gtk.Separator() + sep1.get_style_context().add_class('divider') + main.pack_start(sep1, False, False, 0) + + # ── Selección de modo ── + mode_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + mode_label = Gtk.Label(label="MODO DE PANTALLA") + mode_label.get_style_context().add_class('section-label') + mode_label.set_halign(Gtk.Align.START) + mode_section.pack_start(mode_label, False, False, 0) + + mode_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + self.btn_mirror = self._make_mode_btn("⊡", "DUPLICAR", "Misma imagen\nen ambas pantallas", 'mirror') + self.btn_extend = self._make_mode_btn("⊞", "EXTENDER", "Escritorio\nampliado", 'extend') + mode_buttons.pack_start(self.btn_mirror, True, True, 0) + mode_buttons.pack_start(self.btn_extend, True, True, 0) + mode_section.pack_start(mode_buttons, False, False, 0) + main.pack_start(mode_section, False, False, 0) + + # ── Panel de extensión (oculto inicialmente) ── + self.extend_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) + self.extend_panel.set_no_show_all(True) + + sep2 = Gtk.Separator() + sep2.get_style_context().add_class('divider') + self.extend_panel.pack_start(sep2, False, False, 0) + + # Resolución + res_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + res_label = Gtk.Label(label="RESOLUCIÓN DEL MONITOR EXTERNO") + res_label.get_style_context().add_class('section-label') + res_label.set_halign(Gtk.Align.START) + res_section.pack_start(res_label, False, False, 0) + + self.res_combo = Gtk.ComboBoxText() + self.res_combo.get_style_context().add_class('res-combo') + if self.external: + resolutions = get_available_resolutions(self.external['name']) + for r in resolutions: + self.res_combo.append_text(r) + if resolutions: + self.res_combo.set_active(0) + self.selected_resolution = resolutions[0] + self.res_combo.connect('changed', self._on_resolution_changed) + res_section.pack_start(self.res_combo, False, False, 0) + self.extend_panel.pack_start(res_section, False, False, 0) + + # Dirección + dir_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + dir_label = Gtk.Label(label="POSICIÓN DEL MONITOR EXTERNO") + dir_label.get_style_context().add_class('section-label') + dir_label.set_halign(Gtk.Align.START) + dir_section.pack_start(dir_label, False, False, 0) + + # Grid de dirección + dir_center_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + dir_center_box.set_halign(Gtk.Align.CENTER) + + # Fila superior (arriba) + row_top = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + row_top.set_halign(Gtk.Align.CENTER) + spacer1 = Gtk.Box() + spacer1.set_size_request(52, 52) + self.btn_above = self._make_dir_btn("▲", 'above') + spacer2 = Gtk.Box() + spacer2.set_size_request(52, 52) + row_top.pack_start(spacer1, False, False, 0) + row_top.pack_start(self.btn_above, False, False, 0) + row_top.pack_start(spacer2, False, False, 0) + + # Fila media (izq / centro / der) + row_mid = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + row_mid.set_halign(Gtk.Align.CENTER) + self.btn_left = self._make_dir_btn("◀", 'left') + center_lbl = Gtk.Label(label="🖥") + center_lbl.get_style_context().add_class('dir-center') + center_lbl.set_size_request(52, 52) + self.btn_right = self._make_dir_btn("▶", 'right') + row_mid.pack_start(self.btn_left, False, False, 0) + row_mid.pack_start(center_lbl, False, False, 0) + row_mid.pack_start(self.btn_right, False, False, 0) + + # Fila inferior (abajo) + row_bot = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + row_bot.set_halign(Gtk.Align.CENTER) + spacer3 = Gtk.Box() + spacer3.set_size_request(52, 52) + self.btn_below = self._make_dir_btn("▼", 'below') + spacer4 = Gtk.Box() + spacer4.set_size_request(52, 52) + row_bot.pack_start(spacer3, False, False, 0) + row_bot.pack_start(self.btn_below, False, False, 0) + row_bot.pack_start(spacer4, False, False, 0) + + dir_center_box.pack_start(row_top, False, False, 0) + dir_center_box.pack_start(row_mid, False, False, 0) + dir_center_box.pack_start(row_bot, False, False, 0) + + # Descripción de dirección seleccionada + self.dir_desc = Gtk.Label(label="Seleccioná una dirección") + self.dir_desc.get_style_context().add_class('monitor-res') + self.dir_desc.set_halign(Gtk.Align.CENTER) + + dir_section.pack_start(dir_center_box, False, False, 0) + dir_section.pack_start(self.dir_desc, False, False, 4) + self.extend_panel.pack_start(dir_section, False, False, 0) + + main.pack_start(self.extend_panel, False, False, 0) + + # ── Separador final ── + sep3 = Gtk.Separator() + sep3.get_style_context().add_class('divider') + main.pack_start(sep3, False, False, 0) + + # ── Opción guardar ── + save_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + self.save_check = Gtk.CheckButton(label=" Guardar en hyprland.conf") + self.save_check.get_style_context().add_class('save-check') + self.save_check.set_active(True) + save_box.pack_start(self.save_check, False, False, 0) + main.pack_start(save_box, False, False, 0) + + # ── Botones de acción ── + action_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + action_box.set_halign(Gtk.Align.CENTER) + + btn_cancel = Gtk.Button(label="CANCELAR") + btn_cancel.get_style_context().add_class('cancel-btn') + btn_cancel.connect('clicked', lambda _: self.destroy()) + + self.btn_apply = Gtk.Button(label="APLICAR") + self.btn_apply.get_style_context().add_class('apply-btn') + self.btn_apply.connect('clicked', self._on_apply) + self.btn_apply.set_sensitive(False) + + action_box.pack_start(btn_cancel, False, False, 0) + action_box.pack_start(self.btn_apply, False, False, 0) + main.pack_start(action_box, False, False, 0) + + # ── Status bar ── + status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + status_box.get_style_context().add_class('status-bar') + self.status_label = Gtk.Label(label="● Listo") + self.status_label.get_style_context().add_class('status-text') + self.status_label.set_halign(Gtk.Align.START) + status_box.pack_start(self.status_label, True, True, 0) + outer.pack_start(status_box, False, False, 0) + + def _make_monitor_badge(self, icon, name, desc): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + box.get_style_context().add_class('monitor-badge') + icon_lbl = Gtk.Label() + icon_lbl.set_markup(f'{icon}') + name_lbl = Gtk.Label(label=name) + name_lbl.get_style_context().add_class('monitor-name') + desc_lbl = Gtk.Label(label=desc) + desc_lbl.get_style_context().add_class('monitor-res') + box.pack_start(icon_lbl, False, False, 4) + box.pack_start(name_lbl, False, False, 0) + box.pack_start(desc_lbl, False, False, 0) + return box + + def _make_mode_btn(self, icon, label_text, desc, mode): + btn = Gtk.Button() + btn.get_style_context().add_class('mode-btn') + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + icon_lbl = Gtk.Label() + icon_lbl.set_markup(f'{icon}') + icon_lbl.get_style_context().add_class('mode-icon') + lbl = Gtk.Label(label=label_text) + desc_lbl = Gtk.Label(label=desc) + desc_lbl.get_style_context().add_class('monitor-res') + desc_lbl.set_justify(Gtk.Justification.CENTER) + box.pack_start(icon_lbl, False, False, 0) + box.pack_start(lbl, False, False, 0) + box.pack_start(desc_lbl, False, False, 0) + btn.add(box) + btn.connect('clicked', self._on_mode_selected, mode) + return btn + + def _make_dir_btn(self, label_text, direction): + btn = Gtk.Button(label=label_text) + btn.get_style_context().add_class('dir-btn') + btn.set_size_request(52, 52) + btn.connect('clicked', self._on_direction_selected, direction) + return btn + + def _on_mode_selected(self, btn, mode): + self.selected_mode = mode + # Actualizar estilos + for b, m in [(self.btn_mirror, 'mirror'), (self.btn_extend, 'extend')]: + ctx = b.get_style_context() + if m == mode: + ctx.add_class('selected') + else: + ctx.remove_class('selected') + + # Mostrar/ocultar panel de extensión + if mode == 'extend': + self.extend_panel.set_no_show_all(False) + self.extend_panel.show_all() + else: + self.extend_panel.hide() + + self._update_apply_state() + + def _on_direction_selected(self, btn, direction): + self.selected_direction = direction + dir_labels = { + 'right': 'Monitor externo a la DERECHA del principal', + 'left': 'Monitor externo a la IZQUIERDA del principal', + 'above': 'Monitor externo ENCIMA del principal', + 'below': 'Monitor externo DEBAJO del principal', + } + self.dir_desc.set_text(dir_labels.get(direction, '')) + + for b, d in [(self.btn_right, 'right'), (self.btn_left, 'left'), + (self.btn_above, 'above'), (self.btn_below, 'below')]: + ctx = b.get_style_context() + if d == direction: + ctx.add_class('selected') + else: + ctx.remove_class('selected') + + self._update_apply_state() + + def _on_resolution_changed(self, combo): + self.selected_resolution = combo.get_active_text() + + def _update_apply_state(self): + if self.selected_mode == 'mirror': + self.btn_apply.set_sensitive(True) + elif self.selected_mode == 'extend' and self.selected_direction: + self.btn_apply.set_sensitive(True) + else: + self.btn_apply.set_sensitive(False) + + def _on_apply(self, btn): + if not self.external or not self.primary: + self._set_status("⚠ No se detectaron monitores") + return + + self.btn_apply.set_sensitive(False) + self._set_status("⟳ Aplicando configuración...") + + def do_apply(): + try: + if self.selected_mode == 'mirror': + result = apply_mirror(self.primary, self.external) + msg = "✓ Pantalla duplicada correctamente" + save_args = (self.primary, self.external, 'mirror', None, None) + else: + result = apply_extend(self.primary, self.external, + self.selected_resolution, + self.selected_direction) + dir_names = {'right': 'derecha', 'left': 'izquierda', + 'above': 'arriba', 'below': 'abajo'} + msg = f"✓ Extendido hacia la {dir_names.get(self.selected_direction,'')}" + save_args = (self.primary, self.external, 'extend', + self.selected_resolution, self.selected_direction) + + if result.returncode != 0: + msg = f"⚠ Error: {result.stderr.strip()}" + log(f"Error aplicando: {result.stderr}") + elif self.save_check.get_active(): + saved = save_to_hyprland_conf(*save_args) + if saved: + msg += " • Guardado en config" + + GLib.idle_add(self._set_status, msg) + GLib.idle_add(self.btn_apply.set_sensitive, True) + + if result.returncode == 0: + GLib.timeout_add(2000, self.destroy) + + except Exception as e: + log(f"Excepción en apply: {e}") + GLib.idle_add(self._set_status, f"⚠ Error: {str(e)}") + GLib.idle_add(self.btn_apply.set_sensitive, True) + + thread = threading.Thread(target=do_apply, daemon=True) + thread.start() + + def _set_status(self, msg): + self.status_label.set_text(msg) + return False + + def _on_key(self, widget, event): + if event.keyval == Gdk.KEY_Escape: + self.destroy() + + def _on_destroy(self, widget): + if os.path.exists(LOCK_FILE): + os.remove(LOCK_FILE) + log("Ventana cerrada") + Gtk.main_quit() + + +def main(): + # Verificar que hay monitores externos + monitors = get_connected_monitors() #get_monitors() + external = get_external_monitors(monitors) + if not external: + log("No hay monitores externos conectados, saliendo") + sys.exit(0) + + signal.signal(signal.SIGTERM, lambda *_: Gtk.main_quit()) + signal.signal(signal.SIGINT, lambda *_: Gtk.main_quit()) + + win = HdmiManagerWindow() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/configs/hypr/scripts/monitor-switch.sh b/configs/hypr/scripts/monitor-switch.sh new file mode 100755 index 000000000..3bd65ecc1 --- /dev/null +++ b/configs/hypr/scripts/monitor-switch.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Define monitor names +INTERNAL="eDP-1" +# Fix syntax error (removed space after =) and made detection more robust +EXTERNAL=$(hyprctl monitors | grep -v "$INTERNAL" | grep "Monitor" | awk '{print $2}' | head -1) + +if [ -z "$EXTERNAL" ]; then + # Fallback to HDMI if not found + EXTERNAL=$(hyprctl monitors | grep "HDMI" | awk '{print $2}' | head -1) +fi + +# Options for Rofi +options="Extend\nMirror\nExternal Only\nInternal Only" + +# Get user selection +choice=$(echo -e "$options" | rofi -dmenu -i -p "Monitor Mode:") + +case "$choice" in + "Extend") + hyprctl keyword monitor "$INTERNAL, 1920x1080@60, 0x1080, 1" + hyprctl keyword monitor "$EXTERNAL, preferred, 0x0, 1" + ;; + "Mirror") + hyprctl keyword monitor "$INTERNAL, 1920x1080@60, 0x0, 1" + hyprctl keyword monitor "$EXTERNAL, preferred, 0x0, 1, mirror, $INTERNAL" + ;; + "External Only") + hyprctl keyword monitor "$INTERNAL, disable" + hyprctl keyword monitor "$EXTERNAL, preferred, 0x0, 1" + ;; + "Internal Only") + hyprctl keyword monitor "$INTERNAL, 1920x1080@60, 0x0, 1" + hyprctl keyword monitor "$EXTERNAL, disable" + ;; +esac diff --git a/configs/hypr/wallpapers/mo.png b/configs/hypr/wallpapers/mo.png new file mode 100644 index 000000000..44a556120 Binary files /dev/null and b/configs/hypr/wallpapers/mo.png differ diff --git a/configs/kitty/kitty.conf b/configs/kitty/kitty.conf index 8bbf9ca76..ccba79b91 100644 --- a/configs/kitty/kitty.conf +++ b/configs/kitty/kitty.conf @@ -7,7 +7,7 @@ italic_font auto bold_italic_font auto font_size 13 -# background_opacity 0.8 +background_opacity 0.9 # window settings initial_window_width 95c diff --git a/configs/waybar/config b/configs/waybar/config index 8d7a3abab..32059e948 100644 --- a/configs/waybar/config +++ b/configs/waybar/config @@ -1,6 +1,7 @@ { "height": 30, // Waybar height (to be removed for auto height) "layer": "top", // Waybar at top layer + "output": ["*"], "margin-top": 6, "margin-left": 10, "margin-bottom": 0, @@ -32,33 +33,32 @@ "clock": { "format": "{:%a %b %d}", "format-alt": "{:%I:%M %p}", - "tooltip-format": "{:%B %Y}\n{calendar}", + "tooltip-format": "{:%B %Y}\n{calendar}" }, "cpu": { "interval": 10, "format": " {}%", "max-length": 10, - "on-click": "", + "on-click": "" }, "memory": { "interval": 30, "format": " {}%", "format-alt":" {used:0.1f}G", - "max-length": 10, + "max-length": 10 }, "backlight": { "device": "DP-1", "format": "{icon} {percent}%", "format-icons": ["", "", "", "", "", "", "", "", ""], - "on-click": "", + "on-click": "" }, "network": { "format-wifi": "直 {signalStrength}%", "format-ethernet": " wired", "format-disconnected": "睊", - "on-click": "bash ~/.config/waybar/scripts/rofi-wifi-menu.sh", - "format-disconnected": "Disconnected  ", + "on-click": "bash ~/.config/waybar/scripts/rofi-wifi-menu.sh" }, "pulseaudio": { @@ -80,7 +80,7 @@ "bluetooth": { "on-click": "~/.config/waybar/scripts/rofi-bluetooth &", - "format": " {status}", + "format": " {status}" }, "battery": { @@ -100,12 +100,7 @@ "format-alt": "{icon} {time}", "format-full": " {capacity}%", "format-icons": [" ", " ", " ", " ", " "], - }, - "custom/weather": { - "exec": "python3 ~/.config/waybar/scripts/weather.py", - "restart-interval": 300, - "return-type": "json", - "on-click": "xdg-open https://weather.com/en-IN/weather/today/l/a319796a4173829988d68c4e3a5f90c1b6832667ea7aaa201757a1c887ec667a" + "on-click": "kitty --hold -e ~/.config/waybar/scripts/toggle_conservation.sh" }, "custom/spotify": { @@ -114,14 +109,14 @@ "return-type": "json", "on-click": "playerctl play-pause", "on-double-click-right": "playerctl next", - "on-scroll-down": "playerctl previous", + "on-scroll-down": "playerctl previous" }, "custom/power-menu": { "format": " ", - "on-click": "bash ~/.config/waybar/scripts/power-menu/powermenu.sh", + "on-click": "bash ~/.config/waybar/scripts/power-menu/powermenu.sh" }, "custom/launcher": { "format": " ", - "on-click": "rofi -show drun", - }, + "on-click": "rofi -show drun" + } } diff --git a/configs/waybar/scripts/mediaplayer.py b/configs/waybar/scripts/mediaplayer.py index 1630d97c8..4a85528e8 100644 --- a/configs/waybar/scripts/mediaplayer.py +++ b/configs/waybar/scripts/mediaplayer.py @@ -1,76 +1,86 @@ -#!/usr/bin/env python3 -import argparse -import logging -import sys -import signal +#!/usr/bin/python3 + import gi -import json -gi.require_version('Playerctl', '2.0') from gi.repository import Playerctl, GLib +import sys +import json +import signal +import logging +import argparse + +gi.require_version("Playerctl", "2.0") + logger = logging.getLogger(__name__) def write_output(text, player): - logger.info('Writing output') + logger.info("Writing output") - output = {'text': text, - 'class': 'custom-' + player.props.player_name, - 'alt': player.props.player_name} + output = { + "text": text, + "class": "custom-" + player.props.player_name, + "alt": player.props.player_name, + } - sys.stdout.write(json.dumps(output) + '\n') + sys.stdout.write(json.dumps(output) + "\n") sys.stdout.flush() def on_play(player, status, manager): - logger.info('Received new playback status') + logger.info("Received new playback status") on_metadata(player, player.props.metadata, manager) def on_metadata(player, metadata, manager): - logger.info('Received new metadata') - track_info = '' - - if player.props.player_name == 'spotify' and \ - 'mpris:trackid' in metadata.keys() and \ - ':ad:' in player.props.metadata['mpris:trackid']: - track_info = 'AD PLAYING' - elif player.get_artist() != '' and player.get_title() != '': - track_info = '{artist} - {title}'.format(artist=player.get_artist(), - title=player.get_title()) + logger.info("Received new metadata") + track_info = "" + + if ( + player.props.player_name == "spotify" + and "mpris:trackid" in metadata.keys() + and ":ad:" in player.props.metadata["mpris:trackid"] + ): + track_info = "AD PLAYING" + elif player.get_artist() != "" and player.get_title() != "": + track_info = "{artist} - {title}".format( + artist=player.get_artist(), title=player.get_title() + ) else: track_info = player.get_title() - if player.props.status != 'Playing' and track_info: - track_info = ' ' + track_info + if player.props.status != "Playing" and track_info: + track_info = " " + track_info write_output(track_info, player) def on_player_appeared(manager, player, selected_player=None): - if player is not None and (selected_player is None or player.name == selected_player): + if player is not None and ( + selected_player is None or player.name == selected_player + ): init_player(manager, player) else: logger.debug("New player appeared, but it's not the selected player, skipping") def on_player_vanished(manager, player): - logger.info('Player has vanished') - sys.stdout.write('\n') + logger.info("Player has vanished") + sys.stdout.write("\n") sys.stdout.flush() def init_player(manager, name): - logger.debug('Initialize player: {player}'.format(player=name.name)) + logger.debug("Initialize player: {player}".format(player=name.name)) player = Playerctl.Player.new_from_name(name) - player.connect('playback-status', on_play, manager) - player.connect('metadata', on_metadata, manager) + player.connect("playback-status", on_play, manager) + player.connect("metadata", on_metadata, manager) manager.manage_player(player) on_metadata(player, player.props.metadata, manager) def signal_handler(sig, frame): - logger.debug('Received signal to stop, exiting') - sys.stdout.write('\n') + logger.debug("Received signal to stop, exiting") + sys.stdout.write("\n") sys.stdout.flush() # loop.quit() sys.exit(0) @@ -80,10 +90,10 @@ def parse_arguments(): parser = argparse.ArgumentParser() # Increase verbosity with every occurrence of -v - parser.add_argument('-v', '--verbose', action='count', default=0) + parser.add_argument("-v", "--verbose", action="count", default=0) # Define for which player we're listening - parser.add_argument('--player') + parser.add_argument("--player") return parser.parse_args() @@ -92,21 +102,26 @@ def main(): arguments = parse_arguments() # Initialize logging - logging.basicConfig(stream=sys.stderr, level=logging.DEBUG, - format='%(name)s %(levelname)s %(message)s') + logging.basicConfig( + stream=sys.stderr, + level=logging.DEBUG, + format="%(name)s %(levelname)s %(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)) # Log the sent command line arguments - logger.debug('Arguments received {}'.format(vars(arguments))) + logger.debug("Arguments received {}".format(vars(arguments))) manager = Playerctl.PlayerManager() loop = GLib.MainLoop() - manager.connect('name-appeared', lambda *args: on_player_appeared(*args, arguments.player)) - manager.connect('player-vanished', on_player_vanished) + manager.connect( + "name-appeared", lambda *args: on_player_appeared(*args, arguments.player) + ) + manager.connect("player-vanished", on_player_vanished) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) @@ -114,9 +129,11 @@ def main(): for player in manager.props.player_names: if arguments.player is not None and arguments.player != player.name: - logger.debug('{player} is not the filtered player, skipping it' - .format(player=player.name) - ) + logger.debug( + "{player} is not the filtered player, skipping it".format( + player=player.name + ) + ) continue init_player(manager, player) @@ -124,5 +141,5 @@ def main(): loop.run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/configs/waybar/scripts/power-menu/powermenu.sh b/configs/waybar/scripts/power-menu/powermenu.sh index d159b9296..988c8c804 100755 --- a/configs/waybar/scripts/power-menu/powermenu.sh +++ b/configs/waybar/scripts/power-menu/powermenu.sh @@ -57,20 +57,20 @@ run_cmd() { elif [[ $1 == '--reboot' ]]; then systemctl reboot elif [[ $1 == '--suspend' ]]; then - mpc -q pause - amixer set Master mute systemctl suspend + elif [[ $1 == '--logout' ]]; then + hyprctl dispatch exit 1 if [[ "$DESKTOP_SESSION" == 'openbox' ]]; then - openbox --exit + openbox --exit elif [[ "$DESKTOP_SESSION" == 'bspwm' ]]; then - bspc quit + bspc quit elif [[ "$DESKTOP_SESSION" == 'i3' ]]; then - i3-msg exit + i3-msg exit elif [[ "$DESKTOP_SESSION" == 'plasma' ]]; then - qdbus org.kde.ksmserver /KSMServer logout 0 0 0 + qdbus org.kde.ksmserver /KSMServer logout 0 0 0 elif [[ "$DESKTOP_SESSION" == 'Hyprland' ]]; then - hyprctl dispatch exit 1 + hyprctl dispatch exit 1 fi fi else @@ -93,7 +93,7 @@ case $chosen in elif [[ -x '/usr/bin/i3lock' ]]; then i3lock elif [[ -x '/usr/bin/Hyprland' ]]; then - swaylock + swaylock-fancy fi ;; $suspend) diff --git a/configs/waybar/scripts/toggle_conservation.sh b/configs/waybar/scripts/toggle_conservation.sh new file mode 100755 index 000000000..543091460 --- /dev/null +++ b/configs/waybar/scripts/toggle_conservation.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Archivo del modo conservación en Lenovo IdeaPad +FILE="/sys/bus/platform/drivers/ideapad_acpi/VPC2004:00/conservation_mode" + +# Comprobar si existe +if [ ! -f "$FILE" ]; then + notify-send "Battery Mode" "No se encontró el archivo de conservación." + exit 1 +fi + +# Leer el valor actual +current=$(cat "$FILE") + +if [ "$current" -eq 1 ]; then + # Desactivar modo conservación + echo 0 | sudo tee "$FILE" > /dev/null + notify-send "Battery Mode" "Modo conservación DESACTIVADO (carga al 100%)." +else + # Activar modo conservación + echo 1 | sudo tee "$FILE" > /dev/null + notify-send "Battery Mode" "Modo conservación ACTIVADO (limita la carga)." +fi diff --git a/configs/waybar/scripts/weather.py b/configs/waybar/scripts/weather.py index c7ac5c200..4ea0fc579 100644 --- a/configs/waybar/scripts/weather.py +++ b/configs/waybar/scripts/weather.py @@ -58,9 +58,10 @@ data = {} -weather = requests.get("https://wttr.in/?format=j1").json() - +# weather = requests.get("https://wttr.in/?format=j1").json() +weather = requests.get("https://wttr.in/San%20Diego,Cesar?format=j1").json() + def format_time(time): return time.replace("00", "").zfill(2)