From 22e29672b95e64c1d3718fa108eb2a4b13b03c86 Mon Sep 17 00:00:00 2001 From: "Mr. Pennyworth" Date: Thu, 11 Jul 2024 18:42:00 -0500 Subject: [PATCH] Add support for custom CSS --- AlfredExtraPane.xcodeproj/project.pbxproj | 4 + AlfredExtraPane/Menu.swift | 2 +- AlfredExtraPane/Pane.swift | 66 ++++++----- AlfredExtraPane/main.swift | 105 +++++++++--------- .../AlfredExtraPaneTests.swift | 13 ++- README.md | 3 + 6 files changed, 104 insertions(+), 89 deletions(-) diff --git a/AlfredExtraPane.xcodeproj/project.pbxproj b/AlfredExtraPane.xcodeproj/project.pbxproj index 7dbbbb0..024d005 100644 --- a/AlfredExtraPane.xcodeproj/project.pbxproj +++ b/AlfredExtraPane.xcodeproj/project.pbxproj @@ -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 */ @@ -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 = ""; }; D0FCE9402C40049200F5006C /* Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = ""; }; + D0FCE9422C400A7600F5006C /* CSSInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSSInjection.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -119,6 +121,7 @@ D08B6EE4266F31790099EB36 /* Pane.swift */, D08B6EE6266F9B520099EB36 /* PanePositionCodable.swift */, D0FCE9402C40049200F5006C /* Menu.swift */, + D0FCE9422C400A7600F5006C /* CSSInjection.swift */, ); path = AlfredExtraPane; sourceTree = ""; @@ -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; diff --git a/AlfredExtraPane/Menu.swift b/AlfredExtraPane/Menu.swift index 85353d5..307f718 100644 --- a/AlfredExtraPane/Menu.swift +++ b/AlfredExtraPane/Menu.swift @@ -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}) { diff --git a/AlfredExtraPane/Pane.swift b/AlfredExtraPane/Pane.swift index 8f880c2..861c587 100644 --- a/AlfredExtraPane/Pane.swift +++ b/AlfredExtraPane/Pane.swift @@ -16,11 +16,20 @@ 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 { @@ -28,9 +37,14 @@ class Pane { let config: PaneConfig var alfredFrame: NSRect = .zero let window: NSWindow = makeWindow() - let webView: WKWebView = makeWebView() let margin: CGFloat = 5 + private lazy var webView: WKWebView = { + makeWebView( + WorkflowPaneConfig(paneConfig: config, workflowUID: workflowUID) + ) + }() + private lazy var workflowIDtoUID: [String: String] = { Alfred.workflows().reduce(into: [:]) { acc, workflow in acc[workflow.id] = workflow.uid @@ -40,6 +54,9 @@ class Pane { init(workflowPaneConfig: WorkflowPaneConfig) { self.config = workflowPaneConfig.paneConfig self.workflowUID = workflowPaneConfig.workflowUID + if let customUserAgent = config.customUserAgent { + webView.customUserAgent = customUserAgent + } window.contentView!.addSubview(webView) Alfred.onHide { self.hide() } @@ -56,14 +73,12 @@ 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 { - if let customUserAgent = self.config.customUserAgent { - webView.customUserAgent = customUserAgent - } webView.load(URLRequest(url: url)) } showWindow() @@ -205,7 +220,7 @@ func makeWindow() -> NSWindow { return window } -func makeWebView() -> WKWebView { +func makeWebView(_ workflowPaneConfig: WorkflowPaneConfig) -> WKWebView { let conf = WKWebViewConfiguration() conf.preferences.setValue(true, forKey: "developerExtrasEnabled") @@ -215,32 +230,25 @@ 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 + ) 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("") { - cssContainer = "head" - } - return html.replacingOccurrences( - of: "", - with: "" - ) -} - -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 -} diff --git a/AlfredExtraPane/main.swift b/AlfredExtraPane/main.swift index 1c43652..6de1f54 100644 --- a/AlfredExtraPane/main.swift +++ b/AlfredExtraPane/main.swift @@ -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, @@ -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 @@ -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() diff --git a/AlfredExtraPaneTests/AlfredExtraPaneTests.swift b/AlfredExtraPaneTests/AlfredExtraPaneTests.swift index acf3fb0..f499842 100644 --- a/AlfredExtraPaneTests/AlfredExtraPaneTests.swift +++ b/AlfredExtraPaneTests/AlfredExtraPaneTests.swift @@ -9,6 +9,7 @@ final class AlfredExtraPaneTests: XCTestCase { "alignment" : { "horizontal" : {"placement" : "right", "width" : 300, "minHeight" : 400}} }, { + "customCSSFilename": "style.css", "alignment" : { "horizontal" : {"placement" : "left", "width" : 300, "minHeight" : null}} }, { @@ -23,19 +24,23 @@ final class AlfredExtraPaneTests: XCTestCase { let expected: [AlfredExtraPane.PaneConfig] = [ AlfredExtraPane.PaneConfig( alignment: .horizontal(placement: .right, width: 300, minHeight: 400), - customUserAgent: "agent of S.H.I.E.L.D." + customUserAgent: "agent of S.H.I.E.L.D.", + customCSSFilename: nil ), AlfredExtraPane.PaneConfig( alignment: .horizontal(placement: .left, width: 300, minHeight: nil), - customUserAgent: nil + customUserAgent: nil, + customCSSFilename: "style.css" ), AlfredExtraPane.PaneConfig( alignment: .vertical(placement: .top, height: 100), - customUserAgent: nil + customUserAgent: nil, + customCSSFilename: nil ), AlfredExtraPane.PaneConfig( alignment: .vertical(placement: .bottom, height: 200), - customUserAgent: nil + customUserAgent: nil, + customCSSFilename: nil ) ] let decoded = try! JSONDecoder().decode( diff --git a/README.md b/README.md index 175d286..8c8fafa 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ Configurable parameters are: - `placement`: `top` or `bottom` - `height`: height of the pane - `customUserAgent` (optional): User-Agent string for HTTP(S) URLs + - `customCSSFilename` (optional): Name of the CSS file to be loaded + in the pane. The file should be in the same directory as the JSON + config file. Here's an example with four panes configured: ```json