трансфер с гитхаба

This commit is contained in:
2026-05-15 04:05:54 +06:00
parent a7734dc03c
commit c850c1157b
14 changed files with 727 additions and 9 deletions
+38 -7
View File
@@ -1,7 +1,6 @@
# ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[codz]
*$py.class *$py.class
# C extensions # C extensions
@@ -47,7 +46,7 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/ cover/
@@ -107,17 +106,24 @@ ipython_config.py
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock #poetry.lock
#poetry.toml
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock #pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it #pdm.toml
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python .pdm-python
.pdm-build/ .pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
@@ -130,6 +136,7 @@ celerybeat.pid
# Environments # Environments
.env .env
.envrc
.venv .venv
env/ env/
venv/ venv/
@@ -168,9 +175,33 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff: # Ruff stuff:
.ruff_cache/ .ruff_cache/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 s13bby
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.
+251 -1
View File
@@ -1,3 +1,253 @@
# wallpaper_demon # 🖼️ Wallpaper Daemon
Служба для автоматической смены обоев рабочего стола в зависимости от времени суток. Служба для автоматической смены обоев рабочего стола в зависимости от времени суток.
## 📋 Возможности
- 🌅 **Автоматическая смена обоев** — установка разных обоев для разных периодов дня
- 🎨 **Смена темы GNOME** — автоматическое переключение светлой/тёмной темы
-**Гибкое расписание** — настройка времени смены обоев через JSON
- 🔧 **Systemd-служба** — интеграция с systemd для автоматического запуска
- 📝 **Логирование** — запись всех событий в лог
## 🚀 Требования
- Python 3.13+
- Linux с GNOME и systemd
- Установленные пакеты:
```bash
sudo apt install python3-pip python3-venv rsync
```
- Библиотеки Python (стандартные, установка не требуется):
```
subprocess, os, json, sys, signal, logging, datetime
```
## 📖 Использование
### Быстрый старт
**Однократная установка обоев:**
```bash
python wallpaper_handle.py
```
**Запуск демона (вручную):**
```bash
python wallpaper_daemon.py
```
### Управление через systemd
| Действие | Команда |
|----------|---------|
| Установка службы | `sudo ./install_service.sh` |
| Включение службы | `systemctl --user enable wallpaper-daemon.service` |
| Запуск службы | `systemctl --user start wallpaper-daemon.service` |
| Остановка службы | `systemctl --user stop wallpaper-daemon.service` |
| Проверка статуса | `systemctl --user status wallpaper-daemon.service` |
| Удаление службы | `sudo ./uninstall_service.sh` |
## ⏰ Расписание смены обоев
| Время суток | Период | Обои | Тема |
|-------------|--------|------|------|
| 🌙 Ночь | 00:00 - 07:59 | `night.jpg` | 🌑 Тёмная |
| 🌅 Утро | 08:00 - 11:59 | `morning.jpg` | ☀️ Светлая |
| ☀️ День | 12:00 - 15:59 | `day.jpg` | ☀️ Светлая |
| 🌆 Вечер | 16:00 - 19:59 | `evening.jpg` | ☀️ Светлая |
| 🌃 Перед ночью | 20:00 - 23:59 | `before_night.jpg` | 🌑 Тёмная |
## ⚙️ Настройки
Файл `settings.json` в корне проекта:
```json
{
"night": {
"time": ["0:0", "7:59"],
"theme": "dark",
"file": "night.jpg"
},
"morning": {
"time": ["8:0", "11:59"],
"theme": "light",
"file": "morning.jpg"
},
"day": {
"time": ["12:0", "15:59"],
"theme": "light",
"file": "day.jpg"
},
"evening": {
"time": ["16:0", "19:59"],
"theme": "light",
"file": "evening.jpg"
},
"before_night": {
"time": ["20:0", "23:59"],
"theme": "dark",
"file": "before_night.jpg"
}
}
```
### Параметры
| Параметр | Описание |
|----------|----------|
| `time` | Период времени в формате `["часы:минуты", "часы:минуты"]` |
| `theme` | Тема GNOME: `light` или `dark` |
| `file` | Имя файла обоев в папке `wallpapers/` |
## 📂 Структура проекта
```
wallpaper-daemon/
├── wallpaper_daemon.py # Основной демон (фоновый режим)
├── wallpaper_handle.py # Однократная установка обоев
├── settings.json # Конфигурация расписания
├── install_service.sh # Скрипт установки systemd-службы
├── uninstall_service.sh # Скрипт удаления systemd-службы
├── wallpaper-daemon.service # Unit-файл systemd
├── README.md
├── LICENSE
├── .gitignore
└── wallpapers/
├── morning.jpg # Утренние обои
├── day.jpg # Дневные обои
├── evening.jpg # Вечерние обои
├── before_night.jpg # Обои перед ночью
└── night.jpg # Ночные обои
```
## 🔧 Установка службы
### Автоматическая установка
```bash
sudo ./install_service.sh
```
Скрипт:
1. Копирует файлы в `~/.local/share/wallpaper-daemon`
2. Устанавливает службу в `/etc/systemd/user`
3. Включает linger для пользователя
> **Примечание:** После установки службы нужно включить и запустить её от имени пользователя:
> ```bash
> systemctl --user enable wallpaper-daemon.service
> systemctl --user start wallpaper-daemon.service
> ```
### Переустановка
Если служба была установлена неправильно (например, в `/root/.local/...`):
```bash
# Удалить старую службу
sudo ./uninstall_service.sh
# Установить заново
sudo ./install_service.sh
# Включить и запустить
systemctl --user enable wallpaper-daemon.service
systemctl --user start wallpaper-daemon.service
```
### Ручная установка
```bash
# Создать директорию
mkdir -p ~/.local/share/wallpaper-daemon
# Скопировать файлы (без скриптов установки)
rsync -av --exclude='install_service.sh' --exclude='uninstall_service.sh' ./ ~/.local/share/wallpaper-daemon/
# Скопировать unit-файл
sudo cp wallpaper-daemon.service /etc/systemd/user/
# Перезагрузить демон
sudo systemctl daemon-reload
# Включить linger
loginctl enable-linger $USER
# Включить и запустить службу
systemctl --user enable wallpaper-daemon.service
systemctl --user start wallpaper-daemon.service
```
## 🛠️ Устранение неполадок
### Служба не запускается
**Проверьте статус:**
```bash
systemctl --user status wallpaper-daemon.service
```
**Посмотрите детальные логи:**
```bash
journalctl --user -u wallpaper-daemon.service -n 100 --no-pager
```
**Просмотр логов в реальном времени:**
```bash
journalctl --user -u wallpaper-daemon.service -f
```
### Обои не меняются
1. **Проверьте наличие файлов:**
```bash
ls -la ~/.local/share/wallpaper-daemon/wallpapers/
```
2. **Убедитесь, что GNOME активен:**
```bash
gsettings get org.gnome.desktop.background picture-uri
```
3. **Проверьте переменные окружения:**
```bash
echo $DISPLAY
echo $DBUS_SESSION_BUS_ADDRESS
```
### Ошибки в логах
| Ошибка | Решение |
|--------|---------|
| `Файл не найден` | Проверьте наличие обоев в папке `wallpapers/` |
| `gsettings: не удалось открыть` | Убедитесь, что GNOME запущен |
| `Permission denied` | Проверьте права доступа к файлам |
## 🎨 Добавление своих обоев
1. Поместите изображения в папку `wallpapers/`
2. Отредактируйте `settings.json`:
- Измените `file` на имя вашего файла
- Настройте `time` для нужного периода
- Выберите `theme` (`light` или `dark`)
3. Перезапустите службу:
```bash
systemctl --user restart wallpaper-daemon.service
```
---
**сделано: s13bby**
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVICE_NAME="wallpaper-daemon"
SERVICE_FILE="$SCRIPT_DIR/wallpaper-daemon.service"
SYSTEM_SERVICE_DIR="/etc/systemd/user"
# Используем домашнюю директорию реального пользователя, а не root
if [ "$EUID" -eq 0 ]; then
REAL_USER="${SUDO_USER:-$(logname)}"
INSTALL_DIR="/home/$REAL_USER/.local/share/wallpaper-daemon"
else
INSTALL_DIR="$HOME/.local/share/wallpaper-daemon"
fi
echo "Установка файлов в $INSTALL_DIR..."
mkdir -p "$INSTALL_DIR"
rsync -av --exclude='install_service.sh' --exclude='uninstall_service.sh' \
"$SCRIPT_DIR/" "$INSTALL_DIR/" 2>/dev/null || cp -r "$SCRIPT_DIR"/* "$INSTALL_DIR/"
if [ "$EUID" -ne 0 ]; then
echo "Для установки сервиса требуются права root. Запустите с sudo:"
echo " sudo $0"
exit 1
fi
mkdir -p "$SYSTEM_SERVICE_DIR"
cp "$SERVICE_FILE" "$SYSTEM_SERVICE_DIR/$SERVICE_NAME.service"
systemctl daemon-reload
loginctl enable-linger "$REAL_USER"
# Устанавливаем владельца файлов
chown -R "$REAL_USER:$REAL_USER" "$INSTALL_DIR"
echo "Сервис установлен в $SYSTEM_SERVICE_DIR"
echo "Файлы скопированы в $INSTALL_DIR"
echo ""
echo "Для запуска выполните:"
echo " systemctl --user enable $SERVICE_NAME.service"
echo " systemctl --user start $SERVICE_NAME.service"
echo ""
echo "Проверить статус:"
echo " systemctl --user status $SERVICE_NAME.service"
echo " journalctl --user -u $SERVICE_NAME.service -f"
+32
View File
@@ -0,0 +1,32 @@
{
"night":
{
"time":["0:0", "7:59"],
"theme": "dark",
"file": "night.jpg"
},
"morning":
{
"time":["8:0", "11:59"],
"theme": "light",
"file": "morning.jpg"
},
"day":
{
"time": ["12:0", "15:59"],
"theme": "light",
"file": "day.jpg"
},
"evening":
{
"time": ["16:0", "19:59"],
"theme": "light",
"file": "evening.jpg"
},
"before_night":
{
"time": ["20:0", "23:59"],
"theme": "dark",
"file": "before_night.jpg"
}
}
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
set -e
SERVICE_NAME="wallpaper-daemon"
SYSTEM_SERVICE_DIR="/etc/systemd/user"
# Используем домашнюю директорию реального пользователя, а не root
if [ "$EUID" -eq 0 ]; then
REAL_USER="${SUDO_USER:-$(logname)}"
INSTALL_DIR="/home/$REAL_USER/.local/share/wallpaper-daemon"
else
INSTALL_DIR="$HOME/.local/share/wallpaper-daemon"
fi
if [ "$EUID" -ne 0 ]; then
echo "Для удаления сервиса требуются права root. Запустите с sudo:"
echo " sudo $0"
exit 1
fi
echo "Остановка сервисов у активных пользователей..."
for uid in $(loginctl list-users --no-legend | awk '{print $1}'); do
user=$(id -un "$uid" 2>/dev/null)
if [ -n "$user" ]; then
echo " - $user"
runuser -u "$user" -- systemctl --user stop "$SERVICE_NAME.service" 2>/dev/null || true
runuser -u "$user" -- systemctl --user disable "$SERVICE_NAME.service" 2>/dev/null || true
fi
done
for uid in $(loginctl list-users --no-legend | awk '{print $1}'); do
user=$(id -un "$uid" 2>/dev/null)
if [ -n "$user" ]; then
loginctl disable-linger "$user" 2>/dev/null || true
fi
done
rm -f "$SYSTEM_SERVICE_DIR/$SERVICE_NAME.service"
systemctl daemon-reload
echo "Удаление файлов из $INSTALL_DIR..."
rm -rf "$INSTALL_DIR"
echo "Сервис удален"
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Wallpaper Auto-Switcher Daemon
After=graphical-session.target
PartOf=graphical-session.target
Wants=sleep.target suspend.target hibernate.target hybrid-sleep.target
[Service]
Type=simple
WorkingDirectory=%h/.local/share/wallpaper-daemon
ExecStart=%h/.local/share/wallpaper-daemon/venv/bin/python %h/.local/share/wallpaper-daemon/wallpaper_daemon.py
Restart=on-failure
RestartSec=5
Environment=DISPLAY=:0
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%U/bus
IOSchedulingClass=idle
CPUSchedulingPolicy=idle
Nice=19
[Install]
WantedBy=graphical-session.target
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
from datetime import datetime, timedelta
import subprocess
import os
import json
import sys
import signal
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger(__name__)
RUNNING = True
def signal_handler(signum, frame):
global RUNNING
RUNNING = False
logger.info("Получен сигнал остановки, завершаю работу...")
def get_base_dir():
return os.path.dirname(os.path.abspath(__file__))
def get_settings():
path = os.path.join(get_base_dir(), "settings.json")
try:
with open(path, "r", encoding="utf-8") as data:
return json.load(data)
except Exception as e:
logger.error(f"Ошибка чтения {path}: {e}")
sys.exit(1)
def get_current_time():
now = datetime.now()
return now.hour, now.minute, now.second
def parse_time(time_str):
h, m = map(int, time_str.split(':'))
return h * 60 + m
def choose_wallpaper(current_time, settings):
current_hours, current_minutes, _ = current_time
current_minutes_total = current_hours * 60 + current_minutes
for phase_name, data in settings.items():
start = parse_time(data["time"][0])
end = parse_time(data["time"][1])
if start <= end:
if start <= current_minutes_total <= end:
return data["file"], data["theme"], phase_name
else:
if current_minutes_total >= start or current_minutes_total <= end:
return data["file"], data["theme"], phase_name
return "night.jpg", "dark", "night"
def check_system_active():
"""Проверяет, активна ли система (не в процессе suspend/resume)"""
try:
result = subprocess.run(
["systemctl", "status", "sleep.target", "suspend.target",
"hibernate.target", "hybrid-sleep.target"],
capture_output=True, text=True, timeout=2
)
# Если есть активные таргеты сна - ждём
if result.returncode == 0:
return False
except Exception:
pass
return True
def set_wallpaper(image):
wallpaper_path = os.path.join(get_base_dir(), "wallpapers", image)
if not os.path.exists(wallpaper_path):
logger.error(f"Файл не найден: {wallpaper_path}")
return False
uri = f"file://{wallpaper_path}"
try:
subprocess.run(
["gsettings", "set", "org.gnome.desktop.background", "picture-options", "zoom"],
check=True, capture_output=True
)
except subprocess.CalledProcessError as e:
logger.warning(f"Ошибка picture-options: {e}")
keys = [
("org.gnome.desktop.background", "picture-uri"),
("org.gnome.desktop.background", "picture-uri-dark"),
]
success = True
for schema, setting in keys:
try:
subprocess.run(
["gsettings", "set", schema, setting, f"'{uri}'"],
check=True, capture_output=True
)
except subprocess.CalledProcessError as e:
logger.warning(f"Ошибка {setting}: {e}")
success = False
return success
def switch_theme(theme):
scheme = "prefer-dark" if theme == "dark" else "prefer-light"
try:
subprocess.run(
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
check=True, capture_output=True
)
except subprocess.CalledProcessError as e:
logger.warning(f"Ошибка смены темы: {e}")
def main():
global RUNNING
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
settings = get_settings()
last_applied = None
was_inactive = False
logger.info("Демон обоев запущен. Ожидание переключений...")
while RUNNING:
current_time = get_current_time()
image, theme, phase = choose_wallpaper(current_time, settings)
current_key = f"{image}_{theme}"
# Проверяем активность системы
system_active = check_system_active()
# Если система только что вернулась из сна - применяем обои принудительно
if was_inactive and system_active:
logger.info("Система вернулась из сна, применяю обои...")
switch_theme(theme)
if set_wallpaper(image):
last_applied = current_key
was_inactive = False
elif current_key != last_applied:
logger.info(f"Переключение: {phase} -> {image} (тема: {theme})")
switch_theme(theme)
if set_wallpaper(image):
last_applied = current_key
else:
logger.debug(f"Без изменений: {phase} -> {image}")
if not system_active:
was_inactive = True
logger.debug("Система неактивна (сон/приостановка)")
# Короткий интервал опроса для быстрой реакции после сна
# Используем interruptible sleep для корректной обработки сигналов
sleep_seconds = 30
logger.debug(f"Сон на {sleep_seconds} сек")
# Прерываемый сон - проверяем RUNNING каждые 0.5 сек
import time
for _ in range(sleep_seconds * 2):
if not RUNNING:
break
time.sleep(0.5)
logger.info("Демон остановлен")
if __name__ == "__main__":
main()
+89
View File
@@ -0,0 +1,89 @@
from datetime import datetime
import subprocess
import os
import json
import sys
def get_settings():
base_dir = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(base_dir, "settings.json")
try:
with open(path, "r", encoding="utf-8") as data:
return json.load(data)
except Exception:
print(f"Ошибка: Не удалось прочитать {path}")
sys.exit()
def get_current_time():
return [datetime.now().hour, datetime.now().minute]
def switch_theme(theme):
scheme = "prefer-dark" if theme == "dark" else "prefer-light"
cmd = ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
print(f"Ошибка при смене темы: {e}")
def choose_wallpaper(current_time, settings):
current_hours, current_minutes = current_time
for phase_name, data in settings.items():
start_hours, start_minutes = map(int, data["time"][0].split(':'))
end_hours, end_minutes = map(int, data["time"][1].split(':'))
if (start_hours, start_minutes) <= (current_hours, current_minutes) \
<= (end_hours, end_minutes):
return data["file"], data["theme"]
return "night.jpg", "dark"
def main():
settings = get_settings()
current_time = get_current_time()
image, theme = choose_wallpaper(current_time, settings)
switch_theme(theme)
set_wallpaper(image)
def set_wallpaper(image):
base_dir = os.path.dirname(os.path.abspath(__file__))
wallpaper_path = os.path.join(base_dir, "wallpapers", image)
if not os.path.exists(wallpaper_path):
print(f"ОШИБКА: Файл не найден: {wallpaper_path}")
return
uri = f"file://{wallpaper_path}"
try:
subprocess.run(
["gsettings", "set", "org.gnome.desktop.background", "picture-options", "zoom"],
check=True
)
except subprocess.CalledProcessError as e:
print(f"Ошибка при установке picture-options: {e}")
keys = [
("org.gnome.desktop.background", "picture-uri"),
("org.gnome.desktop.background", "picture-uri-dark"),
("org.gnome.desktop.screensaver", "picture-uri")
]
for schema, setting in keys:
command = ["gsettings", "set", schema, setting, f"'{uri}'"]
try:
subprocess.run(command, check=True)
print(f"Успешно: {setting} -> {image}")
except subprocess.CalledProcessError as e:
print(f"Ошибка: {e}")
try:
subprocess.run([
"gdbus", "call", "--session",
"--dest", "org.gnome.Shell",
"--object-path", "/org/gnome/Shell",
"--method", "org.gnome.Shell.Eval",
"imports.gi.Meta.BackgroundActor.get_for_screen(global.display.get_screen()).force_redraw()"
], check=False, capture_output=True)
except Exception:
pass
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB