Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
run: |
swift test --package-path Tools
swift build --package-path Tools -c release
Tools/.build/release/tatami-tools build-docs --check

- name: Import Developer ID certificate
env:
Expand All @@ -60,7 +61,11 @@ jobs:
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$CERT_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/productsign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
EXISTING_KEYCHAINS=()
while IFS= read -r existing_keychain; do
EXISTING_KEYCHAINS+=("$existing_keychain")
done < <(security list-keychains -d user | sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//')
security list-keychains -d user -s "$KEYCHAIN" "${EXISTING_KEYCHAINS[@]}"

- name: Generate Xcode project
env:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- "Localization/**"
- "docs/**"
- "README.md"
- "CHANGELOG.md"
- "NOTICE.md"
- "THIRD_PARTY_NOTICES.md"
- "web/**"
Expand Down
29 changes: 29 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- SPDX-FileCopyrightText: 2026 PangMo5 and contributors -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->

Write repository guidance, documentation, and commit subjects/bodies in English.

## Shared guidance and personal memory

- Keep conventions needed by all contributors in repository-owned guidance, not only in an agent's personal memory. Update the relevant guidance when a shared rule changes.
- Keep personal preferences and machine-specific details in private local memory, outside repository-wide instructions.
- Keep `AGENTS.md` concise, actionable, and durable. Exclude verbose explanations, task histories, and facts likely to become stale; put version-specific findings, measurements, and temporary status in issues or reports.

For user-facing copy, follow [docs/LOCALIZATION.md](docs/LOCALIZATION.md). Keep agent guidance in English; translate only explicitly listed user documentation.

## Performance and responsiveness

- Reserve the main thread for input, UI state, and AppKit presentation. Run potentially blocking AX/WindowServer IPC, file I/O, parsing, and image processing on appropriate workers.
- Do not assume `async` or `Task` moves work off-main. Check actual isolation, synchronous calls, and locks that the main thread may wait for.
- Preserve request ownership across suspension. Cancellation, app relaunches, window replacement, and workspace changes must prevent stale results from changing focus or presentation.
- Never hold a lock needed by UI readers across IPC or I/O. Preserve atomic persistence and concurrent-edit guarantees when changing execution boundaries.
- Share or coalesce repeated queries and keep input sequences deterministic. Give caches explicit invalidation rules; an unavailable result is not authoritative emptiness.
- Keep focus, clicks, and scrolling connected to the real window. Mirrors, animations, and loading states must not trap input or expose stale content. With FFM disabled, mirrors may remain on hover, but the first click or drag must reach the real window without an extra activation click. Retain the initial input until native focus and presentation are verified, then preserve OS gesture ownership through mouse-up. Background scrolling must not require keyboard focus.
- Measure the reported interaction sequence. Separate input handling, external work, and final presentation; passing tests or CLI completion times do not establish perceived responsiveness.
- Prefer fixing ownership, state, redundant work, or execution boundaries over delays, debouncing, or throttling that worsen responsiveness. Coalesce redundant work without postponing immediate input feedback whenever possible.
- Use timing controls only when the interaction or external system requires them, with evidence for the chosen behavior and latency cost. Do not use arbitrary delays, retries, or silent fallbacks to hide a broken contract or bottleneck.

## Demo recording

- Follow [the VM recording guide](DemoLab/docs/VM-TART.md) for VM ownership and rendering checks. Passing action assertions, decoding, OCR, and frame-drop checks does not establish visual correctness.
- Target Tart VMs by exact name. Other projects may have active VMs; never stop all Tart processes or select the first running VM. Confirm ownership before restarting a guest.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<!-- LANGUAGE-LINKS:START -->
[English](CHANGELOG.md) · [한국어](docs/ko/CHANGELOG.md) · [日本語](docs/ja/CHANGELOG.md) · [简体中文](docs/zh-Hans/CHANGELOG.md) · [繁體中文](docs/zh-Hant/CHANGELOG.md)
<!-- LANGUAGE-LINKS:END -->

# Changelog

All notable changes to Tatami. This file is the source of truth for the release
Expand All @@ -6,6 +10,27 @@ an Install / Update section when publishing). Sparkle's in-app update dialog
accumulates every patch in a release's minor series, so each section here only
needs its own version's changes.

## 1.14.0 (2026-09-09)

More responsive workspace switching and window interaction, with confirmations for changes that affect your saved setup.

### New
- **Confirm changes before applying them:** Review app membership and layout changes, deletions, configuration overwrites, and Guided Setup replacements before they take effect. Confirmation is enabled independently for each action. "Don't ask again" disables only the confirmed action; cancelling keeps its preference unchanged. App membership and layout actions show the result in the same HUD after confirmation.

