From 7dc2509f0d4ced9541460b31546b9ab607340762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mu=C3=B1oz?= Date: Tue, 6 Oct 2026 17:07:09 +0200 Subject: [PATCH] Add configurable new-task shortcut --- .github/workflows/check.yml | 5 ++++ CHANGELOG.md | 12 ++++++++ Panel.qml | 12 ++++++++ README.md | 15 ++++++++-- README.ru.md | 15 ++++++++-- ShortcutActions.js | 7 +++++ scripts/check | 9 ++++++ tests/shortcut-actions.test.js | 54 ++++++++++++++++++++++++++++++++++ 8 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 ShortcutActions.js create mode 100644 tests/shortcut-actions.test.js diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6651564..11180ed 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,6 +21,11 @@ jobs: sudo apt-get update sudo apt-get install -y jq qt6-declarative-dev + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Checkout Omarchy shell (qmllint import path) uses: actions/checkout@v5 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb4d09..a0296c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ All notable changes to **Taskwarrior Time** are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- IPC action `newTask` opens the Tasks view and focuses the composer for configurable Hyprland keybindings + ## [1.0.2] - 2026-09-29 ### Fixed @@ -65,6 +71,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии следуют [Semantic Versioning](https://semver.org/lang/ru/). +## [Не выпущено] + +### Добавлено + +- IPC-действие `newTask` открывает вкладку «Задачи» и фокусирует композер для настройки горячих клавиш Hyprland + ## [1.0.2] - 2026-09-29 ### Исправлено diff --git a/Panel.qml b/Panel.qml index 748283d..9187bb5 100644 --- a/Panel.qml +++ b/Panel.qml @@ -6,6 +6,7 @@ import Quickshell.Io import qs.Commons import qs.Ui import "Model.js" as Model +import "ShortcutActions.js" as ShortcutActions import "i18n.js" as I18n Panel { @@ -881,6 +882,16 @@ Panel { refresh() } + function focusNewTaskComposer() { + if (addField) addField.forceActiveFocus() + } + + function openNewTaskFromHotkey() { + ShortcutActions.openNewTask(root, function(callback) { + Qt.callLater(callback) + }) + } + function close() { confirmDelete.opened = false confirmClear.opened = false @@ -4508,6 +4519,7 @@ Panel { function open(): void { root.openFromHotkey() } function close(): void { root.close() } function toggle(): void { root.toggle() } + function newTask(): void { root.openNewTaskFromHotkey() } function about(): void { root.openFromHotkey() root.viewMode = "about" diff --git a/README.md b/README.md index 5c23e6b..05c92d9 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Anything you do here is stored in your normal Taskwarrior / Timewarrior data. Yo - **Projects** view: browse projects, rename, clear project from tasks - **Unsaved changes** dialog when closing a dirty editor (Save / Keep editing / Discard) - Uniform **hotkeys**: `Ctrl+Enter` commit, `Esc` cancel, `Ctrl+Delete` destroy +- **Configurable Hyprland shortcut** to open the new-task composer - **i18n**: English and Russian UI (system language, or pick one in About) - **About** tab with version, developer info, What’s new / Marketplace / Report a problem / GitHub, language switch, and debug logging @@ -145,6 +146,16 @@ This disables the widget and deletes the git checkout under `~/.config/omarchy/p ![About & debug](docs/screenshots/en/03-about.png) +### Keyboard shortcut (Hyprland) + +The plugin exposes a `newTask` action through Quickshell IPC. Omarchy uses Lua for Hyprland bindings; add this to `~/.config/hypr/bindings.lua` (change the key combination if needed): + +```lua +o.bind("SUPER + SHIFT + T", "New task", "qs ipc --path /usr/share/omarchy/shell call taskwarrior-time newTask") +``` + +Replace `/usr/share/omarchy/shell` if your Omarchy shell lives elsewhere. Omarchy normally reloads Hyprland config on save; run `hyprctl reload` if needed. The shortcut opens the Tasks view and focuses the new-task title field; it does not create a task until you save one. + ## Debug logging When something misbehaves, turn on debug logging from the **About** tab. @@ -180,11 +191,11 @@ Please do **not** paste secrets, tokens, or private task content. ## Development checks -From a clone of this repository: +From a clone of this repository (Node.js 22+ required for behavior tests): ```bash ./scripts/enable-git-hooks # once per clone — enables pre-push checks -./scripts/check # manifest validate + qmllint +./scripts/check # manifest validate + behavior tests + qmllint ``` `git push` runs the same checks via `.githooks/pre-push` and aborts if they fail. GitHub Actions runs `./scripts/check` on push and pull requests. diff --git a/README.ru.md b/README.ru.md index 383f9c8..68e2255 100644 --- a/README.ru.md +++ b/README.ru.md @@ -29,6 +29,7 @@ - Вкладка **Проекты**: просмотр, переименование, очистка проекта у задач - Диалог **несохранённых изменений** при закрытии черновика (Сохранить / Продолжить / Отменить) - Единые **горячие клавиши**: `Ctrl+Enter` — сохранить/добавить, `Esc` — отмена, `Ctrl+Delete` — удалить/сбросить +- **Настраиваемая горячая клавиша Hyprland** для открытия формы новой задачи - **Локализация**: английский и русский (системный язык или выбор на вкладке «О плагине») - Вкладка **О плагине**: версия, разработчик, Что нового / Маркетплейс / Сообщить о проблеме / GitHub, язык интерфейса и отладочный лог @@ -145,6 +146,16 @@ omarchy plugin remove taskwarrior-time ![О плагине и отладка](docs/screenshots/ru/03-about.png) +### Горячая клавиша (Hyprland) + +Плагин предоставляет действие `newTask` через Quickshell IPC. В Omarchy биндинги Hyprland задаются на Lua; добавьте это в `~/.config/hypr/bindings.lua` (при необходимости замените сочетание): + +```lua +o.bind("SUPER + SHIFT + T", "New task", "qs ipc --path /usr/share/omarchy/shell call taskwarrior-time newTask") +``` + +Если Omarchy shell установлен в другом месте, замените `/usr/share/omarchy/shell`. Конфигурация Hyprland обычно перезагружается при сохранении; если сочетание не появилось, выполните `hyprctl reload`. Оно открывает вкладку «Задачи» и фокусирует поле названия; задача создаётся только после сохранения. + ## Отладочный лог Если что-то ведёт себя странно, включите отладку на вкладке **О плагине**. @@ -180,11 +191,11 @@ omarchy plugin remove taskwarrior-time ## Проверки при разработке -В клоне этого репозитория: +В клоне этого репозитория (для поведенческих тестов нужен Node.js 22+): ```bash ./scripts/enable-git-hooks # один раз на клон — включает pre-push -./scripts/check # validate манифеста + qmllint +./scripts/check # validate манифеста + поведенческие тесты + qmllint ``` `git push` прогоняет те же проверки через `.githooks/pre-push` и отменяет push при ошибке. GitHub Actions запускает `./scripts/check` на push и pull request. diff --git a/ShortcutActions.js b/ShortcutActions.js new file mode 100644 index 0000000..21795b3 --- /dev/null +++ b/ShortcutActions.js @@ -0,0 +1,7 @@ +function openNewTask(panel, defer) { + panel.viewMode = "tasks" + panel.openFromHotkey() + defer(function() { + panel.focusNewTaskComposer() + }) +} diff --git a/scripts/check b/scripts/check index a419b48..93184ce 100755 --- a/scripts/check +++ b/scripts/check @@ -40,6 +40,15 @@ else fi fi +# --- shortcut behavior tests ------------------------------------------------ +section "shortcut behavior tests" +if command -v node >/dev/null 2>&1; then + node --test tests/shortcut-actions.test.js || FAILED=1 +else + echo "shortcut behavior tests: node is required" >&2 + FAILED=1 +fi + # --- 2) qmllint ------------------------------------------------------------- section "qmllint" diff --git a/tests/shortcut-actions.test.js b/tests/shortcut-actions.test.js new file mode 100644 index 0000000..c497321 --- /dev/null +++ b/tests/shortcut-actions.test.js @@ -0,0 +1,54 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +const root = path.join(__dirname, ".."); +const context = {}; +vm.createContext(context); +vm.runInContext(fs.readFileSync(path.join(root, "ShortcutActions.js"), "utf8"), context); + +test("newTask IPC handler connects to the deferred focus path", () => { + const panel = fs.readFileSync(path.join(root, "Panel.qml"), "utf8"); + assert.match(panel, /function newTask\(\): void \{ root\.openNewTaskFromHotkey\(\) \}/); + assert.match(panel, /ShortcutActions\.openNewTask\(root, function\(callback\) \{\s*Qt\.callLater\(callback\)/); +}); + +test("composer focus retains the unsaved-editor guard", () => { + const panel = fs.readFileSync(path.join(root, "Panel.qml"), "utf8"); + const fieldStart = panel.indexOf("id: addField"); + const buttonStart = panel.indexOf("id: addBtnWrap", fieldStart); + assert.notEqual(fieldStart, -1); + assert.notEqual(buttonStart, -1); + const focusHandler = panel.slice(fieldStart, buttonStart); + + assert.match(panel, /function focusNewTaskComposer\(\) \{\s*if \(addField\) addField\.forceActiveFocus\(\)\s*\}/); + assert.match(focusHandler, /root\.requestCloseEditor\(\{ type: "dismiss", thenCompose: true \}\)/); + assert.match(focusHandler, /if \(root\.editDirty\) \{\s*[\s\S]*?return\s*\}/); +}); + +test("new-task action opens Tasks before deferring composer focus", () => { + const events = []; + let focusCallback; + let createdTasks = 0; + const panel = { + viewMode: "about", + openFromHotkey() { events.push("open"); }, + focusNewTaskComposer() { events.push("focus"); }, + addTask() { createdTasks++; }, + }; + + context.openNewTask(panel, (callback) => { + events.push("defer"); + focusCallback = callback; + }); + + assert.equal(panel.viewMode, "tasks"); + assert.deepEqual(events, ["open", "defer"]); + assert.equal(createdTasks, 0); + + focusCallback(); + assert.deepEqual(events, ["open", "defer", "focus"]); + assert.equal(createdTasks, 0); +});