5 Commits

Author SHA1 Message Date
Inorishio 34579d8f31 Fixed hyprland shortcuts 2026-05-16 12:52:09 +02:00
Inorishio 21bf6758c5 Readme update 2026-03-23 15:57:02 +01:00
Inorishio 006fab8cb2 Clean up 2026-03-23 13:45:26 +01:00
Inorishio 2a70f71ae7 binary 2026-03-23 13:28:34 +01:00
Inorishio 4eb82c9f29 binary 2026-03-23 13:27:53 +01:00
13 changed files with 792 additions and 423 deletions
+17
View File
@@ -2,3 +2,20 @@
# will have compiled files and executables # will have compiled files and executables
debug debug
target target
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

Generated
+16
View File
@@ -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"
+18
View File
@@ -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"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

-56
View File
@@ -1,56 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import Quickshell
Singleton {
id: root
property string configDir: Quickshell.env("HOME") + "/.config/I-DeskPet"
property string configPath: configDir + "/config.json"
property alias gifFolder: adapter.gifFolder
property alias maxScaling: adapter.maxScale
Process {
id: dirCheck
command: ["test", "-d", root.configDir]
running: true
onExited: function (exitCode) {
if (exitCode !== 0) {
console.log("creating dir");
dirCreate.running = true;
}
}
}
Process {
id: dirCreate
command: ["mkdir", "-p", root.configDir]
running: false
onExited: function (): void {
console.log("Created config directory:", root.configDir);
}
}
FileView {
id: watcher
path: root.configPath
watchChanges: true
onAdapterUpdated: writeAdapter()
onFileChanged: reload()
JsonAdapter {
id: adapter
property string gifFolder: Quickshell.shellDir + "/Gifs"
property real maxScale: 1
}
}
}
-20
View File
@@ -1,20 +0,0 @@
import QtQuick
import Qt.labs.folderlistmodel
Item {
id: root
property alias count: folderModel.count
required property string gifFolder
property alias gifsModel: folderModel
FolderListModel {
id: folderModel
folder: "file://" + root.gifFolder
nameFilters: ["*.gif"]
showDirs: false
showHidden: false
sortField: FolderListModel.Name
}
}
-88
View File
@@ -1,88 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Qt.labs.folderlistmodel
import qs.Modules
Repeater {
id: gifRepeater
required property FolderListModel gifsModel
model: gifsModel
Item {
id: gifItem
required property string fileBaseName
required property url fileUrl
property alias hovered: mouse.containsMouse
required property int index
property bool loaded: false
property alias zIndex: gifSaved.zIndex
height: Math.floor(gif.sourceSize.height / gifSaved.scaling)
visible: gifItem.loaded
width: Math.floor(gif.sourceSize.width / gifSaved.scaling)
z: gifSaved.zIndex
onXChanged: if (gifItem.loaded)
gifSaved.positionX = gifItem.x
onYChanged: if (gifItem.loaded)
gifSaved.positionY = gifItem.y
AnimatedImage {
id: gif
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: gifItem.fileUrl
}
Mouse {
id: mouse
onDoubleClicked: gifSaved.scaling = 1
onWheel: wheel => {
gifSaved.scaling = Math.max(ConfigLoader.maxScaling, (gifSaved.scaling + 0.1 * (wheel.angleDelta.y / 120)));
}
}
FileView {
id: watcher
property string configDir: Quickshell.env("HOME") + "/.config/I-DeskPet/"
property string configPath: configDir + name
property string name: gifItem.fileBaseName + ".json"
path: configPath
watchChanges: true
onAdapterUpdated: writeAdapter()
onFileChanged: reload()
onLoadFailed: {
gifSaved.zIndex = gifItem.index;
writeAdapter();
gifItem.loaded = true;
}
onLoaded: {
if (gifSaved.zIndex === -1)
gifSaved.zIndex = gifItem.index;
gifItem.x = gifSaved.positionX;
gifItem.y = gifSaved.positionY;
gifItem.loaded = true;
}
JsonAdapter {
id: gifSaved
property int positionX: 0
property int positionY: 0
property real scaling: 1
property int zIndex: -1
}
}
}
}
-13
View File
@@ -1,13 +0,0 @@
import QtQuick
MouseArea {
acceptedButtons: Qt.LeftButton
anchors.fill: parent
drag.axis: Drag.XAndYAxis
drag.maximumX: Screen.width - parent.width
drag.maximumY: Screen.height - parent.height
drag.minimumX: 0
drag.minimumY: 0
drag.target: parent
hoverEnabled: true
}
+63 -63
View File
@@ -1,63 +1,63 @@
<div align="Center"> <div align="Center">
<h3> Pet March (Evernight) </h3> <h3> Pet March (Evernight) </h3>
<p>My selfmade desktop pet using QT </p> <p> My selfmade desktop pet using QT </p>
<img src=./Assets/Evernight.gif style="margin: 0px 30px 0px 0px;" /> <img src=./Assets/Evernight.gif style="margin: 0px 30px 0px 0px;" />
</div> </div>
## Feature list ## Installation
- [x] Hyprland keybind support ```zsh
- [x] Toggle layer ontop/bottom cargo run --bin ideskpet-installer
- [x] Toggle active mouse area ```
- [x] Dynamic path + live update
- [x] Supports multiple gifs ## Feature list
- [x] User config options
- [x] Evernight base gif img - [x] Binary for I-DeskPet (Branch Main)
# Config # Config
Configuration is found at: Configuration is found at:
```zsh ```zsh
~/.config/I-DeskPet ~/.config/I-DeskPet
``` ```
Options: Options:
- gifFolder - gifFolder
- maxScaling - maxScaling
Example for config.json: Example for config.json:
```json ```json
{ {
"gifFolder": "/home/inorishio/Pictures/Pets", "gifFolder": "/home/inorishio/Pictures/Pets",
"maxScaling": 1 "maxScaling": 1
} }
``` ```
# Hyprland keybinds # Hyprland keybinds
Toggle click through Toggle click through
```zsh ```zsh
bind = CTRL, mouse:274, global, I-DeskPet:toggle-Region bind = CTRL, mouse:274, global, I-DeskPet:toggle-Region
``` ```
Toggle between having your gif on your background vs foreground Toggle between having your gif on your background vs foreground
```zsh ```zsh
bind = SHIFT, mouse:274, global, I-DeskPet:toggle-Layer bind = SHIFT, mouse:274, global, I-DeskPet:toggle-Layer
``` ```
Keybind for cycling through gif layering. Keybind for cycling through gif layering.
Hover over which gif you want to cycle it's layer for and use the keybind. Hover over which gif you want to cycle it's layer for and use the keybind.
```zsh ```zsh
bind = $mainMod, Z, global, I-DeskPet:cycle-zIndex bind = $mainMod, Z, global, I-DeskPet:cycle-zIndex
``` ```
# Other keybinds # Other keybinds
- Double click = Reset gif size to original - Double click = Reset gif size to original
- Scroll = Scales the gif up and or down - Scroll = Scales the gif up and or down
-183
View File
@@ -1,183 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Modules
PanelWindow {
id: mainWindow
property var noMove: Region {
}
property bool onTop: true
property var petMove: Region {
id: pets
height: Screen.height
intersection: Intersection.Xor
regions: maskVariants.instances
width: Screen.width
}
property list<Item> repeaterItems: []
property bool setMask: true
function petRegion(itemObject) {
let newregion = regionComponent.createObject(pets, {
"item": itemObject
});
pets.regions.push(newregion);
}
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "I-DeskPet"
color: "transparent"
surfaceFormat.opaque: false
mask: Region {
height: Screen.height
intersection: Intersection.Xor
regions: maskVariants.instances
width: Screen.width
}
anchors {
bottom: true
left: true
right: true
top: true
}
margins {
bottom: 0
left: 0
right: 0
top: 0
}
GetGifs {
id: getGifs
gifFolder: ConfigLoader.gifFolder
}
GifsLoader {
id: gifLoader
gifsModel: getGifs.gifsModel
onItemAdded: function (index, item) {
mainWindow.repeaterItems = Array.from({
length: gifLoader.count
}, (_, i) => gifLoader.itemAt(i)).filter(v => v !== null);
}
onItemRemoved: function (index, item) {
mainWindow.repeaterItems = Array.from({
length: gifLoader.count
}, (_, i) => gifLoader.itemAt(i)).filter(v => v !== null);
}
}
Variants {
id: maskVariants
model: [...mainWindow.repeaterItems]
Region {
required property Item modelData
height: modelData.height
intersection: Intersection.Subtract
width: modelData.width
x: modelData.x
y: modelData.y
Component.onCompleted: {
console.log(modelData);
}
}
}
Component {
id: regionComponent
Region {
}
}
GlobalShortcut {
appid: "I-DeskPet"
name: "toggle-Layer"
onPressed: {
if (!mainWindow.onTop) {
mainWindow.WlrLayershell.layer = WlrLayer.Overlay;
mainWindow.onTop = true;
} else {
mainWindow.WlrLayershell.layer = WlrLayer.Bottom;
mainWindow.onTop = false;
}
}
}
GlobalShortcut {
appid: "I-DeskPet"
name: "toggle-Region"
onPressed: {
if (!mainWindow.setMask) {
mainWindow.mask = mainWindow.petMove;
mainWindow.setMask = true;
} else {
mainWindow.mask = mainWindow.noMove;
mainWindow.setMask = false;
}
}
}
GlobalShortcut {
appid: "I-DeskPet"
name: "cycle-zIndex"
onPressed: {
let items = mainWindow.repeaterItems;
if (items.length < 2)
return;
// Find the hovered GIF
let hovered = null;
for (let i = 0; i < items.length; i++) {
if (items[i].hovered) {
hovered = items[i];
break;
}
}
if (!hovered)
return;
let currentZ = hovered.zIndex;
let maxZ = items.length - 1;
if (currentZ >= maxZ) {
// Already on top, wrap to bottom: shift everyone else up by 1
for (let i = 0; i < items.length; i++) {
if (items[i] !== hovered) {
items[i].zIndex += 1;
}
}
hovered.zIndex = 0;
} else {
// Swap with the item directly above
for (let i = 0; i < items.length; i++) {
if (items[i] !== hovered && items[i].zIndex === currentZ + 1) {
items[i].zIndex = currentZ;
break;
}
}
hovered.zIndex = currentZ + 1;
}
}
}
}
+492
View File
@@ -0,0 +1,492 @@
//! I-DeskPet CLI Tool
//!
//! A command-line interface for managing the I-DeskPet desktop pet application.
//!
//! Usage:
//! ideskpet <command> [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<String> = 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<u32> {
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 <lines>]");
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 dispatch_arg = format!("hl.dsp.global(\"{shortcut_full}\")");
let status = Command::new("hyprctl")
.args(["dispatch", &dispatch_arg])
.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 <COMMAND> [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 <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
"
);
}
+186
View File
@@ -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 <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!");
}