Skip to content

fix(studio): make 파일 → 인쇄 work in the desktop app - #23

Merged
amondnet merged 5 commits into
mainfrom
fix/print-popup-shim
Jul 29, 2026
Merged

amondnet merged 5 commits into
mainfrom
fix/print-popup-shim

Conversation

@amondnet

@amondnet amondnet commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

The bug

파일 → 인쇄 shows "팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요." and nothing prints.

Cause

Upstream's file:print builds its preview into a popup window
(rhwp-studio/src/command/commands/file.ts:493):

const printWin = window.open('', '_blank');
if (!printWin) {
  alert('팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.');
  return;
}

deno desktop's CEF backend never creates popup windows. Probed against a real CEF build:

probe result
window.open('', '_blank') — no gesture null (BLOCKED)
window.open('', '_blank') — from a click null (BLOCKED)

So it is not a popup blocker that a user gesture can satisfy, and Deno.BrowserWindow
exposes no popup/new-window hook for the shell to opt in with (its events are close,
navigation, resize, menuclick).

Printing itself works. With a print call in flight the app's window list contains a real
system dialog and PrintCore.framework is loaded:

windows: Print, print probe
… /System/Library/Frameworks/…/PrintCore.framework/…

print() blocks only because that dialog is modal. The single missing piece is something for
window.open() to return.

Fix

apps/studio-host/shims/openhwp-popup.js — window.open() for a blank target returns a
same-origin about:blank iframe overlaid on the window, which provides exactly what the caller
uses: .document to build into, .print() to print just that document, .close() to dismiss
(shadowed, since Window.close() is a no-op for an iframe and would strand the overlay).
Popups with a real URL keep the host's behaviour, so link targets never change silently. Escape
also dismisses, since the overlay has no window chrome.

Upstream's print stylesheet is already written for this — @media screen styles the document as
a centred preview with a fixed toolbar, @media print hides the toolbar — so the overlay
reproduces the intended preview rather than approximating it.

Additive, not an override. The studio is still built unmodified and
config/rhwp-studio-overrides.json stays empty. scripts/build-studio.ts (step 7) copies the
shim into dist/ and injects a classic <script> tag, following upstream's own theme-init.js
pattern so it runs before the deferred module bundle.

Verification

Run in a real CEF build serving the actual studio bundle:

check result
window.open('', '_blank') Window (was null)
overlay in DOM iframe.openhwp-popup present
.document writable yes
typeof .print function
.close() overlay removed
overlay geometry 1100×772, exactly filling the window
toolbar visible, 36px, both 인쇄/닫기 buttons
pages 794×1123 (A4 @96dpi), white, SVG painted
toolbar excluded from print @media print { .print-bar { display: none } }
injection guard, anchor missing throws, deno exits 1

deno fmt --check, deno lint, deno check all clean.

Note

The upstream popup approach is fragile in browsers too (a popup outside a user gesture is
blocked there as well), so this is worth reporting upstream — but the shim fixes the desktop app
without waiting on it.


Summary by cubic

Fixes 파일 → 인쇄 in the desktop app by shimming window.open so the print preview renders in-app and printing works without popups. Supports blank and same-origin popups for v0.7.x–v0.8.x compatibility.

  • Bug Fixes
    • Added apps/studio-host/shims/openhwp-popup.js: shims window.open for blank and same-origin URLs, returning a full-window same-origin iframe overlay with a façade that exposes .document, .print(), .close(), and forwards load in a way that survives navigation; cross-origin popups are unchanged; Esc closes.
    • Updated scripts/build-studio.ts to copy the shim and inject a non-module <script> before the deferred bundle; the </head> anchor match is case-insensitive and tolerates trailing whitespace; still throws if injection fails.
    • Updated apps/studio-host/README.md to document the shim and why it exists.

Written for commit 8d0673c. Summary will update on new commits.

Printing reported "팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요."
and nothing happened.

Upstream's file:print builds its preview into a popup:

    const printWin = window.open('', '_blank');
    if (!printWin) { alert('팝업이 차단되었습니다. ...'); return; }

but deno desktop's CEF backend never creates popup windows. Probed against
the real host: window.open('', '_blank') returns null both with and without
a user gesture, so it is not a popup blocker a gesture can satisfy, and
Deno.BrowserWindow exposes no popup hook for the shell to opt in with.

Printing itself is fine — the system print dialog opens and PrintCore is
loaded — so the only missing piece is something for window.open() to return.

