Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
Executable
+188
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env bash
|
||||
# MARK-05, MARK-10 live: folder tree, outline, search, recent.
|
||||
set -euo pipefail
|
||||
|
||||
root=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
[ "$(uname)" = Darwin ] || { echo "SKIP: macOS only"; exit 0; }
|
||||
[ -d "${ELY_CEF:-$root/engines/cef}/Chromium Embedded Framework.framework" ] || {
|
||||
echo "SKIP: no CEF; run scripts/fetch-cef.sh"
|
||||
exit 0
|
||||
}
|
||||
command -v npm >/dev/null || { echo "SKIP: no npm"; exit 0; }
|
||||
work=$(mktemp -d)
|
||||
app=
|
||||
trap 'if [ -n "$app" ]; then kill "$app" 2>/dev/null || true; wait "$app" 2>/dev/null || true; fi; rm -rf "$work"' EXIT
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
"$root/scripts/build-mark-content.sh" >"$work/build" 2>&1 || fail "build: $(tail -n 5 "$work/build")"
|
||||
cargo build -q -p ely-mark --manifest-path "$root/Cargo.toml"
|
||||
for tool in ax-values ax-press click; do
|
||||
swiftc -O -o "$work/$tool" "$root/tests/scripts/$tool.swift"
|
||||
done
|
||||
mkdir -p "$work/notes/sub"
|
||||
# A heading inside a quote Milkdown rewrites: one raw block.
|
||||
printf '# Alpha\n\nSome text.\n\n> ## Beta ##\n>\n> More words here.\n' >"$work/notes/a.md"
|
||||
# A hit hidden in a link's address comes first.
|
||||
printf '# Other\n\nwords [link](words)\nwords too\n' >"$work/notes/sub/b.md"
|
||||
printf 'banana\nana too\n\n[link](words)\nwords two' >"$work/notes/d.md"
|
||||
printf -- '---\ntitle: T\n---\n# Heading\n\nneedle here.\n\n# Other\n\nother paragraph.\n' >"$work/notes/c.md"
|
||||
printf 'words\n' >"$work/notes/skip.txt"
|
||||
# A file the search cannot read.
|
||||
printf 'words\n' >"$work/notes/bad.md"
|
||||
chmod 000 "$work/notes/bad.md"
|
||||
launch() {
|
||||
ELY_LOG=info ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$work" \
|
||||
"$root/target/debug/ely-mark" "$@" >"$work/out" 2>"$work/err" &
|
||||
app=$!
|
||||
}
|
||||
waits() {
|
||||
for _ in $(seq 120); do
|
||||
grep -qF "$1" "$work/err" && return 0
|
||||
kill -0 "$app" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/err")"
|
||||
sleep 0.25
|
||||
done
|
||||
fail "never saw '$1': $(grep -E 'mark|office' "$work/err" | tail -n 5)"
|
||||
}
|
||||
# Sets x and y to element `$1`'s center.
|
||||
at() {
|
||||
local found
|
||||
for _ in $(seq 40); do
|
||||
found=$("$work/ax-values" "$app" | awk -F'\t' -v want="title=$1" '$1 != "AXMenuItem" && $3 == want { sub("at=", "", $5); print $5; exit }')
|
||||
if [ -n "$found" ]; then
|
||||
IFS=, read -r x y <<<"$found"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
fail "no '$1' in the AX tree"
|
||||
}
|
||||
key() { osascript -e "tell application \"System Events\" to $1" -e 'delay 0.8' >/dev/null; }
|
||||
press() { [ "$("$work/ax-press" "$app" "$1")" = pressed ] || fail "could not press $1"; sleep 0.8; }
|
||||
saves() {
|
||||
local before
|
||||
before=$(grep -c "office: saved" "$work/err" || true)
|
||||
key 'keystroke "s" using command down'
|
||||
for _ in $(seq 40); do
|
||||
[ "$(grep -c "office: saved" "$work/err" || true)" -gt "$before" ] && return 0
|
||||
sleep 0.25
|
||||
done
|
||||
fail "no save"
|
||||
}
|
||||
|
||||
launch "$work/notes" "$work/notes/a.md"
|
||||
waits 'mark: shown as'
|
||||
[ "$("$work/ax-press" $$ none)" != untrusted ] || fail "needs Accessibility permission"
|
||||
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 1' >/dev/null
|
||||
# Tree rows are tree items; Return opens a picked one.
|
||||
at b.md
|
||||
"$work/click" "$x" "$y"
|
||||
key 'key code 36'
|
||||
waits "opened b.md"
|
||||
# An outline heading takes the caret there.
|
||||
press a.md
|
||||
press Outline
|
||||
at Outline
|
||||
"$work/click" "$((x - 64))" "$((y + 64))"
|
||||
key 'keystroke "Z"'
|
||||
saves
|
||||
grep -qx '> ## ZBeta ##' "$work/notes/a.md" || fail "outline jump: $(cat "$work/notes/a.md")"
|
||||
# Folder search finds lines; a hit opens there.
|
||||
press Search
|
||||
at Folder
|
||||
"$work/click" "$x" "$y"
|
||||
press "This file"
|
||||
"$work/click" "$x" "$((y - 37))"
|
||||
key 'keystroke "words"'
|
||||
key 'key code 36'
|
||||
press Folder
|
||||
at "> More words here."
|
||||
at "words too"
|
||||
at "bad.md was not searched"
|
||||
! "$work/ax-values" "$app" | grep -q "title=words " || fail "a .txt file was searched"
|
||||
chmod 600 "$work/notes/bad.md"
|
||||
# Inside a paragraph the caret lands on the hit's line.
|
||||
press "words too"
|
||||
key 'key code 36'
|
||||
key 'keystroke "Q"'
|
||||
saves
|
||||
[ "$(cat "$work/notes/sub/b.md")" = $'# Other\n\nwords [link](words)\nQwords too' ] || fail "search hit: $(cat "$work/notes/sub/b.md")"
|
||||
# Lines count from the file's top, past front matter.
|
||||
press Search
|
||||
at Folder
|
||||
"$work/click" "$x" "$((y - 37))"
|
||||
key 'keystroke "a" using command down'
|
||||
key 'keystroke "needle"'
|
||||
key 'key code 36'
|
||||
at "needle here."
|
||||
press "needle here."
|
||||
key 'key code 36'
|
||||
waits "opened c.md"
|
||||
sleep 1
|
||||
key 'keystroke "Q"'
|
||||
saves
|
||||
grep -qx 'Qneedle here.' "$work/notes/c.md" || fail "front matter offset: $(cat "$work/notes/c.md")"
|
||||
# This file's results follow the shown tab.
|
||||
press Search
|
||||
press "This file"
|
||||
at "Qneedle here."
|
||||
press b.md
|
||||
sleep 1.5
|
||||
! "$work/ax-values" "$app" | grep -q "title=Qneedle here." || fail "results stayed with the last tab"
|
||||
# Overlapping hits; a hit hidden in a link's address.
|
||||
press Search
|
||||
at Folder
|
||||
"$work/click" "$x" "$((y - 37))"
|
||||
key 'keystroke "a" using command down'
|
||||
key 'keystroke "ana"'
|
||||
key 'key code 36'
|
||||
press Folder
|
||||
press "ana too"
|
||||
key 'key code 36'
|
||||
waits "opened d.md"
|
||||
sleep 1
|
||||
key 'keystroke "Q"'
|
||||
saves
|
||||
[ "$(cat "$work/notes/d.md")" = $'banana\nQana too\n\n[link](words)\nwords two' ] || fail "overlapping hit: $(cat "$work/notes/d.md")"
|
||||
press Search
|
||||
at Folder
|
||||
"$work/click" "$x" "$((y - 37))"
|
||||
key 'keystroke "a" using command down'
|
||||
key 'keystroke "words"'
|
||||
key 'key code 36'
|
||||
press Folder
|
||||
press "[link](words)"
|
||||
key 'key code 36'
|
||||
sleep 1
|
||||
key 'keystroke "R"'
|
||||
saves
|
||||
[ "$(cat "$work/notes/d.md")" = $'banana\nQana too\n\n[Rlink](words)\nwords two' ] || fail "hidden hit: $(cat "$work/notes/d.md")"
|
||||
# A rename shows in the folder's tree.
|
||||
press Files
|
||||
key 'key code 120'
|
||||
key 'keystroke "a" using command down'
|
||||
key 'keystroke "moved.md"'
|
||||
key 'key code 36'
|
||||
waits "renamed"
|
||||
at moved.md
|
||||
! "$work/ax-values" "$app" | grep -q "^AXRow.*title=d.md " || fail "the tree kept the old name"
|
||||
kill "$app"
|
||||
wait "$app" 2>/dev/null || true
|
||||
app=
|
||||
# The recent list outlives the app.
|
||||
python3 -c 'import json,sys; l=json.load(open(sys.argv[1])); sys.exit(0 if any(p.endswith("notes") for p in l) and any(p.endswith("a.md") for p in l) else 1)' \
|
||||
"$(find "$work/Library" -name recent.json)" || fail "recent: $(cat "$(find "$work/Library" -name recent.json)")"
|
||||
launch
|
||||
waits 'ElyMark window open'
|
||||
at notes
|
||||
press notes
|
||||
key 'key code 36'
|
||||
waits "opens folder"
|
||||
at Outline
|
||||
kill "$app"
|
||||
wait "$app" 2>/dev/null || true
|
||||
app=
|
||||
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
|
||||
echo "ok: mark places"
|
||||
Reference in new issue
Block a user