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 11, 2024
1 parent ead5665 commit 22e2967
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 89 deletions.
4 changes: 4 additions & 0 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
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
66 changes: 37 additions & 29 deletions AlfredExtraPane/Pane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,35 @@ 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

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
Expand All @@ -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() }
Expand All @@ -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()
Expand Down Expand Up @@ -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")

Expand All @@ -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("</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
13 changes: 9 additions & 4 deletions AlfredExtraPaneTests/AlfredExtraPaneTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
}, {
Expand All @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 22e2967

Please sign in to comment.