
Kawai.Focus - приложение для фокусировки внимания (часть 2)
Данная статья посвящена:
- Продолжению работы над Open Source приложением для фокусировки внимания "Kawai.Focus";
- Реализации минимального функционала таймера Pomodoro;
- Работе с фреймворком Kivy.
Дополнительные материалы

Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 913333
Реклама

Вступление
Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 1) я начал работу над приложением для фокусировки Kawai.Focus. В ней осветил идею приложения, рассказал о технике Pomodoro, а также сформулировал функционал для MVP1. Кроме того, провел анализ фреймворков и выбрал Kivy, на базе которого реализовал первую программу - "Hello world!".
Сегодня я реализую минимальный функционал таймера Pomodoro, а также создам блок-схему пользовательского пути. Заварите себе чай и приготовьте вкусняшки - время разбираться с "помидором"! =)
Пользовательский путь
Прежде чем приступать к реализации функций таймера Pomodoro в коде, необходимо составить блок-схему для пользовательского пути. Блок-схема для пользовательского пути – это визуальное представление шагов, через которые проходит пользователь при взаимодействии с продуктом или услугой.
Есть несколько видов блок-схем, и они все разные по применению. Вот некоторые из них:
- UseCase - как раз схема, описывающая пользовательский путь;
- ERD - диаграмма взаимосвязей в базе данных;
- UML - диаграмма классов;
- Схемы, описывающие именно логику, то, что происходит под капотом.
В нашем случае она называется UseCase, которая подробно покажет, как пользователь попадёт в ту или иную функцию таймера Pomodoro. Рисование блок-схем помогает наглядно увидеть путь пользователя, а также сделать этот процесс наиболее оптимальным и понятным.
Я буду использовать приложение Figma-linux для рисования блок-схемы. По сути, это обычная Figma, неофициально портированная на Linux. Мне нужно создать новую блок-схему. Для этого я открою приложение Figma-linux и нажму на кнопку "New Fig Jam board" для создания нового рабочего пространства блок-схемы.
Сама блок-схема в программе должна состоять из секций, которые будут связаны стрелочками между собой. Каждая секция будет иметь своё название, которое будет идентифицировать функционал, связанный с ней. Это чем-то похоже на игру, где есть старт и финиш, а фишка – это текущая позиция игрока.
Главным отличием игрока от пользователя программы является тот факт, что пользователь может пойти по любому пути и ходить как вперёд, так и назад по программе. Игрок же в настольной игре может ходить только по правилам игры.
Мне нужно отобразить на блок-схеме пользовательский путь для следующего функционала:
- Конструктор таймеров;
- Запуск/пауза/отмена серии таймеров;
- Настройка длительности таймера работы/таймера короткого перерыва/таймера длинного перерыва;
- Настройка количества помидоров до длинного перерыва;
- Текстовый гайд по использованию и технике Pomodoro;
- О программе.
Блок-схема
Поработав в Figma-linux некоторое время, у меня получилась блок-схема, описывающая пользовательский путь. Также схема описывает окна, кнопки, ввод и текст.
Меню и окна с текстом

На первом скриншоте схемы видно, что у нас есть меню с тремя кнопками. Это будет меню, которое доступно из любого окна приложения. Это позволит не делать отдельное окно для меню, сделает навигацию в приложении быстрее и будет экономить код на кнопках "назад". Кроме того, такой подход будет удобен и для мобильной версии приложения (если до него дойдёт).
Кнопки "Инструкция" и "О программе" ведут на окна с текстовым содержимым.
Таймеры
Кнопка "Таймеры" ведёт на окно со списком созданных таймеров и кнопкой "Новый таймер" для создания нового таймера.
Я хотел сделать приложение более гибким, чем классические Pomodoro таймеры, так как я человек ленивый =)
Дело в том, что постоянно настраивать один таймер под разные временные условия может довольно утомить. Поэтому я решил иметь возможность создавать сколько угодно таймеров, которые можно настроить по-разному.
Как видно из блок-схемы, пользователь может переключиться на готовый таймер. В текущей реализации в окне таймера доступно: общее время таймера, обратный отсчёт времени таймера, кнопка "Старт", кнопка "Пауза" и кнопка "Стоп". Кнопки "Стоп" и "Пауза" будут появляться, когда пользователь нажмёт на кнопку "Старт".
Окно таймера пока не имеет полного функционала. В следующих статьях я добавлю туда кнопку "Удалить". Также не помешал бы звуковой сигнал — это же таймер всё-таки! Настройка для включения/выключения звука тоже пригодится впридачу. Ещё не помешало бы подписывать сам таймер: помидор, перерыв, долгий отдых. Ну и конечно хотелось бы знать, сколько осталось помидоров =)
Конструктор

