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
17 changed files with 796 additions and 896 deletions
+21
View File
@@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug
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"
-18
View File
@@ -1,18 +0,0 @@
FileView {
id: watcher
path: root.configPath
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
JsonAdapter {
id: adapter
property string gifFolder: Quickshell.shellDir + "/Gifs"
property real maxScaling: 1
}
}
-38
View File
@@ -1,38 +0,0 @@
FileView {
id: watcher
path: configPath
property string name: gifItem.fileBaseName + ".json"
property string configDir: Quickshell.env("HOME") + "/.config/I-DeskPet/"
property string configPath: configDir + name
onLoaded: {
if ( gifSaved.zIndex === -1 ) gifSaved.zIndex = gifItem.index
gifItem.x = gifSaved.positionX
gifItem.y = gifSaved.positionY
gifItem.loaded = true
}
onLoadFailed: {
gifSaved.zIndex = gifItem.index
writeAdapter()
gifItem.loaded = true
}
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
JsonAdapter {
id: gifSaved
property real scaling: 1
property int positionX: 0
property int positionY: 0
property int zIndex: -1
}
}
}
-38
View File
@@ -1,38 +0,0 @@
FileView {
id: watcher
path: configPath
property string name: gifItem.fileBaseName + ".json"
property string configDir: Quickshell.env("HOME") + "/.config/I-DeskPet/"
property string configPath: configDir + name
onLoaded: {
if ( gifSaved.zIndex === -1 ) gifSaved.zIndex = gifItem.index
gifItem.x = gifSaved.positionX
gifItem.y = gifSaved.positionY
gifItem.loaded = true
}
onLoadFailed: {
gifSaved.zIndex = gifItem.index
writeAdapter()
gifItem.loaded = true
}
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
JsonAdapter {
id: gifSaved
property real scaling: 1
property int positionX: 0
property int positionY: 0
property int zIndex: -1
}
}
}
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
-379
View File
@@ -1,379 +0,0 @@
#!/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 <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}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 <lines>]"
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 "$@"
-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!");
}