diff --git a/README.md b/README.md index e03e1b8..c7ca5e4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,16 @@ Personal Hyprland configuration focused on productivity and ergonomics. --- -## Complete Installation Guide +## Quick Install + +```bash +git clone https://github.com/duma799/hyprduma-config.git ~/Downloads/hyprduma-config +python3 ~/Downloads/hyprduma-config/install.py +``` + +--- + +## Complete Installation Guide (Manual) Follow these steps in order to install the complete setup from scratch. @@ -77,7 +86,7 @@ caelestia shell -d & ```bash # Clone to a temporary location cd ~/Downloads -git clone https://github.com/duma97/hyprduma-config.git +git clone https://github.com/duma799/hyprduma-config.git cd hyprduma-config ``` diff --git a/install.py b/install.py new file mode 100755 index 0000000..29f7e11 --- /dev/null +++ b/install.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +""" +HyprDuma Config Auto-Installer +Automates the complete installation of HyprDuma dotfiles. +Run: python3 install.py +""" + +import subprocess +import sys +import shutil +from pathlib import Path + +# --- Colors --- +RESET = "\033[0m" +BOLD = "\033[1m" +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +MAGENTA = "\033[35m" +CYAN = "\033[36m" + +REPO_URL = "https://github.com/duma799/hyprduma-config.git" + +PACMAN_PACKAGES = [ + "hyprland", "hyprlock", "hyprshot", "wlogout", "kitty", "waybar", + "swaybg", "waypaper", "wofi", "nautilus", "wireplumber", + "pipewire-pulse", "brightnessctl", "playerctl", "adwaita-cursors", + "python-pywal", +] + + +def print_step(num, total, msg): + print(f"\n{BOLD}{BLUE}[{num}/{total}]{RESET} {BOLD}{msg}{RESET}") + + +def print_ok(msg): + print(f" {GREEN}✓{RESET} {msg}") + + +def print_warn(msg): + print(f" {YELLOW}!{RESET} {msg}") + + +def print_err(msg): + print(f" {RED}✗{RESET} {msg}") + + +def print_info(msg): + print(f" {CYAN}→{RESET} {msg}") + + +def ask_yn(prompt, default=True): + suffix = " [Y/n] " if default else " [y/N] " + try: + answer = input(f" {MAGENTA}?{RESET} {prompt}{suffix}").strip().lower() + except (EOFError, KeyboardInterrupt): + print() + sys.exit(1) + if not answer: + return default + return answer in ("y", "yes") + + +def run(cmd, check=True, capture=False, **kwargs): + """Run a shell command.""" + if capture: + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, **kwargs + ) + if check and result.returncode != 0: + return None + return result.stdout.strip() + else: + result = subprocess.run(cmd, shell=True, **kwargs) + if check and result.returncode != 0: + return False + return result.returncode == 0 + + +def cmd_exists(name): + return shutil.which(name) is not None + + +def find_repo_dir(): + """Determine the repo directory - either CWD or needs cloning.""" + cwd = Path.cwd() + # Check if we're inside the cloned repo + if (cwd / "hyprland.conf").exists() and (cwd / "pywal.sh").exists(): + return cwd + # Check if install.py is in the repo + script_dir = Path(__file__).resolve().parent + if (script_dir / "hyprland.conf").exists() and (script_dir / "pywal.sh").exists(): + return script_dir + return None + + +def check_arch(): + """Verify we're on Arch Linux.""" + if not Path("/etc/arch-release").exists(): + print_err("This installer is designed for Arch Linux (or Arch-based distros).") + if not ask_yn("Continue anyway?", default=False): + sys.exit(1) + + +def install_aur_helpers(): + """Step: Install yay and paru (optional).""" + has_yay = cmd_exists("yay") + has_paru = cmd_exists("paru") + + if has_yay and has_paru: + print_ok("yay and paru are already installed") + return + + if has_yay: + print_ok("yay is already installed") + if has_paru: + print_ok("paru is already installed") + + missing = [] + if not has_yay: + missing.append("yay") + if not has_paru: + missing.append("paru") + + if not ask_yn(f"Install AUR helper(s): {', '.join(missing)}?"): + print_warn("Skipping AUR helpers (caelestia-shell will require manual install)") + return + + run("sudo pacman -S --needed --noconfirm git base-devel") + + for helper in missing: + print_info(f"Building {helper} from AUR...") + build_dir = Path(f"/tmp/{helper}-build") + if build_dir.exists(): + shutil.rmtree(build_dir) + ok = run( + f"git clone https://aur.archlinux.org/{helper}.git {build_dir}" + f" && cd {build_dir} && makepkg -si --noconfirm" + ) + if ok: + print_ok(f"{helper} installed") + else: + print_err(f"Failed to install {helper} - you can install it manually later") + if build_dir.exists(): + shutil.rmtree(build_dir) + + +def install_packages(): + """Step: Install required packages via pacman.""" + print_info(f"Packages: {' '.join(PACMAN_PACKAGES)}") + + if not ask_yn("Install required packages via pacman?"): + print_warn("Skipping package installation") + return + + pkg_str = " ".join(PACMAN_PACKAGES) + if not run(f"sudo pacman -S --needed --noconfirm {pkg_str}"): + print_err("Some packages failed to install - check output above") + else: + print_ok("All packages installed") + + +def install_caelestia(): + """Step: Install Caelestia Shell (optional).""" + if cmd_exists("caelestia"): + print_ok("Caelestia shell is already installed") + return + + if not cmd_exists("yay") and not cmd_exists("paru"): + print_warn("No AUR helper found - skipping Caelestia (install manually: yay -S caelestia-shell)") + return + + if not ask_yn("Install Caelestia Shell (recommended for dynamic theming)?"): + print_warn("Skipping Caelestia shell") + return + + helper = "yay" if cmd_exists("yay") else "paru" + if run(f"{helper} -S --noconfirm caelestia-shell"): + print_ok("Caelestia shell installed") + else: + print_err("Failed to install Caelestia - you can try manually: yay -S caelestia-shell") + + +def clone_repo(): + """Step: Clone the repository if needed. Returns repo Path.""" + repo_dir = find_repo_dir() + if repo_dir: + print_ok(f"Using repo at: {repo_dir}") + return repo_dir + + clone_target = Path.home() / "Downloads" / "hyprduma-config" + print_info(f"Cloning to {clone_target}") + + if clone_target.exists(): + if ask_yn(f"{clone_target} already exists. Remove and re-clone?"): + shutil.rmtree(clone_target) + else: + if (clone_target / "hyprland.conf").exists(): + return clone_target + print_err("Directory exists but doesn't contain config files") + sys.exit(1) + + if run(f"git clone {REPO_URL} {clone_target}"): + print_ok("Repository cloned") + return clone_target + else: + print_err("Failed to clone repository") + sys.exit(1) + + +def backup_configs(): + """Step: Backup existing configs.""" + config = Path.home() / ".config" + backed_up = [] + + for name in ["hypr", "waybar", "wlogout"]: + src = config / name + dst = config / f"{name}.backup" + if src.exists() and not src.is_symlink(): + if dst.exists(): + print_warn(f"{dst} already exists - skipping backup of {name}") + continue + shutil.move(str(src), str(dst)) + backed_up.append(name) + + if backed_up: + print_ok(f"Backed up: {', '.join(f'~/.config/{n}' for n in backed_up)}") + else: + print_ok("No existing configs to back up") + + +def install_hypr_config(repo): + """Step: Copy Hyprland configuration files.""" + hypr_dir = Path.home() / ".config" / "hypr" + hypr_dir.mkdir(parents=True, exist_ok=True) + + # Copy main config + shutil.copy2(repo / "hyprland.conf", hypr_dir / "hyprland.conf") + print_ok("Copied hyprland.conf") + + # Create screenshots directory + screenshots = Path.home() / "Pictures" / "Screenshots" + screenshots.mkdir(parents=True, exist_ok=True) + print_ok(f"Created {screenshots}") + + # Copy wallpapers + wallpapers_src = repo / "wallpapers" + wallpapers_dst = hypr_dir / "wallpapers" + if wallpapers_src.exists(): + if wallpapers_dst.exists(): + shutil.rmtree(wallpapers_dst) + shutil.copytree(str(wallpapers_src), str(wallpapers_dst)) + count = len(list(wallpapers_dst.iterdir())) + print_ok(f"Copied {count} wallpapers to ~/.config/hypr/wallpapers/") + + +def install_pywal(repo): + """Step: Install pywal integration.""" + home = Path.home() + hypr_dir = home / ".config" / "hypr" + hypr_dir.mkdir(parents=True, exist_ok=True) + + # 1. Pywal templates + templates_dst = home / ".config" / "wal" / "templates" + templates_dst.mkdir(parents=True, exist_ok=True) + templates_src = repo / "wal" / "templates" + if templates_src.exists(): + for f in templates_src.iterdir(): + shutil.copy2(str(f), str(templates_dst / f.name)) + print_ok("Installed pywal templates") + else: + print_err("wal/templates not found in repo") + + # 2. Copy scripts to ~/.config/hypr/ + for script in ["pywal.sh", "sync-caelestia-wallpaper.sh"]: + src = repo / script + dst = hypr_dir / script + if src.exists(): + shutil.copy2(str(src), str(dst)) + dst.chmod(0o755) + print_ok("Copied pywal.sh and sync-caelestia-wallpaper.sh to ~/.config/hypr/") + + # 3. Copy pywal.sh to home for easy access + home_pywal = home / "pywal.sh" + shutil.copy2(str(repo / "pywal.sh"), str(home_pywal)) + home_pywal.chmod(0o755) + print_ok("Copied pywal.sh to ~/pywal.sh") + + # 4. Kitty config + kitty_dir = home / ".config" / "kitty" + kitty_dir.mkdir(parents=True, exist_ok=True) + kitty_src = repo / "kitty" / "kitty.conf" + if kitty_src.exists(): + shutil.copy2(str(kitty_src), kitty_dir / "kitty.conf") + print_ok("Installed kitty config with pywal colors") + + # 5. Bashrc pywal integration + bashrc = home / ".bashrc" + pywal_marker = "# Import pywal colorscheme from cache" + already_configured = False + + if bashrc.exists(): + content = bashrc.read_text() + if pywal_marker in content: + already_configured = True + + if already_configured: + print_ok("~/.bashrc already has pywal integration") + else: + snippet = ( + "\n# Import pywal colorscheme from cache\n" + "(cat ~/.cache/wal/sequences &)\n" + "\n# To add support for TTYs (optional)\n" + "source ~/.cache/wal/colors-tty.sh 2>/dev/null\n" + ) + with open(bashrc, "a") as f: + f.write(snippet) + print_ok("Added pywal integration to ~/.bashrc") + + # 6. Generate initial colors from default wallpaper + wallpaper = hypr_dir / "wallpapers" / "sakura.jpg" + if not wallpaper.exists(): + # Fallback: find any wallpaper + wp_dir = hypr_dir / "wallpapers" + if wp_dir.exists(): + for ext in ("*.jpg", "*.png", "*.jpeg"): + found = list(wp_dir.glob(ext)) + if found: + wallpaper = found[0] + break + + if wallpaper.exists() and cmd_exists("wal"): + print_info(f"Generating pywal colors from {wallpaper.name}...") + run(f'wal -i "{wallpaper}"') + print_ok("Generated initial pywal color scheme") + + # Apply colors + pywal_script = home / "pywal.sh" + if pywal_script.exists(): + if run(f'bash "{pywal_script}"'): + print_ok("Applied pywal colors to all components") + else: + print_warn("pywal.sh had errors (normal if Hyprland isn't running yet)") + elif not cmd_exists("wal"): + print_warn("pywal (wal) not found - install python-pywal and run: wal -i && ~/pywal.sh") + else: + print_warn("No wallpaper found - run manually: wal -i && ~/pywal.sh") + + +def print_banner(): + print(f"""{CYAN}{BOLD} + ╦ ╦╦ ╦╔═╗╦═╗╔╦╗╦ ╦╔╦╗╔═╗ + ╠═╣╚╦╝╠═╝╠╦╝ ║║║ ║║║║╠═╣ + ╩ ╩ ╩ ╩ ╩╚══╩╝╚═╝╩ ╩╩ ╩ + Auto-Installer{RESET} +""") + + +def print_post_install(): + print(f""" +{BOLD}{GREEN}Installation complete!{RESET} + +{BOLD}Next steps:{RESET} + {CYAN}1.{RESET} Edit your app preferences in ~/.config/hypr/hyprland.conf (lines 27-34): + $terminal, $fileManager, $menu, $browser, etc. + + {CYAN}2.{RESET} Adjust monitor config (lines 4-18) if your setup differs. + + {CYAN}3.{RESET} Start Hyprland from TTY: + $ {BOLD}Hyprland{RESET} + + Or if already running, reload with: {BOLD}SUPER + SHIFT + R{RESET} + + {CYAN}4.{RESET} Change wallpaper + colors anytime: + $ wal -i ~/path/to/wallpaper.jpg && ~/pywal.sh + +{BOLD}Backups:{RESET} Original configs saved as ~/.config/.backup +{BOLD}Docs:{RESET} See KEYBINDS.md for all keyboard shortcuts +""") + + +def main(): + print_banner() + + # Pre-flight check + check_arch() + + total = 7 + step = 0 + + # Step 1: AUR helpers + step += 1 + print_step(step, total, "AUR Helpers (yay/paru)") + install_aur_helpers() + + # Step 2: Packages + step += 1 + print_step(step, total, "Install Required Packages") + install_packages() + + # Step 3: Caelestia + step += 1 + print_step(step, total, "Install Caelestia Shell") + install_caelestia() + + # Step 4: Clone / locate repo + step += 1 + print_step(step, total, "Locate/Clone Repository") + repo = clone_repo() + + # Step 5: Backup + step += 1 + print_step(step, total, "Backup Existing Configs") + backup_configs() + + # Step 6: Install configs + step += 1 + print_step(step, total, "Install Configuration Files") + install_hypr_config(repo) + + # Step 7: Pywal + step += 1 + print_step(step, total, "Setup Pywal Integration") + install_pywal(repo) + + print_post_install() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print(f"\n{YELLOW}Interrupted by user{RESET}") + sys.exit(130)