Merge origin/main into issue-122-import-browser-cookies-history-settings
|
|
@ -9,19 +9,29 @@ Full end-to-end release built locally. Bumps version, updates changelog, tags, t
|
|||
- Get the current version from `GhosttyTabs.xcodeproj/project.pbxproj` (look for `MARKETING_VERSION`)
|
||||
- Bump the minor version unless the user specifies otherwise (e.g., 0.54.0 → 0.55.0)
|
||||
|
||||
### 2. Gather changes since the last release
|
||||
### 2. Gather changes and contributors since the last release
|
||||
|
||||
- Find the most recent git tag: `git describe --tags --abbrev=0`
|
||||
- Get commits since that tag: `git log --oneline <last-tag>..HEAD --no-merges`
|
||||
- **Filter for end-user visible changes only** — ignore developer tooling, CI, docs, tests
|
||||
- Categorize changes into: Added, Changed, Fixed, Removed
|
||||
- If there are no user-facing changes, ask the user if they still want to release
|
||||
- **Collect contributors:** For each PR referenced in the commits, get the author:
|
||||
```bash
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Also check for linked issue reporters (the person who filed the bug):
|
||||
```bash
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Build a deduplicated list of all contributor `@handle`s for the release
|
||||
|
||||
### 3. Update the changelog
|
||||
|
||||
- Add a new section at the top of `CHANGELOG.md` with the new version and today's date
|
||||
- **Only include changes that affect the end-user experience**
|
||||
- Write clear, user-facing descriptions (not raw commit messages)
|
||||
- **Credit contributors inline** (see Contributor Credits below)
|
||||
- Also update `docs-site/content/docs/changelog.mdx` if it exists
|
||||
|
||||
### 4. Bump the version
|
||||
|
|
@ -72,3 +82,25 @@ If the script fails, run `say "cmux release failed"`.
|
|||
- Group by category: Added, Changed, Fixed, Removed
|
||||
- Be concise but descriptive
|
||||
- Focus on what the user experiences, not how it was implemented
|
||||
|
||||
## Contributor Credits
|
||||
|
||||
Credit the people who made each release happen. This builds community and encourages contributions.
|
||||
|
||||
**Per-entry attribution** — append contributor credit after each changelog bullet:
|
||||
- For code contributions (PR author): `— thanks @user!`
|
||||
- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!`
|
||||
- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline
|
||||
|
||||
**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release:
|
||||
```markdown
|
||||
### Thanks to N contributors!
|
||||
|
||||
- [@user1](https://github.com/user1)
|
||||
- [@user2](https://github.com/user2)
|
||||
```
|
||||
- List all contributors alphabetically by GitHub handle (including core team)
|
||||
- Link each handle to their GitHub profile
|
||||
- Include everyone: PR authors, issue reporters, anyone whose work is in the release
|
||||
|
||||
**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles.
|
||||
|
|
|
|||
|
|
@ -13,16 +13,26 @@ End-to-end release via PR flow: bump version, update changelog, create PR, merge
|
|||
2. **Create a release branch**
|
||||
- Create branch: `git checkout -b release/vX.Y.Z`
|
||||
|
||||
3. **Gather changes since the last release**
|
||||
3. **Gather changes and contributors since the last release**
|
||||
- Find the most recent git tag: `git describe --tags --abbrev=0`
|
||||
- Get commits since that tag: `git log --oneline <last-tag>..HEAD --no-merges`
|
||||
- **Filter for end-user visible changes only** - ignore developer tooling, CI, docs, tests
|
||||
- Categorize changes into: Added, Changed, Fixed, Removed
|
||||
- **Collect contributors:** For each PR referenced in the commits, get the author:
|
||||
```bash
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Also check for linked issue reporters (the person who filed the bug):
|
||||
```bash
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Build a deduplicated list of all contributor `@handle`s for the release
|
||||
|
||||
4. **Update the changelog**
|
||||
- Add a new section at the top of `CHANGELOG.md` with the new version and today's date
|
||||
- **Only include changes that affect the end-user experience**
|
||||
- Write clear, user-facing descriptions (not raw commit messages)
|
||||
- **Credit contributors inline** (see Contributor Credits below)
|
||||
- Also update `docs-site/content/docs/changelog.mdx` if it exists
|
||||
- If there are no user-facing changes, ask the user if they still want to release
|
||||
|
||||
|
|
@ -73,3 +83,25 @@ If the script fails, run `say "cmux release failed"`.
|
|||
- Test additions or fixes
|
||||
- Internal refactoring with no user-visible effect
|
||||
- Dependency updates (unless they fix a user-facing bug)
|
||||
|
||||
## Contributor Credits
|
||||
|
||||
Credit the people who made each release happen. This builds community and encourages contributions.
|
||||
|
||||
**Per-entry attribution** — append contributor credit after each changelog bullet:
|
||||
- For code contributions (PR author): `— thanks @user!`
|
||||
- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!`
|
||||
- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline
|
||||
|
||||
**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release:
|
||||
```markdown
|
||||
### Thanks to N contributors!
|
||||
|
||||
- [@user1](https://github.com/user1)
|
||||
- [@user2](https://github.com/user2)
|
||||
```
|
||||
- List all contributors alphabetically by GitHub handle (including core team)
|
||||
- Link each handle to their GitHub profile
|
||||
- Include everyone: PR authors, issue reporters, anyone whose work is in the release
|
||||
|
||||
**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles.
|
||||
|
|
|
|||
|
|
@ -11,16 +11,26 @@ Prepare a new release for cmux. This command updates the changelog, bumps the ve
|
|||
2. **Create a release branch**
|
||||
- Create branch: `git checkout -b release/vX.Y.Z`
|
||||
|
||||
3. **Gather changes since the last release**
|
||||
3. **Gather changes and contributors since the last release**
|
||||
- Find the most recent git tag: `git describe --tags --abbrev=0`
|
||||
- Get commits since that tag: `git log --oneline <last-tag>..HEAD --no-merges`
|
||||
- **Filter for end-user visible changes only** - ignore developer tooling, CI, docs, tests
|
||||
- Categorize changes into: Added, Changed, Fixed, Removed
|
||||
- **Collect contributors:** For each PR referenced in the commits, get the author:
|
||||
```bash
|
||||
gh pr view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Also check for linked issue reporters (the person who filed the bug):
|
||||
```bash
|
||||
gh issue view <N> --repo manaflow-ai/cmux --json author --jq '.author.login'
|
||||
```
|
||||
- Build a deduplicated list of all contributor `@handle`s for the release
|
||||
|
||||
4. **Update the changelog**
|
||||
- Add a new section at the top of `CHANGELOG.md` with the new version and today's date
|
||||
- **Only include changes that affect the end-user experience** - things users will see, feel, or interact with
|
||||
- Write clear, user-facing descriptions (not raw commit messages)
|
||||
- **Credit contributors inline** (see Contributor Credits below)
|
||||
- Also update `docs-site/content/docs/changelog.mdx` with the same content
|
||||
- If there are no user-facing changes, ask the user if they still want to release
|
||||
|
||||
|
|
@ -89,18 +99,47 @@ Prepare a new release for cmux. This command updates the changelog, bumps the ve
|
|||
- Focus on what the user experiences, not how it was implemented
|
||||
- Link to issues/PRs if relevant
|
||||
|
||||
## Contributor Credits
|
||||
|
||||
Credit the people who made each release happen. This builds community and encourages contributions.
|
||||
|
||||
**Per-entry attribution** — append contributor credit after each changelog bullet:
|
||||
- For code contributions (PR author): `— thanks @user!`
|
||||
- For bug reports (issue reporter, if different from PR author): `— thanks @reporter for the report!`
|
||||
- Core team (`lawrencecchen`, `austinywang`) contributions get no per-entry callout — core work is the baseline
|
||||
|
||||
**Summary section** — add a "Thanks to N contributors!" section at the bottom of each release:
|
||||
```markdown
|
||||
### Thanks to N contributors!
|
||||
|
||||
- [@user1](https://github.com/user1)
|
||||
- [@user2](https://github.com/user2)
|
||||
```
|
||||
- List all contributors alphabetically by GitHub handle (including core team)
|
||||
- Link each handle to their GitHub profile
|
||||
- Include everyone: PR authors, issue reporters, anyone whose work is in the release
|
||||
|
||||
**GitHub Release body** — when the release is published, the GitHub Release should also include the "Thanks to N contributors!" section with linked handles.
|
||||
|
||||
## Example Changelog Entry
|
||||
|
||||
```markdown
|
||||
## [0.13.0] - 2025-01-30
|
||||
|
||||
### Added
|
||||
- New keyboard shortcut for quick tab switching
|
||||
- New keyboard shortcut for quick tab switching ([#42](https://github.com/manaflow-ai/cmux/pull/42)) — thanks @contributor!
|
||||
|
||||
### Fixed
|
||||
- Memory leak when closing split panes
|
||||
- Notification badges not clearing properly
|
||||
- Memory leak when closing split panes ([#38](https://github.com/manaflow-ai/cmux/pull/38)) — thanks @fixer!
|
||||
- Notification badges not clearing properly ([#35](https://github.com/manaflow-ai/cmux/pull/35)) — thanks @reporter for the report!
|
||||
|
||||
### Changed
|
||||
- Improved terminal rendering performance
|
||||
- Improved terminal rendering performance ([#40](https://github.com/manaflow-ai/cmux/pull/40))
|
||||
|
||||
### Thanks to 4 contributors!
|
||||
|
||||
- [@contributor](https://github.com/contributor)
|
||||
- [@fixer](https://github.com/fixer)
|
||||
- [@lawrencechen](https://github.com/lawrencechen)
|
||||
- [@reporter](https://github.com/reporter)
|
||||
```
|
||||
|
|
|
|||
12
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -46,6 +46,18 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: nightly-repro
|
||||
attributes:
|
||||
label: Can you reproduce this on cmux NIGHTLY?
|
||||
description: "Please test with the latest NIGHTLY build first: https://github.com/manaflow-ai/cmux?tab=readme-ov-file#nightly-builds"
|
||||
options:
|
||||
- Yes, it still reproduces on NIGHTLY
|
||||
- No, it does not reproduce on NIGHTLY
|
||||
- I could not test NIGHTLY
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
|
|
|||
33
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
## Summary
|
||||
|
||||
- What changed?
|
||||
- Why?
|
||||
|
||||
## Testing
|
||||
|
||||
- How did you test this change?
|
||||
- What did you verify manually?
|
||||
|
||||
## Demo Video
|
||||
|
||||
For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).
|
||||
|
||||
- Video URL or attachment:
|
||||
|
||||
## Review Trigger (Copy/Paste as PR comment)
|
||||
|
||||
```text
|
||||
@codex review
|
||||
@coderabbitai review
|
||||
@greptile-apps review
|
||||
@cubic-dev-ai review
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I tested the change locally
|
||||
- [ ] I added or updated tests for behavior changes
|
||||
- [ ] I updated docs/changelog if needed
|
||||
- [ ] I requested bot reviews after my latest commit (copy/paste block above or equivalent)
|
||||
- [ ] All code review bot comments are resolved
|
||||
- [ ] All human review comments are resolved
|
||||
94
.github/workflows/build-ghosttykit.yml
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
name: Build GhosttyKit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-ghosttykit:
|
||||
# Never run Depot jobs for fork pull requests (avoid billing on external PRs).
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: depot-macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Get ghostty SHA
|
||||
id: ghostty-sha
|
||||
run: |
|
||||
SHA=$(git -C ghostty rev-parse HEAD)
|
||||
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Ghostty SHA: $SHA"
|
||||
|
||||
- name: Check if xcframework release already exists
|
||||
id: check-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="xcframework-${{ steps.ghostty-sha.outputs.sha }}"
|
||||
if gh release view "$TAG" --repo manaflow-ai/ghostty >/dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Release $TAG already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Release $TAG not found, will build"
|
||||
fi
|
||||
|
||||
- name: Select Xcode
|
||||
if: steps.check-release.outputs.exists == 'false'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
else
|
||||
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
|
||||
if [ -n "$XCODE_APP" ]; then
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
else
|
||||
echo "No Xcode.app found under /Applications" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
|
||||
- name: Build GhosttyKit.xcframework
|
||||
if: steps.check-release.outputs.exists == 'false'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v zig >/dev/null 2>&1; then
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install zig
|
||||
else
|
||||
echo "zig is required to build GhosttyKit.xcframework. Install zig and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
|
||||
- name: Package xcframework
|
||||
if: steps.check-release.outputs.exists == 'false'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf GhosttyKit.xcframework
|
||||
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
|
||||
tar czf GhosttyKit.xcframework.tar.gz GhosttyKit.xcframework
|
||||
|
||||
- name: Upload xcframework release
|
||||
if: steps.check-release.outputs.exists == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GHOSTTY_RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="xcframework-${{ steps.ghostty-sha.outputs.sha }}"
|
||||
gh release create "$TAG" \
|
||||
--repo manaflow-ai/ghostty \
|
||||
--title "GhosttyKit xcframework (${{ steps.ghostty-sha.outputs.sha }})" \
|
||||
--notes "Pre-built GhosttyKit.xcframework for commit ${{ steps.ghostty-sha.outputs.sha }}" \
|
||||
GhosttyKit.xcframework.tar.gz
|
||||
echo "Published release $TAG"
|
||||
171
.github/workflows/ci-macos-compat.yml
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
name: macOS Compatibility
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
compat-tests:
|
||||
# Only run for the repo itself, not forks (GhosttyKit download needs repo access).
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-14, macos-15]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Select Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Pick the latest Xcode installed on the runner. GitHub-hosted macos-14
|
||||
# defaults to Xcode 15.4, but the project needs Xcode 16+ (Swift tools
|
||||
# version 6.0 required by sentry-cocoa).
|
||||
XCODE_APP="$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort | tail -n 1)"
|
||||
if [ -z "$XCODE_APP" ]; then
|
||||
XCODE_APP="/Applications/Xcode.app"
|
||||
fi
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
if [ ! -d "$XCODE_DIR" ]; then
|
||||
echo "No Xcode found under /Applications" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Selected: $XCODE_APP"
|
||||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
xcrun --sdk macosx --show-sdk-path
|
||||
sw_vers
|
||||
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
|
||||
TAG="xcframework-$GHOSTTY_SHA"
|
||||
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
|
||||
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
|
||||
MAX_RETRIES=30
|
||||
RETRY_DELAY=20
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
|
||||
echo "Download succeeded on attempt $i"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$MAX_RETRIES" ]; then
|
||||
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
|
||||
sleep $RETRY_DELAY
|
||||
done
|
||||
tar xzf GhosttyKit.xcframework.tar.gz
|
||||
rm GhosttyKit.xcframework.tar.gz
|
||||
test -d GhosttyKit.xcframework
|
||||
|
||||
- name: Clean DerivedData
|
||||
run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Cache Swift packages
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .ci-source-packages
|
||||
key: spm-${{ matrix.os }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
|
||||
restore-keys: spm-${{ matrix.os }}-
|
||||
|
||||
- name: Resolve Swift packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
mkdir -p "$SOURCE_PACKAGES_DIR"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-resolvePackageDependencies; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Failed to resolve Swift packages after 3 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Package resolution failed on attempt $attempt, retrying..."
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
run_unit_tests() {
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" test 2>&1
|
||||
}
|
||||
|
||||
set +e
|
||||
OUTPUT=$(run_unit_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
# SwiftPM binary artifact resolution can occasionally fail on ephemeral
|
||||
# runners. Retry once after clearing caches.
|
||||
if [ "$EXIT_CODE" -ne 0 ] && echo "$OUTPUT" | grep -q "Could not resolve package dependencies"; then
|
||||
echo "SwiftPM package resolution failed, clearing caches and retrying once"
|
||||
rm -rf ~/Library/Caches/org.swift.swiftpm
|
||||
mkdir -p ~/Library/Caches/org.swift.swiftpm
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
set +e
|
||||
OUTPUT=$(run_unit_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
fi
|
||||
|
||||
echo "$OUTPUT"
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1)
|
||||
if echo "$SUMMARY" | grep -q "(0 unexpected)"; then
|
||||
echo "All failures are expected, treating as pass"
|
||||
else
|
||||
echo "Unexpected test failures detected"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Create virtual display
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=== Display before ==="
|
||||
system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)"
|
||||
echo ""
|
||||
clang -framework Foundation -framework CoreGraphics \
|
||||
-o /tmp/create-virtual-display scripts/create-virtual-display.m
|
||||
/tmp/create-virtual-display &
|
||||
VDISPLAY_PID=$!
|
||||
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
|
||||
sleep 3
|
||||
echo "=== Display after ==="
|
||||
system_profiler SPDisplaysDataType 2>/dev/null || echo "(no display info)"
|
||||
|
||||
- name: Build app for smoke test
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" build
|
||||
|
||||
- name: Smoke test
|
||||
run: |
|
||||
set -euo pipefail
|
||||
chmod +x scripts/smoke-test-ci.sh
|
||||
scripts/smoke-test-ci.sh
|
||||
294
.github/workflows/ci.yml
vendored
|
|
@ -7,6 +7,27 @@ on:
|
|||
pull_request:
|
||||
|
||||
jobs:
|
||||
workflow-guard-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Validate Depot runner guards
|
||||
run: ./tests/test_ci_self_hosted_guard.sh
|
||||
|
||||
- name: Validate create-dmg version pinning
|
||||
run: ./tests/test_ci_create_dmg_pinned.sh
|
||||
|
||||
- name: Validate unit-test SwiftPM retry guard
|
||||
run: ./tests/test_ci_unit_test_spm_retry.sh
|
||||
|
||||
- name: Validate cmux scheme test configuration
|
||||
run: ./tests/test_ci_scheme_testaction_debug.sh
|
||||
|
||||
- name: Validate GhosttyKit checksum verification
|
||||
run: ./tests/test_ci_ghosttykit_checksum_verification.sh
|
||||
|
||||
web-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
|
|
@ -14,10 +35,10 @@ jobs:
|
|||
working-directory: web
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
|
@ -25,14 +46,135 @@ jobs:
|
|||
- name: Typecheck
|
||||
run: bun tsc --noEmit
|
||||
|
||||
ui-tests:
|
||||
runs-on: self-hosted
|
||||
concurrency:
|
||||
group: self-hosted-build
|
||||
cancel-in-progress: false
|
||||
tests:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Select Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
XCODE_APP="$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort | tail -n 1 || true)"
|
||||
if [ -z "$XCODE_APP" ]; then
|
||||
XCODE_APP="/Applications/Xcode.app"
|
||||
fi
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
if [ ! -d "$XCODE_DIR" ]; then
|
||||
echo "No Xcode found under /Applications" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Selected: $XCODE_APP"
|
||||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
xcrun --sdk macosx --show-sdk-path
|
||||
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
run: |
|
||||
./scripts/download-prebuilt-ghosttykit.sh
|
||||
|
||||
- name: Clean DerivedData
|
||||
run: |
|
||||
# Remove stale build cache to avoid incremental build errors
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Cache Swift packages
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .ci-source-packages
|
||||
key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
|
||||
restore-keys: spm-
|
||||
|
||||
- name: Resolve Swift packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
mkdir -p "$SOURCE_PACKAGES_DIR"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-resolvePackageDependencies; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Failed to resolve Swift packages after 3 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Package resolution failed on attempt $attempt, retrying..."
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
run_unit_tests() {
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" test 2>&1
|
||||
}
|
||||
|
||||
# xcodebuild exits 65 even for expected failures (XCTExpectFailure).
|
||||
# Capture output and fail only if there are unexpected failures.
|
||||
set +e
|
||||
OUTPUT=$(run_unit_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
# SwiftPM binary artifact resolution can occasionally fail on ephemeral
|
||||
# runners with "Could not resolve package dependencies". Retry once after
|
||||
# clearing SwiftPM/DerivedData caches to recover from transient corruption.
|
||||
if [ "$EXIT_CODE" -ne 0 ] && echo "$OUTPUT" | grep -q "Could not resolve package dependencies"; then
|
||||
echo "SwiftPM package resolution failed, clearing caches and retrying once"
|
||||
rm -rf ~/Library/Caches/org.swift.swiftpm
|
||||
mkdir -p ~/Library/Caches/org.swift.swiftpm
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
set +e
|
||||
OUTPUT=$(run_unit_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
fi
|
||||
|
||||
echo "$OUTPUT"
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1)
|
||||
if echo "$SUMMARY" | grep -q "(0 unexpected)"; then
|
||||
echo "All failures are expected, treating as pass"
|
||||
else
|
||||
echo "Unexpected test failures detected"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run CLI version memory guard regression
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
CLI_BIN="$(
|
||||
find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -exec stat -f '%m %N' {} \; \
|
||||
| sort -nr \
|
||||
| head -1 \
|
||||
| cut -d' ' -f2-
|
||||
)"
|
||||
if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then
|
||||
echo "cmux CLI binary not found in DerivedData" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py
|
||||
|
||||
tests-depot:
|
||||
# Never run Depot jobs for fork pull requests (avoid billing on external PRs).
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: depot-macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
|
@ -42,7 +184,7 @@ jobs:
|
|||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
else
|
||||
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
|
||||
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | sort | tail -n 1 || true)"
|
||||
if [ -n "$XCODE_APP" ]; then
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
else
|
||||
|
|
@ -53,34 +195,124 @@ jobs:
|
|||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
xcrun --sdk macosx --show-sdk-path
|
||||
|
||||
- name: Download Metal Toolchain
|
||||
run: xcodebuild -downloadComponent MetalToolchain
|
||||
|
||||
- name: Build GhosttyKit.xcframework
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v zig >/dev/null 2>&1; then
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install zig
|
||||
else
|
||||
echo "zig is required to build GhosttyKit.xcframework. Install zig and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
(cd ghostty && zig build -Demit-xcframework=true -Demit-macos-app=false)
|
||||
rm -rf GhosttyKit.xcframework
|
||||
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
|
||||
test -d GhosttyKit.xcframework
|
||||
./scripts/download-prebuilt-ghosttykit.sh
|
||||
|
||||
- name: Clean DerivedData
|
||||
run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Cache Swift packages
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .ci-source-packages
|
||||
key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
|
||||
restore-keys: spm-
|
||||
|
||||
- name: Resolve Swift packages
|
||||
run: |
|
||||
# Remove stale build cache to avoid incremental build errors
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
mkdir -p "$SOURCE_PACKAGES_DIR"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-resolvePackageDependencies; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Failed to resolve Swift packages after 3 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Package resolution failed on attempt $attempt, retrying..."
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
- name: Create virtual display
|
||||
run: |
|
||||
set -euo pipefail
|
||||
clang -framework Foundation -framework CoreGraphics \
|
||||
-o /tmp/create-virtual-display scripts/create-virtual-display.m
|
||||
/tmp/create-virtual-display &
|
||||
VDISPLAY_PID=$!
|
||||
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
|
||||
sleep 3
|
||||
|
||||
- name: Run UI tests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Run directly on the self-hosted macOS runner.
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:cmuxUITests/UpdatePillUITests test
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
# SidebarResizeUITests hangs on headless runners (mouse drag simulation
|
||||
# doesn't work without a physical display, even with virtual display).
|
||||
# Skip it in CI; it runs fine on local machines.
|
||||
run_ui_tests() {
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" \
|
||||
-maximum-test-execution-time-allowance 120 \
|
||||
-only-testing:cmuxUITests \
|
||||
-skip-testing:cmuxUITests/SidebarResizeUITests test 2>&1
|
||||
}
|
||||
|
||||
# xcodebuild exits 65 even for expected failures (XCTExpectFailure).
|
||||
# Capture output and fail only if there are unexpected failures.
|
||||
set +e
|
||||
OUTPUT=$(run_ui_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
echo "$OUTPUT"
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1)
|
||||
if echo "$SUMMARY" | grep -q "(0 unexpected)"; then
|
||||
echo "All failures are expected, treating as pass"
|
||||
else
|
||||
echo "Unexpected test failures detected"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run workspace churn typing-lag regression
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
APP="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux DEV.app" -print -quit)"
|
||||
if [ -z "${APP:-}" ] || [ ! -d "$APP" ]; then
|
||||
echo "cmux DEV.app not found in DerivedData" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="ci-lag"
|
||||
SOCK="/tmp/cmux-debug-${TAG}.sock"
|
||||
BUNDLE_ID="$(
|
||||
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist" 2>/dev/null \
|
||||
|| echo 'com.cmuxterm.app.debug'
|
||||
)"
|
||||
|
||||
pkill -x "cmux DEV" || true
|
||||
rm -f "$SOCK" "/tmp/cmux-${TAG}.sock" || true
|
||||
defaults write "$BUNDLE_ID" socketControlMode -string full >/dev/null 2>&1 || true
|
||||
|
||||
CMUX_TAG="$TAG" CMUX_SOCKET_PATH="$SOCK" CMUX_UI_TEST_MODE=1 "$APP/Contents/MacOS/cmux DEV" >/tmp/cmux-ci-lag.log 2>&1 &
|
||||
APP_PID=$!
|
||||
trap 'kill "$APP_PID" >/dev/null 2>&1 || true' EXIT
|
||||
|
||||
for _ in {1..240}; do
|
||||
[ -S "$SOCK" ] && break
|
||||
sleep 0.25
|
||||
done
|
||||
[ -S "$SOCK" ] || { echo "Socket not ready at $SOCK" >&2; exit 1; }
|
||||
|
||||
CMUX_SOCKET_PATH="$SOCK" \
|
||||
CMUX_LAG_MAX_P95_RATIO=1.70 \
|
||||
CMUX_LAG_MAX_AVG_RATIO=1.70 \
|
||||
CMUX_LAG_MIN_BASELINE_P95_MS_FOR_RATIO=6.0 \
|
||||
CMUX_LAG_MIN_BASELINE_AVG_MS_FOR_RATIO=4.0 \
|
||||
CMUX_LAG_MAX_P95_DELTA_MS=20.0 \
|
||||
CMUX_LAG_MAX_AVG_DELTA_MS=12.0 \
|
||||
CMUX_LAG_MAX_CHURN_P95_MS=35 \
|
||||
CMUX_LAG_KEY_EVENTS=180 \
|
||||
python3 tests/test_workspace_churn_up_arrow_lag.py
|
||||
|
|
|
|||
50
.github/workflows/claude.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
428
.github/workflows/nightly.yml
vendored
|
|
@ -1,9 +1,8 @@
|
|||
name: Nightly macOS build
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every hour at :30. The 'decide' job skips if main has no new commits.
|
||||
- cron: "30 * * * *"
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
|
|
@ -12,9 +11,19 @@ on:
|
|||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: nightly-build-${{ github.ref_name }}
|
||||
# Queue main pushes instead of hard-canceling older runs. The decide job
|
||||
# already coalesces to the current main HEAD, and we re-check HEAD before
|
||||
# publishing so stale queued runs exit cleanly instead of showing up red.
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CREATE_DMG_VERSION: 8.0.0
|
||||
|
||||
jobs:
|
||||
decide:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -22,69 +31,79 @@ jobs:
|
|||
should_build: ${{ steps.decide.outputs.should_build }}
|
||||
head_sha: ${{ steps.decide.outputs.head_sha }}
|
||||
short_sha: ${{ steps.decide.outputs.short_sha }}
|
||||
should_publish: ${{ steps.decide.outputs.should_publish }}
|
||||
steps:
|
||||
- name: Decide whether a nightly build is needed
|
||||
id: decide
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
FORCE_BUILD: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force == 'true' && 'true' || 'false' }}
|
||||
with:
|
||||
script: |
|
||||
const forceBuild = process.env.FORCE_BUILD === 'true';
|
||||
const { owner, repo } = context.repo;
|
||||
const requestedRef = context.ref.startsWith('refs/heads/')
|
||||
? context.ref.replace('refs/heads/', '')
|
||||
: 'main';
|
||||
const isMainRef = requestedRef === 'main';
|
||||
|
||||
const branch = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: 'main',
|
||||
});
|
||||
const headSha = branch.data.commit.sha;
|
||||
|
||||
let nightlySha = null;
|
||||
try {
|
||||
const ref = await github.rest.git.getRef({
|
||||
let headSha = context.sha;
|
||||
if (isMainRef) {
|
||||
const branch = await github.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
ref: 'tags/nightly',
|
||||
branch: 'main',
|
||||
});
|
||||
if (ref.data.object.type === 'commit') {
|
||||
nightlySha = ref.data.object.sha;
|
||||
} else if (ref.data.object.type === 'tag') {
|
||||
const tagObject = await github.rest.git.getTag({
|
||||
owner,
|
||||
repo,
|
||||
tag_sha: ref.data.object.sha,
|
||||
});
|
||||
nightlySha = tagObject.data.object.sha;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
headSha = branch.data.commit.sha;
|
||||
}
|
||||
|
||||
const shouldBuild = forceBuild || nightlySha !== headSha;
|
||||
let nightlySha = null;
|
||||
if (isMainRef) {
|
||||
try {
|
||||
const ref = await github.rest.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: 'tags/nightly',
|
||||
});
|
||||
if (ref.data.object.type === 'commit') {
|
||||
nightlySha = ref.data.object.sha;
|
||||
} else if (ref.data.object.type === 'tag') {
|
||||
const tagObject = await github.rest.git.getTag({
|
||||
owner,
|
||||
repo,
|
||||
tag_sha: ref.data.object.sha,
|
||||
});
|
||||
nightlySha = tagObject.data.object.sha;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const shouldBuild = !isMainRef || forceBuild || nightlySha !== headSha;
|
||||
core.setOutput('should_build', shouldBuild ? 'true' : 'false');
|
||||
core.setOutput('head_sha', headSha);
|
||||
core.setOutput('short_sha', headSha.slice(0, 7));
|
||||
core.setOutput('should_publish', isMainRef ? 'true' : 'false');
|
||||
core.summary
|
||||
.addHeading('Nightly build decision')
|
||||
.addTable([
|
||||
[{data: 'main HEAD', header: true}, headSha],
|
||||
[{data: 'requested ref', header: true}, requestedRef],
|
||||
[{data: 'build HEAD', header: true}, headSha],
|
||||
[{data: 'nightly tag', header: true}, nightlySha ?? '(missing)'],
|
||||
[{data: 'force build', header: true}, String(forceBuild)],
|
||||
[{data: 'should build', header: true}, String(shouldBuild)],
|
||||
[{data: 'should publish', header: true}, String(isMainRef)],
|
||||
])
|
||||
.write();
|
||||
|
||||
build-sign-notarize-nightly:
|
||||
needs: decide
|
||||
if: needs.decide.outputs.should_build == 'true'
|
||||
runs-on: self-hosted
|
||||
concurrency:
|
||||
group: self-hosted-build
|
||||
cancel-in-progress: false
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout build ref
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ needs.decide.outputs.head_sha }}
|
||||
submodules: recursive
|
||||
|
|
@ -110,30 +129,18 @@ jobs:
|
|||
|
||||
- name: Install build deps
|
||||
run: |
|
||||
brew update
|
||||
brew install zig
|
||||
npm install --global create-dmg
|
||||
npm install --global "create-dmg@${CREATE_DMG_VERSION}"
|
||||
|
||||
- name: Build GhosttyKit.xcframework
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
run: |
|
||||
cd ghostty
|
||||
zig build -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=native -Doptimize=ReleaseFast
|
||||
cd ..
|
||||
rm -rf GhosttyKit.xcframework
|
||||
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
|
||||
./scripts/download-prebuilt-ghosttykit.sh
|
||||
|
||||
- name: Clear SPM cache
|
||||
run: |
|
||||
rm -rf ~/Library/Caches/org.swift.swiftpm
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Configure SwiftPM cache
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CACHE_DIR="${RUNNER_TEMP}/swiftpm-cache/${GITHUB_RUN_ID}"
|
||||
rm -rf "$CACHE_DIR"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
echo "SWIFTPM_CACHE_PATH=$CACHE_DIR" >> "$GITHUB_ENV"
|
||||
- name: Cache Swift packages
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .spm-cache
|
||||
key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
|
||||
restore-keys: spm-
|
||||
|
||||
- name: Derive Sparkle public key from private key
|
||||
env:
|
||||
|
|
@ -147,38 +154,82 @@ jobs:
|
|||
echo "Derived Sparkle public key: $DERIVED_PUBLIC_KEY"
|
||||
echo "SPARKLE_PUBLIC_KEY=$DERIVED_PUBLIC_KEY" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build app (Release)
|
||||
- name: Build Apple Silicon app (Release)
|
||||
run: |
|
||||
xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build
|
||||
xcodebuild -scheme cmux -configuration Release -derivedDataPath build-arm \
|
||||
-destination 'platform=macOS,arch=arm64' \
|
||||
-clonedSourcePackagesDirPath .spm-cache \
|
||||
ARCHS="arm64" \
|
||||
ONLY_ACTIVE_ARCH=YES \
|
||||
CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build
|
||||
|
||||
- name: Inject nightly identity and metadata
|
||||
- name: Build universal app (Release)
|
||||
run: |
|
||||
xcodebuild -scheme cmux -configuration Release -derivedDataPath build-universal \
|
||||
-destination 'generic/platform=macOS' \
|
||||
-clonedSourcePackagesDirPath .spm-cache \
|
||||
ARCHS="arm64 x86_64" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
CODE_SIGNING_ALLOWED=NO ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon-Nightly build
|
||||
|
||||
- name: Verify nightly binary architectures
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ARM_APP_BINARY="build-arm/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
|
||||
ARM_CLI_BINARY="build-arm/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
|
||||
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
|
||||
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
|
||||
ARM_APP_ARCHS="$(lipo -archs "$ARM_APP_BINARY")"
|
||||
ARM_CLI_ARCHS="$(lipo -archs "$ARM_CLI_BINARY")"
|
||||
APP_ARCHS="$(lipo -archs "$APP_BINARY")"
|
||||
CLI_ARCHS="$(lipo -archs "$CLI_BINARY")"
|
||||
echo "Arm app binary architectures: $ARM_APP_ARCHS"
|
||||
echo "Arm CLI binary architectures: $ARM_CLI_ARCHS"
|
||||
echo "App binary architectures: $APP_ARCHS"
|
||||
echo "CLI binary architectures: $CLI_ARCHS"
|
||||
[[ "$ARM_APP_ARCHS" == "arm64" ]]
|
||||
[[ "$ARM_CLI_ARCHS" == "arm64" ]]
|
||||
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
|
||||
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
|
||||
|
||||
- name: Run CLI version memory guard regression
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
|
||||
[ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; }
|
||||
CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py
|
||||
|
||||
- name: Check whether build commit is still current main HEAD
|
||||
if: needs.decide.outputs.should_publish == 'true'
|
||||
id: current_head
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CURRENT_MAIN_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')"
|
||||
BUILD_SHA="${{ needs.decide.outputs.head_sha }}"
|
||||
if [ "$CURRENT_MAIN_SHA" = "$BUILD_SHA" ]; then
|
||||
STILL_CURRENT=true
|
||||
else
|
||||
STILL_CURRENT=false
|
||||
fi
|
||||
echo "still_current=${STILL_CURRENT}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "### Publish guard"
|
||||
echo
|
||||
echo "- build sha: \`$BUILD_SHA\`"
|
||||
echo "- current main sha: \`$CURRENT_MAIN_SHA\`"
|
||||
echo "- continue signing/publish: \`$STILL_CURRENT\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Inject nightly identities and metadata
|
||||
if: needs.decide.outputs.should_publish != 'true' || steps.current_head.outputs.still_current == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APP_DIR="build/Build/Products/Release"
|
||||
APP_PLIST="${APP_DIR}/cmux.app/Contents/Info.plist"
|
||||
SHORT_SHA="${{ needs.decide.outputs.short_sha }}"
|
||||
ARM_APP_DIR="build-arm/Build/Products/Release"
|
||||
UNIVERSAL_APP_DIR="build-universal/Build/Products/Release"
|
||||
|
||||
# --- Separate app identity: "cmux NIGHTLY" with its own bundle ID ---
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleName cmux NIGHTLY" "$APP_PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName cmux NIGHTLY" "$APP_PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.cmuxterm.app.nightly" "$APP_PLIST"
|
||||
|
||||
# Rename the .app bundle to match the display name
|
||||
mv "${APP_DIR}/cmux.app" "${APP_DIR}/cmux NIGHTLY.app"
|
||||
|
||||
# Update plist path after rename
|
||||
APP_PLIST="${APP_DIR}/cmux NIGHTLY.app/Contents/Info.plist"
|
||||
|
||||
# --- Sparkle: point at the nightly appcast ---
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$APP_PLIST" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$APP_PLIST" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string ${SPARKLE_PUBLIC_KEY}" "$APP_PLIST"
|
||||
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast.xml" "$APP_PLIST"
|
||||
|
||||
# Marketing version: append -nightly.YYYYMMDD so users can identify the channel and date
|
||||
BASE_MARKETING=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PLIST")
|
||||
BASE_MARKETING=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${ARM_APP_DIR}/cmux.app/Contents/Info.plist")
|
||||
NIGHTLY_DATE=$(date -u +%Y%m%d)
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}" "$APP_PLIST"
|
||||
|
||||
# Build number: unique/monotonic per workflow run attempt so same-day
|
||||
# nightlies and reruns still compare as newer in Sparkle.
|
||||
|
|
@ -188,26 +239,53 @@ jobs:
|
|||
else
|
||||
NIGHTLY_BUILD="${NIGHTLY_DATE}000000"
|
||||
fi
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${NIGHTLY_BUILD}" "$APP_PLIST"
|
||||
|
||||
# Use an immutable DMG filename in appcast URLs so old appcasts keep
|
||||
# pointing at matching archives while nightly assets roll forward.
|
||||
NIGHTLY_DMG_IMMUTABLE="cmux-nightly-macos-${NIGHTLY_BUILD}.dmg"
|
||||
echo "NIGHTLY_BUILD=${NIGHTLY_BUILD}" >> "$GITHUB_ENV"
|
||||
echo "NIGHTLY_DMG_IMMUTABLE=${NIGHTLY_DMG_IMMUTABLE}" >> "$GITHUB_ENV"
|
||||
|
||||
# Embed commit SHA for bug reports
|
||||
/usr/libexec/PlistBuddy -c "Delete :CMUXCommit" "$APP_PLIST" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Add :CMUXCommit string ${SHORT_SHA}" "$APP_PLIST"
|
||||
ARM_DMG_IMMUTABLE="cmux-nightly-macos-${NIGHTLY_BUILD}.dmg"
|
||||
UNIVERSAL_DMG_IMMUTABLE="cmux-nightly-universal-macos-${NIGHTLY_BUILD}.dmg"
|
||||
echo "NIGHTLY_DMG_IMMUTABLE=${ARM_DMG_IMMUTABLE}" >> "$GITHUB_ENV"
|
||||
echo "NIGHTLY_UNIVERSAL_DMG_IMMUTABLE=${UNIVERSAL_DMG_IMMUTABLE}" >> "$GITHUB_ENV"
|
||||
|
||||
prepare_variant() {
|
||||
local app_dir="$1"
|
||||
local bundle_id="$2"
|
||||
local feed_url="$3"
|
||||
local app_plist="$app_dir/cmux.app/Contents/Info.plist"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleName cmux NIGHTLY" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName cmux NIGHTLY" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${bundle_id}" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$app_plist" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$app_plist" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string ${SPARKLE_PUBLIC_KEY}" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string ${feed_url}" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${NIGHTLY_BUILD}" "$app_plist"
|
||||
/usr/libexec/PlistBuddy -c "Delete :CMUXCommit" "$app_plist" >/dev/null 2>&1 || true
|
||||
/usr/libexec/PlistBuddy -c "Add :CMUXCommit string ${SHORT_SHA}" "$app_plist"
|
||||
mv "$app_dir/cmux.app" "$app_dir/cmux NIGHTLY.app"
|
||||
}
|
||||
|
||||
prepare_variant \
|
||||
"$ARM_APP_DIR" \
|
||||
"com.cmuxterm.app.nightly" \
|
||||
"https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast.xml"
|
||||
prepare_variant \
|
||||
"$UNIVERSAL_APP_DIR" \
|
||||
"com.cmuxterm.app.nightly.universal" \
|
||||
"https://github.com/manaflow-ai/cmux/releases/download/nightly/appcast-universal.xml"
|
||||
|
||||
echo "Nightly app name: cmux NIGHTLY"
|
||||
echo "Nightly bundle ID: com.cmuxterm.app.nightly"
|
||||
echo "Nightly arm64 bundle ID: com.cmuxterm.app.nightly"
|
||||
echo "Nightly universal bundle ID: com.cmuxterm.app.nightly.universal"
|
||||
echo "Nightly marketing version: ${BASE_MARKETING}-nightly.${NIGHTLY_DATE}"
|
||||
echo "Nightly build number: ${NIGHTLY_BUILD}"
|
||||
echo "Nightly immutable DMG: ${NIGHTLY_DMG_IMMUTABLE}"
|
||||
echo "Nightly arm64 immutable DMG: ${ARM_DMG_IMMUTABLE}"
|
||||
echo "Nightly universal immutable DMG: ${UNIVERSAL_DMG_IMMUTABLE}"
|
||||
echo "Commit SHA: ${SHORT_SHA}"
|
||||
|
||||
- name: Import signing cert
|
||||
if: needs.decide.outputs.should_publish != 'true' || steps.current_head.outputs.still_current == 'true'
|
||||
env:
|
||||
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
|
@ -230,7 +308,8 @@ jobs:
|
|||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security list-keychains -d user -s build.keychain
|
||||
|
||||
- name: Codesign app
|
||||
- name: Codesign apps
|
||||
if: needs.decide.outputs.should_publish != 'true' || steps.current_head.outputs.still_current == 'true'
|
||||
env:
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
|
|
@ -238,16 +317,21 @@ jobs:
|
|||
echo "Missing APPLE_SIGNING_IDENTITY secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
APP_PATH="build/Build/Products/Release/cmux NIGHTLY.app"
|
||||
ENTITLEMENTS="cmux.entitlements"
|
||||
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
|
||||
if [ -f "$CLI_PATH" ]; then
|
||||
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH"
|
||||
fi
|
||||
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH"
|
||||
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
||||
for APP_PATH in \
|
||||
"build-arm/Build/Products/Release/cmux NIGHTLY.app" \
|
||||
"build-universal/Build/Products/Release/cmux NIGHTLY.app"
|
||||
do
|
||||
CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux"
|
||||
if [ -f "$CLI_PATH" ]; then
|
||||
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH"
|
||||
fi
|
||||
/usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH"
|
||||
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
||||
done
|
||||
|
||||
- name: Notarize app and dmg
|
||||
- name: Notarize apps and dmgs
|
||||
if: needs.decide.outputs.should_publish != 'true' || steps.current_head.outputs.still_current == 'true'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
|
|
@ -258,43 +342,81 @@ jobs:
|
|||
echo "Missing notarization secrets (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID)" >&2
|
||||
exit 1
|
||||
fi
|
||||
APP_PATH="build/Build/Products/Release/cmux NIGHTLY.app"
|
||||
ZIP_SUBMIT="cmux-nightly-notary.zip"
|
||||
DMG_RELEASE="cmux-nightly-macos.dmg"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_SUBMIT"
|
||||
APP_SUBMIT_JSON="$(xcrun notarytool submit "$ZIP_SUBMIT" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)"
|
||||
APP_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$APP_SUBMIT_JSON")"
|
||||
APP_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$APP_SUBMIT_JSON")"
|
||||
if [ "$APP_STATUS" != "Accepted" ]; then
|
||||
echo "App notarization failed with status: $APP_STATUS" >&2
|
||||
xcrun notarytool log "$APP_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true
|
||||
exit 1
|
||||
fi
|
||||
xcrun stapler staple "$APP_PATH"
|
||||
xcrun stapler validate "$APP_PATH"
|
||||
spctl -a -vv --type execute "$APP_PATH"
|
||||
rm -f "$ZIP_SUBMIT"
|
||||
create-dmg \
|
||||
--identity="$APPLE_SIGNING_IDENTITY" \
|
||||
"$APP_PATH" \
|
||||
./
|
||||
mv ./"cmux NIGHTLY"*.dmg "$DMG_RELEASE" 2>/dev/null || mv ./cmux*.dmg "$DMG_RELEASE"
|
||||
DMG_SUBMIT_JSON="$(xcrun notarytool submit "$DMG_RELEASE" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)"
|
||||
DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")"
|
||||
DMG_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$DMG_SUBMIT_JSON")"
|
||||
if [ "$DMG_STATUS" != "Accepted" ]; then
|
||||
echo "DMG notarization failed with status: $DMG_STATUS" >&2
|
||||
xcrun notarytool log "$DMG_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true
|
||||
exit 1
|
||||
fi
|
||||
xcrun stapler staple "$DMG_RELEASE"
|
||||
xcrun stapler validate "$DMG_RELEASE"
|
||||
notarize_and_package() {
|
||||
local app_path="$1"
|
||||
local dmg_release="$2"
|
||||
local dmg_immutable="$3"
|
||||
local zip_submit="${dmg_release%.dmg}-notary.zip"
|
||||
local dmg_tmp_dir
|
||||
local created_dmg
|
||||
|
||||
# Keep a stable filename for humans and an immutable filename used
|
||||
# by appcast URLs to prevent signature/asset mismatch races.
|
||||
cp "$DMG_RELEASE" "$NIGHTLY_DMG_IMMUTABLE"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_submit"
|
||||
APP_SUBMIT_JSON="$(xcrun notarytool submit "$zip_submit" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)"
|
||||
APP_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$APP_SUBMIT_JSON")"
|
||||
APP_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$APP_SUBMIT_JSON")"
|
||||
if [ "$APP_STATUS" != "Accepted" ]; then
|
||||
echo "App notarization failed for $app_path with status: $APP_STATUS" >&2
|
||||
xcrun notarytool log "$APP_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true
|
||||
exit 1
|
||||
fi
|
||||
xcrun stapler staple "$app_path"
|
||||
xcrun stapler validate "$app_path"
|
||||
spctl -a -vv --type execute "$app_path"
|
||||
rm -f "$zip_submit"
|
||||
|
||||
- name: Generate Sparkle appcast (nightly)
|
||||
dmg_tmp_dir="$(mktemp -d)"
|
||||
create-dmg \
|
||||
--identity="$APPLE_SIGNING_IDENTITY" \
|
||||
"$app_path" \
|
||||
"$dmg_tmp_dir"
|
||||
created_dmg="$(find "$dmg_tmp_dir" -maxdepth 1 -name '*.dmg' | head -n 1)"
|
||||
if [ -z "$created_dmg" ]; then
|
||||
echo "Failed to locate created DMG for $app_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv "$created_dmg" "$dmg_release"
|
||||
rm -rf "$dmg_tmp_dir"
|
||||
|
||||
DMG_SUBMIT_JSON="$(xcrun notarytool submit "$dmg_release" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)"
|
||||
DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")"
|
||||
DMG_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$DMG_SUBMIT_JSON")"
|
||||
if [ "$DMG_STATUS" != "Accepted" ]; then
|
||||
echo "DMG notarization failed for $dmg_release with status: $DMG_STATUS" >&2
|
||||
xcrun notarytool log "$DMG_SUBMIT_ID" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" || true
|
||||
exit 1
|
||||
fi
|
||||
xcrun stapler staple "$dmg_release"
|
||||
xcrun stapler validate "$dmg_release"
|
||||
cp "$dmg_release" "$dmg_immutable"
|
||||
}
|
||||
|
||||
notarize_and_package \
|
||||
"build-arm/Build/Products/Release/cmux NIGHTLY.app" \
|
||||
"cmux-nightly-macos.dmg" \
|
||||
"$NIGHTLY_DMG_IMMUTABLE"
|
||||
notarize_and_package \
|
||||
"build-universal/Build/Products/Release/cmux NIGHTLY.app" \
|
||||
"cmux-nightly-universal-macos.dmg" \
|
||||
"$NIGHTLY_UNIVERSAL_DMG_IMMUTABLE"
|
||||
|
||||
- name: Upload dSYMs to Sentry
|
||||
if: needs.decide.outputs.should_publish != 'true' || steps.current_head.outputs.still_current == 'true'
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: manaflow
|
||||
SENTRY_PROJECT: cmuxterm-macos
|
||||
run: |
|
||||
if [ -z "$SENTRY_AUTH_TOKEN" ]; then
|
||||
echo "SENTRY_AUTH_TOKEN not set, skipping dSYM upload"
|
||||
exit 0
|
||||
fi
|
||||
brew install getsentry/tools/sentry-cli || true
|
||||
sentry-cli debug-files upload --include-sources \
|
||||
build-arm/Build/Products/Release/ \
|
||||
build-universal/Build/Products/Release/
|
||||
|
||||
- name: Generate Sparkle appcasts (nightly)
|
||||
if: needs.decide.outputs.should_publish != 'true' || steps.current_head.outputs.still_current == 'true'
|
||||
env:
|
||||
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
|
||||
run: |
|
||||
|
|
@ -303,8 +425,22 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
./scripts/sparkle_generate_appcast.sh "$NIGHTLY_DMG_IMMUTABLE" nightly appcast.xml
|
||||
./scripts/sparkle_generate_appcast.sh "$NIGHTLY_UNIVERSAL_DMG_IMMUTABLE" nightly appcast-universal.xml
|
||||
|
||||
- name: Upload branch nightly artifacts
|
||||
if: needs.decide.outputs.should_publish != 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: cmux-nightly-${{ needs.decide.outputs.short_sha }}
|
||||
path: |
|
||||
cmux-nightly-macos*.dmg
|
||||
cmux-nightly-universal-macos*.dmg
|
||||
appcast.xml
|
||||
appcast-universal.xml
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Move nightly tag to built commit
|
||||
if: needs.decide.outputs.should_publish == 'true' && steps.current_head.outputs.still_current == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
|
|
@ -313,7 +449,8 @@ jobs:
|
|||
git push origin refs/tags/nightly --force
|
||||
|
||||
- name: Publish nightly release assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: needs.decide.outputs.should_publish == 'true' && steps.current_head.outputs.still_current == 'true'
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
tag_name: nightly
|
||||
name: Nightly
|
||||
|
|
@ -322,13 +459,19 @@ jobs:
|
|||
body: |
|
||||
Automated nightly build for `${{ needs.decide.outputs.short_sha }}`.
|
||||
|
||||
**cmux NIGHTLY** is a separate app (bundle ID `com.cmuxterm.app.nightly`) that can be installed alongside the stable release. It receives nightly updates automatically via its own Sparkle feed.
|
||||
**cmux NIGHTLY** has two update tracks:
|
||||
- Apple Silicon: bundle ID `com.cmuxterm.app.nightly`, feed `appcast.xml`
|
||||
- Universal: bundle ID `com.cmuxterm.app.nightly.universal`, feed `appcast-universal.xml`
|
||||
|
||||
[Download cmux-nightly-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
[Download cmux-nightly-universal-macos.dmg](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-universal-macos.dmg)
|
||||
files: |
|
||||
cmux-nightly-macos-${{ github.run_id }}*.dmg
|
||||
cmux-nightly-macos.dmg
|
||||
cmux-nightly-universal-macos-${{ github.run_id }}*.dmg
|
||||
cmux-nightly-universal-macos.dmg
|
||||
appcast.xml
|
||||
appcast-universal.xml
|
||||
overwrite_files: true
|
||||
|
||||
- name: Cleanup keychain
|
||||
|
|
@ -336,12 +479,3 @@ jobs:
|
|||
run: |
|
||||
security delete-keychain build.keychain >/dev/null 2>&1 || true
|
||||
rm -f /tmp/cert.p12
|
||||
|
||||
skipped:
|
||||
needs: decide
|
||||
if: needs.decide.outputs.should_build != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: No nightly build needed
|
||||
run: |
|
||||
echo "No changes on main since last nightly tag; skipping build."
|
||||
|
|
|
|||
75
.github/workflows/release.yml
vendored
|
|
@ -9,21 +9,21 @@ on:
|
|||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CREATE_DMG_VERSION: 8.0.0
|
||||
|
||||
jobs:
|
||||
build-sign-notarize:
|
||||
runs-on: self-hosted
|
||||
concurrency:
|
||||
group: self-hosted-build
|
||||
cancel-in-progress: false
|
||||
runs-on: depot-macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Guard immutable release assets
|
||||
id: guard_release_assets
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
script: |
|
||||
const { evaluateReleaseAssetGuard } = require('./scripts/release_asset_guard');
|
||||
|
|
@ -99,37 +99,20 @@ jobs:
|
|||
- name: Install build deps
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: |
|
||||
brew update
|
||||
brew install zig
|
||||
npm install --global create-dmg
|
||||
npm install --global "create-dmg@${CREATE_DMG_VERSION}"
|
||||
|
||||
- name: Download Metal Toolchain
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: xcodebuild -downloadComponent MetalToolchain
|
||||
|
||||
- name: Build GhosttyKit.xcframework
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: |
|
||||
cd ghostty
|
||||
zig build -Demit-xcframework=true -Demit-macos-app=false -Doptimize=ReleaseFast
|
||||
cd ..
|
||||
rm -rf GhosttyKit.xcframework
|
||||
cp -R ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
|
||||
./scripts/download-prebuilt-ghosttykit.sh
|
||||
|
||||
- name: Clear SPM cache
|
||||
- name: Cache Swift packages
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: |
|
||||
rm -rf ~/Library/Caches/org.swift.swiftpm
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Configure SwiftPM cache
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CACHE_DIR="${RUNNER_TEMP}/swiftpm-cache/${GITHUB_RUN_ID}"
|
||||
rm -rf "$CACHE_DIR"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
echo "SWIFTPM_CACHE_PATH=$CACHE_DIR" >> "$GITHUB_ENV"
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .spm-cache
|
||||
key: spm-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
|
||||
restore-keys: spm-
|
||||
|
||||
- name: Derive Sparkle public key from private key
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
|
|
@ -147,7 +130,17 @@ jobs:
|
|||
- name: Build app (Release)
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: |
|
||||
xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build
|
||||
xcodebuild -scheme cmux -configuration Release -derivedDataPath build \
|
||||
-clonedSourcePackagesDirPath .spm-cache \
|
||||
CODE_SIGNING_ALLOWED=NO build
|
||||
|
||||
- name: Run CLI version memory guard regression
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CLI_BINARY="build/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
|
||||
[ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; }
|
||||
CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py
|
||||
|
||||
- name: Inject Sparkle keys into Info.plist
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
|
|
@ -250,6 +243,20 @@ jobs:
|
|||
xcrun stapler staple "$DMG_RELEASE"
|
||||
xcrun stapler validate "$DMG_RELEASE"
|
||||
|
||||
- name: Upload dSYMs to Sentry
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: manaflow
|
||||
SENTRY_PROJECT: cmuxterm-macos
|
||||
run: |
|
||||
if [ -z "$SENTRY_AUTH_TOKEN" ]; then
|
||||
echo "SENTRY_AUTH_TOKEN not set, skipping dSYM upload"
|
||||
exit 0
|
||||
fi
|
||||
brew install getsentry/tools/sentry-cli || true
|
||||
sentry-cli debug-files upload --include-sources build/Build/Products/Release/
|
||||
|
||||
- name: Generate Sparkle appcast
|
||||
if: steps.guard_release_assets.outputs.skip_all != 'true'
|
||||
env:
|
||||
|
|
@ -263,7 +270,7 @@ jobs:
|
|||
|
||||
- name: Upload release asset
|
||||
if: steps.guard_release_assets.outputs.skip_upload != 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
files: |
|
||||
cmux-macos.dmg
|
||||
|
|
|
|||
187
.github/workflows/test-depot.yml
vendored
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
name: Run tests on Depot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Branch or SHA to test
|
||||
required: false
|
||||
default: ""
|
||||
skip_unit_tests:
|
||||
description: Skip unit tests (run only UI tests)
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_ui_tests:
|
||||
description: Skip UI tests (run only unit tests)
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
test_filter:
|
||||
description: "Run specific UI test class (e.g. UpdatePillUITests) or empty for all"
|
||||
required: false
|
||||
default: ""
|
||||
test_timeout:
|
||||
description: "Per-test timeout in seconds (default: 120)"
|
||||
required: false
|
||||
default: "120"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: depot-macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref || github.ref }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Select Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
else
|
||||
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
|
||||
if [ -n "$XCODE_APP" ]; then
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
else
|
||||
echo "No Xcode.app found under /Applications" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
xcrun --sdk macosx --show-sdk-path
|
||||
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
|
||||
TAG="xcframework-$GHOSTTY_SHA"
|
||||
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
|
||||
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
|
||||
MAX_RETRIES=30
|
||||
RETRY_DELAY=20
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
|
||||
echo "Download succeeded on attempt $i"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$MAX_RETRIES" ]; then
|
||||
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
|
||||
sleep $RETRY_DELAY
|
||||
done
|
||||
tar xzf GhosttyKit.xcframework.tar.gz
|
||||
rm GhosttyKit.xcframework.tar.gz
|
||||
test -d GhosttyKit.xcframework
|
||||
|
||||
- name: Create virtual display
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=== Display before ==="
|
||||
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
|
||||
echo ""
|
||||
clang -framework Foundation -framework CoreGraphics \
|
||||
-o /tmp/create-virtual-display scripts/create-virtual-display.m
|
||||
/tmp/create-virtual-display &
|
||||
VDISPLAY_PID=$!
|
||||
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
|
||||
sleep 3
|
||||
echo "=== Display after ==="
|
||||
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
|
||||
|
||||
- name: Clean DerivedData
|
||||
run: |
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Resolve Swift packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
rm -rf "$SOURCE_PACKAGES_DIR"
|
||||
mkdir -p "$SOURCE_PACKAGES_DIR"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-resolvePackageDependencies; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Failed to resolve Swift packages after 3 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Package resolution failed on attempt $attempt, retrying..."
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
- name: Run unit tests
|
||||
if: ${{ !inputs.skip_unit_tests }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
run_unit_tests() {
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" test 2>&1
|
||||
}
|
||||
|
||||
set +e
|
||||
OUTPUT=$(run_unit_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
# SwiftPM binary artifact resolution can occasionally fail with
|
||||
# "Could not resolve package dependencies". Retry once after clearing
|
||||
# SwiftPM/DerivedData caches to recover from transient corruption.
|
||||
if [ "$EXIT_CODE" -ne 0 ] && echo "$OUTPUT" | grep -q "Could not resolve package dependencies"; then
|
||||
echo "SwiftPM package resolution failed, clearing caches and retrying once"
|
||||
rm -rf ~/Library/Caches/org.swift.swiftpm
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
set +e
|
||||
OUTPUT=$(run_unit_tests)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
fi
|
||||
|
||||
echo "$OUTPUT"
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
SUMMARY=$(echo "$OUTPUT" | grep "Executed.*tests.*with.*failures" | tail -1)
|
||||
if echo "$SUMMARY" | grep -q "(0 unexpected)"; then
|
||||
echo "All failures are expected, treating as pass"
|
||||
else
|
||||
echo "Unexpected test failures detected"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run UI tests
|
||||
if: ${{ !inputs.skip_ui_tests }}
|
||||
env:
|
||||
TEST_FILTER: ${{ inputs.test_filter }}
|
||||
TEST_TIMEOUT: ${{ inputs.test_timeout || '120' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
|
||||
# Build the -only-testing argument
|
||||
if [ -n "$TEST_FILTER" ]; then
|
||||
ONLY_TESTING="-only-testing:cmuxUITests/$TEST_FILTER"
|
||||
else
|
||||
ONLY_TESTING="-only-testing:cmuxUITests"
|
||||
fi
|
||||
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" \
|
||||
-maximum-test-execution-time-allowance "$TEST_TIMEOUT" \
|
||||
$ONLY_TESTING test
|
||||
363
.github/workflows/test-e2e.yml
vendored
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
name: E2E test with video recording
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Branch or SHA to test
|
||||
required: false
|
||||
default: ""
|
||||
test_filter:
|
||||
description: "Test class or class/method (e.g. UpdatePillUITests or UpdatePillUITests/testSomething)"
|
||||
required: true
|
||||
test_timeout:
|
||||
description: "Per-test timeout in seconds"
|
||||
required: false
|
||||
default: "120"
|
||||
record_video:
|
||||
description: Record the virtual display during tests
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
runner:
|
||||
description: "Runner OS (macos-15 or macos-26)"
|
||||
required: false
|
||||
default: "macos-15"
|
||||
type: choice
|
||||
options:
|
||||
- macos-15
|
||||
- macos-26
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ${{ inputs.runner || 'macos-15' }}
|
||||
env:
|
||||
TEST_REF: ${{ inputs.ref || github.ref }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Capture SHA
|
||||
id: sha
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Select Xcode
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
else
|
||||
XCODE_APP="$(ls -d /Applications/Xcode*.app 2>/dev/null | head -n 1 || true)"
|
||||
if [ -n "$XCODE_APP" ]; then
|
||||
XCODE_DIR="$XCODE_APP/Contents/Developer"
|
||||
else
|
||||
echo "No Xcode.app found under /Applications" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
|
||||
export DEVELOPER_DIR="$XCODE_DIR"
|
||||
xcodebuild -version
|
||||
xcrun --sdk macosx --show-sdk-path
|
||||
|
||||
- name: Download pre-built GhosttyKit.xcframework
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
|
||||
TAG="xcframework-$GHOSTTY_SHA"
|
||||
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
|
||||
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
|
||||
MAX_RETRIES=30
|
||||
RETRY_DELAY=20
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
|
||||
echo "Download succeeded on attempt $i"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$MAX_RETRIES" ]; then
|
||||
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
|
||||
sleep $RETRY_DELAY
|
||||
done
|
||||
tar xzf GhosttyKit.xcframework.tar.gz
|
||||
rm GhosttyKit.xcframework.tar.gz
|
||||
test -d GhosttyKit.xcframework
|
||||
|
||||
- name: Create virtual display
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=== Display before ==="
|
||||
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
|
||||
echo ""
|
||||
clang -framework Foundation -framework CoreGraphics \
|
||||
-o /tmp/create-virtual-display scripts/create-virtual-display.m
|
||||
/tmp/create-virtual-display &
|
||||
VDISPLAY_PID=$!
|
||||
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
|
||||
sleep 3
|
||||
echo "=== Display after ==="
|
||||
system_profiler SPDisplaysDataType 2>/dev/null || echo "(none)"
|
||||
|
||||
- name: Install ffmpeg
|
||||
if: ${{ inputs.record_video }}
|
||||
run: |
|
||||
brew install --quiet ffmpeg
|
||||
FFMPEG_PATH=$(which ffmpeg)
|
||||
echo "ffmpeg: $FFMPEG_PATH"
|
||||
ffmpeg -version | head -1
|
||||
echo "FFMPEG_PATH=$FFMPEG_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Grant TCC screen recording permission
|
||||
if: ${{ inputs.record_video }}
|
||||
continue-on-error: true
|
||||
run: |
|
||||
FFMPEG_BIN="${FFMPEG_PATH:-/opt/homebrew/bin/ffmpeg}"
|
||||
|
||||
# System-level TCC database (where kTCCServiceScreenCapture lives)
|
||||
SYS_TCC="/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
if [ -f "$SYS_TCC" ]; then
|
||||
echo "Granting screen capture in system TCC database"
|
||||
for client in "$FFMPEG_BIN" /opt/homebrew/bin/ffmpeg /usr/local/bin/ffmpeg /usr/sbin/screencapture; do
|
||||
sudo sqlite3 "$SYS_TCC" \
|
||||
"INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, csreq, policy_id, indirect_object_identifier_type, indirect_object_identifier, indirect_object_code_identity, flags, last_modified) VALUES ('kTCCServiceScreenCapture', '$client', 1, 2, 4, 1, NULL, NULL, 0, 'UNUSED', NULL, 0, $(date +%s));" 2>&1 || echo " (failed for $client)"
|
||||
done
|
||||
fi
|
||||
|
||||
# User-level TCC database (fallback)
|
||||
USER_TCC="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
if [ -f "$USER_TCC" ]; then
|
||||
echo "Granting screen capture in user TCC database"
|
||||
for client in "$FFMPEG_BIN" /opt/homebrew/bin/ffmpeg /usr/local/bin/ffmpeg; do
|
||||
sqlite3 "$USER_TCC" \
|
||||
"INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, csreq, policy_id, indirect_object_identifier_type, indirect_object_identifier, indirect_object_code_identity, flags, last_modified) VALUES ('kTCCServiceScreenCapture', '$client', 1, 2, 4, 1, NULL, NULL, 0, 'UNUSED', NULL, 0, $(date +%s));" 2>&1 || echo " (failed for $client)"
|
||||
done
|
||||
fi
|
||||
|
||||
# Suppress Sequoia's ScreenCaptureApprovals prompt by pre-dating approval
|
||||
APPROVALS_PLIST="$HOME/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plist"
|
||||
if [ -d "$(dirname "$APPROVALS_PLIST")" ]; then
|
||||
echo "Pre-dating ScreenCaptureApprovals"
|
||||
# Set approval date far in the future so the monthly prompt never fires
|
||||
defaults write "$APPROVALS_PLIST" "$FFMPEG_BIN" -date "3000-01-01T00:00:00Z" 2>&1 || echo " (failed)"
|
||||
fi
|
||||
|
||||
- name: Clean DerivedData
|
||||
run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-*
|
||||
|
||||
- name: Cache Swift packages
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
path: .ci-source-packages
|
||||
key: spm-${{ inputs.runner || 'macos-15' }}-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
|
||||
restore-keys: spm-${{ inputs.runner || 'macos-15' }}-
|
||||
|
||||
- name: Resolve Swift packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
mkdir -p "$SOURCE_PACKAGES_DIR"
|
||||
for attempt in 1 2 3; do
|
||||
if xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-resolvePackageDependencies; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Failed to resolve Swift packages after 3 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Package resolution failed on attempt $attempt, retrying..."
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
- name: Run UI tests
|
||||
id: tests
|
||||
env:
|
||||
TEST_FILTER: ${{ inputs.test_filter }}
|
||||
TEST_TIMEOUT: ${{ inputs.test_timeout || '120' }}
|
||||
RECORD_VIDEO: ${{ inputs.record_video }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
|
||||
ONLY_TESTING="-only-testing:cmuxUITests/$TEST_FILTER"
|
||||
|
||||
# Start recording right before the test (after build/resolve)
|
||||
if [ "$RECORD_VIDEO" = "true" ]; then
|
||||
DEVLIST=$( ffmpeg -f avfoundation -list_devices true -i "" 2>&1 || true )
|
||||
echo "Available devices:"
|
||||
echo "$DEVLIST" | grep -E "AVFoundation|Capture screen"
|
||||
|
||||
SCREEN_INDEX=$( echo "$DEVLIST" | grep "Capture screen" | head -1 \
|
||||
| sed 's/.*\[\([0-9]*\)\].*/\1/' )
|
||||
SCREEN_INDEX="${SCREEN_INDEX:-0}"
|
||||
echo "Using screen device index: $SCREEN_INDEX"
|
||||
|
||||
ffmpeg -f avfoundation -framerate 10 -capture_cursor 1 \
|
||||
-i "${SCREEN_INDEX}:none" \
|
||||
-c:v libx264 -preset ultrafast -pix_fmt yuv420p \
|
||||
/tmp/test-recording-raw.mp4 </dev/null >/tmp/ffmpeg.log 2>&1 &
|
||||
RECORD_PID=$!
|
||||
echo "RECORD_PID=$RECORD_PID" >> "$GITHUB_ENV"
|
||||
sleep 2
|
||||
|
||||
if kill -0 "$RECORD_PID" 2>/dev/null; then
|
||||
echo "Recording started (PID $RECORD_PID)"
|
||||
else
|
||||
echo "::warning::ffmpeg screen recording failed to start"
|
||||
cat /tmp/ffmpeg.log
|
||||
fi
|
||||
fi
|
||||
|
||||
set +e
|
||||
OUTPUT=$(xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
|
||||
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
|
||||
-disableAutomaticPackageResolution \
|
||||
-destination "platform=macOS" \
|
||||
-maximum-test-execution-time-allowance "$TEST_TIMEOUT" \
|
||||
$ONLY_TESTING test 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Save summary for the issue
|
||||
SUMMARY=$(echo "$OUTPUT" | grep -E "(Test Suite|Executed|FAIL|PASS)" | tail -20)
|
||||
{
|
||||
echo "test_summary<<EOFSUM"
|
||||
echo "$SUMMARY"
|
||||
echo "EOFSUM"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "$EXIT_CODE" -eq 0 ]; then
|
||||
echo "test_result=passed" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "test_result=failed" >> "$GITHUB_OUTPUT"
|
||||
# Save full output for the issue body
|
||||
{
|
||||
echo "test_output<<EOFOUT"
|
||||
echo "$OUTPUT" | tail -200
|
||||
echo "EOFOUT"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Stop recording and trim
|
||||
if: ${{ always() && inputs.record_video && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
# Stop ffmpeg cleanly
|
||||
kill -INT "$RECORD_PID" 2>/dev/null || true
|
||||
for i in $(seq 1 15); do
|
||||
if ! kill -0 "$RECORD_PID" 2>/dev/null; then
|
||||
echo "Recording stopped after ${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
kill -9 "$RECORD_PID" 2>/dev/null || true
|
||||
|
||||
echo "=== raw recording ==="
|
||||
ls -lh /tmp/test-recording-raw.mp4 2>/dev/null || { echo "No recording file"; exit 0; }
|
||||
|
||||
# Trim: detect first non-black frame and cut from there
|
||||
BLACK_END=$(ffmpeg -i /tmp/test-recording-raw.mp4 \
|
||||
-vf "blackdetect=d=0.3:pic_th=0.95:pix_th=0.1" \
|
||||
-an -f null - 2>&1 \
|
||||
| grep "black_end" | tail -1 \
|
||||
| sed 's/.*black_end:\([0-9.]*\).*/\1/' || true)
|
||||
|
||||
if [ -n "$BLACK_END" ] && [ "$BLACK_END" != "0" ]; then
|
||||
echo "Trimming ${BLACK_END}s of black frames from start"
|
||||
ffmpeg -y -i /tmp/test-recording-raw.mp4 -ss "$BLACK_END" \
|
||||
-c:v libx264 -preset ultrafast -pix_fmt yuv420p \
|
||||
/tmp/test-recording.mp4 2>/dev/null
|
||||
else
|
||||
echo "No black frames detected, using raw recording"
|
||||
mv /tmp/test-recording-raw.mp4 /tmp/test-recording.mp4
|
||||
fi
|
||||
|
||||
echo "=== final recording ==="
|
||||
ls -lh /tmp/test-recording.mp4
|
||||
# Print duration
|
||||
ffprobe -v error -show_entries format=duration \
|
||||
-of default=noprint_wrappers=1:nokey=1 /tmp/test-recording.mp4 2>/dev/null \
|
||||
| xargs -I{} echo "Duration: {}s"
|
||||
|
||||
- name: Upload recording artifact
|
||||
if: ${{ always() && inputs.record_video }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: test-recording
|
||||
path: /tmp/test-recording.mp4
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Post results to cmux-dev-artifacts
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.DEV_ARTIFACTS_TOKEN }}
|
||||
TEST_RESULT: ${{ steps.tests.outputs.test_result || 'failed' }}
|
||||
TEST_SUMMARY: ${{ steps.tests.outputs.test_summary }}
|
||||
TEST_OUTPUT: ${{ steps.tests.outputs.test_output }}
|
||||
TEST_FILTER: ${{ inputs.test_filter }}
|
||||
COMMIT_SHA: ${{ steps.sha.outputs.sha }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
RECORD_VIDEO: ${{ inputs.record_video }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
LABEL="$TEST_RESULT"
|
||||
if [ "$TEST_RESULT" = "passed" ]; then
|
||||
STATUS_EMOJI="PASSED"
|
||||
else
|
||||
STATUS_EMOJI="FAILED"
|
||||
fi
|
||||
|
||||
REF_DISPLAY="${{ inputs.ref || github.ref_name }}"
|
||||
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/$RUN_ID"
|
||||
ARTIFACT_URL="$RUN_URL#artifacts"
|
||||
|
||||
BODY="**Status:** $STATUS_EMOJI
|
||||
**Ref:** \`$REF_DISPLAY\`
|
||||
**SHA:** [\`${COMMIT_SHA:0:12}\`](https://github.com/${{ github.repository }}/commit/$COMMIT_SHA)
|
||||
**Test:** \`$TEST_FILTER\`
|
||||
**Workflow run:** $RUN_URL"
|
||||
|
||||
if [ "$RECORD_VIDEO" = "true" ]; then
|
||||
BODY="$BODY
|
||||
**Recording:** [Download from artifacts]($ARTIFACT_URL)"
|
||||
fi
|
||||
|
||||
if [ -n "$TEST_OUTPUT" ]; then
|
||||
BODY="$BODY
|
||||
|
||||
<details><summary>Test output (last 200 lines)</summary>
|
||||
|
||||
\`\`\`
|
||||
$TEST_OUTPUT
|
||||
\`\`\`
|
||||
|
||||
</details>"
|
||||
fi
|
||||
|
||||
if [ -n "$TEST_SUMMARY" ]; then
|
||||
BODY="$BODY
|
||||
|
||||
\`\`\`
|
||||
$TEST_SUMMARY
|
||||
\`\`\`"
|
||||
fi
|
||||
|
||||
ISSUE_URL=$(gh issue create \
|
||||
--repo manaflow-ai/cmux-dev-artifacts \
|
||||
--title "[$STATUS_EMOJI] $TEST_FILTER @ ${COMMIT_SHA:0:7} ($REF_DISPLAY)" \
|
||||
--body "$BODY" \
|
||||
--label "$LABEL")
|
||||
|
||||
echo "Issue posted: $ISSUE_URL"
|
||||
echo "::notice title=Test Result Issue::$ISSUE_URL"
|
||||
18
.github/workflows/update-homebrew.yml
vendored
|
|
@ -37,11 +37,22 @@ jobs:
|
|||
echo "Could not determine version" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "Invalid version: ${VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Skipping homebrew cask update for non-release ref: ${VERSION}"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Updating homebrew cask to version $VERSION"
|
||||
|
||||
- name: Download DMG and get SHA256
|
||||
id: sha
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
URL="https://github.com/manaflow-ai/cmux/releases/download/v${VERSION}/cmux-macos.dmg"
|
||||
|
|
@ -65,13 +76,15 @@ jobs:
|
|||
echo "DMG SHA256: $SHA256"
|
||||
|
||||
- name: Checkout homebrew-cmux
|
||||
uses: actions/checkout@v4
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
repository: manaflow-ai/homebrew-cmux
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
path: homebrew-cmux
|
||||
|
||||
- name: Update cask formula
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
SHA256: ${{ steps.sha.outputs.sha256 }}
|
||||
|
|
@ -94,6 +107,7 @@ jobs:
|
|||
depends_on macos: ">= :sonoma"
|
||||
|
||||
app "cmux.app"
|
||||
binary "#{appdir}/cmux.app/Contents/Resources/bin/cmux"
|
||||
|
||||
zap trash: [
|
||||
"~/Library/Application Support/cmux",
|
||||
|
|
@ -106,6 +120,7 @@ jobs:
|
|||
sed -i 's/^ //' homebrew-cmux/Casks/cmux.rb
|
||||
|
||||
- name: Verify cask SHA matches DMG
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
CASK_SHA=$(grep 'sha256' homebrew-cmux/Casks/cmux.rb | sed 's/.*"\(.*\)".*/\1/')
|
||||
ACTUAL_SHA=$(shasum -a 256 cmux.dmg | cut -d' ' -f1)
|
||||
|
|
@ -116,6 +131,7 @@ jobs:
|
|||
echo "SHA verification passed: $CASK_SHA"
|
||||
|
||||
- name: Commit and push
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
|
|
|
|||
BIN
AppIcon.icon/Assets/cmux-icon-chevron 2.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
35
AppIcon.icon/icon.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"fill" : "automatic",
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "cmux-icon-chevron 2.png",
|
||||
"name" : "cmux-icon-chevron 2",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
37.357790031201375,
|
||||
-0.5
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 622 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 385 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 24 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/128@2x_dark.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/128_dark.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 587 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/16@2x_dark.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/16_dark.png
Normal file
|
After Width: | Height: | Size: 591 B |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 92 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/256@2x_dark.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/256_dark.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/32@2x_dark.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/32_dark.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 404 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/512@2x_dark.png
Normal file
|
After Width: | Height: | Size: 659 KiB |
BIN
Assets.xcassets/AppIcon.appiconset/512_dark.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
|
@ -1,68 +1,188 @@
|
|||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
"filename": "16.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "16_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
"filename": "16@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "16@2x_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
"filename": "32.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "32_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
"filename": "32@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "32@2x_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
"filename": "128.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "128_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "128@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "128@2x_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "256.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "256_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "256@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "256@2x_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "512.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "512_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"filename": "512@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"filename": "512@2x_dark.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
Assets.xcassets/AppIconDark.imageset/AppIconDark.png
vendored
Normal file
|
After Width: | Height: | Size: 659 KiB |
12
Assets.xcassets/AppIconDark.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconDark.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Assets.xcassets/AppIconLight.imageset/AppIconLight.png
vendored
Normal file
|
After Width: | Height: | Size: 404 KiB |
12
Assets.xcassets/AppIconLight.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconLight.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
185
CHANGELOG.md
|
|
@ -2,6 +2,191 @@
|
|||
|
||||
All notable changes to cmux are documented here.
|
||||
|
||||
## [0.62.1] - 2026-03-13
|
||||
|
||||
### Added
|
||||
- Cmd+T (New tab) shortcut on the welcome screen ([#1258](https://github.com/manaflow-ai/cmux/pull/1258))
|
||||
|
||||
### Fixed
|
||||
- Cmd+backtick window cycling skipping windows
|
||||
- Titlebar shortcut hint clipping ([#1259](https://github.com/manaflow-ai/cmux/pull/1259))
|
||||
- Terminal portals desyncing after sidebar changes ([#1253](https://github.com/manaflow-ai/cmux/pull/1253))
|
||||
- Background terminal focus retries reordering windows
|
||||
- Pure-style multiline prompt redraws in Ghostty
|
||||
- Return key not working on Cmd+Ctrl+W close confirmation ([#1279](https://github.com/manaflow-ai/cmux/pull/1279))
|
||||
- Concurrent remote daemon RPC calls timing out ([#1281](https://github.com/manaflow-ai/cmux/pull/1281))
|
||||
|
||||
### Removed
|
||||
- SSH remote port proxying (reverted, will return in a future release)
|
||||
|
||||
## [0.62.0] - 2026-03-12
|
||||
|
||||
### Added
|
||||
- Markdown viewer panel with live file watching ([#883](https://github.com/manaflow-ai/cmux/pull/883))
|
||||
- Find-in-page (Cmd+F) for browser panels ([#837](https://github.com/manaflow-ai/cmux/issues/837), [#875](https://github.com/manaflow-ai/cmux/pull/875))
|
||||
- Keyboard copy mode for terminal scrollback with vi-style navigation ([#792](https://github.com/manaflow-ai/cmux/pull/792))
|
||||
- Custom notification sounds with file picker support ([#839](https://github.com/manaflow-ai/cmux/pull/839), [#869](https://github.com/manaflow-ai/cmux/pull/869))
|
||||
- Browser camera and microphone permission support ([#760](https://github.com/manaflow-ai/cmux/issues/760), [#913](https://github.com/manaflow-ai/cmux/pull/913))
|
||||
- Language setting for per-app locale override ([#886](https://github.com/manaflow-ai/cmux/pull/886))
|
||||
- Japanese localization ([#819](https://github.com/manaflow-ai/cmux/pull/819))
|
||||
- 16 new languages added to localization ([#895](https://github.com/manaflow-ai/cmux/pull/895))
|
||||
- Kagi as a search provider option ([#561](https://github.com/manaflow-ai/cmux/pull/561))
|
||||
- Open Folder command (Cmd+O) ([#656](https://github.com/manaflow-ai/cmux/pull/656))
|
||||
- Dark mode app icon for macOS Sequoia ([#702](https://github.com/manaflow-ai/cmux/pull/702))
|
||||
- Close other pane tabs with confirmation ([#475](https://github.com/manaflow-ai/cmux/pull/475))
|
||||
- Flash Focused Panel command palette action ([#638](https://github.com/manaflow-ai/cmux/pull/638))
|
||||
- Zoom/maximize focused pane in splits ([#634](https://github.com/manaflow-ai/cmux/pull/634))
|
||||
- `cmux tree` command for full CLI hierarchy view ([#592](https://github.com/manaflow-ai/cmux/pull/592))
|
||||
- Install or uninstall the `cmux` CLI from the command palette ([#626](https://github.com/manaflow-ai/cmux/pull/626))
|
||||
- Clipboard image paste in terminal with Cmd+V ([#562](https://github.com/manaflow-ai/cmux/pull/562), [#853](https://github.com/manaflow-ai/cmux/pull/853))
|
||||
- Middle-click X11-style selection paste in terminal ([#369](https://github.com/manaflow-ai/cmux/pull/369))
|
||||
- Honor Ghostty `background-opacity` across all cmux chrome ([#667](https://github.com/manaflow-ai/cmux/pull/667))
|
||||
- Setting to hide Cmd-hold shortcut hints ([#765](https://github.com/manaflow-ai/cmux/pull/765))
|
||||
- Focus-follows-mouse on terminal hover ([#519](https://github.com/manaflow-ai/cmux/pull/519))
|
||||
- Sidebar help menu in the footer ([#958](https://github.com/manaflow-ai/cmux/pull/958))
|
||||
- External URL bypass rules for the embedded browser ([#768](https://github.com/manaflow-ai/cmux/pull/768))
|
||||
- Telemetry opt-out setting ([#610](https://github.com/manaflow-ai/cmux/pull/610))
|
||||
- Browser automation docs page ([#622](https://github.com/manaflow-ai/cmux/pull/622))
|
||||
- Vim mode indicator badge on terminal panes ([#1092](https://github.com/manaflow-ai/cmux/pull/1092))
|
||||
- Sidebar workspace color in CLI sidebar_state output ([#1101](https://github.com/manaflow-ai/cmux/pull/1101))
|
||||
- Prompt before closing window with Cmd+Ctrl+W ([#1219](https://github.com/manaflow-ai/cmux/pull/1219))
|
||||
- Jump to Latest button in notifications popover ([#1167](https://github.com/manaflow-ai/cmux/pull/1167))
|
||||
- Khmer localization ([#1198](https://github.com/manaflow-ai/cmux/pull/1198))
|
||||
- cmux claude-teams launcher ([#1179](https://github.com/manaflow-ai/cmux/pull/1179))
|
||||
|
||||
### Changed
|
||||
- Command palette search is now async and decoupled from typing for reduced lag
|
||||
- Fuzzy matching improved with single-edit and omitted-character word matches
|
||||
- Replaced keychain password storage with file-based storage ([#576](https://github.com/manaflow-ai/cmux/pull/576))
|
||||
- Fullscreen shortcut changed to Cmd+Ctrl+F, and Cmd+Enter also toggles fullscreen ([#530](https://github.com/manaflow-ai/cmux/pull/530))
|
||||
- Workspace rename shortcut Cmd+Shift+R now uses the command palette flow
|
||||
- Renamed tab color to workspace color in user-facing strings ([#637](https://github.com/manaflow-ai/cmux/pull/637))
|
||||
- Feedback recipient changed to `feedback@manaflow.com` ([#1007](https://github.com/manaflow-ai/cmux/pull/1007))
|
||||
- Regenerated app icons from Icon Composer ([#1005](https://github.com/manaflow-ai/cmux/pull/1005))
|
||||
- Moved update logs into the Debug menu ([#1008](https://github.com/manaflow-ai/cmux/pull/1008))
|
||||
- Updated Ghostty to v1.3.0 ([#1142](https://github.com/manaflow-ai/cmux/pull/1142))
|
||||
- Welcome screen colors adapted for light mode ([#1214](https://github.com/manaflow-ai/cmux/pull/1214))
|
||||
- Notification sound picker width constrained ([#1168](https://github.com/manaflow-ai/cmux/pull/1168))
|
||||
|
||||
### Fixed
|
||||
- Frozen blank launch from session restore race condition ([#399](https://github.com/manaflow-ai/cmux/issues/399), [#565](https://github.com/manaflow-ai/cmux/pull/565))
|
||||
- Crash on launch from an exclusive access violation in drag-handle hit testing ([#490](https://github.com/manaflow-ai/cmux/issues/490))
|
||||
- Use-after-free in `ghostty_surface_refresh` after sleep/wake ([#432](https://github.com/manaflow-ai/cmux/issues/432), [#619](https://github.com/manaflow-ai/cmux/pull/619))
|
||||
- Startup SIGSEGV by pre-warming locale before `SentrySDK.start` ([#927](https://github.com/manaflow-ai/cmux/pull/927))
|
||||
- IME issues: Shift+Space toggle inserting a space ([#641](https://github.com/manaflow-ai/cmux/issues/641), [#670](https://github.com/manaflow-ai/cmux/pull/670)), Ctrl fast path blocking IME events, browser address bar Japanese IME ([#789](https://github.com/manaflow-ai/cmux/issues/789), [#867](https://github.com/manaflow-ai/cmux/pull/867)), and Cmd shortcuts during IME composition
|
||||
- CLI socket autodiscovery for tagged sockets ([#832](https://github.com/manaflow-ai/cmux/pull/832))
|
||||
- Flaky CLI socket listener recovery ([#952](https://github.com/manaflow-ai/cmux/issues/952), [#954](https://github.com/manaflow-ai/cmux/pull/954))
|
||||
- Side-docked dev tools resize ([#712](https://github.com/manaflow-ai/cmux/pull/712))
|
||||
- Dvorak Cmd+C colliding with the notifications shortcut ([#762](https://github.com/manaflow-ai/cmux/pull/762))
|
||||
- Terminal drag hover overlay flicker
|
||||
- Titlebar controls clipped at the bottom edge ([#1016](https://github.com/manaflow-ai/cmux/pull/1016))
|
||||
- Sidebar git branch recovery after sleep/wake and agent checkout ([#494](https://github.com/manaflow-ai/cmux/issues/494), [#671](https://github.com/manaflow-ai/cmux/pull/671), [#905](https://github.com/manaflow-ai/cmux/pull/905))
|
||||
- Browser portal routing, uploads, and click focus regressions ([#908](https://github.com/manaflow-ai/cmux/pull/908), [#961](https://github.com/manaflow-ai/cmux/pull/961))
|
||||
- Notification unread persistence on workspace focus
|
||||
- Escape propagation when the command palette is visible ([#847](https://github.com/manaflow-ai/cmux/pull/847))
|
||||
- Cmd+Shift+Enter pane zoom regression in browser focus ([#826](https://github.com/manaflow-ai/cmux/pull/826))
|
||||
- Cross-window theme background after jump-to-unread ([#861](https://github.com/manaflow-ai/cmux/pull/861))
|
||||
- `window.open()` and `target=_blank` not opening in a new tab ([#693](https://github.com/manaflow-ai/cmux/pull/693))
|
||||
- Terminal wrap width for the overlay scrollbar ([#522](https://github.com/manaflow-ai/cmux/pull/522))
|
||||
- Orphaned child processes when closing workspace tabs ([#889](https://github.com/manaflow-ai/cmux/pull/889))
|
||||
- Cmd+F Escape passthrough into terminal ([#918](https://github.com/manaflow-ai/cmux/pull/918))
|
||||
- Terminal link opens staying in the source workspace ([#912](https://github.com/manaflow-ai/cmux/pull/912))
|
||||
- Ghost terminal surface rebind after close ([#808](https://github.com/manaflow-ai/cmux/pull/808))
|
||||
- Cmd+plus zoom handling on non-US keyboard layouts ([#680](https://github.com/manaflow-ai/cmux/pull/680))
|
||||
- Menubar icon invisible in light mode ([#741](https://github.com/manaflow-ai/cmux/pull/741))
|
||||
- Various drag-handle crash fixes and reentrancy guards
|
||||
- Background workspace git metadata refresh after external checkout
|
||||
- Markdown panel text click focus ([#991](https://github.com/manaflow-ai/cmux/pull/991))
|
||||
- Browser Cmd+F overlay clipping in portal mode ([#916](https://github.com/manaflow-ai/cmux/pull/916))
|
||||
- Voice dictation text insertion ([#857](https://github.com/manaflow-ai/cmux/pull/857))
|
||||
- Browser panel lifecycle after WebContent process termination ([#892](https://github.com/manaflow-ai/cmux/pull/892))
|
||||
- Typing lag reduction by hiding invisible views from the accessibility tree ([#862](https://github.com/manaflow-ai/cmux/pull/862))
|
||||
- CJK font fallback preventing decorative font rendering for CJK characters ([#1017](https://github.com/manaflow-ai/cmux/pull/1017))
|
||||
- Inline VS Code serve-web token exposure via argv ([#1033](https://github.com/manaflow-ai/cmux/pull/1033))
|
||||
- Browser pane portal anchor sizing ([#1094](https://github.com/manaflow-ai/cmux/pull/1094))
|
||||
- Pinned workspace notification reordering ([#1116](https://github.com/manaflow-ai/cmux/pull/1116))
|
||||
- cmux --version memory blowup ([#1121](https://github.com/manaflow-ai/cmux/pull/1121))
|
||||
- Notification ring dismissal on direct terminal clicks ([#1126](https://github.com/manaflow-ai/cmux/pull/1126))
|
||||
- Browser portal visibility when terminal tab is active ([#1130](https://github.com/manaflow-ai/cmux/pull/1130))
|
||||
- Browser panes reloading when switching workspaces ([#1136](https://github.com/manaflow-ai/cmux/pull/1136))
|
||||
- Sidebar PR badge detection ([#1139](https://github.com/manaflow-ai/cmux/pull/1139))
|
||||
- Browser address bar disappearing during pane zoom ([#1145](https://github.com/manaflow-ai/cmux/pull/1145))
|
||||
- Ghost terminal surface focus after split close ([#1148](https://github.com/manaflow-ai/cmux/pull/1148))
|
||||
- Browser DevTools resize loop and layout stability ([#1170](https://github.com/manaflow-ai/cmux/pull/1170), [#1173](https://github.com/manaflow-ai/cmux/pull/1173), [#1189](https://github.com/manaflow-ai/cmux/pull/1189))
|
||||
- Typing lag from sidebar re-evaluation and hitTest overhead ([#1204](https://github.com/manaflow-ai/cmux/issues/1204))
|
||||
- Browser pane stale content after drag splits ([#1215](https://github.com/manaflow-ai/cmux/pull/1215))
|
||||
- Terminal drop overlay misplacement during drag hover ([#1213](https://github.com/manaflow-ai/cmux/pull/1213))
|
||||
- Hidden browser slot inspector focus crash ([#1211](https://github.com/manaflow-ai/cmux/pull/1211))
|
||||
- Browser devtools hide fallback ([#1220](https://github.com/manaflow-ai/cmux/pull/1220))
|
||||
- Browser portal refresh on geometry churn ([#1224](https://github.com/manaflow-ai/cmux/pull/1224))
|
||||
- Browser tab switch triggering unnecessary reload ([#1228](https://github.com/manaflow-ai/cmux/pull/1228))
|
||||
- Devtools side dock guard for attached devtools ([#1230](https://github.com/manaflow-ai/cmux/pull/1230))
|
||||
|
||||
### Thanks to 24 contributors!
|
||||
- [@0xble](https://github.com/0xble)
|
||||
- [@afxjzs](https://github.com/afxjzs)
|
||||
- [@AI-per](https://github.com/AI-per)
|
||||
- [@atani](https://github.com/atani)
|
||||
- [@atmigtnca](https://github.com/atmigtnca)
|
||||
- [@austinywang](https://github.com/austinywang)
|
||||
- [@cheulyop](https://github.com/cheulyop)
|
||||
- [@ConnorCallison](https://github.com/ConnorCallison)
|
||||
- [@gonzaloserrano](https://github.com/gonzaloserrano)
|
||||
- [@harukitosa](https://github.com/harukitosa)
|
||||
- [@homanp](https://github.com/homanp)
|
||||
- [@JLeeChan](https://github.com/JLeeChan)
|
||||
- [@josemasri](https://github.com/josemasri)
|
||||
- [@lawrencecchen](https://github.com/lawrencecchen)
|
||||
- [@novarii](https://github.com/novarii)
|
||||
- [@orkhanrz](https://github.com/orkhanrz)
|
||||
- [@qianwan](https://github.com/qianwan)
|
||||
- [@rjwittams](https://github.com/rjwittams)
|
||||
- [@sminamot](https://github.com/sminamot)
|
||||
- [@tmcarr](https://github.com/tmcarr)
|
||||
- [@trydis](https://github.com/trydis)
|
||||
- [@ukoasis](https://github.com/ukoasis)
|
||||
- [@y-agatsuma](https://github.com/y-agatsuma)
|
||||
- [@yasunogithub](https://github.com/yasunogithub)
|
||||
|
||||
## [0.61.0] - 2026-02-25
|
||||
|
||||
### Added
|
||||
- Command palette (Cmd+Shift+P) with update actions and all-window switcher results ([#358](https://github.com/manaflow-ai/cmux/pull/358), [#361](https://github.com/manaflow-ai/cmux/pull/361))
|
||||
- Split actions and shortcut hints in terminal context menus
|
||||
- Cross-window tab and workspace move UI with improved destination focus behavior
|
||||
- Sidebar pull request metadata rows and workspace PR open actions
|
||||
- Workspace color schemes and left-rail workspace indicator settings ([#324](https://github.com/manaflow-ai/cmux/pull/324), [#329](https://github.com/manaflow-ai/cmux/pull/329), [#332](https://github.com/manaflow-ai/cmux/pull/332))
|
||||
- URL open-wrapper routing into the embedded browser ([#332](https://github.com/manaflow-ai/cmux/pull/332))
|
||||
- Cmd+Q quit warning with suppression toggle ([#295](https://github.com/manaflow-ai/cmux/pull/295))
|
||||
- `cmux --version` output now includes commit metadata
|
||||
|
||||
### Changed
|
||||
- Added light mode and unified theme refresh across app surfaces ([#258](https://github.com/manaflow-ai/cmux/pull/258)) — thanks @ijpatricio for the report!
|
||||
- Browser link middle-click handling now uses native WebKit behavior ([#416](https://github.com/manaflow-ai/cmux/pull/416))
|
||||
- Settings-window actions now route through a single command-palette/settings flow
|
||||
- Sentry upgraded with tracing, breadcrumbs, and dSYM upload support ([#366](https://github.com/manaflow-ai/cmux/pull/366))
|
||||
- Session restore scope clarification: cmux restores layout, working directory, scrollback, and browser history, but does not resume live terminal process state yet
|
||||
|
||||
### Fixed
|
||||
- Startup split hang when pressing Cmd+D then Ctrl+D early after launch ([#364](https://github.com/manaflow-ai/cmux/pull/364))
|
||||
- Browser focus handoff and click-to-focus regressions in mixed terminal/browser workspaces ([#381](https://github.com/manaflow-ai/cmux/pull/381), [#355](https://github.com/manaflow-ai/cmux/pull/355))
|
||||
- Caps Lock handling in browser omnibar keyboard paths ([#382](https://github.com/manaflow-ai/cmux/pull/382))
|
||||
- Embedded browser deeplink URL scheme handling ([#392](https://github.com/manaflow-ai/cmux/pull/392))
|
||||
- Sidebar resize cap regression ([#393](https://github.com/manaflow-ai/cmux/pull/393))
|
||||
- Terminal zoom inheritance for new splits, surfaces, and workspaces ([#384](https://github.com/manaflow-ai/cmux/pull/384))
|
||||
- Terminal find overlay layering across split and portal-hosted layouts
|
||||
- Titlebar drag and double-click zoom handling on browser-side panes
|
||||
- Stale browser favicon and window-title updates after navigation
|
||||
|
||||
### Thanks to 7 contributors!
|
||||
- [@austinywang](https://github.com/austinywang)
|
||||
- [@avisser](https://github.com/avisser)
|
||||
- [@gnguralnick](https://github.com/gnguralnick)
|
||||
- [@ijpatricio](https://github.com/ijpatricio)
|
||||
- [@jperkin](https://github.com/jperkin)
|
||||
- [@jungcome7](https://github.com/jungcome7)
|
||||
- [@lawrencecchen](https://github.com/lawrencecchen)
|
||||
|
||||
## [0.60.0] - 2026-02-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
77
CLAUDE.md
|
|
@ -16,16 +16,40 @@ After making code changes, always run the reload script with a tag to launch the
|
|||
./scripts/reload.sh --tag fix-zsh-autosuggestions
|
||||
```
|
||||
|
||||
After making code changes, always run the build:
|
||||
When reporting a tagged reload result in chat, use the format for your agent type:
|
||||
|
||||
**Claude Code** (markdown link with correct derived-data path, cmd+clickable):
|
||||
```markdown
|
||||
=======================================================
|
||||
[cmux DEV <tag-name>.app](file:///Users/lawrencechen/Library/Developer/Xcode/DerivedData/cmux-<tag-name>/Build/Products/Debug/cmux%20DEV%20<tag-name>.app)
|
||||
=======================================================
|
||||
```
|
||||
|
||||
**Codex** (plain text format):
|
||||
```
|
||||
=======================================================
|
||||
[<tag-name>: file:///Users/lawrencechen/Library/Developer/Xcode/DerivedData/cmux-<tag-name>/Build/Products/Debug/cmux%20DEV%20<tag-name>.app](file:///Users/lawrencechen/Library/Developer/Xcode/DerivedData/cmux-<tag-name>/Build/Products/Debug/cmux%20DEV%20<tag-name>.app)
|
||||
=======================================================
|
||||
```
|
||||
|
||||
Never use `/tmp/cmux-<tag>/...` app links in chat output. If the expected DerivedData path is missing, resolve the real `.app` path and report that `file://` URL.
|
||||
|
||||
After making code changes, always use `reload.sh --tag` to build and launch. **Never run bare `xcodebuild` or `open` an untagged `cmux DEV.app`.** Untagged builds share the default debug socket and bundle ID with other agents, causing conflicts and stealing focus.
|
||||
|
||||
```bash
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' build
|
||||
./scripts/reload.sh --tag <your-branch-slug>
|
||||
```
|
||||
|
||||
If you only need to verify the build compiles (no launch), use a tagged derivedDataPath:
|
||||
|
||||
```bash
|
||||
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-<your-tag> build
|
||||
```
|
||||
|
||||
When rebuilding GhosttyKit.xcframework, always use Release optimizations:
|
||||
|
||||
```bash
|
||||
cd ghostty && zig build -Demit-xcframework=true -Doptimize=ReleaseFast
|
||||
cd ghostty && zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast
|
||||
```
|
||||
|
||||
When rebuilding cmuxd for release/bundling, always use ReleaseFast:
|
||||
|
|
@ -89,11 +113,35 @@ tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug
|
|||
- Focus events: `focus.panel`, `focus.bonsplit`, `focus.firstResponder`, `focus.moveFocus`
|
||||
- Bonsplit events: `tab.select`, `tab.close`, `tab.dragStart`, `tab.drop`, `pane.focus`, `pane.drop`, `divider.dragStart`
|
||||
|
||||
## Regression test commit policy
|
||||
|
||||
When adding a regression test for a bug fix, use a two-commit structure so CI proves the test catches the bug:
|
||||
|
||||
1. **Commit 1:** Add the failing test only (no fix). CI should go red.
|
||||
2. **Commit 2:** Add the fix. CI should go green.
|
||||
|
||||
This makes it visible in the GitHub PR UI (Commits tab, check statuses) that the test genuinely fails without the fix.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Custom UTTypes** for drag-and-drop must be declared in `Resources/Info.plist` under `UTExportedTypeDeclarations` (e.g. `com.splittabbar.tabtransfer`, `com.cmux.sidebar-tab-reorder`).
|
||||
- Do not add an app-level display link or manual `ghostty_surface_draw` loop; rely on Ghostty wakeups/renderer to avoid typing lag.
|
||||
- **Typing-latency-sensitive paths** (read carefully before touching these areas):
|
||||
- `WindowTerminalHostView.hitTest()` in `TerminalWindowPortal.swift`: called on every event including keyboard. All divider/sidebar/drag routing is gated to pointer events only. Do not add work outside the `isPointerEvent` guard.
|
||||
- `TabItemView` in `ContentView.swift`: uses `Equatable` conformance + `.equatable()` to skip body re-evaluation during typing. Do not add `@EnvironmentObject`, `@ObservedObject` (besides `tab`), or `@Binding` properties without updating the `==` function. Do not remove `.equatable()` from the ForEach call site. Do not read `tabManager` or `notificationStore` in the body; use the precomputed `let` parameters instead.
|
||||
- `TerminalSurface.forceRefresh()` in `GhosttyTerminalView.swift`: called on every keystroke. Do not add allocations, file I/O, or formatting here.
|
||||
- **Terminal find layering contract:** `SurfaceSearchOverlay` must be mounted from `GhosttySurfaceScrollView` in `Sources/GhosttyTerminalView.swift` (AppKit portal layer), not from SwiftUI panel containers such as `Sources/Panels/TerminalPanelView.swift`. Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.
|
||||
- **Submodule safety:** When modifying a submodule (ghostty, vendor/bonsplit, etc.), always push the submodule commit to its remote `main` branch BEFORE committing the updated pointer in the parent repo. Never commit on a detached HEAD or temporary branch — the commit will be orphaned and lost. Verify with: `cd <submodule> && git merge-base --is-ancestor HEAD origin/main`.
|
||||
- **All user-facing strings must be localized.** Use `String(localized: "key.name", defaultValue: "English text")` for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages). Keys go in `Resources/Localizable.xcstrings` with translations for all supported languages (currently English and Japanese). Never use bare string literals in SwiftUI `Text()`, `Button()`, alert titles, etc.
|
||||
|
||||
## Test quality policy
|
||||
|
||||
- Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns.
|
||||
- Do not add tests that read checked-in metadata or project files such as `Resources/Info.plist`, `project.pbxproj`, `.xcconfig`, or source files only to assert that a key, string, plist entry, or snippet exists.
|
||||
- Tests must verify observable runtime behavior through executable paths (unit/integration/e2e/CLI), not implementation shape.
|
||||
- For metadata changes, prefer verifying the built app bundle or the runtime behavior that depends on that metadata, not the checked-in source file.
|
||||
- If a behavior cannot be exercised end-to-end yet, add a small runtime seam or harness first, then test through that seam.
|
||||
- If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and state that explicitly.
|
||||
|
||||
## Socket command threading policy
|
||||
|
||||
|
|
@ -111,21 +159,14 @@ tail -f "$(cat /tmp/cmux-last-debug-log-path 2>/dev/null || echo /tmp/cmux-debug
|
|||
- Only explicit focus-intent commands may mutate in-app focus/selection (`window.focus`, `workspace.select/next/previous/last`, `surface.focus`, `pane.focus/last`, browser focus commands, and v1 focus equivalents).
|
||||
- All non-focus commands should preserve current user focus context while still applying data/model changes.
|
||||
|
||||
## E2E mac UI tests
|
||||
## Testing policy
|
||||
|
||||
Run UI tests on the UTM macOS VM (never on the host machine). Always run e2e UI tests via `ssh cmux-vm`:
|
||||
**Never run tests locally.** All tests (E2E, UI, python socket tests) run via GitHub Actions or on the VM.
|
||||
|
||||
```bash
|
||||
ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:cmuxUITests/UpdatePillUITests test'
|
||||
```
|
||||
|
||||
## Basic tests
|
||||
|
||||
Run basic automated tests on the UTM macOS VM (never on the host machine):
|
||||
|
||||
```bash
|
||||
ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" build && pkill -x "cmux DEV" || true && APP=$(find /Users/cmux/Library/Developer/Xcode/DerivedData -path "*/Build/Products/Debug/cmux DEV.app" -print -quit) && open "$APP" --env CMUX_SOCKET_MODE=allowAll && for i in {1..20}; do [ -S /tmp/cmux-debug.sock ] && break; sleep 0.5; done && python3 tests/test_update_timing.py && python3 tests/test_signals_auto.py && python3 tests/test_ctrl_socket.py && python3 tests/test_notifications.py'
|
||||
```
|
||||
- **E2E / UI tests:** trigger via `gh workflow run test-e2e.yml` (see cmuxterm-hq CLAUDE.md for details)
|
||||
- **Unit tests:** `xcodebuild -scheme cmux-unit` is safe (no app launch), but prefer CI
|
||||
- **Python socket tests (tests_v2/):** these connect to a running cmux instance's socket. Never launch an untagged `cmux DEV.app` to run them. If you must test locally, use a tagged build's socket (`/tmp/cmux-debug-<tag>.sock`) with `CMUX_SOCKET=/tmp/cmux-debug-<tag>.sock`
|
||||
- **Never `open` an untagged `cmux DEV.app`** from DerivedData. It conflicts with the user's running debug instance.
|
||||
|
||||
## Ghostty submodule workflow
|
||||
|
||||
|
|
@ -164,7 +205,7 @@ git commit -m "Update ghostty submodule"
|
|||
Use the `/release` command to prepare a new release. This will:
|
||||
1. Determine the new version (bumps minor by default)
|
||||
2. Gather commits since the last tag and update the changelog
|
||||
3. Update `CHANGELOG.md` and `docs-site/content/docs/changelog.mdx`
|
||||
3. Update `CHANGELOG.md` (the docs changelog page at `web/app/docs/changelog/page.tsx` reads from it)
|
||||
4. Run `./scripts/bump-version.sh` to update both versions
|
||||
5. Commit, tag, and push
|
||||
|
||||
|
|
@ -193,4 +234,4 @@ Notes:
|
|||
- The release asset is `cmux-macos.dmg` attached to the tag.
|
||||
- README download button points to `releases/latest/download/cmux-macos.dmg`.
|
||||
- Versioning: bump the minor version for updates unless explicitly asked otherwise.
|
||||
- Changelog: always update both `CHANGELOG.md` and the docs-site version.
|
||||
- Changelog: update `CHANGELOG.md`; docs changelog is rendered from it.
|
||||
|
|
|
|||
4160
CLI/cmux.swift
|
|
@ -22,11 +22,16 @@
|
|||
A5001500 /* CmuxWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001510 /* CmuxWebView.swift */; };
|
||||
A5001501 /* UITestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001511 /* UITestRecorder.swift */; };
|
||||
A5001226 /* SocketControlSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001225 /* SocketControlSettings.swift */; };
|
||||
A5001601 /* SentryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001600 /* SentryHelper.swift */; };
|
||||
A5001621 /* AppleScriptSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001620 /* AppleScriptSupport.swift */; };
|
||||
A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; };
|
||||
A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; };
|
||||
A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; };
|
||||
A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; };
|
||||
A5001404 /* BrowserPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001414 /* BrowserPanelView.swift */; };
|
||||
A5001420 /* MarkdownPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001418 /* MarkdownPanel.swift */; };
|
||||
A5001421 /* MarkdownPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001419 /* MarkdownPanelView.swift */; };
|
||||
A5001290 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = A5001291 /* MarkdownUI */; };
|
||||
A5001405 /* PanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001415 /* PanelContentView.swift */; };
|
||||
A5001406 /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001416 /* Workspace.swift */; };
|
||||
A5001407 /* WorkspaceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001417 /* WorkspaceContentView.swift */; };
|
||||
|
|
@ -34,10 +39,14 @@
|
|||
A5001094 /* NotificationsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001091 /* NotificationsPage.swift */; };
|
||||
A5001095 /* TerminalNotificationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001092 /* TerminalNotificationStore.swift */; };
|
||||
A5001250 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = A5001251 /* Sentry */; };
|
||||
B9000024A1B2C3D4E5F60719 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = A5001251 /* Sentry */; };
|
||||
A5001270 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = A5001271 /* PostHog */; };
|
||||
A5001303 /* SurfaceSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001301 /* SurfaceSearchOverlay.swift */; };
|
||||
A5008371 /* BrowserSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008370 /* BrowserSearchOverlay.swift */; };
|
||||
A5008373 /* BrowserFindJavaScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008372 /* BrowserFindJavaScript.swift */; };
|
||||
A50012F1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F0 /* Backport.swift */; };
|
||||
A50012F3 /* KeyboardShortcutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F2 /* KeyboardShortcutSettings.swift */; };
|
||||
A50012F5 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F4 /* KeyboardLayout.swift */; };
|
||||
A5001521 /* PostHogAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001520 /* PostHogAnalytics.swift */; };
|
||||
A5001201 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001211 /* UpdateController.swift */; };
|
||||
A5001202 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001212 /* UpdateDelegate.swift */; };
|
||||
|
|
@ -54,28 +63,42 @@
|
|||
A5001208 /* UpdateTitlebarAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001218 /* UpdateTitlebarAccessory.swift */; };
|
||||
A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; };
|
||||
A5001240 /* WindowDecorationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001241 /* WindowDecorationsController.swift */; };
|
||||
A5001610 /* SessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001611 /* SessionPersistence.swift */; };
|
||||
A5001100 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5001101 /* Assets.xcassets */; };
|
||||
A5001230 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A5001231 /* Sparkle */; };
|
||||
B9000002A1B2C3D4E5F60719 /* cmux.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000001A1B2C3D4E5F60719 /* cmux.swift */; };
|
||||
B900000BA1B2C3D4E5F60719 /* cmux in Copy CLI */ = {isa = PBXBuildFile; fileRef = B9000004A1B2C3D4E5F60719 /* cmux */; };
|
||||
C1ADE00002A1B2C3D4E5F719 /* claude in Copy CLI */ = {isa = PBXBuildFile; fileRef = C1ADE00001A1B2C3D4E5F719 /* claude */; };
|
||||
D1BEF00002A1B2C3D4E5F719 /* open in Copy CLI */ = {isa = PBXBuildFile; fileRef = D1BEF00001A1B2C3D4E5F719 /* open */; };
|
||||
84E00D47E4584162AE53BC8D /* xterm-ghostty in Resources */ = {isa = PBXBuildFile; fileRef = B2E7294509CC42FE9191870E /* xterm-ghostty */; };
|
||||
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */ = {isa = PBXBuildFile; fileRef = A5002001 /* THIRD_PARTY_LICENSES.md */; };
|
||||
B9000012A1B2C3D4E5F60719 /* AutomationSocketUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */; };
|
||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */; };
|
||||
B8F266246A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */; };
|
||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */; };
|
||||
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */; };
|
||||
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */; };
|
||||
B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */; };
|
||||
B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */; };
|
||||
B9000025A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000026A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift */; };
|
||||
D0E0F0B0A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */; };
|
||||
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */; };
|
||||
E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; };
|
||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */; };
|
||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; };
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */; };
|
||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; };
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */; };
|
||||
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */; };
|
||||
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */; };
|
||||
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
|
||||
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */; };
|
||||
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; };
|
||||
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008380 /* BrowserFindJavaScriptTests.swift */; };
|
||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
|
||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
|
||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
|
||||
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
A5001020 /* Embed Frameworks */ = {
|
||||
|
|
@ -96,6 +119,7 @@
|
|||
files = (
|
||||
B900000BA1B2C3D4E5F60719 /* cmux in Copy CLI */,
|
||||
C1ADE00002A1B2C3D4E5F719 /* claude in Copy CLI */,
|
||||
D1BEF00002A1B2C3D4E5F719 /* open in Copy CLI */,
|
||||
);
|
||||
name = "Copy CLI";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -144,6 +168,8 @@
|
|||
A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = "<group>"; };
|
||||
A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = "<group>"; };
|
||||
A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = "<group>"; };
|
||||
A5001620 /* AppleScriptSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScriptSupport.swift; sourceTree = "<group>"; };
|
||||
A5001510 /* CmuxWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CmuxWebView.swift; sourceTree = "<group>"; };
|
||||
A5001511 /* UITestRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestRecorder.swift; sourceTree = "<group>"; };
|
||||
A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -154,14 +180,19 @@
|
|||
A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = "<group>"; };
|
||||
A5001414 /* BrowserPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanelView.swift; sourceTree = "<group>"; };
|
||||
A5001415 /* PanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/PanelContentView.swift; sourceTree = "<group>"; };
|
||||
A5001418 /* MarkdownPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/MarkdownPanel.swift; sourceTree = "<group>"; };
|
||||
A5001419 /* MarkdownPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/MarkdownPanelView.swift; sourceTree = "<group>"; };
|
||||
A5001416 /* Workspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Workspace.swift; sourceTree = "<group>"; };
|
||||
A5001417 /* WorkspaceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentView.swift; sourceTree = "<group>"; };
|
||||
A5001090 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A5001091 /* NotificationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPage.swift; sourceTree = "<group>"; };
|
||||
A5001092 /* TerminalNotificationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalNotificationStore.swift; sourceTree = "<group>"; };
|
||||
A5001301 /* SurfaceSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/SurfaceSearchOverlay.swift; sourceTree = "<group>"; };
|
||||
A5008370 /* BrowserSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/BrowserSearchOverlay.swift; sourceTree = "<group>"; };
|
||||
A5008372 /* BrowserFindJavaScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/BrowserFindJavaScript.swift; sourceTree = "<group>"; };
|
||||
A50012F0 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
A50012F2 /* KeyboardShortcutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSettings.swift; sourceTree = "<group>"; };
|
||||
A50012F4 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
|
||||
A5001211 /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateController.swift; sourceTree = "<group>"; };
|
||||
A5001212 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateDelegate.swift; sourceTree = "<group>"; };
|
||||
A5001213 /* UpdateDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateDriver.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -177,11 +208,15 @@
|
|||
A5001222 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = "<group>"; };
|
||||
A5001223 /* UpdateLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateLogStore.swift; sourceTree = "<group>"; };
|
||||
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
|
||||
A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
|
||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
|
||||
B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarHelpMenuUITests.swift; sourceTree = "<group>"; };
|
||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
|
||||
A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
IC000002 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
B2E7294509CC42FE9191870E /* xterm-ghostty */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ghostty/terminfo/78/xterm-ghostty"; sourceTree = "<group>"; };
|
||||
C1ADE00001A1B2C3D4E5F719 /* claude */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "Resources/bin/claude"; sourceTree = SOURCE_ROOT; };
|
||||
D1BEF00001A1B2C3D4E5F719 /* open */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "Resources/bin/open"; sourceTree = SOURCE_ROOT; };
|
||||
A5002001 /* THIRD_PARTY_LICENSES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = THIRD_PARTY_LICENSES.md; sourceTree = SOURCE_ROOT; };
|
||||
B9000001A1B2C3D4E5F60719 /* cmux.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmux.swift; sourceTree = "<group>"; };
|
||||
B9000004A1B2C3D4E5F60719 /* cmux */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = cmux; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
|
@ -190,14 +225,25 @@
|
|||
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiWindowNotificationsUITests.swift; sourceTree = "<group>"; };
|
||||
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceConfirmDialogUITests.swift; sourceTree = "<group>"; };
|
||||
B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceCmdDUITests.swift; sourceTree = "<group>"; };
|
||||
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPaneNavigationKeybindUITests.swift; sourceTree = "<group>"; };
|
||||
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = "<group>"; };
|
||||
E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = "<group>"; };
|
||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWebViewKeyEquivalentTests.swift; sourceTree = "<group>"; };
|
||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillReleaseVisibilityTests.swift; sourceTree = "<group>"; };
|
||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = "<group>"; };
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfigTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
B9000026A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWindowConfirmDialogUITests.swift; sourceTree = "<group>"; };
|
||||
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPaneNavigationKeybindUITests.swift; sourceTree = "<group>"; };
|
||||
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = "<group>"; };
|
||||
E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = "<group>"; };
|
||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWebViewKeyEquivalentTests.swift; sourceTree = "<group>"; };
|
||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillReleaseVisibilityTests.swift; sourceTree = "<group>"; };
|
||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = "<group>"; };
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfigTests.swift; sourceTree = "<group>"; };
|
||||
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistenceTests.swift; sourceTree = "<group>"; };
|
||||
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateShortcutRoutingTests.swift; sourceTree = "<group>"; };
|
||||
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
|
||||
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketControlPasswordStoreTests.swift; sourceTree = "<group>"; };
|
||||
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyEnsureFocusWindowActivationTests.swift; sourceTree = "<group>"; };
|
||||
A5008380 /* BrowserFindJavaScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFindJavaScriptTests.swift; sourceTree = "<group>"; };
|
||||
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
|
||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
A5001030 /* Frameworks */ = {
|
||||
|
|
@ -208,6 +254,7 @@
|
|||
A5001230 /* Sparkle in Frameworks */,
|
||||
A5001250 /* Sentry in Frameworks */,
|
||||
A5001270 /* PostHog in Frameworks */,
|
||||
A5001290 /* MarkdownUI in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -229,6 +276,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B9000024A1B2C3D4E5F60719 /* Sentry in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -242,6 +290,9 @@
|
|||
A5001100 /* Assets.xcassets in Resources */,
|
||||
84E00D47E4584162AE53BC8D /* xterm-ghostty in Resources */,
|
||||
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */,
|
||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */,
|
||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */,
|
||||
A5001623 /* cmux.sdef in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -289,6 +340,7 @@
|
|||
B9000003A1B2C3D4E5F60719 /* CLI */,
|
||||
087C454FFF74443AB06942C3 /* Resources */,
|
||||
A5001101 /* Assets.xcassets */,
|
||||
IC000002 /* AppIcon.icon */,
|
||||
A5001016 /* GhosttyKit.xcframework */,
|
||||
A5001017 /* ghostty.h */,
|
||||
A5001018 /* cmux-Bridging-Header.h */,
|
||||
|
|
@ -307,6 +359,7 @@
|
|||
B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */,
|
||||
A50012F0 /* Backport.swift */,
|
||||
A50012F2 /* KeyboardShortcutSettings.swift */,
|
||||
A50012F4 /* KeyboardLayout.swift */,
|
||||
A5001013 /* TabManager.swift */,
|
||||
A5001511 /* UITestRecorder.swift */,
|
||||
A5001520 /* PostHogAnalytics.swift */,
|
||||
|
|
@ -319,15 +372,21 @@
|
|||
A5001019 /* TerminalController.swift */,
|
||||
A5001541 /* PortScanner.swift */,
|
||||
A5001225 /* SocketControlSettings.swift */,
|
||||
A5001600 /* SentryHelper.swift */,
|
||||
A5001620 /* AppleScriptSupport.swift */,
|
||||
A5001090 /* AppDelegate.swift */,
|
||||
A5001091 /* NotificationsPage.swift */,
|
||||
A5001092 /* TerminalNotificationStore.swift */,
|
||||
A5001301 /* SurfaceSearchOverlay.swift */,
|
||||
A5008370 /* BrowserSearchOverlay.swift */,
|
||||
A5008372 /* BrowserFindJavaScript.swift */,
|
||||
A5001410 /* Panel.swift */,
|
||||
A5001411 /* TerminalPanel.swift */,
|
||||
A5001412 /* BrowserPanel.swift */,
|
||||
A5001413 /* TerminalPanelView.swift */,
|
||||
A5001414 /* BrowserPanelView.swift */,
|
||||
A5001418 /* MarkdownPanel.swift */,
|
||||
A5001419 /* MarkdownPanelView.swift */,
|
||||
A5001510 /* CmuxWebView.swift */,
|
||||
A5001415 /* PanelContentView.swift */,
|
||||
A5001211 /* UpdateController.swift */,
|
||||
|
|
@ -345,6 +404,7 @@
|
|||
A5001219 /* WindowToolbarController.swift */,
|
||||
A5001241 /* WindowDecorationsController.swift */,
|
||||
A5001222 /* WindowAccessor.swift */,
|
||||
A5001611 /* SessionPersistence.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -363,6 +423,9 @@
|
|||
B2E7294509CC42FE9191870E /* xterm-ghostty */,
|
||||
A5002001 /* THIRD_PARTY_LICENSES.md */,
|
||||
C1ADE00001A1B2C3D4E5F719 /* claude */,
|
||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */,
|
||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */,
|
||||
A5001622 /* cmux.sdef */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -385,8 +448,10 @@
|
|||
B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */,
|
||||
B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */,
|
||||
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */,
|
||||
B9000026A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift */,
|
||||
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */,
|
||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */,
|
||||
B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */,
|
||||
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */,
|
||||
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */,
|
||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */,
|
||||
|
|
@ -395,17 +460,24 @@
|
|||
path = cmuxUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
|
||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
|
||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
|
||||
);
|
||||
path = cmuxTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F1000003A1B2C3D4E5F60718 /* cmuxTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */,
|
||||
F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */,
|
||||
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */,
|
||||
F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */,
|
||||
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */,
|
||||
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */,
|
||||
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */,
|
||||
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */,
|
||||
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */,
|
||||
A5008380 /* BrowserFindJavaScriptTests.swift */,
|
||||
A5008382 /* CommandPaletteSearchEngineTests.swift */,
|
||||
);
|
||||
path = cmuxTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -430,6 +502,7 @@
|
|||
A5001251 /* Sentry */,
|
||||
A5001271 /* PostHog */,
|
||||
A5001261 /* Bonsplit */,
|
||||
A5001291 /* MarkdownUI */,
|
||||
);
|
||||
name = GhosttyTabs;
|
||||
productName = GhosttyTabs;
|
||||
|
|
@ -447,6 +520,9 @@
|
|||
);
|
||||
dependencies = (
|
||||
);
|
||||
packageProductDependencies = (
|
||||
A5001251 /* Sentry */,
|
||||
);
|
||||
name = "cmux-cli";
|
||||
productName = cmux;
|
||||
productReference = B9000004A1B2C3D4E5F60719 /* cmux */;
|
||||
|
|
@ -505,12 +581,30 @@
|
|||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
ja,
|
||||
ar,
|
||||
bs,
|
||||
da,
|
||||
de,
|
||||
es,
|
||||
fr,
|
||||
it,
|
||||
ko,
|
||||
nb,
|
||||
pl,
|
||||
pt-BR,
|
||||
ru,
|
||||
th,
|
||||
tr,
|
||||
zh-Hans,
|
||||
zh-Hant,
|
||||
);
|
||||
mainGroup = A5001040;
|
||||
packageReferences = (
|
||||
A5001232 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
A5001252 /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||
A5001272 /* XCRemoteSwiftPackageReference "posthog-ios" */,
|
||||
A5001292 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
|
||||
A5001260 /* XCLocalSwiftPackageReference "bonsplit" */,
|
||||
);
|
||||
productRefGroup = A5001042 /* Products */;
|
||||
|
|
@ -536,6 +630,7 @@
|
|||
B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */,
|
||||
A50012F1 /* Backport.swift in Sources */,
|
||||
A50012F3 /* KeyboardShortcutSettings.swift in Sources */,
|
||||
A50012F5 /* KeyboardLayout.swift in Sources */,
|
||||
A5001003 /* TabManager.swift in Sources */,
|
||||
A5001501 /* UITestRecorder.swift in Sources */,
|
||||
A5001521 /* PostHogAnalytics.swift in Sources */,
|
||||
|
|
@ -548,15 +643,21 @@
|
|||
A5001007 /* TerminalController.swift in Sources */,
|
||||
A5001540 /* PortScanner.swift in Sources */,
|
||||
A5001226 /* SocketControlSettings.swift in Sources */,
|
||||
A5001601 /* SentryHelper.swift in Sources */,
|
||||
A5001621 /* AppleScriptSupport.swift in Sources */,
|
||||
A5001093 /* AppDelegate.swift in Sources */,
|
||||
A5001094 /* NotificationsPage.swift in Sources */,
|
||||
A5001095 /* TerminalNotificationStore.swift in Sources */,
|
||||
A5001303 /* SurfaceSearchOverlay.swift in Sources */,
|
||||
A5008371 /* BrowserSearchOverlay.swift in Sources */,
|
||||
A5008373 /* BrowserFindJavaScript.swift in Sources */,
|
||||
A5001400 /* Panel.swift in Sources */,
|
||||
A5001401 /* TerminalPanel.swift in Sources */,
|
||||
A5001402 /* BrowserPanel.swift in Sources */,
|
||||
A5001403 /* TerminalPanelView.swift in Sources */,
|
||||
A5001404 /* BrowserPanelView.swift in Sources */,
|
||||
A5001420 /* MarkdownPanel.swift in Sources */,
|
||||
A5001421 /* MarkdownPanelView.swift in Sources */,
|
||||
A5001500 /* CmuxWebView.swift in Sources */,
|
||||
A5001405 /* PanelContentView.swift in Sources */,
|
||||
A5001201 /* UpdateController.swift in Sources */,
|
||||
|
|
@ -574,6 +675,7 @@
|
|||
A5001209 /* WindowToolbarController.swift in Sources */,
|
||||
A5001240 /* WindowDecorationsController.swift in Sources */,
|
||||
A500120C /* WindowAccessor.swift in Sources */,
|
||||
A5001610 /* SessionPersistence.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -585,8 +687,10 @@
|
|||
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */,
|
||||
B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */,
|
||||
B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */,
|
||||
B9000025A1B2C3D4E5F60719 /* CloseWindowConfirmDialogUITests.swift in Sources */,
|
||||
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */,
|
||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */,
|
||||
B8F266246A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift in Sources */,
|
||||
D0E0F0B0A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift in Sources */,
|
||||
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */,
|
||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */,
|
||||
|
|
@ -594,18 +698,25 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F1000005A1B2C3D4E5F60718 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
|
||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B9000006A1B2C3D4E5F60719 /* Sources */ = {
|
||||
F1000005A1B2C3D4E5F60718 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */,
|
||||
F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */,
|
||||
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
|
||||
F4000000A1B2C3D4E5F60718 /* GhosttyConfigTests.swift in Sources */,
|
||||
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */,
|
||||
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */,
|
||||
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */,
|
||||
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */,
|
||||
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */,
|
||||
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */,
|
||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B9000006A1B2C3D4E5F60719 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
|
|
@ -689,6 +800,7 @@
|
|||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
|
|
@ -702,7 +814,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
CURRENT_PROJECT_VERSION = 76;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
|
|
@ -711,7 +823,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.60.0;
|
||||
MARKETING_VERSION = 0.62.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -741,7 +853,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
CURRENT_PROJECT_VERSION = 76;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
|
|
@ -750,7 +862,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.60.0;
|
||||
MARKETING_VERSION = 0.62.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-lc++",
|
||||
"-framework",
|
||||
|
|
@ -764,7 +876,7 @@
|
|||
"-framework",
|
||||
Carbon,
|
||||
);
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app;
|
||||
PRODUCT_NAME = cmux;
|
||||
SPARKLE_PUBLIC_KEY = "avjcgKibf1FTvhIjLBxhd+0HSpsXU4D0IGlVk8cgqRc=";
|
||||
|
|
@ -778,6 +890,12 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_NAME = cmux;
|
||||
PRODUCT_MODULE_NAME = cmux_cli;
|
||||
|
|
@ -791,9 +909,16 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
PRODUCT_NAME = cmux;
|
||||
PRODUCT_MODULE_NAME = cmux_cli;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -804,10 +929,10 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
CURRENT_PROJECT_VERSION = 76;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.60.0;
|
||||
MARKETING_VERSION = 0.62.1;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -821,11 +946,11 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
CURRENT_PROJECT_VERSION = 76;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.60.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
MARKETING_VERSION = 0.62.1;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -838,10 +963,10 @@
|
|||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
CURRENT_PROJECT_VERSION = 76;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.60.0;
|
||||
MARKETING_VERSION = 0.62.1;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -857,11 +982,11 @@
|
|||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
CURRENT_PROJECT_VERSION = 76;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.60.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
MARKETING_VERSION = 0.62.1;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.apptests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -897,6 +1022,14 @@
|
|||
minimumVersion = 3.41.0;
|
||||
};
|
||||
};
|
||||
A5001292 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.4.1;
|
||||
};
|
||||
};
|
||||
A5001260 /* XCLocalSwiftPackageReference "bonsplit" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = vendor/bonsplit;
|
||||
|
|
@ -924,6 +1057,11 @@
|
|||
package = A5001260 /* XCLocalSwiftPackageReference "bonsplit" */;
|
||||
productName = Bonsplit;
|
||||
};
|
||||
A5001291 /* MarkdownUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = A5001292 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
|
||||
productName = MarkdownUI;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"originHash" : "a1df212ee81645b29368e6cc39c83aebbbafb5c592f726afc990bab228304987",
|
||||
"originHash" : "b66d812c506be67c70b46c63421ab2eb2db013613c74252ad1205f662ada079b",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "networkimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/NetworkImage",
|
||||
"state" : {
|
||||
"revision" : "2849f5323265386e200484b0d0f896e73c3411b9",
|
||||
"version" : "6.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "posthog-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
@ -27,6 +36,24 @@
|
|||
"revision" : "5581748cef2bae787496fe6d61139aebe0a451f6",
|
||||
"version" : "2.8.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cmark",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-cmark",
|
||||
"state" : {
|
||||
"revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe",
|
||||
"version" : "0.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-ui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/gonzalezreal/swift-markdown-ui",
|
||||
"state" : {
|
||||
"revision" : "5f613358148239d0292c0cef674a3c2314737f9e",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme LastUpgradeVersion="1500" version="1.7">
|
||||
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F1000004A1B2C3D4E5F60718" BuildableName="cmuxTests.xctest" BlueprintName="cmuxTests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
||||
<Testables>
|
||||
<TestableReference skipped="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F1000004A1B2C3D4E5F60718" BuildableName="cmuxTests.xctest" BlueprintName="cmuxTests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</TestableReference>
|
||||
<TestableReference skipped="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="cmuxUITests.xctest" BlueprintName="cmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="YES" debugDocumentVersioning="YES" allowLocationSimulation="YES">
|
||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction buildConfiguration="Debug" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
|
||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction buildConfiguration="Debug"/>
|
||||
<ArchiveAction buildConfiguration="Debug" revealArchiveInOrganizer="YES"/>
|
||||
</Scheme>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction buildConfiguration="Release" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
||||
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
||||
<Testables>
|
||||
<TestableReference skipped="NO">
|
||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="CB450DF0F0B3839599082C4D" BuildableName="cmuxUITests.xctest" BlueprintName="cmuxUITests" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
|
||||
|
|
|
|||
148
README.ar.md
|
|
@ -1,9 +1,5 @@
|
|||
> تمت هذه الترجمة بواسطة Claude. إذا كانت لديك اقتراحات للتحسين، يرجى فتح PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | العربية | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">تطبيق طرفية لنظام macOS مبني على Ghostty مع علامات تبويب عمودية وإشعارات لوكلاء البرمجة بالذكاء الاصطناعي</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="لقطة شاشة cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | العربية | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="لقطة شاشة cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ فيديو توضيحي</a> · <a href="https://cmux.dev/blog/zen-of-cmux">فلسفة cmux</a>
|
||||
</p>
|
||||
|
||||
## الميزات
|
||||
|
||||
- **علامات تبويب عمودية** — يعرض الشريط الجانبي فرع git ومجلد العمل والمنافذ المستمعة وآخر نص إشعار
|
||||
- **حلقات الإشعارات** — تحصل الأجزاء على حلقة زرقاء وتضيء علامات التبويب عندما يحتاج وكلاء الذكاء الاصطناعي (Claude Code، OpenCode) انتباهك
|
||||
- **لوحة الإشعارات** — عرض جميع الإشعارات المعلقة في مكان واحد، والانتقال إلى أحدث إشعار غير مقروء
|
||||
- **أجزاء مقسمة** — تقسيم أفقي وعمودي
|
||||
- **متصفح مدمج** — قسّم متصفحاً بجانب الطرفية مع API قابل للبرمجة مأخوذ من [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>حلقات الإشعارات</h3>
|
||||
تحصل الأجزاء على حلقة زرقاء وتضيء علامات التبويب عندما يحتاج وكلاء البرمجة انتباهك
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="حلقات الإشعارات" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>لوحة الإشعارات</h3>
|
||||
عرض جميع الإشعارات المعلقة في مكان واحد، والانتقال إلى أحدث إشعار غير مقروء
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="شارة إشعارات الشريط الجانبي" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>متصفح مدمج</h3>
|
||||
قسّم متصفحًا بجانب الطرفية مع API قابل للبرمجة مأخوذ من <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="المتصفح المدمج" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>علامات تبويب عمودية + أفقية</h3>
|
||||
يعرض الشريط الجانبي فرع git وحالة/رقم طلب السحب المرتبط ومجلد العمل والمنافذ المستمعة وآخر نص إشعار. تقسيم أفقي وعمودي.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="علامات تبويب عمودية وأجزاء مقسمة" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **قابل للبرمجة** — CLI وsocket API لإنشاء مساحات العمل وتقسيم الأجزاء وإرسال ضغطات المفاتيح وأتمتة المتصفح
|
||||
- **تطبيق macOS أصلي** — مبني بـ Swift وAppKit، وليس Electron. بدء تشغيل سريع واستهلاك ذاكرة منخفض.
|
||||
- **متوافق مع Ghostty** — يقرأ إعداداتك الحالية من `~/.config/ghostty/config` للسمات والخطوط والألوان
|
||||
|
|
@ -37,7 +80,7 @@
|
|||
<img src="./docs/assets/macos-badge.png" alt="تحميل cmux لنظام macOS" width="180" />
|
||||
</a>
|
||||
|
||||
افتح ملف `.dmg` واسحب cmux إلى مجلد التطبيقات. يتم تحديث cmux تلقائياً عبر Sparkle، لذا تحتاج للتحميل مرة واحدة فقط.
|
||||
افتح ملف `.dmg` واسحب cmux إلى مجلد التطبيقات. يتم تحديث cmux تلقائيًا عبر Sparkle، لذا تحتاج للتحميل مرة واحدة فقط.
|
||||
|
||||
### Homebrew
|
||||
|
||||
|
|
@ -46,7 +89,7 @@ brew tap manaflow-ai/cmux
|
|||
brew install --cask cmux
|
||||
```
|
||||
|
||||
للتحديث لاحقاً:
|
||||
للتحديث لاحقًا:
|
||||
|
||||
```bash
|
||||
brew upgrade --cask cmux
|
||||
|
|
@ -56,16 +99,30 @@ brew upgrade --cask cmux
|
|||
|
||||
## لماذا cmux؟
|
||||
|
||||
أقوم بتشغيل الكثير من جلسات Claude Code وCodex بالتوازي. كنت أستخدم Ghostty مع مجموعة من الأجزاء المقسمة، وأعتمد على إشعارات macOS الأصلية لمعرفة متى يحتاجني وكيل ما. لكن نص إشعار Claude Code يكون دائماً مجرد "Claude is waiting for your input" بدون أي سياق، ومع فتح عدد كافٍ من علامات التبويب لم أعد قادراً حتى على قراءة العناوين.
|
||||
أقوم بتشغيل الكثير من جلسات Claude Code وCodex بالتوازي. كنت أستخدم Ghostty مع مجموعة من الأجزاء المقسمة، وأعتمد على إشعارات macOS الأصلية لمعرفة متى يحتاجني وكيل ما. لكن نص إشعار Claude Code يكون دائمًا مجرد "Claude is waiting for your input" بدون أي سياق، ومع فتح عدد كافٍ من علامات التبويب لم أعد قادرًا حتى على قراءة العناوين.
|
||||
|
||||
جربت بعض منظمات البرمجة لكن معظمها كانت تطبيقات Electron/Tauri وأداؤها كان يزعجني. كما أنني أفضل الطرفية لأن منظمات GUI تحبسك في سير عملها. لذا بنيت cmux كتطبيق macOS أصلي بـ Swift/AppKit. يستخدم libghostty لعرض الطرفية ويقرأ إعدادات Ghostty الحالية للسمات والخطوط والألوان.
|
||||
|
||||
الإضافات الرئيسية هي الشريط الجانبي ونظام الإشعارات. يحتوي الشريط الجانبي على علامات تبويب عمودية تعرض فرع git ومجلد العمل والمنافذ المستمعة وآخر نص إشعار لكل مساحة عمل. يلتقط نظام الإشعارات تسلسلات الطرفية (OSC 9/99/777) ولديه CLI (`cmux notify`) يمكنك ربطه بخطافات الوكلاء لـ Claude Code وOpenCode وغيرها. عندما ينتظر وكيل ما، يحصل جزؤه على حلقة زرقاء وتضيء علامة التبويب في الشريط الجانبي، حتى أتمكن من معرفة أيها يحتاجني عبر الأقسام وعلامات التبويب. Cmd+Shift+U ينتقل إلى أحدث إشعار غير مقروء.
|
||||
الإضافات الرئيسية هي الشريط الجانبي ونظام الإشعارات. يحتوي الشريط الجانبي على علامات تبويب عمودية تعرض فرع git وحالة/رقم طلب السحب المرتبط ومجلد العمل والمنافذ المستمعة وآخر نص إشعار لكل مساحة عمل. يلتقط نظام الإشعارات تسلسلات الطرفية (OSC 9/99/777) ولديه CLI (`cmux notify`) يمكنك ربطه بخطافات الوكلاء لـ Claude Code وOpenCode وغيرها. عندما ينتظر وكيل ما، يحصل جزؤه على حلقة زرقاء وتضيء علامة التبويب في الشريط الجانبي، حتى أتمكن من معرفة أيها يحتاجني عبر الأقسام وعلامات التبويب. Cmd+Shift+U ينتقل إلى أحدث إشعار غير مقروء.
|
||||
|
||||
المتصفح المدمج لديه API قابل للبرمجة مأخوذ من [agent-browser](https://github.com/vercel-labs/agent-browser). يمكن للوكلاء التقاط شجرة إمكانية الوصول والحصول على مراجع العناصر والنقر وملء النماذج وتنفيذ JS. يمكنك تقسيم جزء متصفح بجانب الطرفية وجعل Claude Code يتفاعل مع خادم التطوير مباشرة.
|
||||
|
||||
كل شيء قابل للبرمجة عبر CLI وsocket API — إنشاء مساحات العمل/علامات التبويب، تقسيم الأجزاء، إرسال ضغطات المفاتيح، فتح عناوين URL في المتصفح.
|
||||
|
||||
## فلسفة cmux
|
||||
|
||||
cmux لا يفرض على المطورين طريقة استخدام أدواتهم. إنه طرفية ومتصفح مع واجهة سطر أوامر، والباقي متروك لك.
|
||||
|
||||
cmux هو لبنة أساسية وليس حلًا جاهزًا. يمنحك طرفية ومتصفحًا وإشعارات ومساحات عمل وأقسامًا وعلامات تبويب وواجهة سطر أوامر للتحكم في كل ذلك. cmux لا يجبرك على طريقة محددة لاستخدام وكلاء البرمجة. ما تبنيه باستخدام هذه اللبنات الأساسية هو ملكك.
|
||||
|
||||
أفضل المطورين دائمًا ما بنوا أدواتهم الخاصة. لم يكتشف أحد بعد أفضل طريقة للعمل مع الوكلاء، والفرق التي تبني منتجات مغلقة لم تكتشفها أيضًا بالتأكيد. المطورون الأقرب لقواعد بياناتهم الخاصة سيكتشفونها أولًا.
|
||||
|
||||
أعطِ مليون مطور لبنات أساسية قابلة للتركيب وسيجدون بشكل جماعي أكثر سير العمل كفاءة أسرع مما يمكن لأي فريق منتج تصميمه من الأعلى إلى الأسفل.
|
||||
|
||||
## التوثيق
|
||||
|
||||
لمزيد من المعلومات حول كيفية إعداد cmux، [توجه إلى وثائقنا](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## اختصارات لوحة المفاتيح
|
||||
|
||||
### مساحات العمل
|
||||
|
|
@ -78,6 +135,7 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | مساحة العمل التالية |
|
||||
| ⌃ ⌘ [ | مساحة العمل السابقة |
|
||||
| ⌘ ⇧ W | إغلاق مساحة العمل |
|
||||
| ⌘ ⇧ R | إعادة تسمية مساحة العمل |
|
||||
| ⌘ B | تبديل الشريط الجانبي |
|
||||
|
||||
### الأسطح
|
||||
|
|
@ -104,6 +162,8 @@ brew upgrade --cask cmux
|
|||
|
||||
### المتصفح
|
||||
|
||||
اختصارات أدوات المطور في المتصفح تتبع إعدادات Safari الافتراضية ويمكن تخصيصها في `الإعدادات ← اختصارات لوحة المفاتيح`.
|
||||
|
||||
| الاختصار | الإجراء |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | فتح المتصفح في قسم |
|
||||
|
|
@ -111,7 +171,8 @@ brew upgrade --cask cmux
|
|||
| ⌘ [ | للخلف |
|
||||
| ⌘ ] | للأمام |
|
||||
| ⌘ R | إعادة تحميل الصفحة |
|
||||
| ⌥ ⌘ I | فتح أدوات المطور |
|
||||
| ⌥ ⌘ I | تبديل أدوات المطور (إعداد Safari الافتراضي) |
|
||||
| ⌥ ⌘ C | عرض وحدة تحكم JavaScript (إعداد Safari الافتراضي) |
|
||||
|
||||
### الإشعارات
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | إعادة تحميل الإعدادات |
|
||||
| ⌘ Q | إنهاء |
|
||||
|
||||
## الإصدارات الليلية
|
||||
|
||||
[تحميل cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY هو تطبيق منفصل بمعرّف حزمة خاص به، لذا يعمل بجانب الإصدار المستقر. يُبنى تلقائيًا من أحدث commit على فرع `main` ويتم تحديثه تلقائيًا عبر Sparkle الخاص به.
|
||||
|
||||
## استعادة الجلسة (السلوك الحالي)
|
||||
|
||||
عند إعادة التشغيل، يستعيد cmux حاليًا تخطيط التطبيق والبيانات الوصفية فقط:
|
||||
- تخطيط النوافذ/مساحات العمل/الأجزاء
|
||||
- مجلدات العمل
|
||||
- سجل تمرير الطرفية (أفضل جهد)
|
||||
- عنوان URL للمتصفح وسجل التنقل
|
||||
|
||||
cmux **لا** يستعيد حالة العمليات الحية داخل تطبيقات الطرفية. على سبيل المثال، جلسات Claude Code/tmux/vim النشطة لا يتم استئنافها بعد إعادة التشغيل بعد.
|
||||
|
||||
## تاريخ النجوم
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## المساهمة
|
||||
|
||||
طرق للمشاركة:
|
||||
|
||||
- تابعنا على X للتحديثات [@manaflowai](https://x.com/manaflowai)، [@lawrencecchen](https://x.com/lawrencecchen)، و[@austinywang](https://x.com/austinywang)
|
||||
- انضم إلى المحادثة على [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- أنشئ وشارك في [قضايا GitHub](https://github.com/manaflow-ai/cmux/issues) و[المناقشات](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- أخبرنا بما تبنيه باستخدام cmux
|
||||
|
||||
## المجتمع
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## إصدار المؤسسين
|
||||
|
||||
cmux مجاني ومفتوح المصدر وسيظل كذلك دائمًا. إذا كنت ترغب في دعم التطوير والحصول على وصول مبكر لما هو قادم:
|
||||
|
||||
**[احصل على إصدار المؤسسين](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **أولوية لطلبات الميزات/إصلاح الأخطاء**
|
||||
- **وصول مبكر: ذكاء اصطناعي لـ cmux يمنحك سياقًا عن كل مساحة عمل وعلامة تبويب ولوحة**
|
||||
- **وصول مبكر: تطبيق iOS مع مزامنة الطرفيات بين سطح المكتب والهاتف**
|
||||
- **وصول مبكر: أجهزة افتراضية سحابية**
|
||||
- **وصول مبكر: وضع الصوت**
|
||||
- **iMessage/WhatsApp الشخصي الخاص بي**
|
||||
|
||||
## الرخصة
|
||||
|
||||
هذا المشروع مرخص بموجب رخصة GNU Affero العامة الإصدار 3.0 أو أحدث (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
142
README.bs.md
|
|
@ -1,9 +1,5 @@
|
|||
> Ovaj prijevod je generisan od strane Claude. Ako imate prijedloge za poboljšanje, otvorite PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | Bosanski | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">macOS terminal baziran na Ghostty sa vertikalnim tabovima i obavještenjima za AI agente za programiranje</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux snimak ekrana" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | Bosanski | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux snimak ekrana" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demo video</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Funkcije
|
||||
|
||||
- **Vertikalni tabovi** — Bočna traka prikazuje git granu, radni direktorij, portove koji slušaju i tekst posljednjeg obavještenja
|
||||
- **Prstenovi obavještenja** — Paneli dobijaju plavi prsten, a tabovi se osvjetljavaju kada AI agenti (Claude Code, OpenCode) trebaju vašu pažnju
|
||||
- **Panel obavještenja** — Pregledajte sva obavještenja na čekanju na jednom mjestu, skočite na najnovije nepročitano
|
||||
- **Podijeljeni paneli** — Horizontalna i vertikalna podjela
|
||||
- **Ugrađeni preglednik** — Podijelite preglednik pored terminala sa skriptabilnim API portiranim iz [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Prstenovi obavještenja</h3>
|
||||
Paneli dobijaju plavi prsten, a tabovi se osvjetljavaju kada agenti za programiranje trebaju vašu pažnju
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Prstenovi obavještenja" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Panel obavještenja</h3>
|
||||
Pregledajte sva obavještenja na čekanju na jednom mjestu, skočite na najnovije nepročitano
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Značka obavještenja u bočnoj traci" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Ugrađeni preglednik</h3>
|
||||
Podijelite preglednik pored terminala sa skriptabilnim API portiranim iz <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Ugrađeni preglednik" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Vertikalni + horizontalni tabovi</h3>
|
||||
Bočna traka prikazuje git granu, status/broj povezanog PR-a, radni direktorij, portove koji slušaju i tekst posljednjeg obavještenja. Horizontalna i vertikalna podjela.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Vertikalni tabovi i podijeljeni paneli" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Skriptabilan** — CLI i socket API za kreiranje radnih prostora, dijeljenje panela, slanje pritisaka tipki i automatizaciju preglednika
|
||||
- **Nativna macOS aplikacija** — Izgrađena sa Swift i AppKit, ne Electron. Brzo pokretanje, niska potrošnja memorije.
|
||||
- **Kompatibilan sa Ghostty** — Čita vašu postojeću konfiguraciju `~/.config/ghostty/config` za teme, fontove i boje
|
||||
|
|
@ -60,12 +103,26 @@ Pokrećem mnogo Claude Code i Codex sesija paralelno. Koristio sam Ghostty sa go
|
|||
|
||||
Isprobao sam nekoliko orkestratora za kodiranje, ali većina ih je bila Electron/Tauri aplikacije i performanse su me nervirale. Također jednostavno preferiram terminal jer GUI orkestratori vas zaključavaju u svoj radni tok. Zato sam izgradio cmux kao nativnu macOS aplikaciju u Swift/AppKit. Koristi libghostty za renderiranje terminala i čita vašu postojeću Ghostty konfiguraciju za teme, fontove i boje.
|
||||
|
||||
Glavni dodaci su bočna traka i sistem obavještenja. Bočna traka ima vertikalne tabove koji prikazuju git granu, radni direktorij, portove koji slušaju i tekst posljednjeg obavještenja za svaki radni prostor. Sistem obavještenja hvata terminalne sekvence (OSC 9/99/777) i ima CLI (`cmux notify`) koji možete povezati sa hookovima agenata za Claude Code, OpenCode itd. Kada agent čeka, njegov panel dobija plavi prsten, a tab se osvjetljava u bočnoj traci, tako da mogu vidjeti koji me treba kroz podjele i tabove. Cmd+Shift+U skače na najnovije nepročitano.
|
||||
Glavni dodaci su bočna traka i sistem obavještenja. Bočna traka ima vertikalne tabove koji prikazuju git granu, status/broj povezanog PR-a, radni direktorij, portove koji slušaju i tekst posljednjeg obavještenja za svaki radni prostor. Sistem obavještenja hvata terminalne sekvence (OSC 9/99/777) i ima CLI (`cmux notify`) koji možete povezati sa hookovima agenata za Claude Code, OpenCode itd. Kada agent čeka, njegov panel dobija plavi prsten, a tab se osvjetljava u bočnoj traci, tako da mogu vidjeti koji me treba kroz podjele i tabove. Cmd+Shift+U skače na najnovije nepročitano.
|
||||
|
||||
Ugrađeni preglednik ima skriptabilni API portiran iz [agent-browser](https://github.com/vercel-labs/agent-browser). Agenti mogu snimiti stablo pristupačnosti, dobiti reference elemenata, kliknuti, popuniti formulare i evaluirati JS. Možete podijeliti panel preglednika pored terminala i omogućiti Claude Code da direktno komunicira sa vašim razvojnim serverom.
|
||||
|
||||
Sve je skriptabilno kroz CLI i socket API — kreiranje radnih prostora/tabova, dijeljenje panela, slanje pritisaka tipki, otvaranje URL-ova u pregledniku.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux ne propisuje programerima kako da koriste svoje alate. To je terminal i preglednik sa CLI-jem, a ostatak je na vama.
|
||||
|
||||
cmux je primitiv, ne rješenje. Daje vam terminal, preglednik, obavještenja, radne prostore, podjele, tabove i CLI za kontrolu svega toga. cmux vas ne prisiljava na određeni način korištenja agenata za kodiranje. Ono što izgradite sa tim primitivima je vaše.
|
||||
|
||||
Najbolji programeri su oduvijek gradili vlastite alate. Niko još nije otkrio najbolji način rada sa agentima, a timovi koji grade zatvorene proizvode to također nisu uradili. Programeri koji su najbliži svojim bazama koda će to otkriti prvi.
|
||||
|
||||
Dajte milion programera kompozabilne primitive i oni će kolektivno pronaći najefikasnije tokove rada brže nego što bi bilo koji produktni tim mogao dizajnirati odozgo prema dolje.
|
||||
|
||||
## Dokumentacija
|
||||
|
||||
Za više informacija o konfiguraciji cmux, posjetite [našu dokumentaciju](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Prečice na Tastaturi
|
||||
|
||||
### Radni prostori
|
||||
|
|
@ -78,6 +135,7 @@ Sve je skriptabilno kroz CLI i socket API — kreiranje radnih prostora/tabova,
|
|||
| ⌃ ⌘ ] | Sljedeći radni prostor |
|
||||
| ⌃ ⌘ [ | Prethodni radni prostor |
|
||||
| ⌘ ⇧ W | Zatvori radni prostor |
|
||||
| ⌘ ⇧ R | Preimenuj radni prostor |
|
||||
| ⌘ B | Prikaži/sakrij bočnu traku |
|
||||
|
||||
### Površine
|
||||
|
|
@ -104,6 +162,8 @@ Sve je skriptabilno kroz CLI i socket API — kreiranje radnih prostora/tabova,
|
|||
|
||||
### Preglednik
|
||||
|
||||
Prečice razvojnih alata preglednika prate Safari zadane postavke i mogu se prilagoditi u `Postavke → Prečice na tastaturi`.
|
||||
|
||||
| Prečica | Akcija |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Otvori preglednik u podjeli |
|
||||
|
|
@ -111,7 +171,8 @@ Sve je skriptabilno kroz CLI i socket API — kreiranje radnih prostora/tabova,
|
|||
| ⌘ [ | Nazad |
|
||||
| ⌘ ] | Naprijed |
|
||||
| ⌘ R | Ponovo učitaj stranicu |
|
||||
| ⌥ ⌘ I | Otvori Alate za Programere |
|
||||
| ⌥ ⌘ I | Prikaži/sakrij Alate za Programere (Safari zadano) |
|
||||
| ⌥ ⌘ C | Prikaži JavaScript Konzolu (Safari zadano) |
|
||||
|
||||
### Obavještenja
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ Sve je skriptabilno kroz CLI i socket API — kreiranje radnih prostora/tabova,
|
|||
| ⌘ ⇧ , | Ponovo učitaj konfiguraciju |
|
||||
| ⌘ Q | Zatvori |
|
||||
|
||||
## Noćne verzije
|
||||
|
||||
[Preuzmi cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY je zasebna aplikacija sa vlastitim bundle ID-om, tako da radi uporedo sa stabilnom verzijom. Automatski se gradi iz najnovijeg `main` commita i ažurira se putem vlastitog Sparkle feeda.
|
||||
|
||||
## Vraćanje sesije (trenutno ponašanje)
|
||||
|
||||
Prilikom ponovnog pokretanja, cmux trenutno vraća samo raspored aplikacije i metapodatke:
|
||||
- Raspored prozora/radnih prostora/panela
|
||||
- Radne direktorije
|
||||
- Scrollback terminala (po mogućnosti)
|
||||
- URL preglednika i historija navigacije
|
||||
|
||||
cmux **ne** vraća stanje živih procesa unutar terminalnih aplikacija. Na primjer, aktivne sesije Claude Code/tmux/vim se još ne nastavljaju nakon restarta.
|
||||
|
||||
## Historija zvjezdica
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Doprinos
|
||||
|
||||
Načini da se uključite:
|
||||
|
||||
- Pratite nas na X za ažuriranja [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) i [@austinywang](https://x.com/austinywang)
|
||||
- Pridružite se razgovoru na [Discordu](https://discord.gg/xsgFEVrWCZ)
|
||||
- Kreirajte i učestvujte u [GitHub issues](https://github.com/manaflow-ai/cmux/issues) i [diskusijama](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Javite nam šta gradite sa cmux
|
||||
|
||||
## Zajednica
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Osnivačko izdanje
|
||||
|
||||
cmux je besplatan, otvorenog koda i uvijek će biti. Ako želite podržati razvoj i dobiti rani pristup onome što dolazi:
|
||||
|
||||
**[Nabavite Osnivačko izdanje](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Prioritetni zahtjevi za funkcije/ispravke grešaka**
|
||||
- **Rani pristup: cmux AI koji vam daje kontekst o svakom radnom prostoru, tabu i panelu**
|
||||
- **Rani pristup: iOS aplikacija sa terminalima sinhroniziranim između desktopa i telefona**
|
||||
- **Rani pristup: Cloud VM-ovi**
|
||||
- **Rani pristup: Glasovni režim**
|
||||
- **Moj lični iMessage/WhatsApp**
|
||||
|
||||
## Licenca
|
||||
|
||||
Ovaj projekat je licenciran pod GNU Affero General Public License v3.0 ili novijom (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
142
README.da.md
|
|
@ -1,9 +1,5 @@
|
|||
> Denne oversættelse er genereret af Claude. Har du forslag til forbedringer, er du velkommen til at oprette en PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | Dansk | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">En Ghostty-baseret macOS-terminal med lodrette faner og notifikationer til AI-kodningsagenter</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux skærmbillede" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | Dansk | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux skærmbillede" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demovideo</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Funktioner
|
||||
|
||||
- **Lodrette faner** — Sidebjælken viser git-branch, arbejdsmappe, lyttende porte og seneste notifikationstekst
|
||||
- **Notifikationsringe** — Paneler får en blå ring, og faner lyser op, når AI-agenter (Claude Code, OpenCode) har brug for din opmærksomhed
|
||||
- **Notifikationspanel** — Se alle ventende notifikationer ét sted, hop til den seneste ulæste
|
||||
- **Delte paneler** — Vandrette og lodrette opdelinger
|
||||
- **Indbygget browser** — Del en browser ved siden af din terminal med en scriptbar API porteret fra [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Notifikationsringe</h3>
|
||||
Paneler får en blå ring, og faner lyser op, når kodningsagenter har brug for din opmærksomhed
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Notifikationsringe" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Notifikationspanel</h3>
|
||||
Se alle ventende notifikationer ét sted, hop til den seneste ulæste
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Notifikationsbadge i sidebjælken" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Indbygget browser</h3>
|
||||
Del en browser ved siden af din terminal med en scriptbar API porteret fra <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Indbygget browser" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Lodrette + vandrette faner</h3>
|
||||
Sidebjælken viser git-branch, tilknyttet PR-status/nummer, arbejdsmappe, lyttende porte og seneste notifikationstekst. Del vandret og lodret.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Lodrette faner og delte paneler" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Scriptbar** — CLI og socket API til at oprette workspaces, dele paneler, sende tastetryk og automatisere browseren
|
||||
- **Nativ macOS-app** — Bygget med Swift og AppKit, ikke Electron. Hurtig opstart, lavt hukommelsesforbrug.
|
||||
- **Ghostty-kompatibel** — Læser din eksisterende `~/.config/ghostty/config` til temaer, skrifttyper og farver
|
||||
|
|
@ -60,12 +103,26 @@ Jeg kører mange Claude Code- og Codex-sessioner parallelt. Jeg brugte Ghostty m
|
|||
|
||||
Jeg prøvede et par kodningsorkestratore, men de fleste var Electron/Tauri-apps, og ydelsen irriterede mig. Jeg foretrækker også bare terminalen, da GUI-orkestratore låser dig ind i deres arbejdsgang. Så jeg byggede cmux som en nativ macOS-app i Swift/AppKit. Den bruger libghostty til terminal-rendering og læser din eksisterende Ghostty-konfiguration til temaer, skrifttyper og farver.
|
||||
|
||||
De vigtigste tilføjelser er sidebjælken og notifikationssystemet. Sidebjælken har lodrette faner, der viser git-branch, arbejdsmappe, lyttende porte og den seneste notifikationstekst for hvert workspace. Notifikationssystemet opfanger terminalsekvenser (OSC 9/99/777) og har en CLI (`cmux notify`), du kan koble til agent-hooks for Claude Code, OpenCode osv. Når en agent venter, får dens panel en blå ring, og fanen lyser op i sidebjælken, så jeg kan se, hvilken der har brug for mig på tværs af opdelinger og faner. Cmd+Shift+U hopper til den seneste ulæste.
|
||||
De vigtigste tilføjelser er sidebjælken og notifikationssystemet. Sidebjælken har lodrette faner, der viser git-branch, tilknyttet PR-status/nummer, arbejdsmappe, lyttende porte og den seneste notifikationstekst for hvert workspace. Notifikationssystemet opfanger terminalsekvenser (OSC 9/99/777) og har en CLI (`cmux notify`), du kan koble til agent-hooks for Claude Code, OpenCode osv. Når en agent venter, får dens panel en blå ring, og fanen lyser op i sidebjælken, så jeg kan se, hvilken der har brug for mig på tværs af opdelinger og faner. Cmd+Shift+U hopper til den seneste ulæste.
|
||||
|
||||
Den indbyggede browser har en scriptbar API porteret fra [agent-browser](https://github.com/vercel-labs/agent-browser). Agenter kan tage et snapshot af tilgængelighedstræet, få elementreferencer, klikke, udfylde formularer og evaluere JS. Du kan dele et browserpanel ved siden af din terminal og lade Claude Code interagere direkte med din udviklingsserver.
|
||||
|
||||
Alt er scriptbart gennem CLI og socket API — opret workspaces/faner, del paneler, send tastetryk, åbn URL'er i browseren.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux foreskriver ikke, hvordan udviklere bruger deres værktøjer. Det er en terminal og browser med en CLI, resten er op til dig.
|
||||
|
||||
cmux er en primitiv, ikke en løsning. Det giver dig en terminal, en browser, notifikationer, workspaces, opdelinger, faner og en CLI til at styre det hele. cmux tvinger dig ikke ind i en forudbestemt måde at bruge kodningsagenter på. Hvad du bygger med primitiverne, er dit eget.
|
||||
|
||||
De bedste udviklere har altid bygget deres egne værktøjer. Ingen har endnu fundet den bedste måde at arbejde med agenter på, og holdene bag lukkede produkter har heller ikke. De udviklere, der er tættest på deres egne kodebaser, vil finde ud af det først.
|
||||
|
||||
Giv en million udviklere komponerbare primitiver, og de vil kollektivt finde de mest effektive arbejdsgange hurtigere, end noget produkthold kunne designe oppefra.
|
||||
|
||||
## Dokumentation
|
||||
|
||||
For mere information om konfiguration af cmux, [se vores dokumentation](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Tastaturgenveje
|
||||
|
||||
### Workspaces
|
||||
|
|
@ -78,6 +135,7 @@ Alt er scriptbart gennem CLI og socket API — opret workspaces/faner, del panel
|
|||
| ⌃ ⌘ ] | Næste workspace |
|
||||
| ⌃ ⌘ [ | Forrige workspace |
|
||||
| ⌘ ⇧ W | Luk workspace |
|
||||
| ⌘ ⇧ R | Omdøb workspace |
|
||||
| ⌘ B | Skjul/vis sidebjælke |
|
||||
|
||||
### Overflader
|
||||
|
|
@ -104,6 +162,8 @@ Alt er scriptbart gennem CLI og socket API — opret workspaces/faner, del panel
|
|||
|
||||
### Browser
|
||||
|
||||
Browserens udviklerværktøjsgenveje følger Safaris standarder og kan tilpasses i `Indstillinger → Tastaturgenveje`.
|
||||
|
||||
| Genvej | Handling |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Åbn browser i opdeling |
|
||||
|
|
@ -111,7 +171,8 @@ Alt er scriptbart gennem CLI og socket API — opret workspaces/faner, del panel
|
|||
| ⌘ [ | Tilbage |
|
||||
| ⌘ ] | Frem |
|
||||
| ⌘ R | Genindlæs side |
|
||||
| ⌥ ⌘ I | Åbn Udviklerværktøjer |
|
||||
| ⌥ ⌘ I | Slå Udviklerværktøjer til/fra (Safari-standard) |
|
||||
| ⌥ ⌘ C | Vis JavaScript-konsol (Safari-standard) |
|
||||
|
||||
### Notifikationer
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ Alt er scriptbart gennem CLI og socket API — opret workspaces/faner, del panel
|
|||
| ⌘ ⇧ , | Genindlæs konfiguration |
|
||||
| ⌘ Q | Afslut |
|
||||
|
||||
## Nightly Builds
|
||||
|
||||
[Download cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY er en separat app med sit eget bundle-ID, så den kører side om side med den stabile version. Bygges automatisk fra det seneste `main`-commit og opdaterer sig selv automatisk via sit eget Sparkle-feed.
|
||||
|
||||
## Sessionsgenoprettelse (nuværende adfærd)
|
||||
|
||||
Ved genstart genopretter cmux i øjeblikket kun app-layout og metadata:
|
||||
- Vindue/workspace/panel-layout
|
||||
- Arbejdsmapper
|
||||
- Terminal-scrollback (best effort)
|
||||
- Browser-URL og navigationshistorik
|
||||
|
||||
cmux genopretter **ikke** aktive procestilstande i terminalapps. For eksempel genoptages aktive Claude Code/tmux/vim-sessioner endnu ikke efter genstart.
|
||||
|
||||
## Stjernehistorik
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Bidrag
|
||||
|
||||
Måder at deltage:
|
||||
|
||||
- Følg os på X for opdateringer [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) og [@austinywang](https://x.com/austinywang)
|
||||
- Deltag i samtalen på [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Opret og deltag i [GitHub issues](https://github.com/manaflow-ai/cmux/issues) og [diskussioner](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Fortæl os, hvad du bygger med cmux
|
||||
|
||||
## Fællesskab
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux er gratis, open source og vil altid være det. Hvis du gerne vil støtte udviklingen og få tidlig adgang til det, der kommer:
|
||||
|
||||
**[Få Founder's Edition](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Prioriterede funktionsønsker og fejlrettelser**
|
||||
- **Tidlig adgang: cmux AI der giver dig kontekst om hvert workspace, fane og panel**
|
||||
- **Tidlig adgang: iOS-app med terminaler synkroniseret mellem desktop og telefon**
|
||||
- **Tidlig adgang: Cloud VM'er**
|
||||
- **Tidlig adgang: Stemmetilstand**
|
||||
- **Min personlige iMessage/WhatsApp**
|
||||
|
||||
## Licens
|
||||
|
||||
Dette projekt er licenseret under GNU Affero General Public License v3.0 eller senere (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
140
README.de.md
|
|
@ -1,7 +1,5 @@
|
|||
> Diese Übersetzung wurde von Claude erstellt. Verbesserungsvorschläge sind als PR willkommen.
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | Deutsch | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a></p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Ein Ghostty-basiertes macOS-Terminal mit vertikalen Tabs und Benachrichtigungen für AI-Coding-Agenten</p>
|
||||
|
||||
|
|
@ -12,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux Screenshot" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | Deutsch | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux Screenshot" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demo-Video</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Funktionen
|
||||
|
||||
- **Vertikale Tabs** — Die Seitenleiste zeigt Git-Branch, Arbeitsverzeichnis, lauschende Ports und den neuesten Benachrichtigungstext
|
||||
- **Benachrichtigungsringe** — Bereiche erhalten einen blauen Ring und Tabs leuchten auf, wenn AI-Agenten (Claude Code, OpenCode) Ihre Aufmerksamkeit benötigen
|
||||
- **Benachrichtigungspanel** — Alle ausstehenden Benachrichtigungen auf einen Blick sehen und zur neuesten ungelesenen springen
|
||||
- **Geteilte Bereiche** — Horizontale und vertikale Teilung
|
||||
- **Integrierter Browser** — Teilen Sie einen Browser neben Ihrem Terminal mit einer skriptfähigen API, portiert von [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Benachrichtigungsringe</h3>
|
||||
Bereiche erhalten einen blauen Ring und Tabs leuchten auf, wenn Coding-Agenten Ihre Aufmerksamkeit benötigen
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Benachrichtigungsringe" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Benachrichtigungspanel</h3>
|
||||
Alle ausstehenden Benachrichtigungen auf einen Blick sehen und zur neuesten ungelesenen springen
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Seitenleisten-Benachrichtigungsabzeichen" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Integrierter Browser</h3>
|
||||
Teilen Sie einen Browser neben Ihrem Terminal mit einer skriptfähigen API, portiert von <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Integrierter Browser" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Vertikale + horizontale Tabs</h3>
|
||||
Die Seitenleiste zeigt Git-Branch, verknüpften PR-Status/Nummer, Arbeitsverzeichnis, lauschende Ports und den neuesten Benachrichtigungstext. Horizontal und vertikal teilen.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Vertikale Tabs und geteilte Bereiche" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Skriptfähig** — CLI und Socket-API zum Erstellen von Arbeitsbereichen, Teilen von Bereichen, Senden von Tastenanschlägen und Automatisieren des Browsers
|
||||
- **Native macOS-App** — Entwickelt mit Swift und AppKit, nicht Electron. Schneller Start, geringer Speicherverbrauch.
|
||||
- **Ghostty-kompatibel** — Liest Ihre vorhandene `~/.config/ghostty/config` für Themes, Schriftarten und Farben
|
||||
|
|
@ -58,12 +103,26 @@ Ich führe viele Claude Code- und Codex-Sitzungen parallel aus. Ich habe Ghostty
|
|||
|
||||
Ich habe einige Coding-Orchestratoren ausprobiert, aber die meisten waren Electron/Tauri-Apps und die Performance hat mich gestört. Ich bevorzuge außerdem das Terminal, da GUI-Orchestratoren einen in ihren Workflow einschließen. Also habe ich cmux als native macOS-App in Swift/AppKit gebaut. Es verwendet libghostty für das Terminal-Rendering und liest Ihre vorhandene Ghostty-Konfiguration für Themes, Schriftarten und Farben.
|
||||
|
||||
Die wesentlichen Ergänzungen sind die Seitenleiste und das Benachrichtigungssystem. Die Seitenleiste hat vertikale Tabs, die Git-Branch, Arbeitsverzeichnis, lauschende Ports und den neuesten Benachrichtigungstext für jeden Arbeitsbereich anzeigen. Das Benachrichtigungssystem erkennt Terminal-Sequenzen (OSC 9/99/777) und bietet eine CLI (`cmux notify`), die Sie in Agent-Hooks für Claude Code, OpenCode usw. einbinden können. Wenn ein Agent wartet, bekommt sein Bereich einen blauen Ring und der Tab leuchtet in der Seitenleiste auf, sodass ich über Teilungen und Tabs hinweg erkennen kann, welcher mich braucht. ⌘⇧U springt zur neuesten ungelesenen Benachrichtigung.
|
||||
Die wesentlichen Ergänzungen sind die Seitenleiste und das Benachrichtigungssystem. Die Seitenleiste hat vertikale Tabs, die Git-Branch, verknüpften PR-Status/Nummer, Arbeitsverzeichnis, lauschende Ports und den neuesten Benachrichtigungstext für jeden Arbeitsbereich anzeigen. Das Benachrichtigungssystem erkennt Terminal-Sequenzen (OSC 9/99/777) und bietet eine CLI (`cmux notify`), die Sie in Agent-Hooks für Claude Code, OpenCode usw. einbinden können. Wenn ein Agent wartet, bekommt sein Bereich einen blauen Ring und der Tab leuchtet in der Seitenleiste auf, sodass ich über Teilungen und Tabs hinweg erkennen kann, welcher mich braucht. ⌘⇧U springt zur neuesten ungelesenen Benachrichtigung.
|
||||
|
||||
Der integrierte Browser hat eine skriptfähige API, portiert von [agent-browser](https://github.com/vercel-labs/agent-browser). Agenten können den Barrierefreiheitsbaum erfassen, Elementreferenzen erhalten, klicken, Formulare ausfüllen und JS ausführen. Sie können einen Browser-Bereich neben Ihrem Terminal teilen und Claude Code direkt mit Ihrem Entwicklungsserver interagieren lassen.
|
||||
|
||||
Alles ist über CLI und Socket-API skriptfähig — Arbeitsbereiche/Tabs erstellen, Bereiche teilen, Tastenanschläge senden, URLs im Browser öffnen.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux schreibt Entwicklern nicht vor, wie sie ihre Werkzeuge nutzen sollen. Es ist ein Terminal und Browser mit einer CLI, und der Rest liegt bei Ihnen.
|
||||
|
||||
cmux ist ein Grundbaustein, keine fertige Lösung. Es bietet Ihnen ein Terminal, einen Browser, Benachrichtigungen, Arbeitsbereiche, Teilungen, Tabs und eine CLI, um alles zu steuern. cmux zwingt Sie nicht in eine bestimmte Art, Coding-Agenten zu nutzen. Was Sie mit den Grundbausteinen bauen, ist Ihre Sache.
|
||||
|
||||
Die besten Entwickler haben schon immer ihre eigenen Werkzeuge gebaut. Niemand hat bisher die beste Art gefunden, mit Agenten zu arbeiten, und die Teams, die geschlossene Produkte bauen, auch nicht. Die Entwickler, die ihren eigenen Codebasen am nächsten sind, werden es zuerst herausfinden.
|
||||
|
||||
Geben Sie einer Million Entwickler komponierbare Grundbausteine, und sie werden gemeinsam die effizientesten Workflows schneller finden, als jedes Produktteam es von oben herab entwerfen könnte.
|
||||
|
||||
## Dokumentation
|
||||
|
||||
Weitere Informationen zur Konfiguration von cmux finden Sie in [unserer Dokumentation](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Tastenkürzel
|
||||
|
||||
### Arbeitsbereiche
|
||||
|
|
@ -76,6 +135,7 @@ Alles ist über CLI und Socket-API skriptfähig — Arbeitsbereiche/Tabs erstell
|
|||
| ⌃ ⌘ ] | Nächster Arbeitsbereich |
|
||||
| ⌃ ⌘ [ | Vorheriger Arbeitsbereich |
|
||||
| ⌘ ⇧ W | Arbeitsbereich schließen |
|
||||
| ⌘ ⇧ R | Arbeitsbereich umbenennen |
|
||||
| ⌘ B | Seitenleiste umschalten |
|
||||
|
||||
### Oberflächen
|
||||
|
|
@ -102,6 +162,8 @@ Alles ist über CLI und Socket-API skriptfähig — Arbeitsbereiche/Tabs erstell
|
|||
|
||||
### Browser
|
||||
|
||||
Tastenkürzel für Browser-Entwicklertools folgen den Safari-Standardeinstellungen und sind in `Einstellungen → Tastenkürzel` anpassbar.
|
||||
|
||||
| Tastenkürzel | Aktion |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Browser in Teilung öffnen |
|
||||
|
|
@ -109,7 +171,8 @@ Alles ist über CLI und Socket-API skriptfähig — Arbeitsbereiche/Tabs erstell
|
|||
| ⌘ [ | Zurück |
|
||||
| ⌘ ] | Vorwärts |
|
||||
| ⌘ R | Seite neu laden |
|
||||
| ⌥ ⌘ I | Entwicklertools öffnen |
|
||||
| ⌥ ⌘ I | Entwicklertools umschalten (Safari-Standard) |
|
||||
| ⌥ ⌘ C | JavaScript-Konsole anzeigen (Safari-Standard) |
|
||||
|
||||
### Benachrichtigungen
|
||||
|
||||
|
|
@ -146,6 +209,63 @@ Alles ist über CLI und Socket-API skriptfähig — Arbeitsbereiche/Tabs erstell
|
|||
| ⌘ ⇧ , | Konfiguration neu laden |
|
||||
| ⌘ Q | Beenden |
|
||||
|
||||
## Nightly Builds
|
||||
|
||||
[cmux NIGHTLY herunterladen](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY ist eine separate App mit eigener Bundle-ID, die neben der stabilen Version läuft. Wird automatisch vom neuesten `main`-Commit gebaut und aktualisiert sich über einen eigenen Sparkle-Feed.
|
||||
|
||||
## Sitzungswiederherstellung (aktuelles Verhalten)
|
||||
|
||||
Beim Neustart stellt cmux derzeit nur App-Layout und Metadaten wieder her:
|
||||
- Fenster-/Arbeitsbereich-/Bereichs-Layout
|
||||
- Arbeitsverzeichnisse
|
||||
- Terminal-Scrollback (bestmöglich)
|
||||
- Browser-URL und Navigationsverlauf
|
||||
|
||||
cmux stellt **keine** laufenden Prozesse in Terminal-Apps wieder her. Zum Beispiel werden aktive Claude Code-/tmux-/vim-Sitzungen nach einem Neustart noch nicht fortgesetzt.
|
||||
|
||||
## Star-Verlauf
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Mitwirken
|
||||
|
||||
Möglichkeiten, sich einzubringen:
|
||||
|
||||
- Folgen Sie uns auf X für Updates [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) und [@austinywang](https://x.com/austinywang)
|
||||
- Nehmen Sie an der Diskussion auf [Discord](https://discord.gg/xsgFEVrWCZ) teil
|
||||
- Erstellen Sie [GitHub Issues](https://github.com/manaflow-ai/cmux/issues) und beteiligen Sie sich an [Diskussionen](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Lassen Sie uns wissen, was Sie mit cmux bauen
|
||||
|
||||
## Community
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux ist kostenlos, Open Source und wird es immer sein. Wenn Sie die Entwicklung unterstützen und frühen Zugang zu kommenden Funktionen erhalten möchten:
|
||||
|
||||
**[Founder's Edition erhalten](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Priorisierte Feature-Requests/Bugfixes**
|
||||
- **Früher Zugang: cmux AI, das Ihnen Kontext zu jedem Arbeitsbereich, Tab und Panel gibt**
|
||||
- **Früher Zugang: iOS-App mit zwischen Desktop und Telefon synchronisierten Terminals**
|
||||
- **Früher Zugang: Cloud-VMs**
|
||||
- **Früher Zugang: Sprachmodus**
|
||||
- **Meine persönliche iMessage/WhatsApp**
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt ist unter der GNU Affero General Public License v3.0 oder neuer (`AGPL-3.0-or-later`) lizenziert.
|
||||
|
|
|
|||
140
README.es.md
|
|
@ -1,7 +1,5 @@
|
|||
> Esta traducción fue generada por Claude. Si tienes sugerencias de mejora, abre un PR.
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | Español | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a></p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Un terminal macOS basado en Ghostty con pestañas verticales y notificaciones para agentes de programación con IA</p>
|
||||
|
||||
|
|
@ -12,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="Captura de pantalla de cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | Español | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="Captura de pantalla de cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Video de demostración</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Características
|
||||
|
||||
- **Pestañas verticales** — La barra lateral muestra la rama de git, el directorio de trabajo, los puertos en escucha y el texto de la última notificación
|
||||
- **Anillos de notificación** — Los paneles obtienen un anillo azul y las pestañas se iluminan cuando los agentes de IA (Claude Code, OpenCode) necesitan tu atención
|
||||
- **Panel de notificaciones** — Ve todas las notificaciones pendientes en un solo lugar, salta a la más reciente no leída
|
||||
- **Paneles divididos** — Divisiones horizontales y verticales
|
||||
- **Navegador integrado** — Divide un navegador junto a tu terminal con una API programable portada de [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Anillos de notificación</h3>
|
||||
Los paneles obtienen un anillo azul y las pestañas se iluminan cuando los agentes de programación necesitan tu atención
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Anillos de notificación" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Panel de notificaciones</h3>
|
||||
Ve todas las notificaciones pendientes en un solo lugar, salta a la más reciente no leída
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Insignia de notificación en la barra lateral" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Navegador integrado</h3>
|
||||
Divide un navegador junto a tu terminal con una API programable portada de <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Navegador integrado" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Pestañas verticales + horizontales</h3>
|
||||
La barra lateral muestra la rama de git, el estado/número del PR vinculado, el directorio de trabajo, los puertos en escucha y el texto de la última notificación. Divide horizontal y verticalmente.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Pestañas verticales y paneles divididos" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Programable** — CLI y API de socket para crear espacios de trabajo, dividir paneles, enviar pulsaciones de teclas y automatizar el navegador
|
||||
- **App nativa de macOS** — Construida con Swift y AppKit, no con Electron. Inicio rápido, bajo consumo de memoria.
|
||||
- **Compatible con Ghostty** — Lee tu configuración existente en `~/.config/ghostty/config` para temas, fuentes y colores
|
||||
|
|
@ -58,12 +103,26 @@ Ejecuto muchas sesiones de Claude Code y Codex en paralelo. Estaba usando Ghostt
|
|||
|
||||
Probé algunos orquestadores de programación, pero la mayoría eran aplicaciones Electron/Tauri y el rendimiento me molestaba. Además, simplemente prefiero la terminal ya que los orquestadores con GUI te encierran en su flujo de trabajo. Así que construí cmux como una app nativa de macOS en Swift/AppKit. Usa libghostty para el renderizado del terminal y lee tu configuración existente de Ghostty para temas, fuentes y colores.
|
||||
|
||||
Las principales adiciones son la barra lateral y el sistema de notificaciones. La barra lateral tiene pestañas verticales que muestran la rama de git, el directorio de trabajo, los puertos en escucha y el texto de la última notificación para cada espacio de trabajo. El sistema de notificaciones detecta secuencias de terminal (OSC 9/99/777) y tiene un CLI (`cmux notify`) que puedes conectar a los hooks de agentes para Claude Code, OpenCode, etc. Cuando un agente está esperando, su panel obtiene un anillo azul y la pestaña se ilumina en la barra lateral, para que pueda saber cuál me necesita entre divisiones y pestañas. ⌘⇧U salta a la notificación no leída más reciente.
|
||||
Las principales adiciones son la barra lateral y el sistema de notificaciones. La barra lateral tiene pestañas verticales que muestran la rama de git, el estado/número del PR vinculado, el directorio de trabajo, los puertos en escucha y el texto de la última notificación para cada espacio de trabajo. El sistema de notificaciones detecta secuencias de terminal (OSC 9/99/777) y tiene un CLI (`cmux notify`) que puedes conectar a los hooks de agentes para Claude Code, OpenCode, etc. Cuando un agente está esperando, su panel obtiene un anillo azul y la pestaña se ilumina en la barra lateral, para que pueda saber cuál me necesita entre divisiones y pestañas. ⌘⇧U salta a la notificación no leída más reciente.
|
||||
|
||||
El navegador integrado tiene una API programable portada de [agent-browser](https://github.com/vercel-labs/agent-browser). Los agentes pueden capturar el árbol de accesibilidad, obtener referencias de elementos, hacer clic, rellenar formularios y ejecutar JS. Puedes dividir un panel de navegador junto a tu terminal y hacer que Claude Code interactúe directamente con tu servidor de desarrollo.
|
||||
|
||||
Todo es programable a través del CLI y la API de socket — crear espacios de trabajo/pestañas, dividir paneles, enviar pulsaciones de teclas, abrir URLs en el navegador.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux no prescribe cómo los desarrolladores deben usar sus herramientas. Es un terminal y navegador con un CLI, y el resto depende de ti.
|
||||
|
||||
cmux es un primitivo, no una solución. Te da un terminal, un navegador, notificaciones, espacios de trabajo, divisiones, pestañas y un CLI para controlarlo todo. cmux no te obliga a usar los agentes de programación de una manera específica. Lo que construyas con los primitivos es tuyo.
|
||||
|
||||
Los mejores desarrolladores siempre han construido sus propias herramientas. Nadie ha descubierto la mejor manera de trabajar con agentes todavía, y los equipos que construyen productos cerrados tampoco. Los desarrolladores más cercanos a sus propias bases de código lo descubrirán primero.
|
||||
|
||||
Dale a un millón de desarrolladores primitivos componibles y encontrarán colectivamente los flujos de trabajo más eficientes más rápido de lo que cualquier equipo de producto podría diseñar de arriba hacia abajo.
|
||||
|
||||
## Documentación
|
||||
|
||||
Para más información sobre cómo configurar cmux, [visita nuestra documentación](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Atajos de teclado
|
||||
|
||||
### Espacios de trabajo
|
||||
|
|
@ -76,6 +135,7 @@ Todo es programable a través del CLI y la API de socket — crear espacios de t
|
|||
| ⌃ ⌘ ] | Siguiente espacio de trabajo |
|
||||
| ⌃ ⌘ [ | Espacio de trabajo anterior |
|
||||
| ⌘ ⇧ W | Cerrar espacio de trabajo |
|
||||
| ⌘ ⇧ R | Renombrar espacio de trabajo |
|
||||
| ⌘ B | Alternar barra lateral |
|
||||
|
||||
### Superficies
|
||||
|
|
@ -102,6 +162,8 @@ Todo es programable a través del CLI y la API de socket — crear espacios de t
|
|||
|
||||
### Navegador
|
||||
|
||||
Los atajos de herramientas de desarrollo del navegador siguen los valores predeterminados de Safari y son personalizables en `Ajustes → Atajos de teclado`.
|
||||
|
||||
| Atajo | Acción |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Abrir navegador en división |
|
||||
|
|
@ -109,7 +171,8 @@ Todo es programable a través del CLI y la API de socket — crear espacios de t
|
|||
| ⌘ [ | Atrás |
|
||||
| ⌘ ] | Adelante |
|
||||
| ⌘ R | Recargar página |
|
||||
| ⌥ ⌘ I | Abrir herramientas de desarrollo |
|
||||
| ⌥ ⌘ I | Alternar herramientas de desarrollo (predeterminado de Safari) |
|
||||
| ⌥ ⌘ C | Mostrar consola de JavaScript (predeterminado de Safari) |
|
||||
|
||||
### Notificaciones
|
||||
|
||||
|
|
@ -146,6 +209,63 @@ Todo es programable a través del CLI y la API de socket — crear espacios de t
|
|||
| ⌘ ⇧ , | Recargar configuración |
|
||||
| ⌘ Q | Salir |
|
||||
|
||||
## Compilaciones nocturnas
|
||||
|
||||
[Descargar cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY es una app separada con su propio bundle ID, por lo que se ejecuta junto a la versión estable. Se compila automáticamente desde el último commit de `main` y se actualiza automáticamente a través de su propio feed de Sparkle.
|
||||
|
||||
## Restauración de sesión (comportamiento actual)
|
||||
|
||||
Al relanzar, cmux actualmente restaura solo el diseño y los metadatos de la aplicación:
|
||||
- Diseño de ventanas/espacios de trabajo/paneles
|
||||
- Directorios de trabajo
|
||||
- Historial de desplazamiento del terminal (mejor esfuerzo)
|
||||
- URL del navegador e historial de navegación
|
||||
|
||||
cmux **no** restaura el estado de los procesos activos dentro de las aplicaciones de terminal. Por ejemplo, las sesiones activas de Claude Code/tmux/vim no se reanudan después de reiniciar todavía.
|
||||
|
||||
## Historial de estrellas
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contribuir
|
||||
|
||||
Formas de participar:
|
||||
|
||||
- Síguenos en X para actualizaciones [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) y [@austinywang](https://x.com/austinywang)
|
||||
- Únete a la conversación en [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Crea y participa en [GitHub issues](https://github.com/manaflow-ai/cmux/issues) y [discusiones](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Cuéntanos qué estás construyendo con cmux
|
||||
|
||||
## Comunidad
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux es gratuito, de código abierto, y siempre lo será. Si deseas apoyar el desarrollo y obtener acceso anticipado a lo que viene:
|
||||
|
||||
**[Obtener Founder's Edition](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Solicitudes de funciones/corrección de errores priorizadas**
|
||||
- **Acceso anticipado: cmux AI que te da contexto sobre cada espacio de trabajo, pestaña y panel**
|
||||
- **Acceso anticipado: app de iOS con terminales sincronizadas entre escritorio y teléfono**
|
||||
- **Acceso anticipado: VMs en la nube**
|
||||
- **Acceso anticipado: Modo de voz**
|
||||
- **Mi iMessage/WhatsApp personal**
|
||||
|
||||
## Licencia
|
||||
|
||||
Este proyecto está licenciado bajo la Licencia Pública General Affero de GNU v3.0 o posterior (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
140
README.fr.md
|
|
@ -1,7 +1,5 @@
|
|||
> Cette traduction a été générée par Claude. Si vous avez des suggestions d'amélioration, ouvrez une PR.
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | Français | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a></p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Un terminal macOS basé sur Ghostty avec des onglets verticaux et des notifications pour les agents de programmation IA</p>
|
||||
|
||||
|
|
@ -12,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="Capture d'écran de cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | Français | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="Capture d'écran de cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Vidéo de démonstration</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Onglets verticaux** — La barre latérale affiche la branche git, le répertoire de travail, les ports en écoute et le texte de la dernière notification
|
||||
- **Anneaux de notification** — Les panneaux reçoivent un anneau bleu et les onglets s'illuminent lorsque les agents IA (Claude Code, OpenCode) ont besoin de votre attention
|
||||
- **Panneau de notifications** — Consultez toutes les notifications en attente au même endroit, accédez directement à la plus récente non lue
|
||||
- **Panneaux divisés** — Divisions horizontales et verticales
|
||||
- **Navigateur intégré** — Divisez un navigateur à côté de votre terminal avec une API scriptable portée depuis [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Anneaux de notification</h3>
|
||||
Les panneaux reçoivent un anneau bleu et les onglets s'illuminent lorsque les agents de programmation ont besoin de votre attention
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Anneaux de notification" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Panneau de notifications</h3>
|
||||
Consultez toutes les notifications en attente au même endroit, accédez directement à la plus récente non lue
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Badge de notification dans la barre latérale" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Navigateur intégré</h3>
|
||||
Divisez un navigateur à côté de votre terminal avec une API scriptable portée depuis <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Navigateur intégré" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Onglets verticaux + horizontaux</h3>
|
||||
La barre latérale affiche la branche git, le statut/numéro de PR lié, le répertoire de travail, les ports en écoute et le texte de la dernière notification. Divisez horizontalement et verticalement.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Onglets verticaux et panneaux divisés" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Scriptable** — CLI et API socket pour créer des espaces de travail, diviser des panneaux, envoyer des frappes clavier et automatiser le navigateur
|
||||
- **Application macOS native** — Construite avec Swift et AppKit, pas Electron. Démarrage rapide, faible consommation mémoire.
|
||||
- **Compatible Ghostty** — Lit votre fichier `~/.config/ghostty/config` existant pour les thèmes, polices et couleurs
|
||||
|
|
@ -58,12 +103,26 @@ J'exécute beaucoup de sessions Claude Code et Codex en parallèle. J'utilisais
|
|||
|
||||
J'ai essayé quelques orchestrateurs de programmation, mais la plupart étaient des applications Electron/Tauri et les performances me dérangeaient. Je préfère aussi simplement le terminal, car les orchestrateurs à interface graphique vous enferment dans leur flux de travail. J'ai donc construit cmux comme une application macOS native en Swift/AppKit. Elle utilise libghostty pour le rendu du terminal et lit votre configuration Ghostty existante pour les thèmes, polices et couleurs.
|
||||
|
||||
Les principaux ajouts sont la barre latérale et le système de notifications. La barre latérale comporte des onglets verticaux qui affichent la branche git, le répertoire de travail, les ports en écoute et le texte de la dernière notification pour chaque espace de travail. Le système de notifications capte les séquences de terminal (OSC 9/99/777) et dispose d'un CLI (`cmux notify`) que vous pouvez brancher aux hooks d'agents pour Claude Code, OpenCode, etc. Quand un agent est en attente, son panneau reçoit un anneau bleu et l'onglet s'illumine dans la barre latérale, pour que je puisse identifier lequel a besoin de moi parmi les divisions et les onglets. ⌘⇧U permet de sauter à la notification non lue la plus récente.
|
||||
Les principaux ajouts sont la barre latérale et le système de notifications. La barre latérale comporte des onglets verticaux qui affichent la branche git, le statut/numéro de PR lié, le répertoire de travail, les ports en écoute et le texte de la dernière notification pour chaque espace de travail. Le système de notifications capte les séquences de terminal (OSC 9/99/777) et dispose d'un CLI (`cmux notify`) que vous pouvez brancher aux hooks d'agents pour Claude Code, OpenCode, etc. Quand un agent est en attente, son panneau reçoit un anneau bleu et l'onglet s'illumine dans la barre latérale, pour que je puisse identifier lequel a besoin de moi parmi les divisions et les onglets. ⌘⇧U permet de sauter à la notification non lue la plus récente.
|
||||
|
||||
Le navigateur intégré dispose d'une API scriptable portée depuis [agent-browser](https://github.com/vercel-labs/agent-browser). Les agents peuvent capturer l'arbre d'accessibilité, obtenir des références d'éléments, cliquer, remplir des formulaires et exécuter du JS. Vous pouvez diviser un panneau navigateur à côté de votre terminal et laisser Claude Code interagir directement avec votre serveur de développement.
|
||||
|
||||
Tout est scriptable via le CLI et l'API socket — créer des espaces de travail/onglets, diviser des panneaux, envoyer des frappes clavier, ouvrir des URL dans le navigateur.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux ne prescrit pas comment les développeurs utilisent leurs outils. C'est un terminal et un navigateur avec un CLI, le reste vous appartient.
|
||||
|
||||
cmux est une primitive, pas une solution. Il vous donne un terminal, un navigateur, des notifications, des espaces de travail, des divisions, des onglets et un CLI pour tout contrôler. cmux ne vous impose pas une façon préconçue d'utiliser les agents de programmation. Ce que vous construisez avec ces primitives vous appartient.
|
||||
|
||||
Les meilleurs développeurs ont toujours construit leurs propres outils. Personne n'a encore trouvé la meilleure façon de travailler avec les agents, et les équipes qui construisent des produits fermés ne l'ont pas trouvée non plus. Les développeurs les plus proches de leurs propres bases de code trouveront la solution en premier.
|
||||
|
||||
Donnez à un million de développeurs des primitives composables et ils trouveront collectivement les flux de travail les plus efficaces plus rapidement que n'importe quelle équipe produit ne pourrait les concevoir de manière descendante.
|
||||
|
||||
## Documentation
|
||||
|
||||
Pour plus d'informations sur la configuration de cmux, [consultez notre documentation](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Raccourcis clavier
|
||||
|
||||
### Espaces de travail
|
||||
|
|
@ -76,6 +135,7 @@ Tout est scriptable via le CLI et l'API socket — créer des espaces de travail
|
|||
| ⌃ ⌘ ] | Espace de travail suivant |
|
||||
| ⌃ ⌘ [ | Espace de travail précédent |
|
||||
| ⌘ ⇧ W | Fermer l'espace de travail |
|
||||
| ⌘ ⇧ R | Renommer l'espace de travail |
|
||||
| ⌘ B | Basculer la barre latérale |
|
||||
|
||||
### Surfaces
|
||||
|
|
@ -102,6 +162,8 @@ Tout est scriptable via le CLI et l'API socket — créer des espaces de travail
|
|||
|
||||
### Navigateur
|
||||
|
||||
Les raccourcis des outils de développement du navigateur suivent les valeurs par défaut de Safari et sont personnalisables dans `Paramètres → Raccourcis clavier`.
|
||||
|
||||
| Raccourci | Action |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Ouvrir le navigateur en division |
|
||||
|
|
@ -109,7 +171,8 @@ Tout est scriptable via le CLI et l'API socket — créer des espaces de travail
|
|||
| ⌘ [ | Reculer |
|
||||
| ⌘ ] | Avancer |
|
||||
| ⌘ R | Recharger la page |
|
||||
| ⌥ ⌘ I | Ouvrir les outils de développement |
|
||||
| ⌥ ⌘ I | Basculer les outils de développement (par défaut Safari) |
|
||||
| ⌥ ⌘ C | Afficher la console JavaScript (par défaut Safari) |
|
||||
|
||||
### Notifications
|
||||
|
||||
|
|
@ -146,6 +209,63 @@ Tout est scriptable via le CLI et l'API socket — créer des espaces de travail
|
|||
| ⌘ ⇧ , | Recharger la configuration |
|
||||
| ⌘ Q | Quitter |
|
||||
|
||||
## Builds Nightly
|
||||
|
||||
[Télécharger cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY est une application séparée avec son propre identifiant de bundle, elle fonctionne donc en parallèle de la version stable. Construite automatiquement à partir du dernier commit `main` et mise à jour automatiquement via son propre flux Sparkle.
|
||||
|
||||
## Restauration de session (comportement actuel)
|
||||
|
||||
Au relancement, cmux restaure actuellement uniquement la disposition et les métadonnées de l'application :
|
||||
- Disposition des fenêtres/espaces de travail/panneaux
|
||||
- Répertoires de travail
|
||||
- Historique de défilement du terminal (au mieux)
|
||||
- URL du navigateur et historique de navigation
|
||||
|
||||
cmux ne restaure **pas** l'état des processus actifs dans les applications du terminal. Par exemple, les sessions actives de Claude Code/tmux/vim ne sont pas encore reprises après un redémarrage.
|
||||
|
||||
## Historique des étoiles
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contribuer
|
||||
|
||||
Façons de s'impliquer :
|
||||
|
||||
- Suivez-nous sur X pour les mises à jour [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), et [@austinywang](https://x.com/austinywang)
|
||||
- Rejoignez la conversation sur [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Créez et participez aux [issues GitHub](https://github.com/manaflow-ai/cmux/issues) et aux [discussions](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Dites-nous ce que vous construisez avec cmux
|
||||
|
||||
## Communauté
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Édition Fondateur
|
||||
|
||||
cmux est gratuit, open source, et le restera toujours. Si vous souhaitez soutenir le développement et obtenir un accès anticipé à ce qui arrive :
|
||||
|
||||
**[Obtenir l'Édition Fondateur](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Demandes de fonctionnalités et corrections de bugs prioritaires**
|
||||
- **Accès anticipé : cmux AI qui vous donne du contexte sur chaque espace de travail, onglet et panneau**
|
||||
- **Accès anticipé : application iOS avec des terminaux synchronisés entre ordinateur et téléphone**
|
||||
- **Accès anticipé : VMs cloud**
|
||||
- **Accès anticipé : Mode vocal**
|
||||
- **Mon iMessage/WhatsApp personnel**
|
||||
|
||||
## Licence
|
||||
|
||||
Ce projet est sous licence GNU Affero General Public License v3.0 ou ultérieure (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
142
README.it.md
|
|
@ -1,9 +1,5 @@
|
|||
> Questa traduzione è stata generata da Claude. Se hai suggerimenti per migliorarla, apri una PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | Italiano | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Un terminale macOS basato su Ghostty con schede verticali e notifiche per agenti di programmazione AI</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="Screenshot di cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | Italiano | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="Screenshot di cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Video demo</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Funzionalità
|
||||
|
||||
- **Schede verticali** — La barra laterale mostra il branch git, la directory di lavoro, le porte in ascolto e il testo dell'ultima notifica
|
||||
- **Anelli di notifica** — I pannelli ricevono un anello blu e le schede si illuminano quando gli agenti AI (Claude Code, OpenCode) richiedono la tua attenzione
|
||||
- **Pannello notifiche** — Visualizza tutte le notifiche in sospeso in un unico posto, salta alla più recente non letta
|
||||
- **Pannelli divisi** — Divisioni orizzontali e verticali
|
||||
- **Browser integrato** — Dividi un browser accanto al tuo terminale con un'API scriptabile derivata da [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Anelli di notifica</h3>
|
||||
I pannelli ricevono un anello blu e le schede si illuminano quando gli agenti di programmazione richiedono la tua attenzione
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Anelli di notifica" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Pannello notifiche</h3>
|
||||
Visualizza tutte le notifiche in sospeso in un unico posto, salta alla più recente non letta
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Badge notifica nella barra laterale" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Browser integrato</h3>
|
||||
Dividi un browser accanto al tuo terminale con un'API scriptabile derivata da <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Browser integrato" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Schede verticali + orizzontali</h3>
|
||||
La barra laterale mostra il branch git, lo stato/numero della PR collegata, la directory di lavoro, le porte in ascolto e il testo dell'ultima notifica. Dividi orizzontalmente e verticalmente.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Schede verticali e pannelli divisi" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Scriptabile** — CLI e socket API per creare workspace, dividere pannelli, inviare sequenze di tasti e automatizzare il browser
|
||||
- **App macOS nativa** — Costruita con Swift e AppKit, non Electron. Avvio rapido, basso consumo di memoria.
|
||||
- **Compatibile con Ghostty** — Legge la tua configurazione esistente `~/.config/ghostty/config` per temi, font e colori
|
||||
|
|
@ -60,12 +103,26 @@ Eseguo molte sessioni di Claude Code e Codex in parallelo. Usavo Ghostty con un
|
|||
|
||||
Ho provato alcuni orchestratori di codifica, ma la maggior parte erano app Electron/Tauri e le prestazioni mi infastidivano. Inoltre preferisco semplicemente il terminale dato che gli orchestratori con interfaccia grafica ti vincolano al loro flusso di lavoro. Così ho costruito cmux come app macOS nativa in Swift/AppKit. Usa libghostty per il rendering del terminale e legge la tua configurazione Ghostty esistente per temi, font e colori.
|
||||
|
||||
Le aggiunte principali sono la barra laterale e il sistema di notifiche. La barra laterale ha schede verticali che mostrano il branch git, la directory di lavoro, le porte in ascolto e il testo dell'ultima notifica per ogni workspace. Il sistema di notifiche rileva le sequenze terminale (OSC 9/99/777) e ha un CLI (`cmux notify`) che puoi collegare agli hook degli agenti per Claude Code, OpenCode, ecc. Quando un agente è in attesa, il suo pannello riceve un anello blu e la scheda si illumina nella barra laterale, così posso capire quale ha bisogno di me tra divisioni e schede. Cmd+Shift+U salta alla più recente non letta.
|
||||
Le aggiunte principali sono la barra laterale e il sistema di notifiche. La barra laterale ha schede verticali che mostrano il branch git, lo stato/numero della PR collegata, la directory di lavoro, le porte in ascolto e il testo dell'ultima notifica per ogni workspace. Il sistema di notifiche rileva le sequenze terminale (OSC 9/99/777) e ha un CLI (`cmux notify`) che puoi collegare agli hook degli agenti per Claude Code, OpenCode, ecc. Quando un agente è in attesa, il suo pannello riceve un anello blu e la scheda si illumina nella barra laterale, così posso capire quale ha bisogno di me tra divisioni e schede. Cmd+Shift+U salta alla più recente non letta.
|
||||
|
||||
Il browser integrato ha un'API scriptabile derivata da [agent-browser](https://github.com/vercel-labs/agent-browser). Gli agenti possono acquisire l'albero di accessibilità, ottenere riferimenti agli elementi, fare clic, compilare moduli e valutare JS. Puoi dividere un pannello browser accanto al tuo terminale e far interagire Claude Code direttamente con il tuo server di sviluppo.
|
||||
|
||||
Tutto è scriptabile attraverso il CLI e la socket API — creare workspace/schede, dividere pannelli, inviare sequenze di tasti, aprire URL nel browser.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux non prescrive come gli sviluppatori usano i propri strumenti. È un terminale e un browser con un CLI, il resto dipende da te.
|
||||
|
||||
cmux è una primitiva, non una soluzione. Ti dà un terminale, un browser, notifiche, workspace, divisioni, schede e un CLI per controllare tutto. cmux non ti obbliga a usare gli agenti di programmazione in un modo predefinito. Quello che costruisci con le primitive è tuo.
|
||||
|
||||
I migliori sviluppatori hanno sempre costruito i propri strumenti. Nessuno ha ancora trovato il modo migliore di lavorare con gli agenti, e i team che costruiscono prodotti chiusi non l'hanno trovato nemmeno loro. Gli sviluppatori più vicini alle proprie basi di codice lo troveranno per primi.
|
||||
|
||||
Date a un milione di sviluppatori primitive componibili e troveranno collettivamente i flussi di lavoro più efficienti più velocemente di quanto qualsiasi team di prodotto potrebbe progettare dall'alto.
|
||||
|
||||
## Documentazione
|
||||
|
||||
Per maggiori informazioni su come configurare cmux, [consulta la nostra documentazione](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Scorciatoie da Tastiera
|
||||
|
||||
### Workspace
|
||||
|
|
@ -78,6 +135,7 @@ Tutto è scriptabile attraverso il CLI e la socket API — creare workspace/sche
|
|||
| ⌃ ⌘ ] | Workspace successivo |
|
||||
| ⌃ ⌘ [ | Workspace precedente |
|
||||
| ⌘ ⇧ W | Chiudi workspace |
|
||||
| ⌘ ⇧ R | Rinomina workspace |
|
||||
| ⌘ B | Mostra/nascondi barra laterale |
|
||||
|
||||
### Superfici
|
||||
|
|
@ -104,6 +162,8 @@ Tutto è scriptabile attraverso il CLI e la socket API — creare workspace/sche
|
|||
|
||||
### Browser
|
||||
|
||||
Le scorciatoie degli strumenti di sviluppo del browser seguono i valori predefiniti di Safari e sono personalizzabili in `Impostazioni → Scorciatoie da tastiera`.
|
||||
|
||||
| Scorciatoia | Azione |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Apri browser in divisione |
|
||||
|
|
@ -111,7 +171,8 @@ Tutto è scriptabile attraverso il CLI e la socket API — creare workspace/sche
|
|||
| ⌘ [ | Indietro |
|
||||
| ⌘ ] | Avanti |
|
||||
| ⌘ R | Ricarica pagina |
|
||||
| ⌥ ⌘ I | Apri Strumenti di Sviluppo |
|
||||
| ⌥ ⌘ I | Mostra/Nascondi Strumenti di Sviluppo (predefinito Safari) |
|
||||
| ⌥ ⌘ C | Mostra Console JavaScript (predefinito Safari) |
|
||||
|
||||
### Notifiche
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ Tutto è scriptabile attraverso il CLI e la socket API — creare workspace/sche
|
|||
| ⌘ ⇧ , | Ricarica configurazione |
|
||||
| ⌘ Q | Esci |
|
||||
|
||||
## Build Nightly
|
||||
|
||||
[Scarica cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY è un'app separata con il proprio bundle ID, quindi funziona in parallelo alla versione stabile. Compilata automaticamente dall'ultimo commit `main` e aggiornata automaticamente tramite il proprio feed Sparkle.
|
||||
|
||||
## Ripristino sessione (comportamento attuale)
|
||||
|
||||
Al riavvio, cmux attualmente ripristina solo il layout e i metadati dell'applicazione:
|
||||
- Layout di finestre/workspace/pannelli
|
||||
- Directory di lavoro
|
||||
- Scrollback del terminale (best effort)
|
||||
- URL del browser e cronologia di navigazione
|
||||
|
||||
cmux **non** ripristina lo stato dei processi attivi nelle applicazioni del terminale. Per esempio, le sessioni attive di Claude Code/tmux/vim non vengono ancora riprese dopo un riavvio.
|
||||
|
||||
## Cronologia Stelle
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contribuire
|
||||
|
||||
Modi per partecipare:
|
||||
|
||||
- Seguici su X per aggiornamenti [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), e [@austinywang](https://x.com/austinywang)
|
||||
- Unisciti alla conversazione su [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Crea e partecipa alle [issue su GitHub](https://github.com/manaflow-ai/cmux/issues) e alle [discussioni](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Facci sapere cosa stai costruendo con cmux
|
||||
|
||||
## Comunità
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Edizione Fondatore
|
||||
|
||||
cmux è gratuito, open source, e lo sarà sempre. Se vuoi supportare lo sviluppo e ottenere accesso anticipato a ciò che arriverà:
|
||||
|
||||
**[Ottieni l'Edizione Fondatore](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Richieste di funzionalità e correzioni di bug prioritarie**
|
||||
- **Accesso anticipato: cmux AI che ti dà contesto su ogni workspace, scheda e pannello**
|
||||
- **Accesso anticipato: app iOS con terminali sincronizzati tra desktop e telefono**
|
||||
- **Accesso anticipato: VM cloud**
|
||||
- **Accesso anticipato: Modalità vocale**
|
||||
- **Il mio iMessage/WhatsApp personale**
|
||||
|
||||
## Licenza
|
||||
|
||||
Questo progetto è distribuito sotto la GNU Affero General Public License v3.0 o successiva (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
142
README.ja.md
|
|
@ -1,9 +1,5 @@
|
|||
> この翻訳は Claude によって生成されました。改善の提案がある場合は、PR を作成してください。
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | 日本語 | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">AIコーディングエージェント向けの縦タブと通知機能を備えたGhosttyベースのmacOSターミナル</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmuxスクリーンショット" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | 日本語 | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmuxスクリーンショット" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ デモ動画</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## 機能
|
||||
|
||||
- **縦タブ** — サイドバーにgitブランチ、作業ディレクトリ、リッスン中のポート、最新の通知テキストを表示
|
||||
- **通知リング** — AIエージェント(Claude Code、OpenCode)があなたの注意を必要とするとき、ペインに青いリングが表示され、タブが点灯
|
||||
- **通知パネル** — 保留中のすべての通知を一か所で確認、最新の未読にジャンプ
|
||||
- **分割ペイン** — 水平・垂直分割
|
||||
- **アプリ内ブラウザ** — [agent-browser](https://github.com/vercel-labs/agent-browser)から移植されたスクリプタブルなAPIで、ターミナルの横にブラウザを分割表示
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>通知リング</h3>
|
||||
コーディングエージェントがあなたの注意を必要とするとき、ペインに青いリングが表示され、タブが点灯します
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="通知リング" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>通知パネル</h3>
|
||||
保留中のすべての通知を一か所で確認、最新の未読にジャンプ
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="サイドバー通知バッジ" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>アプリ内ブラウザ</h3>
|
||||
<a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>から移植されたスクリプタブルなAPIで、ターミナルの横にブラウザを分割表示
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="内蔵ブラウザ" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>縦タブ + 横タブ</h3>
|
||||
サイドバーにgitブランチ、リンクされたPRのステータス/番号、作業ディレクトリ、リッスン中のポート、最新の通知テキストを表示。水平・垂直に分割可能。
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="縦タブと分割ペイン" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **スクリプタブル** — CLIとsocket APIでワークスペースの作成、ペインの分割、キーストロークの送信、ブラウザの自動化が可能
|
||||
- **ネイティブmacOSアプリ** — SwiftとAppKitで構築、Electronではありません。高速起動、低メモリ消費。
|
||||
- **Ghostty互換** — 既存の`~/.config/ghostty/config`からテーマ、フォント、カラーを読み込み
|
||||
|
|
@ -60,12 +103,26 @@ brew upgrade --cask cmux
|
|||
|
||||
いくつかのコーディングオーケストレーターを試しましたが、そのほとんどがElectron/Tauriアプリで、パフォーマンスが気になりました。また、GUIオーケストレーターはそのワークフローに縛られるため、単純にターミナルのほうが好みです。そこで、cmuxをSwift/AppKitのネイティブmacOSアプリとして構築しました。ターミナルレンダリングにはlibghosttyを使用し、テーマ、フォント、カラーは既存のGhostty設定を読み込みます。
|
||||
|
||||
主な追加機能はサイドバーと通知システムです。サイドバーには、各ワークスペースのgitブランチ、作業ディレクトリ、リッスン中のポート、最新の通知テキストを表示する縦タブがあります。通知システムはターミナルシーケンス(OSC 9/99/777)を検出し、Claude Code、OpenCodeなどのエージェントフックに接続できるCLI(`cmux notify`)を備えています。エージェントが待機中のとき、そのペインに青いリングが表示され、サイドバーのタブが点灯するので、分割やタブをまたいでどれが私を必要としているかがわかります。Cmd+Shift+Uで最新の未読にジャンプします。
|
||||
主な追加機能はサイドバーと通知システムです。サイドバーには、各ワークスペースのgitブランチ、リンクされたPRのステータス/番号、作業ディレクトリ、リッスン中のポート、最新の通知テキストを表示する縦タブがあります。通知システムはターミナルシーケンス(OSC 9/99/777)を検出し、Claude Code、OpenCodeなどのエージェントフックに接続できるCLI(`cmux notify`)を備えています。エージェントが待機中のとき、そのペインに青いリングが表示され、サイドバーのタブが点灯するので、分割やタブをまたいでどれが私を必要としているかがわかります。Cmd+Shift+Uで最新の未読にジャンプします。
|
||||
|
||||
アプリ内ブラウザには[agent-browser](https://github.com/vercel-labs/agent-browser)から移植されたスクリプタブルなAPIがあります。エージェントはアクセシビリティツリーのスナップショットを取得し、要素参照を取得し、クリック、フォーム入力、JSの評価が可能です。ターミナルの横にブラウザペインを分割し、Claude Codeに開発サーバーと直接やり取りさせることができます。
|
||||
|
||||
すべてがCLIとsocket APIを通じてスクリプタブルです — ワークスペース/タブの作成、ペインの分割、キーストロークの送信、ブラウザでのURL表示。
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmuxは開発者のツールの使い方を規定しません。ターミナルとブラウザにCLIがあり、あとはあなた次第です。
|
||||
|
||||
cmuxはソリューションではなくプリミティブです。ターミナル、ブラウザ、通知、ワークスペース、分割、タブ、そしてそのすべてを制御するCLIを提供します。cmuxはコーディングエージェントの使い方を強制しません。プリミティブで何を構築するかはあなた次第です。
|
||||
|
||||
優れた開発者は常に自分のツールを構築してきました。エージェントとの最適な作業方法はまだ誰も見つけていませんし、クローズドな製品を作っているチームも見つけていません。自分のコードベースに最も近い開発者が最初に見つけるでしょう。
|
||||
|
||||
100万人の開発者にコンポーザブルなプリミティブを与えれば、どんなプロダクトチームがトップダウンで設計するよりも速く、最も効率的なワークフローを集合的に見つけ出すでしょう。
|
||||
|
||||
## ドキュメント
|
||||
|
||||
cmuxの設定方法の詳細は、[ドキュメントをご覧ください](https://cmux.dev/docs/getting-started?utm_source=readme)。
|
||||
|
||||
## キーボードショートカット
|
||||
|
||||
### ワークスペース
|
||||
|
|
@ -78,6 +135,7 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | 次のワークスペース |
|
||||
| ⌃ ⌘ [ | 前のワークスペース |
|
||||
| ⌘ ⇧ W | ワークスペースを閉じる |
|
||||
| ⌘ ⇧ R | ワークスペースの名前を変更 |
|
||||
| ⌘ B | サイドバーの表示切替 |
|
||||
|
||||
### サーフェス
|
||||
|
|
@ -104,6 +162,8 @@ brew upgrade --cask cmux
|
|||
|
||||
### ブラウザ
|
||||
|
||||
ブラウザの開発者ツールのショートカットはSafariのデフォルトに従い、`設定 → キーボードショートカット`でカスタマイズできます。
|
||||
|
||||
| ショートカット | アクション |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | 分割でブラウザを開く |
|
||||
|
|
@ -111,7 +171,8 @@ brew upgrade --cask cmux
|
|||
| ⌘ [ | 戻る |
|
||||
| ⌘ ] | 進む |
|
||||
| ⌘ R | ページを再読み込み |
|
||||
| ⌥ ⌘ I | 開発者ツールを開く |
|
||||
| ⌥ ⌘ I | 開発者ツールの表示切替(Safariデフォルト) |
|
||||
| ⌥ ⌘ C | JavaScriptコンソールを表示(Safariデフォルト) |
|
||||
|
||||
### 通知
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | 設定を再読み込み |
|
||||
| ⌘ Q | 終了 |
|
||||
|
||||
## ナイトリービルド
|
||||
|
||||
[cmux NIGHTLYをダウンロード](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLYは独自のバンドルIDを持つ別のアプリなので、安定版と並行して実行できます。最新の`main`コミットから自動的にビルドされ、独自のSparkleフィード経由で自動更新されます。
|
||||
|
||||
## セッション復元(現在の動作)
|
||||
|
||||
再起動時、cmuxは現在アプリのレイアウトとメタデータのみを復元します:
|
||||
- ウィンドウ/ワークスペース/ペインのレイアウト
|
||||
- 作業ディレクトリ
|
||||
- ターミナルのスクロールバック(ベストエフォート)
|
||||
- ブラウザのURLとナビゲーション履歴
|
||||
|
||||
cmuxはターミナルアプリ内のライブプロセスの状態を復元**しません**。例えば、アクティブなClaude Code/tmux/vimセッションは再起動後にまだ再開されません。
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## コントリビューション
|
||||
|
||||
参加方法:
|
||||
|
||||
- Xでフォロー:[@manaflowai](https://x.com/manaflowai)、[@lawrencecchen](https://x.com/lawrencecchen)、[@austinywang](https://x.com/austinywang)
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)で会話に参加
|
||||
- [GitHubのIssues](https://github.com/manaflow-ai/cmux/issues)や[ディスカッション](https://github.com/manaflow-ai/cmux/discussions)に参加
|
||||
- cmuxで何を構築しているか教えてください
|
||||
|
||||
## コミュニティ
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmuxは無料でオープンソースであり、今後もそうあり続けます。開発をサポートし、次に来る機能への早期アクセスを得たい方へ:
|
||||
|
||||
**[Founder's Editionを入手](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **機能リクエスト/バグ修正の優先対応**
|
||||
- **早期アクセス:すべてのワークスペース、タブ、パネルのコンテキストを提供するcmux AI**
|
||||
- **早期アクセス:デスクトップと携帯電話間でターミナルを同期するiOSアプリ**
|
||||
- **早期アクセス:クラウドVM**
|
||||
- **早期アクセス:ボイスモード**
|
||||
- **私の個人的なiMessage/WhatsApp**
|
||||
|
||||
## ライセンス
|
||||
|
||||
このプロジェクトはGNU Affero General Public License v3.0以降(`AGPL-3.0-or-later`)の下でライセンスされています。
|
||||
|
|
|
|||
274
README.km.md
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
> ការបកប្រែនេះត្រូវបានបង្កើតដោយ Claude។ ប្រសិនបើអ្នកមានការកែលម្អ សូមបង្កើត PR។
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Terminal សម្រាប់ macOS ផ្អែកលើ Ghostty ដែលមាន tab បញ្ឈរ និងការជូនដំណឹងសម្រាប់ AI coding agents</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg">
|
||||
<img src="./docs/assets/macos-badge.png" alt="Download cmux for macOS" width="180" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | ភាសាខ្មែរ
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux screenshot" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ វីដេអូបង្ហាញពីដំណើរការ (Demo)</a> · <a href="https://cmux.dev/blog/zen-of-cmux">ទស្សនវិជ្ជារបស់ cmux (The Zen of cmux)</a>
|
||||
</p>
|
||||
|
||||
## លក្ខណៈពិសេសនានា (Features)
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>រង្វង់ជូនដំណឹង (Notification rings)</h3>
|
||||
ផ្ទាំង (Panes) នឹងមានរង្វង់ពណ៌ខៀវ ហើយ tabs នឹងភ្លឺឡើង នៅពេល coding agents ត្រូវការការយកចិត្តទុកដាក់របស់អ្នក
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Notification rings" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>ផ្ទាំងជូនដំណឹង (Notification panel)</h3>
|
||||
មើលការជូនដំណឹងដែលកំពុងរង់ចាំទាំងអស់នៅកន្លែងតែមួយ លោតទៅកាន់សារមិនទាន់អានថ្មីបំផុត
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Sidebar notification badge" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>កម្មវិធីរុករកក្នុងកម្មវិធី (In-app browser)</h3>
|
||||
បំបែកកម្មវិធីរុករកនៅក្បែរ terminal របស់អ្នកជាមួយ scriptable API ដែលបានយកចេញពី <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Built-in browser" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Tab បញ្ឈរ + ផ្ដេក (Vertical + horizontal tabs)</h3>
|
||||
របារចំហៀងបង្ហាញ git branch, ស្ថានភាព/លេខ PR, ថតការងារ, port ដែលកំពុងស្តាប់ និងអត្ថបទជូនដំណឹងចុងក្រោយ។ បំបែកទាំងផ្ដេក និងបញ្ឈរ។
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Vertical tabs and split panes" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
* **អាចសរសេរ Script បាន (Scriptable)** — CLI និង socket API ដើម្បីបង្កើត workspaces, បំបែក panes, បញ្ជូន keystrokes, និងធ្វើស្វ័យប្រវត្តិកម្មកម្មវិធីរុករក (browser)
|
||||
* **កម្មវិធីដើមរបស់ macOS (Native macOS app)** — បង្កើតឡើងដោយប្រើ Swift និង AppKit មិនមែន Electron ទេ។ ចាប់ផ្តើមលឿន, ស៊ីមេម៉ូរី (memory) តិច។
|
||||
* **ត្រូវគ្នាជាមួយ Ghostty (Ghostty compatible)** — អានការកំណត់ `~/.config/ghostty/config` ដែលអ្នកមានស្រាប់សម្រាប់ theme, font, និងពណ៌
|
||||
* **បង្កើនល្បឿនដោយ GPU (GPU-accelerated)** — ដំណើរការដោយ libghostty ដើម្បីការបង្ហាញរូបភាពរលូនល្អ (smooth rendering)
|
||||
|
||||
## ការដំឡើង (Install)
|
||||
|
||||
### DMG (ត្រូវបានណែនាំ)
|
||||
|
||||
<a href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg">
|
||||
<img src="./docs/assets/macos-badge.png" alt="ទាញយក cmux សម្រាប់ macOS" width="180" />
|
||||
</a>
|
||||
|
||||
បើកឯកសារ `.dmg` ហើយអូស cmux បញ្ចូលទៅក្នុងថត Applications របស់អ្នក។ cmux ធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិតាមរយៈ Sparkle ដូច្នេះអ្នកគ្រាន់តែទាញយកវាតែម្តងគត់។
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
brew tap manaflow-ai/cmux
|
||||
brew install --cask cmux
|
||||
```
|
||||
|
||||
ដើម្បីធ្វើបច្ចុប្បន្នភាពនៅពេលក្រោយ៖
|
||||
|
||||
```bash
|
||||
brew upgrade --cask cmux
|
||||
```
|
||||
|
||||
នៅពេលបើកដំណើរការជាលើកដំបូង macOS អាចនឹងសុំឱ្យអ្នកបញ្ជាក់ការបើកកម្មវិធីពីអ្នកអភិវឌ្ឍន៍ដែលបានកំណត់អត្តសញ្ញាណ។ ចុច **Open** ដើម្បីបន្ត។
|
||||
|
||||
## ហេតុអ្វីត្រូវជ្រើសរើស cmux?
|
||||
|
||||
ខ្ញុំបើកដំណើរការ Claude Code និង Codex ច្រើនក្នុងពេលតែមួយ។ ខ្ញុំធ្លាប់ប្រើ Ghostty ជាមួយ split panes ជាច្រើន ហើយពឹងផ្អែកលើការជូនដំណឹងដើមរបស់ macOS ដើម្បីដឹងថានៅពេលណាដែល agent ត្រូវការខ្ញុំ។ ប៉ុន្តែខ្លឹមសារជូនដំណឹងរបស់ Claude Code តែងតែសរសេរត្រឹម "Claude វាកំពុងរង់ចាំការបញ្ចូលព័ត៌មានពីអ្នក" ដោយគ្មានបរិបទ (context) ហើយនៅពេលដែលបើក tab ច្រើនពេក ខ្ញុំសឹងតែមិនអាចអានចំណងជើងបានទៀតផង។
|
||||
|
||||
ខ្ញុំបានសាកល្បងប្រើ coding orchestrators មួយចំនួន ប៉ុន្តែភាគច្រើននៃពួកវាគឺជាកម្មវិធី Electron/Tauri ហើយដំណើរការ (performance) របស់វារំខានដល់ខ្ញុំ។ ម្យ៉ាងទៀត ខ្ញុំចូលចិត្តប្រើ terminal ជាង ពីព្រោះ GUI orchestrators តែងតែកំណត់លំហូរការងារ (workflow) របស់អ្នក។ ដូច្នេះ ខ្ញុំបានបង្កើត cmux ជាកម្មវិធីដើមសម្រាប់ macOS នៅក្នុង Swift/AppKit។ វាប្រើប្រាស់ libghostty សម្រាប់ការបង្ហាញ terminal និងអាន config របស់ Ghostty ដែលអ្នកមានស្រាប់សម្រាប់ themes, fonts និងពណ៌។
|
||||
|
||||
ការបន្ថែមដ៏សំខាន់គឺរបារចំហៀង (sidebar) និងប្រព័ន្ធជូនដំណឹង។ របារចំហៀងមាន tab បញ្ឈរដែលបង្ហាញពី git branch, ស្ថានភាព/លេខ PR, ថតការងារ, port ដែលកំពុងស្តាប់ និងអត្ថបទជូនដំណឹងចុងក្រោយសម្រាប់ workspace នីមួយៗ។ ប្រព័ន្ធជូនដំណឹងចាប់យក terminal sequences (OSC 9/99/777) និងមាន CLI (`cmux notify`) ដែលអ្នកអាចភ្ជាប់ទៅកាន់ agent hooks សម្រាប់ Claude Code, OpenCode ជាដើម។ នៅពេល agent កំពុងរង់ចាំ ផ្ទាំង (pane) របស់វានឹងមានរង្វង់ពណ៌ខៀវ ហើយ tab នឹងភ្លឺឡើងនៅលើរបារចំហៀង ដូច្នេះខ្ញុំអាចដឹងថាមួយណាដែលត្រូវការខ្ញុំនៅទូទាំង splits និង tabs ទាំងអស់។ ចុច Cmd+Shift+U ដើម្បីលោតទៅកាន់សារមិនទាន់អានថ្មីបំផុត។
|
||||
|
||||
កម្មវិធីរុករកក្នុងកម្មវិធី (in-app browser) មាន scriptable API ដែលបានយកចេញពី [agent-browser](https://github.com/vercel-labs/agent-browser)។ Agents អាចថតចម្លង (snapshot) ដើមឈើភាពងាយស្រួល (accessibility tree), យក element refs, ចុច (click), បំពេញទម្រង់បែបបទ (fill forms) និងវាយតម្លៃ (evaluate) JS។ អ្នកអាចបំបែកផ្ទាំងកម្មវិធីរុករកនៅក្បែរ terminal របស់អ្នក ហើយឱ្យ Claude Code ប្រាស្រ័យទាក់ទងដោយផ្ទាល់ជាមួយ dev server របស់អ្នក។
|
||||
|
||||
អ្វីៗទាំងអស់អាចសរសេរ script បានតាមរយៈ CLI និង socket API — បង្កើត workspaces/tabs, បំបែក panes, បញ្ជូន keystrokes, បើក URLs នៅក្នុងកម្មវិធីរុករក។
|
||||
|
||||
## ទស្សនវិជ្ជារបស់ cmux (The Zen of cmux)
|
||||
|
||||
cmux មិនបង្ខំអំពីរបៀបដែលអ្នកអភិវឌ្ឍន៍ប្រើប្រាស់ឧបករណ៍របស់ពួកគេទេ។ វាគឺជា terminal និងកម្មវិធីរុករកដែលមាន CLI ហើយអ្វីៗផ្សេងទៀតគឺអាស្រ័យលើអ្នក។
|
||||
|
||||
cmux គឺជាមូលដ្ឋានគ្រឹះ (primitive) មិនមែនជាដំណោះស្រាយពេញលេញទេ។ វាផ្តល់ឱ្យអ្នកនូវ terminal, កម្មវិធីរុករក, ការជូនដំណឹង, workspaces, splits, tabs និង CLI ដើម្បីគ្រប់គ្រងអ្វីៗទាំងអស់នេះ។ cmux មិនបង្ខំអ្នកឱ្យប្រើវិធីសាស្ត្រណាមួយដែលវាបានកំណត់ទុកមុនក្នុងការប្រើប្រាស់ coding agents នោះទេ។ អ្វីដែលអ្នកបង្កើតជាមួយមូលដ្ឋានគ្រឹះទាំងនេះ គឺជារបស់អ្នក។
|
||||
|
||||
អ្នកអភិវឌ្ឍន៍ដ៏ល្អបំផុតតែងតែបង្កើតឧបករណ៍ដោយខ្លួនឯង។ មិនទាន់មាននរណាម្នាក់រកឃើញវិធីល្អបំផុតក្នុងការធ្វើការជាមួយ agents នៅឡើយទេ ហើយក្រុមដែលបង្កើតផលិតផលបិទជិត (closed products) ក៏ច្បាស់ជាមិនទាន់រកឃើញដូចគ្នា។ អ្នកអភិវឌ្ឍន៍ដែលយល់ច្បាស់ពី codebases របស់ពួកគេ នឹងរកឃើញវាមុនគេ។
|
||||
|
||||
ផ្តល់ឱ្យអ្នកអភិវឌ្ឍន៍មួយលាននាក់នូវមូលដ្ឋានគ្រឹះដែលអាចផ្សំបញ្ចូលគ្នាបាន នោះពួកគេរួមគ្នានឹងស្វែងរកលំហូរការងារដែលមានប្រសិទ្ធភាពបំផុត លឿនជាងក្រុមការងារផលិតផលណាមួយអាចរចនាពីលើចុះក្រោម (top-down) ទៅទៀត។
|
||||
|
||||
## ឯកសារ (Documentation)
|
||||
|
||||
សម្រាប់ព័ត៌មានបន្ថែមអំពីរបៀបកំណត់រចនាសម្ព័ន្ធ cmux, [សូមចូលទៅកាន់ឯកសាររបស់យើង](https://cmux.dev/docs/getting-started?utm_source=readme)។
|
||||
|
||||
## គ្រាប់ចុចផ្លូវកាត់ (Keyboard Shortcuts)
|
||||
|
||||
### តំបន់ការងារ (Workspaces)
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ N | បង្កើត workspace ថ្មី |
|
||||
| ⌘ 1–8 | លោតទៅ workspace ទី 1–8 |
|
||||
| ⌘ 9 | លោតទៅ workspace ចុងក្រោយ |
|
||||
| ⌃ ⌘ ] | workspace បន្ទាប់ |
|
||||
| ⌃ ⌘ [ | workspace មុន |
|
||||
| ⌘ ⇧ W | បិទ workspace |
|
||||
| ⌘ ⇧ R | ប្តូរឈ្មោះ workspace |
|
||||
| ⌘ B | បិទ/បើក របារចំហៀង |
|
||||
|
||||
### ផ្ទៃ (Surfaces)
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ T | បង្កើត surface ថ្មី |
|
||||
| ⌘ ⇧ ] | surface បន្ទាប់ |
|
||||
| ⌘ ⇧ [ | surface មុន |
|
||||
| ⌃ Tab | surface បន្ទាប់ |
|
||||
| ⌃ ⇧ Tab | surface មុន |
|
||||
| ⌃ 1–8 | លោតទៅ surface ទី 1–8 |
|
||||
| ⌃ 9 | លោតទៅ surface ចុងក្រោយ |
|
||||
| ⌘ W | បិទ surface |
|
||||
|
||||
### បំបែកផ្ទាំង (Split Panes)
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ D | បំបែកទៅស្តាំ |
|
||||
| ⌘ ⇧ D | បំបែកចុះក្រោម |
|
||||
| ⌥ ⌘ ← → ↑ ↓ | ផ្ដោតលើ pane តាមទិសដៅ |
|
||||
| ⌘ ⇧ H | បញ្ចេញពន្លឺលើ panel ដែលកំពុងផ្ដោត |
|
||||
|
||||
### កម្មវិធីរុករក (Browser)
|
||||
|
||||
ផ្លូវកាត់ឧបករណ៍អ្នកអភិវឌ្ឍន៍កម្មវិធីរុករក (Browser developer-tool shortcuts) ប្រើតាមលំនាំដើមរបស់ Safari ហើយអាចប្ដូរតាមបំណងបាននៅក្នុង `Settings → Keyboard Shortcuts`។
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ ⇧ L | បើកកម្មវិធីរុករកក្នុងលក្ខណៈបំបែក (split) |
|
||||
| ⌘ L | ផ្ដោតលើរបារអាសយដ្ឋាន |
|
||||
| ⌘ [ | ថយក្រោយ |
|
||||
| ⌘ ] | ទៅមុខ |
|
||||
| ⌘ R | ផ្ទុកទំព័រឡើងវិញ |
|
||||
| ⌥ ⌘ I | បិទ/បើក ឧបករណ៍អ្នកអភិវឌ្ឍន៍ (លំនាំដើម Safari) |
|
||||
| ⌥ ⌘ C | បង្ហាញ JavaScript Console (លំនាំដើម Safari) |
|
||||
|
||||
### ការជូនដំណឹង (Notifications)
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ I | បង្ហាញផ្ទាំងជូនដំណឹង |
|
||||
| ⌘ ⇧ U | លោតទៅសារមិនទាន់អានថ្មីបំផុត |
|
||||
|
||||
### ស្វែងរក (Find)
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ F | ស្វែងរក |
|
||||
| ⌘ G / ⌘ ⇧ G | ស្វែងរកបន្ទាប់ / មុន |
|
||||
| ⌘ ⇧ F | លាក់របារស្វែងរក |
|
||||
| ⌘ E | ប្រើអត្ថបទដែលបានជ្រើសរើសដើម្បីស្វែងរក |
|
||||
|
||||
### Terminal
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ K | សម្អាត scrollback |
|
||||
| ⌘ C | ចម្លង (ជាមួយនឹងការជ្រើសរើស) |
|
||||
| ⌘ V | ដាក់បញ្ចូល (Paste) |
|
||||
| ⌘ + / ⌘ - | បង្កើន / បន្ថយ ទំហំអក្សរ |
|
||||
| ⌘ 0 | កំណត់ទំហំអក្សរឡើងវិញ |
|
||||
|
||||
### ផ្ទាំងវីនដូ (Window)
|
||||
|
||||
| ផ្លូវកាត់ (Shortcut) | សកម្មភាព (Action) |
|
||||
|---|---|
|
||||
| ⌘ ⇧ N | បង្កើតវីនដូថ្មី |
|
||||
| ⌘ , | ការកំណត់ (Settings) |
|
||||
| ⌘ ⇧ , | ផ្ទុកការកំណត់ឡើងវិញ (Reload configuration) |
|
||||
| ⌘ Q | ចាកចេញ |
|
||||
|
||||
## កំណែ Nightly Builds
|
||||
|
||||
[ទាញយក cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY គឺជាកម្មវិធីដាច់ដោយឡែកមួយដែលមាន bundle ID ផ្ទាល់ខ្លួន ដូច្នេះវាអាចដំណើរការទន្ទឹមគ្នាជាមួយនឹងកំណែធម្មតា (stable version)។ វាត្រូវបានបង្កើតឡើងដោយស្វ័យប្រវត្តិពី commit `main` ចុងក្រោយបង្អស់ និងធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិតាមរយៈ Sparkle feed របស់វាផ្ទាល់។
|
||||
|
||||
## ការស្ដារ Session ឡើងវិញ (អាកប្បកិរិយាបច្ចុប្បន្ន)
|
||||
|
||||
នៅពេលបើកឡើងវិញ បច្ចុប្បន្ន cmux នឹងស្ដារតែប្លង់កម្មវិធី និងទិន្នន័យមេតា (metadata) ប៉ុណ្ណោះ៖
|
||||
|
||||
* ប្លង់ Window/workspace/pane
|
||||
* ថតការងារ (Working directories)
|
||||
* Terminal scrollback (ប្រឹងប្រែងឱ្យអស់លទ្ធភាព)
|
||||
* ប្រវត្តិរុករក និង URL របស់កម្មវិធីរុករក
|
||||
|
||||
cmux **មិន** ស្ដារស្ថានភាពដំណើរការផ្ទាល់ (live process state) នៅក្នុងកម្មវិធី terminal ឡើយ។ ឧទាហរណ៍ session របស់ Claude Code/tmux/vim ដែលកំពុងដំណើរការ មិនទាន់អាចបន្តឡើងវិញបានទេបន្ទាប់ពីចាប់ផ្ដើមឡើងវិញ។
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## ការចូលរួមចំណែក (Contributing)
|
||||
|
||||
វិធីក្នុងការចូលរួម៖
|
||||
|
||||
* តាមដានពួកយើងនៅលើ X សម្រាប់ការធ្វើបច្ចុប្បន្នភាពនានា [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), និង [@austinywang](https://x.com/austinywang)
|
||||
* ចូលរួមការសន្ទនានៅលើ [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
* បង្កើត និងចូលរួមក្នុង [GitHub issues](https://github.com/manaflow-ai/cmux/issues) និង [discussions](https://github.com/manaflow-ai/cmux/discussions)
|
||||
* ប្រាប់ពួកយើងអំពីអ្វីដែលអ្នកកំពុងបង្កើតជាមួយ cmux
|
||||
|
||||
## សហគមន៍ (Community)
|
||||
|
||||
* [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
* [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
* [X / Twitter](https://twitter.com/manaflowai)
|
||||
* [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
* [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
* [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## កំណែអ្នកស្ថាបនិក (Founder's Edition)
|
||||
|
||||
cmux គឺឥតគិតថ្លៃ ជាកូដបើកចំហ (open source) និងតែងតែបែបនេះជារៀងរហូត។ ប្រសិនបើអ្នកចង់គាំទ្រដល់ការអភិវឌ្ឍន៍ និងទទួលបានសិទ្ធិប្រើប្រាស់មុខងារថ្មីៗមុនគេ (early access)៖
|
||||
|
||||
[**ទទួលបានកំណែអ្នកស្ថាបនិក (Get Founder's Edition)**](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)
|
||||
|
||||
* **ការស្នើសុំមុខងារ/ការជួសជុលកំហុសត្រូវបានផ្តល់អាទិភាព**
|
||||
* **សិទ្ធិប្រើប្រាស់មុនគេ៖ cmux AI ដែលផ្តល់ឱ្យអ្នកនូវបរិបទ (context) លើរាល់ workspace, tab និង panel**
|
||||
* **សិទ្ធិប្រើប្រាស់មុនគេ៖ កម្មវិធី iOS ដែលមាន terminal ធ្វើសមកាលកម្ម (synced) រវាងកុំព្យូទ័រ និងទូរស័ព្ទ**
|
||||
* **សិទ្ធិប្រើប្រាស់មុនគេ៖ Cloud VMs**
|
||||
* **សិទ្ធិប្រើប្រាស់មុនគេ៖ មុខងារសំឡេង (Voice mode)**
|
||||
* **iMessage/WhatsApp ផ្ទាល់ខ្លួនរបស់ខ្ញុំ**
|
||||
|
||||
## អាជ្ញាប័ណ្ណ (License)
|
||||
|
||||
គម្រោងនេះត្រូវបានផ្តល់អាជ្ញាប័ណ្ណក្រោម GNU Affero General Public License v3.0 ឬក្រោយនេះ (`AGPL-3.0-or-later`)។
|
||||
|
||||
សូមមើលឯកសារ `LICENSE` សម្រាប់អត្ថបទពេញលេញ។
|
||||
180
README.ko.md
|
|
@ -1,9 +1,7 @@
|
|||
> 이 번역은 Claude에 의해 생성되었습니다. 개선 사항이 있으면 PR을 제출해 주세요.
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | 한국어 | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a></p>
|
||||
> 이 문서는 Claude가 번역했어요. 개선할 부분이 있다면 PR을 보내주세요.
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">AI 코딩 에이전트를 위한 세로 탭과 알림 기능을 갖춘 Ghostty 기반 macOS 터미널</p>
|
||||
<p align="center">세로 탭과 알림을 지원하는 AI 코딩 에이전트용 Ghostty 기반 macOS 터미널</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg">
|
||||
|
|
@ -12,22 +10,69 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux 스크린샷" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | 한국어 | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux 스크린샷" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ 데모 영상</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## 기능
|
||||
|
||||
- **세로 탭** — 사이드바에 git 브랜치, 작업 디렉토리, 리스닝 포트, 최신 알림 텍스트 표시
|
||||
- **알림 링** — AI 에이전트(Claude Code, OpenCode)가 사용자의 주의를 필요로 할 때 패널에 파란색 링이 표시되고 탭이 강조됨
|
||||
- **알림 패널** — 모든 대기 중인 알림을 한 곳에서 확인하고, 가장 최근의 읽지 않은 알림으로 바로 이동
|
||||
- **분할 패널** — 수평 및 수직 분할 지원
|
||||
- **내장 브라우저** — [agent-browser](https://github.com/vercel-labs/agent-browser)에서 포팅된 스크립트 가능한 API를 갖춘 브라우저를 터미널 옆에 분할하여 사용
|
||||
- **스크립트 가능** — CLI와 socket API로 워크스페이스 생성, 패널 분할, 키 입력 전송, 브라우저 자동화 가능
|
||||
- **네이티브 macOS 앱** — Swift와 AppKit으로 구축, Electron이 아닙니다. 빠른 시작, 낮은 메모리 사용량.
|
||||
- **Ghostty 호환** — 기존 `~/.config/ghostty/config`에서 테마, 글꼴, 색상 설정을 읽어옴
|
||||
- **GPU 가속** — libghostty로 구동되어 부드러운 렌더링 제공
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>알림 링</h3>
|
||||
코딩 에이전트가 입력을 기다리면 패널에 파란색 링이 뜨고 탭이 강조돼요
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="알림 링" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>알림 패널</h3>
|
||||
대기 중인 알림을 한곳에서 확인하고, 가장 최근 읽지 않은 알림으로 바로 이동할 수 있어요
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="사이드바 알림 배지" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>내장 브라우저</h3>
|
||||
<a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>에서 포팅된 스크립팅 API를 갖춘 브라우저를 터미널 옆에 띄울 수 있어요
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="내장 브라우저" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>세로 + 가로 탭</h3>
|
||||
사이드바에서 git 브랜치, 연결된 PR 상태/번호, 작업 디렉토리, 수신 포트, 최근 알림 텍스트를 한눈에 볼 수 있어요. 수평·수직 분할을 지원해요.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="세로 탭과 분할 패널" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 설치
|
||||
- **스크립팅** — CLI와 socket API로 워크스페이스 생성, 패널 분할, 키 입력 전송, 브라우저 자동화가 가능해요
|
||||
- **네이티브 macOS 앱** — Electron이 아닌 Swift와 AppKit으로 만들었어요. 빠르게 실행되고 메모리도 적게 써요.
|
||||
- **Ghostty 호환** — 기존 `~/.config/ghostty/config`에서 테마, 글꼴, 색상 설정을 그대로 읽어와요
|
||||
- **GPU 가속** — libghostty 기반이라 렌더링이 부드러워요
|
||||
|
||||
## 설치하기
|
||||
|
||||
### DMG (권장)
|
||||
|
||||
|
|
@ -35,7 +80,7 @@
|
|||
<img src="./docs/assets/macos-badge.png" alt="macOS용 cmux 다운로드" width="180" />
|
||||
</a>
|
||||
|
||||
`.dmg` 파일을 열고 cmux를 응용 프로그램 폴더로 드래그하세요. cmux는 Sparkle을 통해 자동 업데이트되므로, 한 번만 다운로드하면 됩니다.
|
||||
`.dmg` 파일을 열고 cmux를 응용 프로그램 폴더로 드래그하면 돼요. Sparkle을 통해 자동 업데이트되니 한 번만 다운로드하면 돼요.
|
||||
|
||||
### Homebrew
|
||||
|
||||
|
|
@ -44,25 +89,39 @@ brew tap manaflow-ai/cmux
|
|||
brew install --cask cmux
|
||||
```
|
||||
|
||||
나중에 업데이트하려면:
|
||||
나중에 업데이트하려면 아래 명령어를 실행해주세요:
|
||||
|
||||
```bash
|
||||
brew upgrade --cask cmux
|
||||
```
|
||||
|
||||
처음 실행 시, macOS가 확인된 개발자의 앱을 여는 것을 확인하도록 요청할 수 있습니다. **열기**를 클릭하여 계속 진행하세요.
|
||||
처음 실행할 때 macOS에서 개발자 확인 팝업이 뜰 수 있어요. **열기**를 클릭하면 돼요.
|
||||
|
||||
## 왜 cmux를 만들었나요?
|
||||
|
||||
저는 Claude Code와 Codex 세션을 대량으로 병렬 실행합니다. 이전에는 Ghostty에서 분할 패널을 여러 개 열어놓고, 에이전트가 저를 필요로 할 때 macOS 기본 알림에 의존했습니다. 하지만 Claude Code의 알림 내용은 항상 "Claude is waiting for your input"이라는 맥락 없는 동일한 메시지뿐이었고, 탭이 많아지면 제목조차 읽을 수 없었습니다.
|
||||
저는 Claude Code와 Codex 세션을 여러 개 동시에 돌려요. 예전에는 Ghostty에서 분할 패널을 여러 개 열어놓고, 에이전트가 입력을 기다릴 때 macOS 기본 알림에 의존했어요. 그런데 Claude Code 알림은 항상 "Claude is waiting for your input"이라는 아무 맥락 없이 똑같은 메시지뿐이었고, 탭이 많아지면 제목조차 읽을 수가 없었어요.
|
||||
|
||||
몇 가지 코딩 오케스트레이터를 시도해봤지만, 대부분 Electron/Tauri 앱이어서 성능이 마음에 들지 않았습니다. 또한 GUI 오케스트레이터는 특정 워크플로우에 갇히게 되므로 터미널을 더 선호합니다. 그래서 Swift/AppKit으로 네이티브 macOS 앱인 cmux를 만들었습니다. 터미널 렌더링에 libghostty를 사용하고, 기존 Ghostty 설정에서 테마, 글꼴, 색상을 읽어옵니다.
|
||||
여러 코딩 오케스트레이터를 써봤는데, 대부분 Electron/Tauri 앱이라 성능이 별로였어요. GUI 오케스트레이터는 특정 워크플로우에 갇히게 돼서 터미널이 더 낫다고 생각했고요. 그래서 Swift/AppKit으로 네이티브 macOS 앱인 cmux를 직접 만들었어요. 터미널 렌더링에는 libghostty를 쓰고, 기존 Ghostty 설정에서 테마, 글꼴, 색상을 그대로 가져와요.
|
||||
|
||||
주요 추가 기능은 사이드바와 알림 시스템입니다. 사이드바에는 각 워크스페이스의 git 브랜치, 작업 디렉토리, 리스닝 포트, 최신 알림 텍스트를 보여주는 세로 탭이 있습니다. 알림 시스템은 터미널 시퀀스(OSC 9/99/777)를 감지하고, Claude Code, OpenCode 등의 에이전트 훅에 연결할 수 있는 CLI(`cmux notify`)를 제공합니다. 에이전트가 대기 중일 때 해당 패널에 파란색 링이 표시되고 사이드바에서 탭이 강조되어, 여러 분할 패널과 탭에서 어떤 것이 저를 필요로 하는지 한눈에 알 수 있습니다. ⌘⇧U로 가장 최근의 읽지 않은 알림으로 이동합니다.
|
||||
핵심은 사이드바와 알림 시스템이에요. 사이드바에는 각 워크스페이스의 git 브랜치, 연결된 PR 상태/번호, 작업 디렉토리, 수신 포트, 최근 알림 텍스트를 보여주는 세로 탭이 있어요. 알림 시스템은 터미널 시퀀스(OSC 9/99/777)를 감지하고, Claude Code나 OpenCode 같은 에이전트 훅에 연결할 수 있는 CLI(`cmux notify`)를 제공해요. 에이전트가 대기 중이면 해당 패널에 파란색 링이 뜨고 사이드바 탭이 강조되니까, 여러 패널과 탭 중에서 어디서 입력을 기다리는지 바로 알 수 있어요. ⌘⇧U를 누르면 가장 최근 읽지 않은 알림으로 이동해요.
|
||||
|
||||
내장 브라우저는 [agent-browser](https://github.com/vercel-labs/agent-browser)에서 포팅된 스크립트 가능한 API를 갖추고 있습니다. 에이전트가 접근성 트리 스냅샷을 가져오고, 요소 참조를 얻고, 클릭하고, 양식을 작성하고, JS를 실행할 수 있습니다. 터미널 옆에 브라우저 패널을 분할하여 Claude Code가 개발 서버와 직접 상호작용하도록 할 수 있습니다.
|
||||
내장 브라우저는 [agent-browser](https://github.com/vercel-labs/agent-browser)에서 포팅한 스크립팅 API를 제공해요. 에이전트가 접근성 트리 스냅샷을 가져오고, 요소를 참조·클릭하고, 양식을 채우고, JS를 실행할 수 있어요. 터미널 옆에 브라우저 패널을 띄워서 Claude Code가 개발 서버와 직접 상호작용하게 할 수 있어요.
|
||||
|
||||
모든 것은 CLI와 socket API를 통해 스크립트 가능합니다 — 워크스페이스/탭 생성, 패널 분할, 키 입력 전송, 브라우저에서 URL 열기.
|
||||
CLI와 socket API로 모든 걸 자동화할 수 있어요 — 워크스페이스/탭 생성, 패널 분할, 키 입력 전송, 브라우저에서 URL 열기까지요.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux는 개발자가 도구를 어떻게 사용해야 하는지 규정하지 않아요. 터미널과 브라우저에 CLI가 있고, 나머지는 여러분의 몫이에요.
|
||||
|
||||
cmux는 솔루션이 아니라 프리미티브예요. 터미널, 브라우저, 알림, 워크스페이스, 분할, 탭, 그리고 이 모든 것을 제어하는 CLI를 제공해요. cmux는 코딩 에이전트를 특정 방식으로 사용하도록 강요하지 않아요. 프리미티브로 무엇을 만들지는 여러분에게 달려 있어요.
|
||||
|
||||
최고의 개발자들은 항상 자신만의 도구를 만들어왔어요. 에이전트와 함께 일하는 최적의 방법은 아직 아무도 찾지 못했고, 폐쇄적인 제품을 만드는 팀들도 마찬가지예요. 자신의 코드베이스에 가장 가까운 개발자가 먼저 답을 찾을 거예요.
|
||||
|
||||
100만 명의 개발자에게 조합 가능한 프리미티브를 주면, 어떤 프로덕트 팀이 위에서 설계하는 것보다 빠르게 가장 효율적인 워크플로우를 함께 찾아낼 거예요.
|
||||
|
||||
## 문서
|
||||
|
||||
cmux 설정 방법에 대한 자세한 내용은 [문서를 확인해주세요](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## 키보드 단축키
|
||||
|
||||
|
|
@ -76,6 +135,7 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | 다음 워크스페이스 |
|
||||
| ⌃ ⌘ [ | 이전 워크스페이스 |
|
||||
| ⌘ ⇧ W | 워크스페이스 닫기 |
|
||||
| ⌘ ⇧ R | 워크스페이스 이름 변경 |
|
||||
| ⌘ B | 사이드바 토글 |
|
||||
|
||||
### 서피스
|
||||
|
|
@ -98,25 +158,28 @@ brew upgrade --cask cmux
|
|||
| ⌘ D | 오른쪽으로 분할 |
|
||||
| ⌘ ⇧ D | 아래로 분할 |
|
||||
| ⌥ ⌘ ← → ↑ ↓ | 방향키로 패널 포커스 이동 |
|
||||
| ⌘ ⇧ H | 포커스된 패널 깜빡임 |
|
||||
| ⌘ ⇧ H | 현재 패널 깜빡임 |
|
||||
|
||||
### 브라우저
|
||||
|
||||
브라우저 개발자 도구 단축키는 Safari 기본값을 따르며, `설정 → 키보드 단축키`에서 변경할 수 있어요.
|
||||
|
||||
| 단축키 | 동작 |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | 분할에서 브라우저 열기 |
|
||||
| ⌘ ⇧ L | 분할 패널로 브라우저 열기 |
|
||||
| ⌘ L | 주소창 포커스 |
|
||||
| ⌘ [ | 뒤로 |
|
||||
| ⌘ ] | 앞으로 |
|
||||
| ⌘ R | 페이지 새로고침 |
|
||||
| ⌥ ⌘ I | 개발자 도구 열기 |
|
||||
| ⌥ ⌘ I | 개발자 도구 열기 (Safari 기본값) |
|
||||
| ⌥ ⌘ C | JavaScript 콘솔 표시 (Safari 기본값) |
|
||||
|
||||
### 알림
|
||||
|
||||
| 단축키 | 동작 |
|
||||
|----------|--------|
|
||||
| ⌘ I | 알림 패널 표시 |
|
||||
| ⌘ ⇧ U | 최신 읽지 않은 알림으로 이동 |
|
||||
| ⌘ ⇧ U | 최근 읽지 않은 알림으로 이동 |
|
||||
|
||||
### 찾기
|
||||
|
||||
|
|
@ -125,7 +188,7 @@ brew upgrade --cask cmux
|
|||
| ⌘ F | 찾기 |
|
||||
| ⌘ G / ⌘ ⇧ G | 다음 찾기 / 이전 찾기 |
|
||||
| ⌘ ⇧ F | 찾기 바 숨기기 |
|
||||
| ⌘ E | 선택 영역으로 찾기 |
|
||||
| ⌘ E | 선택한 텍스트로 찾기 |
|
||||
|
||||
### 터미널
|
||||
|
||||
|
|
@ -146,8 +209,65 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | 설정 다시 불러오기 |
|
||||
| ⌘ Q | 종료 |
|
||||
|
||||
## 나이틀리 빌드
|
||||
|
||||
[cmux NIGHTLY 다운로드](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY는 자체 번들 ID를 가진 별도의 앱이라 안정 버전과 함께 실행할 수 있어요. 최신 `main` 커밋에서 자동으로 빌드되고, 자체 Sparkle 피드를 통해 자동 업데이트돼요.
|
||||
|
||||
## 세션 복원 (현재 동작)
|
||||
|
||||
재실행 시 cmux는 현재 앱 레이아웃과 메타데이터만 복원해요:
|
||||
- 창/워크스페이스/패널 레이아웃
|
||||
- 작업 디렉토리
|
||||
- 터미널 스크롤백 (최선 노력)
|
||||
- 브라우저 URL 및 탐색 기록
|
||||
|
||||
cmux는 터미널 앱 내부의 라이브 프로세스 상태를 복원하지 **않아요**. 예를 들어 활성 Claude Code/tmux/vim 세션은 재시작 후 아직 복원되지 않아요.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 기여하기
|
||||
|
||||
참여 방법:
|
||||
|
||||
- X에서 팔로우해주세요: [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), [@austinywang](https://x.com/austinywang)
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)에서 대화에 참여해주세요
|
||||
- [GitHub Issues](https://github.com/manaflow-ai/cmux/issues)와 [토론](https://github.com/manaflow-ai/cmux/discussions)에 참여해주세요
|
||||
- cmux로 무엇을 만들고 있는지 알려주세요
|
||||
|
||||
## 커뮤니티
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux는 무료이고 오픈 소스이며, 앞으로도 그럴 거예요. 개발을 지원하고 다음에 나올 기능에 먼저 접근하고 싶다면:
|
||||
|
||||
**[Founder's Edition 구매하기](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **기능 요청/버그 수정 우선 처리**
|
||||
- **얼리 액세스: 모든 워크스페이스, 탭, 패널의 컨텍스트를 제공하는 cmux AI**
|
||||
- **얼리 액세스: 데스크톱과 휴대폰 간 터미널을 동기화하는 iOS 앱**
|
||||
- **얼리 액세스: 클라우드 VM**
|
||||
- **얼리 액세스: 음성 모드**
|
||||
- **저의 개인 iMessage/WhatsApp**
|
||||
|
||||
## 라이선스
|
||||
|
||||
이 프로젝트는 GNU Affero 일반 공중 사용 허가서 v3.0 이상(`AGPL-3.0-or-later`)에 따라 라이선스가 부여됩니다.
|
||||
이 프로젝트는 GNU Affero General Public License v3.0 이상(`AGPL-3.0-or-later`)으로 배포돼요.
|
||||
|
||||
전체 라이선스 텍스트는 `LICENSE` 파일을 참조하세요.
|
||||
자세한 내용은 `LICENSE` 파일을 확인해주세요.
|
||||
|
|
|
|||
71
README.md
|
|
@ -8,7 +8,12 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
English | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
English | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
|
@ -16,7 +21,7 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demo video</a>
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demo video</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
|
@ -52,7 +57,7 @@ Split a browser alongside your terminal with a scriptable API ported from <a hre
|
|||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Vertical + horizontal tabs</h3>
|
||||
Sidebar shows git branch, working directory, listening ports, and latest notification text. Split horizontally and vertically.
|
||||
Sidebar shows git branch, linked PR status/number, working directory, listening ports, and latest notification text. Split horizontally and vertically.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Vertical tabs and split panes" width="100%" />
|
||||
|
|
@ -96,12 +101,26 @@ I run a lot of Claude Code and Codex sessions in parallel. I was using Ghostty w
|
|||
|
||||
I tried a few coding orchestrators but most of them were Electron/Tauri apps and the performance bugged me. I also just prefer the terminal since GUI orchestrators lock you into their workflow. So I built cmux as a native macOS app in Swift/AppKit. It uses libghostty for terminal rendering and reads your existing Ghostty config for themes, fonts, and colors.
|
||||
|
||||
The main additions are the sidebar and notification system. The sidebar has vertical tabs that show git branch, working directory, listening ports, and the latest notification text for each workspace. The notification system picks up terminal sequences (OSC 9/99/777) and has a CLI (`cmux notify`) you can wire into agent hooks for Claude Code, OpenCode, etc. When an agent is waiting, its pane gets a blue ring and the tab lights up in the sidebar, so I can tell which one needs me across splits and tabs. Cmd+Shift+U jumps to the most recent unread.
|
||||
The main additions are the sidebar and notification system. The sidebar has vertical tabs that show git branch, linked PR status/number, working directory, listening ports, and the latest notification text for each workspace. The notification system picks up terminal sequences (OSC 9/99/777) and has a CLI (`cmux notify`) you can wire into agent hooks for Claude Code, OpenCode, etc. When an agent is waiting, its pane gets a blue ring and the tab lights up in the sidebar, so I can tell which one needs me across splits and tabs. Cmd+Shift+U jumps to the most recent unread.
|
||||
|
||||
The in-app browser has a scriptable API ported from [agent-browser](https://github.com/vercel-labs/agent-browser). Agents can snapshot the accessibility tree, get element refs, click, fill forms, and evaluate JS. You can split a browser pane next to your terminal and have Claude Code interact with your dev server directly.
|
||||
|
||||
Everything is scriptable through the CLI and socket API — create workspaces/tabs, split panes, send keystrokes, open URLs in the browser.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux is not prescriptive about how developers hold their tools. It's a terminal and browser with a CLI, and the rest is up to you.
|
||||
|
||||
cmux is a primitive, not a solution. It gives you a terminal, a browser, notifications, workspaces, splits, tabs, and a CLI to control all of it. cmux doesn't force you into an opinionated way to use coding agents. What you build with the primitives is yours.
|
||||
|
||||
The best developers have always built their own tools. Nobody has figured out the best way to work with agents yet, and the teams building closed products definitely haven't either. The developers closest to their own codebases will figure it out first.
|
||||
|
||||
Give a million developers composable primitives and they'll collectively find the most efficient workflows faster than any product team could design top-down.
|
||||
|
||||
## Documentation
|
||||
|
||||
For more info on how to configure cmux, [head over to our docs](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
### Workspaces
|
||||
|
|
@ -114,6 +133,7 @@ Everything is scriptable through the CLI and socket API — create workspaces/ta
|
|||
| ⌃ ⌘ ] | Next workspace |
|
||||
| ⌃ ⌘ [ | Previous workspace |
|
||||
| ⌘ ⇧ W | Close workspace |
|
||||
| ⌘ ⇧ R | Rename workspace |
|
||||
| ⌘ B | Toggle sidebar |
|
||||
|
||||
### Surfaces
|
||||
|
|
@ -193,6 +213,35 @@ Browser developer-tool shortcuts follow Safari defaults and are customizable in
|
|||
|
||||
cmux NIGHTLY is a separate app with its own bundle ID, so it runs alongside the stable version. Built automatically from the latest `main` commit and auto-updates via its own Sparkle feed.
|
||||
|
||||
## Session restore (current behavior)
|
||||
|
||||
On relaunch, cmux currently restores app layout and metadata only:
|
||||
- Window/workspace/pane layout
|
||||
- Working directories
|
||||
- Terminal scrollback (best effort)
|
||||
- Browser URL and navigation history
|
||||
|
||||
cmux does **not** restore live process state inside terminal apps. For example, active Claude Code/tmux/vim sessions are not resumed after restart yet.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contributing
|
||||
|
||||
Ways to get involved:
|
||||
|
||||
- Follow us on X for updates [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), and [@austinywang](https://x.com/austinywang)
|
||||
- Join the conversation on [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Create and participate in [GitHub issues](https://github.com/manaflow-ai/cmux/issues) and [discussions](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Let us know what you're building with cmux
|
||||
|
||||
## Community
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
|
|
@ -200,6 +249,20 @@ cmux NIGHTLY is a separate app with its own bundle ID, so it runs alongside the
|
|||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux is free, open source, and always will be. If you'd like to support development and get early access to what's coming next:
|
||||
|
||||
**[Get Founder's Edition](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Prioritized feature requests/bug fixes**
|
||||
- **Early access: cmux AI that gives you context on every workspace, tab and panel**
|
||||
- **Early access: iOS app with terminals synced between desktop and phone**
|
||||
- **Early access: Cloud VMs**
|
||||
- **Early access: Voice mode**
|
||||
- **My personal iMessage/WhatsApp**
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
190
README.no.md
|
|
@ -1,9 +1,5 @@
|
|||
> Denne oversettelsen ble generert av Claude. Hvis du har forslag til forbedringer, send gjerne en PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | Norsk | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">En Ghostty-basert macOS-terminal med vertikale faner og varsler for AI-kodeagenter</p>
|
||||
|
||||
|
|
@ -14,17 +10,64 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux skjermbilde" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | Norsk | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux skjermbilde" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demovideo</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Funksjoner
|
||||
|
||||
- **Vertikale faner** — Sidefeltet viser git-gren, arbeidsmappe, lyttende porter og siste varselstekst
|
||||
- **Varselringer** — Paneler far en bla ring og faner lyser opp nar AI-agenter (Claude Code, OpenCode) trenger oppmerksomheten din
|
||||
- **Varselpanel** — Se alle ventende varsler pa ett sted, hopp til det nyeste uleste
|
||||
- **Delte paneler** — Horisontale og vertikale delinger
|
||||
- **Innebygd nettleser** — Del en nettleser ved siden av terminalen med et skriptbart API portet fra [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
- **Skriptbar** — CLI og socket API for a opprette arbeidsomrader, dele paneler, sende tastetrykk og automatisere nettleseren
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Varselringer</h3>
|
||||
Paneler får en blå ring og faner lyser opp når kodeagenter trenger oppmerksomheten din
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Varselringer" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Varselpanel</h3>
|
||||
Se alle ventende varsler på ett sted, hopp til det nyeste uleste
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Varselmerke i sidefeltet" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Innebygd nettleser</h3>
|
||||
Del en nettleser ved siden av terminalen med et skriptbart API portet fra <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Innebygd nettleser" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Vertikale + horisontale faner</h3>
|
||||
Sidefeltet viser git-gren, tilknyttet PR-status/nummer, arbeidsmappe, lyttende porter og siste varselstekst. Del horisontalt og vertikalt.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Vertikale faner og delte paneler" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Skriptbar** — CLI og socket API for å opprette arbeidsområder, dele paneler, sende tastetrykk og automatisere nettleseren
|
||||
- **Nativ macOS-app** — Bygget med Swift og AppKit, ikke Electron. Rask oppstart, lavt minneforbruk.
|
||||
- **Ghostty-kompatibel** — Leser din eksisterende `~/.config/ghostty/config` for temaer, skrifttyper og farger
|
||||
- **GPU-akselerert** — Drevet av libghostty for jevn gjengivelse
|
||||
|
|
@ -37,7 +80,7 @@
|
|||
<img src="./docs/assets/macos-badge.png" alt="Last ned cmux for macOS" width="180" />
|
||||
</a>
|
||||
|
||||
Apne `.dmg`-filen og dra cmux til Programmer-mappen. cmux oppdaterer seg selv automatisk via Sparkle, sa du trenger bare a laste ned en gang.
|
||||
Åpne `.dmg`-filen og dra cmux til Programmer-mappen. cmux oppdaterer seg selv automatisk via Sparkle, så du trenger bare å laste ned én gang.
|
||||
|
||||
### Homebrew
|
||||
|
||||
|
|
@ -46,38 +89,53 @@ brew tap manaflow-ai/cmux
|
|||
brew install --cask cmux
|
||||
```
|
||||
|
||||
For a oppdatere senere:
|
||||
For å oppdatere senere:
|
||||
|
||||
```bash
|
||||
brew upgrade --cask cmux
|
||||
```
|
||||
|
||||
Ved forste oppstart kan macOS be deg bekrefte apning av en app fra en identifisert utvikler. Klikk **Apne** for a fortsette.
|
||||
Ved første oppstart kan macOS be deg bekrefte åpning av en app fra en identifisert utvikler. Klikk **Åpne** for å fortsette.
|
||||
|
||||
## Hvorfor cmux?
|
||||
|
||||
Jeg kjorer mange Claude Code- og Codex-sesjoner parallelt. Jeg brukte Ghostty med en haug delte paneler, og stolte pa native macOS-varsler for a vite nar en agent trengte meg. Men Claude Codes varselstekst er alltid bare "Claude is waiting for your input" uten kontekst, og med nok faner apne kunne jeg ikke engang lese titlene lenger.
|
||||
Jeg kjører mange Claude Code- og Codex-sesjoner parallelt. Jeg brukte Ghostty med en haug delte paneler, og stolte på native macOS-varsler for å vite når en agent trengte meg. Men Claude Codes varselstekst er alltid bare "Claude is waiting for your input" uten kontekst, og med nok faner åpne kunne jeg ikke engang lese titlene lenger.
|
||||
|
||||
Jeg provde noen kodeorkestratorer, men de fleste var Electron/Tauri-apper og ytelsen irriterte meg. Jeg foretrekker ogsa terminalen siden GUI-orkestratorer laser deg inn i arbeidsflyten deres. Sa jeg bygde cmux som en nativ macOS-app i Swift/AppKit. Den bruker libghostty for terminalgjengivelse og leser din eksisterende Ghostty-konfigurasjon for temaer, skrifttyper og farger.
|
||||
Jeg prøvde noen kodeorkestratorer, men de fleste var Electron/Tauri-apper og ytelsen irriterte meg. Jeg foretrekker også terminalen siden GUI-orkestratorer låser deg inn i arbeidsflyten deres. Så jeg bygde cmux som en nativ macOS-app i Swift/AppKit. Den bruker libghostty for terminalgjengivelse og leser din eksisterende Ghostty-konfigurasjon for temaer, skrifttyper og farger.
|
||||
|
||||
Hovedtilleggene er sidefeltet og varselsystemet. Sidefeltet har vertikale faner som viser git-gren, arbeidsmappe, lyttende porter og siste varselstekst for hvert arbeidsomrade. Varselsystemet fanger opp terminalsekvenser (OSC 9/99/777) og har en CLI (`cmux notify`) du kan koble til agentkroker for Claude Code, OpenCode osv. Nar en agent venter, far panelet en bla ring og fanen lyser opp i sidefeltet, sa jeg kan se hvilken som trenger meg pa tvers av delinger og faner. Cmd+Shift+U hopper til det nyeste uleste.
|
||||
Hovedtilleggene er sidefeltet og varselsystemet. Sidefeltet har vertikale faner som viser git-gren, tilknyttet PR-status/nummer, arbeidsmappe, lyttende porter og siste varselstekst for hvert arbeidsområde. Varselsystemet fanger opp terminalsekvenser (OSC 9/99/777) og har en CLI (`cmux notify`) du kan koble til agentkroker for Claude Code, OpenCode osv. Når en agent venter, får panelet en blå ring og fanen lyser opp i sidefeltet, så jeg kan se hvilken som trenger meg på tvers av delinger og faner. Cmd+Shift+U hopper til det nyeste uleste.
|
||||
|
||||
Den innebygde nettleseren har et skriptbart API portet fra [agent-browser](https://github.com/vercel-labs/agent-browser). Agenter kan ta overblikk over tilgjengelighetstreet, hente elementreferanser, klikke, fylle ut skjemaer og kjore JS. Du kan dele et nettleserpanel ved siden av terminalen og la Claude Code samhandle med utviklingsserveren din direkte.
|
||||
Den innebygde nettleseren har et skriptbart API portet fra [agent-browser](https://github.com/vercel-labs/agent-browser). Agenter kan ta overblikk over tilgjengelighetstreet, hente elementreferanser, klikke, fylle ut skjemaer og kjøre JS. Du kan dele et nettleserpanel ved siden av terminalen og la Claude Code samhandle med utviklingsserveren din direkte.
|
||||
|
||||
Alt er skriptbart gjennom CLI og socket API — opprett arbeidsomrader/faner, del paneler, send tastetrykk, apne URLer i nettleseren.
|
||||
Alt er skriptbart gjennom CLI og socket API — opprett arbeidsområder/faner, del paneler, send tastetrykk, åpne URLer i nettleseren.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux er ikke foreskrivende om hvordan utviklere bruker verktøyene sine. Det er en terminal og nettleser med en CLI, og resten er opp til deg.
|
||||
|
||||
cmux er en primitiv, ikke en løsning. Det gir deg en terminal, en nettleser, varsler, arbeidsområder, delinger, faner og en CLI for å kontrollere alt sammen. cmux tvinger deg ikke inn i en bestemt måte å bruke kodeagenter på. Hva du bygger med primitivene er ditt.
|
||||
|
||||
De beste utviklerne har alltid bygget sine egne verktøy. Ingen har funnet ut den beste måten å jobbe med agenter på ennå, og teamene som bygger lukkede produkter har definitivt ikke gjort det heller. Utviklerne som er nærmest sine egne kodebaser vil finne det ut først.
|
||||
|
||||
Gi en million utviklere komponerbare primitiver og de vil kollektivt finne de mest effektive arbeidsflytene raskere enn noe produktteam kunne designet ovenfra og ned.
|
||||
|
||||
## Dokumentasjon
|
||||
|
||||
For mer informasjon om hvordan du konfigurerer cmux, [gå til dokumentasjonen vår](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Tastatursnarveier
|
||||
|
||||
### Arbeidsomrader
|
||||
### Arbeidsområder
|
||||
|
||||
| Snarvei | Handling |
|
||||
|----------|--------|
|
||||
| ⌘ N | Nytt arbeidsomrade |
|
||||
| ⌘ 1–8 | Hopp til arbeidsomrade 1–8 |
|
||||
| ⌘ 9 | Hopp til siste arbeidsomrade |
|
||||
| ⌃ ⌘ ] | Neste arbeidsomrade |
|
||||
| ⌃ ⌘ [ | Forrige arbeidsomrade |
|
||||
| ⌘ ⇧ W | Lukk arbeidsomrade |
|
||||
| ⌘ N | Nytt arbeidsområde |
|
||||
| ⌘ 1–8 | Hopp til arbeidsområde 1–8 |
|
||||
| ⌘ 9 | Hopp til siste arbeidsområde |
|
||||
| ⌃ ⌘ ] | Neste arbeidsområde |
|
||||
| ⌃ ⌘ [ | Forrige arbeidsområde |
|
||||
| ⌘ ⇧ W | Lukk arbeidsområde |
|
||||
| ⌘ ⇧ R | Gi nytt navn til arbeidsområde |
|
||||
| ⌘ B | Vis/skjul sidefelt |
|
||||
|
||||
### Overflater
|
||||
|
|
@ -97,21 +155,24 @@ Alt er skriptbart gjennom CLI og socket API — opprett arbeidsomrader/faner, de
|
|||
|
||||
| Snarvei | Handling |
|
||||
|----------|--------|
|
||||
| ⌘ D | Del til hoyre |
|
||||
| ⌘ D | Del til høyre |
|
||||
| ⌘ ⇧ D | Del nedover |
|
||||
| ⌥ ⌘ ← → ↑ ↓ | Fokuser panel i retning |
|
||||
| ⌘ ⇧ H | Blink fokusert panel |
|
||||
|
||||
### Nettleser
|
||||
|
||||
Nettleserens utviklerverktøysnarveier følger Safari-standarder og kan tilpasses i `Innstillinger → Tastatursnarveier`.
|
||||
|
||||
| Snarvei | Handling |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Apne nettleser i deling |
|
||||
| ⌘ ⇧ L | Åpne nettleser i deling |
|
||||
| ⌘ L | Fokuser adressefeltet |
|
||||
| ⌘ [ | Tilbake |
|
||||
| ⌘ ] | Fremover |
|
||||
| ⌘ R | Last inn siden pa nytt |
|
||||
| ⌥ ⌘ I | Apne utviklerverktoy |
|
||||
| ⌘ R | Last inn siden på nytt |
|
||||
| ⌥ ⌘ I | Vis/skjul utviklerverktøy (Safari-standard) |
|
||||
| ⌥ ⌘ C | Vis JavaScript-konsoll (Safari-standard) |
|
||||
|
||||
### Varsler
|
||||
|
||||
|
|
@ -120,14 +181,14 @@ Alt er skriptbart gjennom CLI og socket API — opprett arbeidsomrader/faner, de
|
|||
| ⌘ I | Vis varselpanel |
|
||||
| ⌘ ⇧ U | Hopp til nyeste uleste |
|
||||
|
||||
### Sok
|
||||
### Søk
|
||||
|
||||
| Snarvei | Handling |
|
||||
|----------|--------|
|
||||
| ⌘ F | Sok |
|
||||
| ⌘ G / ⌘ ⇧ G | Sok neste / forrige |
|
||||
| ⌘ ⇧ F | Skjul sokelinje |
|
||||
| ⌘ E | Bruk utvalg til sok |
|
||||
| ⌘ F | Søk |
|
||||
| ⌘ G / ⌘ ⇧ G | Søk neste / forrige |
|
||||
| ⌘ ⇧ F | Skjul søkelinje |
|
||||
| ⌘ E | Bruk utvalg til søk |
|
||||
|
||||
### Terminal
|
||||
|
||||
|
|
@ -145,9 +206,66 @@ Alt er skriptbart gjennom CLI og socket API — opprett arbeidsomrader/faner, de
|
|||
|----------|--------|
|
||||
| ⌘ ⇧ N | Nytt vindu |
|
||||
| ⌘ , | Innstillinger |
|
||||
| ⌘ ⇧ , | Last inn konfigurasjon pa nytt |
|
||||
| ⌘ ⇧ , | Last inn konfigurasjon på nytt |
|
||||
| ⌘ Q | Avslutt |
|
||||
|
||||
## Nattlige bygg
|
||||
|
||||
[Last ned cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY er en separat app med sin egen bundle-ID, så den kjører ved siden av den stabile versjonen. Bygges automatisk fra den siste `main`-commiten og oppdateres automatisk via sin egen Sparkle-feed.
|
||||
|
||||
## Sesjonssgjenoppretting (nåværende oppførsel)
|
||||
|
||||
Ved omstart gjenoppretter cmux for øyeblikket kun applayouten og metadata:
|
||||
- Vindu-/arbeidsområde-/panellayout
|
||||
- Arbeidsmapper
|
||||
- Terminal-rullingshistorikk (best effort)
|
||||
- Nettleser-URL og navigasjonshistorikk
|
||||
|
||||
cmux gjenoppretter **ikke** aktive prosesstilstander inne i terminalapper. For eksempel blir aktive Claude Code/tmux/vim-sesjoner ikke gjenopptatt etter omstart ennå.
|
||||
|
||||
## Stjernehistorikk
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Bidra
|
||||
|
||||
Måter å engasjere seg:
|
||||
|
||||
- Følg oss på X for oppdateringer [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), og [@austinywang](https://x.com/austinywang)
|
||||
- Bli med i samtalen på [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Opprett og delta i [GitHub-issues](https://github.com/manaflow-ai/cmux/issues) og [diskusjoner](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Fortell oss hva du bygger med cmux
|
||||
|
||||
## Fellesskap
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Grunnleggerutgaven
|
||||
|
||||
cmux er gratis, åpen kildekode, og vil alltid være det. Hvis du vil støtte utviklingen og få tidlig tilgang til det som kommer:
|
||||
|
||||
**[Få Grunnleggerutgaven](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Prioriterte funksjonsforespørsler/feilrettinger**
|
||||
- **Tidlig tilgang: cmux AI som gir deg kontekst om hvert arbeidsområde, fane og panel**
|
||||
- **Tidlig tilgang: iOS-app med terminaler synkronisert mellom desktop og telefon**
|
||||
- **Tidlig tilgang: Sky-VMer**
|
||||
- **Tidlig tilgang: Stemmemodus**
|
||||
- **Min personlige iMessage/WhatsApp**
|
||||
|
||||
## Lisens
|
||||
|
||||
Dette prosjektet er lisensiert under GNU Affero General Public License v3.0 eller nyere (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
142
README.pl.md
|
|
@ -1,9 +1,5 @@
|
|||
> To tłumaczenie zostało wygenerowane przez Claude. Jeśli masz sugestie dotyczące poprawek, otwórz PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | Polski | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Terminal macOS oparty na Ghostty z pionowymi kartami i powiadomieniami dla agentów kodowania AI</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="Zrzut ekranu cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | Polski | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="Zrzut ekranu cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Film demonstracyjny</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Funkcje
|
||||
|
||||
- **Pionowe karty** — Pasek boczny pokazuje gałąź git, katalog roboczy, nasłuchujące porty i tekst ostatniego powiadomienia
|
||||
- **Pierścienie powiadomień** — Panele otrzymują niebieski pierścień, a karty podświetlają się, gdy agenci AI (Claude Code, OpenCode) potrzebują Twojej uwagi
|
||||
- **Panel powiadomień** — Zobacz wszystkie oczekujące powiadomienia w jednym miejscu, przeskocz do najnowszego nieprzeczytanego
|
||||
- **Podzielone panele** — Podziały poziome i pionowe
|
||||
- **Wbudowana przeglądarka** — Podziel przeglądarkę obok terminala ze skryptowalnym API przeniesionym z [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Pierścienie powiadomień</h3>
|
||||
Panele otrzymują niebieski pierścień, a karty podświetlają się, gdy agenci kodowania potrzebują Twojej uwagi
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Pierścienie powiadomień" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Panel powiadomień</h3>
|
||||
Zobacz wszystkie oczekujące powiadomienia w jednym miejscu, przeskocz do najnowszego nieprzeczytanego
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Znacznik powiadomień w pasku bocznym" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Wbudowana przeglądarka</h3>
|
||||
Podziel przeglądarkę obok terminala ze skryptowalnym API przeniesionym z <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Wbudowana przeglądarka" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Pionowe + poziome karty</h3>
|
||||
Pasek boczny pokazuje gałąź git, status/numer powiązanego PR, katalog roboczy, nasłuchujące porty i tekst ostatniego powiadomienia. Podziały poziome i pionowe.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Pionowe karty i podzielone panele" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Skryptowalny** — CLI i socket API do tworzenia przestrzeni roboczych, dzielenia paneli, wysyłania naciśnięć klawiszy i automatyzacji przeglądarki
|
||||
- **Natywna aplikacja macOS** — Zbudowana w Swift i AppKit, nie Electron. Szybki start, niskie zużycie pamięci.
|
||||
- **Kompatybilny z Ghostty** — Odczytuje istniejącą konfigurację `~/.config/ghostty/config` dla motywów, czcionek i kolorów
|
||||
|
|
@ -60,12 +103,26 @@ Uruchamiam wiele sesji Claude Code i Codex równolegle. Używałem Ghostty z mas
|
|||
|
||||
Wypróbowałem kilka orkiestratorów kodowania, ale większość z nich to aplikacje Electron/Tauri, a ich wydajność mi przeszkadzała. Po prostu wolę też terminal, ponieważ orkiestratory GUI zamykają cię w swoim przepływie pracy. Dlatego zbudowałem cmux jako natywną aplikację macOS w Swift/AppKit. Używa libghostty do renderowania terminala i odczytuje istniejącą konfigurację Ghostty dla motywów, czcionek i kolorów.
|
||||
|
||||
Główne dodatki to pasek boczny i system powiadomień. Pasek boczny ma pionowe karty pokazujące gałąź git, katalog roboczy, nasłuchujące porty i tekst ostatniego powiadomienia dla każdej przestrzeni roboczej. System powiadomień przechwytuje sekwencje terminala (OSC 9/99/777) i ma CLI (`cmux notify`), który można podpiąć do hooków agentów dla Claude Code, OpenCode itp. Gdy agent czeka, jego panel otrzymuje niebieski pierścień, a karta podświetla się w pasku bocznym, więc mogę powiedzieć, który mnie potrzebuje, niezależnie od podziałów i kart. Cmd+Shift+U przeskakuje do najnowszego nieprzeczytanego.
|
||||
Główne dodatki to pasek boczny i system powiadomień. Pasek boczny ma pionowe karty pokazujące gałąź git, status/numer powiązanego PR, katalog roboczy, nasłuchujące porty i tekst ostatniego powiadomienia dla każdej przestrzeni roboczej. System powiadomień przechwytuje sekwencje terminala (OSC 9/99/777) i ma CLI (`cmux notify`), który można podpiąć do hooków agentów dla Claude Code, OpenCode itp. Gdy agent czeka, jego panel otrzymuje niebieski pierścień, a karta podświetla się w pasku bocznym, więc mogę powiedzieć, który mnie potrzebuje, niezależnie od podziałów i kart. Cmd+Shift+U przeskakuje do najnowszego nieprzeczytanego.
|
||||
|
||||
Wbudowana przeglądarka ma skryptowalny API przeniesiony z [agent-browser](https://github.com/vercel-labs/agent-browser). Agenci mogą wykonać migawkę drzewa dostępności, uzyskać referencje elementów, klikać, wypełniać formularze i ewaluować JS. Możesz podzielić panel przeglądarki obok terminala i pozwolić Claude Code bezpośrednio komunikować się z Twoim serwerem deweloperskim.
|
||||
|
||||
Wszystko jest skryptowalne przez CLI i socket API — tworzenie przestrzeni roboczych/kart, dzielenie paneli, wysyłanie naciśnięć klawiszy, otwieranie URL-ów w przeglądarce.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux nie narzuca programistom sposobu korzystania z narzędzi. To terminal i przeglądarka z CLI, a reszta zależy od Ciebie.
|
||||
|
||||
cmux jest prymitywem, nie rozwiązaniem. Daje Ci terminal, przeglądarkę, powiadomienia, przestrzenie robocze, podziały, karty i CLI do kontrolowania tego wszystkiego. cmux nie zmusza Cię do określonego sposobu korzystania z agentów kodowania. To, co zbudujesz z tych prymitywów, jest Twoje.
|
||||
|
||||
Najlepsi programiści zawsze budowali własne narzędzia. Nikt jeszcze nie wymyślił najlepszego sposobu pracy z agentami, a zespoły budujące zamknięte produkty też tego nie odkryły. Programiści najbliżej swoich własnych baz kodu wymyślą to pierwsi.
|
||||
|
||||
Daj milionowi programistów kompozycyjne prymitywy, a wspólnie znajdą najefektywniejsze przepływy pracy szybciej, niż jakikolwiek zespół produktowy mógłby zaprojektować odgórnie.
|
||||
|
||||
## Dokumentacja
|
||||
|
||||
Więcej informacji o konfiguracji cmux znajdziesz w [naszej dokumentacji](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Skróty Klawiszowe
|
||||
|
||||
### Przestrzenie robocze
|
||||
|
|
@ -78,6 +135,7 @@ Wszystko jest skryptowalne przez CLI i socket API — tworzenie przestrzeni robo
|
|||
| ⌃ ⌘ ] | Następna przestrzeń robocza |
|
||||
| ⌃ ⌘ [ | Poprzednia przestrzeń robocza |
|
||||
| ⌘ ⇧ W | Zamknij przestrzeń roboczą |
|
||||
| ⌘ ⇧ R | Zmień nazwę przestrzeni roboczej |
|
||||
| ⌘ B | Przełącz pasek boczny |
|
||||
|
||||
### Powierzchnie
|
||||
|
|
@ -104,6 +162,8 @@ Wszystko jest skryptowalne przez CLI i socket API — tworzenie przestrzeni robo
|
|||
|
||||
### Przeglądarka
|
||||
|
||||
Skróty narzędzi deweloperskich przeglądarki odpowiadają domyślnym ustawieniom Safari i można je dostosować w `Ustawienia → Skróty klawiszowe`.
|
||||
|
||||
| Skrót | Akcja |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Otwórz przeglądarkę w podziale |
|
||||
|
|
@ -111,7 +171,8 @@ Wszystko jest skryptowalne przez CLI i socket API — tworzenie przestrzeni robo
|
|||
| ⌘ [ | Wstecz |
|
||||
| ⌘ ] | Do przodu |
|
||||
| ⌘ R | Przeładuj stronę |
|
||||
| ⌥ ⌘ I | Otwórz Narzędzia Deweloperskie |
|
||||
| ⌥ ⌘ I | Przełącz Narzędzia Deweloperskie (domyślne Safari) |
|
||||
| ⌥ ⌘ C | Pokaż Konsolę JavaScript (domyślne Safari) |
|
||||
|
||||
### Powiadomienia
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ Wszystko jest skryptowalne przez CLI i socket API — tworzenie przestrzeni robo
|
|||
| ⌘ ⇧ , | Przeładuj konfigurację |
|
||||
| ⌘ Q | Zakończ |
|
||||
|
||||
## Wersje Nightly
|
||||
|
||||
[Pobierz cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY to osobna aplikacja z własnym identyfikatorem pakietu, więc działa obok wersji stabilnej. Budowana automatycznie z najnowszego commitu `main` i aktualizuje się automatycznie przez własny kanał Sparkle.
|
||||
|
||||
## Przywracanie sesji (obecne zachowanie)
|
||||
|
||||
Przy ponownym uruchomieniu cmux obecnie przywraca tylko układ aplikacji i metadane:
|
||||
- Układ okien/przestrzeni roboczych/paneli
|
||||
- Katalogi robocze
|
||||
- Scrollback terminala (najlepsza próba)
|
||||
- URL przeglądarki i historia nawigacji
|
||||
|
||||
cmux **nie** przywraca stanu żywych procesów wewnątrz aplikacji terminalowych. Na przykład aktywne sesje Claude Code/tmux/vim nie są jeszcze wznawiane po restarcie.
|
||||
|
||||
## Historia Gwiazdek
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Współtworzenie
|
||||
|
||||
Sposoby zaangażowania się:
|
||||
|
||||
- Obserwuj nas na X po aktualizacje [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) i [@austinywang](https://x.com/austinywang)
|
||||
- Dołącz do rozmowy na [Discordzie](https://discord.gg/xsgFEVrWCZ)
|
||||
- Twórz i uczestniczaj w [zgłoszeniach GitHub](https://github.com/manaflow-ai/cmux/issues) i [dyskusjach](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Daj nam znać, co budujesz z cmux
|
||||
|
||||
## Społeczność
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Edycja Założycielska
|
||||
|
||||
cmux jest darmowy, open source i zawsze taki będzie. Jeśli chcesz wesprzeć rozwój i uzyskać wczesny dostęp do nadchodzących funkcji:
|
||||
|
||||
**[Zdobądź Edycję Założycielską](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Priorytetowe prośby o funkcje/poprawki błędów**
|
||||
- **Wczesny dostęp: cmux AI, które daje Ci kontekst każdej przestrzeni roboczej, karty i panelu**
|
||||
- **Wczesny dostęp: aplikacja iOS z terminalami synchronizowanymi między komputerem a telefonem**
|
||||
- **Wczesny dostęp: maszyny wirtualne w chmurze**
|
||||
- **Wczesny dostęp: tryb głosowy**
|
||||
- **Mój osobisty iMessage/WhatsApp**
|
||||
|
||||
## Licencja
|
||||
|
||||
Ten projekt jest licencjonowany na warunkach GNU Affero General Public License v3.0 lub nowszej (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
146
README.pt-BR.md
|
|
@ -1,9 +1,5 @@
|
|||
> Esta tradução foi gerada pelo Claude. Se você tiver sugestões de melhoria, abra um PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | Português (Brasil) | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Um terminal macOS baseado em Ghostty com abas verticais e notificações para agentes de programação com IA</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="Captura de tela do cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | Português (Brasil) | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="Captura de tela do cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Vídeo de demonstração</a> · <a href="https://cmux.dev/blog/zen-of-cmux">O Zen do cmux</a>
|
||||
</p>
|
||||
|
||||
## Recursos
|
||||
|
||||
- **Abas verticais** — A barra lateral mostra o branch do git, diretório de trabalho, portas em escuta e o texto da última notificação
|
||||
- **Anéis de notificação** — Os painéis recebem um anel azul e as abas acendem quando agentes de IA (Claude Code, OpenCode) precisam da sua atenção
|
||||
- **Painel de notificações** — Veja todas as notificações pendentes em um só lugar, vá direto para a mais recente não lida
|
||||
- **Painéis divididos** — Divisões horizontais e verticais
|
||||
- **Navegador integrado** — Divida um navegador ao lado do seu terminal com uma API programável portada do [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Anéis de notificação</h3>
|
||||
Os painéis recebem um anel azul e as abas acendem quando agentes de programação precisam da sua atenção
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Anéis de notificação" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Painel de notificações</h3>
|
||||
Veja todas as notificações pendentes em um só lugar, vá direto para a mais recente não lida
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Badge de notificação na barra lateral" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Navegador integrado</h3>
|
||||
Divida um navegador ao lado do seu terminal com uma API programável portada do <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Navegador integrado" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Abas verticais + horizontais</h3>
|
||||
A barra lateral mostra o branch do git, status/número do PR vinculado, diretório de trabalho, portas em escuta e texto da última notificação. Divida horizontal e verticalmente.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Abas verticais e painéis divididos" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Programável** — CLI e socket API para criar workspaces, dividir painéis, enviar teclas e automatizar o navegador
|
||||
- **App nativo macOS** — Construído com Swift e AppKit, não Electron. Inicialização rápida, baixo consumo de memória.
|
||||
- **Compatível com Ghostty** — Lê sua configuração existente em `~/.config/ghostty/config` para temas, fontes e cores
|
||||
|
|
@ -60,15 +103,29 @@ Eu executo muitas sessões de Claude Code e Codex em paralelo. Eu estava usando
|
|||
|
||||
Eu tentei alguns orquestradores de código, mas a maioria era apps Electron/Tauri e o desempenho me incomodava. Eu também prefiro o terminal, já que orquestradores GUI te prendem no fluxo de trabalho deles. Então eu construí o cmux como um app nativo macOS em Swift/AppKit. Ele usa o libghostty para renderização do terminal e lê sua configuração existente do Ghostty para temas, fontes e cores.
|
||||
|
||||
As principais adições são a barra lateral e o sistema de notificações. A barra lateral tem abas verticais que mostram o branch do git, diretório de trabalho, portas em escuta e o texto da última notificação para cada workspace. O sistema de notificações captura sequências do terminal (OSC 9/99/777) e tem uma CLI (`cmux notify`) que você pode conectar aos hooks de agentes para Claude Code, OpenCode, etc. Quando um agente está esperando, seu painel recebe um anel azul e a aba acende na barra lateral, para que eu possa ver qual precisa de mim entre divisões e abas. Cmd+Shift+U pula para o mais recente não lido.
|
||||
As principais adições são a barra lateral e o sistema de notificações. A barra lateral tem abas verticais que mostram o branch do git, status/número do PR vinculado, diretório de trabalho, portas em escuta e o texto da última notificação para cada workspace. O sistema de notificações captura sequências do terminal (OSC 9/99/777) e tem uma CLI (`cmux notify`) que você pode conectar aos hooks de agentes para Claude Code, OpenCode, etc. Quando um agente está esperando, seu painel recebe um anel azul e a aba acende na barra lateral, para que eu possa ver qual precisa de mim entre divisões e abas. Cmd+Shift+U pula para o mais recente não lido.
|
||||
|
||||
O navegador integrado tem uma API programável portada do [agent-browser](https://github.com/vercel-labs/agent-browser). Agentes podem capturar a árvore de acessibilidade, obter referências de elementos, clicar, preencher formulários e executar JS. Você pode dividir um painel de navegador ao lado do seu terminal e fazer o Claude Code interagir diretamente com seu servidor de desenvolvimento.
|
||||
|
||||
Tudo é programável através da CLI e socket API — criar workspaces/abas, dividir painéis, enviar teclas, abrir URLs no navegador.
|
||||
|
||||
## O Zen do cmux
|
||||
|
||||
O cmux não é prescritivo sobre como os desenvolvedores usam suas ferramentas. É um terminal e navegador com uma CLI, e o resto é com você.
|
||||
|
||||
O cmux é uma primitiva, não uma solução. Ele te dá um terminal, um navegador, notificações, workspaces, divisões, abas e uma CLI para controlar tudo isso. O cmux não te força a usar agentes de programação de uma forma específica. O que você constrói com as primitivas é seu.
|
||||
|
||||
Os melhores desenvolvedores sempre construíram suas próprias ferramentas. Ninguém descobriu ainda a melhor forma de trabalhar com agentes, e as equipes construindo produtos fechados definitivamente também não. Os desenvolvedores mais próximos de suas próprias bases de código vão descobrir primeiro.
|
||||
|
||||
Dê a um milhão de desenvolvedores primitivas combináveis e eles coletivamente encontrarão os fluxos de trabalho mais eficientes mais rápido do que qualquer equipe de produto poderia projetar de cima para baixo.
|
||||
|
||||
## Documentação
|
||||
|
||||
Para mais informações sobre como configurar o cmux, [acesse nossa documentação](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Atalhos de Teclado
|
||||
|
||||
### Workspaces
|
||||
### Áreas de Trabalho
|
||||
|
||||
| Atalho | Ação |
|
||||
|----------|--------|
|
||||
|
|
@ -78,9 +135,10 @@ Tudo é programável através da CLI e socket API — criar workspaces/abas, div
|
|||
| ⌃ ⌘ ] | Próximo workspace |
|
||||
| ⌃ ⌘ [ | Workspace anterior |
|
||||
| ⌘ ⇧ W | Fechar workspace |
|
||||
| ⌘ ⇧ R | Renomear workspace |
|
||||
| ⌘ B | Alternar barra lateral |
|
||||
|
||||
### Surfaces
|
||||
### Superfícies
|
||||
|
||||
| Atalho | Ação |
|
||||
|----------|--------|
|
||||
|
|
@ -104,6 +162,8 @@ Tudo é programável através da CLI e socket API — criar workspaces/abas, div
|
|||
|
||||
### Navegador
|
||||
|
||||
Os atalhos de ferramentas do desenvolvedor do navegador seguem os padrões do Safari e podem ser personalizados em `Configurações → Atalhos de Teclado`.
|
||||
|
||||
| Atalho | Ação |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Abrir navegador em divisão |
|
||||
|
|
@ -111,7 +171,8 @@ Tudo é programável através da CLI e socket API — criar workspaces/abas, div
|
|||
| ⌘ [ | Voltar |
|
||||
| ⌘ ] | Avançar |
|
||||
| ⌘ R | Recarregar página |
|
||||
| ⌥ ⌘ I | Abrir Ferramentas do Desenvolvedor |
|
||||
| ⌥ ⌘ I | Alternar Ferramentas do Desenvolvedor (padrão Safari) |
|
||||
| ⌥ ⌘ C | Mostrar Console JavaScript (padrão Safari) |
|
||||
|
||||
### Notificações
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ Tudo é programável através da CLI e socket API — criar workspaces/abas, div
|
|||
| ⌘ ⇧ , | Recarregar configuração |
|
||||
| ⌘ Q | Sair |
|
||||
|
||||
## Builds Noturnos
|
||||
|
||||
[Baixar cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
O cmux NIGHTLY é um app separado com seu próprio bundle ID, então roda ao lado da versão estável. Construído automaticamente a partir do último commit em `main` e se atualiza automaticamente via seu próprio feed Sparkle.
|
||||
|
||||
## Restauração de sessão (comportamento atual)
|
||||
|
||||
Ao reiniciar, o cmux atualmente restaura apenas o layout do app e metadados:
|
||||
- Layout de janelas/workspaces/painéis
|
||||
- Diretórios de trabalho
|
||||
- Histórico de rolagem do terminal (melhor esforço)
|
||||
- URL do navegador e histórico de navegação
|
||||
|
||||
O cmux **não** restaura o estado de processos ativos dentro de apps de terminal. Por exemplo, sessões ativas de Claude Code/tmux/vim não são retomadas após reiniciar ainda.
|
||||
|
||||
## Histórico de Estrelas
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Formas de participar:
|
||||
|
||||
- Siga-nos no X para atualizações [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen), e [@austinywang](https://x.com/austinywang)
|
||||
- Participe da conversa no [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Crie e participe de [issues no GitHub](https://github.com/manaflow-ai/cmux/issues) e [discussões](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Nos conte o que você está construindo com o cmux
|
||||
|
||||
## Comunidade
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Edição do Fundador
|
||||
|
||||
O cmux é gratuito, open source, e sempre será. Se você gostaria de apoiar o desenvolvimento e ter acesso antecipado ao que está por vir:
|
||||
|
||||
**[Obter Edição do Fundador](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Solicitações de recursos/correções de bugs priorizadas**
|
||||
- **Acesso antecipado: cmux AI que te dá contexto sobre cada workspace, aba e painel**
|
||||
- **Acesso antecipado: app iOS com terminais sincronizados entre desktop e celular**
|
||||
- **Acesso antecipado: VMs na nuvem**
|
||||
- **Acesso antecipado: Modo de voz**
|
||||
- **Meu iMessage/WhatsApp pessoal**
|
||||
|
||||
## Licença
|
||||
|
||||
Este projeto é licenciado sob a GNU Affero General Public License v3.0 ou posterior (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
142
README.ru.md
|
|
@ -1,9 +1,5 @@
|
|||
> Этот перевод создан Claude. Если у вас есть предложения по улучшению, откройте PR.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | Русский | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">Терминал macOS на базе Ghostty с вертикальными вкладками и уведомлениями для AI-агентов программирования</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="Скриншот cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | Русский | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="Скриншот cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Демо-видео</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Возможности
|
||||
|
||||
- **Вертикальные вкладки** — Боковая панель показывает ветку git, рабочий каталог, прослушиваемые порты и текст последнего уведомления
|
||||
- **Кольца уведомлений** — Панели получают синее кольцо, а вкладки подсвечиваются, когда AI-агенты (Claude Code, OpenCode) нуждаются в вашем внимании
|
||||
- **Панель уведомлений** — Просматривайте все ожидающие уведомления в одном месте, переходите к последнему непрочитанному
|
||||
- **Разделённые панели** — Горизонтальное и вертикальное разделение
|
||||
- **Встроенный браузер** — Разделите браузер рядом с терминалом со скриптуемым API, портированным из [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Кольца уведомлений</h3>
|
||||
Панели получают синее кольцо, а вкладки подсвечиваются, когда агенты программирования нуждаются в вашем внимании
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Кольца уведомлений" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Панель уведомлений</h3>
|
||||
Просматривайте все ожидающие уведомления в одном месте, переходите к последнему непрочитанному
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Значок уведомлений в боковой панели" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Встроенный браузер</h3>
|
||||
Разделите браузер рядом с терминалом со скриптуемым API, портированным из <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Встроенный браузер" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Вертикальные + горизонтальные вкладки</h3>
|
||||
Боковая панель показывает ветку git, статус/номер связанного PR, рабочий каталог, прослушиваемые порты и текст последнего уведомления. Горизонтальное и вертикальное разделение.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Вертикальные вкладки и разделённые панели" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Скриптуемость** — CLI и socket API для создания рабочих пространств, разделения панелей, отправки нажатий клавиш и автоматизации браузера
|
||||
- **Нативное приложение macOS** — Создано на Swift и AppKit, не Electron. Быстрый запуск, низкое потребление памяти.
|
||||
- **Совместимость с Ghostty** — Читает вашу существующую конфигурацию `~/.config/ghostty/config` для тем, шрифтов и цветов
|
||||
|
|
@ -60,12 +103,26 @@ brew upgrade --cask cmux
|
|||
|
||||
Я попробовал несколько оркестраторов для кодирования, но большинство из них были приложениями Electron/Tauri, и их производительность меня раздражала. К тому же я просто предпочитаю терминал, поскольку GUI-оркестраторы привязывают вас к своему рабочему процессу. Поэтому я создал cmux как нативное приложение macOS на Swift/AppKit. Оно использует libghostty для рендеринга терминала и читает вашу существующую конфигурацию Ghostty для тем, шрифтов и цветов.
|
||||
|
||||
Основные дополнения — это боковая панель и система уведомлений. Боковая панель имеет вертикальные вкладки, которые показывают ветку git, рабочий каталог, прослушиваемые порты и текст последнего уведомления для каждого рабочего пространства. Система уведомлений перехватывает терминальные последовательности (OSC 9/99/777) и имеет CLI (`cmux notify`), который можно подключить к хукам агентов для Claude Code, OpenCode и т.д. Когда агент ожидает, его панель получает синее кольцо, а вкладка подсвечивается в боковой панели, так что я могу определить, какой из них нуждается во мне, среди разделений и вкладок. Cmd+Shift+U переходит к последнему непрочитанному.
|
||||
Основные дополнения — это боковая панель и система уведомлений. Боковая панель имеет вертикальные вкладки, которые показывают ветку git, статус/номер связанного PR, рабочий каталог, прослушиваемые порты и текст последнего уведомления для каждого рабочего пространства. Система уведомлений перехватывает терминальные последовательности (OSC 9/99/777) и имеет CLI (`cmux notify`), который можно подключить к хукам агентов для Claude Code, OpenCode и т.д. Когда агент ожидает, его панель получает синее кольцо, а вкладка подсвечивается в боковой панели, так что я могу определить, какой из них нуждается во мне, среди разделений и вкладок. Cmd+Shift+U переходит к последнему непрочитанному.
|
||||
|
||||
Встроенный браузер имеет скриптуемый API, портированный из [agent-browser](https://github.com/vercel-labs/agent-browser). Агенты могут делать снимок дерева доступности, получать ссылки на элементы, кликать, заполнять формы и выполнять JS. Вы можете разделить панель браузера рядом с терминалом и позволить Claude Code взаимодействовать с вашим сервером разработки напрямую.
|
||||
|
||||
Всё скриптуемо через CLI и socket API — создание рабочих пространств/вкладок, разделение панелей, отправка нажатий клавиш, открытие URL в браузере.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux не навязывает разработчикам, как использовать свои инструменты. Это терминал и браузер с CLI, а остальное зависит от вас.
|
||||
|
||||
cmux — это примитив, а не решение. Он даёт вам терминал, браузер, уведомления, рабочие пространства, разделения, вкладки и CLI для управления всем этим. cmux не заставляет вас использовать агентов для кодирования определённым образом. То, что вы построите из этих примитивов, принадлежит вам.
|
||||
|
||||
Лучшие разработчики всегда создавали собственные инструменты. Никто ещё не нашёл лучший способ работы с агентами, и команды, создающие закрытые продукты, тоже этого не сделали. Разработчики, ближе всех к своим кодовым базам, найдут это первыми.
|
||||
|
||||
Дайте миллиону разработчиков композируемые примитивы, и они коллективно найдут наиболее эффективные рабочие процессы быстрее, чем любая продуктовая команда могла бы спроектировать сверху вниз.
|
||||
|
||||
## Документация
|
||||
|
||||
Подробнее о настройке cmux читайте в [нашей документации](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Сочетания Клавиш
|
||||
|
||||
### Рабочие пространства
|
||||
|
|
@ -78,6 +135,7 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | Следующее рабочее пространство |
|
||||
| ⌃ ⌘ [ | Предыдущее рабочее пространство |
|
||||
| ⌘ ⇧ W | Закрыть рабочее пространство |
|
||||
| ⌘ ⇧ R | Переименовать рабочее пространство |
|
||||
| ⌘ B | Переключить боковую панель |
|
||||
|
||||
### Поверхности
|
||||
|
|
@ -104,6 +162,8 @@ brew upgrade --cask cmux
|
|||
|
||||
### Браузер
|
||||
|
||||
Сочетания клавиш инструментов разработчика браузера соответствуют настройкам Safari по умолчанию и настраиваются в `Настройки → Сочетания клавиш`.
|
||||
|
||||
| Сочетание | Действие |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Открыть браузер в разделении |
|
||||
|
|
@ -111,7 +171,8 @@ brew upgrade --cask cmux
|
|||
| ⌘ [ | Назад |
|
||||
| ⌘ ] | Вперёд |
|
||||
| ⌘ R | Перезагрузить страницу |
|
||||
| ⌥ ⌘ I | Открыть Инструменты Разработчика |
|
||||
| ⌥ ⌘ I | Переключить Инструменты Разработчика (по умолчанию Safari) |
|
||||
| ⌥ ⌘ C | Показать Консоль JavaScript (по умолчанию Safari) |
|
||||
|
||||
### Уведомления
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | Перезагрузить конфигурацию |
|
||||
| ⌘ Q | Выход |
|
||||
|
||||
## Ночные сборки
|
||||
|
||||
[Скачать cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY — это отдельное приложение с собственным идентификатором пакета, поэтому оно работает параллельно со стабильной версией. Собирается автоматически из последнего коммита `main` и обновляется через собственный канал Sparkle.
|
||||
|
||||
## Восстановление сессии (текущее поведение)
|
||||
|
||||
При перезапуске cmux в настоящее время восстанавливает только макет приложения и метаданные:
|
||||
- Макет окон/рабочих пространств/панелей
|
||||
- Рабочие каталоги
|
||||
- Scrollback терминала (по возможности)
|
||||
- URL браузера и история навигации
|
||||
|
||||
cmux **не** восстанавливает состояние живых процессов внутри терминальных приложений. Например, активные сессии Claude Code/tmux/vim пока не возобновляются после перезапуска.
|
||||
|
||||
## История звёзд
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Участие
|
||||
|
||||
Способы принять участие:
|
||||
|
||||
- Подписывайтесь на нас в X для получения обновлений [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) и [@austinywang](https://x.com/austinywang)
|
||||
- Присоединяйтесь к обсуждению в [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- Создавайте и участвуйте в [GitHub issues](https://github.com/manaflow-ai/cmux/issues) и [обсуждениях](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- Расскажите нам, что вы создаёте с помощью cmux
|
||||
|
||||
## Сообщество
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Издание основателя
|
||||
|
||||
cmux бесплатен, с открытым исходным кодом и всегда будет таким. Если вы хотите поддержать разработку и получить ранний доступ к будущим возможностям:
|
||||
|
||||
**[Получить Издание основателя](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Приоритетные запросы на функции/исправления ошибок**
|
||||
- **Ранний доступ: cmux AI, который даёт контекст по каждому рабочему пространству, вкладке и панели**
|
||||
- **Ранний доступ: приложение для iOS с терминалами, синхронизированными между компьютером и телефоном**
|
||||
- **Ранний доступ: облачные виртуальные машины**
|
||||
- **Ранний доступ: голосовой режим**
|
||||
- **Мой личный iMessage/WhatsApp**
|
||||
|
||||
## Лицензия
|
||||
|
||||
Этот проект лицензирован под GNU Affero General Public License v3.0 или более поздней версии (`AGPL-3.0-or-later`).
|
||||
|
|
|
|||
160
README.th.md
|
|
@ -1,9 +1,5 @@
|
|||
> การแปลนี้สร้างโดย Claude หากมีข้อเสนอแนะในการปรับปรุง กรุณาเปิด PR
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | ไทย | <a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">เทอร์มินัล macOS ที่ใช้ Ghostty พร้อมแท็บแนวตั้งและการแจ้งเตือนสำหรับเอเจนต์เขียนโค้ด AI</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="ภาพหน้าจอ cmux" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | ไทย | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="ภาพหน้าจอ cmux" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ วิดีโอสาธิต</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## คุณสมบัติ
|
||||
|
||||
- **แท็บแนวตั้ง** — แถบด้านข้างแสดง git branch, ไดเรกทอรีทำงาน, พอร์ตที่กำลังฟัง และข้อความแจ้งเตือนล่าสุด
|
||||
- **วงแหวนแจ้งเตือน** — แผงจะมีวงแหวนสีน้ำเงินและแท็บจะสว่างขึ้นเมื่อเอเจนต์ AI (Claude Code, OpenCode) ต้องการความสนใจของคุณ
|
||||
- **แผงแจ้งเตือน** — ดูการแจ้งเตือนที่รอดำเนินการทั้งหมดในที่เดียว ข้ามไปยังรายการที่ยังไม่ได้อ่านล่าสุด
|
||||
- **แผงแบ่ง** — แบ่งแนวนอนและแนวตั้ง
|
||||
- **เบราว์เซอร์ในแอป** — แบ่งเบราว์เซอร์ข้างเทอร์มินัลพร้อม API ที่เขียนสคริปต์ได้ ย้ายมาจาก [agent-browser](https://github.com/vercel-labs/agent-browser)
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>วงแหวนแจ้งเตือน</h3>
|
||||
แผงจะมีวงแหวนสีน้ำเงินและแท็บจะสว่างขึ้นเมื่อเอเจนต์เขียนโค้ดต้องการความสนใจของคุณ
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="วงแหวนแจ้งเตือน" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>แผงแจ้งเตือน</h3>
|
||||
ดูการแจ้งเตือนที่รอดำเนินการทั้งหมดในที่เดียว ข้ามไปยังรายการที่ยังไม่ได้อ่านล่าสุด
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="ป้ายแจ้งเตือนแถบด้านข้าง" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>เบราว์เซอร์ในแอป</h3>
|
||||
แบ่งเบราว์เซอร์ข้างเทอร์มินัลพร้อม API ที่เขียนสคริปต์ได้ ย้ายมาจาก <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="เบราว์เซอร์ในตัว" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>แท็บแนวตั้ง + แนวนอน</h3>
|
||||
แถบด้านข้างแสดง git branch, สถานะ/หมายเลข PR ที่เชื่อมโยง, ไดเรกทอรีทำงาน, พอร์ตที่กำลังฟัง และข้อความแจ้งเตือนล่าสุด แบ่งแนวนอนและแนวตั้ง
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="แท็บแนวตั้งและแผงแบ่ง" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **เขียนสคริปต์ได้** — CLI และ socket API สำหรับสร้างเวิร์กสเปซ แบ่งแผง ส่งการกดแป้นพิมพ์ และควบคุมเบราว์เซอร์อัตโนมัติ
|
||||
- **แอป macOS ดั้งเดิม** — สร้างด้วย Swift และ AppKit ไม่ใช่ Electron เริ่มต้นเร็ว ใช้หน่วยความจำน้อย
|
||||
- **เข้ากันได้กับ Ghostty** — อ่านการตั้งค่าที่มีอยู่ของคุณจาก `~/.config/ghostty/config` สำหรับธีม ฟอนต์ และสี
|
||||
|
|
@ -60,12 +103,26 @@ brew upgrade --cask cmux
|
|||
|
||||
ผมลองใช้ออร์เคสเตรเตอร์สำหรับเขียนโค้ดบางตัว แต่ส่วนใหญ่เป็นแอป Electron/Tauri และประสิทธิภาพทำให้ผมรำคาญ ผมยังชอบเทอร์มินัลมากกว่าเพราะออร์เคสเตรเตอร์ GUI บังคับให้คุณใช้เวิร์กโฟลว์ของมัน ผมจึงสร้าง cmux เป็นแอป macOS ดั้งเดิมด้วย Swift/AppKit มันใช้ libghostty สำหรับการแสดงผลเทอร์มินัลและอ่านการตั้งค่า Ghostty ที่มีอยู่ของคุณสำหรับธีม ฟอนต์ และสี
|
||||
|
||||
สิ่งที่เพิ่มเติมหลักคือแถบด้านข้างและระบบแจ้งเตือน แถบด้านข้างมีแท็บแนวตั้งที่แสดง git branch, ไดเรกทอรีทำงาน, พอร์ตที่กำลังฟัง และข้อความแจ้งเตือนล่าสุดสำหรับแต่ละเวิร์กสเปซ ระบบแจ้งเตือนจับลำดับเทอร์มินัล (OSC 9/99/777) และมี CLI (`cmux notify`) ที่คุณสามารถเชื่อมต่อกับ hook ของเอเจนต์สำหรับ Claude Code, OpenCode เป็นต้น เมื่อเอเจนต์กำลังรอ แผงของมันจะมีวงแหวนสีน้ำเงินและแท็บจะสว่างขึ้นในแถบด้านข้าง เพื่อให้ผมบอกได้ว่าอันไหนต้องการผมข้ามแผงแบ่งและแท็บต่าง ๆ Cmd+Shift+U ข้ามไปยังรายการที่ยังไม่ได้อ่านล่าสุด
|
||||
สิ่งที่เพิ่มเติมหลักคือแถบด้านข้างและระบบแจ้งเตือน แถบด้านข้างมีแท็บแนวตั้งที่แสดง git branch, สถานะ/หมายเลข PR ที่เชื่อมโยง, ไดเรกทอรีทำงาน, พอร์ตที่กำลังฟัง และข้อความแจ้งเตือนล่าสุดสำหรับแต่ละเวิร์กสเปซ ระบบแจ้งเตือนจับลำดับเทอร์มินัล (OSC 9/99/777) และมี CLI (`cmux notify`) ที่คุณสามารถเชื่อมต่อกับ hook ของเอเจนต์สำหรับ Claude Code, OpenCode เป็นต้น เมื่อเอเจนต์กำลังรอ แผงของมันจะมีวงแหวนสีน้ำเงินและแท็บจะสว่างขึ้นในแถบด้านข้าง เพื่อให้ผมบอกได้ว่าอันไหนต้องการผมข้ามแผงแบ่งและแท็บต่าง ๆ Cmd+Shift+U ข้ามไปยังรายการที่ยังไม่ได้อ่านล่าสุด
|
||||
|
||||
เบราว์เซอร์ในแอปมี API ที่เขียนสคริปต์ได้ ย้ายมาจาก [agent-browser](https://github.com/vercel-labs/agent-browser) เอเจนต์สามารถจับภาพ accessibility tree, รับ element refs, คลิก, กรอกฟอร์ม และรัน JS ได้ คุณสามารถแบ่งแผงเบราว์เซอร์ข้างเทอร์มินัลและให้ Claude Code โต้ตอบกับเซิร์ฟเวอร์สำหรับพัฒนาของคุณโดยตรง
|
||||
|
||||
ทุกอย่างเขียนสคริปต์ได้ผ่าน CLI และ socket API — สร้างเวิร์กสเปซ/แท็บ แบ่งแผง ส่งการกดแป้นพิมพ์ เปิด URL ในเบราว์เซอร์
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux ไม่ได้กำหนดว่านักพัฒนาต้องใช้เครื่องมืออย่างไร มันเป็นเทอร์มินัลและเบราว์เซอร์พร้อม CLI ส่วนที่เหลือขึ้นอยู่กับคุณ
|
||||
|
||||
cmux เป็นส่วนประกอบพื้นฐาน ไม่ใช่โซลูชันสำเร็จรูป มันให้เทอร์มินัล เบราว์เซอร์ การแจ้งเตือน เวิร์กสเปซ แผงแบ่ง แท็บ และ CLI เพื่อควบคุมทั้งหมด cmux ไม่บังคับให้คุณใช้เอเจนต์เขียนโค้ดในแบบที่มีความคิดเห็นตายตัว สิ่งที่คุณสร้างด้วยส่วนประกอบพื้นฐานเหล่านี้เป็นของคุณ
|
||||
|
||||
นักพัฒนาที่ดีที่สุดสร้างเครื่องมือของตัวเองมาตลอด ยังไม่มีใครหาวิธีทำงานกับเอเจนต์ที่ดีที่สุด และทีมที่สร้างผลิตภัณฑ์แบบปิดก็ยังไม่ได้หาเช่นกัน นักพัฒนาที่อยู่ใกล้โค้ดเบสของตัวเองมากที่สุดจะเป็นคนหาคำตอบก่อน
|
||||
|
||||
ให้ส่วนประกอบพื้นฐานที่ประกอบกันได้แก่นักพัฒนาล้านคน แล้วพวกเขาจะร่วมกันค้นพบเวิร์กโฟลว์ที่มีประสิทธิภาพที่สุดได้เร็วกว่าทีมผลิตภัณฑ์ใดจะออกแบบจากบนลงล่าง
|
||||
|
||||
## เอกสารประกอบ
|
||||
|
||||
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการตั้งค่า cmux, [ไปที่เอกสารของเรา](https://cmux.dev/docs/getting-started?utm_source=readme)
|
||||
|
||||
## ปุ่มลัด
|
||||
|
||||
### เวิร์กสเปซ
|
||||
|
|
@ -78,20 +135,21 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | เวิร์กสเปซถัดไป |
|
||||
| ⌃ ⌘ [ | เวิร์กสเปซก่อนหน้า |
|
||||
| ⌘ ⇧ W | ปิดเวิร์กสเปซ |
|
||||
| ⌘ ⇧ R | เปลี่ยนชื่อเวิร์กสเปซ |
|
||||
| ⌘ B | สลับแถบด้านข้าง |
|
||||
|
||||
### Surfaces
|
||||
### เซอร์เฟซ
|
||||
|
||||
| ปุ่มลัด | การทำงาน |
|
||||
|----------|--------|
|
||||
| ⌘ T | Surface ใหม่ |
|
||||
| ⌘ ⇧ ] | Surface ถัดไป |
|
||||
| ⌘ ⇧ [ | Surface ก่อนหน้า |
|
||||
| ⌃ Tab | Surface ถัดไป |
|
||||
| ⌃ ⇧ Tab | Surface ก่อนหน้า |
|
||||
| ⌃ 1–8 | ข้ามไป surface 1–8 |
|
||||
| ⌃ 9 | ข้ามไป surface สุดท้าย |
|
||||
| ⌘ W | ปิด surface |
|
||||
| ⌘ T | เซอร์เฟซใหม่ |
|
||||
| ⌘ ⇧ ] | เซอร์เฟซถัดไป |
|
||||
| ⌘ ⇧ [ | เซอร์เฟซก่อนหน้า |
|
||||
| ⌃ Tab | เซอร์เฟซถัดไป |
|
||||
| ⌃ ⇧ Tab | เซอร์เฟซก่อนหน้า |
|
||||
| ⌃ 1–8 | ข้ามไปเซอร์เฟซ 1–8 |
|
||||
| ⌃ 9 | ข้ามไปเซอร์เฟซสุดท้าย |
|
||||
| ⌘ W | ปิดเซอร์เฟซ |
|
||||
|
||||
### แผงแบ่ง
|
||||
|
||||
|
|
@ -104,6 +162,8 @@ brew upgrade --cask cmux
|
|||
|
||||
### เบราว์เซอร์
|
||||
|
||||
ปุ่มลัดเครื่องมือสำหรับนักพัฒนาของเบราว์เซอร์ใช้ค่าเริ่มต้นของ Safari และสามารถปรับแต่งได้ใน `Settings → Keyboard Shortcuts`
|
||||
|
||||
| ปุ่มลัด | การทำงาน |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | เปิดเบราว์เซอร์ในแผงแบ่ง |
|
||||
|
|
@ -111,7 +171,8 @@ brew upgrade --cask cmux
|
|||
| ⌘ [ | ย้อนกลับ |
|
||||
| ⌘ ] | ไปข้างหน้า |
|
||||
| ⌘ R | โหลดหน้าใหม่ |
|
||||
| ⌥ ⌘ I | เปิดเครื่องมือสำหรับนักพัฒนา |
|
||||
| ⌥ ⌘ I | เปิด/ปิดเครื่องมือสำหรับนักพัฒนา (ค่าเริ่มต้น Safari) |
|
||||
| ⌥ ⌘ C | แสดง JavaScript Console (ค่าเริ่มต้น Safari) |
|
||||
|
||||
### การแจ้งเตือน
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | โหลดการตั้งค่าใหม่ |
|
||||
| ⌘ Q | ออก |
|
||||
|
||||
## บิลด์ Nightly
|
||||
|
||||
[ดาวน์โหลด cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY เป็นแอปแยกต่างหากที่มี bundle ID เป็นของตัวเอง จึงสามารถรันควบคู่กับเวอร์ชันเสถียรได้ สร้างอัตโนมัติจากคอมมิต `main` ล่าสุดและอัปเดตอัตโนมัติผ่านฟีด Sparkle ของตัวเอง
|
||||
|
||||
## การกู้คืนเซสชัน (พฤติกรรมปัจจุบัน)
|
||||
|
||||
เมื่อเปิดใหม่ cmux จะกู้คืนเลย์เอาต์และข้อมูลเมตาของแอปเท่านั้น:
|
||||
- เลย์เอาต์หน้าต่าง/เวิร์กสเปซ/แผง
|
||||
- ไดเรกทอรีทำงาน
|
||||
- ประวัติการเลื่อนของเทอร์มินัล (พยายามอย่างดีที่สุด)
|
||||
- URL ของเบราว์เซอร์และประวัติการนำทาง
|
||||
|
||||
cmux **ไม่**กู้คืนสถานะกระบวนการที่กำลังทำงานภายในแอปเทอร์มินัล ตัวอย่างเช่น เซสชัน Claude Code/tmux/vim ที่กำลังทำงานอยู่จะยังไม่ถูกกู้คืนหลังจากรีสตาร์ท
|
||||
|
||||
## ประวัติดาว
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## การมีส่วนร่วม
|
||||
|
||||
วิธีเข้าร่วม:
|
||||
|
||||
- ติดตามเราบน X สำหรับข่าวสาร [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) และ [@austinywang](https://x.com/austinywang)
|
||||
- เข้าร่วมสนทนาบน [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- สร้างและมีส่วนร่วมใน [GitHub issues](https://github.com/manaflow-ai/cmux/issues) และ [discussions](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- แจ้งให้เรารู้ว่าคุณกำลังสร้างอะไรด้วย cmux
|
||||
|
||||
## ชุมชน
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux เป็นซอฟต์แวร์ฟรี โอเพนซอร์ส และจะเป็นเช่นนั้นตลอดไป หากคุณต้องการสนับสนุนการพัฒนาและเข้าถึงสิ่งที่กำลังจะมาถึงก่อนใคร:
|
||||
|
||||
**[รับ Founder's Edition](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **คำขอฟีเจอร์/แก้ไขบั๊กที่ได้รับความสำคัญ**
|
||||
- **เข้าถึงก่อน: cmux AI ที่ให้บริบทเกี่ยวกับทุกเวิร์กสเปซ แท็บ และแผง**
|
||||
- **เข้าถึงก่อน: แอป iOS ที่ซิงค์เทอร์มินัลระหว่างเดสก์ท็อปและโทรศัพท์**
|
||||
- **เข้าถึงก่อน: Cloud VMs**
|
||||
- **เข้าถึงก่อน: โหมดเสียง**
|
||||
- **iMessage/WhatsApp ส่วนตัวของผม**
|
||||
|
||||
## สัญญาอนุญาต
|
||||
|
||||
โปรเจกต์นี้อยู่ภายใต้สัญญาอนุญาต GNU Affero General Public License v3.0 หรือใหม่กว่า (`AGPL-3.0-or-later`)
|
||||
|
|
|
|||
142
README.tr.md
|
|
@ -1,9 +1,5 @@
|
|||
> Bu çeviri Claude tarafından oluşturulmuştur. İyileştirme önerileriniz varsa lütfen bir PR açın.
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | Türkçe
|
||||
</p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">AI kodlama ajanları için dikey sekmeler ve bildirimler içeren Ghostty tabanlı macOS terminali</p>
|
||||
|
||||
|
|
@ -14,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux ekran görüntüsü" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | Türkçe | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux ekran görüntüsü" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ Demo videosu</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## Özellikler
|
||||
|
||||
- **Dikey sekmeler** — Kenar çubuğu git dalını, çalışma dizinini, dinlenen portları ve en son bildirim metnini gösterir
|
||||
- **Bildirim halkaları** — AI ajanları (Claude Code, OpenCode) dikkatinizi istediğinde paneller mavi bir halka alır ve sekmeler yanar
|
||||
- **Bildirim paneli** — Bekleyen tüm bildirimleri tek bir yerden görün, en son okunmamışa atlayın
|
||||
- **Bölünmüş paneller** — Yatay ve dikey bölmeler
|
||||
- **Uygulama içi tarayıcı** — [agent-browser](https://github.com/vercel-labs/agent-browser)'dan aktarılmış betiklenebilir bir API ile terminalinizin yanında bir tarayıcı bölün
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Bildirim halkaları</h3>
|
||||
Kodlama ajanları dikkatinizi istediğinde paneller mavi bir halka alır ve sekmeler yanar
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="Bildirim halkaları" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Bildirim paneli</h3>
|
||||
Bekleyen tüm bildirimleri tek bir yerden görün, en son okunmamışa atlayın
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="Kenar çubuğu bildirim rozeti" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Uygulama içi tarayıcı</h3>
|
||||
<a href="https://github.com/vercel-labs/agent-browser">agent-browser</a>'dan aktarılmış betiklenebilir bir API ile terminalinizin yanında bir tarayıcı bölün
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="Yerleşik tarayıcı" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>Dikey + yatay sekmeler</h3>
|
||||
Kenar çubuğu git dalını, bağlantılı PR durumunu/numarasını, çalışma dizinini, dinlenen portları ve en son bildirim metnini gösterir. Yatay ve dikey bölmeler.
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="Dikey sekmeler ve bölünmüş paneller" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **Betiklenebilir** — Çalışma alanları oluşturmak, panelleri bölmek, tuş vuruşları göndermek ve tarayıcıyı otomatikleştirmek için CLI ve socket API
|
||||
- **Yerel macOS uygulaması** — Swift ve AppKit ile yapılmıştır, Electron değil. Hızlı başlangıç, düşük bellek kullanımı.
|
||||
- **Ghostty uyumlu** — Temalar, yazı tipleri ve renkler için mevcut `~/.config/ghostty/config` dosyanızı okur
|
||||
|
|
@ -60,12 +103,26 @@ Birçok Claude Code ve Codex oturumunu paralel olarak çalıştırıyorum. Ghost
|
|||
|
||||
Birkaç kodlama orkestratörü denedim ama çoğu Electron/Tauri uygulamasıydı ve performansları beni rahatsız ediyordu. Ayrıca terminali tercih ediyorum çünkü GUI orkestratörleri sizi kendi iş akışlarına kilitliyor. Bu yüzden cmux'u Swift/AppKit'te yerel bir macOS uygulaması olarak geliştirdim. Terminal görüntüleme için libghostty kullanıyor ve temalar, yazı tipleri ve renkler için mevcut Ghostty yapılandırmanızı okuyor.
|
||||
|
||||
Ana eklemeler kenar çubuğu ve bildirim sistemi. Kenar çubuğunda her çalışma alanı için git dalını, çalışma dizinini, dinlenen portları ve en son bildirim metnini gösteren dikey sekmeler var. Bildirim sistemi terminal dizilerini (OSC 9/99/777) yakalıyor ve Claude Code, OpenCode vb. için ajan kancalarına bağlayabileceğiniz bir CLI'ye (`cmux notify`) sahip. Bir ajan beklerken paneli mavi bir halka alıyor ve sekme kenar çubuğunda yanıyor, böylece bölmeler ve sekmeler arasında hangisinin bana ihtiyacı olduğunu görebiliyorum. Cmd+Shift+U en son okunmamışa atlıyor.
|
||||
Ana eklemeler kenar çubuğu ve bildirim sistemi. Kenar çubuğunda her çalışma alanı için git dalını, bağlantılı PR durumunu/numarasını, çalışma dizinini, dinlenen portları ve en son bildirim metnini gösteren dikey sekmeler var. Bildirim sistemi terminal dizilerini (OSC 9/99/777) yakalıyor ve Claude Code, OpenCode vb. için ajan kancalarına bağlayabileceğiniz bir CLI'ye (`cmux notify`) sahip. Bir ajan beklerken paneli mavi bir halka alıyor ve sekme kenar çubuğunda yanıyor, böylece bölmeler ve sekmeler arasında hangisinin bana ihtiyacı olduğunu görebiliyorum. Cmd+Shift+U en son okunmamışa atlıyor.
|
||||
|
||||
Uygulama içi tarayıcının [agent-browser](https://github.com/vercel-labs/agent-browser)'dan aktarılmış betiklenebilir bir API'si var. Ajanlar erişilebilirlik ağacının anlık görüntüsünü alabilir, öğe referansları elde edebilir, tıklayabilir, formları doldurabilir ve JS çalıştırabilir. Terminalinizin yanında bir tarayıcı paneli bölebilir ve Claude Code'un geliştirme sunucunuzla doğrudan etkileşime girmesini sağlayabilirsiniz.
|
||||
|
||||
Her şey CLI ve socket API aracılığıyla betiklenebilir — çalışma alanları/sekmeler oluşturun, panelleri bölün, tuş vuruşları gönderin, tarayıcıda URL'ler açın.
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux, geliştiricilerin araçlarını nasıl kullandığını dikte etmez. Bir terminal ve tarayıcı ile CLI'dir, geri kalanı size kalmış.
|
||||
|
||||
cmux bir ilkel yapıdır, hazır bir çözüm değil. Size bir terminal, bir tarayıcı, bildirimler, çalışma alanları, bölmeler, sekmeler ve hepsini kontrol etmek için bir CLI verir. cmux sizi kodlama ajanlarını belirli bir şekilde kullanmaya zorlamaz. İlkel yapılarla ne inşa edeceğiniz tamamen size aittir.
|
||||
|
||||
En iyi geliştiriciler her zaman kendi araçlarını yapmıştır. Ajanlarla çalışmanın en iyi yolunu henüz kimse bulamadı ve kapalı ürünler geliştiren ekipler de kesinlikle bulamadı. Kendi kod tabanlarına en yakın olan geliştiriciler bunu ilk keşfedenler olacak.
|
||||
|
||||
Bir milyon geliştiriciye birleştirilebilir ilkel yapılar verin, en verimli iş akışlarını herhangi bir ürün ekibinin yukarıdan aşağıya tasarlayabileceğinden daha hızlı bulacaklardır.
|
||||
|
||||
## Dokümantasyon
|
||||
|
||||
cmux'u nasıl yapılandıracağınız hakkında daha fazla bilgi için, [dokümantasyonumuza gidin](https://cmux.dev/docs/getting-started?utm_source=readme).
|
||||
|
||||
## Klavye Kısayolları
|
||||
|
||||
### Çalışma Alanları
|
||||
|
|
@ -78,6 +135,7 @@ Her şey CLI ve socket API aracılığıyla betiklenebilir — çalışma alanla
|
|||
| ⌃ ⌘ ] | Sonraki çalışma alanı |
|
||||
| ⌃ ⌘ [ | Önceki çalışma alanı |
|
||||
| ⌘ ⇧ W | Çalışma alanını kapat |
|
||||
| ⌘ ⇧ R | Çalışma alanını yeniden adlandır |
|
||||
| ⌘ B | Kenar çubuğunu aç/kapat |
|
||||
|
||||
### Surfaces
|
||||
|
|
@ -104,6 +162,8 @@ Her şey CLI ve socket API aracılığıyla betiklenebilir — çalışma alanla
|
|||
|
||||
### Tarayıcı
|
||||
|
||||
Tarayıcı geliştirici araçları kısayolları Safari varsayılanlarını takip eder ve `Settings → Keyboard Shortcuts` bölümünden özelleştirilebilir.
|
||||
|
||||
| Kısayol | Eylem |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | Bölmede tarayıcı aç |
|
||||
|
|
@ -111,7 +171,8 @@ Her şey CLI ve socket API aracılığıyla betiklenebilir — çalışma alanla
|
|||
| ⌘ [ | Geri |
|
||||
| ⌘ ] | İleri |
|
||||
| ⌘ R | Sayfayı yeniden yükle |
|
||||
| ⌥ ⌘ I | Geliştirici Araçlarını aç |
|
||||
| ⌥ ⌘ I | Geliştirici Araçlarını aç/kapat (Safari varsayılanı) |
|
||||
| ⌥ ⌘ C | JavaScript Konsolunu göster (Safari varsayılanı) |
|
||||
|
||||
### Bildirimler
|
||||
|
||||
|
|
@ -148,6 +209,63 @@ Her şey CLI ve socket API aracılığıyla betiklenebilir — çalışma alanla
|
|||
| ⌘ ⇧ , | Yapılandırmayı yeniden yükle |
|
||||
| ⌘ Q | Çıkış |
|
||||
|
||||
## Nightly Sürümler
|
||||
|
||||
[cmux NIGHTLY'i indir](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY, kendi bundle ID'sine sahip ayrı bir uygulamadır, bu yüzden kararlı sürümle yan yana çalışır. En son `main` commit'inden otomatik olarak derlenir ve kendi Sparkle akışı aracılığıyla otomatik güncellenir.
|
||||
|
||||
## Oturum geri yükleme (mevcut davranış)
|
||||
|
||||
Yeniden başlatıldığında, cmux şu anda yalnızca uygulama düzenini ve meta verileri geri yükler:
|
||||
- Pencere/çalışma alanı/panel düzeni
|
||||
- Çalışma dizinleri
|
||||
- Terminal kaydırma geçmişi (en iyi çaba)
|
||||
- Tarayıcı URL'si ve gezinme geçmişi
|
||||
|
||||
cmux, terminal uygulamaları içindeki canlı işlem durumunu geri **yüklemez**. Örneğin, aktif Claude Code/tmux/vim oturumları yeniden başlatma sonrasında henüz devam ettirilmez.
|
||||
|
||||
## Yıldız Geçmişi
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Katkıda Bulunma
|
||||
|
||||
Katılım yolları:
|
||||
|
||||
- Güncellemeler için bizi X'te takip edin [@manaflowai](https://x.com/manaflowai), [@lawrencecchen](https://x.com/lawrencecchen) ve [@austinywang](https://x.com/austinywang)
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)'da sohbete katılın
|
||||
- [GitHub issues](https://github.com/manaflow-ai/cmux/issues) ve [discussions](https://github.com/manaflow-ai/cmux/discussions) oluşturun ve katılın
|
||||
- cmux ile ne inşa ettiğinizi bize bildirin
|
||||
|
||||
## Topluluk
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux ücretsiz, açık kaynak ve her zaman öyle olacak. Geliştirmeyi desteklemek ve sırada ne olduğuna erken erişim almak isterseniz:
|
||||
|
||||
**[Founder's Edition'ı Edinin](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **Öncelikli özellik istekleri/hata düzeltmeleri**
|
||||
- **Erken erişim: Her çalışma alanı, sekme ve panel hakkında bağlam sağlayan cmux AI**
|
||||
- **Erken erişim: Masaüstü ve telefon arasında senkronize terminallere sahip iOS uygulaması**
|
||||
- **Erken erişim: Bulut VM'ler**
|
||||
- **Erken erişim: Sesli mod**
|
||||
- **Kişisel iMessage/WhatsApp'ım**
|
||||
|
||||
## Lisans
|
||||
|
||||
Bu proje GNU Affero Genel Kamu Lisansı v3.0 veya sonrası (`AGPL-3.0-or-later`) ile lisanslanmıştır.
|
||||
|
|
|
|||
140
README.zh-CN.md
|
|
@ -1,7 +1,5 @@
|
|||
> 此翻译由 Claude 生成。如有改进建议,欢迎提交 PR。
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | 简体中文 | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a></p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">基于 Ghostty 的 macOS 终端,带有垂直标签页和为 AI 编程代理设计的通知系统</p>
|
||||
|
||||
|
|
@ -12,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux 截图" width="900" />
|
||||
<a href="README.md">English</a> | 简体中文 | <a href="README.zh-TW.md">繁體中文</a> | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux 截图" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ 演示视频</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **垂直标签页** — 侧边栏显示 git 分支、工作目录、监听端口和最新通知文本
|
||||
- **通知提示环** — 当 AI 代理(Claude Code、OpenCode)需要您注意时,窗格会显示蓝色光环,标签页会高亮
|
||||
- **通知面板** — 在一处查看所有待处理通知,快速跳转到最新未读通知
|
||||
- **分割窗格** — 支持水平和垂直分割
|
||||
- **内置浏览器** — 在终端旁边分割出浏览器窗格,提供从 [agent-browser](https://github.com/vercel-labs/agent-browser) 移植的可脚本化 API
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>通知提示环</h3>
|
||||
当编程代理需要您注意时,窗格会显示蓝色光环,标签页会高亮
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="通知提示环" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>通知面板</h3>
|
||||
在一处查看所有待处理通知,快速跳转到最新未读通知
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="侧边栏通知徽章" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>内置浏览器</h3>
|
||||
在终端旁边分割出浏览器窗格,提供从 <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a> 移植的可脚本化 API
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="内置浏览器" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>垂直 + 水平标签页</h3>
|
||||
侧边栏显示 git 分支、关联 PR 状态/编号、工作目录、监听端口和最新通知文本。支持水平和垂直分割。
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="垂直标签页和分割窗格" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **可脚本化** — 通过 CLI 和 socket API 创建工作区、分割窗格、发送按键和自动化浏览器操作
|
||||
- **原生 macOS 应用** — 使用 Swift 和 AppKit 构建,非 Electron。启动快速,内存占用低。
|
||||
- **兼容 Ghostty** — 读取您现有的 `~/.config/ghostty/config` 配置文件中的主题、字体和颜色设置
|
||||
|
|
@ -58,12 +103,26 @@ brew upgrade --cask cmux
|
|||
|
||||
我试过几个编程协调工具,但大多数都是 Electron/Tauri 应用,性能让我不满意。我也更喜欢终端,因为 GUI 协调工具会把你锁定在它们的工作流里。所以我用 Swift/AppKit 构建了 cmux,作为一个原生 macOS 应用。它使用 libghostty 进行终端渲染,并读取您现有的 Ghostty 配置中的主题、字体和颜色设置。
|
||||
|
||||
主要新增的是侧边栏和通知系统。侧边栏有垂直标签页,显示每个工作区的 git 分支、工作目录、监听端口和最新通知文本。通知系统能捕获终端序列(OSC 9/99/777),并提供 CLI(`cmux notify`),您可以将其接入 Claude Code、OpenCode 等代理的钩子。当代理等待时,其窗格会显示蓝色光环,标签页会在侧边栏高亮,这样我就能在多个分割窗格和标签页之间一眼看出哪个需要我。⌘⇧U 可以跳转到最新的未读通知。
|
||||
主要新增的是侧边栏和通知系统。侧边栏有垂直标签页,显示每个工作区的 git 分支、关联 PR 状态/编号、工作目录、监听端口和最新通知文本。通知系统能捕获终端序列(OSC 9/99/777),并提供 CLI(`cmux notify`),您可以将其接入 Claude Code、OpenCode 等代理的钩子。当代理等待时,其窗格会显示蓝色光环,标签页会在侧边栏高亮,这样我就能在多个分割窗格和标签页之间一眼看出哪个需要我。⌘⇧U 可以跳转到最新的未读通知。
|
||||
|
||||
内置浏览器拥有从 [agent-browser](https://github.com/vercel-labs/agent-browser) 移植的可脚本化 API。代理可以抓取无障碍树快照、获取元素引用、执行点击、填写表单和执行 JS。您可以在终端旁边分割出浏览器窗格,让 Claude Code 直接与您的开发服务器交互。
|
||||
|
||||
所有操作都可以通过 CLI 和 socket API 进行脚本化 — 创建工作区/标签页、分割窗格、发送按键、在浏览器中打开 URL。
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux 不规定开发者应该如何使用工具。它是一个带有 CLI 的终端和浏览器,其余的由你决定。
|
||||
|
||||
cmux 是原语,而非解决方案。它提供终端、浏览器、通知、工作区、分割、标签页,以及控制这一切的 CLI。cmux 不强迫你以特定方式使用编程代理。你用这些原语构建什么,完全取决于你自己。
|
||||
|
||||
最优秀的开发者一直在构建自己的工具。还没有人找到与代理协作的最佳方式,那些构建封闭产品的团队也没有找到。最接近自己代码库的开发者会最先找到答案。
|
||||
|
||||
给一百万个开发者可组合的原语,他们会比任何自上而下设计的产品团队更快地找到最高效的工作流。
|
||||
|
||||
## 文档
|
||||
|
||||
有关 cmux 配置的更多信息,请[查看我们的文档](https://cmux.dev/docs/getting-started?utm_source=readme)。
|
||||
|
||||
## 键盘快捷键
|
||||
|
||||
### 工作区
|
||||
|
|
@ -76,6 +135,7 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | 下一个工作区 |
|
||||
| ⌃ ⌘ [ | 上一个工作区 |
|
||||
| ⌘ ⇧ W | 关闭工作区 |
|
||||
| ⌘ ⇧ R | 重命名工作区 |
|
||||
| ⌘ B | 切换侧边栏 |
|
||||
|
||||
### 界面
|
||||
|
|
@ -102,6 +162,8 @@ brew upgrade --cask cmux
|
|||
|
||||
### 浏览器
|
||||
|
||||
浏览器开发者工具快捷键遵循 Safari 默认设置,可在`设置 → 键盘快捷键`中自定义。
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | 在分割中打开浏览器 |
|
||||
|
|
@ -109,7 +171,8 @@ brew upgrade --cask cmux
|
|||
| ⌘ [ | 后退 |
|
||||
| ⌘ ] | 前进 |
|
||||
| ⌘ R | 刷新页面 |
|
||||
| ⌥ ⌘ I | 打开开发者工具 |
|
||||
| ⌥ ⌘ I | 切换开发者工具(Safari 默认) |
|
||||
| ⌥ ⌘ C | 显示 JavaScript 控制台(Safari 默认) |
|
||||
|
||||
### 通知
|
||||
|
||||
|
|
@ -146,6 +209,63 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | 重新加载配置 |
|
||||
| ⌘ Q | 退出 |
|
||||
|
||||
## 每夜构建
|
||||
|
||||
[下载 cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY 是一个拥有独立 Bundle ID 的单独应用,因此可以与稳定版并行运行。它从最新的 `main` 提交自动构建,并通过独立的 Sparkle 更新源自动更新。
|
||||
|
||||
## 会话恢复(当前行为)
|
||||
|
||||
重新启动时,cmux 目前仅恢复应用布局和元数据:
|
||||
- 窗口/工作区/窗格布局
|
||||
- 工作目录
|
||||
- 终端回滚缓冲区(尽力恢复)
|
||||
- 浏览器 URL 和导航历史
|
||||
|
||||
cmux **不会**恢复终端应用内部的实时进程状态。例如,活动的 Claude Code/tmux/vim 会话在重启后尚无法恢复。
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 参与贡献
|
||||
|
||||
参与方式:
|
||||
|
||||
- 在 X 上关注我们:[@manaflowai](https://x.com/manaflowai)、[@lawrencecchen](https://x.com/lawrencecchen)、[@austinywang](https://x.com/austinywang)
|
||||
- 加入 [Discord](https://discord.gg/xsgFEVrWCZ) 讨论
|
||||
- 创建和参与 [GitHub Issues](https://github.com/manaflow-ai/cmux/issues) 和[讨论](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- 告诉我们您在用 cmux 构建什么
|
||||
|
||||
## 社区
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## Founder's Edition
|
||||
|
||||
cmux 免费、开源,并将一直如此。如果您想支持开发并提前体验即将推出的功能:
|
||||
|
||||
**[获取 Founder's Edition](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **功能请求/Bug 修复优先处理**
|
||||
- **抢先体验:为每个工作区、标签页和面板提供上下文的 cmux AI**
|
||||
- **抢先体验:桌面与手机间终端同步的 iOS 应用**
|
||||
- **抢先体验:云端虚拟机**
|
||||
- **抢先体验:语音模式**
|
||||
- **我的个人 iMessage/WhatsApp**
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 GNU Affero 通用公共许可证 v3.0 或更高版本(`AGPL-3.0-or-later`)授权。
|
||||
|
|
|
|||
140
README.zh-TW.md
|
|
@ -1,7 +1,5 @@
|
|||
> 此翻譯由 Claude 生成。如有改進建議,歡迎提交 PR。
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | 繁體中文 | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a></p>
|
||||
|
||||
<h1 align="center">cmux</h1>
|
||||
<p align="center">基於 Ghostty 的 macOS 終端機,具備垂直分頁和為 AI 程式設計代理設計的通知系統</p>
|
||||
|
||||
|
|
@ -12,16 +10,63 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/screenshot.png" alt="cmux 螢幕截圖" width="900" />
|
||||
<a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a> | 繁體中文 | <a href="README.ko.md">한국어</a> | <a href="README.de.md">Deutsch</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.it.md">Italiano</a> | <a href="README.da.md">Dansk</a> | <a href="README.ja.md">日本語</a> | <a href="README.pl.md">Polski</a> | <a href="README.ru.md">Русский</a> | <a href="README.bs.md">Bosanski</a> | <a href="README.ar.md">العربية</a> | <a href="README.no.md">Norsk</a> | <a href="README.pt-BR.md">Português (Brasil)</a> | <a href="README.th.md">ไทย</a> | <a href="README.tr.md">Türkçe</a> | <a href="README.km.md">ភាសាខ្មែរ</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/manaflowai"><img src="https://img.shields.io/badge/@manaflow-555?logo=x" alt="X / Twitter" /></a>
|
||||
<a href="https://discord.gg/xsgFEVrWCZ"><img src="https://img.shields.io/badge/Discord-555?logo=discord" alt="Discord" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/assets/main-first-image.png" alt="cmux 螢幕截圖" width="900" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=i-WxO5YUTOs">▶ 示範影片</a> · <a href="https://cmux.dev/blog/zen-of-cmux">The Zen of cmux</a>
|
||||
</p>
|
||||
|
||||
## 功能特色
|
||||
|
||||
- **垂直分頁** — 側邊欄顯示 git 分支、工作目錄、監聽連接埠和最新通知文字
|
||||
- **通知提示環** — 當 AI 代理(Claude Code、OpenCode)需要您注意時,窗格會顯示藍色光環,分頁會亮起
|
||||
- **通知面板** — 在同一處檢視所有待處理通知,快速跳轉到最新未讀通知
|
||||
- **分割窗格** — 支援水平和垂直分割
|
||||
- **內建瀏覽器** — 在終端機旁分割出瀏覽器窗格,提供從 [agent-browser](https://github.com/vercel-labs/agent-browser) 移植的可腳本化 API
|
||||
<table>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>通知提示環</h3>
|
||||
當 AI 代理需要您注意時,窗格會顯示藍色光環,分頁會亮起
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/notification-rings.png" alt="通知提示環" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>通知面板</h3>
|
||||
在同一處檢視所有待處理通知,快速跳轉到最新未讀通知
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/sidebar-notification-badge.png" alt="側邊欄通知徽章" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>內建瀏覽器</h3>
|
||||
在終端機旁分割出瀏覽器窗格,提供從 <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a> 移植的可腳本化 API
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/built-in-browser.png" alt="內建瀏覽器" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="40%" valign="middle">
|
||||
<h3>垂直 + 水平分頁</h3>
|
||||
側邊欄顯示 git 分支、關聯的 PR 狀態/編號、工作目錄、監聽連接埠和最新通知文字。支援水平和垂直分割。
|
||||
</td>
|
||||
<td width="60%">
|
||||
<img src="./docs/assets/vertical-horizontal-tabs-and-splits.png" alt="垂直分頁和分割窗格" width="100%" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- **可腳本化** — 透過 CLI 和 socket API 建立工作區、分割窗格、傳送按鍵和自動化瀏覽器操作
|
||||
- **原生 macOS 應用程式** — 使用 Swift 和 AppKit 建構,非 Electron。啟動快速,記憶體佔用低。
|
||||
- **相容 Ghostty** — 讀取您現有的 `~/.config/ghostty/config` 設定檔中的主題、字型和色彩設定
|
||||
|
|
@ -58,12 +103,26 @@ brew upgrade --cask cmux
|
|||
|
||||
我試過幾個程式設計協調工具,但大多數都是 Electron/Tauri 應用程式,效能讓我不滿意。我也更偏好終端機,因為 GUI 協調工具會把你鎖定在它們的工作流程裡。所以我用 Swift/AppKit 建構了 cmux,作為一個原生 macOS 應用程式。它使用 libghostty 進行終端機渲染,並讀取您現有的 Ghostty 設定中的主題、字型和色彩設定。
|
||||
|
||||
主要新增的是側邊欄和通知系統。側邊欄有垂直分頁,顯示每個工作區的 git 分支、工作目錄、監聽連接埠和最新通知文字。通知系統能擷取終端機序列(OSC 9/99/777),並提供 CLI(`cmux notify`),您可以將其接入 Claude Code、OpenCode 等代理的鉤子。當代理等待時,其窗格會顯示藍色光環,分頁會在側邊欄亮起,這樣我就能在多個分割窗格和分頁之間一眼看出哪個需要我。⌘⇧U 可以跳轉到最新的未讀通知。
|
||||
主要新增的是側邊欄和通知系統。側邊欄有垂直分頁,顯示每個工作區的 git 分支、關聯的 PR 狀態/編號、工作目錄、監聽連接埠和最新通知文字。通知系統能擷取終端機序列(OSC 9/99/777),並提供 CLI(`cmux notify`),您可以將其接入 Claude Code、OpenCode 等代理的鉤子。當代理等待時,其窗格會顯示藍色光環,分頁會在側邊欄亮起,這樣我就能在多個分割窗格和分頁之間一眼看出哪個需要我。⌘⇧U 可以跳轉到最新的未讀通知。
|
||||
|
||||
內建瀏覽器擁有從 [agent-browser](https://github.com/vercel-labs/agent-browser) 移植的可腳本化 API。代理可以擷取無障礙樹快照、取得元素參考、執行點擊、填寫表單和執行 JS。您可以在終端機旁分割出瀏覽器窗格,讓 Claude Code 直接與您的開發伺服器互動。
|
||||
|
||||
所有操作都可以透過 CLI 和 socket API 進行腳本化 — 建立工作區/分頁、分割窗格、傳送按鍵、在瀏覽器中開啟 URL。
|
||||
|
||||
## The Zen of cmux
|
||||
|
||||
cmux 不會規定開發者如何使用工具。它是一個帶有 CLI 的終端機和瀏覽器,其餘由您決定。
|
||||
|
||||
cmux 是一個基礎元件,而非完整方案。它提供終端機、瀏覽器、通知、工作區、分割、分頁,以及控制一切的 CLI。cmux 不會強迫您採用特定的方式使用程式設計代理。您用這些基礎元件打造什麼,由您決定。
|
||||
|
||||
最好的開發者一直在打造自己的工具。沒有人知道與代理協作的最佳方式,那些打造封閉產品的團隊也一樣。最了解自己程式碼庫的開發者會最先找到答案。
|
||||
|
||||
給一百萬個開發者可組合的基礎元件,他們會比任何自上而下設計的產品團隊更快地集體找到最高效的工作流程。
|
||||
|
||||
## 文件
|
||||
|
||||
如需更多 cmux 設定資訊,[請前往我們的文件](https://cmux.dev/docs/getting-started?utm_source=readme)。
|
||||
|
||||
## 鍵盤快捷鍵
|
||||
|
||||
### 工作區
|
||||
|
|
@ -76,6 +135,7 @@ brew upgrade --cask cmux
|
|||
| ⌃ ⌘ ] | 下一個工作區 |
|
||||
| ⌃ ⌘ [ | 上一個工作區 |
|
||||
| ⌘ ⇧ W | 關閉工作區 |
|
||||
| ⌘ ⇧ R | 重新命名工作區 |
|
||||
| ⌘ B | 切換側邊欄 |
|
||||
|
||||
### 介面
|
||||
|
|
@ -102,6 +162,8 @@ brew upgrade --cask cmux
|
|||
|
||||
### 瀏覽器
|
||||
|
||||
瀏覽器開發者工具快捷鍵遵循 Safari 預設設定,可在 `設定 → 鍵盤快捷鍵` 中自訂。
|
||||
|
||||
| 快捷鍵 | 動作 |
|
||||
|----------|--------|
|
||||
| ⌘ ⇧ L | 在分割中開啟瀏覽器 |
|
||||
|
|
@ -109,7 +171,8 @@ brew upgrade --cask cmux
|
|||
| ⌘ [ | 後退 |
|
||||
| ⌘ ] | 前進 |
|
||||
| ⌘ R | 重新整理頁面 |
|
||||
| ⌥ ⌘ I | 開啟開發者工具 |
|
||||
| ⌥ ⌘ I | 切換開發者工具(Safari 預設) |
|
||||
| ⌥ ⌘ C | 顯示 JavaScript 主控台(Safari 預設) |
|
||||
|
||||
### 通知
|
||||
|
||||
|
|
@ -146,6 +209,63 @@ brew upgrade --cask cmux
|
|||
| ⌘ ⇧ , | 重新載入設定 |
|
||||
| ⌘ Q | 結束 |
|
||||
|
||||
## 每夜建構
|
||||
|
||||
[下載 cmux NIGHTLY](https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg)
|
||||
|
||||
cmux NIGHTLY 是一個獨立的應用程式,擁有自己的 bundle ID,因此可以與穩定版並行執行。每次從最新的 `main` 提交自動建構,並透過自己的 Sparkle 來源自動更新。
|
||||
|
||||
## 工作階段還原(目前行為)
|
||||
|
||||
重新啟動時,cmux 目前僅還原應用程式佈局和中繼資料:
|
||||
- 視窗/工作區/窗格佈局
|
||||
- 工作目錄
|
||||
- 終端機捲動緩衝區(盡力而為)
|
||||
- 瀏覽器 URL 和瀏覽歷程
|
||||
|
||||
cmux **不會**還原終端機應用程式內的即時程序狀態。例如,活躍的 Claude Code/tmux/vim 工作階段在重新啟動後尚無法恢復。
|
||||
|
||||
## Star 歷史
|
||||
|
||||
<a href="https://star-history.com/#manaflow-ai/cmux&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=manaflow-ai/cmux&type=Date" width="600" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 參與貢獻
|
||||
|
||||
參與方式:
|
||||
|
||||
- 在 X 上追蹤我們獲取最新動態 [@manaflowai](https://x.com/manaflowai)、[@lawrencecchen](https://x.com/lawrencecchen) 和 [@austinywang](https://x.com/austinywang)
|
||||
- 加入 [Discord](https://discord.gg/xsgFEVrWCZ) 上的討論
|
||||
- 建立和參與 [GitHub issues](https://github.com/manaflow-ai/cmux/issues) 和 [discussions](https://github.com/manaflow-ai/cmux/discussions)
|
||||
- 讓我們知道您正在用 cmux 打造什麼
|
||||
|
||||
## 社群
|
||||
|
||||
- [Discord](https://discord.gg/xsgFEVrWCZ)
|
||||
- [GitHub](https://github.com/manaflow-ai/cmux)
|
||||
- [X / Twitter](https://twitter.com/manaflowai)
|
||||
- [YouTube](https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw)
|
||||
- [LinkedIn](https://www.linkedin.com/company/manaflow-ai/)
|
||||
- [Reddit](https://www.reddit.com/r/cmux/)
|
||||
|
||||
## 創始版
|
||||
|
||||
cmux 免費、開源,且將永遠如此。如果您想支持開發並提前體驗即將推出的功能:
|
||||
|
||||
**[取得創始版](https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q)**
|
||||
|
||||
- **優先處理的功能請求/錯誤修復**
|
||||
- **搶先體驗:cmux AI 為您提供每個工作區、分頁和面板的上下文資訊**
|
||||
- **搶先體驗:iOS 應用程式,終端機在桌面和手機之間同步**
|
||||
- **搶先體驗:雲端虛擬機**
|
||||
- **搶先體驗:語音模式**
|
||||
- **我的個人 iMessage/WhatsApp**
|
||||
|
||||
## 授權條款
|
||||
|
||||
本專案採用 GNU Affero 通用公共授權條款 v3.0 或更新版本(`AGPL-3.0-or-later`)授權。
|
||||
|
|
|
|||
|
|
@ -26,8 +26,32 @@
|
|||
<string></string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string></string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>A program running within cmux would like to use your microphone.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>A program running within cmux would like to use your camera.</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).web</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>OSAScriptingDefinition</key>
|
||||
<string>cmux.sdef</string>
|
||||
<key>NSServices</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
|
@ -69,27 +93,15 @@
|
|||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.splittabbar.tabtransfer</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Bonsplit Tab Transfer</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.cmux.sidebar-tab-reorder</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>cmux Sidebar Tab Reorder</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
|
|
|||
362
Resources/InfoPlist.xcstrings
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
{
|
||||
"sourceLanguage": "en",
|
||||
"version": "1.0",
|
||||
"strings": {
|
||||
"NSCameraUsageDescription": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "A program running within cmux would like to use your camera."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux 内で実行中のプログラムがカメラの使用を求めています。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NSMicrophoneUsageDescription": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "A program running within cmux would like to use your microphone."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux 内で実行中のプログラムがマイクの使用を求めています。"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在 cmux 中运行的程序想要使用您的麦克风。"
|
||||
}
|
||||
},
|
||||
"zh-Hant": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在 cmux 中執行的程式想要使用您的麥克風。"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux 내에서 실행 중인 프로그램이 마이크를 사용하려고 합니다."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ein in cmux ausgeführtes Programm möchte Ihr Mikrofon verwenden."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Un programa en ejecución dentro de cmux desea usar tu micrófono."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Un programme s'exécutant dans cmux souhaite utiliser votre microphone."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Un programma in esecuzione in cmux desidera utilizzare il microfono."
|
||||
}
|
||||
},
|
||||
"da": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Et program, der kører i cmux, vil gerne bruge din mikrofon."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Program działający w cmux chciałby użyć Twojego mikrofonu."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Программа, запущенная в cmux, хотела бы использовать ваш микрофон."
|
||||
}
|
||||
},
|
||||
"bs": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Program koji se izvršava unutar cmux želi koristiti vaš mikrofon."
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "يرغب برنامج يعمل داخل cmux في استخدام الميكروفون."
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Et program som kjører i cmux ønsker å bruke mikrofonen din."
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Um programa em execução no cmux gostaria de usar seu microfone."
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "โปรแกรมที่ทำงานภายใน cmux ต้องการใช้ไมโครโฟนของคุณ"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmux içinde çalışan bir program mikrofonunuzu kullanmak istiyor."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New $(PRODUCT_NAME) Workspace Here": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "New $(PRODUCT_NAME) Workspace Here"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ここに新規 $(PRODUCT_NAME) ワークスペースを作成"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在此新建 $(PRODUCT_NAME) 工作区"
|
||||
}
|
||||
},
|
||||
"zh-Hant": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在此新增 $(PRODUCT_NAME) 工作區"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "여기에 새 $(PRODUCT_NAME) 작업 공간"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Neuer $(PRODUCT_NAME)-Arbeitsbereich hier"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nuevo espacio de trabajo de $(PRODUCT_NAME) aquí"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nouvel espace de travail $(PRODUCT_NAME) ici"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nuova area di lavoro $(PRODUCT_NAME) qui"
|
||||
}
|
||||
},
|
||||
"da": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nyt $(PRODUCT_NAME)-arbejdsområde her"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nowa przestrzeń robocza $(PRODUCT_NAME) tutaj"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Новое рабочее пространство $(PRODUCT_NAME) здесь"
|
||||
}
|
||||
},
|
||||
"bs": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Novi $(PRODUCT_NAME) radni prostor ovdje"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "مساحة عمل $(PRODUCT_NAME) جديدة هنا"
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nytt $(PRODUCT_NAME)-arbeidsområde her"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nova Área de Trabalho do $(PRODUCT_NAME) Aqui"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "เวิร์กสเปซ $(PRODUCT_NAME) ใหม่ที่นี่"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Buraya Yeni $(PRODUCT_NAME) Çalışma Alanı"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New $(PRODUCT_NAME) Window Here": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "New $(PRODUCT_NAME) Window Here"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ここに新規 $(PRODUCT_NAME) ウインドウを作成"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在此新建 $(PRODUCT_NAME) 窗口"
|
||||
}
|
||||
},
|
||||
"zh-Hant": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "在此新增 $(PRODUCT_NAME) 視窗"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "여기에 새 $(PRODUCT_NAME) 윈도우"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Neues $(PRODUCT_NAME)-Fenster hier"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nueva ventana de $(PRODUCT_NAME) aquí"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nouvelle fenêtre $(PRODUCT_NAME) ici"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nuova finestra $(PRODUCT_NAME) qui"
|
||||
}
|
||||
},
|
||||
"da": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nyt $(PRODUCT_NAME)-vindue her"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nowe okno $(PRODUCT_NAME) tutaj"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Новое окно $(PRODUCT_NAME) здесь"
|
||||
}
|
||||
},
|
||||
"bs": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Novi $(PRODUCT_NAME) prozor ovdje"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "نافذة $(PRODUCT_NAME) جديدة هنا"
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nytt $(PRODUCT_NAME)-vindu her"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nova Janela do $(PRODUCT_NAME) Aqui"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "หน้าต่าง $(PRODUCT_NAME) ใหม่ที่นี่"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Buraya Yeni $(PRODUCT_NAME) Penceresi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73115
Resources/Localizable.xcstrings
Normal file
|
|
@ -18,8 +18,36 @@ find_real_claude() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# Pass through if not in a cmux terminal or hooks are disabled.
|
||||
if [[ -z "$CMUX_SURFACE_ID" || "$CMUX_CLAUDE_HOOKS_DISABLED" == "1" ]]; then
|
||||
# Return 0 only when CMUX_SOCKET_PATH points to a live cmux socket.
|
||||
cmux_socket_available() {
|
||||
local socket="${CMUX_SOCKET_PATH:-}"
|
||||
[[ -n "$socket" && -S "$socket" ]] || return 1
|
||||
|
||||
local self_dir cmux_bin
|
||||
self_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||
cmux_bin="$self_dir/cmux"
|
||||
[[ -x "$cmux_bin" ]] || cmux_bin="$(command -v cmux || true)"
|
||||
[[ -n "$cmux_bin" ]] || return 1
|
||||
|
||||
# Keep stale/hung socket checks bounded so claude startup does not block
|
||||
# behind the CLI default timeout (15s).
|
||||
CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.75 \
|
||||
"$cmux_bin" --socket "$socket" ping >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Pass through if not in a cmux terminal, hooks are disabled, or the cmux
|
||||
# socket is unavailable (stale env / app not running).
|
||||
IN_CMUX=0
|
||||
if [[ -n "$CMUX_SURFACE_ID" ]]; then
|
||||
IN_CMUX=1
|
||||
fi
|
||||
|
||||
if [[ "$IN_CMUX" == "0" || "$CMUX_CLAUDE_HOOKS_DISABLED" == "1" ]] || ! cmux_socket_available; then
|
||||
# In cmux-launched shells, preserve old behavior and always clear nested
|
||||
# Claude session markers, even when we must pass through due to stale socket.
|
||||
if [[ "$IN_CMUX" == "1" ]]; then
|
||||
unset CLAUDECODE
|
||||
fi
|
||||
REAL_CLAUDE="$(find_real_claude)" || { echo "Error: claude not found in PATH" >&2; exit 127; }
|
||||
exec "$REAL_CLAUDE" "$@"
|
||||
fi
|
||||
|
|
|
|||
466
Resources/bin/open
Executable file
|
|
@ -0,0 +1,466 @@
|
|||
#!/usr/bin/env bash
|
||||
# cmux open wrapper - routes HTTP(S) URLs to cmux's in-app browser
|
||||
#
|
||||
# When running inside a cmux terminal (CMUX_SOCKET_PATH is set), this wrapper
|
||||
# intercepts `open https://...` invocations and opens them in cmux's built-in
|
||||
# browser within the same workspace. All other arguments pass through to
|
||||
# /usr/bin/open unchanged.
|
||||
|
||||
SYSTEM_OPEN_BIN="${CMUX_OPEN_WRAPPER_SYSTEM_OPEN:-/usr/bin/open}"
|
||||
DEFAULTS_BIN="${CMUX_OPEN_WRAPPER_DEFAULTS:-/usr/bin/defaults}"
|
||||
PYTHON3_BIN="${CMUX_OPEN_WRAPPER_PYTHON3:-}"
|
||||
|
||||
if [[ ! -x "$SYSTEM_OPEN_BIN" ]]; then
|
||||
SYSTEM_OPEN_BIN="/usr/bin/open"
|
||||
fi
|
||||
|
||||
if [[ ! -x "$DEFAULTS_BIN" ]]; then
|
||||
DEFAULTS_BIN="/usr/bin/defaults"
|
||||
fi
|
||||
|
||||
if [[ -n "$PYTHON3_BIN" ]]; then
|
||||
if [[ ! -x "$PYTHON3_BIN" ]]; then
|
||||
PYTHON3_BIN=""
|
||||
fi
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON3_BIN="$(command -v python3)"
|
||||
fi
|
||||
|
||||
settings_domain="${CMUX_BUNDLE_ID:-}"
|
||||
whitelist_raw=""
|
||||
whitelist_patterns=()
|
||||
|
||||
system_open() {
|
||||
exec "$SYSTEM_OPEN_BIN" "$@"
|
||||
}
|
||||
|
||||
trim() {
|
||||
local value="$1"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
to_lower_ascii() {
|
||||
# Bash 3.2-compatible lowercase conversion.
|
||||
LC_ALL=C printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
normalize_boolean() {
|
||||
to_lower_ascii "$(trim "$1")"
|
||||
}
|
||||
|
||||
is_false_setting() {
|
||||
local normalized
|
||||
normalized="$(normalize_boolean "$1")"
|
||||
case "$normalized" in
|
||||
0|false|no|off)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
canonicalize_idn_host() {
|
||||
local value="$1"
|
||||
[[ -z "$PYTHON3_BIN" ]] && {
|
||||
printf '%s' "$value"
|
||||
return 0
|
||||
}
|
||||
|
||||
local canonicalized
|
||||
canonicalized="$("$PYTHON3_BIN" - "$value" <<'PY' 2>/dev/null || true
|
||||
import sys
|
||||
|
||||
host = sys.argv[1].strip().rstrip(".")
|
||||
if not host:
|
||||
raise SystemExit(1)
|
||||
|
||||
labels = host.split(".")
|
||||
if any(not label for label in labels):
|
||||
raise SystemExit(1)
|
||||
|
||||
try:
|
||||
canonical = ".".join(label.encode("idna").decode("ascii") for label in labels)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
|
||||
sys.stdout.write(canonical.lower())
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$canonicalized" ]]; then
|
||||
printf '%s' "$canonicalized"
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
is_http_url() {
|
||||
local value="$1"
|
||||
case "$value" in
|
||||
[Hh][Tt][Tt][Pp]://*|[Hh][Tt][Tt][Pp][Ss]://*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_file_url() {
|
||||
local value="$1"
|
||||
case "$value" in
|
||||
[Ff][Ii][Ll][Ee]://*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
has_uri_scheme() {
|
||||
local value="$1"
|
||||
[[ "$value" =~ ^[A-Za-z][A-Za-z0-9+.-]*: ]]
|
||||
}
|
||||
|
||||
is_html_extension() {
|
||||
local value
|
||||
value="$(to_lower_ascii "$(trim "$1")")"
|
||||
case "$value" in
|
||||
*.html|*.htm)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_explicit_local_path() {
|
||||
local value="$1"
|
||||
case "$value" in
|
||||
/*|./*|../*|~|~/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
file_url_points_to_html() {
|
||||
local value="$1"
|
||||
if [[ -n "$PYTHON3_BIN" ]]; then
|
||||
"$PYTHON3_BIN" - "$value" <<'PY' >/dev/null 2>&1
|
||||
import sys
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
value = sys.argv[1].strip()
|
||||
if not value:
|
||||
raise SystemExit(1)
|
||||
|
||||
parts = urlsplit(value)
|
||||
path = unquote(parts.path or "")
|
||||
lower = path.lower()
|
||||
if lower.endswith(".html") or lower.endswith(".htm"):
|
||||
raise SystemExit(0)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
return $?
|
||||
fi
|
||||
|
||||
local without_fragment="${value%%\#*}"
|
||||
local without_query="${without_fragment%%\?*}"
|
||||
local remainder path_part
|
||||
|
||||
case "$without_query" in
|
||||
[Ff][Ii][Ll][Ee]://*)
|
||||
remainder="${without_query#*://}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$remainder" == /* ]]; then
|
||||
path_part="$remainder"
|
||||
elif [[ "$remainder" == */* ]]; then
|
||||
path_part="/${remainder#*/}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
is_html_extension "$path_part"
|
||||
}
|
||||
|
||||
path_to_file_url_without_python() {
|
||||
local raw="$1"
|
||||
local expanded="$raw"
|
||||
case "$expanded" in
|
||||
"~")
|
||||
expanded="$HOME"
|
||||
;;
|
||||
"~/"*)
|
||||
expanded="$HOME/${expanded#~/}"
|
||||
;;
|
||||
esac
|
||||
|
||||
local absolute
|
||||
if [[ "$expanded" == /* ]]; then
|
||||
absolute="$expanded"
|
||||
else
|
||||
absolute="$(pwd)/$expanded"
|
||||
fi
|
||||
|
||||
local directory="$absolute"
|
||||
local basename=""
|
||||
if [[ "$absolute" == */* ]]; then
|
||||
directory="${absolute%/*}"
|
||||
basename="${absolute##*/}"
|
||||
fi
|
||||
|
||||
local resolved_directory
|
||||
if resolved_directory="$(cd "$directory" 2>/dev/null && pwd -P)"; then
|
||||
absolute="$resolved_directory"
|
||||
if [[ -n "$basename" ]]; then
|
||||
absolute="$absolute/$basename"
|
||||
fi
|
||||
fi
|
||||
|
||||
local encoded=""
|
||||
local length=${#absolute}
|
||||
local index char hex
|
||||
local LC_ALL=C
|
||||
for ((index = 0; index < length; index++)); do
|
||||
char="${absolute:index:1}"
|
||||
case "$char" in
|
||||
[a-zA-Z0-9.~_-]|/)
|
||||
encoded+="$char"
|
||||
;;
|
||||
*)
|
||||
printf -v hex '%02X' "'$char"
|
||||
encoded+="%$hex"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf 'file://%s\n' "$encoded"
|
||||
}
|
||||
|
||||
path_to_file_url() {
|
||||
local raw="$1"
|
||||
if [[ -n "$PYTHON3_BIN" ]]; then
|
||||
local converted
|
||||
if converted="$("$PYTHON3_BIN" - "$raw" <<'PY' 2>/dev/null
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
raw = sys.argv[1]
|
||||
if not raw:
|
||||
raise SystemExit(1)
|
||||
|
||||
path = pathlib.Path(raw).expanduser()
|
||||
if path.is_absolute():
|
||||
resolved = path.resolve(strict=False)
|
||||
else:
|
||||
resolved = (pathlib.Path.cwd() / path).resolve(strict=False)
|
||||
|
||||
sys.stdout.write(resolved.as_uri())
|
||||
PY
|
||||
)"; then
|
||||
printf '%s\n' "$converted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
path_to_file_url_without_python "$raw"
|
||||
}
|
||||
|
||||
normalize_host() {
|
||||
local value
|
||||
value="$(trim "$1")"
|
||||
value="$(to_lower_ascii "$value")"
|
||||
[[ -z "$value" ]] && return 1
|
||||
|
||||
if [[ "$value" == *"://"* ]]; then
|
||||
value="${value#*://}"
|
||||
fi
|
||||
|
||||
value="${value%%/*}"
|
||||
value="${value%%\?*}"
|
||||
value="${value%%\#*}"
|
||||
|
||||
if [[ "$value" == *"@"* ]]; then
|
||||
value="${value##*@}"
|
||||
fi
|
||||
|
||||
if [[ "$value" == \[* ]]; then
|
||||
value="${value#\[}"
|
||||
value="${value%%\]*}"
|
||||
elif [[ "$value" == *:* ]]; then
|
||||
local colons="${value//[^:]}"
|
||||
if [[ ${#colons} -eq 1 ]] && [[ "$value" =~ :[0-9]+$ ]]; then
|
||||
value="${value%:*}"
|
||||
fi
|
||||
fi
|
||||
|
||||
while [[ "$value" == .* ]]; do
|
||||
value="${value#.}"
|
||||
done
|
||||
while [[ "$value" == *. ]]; do
|
||||
value="${value%.}"
|
||||
done
|
||||
|
||||
[[ -z "$value" ]] && return 1
|
||||
value="$(canonicalize_idn_host "$value")"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
normalize_whitelist_pattern() {
|
||||
local value
|
||||
value="$(trim "$1")"
|
||||
value="$(to_lower_ascii "$value")"
|
||||
[[ -z "$value" ]] && return 1
|
||||
|
||||
if [[ "$value" == \*.* ]]; then
|
||||
local suffix
|
||||
suffix="$(normalize_host "${value#*.}")" || return 1
|
||||
printf '*.%s' "$suffix"
|
||||
return 0
|
||||
fi
|
||||
|
||||
normalize_host "$value"
|
||||
}
|
||||
|
||||
host_matches_pattern() {
|
||||
local host="$1"
|
||||
local pattern="$2"
|
||||
|
||||
if [[ "$pattern" == \*.* ]]; then
|
||||
local suffix="${pattern#*.}"
|
||||
[[ "$host" == "$suffix" ]] && return 0
|
||||
[[ "$host" == *".$suffix" ]] && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ "$host" == "$pattern" ]]
|
||||
}
|
||||
|
||||
host_matches_whitelist() {
|
||||
local url="$1"
|
||||
if [[ ${#whitelist_patterns[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local host
|
||||
host="$(normalize_host "$url")" || return 1
|
||||
for pattern in "${whitelist_patterns[@]}"; do
|
||||
if host_matches_pattern "$host" "$pattern"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
load_whitelist_patterns() {
|
||||
local raw="$1"
|
||||
local line
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
local normalized
|
||||
normalized="$(normalize_whitelist_pattern "$line")" || continue
|
||||
whitelist_patterns+=("$normalized")
|
||||
done <<< "$raw"
|
||||
}
|
||||
|
||||
# Pass through immediately if not in a cmux terminal.
|
||||
if [[ -z "$CMUX_SOCKET_PATH" ]]; then
|
||||
system_open "$@"
|
||||
fi
|
||||
|
||||
# No arguments → pass through.
|
||||
if [[ $# -eq 0 ]]; then
|
||||
system_open "$@"
|
||||
fi
|
||||
|
||||
# Scan for flags that indicate explicit user intent → pass through.
|
||||
# Also collect non-flag arguments and route eligible browser targets to cmux.
|
||||
passthrough=false
|
||||
cmux_targets=()
|
||||
passthrough_args=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-a|-b|-R|-e|-t|-f|-W|-g|-n|-h|-s|-j|-u|--env|--stdin|--stdout|--stderr)
|
||||
passthrough=true
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
# Unknown flag → be conservative, pass through
|
||||
passthrough=true
|
||||
break
|
||||
;;
|
||||
*)
|
||||
if is_http_url "$arg"; then
|
||||
cmux_targets+=("$arg")
|
||||
elif is_file_url "$arg"; then
|
||||
if file_url_points_to_html "$arg"; then
|
||||
cmux_targets+=("$arg")
|
||||
else
|
||||
passthrough_args+=("$arg")
|
||||
fi
|
||||
elif has_uri_scheme "$arg"; then
|
||||
passthrough_args+=("$arg")
|
||||
elif is_html_extension "$arg"; then
|
||||
if is_explicit_local_path "$arg" || [[ -e "$arg" ]]; then
|
||||
if local_file_url="$(path_to_file_url "$arg")"; then
|
||||
cmux_targets+=("$local_file_url")
|
||||
else
|
||||
passthrough_args+=("$arg")
|
||||
fi
|
||||
else
|
||||
passthrough_args+=("$arg")
|
||||
fi
|
||||
else
|
||||
passthrough_args+=("$arg")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$passthrough" == true ]] || [[ ${#cmux_targets[@]} -eq 0 ]]; then
|
||||
system_open "$@"
|
||||
fi
|
||||
|
||||
# Respect the same settings used for terminal link clicks.
|
||||
if [[ -n "$settings_domain" ]]; then
|
||||
open_in_cmux="$("$DEFAULTS_BIN" read "$settings_domain" browserInterceptTerminalOpenCommandInCmuxBrowser 2>/dev/null || true)"
|
||||
if [[ -z "$open_in_cmux" ]]; then
|
||||
# Backward compatibility for installs that predate the dedicated open-wrapper toggle.
|
||||
open_in_cmux="$("$DEFAULTS_BIN" read "$settings_domain" browserOpenTerminalLinksInCmuxBrowser 2>/dev/null || true)"
|
||||
fi
|
||||
if is_false_setting "$open_in_cmux"; then
|
||||
system_open "$@"
|
||||
fi
|
||||
|
||||
whitelist_raw="$("$DEFAULTS_BIN" read "$settings_domain" browserHostWhitelist 2>/dev/null || true)"
|
||||
if [[ -n "$whitelist_raw" ]]; then
|
||||
load_whitelist_patterns "$whitelist_raw"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# Find cmux CLI (same directory as this script).
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CMUX_CLI="$SELF_DIR/cmux"
|
||||
|
||||
if [[ ! -x "$CMUX_CLI" ]]; then
|
||||
system_open "$@"
|
||||
fi
|
||||
|
||||
# Open each URL in cmux's in-app browser; track failures individually.
|
||||
# External-open pattern rules are evaluated in-app (NSRegularExpression) so
|
||||
# terminal link clicks and intercepted `open` commands share one regex dialect.
|
||||
failed_urls=()
|
||||
for url in "${cmux_targets[@]}"; do
|
||||
if is_http_url "$url" && ! host_matches_whitelist "$url"; then
|
||||
failed_urls+=("$url")
|
||||
continue
|
||||
fi
|
||||
CMUX_RESPECT_EXTERNAL_OPEN_RULES=1 "$CMUX_CLI" browser open "$url" 2>/dev/null || failed_urls+=("$url")
|
||||
done
|
||||
|
||||
# Fall back to system open for unmatched args and URLs that failed.
|
||||
if [[ ${#passthrough_args[@]} -gt 0 ]] || [[ ${#failed_urls[@]} -gt 0 ]]; then
|
||||
system_open "${passthrough_args[@]}" "${failed_urls[@]}"
|
||||
fi
|
||||
192
Resources/cmux.sdef
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
|
||||
|
||||
<dictionary title="cmux Scripting Dictionary">
|
||||
<suite name="cmux Suite" code="Cmux" description="cmux scripting support.">
|
||||
<class name="application" code="capp" description="The cmux application.">
|
||||
<cocoa class="NSApplication"/>
|
||||
<property name="name" code="pnam" type="text" access="r" description="The name of the application."/>
|
||||
<property name="frontmost" code="pisf" type="boolean" access="r" description="Is this the active application?">
|
||||
<cocoa key="isActive"/>
|
||||
</property>
|
||||
<property name="front window" code="CMFW" type="window" access="r" description="The frontmost cmux window.">
|
||||
<cocoa key="frontWindow"/>
|
||||
</property>
|
||||
<property name="version" code="vers" type="text" access="r" description="The version number of the application."/>
|
||||
<responds-to command="perform action">
|
||||
<cocoa method="handlePerformActionScriptCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="new window">
|
||||
<cocoa method="handleNewWindowScriptCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="new tab">
|
||||
<cocoa method="handleNewTabScriptCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="quit">
|
||||
<cocoa method="handleQuitScriptCommand:"/>
|
||||
</responds-to>
|
||||
|
||||
<element type="window" access="r">
|
||||
<cocoa key="scriptWindows"/>
|
||||
</element>
|
||||
|
||||
<element type="terminal" access="r">
|
||||
<cocoa key="terminals"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
<class name="window" code="CMwn" plural="windows" description="A cmux window containing one or more workspaces.">
|
||||
<cocoa class="CmuxScriptWindow"/>
|
||||
<property name="id" code="ID " type="text" access="r" description="Stable ID for this window."/>
|
||||
<property name="name" code="pnam" type="text" access="r" description="The title of the window.">
|
||||
<cocoa key="title"/>
|
||||
</property>
|
||||
<property name="selected tab" code="CMsT" type="tab" access="r" description="The selected workspace in this window.">
|
||||
<cocoa key="selectedTab"/>
|
||||
</property>
|
||||
<responds-to command="activate window">
|
||||
<cocoa method="handleActivateWindowCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="close window">
|
||||
<cocoa method="handleCloseWindowCommand:"/>
|
||||
</responds-to>
|
||||
<element type="tab" access="r">
|
||||
<cocoa key="tabs"/>
|
||||
</element>
|
||||
<element type="terminal" access="r">
|
||||
<cocoa key="terminals"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
<class name="tab" code="CMtb" plural="tabs" description="A cmux workspace.">
|
||||
<cocoa class="CmuxScriptTab"/>
|
||||
<property name="id" code="ID " type="text" access="r" description="Stable ID for this workspace."/>
|
||||
<property name="name" code="pnam" type="text" access="r" description="The title of the workspace.">
|
||||
<cocoa key="title"/>
|
||||
</property>
|
||||
<property name="index" code="pidx" type="integer" access="r" description="1-based index of this workspace in its window."/>
|
||||
<property name="selected" code="CMsl" type="boolean" access="r" description="Whether this workspace is selected in its window."/>
|
||||
<property name="focused terminal" code="CMfT" type="terminal" access="r" description="The currently focused terminal panel in this workspace.">
|
||||
<cocoa key="focusedTerminal"/>
|
||||
</property>
|
||||
<responds-to command="select tab">
|
||||
<cocoa method="handleSelectTabCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="close tab">
|
||||
<cocoa method="handleCloseTabCommand:"/>
|
||||
</responds-to>
|
||||
<element type="terminal" access="r">
|
||||
<cocoa key="terminals"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
<class name="terminal" code="CMtr" plural="terminals" description="An individual terminal panel.">
|
||||
<cocoa class="CmuxScriptTerminal"/>
|
||||
<property name="id" code="ID " type="text" access="r" description="Stable ID for this terminal panel."/>
|
||||
<property name="name" code="pnam" type="text" access="r" description="Current terminal title.">
|
||||
<cocoa key="title"/>
|
||||
</property>
|
||||
<property name="working directory" code="CMwd" type="text" access="r" description="Current working directory for the terminal process.">
|
||||
<cocoa key="workingDirectory"/>
|
||||
</property>
|
||||
<responds-to command="split">
|
||||
<cocoa method="handleSplitCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="focus">
|
||||
<cocoa method="handleFocusCommand:"/>
|
||||
</responds-to>
|
||||
<responds-to command="close">
|
||||
<cocoa method="handleCloseCommand:"/>
|
||||
</responds-to>
|
||||
</class>
|
||||
|
||||
<enumeration name="split direction" code="CMSD" description="Direction for a new split.">
|
||||
<enumerator name="right" code="GSrt" description="Split to the right."/>
|
||||
<enumerator name="left" code="GSlf" description="Split to the left."/>
|
||||
<enumerator name="down" code="GSdn" description="Split downward."/>
|
||||
<enumerator name="up" code="GSup" description="Split upward."/>
|
||||
</enumeration>
|
||||
|
||||
<command name="perform action" code="CmuxPfAc" description="Perform a Ghostty action string on a terminal.">
|
||||
<direct-parameter type="text" description="The Ghostty action string."/>
|
||||
<parameter name="on" code="CMoT" type="terminal" description="Target terminal.">
|
||||
<cocoa key="on"/>
|
||||
</parameter>
|
||||
<result type="boolean" description="True when the action was performed."/>
|
||||
</command>
|
||||
|
||||
<command name="new window" code="CmuxNWin" description="Create a new cmux window.">
|
||||
<result type="window" description="The newly created window."/>
|
||||
</command>
|
||||
|
||||
<command name="new tab" code="CmuxNTab" description="Create a new workspace.">
|
||||
<parameter name="in" code="CMtW" type="window" optional="yes" description="Target window for the new workspace.">
|
||||
<cocoa key="window"/>
|
||||
</parameter>
|
||||
<result type="tab" description="The newly created workspace."/>
|
||||
</command>
|
||||
|
||||
<command name="split" code="CmuxSplt" description="Split a terminal in the given direction.">
|
||||
<direct-parameter type="specifier" description="The terminal to split."/>
|
||||
<parameter name="direction" code="CMpd" type="split direction" description="The direction to split.">
|
||||
<cocoa key="direction"/>
|
||||
</parameter>
|
||||
<result type="terminal" description="The newly created terminal."/>
|
||||
</command>
|
||||
|
||||
<command name="focus" code="CmuxFcus" description="Focus a terminal, bringing its window to the front.">
|
||||
<direct-parameter type="specifier" description="The terminal to focus."/>
|
||||
</command>
|
||||
|
||||
<command name="close" code="CmuxClos" description="Close a terminal.">
|
||||
<direct-parameter type="specifier" description="The terminal to close."/>
|
||||
</command>
|
||||
|
||||
<command name="activate window" code="CmuxAcWn" description="Activate a cmux window, bringing it to the front.">
|
||||
<direct-parameter type="specifier" description="The window to activate."/>
|
||||
</command>
|
||||
|
||||
<command name="select tab" code="CmuxSlTb" description="Select a workspace in its window.">
|
||||
<direct-parameter type="specifier" description="The workspace to select."/>
|
||||
</command>
|
||||
|
||||
<command name="close tab" code="CmuxClTb" description="Close a workspace.">
|
||||
<direct-parameter type="specifier" description="The workspace to close."/>
|
||||
</command>
|
||||
|
||||
<command name="close window" code="CmuxClWn" description="Close a window.">
|
||||
<direct-parameter type="specifier" description="The window to close."/>
|
||||
</command>
|
||||
|
||||
<command name="input text" code="CmuxInTx" description="Input text to a terminal as if it was pasted.">
|
||||
<cocoa class="CmuxScriptInputTextCommand"/>
|
||||
<direct-parameter type="text" description="The text to input."/>
|
||||
<parameter name="to" code="CMiT" type="terminal" description="The terminal to input text to.">
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
</suite>
|
||||
|
||||
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
|
||||
<command name="count" code="corecnte" description="Return the number of elements of a particular class within an object.">
|
||||
<cocoa class="NSCountCommand"/>
|
||||
<access-group identifier="*"/>
|
||||
<direct-parameter type="specifier" requires-access="r" description="The objects to be counted."/>
|
||||
<parameter name="each" code="kocl" type="type" optional="yes" description="The class of objects to be counted." hidden="yes">
|
||||
<cocoa key="ObjectClass"/>
|
||||
</parameter>
|
||||
<result type="integer" description="The count."/>
|
||||
</command>
|
||||
|
||||
<command name="exists" code="coredoex" description="Verify that an object exists.">
|
||||
<cocoa class="NSExistsCommand"/>
|
||||
<access-group identifier="*"/>
|
||||
<direct-parameter type="any" requires-access="r" description="The object(s) to check."/>
|
||||
<result type="boolean" description="Did the object(s) exist?"/>
|
||||
</command>
|
||||
|
||||
<command name="quit" code="aevtquit" description="Quit the application.">
|
||||
<cocoa class="NSQuitCommand"/>
|
||||
</command>
|
||||
</suite>
|
||||
</dictionary>
|
||||
|
|
@ -31,7 +31,10 @@ fi
|
|||
if [[ -o interactive ]]; then
|
||||
# We overwrote GhosttyKit's injected ZDOTDIR, so manually load Ghostty's
|
||||
# zsh integration if available.
|
||||
if [[ -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then
|
||||
#
|
||||
# We can't rely on GHOSTTY_ZSH_ZDOTDIR here because Ghostty's own zsh
|
||||
# bootstrap unsets it before chaining into this cmux wrapper.
|
||||
if [[ "${CMUX_LOAD_GHOSTTY_ZSH_INTEGRATION:-0}" == "1" && -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then
|
||||
builtin typeset _cmux_ghostty="$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
|
||||
[[ -r "$_cmux_ghostty" ]] && builtin source -- "$_cmux_ghostty"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
_cmux_send() {
|
||||
local payload="$1"
|
||||
if command -v ncat >/dev/null 2>&1; then
|
||||
printf '%s\n' "$payload" | ncat -U "$CMUX_SOCKET_PATH" --send-only
|
||||
printf '%s\n' "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
|
||||
elif command -v socat >/dev/null 2>&1; then
|
||||
printf '%s\n' "$payload" | socat - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
|
||||
printf '%s\n' "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
|
||||
elif command -v nc >/dev/null 2>&1; then
|
||||
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
|
||||
#
|
||||
|
|
@ -23,16 +23,72 @@ _cmux_send() {
|
|||
fi
|
||||
}
|
||||
|
||||
_cmux_restore_scrollback_once() {
|
||||
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
|
||||
[[ -n "$path" ]] || return 0
|
||||
unset CMUX_RESTORE_SCROLLBACK_FILE
|
||||
|
||||
if [[ -r "$path" ]]; then
|
||||
/bin/cat -- "$path" 2>/dev/null || true
|
||||
/bin/rm -f -- "$path" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
_cmux_restore_scrollback_once
|
||||
|
||||
# Throttle heavy work to avoid prompt latency.
|
||||
_CMUX_PWD_LAST_PWD="${_CMUX_PWD_LAST_PWD:-}"
|
||||
_CMUX_GIT_LAST_PWD="${_CMUX_GIT_LAST_PWD:-}"
|
||||
_CMUX_GIT_LAST_RUN="${_CMUX_GIT_LAST_RUN:-0}"
|
||||
_CMUX_GIT_JOB_PID="${_CMUX_GIT_JOB_PID:-}"
|
||||
_CMUX_GIT_JOB_STARTED_AT="${_CMUX_GIT_JOB_STARTED_AT:-0}"
|
||||
_CMUX_GIT_HEAD_LAST_PWD="${_CMUX_GIT_HEAD_LAST_PWD:-}"
|
||||
_CMUX_GIT_HEAD_PATH="${_CMUX_GIT_HEAD_PATH:-}"
|
||||
_CMUX_GIT_HEAD_SIGNATURE="${_CMUX_GIT_HEAD_SIGNATURE:-}"
|
||||
_CMUX_PR_POLL_PID="${_CMUX_PR_POLL_PID:-}"
|
||||
_CMUX_PR_POLL_PWD="${_CMUX_PR_POLL_PWD:-}"
|
||||
_CMUX_PR_POLL_INTERVAL="${_CMUX_PR_POLL_INTERVAL:-45}"
|
||||
_CMUX_PR_FORCE="${_CMUX_PR_FORCE:-0}"
|
||||
_CMUX_ASYNC_JOB_TIMEOUT="${_CMUX_ASYNC_JOB_TIMEOUT:-20}"
|
||||
|
||||
_CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}"
|
||||
_CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}"
|
||||
_CMUX_TTY_REPORTED="${_CMUX_TTY_REPORTED:-0}"
|
||||
|
||||
_cmux_git_resolve_head_path() {
|
||||
# Resolve the HEAD file path without invoking git (fast; works for worktrees).
|
||||
local dir="$PWD"
|
||||
while :; do
|
||||
if [[ -d "$dir/.git" ]]; then
|
||||
printf '%s\n' "$dir/.git/HEAD"
|
||||
return 0
|
||||
fi
|
||||
if [[ -f "$dir/.git" ]]; then
|
||||
local line gitdir
|
||||
IFS= read -r line < "$dir/.git" || line=""
|
||||
if [[ "$line" == gitdir:* ]]; then
|
||||
gitdir="${line#gitdir:}"
|
||||
gitdir="${gitdir## }"
|
||||
gitdir="${gitdir%% }"
|
||||
[[ -n "$gitdir" ]] || return 1
|
||||
[[ "$gitdir" != /* ]] && gitdir="$dir/$gitdir"
|
||||
printf '%s\n' "$gitdir/HEAD"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
[[ "$dir" == "/" || -z "$dir" ]] && break
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_cmux_git_head_signature() {
|
||||
local head_path="$1"
|
||||
[[ -n "$head_path" && -r "$head_path" ]] || return 1
|
||||
local line
|
||||
IFS= read -r line < "$head_path" || return 1
|
||||
printf '%s\n' "$line"
|
||||
}
|
||||
|
||||
_cmux_report_tty_once() {
|
||||
# Send the TTY name to the app once per session so the batched port scanner
|
||||
# knows which TTY belongs to this panel.
|
||||
|
|
@ -44,7 +100,7 @@ _cmux_report_tty_once() {
|
|||
_CMUX_TTY_REPORTED=1
|
||||
{
|
||||
_cmux_send "report_tty $_CMUX_TTY_NAME --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &
|
||||
} >/dev/null 2>&1 & disown
|
||||
}
|
||||
|
||||
_cmux_ports_kick() {
|
||||
|
|
@ -56,7 +112,183 @@ _cmux_ports_kick() {
|
|||
_CMUX_PORTS_LAST_RUN=$SECONDS
|
||||
{
|
||||
_cmux_send "ports_kick --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 & disown
|
||||
}
|
||||
|
||||
_cmux_clear_pr_for_panel() {
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_pr_output_indicates_no_pull_request() {
|
||||
local output="$1"
|
||||
output="$(printf '%s' "$output" | tr '[:upper:]' '[:lower:]')"
|
||||
[[ "$output" == *"no pull requests found"* \
|
||||
|| "$output" == *"no pull request found"* \
|
||||
|| "$output" == *"no pull requests associated"* \
|
||||
|| "$output" == *"no pull request associated"* ]]
|
||||
}
|
||||
|
||||
_cmux_report_pr_for_path() {
|
||||
local repo_path="$1"
|
||||
[[ -n "$repo_path" ]] || {
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
}
|
||||
[[ -d "$repo_path" ]] || {
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
}
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
|
||||
local branch gh_output gh_error="" err_file="" gh_status number state url status_opt=""
|
||||
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
|
||||
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
fi
|
||||
|
||||
err_file="$(/usr/bin/mktemp "${TMPDIR:-/tmp}/cmux-gh-pr-view.XXXXXX" 2>/dev/null || true)"
|
||||
[[ -n "$err_file" ]] || return 1
|
||||
gh_output="$(
|
||||
builtin cd "$repo_path" 2>/dev/null \
|
||||
&& gh pr view \
|
||||
--json number,state,url \
|
||||
--jq '[.number, .state, .url] | @tsv' \
|
||||
2>"$err_file"
|
||||
)"
|
||||
gh_status=$?
|
||||
if [[ -f "$err_file" ]]; then
|
||||
gh_error="$("/bin/cat" -- "$err_file" 2>/dev/null || true)"
|
||||
/bin/rm -f -- "$err_file" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if (( gh_status != 0 )); then
|
||||
if _cmux_pr_output_indicates_no_pull_request "$gh_error"; then
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
fi
|
||||
# Preserve the last-known PR badge when gh fails transiently, then retry
|
||||
# on the next background poll instead of clearing visible state.
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$gh_output" ]]; then
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
fi
|
||||
|
||||
IFS=$'\t' read -r number state url <<< "$gh_output"
|
||||
if [[ -z "$number" || -z "$url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$state" in
|
||||
MERGED) status_opt="--state=merged" ;;
|
||||
OPEN) status_opt="--state=open" ;;
|
||||
CLOSED) status_opt="--state=closed" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_child_pids() {
|
||||
local parent_pid="$1"
|
||||
[[ -n "$parent_pid" ]] || return 0
|
||||
/bin/ps -ax -o pid= -o ppid= 2>/dev/null | /usr/bin/awk -v parent="$parent_pid" '$2 == parent { print $1 }'
|
||||
}
|
||||
|
||||
_cmux_kill_process_tree() {
|
||||
local pid="$1"
|
||||
local signal="${2:-TERM}"
|
||||
local child_pid=""
|
||||
[[ -n "$pid" ]] || return 0
|
||||
|
||||
while IFS= read -r child_pid; do
|
||||
[[ -n "$child_pid" ]] || continue
|
||||
[[ "$child_pid" == "$pid" ]] && continue
|
||||
_cmux_kill_process_tree "$child_pid" "$signal"
|
||||
done < <(_cmux_child_pids "$pid")
|
||||
|
||||
kill "-$signal" "$pid" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
_cmux_run_pr_probe_with_timeout() {
|
||||
local repo_path="$1"
|
||||
local probe_pid=""
|
||||
local started_at=$SECONDS
|
||||
local now=$started_at
|
||||
|
||||
(
|
||||
_cmux_report_pr_for_path "$repo_path"
|
||||
) &
|
||||
probe_pid=$!
|
||||
|
||||
while kill -0 "$probe_pid" >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
now=$SECONDS
|
||||
if (( _CMUX_ASYNC_JOB_TIMEOUT > 0 )) && (( now - started_at >= _CMUX_ASYNC_JOB_TIMEOUT )); then
|
||||
_cmux_kill_process_tree "$probe_pid" TERM
|
||||
sleep 0.2
|
||||
if kill -0 "$probe_pid" >/dev/null 2>&1; then
|
||||
_cmux_kill_process_tree "$probe_pid" KILL
|
||||
sleep 0.2
|
||||
fi
|
||||
if ! kill -0 "$probe_pid" >/dev/null 2>&1; then
|
||||
wait "$probe_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
wait "$probe_pid"
|
||||
}
|
||||
|
||||
_cmux_stop_pr_poll_loop() {
|
||||
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
|
||||
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" TERM
|
||||
sleep 0.1
|
||||
if kill -0 "$_CMUX_PR_POLL_PID" >/dev/null 2>&1; then
|
||||
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" KILL
|
||||
fi
|
||||
_CMUX_PR_POLL_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
_cmux_start_pr_poll_loop() {
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
|
||||
local watch_pwd="${1:-$PWD}"
|
||||
local force_restart="${2:-0}"
|
||||
local watch_shell_pid="$$"
|
||||
local interval="${_CMUX_PR_POLL_INTERVAL:-45}"
|
||||
|
||||
if [[ "$force_restart" != "1" && "$watch_pwd" == "$_CMUX_PR_POLL_PWD" && -n "$_CMUX_PR_POLL_PID" ]] \
|
||||
&& kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
_cmux_stop_pr_poll_loop
|
||||
_CMUX_PR_POLL_PWD="$watch_pwd"
|
||||
|
||||
{
|
||||
while :; do
|
||||
kill -0 "$watch_shell_pid" 2>/dev/null || break
|
||||
_cmux_run_pr_probe_with_timeout "$watch_pwd" || true
|
||||
sleep "$interval"
|
||||
done
|
||||
} >/dev/null 2>&1 &
|
||||
_CMUX_PR_POLL_PID=$!
|
||||
disown "$_CMUX_PR_POLL_PID" 2>/dev/null || disown
|
||||
}
|
||||
|
||||
_cmux_bash_cleanup() {
|
||||
_cmux_stop_pr_poll_loop
|
||||
}
|
||||
|
||||
_cmux_prompt_command() {
|
||||
|
|
@ -67,6 +299,18 @@ _cmux_prompt_command() {
|
|||
local now=$SECONDS
|
||||
local pwd="$PWD"
|
||||
|
||||
# Post-wake socket writes can occasionally leave a probe process wedged.
|
||||
# If one probe is stale, clear the guard so fresh async probes can resume.
|
||||
if [[ -n "$_CMUX_GIT_JOB_PID" ]]; then
|
||||
if ! kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
|
||||
_CMUX_GIT_JOB_PID=""
|
||||
_CMUX_GIT_JOB_STARTED_AT=0
|
||||
elif (( _CMUX_GIT_JOB_STARTED_AT > 0 )) && (( now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
|
||||
_CMUX_GIT_JOB_PID=""
|
||||
_CMUX_GIT_JOB_STARTED_AT=0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve TTY name once.
|
||||
if [[ -z "$_CMUX_TTY_NAME" ]]; then
|
||||
local t
|
||||
|
|
@ -83,7 +327,26 @@ _cmux_prompt_command() {
|
|||
{
|
||||
local qpwd="${pwd//\"/\\\"}"
|
||||
_cmux_send "report_pwd \"${qpwd}\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
} >/dev/null 2>&1 &
|
||||
} >/dev/null 2>&1 & disown
|
||||
fi
|
||||
|
||||
# Branch can change via aliases/tools while an older probe is still in flight.
|
||||
# Track .git/HEAD content so we can restart stale probes immediately.
|
||||
local git_head_changed=0
|
||||
if [[ "$pwd" != "$_CMUX_GIT_HEAD_LAST_PWD" ]]; then
|
||||
_CMUX_GIT_HEAD_LAST_PWD="$pwd"
|
||||
_CMUX_GIT_HEAD_PATH="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
|
||||
_CMUX_GIT_HEAD_SIGNATURE=""
|
||||
fi
|
||||
if [[ -n "$_CMUX_GIT_HEAD_PATH" ]]; then
|
||||
local head_signature
|
||||
head_signature="$(_cmux_git_head_signature "$_CMUX_GIT_HEAD_PATH" 2>/dev/null || true)"
|
||||
if [[ -n "$head_signature" && "$head_signature" != "$_CMUX_GIT_HEAD_SIGNATURE" ]]; then
|
||||
_CMUX_GIT_HEAD_SIGNATURE="$head_signature"
|
||||
git_head_changed=1
|
||||
# Also invalidate the PR poller so it refreshes with the new branch.
|
||||
_CMUX_PR_FORCE=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Git branch/dirty can change without a directory change (e.g. `git checkout`),
|
||||
|
|
@ -91,9 +354,10 @@ _cmux_prompt_command() {
|
|||
# When pwd changes (cd into a different repo), kill the old probe and start fresh
|
||||
# so the sidebar picks up the new branch immediately.
|
||||
if [[ -n "$_CMUX_GIT_JOB_PID" ]] && kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
|
||||
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" ]]; then
|
||||
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" || "$git_head_changed" == "1" ]]; then
|
||||
kill "$_CMUX_GIT_JOB_PID" >/dev/null 2>&1 || true
|
||||
_CMUX_GIT_JOB_PID=""
|
||||
_CMUX_GIT_JOB_STARTED_AT=0
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -113,6 +377,33 @@ _cmux_prompt_command() {
|
|||
fi
|
||||
} >/dev/null 2>&1 &
|
||||
_CMUX_GIT_JOB_PID=$!
|
||||
disown
|
||||
_CMUX_GIT_JOB_STARTED_AT=$now
|
||||
fi
|
||||
|
||||
# Pull request metadata is remote state. Keep polling while the shell sits
|
||||
# at a prompt so newly created or merged PRs appear without another command.
|
||||
local should_restart_pr_poll=0
|
||||
local pr_context_changed=0
|
||||
if [[ -n "$_CMUX_PR_POLL_PWD" && "$pwd" != "$_CMUX_PR_POLL_PWD" ]]; then
|
||||
pr_context_changed=1
|
||||
elif [[ "$git_head_changed" == "1" ]]; then
|
||||
pr_context_changed=1
|
||||
fi
|
||||
if [[ "$pwd" != "$_CMUX_PR_POLL_PWD" || "$git_head_changed" == "1" ]]; then
|
||||
should_restart_pr_poll=1
|
||||
elif (( _CMUX_PR_FORCE )); then
|
||||
should_restart_pr_poll=1
|
||||
elif [[ -z "$_CMUX_PR_POLL_PID" ]] || ! kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
|
||||
should_restart_pr_poll=1
|
||||
fi
|
||||
|
||||
if (( should_restart_pr_poll )); then
|
||||
_CMUX_PR_FORCE=0
|
||||
if (( pr_context_changed )); then
|
||||
_cmux_clear_pr_for_panel
|
||||
fi
|
||||
_cmux_start_pr_poll_loop "$pwd" 1
|
||||
fi
|
||||
|
||||
# Ports: lightweight kick to the app's batched scanner every ~10s.
|
||||
|
|
@ -150,15 +441,17 @@ _cmux_install_prompt_command() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Ensure Resources/bin is at the front of PATH. Shell init (.bashrc/.bash_profile)
|
||||
# may prepend other dirs that push our wrapper behind the system claude binary.
|
||||
# Ensure Resources/bin is at the front of PATH, and remove the app's
|
||||
# Contents/MacOS entry so the GUI cmux binary cannot shadow the CLI cmux.
|
||||
# Shell init (.bashrc/.bash_profile) may prepend other dirs after launch.
|
||||
_cmux_fix_path() {
|
||||
if [[ -n "${GHOSTTY_BIN_DIR:-}" ]]; then
|
||||
local bin_dir="${GHOSTTY_BIN_DIR%/MacOS}"
|
||||
bin_dir="${bin_dir}/Resources/bin"
|
||||
local gui_dir="${GHOSTTY_BIN_DIR%/}"
|
||||
local bin_dir="${gui_dir%/MacOS}/Resources/bin"
|
||||
if [[ -d "$bin_dir" ]]; then
|
||||
local new_path=":${PATH}:"
|
||||
new_path="${new_path//:${bin_dir}:/:}"
|
||||
new_path="${new_path//:${gui_dir}:/:}"
|
||||
new_path="${new_path#:}"
|
||||
new_path="${new_path%:}"
|
||||
PATH="${bin_dir}:${new_path}"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
_cmux_send() {
|
||||
local payload="$1"
|
||||
if command -v ncat >/dev/null 2>&1; then
|
||||
print -r -- "$payload" | ncat -U "$CMUX_SOCKET_PATH" --send-only
|
||||
print -r -- "$payload" | ncat -w 1 -U "$CMUX_SOCKET_PATH" --send-only
|
||||
elif command -v socat >/dev/null 2>&1; then
|
||||
print -r -- "$payload" | socat - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
|
||||
print -r -- "$payload" | socat -T 1 - "UNIX-CONNECT:$CMUX_SOCKET_PATH"
|
||||
elif command -v nc >/dev/null 2>&1; then
|
||||
# Some nc builds don't support unix sockets, but keep as a last-ditch fallback.
|
||||
#
|
||||
|
|
@ -24,35 +24,40 @@ _cmux_send() {
|
|||
fi
|
||||
}
|
||||
|
||||
_cmux_restore_scrollback_once() {
|
||||
local path="${CMUX_RESTORE_SCROLLBACK_FILE:-}"
|
||||
[[ -n "$path" ]] || return 0
|
||||
unset CMUX_RESTORE_SCROLLBACK_FILE
|
||||
|
||||
if [[ -r "$path" ]]; then
|
||||
/bin/cat -- "$path" 2>/dev/null || true
|
||||
/bin/rm -f -- "$path" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
_cmux_restore_scrollback_once
|
||||
|
||||
# Throttle heavy work to avoid prompt latency.
|
||||
typeset -g _CMUX_PWD_LAST_PWD=""
|
||||
typeset -g _CMUX_GIT_LAST_PWD=""
|
||||
typeset -g _CMUX_GIT_LAST_RUN=0
|
||||
typeset -g _CMUX_GIT_JOB_PID=""
|
||||
typeset -g _CMUX_GIT_JOB_STARTED_AT=0
|
||||
typeset -g _CMUX_GIT_FORCE=0
|
||||
typeset -g _CMUX_GIT_HEAD_LAST_PWD=""
|
||||
typeset -g _CMUX_GIT_HEAD_PATH=""
|
||||
typeset -g _CMUX_GIT_HEAD_MTIME=0
|
||||
typeset -g _CMUX_HAVE_ZSTAT=0
|
||||
typeset -g _CMUX_GIT_HEAD_SIGNATURE=""
|
||||
typeset -g _CMUX_GIT_HEAD_WATCH_PID=""
|
||||
typeset -g _CMUX_PR_POLL_PID=""
|
||||
typeset -g _CMUX_PR_POLL_PWD=""
|
||||
typeset -g _CMUX_PR_POLL_INTERVAL=45
|
||||
typeset -g _CMUX_PR_FORCE=0
|
||||
typeset -g _CMUX_ASYNC_JOB_TIMEOUT=20
|
||||
|
||||
typeset -g _CMUX_PORTS_LAST_RUN=0
|
||||
typeset -g _CMUX_CMD_START=0
|
||||
typeset -g _CMUX_TTY_NAME=""
|
||||
typeset -g _CMUX_TTY_REPORTED=0
|
||||
|
||||
_cmux_ensure_zstat() {
|
||||
# zstat is substantially cheaper than spawning external `stat`.
|
||||
if (( _CMUX_HAVE_ZSTAT != 0 )); then
|
||||
return 0
|
||||
fi
|
||||
if zmodload -F zsh/stat b:zstat 2>/dev/null; then
|
||||
_CMUX_HAVE_ZSTAT=1
|
||||
return 0
|
||||
fi
|
||||
_CMUX_HAVE_ZSTAT=-1
|
||||
return 1
|
||||
}
|
||||
|
||||
_cmux_git_resolve_head_path() {
|
||||
# Resolve the HEAD file path without invoking git (fast; works for worktrees).
|
||||
local dir="$PWD"
|
||||
|
|
@ -80,27 +85,15 @@ _cmux_git_resolve_head_path() {
|
|||
return 1
|
||||
}
|
||||
|
||||
_cmux_git_head_mtime() {
|
||||
_cmux_git_head_signature() {
|
||||
local head_path="$1"
|
||||
[[ -n "$head_path" && -f "$head_path" ]] || { print -r -- 0; return 0; }
|
||||
|
||||
if _cmux_ensure_zstat; then
|
||||
typeset -A st
|
||||
if zstat -H st +mtime -- "$head_path" 2>/dev/null; then
|
||||
print -r -- "${st[mtime]:-0}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback for environments where zsh/stat isn't available.
|
||||
if command -v stat >/dev/null 2>&1; then
|
||||
local mtime
|
||||
mtime="$(stat -f %m "$head_path" 2>/dev/null || stat -c %Y "$head_path" 2>/dev/null || echo 0)"
|
||||
print -r -- "$mtime"
|
||||
[[ -n "$head_path" && -r "$head_path" ]] || return 1
|
||||
local line=""
|
||||
if IFS= read -r line < "$head_path"; then
|
||||
print -r -- "$line"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print -r -- 0
|
||||
return 1
|
||||
}
|
||||
|
||||
_cmux_report_tty_once() {
|
||||
|
|
@ -129,6 +122,236 @@ _cmux_ports_kick() {
|
|||
} >/dev/null 2>&1 &!
|
||||
}
|
||||
|
||||
_cmux_report_git_branch_for_path() {
|
||||
local repo_path="$1"
|
||||
[[ -n "$repo_path" ]] || return 0
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
|
||||
local branch dirty_opt="" first
|
||||
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
|
||||
if [[ -n "$branch" ]]; then
|
||||
first="$(git -C "$repo_path" status --porcelain -uno 2>/dev/null | head -1)"
|
||||
[[ -n "$first" ]] && dirty_opt="--status=dirty"
|
||||
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
else
|
||||
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
fi
|
||||
}
|
||||
|
||||
_cmux_clear_pr_for_panel() {
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
_cmux_send "clear_pr --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_pr_output_indicates_no_pull_request() {
|
||||
local output="${1:l}"
|
||||
[[ "$output" == *"no pull requests found"* \
|
||||
|| "$output" == *"no pull request found"* \
|
||||
|| "$output" == *"no pull requests associated"* \
|
||||
|| "$output" == *"no pull request associated"* ]]
|
||||
}
|
||||
|
||||
_cmux_report_pr_for_path() {
|
||||
local repo_path="$1"
|
||||
[[ -n "$repo_path" ]] || {
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
}
|
||||
[[ -d "$repo_path" ]] || {
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
}
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
|
||||
local branch gh_output gh_error="" err_file="" number state url status_opt="" gh_status
|
||||
branch="$(git -C "$repo_path" branch --show-current 2>/dev/null)"
|
||||
if [[ -z "$branch" ]] || ! command -v gh >/dev/null 2>&1; then
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
fi
|
||||
|
||||
err_file="$(/usr/bin/mktemp "${TMPDIR:-/tmp}/cmux-gh-pr-view.XXXXXX" 2>/dev/null || true)"
|
||||
[[ -n "$err_file" ]] || return 1
|
||||
gh_output="$(
|
||||
builtin cd "$repo_path" 2>/dev/null \
|
||||
&& gh pr view \
|
||||
--json number,state,url \
|
||||
--jq '[.number, .state, .url] | @tsv' \
|
||||
2>"$err_file"
|
||||
)"
|
||||
gh_status=$?
|
||||
if [[ -f "$err_file" ]]; then
|
||||
gh_error="$("/bin/cat" -- "$err_file" 2>/dev/null || true)"
|
||||
/bin/rm -f -- "$err_file" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if (( gh_status != 0 )); then
|
||||
if _cmux_pr_output_indicates_no_pull_request "$gh_error"; then
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
fi
|
||||
# Keep the last-known PR badge on transient gh failures (auth hiccups,
|
||||
# API lag after creation, or rate limiting) and retry on the next poll.
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$gh_output" ]]; then
|
||||
_cmux_clear_pr_for_panel
|
||||
return 0
|
||||
fi
|
||||
|
||||
local IFS=$'\t'
|
||||
read -r number state url <<< "$gh_output"
|
||||
if [[ -z "$number" ]] || [[ -z "$url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$state" in
|
||||
MERGED) status_opt="--state=merged" ;;
|
||||
OPEN) status_opt="--state=open" ;;
|
||||
CLOSED) status_opt="--state=closed" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
_cmux_send "report_pr $number $url $status_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
}
|
||||
|
||||
_cmux_child_pids() {
|
||||
local parent_pid="$1"
|
||||
[[ -n "$parent_pid" ]] || return 0
|
||||
/bin/ps -ax -o pid= -o ppid= 2>/dev/null | /usr/bin/awk -v parent="$parent_pid" '$2 == parent { print $1 }'
|
||||
}
|
||||
|
||||
_cmux_kill_process_tree() {
|
||||
local pid="$1"
|
||||
local signal="${2:-TERM}"
|
||||
local child_pid=""
|
||||
[[ -n "$pid" ]] || return 0
|
||||
|
||||
while IFS= read -r child_pid; do
|
||||
[[ -n "$child_pid" ]] || continue
|
||||
[[ "$child_pid" == "$pid" ]] && continue
|
||||
_cmux_kill_process_tree "$child_pid" "$signal"
|
||||
done < <(_cmux_child_pids "$pid")
|
||||
|
||||
kill "-$signal" "$pid" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
_cmux_run_pr_probe_with_timeout() {
|
||||
local repo_path="$1"
|
||||
local probe_pid=""
|
||||
local started_at=$EPOCHSECONDS
|
||||
local now=$started_at
|
||||
|
||||
(
|
||||
_cmux_report_pr_for_path "$repo_path"
|
||||
) &
|
||||
probe_pid=$!
|
||||
|
||||
while kill -0 "$probe_pid" >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
now=$EPOCHSECONDS
|
||||
if (( _CMUX_ASYNC_JOB_TIMEOUT > 0 )) && (( now - started_at >= _CMUX_ASYNC_JOB_TIMEOUT )); then
|
||||
_cmux_kill_process_tree "$probe_pid" TERM
|
||||
sleep 0.2
|
||||
if kill -0 "$probe_pid" >/dev/null 2>&1; then
|
||||
_cmux_kill_process_tree "$probe_pid" KILL
|
||||
sleep 0.2
|
||||
fi
|
||||
if ! kill -0 "$probe_pid" >/dev/null 2>&1; then
|
||||
wait "$probe_pid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
wait "$probe_pid"
|
||||
}
|
||||
|
||||
_cmux_stop_pr_poll_loop() {
|
||||
if [[ -n "$_CMUX_PR_POLL_PID" ]]; then
|
||||
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" TERM
|
||||
sleep 0.1
|
||||
if kill -0 "$_CMUX_PR_POLL_PID" >/dev/null 2>&1; then
|
||||
_cmux_kill_process_tree "$_CMUX_PR_POLL_PID" KILL
|
||||
fi
|
||||
_CMUX_PR_POLL_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
_cmux_start_pr_poll_loop() {
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
|
||||
local watch_pwd="${1:-$PWD}"
|
||||
local force_restart="${2:-0}"
|
||||
local watch_shell_pid="$$"
|
||||
local interval="${_CMUX_PR_POLL_INTERVAL:-45}"
|
||||
|
||||
if [[ "$force_restart" != "1" && "$watch_pwd" == "$_CMUX_PR_POLL_PWD" && -n "$_CMUX_PR_POLL_PID" ]] \
|
||||
&& kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
_cmux_stop_pr_poll_loop
|
||||
_CMUX_PR_POLL_PWD="$watch_pwd"
|
||||
|
||||
{
|
||||
while true; do
|
||||
kill -0 "$watch_shell_pid" >/dev/null 2>&1 || break
|
||||
_cmux_run_pr_probe_with_timeout "$watch_pwd" || true
|
||||
sleep "$interval"
|
||||
done
|
||||
} >/dev/null 2>&1 &!
|
||||
_CMUX_PR_POLL_PID=$!
|
||||
}
|
||||
|
||||
_cmux_stop_git_head_watch() {
|
||||
if [[ -n "$_CMUX_GIT_HEAD_WATCH_PID" ]]; then
|
||||
kill "$_CMUX_GIT_HEAD_WATCH_PID" >/dev/null 2>&1 || true
|
||||
_CMUX_GIT_HEAD_WATCH_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
_cmux_start_git_head_watch() {
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
[[ -n "$CMUX_PANEL_ID" ]] || return 0
|
||||
|
||||
local watch_pwd="$PWD"
|
||||
local watch_head_path
|
||||
watch_head_path="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
|
||||
[[ -n "$watch_head_path" ]] || return 0
|
||||
|
||||
local watch_head_signature
|
||||
watch_head_signature="$(_cmux_git_head_signature "$watch_head_path" 2>/dev/null || true)"
|
||||
|
||||
_CMUX_GIT_HEAD_LAST_PWD="$watch_pwd"
|
||||
_CMUX_GIT_HEAD_PATH="$watch_head_path"
|
||||
_CMUX_GIT_HEAD_SIGNATURE="$watch_head_signature"
|
||||
|
||||
_cmux_stop_git_head_watch
|
||||
{
|
||||
local last_signature="$watch_head_signature"
|
||||
while true; do
|
||||
sleep 1
|
||||
|
||||
local signature
|
||||
signature="$(_cmux_git_head_signature "$watch_head_path" 2>/dev/null || true)"
|
||||
if [[ -n "$signature" && "$signature" != "$last_signature" ]]; then
|
||||
last_signature="$signature"
|
||||
_cmux_report_git_branch_for_path "$watch_pwd"
|
||||
fi
|
||||
done
|
||||
} >/dev/null 2>&1 &!
|
||||
_CMUX_GIT_HEAD_WATCH_PID=$!
|
||||
}
|
||||
|
||||
_cmux_preexec() {
|
||||
if [[ -z "$_CMUX_TTY_NAME" ]]; then
|
||||
local t
|
||||
|
|
@ -143,15 +366,20 @@ _cmux_preexec() {
|
|||
local cmd="${1## }"
|
||||
case "$cmd" in
|
||||
git\ *|git|gh\ *|lazygit|lazygit\ *|tig|tig\ *|gitui|gitui\ *|stg\ *|jj\ *)
|
||||
_CMUX_GIT_FORCE=1 ;;
|
||||
_CMUX_GIT_FORCE=1
|
||||
_CMUX_PR_FORCE=1 ;;
|
||||
esac
|
||||
|
||||
# Register TTY + kick batched port scan for foreground commands (servers).
|
||||
_cmux_report_tty_once
|
||||
_cmux_ports_kick
|
||||
_cmux_stop_pr_poll_loop
|
||||
_cmux_start_git_head_watch
|
||||
}
|
||||
|
||||
_cmux_precmd() {
|
||||
_cmux_stop_git_head_watch
|
||||
|
||||
# Skip if socket doesn't exist yet
|
||||
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
|
||||
[[ -n "$CMUX_TAB_ID" ]] || return 0
|
||||
|
|
@ -171,6 +399,19 @@ _cmux_precmd() {
|
|||
local cmd_start="$_CMUX_CMD_START"
|
||||
_CMUX_CMD_START=0
|
||||
|
||||
# Post-wake socket writes can occasionally leave a probe process wedged.
|
||||
# If one probe is stale, clear the guard so fresh async probes can resume.
|
||||
if [[ -n "$_CMUX_GIT_JOB_PID" ]]; then
|
||||
if ! kill -0 "$_CMUX_GIT_JOB_PID" 2>/dev/null; then
|
||||
_CMUX_GIT_JOB_PID=""
|
||||
_CMUX_GIT_JOB_STARTED_AT=0
|
||||
elif (( _CMUX_GIT_JOB_STARTED_AT > 0 )) && (( now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT )); then
|
||||
_CMUX_GIT_JOB_PID=""
|
||||
_CMUX_GIT_JOB_STARTED_AT=0
|
||||
_CMUX_GIT_FORCE=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# CWD: keep the app in sync with the actual shell directory.
|
||||
# This is also the simplest way to test sidebar directory behavior end-to-end.
|
||||
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
|
||||
|
|
@ -183,23 +424,28 @@ _cmux_precmd() {
|
|||
fi
|
||||
|
||||
# Git branch/dirty: update immediately on directory change, otherwise every ~3s.
|
||||
# While a foreground command is running, _cmux_start_git_head_watch probes HEAD
|
||||
# once per second so agent-initiated git checkouts still surface quickly.
|
||||
local should_git=0
|
||||
local git_head_changed=0
|
||||
|
||||
# Git branch can change without a `git ...`-prefixed command (aliases like `gco`,
|
||||
# tools like `gh pr checkout`, etc.). Detect HEAD changes and force a refresh.
|
||||
if [[ "$pwd" != "$_CMUX_GIT_HEAD_LAST_PWD" ]]; then
|
||||
_CMUX_GIT_HEAD_LAST_PWD="$pwd"
|
||||
_CMUX_GIT_HEAD_PATH="$(_cmux_git_resolve_head_path 2>/dev/null || true)"
|
||||
_CMUX_GIT_HEAD_MTIME=0
|
||||
_CMUX_GIT_HEAD_SIGNATURE=""
|
||||
fi
|
||||
if [[ -n "$_CMUX_GIT_HEAD_PATH" ]]; then
|
||||
local head_mtime
|
||||
head_mtime="$(_cmux_git_head_mtime "$_CMUX_GIT_HEAD_PATH" 2>/dev/null || echo 0)"
|
||||
if [[ -n "$head_mtime" && "$head_mtime" != 0 && "$head_mtime" != "$_CMUX_GIT_HEAD_MTIME" ]]; then
|
||||
_CMUX_GIT_HEAD_MTIME="$head_mtime"
|
||||
local head_signature
|
||||
head_signature="$(_cmux_git_head_signature "$_CMUX_GIT_HEAD_PATH" 2>/dev/null || true)"
|
||||
if [[ -n "$head_signature" && "$head_signature" != "$_CMUX_GIT_HEAD_SIGNATURE" ]]; then
|
||||
_CMUX_GIT_HEAD_SIGNATURE="$head_signature"
|
||||
git_head_changed=1
|
||||
# Treat HEAD file change like a git command — force-replace any
|
||||
# running probe so the sidebar picks up the new branch immediately.
|
||||
_CMUX_GIT_FORCE=1
|
||||
_CMUX_PR_FORCE=1
|
||||
should_git=1
|
||||
fi
|
||||
fi
|
||||
|
|
@ -224,6 +470,7 @@ _cmux_precmd() {
|
|||
if [[ "$pwd" != "$_CMUX_GIT_LAST_PWD" ]] || (( _CMUX_GIT_FORCE )); then
|
||||
kill "$_CMUX_GIT_JOB_PID" >/dev/null 2>&1 || true
|
||||
_CMUX_GIT_JOB_PID=""
|
||||
_CMUX_GIT_JOB_STARTED_AT=0
|
||||
else
|
||||
can_launch_git=0
|
||||
fi
|
||||
|
|
@ -234,21 +481,39 @@ _cmux_precmd() {
|
|||
_CMUX_GIT_LAST_PWD="$pwd"
|
||||
_CMUX_GIT_LAST_RUN=$now
|
||||
{
|
||||
local branch dirty_opt=""
|
||||
branch=$(git branch --show-current 2>/dev/null)
|
||||
if [[ -n "$branch" ]]; then
|
||||
local first
|
||||
first=$(git status --porcelain -uno 2>/dev/null | head -1)
|
||||
[[ -n "$first" ]] && dirty_opt="--status=dirty"
|
||||
_cmux_send "report_git_branch $branch $dirty_opt --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
else
|
||||
_cmux_send "clear_git_branch --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
|
||||
fi
|
||||
_cmux_report_git_branch_for_path "$pwd"
|
||||
} >/dev/null 2>&1 &!
|
||||
_CMUX_GIT_JOB_PID=$!
|
||||
_CMUX_GIT_JOB_STARTED_AT=$now
|
||||
fi
|
||||
fi
|
||||
|
||||
# Pull request metadata is remote state. Keep a lightweight background poll
|
||||
# alive while the shell is idle so gh-created PRs and merge status changes
|
||||
# appear even without another prompt.
|
||||
local should_restart_pr_poll=0
|
||||
local pr_context_changed=0
|
||||
if [[ -n "$_CMUX_PR_POLL_PWD" && "$pwd" != "$_CMUX_PR_POLL_PWD" ]]; then
|
||||
pr_context_changed=1
|
||||
elif (( git_head_changed )); then
|
||||
pr_context_changed=1
|
||||
fi
|
||||
if [[ "$pwd" != "$_CMUX_PR_POLL_PWD" ]]; then
|
||||
should_restart_pr_poll=1
|
||||
elif (( _CMUX_PR_FORCE )); then
|
||||
should_restart_pr_poll=1
|
||||
elif [[ -z "$_CMUX_PR_POLL_PID" ]] || ! kill -0 "$_CMUX_PR_POLL_PID" 2>/dev/null; then
|
||||
should_restart_pr_poll=1
|
||||
fi
|
||||
|
||||
if (( should_restart_pr_poll )); then
|
||||
_CMUX_PR_FORCE=0
|
||||
if (( pr_context_changed )); then
|
||||
_cmux_clear_pr_for_panel
|
||||
fi
|
||||
_cmux_start_pr_poll_loop "$pwd" 1
|
||||
fi
|
||||
|
||||
# Ports: lightweight kick to the app's batched scanner.
|
||||
# - Periodic scan to avoid stale values.
|
||||
# - Forced scan when a long-running command returns to the prompt (common when stopping a server).
|
||||
|
|
@ -262,24 +527,32 @@ _cmux_precmd() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Ensure Resources/bin is at the front of PATH. Shell init (.zprofile/.zshrc)
|
||||
# may prepend other dirs that push our wrapper behind the system claude binary.
|
||||
# Ensure Resources/bin is at the front of PATH, and remove the app's
|
||||
# Contents/MacOS entry so the GUI cmux binary cannot shadow the CLI cmux.
|
||||
# Shell init (.zprofile/.zshrc) may prepend other dirs after launch.
|
||||
# We fix this once on first prompt (after all init files have run).
|
||||
_cmux_fix_path() {
|
||||
if [[ -n "${GHOSTTY_BIN_DIR:-}" ]]; then
|
||||
local bin_dir="${GHOSTTY_BIN_DIR%/MacOS}"
|
||||
bin_dir="${bin_dir}/Resources/bin"
|
||||
local gui_dir="${GHOSTTY_BIN_DIR%/}"
|
||||
local bin_dir="${gui_dir%/MacOS}/Resources/bin"
|
||||
if [[ -d "$bin_dir" ]]; then
|
||||
# Remove existing entry and re-prepend.
|
||||
# Remove existing entries and re-prepend the CLI bin dir.
|
||||
local -a parts=("${(@s/:/)PATH}")
|
||||
parts=("${(@)parts:#$bin_dir}")
|
||||
parts=("${(@)parts:#$gui_dir}")
|
||||
PATH="${bin_dir}:${(j/:/)parts}"
|
||||
fi
|
||||
fi
|
||||
add-zsh-hook -d precmd _cmux_fix_path
|
||||
}
|
||||
|
||||
_cmux_zshexit() {
|
||||
_cmux_stop_git_head_watch
|
||||
_cmux_stop_pr_poll_loop
|
||||
}
|
||||
|
||||
autoload -Uz add-zsh-hook
|
||||
add-zsh-hook preexec _cmux_preexec
|
||||
add-zsh-hook precmd _cmux_precmd
|
||||
add-zsh-hook precmd _cmux_fix_path
|
||||
add-zsh-hook zshexit _cmux_zshexit
|
||||
|
|
|
|||
705
Sources/AppleScriptSupport.swift
Normal file
|
|
@ -0,0 +1,705 @@
|
|||
import AppKit
|
||||
|
||||
private enum AppleScriptStrings {
|
||||
static let disabled = String(
|
||||
localized: "applescript.error.disabled",
|
||||
defaultValue: "AppleScript is disabled by the macos-applescript configuration."
|
||||
)
|
||||
static let missingAction = String(
|
||||
localized: "applescript.error.missingAction",
|
||||
defaultValue: "Missing action string."
|
||||
)
|
||||
static let missingInputText = String(
|
||||
localized: "applescript.error.missingInputText",
|
||||
defaultValue: "Missing input text."
|
||||
)
|
||||
static let missingTerminalTarget = String(
|
||||
localized: "applescript.error.missingTerminalTarget",
|
||||
defaultValue: "Missing terminal target."
|
||||
)
|
||||
static let missingSplitDirection = String(
|
||||
localized: "applescript.error.missingSplitDirection",
|
||||
defaultValue: "Missing or unknown split direction."
|
||||
)
|
||||
static let windowUnavailable = String(
|
||||
localized: "applescript.error.windowUnavailable",
|
||||
defaultValue: "Window is no longer available."
|
||||
)
|
||||
static let workspaceUnavailable = String(
|
||||
localized: "applescript.error.workspaceUnavailable",
|
||||
defaultValue: "Workspace is no longer available."
|
||||
)
|
||||
static let terminalUnavailable = String(
|
||||
localized: "applescript.error.terminalUnavailable",
|
||||
defaultValue: "Terminal is no longer available."
|
||||
)
|
||||
static let failedToCreateWindow = String(
|
||||
localized: "applescript.error.failedToCreateWindow",
|
||||
defaultValue: "Failed to create window."
|
||||
)
|
||||
static let failedToCreateWorkspace = String(
|
||||
localized: "applescript.error.failedToCreateWorkspace",
|
||||
defaultValue: "Failed to create workspace."
|
||||
)
|
||||
static let failedToCreateSplit = String(
|
||||
localized: "applescript.error.failedToCreateSplit",
|
||||
defaultValue: "Failed to create split."
|
||||
)
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var fourCharCode: UInt32 {
|
||||
utf8.reduce(0) { ($0 << 8) + UInt32($1) }
|
||||
}
|
||||
}
|
||||
|
||||
private extension Workspace {
|
||||
func scriptingTerminalPanels() -> [TerminalPanel] {
|
||||
var results: [TerminalPanel] = []
|
||||
var seen: Set<UUID> = []
|
||||
|
||||
for panelId in sidebarOrderedPanelIds() {
|
||||
guard seen.insert(panelId).inserted,
|
||||
let terminal = terminalPanel(for: panelId) else {
|
||||
continue
|
||||
}
|
||||
results.append(terminal)
|
||||
}
|
||||
|
||||
let remaining = panels.values
|
||||
.compactMap { $0 as? TerminalPanel }
|
||||
.sorted { $0.id.uuidString < $1.id.uuidString }
|
||||
|
||||
for terminal in remaining where seen.insert(terminal.id).inserted {
|
||||
results.append(terminal)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
extension NSApplication {
|
||||
var isAppleScriptEnabled: Bool {
|
||||
GhosttyApp.shared.appleScriptAutomationEnabled()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func validateScript(command: NSScriptCommand) -> Bool {
|
||||
guard isAppleScriptEnabled else {
|
||||
command.scriptErrorNumber = errAEEventNotPermitted
|
||||
command.scriptErrorString = AppleScriptStrings.disabled
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@objc(scriptWindows)
|
||||
var scriptWindows: [ScriptWindow] {
|
||||
guard isAppleScriptEnabled,
|
||||
let appDelegate = AppDelegate.shared else {
|
||||
return []
|
||||
}
|
||||
return appDelegate.scriptableMainWindows().map { ScriptWindow(windowId: $0.windowId) }
|
||||
}
|
||||
|
||||
@objc(frontWindow)
|
||||
var frontWindow: ScriptWindow? {
|
||||
scriptWindows.first
|
||||
}
|
||||
|
||||
@objc(valueInScriptWindowsWithUniqueID:)
|
||||
func valueInScriptWindows(uniqueID: String) -> ScriptWindow? {
|
||||
guard isAppleScriptEnabled,
|
||||
let windowId = UUID(uuidString: uniqueID),
|
||||
let appDelegate = AppDelegate.shared,
|
||||
appDelegate.scriptableMainWindow(windowId: windowId) != nil else {
|
||||
return nil
|
||||
}
|
||||
return ScriptWindow(windowId: windowId)
|
||||
}
|
||||
|
||||
@objc(terminals)
|
||||
var terminals: [ScriptTerminal] {
|
||||
guard isAppleScriptEnabled,
|
||||
let appDelegate = AppDelegate.shared else {
|
||||
return []
|
||||
}
|
||||
|
||||
return appDelegate.scriptableMainWindows()
|
||||
.flatMap { state in
|
||||
state.tabManager.tabs.flatMap { workspace in
|
||||
workspace.scriptingTerminalPanels().map {
|
||||
ScriptTerminal(workspaceId: workspace.id, terminalId: $0.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(valueInTerminalsWithUniqueID:)
|
||||
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||
guard isAppleScriptEnabled,
|
||||
let terminalId = UUID(uuidString: uniqueID),
|
||||
let appDelegate = AppDelegate.shared else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for state in appDelegate.scriptableMainWindows() {
|
||||
for workspace in state.tabManager.tabs where workspace.terminalPanel(for: terminalId) != nil {
|
||||
return ScriptTerminal(workspaceId: workspace.id, terminalId: terminalId)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc(handlePerformActionScriptCommand:)
|
||||
func handlePerformActionScriptCommand(_ command: NSScriptCommand) -> NSNumber? {
|
||||
guard validateScript(command: command) else { return nil }
|
||||
|
||||
guard let action = command.directParameter as? String else {
|
||||
command.scriptErrorNumber = errAEParamMissed
|
||||
command.scriptErrorString = AppleScriptStrings.missingAction
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let terminal = command.evaluatedArguments?["on"] as? ScriptTerminal else {
|
||||
command.scriptErrorNumber = errAEParamMissed
|
||||
command.scriptErrorString = AppleScriptStrings.missingTerminalTarget
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSNumber(value: terminal.perform(action: action))
|
||||
}
|
||||
|
||||
@objc(handleNewWindowScriptCommand:)
|
||||
func handleNewWindowScriptCommand(_ command: NSScriptCommand) -> ScriptWindow? {
|
||||
guard validateScript(command: command) else { return nil }
|
||||
|
||||
guard let appDelegate = AppDelegate.shared else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.failedToCreateWindow
|
||||
return nil
|
||||
}
|
||||
|
||||
let windowId = appDelegate.createMainWindow()
|
||||
return ScriptWindow(windowId: windowId)
|
||||
}
|
||||
|
||||
@objc(handleNewTabScriptCommand:)
|
||||
func handleNewTabScriptCommand(_ command: NSScriptCommand) -> ScriptTab? {
|
||||
guard validateScript(command: command) else { return nil }
|
||||
|
||||
guard let appDelegate = AppDelegate.shared else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.failedToCreateWorkspace
|
||||
return nil
|
||||
}
|
||||
|
||||
if let targetWindow = command.evaluatedArguments?["window"] as? ScriptWindow {
|
||||
guard let workspaceId = appDelegate.addWorkspace(windowId: targetWindow.windowId, bringToFront: false) else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.failedToCreateWorkspace
|
||||
return nil
|
||||
}
|
||||
return ScriptTab(windowId: targetWindow.windowId, tabId: workspaceId)
|
||||
}
|
||||
|
||||
if let frontWindow = scriptWindows.first,
|
||||
let workspaceId = appDelegate.addWorkspace(windowId: frontWindow.windowId, bringToFront: false) {
|
||||
return ScriptTab(windowId: frontWindow.windowId, tabId: workspaceId)
|
||||
}
|
||||
|
||||
let windowId = appDelegate.createMainWindow()
|
||||
return ScriptWindow(windowId: windowId).selectedTab
|
||||
}
|
||||
|
||||
@objc(handleQuitScriptCommand:)
|
||||
func handleQuitScriptCommand(_ command: NSScriptCommand) {
|
||||
guard validateScript(command: command) else { return }
|
||||
terminate(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc(CmuxScriptWindow)
|
||||
final class ScriptWindow: NSObject {
|
||||
let windowId: UUID
|
||||
|
||||
init(windowId: UUID) {
|
||||
self.windowId = windowId
|
||||
}
|
||||
|
||||
private var state: AppDelegate.ScriptableMainWindowState? {
|
||||
AppDelegate.shared?.scriptableMainWindow(windowId: windowId)
|
||||
}
|
||||
|
||||
@objc(id)
|
||||
var idValue: String {
|
||||
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||
return windowId.uuidString
|
||||
}
|
||||
|
||||
@objc(title)
|
||||
var title: String {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let state else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let windowTitle = state.window?.title.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !windowTitle.isEmpty {
|
||||
return windowTitle
|
||||
}
|
||||
|
||||
return state.tabManager.selectedWorkspace?.title ?? ""
|
||||
}
|
||||
|
||||
@objc(tabs)
|
||||
var tabs: [ScriptTab] {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let state else {
|
||||
return []
|
||||
}
|
||||
return state.tabManager.tabs.map { ScriptTab(windowId: windowId, tabId: $0.id) }
|
||||
}
|
||||
|
||||
@objc(selectedTab)
|
||||
var selectedTab: ScriptTab? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let selectedId = state?.tabManager.selectedTabId else {
|
||||
return nil
|
||||
}
|
||||
return ScriptTab(windowId: windowId, tabId: selectedId)
|
||||
}
|
||||
|
||||
@objc(terminals)
|
||||
var terminals: [ScriptTerminal] {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let state else {
|
||||
return []
|
||||
}
|
||||
return state.tabManager.tabs.flatMap { workspace in
|
||||
workspace.scriptingTerminalPanels().map {
|
||||
ScriptTerminal(workspaceId: workspace.id, terminalId: $0.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(valueInTabsWithUniqueID:)
|
||||
func valueInTabs(uniqueID: String) -> ScriptTab? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let tabId = UUID(uuidString: uniqueID),
|
||||
let state,
|
||||
state.tabManager.tabs.contains(where: { $0.id == tabId }) else {
|
||||
return nil
|
||||
}
|
||||
return ScriptTab(windowId: windowId, tabId: tabId)
|
||||
}
|
||||
|
||||
@objc(valueInTerminalsWithUniqueID:)
|
||||
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let terminalId = UUID(uuidString: uniqueID),
|
||||
let state else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for workspace in state.tabManager.tabs where workspace.terminalPanel(for: terminalId) != nil {
|
||||
return ScriptTerminal(workspaceId: workspace.id, terminalId: terminalId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc(handleActivateWindowCommand:)
|
||||
func handleActivateWindow(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard AppDelegate.shared?.focusScriptableMainWindow(windowId: windowId, bringToFront: true) == true else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc(handleCloseWindowCommand:)
|
||||
func handleCloseWindow(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard let window = state?.window else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
window.performClose(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSUniqueIDSpecifier(
|
||||
containerClassDescription: appClassDescription,
|
||||
containerSpecifier: nil,
|
||||
key: "scriptWindows",
|
||||
uniqueID: windowId.uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc(CmuxScriptTab)
|
||||
final class ScriptTab: NSObject {
|
||||
let windowId: UUID
|
||||
let tabId: UUID
|
||||
|
||||
init(windowId: UUID, tabId: UUID) {
|
||||
self.windowId = windowId
|
||||
self.tabId = tabId
|
||||
}
|
||||
|
||||
private var state: AppDelegate.ScriptableMainWindowState? {
|
||||
AppDelegate.shared?.scriptableMainWindow(windowId: windowId)
|
||||
}
|
||||
|
||||
private var workspace: Workspace? {
|
||||
state?.tabManager.tabs.first(where: { $0.id == tabId })
|
||||
}
|
||||
|
||||
private var window: ScriptWindow {
|
||||
ScriptWindow(windowId: windowId)
|
||||
}
|
||||
|
||||
@objc(id)
|
||||
var idValue: String {
|
||||
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||
return tabId.uuidString
|
||||
}
|
||||
|
||||
@objc(title)
|
||||
var title: String {
|
||||
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||
return workspace?.title ?? ""
|
||||
}
|
||||
|
||||
@objc(index)
|
||||
var index: Int {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let state,
|
||||
let idx = state.tabManager.tabs.firstIndex(where: { $0.id == tabId }) else {
|
||||
return 0
|
||||
}
|
||||
return idx + 1
|
||||
}
|
||||
|
||||
@objc(selected)
|
||||
var selected: Bool {
|
||||
guard NSApp.isAppleScriptEnabled else { return false }
|
||||
return state?.tabManager.selectedTabId == tabId
|
||||
}
|
||||
|
||||
@objc(focusedTerminal)
|
||||
var focusedTerminal: ScriptTerminal? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let terminalId = workspace?.focusedTerminalPanel?.id else {
|
||||
return nil
|
||||
}
|
||||
return ScriptTerminal(workspaceId: tabId, terminalId: terminalId)
|
||||
}
|
||||
|
||||
@objc(terminals)
|
||||
var terminals: [ScriptTerminal] {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let workspace else {
|
||||
return []
|
||||
}
|
||||
return workspace.scriptingTerminalPanels().map {
|
||||
ScriptTerminal(workspaceId: tabId, terminalId: $0.id)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(valueInTerminalsWithUniqueID:)
|
||||
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let workspace,
|
||||
let terminalId = UUID(uuidString: uniqueID),
|
||||
workspace.terminalPanel(for: terminalId) != nil else {
|
||||
return nil
|
||||
}
|
||||
return ScriptTerminal(workspaceId: tabId, terminalId: terminalId)
|
||||
}
|
||||
|
||||
@objc(handleSelectTabCommand:)
|
||||
func handleSelectTab(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard let state,
|
||||
let workspace else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.workspaceUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
state.tabManager.selectWorkspace(workspace)
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc(handleCloseTabCommand:)
|
||||
func handleCloseTab(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard let state,
|
||||
let workspace else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.workspaceUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
if state.tabManager.tabs.count > 1 {
|
||||
state.tabManager.closeWorkspace(workspace)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let window = state.window else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
window.performClose(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let windowClassDescription = window.classDescription as? NSScriptClassDescription,
|
||||
let windowSpecifier = window.objectSpecifier else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSUniqueIDSpecifier(
|
||||
containerClassDescription: windowClassDescription,
|
||||
containerSpecifier: windowSpecifier,
|
||||
key: "tabs",
|
||||
uniqueID: tabId.uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc(CmuxScriptTerminal)
|
||||
final class ScriptTerminal: NSObject {
|
||||
let workspaceId: UUID
|
||||
let terminalId: UUID
|
||||
|
||||
init(workspaceId: UUID, terminalId: UUID) {
|
||||
self.workspaceId = workspaceId
|
||||
self.terminalId = terminalId
|
||||
}
|
||||
|
||||
private var state: AppDelegate.ScriptableMainWindowState? {
|
||||
AppDelegate.shared?.scriptableMainWindowForTab(workspaceId)
|
||||
}
|
||||
|
||||
private var workspace: Workspace? {
|
||||
state?.tabManager.tabs.first(where: { $0.id == workspaceId })
|
||||
}
|
||||
|
||||
private var terminal: TerminalPanel? {
|
||||
workspace?.terminalPanel(for: terminalId)
|
||||
}
|
||||
|
||||
@objc(id)
|
||||
var stableID: String {
|
||||
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||
return terminalId.uuidString
|
||||
}
|
||||
|
||||
@objc(title)
|
||||
var title: String {
|
||||
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||
return terminal?.displayTitle ?? ""
|
||||
}
|
||||
|
||||
@objc(workingDirectory)
|
||||
var workingDirectory: String {
|
||||
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||
return terminal?.directory ?? ""
|
||||
}
|
||||
|
||||
func input(text: String) -> Bool {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let terminal else {
|
||||
return false
|
||||
}
|
||||
terminal.sendText(text)
|
||||
return true
|
||||
}
|
||||
|
||||
func perform(action: String) -> Bool {
|
||||
guard NSApp.isAppleScriptEnabled else { return false }
|
||||
return terminal?.performBindingAction(action) ?? false
|
||||
}
|
||||
|
||||
@objc(handleSplitCommand:)
|
||||
func handleSplit(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard let directionCode = command.evaluatedArguments?["direction"] as? UInt32,
|
||||
let direction = ScriptSplitDirection(code: directionCode)?.splitDirection else {
|
||||
command.scriptErrorNumber = errAEParamMissed
|
||||
command.scriptErrorString = AppleScriptStrings.missingSplitDirection
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let state,
|
||||
let workspace,
|
||||
terminal != nil else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let newPanelId = state.tabManager.newSplit(tabId: workspaceId, surfaceId: terminalId, direction: direction),
|
||||
workspace.terminalPanel(for: newPanelId) != nil else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.failedToCreateSplit
|
||||
return nil
|
||||
}
|
||||
|
||||
return ScriptTerminal(workspaceId: workspaceId, terminalId: newPanelId)
|
||||
}
|
||||
|
||||
@objc(handleFocusCommand:)
|
||||
func handleFocus(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard let state,
|
||||
let workspace,
|
||||
terminal != nil else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
if let app = AppDelegate.shared {
|
||||
_ = app.focusScriptableMainWindow(windowId: state.windowId, bringToFront: true)
|
||||
}
|
||||
state.tabManager.selectWorkspace(workspace)
|
||||
workspace.focusPanel(terminalId)
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc(handleCloseCommand:)
|
||||
func handleClose(_ command: NSScriptCommand) -> Any? {
|
||||
guard NSApp.validateScript(command: command) else { return nil }
|
||||
|
||||
guard let state,
|
||||
let workspace,
|
||||
terminal != nil else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
if workspace.panels.count == 1 {
|
||||
if state.tabManager.tabs.count > 1 {
|
||||
state.tabManager.closeWorkspace(workspace)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let window = state.window else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
window.performClose(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard workspace.closePanel(terminalId, force: true) else {
|
||||
command.scriptErrorNumber = errAEEventFailed
|
||||
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||
return nil
|
||||
}
|
||||
|
||||
AppDelegate.shared?.notificationStore?.clearNotifications(forTabId: workspaceId, surfaceId: terminalId)
|
||||
return nil
|
||||
}
|
||||
|
||||
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||
guard NSApp.isAppleScriptEnabled,
|
||||
let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSUniqueIDSpecifier(
|
||||
containerClassDescription: appClassDescription,
|
||||
containerSpecifier: nil,
|
||||
key: "terminals",
|
||||
uniqueID: terminalId.uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc(CmuxScriptInputTextCommand)
|
||||
final class ScriptInputTextCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard NSApp.validateScript(command: self) else { return nil }
|
||||
|
||||
guard let text = directParameter as? String else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = AppleScriptStrings.missingInputText
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = AppleScriptStrings.missingTerminalTarget
|
||||
return nil
|
||||
}
|
||||
|
||||
guard terminal.input(text: text) else {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private enum ScriptSplitDirection {
|
||||
case right
|
||||
case left
|
||||
case down
|
||||
case up
|
||||
|
||||
init?(code: UInt32) {
|
||||
switch code {
|
||||
case "GSrt".fourCharCode: self = .right
|
||||
case "GSlf".fourCharCode: self = .left
|
||||
case "GSdn".fourCharCode: self = .down
|
||||
case "GSup".fourCharCode: self = .up
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var splitDirection: SplitDirection {
|
||||
switch self {
|
||||
case .right: return .right
|
||||
case .left: return .left
|
||||
case .down: return .down
|
||||
case .up: return .up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,15 @@ struct Backport<Content> {
|
|||
|
||||
extension View {
|
||||
var backport: Backport<Self> { Backport(content: self) }
|
||||
|
||||
@ViewBuilder
|
||||
func safeHelp(_ text: String) -> some View {
|
||||
if text.isEmpty {
|
||||
self
|
||||
} else {
|
||||
self.help(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Scene {
|
||||
|
|
|
|||
207
Sources/Find/BrowserFindJavaScript.swift
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import Foundation
|
||||
|
||||
/// JavaScript snippets for find-in-page in WKWebView.
|
||||
///
|
||||
/// Uses TreeWalker to scan text nodes and wraps matches with `<mark>` elements.
|
||||
/// The current match gets an additional `.current` class and is scrolled into view.
|
||||
enum BrowserFindJavaScript {
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Returns JS that highlights all occurrences of `query` in the document body.
|
||||
/// The script evaluates to a JSON string `{"total":N,"current":0}`.
|
||||
static func searchScript(query: String) -> String {
|
||||
let escaped = jsStringEscape(query)
|
||||
return """
|
||||
(() => {
|
||||
const MARK_CLASS = '__cmux-find';
|
||||
const CURRENT_CLASS = '__cmux-find-current';
|
||||
|
||||
// Remove previous highlights first.
|
||||
\(clearBody)
|
||||
|
||||
const query = "\(escaped)";
|
||||
if (!query) return JSON.stringify({total: 0, current: 0});
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const SKIP_TAGS = new Set(['SCRIPT','STYLE','NOSCRIPT','TEMPLATE','IFRAME','SVG']);
|
||||
const isVisible = (el) => {
|
||||
while (el && el !== document.body) {
|
||||
if (SKIP_TAGS.has(el.tagName)) return false;
|
||||
if (el.getAttribute('aria-hidden') === 'true') return false;
|
||||
const st = getComputedStyle(el);
|
||||
if (st.display === 'none' || st.visibility === 'hidden') return false;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{ acceptNode(node) { return isVisible(node.parentElement) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } }
|
||||
);
|
||||
const matches = [];
|
||||
const textNodes = [];
|
||||
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
||||
|
||||
for (const node of textNodes) {
|
||||
const text = node.textContent || '';
|
||||
const lowerText = text.toLowerCase();
|
||||
let startIndex = 0;
|
||||
const parts = [];
|
||||
let lastEnd = 0;
|
||||
while (true) {
|
||||
const idx = lowerText.indexOf(lowerQuery, startIndex);
|
||||
if (idx === -1) break;
|
||||
parts.push({ start: idx, end: idx + query.length });
|
||||
startIndex = idx + query.length;
|
||||
}
|
||||
if (parts.length === 0) continue;
|
||||
|
||||
const parent = node.parentNode;
|
||||
if (!parent) continue;
|
||||
const frag = document.createDocumentFragment();
|
||||
let pos = 0;
|
||||
for (const part of parts) {
|
||||
if (part.start > pos) {
|
||||
frag.appendChild(document.createTextNode(text.substring(pos, part.start)));
|
||||
}
|
||||
const mark = document.createElement('mark');
|
||||
mark.className = MARK_CLASS;
|
||||
mark.textContent = text.substring(part.start, part.end);
|
||||
frag.appendChild(mark);
|
||||
matches.push(mark);
|
||||
pos = part.end;
|
||||
}
|
||||
if (pos < text.length) {
|
||||
frag.appendChild(document.createTextNode(text.substring(pos)));
|
||||
}
|
||||
parent.replaceChild(frag, node);
|
||||
}
|
||||
|
||||
window.__cmuxFindMatches = matches;
|
||||
window.__cmuxFindIndex = 0;
|
||||
|
||||
if (matches.length > 0) {
|
||||
matches[0].classList.add(CURRENT_CLASS);
|
||||
matches[0].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Inject highlight styles if not already present.
|
||||
if (!document.getElementById('__cmux-find-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = '__cmux-find-style';
|
||||
style.textContent = `
|
||||
mark.__cmux-find { background: #facc15; color: #000; border-radius: 2px; }
|
||||
mark.__cmux-find.__cmux-find-current { background: #f97316; color: #fff; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
return JSON.stringify({ total: matches.length, current: 0 });
|
||||
})()
|
||||
"""
|
||||
}
|
||||
|
||||
/// Returns JS that moves to the next match. Evaluates to `{"total":N,"current":M}`.
|
||||
static func nextScript() -> String {
|
||||
"""
|
||||
(() => {
|
||||
const matches = window.__cmuxFindMatches || [];
|
||||
if (matches.length === 0) return JSON.stringify({ total: 0, current: 0 });
|
||||
let idx = window.__cmuxFindIndex || 0;
|
||||
if (!matches[idx] || !matches[idx].isConnected) {
|
||||
window.__cmuxFindMatches = [];
|
||||
window.__cmuxFindIndex = 0;
|
||||
return JSON.stringify({ total: 0, current: 0 });
|
||||
}
|
||||
matches[idx].classList.remove('__cmux-find-current');
|
||||
idx = (idx + 1) % matches.length;
|
||||
if (!matches[idx] || !matches[idx].isConnected) {
|
||||
window.__cmuxFindMatches = [];
|
||||
window.__cmuxFindIndex = 0;
|
||||
return JSON.stringify({ total: 0, current: 0 });
|
||||
}
|
||||
matches[idx].classList.add('__cmux-find-current');
|
||||
matches[idx].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
window.__cmuxFindIndex = idx;
|
||||
return JSON.stringify({ total: matches.length, current: idx });
|
||||
})()
|
||||
"""
|
||||
}
|
||||
|
||||
/// Returns JS that moves to the previous match. Evaluates to `{"total":N,"current":M}`.
|
||||
static func previousScript() -> String {
|
||||
"""
|
||||
(() => {
|
||||
const matches = window.__cmuxFindMatches || [];
|
||||
if (matches.length === 0) return JSON.stringify({ total: 0, current: 0 });
|
||||
let idx = window.__cmuxFindIndex || 0;
|
||||
if (!matches[idx] || !matches[idx].isConnected) {
|
||||
window.__cmuxFindMatches = [];
|
||||
window.__cmuxFindIndex = 0;
|
||||
return JSON.stringify({ total: 0, current: 0 });
|
||||
}
|
||||
matches[idx].classList.remove('__cmux-find-current');
|
||||
idx = (idx - 1 + matches.length) % matches.length;
|
||||
if (!matches[idx] || !matches[idx].isConnected) {
|
||||
window.__cmuxFindMatches = [];
|
||||
window.__cmuxFindIndex = 0;
|
||||
return JSON.stringify({ total: 0, current: 0 });
|
||||
}
|
||||
matches[idx].classList.add('__cmux-find-current');
|
||||
matches[idx].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
window.__cmuxFindIndex = idx;
|
||||
return JSON.stringify({ total: matches.length, current: idx });
|
||||
})()
|
||||
"""
|
||||
}
|
||||
|
||||
/// Returns JS that removes all find highlights and restores the DOM.
|
||||
static func clearScript() -> String {
|
||||
"""
|
||||
(() => {
|
||||
\(clearBody)
|
||||
window.__cmuxFindMatches = [];
|
||||
window.__cmuxFindIndex = 0;
|
||||
const style = document.getElementById('__cmux-find-style');
|
||||
if (style) style.remove();
|
||||
return 'ok';
|
||||
})()
|
||||
"""
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
/// JS snippet (no wrapping IIFE) that removes existing mark highlights.
|
||||
private static let clearBody = """
|
||||
document.querySelectorAll('mark.__cmux-find').forEach(mark => {
|
||||
const parent = mark.parentNode;
|
||||
if (!parent) return;
|
||||
const text = document.createTextNode(mark.textContent || '');
|
||||
parent.replaceChild(text, mark);
|
||||
parent.normalize();
|
||||
});
|
||||
"""
|
||||
|
||||
/// Escape a Swift string for safe embedding inside a JS double-quoted string literal.
|
||||
static func jsStringEscape(_ string: String) -> String {
|
||||
var result = ""
|
||||
result.reserveCapacity(string.count)
|
||||
for scalar in string.unicodeScalars {
|
||||
switch scalar {
|
||||
case "\\": result += "\\\\"
|
||||
case "\"": result += "\\\""
|
||||
case "\n": result += "\\n"
|
||||
case "\r": result += "\\r"
|
||||
case "\t": result += "\\t"
|
||||
case "\0": result += "\\0"
|
||||
case "\u{2028}": result += "\\u2028"
|
||||
case "\u{2029}": result += "\\u2029"
|
||||
default:
|
||||
result.append(Character(scalar))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
251
Sources/Find/BrowserSearchOverlay.swift
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import AppKit
|
||||
import Bonsplit
|
||||
import SwiftUI
|
||||
|
||||
struct BrowserSearchOverlay: View {
|
||||
let panelId: UUID
|
||||
@ObservedObject var searchState: BrowserSearchState
|
||||
let focusRequestGeneration: UInt64
|
||||
let canApplyFocusRequest: (UInt64) -> Bool
|
||||
let onNext: () -> Void
|
||||
let onPrevious: () -> Void
|
||||
let onClose: () -> Void
|
||||
let onFieldDidFocus: () -> Void
|
||||
@State private var corner: Corner = .topRight
|
||||
@State private var dragOffset: CGSize = .zero
|
||||
@State private var barSize: CGSize = .zero
|
||||
@FocusState private var isSearchFieldFocused: Bool
|
||||
|
||||
private let padding: CGFloat = 8
|
||||
|
||||
#if DEBUG
|
||||
private func debugFirstResponderSummary() -> String {
|
||||
guard let window = NSApp.keyWindow else { return "nil" }
|
||||
guard let firstResponder = window.firstResponder else { return "nil" }
|
||||
if let editor = firstResponder as? NSTextView, editor.isFieldEditor {
|
||||
let delegateSummary = editor.delegate.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
return "fieldEditor(delegate=\(delegateSummary))"
|
||||
}
|
||||
return String(describing: type(of: firstResponder))
|
||||
}
|
||||
#endif
|
||||
|
||||
private func logFocusState(_ event: String) {
|
||||
#if DEBUG
|
||||
let keyWindow = NSApp.keyWindow
|
||||
dlog(
|
||||
"browser.findbar.focus panel=\(panelId.uuidString.prefix(5)) " +
|
||||
"event=\(event) keyWindow=\(keyWindow?.windowNumber ?? -1) " +
|
||||
"firstResponder=\(debugFirstResponderSummary()) " +
|
||||
"focused=\(isSearchFieldFocused ? 1 : 0)"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func requestSearchFieldFocus(maxAttempts: Int = 3, origin: String) {
|
||||
guard maxAttempts > 0 else { return }
|
||||
guard canApplyFocusRequest(focusRequestGeneration) else {
|
||||
#if DEBUG
|
||||
logFocusState("request.skip origin=\(origin) generation=\(focusRequestGeneration)")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
logFocusState("request.begin origin=\(origin) remaining=\(maxAttempts)")
|
||||
isSearchFieldFocused = true
|
||||
#if DEBUG
|
||||
DispatchQueue.main.async {
|
||||
guard canApplyFocusRequest(focusRequestGeneration) else {
|
||||
logFocusState("request.skipAsync origin=\(origin) generation=\(focusRequestGeneration)")
|
||||
return
|
||||
}
|
||||
logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
|
||||
}
|
||||
#endif
|
||||
guard maxAttempts > 1 else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
guard canApplyFocusRequest(focusRequestGeneration) else {
|
||||
#if DEBUG
|
||||
logFocusState("request.skipRetry origin=\(origin) generation=\(focusRequestGeneration)")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
requestSearchFieldFocus(maxAttempts: maxAttempts - 1, origin: origin)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
HStack(spacing: 4) {
|
||||
TextField("Search", text: $searchState.needle)
|
||||
.textFieldStyle(.plain)
|
||||
.accessibilityIdentifier("BrowserFindSearchTextField")
|
||||
.frame(width: 180)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 50)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.primary.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
.focused($isSearchFieldFocused)
|
||||
.overlay(alignment: .trailing) {
|
||||
if let selected = searchState.selected {
|
||||
let totalText = searchState.total.map { String($0) } ?? "?"
|
||||
Text("\(selected + 1)/\(totalText)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.monospacedDigit()
|
||||
.padding(.trailing, 8)
|
||||
} else if let total = searchState.total {
|
||||
Text(total == 0 ? "0/0" : "-/\(total)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.monospacedDigit()
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
.onExitCommand {
|
||||
onClose()
|
||||
}
|
||||
.onSubmit {
|
||||
// onSubmit fires only after IME composition is committed.
|
||||
if NSEvent.modifierFlags.contains(.shift) {
|
||||
onPrevious()
|
||||
} else {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("browser.findbar.next panel=\(panelId.uuidString.prefix(5))")
|
||||
#endif
|
||||
onNext()
|
||||
}) {
|
||||
Image(systemName: "chevron.up")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.safeHelp("Next match (Return)")
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("browser.findbar.prev panel=\(panelId.uuidString.prefix(5))")
|
||||
#endif
|
||||
onPrevious()
|
||||
}) {
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.safeHelp("Previous match (Shift+Return)")
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("browser.findbar.close panel=\(panelId.uuidString.prefix(5))")
|
||||
#endif
|
||||
onClose()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.safeHelp("Close (Esc)")
|
||||
}
|
||||
.padding(8)
|
||||
.background(.background)
|
||||
.clipShape(clipShape)
|
||||
.shadow(radius: 4)
|
||||
.onAppear {
|
||||
#if DEBUG
|
||||
dlog("browser.findbar.appear panel=\(panelId.uuidString.prefix(5))")
|
||||
#endif
|
||||
logFocusState("appear")
|
||||
requestSearchFieldFocus(origin: "appear")
|
||||
}
|
||||
.onChange(of: isSearchFieldFocused) { _, focused in
|
||||
logFocusState("focusState.change next=\(focused ? 1 : 0)")
|
||||
if focused {
|
||||
onFieldDidFocus()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .browserSearchFocus)) { notification in
|
||||
guard let notifiedPanelId = notification.object as? UUID,
|
||||
notifiedPanelId == panelId else { return }
|
||||
logFocusState("notification.received")
|
||||
DispatchQueue.main.async {
|
||||
requestSearchFieldFocus(origin: "notification")
|
||||
}
|
||||
}
|
||||
.background(
|
||||
GeometryReader { barGeo in
|
||||
Color.clear.onAppear {
|
||||
barSize = barGeo.size
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(padding)
|
||||
.offset(dragOffset)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: corner.alignment)
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onChanged { value in
|
||||
dragOffset = value.translation
|
||||
}
|
||||
.onEnded { value in
|
||||
let centerPos = centerPosition(for: corner, in: geo.size, barSize: barSize)
|
||||
let newCenter = CGPoint(
|
||||
x: centerPos.x + value.translation.width,
|
||||
y: centerPos.y + value.translation.height
|
||||
)
|
||||
let newCorner = closestCorner(to: newCenter, in: geo.size)
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
corner = newCorner
|
||||
dragOffset = .zero
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var clipShape: some Shape {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
}
|
||||
|
||||
enum Corner {
|
||||
case topLeft
|
||||
case topRight
|
||||
case bottomLeft
|
||||
case bottomRight
|
||||
|
||||
var alignment: Alignment {
|
||||
switch self {
|
||||
case .topLeft: return .topLeading
|
||||
case .topRight: return .topTrailing
|
||||
case .bottomLeft: return .bottomLeading
|
||||
case .bottomRight: return .bottomTrailing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func centerPosition(for corner: Corner, in containerSize: CGSize, barSize: CGSize) -> CGPoint {
|
||||
let halfWidth = barSize.width / 2 + padding
|
||||
let halfHeight = barSize.height / 2 + padding
|
||||
|
||||
switch corner {
|
||||
case .topLeft:
|
||||
return CGPoint(x: halfWidth, y: halfHeight)
|
||||
case .topRight:
|
||||
return CGPoint(x: containerSize.width - halfWidth, y: halfHeight)
|
||||
case .bottomLeft:
|
||||
return CGPoint(x: halfWidth, y: containerSize.height - halfHeight)
|
||||
case .bottomRight:
|
||||
return CGPoint(x: containerSize.width - halfWidth, y: containerSize.height - halfHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private func closestCorner(to point: CGPoint, in containerSize: CGSize) -> Corner {
|
||||
let midX = containerSize.width / 2
|
||||
let midY = containerSize.height / 2
|
||||
|
||||
if point.x < midX {
|
||||
return point.y < midY ? .topLeft : .bottomLeft
|
||||
}
|
||||
return point.y < midY ? .topRight : .bottomRight
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,68 @@
|
|||
import AppKit
|
||||
import Bonsplit
|
||||
import SwiftUI
|
||||
|
||||
private extension NSView {
|
||||
func cmuxAncestor<T: NSView>(of type: T.Type) -> T? {
|
||||
var current: NSView? = self
|
||||
while let view = current {
|
||||
if let target = view as? T {
|
||||
return target
|
||||
}
|
||||
current = view.superview
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct SurfaceSearchOverlay: View {
|
||||
let surface: TerminalSurface
|
||||
let tabId: UUID
|
||||
let surfaceId: UUID
|
||||
@ObservedObject var searchState: TerminalSurface.SearchState
|
||||
let onMoveFocusToTerminal: () -> Void
|
||||
let onNavigateSearch: (_ action: String) -> Void
|
||||
let onFieldDidFocus: () -> Void
|
||||
let onClose: () -> Void
|
||||
@State private var corner: Corner = .topRight
|
||||
@State private var dragOffset: CGSize = .zero
|
||||
@State private var barSize: CGSize = .zero
|
||||
@FocusState private var isSearchFieldFocused: Bool
|
||||
@State private var isSearchFieldFocused: Bool = true
|
||||
|
||||
private let padding: CGFloat = 8
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
HStack(spacing: 4) {
|
||||
TextField("Search", text: $searchState.needle)
|
||||
.textFieldStyle(.plain)
|
||||
.frame(width: 180)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 50)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.primary.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
.focused($isSearchFieldFocused)
|
||||
.overlay(alignment: .trailing) {
|
||||
SearchTextFieldRepresentable(
|
||||
text: $searchState.needle,
|
||||
isFocused: $isSearchFieldFocused,
|
||||
surfaceId: surfaceId,
|
||||
onFieldDidFocus: onFieldDidFocus,
|
||||
onEscape: {
|
||||
#if DEBUG
|
||||
dlog("find.nativeField.escape surface=\(surfaceId.uuidString.prefix(5)) needleEmpty=\(searchState.needle.isEmpty)")
|
||||
#endif
|
||||
if searchState.needle.isEmpty {
|
||||
onClose()
|
||||
} else {
|
||||
onMoveFocusToTerminal()
|
||||
}
|
||||
},
|
||||
onReturn: { isShift in
|
||||
let action = isShift
|
||||
? "navigate_search:previous"
|
||||
: "navigate_search:next"
|
||||
onNavigateSearch(action)
|
||||
}
|
||||
)
|
||||
.accessibilityIdentifier("TerminalFindSearchTextField")
|
||||
.frame(width: 180)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 50)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.primary.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
.overlay(alignment: .trailing) {
|
||||
if let selected = searchState.selected {
|
||||
let totalText = searchState.total.map { String($0) } ?? "?"
|
||||
Text("\(selected + 1)/\(totalText)")
|
||||
|
|
@ -40,69 +78,50 @@ struct SurfaceSearchOverlay: View {
|
|||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
.onExitCommand {
|
||||
if searchState.needle.isEmpty {
|
||||
onClose()
|
||||
} else {
|
||||
surface.hostedView.moveFocus()
|
||||
}
|
||||
}
|
||||
.backport.onKeyPress(.return) { modifiers in
|
||||
let action = modifiers.contains(.shift)
|
||||
? "navigate_search:previous"
|
||||
: "navigate_search:next"
|
||||
_ = surface.performBindingAction(action)
|
||||
return .handled
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("findbar.next surface=\(surface.id.uuidString.prefix(5))")
|
||||
dlog("findbar.next surface=\(surfaceId.uuidString.prefix(5))")
|
||||
#endif
|
||||
_ = surface.performBindingAction("navigate_search:next")
|
||||
onNavigateSearch("navigate_search:next")
|
||||
}) {
|
||||
Image(systemName: "chevron.up")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.help("Next match (Return)")
|
||||
.safeHelp(String(localized: "search.nextMatch.help", defaultValue: "Next match (Return)"))
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("findbar.prev surface=\(surface.id.uuidString.prefix(5))")
|
||||
dlog("findbar.prev surface=\(surfaceId.uuidString.prefix(5))")
|
||||
#endif
|
||||
_ = surface.performBindingAction("navigate_search:previous")
|
||||
onNavigateSearch("navigate_search:previous")
|
||||
}) {
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.help("Previous match (Shift+Return)")
|
||||
.safeHelp(String(localized: "search.previousMatch.help", defaultValue: "Previous match (Shift+Return)"))
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
dlog("findbar.close surface=\(surface.id.uuidString.prefix(5))")
|
||||
dlog("findbar.close surface=\(surfaceId.uuidString.prefix(5))")
|
||||
#endif
|
||||
onClose()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.help("Close (Esc)")
|
||||
.safeHelp(String(localized: "search.close.help", defaultValue: "Close (Esc)"))
|
||||
}
|
||||
.padding(8)
|
||||
.background(.background)
|
||||
.clipShape(clipShape)
|
||||
.shadow(radius: 4)
|
||||
.onAppear {
|
||||
NSLog("Find: overlay appear tab=%@ surface=%@", surface.tabId.uuidString, surface.id.uuidString)
|
||||
#if DEBUG
|
||||
dlog("find.overlay.appear tab=\(tabId.uuidString.prefix(5)) surface=\(surfaceId.uuidString.prefix(5))")
|
||||
#endif
|
||||
isSearchFieldFocused = true
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ghosttySearchFocus)) { notification in
|
||||
guard notification.object as? TerminalSurface === surface else { return }
|
||||
NSLog("Find: overlay focus tab=%@ surface=%@", surface.tabId.uuidString, surface.id.uuidString)
|
||||
DispatchQueue.main.async {
|
||||
isSearchFieldFocused = true
|
||||
}
|
||||
}
|
||||
.background(
|
||||
GeometryReader { barGeo in
|
||||
Color.clear.onAppear {
|
||||
|
|
@ -181,6 +200,203 @@ struct SurfaceSearchOverlay: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Native Search Text Field (AppKit)
|
||||
|
||||
/// NSTextField subclass for the terminal find bar.
|
||||
/// Strips visual chrome so SwiftUI handles the background/border appearance.
|
||||
private final class SearchNativeTextField: NSTextField {
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
isBordered = false
|
||||
isBezeled = false
|
||||
drawsBackground = false
|
||||
focusRingType = .none
|
||||
usesSingleLineMode = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
/// NSViewRepresentable wrapping SearchNativeTextField.
|
||||
/// Handles Escape and Return at the AppKit delegate level, eliminating the
|
||||
/// SwiftUI @FocusState / AppKit first-responder mismatch that broke focus
|
||||
/// after window switching.
|
||||
private struct SearchTextFieldRepresentable: NSViewRepresentable {
|
||||
@Binding var text: String
|
||||
@Binding var isFocused: Bool
|
||||
let surfaceId: UUID
|
||||
let onFieldDidFocus: () -> Void
|
||||
let onEscape: () -> Void
|
||||
let onReturn: (_ isShift: Bool) -> Void
|
||||
|
||||
final class Coordinator: NSObject, NSTextFieldDelegate {
|
||||
var parent: SearchTextFieldRepresentable
|
||||
var isProgrammaticMutation = false
|
||||
weak var parentField: SearchNativeTextField?
|
||||
var pendingFocusRequest: Bool?
|
||||
var searchFocusObserver: NSObjectProtocol?
|
||||
|
||||
init(parent: SearchTextFieldRepresentable) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let searchFocusObserver {
|
||||
NotificationCenter.default.removeObserver(searchFocusObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
guard !isProgrammaticMutation else { return }
|
||||
guard let field = obj.object as? NSTextField else { return }
|
||||
parent.text = field.stringValue
|
||||
}
|
||||
|
||||
func controlTextDidBeginEditing(_ obj: Notification) {
|
||||
#if DEBUG
|
||||
dlog("find.nativeField.beginEditing surface=\(parent.surfaceId.uuidString.prefix(5))")
|
||||
#endif
|
||||
parent.onFieldDidFocus()
|
||||
if !parent.isFocused {
|
||||
DispatchQueue.main.async {
|
||||
self.parent.isFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification) {
|
||||
#if DEBUG
|
||||
dlog("find.nativeField.endEditing surface=\(parent.surfaceId.uuidString.prefix(5))")
|
||||
#endif
|
||||
if parent.isFocused {
|
||||
DispatchQueue.main.async {
|
||||
self.parent.isFocused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
switch commandSelector {
|
||||
case #selector(NSResponder.cancelOperation(_:)):
|
||||
// Don't intercept Escape during CJK IME composition (issue #118)
|
||||
if textView.hasMarkedText() { return false }
|
||||
control.cmuxAncestor(of: GhosttySurfaceScrollView.self)?.beginFindEscapeSuppression()
|
||||
parent.onEscape()
|
||||
return true
|
||||
case #selector(NSResponder.insertNewline(_:)):
|
||||
if textView.hasMarkedText() { return false }
|
||||
let isShift = NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false
|
||||
parent.onReturn(isShift)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(parent: self)
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> SearchNativeTextField {
|
||||
let field = SearchNativeTextField(frame: .zero)
|
||||
field.font = .systemFont(ofSize: NSFont.systemFontSize)
|
||||
field.placeholderString = String(localized: "search.placeholder", defaultValue: "Search")
|
||||
field.setAccessibilityIdentifier("TerminalFindSearchTextField")
|
||||
field.delegate = context.coordinator
|
||||
field.stringValue = text
|
||||
context.coordinator.parentField = field
|
||||
|
||||
// Observe .ghosttySearchFocus to immediately focus from AppKit level.
|
||||
// This is the primary mechanism for restoring focus after window switches.
|
||||
context.coordinator.searchFocusObserver = NotificationCenter.default.addObserver(
|
||||
forName: .ghosttySearchFocus,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak field, weak coordinator = context.coordinator] notification in
|
||||
guard let field, let coordinator else { return }
|
||||
guard let surface = notification.object as? TerminalSurface,
|
||||
surface.id == coordinator.parent.surfaceId else { return }
|
||||
guard let window = field.window else { return }
|
||||
// Don't re-focus if already first responder. makeFirstResponder on an
|
||||
// already-editing NSTextField ends the editing session and restarts it
|
||||
// with all text selected, causing typed characters to replace each other.
|
||||
let fr = window.firstResponder
|
||||
let alreadyFocused = fr === field ||
|
||||
field.currentEditor() != nil ||
|
||||
((fr as? NSTextView)?.delegate as? NSTextField) === field
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"find.nativeField.searchFocusNotification surface=\(coordinator.parent.surfaceId.uuidString.prefix(5)) " +
|
||||
"alreadyFocused=\(alreadyFocused) firstResponder=\(String(describing: fr))"
|
||||
)
|
||||
#endif
|
||||
guard !alreadyFocused else { return }
|
||||
let result = window.makeFirstResponder(field)
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"find.nativeField.searchFocusApply surface=\(coordinator.parent.surfaceId.uuidString.prefix(5)) " +
|
||||
"result=\(result ? 1 : 0) firstResponder=\(String(describing: window.firstResponder))"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: SearchNativeTextField, context: Context) {
|
||||
context.coordinator.parent = self
|
||||
context.coordinator.parentField = nsView
|
||||
|
||||
// Sync text from binding to field (skip during active IME composition)
|
||||
if let editor = nsView.currentEditor() as? NSTextView {
|
||||
if editor.string != text, !editor.hasMarkedText() {
|
||||
context.coordinator.isProgrammaticMutation = true
|
||||
editor.string = text
|
||||
nsView.stringValue = text
|
||||
context.coordinator.isProgrammaticMutation = false
|
||||
}
|
||||
} else if nsView.stringValue != text {
|
||||
nsView.stringValue = text
|
||||
}
|
||||
|
||||
// Sync focus from binding to AppKit
|
||||
if let window = nsView.window {
|
||||
let fr = window.firstResponder
|
||||
let isFirstResponder =
|
||||
fr === nsView ||
|
||||
nsView.currentEditor() != nil ||
|
||||
((fr as? NSTextView)?.delegate as? NSTextField) === nsView
|
||||
|
||||
if isFocused, !isFirstResponder, context.coordinator.pendingFocusRequest != true {
|
||||
context.coordinator.pendingFocusRequest = true
|
||||
DispatchQueue.main.async { [weak nsView, weak coordinator = context.coordinator] in
|
||||
coordinator?.pendingFocusRequest = nil
|
||||
guard let coordinator, coordinator.parent.isFocused else { return }
|
||||
guard let nsView, let window = nsView.window else { return }
|
||||
let fr = window.firstResponder
|
||||
let alreadyFocused = fr === nsView ||
|
||||
nsView.currentEditor() != nil ||
|
||||
((fr as? NSTextView)?.delegate as? NSTextField) === nsView
|
||||
guard !alreadyFocused else { return }
|
||||
window.makeFirstResponder(nsView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func dismantleNSView(_ nsView: SearchNativeTextField, coordinator: Coordinator) {
|
||||
if let observer = coordinator.searchFocusObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
coordinator.searchFocusObserver = nil
|
||||
}
|
||||
nsView.delegate = nil
|
||||
coordinator.parentField = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchButtonStyle: ButtonStyle {
|
||||
@State private var isHovered = false
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import Foundation
|
|||
import AppKit
|
||||
|
||||
struct GhosttyConfig {
|
||||
enum ColorSchemePreference {
|
||||
enum ColorSchemePreference: Hashable {
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
private static let loadCacheLock = NSLock()
|
||||
private static var cachedConfigsByColorScheme: [ColorSchemePreference: GhosttyConfig] = [:]
|
||||
|
||||
var fontFamily: String = "Menlo"
|
||||
var fontSize: CGFloat = 12
|
||||
var theme: String?
|
||||
|
|
@ -18,6 +21,7 @@ struct GhosttyConfig {
|
|||
|
||||
// Colors (from theme or config)
|
||||
var backgroundColor: NSColor = NSColor(hex: "#272822")!
|
||||
var backgroundOpacity: Double = 1.0
|
||||
var foregroundColor: NSColor = NSColor(hex: "#fdfff1")!
|
||||
var cursorColor: NSColor = NSColor(hex: "#c0c1b5")!
|
||||
var cursorTextColor: NSColor = NSColor(hex: "#8d8e82")!
|
||||
|
|
@ -45,7 +49,45 @@ struct GhosttyConfig {
|
|||
return backgroundColor.darken(by: isLightBackground ? 0.08 : 0.4)
|
||||
}
|
||||
|
||||
static func load() -> GhosttyConfig {
|
||||
static func load(
|
||||
preferredColorScheme: ColorSchemePreference? = nil,
|
||||
useCache: Bool = true,
|
||||
loadFromDisk: (_ preferredColorScheme: ColorSchemePreference) -> GhosttyConfig = Self.loadFromDisk
|
||||
) -> GhosttyConfig {
|
||||
let resolvedColorScheme = preferredColorScheme ?? currentColorSchemePreference()
|
||||
if useCache, let cached = cachedLoad(for: resolvedColorScheme) {
|
||||
return cached
|
||||
}
|
||||
|
||||
let loaded = loadFromDisk(resolvedColorScheme)
|
||||
if useCache {
|
||||
storeCachedLoad(loaded, for: resolvedColorScheme)
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
static func invalidateLoadCache() {
|
||||
loadCacheLock.lock()
|
||||
cachedConfigsByColorScheme.removeAll()
|
||||
loadCacheLock.unlock()
|
||||
}
|
||||
|
||||
private static func cachedLoad(for colorScheme: ColorSchemePreference) -> GhosttyConfig? {
|
||||
loadCacheLock.lock()
|
||||
defer { loadCacheLock.unlock() }
|
||||
return cachedConfigsByColorScheme[colorScheme]
|
||||
}
|
||||
|
||||
private static func storeCachedLoad(
|
||||
_ config: GhosttyConfig,
|
||||
for colorScheme: ColorSchemePreference
|
||||
) {
|
||||
loadCacheLock.lock()
|
||||
cachedConfigsByColorScheme[colorScheme] = config
|
||||
loadCacheLock.unlock()
|
||||
}
|
||||
|
||||
private static func loadFromDisk(preferredColorScheme: ColorSchemePreference) -> GhosttyConfig {
|
||||
var config = GhosttyConfig()
|
||||
|
||||
// Match Ghostty's default load order on macOS.
|
||||
|
|
@ -64,7 +106,12 @@ struct GhosttyConfig {
|
|||
|
||||
// Load theme if specified
|
||||
if let themeName = config.theme {
|
||||
config.loadTheme(themeName)
|
||||
config.loadTheme(
|
||||
themeName,
|
||||
environment: ProcessInfo.processInfo.environment,
|
||||
bundleResourceURL: Bundle.main.resourceURL,
|
||||
preferredColorScheme: preferredColorScheme
|
||||
)
|
||||
}
|
||||
|
||||
return config
|
||||
|
|
@ -102,6 +149,10 @@ struct GhosttyConfig {
|
|||
if let color = NSColor(hex: value) {
|
||||
backgroundColor = color
|
||||
}
|
||||
case "background-opacity":
|
||||
if let opacity = Double(value) {
|
||||
backgroundOpacity = opacity
|
||||
}
|
||||
case "foreground":
|
||||
if let color = NSColor(hex: value) {
|
||||
foregroundColor = color
|
||||
|
|
|
|||
67
Sources/KeyboardLayout.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import AppKit
|
||||
import Carbon
|
||||
|
||||
class KeyboardLayout {
|
||||
/// Return a string ID of the current keyboard input source.
|
||||
static var id: String? {
|
||||
if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
|
||||
let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) {
|
||||
let sourceId = Unmanaged<CFString>.fromOpaque(sourceIdPointer).takeUnretainedValue()
|
||||
return sourceId as String
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Translate a physical keyCode to the character AppKit would use for shortcut matching,
|
||||
/// preserving command-aware layouts such as "Dvorak - QWERTY Command".
|
||||
static func character(
|
||||
forKeyCode keyCode: UInt16,
|
||||
modifierFlags: NSEvent.ModifierFlags = []
|
||||
) -> String? {
|
||||
guard let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
|
||||
let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let layoutData = unsafeBitCast(layoutDataPointer, to: CFData.self)
|
||||
guard let bytes = CFDataGetBytePtr(layoutData) else { return nil }
|
||||
let keyboardLayout = UnsafeRawPointer(bytes).assumingMemoryBound(to: UCKeyboardLayout.self)
|
||||
|
||||
var deadKeyState: UInt32 = 0
|
||||
var chars = [UniChar](repeating: 0, count: 4)
|
||||
var length = 0
|
||||
|
||||
let status = UCKeyTranslate(
|
||||
keyboardLayout,
|
||||
keyCode,
|
||||
UInt16(kUCKeyActionDisplay),
|
||||
translationModifierKeyState(for: modifierFlags),
|
||||
UInt32(LMGetKbdType()),
|
||||
UInt32(kUCKeyTranslateNoDeadKeysBit),
|
||||
&deadKeyState,
|
||||
chars.count,
|
||||
&length,
|
||||
&chars
|
||||
)
|
||||
|
||||
guard status == noErr, length > 0 else { return nil }
|
||||
return String(utf16CodeUnits: chars, count: length).lowercased()
|
||||
}
|
||||
|
||||
private static func translationModifierKeyState(for modifierFlags: NSEvent.ModifierFlags) -> UInt32 {
|
||||
let normalized = modifierFlags
|
||||
.intersection(.deviceIndependentFlagsMask)
|
||||
.intersection([.shift, .command])
|
||||
|
||||
var carbonModifiers: Int = 0
|
||||
if normalized.contains(.shift) {
|
||||
carbonModifiers |= shiftKey
|
||||
}
|
||||
if normalized.contains(.command) {
|
||||
carbonModifiers |= cmdKey
|
||||
}
|
||||
|
||||
return UInt32((carbonModifiers >> 8) & 0xFF)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ enum KeyboardShortcutSettings {
|
|||
case toggleSidebar
|
||||
case newTab
|
||||
case newWindow
|
||||
case closeWindow
|
||||
case openFolder
|
||||
case sendFeedback
|
||||
case showNotifications
|
||||
case jumpToUnread
|
||||
case triggerFlash
|
||||
|
|
@ -17,7 +20,11 @@ enum KeyboardShortcutSettings {
|
|||
case prevSurface
|
||||
case nextSidebarTab
|
||||
case prevSidebarTab
|
||||
case renameTab
|
||||
case renameWorkspace
|
||||
case closeWorkspace
|
||||
case newSurface
|
||||
case toggleTerminalCopyMode
|
||||
|
||||
// Panes / splits
|
||||
case focusLeft
|
||||
|
|
@ -26,6 +33,7 @@ enum KeyboardShortcutSettings {
|
|||
case focusDown
|
||||
case splitRight
|
||||
case splitDown
|
||||
case toggleSplitZoom
|
||||
case splitBrowserRight
|
||||
case splitBrowserDown
|
||||
|
||||
|
|
@ -38,28 +46,36 @@ enum KeyboardShortcutSettings {
|
|||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .toggleSidebar: return "Toggle Sidebar"
|
||||
case .newTab: return "New Workspace"
|
||||
case .newWindow: return "New Window"
|
||||
case .showNotifications: return "Show Notifications"
|
||||
case .jumpToUnread: return "Jump to Latest Unread"
|
||||
case .triggerFlash: return "Flash Focused Panel"
|
||||
case .nextSurface: return "Next Surface"
|
||||
case .prevSurface: return "Previous Surface"
|
||||
case .nextSidebarTab: return "Next Workspace"
|
||||
case .prevSidebarTab: return "Previous Workspace"
|
||||
case .newSurface: return "New Surface"
|
||||
case .focusLeft: return "Focus Pane Left"
|
||||
case .focusRight: return "Focus Pane Right"
|
||||
case .focusUp: return "Focus Pane Up"
|
||||
case .focusDown: return "Focus Pane Down"
|
||||
case .splitRight: return "Split Right"
|
||||
case .splitDown: return "Split Down"
|
||||
case .splitBrowserRight: return "Split Browser Right"
|
||||
case .splitBrowserDown: return "Split Browser Down"
|
||||
case .openBrowser: return "Open Browser"
|
||||
case .toggleBrowserDeveloperTools: return "Toggle Browser Developer Tools"
|
||||
case .showBrowserJavaScriptConsole: return "Show Browser JavaScript Console"
|
||||
case .toggleSidebar: return String(localized: "shortcut.toggleSidebar.label", defaultValue: "Toggle Sidebar")
|
||||
case .newTab: return String(localized: "shortcut.newWorkspace.label", defaultValue: "New Workspace")
|
||||
case .newWindow: return String(localized: "shortcut.newWindow.label", defaultValue: "New Window")
|
||||
case .closeWindow: return String(localized: "shortcut.closeWindow.label", defaultValue: "Close Window")
|
||||
case .openFolder: return String(localized: "shortcut.openFolder.label", defaultValue: "Open Folder")
|
||||
case .sendFeedback: return String(localized: "sidebar.help.sendFeedback", defaultValue: "Send Feedback")
|
||||
case .showNotifications: return String(localized: "shortcut.showNotifications.label", defaultValue: "Show Notifications")
|
||||
case .jumpToUnread: return String(localized: "shortcut.jumpToUnread.label", defaultValue: "Jump to Latest Unread")
|
||||
case .triggerFlash: return String(localized: "shortcut.flashFocusedPanel.label", defaultValue: "Flash Focused Panel")
|
||||
case .nextSurface: return String(localized: "shortcut.nextSurface.label", defaultValue: "Next Surface")
|
||||
case .prevSurface: return String(localized: "shortcut.previousSurface.label", defaultValue: "Previous Surface")
|
||||
case .nextSidebarTab: return String(localized: "shortcut.nextWorkspace.label", defaultValue: "Next Workspace")
|
||||
case .prevSidebarTab: return String(localized: "shortcut.previousWorkspace.label", defaultValue: "Previous Workspace")
|
||||
case .renameTab: return String(localized: "shortcut.renameTab.label", defaultValue: "Rename Tab")
|
||||
case .renameWorkspace: return String(localized: "shortcut.renameWorkspace.label", defaultValue: "Rename Workspace")
|
||||
case .closeWorkspace: return String(localized: "shortcut.closeWorkspace.label", defaultValue: "Close Workspace")
|
||||
case .newSurface: return String(localized: "shortcut.newSurface.label", defaultValue: "New Surface")
|
||||
case .toggleTerminalCopyMode: return String(localized: "shortcut.toggleTerminalCopyMode.label", defaultValue: "Toggle Terminal Copy Mode")
|
||||
case .focusLeft: return String(localized: "shortcut.focusPaneLeft.label", defaultValue: "Focus Pane Left")
|
||||
case .focusRight: return String(localized: "shortcut.focusPaneRight.label", defaultValue: "Focus Pane Right")
|
||||
case .focusUp: return String(localized: "shortcut.focusPaneUp.label", defaultValue: "Focus Pane Up")
|
||||
case .focusDown: return String(localized: "shortcut.focusPaneDown.label", defaultValue: "Focus Pane Down")
|
||||
case .splitRight: return String(localized: "shortcut.splitRight.label", defaultValue: "Split Right")
|
||||
case .splitDown: return String(localized: "shortcut.splitDown.label", defaultValue: "Split Down")
|
||||
case .toggleSplitZoom: return String(localized: "shortcut.togglePaneZoom.label", defaultValue: "Toggle Pane Zoom")
|
||||
case .splitBrowserRight: return String(localized: "shortcut.splitBrowserRight.label", defaultValue: "Split Browser Right")
|
||||
case .splitBrowserDown: return String(localized: "shortcut.splitBrowserDown.label", defaultValue: "Split Browser Down")
|
||||
case .openBrowser: return String(localized: "shortcut.openBrowser.label", defaultValue: "Open Browser")
|
||||
case .toggleBrowserDeveloperTools: return String(localized: "shortcut.toggleBrowserDevTools.label", defaultValue: "Toggle Browser Developer Tools")
|
||||
case .showBrowserJavaScriptConsole: return String(localized: "shortcut.showBrowserJSConsole.label", defaultValue: "Show Browser JavaScript Console")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,22 +84,30 @@ enum KeyboardShortcutSettings {
|
|||
case .toggleSidebar: return "shortcut.toggleSidebar"
|
||||
case .newTab: return "shortcut.newTab"
|
||||
case .newWindow: return "shortcut.newWindow"
|
||||
case .closeWindow: return "shortcut.closeWindow"
|
||||
case .openFolder: return "shortcut.openFolder"
|
||||
case .sendFeedback: return "shortcut.sendFeedback"
|
||||
case .showNotifications: return "shortcut.showNotifications"
|
||||
case .jumpToUnread: return "shortcut.jumpToUnread"
|
||||
case .triggerFlash: return "shortcut.triggerFlash"
|
||||
case .nextSidebarTab: return "shortcut.nextSidebarTab"
|
||||
case .prevSidebarTab: return "shortcut.prevSidebarTab"
|
||||
case .renameTab: return "shortcut.renameTab"
|
||||
case .renameWorkspace: return "shortcut.renameWorkspace"
|
||||
case .closeWorkspace: return "shortcut.closeWorkspace"
|
||||
case .focusLeft: return "shortcut.focusLeft"
|
||||
case .focusRight: return "shortcut.focusRight"
|
||||
case .focusUp: return "shortcut.focusUp"
|
||||
case .focusDown: return "shortcut.focusDown"
|
||||
case .splitRight: return "shortcut.splitRight"
|
||||
case .splitDown: return "shortcut.splitDown"
|
||||
case .toggleSplitZoom: return "shortcut.toggleSplitZoom"
|
||||
case .splitBrowserRight: return "shortcut.splitBrowserRight"
|
||||
case .splitBrowserDown: return "shortcut.splitBrowserDown"
|
||||
case .nextSurface: return "shortcut.nextSurface"
|
||||
case .prevSurface: return "shortcut.prevSurface"
|
||||
case .newSurface: return "shortcut.newSurface"
|
||||
case .toggleTerminalCopyMode: return "shortcut.toggleTerminalCopyMode"
|
||||
case .openBrowser: return "shortcut.openBrowser"
|
||||
case .toggleBrowserDeveloperTools: return "shortcut.toggleBrowserDeveloperTools"
|
||||
case .showBrowserJavaScriptConsole: return "shortcut.showBrowserJavaScriptConsole"
|
||||
|
|
@ -98,6 +122,12 @@ enum KeyboardShortcutSettings {
|
|||
return StoredShortcut(key: "n", command: true, shift: false, option: false, control: false)
|
||||
case .newWindow:
|
||||
return StoredShortcut(key: "n", command: true, shift: true, option: false, control: false)
|
||||
case .closeWindow:
|
||||
return StoredShortcut(key: "w", command: true, shift: false, option: false, control: true)
|
||||
case .openFolder:
|
||||
return StoredShortcut(key: "o", command: true, shift: false, option: false, control: false)
|
||||
case .sendFeedback:
|
||||
return StoredShortcut(key: "f", command: true, shift: false, option: true, control: false)
|
||||
case .showNotifications:
|
||||
return StoredShortcut(key: "i", command: true, shift: false, option: false, control: false)
|
||||
case .jumpToUnread:
|
||||
|
|
@ -108,6 +138,12 @@ enum KeyboardShortcutSettings {
|
|||
return StoredShortcut(key: "]", command: true, shift: false, option: false, control: true)
|
||||
case .prevSidebarTab:
|
||||
return StoredShortcut(key: "[", command: true, shift: false, option: false, control: true)
|
||||
case .renameTab:
|
||||
return StoredShortcut(key: "r", command: true, shift: false, option: false, control: false)
|
||||
case .renameWorkspace:
|
||||
return StoredShortcut(key: "r", command: true, shift: true, option: false, control: false)
|
||||
case .closeWorkspace:
|
||||
return StoredShortcut(key: "w", command: true, shift: true, option: false, control: false)
|
||||
case .focusLeft:
|
||||
return StoredShortcut(key: "←", command: true, shift: false, option: true, control: false)
|
||||
case .focusRight:
|
||||
|
|
@ -120,6 +156,8 @@ enum KeyboardShortcutSettings {
|
|||
return StoredShortcut(key: "d", command: true, shift: false, option: false, control: false)
|
||||
case .splitDown:
|
||||
return StoredShortcut(key: "d", command: true, shift: true, option: false, control: false)
|
||||
case .toggleSplitZoom:
|
||||
return StoredShortcut(key: "\r", command: true, shift: true, option: false, control: false)
|
||||
case .splitBrowserRight:
|
||||
return StoredShortcut(key: "d", command: true, shift: false, option: true, control: false)
|
||||
case .splitBrowserDown:
|
||||
|
|
@ -130,6 +168,8 @@ enum KeyboardShortcutSettings {
|
|||
return StoredShortcut(key: "[", command: true, shift: true, option: false, control: false)
|
||||
case .newSurface:
|
||||
return StoredShortcut(key: "t", command: true, shift: false, option: false, control: false)
|
||||
case .toggleTerminalCopyMode:
|
||||
return StoredShortcut(key: "m", command: true, shift: true, option: false, control: false)
|
||||
case .openBrowser:
|
||||
return StoredShortcut(key: "l", command: true, shift: true, option: false, control: false)
|
||||
case .toggleBrowserDeveloperTools:
|
||||
|
|
@ -190,6 +230,8 @@ enum KeyboardShortcutSettings {
|
|||
|
||||
static func nextSidebarTabShortcut() -> StoredShortcut { shortcut(for: .nextSidebarTab) }
|
||||
static func prevSidebarTabShortcut() -> StoredShortcut { shortcut(for: .prevSidebarTab) }
|
||||
static func renameWorkspaceShortcut() -> StoredShortcut { shortcut(for: .renameWorkspace) }
|
||||
static func closeWorkspaceShortcut() -> StoredShortcut { shortcut(for: .closeWorkspace) }
|
||||
|
||||
static func focusLeftShortcut() -> StoredShortcut { shortcut(for: .focusLeft) }
|
||||
static func focusRightShortcut() -> StoredShortcut { shortcut(for: .focusRight) }
|
||||
|
|
@ -198,6 +240,7 @@ enum KeyboardShortcutSettings {
|
|||
|
||||
static func splitRightShortcut() -> StoredShortcut { shortcut(for: .splitRight) }
|
||||
static func splitDownShortcut() -> StoredShortcut { shortcut(for: .splitDown) }
|
||||
static func toggleSplitZoomShortcut() -> StoredShortcut { shortcut(for: .toggleSplitZoom) }
|
||||
static func splitBrowserRightShortcut() -> StoredShortcut { shortcut(for: .splitBrowserRight) }
|
||||
static func splitBrowserDownShortcut() -> StoredShortcut { shortcut(for: .splitBrowserDown) }
|
||||
|
||||
|
|
@ -228,6 +271,8 @@ struct StoredShortcut: Codable, Equatable {
|
|||
switch key {
|
||||
case "\t":
|
||||
keyText = "TAB"
|
||||
case "\r":
|
||||
keyText = "↩"
|
||||
default:
|
||||
keyText = key.uppercased()
|
||||
}
|
||||
|
|
@ -244,6 +289,69 @@ struct StoredShortcut: Codable, Equatable {
|
|||
return flags
|
||||
}
|
||||
|
||||
var keyEquivalent: KeyEquivalent? {
|
||||
switch key {
|
||||
case "←":
|
||||
return .leftArrow
|
||||
case "→":
|
||||
return .rightArrow
|
||||
case "↑":
|
||||
return .upArrow
|
||||
case "↓":
|
||||
return .downArrow
|
||||
case "\t":
|
||||
return .tab
|
||||
case "\r":
|
||||
return KeyEquivalent(Character("\r"))
|
||||
default:
|
||||
let lowered = key.lowercased()
|
||||
guard lowered.count == 1, let character = lowered.first else { return nil }
|
||||
return KeyEquivalent(character)
|
||||
}
|
||||
}
|
||||
|
||||
var eventModifiers: EventModifiers {
|
||||
var modifiers: EventModifiers = []
|
||||
if command {
|
||||
modifiers.insert(.command)
|
||||
}
|
||||
if shift {
|
||||
modifiers.insert(.shift)
|
||||
}
|
||||
if option {
|
||||
modifiers.insert(.option)
|
||||
}
|
||||
if control {
|
||||
modifiers.insert(.control)
|
||||
}
|
||||
return modifiers
|
||||
}
|
||||
|
||||
var menuItemKeyEquivalent: String? {
|
||||
switch key {
|
||||
case "←":
|
||||
guard let scalar = UnicodeScalar(NSLeftArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "→":
|
||||
guard let scalar = UnicodeScalar(NSRightArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "↑":
|
||||
guard let scalar = UnicodeScalar(NSUpArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "↓":
|
||||
guard let scalar = UnicodeScalar(NSDownArrowFunctionKey) else { return nil }
|
||||
return String(Character(scalar))
|
||||
case "\t":
|
||||
return "\t"
|
||||
case "\r":
|
||||
return "\r"
|
||||
default:
|
||||
let lowered = key.lowercased()
|
||||
guard lowered.count == 1 else { return nil }
|
||||
return lowered
|
||||
}
|
||||
}
|
||||
|
||||
static func from(event: NSEvent) -> StoredShortcut? {
|
||||
guard let key = storedKey(from: event) else { return nil }
|
||||
|
||||
|
|
@ -274,6 +382,7 @@ struct StoredShortcut: Codable, Equatable {
|
|||
case 125: return "↓" // down arrow
|
||||
case 126: return "↑" // up arrow
|
||||
case 48: return "\t" // tab
|
||||
case 36, 76: return "\r" // return, keypad enter
|
||||
case 33: return "[" // kVK_ANSI_LeftBracket
|
||||
case 30: return "]" // kVK_ANSI_RightBracket
|
||||
case 27: return "-" // kVK_ANSI_Minus
|
||||
|
|
@ -370,7 +479,7 @@ private class ShortcutRecorderNSButton: NSButton {
|
|||
|
||||
func updateTitle() {
|
||||
if isRecording {
|
||||
title = "Press shortcut…"
|
||||
title = String(localized: "shortcut.pressShortcut.prompt", defaultValue: "Press shortcut…")
|
||||
} else {
|
||||
title = shortcut.displayString
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Bonsplit
|
||||
import SwiftUI
|
||||
|
||||
struct NotificationsPage: View {
|
||||
|
|
@ -5,6 +6,7 @@ struct NotificationsPage: View {
|
|||
@EnvironmentObject var tabManager: TabManager
|
||||
@Binding var selection: SidebarSelection
|
||||
@FocusState private var focusedNotificationId: UUID?
|
||||
@AppStorage(KeyboardShortcutSettings.Action.jumpToUnread.defaultsKey) private var jumpToUnreadShortcutData = Data()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -66,14 +68,16 @@ struct NotificationsPage: View {
|
|||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Text("Notifications")
|
||||
Text(String(localized: "notifications.title", defaultValue: "Notifications"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !notificationStore.notifications.isEmpty {
|
||||
Button("Clear All") {
|
||||
jumpToUnreadButton
|
||||
|
||||
Button(String(localized: "notifications.clearAll", defaultValue: "Clear All")) {
|
||||
notificationStore.clearAll()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -88,20 +92,95 @@ struct NotificationsPage: View {
|
|||
Image(systemName: "bell.slash")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.secondary)
|
||||
Text("No notifications yet")
|
||||
Text(String(localized: "notifications.empty.title", defaultValue: "No notifications yet"))
|
||||
.font(.headline)
|
||||
Text("Desktop notifications will appear here for quick review.")
|
||||
Text(String(localized: "notifications.empty.description", defaultValue: "Desktop notifications will appear here for quick review."))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var jumpToUnreadButton: some View {
|
||||
if let key = jumpToUnreadShortcut.keyEquivalent {
|
||||
Button(action: {
|
||||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))
|
||||
ShortcutAnnotation(text: jumpToUnreadShortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.keyboardShortcut(key, modifiers: jumpToUnreadShortcut.eventModifiers)
|
||||
.safeHelp(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread")))
|
||||
.disabled(!hasUnreadNotifications)
|
||||
} else {
|
||||
Button(action: {
|
||||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))
|
||||
ShortcutAnnotation(text: jumpToUnreadShortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.safeHelp(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread")))
|
||||
.disabled(!hasUnreadNotifications)
|
||||
}
|
||||
}
|
||||
|
||||
private var jumpToUnreadShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: jumpToUnreadShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.jumpToUnread.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var hasUnreadNotifications: Bool {
|
||||
notificationStore.notifications.contains(where: { !$0.isRead })
|
||||
}
|
||||
|
||||
private func decodeShortcut(from data: Data, fallback: StoredShortcut) -> StoredShortcut {
|
||||
guard !data.isEmpty,
|
||||
let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else {
|
||||
return fallback
|
||||
}
|
||||
return shortcut
|
||||
}
|
||||
|
||||
private func tabTitle(for tabId: UUID) -> String? {
|
||||
AppDelegate.shared?.tabTitle(for: tabId) ?? tabManager.tabs.first(where: { $0.id == tabId })?.title
|
||||
}
|
||||
}
|
||||
|
||||
struct ShortcutAnnotation: View {
|
||||
let text: String
|
||||
var accessibilityIdentifier: String? = nil
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
if let accessibilityIdentifier {
|
||||
badge.accessibilityIdentifier(accessibilityIdentifier)
|
||||
} else {
|
||||
badge
|
||||
}
|
||||
}
|
||||
|
||||
private var badge: some View {
|
||||
Text(text)
|
||||
.font(.system(size: 10, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.fill(Color(nsColor: .controlBackgroundColor))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationRow: View {
|
||||
let notification: TerminalNotification
|
||||
let tabTitle: String?
|
||||
|
|
@ -114,11 +193,11 @@ private struct NotificationRow: View {
|
|||
Button(action: onOpen) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Circle()
|
||||
.fill(notification.isRead ? Color.clear : Color.accentColor)
|
||||
.fill(notification.isRead ? Color.clear : cmuxAccentColor())
|
||||
.frame(width: 8, height: 8)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.accentColor.opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
||||
.stroke(cmuxAccentColor().opacity(notification.isRead ? 0.2 : 1), lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 6)
|
||||
|
||||
|
|
|
|||