philocalyst/infat/main 14k tokens More Tools
```
├── .github/
   ├── workflows/
      ├── release.yml (1200 tokens)
├── .gitignore
├── CHANGELOG.md (3.4k tokens)
├── Completions/
   ├── _infat (600 tokens)
   ├── infat (500 tokens)
   ├── infat.fish (600 tokens)
├── LICENSE (omitted)
├── Package.swift (200 tokens)
├── README.md (1000 tokens)
├── Sources/
   ├── AssociationManager.swift (1000 tokens)
   ├── Commands/
      ├── Info.swift (1000 tokens)
      ├── Set.swift (600 tokens)
   ├── ConfigManager.swift (900 tokens)
   ├── ConformanceTypes.swift (500 tokens)
   ├── Error.swift (600 tokens)
   ├── FileSystemUtilities.swift (400 tokens)
   ├── FileUTIInfo.swift (100 tokens)
   ├── infat.swift (500 tokens)
├── dist/
   ├── infat-arm64-apple-macos
├── justfile (1000 tokens)
```


## /.github/workflows/release.yml

```yml path="/.github/workflows/release.yml" 
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - '*' # Trigger on any tag push

defaults:
  run:
    shell: bash

env:
  # Project name used in the justfile for artifact naming
  BINARY_NAME: infat

jobs:
  prerelease:
    # This job determines if the tag is a pre-release based on its format.
    # It remains unchanged as it controls the GitHub Release 'prerelease' flag.
    runs-on: macos-latest
    outputs:
      value: ${{ steps.prerelease.outputs.value }}
    steps:
      - name: Prerelease Check
        id: prerelease
        run: |
          # extract just the tag name (e.g. v1.2.3-alpha)
          tag=${GITHUB_REF##*/}

          # if it ends in -alpha or -beta → prerelease
          if [[ "$tag" =~ -(alpha|beta)$ ]]; then
            echo "value=true" >> $GITHUB_OUTPUT
          else
            echo "value=false" >> $GITHUB_OUTPUT
          fi

  package:
    # This job builds and packages the project for various targets using the justfile.
    strategy:
      fail-fast: false # Don't cancel other jobs if one fails
      matrix:
        target:
          - arm64-apple-macos
          - x86_64-apple-macos
        include:
          # Define OS and specific flags for cross-compilation targets
          - target: arm64-apple-macos
            os: macos-latest
            target_flags: ''
          - target: x86_64-apple-macos
            os: macos-latest
            target_flags: ''

    runs-on: ${{ matrix.os }}
    needs:
      - prerelease # Wait for prerelease check

    environment:
      name: main
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # Cache the Swift Package Manager cache
      - name: Cache Swift Package Manager
        uses: actions/cache@v3
        with:
          path: |
            ~/.swiftpm
          key: ${{ runner.os }}-swiftpm-${{ hashFiles('**/Package.swift') }}
          restore-keys: |
            ${{ runner.os }}-swiftpm-

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

      - name: Install Just (Command Runner)
        run: |
          set -euxo pipefail

          # Using || true to prevent failure if package isn't found to catch on fallback
          # Try installing via native package manager first
            if [[ "$RUNNER_OS" == "Linux" ]]; then
              echo "Attempting to install just via apt..."
              sudo apt-get update -y
              sudo apt-get install -y just || echo "apt install failed or package not found."
            elif [[ "$RUNNER_OS" == "macOS" ]]; then
              echo "Attempting to install just via Homebrew..."
              brew install just || echo "brew install failed."
            elif [[ "$RUNNER_OS" == "Windows" ]]; then
              echo "Attempting to install just via Chocolatey..."
              choco install just --yes || echo "choco install failed."
            else
              echo "Unsupported OS for package manager installation: $RUNNER_OS."
            fi

            # Fallback to cargo install if 'just' command is not found after package manager attempt
            if ! command -v just &>/dev/null; then
              echo "Just not found after package manager attempt. Installing via cargo install..."
              cargo install just
            else
              echo "Just installed successfully via package manager or was already present."
            fi

      # --- Build using Just ---
      - name: Build the release version
        # Set flags combining global and target-specific flags for swift build inside just
        env:
          SWIFT_FLAGS: ${{ matrix.target_flags }}
        # Run the just recipe, passing the target from the matrix, outputs to "dist/"
        run: just package ${{ matrix.target }}

      - name: Compress the binaries
        run: just compress-binaries "dist/"

      # --- Publish Artifact ---
      - name: Determine Artifact Name
        id: artifact_name
        run: |
          ARTIFACT_PATH="dist/${{ env.BINARY_NAME }}-${{ matrix.target }}"
          # Get the archived version
          echo "path=${ARTIFACT_PATH}.tar.gz" >> $GITHUB_OUTPUT

      - name: Extract changelog for the tag
        id: extract_changelog
        run: just create-notes ${{ github.ref_name }} release_notes.md CHANGELOG.md

      - name: Publish Release
        uses: softprops/action-gh-release@v2
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: ${{ steps.artifact_name.outputs.path }}
          body_path: release_notes.md
          draft: false
          overwrite: true
          prerelease: ${{ needs.prerelease.outputs.value }}
          make_latest: true
          token: ${{ secrets.PAT }}

  checksum:
    # This job downloads all published artifacts and creates a checksum file.
    runs-on: ubuntu-latest
    needs:
      - package # Wait for all package jobs to potentially complete
      - prerelease
    # Only run for tag pushes
    if: startsWith(github.ref, 'refs/tags/')

    environment:
      name: main

    steps:
      - name: Install GitHub CLI
        run: sudo apt-get update && sudo apt-get install -y gh

      - name: Download Release Archives
        env:
          # Use PAT for gh CLI authentication
          GH_TOKEN: ${{ secrets.PAT }}
          # Get the tag name from the ref
          TAG_NAME: ${{ github.ref_name }}
        run: |
          gh release download "$TAG_NAME" \
            --repo "$GITHUB_REPOSITORY" \
            --pattern '*' \
            --dir release

      - name: Create Checksums
        run: |
          find release/ -type f \
          ! -name "checksums.sha256" \
          ! -name "README*" \
          ! -name "*.sha256" \
          -print0 \
          | while IFS= read -r -d '' file; do
            sha256sum "$file" > "$file.sha256"
          done


      - name: Publish Individual Checksums
        uses: softprops/action-gh-release@v2
        with:
          # Use a wildcard to upload all generated .sha256 files from the release dir
          files: release/*.sha256
          draft: false
          prerelease: ${{ needs.prerelease.outputs.value }}
          token: ${{ secrets.PAT }}

```

## /.gitignore

```gitignore path="/.gitignore" 
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved

```

## /CHANGELOG.md

# Changelog

All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [2.4.0] – 2025-05-22

### Added
- Introduce a new `--robust` flag to the `infat set` command and configuration loader.  
  When enabled, missing applications are no longer treated as errors; instead you’ll see  
  a clear warning:
  ```
  Application '<name>' not found but ignoring due to passed options
  ```
- Add a `format` target to the Justfile (`just format`), which runs `swift-format` on all  
  `.swift` source files for consistent code styling.

## [2.3.4] – 2025-04-29

### Changed
- Loading order. Now goes Types -> Extensions -> Schemes.

## [2.3.3] – 2025-04-29

### Added
- Support XDG Base Directory spec for configuration file search: respect  
  `XDG_CONFIG_HOME` (default `~/.config/infat/config.toml`) and  
  `XDG_CONFIG_DIRS` (default `/etc/xdg/infat/config.toml`).
- Add a GitHub Actions **homebrew** job to automatically bump the Homebrew  
  formula on tagged releases.

### Changed
- Refactor Zsh, Bash and Fish completion scripts to use the official file-type  
  list and improve argument parsing.
- Update README:
  - Change Homebrew installation to `brew install infat`.
  - Add instructions for manual generation of shell completions until the formula  
    supports them.
- Update `.github/workflows/release.yml` to integrate the Homebrew bump step.

### Fixed
- Correct README misdocumentation by updating the list of supported file supertypes.

## [2.3.2] – 2025-04-27

### Fixed
- Set `overwrite: true` in the GitHub Actions release workflow to ensure existing releases can be replaced.
- Refine the `just check` recipe to ignore `CHANGELOG*`, `README*`, `Package*` files and the `.build` directory when scanning for version patterns.
- Update the `compress-binaries` recipe in Justfile so that archives
  - strip version suffixes from file names  
  - use only the base filename when creating the `.tar.gz`  

## [2.3.1] – 2025-04-27

