diff --git a/.gitignore b/.gitignore index cfcbfcf..9ffaa3d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,10 @@ # Cursor IDE .cursor/ -# Large animation assets (downloaded separately) +# Large animation scratch files assets/*.mov assets/*.mp4 +!assets/cat-bedtime.mov assets/*.backup-* # Compiled binaries diff --git a/.rule.md b/.rule.md new file mode 100644 index 0000000..412f3a7 --- /dev/null +++ b/.rule.md @@ -0,0 +1,31 @@ +这是项目的文件目录,根据你当前的任务,维护和查阅相关有关文档,不需要一口气阅读所有文档 + +当代码做出调整后,也请维护相关文档,保持文档和代码是同步的 + +Plan和需求文档的区别是,我下的指令应该先进plan,plan是会动态不断变化的,刷新频率高,需求文档是现有项目的现状,刷新频率较低 + +ARCHITECTURE.md +docs/ +├── core-beliefs.md #产品设计的核心理念 +├── tech-docs/ #技术架构 +│ ├── index.md +│ └── ... +├── exec-plans/ # 如果遇到复杂任务,先写Plan +│ ├── active/ #正在执行的 +│ ├── completed/ #已完成的,归档 +│ └── tech-debt-tracker.md +├── generated/ +│ └── db-schema.md +├── product-specs/ #需求文档,详细写明产品的需求内容 +│ ├── index.md +│ ├── new-user-onboarding.md +│ └── ... +├── references/ # 可能会使用的参考资料 +│ └── ... +├── DESIGN.md +├── FRONTEND.md +├── FUTURE_Plan.md #未来可能的改进,由我编写 +├── PRODUCT_SENSE.md +├── QUALITY_SCORE.md +├── RELIABILITY.md +└── SECURITY.md \ No newline at end of file diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..5b9e11a --- /dev/null +++ b/AGENT.md @@ -0,0 +1,35 @@ +这是项目的文件目录,根据你当前的任务,维护和查阅相关有关文档,不需要一口气阅读所有文档 + +当代码做出调整后,也请维护相关文档,保持文档和代码是同步的 + +我下的需求和指令应该先进plan,plan是会动态不断变化的,刷新频率高,需求文档是现有项目的现状,刷新频率较低,我让你刷新后你基于plan的历史进行刷新 + +ARCHITECTURE.md +docs/ +├── core-beliefs.md #产品设计的核心理念,必读 +├── tech-docs/ #技术架构 +│ ├── index.md +│ └── ... +├── task/ # 如果遇到复杂任务,先写任务plan到task文件,积极维护这个文件,这是任务的起点,要写一下任务时间 +│ ├── active/ #正在执行的task,需要写清楚产品需求、技术方案、验收标准 +│ ├── completed/ #我没有异议和没有后续调整的,进行归档,自动归档条件:该任务已经过去半小时了没有修改 +│ └── tech-debt-tracker.md +├── generated/ +│ └── db-schema.md +├── product-specs/ #需求文档,详细写明产品的需求内容,不许出现技术语言 +│ ├── index.md +│ ├── new-user-onboarding.md +│ └── ... +├── references/ # 可能会使用的参考资料 +│ └── ... +├── DESIGN.md +├── FRONTEND.md +├── FUTURE_Plan.md #未来可能的改进,由我编写 +├── PRODUCT_SENSE.md +├── QUALITY_SCORE.md +├── RELIABILITY.md +└── SECURITY.md + +## 发版提醒 + +每次发布新版本时,必须同步更新 `README.md` 和 `README_EN.md` 里的 GitHub Release 下载链接,让 DMG 和 CLI 链接指向最新 tag。否则用户会继续下载旧包,可能遇到旧包缺少 `assets/cat-bedtime.mov` 导致睡眠动画显示“缺少猫猫动画素材”。 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b2561c3..a2ec6c3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,8 @@ # Cat Bedtime · 猫猫困了 — 架构 -本文档描述当前代码现状。产品英文名 **Cat Bedtime**,中文名 **猫猫困了**;已从早期的 TimeToSleep 契约叙事转为猫猫睡觉叙事,但运行时目录和 launchd label 仍保留 `timetosleep`,用于兼容已有安装 +本文档描述**当前代码现状**。产品英文名 **Cat Bedtime**,中文名 **猫猫困了**;运行时目录和 launchd label 仍保留 `timetosleep`,用于兼容已有安装。 + +详细需求见 [`docs/product-specs/`](docs/product-specs/index.md),技术细节见 [`docs/tech-docs/`](docs/tech-docs/index.md),踩坑见 [`docs/RELIABILITY.md`](docs/RELIABILITY.md)。 ## 目录结构 @@ -8,200 +10,170 @@ cat-bedtime/ ├── bin/ │ ├── zzz # CLI 入口 -│ └── zzz-overlay # 预编译 Swift 全屏覆盖层 +│ ├── zzz-overlay # 预编译 Swift 全屏覆盖层(universal binary) +│ └── Cat Bedtime.app # 构建产物(可执行文件 zzz-app) ├── lib/ -│ ├── config.sh # 配置读写与时间工具 -│ ├── schedule.sh # launchd agent 安装 / 更新 / 卸载 -│ ├── stats.sh # 猫猫来访记录和连续记录计算 -│ └── ui.sh # 终端 UI 组件 +│ ├── config.sh # 配置读写、时间工具、锁屏窗口校验 +│ ├── schedule.sh # launchd 安装 / 更新 / 卸载、catch-up +│ ├── stats.sh # 到访记录 +│ ├── ui.sh # 终端 UI +│ ├── i18n.sh / i18n.py # 多语言 msg 解析 ├── src/ │ ├── cli/ -│ │ ├── init.sh # zzz init 领养流程 +│ │ ├── init.sh # zzz init │ │ ├── daemon.sh # 睡前提醒 → 锁屏 → 唤醒恢复 -│ │ ├── media.sh # 暂停媒体、保存 / 恢复音量 -│ │ └── brightness.sh # 保存 / 恢复 / 渐变亮度 +│ │ ├── media.sh +│ │ └── brightness.sh │ ├── app/ -│ │ ├── CatBedtimeApp.swift # App 版入口 -│ │ └── build.sh # 编译 App bundle +│ │ ├── CatBedtimeApp.swift +│ │ ├── build.sh # 打包 App bundle + Resources +│ │ └── ... +│ ├── shared/L10n.swift # App 侧 i18n │ └── overlay/ -│ ├── LockScreen.swift # 正式视频猫猫锁屏覆盖层 -│ ├── build.sh # 编译正式覆盖层 -│ ├── CatBedtimePreview.swift # 手绘猫猫动效预览 -│ ├── build-cat-bedtime-preview.sh # 手绘预览编译脚本 -│ ├── CatVideoBedtimePreview.swift # 视频合成预览 -│ └── build-cat-video-preview.sh # 视频预览编译脚本 -├── tests/ # Bash 回归测试 -├── scripts/ -│ ├── release-macos.sh # App DMG 发布包 -│ └── release-cli-macos.sh # CLI tar.gz 发布包 -├── install.sh # 安装脚本 -├── README.md # 中文产品说明 -├── README_EN.md # 英文产品说明 -└── PITFALLS.md # 历史踩坑与回归风险 +│ ├── LockScreen.swift # 正式锁屏 +│ ├── build.sh +│ ├── CatBedtimePreview.swift # 开发预览(不参与正式安装) +│ └── CatVideoBedtimePreview.swift +├── locales/messages.json +├── scripts/generate-messages.py +├── tests/test-*.sh +├── docs/ # 见 .rule.md +├── install.sh +└── README.md / README_EN.md ``` ## 运行时数据 -安装后数据存储在 `~/.timetosleep/`: +`~/.timetosleep/`: ```text ~/.timetosleep/ -├── bin/ # 安装后的 zzz 和 zzz-overlay -├── lib/ # 安装后的库脚本 -├── src/ # 安装后的运行脚本 -├── assets/ # 猫猫锁屏视频素材 -├── config.json # 猫猫日程配置 -├── stats.json # 到访 / 请假记录 -├── saved_brightness # 锁屏前亮度备份 -├── saved_volume # 锁屏前音量备份 -├── skip_tonight # 今晚请假标记 -└── daemon.log # daemon 日志 +├── bin/zzz, bin/zzz-overlay # CLI 安装复制 +├── lib/, src/cli/, assets/ +├── config.json +├── stats.json +├── onboarding_install_id # App:当前安装是否完成 onboarding +├── skip_tonight # 今晚请假 +├── postpone_tonight # 今晚晚点(有效 bedtime + 原因) +├── saved_brightness, saved_volume +└── daemon.log # launchd 标准输出/错误 ``` ## 两种安装方式 -Cat Bedtime 维护两条用户安装路径: - -| 方式 | 发布产物 | 用户入口 | 运行资源来源 | +| 方式 | 产物 | 入口 | 资源 | | --- | --- | --- | --- | -| App 版 | `dist/Cat-Bedtime-macOS.dmg` | `Cat Bedtime.app` | app bundle 内的 `Contents/Resources/` | -| CLI 版 | `dist/cat-bedtime-cli-macos.tar.gz` | `zzz` 命令 | `~/.timetosleep/` | - -两者共享用户数据目录 `~/.timetosleep/`,包括配置、记录、日志和 launchd plist。用户安装任一版本即可;同时安装时,最后一次写入配置 / 注册 launchd 的版本会成为当前定时任务入口 - -## 核心流程 - -### 1. 安装 - -CLI 版的 `install.sh` 做四件事: - -1. 检查 macOS 和 `python3` -2. 复制 `bin/zzz`、`bin/zzz-overlay`、`lib/`、`src/cli/` 和 `assets/` 到 `~/.timetosleep/` -3. 创建 `zzz` 命令链接或写入 shell PATH -4. 打开一个 Terminal 窗口运行 `zzz init` - -正式安装不需要 Xcode;发布包内的 `bin/zzz-overlay` 已经是预编译二进制。CLI 版不会安装 `Cat Bedtime.app` - -App 版通过 DMG 拖拽安装。发布脚本会把 `zzz-overlay`、`lib/`、`src/cli/` 和 `assets/` 打进 `Cat Bedtime.app/Contents/Resources/`,所以 app 拖到 `/Applications` 后可以独立注册 launchd 和运行锁屏流程 - -### 2. 领养设置 +| App | `dist/Cat-Bedtime-macOS.dmg` | `Cat Bedtime.app` | `Contents/Resources/`:`bin/zzz-overlay`、`src/cli/`、`lib/`、`assets/`、`locales/` | +| CLI | `dist/cat-bedtime-cli-macos.tar.gz` | `zzz` | 复制到 `~/.timetosleep/` | + +App 可执行文件为 **`Contents/MacOS/zzz-app`**。daemon 通过该二进制调用 `--toast-bedtime-warning` 等参数。 + +两者共享 `~/.timetosleep/`。同时安装时,**最后一次**写入配置 / 注册 launchd 的版本生效。 + +## 全局流程 + +```mermaid +flowchart TD + U["用户"] --> App["Cat Bedtime.app / zzz-app"] + U --> CLI["zzz CLI"] + App --> Config["config.json"] + CLI --> Config + App --> State["skip_tonight / postpone_tonight"] + CLI --> State + App --> Launchd["com.timetosleep.daemon"] + CLI --> Launchd + Launchd --> Daemon["daemon.sh"] + Daemon --> Toast["zzz-app --toast-bedtime-warning"] + Daemon --> Overlay["zzz-overlay"] + Overlay --> Screens["多显示器"] + Daemon --> Restore["亮度音量 + Focus 恢复"] +``` -`zzz init` 会执行 `src/cli/init.sh`: +## 核心约束 -1. 展示猫猫领养说明 -2. 收集猫猫睡觉时间、离开时间、来住日子和提前提醒时间 -3. 要求用户输入确认句 -4. 写入 `config.json`,初始化 `stats.json` -5. 调用 `lib/schedule.sh` 注册 launchd agent +- 正式锁屏唯一实现:`src/cli/daemon.sh` + `src/overlay/LockScreen.swift`。 +- daemon 是提醒与锁屏的**唯一执行者**;App 不做每周预排程。 +- 锁屏窗口(bedtime → wakeup)须 **0 < 时长 < 15h**(`MAX_LOCK_MINUTES=900`)。 +- 睡前提醒:**T-5min / T-1min 强制 App Toast**(`notify.bedtime.*`),不依赖 Notification Center。 +- T-1 Toast 可**一次性推迟 5 分钟**(写 `postpone_tonight`,App 进程 exit 2)。 -### 3. 每晚流程 +### winddown 的两层含义(易混) -`com.timetosleep.daemon` 在 `bedtime - winddown_minutes` 触发 `src/cli/daemon.sh` +| 概念 | 来源 | 作用 | +| --- | --- | --- | +| `config.winddown_minutes` | 配置(默认 5;CLI 可改 5–120) | launchd **何时启动** daemon(`bedtime - winddown`) | +| `wind_down()` 内 `total_min=5` | daemon 硬编码 | **Toast 与渐变** 仍按睡前 5 分钟编排(T-5 / T-1) | -流程: +App Dashboard **不提供** winddown 编辑;仅 CLI `zzz config winddown N` 可改。旧配置 `winddown_minutes=15` 会在 App 启动时迁移为 5。 -1. 读取配置,检查今天是否启用 -2. 如果存在有效 `skip_tonight`,记录为请假并退出 -3. 保存当前亮度和音量 -4. 根据真实墙钟分阶段发送提醒、降低亮度和音量 -5. 到睡觉时间后暂停媒体、静音、降低亮度并启动 `zzz-overlay` -6. 如果覆盖层意外退出且仍在锁屏时段,2 秒后重新启动 -7. 起床时间后恢复亮度和音量,记录 `completed` +## 每晚流程 -`daemon.sh` 的等待逻辑按墙钟轮询,不依赖一次长 `sleep`,避免 Mac 合盖休眠后时间错位 +1. launchd 在 wind-down 起点触发 daemon(`RunAtLoad=true`,若已在 wind-down/锁屏窗内会 catch-up 立即跑)。 +2. 读配置、`effective_bedtime`(含 `postpone_tonight`);校验锁屏窗;处理 `skip_tonight`。 +3. **wind_down**:T-5 Toast → 亮度/音量渐变 → T-1 Toast(可推迟 5min 一次)→ 到 bedtime。 +4. **lockdown**:暂停媒体、静音、最低亮度、尝试 `shortcuts run "Turn On Focus"`、启动 overlay 循环(被杀则 2s 重启)。 +5. **wake_up**:恢复亮度音量、`Turn Off Focus`、记录 `completed`(无起床弹窗)。 -### 4. 正式锁屏覆盖层 +非启用日的 wind-down:仍可发 T-5 提醒,但 **跳过 lockdown**(`wind_down` 返回 2)。 -`src/overlay/LockScreen.swift` 是正式锁屏程序,编译产物为 `bin/zzz-overlay` +## App 命令行参数 -当前正式覆盖层做这些事: +```text +zzz-app --toast-bedtime-warning [--allow-postpone] +zzz-app --toast-locksoon # 等价 T-1min,不可推迟 +zzz-app --notify / --notify-l10n ... # 保留;daemon 睡前流程不调用 +``` -- 为每个显示器创建一个全屏 borderless window -- 使用 `CGShieldingWindowLevel + 1` 保持在大多数窗口之上 -- 每 2 秒重置窗口层级和大小 -- 监听显示器变化并重建窗口 -- 从 `~/.timetosleep/assets/cat-bedtime.mov` 读取透明猫猫视频 -- 渲染猫猫出现、关灯、睡下的正式动画效果 -- 关灯后在左上角显示时间、猫猫文案和醒来倒计时 -- 到起床窗口后自动退出 -- 支持异常逃生:短时间内连按两下 ESC 会重新读取配置;只有当前不在锁屏窗口或今天不是启用日时才退出 +无 App 时 daemon 用 AppleScript `display alert` 兜底(12s 自动关闭)。 -### 5. 预览程序 +## overlay 预览(onboarding) -`CatBedtimePreview.swift` 和 `CatVideoBedtimePreview.swift` 是开发预览。它们不会进入 CLI 安装包或 App 运行路径,也不会参与正式覆盖层 +`LockPreviewView` 启动: -- 手绘预览用于验证猫猫走动、拉灯、上床睡觉的纯代码动效 -- 视频预览用于验证透明视频 / 绿幕 / 黑底素材与真实桌面截图的合成 +```text +zzz-overlay --preview-exit-on-esc --preview-duration 18 +``` -这些文件是可继续实验的素材管线;正式锁屏入口已经在 `LockScreen.swift` 中使用视频猫猫效果 +预览可 ESC 退出或 18s 超时;不写 `stats.json` completed。 ## launchd -安装后会写入一个用户级 agent: +| 项 | 值 | +| --- | --- | +| Label | `com.timetosleep.daemon` | +| Plist | `~/Library/LaunchAgents/com.timetosleep.daemon.plist` | +| 触发 | 每天 `bedtime - winddown_minutes` | +| RunAtLoad | `true`(配合 `_should_kickstart_now` catch-up) | +| 日志 | `~/.timetosleep/daemon.log` | -| Label | 触发方式 | 职责 | -| --- | --- | --- | -| `com.timetosleep.daemon` | 每天 `bedtime - winddown` | 执行睡前提醒和锁屏流程 | - -Plist 文件位于 `~/Library/LaunchAgents/`。更新或卸载时会清理旧版本留下的 `com.timetosleep.bootcheck` 登录自检 agent - -## 模块依赖 - -```text -bin/zzz -├── lib/config.sh -├── lib/ui.sh -├── lib/stats.sh -└── lib/schedule.sh # 仅 config / init / uninstall / 时间变更时加载 - -src/cli/init.sh -├── lib/ui.sh -├── lib/config.sh -├── lib/stats.sh -└── lib/schedule.sh - -src/cli/daemon.sh -├── lib/config.sh -├── lib/stats.sh -├── src/cli/media.sh -├── src/cli/brightness.sh -└── bin/zzz-overlay - -src/app/CatBedtimeApp.swift -├── ~/.timetosleep/config.json -├── bundled Resources/src/cli/daemon.sh -├── bundled Resources/bin/zzz-overlay -└── bundled Resources/assets/ - -bin/zzz-overlay -├── ~/.timetosleep/config.json -└── ~/.timetosleep/stats.json -``` +卸载时会清理遗留的 `com.timetosleep.bootcheck`。 ## 测试 -当前测试是 Bash 脚本: - ```bash -tests/test-lockdown-window-and-syntax.sh -tests/test-overslept-detection.sh -tests/test-config-numeric-fallback.sh +for t in tests/test-*.sh; do bash "$t"; done ``` -它们覆盖: - -- `src/cli/daemon.sh` 语法 -- 23:00 到 07:00 这类跨午夜锁屏窗口 -- 合盖休眠后不应在白天误锁 -- `winddown_minutes` 缺失或非法时的默认值 - -## 改代码从哪里开始 - -- 改 CLI 文案或命令:`bin/zzz` -- 改领养流程:`src/cli/init.sh` -- 改提醒、锁屏、唤醒恢复:`src/cli/daemon.sh` -- 改 launchd plist:`lib/schedule.sh` -- 改 App 版:`src/app/CatBedtimeApp.swift` -- 改正式锁屏 UI:`src/overlay/LockScreen.swift` -- 重新编译正式覆盖层:`src/overlay/build.sh` -- 做猫猫动效实验:`src/overlay/CatBedtimePreview.swift` 或 `src/overlay/CatVideoBedtimePreview.swift` +| 脚本 | 覆盖 | +| --- | --- | +| `test-lockdown-window-and-syntax.sh` | daemon 语法、跨午夜 | +| `test-overslept-detection.sh` | 休眠后不误锁 | +| `test-locksoon-toast.sh` | T-5/T-1 Toast、无 NC 睡前提醒 | +| `test-lock-duration-limit.sh` | 锁屏窗 `< 15h` | +| `test-daemon-startup-phase.sh` | winddown / lockdown 阶段 | +| `test-schedule-catchup.sh` | RunAtLoad catch-up | +| `test-config-numeric-fallback.sh` | 无 jq 读数字配置 | +| `test-i18n-lang.sh` | 多语言 | + +## 改代码入口 + +| 目标 | 路径 | +| --- | --- | +| CLI | `bin/zzz` | +| 领养 | `src/cli/init.sh` | +| 提醒 / 锁屏 | `src/cli/daemon.sh` | +| launchd | `lib/schedule.sh` | +| 配置 / 锁窗 | `lib/config.sh` | +| App UI / Toast | `src/app/CatBedtimeApp.swift` | +| 锁屏 UI | `src/overlay/LockScreen.swift` | +| 文案 | `locales/messages.json` | diff --git a/CHANGELOG.md b/CHANGELOG.md index a750694..7c3fa2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ --- +## [v1.1.2](https://github.com/znygithub/cat-bedtime/releases/tag/v1.1.2) — 2026-06-14 + +### 修复 +- README 下载链接更新至 v1.1.2,避开缺少 `cat-bedtime.mov` 的 v1.1.0 DMG。 +- 将锁屏动画 `assets/cat-bedtime.mov` 纳入源码发布,并在 App / CLI 构建时校验缺失素材,避免再次发出缺动画安装包。 + +--- + +## [v1.1.1](https://github.com/znygithub/cat-bedtime/releases/tag/v1.1.1) — 2026-05-26 + +### 修复 +- App 退出后不再继续触发睡前提醒或锁屏流程。 +- 修复提前 1 分钟提醒缺失的问题。 +- 打开 App 或刚完成 onboarding 时,如果已经过了睡觉时间,先弹出紧凑确认弹窗,可选择「15 分钟后锁屏」或「明天再说」。 +- 早晨自然解除锁屏后不再保留 App 界面,改为在通知中心提示猫猫睡得很好,并显示来到家里的天数。 + +### 改进 +- 过点确认弹窗改为和睡前弹窗一致的紧凑样式。 +- 移除过点确认弹窗里的安全预览说明文案。 + +--- + ## [v1.1.0](https://github.com/znygithub/cat-bedtime/releases/tag/v1.1.0) — 2026-05-20 ### 新增 @@ -48,4 +70,4 @@ --- -[Unreleased]: https://github.com/znygithub/cat-bedtime/compare/v1.1.0...main +[Unreleased]: https://github.com/znygithub/cat-bedtime/compare/v1.1.2...main diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7c22df8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,18 @@ +# Claude Coding Rules + +This file is for Claude-style coding tools. + +文档入口见 [`.rule.md`](.rule.md)。**`ARCHITECTURE.md` + `docs/`** 是本项目的唯一产品与技术文档树。 + +首次接触项目请读: + +- [`docs/core-beliefs.md`](docs/core-beliefs.md) — 产品哲学与边界 +- [`ARCHITECTURE.md`](ARCHITECTURE.md) — 代码现状、全局流程、测试入口 + +按需阅读: + +- [`docs/product-specs/`](docs/product-specs/index.md) — 需求(做什么) +- [`docs/tech-docs/`](docs/tech-docs/index.md) — 技术方案(怎么做) +- [`docs/RELIABILITY.md`](docs/RELIABILITY.md) — 踩坑与回归风险 + +当代码变更时,同步更新相关 `docs/` 文档与 `ARCHITECTURE.md`;有歧义时以代码为准。 diff --git a/PITFALLS.md b/PITFALLS.md deleted file mode 100644 index 130f27c..0000000 --- a/PITFALLS.md +++ /dev/null @@ -1,199 +0,0 @@ -# 历史踩坑与回归风险 - -这些记录保留给后续维护者。它们不是待办事项,而是已经修过、以后容易被改坏的地方 - -## 1. macOS bash 3.2 + 中文退格删不动 - -macOS 自带 bash 3.2,内核行规程不支持 `iutf8`(Linux 专属)。`read -r` 走 canonical 模式,退格按字节删而非按字符删——中文 UTF-8 一个字三字节,删几下就卡死了 - -**解法:** `read -e -r` 启用 readline,readline 自己处理 UTF-8,不依赖内核 - -## 2. read -e 退格会吃掉 prompt - -用 `printf` 先打印带颜色的 prompt 再 `read -e`,readline 不知道 prompt 的存在,退格会把 `›` 都删掉 - -**解法:** 用 `read -e -r -p "$prompt"` 把 prompt 交给 readline 管理,ANSI 转义码用 `\001`..`\002` 包裹告诉 readline 这些是不可见字符: - -```bash -_rl() { printf '\001%b\002' "$1"; } -local prompt=" $(_rl '\033[38;5;245m')›$(_rl '\033[0m') " -read -e -r -p "$prompt" answer -``` - -## 3. set -e 下子命令返回非零直接退出 - -`_parse_time` 解析失败 `return 1`,外层 `parsed=$(_parse_time "$input")` 拿到非零退出码,`set -e` 直接杀掉整个脚本——`while true` 重试循环根本走不到 - -**解法:** `parsed=$(_parse_time "$input") || true` - -## 4. 锁屏靠字符串匹配起床时间,Mac 睡眠一觉就永远解不开 - -`LockScreen.swift` 里最早的 `checkWakeTime()` 用 `DateFormatter("HH:mm")` 把当前时间转成字符串,跟 `config.wakeupTime` 精确比对,匹配上才 `exit(0)` - -问题是 Timer 只在进程实际运行时才跑。Mac 深度睡眠时 Timer 全部挂起——如果 07:00 那整整一分钟 Mac 都在睡,醒来时钟已经过了 07:00,字符串再也不可能等于 "07:00",overlay 就**永远不退出**。daemon.sh 外层是 `wait $OVERLAY_PID`,连带整条唤醒流水线一起卡死 - -日志表现:当晚的 `Launching overlay` 之后,第二天早上**没有** `Wake time reached` 这行。用户一打开电脑就是黑屏锁死 - -**解法:** 判断"是否已经进入白天窗口"而不是精确匹配时分。使用统一的锁屏窗口逻辑(支持跨午夜),哪怕 Timer 错过 07:00 的整个窗口,醒来后第一次采样就能退出: - -```swift -let inAwakeWindow: Bool -if bedMin > wakeMin { - inAwakeWindow = nowMin >= wakeMin && nowMin < bedMin -} else { - inAwakeWindow = !(nowMin >= bedMin && nowMin < wakeMin) -} -if inAwakeWindow { exit(0) } -``` - -## 5. build.sh 只出本机架构,装到别的机器上跑不起来 - -原来 `src/overlay/build.sh` 直接 `swiftc -o` 一次,在 Apple Silicon 上就只出 arm64,而仓库承诺的是 universal 二进制(`ARCHITECTURE.md` 里"预编译通用二进制 arm64 + x86_64")。Intel Mac 拿到的话直接跑不起来 - -**解法:** 分别用 `-target arm64-apple-macos11` / `-target x86_64-apple-macos11` 编出两份,再 `lipo -create` 合成 fat binary - -## 6. install.sh 结束后用户不知道下一步 - -装完只打印"请运行 `zzz init`",用户(或 AI)跑完安装脚本后什么也没弹出来,不知道接下来该干嘛 - -**解法:** 安装末尾用 `osascript` 弹一个新 Terminal 窗口跑 `zzz init`,不管谁触发安装都能看到 onboarding: - -```bash -osascript -e " -tell application \"Terminal\" - activate - do script \"'$INSTALL_DIR/bin/zzz' init\" -end tell -" -``` - -## 7. 合盖休眠让 `sleep` 失真 + `wind_down` 误用弯引号 - -**现象①** 设定 23:00 睡、07:00 起,却在非锁机时段(如 21:00)被锁,或唤醒后锁在奇怪的时间。合盖后 `sleep N` 随进程挂起,唤醒后剩下的 `sleep` 仍按「冻结前」的剩余秒数跑完,墙钟与阶段时间错位 - -**解法①** `sleep_until` 改为短间隔轮询并每次用 `minutes_until` 看墙钟;`wind_down` 用阶段绝对时刻;若醒在「起床~风睡前」白天区则 `_overslept` 中止、不锁。见 `src/daemon.sh` 与 `tests/test-overslept-detection.sh` - -**现象②** `wind_down` 里部分字符串用了 Unicode 弯引号(U+201C/U+201D),bash 不认,`bash -n` 报错,该段逻辑无法执行 - -**解法②** 全部改为 ASCII `"`。`tests/test-lockdown-window-and-syntax.sh` 里用 `bash -n` + 23:00~07:00 锁窗(含 21:00 不应锁)做回归 - -## 8. 偶发锁死时无后门 - -**需求** 若再出现逻辑/时间异常导致误锁,需要一条不破坏「契约时段内绝对锁死」的逃生口 - -**解法** 全屏层 `LockScreen.swift`:在约 0.45s 内连按两下 ESC → 重新读取 `config.json` 与当前系统时间;仅当**不在**锁机窗或**今天不是契约日**时可 `exit(0)`,否则忽略。Timer 仍按原逻辑到点自动退出 - -**补充** 到点退出的 `checkWakeTime` 必须检测「**刚才还在锁机窗 → 本秒已出窗**」再 `exit`;若误写成「只要当前不在锁机窗就 exit」,白天手动 `open` 测锁屏会约 1s 内被关,还容易被误以为「点一下屏就没了」 - -**补充2** 双击 ESC 不能只靠 local monitor。`NSApplication.ActivationPolicy.prohibited` + 普通 borderless window 可能拿不到键盘焦点,ESC 根本进不来。窗口需可成为 key/main window,content view 需接受 first responder,并用 `.accessory` 后主动 `activate` - -## 9. launchd 首次安装时 `bootout` 失败会吞掉后续 `bootstrap` - -`bin/zzz` 使用 `set -e`。`lib/schedule.sh` 在安装 / 更新任务时会先执行 `launchctl bootout` 再 `launchctl bootstrap` - -首次安装时,`com.timetosleep.daemon` 本来还不存在,`launchctl bootout gui//