### Improvements
- **Keep workspace switching and input responsive:** Configuration and layout writes are serialized off input-sensitive paths, redundant pointer updates are coalesced, and stale work is discarded when a window or focus target changes.
- **Adjust app behavior in one place:** App rows expose Auto-open and layout mode directly, with removal in the adjacent menu. The same controls work for workspace apps and Shared Apps, and workspace rows remain centered when no chain is shown.
- **Find settings by task:** A category sidebar separates General, Tiling, Workspaces, Workspace Keys, Focus & Mouse, Gestures, Appearance, and Hooks. Confirmation preferences are grouped by the actions they control.
- **Read and watch workflows in five languages:** In-app documentation now renders the selected language with working headings, tables, and links. The website includes an end-to-end workflow and focused demo collections with native playback controls, keyboard navigation, and links to related settings.

### Fixes
- **Deliver the first click or drag to Always on Top windows:** Always on Top windows retain the initial mouse input through activation and mirror handover. Background scrolling reaches the real window without requiring keyboard focus, and hovering respects Focus Follows Mouse.
- **Preserve layouts when windows are temporarily unavailable:** A failed or incomplete layout read no longer replaces saved positions with an empty layout. Workspace restoration keeps valid layout information until the required windows can be resolved.
- **Skip workspace copies with no changes:** Matching copy sources show "No differences" and cannot be selected. The source and destination are checked again before opening a review, so a stale menu cannot present an empty copy sheet.

### Compatibility
- **Review confirmation preferences for automation:** Existing configurations load with all new per-action confirmations enabled. Persistent app membership and layout commands also use these confirmations through the CLI. For unattended scripts, configure the relevant options in Settings → General → Confirm Before Changes or `[settings.confirmations]`. Back up `config.toml` before downgrading: older versions do not preserve the new confirmation options when saving.

## 1.13.0 (2026-09-03)

### New
Expand Down
35 changes: 35 additions & 0 deletions DemoLab/Localization/Films.json
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,41 @@
"ja": "ショートカットで隣のウインドウを選び、ポインタも移動します。マウスを重ねて選ぶ方法も紹介します。",
"zh-Hans": "用快捷键选择相邻窗口并移动鼠标指针,也可以将指针移入窗口来选中它。",
"zh-Hant": "用快速鍵選擇相鄰視窗並移動滑鼠游標,也可以將游標移入視窗來選取它。"
},
"Cancel keeps the app in its original workspace.": {
"en": "Cancel keeps the app in its original workspace.",
"ko": "취소하면 앱은 원래 워크스페이스에 남습니다.",
"ja": "キャンセルすると、アプリは元のワークスペースに残ります。",
"zh-Hans": "取消后,应用仍留在原工作区。",
"zh-Hant": "取消後,App 仍留在原工作區。"
},
"Review the destination, then confirm the move.": {
"en": "Review the destination, then confirm the move.",
"ko": "대상을 검토한 뒤 이동을 확인하세요.",
"ja": "移動先を確認してから、移動を確定します。",
"zh-Hans": "检查目标工作区,然后确认移动。",
"zh-Hant": "檢查目標工作區,然後確認移動。"
},
"Confirm before replacing the current draft.": {
"en": "Confirm before replacing the current draft.",
"ko": "현재 초안을 교체하기 전에 확인합니다.",
"ja": "現在の下書きを置き換える前に確認します。",
"zh-Hans": "替换当前草稿前先确认。",
"zh-Hant": "取代目前的草稿前先確認。"
},
"Confirm before overwriting existing workspace settings.": {
"en": "Confirm before overwriting existing workspace settings.",
"ko": "기존 워크스페이스 설정을 덮어쓰기 전에 확인합니다.",
"ja": "既存のワークスペース設定を上書きする前に確認します。",
"zh-Hans": "覆盖现有工作区设置前先确认。",
"zh-Hant": "覆寫現有工作區設定前先確認。"
},
"Matching workspaces show No differences and cannot be selected.": {
"en": "Matching workspaces show No differences and cannot be selected.",
"ko": "설정이 같으면 ‘차이 없음’으로 표시되며 선택할 수 없습니다.",
"ja": "設定が同じワークスペースは「違いはありません」と表示され、選択できません。",
"zh-Hans": "设置相同的工作区显示“无差异”,无法选择。",
"zh-Hant": "設定相同的工作區顯示「沒有差異」,無法選取。"
}
}
}
4 changes: 0 additions & 4 deletions DemoLab/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
<!-- LANGUAGE-LINKS:START -->
[English](README.md) · [한국어](docs/ko/README.md) · [日本語](docs/ja/README.md) · [简体中文](docs/zh-Hans/README.md) · [繁體中文](docs/zh-Hant/README.md)
<!-- LANGUAGE-LINKS:END -->

# Tatami Demo Lab