### Changed
- Print success messages in italic formatting for `infat set` commands (file, scheme, and supertype bindings).
- Clarify README instructions: allow user-relative paths via `~` and note that shell expansions are not supported.

### Fixed
- Remove duplicate `run` step in the GitHub Actions `release.yml` workflow.

## [2.3.0] – 2025-04-27

### Added
- compress-binaries: use the project’s `infat` name to create `.tar.gz` archives with un-versioned internal filenames  
- AssociationManager: support relative paths (tilde expansion) and file:// URLs in `findApplication(named:)`

### Changed
- restyled `justfile` with decorative section markers and switched to `/`-based path concatenation for clarity

### Fixed
- release workflow: replaced `overwrite: true` with `make_latest: true` to correctly mark the latest GitHub release  
- prerelease check in Actions: now properly detects `-alpha` and `-beta` tags

## [2.2.0] – 2025-04-26

### Added
- Introduce ColorizeSwift (v1.5.0) as a new dependency for rich terminal styling  
- Reroute `.html` file‐extension and `https` URL‐scheme inputs to the HTTP handler  
- Support colorized output styling via `.bold()` and `.underline()` in `ConfigManager`

### Changed
- Replace custom ANSI escape‐sequence constants with ColorizeSwift’s `.bold()` and `.underline()` methods  
- Docs updates:
  - Clarify application name casing and optional `.app` suffix in README  
  - Expand configuration section to cover three TOML tables and XDG_CONFIG_HOME usage  
  - Correct CLI usage examples, header numbering, typos, and outdated information  

## [2.1.0] – 2025-04-26

### Added
- Justfile  
  Introduce a `check` task that prompts you to confirm version bumps in the README, Swift bundle and CHANGELOG.
- Commands  
  Print a success message when an application is bound to a file extension or URL scheme.
- FileSystemUtilities  
  Include `/System/Library/CoreServices/Applications/` in the list of search paths for installed apps.
- AssociationManager  
  Add a fallback for `setDefaultApplication` failures: if `NSWorkspace.setDefaultApplication` is restricted, catch the error and invoke `LSSetDefaultRoleHandlerForContentType` directly.

### Changed
- Package.swift  
  Pin all external Swift package dependencies to exact versions (ArgumentParser 1.2.0, Swift-Log 1.5.3, PListKit 2.0.3, swift-toml 1.0.0).
