Kawai-Focus 2.3: логика приложения на TypeScript
- суббота, 21 февраля 2026 г. в 00:00:11
Всем доброго дня! В предыдущей статье Kawai-Focus 2.2: Python-бинарник в Tauri — проблемы и альтернативы:
Освещены неработающие моменты с бинарником на Arch Linux;
Рассмотрены альтернативы, которые могут исправить проблемы с бинарником;
Внедрён оптимальный (для меня) вариант, который исправил половину неисправностей.
В данной статье я покажу код на JS, который не поместился в предыдущей статье, а также перепишу его на TS. Кратко расскажу о преимуществах TS над JS и о том, что необходимо понимать для перехода.
В прошлой статье я также упоминал, что у Сергея получилось запустить мой проект на Tauri в режиме разработки на Arch. Он поделился со мной информацией в issue на GitHub и тем самым внёс вклад в проект. Поэтому я решил попробовать исправить проблему на основе его issue. Заодно расскажу, что такое issue и как оно выглядит.
Заваривайте чай, доставайте вкусняшки — пора «снимать первый урожай помидор»! 🍅
Как я писал в предыдущей статье, логика, которую я реализовал на JS, не поместилась в неё полностью, поэтому я решил подробнее рассказать об этом коде здесь. Ранее я уже имел дело с чистым JS, хоть и не очень много, поэтому написание прототипа не оказалось для меня сложной задачей.
Однако чем больше кода я писал, тем сильнее осознавал, что чистый JS подходит в основном для относительно простых задач. Например, в нём отсутствует строгая типизация и ряд возможностей, к которым я привык при работе с Python. В прошлой статье я использовал JS, чтобы ускорить разработку, поскольку параллельно было много других задач. Освоение TS тогда заняло бы слишком много времени — для меня он был «тёмным лесом».
К счастью, сейчас у меня появилось больше времени TS, чтобы улучшить текущую реализацию и при этом переписав минимальное количество кода.
Почему минимум?
Потому что TypeScript — это надстройка над JavaScript. Он не заменяет JS, а расширяет его. Большая часть уже написанного кода остаётся валидной и продолжает работать без изменений. В большинстве случаев переход начинается с простого добавления типов к переменным, функциям и возвращаемым значениям.
Кроме того, TypeScript компилируется в обычный JavaScript, поэтому логика приложения остаётся прежней — меняется лишь уровень контроля на этапе разработки. Фактически, разработчик не переписывает архитектуру, а постепенно усиливает её типами. Это особенно удобно, когда проект уже рабочий: можно внедрять TS поэтапно, начиная с отдельных файлов.
Главное преимущество TS перед JS — возможность находить ошибки ещё до запуска программы, улучшенная читаемость и поддерживаемость кода, а также более удобная работа в больших проектах благодаря автодополнению и строгой структуре типов.
Первым делом мне нужно установить необходимые для работы c Tauri через JS/TS плагины, а лишние, которые ещё остались со времен Python бинарника удалить.
Перехожу в папку client:
cd client
Для начала удалю @tauri-apps/plugin-shell , который позволяет вызывать системные команды и запускать внешние процессы (например, Python-бинарник) из приложения, управляя ими через безопасный интерфейс между фронтендом и нативной частью.
npm uninstall @tauri-apps/plugin-shell
Теперь установлю пару плагинов, которые мне пригодятся для работы.
npm install @tauri-apps/plugin-sql @tauri-apps/api
Разбор новых плагинов:
@tauri-apps/plugin-sql — плагин Tauri, который добавляет в приложение доступ к SQL‑базам (например, SQLite) через единый API для выполнения запросов и работы с подключениями;
@tauri-apps/api — основной клиентский пакет JS/TS с API Tauri для взаимодействия фронтенда с “нативной” частью (окна, файловая система, диалоги, события, invoke к Rust-командам и т.п.).
Первым делом я написал скрипт, который будет создавать базу данных при старте приложения, если он не обнаружит её в указанном каталоге. Логика должна быть максимально разделена, чтобы при необходимости можно было заменить CRUD-подход с JS/TS на подход через API для веб-версии, изменив при этом минимум кода.
Плагин tauri-plugin-sql работает с базой данных не через ORM-подход, а через SQL-запросы, которые программист вставляет в нужные функции в виде строк. Если база данных небольшая и состоит из одной таблицы timer, как в моём случае, то SQL-подход вполне подойдёт. Кроме того плагин довольно лёгкий, что даёт плюс по ресурсам когда проект небольшой.
Когда-то давно мне доводилось писать прототип базы данных MySQL на чистых DML- и DDL-запросах. Кроме того, в некоторых компаниях этот навык требуется, поэтому его использование будет полезно для освежения знаний SQL.
DML (Data Manipulation Language) — это группа SQL-операторов для работы с данными в таблицах: выборка, вставка, обновление и удаление (например, SELECT, INSERT, UPDATE, DELETE).
DDL (Data Definition Language) — это группа SQL-операторов для описания и изменения структуры объектов базы данных (например, CREATE, ALTER, DROP).
Сначала я создаю файл client/db/ddl/timerDDL.js для формирования SQL-запроса на создание базы данных. Поскольку база данных у меня уже была создана ранее, я могу посмотреть схему таблицы timer в виде DDL-запроса в VS Code, открыв её через плагин SQLite3 Editor.

