Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions WEBKIT_ENTITLEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# WebKit Media Playback Entitlements

## Background

The transparent browser feature requires WebKit to handle media playback. However, certain system-level entitlements are restricted and cannot be used in regular development builds.

## Restricted Entitlements

The following entitlements are **system-level privileges** that require special approval from Apple:

- `com.apple.runningboard.assertions.webkit` - WebKit process assertion control
- `com.apple.multitasking.systemappassertions` - System-wide multitasking assertions

## Impact on Development Builds

### What Works
✅ Basic web browsing with transparency
✅ Static content rendering
✅ JavaScript execution
✅ User interaction
✅ Transparency slider control

### Known Limitations
⚠️ **Media playback warnings**: You'll see console warnings like:
```
ProcessAssertion::acquireSync Failed to acquire RBS assertion 'WebKit Media Playback'
```

These warnings are **cosmetic** and don't prevent media playback. WebKit falls back to a restricted mode that still allows:
- Video/audio playback (with some restrictions)
- Basic media controls
- Streaming content

### What Doesn't Work Without Entitlements
❌ Background media playback (when app is hidden)
❌ System-wide media control integration
❌ Picture-in-picture mode
❌ Advanced power management for media

## Solutions

### For Development
The current configuration works for development and testing. The warnings can be ignored.

### For Production Release
To fully enable media playback features, you would need to:

1. Apply for special entitlements from Apple
2. Provide justification for why your terminal emulator needs these privileges
3. Go through Apple's review process
4. Use the approved entitlements in your production build

## Files

- `iTerm2.entitlements` - Standard entitlements for regular builds
- `iTerm2-Development.entitlements` - Development-specific configuration (currently same as standard)
- `iTermFileProviderNightly.entitlements` - Nightly build configuration

## Code Changes

The JavaScript transparency code has been updated to handle edge cases:

```javascript
// Check if setProperty exists before using it (fixes SVG and other special elements)
if (!isMedia && el.style && typeof el.style.setProperty === 'function') {
el.style.setProperty('background-color', bgColor, 'important');
el.style.setProperty('background-image', 'none', 'important');
}
```

This prevents errors on special elements (SVG, custom components) that don't support standard CSS manipulation.

## Testing

The browser should work correctly for:
- YouTube videos (with warnings in console)
- Other video streaming sites
- Audio playback
- Transparency adjustments during playback

## References

- [Apple Entitlements Documentation](https://developer.apple.com/documentation/bundleresources/entitlements)
- [WebKit Process Model](https://webkit.org/blog/7134/webassembly/)
- iTerm2 issue tracker for related discussions
26 changes: 26 additions & 0 deletions iTerm2-Development.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)iTerm</string>
</array>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
</dict>
</plist>
173 changes: 173 additions & 0 deletions sources/Browser/Core/iTermBrowserManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class iTermBrowserManager: NSObject, WKURLSchemeHandler, WKScriptMessageHandler
case autofillHandler
case hoverLinkHandler
case copyModeHandler
case transparentBackground
}

private func configure(_ configuration: WKWebViewConfiguration,
Expand Down Expand Up @@ -220,6 +221,144 @@ class iTermBrowserManager: NSObject, WKURLSchemeHandler, WKScriptMessageHandler
forMainFrameOnly: false,
worlds: [.page, .defaultClient],
identifier: UserScripts.consoleLog.rawValue))

