mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 18:34:21 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
400623f117 | ||
|
|
4b047d3dab | ||
|
|
550d2d3f99 | ||
|
|
149f133747 |
59
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
59
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Bug report
|
||||
description: Report an issue that should be fixed
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Describe the bug you encountered
|
||||
placeholder: What happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: opencode-version
|
||||
attributes:
|
||||
label: OpenCode version
|
||||
description: What version of OpenCode are you using?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How can we reproduce this issue?
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: screenshot-or-link
|
||||
attributes:
|
||||
label: Screenshot and/or share link
|
||||
description: Run `/share` to get a share link, or attach a screenshot
|
||||
placeholder: Paste link or drag and drop screenshot here
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: what OS are you using?
|
||||
placeholder: e.g., macOS 26.0.1, Ubuntu 22.04, Windows 11
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: terminal
|
||||
attributes:
|
||||
label: Terminal
|
||||
description: what terminal are you using?
|
||||
placeholder: e.g., iTerm2, Ghostty, Alacritty, Windows Terminal
|
||||
validations:
|
||||
required: false
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💬 Discord Community
|
||||
url: https://discord.gg/opencode
|
||||
about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question.
|
||||
20
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
20
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 🚀 Feature Request
|
||||
description: Suggest an idea, feature, or enhancement
|
||||
labels: [discussion]
|
||||
title: "[FEATURE]:"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: verified
|
||||
attributes:
|
||||
label: Feature hasn't been suggested before.
|
||||
options:
|
||||
- label: I have verified this feature I'm about to request hasn't been suggested before.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the enhancement you want to request
|
||||
description: What do you want to change or add? What are the benefits of implementing this? Try to be detailed so we can understand your request better :)
|
||||
validations:
|
||||
required: true
|
||||
11
.github/ISSUE_TEMPLATE/question.yml
vendored
11
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Question
|
||||
description: Ask a question
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: What's your question?
|
||||
validations:
|
||||
required: true
|
||||
22
.github/actions/setup-bun/action.yml
vendored
22
.github/actions/setup-bun/action.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: "Setup Bun"
|
||||
description: "Setup Bun with caching and install dependencies"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-${{ hashFiles('package.json') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
shell: bash
|
||||
71
.github/publish-python-sdk.yml
vendored
71
.github/publish-python-sdk.yml
vendored
@@ -1,71 +0,0 @@
|
||||
#
|
||||
# This file is intentionally in the wrong dir, will move and add later....
|
||||
#
|
||||
|
||||
# name: publish-python-sdk
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types: [published]
|
||||
# workflow_dispatch:
|
||||
|
||||
# jobs:
|
||||
# publish:
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Setup Bun
|
||||
# uses: oven-sh/setup-bun@v1
|
||||
# with:
|
||||
# bun-version: 1.2.21
|
||||
|
||||
# - name: Install dependencies (JS/Bun)
|
||||
# run: bun install
|
||||
|
||||
# - name: Install uv
|
||||
# shell: bash
|
||||
# run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# - name: Generate Python SDK from OpenAPI (CLI)
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/generate.py --source cli
|
||||
|
||||
# - name: Sync Python dependencies
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ~/.local/bin/uv sync --dev --project packages/sdk/python
|
||||
|
||||
# - name: Set version from release tag
|
||||
# shell: bash
|
||||
# run: |
|
||||
# TAG="${GITHUB_REF_NAME:-}"
|
||||
# if [ -z "$TAG" ]; then
|
||||
# TAG="$(git describe --tags --abbrev=0 || echo 0.0.0)"
|
||||
# fi
|
||||
# echo "Using version: $TAG"
|
||||
# VERSION="$TAG" ~/.local/bin/uv run --project packages/sdk/python python - <<'PY'
|
||||
# import os, re, pathlib
|
||||
# root = pathlib.Path('packages/sdk/python')
|
||||
# pt = (root / 'pyproject.toml').read_text()
|
||||
# version = os.environ.get('VERSION','0.0.0').lstrip('v')
|
||||
# pt = re.sub(r'(?m)^(version\s*=\s*")[^"]+("\s*)$', f"\\1{version}\\2", pt)
|
||||
# (root / 'pyproject.toml').write_text(pt)
|
||||
# # Also update generator config override for consistency
|
||||
# cfgp = root / 'openapi-python-client.yaml'
|
||||
# if cfgp.exists():
|
||||
# cfg = cfgp.read_text()
|
||||
# cfg = re.sub(r'(?m)^(package_version_override:\s*)\S+$', f"\\1{version}", cfg)
|
||||
# cfgp.write_text(cfg)
|
||||
# PY
|
||||
|
||||
# - name: Build and publish to PyPI
|
||||
# env:
|
||||
# PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/publish.py
|
||||
63
.github/workflows/auto-label-tui.yml
vendored
63
.github/workflows/auto-label-tui.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Auto-label TUI Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label and assign issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title;
|
||||
const description = issue.body || '';
|
||||
|
||||
// Check for "opencode web" keyword
|
||||
const webPattern = /(opencode web)/i;
|
||||
const isWebRelated = webPattern.test(title) || webPattern.test(description);
|
||||
|
||||
// Check for version patterns like v1.0.x or 1.0.x
|
||||
const versionPattern = /[v]?1\.0\./i;
|
||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||
|
||||
// Check for "nix" keyword
|
||||
const nixPattern = /\bnix\b/i;
|
||||
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||
|
||||
const labels = [];
|
||||
|
||||
if (isWebRelated) {
|
||||
labels.push('web');
|
||||
|
||||
// Assign to adamdotdevin
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: ['adamdotdevin']
|
||||
});
|
||||
} else if (isVersionRelated) {
|
||||
// Only add opentui if NOT web-related
|
||||
labels.push('opentui');
|
||||
}
|
||||
|
||||
if (isNixRelated) {
|
||||
labels.push('nix');
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
13
.github/workflows/deploy.yml
vendored
13
.github/workflows/deploy.yml
vendored
@@ -11,19 +11,16 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
node-version: "24"
|
||||
bun-version: 1.2.17
|
||||
|
||||
- run: bun install
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||
|
||||
61
.github/workflows/duplicate-issues.yml
vendored
61
.github/workflows/duplicate-issues.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Duplicate Issue Detection
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check for duplicate issues
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
"bash": {
|
||||
"gh issue*": "allow",
|
||||
"*": "deny"
|
||||
},
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
|
||||
|
||||
Issue number:
|
||||
${{ github.event.issue.number }}
|
||||
|
||||
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
||||
Consider:
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
|
||||
If you find any potential duplicates, please comment on the new issue with:
|
||||
- A brief explanation of why it might be a duplicate
|
||||
- Links to the potentially duplicate issues
|
||||
- A suggestion to check those issues first
|
||||
|
||||
Use this format for the comment:
|
||||
'This issue might be a duplicate of existing issues. Please check:
|
||||
- #[issue_number]: [brief description of similarity]
|
||||
|
||||
Feel free to ignore if none of these address your specific case.'
|
||||
|
||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
|
||||
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
|
||||
|
||||
If no clear duplicates are found, do not comment."
|
||||
32
.github/workflows/format.yml
vendored
32
.github/workflows/format.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
format:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
./script/format.ts
|
||||
env:
|
||||
CI: true
|
||||
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
4
.github/workflows/notify-discord.yml
vendored
4
.github/workflows/notify-discord.yml
vendored
@@ -2,11 +2,11 @@ name: discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published] # fires only when a release is published
|
||||
types: [published] # fires only when a release is published
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send nicely-formatted embed to Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1
|
||||
|
||||
23
.github/workflows/opencode.yml
vendored
23
.github/workflows/opencode.yml
vendored
@@ -3,31 +3,22 @@ name: opencode
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: startsWith(github.event.comment.body, 'hey opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
uses: sst/opencode/sdks/github@github-v1
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: opencode/claude-haiku-4-5
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
|
||||
4
.github/workflows/publish-github-action.yml
vendored
4
.github/workflows/publish-github-action.yml
vendored
@@ -14,7 +14,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -27,4 +27,4 @@ jobs:
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./script/publish
|
||||
working-directory: ./github
|
||||
working-directory: ./sdks/github
|
||||
|
||||
11
.github/workflows/publish-vscode.yml
vendored
11
.github/workflows/publish-vscode.yml
vendored
@@ -13,23 +13,22 @@ permissions:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
|
||||
- run: git fetch --force --tags
|
||||
- run: bun install -g @vscode/vsce
|
||||
|
||||
- name: Install extension dependencies
|
||||
run: bun install
|
||||
working-directory: ./sdks/vscode
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
bun install
|
||||
./script/publish
|
||||
working-directory: ./sdks/vscode
|
||||
env:
|
||||
|
||||
157
.github/workflows/publish.yml
vendored
157
.github/workflows/publish.yml
vendored
@@ -1,21 +1,14 @@
|
||||
name: publish
|
||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: "Bump major, minor, or patch"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
version:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- "*"
|
||||
- "!vscode-v*"
|
||||
- "!github-v*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -25,8 +18,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -40,143 +32,34 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pacman-package-manager
|
||||
|
||||
- name: Setup SSH for AUR
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Install OpenCode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Setup npm auth
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./script/publish.ts
|
||||
bun install
|
||||
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
|
||||
./script/publish.ts
|
||||
else
|
||||
./script/publish.ts --snapshot
|
||||
fi
|
||||
working-directory: ./packages/opencode
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
|
||||
publish-tauri:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- host: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- host: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- host: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
with:
|
||||
keychain: build
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: Verify Certificate
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
|
||||
- name: Setup Apple API Key
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: startsWith(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.settings.target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/tauri/src-tauri
|
||||
shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
cd packages/tauri
|
||||
bun ./scripts/prepare.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
|
||||
if: startsWith(matrix.settings.host, 'ubuntu')
|
||||
|
||||
- name: Build and upload artifacts
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
with:
|
||||
projectPath: packages/tauri
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }}
|
||||
updaterJsonPreferNsis: true
|
||||
# releaseId: TODO
|
||||
|
||||
79
.github/workflows/review.yml
vendored
79
.github/workflows/review.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/review') &&
|
||||
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Get PR details
|
||||
id: pr-details
|
||||
run: |
|
||||
gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
|
||||
echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
|
||||
echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
||||
|
||||
<pr-number>
|
||||
${{ steps.pr-number.outputs.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
$PR_BODY
|
||||
</pr-description>
|
||||
|
||||
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||
|
||||
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
|
||||
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
||||
|
||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||
|
||||
Command MUST be like this.
|
||||
\`\`\`
|
||||
gh api \
|
||||
--method POST \
|
||||
-H \"Accept: application/vnd.github+json\" \
|
||||
-H \"X-GitHub-Api-Version: 2022-11-28\" \
|
||||
/repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
|
||||
\`\`\`
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!."
|
||||
39
.github/workflows/sdk.yml
vendored
39
.github/workflows/sdk.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: sdk
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
format:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
bun ./packages/sdk/js/script/build.ts
|
||||
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add -A
|
||||
git commit -m "chore: regen sdk"
|
||||
git push --no-verify
|
||||
env:
|
||||
CI: true
|
||||
38
.github/workflows/snapshot.yml
vendored
38
.github/workflows/snapshot.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: snapshot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- test-bedrock
|
||||
- v0
|
||||
- otui-diffs
|
||||
- snapshot-*
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.24.0"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./script/publish.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
10
.github/workflows/stats.yml
vendored
10
.github/workflows/stats.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -16,10 +16,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Run stats script
|
||||
run: bun script/stats.ts
|
||||
run: bun scripts/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
@@ -28,5 +30,3 @@ jobs:
|
||||
git add STATS.md
|
||||
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
|
||||
git push
|
||||
env:
|
||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||
|
||||
34
.github/workflows/sync-zed-extension.yml
vendored
34
.github/workflows/sync-zed-extension.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: "sync-zed-extension"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
zed:
|
||||
name: Release Zed Extension
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Get version tag
|
||||
id: get_tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
else
|
||||
TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1)
|
||||
fi
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "Using tag: ${TAG}"
|
||||
|
||||
- name: Sync Zed extension
|
||||
run: |
|
||||
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
bun turbo typecheck
|
||||
bun turbo test
|
||||
env:
|
||||
CI: true
|
||||
19
.github/workflows/typecheck.yml
vendored
19
.github/workflows/typecheck.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: typecheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
102
.github/workflows/update-nix-hashes.yml
vendored
102
.github/workflows/update-nix-hashes.yml
vendored
@@ -1,102 +0,0 @@
|
||||
name: Update Nix Hashes
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
pull_request:
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
SYSTEM: x86_64-linux
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Update flake.lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "📦 Updating flake.lock..."
|
||||
nix flake update
|
||||
echo "✅ flake.lock updated successfully"
|
||||
|
||||
- name: Update node_modules hash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash updated successfully"
|
||||
|
||||
- name: Commit hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
|
||||
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hashes are already up to date."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and hashes"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,20 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.worktrees
|
||||
.opencode
|
||||
.sst
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
*~
|
||||
playground
|
||||
tmp
|
||||
dist
|
||||
.turbo
|
||||
**/.serena
|
||||
.serena/
|
||||
/result
|
||||
refs
|
||||
Session.vim
|
||||
opencode.json
|
||||
a.out
|
||||
target
|
||||
openapi.json
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Check if bun version matches package.json
|
||||
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
|
||||
CURRENT_VERSION=$(bun --version)
|
||||
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
|
||||
exit 1
|
||||
fi
|
||||
bun typecheck
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
You are not verbose
|
||||
|
||||
The title of the page should be a word or a 2-3 word phrase
|
||||
|
||||
The description should be one short line, should not start with "The", should
|
||||
avoid repeating the title of the page, should be 5-10 words long
|
||||
|
||||
Chunks of text should not be more than 2 sentences long
|
||||
|
||||
Each section is separated by a divider of 3 dashes
|
||||
|
||||
The section titles are short with only the first letter of the word capitalized
|
||||
|
||||
The section titles are in the imperative mood
|
||||
|
||||
The section titles should not repeat the term used in the page title, for
|
||||
example, if the page title is "Models", avoid using a section title like "Add
|
||||
new models". This might be unavoidable in some cases, but try to avoid it.
|
||||
|
||||
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
|
||||
|
||||
For JS or TS code snippets remove trailing semicolons and any trailing commas
|
||||
that might not be needed.
|
||||
|
||||
If you are making a commit prefix the commit message with `docs:`
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: Use this agent when you are asked to commit and push code changes to a git repository.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You commit and push to git
|
||||
|
||||
Commit messages should be brief since they are used to generate release notes.
|
||||
|
||||
Messages should say WHY the change was made and not WHAT was changed.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
description: git commit and push
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
make sure it includes a prefix like
|
||||
docs:
|
||||
tui:
|
||||
core:
|
||||
ci:
|
||||
ignore:
|
||||
wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
For anything in the packages/app use the ignore: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
do not do generic messages like "improved agent experience" be very specific
|
||||
about what user facing changes were made
|
||||
|
||||
if there are changes do a git pull --rebase
|
||||
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
Consider:
|
||||
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
|
||||
Please list any matching issues with:
|
||||
|
||||
- Issue number and title
|
||||
- Brief explanation of why it matches the query
|
||||
- Link to the issue
|
||||
|
||||
If no clear matches are found, say so.
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
description: Remove AI code slop
|
||||
---
|
||||
|
||||
Check the diff against dev, and remove all AI generated slop introduced in this branch.
|
||||
|
||||
This includes:
|
||||
|
||||
- Extra comments that a human wouldn't add or is inconsistent with the rest of the file
|
||||
- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)
|
||||
- Casts to any to get around type issues
|
||||
- Any other style that is inconsistent with the file
|
||||
- Unnecessary emoji usage
|
||||
|
||||
Report at the end with only a 1-3 sentence summary of what you changed
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description: spellcheck all markdown file changes
|
||||
---
|
||||
|
||||
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"instructions": ["STYLE_GUIDE.md"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {
|
||||
// "baseURL": "http://localhost:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"exa": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.exa.ai/mcp",
|
||||
},
|
||||
"morph": {
|
||||
"type": "local",
|
||||
"command": ["bunx", "@morphllm/morphmcp"],
|
||||
"environment": {
|
||||
"ENABLED_TOOLS": "warp_grep",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord2": "#434C5E",
|
||||
"nord3": "#4C566A",
|
||||
"nord4": "#D8DEE9",
|
||||
"nord5": "#E5E9F0",
|
||||
"nord6": "#ECEFF4",
|
||||
"nord7": "#8FBCBB",
|
||||
"nord8": "#88C0D0",
|
||||
"nord9": "#81A1C1",
|
||||
"nord10": "#5E81AC",
|
||||
"nord11": "#BF616A",
|
||||
"nord12": "#D08770",
|
||||
"nord13": "#EBCB8B",
|
||||
"nord14": "#A3BE8C",
|
||||
"nord15": "#B48EAD"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"error": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"success": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"info": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"text": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "nord3",
|
||||
"light": "nord1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "nord0",
|
||||
"light": "nord6"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "nord1",
|
||||
"light": "nord4"
|
||||
},
|
||||
"border": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "nord3",
|
||||
"light": "nord2"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "nord13",
|
||||
"light": "nord13"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "nord8",
|
||||
"light": "nord8"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "nord15",
|
||||
"light": "nord15"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
sst-env.d.ts
|
||||
11
.vscode/launch.example.json
vendored
11
.vscode/launch.example.json
vendored
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "opencode (attach)",
|
||||
"url": "ws://localhost:6499/"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
.vscode/settings.example.json
vendored
5
.vscode/settings.example.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"oven.bun-vscode"
|
||||
]
|
||||
}
|
||||
41
AGENTS.md
41
AGENTS.md
@@ -1,34 +1,13 @@
|
||||
## Debugging
|
||||
## Style
|
||||
|
||||
- To test opencode in the `packages/opencode` directory you can run `bun dev`
|
||||
- prefer single word variable/function names
|
||||
- avoid try catch where possible - prefer to let exceptions bubble up
|
||||
- avoid else statements where possible
|
||||
- do not make useless helper functions - inline functionality unless the
|
||||
function is reusable or composable
|
||||
- prefer Bun apis
|
||||
|
||||
## Tool Calling
|
||||
## Workflow
|
||||
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
|
||||
|
||||
json
|
||||
{
|
||||
"recipient_name": "multi_tool_use.parallel",
|
||||
"parameters": {
|
||||
"tool_uses": [
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.tsx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.md"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
- you can regenerate the golang sdk by calling ./scripts/stainless.ts
|
||||
- we use bun for everything
|
||||
|
||||
102
CONTRIBUTING.md
102
CONTRIBUTING.md
@@ -1,102 +0,0 @@
|
||||
# Contributing to OpenCode
|
||||
|
||||
We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged:
|
||||
|
||||
- Bug fixes
|
||||
- Additional LSPs / Formatters
|
||||
- Improvements to LLM performance
|
||||
- Support for new providers
|
||||
- Fixes for environment-specific quirks
|
||||
- Missing standard behavior
|
||||
- Documentation improvements
|
||||
|
||||
However, any UI or core product feature must go through a design review with the core team before implementation.
|
||||
|
||||
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
|
||||
|
||||
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
|
||||
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
|
||||
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
|
||||
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
|
||||
|
||||
> [!NOTE]
|
||||
> PRs that ignore these guardrails will likely be closed.
|
||||
|
||||
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
|
||||
|
||||
## Developing OpenCode
|
||||
|
||||
- Requirements: Bun 1.3+
|
||||
- Install dependencies and start the dev server from the repo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun dev
|
||||
```
|
||||
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
> [!NOTE]
|
||||
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||
|
||||
Please try to follow the [style guide](./STYLE_GUIDE.md)
|
||||
|
||||
### Setting up a Debugger
|
||||
|
||||
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
|
||||
|
||||
The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect=<url> dev ...` and attach
|
||||
your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV).
|
||||
|
||||
Caveats:
|
||||
|
||||
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
|
||||
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
|
||||
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
|
||||
is triggered.
|
||||
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
|
||||
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow
|
||||
- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead
|
||||
|
||||
#### VSCode Setup
|
||||
|
||||
If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json).
|
||||
|
||||
Some debug methods that can be problematic:
|
||||
|
||||
- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable
|
||||
- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal`
|
||||
|
||||
With that said, you may want to try these methods, as they might work for you.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
- Try to keep pull requests small and focused.
|
||||
- Link relevant issue(s) in the description
|
||||
- Explain the issue and why your change fixes it
|
||||
- Avoid having verbose LLM generated PR descriptions
|
||||
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
|
||||
|
||||
### Style Preferences
|
||||
|
||||
These are not strictly enforced, they are just general guidelines:
|
||||
|
||||
- **Functions:** Keep logic within a single function unless breaking it out adds clear reuse or composition benefits.
|
||||
- **Destructuring:** Do not do unnecessary destructuring of variables.
|
||||
- **Control flow:** Avoid `else` statements.
|
||||
- **Error handling:** Prefer `.catch(...)` instead of `try`/`catch` when possible.
|
||||
- **Types:** Reach for precise types and avoid `any`.
|
||||
- **Variables:** Stick to immutable patterns and avoid `let`.
|
||||
- **Naming:** Choose concise single-word identifiers when they remain descriptive.
|
||||
- **Runtime APIs:** Use Bun helpers such as `Bun.file()` when they fit the use case.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
||||
79
README.md
79
README.md
@@ -1,20 +1,20 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai">
|
||||
<picture>
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
||||
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/web/src/assets/logo-ornate-light.svg" alt="opencode logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">The AI coding agent built for the terminal.</p>
|
||||
<p align="center">AI coding agent, built for the terminal.</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,12 +26,8 @@ curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Package managers
|
||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS and Linux
|
||||
brew install sst/tap/opencode # macOS
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use --pin -g ubi:sst/opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -52,33 +48,47 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas
|
||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### Agents
|
||||
|
||||
OpenCode includes two built-in agents you can switch between,
|
||||
you can switch between these using the `Tab` key.
|
||||
|
||||
- **build** - Default, full access agent for development work
|
||||
- **plan** - Read-only agent for analysis and code exploration
|
||||
- Denies file edits by default
|
||||
- Asks permission before running bash commands
|
||||
- Ideal for exploring unfamiliar codebases or planning changes
|
||||
|
||||
Also, included is a **general** subagent for complex searches and multi-step tasks.
|
||||
This is used internally and can be invoked using `@general` in messages.
|
||||
|
||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||
|
||||
### Documentation
|
||||
|
||||
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
|
||||
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||
|
||||
### Contributing
|
||||
|
||||
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
|
||||
opencode is an opinionated tool so any fundamental feature needs to go through a
|
||||
design process with the core team.
|
||||
|
||||
### Building on OpenCode
|
||||
> [!IMPORTANT]
|
||||
> We do not accept PRs for core features.
|
||||
|
||||
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
|
||||
However we still merge a ton of PRs - you can contribute:
|
||||
|
||||
- Bug fixes
|
||||
- Improvements to LLM performance
|
||||
- Support for new providers
|
||||
- Fixes for env specific quirks
|
||||
- Missing standard behavior
|
||||
- Documentation
|
||||
|
||||
Take a look at the git history to see what kind of PRs we end up merging.
|
||||
|
||||
> [!NOTE]
|
||||
> If you do not follow the above guidelines we might close your PR.
|
||||
|
||||
To run opencode locally you need.
|
||||
|
||||
- Bun
|
||||
- Golang 1.24.x
|
||||
|
||||
And run.
|
||||
|
||||
```bash
|
||||
$ bun install
|
||||
$ bun run packages/opencode/src/index.ts
|
||||
```
|
||||
|
||||
#### Development Notes
|
||||
|
||||
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
|
||||
|
||||
### FAQ
|
||||
|
||||
@@ -87,10 +97,9 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
- 100% open source
|
||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||
- Out of the box LSP support
|
||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
|
||||
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
|
||||
#### What's the other repo?
|
||||
|
||||
@@ -98,4 +107,4 @@ The other confusingly named repo has no relation to this one. You can [read the
|
||||
|
||||
---
|
||||
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||
|
||||
189
STATS.md
189
STATS.md
@@ -1,166 +1,27 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ------------------ | ----------------- | ------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ----------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
## Style Guide
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
@@ -1,2 +1,2 @@
|
||||
[install]
|
||||
exact = true
|
||||
exact = true
|
||||
27
flake.lock
generated
27
flake.lock
generated
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764947035,
|
||||
"narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a672be65651c80d3f592a89b3945466584a22069",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
107
flake.nix
107
flake.nix
@@ -1,107 +0,0 @@
|
||||
{
|
||||
description = "OpenCode development flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
systems = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
lib = nixpkgs.lib;
|
||||
forEachSystem = lib.genAttrs systems;
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||
bunTarget = {
|
||||
"aarch64-linux" = "bun-linux-arm64";
|
||||
"x86_64-linux" = "bun-linux-x64";
|
||||
"aarch64-darwin" = "bun-darwin-arm64";
|
||||
"x86_64-darwin" = "bun-darwin-x64";
|
||||
};
|
||||
defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
hashesFile = "${./nix}/hashes.json";
|
||||
hashesData =
|
||||
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
|
||||
nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
|
||||
modelsDev = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
pkgs."models-dev"
|
||||
);
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
packages = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||
hash = nodeModulesHash;
|
||||
};
|
||||
mkPackage = pkgs.callPackage ./nix/opencode.nix { };
|
||||
in
|
||||
{
|
||||
default = mkPackage {
|
||||
version = packageJson.version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
mkNodeModules = mkNodeModules;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
apps = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
opencode-dev = {
|
||||
type = "app";
|
||||
meta = {
|
||||
description = "Nix devshell shell for OpenCode";
|
||||
runtimeInputs = [ pkgs.bun ];
|
||||
};
|
||||
program = "${
|
||||
pkgs.writeShellApplication {
|
||||
name = "opencode-dev";
|
||||
text = ''
|
||||
exec bun run dev "$@"
|
||||
'';
|
||||
}
|
||||
}/bin/opencode-dev";
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
34
github/.gitignore
vendored
34
github/.gitignore
vendored
@@ -1,34 +0,0 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
163
github/README.md
163
github/README.md
@@ -1,163 +0,0 @@
|
||||
# opencode GitHub Action
|
||||
|
||||
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
|
||||
|
||||
Mention `/opencode` in your comment, and opencode will execute tasks within your GitHub Actions runner.
|
||||
|
||||
## Features
|
||||
|
||||
#### Explain an issues
|
||||
|
||||
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
|
||||
|
||||
```
|
||||
/opencode explain this issue
|
||||
```
|
||||
|
||||
#### Fix an issues
|
||||
|
||||
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
|
||||
|
||||
```
|
||||
/opencode fix this
|
||||
```
|
||||
|
||||
#### Review PRs and make changes
|
||||
|
||||
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
|
||||
|
||||
```
|
||||
Delete the attachment from S3 when the note is removed /oc
|
||||
```
|
||||
|
||||
#### Review specific code lines
|
||||
|
||||
Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses.
|
||||
|
||||
```
|
||||
[Comment on specific lines in Files tab]
|
||||
/oc add error handling here
|
||||
```
|
||||
|
||||
When commenting on specific lines, opencode receives:
|
||||
|
||||
- The exact file being reviewed
|
||||
- The specific lines of code
|
||||
- The surrounding diff context
|
||||
- Line number information
|
||||
|
||||
This allows for more targeted requests without needing to specify file paths or line numbers manually.
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
|
||||
```bash
|
||||
opencode github install
|
||||
```
|
||||
|
||||
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
|
||||
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
|
||||
|
||||
```yml
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
## Support
|
||||
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
|
||||
|
||||
## Development
|
||||
|
||||
To test locally:
|
||||
|
||||
1. Navigate to a test repo (e.g. `hello-world`):
|
||||
|
||||
```bash
|
||||
cd hello-world
|
||||
```
|
||||
|
||||
2. Run:
|
||||
|
||||
```bash
|
||||
MODEL=anthropic/claude-sonnet-4-20250514 \
|
||||
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
|
||||
GITHUB_RUN_ID=dummy \
|
||||
MOCK_TOKEN=github_pat_1234567890 \
|
||||
MOCK_EVENT='{"eventName":"issue_comment",...}' \
|
||||
bun /path/to/opencode/github/index.ts
|
||||
```
|
||||
|
||||
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
|
||||
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
|
||||
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
|
||||
- `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
|
||||
- `MOCK_EVENT`: Mock GitHub event payload (see templates below).
|
||||
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/github/index.ts` runs your local version of `opencode`.
|
||||
|
||||
### Issue comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
Replace:
|
||||
|
||||
- `"owner":"sst"` with repo owner
|
||||
- `"repo":"hello-world"` with repo name
|
||||
- `"actor":"fwang"` with the GitHub username of commenter
|
||||
- `"number":4` with the GitHub issue id
|
||||
- `"body":"hey opencode, summarize thread"` with comment body
|
||||
|
||||
### Issue comment with image attachment.
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image "}}}'
|
||||
```
|
||||
|
||||
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
|
||||
|
||||
### PR comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
### PR review comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"pull_request_review_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"pull_request":{"number":7},"comment":{"id":1,"body":"hey opencode, add error handling","path":"src/components/Button.tsx","diff_hunk":"@@ -45,8 +45,11 @@\n- const handleClick = () => {\n- console.log('clicked')\n+ const handleClick = useCallback(() => {\n+ console.log('clicked')\n+ doSomething()\n+ }, [doSomething])","line":47,"original_line":45,"position":10,"commit_id":"abc123","original_commit_id":"def456"}}}'
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
name: "opencode GitHub Action"
|
||||
description: "Run opencode in GitHub Actions workflows"
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "orange"
|
||||
|
||||
inputs:
|
||||
model:
|
||||
description: "Model to use"
|
||||
required: true
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
prompt:
|
||||
description: "Custom prompt to override the default prompt"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install opencode
|
||||
shell: bash
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Run opencode
|
||||
shell: bash
|
||||
id: run_opencode
|
||||
run: opencode github run
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
1023
github/index.ts
1023
github/index.ts
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "github",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
9
github/sst-env.d.ts
vendored
9
github/sst-env.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
99
go.mod
Normal file
99
go.mod
Normal file
@@ -0,0 +1,99 @@
|
||||
module github.com/sst/opencode
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/alecthomas/chroma/v2 v2.18.0
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
|
||||
github.com/charmbracelet/x/ansi v0.9.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||
golang.org/x/image v0.28.0
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/charmbracelet/x/input => ./packages/tui/input
|
||||
)
|
||||
|
||||
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
github.com/charmbracelet/x/input v0.3.7 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.1 // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.127.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sanity-io/litter v1.5.8 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/atombender/go-jsonschema
|
||||
github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
|
||||
)
|
||||
315
go.sum
Normal file
315
go.sum
Normal file
@@ -0,0 +1,315 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY=
|
||||
github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4/go.mod h1:0wWFRpsgF7vHsCukVZ5LAhZkiR4j875H6KEM2/tFQmA=
|
||||
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
|
||||
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 h1:W6DpZX6zSkZr0iFq6JVh1vItLoxfYtNlaxOJtWp8Kis=
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3/go.mod h1:65HTtKURcv/ict9ZQhr6zT84JqIjMcJbyrZYHHKNfKA=
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 h1:MTSs/nsZNfZPbYk/r9hluK2BtwoqvEYruAujNVwgDv0=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
|
||||
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
|
||||
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
18
infra/app.ts
18
infra/app.ts
@@ -1,9 +1,11 @@
|
||||
import { domain } from "./stage"
|
||||
export const domain = (() => {
|
||||
if ($app.stage === "production") return "opencode.ai"
|
||||
if ($app.stage === "dev") return "dev.opencode.ai"
|
||||
return `${$app.stage}.dev.opencode.ai`
|
||||
})()
|
||||
|
||||
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
|
||||
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
|
||||
export const EMAILOCTOPUS_API_KEY = new sst.Secret("EMAILOCTOPUS_API_KEY")
|
||||
const ADMIN_SECRET = new sst.Secret("ADMIN_SECRET")
|
||||
const bucket = new sst.cloudflare.Bucket("Bucket")
|
||||
|
||||
export const api = new sst.cloudflare.Worker("Api", {
|
||||
@@ -13,7 +15,7 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||
WEB_DOMAIN: domain,
|
||||
},
|
||||
url: true,
|
||||
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ADMIN_SECRET],
|
||||
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY],
|
||||
transform: {
|
||||
worker: (args) => {
|
||||
args.logpush = true
|
||||
@@ -27,8 +29,8 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||
])
|
||||
args.migrations = {
|
||||
// Note: when releasing the next tag, make sure all stages use tag v2
|
||||
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
|
||||
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
|
||||
oldTag: $app.stage === "production" ? "" : "v1",
|
||||
newTag: $app.stage === "production" ? "" : "v1",
|
||||
//newSqliteClasses: ["SyncServer"],
|
||||
}
|
||||
},
|
||||
@@ -36,11 +38,11 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||
})
|
||||
|
||||
new sst.cloudflare.x.Astro("Web", {
|
||||
domain: "docs." + domain,
|
||||
domain,
|
||||
path: "packages/web",
|
||||
environment: {
|
||||
// For astro config
|
||||
SST_STAGE: $app.stage,
|
||||
VITE_API_URL: api.url.apply((url) => url!),
|
||||
VITE_API_URL: api.url,
|
||||
},
|
||||
})
|
||||
|
||||
169
infra/console.ts
169
infra/console.ts
@@ -1,169 +0,0 @@
|
||||
import { domain } from "./stage"
|
||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||
|
||||
////////////////
|
||||
// DATABASE
|
||||
////////////////
|
||||
|
||||
const cluster = planetscale.getDatabaseOutput({
|
||||
name: "opencode",
|
||||
organization: "anomalyco",
|
||||
})
|
||||
|
||||
const branch =
|
||||
$app.stage === "production"
|
||||
? planetscale.getBranchOutput({
|
||||
name: "production",
|
||||
organization: cluster.organization,
|
||||
database: cluster.name,
|
||||
})
|
||||
: new planetscale.Branch("DatabaseBranch", {
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
name: $app.stage,
|
||||
parentBranch: "production",
|
||||
})
|
||||
const password = new planetscale.Password("DatabasePassword", {
|
||||
name: $app.stage,
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
branch: branch.name,
|
||||
})
|
||||
|
||||
export const database = new sst.Linkable("Database", {
|
||||
properties: {
|
||||
host: password.accessHostUrl,
|
||||
database: cluster.name,
|
||||
username: password.username,
|
||||
password: password.plaintext,
|
||||
port: 3306,
|
||||
},
|
||||
})
|
||||
|
||||
new sst.x.DevCommand("Studio", {
|
||||
link: [database],
|
||||
dev: {
|
||||
command: "bun db studio",
|
||||
directory: "packages/console/core",
|
||||
autostart: true,
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// AUTH
|
||||
////////////////
|
||||
|
||||
const GITHUB_CLIENT_ID_CONSOLE = new sst.Secret("GITHUB_CLIENT_ID_CONSOLE")
|
||||
const GITHUB_CLIENT_SECRET_CONSOLE = new sst.Secret("GITHUB_CLIENT_SECRET_CONSOLE")
|
||||
const GOOGLE_CLIENT_ID = new sst.Secret("GOOGLE_CLIENT_ID")
|
||||
const authStorage = new sst.cloudflare.Kv("AuthStorage")
|
||||
export const auth = new sst.cloudflare.Worker("AuthApi", {
|
||||
domain: `auth.${domain}`,
|
||||
handler: "packages/console/function/src/auth.ts",
|
||||
url: true,
|
||||
link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE, GOOGLE_CLIENT_ID],
|
||||
})
|
||||
|
||||
////////////////
|
||||
// GATEWAY
|
||||
////////////////
|
||||
|
||||
export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", {
|
||||
url: $interpolate`https://${domain}/stripe/webhook`,
|
||||
enabledEvents: [
|
||||
"checkout.session.async_payment_failed",
|
||||
"checkout.session.async_payment_succeeded",
|
||||
"checkout.session.completed",
|
||||
"checkout.session.expired",
|
||||
"charge.refunded",
|
||||
"customer.created",
|
||||
"customer.deleted",
|
||||
"customer.updated",
|
||||
"customer.discount.created",
|
||||
"customer.discount.deleted",
|
||||
"customer.discount.updated",
|
||||
"customer.source.created",
|
||||
"customer.source.deleted",
|
||||
"customer.source.expiring",
|
||||
"customer.source.updated",
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.deleted",
|
||||
"customer.subscription.paused",
|
||||
"customer.subscription.pending_update_applied",
|
||||
"customer.subscription.pending_update_expired",
|
||||
"customer.subscription.resumed",
|
||||
"customer.subscription.trial_will_end",
|
||||
"customer.subscription.updated",
|
||||
],
|
||||
})
|
||||
|
||||
const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS1"),
|
||||
new sst.Secret("ZEN_MODELS2"),
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
})
|
||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||
properties: { value: stripeWebhook.secret },
|
||||
})
|
||||
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
|
||||
////////////////
|
||||
// CONSOLE
|
||||
////////////////
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
|
||||
let logProcessor
|
||||
if ($app.stage === "production" || $app.stage === "frank") {
|
||||
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
||||
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
link: [HONEYCOMB_API_KEY],
|
||||
})
|
||||
}
|
||||
|
||||
new sst.cloudflare.x.SolidStart("Console", {
|
||||
domain,
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
bucket,
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
STRIPE_SECRET_KEY,
|
||||
EMAILOCTOPUS_API_KEY,
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
? [
|
||||
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
||||
new sst.Secret("CLOUDFLARE_API_TOKEN", process.env.CLOUDFLARE_API_TOKEN!),
|
||||
]
|
||||
: []),
|
||||
gatewayKv,
|
||||
],
|
||||
environment: {
|
||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
},
|
||||
transform: {
|
||||
server: {
|
||||
transform: {
|
||||
worker: {
|
||||
placement: { mode: "smart" },
|
||||
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import { domain } from "./stage"
|
||||
|
||||
new sst.cloudflare.StaticSite("Desktop", {
|
||||
domain: "desktop." + domain,
|
||||
path: "packages/desktop",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { SECRET } from "./secret"
|
||||
import { domain } from "./stage"
|
||||
|
||||
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||
|
||||
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
|
||||
domain: "enterprise." + domain,
|
||||
path: "packages/enterprise",
|
||||
buildCommand: "bun run build:cloudflare",
|
||||
environment: {
|
||||
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
|
||||
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
|
||||
OPENCODE_STORAGE_BUCKET: storage.name,
|
||||
},
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
export const SECRET = {
|
||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export const domain = (() => {
|
||||
if ($app.stage === "production") return "opencode.ai"
|
||||
if ($app.stage === "dev") return "dev.opencode.ai"
|
||||
return `${$app.stage}.dev.opencode.ai`
|
||||
})()
|
||||
|
||||
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
||||
|
||||
new cloudflare.RegionalHostname("RegionalHostname", {
|
||||
hostname: domain,
|
||||
regionKey: "us",
|
||||
zoneId: zoneID,
|
||||
})
|
||||
244
install
244
install
@@ -2,112 +2,54 @@
|
||||
set -euo pipefail
|
||||
APP=opencode
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
ORANGE='\033[38;5;214m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
ORANGE='\033[38;2;255;140;0m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
case "$raw_os" in
|
||||
Darwin*) os="darwin" ;;
|
||||
Linux*) os="linux" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||
esac
|
||||
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$os" == "darwin" ]]; then
|
||||
os="darwin"
|
||||
fi
|
||||
arch=$(uname -m)
|
||||
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
elif [[ "$arch" == "x86_64" ]]; then
|
||||
arch="x64"
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||
if [ "$rosetta_flag" = "1" ]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
fi
|
||||
filename="$APP-$os-$arch.zip"
|
||||
|
||||
combo="$os-$arch"
|
||||
case "$combo" in
|
||||
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||
|
||||
case "$filename" in
|
||||
*"-linux-"*)
|
||||
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
*"-darwin-"*)
|
||||
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||
;;
|
||||
*"-windows-"*)
|
||||
[[ "$arch" == "x64" ]] || exit 1
|
||||
;;
|
||||
*)
|
||||
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive_ext=".zip"
|
||||
if [ "$os" = "linux" ]; then
|
||||
archive_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
is_musl=false
|
||||
if [ "$os" = "linux" ]; then
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
is_musl=true
|
||||
fi
|
||||
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
is_musl=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ]; then
|
||||
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||
if [ "$avx2" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
if [ "$needs_baseline" = "true" ]; then
|
||||
target="$target-baseline"
|
||||
fi
|
||||
if [ "$is_musl" = "true" ]; then
|
||||
target="$target-musl"
|
||||
fi
|
||||
|
||||
filename="$APP-$target$archive_ext"
|
||||
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
INSTALL_DIR=$HOME/.opencode/bin
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/sst/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
echo "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
@@ -121,8 +63,8 @@ print_message() {
|
||||
local color=""
|
||||
|
||||
case $level in
|
||||
info) color="${NC}" ;;
|
||||
warning) color="${NC}" ;;
|
||||
info) color="${GREEN}" ;;
|
||||
warning) color="${YELLOW}" ;;
|
||||
error) color="${RED}" ;;
|
||||
esac
|
||||
|
||||
@@ -140,122 +82,21 @@ check_version() {
|
||||
installed_version=$(echo $installed_version | awk '{print $2}')
|
||||
|
||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||
print_message info "Installed version: ${YELLOW}$installed_version."
|
||||
else
|
||||
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
|
||||
print_message info "Version ${YELLOW}$specific_version${GREEN} already installed"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
unbuffered_sed() {
|
||||
if echo | sed -u -e "" >/dev/null 2>&1; then
|
||||
sed -nu "$@"
|
||||
elif echo | sed -l -e "" >/dev/null 2>&1; then
|
||||
sed -nl "$@"
|
||||
else
|
||||
local pad="$(printf "\n%512s" "")"
|
||||
sed -ne "s/$/\\${pad}/" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
print_progress() {
|
||||
local bytes="$1"
|
||||
local length="$2"
|
||||
[ "$length" -gt 0 ] || return 0
|
||||
|
||||
local width=50
|
||||
local percent=$(( bytes * 100 / length ))
|
||||
[ "$percent" -gt 100 ] && percent=100
|
||||
local on=$(( percent * width / 100 ))
|
||||
local off=$(( width - on ))
|
||||
|
||||
local filled=$(printf "%*s" "$on" "")
|
||||
filled=${filled// /■}
|
||||
local empty=$(printf "%*s" "$off" "")
|
||||
empty=${empty// /・}
|
||||
|
||||
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
|
||||
}
|
||||
|
||||
download_with_progress() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
|
||||
if [ -t 2 ]; then
|
||||
exec 4>&2
|
||||
else
|
||||
exec 4>/dev/null
|
||||
fi
|
||||
|
||||
local tmp_dir=${TMPDIR:-/tmp}
|
||||
local basename="${tmp_dir}/opencode_install_$$"
|
||||
local tracefile="${basename}.trace"
|
||||
|
||||
rm -f "$tracefile"
|
||||
mkfifo "$tracefile"
|
||||
|
||||
# Hide cursor
|
||||
printf "\033[?25l" >&4
|
||||
|
||||
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
|
||||
|
||||
(
|
||||
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
|
||||
) &
|
||||
local curl_pid=$!
|
||||
|
||||
unbuffered_sed \
|
||||
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
|
||||
-e '/^0000: content-length:/p' \
|
||||
-e '/^<= recv data/p' \
|
||||
"$tracefile" | \
|
||||
{
|
||||
local length=0
|
||||
local bytes=0
|
||||
|
||||
while IFS=" " read -r -a line; do
|
||||
[ "${#line[@]}" -lt 2 ] && continue
|
||||
local tag="${line[0]} ${line[1]}"
|
||||
|
||||
if [ "$tag" = "0000: content-length:" ]; then
|
||||
length="${line[2]}"
|
||||
length=$(echo "$length" | tr -d '\r')
|
||||
bytes=0
|
||||
elif [ "$tag" = "<= recv" ]; then
|
||||
local size="${line[3]}"
|
||||
bytes=$(( bytes + size ))
|
||||
if [ "$length" -gt 0 ]; then
|
||||
print_progress "$bytes" "$length"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wait $curl_pid
|
||||
local ret=$?
|
||||
echo "" >&4
|
||||
return $ret
|
||||
}
|
||||
|
||||
download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
||||
mkdir -p opencodetmp && cd opencodetmp
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||
# Fallback to standard curl on Windows or if custom progress fails
|
||||
curl -# -L -o "$filename" "$url"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
tar -xzf "$filename"
|
||||
else
|
||||
unzip -q "$filename"
|
||||
fi
|
||||
|
||||
curl -# -L -o "$filename" "$url"
|
||||
unzip -q "$filename"
|
||||
mv opencode "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
cd .. && rm -rf opencodetmp
|
||||
cd .. && rm -rf opencodetmp
|
||||
}
|
||||
|
||||
check_version
|
||||
@@ -271,7 +112,7 @@ add_to_path() {
|
||||
elif [[ -w $config_file ]]; then
|
||||
echo -e "\n# opencode" >> "$config_file"
|
||||
echo "$command" >> "$config_file"
|
||||
print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
|
||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
||||
else
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " $command"
|
||||
@@ -345,20 +186,3 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
||||
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
echo -e ""
|
||||
echo -e "${MUTED} ${NC} ▄ "
|
||||
echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
|
||||
echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
|
||||
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
|
||||
echo -e ""
|
||||
echo -e "cd <project> ${MUTED}# Open directory${NC}"
|
||||
echo -e "opencode ${MUTED}# Run command${NC}"
|
||||
echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1759827172859,
|
||||
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
|
||||
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
|
||||
{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"}
|
||||
{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const dir = process.cwd()
|
||||
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
|
||||
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||
const version = process.env.OPENCODE_VERSION ?? "local"
|
||||
const channel = process.env.OPENCODE_CHANNEL ?? "local"
|
||||
|
||||
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts", worker, parser],
|
||||
outdir: "./dist",
|
||||
target: "bun",
|
||||
sourcemap: "none",
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
external: ["@opentui/core"],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${version}'`,
|
||||
OPENCODE_CHANNEL: `'${channel}'`,
|
||||
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
|
||||
OPENCODE_WORKER_PATH: "undefined",
|
||||
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error("bundle failed")
|
||||
for (const log of result.logs) console.error(log)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
|
||||
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
|
||||
await Bun.write(parserOut, Bun.file(parser))
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"nodeModules": "sha256-IzF5XDY09Z1p/8jgYIHhE/jpKPub15KKUpV+a/aKpuc="
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
|
||||
args:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "opencode-node_modules";
|
||||
version = args.version;
|
||||
src = args.src;
|
||||
|
||||
impureEnvVars =
|
||||
lib.fetchers.proxyImpureEnvVars
|
||||
++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [ bun cacert curl ];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
export HOME=$(mktemp -d)
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
bun install \
|
||||
--cpu="*" \
|
||||
--os="*" \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress \
|
||||
--linker=isolated
|
||||
bun --bun ${args.canonicalizeScript}
|
||||
bun --bun ${args.normalizeBinsScript}
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
while IFS= read -r dir; do
|
||||
rel="''${dir#./}"
|
||||
dest="$out/$rel"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp -R "$dir" "$dest"
|
||||
done < <(find . -type d -name node_modules -prune | sort)
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash = hash;
|
||||
}
|
||||
135
nix/opencode.nix
135
nix/opencode.nix
@@ -1,135 +0,0 @@
|
||||
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
in
|
||||
stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
pname = "opencode";
|
||||
version = args.version;
|
||||
|
||||
src = args.src;
|
||||
|
||||
node_modules = mkModules {
|
||||
version = finalAttrs.version;
|
||||
src = finalAttrs.src;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
makeBinaryWrapper
|
||||
];
|
||||
|
||||
env.MODELS_DEV_API_JSON = args.modelsDev;
|
||||
env.OPENCODE_VERSION = args.version;
|
||||
env.OPENCODE_CHANNEL = "stable";
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
cp -r ${finalAttrs.node_modules}/node_modules .
|
||||
cp -r ${finalAttrs.node_modules}/packages .
|
||||
|
||||
(
|
||||
cd packages/opencode
|
||||
|
||||
chmod -R u+w ./node_modules
|
||||
mkdir -p ./node_modules/@opencode-ai
|
||||
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
|
||||
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
|
||||
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
|
||||
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
|
||||
|
||||
cp ${./bundle.ts} ./bundle.ts
|
||||
chmod +x ./bundle.ts
|
||||
bun run ./bundle.ts
|
||||
)
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
cd packages/opencode
|
||||
if [ ! -d dist ]; then
|
||||
echo "ERROR: dist directory missing after bundle step"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p $out/lib/opencode
|
||||
cp -r dist $out/lib/opencode/
|
||||
chmod -R u+w $out/lib/opencode/dist
|
||||
|
||||
# Select bundled worker assets deterministically (sorted find output)
|
||||
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
|
||||
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
|
||||
if [ -z "$worker_file" ]; then
|
||||
echo "ERROR: bundled worker not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
|
||||
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
|
||||
for patch_file in "$worker_file" "$parser_worker_file"; do
|
||||
[ -z "$patch_file" ] && continue
|
||||
[ ! -f "$patch_file" ] && continue
|
||||
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
|
||||
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
|
||||
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p $out/lib/opencode/node_modules
|
||||
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
|
||||
mkdir -p $out/lib/opencode/node_modules/@opentui
|
||||
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||
--add-flags "run" \
|
||||
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
|
||||
--argv0 opencode
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
|
||||
if [ -d "$pkg" ]; then
|
||||
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
|
||||
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
|
||||
$out/lib/opencode/node_modules/@opentui/$pkgName
|
||||
fi
|
||||
done
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
meta = {
|
||||
description = "AI coding agent built for the terminal";
|
||||
longDescription = ''
|
||||
OpenCode is a terminal-based agent that can build anything.
|
||||
It combines a TypeScript/JavaScript core with a Go-based TUI
|
||||
to provide an interactive AI coding experience.
|
||||
'';
|
||||
homepage = "https://github.com/sst/opencode";
|
||||
license = lib.licenses.mit;
|
||||
platforms = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
mainProgram = "opencode";
|
||||
};
|
||||
})
|
||||
@@ -1,115 +0,0 @@
|
||||
import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
const version = "@VERSION@"
|
||||
const pkg = path.join(process.cwd(), "packages/opencode")
|
||||
const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"))
|
||||
const worker = "./src/cli/cmd/tui/worker.ts"
|
||||
const target = process.env["BUN_COMPILE_TARGET"]
|
||||
|
||||
if (!target) {
|
||||
throw new Error("BUN_COMPILE_TARGET not set")
|
||||
}
|
||||
|
||||
process.chdir(pkg)
|
||||
|
||||
const manifestName = "opencode-assets.manifest"
|
||||
const manifestPath = path.join(pkg, manifestName)
|
||||
|
||||
const readTrackedAssets = () => {
|
||||
if (!fs.existsSync(manifestPath)) return []
|
||||
return fs
|
||||
.readFileSync(manifestPath, "utf8")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
const removeTrackedAssets = () => {
|
||||
for (const file of readTrackedAssets()) {
|
||||
const filePath = path.join(pkg, file)
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.rmSync(filePath, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assets = new Set<string>()
|
||||
|
||||
const addAsset = async (p: string) => {
|
||||
const file = path.basename(p)
|
||||
const dest = path.join(pkg, file)
|
||||
await Bun.write(dest, Bun.file(p))
|
||||
assets.add(file)
|
||||
}
|
||||
|
||||
removeTrackedAssets()
|
||||
|
||||
const result = await Bun.build({
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
sourcemap: "external",
|
||||
entrypoints: ["./src/index.ts", parser, worker],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'@VERSION@'`,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"),
|
||||
OPENCODE_CHANNEL: "'latest'",
|
||||
},
|
||||
compile: {
|
||||
target,
|
||||
outfile: "opencode",
|
||||
execArgv: ["--user-agent=opencode/" + version, '--env-file=""', "--"],
|
||||
windows: {},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Build failed!")
|
||||
for (const log of result.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
throw new Error("Compilation failed")
|
||||
}
|
||||
|
||||
const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||
for (const x of assetOutputs) {
|
||||
await addAsset(x.path)
|
||||
}
|
||||
|
||||
const bundle = await Bun.build({
|
||||
entrypoints: [worker],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
target: "bun",
|
||||
outdir: "./.opencode-worker",
|
||||
sourcemap: "none",
|
||||
})
|
||||
|
||||
if (!bundle.success) {
|
||||
console.error("Worker build failed!")
|
||||
for (const log of bundle.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
throw new Error("Worker compilation failed")
|
||||
}
|
||||
|
||||
const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? []
|
||||
for (const x of workerAssets) {
|
||||
await addAsset(x.path)
|
||||
}
|
||||
|
||||
const output = bundle.outputs.find((x) => x.kind === "entry-point")
|
||||
if (!output) {
|
||||
throw new Error("Worker build produced no entry-point output")
|
||||
}
|
||||
|
||||
const dest = path.join(pkg, "opencode-worker.js")
|
||||
await Bun.write(dest, Bun.file(output.path))
|
||||
fs.rmSync(path.dirname(output.path), { recursive: true, force: true })
|
||||
|
||||
const list = Array.from(assets)
|
||||
await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "")
|
||||
|
||||
console.log("Build successful!")
|
||||
@@ -1,113 +0,0 @@
|
||||
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
||||
import { join, relative } from "path"
|
||||
|
||||
type SemverLike = {
|
||||
valid: (value: string) => string | null
|
||||
rcompare: (left: string, right: string) => number
|
||||
}
|
||||
|
||||
type Entry = {
|
||||
dir: string
|
||||
version: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const root = process.cwd()
|
||||
const bunRoot = join(root, "node_modules/.bun")
|
||||
const linkRoot = join(bunRoot, "node_modules")
|
||||
const directories = (await readdir(bunRoot)).sort()
|
||||
const versions = new Map<string, Entry[]>()
|
||||
|
||||
for (const entry of directories) {
|
||||
const full = join(bunRoot, entry)
|
||||
const info = await lstat(full)
|
||||
if (!info.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
const parsed = parseEntry(entry)
|
||||
if (!parsed) {
|
||||
continue
|
||||
}
|
||||
const list = versions.get(parsed.name) ?? []
|
||||
list.push({ dir: full, version: parsed.version, label: entry })
|
||||
versions.set(parsed.name, list)
|
||||
}
|
||||
|
||||
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
||||
| SemverLike
|
||||
| {
|
||||
default: SemverLike
|
||||
}
|
||||
const semver = "default" in semverModule ? semverModule.default : semverModule
|
||||
const selections = new Map<string, Entry>()
|
||||
|
||||
for (const [slug, list] of versions) {
|
||||
list.sort((a, b) => {
|
||||
const left = semver.valid(a.version)
|
||||
const right = semver.valid(b.version)
|
||||
if (left && right) {
|
||||
const delta = semver.rcompare(left, right)
|
||||
if (delta !== 0) {
|
||||
return delta
|
||||
}
|
||||
}
|
||||
if (left && !right) {
|
||||
return -1
|
||||
}
|
||||
if (!left && right) {
|
||||
return 1
|
||||
}
|
||||
return b.version.localeCompare(a.version)
|
||||
})
|
||||
selections.set(slug, list[0])
|
||||
}
|
||||
|
||||
await rm(linkRoot, { recursive: true, force: true })
|
||||
await mkdir(linkRoot, { recursive: true })
|
||||
|
||||
const rewrites: string[] = []
|
||||
|
||||
for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
|
||||
const parts = slug.split("/")
|
||||
const leaf = parts.pop()
|
||||
if (!leaf) {
|
||||
continue
|
||||
}
|
||||
const parent = join(linkRoot, ...parts)
|
||||
await mkdir(parent, { recursive: true })
|
||||
const linkPath = join(parent, leaf)
|
||||
const desired = join(entry.dir, "node_modules", slug)
|
||||
const exists = await lstat(desired)
|
||||
.then((info) => info.isDirectory())
|
||||
.catch(() => false)
|
||||
if (!exists) {
|
||||
continue
|
||||
}
|
||||
const relativeTarget = relative(parent, desired)
|
||||
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
|
||||
await rm(linkPath, { recursive: true, force: true })
|
||||
await symlink(resolved, linkPath)
|
||||
rewrites.push(slug + " -> " + resolved)
|
||||
}
|
||||
|
||||
rewrites.sort()
|
||||
console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links")
|
||||
for (const line of rewrites.slice(0, 20)) {
|
||||
console.log(" ", line)
|
||||
}
|
||||
if (rewrites.length > 20) {
|
||||
console.log(" ...")
|
||||
}
|
||||
|
||||
function parseEntry(label: string) {
|
||||
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
|
||||
if (marker <= 0) {
|
||||
return null
|
||||
}
|
||||
const name = label.slice(0, marker).replace(/\+/g, "/")
|
||||
const version = label.slice(marker + 1)
|
||||
if (!name || !version) {
|
||||
return null
|
||||
}
|
||||
return { name, version }
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
||||
import { join, relative } from "path"
|
||||
|
||||
type PackageManifest = {
|
||||
name?: string
|
||||
bin?: string | Record<string, string>
|
||||
}
|
||||
|
||||
const root = process.cwd()
|
||||
const bunRoot = join(root, "node_modules/.bun")
|
||||
const bunEntries = (await safeReadDir(bunRoot)).sort()
|
||||
let rewritten = 0
|
||||
|
||||
for (const entry of bunEntries) {
|
||||
const modulesRoot = join(bunRoot, entry, "node_modules")
|
||||
if (!(await exists(modulesRoot))) {
|
||||
continue
|
||||
}
|
||||
const binRoot = join(modulesRoot, ".bin")
|
||||
await rm(binRoot, { recursive: true, force: true })
|
||||
await mkdir(binRoot, { recursive: true })
|
||||
|
||||
const packageDirs = await collectPackages(modulesRoot)
|
||||
for (const packageDir of packageDirs) {
|
||||
const manifest = await readManifest(packageDir)
|
||||
if (!manifest) {
|
||||
continue
|
||||
}
|
||||
const binField = manifest.bin
|
||||
if (!binField) {
|
||||
continue
|
||||
}
|
||||
const seen = new Set<string>()
|
||||
if (typeof binField === "string") {
|
||||
const fallback = manifest.name ?? packageDir.split("/").pop()
|
||||
if (fallback) {
|
||||
await linkBinary(binRoot, fallback, packageDir, binField, seen)
|
||||
}
|
||||
} else {
|
||||
const entries = Object.entries(binField).sort((a, b) => a[0].localeCompare(b[0]))
|
||||
for (const [name, target] of entries) {
|
||||
await linkBinary(binRoot, name, packageDir, target, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
|
||||
|
||||
async function collectPackages(modulesRoot: string) {
|
||||
const found: string[] = []
|
||||
const topLevel = (await safeReadDir(modulesRoot)).sort()
|
||||
for (const name of topLevel) {
|
||||
if (name === ".bin" || name === ".bun") {
|
||||
continue
|
||||
}
|
||||
const full = join(modulesRoot, name)
|
||||
if (!(await isDirectory(full))) {
|
||||
continue
|
||||
}
|
||||
if (name.startsWith("@")) {
|
||||
const scoped = (await safeReadDir(full)).sort()
|
||||
for (const child of scoped) {
|
||||
const scopedDir = join(full, child)
|
||||
if (await isDirectory(scopedDir)) {
|
||||
found.push(scopedDir)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
found.push(full)
|
||||
}
|
||||
return found.sort()
|
||||
}
|
||||
|
||||
async function readManifest(dir: string) {
|
||||
const file = Bun.file(join(dir, "package.json"))
|
||||
if (!(await file.exists())) {
|
||||
return null
|
||||
}
|
||||
const data = (await file.json()) as PackageManifest
|
||||
return data
|
||||
}
|
||||
|
||||
async function linkBinary(binRoot: string, name: string, packageDir: string, target: string, seen: Set<string>) {
|
||||
if (!name || !target) {
|
||||
return
|
||||
}
|
||||
const normalizedName = normalizeBinName(name)
|
||||
if (seen.has(normalizedName)) {
|
||||
return
|
||||
}
|
||||
const resolved = join(packageDir, target)
|
||||
const script = Bun.file(resolved)
|
||||
if (!(await script.exists())) {
|
||||
return
|
||||
}
|
||||
seen.add(normalizedName)
|
||||
const destination = join(binRoot, normalizedName)
|
||||
const relativeTarget = relative(binRoot, resolved) || "."
|
||||
await rm(destination, { force: true })
|
||||
await symlink(relativeTarget, destination)
|
||||
rewritten++
|
||||
}
|
||||
|
||||
async function exists(path: string) {
|
||||
try {
|
||||
await lstat(path)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function isDirectory(path: string) {
|
||||
try {
|
||||
const info = await lstat(path)
|
||||
return info.isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function safeReadDir(path: string) {
|
||||
try {
|
||||
return await readdir(path)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBinName(name: string) {
|
||||
const slash = name.lastIndexOf("/")
|
||||
if (slash >= 0) {
|
||||
return name.slice(slash + 1)
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
/**
|
||||
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
|
||||
* argv: [node, script, file, mainWasm, ...wasmPaths]
|
||||
*/
|
||||
const [, , file, mainWasm, ...wasmPaths] = process.argv
|
||||
|
||||
if (!file || !mainWasm) {
|
||||
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(file, "utf8")
|
||||
const byName = new Map<string, string>()
|
||||
|
||||
for (const wasm of wasmPaths) {
|
||||
const name = path.basename(wasm)
|
||||
byName.set(name, wasm)
|
||||
}
|
||||
|
||||
let next = content
|
||||
|
||||
for (const [name, wasmPath] of byName) {
|
||||
next = next.replaceAll(name, wasmPath)
|
||||
}
|
||||
|
||||
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
|
||||
|
||||
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
|
||||
next = next.replace(/(\.\/)+/g, "./")
|
||||
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
|
||||
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
|
||||
|
||||
if (next !== content) fs.writeFileSync(file, next)
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
SYSTEM=${SYSTEM:-x86_64-linux}
|
||||
DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json}
|
||||
HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
|
||||
|
||||
if [ ! -f "$HASH_FILE" ]; then
|
||||
cat >"$HASH_FILE" <<EOF
|
||||
{
|
||||
"nodeModules": "$DUMMY"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then
|
||||
git add -N "$HASH_FILE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
export DUMMY
|
||||
export NIX_KEEP_OUTPUTS=1
|
||||
export NIX_KEEP_DERIVATIONS=1
|
||||
|
||||
cleanup() {
|
||||
rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
write_node_modules_hash() {
|
||||
local value="$1"
|
||||
local temp
|
||||
temp=$(mktemp)
|
||||
jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp"
|
||||
mv "$temp" "$HASH_FILE"
|
||||
}
|
||||
|
||||
TARGET="packages.${SYSTEM}.default"
|
||||
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
|
||||
CORRECT_HASH=""
|
||||
|
||||
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
|
||||
|
||||
echo "Setting dummy node_modules outputHash for ${SYSTEM}..."
|
||||
write_node_modules_hash "$DUMMY"
|
||||
|
||||
BUILD_LOG=$(mktemp)
|
||||
JSON_OUTPUT=$(mktemp)
|
||||
|
||||
echo "Building node_modules for ${SYSTEM} to discover correct outputHash..."
|
||||
echo "Attempting to realize derivation: ${DRV_PATH}"
|
||||
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
|
||||
|
||||
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
|
||||
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
|
||||
echo "Realized node_modules output: $BUILD_PATH"
|
||||
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
echo "Searching for kept failed build directory..."
|
||||
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1)
|
||||
|
||||
if [ -z "$KEPT_DIR" ]; then
|
||||
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1)
|
||||
fi
|
||||
|
||||
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
|
||||
echo "Found kept build directory: $KEPT_DIR"
|
||||
if [ -d "$KEPT_DIR/build" ]; then
|
||||
HASH_PATH="$KEPT_DIR/build"
|
||||
else
|
||||
HASH_PATH="$KEPT_DIR"
|
||||
fi
|
||||
|
||||
echo "Attempting to hash: $HASH_PATH"
|
||||
ls -la "$HASH_PATH" || true
|
||||
|
||||
if [ -d "$HASH_PATH/node_modules" ]; then
|
||||
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
|
||||
echo "Computed hash from kept build: $CORRECT_HASH"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
|
||||
echo "Build log:"
|
||||
cat "$BUILD_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_node_modules_hash "$CORRECT_HASH"
|
||||
|
||||
jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
|
||||
|
||||
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
||||
|
||||
rm -f "$BUILD_LOG"
|
||||
unset BUILD_LOG
|
||||
28
opencode.json
Normal file
28
opencode.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openrouter": {
|
||||
"models": {
|
||||
"moonshotai/kimi-k2": {
|
||||
"options": {
|
||||
"provider": {
|
||||
"order": ["baseten"],
|
||||
"allow_fallbacks": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"huggingface": {
|
||||
"models": {
|
||||
"Qwen/Qwen3-235B-A22B-Instruct-2507:fireworks-ai": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"weather": {
|
||||
"type": "local",
|
||||
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
|
||||
}
|
||||
}
|
||||
}
|
||||
76
package.json
76
package.json
@@ -1,70 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.3",
|
||||
"packageManager": "bun@1.2.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'"
|
||||
"dev": "bun run packages/opencode/src/index.ts",
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"stainless": "./scripts/stainless",
|
||||
"postinstall": "./scripts/hooks"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"packages/console/*",
|
||||
"packages/sdk/js",
|
||||
"packages/slack"
|
||||
"packages/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.3",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.6.0-beta.10",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"solid-list": "0.3.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"virtua": "0.42.3",
|
||||
"vite": "7.1.4",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"solid-js": "1.9.10",
|
||||
"vite-plugin-solid": "2.11.10"
|
||||
"@types/node": "22.13.9",
|
||||
"zod": "3.25.49",
|
||||
"ai": "5.0.0-beta.21"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"sst": "3.17.23",
|
||||
"turbo": "2.5.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -78,16 +37,7 @@
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"sharp",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"web-tree-sitter"
|
||||
"sharp"
|
||||
],
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
|
||||
}
|
||||
"patchedDependencies": {}
|
||||
}
|
||||
|
||||
145
packages/client/tui/cmd/tui/main.go
Normal file
145
packages/client/tui/cmd/tui/main.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
"github.com/sst/opencode/internal/api"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/clipboard"
|
||||
"github.com/sst/opencode/internal/tui"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
func main() {
|
||||
version := Version
|
||||
if version != "dev" && !strings.HasPrefix(Version, "v") {
|
||||
version = "v" + Version
|
||||
}
|
||||
|
||||
var model *string = flag.String("model", "", "model to begin with")
|
||||
var prompt *string = flag.String("prompt", "", "prompt to begin with")
|
||||
var mode *string = flag.String("mode", "", "mode to begin with")
|
||||
flag.Parse()
|
||||
|
||||
url := os.Getenv("OPENCODE_SERVER")
|
||||
|
||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||
var appInfo opencode.App
|
||||
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal app info", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
modesStr := os.Getenv("OPENCODE_MODES")
|
||||
var modes []opencode.Mode
|
||||
err = json.Unmarshal([]byte(modesStr), &modes)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal modes", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
slog.Error("Failed to stat stdin", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if there's data piped to stdin
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
stdin, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
slog.Error("Failed to read stdin", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
stdinContent := strings.TrimSpace(string(stdin))
|
||||
if stdinContent != "" {
|
||||
if prompt == nil || *prompt == "" {
|
||||
prompt = &stdinContent
|
||||
} else {
|
||||
combined := *prompt + "\n" + stdinContent
|
||||
prompt = &combined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := opencode.NewClient(
|
||||
option.WithBaseURL(url),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
apiHandler := util.NewAPILogHandler(ctx, httpClient, "tui", slog.LevelDebug)
|
||||
logger := slog.New(apiHandler)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
slog.Debug("TUI launched", "app", appInfoStr, "modes", modesStr)
|
||||
|
||||
go func() {
|
||||
err = clipboard.Init()
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize clipboard", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create main context for the application
|
||||
app_, err := app.New(ctx, version, appInfo, modes, httpClient, model, prompt, mode)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
program := tea.NewProgram(
|
||||
tui.NewModel(app_),
|
||||
tea.WithAltScreen(),
|
||||
tea.WithMouseCellMotion(),
|
||||
)
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
go func() {
|
||||
stream := httpClient.Event.ListStreaming(ctx)
|
||||
for stream.Next() {
|
||||
evt := stream.Current().AsUnion()
|
||||
if _, ok := evt.(opencode.EventListResponseEventStorageWrite); ok {
|
||||
continue
|
||||
}
|
||||
program.Send(evt)
|
||||
}
|
||||
if err := stream.Err(); err != nil {
|
||||
slog.Error("Error streaming events", "error", err)
|
||||
program.Send(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go api.Start(ctx, program, httpClient)
|
||||
|
||||
// Handle signals in a separate goroutine
|
||||
go func() {
|
||||
sig := <-sigChan
|
||||
slog.Info("Received signal, shutting down gracefully", "signal", sig)
|
||||
program.Quit()
|
||||
}()
|
||||
|
||||
// Run the TUI
|
||||
result, err := program.Run()
|
||||
if err != nil {
|
||||
slog.Error("TUI error", "error", err)
|
||||
}
|
||||
|
||||
slog.Info("TUI exited", "result", result)
|
||||
}
|
||||
99
packages/client/tui/go.mod
Normal file
99
packages/client/tui/go.mod
Normal file
@@ -0,0 +1,99 @@
|
||||
module github.com/sst/opencode
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/alecthomas/chroma/v2 v2.18.0
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
|
||||
github.com/charmbracelet/x/ansi v0.9.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||
golang.org/x/image v0.28.0
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/charmbracelet/x/input => ./input
|
||||
github.com/sst/opencode/packages/sdk/go => ../sdk/go
|
||||
)
|
||||
|
||||
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
github.com/charmbracelet/x/input v0.3.7 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.1 // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.127.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sanity-io/litter v1.5.8 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/atombender/go-jsonschema
|
||||
github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
|
||||
)
|
||||
315
packages/client/tui/go.sum
Normal file
315
packages/client/tui/go.sum
Normal file
@@ -0,0 +1,315 @@
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY=
|
||||
github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=
|
||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4/go.mod h1:0wWFRpsgF7vHsCukVZ5LAhZkiR4j875H6KEM2/tFQmA=
|
||||
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
|
||||
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 h1:W6DpZX6zSkZr0iFq6JVh1vItLoxfYtNlaxOJtWp8Kis=
|
||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3/go.mod h1:65HTtKURcv/ict9ZQhr6zT84JqIjMcJbyrZYHHKNfKA=
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 h1:MTSs/nsZNfZPbYk/r9hluK2BtwoqvEYruAujNVwgDv0=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
|
||||
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
|
||||
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8 h1:Tp7nbckbMCwAA/ieVZeeZCp79xXtrPMaWLRk5mhNwrw=
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8/go.mod h1:uagorfAHZsVy6vf0xY6TlQraM4uCILdZ5tKKhl1oToM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
14
packages/client/tui/input/cancelreader_other.go
Normal file
14
packages/client/tui/input/cancelreader_other.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/muesli/cancelreader"
|
||||
)
|
||||
|
||||
func newCancelreader(r io.Reader, _ int) (cancelreader.CancelReader, error) {
|
||||
return cancelreader.NewReader(r) //nolint:wrapcheck
|
||||
}
|
||||
143
packages/client/tui/input/cancelreader_windows.go
Normal file
143
packages/client/tui/input/cancelreader_windows.go
Normal file
@@ -0,0 +1,143 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
xwindows "github.com/charmbracelet/x/windows"
|
||||
"github.com/muesli/cancelreader"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type conInputReader struct {
|
||||
cancelMixin
|
||||
conin windows.Handle
|
||||
originalMode uint32
|
||||
}
|
||||
|
||||
var _ cancelreader.CancelReader = &conInputReader{}
|
||||
|
||||
func newCancelreader(r io.Reader, flags int) (cancelreader.CancelReader, error) {
|
||||
fallback := func(io.Reader) (cancelreader.CancelReader, error) {
|
||||
return cancelreader.NewReader(r)
|
||||
}
|
||||
|
||||
var dummy uint32
|
||||
if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
|
||||
// If data was piped to the standard input, it does not emit events
|
||||
// anymore. We can detect this if the console mode cannot be set anymore,
|
||||
// in this case, we fallback to the default cancelreader implementation.
|
||||
windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
|
||||
return fallback(r)
|
||||
}
|
||||
|
||||
conin, err := windows.GetStdHandle(windows.STD_INPUT_HANDLE)
|
||||
if err != nil {
|
||||
return fallback(r)
|
||||
}
|
||||
|
||||
// Discard any pending input events.
|
||||
if err := xwindows.FlushConsoleInputBuffer(conin); err != nil {
|
||||
return fallback(r)
|
||||
}
|
||||
|
||||
modes := []uint32{
|
||||
windows.ENABLE_WINDOW_INPUT,
|
||||
windows.ENABLE_EXTENDED_FLAGS,
|
||||
}
|
||||
|
||||
// Enabling mouse mode implicitly blocks console text selection. Thus, we
|
||||
// need to enable it only if the mouse mode is requested.
|
||||
// In order to toggle mouse mode, the caller must recreate the reader with
|
||||
// the appropriate flag toggled.
|
||||
if flags&FlagMouseMode != 0 {
|
||||
modes = append(modes, windows.ENABLE_MOUSE_INPUT)
|
||||
}
|
||||
|
||||
originalMode, err := prepareConsole(conin, modes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare console input: %w", err)
|
||||
}
|
||||
|
||||
return &conInputReader{
|
||||
conin: conin,
|
||||
originalMode: originalMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Cancel implements cancelreader.CancelReader.
|
||||
func (r *conInputReader) Cancel() bool {
|
||||
r.setCanceled()
|
||||
|
||||
return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
|
||||
}
|
||||
|
||||
// Close implements cancelreader.CancelReader.
|
||||
func (r *conInputReader) Close() error {
|
||||
if r.originalMode != 0 {
|
||||
err := windows.SetConsoleMode(r.conin, r.originalMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reset console mode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements cancelreader.CancelReader.
|
||||
func (r *conInputReader) Read(data []byte) (int, error) {
|
||||
if r.isCanceled() {
|
||||
return 0, cancelreader.ErrCanceled
|
||||
}
|
||||
|
||||
var n uint32
|
||||
if err := windows.ReadFile(r.conin, data, &n, nil); err != nil {
|
||||
return int(n), fmt.Errorf("read console input: %w", err)
|
||||
}
|
||||
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
|
||||
err = windows.GetConsoleMode(input, &originalMode)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get console mode: %w", err)
|
||||
}
|
||||
|
||||
var newMode uint32
|
||||
for _, mode := range modes {
|
||||
newMode |= mode
|
||||
}
|
||||
|
||||
err = windows.SetConsoleMode(input, newMode)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("set console mode: %w", err)
|
||||
}
|
||||
|
||||
return originalMode, nil
|
||||
}
|
||||
|
||||
// cancelMixin represents a goroutine-safe cancelation status.
|
||||
type cancelMixin struct {
|
||||
unsafeCanceled bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (c *cancelMixin) setCanceled() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.unsafeCanceled = true
|
||||
}
|
||||
|
||||
func (c *cancelMixin) isCanceled() bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.unsafeCanceled
|
||||
}
|
||||
25
packages/client/tui/input/clipboard.go
Normal file
25
packages/client/tui/input/clipboard.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package input
|
||||
|
||||
import "github.com/charmbracelet/x/ansi"
|
||||
|
||||
// ClipboardSelection represents a clipboard selection. The most common
|
||||
// clipboard selections are "system" and "primary" and selections.
|
||||
type ClipboardSelection = byte
|
||||
|
||||
// Clipboard selections.
|
||||
const (
|
||||
SystemClipboard ClipboardSelection = ansi.SystemClipboard
|
||||
PrimaryClipboard ClipboardSelection = ansi.PrimaryClipboard
|
||||
)
|
||||
|
||||
// ClipboardEvent is a clipboard read message event. This message is emitted when
|
||||
// a terminal receives an OSC52 clipboard read message event.
|
||||
type ClipboardEvent struct {
|
||||
Content string
|
||||
Selection ClipboardSelection
|
||||
}
|
||||
|
||||
// String returns the string representation of the clipboard message.
|
||||
func (e ClipboardEvent) String() string {
|
||||
return e.Content
|
||||
}
|
||||
136
packages/client/tui/input/color.go
Normal file
136
packages/client/tui/input/color.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
)
|
||||
|
||||
// ForegroundColorEvent represents a foreground color event. This event is
|
||||
// emitted when the terminal requests the terminal foreground color using
|
||||
// [ansi.RequestForegroundColor].
|
||||
type ForegroundColorEvent struct{ color.Color }
|
||||
|
||||
// String returns the hex representation of the color.
|
||||
func (e ForegroundColorEvent) String() string {
|
||||
return colorToHex(e.Color)
|
||||
}
|
||||
|
||||
// IsDark returns whether the color is dark.
|
||||
func (e ForegroundColorEvent) IsDark() bool {
|
||||
return isDarkColor(e.Color)
|
||||
}
|
||||
|
||||
// BackgroundColorEvent represents a background color event. This event is
|
||||
// emitted when the terminal requests the terminal background color using
|
||||
// [ansi.RequestBackgroundColor].
|
||||
type BackgroundColorEvent struct{ color.Color }
|
||||
|
||||
// String returns the hex representation of the color.
|
||||
func (e BackgroundColorEvent) String() string {
|
||||
return colorToHex(e)
|
||||
}
|
||||
|
||||
// IsDark returns whether the color is dark.
|
||||
func (e BackgroundColorEvent) IsDark() bool {
|
||||
return isDarkColor(e.Color)
|
||||
}
|
||||
|
||||
// CursorColorEvent represents a cursor color change event. This event is
|
||||
// emitted when the program requests the terminal cursor color using
|
||||
// [ansi.RequestCursorColor].
|
||||
type CursorColorEvent struct{ color.Color }
|
||||
|
||||
// String returns the hex representation of the color.
|
||||
func (e CursorColorEvent) String() string {
|
||||
return colorToHex(e)
|
||||
}
|
||||
|
||||
// IsDark returns whether the color is dark.
|
||||
func (e CursorColorEvent) IsDark() bool {
|
||||
return isDarkColor(e)
|
||||
}
|
||||
|
||||
type shiftable interface {
|
||||
~uint | ~uint16 | ~uint32 | ~uint64
|
||||
}
|
||||
|
||||
func shift[T shiftable](x T) T {
|
||||
if x > 0xff {
|
||||
x >>= 8
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func colorToHex(c color.Color) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
r, g, b, _ := c.RGBA()
|
||||
return fmt.Sprintf("#%02x%02x%02x", shift(r), shift(g), shift(b))
|
||||
}
|
||||
|
||||
func getMaxMin(a, b, c float64) (ma, mi float64) {
|
||||
if a > b {
|
||||
ma = a
|
||||
mi = b
|
||||
} else {
|
||||
ma = b
|
||||
mi = a
|
||||
}
|
||||
if c > ma {
|
||||
ma = c
|
||||
} else if c < mi {
|
||||
mi = c
|
||||
}
|
||||
return ma, mi
|
||||
}
|
||||
|
||||
func round(x float64) float64 {
|
||||
return math.Round(x*1000) / 1000
|
||||
}
|
||||
|
||||
// rgbToHSL converts an RGB triple to an HSL triple.
|
||||
func rgbToHSL(r, g, b uint8) (h, s, l float64) {
|
||||
// convert uint32 pre-multiplied value to uint8
|
||||
// The r,g,b values are divided by 255 to change the range from 0..255 to 0..1:
|
||||
Rnot := float64(r) / 255
|
||||
Gnot := float64(g) / 255
|
||||
Bnot := float64(b) / 255
|
||||
Cmax, Cmin := getMaxMin(Rnot, Gnot, Bnot)
|
||||
Δ := Cmax - Cmin
|
||||
// Lightness calculation:
|
||||
l = (Cmax + Cmin) / 2
|
||||
// Hue and Saturation Calculation:
|
||||
if Δ == 0 {
|
||||
h = 0
|
||||
s = 0
|
||||
} else {
|
||||
switch Cmax {
|
||||
case Rnot:
|
||||
h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6))
|
||||
case Gnot:
|
||||
h = 60 * (((Bnot - Rnot) / Δ) + 2)
|
||||
case Bnot:
|
||||
h = 60 * (((Rnot - Gnot) / Δ) + 4)
|
||||
}
|
||||
if h < 0 {
|
||||
h += 360
|
||||
}
|
||||
|
||||
s = Δ / (1 - math.Abs((2*l)-1))
|
||||
}
|
||||
|
||||
return h, round(s), round(l)
|
||||
}
|
||||
|
||||
// isDarkColor returns whether the given color is dark.
|
||||
func isDarkColor(c color.Color) bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
r, g, b, _ := c.RGBA()
|
||||
_, _, l := rgbToHSL(uint8(r>>8), uint8(g>>8), uint8(b>>8)) //nolint:gosec
|
||||
return l < 0.5
|
||||
}
|
||||
7
packages/client/tui/input/cursor.go
Normal file
7
packages/client/tui/input/cursor.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package input
|
||||
|
||||
import "image"
|
||||
|
||||
// CursorPositionEvent represents a cursor position event. Where X is the
|
||||
// zero-based column and Y is the zero-based row.
|
||||
type CursorPositionEvent image.Point
|
||||
18
packages/client/tui/input/da1.go
Normal file
18
packages/client/tui/input/da1.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package input
|
||||
|
||||
import "github.com/charmbracelet/x/ansi"
|
||||
|
||||
// PrimaryDeviceAttributesEvent is an event that represents the terminal
|
||||
// primary device attributes.
|
||||
type PrimaryDeviceAttributesEvent []int
|
||||
|
||||
func parsePrimaryDevAttrs(params ansi.Params) Event {
|
||||
// Primary Device Attributes
|
||||
da1 := make(PrimaryDeviceAttributesEvent, len(params))
|
||||
for i, p := range params {
|
||||
if !p.HasMore() {
|
||||
da1[i] = p.Param(0)
|
||||
}
|
||||
}
|
||||
return da1
|
||||
}
|
||||
6
packages/client/tui/input/doc.go
Normal file
6
packages/client/tui/input/doc.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Package input provides a set of utilities for handling input events in a
|
||||
// terminal environment. It includes support for reading input events, parsing
|
||||
// escape sequences, and handling clipboard events.
|
||||
// The package is designed to work with various terminal types and supports
|
||||
// customization through flags and options.
|
||||
package input
|
||||
192
packages/client/tui/input/driver.go
Normal file
192
packages/client/tui/input/driver.go
Normal file
@@ -0,0 +1,192 @@
|
||||
//nolint:unused,revive,nolintlint
|
||||
package input
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/muesli/cancelreader"
|
||||
)
|
||||
|
||||
// Logger is a simple logger interface.
|
||||
type Logger interface {
|
||||
Printf(format string, v ...any)
|
||||
}
|
||||
|
||||
// win32InputState is a state machine for parsing key events from the Windows
|
||||
// Console API into escape sequences and utf8 runes, and keeps track of the last
|
||||
// control key state to determine modifier key changes. It also keeps track of
|
||||
// the last mouse button state and window size changes to determine which mouse
|
||||
// buttons were released and to prevent multiple size events from firing.
|
||||
type win32InputState struct {
|
||||
ansiBuf [256]byte
|
||||
ansiIdx int
|
||||
utf16Buf [2]rune
|
||||
utf16Half bool
|
||||
lastCks uint32 // the last control key state for the previous event
|
||||
lastMouseBtns uint32 // the last mouse button state for the previous event
|
||||
lastWinsizeX, lastWinsizeY int16 // the last window size for the previous event to prevent multiple size events from firing
|
||||
}
|
||||
|
||||
// Reader represents an input event reader. It reads input events and parses
|
||||
// escape sequences from the terminal input buffer and translates them into
|
||||
// human‑readable events.
|
||||
type Reader struct {
|
||||
rd cancelreader.CancelReader
|
||||
table map[string]Key // table is a lookup table for key sequences.
|
||||
term string // $TERM
|
||||
paste []byte // bracketed paste buffer; nil when disabled
|
||||
buf [256]byte // read buffer
|
||||
partialSeq []byte // holds incomplete escape sequences
|
||||
keyState win32InputState
|
||||
parser Parser
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// NewReader returns a new input event reader.
|
||||
func NewReader(r io.Reader, termType string, flags int) (*Reader, error) {
|
||||
d := new(Reader)
|
||||
cr, err := newCancelreader(r, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.rd = cr
|
||||
d.table = buildKeysTable(flags, termType)
|
||||
d.term = termType
|
||||
d.parser.flags = flags
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// SetLogger sets a logger for the reader.
|
||||
func (d *Reader) SetLogger(l Logger) { d.logger = l }
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (d *Reader) Read(p []byte) (int, error) { return d.rd.Read(p) }
|
||||
|
||||
// Cancel cancels the underlying reader.
|
||||
func (d *Reader) Cancel() bool { return d.rd.Cancel() }
|
||||
|
||||
// Close closes the underlying reader.
|
||||
func (d *Reader) Close() error { return d.rd.Close() }
|
||||
|
||||
func (d *Reader) readEvents() ([]Event, error) {
|
||||
nb, err := d.rd.Read(d.buf[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events []Event
|
||||
|
||||
// Combine any partial sequence from previous read with new data.
|
||||
var buf []byte
|
||||
if len(d.partialSeq) > 0 {
|
||||
buf = make([]byte, len(d.partialSeq)+nb)
|
||||
copy(buf, d.partialSeq)
|
||||
copy(buf[len(d.partialSeq):], d.buf[:nb])
|
||||
d.partialSeq = nil
|
||||
} else {
|
||||
buf = d.buf[:nb]
|
||||
}
|
||||
|
||||
// Fast path: direct lookup for simple escape sequences.
|
||||
if bytes.HasPrefix(buf, []byte{0x1b}) {
|
||||
if k, ok := d.table[string(buf)]; ok {
|
||||
if d.logger != nil {
|
||||
d.logger.Printf("input: %q", buf)
|
||||
}
|
||||
events = append(events, KeyPressEvent(k))
|
||||
return events, nil
|
||||
}
|
||||
}
|
||||
|
||||
var i int
|
||||
for i < len(buf) {
|
||||
consumed, ev := d.parser.parseSequence(buf[i:])
|
||||
if d.logger != nil && consumed > 0 {
|
||||
d.logger.Printf("input: %q", buf[i:i+consumed])
|
||||
}
|
||||
|
||||
// Incomplete sequence – store remainder and exit.
|
||||
if consumed == 0 && ev == nil {
|
||||
rem := len(buf) - i
|
||||
if rem > 0 {
|
||||
d.partialSeq = make([]byte, rem)
|
||||
copy(d.partialSeq, buf[i:])
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Handle bracketed paste specially so we don’t emit a paste event for
|
||||
// every byte.
|
||||
if d.paste != nil {
|
||||
if _, ok := ev.(PasteEndEvent); !ok {
|
||||
d.paste = append(d.paste, buf[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch ev.(type) {
|
||||
case PasteStartEvent:
|
||||
d.paste = []byte{}
|
||||
case PasteEndEvent:
|
||||
var paste []rune
|
||||
for len(d.paste) > 0 {
|
||||
r, w := utf8.DecodeRune(d.paste)
|
||||
if r != utf8.RuneError {
|
||||
paste = append(paste, r)
|
||||
}
|
||||
d.paste = d.paste[w:]
|
||||
}
|
||||
d.paste = nil
|
||||
events = append(events, PasteEvent(paste))
|
||||
case nil:
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if mevs, ok := ev.(MultiEvent); ok {
|
||||
events = append(events, []Event(mevs)...)
|
||||
} else {
|
||||
events = append(events, ev)
|
||||
}
|
||||
i += consumed
|
||||
}
|
||||
|
||||
// Collapse bursts of wheel/motion events into a single event each.
|
||||
events = coalesceMouseEvents(events)
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// coalesceMouseEvents reduces the volume of MouseWheelEvent and MouseMotionEvent
|
||||
// objects that arrive in rapid succession by keeping only the most recent
|
||||
// event in each contiguous run.
|
||||
func coalesceMouseEvents(in []Event) []Event {
|
||||
if len(in) < 2 {
|
||||
return in
|
||||
}
|
||||
|
||||
out := make([]Event, 0, len(in))
|
||||
for _, ev := range in {
|
||||
switch ev.(type) {
|
||||
case MouseWheelEvent:
|
||||
if len(out) > 0 {
|
||||
if _, ok := out[len(out)-1].(MouseWheelEvent); ok {
|
||||
out[len(out)-1] = ev // replace previous wheel event
|
||||
continue
|
||||
}
|
||||
}
|
||||
case MouseMotionEvent:
|
||||
if len(out) > 0 {
|
||||
if _, ok := out[len(out)-1].(MouseMotionEvent); ok {
|
||||
out[len(out)-1] = ev // replace previous motion event
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, ev)
|
||||
}
|
||||
return out
|
||||
}
|
||||
17
packages/client/tui/input/driver_other.go
Normal file
17
packages/client/tui/input/driver_other.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package input
|
||||
|
||||
// ReadEvents reads input events from the terminal.
|
||||
//
|
||||
// It reads the events available in the input buffer and returns them.
|
||||
func (d *Reader) ReadEvents() ([]Event, error) {
|
||||
return d.readEvents()
|
||||
}
|
||||
|
||||
// parseWin32InputKeyEvent parses a Win32 input key events. This function is
|
||||
// only available on Windows.
|
||||
func (p *Parser) parseWin32InputKeyEvent(*win32InputState, uint16, uint16, rune, bool, uint32, uint16) Event {
|
||||
return nil
|
||||
}
|
||||
25
packages/client/tui/input/driver_test.go
Normal file
25
packages/client/tui/input/driver_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkDriver(b *testing.B) {
|
||||
input := "\x1b\x1b[Ztest\x00\x1b]10;1234/1234/1234\x07\x1b[27;2;27~"
|
||||
rdr := strings.NewReader(input)
|
||||
drv, err := NewReader(rdr, "dumb", 0)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create driver: %v", err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rdr.Reset(input)
|
||||
if _, err := drv.ReadEvents(); err != nil && err != io.EOF {
|
||||
b.Errorf("error reading input: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
642
packages/client/tui/input/driver_windows.go
Normal file
642
packages/client/tui/input/driver_windows.go
Normal file
@@ -0,0 +1,642 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
xwindows "github.com/charmbracelet/x/windows"
|
||||
"github.com/muesli/cancelreader"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// ReadEvents reads input events from the terminal.
|
||||
//
|
||||
// It reads the events available in the input buffer and returns them.
|
||||
func (d *Reader) ReadEvents() ([]Event, error) {
|
||||
events, err := d.handleConInput()
|
||||
if errors.Is(err, errNotConInputReader) {
|
||||
return d.readEvents()
|
||||
}
|
||||
return events, err
|
||||
}
|
||||
|
||||
var errNotConInputReader = fmt.Errorf("handleConInput: not a conInputReader")
|
||||
|
||||
func (d *Reader) handleConInput() ([]Event, error) {
|
||||
cc, ok := d.rd.(*conInputReader)
|
||||
if !ok {
|
||||
return nil, errNotConInputReader
|
||||
}
|
||||
|
||||
var (
|
||||
events []xwindows.InputRecord
|
||||
err error
|
||||
)
|
||||
for {
|
||||
// Peek up to 256 events, this is to allow for sequences events reported as
|
||||
// key events.
|
||||
events, err = peekNConsoleInputs(cc.conin, 256)
|
||||
if cc.isCanceled() {
|
||||
return nil, cancelreader.ErrCanceled
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peek coninput events: %w", err)
|
||||
}
|
||||
if len(events) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Sleep for a bit to avoid busy waiting.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
events, err = readNConsoleInputs(cc.conin, uint32(len(events)))
|
||||
if cc.isCanceled() {
|
||||
return nil, cancelreader.ErrCanceled
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read coninput events: %w", err)
|
||||
}
|
||||
|
||||
var evs []Event
|
||||
for _, event := range events {
|
||||
if e := d.parser.parseConInputEvent(event, &d.keyState); e != nil {
|
||||
if multi, ok := e.(MultiEvent); ok {
|
||||
evs = append(evs, multi...)
|
||||
} else {
|
||||
evs = append(evs, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return evs, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseConInputEvent(event xwindows.InputRecord, keyState *win32InputState) Event {
|
||||
switch event.EventType {
|
||||
case xwindows.KEY_EVENT:
|
||||
kevent := event.KeyEvent()
|
||||
return p.parseWin32InputKeyEvent(keyState, kevent.VirtualKeyCode, kevent.VirtualScanCode,
|
||||
kevent.Char, kevent.KeyDown, kevent.ControlKeyState, kevent.RepeatCount)
|
||||
|
||||
case xwindows.WINDOW_BUFFER_SIZE_EVENT:
|
||||
wevent := event.WindowBufferSizeEvent()
|
||||
if wevent.Size.X != keyState.lastWinsizeX || wevent.Size.Y != keyState.lastWinsizeY {
|
||||
keyState.lastWinsizeX, keyState.lastWinsizeY = wevent.Size.X, wevent.Size.Y
|
||||
return WindowSizeEvent{
|
||||
Width: int(wevent.Size.X),
|
||||
Height: int(wevent.Size.Y),
|
||||
}
|
||||
}
|
||||
case xwindows.MOUSE_EVENT:
|
||||
mevent := event.MouseEvent()
|
||||
Event := mouseEvent(keyState.lastMouseBtns, mevent)
|
||||
keyState.lastMouseBtns = mevent.ButtonState
|
||||
return Event
|
||||
case xwindows.FOCUS_EVENT:
|
||||
fevent := event.FocusEvent()
|
||||
if fevent.SetFocus {
|
||||
return FocusEvent{}
|
||||
}
|
||||
return BlurEvent{}
|
||||
case xwindows.MENU_EVENT:
|
||||
// ignore
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mouseEventButton(p, s uint32) (MouseButton, bool) {
|
||||
var isRelease bool
|
||||
button := MouseNone
|
||||
btn := p ^ s
|
||||
if btn&s == 0 {
|
||||
isRelease = true
|
||||
}
|
||||
|
||||
if btn == 0 {
|
||||
switch {
|
||||
case s&xwindows.FROM_LEFT_1ST_BUTTON_PRESSED > 0:
|
||||
button = MouseLeft
|
||||
case s&xwindows.FROM_LEFT_2ND_BUTTON_PRESSED > 0:
|
||||
button = MouseMiddle
|
||||
case s&xwindows.RIGHTMOST_BUTTON_PRESSED > 0:
|
||||
button = MouseRight
|
||||
case s&xwindows.FROM_LEFT_3RD_BUTTON_PRESSED > 0:
|
||||
button = MouseBackward
|
||||
case s&xwindows.FROM_LEFT_4TH_BUTTON_PRESSED > 0:
|
||||
button = MouseForward
|
||||
}
|
||||
return button, isRelease
|
||||
}
|
||||
|
||||
switch btn {
|
||||
case xwindows.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
|
||||
button = MouseLeft
|
||||
case xwindows.RIGHTMOST_BUTTON_PRESSED: // right button
|
||||
button = MouseRight
|
||||
case xwindows.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
|
||||
button = MouseMiddle
|
||||
case xwindows.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
|
||||
button = MouseBackward
|
||||
case xwindows.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
|
||||
button = MouseForward
|
||||
}
|
||||
|
||||
return button, isRelease
|
||||
}
|
||||
|
||||
func mouseEvent(p uint32, e xwindows.MouseEventRecord) (ev Event) {
|
||||
var mod KeyMod
|
||||
var isRelease bool
|
||||
if e.ControlKeyState&(xwindows.LEFT_ALT_PRESSED|xwindows.RIGHT_ALT_PRESSED) != 0 {
|
||||
mod |= ModAlt
|
||||
}
|
||||
if e.ControlKeyState&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_CTRL_PRESSED) != 0 {
|
||||
mod |= ModCtrl
|
||||
}
|
||||
if e.ControlKeyState&(xwindows.SHIFT_PRESSED) != 0 {
|
||||
mod |= ModShift
|
||||
}
|
||||
|
||||
m := Mouse{
|
||||
X: int(e.MousePositon.X),
|
||||
Y: int(e.MousePositon.Y),
|
||||
Mod: mod,
|
||||
}
|
||||
|
||||
wheelDirection := int16(highWord(e.ButtonState)) //nolint:gosec
|
||||
switch e.EventFlags {
|
||||
case 0, xwindows.DOUBLE_CLICK:
|
||||
m.Button, isRelease = mouseEventButton(p, e.ButtonState)
|
||||
case xwindows.MOUSE_WHEELED:
|
||||
if wheelDirection > 0 {
|
||||
m.Button = MouseWheelUp
|
||||
} else {
|
||||
m.Button = MouseWheelDown
|
||||
}
|
||||
case xwindows.MOUSE_HWHEELED:
|
||||
if wheelDirection > 0 {
|
||||
m.Button = MouseWheelRight
|
||||
} else {
|
||||
m.Button = MouseWheelLeft
|
||||
}
|
||||
case xwindows.MOUSE_MOVED:
|
||||
m.Button, _ = mouseEventButton(p, e.ButtonState)
|
||||
return MouseMotionEvent(m)
|
||||
}
|
||||
|
||||
if isWheel(m.Button) {
|
||||
return MouseWheelEvent(m)
|
||||
} else if isRelease {
|
||||
return MouseReleaseEvent(m)
|
||||
}
|
||||
|
||||
return MouseClickEvent(m)
|
||||
}
|
||||
|
||||
func highWord(data uint32) uint16 {
|
||||
return uint16((data & 0xFFFF0000) >> 16) //nolint:gosec
|
||||
}
|
||||
|
||||
func readNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) {
|
||||
if maxEvents == 0 {
|
||||
return nil, fmt.Errorf("maxEvents cannot be zero")
|
||||
}
|
||||
|
||||
records := make([]xwindows.InputRecord, maxEvents)
|
||||
n, err := readConsoleInput(console, records)
|
||||
return records[:n], err
|
||||
}
|
||||
|
||||
func readConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) {
|
||||
if len(inputRecords) == 0 {
|
||||
return 0, fmt.Errorf("size of input record buffer cannot be zero")
|
||||
}
|
||||
|
||||
var read uint32
|
||||
|
||||
err := xwindows.ReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec
|
||||
|
||||
return read, err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
func peekConsoleInput(console windows.Handle, inputRecords []xwindows.InputRecord) (uint32, error) {
|
||||
if len(inputRecords) == 0 {
|
||||
return 0, fmt.Errorf("size of input record buffer cannot be zero")
|
||||
}
|
||||
|
||||
var read uint32
|
||||
|
||||
err := xwindows.PeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) //nolint:gosec
|
||||
|
||||
return read, err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
func peekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]xwindows.InputRecord, error) {
|
||||
if maxEvents == 0 {
|
||||
return nil, fmt.Errorf("maxEvents cannot be zero")
|
||||
}
|
||||
|
||||
records := make([]xwindows.InputRecord, maxEvents)
|
||||
n, err := peekConsoleInput(console, records)
|
||||
return records[:n], err
|
||||
}
|
||||
|
||||
// parseWin32InputKeyEvent parses a single key event from either the Windows
|
||||
// Console API or win32-input-mode events. When state is nil, it means this is
|
||||
// an event from win32-input-mode. Otherwise, it's a key event from the Windows
|
||||
// Console API and needs a state to decode ANSI escape sequences and utf16
|
||||
// runes.
|
||||
func (p *Parser) parseWin32InputKeyEvent(state *win32InputState, vkc uint16, _ uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) (event Event) {
|
||||
defer func() {
|
||||
// Respect the repeat count.
|
||||
if repeatCount > 1 {
|
||||
var multi MultiEvent
|
||||
for i := 0; i < int(repeatCount); i++ {
|
||||
multi = append(multi, event)
|
||||
}
|
||||
event = multi
|
||||
}
|
||||
}()
|
||||
if state != nil {
|
||||
defer func() {
|
||||
state.lastCks = cks
|
||||
}()
|
||||
}
|
||||
|
||||
var utf8Buf [utf8.UTFMax]byte
|
||||
var key Key
|
||||
if state != nil && state.utf16Half {
|
||||
state.utf16Half = false
|
||||
state.utf16Buf[1] = r
|
||||
codepoint := utf16.DecodeRune(state.utf16Buf[0], state.utf16Buf[1])
|
||||
rw := utf8.EncodeRune(utf8Buf[:], codepoint)
|
||||
r, _ = utf8.DecodeRune(utf8Buf[:rw])
|
||||
key.Code = r
|
||||
key.Text = string(r)
|
||||
key.Mod = translateControlKeyState(cks)
|
||||
key = ensureKeyCase(key, cks)
|
||||
if keyDown {
|
||||
return KeyPressEvent(key)
|
||||
}
|
||||
return KeyReleaseEvent(key)
|
||||
}
|
||||
|
||||
var baseCode rune
|
||||
switch {
|
||||
case vkc == 0:
|
||||
// Zero means this event is either an escape code or a unicode
|
||||
// codepoint.
|
||||
if state != nil && state.ansiIdx == 0 && r != ansi.ESC {
|
||||
// This is a unicode codepoint.
|
||||
baseCode = r
|
||||
break
|
||||
}
|
||||
|
||||
if state != nil {
|
||||
// Collect ANSI escape code.
|
||||
state.ansiBuf[state.ansiIdx] = byte(r)
|
||||
state.ansiIdx++
|
||||
if state.ansiIdx <= 2 {
|
||||
// We haven't received enough bytes to determine if this is an
|
||||
// ANSI escape code.
|
||||
return nil
|
||||
}
|
||||
if r == ansi.ESC {
|
||||
// We're expecting a closing String Terminator [ansi.ST].
|
||||
return nil
|
||||
}
|
||||
|
||||
n, event := p.parseSequence(state.ansiBuf[:state.ansiIdx])
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, ok := event.(UnknownEvent); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.ansiIdx = 0
|
||||
return event
|
||||
}
|
||||
case vkc == xwindows.VK_BACK:
|
||||
baseCode = KeyBackspace
|
||||
case vkc == xwindows.VK_TAB:
|
||||
baseCode = KeyTab
|
||||
case vkc == xwindows.VK_RETURN:
|
||||
baseCode = KeyEnter
|
||||
case vkc == xwindows.VK_SHIFT:
|
||||
//nolint:nestif
|
||||
if cks&xwindows.SHIFT_PRESSED != 0 {
|
||||
if cks&xwindows.ENHANCED_KEY != 0 {
|
||||
baseCode = KeyRightShift
|
||||
} else {
|
||||
baseCode = KeyLeftShift
|
||||
}
|
||||
} else if state != nil {
|
||||
if state.lastCks&xwindows.SHIFT_PRESSED != 0 {
|
||||
if state.lastCks&xwindows.ENHANCED_KEY != 0 {
|
||||
baseCode = KeyRightShift
|
||||
} else {
|
||||
baseCode = KeyLeftShift
|
||||
}
|
||||
}
|
||||
}
|
||||
case vkc == xwindows.VK_CONTROL:
|
||||
if cks&xwindows.LEFT_CTRL_PRESSED != 0 {
|
||||
baseCode = KeyLeftCtrl
|
||||
} else if cks&xwindows.RIGHT_CTRL_PRESSED != 0 {
|
||||
baseCode = KeyRightCtrl
|
||||
} else if state != nil {
|
||||
if state.lastCks&xwindows.LEFT_CTRL_PRESSED != 0 {
|
||||
baseCode = KeyLeftCtrl
|
||||
} else if state.lastCks&xwindows.RIGHT_CTRL_PRESSED != 0 {
|
||||
baseCode = KeyRightCtrl
|
||||
}
|
||||
}
|
||||
case vkc == xwindows.VK_MENU:
|
||||
if cks&xwindows.LEFT_ALT_PRESSED != 0 {
|
||||
baseCode = KeyLeftAlt
|
||||
} else if cks&xwindows.RIGHT_ALT_PRESSED != 0 {
|
||||
baseCode = KeyRightAlt
|
||||
} else if state != nil {
|
||||
if state.lastCks&xwindows.LEFT_ALT_PRESSED != 0 {
|
||||
baseCode = KeyLeftAlt
|
||||
} else if state.lastCks&xwindows.RIGHT_ALT_PRESSED != 0 {
|
||||
baseCode = KeyRightAlt
|
||||
}
|
||||
}
|
||||
case vkc == xwindows.VK_PAUSE:
|
||||
baseCode = KeyPause
|
||||
case vkc == xwindows.VK_CAPITAL:
|
||||
baseCode = KeyCapsLock
|
||||
case vkc == xwindows.VK_ESCAPE:
|
||||
baseCode = KeyEscape
|
||||
case vkc == xwindows.VK_SPACE:
|
||||
baseCode = KeySpace
|
||||
case vkc == xwindows.VK_PRIOR:
|
||||
baseCode = KeyPgUp
|
||||
case vkc == xwindows.VK_NEXT:
|
||||
baseCode = KeyPgDown
|
||||
case vkc == xwindows.VK_END:
|
||||
baseCode = KeyEnd
|
||||
case vkc == xwindows.VK_HOME:
|
||||
baseCode = KeyHome
|
||||
case vkc == xwindows.VK_LEFT:
|
||||
baseCode = KeyLeft
|
||||
case vkc == xwindows.VK_UP:
|
||||
baseCode = KeyUp
|
||||
case vkc == xwindows.VK_RIGHT:
|
||||
baseCode = KeyRight
|
||||
case vkc == xwindows.VK_DOWN:
|
||||
baseCode = KeyDown
|
||||
case vkc == xwindows.VK_SELECT:
|
||||
baseCode = KeySelect
|
||||
case vkc == xwindows.VK_SNAPSHOT:
|
||||
baseCode = KeyPrintScreen
|
||||
case vkc == xwindows.VK_INSERT:
|
||||
baseCode = KeyInsert
|
||||
case vkc == xwindows.VK_DELETE:
|
||||
baseCode = KeyDelete
|
||||
case vkc >= '0' && vkc <= '9':
|
||||
baseCode = rune(vkc)
|
||||
case vkc >= 'A' && vkc <= 'Z':
|
||||
// Convert to lowercase.
|
||||
baseCode = rune(vkc) + 32
|
||||
case vkc == xwindows.VK_LWIN:
|
||||
baseCode = KeyLeftSuper
|
||||
case vkc == xwindows.VK_RWIN:
|
||||
baseCode = KeyRightSuper
|
||||
case vkc == xwindows.VK_APPS:
|
||||
baseCode = KeyMenu
|
||||
case vkc >= xwindows.VK_NUMPAD0 && vkc <= xwindows.VK_NUMPAD9:
|
||||
baseCode = rune(vkc-xwindows.VK_NUMPAD0) + KeyKp0
|
||||
case vkc == xwindows.VK_MULTIPLY:
|
||||
baseCode = KeyKpMultiply
|
||||
case vkc == xwindows.VK_ADD:
|
||||
baseCode = KeyKpPlus
|
||||
case vkc == xwindows.VK_SEPARATOR:
|
||||
baseCode = KeyKpComma
|
||||
case vkc == xwindows.VK_SUBTRACT:
|
||||
baseCode = KeyKpMinus
|
||||
case vkc == xwindows.VK_DECIMAL:
|
||||
baseCode = KeyKpDecimal
|
||||
case vkc == xwindows.VK_DIVIDE:
|
||||
baseCode = KeyKpDivide
|
||||
case vkc >= xwindows.VK_F1 && vkc <= xwindows.VK_F24:
|
||||
baseCode = rune(vkc-xwindows.VK_F1) + KeyF1
|
||||
case vkc == xwindows.VK_NUMLOCK:
|
||||
baseCode = KeyNumLock
|
||||
case vkc == xwindows.VK_SCROLL:
|
||||
baseCode = KeyScrollLock
|
||||
case vkc == xwindows.VK_LSHIFT:
|
||||
baseCode = KeyLeftShift
|
||||
case vkc == xwindows.VK_RSHIFT:
|
||||
baseCode = KeyRightShift
|
||||
case vkc == xwindows.VK_LCONTROL:
|
||||
baseCode = KeyLeftCtrl
|
||||
case vkc == xwindows.VK_RCONTROL:
|
||||
baseCode = KeyRightCtrl
|
||||
case vkc == xwindows.VK_LMENU:
|
||||
baseCode = KeyLeftAlt
|
||||
case vkc == xwindows.VK_RMENU:
|
||||
baseCode = KeyRightAlt
|
||||
case vkc == xwindows.VK_VOLUME_MUTE:
|
||||
baseCode = KeyMute
|
||||
case vkc == xwindows.VK_VOLUME_DOWN:
|
||||
baseCode = KeyLowerVol
|
||||
case vkc == xwindows.VK_VOLUME_UP:
|
||||
baseCode = KeyRaiseVol
|
||||
case vkc == xwindows.VK_MEDIA_NEXT_TRACK:
|
||||
baseCode = KeyMediaNext
|
||||
case vkc == xwindows.VK_MEDIA_PREV_TRACK:
|
||||
baseCode = KeyMediaPrev
|
||||
case vkc == xwindows.VK_MEDIA_STOP:
|
||||
baseCode = KeyMediaStop
|
||||
case vkc == xwindows.VK_MEDIA_PLAY_PAUSE:
|
||||
baseCode = KeyMediaPlayPause
|
||||
case vkc == xwindows.VK_OEM_1, vkc == xwindows.VK_OEM_PLUS, vkc == xwindows.VK_OEM_COMMA,
|
||||
vkc == xwindows.VK_OEM_MINUS, vkc == xwindows.VK_OEM_PERIOD, vkc == xwindows.VK_OEM_2,
|
||||
vkc == xwindows.VK_OEM_3, vkc == xwindows.VK_OEM_4, vkc == xwindows.VK_OEM_5,
|
||||
vkc == xwindows.VK_OEM_6, vkc == xwindows.VK_OEM_7:
|
||||
// Use the actual character provided by Windows for current keyboard layout
|
||||
// instead of hardcoded US layout mappings
|
||||
if !unicode.IsControl(r) && unicode.IsPrint(r) {
|
||||
baseCode = r
|
||||
} else {
|
||||
// Fallback to original hardcoded mappings for non-printable cases
|
||||
switch vkc {
|
||||
case xwindows.VK_OEM_1:
|
||||
baseCode = ';'
|
||||
case xwindows.VK_OEM_PLUS:
|
||||
baseCode = '+'
|
||||
case xwindows.VK_OEM_COMMA:
|
||||
baseCode = ','
|
||||
case xwindows.VK_OEM_MINUS:
|
||||
baseCode = '-'
|
||||
case xwindows.VK_OEM_PERIOD:
|
||||
baseCode = '.'
|
||||
case xwindows.VK_OEM_2:
|
||||
baseCode = '/'
|
||||
case xwindows.VK_OEM_3:
|
||||
baseCode = '`'
|
||||
case xwindows.VK_OEM_4:
|
||||
baseCode = '['
|
||||
case xwindows.VK_OEM_5:
|
||||
baseCode = '\\'
|
||||
case xwindows.VK_OEM_6:
|
||||
baseCode = ']'
|
||||
case xwindows.VK_OEM_7:
|
||||
baseCode = '\''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if utf16.IsSurrogate(r) {
|
||||
if state != nil {
|
||||
state.utf16Buf[0] = r
|
||||
state.utf16Half = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AltGr is left ctrl + right alt. On non-US keyboards, this is used to type
|
||||
// special characters and produce printable events.
|
||||
// XXX: Should this be a KeyMod?
|
||||
altGr := cks&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED) == xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_ALT_PRESSED
|
||||
|
||||
// FIXED: Remove numlock and scroll lock states when checking for printable text
|
||||
// These lock states shouldn't affect normal typing
|
||||
cksForTextCheck := cks &^ (xwindows.NUMLOCK_ON | xwindows.SCROLLLOCK_ON)
|
||||
|
||||
var text string
|
||||
keyCode := baseCode
|
||||
if !unicode.IsControl(r) {
|
||||
rw := utf8.EncodeRune(utf8Buf[:], r)
|
||||
keyCode, _ = utf8.DecodeRune(utf8Buf[:rw])
|
||||
if unicode.IsPrint(keyCode) && (cksForTextCheck == 0 ||
|
||||
cksForTextCheck == xwindows.SHIFT_PRESSED ||
|
||||
cksForTextCheck == xwindows.CAPSLOCK_ON ||
|
||||
altGr) {
|
||||
// If the control key state is 0, shift is pressed, or caps lock
|
||||
// then the key event is a printable event i.e. [text] is not empty.
|
||||
text = string(keyCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: numeric keypad divide should produce "/" text on all layouts (fix french keyboard layout)
|
||||
if baseCode == KeyKpDivide {
|
||||
text = "/"
|
||||
}
|
||||
|
||||
key.Code = keyCode
|
||||
key.Text = text
|
||||
key.Mod = translateControlKeyState(cks)
|
||||
key.BaseCode = baseCode
|
||||
key = ensureKeyCase(key, cks)
|
||||
if keyDown {
|
||||
return KeyPressEvent(key)
|
||||
}
|
||||
|
||||
return KeyReleaseEvent(key)
|
||||
}
|
||||
|
||||
// ensureKeyCase ensures that the key's text is in the correct case based on the
|
||||
// control key state.
|
||||
func ensureKeyCase(key Key, cks uint32) Key {
|
||||
if len(key.Text) == 0 {
|
||||
return key
|
||||
}
|
||||
|
||||
hasShift := cks&xwindows.SHIFT_PRESSED != 0
|
||||
hasCaps := cks&xwindows.CAPSLOCK_ON != 0
|
||||
if hasShift || hasCaps {
|
||||
if unicode.IsLower(key.Code) {
|
||||
key.ShiftedCode = unicode.ToUpper(key.Code)
|
||||
key.Text = string(key.ShiftedCode)
|
||||
}
|
||||
} else {
|
||||
if unicode.IsUpper(key.Code) {
|
||||
key.ShiftedCode = unicode.ToLower(key.Code)
|
||||
key.Text = string(key.ShiftedCode)
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// translateControlKeyState translates the control key state from the Windows
|
||||
// Console API into a Mod bitmask.
|
||||
func translateControlKeyState(cks uint32) (m KeyMod) {
|
||||
if cks&xwindows.LEFT_CTRL_PRESSED != 0 || cks&xwindows.RIGHT_CTRL_PRESSED != 0 {
|
||||
m |= ModCtrl
|
||||
}
|
||||
if cks&xwindows.LEFT_ALT_PRESSED != 0 || cks&xwindows.RIGHT_ALT_PRESSED != 0 {
|
||||
m |= ModAlt
|
||||
}
|
||||
if cks&xwindows.SHIFT_PRESSED != 0 {
|
||||
m |= ModShift
|
||||
}
|
||||
if cks&xwindows.CAPSLOCK_ON != 0 {
|
||||
m |= ModCapsLock
|
||||
}
|
||||
if cks&xwindows.NUMLOCK_ON != 0 {
|
||||
m |= ModNumLock
|
||||
}
|
||||
if cks&xwindows.SCROLLLOCK_ON != 0 {
|
||||
m |= ModScrollLock
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func keyEventString(vkc, sc uint16, r rune, keyDown bool, cks uint32, repeatCount uint16) string {
|
||||
var s strings.Builder
|
||||
s.WriteString("vkc: ")
|
||||
s.WriteString(fmt.Sprintf("%d, 0x%02x", vkc, vkc))
|
||||
s.WriteString(", sc: ")
|
||||
s.WriteString(fmt.Sprintf("%d, 0x%02x", sc, sc))
|
||||
s.WriteString(", r: ")
|
||||
s.WriteString(fmt.Sprintf("%q", r))
|
||||
s.WriteString(", down: ")
|
||||
s.WriteString(fmt.Sprintf("%v", keyDown))
|
||||
s.WriteString(", cks: [")
|
||||
if cks&xwindows.LEFT_ALT_PRESSED != 0 {
|
||||
s.WriteString("left alt, ")
|
||||
}
|
||||
if cks&xwindows.RIGHT_ALT_PRESSED != 0 {
|
||||
s.WriteString("right alt, ")
|
||||
}
|
||||
if cks&xwindows.LEFT_CTRL_PRESSED != 0 {
|
||||
s.WriteString("left ctrl, ")
|
||||
}
|
||||
if cks&xwindows.RIGHT_CTRL_PRESSED != 0 {
|
||||
s.WriteString("right ctrl, ")
|
||||
}
|
||||
if cks&xwindows.SHIFT_PRESSED != 0 {
|
||||
s.WriteString("shift, ")
|
||||
}
|
||||
if cks&xwindows.CAPSLOCK_ON != 0 {
|
||||
s.WriteString("caps lock, ")
|
||||
}
|
||||
if cks&xwindows.NUMLOCK_ON != 0 {
|
||||
s.WriteString("num lock, ")
|
||||
}
|
||||
if cks&xwindows.SCROLLLOCK_ON != 0 {
|
||||
s.WriteString("scroll lock, ")
|
||||
}
|
||||
if cks&xwindows.ENHANCED_KEY != 0 {
|
||||
s.WriteString("enhanced key, ")
|
||||
}
|
||||
s.WriteString("], repeat count: ")
|
||||
s.WriteString(fmt.Sprintf("%d", repeatCount))
|
||||
return s.String()
|
||||
}
|
||||
271
packages/client/tui/input/driver_windows_test.go
Normal file
271
packages/client/tui/input/driver_windows_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"image/color"
|
||||
"reflect"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
xwindows "github.com/charmbracelet/x/windows"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func TestWindowsInputEvents(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
events []xwindows.InputRecord
|
||||
expected []Event
|
||||
sequence bool // indicates that the input events are ANSI sequence or utf16
|
||||
}{
|
||||
{
|
||||
name: "single key event",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: 'a',
|
||||
VirtualKeyCode: 'A',
|
||||
}),
|
||||
},
|
||||
expected: []Event{KeyPressEvent{Code: 'a', BaseCode: 'a', Text: "a"}},
|
||||
},
|
||||
{
|
||||
name: "single key event with control key",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: 'a',
|
||||
VirtualKeyCode: 'A',
|
||||
ControlKeyState: xwindows.LEFT_CTRL_PRESSED,
|
||||
}),
|
||||
},
|
||||
expected: []Event{KeyPressEvent{Code: 'a', BaseCode: 'a', Mod: ModCtrl}},
|
||||
},
|
||||
{
|
||||
name: "escape alt key event",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: ansi.ESC,
|
||||
VirtualKeyCode: ansi.ESC,
|
||||
ControlKeyState: xwindows.LEFT_ALT_PRESSED,
|
||||
}),
|
||||
},
|
||||
expected: []Event{KeyPressEvent{Code: ansi.ESC, BaseCode: ansi.ESC, Mod: ModAlt}},
|
||||
},
|
||||
{
|
||||
name: "single shifted key event",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: 'A',
|
||||
VirtualKeyCode: 'A',
|
||||
ControlKeyState: xwindows.SHIFT_PRESSED,
|
||||
}),
|
||||
},
|
||||
expected: []Event{KeyPressEvent{Code: 'A', BaseCode: 'a', Text: "A", Mod: ModShift}},
|
||||
},
|
||||
{
|
||||
name: "utf16 rune",
|
||||
events: encodeUtf16Rune('😊'), // smiley emoji '😊'
|
||||
expected: []Event{
|
||||
KeyPressEvent{Code: '😊', Text: "😊"},
|
||||
},
|
||||
sequence: true,
|
||||
},
|
||||
{
|
||||
name: "background color response",
|
||||
events: encodeSequence("\x1b]11;rgb:ff/ff/ff\x07"),
|
||||
expected: []Event{BackgroundColorEvent{Color: color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}}},
|
||||
sequence: true,
|
||||
},
|
||||
{
|
||||
name: "st terminated background color response",
|
||||
events: encodeSequence("\x1b]11;rgb:ffff/ffff/ffff\x1b\\"),
|
||||
expected: []Event{BackgroundColorEvent{Color: color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}}},
|
||||
sequence: true,
|
||||
},
|
||||
{
|
||||
name: "simple mouse event",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeMouseEvent(xwindows.MouseEventRecord{
|
||||
MousePositon: windows.Coord{X: 10, Y: 20},
|
||||
ButtonState: xwindows.FROM_LEFT_1ST_BUTTON_PRESSED,
|
||||
EventFlags: 0,
|
||||
}),
|
||||
encodeMouseEvent(xwindows.MouseEventRecord{
|
||||
MousePositon: windows.Coord{X: 10, Y: 20},
|
||||
EventFlags: 0,
|
||||
}),
|
||||
},
|
||||
expected: []Event{
|
||||
MouseClickEvent{Button: MouseLeft, X: 10, Y: 20},
|
||||
MouseReleaseEvent{Button: MouseLeft, X: 10, Y: 20},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "focus event",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeFocusEvent(xwindows.FocusEventRecord{
|
||||
SetFocus: true,
|
||||
}),
|
||||
encodeFocusEvent(xwindows.FocusEventRecord{
|
||||
SetFocus: false,
|
||||
}),
|
||||
},
|
||||
expected: []Event{
|
||||
FocusEvent{},
|
||||
BlurEvent{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "window size event",
|
||||
events: []xwindows.InputRecord{
|
||||
encodeWindowBufferSizeEvent(xwindows.WindowBufferSizeRecord{
|
||||
Size: windows.Coord{X: 10, Y: 20},
|
||||
}),
|
||||
},
|
||||
expected: []Event{
|
||||
WindowSizeEvent{Width: 10, Height: 20},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// p is the parser to parse the input events
|
||||
var p Parser
|
||||
|
||||
// keep track of the state of the driver to handle ANSI sequences and utf16
|
||||
var state win32InputState
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.sequence {
|
||||
var Event Event
|
||||
for _, ev := range tc.events {
|
||||
if ev.EventType != xwindows.KEY_EVENT {
|
||||
t.Fatalf("expected key event, got %v", ev.EventType)
|
||||
}
|
||||
|
||||
key := ev.KeyEvent()
|
||||
Event = p.parseWin32InputKeyEvent(&state, key.VirtualKeyCode, key.VirtualScanCode, key.Char, key.KeyDown, key.ControlKeyState, key.RepeatCount)
|
||||
}
|
||||
if len(tc.expected) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(tc.expected))
|
||||
}
|
||||
if !reflect.DeepEqual(Event, tc.expected[0]) {
|
||||
t.Errorf("expected %v, got %v", tc.expected[0], Event)
|
||||
}
|
||||
} else {
|
||||
if len(tc.events) != len(tc.expected) {
|
||||
t.Fatalf("expected %d events, got %d", len(tc.expected), len(tc.events))
|
||||
}
|
||||
for j, ev := range tc.events {
|
||||
Event := p.parseConInputEvent(ev, &state)
|
||||
if !reflect.DeepEqual(Event, tc.expected[j]) {
|
||||
t.Errorf("expected %#v, got %#v", tc.expected[j], Event)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func boolToUint32(b bool) uint32 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func encodeMenuEvent(menu xwindows.MenuEventRecord) xwindows.InputRecord {
|
||||
var bts [16]byte
|
||||
binary.LittleEndian.PutUint32(bts[0:4], menu.CommandID)
|
||||
return xwindows.InputRecord{
|
||||
EventType: xwindows.MENU_EVENT,
|
||||
Event: bts,
|
||||
}
|
||||
}
|
||||
|
||||
func encodeWindowBufferSizeEvent(size xwindows.WindowBufferSizeRecord) xwindows.InputRecord {
|
||||
var bts [16]byte
|
||||
binary.LittleEndian.PutUint16(bts[0:2], uint16(size.Size.X))
|
||||
binary.LittleEndian.PutUint16(bts[2:4], uint16(size.Size.Y))
|
||||
return xwindows.InputRecord{
|
||||
EventType: xwindows.WINDOW_BUFFER_SIZE_EVENT,
|
||||
Event: bts,
|
||||
}
|
||||
}
|
||||
|
||||
func encodeFocusEvent(focus xwindows.FocusEventRecord) xwindows.InputRecord {
|
||||
var bts [16]byte
|
||||
if focus.SetFocus {
|
||||
bts[0] = 1
|
||||
}
|
||||
return xwindows.InputRecord{
|
||||
EventType: xwindows.FOCUS_EVENT,
|
||||
Event: bts,
|
||||
}
|
||||
}
|
||||
|
||||
func encodeMouseEvent(mouse xwindows.MouseEventRecord) xwindows.InputRecord {
|
||||
var bts [16]byte
|
||||
binary.LittleEndian.PutUint16(bts[0:2], uint16(mouse.MousePositon.X))
|
||||
binary.LittleEndian.PutUint16(bts[2:4], uint16(mouse.MousePositon.Y))
|
||||
binary.LittleEndian.PutUint32(bts[4:8], mouse.ButtonState)
|
||||
binary.LittleEndian.PutUint32(bts[8:12], mouse.ControlKeyState)
|
||||
binary.LittleEndian.PutUint32(bts[12:16], mouse.EventFlags)
|
||||
return xwindows.InputRecord{
|
||||
EventType: xwindows.MOUSE_EVENT,
|
||||
Event: bts,
|
||||
}
|
||||
}
|
||||
|
||||
func encodeKeyEvent(key xwindows.KeyEventRecord) xwindows.InputRecord {
|
||||
var bts [16]byte
|
||||
binary.LittleEndian.PutUint32(bts[0:4], boolToUint32(key.KeyDown))
|
||||
binary.LittleEndian.PutUint16(bts[4:6], key.RepeatCount)
|
||||
binary.LittleEndian.PutUint16(bts[6:8], key.VirtualKeyCode)
|
||||
binary.LittleEndian.PutUint16(bts[8:10], key.VirtualScanCode)
|
||||
binary.LittleEndian.PutUint16(bts[10:12], uint16(key.Char))
|
||||
binary.LittleEndian.PutUint32(bts[12:16], key.ControlKeyState)
|
||||
return xwindows.InputRecord{
|
||||
EventType: xwindows.KEY_EVENT,
|
||||
Event: bts,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeSequence encodes a string of ANSI escape sequences into a slice of
|
||||
// Windows input key records.
|
||||
func encodeSequence(s string) (evs []xwindows.InputRecord) {
|
||||
var state byte
|
||||
for len(s) > 0 {
|
||||
seq, _, n, newState := ansi.DecodeSequence(s, state, nil)
|
||||
for i := 0; i < n; i++ {
|
||||
evs = append(evs, encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: rune(seq[i]),
|
||||
}))
|
||||
}
|
||||
state = newState
|
||||
s = s[n:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func encodeUtf16Rune(r rune) []xwindows.InputRecord {
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
return encodeUtf16Pair(r1, r2)
|
||||
}
|
||||
|
||||
func encodeUtf16Pair(r1, r2 rune) []xwindows.InputRecord {
|
||||
return []xwindows.InputRecord{
|
||||
encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: r1,
|
||||
}),
|
||||
encodeKeyEvent(xwindows.KeyEventRecord{
|
||||
KeyDown: true,
|
||||
Char: r2,
|
||||
}),
|
||||
}
|
||||
}
|
||||
9
packages/client/tui/input/focus.go
Normal file
9
packages/client/tui/input/focus.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package input
|
||||
|
||||
// FocusEvent represents a terminal focus event.
|
||||
// This occurs when the terminal gains focus.
|
||||
type FocusEvent struct{}
|
||||
|
||||
// BlurEvent represents a terminal blur event.
|
||||
// This occurs when the terminal loses focus.
|
||||
type BlurEvent struct{}
|
||||
27
packages/client/tui/input/focus_test.go
Normal file
27
packages/client/tui/input/focus_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFocus(t *testing.T) {
|
||||
var p Parser
|
||||
_, e := p.parseSequence([]byte("\x1b[I"))
|
||||
switch e.(type) {
|
||||
case FocusEvent:
|
||||
// ok
|
||||
default:
|
||||
t.Error("invalid sequence")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlur(t *testing.T) {
|
||||
var p Parser
|
||||
_, e := p.parseSequence([]byte("\x1b[O"))
|
||||
switch e.(type) {
|
||||
case BlurEvent:
|
||||
// ok
|
||||
default:
|
||||
t.Error("invalid sequence")
|
||||
}
|
||||
}
|
||||
18
packages/client/tui/input/go.mod
Normal file
18
packages/client/tui/input/go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module github.com/charmbracelet/x/input
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/x/ansi v0.9.3
|
||||
github.com/charmbracelet/x/windows v0.2.1
|
||||
github.com/muesli/cancelreader v0.2.2
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
)
|
||||
19
packages/client/tui/input/go.sum
Normal file
19
packages/client/tui/input/go.sum
Normal file
@@ -0,0 +1,19 @@
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
|
||||
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
45
packages/client/tui/input/input.go
Normal file
45
packages/client/tui/input/input.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Event represents a terminal event.
|
||||
type Event any
|
||||
|
||||
// UnknownEvent represents an unknown event.
|
||||
type UnknownEvent string
|
||||
|
||||
// String returns a string representation of the unknown event.
|
||||
func (e UnknownEvent) String() string {
|
||||
return fmt.Sprintf("%q", string(e))
|
||||
}
|
||||
|
||||
// MultiEvent represents multiple messages event.
|
||||
type MultiEvent []Event
|
||||
|
||||
// String returns a string representation of the multiple messages event.
|
||||
func (e MultiEvent) String() string {
|
||||
var sb strings.Builder
|
||||
for _, ev := range e {
|
||||
sb.WriteString(fmt.Sprintf("%v\n", ev))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// WindowSizeEvent is used to report the terminal size. Note that Windows does
|
||||
// not have support for reporting resizes via SIGWINCH signals and relies on
|
||||
// the Windows Console API to report window size changes.
|
||||
type WindowSizeEvent struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// WindowOpEvent is a window operation (XTWINOPS) report event. This is used to
|
||||
// report various window operations such as reporting the window size or cell
|
||||
// size.
|
||||
type WindowOpEvent struct {
|
||||
Op int
|
||||
Args []int
|
||||
}
|
||||
574
packages/client/tui/input/key.go
Normal file
574
packages/client/tui/input/key.go
Normal file
@@ -0,0 +1,574 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const (
|
||||
// KeyExtended is a special key code used to signify that a key event
|
||||
// contains multiple runes.
|
||||
KeyExtended = unicode.MaxRune + 1
|
||||
)
|
||||
|
||||
// Special key symbols.
|
||||
const (
|
||||
|
||||
// Special keys.
|
||||
|
||||
KeyUp rune = KeyExtended + iota + 1
|
||||
KeyDown
|
||||
KeyRight
|
||||
KeyLeft
|
||||
KeyBegin
|
||||
KeyFind
|
||||
KeyInsert
|
||||
KeyDelete
|
||||
KeySelect
|
||||
KeyPgUp
|
||||
KeyPgDown
|
||||
KeyHome
|
||||
KeyEnd
|
||||
|
||||
// Keypad keys.
|
||||
|
||||
KeyKpEnter
|
||||
KeyKpEqual
|
||||
KeyKpMultiply
|
||||
KeyKpPlus
|
||||
KeyKpComma
|
||||
KeyKpMinus
|
||||
KeyKpDecimal
|
||||
KeyKpDivide
|
||||
KeyKp0
|
||||
KeyKp1
|
||||
KeyKp2
|
||||
KeyKp3
|
||||
KeyKp4
|
||||
KeyKp5
|
||||
KeyKp6
|
||||
KeyKp7
|
||||
KeyKp8
|
||||
KeyKp9
|
||||
|
||||
//nolint:godox
|
||||
// The following are keys defined in the Kitty keyboard protocol.
|
||||
// TODO: Investigate the names of these keys.
|
||||
|
||||
KeyKpSep
|
||||
KeyKpUp
|
||||
KeyKpDown
|
||||
KeyKpLeft
|
||||
KeyKpRight
|
||||
KeyKpPgUp
|
||||
KeyKpPgDown
|
||||
KeyKpHome
|
||||
KeyKpEnd
|
||||
KeyKpInsert
|
||||
KeyKpDelete
|
||||
KeyKpBegin
|
||||
|
||||
// Function keys.
|
||||
|
||||
KeyF1
|
||||
KeyF2
|
||||
KeyF3
|
||||
KeyF4
|
||||
KeyF5
|
||||
KeyF6
|
||||
KeyF7
|
||||
KeyF8
|
||||
KeyF9
|
||||
KeyF10
|
||||
KeyF11
|
||||
KeyF12
|
||||
KeyF13
|
||||
KeyF14
|
||||
KeyF15
|
||||
KeyF16
|
||||
KeyF17
|
||||
KeyF18
|
||||
KeyF19
|
||||
KeyF20
|
||||
KeyF21
|
||||
KeyF22
|
||||
KeyF23
|
||||
KeyF24
|
||||
KeyF25
|
||||
KeyF26
|
||||
KeyF27
|
||||
KeyF28
|
||||
KeyF29
|
||||
KeyF30
|
||||
KeyF31
|
||||
KeyF32
|
||||
KeyF33
|
||||
KeyF34
|
||||
KeyF35
|
||||
KeyF36
|
||||
KeyF37
|
||||
KeyF38
|
||||
KeyF39
|
||||
KeyF40
|
||||
KeyF41
|
||||
KeyF42
|
||||
KeyF43
|
||||
KeyF44
|
||||
KeyF45
|
||||
KeyF46
|
||||
KeyF47
|
||||
KeyF48
|
||||
KeyF49
|
||||
KeyF50
|
||||
KeyF51
|
||||
KeyF52
|
||||
KeyF53
|
||||
KeyF54
|
||||
KeyF55
|
||||
KeyF56
|
||||
KeyF57
|
||||
KeyF58
|
||||
KeyF59
|
||||
KeyF60
|
||||
KeyF61
|
||||
KeyF62
|
||||
KeyF63
|
||||
|
||||
//nolint:godox
|
||||
// The following are keys defined in the Kitty keyboard protocol.
|
||||
// TODO: Investigate the names of these keys.
|
||||
|
||||
KeyCapsLock
|
||||
KeyScrollLock
|
||||
KeyNumLock
|
||||
KeyPrintScreen
|
||||
KeyPause
|
||||
KeyMenu
|
||||
|
||||
KeyMediaPlay
|
||||
KeyMediaPause
|
||||
KeyMediaPlayPause
|
||||
KeyMediaReverse
|
||||
KeyMediaStop
|
||||
KeyMediaFastForward
|
||||
KeyMediaRewind
|
||||
KeyMediaNext
|
||||
KeyMediaPrev
|
||||
KeyMediaRecord
|
||||
|
||||
KeyLowerVol
|
||||
KeyRaiseVol
|
||||
KeyMute
|
||||
|
||||
KeyLeftShift
|
||||
KeyLeftAlt
|
||||
KeyLeftCtrl
|
||||
KeyLeftSuper
|
||||
KeyLeftHyper
|
||||
KeyLeftMeta
|
||||
KeyRightShift
|
||||
KeyRightAlt
|
||||
KeyRightCtrl
|
||||
KeyRightSuper
|
||||
KeyRightHyper
|
||||
KeyRightMeta
|
||||
KeyIsoLevel3Shift
|
||||
KeyIsoLevel5Shift
|
||||
|
||||
// Special names in C0.
|
||||
|
||||
KeyBackspace = rune(ansi.DEL)
|
||||
KeyTab = rune(ansi.HT)
|
||||
KeyEnter = rune(ansi.CR)
|
||||
KeyReturn = KeyEnter
|
||||
KeyEscape = rune(ansi.ESC)
|
||||
KeyEsc = KeyEscape
|
||||
|
||||
// Special names in G0.
|
||||
|
||||
KeySpace = rune(ansi.SP)
|
||||
)
|
||||
|
||||
// KeyPressEvent represents a key press event.
|
||||
type KeyPressEvent Key
|
||||
|
||||
// String implements [fmt.Stringer] and is quite useful for matching key
|
||||
// events. For details, on what this returns see [Key.String].
|
||||
func (k KeyPressEvent) String() string {
|
||||
return Key(k).String()
|
||||
}
|
||||
|
||||
// Keystroke returns the keystroke representation of the [Key]. While less type
|
||||
// safe than looking at the individual fields, it will usually be more
|
||||
// convenient and readable to use this method when matching against keys.
|
||||
//
|
||||
// Note that modifier keys are always printed in the following order:
|
||||
// - ctrl
|
||||
// - alt
|
||||
// - shift
|
||||
// - meta
|
||||
// - hyper
|
||||
// - super
|
||||
//
|
||||
// For example, you'll always see "ctrl+shift+alt+a" and never
|
||||
// "shift+ctrl+alt+a".
|
||||
func (k KeyPressEvent) Keystroke() string {
|
||||
return Key(k).Keystroke()
|
||||
}
|
||||
|
||||
// Key returns the underlying key event. This is a syntactic sugar for casting
|
||||
// the key event to a [Key].
|
||||
func (k KeyPressEvent) Key() Key {
|
||||
return Key(k)
|
||||
}
|
||||
|
||||
// KeyReleaseEvent represents a key release event.
|
||||
type KeyReleaseEvent Key
|
||||
|
||||
// String implements [fmt.Stringer] and is quite useful for matching key
|
||||
// events. For details, on what this returns see [Key.String].
|
||||
func (k KeyReleaseEvent) String() string {
|
||||
return Key(k).String()
|
||||
}
|
||||
|
||||
// Keystroke returns the keystroke representation of the [Key]. While less type
|
||||
// safe than looking at the individual fields, it will usually be more
|
||||
// convenient and readable to use this method when matching against keys.
|
||||
//
|
||||
// Note that modifier keys are always printed in the following order:
|
||||
// - ctrl
|
||||
// - alt
|
||||
// - shift
|
||||
// - meta
|
||||
// - hyper
|
||||
// - super
|
||||
//
|
||||
// For example, you'll always see "ctrl+shift+alt+a" and never
|
||||
// "shift+ctrl+alt+a".
|
||||
func (k KeyReleaseEvent) Keystroke() string {
|
||||
return Key(k).Keystroke()
|
||||
}
|
||||
|
||||
// Key returns the underlying key event. This is a convenience method and
|
||||
// syntactic sugar to satisfy the [KeyEvent] interface, and cast the key event to
|
||||
// [Key].
|
||||
func (k KeyReleaseEvent) Key() Key {
|
||||
return Key(k)
|
||||
}
|
||||
|
||||
// KeyEvent represents a key event. This can be either a key press or a key
|
||||
// release event.
|
||||
type KeyEvent interface {
|
||||
fmt.Stringer
|
||||
|
||||
// Key returns the underlying key event.
|
||||
Key() Key
|
||||
}
|
||||
|
||||
// Key represents a Key press or release event. It contains information about
|
||||
// the Key pressed, like the runes, the type of Key, and the modifiers pressed.
|
||||
// There are a couple general patterns you could use to check for key presses
|
||||
// or releases:
|
||||
//
|
||||
// // Switch on the string representation of the key (shorter)
|
||||
// switch ev := ev.(type) {
|
||||
// case KeyPressEvent:
|
||||
// switch ev.String() {
|
||||
// case "enter":
|
||||
// fmt.Println("you pressed enter!")
|
||||
// case "a":
|
||||
// fmt.Println("you pressed a!")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Switch on the key type (more foolproof)
|
||||
// switch ev := ev.(type) {
|
||||
// case KeyEvent:
|
||||
// // catch both KeyPressEvent and KeyReleaseEvent
|
||||
// switch key := ev.Key(); key.Code {
|
||||
// case KeyEnter:
|
||||
// fmt.Println("you pressed enter!")
|
||||
// default:
|
||||
// switch key.Text {
|
||||
// case "a":
|
||||
// fmt.Println("you pressed a!")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Note that [Key.Text] will be empty for special keys like [KeyEnter],
|
||||
// [KeyTab], and for keys that don't represent printable characters like key
|
||||
// combos with modifier keys. In other words, [Key.Text] is populated only for
|
||||
// keys that represent printable characters shifted or unshifted (like 'a',
|
||||
// 'A', '1', '!', etc.).
|
||||
type Key struct {
|
||||
// Text contains the actual characters received. This usually the same as
|
||||
// [Key.Code]. When [Key.Text] is non-empty, it indicates that the key
|
||||
// pressed represents printable character(s).
|
||||
Text string
|
||||
|
||||
// Mod represents modifier keys, like [ModCtrl], [ModAlt], and so on.
|
||||
Mod KeyMod
|
||||
|
||||
// Code represents the key pressed. This is usually a special key like
|
||||
// [KeyTab], [KeyEnter], [KeyF1], or a printable character like 'a'.
|
||||
Code rune
|
||||
|
||||
// ShiftedCode is the actual, shifted key pressed by the user. For example,
|
||||
// if the user presses shift+a, or caps lock is on, [Key.ShiftedCode] will
|
||||
// be 'A' and [Key.Code] will be 'a'.
|
||||
//
|
||||
// In the case of non-latin keyboards, like Arabic, [Key.ShiftedCode] is the
|
||||
// unshifted key on the keyboard.
|
||||
//
|
||||
// This is only available with the Kitty Keyboard Protocol or the Windows
|
||||
// Console API.
|
||||
ShiftedCode rune
|
||||
|
||||
// BaseCode is the key pressed according to the standard PC-101 key layout.
|
||||
// On international keyboards, this is the key that would be pressed if the
|
||||
// keyboard was set to US PC-101 layout.
|
||||
//
|
||||
// For example, if the user presses 'q' on a French AZERTY keyboard,
|
||||
// [Key.BaseCode] will be 'q'.
|
||||
//
|
||||
// This is only available with the Kitty Keyboard Protocol or the Windows
|
||||
// Console API.
|
||||
BaseCode rune
|
||||
|
||||
// IsRepeat indicates whether the key is being held down and sending events
|
||||
// repeatedly.
|
||||
//
|
||||
// This is only available with the Kitty Keyboard Protocol or the Windows
|
||||
// Console API.
|
||||
IsRepeat bool
|
||||
}
|
||||
|
||||
// String implements [fmt.Stringer] and is quite useful for matching key
|
||||
// events. It will return the textual representation of the [Key] if there is
|
||||
// one, otherwise, it will fallback to [Key.Keystroke].
|
||||
//
|
||||
// For example, you'll always get "?" and instead of "shift+/" on a US ANSI
|
||||
// keyboard.
|
||||
func (k Key) String() string {
|
||||
if len(k.Text) > 0 && k.Text != " " {
|
||||
return k.Text
|
||||
}
|
||||
return k.Keystroke()
|
||||
}
|
||||
|
||||
// Keystroke returns the keystroke representation of the [Key]. While less type
|
||||
// safe than looking at the individual fields, it will usually be more
|
||||
// convenient and readable to use this method when matching against keys.
|
||||
//
|
||||
// Note that modifier keys are always printed in the following order:
|
||||
// - ctrl
|
||||
// - alt
|
||||
// - shift
|
||||
// - meta
|
||||
// - hyper
|
||||
// - super
|
||||
//
|
||||
// For example, you'll always see "ctrl+shift+alt+a" and never
|
||||
// "shift+ctrl+alt+a".
|
||||
func (k Key) Keystroke() string {
|
||||
var sb strings.Builder
|
||||
if k.Mod.Contains(ModCtrl) && k.Code != KeyLeftCtrl && k.Code != KeyRightCtrl {
|
||||
sb.WriteString("ctrl+")
|
||||
}
|
||||
if k.Mod.Contains(ModAlt) && k.Code != KeyLeftAlt && k.Code != KeyRightAlt {
|
||||
sb.WriteString("alt+")
|
||||
}
|
||||
if k.Mod.Contains(ModShift) && k.Code != KeyLeftShift && k.Code != KeyRightShift {
|
||||
sb.WriteString("shift+")
|
||||
}
|
||||
if k.Mod.Contains(ModMeta) && k.Code != KeyLeftMeta && k.Code != KeyRightMeta {
|
||||
sb.WriteString("meta+")
|
||||
}
|
||||
if k.Mod.Contains(ModHyper) && k.Code != KeyLeftHyper && k.Code != KeyRightHyper {
|
||||
sb.WriteString("hyper+")
|
||||
}
|
||||
if k.Mod.Contains(ModSuper) && k.Code != KeyLeftSuper && k.Code != KeyRightSuper {
|
||||
sb.WriteString("super+")
|
||||
}
|
||||
|
||||
if kt, ok := keyTypeString[k.Code]; ok {
|
||||
sb.WriteString(kt)
|
||||
} else {
|
||||
code := k.Code
|
||||
if k.BaseCode != 0 {
|
||||
// If a [Key.BaseCode] is present, use it to represent a key using the standard
|
||||
// PC-101 key layout.
|
||||
code = k.BaseCode
|
||||
}
|
||||
|
||||
switch code {
|
||||
case KeySpace:
|
||||
// Space is the only invisible printable character.
|
||||
sb.WriteString("space")
|
||||
case KeyExtended:
|
||||
// Write the actual text of the key when the key contains multiple
|
||||
// runes.
|
||||
sb.WriteString(k.Text)
|
||||
default:
|
||||
sb.WriteRune(code)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var keyTypeString = map[rune]string{
|
||||
KeyEnter: "enter",
|
||||
KeyTab: "tab",
|
||||
KeyBackspace: "backspace",
|
||||
KeyEscape: "esc",
|
||||
KeySpace: "space",
|
||||
KeyUp: "up",
|
||||
KeyDown: "down",
|
||||
KeyLeft: "left",
|
||||
KeyRight: "right",
|
||||
KeyBegin: "begin",
|
||||
KeyFind: "find",
|
||||
KeyInsert: "insert",
|
||||
KeyDelete: "delete",
|
||||
KeySelect: "select",
|
||||
KeyPgUp: "pgup",
|
||||
KeyPgDown: "pgdown",
|
||||
KeyHome: "home",
|
||||
KeyEnd: "end",
|
||||
KeyKpEnter: "kpenter",
|
||||
KeyKpEqual: "kpequal",
|
||||
KeyKpMultiply: "kpmul",
|
||||
KeyKpPlus: "kpplus",
|
||||
KeyKpComma: "kpcomma",
|
||||
KeyKpMinus: "kpminus",
|
||||
KeyKpDecimal: "kpperiod",
|
||||
KeyKpDivide: "kpdiv",
|
||||
KeyKp0: "kp0",
|
||||
KeyKp1: "kp1",
|
||||
KeyKp2: "kp2",
|
||||
KeyKp3: "kp3",
|
||||
KeyKp4: "kp4",
|
||||
KeyKp5: "kp5",
|
||||
KeyKp6: "kp6",
|
||||
KeyKp7: "kp7",
|
||||
KeyKp8: "kp8",
|
||||
KeyKp9: "kp9",
|
||||
|
||||
// Kitty keyboard extension
|
||||
KeyKpSep: "kpsep",
|
||||
KeyKpUp: "kpup",
|
||||
KeyKpDown: "kpdown",
|
||||
KeyKpLeft: "kpleft",
|
||||
KeyKpRight: "kpright",
|
||||
KeyKpPgUp: "kppgup",
|
||||
KeyKpPgDown: "kppgdown",
|
||||
KeyKpHome: "kphome",
|
||||
KeyKpEnd: "kpend",
|
||||
KeyKpInsert: "kpinsert",
|
||||
KeyKpDelete: "kpdelete",
|
||||
KeyKpBegin: "kpbegin",
|
||||
|
||||
KeyF1: "f1",
|
||||
KeyF2: "f2",
|
||||
KeyF3: "f3",
|
||||
KeyF4: "f4",
|
||||
KeyF5: "f5",
|
||||
KeyF6: "f6",
|
||||
KeyF7: "f7",
|
||||
KeyF8: "f8",
|
||||
KeyF9: "f9",
|
||||
KeyF10: "f10",
|
||||
KeyF11: "f11",
|
||||
KeyF12: "f12",
|
||||
KeyF13: "f13",
|
||||
KeyF14: "f14",
|
||||
KeyF15: "f15",
|
||||
KeyF16: "f16",
|
||||
KeyF17: "f17",
|
||||
KeyF18: "f18",
|
||||
KeyF19: "f19",
|
||||
KeyF20: "f20",
|
||||
KeyF21: "f21",
|
||||
KeyF22: "f22",
|
||||
KeyF23: "f23",
|
||||
KeyF24: "f24",
|
||||
KeyF25: "f25",
|
||||
KeyF26: "f26",
|
||||
KeyF27: "f27",
|
||||
KeyF28: "f28",
|
||||
KeyF29: "f29",
|
||||
KeyF30: "f30",
|
||||
KeyF31: "f31",
|
||||
KeyF32: "f32",
|
||||
KeyF33: "f33",
|
||||
KeyF34: "f34",
|
||||
KeyF35: "f35",
|
||||
KeyF36: "f36",
|
||||
KeyF37: "f37",
|
||||
KeyF38: "f38",
|
||||
KeyF39: "f39",
|
||||
KeyF40: "f40",
|
||||
KeyF41: "f41",
|
||||
KeyF42: "f42",
|
||||
KeyF43: "f43",
|
||||
KeyF44: "f44",
|
||||
KeyF45: "f45",
|
||||
KeyF46: "f46",
|
||||
KeyF47: "f47",
|
||||
KeyF48: "f48",
|
||||
KeyF49: "f49",
|
||||
KeyF50: "f50",
|
||||
KeyF51: "f51",
|
||||
KeyF52: "f52",
|
||||
KeyF53: "f53",
|
||||
KeyF54: "f54",
|
||||
KeyF55: "f55",
|
||||
KeyF56: "f56",
|
||||
KeyF57: "f57",
|
||||
KeyF58: "f58",
|
||||
KeyF59: "f59",
|
||||
KeyF60: "f60",
|
||||
KeyF61: "f61",
|
||||
KeyF62: "f62",
|
||||
KeyF63: "f63",
|
||||
|
||||
// Kitty keyboard extension
|
||||
KeyCapsLock: "capslock",
|
||||
KeyScrollLock: "scrolllock",
|
||||
KeyNumLock: "numlock",
|
||||
KeyPrintScreen: "printscreen",
|
||||
KeyPause: "pause",
|
||||
KeyMenu: "menu",
|
||||
KeyMediaPlay: "mediaplay",
|
||||
KeyMediaPause: "mediapause",
|
||||
KeyMediaPlayPause: "mediaplaypause",
|
||||
KeyMediaReverse: "mediareverse",
|
||||
KeyMediaStop: "mediastop",
|
||||
KeyMediaFastForward: "mediafastforward",
|
||||
KeyMediaRewind: "mediarewind",
|
||||
KeyMediaNext: "medianext",
|
||||
KeyMediaPrev: "mediaprev",
|
||||
KeyMediaRecord: "mediarecord",
|
||||
KeyLowerVol: "lowervol",
|
||||
KeyRaiseVol: "raisevol",
|
||||
KeyMute: "mute",
|
||||
KeyLeftShift: "leftshift",
|
||||
KeyLeftAlt: "leftalt",
|
||||
KeyLeftCtrl: "leftctrl",
|
||||
KeyLeftSuper: "leftsuper",
|
||||
KeyLeftHyper: "lefthyper",
|
||||
KeyLeftMeta: "leftmeta",
|
||||
KeyRightShift: "rightshift",
|
||||
KeyRightAlt: "rightalt",
|
||||
KeyRightCtrl: "rightctrl",
|
||||
KeyRightSuper: "rightsuper",
|
||||
KeyRightHyper: "righthyper",
|
||||
KeyRightMeta: "rightmeta",
|
||||
KeyIsoLevel3Shift: "isolevel3shift",
|
||||
KeyIsoLevel5Shift: "isolevel5shift",
|
||||
}
|
||||
880
packages/client/tui/input/key_test.go
Normal file
880
packages/client/tui/input/key_test.go
Normal file
@@ -0,0 +1,880 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/charmbracelet/x/ansi/kitty"
|
||||
)
|
||||
|
||||
var sequences = buildKeysTable(FlagTerminfo, "dumb")
|
||||
|
||||
func TestKeyString(t *testing.T) {
|
||||
t.Run("alt+space", func(t *testing.T) {
|
||||
k := KeyPressEvent{Code: KeySpace, Mod: ModAlt}
|
||||
if got := k.String(); got != "alt+space" {
|
||||
t.Fatalf(`expected a "alt+space", got %q`, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("runes", func(t *testing.T) {
|
||||
k := KeyPressEvent{Code: 'a', Text: "a"}
|
||||
if got := k.String(); got != "a" {
|
||||
t.Fatalf(`expected an "a", got %q`, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
k := KeyPressEvent{Code: 99999}
|
||||
if got := k.String(); got != "𘚟" {
|
||||
t.Fatalf(`expected a "unknown", got %q`, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("space", func(t *testing.T) {
|
||||
k := KeyPressEvent{Code: KeySpace, Text: " "}
|
||||
if got := k.String(); got != "space" {
|
||||
t.Fatalf(`expected a "space", got %q`, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shift+space", func(t *testing.T) {
|
||||
k := KeyPressEvent{Code: KeySpace, Mod: ModShift}
|
||||
if got := k.String(); got != "shift+space" {
|
||||
t.Fatalf(`expected a "shift+space", got %q`, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("?", func(t *testing.T) {
|
||||
k := KeyPressEvent{Code: '/', Mod: ModShift, Text: "?"}
|
||||
if got := k.String(); got != "?" {
|
||||
t.Fatalf(`expected a "?", got %q`, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type seqTest struct {
|
||||
seq []byte
|
||||
Events []Event
|
||||
}
|
||||
|
||||
var f3CurPosRegexp = regexp.MustCompile(`\x1b\[1;(\d+)R`)
|
||||
|
||||
// buildBaseSeqTests returns sequence tests that are valid for the
|
||||
// detectSequence() function.
|
||||
func buildBaseSeqTests() []seqTest {
|
||||
td := []seqTest{}
|
||||
for seq, key := range sequences {
|
||||
k := KeyPressEvent(key)
|
||||
st := seqTest{seq: []byte(seq), Events: []Event{k}}
|
||||
|
||||
// XXX: This is a special case to handle F3 key sequence and cursor
|
||||
// position report having the same sequence. See [parseCsi] for more
|
||||
// information.
|
||||
if f3CurPosRegexp.MatchString(seq) {
|
||||
st.Events = []Event{k, CursorPositionEvent{Y: 0, X: int(key.Mod)}}
|
||||
}
|
||||
td = append(td, st)
|
||||
}
|
||||
|
||||
// Additional special cases.
|
||||
td = append(td,
|
||||
// Unrecognized CSI sequence.
|
||||
seqTest{
|
||||
[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
|
||||
[]Event{
|
||||
UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}),
|
||||
},
|
||||
},
|
||||
// A lone space character.
|
||||
seqTest{
|
||||
[]byte{' '},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeySpace, Text: " "},
|
||||
},
|
||||
},
|
||||
// An escape character with the alt modifier.
|
||||
seqTest{
|
||||
[]byte{'\x1b', ' '},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeySpace, Mod: ModAlt},
|
||||
},
|
||||
},
|
||||
)
|
||||
return td
|
||||
}
|
||||
|
||||
func TestParseSequence(t *testing.T) {
|
||||
td := buildBaseSeqTests()
|
||||
td = append(td,
|
||||
// Background color.
|
||||
seqTest{
|
||||
[]byte("\x1b]11;rgb:1234/1234/1234\x07"),
|
||||
[]Event{BackgroundColorEvent{
|
||||
Color: color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff},
|
||||
}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b]11;rgb:1234/1234/1234\x1b\\"),
|
||||
[]Event{BackgroundColorEvent{
|
||||
Color: color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff},
|
||||
}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b]11;rgb:1234/1234/1234\x1b"), // Incomplete sequences are ignored.
|
||||
[]Event{
|
||||
UnknownEvent("\x1b]11;rgb:1234/1234/1234\x1b"),
|
||||
},
|
||||
},
|
||||
|
||||
// Kitty Graphics response.
|
||||
seqTest{
|
||||
[]byte("\x1b_Ga=t;OK\x1b\\"),
|
||||
[]Event{KittyGraphicsEvent{
|
||||
Options: kitty.Options{Action: kitty.Transmit},
|
||||
Payload: []byte("OK"),
|
||||
}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b_Gi=99,I=13;OK\x1b\\"),
|
||||
[]Event{KittyGraphicsEvent{
|
||||
Options: kitty.Options{ID: 99, Number: 13},
|
||||
Payload: []byte("OK"),
|
||||
}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b_Gi=1337,q=1;EINVAL:your face\x1b\\"),
|
||||
[]Event{KittyGraphicsEvent{
|
||||
Options: kitty.Options{ID: 1337, Quite: 1},
|
||||
Payload: []byte("EINVAL:your face"),
|
||||
}},
|
||||
},
|
||||
|
||||
// Xterm modifyOtherKeys CSI 27 ; <modifier> ; <code> ~
|
||||
seqTest{
|
||||
[]byte("\x1b[27;3;20320~"),
|
||||
[]Event{KeyPressEvent{Code: '你', Mod: ModAlt}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[27;3;65~"),
|
||||
[]Event{KeyPressEvent{Code: 'A', Mod: ModAlt}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[27;3;8~"),
|
||||
[]Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[27;3;27~"),
|
||||
[]Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[27;3;127~"),
|
||||
[]Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}},
|
||||
},
|
||||
|
||||
// Xterm report window text area size.
|
||||
seqTest{
|
||||
[]byte("\x1b[4;24;80t"),
|
||||
[]Event{
|
||||
WindowOpEvent{Op: 4, Args: []int{24, 80}},
|
||||
},
|
||||
},
|
||||
|
||||
// Kitty keyboard / CSI u (fixterms)
|
||||
seqTest{
|
||||
[]byte("\x1b[1B"),
|
||||
[]Event{KeyPressEvent{Code: KeyDown}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[1;B"),
|
||||
[]Event{KeyPressEvent{Code: KeyDown}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[1;4B"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[1;4:1B"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[1;4:2B"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown, IsRepeat: true}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[1;4:3B"),
|
||||
[]Event{KeyReleaseEvent{Mod: ModShift | ModAlt, Code: KeyDown}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[8~"),
|
||||
[]Event{KeyPressEvent{Code: KeyEnd}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[8;~"),
|
||||
[]Event{KeyPressEvent{Code: KeyEnd}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[8;10~"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModMeta, Code: KeyEnd}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[27;4u"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyEscape}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[127;4u"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyBackspace}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[57358;4u"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyCapsLock}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[9;2u"),
|
||||
[]Event{KeyPressEvent{Mod: ModShift, Code: KeyTab}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[195;u"),
|
||||
[]Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[20320;2u"),
|
||||
[]Event{KeyPressEvent{Text: "你", Mod: ModShift, Code: '你'}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[195;:1u"),
|
||||
[]Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[195;2:3u"),
|
||||
[]Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[195;2:2u"),
|
||||
[]Event{KeyPressEvent{Code: 'Ã', Text: "Ã", IsRepeat: true, Mod: ModShift}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[195;2:1u"),
|
||||
[]Event{KeyPressEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[195;2:3u"),
|
||||
[]Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[97;2;65u"),
|
||||
[]Event{KeyPressEvent{Code: 'a', Text: "A", Mod: ModShift}},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b[97;;229u"),
|
||||
[]Event{KeyPressEvent{Code: 'a', Text: "å"}},
|
||||
},
|
||||
|
||||
// focus/blur
|
||||
seqTest{
|
||||
[]byte{'\x1b', '[', 'I'},
|
||||
[]Event{
|
||||
FocusEvent{},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{'\x1b', '[', 'O'},
|
||||
[]Event{
|
||||
BlurEvent{},
|
||||
},
|
||||
},
|
||||
// Mouse event.
|
||||
seqTest{
|
||||
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
|
||||
[]Event{
|
||||
MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp},
|
||||
},
|
||||
},
|
||||
// SGR Mouse event.
|
||||
seqTest{
|
||||
[]byte("\x1b[<0;33;17M"),
|
||||
[]Event{
|
||||
MouseClickEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
},
|
||||
// Runes.
|
||||
seqTest{
|
||||
[]byte{'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{'\x1b', 'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModAlt},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{'a', 'a', 'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
},
|
||||
},
|
||||
// Multi-byte rune.
|
||||
seqTest{
|
||||
[]byte("☃"),
|
||||
[]Event{
|
||||
KeyPressEvent{Code: '☃', Text: "☃"},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte("\x1b☃"),
|
||||
[]Event{
|
||||
KeyPressEvent{Code: '☃', Mod: ModAlt},
|
||||
},
|
||||
},
|
||||
// Standalone control characters.
|
||||
seqTest{
|
||||
[]byte{'\x1b'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeyEscape},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{ansi.SOH},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModCtrl},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{'\x1b', ansi.SOH},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{ansi.NUL},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeySpace, Mod: ModCtrl},
|
||||
},
|
||||
},
|
||||
seqTest{
|
||||
[]byte{'\x1b', ansi.NUL},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt},
|
||||
},
|
||||
},
|
||||
// C1 control characters.
|
||||
seqTest{
|
||||
[]byte{'\x80'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: rune(0x80 - '@'), Mod: ModCtrl | ModAlt},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows.
|
||||
// This is incorrect, but it makes our test fail if we try it out.
|
||||
td = append(td, seqTest{
|
||||
[]byte{'\xfe'},
|
||||
[]Event{
|
||||
UnknownEvent(rune(0xfe)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
var p Parser
|
||||
for _, tc := range td {
|
||||
t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) {
|
||||
var events []Event
|
||||
buf := tc.seq
|
||||
for len(buf) > 0 {
|
||||
width, Event := p.parseSequence(buf)
|
||||
switch Event := Event.(type) {
|
||||
case MultiEvent:
|
||||
events = append(events, Event...)
|
||||
default:
|
||||
events = append(events, Event)
|
||||
}
|
||||
buf = buf[width:]
|
||||
}
|
||||
if !reflect.DeepEqual(tc.Events, events) {
|
||||
t.Errorf("\nexpected event for %q:\n %#v\ngot:\n %#v", tc.seq, tc.Events, events)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLongInput(t *testing.T) {
|
||||
expect := make([]Event, 1000)
|
||||
for i := range 1000 {
|
||||
expect[i] = KeyPressEvent{Code: 'a', Text: "a"}
|
||||
}
|
||||
input := strings.Repeat("a", 1000)
|
||||
drv, err := NewReader(strings.NewReader(input), "dumb", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected input driver error: %v", err)
|
||||
}
|
||||
|
||||
var Events []Event
|
||||
for {
|
||||
events, err := drv.ReadEvents()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected input error: %v", err)
|
||||
}
|
||||
Events = append(Events, events...)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expect, Events) {
|
||||
t.Errorf("unexpected messages, expected:\n %+v\ngot:\n %+v", expect, Events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadInput(t *testing.T) {
|
||||
type test struct {
|
||||
keyname string
|
||||
in []byte
|
||||
out []Event
|
||||
}
|
||||
testData := []test{
|
||||
{
|
||||
"a",
|
||||
[]byte{'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"space",
|
||||
[]byte{' '},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeySpace, Text: " "},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a alt+a",
|
||||
[]byte{'a', '\x1b', 'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
KeyPressEvent{Code: 'a', Mod: ModAlt},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a alt+a a",
|
||||
[]byte{'a', '\x1b', 'a', 'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
KeyPressEvent{Code: 'a', Mod: ModAlt},
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"ctrl+a",
|
||||
[]byte{byte(ansi.SOH)},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModCtrl},
|
||||
},
|
||||
},
|
||||
{
|
||||
"ctrl+a ctrl+b",
|
||||
[]byte{byte(ansi.SOH), byte(ansi.STX)},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModCtrl},
|
||||
KeyPressEvent{Code: 'b', Mod: ModCtrl},
|
||||
},
|
||||
},
|
||||
{
|
||||
"alt+a",
|
||||
[]byte{byte(0x1b), 'a'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModAlt},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a b c d",
|
||||
[]byte{'a', 'b', 'c', 'd'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
KeyPressEvent{Code: 'b', Text: "b"},
|
||||
KeyPressEvent{Code: 'c', Text: "c"},
|
||||
KeyPressEvent{Code: 'd', Text: "d"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"up",
|
||||
[]byte("\x1b[A"),
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeyUp},
|
||||
},
|
||||
},
|
||||
{
|
||||
"wheel up",
|
||||
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
|
||||
[]Event{
|
||||
MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp},
|
||||
},
|
||||
},
|
||||
{
|
||||
"left motion release",
|
||||
[]byte{
|
||||
'\x1b', '[', 'M', byte(32) + 0b0010_0000, byte(32 + 33), byte(16 + 33),
|
||||
'\x1b', '[', 'M', byte(32) + 0b0000_0011, byte(64 + 33), byte(32 + 33),
|
||||
},
|
||||
[]Event{
|
||||
MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
MouseReleaseEvent{X: 64, Y: 32, Button: MouseNone},
|
||||
},
|
||||
},
|
||||
{
|
||||
"shift+tab",
|
||||
[]byte{'\x1b', '[', 'Z'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeyTab, Mod: ModShift},
|
||||
},
|
||||
},
|
||||
{
|
||||
"enter",
|
||||
[]byte{'\r'},
|
||||
[]Event{KeyPressEvent{Code: KeyEnter}},
|
||||
},
|
||||
{
|
||||
"alt+enter",
|
||||
[]byte{'\x1b', '\r'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeyEnter, Mod: ModAlt},
|
||||
},
|
||||
},
|
||||
{
|
||||
"insert",
|
||||
[]byte{'\x1b', '[', '2', '~'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: KeyInsert},
|
||||
},
|
||||
},
|
||||
{
|
||||
"ctrl+alt+a",
|
||||
[]byte{'\x1b', byte(ansi.SOH)},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt},
|
||||
},
|
||||
},
|
||||
{
|
||||
"CSI?----X?",
|
||||
[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
|
||||
[]Event{UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})},
|
||||
},
|
||||
// Powershell sequences.
|
||||
{
|
||||
"up",
|
||||
[]byte{'\x1b', 'O', 'A'},
|
||||
[]Event{KeyPressEvent{Code: KeyUp}},
|
||||
},
|
||||
{
|
||||
"down",
|
||||
[]byte{'\x1b', 'O', 'B'},
|
||||
[]Event{KeyPressEvent{Code: KeyDown}},
|
||||
},
|
||||
{
|
||||
"right",
|
||||
[]byte{'\x1b', 'O', 'C'},
|
||||
[]Event{KeyPressEvent{Code: KeyRight}},
|
||||
},
|
||||
{
|
||||
"left",
|
||||
[]byte{'\x1b', 'O', 'D'},
|
||||
[]Event{KeyPressEvent{Code: KeyLeft}},
|
||||
},
|
||||
{
|
||||
"alt+enter",
|
||||
[]byte{'\x1b', '\x0d'},
|
||||
[]Event{KeyPressEvent{Code: KeyEnter, Mod: ModAlt}},
|
||||
},
|
||||
{
|
||||
"alt+backspace",
|
||||
[]byte{'\x1b', '\x7f'},
|
||||
[]Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}},
|
||||
},
|
||||
{
|
||||
"ctrl+space",
|
||||
[]byte{'\x00'},
|
||||
[]Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl}},
|
||||
},
|
||||
{
|
||||
"ctrl+alt+space",
|
||||
[]byte{'\x1b', '\x00'},
|
||||
[]Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}},
|
||||
},
|
||||
{
|
||||
"esc",
|
||||
[]byte{'\x1b'},
|
||||
[]Event{KeyPressEvent{Code: KeyEscape}},
|
||||
},
|
||||
{
|
||||
"alt+esc",
|
||||
[]byte{'\x1b', '\x1b'},
|
||||
[]Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}},
|
||||
},
|
||||
{
|
||||
"a b o",
|
||||
[]byte{
|
||||
'\x1b', '[', '2', '0', '0', '~',
|
||||
'a', ' ', 'b',
|
||||
'\x1b', '[', '2', '0', '1', '~',
|
||||
'o',
|
||||
},
|
||||
[]Event{
|
||||
PasteStartEvent{},
|
||||
PasteEvent("a b"),
|
||||
PasteEndEvent{},
|
||||
KeyPressEvent{Code: 'o', Text: "o"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"a\x03\nb",
|
||||
[]byte{
|
||||
'\x1b', '[', '2', '0', '0', '~',
|
||||
'a', '\x03', '\n', 'b',
|
||||
'\x1b', '[', '2', '0', '1', '~',
|
||||
},
|
||||
[]Event{
|
||||
PasteStartEvent{},
|
||||
PasteEvent("a\x03\nb"),
|
||||
PasteEndEvent{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"?0xfe?",
|
||||
[]byte{'\xfe'},
|
||||
[]Event{
|
||||
UnknownEvent(rune(0xfe)),
|
||||
},
|
||||
},
|
||||
{
|
||||
"a ?0xfe? b",
|
||||
[]byte{'a', '\xfe', ' ', 'b'},
|
||||
[]Event{
|
||||
KeyPressEvent{Code: 'a', Text: "a"},
|
||||
UnknownEvent(rune(0xfe)),
|
||||
KeyPressEvent{Code: KeySpace, Text: " "},
|
||||
KeyPressEvent{Code: 'b', Text: "b"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, td := range testData {
|
||||
t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) {
|
||||
Events := testReadInputs(t, bytes.NewReader(td.in))
|
||||
var buf strings.Builder
|
||||
for i, Event := range Events {
|
||||
if i > 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
if s, ok := Event.(fmt.Stringer); ok {
|
||||
buf.WriteString(s.String())
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%#v:%T", Event, Event)
|
||||
}
|
||||
}
|
||||
|
||||
if len(Events) != len(td.out) {
|
||||
t.Fatalf("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n", len(Events), len(td.out), Events, td.out)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(td.out, Events) {
|
||||
t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, Events)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testReadInputs(t *testing.T, input io.Reader) []Event {
|
||||
// We'll check that the input reader finishes at the end
|
||||
// without error.
|
||||
var wg sync.WaitGroup
|
||||
var inputErr error
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer func() {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
if inputErr != nil && !errors.Is(inputErr, io.EOF) {
|
||||
t.Fatalf("unexpected input error: %v", inputErr)
|
||||
}
|
||||
}()
|
||||
|
||||
dr, err := NewReader(input, "dumb", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected input driver error: %v", err)
|
||||
}
|
||||
|
||||
// The messages we're consuming.
|
||||
EventsC := make(chan Event)
|
||||
|
||||
// Start the reader in the background.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var events []Event
|
||||
events, inputErr = dr.ReadEvents()
|
||||
out:
|
||||
for _, ev := range events {
|
||||
select {
|
||||
case EventsC <- ev:
|
||||
case <-ctx.Done():
|
||||
break out
|
||||
}
|
||||
}
|
||||
EventsC <- nil
|
||||
}()
|
||||
|
||||
var Events []Event
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case Event := <-EventsC:
|
||||
if Event == nil {
|
||||
// end of input marker for the test.
|
||||
break loop
|
||||
}
|
||||
Events = append(Events, Event)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Errorf("timeout waiting for input event")
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return Events
|
||||
}
|
||||
|
||||
// randTest defines the test input and expected output for a sequence
|
||||
// of interleaved control sequences and control characters.
|
||||
type randTest struct {
|
||||
data []byte
|
||||
lengths []int
|
||||
names []string
|
||||
}
|
||||
|
||||
// seed is the random seed to randomize the input. This helps check
|
||||
// that all the sequences get ultimately exercised.
|
||||
var seed = flag.Int64("seed", 0, "random seed (0 to autoselect)")
|
||||
|
||||
// genRandomData generates a randomized test, with a random seed unless
|
||||
// the seed flag was set.
|
||||
func genRandomData(logfn func(int64), length int) randTest {
|
||||
// We'll use a random source. However, we give the user the option
|
||||
// to override it to a specific value for reproduceability.
|
||||
s := *seed
|
||||
if s == 0 {
|
||||
s = time.Now().UnixNano()
|
||||
}
|
||||
// Inform the user so they know what to reuse to get the same data.
|
||||
logfn(s)
|
||||
return genRandomDataWithSeed(s, length)
|
||||
}
|
||||
|
||||
// genRandomDataWithSeed generates a randomized test with a fixed seed.
|
||||
func genRandomDataWithSeed(s int64, length int) randTest {
|
||||
src := rand.NewSource(s)
|
||||
r := rand.New(src)
|
||||
|
||||
// allseqs contains all the sequences, in sorted order. We sort
|
||||
// to make the test deterministic (when the seed is also fixed).
|
||||
type seqpair struct {
|
||||
seq string
|
||||
name string
|
||||
}
|
||||
var allseqs []seqpair
|
||||
for seq, key := range sequences {
|
||||
allseqs = append(allseqs, seqpair{seq, key.String()})
|
||||
}
|
||||
sort.Slice(allseqs, func(i, j int) bool { return allseqs[i].seq < allseqs[j].seq })
|
||||
|
||||
// res contains the computed test.
|
||||
var res randTest
|
||||
|
||||
for len(res.data) < length {
|
||||
alt := r.Intn(2)
|
||||
prefix := ""
|
||||
esclen := 0
|
||||
if alt == 1 {
|
||||
prefix = "alt+"
|
||||
esclen = 1
|
||||
}
|
||||
kind := r.Intn(3)
|
||||
switch kind {
|
||||
case 0:
|
||||
// A control character.
|
||||
if alt == 1 {
|
||||
res.data = append(res.data, '\x1b')
|
||||
}
|
||||
res.data = append(res.data, 1)
|
||||
res.names = append(res.names, "ctrl+"+prefix+"a")
|
||||
res.lengths = append(res.lengths, 1+esclen)
|
||||
|
||||
case 1, 2:
|
||||
// A sequence.
|
||||
seqi := r.Intn(len(allseqs))
|
||||
s := allseqs[seqi]
|
||||
if strings.Contains(s.name, "alt+") || strings.Contains(s.name, "meta+") {
|
||||
esclen = 0
|
||||
prefix = ""
|
||||
alt = 0
|
||||
}
|
||||
if alt == 1 {
|
||||
res.data = append(res.data, '\x1b')
|
||||
}
|
||||
res.data = append(res.data, s.seq...)
|
||||
if strings.HasPrefix(s.name, "ctrl+") {
|
||||
prefix = "ctrl+" + prefix
|
||||
}
|
||||
name := prefix + strings.TrimPrefix(s.name, "ctrl+")
|
||||
res.names = append(res.names, name)
|
||||
res.lengths = append(res.lengths, len(s.seq)+esclen)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func FuzzParseSequence(f *testing.F) {
|
||||
var p Parser
|
||||
for seq := range sequences {
|
||||
f.Add(seq)
|
||||
}
|
||||
f.Add("\x1b]52;?\x07") // OSC 52
|
||||
f.Add("\x1b]11;rgb:0000/0000/0000\x1b\\") // OSC 11
|
||||
f.Add("\x1bP>|charm terminal(0.1.2)\x1b\\") // DCS (XTVERSION)
|
||||
f.Add("\x1b_Gi=123\x1b\\") // APC
|
||||
f.Fuzz(func(t *testing.T, seq string) {
|
||||
n, _ := p.parseSequence([]byte(seq))
|
||||
if n == 0 && seq != "" {
|
||||
t.Errorf("expected a non-zero width for %q", seq)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkDetectSequenceMap benchmarks the map-based sequence
|
||||
// detector.
|
||||
func BenchmarkDetectSequenceMap(b *testing.B) {
|
||||
var p Parser
|
||||
td := genRandomDataWithSeed(123, 10000)
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j, w := 0, 0; j < len(td.data); j += w {
|
||||
w, _ = p.parseSequence(td.data[j:])
|
||||
}
|
||||
}
|
||||
}
|
||||
353
packages/client/tui/input/kitty.go
Normal file
353
packages/client/tui/input/kitty.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/charmbracelet/x/ansi/kitty"
|
||||
)
|
||||
|
||||
// KittyGraphicsEvent represents a Kitty Graphics response event.
|
||||
//
|
||||
// See https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
||||
type KittyGraphicsEvent struct {
|
||||
Options kitty.Options
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// KittyEnhancementsEvent represents a Kitty enhancements event.
|
||||
type KittyEnhancementsEvent int
|
||||
|
||||
// Kitty keyboard enhancement constants.
|
||||
// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
|
||||
const (
|
||||
KittyDisambiguateEscapeCodes KittyEnhancementsEvent = 1 << iota
|
||||
KittyReportEventTypes
|
||||
KittyReportAlternateKeys
|
||||
KittyReportAllKeysAsEscapeCodes
|
||||
KittyReportAssociatedText
|
||||
)
|
||||
|
||||
// Contains reports whether m contains the given enhancements.
|
||||
func (e KittyEnhancementsEvent) Contains(enhancements KittyEnhancementsEvent) bool {
|
||||
return e&enhancements == enhancements
|
||||
}
|
||||
|
||||
// Kitty Clipboard Control Sequences.
|
||||
var kittyKeyMap = map[int]Key{
|
||||
ansi.BS: {Code: KeyBackspace},
|
||||
ansi.HT: {Code: KeyTab},
|
||||
ansi.CR: {Code: KeyEnter},
|
||||
ansi.ESC: {Code: KeyEscape},
|
||||
ansi.DEL: {Code: KeyBackspace},
|
||||
|
||||
57344: {Code: KeyEscape},
|
||||
57345: {Code: KeyEnter},
|
||||
57346: {Code: KeyTab},
|
||||
57347: {Code: KeyBackspace},
|
||||
57348: {Code: KeyInsert},
|
||||
57349: {Code: KeyDelete},
|
||||
57350: {Code: KeyLeft},
|
||||
57351: {Code: KeyRight},
|
||||
57352: {Code: KeyUp},
|
||||
57353: {Code: KeyDown},
|
||||
57354: {Code: KeyPgUp},
|
||||
57355: {Code: KeyPgDown},
|
||||
57356: {Code: KeyHome},
|
||||
57357: {Code: KeyEnd},
|
||||
57358: {Code: KeyCapsLock},
|
||||
57359: {Code: KeyScrollLock},
|
||||
57360: {Code: KeyNumLock},
|
||||
57361: {Code: KeyPrintScreen},
|
||||
57362: {Code: KeyPause},
|
||||
57363: {Code: KeyMenu},
|
||||
57364: {Code: KeyF1},
|
||||
57365: {Code: KeyF2},
|
||||
57366: {Code: KeyF3},
|
||||
57367: {Code: KeyF4},
|
||||
57368: {Code: KeyF5},
|
||||
57369: {Code: KeyF6},
|
||||
57370: {Code: KeyF7},
|
||||
57371: {Code: KeyF8},
|
||||
57372: {Code: KeyF9},
|
||||
57373: {Code: KeyF10},
|
||||
57374: {Code: KeyF11},
|
||||
57375: {Code: KeyF12},
|
||||
57376: {Code: KeyF13},
|
||||
57377: {Code: KeyF14},
|
||||
57378: {Code: KeyF15},
|
||||
57379: {Code: KeyF16},
|
||||
57380: {Code: KeyF17},
|
||||
57381: {Code: KeyF18},
|
||||
57382: {Code: KeyF19},
|
||||
57383: {Code: KeyF20},
|
||||
57384: {Code: KeyF21},
|
||||
57385: {Code: KeyF22},
|
||||
57386: {Code: KeyF23},
|
||||
57387: {Code: KeyF24},
|
||||
57388: {Code: KeyF25},
|
||||
57389: {Code: KeyF26},
|
||||
57390: {Code: KeyF27},
|
||||
57391: {Code: KeyF28},
|
||||
57392: {Code: KeyF29},
|
||||
57393: {Code: KeyF30},
|
||||
57394: {Code: KeyF31},
|
||||
57395: {Code: KeyF32},
|
||||
57396: {Code: KeyF33},
|
||||
57397: {Code: KeyF34},
|
||||
57398: {Code: KeyF35},
|
||||
57399: {Code: KeyKp0},
|
||||
57400: {Code: KeyKp1},
|
||||
57401: {Code: KeyKp2},
|
||||
57402: {Code: KeyKp3},
|
||||
57403: {Code: KeyKp4},
|
||||
57404: {Code: KeyKp5},
|
||||
57405: {Code: KeyKp6},
|
||||
57406: {Code: KeyKp7},
|
||||
57407: {Code: KeyKp8},
|
||||
57408: {Code: KeyKp9},
|
||||
57409: {Code: KeyKpDecimal},
|
||||
57410: {Code: KeyKpDivide},
|
||||
57411: {Code: KeyKpMultiply},
|
||||
57412: {Code: KeyKpMinus},
|
||||
57413: {Code: KeyKpPlus},
|
||||
57414: {Code: KeyKpEnter},
|
||||
57415: {Code: KeyKpEqual},
|
||||
57416: {Code: KeyKpSep},
|
||||
57417: {Code: KeyKpLeft},
|
||||
57418: {Code: KeyKpRight},
|
||||
57419: {Code: KeyKpUp},
|
||||
57420: {Code: KeyKpDown},
|
||||
57421: {Code: KeyKpPgUp},
|
||||
57422: {Code: KeyKpPgDown},
|
||||
57423: {Code: KeyKpHome},
|
||||
57424: {Code: KeyKpEnd},
|
||||
57425: {Code: KeyKpInsert},
|
||||
57426: {Code: KeyKpDelete},
|
||||
57427: {Code: KeyKpBegin},
|
||||
57428: {Code: KeyMediaPlay},
|
||||
57429: {Code: KeyMediaPause},
|
||||
57430: {Code: KeyMediaPlayPause},
|
||||
57431: {Code: KeyMediaReverse},
|
||||
57432: {Code: KeyMediaStop},
|
||||
57433: {Code: KeyMediaFastForward},
|
||||
57434: {Code: KeyMediaRewind},
|
||||
57435: {Code: KeyMediaNext},
|
||||
57436: {Code: KeyMediaPrev},
|
||||
57437: {Code: KeyMediaRecord},
|
||||
57438: {Code: KeyLowerVol},
|
||||
57439: {Code: KeyRaiseVol},
|
||||
57440: {Code: KeyMute},
|
||||
57441: {Code: KeyLeftShift},
|
||||
57442: {Code: KeyLeftCtrl},
|
||||
57443: {Code: KeyLeftAlt},
|
||||
57444: {Code: KeyLeftSuper},
|
||||
57445: {Code: KeyLeftHyper},
|
||||
57446: {Code: KeyLeftMeta},
|
||||
57447: {Code: KeyRightShift},
|
||||
57448: {Code: KeyRightCtrl},
|
||||
57449: {Code: KeyRightAlt},
|
||||
57450: {Code: KeyRightSuper},
|
||||
57451: {Code: KeyRightHyper},
|
||||
57452: {Code: KeyRightMeta},
|
||||
57453: {Code: KeyIsoLevel3Shift},
|
||||
57454: {Code: KeyIsoLevel5Shift},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// These are some faulty C0 mappings some terminals such as WezTerm have
|
||||
// and doesn't follow the specs.
|
||||
kittyKeyMap[ansi.NUL] = Key{Code: KeySpace, Mod: ModCtrl}
|
||||
for i := ansi.SOH; i <= ansi.SUB; i++ {
|
||||
if _, ok := kittyKeyMap[i]; !ok {
|
||||
kittyKeyMap[i] = Key{Code: rune(i + 0x60), Mod: ModCtrl}
|
||||
}
|
||||
}
|
||||
for i := ansi.FS; i <= ansi.US; i++ {
|
||||
if _, ok := kittyKeyMap[i]; !ok {
|
||||
kittyKeyMap[i] = Key{Code: rune(i + 0x40), Mod: ModCtrl}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
kittyShift = 1 << iota
|
||||
kittyAlt
|
||||
kittyCtrl
|
||||
kittySuper
|
||||
kittyHyper
|
||||
kittyMeta
|
||||
kittyCapsLock
|
||||
kittyNumLock
|
||||
)
|
||||
|
||||
func fromKittyMod(mod int) KeyMod {
|
||||
var m KeyMod
|
||||
if mod&kittyShift != 0 {
|
||||
m |= ModShift
|
||||
}
|
||||
if mod&kittyAlt != 0 {
|
||||
m |= ModAlt
|
||||
}
|
||||
if mod&kittyCtrl != 0 {
|
||||
m |= ModCtrl
|
||||
}
|
||||
if mod&kittySuper != 0 {
|
||||
m |= ModSuper
|
||||
}
|
||||
if mod&kittyHyper != 0 {
|
||||
m |= ModHyper
|
||||
}
|
||||
if mod&kittyMeta != 0 {
|
||||
m |= ModMeta
|
||||
}
|
||||
if mod&kittyCapsLock != 0 {
|
||||
m |= ModCapsLock
|
||||
}
|
||||
if mod&kittyNumLock != 0 {
|
||||
m |= ModNumLock
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// parseKittyKeyboard parses a Kitty Keyboard Protocol sequence.
|
||||
//
|
||||
// In `CSI u`, this is parsed as:
|
||||
//
|
||||
// CSI codepoint ; modifiers u
|
||||
// codepoint: ASCII Dec value
|
||||
//
|
||||
// The Kitty Keyboard Protocol extends this with optional components that can be
|
||||
// enabled progressively. The full sequence is parsed as:
|
||||
//
|
||||
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
|
||||
//
|
||||
// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||
func parseKittyKeyboard(params ansi.Params) (Event Event) {
|
||||
var isRelease bool
|
||||
var key Key
|
||||
|
||||
// The index of parameters separated by semicolons ';'. Sub parameters are
|
||||
// separated by colons ':'.
|
||||
var paramIdx int
|
||||
var sudIdx int // The sub parameter index
|
||||
for _, p := range params {
|
||||
// Kitty Keyboard Protocol has 3 optional components.
|
||||
switch paramIdx {
|
||||
case 0:
|
||||
switch sudIdx {
|
||||
case 0:
|
||||
var foundKey bool
|
||||
code := p.Param(1) // CSI u has a default value of 1
|
||||
key, foundKey = kittyKeyMap[code]
|
||||
if !foundKey {
|
||||
r := rune(code)
|
||||
if !utf8.ValidRune(r) {
|
||||
r = utf8.RuneError
|
||||
}
|
||||
|
||||
key.Code = r
|
||||
}
|
||||
|
||||
case 2:
|
||||
// shifted key + base key
|
||||
if b := rune(p.Param(1)); unicode.IsPrint(b) {
|
||||
// XXX: When alternate key reporting is enabled, the protocol
|
||||
// can return 3 things, the unicode codepoint of the key,
|
||||
// the shifted codepoint of the key, and the standard
|
||||
// PC-101 key layout codepoint.
|
||||
// This is useful to create an unambiguous mapping of keys
|
||||
// when using a different language layout.
|
||||
key.BaseCode = b
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case 1:
|
||||
// shifted key
|
||||
if s := rune(p.Param(1)); unicode.IsPrint(s) {
|
||||
// XXX: We swap keys here because we want the shifted key
|
||||
// to be the Rune that is returned by the event.
|
||||
// For example, shift+a should produce "A" not "a".
|
||||
// In such a case, we set AltRune to the original key "a"
|
||||
// and Rune to "A".
|
||||
key.ShiftedCode = s
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
switch sudIdx {
|
||||
case 0:
|
||||
mod := p.Param(1)
|
||||
if mod > 1 {
|
||||
key.Mod = fromKittyMod(mod - 1)
|
||||
if key.Mod > ModShift {
|
||||
// XXX: We need to clear the text if we have a modifier key
|
||||
// other than a [ModShift] key.
|
||||
key.Text = ""
|
||||
}
|
||||
}
|
||||
|
||||
case 1:
|
||||
switch p.Param(1) {
|
||||
case 2:
|
||||
key.IsRepeat = true
|
||||
case 3:
|
||||
isRelease = true
|
||||
}
|
||||
case 2:
|
||||
}
|
||||
case 2:
|
||||
if code := p.Param(0); code != 0 {
|
||||
key.Text += string(rune(code))
|
||||
}
|
||||
}
|
||||
|
||||
sudIdx++
|
||||
if !p.HasMore() {
|
||||
paramIdx++
|
||||
sudIdx = 0
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:nestif
|
||||
if len(key.Text) == 0 && unicode.IsPrint(key.Code) &&
|
||||
(key.Mod <= ModShift || key.Mod == ModCapsLock || key.Mod == ModShift|ModCapsLock) {
|
||||
if key.Mod == 0 {
|
||||
key.Text = string(key.Code)
|
||||
} else {
|
||||
desiredCase := unicode.ToLower
|
||||
if key.Mod.Contains(ModShift) || key.Mod.Contains(ModCapsLock) {
|
||||
desiredCase = unicode.ToUpper
|
||||
}
|
||||
if key.ShiftedCode != 0 {
|
||||
key.Text = string(key.ShiftedCode)
|
||||
} else {
|
||||
key.Text = string(desiredCase(key.Code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isRelease {
|
||||
return KeyReleaseEvent(key)
|
||||
}
|
||||
|
||||
return KeyPressEvent(key)
|
||||
}
|
||||
|
||||
// parseKittyKeyboardExt parses a Kitty Keyboard Protocol sequence extensions
|
||||
// for non CSI u sequences. This includes things like CSI A, SS3 A and others,
|
||||
// and CSI ~.
|
||||
func parseKittyKeyboardExt(params ansi.Params, k KeyPressEvent) Event {
|
||||
// Handle Kitty keyboard protocol
|
||||
if len(params) > 2 && // We have at least 3 parameters
|
||||
params[0].Param(1) == 1 && // The first parameter is 1 (defaults to 1)
|
||||
params[1].HasMore() { // The second parameter is a subparameter (separated by a ":")
|
||||
switch params[2].Param(1) { // The third parameter is the event type (defaults to 1)
|
||||
case 2:
|
||||
k.IsRepeat = true
|
||||
case 3:
|
||||
return KeyReleaseEvent(k)
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
37
packages/client/tui/input/mod.go
Normal file
37
packages/client/tui/input/mod.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package input
|
||||
|
||||
// KeyMod represents modifier keys.
|
||||
type KeyMod int
|
||||
|
||||
// Modifier keys.
|
||||
const (
|
||||
ModShift KeyMod = 1 << iota
|
||||
ModAlt
|
||||
ModCtrl
|
||||
ModMeta
|
||||
|
||||
// These modifiers are used with the Kitty protocol.
|
||||
// XXX: Meta and Super are swapped in the Kitty protocol,
|
||||
// this is to preserve compatibility with XTerm modifiers.
|
||||
|
||||
ModHyper
|
||||
ModSuper // Windows/Command keys
|
||||
|
||||
// These are key lock states.
|
||||
|
||||
ModCapsLock
|
||||
ModNumLock
|
||||
ModScrollLock // Defined in Windows API only
|
||||
)
|
||||
|
||||
// Contains reports whether m contains the given modifiers.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := ModAlt | ModCtrl
|
||||
// m.Contains(ModCtrl) // true
|
||||
// m.Contains(ModAlt | ModCtrl) // true
|
||||
// m.Contains(ModAlt | ModCtrl | ModShift) // false
|
||||
func (m KeyMod) Contains(mods KeyMod) bool {
|
||||
return m&mods == mods
|
||||
}
|
||||
14
packages/client/tui/input/mode.go
Normal file
14
packages/client/tui/input/mode.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package input
|
||||
|
||||
import "github.com/charmbracelet/x/ansi"
|
||||
|
||||
// ModeReportEvent is a message that represents a mode report event (DECRPM).
|
||||
//
|
||||
// See: https://vt100.net/docs/vt510-rm/DECRPM.html
|
||||
type ModeReportEvent struct {
|
||||
// Mode is the mode number.
|
||||
Mode ansi.Mode
|
||||
|
||||
// Value is the mode value.
|
||||
Value ansi.ModeSetting
|
||||
}
|
||||
292
packages/client/tui/input/mouse.go
Normal file
292
packages/client/tui/input/mouse.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
// MouseButton represents the button that was pressed during a mouse message.
|
||||
type MouseButton = ansi.MouseButton
|
||||
|
||||
// Mouse event buttons
|
||||
//
|
||||
// This is based on X11 mouse button codes.
|
||||
//
|
||||
// 1 = left button
|
||||
// 2 = middle button (pressing the scroll wheel)
|
||||
// 3 = right button
|
||||
// 4 = turn scroll wheel up
|
||||
// 5 = turn scroll wheel down
|
||||
// 6 = push scroll wheel left
|
||||
// 7 = push scroll wheel right
|
||||
// 8 = 4th button (aka browser backward button)
|
||||
// 9 = 5th button (aka browser forward button)
|
||||
// 10
|
||||
// 11
|
||||
//
|
||||
// Other buttons are not supported.
|
||||
const (
|
||||
MouseNone = ansi.MouseNone
|
||||
MouseLeft = ansi.MouseLeft
|
||||
MouseMiddle = ansi.MouseMiddle
|
||||
MouseRight = ansi.MouseRight
|
||||
MouseWheelUp = ansi.MouseWheelUp
|
||||
MouseWheelDown = ansi.MouseWheelDown
|
||||
MouseWheelLeft = ansi.MouseWheelLeft
|
||||
MouseWheelRight = ansi.MouseWheelRight
|
||||
MouseBackward = ansi.MouseBackward
|
||||
MouseForward = ansi.MouseForward
|
||||
MouseButton10 = ansi.MouseButton10
|
||||
MouseButton11 = ansi.MouseButton11
|
||||
)
|
||||
|
||||
// MouseEvent represents a mouse message. This is a generic mouse message that
|
||||
// can represent any kind of mouse event.
|
||||
type MouseEvent interface {
|
||||
fmt.Stringer
|
||||
|
||||
// Mouse returns the underlying mouse event.
|
||||
Mouse() Mouse
|
||||
}
|
||||
|
||||
// Mouse represents a Mouse message. Use [MouseEvent] to represent all mouse
|
||||
// messages.
|
||||
//
|
||||
// The X and Y coordinates are zero-based, with (0,0) being the upper left
|
||||
// corner of the terminal.
|
||||
//
|
||||
// // Catch all mouse events
|
||||
// switch Event := Event.(type) {
|
||||
// case MouseEvent:
|
||||
// m := Event.Mouse()
|
||||
// fmt.Println("Mouse event:", m.X, m.Y, m)
|
||||
// }
|
||||
//
|
||||
// // Only catch mouse click events
|
||||
// switch Event := Event.(type) {
|
||||
// case MouseClickEvent:
|
||||
// fmt.Println("Mouse click event:", Event.X, Event.Y, Event)
|
||||
// }
|
||||
type Mouse struct {
|
||||
X, Y int
|
||||
Button MouseButton
|
||||
Mod KeyMod
|
||||
}
|
||||
|
||||
// String returns a string representation of the mouse message.
|
||||
func (m Mouse) String() (s string) {
|
||||
if m.Mod.Contains(ModCtrl) {
|
||||
s += "ctrl+"
|
||||
}
|
||||
if m.Mod.Contains(ModAlt) {
|
||||
s += "alt+"
|
||||
}
|
||||
if m.Mod.Contains(ModShift) {
|
||||
s += "shift+"
|
||||
}
|
||||
|
||||
str := m.Button.String()
|
||||
if str == "" {
|
||||
s += "unknown"
|
||||
} else if str != "none" { // motion events don't have a button
|
||||
s += str
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// MouseClickEvent represents a mouse button click event.
|
||||
type MouseClickEvent Mouse
|
||||
|
||||
// String returns a string representation of the mouse click event.
|
||||
func (e MouseClickEvent) String() string {
|
||||
return Mouse(e).String()
|
||||
}
|
||||
|
||||
// Mouse returns the underlying mouse event. This is a convenience method and
|
||||
// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
|
||||
// event to [Mouse].
|
||||
func (e MouseClickEvent) Mouse() Mouse {
|
||||
return Mouse(e)
|
||||
}
|
||||
|
||||
// MouseReleaseEvent represents a mouse button release event.
|
||||
type MouseReleaseEvent Mouse
|
||||
|
||||
// String returns a string representation of the mouse release event.
|
||||
func (e MouseReleaseEvent) String() string {
|
||||
return Mouse(e).String()
|
||||
}
|
||||
|
||||
// Mouse returns the underlying mouse event. This is a convenience method and
|
||||
// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
|
||||
// event to [Mouse].
|
||||
func (e MouseReleaseEvent) Mouse() Mouse {
|
||||
return Mouse(e)
|
||||
}
|
||||
|
||||
// MouseWheelEvent represents a mouse wheel message event.
|
||||
type MouseWheelEvent Mouse
|
||||
|
||||
// String returns a string representation of the mouse wheel event.
|
||||
func (e MouseWheelEvent) String() string {
|
||||
return Mouse(e).String()
|
||||
}
|
||||
|
||||
// Mouse returns the underlying mouse event. This is a convenience method and
|
||||
// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
|
||||
// event to [Mouse].
|
||||
func (e MouseWheelEvent) Mouse() Mouse {
|
||||
return Mouse(e)
|
||||
}
|
||||
|
||||
// MouseMotionEvent represents a mouse motion event.
|
||||
type MouseMotionEvent Mouse
|
||||
|
||||
// String returns a string representation of the mouse motion event.
|
||||
func (e MouseMotionEvent) String() string {
|
||||
m := Mouse(e)
|
||||
if m.Button != 0 {
|
||||
return m.String() + "+motion"
|
||||
}
|
||||
return m.String() + "motion"
|
||||
}
|
||||
|
||||
// Mouse returns the underlying mouse event. This is a convenience method and
|
||||
// syntactic sugar to satisfy the [MouseEvent] interface, and cast the mouse
|
||||
// event to [Mouse].
|
||||
func (e MouseMotionEvent) Mouse() Mouse {
|
||||
return Mouse(e)
|
||||
}
|
||||
|
||||
// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
|
||||
// look like:
|
||||
//
|
||||
// ESC [ < Cb ; Cx ; Cy (M or m)
|
||||
//
|
||||
// where:
|
||||
//
|
||||
// Cb is the encoded button code
|
||||
// Cx is the x-coordinate of the mouse
|
||||
// Cy is the y-coordinate of the mouse
|
||||
// M is for button press, m is for button release
|
||||
//
|
||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
|
||||
func parseSGRMouseEvent(cmd ansi.Cmd, params ansi.Params) Event {
|
||||
x, _, ok := params.Param(1, 1)
|
||||
if !ok {
|
||||
x = 1
|
||||
}
|
||||
y, _, ok := params.Param(2, 1)
|
||||
if !ok {
|
||||
y = 1
|
||||
}
|
||||
release := cmd.Final() == 'm'
|
||||
b, _, _ := params.Param(0, 0)
|
||||
mod, btn, _, isMotion := parseMouseButton(b)
|
||||
|
||||
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||
x--
|
||||
y--
|
||||
|
||||
m := Mouse{X: x, Y: y, Button: btn, Mod: mod}
|
||||
|
||||
// Wheel buttons don't have release events
|
||||
// Motion can be reported as a release event in some terminals (Windows Terminal)
|
||||
if isWheel(m.Button) {
|
||||
return MouseWheelEvent(m)
|
||||
} else if !isMotion && release {
|
||||
return MouseReleaseEvent(m)
|
||||
} else if isMotion {
|
||||
return MouseMotionEvent(m)
|
||||
}
|
||||
return MouseClickEvent(m)
|
||||
}
|
||||
|
||||
const x10MouseByteOffset = 32
|
||||
|
||||
// Parse X10-encoded mouse events; the simplest kind. The last release of X10
|
||||
// was December 1986, by the way. The original X10 mouse protocol limits the Cx
|
||||
// and Cy coordinates to 223 (=255-032).
|
||||
//
|
||||
// X10 mouse events look like:
|
||||
//
|
||||
// ESC [M Cb Cx Cy
|
||||
//
|
||||
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
||||
func parseX10MouseEvent(buf []byte) Event {
|
||||
v := buf[3:6]
|
||||
b := int(v[0])
|
||||
if b >= x10MouseByteOffset {
|
||||
// XXX: b < 32 should be impossible, but we're being defensive.
|
||||
b -= x10MouseByteOffset
|
||||
}
|
||||
|
||||
mod, btn, isRelease, isMotion := parseMouseButton(b)
|
||||
|
||||
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||
x := int(v[1]) - x10MouseByteOffset - 1
|
||||
y := int(v[2]) - x10MouseByteOffset - 1
|
||||
|
||||
m := Mouse{X: x, Y: y, Button: btn, Mod: mod}
|
||||
if isWheel(m.Button) {
|
||||
return MouseWheelEvent(m)
|
||||
} else if isMotion {
|
||||
return MouseMotionEvent(m)
|
||||
} else if isRelease {
|
||||
return MouseReleaseEvent(m)
|
||||
}
|
||||
return MouseClickEvent(m)
|
||||
}
|
||||
|
||||
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
|
||||
func parseMouseButton(b int) (mod KeyMod, btn MouseButton, isRelease bool, isMotion bool) {
|
||||
// mouse bit shifts
|
||||
const (
|
||||
bitShift = 0b0000_0100
|
||||
bitAlt = 0b0000_1000
|
||||
bitCtrl = 0b0001_0000
|
||||
bitMotion = 0b0010_0000
|
||||
bitWheel = 0b0100_0000
|
||||
bitAdd = 0b1000_0000 // additional buttons 8-11
|
||||
|
||||
bitsMask = 0b0000_0011
|
||||
)
|
||||
|
||||
// Modifiers
|
||||
if b&bitAlt != 0 {
|
||||
mod |= ModAlt
|
||||
}
|
||||
if b&bitCtrl != 0 {
|
||||
mod |= ModCtrl
|
||||
}
|
||||
if b&bitShift != 0 {
|
||||
mod |= ModShift
|
||||
}
|
||||
|
||||
if b&bitAdd != 0 {
|
||||
btn = MouseBackward + MouseButton(b&bitsMask)
|
||||
} else if b&bitWheel != 0 {
|
||||
btn = MouseWheelUp + MouseButton(b&bitsMask)
|
||||
} else {
|
||||
btn = MouseLeft + MouseButton(b&bitsMask)
|
||||
// X10 reports a button release as 0b0000_0011 (3)
|
||||
if b&bitsMask == bitsMask {
|
||||
btn = MouseNone
|
||||
isRelease = true
|
||||
}
|
||||
}
|
||||
|
||||
// Motion bit doesn't get reported for wheel events.
|
||||
if b&bitMotion != 0 && !isWheel(btn) {
|
||||
isMotion = true
|
||||
}
|
||||
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
// isWheel returns true if the mouse event is a wheel event.
|
||||
func isWheel(btn MouseButton) bool {
|
||||
return btn >= MouseWheelUp && btn <= MouseWheelRight
|
||||
}
|
||||
481
packages/client/tui/input/mouse_test.go
Normal file
481
packages/client/tui/input/mouse_test.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/charmbracelet/x/ansi/parser"
|
||||
)
|
||||
|
||||
func TestMouseEvent_String(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
event Event
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "unknown",
|
||||
event: MouseClickEvent{Button: MouseButton(0xff)},
|
||||
expected: "unknown",
|
||||
},
|
||||
{
|
||||
name: "left",
|
||||
event: MouseClickEvent{Button: MouseLeft},
|
||||
expected: "left",
|
||||
},
|
||||
{
|
||||
name: "right",
|
||||
event: MouseClickEvent{Button: MouseRight},
|
||||
expected: "right",
|
||||
},
|
||||
{
|
||||
name: "middle",
|
||||
event: MouseClickEvent{Button: MouseMiddle},
|
||||
expected: "middle",
|
||||
},
|
||||
{
|
||||
name: "release",
|
||||
event: MouseReleaseEvent{Button: MouseNone},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "wheelup",
|
||||
event: MouseWheelEvent{Button: MouseWheelUp},
|
||||
expected: "wheelup",
|
||||
},
|
||||
{
|
||||
name: "wheeldown",
|
||||
event: MouseWheelEvent{Button: MouseWheelDown},
|
||||
expected: "wheeldown",
|
||||
},
|
||||
{
|
||||
name: "wheelleft",
|
||||
event: MouseWheelEvent{Button: MouseWheelLeft},
|
||||
expected: "wheelleft",
|
||||
},
|
||||
{
|
||||
name: "wheelright",
|
||||
event: MouseWheelEvent{Button: MouseWheelRight},
|
||||
expected: "wheelright",
|
||||
},
|
||||
{
|
||||
name: "motion",
|
||||
event: MouseMotionEvent{Button: MouseNone},
|
||||
expected: "motion",
|
||||
},
|
||||
{
|
||||
name: "shift+left",
|
||||
event: MouseReleaseEvent{Button: MouseLeft, Mod: ModShift},
|
||||
expected: "shift+left",
|
||||
},
|
||||
{
|
||||
name: "shift+left", event: MouseClickEvent{Button: MouseLeft, Mod: ModShift},
|
||||
expected: "shift+left",
|
||||
},
|
||||
{
|
||||
name: "ctrl+shift+left",
|
||||
event: MouseClickEvent{Button: MouseLeft, Mod: ModCtrl | ModShift},
|
||||
expected: "ctrl+shift+left",
|
||||
},
|
||||
{
|
||||
name: "alt+left",
|
||||
event: MouseClickEvent{Button: MouseLeft, Mod: ModAlt},
|
||||
expected: "alt+left",
|
||||
},
|
||||
{
|
||||
name: "ctrl+left",
|
||||
event: MouseClickEvent{Button: MouseLeft, Mod: ModCtrl},
|
||||
expected: "ctrl+left",
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+left",
|
||||
event: MouseClickEvent{Button: MouseLeft, Mod: ModAlt | ModCtrl},
|
||||
expected: "ctrl+alt+left",
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+shift+left",
|
||||
event: MouseClickEvent{Button: MouseLeft, Mod: ModAlt | ModCtrl | ModShift},
|
||||
expected: "ctrl+alt+shift+left",
|
||||
},
|
||||
{
|
||||
name: "ignore coordinates",
|
||||
event: MouseClickEvent{X: 100, Y: 200, Button: MouseLeft},
|
||||
expected: "left",
|
||||
},
|
||||
{
|
||||
name: "broken type",
|
||||
event: MouseClickEvent{Button: MouseButton(120)},
|
||||
expected: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tt {
|
||||
tc := tt[i]
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := fmt.Sprint(tc.event)
|
||||
|
||||
if tc.expected != actual {
|
||||
t.Fatalf("expected %q but got %q",
|
||||
tc.expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseX10MouseDownEvent(t *testing.T) {
|
||||
encode := func(b byte, x, y int) []byte {
|
||||
return []byte{
|
||||
'\x1b',
|
||||
'[',
|
||||
'M',
|
||||
byte(32) + b,
|
||||
byte(x + 32 + 1),
|
||||
byte(y + 32 + 1),
|
||||
}
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
buf []byte
|
||||
expected Event
|
||||
}{
|
||||
// Position.
|
||||
{
|
||||
name: "zero position",
|
||||
buf: encode(0b0000_0000, 0, 0),
|
||||
expected: MouseClickEvent{X: 0, Y: 0, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "max position",
|
||||
buf: encode(0b0000_0000, 222, 222), // Because 255 (max int8) - 32 - 1.
|
||||
expected: MouseClickEvent{X: 222, Y: 222, Button: MouseLeft},
|
||||
},
|
||||
// Simple.
|
||||
{
|
||||
name: "left",
|
||||
buf: encode(0b0000_0000, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "left in motion",
|
||||
buf: encode(0b0010_0000, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "middle",
|
||||
buf: encode(0b0000_0001, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseMiddle},
|
||||
},
|
||||
{
|
||||
name: "middle in motion",
|
||||
buf: encode(0b0010_0001, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseMiddle},
|
||||
},
|
||||
{
|
||||
name: "right",
|
||||
buf: encode(0b0000_0010, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "right in motion",
|
||||
buf: encode(0b0010_0010, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "motion",
|
||||
buf: encode(0b0010_0011, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseNone},
|
||||
},
|
||||
{
|
||||
name: "wheel up",
|
||||
buf: encode(0b0100_0000, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp},
|
||||
},
|
||||
{
|
||||
name: "wheel down",
|
||||
buf: encode(0b0100_0001, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelDown},
|
||||
},
|
||||
{
|
||||
name: "wheel left",
|
||||
buf: encode(0b0100_0010, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelLeft},
|
||||
},
|
||||
{
|
||||
name: "wheel right",
|
||||
buf: encode(0b0100_0011, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelRight},
|
||||
},
|
||||
{
|
||||
name: "release",
|
||||
buf: encode(0b0000_0011, 32, 16),
|
||||
expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseNone},
|
||||
},
|
||||
{
|
||||
name: "backward",
|
||||
buf: encode(0b1000_0000, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseBackward},
|
||||
},
|
||||
{
|
||||
name: "forward",
|
||||
buf: encode(0b1000_0001, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseForward},
|
||||
},
|
||||
{
|
||||
name: "button 10",
|
||||
buf: encode(0b1000_0010, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseButton10},
|
||||
},
|
||||
{
|
||||
name: "button 11",
|
||||
buf: encode(0b1000_0011, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseButton11},
|
||||
},
|
||||
// Combinations.
|
||||
{
|
||||
name: "alt+right",
|
||||
buf: encode(0b0000_1010, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "ctrl+right",
|
||||
buf: encode(0b0001_0010, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "left in motion",
|
||||
buf: encode(0b0010_0000, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "alt+right in motion",
|
||||
buf: encode(0b0010_1010, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "ctrl+right in motion",
|
||||
buf: encode(0b0011_0010, 32, 16),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+right",
|
||||
buf: encode(0b0001_1010, 32, 16),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "ctrl+wheel up",
|
||||
buf: encode(0b0101_0000, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseWheelUp},
|
||||
},
|
||||
{
|
||||
name: "alt+wheel down",
|
||||
buf: encode(0b0100_1001, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseWheelDown},
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+wheel down",
|
||||
buf: encode(0b0101_1001, 32, 16),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseWheelDown},
|
||||
},
|
||||
// Overflow position.
|
||||
{
|
||||
name: "overflow position",
|
||||
buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1.
|
||||
expected: MouseMotionEvent{X: -6, Y: -33, Button: MouseLeft},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tt {
|
||||
tc := tt[i]
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := parseX10MouseEvent(tc.buf)
|
||||
|
||||
if tc.expected != actual {
|
||||
t.Fatalf("expected %#v but got %#v",
|
||||
tc.expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSGRMouseEvent(t *testing.T) {
|
||||
type csiSequence struct {
|
||||
params []ansi.Param
|
||||
cmd ansi.Cmd
|
||||
}
|
||||
encode := func(b, x, y int, r bool) *csiSequence {
|
||||
re := 'M'
|
||||
if r {
|
||||
re = 'm'
|
||||
}
|
||||
return &csiSequence{
|
||||
params: []ansi.Param{
|
||||
ansi.Param(b),
|
||||
ansi.Param(x + 1),
|
||||
ansi.Param(y + 1),
|
||||
},
|
||||
cmd: ansi.Cmd(re) | ('<' << parser.PrefixShift),
|
||||
}
|
||||
}
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
buf *csiSequence
|
||||
expected Event
|
||||
}{
|
||||
// Position.
|
||||
{
|
||||
name: "zero position",
|
||||
buf: encode(0, 0, 0, false),
|
||||
expected: MouseClickEvent{X: 0, Y: 0, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "225 position",
|
||||
buf: encode(0, 225, 225, false),
|
||||
expected: MouseClickEvent{X: 225, Y: 225, Button: MouseLeft},
|
||||
},
|
||||
// Simple.
|
||||
{
|
||||
name: "left",
|
||||
buf: encode(0, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "left in motion",
|
||||
buf: encode(32, 32, 16, false),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "left",
|
||||
buf: encode(0, 32, 16, true),
|
||||
expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseLeft},
|
||||
},
|
||||
{
|
||||
name: "middle",
|
||||
buf: encode(1, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseMiddle},
|
||||
},
|
||||
{
|
||||
name: "middle in motion",
|
||||
buf: encode(33, 32, 16, false),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseMiddle},
|
||||
},
|
||||
{
|
||||
name: "middle",
|
||||
buf: encode(1, 32, 16, true),
|
||||
expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseMiddle},
|
||||
},
|
||||
{
|
||||
name: "right",
|
||||
buf: encode(2, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "right",
|
||||
buf: encode(2, 32, 16, true),
|
||||
expected: MouseReleaseEvent{X: 32, Y: 16, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "motion",
|
||||
buf: encode(35, 32, 16, false),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseNone},
|
||||
},
|
||||
{
|
||||
name: "wheel up",
|
||||
buf: encode(64, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp},
|
||||
},
|
||||
{
|
||||
name: "wheel down",
|
||||
buf: encode(65, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelDown},
|
||||
},
|
||||
{
|
||||
name: "wheel left",
|
||||
buf: encode(66, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelLeft},
|
||||
},
|
||||
{
|
||||
name: "wheel right",
|
||||
buf: encode(67, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelRight},
|
||||
},
|
||||
{
|
||||
name: "backward",
|
||||
buf: encode(128, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseBackward},
|
||||
},
|
||||
{
|
||||
name: "backward in motion",
|
||||
buf: encode(160, 32, 16, false),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseBackward},
|
||||
},
|
||||
{
|
||||
name: "forward",
|
||||
buf: encode(129, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Button: MouseForward},
|
||||
},
|
||||
{
|
||||
name: "forward in motion",
|
||||
buf: encode(161, 32, 16, false),
|
||||
expected: MouseMotionEvent{X: 32, Y: 16, Button: MouseForward},
|
||||
},
|
||||
// Combinations.
|
||||
{
|
||||
name: "alt+right",
|
||||
buf: encode(10, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "ctrl+right",
|
||||
buf: encode(18, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+right",
|
||||
buf: encode(26, 32, 16, false),
|
||||
expected: MouseClickEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseRight},
|
||||
},
|
||||
{
|
||||
name: "alt+wheel",
|
||||
buf: encode(73, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt, Button: MouseWheelDown},
|
||||
},
|
||||
{
|
||||
name: "ctrl+wheel",
|
||||
buf: encode(81, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModCtrl, Button: MouseWheelDown},
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+wheel",
|
||||
buf: encode(89, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt | ModCtrl, Button: MouseWheelDown},
|
||||
},
|
||||
{
|
||||
name: "ctrl+alt+shift+wheel",
|
||||
buf: encode(93, 32, 16, false),
|
||||
expected: MouseWheelEvent{X: 32, Y: 16, Mod: ModAlt | ModShift | ModCtrl, Button: MouseWheelDown},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tt {
|
||||
tc := tt[i]
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := parseSGRMouseEvent(tc.buf.cmd, tc.buf.params)
|
||||
if tc.expected != actual {
|
||||
t.Fatalf("expected %#v but got %#v",
|
||||
tc.expected,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user