timerDDL.js
export const CREATE_TIMER = ` PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS timer ( title VARCHAR(200) NOT NULL, pomodoro_time INTEGER NOT NULL, break_time INTEGER NOT NULL, break_long_time INTEGER NOT NULL, count_pomodoro INTEGER NOT NULL, id INTEGER NOT NULL, PRIMARY KEY (id) ); `;
Разбор кода:
export const CREATE_TIMER — экспортируемая JavaScript константа для SQL-запроса;
Обратные кавычки (backticks) — шаблонная строка (template literal) для многострочного SQL;
PRAGMA foreign_keys = ON; — включает поддержку внешних ключей в SQLite (иначе они игнорируются);
CREATE TABLE IF NOT EXISTS — создаёт таблицу timer, только если её ещё нет (безопасно при повторном выполнении);
title VARCHAR(200) NOT NULL — название таймера, до 200 символов, обязательно;
pomodoro_time INTEGER NOT NULL — длительность одного помидоро в минутах;
break_time INTEGER NOT NULL — длительность короткого перерыва;
break_long_time INTEGER NOT NULL — длительность длинного перерыва;
count_pomodoro INTEGER NOT NULL — количество помидоро в цикле до длинного перерыва;
id INTEGER NOT NULL — уникальный идентификатор таймера;
PRIMARY KEY (id) — id является первичным ключом (уникальный, индексированный);
Точка с запятой в конце — корректное завершение SQL-скрипта.
Данный .js-файл хранит простую переменную со строкой, поэтому для преобразования в TypeScript достаточно переименовать его расширение с .js на .ts.
Далее я создал client/src/db/dml/timerDML.js для работы с таблицей timer.
export const SELECT_TIMERS = 'SELECT id, title, pomodoro_time, count_pomodoro FROM timer ORDER BY id DESC' export const COUNT_TIMERS = 'SELECT COUNT(*) as cnt FROM timer' export const INSERT_SEED_DB = ` INSERT INTO timer (title, pomodoro_time, break_time, break_long_time, count_pomodoro) VALUES ('Timer mini example', 10, 3, 15, 2), ('Timer max example', 90, 10, 40, 8) `
SELECT_TIMERS:
SELECT id, title, pomodoro_time, count_pomodoro — выбирает ключевые поля для отображения;
FROM timer — из таблицы таймеров;
ORDER BY id DESC — сортировка по ID по убыванию (новые таймеры сверху).
COUNT_TIMERS:
SELECT COUNT(*) — подсчёт общего количества записей в таблице;
as cnt — псевдоним результата ({ cnt: 5 });
Используется для пагинации или проверки пустоты БД.
INSERT_SEED_DB:
INSERT INTO timer (...) — вставка начальных данных (seed);
Множественная вставка — 2 записи за один запрос (эффективнее);
('Timer mini example', 10, 3, 15, 2) — короткий таймер: 10 мин работа, 3 мин перерыв, 15 мин длинный, 2 помидоро на цикл;
('Timer max example', 90, 10, 40, 8) — длинный таймер: 90 мин работа, 10 мин перерыв, 40 мин длинный, 8 помидоро на цикл.
id не указан — автоинкремент (PRIMARY KEY).
Здесь я, аналогично предыдущему файлу, переименовал расширение с .js на .ts.
После того как я создал SQL-запросы в виде строк, я приступил к реализации механизма, который будет их использовать. Чтобы добраться до самих запросов, необходимо сначала подключиться к базе данных, для чего нужно сформировать url. Для этого я создал файл client/src/config.js, в котором реализовал функцию getDB_URL().
import { appLocalDataDir } from '@tauri-apps/api/path'; export async function getDB_URL() { const appDir = await appLocalDataDir(); return `sqlite:${appDir}/timer.db`; }
Разбор кода:
import { appLocalDataDir } — импортирует API Tauri для получения локальной папки приложения;
export async function getDB_URL() — асинхронная функция, возвращающая URL базы данных;
await appLocalDataDir() — получает путь к папке данных приложения:
Windows: C:\Users<user>\AppData\Local<appname>;
macOS: ~/Library/Application Support/<appname>;
Linux: ~/.local/share/<appname>.
sqlite:${appDir}/timer.db — формирует URI для SQLite-плагина Tauri:
sqlite: — схема протокола для @tauri-apps/plugin-sql;
${appDir}/timer.db — путь к файлу timer.db в папке приложения.
Результат: "sqlite:/home/user/.local/share/kawai-focus/timer.db"
В данной функции не хватает аннотации возвращаемого значения и строки документации.
import { appLocalDataDir } from '@tauri-apps/api/path'; /** Возвращает URL подключения к SQLite */ export async function getDB_URL(): Promise<string> { const appDir = await appLocalDataDir(); return `sqlite:${appDir}/timer.db`; }
Promise<string> в TypeScript — это тип, который означает, что функция возвращает промис — объект, представляющий результат асинхронной операции.
Промис может находиться в трёх состояниях:
ожидание (pending);
выполнен успешно (fulfilled / resolve);
завершён с ошибкой (rejected / reject).
Если промис завершается успешно, то в случае Promise<string> он вернёт значение типа string. Это даёт статическую проверку типов и гарантирует, что результат асинхронной операции будет именно строкой, а не числом, объектом или undefined.
Когда запускается приложение Kawai-Focus, оно должно заполнить базу демонстрационными данными. В моём случае — двумя демонстрационными таймерами в таблице timer. Это должно происходить только в том случае, если база данных пуста. Для этого я создал файл client/src/db/seed.ts, в котором будет находиться функция seedDb() для заполнения данных.
Думаю, с .js-файлами я уже привёл достаточно примеров, а основное отличие заключается в использовании типов в TS. Поэтому далее я буду сразу показывать .ts-файлы.
Перед тем как перейти к коду функции seedDb(), я создам для неё тип, который поможет с валидацией. В данном случае мне потребуется валидация количества записей в таблице. Для этого я создам файл client/src/types/timerType.ts.
timerType.ts
export type CountRow = { cnt: number; };
Разбор кода:
export — делает тип доступным для использования в других файлах проекта;
type — объявляет пользовательский тип в TypeScript;
CountRow — имя типа, которое описывает структуру строки результата SQL-запроса;
{ cnt: number; } — объектный тип с одним свойством:
cnt — поле, соответствующее алиасу в SQL-запросе (например, SELECT COUNT(*) as cnt);
number — тип значения (число);
Назначение типа — гарантирует, что результат запроса COUNT_TIMERS будет содержать поле cnt именно числового типа, что позволяет избежать ошибок при обращении к нему;
seed.ts
import Database from '@tauri-apps/plugin-sql'; import { INSERT_SEED_DB, COUNT_TIMERS } from './dml/timerDML'; import { CountRow } from '../types/timerType'; /** Заполняет бд данными (демо таймерами) */ export async function seedDb(db: Database) { const count = await db.select<CountRow[]>(COUNT_TIMERS); const cnt = count[0]?.cnt ?? 0; if (cnt = 0) { await db.execute(INSERT_SEED_DB); } }
Разбор кода:
import Database from '@tauri-apps/plugin-sql' — импортирует класс Database из плагина Tauri для работы с SQL-базой данных (SQLite, MySQL и др. в зависимости от конфигурации);
import { INSERT_SEED_DB, COUNT_TIMERS } — импортирует SQL-запросы:
COUNT_TIMERS — запрос для получения количества записей в таблице timer;
INSERT_SEED_DB — запрос для вставки демонстрационных таймеров;
import { CountRow } — импортирует TypeScript-тип, описывающий структуру строки результата запроса подсчёта (например, { cnt: number });
export async function seedDb(db: Database) — асинхронная функция, которая принимает подключение к базе данных и выполняет начальное заполнение таблицы;
await db.select<CountRow[]>(COUNT_TIMERS) — выполняет SQL-запрос на выборку:
<CountRow[]> — указывает тип возвращаемых данных (массив строк результата);
результатом будет массив объектов, соответствующих структуре CountRow;
const cnt = count[0]?.cnt ?? 0;:
count[0]?.cnt — безопасно получает значение поля cnt из первой строки результата;
?. — optional chaining (защита от undefined);
?? 0 — если значение отсутствует, используется 0 по умолчанию;
if (cnt = 0) — проверяет, пуста ли таблица timer;
await db.execute(INSERT_SEED_DB); — если таблица пуста, выполняет SQL-запрос вставки демо-данных;
Результат: при первом запуске приложение автоматически заполняет базу демонстрационными таймерами, а при повторных запусках данные не дублируются.
Вы, наверное, уже обратили внимание на неудобные относительные пути в импортах вроде './dml/timerDML', которые режут глаза и сильно неудобны. Для упрощения написания импортов можно указать в client/src/tsconfig.json, чтобы корень приложения ассоциировался с @/. Таким образом не нужно будет прыгать по относительным путям туда-сюда.
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "module": "ESNext", "target": "ES2020", "moduleResolution": "Bundler" } }
Разбор конфига:
baseUrl: "." — задаёт базовую директорию проекта для TypeScript, от которой будут считаться пути;
paths — позволяет создать алиасы для импортов:
"@/*": ["src/*"] — @/ теперь соответствует папке src, что сокращает и упрощает импорты;
module: "ESNext" — указывает формат модулей (поддержка современных ES-модулей);
target: "ES2020" — определяет версию JavaScript, в которую будет транслироваться код;
moduleResolution: "Bundler" — указывает, как TypeScript будет разрешать пути модулей, оптимально для сборщиков вроде Vite или Webpack.
Старые импорты в seed.ts:
import { INSERT_SEED_DB, COUNT_TIMERS } from './dml/timerDML'; import { CountRow } from '../types/timerType'
Новые импорты в seed.ts:
import { INSERT_SEED_DB, COUNT_TIMERS } from '@/db/dml/timerDML' import { CountRow } from '@/types/timerType'
Теперь импорты стали намного удобнее и читаемее.
Далее мне понадобилась функция getDb(), которая инициализирует базу данных и возвращает подключение к ней. Для этого я создал файл client/src/db/initDb.ts.
import Database from '@tauri-apps/plugin-sql'; import { CREATE_TIMER } from '@/db/ddl/timerDDL'; import { getDB_URL } from '@/config'; import { seedDb } from '@/db/seed'; let dbPromise: Promise<Database> | null = null; /** Получает подключение к бд */ export async function getDb(): Promise<Database> { if (!dbPromise) { dbPromise = (async () => { try { const dbUrl = await getDB_URL(); const db = await Database.load(dbUrl); await db.execute(CREATE_TIMER); await seedDb(db); return db; } catch (error) { console.error('Ошибка инициализации базы данных', error); throw error; } })(); } return await dbPromise; }
Функция getDb()
Асинхронная функция, которая возвращает подключение к базе данных (Database);
Использует паттерн singleton: создаётся один экземпляр подключения и повторно используется при последующих вызовах;
Основные шаги при первом вызове:
Получает URL базы данных через getDB_URL();
Загружает базу данных с помощью Database.load(dbUrl);
Создаёт таблицу timer, если она ещё не существует (CREATE_TIMER);
Заполняет таблицу демонстрационными данными через seedDb(db).
В случае ошибки логирует её в консоль и пробрасывает дальше;
При повторных вызовах возвращает уже готовое подключение, не создавая новый экземпляр.
Наконец я дошёл до первой CRUD-операции, которая является частью функционала приложения. Для этого нужно реализовать select-операцию, которая получает список таймеров. CRUD-операции для таймеров будут находиться в client/src/db/crud/timerCrud.ts.
Для проверки данных, которые функция будет получать из базы данных, я создал тип TimersRow.
timerType.ts
export type TimersRow = { id: number; title: string; pomodoro_time: number; count_pomodoro: number; };
timerCrud.ts
import { getDb } from "@/db/initDb"; import { SELECT_TIMERS } from "@/db/dml/timerDML"; import { TimersRow } from "@/types/timerType" /** Получает список таймеров */ export async function getTimers(): Promise<TimersRow[]> { const db = await getDb(); return await db.select<TimersRow[]>(SELECT_TIMERS); }
Функции getTimers():
Асинхронная функция, которая возвращает список всех таймеров из базы данных;
Подключается к базе через getDb();
Выполняет SQL-запрос SELECT_TIMERS и возвращает результат в виде массива объектов типа TimersRow;
Возвращаемый тип: Promise<TimersRow[]>, что гарантирует корректность данных через TypeScript.
Этого достаточно, чтобы начать работать с базой данных. Единственное, чего я пока не реализовал, — это работа с миграциями. Хорошая новость в том, что tauri-plugin-sql их поддерживает. Я обязательно внедрю в этот проект миграции, которые позволят удобно расширять базу данных по мере необходимости, но сделаю это в одной из следующих статей.
Следующее и очень важное, что я должен был реализовать, — это экран «Таймеры». В данном случае это веб-страница со стилями и логикой на TypeScript, которая вызывает CRUD-функцию, получает данные таймеров и отображает их.
Данная статья не о HTML и CSS, поэтому я не буду на них останавливаться, а уделю внимание логике на TypeScript и Vue.
Я написал файл client/src/views/TimersList.ts, который необходим для логики работы экрана Таймеры.
import { defineComponent, onMounted, ref } from 'vue' import { IonIcon } from '@ionic/vue' import { timerOutline, chevronUp, chevronDown, playCircleOutline, pencilOutline, trashOutline, } from 'ionicons/icons' import { getTimers } from '@/db/crud/timerCrud' import type { TimersRow } from '@/types/timerType' export default defineComponent({ name: 'TimersList', components: { IonIcon }, setup() { const timers = ref<TimersRow[]>([]) const loading = ref<boolean>(true) const error = ref<string | null>(null) const expandedId = ref<number | null>(null) const loadTimers = async (): Promise<void> => { loading.value = true error.value = null try { const result = await getTimers() timers.value = result } catch (e: unknown) { error.value = e instanceof Error ? e.message : 'Ошибка загрузки таймеров' } finally { loading.value = false } } const toggleExpand = (id: number): void => { expandedId.value = expandedId.value = id ? null : id } onMounted(loadTimers) return { timers, loading, error, expandedId, toggleExpand, timerOutline, chevronUp, chevronDown, playCircleOutline, pencilOutline, trashOutline, } }, })
Разбор TimersList.vue:
Это Vue 3 компонент, написанный с использованием Composition API.
Импортирует иконки из Ionicons (IonIcon) для отображения интерфейса.
Получает данные из базы через функцию getTimers() из timerCrud.
Основные части:
Состояния (ref):
timers — массив таймеров (TimersRow[]);
loading — индикатор загрузки;
error — текст ошибки (если загрузка не удалась);
expandedId — id таймера, который в данный момент раскрыт (для UI).
Функции:
loadTimers() — асинхронно загружает таймеры из базы, обрабатывает ошибки и обновляет состояние загрузки;
toggleExpand(id) — переключает раскрытие/сворачивание таймера в списке.
Хук жизненного цикла:
onMounted(loadTimers) — при монтировании компонента автоматически запускает загрузку таймеров.
Возврат значений:
Все состояния и функции возвращаются из setup() для использования в шаблоне;
Иконки экспортируются для удобного использования в интерфейсе.
Суть: компонент отображает список таймеров из базы данных, поддерживает индикатор загрузки, обработку ошибок и возможность раскрытия деталей конкретного таймера.
Следующее, что я реализовал в проекте — маршрутизацию. Она нужна, чтобы переходить между страницами приложения и корректно отображать компоненты Vue.
import { createRouter, createWebHistory } from '@ionic/vue-router' import type { RouteRecordRaw } from 'vue-router' import TimersList from '@/views/TimersList/TimersList.vue' const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/timers', }, { path: '/timers', name: 'Timers', component: TimersList, }, ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, }) export default router
Разбор index.ts:
Импортирует функции createRouter и createWebHistory из @ionic/vue-router для настройки маршрутизатора;
Определяет массив маршрутов routes типа RouteRecordRaw[]:
/ — перенаправляет на /timers;
/timers — отображает компонент TimersList.
Создаёт роутер router с историей браузера (createWebHistory) и подключёнными маршрутами;
Экспортирует роутер для использования в основном приложении.
Суть: этот файл отвечает за навигацию внутри приложения, позволяя Vue корректно отображать нужные страницы при переходах.
Мне нужен небольшой, но важный файл, который помогает TypeScript понимать типы, связанные с Vite. Без него редактор ругался на глобальные переменные и импорты из Vite.
/// <reference types="vite/client" />
Строка /// <reference types="vite/client" /> подключает глобальные типы Vite.
Теперь я покажу, как устроен сам компонент Vue для экрана «Таймеры». В этом файле подключаются шаблон, логика на TypeScript и стили, чтобы всё было красиво и работало вместе.
<!-- TimersList.vue --> <template src="@/views/TimersList/TimersList.html"></template> <script lang="ts" src="@/views/TimersList/TimersList.ts"></script> <style src="@/views/TimersList/TimersList.css" scoped></style>
Разбор TimersList.vue:
<template> — подключает HTML-шаблон компонента из отдельного файла TimersList.html;
<script lang="ts"> — подключает TypeScript-логику из TimersList.ts, где описан функционал компонента;
<style scoped> — подключает CSS-стили из TimersList.css и ограничивает их действие только этим компонентом.
Суть: этот файл объединяет шаблон, логику и стили компонента, сохраняя код чистым и модульным.
App.vue — это корневой компонент приложения. Он задаёт базовую структуру и подключает роутер, чтобы страницы отображались корректно.
<template> <ion-app> <ion-router-outlet /> </ion-app> </template> <script setup> import { IonApp, IonRouterOutlet } from '@ionic/vue' </script>
Разбор App.vue:
<template> — оборачивает всё приложение в компонент IonApp из Ionic;
<ion-router-outlet /> — контейнер для отображения страниц в зависимости от маршрута;
<script setup> — импортирует необходимые компоненты Ionic (IonApp, IonRouterOutlet) и подключает их к шаблону.
Суть: этот файл создаёт основу приложения и обеспечивает место для динамического отображения страниц через роутер.
Настало время для «финального аккорда» — файла main.ts, который собирает все компоненты в приложение.
import { createApp } from 'vue' import App from '@/App.vue' import router from '@/router' import { IonicVue } from '@ionic/vue' /* Core CSS required for Ionic components to work properly */ import '@ionic/vue/css/core.css' /* Basic CSS for apps built with Ionic */ import '@ionic/vue/css/normalize.css' import '@ionic/vue/css/structure.css' import '@ionic/vue/css/typography.css' /* Optional CSS utils */ import '@ionic/vue/css/padding.css' import '@ionic/vue/css/float-elements.css' import '@ionic/vue/css/text-alignment.css' import '@ionic/vue/css/text-transformation.css' import '@ionic/vue/css/flex-utils.css' import '@ionic/vue/css/display.css' /* Theme variables */ import '@/theme/variables.css' const app = createApp(App) .use(IonicVue) .use(router) router.isReady().then(() => { app.mount('#app') })
Разбор main.ts:
Импорт Vue и корневого компонента: createApp и App.vue;
Импорт роутера: подключает маршрутизацию;
Использование IonicVue: интегрирует компоненты и стили Ionic.
Подключение CSS:
Core CSS и базовые стили для корректной работы компонентов;
Опциональные утилиты для отступов, выравнивания текста, flex и display;
Темы и переменные проекта (variables.css).
Создание приложения: createApp(App).use(IonicVue).use(router).
Монтирование приложения: ждёт готовности роутера (router.isReady()) и монтирует в элемент с id="app".
Суть: этот файл собирает приложение из компонентов, подключает роутер и стили Ionic и запускает его в браузере.
В конце нужно обязательно поменять .js на .ts в файле index.html.
<script type="module" src="/src/main.ts"></script>
На этом написание кода для экрана «Таймеры» завершено.
В конце прошлой статьи я рассказывал, что Сергей, который помогал мне тестировать приложение на Arch Linux, создал для меня issue в GitHub.
Issue — это запись или сообщение в репозитории, с помощью которого можно описать проблему, задачу или предложение по улучшению проекта. В нём обычно указывают:
что произошло (описание ошибки или задачи);
шаги для воспроизведения проблемы;
ожидаемый результат;
фактический результат;
дополнительные файлы, скриншоты или логи.
Суть: issue помогает отслеживать баги, запросы на улучшения и организовывать работу над проектом в команде.

