{"id":49524366,"url":"https://github.com/alibosworth/inlinetokenfield","last_synced_at":"2026-05-02T02:01:06.909Z","repository":{"id":354671820,"uuid":"1218207649","full_name":"alibosworth/InlineTokenField","owner":"alibosworth","description":"A macOS SwiftUI text field for mixing free text with inline token pills.","archived":false,"fork":false,"pushed_at":"2026-04-22T16:48:04.000Z","size":20,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-29T16:07:00.720Z","etag":null,"topics":["appkit","macos","spm","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alibosworth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-22T16:34:44.000Z","updated_at":"2026-04-22T17:09:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/alibosworth/InlineTokenField","commit_stats":null,"previous_names":["alibosworth/inlinetokenfield"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/alibosworth/InlineTokenField","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alibosworth%2FInlineTokenField","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alibosworth%2FInlineTokenField/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alibosworth%2FInlineTokenField/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alibosworth%2FInlineTokenField/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alibosworth","download_url":"https://codeload.github.com/alibosworth/InlineTokenField/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alibosworth%2FInlineTokenField/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32520156,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T01:12:54.858Z","status":"online","status_checked_at":"2026-05-02T02:00:05.923Z","response_time":132,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appkit","macos","spm","swift","swiftui"],"created_at":"2026-05-02T02:01:05.997Z","updated_at":"2026-05-02T02:01:06.864Z","avatar_url":"https://github.com/alibosworth.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# InlineTokenField\n\nA macOS SwiftUI text field that supports inline token pills mixed with free text.\n\nUnlike `NSTokenField`, which tokenizes *all* text, InlineTokenField lets users type free text and insert specific named tokens as styled pills at the cursor — useful for path templates, rename patterns, and similar structured strings.\n\n\u003cvideo src=\"https://github.com/user-attachments/assets/1afaa175-3a01-4f33-83be-a732bb8b8564\" autoplay loop muted playsinline\u003e\u003c/video\u003e\n\n## Requirements\n\n- macOS 14.0+\n- Swift 5.9+, Swift 6 compatible\n- AppKit-backed — macOS only, no iOS support\n\nEditor note: SourceKit may show `\"No such module 'PackageDescription'\"` and cross-file type errors. These are false positives — the package builds correctly with `swift build`.\n\n## Installation\n\n```swift\n// Package.swift\n.package(url: \"https://github.com/alibosworth/InlineTokenField\", from: \"0.1.0\")\n```\n\n## Usage\n\n### Core API\n\n```swift\nimport SwiftUI\nimport InlineTokenField\n\nstruct MyView: View {\n    @State private var segments: [TokenSegment] = [\n        .text(\"../\"),\n        .token(\"INPUT_DIR\"),\n        .text(\"_processed\")\n    ]\n    @StateObject private var controller = InlineTokenFieldController()\n\n    var body: some View {\n        VStack {\n            InlineTokenField(\n                value: $segments,\n                tokens: [\"INPUT_DIR\", \"OUTPUT_DIR\", \"FILENAME\"],\n                controller: controller\n            )\n\n            // Trigger insertion from a SwiftUI button while the field is on screen\n            Button(\"Insert INPUT_DIR\") { controller.insertToken(\"INPUT_DIR\") }\n        }\n    }\n}\n```\n\n`InlineTokenFieldController` is a view helper — it holds a weak reference to the mounted field and only works while the field is in the view hierarchy.\n\n`tokens:` controls what the controller can insert. It is not a validator on the bound value: any `.token(\"X\")` already present in `[TokenSegment]` still renders as a pill even if `\"X\"` is not in the current `tokens` array.\n\n### Convenience row\n\n`InlineTokenFieldRow` bundles the field with an insert button per token:\n\n```swift\nInlineTokenFieldRow(\n    value: $segments,\n    tokens: [\"INPUT_DIR\", \"OUTPUT_DIR\", \"FILENAME\"],\n    tokenLabels: [\"INPUT_DIR\": \"Input\", \"OUTPUT_DIR\": \"Output\", \"FILENAME\": \"Filename\"],\n    fieldHeight: 24  // optional, defaults to 24\n)\n```\n\n### Template string serialization\n\nIf you need to store or exchange the value as a plain string, `TokenTemplate` provides an opt-in `[TOKEN]` serialization format:\n\n```swift\n// [TokenSegment] → String\nlet str = TokenTemplate.string(from: segments)\n// e.g. \"../[INPUT_DIR]_processed\"\n\n// String → [TokenSegment]\nlet segments = TokenTemplate.parse(\"../[INPUT_DIR]_processed\", tokens: [\"INPUT_DIR\", \"OUTPUT_DIR\"])\n```\n\n`[X]` is parsed as a token segment only when `X` exactly matches a string in the `tokens` array. Unrecognized brackets (e.g. `[unknown]`, unclosed `[`) are passed through as plain text with brackets intact.\n\nThe segment round-trip is stable: a `[TokenSegment]` value serialized to a string and parsed back with the same token set produces identical segments. The inverse is not guaranteed — an arbitrary string parsed and re-serialized may differ if it contains unrecognized brackets.\n\n**Limitation:** `TokenTemplate` does not support escape syntax in 0.1.0. If your plain text can legitimately contain `[knownTokenName]`, work with `[TokenSegment]` directly rather than using the template string format.\n\n### Custom style\n\n```swift\nimport AppKit\n\nlet style = TokenStyle(\n    font: .systemFont(ofSize: 13, weight: .medium),\n    horizontalPadding: 7,\n    verticalPadding: 1,\n    fillColor: .systemBlue.withAlphaComponent(0.15),\n    strokeColor: .systemBlue.withAlphaComponent(0.4),\n    textColor: .systemBlue\n)\nInlineTokenField(value: $segments, tokens: tokens, controller: controller, style: style)\n```\n\nSet `showsCloseButton: false` to hide the inline × button and rely on selection + backspace for deletion:\n\n```swift\nlet style = TokenStyle(\n    font: .systemFont(ofSize: 13, weight: .medium),\n    horizontalPadding: 7,\n    verticalPadding: 1,\n    fillColor: .systemBlue.withAlphaComponent(0.15),\n    strokeColor: .systemBlue.withAlphaComponent(0.4),\n    textColor: .systemBlue,\n    showsCloseButton: false\n)\n```\n\n## Behavior Guarantees\n\n- **Token copy/paste is preserved within the field** — cut, copy, and paste of a selection containing token pills restores the pills intact. Token values are preserved; pasted pills are rendered using the field's current style at paste time. Tokens are written to the pasteboard using a structured payload alongside the system RTF representation.\n- **Token body and close affordance are independent interaction regions** — clicking a token body selects it as an inline object; clicking the × deletes it. Hover, cursor, and hit-testing for the × share one geometry; they do not drift independently.\n- **Plain text cursor semantics apply only to plain text** — the I-beam cursor and normal text-editing behavior are not active over token bodies or the close affordance.\n- **Undefined: pasting token content from outside the field** — the custom pasteboard type is only written by this field. Pasting token-shaped RTF from an external source may or may not reconstruct pills depending on whether the attachment data survives the roundtrip.\n\n## Constraints\n\n- **macOS only** — AppKit-backed, no UIKit port\n- **Single-line** — horizontally scrolling, vertical wrapping disabled\n- **`tokens:` is the insertable set, not a validator** — the field renders any `.token(string)` as a pill regardless of whether that string is currently in `tokens`. A saved document can contain tokens that are no longer in the active set.\n- **`TokenTemplate` bracket semantics are contract surface** — the `[TOKEN]` format is stable as of 0.1.0. Escape syntax is not supported; see limitation above.\n- **Token interactions are custom AppKit behavior** — token bodies behave as selectable inline objects, the × close affordance has its own hover/click target, and only plain text uses normal text-editing cursor semantics.\n\n## Running the demo\n\n```bash\nswift run InlineTokenFieldDemo\n```\n\nThe demo shows two fields — one with the × close button, one without — along with token insert buttons and a live segment/template debug display.\n\n## Why not NSTokenField?\n\n`NSTokenField` tokenizes every word the user types — it's designed for tag/recipient inputs where all content is tokens. InlineTokenField is for content where most text is free and only specific named values need to be visually distinguished.\n\n[`fcanas/TokenField`](https://github.com/fcanas/TokenField) (SPM) wraps `NSTokenField` and has the same limitation. [FriedText](https://github.com/BenedictSt/FriedText) took a similar `NSTextView`+`NSTextAttachment` approach and was consulted as a reference, but is archived.\n\n## How it works\n\nTokens are stored as `NSTextAttachment` with a `NSTextAttachmentCell` subclass that draws the pill. The token value lives in the attachment's `FileWrapper` data. The binding is `[TokenSegment]` — a structured value with no in-band sentinel logic.\n\nWithin the field, cut/copy/paste of token content is preserved by writing a custom segment payload to the pasteboard and reconstructing token attachments on paste. Hover, cursor, and click behavior for the × close affordance are driven by shared custom hit geometry rather than `NSTextView` defaults.\n\n**Swift 6 concurrency note:** `NSTextAttachmentCell` inherits `@MainActor` from `NSCell`, but AppKit calls drawing overrides from a nonisolated context. All state accessed from drawing overrides lives at file scope with `nonisolated(unsafe)` — never as stored properties on the cell.\n\n## Implementation Notes\n\n- `NSTextView`'s default cursor handling conflicts with token-specific hover regions, so cursor choice is centralized in `TokenNSTextView`.\n- Token bodies behave as selectable inline objects; the × close affordance is a separate interactive region; only plain text should use normal text-editing cursor semantics.\n- Hover color, cursor choice, and click hit-testing for the × close affordance must use the same hit geometry. If these drift apart, cursor flicker and inconsistent hover behavior return quickly.\n\n## License\n\nMIT © 2026\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falibosworth%2Finlinetokenfield","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falibosworth%2Finlinetokenfield","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falibosworth%2Finlinetokenfield/lists"}