mirror of
https://github.com/neoromantique/dotfiles.git
synced 2026-03-13 21:53:20 +03:00
update dotfiles
This commit is contained in:
57
.chezmoi.toml.tmpl
Normal file
57
.chezmoi.toml.tmpl
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{{- /* Detect hostname and suggest default profile */ -}}
|
||||||
|
{{- $hostname := .chezmoi.hostname -}}
|
||||||
|
{{- $defaultProfile := "desktop" -}}
|
||||||
|
{{- if eq $hostname "box" -}}
|
||||||
|
{{- $defaultProfile = "desktop" -}}
|
||||||
|
{{- else if eq $hostname "bluefin" -}}
|
||||||
|
{{- $defaultProfile = "laptop" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- /* Prompt for device profile */ -}}
|
||||||
|
{{- $deviceProfile := promptStringOnce . "deviceProfile" (printf "Device profile (desktop/laptop) [%s]" $defaultProfile) -}}
|
||||||
|
{{- if eq $deviceProfile "" -}}
|
||||||
|
{{- $deviceProfile = $defaultProfile -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- /* Detect distro */ -}}
|
||||||
|
{{- $distro := "unknown" -}}
|
||||||
|
{{- if stat "/etc/arch-release" -}}
|
||||||
|
{{- $distro = "arch" -}}
|
||||||
|
{{- else if lookPath "rpm-ostree" -}}
|
||||||
|
{{- $distro = "fedora-atomic" -}}
|
||||||
|
{{- else if stat "/etc/fedora-release" -}}
|
||||||
|
{{- $distro = "fedora" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- /* Prompt for secrets path */ -}}
|
||||||
|
{{- $secretsPath := promptStringOnce . "secretsPath" "Path to secrets directory [~/secrets]" -}}
|
||||||
|
{{- if eq $secretsPath "" -}}
|
||||||
|
{{- $secretsPath = "~/secrets" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
[data]
|
||||||
|
deviceProfile = {{ $deviceProfile | quote }}
|
||||||
|
hostname = {{ $hostname | quote }}
|
||||||
|
distro = {{ $distro | quote }}
|
||||||
|
secretsPath = {{ $secretsPath | quote }}
|
||||||
|
|
||||||
|
# Device-specific configuration
|
||||||
|
{{- if eq $deviceProfile "desktop" }}
|
||||||
|
primaryMonitor = "DP-1"
|
||||||
|
secondaryMonitor = "DP-2"
|
||||||
|
primaryResolution = "2560x1440@165"
|
||||||
|
secondaryResolution = "2560x1440@60"
|
||||||
|
hasMultipleMonitors = true
|
||||||
|
hasTouchpad = false
|
||||||
|
hasBattery = false
|
||||||
|
idleTimeout = 300
|
||||||
|
{{- else if eq $deviceProfile "laptop" }}
|
||||||
|
primaryMonitor = "eDP-1"
|
||||||
|
secondaryMonitor = ""
|
||||||
|
primaryResolution = "preferred"
|
||||||
|
secondaryResolution = ""
|
||||||
|
hasMultipleMonitors = false
|
||||||
|
hasTouchpad = true
|
||||||
|
hasBattery = true
|
||||||
|
idleTimeout = 180
|
||||||
|
{{- end }}
|
||||||
20
.chezmoiignore
Normal file
20
.chezmoiignore
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Ignore README and install script in home directory
|
||||||
|
README.md
|
||||||
|
install.sh
|
||||||
|
|
||||||
|
# Ignore backup files
|
||||||
|
*.pre-ai
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Ignore git and editor files
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# OS-specific ignores
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
# dotfiles
|
# dotfiles
|
||||||
|
|
||||||
init script tba
|
Dotfiles managed with [chezmoi](https://chezmoi.io/). Supports CachyOS (desktop) and Bluefin (laptop) with device-specific configs for Hyprland, Waybar, and related tools. Templates handle monitor setup, touchpad/gestures, battery modules, and idle behavior automatically based on hostname detection.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|||||||
BIN
home/Pictures/wlpp/wallhaven-9dkeqd.png
Normal file
BIN
home/Pictures/wlpp/wallhaven-9dkeqd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 MiB |
27
home/private_dot_config/hypr/autostart.conf.tmpl
Normal file
27
home/private_dot_config/hypr/autostart.conf.tmpl
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Autostart applications
|
||||||
|
# Device: {{ .deviceProfile }}
|
||||||
|
|
||||||
|
# Polkit agent (GUI password prompts)
|
||||||
|
exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
|
||||||
|
|
||||||
|
# System tray & bar
|
||||||
|
exec-once = nm-applet &
|
||||||
|
exec-once = waybar
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
exec-once = swaync
|
||||||
|
|
||||||
|
# Wallpaper & idle
|
||||||
|
exec-once = hyprpaper
|
||||||
|
exec-once = hypridle
|
||||||
|
|
||||||
|
# Clipboard
|
||||||
|
exec-once = copyq --start-server
|
||||||
|
|
||||||
|
{{- if eq .deviceProfile "desktop" }}
|
||||||
|
# Desktop: Hyprland plugins
|
||||||
|
exec-once = hyprpm reload -n
|
||||||
|
{{- else }}
|
||||||
|
# Laptop: Power management
|
||||||
|
exec-once = power-profiles-daemon || true
|
||||||
|
{{- end }}
|
||||||
18
home/private_dot_config/hypr/colors.conf
Normal file
18
home/private_dot_config/hypr/colors.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
$background = rgb(150E11)
|
||||||
|
$foreground = rgb(FEEFF0)
|
||||||
|
$color0 = rgb(3D373A)
|
||||||
|
$color1 = rgb(220615)
|
||||||
|
$color2 = rgb(08432C)
|
||||||
|
$color3 = rgb(CE2E9E)
|
||||||
|
$color4 = rgb(0881BC)
|
||||||
|
$color5 = rgb(C1E177)
|
||||||
|
$color6 = rgb(FCD0D3)
|
||||||
|
$color7 = rgb(F5DFE1)
|
||||||
|
$color8 = rgb(AC9C9D)
|
||||||
|
$color9 = rgb(220615)
|
||||||
|
$color10 = rgb(08432C)
|
||||||
|
$color11 = rgb(CE2E9E)
|
||||||
|
$color12 = rgb(0881BC)
|
||||||
|
$color13 = rgb(C1E177)
|
||||||
|
$color14 = rgb(FCD0D3)
|
||||||
|
$color15 = rgb(F5DFE1)
|
||||||
30
home/private_dot_config/hypr/hypridle.conf.tmpl
Normal file
30
home/private_dot_config/hypr/hypridle.conf.tmpl
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Hypridle Configuration
|
||||||
|
# Device: {{ .deviceProfile }} ({{ .hostname }})
|
||||||
|
# Idle timeout: {{ .idleTimeout }} seconds
|
||||||
|
|
||||||
|
general {
|
||||||
|
lock_cmd = pidof hyprlock || hyprlock
|
||||||
|
before_sleep_cmd = loginctl lock-session
|
||||||
|
after_sleep_cmd = hyprctl dispatch dpms on
|
||||||
|
}
|
||||||
|
|
||||||
|
listener {
|
||||||
|
timeout = {{ .idleTimeout }}
|
||||||
|
on-timeout = hyprctl dispatch dpms off
|
||||||
|
on-resume = hyprctl dispatch dpms on
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if .hasBattery }}
|
||||||
|
# Laptop: dim screen before turning off
|
||||||
|
listener {
|
||||||
|
timeout = {{ sub .idleTimeout 60 }}
|
||||||
|
on-timeout = brightnessctl -s set 30%
|
||||||
|
on-resume = brightnessctl -r
|
||||||
|
}
|
||||||
|
|
||||||
|
# Laptop: suspend after extended idle
|
||||||
|
listener {
|
||||||
|
timeout = {{ mul .idleTimeout 2 }}
|
||||||
|
on-timeout = systemctl suspend
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
293
home/private_dot_config/hypr/hyprland.conf.tmpl
Normal file
293
home/private_dot_config/hypr/hyprland.conf.tmpl
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Hyprland Configuration
|
||||||
|
# Device: {{ .deviceProfile }} ({{ .hostname }})
|
||||||
|
# Managed by chezmoi - edit source at ~/dotfiles
|
||||||
|
|
||||||
|
source = ~/.config/hypr/colors.conf
|
||||||
|
source = ~/.config/hypr/autostart.conf
|
||||||
|
source = ~/.config/hypr/workspaces.conf
|
||||||
|
source = ~/.config/hypr/monitors.conf
|
||||||
|
|
||||||
|
################
|
||||||
|
### MONITORS ###
|
||||||
|
################
|
||||||
|
|
||||||
|
# Monitor configuration is in monitors.conf (device-specific)
|
||||||
|
|
||||||
|
###################
|
||||||
|
### MY PROGRAMS ###
|
||||||
|
###################
|
||||||
|
|
||||||
|
$terminal = ghostty
|
||||||
|
$fileManager = thunar
|
||||||
|
$menu = wofi --show drun
|
||||||
|
$run_menu = wofi --show run
|
||||||
|
|
||||||
|
#############################
|
||||||
|
### ENVIRONMENT VARIABLES ###
|
||||||
|
#############################
|
||||||
|
|
||||||
|
env = XCURSOR_SIZE,24
|
||||||
|
env = HYPRCURSOR_SIZE,24
|
||||||
|
|
||||||
|
#####################
|
||||||
|
### LOOK AND FEEL ###
|
||||||
|
#####################
|
||||||
|
|
||||||
|
general {
|
||||||
|
gaps_in = 2
|
||||||
|
gaps_out = 5
|
||||||
|
border_size = 2
|
||||||
|
col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg
|
||||||
|
col.inactive_border = rgba(595959aa)
|
||||||
|
resize_on_border = false
|
||||||
|
allow_tearing = false
|
||||||
|
layout = dwindle
|
||||||
|
}
|
||||||
|
|
||||||
|
decoration {
|
||||||
|
rounding = 10
|
||||||
|
rounding_power = 2
|
||||||
|
active_opacity = 1.0
|
||||||
|
inactive_opacity = 1.0
|
||||||
|
|
||||||
|
shadow {
|
||||||
|
enabled = true
|
||||||
|
range = 4
|
||||||
|
render_power = 3
|
||||||
|
color = rgba(1a1a1aee)
|
||||||
|
}
|
||||||
|
|
||||||
|
blur {
|
||||||
|
enabled = true
|
||||||
|
size = 3
|
||||||
|
passes = 1
|
||||||
|
vibrancy = 0.1696
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animations {
|
||||||
|
enabled = yes
|
||||||
|
|
||||||
|
bezier = easeOutQuint,0.23,1,0.32,1
|
||||||
|
bezier = easeInOutCubic,0.65,0.05,0.36,1
|
||||||
|
bezier = linear,0,0,1,1
|
||||||
|
bezier = almostLinear,0.5,0.5,0.75,1.0
|
||||||
|
bezier = quick,0.15,0,0.1,1
|
||||||
|
bezier = snappy,0.2,0.8,0.2,1
|
||||||
|
bezier = overshoot,0.34,1.56,0.64,1
|
||||||
|
bezier = bounce,0.68,-0.55,0.27,1.55
|
||||||
|
bezier = whip,0.1,0.9,0.2,1
|
||||||
|
|
||||||
|
animation = global, 1, 5, default
|
||||||
|
animation = border, 1, 2.5, snappy
|
||||||
|
animation = windows, 1, 2.8, easeOutQuint
|
||||||
|
animation = windowsIn, 1, 2.2, bounce, popin 95%
|
||||||
|
animation = windowsOut, 1, 0.9, whip, popin 85%
|
||||||
|
animation = fadeIn, 1, 0.9, snappy
|
||||||
|
animation = fadeOut, 1, 0.8, snappy
|
||||||
|
animation = fade, 1, 1.6, quick
|
||||||
|
animation = layers, 1, 2.0, easeOutQuint
|
||||||
|
animation = layersIn, 1, 1.8, bounce, fade
|
||||||
|
animation = layersOut, 1, 0.8, whip, fade
|
||||||
|
animation = fadeLayersIn, 1, 0.9, snappy
|
||||||
|
animation = fadeLayersOut, 1, 0.7, snappy
|
||||||
|
animation = workspaces, 1, 1.2, bounce, fade
|
||||||
|
animation = workspacesIn, 1, 0.9, overshoot, fade
|
||||||
|
animation = workspacesOut, 1, 1.0, whip, fade
|
||||||
|
}
|
||||||
|
|
||||||
|
# Smart gaps window rules
|
||||||
|
windowrulev2 = bordersize 0, floating:0, onworkspace:w[tv1]
|
||||||
|
windowrulev2 = rounding 0, floating:0, onworkspace:w[tv1]
|
||||||
|
windowrulev2 = bordersize 0, floating:0, onworkspace:f[1]
|
||||||
|
windowrulev2 = rounding 0, floating:0, onworkspace:f[1]
|
||||||
|
|
||||||
|
binds {
|
||||||
|
hide_special_on_workspace_change = true
|
||||||
|
workspace_back_and_forth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ecosystem {
|
||||||
|
enforce_permissions = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dwindle {
|
||||||
|
pseudotile = true
|
||||||
|
preserve_split = true
|
||||||
|
}
|
||||||
|
|
||||||
|
master {
|
||||||
|
new_status = master
|
||||||
|
}
|
||||||
|
|
||||||
|
misc {
|
||||||
|
force_default_wallpaper = -1
|
||||||
|
disable_hyprland_logo = false
|
||||||
|
enable_anr_dialog = false
|
||||||
|
}
|
||||||
|
|
||||||
|
#############
|
||||||
|
### INPUT ###
|
||||||
|
#############
|
||||||
|
|
||||||
|
input {
|
||||||
|
kb_layout = us, ru
|
||||||
|
kb_variant =
|
||||||
|
kb_model =
|
||||||
|
kb_options = caps:escape
|
||||||
|
kb_rules =
|
||||||
|
numlock_by_default = true
|
||||||
|
follow_mouse = 1
|
||||||
|
sensitivity = 0
|
||||||
|
|
||||||
|
{{- if .hasTouchpad }}
|
||||||
|
touchpad {
|
||||||
|
natural_scroll = true
|
||||||
|
tap-to-click = true
|
||||||
|
disable_while_typing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gestures {
|
||||||
|
workspace_swipe = true
|
||||||
|
workspace_swipe_fingers = 3
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
touchpad {
|
||||||
|
natural_scroll = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
device {
|
||||||
|
name = epic-mouse-v1
|
||||||
|
sensitivity = -0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
###################
|
||||||
|
### KEYBINDINGS ###
|
||||||
|
###################
|
||||||
|
|
||||||
|
$mainMod = SUPER
|
||||||
|
|
||||||
|
# Keyboard layout toggle
|
||||||
|
bind = SUPER, SPACE, exec, bash -c "current=$(hyprctl getoption input:kb_layout -j | jq -r '.str'); if [[ $current == \"us,ru\" ]]; then hyprctl keyword input:kb_layout 'ru,us'; else hyprctl keyword input:kb_layout 'us,ru'; fi"
|
||||||
|
|
||||||
|
# Wallpapers
|
||||||
|
bind = SUPER, bracketright, exec, ~/Scripts/change_wallpaper.sh next
|
||||||
|
bind = SUPER, bracketleft, exec, ~/Scripts/change_wallpaper.sh prev
|
||||||
|
|
||||||
|
# Core bindings
|
||||||
|
bind = $mainMod, Q, exec, $terminal
|
||||||
|
bind = $mainMod, K, killactive,
|
||||||
|
bind = $mainMod, M, exit,
|
||||||
|
bind = $mainMod, E, exec, ~/.config/hypr/scripts/toggle_expo_on_primary.sh
|
||||||
|
bind = $mainMod, V, togglefloating,
|
||||||
|
bind = $mainMod, R, exec, $menu
|
||||||
|
bind = $mainMod SHIFT, R, exec, hyprctl reload
|
||||||
|
bind = $mainMod, P, pseudo,
|
||||||
|
bind = $mainMod, J, togglesplit,
|
||||||
|
bind = $mainMod, L, exec, pactl set-sink-mute @DEFAULT_SINK@ 1 && hyprlock
|
||||||
|
bind = $mainMod, t, togglegroup
|
||||||
|
|
||||||
|
# VPN switcher
|
||||||
|
bind = , F6, exec, ~/.config/hypr/scripts/vpn-switcher.sh
|
||||||
|
|
||||||
|
# Move focus with mainMod + arrow keys
|
||||||
|
bind = $mainMod, left, movefocus, l
|
||||||
|
bind = $mainMod, right, movefocus, r
|
||||||
|
bind = $mainMod, up, movefocus, u
|
||||||
|
bind = $mainMod, down, movefocus, d
|
||||||
|
|
||||||
|
# Switch workspaces with mainMod + [0-9]
|
||||||
|
bind = $mainMod, 1, workspace, 1
|
||||||
|
bind = $mainMod, 2, workspace, 2
|
||||||
|
bind = $mainMod, 3, workspace, 3
|
||||||
|
bind = $mainMod, 4, workspace, 4
|
||||||
|
bind = $mainMod, 5, workspace, 5
|
||||||
|
bind = $mainMod, 6, workspace, 6
|
||||||
|
bind = $mainMod, 7, workspace, 7
|
||||||
|
bind = $mainMod, 8, workspace, 8
|
||||||
|
bind = $mainMod, 9, workspace, 9
|
||||||
|
|
||||||
|
# Special workspaces
|
||||||
|
bind = SUPER, F12, exec, ~/.config/hypr/scripts/workspace-pin.sh 1337
|
||||||
|
bind = , F12, togglespecialworkspace, org
|
||||||
|
bind = SUPER, A, togglespecialworkspace, org
|
||||||
|
bind = SUPER SHIFT, F12, movetoworkspace, special:org
|
||||||
|
bind = SUPER, X, workspace, name:media
|
||||||
|
bind = SUPER SHIFT, X, movetoworkspace, name:media
|
||||||
|
bind = SUPER SHIFT, C, movetoworkspace, name:tg
|
||||||
|
bind = SUPER, c, workspace, name:tg
|
||||||
|
bind = $mainMod, S, togglespecialworkspace, termius
|
||||||
|
bind = $mainMod SHIFT, S, movetoworkspacesilent, special:termius
|
||||||
|
bind = $mainMod, G, togglespecialworkspace, llm
|
||||||
|
bind = $mainMod SHIFT, G, movetoworkspacesilent, special:llm
|
||||||
|
|
||||||
|
# Move active window to workspace
|
||||||
|
bind = $mainMod SHIFT, 1, movetoworkspacesilent, 1
|
||||||
|
bind = $mainMod SHIFT, 2, movetoworkspacesilent, 2
|
||||||
|
bind = $mainMod SHIFT, 3, movetoworkspacesilent, 3
|
||||||
|
bind = $mainMod SHIFT, 4, movetoworkspacesilent, 4
|
||||||
|
bind = $mainMod SHIFT, 5, movetoworkspacesilent, 5
|
||||||
|
bind = $mainMod SHIFT, 6, movetoworkspacesilent, 6
|
||||||
|
bind = $mainMod SHIFT, 7, movetoworkspacesilent, 7
|
||||||
|
bind = $mainMod SHIFT, 8, movetoworkspacesilent, 8
|
||||||
|
bind = $mainMod SHIFT, 9, movetoworkspacesilent, 9
|
||||||
|
bind = $mainMod SHIFT, 0, movetoworkspacesilent, 10
|
||||||
|
|
||||||
|
# Screenshot
|
||||||
|
bind = , Print, exec, ~/.config/hypr/scripts/screenshot.sh
|
||||||
|
|
||||||
|
# Scroll through existing workspaces
|
||||||
|
bind = $mainMod, mouse_down, workspace, e+1
|
||||||
|
bind = $mainMod, mouse_up, workspace, e-1
|
||||||
|
|
||||||
|
# Move/resize windows with mainMod + LMB/RMB
|
||||||
|
bindm = $mainMod, mouse:272, movewindow
|
||||||
|
bindm = $mainMod, mouse:273, resizewindow
|
||||||
|
|
||||||
|
# Media keys
|
||||||
|
bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
|
||||||
|
bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
|
||||||
|
bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
|
||||||
|
bindel = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
|
||||||
|
{{- if .hasBattery }}
|
||||||
|
bindel = ,XF86MonBrightnessUp, exec, brightnessctl s 10%+
|
||||||
|
bindel = ,XF86MonBrightnessDown, exec, brightnessctl s 10%-
|
||||||
|
{{- end }}
|
||||||
|
bindl = , XF86AudioNext, exec, playerctl next
|
||||||
|
bindl = , XF86AudioPause, exec, playerctl play-pause
|
||||||
|
bindl = , XF86AudioPlay, exec, playerctl play-pause
|
||||||
|
bindl = , XF86AudioPrev, exec, playerctl previous
|
||||||
|
|
||||||
|
##############################
|
||||||
|
### WINDOWS AND WORKSPACES ###
|
||||||
|
##############################
|
||||||
|
|
||||||
|
# Window rules for apps
|
||||||
|
windowrulev2 = workspace name:tg, class:^(org.telegram.desktop)$
|
||||||
|
windowrulev2 = workspace name:tg, class:^(discord)$
|
||||||
|
windowrulev2 = suppressevent maximize, class:.*
|
||||||
|
windowrulev2 = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0
|
||||||
|
|
||||||
|
#############
|
||||||
|
### PLUGINS ###
|
||||||
|
#############
|
||||||
|
|
||||||
|
plugin {
|
||||||
|
hyprexpo {
|
||||||
|
columns = 3
|
||||||
|
gap_size = 5
|
||||||
|
bg_col = rgb(111111)
|
||||||
|
workspace_method = first 1
|
||||||
|
{{- if .hasTouchpad }}
|
||||||
|
enable_gesture = true
|
||||||
|
gesture_fingers = 3
|
||||||
|
gesture_distance = 300
|
||||||
|
gesture_positive = true
|
||||||
|
{{- else }}
|
||||||
|
enable_gesture = false
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
home/private_dot_config/hypr/hyprlock.conf
Normal file
124
home/private_dot_config/hypr/hyprlock.conf
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Hyprlock Configuration
|
||||||
|
# Sourcing colors
|
||||||
|
source = $HOME/.config/hypr/colors.conf
|
||||||
|
$Scripts = $HOME/.config/hypr/scripts
|
||||||
|
|
||||||
|
general {
|
||||||
|
grace = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
background {
|
||||||
|
monitor =
|
||||||
|
path = screenshot
|
||||||
|
blur_size = 5
|
||||||
|
blur_passes = 1
|
||||||
|
noise = 0.0117
|
||||||
|
contrast = 1.3000
|
||||||
|
brightness = 0.8000
|
||||||
|
vibrancy = 0.2100
|
||||||
|
vibrancy_darkness = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
input-field {
|
||||||
|
monitor =
|
||||||
|
size = 250, 50
|
||||||
|
outline_thickness = 3
|
||||||
|
dots_size = 0.33
|
||||||
|
dots_spacing = 0.15
|
||||||
|
dots_center = true
|
||||||
|
outer_color = $color5
|
||||||
|
inner_color = $color0
|
||||||
|
font_color = $color12
|
||||||
|
placeholder_text = <i>Password...</i>
|
||||||
|
hide_input = false
|
||||||
|
position = 0, 200
|
||||||
|
halign = center
|
||||||
|
valign = bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
# Date
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:18000000] date +'%A, %-d %B %Y'
|
||||||
|
color = $color12
|
||||||
|
font_size = 34
|
||||||
|
font_family = JetBrains Mono Nerd Font 10
|
||||||
|
position = 0, -150
|
||||||
|
halign = center
|
||||||
|
valign = top
|
||||||
|
}
|
||||||
|
|
||||||
|
# Week
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:18000000] date +'Week %U'
|
||||||
|
color = $color5
|
||||||
|
font_size = 24
|
||||||
|
font_family = JetBrains Mono Nerd Font 10
|
||||||
|
position = 0, -250
|
||||||
|
halign = center
|
||||||
|
valign = top
|
||||||
|
}
|
||||||
|
|
||||||
|
# Time
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:1000] date +"%H:%M:%S"
|
||||||
|
color = $color15
|
||||||
|
font_size = 94
|
||||||
|
font_family = JetBrains Mono Nerd Font 10
|
||||||
|
position = 0, 0
|
||||||
|
halign = center
|
||||||
|
valign = center
|
||||||
|
}
|
||||||
|
|
||||||
|
# User
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = $USER
|
||||||
|
color = $color12
|
||||||
|
font_size = 18
|
||||||
|
font_family = Inter Display Medium
|
||||||
|
position = 0, 100
|
||||||
|
halign = center
|
||||||
|
valign = bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uptime
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:60000] uptime -p
|
||||||
|
color = $color12
|
||||||
|
font_size = 24
|
||||||
|
font_family = JetBrains Mono Nerd Font 10
|
||||||
|
position = 0, 0
|
||||||
|
halign = right
|
||||||
|
valign = bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
# Weather
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:3600000] [ -f ~/.cache/.weather_cache ] && cat ~/.cache/.weather_cache
|
||||||
|
color = $color12
|
||||||
|
font_size = 24
|
||||||
|
font_family = JetBrains Mono Nerd Font 10
|
||||||
|
position = 50, 0
|
||||||
|
halign = left
|
||||||
|
valign = bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wallpaper image
|
||||||
|
image {
|
||||||
|
monitor =
|
||||||
|
path = $HOME/.config/hypr/wallpaper_effects/.wallpaper_current
|
||||||
|
size = 230
|
||||||
|
rounding = -1
|
||||||
|
border_size = 2
|
||||||
|
border_color = $color11
|
||||||
|
rotate = 0
|
||||||
|
reload_time = -1
|
||||||
|
position = 0, 300
|
||||||
|
halign = center
|
||||||
|
valign = bottom
|
||||||
|
}
|
||||||
13
home/private_dot_config/hypr/hyprpaper.conf.tmpl
Normal file
13
home/private_dot_config/hypr/hyprpaper.conf.tmpl
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Hyprpaper Configuration
|
||||||
|
# Device: {{ .deviceProfile }} ({{ .hostname }})
|
||||||
|
|
||||||
|
preload = ~/Pictures/wlpp/wallhaven-9dkeqd.png
|
||||||
|
|
||||||
|
{{- if eq .deviceProfile "desktop" }}
|
||||||
|
wallpaper = {{ .primaryMonitor }}, ~/Pictures/wlpp/wallhaven-9dkeqd.png
|
||||||
|
{{- if .secondaryMonitor }}
|
||||||
|
wallpaper = {{ .secondaryMonitor }}, ~/Pictures/wlpp/wallhaven-9dkeqd.png
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
wallpaper = {{ .primaryMonitor }}, ~/Pictures/wlpp/wallhaven-9dkeqd.png
|
||||||
|
{{- end }}
|
||||||
21
home/private_dot_config/hypr/monitors.conf.tmpl
Normal file
21
home/private_dot_config/hypr/monitors.conf.tmpl
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Monitor Configuration
|
||||||
|
# Device: {{ .deviceProfile }} ({{ .hostname }})
|
||||||
|
|
||||||
|
{{- if eq .deviceProfile "desktop" }}
|
||||||
|
# Desktop dual-monitor setup
|
||||||
|
# Primary: {{ .primaryMonitor }} at {{ .primaryResolution }}
|
||||||
|
monitor = {{ .primaryMonitor }},{{ .primaryResolution }},0x0,1
|
||||||
|
|
||||||
|
{{- if .secondaryMonitor }}
|
||||||
|
# Secondary: {{ .secondaryMonitor }} at {{ .secondaryResolution }}
|
||||||
|
# Positioned to the right of primary
|
||||||
|
monitor = {{ .secondaryMonitor }},{{ .secondaryResolution }},2560x0,1
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- else }}
|
||||||
|
# Laptop single monitor
|
||||||
|
monitor = {{ .primaryMonitor }},{{ .primaryResolution }},auto,1
|
||||||
|
|
||||||
|
# External displays (when docked)
|
||||||
|
monitor = ,preferred,auto,1
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
if [ -z "$XDG_PICTURES_DIR" ] ; then
|
||||||
|
XDG_PICTURES_DIR="$HOME/Pictures"
|
||||||
|
fi
|
||||||
|
|
||||||
|
save_dir="${2:-$XDG_PICTURES_DIR/Screenshots}"
|
||||||
|
save_file=$(date +'%Y-%m-%d_%H-%M-%S_screenshot.png')
|
||||||
|
|
||||||
|
gtkMode=`gsettings get org.gnome.desktop.interface color-scheme | sed "s/'//g" | awk -F '-' '{print $2}'`
|
||||||
|
ncolor="-h string:bgcolor:#191724 -h string:fgcolor:#faf4ed -h string:frcolor:#56526e"
|
||||||
|
|
||||||
|
if [ "${gtkMode}" == "light" ] ; then
|
||||||
|
ncolor="-h string:bgcolor:#f4ede8 -h string:fgcolor:#9893a5 -h string:frcolor:#908caa"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$save_dir" ] ; then
|
||||||
|
mkdir -p $save_dir
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
p) grim $save_dir/$save_file ;;
|
||||||
|
s) grim -g "$(slurp)" - | satty --filename - --fullscreen --output-filename ~/Pictures/Screenshots/satty-$(date '+%Y%m%d-%H:%M:%S').png;;
|
||||||
|
*) echo "...valid options are..."
|
||||||
|
echo "p : print screen to $save_dir"
|
||||||
|
echo "s : snip current screen to $save_dir"
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -f "$save_dir/$save_file" ] ; then
|
||||||
|
notify-send $ncolor "theme" -a "saved in $save_dir" -i "$save_dir/$save_file" -r 91190 -t 2200
|
||||||
|
fi
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get a list of sink names
|
||||||
|
sinks=($(pactl list sinks short | awk '{print $2}'))
|
||||||
|
|
||||||
|
# Get the current default sink name
|
||||||
|
current_sink=$(pactl info | grep "Default Sink:" | awk '{print $3}')
|
||||||
|
|
||||||
|
# Check if we found the current sink
|
||||||
|
if [ -z "$current_sink" ]; then
|
||||||
|
echo "Error: Could not determine the current default sink." >&2
|
||||||
|
# Attempt to set the first sink as default if we can't find the current one
|
||||||
|
if [ ${#sinks[@]} -gt 0 ]; then
|
||||||
|
pactl set-default-sink "${sinks[0]}"
|
||||||
|
echo "Attempting to set default sink to first available: ${sinks[0]}" >&2
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Error: No sinks found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the index of the current sink in the list
|
||||||
|
current_sink_index=-1
|
||||||
|
for i in "${!sinks[@]}"; do
|
||||||
|
if [[ "${sinks[$i]}" == "$current_sink" ]]; then
|
||||||
|
current_sink_index=$i
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if the current sink was found in the list
|
||||||
|
if [ "$current_sink_index" -eq -1 ]; then
|
||||||
|
echo "Error: Current default sink '$current_sink' not found in the list of sinks." >&2
|
||||||
|
# Reset to the first sink as a fallback
|
||||||
|
if [ ${#sinks[@]} -gt 0 ]; then
|
||||||
|
pactl set-default-sink "${sinks[0]}"
|
||||||
|
echo "Resetting to first available sink: ${sinks[0]}" >&2
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Error: No sinks found to reset to." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the direction
|
||||||
|
direction=$1
|
||||||
|
|
||||||
|
# Calculate the index of the next sink
|
||||||
|
if [ "$direction" == "up" ]; then
|
||||||
|
next_sink_index=$(( (current_sink_index - 1 + ${#sinks[@]}) % ${#sinks[@]} ))
|
||||||
|
else # default to down
|
||||||
|
next_sink_index=$(( (current_sink_index + 1) % ${#sinks[@]} ))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the name of the next sink
|
||||||
|
next_sink="${sinks[$next_sink_index]}"
|
||||||
|
|
||||||
|
# Set the new default sink
|
||||||
|
pactl set-default-sink "$next_sink"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Just open expo, no special handling
|
||||||
|
hyprctl dispatch hyprexpo:expo toggle
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Helper script for vpn-switcher to run privileged commands
|
||||||
|
# This gets called via pkexec
|
||||||
|
|
||||||
|
ACTION="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
wg-up)
|
||||||
|
wg-quick up "$1"
|
||||||
|
;;
|
||||||
|
wg-down)
|
||||||
|
wg-quick down "$1"
|
||||||
|
;;
|
||||||
|
systemctl-start)
|
||||||
|
systemctl start "$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown action: $ACTION" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Secrets directory - configurable via chezmoi
|
||||||
|
SECRETS_DIR = os.path.expanduser("{{ .secretsPath }}")
|
||||||
|
VPN_DIR = os.path.join(SECRETS_DIR, "vpn") if os.path.isdir(os.path.join(os.path.expanduser("{{ .secretsPath }}"), "vpn")) else os.path.expanduser("~/cfg/vpn")
|
||||||
|
HELPER = os.path.expanduser("~/.config/hypr/scripts/vpn-switcher-helper.sh")
|
||||||
|
WOFI_CMD = ["wofi", "--dmenu", "--width", "450", "--height", "350", "--prompt", "VPN Switcher", "--cache-file", "/dev/null"]
|
||||||
|
|
||||||
|
def run(cmd, check=False):
|
||||||
|
"""Run command and return stdout, or None on failure."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, check=check)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def notify(msg):
|
||||||
|
subprocess.run(["notify-send", "VPN Switcher", msg, "-r", "91191", "-t", "2000"])
|
||||||
|
|
||||||
|
def get_active_wg():
|
||||||
|
"""Get list of active WireGuard interfaces."""
|
||||||
|
out = run(["wg", "show", "interfaces"])
|
||||||
|
return out.split() if out else []
|
||||||
|
|
||||||
|
def is_tailscale_up():
|
||||||
|
"""Check if tailscale is running."""
|
||||||
|
result = subprocess.run(["tailscale", "status"], capture_output=True)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def get_ts_exit_node():
|
||||||
|
"""Get current tailscale exit node if any."""
|
||||||
|
if not is_tailscale_up():
|
||||||
|
return None
|
||||||
|
out = run(["tailscale", "status", "--json"])
|
||||||
|
if out:
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
data = json.loads(out)
|
||||||
|
return data.get("ExitNodeStatus", {}).get("ID")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_wg_configs():
|
||||||
|
"""Get list of WireGuard config files."""
|
||||||
|
configs = []
|
||||||
|
if os.path.isdir(VPN_DIR):
|
||||||
|
for f in os.listdir(VPN_DIR):
|
||||||
|
if f.endswith(".conf"):
|
||||||
|
configs.append(f[:-5]) # Remove .conf
|
||||||
|
return sorted(configs)
|
||||||
|
|
||||||
|
def get_ts_accounts():
|
||||||
|
"""Get list of tailscale accounts."""
|
||||||
|
out = run(["tailscale", "switch", "--list"])
|
||||||
|
if not out:
|
||||||
|
return []
|
||||||
|
|
||||||
|
accounts = []
|
||||||
|
for line in out.splitlines()[1:]: # Skip header
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 3:
|
||||||
|
id_, tailnet, account = parts[0], parts[1], parts[2]
|
||||||
|
is_active = account.endswith("*")
|
||||||
|
if is_active:
|
||||||
|
account = account[:-1]
|
||||||
|
accounts.append({"id": id_, "tailnet": tailnet, "account": account, "active": is_active})
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
def get_ts_exit_nodes():
|
||||||
|
"""Get list of available tailscale exit nodes."""
|
||||||
|
if not is_tailscale_up():
|
||||||
|
return []
|
||||||
|
|
||||||
|
out = run(["tailscale", "exit-node", "list"])
|
||||||
|
if not out:
|
||||||
|
return []
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for line in out.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
# Skip header, empty lines, and comment lines
|
||||||
|
if not line or line.startswith("IP") or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
ip, hostname = parts[0], parts[1]
|
||||||
|
country = parts[2] if len(parts) > 2 else "-"
|
||||||
|
city = parts[3] if len(parts) > 3 else "-"
|
||||||
|
status = " ".join(parts[4:]) if len(parts) > 4 else ""
|
||||||
|
# Skip offline nodes
|
||||||
|
if "offline" in status.lower():
|
||||||
|
continue
|
||||||
|
# Extract short name (before first dot)
|
||||||
|
short_name = hostname.split(".")[0]
|
||||||
|
nodes.append({"hostname": hostname, "short": short_name, "ip": ip, "country": country, "city": city})
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def build_menu():
|
||||||
|
"""Build the menu entries."""
|
||||||
|
lines = []
|
||||||
|
active_wg = get_active_wg()
|
||||||
|
ts_exit = get_ts_exit_node()
|
||||||
|
|
||||||
|
# WireGuard section
|
||||||
|
lines.append("━━━ WireGuard ━━━")
|
||||||
|
for name in get_wg_configs():
|
||||||
|
status = "●" if name in active_wg else "○"
|
||||||
|
lines.append(f" {status} {name}")
|
||||||
|
|
||||||
|
# Tailscale accounts section
|
||||||
|
lines.append("━━━ Tailscale Accounts ━━━")
|
||||||
|
for acc in get_ts_accounts():
|
||||||
|
status = "●" if acc["active"] else "○"
|
||||||
|
if acc["active"]:
|
||||||
|
lines.append(f" {status} {acc['tailnet']} ({acc['account']})")
|
||||||
|
else:
|
||||||
|
lines.append(f" {status} {acc['tailnet']} ({acc['account']}) [{acc['id']}]")
|
||||||
|
|
||||||
|
# Tailscale exit nodes section
|
||||||
|
lines.append("━━━ Tailscale Exit Nodes ━━━")
|
||||||
|
if is_tailscale_up():
|
||||||
|
if ts_exit:
|
||||||
|
lines.append(" ● Exit Node Active - Disconnect")
|
||||||
|
for node in get_ts_exit_nodes():
|
||||||
|
lines.append(f" ○ {node['hostname']} ({node['country']}/{node['city']})")
|
||||||
|
else:
|
||||||
|
lines.append(" ○ Start Tailscale")
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
lines.append("━━━ Actions ━━━")
|
||||||
|
lines.append(" ⊘ Disconnect All")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def pkexec_helper(action, arg):
|
||||||
|
"""Run helper script via pkexec."""
|
||||||
|
subprocess.run(["pkexec", HELPER, action, arg], capture_output=True)
|
||||||
|
|
||||||
|
def disconnect_all_wg():
|
||||||
|
"""Disconnect all WireGuard interfaces."""
|
||||||
|
for iface in get_active_wg():
|
||||||
|
conf_path = os.path.join(VPN_DIR, f"{iface}.conf")
|
||||||
|
if os.path.exists(conf_path):
|
||||||
|
pkexec_helper("wg-down", conf_path)
|
||||||
|
else:
|
||||||
|
pkexec_helper("wg-down", iface)
|
||||||
|
|
||||||
|
def handle_selection(selection):
|
||||||
|
"""Handle the user's menu selection."""
|
||||||
|
# Skip headers and empty
|
||||||
|
if not selection or selection.startswith("━━━"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Strip leading whitespace and status icon
|
||||||
|
clean = selection.strip()
|
||||||
|
if clean.startswith("●") or clean.startswith("○") or clean.startswith("⊘"):
|
||||||
|
clean = clean[1:].strip()
|
||||||
|
|
||||||
|
# Get current state
|
||||||
|
active_wg = get_active_wg()
|
||||||
|
wg_configs = get_wg_configs()
|
||||||
|
ts_accounts = get_ts_accounts()
|
||||||
|
ts_exit_nodes = get_ts_exit_nodes()
|
||||||
|
|
||||||
|
# Check if it's a WireGuard config
|
||||||
|
for name in wg_configs:
|
||||||
|
if clean == name:
|
||||||
|
if name in active_wg:
|
||||||
|
# Turn off
|
||||||
|
conf_path = os.path.join(VPN_DIR, f"{name}.conf")
|
||||||
|
pkexec_helper("wg-down", conf_path)
|
||||||
|
notify(f"WireGuard: {name} disconnected")
|
||||||
|
else:
|
||||||
|
# Turn on (disable others first)
|
||||||
|
disconnect_all_wg()
|
||||||
|
if is_tailscale_up():
|
||||||
|
run(["tailscale", "set", "--exit-node="])
|
||||||
|
conf_path = os.path.join(VPN_DIR, f"{name}.conf")
|
||||||
|
pkexec_helper("wg-up", conf_path)
|
||||||
|
notify(f"WireGuard: {name} connected")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if it's a Tailscale account
|
||||||
|
for acc in ts_accounts:
|
||||||
|
if acc["tailnet"] in clean and acc["account"] in clean:
|
||||||
|
if acc["active"]:
|
||||||
|
notify("Already on this Tailscale account")
|
||||||
|
else:
|
||||||
|
run(["tailscale", "switch", acc["id"]])
|
||||||
|
notify(f"Tailscale: Switched to {acc['tailnet']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if it's a Tailscale exit node
|
||||||
|
if clean == "Exit Node Active - Disconnect":
|
||||||
|
run(["tailscale", "set", "--exit-node="])
|
||||||
|
notify("Tailscale exit node disconnected")
|
||||||
|
return
|
||||||
|
|
||||||
|
if clean == "Start Tailscale":
|
||||||
|
run(["tailscale", "up"])
|
||||||
|
notify("Tailscale started")
|
||||||
|
return
|
||||||
|
|
||||||
|
for node in ts_exit_nodes:
|
||||||
|
if node["hostname"] in clean or node["short"] in clean:
|
||||||
|
disconnect_all_wg()
|
||||||
|
run(["tailscale", "set", f"--exit-node={node['ip']}"])
|
||||||
|
notify(f"Tailscale: Connected via {node['short']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
if clean == "Disconnect All":
|
||||||
|
disconnect_all_wg()
|
||||||
|
if is_tailscale_up():
|
||||||
|
run(["tailscale", "set", "--exit-node="])
|
||||||
|
notify("All VPNs disconnected")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
menu = build_menu()
|
||||||
|
|
||||||
|
# Run wofi
|
||||||
|
result = subprocess.run(
|
||||||
|
WOFI_CMD,
|
||||||
|
input=menu,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
selection = result.stdout.strip()
|
||||||
|
if selection:
|
||||||
|
handle_selection(selection)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
WORKSPACE=$1
|
||||||
|
CORRECT_PIN="1234"
|
||||||
|
LOG_FILE="$HOME/.workspace_access_log"
|
||||||
|
|
||||||
|
# Fuzzel colors - Nord theme
|
||||||
|
BG_COLOR="242a36ee"
|
||||||
|
TEXT_COLOR="eceff4ff"
|
||||||
|
SELECTION_COLOR="88c0d0ff"
|
||||||
|
SELECTION_TEXT_COLOR="2e3440ff"
|
||||||
|
BORDER_COLOR="5e81acff"
|
||||||
|
|
||||||
|
# Display an informational notification before showing PIN dialog
|
||||||
|
notify-send --icon=dialog-password-symbolic \
|
||||||
|
--app-name="Workspace Security" \
|
||||||
|
"Workspace $WORKSPACE" "Protected workspace - PIN required" \
|
||||||
|
--urgency=low
|
||||||
|
|
||||||
|
# Get PIN using fuzzel
|
||||||
|
ENTERED_PIN=$(echo -e "\n" | fuzzel \
|
||||||
|
--dmenu \
|
||||||
|
--password \
|
||||||
|
--prompt="Enter PIN for workspace $WORKSPACE: " \
|
||||||
|
--width=35 \
|
||||||
|
--lines=0 \
|
||||||
|
--horizontal-pad=20 \
|
||||||
|
--vertical-pad=15 \
|
||||||
|
--inner-pad=10 \
|
||||||
|
--border-width=2 \
|
||||||
|
--border-radius=10 \
|
||||||
|
--font="JetBrains Mono:size=14" \
|
||||||
|
--background=${BG_COLOR} \
|
||||||
|
--text-color=${TEXT_COLOR} \
|
||||||
|
--match-color=${SELECTION_COLOR} \
|
||||||
|
--selection-color=${SELECTION_COLOR} \
|
||||||
|
--selection-text-color=${SELECTION_TEXT_COLOR} \
|
||||||
|
--border-color=${BORDER_COLOR} )
|
||||||
|
|
||||||
|
# Trim whitespace
|
||||||
|
ENTERED_PIN=$(echo "$ENTERED_PIN" | xargs)
|
||||||
|
|
||||||
|
# Check if PIN entry was cancelled (empty)
|
||||||
|
if [ -z "$ENTERED_PIN" ]; then
|
||||||
|
notify-send --icon=dialog-information \
|
||||||
|
--app-name="Workspace Security" \
|
||||||
|
"Access Cancelled" "PIN entry was cancelled" \
|
||||||
|
--urgency=low
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify PIN
|
||||||
|
if [ "$ENTERED_PIN" == "$CORRECT_PIN" ]; then
|
||||||
|
# Correct PIN - switch to workspace with nice visual feedback
|
||||||
|
notify-send --icon=dialog-password \
|
||||||
|
--app-name="Workspace Security" \
|
||||||
|
"Access Granted" "Switching to workspace $WORKSPACE" \
|
||||||
|
--urgency=low
|
||||||
|
|
||||||
|
# Optional: Log successful attempt
|
||||||
|
echo "$(date): Successful PIN entry for workspace $WORKSPACE" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Small delay for notification visibility before switching
|
||||||
|
sleep 0.2
|
||||||
|
hyprctl dispatch workspace "$WORKSPACE"
|
||||||
|
else
|
||||||
|
# Wrong PIN - show notification with error icon
|
||||||
|
notify-send --icon=dialog-error \
|
||||||
|
--app-name="Workspace Security" \
|
||||||
|
"Access Denied" "Incorrect PIN for workspace $WORKSPACE" \
|
||||||
|
--urgency=normal
|
||||||
|
|
||||||
|
# Log failed attempt
|
||||||
|
echo "$(date): Failed PIN attempt for workspace $WORKSPACE" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
15
home/private_dot_config/hypr/workspaces.conf
Normal file
15
home/private_dot_config/hypr/workspaces.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Workspace definitions
|
||||||
|
workspace = 1, name:WEB
|
||||||
|
workspace = 2, name:CODE
|
||||||
|
workspace = 3, name:TERM
|
||||||
|
workspace = 4, name:IDE
|
||||||
|
workspace = 5, name:VM
|
||||||
|
workspace = 6, name:GFX
|
||||||
|
workspace = 7, name:DOC
|
||||||
|
workspace = 8, name:GAME
|
||||||
|
workspace = 9, name:MISC
|
||||||
|
workspace = 10, name:TMP
|
||||||
|
|
||||||
|
# Smart gaps rules
|
||||||
|
workspace = w[tv1], gapsout:0, gapsin:0
|
||||||
|
workspace = f[1], gapsout:0, gapsin:0
|
||||||
124
home/private_dot_config/waybar/config.jsonc.tmpl
Normal file
124
home/private_dot_config/waybar/config.jsonc.tmpl
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// -*- mode: jsonc -*-
|
||||||
|
// Device: {{ .deviceProfile }} ({{ .hostname }})
|
||||||
|
{
|
||||||
|
"height": 30,
|
||||||
|
"spacing": 6,
|
||||||
|
{{- if eq .deviceProfile "desktop" }}
|
||||||
|
"output": ["{{ .primaryMonitor }}"],
|
||||||
|
{{- end }}
|
||||||
|
"position": "bottom",
|
||||||
|
"modules-left": ["hyprland/workspaces"],
|
||||||
|
"modules-center": [],
|
||||||
|
"modules-right": [
|
||||||
|
"custom/vpn",
|
||||||
|
"pulseaudio#spk",
|
||||||
|
"pulseaudio#mic",
|
||||||
|
"network",
|
||||||
|
{{- if .hasBattery }}
|
||||||
|
"battery",
|
||||||
|
"backlight",
|
||||||
|
{{- end }}
|
||||||
|
"cpu",
|
||||||
|
"memory",
|
||||||
|
"clock",
|
||||||
|
"tray"
|
||||||
|
],
|
||||||
|
|
||||||
|
"hyprland/workspaces": {
|
||||||
|
"disable-scroll": true,
|
||||||
|
"warp-on-scroll": false,
|
||||||
|
"show-special": true,
|
||||||
|
"sort-by": "id",
|
||||||
|
"format": "{name}",
|
||||||
|
"on-scroll-up": "hyprctl dispatch workspace e+1",
|
||||||
|
"on-scroll-down": "hyprctl dispatch workspace e-1",
|
||||||
|
"ignore-workspaces": ["(special:)?chrome-sharing-indicator"],
|
||||||
|
"format-icons": {
|
||||||
|
"1": "WEB",
|
||||||
|
"2": "CODE",
|
||||||
|
"3": "TERM",
|
||||||
|
"4": "IDE",
|
||||||
|
"5": "VM",
|
||||||
|
"6": "GFX",
|
||||||
|
"7": "DOC",
|
||||||
|
"8": "GAME",
|
||||||
|
"9": "MISC",
|
||||||
|
"10": "TMP",
|
||||||
|
"special:termius": "s:term",
|
||||||
|
"special:org": "s:org",
|
||||||
|
"special:llm": "s:llm",
|
||||||
|
"default": "WS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"hyprland/window": {
|
||||||
|
"format": "{title}",
|
||||||
|
"max-length": 60
|
||||||
|
},
|
||||||
|
|
||||||
|
"pulseaudio#spk": {
|
||||||
|
"scroll-step": 2,
|
||||||
|
"format": "VOL {volume}%",
|
||||||
|
"format-muted": "VOL muted",
|
||||||
|
"on-click": "pavucontrol -t 3",
|
||||||
|
"on-click-right": "pactl set-sink-mute @DEFAULT_SINK@ toggle",
|
||||||
|
"on-scroll-up": "~/.config/hypr/scripts/scroll-audio-sink.sh up",
|
||||||
|
"on-scroll-down": "~/.config/hypr/scripts/scroll-audio-sink.sh down"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pulseaudio#mic": {
|
||||||
|
"scroll-step": 2,
|
||||||
|
"format": "{format_source}",
|
||||||
|
"format-source": "MIC {volume}%",
|
||||||
|
"format-source-muted": "MIC muted",
|
||||||
|
"on-click": "pavucontrol -t 4",
|
||||||
|
"on-click-right": "pactl set-source-mute @DEFAULT_SOURCE@ toggle",
|
||||||
|
"on-scroll-up": "pactl set-source-volume @DEFAULT_SOURCE@ +2%",
|
||||||
|
"on-scroll-down": "pactl set-source-volume @DEFAULT_SOURCE@ -2%"
|
||||||
|
},
|
||||||
|
|
||||||
|
"network": {
|
||||||
|
"format-wifi": "NET wifi {essid} {signalStrength}%",
|
||||||
|
"format-ethernet": "NET eth {ipaddr}",
|
||||||
|
"format-linked": "NET link",
|
||||||
|
"format-disconnected": "NET down",
|
||||||
|
"tooltip-format": "{ifname} via {gwaddr}"
|
||||||
|
},
|
||||||
|
|
||||||
|
{{- if .hasBattery }}
|
||||||
|
"battery": {
|
||||||
|
"format": "BAT {capacity}%",
|
||||||
|
"format-charging": "BAT+ {capacity}%",
|
||||||
|
"format-plugged": "BAT= {capacity}%",
|
||||||
|
"format-full": "BAT full",
|
||||||
|
"states": {
|
||||||
|
"warning": 30,
|
||||||
|
"critical": 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"backlight": {
|
||||||
|
"format": "BRT {percent}%",
|
||||||
|
"on-scroll-up": "brightnessctl s 5%+",
|
||||||
|
"on-scroll-down": "brightnessctl s 5%-"
|
||||||
|
},
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
"cpu": { "format": "CPU {usage}%", "interval": 5, "tooltip": false },
|
||||||
|
"memory": { "format": "MEM {}%", "interval": 5 },
|
||||||
|
|
||||||
|
"clock": {
|
||||||
|
"format": "TIME {:%Y-%m-%d %H:%M}",
|
||||||
|
"tooltip-format": "<tt>{calendar}</tt>",
|
||||||
|
"interval": 60
|
||||||
|
},
|
||||||
|
|
||||||
|
"tray": { "spacing": 4 },
|
||||||
|
|
||||||
|
"custom/vpn": {
|
||||||
|
"exec": "~/.config/waybar/scripts/vpn-status.sh",
|
||||||
|
"return-type": "json",
|
||||||
|
"interval": 5,
|
||||||
|
"on-click": "~/.config/hypr/scripts/vpn-switcher.sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
101
home/private_dot_config/waybar/scripts/executable_vpn-status.sh
Normal file
101
home/private_dot_config/waybar/scripts/executable_vpn-status.sh
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# VPN status for waybar - outputs JSON
|
||||||
|
|
||||||
|
get_status() {
|
||||||
|
local wg_status=""
|
||||||
|
local nb_status=""
|
||||||
|
local ts_status=""
|
||||||
|
local text="VPN off"
|
||||||
|
local tooltip="No VPN active"
|
||||||
|
local class="disconnected"
|
||||||
|
local has_tunnel=false # true if WG, NetBird, or TS with exit node
|
||||||
|
|
||||||
|
# Check NetBird
|
||||||
|
if command -v netbird &>/dev/null; then
|
||||||
|
nb_json=$(netbird status --json 2>/dev/null)
|
||||||
|
if [[ -n "$nb_json" ]]; then
|
||||||
|
nb_mgmt=$(echo "$nb_json" | jq -r '.management.connected // false')
|
||||||
|
if [[ "$nb_mgmt" == "true" ]]; then
|
||||||
|
nb_ip=$(echo "$nb_json" | jq -r '.netbirdIp // empty' | cut -d'/' -f1)
|
||||||
|
nb_fqdn=$(echo "$nb_json" | jq -r '.fqdn // empty')
|
||||||
|
nb_connected=$(echo "$nb_json" | jq -r '.peers.connected // 0')
|
||||||
|
nb_total=$(echo "$nb_json" | jq -r '.peers.total // 0')
|
||||||
|
|
||||||
|
# Extract network name from fqdn (e.g., box-128-45.lumia.overlay -> lumia)
|
||||||
|
nb_net=$(echo "$nb_fqdn" | awk -F'.' '{print $(NF-1)}')
|
||||||
|
|
||||||
|
nb_status="NB:${nb_net}(${nb_connected}/${nb_total})"
|
||||||
|
tooltip="NetBird: ${nb_fqdn}\nIP: ${nb_ip}\nPeers: ${nb_connected}/${nb_total}"
|
||||||
|
# NetBird mesh connection alone doesn't route all traffic like a VPN
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check WireGuard interfaces (excluding NetBird's wt0)
|
||||||
|
wg_ifaces=$(ip link show type wireguard 2>/dev/null | grep -oP '^\d+: \K[^:]+' | grep -v '^wt')
|
||||||
|
if [[ -n "$wg_ifaces" ]]; then
|
||||||
|
wg_name=$(echo "$wg_ifaces" | head -1)
|
||||||
|
wg_status="WG:$wg_name"
|
||||||
|
[[ -z "$tooltip" || "$tooltip" == "No VPN active" ]] && tooltip=""
|
||||||
|
[[ -n "$tooltip" ]] && tooltip+="\n"
|
||||||
|
tooltip+="WireGuard: $wg_name"
|
||||||
|
has_tunnel=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Tailscale
|
||||||
|
if command -v tailscale &>/dev/null; then
|
||||||
|
ts_json=$(tailscale status --json 2>/dev/null)
|
||||||
|
if [[ -n "$ts_json" ]]; then
|
||||||
|
ts_state=$(echo "$ts_json" | jq -r '.BackendState // empty')
|
||||||
|
|
||||||
|
if [[ "$ts_state" == "Running" ]]; then
|
||||||
|
# Get tailnet name (domain)
|
||||||
|
tailnet=$(echo "$ts_json" | jq -r '.CurrentTailnet.Name // empty')
|
||||||
|
tailnet_short="${tailnet%.ts.net}"
|
||||||
|
tailnet_short="${tailnet_short%.tail*}"
|
||||||
|
|
||||||
|
# Check for exit node
|
||||||
|
exit_node_id=$(echo "$ts_json" | jq -r '.ExitNodeStatus.ID // empty')
|
||||||
|
exit_online=$(echo "$ts_json" | jq -r '.ExitNodeStatus.Online // false')
|
||||||
|
|
||||||
|
if [[ -n "$exit_node_id" && "$exit_online" == "true" ]]; then
|
||||||
|
exit_host=$(echo "$ts_json" | jq -r '
|
||||||
|
.Peer | to_entries[] | select(.value.ExitNode == true) | .value.HostName // "exit"
|
||||||
|
' 2>/dev/null)
|
||||||
|
[[ -z "$exit_host" || "$exit_host" == "null" ]] && exit_host="exit"
|
||||||
|
|
||||||
|
ts_status="TS:${tailnet_short}→${exit_host}"
|
||||||
|
[[ -n "$tooltip" ]] && tooltip+="\n"
|
||||||
|
tooltip+="Tailscale: ${tailnet}\nExit: ${exit_host}"
|
||||||
|
has_tunnel=true
|
||||||
|
else
|
||||||
|
ts_status="TS:${tailnet_short}"
|
||||||
|
[[ -n "$tooltip" ]] && tooltip+="\n"
|
||||||
|
tooltip+="Tailscale: ${tailnet} (no exit)"
|
||||||
|
# No exit node = no tunnel, don't set has_tunnel
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build final output
|
||||||
|
local parts=()
|
||||||
|
[[ -n "$wg_status" ]] && parts+=("$wg_status")
|
||||||
|
[[ -n "$nb_status" ]] && parts+=("$nb_status")
|
||||||
|
[[ -n "$ts_status" ]] && parts+=("$ts_status")
|
||||||
|
|
||||||
|
if [[ ${#parts[@]} -gt 0 ]]; then
|
||||||
|
text=$(IFS=' | '; echo "${parts[*]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only green if actual tunnel (WG, NetBird, or TS+exit)
|
||||||
|
if [[ "$has_tunnel" == true ]]; then
|
||||||
|
class="connected"
|
||||||
|
elif [[ ${#parts[@]} -gt 0 ]]; then
|
||||||
|
class="connected-no-tunnel"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '{"text": "%s", "tooltip": "%s", "class": "%s"}\n' "$text" "$tooltip" "$class"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_status
|
||||||
88
home/private_dot_config/waybar/style.css
Normal file
88
home/private_dot_config/waybar/style.css
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/* ANSI/BBS monospace */
|
||||||
|
* {
|
||||||
|
font-family: "Terminus", "IBM Plex Mono", "JetBrainsMono Nerd Font",
|
||||||
|
monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
window#waybar {
|
||||||
|
background: #141414;
|
||||||
|
color: #cccccc;
|
||||||
|
border: 0px solid #cccccc;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 4px 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boxy ASCII-ish modules */
|
||||||
|
#workspaces button,
|
||||||
|
#custom-vpn,
|
||||||
|
#pulseaudio,
|
||||||
|
#network,
|
||||||
|
#battery,
|
||||||
|
#backlight,
|
||||||
|
#cpu,
|
||||||
|
#memory,
|
||||||
|
#clock,
|
||||||
|
#tray {
|
||||||
|
background: transparent;
|
||||||
|
color: #cccccc;
|
||||||
|
border: 1px dashed #cccccc;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 1px 6px;
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workspaces: focused/urgent with inverse/red */
|
||||||
|
#workspaces button:hover {
|
||||||
|
background: #001900;
|
||||||
|
}
|
||||||
|
#workspaces button.focused,
|
||||||
|
#workspaces button.active {
|
||||||
|
background: #cccccc;
|
||||||
|
color: #000;
|
||||||
|
border-color: #cccccc;
|
||||||
|
}
|
||||||
|
#workspaces button.urgent {
|
||||||
|
background: #ff0000;
|
||||||
|
color: #000;
|
||||||
|
border-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specials: dotted border to mark them */
|
||||||
|
#workspaces button.special {
|
||||||
|
border-style: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VPN status colors */
|
||||||
|
#custom-vpn.connected {
|
||||||
|
color: #00ff00;
|
||||||
|
border-color: #00ff00;
|
||||||
|
}
|
||||||
|
#custom-vpn.connected-no-tunnel {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
#custom-vpn.disconnected {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Battery states */
|
||||||
|
#battery.warning {
|
||||||
|
color: #ffaa00;
|
||||||
|
border-color: #ffaa00;
|
||||||
|
}
|
||||||
|
#battery.critical {
|
||||||
|
color: #ff0000;
|
||||||
|
border-color: #ff0000;
|
||||||
|
}
|
||||||
|
#battery.charging {
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retro tooltip */
|
||||||
|
tooltip {
|
||||||
|
background: #000;
|
||||||
|
color: #cccccc;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
}
|
||||||
180
install.sh
Executable file
180
install.sh
Executable file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Dotfiles Bootstrap & Management Script
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||||
|
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||||
|
|
||||||
|
DISTRO="unknown"
|
||||||
|
DOTFILES_DIR="$HOME/dotfiles"
|
||||||
|
|
||||||
|
detect_distro() {
|
||||||
|
if [[ -f /etc/arch-release ]]; then
|
||||||
|
echo "arch"
|
||||||
|
elif command -v rpm-ostree &>/dev/null; then
|
||||||
|
echo "fedora-atomic"
|
||||||
|
elif [[ -f /etc/fedora-release ]]; then
|
||||||
|
echo "fedora"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_chezmoi() {
|
||||||
|
if command -v chezmoi &>/dev/null; then
|
||||||
|
log_ok "chezmoi already installed: $(chezmoi --version | head -1)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing chezmoi..."
|
||||||
|
case "$DISTRO" in
|
||||||
|
arch)
|
||||||
|
sudo pacman -S --noconfirm chezmoi
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
sudo dnf install -y chezmoi
|
||||||
|
;;
|
||||||
|
fedora-atomic)
|
||||||
|
if command -v brew &>/dev/null; then
|
||||||
|
brew install chezmoi
|
||||||
|
else
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b ~/.local/bin
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b ~/.local/bin
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
log_ok "chezmoi installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_secrets() {
|
||||||
|
local secrets_dir="$HOME/secrets"
|
||||||
|
if [[ ! -d "$secrets_dir" ]]; then
|
||||||
|
log_info "Creating secrets directory..."
|
||||||
|
mkdir -p "$secrets_dir/vpn"
|
||||||
|
chmod 700 "$secrets_dir"
|
||||||
|
log_ok "Created $secrets_dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
init_chezmoi() {
|
||||||
|
local repo_url="${1:-}"
|
||||||
|
if [[ -n "$repo_url" ]]; then
|
||||||
|
log_info "Initializing from: $repo_url"
|
||||||
|
chezmoi init "$repo_url"
|
||||||
|
elif [[ -d "$DOTFILES_DIR/home" ]]; then
|
||||||
|
log_info "Initializing from local: $DOTFILES_DIR/home"
|
||||||
|
chezmoi init --source="$DOTFILES_DIR/home" --prompt=false
|
||||||
|
else
|
||||||
|
log_error "No repo URL and ~/dotfiles not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_initialized() {
|
||||||
|
[[ -f "$HOME/.config/chezmoi/chezmoi.toml" ]] && chezmoi source-path &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
check_config_outdated() {
|
||||||
|
# Check if config template has changed
|
||||||
|
if chezmoi diff 2>&1 | grep -q "config file template has changed"; then
|
||||||
|
log_warn "Config template changed, regenerating..."
|
||||||
|
chezmoi init --source="$DOTFILES_DIR/home" --prompt=false
|
||||||
|
log_ok "Config regenerated"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
echo ""
|
||||||
|
echo " 1) diff - Show pending changes"
|
||||||
|
echo " 2) apply - Apply dotfiles"
|
||||||
|
echo " 3) reinit - Re-initialize (change profile)"
|
||||||
|
echo " 4) update - Pull latest and apply"
|
||||||
|
echo " 5) edit - Edit a managed file"
|
||||||
|
echo " q) quit"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
manage_menu() {
|
||||||
|
while true; do
|
||||||
|
show_menu
|
||||||
|
read -rp "Choice: " choice
|
||||||
|
echo ""
|
||||||
|
case "$choice" in
|
||||||
|
1|diff)
|
||||||
|
chezmoi diff | less -R
|
||||||
|
;;
|
||||||
|
2|apply)
|
||||||
|
chezmoi apply -v
|
||||||
|
log_ok "Applied!"
|
||||||
|
;;
|
||||||
|
3|reinit)
|
||||||
|
chezmoi init --force
|
||||||
|
;;
|
||||||
|
4|update)
|
||||||
|
chezmoi update -v
|
||||||
|
log_ok "Updated!"
|
||||||
|
;;
|
||||||
|
5|edit)
|
||||||
|
read -rp "File to edit (e.g. ~/.config/hypr/hyprland.conf): " file
|
||||||
|
[[ -n "$file" ]] && chezmoi edit "$file"
|
||||||
|
;;
|
||||||
|
q|quit|exit)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_warn "Invalid choice"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
DISTRO=$(detect_distro)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dotfiles Manager "
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
log_info "Distro: $DISTRO | Host: $(hostname)"
|
||||||
|
|
||||||
|
install_chezmoi
|
||||||
|
setup_secrets
|
||||||
|
|
||||||
|
if is_initialized; then
|
||||||
|
log_ok "Chezmoi already initialized"
|
||||||
|
check_config_outdated
|
||||||
|
manage_menu
|
||||||
|
else
|
||||||
|
init_chezmoi "${1:-}"
|
||||||
|
echo ""
|
||||||
|
log_ok "Bootstrap complete!"
|
||||||
|
echo ""
|
||||||
|
read -rp "Apply dotfiles now? [Y/n] " apply
|
||||||
|
if [[ ! "$apply" =~ ^[Nn]$ ]]; then
|
||||||
|
chezmoi apply -v
|
||||||
|
log_ok "Applied!"
|
||||||
|
else
|
||||||
|
echo "Run 'chezmoi diff' to review, 'chezmoi apply' to install"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user