ElyTree: no Git version in the status bar; plain real paths on Windows

The component pin moves to bd01470 (long change-list names end before the
row's buttons). Real paths come from dunce, so Windows shows C:\... rather
than \?\C:\..., and 36 more ElyTree tests pass there. The lock returns to
windows-sys 0.61, which the last pin change had resolved down.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-11 00:51:22 -04:00
1 parent d3cfff86db
commit bfa2f1908c
14 files changed
+450 -471

No files matched your search

Generated
+13 -12
View File
@@ -246,7 +246,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -257,7 +257,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1782,7 +1782,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.3",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1966,7 +1966,7 @@ dependencies = [
[[package]]
name = "ely-gpui-component"
version = "0.1.1"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=3eb4d346fe88efe475daa88e0e042ca3680a386c#3eb4d346fe88efe475daa88e0e042ca3680a386c"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=bd014707fd47cd9d0651a9af8c1db6033718a6ea#bd014707fd47cd9d0651a9af8c1db6033718a6ea"
dependencies = [
"alacritty_terminal",
"anyhow",
@@ -2188,6 +2188,7 @@ dependencies = [
name = "ely-tree"
version = "0.1.3"
dependencies = [
"dunce",
"ely-files",
"ely-gpui-component",
"ely-platform",
@@ -2389,7 +2390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4498,7 +4499,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4663,7 +4664,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6258,7 +6259,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6851,7 +6852,7 @@ dependencies = [
"cfg-if",
"libc",
"psm",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -7197,7 +7198,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7532,7 +7533,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -8239,7 +8240,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
+2 -1
View File
@@ -18,7 +18,8 @@ encoding_rs = "0.8"
jiff = "0.2"
anyhow = "1"
directories = "6"
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "3eb4d346fe88efe475daa88e0e042ca3680a386c" }
dunce = "1"
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "bd014707fd47cd9d0651a9af8c1db6033718a6ea" }
env_logger = "0.11"
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] }
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] }
+6 -4
View File
@@ -40,7 +40,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木
## 组件映射
依赖 `ely-gpui-component`(pin 3eb4d34,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。
依赖 `ely-gpui-component`(pin bd01470,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。
| 需要 | 用 |
| --- | --- |
@@ -191,7 +191,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
## 决策
- 2026-10-01:上游版本以 `scripts/fetch-references.sh` 为准。LibreOffice 26.2.6(8221e31)、MuPDF 1.28.5(8ad45e9)、gpui 为 `gpui-pre` =0.3.8 与 `gpui-pre-platform` =0.3.8(crates.io,2026-10-07 起,随组件库 K01)、组件库 3eb4d34(2026-10-11 起)。
- 2026-10-01:上游版本以 `scripts/fetch-references.sh` 为准。LibreOffice 26.2.6(8221e31)、MuPDF 1.28.5(8ad45e9)、gpui 为 `gpui-pre` =0.3.8 与 `gpui-pre-platform` =0.3.8(crates.io,2026-10-07 起,随组件库 K01)、组件库 bd01470(2026-10-11 起)。
- 2026-10-02:组件库 7380afb 起:对话框与面板的 Enter 提交改走字段的 key-down 动作(macOS IME 天然守卫,不再提前提交);面板族文案经组件 `i18n::text`(内建 en/zh-CN,应用目录可覆盖);核心控件带 role/aria;对话框带角色、名称与说明,Cancel/Close 与禁用说明本地化。同仓库 `~/Documents/GitHub/Ely-GPUI-Components` 修复并推送(2026-10-09 起推到 git.zacharyzhang.com,HTTPS,令牌在本机钥匙串)。2026-10-04 组件库 b6cb94c:`NumberInput::on_commit` 只在 Enter、失焦、步进、拖动时触发,键入不触发;改文档的数值框用它,否则每个键入数字都成一条内核命令与一步撤销。同批带入上游的 Checkbox、Switch、Select 等 role/aria 与 RenderSurface。c0f17ee:`ToggleButton::disabled`(不可按、无 Tab 停靠、说明不可用,提示可写原因)。967d1e4:`ContextMenu::manual`(按属主编号的请求在某点打开,右键不开),供行在按下之后才到的菜单,如内核给的拼写建议。
- 2026-10-06:审查者为探针复制的源码树须用自己的 `CARGO_TARGET_DIR`(`scripts/review.sh` 写明);写完结论前删掉这些副本与其构建目录(曾积到 /tmp 近 900 GB,磁盘写满、链接失败):曾编进本仓库 `target/`,ely-lok 里烘焙的源码路径指向已删的副本,单测找不到 worker。
- 2026-10-03:每条任务由 `scripts/review.sh` 在 herdr 分栏里交给 codex(gpt-6.1-sol,effort high)审查,最多三轮。此前各轮用 gpt-6-astra xhigh。2026-10-10(用户定):`--yolo`、`service_tier="fast"`、effort xhigh,每条最多五轮;herdr 直接启动 codex 程序,不经 shell 别名。
@@ -228,7 +228,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-06(用户定):ElyMail 在八个应用里最后做:任务表中 C12 移到 C15 之后、C16 之前;CEF 内容宿主随 ElyMark 先建(C14.0),邮件编号依次前移。
- 2026-10-06(用户定):`ely-office` 的外壳不再只服务 LOK:`Host` 带关联类型 `Engine`(`ely_office::Engine`:后台载入、视图、撤销历史的标记、写出、关闭,视图的落定、编辑计数、待落地、焦点与事件),`Tab` 与 `Core` 按引擎取文档、视图与 worker 类型。LOK 的实现在 `ely_office::lok`(`Lok`、`start`、只对 LOK 的 `Commands` 即 `run_active`/`run_in`;查找替换、面板、图表仍只限 LOK);MuPDF 的在 `apps/ely-pdf/src/engine.rs`(`Pdf`;worker 首次载入才起,`Mupdf::get`)。`Kind::macros` 为 `None` 表示该格式无宏,不扫文件。ElyPDF 每个标签一个 `PdfView` 实体(页面、图块、文字、选择、找到的词都在里面,自己绘制),`Viewer` 只管标签条、书签栏、查找框与状态栏;打开、密码、关闭询问与保存走外壳。外壳的密码框:对话框开着时后到的加锁文件排队(`Core::locked`),对话框关闭(含取消、提交)后下一个再问;每次询问一个身份(按输入框实体),连续询问的 Esc 各归各的;提交时对话框先关、再载入。ElyDoc 的加锁测试原先清的是应用自己的对话框字段,改为清外壳的。
- 2026-10-07:本地 Web 内容宿主(C14.0,§17.3)。`crates/ely-web-content`:CEF 154.0.32(Chromium 154.0.8037.58,cef-rs a712ad9,`default-features = false`),`scripts/fetch-cef.sh` 取 minimal 包到 `engines/cef`,`.cargo/config.toml` 的 `CEF_PATH` 让构建用它。进程:应用的 `main` 先调 `ely_web_content::subprocess()`(参数里有 `--type=` 即为 CEF 子进程,载入框架后 `execute_process`,不碰 GPUI);开发构建子进程即应用本身的二进制;macOS 包里是五个辅助应用(`<二进制名> Helper`、`(GPU)`、`(Renderer)`、`(Plugin)`、`(Alerts)`,`Contents/Frameworks`,`LSUIElement`,可执行文件为 `ely-web-helper`),框架在 `Contents/Frameworks`,`packaging/macos/bundle.sh` 对依赖 ely-web-content 的应用自动放入并先签内层;查找次序 `ELY_CEF`、包内、辅助应用所在、构建时的 `engines/cef`。启动:`start` 在 GPUI 里只做准备(载入框架、给 NSApp 的类补上 Chromium 要的 `isHandlingSendEvent`、`setHandlingSendEvent:` 与包住 `sendEvent:`,并加 `CrAppProtocol`、`CrAppControlProtocol`),`initialize` 与建浏览器都在消息泵任务里、GPUI 的更新之外进行(实测:在更新里同步调 CEF,CEF 转动 AppKit 的循环,GPUI 报 RefCell 已借用、整窗不画);浏览器在泵里同步建(`browser_host_create_browser_sync`;原为异步,建好之前视图已释放时浏览器无人关闭),视图要么在建之前已走(不建),要么有浏览器可关。凡在 GPUI 里对 CEF 的调用(输入、焦点、尺寸、脚本、缩放、重新载入、关闭)都排进主线程的队列,由泵在下一轮、更新之外依次执行(实测:在实体释放时同步关浏览器,CEF 转动 AppKit 的循环去校验菜单,GPUI 的菜单回调遇已借用的应用状态而中止,约三成复现)。泵:外部消息泵,按 `on_schedule_message_pump_work` 的时间,另照 cefclient 每轮后以 1/30 秒兜底(实测:只按请求时第一轮后 CEF 再不请求,页面不载入);空闲时 CPU 约 1.5%。离屏渲染须用 Alloy 风格(`runtime_style`;Chrome 风格不出画面)。同一缓存目录只允许一个实例,第二个启动时报出占用它的进程号。画面:`on_paint` 的 BGRA 直接成 GPUI 图像,上一帧在下一次绘制时从图集释放(`drop_image`);光标随页面。输入:鼠标点击、移动、滚轮(行按 40 点),键照 cefclient 的 macOS 做法:键盘按下带字符与系统键码发 `KEYDOWN` 再发 `CHAR`(退格、回车、方向等用 AppKit 的字符),可打出字的键等 GPUI 的输入法回调后以所打的字发同样两个事件,多字或组字经 `ime_commit_text`/`ime_set_composition`(无替换范围时用 CEF 的“无范围”,两端为最大值;传空指针 CEF 不收);松开带同一字符(不带时 Chromium 当作又按一次,实测退格删两个字);拷贝、剪切、粘贴、全选走框架命令。焦点随 GPUI 的焦点给页面。页面与应用之间:CEF 的消息路由,页面调 `window.elyQuery({request, onSuccess, onFailure})`(`onSuccess` 与 `onFailure` 都必须有,缺一路由即不发;页面载入时路由未就绪,早到的查询丢失),应用收到 `Event::Query(请求, 回复)`;应用以 `run_js` 调页面。主题:每次载入与主题变化时把调色板、代码色、字体与字号比例、深浅写成 `:root` 的 `--ely-*` 变量与 `color-scheme`。缩放 `set_zoom(倍数)`。页面只许载入所在内容文件夹里的文件:按真实路径判定(`..`、百分号转义、指向外面的符号链接都算外面,文件须存在),导航与每个资源请求(脚本、图片、样式、请求等)都过这道门,资源另许 `data:` 与本页的 `blob:`,其余一律取消;另在每个 CEF 进程以 `--proxy-server=127.0.0.1:9` 与 `--proxy-bypass-list=<-loopback>` 把一切网络(含本机回环与 WebSocket,后者不经资源回调)送往不通的代理;WebRTC 走 UDP 不经代理,故另以 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 只许它经代理,并在每个页面上下文建立时、页面脚本之前删去 `RTCPeerConnection` 等构造(渲染进程的 `on_context_created`);页面的地址由真实路径逐字节转义生成(文件名含 `#`、`?` 照样载入);用户点出的链接以 `Event::Leave` 交给应用;`WebView::open_inert` 开的页面不运行脚本(`BrowserSettings.javascript` 关),ElyMark 的打印与导出 PDF 暂存页用它,文档里的 HTML 脚本不运行(M-15);页面的警告与错误只记行号(警告文字与来源都可由文档伪造,不进日志);页面自己的诊断经 `elyQuery` 交应用记录。载入失败(主框架,停止不算)、渲染进程退出、CEF 起不来都报 `Event::Failed`,页面区改显示组件库的错误视图与“重新载入”(Line truncated
- 2026-10-07:ElyTree 恢复与清理、外部工具、操作规则(C13.6,TREE-13、14,§13.3,QA-19、20)。仓库页第二行:在终端中打开(macOS `open -a Terminal`,其他平台报未构建)、在访达中显示(`reveal_path`)、清理未跟踪的文件…;页底“引用日志”列出 HEAD 最近 200 次移动(`reflog show -z`,位置、Git 写的原因、时间),选中即中间显示该提交(同提交详情,`Whose::Reflog`),上方“在此新建分支…”(找回删掉的分支)与“把当前分支移到此…”:有已暂存的改动即拒绝(`reset --keep` 会丢掉它们,请先提交或取消暂存);否则在后台算出离开该分支的提交,对话框写明分支、从哪到哪、离开的提交(引用日志仍可找回)与“工作区改动保留”,确认后 `reset --keep`(Git 在改动冲突时拒绝;没有任何按钮做硬重置),执行前在写队列里核对 HEAD 仍在预览时的分支(完整引用名,或仍游离)与提交、仍无暂存,任一变了即什么也不做。引用日志的时间是该次移动的时间(`--date=unix` 下的 `%gd`),不是提交时间;选中按事件本身(提交、原因、时间与它在全部引用日志中自最早起的序号,后者新移动不改,同一秒同原因的两次也分得开),同一提交的多条不会一起选中;之后有新的移动时该事件的位置后移,选中与详情跟着它,事件已不在引用日志里即不显示。清理:对话框先由 `git clean -n -d`(勾“包括被忽略的文件”另加 `-x`)得出 Git 会删除的路径(`core.quotePath=false`,其余 C 引号照 Git 规则还原),其中的文件夹当场展开为其中的文件(`ls-files --others`,同一忽略规则,另一仓库不入),对话框逐个列出文件与文件夹;确认后先逐段查每个所列路径:仍在仓库里、途中没有符号链接、文件仍是文件、文件夹仍是文件夹(任一变了即整体拒绝,什么也不删),再以 `git clean -f [-x] -- 所列文件` 删去(只给文件,Git 不递归;此后被跟踪了的文件 Git 留下,也不穿过链接),最后自深而浅删去所列文件夹中已空的(预览后在其中新出现的文件不动,文件夹随之留下),每次都问。文件标题栏多出“在编辑器中打开”(用户在“仓库›选择编辑器…”选的程序,存在数据目录 `tools.json`,字段类型不对与读不出同样报错且不覆盖;macOS 的 .app 经 `open -a`,在后台等它结束,失败即显示其原话;未选时先问)与 Markdown 的“在 ElyMark 中打开”(`ely_platform::apps::open`:按安装布局找同级的应用,macOS 的 `名称.app/Contents/MacOS`,Windows 的 `Ely Office\名称`,开发构建的同一目录;找不到即报错)。锁(§13.3):任何写操作因 Git 的锁文件失败(`….lock': File exists`,或配置文件的 `could not lock config file …: File exists`)时,对话框说明另一个 Git 正在工作、Ely 从不删除锁文件,“重试”把同一写操作重新排队。QA-19:测试都以独立的 Git 命令核对索引、提交、引用与工作区。QA-20:冲突、钩子失败、凭据被拒(本机 401 服务器,显示 Git 的 “terminal prompts disabled” 原话)、锁、变基中断后中止、取消(停止取回)都有窗口测试,结束时仓库状态以 Git 读回核对。切换到系统 Git 目前经 `ELY_GIT`(设置页随 C18.1);状态栏显示所用 Git 的路径与版本。真窗口(macOS):仓库页工具行、清理预览对话框、文件标题栏的编辑器与 ElyMark 按钮,按下后同级的 ely-mark 以该文件启动。未验证:Windows 与 Linux 的终端;ElyMark 打开文件的内容(C14)。
- 2026-10-07:ElyTree 恢复与清理、外部工具、操作规则(C13.6,TREE-13、14,§13.3,QA-19、20)。仓库页第二行:在终端中打开(macOS `open -a Terminal`,其他平台报未构建)、在访达中显示(`reveal_path`)、清理未跟踪的文件…;页底“引用日志”列出 HEAD 最近 200 次移动(`reflog show -z`,位置、Git 写的原因、时间),选中即中间显示该提交(同提交详情,`Whose::Reflog`),上方“在此新建分支…”(找回删掉的分支)与“把当前分支移到此…”:有已暂存的改动即拒绝(`reset --keep` 会丢掉它们,请先提交或取消暂存);否则在后台算出离开该分支的提交,对话框写明分支、从哪到哪、离开的提交(引用日志仍可找回)与“工作区改动保留”,确认后 `reset --keep`(Git 在改动冲突时拒绝;没有任何按钮做硬重置),执行前在写队列里核对 HEAD 仍在预览时的分支(完整引用名,或仍游离)与提交、仍无暂存,任一变了即什么也不做。引用日志的时间是该次移动的时间(`--date=unix` 下的 `%gd`),不是提交时间;选中按事件本身(提交、原因、时间与它在全部引用日志中自最早起的序号,后者新移动不改,同一秒同原因的两次也分得开),同一提交的多条不会一起选中;之后有新的移动时该事件的位置后移,选中与详情跟着它,事件已不在引用日志里即不显示。清理:对话框先由 `git clean -n -d`(勾“包括被忽略的文件”另加 `-x`)得出 Git 会删除的路径(`core.quotePath=false`,其余 C 引号照 Git 规则还原),其中的文件夹当场展开为其中的文件(`ls-files --others`,同一忽略规则,另一仓库不入),对话框逐个列出文件与文件夹;确认后先逐段查每个所列路径:仍在仓库里、途中没有符号链接、文件仍是文件、文件夹仍是文件夹(任一变了即整体拒绝,什么也不删),再以 `git clean -f [-x] -- 所列文件` 删去(只给文件,Git 不递归;此后被跟踪了的文件 Git 留下,也不穿过链接),最后自深而浅删去所列文件夹中已空的(预览后在其中新出现的文件不动,文件夹随之留下),每次都问。文件标题栏多出“在编辑器中打开”(用户在“仓库›选择编辑器…”选的程序,存在数据目录 `tools.json`,字段类型不对与读不出同样报错且不覆盖;macOS 的 .app 经 `open -a`,在后台等它结束,失败即显示其原话;未选时先问)与 Markdown 的“在 ElyMark 中打开”(`ely_platform::apps::open`:按安装布局找同级的应用,macOS 的 `名称.app/Contents/MacOS`,Windows 的 `Ely Office\名称`,开发构建的同一目录;找不到即报错)。锁(§13.3):任何写操作因 Git 的锁文件失败(`….lock': File exists`,或配置文件的 `could not lock config file …: File exists`)时,对话框说明另一个 Git 正在工作、Ely 从不删除锁文件,“重试”把同一写操作重新排队。QA-19:测试都以独立的 Git 命令核对索引、提交、引用与工作区。QA-20:冲突、钩子失败、凭据被拒(本机 401 服务器,显示 Git 的 “terminal prompts disabled” 原话)、锁、变基中断后中止、取消(停止取回)都有窗口测试,结束时仓库状态以 Git 读回核对。切换到系统 Git 目前经 `ELY_GIT`(设置页随 C18.1)。真窗口(macOS):仓库页工具行、清理预览对话框、文件标题栏的编辑器与 ElyMark 按钮,按下后同级的 ely-mark 以该文件启动。未验证:Windows 与 Linux 的终端;ElyMark 打开文件的内容(C14)。
- 2026-10-07:ElyTree 储藏、远程、工作树、子模块、LFS(C13.5,TREE-10 至 12,§13.3)。左栏第四页“仓库”:上方取回(`fetch --progress --all`)、拉取(`pull --progress`,照用户的 pull 设置)、推送、强制推送…;储藏(`stash list -z`,新的在前;“储藏改动…”问说明与是否包括未跟踪的文件,`stash push`),远程(名称与地址,`remote` 加 `config --get-regexp` 读出取回与推送地址),工作树(`worktree list --porcelain -z`,行名为文件夹名,有失效的可“清理”即 `worktree prune`),子模块(索引里的 gitlink 与文件夹自身 `.git` 的 HEAD:未检出、在记录的提交上、在别的提交上、冲突),LFS 文件(`git lfs ls-files --long --json`:工作树里是内容、对象在本机而工作树是指针、只有指针而对象不在本机);没有子模块或 LFS 文件时不列。各列表分别读出,一个读不出只在该处显示 Git 原话;切到此页时读,此后每次状态重读时重读,只取最新一次。选中一项,中间显示:储藏为提交详情的同一套(`hist::Picked`,历史与储藏共用读取与面板):改动的文件(对第一个父提交)与储藏的未跟踪文件(第三个父提交,各自对空树),选文件看差异,上方应用、弹出、删除…;远程为地址与推送地址,取回、修改…(改名 `remote rename`、改地址 `remote set-url`)、移除…(先问);工作树为路径与分支或提交、锁定与失效原因,打开(作为标签)、移除…(先问,主工作树与当前的不给);子模块为记录的与检出的提交,“检出记录的提交”(`submodule update --init`,经远程运行)与打开。用户输入的名称、地址、路径前一律加 `--`(推送的远程名同样)。表单与选择对话框(新建分支、标签、改名、身份、储藏、远程、工作树、选远程与上游)记下打开时的仓库,提交时写进那个仓库,不随当前标签变;储藏列表按每条三个字段读(说明可为空)。储藏的应用、弹出、删除按 `stash@{n}` 执行,执行前在写队列里核对它仍是所见的那个储藏(另有储藏插入即拒绝,什么也不做);弹出遇冲突时 Git 保留储藏,对话框显示 Git 原话。删除储藏、移除远程与工作树每次都问。分支页的本地分支多出“设置上游…”(列出远程分支,`branch --set-upstream-to=refs/remotes/名`)与“取消上游”。推送:有上游即 `push --progress`;没有上游时后台读出远程,问推到哪个远程(`push --set-upstream -- 远程 refs/heads/分支:refs/heads/分支`),没有远程或 HEAD 游离即说明。强制推送(§13.3):后台读出当前分支、上游的远程与远端分支、上次取回时远端的提交(租约)、此刻 HEAD 的提交(源)与远端有而源没有的提交,对话框写明远程、分支与将被覆盖的提交(或写明没有),确认后推送的就是所示的源提交(`push --force-with-lease=远端分支:租约提交 -- 远程 源提交:远端分支`),确认前本地分支再动也不改变推送内容;远端自上次取回后变了即被拒绝;没有上游或游离即说明不能。远程命令(取回、拉取、推送、子模块更新)是写队列里的一种(`Write::Remote`),经 `running::Running` 运行:Git 在自己的会话里(`setsid`,SSH 找不到终端,不会挂在终端提示上;凭据照用 credential helper 与 ssh-agent),进度逐行显示在中间上方的提示条,可停止(向整个进程组发 SIGTERM,Git 自行清理;已结束的不再发);用户停止时提示“已停止”、清掉其后的排队写,不当作失败;失败时对话框显示 Git 的 stdout 与 stderr(以回车结尾的进度行被下一行改写,只留最后一次;以换行结尾的行全部保留,钩子的每一行都在);拉取遇冲突照合并类命令判定为停下。关窗与退出时停下所有远程命令与克隆。克隆改用同一运行器。真窗口(macOS):仓库页五类列表、储藏详情与文件、工作树详情、分支行。未验证:Windows 与 Linux(停止远程命令在非 Unix 上报未构建);真实网络与 HTTPS、SSH 凭据;LFS 对象的下载(只显示状态)。
- 2026-10-07(用户定):跟随组件库最新的 main。组件库 K01 把 gpui 从 zed 仓库的提交换成 crates.io 的 `gpui-pre` =0.3.8(`gpui-pre-platform` 同版本),K02 让 Tab 绑在 FocusScope 的 ElyFocus 上下文下、更深的上下文先得;本项目工作区的 `gpui`、`gpui_platform` 随之改为这两个包(以 `package =` 保留原名,源码不变),组件库 pin 到 dabc1eb(其上加本项目一个提交:树与树行的无障碍角色)。升级后全工作区编译无改动。
- 2026-10-08:首版打包(R1.1)。图标(用户定:logo-design 技能出方案,选 C“实心几何”;macOS 标准 squircle,Linux 与 Windows 无底形,八个同一套设计):母版在 `assets/icons`(我按圆角矩形、圆与 30°/45° 斜线在 256 网格上画;每个颜色对白与 `#1E1E1E` 都不低于 3:1,ElyMark 原定的石板灰在深色上 1.75:1,改为琥珀 `#B87A00`;ElyTable 在 48 像素以下用简化的一版),`packaging/icons/build.sh` 以 AppKit 渲染出 icns(macOS)、512 像素 png(Linux 的 AppImage 与桌面文件)、16 至 256 像素 ico(Windows 安装程序、开始菜单快捷方式与卸载项);生成物入库,Linux 与 Windows 构建不需要 macOS。系统交给应用的文件:`ely_ui::run` 收下启动参数与 `on_open_urls`(访达的打开请求,启动前到达的也在内),应用先取 `launch_files`(有则不开空白文档;ElyMark 有则不恢复上次的标签),再以 `on_open` 处理运行中的请求;六个应用都走这一条(ElyDoc、ElyTable、ElyShow 原先不收启动参数)。macOS 包:LOK 应用只带内核,按 LibreOffice 自己的布局并入本应用:内核的库进 `Contents/Frameworks`(合并库构建留下的“invalid - merged lib”占位文件不拷,其余非代码文件即报错),内核的资料进 `Contents/Resources`,另有 `MacOS/ely-lok-worker`;`install_dir` 先找包内(`Frameworks/libmergedlo.dylib`)。不带 `LibreOffice.app` 本身:原先整份嵌入时,其快速查看与聚焦插件、`soffice` 等启动程序与文档类型被系统登记,系统设置里出现“LibreOffice 扩展”(用户发现;已注销本机七处登记);测试也不再运行 LibreOffice 应用(样本改为 `tests/fixtures/office` 的固定文件,运行开发构建的 `soffice` 会让系统重新登记它)。ElyPDF 带 `MacOS/ely-pdf-worker` 与 `Resources/tessdata`(三种语言),OCR 数据先找应用旁再找包内;ElyTree 带 `Resources/git`,其中每个 Mach-O 先签(Git 以 `INSTALL_SYMLINKS` 构建,内建命令是指向同一程序的相对链接;原先是硬链接,拷进包后变成约 150 份副本、689 MB;以 `MACOSX_DEPLOYMENT_TARGET=14.0` 构建,原先随本机 SDK 要求 macOS 27);ElyMark 用同级 ElyTree 包里的 Git。文档类型(`packaging/macos/documents.sh`)一律 `Alternate`,不抢系统默认:Doc 为 docx、odt、doc、rtf;Table 为 xlsx、ods、xls、csv、tsv;Show 为 pptx、odp、ppt;PDF;Markdown;Tree 为文件夹(查看者)。worker、内核、OCR 数据、CEF 与页面的所在路径都写日志,`tests/scripts/release-macos.sh` 据此核对都在包内。许可(§21.2、§21.3):每个包的 `Resources/Licenses` 有 Ely 的 AGPL 全文(仓库根 `LICENSE`)、`rust-crates.txt`(`packaging/crates.py`:本机目标下链进各二进制的全部 crate 与其许可文件,无许可文件的取 `packaging/licenses` 里该表达式的标准文本并写明作者,都没有即失败)与所带引擎的许可:LibreOffice 的 LICENSE、NOTICE、CREDITS;MuPDF 的 COPYING 与其编进去的每个第三方及每组内嵌字体(按构建实际嵌入的)的许可、OpenSSL、tessdata(Apache-2.0,按提交与散列取)、Great Vibes 的 OFL;Git 的 COPYING 与 LGPL-2.1、git-lfs 的 LICENSE.md(发布包里没有,按标签与散列取);CEF 的 LICENSE 与 Chromium 的 CREDITS、页面的第三方通知;缺任何一份即打包失败(`packaging/licenses.sh`)。`release.sh` 另出 `ElyOffice-<版本>-source.tar.gz`:自有源码、锁文件、上游补丁与构建脚本(上游各自钉在提交,取得方式即其中的 fetch 脚本)。正式签名(hardened runtime)时 CEF 的渲染与 GPU 辅助程序带 `allow-jit`,插件辅助程序带 `allow-unsigned-executable-memory` 与 `disable-library-validation`(`packaging/macos/entitlements`;原先不带,V8 保留不到代码区,页面载入失败);`tests/scripts/hardened-macos.sh` 以 `ELY_SIGN_IDENTITY` 签 ElyMark 并核对页面画出。ElyTable 同时收到多个文本文件(访达一次交来多份 CSV)时,读完的导入排队,没有对话框开着才逐个询问(原先后到的一份替换前一份,前一份静默丢失)。`packaging/macos/release.sh` 按 `apps.tsv` 的批次出各应用的 dmg;本机只有 Apple Development 证书,无 Developer ID,未公证。未验证:Intel 包(只建了 arm64)、公证、Linux 与 Windows 的包(本机不能建)。
@@ -260,7 +260,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-07:ElyTree 提交、历史、Blame(C13.3,TREE-05、06)。左栏分“改动”与“历史”两页(`navigation::Tabs`)。提交:改动页下方是 `git::CommitInput`(摘要与说明两个框,摘要满 50 变黄、72 变红),上面一行是 Git 设置里的身份(每次状态重读与窗口回到前台时重读,只取最新一次;`user.name <user.email>`,未设即写明,点它开身份对话框:姓名、邮箱(两个输入框以其标签为无障碍名),可勾“用于所有仓库”写 `--global`,否则写本仓库;空着即提示不写)与签名(`commit.gpgsign` 为真时写明以 `gpg.format` 签名,默认 openpgp),签名照用户自己的 Git 与工具,Ely 不另设。提交框按仓库各存一份(换标签时存下当前的、换上另一个的:摘要、说明、修改最近提交与其草稿),提交为 `commit --cleanup=whitespace -F -`(信息经标准输入,`#` 开头的行照留,用户的 `commit.cleanup` 不删它们,不经 shell),“修改最近一次提交”勾上即显示为勾选、读出上次的信息填入(读回前不能提交;先存下已键入的,取消勾选时放回;每次勾选或取消、换仓库都使在途的读取作废,读回时须仍是发起的仓库与那一次),提交成功后只在提交框仍是所提交的内容时清空(提交期间又键入的留着,别的仓库的提交框不动);钩子或签名失败时以对话框显示 Git 的全部原话,不只说失败。提交同其他写操作排队。历史:`log --topo-order --exclude=refs/stash --all`,每页 2000 个,滚到距末尾 20 行内读下一页;每次状态重读后比对 `show-ref --head`,引用变了(含外部提交、切分支、取回)即从头重读;读取期间来的比对记下,读完再比;引用读不出时历史栏显示 Git 的原话,之后读通即重读并清掉;提交图由 `git::lanes` 按已读的全部提交算出;时间按本机时区写到分钟(不用相对时间,长列表不必随时刷新)。搜索:在“提交信息”(`--grep`)、“作者”(`--author`)、“改动的文字”(`-S`)中按字面、不分大小写找,或按提交号(`rev-parse --verify`,前缀可);搜索时不画图。选中一个提交:中间上方是摘要、说明、作者与提交者(不同时才列)、完整提交号、父提交(可点,转到父提交)、签名状态(`%G?` 按字母译出,带签名者)、引用;下方左列是相对第一个父提交改动的文件(`diff --name-status -M` 与 `--numstat`,根提交对空树,空树由 `hash-object -t tree` 算,不写死 SHA-1),右边是所选文件的差异(与改动区同一读法:Git 自己的差异,图片、二进制、非 UTF-8 同样处理,无块的纯重命名显示为无改动)。改动区差异与历史页所选文件的标题栏都有“文件历史”与“逐行追溯”(改动区限已跟踪的文件):文件历史为 `log --follow --numstat -z`,跨重命名,每个提交带当时的路径与行数,选中即显示该提交里这个文件的差异(状态按两侧是否存在判定);逐行追溯为工作树文件的 `blame --porcelain`,未提交的行写“尚未提交”,每段首行可按,转到历史页的该提交;条色按提交时间新旧。读取都在后台,按发起时的仓库、提交与文件核对;文件历史与逐行追溯每次打开有窗口内唯一的序号,迟到的结果即弃。提交详情的作者、提交者、提交号、签名、引用、摘要与说明在 AX 树里有文字。真窗口(macOS)经 AX:历史页、提交图、引用、提交详情、文件列表与差异。未验证:Windows 与 Linux;GPG 与 SSH 实际签名(只读设置,签名由用户的 Git 完成)。
- 2026-10-07:ElyTree 差异与暂存(C13.2,TREE-03、04)。差异一律取 Git 自己的输出(`-c diff.suppressBlankEmpty=false diff --no-color --no-ext-diff --no-textconv -U3`,前缀写明 `a/`、`b/`,覆盖用户的 noprefix 等设置;空白的上下文行照样带前导空格):已暂存为 `--cached -M`(HEAD 对索引,即提交前的实际索引内容),改动为索引对工作树,未跟踪为 `--no-index` 对 `/dev/null`(退出码 1 即有差异);Git 的清理过滤(换行转换等)由它自己做,按块、按行暂存的补丁对得上索引。块之间未改动的行从旧侧的 blob 取(`cat-file blob`,已暂存为 `HEAD:旧路径`,改动为 `:路径`),可展开;行号按块头推算,旧文件比差异短即报错。改动的词按每对删去与加入的行标出(Unicode 词)。空白:精确、忽略空白变化(`-b`)、忽略全部空白(`-w`);行内与并排。按块或按行:只在精确比较、且是普通内容改动时(无新建、删除、重命名、复制、权限变化的头),已暂存区为取消暂存(`apply --cached --reverse`),改动区为暂存(`apply --cached`);未跟踪、重命名等整份暂存,界面写明。补丁由所选行切出:暂存时未选的删去行留作上下文、未选的加入行不写;取消暂存时反之;块头按“范围前的行数”换算,空范围照 Git 的写法;“无换行结尾”按切出后两侧的实际行尾:无换行的行在哪一侧后面还有行,那一侧就带上换行(所选的删去或加入行同样),两侧不同的上下文行拆成一删一加;`apply` 带 `--whitespace=nowarn`,用户的 `apply.whitespace` 不改所选的字节。写入前在后台重读同一条差异,与所选时读到的字节不同即拒绝(“文件在显示后改了,未暂存”),索引不动;同一仓库的写仍排队。所选行按住 Shift 取同一块内的范围;差异一变(任何重读得到不同的输出)所选即清空;每次读取有窗口内唯一的序号,换文件后旧文件迟到的读取即弃。图片(png、jpg、gif、webp、bmp、tiff、ico)以 `BeforeAfter` 左右拖动对比,一侧缺失时只显示另一侧;其他二进制文件,以及两侧任一侧不全是 UTF-8 的文件(差异以外的字节也算),显示两侧大小,整份暂存;文件与符号链接互换时 Git 为同一路径打出两段(删去与新增),两段都显示(不折叠:两段没有共同的旧文),整份暂存;工作树侧照 Git 读:符号链接读其自身的路径,不跟随;未跟踪的文件夹与冲突文件(冲突在 C13.4 处理)只说明。单击文件即在中间显示其差异(已忽略的文件以系统程序打开),标题栏另有“打开文件”。外部改动后状态重读,正显示的差异随之重读;文件不再列在原分区即收起。真窗口(macOS):改动区选两行暂存,索引恰为所选,已暂存区显示实际索引内容,并排视图;AX 树中各行、块按钮与分段可按。
- 2026-10-07(用户定):ElyTree 的 Git。随包自建钉住的 Git v2.56.0(a018953)加 git-lfs v3.8.0(各平台散列钉在 `scripts/fetch-git-lfs.sh`),`scripts/build-git.sh` 以 RUNTIME_PREFIX 构建到 `engines/git`,LFS 放在 `libexec/git-core`。查找次序:`ELY_GIT`(系统 Git,低于 2.40 即拒绝),否则应用旁或 `Resources/git`,否则本构建的 `engines/git`;没有即报错,不退回别的 Git。每次运行都是参数数组,带 `GIT_OPTIONAL_LOCKS=0`(读取不抢索引锁)、`GIT_TERMINAL_PROMPT=0`(从不在终端问凭据)、`GIT_LITERAL_PATHSPECS=1`(路径不当通配)。
- 2026-10-07:ElyTree 仓库与工作区(C13.1,TREE-01、02)。窗口经 `ely_ui::tool_root`:“文件”只有新建(选文件夹 `git init`)与打开(可多选文件夹),`commands::menus` 改收各应用自己的文件命令(文档应用照旧六项),保存、另存、导出、打印的动作在工具窗口里不挂;“仓库”菜单:克隆…、关闭仓库(Cmd+W)。每个仓库一个标签,同一工作树(按 `--show-toplevel` 的真实路径)只开一个,再开即切过去;裸仓库拒绝。最近列表在数据目录的 `recent.json`(新的在前,留 20 个),读不出即报错且不覆盖;最近项打不开即移出并说明。克隆:先问地址,再在最近仓库旁问新文件夹(名取地址末段去 `.git`),文件夹非空即拒绝;Git 的进度逐行显示在正中(回车与换行都断行,读到即显示),可停止(SIGTERM,Git 自己删去半成品;已结束的进程不再发信号,免误伤重用的进程号);关窗或退出时停下进行中的克隆。状态读 `status --porcelain=v2 -z --branch --untracked-files=normal --ignored`,未知字母即报错;行数取 `diff --numstat -z`(已暂存与未暂存各一次,二进制不计),未跟踪、冲突、忽略的不计行数。左栏 `git::ChangesList` 分冲突、已暂存、改动、未跟踪、已忽略;单击文件以系统程序打开;暂存(`add --all`)、取消暂存(有提交时 `restore --staged`,首个提交前 `rm --cached -f`:索引与文件不同也照取消,文件不动)、全部暂存(`add --update`)、全部取消(`reset -q`);取消暂存重命名连旧路径,暂存只用新路径(已暂存的重命名再改时旧路径已不在);放弃改动每次都问(已跟踪的 `restore --worktree`,未跟踪的 `clean -f -d` 并写明会删除)。同一仓库的写操作排队、一次一条,失败即清掉其后的排队并报出 Git 原话,关闭标签要等排队写完。外部改动:`notify` 监视工作树、Git 目录与共用目录(`--git-common-dir`;链接的 worktree 的分支与引用在主仓库里),250 毫秒内的一串改动合成一次重读,读取中再来的合成一次;空闲不读。状态栏:分支(游离显示短提交号,无提交写明)、上游与领先落后(上游引用已删即写明不存在,不显示为 0)、真实路径、Git 版本、随附或系统与所用 Git 的路径。中间区在 C13.2 前只显示改动数或“没有要提交的改动”。组件库 4f8f3bd 与 37c5187:`Changed::lines` 可缺。真窗口(macOS):五个分区、行数、状态栏。未验证:Windows 与 Linux(停止克隆在非 Unix 上报未构建);LFS 仓库的克隆。
- 2026-10-07:ElyTree 仓库与工作区(C13.1,TREE-01、02)。窗口经 `ely_ui::tool_root`:“文件”只有新建(选文件夹 `git init`)与打开(可多选文件夹),`commands::menus` 改收各应用自己的文件命令(文档应用照旧六项),保存、另存、导出、打印的动作在工具窗口里不挂;“仓库”菜单:克隆…、关闭仓库(Cmd+W)。每个仓库一个标签,同一工作树(按 `--show-toplevel` 的真实路径)只开一个,再开即切过去;裸仓库拒绝。最近列表在数据目录的 `recent.json`(新的在前,留 20 个),读不出即报错且不覆盖;最近项打不开即移出并说明。克隆:先问地址,再在最近仓库旁问新文件夹(名取地址末段去 `.git`),文件夹非空即拒绝;Git 的进度逐行显示在正中(回车与换行都断行,读到即显示),可停止(SIGTERM,Git 自己删去半成品;已结束的进程不再发信号,免误伤重用的进程号);关窗或退出时停下进行中的克隆。状态读 `status --porcelain=v2 -z --branch --untracked-files=normal --ignored`,未知字母即报错;行数取 `diff --numstat -z`(已暂存与未暂存各一次,二进制不计),未跟踪、冲突、忽略的不计行数。左栏 `git::ChangesList` 分冲突、已暂存、改动、未跟踪、已忽略;单击文件以系统程序打开;暂存(`add --all`)、取消暂存(有提交时 `restore --staged`,首个提交前 `rm --cached -f`:索引与文件不同也照取消,文件不动)、全部暂存(`add --update`)、全部取消(`reset -q`);取消暂存重命名连旧路径,暂存只用新路径(已暂存的重命名再改时旧路径已不在);放弃改动每次都问(已跟踪的 `restore --worktree`,未跟踪的 `clean -f -d` 并写明会删除)。同一仓库的写操作排队、一次一条,失败即清掉其后的排队并报出 Git 原话,关闭标签要等排队写完。外部改动:`notify` 监视工作树、Git 目录与共用目录(`--git-common-dir`;链接的 worktree 的分支与引用在主仓库里),250 毫秒内的一串改动合成一次重读,读取中再来的合成一次;空闲不读。状态栏:分支(游离显示短提交号,无提交写明)、上游与领先落后(上游引用已删即写明不存在,不显示为 0)、真实路径(所用 Git 的版本与路径只记日志,2026-10-11 用户定从状态栏去掉)。中间区在 C13.2 前只显示改动数或“没有要提交的改动”。组件库 4f8f3bd 与 37c5187:`Changed::lines` 可缺。真窗口(macOS):五个分区、行数、状态栏。未验证:Windows 与 Linux(停止克隆在非 Unix 上报未构建);LFS 仓库的克隆。
- 2026-10-07:ElyPDF 输出与文件恢复(C11.8,PDF-13、14,QA-13)。“文件›导出…”(Cmd+E)一个对话框三种:页面图片(PNG,72、150、300 dpi,全部或所选页,总是浅色,选文件夹,`名称-序号.png` 按页数补零,先以 `ely_files::claim_all` 占下全部目标,任一失败即删去本次全部);文字(UTF-8,各页以换页符分开,MuPDF 的 stext);优化的 PDF(可看可改的设置:压缩数据流、字体与图片;图片高于某 dpi 降到某 dpi,双三次重采样到确切目标,重压缩:无损的仍无损、有损的保持原格式,默认 300 降到 150;降到的 dpi 须低于阈值,否则不可导出;完成提示写明原文件与新文件大小)。导出图片与文字要“复制”许可,优化副本要“修改”许可。“打印…”(Cmd+P)把文档此刻写成副本(要“打印”许可,worker 的 `PRINT_COPY`)再交系统打印面板(`ely_office::print_later`)。文件恢复:MuPDF 打开时修复了文件结构(`pdf_was_repaired`),worker 即把修复后的文档整份写成临时副本、改开这份副本(日志与快照从此正常;原先修复过的文件不能保存),关闭文档时删去;打开时 MuPDF 的警告记下作修复报告(`ely_open` 期间接管警告回调,照样转给原回调)。外壳的 `Engine::signed` 改为 `Engine::risk`(宏由外壳判,签名、修复由引擎答),加 `Risk::Repaired`:提示条说明打开的是修复的副本、“另存为”写新文件、原件不动,带“报告…”按钮显示 MuPDF 的原话;保存一律另存,另存到原件被拒;签名或修复的文件另存成功后标签记下“待重开”,到内存与文件一致(已保存、无待落地编辑、不在导出)时才按新文件重开;另存期间又有编辑则留着编辑,下一次保存后再重开。MuPDF 有时读到坏对象才修复(尾部完好、某对象偏移坏):打开与解锁后先把每个对象读一遍,并解码图片以外的每个数据流(`ely_scan`,读不出的对象记入报告);MuPDF 修复了结构或读的过程中有任何警告(如内容流解压失败),都当作损坏文件,改开整份写出的副本、报告列出警告、保存一律另存。图片(用户定)在画出时检查:画图块或缩略图时 MuPDF 有警告即为损坏,报告补上警告,标签转为修复风险(保存一律另存);没画过的图片在原位保存(覆盖文件)之前统一解码检查(外壳的 `Engine::unsound`,有问题即转为另存为)。全部解码放在打开时的代价实测:100 张 2000×2000 的 JPEG(281 MB)打开到首块 2.6 秒(release,本机)。导出图片与文字期间 MuPDF 有警告即报错不写(读不全的内容不当作导出)。打印副本不带加密(已核过打印许可;否则系统打印面板打不开加了打开密码的副本),`print_later` 在面板关闭后删去它。导出对话框的降采样数值随键入生效(不等回车或失焦),直接点导出用的是所见的数。补丁 `0005` 见上(保存打不开输出时的保存中标志)。QA-13:worker 改字、加并填文本字段、移页后保存,以 PDFKit(另一阅读器)读回:页序、新字取代旧字、字段值都对(`tests/independent.rs`,只在 macOS);图片编辑在 PDFKit 里未单独核对(PDFKit 无图片清单接口)。组件库 a6b1bb0 的 `init` 改为返回错误(缺资源时),启动时出错即以 `startup::fail` 报出。真窗口(macOS):损坏文件的提示条与“修复的副本”状态、导出对话框。未验证:Windows 与 Linux 的打印(打印面板只在 macOS 实现);真窗口里实际打印。
- 2026-10-06:ElyPDF 密码与签名(C11.7,PDF-11、12,QA-15,§11.4)。权限:worker 记下每个文档的访问(MuPDF 的认证结果,4 为所有者;未加密即全权),`PERMISSIONS` 按 Ely 的位报出(打印、复制、修改、批注、填表、页面与书签、所有者),每个改动请求先按表核对(`secure::needs`:页面增删移转与书签要“页面与书签”或“修改”,内容、链接、属性、附件、OCR 层、删除与清理要“修改”,批注要“批注”,建改字段要修改加批注,填表要填表或批注,提取要复制,改保护要所有者),不许即以 `restricted:名` 报错,客户端转成 `PermissionDenied`,界面按名说明并提示输入所有者密码;复制文字在应用里按位拦(读文字本身不拦)。“文档”菜单“输入所有者密码…”:对时解除限制;错时 MuPDF 已丢了原密钥(RC4 与 AES-128 实测文字读不出),worker 以原密码重新认证。“密码与权限…”只在所有者权限下:写新文件(AES-256,`ely_protect` 经 `ely_save_copy` 带写出选项),打开密码可空,所有者密码必填且须与打开密码不同,六项许可勾选;或不保护(去掉加密)。只凭用户密码打开的文件改保护即拒绝。签名:MuPDF 以 `HAVE_LIBCRYPTO` 构建,worker 另链 `libmupdf-pkcs7.a` 与 OpenSSL 的静态 `libcrypto.a`(pkg-config 给目录)。`SIGNATURES` 按表单字段树列出全部签名字段(不画在页面上的也列,页与框可无;控件无 /P 时从各页 /Annots 找页):已签的给出摘要核对(`pdf_check_widget_digest`:未改、已改、读不出)、其后有无更新与更新是否越出签名所许(`pdf_signature_incremental_change_since_signing`、`pdf_validate_signature`)、签名者(MuPDF 的识别名与通用名)、签名者自称的时间、原因、地点,以及签名里的证书(签名者在前,DER);单个签名读不出只报该项的原因。证书信任由应用交系统判定(`ely_platform::trust`:macOS 用 `SecTrust`、`SecPolicy::create_x509`、禁网,故从不核验吊销;Windows 与 Linux 未构建,报“此平台未构建”)。界面“文档›签名…”对话框逐个列出完整性、证书、吊销(一律“未核验吊销状态”)、时间(带时区)、原因、地点与所在页,不给笼统的“有效”。新签:“以证书签名…”工具,拖出框或点空的签名字段,对话框选 .p12/.pfx、填证书密码、原因、地点,另存:快照副本上建字段(名 `SignatureN` 避开表单已有名)并签,以增量更新写出(旧签名覆盖的字节不动),写完重开核对新签名的摘要,不过即报错删文件;原文档不动;发起时绑定视图版本与所选位置。补丁 `patches/mupdf/0004-signature-reason-and-location.patch`:原因与地点写进签名字典(MuPDF 原只画进外观;签后再改会落到第二个更新段,字节范围算成负数)。外观只画标签、名字与时间(不画空的识别名各栏,不画 MuPDF 标志)。已签文件(`ely_signed` 数表单里有值的签名字段)打开为 `Risk::Signed`:上方提示条说明修改会使签名失效、“另存为”写出未签名副本,带“签名…”按钮;保存不覆盖原文件。外壳的 `risk` 由布尔改为 `Option<Risk>`(宏或签名),`Engine::signed` 由引擎回答。Ely 一切整份写出(保存、另存、提取、删除、清理、保护)在 `ely_save_copy` 里去掉签名值与其外观(字段留下、空着,去掉 AppendOnly 标志):整份重写本就使签名失效,不留假签名。签名核对的是文件,不是编辑中的文档:`SIGNATURES` 带路径,worker 以文档的密码另开该文件读出再关闭;签名列表按标签当前的文件路径读、按路径缓存,保存后重读。已签文件另存为成功后,标签按新文件重新打开(外壳的 `reload`,同冲突时;撤销历史随之重新开始),打开着的文档、签名列表与以后的签名都对应这份未签名的副本;另存失败时标签与签名都仍是原文件。签名对话框每次打开都重读文件,磁盘上被改动即显示已改。去掉签名值时一并去掉该签名字段自身的只读位(签名时由它的锁设下),以便再签;它锁住的其他字段的只读位留着(分不出原本是否只读,已知限制)。补丁 `patches/mupdf/0005-failed-save-clears-save-in-progress.patch`:MuPDF 的 `prepare_for_save` 置起保存中标志、只有写出过程清除它;打不开输出文件时标志留着,此后一切改动绕过日志,编辑没有撤销步(实测:存到不存在的目录后旋转,历史标记仍为 0);补丁让打开输出或预排签名失败时清除它。可视签名(用户定三种来源):“添加可视签名…”对话框,手写(组件库 `SiLine truncated
- 2026-10-06:ElyPDF 内容删除与文档清理(C11.6,PDF-09、10,§11.4,QA-14)。标记只在视图里(“标记文字以删除”按所选文字每行一框,“标记区域以删除”拖框,单击已有标记取消;预览为页面上的深色框;任何编辑落地即清空,页码可能已变),文档本身不动。“删除标记的内容…”与“清理文档…”都写新文件:先把文档快照写出、重开副本(`ely_save_copy`,提取与保存同用),在副本上改,再以 `garbage=2` 整份写出(不增量,旧修订不留),写到工作目录、核对通过后经 `ely_files::export` 交到所选位置,工作副本普通删除(对话框写明:原件、旧备份、同步历史与已发出的副本仍可能含有被删内容;不是磁盘级擦除)。删除:先删去碰到区域的批注与字段(MuPDF 的删除只管链接与文本框批注,其他批注的内容、字段的值会留下),字段失去最后一个控件即连同空了的上级一起从表单去掉(否则值留在字段树里);全部区域的删除做完后才建 Redact 批注(否则后一区域会删掉前一区域的标记)并 `pdf_redact_page`(黑框;图片在区域内的像素由 MuPDF 改成白色;碰到区域的线条整条删去,只删被盖满的会留下可复原的路径;碰到的文字删去,含隐藏文字)。有区域的页再过一遍内容(含表单):标记内容的 /ActualText、/Alt、/E 一律去掉,结构树里属于该页的元素(自身或继承的 `Pg`,或子项 MCR、OBJR 的 `Pg` 指向该页,连同其上级)去掉这三项与 /T;整页去掉而不只区域内的,宁多勿漏。页面缩略图 /Thumb 删去(它可引用原图),核对时区域所在页仍有缩略图即失败。写出后重开核对:区域内的字(与删除同一判法:字框各边缩一成后相交)为零;区域内画出来不是黑色的像素为零;逐页以设备重放,落进区域的路径、渐变、图像蒙版除我们的黑框外为零,图片单独画出在区域内只剩白或黑;文件只有一个版本;任一不过即报错并删去输出。确认时绑定标记与文档版本,选完路径后标记或版本变了即不写。清理:先扫出数量(属性与元数据条数、批注、附件含批注里的文件、有隐藏文字的页、脚本入口),勾选后写出:删 Info;其余按一遍遍历全部对象及其中的直接字典与数组:/Metadata 与 /PieceInfo,文件说明的 /EF 与 /RF 及关联文件 /AF,脚本;删批注(链接与字段不算批注,保留),删附件(名称树与文件批注;附件按带 /EF 的文件说明计数,每个一次),隐藏内容为:内容过滤(包住 MuPDF 的 sanitize 过滤器,用途设为 View 才认关着的图层)删去关着的图层里的一切、3 与 7 渲染模式及零透明度的文字,脚本(名称树 JavaScript;任何对象的 /A、/OpenAction、/Next 与 /AA 各项里 S 为 JavaScript 或带 /JS 的动作,从链中摘除,其后的 /Next 链接回原处,非脚本动作保留),带隐藏或不可见标志的批注(含链接与字段控件:链接不在 MuPDF 的批注表里,按页面 /Annots 删;控件删去时字段连同值从表单去掉);有隐藏文字被删的页,标记内容的替代文字照删除的做法去掉。扫描与写出后的核对都在快照副本上跑同一遍清理来计数,所以计数与执行一致;所选类别核对不为零即报错。扫描回来时须仍是当前标签且版本未变,对话框写明文件名。未验证:Windows 与 Linux。
@@ -288,5 +288,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-11(用户报:Windows 启动带黑色命令行窗口、“打开方式”里无图标、点 X 不退出、表格全黑无网格线):四处根因。命令行窗口:应用是控制台子系统;发行构建改为 `windows_subsystem = "windows"`(开发构建照旧有控制台看日志),子进程(两个 worker、Git、`cmd start`)一律经 `ely_ipc::windowless` 以 `CREATE_NO_WINDOW` 启动。图标:exe 里只有 gpui 的清单资源;各应用共用构建脚本 `packaging/windows/icon.rs`,把 `assets/icons/<应用>.ico` 编成资源 1(gpui 的窗口图标也取资源 1)。点 X 不退出:窗口其实弹了“保存更改”询问。`ely.xcd` 深色方案的 `CalcNotesBackground` 与浅色不同,Calc 在配色变化时改写批注样式并记一步撤销(`ScDetectiveFunc::UpdateAllComments`),未动的表格切到深色即算已修改;两套方案的批注底色改为同一值(LibreOffice 默认的浅黄),切换主题不再改文档,三端同此(`sheet.rs` 的 `a_theme_switch_is_no_edit`)。全黑:Win32 的虚拟设备没有 `SetSizeUsingBuffer`(基类默认返回假,断言在发行构建里不在),给客户缓冲区的设备停在 1×1 像素,Calc 图块只画了一个像素,放映图层同空;补丁 `0044-lok-win32-paints-then-copies.patch` 让 Windows 走 macOS 的路:按全尺寸画进设备,再拷进客户缓冲区(`doc_paintTile` 与 `SlideshowLayerRenderer`)。Writer 自设设备尺寸,原本不受影响。`tests/scripts/lo-patches.sh` 的临时库也设 `core.autocrlf false`(原先在 Windows 上恒失败)。
- 2026-10-11(用户报:Windows 上标题栏的最小化、最大化、关闭三个按钮都按不动):gpui 回答系统的命中测试(`WM_NCHITTEST`)时按绘制次序取第一个含指针的窗口控制区,标题栏的拖动区先于其中的按钮画出,按钮上一律答“标题栏”,系统把按下当作拖动窗口的开始,点击到不了按钮。修复在 `vendor/gpui-pre`(原样一个提交,其后是修复):按命中测试的次序取最上层的控制区,且只看未被遮挡的部分(同 `is_hovered`,盖在标题栏上的对话框挡住其下的按钮)。实测(发行构建,DPI 感知的探针移动真实指针):自右向左依次答关闭、最大化、最小化、拖动;按下最小化即最小化,最大化即最大化,关闭即退出、worker 随之结束;v0.1.3 的安装版同一探针处处答拖动。该回调只有 Windows 平台调用。
- 2026-10-11:Linux 标题栏按钮(核查 Windows 按钮问题时查出)。macOS 的红绿灯由系统画,不受影响;Linux 为客户端装饰,按钮由组件库画在标题栏的拖动区里,拖动区对任何按下都准备移动窗口,按住按钮时指针动一像素即开始移动窗口,合成器接管指针,松开到不了按钮,点击落空。组件库 3eb4d34:`drag_region` 只在按下未被里面的按钮认领(按钮以 `prevent_default` 认领)时才准备移动;`shell/title_tests.rs` 核对晃动的按下点得动 Linux 的关闭、标题栏空处照样移动窗口(去掉修复时前者失败)。未在真 Linux 桌面上点过。
- 2026-10-11(用户报:ElyTree 改动列表里文件名长时盖住行尾的打开按钮):组件库 bd01470:`ChangesList` 行里的文件名原为不缩的盒子,长名越过行宽压在按钮下;现可缩、以省略号结尾,先于按钮让位;`git/tests/narrow.rs` 核对 260 宽时文件名止于按钮之前(原布局时失败)。
- 2026-10-11(用户定:ElyTree 状态栏去掉 Git 的版本与路径那一项):所用 Git 的版本与路径只记日志。同时查出 Windows 上 `std::fs::canonicalize` 给出 `\?\C:\…` 形式,仓库根、Git 目录、工作树与文件的真实路径一律改用 `dunce::canonicalize`(能写成普通路径即普通路径,同一文件仍是同一真实路径),状态栏的仓库路径随之为普通形式;ElyTree 的单测在 Windows 上由 44 个失败降到 8 个,余下 8 个改前同样失败(如写入 Windows 不许的文件名),未处理。
- 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 地址取。
+1
View File
@@ -11,6 +11,7 @@ build = "../../packaging/windows/icon.rs"
embed-resource.workspace = true
[dependencies]
dunce.workspace = true
ely-files.workspace = true
ely-gpui-component.workspace = true
ely-platform.workspace = true
+148 -155
View File
@@ -1,155 +1,148 @@
//! Git as a program: argument arrays, never a shell (§13.1).
use std::{
io::{self, Write as _},
path::{Path, PathBuf},
process::{Command, Output, Stdio},
};
/// A Git binary and its version.
#[derive(Clone, Debug)]
pub struct Git {
pub path: PathBuf,
pub version: String,
/// Ely's own build, not a system Git.
pub bundled: bool,
}
impl Git {
/// `ELY_GIT` or the setting picks Git; else Ely's own.
pub fn find() -> io::Result<Self> {
let (path, bundled) = ely_platform::git::locate()?;
if !path.is_file() {
let hint = if bundled {
"run scripts/build-git.sh"
} else {
"check ELY_GIT or the Git setting"
};
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no Git at {} ({hint})", path.display()),
));
}
let version = match bundled {
true => bundled_version(&path)?,
false => ely_platform::git::system_version(&path)?,
};
log::info!(
"tree: {version} at {} ({})",
path.display(),
if bundled { "bundled" } else { "system" }
);
Ok(Git {
path,
version,
bundled,
})
}
/// Runs Git in `dir`; failure carries Git's words.
pub fn run(&self, dir: &Path, args: &[&str]) -> io::Result<Vec<u8>> {
let output = self.command(dir, args).output()?;
checked(args, output)
}
/// Runs Git with `input` on its standard input.
pub fn run_input(&self, dir: &Path, args: &[&str], input: &[u8]) -> io::Result<Vec<u8>> {
let mut child = self
.command(dir, args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| io::Error::other("no stdin"))?;
let input = input.to_vec();
// Writing apart from reading cannot fill both pipes.
let writer = std::thread::spawn(move || stdin.write_all(&input));
let output = child.wait_with_output()?;
writer
.join()
.map_err(|_| io::Error::other("the stdin writer panicked"))??;
checked(args, output)
}
/// HEAD names a commit; a new repository has none yet.
pub fn born(&self, dir: &Path) -> io::Result<bool> {
if !self.run_or_one(dir, &["rev-parse", "--verify", "-q", "HEAD"])?.is_empty() {
return Ok(true);
}
// Unborn: HEAD names a branch with no ref at all.
let branch = String::from_utf8(self.run(dir, &["symbolic-ref", "-q", "HEAD"])?)
.map_err(|_| io::Error::other("HEAD names a ref that is not UTF-8"))?;
// Only Git's line end goes; a name keeps every byte.
let branch = branch.strip_suffix('\n').unwrap_or(&branch).to_owned();
let found = self.command(dir, &["for-each-ref", "--format=%(refname)", &branch]).output()?;
let unborn = branch.starts_with("refs/heads/") && found.status.success();
if unborn && found.stdout.is_empty() && found.stderr.is_empty() {
return Ok(false);
}
let mut why = format!("HEAD names {branch}: no commit, and no branch yet to begin");
let said = String::from_utf8_lossy(&found.stderr);
if !said.trim().is_empty() {
why = format!("{why}; Git says {}", said.trim());
}
Err(io::Error::other(why))
}
/// Exit 1 is an answer: files differ, key unset.
pub fn run_or_one(&self, dir: &Path, args: &[&str]) -> io::Result<Vec<u8>> {
let output = self.command(dir, args).output()?;
match output.status.code() {
Some(1) => Ok(output.stdout),
_ => checked(args, output),
}
}
/// `merge-file` exits with its conflict count; past 127 fails.
pub fn run_counted(&self, dir: &Path, args: &[&str]) -> io::Result<Vec<u8>> {
let output = self.command(dir, args).output()?;
match output.status.code() {
Some(0..=127) => Ok(output.stdout),
_ => checked(args, output),
}
}
pub(crate) fn command(&self, dir: &Path, args: &[&str]) -> Command {
ely_platform::git::command(&self.path, dir, args)
}
}
fn checked(args: &[&str], output: Output) -> io::Result<Vec<u8>> {
// The verb follows any `-c name=value` pairs.
let verb = args
.iter()
.copied()
.find(|arg| !arg.starts_with('-') && !arg.contains('='))
.unwrap_or("");
match output.status.success() {
true => {
log::debug!("tree: git {verb} done");
Ok(output.stdout)
}
false => {
// Conflicts go to stdout, hooks to stderr.
let said: Vec<String> = [&output.stdout, &output.stderr]
.into_iter()
.map(|bytes| String::from_utf8_lossy(bytes).trim().to_string())
.filter(|said| !said.is_empty())
.collect();
log::warn!("tree: git {verb} failed, {}", output.status);
Err(io::Error::other(format!(
"git {verb} ({}):\n{}",
output.status,
said.join("\n")
)))
}
}
}
/// Ely's own Git, whatever version it was pinned at.
fn bundled_version(path: &Path) -> io::Result<String> {
ely_platform::git::version_of(path).map(|(version, _)| version)
}
//! Git as a program: argument arrays, never a shell (§13.1).
use std::{
io::{self, Write as _},
path::{Path, PathBuf},
process::{Command, Output, Stdio},
};
/// A Git binary whose version was checked.
#[derive(Clone, Debug)]
pub struct Git {
pub path: PathBuf,
}
impl Git {
/// `ELY_GIT` or the setting picks Git; else Ely's own.
pub fn find() -> io::Result<Self> {
let (path, bundled) = ely_platform::git::locate()?;
if !path.is_file() {
let hint = if bundled {
"run scripts/build-git.sh"
} else {
"check ELY_GIT or the Git setting"
};
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no Git at {} ({hint})", path.display()),
));
}
let version = match bundled {
true => bundled_version(&path)?,
false => ely_platform::git::system_version(&path)?,
};
log::info!(
"tree: {version} at {} ({})",
path.display(),
if bundled { "bundled" } else { "system" }
);
Ok(Git { path })
}
/// Runs Git in `dir`; failure carries Git's words.
pub fn run(&self, dir: &Path, args: &[&str]) -> io::Result<Vec<u8>> {
let output = self.command(dir, args).output()?;
checked(args, output)
}
/// Runs Git with `input` on its standard input.
pub fn run_input(&self, dir: &Path, args: &[&str], input: &[u8]) -> io::Result<Vec<u8>> {
let mut child = self
.command(dir, args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| io::Error::other("no stdin"))?;
let input = input.to_vec();
// Writing apart from reading cannot fill both pipes.
let writer = std::thread::spawn(move || stdin.write_all(&input));
let output = child.wait_with_output()?;
writer
.join()
.map_err(|_| io::Error::other("the stdin writer panicked"))??;
checked(args, output)
}
/// HEAD names a commit; a new repository has none yet.
pub fn born(&self, dir: &Path) -> io::Result<bool> {
if !self.run_or_one(dir, &["rev-parse", "--verify", "-q", "HEAD"])?.is_empty() {
return Ok(true);
}
// Unborn: HEAD names a branch with no ref at all.
let branch = String::from_utf8(self.run(dir, &["symbolic-ref", "-q", "HEAD"])?)
.map_err(|_| io::Error::other("HEAD names a ref that is not UTF-8"))?;
// Only Git's line end goes; a name keeps every byte.
let branch = branch.strip_suffix('\n').unwrap_or(&branch).to_owned();
let found = self.command(dir, &["for-each-ref", "--format=%(refname)", &branch]).output()?;
let unborn = branch.starts_with("refs/heads/") && found.status.success();
if unborn && found.stdout.is_empty() && found.stderr.is_empty() {
return Ok(false);
}
let mut why = format!("HEAD names {branch}: no commit, and no branch yet to begin");
let said = String::from_utf8_lossy(&found.stderr);
if !said.trim().is_empty() {
why = format!("{why}; Git says {}", said.trim());
}
Err(io::Error::other(why))
}
/// Exit 1 is an answer: files differ, key unset.
pub fn run_or_one(&self, dir: &Path, args: &[&str]) -> io::Result<Vec<u8>> {
let output = self.command(dir, args).output()?;
match output.status.code() {
Some(1) => Ok(output.stdout),
_ => checked(args, output),
}
}
/// `merge-file` exits with its conflict count; past 127 fails.
pub fn run_counted(&self, dir: &Path, args: &[&str]) -> io::Result<Vec<u8>> {
let output = self.command(dir, args).output()?;
match output.status.code() {
Some(0..=127) => Ok(output.stdout),
_ => checked(args, output),
}
}
pub(crate) fn command(&self, dir: &Path, args: &[&str]) -> Command {
ely_platform::git::command(&self.path, dir, args)
}
}
fn checked(args: &[&str], output: Output) -> io::Result<Vec<u8>> {
// The verb follows any `-c name=value` pairs.
let verb = args
.iter()
.copied()
.find(|arg| !arg.starts_with('-') && !arg.contains('='))
.unwrap_or("");
match output.status.success() {
true => {
log::debug!("tree: git {verb} done");
Ok(output.stdout)
}
false => {
// Conflicts go to stdout, hooks to stderr.
let said: Vec<String> = [&output.stdout, &output.stderr]
.into_iter()
.map(|bytes| String::from_utf8_lossy(bytes).trim().to_string())
.filter(|said| !said.is_empty())
.collect();
log::warn!("tree: git {verb} failed, {}", output.status);
Err(io::Error::other(format!(
"git {verb} ({}):\n{}",
output.status,
said.join("\n")
)))
}
}
}
/// Ely's own Git, whatever version it was pinned at.
fn bundled_version(path: &Path) -> io::Result<String> {
ely_platform::git::version_of(path).map(|(version, _)| version)
}
+3 -3
View File
@@ -36,9 +36,9 @@ pub fn open(git: &Git, path: &Path) -> io::Result<Repo> {
let common = ["rev-parse", "--path-format=absolute", "--git-common-dir"];
let common_dir = text(git.run(path, &common)?)?;
let repo = Repo {
root: std::fs::canonicalize(root)?,
git_dir: std::fs::canonicalize(git_dir)?,
common_dir: std::fs::canonicalize(common_dir)?,
root: dunce::canonicalize(root)?,
git_dir: dunce::canonicalize(git_dir)?,
common_dir: dunce::canonicalize(common_dir)?,
};
log::info!("tree: opened {}", repo.root.display());
Ok(repo)
+1 -1
View File
@@ -66,7 +66,7 @@ pub fn worktrees(git: &Git, root: &Path) -> io::Result<Vec<Worktree>> {
found.extend(now);
// Real paths, as the open repository's root is.
for tree in &mut found {
match std::fs::canonicalize(&tree.path) {
match dunce::canonicalize(&tree.path) {
Ok(real) => tree.path = real,
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
+2 -2
View File
@@ -70,7 +70,7 @@ fn a_repository_opens_from_inside_and_a_bare_one_does_not() {
let opened = repo::open(&git(), &root.join("deep")).expect("opens");
assert_eq!(
opened.root,
std::fs::canonicalize(&root).expect("real"),
dunce::canonicalize(&root).expect("real"),
"the real top"
);
assert!(opened.git_dir.ends_with(".git"));
@@ -324,7 +324,7 @@ fn a_gone_upstream_has_no_counts() {
#[test]
fn a_linked_file_keeps_its_own_name() {
let dir = tempfile::tempdir().expect("a temp dir");
let root = std::fs::canonicalize(dir.path()).expect("real");
let root = dunce::canonicalize(dir.path()).expect("real");
std::fs::create_dir(root.join("docs")).expect("a folder");
std::fs::write(root.join("real.md"), "a").expect("a file");
let outside = tempfile::tempdir().expect("another dir");
+3 -3
View File
@@ -252,10 +252,10 @@ fn worktrees_list_branches_and_prunable_folders() {
.expect("adds");
let list = structure::worktrees(&git(), &root).expect("reads");
assert_eq!(list.len(), 2);
assert_eq!(list[0].path, std::fs::canonicalize(&root).expect("real"));
assert_eq!(list[0].path, dunce::canonicalize(&root).expect("real"));
assert_eq!(
(list[1].path.clone(), list[1].branch.as_deref()),
(std::fs::canonicalize(&other).expect("real"), Some("side"))
(dunce::canonicalize(&other).expect("real"), Some("side"))
);
std::fs::remove_dir_all(&other).expect("removes");
let list = structure::worktrees(&git(), &root).expect("reads");
@@ -497,5 +497,5 @@ fn a_worktree_registered_through_a_link_lists_its_real_path() {
)
.expect("writes");
let list = structure::worktrees(&git(), &root).expect("reads");
assert_eq!(list[1].path, std::fs::canonicalize(&other).expect("real"));
assert_eq!(list[1].path, dunce::canonicalize(&other).expect("real"));
}
+1 -1
View File
@@ -101,7 +101,7 @@ fn one_tab_per_repository_and_reopening_selects_it() {
let recent = cx.update(|_, app| tree.read(app).recent.clone());
assert_eq!(
recent[0],
std::fs::canonicalize(&first).expect("real"),
dunce::canonicalize(&first).expect("real"),
"newest first"
);
cx.update(|_, app| tree.update(app, |tree, cx| tree.close_active(cx)));
+1 -1
View File
@@ -156,7 +156,7 @@ pub fn open(app: AppId, window: &mut Window, cx: &mut App) -> Entity<Root> {
/// `file` as Git names it; a symlink stays itself.
pub(crate) fn inside(root: &Path, file: &Path) -> io::Result<String> {
let name = file.file_name().ok_or_else(|| io::Error::other("a path without a name"))?;
let dir = std::fs::canonicalize(file.parent().unwrap_or(Path::new(".")))?;
let dir = dunce::canonicalize(file.parent().unwrap_or(Path::new(".")))?;
let rel = dir.join(name);
let rel = rel.strip_prefix(root).map_err(io::Error::other)?;
rel.to_str().map(str::to_owned).ok_or_else(|| io::Error::other("a path that is not UTF-8"))
+268 -283
View File
@@ -1,283 +1,268 @@
//! What the window draws.
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
feedback::{EmptyState, ErrorView},
lists::{List, ListItem},
navigation::{EditorTab, EditorTabs},
primitives::IconName,
shell::{StatusBar, StatusBarItem},
};
use ely_platform::AppId;
use ely_ui::{Shell, commands};
use gpui::{
AnyElement, App, Context, InteractiveElement as _, IntoElement, ParentElement as _, Render,
SharedString, Styled as _, Window, div,
};
use crate::{
menu,
status::{Head, State},
tree::{Side, Tree, text},
};
fn name(path: &std::path::Path) -> String {
path.file_name().map_or_else(
|| path.display().to_string(),
|name| name.to_string_lossy().into_owned(),
)
}
/// Where HEAD is, in words.
pub fn branch(head: &Head, cx: &App) -> SharedString {
match (&head.branch, &head.oid) {
(Some(branch), Some(_)) => branch.clone().into(),
(Some(branch), None) => text(cx, "tree.branch.unborn", &[("branch", branch)]),
(None, oid) => {
let short = oid.as_deref().map_or("", |oid| &oid[..oid.len().min(7)]);
text(cx, "tree.branch.detached", &[("oid", short)])
}
}
}
impl Tree {
fn tab_strip(&self, cx: &mut Context<Self>) -> Option<EditorTabs> {
if self.repos.is_empty() {
return None;
}
let tabs = self
.repos
.iter()
.map(|opened| EditorTab::new(opened.id.to_string(), name(&opened.repo.root)));
let (select, close) = (cx.weak_entity(), cx.weak_entity());
let strip = EditorTabs::new("tree-tabs", tabs.collect::<Vec<_>>())
.on_select(move |id, _, cx| {
let id = id.parse().expect("tab ids are numbers");
select
.update(cx, |tree, cx| tree.activate(Some(id), cx))
.ok();
})
.on_close(move |id, _, cx| {
let id = id.parse().expect("tab ids are numbers");
close.update(cx, |tree, cx| tree.close(id, cx)).ok();
});
Some(match self.active {
Some(id) => strip.selected(id.to_string()),
None => strip,
})
}
fn status_bar(&self, cx: &mut Context<Self>) -> StatusBar {
let mut bar = StatusBar::new();
if let Some(opened) = self.active() {
if let Some(Ok(status)) = &opened.status {
let head = &status.head;
bar = bar.left(
StatusBarItem::new("tree-branch")
.icon(IconName::GitBranch)
.label(branch(head, cx)),
);
if let Some(upstream) = &head.upstream {
let words = match head.counts {
Some((ahead, behind)) => text(
cx,
"tree.ahead_behind",
&[
("upstream", upstream),
("ahead", &ahead.to_string()),
("behind", &behind.to_string()),
],
),
None => text(cx, "tree.upstream_gone", &[("upstream", upstream)]),
};
bar = bar.left(StatusBarItem::new("tree-upstream").label(words));
}
}
bar = bar.left(
StatusBarItem::new("tree-path").label(opened.repo.root.display().to_string()),
);
}
if let Ok(git) = &self.git {
let key = if git.bundled {
"tree.git.bundled"
} else {
"tree.git.system"
};
bar = bar.right(StatusBarItem::new("tree-git").label(text(
cx,
key,
&[
("version", &git.version),
("path", &git.path.display().to_string()),
],
)));
}
bar
}
fn start_page(&self, cx: &mut Context<Self>) -> AnyElement {
let this = cx.weak_entity();
let button = |id: &'static str, key: &str, run: fn(&mut Window, &mut App)| {
Button::new(id, text(cx, key, &[])).on_click(move |_, window, cx| run(window, cx))
};
let empty = EmptyState::new(
"tree-start",
IconName::GitBranch,
text(cx, "tree.empty", &[]),
)
.action(
button("tree-open", "tree.open", |window, cx| {
window.dispatch_action(Box::new(commands::Open), cx)
})
.variant(ButtonVariant::Primary),
)
.action(button("tree-new", "tree.new", |window, cx| {
window.dispatch_action(Box::new(commands::New), cx)
}))
.action(button("tree-clone", "tree.clone", |window, cx| {
window.dispatch_action(Box::new(menu::CloneRepository), cx)
}));
let recent = self.recent.iter().enumerate().map(|(at, path)| {
let (this, path) = (this.clone(), path.clone());
ListItem::new(("tree-recent", at), name(&path))
.description(path.display().to_string())
.on_click(move |_, _, cx| {
this.update(cx, |tree, cx| tree.open_path(path.clone(), cx))
.ok();
})
.into_any_element()
});
div()
.flex()
.flex_col()
.items_center()
.child(empty)
.child(div().w_96().child(List::new().children(recent)))
.into_any_element()
}
fn body(&self, cx: &mut Context<Self>) -> AnyElement {
if let Err(error) = &self.git {
return ErrorView::new(
"tree-no-git",
IconName::TriangleAlert,
text(cx, "tree.no_git", &[]),
)
.detail(error.clone())
.into_any_element();
}
if let Some(cloning) = &self.cloning {
let this = cx.weak_entity();
let title = text(cx, "tree.cloning", &[("name", &name(&cloning.into))]);
let state = EmptyState::new("tree-cloning", IconName::Download, title)
.body(cloning.said.clone());
if !crate::running::STOPS {
return state.into_any_element();
}
let stop = Button::new("tree-clone-stop", text(cx, "tree.clone.stop", &[]))
.disabled(cloning.stopping)
.on_click(move |_, _, cx| {
this.update(cx, |tree, cx| tree.stop_clone(cx)).ok();
});
return state.action(stop).into_any_element();
}
let Some(opened) = self.active() else {
return self.start_page(cx);
};
if let Some(look) = self.look.as_ref().filter(|look| look.repo() == opened.id) {
return self.look_pane(look, cx);
}
match self.side {
Side::History => return self.commit_pane(&opened.hist, cx),
Side::Branches => return self.refs_pane(cx),
Side::Places => return self.places_pane(cx),
Side::Changes => {}
}
if let Some(shown) = self.shown.as_ref().filter(|shown| shown.repo == opened.id) {
return self.pane(shown, cx);
}
match &opened.status {
None => div().into_any_element(),
Some(Err(error)) => ErrorView::new(
"tree-unread",
IconName::TriangleAlert,
text(cx, "tree.unreadable", &[]),
)
.detail(error.clone())
.into_any_element(),
Some(Ok(status)) => {
let changed = status
.entries
.iter()
.filter(|entry| entry.state != State::Ignored)
.count();
let (icon, title) = summary(changed, cx);
EmptyState::new(("tree-summary", opened.id), icon, title).into_any_element()
}
}
}
}
impl Render for Tree {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let title: SharedString = self.active().map_or_else(
|| AppId::Tree.name().into(),
|opened| name(&opened.repo.root).into(),
);
let left = self
.active()
.filter(|opened| matches!(opened.status, Some(Ok(_))))
.map(|_| self.changes.clone().into());
let mut shell = Shell::new(title, &self.focus)
.left(left)
.status(self.status_bar(cx))
.children(self.net_alert(cx))
.children(self.op_alert(cx))
.child(self.body(cx));
if let Some(strip) = self.tab_strip(cx) {
shell = shell.tabs(strip);
}
div()
.size_full()
.key_context(menu::CONTEXT)
.track_focus(&self.focus)
.on_action(cx.listener(Tree::on_open))
.on_action(cx.listener(Tree::on_new))
.on_action(cx.listener(Tree::on_clone))
.on_action(cx.listener(Tree::on_close))
.on_action(cx.listener(|tree, _: &menu::Fetch, _, cx| tree.fetch_all(cx)))
.on_action(cx.listener(|tree, _: &menu::Pull, _, cx| tree.pull(cx)))
.on_action(cx.listener(|tree, _: &menu::Push, _, cx| tree.push(cx)))
.on_action(cx.listener(|tree, _: &menu::ForcePush, _, cx| tree.ask_force(cx)))
.on_action(cx.listener(|tree, _: &menu::OpenTerminal, _, cx| tree.open_terminal(cx)))
.on_action(cx.listener(|tree, _: &menu::Reveal, _, cx| tree.reveal(cx)))
.on_action(cx.listener(|tree, _: &menu::CleanUntracked, _, cx| tree.ask_clean(cx)))
.on_action(
cx.listener(|tree, _: &menu::ChooseEditor, _, cx| tree.pick_editor(None, cx)),
)
.child(shell)
.children(self.dialog(cx))
}
}
/// The work tree's line: clean, or how many files changed.
pub(crate) fn summary(changed: usize, cx: &App) -> (IconName, SharedString) {
match changed {
0 => (IconName::CircleCheck, text(cx, "tree.clean", &[])),
n => (IconName::GitBranch, ely_ui::i18n::counted(cx, "tree.changed", n as u64, &[("n", &n.to_string())])),
}
}
#[cfg(test)]
mod tests {
#[gpui::test]
fn the_summary_counts_files_in_both_languages(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
ely_ui::i18n::install_as("en-US", cx);
let said = |n: usize, cx: &gpui::App| super::summary(n, cx).1.to_string();
assert_eq!([said(0, cx), said(1, cx), said(2, cx)], ["Nothing to commit", "1 file changed", "2 files changed"]);
ely_ui::i18n::install_as("zh-CN", cx);
assert_eq!([said(1, cx), said(2, cx)], ["1 个文件有改动", "2 个文件有改动"]);
});
}
}
//! What the window draws.
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
feedback::{EmptyState, ErrorView},
lists::{List, ListItem},
navigation::{EditorTab, EditorTabs},
primitives::IconName,
shell::{StatusBar, StatusBarItem},
};
use ely_platform::AppId;
use ely_ui::{Shell, commands};
use gpui::{
AnyElement, App, Context, InteractiveElement as _, IntoElement, ParentElement as _, Render,
SharedString, Styled as _, Window, div,
};
use crate::{
menu,
status::{Head, State},
tree::{Side, Tree, text},
};
fn name(path: &std::path::Path) -> String {
path.file_name().map_or_else(
|| path.display().to_string(),
|name| name.to_string_lossy().into_owned(),
)
}
/// Where HEAD is, in words.
pub fn branch(head: &Head, cx: &App) -> SharedString {
match (&head.branch, &head.oid) {
(Some(branch), Some(_)) => branch.clone().into(),
(Some(branch), None) => text(cx, "tree.branch.unborn", &[("branch", branch)]),
(None, oid) => {
let short = oid.as_deref().map_or("", |oid| &oid[..oid.len().min(7)]);
text(cx, "tree.branch.detached", &[("oid", short)])
}
}
}
impl Tree {
fn tab_strip(&self, cx: &mut Context<Self>) -> Option<EditorTabs> {
if self.repos.is_empty() {
return None;
}
let tabs = self
.repos
.iter()
.map(|opened| EditorTab::new(opened.id.to_string(), name(&opened.repo.root)));
let (select, close) = (cx.weak_entity(), cx.weak_entity());
let strip = EditorTabs::new("tree-tabs", tabs.collect::<Vec<_>>())
.on_select(move |id, _, cx| {
let id = id.parse().expect("tab ids are numbers");
select
.update(cx, |tree, cx| tree.activate(Some(id), cx))
.ok();
})
.on_close(move |id, _, cx| {
let id = id.parse().expect("tab ids are numbers");
close.update(cx, |tree, cx| tree.close(id, cx)).ok();
});
Some(match self.active {
Some(id) => strip.selected(id.to_string()),
None => strip,
})
}
fn status_bar(&self, cx: &mut Context<Self>) -> StatusBar {
let mut bar = StatusBar::new();
if let Some(opened) = self.active() {
if let Some(Ok(status)) = &opened.status {
let head = &status.head;
bar = bar.left(
StatusBarItem::new("tree-branch")
.icon(IconName::GitBranch)
.label(branch(head, cx)),
);
if let Some(upstream) = &head.upstream {
let words = match head.counts {
Some((ahead, behind)) => text(
cx,
"tree.ahead_behind",
&[
("upstream", upstream),
("ahead", &ahead.to_string()),
("behind", &behind.to_string()),
],
),
None => text(cx, "tree.upstream_gone", &[("upstream", upstream)]),
};
bar = bar.left(StatusBarItem::new("tree-upstream").label(words));
}
}
bar = bar.left(
StatusBarItem::new("tree-path").label(opened.repo.root.display().to_string()),
);
}
bar
}
fn start_page(&self, cx: &mut Context<Self>) -> AnyElement {
let this = cx.weak_entity();
let button = |id: &'static str, key: &str, run: fn(&mut Window, &mut App)| {
Button::new(id, text(cx, key, &[])).on_click(move |_, window, cx| run(window, cx))
};
let empty = EmptyState::new(
"tree-start",
IconName::GitBranch,
text(cx, "tree.empty", &[]),
)
.action(
button("tree-open", "tree.open", |window, cx| {
window.dispatch_action(Box::new(commands::Open), cx)
})
.variant(ButtonVariant::Primary),
)
.action(button("tree-new", "tree.new", |window, cx| {
window.dispatch_action(Box::new(commands::New), cx)
}))
.action(button("tree-clone", "tree.clone", |window, cx| {
window.dispatch_action(Box::new(menu::CloneRepository), cx)
}));
let recent = self.recent.iter().enumerate().map(|(at, path)| {
let (this, path) = (this.clone(), path.clone());
ListItem::new(("tree-recent", at), name(&path))
.description(path.display().to_string())
.on_click(move |_, _, cx| {
this.update(cx, |tree, cx| tree.open_path(path.clone(), cx))
.ok();
})
.into_any_element()
});
div()
.flex()
.flex_col()
.items_center()
.child(empty)
.child(div().w_96().child(List::new().children(recent)))
.into_any_element()
}
fn body(&self, cx: &mut Context<Self>) -> AnyElement {
if let Err(error) = &self.git {
return ErrorView::new(
"tree-no-git",
IconName::TriangleAlert,
text(cx, "tree.no_git", &[]),
)
.detail(error.clone())
.into_any_element();
}
if let Some(cloning) = &self.cloning {
let this = cx.weak_entity();
let title = text(cx, "tree.cloning", &[("name", &name(&cloning.into))]);
let state = EmptyState::new("tree-cloning", IconName::Download, title)
.body(cloning.said.clone());
if !crate::running::STOPS {
return state.into_any_element();
}
let stop = Button::new("tree-clone-stop", text(cx, "tree.clone.stop", &[]))
.disabled(cloning.stopping)
.on_click(move |_, _, cx| {
this.update(cx, |tree, cx| tree.stop_clone(cx)).ok();
});
return state.action(stop).into_any_element();
}
let Some(opened) = self.active() else {
return self.start_page(cx);
};
if let Some(look) = self.look.as_ref().filter(|look| look.repo() == opened.id) {
return self.look_pane(look, cx);
}
match self.side {
Side::History => return self.commit_pane(&opened.hist, cx),
Side::Branches => return self.refs_pane(cx),
Side::Places => return self.places_pane(cx),
Side::Changes => {}
}
if let Some(shown) = self.shown.as_ref().filter(|shown| shown.repo == opened.id) {
return self.pane(shown, cx);
}
match &opened.status {
None => div().into_any_element(),
Some(Err(error)) => ErrorView::new(
"tree-unread",
IconName::TriangleAlert,
text(cx, "tree.unreadable", &[]),
)
.detail(error.clone())
.into_any_element(),
Some(Ok(status)) => {
let changed = status
.entries
.iter()
.filter(|entry| entry.state != State::Ignored)
.count();
let (icon, title) = summary(changed, cx);
EmptyState::new(("tree-summary", opened.id), icon, title).into_any_element()
}
}
}
}
impl Render for Tree {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let title: SharedString = self.active().map_or_else(
|| AppId::Tree.name().into(),
|opened| name(&opened.repo.root).into(),
);
let left = self
.active()
.filter(|opened| matches!(opened.status, Some(Ok(_))))
.map(|_| self.changes.clone().into());
let mut shell = Shell::new(title, &self.focus)
.left(left)
.status(self.status_bar(cx))
.children(self.net_alert(cx))
.children(self.op_alert(cx))
.child(self.body(cx));
if let Some(strip) = self.tab_strip(cx) {
shell = shell.tabs(strip);
}
div()
.size_full()
.key_context(menu::CONTEXT)
.track_focus(&self.focus)
.on_action(cx.listener(Tree::on_open))
.on_action(cx.listener(Tree::on_new))
.on_action(cx.listener(Tree::on_clone))
.on_action(cx.listener(Tree::on_close))
.on_action(cx.listener(|tree, _: &menu::Fetch, _, cx| tree.fetch_all(cx)))
.on_action(cx.listener(|tree, _: &menu::Pull, _, cx| tree.pull(cx)))
.on_action(cx.listener(|tree, _: &menu::Push, _, cx| tree.push(cx)))
.on_action(cx.listener(|tree, _: &menu::ForcePush, _, cx| tree.ask_force(cx)))
.on_action(cx.listener(|tree, _: &menu::OpenTerminal, _, cx| tree.open_terminal(cx)))
.on_action(cx.listener(|tree, _: &menu::Reveal, _, cx| tree.reveal(cx)))
.on_action(cx.listener(|tree, _: &menu::CleanUntracked, _, cx| tree.ask_clean(cx)))
.on_action(
cx.listener(|tree, _: &menu::ChooseEditor, _, cx| tree.pick_editor(None, cx)),
)
.child(shell)
.children(self.dialog(cx))
}
}
/// The work tree's line: clean, or how many files changed.
pub(crate) fn summary(changed: usize, cx: &App) -> (IconName, SharedString) {
match changed {
0 => (IconName::CircleCheck, text(cx, "tree.clean", &[])),
n => (IconName::GitBranch, ely_ui::i18n::counted(cx, "tree.changed", n as u64, &[("n", &n.to_string())])),
}
}
#[cfg(test)]
mod tests {
#[gpui::test]
fn the_summary_counts_files_in_both_languages(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
ely_ui::i18n::install_as("en-US", cx);
let said = |n: usize, cx: &gpui::App| super::summary(n, cx).1.to_string();
assert_eq!([said(0, cx), said(1, cx), said(2, cx)], ["Nothing to commit", "1 file changed", "2 files changed"]);
ely_ui::i18n::install_as("zh-CN", cx);
assert_eq!([said(1, cx), said(2, cx)], ["1 个文件有改动", "2 个文件有改动"]);
});
}
}
-4
View File
@@ -24,8 +24,6 @@ pub(super) const EN: &[(&str, &str)] = &[
("tree.branch.unborn", "{branch}, no commits yet"),
("tree.ahead_behind", "{upstream} ↑{ahead} ↓{behind}"),
("tree.upstream_gone", "{upstream} is gone"),
("tree.git.bundled", "{version}, bundled, {path}"),
("tree.git.system", "{version}, system, {path}"),
("tree.busy", "Wait for this repository's changes to finish."),
("tree.gone", "{path} is not on disk."),
("tree.discard.title", "Discard changes to {path}?"),
@@ -155,8 +153,6 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("tree.branch.unborn", "{branch},尚无提交"),
("tree.ahead_behind", "{upstream} ↑{ahead} ↓{behind}"),
("tree.upstream_gone", "{upstream} 已不存在"),
("tree.git.bundled", "{version},随附,{path}"),
("tree.git.system", "{version},系统,{path}"),
("tree.busy", "请等此仓库的改动完成。"),
("tree.gone", "{path} 不在磁盘上。"),
("tree.discard.title", "放弃 {path} 的改动?"),
+1 -1
View File
@@ -36,7 +36,7 @@ fetch() { # name url rev [sparse paths...]
echo "fetch $name $rev"
}
fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git 3eb4d346fe88efe475daa88e0e042ca3680a386c
fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git bd014707fd47cd9d0651a9af8c1db6033718a6ea
fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b
fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \
include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \