Skip to content

Commit

Permalink
Add support for custom CSS
Browse files Browse the repository at this point in the history
  • Loading branch information
mr-pennyworth committed Jul 12, 2024
1 parent aefe5ff commit ea07907
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 93 deletions.
8 changes: 6 additions & 2 deletions AlfredExtraPane.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
D0B3E0A42B8A1F3A008EA695 /* AlfredExtraPaneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B3E0A32B8A1F3A008EA695 /* AlfredExtraPaneTests.swift */; };
D0E764282C3C1E6D00E662F3 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = D0E764272C3C1E6D00E662F3 /* Sparkle */; };
D0FCE9412C40049200F5006C /* Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCE9402C40049200F5006C /* Menu.swift */; };
D0FCE9432C400A7600F5006C /* CSSInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCE9422C400A7600F5006C /* CSSInjection.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -65,6 +66,7 @@
D0B3E0A12B8A1F39008EA695 /* AlfredExtraPaneTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AlfredExtraPaneTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D0B3E0A32B8A1F3A008EA695 /* AlfredExtraPaneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlfredExtraPaneTests.swift; sourceTree = "<group>"; };
D0FCE9402C40049200F5006C /* Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = "<group>"; };
D0FCE9422C400A7600F5006C /* CSSInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSSInjection.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -119,6 +121,7 @@
D08B6EE4266F31790099EB36 /* Pane.swift */,
D08B6EE6266F9B520099EB36 /* PanePositionCodable.swift */,
D0FCE9402C40049200F5006C /* Menu.swift */,
D0FCE9422C400A7600F5006C /* CSSInjection.swift */,
);
path = AlfredExtraPane;
sourceTree = "<group>";
Expand Down Expand Up @@ -253,6 +256,7 @@
D08B6EE5266F31790099EB36 /* Pane.swift in Sources */,
D04F70D5255893B3008E17A4 /* NSColorExtension.swift in Sources */,
D08B6EE7266F9B520099EB36 /* PanePositionCodable.swift in Sources */,
D0FCE9432C400A7600F5006C /* CSSInjection.swift in Sources */,
D01646BA255AF27A00646F0C /* utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -405,7 +409,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 0.1.8;
MARKETING_VERSION = 0.1.9;
ONLY_ACTIVE_ARCH = NO;
OTHER_CODE_SIGN_FLAGS = "--deep";
PRODUCT_BUNDLE_IDENTIFIER = mr.pennyworth.AlfredExtraPane;
Expand All @@ -426,7 +430,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = 0.1.8;
MARKETING_VERSION = 0.1.9;
ONLY_ACTIVE_ARCH = NO;
OTHER_CODE_SIGN_FLAGS = "--deep";
PRODUCT_BUNDLE_IDENTIFIER = mr.pennyworth.AlfredExtraPane;
Expand Down
75 changes: 75 additions & 0 deletions AlfredExtraPane/CSSInjection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Alfred
import WebKit

/// Once the CSS in injected, the JS that did the injection will send
/// a message with this name to the webview. See `injecterJS`.
private let cssInjectedMessageName = "cssInjected"

/// There's no way to directly inject CSS into a WKWebView.
/// The only way is to inject a <style> tag into the document by executing
/// JS in the webview.
private func injecterJS(_ cssString: String) -> String {
"""
var style = document.createElement('style');
style.innerHTML = `\(cssString)`;
document.head.appendChild(style);
window.webkit.messageHandlers.\(cssInjectedMessageName).postMessage('done');
"""
}

class InjectedCSSWKWebView: WKWebView, WKScriptMessageHandler {
// This is required because we're subclassing WKWebView,
// and has nothing to do with the CSS injection.
required init?(coder: NSCoder) {
super.init(coder: coder)
}

init(
frame: CGRect,
configuration: WKWebViewConfiguration,
cssString: String
) {
log("will inject css: \(cssString)")

let userScript = WKUserScript(
source: injecterJS(cssString),
injectionTime: .atDocumentEnd,
forMainFrameOnly: true
)

let contentController = WKUserContentController()
contentController.addUserScript(userScript)

configuration.userContentController = contentController
super.init(frame: frame, configuration: configuration)

contentController.add(self, name: cssInjectedMessageName)
}

override func load(_ request: URLRequest) -> WKNavigation? {
// we don't want the webview to be visible till the css is injected, and
// has taken effect.
self.isHidden = true
return super.load(request)
}

override func loadFileURL(
_ URL: URL,
allowingReadAccessTo readAccessURL: URL
) -> WKNavigation? {
// we don't want the webview to be visible till the css is injected, and
// has taken effect.
self.isHidden = true
return super.loadFileURL(URL, allowingReadAccessTo: readAccessURL)
}

func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
// Once the CSS is injected, make the webview visible.
if message.name == cssInjectedMessageName {
self.isHidden = false
}
}
}
2 changes: 1 addition & 1 deletion AlfredExtraPane/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension AppDelegate {
action: #selector(openFileAction(_:)),
keyEquivalent: ""
)
globalConfigMenuItem.representedObject = try? globalConfigFile()
globalConfigMenuItem.representedObject = globalConfigFile
configureMenu.addItem(globalConfigMenuItem)
configureMenu.addItem(NSMenuItem.separator())
for workflow in Alfred.workflows().sorted(by: {$0.name < $1.name}) {
Expand Down
71 changes: 40 additions & 31 deletions AlfredExtraPane/Pane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,39 @@ public struct WorkflowPaneConfig {
/// A nil workflowUID means that this pane is a global pane,
/// applicable to all the workflows.
let workflowUID: String?

func dir() -> URL {
if let uid = workflowUID {
return Alfred.workflows().first(where: { $0.uid == uid })!.dir
} else {
return appPrefsDir
}
}
}

public struct PaneConfig: Codable, Equatable {
let alignment: PanePosition
let customUserAgent: String?
let customCSSFilename: String?
}

class Pane {
let workflowUID: String? // nil for global panes
let config: PaneConfig
var alfredFrame: NSRect = .zero
let window: NSWindow = makeWindow()
let webView: WKWebView = makeWebView()
let margin: CGFloat = 5

init(config: PaneConfig) {
self.config = config
private lazy var webView: WKWebView = {
makeWebView(
WorkflowPaneConfig(paneConfig: config, workflowUID: workflowUID)
)
}()

init(workflowPaneConfig: WorkflowPaneConfig) {
self.config = workflowPaneConfig.paneConfig
self.workflowUID = workflowPaneConfig.workflowUID
window.contentView!.addSubview(webView)
if let customUserAgent = config.customUserAgent {
webView.customUserAgent = customUserAgent
}

Alfred.onHide { self.hide() }
Alfred.onFrameChange { self.alfredFrame = $0 }
Expand All @@ -52,8 +64,9 @@ class Pane {
if url.isFileURL {
if url.absoluteString.hasSuffix(".html") {
let dir = url.deletingLastPathComponent()
webView.loadFileURL(injectCSS(url), allowingReadAccessTo: dir)
webView.loadFileURL(url, allowingReadAccessTo: dir)
} else {
log("skipping displaying '\(url)' as it isn't HTML")
return
}
} else {
Expand Down Expand Up @@ -198,7 +211,7 @@ func makeWindow() -> NSWindow {
return window
}

func makeWebView() -> WKWebView {
func makeWebView(_ workflowPaneConfig: WorkflowPaneConfig) -> WKWebView {
let conf = WKWebViewConfiguration()
conf.preferences.setValue(true, forKey: "developerExtrasEnabled")

Expand All @@ -208,32 +221,28 @@ func makeWebView() -> WKWebView {
// quick fix is to disable autoplay).
conf.mediaTypesRequiringUserActionForPlayback = .all

let webView = WKWebView(frame: .zero, configuration: conf)
var cssString = Alfred.themeCSS
if let wfCSSFilename = workflowPaneConfig.paneConfig.customCSSFilename {
let wfCSSFile =
workflowPaneConfig.dir().appendingPathComponent(wfCSSFilename)
if let wfCSSString = try? String(contentsOf: wfCSSFile) {
cssString += "\n" + wfCSSString
} else {
log("Failed to read custom CSS file: \(wfCSSFile)")
}
}

let webView = InjectedCSSWKWebView(
frame: .zero,
configuration: conf,
cssString: cssString
)
if let userAgent = workflowPaneConfig.paneConfig.customUserAgent {
webView.customUserAgent = userAgent
}
webView.backgroundColor = .clear
webView.setValue(false, forKey: "drawsBackground")
webView.wantsLayer = true
webView.layer?.cornerRadius = cornerRadius
return webView
}

func injectCSS(_ html: String) -> String {
var cssContainer = "body"
if html.contains("</head>") {
cssContainer = "head"
}
return html.replacingOccurrences(
of: "</\(cssContainer)>",
with: "<style>\n\(Alfred.themeCSS)</style></\(cssContainer)>"
)
}

func injectCSS(_ fileUrl: URL) -> URL {
// if you load html into webview using loadHTMLString,
// the resultant webview can't be given access to filesystem
// that means all the css and js references won't resolve anymore
let injectedHtmlPath = fileUrl.path + ".injected.html"
let injectedHtmlUrl = URL(fileURLWithPath: injectedHtmlPath)
let injectedHtml = readFile(named: fileUrl.path, then: injectCSS)!
try! injectedHtml.write(to: injectedHtmlUrl, atomically: true, encoding: .utf8)
return injectedHtmlUrl
}
105 changes: 50 additions & 55 deletions AlfredExtraPane/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,57 @@ import Sparkle

let globalConfigFilename = "config.json"
let workflowConfigFilename = "extra-pane-config.json"
let fs = FileManager.default

class AppDelegate: NSObject, NSApplicationDelegate {
let fs = FileManager.default
var panes: [Pane] = []
private let defaultConfig = PaneConfig(
public let appPrefsDir: URL = {
let bundleID = Bundle.main.bundleIdentifier ?? "mr.pennyworth.AlfredExtraPane"
let prefsDir = Alfred
.prefsDir
.appendingPathComponent("preferences")
.appendingPathComponent(bundleID)

if !fs.fileExists(atPath: prefsDir.path) {
try? fs.createDirectory(
at: prefsDir,
withIntermediateDirectories: true,
attributes: nil
)
}

return prefsDir
}()

let globalConfigFile: URL = {
let conf = appPrefsDir.appendingPathComponent(globalConfigFilename)
let defaultConfig = PaneConfig(
alignment: .horizontal(placement: .right, width: 300, minHeight: 400),
customUserAgent: nil
customUserAgent: nil,
customCSSFilename: nil
)
if !fs.fileExists(atPath: conf.path) {
write([defaultConfig], to: conf)
}
return conf
}()

let globalConfigs: [WorkflowPaneConfig] = {
(read(contentsOf: globalConfigFile) ?? [])
.map { WorkflowPaneConfig(paneConfig: $0, workflowUID: nil) }
}()

let workflowConfigs: [WorkflowPaneConfig] = {
Alfred.workflows().flatMap { wf in
let confPath = wf.dir.appendingPathComponent(workflowConfigFilename)
let confs: [PaneConfig] = read(contentsOf: confPath) ?? []
return confs.map {
WorkflowPaneConfig(paneConfig: $0, workflowUID: wf.uid)
}
}
}()


class AppDelegate: NSObject, NSApplicationDelegate {
var panes: [Pane] = []
var statusItem: NSStatusItem?
let updaterController = SPUStandardUpdaterController(
startingUpdater: true,
Expand All @@ -22,7 +65,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

override init() {
super.init()
let confs = globalConfigs() + workflowConfigs()
let confs = globalConfigs + workflowConfigs
dump(confs)
panes = confs.map { Pane(workflowPaneConfig: $0) }
Alfred.onItemSelect { item in
Expand All @@ -39,60 +82,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
setupMenubarExtra()
}

func application(_ application: NSApplication, open urls: [URL]) {
for url in urls {
log("\(url)")
log("\(url.queryParameters)")
}
}

func appPrefsDir() throws -> URL {
let bundleID = Bundle.main.bundleIdentifier!
let prefsDir = Alfred
.prefsDir
.appendingPathComponent("preferences")
.appendingPathComponent(bundleID)

if !fs.fileExists(atPath: prefsDir.path) {
try fs.createDirectory(
at: prefsDir,
withIntermediateDirectories: true,
attributes: nil
)
}

return prefsDir
}

func globalConfigFile() throws -> URL {
let conf = try! appPrefsDir().appendingPathComponent(globalConfigFilename)
if !fs.fileExists(atPath: conf.path) {
write([defaultConfig], to: conf)
}
return conf
}

func globalConfigs() -> [WorkflowPaneConfig] {
((try? read(contentsOf: globalConfigFile())) ?? [defaultConfig])
.map { WorkflowPaneConfig(paneConfig: $0, workflowUID: nil) }
}

func workflowConfigs() -> [WorkflowPaneConfig] {
Alfred.workflows().flatMap { wf in
let confPath = wf.dir.appendingPathComponent(workflowConfigFilename)
let confs: [PaneConfig] = read(contentsOf: confPath) ?? []
return confs.map {
WorkflowPaneConfig(paneConfig: $0, workflowUID: wf.uid)
}
}
}
}

autoreleasepool {
let app = NSApplication.shared
let delegate = AppDelegate()
print("\(delegate.panes)")
log("\(delegate.panes)")
app.setActivationPolicy(.accessory)
app.delegate = delegate
app.run()
Expand Down
Loading

0 comments on commit ea07907

Please sign in to comment.