Описание ошибки: Предоставленный AppImage не запускается в Arch Linux (протестировано на Hyprland/BSPWM). В терминале отображается следующая ошибка: Не удалось создать EGL-дисплей по умолчанию: EGL_BAD_PARAMETER
Основная причина: Это известная проблема с webkit2gtk в дистрибутивах с непрерывным обновлением (например, Arch) при работе в Wayland или некоторых средах X11. Аппаратное ускорение/режим композитинга в WebKit конфликтует с инициализацией EGL-дисплея системы.
Решение (протестировано): Мне удалось успешно собрать проект из исходного кода на моей машине Arch с определенным исправлением. Добавление простого переключения переменной окружения в main.rs перед запуском сборщика Tauri полностью решает проблему.
Предложенное изменение кода: В файле desktop/src-tauri/src/main.rs измените функцию main следующим образом:
fn main() { // Fix for EGL_BAD_PARAMETER on Arch Linux #[cfg(target_os = "linux")] { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); } println!("Starting Tauri application..."); app_lib::run(); }
Локальная сборка: После применения этого изменения я запустил команду cargo tauri dev, и приложение запустилось без проблем.
Бинарный файл: Скомпилированный релизный бинарный файл также работает без каких-либо внешних флагов или переменных окружения.
Дополнительные замечания: Текущая структура проекта несколько сложна (разделена на папки клиента и рабочего стола). Это может вызывать проблемы для автоматических сборщиков или GitHub Actions при создании AppImage, что приводит к отсутствию ресурсов или некорректным путям. Упрощение структуры или обеспечение корректного указания файла tauri.conf.json на ресурсы фронтенда повысит надежность сборки.
Пожалуйста, рассмотрите возможность добавления этого исправления в основную ветку для поддержки пользователей Linux на современных графических процессорах.
В issue написано подробно как устранить проблему, но решение слишком радикальное чтоб делать WEBKIT_DISABLE_COMPOSITING_MODE в 1 у всех на Linux.
WEBKIT_DISABLE_COMPOSITING_MODE — это переменная окружения WebKitGTK, которая принудительно отключает accelerated compositing (аппаратно-ускоренный композитинг/рендеринг) в WebKit, и её нельзя выключать всем пользователям, потому что это глобально переводит WebView на более “safe”, но потенциально более медленный/менее плавный режим и может ухудшить производительность/поведение там, где проблемы с EGL вообще нет.
Вместо радикального решения я добавил флаг --disable-webkit-compositing, которым можно отключить accelerated compositing если возникла ошибка Could not create default EGL display: EGL_BAD_PARAMETER.
И это, к сожалению, не исправило его проблему с запуском AppImage. Одна ошибка сменяла другую. Например, появилась ошибка sqfs_read_range error, которая означает, что при чтении файловой системы SquashFS внутри AppImage произошёл сбой.

Это стало для меня последней каплей. Мало того, что AppImage включает в себя все зависимости и весит 94 МБ (для сравнения, версия deb занимает всего 4,5 МБ), так ещё и в некоторых случаях чтение его файловой системы вызывает проблемы. Поэтому я решил поступить радикально и полностью от него отказаться.
Для Arch Linux есть более правильный вариант, о котором я расскажу чуть позже, а пока я уберу AppImage из проекта.
Фрагмент файла tauri.conf.json:
"targets": ["deb", "rpm"],
Я просто указал в списке "targets" форматы, которые мне нужны, а неуказанные он собирать не будет.
Сегодня я наконец доделал основу приложения Kawai-Focus-v2, которая позволит мне легко расширить проект до того вида, который у меня был на Kivy.
Однако у меня остаётся незакрытый «генштальт» с Arch Linux. Самое правильное для Arch, и это то, о чём меня просили изначально арчеводы, — это добавить описание сборки в AUR.
AUR в Arch — это Arch User Repository: пользовательский репозиторий, поддерживаемый сообществом. Он содержит не готовые бинарные пакеты, как официальные репозитории, а в основном описания сборки (PKGBUILD), по которым собирается пакет (обычно с помощью makepkg) и потом устанавливается через pacman.
В следующей статье для тестирования и более глубокого понимания операционной системы я установлю полноценный Arch Linux на свой ПК. Также попробую на нём собрать и запустить своё приложение и в конце добавить его в AUR.
Я сделал ещё кое-какие настройки для deb пакета, о которых расскажу в следующей статье, так как этот материал не поместился в текущую. Ещё хочу похвастаться, что я выложил предварительные релизы deb и rpm на GitHub. Если кто-то захочет их протестировать на своих системах, пишите о результатах в комментариях.
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Переписана логика с JS на TS;
Исправлена вторая проблема запуска на Arch по issue Сергея (пришлось отключить сборку AppImage).
Репозиторий проекта на Github Kawai-Focus-v2.