Record the real Tatami, export a consistent set of marketing videos, and place
Expand Down
1 change: 1 addition & 0 deletions DemoLab/Sources/DemoCtlKit/ConfigValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public enum ConfigValidator {
"general": TableSchema(keys: [
"launchAtLogin", "checkForUpdatesAutomatically", "checkInterval", "debugLogging",
]),
"confirmations": TableSchema(keys: ["addWorkspaceApp", "moveWorkspaceApp", "removeWorkspaceApp", "floatWorkspaceApp", "tileWorkspaceApp", "unmanageWorkspaceApp", "addSharedApp", "removeSharedApp", "floatSharedApp", "tileSharedApp", "unmanageSharedApp", "deleteWorkspace", "deleteProfile", "deleteWorkspaceChain", "deleteHook", "removeOverlayException", "copyWorkspace", "copyProfile", "resetSetup", "reloadSetup", "applySetup", "applySetupRecommendation", "deleteSetupWorkspace", "deleteSetupProfile", "uninstallCLI"]),
"visibility": TableSchema(keys: ["overlayAwareApps"]),
"menuBar": TableSchema(keys: [
"showWorkspaceIcon", "showWorkspaceName", "showProfileIcon", "showProfileName",
Expand Down
32 changes: 32 additions & 0 deletions DemoLab/Sources/DemoCtlKit/LiveConfigEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@
// SPDX-License-Identifier: AGPL-3.0-only
import Foundation
public enum LiveConfigEditor {
/// Prepare one existing assignment off camera without changing other scenes' seed.
public static func updateAssignment(file: URL, bundleIdentifier: String, workspace: String, profile: String, autoOpen: Bool) throws {
var lines = try String(contentsOf: file, encoding: .utf8).components(separatedBy: "\n")
var currentProfile: String?, currentWorkspace: String?
var matches = [Range<Int>]()
var index = 0
while index < lines.count {
let header = lines[index].trimmingCharacters(in: .whitespaces)
guard header.hasPrefix("[") else { index += 1; continue }
let start = index + 1
index = start
while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).hasPrefix("[") { index += 1 }
let body = try TomlLite.parse(lines[start..<index].joined(separator: "\n"))
switch header {
case "[[profiles]]": currentProfile = body["name"]?.stringValue; currentWorkspace = nil
case "[[profiles.workspaces]]": currentWorkspace = body["name"]?.stringValue
case "[[profiles.workspaces.apps]]":
if currentProfile == profile && currentWorkspace == workspace && body["bundleIdentifier"]?.stringValue == bundleIdentifier {
matches.append(start..<index)
}
default: break
}
}
guard matches.count == 1, let range = matches.first else { throw DemoCtlError.usage("expected one assignment to prepare") }
let body = lines[range].filter { !$0.trimmingCharacters(in: .whitespaces).hasPrefix("autoOpen =") }
lines.replaceSubrange(range, with: body + ["autoOpen = \(autoOpen)"])
let text = lines.joined(separator: "\n")
let problems = ConfigValidator.problems(in: text)
guard problems.isEmpty else { throw DemoCtlError.usage(problems.joined(separator: "; ")) }
try Data(text.utf8).write(to: file, options: .atomic)
}

public static func update(file:URL,key:String,value:String) throws {
let sections=["borrowDefaultEdge":"switching","borrowFraction":"switching","mouseFollowsFocus":"focus","focusFollowsMouse":"focus","gapInner":"layout","gapOuter":"layout"]
guard let section=sections[key] else {throw DemoCtlError.usage("unsupported live setting: \(key)")}
Expand Down
23 changes: 16 additions & 7 deletions DemoLab/Sources/DemoCtlKit/Scene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ public enum SceneStep: Sendable {
case virtualDisplay(connected:Bool)
case expectCommand(command:String,code:Int)
case expectHook(field:String,value:String)
case click(app: String, identifier: String)
case click(app: String, identifier: String, activate: Bool)
case expectEnabled(app: String, identifier: String, enabled: Bool)
case configureAssignment(app: String, workspace: String, profile: String, autoOpen: Bool)
case rightClick(app: String, identifier: String)
case expectPlacement(app: String, target: String, edge: String)
case expectProfileCount(Int)
case expectAssignment(app: String, workspace: String, profile: String)
case expectAssignment(app: String, workspace: String, profile: String, present: Bool, layout: String?, autoOpen: Bool?)
case typeText(app: String, text: String, intervalMilliseconds: Int)
case expectStory(field: String, value: String)
case expectValue(app: String, identifier: String, value: String)
Expand Down Expand Up @@ -103,7 +105,7 @@ public enum SceneStep: Sendable {
case .rightClick(let app, let id): "context menu \(app) / \(id)"
case .expectPlacement(let app, let target, let edge): "verify \(app) is \(edge) of \(target)"
case .expectProfileCount(let count): "verify \(count) profiles"
case .expectAssignment(let app, let workspace, let profile): "verify \(app) belongs to \(profile)/\(workspace)"
case .expectAssignment(let app, let workspace, let profile, let present, _, _): "verify \(app) is \(present ? "assigned to" : "absent from") \(profile)/\(workspace)"
case .clipboard: "prepare example clipboard content"
case .closeSettings: "close Tatami window"
case .prepareSettings: "size and position the native Tatami window"
Expand All @@ -125,7 +127,9 @@ public enum SceneStep: Sendable {
case .waitWindows: "waitWindows"
case .saveLayout: "saveLayout"
case .assertLayout: "assertLayout"
case .click(let app, let id): "click \(app) / \(id)"
case .expectEnabled(let app, let id, let enabled): "verify control \(app) / \(id) enabled = \(enabled)"
case .configureAssignment(let app, let workspace, let profile, let autoOpen): "prepare \(profile)/\(workspace)/\(app) autoOpen = \(autoOpen)"
case .click(let app, let id, _): "click \(app) / \(id)"
case .typeText(let app, let text, _): "type in \(app): \(text)"
case .expectStory(let field, let value): "verify \(field) = \(value)"
case .expectValue(let app, let id, _): "verify native input \(app) / \(id)"
Expand Down Expand Up @@ -168,7 +172,12 @@ extension SceneStep: Decodable {
case "rightClick": self = .rightClick(app: try container.decode(String.self, forKey: .app), identifier: try container.decode(String.self, forKey: .identifier))
case "expectPlacement": self = .expectPlacement(app: try container.decode(String.self, forKey: .app), target: try container.decode(String.self, forKey: .target), edge: try container.decode(String.self, forKey: .value))
case "expectProfileCount": self = .expectProfileCount(try container.decode(Int.self, forKey: .count))
case "expectAssignment": self = .expectAssignment(app: try container.decode(String.self, forKey: .app), workspace: try container.decode(String.self, forKey: .workspace), profile: try container.decode(String.self, forKey: .profile))
case "expectAssignment":
self = .expectAssignment(app: try container.decode(String.self, forKey: .app), workspace: try container.decode(String.self, forKey: .workspace), profile: try container.decode(String.self, forKey: .profile), present: try container.decodeIfPresent(Bool.self, forKey: .present) ?? true, layout: try container.decodeIfPresent(String.self, forKey: .layout), autoOpen: try container.decodeIfPresent(Bool.self, forKey: .autoOpen))
case "configureAssignment":
self = .configureAssignment(app: try container.decode(String.self, forKey: .app), workspace: try container.decode(String.self, forKey: .workspace), profile: try container.decode(String.self, forKey: .profile), autoOpen: try container.decode(Bool.self, forKey: .autoOpen))
case "expectEnabled":
self = .expectEnabled(app: try container.decode(String.self, forKey: .app), identifier: try container.decode(String.self, forKey: .identifier), enabled: try container.decode(Bool.self, forKey: .enabled))
case "waitWindows":
self = .waitWindows(apps: try container.decode([String].self, forKey: .apps),
timeoutMilliseconds: try container.decodeIfPresent(Int.self, forKey: .timeoutMs) ?? 12000)
Expand All @@ -195,7 +204,7 @@ extension SceneStep: Decodable {
case "expectCommand": self = .expectCommand(command:try container.decode(String.self,forKey:.text),code:try container.decodeIfPresent(Int.self,forKey:.code) ?? 0)
case "expectHook": self = .expectHook(field:try container.decode(String.self,forKey:.field),value:try container.decode(String.self,forKey:.value))
case "click":
self = .click(app: try container.decode(String.self, forKey: .app), identifier: try container.decode(String.self, forKey: .identifier))
self = .click(app: try container.decode(String.self, forKey: .app), identifier: try container.decode(String.self, forKey: .identifier), activate: try container.decodeIfPresent(Bool.self, forKey: .activate) ?? true)
case "typeText":
self = .typeText(app: try container.decode(String.self, forKey: .app), text: try container.decode(String.self, forKey: .text), intervalMilliseconds: try container.decodeIfPresent(Int.self, forKey: .ms) ?? 55)
case "expectStory":
Expand Down Expand Up @@ -320,7 +329,7 @@ extension SceneStep: Decodable {

private enum CodingKeys: String, CodingKey {
case kind, text, ms, note, apps, windows, display, x, y, app, state, count
case identifier, field, value, pixels, target, connected, code
case identifier, field, value, pixels, target, connected, code, activate, present, layout, autoOpen, enabled
case workspace, profile, args, expect, chord, repeats, holdMs
case modifiers, keys, gapMs, releaseAfterMs, timeoutMs, expectApps, settleMs
}
Expand Down
Loading