Это окно "Конструктор", которое показывает процесс создания нового таймера. Пользователь может ввести название для таймера, настроить временные характеристики таймера кнопками "+" и "-". Также может выйти назад в "Таймеры" или, нажав на кнопку "Создать", переместиться на окно нового таймера.
Таблица сущностей

Я написал эту таблицу для чёткого понимания того, что есть что на блок-схеме. Сразу видно, что является кнопками, текстом, окном и полем ввода.
После создания блок-схемы настала пора писать сам код, но прежде мне нужно обновить Poetry.
Обновление Poetry
Недавно я переустановил Debian на SSD и заново установил всё, что нужно для программирования. На старой системе у меня стояла версия Poetry 1.8.3, и мне было лень её обновлять. Сейчас я установил самую новую версию Poetry 2.1.1, которая несовместима со старой 1.8.3. Как я понял, много изменений произошло в синтаксисе конфига pyproject.toml
.
Я человек простой: я просто удалил старые файлы poetry.lock
и pyproject.toml
. После этого ввёл команды, которые вам уже известны. Более подробно об изменениях новой версии Poetry вы можете узнать в статье Обновление Poetry: 2.0.0 на сайте Кот на салфетке.
Код
Вначале я хочу реализовать логику самого таймера. Для этого я создам файл utils.py
в пакете kawai_focus
, который будет хранить все необходимые утилиты.
kawai_focus/utils.py
В файле utils.py
я создам функцию custom_timer()
, которая будет отсчитывать время в обратном порядке для заданного времени таймера.
Код для функции:
import time
import re
from typing import Generator
import logging
from kivy.logger import Logger
Logger.setLevel(logging.DEBUG)
def custom_timer(timer_str: str) -> Generator[str, None, None]:
"""Функция отсчитывает время, установленное для таймера в формате 'XhYmZs'."""
try:
match = re.match(r'(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?', timer_str)
if not match:
raise ValueError("Неверный формат времени, используйте 'XhYmZs'!")
hours, minutes, seconds = match.groups()
total_seconds = (int(hours or 0) * 3600) + (int(minutes or 0) * 60) + (int(seconds or 0))
if total_seconds 0:
raise ValueError("Не указано время таймера!")
for remaining in range(total_seconds, -1, -1):
yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}"
time.sleep(1) # Todo: дублирует задержку нужно будет удалить!
except ValueError as err:
Logger.error(f'{err.__class__.__name__}: {err}')
Разбор кода таймера:
from kivy.logger import Logger
необходим для создания логгера для kivy;def custom_timer(timer_str: str) -> Generator[str, None, None]:
функция принимает аргументstr
типа для стартового времени таймера в параметрtimer_str
. Возвращает функцияGenerator
со строками;match = re.match(r'(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?', timer_str)
регулярное выражения для строки с временем, которая будет приходить в параметреtimer_str
(пример: 0h0m10s);raise ValueError("Неверный формат времени, используйте 'XhYmZs'!")
сли строка не соответствует регулярному выражению, то будет выброшена ошибкаValueError
;total_seconds
хранит в себе результат сложения всех секунд таймера изtimer_str
:(int(hours or 0) * 3600)
для часов;(int(minutes or 0) * 60)
для минут;(int(seconds or 0))
для секунд;
raise ValueError("Не указано время таймера!")
если сумма секунд для часов, минут и секунд равна нулю, то будет выброшенаValueError
;for remaining in range(total_seconds, -1, -1):
пройдётся по диапазону секунд до -1. Это необходимо для того, чтобы было включено в вывод"00:00:00"
;yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}"
создание строки времени и вычисление часов, минут и секунд. Формат02d
нужен для автоматического добавления ведущих нулей (например, для 1 будет 01);time.sleep(1)
используется для задержки 1 секунду, которая необходима для правильной работы таймера;Logger.error(f'{err.__class__.__name__}: {err}')
логгируем ошибку.
Логирование в Kivy
Я создам ситуацию, когда будет выброшена ошибка, чтобы посмотреть, как выглядит кастомный лог в логгере Kivy. Для этого создам в файле utils.py
следующий код:
if __name__ '__main__':
# Пример использования
for time_now in custom_timer(''):
print(time_now)
Пояснения по коду:
for time_now in custom_timer(""):
перебор значений, возвращаемых генераторной функцией, у которой в качестве параметра выступает пустая строка;print(time_now)
для вывода в terminal текущего времени таймера.
Запускаем неисправный код через кнопку Run в IDE (у меня это VSCode):
[INFO ] [Logger ] Record log in /home/arduinum628/.kivy/logs/kivy_25-02-25_5.txt
[INFO ] [Kivy ] v2.3.1
[INFO ] [Kivy ] Installed at "/home/arduinum628/.pyenv/versions/3.12.0/envs/kawai_fokus/lib/python3.12/site-packages/kivy/__init__.py"
[INFO ] [Python ] v3.12.0 (main, Feb 18 2025, 21:30:11) [GCC 12.2.0]
[INFO ] [Python ] Interpreter at "/home/arduinum628/.pyenv/versions/3.12.0/envs/kawai_fokus/bin/python"
[INFO ] [Logger ] Purge log fired. Processing...
[INFO ] [Logger ] Purge finished!
[ERROR ] [ValueError ] Не указано время таймера!
Как видно из лога в терминале, строка [ERROR ] [ValueError ] Не указано время таймера!
выглядит как другие логи в Kivy.
Теперь я сделаю строку валидного вида для проверки работы функции:
for time_now in custom_timer('0h1m3s'):
print(time_now)
Результат запуска кода:
00:01:03
00:01:02
00:01:01
00:01:00
00:00:59
00:00:58
00:00:57
00:00:56
Теперь код функции отрабатывает верно. Как видно из вывода в terminal функция custom_timer()
каждую секунду отдаёт сгенерированную строку с временем.
kawai_focus/screens/time_screen.py
Далее мне необходимо реализовать основу для экрана таймера. Пока речь пойдёт лишь о базовом функционале таймера. Как минимум, нужно реализовать запуск таймера, его полную остановку и паузу. При этом нужно обновлять экран таймера каждую секунду.
Для реализации экрана таймера я создам пакет screens
, в котором будут храниться экраны. Экран или Screen - это окно приложения, внутри которого находятся сущности с кнопками, текстом и прочим. Экраны довольно прожорливы по строкам кода, поэтому я решил сразу создать отдельный пакет для них.
Пакет имеет следующую структуру:
├── screens
│ └── __init__.py
│ └── timer_screen.py
В файле timer_screen.py
я создам класс TimerScreen
, который будет отвечать за экран таймера.
Код для класса:
from kivy.uix.screenmanager import Screen
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.clock import Clock
from kawai_focus.utils import custom_timer
class TimerScreen(Screen):
"""Экран таймера"""
def __init__(self, **kwargs):
super(TimerScreen, self).__init__(**kwargs)
self.layout = FloatLayout()
# Добавляем кнопку "Старт"
self.start_button = Button(
text='Старт',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.3,
'center_y': 0.5
}
)
self.start_button.bind(on_release=self.start_timer)
self.layout.add_widget(self.start_button)
# Добавляем кнопку "Пауза"
self.pause_button = Button(
text='Пауза',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.5,
'center_y': 0.5
}
)
self.pause_button.bind(on_release=self.pause_timer)
self.layout.add_widget(self.pause_button)
# Добавляем кнопку "Стоп"
self.stop_button = Button(
text='Стоп',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.7,
'center_y': 0.5
}
)
self.stop_button.bind(on_release=self.stop_timer)
self.layout.add_widget(self.stop_button)
# Добавляем Label для отображения времени
self.time_label = Label(
text='00:00:10',
size_hint=(None, None),
pos_hint={
'center_x': 0.5,
'center_y': 0.6
}
)
self.layout.add_widget(self.time_label)
self.add_widget(self.layout)
# Переменные для управления таймером
self.timer_generator = None
self.paused = False
self.remaining_time = None
def start_timer(self, instance):
if self.paused:
self.paused = False
else:
# Инициализация генератора таймера
# Todo: тут пока харкодное время
self.timer_generator = custom_timer('0h0m10s')
self.remaining_time = next(self.timer_generator, '00:00:00')
# Запуск обновления времени каждую секунду
Clock.schedule_interval(self.update_time, 1)
def pause_timer(self, instance):
if not self.paused:
self.paused = True
# Остановка обновления времени
Clock.unschedule(self.update_time)
def stop_timer(self, instance):
# Остановка обновления времени
Clock.unschedule(self.update_time)
self.paused = False
self.remaining_time = '00:00:00'
self.time_label.text = self.remaining_time
def update_time(self, dt):
if self.paused:
return
try:
# Получение следующего значения времени из генератора
self.remaining_time = next(self.timer_generator, '00:00:00')
self.time_label.text = self.remaining_time
if self.remaining_time '00:00:00':
# Остановка обновления времени
Clock.unschedule(self.update_time)
except StopIteration:
# Остановка обновления времени при завершении генератора
Clock.unschedule(self.update_time)
Разбор кода экрана таймера:
Импорты:
from kivy.uix.screenmanager import Screen
нужен для создания экранов и переключения между ними;from kivy.uix.floatlayout import FloatLayout
один из классов для компоновки layout в Kivy. Точно контролирует размер и положение элементов;from kivy.uix.button import Button
виджет для кнопки;from kivy.uix.label import Label
виджет для текста;from kivy.clock import Clock
планировщик задач в Kivy. Способен вызвать функцию с заданным интервалом времени и на многое другое.
Метод init():
def __init__(self, **kwargs):
конструктор класса с**kwargs
;super(TimerScreen, self).__init__(**kwargs)
вызов родительского классаScreen
, для инициализации базового функционала экрана;self.layout = FloatLayout()
создание компоновщика layout;self.start_button
хранит кнопку с текстомtext
, размерами высотыheight
и шириныweight
, положением кнопки на экранеpos_hint
;self.start_button.bind(on_release=self.start_timer)
привязывает событие к кнопке (отпускание кнопки), которая запускает старт таймера;self.layout.add_widget(self.start_button)
добавление кнопки в компоновщикlayout
;- Для остальных кнопок аналогичные действия;
self.time_label
хранит текст самого таймера, у которого стартовое значениеtext='00:00:10'
пока захардкожено (позже будет задаваться в конструкторе таймера). Параметрsize_hint=(None, None)
отключает адаптивный размер кнопки. Параметрpos_hint
отвечает за позицию на экране;- На данный момент реализуется временный вариант вёрстки для тестирования самих функций таймера. В последующем внешний вид должен будет стать адаптивным и красивым;
self.timer_generator = None
переменная будет хранить генератор иNone
, если генератор не запущен;self.paused = False
переменная-флаг для паузы таймера;self.remaining_time = None
переменная для хранения оставшегося времени в секундах;
Метод start_timer()
:
- Его назначение - запустить таймер;
def start_timer(self, instance):
в котором аргумент в виде экземпляра кнопки для параметраinstance
будет передаваться автоматически черезself.start_button.bind(on_release=self.start_timer)
;if self.paused:
если флаг установлен вTrue
на паузе переводим флаг вFalse
вself.paused = False
;else:
если не на паузе, то происходитself.timer_generator = custom_timer('0h0m10s')
установка заданного времени старта иself.remaining_time = next(self.timer_generator, '00:00:00')
извлечение значения из генератора;Clock.schedule_interval(self.update_time, 1)
вызов функции обновления времени таймераself.update_time
каждую секунду. Во избежание дублирования задержки функцияsleep(1)
будет удалена из логики функцииcustom_timer()
!
Метод pause_timer()
:
- Предназначен для постановки обновления таймера на паузу;
if not self.paused:
если флаг паузы вFalse
, то делает егоTrue
вself.paused = True
;Clock.unschedule(self.update_time)
останавливает обновление времени таймера;
Метод stop_timer()
:
- Нужен для полной остановки таймера;
Clock.unschedule(self.update_time)
останавливаем обновление времени таймера;self.paused = False
переключает флаг паузы вFalse
;self.remaining_time = '00:00:00'
сбрасывает текущее время таймера в нуль;self.time_label.text = self.remaining_time
обновление на экране виджета сLabel
таймера;
Метод update_time()
:
- Нужен для обновления времени таймера и получения его из генератора таймера;
if self.paused:
если таймер на паузе, то метод завершит своё выполнение черезreturn
;self.remaining_time = next(self.timer_generator, '00:00:00')
получает новое значение из генератора таймера, а если ничего не получает получаем дефолтное значение'00:00:00'
;self.time_label.text = self.remaining_time
обновляем значение таймера на экране;if self.remaining_time '00:00:00':
если таймер в нулевом значении, то останавливаем обновление таймераClock.unschedule(self.update_time)
;except StopIteration:
обработка исключения, в котором тоже останавливается обновление таймераClock.unschedule(self.update_time)
. Это нужно чтобы предотвратить ситуацию, если генератор завершится раньше, чемself.remaining_time
станет'00:00:00'
.
kawai_focus/main.py
Перейдём в уже созданный ранее файл main.py
и добавим в него код для экрана:
from kivy.uix.screenmanager import ScreenManager
from kawai_focus.screens.timer_screen import TimerScreen
class KawaiFocusApp(App):
"""Класс для создания приложения"""
title = 'Kawai.Focus'
def build(self):
screen_manager = ScreenManager()
screen_manager.add_widget(TimerScreen(name='timers_screen'))
return screen_manager
Пояснения по новому коду:
Импорты:
from kivy.uix.screenmanager import ScreenManager
менеджер экранов, в который я буду добавлять новые экраны;from kawai_focus.screens.timer_screen import TimerScreen
импорт экрана таймера, который я писал выше;
Метод build()
:
screen_manager = ScreenManager()
создаст менеджер экранов;screen_manager.add_widget(TimerScreen(name='timers_screen'))
добавляет экран как виджет в менеджер экранов. Параметрname='timers_screen'
потребуется в будущем для переключения на этот экран из меню;return screen_manager
вернёт менеджер экранов.
Запуск приложения
Я запущу своё приложение с помощью Poetry в terminal:
poetry run kawai-focus
Во время запуска появляется окно с временем 10 секунд. В последующем у нового таймера будет дефолтное значение в конструкторе таймера, которое можно будет поменять вручную. Пока 10 секунд установлено для быстрого тестирования.

