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)