[
  {
    "path": ".github/demo_activity_style.tape",
    "content": "Output out.gif\n\nSet Shell \"zsh\"\nSet FontSize 32\nSet Width 660\nSet Height 300\nSet TypingSpeed 75ms\n\nType \"progressline -s snake\"\n\nEnter\n\nSleep 8\n"
  },
  {
    "path": ".github/demo_progressline_output.tape",
    "content": "Output progressline_output.gif\n\nRequire progressline\n\nSet Shell \"zsh\"\nSet FontSize 32\nSet Width 1600\nSet Height 400\nSet TypingSpeed 75ms\n\nHide\nType \"make long-running-command\"\nShow\nSleep 1\nType \" | progressline\"\n\nEnter\n\nSleep 11\n"
  },
  {
    "path": ".github/demo_standard_output.tape",
    "content": "Output standard_output.gif\n\nRequire progressline\n\nSet Shell \"zsh\"\nSet FontSize 32\nSet Width 1600\nSet Height 400\nSet TypingSpeed 75ms\n\nHide\nType \"make long-running-command\"\nShow\nSleep 1\n\nEnter\n\nSleep 11\n"
  },
  {
    "path": ".github/workflows/checks.yml",
    "content": "name: Checks\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\njobs:\n  build:\n    name: Integration tests\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: swift-actions/setup-swift@v2\n        with:\n          swift-version: \"5.10\"\n      - name: Prepare test build\n        run: swift build\n      - name: Run tests\n        run: ./Tests/integration_tests.sh .build/debug/progressline\n  lint:\n    runs-on: macos-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n      - name: Run SwiftFormat Linting\n        run: swiftformat Sources SakeApp Package.swift --lint\n"
  },
  {
    "path": ".github/workflows/semantic-pr-lint.yml",
    "content": "name: \"Lint PR\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n      - reopened\n\npermissions:\n  pull-requests: read\n\njobs:\n  main:\n    name: Validate PR title\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v5\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          types: |\n            fix\n            feat\n            chore\n            feat\n            fix\n            test\n            perf\n            refactor\n            doc\n            project\n            revert\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/.build\n/.index-build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\nTODO.md\n"
  },
  {
    "path": ".sake.yml",
    "content": "case_converting_strategy: toSnakeCase\n"
  },
  {
    "path": ".swiftformat",
    "content": "--swiftversion 5.10\n\n--exclude .build,.index-build,SakeApp/.build,SakeApp/.index-build\n\n--maxwidth 140\n--wraparguments before-first\n--wrapparameters before-first\n--wrapcollections before-first\n\n--enable isEmpty,wrapSwitchCases,wrapConditionalBodies,wrapEnumCases\n--disable redundantRawValues,redundantSelf,redundantStaticSelf,redundantType\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Vasilii Ianguzin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Package.resolved",
    "content": "{\n  \"originHash\" : \"e759c45271facbb3650829c703702a2ac4817adf75a8116cc3d77eae8e3d3bae\",\n  \"pins\" : [\n    {\n      \"identity\" : \"swift-argument-parser\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-argument-parser.git\",\n      \"state\" : {\n        \"revision\" : \"0fbc8848e389af3bb55c182bc19ca9d5dc2f255b\",\n        \"version\" : \"1.4.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-concurrency-extras\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/pointfreeco/swift-concurrency-extras.git\",\n      \"state\" : {\n        \"revision\" : \"bb5059bde9022d69ac516803f4f227d8ac967f71\",\n        \"version\" : \"1.1.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-tagged\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/pointfreeco/swift-tagged.git\",\n      \"state\" : {\n        \"revision\" : \"3907a9438f5b57d317001dc99f3f11b46882272b\",\n        \"version\" : \"0.10.0\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version: 5.10\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"ProgressLine\",\n    platforms: [\n        .macOS(.v10_15),\n    ],\n    products: [\n        .executable(name: \"progressline\", targets: [\"progressline\"]),\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/apple/swift-argument-parser.git\", from: \"1.4.0\"),\n        .package(url: \"https://github.com/pointfreeco/swift-tagged.git\", from: \"0.10.0\"),\n        .package(url: \"https://github.com/pointfreeco/swift-concurrency-extras.git\", from: \"1.1.0\"),\n    ],\n    targets: [\n        .executableTarget(\n            name: \"progressline\",\n            dependencies: [\n                .product(name: \"ArgumentParser\", package: \"swift-argument-parser\"),\n                .product(name: \"TaggedTime\", package: \"swift-tagged\"),\n                .product(name: \"ConcurrencyExtras\", package: \"swift-concurrency-extras\"),\n            ], swiftSettings: [\n                .enableExperimentalFeature(\"StrictConcurrency\"),\n            ]\n        ),\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "## ProgressLine\n\n![](https://img.shields.io/badge/Platform-macOS-6464aa)\n![](https://img.shields.io/badge/Platform-Linux-6464aa)\n[![Latest Release](https://img.shields.io/github/release/kattouf/ProgressLine.svg)](https://github.com/kattouf/ProgressLine/releases/latest)\n![](https://github.com/kattouf/ProgressLine/actions/workflows/checks.yml/badge.svg?branch=main)\n\nTrack commands progress in a compact one-line format.\n\n| ⏳ `progressline` output |\n|:--:|\n| ![](./.github/progressline_output.gif) |\n\n| 📝 standard output |\n|:--:|\n| ![](./.github/standard_output.gif) |\n\n[Usage](#usage) • [Features](#features) • [Installation](#installation)\n\n## Usage\n\nSimply pipe your command output into `progressline` to start tracking:\n\n```sh\nlong-running-command | progressline\n```\n\nIf the command you are executing also writes data to `stderr`, then you should probably use [\"redirection\"](https://www.gnu.org/software/bash/manual/html_node/Redirections.html) and send `stderr` messages to `stdout` so that they also go through the `progressline`:\n\n``` sh\nlong-running-command 2>&1 | progressline\n```\n\n## Features\n\n### Change activity indicator styles\n\nProgressLine offers different styles to represent activity, they can be changed using `-s, --activity-style` option:\n\n``` sh\nlong-running-command | progressline --activity-style snake\n```\n\nAvailable styles:\n\n| dots (Default) | snake | [kitt](https://en.wikipedia.org/wiki/KITT) | spinner |\n|:--:|:--:|:--:|:--:|\n| ![](./.github/activity_style_dots.gif) | ![](./.github/activity_style_snake.gif) | ![](./.github/activity_style_kitt.gif) | ![](./.github/activity_style_spinner.gif) |\n\n### Replace log output with custom text\n\nIf you don't need to see the log output during execution, even in a single line, you can replace it with your own text using the `-t, --static-text` option.\n\n``` sh\nlong-running-command | progressline --static-text \"Updating sources...\"\n```\n\n### Highlight important lines\n\nLog specific stdin lines above the progress line using the `-m, --log-matches` option:\n\n``` sh\nlong-running-command | progressline --log-matches \"regex-1\" --log-matches \"regex-2\"\n```\n\n### Use progress line as an addition to standard output\n\nLog all stdin data above the progress line using the `-a, --log-all` option:\n\n```sh\nlong-running-command | progressline --log-all\n```\n\n### Save original log\n\nYou have two options for saving the full original log:\n\n1. Using [tee](https://en.wikipedia.org/wiki/Tee_(command))\n\n``` sh\nlong-running-command | tee original-log.txt | progressline\n```\n\n2. Using `-l, --original-log-path` option:\n\n``` sh\nlong-running-command | progressline --original-log-path original-log.txt\n```\n\n## Installation\n\n### [Homebrew](https://brew.sh) (macOS / Linux)\n\n``` sh\nbrew install progressline\n```\n\n</details>\n\n### [Mint](https://github.com/yonaskolb/Mint) (macOS)\n\n``` sh\nmint install kattouf/ProgressLine\n```\n\n### [Mise](https://mise.jdx.dev) (macOS)\n\n``` sh\nmise use -g spm:kattouf/ProgressLine\n```\n\n### Manual Installation (macOS / Linux)\n\nDownload the binary for your platform from the [releases page](https://github.com/kattouf/ProgressLine/releases), and place it in your executable path.\n\n## Contributing\n\nFeel free to open a pull request or a discussion.\n"
  },
  {
    "path": "SakeApp/.gitignore",
    "content": ".DS_Store\n/.build\n/.index-build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc"
  },
  {
    "path": "SakeApp/BrewCommands.swift",
    "content": "import Sake\nimport SwiftShell\n\n@CommandGroup\nstruct BrewCommands {\n    static var ensureSwiftFormatInstalled: Command {\n        Command(\n            description: \"Ensure swiftformat is installed\",\n            skipIf: { _ in\n                run(\"which\", \"swiftformat\").succeeded\n            },\n            run: { _ in\n                try runAndPrint(\"brew\", \"install\", \"swiftformat\")\n            }\n        )\n    }\n\n    static var ensureGhInstalled: Command {\n        Command(\n            description: \"Ensure gh is installed\",\n            skipIf: { _ in\n                run(\"which\", \"gh\").succeeded\n            },\n            run: { _ in\n                try runAndPrint(\"brew\", \"install\", \"gh\")\n            }\n        )\n    }\n\n    static var ensureGitCliffInstalled: Command {\n        Command(\n            description: \"Ensure git-cliff is installed\",\n            skipIf: { _ in\n                run(\"which\", \"git-cliff\").succeeded\n            },\n            run: { _ in\n                try runAndPrint(\"brew\", \"install\", \"git-cliff\")\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "SakeApp/Package.resolved",
    "content": "{\n  \"originHash\" : \"564ae29a93959e0a64ff9ad1a401e5db179007f3ecef20571f852606328631f6\",\n  \"pins\" : [\n    {\n      \"identity\" : \"sake\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kattouf/Sake\",\n      \"state\" : {\n        \"revision\" : \"f2c91c8ecb4f67f0c565b081deb7d180761a21d7\",\n        \"version\" : \"0.2.2\"\n      }\n    },\n    {\n      \"identity\" : \"swift-argument-parser\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-argument-parser.git\",\n      \"state\" : {\n        \"revision\" : \"41982a3656a71c768319979febd796c6fd111d5c\",\n        \"version\" : \"1.5.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-syntax\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-syntax\",\n      \"state\" : {\n        \"revision\" : \"64889f0c732f210a935a0ad7cda38f77f876262d\",\n        \"version\" : \"509.1.1\"\n      }\n    },\n    {\n      \"identity\" : \"swiftshell\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kareman/SwiftShell\",\n      \"state\" : {\n        \"revision\" : \"99680b2efc7c7dbcace1da0b3979d266f02e213c\",\n        \"version\" : \"5.1.0\"\n      }\n    },\n    {\n      \"identity\" : \"yams\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/jpsim/Yams.git\",\n      \"state\" : {\n        \"revision\" : \"3036ba9d69cf1fd04d433527bc339dc0dc75433d\",\n        \"version\" : \"5.1.3\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "SakeApp/Package.swift",
    "content": "// swift-tools-version: 5.10\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport CompilerPluginSupport\nimport PackageDescription\n\nlet package = Package(\n    name: \"SakeApp\",\n    platforms: [.macOS(.v10_15)], // Required by SwiftSyntax for the macro feature in Sake\n    products: [\n        .executable(name: \"SakeApp\", targets: [\"SakeApp\"]),\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/apple/swift-argument-parser.git\", from: \"1.2.0\"),\n        .package(url: \"https://github.com/kattouf/Sake\", from: \"0.1.0\"),\n        .package(url: \"https://github.com/kareman/SwiftShell\", from: \"5.1.0\"),\n    ],\n    targets: [\n        .executableTarget(\n            name: \"SakeApp\",\n            dependencies: [\n                .product(name: \"ArgumentParser\", package: \"swift-argument-parser\"),\n                \"Sake\",\n                \"SwiftShell\",\n            ],\n            path: \".\"\n        ),\n    ]\n)\n"
  },
  {
    "path": "SakeApp/ReleaseCommands.swift",
    "content": "import ArgumentParser\nimport CryptoKit\nimport Foundation\nimport Sake\nimport SwiftShell\n\n@CommandGroup\nstruct ReleaseCommands {\n    private struct BuildTarget {\n        enum Arch {\n            case x86\n            case arm\n        }\n\n        enum OS {\n            case macos\n            case linux\n        }\n\n        let arch: Arch\n        let os: OS\n\n        var triple: String {\n            switch (arch, os) {\n            case (.x86, .macos): \"x86_64-apple-macosx\"\n            case (.arm, .macos): \"arm64-apple-macosx\"\n            case (.x86, .linux): \"x86_64-unknown-linux-gnu\"\n            case (.arm, .linux): \"aarch64-unknown-linux-gnu\"\n            }\n        }\n    }\n\n    private enum Constants {\n        static let buildArtifactsDirectory = \".build/artifacts\"\n        static let swiftVersion = \"6.0\"\n        static let buildTargets: [BuildTarget] = [\n            .init(arch: .arm, os: .macos),\n            .init(arch: .x86, os: .macos),\n            .init(arch: .x86, os: .linux),\n            .init(arch: .arm, os: .linux),\n        ]\n        static let executableName = \"progressline\"\n    }\n\n    private struct ReleaseArguments: ParsableArguments {\n        @Argument(help: \"Version number\")\n        var version: String\n\n        func validate() throws {\n            guard version.range(of: #\"^\\d+\\.\\d+\\.\\d+$\"#, options: .regularExpression) != nil else {\n                throw ValidationError(\"Invalid version number. Should be in the format 'x.y.z'\")\n            }\n        }\n    }\n\n    public static var brewRelease: Command {\n        Command(\n            description: \"Brew to Homebrew\",\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n                try runAndPrint(\"brew\", \"bump-formula-pr\", \"--version=\\(version)\", \"progressline\")\n            }\n        )\n    }\n\n    public static var githubRelease: Command {\n        Command(\n            description: \"Release to GitHub\",\n            dependencies: [\n                bumpVersion,\n                cleanReleaseArtifacts,\n                buildReleaseArtifacts,\n                calculateBuildArtifactsSha256,\n                createAndPushTag,\n                generateReleaseNotes,\n                draftReleaseWithArtifacts,\n            ]\n        )\n    }\n\n    static var bumpVersion: Command {\n        Command(\n            description: \"Bump version\",\n            skipIf: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n                let versionFilePath = \"Sources/Version.swift\"\n                let currentVersion = try String(contentsOfFile: versionFilePath)\n                    .split(separator: \"\\\"\")[1]\n                if currentVersion == version {\n                    print(\"Version is already \\(version). Skipping...\")\n                    return true\n                } else {\n                    return false\n                }\n            },\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n                let versionFilePath = \"Sources/Version.swift\"\n                let versionFileContent = \"\"\"\n                // This file is autogenerated. Do not edit.\n                let progressLineVersion = \"\\(version)\"\n\n                \"\"\"\n                try versionFileContent.write(toFile: versionFilePath, atomically: true, encoding: .utf8)\n\n                try runAndPrint(\"git\", \"add\", versionFilePath)\n                try runAndPrint(\"git\", \"commit\", \"-m\", \"chore(release): Bump version to \\(version)\")\n                print(\"Version bumped to \\(version)\")\n            }\n        )\n    }\n\n    static var cleanReleaseArtifacts: Command {\n        Command(\n            description: \"Clean release artifacts\",\n            run: { _ in\n                try? runAndPrint(\"rm\", \"-rf\", Constants.buildArtifactsDirectory)\n            }\n        )\n    }\n\n    static var buildReleaseArtifacts: Command {\n        Command(\n            description: \"Build release artifacts\",\n            skipIf: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n                let version = arguments.version\n\n                let targetsWithExistingArtifacts = Constants.buildTargets.filter { target in\n                    let archivePath = executableArchivePath(target: target, version: version)\n                    return FileManager.default.fileExists(atPath: archivePath)\n                }\n                if targetsWithExistingArtifacts.count == Constants.buildTargets.count {\n                    print(\"Release artifacts already exist. Skipping...\")\n                    return true\n                } else {\n                    context.storage[\"existing-artifacts-triples\"] = targetsWithExistingArtifacts.map(\\.triple)\n                    return false\n                }\n            },\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n                let version = arguments.version\n\n                try FileManager.default.createDirectory(\n                    atPath: Constants.buildArtifactsDirectory,\n                    withIntermediateDirectories: true,\n                    attributes: nil\n                )\n                let existingArtifactsTriples = context.storage[\"existing-artifacts-triples\"] as? [String] ?? []\n                for target in Constants.buildTargets {\n                    if existingArtifactsTriples.contains(target.triple) {\n                        print(\"Skipping \\(target.triple) as artifacts already exist\")\n                        continue\n                    }\n                    let (swiftBuild, swiftClean, strip, zip) = {\n                        let buildFlags = [\"--disable-sandbox\", \"--configuration\", \"release\", \"--triple\", target.triple]\n                        if target.os == .linux {\n                            let platform = target.arch == .arm ? \"linux/arm64\" : \"linux/amd64\"\n                            let dockerExec =\n                                \"docker run --rm --volume \\(context.projectRoot):/workdir --workdir /workdir --platform \\(platform) swift:\\(Constants.swiftVersion)\"\n                            let buildFlags = (buildFlags + [\"--static-swift-stdlib\"]).joined(separator: \" \")\n                            return (\n                                \"\\(dockerExec) swift build \\(buildFlags)\",\n                                \"\\(dockerExec) swift package clean\",\n                                \"\\(dockerExec) strip -s\",\n                                \"zip -j\"\n                            )\n                        } else {\n                            let buildFlags = buildFlags.joined(separator: \" \")\n                            return (\n                                \"swift build \\(buildFlags)\",\n                                \"swift package clean\",\n                                \"strip -rSTx\",\n                                \"zip -j\"\n                            )\n                        }\n                    }()\n\n                    try runAndPrint(bash: swiftClean)\n                    try runAndPrint(bash: swiftBuild)\n\n                    let binPath: String = run(bash: \"\\(swiftBuild) --show-bin-path\").stdout\n                    if binPath.isEmpty {\n                        throw NSError(domain: \"Fail to get bin path\", code: -999)\n                    }\n                    let executablePath = binPath + \"/\\(Constants.executableName)\"\n\n                    try runAndPrint(bash: \"\\(strip) \\(executablePath)\")\n\n                    let executableArchivePath = executableArchivePath(target: target, version: version)\n                    try runAndPrint(\n                        bash: \"\\(zip) \\(executableArchivePath) \\(executablePath.replacingOccurrences(of: \"/workdir\", with: context.projectRoot))\"\n                    )\n                }\n\n                print(\"Release artifacts built successfully at '\\(Constants.buildArtifactsDirectory)'\")\n            }\n        )\n    }\n\n    static var calculateBuildArtifactsSha256: Command {\n        @Sendable\n        func shasumFilePath(version: String) -> String {\n            \".build/artifacts/shasum-\\(version)\"\n        }\n\n        return Command(\n            description: \"Calculate SHA-256 checksums for build artifacts\",\n            skipIf: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n                let version = arguments.version\n\n                let shasumFilePath = shasumFilePath(version: version)\n\n                return FileManager.default.fileExists(atPath: shasumFilePath)\n            },\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n                let version = arguments.version\n\n                var shasumResults = [String]()\n                for target in Constants.buildTargets {\n                    let archivePath = executableArchivePath(target: target, version: version)\n                    let file = FileHandle(forReadingAtPath: archivePath)!\n                    let shasum = SHA256.hash(data: file.readDataToEndOfFile())\n                    let shasumString = shasum.compactMap { String(format: \"%02x\", $0) }.joined()\n                    shasumResults.append(\"\\(shasumString)  \\(archivePath)\")\n                }\n                FileManager.default.createFile(\n                    atPath: shasumFilePath(version: version),\n                    contents: shasumResults.joined(separator: \"\\n\").data(using: .utf8)\n                )\n            }\n        )\n    }\n\n    static var createAndPushTag: Command {\n        Command(\n            description: \"Create and push a tag\",\n            skipIf: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n\n                let grepResult = run(bash: \"git tag | grep \\(arguments.version)\")\n                if grepResult.succeeded {\n                    print(\"Tag \\(version) already exists. Skipping...\")\n                    return true\n                } else {\n                    return false\n                }\n            },\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n\n                print(\"Creating and pushing tag \\(version)\")\n                try runAndPrint(\"git\", \"tag\", version)\n                try runAndPrint(\"git\", \"push\", \"origin\", \"tag\", version)\n                try runAndPrint(\"git\", \"push\") // push local changes like version bump\n            }\n        )\n    }\n\n    static var generateReleaseNotes: Command {\n        Command(\n            description: \"Generate release notes\",\n            dependencies: [BrewCommands.ensureGitCliffInstalled],\n            skipIf: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n                let releaseNotesPath = releaseNotesPath(version: version)\n                if FileManager.default.fileExists(atPath: releaseNotesPath) {\n                    print(\"Release notes for \\(version) already exist at \\(releaseNotesPath). Skipping...\")\n                    return true\n                } else {\n                    return false\n                }\n            },\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let version = arguments.version\n                let releaseNotesPath = releaseNotesPath(version: version)\n                try runAndPrint(\"git\", \"cliff\", \"--latest\", \"--strip=all\", \"--tag\", version, \"--output\", releaseNotesPath)\n                print(\"Release notes generated at \\(releaseNotesPath)\")\n            }\n        )\n    }\n\n    static var draftReleaseWithArtifacts: Command {\n        Command(\n            description: \"Draft a release on GitHub\",\n            dependencies: [BrewCommands.ensureGhInstalled],\n            skipIf: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                let tagName = arguments.version\n                let ghViewResult = run(bash: \"gh release view \\(tagName)\")\n                if ghViewResult.succeeded {\n                    print(\"Release \\(tagName) already exists. Skipping...\")\n                    return true\n                } else {\n                    return false\n                }\n            },\n            run: { context in\n                let arguments = try ReleaseArguments.parse(context.arguments)\n                try arguments.validate()\n\n                print(\"Drafting release \\(arguments.version) on GitHub\")\n                let tagName = arguments.version\n                let releaseTitle = arguments.version\n                let draftReleaseCommand =\n                    \"gh release create \\(tagName) \\(Constants.buildArtifactsDirectory)/*.zip --title '\\(releaseTitle)' --draft --verify-tag --notes-file \\(releaseNotesPath(version: tagName))\"\n                try runAndPrint(bash: draftReleaseCommand)\n            }\n        )\n    }\n\n    private static func executableArchivePath(target: BuildTarget, version: String) -> String {\n        \"\\(Constants.buildArtifactsDirectory)/\\(Constants.executableName)-\\(version)-\\(target.triple).zip\"\n    }\n\n    private static func releaseNotesPath(version: String) -> String {\n        \".build/artifacts/release-notes-\\(version).md\"\n    }\n}\n"
  },
  {
    "path": "SakeApp/Sakefile.swift",
    "content": "import ArgumentParser\nimport Foundation\nimport Sake\nimport SwiftShell\n\n@main\n@CommandGroup\nstruct Commands: SakeApp {\n    public static let configuration = SakeAppConfiguration(\n        commandGroups: [\n            TestCommands.self,\n            ReleaseCommands.self,\n        ]\n    )\n\n    public static var lint: Command {\n        Command(\n            description: \"Lint code\",\n            dependencies: [BrewCommands.ensureSwiftFormatInstalled],\n            run: { _ in\n                try runAndPrint(\"swiftformat\", \"Sources\", \"SakeApp\", \"Package.swift\", \"--lint\")\n            }\n        )\n    }\n\n    public static var format: Command {\n        Command(\n            description: \"Format code\",\n            dependencies: [BrewCommands.ensureSwiftFormatInstalled],\n            run: { _ in\n                try runAndPrint(\"swiftformat\", \"Sources\", \"SakeApp\", \"Package.swift\")\n            }\n        )\n    }\n}\n\n@CommandGroup\nstruct TestCommands {\n    public static var test: Command {\n        Command(\n            description: \"Run tests\",\n            dependencies: [ensureDebugBuildIsUpToDate],\n            run: { context in\n                try runAndPrint(\n                    bash:\n                    \"\\(context.projectRoot)/Tests/integration_tests.sh \\(context.projectRoot)/.build/debug/progressline\"\n                )\n            }\n        )\n    }\n\n    private static var ensureDebugBuildIsUpToDate: Command {\n        Command(\n            description: \"Ensure debug build is up to date\",\n            run: { context in\n                try runAndPrint(bash: \"swift build --package-path \\(context.projectRoot)\")\n            }\n        )\n    }\n}\n\nextension Command.Context {\n    var projectRoot: String {\n        \"\\(appDirectory)/..\"\n    }\n}\n"
  },
  {
    "path": "Sources/ANSI.swift",
    "content": "enum ANSI {\n    // Cursor controls\n    static func cursorUp(_ count: Int) -> String {\n        \"\\u{1B}[\\(count)A\"\n    }\n\n    static func cursorToColumn(_ column: Int) -> String {\n        \"\\u{1B}[\\(column)G\"\n    }\n\n    static let eraseLine = \"\\u{1B}[2K\"\n\n    // Colors and styles\n    static let noStyleMode = !isTTY\n    static var red: String {\n        noStyleMode ? \"\" : \"\\u{1B}[31m\"\n    }\n\n    static var green: String {\n        noStyleMode ? \"\" : \"\\u{1B}[32m\"\n    }\n\n    static var yellow: String {\n        noStyleMode ? \"\" : \"\\u{1B}[33m\"\n    }\n\n    static var blue: String {\n        noStyleMode ? \"\" : \"\\u{1B}[34m\"\n    }\n\n    static var magenta: String {\n        noStyleMode ? \"\" : \"\\u{1B}[35m\"\n    }\n\n    static var bold: String {\n        noStyleMode ? \"\" : \"\\u{1B}[1m\"\n    }\n\n    static var reset: String {\n        noStyleMode ? \"\" : \"\\u{1B}[0m\"\n    }\n}\n"
  },
  {
    "path": "Sources/ActivityIndicator+CommandArgument.swift",
    "content": "import ArgumentParser\n\nenum ActivityIndicatorStyle: String, CaseIterable, ExpressibleByArgument {\n    case dots\n    case kitt\n    case snake\n    case spinner\n}\n\nextension ActivityIndicator {\n    static func make(style: ActivityIndicatorStyle) -> ActivityIndicator {\n        switch style {\n        case .dots:\n            .dots\n        case .kitt:\n            .kitt\n        case .snake:\n            .snake\n        case .spinner:\n            .spinner\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ActivityIndicator.swift",
    "content": "import Foundation\nimport TaggedTime\n\nfinal class ActivityIndicator: Sendable {\n    struct Configuration {\n        let refreshRate: Milliseconds<UInt64>\n        let states: [String]\n    }\n\n    let configuration: Configuration\n\n    init(configuration: Configuration) {\n        self.configuration = configuration\n    }\n\n    func state(forDuration duration: Seconds<TimeInterval>) -> String {\n        let iteration = Int(duration.milliseconds.rawValue / TimeInterval(configuration.refreshRate.rawValue)) % configuration.states.count\n        return configuration.states[iteration]\n    }\n}\n\nextension ActivityIndicator {\n    static let dots: ActivityIndicator = {\n        let configuration = Configuration(\n            refreshRate: 125,\n            states: [\n                \"⠋\",\n                \"⠙\",\n                \"⠹\",\n                \"⠸\",\n                \"⠼\",\n                \"⠴\",\n                \"⠦\",\n                \"⠧\",\n                \"⠇\",\n                \"⠏\",\n            ]\n        )\n        return ActivityIndicator(configuration: configuration)\n    }()\n\n    static let kitt: ActivityIndicator = {\n        let configuration = Configuration(\n            refreshRate: 125,\n            states: [\n                \"▰▱▱▱▱\",\n                \"▰▰▱▱▱\",\n                \"▰▰▰▱▱\",\n                \"▱▰▰▰▱\",\n                \"▱▱▰▰▰\",\n                \"▱▱▱▰▰\",\n                \"▱▱▱▱▰\",\n                \"▱▱▱▰▰\",\n                \"▱▱▰▰▰\",\n                \"▱▰▰▰▱\",\n                \"▰▰▰▱▱\",\n                \"▰▰▱▱▱\",\n            ]\n        )\n        return ActivityIndicator(configuration: configuration)\n    }()\n\n    static let snake: ActivityIndicator = {\n        let configuration = Configuration(\n            refreshRate: 125,\n            states: [\n                \"▰▱▱▱▱\",\n                \"▰▰▱▱▱\",\n                \"▰▰▰▱▱\",\n                \"▱▰▰▰▱\",\n                \"▱▱▰▰▰\",\n                \"▱▱▱▰▰\",\n                \"▱▱▱▱▰\",\n                \"▱▱▱▱▱\",\n            ]\n        )\n        return ActivityIndicator(configuration: configuration)\n    }()\n\n    static let spinner: ActivityIndicator = {\n        let configuration = Configuration(\n            refreshRate: 125,\n            states: [\n                \"\\\\\",\n                \"|\",\n                \"/\",\n                \"-\",\n            ]\n        )\n        return ActivityIndicator(configuration: configuration)\n    }()\n}\n\n#if DEBUG\n    extension ActivityIndicator {\n        static func disabled() -> ActivityIndicator {\n            .init(\n                configuration: .init(refreshRate: 1_000_000_000, states: [])\n            )\n        }\n    }\n#endif\n"
  },
  {
    "path": "Sources/ErrorMessage.swift",
    "content": "enum ErrorMessage {\n    static let canNotDecodeData = \"\\(ANSI.yellow)[!] progressline: Failed to decode stdin data as UTF-8\\(ANSI.reset)\"\n    static func canNotCompileRegex(_ regex: String) -> String {\n        \"\\(ANSI.yellow)[!] progressline: Failed to compile regular expression: \\(regex)\\(ANSI.reset)\"\n    }\n\n    static func canNotOpenFile(_ path: String) -> String {\n        \"\\(ANSI.yellow)[!] progressline: Failed to open file at path: \\(path)\\(ANSI.reset)\"\n    }\n}\n"
  },
  {
    "path": "Sources/FileHandler+AsyncStream.swift",
    "content": "#if os(Linux)\n    // Linux implementation of FileHandle not Sendable\n    @preconcurrency import Foundation\n#else\n    import Foundation\n#endif\n\nextension FileHandle {\n    var asyncStream: AsyncStream<Data> {\n        AsyncStream { continuation in\n            Task {\n                while let data = try waitAndReadAvailableData() {\n                    continuation.yield(data)\n                }\n                continuation.finish()\n            }\n        }\n    }\n\n    private func waitAndReadAvailableData() throws -> Data? {\n        let data = availableData\n        guard !data.isEmpty else {\n            return nil\n        }\n        return data\n    }\n}\n"
  },
  {
    "path": "Sources/LogAllController.swift",
    "content": "import Foundation\n\nfinal class LogAllController {\n    private let logger: AboveProgressLineLogger\n\n    init(logger: AboveProgressLineLogger) {\n        self.logger = logger\n    }\n\n    func didGetStdinDataChunk(_ data: Data) async {\n        let text = String(data: data, encoding: .utf8)\n        guard let text else {\n            await logger.logError(ErrorMessage.canNotDecodeData)\n            return\n        }\n        // we control newlines and whitespaces in the logger\n        let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)\n        await logger.log(trimmedText)\n    }\n}\n"
  },
  {
    "path": "Sources/MatchesController.swift",
    "content": "import Foundation\n\nfinal class MatchesController {\n    private let logger: AboveProgressLineLogger\n    let regexps: [NSRegularExpression]\n\n    init?(logger: AboveProgressLineLogger, regexps: [String]) async {\n        self.logger = logger\n        guard !regexps.isEmpty else {\n            return nil\n        }\n        var invalidRegexps = [String]()\n        self.regexps = regexps.compactMap { regexp in\n            do {\n                return try NSRegularExpression(pattern: regexp)\n            } catch {\n                invalidRegexps.append(regexp)\n                return nil\n            }\n        }\n        for invalidRegexp in invalidRegexps {\n            await logger.logError(ErrorMessage.canNotCompileRegex(invalidRegexp))\n        }\n    }\n\n    func didGetStdinDataChunk(_ data: Data) async {\n        let text = String(data: data, encoding: .utf8)\n        guard let text else {\n            await logger.logError(ErrorMessage.canNotDecodeData)\n            return\n        }\n        for line in text.split(whereSeparator: \\.isNewline) {\n            let range = NSRange(location: 0, length: line.utf16.count)\n            for regex in regexps {\n                if regex.firstMatch(in: String(line), range: range) != nil {\n                    await logger.log(String(line))\n                    break\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/OriginalLogController.swift",
    "content": "import Foundation\n\nfinal class OriginalLogController {\n    private let logger: AboveProgressLineLogger\n    let fileHandle: FileHandle\n\n    init?(logger: AboveProgressLineLogger, path: String) async {\n        self.logger = logger\n\n        do {\n            let url = URL(fileURLWithPath: path)\n            try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)\n            _ = FileManager.default.createFile(atPath: path, contents: nil)\n            self.fileHandle = try FileHandle(forWritingTo: url)\n            fileHandle.seekToEndOfFile()\n        } catch {\n            await logger.logError(ErrorMessage.canNotOpenFile(path))\n            return nil\n        }\n    }\n\n    deinit {\n        fileHandle.closeFile()\n    }\n\n    func didGetStdinDataChunk(_ data: Data) async {\n        fileHandle.write(data)\n    }\n}\n"
  },
  {
    "path": "Sources/Printer.swift",
    "content": "import ConcurrencyExtras\n#if os(Linux)\n    // Linux implementation of FileHandle not Sendable\n    @preconcurrency import Foundation\n#else\n    import Foundation\n#endif\n\nfinal class Printer: Sendable {\n    private let fileHandle: LockIsolated<FileHandle>\n    private let buffer = LockIsolated(String())\n    private let _wasWritten = LockIsolated(false)\n\n    var wasWritten: Bool {\n        _wasWritten.value\n    }\n\n    init(fileHandle: FileHandle) {\n        self.fileHandle = .init(fileHandle)\n    }\n\n    @discardableResult\n    func writeln(_ text: String) -> Self {\n        buffer.withValue { $0 += text + \"\\n\" }\n        return self\n    }\n\n    @discardableResult\n    func write(_ text: String) -> Self {\n        buffer.withValue { $0 += text }\n        return self\n    }\n\n    @discardableResult\n    func cursorToColumn(_ column: Int) -> Self {\n        buffer.withValue { $0 += ANSI.cursorToColumn(column) }\n        return self\n    }\n\n    @discardableResult\n    func cursorUp(_ count: Int = 1) -> Self {\n        buffer.withValue { $0 += ANSI.cursorUp(count) }\n        return self\n    }\n\n    @discardableResult\n    func eraseLine() -> Self {\n        buffer.withValue { $0 += ANSI.eraseLine }\n        return self\n    }\n\n    func flush() {\n        fileHandle.withValue {\n            $0.write(buffer.value.data(using: .utf8)!)\n            try? $0.synchronize()\n        }\n        if !_wasWritten.value {\n            _wasWritten.setValue(true)\n        }\n        buffer.setValue(String())\n    }\n}\n"
  },
  {
    "path": "Sources/PrintersHolder.swift",
    "content": "import Foundation\n\n// \"Lock\" access to printers to prevent write conflicts\nfinal actor PrintersHolder {\n    private let printer: Printer\n    private let errorsPrinter: Printer\n\n    init(printer: Printer, errorsPrinter: Printer) {\n        self.printer = printer\n        self.errorsPrinter = errorsPrinter\n    }\n\n    func withPrinter<T: Sendable>(_ body: @Sendable (Printer) async throws -> T) async rethrows -> T {\n        try await body(printer)\n    }\n\n    func withErrorsPrinter<T: Sendable>(_ body: @Sendable (Printer) async throws -> T) async rethrows -> T {\n        try await body(errorsPrinter)\n    }\n}\n"
  },
  {
    "path": "Sources/ProgressLine.swift",
    "content": "import ArgumentParser\nimport ConcurrencyExtras\nimport Foundation\nimport TaggedTime\n\n@main\nstruct ProgressLine: AsyncParsableCommand {\n    static let configuration = CommandConfiguration(\n        commandName: \"progressline\",\n        abstract: \"A command-line tool for compactly tracking the progress of piped commands.\",\n        usage: \"some-command | progressline\",\n        version: progressLineVersion\n    )\n\n    @Option(name: [.long, .customShort(\"t\")], help: \"The static text to display instead of the latest stdin data.\")\n    var staticText: String?\n\n    @Option(name: [.customLong(\"activity-style\"), .customShort(\"s\")], help: \"The style of the activity indicator.\")\n    var activityIndicatorStyle: ActivityIndicatorStyle = .dots\n\n    @Option(name: [.customLong(\"original-log-path\"), .customShort(\"l\")], help: \"Save the original log to a file.\")\n    var originalLogPath: String?\n\n    @Option(\n        name: [.customLong(\"log-matches\"), .customShort(\"m\")],\n        help: \"Log above progress line lines matching the given regular expressions.\"\n    )\n    var matchesToLog: [String] = []\n\n    @Flag(name: [.customLong(\"log-all\"), .customShort(\"a\")], help: \"Log all lines above the progress line.\")\n    var shouldLogAll: Bool = false\n\n    #if DEBUG\n        @Flag(name: [.customLong(\"test-mode\")], help: \"Enable test mode. Activity indicator will be replaced with a static string.\")\n        var testMode: Bool = false\n    #endif\n\n    mutating func run() async throws {\n        try validateConfiguration()\n\n        let printers = PrintersHolder(\n            printer: Printer(fileHandle: .standardOutput),\n            errorsPrinter: Printer(fileHandle: .standardError)\n        )\n        let logger = AboveProgressLineLogger(printers: printers)\n\n        #if DEBUG\n            let activityIndicator: ActivityIndicator = testMode ? .disabled() : .make(style: activityIndicatorStyle)\n        #else\n            let testMode = false\n            let activityIndicator: ActivityIndicator = .make(style: activityIndicatorStyle)\n        #endif\n        let progressLineController = await ProgressLineController.buildAndStart(\n            textMode: staticText.map { .staticText($0) } ?? .stdin,\n            printers: printers,\n            logger: logger,\n            activityIndicator: activityIndicator,\n            mockActivityAndDuration: testMode\n        )\n        let originalLogController = if let originalLogPath {\n            await OriginalLogController(logger: logger, path: originalLogPath)\n        } else {\n            OriginalLogController?.none\n        }\n        let matchesController = await MatchesController(logger: logger, regexps: matchesToLog)\n        let logAllController = shouldLogAll ? LogAllController(logger: logger) : nil\n\n        for await data in FileHandle.standardInput.asyncStream {\n            await logAllController?.didGetStdinDataChunk(data)\n            await matchesController?.didGetStdinDataChunk(data)\n            await progressLineController.didGetStdinDataChunk(data)\n            await originalLogController?.didGetStdinDataChunk(data)\n        }\n\n        await progressLineController.didReachEndOfStdin()\n    }\n\n    private func validateConfiguration() throws {\n        guard !shouldLogAll || matchesToLog.isEmpty else {\n            throw ValidationError(\"The --log-all and --log-matches options are mutually exclusive.\")\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ProgressLineController.swift",
    "content": "import Foundation\nimport TaggedTime\n\nfinal actor ProgressLineController {\n    enum TextMode {\n        case staticText(String)\n        case stdin\n    }\n\n    // Dependencies\n    private let textMode: TextMode\n    private let printers: PrintersHolder\n    private let logger: AboveProgressLineLogger\n    private let progressLineFormatter: ProgressLineFormatter\n    private let progressTracker: ProgressTracker\n    // State\n    private var renderLoopTask: Task<Never, any Error>?\n    private var lastStdinLine: String?\n    private var progress: Progress?\n\n    private init(\n        textMode: TextMode,\n        printers: PrintersHolder,\n        logger: AboveProgressLineLogger,\n        progressLineFormatter: ProgressLineFormatter,\n        progressTracker: ProgressTracker\n    ) {\n        self.textMode = textMode\n        self.printers = printers\n        self.logger = logger\n        self.progressLineFormatter = progressLineFormatter\n        self.progressTracker = progressTracker\n    }\n\n    // MARK: - Public\n\n    static func buildAndStart(\n        textMode: TextMode,\n        printers: PrintersHolder,\n        logger: AboveProgressLineLogger,\n        activityIndicator: ActivityIndicator,\n        mockActivityAndDuration: Bool = false\n    ) async -> Self {\n        let progressTracker = ProgressTracker.start()\n        let windowSizeObserver = WindowSizeObserver.startObserving()\n        let progressLineFormatter = ProgressLineFormatter(\n            activityIndicator: activityIndicator,\n            windowSizeObserver: windowSizeObserver,\n            mockActivityAndDuration: mockActivityAndDuration\n        )\n\n        let controller = Self(\n            textMode: textMode,\n            printers: printers,\n            logger: logger,\n            progressLineFormatter: progressLineFormatter,\n            progressTracker: progressTracker\n        )\n        await controller.startAnimationLoop(refreshRate: activityIndicator.configuration.refreshRate)\n\n        return controller\n    }\n\n    // MARK: - Input\n\n    func didGetStdinDataChunk(_ data: Data) async {\n        guard case .stdin = textMode else {\n            // we will redraw anyway to sync (prevent flickering) with other log controllers\n            await redrawProgressLine()\n            return\n        }\n\n        let stdinText = String(data: data, encoding: .utf8)\n        guard let stdinText else {\n            await logger.logError(ErrorMessage.canNotDecodeData)\n            return\n        }\n\n        lastStdinLine = stdinText\n            .split(whereSeparator: \\.isNewline)\n            .last { !$0.isEmpty }\n            .map(String.init)\n\n        await redrawProgressLine()\n    }\n\n    func didReachEndOfStdin() async {\n        stopAnimationLoop()\n\n        let progressLine = progressLineFormatter.finished(progress: progress)\n        await printers.withPrinter { printer in\n            if printer.wasWritten {\n                printer\n                    .cursorUp()\n                    .eraseLine()\n            }\n            printer\n                .writeln(progressLine)\n                .flush()\n        }\n    }\n\n    // MARK: - Private\n\n    private func startAnimationLoop(refreshRate: Milliseconds<UInt64>) {\n        renderLoopTask = Task.periodic(interval: refreshRate) { [weak self] in\n            guard !Task.isCancelled else {\n                return\n            }\n            await self?.redrawProgressLine()\n        }\n    }\n\n    private func stopAnimationLoop() {\n        renderLoopTask?.cancel()\n    }\n\n    private func redrawProgressLine() async {\n        let lineText: String? = switch textMode {\n        case let .staticText(text):\n            text\n        case .stdin:\n            lastStdinLine\n        }\n        let progress = progressTracker.moveForward(lineText)\n        let progressLine = progressLineFormatter.inProgress(progress: progress)\n        self.progress = progress\n        await printers.withPrinter { printer in\n            if printer.wasWritten {\n                printer\n                    .cursorUp()\n                    .eraseLine()\n            }\n            printer\n                .writeln(progressLine)\n                .flush()\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/ProgressLineFormatter.swift",
    "content": "import Foundation\nimport TaggedTime\n\nprivate enum Symbol {\n    static let checkmark = \"✓\"\n    static let prompt = \"❯\"\n}\n\nfinal class ProgressLineFormatter: Sendable {\n    // Linux doesn't support DateComponentsFormatter\n    #if os(macOS)\n        private let durationFormatter: DateComponentsFormatter = {\n            let durationFormatter = DateComponentsFormatter()\n            durationFormatter.unitsStyle = .abbreviated\n            durationFormatter.allowedUnits = [.hour, .minute, .second]\n            durationFormatter.maximumUnitCount = 2\n            return durationFormatter\n        }()\n    #endif\n\n    private let activityIndicator: ActivityIndicator\n    private let windowSizeObserver: WindowSizeObserver?\n    private let mockActivityAndDuration: Bool\n\n    init(\n        activityIndicator: ActivityIndicator,\n        windowSizeObserver: WindowSizeObserver?,\n        mockActivityAndDuration: Bool\n    ) {\n        self.activityIndicator = activityIndicator\n        self.windowSizeObserver = windowSizeObserver\n        self.mockActivityAndDuration = mockActivityAndDuration\n    }\n\n    func inProgress(progress: Progress) -> String {\n        let activityIndicator = mockActivityAndDuration ? \"<activity>\" : activityIndicator.state(forDuration: progress.duration)\n        let formattedDuration = mockActivityAndDuration ? \"<duration>\" : formatDuration(from: progress.duration)\n\n        let styledActivityIndicator = ANSI.blue + activityIndicator + ANSI.reset\n        let styledDuration = ANSI.bold + formattedDuration + ANSI.reset\n        let styledPrompt = ANSI.blue + Symbol.prompt + ANSI.reset\n\n        return buildResultString(\n            styledActivityIndicator: styledActivityIndicator,\n            styledDuration: styledDuration,\n            styledPrompt: styledPrompt,\n            progressLine: progress.line\n        )\n    }\n\n    func finished(progress: Progress?) -> String {\n        let formattedDuration = mockActivityAndDuration ? \"<duration>\" : progress.map { formatDuration(from: $0.duration) }\n\n        let styledActivityIndicator = ANSI.green + Symbol.checkmark + ANSI.reset\n        let styledDuration = formattedDuration.map { ANSI.bold + $0 + ANSI.reset }\n        let styledPrompt = ANSI.green + Symbol.prompt + ANSI.reset\n\n        return buildResultString(\n            styledActivityIndicator: styledActivityIndicator,\n            styledDuration: styledDuration,\n            styledPrompt: styledPrompt,\n            progressLine: progress?.line\n        )\n    }\n\n    private func buildResultString(\n        styledActivityIndicator: String,\n        styledDuration: String?,\n        styledPrompt: String,\n        progressLine: String?\n    ) -> String {\n        let buildResultWithProgressLine = { (progressLine: String?) -> String in\n            [styledActivityIndicator, styledDuration, styledPrompt, progressLine]\n                .compactMap { $0 }\n                .joined(separator: \" \")\n        }\n        let result = buildResultWithProgressLine(progressLine)\n\n        let notFittedToWindowLength = calculateStringNotFittedToWindowLength(result)\n        if let progressLine, notFittedToWindowLength > 0 {\n            let fittedProgressLine = String(progressLine.prefix(progressLine.count - notFittedToWindowLength))\n            return buildResultWithProgressLine(fittedProgressLine)\n        } else {\n            return result\n        }\n    }\n\n    private func calculateStringNotFittedToWindowLength(_ string: String) -> Int {\n        guard let windowSizeObserver else {\n            return 0\n        }\n        let stringWithoutANSI = string.withoutANSI()\n        let windowWidth = windowSizeObserver.size.width\n        return max(stringWithoutANSI.count - windowWidth, 0)\n    }\n\n    private func formatDuration(from duration: Seconds<TimeInterval>) -> String {\n        #if os(Linux)\n            duration.rawValue.formattedDuration()\n        #else\n            durationFormatter.string(from: duration.rawValue)!\n        #endif\n    }\n}\n\n#if os(Linux)\n    extension TimeInterval {\n        func formattedDuration() -> String {\n            let totalSeconds = Int(self)\n            let hours = totalSeconds / 3600\n            let minutes = (totalSeconds % 3600) / 60\n            let seconds = totalSeconds % 60\n\n            if hours >= 1 {\n                return \"\\(hours)h \\(minutes)m\"\n            } else if minutes >= 1 {\n                return \"\\(minutes)m \\(seconds)s\"\n            } else {\n                return \"\\(seconds)s\"\n            }\n        }\n    }\n#endif\n"
  },
  {
    "path": "Sources/ProgressTracker.swift",
    "content": "import Foundation\nimport TaggedTime\n\nstruct Progress {\n    let line: String?\n    let duration: Seconds<TimeInterval>\n}\n\nfinal class ProgressTracker: Sendable {\n    private let startTimestamp: Seconds<TimeInterval>\n\n    private init(startTimestamp: Seconds<TimeInterval>) {\n        self.startTimestamp = startTimestamp\n    }\n\n    static func start() -> ProgressTracker {\n        ProgressTracker(startTimestamp: Seconds(Date().timeIntervalSince1970))\n    }\n\n    func moveForward(_ line: String?) -> Progress {\n        let duration = Seconds(Date().timeIntervalSince1970) - startTimestamp\n        return Progress(line: line, duration: duration)\n    }\n}\n"
  },
  {
    "path": "Sources/String+ANSI.swift",
    "content": "import Foundation\n\nextension String {\n    private static let ansiRegex = try! NSRegularExpression(pattern: \"\\u{1B}(?:[@-Z\\\\-_]|\\\\[[0-?]*[ -/]*[@-~])\")\n\n    func withoutANSI() -> String {\n        let range = NSRange(startIndex ..< endIndex, in: self)\n        return Self.ansiRegex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: \"\")\n    }\n}\n"
  },
  {
    "path": "Sources/Task+Periodic.swift",
    "content": "import Foundation\nimport TaggedTime\n\nextension Task where Success == Never, Failure == any Error {\n    @discardableResult\n    static func periodic(interval: Milliseconds<UInt64>, operation: @Sendable @escaping () async throws -> Void) -> Task {\n        Task {\n            while true {\n                try Task<Never, Never>.checkCancellation()\n                try await operation()\n                try await Task<Never, Never>.sleep(nanoseconds: 1_000_000 * interval.rawValue)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/UnderProgressLineLogger.swift",
    "content": "import Foundation\n\nfinal class AboveProgressLineLogger: Sendable {\n    private let printers: PrintersHolder\n\n    init(printers: PrintersHolder) {\n        self.printers = printers\n    }\n\n    func log(_ text: String) async {\n        await printers.withPrinter { printer in\n            await log(printer: printer, text: text)\n        }\n    }\n\n    func logError(_ text: String) async {\n        await printers.withErrorsPrinter { errorsPrinter in\n            await log(printer: errorsPrinter, text: text)\n        }\n    }\n\n    private func log(printer: Printer, text: String) async {\n        if printer.wasWritten {\n            printer\n                .cursorUp()\n                .eraseLine()\n        }\n        printer\n            .writeln(text)\n            .writeln(\"\") // Add an empty line after the message for delete it by progress line controller\n            .flush()\n    }\n}\n"
  },
  {
    "path": "Sources/Version.swift",
    "content": "// This file is autogenerated. Do not edit.\nlet progressLineVersion = \"0.2.4\"\n"
  },
  {
    "path": "Sources/WindowSizeObserver.swift",
    "content": "import ConcurrencyExtras\nimport Foundation\n\nfinal class WindowSizeObserver: Sendable {\n    struct Size {\n        let width: Int\n        let height: Int\n    }\n\n    private let signalHandler: LockIsolated<UncheckedSendable<DispatchSourceSignal>?> = .init(nil)\n    private let _size: LockIsolated<Size> = .init(getTerminalSize())\n\n    var size: Size {\n        _size.value\n    }\n\n    static func startObserving() -> WindowSizeObserver? {\n        guard isTTY else {\n            return nil\n        }\n        let observer = WindowSizeObserver()\n        observer.setupSignalHandler()\n        return observer\n    }\n\n    private init() {}\n\n    private func setupSignalHandler() {\n        let sigwinch = SIGWINCH\n\n        let signalHandler = DispatchSource.makeSignalSource(signal: sigwinch)\n        signal(sigwinch, SIG_IGN)\n\n        signalHandler.setEventHandler { [weak self] in\n            guard let self else {\n                return\n            }\n            self.syncWindowSize()\n        }\n        signalHandler.resume()\n\n        let uncheckedSendable = UncheckedSendable(signalHandler)\n        self.signalHandler.setValue(uncheckedSendable)\n    }\n\n    private func syncWindowSize() {\n        _size.setValue(Self.getTerminalSize())\n    }\n\n    static func getTerminalSize() -> Size {\n        var w = winsize()\n        #if os(Linux)\n            _ = ioctl(STDOUT_FILENO, UInt(TIOCGWINSZ), &w)\n        #else\n            _ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w)\n        #endif\n        return Size(width: Int(w.ws_col), height: Int(w.ws_row))\n    }\n}\n"
  },
  {
    "path": "Sources/isTTY.swift",
    "content": "import Foundation\n\nlet isTTY = isatty(STDOUT_FILENO) != 0\n"
  },
  {
    "path": "Tests/assert.sh",
    "content": "#!/usr/bin/env bash\n#\n# Determine if the script is sourced or executed\nif [[ \"${BASH_SOURCE[0]}\" != \"${0}\" ]]; then\n    # Script is being sourced\n    SNAPSHOTS_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[1]}\")\" && pwd)\"\nelse\n    # Script is being executed\n    SNAPSHOTS_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nfi\n\n# takes a name for snapshot file and string to compare to reference.\n# If the snapshot file does not exist, it will be created.\n# If the snapshot file does exist, the string will be compared to the snapshot.\n# If the string is different, assertion will fail and print the diff.\n# For recording new snapshots, use the `SNAPSHOT_RECORD=true` environment variable.\nassert_snapshot() {\n  local snapshot_name=\"$1\"\n  local snapshot_value=\"$2\"\n  local snapshot_file=\"$SNAPSHOTS_DIR/snapshots/$snapshot_name.snapshot\"\n  mkdir -p \"$SNAPSHOTS_DIR\"\n\n  if [ \"$SNAPSHOT_RECORD\" = \"true\" ]; then\n    echo \"Recording snapshot $snapshot_name\" >&2\n    echo \"$snapshot_value\" > \"$snapshot_file\"\n    return\n  fi\n\n  if [ ! -f \"$snapshot_file\" ]; then\n    echo \"Snapshot $snapshot_name does not exist. Recording new snapshot.\" >&2\n    echo \"$snapshot_value\" > \"$snapshot_file\"\n    return\n  fi\n\n  local snapshot_diff=$(diff -u \"$snapshot_file\" <(echo \"$snapshot_value\"))\n  if [ -n \"$snapshot_diff\" ]; then\n    echo \"Snapshot $snapshot_name does not match reference.\" >&2\n    echo \"$snapshot_diff\"\n    exit 1\n  fi\n\n  echo \"Snapshot $snapshot_name matches reference.\" >&2\n}\n"
  },
  {
    "path": "Tests/integration_tests.sh",
    "content": "#!/usr/bin/env bash\n\nset -eo pipefail\n\nTESTS_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" &>/dev/null && pwd)\"\n\nsource \"$TESTS_DIR/assert.sh\"\n\n# Parse arg: <executable path>\nexecutable_path=\"$1\"\nif [ -z \"$executable_path\" ]; then\n  echo \"Usage: $0 <executable path>\"\n  exit 1\nfi\n\n# Prepare test data\n\ntest_data_producer_config=\"{\n  \\\"chunk_count\\\": 30,\n  \\\"chunk_size\\\": 3,\n  \\\"write_delay\\\": 10,\n}\"\ntest_data_producer_config_file=\"/tmp/progressline_test_data_producer_config.json\"\necho \"$test_data_producer_config\" > \"$test_data_producer_config_file\"\n\n# warmup test data producer\nswift \"$TESTS_DIR\"/test_data_producer.swift $test_data_producer_config_file > /dev/null\n\ngenerate_test_output=\"swift $TESTS_DIR/test_data_producer.swift $test_data_producer_config_file\"\n\n# Test default mode\n\noutput=$($generate_test_output | \"$executable_path\" --test-mode)\nassert_snapshot \"default\" \"$output\"\n\n# Test static text mode\n\noutput=$($generate_test_output | \"$executable_path\" --test-mode --static-text \"Static text\")\nassert_snapshot \"static_text\" \"$output\"\n\n# Test default mode with save original log\n\noutput=$($generate_test_output | \"$executable_path\" --test-mode --original-log-path /tmp/progressline_test_original_log.txt)\nassert_snapshot \"default_with_original_log\" \"$output\"\nassert_snapshot \"default_with_original_log_original_log\" \"$(cat /tmp/progressline_test_original_log.txt)\"\nrm /tmp/progressline_test_original_log.txt\n\n# Test log matches\n\noutput=$($generate_test_output | \"$executable_path\" --test-mode --log-matches \"Chunk number: \\d+[1-5]{1}\")\nassert_snapshot \"log_matches\" \"$output\"\n\n# Test log all\n\noutput=$($generate_test_output | \"$executable_path\" --test-mode --log-all)\nassert_snapshot \"log_all\" \"$output\"\n"
  },
  {
    "path": "Tests/snapshots/default.snapshot",
    "content": "<activity> <duration> ❯\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3\n\u001b[1A\u001b[2K✓ <duration> ❯ Chunk number: 30, Chunk Line: 3\n"
  },
  {
    "path": "Tests/snapshots/default_with_original_log.snapshot",
    "content": "<activity> <duration> ❯\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3\n\u001b[1A\u001b[2K✓ <duration> ❯ Chunk number: 30, Chunk Line: 3\n"
  },
  {
    "path": "Tests/snapshots/default_with_original_log_original_log.snapshot",
    "content": "Chunk number: 1, Chunk Line: 1\nChunk number: 1, Chunk Line: 2\nChunk number: 1, Chunk Line: 3\nChunk number: 2, Chunk Line: 1\nChunk number: 2, Chunk Line: 2\nChunk number: 2, Chunk Line: 3\nChunk number: 3, Chunk Line: 1\nChunk number: 3, Chunk Line: 2\nChunk number: 3, Chunk Line: 3\nChunk number: 4, Chunk Line: 1\nChunk number: 4, Chunk Line: 2\nChunk number: 4, Chunk Line: 3\nChunk number: 5, Chunk Line: 1\nChunk number: 5, Chunk Line: 2\nChunk number: 5, Chunk Line: 3\nChunk number: 6, Chunk Line: 1\nChunk number: 6, Chunk Line: 2\nChunk number: 6, Chunk Line: 3\nChunk number: 7, Chunk Line: 1\nChunk number: 7, Chunk Line: 2\nChunk number: 7, Chunk Line: 3\nChunk number: 8, Chunk Line: 1\nChunk number: 8, Chunk Line: 2\nChunk number: 8, Chunk Line: 3\nChunk number: 9, Chunk Line: 1\nChunk number: 9, Chunk Line: 2\nChunk number: 9, Chunk Line: 3\nChunk number: 10, Chunk Line: 1\nChunk number: 10, Chunk Line: 2\nChunk number: 10, Chunk Line: 3\nChunk number: 11, Chunk Line: 1\nChunk number: 11, Chunk Line: 2\nChunk number: 11, Chunk Line: 3\nChunk number: 12, Chunk Line: 1\nChunk number: 12, Chunk Line: 2\nChunk number: 12, Chunk Line: 3\nChunk number: 13, Chunk Line: 1\nChunk number: 13, Chunk Line: 2\nChunk number: 13, Chunk Line: 3\nChunk number: 14, Chunk Line: 1\nChunk number: 14, Chunk Line: 2\nChunk number: 14, Chunk Line: 3\nChunk number: 15, Chunk Line: 1\nChunk number: 15, Chunk Line: 2\nChunk number: 15, Chunk Line: 3\nChunk number: 16, Chunk Line: 1\nChunk number: 16, Chunk Line: 2\nChunk number: 16, Chunk Line: 3\nChunk number: 17, Chunk Line: 1\nChunk number: 17, Chunk Line: 2\nChunk number: 17, Chunk Line: 3\nChunk number: 18, Chunk Line: 1\nChunk number: 18, Chunk Line: 2\nChunk number: 18, Chunk Line: 3\nChunk number: 19, Chunk Line: 1\nChunk number: 19, Chunk Line: 2\nChunk number: 19, Chunk Line: 3\nChunk number: 20, Chunk Line: 1\nChunk number: 20, Chunk Line: 2\nChunk number: 20, Chunk Line: 3\nChunk number: 21, Chunk Line: 1\nChunk number: 21, Chunk Line: 2\nChunk number: 21, Chunk Line: 3\nChunk number: 22, Chunk Line: 1\nChunk number: 22, Chunk Line: 2\nChunk number: 22, Chunk Line: 3\nChunk number: 23, Chunk Line: 1\nChunk number: 23, Chunk Line: 2\nChunk number: 23, Chunk Line: 3\nChunk number: 24, Chunk Line: 1\nChunk number: 24, Chunk Line: 2\nChunk number: 24, Chunk Line: 3\nChunk number: 25, Chunk Line: 1\nChunk number: 25, Chunk Line: 2\nChunk number: 25, Chunk Line: 3\nChunk number: 26, Chunk Line: 1\nChunk number: 26, Chunk Line: 2\nChunk number: 26, Chunk Line: 3\nChunk number: 27, Chunk Line: 1\nChunk number: 27, Chunk Line: 2\nChunk number: 27, Chunk Line: 3\nChunk number: 28, Chunk Line: 1\nChunk number: 28, Chunk Line: 2\nChunk number: 28, Chunk Line: 3\nChunk number: 29, Chunk Line: 1\nChunk number: 29, Chunk Line: 2\nChunk number: 29, Chunk Line: 3\nChunk number: 30, Chunk Line: 1\nChunk number: 30, Chunk Line: 2\nChunk number: 30, Chunk Line: 3\n"
  },
  {
    "path": "Tests/snapshots/log_all.snapshot",
    "content": "<activity> <duration> ❯\n\u001b[1A\u001b[2KChunk number: 1, Chunk Line: 1\nChunk number: 1, Chunk Line: 2\nChunk number: 1, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 2, Chunk Line: 1\nChunk number: 2, Chunk Line: 2\nChunk number: 2, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 3, Chunk Line: 1\nChunk number: 3, Chunk Line: 2\nChunk number: 3, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 4, Chunk Line: 1\nChunk number: 4, Chunk Line: 2\nChunk number: 4, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 5, Chunk Line: 1\nChunk number: 5, Chunk Line: 2\nChunk number: 5, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 6, Chunk Line: 1\nChunk number: 6, Chunk Line: 2\nChunk number: 6, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 7, Chunk Line: 1\nChunk number: 7, Chunk Line: 2\nChunk number: 7, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 8, Chunk Line: 1\nChunk number: 8, Chunk Line: 2\nChunk number: 8, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 9, Chunk Line: 1\nChunk number: 9, Chunk Line: 2\nChunk number: 9, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 10, Chunk Line: 1\nChunk number: 10, Chunk Line: 2\nChunk number: 10, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 11, Chunk Line: 1\nChunk number: 11, Chunk Line: 2\nChunk number: 11, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 12, Chunk Line: 1\nChunk number: 12, Chunk Line: 2\nChunk number: 12, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 13, Chunk Line: 1\nChunk number: 13, Chunk Line: 2\nChunk number: 13, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 14, Chunk Line: 1\nChunk number: 14, Chunk Line: 2\nChunk number: 14, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 15, Chunk Line: 1\nChunk number: 15, Chunk Line: 2\nChunk number: 15, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 16, Chunk Line: 1\nChunk number: 16, Chunk Line: 2\nChunk number: 16, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 17, Chunk Line: 1\nChunk number: 17, Chunk Line: 2\nChunk number: 17, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 18, Chunk Line: 1\nChunk number: 18, Chunk Line: 2\nChunk number: 18, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 19, Chunk Line: 1\nChunk number: 19, Chunk Line: 2\nChunk number: 19, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 20, Chunk Line: 1\nChunk number: 20, Chunk Line: 2\nChunk number: 20, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 21, Chunk Line: 1\nChunk number: 21, Chunk Line: 2\nChunk number: 21, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 22, Chunk Line: 1\nChunk number: 22, Chunk Line: 2\nChunk number: 22, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 23, Chunk Line: 1\nChunk number: 23, Chunk Line: 2\nChunk number: 23, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 24, Chunk Line: 1\nChunk number: 24, Chunk Line: 2\nChunk number: 24, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 25, Chunk Line: 1\nChunk number: 25, Chunk Line: 2\nChunk number: 25, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 26, Chunk Line: 1\nChunk number: 26, Chunk Line: 2\nChunk number: 26, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 27, Chunk Line: 1\nChunk number: 27, Chunk Line: 2\nChunk number: 27, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 28, Chunk Line: 1\nChunk number: 28, Chunk Line: 2\nChunk number: 28, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 29, Chunk Line: 1\nChunk number: 29, Chunk Line: 2\nChunk number: 29, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 30, Chunk Line: 1\nChunk number: 30, Chunk Line: 2\nChunk number: 30, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3\n\u001b[1A\u001b[2K✓ <duration> ❯ Chunk number: 30, Chunk Line: 3\n"
  },
  {
    "path": "Tests/snapshots/log_matches.snapshot",
    "content": "<activity> <duration> ❯\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 11, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 11, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 11, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 12, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 12, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 12, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 13, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 13, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 13, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 14, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 14, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 14, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 15, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 15, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 15, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 21, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 21, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 21, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 22, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 22, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 22, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 23, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 23, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 23, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 24, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 24, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 24, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3\n\u001b[1A\u001b[2KChunk number: 25, Chunk Line: 1\n\n\u001b[1A\u001b[2KChunk number: 25, Chunk Line: 2\n\n\u001b[1A\u001b[2KChunk number: 25, Chunk Line: 3\n\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3\n\u001b[1A\u001b[2K✓ <duration> ❯ Chunk number: 30, Chunk Line: 3\n"
  },
  {
    "path": "Tests/snapshots/static_text.snapshot",
    "content": "<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K✓ <duration> ❯ Static text\n"
  },
  {
    "path": "Tests/test_data_producer.swift",
    "content": "/*\n Generates test data for the app.\n Features:\n - iterates over the given number and prints the iteration number\n - prints with a delay if the delay is greater than 0\n - can produce a given number of lines per iteration\n Usage:\n    test_data_producer.swift <iterations> [<delay in ms>] [<lines>]\n */\n\nimport Foundation\n\nstruct Configuration: Decodable {\n    let chunkCount: Int\n    let chunkSize: Int\n    let writeDelay: TimeInterval\n\n    enum CodingKeys: String, CodingKey {\n        case chunkCount = \"chunk_count\"\n        case chunkSize = \"chunk_size\"\n        case writeDelay = \"write_delay\"\n    }\n}\n\nlet arguments = CommandLine.arguments.dropFirst()\nguard arguments.count >= 1 else {\n    print(\"Usage: \\(CommandLine.arguments.first!) <configuration file path>\")\n    exit(1)\n}\n\nlet filePath = arguments.first!\nlet configuration = try JSONDecoder().decode(Configuration.self, from: try Data(contentsOf: URL(fileURLWithPath: filePath)))\n\nfor i in 0 ..< configuration.chunkCount {\n    let data = (0 ..< configuration.chunkSize)\n        .map { \"Chunk number: \\(i + 1), Chunk Line: \\($0 + 1)\" }\n        .joined(separator: \"\\n\")\n    print(data)\n    fflush(stdout)\n    if configuration.writeDelay > 0 {\n        usleep(useconds_t(configuration.writeDelay * 1000))\n    }\n}\n"
  },
  {
    "path": "cliff.toml",
    "content": "# git-cliff ~ configuration file\n# https://git-cliff.org/docs/configuration\n\n[remote.github]\nowner = \"kattouf\"\nrepo = \"ProgressLine\"\n# token = \"\"\n\n[changelog]\n# changelog header\nheader = \"\"\"\n# Changelog\\n\n\"\"\"\n# template for the changelog body\n# https://keats.github.io/tera/docs/#introduction\nbody = \"\"\"\n{% if version %}\\\n    {% if previous.version %}\\\n        ## [{{ version | trim_start_matches(pat=\"v\") }}]({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n    {% else %}\\\n        ## {{ version | trim_start_matches(pat=\"v\") }} - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n    {% endif %}\\\n{% else %}\\\n    ## Unreleased\n{% endif %}\\\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n    ### {{ group | striptags | trim | upper_first }}\n\n    {% for commit in commits\n    | filter(attribute=\"scope\")\n    | sort(attribute=\"scope\") -%}\n        {% if commit.scope -%}\n        * {{self::commit(commit=commit)}}\\\n        {% endif -%}\n    {% endfor -%}\n    {% for commit in commits -%}\n        {% if commit.scope -%}\n        {% else -%}\n          * {{self::commit(commit=commit)}}\\\n        {% endif -%}\n    {% endfor -%}\n{% endfor %}\n\n{%- if github -%}\n{% if github.contributors | filter(attribute=\"is_first_time\", value=true) | length != 0 %}\n  {% raw %}\\n{% endraw -%}\n  ## New Contributors\n{%- endif %}\\\n{% for contributor in github.contributors | filter(attribute=\"is_first_time\", value=true) %}\n  * @{{ contributor.username }} made their first contribution\n    {%- if contributor.pr_number %} in \\\n      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \\\n    {%- endif %}\n{%- endfor -%}\n{%- endif -%}\n\n{% if version %}\n    {% if previous.version %}\n      **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}\n    {% endif %}\n{% else -%}\n  {% raw %}\\n{% endraw %}\n{% endif %}\n\n{%- macro remote_url() -%}\n  https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\n{%- endmacro -%}\n\n{% macro commit(commit) -%}\n{% if commit.scope %}**({{commit.scope}})** {% endif -%}\n{% if commit.breaking %}**breaking** {% endif -%}\n{{ commit.message | split(pat=\"\\n\") | first | trim }} by \\\n{% if commit.remote.username %}[@{{commit.remote.username}}](https://github.com/{{commit.remote.username}})\\\n{% else %}{{commit.author.name}}{% endif %} in \\\n{% if commit.remote.pr_number %}[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})\\\n{% else %}[{{ commit.id | truncate(length=7, end=\"\") }}]({{ self::remote_url() }}/commit/{{ commit.id }})\\\n{%- endif %}\n{% endmacro commit -%}\n\"\"\"\n# template for the changelog footer\nfooter = \"\"\"\n<!-- generated by git-cliff -->\n\"\"\"\n# remove the leading and trailing whitespace from the template\ntrim = true\n# postprocessors\npostprocessors = []\n\n[git]\n# parse the commits based on https://www.conventionalcommits.org\nconventional_commits = true\n# filter out the commits that are not conventional\nfilter_unconventional = false\n# process each line of a commit as an individual commit\nsplit_commits = false\n# regex for preprocessing the commit messages\ncommit_preprocessors = [\n  # remove issue numbers from commits\n  { pattern = '\\((\\w+\\s)?#([0-9]+)\\)', replace = \"\" },\n]\n# regex for parsing and grouping commits\ncommit_parsers = [\n  { message = '^chore\\(release\\): Bump version to', skip = true },\n  { message = '^(chore|fix)\\(deps\\):', group = \"<!-- 99 -->:package: Dependency Updates\", scope = \"\" },\n  { message = '^feat', group = \"<!-- 00 -->:rocket: Features\" },\n  { message = '^fix', group = \"<!-- 01 -->:bug: Bug Fixes\" },\n  { message = '^test', group = \"<!-- 02 -->:test_tube: Testing\" },\n  { message = '^perf', group = \"<!-- 03 -->:zap: Performance\" },\n  { message = '^refactor', group = \"<!-- 04 -->:tractor: Refactoring\" },\n  { message = '^doc', group = \"<!-- 05 -->:books: Documentation\" },\n  { body = '.*security', group = \"<!-- 06 -->:shield: Security\" },\n  { message = '^project', group = \"<!-- 07 -->:file_folder: Project\" },\n  { message = '^revert', group = \"<!-- 39 -->:leftwards_arrow_with_hook: Revert\" },\n  { message = '.', group = \"<!-- 49 -->:card_index_dividers: Other Changes\" },\n]\n# filter out the commits that are not matched by commit parsers\nfilter_commits = false\n# sort the tags topologically\ntopo_order = false\n# sort the commits inside sections by oldest/newest order\nsort_commits = \"newest\"\n"
  }
]