diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fc9ef66 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ideskpet-installer" +version = "1.0.0" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a1feaa1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ideskpet-installer" +version = "1.0.0" +edition = "2021" +description = "Installer and CLI for I-DeskPet desktop pet application" +authors = ["InoriShio"] +license = "MIT" + +[[bin]] +name = "ideskpet-installer" +path = "src/main.rs" + +[[bin]] +name = "ideskpet" +path = "src/bin/ideskpet.rs" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/ideskpet b/ideskpet new file mode 100644 index 0000000..6be033e --- /dev/null +++ b/ideskpet @@ -0,0 +1,379 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# ideskpet - CLI tool for I-DeskPet Quickshell application +# ============================================================================= + +VERSION="1.0.0" +REPO_URL="https://github.com/InoriShio/I-DeskPet" +INSTALL_DIR="/etc/xdg/quickshell/I-DeskPet" +LOG_DIR="$HOME/.local/state/ideskpet" +LOG_FILE="$LOG_DIR/ideskpet.log" +APP_ID="I-DeskPet" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +# ============================================================================= +# Helper Functions +# ============================================================================= + +print_help() { + cat << EOF +${BOLD}ideskpet${NC} - CLI tool for I-DeskPet desktop pet application (v${VERSION}) + +${BOLD}USAGE:${NC} + ideskpet [OPTIONS] + +${BOLD}COMMANDS:${NC} + ${GREEN}start${NC} Start the I-DeskPet application + ${GREEN}stop${NC} Stop the running I-DeskPet instance + ${GREEN}restart${NC} Restart the I-DeskPet application + ${GREEN}log${NC} Show application logs + ${GREEN}update${NC} Update I-DeskPet from GitHub + +${BOLD}HYPRLAND SHORTCUTS:${NC} + ${BLUE}toggle-layer${NC} Toggle pet between overlay/bottom layer + ${BLUE}toggle-region${NC} Toggle click-through mode + ${BLUE}cycle-zindex${NC} Cycle z-index of hovered gif + +${BOLD}LOG OPTIONS:${NC} + ideskpet log Show last 50 lines of logs + ideskpet log -f Follow logs in real-time (Ctrl+C to exit) + ideskpet log -n Show last N lines of logs + +${BOLD}OTHER:${NC} + -h, --help Show this help message + -v, --version Show version + +${BOLD}CONFIGURATION:${NC} + User config: ~/.config/I-DeskPet/config.json + + Example config.json: + { + "gifFolder": "/home/user/Pictures/Pets", + "maxScaling": 1 + } + +${BOLD}HYPRLAND KEYBIND EXAMPLES:${NC} + bind = CTRL, mouse:274, global, I-DeskPet:toggle-Region + bind = SHIFT, mouse:274, global, I-DeskPet:toggle-Layer + bind = \$mainMod, Z, global, I-DeskPet:cycle-zIndex + +${BOLD}INSTALLATION:${NC} + sudo cp ideskpet /usr/bin/ideskpet + sudo chmod +x /usr/bin/ideskpet + +EOF +} + +print_version() { + echo "ideskpet v${VERSION}" +} + +info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +is_running() { + pgrep -f "quickshell.*${APP_ID}" > /dev/null 2>&1 +} + +get_pid() { + pgrep -f "quickshell.*${APP_ID}" 2>/dev/null || true +} + +ensure_log_dir() { + if [[ ! -d "$LOG_DIR" ]]; then + mkdir -p "$LOG_DIR" + fi +} + +ensure_installed() { + if [[ ! -d "$INSTALL_DIR" ]]; then + warn "I-DeskPet is not installed at ${INSTALL_DIR}" + info "Cloning from ${REPO_URL}..." + + # Check if /etc/xdg/quickshell exists + if [[ ! -d "/etc/xdg/quickshell" ]]; then + info "Creating /etc/xdg/quickshell directory (requires sudo)..." + sudo mkdir -p /etc/xdg/quickshell + fi + + info "Cloning repository (requires sudo)..." + sudo git clone "$REPO_URL" "$INSTALL_DIR" + + if [[ $? -eq 0 ]]; then + info "Successfully installed I-DeskPet to ${INSTALL_DIR}" + else + error "Failed to clone repository" + exit 1 + fi + fi +} + +check_dependencies() { + local missing=() + + if ! command -v quickshell &> /dev/null; then + missing+=("quickshell") + fi + + if ! command -v hyprctl &> /dev/null; then + missing+=("hyprctl (hyprland)") + fi + + if ! command -v git &> /dev/null; then + missing+=("git") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + error "Missing dependencies: ${missing[*]}" + echo "Please install the missing packages and try again." + exit 1 + fi +} + +# ============================================================================= +# Command Implementations +# ============================================================================= + +cmd_start() { + check_dependencies + ensure_installed + ensure_log_dir + + if is_running; then + warn "I-DeskPet is already running (PID: $(get_pid))" + echo "Use 'ideskpet restart' to restart, or 'ideskpet stop' to stop." + exit 1 + fi + + info "Starting I-DeskPet..." + + # Start quickshell in background, redirect output to log file + nohup quickshell -p "$INSTALL_DIR" >> "$LOG_FILE" 2>&1 & + disown + + # Wait a moment and check if it started successfully + sleep 1 + + if is_running; then + info "I-DeskPet started successfully (PID: $(get_pid))" + echo "Use 'ideskpet log -f' to view logs" + else + error "Failed to start I-DeskPet" + echo "Check logs with 'ideskpet log' for details" + exit 1 + fi +} + +cmd_stop() { + if ! is_running; then + warn "I-DeskPet is not running" + exit 0 + fi + + local pid + pid=$(get_pid) + + info "Stopping I-DeskPet (PID: ${pid})..." + + # Try graceful shutdown first + kill "$pid" 2>/dev/null || true + + # Wait up to 5 seconds for graceful shutdown + local count=0 + while is_running && [[ $count -lt 10 ]]; do + sleep 0.5 + ((count++)) + done + + # Force kill if still running + if is_running; then + warn "Graceful shutdown failed, force killing..." + kill -9 "$pid" 2>/dev/null || true + sleep 0.5 + fi + + if is_running; then + error "Failed to stop I-DeskPet" + exit 1 + else + info "I-DeskPet stopped successfully" + fi +} + +cmd_restart() { + info "Restarting I-DeskPet..." + + if is_running; then + cmd_stop + fi + + sleep 0.5 + cmd_start +} + +cmd_log() { + ensure_log_dir + + if [[ ! -f "$LOG_FILE" ]]; then + warn "No log file found at ${LOG_FILE}" + echo "Start I-DeskPet first with 'ideskpet start'" + exit 1 + fi + + local follow=false + local lines=50 + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + -f|--follow) + follow=true + shift + ;; + -n|--lines) + if [[ -n "${2:-}" ]] && [[ "$2" =~ ^[0-9]+$ ]]; then + lines="$2" + shift 2 + else + error "Option -n requires a numeric argument" + exit 1 + fi + ;; + *) + error "Unknown log option: $1" + echo "Usage: ideskpet log [-f] [-n ]" + exit 1 + ;; + esac + done + + if [[ "$follow" == true ]]; then + info "Following logs (Ctrl+C to exit)..." + echo "---" + tail -n "$lines" -f "$LOG_FILE" + else + tail -n "$lines" "$LOG_FILE" + fi +} + +cmd_update() { + check_dependencies + + if [[ ! -d "$INSTALL_DIR" ]]; then + error "I-DeskPet is not installed at ${INSTALL_DIR}" + echo "Run 'ideskpet start' to install it first." + exit 1 + fi + + info "Updating I-DeskPet from GitHub..." + + cd "$INSTALL_DIR" + + # Check for local changes + if ! sudo git diff --quiet 2>/dev/null; then + warn "Local changes detected. They may be overwritten." + fi + + sudo git pull + + if [[ $? -eq 0 ]]; then + info "Update completed successfully" + + if is_running; then + echo "" + warn "I-DeskPet is currently running." + echo "Run 'ideskpet restart' to apply changes." + fi + else + error "Update failed" + exit 1 + fi +} + +cmd_shortcut() { + local shortcut="$1" + + if ! command -v hyprctl &> /dev/null; then + error "hyprctl not found. Are you running Hyprland?" + exit 1 + fi + + if ! is_running; then + warn "I-DeskPet is not running" + echo "Start it first with 'ideskpet start'" + exit 1 + fi + + info "Triggering shortcut: ${APP_ID}:${shortcut}" + hyprctl dispatch global "${APP_ID}:${shortcut}" +} + +# ============================================================================= +# Main Entry Point +# ============================================================================= + +main() { + local cmd="${1:-}" + + case "$cmd" in + start) + cmd_start + ;; + stop) + cmd_stop + ;; + restart) + cmd_restart + ;; + log) + shift + cmd_log "$@" + ;; + update) + cmd_update + ;; + toggle-layer) + cmd_shortcut "toggle-Layer" + ;; + toggle-region) + cmd_shortcut "toggle-Region" + ;; + cycle-zindex) + cmd_shortcut "cycle-zIndex" + ;; + -h|--help|help) + print_help + ;; + -v|--version|version) + print_version + ;; + "") + print_help + ;; + *) + error "Unknown command: ${cmd}" + echo "Run 'ideskpet --help' for usage information." + exit 1 + ;; + esac +} + +main "$@" diff --git a/src/bin/ideskpet.rs b/src/bin/ideskpet.rs new file mode 100644 index 0000000..f4b199d --- /dev/null +++ b/src/bin/ideskpet.rs @@ -0,0 +1,490 @@ +//! I-DeskPet CLI Tool +//! +//! A command-line interface for managing the I-DeskPet desktop pet application. +//! +//! Usage: +//! ideskpet [options] +//! +//! Commands: +//! start, stop, restart, log, update, toggle-layer, toggle-region, cycle-zindex + +#[cfg(unix)] +use std::env; +#[cfg(unix)] +use std::fs; +#[cfg(unix)] +use std::io::{BufRead, BufReader}; +#[cfg(unix)] +use std::path::PathBuf; +#[cfg(unix)] +use std::process::{Command, Stdio}; +#[cfg(unix)] +use std::thread; +#[cfg(unix)] +use std::time::Duration; + +#[cfg(unix)] +const VERSION: &str = "1.0.0"; +#[cfg(unix)] +const APP_ID: &str = "I-DeskPet"; +#[cfg(unix)] +const INSTALL_DIR: &str = "/etc/xdg/quickshell/I-DeskPet"; +#[cfg(unix)] +const REPO_URL: &str = "https://github.com/InoriShio/I-DeskPet"; + +// ANSI color codes +#[cfg(unix)] +const RED: &str = "\x1b[31m"; +#[cfg(unix)] +const GREEN: &str = "\x1b[32m"; +#[cfg(unix)] +const YELLOW: &str = "\x1b[33m"; +#[cfg(unix)] +const BLUE: &str = "\x1b[34m"; +#[cfg(unix)] +const BOLD: &str = "\x1b[1m"; +#[cfg(unix)] +const NC: &str = "\x1b[0m"; + +fn main() { + #[cfg(not(unix))] + { + eprintln!("\x1b[31m[ERROR]\x1b[0m ideskpet only works on Linux/Unix systems."); + std::process::exit(1); + } + + #[cfg(unix)] + { + let args: Vec = env::args().collect(); + run_cli(&args); + } +} + +#[cfg(unix)] +fn run_cli(args: &[String]) { + if args.len() < 2 { + print_help(); + return; + } + + let command = args[1].as_str(); + + match command { + "start" => cmd_start(), + "stop" => cmd_stop(), + "restart" => cmd_restart(), + "log" => cmd_log(&args[2..]), + "update" => cmd_update(), + "toggle-layer" => cmd_shortcut("toggle-Layer"), + "toggle-region" => cmd_shortcut("toggle-Region"), + "cycle-zindex" => cmd_shortcut("cycle-zIndex"), + "-h" | "--help" | "help" => print_help(), + "-v" | "--version" | "version" => println!("ideskpet v{VERSION}"), + _ => { + eprintln!("{RED}[ERROR]{NC} Unknown command: {command}"); + eprintln!("Run 'ideskpet --help' for usage information."); + std::process::exit(1); + } + } +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +#[cfg(unix)] +fn get_log_dir() -> PathBuf { + let home = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join(".local/state/ideskpet") +} + +#[cfg(unix)] +fn get_log_file() -> PathBuf { + get_log_dir().join("ideskpet.log") +} + +#[cfg(unix)] +fn ensure_log_dir() { + let log_dir = get_log_dir(); + if !log_dir.exists() { + if let Err(e) = fs::create_dir_all(&log_dir) { + eprintln!("{YELLOW}[WARN]{NC} Failed to create log directory: {e}"); + } + } +} + +#[cfg(unix)] +fn is_running() -> bool { + get_pid().is_some() +} + +#[cfg(unix)] +fn get_pid() -> Option { + let output = Command::new("pgrep") + .args(["-f", &format!("quickshell.*{APP_ID}")]) + .output() + .ok()?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().next()?.trim().parse().ok() + } else { + None + } +} + +#[cfg(unix)] +fn check_dependency(name: &str) -> bool { + Command::new("which") + .arg(name) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +#[cfg(unix)] +fn check_dependencies() -> bool { + let mut ok = true; + let deps = ["quickshell", "hyprctl", "git"]; + + for dep in deps { + if !check_dependency(dep) { + eprintln!("{RED}[ERROR]{NC} Missing dependency: {dep}"); + ok = false; + } + } + + if !ok { + eprintln!("\nPlease install the missing dependencies and try again."); + } + + ok +} + +// ============================================================================= +// Commands +// ============================================================================= + +#[cfg(unix)] +fn cmd_start() { + if !check_dependency("quickshell") { + eprintln!("{RED}[ERROR]{NC} quickshell is not installed"); + std::process::exit(1); + } + + if is_running() { + let pid = get_pid().unwrap(); + eprintln!("{YELLOW}[WARN]{NC} I-DeskPet is already running (PID: {pid})"); + eprintln!("Use 'ideskpet restart' to restart, or 'ideskpet stop' to stop."); + std::process::exit(1); + } + + // Check if install directory exists + if !std::path::Path::new(INSTALL_DIR).exists() { + eprintln!("{RED}[ERROR]{NC} I-DeskPet is not installed at {INSTALL_DIR}"); + eprintln!("Please run the installer first: cargo run"); + std::process::exit(1); + } + + ensure_log_dir(); + let log_file = get_log_file(); + + println!("{GREEN}[INFO]{NC} Starting I-DeskPet..."); + + // Open log file for appending + let log_handle = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_file); + + let (stdout_file, stderr_file) = match log_handle { + Ok(f) => { + let f2 = f.try_clone().unwrap_or_else(|_| { + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_file) + .unwrap() + }); + (Stdio::from(f), Stdio::from(f2)) + } + Err(e) => { + eprintln!("{YELLOW}[WARN]{NC} Could not open log file: {e}"); + (Stdio::null(), Stdio::null()) + } + }; + + // Spawn quickshell process + let result = Command::new("nohup") + .args(["quickshell", "-p", INSTALL_DIR]) + .stdout(stdout_file) + .stderr(stderr_file) + .stdin(Stdio::null()) + .spawn(); + + match result { + Ok(_child) => { + // Wait a moment to check if it started + thread::sleep(Duration::from_secs(1)); + + if is_running() { + let pid = get_pid().unwrap_or(0); + println!("{GREEN}[OK]{NC} I-DeskPet started successfully (PID: {pid})"); + println!("Use 'ideskpet log -f' to view logs"); + } else { + eprintln!("{RED}[ERROR]{NC} I-DeskPet failed to start"); + eprintln!("Check logs with 'ideskpet log' for details"); + std::process::exit(1); + } + } + Err(e) => { + eprintln!("{RED}[ERROR]{NC} Failed to start quickshell: {e}"); + std::process::exit(1); + } + } +} + +#[cfg(unix)] +fn cmd_stop() { + if !is_running() { + println!("{YELLOW}[WARN]{NC} I-DeskPet is not running"); + return; + } + + let pid = get_pid().unwrap(); + println!("{GREEN}[INFO]{NC} Stopping I-DeskPet (PID: {pid})..."); + + // Send SIGTERM + let _ = Command::new("kill").arg(pid.to_string()).status(); + + // Wait for graceful shutdown (up to 5 seconds) + for _ in 0..10 { + thread::sleep(Duration::from_millis(500)); + if !is_running() { + println!("{GREEN}[OK]{NC} I-DeskPet stopped successfully"); + return; + } + } + + // Force kill if still running + println!("{YELLOW}[WARN]{NC} Graceful shutdown failed, force killing..."); + let _ = Command::new("kill").args(["-9", &pid.to_string()]).status(); + + thread::sleep(Duration::from_millis(500)); + + if is_running() { + eprintln!("{RED}[ERROR]{NC} Failed to stop I-DeskPet"); + std::process::exit(1); + } else { + println!("{GREEN}[OK]{NC} I-DeskPet stopped successfully"); + } +} + +#[cfg(unix)] +fn cmd_restart() { + println!("{GREEN}[INFO]{NC} Restarting I-DeskPet..."); + + if is_running() { + cmd_stop(); + } + + thread::sleep(Duration::from_millis(500)); + cmd_start(); +} + +#[cfg(unix)] +fn cmd_log(args: &[String]) { + let log_file = get_log_file(); + + if !log_file.exists() { + eprintln!( + "{YELLOW}[WARN]{NC} No log file found at {}", + log_file.display() + ); + eprintln!("Start I-DeskPet first with 'ideskpet start'"); + std::process::exit(1); + } + + let mut follow = false; + let mut lines: u32 = 50; + + // Parse arguments + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "-f" | "--follow" => { + follow = true; + } + "-n" | "--lines" => { + if i + 1 < args.len() { + if let Ok(n) = args[i + 1].parse() { + lines = n; + i += 1; + } else { + eprintln!("{RED}[ERROR]{NC} Invalid number for -n option"); + std::process::exit(1); + } + } else { + eprintln!("{RED}[ERROR]{NC} -n option requires a number"); + std::process::exit(1); + } + } + _ => { + eprintln!("{RED}[ERROR]{NC} Unknown log option: {}", args[i]); + eprintln!("Usage: ideskpet log [-f] [-n ]"); + std::process::exit(1); + } + } + i += 1; + } + + if follow { + println!("{GREEN}[INFO]{NC} Following logs (Ctrl+C to exit)..."); + println!("---"); + + let status = Command::new("tail") + .args(["-n", &lines.to_string(), "-f", log_file.to_str().unwrap()]) + .status(); + + if let Err(e) = status { + eprintln!("{RED}[ERROR]{NC} Failed to tail log file: {e}"); + std::process::exit(1); + } + } else { + let status = Command::new("tail") + .args(["-n", &lines.to_string(), log_file.to_str().unwrap()]) + .status(); + + if let Err(e) = status { + eprintln!("{RED}[ERROR]{NC} Failed to read log file: {e}"); + std::process::exit(1); + } + } +} + +#[cfg(unix)] +fn cmd_update() { + if !check_dependency("git") { + eprintln!("{RED}[ERROR]{NC} git is not installed"); + std::process::exit(1); + } + + if !std::path::Path::new(INSTALL_DIR).exists() { + eprintln!("{RED}[ERROR]{NC} I-DeskPet is not installed at {INSTALL_DIR}"); + eprintln!("Please run the installer first: cargo run"); + std::process::exit(1); + } + + println!("{GREEN}[INFO]{NC} Updating I-DeskPet from GitHub..."); + + let status = Command::new("sudo") + .args(["git", "-C", INSTALL_DIR, "pull"]) + .status(); + + match status { + Ok(s) if s.success() => { + println!("{GREEN}[OK]{NC} Update completed successfully"); + + if is_running() { + println!(); + println!("{YELLOW}[WARN]{NC} I-DeskPet is currently running."); + println!("Run 'ideskpet restart' to apply changes."); + } + } + Ok(_) => { + eprintln!("{RED}[ERROR]{NC} Git pull failed"); + std::process::exit(1); + } + Err(e) => { + eprintln!("{RED}[ERROR]{NC} Failed to run git: {e}"); + std::process::exit(1); + } + } +} + +#[cfg(unix)] +fn cmd_shortcut(shortcut: &str) { + if !check_dependency("hyprctl") { + eprintln!("{RED}[ERROR]{NC} hyprctl not found. Are you running Hyprland?"); + std::process::exit(1); + } + + if !is_running() { + eprintln!("{YELLOW}[WARN]{NC} I-DeskPet is not running"); + eprintln!("Start it first with 'ideskpet start'"); + std::process::exit(1); + } + + let shortcut_full = format!("{APP_ID}:{shortcut}"); + println!("{GREEN}[INFO]{NC} Triggering shortcut: {shortcut_full}"); + + let status = Command::new("hyprctl") + .args(["dispatch", "global", &shortcut_full]) + .status(); + + match status { + Ok(s) if s.success() => { + println!("{GREEN}[OK]{NC} Shortcut triggered"); + } + Ok(_) => { + eprintln!("{RED}[ERROR]{NC} Failed to trigger shortcut"); + std::process::exit(1); + } + Err(e) => { + eprintln!("{RED}[ERROR]{NC} Failed to run hyprctl: {e}"); + std::process::exit(1); + } + } +} + +#[cfg(unix)] +fn print_help() { + println!( + "{BOLD}ideskpet{NC} - CLI tool for I-DeskPet desktop pet application (v{VERSION}) + +{BOLD}USAGE:{NC} + ideskpet [OPTIONS] + +{BOLD}COMMANDS:{NC} + {GREEN}start{NC} Start the I-DeskPet application + {GREEN}stop{NC} Stop the running I-DeskPet instance + {GREEN}restart{NC} Restart the I-DeskPet application + {GREEN}log{NC} Show application logs + {GREEN}update{NC} Update I-DeskPet from GitHub + +{BOLD}HYPRLAND SHORTCUTS:{NC} + {BLUE}toggle-layer{NC} Toggle pet between overlay/bottom layer + {BLUE}toggle-region{NC} Toggle click-through mode + {BLUE}cycle-zindex{NC} Cycle z-index of hovered gif + +{BOLD}LOG OPTIONS:{NC} + ideskpet log Show last 50 lines of logs + ideskpet log -f Follow logs in real-time (Ctrl+C to exit) + ideskpet log -n Show last N lines of logs + +{BOLD}OTHER:{NC} + -h, --help Show this help message + -v, --version Show version + +{BOLD}CONFIGURATION:{NC} + User config: ~/.config/I-DeskPet/config.json + + Example config.json: + {{ + \"gifFolder\": \"/home/user/Pictures/Pets\", + \"maxScaling\": 1 + }} + +{BOLD}HYPRLAND KEYBIND EXAMPLES:{NC} + bind = CTRL, mouse:274, global, I-DeskPet:toggle-Region + bind = SHIFT, mouse:274, global, I-DeskPet:toggle-Layer + bind = $mainMod, Z, global, I-DeskPet:cycle-zIndex + +{BOLD}OTHER KEYBINDS:{NC} + Double click Reset gif size to original + Scroll Scale the gif up or down +" + ); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..538d890 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,186 @@ +//! I-DeskPet Installer +//! +//! This installer builds the ideskpet CLI binary, clones/updates the repo, +//! and installs everything to the appropriate system locations. +//! +//! Run with `cargo run` on your Arch Linux machine. + +#[cfg(unix)] +use std::path::Path; +#[cfg(unix)] +use std::process::{Command, ExitStatus}; + +#[cfg(unix)] +const REPO_URL: &str = "https://github.com/InoriShio/I-DeskPet"; +#[cfg(unix)] +const INSTALL_DIR: &str = "/etc/xdg/quickshell/I-DeskPet"; +#[cfg(unix)] +const BINARY_DEST: &str = "/usr/bin/ideskpet"; + +// ANSI color codes +#[cfg(unix)] +const RED: &str = "\x1b[31m"; +#[cfg(unix)] +const GREEN: &str = "\x1b[32m"; +#[cfg(unix)] +const YELLOW: &str = "\x1b[33m"; +#[cfg(unix)] +const BLUE: &str = "\x1b[34m"; +#[cfg(unix)] +const BOLD: &str = "\x1b[1m"; +#[cfg(unix)] +const NC: &str = "\x1b[0m"; + +fn main() { + #[cfg(not(unix))] + { + eprintln!("\x1b[31m[ERROR]\x1b[0m This installer only works on Linux/Unix systems."); + eprintln!("Please run this on your Arch Linux machine."); + std::process::exit(1); + } + + #[cfg(unix)] + run_installer(); +} + +#[cfg(unix)] +fn run_installer() { + println!("{BOLD}=== I-DeskPet Installer ==={NC}\n"); + + // Step 1: Build the ideskpet binary + if !build_binary() { + std::process::exit(1); + } + + // Step 2: Clone or update the repository + if !setup_repository() { + std::process::exit(1); + } + + // Step 3: Install the binary to /usr/bin + if !install_binary() { + std::process::exit(1); + } + + // Success! + println!("\n{GREEN}{BOLD}=== Installation Complete ==={NC}\n"); + print_usage(); +} + +#[cfg(unix)] +fn build_binary() -> bool { + println!("{BLUE}[1/3]{NC} Building ideskpet binary..."); + + let status = Command::new("cargo") + .args(["build", "--release", "--bin", "ideskpet"]) + .status(); + + match status { + Ok(s) if s.success() => { + println!("{GREEN}[OK]{NC} Binary built successfully\n"); + true + } + Ok(_) => { + eprintln!("{RED}[ERROR]{NC} Failed to build binary"); + false + } + Err(e) => { + eprintln!("{RED}[ERROR]{NC} Failed to run cargo: {e}"); + false + } + } +} + +#[cfg(unix)] +fn setup_repository() -> bool { + println!("{BLUE}[2/3]{NC} Setting up repository at {INSTALL_DIR}..."); + + let install_path = Path::new(INSTALL_DIR); + let parent_dir = install_path + .parent() + .unwrap_or(Path::new("/etc/xdg/quickshell")); + + // Check if parent directory exists, create if not + if !parent_dir.exists() { + println!(" Creating directory {parent_dir:?} (requires sudo)..."); + if !run_sudo(&["mkdir", "-p", parent_dir.to_str().unwrap()]) { + return false; + } + } + + if install_path.exists() { + // Repository exists, do git pull + println!(" Repository exists, updating with git pull..."); + if !run_sudo(&["git", "-C", INSTALL_DIR, "pull"]) { + eprintln!("{YELLOW}[WARN]{NC} Git pull failed, continuing anyway..."); + } else { + println!("{GREEN}[OK]{NC} Repository updated\n"); + } + } else { + // Clone the repository + println!(" Cloning from {REPO_URL}..."); + if !run_sudo(&["git", "clone", REPO_URL, INSTALL_DIR]) { + eprintln!("{RED}[ERROR]{NC} Failed to clone repository"); + return false; + } + println!("{GREEN}[OK]{NC} Repository cloned\n"); + } + + true +} + +#[cfg(unix)] +fn install_binary() -> bool { + println!("{BLUE}[3/3]{NC} Installing binary to {BINARY_DEST}..."); + + // Get the path to the built binary + let binary_src = "target/release/ideskpet"; + + if !Path::new(binary_src).exists() { + eprintln!("{RED}[ERROR]{NC} Built binary not found at {binary_src}"); + return false; + } + + // Copy binary to /usr/bin + println!(" Copying binary (requires sudo)..."); + if !run_sudo(&["cp", binary_src, BINARY_DEST]) { + eprintln!("{RED}[ERROR]{NC} Failed to copy binary"); + return false; + } + + // Set executable permissions + println!(" Setting executable permissions..."); + if !run_sudo(&["chmod", "+x", BINARY_DEST]) { + eprintln!("{RED}[ERROR]{NC} Failed to set permissions"); + return false; + } + + println!("{GREEN}[OK]{NC} Binary installed to {BINARY_DEST}"); + true +} + +#[cfg(unix)] +fn run_sudo(args: &[&str]) -> bool { + let status = Command::new("sudo").args(args).status(); + + matches!(status, Ok(s) if s.success()) +} + +#[cfg(unix)] +fn print_usage() { + println!("{BOLD}Usage:{NC}"); + println!(" ideskpet start Start the desktop pet"); + println!(" ideskpet stop Stop the desktop pet"); + println!(" ideskpet restart Restart the desktop pet"); + println!(" ideskpet log View last 50 lines of logs"); + println!(" ideskpet log -f Follow logs in real-time"); + println!(" ideskpet log -n View last N lines of logs"); + println!(" ideskpet update Update from GitHub"); + println!(" ideskpet toggle-layer Toggle overlay/bottom layer"); + println!(" ideskpet toggle-region Toggle click-through mode"); + println!(" ideskpet cycle-zindex Cycle z-index of hovered gif"); + println!(" ideskpet --help Show full help"); + println!(); + println!("{BOLD}Get started:{NC}"); + println!(" Run {GREEN}ideskpet start{NC} to launch your desktop pet!"); +}