diff --git a/agents.md b/agents.md index 8fe1de0..34102c5 100644 --- a/agents.md +++ b/agents.md @@ -131,6 +131,7 @@ scripts/review.sh ID 轮次 路径 审查一条任务 scripts/apply-lo-patches.sh engines/lo-src 找出源码已到补丁序列第几个,补上其余(构建脚本调用) tests/scripts/lo-patches.sh 补丁序列从中途接着打、再打一次为空操作 tests/scripts/media-targets.sh 媒体播放接口在 Linux、Windows 目标可编译(缺目标时 SKIP) +tests/scripts/linux-idle.sh 真窗口 ElyTree:打开仓库后、Omarchy 换主题与改设置后,空闲时不读主题、设置,不跑 git status;照样跟随换主题、改设置与改文件,经内存映射的写入也跟随 tests/scripts/linux-backends.sh Linux 构建带 Wayland 与 X11;Hyprland 上 ElyTree 在两者上都映射出窗口、画出页面与起始页文字(截窗口量色,tesseract 读字) cargo test -p ely-show --test compose 切换半程帧经 Metal 取像素(主线程,仅 macOS) tests/scripts/*.sh *.py 脚本测试(缺工具时报 SKIP) @@ -294,3 +295,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-11:ElyMark 的 AppImage(0.1.3 的 Linux 发布查出)。`linuxdeploy` 把可执行文件的 RUNPATH 改成 `$ORIGIN/../lib`,并把它找得到的 `libcef.so` 拷进 `usr/lib`:原先 `libcef.so` 与 CEF 的资料在 `usr/bin`,结果是两份 1.45 GB 的内核、程序载入 `usr/lib` 那份、CEF 在它旁边找不到 ICU 数据而退出(从未在 AppImage 里启动过)。现在 `appimage.sh` 把从 `engines/cef` 分装来的全部条目移进 `usr/lib`(按名字对照 CEF 文件夹,清单仍只在 `stage.sh`),以 `LD_LIBRARY_PATH=AppDir/usr/lib` 运行 `linuxdeploy`,`libcef.so` 只此一份、依赖照样带上,CEF 按所载 `libcef.so` 的文件夹取资料(`ely-web-content` 的 `open`)。ElyMark 的 AppImage 由 715 MB 降到 423 MB;WSL 里启动后 CEF 载入、窗口打开(WSL 里各应用都不出首帧,未在真桌面上验证)。 - 2026-10-09(用户定):代码与发布从 GitHub 搬到自建的 Gitea(git.zacharyzhang.com;GitHub 账号被封,推送与发布都被拒)。公开仓库 `ZacharyZhang-NY/ElyOffice`(原样推上 main、缺陷清单分支与 v0.1.0、v0.1.1 标签),组件库为同站的 `ZacharyZhang-NY/Ely-GPUI-Components`(Cargo 的 git 依赖与 `fetch-references.sh` 随之改址,钉的提交不变)。v0.1.1 的六个 dmg 与两个源码包重新发在 Gitea 的 v0.1.1 发布里。官网的下载与源码链接指向 Gitea(`releases/latest` 会跳到最新发布页)。`.github/workflows` 删去(它们靠 GitHub 的 macOS 机器与 `gh`,发布改在本机以 `packaging/macos/release.sh` 构建、经 Gitea API 上传)。发布视频里写的地址已在源文件改为 Gitea,已发布的成片仍是旧地址,重渲染后才更新。上游项目(LibreOffice、MuPDF、git-lfs、tessdata 等)照旧从它们自己的 GitHub 地址取。 - 2026-10-11(用户报:Omarchy 上六个 AppImage 都打不开):`gpui-pre-platform` 对 `gpui-pre-linux` 关了默认特性,Wayland 与 X11 只经它自己的 `wayland`、`x11` 特性打开;工作区原只开 `font-kit`,Linux 上 gpui 退成无头平台,日志照写“window open”,合成器里从无窗口。工作区的 `gpui_platform` 加开两者;`tests/scripts/linux-backends.sh` 核对特性,在 Hyprland 上核对 Wayland 与 X11 两路都映射出窗口、截图里有均匀的深色页面,tesseract 读出起始页的 “Open a repository”(不交缓冲的窗口 Hyprland 不映射;不画字形时读不出)。 +- 2026-10-11:Linux 的 inotify 把打开文件报成事件(notify 的 `Access`),macOS 的 FSEvents 不报;四处文件监视原把任何事件当作改动,重读本身又触发下一次:Omarchy 主题与通用设置在第一次改动后、ElyTree 打开仓库起(`git status` 读仓库),空闲时每 250 毫秒重读。四处都经 `ely_ui::watching::only_read` 略去读取(打开、读、只读关闭),写后关闭照算改动(经内存映射的写入只报 CLOSE_WRITE,不报 MODIFY);`tests/scripts/linux-idle.sh` 以 inotifywait 从外面数空闲时的读取与 git status 次数。 diff --git a/apps/ely-mark/src/outside.rs b/apps/ely-mark/src/outside.rs index 2b67c94..06d6c17 100644 --- a/apps/ely-mark/src/outside.rs +++ b/apps/ely-mark/src/outside.rs @@ -376,6 +376,10 @@ impl Desk { fn watch(dirs: &[PathBuf], cx: &mut Context) -> notify::Result { let (sender, mut changes) = mpsc::unbounded(); let mut watcher = notify::recommended_watcher(move |event: notify::Result| { + // Reads, ours included, are no change. + if event.as_ref().is_ok_and(|event| ely_ui::watching::only_read(&event.kind)) { + return; + } let _ = sender.unbounded_send(event.map(|_| ()).map_err(|error| error.to_string())); })?; for dir in dirs { diff --git a/apps/ely-tree/src/watch.rs b/apps/ely-tree/src/watch.rs index 49b7515..3a08ee9 100644 --- a/apps/ely-tree/src/watch.rs +++ b/apps/ely-tree/src/watch.rs @@ -17,6 +17,8 @@ pub fn start(id: u64, repo: &Repo, cx: &mut Context) -> notify::Result| match event { + // Git's reads, ours included, are no change. + Ok(event) if ely_ui::watching::only_read(&event.kind) => {} Ok(_) => { let _ = sender.unbounded_send(()); } diff --git a/crates/ely-ui/src/lib.rs b/crates/ely-ui/src/lib.rs index e4cf0f6..89f0b5f 100644 --- a/crates/ely-ui/src/lib.rs +++ b/crates/ely-ui/src/lib.rs @@ -7,6 +7,7 @@ mod shell; pub mod tasks; mod theme; pub mod updates; +pub mod watching; pub use opening::{launch_files, on_open}; pub use run::run; diff --git a/crates/ely-ui/src/settings/mod.rs b/crates/ely-ui/src/settings/mod.rs index 419a11f..3cac732 100644 --- a/crates/ely-ui/src/settings/mod.rs +++ b/crates/ely-ui/src/settings/mod.rs @@ -82,6 +82,8 @@ fn follow(cx: &mut App) -> std::io::Result<()> { std::fs::create_dir_all(&dir)?; let (sender, mut changes) = mpsc::unbounded(); let mut watcher = notify::recommended_watcher(move |event: notify::Result| match event { + // Reads, ours included, are no change. + Ok(event) if crate::watching::only_read(&event.kind) => {} Ok(event) if event.paths.iter().any(|changed| changed.file_name() == path.file_name()) => { let _ = sender.unbounded_send(()); } diff --git a/crates/ely-ui/src/theme/omarchy/mod.rs b/crates/ely-ui/src/theme/omarchy/mod.rs index d8648e7..dc6aa15 100644 --- a/crates/ely-ui/src/theme/omarchy/mod.rs +++ b/crates/ely-ui/src/theme/omarchy/mod.rs @@ -29,9 +29,12 @@ pub fn load(current: &Path) -> Result> { Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), Err(error) => return Err(error).with_context(|| format!("cannot read {}", file.display())), }; - let name = fs::read_to_string(current.join("theme.name")) - .ok() - .map(|name| name.trim().to_string()); + let name_file = current.join("theme.name"); + let name = match fs::read_to_string(&name_file) { + Ok(name) => Some(name.trim().to_string()), + Err(error) if error.kind() == io::ErrorKind::NotFound => None, + Err(error) => return Err(error).with_context(|| format!("cannot read {}", name_file.display())), + }; parse(&text, name, theme.join("light.mode").exists()) .with_context(|| format!("{} is not a usable theme", file.display())) .map(Some) @@ -53,6 +56,8 @@ pub fn changes(current: &Path) -> Result<(RecommendedWatcher, UnboundedReceiver< let (sender, receiver) = mpsc::unbounded(); let mut watcher = notify::recommended_watcher(move |event: notify::Result| match event { + // Reads, ours included, are no change. + Ok(event) if crate::watching::only_read(&event.kind) => {} Ok(_) => { let _ = sender.unbounded_send(()); } diff --git a/crates/ely-ui/src/theme/omarchy/tests.rs b/crates/ely-ui/src/theme/omarchy/tests.rs index 73e51c4..9577c96 100644 --- a/crates/ely-ui/src/theme/omarchy/tests.rs +++ b/crates/ely-ui/src/theme/omarchy/tests.rs @@ -314,3 +314,29 @@ fn changes_sees_a_theme_swap_in_the_order_omarchy_does_it() { "no event for a swapped theme" ); } + +#[test] +fn reading_the_theme_is_no_change() { + let current = tempfile::tempdir().unwrap(); + let theme_dir = current.path().join("theme"); + fs::create_dir(&theme_dir).unwrap(); + fs::write(theme_dir.join("colors.toml"), LATTE).unwrap(); + fs::write(current.path().join("theme.name"), "catppuccin-latte\n").unwrap(); + let (_watcher, mut events) = changes(current.path()).unwrap(); + + load(current.path()).unwrap().expect("a theme"); + assert!(!event_within(&mut events, 10), "a read counted as a change"); +} + +#[test] +fn an_unreadable_theme_name_is_an_error_and_a_missing_one_is_none() { + let current = tempfile::tempdir().unwrap(); + let theme_dir = current.path().join("theme"); + fs::create_dir(&theme_dir).unwrap(); + fs::write(theme_dir.join("colors.toml"), LATTE).unwrap(); + assert_eq!(load(current.path()).unwrap().unwrap().name, None); + + fs::create_dir(current.path().join("theme.name")).unwrap(); + let error = format!("{:#}", load(current.path()).unwrap_err()); + assert!(error.contains("theme.name"), "{error}"); +} diff --git a/crates/ely-ui/src/watching.rs b/crates/ely-ui/src/watching.rs new file mode 100644 index 0000000..005a054 --- /dev/null +++ b/crates/ely-ui/src/watching.rs @@ -0,0 +1,29 @@ +use notify::EventKind; +use notify::event::{AccessKind, AccessMode}; + +/// A read; Linux reports those, macOS does not. +pub fn only_read(kind: &EventKind) -> bool { + matches!(kind, EventKind::Access(access) if *access != AccessKind::Close(AccessMode::Write)) +} + +#[cfg(test)] +mod tests { + use notify::EventKind; + use notify::event::{AccessKind, AccessMode, DataChange, ModifyKind}; + + use super::only_read; + + #[test] + fn opens_and_reads_are_reads_and_a_written_close_is_a_change() { + for read in [ + AccessKind::Open(AccessMode::Any), + AccessKind::Read, + AccessKind::Close(AccessMode::Read), + AccessKind::Any, + ] { + assert!(only_read(&EventKind::Access(read)), "{read:?}"); + } + assert!(!only_read(&EventKind::Access(AccessKind::Close(AccessMode::Write)))); + assert!(!only_read(&EventKind::Modify(ModifyKind::Data(DataChange::Any)))); + } +} diff --git a/tasks/TASKS.md b/tasks/TASKS.md index a25ffec..2e7aecb 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -201,9 +201,10 @@ ## L1 Linux 发布(用户 2026-10-11 报:Omarchy 上六个 AppImage 都打不开;要 deb、rpm、Arch、AppImage;在 ubuntu:26.04 容器里本机构建,替换 v0.1.3 的 Linux 包) -- [x] L1.1 Linux 显示后端:`gpui_platform` 开 `wayland` 与 `x11`(原样无头,“window open”而合成器里没有窗口);Omarchy(Hyprland)上 ElyTree 在 Wayland 与 X11 上画出窗口(其余五个要引擎,随 L1.5 实测);`tests/scripts/linux-backends.sh` — R1 锁文件、目标目录、未证明交帧;R2 产物路径、字形未核;R3 语言、OCR 错误被吞;R4 PASS -- [ ] L1.2 Omarchy 主题同步:查清空闲时每秒数次重套主题的根因并修;Omarchy 换主题时应用跟随 -- [ ] L1.3 Linux 构建环境:`ubuntu:26.04` 容器里构建全部引擎与六个应用(glibc 不高于 2.43) -- [ ] L1.4 Linux 包:deb、rpm、Arch(pkg.tar.zst)、AppImage 出自同一份分装,装到 `/opt/ely-office/<应用>`,带桌面项、图标与文件类型 -- [ ] L1.5 本机出六个应用的四种包;Omarchy 上装 Arch 包与 AppImage 出窗口;deb、rpm 在各自发行版的容器里装上并冒烟 -- [ ] L1.6 v0.1.3:删去旧 AppImage,上传新包;`ci.sh` 的 Linux 产出四种包;agents.md 与官网随之 +- [x] L1.1 Linux 显示后端:`gpui_platform` 开 `wayland` 与 `x11`(原样无头,“window open”而合成器里没有窗口);Omarchy(Hyprland)上 ElyTree 在 Wayland 与 X11 上画出窗口(其余五个要引擎,随 L1.6 实测);`tests/scripts/linux-backends.sh` — R1 锁文件、目标目录、未证明交帧;R2 产物路径、字形未核;R3 语言、OCR 错误被吞;R4 PASS +- [x] L1.2 Linux 上的文件监视把读取当作改动(inotify 报打开,FSEvents 不报):Omarchy 主题、通用设置在第一次改动后、ElyTree 打开仓库起,每次重读自己又触发下一次,空闲时每 250 毫秒读一遍;四处监视(主题、设置、ElyTree、ElyMark)经 `ely_ui::watching::only_read` 略去读取,写后关闭照算改动;`theme.name` 读不出不再吞掉;`tests/scripts/linux-idle.sh`(ElyMark 要 CEF,随 L1.6 实测) — R1 写后关闭也被略去、注释超长;R2 PASS +- [ ] L1.3 Linux 上设置窗口打开即崩(无菜单的窗口画菜单条,组件库断言“a menu bar needs menus”) +- [ ] L1.4 Linux 构建环境:`ubuntu:26.04` 容器里构建全部引擎与六个应用(glibc 不高于 2.43) +- [ ] L1.5 Linux 包:deb、rpm、Arch(pkg.tar.zst)、AppImage 出自同一份分装,装到 `/opt/ely-office/<应用>`,带桌面项、图标与文件类型 +- [ ] L1.6 本机出六个应用的四种包;Omarchy 上装 Arch 包与 AppImage 出窗口;deb、rpm 在各自发行版的容器里装上并冒烟 +- [ ] L1.7 v0.1.3:删去旧 AppImage,上传新包;`ci.sh` 的 Linux 产出四种包;agents.md 与官网随之 diff --git a/tasks/reviews/L1.2-r1.md b/tasks/reviews/L1.2-r1.md new file mode 100644 index 0000000..128fca2 --- /dev/null +++ b/tasks/reviews/L1.2-r1.md @@ -0,0 +1,5 @@ +FAIL + +- apps/ely-tree/src/watch.rs:21; apps/ely-mark/src/outside.rs:380; crates/ely-ui/src/settings/mod.rs:86; crates/ely-ui/src/theme/omarchy/mod.rs:60 — `is_access()` also discards `Access(Close(Write))`, so actual external writes can disappear. Reproduced on Linux by modifying `a.txt`, `theme.name`, and `settings.json` through writable shared memory mappings, flushing, then closing: inotify emitted OPEN and CLOSE_WRITE without MODIFY. The running ElyTree performed no new status read and observed neither the theme-name nor settings change; ordinary writes immediately triggered all three. ElyMark has the same filter, which also drops this content-change signal (MARK-08). Evidence: `.work/L1.2-r1/mmap-results.log` and `mmap-events.log`; the existing idle script passes despite this defect. Preserve writable-close notifications while filtering read events, and add regression coverage for mapped writes. + +- tests/scripts/linux-idle.sh:59 — The comment has 12 words, exceeding the ten-word limit in agents.md. Running `python tests/scripts/comments.py` fails on this line (`.work/L1.2-r1/comments.log`). Shorten the comment to ten words or fewer. diff --git a/tasks/reviews/L1.2-r2.md b/tasks/reviews/L1.2-r2.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/L1.2-r2.md @@ -0,0 +1 @@ +PASS diff --git a/tests/scripts/linux-idle.sh b/tests/scripts/linux-idle.sh new file mode 100755 index 0000000..ba32742 --- /dev/null +++ b/tests/scripts/linux-idle.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Idle on Linux: ElyTree rereads neither theme, settings nor repo. +set -euo pipefail + +root=$(cd "$(dirname "$0")/../.." && pwd) +fail() { + echo "FAIL: $*" >&2 + exit 1 +} +[ "$(uname)" = Linux ] || { + echo "SKIP: inotify is Linux" + exit 0 +} +[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] || { + echo "SKIP: no display" + exit 0 +} +for tool in inotifywait jq git; do + command -v "$tool" >/dev/null || { + echo "SKIP: no $tool" + exit 0 + } +done +app=$(cargo build -q --locked --message-format=json --manifest-path "$root/Cargo.toml" -p ely-tree --bin ely-tree | + jq -r 'select(.reason == "compiler-artifact" and .target.name == "ely-tree") | .executable // empty') +[ -x "$app" ] || fail "Cargo reported no ElyTree binary" + +work=$(mktemp -d) +pid= +spy= +trap '[ -z "$pid" ] || kill "$pid" 2>/dev/null; [ -z "$spy" ] || kill "$spy" 2>/dev/null; rm -rf "$work"' EXIT +current=$work/omarchy/current +mkdir -p "$current/theme" "$work/data/ElyOffice" "$work/bin" +cp "$root/crates/ely-ui/tests/fixtures/omarchy/catppuccin.toml" "$current/theme/colors.toml" +echo catppuccin >"$current/theme.name" +echo '{"version": 1}' >"$work/data/ElyOffice/settings.json" +repo=$work/repo +git init -q "$repo" +echo one >"$repo/a.txt" +git -C "$repo" add a.txt +git -C "$repo" -c user.name=t -c user.email=t@t commit -q -m one +real=$(command -v git) +printf '#!/bin/sh\necho "$*" >>"%s"\nexec "%s" "$@"\n' "$work/git.log" "$real" >"$work/bin/git" +chmod +x "$work/bin/git" + +ELY_GIT="$work/bin/git" ELY_OMARCHY_DIR="$current" ELY_SMOKE=hold ELY_LOG=info HOME="$work" \ + XDG_DATA_HOME="$work/data" "$app" "$repo" 2>"$work/app.err" & +pid=$! +statuses() { if [ -f "$work/git.log" ]; then grep -c '^status' "$work/git.log" || true; else echo 0; fi; } +for _ in $(seq 120); do + [ "$(statuses)" -gt 0 ] && break + kill -0 "$pid" 2>/dev/null || fail "ElyTree quit: $(tail -n 5 "$work/app.err")" + sleep 0.5 +done +[ "$(statuses)" -gt 0 ] || fail "ElyTree never read the repo" +grep -q 'omarchy catppuccin applied' "$work/app.err" || fail "the Omarchy theme was not applied" +sleep 3 + +# Four quiet seconds: no reads, no git status. +idle() { # when + inotifywait -q -m -e open --format '%w%f' "$current" "$current/theme" "$work/data/ElyOffice" >"$work/opens" 2>/dev/null & + spy=$! + sleep 1 + local before opens + before=$(statuses) + sleep 4 + kill "$spy" + spy= + opens=$(grep -c -E '(colors\.toml|theme\.name|settings\.json)$' "$work/opens" || true) + [ "$opens" -eq 0 ] || fail "ElyTree $1 read the theme or settings $opens times: $(sort "$work/opens" | uniq -c)" + [ "$(statuses)" -eq "$before" ] || fail "ElyTree $1 ran git status $(($(statuses) - before)) more times" + echo "linux: ElyTree $1 reads nothing" +} +seen() { grep -c "$1" "$work/app.err" || true; } +logged() { # pattern, what; once more than before + local before=${3:-0} + for _ in $(seq 40); do + [ "$(seen "$1")" -gt "$before" ] && return + sleep 0.25 + done + fail "ElyTree missed $2" +} +grown() { # what + for _ in $(seq 40); do + [ "$(statuses)" -gt "$before" ] && return + sleep 0.25 + done + fail "ElyTree missed $1" +} +# Writes through a mapping: CLOSE_WRITE, no MODIFY. +mapped() { # file, old, new + python3 -I -c 'import mmap, sys +with open(sys.argv[1], "r+b") as f, mmap.mmap(f.fileno(), 0) as m: + at = m.find(sys.argv[2].encode()) + assert at >= 0, sys.argv[2] + m[at:at + len(sys.argv[3])] = sys.argv[3].encode() + m.flush()' "$@" +} +idle "idle after opening" + +# Omarchy's swap, a settings write, an edit; then quiet again. +mkdir "$current/next-theme" +cp "$root/crates/ely-ui/tests/fixtures/omarchy/gruvbox.toml" "$current/next-theme/colors.toml" +rm -rf "$current/theme" +mv "$current/next-theme" "$current/theme" +echo gruvbox >"$current/theme.name" +logged 'omarchy gruvbox applied' "Omarchy's theme swap" +echo '{"version": 1, "author": "Tester"}' >"$work/data/ElyOffice/settings.json" +logged 'settings: changed on disk' "a settings write" +before=$(statuses) +echo two >>"$repo/a.txt" +grown "an edit in the repo" +sleep 2 +idle "idle after changes" + +mapped "$current/theme.name" gruvbox Gruvbox +logged 'omarchy Gruvbox applied' "a mapped theme name" +changed=$(seen 'settings: changed on disk') +mapped "$work/data/ElyOffice/settings.json" Tester Testor +logged 'settings: changed on disk' "a mapped settings write" "$changed" +before=$(statuses) +mapped "$repo/a.txt" two Two +grown "a mapped edit in the repo" +echo "linux: ElyTree follows mapped writes"