Add apps/studio-host/shims/openhwp-popup.js: window.open() for a blank
target now returns a same-origin about:blank iframe overlaid on the window,
which supplies everything the caller uses — .document to build into,
.print() to print just that document, .close() to dismiss. Popups with a
real URL keep the host's behaviour so link targets never change silently.

Upstream's print stylesheet already suits this: @media screen styles the
document as a centred preview with a fixed toolbar, and @media print hides
the toolbar. So the overlay reproduces the intended preview rather than
approximating it.

This is additive — the studio is still built unmodified and
config/rhwp-studio-overrides.json stays empty. build-studio.ts copies the
shim into dist/ and injects the script tag, throwing if </head> is absent
rather than silently shipping a bundle that shows the popup alert again.

Verified in a real CEF build serving the actual studio bundle:
window.open -> Window, overlay present, document writable, print is a
function, close() removes the overlay; and the preview renders — overlay
1100x772 filling the window, toolbar 36px with both buttons, A4 pages at
794x1123 on white with the SVG painted. The injection guard was confirmed
to throw (deno exits 1) when the anchor is missing.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a window.open() shim (openhwp-popup.js) for the CEF host to support printing by overlaying an iframe, and updates the build script to inject this shim into the built index.html. The review feedback recommends attaching the keydown listener to the iframe's window object instead of its document to prevent it from being cleared during document writes, using Object.defineProperty to safely shadow win.close without throwing a TypeError in strict mode, and utilizing a case-insensitive regular expression for the HTML head injection to increase build resilience.

Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
Comment thread scripts/build-studio.ts Resolved
@greptile-apps

greptile-apps Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Greptile Summary

Adds an in-app popup substitute for desktop printing.

  • Introduces a same-origin iframe overlay that implements the popup operations used by the studio’s print flow.
  • Copies and injects the shim into the generated studio bundle before the deferred application code.
  • Documents the desktop-specific shim and build integration.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/studio-host/shims/openhwp-popup.js Adds a same-origin iframe-backed Window facade for popup-dependent print previews while preserving native handling for cross-origin URLs and navigation targets.
scripts/build-studio.ts Copies the popup shim into the built studio and injects its classic script tag, failing the build when the HTML anchor is absent.
apps/studio-host/README.md Documents why the desktop host requires the additive popup shim and where it enters the build.

Sequence Diagram

sequenceDiagram
    participant Studio
    participant Shim as window.open shim
    participant Frame as Preview iframe
    participant System as System print dialog
    Studio->>Shim: window.open(blank or same-origin URL)
    Shim->>Frame: Create full-window iframe overlay
    Shim-->>Studio: Return Window-like facade
    Studio->>Frame: Build or load print preview
    Studio->>Frame: print()
    Frame->>System: Open print dialog
    Studio->>Shim: close()
    Shim->>Frame: Remove overlay
Loading

Reviews (5): Last reviewed commit: "chore: apply AI code review suggestions" | Re-trigger Greptile

Upstream v0.8.x reworked printing. file:print no longer opens a blank
popup and builds into it — it opens print.html:

    const previewWindow = hostWindow.open(surfaceUrl, '_blank');
    if (!previewWindow) return Promise.reject(new PrintPreviewBlockedError());

That is a real same-origin URL, so a blank-only shim would pass it
straight through to the host, get null back, and silently restore the
exact bug this fixes the moment the pin moves to 0.8.x.

Handle same-origin URLs as well, still passing cross-origin popups to the
host so external link targets never change silently.

A navigated frame needs more than the previous approach gave it. Both
shortcuts that worked for a blank popup break once the frame loads a URL:
an own `close` property set on the window is discarded along with that
window, and a `load` listener registered on it never fires, because the
document that finishes loading belongs to the *next* window. So return a
façade instead — reads forward to whichever window the frame currently
holds, and `load` is served by the frame element, which survives
navigation.

Verified in a real CEF build against a faithful replica of v0.8.2's
createPrintPreviewSurface: the surface resolves (the load event does reach
the caller through the façade), document is the loaded page, origin and
href match, print is a function, fonts.ready + requestAnimationFrame work
(the waitForPrintSurfaceReady contract), the closed flag transitions, and
close() removes the overlay. The v0.7.19 blank path still passes
unchanged, the 닫기 button dismisses through the façade, and a
cross-origin popup still returns null with no overlay created.
@amondnet

