Skip to content

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

Open
amondnet wants to merge 2 commits into
mainfrom
fix/print-popup-shim
Open

fix(studio): make 파일 → 인쇄 work in the desktop app#23
amondnet wants to merge 2 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.jswindow.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. Now handles both blank and same-origin popups to stay compatible with upstream v0.8.x.

  • 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; 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; throws if injection fails.
    • Updated apps/studio-host/README.md to document the shim and why it exists.

Written for commit 1ac16f7. 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 Outdated
Comment on lines +83 to +88
try {
win.document.addEventListener("keydown", onKeydown, true);
} catch {
// A cross-origin document would throw; blank popups never are, so this
// is only belt-and-braces.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

[MEDIUM] Attach keydown listener to the window object instead of the document

Problem: The caller of window.open() typically writes content into the new document using document.write() or document.open(). Doing so clears the document and discards any event listeners attached directly to win.document. As a result, the Escape key listener on the iframe's document will be lost, and the user won't be able to dismiss the print preview using the Escape key.

Rationale: Event listeners attached to the window object (win) persist across document.write() and document.open() calls, whereas listeners on win.document are destroyed.

Suggestion: Attach the keydown event listener to win instead of win.document.

Suggested change
try {
win.document.addEventListener("keydown", onKeydown, true);
} catch {
// A cross-origin document would throw; blank popups never are, so this
// is only belt-and-braces.
}
try {
win.addEventListener("keydown", onKeydown, true);
} catch {
// A cross-origin document would throw; blank popups never are, so this
// is only belt-and-braces.
}

Comment thread apps/studio-host/shims/openhwp-popup.js Outdated
Comment on lines +90 to +97
// Shadow close() on the pseudo-window. Window.prototype.close() would be a
// no-op for an iframe, leaving the overlay stuck over the editor.
try {
win.close = dismiss;
} catch {
// If the assignment is refused the overlay is still dismissable with
// Escape, so this stays non-fatal.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

[MEDIUM] Use Object.defineProperty to shadow win.close

Problem: Direct assignment to win.close in strict mode will throw a TypeError in environments where window.close is a read-only property on the Window prototype, causing the override to fail and leaving the print preview overlay stuck when the user clicks the close button.

Rationale: In strict mode, assigning to a read-only property throws an error. Using Object.defineProperty safely shadows the property on the instance.

Suggestion: Use Object.defineProperty to define the close method on the iframe's window object.

    // Shadow close() on the pseudo-window. Window.prototype.close() would be a
    // no-op for an iframe, leaving the overlay stuck over the editor.
    try {
      Object.defineProperty(win, "close", {
        value: dismiss,
        writable: true,
        configurable: true,
      });
    } catch {
      // If the assignment is refused the overlay is still dismissable with
      // Escape, so this stays non-fatal.
    }

Comment thread scripts/build-studio.ts
await Deno.copyFile(SHIM_SRC, `${OUT}/${SHIM_NAME}`);
const htmlPath = `${OUT}/index.html`;
const htmlOrig = await Deno.readTextFile(htmlPath);
const htmlPatched = htmlOrig.replace("</head>", ` ${SHIM_TAG}\n</head>`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

[MEDIUM] Use case-insensitive regex for HTML head injection

Problem: If the upstream index.html uses a different casing for the closing head tag (e.g., </HEAD> or </head with extra whitespace), the string replacement will fail, causing the build script to throw an error and halt.

Rationale: Using a case-insensitive regular expression makes the build process more resilient to minor upstream formatting changes.

Suggestion: Use a case-insensitive regular expression /(<\/head>)/i for the replacement.

const htmlPatched = htmlOrig.replace(/(<\/head>)/i, "  " + SHIM_TAG + "\n$1");

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an in-app print-preview substitute for unsupported CEF popup windows.

  • Injects a host-side window.open() shim into the built Studio bundle.
  • Represents blank and same-origin popup windows with a full-window iframe façade supporting document access, printing, events, and closing.
  • Preserves native handling for cross-origin popup requests.
  • Documents the desktop-specific integration and fails the Studio build if shim injection cannot be completed.

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 the iframe-backed popup façade used to host and dismiss Studio print previews in CEF.
scripts/build-studio.ts Copies the popup shim into the output and injects it before the deferred Studio bundle, with an explicit failure guard.
apps/studio-host/README.md Documents the desktop popup limitation and additive host shim.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Studio calls window.open] --> B{Blank or same-origin URL?}
  B -->|No| C[Delegate to native window.open]
  B -->|Yes| D[Create full-window iframe]
  D --> E[Return popup façade]
  E --> F[Studio builds print preview]
  F --> G[Print iframe document]
  E --> H[Close or Escape]
  H --> I[Remove iframe overlay]
Loading

Reviews (2): Last reviewed commit: "fix(studio): shim same-origin popups too..." | 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 falsetrue
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).

@sonarqubecloud

Copy link
Copy Markdown

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