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
1 change: 1 addition & 0 deletions Pique/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct ContentView: View {
("Shell", "terminal", Color.mint),
("Python", "chevron.left.forwardslash.chevron.right", Color.cyan),
("HCL", "doc.text", Color.indigo),
("Log", "doc.text.below.ecg", Color.gray),
]

var body: some View {
Expand Down
19 changes: 19 additions & 0 deletions Pique/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@
</array>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Log File</string>
<key>UTTypeIdentifier</key>
<string>io.macadmins.pique.log</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>log</string>
<string>out</string>
<string>err</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions Pique/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct SettingsView: View {
("JavaScript", "chevron.left.forwardslash.chevron.right", .yellow),
("Markdown", "doc.richtext", .gray),
("HCL", "doc.text", .indigo),
("Log", "doc.text.below.ecg", .gray),
]

@State private var overrides: [String: AppearanceOverride] = [:]
Expand Down
25 changes: 25 additions & 0 deletions PiquePreview/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>io.macadmins.pique.log</string>
<key>UTTypeDescription</key>
<string>Log File</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.plain-text</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>log</string>
<string>out</string>
<string>err</string>
</array>
</dict>
</dict>
</array>
<key>NSExtension</key>
<dict>
Expand Down Expand Up @@ -146,6 +165,9 @@
<string>io.macadmins.pique.hcl</string>
<string>io.macadmins.pique.recipe</string>
<string>com.apple.terminal.shell-script</string>
<string>io.macadmins.pique.log</string>
<string>com.apple.log</string>
<string>public.log</string>
</array>
<key>QLFileExtensions</key>
<array>
Expand Down Expand Up @@ -184,6 +206,9 @@
<string>tfvars</string>
<string>hcl</string>
<string>recipe</string>
<string>log</string>
<string>out</string>
<string>err</string>
</array>
<key>QLPreviewWidth</key>
<real>1024</real>
Expand Down
1 change: 1 addition & 0 deletions PiquePreview/PreviewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class PreviewProvider: NSViewController, QLPreviewingController {
case "js", "jsx", "ts", "tsx", "mjs", "cjs": return "JavaScript"
case "md", "markdown", "adoc": return "Markdown"
case "tf", "tfvars", "hcl": return "HCL"
case "log", "out", "err": return "Log"
default: return ext
}
}
Expand Down
7 changes: 7 additions & 0 deletions PiqueTests/FileFormatTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,20 @@ final class FileFormatTests: XCTestCase {
}
}

func testLog() {
for ext in ["log", "out", "err"] {
XCTAssertEqual(FileFormat(pathExtension: ext), .log, "Expected .log for .\(ext)")
}
}

// MARK: - Case insensitivity

func testCaseInsensitive() {
XCTAssertEqual(FileFormat(pathExtension: "JSON"), .json)
XCTAssertEqual(FileFormat(pathExtension: "YAML"), .yaml)
XCTAssertEqual(FileFormat(pathExtension: "Toml"), .toml)
XCTAssertEqual(FileFormat(pathExtension: "SH"), .shell)
XCTAssertEqual(FileFormat(pathExtension: "LOG"), .log)
}

// MARK: - Unknown / empty
Expand Down
113 changes: 113 additions & 0 deletions PiqueTests/HighlightIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,117 @@ final class HighlightIntegrationTests: XCTestCase {
let html = body(SyntaxHighlighter.highlight(hcl, format: .hcl))
XCTAssertTrue(html.contains(#"class="string""#))
}

// MARK: - Log files: heuristic highlighting

func testLogSeverityLevels() {
let log = "ERROR something failed\nWARN low disk\nINFO started\nDEBUG tick"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="plistValue">ERROR</span>"#))
XCTAssertTrue(html.contains(#"<span class="attrName">WARN</span>"#))
XCTAssertTrue(html.contains(#"<span class="bool">INFO</span>"#))
XCTAssertTrue(html.contains(#"<span class="comment">DEBUG</span>"#))
}

func testLogISOTimestamp() {
let log = "2026-03-28T14:05:33Z INFO ready"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="comment">2026-03-28T14:05:33Z</span>"#))
}

func testLogSyslogTimestamp() {
let log = "Mar 28 14:05:33 myhost syslogd: restart"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="comment">Mar 28 14:05:33</span>"#))
}

func testLogIPv4Address() {
let log = "connection from 192.168.1.100 accepted"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="number">192.168.1.100</span>"#))
}

func testLogHTTPMethodAndStatusCode() {
let log = #"GET /api/health 200"#
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="keyword">GET</span>"#))
XCTAssertTrue(html.contains(#"<span class="variable">/api/health</span>"#))
}

func testLogQuotedString() {
let log = #"message: "disk space low""#
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="string">&quot;disk space low&quot;</span>"#))
}

func testLogCriticalSeverity() {
let log = "FATAL out of memory\nCRITICAL disk failure"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="plistValue">FATAL</span>"#))
XCTAssertTrue(html.contains(#"<span class="plistValue">CRITICAL</span>"#))
}

func testLogApostropheNotTreatedAsString() {
let log = "INFO: Successfully validated the received JWT's signature..."
let html = body(SyntaxHighlighter.highlight(log, format: .log))
// The apostrophe in JWT's must NOT cause text to be swallowed into a string span
XCTAssertFalse(html.contains(#"<span class="string">'s signature...'"#),
"Apostrophe in JWT's should not start a quoted string")
XCTAssertTrue(html.contains("JWT"))
XCTAssertTrue(html.contains("signature"))
}

func testLogSeverityWithColon() {
let log = "2025-09-16 06:20:56 +0100 – INFO: Notifying that Docker has been installed"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="bool">INFO:</span>"#))
}

func testLogMultiLinePreservesAllLines() {
let log = "INFO line one\nWARN line two\nERROR line three"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="bool">INFO</span>"#))
XCTAssertTrue(html.contains(#"<span class="attrName">WARN</span>"#))
XCTAssertTrue(html.contains(#"<span class="plistValue">ERROR</span>"#))
XCTAssertTrue(html.contains("line one"))
XCTAssertTrue(html.contains("line two"))
XCTAssertTrue(html.contains("line three"))
}

func testLogHTTPStatusCodeColoring() {
let log = "status 200\nstatus 404\nstatus 500"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
// 2xx → .bool (blue/green)
XCTAssertTrue(html.contains(#"<span class="bool">200</span>"#))
// 4xx → .attrName (orange/warning)
XCTAssertTrue(html.contains(#"<span class="attrName">404</span>"#))
// 5xx → .plistValue (bold red)
XCTAssertTrue(html.contains(#"<span class="plistValue">500</span>"#))
}

func testLogFilePath() {
let log = "loading /usr/local/etc/config.yaml"
let html = body(SyntaxHighlighter.highlight(log, format: .log))
XCTAssertTrue(html.contains(#"<span class="variable">/usr/local/etc/config.yaml</span>"#))
}

// MARK: - Truncation

func testTruncationAppliedForLargeInput() {
// Generate a string larger than 512KB
let line = "2026-03-28T09:00:00Z INFO This is a log line for testing truncation purposes.\n"
let count = (512_001 / line.count) + 1
let bigLog = String(repeating: line, count: count)
XCTAssertGreaterThan(bigLog.count, 512_000, "Test input should exceed the limit")

let html = SyntaxHighlighter.highlight(bigLog, format: .log)
XCTAssertTrue(html.contains("Preview truncated"), "Large input should show truncation notice")
XCTAssertTrue(html.contains("lines shown"), "Truncation notice should mention line counts")
}

func testNoTruncationForSmallInput() {
let log = "INFO all good"
let html = SyntaxHighlighter.highlight(log, format: .log)
XCTAssertFalse(html.contains("Preview truncated"), "Small input should not be truncated")
}
}
Loading