Full Code of kattouf/ProgressLine for AI

main 3ef298aeb9d8 cached
48 files
77.9 KB
22.7k tokens
1 requests
Download .txt
Repository: kattouf/ProgressLine
Branch: main
Commit: 3ef298aeb9d8
Files: 48
Total size: 77.9 KB

Directory structure:
gitextract_76fia48v/

├── .github/
│   ├── demo_activity_style.tape
│   ├── demo_progressline_output.tape
│   ├── demo_standard_output.tape
│   └── workflows/
│       ├── checks.yml
│       └── semantic-pr-lint.yml
├── .gitignore
├── .sake.yml
├── .swiftformat
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── SakeApp/
│   ├── .gitignore
│   ├── BrewCommands.swift
│   ├── Package.resolved
│   ├── Package.swift
│   ├── ReleaseCommands.swift
│   └── Sakefile.swift
├── Sources/
│   ├── ANSI.swift
│   ├── ActivityIndicator+CommandArgument.swift
│   ├── ActivityIndicator.swift
│   ├── ErrorMessage.swift
│   ├── FileHandler+AsyncStream.swift
│   ├── LogAllController.swift
│   ├── MatchesController.swift
│   ├── OriginalLogController.swift
│   ├── Printer.swift
│   ├── PrintersHolder.swift
│   ├── ProgressLine.swift
│   ├── ProgressLineController.swift
│   ├── ProgressLineFormatter.swift
│   ├── ProgressTracker.swift
│   ├── String+ANSI.swift
│   ├── Task+Periodic.swift
│   ├── UnderProgressLineLogger.swift
│   ├── Version.swift
│   ├── WindowSizeObserver.swift
│   └── isTTY.swift
├── Tests/
│   ├── assert.sh
│   ├── integration_tests.sh
│   ├── snapshots/
│   │   ├── default.snapshot
│   │   ├── default_with_original_log.snapshot
│   │   ├── default_with_original_log_original_log.snapshot
│   │   ├── log_all.snapshot
│   │   ├── log_matches.snapshot
│   │   └── static_text.snapshot
│   └── test_data_producer.swift
└── cliff.toml

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/demo_activity_style.tape
================================================
Output out.gif

Set Shell "zsh"
Set FontSize 32
Set Width 660
Set Height 300
Set TypingSpeed 75ms

Type "progressline -s snake"

Enter

Sleep 8


================================================
FILE: .github/demo_progressline_output.tape
================================================
Output progressline_output.gif

Require progressline

Set Shell "zsh"
Set FontSize 32
Set Width 1600
Set Height 400
Set TypingSpeed 75ms

Hide
Type "make long-running-command"
Show
Sleep 1
Type " | progressline"

Enter

Sleep 11


================================================
FILE: .github/demo_standard_output.tape
================================================
Output standard_output.gif

Require progressline

Set Shell "zsh"
Set FontSize 32
Set Width 1600
Set Height 400
Set TypingSpeed 75ms

Hide
Type "make long-running-command"
Show
Sleep 1

Enter

Sleep 11


================================================
FILE: .github/workflows/checks.yml
================================================
name: Checks
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
jobs:
  build:
    name: Integration tests
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: swift-actions/setup-swift@v2
        with:
          swift-version: "5.10"
      - name: Prepare test build
        run: swift build
      - name: Run tests
        run: ./Tests/integration_tests.sh .build/debug/progressline
  lint:
    runs-on: macos-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Run SwiftFormat Linting
        run: swiftformat Sources SakeApp Package.swift --lint


================================================
FILE: .github/workflows/semantic-pr-lint.yml
================================================
name: "Lint PR"

on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize
      - reopened

permissions:
  pull-requests: read

jobs:
  main:
    name: Validate PR title
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          types: |
            fix
            feat
            chore
            feat
            fix
            test
            perf
            refactor
            doc
            project
            revert


================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/.index-build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
TODO.md


================================================
FILE: .sake.yml
================================================
case_converting_strategy: toSnakeCase


================================================
FILE: .swiftformat
================================================
--swiftversion 5.10

--exclude .build,.index-build,SakeApp/.build,SakeApp/.index-build

--maxwidth 140
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first

--enable isEmpty,wrapSwitchCases,wrapConditionalBodies,wrapEnumCases
--disable redundantRawValues,redundantSelf,redundantStaticSelf,redundantType


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Vasilii Ianguzin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Package.resolved
================================================
{
  "originHash" : "e759c45271facbb3650829c703702a2ac4817adf75a8116cc3d77eae8e3d3bae",
  "pins" : [
    {
      "identity" : "swift-argument-parser",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-argument-parser.git",
      "state" : {
        "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b",
        "version" : "1.4.0"
      }
    },
    {
      "identity" : "swift-concurrency-extras",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/pointfreeco/swift-concurrency-extras.git",
      "state" : {
        "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
        "version" : "1.1.0"
      }
    },
    {
      "identity" : "swift-tagged",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/pointfreeco/swift-tagged.git",
      "state" : {
        "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b",
        "version" : "0.10.0"
      }
    }
  ],
  "version" : 3
}


================================================
FILE: Package.swift
================================================
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ProgressLine",
    platforms: [
        .macOS(.v10_15),
    ],
    products: [
        .executable(name: "progressline", targets: ["progressline"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"),
        .package(url: "https://github.com/pointfreeco/swift-tagged.git", from: "0.10.0"),
        .package(url: "https://github.com/pointfreeco/swift-concurrency-extras.git", from: "1.1.0"),
    ],
    targets: [
        .executableTarget(
            name: "progressline",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .product(name: "TaggedTime", package: "swift-tagged"),
                .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
            ], swiftSettings: [
                .enableExperimentalFeature("StrictConcurrency"),
            ]
        ),
    ]
)


================================================
FILE: README.md
================================================
## ProgressLine