Если нажать на кнопку "Старт", то начнётся обратный отсчёт времени. Обновление экрана и нажатие кнопок происходят довольно плавно.

Для фото выше я нажал на кнопку "Пауза", и таймер остановился секунда в секунду. Задержек не обнаружено.

Если нажать на кнопку "Стоп" или подождать до конца отсчёта времени, то таймер останется в нулевом положении. Пока никаких внешних или звуковых оповещений я ещё не добавил.
Механика простого таймера у меня получилась на приличного размера статью. В последующем кнопка "Пауза" будет появляться вместо кнопки "Старт" после старта таймера, а кнопка "Стоп" будет появляться при старте таймера и исчезать при полной его остановке.
Пояснения для последователей идеального кода: код и дизайн будут доработаны, а хардкод будет убран по мере работы над приложением. Структура будет разделяться по мере необходимости. Данный фреймворк ощущается сложнее, чем Flet, и я ещё нахожусь в процессе его изучения.
Анонс на следующие статьи
Следующая моя статья выйдет 20 марта, в которой я буду продолжать писать таймер. На этот раз я начну с его улучшений. Необходимо будет убрать хардкод текстовой информации, настроить появление кнопок на экране. Также я бы очень хотел настроить звуковое оповещение и сделать экран со списком таймеров с переходом непосредственно в таймер.
С ботом пока лёгкая пауза. Дело в том, что у меня сейчас занятость - три статьи за три недели для трёх сайтов. Мне приходится паять провода, снимать фото и видео для DIY проекта на Arduino, а это отнимает большое количество времени.
В данный момент работаю над тестовой статьёй, и если она понравится редактору, то у меня будет подработка на статьях. Если нет, то времени станет побольше, и я доделаю бота быстрее.
Читайте продолжение будет интересно =)
Заключение
- Создана блок-схема приложения для пользовательского пути;
- Написана минимальная версия функционала таймера;
- Обновлён Poetry.
Все статьи