From a57ade03d4db60738ac4ae2b42a66980c9028ef7 Mon Sep 17 00:00:00 2001 From: Greem4 Date: Sun, 16 Aug 2026 20:12:14 +0400 Subject: [PATCH 1/3] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=80=D0=B0=D0=B7=D1=80=D1=8F=D0=B4=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D0=B8:=20keep=20=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=B5=D1=81=D0=BB=D0=B8=20=D1=82=D0=B5=D0=B3=20?= =?UTF-8?q?=D1=81=D0=B2=D0=BE=D0=B1=D0=BE=D0=B4=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-on-merge.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-on-merge.yml b/.github/workflows/release-on-merge.yml index 32d02f7..06cdc33 100644 --- a/.github/workflows/release-on-merge.yml +++ b/.github/workflows/release-on-merge.yml @@ -75,8 +75,14 @@ jobs: fi # Версию подняли прямо в PR — уважаем её и не поднимаем второй раз. - if printf '%s\n' "$FILES" | grep -qx 'Sources/ClaudeWeekCore/Version.swift'; then - echo "Version.swift поднят в самом PR — беру его версию как есть" + # Но присутствие Version.swift в списке файлов этого не доказывает: PR + # «Локализация» правил в нём соседние строки, версия осталась + # выпущенной 0.1.10, разряд вышел keep, и релиз упал на занятом теге. + # Признак настоящий один — номер в файле ещё не выпущен тегом. + CURRENT="$(sed -n 's/.*static let version = "\(.*\)".*/\1/p' \ + Sources/ClaudeWeekCore/Version.swift)" + if [ -n "$CURRENT" ] && ! git rev-parse -q --verify "refs/tags/v$CURRENT" >/dev/null; then + echo "версия $CURRENT в Version.swift ещё не выпущена — беру её как есть" KIND=keep fi From 338dce3c7b3e0eb1a55903305b2245a52a47b9fa Mon Sep 17 00:00:00 2001 From: Greem4 Date: Sun, 16 Aug 2026 20:12:50 +0400 Subject: [PATCH 2/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B1=D1=83=20=D0=BF=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BD=D0=B0=20=D0=B4=D0=B2=D1=83=D1=85=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/probe-panel.swift | 139 +++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/scripts/probe-panel.swift b/scripts/probe-panel.swift index 8ce0b7c..022e313 100755 --- a/scripts/probe-panel.swift +++ b/scripts/probe-panel.swift @@ -18,6 +18,14 @@ // `collectionBehavior` у `MenuPanel`: проверять такое чтением кода // бесполезно, см. docs/SPACES.md. // +// swift scripts/probe-panel.swift --displays +// То же самое для двух мониторов: по полоске на каждый, поведение окна — +// как у панели. Отвечает на вопросы, от которых зависит весь показ на +// втором мониторе: что говорит `isOnActiveSpace` про окно на соседнем +// мониторе, когда столы переключают на этом, и остаётся ли окно на своём +// мониторе при показе. Заодно печатает, где лежит окно значка +// ClaudeWeek — панель считает место от него. +// // В обоих режимах пройдитесь по рабочим столам — свайпом или ⌃→. Разбор // собранного и что с ним делать: docs/SPACES.md. // @@ -121,8 +129,11 @@ func watch() -> Never { // MARK: - Режим --behaviors -func makeProbePanel(tag: String, behavior: NSWindow.CollectionBehavior, y: CGFloat) -> NSPanel { - let screen = NSScreen.main!.frame +func makeProbePanel( + tag: String, behavior: NSWindow.CollectionBehavior, y: CGFloat, + on display: NSScreen = NSScreen.main ?? NSScreen.screens[0] +) -> NSPanel { + let screen = display.frame let panel = NSPanel( contentRect: NSRect(x: screen.midX - 200, y: y, width: 400, height: 40), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false @@ -190,12 +201,134 @@ func compareBehaviors() -> Never { exit(0) } +// MARK: - Режим --displays + +/// Слой окна пункта строки меню: `NSWindow.Level.statusBar`. По нему в списке +/// окон находится значок ClaudeWeek — панель считает место от его рамки, и на +/// двух мониторах важно, где эта рамка лежит в момент щелчка. +let statusLayer = 25 + +/// Номер монитора под точкой (координаты AppKit, снизу вверх). +func display(at point: NSPoint) -> String { + guard let index = NSScreen.screens.firstIndex(where: { $0.frame.contains(point) }) + else { return "—" } + return "\(index + 1)" +} + +/// Номер монитора под окном из списка окон. Список отдаёт координаты сверху +/// вниз от главного экрана, NSScreen — снизу вверх; без приведения к одному +/// виду монитор определяется неверно ровно на втором экране. +func display(ofListed rect: CGRect) -> String { + let top = NSScreen.screens[0].frame.maxY + return display(at: NSPoint(x: rect.midX, y: top - rect.midY)) +} + +/// Монитор, который окно считает своим (`NSWindow.screen` — тот, где лежит +/// большая часть окна). «нет» у снятого с экрана окна. +func display(ofWindow window: NSWindow) -> String { + guard let screen = window.screen, + let index = NSScreen.screens.firstIndex(of: screen) + else { return "нет" } + return "\(index + 1)" +} + +func claudeWeekPID() -> pid_t? { + NSWorkspace.shared.runningApplications.first { + $0.localizedName == "ClaudeWeek" || $0.executableURL?.lastPathComponent == "ClaudeWeek" + }?.processIdentifier +} + +/// Рамка окна значка — того самого `anchor`, от которого считается панель. +func statusItemFrame(pid: pid_t) -> CGRect? { + guard let list = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[String: Any]], + let item = list.first(where: { + ($0[kCGWindowOwnerPID as String] as? pid_t) == pid + && ($0[kCGWindowLayer as String] as? Int) == statusLayer + }), + let bounds = item[kCGWindowBounds as String] as? [String: CGFloat] + else { return nil } + return CGRect(x: bounds["X"] ?? 0, y: bounds["Y"] ?? 0, + width: bounds["Width"] ?? 0, height: bounds["Height"] ?? 0) +} + +func compareDisplays() -> Never { + let screens = NSScreen.screens + guard screens.count > 1 else { + say("подключён один монитор — сравнивать нечего. Подключите второй и повторите") + exit(1) + } + + // По полоске на каждый монитор, поведение окна — как у панели. + let strips = screens.enumerated().map { index, screen in + makeProbePanel( + tag: "D\(index + 1) монитор \(index + 1) — поведение панели", + behavior: [.moveToActiveSpace, .fullScreenAuxiliary, .stationary, .ignoresCycle], + y: screen.frame.maxY - 140, + on: screen + ) + } + let pid = claudeWeekPID() + if pid == nil { say("ClaudeWeek не запущен — про значок строки меню данных не будет") } + + func status() -> String { + var parts = ["активный стол:\(activeSpace())"] + if let pid, let rect = statusItemFrame(pid: pid) { + parts.append(String(format: "значок: монитор %@ x%.0f", display(ofListed: rect), rect.midX)) + } + for (index, strip) in strips.enumerated() { + parts.append("D\(index + 1): монитор=\(display(ofWindow: strip))" + + " на своём столе=\(strip.isOnActiveSpace ? "да " : "НЕТ")" + + " столы=\(spaces(ofWindow: strip.windowNumber))") + } + return parts.joined(separator: " | ") + } + + say("по полоске на каждом мониторе, 70 секунд. Переключайте столы на обоих" + + " мониторах и щёлкайте по значку — на том мониторе и на соседнем") + + NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { _ in + let point = NSEvent.mouseLocation + say(String(format: "%@ клик x%.0f y%.0f монитор %@ | %@", + stamp(), point.x, point.y, display(at: point), status())) + // Значок переезжает вслед за строкой меню, и вопрос в том, успевает ли + // он к обработке щелчка: панель считает место от его рамки. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + say("\(stamp()) через 0.3 с | \(status())") + } + } + + var previous = "" + var ticks = 0 + let timer = Timer(timeInterval: 0.5, repeats: true) { _ in + ticks += 1 + let line = status() + if line != previous { + previous = line + say("\(stamp()) \(line)") + } + // Раз в четыре секунды показываем заново — так показывается панель. + // Здесь и видно, остаётся окно на своём мониторе или уезжает. + if ticks.isMultiple(of: 8) { + for strip in strips { + strip.orderOut(nil) + strip.orderFrontRegardless() + } + } + if ticks >= 140 { exit(0) } + } + RunLoop.main.add(timer, forMode: .common) + NSApplication.shared.setActivationPolicy(.accessory) + NSApplication.shared.run() + exit(0) +} + // MARK: - Запуск switch CommandLine.arguments.dropFirst().first { case "--watch", nil: watch() case "--behaviors": compareBehaviors() +case "--displays": compareDisplays() case let other?: - say("не знаю такого режима: \(other). Есть --watch и --behaviors") + say("не знаю такого режима: \(other). Есть --watch, --behaviors и --displays") exit(2) } From e20cb5e14c3850558cb4dd41b1cdbc278ac7a1da Mon Sep 17 00:00:00 2001 From: Greem4 Date: Sun, 16 Aug 2026 20:16:47 +0400 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9E=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=80=D0=B0=D0=B7=D1=80=D1=8F=D0=B4=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D0=B8=2099=20=D1=81=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=BE=D0=BC=20=D0=B2=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D1=88=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ CONTRIBUTING.md | 10 ++++++++-- scripts/bump-version.sh | 10 ++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5d8fed..a21f48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,15 @@ обязанность, она не пережила первого же выпуска — записи 0.1.10 уехали в релиз, так и оставшись под «не выпущено»; заодно эта версия закрыта задним числом. +- Разряд версии считает до 99 и переносится в старший: 0.1.99 → 0.2.0, + 0.99.0 → 1.0.0. Раньше счёт шёл дальше — 0.1.100. + +### Исправлено + +- Разряд «версию подняли в самом PR» определяется по свободному тегу, а не по + тому, попал ли `Version.swift` в список изменённых файлов. PR с локализацией + правил в этом файле соседние строки, workflow счёл версию поднятой, оставил + выпущенную 0.1.10 и остановился на занятом теге — сборка не вышла. ## [0.1.10] — 2026-08-16 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 030fece..b8a7c27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,10 +96,16 @@ ModelsWindowController». Внутренняя правка, о которой Релиз не выйдет и без метки `без-релиза`, если PR не тронул ничего, кроме `*.md` и `docs/`: выпускать сборку ради правки текста незачем. +Разряд считает до 99 и переносится в старший: 0.1.99 → 0.2.0, 0.99.0 → 1.0.0. +Двузначные числа законны — 0.1.10 идёт после 0.1.9, а не наоборот. + Версию в `Sources/ClaudeWeekCore/Version.swift` руками поднимать не надо — подняли в самом PR, workflow это увидит и выпустит ровно ту цифру, что в файле, -а не следующую. После слияния сделайте `git pull`: в `main` уехал коммит бота — -с новой версией и закрытым разделом журнала. +а не следующую. Видит он это по тегу: номер в файле ещё не выпущен — значит его +и берём. Правка соседних строк того же файла на разряд не влияет — раньше +влияла, и релиз локализации встал на уже занятом теге. После слияния сделайте +`git pull`: в `main` уехал коммит бота — с новой версией и закрытым разделом +журнала. Посчитать версию локально — тем же скриптом, которым это делает workflow: diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 57da237..1046e43 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -7,6 +7,10 @@ # ./scripts/bump-version.sh keep версию не трогать, только показать # ./scripts/bump-version.sh patch --dry-run посчитать, ничего не записывая # +# Разряд считает до 99 и переносится в старший: 0.1.99 → 0.2.0, 0.99.0 → 1.0.0, +# 0.99.99 → 1.0.0. Двузначные числа при этом законны — 0.1.10 идёт после 0.1.9, +# и в приложении версии сравниваются числами, а не строками (см. Updater.swift). +# # Единственный источник версии — Version.swift: оттуда её берёт make-app.sh для # Info.plist, оттуда же release.yml сверяет тег. Поэтому скрипт правит один # файл и больше ничего: не коммитит, не тегирует, не пушит — это делает тот, @@ -51,6 +55,12 @@ case "$KIND" in patch) PATCH=$((PATCH + 1)) ;; esac +# Больше 99 в разряде не бывает: переполнение уходит в старший, как в счётчике. +# Патч, добравшийся до сотни на 0.99.99, поднимает и минор, и мажор — отсюда +# две проверки подряд, а не одна. +if [ "$PATCH" -gt 99 ]; then PATCH=0; MINOR=$((MINOR + 1)); fi +if [ "$MINOR" -gt 99 ]; then MINOR=0; MAJOR=$((MAJOR + 1)); fi + VERSION="$MAJOR.$MINOR.$PATCH" echo "версия: $CURRENT → $VERSION ($KIND)" >&2