commit 1574f2853d9d55006f211b3acaf0a8573f663d49 Author: Matteo Benedetto Date: Mon Sep 29 17:33:12 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b40b98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,280 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be added to the global gitignore or merged into this project gitignore. +# For PyCharm Community Edition, use 'PyCharm CE' instead of 'PyCharm'. +.idea/ + +# VSCode +.vscode/ +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Project specific +# ComfyUI installations (if any get created locally) +ComfyUI/ +comfy/ + +# Logs +*.log +logs/ + +# Configuration files that might contain sensitive info +config.json +*.ini + +# Temporary files +*.tmp +*.temp +temp/ +tmp/ + +# Archive files (shouldn't be committed) +*.zip +*.tar.gz +*.rar +*.7z + +# Backup files +*.bak +*.backup +*~ + +# Editor swap files +*.swp +*.swo +*~ + +# GTK/GLib cache and temporary files +.cache/ +gschemas.compiled + +# Application specific temporary files +comfyui_*.pid +launcher_*.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dbf36eb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable changes to ComfyUI Launcher will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-09-29 + +### Added +- Initial release of ComfyUI Launcher +- GTK4 based modern interface with libadwaita +- Full conda environment detection and management +- Automatic ComfyUI installation via comfy-cli +- Real-time process monitoring (checks every 3 seconds) +- Start/stop controls for ComfyUI +- Real-time log viewing with timestamps +- Status indicators with visual feedback +- Cross-platform support (Linux distributions) +- System dependency detection and installation guidance +- Desktop integration file +- Comprehensive documentation in English and Italian +- Automated installation script for Fedora and Debian-based systems + +### Features +- **Environment Management**: Automatic detection of conda environments +- **Process Control**: Start, stop, and monitor ComfyUI processes +- **Real-time Monitoring**: Continuous status checking via port 8188 and process detection +- **Installation Assistant**: One-click installation of ComfyUI and dependencies +- **Log Viewer**: Real-time output display with monospace font +- **Modern UI**: GTK4 interface with proper dark/light theme support +- **Cross-distribution Support**: Works on Fedora, Ubuntu, Debian, and other Linux distributions + +### System Requirements +- Python 3.9 or higher +- GTK4 and libadwaita +- Conda or Miniconda +- Cairo, GObject Introspection development libraries + +### Known Issues +- Font fallback may not work properly on systems without monospace fonts +- Some older GTK themes may not display correctly with libadwaita + +### Documentation +- Complete installation guide for Debian and Fedora +- Troubleshooting section for common issues +- Development setup instructions +- Contributing guidelines + +### Files Added +- `main.py` - Main application code +- `requirements.txt` - Python dependencies +- `install.sh` - Automated installation script +- `README_EN.md` - English documentation +- `README.md` - Italian documentation +- `style.css` - Custom GTK styles +- `pyproject.toml` - Project configuration +- `.gitignore` - Git ignore rules +- `LICENSE` - MIT license +- `CHANGELOG.md` - This changelog diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a98d1a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,41 @@ +MIT License + +Copyright (c) 2025 ComfyUI Launcher Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Copyright (c) 2025 [Il tuo nome] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4adb87 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +# ComfyUI Launcher + +Un launcher GTK4 moderno per ComfyUI con supporto completo per ambienti conda. + +## Caratteristiche + +- 🚀 **Gestione Ambienti Conda**: Selezione facile degli ambienti conda disponibili +- 🔧 **Installazione Automatica**: Installa ComfyUI e comfy-cli con un click +- 📊 **Monitoraggio in Tempo Reale**: Controlla continuamente se ComfyUI è in esecuzione +- 🎮 **Controlli Intuitivi**: Avvia e ferma ComfyUI facilmente +- 📝 **Log in Tempo Reale**: Visualizza l'output di ComfyUI direttamente nel launcher +- 🎨 **Interfaccia Moderna**: Basata su GTK4 e libadwaita + +## Prerequisiti + +- Python 3.9 o superiore +- conda o miniconda installato +- GTK4 e libadwaita +- Librerie di sviluppo (cairo-devel, gobject-introspection-devel, ecc.) + +## Installazione Dipendenze Sistema + +### Fedora/RHEL: +```bash +sudo dnf install cairo-devel gobject-introspection-devel gtk3-devel pkg-config python3-devel +``` + +### Ubuntu/Debian: +```bash +sudo apt install libcairo2-dev libgirepository1.0-dev libgtk-3-dev pkg-config python3-dev +``` + +## Installazione + +1. Clona il repository: +```bash +git clone +cd gtk-app +``` + +2. Crea e attiva l'ambiente virtuale: +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +3. Installa le dipendenze Python: +```bash +pip install pygobject +``` + +## Utilizzo + +1. Avvia il launcher: +```bash +python main.py +``` + +2. Seleziona un ambiente conda dalla lista dropdown + +3. Se ComfyUI non è installato nell'ambiente, clicca "Installa ComfyUI" + +4. Una volta installato, usa "Avvia ComfyUI" per lanciare ComfyUI + +5. Il launcher monitorerà automaticamente lo stato di ComfyUI + +## Funzionalità Dettagliate + +### Gestione Ambienti Conda +- Lista automatica di tutti gli ambienti conda disponibili +- Supporto per l'ambiente base +- Pulsante di aggiornamento per ricaricare la lista + +### Monitoraggio Stato +- Controllo ogni 3 secondi se ComfyUI è in esecuzione +- Indicatore visivo dello stato (icona e colore) +- Rilevamento tramite porta 8188 e processi attivi + +### Controlli +- **Avvia ComfyUI**: Lancia ComfyUI nell'ambiente selezionato +- **Ferma ComfyUI**: Termina gracefully il processo ComfyUI +- **Installa ComfyUI**: Installa comfy-cli e ComfyUI nell'ambiente + +### Log +- Output in tempo reale di ComfyUI +- Timestamp per ogni messaggio +- Font monospace per miglior leggibilità + +## File di Configurazione + +- `main.py`: Applicazione principale +- `style.css`: Stili personalizzati (opzionale) +- `comfyui-launcher.desktop`: File desktop per integrazione sistema + +## Problemi Comuni + +### Conda non trovato +Assicurati che conda sia nel PATH: +```bash +export PATH="$HOME/miniconda3/bin:$PATH" +``` + +### Errori di permessi +Su alcuni sistemi potrebbe essere necessario rendere eseguibile lo script: +```bash +chmod +x main.py +``` + +### ComfyUI non si avvia +- Verifica che l'ambiente conda abbia Python 3.9+ +- Controlla che comfy-cli sia installato correttamente +- Verifica i log per eventuali errori specifici + +## Contribuire + +Contributi sono benvenuti! Per favore: +1. Fai un fork del repository +2. Crea un branch per la tua feature +3. Committa le modifiche +4. Apri una Pull Request + +## Licenza + +MIT License - vedi il file LICENSE per dettagli. diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..266e5d7 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,247 @@ +# ComfyUI Launcher + +A modern GTK4 launcher for ComfyUI with full conda environment support. + +![GTK4](https://img.shields.io/badge/GTK-4.0-blue) +![Python](https://img.shields.io/badge/Python-3.9+-green) +![License](https://img.shields.io/badge/License-MIT-yellow) + +## Features + +- 🚀 **Conda Environment Management**: Easy selection of available conda environments +- 🔧 **Automatic Installation**: Install ComfyUI and comfy-cli with one click +- 📊 **Real-time Monitoring**: Continuously monitors if ComfyUI is running +- 🎮 **Intuitive Controls**: Start and stop ComfyUI easily +- 📝 **Real-time Logs**: View ComfyUI output directly in the launcher +- 🎨 **Modern Interface**: Built with GTK4 and libadwaita + +## Prerequisites + +- Python 3.9 or higher +- conda or miniconda installed +- GTK4 and libadwaita +- Development libraries (cairo-devel, gobject-introspection-devel, etc.) + +## System Dependencies Installation + +### Fedora/RHEL/CentOS/Rocky Linux + +```bash +# Install system dependencies +sudo dnf install cairo-devel gobject-introspection-devel gtk4-devel pkg-config python3-devel + +# Install conda if not already installed +wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh +bash Miniconda3-latest-Linux-x86_64.sh +``` + +### Debian/Ubuntu + +```bash +# Update package list +sudo apt update + +# Install system dependencies +sudo apt install libcairo2-dev libgirepository1.0-dev libgtk-4-dev pkg-config python3-dev python3-venv + +# Install conda if not already installed +wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh +bash Miniconda3-latest-Linux-x86_64.sh +``` + +## Installation + +1. **Clone the repository:** +```bash +git clone +cd gtk-app +``` + +2. **Create and activate virtual environment:** +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +3. **Install Python dependencies:** +```bash +pip install -r requirements.txt +``` + +## Usage + +### Quick Start + +1. **Launch the application:** +```bash +python main.py +``` +or use the provided script: +```bash +./run.sh +``` + +2. **Select a conda environment** from the dropdown list + +3. **Install ComfyUI** (if not already installed) by clicking "Install ComfyUI" + +4. **Start ComfyUI** by clicking "Start ComfyUI" + +5. The launcher will **automatically monitor** ComfyUI status + +### Detailed Workflow + +1. **Environment Selection** + - The launcher automatically detects all available conda environments + - Select your preferred environment from the dropdown + - Click "Refresh Environment List" to reload environments + +2. **ComfyUI Installation** + - If ComfyUI is not installed in the selected environment, click "Install ComfyUI" + - This will install both `comfy-cli` and ComfyUI automatically + - Installation progress is shown in the log window + +3. **Running ComfyUI** + - Click "Start ComfyUI" to launch ComfyUI in the selected environment + - Real-time output appears in the log window + - The status indicator shows when ComfyUI is running + +4. **Monitoring** + - The launcher checks every 3 seconds if ComfyUI is running + - Status is indicated by both text and visual indicators + - Automatic detection works even if ComfyUI was started outside the launcher + +## Features Detail + +### Environment Management +- Automatic detection of all conda environments +- Support for conda base environment +- Refresh button to reload environment list +- Clear indication of selected environment + +### Status Monitoring +- Checks every 3 seconds if ComfyUI is running +- Visual status indicator (icon and color) +- Detection via port 8188 and active processes +- Works even with externally started ComfyUI instances + +### Controls +- **Start ComfyUI**: Launch ComfyUI in selected environment +- **Stop ComfyUI**: Gracefully terminate ComfyUI process +- **Install ComfyUI**: Install comfy-cli and ComfyUI in environment +- **Refresh Environments**: Reload conda environment list + +### Logging +- Real-time output from ComfyUI +- Timestamp for each message +- Monospace font for better readability +- Automatic scrolling to latest messages + +## File Structure + +``` +gtk-app/ +├── main.py # Main application +├── requirements.txt # Python dependencies +├── style.css # Custom styles (optional) +├── comfyui-launcher.desktop # Desktop integration file +├── run.sh # Launch script +├── README.md # This file +├── .gitignore # Git ignore rules +└── .venv/ # Virtual environment (created after setup) +``` + +## Troubleshooting + +### Conda not found +Make sure conda is in your PATH: +```bash +export PATH="$HOME/miniconda3/bin:$PATH" +# Add to ~/.bashrc or ~/.zshrc for persistence +echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> ~/.bashrc +``` + +### Permission errors +Make the script executable: +```bash +chmod +x main.py +chmod +x run.sh +``` + +### ComfyUI won't start +- Verify the conda environment has Python 3.9+ +- Check that comfy-cli is properly installed +- Review logs for specific error messages +- Ensure sufficient disk space and memory + +### GTK/PyGObject errors +Make sure all system dependencies are installed: +```bash +# Fedora/RHEL +sudo dnf install gtk4-devel python3-gobject + +# Debian/Ubuntu +sudo apt install libgtk-4-dev python3-gi +``` + +### Missing fonts +The launcher uses monospace fonts for logs. If fonts appear wrong: +```bash +# Install additional fonts (optional) +sudo dnf install jetbrains-mono-fonts # Fedora +sudo apt install fonts-jetbrains-mono # Ubuntu +``` + +## Development + +### Setting up development environment + +1. Fork the repository +2. Clone your fork +3. Install development dependencies: +```bash +pip install -r requirements.txt +``` +4. Make your changes +5. Test thoroughly +6. Submit a pull request + +### Code Style +- Follow PEP 8 for Python code +- Use meaningful variable names +- Add comments for complex logic +- Keep functions focused and small + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Update documentation +6. Submit a pull request + +## License + +MIT License - see the LICENSE file for details. + +## Acknowledgments + +- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) - The amazing stable diffusion GUI +- [comfy-cli](https://github.com/Comfy-Org/comfy-cli) - Command line tool for ComfyUI +- [GTK](https://gtk.org/) - The GUI toolkit +- [PyGObject](https://pygobject.readthedocs.io/) - Python bindings for GTK + +## Support + +If you encounter issues: + +1. Check the troubleshooting section above +2. Search existing GitHub issues +3. Create a new issue with: + - Your operating system and version + - Python version + - Complete error messages + - Steps to reproduce the problem diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..b88b646 --- /dev/null +++ b/config.example.json @@ -0,0 +1,57 @@ +{ + "_comment": "ComfyUI Launcher Configuration Template", + "_instructions": "Copy this file to ~/.config/comfyui-launcher/config.json to customize settings", + + "launcher": { + "window": { + "width": 900, + "height": 700, + "remember_size": true, + "remember_position": true + }, + "monitoring": { + "check_interval_seconds": 3, + "port_to_check": 8188, + "process_patterns": [ + "comfy.*launch", + "python.*main.py.*comfyui" + ] + }, + "logging": { + "max_log_lines": 1000, + "auto_scroll": true, + "timestamp_format": "%H:%M:%S", + "font_family": "JetBrains Mono, Fira Code, Source Code Pro, monospace", + "font_size": "10pt" + }, + "conda": { + "auto_refresh_on_startup": true, + "preferred_environments": [], + "exclude_environments": [] + }, + "comfyui": { + "default_args": [], + "auto_start_browser": false, + "startup_timeout_seconds": 60, + "shutdown_timeout_seconds": 10 + } + }, + "paths": { + "conda_executable": "conda", + "comfy_executable": "comfy", + "default_comfyui_directory": "~/comfy/ComfyUI" + }, + "ui": { + "theme": "auto", + "use_dark_theme": null, + "accent_color": "#3584e4", + "show_tooltips": true, + "animations_enabled": true + }, + "advanced": { + "debug_mode": false, + "verbose_logging": false, + "check_for_updates": true, + "telemetry_enabled": false + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4d5553d --- /dev/null +++ b/install.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +# ComfyUI Launcher Installation Script +# This script automates the installation process for different Linux distributions + +set -e # Exit on any error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Detect Linux distribution +detect_distro() { + if [ -f /etc/os-release ]; then + . /etc/os-release + DISTRO=$ID + VERSION=$VERSION_ID + else + print_error "Cannot detect Linux distribution" + exit 1 + fi +} + +# Install system dependencies based on distribution +install_system_deps() { + print_status "Installing system dependencies for $DISTRO..." + + case $DISTRO in + "fedora"|"rhel"|"centos"|"rocky") + sudo dnf install -y cairo-devel gobject-introspection-devel gtk4-devel pkg-config python3-devel python3-pip + ;; + "ubuntu"|"debian") + sudo apt update + sudo apt install -y libcairo2-dev libgirepository1.0-dev libgtk-4-dev pkg-config python3-dev python3-pip python3-venv + ;; + "arch"|"manjaro") + sudo pacman -S --needed cairo gobject-introspection gtk4 pkgconf python python-pip + ;; + *) + print_warning "Unsupported distribution: $DISTRO" + print_warning "Please install the following packages manually:" + print_warning "- Cairo development libraries" + print_warning "- GObject Introspection development libraries" + print_warning "- GTK4 development libraries" + print_warning "- pkg-config" + print_warning "- Python 3.9+ development headers" + ;; + esac +} + +# Check if conda is installed +check_conda() { + if command -v conda &> /dev/null; then + print_success "Conda found: $(conda --version)" + return 0 + else + print_warning "Conda not found" + return 1 + fi +} + +# Install conda if not present +install_conda() { + print_status "Installing Miniconda..." + + # Detect architecture + ARCH=$(uname -m) + if [ "$ARCH" = "x86_64" ]; then + CONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh" + elif [ "$ARCH" = "aarch64" ]; then + CONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh" + else + print_error "Unsupported architecture: $ARCH" + exit 1 + fi + + # Download and install conda + wget "$CONDA_URL" -O miniconda.sh + bash miniconda.sh -b -p "$HOME/miniconda3" + rm miniconda.sh + + # Add conda to PATH + echo 'export PATH="$HOME/miniconda3/bin:$PATH"' >> ~/.bashrc + export PATH="$HOME/miniconda3/bin:$PATH" + + # Initialize conda + conda init bash + + print_success "Miniconda installed successfully" + print_warning "Please restart your terminal or run: source ~/.bashrc" +} + +# Setup Python virtual environment +setup_venv() { + print_status "Setting up Python virtual environment..." + + if [ ! -d ".venv" ]; then + python3 -m venv .venv + print_success "Virtual environment created" + else + print_status "Virtual environment already exists" + fi + + # Activate virtual environment + source .venv/bin/activate + + # Upgrade pip + pip install --upgrade pip + + # Install requirements + if [ -f "requirements.txt" ]; then + print_status "Installing Python dependencies..." + pip install -r requirements.txt + print_success "Python dependencies installed" + else + print_warning "requirements.txt not found, installing basic dependencies..." + pip install PyGObject + fi +} + +# Make scripts executable +make_executable() { + print_status "Making scripts executable..." + chmod +x main.py 2>/dev/null || true + chmod +x run.sh 2>/dev/null || true + print_success "Scripts are now executable" +} + +# Create desktop entry (optional) +create_desktop_entry() { + if [ -f "comfyui-launcher.desktop" ]; then + read -p "Install desktop entry? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + DESKTOP_FILE="$HOME/.local/share/applications/comfyui-launcher.desktop" + mkdir -p "$(dirname "$DESKTOP_FILE")" + + # Update paths in desktop file + CURRENT_DIR=$(pwd) + sed "s|/home/enne2/Development/gtk-app|$CURRENT_DIR|g" comfyui-launcher.desktop > "$DESKTOP_FILE" + + print_success "Desktop entry installed to $DESKTOP_FILE" + fi + fi +} + +# Main installation function +main() { + print_status "ComfyUI Launcher Installation Script" + print_status "======================================" + + # Detect distribution + detect_distro + print_status "Detected distribution: $DISTRO $VERSION" + + # Install system dependencies + install_system_deps + + # Check for conda + if ! check_conda; then + read -p "Install Miniconda? (y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + install_conda + else + print_warning "Conda not installed. You'll need to install it manually for full functionality." + fi + fi + + # Setup Python environment + setup_venv + + # Make scripts executable + make_executable + + # Optional desktop entry + create_desktop_entry + + print_success "Installation completed!" + print_status "To run the launcher:" + print_status " ./run.sh" + print_status "or:" + print_status " source .venv/bin/activate && python main.py" +} + +# Run main function +main "$@" diff --git a/main.py b/main.py new file mode 100644 index 0000000..dc4c25f --- /dev/null +++ b/main.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') + +from gi.repository import Gtk, Adw, Gio, GLib +import sys +import subprocess +import json +import os +import threading +import time +import re + +class ComfyUILauncher(Gtk.ApplicationWindow): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # Configurazione finestra principale + self.set_title("ComfyUI Launcher") + self.set_default_size(900, 700) + + # Variabili di stato + self.comfyui_process = None + self.is_running = False + self.selected_conda_env = None + self.conda_envs = [] + self.status_check_timeout = None + + # Crea il layout principale + self.setup_ui() + + # Inizializza il controllo degli ambienti conda + self.load_conda_environments() + + # Avvia il monitoraggio dello stato + self.start_status_monitoring() + + def setup_ui(self): + # Header bar + header = Gtk.HeaderBar() + header.set_title_widget(Gtk.Label(label="ComfyUI Launcher")) + self.set_titlebar(header) + + # Menu button + menu_button = Gtk.MenuButton() + menu_button.set_icon_name("open-menu-symbolic") + header.pack_end(menu_button) + + # Main content area + main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + main_box.set_margin_top(12) + main_box.set_margin_bottom(12) + main_box.set_margin_start(12) + main_box.set_margin_end(12) + + # Title + title_label = Gtk.Label() + title_label.set_markup("ComfyUI Launcher") + title_label.set_margin_bottom(12) + main_box.append(title_label) + + # Status section + status_frame = Gtk.Frame() + status_frame.set_label("Stato ComfyUI") + status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + status_box.set_margin_top(12) + status_box.set_margin_bottom(12) + status_box.set_margin_start(12) + status_box.set_margin_end(12) + + self.status_indicator = Gtk.Image() + self.status_indicator.set_from_icon_name("media-playback-stop-symbolic") + self.status_indicator.set_icon_size(Gtk.IconSize.LARGE) + status_box.append(self.status_indicator) + + self.status_label = Gtk.Label(label="ComfyUI non in esecuzione") + self.status_label.set_hexpand(True) + self.status_label.set_halign(Gtk.Align.START) + status_box.append(self.status_label) + + status_frame.set_child(status_box) + main_box.append(status_frame) + + # Environment selection + env_frame = Gtk.Frame() + env_frame.set_label("Selezione Ambiente Conda") + env_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + env_box.set_margin_top(12) + env_box.set_margin_bottom(12) + env_box.set_margin_start(12) + env_box.set_margin_end(12) + + # Dropdown per ambienti conda + self.env_dropdown = Gtk.DropDown() + self.env_dropdown.set_enable_search(True) + self.env_dropdown.connect("notify::selected-item", self.on_env_selected) + env_box.append(self.env_dropdown) + + # Refresh button + refresh_button = Gtk.Button(label="Aggiorna Lista Ambienti") + refresh_button.connect("clicked", self.on_refresh_envs) + env_box.append(refresh_button) + + env_frame.set_child(env_box) + main_box.append(env_frame) + + # Control buttons + control_frame = Gtk.Frame() + control_frame.set_label("Controlli") + control_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + control_box.set_margin_top(12) + control_box.set_margin_bottom(12) + control_box.set_margin_start(12) + control_box.set_margin_end(12) + control_box.set_homogeneous(True) + + self.start_button = Gtk.Button(label="Avvia ComfyUI") + self.start_button.add_css_class("suggested-action") + self.start_button.connect("clicked", self.on_start_comfyui) + self.start_button.set_sensitive(False) + control_box.append(self.start_button) + + self.stop_button = Gtk.Button(label="Ferma ComfyUI") + self.stop_button.add_css_class("destructive-action") + self.stop_button.connect("clicked", self.on_stop_comfyui) + self.stop_button.set_sensitive(False) + control_box.append(self.stop_button) + + self.install_button = Gtk.Button(label="Installa ComfyUI") + self.install_button.connect("clicked", self.on_install_comfyui) + control_box.append(self.install_button) + + control_frame.set_child(control_box) + main_box.append(control_frame) + + # Log section + log_frame = Gtk.Frame() + log_frame.set_label("Log") + log_frame.set_vexpand(True) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.set_vexpand(True) + + self.log_view = Gtk.TextView() + self.log_view.set_editable(False) + self.log_view.set_monospace(True) + self.log_buffer = self.log_view.get_buffer() + scrolled.set_child(self.log_view) + + log_frame.set_child(scrolled) + main_box.append(log_frame) + + self.set_child(main_box) + + # Setup menu + self.setup_menu(menu_button) + + def setup_menu(self, menu_button): + menu = Gio.Menu() + menu.append("Apri Cartella ComfyUI", "app.open_folder") + menu.append("About", "app.about") + menu.append("Quit", "app.quit") + menu_button.set_menu_model(menu) + + def log_message(self, message): + """Aggiunge un messaggio al log""" + def update_log(): + current_text = self.log_buffer.get_text( + self.log_buffer.get_start_iter(), + self.log_buffer.get_end_iter(), + False + ) + timestamp = time.strftime("%H:%M:%S") + new_text = f"{current_text}[{timestamp}] {message}\n" if current_text else f"[{timestamp}] {message}\n" + self.log_buffer.set_text(new_text) + + # Scorri alla fine + mark = self.log_buffer.get_insert() + self.log_view.scroll_mark_onscreen(mark) + + GLib.idle_add(update_log) + + def load_conda_environments(self): + """Carica la lista degli ambienti conda disponibili""" + def load_envs(): + try: + # Prova a ottenere la lista degli ambienti conda + result = subprocess.run(['conda', 'env', 'list', '--json'], + capture_output=True, text=True, timeout=30) + if result.returncode == 0: + env_data = json.loads(result.stdout) + envs = [] + for env_path in env_data['envs']: + env_name = os.path.basename(env_path) + if env_name == env_path: # È il path base, usa 'base' + env_name = 'base' + envs.append((env_name, env_path)) + + def update_ui(): + self.conda_envs = envs + string_list = Gtk.StringList() + for name, path in envs: + string_list.append(f"{name} ({path})") + self.env_dropdown.set_model(string_list) + if envs: + self.env_dropdown.set_selected(0) + self.log_message(f"Trovati {len(envs)} ambienti conda") + + GLib.idle_add(update_ui) + else: + GLib.idle_add(lambda: self.log_message(f"Errore nel caricare ambienti conda: {result.stderr}")) + except subprocess.TimeoutExpired: + GLib.idle_add(lambda: self.log_message("Timeout nel caricamento ambienti conda")) + except FileNotFoundError: + GLib.idle_add(lambda: self.log_message("Conda non trovato. Assicurati che conda sia installato e nel PATH")) + except Exception as e: + GLib.idle_add(lambda: self.log_message(f"Errore imprevisto: {str(e)}")) + + # Esegui in un thread separato per non bloccare l'UI + threading.Thread(target=load_envs, daemon=True).start() + + def on_env_selected(self, dropdown, param): + """Gestisce la selezione di un ambiente conda""" + selected_idx = dropdown.get_selected() + if selected_idx != Gtk.INVALID_LIST_POSITION and self.conda_envs: + env_name, env_path = self.conda_envs[selected_idx] + self.selected_conda_env = (env_name, env_path) + self.log_message(f"Selezionato ambiente: {env_name}") + self.update_button_states() + + def on_refresh_envs(self, button): + """Aggiorna la lista degli ambienti conda""" + self.log_message("Aggiornamento lista ambienti...") + self.load_conda_environments() + + def update_button_states(self): + """Aggiorna lo stato dei pulsanti in base allo stato corrente""" + has_env = self.selected_conda_env is not None + self.start_button.set_sensitive(has_env and not self.is_running) + self.stop_button.set_sensitive(self.is_running) + self.install_button.set_sensitive(has_env) + + def check_comfyui_running(self): + """Controlla se ComfyUI è in esecuzione""" + try: + # Controlla se il processo ComfyUI è ancora vivo + if self.comfyui_process and self.comfyui_process.poll() is None: + return True + + # Controlla se c'è un processo ComfyUI in esecuzione (porta 8188) + try: + result = subprocess.run(['ss', '-tulpn'], capture_output=True, text=True, timeout=5) + if ':8188' in result.stdout: + return True + except: + # Fallback con netstat se ss non è disponibile + try: + result = subprocess.run(['netstat', '-tulpn'], capture_output=True, text=True, timeout=5) + if ':8188' in result.stdout: + return True + except: + pass + + # Controlla processi con nome 'comfy' o 'python' che potrebbero essere ComfyUI + try: + result = subprocess.run(['pgrep', '-f', 'comfy.*launch'], capture_output=True, text=True, timeout=5) + if result.returncode == 0 and result.stdout.strip(): + return True + except: + pass + + return False + except Exception as e: + self.log_message(f"Errore nel controllo stato: {str(e)}") + return False + + def update_status_ui(self): + """Aggiorna l'interfaccia utente con lo stato corrente""" + if self.is_running: + self.status_indicator.set_from_icon_name("media-playback-start-symbolic") + self.status_indicator.add_css_class("success") + self.status_label.set_text("ComfyUI in esecuzione") + else: + self.status_indicator.set_from_icon_name("media-playback-stop-symbolic") + self.status_indicator.remove_css_class("success") + self.status_label.set_text("ComfyUI non in esecuzione") + + self.update_button_states() + + def start_status_monitoring(self): + """Avvia il monitoraggio periodico dello stato""" + def monitor(): + was_running = self.is_running + self.is_running = self.check_comfyui_running() + + if was_running != self.is_running: + if self.is_running: + self.log_message("ComfyUI rilevato in esecuzione") + else: + self.log_message("ComfyUI non più in esecuzione") + GLib.idle_add(self.update_status_ui) + + return True # Continua il monitoring + + # Controlla ogni 3 secondi + self.status_check_timeout = GLib.timeout_add(3000, monitor) + + def on_start_comfyui(self, button): + """Avvia ComfyUI""" + if not self.selected_conda_env: + self.log_message("Nessun ambiente conda selezionato") + return + + env_name, env_path = self.selected_conda_env + self.log_message(f"Avvio ComfyUI nell'ambiente {env_name}...") + + def start_comfy(): + try: + # Comando per attivare l'ambiente conda e avviare comfyui + if env_name == 'base': + cmd = ['conda', 'run', '-n', 'base', 'comfy', 'launch'] + else: + cmd = ['conda', 'run', '-n', env_name, 'comfy', 'launch'] + + self.comfyui_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True + ) + + GLib.idle_add(lambda: self.log_message("Processo ComfyUI avviato")) + + # Leggi l'output in tempo reale + for line in iter(self.comfyui_process.stdout.readline, ''): + if line: + GLib.idle_add(lambda l=line.strip(): self.log_message(f"ComfyUI: {l}")) + + except Exception as e: + GLib.idle_add(lambda: self.log_message(f"Errore nell'avvio: {str(e)}")) + + threading.Thread(target=start_comfy, daemon=True).start() + + def on_stop_comfyui(self, button): + """Ferma ComfyUI""" + self.log_message("Fermando ComfyUI...") + + if self.comfyui_process: + try: + self.comfyui_process.terminate() + # Aspetta un po' per la terminazione graceful + try: + self.comfyui_process.wait(timeout=5) + except subprocess.TimeoutExpired: + # Forza la terminazione + self.comfyui_process.kill() + self.comfyui_process = None + self.log_message("Processo ComfyUI terminato") + except Exception as e: + self.log_message(f"Errore nella terminazione: {str(e)}") + else: + # Prova a killare qualsiasi processo sulla porta 8188 + try: + subprocess.run(['pkill', '-f', 'comfy'], timeout=5) + self.log_message("Terminati processi ComfyUI") + except: + self.log_message("Nessun processo ComfyUI trovato da terminare") + + def on_install_comfyui(self, button): + """Installa ComfyUI nell'ambiente selezionato""" + if not self.selected_conda_env: + self.log_message("Nessun ambiente conda selezionato") + return + + env_name, env_path = self.selected_conda_env + self.log_message(f"Installazione ComfyUI nell'ambiente {env_name}...") + + def install_comfy(): + try: + # Prima installa comfy-cli + cmd1 = ['conda', 'run', '-n', env_name, 'pip', 'install', 'comfy-cli'] + result1 = subprocess.run(cmd1, capture_output=True, text=True, timeout=300) + + if result1.returncode == 0: + GLib.idle_add(lambda: self.log_message("comfy-cli installato con successo")) + + # Poi installa ComfyUI + cmd2 = ['conda', 'run', '-n', env_name, 'comfy', 'install'] + result2 = subprocess.run(cmd2, capture_output=True, text=True, timeout=600) + + if result2.returncode == 0: + GLib.idle_add(lambda: self.log_message("ComfyUI installato con successo!")) + else: + GLib.idle_add(lambda: self.log_message(f"Errore installazione ComfyUI: {result2.stderr}")) + else: + GLib.idle_add(lambda: self.log_message(f"Errore installazione comfy-cli: {result1.stderr}")) + + except subprocess.TimeoutExpired: + GLib.idle_add(lambda: self.log_message("Timeout durante l'installazione")) + except Exception as e: + GLib.idle_add(lambda: self.log_message(f"Errore durante l'installazione: {str(e)}")) + + threading.Thread(target=install_comfy, daemon=True).start() + +class ComfyUIApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.connect('activate', self.on_activate) + + # Configura il style manager per gestire correttamente i temi + self.setup_style_manager() + + # Aggiungi azioni per il menu + self.setup_actions() + + def setup_style_manager(self): + """Configura il gestore dello stile per evitare warning""" + style_manager = Adw.StyleManager.get_default() + style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT) + + def setup_actions(self): + # Azione About + about_action = Gio.SimpleAction.new("about", None) + about_action.connect("activate", self.on_about_action) + self.add_action(about_action) + + # Azione Quit + quit_action = Gio.SimpleAction.new("quit", None) + quit_action.connect("activate", self.on_quit_action) + self.add_action(quit_action) + + # Azione Open Folder + open_folder_action = Gio.SimpleAction.new("open_folder", None) + open_folder_action.connect("activate", self.on_open_folder_action) + self.add_action(open_folder_action) + + # Shortcut per quit + self.set_accels_for_action("app.quit", ["q"]) + + def on_activate(self, app): + print("Avvio ComfyUI Launcher...") + # Crea la finestra principale + self.win = ComfyUILauncher(application=app) + print("Finestra launcher creata") + self.win.present() + print("Launcher presentato") + + def on_about_action(self, action, param): + # Mostra dialog About + about = Adw.AboutWindow( + transient_for=self.win, + application_name="ComfyUI Launcher", + application_icon="application-x-executable", + developer_name="ComfyUI Launcher", + version="1.0.0", + developers=["ComfyUI Launcher Team"], + copyright="© 2025 ComfyUI Launcher", + comments="Un launcher GTK per ComfyUI con supporto conda" + ) + about.present() + + def on_open_folder_action(self, action, param): + """Apre la cartella ComfyUI""" + try: + # Prova ad aprire la cartella di default di ComfyUI + home_dir = os.path.expanduser("~") + comfy_dir = os.path.join(home_dir, "comfy", "ComfyUI") + if os.path.exists(comfy_dir): + subprocess.run(['xdg-open', comfy_dir]) + else: + self.win.log_message("Cartella ComfyUI non trovata") + except Exception as e: + self.win.log_message(f"Errore nell'apertura cartella: {str(e)}") + + def on_quit_action(self, action, param): + # Ferma ComfyUI se in esecuzione prima di chiudere + if hasattr(self, 'win') and self.win.comfyui_process: + self.win.on_stop_comfyui(None) + self.quit() + +def main(): + print("Avvio del ComfyUI Launcher...") + try: + app = ComfyUIApp(application_id="com.example.ComfyUILauncher") + print("Applicazione launcher creata, avvio in corso...") + exit_code = app.run(sys.argv) + print(f"Launcher terminato con codice: {exit_code}") + return exit_code + except Exception as e: + print(f"Errore durante l'esecuzione del launcher: {e}") + import traceback + traceback.print_exc() + return 1 + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..214a86a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,84 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "comfyui-launcher" +version = "1.0.0" +description = "A modern GTK4 launcher for ComfyUI with conda environment support" +readme = "README_EN.md" +license = {text = "MIT"} +authors = [ + {name = "ComfyUI Launcher Team"}, +] +maintainers = [ + {name = "ComfyUI Launcher Team"}, +] +keywords = ["comfyui", "launcher", "gtk4", "conda", "ai", "stable-diffusion"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Multimedia :: Graphics", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Environment :: X11 Applications :: GTK", +] +requires-python = ">=3.9" +dependencies = [ + "PyGObject>=3.42.0", +] + +[project.optional-dependencies] +monitoring = ["psutil>=5.9.0"] +dev = [ + "black", + "flake8", + "mypy", + "pytest", +] + +[project.urls] +Homepage = "https://github.com/your-username/comfyui-launcher" +Repository = "https://github.com/your-username/comfyui-launcher" +Issues = "https://github.com/your-username/comfyui-launcher/issues" +Documentation = "https://github.com/your-username/comfyui-launcher/blob/main/README_EN.md" + +[project.scripts] +comfyui-launcher = "main:main" + +[tool.setuptools] +py-modules = ["main"] + +[tool.black] +line-length = 88 +target-version = ['py39'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203", "W503"] + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ac48be8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +# ComfyUI Launcher Requirements +# Python dependencies for the GTK4 ComfyUI launcher + +# Core GTK bindings +PyGObject>=3.42.0 + +# System monitoring (optional, fallback to system commands if not available) +psutil>=5.9.0 + +# Note: Additional system dependencies required: +# - GTK4 development libraries +# - Cairo development libraries +# - GObject Introspection development libraries +# - pkg-config +# - Python development headers +# +# Install system dependencies first: +# +# Fedora/RHEL/CentOS: +# sudo dnf install cairo-devel gobject-introspection-devel gtk4-devel pkg-config python3-devel +# +# Debian/Ubuntu: +# sudo apt install libcairo2-dev libgirepository1.0-dev libgtk-4-dev pkg-config python3-dev +# +# Arch Linux: +# sudo pacman -S cairo gobject-introspection gtk4 pkgconf python diff --git a/style.css b/style.css new file mode 100644 index 0000000..c1927a1 --- /dev/null +++ b/style.css @@ -0,0 +1,35 @@ +/* Stili per il ComfyUI Launcher */ + +.status-running { + color: #26a269; +} + +.status-stopped { + color: #c01c28; +} + +.log-view { + font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace; + font-size: 10pt; + background-color: #1e1e1e; + color: #d4d4d4; +} + +.environment-frame { + border: 1px solid #3584e4; + border-radius: 6px; +} + +.control-button { + margin: 6px; + padding: 12px 24px; + font-weight: bold; +} + +.install-button { + background: linear-gradient(to bottom, #62a0ea, #3584e4); +} + +.install-button:hover { + background: linear-gradient(to bottom, #74b0fa, #4894f4); +}