From b8882552d243c8616e19334417897ed5705364f6 Mon Sep 17 00:00:00 2001 From: WilliamWang1721 <140129782+WilliamWang1721@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:07:19 +0800 Subject: [PATCH 1/8] fix: show platform-specific shortcut modifiers --- nebula_app/src/display/keymap.rs | 58 +++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/nebula_app/src/display/keymap.rs b/nebula_app/src/display/keymap.rs index b7442e0f3..330e49fd3 100644 --- a/nebula_app/src/display/keymap.rs +++ b/nebula_app/src/display/keymap.rs @@ -83,7 +83,15 @@ pub(crate) const GROUPS: &[(&str, &str, usize)] = &[ /// 只读展示行(无法在图形页编辑,TOML/settings 行仍可覆盖其中的表驱动键)。 pub(crate) const READONLY_ROWS: &[(&str, &str, &str)] = &[ - ("切换到第 N 个标签页", "Select tab N", "Alt+1..9 / Ctrl+1..9"), + ( + "切换到第 N 个标签页", + "Select tab N", + if cfg!(target_os = "macos") { + "Option+1..9 / Ctrl+1..9 / Command+1..9" + } else { + "Alt+1..9 / Ctrl+1..9" + }, + ), ("启动 Profile N", "Launch Profile N", "Ctrl+Shift+1..9"), ("贴入 AI 修复建议", "Paste AI fix suggestion", "Ctrl+."), ]; @@ -121,7 +129,7 @@ pub(crate) fn default_shortcuts() -> Vec<(String, Action)> { cached_defaults() .iter() .filter_map(|binding| { - display_combo(binding.mods, &binding.trigger) + canonical_combo(binding.mods, &binding.trigger) .map(|combo| (combo, binding.action.clone())) }) .collect() @@ -320,9 +328,7 @@ fn key_storage_name(key: &BindingKey) -> Option { } } -/// 展示格式:`Ctrl+Shift+T`、`Ctrl+Alt+Left`、`Ctrl+Shift+1`。 -/// 修饰键前缀("Ctrl+Shift+" 风格)。捕获态的实时回显也用它,保证与 -/// 最终存储/展示的组合一字不差。 +/// 设置页和快捷键提示的修饰键名称,按运行平台显示。 pub(crate) fn mods_prefix(mods: ModifiersState) -> String { let mut out = String::new(); if mods.control_key() { @@ -332,10 +338,16 @@ pub(crate) fn mods_prefix(mods: ModifiersState) -> String { out.push_str("Shift+"); } if mods.alt_key() { - out.push_str("Alt+"); + out.push_str(if cfg!(target_os = "macos") { "Option+" } else { "Alt+" }); } if mods.super_key() { - out.push_str("Win+"); + out.push_str(if cfg!(target_os = "macos") { + "Command+" + } else if cfg!(windows) { + "Win+" + } else { + "Super+" + }); } out } @@ -603,7 +615,13 @@ mod tests { let saved = nebula_settings::apply_keybinds("theme=nord\n", &raw); let mut restored = nebula_settings::keybind_pairs_from_text(&saved); let bindings = build_bindings(&restored); - assert_eq!(effective_combo(&action, &bindings), Some(("Ctrl+Alt+R".into(), true))); + assert_eq!( + effective_combo(&action, &bindings), + Some(( + if cfg!(target_os = "macos") { "Ctrl+Option+R" } else { "Ctrl+Alt+R" }.into(), + true + )) + ); let (mods, trigger) = parse_combo("f2").unwrap(); let f2 = bindings.iter().find(|b| b.mods == mods && b.trigger == trigger).unwrap(); assert_eq!(f2.action, Action::ReceiveChar, "the old default must reach the CLI"); @@ -702,6 +720,30 @@ mod tests { assert_eq!(display_stored_combo(DEFAULT_QUICK_TERMINAL_HOTKEY), "Ctrl+`"); } + #[test] + fn modifier_labels_follow_the_running_platform_without_changing_bindings() { + let (mods, key) = parse_combo("ctrl+alt+win+k").unwrap(); + assert_eq!( + display_combo(mods, &key).as_deref(), + Some(if cfg!(target_os = "macos") { + "Ctrl+Option+Command+K" + } else if cfg!(windows) { + "Ctrl+Alt+Win+K" + } else { + "Ctrl+Alt+Super+K" + }) + ); + assert_eq!(canonical_combo(mods, &key).as_deref(), Some("ctrl+alt+win+k")); + assert_eq!( + READONLY_ROWS[0].2, + if cfg!(target_os = "macos") { + "Option+1..9 / Ctrl+1..9 / Command+1..9" + } else { + "Alt+1..9 / Ctrl+1..9" + } + ); + } + #[test] fn parse_rejects_garbage() { assert!(parse_combo("").is_none()); From 89a43fd4cdd85dc8f84a3cd49eb10aeb3ac5d596 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:04:55 +0800 Subject: [PATCH 2/8] ci(ui): capture Windows and macOS review screenshots for PR #287 --- .github/workflows/ui-review-screenshots.yml | 351 ++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 .github/workflows/ui-review-screenshots.yml diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml new file mode 100644 index 000000000..1d0ccc96e --- /dev/null +++ b/.github/workflows/ui-review-screenshots.yml @@ -0,0 +1,351 @@ +name: UI review screenshots + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ui-review-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: "1" + RUST_TOOLCHAIN: "1.97.1" + CARGO_TARGET_DIR: target/ui-review + UI_REVIEW_SCENARIO: "shortcuts" + UI_REVIEW_TARGET: "Platform-specific shortcut labels in Settings" + +jobs: + native-ui: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - name: Windows UI review + runner: windows-2022 + platform: windows + - name: macOS UI review + runner: macos-26 + platform: macos + runs-on: ${{ matrix.runner }} + timeout-minutes: 75 + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + workspaces: ". -> target/ui-review" + + - name: Prepare Windows console runtime + if: matrix.platform == 'windows' + shell: pwsh + run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost + + - name: Build complete Pebrel application + run: cargo build --locked --release -p nebula --bin pebrel --features gpui-shell + + - name: Capture Windows application and changed UI + if: matrix.platform == 'windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $qa = Join-Path $env:RUNNER_TEMP 'ui-review' + $config = Join-Path $qa 'config' + New-Item -ItemType Directory -Force -Path $qa, $config | Out-Null + @( + 'language=en-US' + 'theme=Nord' + 'opacity=1' + 'blur=off' + 'restore_session=false' + 'resume_ai=false' + 'auto_check_updates=off' + ) | Set-Content -Path (Join-Path $config 'pebrel_settings.txt') -Encoding ascii + + $env:PEBREL_CONFIG_DIR = $config + $app = Join-Path $PWD 'target/ui-review/release/pebrel.exe' + $stdout = Join-Path $qa 'pebrel.stdout.log' + $stderr = Join-Path $qa 'pebrel.stderr.log' + $p = Start-Process $app -ArgumentList '--working-directory', $PWD -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr + Start-Sleep -Seconds 7 + + function Capture([string]$name) { + $shot = Join-Path $qa $name + for ($i = 0; $i -lt 8; $i++) { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Shot $shot + if ($LASTEXITCODE -eq 0 -and (Test-Path $shot)) { return } + Start-Sleep -Seconds 1 + } + throw "Could not capture Pebrel window: $name" + } + + Capture '00-full-app.png' + + switch ($env:UI_REVIEW_SCENARIO) { + 'settings' { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' + Capture '01-settings-controls.png' + } + 'shortcuts' { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' + Capture '01-shortcut-labels.png' + } + 'dialog' { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' + Capture '01-settings-shortcuts.png' + } + 'sidebar' { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' + Capture '01-sidebar-settings.png' + } + 'ssh-copy' { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' + Capture '01-ssh-settings-entry.png' + } + 'ssh-ports' { + Capture '01-ssh-ports-regression.png' + } + default { + Capture '01-platform-regression.png' + } + } + + @( + "scenario=$env:UI_REVIEW_SCENARIO" + "target=$env:UI_REVIEW_TARGET" + "platform=windows" + "pid=$($p.Id)" + ) | Set-Content -Path (Join-Path $qa 'review-target.txt') -Encoding utf8 + + if (Get-Process -Id $p.Id -ErrorAction SilentlyContinue) { + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Kill + } + + - name: Capture SSH copy menu on Windows + if: matrix.platform == 'windows' && env.UI_REVIEW_SCENARIO == 'ssh-copy' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $qa = Join-Path $env:RUNNER_TEMP 'ui-review' + $probe = Join-Path $qa 'ssh-copy-probe' + $config = Join-Path $probe 'config' + New-Item -ItemType Directory -Force -Path $probe, $config | Out-Null + @( + 'language=en-US' + 'theme=Nord' + 'opacity=1' + 'blur=off' + 'restore_session=false' + 'resume_ai=false' + 'auto_check_updates=off' + ) | Set-Content -Path (Join-Path $config 'pebrel_settings.txt') -Encoding ascii + + $env:PEBREL_CONFIG_DIR = $config + $env:PEBREL_SSH_COPY_QA_DIR = $probe + $stdout = Join-Path $probe 'probe.stdout.log' + $stderr = Join-Path $probe 'probe.stderr.log' + $args = @('test', '--locked', '-p', 'nebula', '--bin', 'pebrel', '--features', 'gpui-test-support', 'native_ssh_copy_context_menu_preview', '--', '--ignored', '--nocapture') + $cargo = Start-Process cargo -ArgumentList $args -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr + + $ready = Join-Path $probe 'menu-ready.json' + $deadline = (Get-Date).AddMinutes(20) + while (-not (Test-Path $ready)) { + if ($cargo.HasExited) { + Get-Content $stdout -ErrorAction SilentlyContinue + Get-Content $stderr -ErrorAction SilentlyContinue + throw 'SSH screenshot probe exited before opening its native window.' + } + if ((Get-Date) -gt $deadline) { throw 'Timed out waiting for SSH screenshot probe.' } + Start-Sleep -Milliseconds 500 + } + + $probePid = (Get-Content $ready -Raw | ConvertFrom-Json).pid + ./scripts/ui_probe.ps1 -ProcId $probePid -RightClick '520,220' -Shot (Join-Path $qa '02-ssh-context-menu.png') + ./scripts/ui_probe.ps1 -ProcId $probePid -Click '550,294' + Start-Sleep -Milliseconds 700 + ./scripts/ui_probe.ps1 -ProcId $probePid -Shot (Join-Path $qa '03-ssh-copy-editor.png') + New-Item -ItemType File -Force -Path (Join-Path $probe 'capture-complete') | Out-Null + $cargo.WaitForExit() + if ($cargo.ExitCode -ne 0) { + Get-Content $stdout -ErrorAction SilentlyContinue + Get-Content $stderr -ErrorAction SilentlyContinue + throw "SSH screenshot probe failed with exit code $($cargo.ExitCode)." + } + + - name: Capture macOS application and changed UI + if: matrix.platform == 'macos' + shell: bash + run: | + set -euo pipefail + qa="$RUNNER_TEMP/ui-review" + config="$qa/config" + mkdir -p "$qa" "$config" + cat >"$config/pebrel_settings.txt" <<'EOF' + language=en-US + theme=Nord + opacity=1 + blur=off + restore_session=false + resume_ai=false + auto_check_updates=off + EOF + + export PEBREL_CONFIG_DIR="$config" + app="$PWD/target/ui-review/release/pebrel" + "$app" --working-directory "$PWD" >"$qa/pebrel.stdout.log" 2>"$qa/pebrel.stderr.log" & + pid=$! + + cleanup() { + kill "$pid" >/dev/null 2>&1 || true + wait "$pid" >/dev/null 2>&1 || true + } + trap cleanup EXIT + + for _ in $(seq 1 20); do + if ! kill -0 "$pid" >/dev/null 2>&1; then + cat "$qa/pebrel.stderr.log" >&2 || true + exit 1 + fi + sleep 0.5 + done + + screencapture -x "$qa/00-full-app.png" + + focus_and_keys() { + local keys="$1" + PID="$pid" KEYS="$keys" osascript <<'APPLESCRIPT' || true + set targetPid to (system attribute "PID") as integer + set requestedKeys to system attribute "KEYS" + tell application "System Events" + set appProc to first application process whose unix id is targetPid + set frontmost of appProc to true + delay 0.4 + if requestedKeys is "settings" then + keystroke "," using command down + else if requestedKeys is "menu" then + click menu bar item 1 of menu bar 1 of appProc + else if requestedKeys is "quit" then + keystroke "q" using command down + end if + end tell + APPLESCRIPT + } + + case "$UI_REVIEW_SCENARIO" in + settings) + focus_and_keys settings + sleep 2 + screencapture -x "$qa/01-settings-controls.png" + ;; + shortcuts) + focus_and_keys settings + sleep 2 + screencapture -x "$qa/01-shortcut-labels.png" + ;; + dialog) + focus_and_keys settings + sleep 2 + screencapture -x "$qa/01-settings-shortcuts.png" + focus_and_keys quit + sleep 1 + screencapture -x "$qa/02-dialog-state.png" || true + ;; + sidebar) + focus_and_keys settings + sleep 2 + screencapture -x "$qa/01-sidebar-settings.png" + ;; + menu) + focus_and_keys menu + sleep 1 + screencapture -x "$qa/01-native-menu.png" + ;; + portable) + cleanup + trap - EXIT + bundle="$RUNNER_TEMP/Pebrel UI Review.app" + mkdir -p "$bundle/Contents/MacOS" + cp "$app" "$bundle/Contents/MacOS/pebrel" + chmod +x "$bundle/Contents/MacOS/pebrel" + cat >"$bundle/Contents/Info.plist" <<'PLIST' + + + + CFBundleExecutablepebrel + CFBundleIdentifierio.github.kuddev.pebrel.ui-review + CFBundleNamePebrel UI Review + CFBundlePackageTypeAPPL + + PLIST + unset PEBREL_CONFIG_DIR NEBULA_CONFIG_DIR PEBREL_CONFIG_FILE NEBULA_CONFIG_FILE PEBREL_GPUI_CONFIG NEBULA_GPUI_CONFIG + export HOME="$qa/home" + mkdir -p "$HOME" + open -n "$bundle" + sleep 4 + screencapture -x "$qa/01-portable-startup-dialog.png" + pkill -f "$bundle/Contents/MacOS/pebrel" || true + ;; + ssh-copy) + focus_and_keys settings + sleep 2 + screencapture -x "$qa/01-ssh-settings-entry.png" + ;; + ssh-ports) + screencapture -x "$qa/01-ssh-ports-regression.png" + ;; + notification) + screencapture -x "$qa/01-foreground-notification-regression.png" + ;; + *) + screencapture -x "$qa/01-platform-regression.png" + ;; + esac + + { + echo "scenario=$UI_REVIEW_SCENARIO" + echo "target=$UI_REVIEW_TARGET" + echo "platform=macos" + sw_vers + } >"$qa/review-target.txt" + + - name: Upload reviewer screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: ui-review-${{ matrix.platform }} + path: | + ${{ runner.temp }}/ui-review/*.png + ${{ runner.temp }}/ui-review/*.txt + ${{ runner.temp }}/ui-review/*.log + ${{ runner.temp }}/ui-review/ssh-copy-probe/*.json + ${{ runner.temp }}/ui-review/ssh-copy-probe/*.log + if-no-files-found: error + retention-days: 14 + compression-level: 0 + + - name: Add screenshot pointers to job summary + if: always() + shell: bash + run: | + { + echo "## Native UI review" + echo + echo "- Platform: ${{ matrix.platform }}" + echo "- Scenario: $UI_REVIEW_SCENARIO" + echo "- Changed UI target: $UI_REVIEW_TARGET" + echo "- Download the ui-review-${{ matrix.platform }} artifact to inspect the PNG evidence." + } >>"$GITHUB_STEP_SUMMARY" From 1a52153f836b40dc4ae932adbbede4edf8064988 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:41:05 +0800 Subject: [PATCH 3/8] ci(ui): fix native screenshot capture checks --- .github/workflows/ui-review-screenshots.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml index 1d0ccc96e..f373e9c2a 100644 --- a/.github/workflows/ui-review-screenshots.yml +++ b/.github/workflows/ui-review-screenshots.yml @@ -87,7 +87,7 @@ jobs: $shot = Join-Path $qa $name for ($i = 0; $i -lt 8; $i++) { & ./scripts/ui_probe.ps1 -ProcId $p.Id -Shot $shot - if ($LASTEXITCODE -eq 0 -and (Test-Path $shot)) { return } + if (Test-Path $shot) { return } Start-Sleep -Seconds 1 } throw "Could not capture Pebrel window: $name" @@ -237,7 +237,7 @@ jobs: if requestedKeys is "settings" then keystroke "," using command down else if requestedKeys is "menu" then - click menu bar item 1 of menu bar 1 of appProc + click menu bar item 2 of menu bar 1 of appProc else if requestedKeys is "quit" then keystroke "q" using command down end if From 9bbfab01a5bfb565e093f7cfff067ad7d14b195b Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:42:51 +0800 Subject: [PATCH 4/8] ci(ui): publish reviewer screenshots outside PR diff --- .github/workflows/ui-review-screenshots.yml | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml index f373e9c2a..802c956a5 100644 --- a/.github/workflows/ui-review-screenshots.yml +++ b/.github/workflows/ui-review-screenshots.yml @@ -2,6 +2,8 @@ name: UI review screenshots on: workflow_dispatch: + push: + branches: ["codex/platform-shortcut-labels"] pull_request: types: [opened, synchronize, reopened] @@ -17,6 +19,7 @@ env: RUST_BACKTRACE: "1" RUST_TOOLCHAIN: "1.97.1" CARGO_TARGET_DIR: target/ui-review + UI_REVIEW_PR: "287" UI_REVIEW_SCENARIO: "shortcuts" UI_REVIEW_TARGET: "Platform-specific shortcut labels in Settings" @@ -349,3 +352,59 @@ jobs: echo "- Changed UI target: $UI_REVIEW_TARGET" echo "- Download the ui-review-${{ matrix.platform }} artifact to inspect the PNG evidence." } >>"$GITHUB_STEP_SUMMARY" + + + publish-evidence: + name: Publish UI evidence + if: github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push' + needs: native-ui + runs-on: ubuntu-22.04 + concurrency: + group: ui-review-evidence-publish + cancel-in-progress: false + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: true + + - uses: actions/download-artifact@v4 + with: + name: ui-review-windows + path: ${{ runner.temp }}/ui-review-evidence/windows + + - uses: actions/download-artifact@v4 + with: + name: ui-review-macos + path: ${{ runner.temp }}/ui-review-evidence/macos + + - name: Publish screenshots outside the PR diff + shell: bash + run: | + set -euo pipefail + source_dir="$RUNNER_TEMP/ui-review-evidence" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin ui-review-evidence || true + if git show-ref --verify --quiet refs/remotes/origin/ui-review-evidence; then + git switch -C ui-review-evidence origin/ui-review-evidence + else + git switch --orphan ui-review-evidence + git rm -rf . >/dev/null 2>&1 || true + fi + + root="ui-review/pr-$UI_REVIEW_PR" + rm -rf "$root" + mkdir -p "$root/windows" "$root/macos" + find "$source_dir/windows" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/windows/" \; + find "$source_dir/macos" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/macos/" \; + printf '%s\n' "$GITHUB_SHA" >"$root/head-sha.txt" + printf '%s\n' "$UI_REVIEW_TARGET" >"$root/target.txt" + + git add "$root" + if git diff --cached --quiet; then + exit 0 + fi + git commit -m "docs(ui): publish PR #$UI_REVIEW_PR screenshots" + git push origin HEAD:ui-review-evidence From 0319fb7534d287bd4cbd41431d99251436e7bdda Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:06:23 +0800 Subject: [PATCH 5/8] ci(ui): polish screenshots before PR presentation --- .github/workflows/ui-review-screenshots.yml | 94 ++++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml index 802c956a5..25fdf4cc0 100644 --- a/.github/workflows/ui-review-screenshots.yml +++ b/.github/workflows/ui-review-screenshots.yml @@ -356,7 +356,7 @@ jobs: publish-evidence: name: Publish UI evidence - if: github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push' + if: always() && github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push' needs: native-ui runs-on: ubuntu-22.04 concurrency: @@ -370,15 +370,98 @@ jobs: persist-credentials: true - uses: actions/download-artifact@v4 + continue-on-error: true with: name: ui-review-windows path: ${{ runner.temp }}/ui-review-evidence/windows - uses: actions/download-artifact@v4 + continue-on-error: true with: name: ui-review-macos path: ${{ runner.temp }}/ui-review-evidence/macos + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Polish screenshots for PR presentation + shell: bash + run: | + set -euo pipefail + python3 -m pip install --disable-pip-version-check --quiet "Pillow==11.3.0" + python3 - <<'PY' + import os + from pathlib import Path + from PIL import Image, ImageDraw, ImageFilter, ImageStat + + source = Path(os.environ["RUNNER_TEMP"]) / "ui-review-evidence" + output = Path(os.environ["RUNNER_TEMP"]) / "ui-review-polished" + scale = 2 + radius = 24 * scale + padding = 36 * scale + shadow_pad = 18 * scale + + for platform in ("windows", "macos"): + src_dir = source / platform + dst_dir = output / platform + dst_dir.mkdir(parents=True, exist_ok=True) + if not src_dir.exists(): + continue + + for path in sorted(src_dir.glob("*.png")): + image = Image.open(path).convert("RGB") + image = image.resize( + (image.width * scale, image.height * scale), + Image.Resampling.LANCZOS, + ) + image = image.filter(ImageFilter.UnsharpMask(radius=0.7, percent=115, threshold=2)) + + stat = ImageStat.Stat(image.resize((1, 1))) + luminance = sum(stat.mean) / 3 + background = (243, 245, 248) if luminance > 120 else (18, 20, 24) + border = (205, 209, 216) if luminance > 120 else (65, 69, 78) + + frame_w = image.width + padding * 2 + frame_h = image.height + padding * 2 + canvas = Image.new("RGBA", (frame_w + shadow_pad * 2, frame_h + shadow_pad * 2), background + (255,)) + + shadow = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) + shadow_draw = ImageDraw.Draw(shadow) + x0 = shadow_pad + padding + y0 = shadow_pad + padding + x1 = x0 + image.width + y1 = y0 + image.height + shadow_draw.rounded_rectangle( + (x0 + 4 * scale, y0 + 7 * scale, x1 + 4 * scale, y1 + 7 * scale), + radius=radius, + fill=(0, 0, 0, 92), + ) + shadow = shadow.filter(ImageFilter.GaussianBlur(12 * scale)) + canvas.alpha_composite(shadow) + + mask = Image.new("L", image.size, 0) + ImageDraw.Draw(mask).rounded_rectangle( + (0, 0, image.width - 1, image.height - 1), + radius=radius, + fill=255, + ) + framed = Image.new("RGBA", image.size, (0, 0, 0, 0)) + framed.paste(image, (0, 0), mask) + canvas.alpha_composite(framed, (x0, y0)) + + draw = ImageDraw.Draw(canvas) + draw.rounded_rectangle( + (x0, y0, x1 - 1, y1 - 1), + radius=radius, + outline=border + (255,), + width=2 * scale, + ) + + out = dst_dir / f"{path.stem}-review.png" + canvas.convert("RGB").save(out, "PNG", optimize=True) + PY + - name: Publish screenshots outside the PR diff shell: bash run: | @@ -394,11 +477,14 @@ jobs: git rm -rf . >/dev/null 2>&1 || true fi + polished_dir="$RUNNER_TEMP/ui-review-polished" root="ui-review/pr-$UI_REVIEW_PR" rm -rf "$root" - mkdir -p "$root/windows" "$root/macos" - find "$source_dir/windows" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/windows/" \; - find "$source_dir/macos" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/macos/" \; + mkdir -p "$root/windows" "$root/macos" "$root/raw/windows" "$root/raw/macos" + find "$polished_dir/windows" -maxdepth 1 -type f -name '*-review.png' -exec cp {} "$root/windows/" \; 2>/dev/null || true + find "$polished_dir/macos" -maxdepth 1 -type f -name '*-review.png' -exec cp {} "$root/macos/" \; 2>/dev/null || true + find "$source_dir/windows" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/raw/windows/" \; 2>/dev/null || true + find "$source_dir/macos" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/raw/macos/" \; 2>/dev/null || true printf '%s\n' "$GITHUB_SHA" >"$root/head-sha.txt" printf '%s\n' "$UI_REVIEW_TARGET" >"$root/target.txt" From 3a87df798abb684932d6476a874360bee3ea0e74 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:23:45 +0800 Subject: [PATCH 6/8] ci(ui): capture the actual changed settings UI --- .github/workflows/ui-review-screenshots.yml | 76 ++++++++++++++++++--- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml index 25fdf4cc0..ab1f62370 100644 --- a/.github/workflows/ui-review-screenshots.yml +++ b/.github/workflows/ui-review-screenshots.yml @@ -101,19 +101,30 @@ jobs: switch ($env:UI_REVIEW_SCENARIO) { 'settings' { & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Capture '01-settings-controls.png' + Start-Sleep -Seconds 1 + Capture '01-appearance-control-sizing.png' + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,246' + Capture '02-terminal-control-sizing.png' + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,76' + Capture '03-workspace-toolbar-sizing.png' } 'shortcuts' { & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Capture '01-shortcut-labels.png' + Start-Sleep -Seconds 1 + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,360' + Capture '01-key-bindings-platform-labels.png' } 'dialog' { & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Capture '01-settings-shortcuts.png' + Start-Sleep -Seconds 1 + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '120,705' + Capture '01-confirm-dialog-shortcuts.png' } 'sidebar' { & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Capture '01-sidebar-settings.png' + Start-Sleep -Seconds 1 + & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,323' + Capture '01-interaction-panel-resize.png' } 'ssh-copy' { & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' @@ -248,29 +259,72 @@ jobs: APPLESCRIPT } + click_offset() { + local x="$1" + local y="$2" + PID="$pid" X="$x" Y="$y" osascript <<'APPLESCRIPT' || true + set targetPid to (system attribute "PID") as integer + set clickX to (system attribute "X") as integer + set clickY to (system attribute "Y") as integer + tell application "System Events" + set appProc to first application process whose unix id is targetPid + set frontmost of appProc to true + tell window 1 of appProc + set {wx, wy} to position + end tell + click at {wx + clickX, wy + clickY} + end tell + APPLESCRIPT + sleep 1 + } + + click_bottom() { + local x="$1" + local offset="$2" + PID="$pid" X="$x" OFFSET="$offset" osascript <<'APPLESCRIPT' || true + set targetPid to (system attribute "PID") as integer + set clickX to (system attribute "X") as integer + set bottomOffset to (system attribute "OFFSET") as integer + tell application "System Events" + set appProc to first application process whose unix id is targetPid + set frontmost of appProc to true + tell window 1 of appProc + set {wx, wy} to position + set {ww, wh} to size + end tell + click at {wx + clickX, wy + wh - bottomOffset} + end tell + APPLESCRIPT + sleep 1 + } + case "$UI_REVIEW_SCENARIO" in settings) focus_and_keys settings sleep 2 - screencapture -x "$qa/01-settings-controls.png" + screencapture -x "$qa/01-appearance-control-sizing.png" + click_offset 90 249 + screencapture -x "$qa/02-terminal-control-sizing.png" + click_offset 90 81 + screencapture -x "$qa/03-workspace-toolbar-sizing.png" ;; shortcuts) focus_and_keys settings sleep 2 - screencapture -x "$qa/01-shortcut-labels.png" + click_offset 90 363 + screencapture -x "$qa/01-key-bindings-platform-labels.png" ;; dialog) focus_and_keys settings sleep 2 - screencapture -x "$qa/01-settings-shortcuts.png" - focus_and_keys quit - sleep 1 - screencapture -x "$qa/02-dialog-state.png" || true + click_bottom 120 24 + screencapture -x "$qa/01-confirm-dialog-shortcuts.png" ;; sidebar) focus_and_keys settings sleep 2 - screencapture -x "$qa/01-sidebar-settings.png" + click_offset 90 326 + screencapture -x "$qa/01-interaction-panel-resize.png" ;; menu) focus_and_keys menu From 57afef71b9df293e5effdb53caa77197e5d385f4 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:45:44 +0800 Subject: [PATCH 7/8] fix(ui): fail closed on shortcut screenshot capture --- .github/workflows/ui-review-screenshots.yml | 15 ++++++--------- nebula_app/src/display/keymap.rs | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml index ab1f62370..c889200c7 100644 --- a/.github/workflows/ui-review-screenshots.yml +++ b/.github/workflows/ui-review-screenshots.yml @@ -1,11 +1,10 @@ name: UI review screenshots on: - workflow_dispatch: push: branches: ["codex/platform-shortcut-labels"] - pull_request: - types: [opened, synchronize, reopened] + paths: + - nebula_app/src/display/keymap.rs permissions: contents: read @@ -241,7 +240,7 @@ jobs: focus_and_keys() { local keys="$1" - PID="$pid" KEYS="$keys" osascript <<'APPLESCRIPT' || true + PID="$pid" KEYS="$keys" osascript <<'APPLESCRIPT' set targetPid to (system attribute "PID") as integer set requestedKeys to system attribute "KEYS" tell application "System Events" @@ -262,7 +261,7 @@ jobs: click_offset() { local x="$1" local y="$2" - PID="$pid" X="$x" Y="$y" osascript <<'APPLESCRIPT' || true + PID="$pid" X="$x" Y="$y" osascript <<'APPLESCRIPT' set targetPid to (system attribute "PID") as integer set clickX to (system attribute "X") as integer set clickY to (system attribute "Y") as integer @@ -281,7 +280,7 @@ jobs: click_bottom() { local x="$1" local offset="$2" - PID="$pid" X="$x" OFFSET="$offset" osascript <<'APPLESCRIPT' || true + PID="$pid" X="$x" OFFSET="$offset" osascript <<'APPLESCRIPT' set targetPid to (system attribute "PID") as integer set clickX to (system attribute "X") as integer set bottomOffset to (system attribute "OFFSET") as integer @@ -410,7 +409,7 @@ jobs: publish-evidence: name: Publish UI evidence - if: always() && github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push' + if: success() && github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push' needs: native-ui runs-on: ubuntu-22.04 concurrency: @@ -424,13 +423,11 @@ jobs: persist-credentials: true - uses: actions/download-artifact@v4 - continue-on-error: true with: name: ui-review-windows path: ${{ runner.temp }}/ui-review-evidence/windows - uses: actions/download-artifact@v4 - continue-on-error: true with: name: ui-review-macos path: ${{ runner.temp }}/ui-review-evidence/macos diff --git a/nebula_app/src/display/keymap.rs b/nebula_app/src/display/keymap.rs index 67c0aacd8..140184cc1 100644 --- a/nebula_app/src/display/keymap.rs +++ b/nebula_app/src/display/keymap.rs @@ -914,7 +914,7 @@ mod tests { } } - /// 超级键键帽随平台渲染:macOS 是 ⌘,其它平台是 Win。 + /// Super 键标签按平台显示:macOS 是 Command,Windows 是 Win,Linux 是 Super。 #[test] fn super_modifier_renders_per_platform() { let expected = if cfg!(target_os = "macos") { From b41c3315884dea6bf6f01fc51171a30170140960 Mon Sep 17 00:00:00 2001 From: GeekMr <140129782+WilliamWang1721@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:59:32 +0800 Subject: [PATCH 8/8] chore: remove one-off UI screenshot workflow --- .github/workflows/ui-review-screenshots.yml | 547 -------------------- 1 file changed, 547 deletions(-) delete mode 100644 .github/workflows/ui-review-screenshots.yml diff --git a/.github/workflows/ui-review-screenshots.yml b/.github/workflows/ui-review-screenshots.yml deleted file mode 100644 index c889200c7..000000000 --- a/.github/workflows/ui-review-screenshots.yml +++ /dev/null @@ -1,547 +0,0 @@ -name: UI review screenshots - -on: - push: - branches: ["codex/platform-shortcut-labels"] - paths: - - nebula_app/src/display/keymap.rs - -permissions: - contents: read - -concurrency: - group: ui-review-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: "1" - RUST_TOOLCHAIN: "1.97.1" - CARGO_TARGET_DIR: target/ui-review - UI_REVIEW_PR: "287" - UI_REVIEW_SCENARIO: "shortcuts" - UI_REVIEW_TARGET: "Platform-specific shortcut labels in Settings" - -jobs: - native-ui: - name: ${{ matrix.name }} - strategy: - fail-fast: false - matrix: - include: - - name: Windows UI review - runner: windows-2022 - platform: windows - - name: macOS UI review - runner: macos-26 - platform: macos - runs-on: ${{ matrix.runner }} - timeout-minutes: 75 - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - - - uses: Swatinem/rust-cache@v2 - with: - cache-on-failure: true - workspaces: ". -> target/ui-review" - - - name: Prepare Windows console runtime - if: matrix.platform == 'windows' - shell: pwsh - run: ./scripts/prepare-windows-runtime.ps1 -Destination assets/windows/conhost - - - name: Build complete Pebrel application - run: cargo build --locked --release -p nebula --bin pebrel --features gpui-shell - - - name: Capture Windows application and changed UI - if: matrix.platform == 'windows' - shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $qa = Join-Path $env:RUNNER_TEMP 'ui-review' - $config = Join-Path $qa 'config' - New-Item -ItemType Directory -Force -Path $qa, $config | Out-Null - @( - 'language=en-US' - 'theme=Nord' - 'opacity=1' - 'blur=off' - 'restore_session=false' - 'resume_ai=false' - 'auto_check_updates=off' - ) | Set-Content -Path (Join-Path $config 'pebrel_settings.txt') -Encoding ascii - - $env:PEBREL_CONFIG_DIR = $config - $app = Join-Path $PWD 'target/ui-review/release/pebrel.exe' - $stdout = Join-Path $qa 'pebrel.stdout.log' - $stderr = Join-Path $qa 'pebrel.stderr.log' - $p = Start-Process $app -ArgumentList '--working-directory', $PWD -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr - Start-Sleep -Seconds 7 - - function Capture([string]$name) { - $shot = Join-Path $qa $name - for ($i = 0; $i -lt 8; $i++) { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Shot $shot - if (Test-Path $shot) { return } - Start-Sleep -Seconds 1 - } - throw "Could not capture Pebrel window: $name" - } - - Capture '00-full-app.png' - - switch ($env:UI_REVIEW_SCENARIO) { - 'settings' { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Start-Sleep -Seconds 1 - Capture '01-appearance-control-sizing.png' - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,246' - Capture '02-terminal-control-sizing.png' - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,76' - Capture '03-workspace-toolbar-sizing.png' - } - 'shortcuts' { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Start-Sleep -Seconds 1 - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,360' - Capture '01-key-bindings-platform-labels.png' - } - 'dialog' { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Start-Sleep -Seconds 1 - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '120,705' - Capture '01-confirm-dialog-shortcuts.png' - } - 'sidebar' { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Start-Sleep -Seconds 1 - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Click '90,323' - Capture '01-interaction-panel-resize.png' - } - 'ssh-copy' { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -TypeText '^,' - Capture '01-ssh-settings-entry.png' - } - 'ssh-ports' { - Capture '01-ssh-ports-regression.png' - } - default { - Capture '01-platform-regression.png' - } - } - - @( - "scenario=$env:UI_REVIEW_SCENARIO" - "target=$env:UI_REVIEW_TARGET" - "platform=windows" - "pid=$($p.Id)" - ) | Set-Content -Path (Join-Path $qa 'review-target.txt') -Encoding utf8 - - if (Get-Process -Id $p.Id -ErrorAction SilentlyContinue) { - & ./scripts/ui_probe.ps1 -ProcId $p.Id -Kill - } - - - name: Capture SSH copy menu on Windows - if: matrix.platform == 'windows' && env.UI_REVIEW_SCENARIO == 'ssh-copy' - shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $qa = Join-Path $env:RUNNER_TEMP 'ui-review' - $probe = Join-Path $qa 'ssh-copy-probe' - $config = Join-Path $probe 'config' - New-Item -ItemType Directory -Force -Path $probe, $config | Out-Null - @( - 'language=en-US' - 'theme=Nord' - 'opacity=1' - 'blur=off' - 'restore_session=false' - 'resume_ai=false' - 'auto_check_updates=off' - ) | Set-Content -Path (Join-Path $config 'pebrel_settings.txt') -Encoding ascii - - $env:PEBREL_CONFIG_DIR = $config - $env:PEBREL_SSH_COPY_QA_DIR = $probe - $stdout = Join-Path $probe 'probe.stdout.log' - $stderr = Join-Path $probe 'probe.stderr.log' - $args = @('test', '--locked', '-p', 'nebula', '--bin', 'pebrel', '--features', 'gpui-test-support', 'native_ssh_copy_context_menu_preview', '--', '--ignored', '--nocapture') - $cargo = Start-Process cargo -ArgumentList $args -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr - - $ready = Join-Path $probe 'menu-ready.json' - $deadline = (Get-Date).AddMinutes(20) - while (-not (Test-Path $ready)) { - if ($cargo.HasExited) { - Get-Content $stdout -ErrorAction SilentlyContinue - Get-Content $stderr -ErrorAction SilentlyContinue - throw 'SSH screenshot probe exited before opening its native window.' - } - if ((Get-Date) -gt $deadline) { throw 'Timed out waiting for SSH screenshot probe.' } - Start-Sleep -Milliseconds 500 - } - - $probePid = (Get-Content $ready -Raw | ConvertFrom-Json).pid - ./scripts/ui_probe.ps1 -ProcId $probePid -RightClick '520,220' -Shot (Join-Path $qa '02-ssh-context-menu.png') - ./scripts/ui_probe.ps1 -ProcId $probePid -Click '550,294' - Start-Sleep -Milliseconds 700 - ./scripts/ui_probe.ps1 -ProcId $probePid -Shot (Join-Path $qa '03-ssh-copy-editor.png') - New-Item -ItemType File -Force -Path (Join-Path $probe 'capture-complete') | Out-Null - $cargo.WaitForExit() - if ($cargo.ExitCode -ne 0) { - Get-Content $stdout -ErrorAction SilentlyContinue - Get-Content $stderr -ErrorAction SilentlyContinue - throw "SSH screenshot probe failed with exit code $($cargo.ExitCode)." - } - - - name: Capture macOS application and changed UI - if: matrix.platform == 'macos' - shell: bash - run: | - set -euo pipefail - qa="$RUNNER_TEMP/ui-review" - config="$qa/config" - mkdir -p "$qa" "$config" - cat >"$config/pebrel_settings.txt" <<'EOF' - language=en-US - theme=Nord - opacity=1 - blur=off - restore_session=false - resume_ai=false - auto_check_updates=off - EOF - - export PEBREL_CONFIG_DIR="$config" - app="$PWD/target/ui-review/release/pebrel" - "$app" --working-directory "$PWD" >"$qa/pebrel.stdout.log" 2>"$qa/pebrel.stderr.log" & - pid=$! - - cleanup() { - kill "$pid" >/dev/null 2>&1 || true - wait "$pid" >/dev/null 2>&1 || true - } - trap cleanup EXIT - - for _ in $(seq 1 20); do - if ! kill -0 "$pid" >/dev/null 2>&1; then - cat "$qa/pebrel.stderr.log" >&2 || true - exit 1 - fi - sleep 0.5 - done - - screencapture -x "$qa/00-full-app.png" - - focus_and_keys() { - local keys="$1" - PID="$pid" KEYS="$keys" osascript <<'APPLESCRIPT' - set targetPid to (system attribute "PID") as integer - set requestedKeys to system attribute "KEYS" - tell application "System Events" - set appProc to first application process whose unix id is targetPid - set frontmost of appProc to true - delay 0.4 - if requestedKeys is "settings" then - keystroke "," using command down - else if requestedKeys is "menu" then - click menu bar item 2 of menu bar 1 of appProc - else if requestedKeys is "quit" then - keystroke "q" using command down - end if - end tell - APPLESCRIPT - } - - click_offset() { - local x="$1" - local y="$2" - PID="$pid" X="$x" Y="$y" osascript <<'APPLESCRIPT' - set targetPid to (system attribute "PID") as integer - set clickX to (system attribute "X") as integer - set clickY to (system attribute "Y") as integer - tell application "System Events" - set appProc to first application process whose unix id is targetPid - set frontmost of appProc to true - tell window 1 of appProc - set {wx, wy} to position - end tell - click at {wx + clickX, wy + clickY} - end tell - APPLESCRIPT - sleep 1 - } - - click_bottom() { - local x="$1" - local offset="$2" - PID="$pid" X="$x" OFFSET="$offset" osascript <<'APPLESCRIPT' - set targetPid to (system attribute "PID") as integer - set clickX to (system attribute "X") as integer - set bottomOffset to (system attribute "OFFSET") as integer - tell application "System Events" - set appProc to first application process whose unix id is targetPid - set frontmost of appProc to true - tell window 1 of appProc - set {wx, wy} to position - set {ww, wh} to size - end tell - click at {wx + clickX, wy + wh - bottomOffset} - end tell - APPLESCRIPT - sleep 1 - } - - case "$UI_REVIEW_SCENARIO" in - settings) - focus_and_keys settings - sleep 2 - screencapture -x "$qa/01-appearance-control-sizing.png" - click_offset 90 249 - screencapture -x "$qa/02-terminal-control-sizing.png" - click_offset 90 81 - screencapture -x "$qa/03-workspace-toolbar-sizing.png" - ;; - shortcuts) - focus_and_keys settings - sleep 2 - click_offset 90 363 - screencapture -x "$qa/01-key-bindings-platform-labels.png" - ;; - dialog) - focus_and_keys settings - sleep 2 - click_bottom 120 24 - screencapture -x "$qa/01-confirm-dialog-shortcuts.png" - ;; - sidebar) - focus_and_keys settings - sleep 2 - click_offset 90 326 - screencapture -x "$qa/01-interaction-panel-resize.png" - ;; - menu) - focus_and_keys menu - sleep 1 - screencapture -x "$qa/01-native-menu.png" - ;; - portable) - cleanup - trap - EXIT - bundle="$RUNNER_TEMP/Pebrel UI Review.app" - mkdir -p "$bundle/Contents/MacOS" - cp "$app" "$bundle/Contents/MacOS/pebrel" - chmod +x "$bundle/Contents/MacOS/pebrel" - cat >"$bundle/Contents/Info.plist" <<'PLIST' - - - - CFBundleExecutablepebrel - CFBundleIdentifierio.github.kuddev.pebrel.ui-review - CFBundleNamePebrel UI Review - CFBundlePackageTypeAPPL - - PLIST - unset PEBREL_CONFIG_DIR NEBULA_CONFIG_DIR PEBREL_CONFIG_FILE NEBULA_CONFIG_FILE PEBREL_GPUI_CONFIG NEBULA_GPUI_CONFIG - export HOME="$qa/home" - mkdir -p "$HOME" - open -n "$bundle" - sleep 4 - screencapture -x "$qa/01-portable-startup-dialog.png" - pkill -f "$bundle/Contents/MacOS/pebrel" || true - ;; - ssh-copy) - focus_and_keys settings - sleep 2 - screencapture -x "$qa/01-ssh-settings-entry.png" - ;; - ssh-ports) - screencapture -x "$qa/01-ssh-ports-regression.png" - ;; - notification) - screencapture -x "$qa/01-foreground-notification-regression.png" - ;; - *) - screencapture -x "$qa/01-platform-regression.png" - ;; - esac - - { - echo "scenario=$UI_REVIEW_SCENARIO" - echo "target=$UI_REVIEW_TARGET" - echo "platform=macos" - sw_vers - } >"$qa/review-target.txt" - - - name: Upload reviewer screenshots - if: always() - uses: actions/upload-artifact@v4 - with: - name: ui-review-${{ matrix.platform }} - path: | - ${{ runner.temp }}/ui-review/*.png - ${{ runner.temp }}/ui-review/*.txt - ${{ runner.temp }}/ui-review/*.log - ${{ runner.temp }}/ui-review/ssh-copy-probe/*.json - ${{ runner.temp }}/ui-review/ssh-copy-probe/*.log - if-no-files-found: error - retention-days: 14 - compression-level: 0 - - - name: Add screenshot pointers to job summary - if: always() - shell: bash - run: | - { - echo "## Native UI review" - echo - echo "- Platform: ${{ matrix.platform }}" - echo "- Scenario: $UI_REVIEW_SCENARIO" - echo "- Changed UI target: $UI_REVIEW_TARGET" - echo "- Download the ui-review-${{ matrix.platform }} artifact to inspect the PNG evidence." - } >>"$GITHUB_STEP_SUMMARY" - - - publish-evidence: - name: Publish UI evidence - if: success() && github.repository == 'WilliamWang1721/pebrel' && github.event_name == 'push' - needs: native-ui - runs-on: ubuntu-22.04 - concurrency: - group: ui-review-evidence-publish - cancel-in-progress: false - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: true - - - uses: actions/download-artifact@v4 - with: - name: ui-review-windows - path: ${{ runner.temp }}/ui-review-evidence/windows - - - uses: actions/download-artifact@v4 - with: - name: ui-review-macos - path: ${{ runner.temp }}/ui-review-evidence/macos - - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Polish screenshots for PR presentation - shell: bash - run: | - set -euo pipefail - python3 -m pip install --disable-pip-version-check --quiet "Pillow==11.3.0" - python3 - <<'PY' - import os - from pathlib import Path - from PIL import Image, ImageDraw, ImageFilter, ImageStat - - source = Path(os.environ["RUNNER_TEMP"]) / "ui-review-evidence" - output = Path(os.environ["RUNNER_TEMP"]) / "ui-review-polished" - scale = 2 - radius = 24 * scale - padding = 36 * scale - shadow_pad = 18 * scale - - for platform in ("windows", "macos"): - src_dir = source / platform - dst_dir = output / platform - dst_dir.mkdir(parents=True, exist_ok=True) - if not src_dir.exists(): - continue - - for path in sorted(src_dir.glob("*.png")): - image = Image.open(path).convert("RGB") - image = image.resize( - (image.width * scale, image.height * scale), - Image.Resampling.LANCZOS, - ) - image = image.filter(ImageFilter.UnsharpMask(radius=0.7, percent=115, threshold=2)) - - stat = ImageStat.Stat(image.resize((1, 1))) - luminance = sum(stat.mean) / 3 - background = (243, 245, 248) if luminance > 120 else (18, 20, 24) - border = (205, 209, 216) if luminance > 120 else (65, 69, 78) - - frame_w = image.width + padding * 2 - frame_h = image.height + padding * 2 - canvas = Image.new("RGBA", (frame_w + shadow_pad * 2, frame_h + shadow_pad * 2), background + (255,)) - - shadow = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) - shadow_draw = ImageDraw.Draw(shadow) - x0 = shadow_pad + padding - y0 = shadow_pad + padding - x1 = x0 + image.width - y1 = y0 + image.height - shadow_draw.rounded_rectangle( - (x0 + 4 * scale, y0 + 7 * scale, x1 + 4 * scale, y1 + 7 * scale), - radius=radius, - fill=(0, 0, 0, 92), - ) - shadow = shadow.filter(ImageFilter.GaussianBlur(12 * scale)) - canvas.alpha_composite(shadow) - - mask = Image.new("L", image.size, 0) - ImageDraw.Draw(mask).rounded_rectangle( - (0, 0, image.width - 1, image.height - 1), - radius=radius, - fill=255, - ) - framed = Image.new("RGBA", image.size, (0, 0, 0, 0)) - framed.paste(image, (0, 0), mask) - canvas.alpha_composite(framed, (x0, y0)) - - draw = ImageDraw.Draw(canvas) - draw.rounded_rectangle( - (x0, y0, x1 - 1, y1 - 1), - radius=radius, - outline=border + (255,), - width=2 * scale, - ) - - out = dst_dir / f"{path.stem}-review.png" - canvas.convert("RGB").save(out, "PNG", optimize=True) - PY - - - name: Publish screenshots outside the PR diff - shell: bash - run: | - set -euo pipefail - source_dir="$RUNNER_TEMP/ui-review-evidence" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch origin ui-review-evidence || true - if git show-ref --verify --quiet refs/remotes/origin/ui-review-evidence; then - git switch -C ui-review-evidence origin/ui-review-evidence - else - git switch --orphan ui-review-evidence - git rm -rf . >/dev/null 2>&1 || true - fi - - polished_dir="$RUNNER_TEMP/ui-review-polished" - root="ui-review/pr-$UI_REVIEW_PR" - rm -rf "$root" - mkdir -p "$root/windows" "$root/macos" "$root/raw/windows" "$root/raw/macos" - find "$polished_dir/windows" -maxdepth 1 -type f -name '*-review.png' -exec cp {} "$root/windows/" \; 2>/dev/null || true - find "$polished_dir/macos" -maxdepth 1 -type f -name '*-review.png' -exec cp {} "$root/macos/" \; 2>/dev/null || true - find "$source_dir/windows" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/raw/windows/" \; 2>/dev/null || true - find "$source_dir/macos" -maxdepth 1 -type f -name '*.png' -exec cp {} "$root/raw/macos/" \; 2>/dev/null || true - printf '%s\n' "$GITHUB_SHA" >"$root/head-sha.txt" - printf '%s\n' "$UI_REVIEW_TARGET" >"$root/target.txt" - - git add "$root" - if git diff --cached --quiet; then - exit 0 - fi - git commit -m "docs(ui): publish PR #$UI_REVIEW_PR screenshots" - git push origin HEAD:ui-review-evidence