Copy link
Copy Markdown
Contributor Author

Widened to same-origin popups (1ac16f7)

Prompted by upstream v0.8.2. That release is a hotfix for the browser extension's print (print.html was missing from the extension build) — it does not fix this bug. But v0.8.x did rework printing, and the rework matters here:

command surface popup?
file:print-to-pdf (PDF로 저장…, new in 0.8) createPrintSurface() → hidden iframe + surface.window.print() no
file:print (인쇄) createPrintPreviewSurface() → hostWindow.open(surfaceUrl, '_blank') yes

At v0.8.x that popup targets print.html — a real same-origin URL, not a blank one. The original blank-only shim would have passed it through to the host, got null, and silently restored this exact bug the moment the pin moved. Fixed now rather than left as a trap for whoever does the bump.

Why a façade was needed

Both shortcuts that worked for a blank popup break once the frame navigates:

  • an own close property set on the window is discarded along with that window;
  • a load listener registered on it never fires, because the document that finishes loading belongs to the next window.

So window.open now returns a façade: reads forward to whichever window the frame currently holds, and load is served by the frame element, which survives navigation.

Verification

Run in a real CEF build, against a faithful replica of v0.8.2's createPrintPreviewSurface:

check result
surface resolves (load reaches the caller) yes
surface.document is the loaded page title + marker element correct
location.origin / location.href both match
typeof surface.window.print function
document.fonts.ready + requestAnimationFrame ok (waitForPrintSurfaceReady contract)
closed flag false → true
close() overlay removed

And the existing behaviour is unchanged:

check result
v0.7.19 blank path doc writable, print function, overlay present, close removes it
preview rendering overlay 1100×772 filling the window, toolbar 36px / 2 buttons, A4 pages 794×1123 on white
닫기 button through the façade dismisses the overlay
cross-origin popup null — passed to the host, no overlay created

deno fmt --check, deno lint clean.

Moving to v0.8.2 is tracked separately, per the plan — it needs the matching @rhwp/core@0.8.x vendor bump and print.html in dist/, and upstream's own notes list two open E2E failures (#3450, #3412).

@amondnet amondnet self-assigned this Jul 29, 2026
Applied: match the </head> anchor case-insensitively and tolerate trailing
whitespace, since HTML end tags allow both. Verified against </head>,
</HEAD>, "</head >" and "</Head\t>" — all inject — while a document with no
end tag still trips the guard and fails the build.

Not applied: moving the frame's Escape listener from the document to the
window. The premise does not hold in CEF — measured, neither survives a
document.open()/write(): both listener counts stay flat afterwards. What
already handles it is the re-bind on the frame's load event, which
document.close() fires. Comment updated to say so, since the old wording
credited only navigation.
@amondnet

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a window.open() shim (openhwp-popup.js) to enable printing functionality in the CEF desktop host, which does not natively support popup windows. The shim overlays a same-origin iframe on the window and returns a proxy facade. The build script is updated to inject this shim into the built index.html. Feedback suggests enhancing the proxy facade to prevent bypasses via self-referential properties like window, self, or frames, and hardening the URL resolver to prevent loading non-standard protocols (e.g., javascript:) when the origin is null.

Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
@amondnet

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a window.open() shim (openhwp-popup.js) for the CEF-based desktop host to support the print preview feature via a same-origin iframe overlay, along with build script integration and documentation updates. The review feedback suggests improving the shim's robustness by supporting direct onload property assignments on the proxy facade, restricting interception to only blank or empty targets (preventing incorrect interception of _self or _parent), and refactoring the shim function to comply with the 50-line limit.

Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
@sonarqubecloud

Copy link
Copy Markdown

@amondnet

Copy link
Copy Markdown
Contributor Author

/gemini review

@amondnet
amondnet merged commit cdf4059 into main Jul 29, 2026
12 checks passed
@amondnet
amondnet deleted the fix/print-popup-shim branch July 29, 2026 04:20

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a window.open() shim (openhwp-popup.js) for the CEF-based desktop host to enable printing functionality via an iframe overlay, and updates the build script to inject this shim into the built index.html. The review feedback recommends two improvements to the shim: dynamically retrieving the contentWindow in the event listener proxy to prevent stale references after navigation, and respecting event.defaultPrevented in the Escape key listener to avoid conflicts with internal elements.

Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
Comment thread apps/studio-host/shims/openhwp-popup.js Resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant