Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
20 changes: 9 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest]
swift: ["6.0"]
swift: ["6.2"]

steps:
- uses: actions/checkout@v4

- name: Set up Swift (Linux)
if: runner.os != 'macOS'
- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: ${{ matrix.swift }}
Expand All @@ -31,12 +30,7 @@ jobs:
id: swift-version
run: |
set -euo pipefail
if [ "${RUNNER_OS}" = "macOS" ]; then
SWIFT_BIN="$(xcrun -f swift)"
else
SWIFT_BIN="swift"
fi
SWIFT_VERSION="$("${SWIFT_BIN}" --version | head -n 1 | awk '{print $4}')"
SWIFT_VERSION="$(swift --version | head -n 1 | awk '{print $4}')"
echo "version=${SWIFT_VERSION}" >> "${GITHUB_OUTPUT}"
echo "Swift version: ${SWIFT_VERSION}"

Expand All @@ -59,12 +53,16 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: "6.2"

- name: Resolve Swift version
id: swift-version
run: |
set -euo pipefail
SWIFT_BIN="$(xcrun -f swift)"
SWIFT_VERSION="$("${SWIFT_BIN}" --version | head -n 1 | awk '{print $4}')"
SWIFT_VERSION="$(swift --version | head -n 1 | awk '{print $4}')"
echo "version=${SWIFT_VERSION}" >> "${GITHUB_OUTPUT}"
echo "Swift version: ${SWIFT_VERSION}"

Expand Down
48 changes: 47 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 22 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -25,9 +25,19 @@ let package = Package(
name: "ChromaDemo",
targets: ["ChromaDemo"]
),
.executable(
name: "ca",
targets: ["Ca"]
),
],
dependencies: [
.package(url: "https://github.com/onevcat/Rainbow.git", from: "4.2.1"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.4.0"),
.package(
url: "https://github.com/apple/swift-configuration",
from: "0.2.0",
traits: ["JSONSupport"]
),
.package(url: "https://github.com/ordo-one/package-benchmark", from: "1.20.0"),
],
targets: [
Expand Down Expand Up @@ -57,6 +67,15 @@ let package = Package(
"ChromaBase46Themes",
]
),
.executableTarget(
name: "Ca",
dependencies: [
"Chroma",
"ChromaBase46Themes",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Configuration", package: "swift-configuration"),
]
),
.executableTarget(
name: "ChromaBenchmarks",
dependencies: [
Expand All @@ -68,5 +87,6 @@ let package = Package(
.plugin(name: "BenchmarkPlugin", package: "package-benchmark"),
]
),
]
],
swiftLanguageModes: [.v5]
)
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ To list all supported languages:
swift run ChromaDemo --list-languages
```

### Running ca (Experimental)

`ca` is an experimental Chroma-powered `cat` replacement:

```bash
swift run ca Sources/Chroma/Highlighter.swift
```

Config file path: `~/.config/ca/config.json`

```json
{
"theme": {
"name": "tokyonight",
"appearance": "auto"
},
"lineNumbers": true,
"paging": "auto",
"headers": true
}
```

### Installation

Add Chroma as a dependency in your `Package.swift`:
Expand Down
92 changes: 92 additions & 0 deletions Sources/Ca/CaCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import ArgumentParser
import Foundation

@main
struct CaCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "ca",
abstract: "A Chroma-powered cat replacement with syntax highlighting."
)

@Argument(help: "Files to display. Use '-' to read from stdin.")
var paths: [String] = []

@Option(help: "Theme name (ChromaBase46Themes or 'dark'/'light').")
var theme: String?

@Option(help: "Paging mode: auto, always, never.")
var paging: PagingMode?

@Flag(inversion: .prefixedNo, help: "Show line numbers.")
var lineNumbers: Bool?

@Flag(inversion: .prefixedNo, help: "Show file headers when rendering multiple inputs.")
var headers: Bool?

@Option(help: "Config file path (default: ~/.config/ca/config.json).")
var config: String?

mutating func run() async throws {
let loader = CaConfigLoader(filePathOverride: config)
var effectiveConfig = await loader.load()

if let theme {
effectiveConfig.theme.name = theme
}
if let paging {
effectiveConfig.paging = paging
}
if let lineNumbers {
effectiveConfig.lineNumbers = lineNumbers
}
if let headers {
effectiveConfig.headers = headers
}

do {
let inputs = try InputCollector().collect(paths: paths)
let theme = ThemeResolver().resolve(using: effectiveConfig)
let highlighter = HighlighterService(theme: theme, lineNumbers: effectiveConfig.lineNumbers)
let documents = try inputs.map { try highlighter.render($0) }
let lines = OutputComposer().compose(documents: documents, showHeaders: effectiveConfig.headers)
output(lines: lines, paging: effectiveConfig.paging)
} catch let error as CaError {
Diagnostics.printError(error.description)
throw ExitCode.failure
} catch {
Diagnostics.printError("Unexpected error: \(error)")
throw ExitCode.failure
}
}

private func output(lines: [String], paging: PagingMode) {
switch paging {
case .never:
write(lines)
case .always:
if let pager = Pager(lines: lines) {
pager.run()
} else {
write(lines)
}
case .auto:
if shouldPage(lines: lines), let pager = Pager(lines: lines) {
pager.run()
} else {
write(lines)
}
}
}

private func shouldPage(lines: [String]) -> Bool {
guard Terminal.isInteractive, let size = Terminal.size() else { return false }
return lines.count > size.rows
}

private func write(_ lines: [String]) {
let output = lines.joined(separator: "\n")
if let data = output.data(using: .utf8) {
FileHandle.standardOutput.write(data)
}
}
}
36 changes: 36 additions & 0 deletions Sources/Ca/CaConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ArgumentParser
import Chroma
import Foundation

struct CaConfig: Equatable {
struct ThemeSelection: Equatable {
var name: String?
var appearance: ThemeAppearancePreference
}

var theme: ThemeSelection
var lineNumbers: Bool
var paging: PagingMode
var headers: Bool

static let `default` = CaConfig(
theme: .init(name: nil, appearance: .auto),
lineNumbers: true,
paging: .auto,
headers: true
)
}

enum ThemeAppearancePreference: String {
case auto
case dark
case light
}

enum PagingMode: String, CaseIterable {
case auto
case always
case never
}

extension PagingMode: ExpressibleByArgument {}
21 changes: 21 additions & 0 deletions Sources/Ca/CaError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

enum CaError: Error, CustomStringConvertible {
case missingInput
case fileNotFound(String)
case unreadableFile(String)
case directoryNotSupported(String)

var description: String {
switch self {
case .missingInput:
return "No input provided. Pass a file path or pipe content into ca."
case .fileNotFound(let path):
return "File not found: \(path)"
case .unreadableFile(let path):
return "Unable to read file: \(path)"
case .directoryNotSupported(let path):
return "Directory input is not supported yet: \(path)"
}
}
}
Loading