diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c18dd8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/build.sh b/build.sh
index 6d9fe78..c8a092f 100755
--- a/build.sh
+++ b/build.sh
@@ -1,115 +1,141 @@
#!/bin/sh
-
set -e
-ORIG_PWD=$(pwd)
-# Elevate to root if needed
+# === Colors ===
+RED="\033[1;31m"
+GREEN="\033[1;32m"
+YELLOW="\033[1;33m"
+BLUE="\033[1;34m"
+MAGENTA="\033[1;35m"
+CYAN="\033[1;36m"
+RESET="\033[0m"
+
+log_info() { printf "${CYAN}[*]${RESET} %s\n" "$1"; }
+log_success() { printf "${GREEN}[✓]${RESET} %s\n" "$1"; }
+log_warn() { printf "${YELLOW}[!]${RESET} %s\n" "$1"; }
+log_error() { printf "${RED}[✗]${RESET} %s\n" "$1"; }
+
+ORIG_PWD=$(pwd)
+ORIG_USER=$(logname)
+ORIG_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
+
+# === Elevate to root if needed ===
if [ "$(id -u)" -ne 0 ]; then
exec sudo "$0" "$@"
fi
-ORIG_USER=$(logname)
-ORIG_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
-
-# === Update system ===
-echo "Updating system..."
-pacman -Syu --noconfirm
-
-# === Essential tools ===
+# === Package groups ===
ESSENTIAL_PACKAGES="
- python3
- vim
- neovim
- ranger
- firefox
+ base-devel python3 vim neovim ranger firefox
"
-# === Display manager ===
DISPLAY_MANAGER="
- sddm
+ sddm qt5-graphicaleffects
"
-# === Wayland & Hyprland environment ===
WAYLAND_ENVIRONMENT="
- hyprland
- hyprpaper
- waybar
- kitty
- wofi
- mako
- libnotify
- grim
- slurp
- wl-clipboard
- wayland-utils
- xorg-xwayland
+ hyprland hyprpaper waybar kitty wofi mako libnotify grim slurp
+ wl-clipboard wl-clip-persist wayland-utils xorg-xwayland
"
-# === Audio ===
AUDIO_PACKAGES="
- pipewire
- wireplumber
- pipewire-pulse
- pavucontrol
+ pipewire wireplumber pipewire-pulse pavucontrol
"
-# === Fonts & theming ===
FONTS_AND_THEME="
- otf-font-awesome
- noto-fonts
- noto-fonts-cjk
- ttf-jetbrains-mono
- orchis-theme
+ otf-font-awesome noto-fonts noto-fonts-cjk ttf-jetbrains-mono orchis-theme
"
-# === Multimedia ===
MEDIA_PACKAGES="
- mpv
- eog
- ffmpeg
+ mpv eog ffmpeg
"
-# === Install all packages ===
-echo "Installing packages..."
-pacman -S --noconfirm --needed \
- $ESSENTIAL_PACKAGES \
- $DISPLAY_MANAGER \
- $WAYLAND_ENVIRONMENT \
- $AUDIO_PACKAGES \
- $FONTS_AND_THEME \
- $MEDIA_PACKAGES
+# === Functions ===
+update_system() {
+ log_info "Updating system..."
+ pacman -Syu --noconfirm
+}
-# === Enable display manager ===
-echo "Enabling SDDM..."
-systemctl enable sddm
-# === Apply GTK settings for theme and color scheme ===
-echo "Applying GTK settings..."
+install_packages() {
+ log_info "Installing packages..."
+ pacman -S --noconfirm --needed \
+ $ESSENTIAL_PACKAGES \
+ $DISPLAY_MANAGER \
+ $WAYLAND_ENVIRONMENT \
+ $AUDIO_PACKAGES \
+ $FONTS_AND_THEME \
+ $MEDIA_PACKAGES
+}
-# Extract DBus address from the user's environment
-USER_DBUS_ENV=$(sudo -u "$ORIG_USER" -- dbus-launch)
-eval "$USER_DBUS_ENV"
+apply_gtk_settings() {
+ log_info "Applying GTK settings..."
+ export XDG_RUNTIME_DIR="/run/user/$(id -u "$ORIG_USER")"
+ DBUS_ADDR="unix:path=${XDG_RUNTIME_DIR}/bus"
-export DBUS_SESSION_BUS_ADDRESS
-export XDG_RUNTIME_DIR="/run/user/$(id -u $ORIG_USER)"
+ for setting in \
+ "org.gnome.desktop.interface gtk-theme Orchis-Dark-Compact" \
+ "org.gnome.desktop.interface icon-theme Orchis-Dark-Compact" \
+ "org.gnome.desktop.interface color-scheme prefer-dark"
+ do
+ if sudo -u "$ORIG_USER" \
+ DBUS_SESSION_BUS_ADDRESS="$DBUS_ADDR" \
+ gsettings set $setting; then
+ log_success "Set $setting"
+ else
+ log_warn "Failed to set $setting"
+ fi
+ done
+}
-sudo -u "$ORIG_USER" DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
- XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \
- gsettings set org.gnome.desktop.interface gtk-theme "Orchis-Dark-Compact"
+enable_sddm() {
+ log_info "Enabling SDDM..."
+ systemctl enable sddm
+}
-sudo -u "$ORIG_USER" DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
- XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \
- gsettings set org.gnome.desktop.interface icon-theme "Orchis-Dark-Compact"
+install_sddm_theme() {
+ log_info "Installing SDDM theme..."
+ if pacman -Q where-is-my-sddm-theme-git >/dev/null 2>&1; then
+ log_success "SDDM theme already installed."
+ return
+ fi
-sudo -u "$ORIG_USER" DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
- XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \
- gsettings set org.gnome.desktop.interface color-scheme "prefer-dark"
+ sudo -u "$ORIG_USER" mkdir -p "$ORIG_HOME/Repositories"
+ cd "$ORIG_HOME/Repositories"
-echo "GTK settings applied successfully."
+ if [ ! -d "where-is-my-sddm-theme" ]; then
+ sudo -u "$ORIG_USER" git clone https://aur.archlinux.org/where-is-my-sddm-theme-git.git where-is-my-sddm-theme
+ else
+ log_warn "Theme repo already exists, skipping clone."
+ fi
-# === Run dotfiles setup ===
-echo "Running dotfiles setup as $ORIG_USER..."
-cd "$ORIG_PWD" || exit 1
-sudo -u "$ORIG_USER" HOME="$ORIG_HOME" python3 setup.py
-sudo -E -u "$ORIG_USER" HOME="$ORIG_HOME" mkdir -p "$ORIG_HOME/Downloads" "$ORIG_HOME/Documents" "$ORIG_HOME/Pictures" "$ORIG_HOME/Videos" "$ORIG_HOME/Repositories"
+ cd "$ORIG_HOME/Repositories/where-is-my-sddm-theme"
+ sudo -u "$ORIG_USER" makepkg -si --noconfirm
+}
-echo "Dotfiles installed successfully."
+run_dotfiles_setup() {
+ log_info "Running dotfiles setup..."
+ cd "$ORIG_PWD"
+
+ [ -f "setup_user.py" ] && sudo -u "$ORIG_USER" HOME="$ORIG_HOME" python3 setup_user.py \
+ || log_warn "setup_user.py not found."
+
+ [ -f "setup_system.py" ] && python3 setup_system.py \
+ || log_warn "setup_system.py not found."
+
+ sudo -u "$ORIG_USER" mkdir -p \
+ "$ORIG_HOME/Downloads" \
+ "$ORIG_HOME/Documents" \
+ "$ORIG_HOME/Pictures" \
+ "$ORIG_HOME/Videos" \
+ "$ORIG_HOME/Repositories"
+}
+
+# === Main flow ===
+update_system
+install_packages
+apply_gtk_settings
+enable_sddm
+install_sddm_theme
+run_dotfiles_setup
+
+log_success "Setup completed successfully."
diff --git a/src/.bashrc b/home/.bashrc
similarity index 100%
rename from src/.bashrc
rename to home/.bashrc
diff --git a/src/.config/gtk-3.0/settings.ini b/home/.config/gtk-3.0/settings.ini
similarity index 100%
rename from src/.config/gtk-3.0/settings.ini
rename to home/.config/gtk-3.0/settings.ini
diff --git a/src/.config/hypr/hyprland.conf b/home/.config/hypr/hyprland.conf
similarity index 99%
rename from src/.config/hypr/hyprland.conf
rename to home/.config/hypr/hyprland.conf
index 6a357b0..0740b9a 100644
--- a/src/.config/hypr/hyprland.conf
+++ b/home/.config/hypr/hyprland.conf
@@ -24,7 +24,7 @@ $screenshot = grim -g "$(slurp -d)" - | wl-copy
#################
# Autostart necessary processes (like notifications daemons, status bars, etc.)
-exec-once=waybar & hyprpaper
+exec-once=waybar & hyprpaper & wl-clip-persist --clipboard both & mako
# & mako --config=$HOME/.config/mako/config
diff --git a/src/.config/hypr/hyprpaper.conf b/home/.config/hypr/hyprpaper.conf
similarity index 100%
rename from src/.config/hypr/hyprpaper.conf
rename to home/.config/hypr/hyprpaper.conf
diff --git a/src/.config/hypr/monitors-1.conf b/home/.config/hypr/monitors-1.conf
similarity index 100%
rename from src/.config/hypr/monitors-1.conf
rename to home/.config/hypr/monitors-1.conf
diff --git a/src/.config/hypr/monitors-2.conf b/home/.config/hypr/monitors-2.conf
similarity index 100%
rename from src/.config/hypr/monitors-2.conf
rename to home/.config/hypr/monitors-2.conf
diff --git a/src/.config/kitty/kitty.conf b/home/.config/kitty/kitty.conf
similarity index 100%
rename from src/.config/kitty/kitty.conf
rename to home/.config/kitty/kitty.conf
diff --git a/src/.config/mako/config b/home/.config/mako/config
similarity index 100%
rename from src/.config/mako/config
rename to home/.config/mako/config
diff --git a/src/.config/mimeapps.list b/home/.config/mimeapps.list
similarity index 100%
rename from src/.config/mimeapps.list
rename to home/.config/mimeapps.list
diff --git a/src/.config/nvim/init.vim b/home/.config/nvim/init.vim
similarity index 100%
rename from src/.config/nvim/init.vim
rename to home/.config/nvim/init.vim
diff --git a/src/.config/waybar/config.jsonc b/home/.config/waybar/config.jsonc
similarity index 100%
rename from src/.config/waybar/config.jsonc
rename to home/.config/waybar/config.jsonc
diff --git a/src/.config/waybar/power_menu.xml b/home/.config/waybar/power_menu.xml
similarity index 100%
rename from src/.config/waybar/power_menu.xml
rename to home/.config/waybar/power_menu.xml
diff --git a/src/.config/waybar/style.css b/home/.config/waybar/style.css
similarity index 100%
rename from src/.config/waybar/style.css
rename to home/.config/waybar/style.css
diff --git a/src/.config/wofi/style.css b/home/.config/wofi/style.css
similarity index 100%
rename from src/.config/wofi/style.css
rename to home/.config/wofi/style.css
diff --git a/src/.profile b/home/.profile
similarity index 100%
rename from src/.profile
rename to home/.profile
diff --git a/home/Documents/bookmarks.html b/home/Documents/bookmarks.html
new file mode 100644
index 0000000..6ffce9b
--- /dev/null
+++ b/home/Documents/bookmarks.html
@@ -0,0 +1,25 @@
+
+
+
+
+
Bookmarks
+Bookmarks Menu
+
+
+
Mozilla Firefox
+
+
- Get Help
+
- Customize Firefox
+
- Get Involved
+
- About Us
+
+
Bookmarks Toolbar
+
+
-
+
-
+
-
+
+
diff --git a/src/Pictures/wallpaper.png b/home/Pictures/wallpaper.png
similarity index 100%
rename from src/Pictures/wallpaper.png
rename to home/Pictures/wallpaper.png
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 7370873..0000000
--- a/setup.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import os
-import shutil
-
-SRC_DIR = "src"
-HOME_DIR = os.path.expanduser("~")
-
-CHOICES = {
- '.config/hypr/monitors.conf': ['.config/hypr/monitors-1.conf', '.config/hypr/monitors-2.conf']
-}
-
-
-def ask_override(path):
- resp = input(f"'{path}' already exists. Override? (Y/N): ").strip().lower()
- return resp == 'y'
-
-
-def choice_files(src_root, dst_root, choices_map, override_all=False):
- for final_name, options in choices_map.items():
- existing_options = [f for f in options if os.path.exists(os.path.join(src_root, f))]
- if not existing_options:
- print(f"No source file found among options for {final_name}. Skipping.")
- continue
-
- if len(existing_options) == 1:
- chosen = existing_options[0]
- else:
- print(f"Choose which file to copy as '{final_name}':")
- for idx, opt in enumerate(existing_options, 1):
- print(f"{idx}: {opt}")
- while True:
- try:
- choice_idx = int(input(f"Enter number (1-{len(existing_options)}): "))
- if 1 <= choice_idx <= len(existing_options):
- chosen = existing_options[choice_idx - 1]
- break
- except ValueError:
- pass
- print("Invalid choice, try again.")
-
- src_file = os.path.join(src_root, chosen)
- dst_file = os.path.join(dst_root, final_name)
-
- if os.path.exists(dst_file):
- if not override_all and not ask_override(dst_file):
- print(f"Skipped overriding {dst_file}")
- continue
-
- shutil.copy2(src_file, dst_file)
- print(f"Copied {chosen} -> {final_name}")
-
-def copy_with_structure(src_root, dst_root, override_all=False):
- for root, dirs, files in os.walk(src_root):
- rel_path = os.path.relpath(root, src_root)
- target_dir = os.path.join(dst_root, rel_path)
-
- os.makedirs(target_dir, exist_ok=True)
-
- for file in files:
- src_file = os.path.join(root, file)
- dst_file = os.path.join(target_dir, file)
-
- if os.path.exists(dst_file):
- if not override_all and not ask_override(dst_file):
- continue
-
- shutil.copy2(src_file, dst_file)
- print(f"Copied: {src_file} -> {dst_file}")
-
-def main():
- override_all = input('Override all existing files? (Y/N): ').strip().lower() == 'y'
-
- choice_files(SRC_DIR, HOME_DIR, CHOICES, override_all)
-
- copy_with_structure(SRC_DIR, HOME_DIR, override_all)
-
-if __name__ == '__main__':
- main()
diff --git a/setup_system.py b/setup_system.py
new file mode 100644
index 0000000..c5d6a2b
--- /dev/null
+++ b/setup_system.py
@@ -0,0 +1,17 @@
+import sys
+from utils import choice_files, copy_with_structure, require_root, log_info, log_error, CYAN, RESET
+
+SRC_DIR = "system_files"
+DST_DIR = "/"
+CHOICES = {}
+
+
+def main():
+ require_root()
+ override_all = input(f"{CYAN}Override all existing system files? (Y/N): {RESET}").strip().lower() == 'y'
+ choice_files(SRC_DIR, DST_DIR, CHOICES, override_all)
+ copy_with_structure(SRC_DIR, DST_DIR, override_all)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/setup_user.py b/setup_user.py
new file mode 100644
index 0000000..78210de
--- /dev/null
+++ b/setup_user.py
@@ -0,0 +1,22 @@
+import os
+from utils import choice_files, copy_with_structure, CYAN, RESET
+
+
+SRC_DIR = "home"
+HOME_DIR = os.path.expanduser("~")
+CHOICES = {
+ '.config/hypr/monitors.conf': [
+ '.config/hypr/monitors-1.conf',
+ '.config/hypr/monitors-2.conf'
+ ]
+}
+
+
+def main():
+ override_all = input(f"{CYAN}Override all existing files for user (mainly .config)? (Y/N): {RESET}").strip().lower() == 'y'
+ choice_files(SRC_DIR, HOME_DIR, CHOICES, override_all)
+ copy_with_structure(SRC_DIR, HOME_DIR, override_all)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/system_files/usr/lib/sddm/sddm.conf.d/default.conf b/system_files/usr/lib/sddm/sddm.conf.d/default.conf
new file mode 100644
index 0000000..13d51d6
--- /dev/null
+++ b/system_files/usr/lib/sddm/sddm.conf.d/default.conf
@@ -0,0 +1,139 @@
+[Autologin]
+# Whether sddm should automatically log back into sessions when they exit
+Relogin=false
+
+# Name of session file for autologin session (if empty try last logged in)
+Session=
+
+# Username for autologin session
+User=
+
+
+[General]
+# Which display server should be used.
+# Valid values are: x11, x11-user, wayland. Wayland support is experimental
+DisplayServer=x11
+
+# Comma-separated list of environment variables to be set
+GreeterEnvironment=
+
+# Halt command
+HaltCommand=/usr/bin/systemctl poweroff
+
+# Input method module
+InputMethod=
+
+# Comma-separated list of Linux namespaces for user session to enter
+Namespaces=
+
+# Initial NumLock state. Can be on, off or none.
+# If property is set to none, numlock won't be changed
+# NOTE: Currently ignored if autologin is enabled.
+Numlock=none
+
+# Reboot command
+RebootCommand=/usr/bin/systemctl reboot
+
+
+[Theme]
+# Current theme name
+Current=where_is_my_sddm_theme
+
+# Cursor size used in the greeter
+CursorSize=
+
+# Cursor theme used in the greeter
+CursorTheme=
+
+# Number of users to use as threshold
+# above which avatars are disabled
+# unless explicitly enabled with EnableAvatars
+DisableAvatarsThreshold=7
+
+# Enable display of custom user avatars
+EnableAvatars=true
+
+# Global directory for user avatars
+# The files should be named .face.icon
+FacesDir=/usr/share/sddm/faces
+
+# Font used in the greeter
+Font=
+
+# Theme directory path
+ThemeDir=/usr/share/sddm/themes
+
+
+[Users]
+# Default $PATH for logged in users
+DefaultPath=/usr/local/sbin:/usr/local/bin:/usr/bin
+
+# Comma-separated list of shells.
+# Users with these shells as their default won't be listed
+HideShells=
+
+# Comma-separated list of users that should not be listed
+HideUsers=
+
+# Maximum user id for displayed users
+MaximumUid=60513
+
+# Minimum user id for displayed users
+MinimumUid=1000
+
+# Remember the session of the last successfully logged in user
+RememberLastSession=true
+
+# Remember the last successfully logged in user
+RememberLastUser=true
+
+# When logging in as the same user twice, restore the original session, rather than create a new one
+ReuseSession=true
+
+
+[Wayland]
+# Path of the Wayland compositor to execute when starting the greeter
+CompositorCommand=weston --shell=kiosk
+
+# Enable Qt's automatic high-DPI scaling
+EnableHiDPI=true
+
+# Path to a script to execute when starting the desktop session
+SessionCommand=/usr/share/sddm/scripts/wayland-session
+
+# Comma-separated list of directories containing available Wayland sessions
+SessionDir=/usr/local/share/wayland-sessions,/usr/share/wayland-sessions
+
+# Path to the user session log file
+SessionLogFile=.local/share/sddm/wayland-session.log
+
+
+[X11]
+# Path to a script to execute when starting the display server
+DisplayCommand=/usr/share/sddm/scripts/Xsetup
+
+# Path to a script to execute when stopping the display server
+DisplayStopCommand=/usr/share/sddm/scripts/Xstop
+
+# Enable Qt's automatic high-DPI scaling
+EnableHiDPI=true
+
+# Arguments passed to the X server invocation
+ServerArguments=-nolisten tcp
+
+# Path to X server binary
+ServerPath=/usr/bin/X
+
+# Path to a script to execute when starting the desktop session
+SessionCommand=/usr/share/sddm/scripts/Xsession
+
+# Comma-separated list of directories containing available X sessions
+SessionDir=/usr/local/share/xsessions,/usr/share/xsessions
+
+# Path to the user session log file
+SessionLogFile=.local/share/sddm/xorg-session.log
+
+# Path to Xephyr binary
+XephyrPath=/usr/bin/Xephyr
+
+
diff --git a/system_files/usr/share/sddm/themes/where_is_my_sddm_theme/theme.conf b/system_files/usr/share/sddm/themes/where_is_my_sddm_theme/theme.conf
new file mode 100644
index 0000000..554724d
--- /dev/null
+++ b/system_files/usr/share/sddm/themes/where_is_my_sddm_theme/theme.conf
@@ -0,0 +1,68 @@
+[General]
+# Password mask character
+passwordCharacter=*
+# Mask password characters or not ("true" or "false")
+passwordMask=true
+# value "1" is all display width, "0.5" is a half of display width etc.
+passwordInputWidth=0.5
+# Background color of password input
+passwordInputBackground=
+# Radius of password input corners
+passwordInputRadius=
+# Width of the border for the password input
+passwordInputBorderWidth=0
+# Border color for the password input
+passwordInputBorderColor=
+# "true" for visible cursor, "false"
+passwordInputCursorVisible=true
+# Font size of password (in points)
+passwordFontSize=96
+passwordCursorColor=random
+passwordTextColor=
+# Allow blank password (e.g., if authentication is done by another PAM module)
+passwordAllowEmpty=false
+
+# Radius of the border which is displayed upon wrong authentication attempt
+wrongPasswordBorderRadius=
+# Color of the border which is displayed upon wrong authentication attempt
+wrongPasswordBorderColor=
+
+# Enable or disable cursor blink animation ("true" or "false")
+cursorBlinkAnimation=true
+
+# Show or not sessions choose label
+showSessionsByDefault=false
+# Font size of sessions choose label (in points).
+sessionsFontSize=24
+
+# Show or not users choose label
+showUsersByDefault=false
+# Font size of users choose label (in points)
+usersFontSize=48
+# Show user real name on label by default
+showUserRealNameByDefault=true
+
+# Path to background image
+background=
+# Or use just one color
+backgroundFill=#000000
+# Fill mode for image background
+# Value must be on of: aspect, fill, tile, pad
+backgroundFillMode=aspect
+
+# Default text color for all labels
+basicTextColor=#ffffff
+
+# Blur radius for background image
+blurRadius=0
+
+# Hide cursor
+hideCursor=false
+
+# Default font
+font=monospace
+
+# Font of help message
+helpFont=monospace
+# Font size of help message (in points)
+helpFontSize=18
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..9578cf7
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,83 @@
+import os
+import shutil
+import sys
+
+
+RED = "\033[1;31m"
+GREEN = "\033[1;32m"
+YELLOW = "\033[1;33m"
+CYAN = "\033[1;36m"
+RESET = "\033[0m"
+
+
+def log_info(msg): print(f"{CYAN}[*]{RESET} {msg}")
+def log_success(msg): print(f"{GREEN}[✓]{RESET} {msg}")
+def log_warn(msg): print(f"{YELLOW}[!]{RESET} {msg}")
+def log_error(msg): print(f"{RED}[✗]{RESET} {msg}")
+
+
+def ask_override(path):
+ resp = input(f"{YELLOW}'{path}' already exists. Override? (Y/N): {RESET}").strip().lower()
+ return resp == 'y'
+
+
+def choice_files(src_root, dst_root, choices_map, override_all=False):
+ for final_name, options in choices_map.items():
+ existing_options = [
+ f for f in options
+ if os.path.exists(os.path.join(src_root, f.lstrip('/')))
+ ]
+ if not existing_options:
+ log_warn(f"No source file found among options for {final_name}. Skipping.")
+ continue
+
+ if len(existing_options) == 1:
+ chosen = existing_options[0]
+ else:
+ log_info(f"Choose which file to copy as '{final_name}':")
+ for idx, opt in enumerate(existing_options, 1):
+ print(f"{idx}: {opt}")
+ while True:
+ try:
+ choice_idx = int(input(f"{CYAN}Enter number (1-{len(existing_options)}): {RESET}"))
+ if 1 <= choice_idx <= len(existing_options):
+ chosen = existing_options[choice_idx - 1]
+ break
+ except ValueError:
+ pass
+ log_warn("Invalid choice, try again.")
+
+ src_file = os.path.join(src_root, chosen.lstrip('/'))
+ dst_file = os.path.join(dst_root, final_name.lstrip('/'))
+
+ if os.path.exists(dst_file):
+ if not override_all and not ask_override(dst_file):
+ log_warn(f"Skipped overriding {dst_file}")
+ continue
+
+ shutil.copy2(src_file, dst_file)
+ log_success(f"Copied {chosen} -> {final_name}")
+
+def copy_with_structure(src_root, dst_root, override_all=False):
+ for root, dirs, files in os.walk(src_root):
+ rel_path = os.path.relpath(root, src_root)
+ target_dir = os.path.join(dst_root, rel_path) if rel_path != '.' else dst_root
+
+ os.makedirs(target_dir, exist_ok=True)
+
+ for file in files:
+ src_file = os.path.join(root, file)
+ dst_file = os.path.join(target_dir, file)
+
+ if os.path.exists(dst_file):
+ if not override_all and not ask_override(dst_file):
+ log_warn(f"Skipped overriding {dst_file}")
+ continue
+
+ shutil.copy2(src_file, dst_file)
+ log_success(f"Copied: {src_file} -> {dst_file}")
+
+def require_root():
+ if os.geteuid() != 0:
+ log_error("This script must be run as root (sudo). Exiting.")
+ sys.exit(1)