// Inject transparent background CSS for see-through effect (like terminal)
// Inline the JS to avoid bundle loading issues
let transparentBackgroundJS = """
(function() {
'use strict';
console.log('[iTerm2] Transparent background script loaded');

// Global transparency level (0.0 = opaque, 1.0 = fully transparent)
window.iTermTransparencyLevel = 1.0;

// Make processedElements and applyTransparency global for external control
window.processedElements = new Set(); // Use Set instead of WeakSet for clearing
let isProcessing = false;

// Aggressive style injection with continuous monitoring
window.applyTransparency = function() {
if (isProcessing) return; // Prevent re-entry
isProcessing = true;

const transparency = window.iTermTransparencyLevel || 1.0;
const bgColor = transparency === 1.0 ? 'transparent' : 'rgba(0,0,0,' + (1 - transparency) + ')';

const style = document.getElementById('iterm2-transparent-background');
if (!style) {
const newStyle = document.createElement('style');
newStyle.id = 'iterm2-transparent-background';
(document.head || document.documentElement).appendChild(newStyle);
console.log('[iTerm2] Style tag injected');
}
// Update CSS with current transparency level
const styleElement = document.getElementById('iterm2-transparent-background');
if (styleElement) {
styleElement.textContent = `
/* Universal transparent background - opacity controlled by slider */
*, *::before, *::after {
background-color: ` + bgColor + ` !important;
background-image: none !important;
}
`;
}

// Only process elements that have inline styles (to override them)
const allElements = document.querySelectorAll('[style]');
let modified = 0;
allElements.forEach(el => {
if (window.processedElements.has(el)) return; // Skip already processed

// Skip media elements
const isMedia = el.tagName.match(/^(IMG|VIDEO|PICTURE|CANVAS|SVG|IFRAME)$/i) ||
el.className.match(/(thumbnail|avatar|icon|img|video|player|yt-image)/i) ||
el.id.match(/(thumbnail|avatar|icon|img|video|player)/i);
// Check if setProperty exists before using it (fixes SVG and other special elements)
if (!isMedia && (el.style.backgroundColor || el.style.background) && typeof el.style.setProperty === 'function') {
el.style.setProperty('background-color', bgColor, 'important');
el.style.setProperty('background-image', 'none', 'important');
window.processedElements.add(el);
modified++;
}
});
if (modified > 0) {
console.log('[iTerm2] Applied transparency to ' + modified + ' inline-styled elements (level: ' + transparency + ')');
}

isProcessing = false;
};

// Expose function for manual triggering
window.updateTransparency = function(value) {
window.iTermTransparencyLevel = value;
window.processedElements.clear();
window.applyTransparency();
};

// Initial application
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', window.applyTransparency);
} else {
window.applyTransparency();
}

// Debounced version to prevent blocking UI
let debounceTimer = null;
function debouncedApplyTransparency() {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
window.applyTransparency();
}, 200); // Increased to 200ms for better responsiveness
}

// Only watch for NEW nodes, not attribute changes
const observer = new MutationObserver((mutations) => {
// Skip if page is hidden to prevent background flickering
if (document.hidden) return;

let hasNewNodes = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
hasNewNodes = true;
break;
}
}
if (hasNewNodes) {
debouncedApplyTransparency();
}
});

// Pause observer when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('[iTerm2] Page hidden, pausing transparency updates');
} else {
console.log('[iTerm2] Page visible, resuming transparency updates');
debouncedApplyTransparency(); // Re-apply when visible again
}
});

if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
} else {
const bodyObserver = new MutationObserver((mutations, obs) => {
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
window.applyTransparency();
obs.disconnect();
}
});
bodyObserver.observe(document.documentElement, { childList: true });
}
})();
"""
contentManager.add(userScript: BrowserExtensionUserContentManager.UserScript(
code: transparentBackgroundJS,
injectionTime: .atDocumentStart,
forMainFrameOnly: false,
worlds: [.page],
identifier: UserScripts.transparentBackground.rawValue))

