#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SAIKYO OS License Activator - Modern GUI Beautiful activation interface with dark theme """ import sys import os import subprocess import re from datetime import datetime try: from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QTextEdit, QFrame, QMessageBox, QFileDialog, QStackedWidget, QGroupBox, QSpacerItem, QSizePolicy ) from PyQt6.QtCore import Qt, QTimer, QSize from PyQt6.QtGui import QFont, QPixmap, QIcon, QPalette, QColor PYQT_VERSION = 6 except ImportError: from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QTextEdit, QFrame, QMessageBox, QFileDialog, QStackedWidget, QGroupBox, QSpacerItem, QSizePolicy ) from PyQt5.QtCore import Qt, QTimer, QSize from PyQt5.QtGui import QFont, QPixmap, QIcon, QPalette, QColor PYQT_VERSION = 5 # Saikyo OS color scheme COLORS = { 'bg_dark': '#0a0a0a', 'bg_card': '#1a1a1a', 'bg_input': '#252525', 'accent': '#00ff66', 'accent_hover': '#00cc52', 'text': '#ffffff', 'text_secondary': '#888888', 'border': '#333333', 'success': '#00ff66', 'warning': '#ffaa00', 'error': '#ff4444', } STYLESHEET = f""" QMainWindow, QWidget {{ background-color: {COLORS['bg_dark']}; color: {COLORS['text']}; font-family: 'Segoe UI', 'Ubuntu', sans-serif; }} QLabel {{ color: {COLORS['text']}; background: transparent; }} QLabel#title {{ font-size: 28px; font-weight: bold; color: {COLORS['accent']}; }} QLabel#subtitle {{ font-size: 14px; color: {COLORS['text_secondary']}; }} QLabel#status_label {{ font-size: 16px; font-weight: bold; padding: 10px; border-radius: 8px; }} QLabel#status_active {{ background-color: rgba(0, 255, 102, 0.15); color: {COLORS['success']}; border: 1px solid {COLORS['success']}; }} QLabel#status_trial {{ background-color: rgba(255, 170, 0, 0.15); color: {COLORS['warning']}; border: 1px solid {COLORS['warning']}; }} QLabel#status_expired, QLabel#status_missing {{ background-color: rgba(255, 68, 68, 0.15); color: {COLORS['error']}; border: 1px solid {COLORS['error']}; }} QPushButton {{ background-color: {COLORS['bg_card']}; color: {COLORS['text']}; border: 1px solid {COLORS['border']}; border-radius: 8px; padding: 12px 24px; font-size: 14px; font-weight: 500; min-width: 120px; }} QPushButton:hover {{ background-color: {COLORS['bg_input']}; border-color: {COLORS['accent']}; }} QPushButton:pressed {{ background-color: {COLORS['accent']}; color: {COLORS['bg_dark']}; }} QPushButton#primary {{ background-color: {COLORS['accent']}; color: {COLORS['bg_dark']}; border: none; font-weight: bold; }} QPushButton#primary:hover {{ background-color: {COLORS['accent_hover']}; }} QLineEdit, QTextEdit {{ background-color: {COLORS['bg_input']}; color: {COLORS['text']}; border: 1px solid {COLORS['border']}; border-radius: 6px; padding: 10px; font-size: 14px; }} QLineEdit:focus, QTextEdit:focus {{ border-color: {COLORS['accent']}; }} QGroupBox {{ background-color: {COLORS['bg_card']}; border: 1px solid {COLORS['border']}; border-radius: 12px; margin-top: 16px; padding: 20px; font-size: 14px; font-weight: bold; }} QGroupBox::title {{ subcontrol-origin: margin; left: 20px; padding: 0 10px; color: {COLORS['text_secondary']}; }} QFrame#separator {{ background-color: {COLORS['border']}; max-height: 1px; }} QFrame#card {{ background-color: {COLORS['bg_card']}; border: 1px solid {COLORS['border']}; border-radius: 12px; padding: 20px; }} """ class LicenseManager: """Interface to saikyo-license CLI""" LICENSE_CMD = '/usr/sbin/saikyo-license' @classmethod def run_cmd(cls, *args): try: result = subprocess.run( [cls.LICENSE_CMD] + list(args), capture_output=True, text=True, timeout=30 ) return result.returncode, result.stdout, result.stderr except Exception as e: return -1, '', str(e) @classmethod def run_privileged(cls, *args): try: result = subprocess.run( ['pkexec', cls.LICENSE_CMD] + list(args), capture_output=True, text=True, timeout=60 ) return result.returncode, result.stdout, result.stderr except Exception as e: return -1, '', str(e) @classmethod def get_status(cls): code, out, err = cls.run_cmd('status') status = { 'status': 'unknown', 'trial_days': '', 'trial_remaining_seconds': '0', 'valid_until': '', 'expired': 'no', 'org_name': '', 'tier': '', 'machine_id': '', } for line in out.split('\n'): if '=' in line: k, v = line.split('=', 1) k = k.strip().replace('-', '_') status[k] = v.strip() return status @classmethod def verify(cls): code, _, _ = cls.run_cmd('verify') return code == 0 @classmethod def install_code(cls, code): return cls.run_privileged('install-code', code) @classmethod def install_files(cls, json_path, sig_path): return cls.run_privileged('install', json_path, sig_path) @classmethod def activate_online(cls, token, server='https://saikyo-os.ru/license'): return cls.run_privileged('activate', '--token', token, '--server', server) @classmethod def get_machine_id(cls): try: with open('/etc/machine-id', 'r') as f: return f.read().strip() except: return 'Не удалось прочитать' def human_duration(seconds): try: s = int(seconds) except: return '—' if s <= 0: return '0м' d = s // 86400 h = (s % 86400) // 3600 m = (s % 3600) // 60 if d > 0: return f'{d}д {h}ч {m}м' elif h > 0: return f'{h}ч {m}м' else: return f'{m}м' class StatusCard(QFrame): def __init__(self, parent=None): super().__init__(parent) self.setObjectName('card') self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(16) # Status badge self.status_label = QLabel('Загрузка...') self.status_label.setObjectName('status_label') self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter) layout.addWidget(self.status_label) # Info grid info_layout = QVBoxLayout() info_layout.setSpacing(8) self.org_label = self._create_info_row('Организация:', '—') self.tier_label = self._create_info_row('Тариф:', '—') self.remaining_label = self._create_info_row('Осталось:', '—') self.valid_until_label = self._create_info_row('Действует до:', '—') self.machine_id_label = self._create_info_row('Machine ID:', '—') info_layout.addLayout(self.org_label[0]) info_layout.addLayout(self.tier_label[0]) info_layout.addLayout(self.remaining_label[0]) info_layout.addLayout(self.valid_until_label[0]) info_layout.addLayout(self.machine_id_label[0]) layout.addLayout(info_layout) def _create_info_row(self, label_text, value_text): layout = QHBoxLayout() label = QLabel(label_text) label.setStyleSheet(f'color: {COLORS["text_secondary"]}; font-size: 13px;') value = QLabel(value_text) value.setStyleSheet('font-size: 13px; font-weight: 500;') value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse if PYQT_VERSION == 6 else Qt.TextSelectableByMouse) layout.addWidget(label) layout.addStretch() layout.addWidget(value) return (layout, value) def update_status(self, status_data): status = status_data.get('status', 'unknown') # Update status badge if status == 'trial': self.status_label.setText('⏱ ПРОБНЫЙ ПЕРИОД') self.status_label.setObjectName('status_trial') elif status == 'missing': self.status_label.setText('⚠ НЕ АКТИВИРОВАНА') self.status_label.setObjectName('status_missing') elif status_data.get('expired') == 'yes': self.status_label.setText('✗ ИСТЕКЛА') self.status_label.setObjectName('status_expired') elif LicenseManager.verify(): self.status_label.setText('✓ АКТИВНА') self.status_label.setObjectName('status_active') else: self.status_label.setText('? НЕИЗВЕСТНО') self.status_label.setObjectName('status_missing') # Force style refresh self.status_label.setStyleSheet(self.status_label.styleSheet()) # Update info self.org_label[1].setText(status_data.get('org_name') or '—') self.tier_label[1].setText(status_data.get('tier') or '—') # Calculate remaining time if status == 'trial': remaining = human_duration(status_data.get('trial_remaining_seconds', 0)) elif status_data.get('valid_until'): try: exp = datetime.fromisoformat(status_data['valid_until'].replace('Z', '+00:00')) now = datetime.now(exp.tzinfo) if exp.tzinfo else datetime.now() delta = (exp - now).total_seconds() remaining = human_duration(delta) if delta > 0 else 'Истекла' except: remaining = '—' else: remaining = '—' self.remaining_label[1].setText(remaining) self.valid_until_label[1].setText(status_data.get('valid_until') or '—') self.machine_id_label[1].setText(LicenseManager.get_machine_id()[:16] + '...') class ActivationWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) layout.setSpacing(20) # Online activation online_group = QGroupBox('Онлайн активация') online_layout = QVBoxLayout(online_group) self.token_input = QLineEdit() self.token_input.setPlaceholderText('Введите токен активации...') online_layout.addWidget(self.token_input) self.activate_btn = QPushButton('Активировать') self.activate_btn.setObjectName('primary') self.activate_btn.clicked.connect(self.activate_online) online_layout.addWidget(self.activate_btn) layout.addWidget(online_group) # Offline activation offline_group = QGroupBox('Оффлайн активация (response-код)') offline_layout = QVBoxLayout(offline_group) self.code_input = QTextEdit() self.code_input.setPlaceholderText('Вставьте response-код (SAI-RC:... или SAI-RESP2:...)') self.code_input.setMaximumHeight(100) offline_layout.addWidget(self.code_input) self.offline_btn = QPushButton('Активировать оффлайн') self.offline_btn.clicked.connect(self.activate_offline) offline_layout.addWidget(self.offline_btn) layout.addWidget(offline_group) # File installation file_group = QGroupBox('Установка из файлов') file_layout = QVBoxLayout(file_group) file_hint = QLabel('Выберите license.json и license.sig') file_hint.setStyleSheet(f'color: {COLORS["text_secondary"]}; font-size: 12px;') file_layout.addWidget(file_hint) self.file_btn = QPushButton('Выбрать файлы...') self.file_btn.clicked.connect(self.install_from_files) file_layout.addWidget(self.file_btn) layout.addWidget(file_group) layout.addStretch() def activate_online(self): token = self.token_input.text().strip() if not token: QMessageBox.warning(self, 'Ошибка', 'Введите токен активации') return code, out, err = LicenseManager.activate_online(token) if code == 0: QMessageBox.information(self, 'Успех', 'Лицензия успешно активирована!') self.token_input.clear() self.parent().parent().refresh_status() else: QMessageBox.critical(self, 'Ошибка', f'Ошибка активации:\n{err or out}') def activate_offline(self): code = self.code_input.toPlainText().strip() if not code: QMessageBox.warning(self, 'Ошибка', 'Вставьте response-код') return ret, out, err = LicenseManager.install_code(code) if ret == 0: QMessageBox.information(self, 'Успех', 'Лицензия успешно установлена!') self.code_input.clear() self.parent().parent().refresh_status() else: QMessageBox.critical(self, 'Ошибка', f'Ошибка установки:\n{err or out}') def install_from_files(self): json_path, _ = QFileDialog.getOpenFileName( self, 'Выберите license.json', os.path.expanduser('~'), 'JSON files (*.json);;All files (*)' ) if not json_path: return sig_path, _ = QFileDialog.getOpenFileName( self, 'Выберите license.sig', os.path.dirname(json_path), 'Signature files (*.sig);;All files (*)' ) if not sig_path: return ret, out, err = LicenseManager.install_files(json_path, sig_path) if ret == 0: QMessageBox.information(self, 'Успех', 'Лицензия успешно установлена!') self.parent().parent().refresh_status() else: QMessageBox.critical(self, 'Ошибка', f'Ошибка установки:\n{err or out}') class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('SAIKYO OS — Активация лицензии') self.setMinimumSize(500, 650) self.setMaximumSize(600, 800) # Set window icon icon_path = '/usr/share/icons/hicolor/512x512/apps/saikyo-activation-logo.png' if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) self.setup_ui() self.refresh_status() def setup_ui(self): central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setContentsMargins(30, 30, 30, 30) layout.setSpacing(20) # Header header_layout = QVBoxLayout() header_layout.setSpacing(8) # Logo logo_path = '/usr/share/icons/hicolor/512x512/apps/saikyo-activation-logo.png' if os.path.exists(logo_path): logo_label = QLabel() pixmap = QPixmap(logo_path).scaled( 80, 80, Qt.AspectRatioMode.KeepAspectRatio if PYQT_VERSION == 6 else Qt.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation if PYQT_VERSION == 6 else Qt.SmoothTransformation ) logo_label.setPixmap(pixmap) logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter) header_layout.addWidget(logo_label) title = QLabel('SAIKYO OS') title.setObjectName('title') title.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter) header_layout.addWidget(title) subtitle = QLabel('Активация лицензии') subtitle.setObjectName('subtitle') subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter if PYQT_VERSION == 6 else Qt.AlignCenter) header_layout.addWidget(subtitle) layout.addLayout(header_layout) # Status card self.status_card = StatusCard() layout.addWidget(self.status_card) # Activation widget self.activation_widget = ActivationWidget(self) layout.addWidget(self.activation_widget) # Footer buttons footer_layout = QHBoxLayout() refresh_btn = QPushButton('Обновить') refresh_btn.clicked.connect(self.refresh_status) footer_layout.addWidget(refresh_btn) portal_btn = QPushButton('Открыть портал') portal_btn.clicked.connect(self.open_portal) footer_layout.addWidget(portal_btn) close_btn = QPushButton('Закрыть') close_btn.clicked.connect(self.close) footer_layout.addWidget(close_btn) layout.addLayout(footer_layout) def refresh_status(self): status = LicenseManager.get_status() self.status_card.update_status(status) def open_portal(self): try: subprocess.Popen(['xdg-open', 'https://saikyo-os.ru/license'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except: pass def main(): app = QApplication(sys.argv) app.setStyle('Fusion') app.setStyleSheet(STYLESHEET) # Dark palette palette = QPalette() palette.setColor(QPalette.ColorRole.Window if PYQT_VERSION == 6 else QPalette.Window, QColor(COLORS['bg_dark'])) palette.setColor(QPalette.ColorRole.WindowText if PYQT_VERSION == 6 else QPalette.WindowText, QColor(COLORS['text'])) palette.setColor(QPalette.ColorRole.Base if PYQT_VERSION == 6 else QPalette.Base, QColor(COLORS['bg_input'])) palette.setColor(QPalette.ColorRole.Text if PYQT_VERSION == 6 else QPalette.Text, QColor(COLORS['text'])) palette.setColor(QPalette.ColorRole.Button if PYQT_VERSION == 6 else QPalette.Button, QColor(COLORS['bg_card'])) palette.setColor(QPalette.ColorRole.ButtonText if PYQT_VERSION == 6 else QPalette.ButtonText, QColor(COLORS['text'])) palette.setColor(QPalette.ColorRole.Highlight if PYQT_VERSION == 6 else QPalette.Highlight, QColor(COLORS['accent'])) app.setPalette(palette) window = MainWindow() window.show() sys.exit(app.exec() if PYQT_VERSION == 6 else app.exec_()) if __name__ == '__main__': main()