- AssociationManager  
  Refactor application lookup into a throwing `findApplication(named:)`, supporting both file paths (or file:// URLs) and plain `.app` names (case-insensitive).
- FileSystemUtilities  
  Downgrade log level for unreadable paths from **warning** to **debug** to reduce noise.

## [2.0.1] – 2025-04-25

### Added
- Support for cascading “blanket” types: use `infat set <App> --type <type>` to
  assign openers for base types (e.g. `plain-text`); introduced a new
  `[types]` table in the TOML schema.
- Explicit handling when no config is provided or found: Infat now prints
  an informative prompt and throws `InfatError.missingOption` if neither
  `--config` nor `$XDG_CONFIG_HOME/infat/config.toml` exist.

### Changed
- Bumped CLI version to **2.0.1** and updated the abstract to
  “Declaritively set associations for URLs and files.”
- Revised README examples and docs:
  - Renamed `infat list` → `infat info`
  - Changed flag `--file-type` → `--ext`
  - Renumbered tutorial steps and cleaned up formatting
  - Updated TOML example: `[files]` → `[extensions]`

### Fixed
- Quiet mode now logs at `warning` (was `error`), preventing silent failures.

## [2.0.0] – 2025-04-25

### Added
- Enforce that exactly one of `--scheme`, `--type`, or `--ext` is provided in both the Info and Set commands; throw clear errors when options are missing or conflicting.  
- Introduce a new `--type` option to the Info command, allowing users to list both the default and all registered applications for a given supertype (e.g. `text`).  
- Add the `plain-text` supertype (mapped to `UTType.plainText`) to the set of supported conformances.  
- Render configuration section headings (`[extensions]`, `[types]`, `[schemes]`) in bold & underlined text when processing TOML files.

### Changed
- Require at least one of the `[extensions]`, `[types]`, or `[schemes]` tables in the TOML configuration (instead of mandating all); process each table if present, emit a debug-level log when a table is missing, and standardize table naming.  
- Change the default logging level for verbose mode from `debug` to `trace`.  
- Strengthen error handling in `_setDefaultApplication`: after attempting to set a default opener, verify success and log an info on success or a warning on failure.

### Deprecated
- Rename the `List` command to `Info`. 
- Renamed files table to extensions to match with cli options

## [1.3.0] – 2025-04-25

### Added
- `--app` option to `infat list` for listing document types handled by a given application.  
- New `InfatError.conflictingOptions` to enforce that exactly one of `--app` or `--ext` is provided.  
- Enhanced UTI-derivation errors via `InfatError.couldNotDeriveUTI`, including the offending extension in the message.

### Changed
- Refactored the `list` command to use two exclusive `@Option` parameters (`--app`, `--ext`) with XOR validation.  
- Switched PList parsing to `DictionaryPList(url:)` and UTI lookup to `UTType(filenameExtension:)`.  
- Replaced ad-hoc `print` calls with `logger.info` for consistent, leveled logging.  
- Renamed `deriveUTIFromExtension(extention:)` to `deriveUTIFromExtension(ext:)` for clarity and consistency.

### Fixed
- Corrected typos in `FileSystemUtilities.deriveUTIFromExtension` signature and related debug messages.  
- Fixed `FileManager` existence checks for `Info.plist` by using the correct `path` property.  
- Resolved parsing discrepancies in `listTypesForApp` to ensure accurate reading of `CFBundleDocumentTypes`.

## [1.2.0] – 2025-04-25

### Fixed
- Swift badge reflects true version

### Changed
- Using function overloading to set default application based on Uttype or extension

### Deprecated
- Removed --assocations option in list into the basic list command
- Filetype option, now ext.

### Added
- A supertype conformance enum
- Class option in CLI and Config

## [1.1.0] – 2025-04-24

### Added
- Add MIT License (`LICENSE`) under the MIT terms.
- Justfile enhancements:
  - Require `just` (command-runner) in documentation.
  - Introduce a `package` recipe (`just package`) to build and bundle release binaries.
  - Detect `current_platform` dynamically via `uname -m`.
- GitHub Actions release workflow: enable `overwrite: true` in `release.yml`.

### Changed
- Migrate `setDefaultApplication` and `ConfigManager.loadConfig` to async/await; remove semaphore-based callbacks.
- Simplify UTI resolution by passing `typeIdentifier` directly.
- Documentation updates:
  - Clarify README summary and usage examples for file-type and URL-scheme associations.
  - Revamp badges and stylistic copy (“ultra-powerful” intro, more user-friendly tone).
  - Streamline source installation instructions (use `just package` and wildcard install).

### Fixed
- Remove redundant separators in README.

## [1.0.0] - 2025-04-24

### Added
- Support for URL‐scheme associations in the `set` command via a new `--scheme` option  
- `InfatError.conflictingOptions` error case to enforce mutual exclusion of `--file-type` and `--scheme`  
- Unified binding functionality—`set` now handles both file‐type and URL‐scheme associations, replacing the standalone `bind` command  

### Changed
- Merged the former `bind` subcommand into `set` and switched its parameters from positional arguments to named options  
- Updated the `justfile` changelog target to use a top‐level `# What's new` header instead of `## Changes`  

### Removed
- Removed the standalone `Bind` subcommand and its `Bind.swift` implementation  
- Removed the `Info` subcommand (and `Info.swift`), which previously displayed system information  

## [0.6.0] - 2025-04-24

### Added
* Homebrew support

## [0.5.3] - 2025-04-24

### Fixed
* Typos in CI

## [0.5.2] - 2025-04-24

### Fixed
* Justfile platform targeting for the CI

## [0.5.1] - 2025-04-24

### Fixed
* Fixed logging to print diff in List command.
* Fixed Swift version in release workflow to a specific version instead of 'latest'.

## [0.5.0] – 2025-04-24

### Fixed
* Wrong swift toolchain action

## [0.4.0] – 2025-04-24

### Added
* Config support for schemes
* Bind subcommand to set URL scheme associations
* GitHub workflow for automated releases
* `create-notes` just recipe to extract changelog entries for release notes

### Changed
* Moved app name resolution logic to a function for better reusability
* Changed argument order in `setURLHandler` function
* Optimized Swift release flags for better performance
* Updated changelog to reflect the current state of the project

### Deprecated
* Associations table in config; it has been replaced by separate tables for files and schemes

### Fixed
* Logic in the Bind command to correctly handle application URL resolution and error handling

## [0.3.0] – 2025-04-23

### Added
- Support loading configuration from the XDG config directory (`$XDG_CONFIG_HOME/infat/config.toml`) when no `--config` flag is supplied.  
- Add a `Justfile` with curated recipes for:
  - building (debug / release)  
  - running (debug / release)  
  - packaging and compressing binaries  
  - generating checksums  
  - installing (and force-installing)  
  - cleaning and updating dependencies  

## [0.2.0] - 2025-04-22

### Added
- Initial project setup with basic command structure (`list`, `set`, `info`) using `swift-argument-parser`.
- Structured logging using `swift-log`.
- Custom error handling via the `InfatError` enum for specific error conditions.
- Defined `FileUTIInfo` struct for holding Uniform Type Identifier data.
- Added utility `findApplications` to locate application bundles in standard macOS directories.
- Added utility `deriveUTIFromExtension` to get UTI info from file extensions, requiring macOS 11.0+.
- Added utility `getBundleName` to extract bundle identifiers from application `Info.plist`.
- Implemented `list` subcommand to show default or all registered applications for a file type, using the logger for output.
- Implemented `set` subcommand to associate a file type with a specified application.
- Implemented `info` subcommand to display details about the current frontmost application.
- Added support for loading file associations from a TOML configuration file (`--config`), including specific error handling for TOML format issues and correcting an initial table name typo ("associations").
- Added dependencies: `PListKit` (for `Info.plist` parsing) and `swift-toml` (for configuration file parsing).
- Added shell completion scripts for Zsh, Bash, and Fish.
- Added comprehensive `README.md` documentation detailing features, usage, installation, and dependencies, with corrected links.

### Changed
- Renamed project, executable target, and main command struct from "bart"/"WorkspaceTool" to "infat".
- Refactored codebase from a single `main.swift` into multiple files (`Commands`, `Utilities`, `Managers`, `Error`, etc.) for better organization and readability.
- Updated the tool's abstract description for better clarity.
- Improved the output formatting of the `list` command for enhanced readability.
- Refactored `list` command options (using `--assigned` flag instead of `--all`, requiring identifier argument) and improved help descriptions.
- Consolidated logging flags: replaced previous `--debug` and `--verbose` flags with a single `--verbose` flag (which includes debug level) and a `--quiet` flag for minimal output.
- Made the global logger instance mutable (`var`) to allow runtime log level configuration based on flags.
- Created a reusable `setDefaultApplication` function to avoid code duplication between the `set` command and configuration loading logic.
- Significantly enhanced error handling with more specific `InfatError` cases (e.g., plist reading, timeout, configuration errors) and improved logging messages throughout the application.
- Implemented a 10-second timeout for the asynchronous `setDefaultApplication` operation using `DispatchSemaphore`.
- Updated `findApplications` utility to search `/System/Applications` in addition to other standard paths and use modern Swift API for home directory path resolution.
- Switched from using UTI strings to `UTType` objects within `FileUTIInfo` and related functions for better type safety and access to UTI properties.
- Updated `README.md` content, added TOML configuration documentation, and noted `set` command status (reflecting commit `1ec6358`).
- Set the minimum required macOS deployment target to 13.0 in `Package.swift`.
- Renamed the `set` command argument from `mimeType` to `fileType` for clarity.
- Updated the main command struct (`Infat`) and removed the redundant explicit `--version` flag (ArgumentParser provides this by default).
- Added `Package.resolved` to `.gitignore`.

### Fixed
- Corrected the bundle ID used internally and for logging from `com.example.burt` to `com.philocalyst.infat`.
- Addressed minor code formatting inconsistencies across several files.

---

[Unreleased]: https://github.com/philocalyst/infat/compare/v2.4.0...HEAD
[2.4.0]: https://github.com/philocalyst/infat/compare/v2.3.4...v2.4.0  
[2.3.3]: https://github.com/philocalyst/infat/compare/v2.3.2...v2.3.3  
[2.3.2]: https://github.com/your-org/your-repo/compare/v2.3.1...v2.3.2
[2.3.1]: https://github.com/your-org/your-repo/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/your-org/your-repo/compare/v2.2.0...v2.3.0
[2.2.0]:     https://github.com/philocalyst/infat/compare/v2.1.0...v2.2.0
[2.1.0]: https://github.com/your-org/your-repo/compare/v2.0.1...v2.1.0  
[2.0.1]: https://github.com/philocalyst/infat/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/yourorg/yourrepo/compare/v1.3.0...v2.0.0  
[1.3.0]: https://github.com/your-org/infat/compare/v1.2.0...v1.3.0  
[1.2.0]: https://github.com/philocalyst/infat/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/philocalyst/infat/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/philocalyst/infat/compare/v0.6.0...v1.0.0 
[0.6.0]: https://github.com/philocalyst/infat/compare/v0.5.3...v0.6.0
[0.5.3]: https://github.com/philocalyst/infat/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/philocalyst/infat/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/philocalyst/infat/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/philocalyst/infat/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/philocalyst/infat/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/philocalyst/infat/compare/v0.2.0...v0.3.0  
[0.2.0]: https://github.com/philocalyst/infat/compare/63822faf94def58bf347f8be4983e62da90383bb...d32aec000bf040c48887f104decf4a9736aea78b (Comparing agaisnt the start of the project)


## /Completions/_infat

``` path="/Completions/_infat" 
#compdef infat
local context state state_descr line
_infat_commandname=$words[1]
typeset -A opt_args

_infat() {
    integer ret=1
    local -a args
    args+=(
        '(-c --config)'{-c,--config}'[Path to the configuration file.]:config:'
        '(-v --verbose)'{-v,--verbose}'[Enable verbose logging.]'
        '(-q --quiet)'{-q,--quiet}'[Quiet output.]'
        '--version[Show the version.]'
        '(-h --help)'{-h,--help}'[Show help information.]'
        '(-): :->command'
        '(-)*:: :->arg'
    )
    _arguments -w -s -S $args[@] && ret=0
    case $state in
        (command)
            local subcommands
            subcommands=(
                'info:Lists file association information.'
                'set:Sets an application association.'
                'help:Show subcommand help information.'
            )
            _describe "subcommand" subcommands
            ;;
        (arg)
            case ${words[1]} in
                (info)
                    _infat_info
                    ;;
                (set)
                    _infat_set
                    ;;
                (help)
                    _infat_help
                    ;;
            esac
            ;;
    esac

    return ret
}

_infat_info() {
    integer ret=1
    local -a args
    args+=(
        '(-a --app)'{-a,--app}'[Application name (e.g., '"'"'Google Chrome'"'"').]:app:'
        '(-e --ext)'{-e,--ext}'[File extension (without the dot, e.g., '"'"'html'"'"').]:ext:'
        '(-t --type)'{-t,--type}'[File type (e.g., text).]:type:(plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle)'
        '--version[Show the version.]'
        '(-h --help)'{-h,--help}'[Show help information.]'
    )
    _arguments -w -s -S $args[@] && ret=0

    return ret
}

_infat_set() {
    integer ret=1
    local -a args
    args+=(
        ':app-name:'
        '--ext[A file extension without leading dot.]:ext:'
        '--scheme[A URL scheme. ex: mailto.]:scheme:'
        '--type[A file class. ex: image]:type:(plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle)'
        '--version[Show the version.]'
        '(-h --help)'{-h,--help}'[Show help information.]'
    )
    _arguments -w -s -S $args[@] && ret=0

    return ret
}

_infat_help() {
    integer ret=1
    local -a args
    args+=(
        ':subcommands:'
        '--version[Show the version.]'
    )
    _arguments -w -s -S $args[@] && ret=0

    return ret
}


_custom_completion() {
    local completions=("${(@f)$($*)}")
    _describe '' completions
}

_infat

```

## /Completions/infat

``` path="/Completions/infat" 
#!/bin/bash

_infat() {
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    COMPREPLY=()
    opts="-c --config -v --verbose -q --quiet --version -h --help info set help"
    if [[ $COMP_CWORD == "1" ]]; then
        COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
        return
    fi
    case $prev in
        -c|--config)

            return
        ;;
    esac
    case ${COMP_WORDS[1]} in
        (info)
            _infat_info 2
            return
            ;;
        (set)
            _infat_set 2
            return
            ;;
        (help)
            _infat_help 2
            return
            ;;
    esac
    COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
}
_infat_info() {
    opts="-a --app -e --ext -t --type --version -h --help"
    if [[ $COMP_CWORD == "$1" ]]; then
        COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
        return
    fi
    case $prev in
        -a|--app)

            return
        ;;
        -e|--ext)

            return
        ;;
        -t|--type)
            COMPREPLY=( $(compgen -W "plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle" -- "$cur") )
            return
        ;;
    esac
    COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
}
_infat_set() {
    opts="--ext --scheme --type --version -h --help"
    if [[ $COMP_CWORD == "$1" ]]; then
        COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
        return
    fi
    case $prev in
        --ext)

            return
        ;;
        --scheme)

            return
        ;;
        --type)
            COMPREPLY=( $(compgen -W "plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle" -- "$cur") )
            return
        ;;
    esac
    COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
}
_infat_help() {
    opts="--version"
    if [[ $COMP_CWORD == "$1" ]]; then
        COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
        return
    fi
    COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
}


complete -F _infat infat

```

## /Completions/infat.fish

```fish path="/Completions/infat.fish" 
function _swift_infat_using_command
    set -l cmd (commandline -opc)
    if [ (count $cmd) -eq (count $argv) ]
        for i in (seq (count $argv))
            if [ $cmd[$i] != $argv[$i] ]
                return 1
            end
        end
        return 0
    end
    return 1
end
complete -c infat -n '_swift_infat_using_command infat' -f -r -s c -l config -d 'Path to the configuration file.'
complete -c infat -n '_swift_infat_using_command infat' -f -s v -l verbose -d 'Enable verbose logging.'
complete -c infat -n '_swift_infat_using_command infat' -f -s q -l quiet -d 'Quiet output.'
complete -c infat -n '_swift_infat_using_command infat' -f -l version -d 'Show the version.'
complete -c infat -n '_swift_infat_using_command infat' -f -s h -l help -d 'Show help information.'
complete -c infat -n '_swift_infat_using_command infat' -f -a info -d 'Lists file association information.'
complete -c infat -n '_swift_infat_using_command infat' -f -a set -d 'Sets an application association.'
complete -c infat -n '_swift_infat_using_command infat' -f -a help -d 'Show subcommand help information.'
complete -c infat -n '_swift_infat_using_command infat info' -f -r -s a -l app -d 'Application name (e.g., \'Google Chrome\').'
complete -c infat -n '_swift_infat_using_command infat info' -f -r -s e -l ext -d 'File extension (without the dot, e.g., \'html\').'
complete -c infat -n '_swift_infat_using_command infat info' -f -r -s t -l type -d 'File type (e.g., text).'
complete -c infat -n '_swift_infat_using_command infat info -t' -f -k -a 'plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle'
complete -c infat -n '_swift_infat_using_command infat info --type' -f -k -a 'plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle'
complete -c infat -n '_swift_infat_using_command infat info' -f -s h -l help -d 'Show help information.'
complete -c infat -n '_swift_infat_using_command infat set' -f -r -l ext -d 'A file extension without leading dot.'
complete -c infat -n '_swift_infat_using_command infat set' -f -r -l scheme -d 'A URL scheme. ex: mailto.'
complete -c infat -n '_swift_infat_using_command infat set' -f -r -l type -d 'A file class. ex: image'
complete -c infat -n '_swift_infat_using_command infat set --type' -f -k -a 'plain-text text csv image raw-image audio video movie mp4-audio quicktime mp4-movie archive sourcecode c-source cpp-source objc-source shell makefile data directory folder symlink executable unix-executable app-bundle'
complete -c infat -n '_swift_infat_using_command infat set' -f -s h -l help -d 'Show help information.'

```

## /Package.swift

```swift path="/Package.swift" 
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "infat",
  platforms: [
    .macOS(.v13)
  ],

  dependencies: [
    .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.2.0"),
    .package(url: "https://github.com/apple/swift-log.git", exact: "1.5.3"),
    .package(url: "https://github.com/orchetect/PListKit", exact: "2.0.3"),
    .package(url: "https://github.com/jdfergason/swift-toml", exact: "1.0.0"),
    .package(url: "https://github.com/mtynior/ColorizeSwift.git", exact: "1.5.0"),
  ],
  targets: [
    .executableTarget(
      name: "infat",
      dependencies: [
        .product(name: "PListKit", package: "PListKit"),
        .product(name: "Toml", package: "swift-toml"),
        "ColorizeSwift",
        .product(name: "ArgumentParser", package: "swift-argument-parser"),
        .product(name: "Logging", package: "swift-log"),
      ]
    )
  ]
)

```

## /README.md

# Welcome to Infat

[![Swift Version](https://badgen.net/static/Swift/5.9/orange)](https://swift.org)
[![Apple Platform](https://badgen.net/badge/icon/macOS%2013+?icon=apple&label)](https://developer.apple.com/macOS)

Infat is an ultra-powerful, macOS-native CLI tool for declaritively managing both file-type and URL-scheme associations. Avoid the hassle of navigating sub-menus to setup your default browser or image viewer, and the pain of doing that *every time* you get a new machine. Setup the rules once, and bask in your own ingenuity forevermore. Take back control, and bind your openers to whatever. You. Want. Override everything! Who's going to stop you?

---

## Summary

- List which apps open for a given file extension or URL scheme (Like when you double click a file in Finder)
- Set a default application for a file extension or URL scheme  
- Load associations from a TOML config (`[extensions]` `[types]` and `[schemes]` tables)  
- Verbose, scriptable, and ideal for power users and admins  

## Get Started

Get started by installing Infat — jump to the [Install](#install) section below.

## Tutorial

### 1. Getting assocation information

```shell
# Show the default app for .txt files and all registered apps
infat info --ext txt
```

### 2. Setting a Default Application
> [!TIP]
> These aren't strict extensions, for example, yml and yaml extensions share a common resolver.

```shell
# Use TextEdit for .md files
infat set TextEdit --ext md

# Use VSCode for .json files
infat set VSCode --ext json
```

### 3. Binding a URL Scheme

```shell
# Use Mail.app for mailto: links
infat set Mail --scheme mailto
```

### 4. Fallback types

> [!TIP]
> Openers are cascading in macOS. Most common file formats will have their own identifier,
> Which will be read from before the plain-text type it inherits from
> Try setting from extension if you face unexpected issues

```shell
# Set VSCode as the opener for files containing text
infat set VSCode --type plain-text
```

Infat currently supports these supertypes:

- plain-text
- text
- csv
- image
- raw-image
- audio
- video
- movie
- mp4-audio
- quicktime
- mp4-movie
- archive
- sourcecode
- c-source
- cpp-source
- objc-source
- shell
- makefile
- data
- directory
- folder
- symlink
- executable
- unix-executable
- app-bundle

### 5. Configuration

Place a TOML file at `$XDG_CONFIG_HOME/infat/config.toml` (or pass `--config path/to/config.toml`). 

> [!NOTE] 
> `$XDG_CONFIG_HOME` is not set by default, you need to set in your shell config ex: `.zshenv`.

On the right is the app you want to bind. You can pass:
1. The name (As seen when you hover on the icon) **IF** It's in a default location.
2. The relative path (To your user directory: ~)
3. The absolute path

All case sensitive, all can be with or without a .app suffix, and no shell expansions...

```toml
[extensions]
md    = "TextEdit"
html  = "Safari"
pdf   = "Preview"

[schemes]
mailto = "Mail"
web    = "Safari"

[types]
plain-text = "VSCode"
```

Run without arguments to apply all entries.

```shell
infat --config ~/.config/infat/config.toml
```

---

## Design Philosophy

- **Minimal & Scriptable**  
  Infat is a single-binary tool that plays well in shells and automation pipelines.

- **macOS-First**  
  Leverages native `NSWorkspace`, Launch Services, and UTType for robust integration.

- **Declarative Configuration**  
  TOML support allows you to version-control your associations alongside other dotfiles.

## Building and Debugging

You’ll need [just](https://github.com/casey/just) and Swift 5.9+:

```shell
# Debug build
just build

# Release build
just build-release

# Run in debug mode
just run "list txt"

# Enable verbose logging for troubleshooting
infat --verbose info --ext pdf
```

---

## Install

### Homebrew

```shell
brew update # Optional but recommended
brew install infat
```

### From Source

Please make sure `just` (our command-runner) is installed before running. If you don't want to use `just`, the project is managed with SPM, and you can build with "Swift build -c release" and move the result in the .build folder to wherever. 

```shell
git clone https://github.com/philocalyst/infat.git && cd infat
just package && mv dist/infat* /usr/local/bin/infat # Wildcard because output name includes platform
```

## Changelog

For the full history of changes, see [CHANGELOG.md](CHANGELOG.md).

## Libraries Used

- [ArgumentParser](https://github.com/apple/swift-argument-parser)  
- [swift-log](https://github.com/apple/swift-log)  
- [PListKit](https://github.com/orchetect/PListKit)  
- [swift-toml](https://github.com/jdfergason/swift-toml)  

## Acknowledgements

- Inspired by [duti](https://github.com/moretension/duti)  
- Built with Swift, thanks to corporate overlord Apple’s frameworks  
- Thanks to all contributors and issue submitters (One day!!)

## License

Infat is licensed under the [MIT License](LICENSE).  
Feel free to use, modify, and distribute!


## /Sources/AssociationManager.swift

```swift path="/Sources/AssociationManager.swift" 
import AppKit
import Foundation
import Logging
import PListKit
import UniformTypeIdentifiers

func getBundleIdentifier(appURL: URL) throws -> String? {
  let plistURL =
    appURL
    .appendingPathComponent("Contents")
    .appendingPathComponent("Info.plist")
  do {
    let plist = try DictionaryPList(url: plistURL)
    return plist.root.string(key: "CFBundleIdentifier").value
  } catch {
    throw InfatError.plistReadError(
      path: plistURL.path,
      underlyingError: error)
  }
}

func setURLHandler(appName: String, scheme: String) throws {
  let applicationURL = try findApplication(named: appName)
  if let appURL = applicationURL {

    let appBundleIdentifier = try getBundleIdentifier(appURL: appURL)
    if let appBundleID = appBundleIdentifier {
      // Register the URL in the launch services database, and yes, update information for the app if it already exists.
      let registerResultCode = LSRegisterURL(appURL as CFURL, true)
      if registerResultCode != 0 {
        throw InfatError.cannotRegisterURL(error: registerResultCode)
      }

      // Takes URL scheme and bundle ID, as CF strings and sets the handler using a deprecated method. Risky code, until big papa Apple releases an alternative.
      let setResultCode = LSSetDefaultHandlerForURLScheme(
        scheme as CFString, appBundleID as CFString)
      if setResultCode != 0 {
        throw InfatError.cannotRegisterURL(error: setResultCode)
      }
    } else {
      throw InfatError.cannotSetURL(appName: appURL.path())
    }
  }
}

func findApplication(named key: String) throws -> URL? {
  let fullKey = (key as NSString).expandingTildeInPath
  let fm = FileManager.default

  // |1| Normalize the key in case user provided a ".app" extension.
  let rawExt = (fullKey as NSString).pathExtension.lowercased()
  let baseName =
    rawExt == "app"
    ? (fullKey as NSString).deletingPathExtension
    : fullKey

  // |2| If this is a valid file‐system path or a file:// URL,
  // perform basic checks and return instantly if a .app bundle.

  let isFileURL = (URL(string: fullKey)?.isFileURL) ?? false
  if isFileURL || fm.fileExists(atPath: fullKey) {
    // Initalize properly depending on type of URL
    let url =
      isFileURL
      ? URL(string: fullKey)!
      : URL(fileURLWithPath: fullKey)

    let r = try url.resourceValues(
      forKeys: [.isDirectoryKey, .typeIdentifierKey]
    )
    // Final check, these attributes are required for app bundles.
    if r.isDirectory == true,
      let tid = r.typeIdentifier,
      let ut = UTType(tid),
      ut.conforms(to: .applicationBundle)
    {
      return url
    }
    return nil
  }

  // |3| Otherwise treat `key` as a provided app name: scan all installed .app bundles
  let installed = try FileSystemUtilities.findApplications()
  return installed.first {
    $0.deletingPathExtension()
      .lastPathComponent
      .caseInsensitiveCompare(baseName)
      == .orderedSame
  }
}

private func setDefaultApplication(
  appName: String, appURL: URL, typeIdentifier: UTType, inputDescription: String
) async throws {
  let workspace = NSWorkspace.shared
  do {
    try await workspace.setDefaultApplication(
      at: appURL,
      toOpen: typeIdentifier
    )
    // success!
  } catch {
    let nsErr = error as NSError
    // Detect the restriction
    let isFileOpenError =
      nsErr.domain == NSCocoaErrorDomain
      && nsErr.code == CocoaError.fileReadUnknown.rawValue

    guard isFileOpenError else {
      // Some other error—rethrow it
      throw error
    }

    // Fallback: call LSSetDefaultRoleHandlerForContentType directly
    guard let bundleID = try getBundleIdentifier(appURL: appURL)
    else {
      throw InfatError.applicationNotFound(name: appURL.path)
    }

    let utiCF = typeIdentifier.identifier as CFString
    let lsErr = LSSetDefaultRoleHandlerForContentType(
      utiCF,
      LSRolesMask.viewer,
      bundleID as CFString
    )
    guard lsErr == noErr else {
      // propagate the LaunchServices error
      throw InfatError.cannotRegisterURL(error: lsErr)
    }
  }
}

/// Sets the default application for a given file type specified by its extension.
func setDefaultApplication(appName: String, ext: String) async throws {
  guard let appURL = try findApplication(named: appName) else {
    throw InfatError.applicationNotFound(name: appName)
  }

  // Derive UTType from the extension string
  guard let uti = UTType(filenameExtension: ext) else {
    throw InfatError.couldNotDeriveUTI(msg: ext)
  }

  try await setDefaultApplication(
    appName: appName,
    appURL: appURL,
    typeIdentifier: uti,  // Pass the UTI string identifier
    inputDescription: ".\(ext)"  // For logging clarity
  )
}

/// Sets the default application for a given file type specified by its UTType.
func setDefaultApplication(appName: String, supertype: UTType) async throws {
  guard let appURL = try findApplication(named: appName) else {
    throw InfatError.applicationNotFound(name: appName)
  }

  try await setDefaultApplication(
    appName: appName,
    appURL: appURL,
    typeIdentifier: supertype,
    inputDescription: supertype.description
  )
}

```

## /Sources/Commands/Info.swift

```swift path="/Sources/Commands/Info.swift" 
import AppKit
import ArgumentParser
import Foundation
import PListKit
import UniformTypeIdentifiers

struct Info: ParsableCommand {
  static let configuration = CommandConfiguration(
    abstract: """
      Lists file association information.
      """
  )

  @Option(name: [.short, .long], help: "Application name (e.g., 'Google Chrome').")
  var app: String?

  @Option(name: [.short, .long], help: "File extension (without the dot, e.g., 'html').")
  var ext: String?

  @Option(name: [.short, .long], help: "File type (e.g., text).")
  var type: Supertypes?

  mutating func run() throws {
    let providedCount = [app != nil, ext != nil, type != nil]
      .filter { $0 }
      .count

    guard providedCount > 0 else {
      throw InfatError.missingOption
    }

    guard providedCount == 1 else {
      throw InfatError.conflictingOptions(
        error:
          "Either --scheme, --type, or --ext must be provided, but not more than one."
      )
    }

    if let appName = app {
      try listForApp(appName: appName)
    } else if let fileExtension = ext {
      try listForExtension(fileExtension: fileExtension)
    } else if let sType = type?.utType {
      try listForType(type: sType)
    }
  }

  // ▰▰▰ Helper Methods ▰▰▰

  private func listForType(type: UTType) throws {
    let workspace = NSWorkspace.shared

    if let defaultAppURL = workspace.urlForApplication(toOpen: type) {
      print("Default app: \(defaultAppURL.lastPathComponent) (\(defaultAppURL.path))")
    } else {
      print(
        "No default application registered for '.\(type.debugDescription)' (UTI: \(type.identifier))."
      )
    }

    let allAppURLs = workspace.urlsForApplications(toOpen: type)
    if !allAppURLs.isEmpty {
      print("\nAll registered apps:")
      allAppURLs.forEach { url in
        print("  • \(url.lastPathComponent) (\(url.path))")
      }
    } else {
      print(
        "No applications specifically registered for '.\(type.debugDescription)' (UTI: \(type.identifier))."
      )
    }
  }

  private func listForApp(appName: String) throws {
    logger.info("Looking for types handled by '\(appName)'...")

    guard let appPath = try findApplication(named: appName) else {
      throw InfatError.applicationNotFound(name: appName)
    }
    logger.info("Found application at: \(appPath)")

    let infoPlistPath =
      appPath
      .appendingPathComponent("Contents")
      .appendingPathComponent("Info.plist")

    guard FileManager.default.fileExists(atPath: infoPlistPath.path) else {
      throw InfatError.infoPlistNotFound(appPath: appPath.path)
    }

    let plist = try DictionaryPList(url: infoPlistPath)

    guard let documentTypes = plist.root.array(key: "CFBundleDocumentTypes").value
    else {
      print(
        "No 'CFBundleDocumentTypes' found in \(infoPlistPath). This app might not declare specific document types."
      )
      return
    }

    print("\nDeclared Document Types:")
    if documentTypes.isEmpty {
      print("  (None declared)")
      return
    }

    for item in documentTypes {
      if let docType = item as? PListDictionary {
        let typeName = docType
        print("  • \(typeName):")

        if let utis = docType["LSItemContentTypes"] as? [String], !utis.isEmpty {
          print("    - UTIs: \(utis.joined(separator: ", "))")
        } else {
          print("    - UTIs: (None specified)")
        }

        if let extensions = docType["CFBundleTypeExtensions"] as? [String],
          !extensions.isEmpty
        {
          print(
            "    - Extensions: \(extensions.map { ".\($0)" }.joined(separator: ", "))")
        } else {
          print("    - Extensions: (None specified)")
        }
        print("")
      }
    }
  }

  private func listForExtension(fileExtension: String) throws {
    print("Looking for apps associated with '.\(fileExtension)'...")

    guard let uti = deriveUTIFromExtension(extension: fileExtension) else {
      throw InfatError.couldNotDeriveUTI(msg: fileExtension)
    }
    print("Derived UTI: \(uti.identifier)")

    let workspace = NSWorkspace.shared

    if let defaultAppURL = workspace.urlForApplication(toOpen: uti) {
      print("Default app: \(defaultAppURL.lastPathComponent) (\(defaultAppURL.path))")
    } else {
      print(
        "No default application registered for '.\(fileExtension)' (UTI: \(uti.identifier))."
      )
    }

    let allAppURLs = workspace.urlsForApplications(toOpen: uti)
    if !allAppURLs.isEmpty {
      print("\nAll registered apps:")
      allAppURLs.forEach { url in
        print("  • \(url.lastPathComponent) (\(url.path))")
      }
    } else {
      print(
        "No applications specifically registered for '.\(fileExtension)' (UTI: \(uti.identifier))."
      )
    }
  }

  private func deriveUTIFromExtension(extension ext: String) -> UTType? {
    return UTType(filenameExtension: ext)
  }
}

```

## /Sources/Commands/Set.swift

```swift path="/Sources/Commands/Set.swift" 
import ArgumentParser
import Logging

extension Infat {
  struct Set: AsyncParsableCommand {
    static let configuration = CommandConfiguration(
      abstract: "Sets an application association."
    )
    @Argument(help: "The name of the application.")
    var appName: String

    @Option(name: .long, help: "A file extension without leading dot.")
    var ext: String?

    @Option(name: .long, help: "A URL scheme. ex: mailto.")
    var scheme: String?

    @Option(name: .long, help: "A file class. ex: image")
    var type: Supertypes?

    @Flag(name: .long, help: "Ignore missing app errors")
    var robust: Bool = false

    mutating func run() async throws {
      let providedCount = [scheme != nil, ext != nil, type != nil]
        .filter { $0 }
        .count

      guard providedCount > 0 else {
        throw InfatError.missingOption
      }

      guard providedCount == 1 else {
        throw InfatError.conflictingOptions(
          error:
            "Either --scheme, --type, or --ext must be provided, but not more than one."
        )
      }

      if let fType = ext {
        switch fType.lowercased() {
        case "html":
          // Route .html to the http URL handler
          try setURLHandler(appName: appName, scheme: "http")
          print("Successfully bound \(appName) to http")
        default:
          do {
            try await setDefaultApplication(
              appName: appName,
              ext: fType,
            )
          } catch InfatError.applicationNotFound(let name) {
            // Only throw for real if not robust
            if !robust {
              throw InfatError.applicationNotFound(name: name)
            }
            print(
              "Application '\(appName)' not found but ignoring due to passed options".bold().red())
            return
            // Otherwise just eat that thing up
          } catch {
            // propogate the rest
            throw error
          }
          print("Successfully bound \(appName) to \(fType)".italic())
        }

      } else if let schm = scheme {
        switch schm.lowercased() {
        case "https":
          // Route https to the http URL handler
          try setURLHandler(appName: appName, scheme: "http")
          print("Successfully bound \(appName) to http")
        default:
          try setURLHandler(
            appName: appName,
            scheme: schm
          )
          print("Successfully bound \(appName) to \(schm)".italic())
        }

      } else if let superType = type {
        if let sUTI = superType.utType {
          try await setDefaultApplication(
            appName: appName,
            supertype: sUTI
          )
        }
        print("Set default app for type \(superType) to \(appName)".italic())
      }
    }
  }
}

```

## /Sources/ConfigManager.swift

```swift path="/Sources/ConfigManager.swift" 
import ColorizeSwift
import Foundation
import Logging
import Toml

struct ConfigManager {
  static func loadConfig(from configPath: String) async throws {
    let tomlConfig = try Toml(contentsOfFile: configPath)

    // Convenience names
    let extensionTableName = "extensions"
    let schemeTableName = "schemes"
    let typeTableName = "types"

    // Check upfront that at least one table is present
    let hasExtensions = tomlConfig.table(extensionTableName) != nil
    let hasSchemes = tomlConfig.table(schemeTableName) != nil
    let hasTypes = tomlConfig.table(typeTableName) != nil

    guard hasExtensions || hasSchemes || hasTypes else {
      throw InfatError.noConfigTables(path: configPath)
    }

    // MARK: – Process [types]
    if let typeTable = tomlConfig.table(typeTableName) {
      logger.info("Processing [types] associations...")
      print("\(typeTableName.uppercased().bold().underline())")
      for key in typeTable.keyNames {
        let typeKey = key.components.joined()
        guard let appName = typeTable.string(key.components) else {
          logger.warning(
            "Value for key '\(typeKey)' in [types] is not a string. Skipping."
          )
          continue
        }
        guard let supertypeEnum = Supertypes(rawValue: typeKey) else {
          logger.error(
            "Invalid type key '\(typeKey)' found in [types]. Skipping."
          )
          continue
        }
        guard let targetUTType = supertypeEnum.utType else {
          logger.error(
            "Well-known type '\(typeKey)' not supported or invalid. Skipping."
          )
          throw InfatError.unsupportedOrInvalidSupertype(name: typeKey)
        }

        logger.debug(
          "Queueing default app for type \(typeKey) (\(targetUTType.identifier)) → \(appName)"
        )
        try await setDefaultApplication(
          appName: appName,
          supertype: targetUTType
        )
        print("Set type \(typeKey) → \(appName)")
      }
    } else {
      logger.debug("No [types] table found in \(configPath)")
    }

    // MARK: – Processs [extensions]
    if let extensionTable = tomlConfig.table(extensionTableName) {
      logger.info("Processing [extensions] associations...")
      print("\(extensionTableName.uppercased().bold().underline())")
      for key in extensionTable.keyNames {
        guard let appName = extensionTable.string(key.components) else {
          throw InfatError.tomlValueNotString(
            path: configPath,
            key: key.components.joined()
          )
        }
        let ext = key.components.joined()
        switch ext.lowercased() {
        case "html":
          // Route .html to the http URL handler
          try setURLHandler(appName: appName, scheme: "http")
          print("Set .\(ext) → \(appName) (routed to http)")
        default:
          do {
            try await setDefaultApplication(
              appName: appName,
              ext: ext,
            )
          } catch InfatError.applicationNotFound(_) {
            print(
              "Application '\(appName)' not found but ignoring due to passed options".bold().red())
            // Just eat that thing up
          } catch {
            // propogate the rest
            throw error
          }

          print("Set .\(ext) → \(appName)")
        }
      }
    } else {
      logger.debug("No [extensions] table found in \(configPath)")
    }

    // MARK: – Process [schemes]
    if let schemeTable = tomlConfig.table(schemeTableName) {
      logger.info("Processing [schemes] associations...")
      print("\(schemeTableName.uppercased().bold().underline())")
      for key in schemeTable.keyNames {
        guard let appName = schemeTable.string(key.components) else {
          throw InfatError.tomlValueNotString(
            path: configPath,
            key: key.components.joined()
          )
        }
        let scheme = key.components.joined()
        switch scheme.lowercased() {
        case "https":
          // Route https to the http URL handler
          try setURLHandler(appName: appName, scheme: "http")
          print("Set https → \(appName) (routed to http)")
        default:
          try setURLHandler(appName: appName, scheme: scheme)
          print("Set \(scheme) → \(appName)")
        }
      }
    } else {
      logger.debug("No [schemes] table found in \(configPath)")
    }
  }
}

```

## /Sources/ConformanceTypes.swift

```swift path="/Sources/ConformanceTypes.swift" 
import ArgumentParser
import UniformTypeIdentifiers

// Define a selection of the UTI supertypes Apple defines (https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct)
// Convert raw values to kebab-case for friendleness
//
// General philosphy for types here, besides the requirement that they be conformance types,
// is that they are not made redudant by extensions. They need to be an umbrella type, or stand
// in for situations where you couldn't derive the type from the extension alone (makefile for example)
enum Supertypes: String, CaseIterable, ExpressibleByArgument {
  // Text & Documents
  case plainText = "plain-text"
  case text = "text"
  case csv = "csv"
  case image = "image"
  case rawImage = "raw-image"

  // Audio & Video
  case audio = "audio"  // Base audio
  case video = "video"  // Base video (no audio)
  case movie = "movie"  // Base audiovisual
  case mpeg4Audio = "mp4-audio"
  case quicktime = "quicktime"
  case mpeg4Movie = "mp4-movie"

  // Archives
  case archive = "archive"

  // Source Code
  case source = "sourcecode"
  case cSource = "c-source"
  case cppSource = "cpp-source"
  case objcSource = "objc-source"
  case shell = "shell"
  case makefile = "makefile"

  // Filesystem & System
  case data = "data"
  case directory = "directory"
  case folder = "folder"
  case symbolicLink = "symlink"
  case executable = "executable"
  case unixExecutable = "unix-executable"
  case applicationBundle = "app-bundle"

  // Computed property to get the actual UTType
  var utType: UTType? {
    switch self {
    // Text & Documents
    case .plainText: return .plainText
    case .text: return .text
    case .csv: return .commaSeparatedText  // Map to most common CSV UTI

    // Images
    case .image: return .image
    case .rawImage: return .rawImage

    // Audio & Video
    case .audio: return .audio
    case .video: return .video
    case .movie: return .movie
    case .mpeg4Audio: return .mpeg4Audio
    case .quicktime: return .quickTimeMovie
    case .mpeg4Movie: return .mpeg4Movie

    // Archives
    case .archive: return .archive

    // Source Code
    case .source: return .sourceCode
    case .cSource: return .cSource
    case .cppSource: return .cPlusPlusSource
    case .objcSource: return .objectiveCSource
    case .shell: return .shellScript
    case .makefile: return .makefile

    // Filesystem & System
    case .data: return .data
    case .directory: return .directory
    case .folder: return .folder
    case .symbolicLink: return .symbolicLink
    case .executable: return .executable
    case .unixExecutable: return .unixExecutable
    case .applicationBundle: return .applicationBundle

    // default: return nil
    }
  }
}

```

## /Sources/Error.swift

```swift path="/Sources/Error.swift" 
import Foundation

enum InfatError: Error, LocalizedError {
  case couldNotDeriveUTI(msg: String)
  case missingOption
  case noConfigTables(path: String)
  case infoPlistNotFound(appPath: String)
  case unsupportedOrInvalidSupertype(name: String)
  case cannotSetURL(appName: String)
  case cannotRegisterURL(error: Int32)
  case unsupportedOSVersion
  case conflictingOptions(error: String)
  case directoryReadError(path: String, underlyingError: Error)
  case pathExpansionError(path: String)
  case applicationNotFound(name: String)
  case plistReadError(path: String, underlyingError: Error)
  case defaultAppSettingError(underlyingError: Error)
  case noActiveApplication
  case configurationLoadError(path: String, underlyingError: Error)
  case operationTimeout
  case tomlLoadError(path: String, underlyingError: Error)
  case tomlTableNotFoundError(path: String, table: String)
  case tomlValueNotString(path: String, key: String)

  var errorDescription: String? {
    switch self {
    case .couldNotDeriveUTI(let ext):
      return "Cannot determine UTI for extension '.\(ext)'"
    case .missingOption:
      return "No option provided"
    case .noConfigTables(let path):
      return "No valid tables found in provided configuration at \(path)"
    case .infoPlistNotFound(let appPath):
      return "Info.plist not found within application bundle: \(appPath)"
    case .conflictingOptions(let str):
      return str
    case .cannotRegisterURL(let error):
      return "Cannot register provided URL, got error \(error)"
    case .cannotSetURL(let app):
      return "Cannot set scheme for app \(app)"
    case .unsupportedOSVersion:
      return "This functionality requires a later version of macOS"
    case .directoryReadError(let path, let error):
      return "Error reading directory at \(path): \(error.localizedDescription)"
    case .pathExpansionError(let path):
      return "Could not expand path: \(path)"
    case .applicationNotFound(let name):
      return "Application not found: \(name)"
    case .plistReadError(let path, let error):
      return "Error reading or parsing Info.plist at \(path): \(error.localizedDescription)"
    case .defaultAppSettingError(let error):
      return "Failed to set default application: \(error.localizedDescription)"
    case .noActiveApplication:
      return "No active application found"
    case .configurationLoadError(let path, let error):
      return "Failed to load configuration from \(path): \(error.localizedDescription)"
    case .operationTimeout:
      return "Operation timed out"
    case .tomlLoadError(let path, let error):
      return "Failed to load TOML from \(path): \(error.localizedDescription)"
    case .tomlTableNotFoundError(let path, let table):
      return "Table '\(table)' not found in TOML file \(path)"
    case .tomlValueNotString(let path, let key):
      return "Value for key '\(key)' in TOML file \(path) is not a string."
    case .unsupportedOrInvalidSupertype(let supertype):
      return "Supertype \(supertype) is invalid or unsupported"
    }
  }
}

```

## /Sources/FileSystemUtilities.swift

```swift path="/Sources/FileSystemUtilities.swift" 
import Foundation
import Logging
import UniformTypeIdentifiers

struct FileSystemUtilities {
  static func findApplications() throws -> [URL] {
    let fileManager = FileManager.default
    let home = fileManager.homeDirectoryForCurrentUser.path
    var allAppURLs: [URL] = []
    let paths = [
      "/Applications/",
      "/System/Library/CoreServices/Applications/",
      "/System/Applications/",
      "\(home)/Applications/",
    ]
    for path in paths {
      do {
        let urls = try fileManager.contentsOfDirectory(
          at: URL(fileURLWithPath: path),
          includingPropertiesForKeys: nil,
          options: []
        )
        allAppURLs.append(contentsOf: urls)
        logger.debug("Found \(urls.count) items in \(path)")
      } catch {
        logger.debug("Could not read \(path): \(error.localizedDescription)")
      }
    }
    guard !allAppURLs.isEmpty else {
      throw InfatError.directoryReadError(
        path: "All application directories",
        underlyingError: NSError(
          domain: "com.philocalyst.infat", code: 1,
          userInfo: [NSLocalizedDescriptionKey: "No applications found"]
        )
      )
    }
    return allAppURLs
  }

  static func deriveUTIFromExtension(ext: String) throws -> FileUTIInfo {
    guard #available(macOS 11.0, *) else {
      throw InfatError.unsupportedOSVersion
    }
    let commonUTTypes: [UTType] = [
      .content, .text, .plainText, .pdf,
      .image, .png, .jpeg, .gif,
      .audio, .mp3, .movie, .mpeg4Movie,
      .zip, .gzip, .archive,
    ]
    guard let utType = UTType(filenameExtension: ext) else {
      throw InfatError.couldNotDeriveUTI(msg: ext)
    }
    let conforms =
      commonUTTypes
      .filter { utType.conforms(to: $0) }
      .map { $0.identifier }
    logger.debug("Determined UTI \(utType.identifier) for .\(ext)")
    return FileUTIInfo(
      typeIdentifier: utType,
      preferredMIMEType: utType.preferredMIMEType,
      localizedDescription: utType.localizedDescription,
      isDynamic: utType.isDynamic,
      conformsTo: conforms
    )
  }
}

```

## /Sources/FileUTIInfo.swift

```swift path="/Sources/FileUTIInfo.swift" 
import UniformTypeIdentifiers

struct FileUTIInfo {
  let typeIdentifier: UTType
  let preferredMIMEType: String?
  let localizedDescription: String?
  let isDynamic: Bool
  let conformsTo: [String]

  var description: String {
    var output = "UTI: \(typeIdentifier)\n"
    if let mimeType = preferredMIMEType {
      output += "MIME Type: \(mimeType)\n"
    }
    if let description = localizedDescription {
      output += "Description: \(description)\n"
    }
    output += "Is Dynamic: \(isDynamic ? "Yes" : "No")\n"
    if !conformsTo.isEmpty {
      output += "Conforms To: \(conformsTo.joined(separator: ", "))\n"
    }
    return output
  }
}

```

## /Sources/infat.swift

```swift path="/Sources/infat.swift" 
import ArgumentParser
import Foundation
import Logging

import class Foundation.ProcessInfo

var logger = Logger(label: "com.philocalyst.infat")

@main
struct Infat: AsyncParsableCommand {
  static let configuration = CommandConfiguration(
    abstract: "Declaritively set assocations for URLs and files",
    version: "2.4.0",
    subcommands: [Info.self, Set.self]
  )

  @Option(
    name: [.short, .long],
    help: "Path to the configuration file.")
  var config: String?

  @Flag(
    name: [.short, .long],
    help: "Enable verbose logging.")
  var verbose = false

  @Flag(
    name: [.short, .long],
    help: "Quiet output.")
  var quiet = false

  func validate() throws {
    let level: Logger.Level = verbose ? .trace : (quiet ? .critical : .warning)
    LoggingSystem.bootstrap { label in
      var h = StreamLogHandler.standardOutput(label: label)
      h.logLevel = level
      return h
    }
    logger = Logger(label: "com.philocalyst.infat")
  }

  mutating func run() async throws {
    // First check if a config was passed through the CLI. Then check if one was found at the XDG config home. If neither, error.
    if let cfg = config {
      try await ConfigManager.loadConfig(from: cfg)
    } else {
      // No config path passed, try XDG‐compliant locations:
      let env = ProcessInfo.processInfo.environment

      // |1| Determine XDG_CONFIG_HOME (or default to ~/.config)
      let homeDir = env["HOME"] ?? NSHomeDirectory()
      let xdgConfigHomePath = env["XDG_CONFIG_HOME"] ?? "\(homeDir)/.config"
      let xdgConfigHome = URL(fileURLWithPath: xdgConfigHomePath, isDirectory: true)

      // |2| Set up the per‐app relative path
      let appConfigSubpath = "infat/config.toml"

      // |3| Build the search list: user then system
      var searchPaths: [URL] = [
        xdgConfigHome.appendingPathComponent(appConfigSubpath)
      ]

      // If user has more than one config directory
      let systemConfigDirs =
        env["XDG_CONFIG_DIRS"]?
        .split(separator: ":")
        .map(String.init)
        ?? ["/etc/xdg"]

      for dir in systemConfigDirs {
        let url = URL(fileURLWithPath: dir, isDirectory: true)
          .appendingPathComponent(appConfigSubpath)
        searchPaths.append(url)
      }

      // |4| Try each path in order
      for url in searchPaths {
        if FileManager.default.fileExists(atPath: url.path) {
          try await ConfigManager.loadConfig(from: url.path)
          return
        }
      }

      // |5| Nothing found → error
      print(
        "Did you mean to pass in a config? Use -c or put one at " + "\(searchPaths[0].path)"
      )
      throw InfatError.missingOption
    }
  }
}

```

## /dist/infat-arm64-apple-macos

Binary file available at https://raw.githubusercontent.com/philocalyst/infat/refs/heads/main/dist/infat-arm64-apple-macos

## /justfile

``` path="/justfile" 
#!/usr/bin/env just

set shell := ["bash", "-euo", "pipefail", "-c"]
set dotenv-load := true
set allow-duplicate-recipes := true

# ▰▰▰ Variables ▰▰▰ #
project_root     := justfile_directory()
output_directory := project_root / "dist"
current_platform := `uname -m` + "-apple-" + os()
default_bin      := "infat"
build_dir        := project_root / ".build"
debug_bin        := build_dir / "debug" / default_bin
release_bin      := build_dir / "release" / default_bin

# ▰▰▰ Default ▰▰▰ #
default: build

[confirm("You've updated the versionings?")]
check:
	@echo "At the README?"
	@echo "At the swift bundle?"
	@echo "At the CHANGELOG?"
	grep -R \
	--exclude='CHANGELOG*' \
	--exclude='README*' \
	--exclude='Package*' \
	--exclude-dir='.build' \
	-nE '\b([0-9]+\.){2}[0-9]+\b' \
	.

# ▰▰▰ Build & Check ▰▰▰ #
build target=(current_platform):
	@echo "🔨 Building Swift package (debug)…"
	swift build --triple {{target}}

build-release target=(current_platform):
	@echo "🚀 Building Swift package (release)…"
	swift build -c release -Xswiftc "-whole-module-optimization" --triple {{target}} -Xlinker "-dead_strip"

# ▰▰▰ Packaging ▰▰▰ #
package target=(current_platform) result_directory=(output_directory): 
	just build-release {{target}}
	@echo "📦 Packaging release binary…"
	@mkdir -p {{output_directory}}
	@cp {{release_bin}} "{{result_directory}}/{{default_bin}}-{{target}}"
	@echo "✅ Packaged → {{result_directory}}/{{default_bin}}-{{target}}"

compress-binaries target_directory=("."):
    #!/usr/bin/env bash
    
    find "{{target_directory}}" -maxdepth 1 -type f -print0 | while IFS= read -r -d {{contextString}}#39;\0' file; do
    # Check if the file command output indicates a binary/executable type
    if file "$file" | grep -q -E 'executable|ELF|Mach-O|shared object'; then
        # Get the base filename without the path
        filename=$(basename "$file")
        
        # Get the base name without version number
        basename="${filename%%-*}"
        
        echo "Archiving binary file: $filename"
        
        # Create archive with just the basename, no directory structure
        tar -czf "${file}.tar.gz" \
            -C "$(dirname "$file")" \
            -s "|^${filename}$|${basename}|" \
            "$(basename "$file")"
    fi
    done


format:
	find . -name "*.swift" -type f -exec swift-format format -i {} +

checksum directory=(output_directory):
	@echo "🔒 Creating checksums in {{directory}}…"
	@find "{{directory}}" -type f \
	    ! -name "checksums.sha256" \
	    ! -name "*.sha256" \
	    -exec sh -c 'sha256sum "$1" > "$1.sha256"' _ {} \;
	@echo "✅ Checksums created!"

create-notes raw_tag outfile changelog:
    #!/usr/bin/env bash
    
    tag_v="{{raw_tag}}"
    tag="${tag_v#v}" # Remove prefix v

    # Changes header for release notes
    printf "# What's new\n" > "{{outfile}}"

    if [[ ! -f "{{changelog}}" ]]; then
      echo "Error: {{changelog}} not found." >&2
      exit 1
    fi

    echo "Extracting notes for tag: {{raw_tag}} (searching for section [$tag])"
    # Use awk to extract the relevant section from the changelog
    awk -v tag="$tag" '
      # start printing when we see "## [<tag>]" (escape brackets for regex)
      $0 ~ ("^## \\[" tag "\\]") { printing = 1; next }
      # stop as soon as we hit the next "## [" section header
      printing && /^## \[/       { exit }
      # otherwise, if printing is enabled, print the current line
      printing                    { print }

      # Error handling
      END {
        if (found_section != 0) {
          # Print error to stderr
          print "Error: awk could not find section header ## [" tag "] in " changelog_file > "/dev/stderr"
          exit 1
        }
      }
    ' "{{changelog}}" >> "{{outfile}}"

    # Check if the output file has content
    if [[ -s {{outfile}} ]]; then
      echo "Successfully extracted release notes to '{{outfile}}'."
    else
      # Output a warning if no notes were found for the tag
      echo "Warning: '{{outfile}}' is empty. Is '## [$tag]' present in '{{changelog}}'?" >&2
    fi


# ▰▰▰ Run ▰▰▰ #
run +args="":
	@echo "▶️ Running (debug)…"
	swift run {{default_bin}} {{args}}

run-release +args="":
	@echo "▶️ Running (release)…"
	swift run -c release -Xswiftc "-whole-module-optimization" {{release_bin}} {{args}}

# ▰▰▰ Cleaning ▰▰▰ #
clean:
	@echo "🧹 Cleaning build artifacts…"
	swift package clean
	swift package resolve

# ▰▰▰ Installation & Update ▰▰▰ #
install: build-release
	@echo "💾 Installing {{default_bin}} → /usr/local/bin…"
	@cp {{release_bin}} /usr/local/bin/{{default_bin}}

install-force: build-release
	@echo "💾 Force installing {{default_bin}} → /usr/local/bin…"
	@cp {{release_bin}} /usr/local/bin/{{default_bin}} --force

update:
	@echo "🔄 Updating Swift package dependencies…"
	swift package update

# ▰▰▰ Aliases ▰▰▰ #
alias b   := build
alias br  := build-release
alias p   := package
alias cb  := compress-binaries
alias ch  := checksum
alias r   := run
alias rr  := run-release
alias cl  := clean
alias i   := install
alias ifo := install-force
alias up  := update

```


The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!