contentManager.add(userScript: .init(
code: iTermBrowserTemplateLoader.load(template: "graph-discovery.js", substitutions: [:]),
injectionTime: .atDocumentStart,
Expand Down Expand Up @@ -431,6 +570,9 @@ class iTermBrowserManager: NSObject, WKURLSchemeHandler, WKScriptMessageHandler
webView = iTermBrowserWebView(frame: .zero,
configuration: configuration,
pointerController: pointerController)
// Enable transparent background - allows seeing through to window behind
webView.setValue(false, forKey: "drawsBackground")

if let safariBundle = Bundle(path: "/Applications/Safari.app"),
let safariVersion = safariBundle.infoDictionary?["CFBundleShortVersionString"] as? String {
webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
Expand Down Expand Up @@ -563,6 +705,37 @@ class iTermBrowserManager: NSObject, WKURLSchemeHandler, WKScriptMessageHandler
}
}

func setTransparency(_ value: Double) {
DLog("setTransparency called with value: \(value)")
Task { @MainActor in
let bgColor = value == 1.0 ? "transparent" : "rgba(0,0,0,\(1.0 - value))"
let script = """
(function() {
const style = document.getElementById('iterm2-transparent-background');
if (style) {
style.textContent = '*, *::before, *::after { background-color: \(bgColor) !important; background-image: none !important; }';
console.log('[iTerm2] Updated transparency to \(value)');
}
document.querySelectorAll('*').forEach(el => {
const isMedia = /^(IMG|VIDEO|PICTURE|CANVAS|SVG|IFRAME)$/i.test(el.tagName);
// Check if setProperty exists before using it (fixes SVG and other special elements)
if (!isMedia && el.style && typeof el.style.setProperty === 'function') {
el.style.setProperty('background-color', '\(bgColor)', 'important');
el.style.setProperty('background-image', 'none', 'important');
}
});
return true;
})()
"""
do {
try await webView.safelyEvaluateJavaScript(script, contentWorld: .page)
DLog("Transparency applied: \(value)")
} catch {
DLog("Failed to apply transparency: \(error)")
}
}
}

struct PageContent {
var title: String
var content: String
Expand Down
8 changes: 8 additions & 0 deletions sources/Browser/Core/iTermBrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class iTermBrowserView: NSView {

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
// Enable transparency for see-through background
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor

// Register for the same drag types as SessionView to allow drag events to be forwarded
registerForDraggedTypes([
NSPasteboard.PasteboardType(iTermMovePaneDragType),
Expand All @@ -21,6 +25,10 @@ class iTermBrowserView: NSView {

required init?(coder: NSCoder) {
super.init(coder: coder)
// Enable transparency for see-through background
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor

registerForDraggedTypes([
NSPasteboard.PasteboardType(iTermMovePaneDragType),
NSPasteboard.PasteboardType("com.iterm2.psm.controlitem")
Expand Down
9 changes: 8 additions & 1 deletion sources/Browser/Core/iTermBrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -801,8 +801,11 @@ extension iTermBrowserViewController {
extension iTermBrowserViewController {
private func setupBackgroundView() {
backgroundView = NSVisualEffectView()
backgroundView.material = .contentBackground
// Use transparent background to see through to window behind (like terminal)
backgroundView.material = .underWindowBackground
backgroundView.blendingMode = .behindWindow
backgroundView.state = .active
backgroundView.alphaValue = 0.0 // Make fully transparent
view.addSubview(backgroundView)
}

Expand Down Expand Up @@ -1076,6 +1079,10 @@ extension iTermBrowserViewController: iTermBrowserToolbarDelegate {
func browserToolbarIsCurrentPageMuted() -> Bool {
return browserManager.currentPageIsMuted
}
func browserToolbarDidChangeTransparency(_ value: Double) {
DLog("Transparency changed to: \(value)")
browserManager.setTransparency(value)
}
}

// MARK: - iTermBrowserManagerDelegate
Expand Down
28 changes: 28 additions & 0 deletions sources/Browser/Core/transparent-background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// transparent-background.js
// Injects CSS to make webpage backgrounds transparent for see-through effect
(function() {
'use strict';

const style = document.createElement('style');
style.id = 'iterm2-transparent-background';
style.textContent = `
html, body {
background-color: transparent !important;
background-image: none !important;
}
`;

// Insert at document start to prevent flash of opaque background
if (document.head) {
document.head.insertBefore(style, document.head.firstChild);
} else {
// If head doesn't exist yet, wait for it
const observer = new MutationObserver(function(mutations, obs) {
if (document.head) {
document.head.insertBefore(style, document.head.firstChild);
obs.disconnect();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
})();
Loading