![](https://img.shields.io/badge/Platform-macOS-6464aa)
![](https://img.shields.io/badge/Platform-Linux-6464aa)
[![Latest Release](https://img.shields.io/github/release/kattouf/ProgressLine.svg)](https://github.com/kattouf/ProgressLine/releases/latest)
![](https://github.com/kattouf/ProgressLine/actions/workflows/checks.yml/badge.svg?branch=main)

Track commands progress in a compact one-line format.

| ⏳ `progressline` output |
|:--:|
| ![](./.github/progressline_output.gif) |

| 📝 standard output |
|:--:|
| ![](./.github/standard_output.gif) |

[Usage](#usage) • [Features](#features) • [Installation](#installation)

## Usage

Simply pipe your command output into `progressline` to start tracking:

```sh
long-running-command | progressline
```

If 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`:

``` sh
long-running-command 2>&1 | progressline
```

## Features

### Change activity indicator styles

ProgressLine offers different styles to represent activity, they can be changed using `-s, --activity-style` option:

``` sh
long-running-command | progressline --activity-style snake
```

Available styles:

| dots (Default) | snake | [kitt](https://en.wikipedia.org/wiki/KITT) | spinner |
|:--:|:--:|:--:|:--:|
| ![](./.github/activity_style_dots.gif) | ![](./.github/activity_style_snake.gif) | ![](./.github/activity_style_kitt.gif) | ![](./.github/activity_style_spinner.gif) |

### Replace log output with custom text

If 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.

``` sh
long-running-command | progressline --static-text "Updating sources..."
```

### Highlight important lines

Log specific stdin lines above the progress line using the `-m, --log-matches` option:

``` sh
long-running-command | progressline --log-matches "regex-1" --log-matches "regex-2"
```

### Use progress line as an addition to standard output

Log all stdin data above the progress line using the `-a, --log-all` option:

```sh
long-running-command | progressline --log-all
```

### Save original log

You have two options for saving the full original log:

1. Using [tee](https://en.wikipedia.org/wiki/Tee_(command))

``` sh
long-running-command | tee original-log.txt | progressline
```

2. Using `-l, --original-log-path` option:

``` sh
long-running-command | progressline --original-log-path original-log.txt
```

## Installation

### [Homebrew](https://brew.sh) (macOS / Linux)

``` sh
brew install progressline
```

</details>

### [Mint](https://github.com/yonaskolb/Mint) (macOS)

``` sh
mint install kattouf/ProgressLine
```

### [Mise](https://mise.jdx.dev) (macOS)

``` sh
mise use -g spm:kattouf/ProgressLine
```

### Manual Installation (macOS / Linux)

Download the binary for your platform from the [releases page](https://github.com/kattouf/ProgressLine/releases), and place it in your executable path.

## Contributing

Feel free to open a pull request or a discussion.


================================================
FILE: SakeApp/.gitignore
================================================
.DS_Store
/.build
/.index-build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

================================================
FILE: SakeApp/BrewCommands.swift
================================================
import Sake
import SwiftShell

@CommandGroup
struct BrewCommands {
    static var ensureSwiftFormatInstalled: Command {
        Command(
            description: "Ensure swiftformat is installed",
            skipIf: { _ in
                run("which", "swiftformat").succeeded
            },
            run: { _ in
                try runAndPrint("brew", "install", "swiftformat")
            }
        )
    }

    static var ensureGhInstalled: Command {
        Command(
            description: "Ensure gh is installed",
            skipIf: { _ in
                run("which", "gh").succeeded
            },
            run: { _ in
                try runAndPrint("brew", "install", "gh")
            }
        )
    }

    static var ensureGitCliffInstalled: Command {
        Command(
            description: "Ensure git-cliff is installed",
            skipIf: { _ in
                run("which", "git-cliff").succeeded
            },
            run: { _ in
                try runAndPrint("brew", "install", "git-cliff")
            }
        )
    }
}


================================================
FILE: SakeApp/Package.resolved
================================================
{
  "originHash" : "564ae29a93959e0a64ff9ad1a401e5db179007f3ecef20571f852606328631f6",
  "pins" : [
    {
      "identity" : "sake",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/kattouf/Sake",
      "state" : {
        "revision" : "f2c91c8ecb4f67f0c565b081deb7d180761a21d7",
        "version" : "0.2.2"
      }
    },
    {
      "identity" : "swift-argument-parser",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-argument-parser.git",
      "state" : {
        "revision" : "41982a3656a71c768319979febd796c6fd111d5c",
        "version" : "1.5.0"
      }
    },
    {
      "identity" : "swift-syntax",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-syntax",
      "state" : {
        "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
        "version" : "509.1.1"
      }
    },
    {
      "identity" : "swiftshell",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/kareman/SwiftShell",
      "state" : {
        "revision" : "99680b2efc7c7dbcace1da0b3979d266f02e213c",
        "version" : "5.1.0"
      }
    },
    {
      "identity" : "yams",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/jpsim/Yams.git",
      "state" : {
        "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d",
        "version" : "5.1.3"
      }
    }
  ],
  "version" : 3
}


================================================
FILE: SakeApp/Package.swift
================================================
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
    name: "SakeApp",
    platforms: [.macOS(.v10_15)], // Required by SwiftSyntax for the macro feature in Sake
    products: [
        .executable(name: "SakeApp", targets: ["SakeApp"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
        .package(url: "https://github.com/kattouf/Sake", from: "0.1.0"),
        .package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0"),
    ],
    targets: [
        .executableTarget(
            name: "SakeApp",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                "Sake",
                "SwiftShell",
            ],
            path: "."
        ),
    ]
)


================================================
FILE: SakeApp/ReleaseCommands.swift
================================================
import ArgumentParser
import CryptoKit
import Foundation
import Sake
import SwiftShell

@CommandGroup
struct ReleaseCommands {
    private struct BuildTarget {
        enum Arch {
            case x86
            case arm
        }

        enum OS {
            case macos
            case linux
        }

        let arch: Arch
        let os: OS

        var triple: String {
            switch (arch, os) {
            case (.x86, .macos): "x86_64-apple-macosx"
            case (.arm, .macos): "arm64-apple-macosx"
            case (.x86, .linux): "x86_64-unknown-linux-gnu"
            case (.arm, .linux): "aarch64-unknown-linux-gnu"
            }
        }
    }

    private enum Constants {
        static let buildArtifactsDirectory = ".build/artifacts"
        static let swiftVersion = "6.0"
        static let buildTargets: [BuildTarget] = [
            .init(arch: .arm, os: .macos),
            .init(arch: .x86, os: .macos),
            .init(arch: .x86, os: .linux),
            .init(arch: .arm, os: .linux),
        ]
        static let executableName = "progressline"
    }

    private struct ReleaseArguments: ParsableArguments {
        @Argument(help: "Version number")
        var version: String

        func validate() throws {
            guard version.range(of: #"^\d+\.\d+\.\d+$"#, options: .regularExpression) != nil else {
                throw ValidationError("Invalid version number. Should be in the format 'x.y.z'")
            }
        }
    }

    public static var brewRelease: Command {
        Command(
            description: "Brew to Homebrew",
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version
                try runAndPrint("brew", "bump-formula-pr", "--version=\(version)", "progressline")
            }
        )
    }

    public static var githubRelease: Command {
        Command(
            description: "Release to GitHub",
            dependencies: [
                bumpVersion,
                cleanReleaseArtifacts,
                buildReleaseArtifacts,
                calculateBuildArtifactsSha256,
                createAndPushTag,
                generateReleaseNotes,
                draftReleaseWithArtifacts,
            ]
        )
    }

    static var bumpVersion: Command {
        Command(
            description: "Bump version",
            skipIf: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version
                let versionFilePath = "Sources/Version.swift"
                let currentVersion = try String(contentsOfFile: versionFilePath)
                    .split(separator: "\"")[1]
                if currentVersion == version {
                    print("Version is already \(version). Skipping...")
                    return true
                } else {
                    return false
                }
            },
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version
                let versionFilePath = "Sources/Version.swift"
                let versionFileContent = """
                // This file is autogenerated. Do not edit.
                let progressLineVersion = "\(version)"

                """
                try versionFileContent.write(toFile: versionFilePath, atomically: true, encoding: .utf8)

                try runAndPrint("git", "add", versionFilePath)
                try runAndPrint("git", "commit", "-m", "chore(release): Bump version to \(version)")
                print("Version bumped to \(version)")
            }
        )
    }

    static var cleanReleaseArtifacts: Command {
        Command(
            description: "Clean release artifacts",
            run: { _ in
                try? runAndPrint("rm", "-rf", Constants.buildArtifactsDirectory)
            }
        )
    }

    static var buildReleaseArtifacts: Command {
        Command(
            description: "Build release artifacts",
            skipIf: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()
                let version = arguments.version

                let targetsWithExistingArtifacts = Constants.buildTargets.filter { target in
                    let archivePath = executableArchivePath(target: target, version: version)
                    return FileManager.default.fileExists(atPath: archivePath)
                }
                if targetsWithExistingArtifacts.count == Constants.buildTargets.count {
                    print("Release artifacts already exist. Skipping...")
                    return true
                } else {
                    context.storage["existing-artifacts-triples"] = targetsWithExistingArtifacts.map(\.triple)
                    return false
                }
            },
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()
                let version = arguments.version

                try FileManager.default.createDirectory(
                    atPath: Constants.buildArtifactsDirectory,
                    withIntermediateDirectories: true,
                    attributes: nil
                )
                let existingArtifactsTriples = context.storage["existing-artifacts-triples"] as? [String] ?? []
                for target in Constants.buildTargets {
                    if existingArtifactsTriples.contains(target.triple) {
                        print("Skipping \(target.triple) as artifacts already exist")
                        continue
                    }
                    let (swiftBuild, swiftClean, strip, zip) = {
                        let buildFlags = ["--disable-sandbox", "--configuration", "release", "--triple", target.triple]
                        if target.os == .linux {
                            let platform = target.arch == .arm ? "linux/arm64" : "linux/amd64"
                            let dockerExec =
                                "docker run --rm --volume \(context.projectRoot):/workdir --workdir /workdir --platform \(platform) swift:\(Constants.swiftVersion)"
                            let buildFlags = (buildFlags + ["--static-swift-stdlib"]).joined(separator: " ")
                            return (
                                "\(dockerExec) swift build \(buildFlags)",
                                "\(dockerExec) swift package clean",
                                "\(dockerExec) strip -s",
                                "zip -j"
                            )
                        } else {
                            let buildFlags = buildFlags.joined(separator: " ")
                            return (
                                "swift build \(buildFlags)",
                                "swift package clean",
                                "strip -rSTx",
                                "zip -j"
                            )
                        }
                    }()

                    try runAndPrint(bash: swiftClean)
                    try runAndPrint(bash: swiftBuild)

                    let binPath: String = run(bash: "\(swiftBuild) --show-bin-path").stdout
                    if binPath.isEmpty {
                        throw NSError(domain: "Fail to get bin path", code: -999)
                    }
                    let executablePath = binPath + "/\(Constants.executableName)"

                    try runAndPrint(bash: "\(strip) \(executablePath)")

                    let executableArchivePath = executableArchivePath(target: target, version: version)
                    try runAndPrint(
                        bash: "\(zip) \(executableArchivePath) \(executablePath.replacingOccurrences(of: "/workdir", with: context.projectRoot))"
                    )
                }

                print("Release artifacts built successfully at '\(Constants.buildArtifactsDirectory)'")
            }
        )
    }

    static var calculateBuildArtifactsSha256: Command {
        @Sendable
        func shasumFilePath(version: String) -> String {
            ".build/artifacts/shasum-\(version)"
        }

        return Command(
            description: "Calculate SHA-256 checksums for build artifacts",
            skipIf: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()
                let version = arguments.version

                let shasumFilePath = shasumFilePath(version: version)

                return FileManager.default.fileExists(atPath: shasumFilePath)
            },
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()
                let version = arguments.version

                var shasumResults = [String]()
                for target in Constants.buildTargets {
                    let archivePath = executableArchivePath(target: target, version: version)
                    let file = FileHandle(forReadingAtPath: archivePath)!
                    let shasum = SHA256.hash(data: file.readDataToEndOfFile())
                    let shasumString = shasum.compactMap { String(format: "%02x", $0) }.joined()
                    shasumResults.append("\(shasumString)  \(archivePath)")
                }
                FileManager.default.createFile(
                    atPath: shasumFilePath(version: version),
                    contents: shasumResults.joined(separator: "\n").data(using: .utf8)
                )
            }
        )
    }

    static var createAndPushTag: Command {
        Command(
            description: "Create and push a tag",
            skipIf: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version

                let grepResult = run(bash: "git tag | grep \(arguments.version)")
                if grepResult.succeeded {
                    print("Tag \(version) already exists. Skipping...")
                    return true
                } else {
                    return false
                }
            },
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version

                print("Creating and pushing tag \(version)")
                try runAndPrint("git", "tag", version)
                try runAndPrint("git", "push", "origin", "tag", version)
                try runAndPrint("git", "push") // push local changes like version bump
            }
        )
    }

    static var generateReleaseNotes: Command {
        Command(
            description: "Generate release notes",
            dependencies: [BrewCommands.ensureGitCliffInstalled],
            skipIf: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version
                let releaseNotesPath = releaseNotesPath(version: version)
                if FileManager.default.fileExists(atPath: releaseNotesPath) {
                    print("Release notes for \(version) already exist at \(releaseNotesPath). Skipping...")
                    return true
                } else {
                    return false
                }
            },
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let version = arguments.version
                let releaseNotesPath = releaseNotesPath(version: version)
                try runAndPrint("git", "cliff", "--latest", "--strip=all", "--tag", version, "--output", releaseNotesPath)
                print("Release notes generated at \(releaseNotesPath)")
            }
        )
    }

    static var draftReleaseWithArtifacts: Command {
        Command(
            description: "Draft a release on GitHub",
            dependencies: [BrewCommands.ensureGhInstalled],
            skipIf: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                let tagName = arguments.version
                let ghViewResult = run(bash: "gh release view \(tagName)")
                if ghViewResult.succeeded {
                    print("Release \(tagName) already exists. Skipping...")
                    return true
                } else {
                    return false
                }
            },
            run: { context in
                let arguments = try ReleaseArguments.parse(context.arguments)
                try arguments.validate()

                print("Drafting release \(arguments.version) on GitHub")
                let tagName = arguments.version
                let releaseTitle = arguments.version
                let draftReleaseCommand =
                    "gh release create \(tagName) \(Constants.buildArtifactsDirectory)/*.zip --title '\(releaseTitle)' --draft --verify-tag --notes-file \(releaseNotesPath(version: tagName))"
                try runAndPrint(bash: draftReleaseCommand)
            }
        )
    }

    private static func executableArchivePath(target: BuildTarget, version: String) -> String {
        "\(Constants.buildArtifactsDirectory)/\(Constants.executableName)-\(version)-\(target.triple).zip"
    }

    private static func releaseNotesPath(version: String) -> String {
        ".build/artifacts/release-notes-\(version).md"
    }
}


================================================
FILE: SakeApp/Sakefile.swift
================================================
import ArgumentParser
import Foundation
import Sake
import SwiftShell

@main
@CommandGroup
struct Commands: SakeApp {
    public static let configuration = SakeAppConfiguration(
        commandGroups: [
            TestCommands.self,
            ReleaseCommands.self,
        ]
    )

    public static var lint: Command {
        Command(
            description: "Lint code",
            dependencies: [BrewCommands.ensureSwiftFormatInstalled],
            run: { _ in
                try runAndPrint("swiftformat", "Sources", "SakeApp", "Package.swift", "--lint")
            }
        )
    }

    public static var format: Command {
        Command(
            description: "Format code",
            dependencies: [BrewCommands.ensureSwiftFormatInstalled],
            run: { _ in
                try runAndPrint("swiftformat", "Sources", "SakeApp", "Package.swift")
            }
        )
    }
}

@CommandGroup
struct TestCommands {
    public static var test: Command {
        Command(
            description: "Run tests",
            dependencies: [ensureDebugBuildIsUpToDate],
            run: { context in
                try runAndPrint(
                    bash:
                    "\(context.projectRoot)/Tests/integration_tests.sh \(context.projectRoot)/.build/debug/progressline"
                )
            }
        )
    }

    private static var ensureDebugBuildIsUpToDate: Command {
        Command(
            description: "Ensure debug build is up to date",
            run: { context in
                try runAndPrint(bash: "swift build --package-path \(context.projectRoot)")
            }
        )
    }
}

extension Command.Context {
    var projectRoot: String {
        "\(appDirectory)/.."
    }
}


================================================
FILE: Sources/ANSI.swift
================================================
enum ANSI {
    // Cursor controls
    static func cursorUp(_ count: Int) -> String {
        "\u{1B}[\(count)A"
    }

    static func cursorToColumn(_ column: Int) -> String {
        "\u{1B}[\(column)G"
    }

    static let eraseLine = "\u{1B}[2K"

    // Colors and styles
    static let noStyleMode = !isTTY
    static var red: String {
        noStyleMode ? "" : "\u{1B}[31m"
    }

    static var green: String {
        noStyleMode ? "" : "\u{1B}[32m"
    }

    static var yellow: String {
        noStyleMode ? "" : "\u{1B}[33m"
    }

    static var blue: String {
        noStyleMode ? "" : "\u{1B}[34m"
    }

    static var magenta: String {
        noStyleMode ? "" : "\u{1B}[35m"
    }

    static var bold: String {
        noStyleMode ? "" : "\u{1B}[1m"
    }

    static var reset: String {
        noStyleMode ? "" : "\u{1B}[0m"
    }
}


================================================
FILE: Sources/ActivityIndicator+CommandArgument.swift
================================================
import ArgumentParser

enum ActivityIndicatorStyle: String, CaseIterable, ExpressibleByArgument {
    case dots
    case kitt
    case snake
    case spinner
}

extension ActivityIndicator {
    static func make(style: ActivityIndicatorStyle) -> ActivityIndicator {
        switch style {
        case .dots:
            .dots
        case .kitt:
            .kitt
        case .snake:
            .snake
        case .spinner:
            .spinner
        }
    }
}


================================================
FILE: Sources/ActivityIndicator.swift
================================================
import Foundation
import TaggedTime

final class ActivityIndicator: Sendable {
    struct Configuration {
        let refreshRate: Milliseconds<UInt64>
        let states: [String]
    }

    let configuration: Configuration

    init(configuration: Configuration) {
        self.configuration = configuration
    }

    func state(forDuration duration: Seconds<TimeInterval>) -> String {
        let iteration = Int(duration.milliseconds.rawValue / TimeInterval(configuration.refreshRate.rawValue)) % configuration.states.count
        return configuration.states[iteration]
    }
}

extension ActivityIndicator {
    static let dots: ActivityIndicator = {
        let configuration = Configuration(
            refreshRate: 125,
            states: [
                "⠋",
                "⠙",
                "⠹",
                "⠸",
                "⠼",
                "⠴",
                "⠦",
                "⠧",
                "⠇",
                "⠏",
            ]
        )
        return ActivityIndicator(configuration: configuration)
    }()

    static let kitt: ActivityIndicator = {
        let configuration = Configuration(
            refreshRate: 125,
            states: [
                "▰▱▱▱▱",
                "▰▰▱▱▱",
                "▰▰▰▱▱",
                "▱▰▰▰▱",
                "▱▱▰▰▰",
                "▱▱▱▰▰",
                "▱▱▱▱▰",
                "▱▱▱▰▰",
                "▱▱▰▰▰",
                "▱▰▰▰▱",
                "▰▰▰▱▱",
                "▰▰▱▱▱",
            ]
        )
        return ActivityIndicator(configuration: configuration)
    }()

    static let snake: ActivityIndicator = {
        let configuration = Configuration(
            refreshRate: 125,
            states: [
                "▰▱▱▱▱",
                "▰▰▱▱▱",
                "▰▰▰▱▱",
                "▱▰▰▰▱",
                "▱▱▰▰▰",
                "▱▱▱▰▰",
                "▱▱▱▱▰",
                "▱▱▱▱▱",
            ]
        )
        return ActivityIndicator(configuration: configuration)
    }()

    static let spinner: ActivityIndicator = {
        let configuration = Configuration(
            refreshRate: 125,
            states: [
                "\\",
                "|",
                "/",
                "-",
            ]
        )
        return ActivityIndicator(configuration: configuration)
    }()
}

#if DEBUG
    extension ActivityIndicator {
        static func disabled() -> ActivityIndicator {
            .init(
                configuration: .init(refreshRate: 1_000_000_000, states: [])
            )
        }
    }
#endif


================================================
FILE: Sources/ErrorMessage.swift
================================================
enum ErrorMessage {
    static let canNotDecodeData = "\(ANSI.yellow)[!] progressline: Failed to decode stdin data as UTF-8\(ANSI.reset)"
    static func canNotCompileRegex(_ regex: String) -> String {
        "\(ANSI.yellow)[!] progressline: Failed to compile regular expression: \(regex)\(ANSI.reset)"
    }

    static func canNotOpenFile(_ path: String) -> String {
        "\(ANSI.yellow)[!] progressline: Failed to open file at path: \(path)\(ANSI.reset)"
    }
}


================================================
FILE: Sources/FileHandler+AsyncStream.swift
================================================
#if os(Linux)
    // Linux implementation of FileHandle not Sendable
    @preconcurrency import Foundation
#else
    import Foundation
#endif

extension FileHandle {
    var asyncStream: AsyncStream<Data> {
        AsyncStream { continuation in
            Task {
                while let data = try waitAndReadAvailableData() {
                    continuation.yield(data)
                }
                continuation.finish()
            }
        }
    }

    private func waitAndReadAvailableData() throws -> Data? {
        let data = availableData
        guard !data.isEmpty else {
            return nil
        }
        return data
    }
}


================================================
FILE: Sources/LogAllController.swift
================================================
import Foundation

final class LogAllController {
    private let logger: AboveProgressLineLogger

    init(logger: AboveProgressLineLogger) {
        self.logger = logger
    }

    func didGetStdinDataChunk(_ data: Data) async {
        let text = String(data: data, encoding: .utf8)
        guard let text else {
            await logger.logError(ErrorMessage.canNotDecodeData)
            return
        }
        // we control newlines and whitespaces in the logger
        let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
        await logger.log(trimmedText)
    }
}


================================================
FILE: Sources/MatchesController.swift
================================================
import Foundation

final class MatchesController {
    private let logger: AboveProgressLineLogger
    let regexps: [NSRegularExpression]

    init?(logger: AboveProgressLineLogger, regexps: [String]) async {
        self.logger = logger
        guard !regexps.isEmpty else {
            return nil
        }
        var invalidRegexps = [String]()
        self.regexps = regexps.compactMap { regexp in
            do {
                return try NSRegularExpression(pattern: regexp)
            } catch {
                invalidRegexps.append(regexp)
                return nil
            }
        }
        for invalidRegexp in invalidRegexps {
            await logger.logError(ErrorMessage.canNotCompileRegex(invalidRegexp))
        }
    }

    func didGetStdinDataChunk(_ data: Data) async {
        let text = String(data: data, encoding: .utf8)
        guard let text else {
            await logger.logError(ErrorMessage.canNotDecodeData)
            return
        }
        for line in text.split(whereSeparator: \.isNewline) {
            let range = NSRange(location: 0, length: line.utf16.count)
            for regex in regexps {
                if regex.firstMatch(in: String(line), range: range) != nil {
                    await logger.log(String(line))
                    break
                }
            }
        }
    }
}


================================================
FILE: Sources/OriginalLogController.swift
================================================
import Foundation

final class OriginalLogController {
    private let logger: AboveProgressLineLogger
    let fileHandle: FileHandle

    init?(logger: AboveProgressLineLogger, path: String) async {
        self.logger = logger

        do {
            let url = URL(fileURLWithPath: path)
            try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
            _ = FileManager.default.createFile(atPath: path, contents: nil)
            self.fileHandle = try FileHandle(forWritingTo: url)
            fileHandle.seekToEndOfFile()
        } catch {
            await logger.logError(ErrorMessage.canNotOpenFile(path))
            return nil
        }
    }

    deinit {
        fileHandle.closeFile()
    }

    func didGetStdinDataChunk(_ data: Data) async {
        fileHandle.write(data)
    }
}


================================================
FILE: Sources/Printer.swift
================================================
import ConcurrencyExtras
#if os(Linux)
    // Linux implementation of FileHandle not Sendable
    @preconcurrency import Foundation
#else
    import Foundation
#endif

final class Printer: Sendable {
    private let fileHandle: LockIsolated<FileHandle>
    private let buffer = LockIsolated(String())
    private let _wasWritten = LockIsolated(false)

    var wasWritten: Bool {
        _wasWritten.value
    }

    init(fileHandle: FileHandle) {
        self.fileHandle = .init(fileHandle)
    }

    @discardableResult
    func writeln(_ text: String) -> Self {
        buffer.withValue { $0 += text + "\n" }
        return self
    }

    @discardableResult
    func write(_ text: String) -> Self {
        buffer.withValue { $0 += text }
        return self
    }

    @discardableResult
    func cursorToColumn(_ column: Int) -> Self {
        buffer.withValue { $0 += ANSI.cursorToColumn(column) }
        return self
    }

    @discardableResult
    func cursorUp(_ count: Int = 1) -> Self {
        buffer.withValue { $0 += ANSI.cursorUp(count) }
        return self
    }

    @discardableResult
    func eraseLine() -> Self {
        buffer.withValue { $0 += ANSI.eraseLine }
        return self
    }

    func flush() {
        fileHandle.withValue {
            $0.write(buffer.value.data(using: .utf8)!)
            try? $0.synchronize()
        }
        if !_wasWritten.value {
            _wasWritten.setValue(true)
        }
        buffer.setValue(String())
    }
}


================================================
FILE: Sources/PrintersHolder.swift
================================================
import Foundation

// "Lock" access to printers to prevent write conflicts
final actor PrintersHolder {
    private let printer: Printer
    private let errorsPrinter: Printer

    init(printer: Printer, errorsPrinter: Printer) {
        self.printer = printer
        self.errorsPrinter = errorsPrinter
    }

    func withPrinter<T: Sendable>(_ body: @Sendable (Printer) async throws -> T) async rethrows -> T {
        try await body(printer)
    }

    func withErrorsPrinter<T: Sendable>(_ body: @Sendable (Printer) async throws -> T) async rethrows -> T {
        try await body(errorsPrinter)
    }
}


================================================
FILE: Sources/ProgressLine.swift
================================================
import ArgumentParser
import ConcurrencyExtras
import Foundation
import TaggedTime

@main
struct ProgressLine: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "progressline",
        abstract: "A command-line tool for compactly tracking the progress of piped commands.",
        usage: "some-command | progressline",
        version: progressLineVersion
    )

    @Option(name: [.long, .customShort("t")], help: "The static text to display instead of the latest stdin data.")
    var staticText: String?

    @Option(name: [.customLong("activity-style"), .customShort("s")], help: "The style of the activity indicator.")
    var activityIndicatorStyle: ActivityIndicatorStyle = .dots

    @Option(name: [.customLong("original-log-path"), .customShort("l")], help: "Save the original log to a file.")
    var originalLogPath: String?

    @Option(
        name: [.customLong("log-matches"), .customShort("m")],
        help: "Log above progress line lines matching the given regular expressions."
    )
    var matchesToLog: [String] = []

    @Flag(name: [.customLong("log-all"), .customShort("a")], help: "Log all lines above the progress line.")
    var shouldLogAll: Bool = false

    #if DEBUG
        @Flag(name: [.customLong("test-mode")], help: "Enable test mode. Activity indicator will be replaced with a static string.")
        var testMode: Bool = false
    #endif

    mutating func run() async throws {
        try validateConfiguration()

        let printers = PrintersHolder(
            printer: Printer(fileHandle: .standardOutput),
            errorsPrinter: Printer(fileHandle: .standardError)
        )
        let logger = AboveProgressLineLogger(printers: printers)

        #if DEBUG
            let activityIndicator: ActivityIndicator = testMode ? .disabled() : .make(style: activityIndicatorStyle)
        #else
            let testMode = false
            let activityIndicator: ActivityIndicator = .make(style: activityIndicatorStyle)
        #endif
        let progressLineController = await ProgressLineController.buildAndStart(
            textMode: staticText.map { .staticText($0) } ?? .stdin,
            printers: printers,
            logger: logger,
            activityIndicator: activityIndicator,
            mockActivityAndDuration: testMode
        )
        let originalLogController = if let originalLogPath {
            await OriginalLogController(logger: logger, path: originalLogPath)
        } else {
            OriginalLogController?.none
        }
        let matchesController = await MatchesController(logger: logger, regexps: matchesToLog)
        let logAllController = shouldLogAll ? LogAllController(logger: logger) : nil

        for await data in FileHandle.standardInput.asyncStream {
            await logAllController?.didGetStdinDataChunk(data)
            await matchesController?.didGetStdinDataChunk(data)
            await progressLineController.didGetStdinDataChunk(data)
            await originalLogController?.didGetStdinDataChunk(data)
        }

        await progressLineController.didReachEndOfStdin()
    }

    private func validateConfiguration() throws {
        guard !shouldLogAll || matchesToLog.isEmpty else {
            throw ValidationError("The --log-all and --log-matches options are mutually exclusive.")
        }
    }
}


================================================
FILE: Sources/ProgressLineController.swift
================================================
import Foundation
import TaggedTime

final actor ProgressLineController {
    enum TextMode {
        case staticText(String)
        case stdin
    }

    // Dependencies
    private let textMode: TextMode
    private let printers: PrintersHolder
    private let logger: AboveProgressLineLogger
    private let progressLineFormatter: ProgressLineFormatter
    private let progressTracker: ProgressTracker
    // State
    private var renderLoopTask: Task<Never, any Error>?
    private var lastStdinLine: String?
    private var progress: Progress?

    private init(
        textMode: TextMode,
        printers: PrintersHolder,
        logger: AboveProgressLineLogger,
        progressLineFormatter: ProgressLineFormatter,
        progressTracker: ProgressTracker
    ) {
        self.textMode = textMode
        self.printers = printers
        self.logger = logger
        self.progressLineFormatter = progressLineFormatter
        self.progressTracker = progressTracker
    }

    // MARK: - Public

    static func buildAndStart(
        textMode: TextMode,
        printers: PrintersHolder,
        logger: AboveProgressLineLogger,
        activityIndicator: ActivityIndicator,
        mockActivityAndDuration: Bool = false
    ) async -> Self {
        let progressTracker = ProgressTracker.start()
        let windowSizeObserver = WindowSizeObserver.startObserving()
        let progressLineFormatter = ProgressLineFormatter(
            activityIndicator: activityIndicator,
            windowSizeObserver: windowSizeObserver,
            mockActivityAndDuration: mockActivityAndDuration
        )

        let controller = Self(
            textMode: textMode,
            printers: printers,
            logger: logger,
            progressLineFormatter: progressLineFormatter,
            progressTracker: progressTracker
        )
        await controller.startAnimationLoop(refreshRate: activityIndicator.configuration.refreshRate)

        return controller
    }

    // MARK: - Input

    func didGetStdinDataChunk(_ data: Data) async {
        guard case .stdin = textMode else {
            // we will redraw anyway to sync (prevent flickering) with other log controllers
            await redrawProgressLine()
            return
        }

        let stdinText = String(data: data, encoding: .utf8)
        guard let stdinText else {
            await logger.logError(ErrorMessage.canNotDecodeData)
            return
        }

        lastStdinLine = stdinText
            .split(whereSeparator: \.isNewline)
            .last { !$0.isEmpty }
            .map(String.init)

        await redrawProgressLine()
    }

    func didReachEndOfStdin() async {
        stopAnimationLoop()

        let progressLine = progressLineFormatter.finished(progress: progress)
        await printers.withPrinter { printer in
            if printer.wasWritten {
                printer
                    .cursorUp()
                    .eraseLine()
            }
            printer
                .writeln(progressLine)
                .flush()
        }
    }

    // MARK: - Private

    private func startAnimationLoop(refreshRate: Milliseconds<UInt64>) {
        renderLoopTask = Task.periodic(interval: refreshRate) { [weak self] in
            guard !Task.isCancelled else {
                return
            }
            await self?.redrawProgressLine()
        }
    }

    private func stopAnimationLoop() {
        renderLoopTask?.cancel()
    }

    private func redrawProgressLine() async {
        let lineText: String? = switch textMode {
        case let .staticText(text):
            text
        case .stdin:
            lastStdinLine
        }
        let progress = progressTracker.moveForward(lineText)
        let progressLine = progressLineFormatter.inProgress(progress: progress)
        self.progress = progress
        await printers.withPrinter { printer in
            if printer.wasWritten {
                printer
                    .cursorUp()
                    .eraseLine()
            }
            printer
                .writeln(progressLine)
                .flush()
        }
    }
}


================================================
FILE: Sources/ProgressLineFormatter.swift
================================================
import Foundation
import TaggedTime

private enum Symbol {
    static let checkmark = "✓"
    static let prompt = "❯"
}

final class ProgressLineFormatter: Sendable {
    // Linux doesn't support DateComponentsFormatter
    #if os(macOS)
        private let durationFormatter: DateComponentsFormatter = {
            let durationFormatter = DateComponentsFormatter()
            durationFormatter.unitsStyle = .abbreviated
            durationFormatter.allowedUnits = [.hour, .minute, .second]
            durationFormatter.maximumUnitCount = 2
            return durationFormatter
        }()
    #endif

    private let activityIndicator: ActivityIndicator
    private let windowSizeObserver: WindowSizeObserver?
    private let mockActivityAndDuration: Bool

    init(
        activityIndicator: ActivityIndicator,
        windowSizeObserver: WindowSizeObserver?,
        mockActivityAndDuration: Bool
    ) {
        self.activityIndicator = activityIndicator
        self.windowSizeObserver = windowSizeObserver
        self.mockActivityAndDuration = mockActivityAndDuration
    }

    func inProgress(progress: Progress) -> String {
        let activityIndicator = mockActivityAndDuration ? "<activity>" : activityIndicator.state(forDuration: progress.duration)
        let formattedDuration = mockActivityAndDuration ? "<duration>" : formatDuration(from: progress.duration)

        let styledActivityIndicator = ANSI.blue + activityIndicator + ANSI.reset
        let styledDuration = ANSI.bold + formattedDuration + ANSI.reset
        let styledPrompt = ANSI.blue + Symbol.prompt + ANSI.reset

        return buildResultString(
            styledActivityIndicator: styledActivityIndicator,
            styledDuration: styledDuration,
            styledPrompt: styledPrompt,
            progressLine: progress.line
        )
    }

    func finished(progress: Progress?) -> String {
        let formattedDuration = mockActivityAndDuration ? "<duration>" : progress.map { formatDuration(from: $0.duration) }

        let styledActivityIndicator = ANSI.green + Symbol.checkmark + ANSI.reset
        let styledDuration = formattedDuration.map { ANSI.bold + $0 + ANSI.reset }
        let styledPrompt = ANSI.green + Symbol.prompt + ANSI.reset

        return buildResultString(
            styledActivityIndicator: styledActivityIndicator,
            styledDuration: styledDuration,
            styledPrompt: styledPrompt,
            progressLine: progress?.line
        )
    }

    private func buildResultString(
        styledActivityIndicator: String,
        styledDuration: String?,
        styledPrompt: String,
        progressLine: String?
    ) -> String {
        let buildResultWithProgressLine = { (progressLine: String?) -> String in
            [styledActivityIndicator, styledDuration, styledPrompt, progressLine]
                .compactMap { $0 }
                .joined(separator: " ")
        }
        let result = buildResultWithProgressLine(progressLine)

        let notFittedToWindowLength = calculateStringNotFittedToWindowLength(result)
        if let progressLine, notFittedToWindowLength > 0 {
            let fittedProgressLine = String(progressLine.prefix(progressLine.count - notFittedToWindowLength))
            return buildResultWithProgressLine(fittedProgressLine)
        } else {
            return result
        }
    }

    private func calculateStringNotFittedToWindowLength(_ string: String) -> Int {
        guard let windowSizeObserver else {
            return 0
        }
        let stringWithoutANSI = string.withoutANSI()
        let windowWidth = windowSizeObserver.size.width
        return max(stringWithoutANSI.count - windowWidth, 0)
    }

    private func formatDuration(from duration: Seconds<TimeInterval>) -> String {
        #if os(Linux)
            duration.rawValue.formattedDuration()
        #else
            durationFormatter.string(from: duration.rawValue)!
        #endif
    }
}

#if os(Linux)
    extension TimeInterval {
        func formattedDuration() -> String {
            let totalSeconds = Int(self)
            let hours = totalSeconds / 3600
            let minutes = (totalSeconds % 3600) / 60
            let seconds = totalSeconds % 60

            if hours >= 1 {
                return "\(hours)h \(minutes)m"
            } else if minutes >= 1 {
                return "\(minutes)m \(seconds)s"
            } else {
                return "\(seconds)s"
            }
        }
    }
#endif


================================================
FILE: Sources/ProgressTracker.swift
================================================
import Foundation
import TaggedTime

struct Progress {
    let line: String?
    let duration: Seconds<TimeInterval>
}

final class ProgressTracker: Sendable {
    private let startTimestamp: Seconds<TimeInterval>

    private init(startTimestamp: Seconds<TimeInterval>) {
        self.startTimestamp = startTimestamp
    }

    static func start() -> ProgressTracker {
        ProgressTracker(startTimestamp: Seconds(Date().timeIntervalSince1970))
    }

    func moveForward(_ line: String?) -> Progress {
        let duration = Seconds(Date().timeIntervalSince1970) - startTimestamp
        return Progress(line: line, duration: duration)
    }
}


================================================
FILE: Sources/String+ANSI.swift
================================================
import Foundation

extension String {
    private static let ansiRegex = try! NSRegularExpression(pattern: "\u{1B}(?:[@-Z\\-_]|\\[[0-?]*[ -/]*[@-~])")

    func withoutANSI() -> String {
        let range = NSRange(startIndex ..< endIndex, in: self)
        return Self.ansiRegex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
    }
}


================================================
FILE: Sources/Task+Periodic.swift
================================================
import Foundation
import TaggedTime

extension Task where Success == Never, Failure == any Error {
    @discardableResult
    static func periodic(interval: Milliseconds<UInt64>, operation: @Sendable @escaping () async throws -> Void) -> Task {
        Task {
            while true {
                try Task<Never, Never>.checkCancellation()
                try await operation()
                try await Task<Never, Never>.sleep(nanoseconds: 1_000_000 * interval.rawValue)
            }
        }
    }
}


================================================
FILE: Sources/UnderProgressLineLogger.swift
================================================
import Foundation

final class AboveProgressLineLogger: Sendable {
    private let printers: PrintersHolder

    init(printers: PrintersHolder) {
        self.printers = printers
    }

    func log(_ text: String) async {
        await printers.withPrinter { printer in
            await log(printer: printer, text: text)
        }
    }

    func logError(_ text: String) async {
        await printers.withErrorsPrinter { errorsPrinter in
            await log(printer: errorsPrinter, text: text)
        }
    }

    private func log(printer: Printer, text: String) async {
        if printer.wasWritten {
            printer
                .cursorUp()
                .eraseLine()
        }
        printer
            .writeln(text)
            .writeln("") // Add an empty line after the message for delete it by progress line controller
            .flush()
    }
}


================================================
FILE: Sources/Version.swift
================================================
// This file is autogenerated. Do not edit.
let progressLineVersion = "0.2.4"


================================================
FILE: Sources/WindowSizeObserver.swift
================================================
import ConcurrencyExtras
import Foundation

final class WindowSizeObserver: Sendable {
    struct Size {
        let width: Int
        let height: Int
    }

    private let signalHandler: LockIsolated<UncheckedSendable<DispatchSourceSignal>?> = .init(nil)
    private let _size: LockIsolated<Size> = .init(getTerminalSize())

    var size: Size {
        _size.value
    }

    static func startObserving() -> WindowSizeObserver? {
        guard isTTY else {
            return nil
        }
        let observer = WindowSizeObserver()
        observer.setupSignalHandler()
        return observer
    }

    private init() {}

    private func setupSignalHandler() {
        let sigwinch = SIGWINCH

        let signalHandler = DispatchSource.makeSignalSource(signal: sigwinch)
        signal(sigwinch, SIG_IGN)

        signalHandler.setEventHandler { [weak self] in
            guard let self else {
                return
            }
            self.syncWindowSize()
        }
        signalHandler.resume()

        let uncheckedSendable = UncheckedSendable(signalHandler)
        self.signalHandler.setValue(uncheckedSendable)
    }

    private func syncWindowSize() {
        _size.setValue(Self.getTerminalSize())
    }

    static func getTerminalSize() -> Size {
        var w = winsize()
        #if os(Linux)
            _ = ioctl(STDOUT_FILENO, UInt(TIOCGWINSZ), &w)
        #else
            _ = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w)
        #endif
        return Size(width: Int(w.ws_col), height: Int(w.ws_row))
    }
}


================================================
FILE: Sources/isTTY.swift
================================================
import Foundation

let isTTY = isatty(STDOUT_FILENO) != 0


================================================
FILE: Tests/assert.sh
================================================
#!/usr/bin/env bash
#
# Determine if the script is sourced or executed
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
    # Script is being sourced
    SNAPSHOTS_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)"
else
    # Script is being executed
    SNAPSHOTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
fi

# takes a name for snapshot file and string to compare to reference.
# If the snapshot file does not exist, it will be created.
# If the snapshot file does exist, the string will be compared to the snapshot.
# If the string is different, assertion will fail and print the diff.
# For recording new snapshots, use the `SNAPSHOT_RECORD=true` environment variable.
assert_snapshot() {
  local snapshot_name="$1"
  local snapshot_value="$2"
  local snapshot_file="$SNAPSHOTS_DIR/snapshots/$snapshot_name.snapshot"
  mkdir -p "$SNAPSHOTS_DIR"

  if [ "$SNAPSHOT_RECORD" = "true" ]; then
    echo "Recording snapshot $snapshot_name" >&2
    echo "$snapshot_value" > "$snapshot_file"
    return
  fi

  if [ ! -f "$snapshot_file" ]; then
    echo "Snapshot $snapshot_name does not exist. Recording new snapshot." >&2
    echo "$snapshot_value" > "$snapshot_file"
    return
  fi

  local snapshot_diff=$(diff -u "$snapshot_file" <(echo "$snapshot_value"))
  if [ -n "$snapshot_diff" ]; then
    echo "Snapshot $snapshot_name does not match reference." >&2
    echo "$snapshot_diff"
    exit 1
  fi

  echo "Snapshot $snapshot_name matches reference." >&2
}


================================================
FILE: Tests/integration_tests.sh
================================================
#!/usr/bin/env bash

set -eo pipefail

TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"

source "$TESTS_DIR/assert.sh"

# Parse arg: <executable path>
executable_path="$1"
if [ -z "$executable_path" ]; then
  echo "Usage: $0 <executable path>"
  exit 1
fi

# Prepare test data

test_data_producer_config="{
  \"chunk_count\": 30,
  \"chunk_size\": 3,
  \"write_delay\": 10,
}"
test_data_producer_config_file="/tmp/progressline_test_data_producer_config.json"
echo "$test_data_producer_config" > "$test_data_producer_config_file"

# warmup test data producer
swift "$TESTS_DIR"/test_data_producer.swift $test_data_producer_config_file > /dev/null

generate_test_output="swift $TESTS_DIR/test_data_producer.swift $test_data_producer_config_file"

# Test default mode

output=$($generate_test_output | "$executable_path" --test-mode)
assert_snapshot "default" "$output"

# Test static text mode

output=$($generate_test_output | "$executable_path" --test-mode --static-text "Static text")
assert_snapshot "static_text" "$output"

# Test default mode with save original log

output=$($generate_test_output | "$executable_path" --test-mode --original-log-path /tmp/progressline_test_original_log.txt)
assert_snapshot "default_with_original_log" "$output"
assert_snapshot "default_with_original_log_original_log" "$(cat /tmp/progressline_test_original_log.txt)"
rm /tmp/progressline_test_original_log.txt

# Test log matches

output=$($generate_test_output | "$executable_path" --test-mode --log-matches "Chunk number: \d+[1-5]{1}")
assert_snapshot "log_matches" "$output"

# Test log all

output=$($generate_test_output | "$executable_path" --test-mode --log-all)
assert_snapshot "log_all" "$output"


================================================
FILE: Tests/snapshots/default.snapshot
================================================
<activity> <duration> ❯
<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3
✓ <duration> ❯ Chunk number: 30, Chunk Line: 3


================================================
FILE: Tests/snapshots/default_with_original_log.snapshot
================================================
<activity> <duration> ❯
<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3
✓ <duration> ❯ Chunk number: 30, Chunk Line: 3


================================================
FILE: Tests/snapshots/default_with_original_log_original_log.snapshot
================================================
Chunk number: 1, Chunk Line: 1
Chunk number: 1, Chunk Line: 2
Chunk number: 1, Chunk Line: 3
Chunk number: 2, Chunk Line: 1
Chunk number: 2, Chunk Line: 2
Chunk number: 2, Chunk Line: 3
Chunk number: 3, Chunk Line: 1
Chunk number: 3, Chunk Line: 2
Chunk number: 3, Chunk Line: 3
Chunk number: 4, Chunk Line: 1
Chunk number: 4, Chunk Line: 2
Chunk number: 4, Chunk Line: 3
Chunk number: 5, Chunk Line: 1
Chunk number: 5, Chunk Line: 2
Chunk number: 5, Chunk Line: 3
Chunk number: 6, Chunk Line: 1
Chunk number: 6, Chunk Line: 2
Chunk number: 6, Chunk Line: 3
Chunk number: 7, Chunk Line: 1
Chunk number: 7, Chunk Line: 2
Chunk number: 7, Chunk Line: 3
Chunk number: 8, Chunk Line: 1
Chunk number: 8, Chunk Line: 2
Chunk number: 8, Chunk Line: 3
Chunk number: 9, Chunk Line: 1
Chunk number: 9, Chunk Line: 2
Chunk number: 9, Chunk Line: 3
Chunk number: 10, Chunk Line: 1
Chunk number: 10, Chunk Line: 2
Chunk number: 10, Chunk Line: 3
Chunk number: 11, Chunk Line: 1
Chunk number: 11, Chunk Line: 2
Chunk number: 11, Chunk Line: 3
Chunk number: 12, Chunk Line: 1
Chunk number: 12, Chunk Line: 2
Chunk number: 12, Chunk Line: 3
Chunk number: 13, Chunk Line: 1
Chunk number: 13, Chunk Line: 2
Chunk number: 13, Chunk Line: 3
Chunk number: 14, Chunk Line: 1
Chunk number: 14, Chunk Line: 2
Chunk number: 14, Chunk Line: 3
Chunk number: 15, Chunk Line: 1
Chunk number: 15, Chunk Line: 2
Chunk number: 15, Chunk Line: 3
Chunk number: 16, Chunk Line: 1
Chunk number: 16, Chunk Line: 2
Chunk number: 16, Chunk Line: 3
Chunk number: 17, Chunk Line: 1
Chunk number: 17, Chunk Line: 2
Chunk number: 17, Chunk Line: 3
Chunk number: 18, Chunk Line: 1
Chunk number: 18, Chunk Line: 2
Chunk number: 18, Chunk Line: 3
Chunk number: 19, Chunk Line: 1
Chunk number: 19, Chunk Line: 2
Chunk number: 19, Chunk Line: 3
Chunk number: 20, Chunk Line: 1
Chunk number: 20, Chunk Line: 2
Chunk number: 20, Chunk Line: 3
Chunk number: 21, Chunk Line: 1
Chunk number: 21, Chunk Line: 2
Chunk number: 21, Chunk Line: 3
Chunk number: 22, Chunk Line: 1
Chunk number: 22, Chunk Line: 2
Chunk number: 22, Chunk Line: 3
Chunk number: 23, Chunk Line: 1
Chunk number: 23, Chunk Line: 2
Chunk number: 23, Chunk Line: 3
Chunk number: 24, Chunk Line: 1
Chunk number: 24, Chunk Line: 2
Chunk number: 24, Chunk Line: 3
Chunk number: 25, Chunk Line: 1
Chunk number: 25, Chunk Line: 2
Chunk number: 25, Chunk Line: 3
Chunk number: 26, Chunk Line: 1
Chunk number: 26, Chunk Line: 2
Chunk number: 26, Chunk Line: 3
Chunk number: 27, Chunk Line: 1
Chunk number: 27, Chunk Line: 2
Chunk number: 27, Chunk Line: 3
Chunk number: 28, Chunk Line: 1
Chunk number: 28, Chunk Line: 2
Chunk number: 28, Chunk Line: 3
Chunk number: 29, Chunk Line: 1
Chunk number: 29, Chunk Line: 2
Chunk number: 29, Chunk Line: 3
Chunk number: 30, Chunk Line: 1
Chunk number: 30, Chunk Line: 2
Chunk number: 30, Chunk Line: 3


================================================
FILE: Tests/snapshots/log_all.snapshot
================================================
<activity> <duration> ❯
Chunk number: 1, Chunk Line: 1
Chunk number: 1, Chunk Line: 2
Chunk number: 1, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3
Chunk number: 2, Chunk Line: 1
Chunk number: 2, Chunk Line: 2
Chunk number: 2, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3
Chunk number: 3, Chunk Line: 1
Chunk number: 3, Chunk Line: 2
Chunk number: 3, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3
Chunk number: 4, Chunk Line: 1
Chunk number: 4, Chunk Line: 2
Chunk number: 4, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3
Chunk number: 5, Chunk Line: 1
Chunk number: 5, Chunk Line: 2
Chunk number: 5, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3
Chunk number: 6, Chunk Line: 1
Chunk number: 6, Chunk Line: 2
Chunk number: 6, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3
Chunk number: 7, Chunk Line: 1
Chunk number: 7, Chunk Line: 2
Chunk number: 7, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3
Chunk number: 8, Chunk Line: 1
Chunk number: 8, Chunk Line: 2
Chunk number: 8, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3
Chunk number: 9, Chunk Line: 1
Chunk number: 9, Chunk Line: 2
Chunk number: 9, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3
Chunk number: 10, Chunk Line: 1
Chunk number: 10, Chunk Line: 2
Chunk number: 10, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3
Chunk number: 11, Chunk Line: 1
Chunk number: 11, Chunk Line: 2
Chunk number: 11, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3
Chunk number: 12, Chunk Line: 1
Chunk number: 12, Chunk Line: 2
Chunk number: 12, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3
Chunk number: 13, Chunk Line: 1
Chunk number: 13, Chunk Line: 2
Chunk number: 13, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3
Chunk number: 14, Chunk Line: 1
Chunk number: 14, Chunk Line: 2
Chunk number: 14, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3
Chunk number: 15, Chunk Line: 1
Chunk number: 15, Chunk Line: 2
Chunk number: 15, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3
Chunk number: 16, Chunk Line: 1
Chunk number: 16, Chunk Line: 2
Chunk number: 16, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3
Chunk number: 17, Chunk Line: 1
Chunk number: 17, Chunk Line: 2
Chunk number: 17, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3
Chunk number: 18, Chunk Line: 1
Chunk number: 18, Chunk Line: 2
Chunk number: 18, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3
Chunk number: 19, Chunk Line: 1
Chunk number: 19, Chunk Line: 2
Chunk number: 19, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3
Chunk number: 20, Chunk Line: 1
Chunk number: 20, Chunk Line: 2
Chunk number: 20, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3
Chunk number: 21, Chunk Line: 1
Chunk number: 21, Chunk Line: 2
Chunk number: 21, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3
Chunk number: 22, Chunk Line: 1
Chunk number: 22, Chunk Line: 2
Chunk number: 22, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3
Chunk number: 23, Chunk Line: 1
Chunk number: 23, Chunk Line: 2
Chunk number: 23, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3
Chunk number: 24, Chunk Line: 1
Chunk number: 24, Chunk Line: 2
Chunk number: 24, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3
Chunk number: 25, Chunk Line: 1
Chunk number: 25, Chunk Line: 2
Chunk number: 25, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3
Chunk number: 26, Chunk Line: 1
Chunk number: 26, Chunk Line: 2
Chunk number: 26, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3
Chunk number: 27, Chunk Line: 1
Chunk number: 27, Chunk Line: 2
Chunk number: 27, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3
Chunk number: 28, Chunk Line: 1
Chunk number: 28, Chunk Line: 2
Chunk number: 28, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3
Chunk number: 29, Chunk Line: 1
Chunk number: 29, Chunk Line: 2
Chunk number: 29, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3
Chunk number: 30, Chunk Line: 1
Chunk number: 30, Chunk Line: 2
Chunk number: 30, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3
✓ <duration> ❯ Chunk number: 30, Chunk Line: 3


================================================
FILE: Tests/snapshots/log_matches.snapshot
================================================
<activity> <duration> ❯
<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 2, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 3, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 4, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 5, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 6, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 7, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 8, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 9, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 10, Chunk Line: 3
Chunk number: 11, Chunk Line: 1

Chunk number: 11, Chunk Line: 2

Chunk number: 11, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 11, Chunk Line: 3
Chunk number: 12, Chunk Line: 1

Chunk number: 12, Chunk Line: 2

Chunk number: 12, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 12, Chunk Line: 3
Chunk number: 13, Chunk Line: 1

Chunk number: 13, Chunk Line: 2

Chunk number: 13, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 13, Chunk Line: 3
Chunk number: 14, Chunk Line: 1

Chunk number: 14, Chunk Line: 2

Chunk number: 14, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 14, Chunk Line: 3
Chunk number: 15, Chunk Line: 1

Chunk number: 15, Chunk Line: 2

Chunk number: 15, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 15, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 16, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 17, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 18, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 19, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 20, Chunk Line: 3
Chunk number: 21, Chunk Line: 1

Chunk number: 21, Chunk Line: 2

Chunk number: 21, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 21, Chunk Line: 3
Chunk number: 22, Chunk Line: 1

Chunk number: 22, Chunk Line: 2

Chunk number: 22, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 22, Chunk Line: 3
Chunk number: 23, Chunk Line: 1

Chunk number: 23, Chunk Line: 2

Chunk number: 23, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 23, Chunk Line: 3
Chunk number: 24, Chunk Line: 1

Chunk number: 24, Chunk Line: 2

Chunk number: 24, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 24, Chunk Line: 3
Chunk number: 25, Chunk Line: 1

Chunk number: 25, Chunk Line: 2

Chunk number: 25, Chunk Line: 3

<activity> <duration> ❯ Chunk number: 25, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 26, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 27, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 28, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 29, Chunk Line: 3
<activity> <duration> ❯ Chunk number: 30, Chunk Line: 3
✓ <duration> ❯ Chunk number: 30, Chunk Line: 3


================================================
FILE: Tests/snapshots/static_text.snapshot
================================================
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
<activity> <duration> ❯ Static text
✓ <duration> ❯ Static text


================================================
FILE: Tests/test_data_producer.swift
================================================
/*
 Generates test data for the app.
 Features:
 - iterates over the given number and prints the iteration number
 - prints with a delay if the delay is greater than 0
 - can produce a given number of lines per iteration
 Usage:
    test_data_producer.swift <iterations> [<delay in ms>] [<lines>]
 */

import Foundation

struct Configuration: Decodable {
    let chunkCount: Int
    let chunkSize: Int
    let writeDelay: TimeInterval

    enum CodingKeys: String, CodingKey {
        case chunkCount = "chunk_count"
        case chunkSize = "chunk_size"
        case writeDelay = "write_delay"
    }
}

let arguments = CommandLine.arguments.dropFirst()
guard arguments.count >= 1 else {
    print("Usage: \(CommandLine.arguments.first!) <configuration file path>")
    exit(1)
}

let filePath = arguments.first!
let configuration = try JSONDecoder().decode(Configuration.self, from: try Data(contentsOf: URL(fileURLWithPath: filePath)))

for i in 0 ..< configuration.chunkCount {
    let data = (0 ..< configuration.chunkSize)
        .map { "Chunk number: \(i + 1), Chunk Line: \($0 + 1)" }
        .joined(separator: "\n")
    print(data)
    fflush(stdout)
    if configuration.writeDelay > 0 {
        usleep(useconds_t(configuration.writeDelay * 1000))
    }
}


================================================
FILE: cliff.toml
================================================
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration

[remote.github]
owner = "kattouf"
repo = "ProgressLine"
# token = ""

[changelog]
# changelog header
header = """
# Changelog\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
    {% if previous.version %}\
        ## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
    {% else %}\
        ## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
    {% endif %}\
{% else %}\
    ## Unreleased
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | striptags | trim | upper_first }}

    {% for commit in commits
    | filter(attribute="scope")
    | sort(attribute="scope") -%}
        {% if commit.scope -%}
        * {{self::commit(commit=commit)}}\
        {% endif -%}
    {% endfor -%}
    {% for commit in commits -%}
        {% if commit.scope -%}
        {% else -%}
          * {{self::commit(commit=commit)}}\
        {% endif -%}
    {% endfor -%}
{% endfor %}

{%- if github -%}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
  {% raw %}\n{% endraw -%}
  ## New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
  * @{{ contributor.username }} made their first contribution
    {%- if contributor.pr_number %} in \
      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
    {%- endif %}
{%- endfor -%}
{%- endif -%}

{% if version %}
    {% if previous.version %}
      **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
    {% endif %}
{% else -%}
  {% raw %}\n{% endraw %}
{% endif %}

{%- macro remote_url() -%}
  https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}

{% macro commit(commit) -%}
{% if commit.scope %}**({{commit.scope}})** {% endif -%}
{% if commit.breaking %}**breaking** {% endif -%}
{{ commit.message | split(pat="\n") | first | trim }} by \
{% if commit.remote.username %}[@{{commit.remote.username}}](https://github.com/{{commit.remote.username}})\
{% else %}{{commit.author.name}}{% endif %} in \
{% if commit.remote.pr_number %}[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})\
{% else %}[{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }})\
{%- endif %}
{% endmacro commit -%}
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the template
trim = true
# postprocessors
postprocessors = []

[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = false
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
  # remove issue numbers from commits
  { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
]
# regex for parsing and grouping commits
commit_parsers = [
  { message = '^chore\(release\): Bump version to', skip = true },
  { message = '^(chore|fix)\(deps\):', group = "<!-- 99 -->:package: Dependency Updates", scope = "" },
  { message = '^feat', group = "<!-- 00 -->:rocket: Features" },
  { message = '^fix', group = "<!-- 01 -->:bug: Bug Fixes" },
  { message = '^test', group = "<!-- 02 -->:test_tube: Testing" },
  { message = '^perf', group = "<!-- 03 -->:zap: Performance" },
  { message = '^refactor', group = "<!-- 04 -->:tractor: Refactoring" },
  { message = '^doc', group = "<!-- 05 -->:books: Documentation" },
  { body = '.*security', group = "<!-- 06 -->:shield: Security" },
  { message = '^project', group = "<!-- 07 -->:file_folder: Project" },
  { message = '^revert', group = "<!-- 39 -->:leftwards_arrow_with_hook: Revert" },
  { message = '.', group = "<!-- 49 -->:card_index_dividers: Other Changes" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"
Download .txt
gitextract_76fia48v/

├── .github/
│   ├── demo_activity_style.tape
│   ├── demo_progressline_output.tape
│   ├── demo_standard_output.tape
│   └── workflows/
│       ├── checks.yml
│       └── semantic-pr-lint.yml
├── .gitignore
├── .sake.yml
├── .swiftformat
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── SakeApp/
│   ├── .gitignore
│   ├── BrewCommands.swift
│   ├── Package.resolved
│   ├── Package.swift
│   ├── ReleaseCommands.swift
│   └── Sakefile.swift
├── Sources/
│   ├── ANSI.swift
│   ├── ActivityIndicator+CommandArgument.swift
│   ├── ActivityIndicator.swift
│   ├── ErrorMessage.swift
│   ├── FileHandler+AsyncStream.swift
│   ├── LogAllController.swift
│   ├── MatchesController.swift
│   ├── OriginalLogController.swift
│   ├── Printer.swift
│   ├── PrintersHolder.swift
│   ├── ProgressLine.swift
│   ├── ProgressLineController.swift
│   ├── ProgressLineFormatter.swift
│   ├── ProgressTracker.swift
│   ├── String+ANSI.swift
│   ├── Task+Periodic.swift
│   ├── UnderProgressLineLogger.swift
│   ├── Version.swift
│   ├── WindowSizeObserver.swift
│   └── isTTY.swift
├── Tests/
│   ├── assert.sh
│   ├── integration_tests.sh
│   ├── snapshots/
│   │   ├── default.snapshot
│   │   ├── default_with_original_log.snapshot
│   │   ├── default_with_original_log_original_log.snapshot
│   │   ├── log_all.snapshot
│   │   ├── log_matches.snapshot
│   │   └── static_text.snapshot
│   └── test_data_producer.swift
└── cliff.toml
Condensed preview — 48 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (89K chars).
[
  {
    "path": ".github/demo_activity_style.tape",
    "chars": 144,
    "preview": "Output out.gif\n\nSet Shell \"zsh\"\nSet FontSize 32\nSet Width 660\nSet Height 300\nSet TypingSpeed 75ms\n\nType \"progressline -s"
  },
  {
    "path": ".github/demo_progressline_output.tape",
    "chars": 229,
    "preview": "Output progressline_output.gif\n\nRequire progressline\n\nSet Shell \"zsh\"\nSet FontSize 32\nSet Width 1600\nSet Height 400\nSet "
  },
  {
    "path": ".github/demo_standard_output.tape",
    "chars": 202,
    "preview": "Output standard_output.gif\n\nRequire progressline\n\nSet Shell \"zsh\"\nSet FontSize 32\nSet Width 1600\nSet Height 400\nSet Typi"
  },
  {
    "path": ".github/workflows/checks.yml",
    "chars": 735,
    "preview": "name: Checks\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\njobs:\n  build:\n    name: "
  },
  {
    "path": ".github/workflows/semantic-pr-lint.yml",
    "chars": 584,
    "preview": "name: \"Lint PR\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n      - reopene"
  },
  {
    "path": ".gitignore",
    "chars": 181,
    "preview": ".DS_Store\n/.build\n/.index-build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode"
  },
  {
    "path": ".sake.yml",
    "chars": 38,
    "preview": "case_converting_strategy: toSnakeCase\n"
  },
  {
    "path": ".swiftformat",
    "chars": 340,
    "preview": "--swiftversion 5.10\n\n--exclude .build,.index-build,SakeApp/.build,SakeApp/.index-build\n\n--maxwidth 140\n--wraparguments b"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2024 Vasilii Ianguzin\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "Package.resolved",
    "chars": 984,
    "preview": "{\n  \"originHash\" : \"e759c45271facbb3650829c703702a2ac4817adf75a8116cc3d77eae8e3d3bae\",\n  \"pins\" : [\n    {\n      \"identit"
  },
  {
    "path": "Package.swift",
    "chars": 1134,
    "preview": "// swift-tools-version: 5.10\n// The swift-tools-version declares the minimum version of Swift required to build this pac"
  },
  {
    "path": "README.md",
    "chars": 3225,
    "preview": "## ProgressLine\n\n![](https://img.shields.io/badge/Platform-macOS-6464aa)\n![](https://img.shields.io/badge/Platform-Linux"
  },
  {
    "path": "SakeApp/.gitignore",
    "chars": 172,
    "preview": ".DS_Store\n/.build\n/.index-build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode"
  },
  {
    "path": "SakeApp/BrewCommands.swift",
    "chars": 1064,
    "preview": "import Sake\nimport SwiftShell\n\n@CommandGroup\nstruct BrewCommands {\n    static var ensureSwiftFormatInstalled: Command {\n"
  },
  {
    "path": "SakeApp/Package.resolved",
    "chars": 1443,
    "preview": "{\n  \"originHash\" : \"564ae29a93959e0a64ff9ad1a401e5db179007f3ecef20571f852606328631f6\",\n  \"pins\" : [\n    {\n      \"identit"
  },
  {
    "path": "SakeApp/Package.swift",
    "chars": 965,
    "preview": "// swift-tools-version: 5.10\n// The swift-tools-version declares the minimum version of Swift required to build this pac"
  },
  {
    "path": "SakeApp/ReleaseCommands.swift",
    "chars": 14034,
    "preview": "import ArgumentParser\nimport CryptoKit\nimport Foundation\nimport Sake\nimport SwiftShell\n\n@CommandGroup\nstruct ReleaseComm"
  },
  {
    "path": "SakeApp/Sakefile.swift",
    "chars": 1739,
    "preview": "import ArgumentParser\nimport Foundation\nimport Sake\nimport SwiftShell\n\n@main\n@CommandGroup\nstruct Commands: SakeApp {\n  "
  },
  {
    "path": "Sources/ANSI.swift",
    "chars": 858,
    "preview": "enum ANSI {\n    // Cursor controls\n    static func cursorUp(_ count: Int) -> String {\n        \"\\u{1B}[\\(count)A\"\n    }\n\n"
  },
  {
    "path": "Sources/ActivityIndicator+CommandArgument.swift",
    "chars": 467,
    "preview": "import ArgumentParser\n\nenum ActivityIndicatorStyle: String, CaseIterable, ExpressibleByArgument {\n    case dots\n    case"
  },
  {
    "path": "Sources/ActivityIndicator.swift",
    "chars": 2583,
    "preview": "import Foundation\nimport TaggedTime\n\nfinal class ActivityIndicator: Sendable {\n    struct Configuration {\n        let re"
  },
  {
    "path": "Sources/ErrorMessage.swift",
    "chars": 470,
    "preview": "enum ErrorMessage {\n    static let canNotDecodeData = \"\\(ANSI.yellow)[!] progressline: Failed to decode stdin data as UT"
  },
  {
    "path": "Sources/FileHandler+AsyncStream.swift",
    "chars": 653,
    "preview": "#if os(Linux)\n    // Linux implementation of FileHandle not Sendable\n    @preconcurrency import Foundation\n#else\n    imp"
  },
  {
    "path": "Sources/LogAllController.swift",
    "chars": 596,
    "preview": "import Foundation\n\nfinal class LogAllController {\n    private let logger: AboveProgressLineLogger\n\n    init(logger: Abov"
  },
  {
    "path": "Sources/MatchesController.swift",
    "chars": 1351,
    "preview": "import Foundation\n\nfinal class MatchesController {\n    private let logger: AboveProgressLineLogger\n    let regexps: [NSR"
  },
  {
    "path": "Sources/OriginalLogController.swift",
    "chars": 866,
    "preview": "import Foundation\n\nfinal class OriginalLogController {\n    private let logger: AboveProgressLineLogger\n    let fileHandl"
  },
  {
    "path": "Sources/Printer.swift",
    "chars": 1486,
    "preview": "import ConcurrencyExtras\n#if os(Linux)\n    // Linux implementation of FileHandle not Sendable\n    @preconcurrency import"
  },
  {
    "path": "Sources/PrintersHolder.swift",
    "chars": 608,
    "preview": "import Foundation\n\n// \"Lock\" access to printers to prevent write conflicts\nfinal actor PrintersHolder {\n    private let "
  },
  {
    "path": "Sources/ProgressLine.swift",
    "chars": 3365,
    "preview": "import ArgumentParser\nimport ConcurrencyExtras\nimport Foundation\nimport TaggedTime\n\n@main\nstruct ProgressLine: AsyncPars"
  },
  {
    "path": "Sources/ProgressLineController.swift",
    "chars": 4140,
    "preview": "import Foundation\nimport TaggedTime\n\nfinal actor ProgressLineController {\n    enum TextMode {\n        case staticText(St"
  },
  {
    "path": "Sources/ProgressLineFormatter.swift",
    "chars": 4484,
    "preview": "import Foundation\nimport TaggedTime\n\nprivate enum Symbol {\n    static let checkmark = \"✓\"\n    static let prompt = \"❯\"\n}\n"
  },
  {
    "path": "Sources/ProgressTracker.swift",
    "chars": 650,
    "preview": "import Foundation\nimport TaggedTime\n\nstruct Progress {\n    let line: String?\n    let duration: Seconds<TimeInterval>\n}\n\n"
  },
  {
    "path": "Sources/String+ANSI.swift",
    "chars": 368,
    "preview": "import Foundation\n\nextension String {\n    private static let ansiRegex = try! NSRegularExpression(pattern: \"\\u{1B}(?:[@-"
  },
  {
    "path": "Sources/Task+Periodic.swift",
    "chars": 509,
    "preview": "import Foundation\nimport TaggedTime\n\nextension Task where Success == Never, Failure == any Error {\n    @discardableResul"
  },
  {
    "path": "Sources/UnderProgressLineLogger.swift",
    "chars": 875,
    "preview": "import Foundation\n\nfinal class AboveProgressLineLogger: Sendable {\n    private let printers: PrintersHolder\n\n    init(pr"
  },
  {
    "path": "Sources/Version.swift",
    "chars": 78,
    "preview": "// This file is autogenerated. Do not edit.\nlet progressLineVersion = \"0.2.4\"\n"
  },
  {
    "path": "Sources/WindowSizeObserver.swift",
    "chars": 1541,
    "preview": "import ConcurrencyExtras\nimport Foundation\n\nfinal class WindowSizeObserver: Sendable {\n    struct Size {\n        let wid"
  },
  {
    "path": "Sources/isTTY.swift",
    "chars": 58,
    "preview": "import Foundation\n\nlet isTTY = isatty(STDOUT_FILENO) != 0\n"
  },
  {
    "path": "Tests/assert.sh",
    "chars": 1468,
    "preview": "#!/usr/bin/env bash\n#\n# Determine if the script is sourced or executed\nif [[ \"${BASH_SOURCE[0]}\" != \"${0}\" ]]; then\n    "
  },
  {
    "path": "Tests/integration_tests.sh",
    "chars": 1716,
    "preview": "#!/usr/bin/env bash\n\nset -eo pipefail\n\nTESTS_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" &>/dev/null && pwd)\"\n\nsource \"$T"
  },
  {
    "path": "Tests/snapshots/default.snapshot",
    "chars": 1990,
    "preview": "<activity> <duration> ❯\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ C"
  },
  {
    "path": "Tests/snapshots/default_with_original_log.snapshot",
    "chars": 1990,
    "preview": "<activity> <duration> ❯\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ C"
  },
  {
    "path": "Tests/snapshots/default_with_original_log_original_log.snapshot",
    "chars": 2853,
    "preview": "Chunk number: 1, Chunk Line: 1\nChunk number: 1, Chunk Line: 2\nChunk number: 1, Chunk Line: 3\nChunk number: 2, Chunk Line"
  },
  {
    "path": "Tests/snapshots/log_all.snapshot",
    "chars": 5113,
    "preview": "<activity> <duration> ❯\n\u001b[1A\u001b[2KChunk number: 1, Chunk Line: 1\nChunk number: 1, Chunk Line: 2\nChunk number: 1, Chunk Lin"
  },
  {
    "path": "Tests/snapshots/log_matches.snapshot",
    "chars": 3220,
    "preview": "<activity> <duration> ❯\n\u001b[1A\u001b[2K<activity> <duration> ❯ Chunk number: 1, Chunk Line: 3\n\u001b[1A\u001b[2K<activity> <duration> ❯ C"
  },
  {
    "path": "Tests/snapshots/static_text.snapshot",
    "chars": 1391,
    "preview": "<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static text\n\u001b[1A\u001b[2K<activity> <duration> ❯ Static t"
  },
  {
    "path": "Tests/test_data_producer.swift",
    "chars": 1267,
    "preview": "/*\n Generates test data for the app.\n Features:\n - iterates over the given number and prints the iteration number\n - pri"
  },
  {
    "path": "cliff.toml",
    "chars": 4454,
    "preview": "# git-cliff ~ configuration file\n# https://git-cliff.org/docs/configuration\n\n[remote.github]\nowner = \"kattouf\"\nrepo = \"P"
  }
]

About this extraction

This page contains the full source code of the kattouf/ProgressLine GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 48 files (77.9 KB), approximately 22.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!